
Threads
- 18 installs
- 253 repo stars
- Updated August 4, 2026
- majiayu000/spellbook
Helps with ai & agent building tasks during AI-assisted development.
About
threads is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- threads
- AI & Agent Building
- AI-coding skill
Threads by the numbers
- 18 all-time installs (skills.sh)
- Ranked #10,736 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/majiayu000/spellbook --skill threadsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 253 |
| Last updated | August 4, 2026 |
| Repository | majiayu000/spellbook ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Threads
Use this skill to turn a broad request into controlled Codex-native subthreads with explicit lanes, file ownership, review gates, and verifiable closure.
Native Codex threads are short-lived parallel work lines inside the Codex workflow. They are not the same as OMX/tmux workers. If native subagent tools are not visible, discover them with tool search. If no native subagent capability is available, produce the thread prompt pack and execution plan instead of pretending threads were launched.
Do Not Use For
Do not use this skill for generic uses of "thread" unless the user explicitly means Codex workflow orchestration:
- operating-system threads, language concurrency, or async programming models
- chat, email, forum, Slack, GitHub discussion, or comment threads
- OpenAI Assistants API threads or other product APIs
- ordinary single-agent repo work where there are no independent lanes
Decision
Choose one mode:
- single_agent: handle a small, well-scoped task locally with the same evidence gates.
- plan_only: map issues, PRs, risks, and parallelization without edits.
- execute_direct: run one or more bounded implementation lanes after planning.
- review_only: launch independent reviewers for PRs, diffs, or risky code.
- research_spec: split exploration by angle, then synthesize docs/spec/issues.
- clarify_first: ask only when repo, target queue, permission, or done-when is missing.
Use single_agent only for one-file fixes, simple questions, or tasks where the next step depends on one immediate result. If the user explicitly asks for threads, subagents, or a GitHub issue/PR queue and native subagents are available, do not silently choose single_agent; record a concrete no_spawn_reason before implementation work begins.
For any implementation mode, start with a lane map before spawning workers. For GitHub issue/PR queues, complete the Capability Gate and Queue Gate first; do not create worker lanes until capability_gate, queue_gate, queue_ledger, and issue_to_pr_map are written.
Explicit Thread Dispatch Gate
When the user explicitly asks for threads, subagents, "开几个子 agent", or a GitHub issue/PR queue that the skill classifies as plan_only, execute_direct, review_only, or research_spec, native dispatch is required whenever native subagent tools are available.
Record this gate before implementation, review, or merge work:
thread_dispatch_gate:
- explicit_thread_request: yes | no
- native_subagents: available | unavailable
- spawn_requirement: required | optional | unavailable
- fallback_mode: single_agent | prompt_pack_only | none
- planned_native_threads:
- id:
role:
target:
write_scope: read_only | disjoint_writable | none
spawn_status: planned | spawned | skipped
no_spawn_reason:
- native_thread_evidence:
user_requested_native_threads: yes | no
spawned_agents:
- lane_id:
spawn_tool:
agent_id_or_thread_id:
wait_evidence:
close_evidence:
result_collected: yes | no
fallback_reason:
- no_spawn_reason:Rules:
- If
explicit_thread_request: yes,native_subagents: available, andspawn_requirement: required, spawn at least one bounded native subagent before claiming the run is using threads. native_subagents: availableplusfallback_mode: noneis valid only whennative_thread_evidence.spawned_agentscontains at least one real agent/thread ID.- Every
planned_native_threadslane must have a matchingnative_thread_evidence.spawned_agents[].lane_idor a lane-levelno_spawn_reason; spawning one native thread does not justify running the remaining planned lanes serially. - A main-thread lane is a coordinator lane, not a native thread. Do not count the coordinator as
native_thread_evidence.spawned_agents. - If no native thread is spawned, set
fallback_mode: single_agentand writeno_spawn_reasonbefore editing files, commenting on GitHub, or merging. Valid reasons are narrow: task is tiny and truly sequential, all possible writable lanes overlap, native tools are unavailable, or the user explicitly asks not to spawn. - For GitHub PR merge work, at least one read-only reviewer or merge-reviewer native thread is required when native subagents are available. A self-review by the coordinator does not satisfy the independent review lane.
- If native tools are unavailable, produce a prompt pack or continue as
single_agentonly after saying that no native threads were launched.
Operating Contract
Before dispatch, record this block as intent_contract:
intent_contract:
goal:
non_goals:
done_when:
merge_policy: no_merge | merge_after_gate | user_confirm_before_merge
remote_truth_required: yes | no
truth_level: A | B | C | D
queue_ledger: required_for_queue | optional | none
ci_truth_source: discovered_workflow | user_supplied | language_default | none
data_collection: final_report | local_jsonl | none
queue_bounds:
max_items:
time_budget:
queue_tranche:
remote_refresh:
cadence:
last_fetch:
stale_base_policy:
thread_dispatch:
explicit_thread_request:
spawn_requirement:
no_spawn_reason:Defaults:
merge_policyisno_mergeunless the user explicitly authorizes merging in the current conversation.data_collectionisfinal_reportunless the user requests durable logging, debug data collection, or an issue/PR queue run explicitly setslocal_jsonl.- If merge permission is ambiguous, stop after merge review and report the exact recommendation or merge command instead of merging.
Direct actions: inspect repo instructions, fetch remote state, map lanes, apply the Explicit Thread Dispatch Gate, spawn required bounded native subagents, integrate results, verify, and report closure.
Escalate before: modifying high-context files, merging without fresh CI/review-thread truth, sharing writable files across workers, or switching to shell/tmux/OMX orchestration.
Evidence-backed pushback: choose single_agent only when parallelism adds coordination risk without independent work, and record the no_spawn_reason; challenge vague worker output, stale remote state, or unverified completion claims.
Feedback loop: record notable failures in threads_run_log, classify the failure mode, tighten the lane prompt or split, then retry only after the hypothesis changes.
If the user asks for issue/PR queue handling, remote_truth_required is yes and queue_ledger is required_for_queue.
Broad queue requests such as "all issues and PRs" are bounded by default. If the user did not give an explicit long-run budget, choose one smallest mergeable tranche, record max_items / time_budget / queue_tranche, and leave the remaining queue for the next run with exact next actions.
Capability Gate
Before dispatching lanes, record whether native Codex subagents are actually available:
capability_gate:
- native_subagents: available | unavailable
- tools_seen:
- explicit_thread_request: yes | no
- spawn_requirement: required | optional | unavailable
- fallback_mode: single_agent | prompt_pack_only | none
- no_spawn_reason:
- manual_orchestration_allowed: yes | noRules:
- If native subagents are unavailable, do not claim threads were launched.
- If native subagents are available and
spawn_requirementisrequired, do not proceed past planning until at least one native subagent is spawned orfallback_modeandno_spawn_reasonare recorded. - Do not switch to shell, tmux, OMX, Harness, or other manual orchestration unless the user explicitly asks for that fallback.
- If
fallback_modeissingle_agent, explain why parallelism was rejected. - If
fallback_modeisprompt_pack_only, output exact lane prompts and stop before implementation.
Queue Gate
For GitHub issue/PR queue handling, write a queue_gate block before the lane map and before any implementation worker is launched. This is mandatory even when all open PRs look MERGEABLE or CLEAN.
The gate must use live state from the current session:
queue_gate:
- fetched_remote:
- truth_level:
- remote_refresh:
base_ref:
origin_main_sha:
local_base_sha:
stale_base:
policy:
- current_branch:
- dirty_files:
- unpushed_commits:
- worktrees:
- open_prs:
- open_issues:
- pr_classification:
- PR:
head_sha:
merge_state:
check_rollup:
review_threads:
classification:
reason:
- issue_to_pr_map:
- issue:
covering_pr:
status: covered | uncovered | stale_or_superseded | needs_human_decision
reason:
- recommended_order:
- stop_conditions:Classify every open PR as exactly one of:
merge_readyreview_thread_blockedci_failedconflict_blockedstale_or_supersededneeds_human_decision
Rules:
MERGEABLEorCLEANis never sufficient by itself. A PR ismerge_readyonly when the current head SHA, check rollup, merge state, and GraphQL review-thread state are all fresh and clean.- Query review threads with a thread-aware source such as GraphQL
reviewThreads { isResolved isOutdated }; flat PR comments are not sufficient. - Map open issues to existing PRs before opening new implementation lanes. Prefer fixing, reviewing, or merging an existing covering PR over opening a competing PR.
- For review-gated queues, work one blocker or bounded tranche to closure unless writable file ownership is clearly disjoint and the PRs are not stacked.
- Keep remote truth separate from local stale or dirty worktree state.
Remote Truth Levels
Use the highest truth level available from the current session and record it in intent_contract, queue_gate, and threads_run_log:
- A:
git fetchplus GitHub API or GraphQL can prove current PR head, check rollup, merge state, and review-thread state. Implementation, review, and merge gates may proceed if all other conditions pass. - B:
git fetchplus REST PR/review/comment data is available, but GraphQL review-thread state is unavailable. Implementation and review may proceed; merge is forbidden. - C: only local git state is reliable. Local implementation and review may proceed; PR closure, merge readiness, and remote cleanup claims are forbidden.
- D: no reliable repo or remote state is available. Use
plan_onlyorprompt_pack_only; do not implement, push, merge, or claim closure.
Never fabricate remote state to reach a higher level. If the tool or permission gap matters, report the lower level and the blocked operation.
Queue Ledger
For issue/PR queues, keep a live queue ledger from discovery through final closure. The ledger can be a concise table in the conversation, a local durable log, or both, but it must survive handoff and compaction when the run is long.
Use these fields:
queue_ledger:
- item:
type: issue | pr | review_thread | local_task
remote_state:
owner_lane:
dependencies:
base_ref:
branch:
worktree:
writable_files:
pr:
head_sha:
ci_status:
review_thread_state:
acceptance_evidence:
merge_sha:
closed_by:
remote_checked_at:Rules:
- Update the ledger after initial remote discovery, after queue gate classification, after each PR open/update, before merge, after merge/close, and before the final report.
- Keep dependency edges explicit. If a lane depends on another PR or a newer
origin/main, rebase or recreate the lane only after recording the dependency and checking for changed files. - Do not claim
Fixes #...or close an issue until each meaningful acceptance point is mapped to evidence: changed files, tests, commands, PR, commit, or remote state. - If a queue item is superseded by another PR or issue, record the superseding item instead of silently dropping it.
Remote Refresh
Long queue runs must refresh remote state without mutating worker worktrees:
- Run
git fetch --prune originat queue start, before opening a new lane, before pushing, before merge review, and after long waits such as CI polling. For runs longer than one focused tranche, refresh at least every 20-30 minutes. - Compare the current
origin/mainSHA with each lane's recordedbase_ref. Do not automatically merge or rebase during a lane. - If
origin/mainadvanced, recordstale_base: yesinqueue_gate,queue_ledger, andthreads_run_log. - Continue without rebase only when changed upstream files are disjoint from the lane's writable files and verification remains meaningful.
- Rebase, recreate the worktree, or stop with
stale_remote_statewhen upstream changes overlap the lane, alter CI, or invalidate the acceptance evidence. - Remote refresh is not required for tiny
single_agenttasks unless the task touches GitHub remote state.
Lane Map
Write a short lane map before dispatch:
mode:
repo:
base_ref:
global_constraints:
verification_owner:
stop_conditions:
lanes:
- id:
role: planner | worker | reviewer | merge_reviewer | researcher
target:
depends_on:
execution_mode: parallel | serial_after_dependency | read_only_until_dependency
base_gate:
worktree:
writable_files:
forbidden_files:
exclusive_verification:
verification_scope: inspection_only | targeted | full_local | ci_only
expected_output:
verification:
native_thread_id:
no_spawn_reason:Rules:
- Search first: inspect repo state, open issues/PRs, current branch, dirty files, and applicable instructions before assigning work.
- For GitHub queues, the lane map must be based on the preceding
queue_gate; no worker lane may start from open issue/PR lists alone. - Build a dependency graph before spawning writable workers. Lanes with no dependency edge and disjoint writable files may run in parallel; lanes with dependency edges must run serially after their upstream lane is stable.
- For stacked PRs or issue chains, set downstream writable lanes to
read_only_until_dependencyuntil the upstream head is pushed and thebase_gateis satisfied. Downstream planners may inspect and prepare a patch plan in parallel, but they must not edit files before the gate opens. base_gatemust name the upstream condition that makes a downstream writable lane safe to start: current head SHA recorded, upstream CI/targeted verification status known, dirty upstream worktree absent or committed, and overlapping files checked.- Keep planners and reviewers read-only.
- Mark coordinator-only lanes with
native_thread_id: none; every spawned lane must record the returned native tool agent ID. - Give implementation workers disjoint writable paths. Never assign two workers the same writable file.
- Do not start two writable workers when either lane depends on the other's branch, PR, generated output, or verification result. Dependency means serial execution even if the writable file lists appear disjoint.
- Put high-context files such as
AGENTS.md,CLAUDE.md, settings, hooks, and setup scripts inforbidden_filesunless the user explicitly asks to modify them. - Prefer existing worktrees when they are already tied to the target branch. Otherwise create clean worktrees from
origin/mainor the requested base. - Prefer creating downstream worktrees only after the upstream
base_gateopens. Avoid starting writable downstream work on a stale base; if early exploration is useful, use a read-only planner lane instead. - Commands that mutate shared state such as
.git/hooks, shared$HOMEfiles, global caches, local daemons, or repo-level generated state belong toverification_ownerand must not run in parallel lanes unless that mutable state is isolated. - Require fresh verification from the worker or the verification owner before claiming success.
- For GitHub queues, treat comments and review threads as first-class remote state; open PR/issue lists alone are not enough.
- Default WIP limit: at most 3 planning/research lanes, 2 concurrent writable implementation lanes, and 2 reviewers per PR unless the user explicitly grants a larger budget.
Dependency-Aware Dispatch
Choose parallelism on demand after the lane map, not by default.
Use parallel writable workers only when all are true:
- no
depends_onedge exists between the lanes - writable files and generated outputs are disjoint
- verification does not mutate shared state
- neither lane's branch, PR, or acceptance evidence depends on the other's result
Use serial writable workers when any lane depends on another lane's branch, PR, generated output, CI result, review result, schema migration, shared setup/install behavior, or version bump. In that case, run the upstream writable lane first, record its head SHA and verification, then create or rebase the downstream worktree from that stable head.
Use parallel read-only planners/reviewers for dependent work only when they do not edit files or mutate GitHub state. Their output should be a patch plan, risk map, or review findings that can be applied after the upstream gate opens.
For stacked PRs:
- Model the stack as a serial pipeline, not a parallel writable queue.
- Do not spawn a downstream writable worker until the upstream branch has a recorded stable head and any required CI or targeted verification has completed or been deliberately deferred.
- If the upstream head changes while a downstream worker has uncommitted changes, stop that downstream lane and require a handoff artifact: committed WIP SHA, patch file, or explicit abandon/recreate decision.
- The coordinator must not edit a worker-owned dirty downstream worktree. Recreate from the stable upstream head and apply a reviewed patch when possible.
- Record
stale_baseand the recovery decision inqueue_ledgerandthreads_run_log.
Verification Budget
Use verification_scope to keep review evidence useful without duplicating expensive full-suite work:
inspection_only: read-only diff/code inspection. Cheap static checks such asgit diff --checkare allowed when they are relevant.targeted: focused tests or linters for touched behavior only.full_local: one owner runs the project-wide local suite for the tranche, usually the root orchestrator,verification_owner, or amerge_reviewer.ci_only: fresh CI tied to the current head SHA is the full-suite truth source.
Rules:
- Assign at most one full-suite owner per tranche. If fresh CI is the full-suite truth source, do not also ask every reviewer to repeat the local full suite.
- Reviewer lanes default to
inspection_onlyortargeted. They must not run full project test suites unless the lane map explicitly names them asverification_ownerormerge_reviewer. - Targeted reviewer checks should be command-valid for the language/tool. For Cargo, pass one test filter per
cargo testcommand, or use a broader module/path filter; do not pass several unrelated test names as positional filters in one command. - If a targeted check touches shared state, global caches, local daemons, or repo-level generated state, move it to the serialized
verification_ownerlane.
Dispatch
Use native subagents when available. If the multi-agent tool is not loaded, search for it using tool discovery. Do not use shell/tmux/OMX orchestration unless explicitly requested.
When multi_agent_v1 tools are available, use spawn_agent for required bounded sidecar lanes, wait_agent only when the next critical-path step needs that result, and close_agent after collecting completed output. Keep immediate blockers in the main thread, but do not count the main thread as a spawned native thread.
Close completed subagents as soon as their evidence has been collected. For long issue/PR queues, finish a bounded tranche, record the ledger and resume query, and consider starting a fresh parent thread instead of carrying oversized context forward.
Use these lane types:
- Planner: read issues/PRs/code and output dependency graph, worktree plan, file ownership, and risk.
- Worker: implement the smallest mergeable slice in one worktree; do not merge.
- Reviewer: inspect one PR/diff/worktree read-only; return findings first.
- Fix Worker: address concrete reviewer findings in the original worker worktree.
- Merge Reviewer: independently verify the final head and CI before merge.
- Closure Auditor: read remote truth after merge or close; verify issue/PR state, review threads, comments, branch cleanup, and local stale state.
- Researcher: inspect one external/source angle and return evidence with uncertainty.
Load prompt-patterns.md when you need ready-to-use prompts for planners, workers, reviewers, or research lanes.
Every lane output must be evidence-bearing:
lane:
root_cause_or_claim:
files_read:
files_changed:
unauthorized_or_unassigned_changes:
commands_run:
head_sha_or_artifact:
native_thread_id:
blockers:Merge Gate
Do not merge from worker output alone. Merge only after:
merge_policyismerge_after_gateoruser_confirm_before_mergewith explicit authorization from the current conversation.truth_levelisA; lower truth levels may produce recommendations but must not merge.- The PR/diff has at least one independent review lane.
- When native subagents are available, the independent review lane must be a spawned native thread with a recorded tool agent ID.
- Blocking findings are fixed or explicitly ruled out with evidence.
- Required checks are fresh and tied to the current head SHA.
- Current merge state is clean.
MERGEABLE,CLEAN, or a green check alone is not sufficient without the matching current head SHA, full check rollup, merge state, and GraphQL review-thread state. - GitHub review-thread state is checked with a thread-aware source such as GraphQL
reviewThreads { isResolved isOutdated }; flat PR comments are not sufficient. - The PR has no unresolved actionable review threads, and any fixed review feedback has an explicit reply or resolved thread unless the user forbids GitHub writes.
- Check review threads after PR creation/update, after CI completes, and immediately before merge. If auto-review can arrive after marking a draft ready or after CI finishes, wait 60-120 seconds and re-check once.
- Stop with
REVIEW_LOOPafter two repeated fix/review cycles on the same class of review-thread finding unless the hypothesis changes. - Use a bounded CI wait. After one complete CI cycle or the configured wait budget, stop with
WAITING_CIwhen there is no actionable local failure. Report PR number, head SHA, pending checks, last observed status, and the exact resume query. - Run a final remote refresh before merge review. If
origin/mainadvanced and overlaps the PR scope, stop withstale_remote_stateuntil the branch is rebased or recreated. - The final answer can state exact PR numbers, commits, changed files, and verification commands.
If the user asked for “review then merge,” the merge reviewer should be a separate lane from the implementation worker.
Run Log
For non-trivial runs, include a compact threads_run_log block in the final report. Append the same JSON object locally with scripts/append_run_log.py only when data_collection: local_jsonl, the user requests durable logging, or debug collection is explicitly enabled. Read run-log.md before writing durable logs.
Run logs are observational. Do not record secrets, credentials, full prompts, or private user data. Prefer short summaries, file paths, PR/issue numbers, command names, failure codes, and verification outcomes.
Final Report
End with a compact status table:
completed:
- lane:
result:
artifact:
verification:
merged:
- PR:
commit:
remaining:
- blocker_or_risk:
next_action:
remote_truth:
- open_prs:
- open_issues:
- checked_pr_heads:
- checked_review_threads:
- checked_ci:
- origin_main_sha:
- stale_base:
- remote_refreshes:
local_state:
- dirty_worktree:
- stale_worktree:
- high_context_file:
threads_run_log:
- mode:
- native_subagents:
- explicit_thread_request:
- spawn_requirement:
- native_thread_evidence:
spawned_agents:
- no_spawn_reason:
- truth_level:
- lanes_total:
- queue_items_total:
- queue_bounds:
queue_tranche:
- failure_codes:
- verification:
fresh:
- remote_closure:
checked:
- log_path:Separate remote truth from local machine state in all GitHub queue final reports. State when a branch is merged remotely but local main is stale, dirty, diverged, or a worktree branch is no longer tied to an open remote branch.
For GitHub queue work, include remote closure fields:
remote_closure:
- open_prs:
- open_issues:
- touched_pr_unresolved_review_threads:
- touched_pr_unanswered_review_comments:
- historical_unresolved_review_threads:
- deleted_remote_branches:
- local_cleanup_left:Gotchas and Failure Rules
- If a subthread returns vague output, ask for evidence or redo that lane with a stricter prompt.
- If a worker touches unassigned files, stop that lane and audit before proceeding.
- If three attempts fail on the same problem, stop and challenge the hypothesis or split the issue differently.
- If a hook/UI status looks stuck, verify process/log evidence before calling the task stuck.
- Classify failures as specification/system design, inter-agent misalignment, or verification/termination before retrying.
- If long-running remote state changes underneath a lane, record
stale_remote_stateand refresh/rebase only through an explicit gate; do not silently continue on a stale base. - If no native subagent capability is available, return the lane map and exact prompts so the user can launch them manually.
interface:
display_name: "Threads"
short_description: "Coordinate native Codex threads with evidence gates and run logs"
default_prompt: "Use $threads only for explicit Codex-native subagent orchestration or repo queues with independent lanes. When the user explicitly asks for threads and native subagents are available, spawn at least one bounded native subagent or record a structured no_spawn_reason before implementation. Require intent contracts, ownership, native_thread_evidence, review gates, merge permission, truth levels, and evidence-backed closure."
Prompt Patterns
Use these templates as raw material. Fill concrete repo paths, PR numbers, issue numbers, file ownership, and verification commands before dispatch.
Root Orchestrator
你看下这个库有哪些 issue 和 PR 应该怎么处理。
使用 Codex-native threads 分别作 plan、impl+PR提交、review+修改、merge reviewer。
先做完整规划,判断哪些可以并行,能并行的使用独立 worktree。
目标是做完整闭环:不合理的 issue/PR 可以建议关闭;合理的要实现、review、修复、验证;只有用户明确授权且 merge gate 通过时才 merge,默认停在 recommendation。
硬约束:
- 先查 repo 指令、git 状态、open issues、open PRs、CI、dirty worktree。
- 只有用户明确要求 Codex-native threads / 子 agent 编排,或 GitHub issue/PR queue 确实需要独立 lanes 时才使用本流程;不要用于 OS/language/chat/email/forum/API threads。
- 先写 capability_gate:native_subagents / tools_seen / explicit_thread_request / spawn_requirement / fallback_mode / no_spawn_reason / manual_orchestration_allowed;不能在 native subagents 不可用时声称开了 threads。
- 用户明确要求 threads/subagents 且 native_subagents=available 时,必须先 spawn 至少一个 native subagent,或在任何实现/评论/merge 前记录 `fallback_mode: single_agent` 和结构化 `no_spawn_reason`。
- `native_subagents: available` + `fallback_mode: none` 只有在 `native_thread_evidence.spawned_agents` 里有真实 agent/thread ID 时才成立;main coordinator 不能算 spawned thread。
- GitHub issue/PR queue 必须先输出 queue_gate 和 issue_to_pr_map,再输出 lane_map;不能只凭 MERGEABLE/CLEAN 或 open 列表开 worker。
- 先写 intent_contract:goal / non_goals / done_when / merge_policy / remote_truth_required / truth_level / queue_ledger / ci_truth_source / data_collection / queue_bounds / remote_refresh。
- merge_policy 默认 no_merge;只有用户在当前对话明确授权 merge,才允许 merge_after_gate。
- queue_gate 必须包含每个 open PR 的分类:merge_ready / review_thread_blocked / ci_failed / conflict_blocked / stale_or_superseded / needs_human_decision。
- queue_gate 必须包含 truth_level:A=GitHub API/GraphQL/head/check/reviewThreads 全新鲜;B=GraphQL 不可用但 REST 可用,禁止 merge;C=仅 local git,禁止 remote closure/merge;D=remote 不可靠,仅 plan/prompt pack。
- queue_gate 必须包含 remote_refresh:origin/main 当前 SHA、lane base SHA、是否 stale_base、处理策略。
- 输出 queue_ledger,并在每个 queue item 上记录 owner_lane、head_sha、ci_status、review_thread_state、acceptance_evidence、remote_checked_at。
- lane_map 每个 spawned lane 必须记录 `native_thread_id`;coordinator-only lane 必须写 `native_thread_id: none` 和需要时的 `no_spawn_reason`。
- issue_to_pr_map 必须先判断 open issue 是否已有覆盖 PR;优先收敛已有 PR,不要开竞争 PR。
- data_collection 默认 final_report;只有用户要求 durable logging 或 debug collection 时才写 local_jsonl。
- broad queue 默认只做一个 bounded tranche;没有明确预算时不要承诺处理所有 issue/PR。
- 默认 WIP limit:planning/research 最多 3 lanes;implementation 最多 2 个并发 writable lanes;每个 PR 最多 2 个 reviewers,除非用户明确给更大预算。
- 长队列执行期间只允许定期 `git fetch --prune origin` + stale-base 判断,不要自动 merge/rebase worker worktree。
- 不要把 Codex threads 路由到 OMX/tmux。
- 每个实现 lane 必须有 disjoint writable_files。
- shared-state verification(如 .git/hooks、HOME、global cache、daemon)必须由 verification_owner 串行执行,除非显式隔离。
- lane_map 必须写 `verification_scope`:`inspection_only` / `targeted` / `full_local` / `ci_only`。
- reviewer lane 默认只读 diff/code inspection,可跑 `git diff --check` 或便宜静态检查;只在需要 review confidence 时跑 touched behavior 的 targeted tests。
- reviewer lane 默认不跑 full project test suite;full suite 只能由一个明确 owner 跑,通常是 root orchestrator、verification_owner、merge_reviewer,或由当前 head SHA 的 fresh CI 提供。
- Cargo 测试过滤要合法:每个 `cargo test` 命令只传一个 filter,或使用 module/path 级 filter;不要把多个无关 test name 当多个 positional filters 传给同一条命令。
- review lane 只读。
- 完成的 subagent 要及时 close;长 issue/PR 队列完成一个 bounded tranche 后,记录 ledger/resume query,并考虑开新 parent thread 降低上下文负担。
- 高上下文文件 AGENTS.md/CLAUDE.md/settings/hooks 默认禁止修改。
- 每个 PR merge 前必须有独立 thread review。
- 当 native_subagents=available 时,merge 前的独立 review 必须来自 spawned native thread,并在 `native_thread_evidence` 记录 agent/thread ID、wait evidence、close evidence。
- merge 前必须 truth_level=A,并用 thread-aware GitHub 数据检查 reviewThreads.isResolved;open PR/issue 为空不等于评论闭环完成。
- CI wait 必须有预算;无本地可行动失败时用 WAITING_CI 停止并给 resume 查询。
- review-thread fix/recheck 同类循环超过 2 次,用 REVIEW_LOOP 停止并报告 blocker。
- 输出 capability_gate、thread_dispatch_gate、native_thread_evidence、queue_gate、queue_ledger、issue_to_pr_map、lane_map、依赖图、执行顺序、验证命令、stop_conditions、threads_run_log。Queue Gate Thread
只读 Queue Gate thread。
Repo: {{repo_path}}
GitHub repo: {{owner_repo}}
Target queue: {{queue_scope}}
不要修改文件,不要发 GitHub 评论,不要关闭 issue/PR。
请读取 repo 指令,并用当前 session 的 live remote state 完成队列门。
必须检查:
1. git fetch --prune 后的当前 branch、dirty files、unpushed commits、worktrees。
2. 可用 remote truth level:A/B/C/D,并说明缺失的工具或权限。
3. origin/main 当前 SHA、每个候选 lane 的 base_ref 是否 stale、是否需要 rebase/recreate/stop。
4. open PRs 和 open issues。
5. 每个 open PR 的 head SHA、merge state、check rollup。
6. GraphQL reviewThreads.isResolved / isOutdated;普通 PR comments 不足以证明闭环。
7. open issues 是否已有覆盖 PR、重复 PR、或 superseding work。
输出:
1. open_prs_count / open_issues_count
2. PR classification table:
- merge_ready
- review_thread_blocked
- ci_failed
- conflict_blocked
- stale_or_superseded
- needs_human_decision
3. issue_to_pr_map:covered issue -> PR;uncovered issue -> still actionable/backlog reason
4. truth_level 和禁止动作:B/C/D 时必须明确 merge/closure claim 被禁止
5. queue_ledger:每个 issue/PR/review_thread 的 owner_lane、head_sha、ci_status、review_thread_state、acceptance_evidence、remote_checked_at
6. 推荐执行顺序,优先关闭已有 PR blocker;没有明确预算时只推荐第一个 bounded tranche
7. top 1-3 items 的第一批 lane prompts
8. stop_conditions
规则:
- MERGEABLE/CLEAN 不足以判定 merge_ready;必须同时有当前 head SHA、check rollup、merge state、GraphQL review-thread state。
- review-gated queue 默认一次收敛一个 blocker,除非 writable_files 明确不重叠且 PR 不 stacked。
- remote_refresh 只能 fetch 和比较;不要在 queue gate 自动 rebase/merge。
- GraphQL 不可用时最多 truth_level=B,允许 review/实现,不允许 merge。Read-Only Planning Thread
只读 planning thread。
Repo: {{repo_path}}
GitHub repo: {{owner_repo}}
Target: {{issue_or_pr_or_queue}}
不要修改文件,不要发 GitHub 评论,不要关闭 issue/PR。
请读取 repo 指令、当前 origin/main、目标 issue/PR、相关代码和测试。
输出:
1. 目标摘要
2. capability_gate(native_subagents / tools_seen / explicit_thread_request / spawn_requirement / fallback_mode / no_spawn_reason)
3. native_thread_evidence:spawned_agents 的 lane_id、spawn_tool、agent_id_or_thread_id、wait_evidence、close_evidence、result_collected;若未 spawn,必须说明 no_spawn_reason / fallback_reason
4. intent_contract(含 merge_policy 默认 no_merge、truth_level、data_collection)
5. queue_gate、queue_ledger 和 issue_to_pr_map(GitHub queue 必填;非 queue 说明 N/A)
6. queue_bounds:max_items / time_budget / queue_tranche
7. remote_refresh:origin/main SHA、stale_base 判断、处理建议
8. 已完成映射和证据
9. 未完成/风险
10. 推荐处理动作和理由
11. 可并行 worktree 拆分
12. 每个 lane 的 writable_files、forbidden_files、exclusive_verification、verification_scope、native_thread_id
13. 必须运行的验证命令
14. 不应在本轮强做的范围
15. 建议的 failure_codes(如 stale_remote_state、duplicate_work_missed、missing_intent_contract、native_thread_not_spawned)Implementation Worker
你负责实现 GitHub issue #{{issue_number}} 的最小可合并 slice。
工作目录必须使用现有 worktree:{{worktree_path}}
分支:{{branch_name}}
基线:{{base_ref}}
你不是唯一一个在代码库工作的人:
- 不要修改主 worktree。
- 不要 revert 他人改动。
- 不要 force push。
- 不要修改 AGENTS.md、CLAUDE.md、settings、hooks,除非先汇报 blocker。
你的写入所有权仅限:
{{writable_files}}
禁止触碰:
{{forbidden_files}}
任务:
{{concrete_scope}}
验证:
{{verification_commands}}
verification_scope:
{{verification_scope}}
Remote refresh:
- 在 push 前运行 git fetch --prune origin 并比较 origin/main 与 lane base_ref。
- 如果 origin/main 已前进,不要自动 rebase;汇报 stale_base、重叠文件和建议动作。
- shared-state verification 只有在你被指定为 verification_owner 时才运行。
完成后汇报:
- root cause or core claim
- changed files
- unauthorized_or_unassigned_changes: yes/no
- commits/PR if created
- verification commands and key output
- remaining risks
不要 merge。Read-Only Code Review
请对 {{target_pr_or_worktree}} 做只读 code review,不要修改文件,不要提交,不要 merge。
目标:{{issue_or_pr_goal}}
验证范围:
- 默认 `verification_scope=inspection_only`:做 diff/code inspection。
- 可运行便宜静态检查,如 `git diff --check`,或与 touched behavior 直接相关的 targeted tests。
- 不要运行 full project test suite,除非 lane_map 明确指定你是 `verification_owner` 或 `merge_reviewer`。
- 如果需要 Cargo targeted tests,每条 `cargo test` 命令只传一个 test filter,或使用 module/path 级 filter;不要把多个无关 test name 放进同一条命令。
- 报告未运行的 full-suite 验证,并说明由谁负责:root orchestrator、verification_owner、merge_reviewer,或 fresh CI。
重点检查:
- security and injection risks
- logic regressions
- silent failure or silent degradation
- owner/project/scope mixups
- test integrity and missing critical coverage
- performance regressions
- high-context file mutations
输出 findings first,按严重程度排序,带文件/行号。
如果没有 blocking issue,明确写:No findings; safe to proceed.
说明残余风险和未运行的验证。Fix Worker After Review
你是 PR #{{pr_number}} 修复线程。
工作目录:{{worktree_path}}
分支:{{branch_name}}
只修复以下 reviewer findings:
{{findings}}
不要扩大范围,不要修改未授权文件,不要 revert 他人改动。
修复后运行:
{{verification_commands}}
输出:
- evidence source: files/commands/PR threads inspected
- root cause
- changed files
- verification output
- whether reviewer should re-checkMerge Reviewer
请作为独立 merge reviewer 审查 PR #{{pr_number}} 的最新 head {{head_sha}}。
只审查,不要修改文件,不要提交,不要 merge。
检查:
1. PR 是否仍 open、非 draft、head 是否匹配 {{head_sha}}
2. 用户当前对话是否明确授权 merge;未授权时只给 recommendation
3. truth_level 是否为 A;低于 A 时禁止 merge
4. CI/checks 是否对当前 head 通过;MERGEABLE/CLEAN 或单个绿灯不足以证明可合并
5. diff 是否只包含声明范围
6. review findings 是否已解决
7. GraphQL reviewThreads 是否无 unresolved actionable thread;不要只看普通 PR comments
8. 已修复的 review feedback 是否有对应回复或已 resolve thread
9. 是否存在 high-context file、test weakening、silent fallback、ownership 冲突
10. git fetch --prune 后 origin/main 是否前进;若 stale_base 且影响 PR 范围,不允许 merge
11. CI wait 是否在预算内;若只剩远端等待,返回 WAITING_CI 和 resume 查询
12. review-thread 同类修复循环是否超过 2 次;若超过,返回 REVIEW_LOOP
13. threads_run_log 是否记录了失败码、验证状态、truth_level、remote_refresh 和 closure 状态
如果无 blocking issue,返回:
No findings; safe to merge.
同时列出残余风险。Research/Spec Threads
开 {{n}} 个只读 researcher threads。
每个 thread 负责一个不同角度,不要修改文件。
角度:
1. repo architecture and current implementation
2. public/external reference evidence
3. UX/product workflow
4. validation/eval/testing strategy
5. risk/security/maintainability
每个 researcher 输出:
- evidence with paths/URLs
- concrete gaps
- confidence
- recommended first PR or spec section
- claims requiring verification
主线程最后合并成:
- evidence table
- conflict table
- recommended architecture
- implementation spec
- umbrella issue plus child issues when gaps are heterogeneousResearch-Only Threads
开 {{n}} 个只读 researcher threads。
默认 n<=3。不要修改文件,不要开 PR,不要 merge。
先输出 thread_dispatch_gate:
- explicit_thread_request
- native_subagents
- spawn_requirement
- fallback_mode
- planned_native_threads(每个 lane 的 id/role/target/spawn_status/no_spawn_reason)
- native_thread_evidence.spawned_agents(每个实际 native thread 的 lane_id/spawn_tool/agent_id_or_thread_id/wait_evidence/close_evidence/result_collected)
- no_spawn_reason(仅未 spawn 时允许)
每个 thread 只负责一个明确角度:
{{angles}}
输出 evidence table、conflicts、risks、next recommended action。Review-Only Threads
开最多 2 个只读 reviewer threads 审查 {{target_pr_or_diff}}。
不要修改文件,不要提交,不要 merge。
默认 verification_scope=inspection_only;只运行便宜静态检查或 touched behavior targeted tests。
不要运行 full project test suite,除非 lane_map 明确指定 reviewer 是 verification_owner 或 merge_reviewer。
先输出 thread_dispatch_gate:
- explicit_thread_request
- native_subagents
- spawn_requirement
- fallback_mode
- planned_native_threads(每个 lane 的 id/role/target/spawn_status/no_spawn_reason)
- native_thread_evidence.spawned_agents(每个实际 native thread 的 lane_id/spawn_tool/agent_id_or_thread_id/wait_evidence/close_evidence/result_collected)
- no_spawn_reason(仅未 spawn 时允许)
输出 findings first;如果没有 blocking issue,写 No findings; safe to proceed。
说明未验证项和残余风险。Implement Without Merge
实现 {{scope}} 的最小可合并 slice,可提交或开 PR,但 merge_policy=no_merge。
必须先写 intent_contract、lane_map、writable_files、forbidden_files、verification。
完成后停在 PR ready/recommendation,不要 merge。Final Cleanup Audit
请只读检查本地和远端是否还有残留:
- gh pr list
- gh issue list
- GraphQL reviewThreads.isResolved for touched PRs
- PR conversation comments, review comments, and whether fixed feedback has replies/resolution
- git fetch --prune
- git status --short --branch
- git log origin/main..HEAD
- git diff --stat origin/main...HEAD
- git worktree list
- dirty worktrees and stale branches
- origin/main 当前 SHA 与 touched branch base_ref 的 stale-base 状态
区分:
- remote truth
- local stale state
- dirty but already superseded work
- high-context untracked files
- actual missing PR work
- historical unresolved review threads that are outside the current queue
最后输出 threads_run_log JSON 草稿:
- mode
- repo
- lanes_total
- native_thread_evidence.spawned_agents
- no_spawn_reason
- failure_codes
- verification.fresh
- truth_level
- remote_truth.open_prs
- remote_truth.open_issues
- remote_truth.checked_review_threads
- local_state.dirty_worktrees
- local_state.stale_branches
- remote_closure.checked
- remote_refresh.origin_main_sha
- remote_refresh.stale_base
- queue_bounds.queue_tranche
- outcomeThreads Run Log
Use this reference when the user asks to collect problems encountered by the threads skill, or when a non-trivial run should leave a compact diagnostic trail. Durable JSONL logging is opt-in through data_collection: local_jsonl, an explicit user request, or an explicitly enabled debug run.
Purpose
Collect enough structured data to answer:
- Which trigger caused the skill to run?
- Did the task need multiple lanes, or should it have stayed single-agent?
- Did any lane drift outside its role or writable files?
- Were GitHub, worktree, CI, and review-thread states checked with fresh evidence?
- What remote
truth_levelwas available, and which actions were forbidden by that level? - Did the run refresh
origin/mainoften enough to notice stale bases? - Was the queue bounded to an explicit tranche instead of expanding indefinitely?
- Which failure modes repeat across runs?
Storage
Default local path is project-scoped and kept out of tracked worktree content when the current directory is inside a Git repository:
<git-dir>/codex/threads/run-log.jsonlOverride with:
CODEX_THREADS_RUN_LOG=/path/to/threads-run-log.jsonlThe script discovers the project by walking up from the current working directory until it finds .git. For a normal checkout, <git-dir> is <project>/.git; for a linked worktree, it is the metadata directory referenced by the .git file. This keeps durable logging per project without adding untracked .codex/ files to the repository. If no Git project is found, the fallback path is <current-directory>/.codex/threads/run-log.jsonl.
Do not use a global log file by default; different repositories should not share durable threads telemetry unless the user explicitly sets CODEX_THREADS_RUN_LOG.
Append one JSON object per run:
python3 skills/threads/scripts/append_run_log.py <<'JSON'
{
"skill": "threads",
"mode": "execute_direct",
"repo": "/abs/repo/path",
"trigger_summary": "user asked to process issue and PR queue with threads",
"goal": "fix and merge actionable PR queue",
"intent_contract": {
"merge_policy": "no_merge",
"remote_truth_required": true,
"data_collection": "local_jsonl"
},
"truth_level": "A",
"native_subagents": "available",
"explicit_thread_request": true,
"spawn_requirement": "required",
"fallback_mode": "none",
"native_thread_evidence": {
"spawned_agents": [
{
"lane_id": "merge-reviewer",
"spawn_tool": "multi_agent_v1.spawn_agent",
"agent_id_or_thread_id": "agent-123",
"wait_evidence": "wait_agent completed",
"close_evidence": "close_agent completed",
"result_collected": true
}
]
},
"queue_bounds": {
"max_items": 1,
"time_budget": "30m",
"queue_tranche": "first merge-ready blocker"
},
"lanes_total": 4,
"failure_codes": ["review_thread_missed"],
"remote_refresh": {
"origin_main_sha": "abc123",
"stale_base": false,
"refreshes": 3
},
"remote_truth": {
"open_prs": 0,
"open_issues": 0
},
"local_state": {
"dirty_worktree": false
},
"outcome": "partial",
"verification": {
"fresh": true,
"commands": ["python3 scripts/validate_skills.py --check"]
}
}
JSONSchema
Recommended fields:
{
"schema_version": 1,
"recorded_at_utc": "auto-filled by script",
"skill": "threads",
"skill_source": "local|spellbook|unknown",
"mode": "single_agent|plan_only|execute_direct|review_only|research_spec|clarify_first",
"repo": "/absolute/repo/path",
"base_ref": "origin/main",
"trigger_summary": "short summary, not the raw prompt",
"goal": "short goal",
"non_goals": ["out of scope item"],
"intent_contract": {
"merge_policy": "no_merge|merge_after_gate|user_confirm_before_merge",
"remote_truth_required": true,
"data_collection": "final_report|local_jsonl|none"
},
"truth_level": "A|B|C|D",
"native_subagents": "available|unavailable",
"explicit_thread_request": true,
"spawn_requirement": "required|optional|unavailable",
"fallback_mode": "none|single_agent|prompt_pack_only",
"no_spawn_reason": "required when an explicit threads run falls back to single_agent; use no_independent_lanes|sequential_dependency|shared_writable_files|tool_unavailable|user_requested_single_agent",
"single_agent_justification": {
"reason": "no_independent_lanes|sequential_dependency|shared_writable_files|tool_unavailable|user_requested_single_agent",
"evidence": "short evidence summary"
},
"native_thread_evidence": {
"spawned_agents": [
{
"lane_id": "review-pr",
"spawn_tool": "multi_agent_v1.spawn_agent",
"agent_id_or_thread_id": "agent-123",
"wait_evidence": "completed status collected",
"close_evidence": "closed after collection",
"result_collected": true
}
],
"fallback_reason": ""
},
"queue_bounds": {
"max_items": 1,
"time_budget": "30m",
"queue_tranche": "first blocker"
},
"remote_refresh": {
"cadence": "queue_start|before_lane|before_push|before_merge|after_ci_wait",
"origin_main_sha": "abc123",
"local_base_sha": "def456",
"stale_base": false,
"refreshes": 1,
"policy": "continue|rebase|required_stop"
},
"queue_ledger": {
"items_total": 0,
"items_closed": 0,
"items_deferred": 0,
"superseded_items": []
},
"lanes_total": 0,
"lanes": [
{
"id": "worker-1",
"role": "worker",
"target": "issue #123",
"worktree": "/tmp/repo-worker-1",
"writable_files": ["src/example.rs"],
"files_changed": ["src/example.rs"],
"native_thread_id": "agent-123",
"verification_scope": "targeted",
"verification": ["cargo test example"],
"result": "passed|blocked|failed"
}
],
"failure_codes": [],
"remote_closure": {
"checked": true,
"open_prs": 0,
"open_issues": 0,
"unresolved_review_threads": 0
},
"remote_truth": {
"open_prs": 0,
"open_issues": 0,
"checked_pr_heads": [],
"checked_review_threads": [],
"checked_ci": [],
"origin_main_sha": "abc123",
"stale_base": false
},
"local_state": {
"dirty_worktree": false,
"stale_worktree": false,
"high_context_file": false
},
"ci_wait": {
"duration_seconds": 0,
"budget_exhausted": false,
"pending_checks": []
},
"review_loop": {
"cycles": 0,
"outcome": "resolved|review_loop|not_applicable"
},
"exclusive_verification": {
"serialized_commands": [],
"reason": ""
},
"verification": {
"fresh": true,
"commands": ["cargo test"],
"failed_commands": []
},
"outcome": "success|partial|blocked|failed",
"notes": "short diagnostic note"
}Truth levels:
A: git fetch plus GitHub API or GraphQL can prove current PR head, checks, merge state, and review-thread state.B: git fetch plus REST PR/review/comment data is available, but GraphQL review-thread state is unavailable; merge is forbidden.C: only local git state is reliable; remote closure and merge claims are forbidden.D: no reliable repo or remote state is available; only plan or prompt-pack output is allowed.
The append script enforces an allowlist of top-level fields by default. Use --allow-extra only for local debugging when extra fields are needed; sensitive keys and common token patterns are still redacted.
Safety limits:
- maximum input size: 64 KiB
- maximum string length: 4000 characters
- maximum nesting depth: 8
- maximum array items retained: 100
- new log files are created with
0600permissions - append uses a POSIX file lock when available
Failure Codes
Use stable codes so later analysis can aggregate them:
trigger_too_broad: skill activated for a task that did not need threads.missing_intent_contract: goal, non-goals, done-when, or merge policy was unclear.truth_level_too_low: requested action required a higher remote truth level.source_drift: local installed skill and Spellbook/source version differed.stale_remote_state: PR, issue, branch, or CI state was not freshly fetched.stale_base:origin/mainadvanced under a lane and may invalidate its base.duplicate_work_missed: existing PR/issue/branch already covered the task.role_drift: planner/reviewer/worker acted outside its lane role.write_scope_violation: worker touched unassigned or forbidden files.vague_lane_output: lane returned claims without commands, files, or evidence.verification_gap: completion was claimed without fresh command output.review_thread_missed: inline review thread/comment state was not checked.review_loop: repeated review-thread fix cycles hit the configured limit.native_thread_not_spawned: explicit threads run did not spawn a native subagent and did not record a valid fallback.waiting_ci: only remote CI remained and the wait budget was exhausted.merge_gate_bypass: merge happened without independent review or closure audit.tool_unavailable: native subagent, GitHub, or validation tool was unavailable.environment_mismatch: wrong cwd, worktree, binary, branch, or runtime was used.context_loss: compaction/resume lost required state.user_interrupt: user redirected or stopped the run before closure.
Analysis Queries
Common local checks:
jq -r '.failure_codes[]?' "$(git rev-parse --git-dir)/codex/threads/run-log.jsonl" | sort | uniq -c | sort -nr
jq -r 'select(.outcome!="success") | [.recorded_at_utc,.repo,.mode,.failure_codes|join(",")] | @tsv' "$(git rev-parse --git-dir)/codex/threads/run-log.jsonl"
jq -r 'select(.verification.fresh==false) | [.recorded_at_utc,.repo,.goal] | @tsv' "$(git rev-parse --git-dir)/codex/threads/run-log.jsonl"Privacy
Do not log secrets, tokens, cookies, private messages, raw prompts, or full command output. Log concise summaries and stable evidence identifiers instead: file paths, command names, PR/issue numbers, head SHAs, and failure codes. Unknown top-level fields are rejected unless --allow-extra is supplied.
#!/usr/bin/env python3
"""Append a sanitized threads run record to a local JSONL file."""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
try:
import fcntl
except ImportError: # pragma: no cover - non-POSIX fallback.
fcntl = None
SENSITIVE_KEY_PARTS = (
"authorization",
"cookie",
"password",
"secret",
"token",
"api_key",
"apikey",
)
MAX_STRING_LENGTH = 4000
MAX_INPUT_BYTES = 64 * 1024
MAX_DEPTH = 8
MAX_ARRAY_ITEMS = 100
ALLOWED_MODES = {
"single_agent",
"plan_only",
"execute_direct",
"review_only",
"research_spec",
"clarify_first",
}
ALLOWED_TRUTH_LEVELS = {"A", "B", "C", "D"}
ALLOWED_FALLBACK_MODES = {"none", "single_agent", "prompt_pack_only"}
ALLOWED_NATIVE_SPAWN_TOOLS = {"multi_agent_v1.spawn_agent"}
INVALID_AGENT_IDS = {"", "none", "n/a", "na", "null", "main", "main_thread", "coordinator"}
ALLOWED_SINGLE_AGENT_REASONS = {
"no_independent_lanes",
"sequential_dependency",
"shared_writable_files",
"tool_unavailable",
"user_requested_single_agent",
}
ALLOWED_TOP_LEVEL_FIELDS = {
"schema_version",
"recorded_at_utc",
"skill",
"skill_source",
"mode",
"repo",
"base_ref",
"trigger_summary",
"goal",
"non_goals",
"intent_contract",
"merge_policy",
"data_collection",
"truth_level",
"native_subagents",
"explicit_thread_request",
"spawn_requirement",
"native_thread_evidence",
"fallback_mode",
"no_spawn_reason",
"single_agent_justification",
"capability_gate",
"thread_dispatch_gate",
"queue_bounds",
"remote_refresh",
"queue_ledger",
"lane_map",
"lanes_total",
"lanes",
"failure_codes",
"remote_truth",
"remote_closure",
"closure_audit",
"local_state",
"ci_wait",
"review_loop",
"exclusive_verification",
"verification",
"outcome",
"notes",
}
SENSITIVE_PATTERNS = (
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.DOTALL),
re.compile(r"github_pat_[A-Za-z0-9_]{20,}"),
re.compile(r"gh[pousr]_[A-Za-z0-9_]{20,}"),
re.compile(r"sk-[A-Za-z0-9_-]{20,}"),
re.compile(r"AKIA[0-9A-Z]{16}"),
re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/=-]{16,}"),
re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"),
)
def find_project_root(start: Path | None = None) -> Path:
current = (start or Path.cwd()).resolve()
for parent in [current, *current.parents]:
if (parent / ".git").exists():
return parent
return current
def find_git_metadata_dir(project_root: Path) -> Path | None:
dot_git = project_root / ".git"
if dot_git.is_dir():
return dot_git
if not dot_git.is_file():
return None
try:
marker = dot_git.read_text(encoding="utf-8").strip()
except OSError:
return None
prefix = "gitdir:"
if not marker.lower().startswith(prefix):
return None
git_dir = Path(marker[len(prefix) :].strip()).expanduser()
if not git_dir.is_absolute():
git_dir = (project_root / git_dir).resolve()
return git_dir
def default_log_path() -> Path:
override = os.environ.get("CODEX_THREADS_RUN_LOG")
if override:
return Path(override).expanduser()
project_root = find_project_root()
git_metadata_dir = find_git_metadata_dir(project_root)
if git_metadata_dir is not None:
return git_metadata_dir / "codex" / "threads" / "run-log.jsonl"
return project_root / ".codex" / "threads" / "run-log.jsonl"
def redact_string(value: str) -> str:
redacted = value
for pattern in SENSITIVE_PATTERNS:
redacted = pattern.sub("[REDACTED]", redacted)
if len(redacted) > MAX_STRING_LENGTH:
return redacted[:MAX_STRING_LENGTH] + "...[TRUNCATED]"
return redacted
def redact(value: Any, key_hint: str = "", depth: int = 0) -> Any:
if depth > MAX_DEPTH:
return "[TRUNCATED_DEPTH]"
lower_key = key_hint.lower()
if any(part in lower_key for part in SENSITIVE_KEY_PARTS):
return "[REDACTED]"
if isinstance(value, dict):
return {str(key): redact(item, str(key), depth + 1) for key, item in value.items()}
if isinstance(value, list):
return [redact(item, key_hint, depth + 1) for item in value[:MAX_ARRAY_ITEMS]]
if isinstance(value, str):
return redact_string(value)
return value
def normalize_record(raw: Any, allow_extra: bool = False) -> dict[str, Any]:
if not isinstance(raw, dict):
raise ValueError("run log input must be a JSON object")
unknown_fields = sorted(set(raw) - ALLOWED_TOP_LEVEL_FIELDS)
if unknown_fields and not allow_extra:
raise ValueError("unknown top-level field(s): " + ", ".join(unknown_fields))
record = redact(raw)
mode = record.get("mode")
if mode is not None and mode not in ALLOWED_MODES:
raise ValueError(f"unknown mode: {mode}")
truth_level = record.get("truth_level")
if truth_level is not None and truth_level not in ALLOWED_TRUTH_LEVELS:
raise ValueError(f"unknown truth_level: {truth_level}")
validate_native_thread_evidence(record)
record.setdefault("schema_version", 1)
record["recorded_at_utc"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
return record
def truthy(value: Any) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.strip().lower() in {"yes", "true", "1", "required"}
return False
def canonical_gate_value(field: str, value: Any) -> Any:
if field == "explicit_thread_request":
return truthy(value)
return value
def nested_get(mapping: dict[str, Any], field: str) -> Any:
values: list[tuple[str, Any]] = []
if field in mapping:
values.append(("top-level", mapping.get(field)))
for container in ("capability_gate", "thread_dispatch_gate"):
nested = mapping.get(container)
if isinstance(nested, dict) and field in nested:
values.append((container, nested.get(field)))
present = [(source, value) for source, value in values if value is not None]
if not present:
return None
canonical_values = {
canonical_gate_value(field, value)
for _, value in present
}
if len(canonical_values) > 1:
sources = ", ".join(source for source, _ in present)
raise ValueError(f"conflicting {field} values across {sources}")
return present[0][1]
def native_thread_evidence(record: dict[str, Any]) -> dict[str, Any] | None:
evidence = record.get("native_thread_evidence")
if isinstance(evidence, dict):
return evidence
dispatch_gate = record.get("thread_dispatch_gate")
if isinstance(dispatch_gate, dict):
nested = dispatch_gate.get("native_thread_evidence")
if isinstance(nested, dict):
return nested
return None
def thread_dispatch_gate(record: dict[str, Any]) -> dict[str, Any] | None:
gate = record.get("thread_dispatch_gate")
return gate if isinstance(gate, dict) else None
def valid_agent_id(value: Any) -> bool:
if not isinstance(value, str):
return False
return value.strip().lower() not in INVALID_AGENT_IDS
def valid_spawned_agents(record: dict[str, Any]) -> list[dict[str, Any]]:
evidence = native_thread_evidence(record)
if evidence is None:
return []
spawned_agents = evidence.get("spawned_agents")
if not isinstance(spawned_agents, list):
return []
valid_agents = []
for agent in spawned_agents:
if not isinstance(agent, dict):
continue
agent_id = agent.get("agent_id_or_thread_id") or agent.get("tool_agent_id")
if (
agent.get("spawn_tool") in ALLOWED_NATIVE_SPAWN_TOOLS
and valid_agent_id(agent_id)
and agent.get("result_collected") is True
and bool(agent.get("wait_evidence"))
and bool(agent.get("close_evidence"))
):
valid_agents.append(agent)
return valid_agents
def has_spawned_agent(record: dict[str, Any]) -> bool:
return bool(valid_spawned_agents(record))
def normalize_reason(value: Any) -> str | None:
if not isinstance(value, str):
return None
normalized = value.strip().lower().replace("-", "_").replace(" ", "_")
return normalized or None
def allowed_reason(reason: Any, evidence: Any = None) -> bool:
normalized = normalize_reason(reason)
if normalized not in ALLOWED_SINGLE_AGENT_REASONS:
return False
return evidence is None or bool(evidence)
def has_single_agent_reason(record: dict[str, Any]) -> bool:
no_spawn_reason = record.get("no_spawn_reason")
if isinstance(no_spawn_reason, dict):
if allowed_reason(no_spawn_reason.get("reason"), no_spawn_reason.get("evidence")):
return True
elif allowed_reason(no_spawn_reason):
return True
gate = thread_dispatch_gate(record)
if gate is not None:
gate_reason = gate.get("no_spawn_reason")
if isinstance(gate_reason, dict):
if allowed_reason(gate_reason.get("reason"), gate_reason.get("evidence")):
return True
elif allowed_reason(gate_reason):
return True
justification = record.get("single_agent_justification")
if (
isinstance(justification, dict)
and allowed_reason(justification.get("reason"), justification.get("evidence"))
):
return True
evidence = native_thread_evidence(record)
if not isinstance(evidence, dict):
return False
return allowed_reason(evidence.get("fallback_reason"))
def lane_has_no_spawn_reason(lane: dict[str, Any]) -> bool:
reason = lane.get("no_spawn_reason")
if isinstance(reason, dict):
return allowed_reason(reason.get("reason"), reason.get("evidence"))
return allowed_reason(reason)
def validate_planned_native_threads(record: dict[str, Any]) -> None:
gate = thread_dispatch_gate(record)
if gate is None:
return
planned_threads = gate.get("planned_native_threads")
if planned_threads is None:
return
if not isinstance(planned_threads, list):
raise ValueError("thread_dispatch_gate.planned_native_threads must be a list")
spawned_lane_ids = {
agent.get("lane_id")
for agent in valid_spawned_agents(record)
if isinstance(agent.get("lane_id"), str) and agent.get("lane_id")
}
missing_reasons = []
for lane in planned_threads:
if not isinstance(lane, dict):
raise ValueError("thread_dispatch_gate.planned_native_threads entries must be objects")
lane_id = lane.get("id") or lane.get("lane_id")
if not lane_id:
raise ValueError("thread_dispatch_gate.planned_native_threads entries require id")
if lane_id in spawned_lane_ids:
continue
if lane_has_no_spawn_reason(lane):
continue
missing_reasons.append(str(lane_id))
if missing_reasons:
raise ValueError(
"thread_dispatch_gate.planned_native_threads missing spawned evidence "
"or no_spawn_reason for lane(s): " + ", ".join(missing_reasons)
)
def validate_native_thread_evidence(record: dict[str, Any]) -> None:
mode = record.get("mode")
native_subagents = nested_get(record, "native_subagents")
fallback_mode = nested_get(record, "fallback_mode")
explicit_request = nested_get(record, "explicit_thread_request")
spawn_requirement = nested_get(record, "spawn_requirement")
dispatch_mode = mode in {
"single_agent",
"plan_only",
"execute_direct",
"review_only",
"research_spec",
}
required = truthy(explicit_request) or spawn_requirement == "required"
if fallback_mode is not None and fallback_mode not in ALLOWED_FALLBACK_MODES:
raise ValueError(f"unknown fallback_mode: {fallback_mode}")
explicit_native_required = dispatch_mode and native_subagents == "available" and required
if explicit_native_required and fallback_mode is None:
raise ValueError(
"fallback_mode is required when native subagents are available "
"for an explicit threads run"
)
if (
explicit_native_required
and fallback_mode == "none"
and not has_spawned_agent(record)
):
raise ValueError(
"native_thread_evidence.spawned_agents is required when native "
"subagents are available for an explicit threads run"
)
if (
explicit_native_required
and fallback_mode == "single_agent"
and not has_single_agent_reason(record)
):
raise ValueError(
"single_agent fallback for an explicit threads run requires "
"an allowed no_spawn_reason or single_agent_justification.reason"
)
if explicit_native_required and fallback_mode == "prompt_pack_only":
raise ValueError(
"prompt_pack_only fallback is invalid when native subagents are "
"available for an explicit threads run"
)
if explicit_native_required and fallback_mode == "none":
validate_planned_native_threads(record)
def append_record(record: dict[str, Any], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
with os.fdopen(fd, "a", encoding="utf-8") as handle:
if fcntl is not None:
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True) + "\n")
finally:
if fcntl is not None:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
def load_input() -> Any:
raw_bytes = sys.stdin.buffer.read(MAX_INPUT_BYTES + 1)
if len(raw_bytes) > MAX_INPUT_BYTES:
raise ValueError(f"run log input exceeds {MAX_INPUT_BYTES} bytes")
return json.loads(raw_bytes.decode("utf-8"))
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--path",
type=Path,
default=None,
help=(
"JSONL path. Defaults to CODEX_THREADS_RUN_LOG or "
"<git-dir>/codex/threads/run-log.jsonl inside a Git project."
),
)
parser.add_argument(
"--allow-extra",
action="store_true",
help="Allow unknown top-level fields after redaction. Defaults to rejecting them.",
)
args = parser.parse_args()
try:
raw = load_input()
record = normalize_record(raw, allow_extra=args.allow_extra)
path = args.path.expanduser() if args.path is not None else default_log_path()
append_record(record, path)
except (OSError, json.JSONDecodeError, ValueError) as exc:
print(f"append_run_log.py: {exc}", file=sys.stderr)
return 1
print(str(path))
return 0
if __name__ == "__main__":
raise SystemExit(main())