autospec-run
Use when the user has already run /autospec-define (or otherwise has a populated set of auto-implement GitHub issues) and wants the implementation half — Phases 4-6 — to run autonomously with admin auto-merge. Supports --profile <name> filtering against ~/.autospec/model-profiles.yml.
- License 许可
- MIT
- 最近更新
- 2026-11-11
- Stars
- ★ 3
- Forks
- 1
01 前言 Front Matter
- name
- autospec-run
- description
- Use when the user has already run /autospec-define (or otherwise has a populated set of auto-implement GitHub issues) and wants the implementation half — Phases 4-6 — to run autonomously with admin auto-merge. Supports --profile <name> filtering against ~/.autospec/model-profiles.yml.
02 资源文件 29
- 参考文档codex/prompt.md128.5 KB
- 脚本install.sh14.3 KB
- 参考文档opencode/agent.md128.8 KB
- 参考文档prompts/implementer-contract.md24.0 KB
- 参考文档prompts/phase4-implementer.md40.1 KB
- 参考文档prompts/reviewer-contract.md11.7 KB
- 参考文档README.md6.2 KB
- 参考文档references/end-of-run.md19.2 KB
- 脚本scripts/autospec-run-session-lock.sh4.3 KB
- 脚本scripts/autospec-run-status.sh10.4 KB
- 脚本scripts/docs-completeness-gaps.sh7.9 KB
- 脚本scripts/fab-completeness.sh5.0 KB
- 脚本scripts/fab-route.sh3.4 KB
- 脚本scripts/heartbeat-read.sh6.8 KB
- 脚本scripts/heartbeat-write.sh9.3 KB
- 脚本scripts/independent-review-adapter.sh7.6 KB
- 脚本scripts/invoke-review.sh7.5 KB
- 脚本scripts/post-token-report.sh6.9 KB
- 脚本scripts/run-groom-preflight.sh3.7 KB
- 脚本scripts/select-model-profile.sh11.6 KB
- 其他tests/duplicate-read.bats9.2 KB
- 其他tests/integration/phase4-drift-gate.bats2.9 KB
- 其他tests/integration/phase55-docs-completeness.bats6.0 KB
- 其他tests/phase4-fab-routing.bats2.1 KB
- 其他tests/phase55-fab-completeness.bats5.6 KB
- 其他tests/post-token-report.bats8.2 KB
- 其他tests/reviewer-tier.bats3.1 KB
- 其他tests/test_watchdog_claim_timeout.bats9.5 KB
- 脚本uninstall.sh3.8 KB
03 SKILL.md 正文
autospec-run workflow (harness-neutral)
Take the populated auto-implement queue on the current GitHub repo and run the implementation half of the autospec pipeline:
autonomous monitor → admin-squash-merge each PR → periodic status updates → final report.
Manage your own context — never exceed 60%. Delegate to subagents whenever your harness supports it; do not investigate, write code, or design directly in the main conversation when a subagent can do it.
Autospec-run is an autonomous loop and should not ask operator questions for normal operations. Only surface a question if a hard blocker requires explicit manual recovery.
<!-- autospec-block:startup-self-update SKILL_NAME=autospec-run -->
Self-update mode
Decide this purely from the request text the harness handed you. Do NOT
shell out (no grep, sed, [[ =~ ]], command substitution, etc.) to
test the user's free-form request — passing it through a shell is what
historically tripped harness permission engines (e.g. parse errors near
backtick/pipe characters in the user's prose). Read the request, normalize
it in your reasoning (collapse whitespace, trim, lowercase), and if the result is
exactly update, this skill enters self-update mode and does NOT run the
normal pipeline.
- Detect harness by checking which install path exists for this skill:
- Claude Code:
~/.claude/skills/autospec-run/SKILL.md - OpenCode:
~/.config/opencode/agent/autospec-run.md - Codex CLI:
~/.codex/prompts/autospec-run.md
- Claude Code:
- Re-install the full autospec suite from
mainby piping the canonical installer:
Run this one-liner once; it refreshes all autospec skills across all harnesses.curl -fsSL https://raw.githubusercontent.com/berlinguyinca/autospec/main/bootstrap.sh | bash -s -- --skill all --harness all --update - Show the diff between the prior installed file(s) and the freshly fetched copy (e.g.
diff <(cat <prior>) <(curl -fsSL ...SKILL.md)or the equivalent recorded by the installer). - Stop. Do not enter Phase 0 / Phase 1 / any pipeline phase. Print the upgrade summary and return to the user.
If no install path is detected, print Self-update: no installed copy of autospec-run found; run install.sh first. and exit.
Stop mode
Apply the same read-and-normalize approach used for self-update mode (do
NOT shell out the user's request). If the normalized request is exactly
stop, or stop followed by one or more --<word> flags (examples:
stop, stop --graceful, stop --immediate, stop --status,
stop --resume, stop --help, stop --flag), this skill enters stop
mode and does NOT run the normal pipeline. When dispatching, pass any
--<flag> tokens the user provided as separate words to the helper.
- Dispatch to
bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-stop.sh" <args>. - Print the helper's stdout to the user.
- Stop. Do not enter Phase 0 or any pipeline phase.
Status mode
Apply the same read-and-normalize approach. If the normalized request is exactly
status (or status followed by --<word> flags such as --json / --repo /
--stale-secs), this skill enters status mode and does NOT run the pipeline — it
prints a one-glance operator view of the live run (in-flight issues with step +
age + branch + PR + STALE detection, the queue counts, and the stop-flag state)
instead of making the operator hand-parse heartbeat JSON.
- Dispatch to
bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-run-status.sh" <args>. - Print the helper's stdout to the user.
- Stop. Do not enter Phase 0 or any pipeline phase.
Invocation
/autospec-run [--profile <name>]
--profile <name>— filter the candidate queue against~/.autospec/model-profiles.ymlso only issues whosectx:*andreasoning:*labels fit the named profile are picked up. Issues that exceed the profile on either axis are appended to adeferred[]list and printed in the run-end summary.- (no flag) — load
~/.autospec/model-profiles.yml'sdefault:profile and run with it. If the file is missing, run auto-init (below) then exit so the user can review/edit before re-running. --profile <unknown>— exit non-zero and print the list of available profile names from~/.autospec/model-profiles.yml.--worker-id <id>— override the distributed worker identity written to GitHub run-state comments. Otherwise derive it from host, user, harness, pid, and start timestamp.--coordination-status— print active workers, claimed issues, blockers, stale claims, conflicts, and the next safe batch, then exit without claiming.--max-parallel-safe— print the next safe parallel batch fromautospec queue readyand exit without claiming.--claim <issue>— attempt a deterministic claim viaautospec claim acquireand exit with that command's status (0claimed,2already claimed/skipped).--release <issue>— release a distributed claim viaautospec claim release.
Auto-init ~/.autospec/model-profiles.yml
If the file is missing on run start:
Dispatching to a local model. A profile resolved to a local model is executed via
bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/local-dispatch.sh" --model <tag> --prompt-file <path>, never by calling a runtime directly. It refuses (exit 3) when
Codex CLI is absent or does not advertise --oss, when the capability probe reports the
model is not dispatch_recommended, or when no wall-clock bound can be applied — on any
refusal, keep the cloud tier. The local GPU is capacity-1, so the helper serializes
dispatches; never run two local dispatches concurrently.
Capping fan-out on a local model. Serialization covers local-dispatch.sh; it does
NOT cover subagents you spawn in this harness, which reach the same node as ordinary
sessions. Children cost no VRAM — that is committed when the server starts — but each
claims a share of a fixed KV pool that has no admission control, so over-subscribing it
fails every live session rather than the newest. Before any parallel dispatch whose
children run on a local model, cap the width by what the pool funds:
W=$(context-budget-check.py --width "$PLANNED" --json 2>/dev/null | jq -r .max_width)
Use min($PLANNED, $W). W <= 1 means run the work inline. If the checker exits 2 —
server unreachable, client config unreadable — keep the cloud tier rather than guessing
a width; a wrong guess is not a slower run, it is a failed one for every session on that
node. See the capacity-gate section of AGENTS.md.
-
Discover local supply with the probe — never enumerate models yourself.
bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/discover-model-supply.sh" --profilesPaste its
profiles:fragment into the file verbatim. The probe measures each model's real context length viaollama show, keeps only completion-capable models, writes the original runtime tag asmodel:, and normalizes the profile key (qwen3:32b→qwen3-32b-laptop).Do not list models from memory, from the model names you happen to know, or from an inferred parameter count. An earlier revision of this section asked the orchestrator to do exactly that, and on a live host it produced nine local profiles for four real models — including an unrunnable
qwen3-coder-480b— and rated every one of themctx: 64kwhen the measured values were 131072 and 262144. Enumerating ground truth is the probe's job.Read these fields off the probe rather than guessing:
accelerator.usable/accelerator.reason— a presentnvidia-smiand a present/dev/nvidia0do not mean a usable GPU. Whenusableis false, surfacereasonto the operator (e.g.nvml_driver_library_mismatchis a five-minute fix) instead of silently accepting CPU inference.dispatch_recommendedper model — false means the probe found the model but the host cannot run it usefully (no accelerator, or weights exceed the memory budget). Leave those profiles commented out; the probe already does this.- A model set that comes back empty (no
ollama, daemon down, or zero completion models) means no local profiles. Never synthesize one.
-
If
ANTHROPIC_API_KEYis set in the environment, append two cloud profiles:claude-sonnet-cloudandclaude-opus-cloud, bothctx: 120k,reasoning: deep, each with itsmodel:id. Also appendclaude-haiku-cloud(ctx: 64k,reasoning: medium) —select-model-profile.shroutesreasoning:shallow|mediumissues to it, so omitting it silently disables the cheap implementer tier. -
If neither Ollama nor
ANTHROPIC_API_KEYis detected, write a singleclaude-sonnet-clouddefault withctx: 120k, reasoning: deepand an# edit-and-reruncomment near the top of the file. -
Set the top-level
default:key to whichever profile makes sense (prefer the largest local profile if any, otherwiseclaude-sonnet-cloud). -
Print:
Wrote ~/.autospec/model-profiles.yml. Edit `default:` and profile ceilings, then re-run /autospec-run [--profile <name>].Exit 0; do not enter Phase 4.
Sample auto-init output:
# ~/.autospec/model-profiles.yml — autospec-run profile ceilings.
# Edit `default:` and individual ceilings, then re-run.
default: claude-sonnet-cloud
profiles:
qwen3-32b-laptop:
model: qwen3:32b # original runtime tag — what a dispatch sends
ctx: 64k # one of: 32k | 64k | 120k
reasoning: medium # one of: shallow | medium | deep
roles: [implementer] # local profiles serve the implementer only (Guardrail R2)
claude-haiku-cloud:
model: claude-haiku-4-5
ctx: 64k
reasoning: medium
claude-sonnet-cloud:
model: claude-sonnet-5
ctx: 120k
reasoning: deep
Profile-filter ordinals
ctx: 32k < 64k < 120kreasoning: shallow < medium < deep
A profile P "fits" issue I when I.ctx_label ≤ P.ctx AND I.reasoning_label ≤ P.reasoning on these ordinals.
Required capabilities & harness adapter
This workflow assumes five capabilities. Map each one to your harness's actual tool. If a capability is missing, use the listed fallback.
| Capability | Claude Code | OpenCode | Codex CLI | Fallback if missing |
|---|---|---|---|---|
| Read-only codebase research | Agent (subagent_type=Explore) |
task agent in read-only mode |
apply_patch read-only / shell grep |
Do the search in-thread with rg/grep |
| Foreground delegation | Agent (subagent_type=general-purpose) |
nested task agent, await output |
spawn nested CLI session | Do the work in-thread (more context cost) |
| Background delegation | Agent with run_in_background: true |
detached task agent |
nohup'd CLI session writing to a logfile | Run the monitor in a separate terminal/tmux pane |
| Ask the user a question | AskUserQuestion |
inline prompt | inline prompt | Ask in the response and wait for the next turn |
| Self-paced future wakeup | ScheduleWakeup inside a /loop |
a recurring task or local cron |
local cron/launchd calling the CLI |
The user runs a status-update prompt manually |
| Subagent model tier | Tier A: opus + ultrathink; Tier B: sonnet + medium thinking |
Tier A: top task model + high reasoning; Tier B: smaller-tier task + medium reasoning |
Tier A: top GPT + reasoning_effort=high; Tier B: current spark/cost-optimized Codex + reasoning_effort=medium |
Honor the per-phase tier mapping in AGENTS.md; retry the same subagent UP on unavailability |
| Local model dispatch | not native — Agent(model:) takes Claude tiers only; shell out to local-dispatch.sh |
provider pointed at :11434/v1 (or :8000 for vLLM) |
codex exec --oss --local-provider ollama |
cloud tier (fail closed) |
| <!-- autospec-block:harness-adapter-core --> |
Persistent project notes: write durable preferences to AGENTS.md in the repo root — this is the de-facto standard recognized by Claude Code (also reads CLAUDE.md), OpenCode, and Codex. If your harness has its own private memory (e.g. Claude Code's ~/.claude/.../memory/), mirror the same content there. Per AGENTS.md, subagent dispatches use a two-tier policy: Tier A (top model + extended thinking) for spec work (research, decompose, review/label — not used by this skill); Tier B (cheaper model + medium thinking) for implementation work (Phase 4 implementer + LGTM review). The orchestrator keeps the user's invoked model. Fall back UP the tier on quota/capacity or other unavailability by retrying the same subagent with the stronger tier while preserving parent context.
Harness detection (run once at skill start, before Phase 0)
Detect your harness by checking available tools before any phase:
-
Claude Code — the
Agenttool with asubagent_typeparameter is available.TIER_A=opus+ultrathink(the alias, which resolves to the current Claude Opus generation)TIER_B=sonnet(the alias, which resolves to the current Claude Sonnet generation)
-
OpenCode — a
tasktool with model/tier configuration is available (nosubagent_type).TIER_A= top-tier task model + high reasoningTIER_B= smaller-tier task model + medium reasoning
-
Codex CLI — neither
Agentnor a configurabletasktool is available;apply_patchis the primary edit tool.TIER_A= current top GPT model +reasoning_effort=highTIER_B= the spark / cost-optimized variant of the configured model when one exists, else the configured model +reasoning_effort=medium
Fallback rule: If TIER_B is not available in your harness (model unknown, quota/capacity failure, authorization failure, or tool call returns an error for that model), silently retry the same subagent dispatch with TIER_A. Preserve the parent context on retry by passing a bounded handoff containing the issue number, repo path, branch/worktree plan, relevant issue body sections, last error, and current queue/claim state. Codex native subagents with explicit agent_type, model, or reasoning_effort MUST use a bounded handoff, not a full-history fork; do not ask Codex to inherit/fork the full parent conversation when those fields are set. If a role-specialized reviewer has a fixed model binding, retry through a fresh configurable independent agent at TIER_A with the identical reviewer prompt, evidence inputs, tool budget, and verdict schema. Never fall back to the author/main lane or a weaker model. The orchestrator records the requested model, actual model, availability error class, and reviewed head SHA in a sidecar review artifact; never append metadata to the reviewer's byte-exact verdict channel. If no equal-or-stronger independent reviewer can run, fail closed and do not merge. Never ask the user.
Codex SpawnAgent call-shape contract: Codex has two valid subagent call shapes. Use the bounded handoff when setting an explicit executor/tier route: SpawnAgent({ prompt: bounded_handoff, agent_type: "executor", model: TIER_B, reasoning_effort: "medium" }). Use a full-history fork only when inheriting the current conversation without explicit routing: SpawnAgent({ prompt: full_history_prompt, full_history: true }). Never combine full_history: true with agent_type, model, or reasoning_effort. On Codex dispatch failure, retry once with the other valid shape when that still satisfies the phase's tier rule; if both valid shapes fail, release the claimed issue back to auto-implement with a visible blocker comment.
Hold TIER_A and TIER_B for the entire skill run. Every "Tier A" and "Tier B" reference below resolves to these harness-specific values.
Relevant memory injection (run-start, once)
Before executing the main pipeline phases, call the injector to surface relevant saved lessons:
bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/inject-relevant-memory.sh" \
--context "<skill-relevant keywords from request/issue/spec>" \
--top-k 5
Prepend the output block (if non-empty) to your working context. This surfaces lessons like
feedback_bash_return_trap_leak.md that prevent re-occurrence of known pitfalls.
Run-start batch timestamp (run-start, once)
Capture the run-start instant so the end-of-run gap-remediation phase (Phase 5.5) can scope its review to work shipped during THIS run. Run exactly once, before the Phase 4 monitor launches:
bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/run-batch-start.sh" --write
This writes the UTC ISO 8601 timestamp to ~/.autospec/.run-batch-start (idempotent within a run; pass --force only when intentionally starting a fresh batch window). Phase 5.5 reads it back via run-batch-start.sh --read, which yields BATCH_START_DATE.
Explore-on-drain counter reset (run-start, once)
Reset the per-repo explore-on-drain cycle counter so a fresh /autospec-run always starts with a clean slate (spec: "counter resets when the operator clears it or starts a fresh /autospec-run"). Run once, immediately after the batch-start timestamp above:
# Derive the canonical per-repo slug (owner__name) for state scoping.
_eod_slug="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null \
| sed 's#/#__#' \
|| printf '%s' "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" \
| tr '/' '_' | sed 's/^_//')"
rm -f "$HOME/.autospec/explore-on-drain/${_eod_slug}/cycles"
This ensures the cycle cap (AUTOSPEC_EXPLORE_ON_DRAIN_MAX_CYCLES, default 3) counts only chains within the current run, not across multiple historical runs. The _eod_slug variable is reused in the ALL_DONE block below.
Single-instance session lock (run-start, once)
Acquire a per-session lock BEFORE launching the Phase 4 monitor so a single harness session never runs two concurrent monitors (which collide on the queue and on git branches). The lock is keyed by the harness session id, so separate sessions still run independently by design.
LOCK="${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-run-session-lock.sh"
if ! bash "$LOCK" acquire --repo "{repo}"; then
# exit 3 = a monitor is already active in THIS session.
echo "autospec-run: refusing to start a second monitor in this session." >&2
bash "$LOCK" status
# STOP — do NOT launch Phase 4. Halt the active monitor with /autospec-stop,
# or re-run the lock helper with --force to override a stale lock.
exit 0
fi
Release the lock on EVERY terminal exit of this run — the Phase 6 final report,
the ALL_DONE shutdown, and stop mode:
bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-run-session-lock.sh" release
The lock is advisory and self-scoped: a crashed session's lock only blocks that
same session id (a fresh session gets a new id), and --force overrides it.
Phase 4 — Background autonomous monitor
Fresh-subagent-per-issue (canonical Phase 4 path, formerly single-agent absorbed-discipline). Each issue is processed by a FRESH top-level subagent dispatched by the orchestrator — expand → implement → finalize → peer-review → evaluate-findings → PR → merge — in that subagent's own context. The orchestrator NEVER implements in its own context; it only claims, dispatches, and waits. Each subagent receives full tool access because it is a top-level Agent call launched directly by the main session orchestrator. Constraint: Subagents spawned by background Agent calls do NOT inherit the Agent tool, so nested monitors cannot dispatch inner implementers. Phase 4 implementers must be top-level agents launched directly by the main session orchestrator, not nested inside a monitor. The process(ISSUE) notation below is shorthand for "dispatch a fresh top-level subagent to do this work", NOT in-context implementation by the orchestrator. Batch>1 is an explicit operator opt-in via AUTOSPEC_BATCH_SIZE=N; the default is 1 (one issue per subagent). The reasoning:deep force-to-1 rule is retained: deep issues stay batch=1 even under an operator AUTOSPEC_BATCH_SIZE=N override.
Record this durable preference in AGENTS.md (idempotent — skip if already present):
Auto-merge authority for auto-implement PRs. Admin-merge auto-implement PRs (through
autospec-guarded-merge.sh, never a baregh pr merge) when (a) the full target-repo validation/test suite has passed locally after the branch is current withmain, (b) all non-advisory required CI checks pass — checks matchingAUTOSPEC_PR_ADVISORY_CHECKS(defaultAUTOSPEC_MAIN_HEALTH_IGNORE_CHECKS; e.g. self-hosted TeamCity) are advisory and may be pending or failing once the full local suite is green, (c) the self-review subagent returnedLGTM, (d) PR closes anauto-implementissue from afeat/*branch.
Off-peak tip: For queues of 10+ issues (8+ hour runs), consider launching at night or on weekends. Usage limits are shared across all sessions — running long batches off-peak preserves daytime tokens for interactive work.
Usage-limit recovery guard. Before launching the monitor, determine the exact non-interactive command that can relaunch this same /autospec-run invocation in the current repo and store it in AUTOSPEC_RESUME_COMMAND if the harness has not already set one. This must be a real shell command, for example the same Claude Code, Codex CLI, OpenCode, tmux, or wrapper command the operator used to start the run with the same --profile and repo path. Do not ask the user for it during a running monitor.
Durable run registry (crash-resume). At monitor launch, also persist that same relaunch command to the durable run registry so it survives a host/session crash and reboot — the session env var AUTOSPEC_RESUME_COMMAND does not. Write it once per launch with the identical command:
REGISTRY="${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-run-registry.sh"
if [ -f "$REGISTRY" ] && [ -n "${AUTOSPEC_RESUME_COMMAND:-}" ]; then
bash "$REGISTRY" write --repo "$(gh repo view --json nameWithOwner --jq .nameWithOwner)" \
--repo-dir "$(pwd)" --harness "<claude|codex|opencode>" --command "$AUTOSPEC_RESUME_COMMAND"
fi
/autospec-resume reads ~/.autospec/active-runs/<repo-slug>.json on a fresh start to relaunch this run. The registry is the only durable carrier of the relaunch command across reboot.
When a harness reports a deterministic usage-limit/quota/capacity pause with a known reset time or wait duration, do not spend tokens diagnosing the message. Immediately arm the shell supervisor and exit:
USAGE_LIMIT="${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-usage-limit.sh"
if [ -n "${AUTOSPEC_USAGE_LIMIT_RESUME_AT:-}" ]; then
bash "$USAGE_LIMIT" arm --harness "<claude|codex|opencode>" --repo-dir "$(pwd)" \
--command "$AUTOSPEC_RESUME_COMMAND" --resume-at "$AUTOSPEC_USAGE_LIMIT_RESUME_AT"
exit 0
elif [ -n "${AUTOSPEC_USAGE_LIMIT_WAIT_SECONDS:-}" ]; then
bash "$USAGE_LIMIT" arm --harness "<claude|codex|opencode>" --repo-dir "$(pwd)" \
--command "$AUTOSPEC_RESUME_COMMAND" --wait-seconds "$AUTOSPEC_USAGE_LIMIT_WAIT_SECONDS"
exit 0
fi
The helper writes ~/.autospec/usage-limits/<run-id>.json, starts a background daemon, polls every 300 seconds by default, and relaunches the recorded command after the reset time. This is intentionally shell-only so recovery does not require an LLM turn after the usage limit has already been hit.
Then launch a background monitor loop — the orchestrator relaunches the monitor with fresh context after each batch of AUTOSPEC_BATCH_SIZE issues (default: 1). The monitor is stateless: all persistent state lives in GitHub labels and heartbeat files, so relaunches are always safe.
Codex Wait call-shape contract: When awaiting a native Codex wait_agent call, omit timeout_ms or pass an integer greater than or equal to 10000. Never pass timeout_ms below 10000; the native router rejects it before the executor can report progress. Keep waiting through task notifications until the monitor reaches a terminal state.
batch_num=1
while true:
launch background subagent paused before its first claim (pass batch_num; AUTOSPEC_BATCH_SIZE=${AUTOSPEC_BATCH_SIZE:-1})
capture the returned ACTUAL_SESSION_ID and bind it into that child's durable session context as WAIT_TARGET_SESSION_ID
do not allow the child to claim work until it can pass that exact WAIT_TARGET_SESSION_ID to `autospec claim acquire --session-id`
wait for task-notification (monitor agent completes)
if Wait returns `write_stdin failed` with `stdin is closed`:
inspect durable agent/session state once for a live recovery path
if the child is reported live:
attempt one harness reattach to the exact failed Wait target
if reattach succeeds: continue waiting for task-notification
explicitly terminate and reap the child through the harness process API
if termination and reap cannot be proven: stop without typed recovery or label mutation
require durable proof that the child exited or was terminated and reaped
use the actual session ID from the failed Wait target (never infer it from an environment variable)
read the immutable heartbeat binding for ACTUAL_SESSION_ID before inspecting any current claim state
BOUND_HEARTBEAT="$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/heartbeat-read.sh" --repo {repo} --session-id "<ACTUAL_SESSION_ID>")" or stop without typed recovery or label mutation
ISSUE="$(printf '%s' "$BOUND_HEARTBEAT" | jq -er '.issue | select((type == "string" and length > 0) or (type == "number" and . > 0)) | tostring')" or stop
BRANCH="$(printf '%s' "$BOUND_HEARTBEAT" | jq -er '.branch | select(type == "string")')" or stop
WORKER_ID="$(printf '%s' "$BOUND_HEARTBEAT" | jq -er '.worker_id | select(type == "string" and length > 0)')" or stop
CLAIM_ID="$(printf '%s' "$BOUND_HEARTBEAT" | jq -er '.claim_id | select(type == "string" and length > 0)')" or stop
if the binding is absent, malformed, or legacy/unbound: stop without typed recovery or label mutation
never read CLAIM_ID from the currently active claim after Wait fails; a successor may already own the same issue, worker, and branch
run `"${AUTOSPEC_BIN:-autospec}" autonomous implementer-wait-failed --repo {repo} --issue "<ISSUE>" --worker-id "<WORKER_ID>" --branch "<BRANCH>" --claim-id "<CLAIM_ID>" --session-id "<ACTUAL_SESSION_ID>" --diagnostic "<REDACTED_WAIT_ERROR>"`
if typed recovery exits non-zero: log the surfaced recovery failure and stop processing that issue
never mutate labels inline or overwrite a successor claim
continue # start a fresh monitor batch from the restored auto-implement queue
# Read and consume the batch-done signal.
if [ -f "$HOME/.autospec/batch-done.json" ]; then
status=$(jq -r .status "$HOME/.autospec/batch-done.json" 2>/dev/null || echo "BATCH_COMPLETE")
rm -f "$HOME/.autospec/batch-done.json"
else
status="BATCH_COMPLETE" # monitor crashed / overflowed — safe to relaunch
fi
if [ "$status" = "ALL_DONE" ]; then
# Queue drained — consult explore-on-drain.sh to decide whether to
# auto-chain into /autospec-explore or exit normally to Phase 6.
# The helper encapsulates: flag check → autonomy gate → dry-well guard
# → per-repo cycle-cap. It emits "chain" or "stop"; default is "stop".
# Pass AUTOSPEC_REPO so the helper scopes state to this repo without
# calling gh a second time (uses the same slug as the run-start reset).
_drain_decision=$(AUTOSPEC_REPO="${AUTOSPEC_REPO:-{repo}}" \
bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/explore-on-drain.sh")
if [ "$_drain_decision" = "chain" ]; then
echo "[orchestrator] explore-on-drain: chaining into /autospec-explore on sandbox branch"
# Record the PR merge watermark before explore starts so we can count
# only the PRs it ships (used by the dry-well sentinel below).
_eod_before="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
/autospec-explore # runs on its own sandbox branch, NEVER main
# Count PRs merged by this explore cycle and write the dry-well sentinel.
# explore-on-drain.sh reads this before the NEXT chain decision:
# if the count is 0 it emits "stop" (dry-well guard).
_eod_shipped="$(gh pr list --state merged \
--json mergedAt,labels \
--jq "[.[] | select(.mergedAt >= \"${_eod_before}\" and any(.labels[]; .name == \"autospec-explore\"))] | length" \
2>/dev/null || echo 1)"
mkdir -p "$HOME/.autospec/explore-on-drain/${_eod_slug}"
printf '%s\n' "${_eod_shipped}" \
> "$HOME/.autospec/explore-on-drain/${_eod_slug}/last-shipped"
fi
break # proceed to Phase 6 final report
fi
batch_num=$((batch_num + 1))
echo "[orchestrator] batch $((batch_num - 1)) complete — relaunching monitor with fresh context (batch ${batch_num})"
# continue immediately, no sleep
Continuation contract: BATCH_COMPLETE is a continuation signal, not a terminal state. reasoning:deep may reduce a single monitor batch to one issue, but the orchestrator MUST relaunch automatically until ALL_DONE. Never tell the operator to rerun /autospec-run after BATCH_COMPLETE; the current invocation owns the relaunch loop until the queue is empty or a hard blocker stops it.
Pass the following prompt verbatim to each background subagent:
You are the auto-implement monitor for
{repo}. Processauto-implementissues one at a time. Exit after processingAUTOSPEC_BATCH_SIZEissues (default: 1) by writing~/.autospec/batch-done.json— the orchestrator will relaunch you with fresh context.
Harness adaptation (loop persistence). The
while true:below is pseudocode. In Claude Code, use/looporScheduleWakeupto persist across turns. In Codex CLI and OpenCode, you lack a built-in loop primitive — implement persistence via one of these patterns:
- Shell wrapper (preferred): `exec bash << 'EOF' while true; do
... monitor logic ...
done EOF
— keeps a single bash process alive with your agent dispatching subcommands inside it. 2. **nohup background process:**nohup bash -c 'while true; do ...; sleep 1; done' > ~/.autospec/monitor.log 2>&1 &3. **tmux pane:**tmux new-window 'bash << '''HEREDOC''' while true; do ...; done HEREDOC'**Session batching:** Exit after processingAUTOSPEC_BATCH_SIZEissues (default 1) by writing~/.autospec/batch-done.jsonwithstatus=BATCH_COMPLETE. BATCH_COMPLETE is a continuation signal, not a terminal state; the orchestrator relaunches you with fresh context. When the queue is fully drained, writestatus=ALL_DONE` instead. This keeps each monitor session short to prevent context overflow.
Profile load (run-start, once). If
--profile <name>was passed, look it up in~/.autospec/model-profiles.yml; if<name>is not a key underprofiles:, exit non-zero and print the available names. If no flag was passed, load the file'sdefault:profile. If the file is missing, run auto-init and exit (per the Invocation section).Missing-label warning (run-start, once). Count open
auto-implementissues that lack either actx:*or areasoning:*label. If non-zero, printWARN: N issues lack model-fit labels (ctx:* / reasoning:*); they will be treated as ctx:64k, reasoning:medium. Run /autospec-classify to backfill.Exactly once at run start.Shared helper scripts. Helper scripts live at
${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}after installation. Do not assume the target repository has an autospecscripts/directory.Outer loop:
# Before the loop (run-once init): # batch_issue_count=0 # BATCH_SIZE="${AUTOSPEC_BATCH_SIZE:-1}" # [ "$BATCH_SIZE" -gt 0 ] 2>/dev/null || BATCH_SIZE=1 # guard against 0 or negative # rm -f "$HOME/.autospec/batch-done.json" # clear stale file from prior crash while true: deferred = [] # issues skipped because they exceed the active profile # Startup/per-scan heartbeat reconciliation — run before candidate selection. # This deletes closed/merged/orphaned heartbeats, rejects old schemas like # {"issue":407,"status":"in_progress"}, normalizes current schemas, and # releases any `claimed` heartbeat older than AUTOSPEC_WATCHDOG_CLAIMED_TIMEOUT_SECS (default: 1800). if [ -d "$HOME/.autospec/process-heartbeats" ]; then if command -v bash >/dev/null 2>&1; then bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-watchdog.sh" elif command -v pwsh >/dev/null 2>&1; then pwsh -NoProfile -ExecutionPolicy Bypass -File "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-watchdog.ps1" elif command -v powershell >/dev/null 2>&1; then powershell -NoProfile -ExecutionPolicy Bypass -File "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-watchdog.ps1" else echo "[watchdog] neither bash nor powershell found; skipping heartbeat reconciliation." fi fi
Backlog grooming preflight — run exactly one existing-backlog grooming
cycle before each queue scan when grooming policy is auto/on. The helper
invokes autonomous-promote-open-issues.sh --apply, but mutations are still
protected by the orchestrator's double gate: --apply AND grooming policy
in {auto,on}. This is no discovery: do not run Tier 2-4 discovery and do
not file new issues from this preflight.
GROOM_PREFLIGHT="${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/run-groom-preflight.sh" [ -x "$GROOM_PREFLIGHT" ] || GROOM_PREFLIGHT="skills/autospec-run/scripts/run-groom-preflight.sh" if [ -f "$GROOM_PREFLIGHT" ]; then bash "$GROOM_PREFLIGHT"
--repo "{repo}"
--report "${AUTOSPEC_RUN_REPORT:-$HOME/.autospec/autospec-run-report.md}" || true else echo "WARN: backlog grooming preflight helper missing; continuing drain" fi
Queue priority sort (autospec-review interlock)
When selecting the next auto-implement issue, sort:
- First: issues with label
priority:high(e.g.[REGRESSION]issues filed by autospec-review). Withinpriority:high, oldest first. - Then: all other
auto-implementissues, oldest first.
priority:high always wins over age. This guarantees regression
issues unblock the queue before continuing with normal feature work.
Fab implementer routing (label → gate)
Decide each claimed issue's implementer gate from its labels with the
fab-route.sh helper (resolved like the other coordinator scripts):
FAB_ROUTE="${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/fab-route.sh"
[ -x "$FAB_ROUTE" ] || FAB_ROUTE="skills/autospec-run/scripts/fab-route.sh"
GATE=$(gh issue view "$ISSUE" --json labels --jq '[.labels[].name] | join(",")' \
| bash "$FAB_ROUTE" --stdin)
GATE=fab— the issue carriesarea:faborautospec:fab-flow. Dispatch the fab implementer: its full-suite gate is clean regen (rm -rf build && .venv/bin/python src/generate.py) →stl-release-gate.pyon affected models (blocking geometry stages must pass; vision is advisory) → unittest, and the Primary smoke is the model's focused regression test.GATE=growth— the issue carriesgrowth:artifact. Keep the standard implementer, and add one content-quality gate to Phase 4 before the standard reviewer +growth-ethics+autospec-secauditgates: run${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/growth-content-quality-precheck.shon the changed content (deterministic pre-checks: keyword-density ceiling, FTC-disclosure presence, citation presence), then aTIER_Areviewer for E-E-A-T / brand-voice, wrapped in the standard 5-attempt adaptive-retry loop that feeds findings back as directives. A failing gate blocks merge (fail-closed); it never ships unreviewed growth content. This makes the gate fire for every path that reaches agrowth:artifactissue — the autonomous Tier-1 drain and/autospec-grow-runR1 alike.GATE=default— every other issue keeps the standard implementer + gate.
The branch is the only difference; the fab branch's gate prose lives in
prompts/phase4-implementer.md. Routing is a pure label decision (no substring
match: area:fabric stays default), so it is deterministic and testable.
Distributed coordinator selection
Before choosing ready[0], resolve the Rust control-plane binary:
AUTOSPEC_QUEUE_BIN="${AUTOSPEC_QUEUE_BIN:-${AUTOSPEC_BIN:-autospec}}"
If --coordination-status is active, run autospec queue ready --repo {repo} --batch-size "${AUTOSPEC_BATCH_SIZE:-1}", print the JSON, and exit. If
--max-parallel-safe is active, print only the .batch array and exit.
During the normal monitor loop:
Issue intent safety claim gate.
autospec claim acquirereads the current labels and issue body, validates the reviewed Safety review block, performs the label transition, and confirms the lowest-ID run-state comment. The monitor never reimplements that safety or lease transition withgh issue edit.
Portfolio admission and reconciliation gate. Before the label transition,
autospec claim acquirealso consults the typed portfolio graph and refuses the claim (no label mutation, no run-state comment) until the issue'sautospec:portfolio-itemmarker is bound to its verified primary delivery Project (kind: spec_portfolio) and every typed repository-local (Depends on issue #N) and cross-repository (Depends on <canonical-issue-url>) dependency is terminal-success in its own repository. Unavailable or contradictory predecessor state fails closed as Blocked, and an unbound or mismatched marker blocks the claim. Admission expands queue gating only; it does not change merge authority after a child is admitted. Reconciliation runs after each Phase 4 lifecycle boundary (claim, PR open, merge, terminal failure), after parent reconciliation, and during monitor sweeps (step 2 below), so the primary Project projection and dependency edges reflect the latest authoritative issue/PR/CI facts before the next admission decision. The monitor never reimplements Project binding or dependency admission withghcommands; a missing typed command fails the monitor start visibly.
- Reconcile parents whose children may have closed outside autospec before
selecting work:
_parent_slug=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/repo-slug.sh" --canonical "{repo}") export AUTOSPEC_PARENT_STATE_ROOT="${AUTOSPEC_PARENT_STATE_ROOT:-$HOME/.autospec/parent-state/$_parent_slug}" if ! "${AUTOSPEC_BIN:-autospec}" parent sweep --repo {repo}; then echo "[monitor] WARN: parent sweep failed; remote parent state is unknown and will be retried" >&2 fi - Run
autospec queue ready --repo {repo} --batch-size "$effective_batch_size"after watchdog reconciliation and profile filtering. - Reconcile delivery portfolios before admission: for each ready candidate
whose body carries an
autospec:portfolio-itemmarker, runautospec portfolio reconcile --portfolio <ID>(ID from the marker) so the typed graph and the primary Project projection reflect the latest authoritative issue/PR/CI facts. Candidates without a marker are outside the portfolio gate. A reconcile failure fails closed: the candidate is skipped this sweep and is never admitted on inferred dependency readiness. - Use
.ready[0].numberas the next issue candidate. - Claim it through
autospec claim acquire --issue "$ISSUE" --repo {repo} --worker-id "${AUTOSPEC_WORKER_ID:-<derived>}" --branch "<BRANCH>". The portfolio admission gate inside the claim refuses a candidate whose primary Project binding is missing or whose typed dependencies are not ready; a refusal surfaces as a non-success claim exit and takes the skip-and-refresh handling below. - Treat claim exit
2as a normal lost-race or conflict outcome: refresh the queue and try another candidate without failing the batch. - On failure, stop, or retry exhaustion, call
autospec claim releasebefore returningauto-implementto the queue.
Final safety-stamp contract
autospec queue review-safety is the only automatic writer of issue-intent
safety outcomes. After an admission surface persists its final issue body and
adds interim auto-implement, it must invoke the exact target:
autospec queue review-safety --repo {repo} --limit 1 --issue <N>
Only the command's pass: 1 total admits that invocation. The ready queue and
claim command consume Rust's typed passing evidence and refuse a missing,
ambiguous, or blocking result. They never reconstruct, stamp, or replace a
safety decision with gh commands or lint issue safety output.
The GitHub autospec-run-state comment written by the Rust control plane is the
cross-workstation source of truth. Local process heartbeat files remain useful
for same-host progress and compatibility, but they are not authoritative across
machines. If the Rust command is unavailable, fail the monitor start visibly;
do not fall back to an inline label-swap path.
Deterministic outer-loop decision: stop-sentinel check, ready-queue scan,
and ALL_DONE detection are handled by the monitor-outer-loop.sh script.
The monitor invokes it at the top of each iteration and follows the
emitted action. Issue details (title, URL, labels, body) are fetched
later via issue-snapshot.sh after the claim.
OUTER_LOOP_OUT="$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/monitor-outer-loop.sh"
--repo "{repo}" --batch-num "${batch_num:-1}" --batch-issue-count "$batch_issue_count")" ACTION="$(printf '%s' "$OUTER_LOOP_OUT" | awk '{print $1}' | cut -d= -f2)" case "$ACTION" in claim) ISSUE="$(printf '%s' "$OUTER_LOOP_OUT" | awk '{print $2}' | cut -d= -f2)" ;; sleep) REASON="$(printf '%s' "$OUTER_LOOP_OUT" | awk '{print $2}' | cut -d= -f2)" echo "[monitor] $REASON — sleeping 300s" sleep 300 continue ;; all_done) echo "[monitor] all issues processed — writing ALL_DONE and exiting" HARD SHUTDOWN — emit final report (incl. deferred summary, see Phase 6) ;; stop) MODE="$(printf '%s' "$OUTER_LOOP_OUT" | awk '{print $2}' | cut -d= -f2)" echo "[monitor] stop signal received: $MODE — exiting" HARD SHUTDOWN with final report exit 0 ;; esacService watch: heartbeat reconciliation already runs before each candidate scan; every 12 iterations also runs a cheap nudge/reclaim pass for long-lived workers.
monitor_tick=$((monitor_tick + 1)) if [ "$monitor_tick" -ge 12 ]; then monitor_tick=0 if [ -d "$HOME/.autospec/process-heartbeats" ]; then # Default reclaim window: 10800s (3h). For local single-threaded workers set # AUTOSPEC_WATCHDOG_RECLAIM_SECS=43200 (12h) before launch. export AUTOSPEC_WATCHDOG_RECLAIM_SECS="${AUTOSPEC_WATCHDOG_RECLAIM_SECS:-10800}" export AUTOSPEC_WATCHDOG_STALE_SECS="${AUTOSPEC_WATCHDOG_STALE_SECS:-1800}" # Cheap service wake-up pass: use low-cost model only. if command -v bash >/dev/null 2>&1; then # Dispatch one background watchdog helper (cheap model) to iterate stale entries. bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-watchdog.sh" elif command -v pwsh >/dev/null 2>&1; then # Windows fallback: PowerShell helper. pwsh -NoProfile -ExecutionPolicy Bypass -File "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-watchdog.ps1" elif command -v powershell >/dev/null 2>&1; then # Windows fallback: classic PowerShell fallback. powershell -NoProfile -ExecutionPolicy Bypass -File "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-watchdog.ps1" else echo "[watchdog] neither bash nor powershell found; skipping service-watch pass." fi fi fi
effective_batch_size probe — recomputed each outer-loop tick (not cached).
reasoning:deep may reduce a single monitor batch to one issue for fresh context.
This is not a run stop: the orchestrator MUST relaunch automatically until ALL_DONE.
_next_reasoning=$(gh issue view "${ready[0]}" --json labels
--jq '[.labels[].name | select(startswith("reasoning:"))] | first // "reasoning:medium"'
2>/dev/null || echo "reasoning:medium") if [ "$_next_reasoning" = "reasoning:deep" ]; then effective_batch_size=1 else effective_batch_size="${AUTOSPEC_BATCH_SIZE:-1}" [ "$effective_batch_size" -gt 0 ] 2>/dev/null || effective_batch_size=1 fi echo "[monitor] effective_batch_size=$effective_batch_size (next issue reasoning: $_next_reasoning)" ISSUE = ready[0]Security prerequisite pre-dispatch gate. Re-read the live body and labels
before claim/dispatch so a post-classification edit cannot bypass the
portfolio gate. A security child is one containing any of the generated
Evidence consumed / Controls covered / Prerequisites headings. Its
Prerequisites section must be absent, exactly
none, or contain onlybullet lines whose content begins
verified:. Anything else is removedfrom the ready queue and marked visibly; never dispatch it.
_security_body="$(gh issue view "$ISSUE" --repo {repo} --json body --jq .body)" if printf '%s\n' "$_security_body" | grep -qE '^## (Evidence consumed|Controls covered|Prerequisites)[[:space:]]$'; then _prerequisites="$(printf '%s\n' "$_security_body" | awk ' /^## Prerequisites[[:space:]]$/ {inside=1; next} /^## / && inside {exit} inside && NF {sub(/^[[:space:]]-[[:space:]]/, ""); print} ')" if [ -n "$_prerequisites" ] && [ "$_prerequisites" != "none" ]
&& printf '%s\n' "$_prerequisites" | grep -qv '^verified:'; then echo "code_health:security_prerequisite_blocked issue=$ISSUE" >&2 gh issue comment "$ISSUE" --repo {repo} --body
"code_health:security_prerequisite_blocked — every security prerequisite must be verified before dispatch." gh issue edit "$ISSUE" --repo {repo}
--remove-label auto-implement --add-label autospec:blocked-prerequisite ready = ready without ISSUE continue fi fiPortfolio admission gate (enforced inside claim acquire, never here): the
claim is refused before label mutation until the issue's
autospec:portfolio-item marker is bound to its verified primary delivery
Project and every typed in-repo / cross-repository dependency is
terminal-success. A refusal is a non-zero claim exit and takes the
skip-and-refresh path below; never re-implement binding or dependency
admission with gh commands.
Atomic claim: autospec claim acquire is the SOLE claim path. It performs the
check-and-swap (auto-implement -> in-progress-by-bot) atomically with a
read-back verification, so the hot loop NEVER re-implements the inline
label swap. Multiple monitors can race the same candidate; exactly one
wins (exit 0), the rest lose (non-zero) and skip to the next candidate.
claim_json="$("$AUTOSPEC_CLAIM_BIN" claim acquire --issue "$ISSUE" --repo {repo} --worker-id "${AUTOSPEC_WORKER_ID:-$(hostname):${USER:-unknown}:monitor:$$}" --branch "${BRANCH:-}" --session-id "${WAIT_TARGET_SESSION_ID:?missing durable Wait target session binding}")" && claim_rc=0 || claim_rc=$?
exit 1 = hard usage error (never a race signal); exit 2 = lost race.
Split them: a misconfigured claim (rc 1 or any other non-2 non-zero) must
surface an operator-visible WARN, not masquerade as a silently-dropped
lost race (rc 2). rc 0 falls through to ownership below.
if [ "$claim_rc" -eq 2 ]; then echo "[monitor] claim lost for #$ISSUE (rc=$claim_rc); refreshing queue" READY_REMOVE=$(printf "%s\n%s" "$READY_REMOVE" "$ISSUE" | grep -v "^${ISSUE}$" || true) ready=($READY_REMOVE) continue elif [ "$claim_rc" -ne 0 ]; then echo "[monitor] WARN: claim hard error for #$ISSUE (rc=$claim_rc) — usage/config error, NOT a lost race; skipping. Check autospec claim acquire invocation." >&2 READY_REMOVE=$(printf "%s\n%s" "$READY_REMOVE" "$ISSUE" | grep -v "^${ISSUE}$" || true) ready=($READY_REMOVE) continue fi
exit 0 only: this monitor now owns #$ISSUE. claim acquire has already
persisted both the per-issue heartbeat and immutable session sidecar.
CLAIM_ID="$(printf '%s' "$claim_json" | jq -er '.claim_id | select(type == "string" and length > 0)')" || exit 1 CLAIM_WORKER_ID="$(printf '%s' "$claim_json" | jq -er '.worker_id | select(type == "string" and length > 0)')" || exit 1 bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/heartbeat-read.sh" --repo "{repo}" --session-id "$WAIT_TARGET_SESSION_ID" >/dev/null || exit 1 bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/notify.sh" "autospec #$ISSUE: claimed" "Starting implementation on {repo}" || true
Issue start summary — print before dispatching process(ISSUE) so the operator
knows exactly what the monitor is about to work on.
Single API call — body+title+url+labels in ONE
gh issue view(D5: duplicate-read elimination).issue-snapshot.sh wraps that one call and caches the JSON per issue; later steps
(start summary, implementer/reviewer prompt assembly) reuse the file instead of
re-fetching. --refresh: labels may have transitioned since the previous run.
_issue_snapshot_file="$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/issue-snapshot.sh" get "$ISSUE" --refresh 2>/dev/null || true)" _issue_body_file="/tmp/issue-${ISSUE}-body.md" if [ -n "${_issue_snapshot_file:-}" ] && [ -f "${_issue_snapshot_file:-}" ]; then jq -r '.body // ""' "$_issue_snapshot_file" > "$_issue_body_file" ISSUE_TITLE=$(jq -r '.title // ""' "$_issue_snapshot_file") ISSUE_URL=$(jq -r '.url // ""' "$_issue_snapshot_file") ISSUE_LABELS=$(jq -r '[.labels[]?.name] | join(", ")' "$_issue_snapshot_file") else # fetch failed — fail open with empty metadata (same end state as a gh outage) : > "$_issue_body_file"; ISSUE_TITLE=""; ISSUE_URL=""; ISSUE_LABELS="" fi ISSUE_GOAL=$(awk ' BEGIN{in_goal=0} /^## Goal[[:space:]]$/ {in_goal=1; next} /^## / && in_goal {exit} in_goal && NF {print; exit} ' "$_issue_body_file") [ -n "$ISSUE_GOAL" ] || ISSUE_GOAL=$(awk 'NF && $0 !~ /^#/ {print; exit}' "$_issue_body_file") ISSUE_SMOKE=$(awk ' /### Primary smoke test/ {seen=1; next} seen && /^```/ {fence++; next} seen && fence==1 && NF && $0 !~ /^[[:space:]]#/ {print; exit} ' "$_issue_body_file") ISSUE_SCOPE=$(awk ' /^## Implementation outline[[:space:]]*$/ {in_scope=1; next} /^## / && in_scope {exit} in_scope && /^- / {gsub(/^- /,""); print; count++; if (count>=3) exit} ' "$_issue_body_file" | paste -sd '; ' -) echo "[monitor] starting #$ISSUE: ${ISSUE_TITLE:-<untitled>}" echo "[monitor] url: ${ISSUE_URL:-<unknown>}" echo "[monitor] labels: ${ISSUE_LABELS:-<none>}" echo "[monitor] goal: ${ISSUE_GOAL:-<not provided>}" echo "[monitor] smoke: ${ISSUE_SMOKE:-<not provided>}" echo "[monitor] scope: ${ISSUE_SCOPE:-<not provided>}" process(ISSUE) # single-agent absorbed-discipline — the monitor IS the implementer; see template below batch_issue_count=$((batch_issue_count + 1)) if [ "$batch_issue_count" -ge "${effective_batch_size:-$BATCH_SIZE}" ]; then bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/diary-write.sh"
--phase 4 --event monitor-exit
--body "Monitor BATCH_COMPLETE: batch=${batch_num:-1} processed=$batch_issue_count repo={repo}"
2>/dev/null || true printf '{"batch":%s,"processed":%s,"repo":"%s","ts":%s,"status":"BATCH_COMPLETE"}\n'
"${batch_num:-1}" "$batch_issue_count" "{repo}" "$(date -u +%s)"
> "$HOME/.autospec/batch-done.json" echo "[monitor] batch ${batch_num:-1}: processed $batch_issue_count/${effective_batch_size:-$BATCH_SIZE} issues — writing batch-done.json and exiting for fresh context" exit 0 fiImmediate next-issue pickup: NO SLEEP after process(ISSUE). Re-enter the top
of this loop immediately so the fresh queue scan can pick any issue unblocked
by the merge or failure cleanup that just completed.
### Parallel implementer worktree isolation Background `Agent` implementers in the same git workdir collide on `git checkout` even when their file-level scopes are disjoint — first agent to call `git checkout -b <BRANCH>` wins; the second lands on the wrong branch and must recover via cherry-pick. File-level disjointness is necessary but NOT sufficient for parallel safety; the collision is at the git-branch level. When dispatching multiple Phase 4 implementers in parallel, the orchestrator MUST wrap each dispatch via `dispatch-implementer.sh` (or an inline equivalent that produces the same contract): 1. Create a per-issue worktree at `/tmp/wt-<BRANCH>` off `origin/main` via `git worktree add -b <BRANCH> /tmp/wt-<BRANCH> origin/main`. 2. Pre-pend the implementer prompt with an explicit workdir directive naming `/tmp/wt-<BRANCH>` and forbidding `cd`/`git checkout` into the main checkout or sibling branches. 3. On agent completion, remove the worktree via `git worktree remove --force /tmp/wt-<BRANCH>` (defer cleanup until the PR has merged so the worktree stays available for retries). Sequential dispatch in the main checkout is the safe default; worktree-isolated parallel dispatch is the only safe parallelism. See `dispatch-implementer.sh` and `tests/autospec-run/test_parallel_dispatch.bats` for the canonical contract. ### Sandbox branch contract (autospec-explore PR-base integration) When `.autospec/explore-mode.json` is present (written by `explore-sandbox.sh`), the Phase 4 implementer MUST target the sandbox branch from that file's `branch` field as PR base instead of `main`, and MUST refuse `gh pr merge` against `main` while the file is present (refusal identifier: `code_health:explore_main_merge_refused`). The full contract lives in `skills/autospec-run/prompts/phase4-implementer.md` under "Sandbox branch contract" and "No accidental main merges"; the trio enforces it via lockstep. ### Implementer prompt selection (turbo-integration routing) Before dispatching, read the issue's labels: ```bash labels=$(gh issue view <ISSUE> --json labels --jq '[.labels[].name] | join(",")')
- If
labelscontainsautospec:v2-flow— load the prompt template fromskills/autospec-run/prompts/phase4-implementer.md(relative to this skill's install location, or viaAUTOSPEC_SKILLS_DIR/the harness's skill mount). That prompt embeds the absorbed-discipline path: expand → implement → finalize → peer-review (viacodex exec) → evaluate-findings. Wire the D3 cached prefix (spec Phase 2 child C): do NOT send thephase4-implementer.mdbody alone — prepend thegen-implementer-prompt.shstatic cached prefix so the default v2 path stops re-reading SKILL.md + AGENTS.md + the RULE_ID table uncached. Assemble the combined v2 prompt by handingphase4-implementer.mdto--body-file, which forwards it tobundle-static-context.sh --static-bodyso it lands INSIDE the cached prefix. It is one fixed template, so emitting it below the boundary re-sent it uncached on every dispatch and every retry:# Reuse the single body fetch written at process(ISSUE) start (D5). v2_combined_prompt=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/gen-implementer-prompt.sh" \ --issue-body "/tmp/issue-<ISSUE>-body.md" \ --branch "<BRANCH>" \ --issue-labels "<ISSUE_LABELS>" \ --repo "<REPO>" \ --body-file "skills/autospec-run/prompts/phase4-implementer.md")gen-implementer-prompt.shemits the static cached prefix (framed by<!-- CACHE BOUNDARY -->markers, containing SKILL.md + AGENTS.md + RULE_ID table + tag-filtered saved-memory) first; thephase4-implementer.mdbody and the issue assignment ride below it as the dynamic suffix. Pass the prefix block (up to and including the closing<!-- CACHE BOUNDARY -->) withcache_control: { type: "ephemeral" }so Anthropic's prompt cache reuses it across dispatches in the same monitor session. This only changes prompt assembly/caching — the implementer's absorbed-discipline BEHAVIOR (every step inphase4-implementer.md) is unchanged. UI/client-interaction issues handled by that prompt must record exactly one browser verification state in the PR body's## Validationsection:browser-verified,fallback-smoke-only, ornot-run. Harness-caused Browser connector skips that producefallback-smoke-onlyornot-runmust open or link a browser remediation issue with the redacted error detail before merge.- Otherwise — use the legacy inline prompt below (current behavior). Legacy path is retained until every pre-v2 issue has drained.
Both paths share the same outer monitor loop (queue scan, lock-step compliance, label-based locking, heartbeat updates, post-process pickup). The selection only changes the inner subagent prompt body.
process(ISSUE)is single-agent absorbed-discipline: the monitor agent performs the implementation work itself, in-context, using the prompt below as its working brief. There is NO nested subagent dispatch here — subagents spawned by backgroundAgentcalls do NOT inherit theAgenttool, so the monitor must own the work directly:Prompt construction (cache-prefix + dynamic suffix): Before dispatch, the orchestrator builds the subagent prompt. Two options:
Option A (recommended):
gen-implementer-prompt.sh— standalone assembler:# Reuse the single body fetch written at process(ISSUE) start (D5). combined_prompt=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/gen-implementer-prompt.sh" \ --issue-body "/tmp/issue-<ISSUE>-body.md" \ --branch "<BRANCH>" \ --issue-labels "<ISSUE_LABELS>" \ --repo "<REPO>")Option B (legacy):
bundle-and-dispatch.sh— wraps bundle-static-context internally:
Static cached prefix + dynamic suffix — call
bundle-and-dispatch.shto assemble the combined prompt:# Write dynamic suffix to a temp file first _suffix_file=$(mktemp -t autospec-suffix-XXXXXX.txt) trap 'rm -f "$_suffix_file"' EXIT printf '%s\n' "<ISSUE_BODY_AND_DIRECTIVES>" > "$_suffix_file" combined_prompt=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/bundle-and-dispatch.sh" \ --role implementer \ --issue-labels "<ISSUE_LABELS>" \ --dynamic-suffix-file "$_suffix_file")
bundle-and-dispatch.shcallsbundle-static-context.shinternally to emit the static cached prefix (framed by<!-- CACHE BOUNDARY -->markers, containing implementer-contract.md + the AGENTS.md quality-contract section with the RULE_ID table + lockstep rules + implementer scaffolding + the --static-body template), then appends the tag-filtered saved-memory and the dynamic suffix after the closing marker. The prefix does NOT hold SKILL.md itself, and saved-memory is deliberately below the marker because it is label-filtered and so cannot be byte-stable. Pass the prefix block (up to and including the closing<!-- CACHE BOUNDARY -->) withcache_control: { type: "ephemeral" }so Anthropic's prompt cache can reuse it across dispatches in the same monitor session (5-min TTL).Dynamic uncached suffix — appended by
bundle-and-dispatch.shafter the cached prefix: the issue body, per-iteration findings (if retry > 1), branch name, and "begin coding now".Resolve the implementer model (deterministic — do this before dispatching) — the issue's
reasoning:*label decides which profile implements it, and the profile carries the concrete model id. Ask the selector rather than deciding yourself:_impl_model=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/select-model-profile.sh" \ --labels "<ISSUE_LABELS>" \ --print-model) || _impl_model=""If
_impl_modelis non-empty, dispatch the implementer subagent with that model in place of the harness-detectedTIER_B. If the selector exits 3 (empty output — nomodel:key is resolvable, which is the case for an auto-initialised~/.autospec/model-profiles.yml), keepTIER_Bexactly as detected. Never invent a model id, and never widen this override:
- It applies to the implementer dispatch only. The reviewer keeps its own tier — a reviewer must never run on a weaker model than the implementer it is checking.
- The fall-back-up rule still holds: if the resolved model is unavailable at dispatch time (quota, capacity, authorization), silently retry the same dispatch at
TIER_B, thenTIER_A.The combined prompt sent to the subagent is:
**Model tier:** `TIER_B` (implementation work) — cheaper model with medium thinking; resolved at startup. Silently fall back to `TIER_A` if unavailable. **Hard limits.** Max 40 tool calls per issue. Max 3 self-review iterations. If you rewrite the same file twice with no test progress, abort: comment the blocker on the issue, release the lock label, exit. No wall-clock cap. Implement GitHub issue #<ISSUE>: "<TITLE>" on {repo}. Spec is the issue body below. ===ISSUE BODY=== <BODY> ===END=== ## Team personality as execution lens Read the issue body's **Team personality** section before choosing an approach. Let that team shape what you emphasize: a reliability/backend team should scrutinize operational safety and data boundaries, a frontend/product team should scrutinize user workflow and accessibility, a security-sensitive team should scrutinize trust boundaries and abuse cases, and so on. Do not invent extra scope; use the team personality to decide which risks deserve extra attention while still satisfying the issue's concrete acceptance criteria. Keep a progress heartbeat so the monitor can prove forward movement: - Create/update `~/.autospec/process-heartbeats/<repo-slug>/<ISSUE>.json` at each major step: - `claimed`, `expand_start`, `worktree_ready`, `tests_started`, `tests_passed`, `pr_created`, `smoke_retry`, `reviewed`, `merged`, `failed` - Recovery-bound schema: `{"issue":"<ISSUE>","branch":"<BRANCH>","step":"<STEP>","ts":<unix_epoch>,"pr":"<PR>","repo":"{repo}","host":"<HOST>","worker_id":"<WORKER_ID>","claim_id":"<CLAIM_ID>","session_id":"<WAIT_TARGET_SESSION_ID>"}`. `heartbeat-write.sh` creates the exact-session sidecar once: an identical identity refresh preserves it, while any session/issue/worker/branch/claim mismatch fails closed before updating liveness. Legacy heartbeats without that sidecar remain valid only for liveness and fail closed for Wait recovery. - Delete this file on terminal SUCCESS/FAILURE in both clean and failure paths. - Transition notifications: on `tests_passed`, `pr_created`, `merged`, and `failed` call `bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/notify.sh" "<title>" "<body>" || true` once per transition. Dedup with `_notify_fired=""` (reset per issue) and the pattern `case "$_notify_fired" in *:<STEP>:*) ;; *) _notify_fired="${_notify_fired}:<STEP>:"; bash ... || true ;; esac`. ## Project rules you MUST honor <verbatim concatenation of relevant feedback_*.md bodies — injected by bundle-static-context.sh --role implementer before dispatch> ## RULE_IDs (from AGENTS.md ## Implementation-quality contract) <verbatim RULE_ID table from AGENTS.md — injected by bundle-static-context.sh --role implementer> ## Acceptance criteria as constraints <verbatim AC checkbox list from issue body — every checkbox must be green before push> 0. **Heartbeat refresh at expand start.** The very first action before any expand work (reading files, pattern survey, verifying paths) is to refresh the heartbeat to `expand_start`. This covers the claim→worktree_ready window: the monitor wrote `claimed` when it dispatched you; without this refresh the watchdog may falsely reclaim the issue during a long expand phase. ```bash bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/heartbeat-write.sh" \ --issue "<ISSUE>" --branch "${BRANCH:-}" --step expand_start --repo "{repo}" \ --worker-id "$CLAIM_WORKER_ID" --claim-id "$CLAIM_ID" --session-id "$WAIT_TARGET_SESSION_ID"
- PR-aware recovery ladder, then worktree. Resolve the branch state FIRST, then act on the verdict. NEVER
cd/git checkout/git commitin the primary checkout — all work happens in a linked worktree offorigin/main. <!-- worktree-ladder:begin --><!-- worktree-ladder:end --> On thecd {repo_root} && git fetch origin LADDER=$(bash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/worktree-guard.sh resolve-branch --branch <BRANCH> --repo {repo}) STATE=$(printf '%s' "$LADDER" | sed -n 's/.*"state":"\([^"]*\)".*/\1/p') PR=$(printf '%s' "$LADDER" | sed -n 's/.*"pr":\([0-9]*\).*/\1/p') case "$STATE" in open-pr) # #886 recovery: a PR already exists. SKIP implementation entirely. # Check out the existing PR in a fresh worktree, run the issue's # verification (tests + validate.sh) and the standard review loop # against the EXISTING PR, then merge if green. Never re-implement. bash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/worktree-guard.sh create --branch <BRANCH> --path /tmp/wt-<BRANCH> --adopt cd /tmp/wt-<BRANCH> gh pr checkout "$PR" echo "[ladder] open-pr #$PR — skip implementation; verify + review + merge existing PR" ;; branch-only) # #917 recovery: the branch exists with un-PR'd work. Adopt it in a # fresh worktree and CONTINUE the remaining work (do not start over). bash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/worktree-guard.sh create --branch <BRANCH> --path /tmp/wt-<BRANCH> --adopt cd /tmp/wt-<BRANCH> echo "[ladder] branch-only — adopted <BRANCH>; continue remaining work" ;; fresh|*) # No branch, no PR: create a new worktree off origin/main. bash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/worktree-guard.sh create --branch <BRANCH> --path /tmp/wt-<BRANCH> cd /tmp/wt-<BRANCH> echo "[ladder] fresh — created <BRANCH>" ;; esac # Reset repo-local state roots after entering the issue worktree. Parent # autospec-run processes may export AUTOSPEC_REPO_DIR for the primary # checkout; premerge/validation helpers must read mutable artifacts such as # .autospec/qa-verdict.json from this active worktree instead. export AUTOSPEC_REPO_DIR="$PWD" # MANDATORY assert gate: MUST exit 0 before the first file edit/commit. A # non-zero exit (in_primary_checkout / dirty / stale_base / wrong_branch) is NEVER worked # around — comment the emitted code_health identifier on the issue, restore # the `auto-implement` label (swap `in-progress-by-bot` → `auto-implement`), # remove the heartbeat, and stop this issue. if ! bash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/worktree-guard.sh assert --expected-branch <BRANCH> --branch-pattern 'feat/*'; then gh issue comment <ISSUE> --body "worktree-guard assert failed (see code_health identifier above); restoring auto-implement" gh issue edit <ISSUE> --remove-label in-progress-by-bot --add-label auto-implement exit 1 fiopen-prpath the verification bar EQUALS fresh work — full tests + the standard review loop, never a blind merge. Cleanup is identical for every path: after the merge is confirmed (or on terminal failure),git worktree removethe linked worktree andgit worktree prune; never delete un-pushed work before merge. <!-- autospec-block:runtime-resource-preflight --> 1b. Claim the edit surface (claim-guard), nested inside the issue claim. After theworktree-guard.sh assertgate passes and BEFORE the first file edit, take a fine-grained lease on the skill(s)/paths this issue will touch so a concurrent session in another worktree cannot stomp the same trio+golden surface. This is the inner layer of the three-layer caller pattern (worktree-guard → claim-guard scan → claim-guard acquire); it composes with — and sits inside — the issue-level lease you already hold. SetTARGETSto the space-separated skill names and/or repo-relative paths the issue's Files touched lists. <!-- claim-guard-acquire:begin --><!-- claim-guard-acquire:end -->TARGETS="<space-separated skills/paths from the issue's Files touched>" bash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/worktree-guard.sh assert --expected-branch <BRANCH> --branch-pattern 'feat/*' || exit $? bash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/claim-guard.sh scan $TARGETS || true if ! AUTOSPEC_CLAIM_GUARD=strict bash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/claim-guard.sh acquire $TARGETS; then gh issue comment <ISSUE> --body "claim-guard acquire conflict (see code_health:claim_conflict identifier above); another live session owns this edit surface — restoring auto-implement" gh issue edit <ISSUE> --remove-label in-progress-by-bot --add-label auto-implement exit 6 fiscanis advisory (it warns on overlapping open PRs / worktrees / live claims but never blocks);acquireis all-or-nothing and exits6onclaim_conflict. Hold this lease across the whole edit+test+PR step. Refresh rides the existing heartbeat tick — at each heartbeat write also runbash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/claim-guard.sh refresh(no new loop) so a slow-but-live editor is never reclaimed. Release on PR open: immediately aftergh pr createsucceeds, runbash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/claim-guard.sh release $TARGETS(the lease has served its purpose once the work is captured in a PR).AUTOSPEC_CLAIM_GUARD=offor an unwritable store degrades the whole block to a no-op and NEVER blocks the issue.- TDD per AGENTS.md: failing test first → implement → refactor → commit. NO DB/external mocks. Follow file paths and signatures from the issue body verbatim.
- Full test suite gate. Run the target repo's full validation/test suite, not only the Primary smoke test. Command resolution order:
- If
AUTOSPEC_FULL_TEST_COMMANDis set, runbash -lc "$AUTOSPEC_FULL_TEST_COMMAND".- Else run every command listed under the issue's Operator/full verification section.
- Else run the repo-standard full suite:
autospec validatewhen present; otherwise use the ecosystem default (npm test,pytest,go test ./...,cargo test --no-fail-fast,mvn test, etc.). The suite must run every target to completion — cargo gets--no-fail-fast, pytest must NOT carry-x/--maxfail, andgo testmust NOT carry-failfast— or the pass/fail evidence is a truncated run. If the full suite fails, fix the failure, recommit, rerun the full suite, and repeat. Do NOT dispatch LGTM review while the full suite is failing. Do NOT rungh pr mergewhile the full suite is failing. Record the exact full-suite command and passing output summary in the PR comment or final report. Once the suite first passes, fire the transition notification:case "$_notify_fired" in *:tests_passed:*) ;; *) _notify_fired="${_notify_fired}:tests_passed:"; bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/notify.sh" "autospec #<ISSUE>: tests_passed" "Full suite green on {repo}" || true ;; esac3a. autospec-test gate (run whenskills/autospec-test/scripts/run-gate.shexists in the repo): invoke the gate against the PR's target repo root. Handle exit codes per spec §7a/§7b:Exit 0: proceed to merge. Exit 1: block PR (post comment + labels; do NOT merge; treat as review finding). Exit 2: halt entire batch (comment on issue, labelGATE_SCRIPT="skills/autospec-test/scripts/run-gate.sh" if [ -f "$GATE_SCRIPT" ] && [ -f ".autospec/test.yml" ]; then GATE_JSON_OUT=$(mktemp -t autospec-gate-XXXXXX.json) trap 'rm -f "$GATE_JSON_OUT"' EXIT bash "$GATE_SCRIPT" . --output-gate "$GATE_JSON_OUT" --pr "<PR>" --repo "{repo}" || GATE_EXIT=$? case "${GATE_EXIT:-0}" in 0) echo "[gate] autospec-test: passed" ;; 1) echo "[gate] autospec-test: blocked — PR comment posted, labels applied; continuing review loop" ;; 2) echo "[gate] autospec-test: fatal (exit 2) — halt batch"; exit 2 ;; esac fiin-progress-by-bot→auto-implement, exit monitor). 3b. <!-- docs-drift-gate:begin -->Docs drift gate
Run after autospec-test gate, before LGTM review. Skip if issue body contains a line matching
^docs:\s*skip\s*$(case-insensitive). Ondrift/missing_scope/example_stalethe classifier emits the pinnedregenerateaction carrying the affected scopes; the gate self-heals by invoking/autospec-doc(viadoc-orchestrator.mjs) scoped to ONLY those scopes, re-verifying the regenerated pages withverify-examples.mjs, and committingdocs: regenerate <scopes>onto the SAME PR branch. Doc generation NEVER blocks the code PR — generation/verification failure only warns, applies thedocs:failedlabel, and comments; the code review loop continues. Only the regenerate commit's own example verification gates the regenerated docs (failing pages are NOT committed):if ! grep -qiE '^docs:[[:space:]]*skip[[:space:]]*$' "/tmp/issue-<ISSUE>-body.md" 2>/dev/null; then SCRIPTS_DIR="${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}" DOC_SCRIPTS_DIR="${AUTOSPEC_DOC_SCRIPTS_DIR:-$HOME/.autospec/skills/autospec-doc/scripts}" DRIFT_JSON="$(bash "$SCRIPTS_DIR/check-doc-drift.sh" --pr "<PR>" 2>/tmp/drift-<PR>.err)"; drift_exit=$? if [ "$drift_exit" = "0" ]; then : # no drift — continue to LGTM else # drift_exit 1 (drift/example_stale) or 2 (missing_scope): classify, then # self-heal in-PR when the classifier emits the `regenerate` action. VERDICT_JSON="$(printf '%s' "$DRIFT_JSON" | node "$SCRIPTS_DIR/loop-classifier-docs-extension.mjs" --drift-json - --issue "<ISSUE>" --pr "<PR>" 2>/dev/null || true)" ACTION="$(printf '%s' "$VERDICT_JSON" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write((JSON.parse(s).action)||"")}catch{}})' 2>/dev/null || true)" if [ "$ACTION" = "regenerate" ]; then # Scopes the classifier flagged — always extract for labelling/reporting. mapfile -t SCOPES < <(printf '%s' "$VERDICT_JSON" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{for(const x of (JSON.parse(s).scopes||[]))console.log(x)}catch{}})' 2>/dev/null || true) gh issue edit <ISSUE> --add-label "docs:drift" 2>/dev/null || true # Resolve the auto_regenerate switch (D2 gate conditional). # Reads config from .autospec/autospec.yml, issue body from single-fetch file, and # AUTOSPEC_WITH_DOCS env. Precedence: docs:skip (already handled above) > # docs:generate > config auto_regenerate=true > AUTOSPEC_WITH_DOCS=1 > default-off. _REGEN=$(node --input-type=module <<'__REGEN_EOF__' 2>/dev/null || echo "0" import { resolveAutoRegenerate, loadConfig } from '${AUTOSPEC_DOC_SCRIPTS_DIR:-$HOME/.autospec/skills/autospec-doc/scripts}/doc-config.mjs'; import fs from 'node:fs'; const cfg = (() => { try { return loadConfig('.autospec/autospec.yml'); } catch { return {}; } })(); const body = (() => { try { return fs.readFileSync('/tmp/issue-<ISSUE>-body.md','utf8'); } catch { return ''; } })(); const flag = process.env.AUTOSPEC_WITH_DOCS === '1'; const { generate } = resolveAutoRegenerate({ config: cfg, issueBody: body, withDocsFlag: flag }); process.stdout.write(generate ? '1' : '0'); __REGEN_EOF__ ) if [ "${_REGEN:-0}" = "1" ]; then # auto_regenerate is ON — run the regenerate self-heal path. doc_ok=1 if node "$DOC_SCRIPTS_DIR/doc-orchestrator.mjs" 2>/tmp/docgen-<PR>.err; then # Re-verify regenerated pages; only verified pages are committed. if [ "${#SCOPES[@]}" -gt 0 ] && node "$DOC_SCRIPTS_DIR/verify-examples.mjs" "${SCOPES[@]}" 2>/tmp/docverify-<PR>.err; then if ! git diff --quiet -- "${SCOPES[@]}" 2>/dev/null; then git add -- "${SCOPES[@]}" git commit -m "docs: regenerate ${SCOPES[*]}" || doc_ok=0 git push || doc_ok=0 fi else doc_ok=0 # example verification failed — do NOT commit regenerated docs fi else doc_ok=0 # generation failed fi if [ "$doc_ok" = "0" ]; then gh issue edit <ISSUE> --add-label "docs:failed" 2>/dev/null || true gh pr comment <PR> --body "$(printf 'docs: regenerate self-heal failed (generation or example verification) — code PR NOT blocked. Scopes: %s' "${SCOPES[*]:-<none>}")" 2>/dev/null || true else gh pr comment <PR> --body "$(printf 'docs: regenerated %s in-PR (examples re-verified).' "${SCOPES[*]:-<none>}")" 2>/dev/null || true fi else # auto_regenerate is OFF — log and continue; detection/labels already applied. echo "docs: regeneration skipped (auto_regenerate=false)" fi else # No regenerate verdict — surface drift for operator review; do not block. gh pr comment <PR> --body "$(printf 'docs drift detected — no self-heal action emitted:\n\n```json\n%s\n```' "$DRIFT_JSON")" 2>/dev/null || true gh issue edit <ISSUE> --add-label "docs:drift" 2>/dev/null || true if [ "$drift_exit" = "2" ]; then gh issue edit <ISSUE> --add-label "docs:missing-scope" 2>/dev/null || true; fi fi fi else gh pr comment <PR> --body "docs: drift check skipped (docs:skip in issue body)" 2>/dev/null || true gh issue edit <ISSUE> --add-label "docs:skipped" 2>/dev/null || true fi<!-- docs-drift-gate:end --> 4. <!-- RETRY-LOOP:begin --> Adaptive commit loop (MAX_IMPL_RETRIES):
attempt=1 MAX_IMPL_RETRIES="${MAX_IMPL_RETRIES:-5}" directive_context="" while [ "$attempt" -le "$MAX_IMPL_RETRIES" ]; do # Conventional commits (feat:/fix:/test:/docs:/refactor:). NEVER bypass hooks. NEVER amend. if git commit -m "<conventional-commit-message>"; then # pre-commit hook passed — verify AC bats tests are green if bats tests/ac/issue-<ISSUE>.bats 2>/dev/null; then break # success fi # AC tests still failing — treat as lint failure, roll back commit git reset HEAD~1 fi # Capture lint directives for next attempt findings=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/lint-implementation.sh" --pre-commit --staged --directives 2>/dev/null || true) if [ -n "$findings" ]; then directive_context="${directive_context} ## Retry attempt ${attempt} findings ${findings} Fix these BEFORE the next code generation." fi attempt=$((attempt + 1)) done if [ "$attempt" -gt "$MAX_IMPL_RETRIES" ]; then gh issue comment <ISSUE> --body "Implementer hit max retries; manual intervention needed" gh issue edit <ISSUE> --remove-label "auto-implement-active" 2>/dev/null || true exit 1 fi<!-- RETRY-LOOP:end --> 4a. <!-- pr-size-pre-push:begin --> Deterministic PR_SIZE pre-push gate. Define this helper in the monitor shell and run it before any remote mutation:
# pr-size-helper:begin run_pr_size_gate() { # Takes no arguments. A harness substitutes its slash-command argument # into $1/$2/$3 when it renders this skill body, so reading positionals # here would let a caller's argument overwrite the gate's own inputs # (issue #3101). Inputs arrive as named variables set by the call site. if [ -z "${PR_SIZE_PHASE:-}" ] || [ -z "${PR_SIZE_BASE_OID:-}" ] \ || [ -z "${PR_SIZE_HEAD_OID:-}" ]; then printf 'ERROR:PR_SIZE: missing PR_SIZE_PHASE/PR_SIZE_BASE_OID/PR_SIZE_HEAD_OID\n' return 1 fi PR_SIZE_DIFF=$(mktemp -t autospec-pr-size-XXXXXX.diff) || return 1 git diff --binary "$PR_SIZE_BASE_OID" "$PR_SIZE_HEAD_OID" >"$PR_SIZE_DIFF" || { rm -f "$PR_SIZE_DIFF" return 1 } PR_SIZE_RC=0 PR_SIZE_OUTPUT=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/lint-implementation.sh" \ --diff-file "$PR_SIZE_DIFF" --issue <ISSUE>) || PR_SIZE_RC=$? rm -f "$PR_SIZE_DIFF" printf '%s\n' "$PR_SIZE_OUTPUT" if [ "$PR_SIZE_RC" -ne 0 ] || printf '%s\n' "$PR_SIZE_OUTPUT" | grep -q '^ERROR:PR_SIZE:'; then printf 'ERROR:PR_SIZE: %s rejected exact diff %s..%s\n' \ "$PR_SIZE_PHASE" "$PR_SIZE_BASE_OID" "$PR_SIZE_HEAD_OID" return 1 fi printf '%s\n' 'INFO:PR_SIZE: acceptance' } # pr-size-helper:end # pr-size-pre-push-exec:begin PR_SIZE_PHASE=pre-push PR_SIZE_BASE_OID=$(git merge-base "origin/${AUTOSPEC_BASE_BRANCH:-main}" HEAD) || exit 1 PR_SIZE_HEAD_OID=$(git rev-parse HEAD) || exit 1 PR_SIZE_EVIDENCE=$(run_pr_size_gate) || { printf '%s\n' "$PR_SIZE_EVIDENCE" exit 1 } printf '%s\n' "$PR_SIZE_EVIDENCE" printf '%s\n' "$PR_SIZE_EVIDENCE" | grep -qxF 'INFO:PR_SIZE: acceptance' || exit 1 # pr-size-pre-push-exec:endThe deterministic linter rejects the first over-limit values: 401 changed lines, 9 raw files, or 4 logical units. On rejection, preserve the branch and do not run
git push,gh pr create,gh pr ready, or any merge command. <!-- pr-size-pre-push:end --> 5. Push: git push -u origin <BRANCH># Stop-sentinel: abort if an immediate stop flag is present after this step. if ! bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-stop-check.sh" "$ISSUE" "$BRANCH" "$LAST_STEP"; then exit 0 fi
- PR: build the body deterministically instead of writing it out — the closing reference, the change list, and the verification line are all already in the branch, and restating them costs output tokens to reproduce what git holds:
Exit 3 from the composer means the commit range is empty: do NOT run# mktemp, not a fixed /tmp/...-<ISSUE> path: two implementers working the same # issue NUMBER in different repos would otherwise overwrite each other's body. PR_SUMMARY_FILE=$(mktemp -t autospec-pr-summary-XXXXXX) || exit 1 PR_BODY_FILE=$(mktemp -t autospec-pr-body-XXXXXX) || exit 1 # Write ONLY the summary — the part no template can derive: what this change # does and which alternative it rejected. Everything else is assembled. # A quoted heredoc, because the summary is MULTI-PARAGRAPH markdown: printf # '%s\n' would flatten it to one line, and any literal % in it is a format # string. 'EOF' is quoted so backticks and $ in the prose are not expanded. cat > "$PR_SUMMARY_FILE" <<'PR_SUMMARY_EOF' <summary> PR_SUMMARY_EOF bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/compose-pr-body.sh" \ --issue "<ISSUE>" --summary-file "$PR_SUMMARY_FILE" \ > "$PR_BODY_FILE" || exit 1gh pr create, and investigate why the branch has no commits. Then — BEFOREgh pr create— assert the composed body actually carries the closing keyword. A merged PR without it leaves its issue open: finished work becomes indistinguishable from pending work. A title-style(#<ISSUE>)reference is NOT equivalent; GitHub auto-closes only on the body keyword.Exit 4 means the body carries nobash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/compose-pr-body.sh" \ --assert-closes "$PR_BODY_FILE" || exit 1Closes #<ISSUE>keyword: do NOT rungh pr create. Then: gh pr create --base main --head <BRANCH> --title "<TITLE>" --body-file "$PR_BODY_FILE". Capture PR. Immediately after the PR opens, release the claim-guard lease taken in step 1a:bash ${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/claim-guard.sh release $TARGETS. Fire the transition notification:case "$_notify_fired" in *:pr_created:*) ;; *) _notify_fired="${_notify_fired}:pr_created:"; bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/notify.sh" "autospec #<ISSUE>: pr_created" "PR #<PR> opened on {repo}" || true ;; esacAfter the LLM subagent returns, record telemetry (tokens JSON written by the harness to.autospec/tokens-<ISSUE>.jsonif present):if [ -f ".autospec/tokens-<ISSUE>.json" ]; then bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/record-telemetry.sh" \ --dispatch-id "<DISPATCH_ID>" --role implementer --issue "<ISSUE>" \ --tokens-json ".autospec/tokens-<ISSUE>.json" fi- Inner loop (max 3 iterations):
# Stop-sentinel: abort if an immediate stop flag is present after this step. if ! bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-stop-check.sh" "$ISSUE" "$BRANCH" "$LAST_STEP"; then exit 0 fi
Run the Primary smoke test from the issue body. If it fails, fix and recommit before review.
Run the Full test suite gate before fused guardian + LGTM review. If it fails, fix and recommit, rerun the full suite, and do not dispatch review until the full suite passes.
Fused guardian + LGTM review (one subagent does both — saves one dispatch per inner-loop iteration): <!-- guardian-block:begin --> Run deterministic lint first (no subagent cost): rm -f /tmp/guardian-<PR>.md if [ "${AUTOSPEC_NO_GUARDIAN:-0}" != "1" ]; then bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/lint-implementation.sh" <PR> --issue <ISSUE> >> /tmp/guardian-<PR>.md 2>&1 fi det_exit=$?
Reuse lens (issues #1439/#1440/#1442): when armed, extract the reuse-triage RULE_IDs from the deterministic findings so the reviewer prompt receives the build-vs-buy block. Flag-OFF or no reuse hits →
_reuse_flags_filestays empty → the reviewer prompt is byte-identical to today (the${_reuse_flags_file:+…}expansion below adds nothing):_reuse_flags_file="" if [ "${AUTOSPEC_REUSE_LENS:-}" = "1" ] && [ -f /tmp/guardian-<PR>.md ]; then _reuse_candidate=$(mktemp -t autospec-reuse-flags-XXXXXX) grep -E '^(REINVENT_REPO_UTIL|NEW_DEP_UNJUSTIFIED|NEW_ABSTRACTION_SINGLE_CALLER):' \ /tmp/guardian-<PR>.md > "$_reuse_candidate" 2>/dev/null || true if [ -s "$_reuse_candidate" ]; then _reuse_flags_file="$_reuse_candidate"; fi fiModel tier:
TIER_Bfor ALL issues — includingregressionandpriority:high. The single fused reviewer carries the regression gap-check (see brief below), so no second Tier-A pass is dispatched. Escape hatch:AUTOSPEC_REVIEWER_TIERoverrides the reviewer tier — unset (or any value other thanopus) →TIER_B(sonnet); setAUTOSPEC_REVIEWER_TIER=opusto restoreTIER_Afor the reviewer. Silently fall back toTIER_AifTIER_Bis unavailable. For a fixed-role model binding, use a fresh configurableTIER_Aindependent reviewer rather than retrying the same unavailable binding. The replacement must receive the byte-equivalent assembled prompt and evidence, inspect the current head independently, and return the same byte-exactLGTM/findings contract. The orchestrator writes reviewer metadata (requested_model,actual_model,fallback_reason,head_sha) to a sidecar evidence artifact, never to the verdict channel. Availability fallback never converts substantive findings intoLGTM; if the replacement cannot run, block the merge.Assemble reviewer prompt — call
gen-reviewer-prompt.shto compose the combined prompt (static cached prefix + dynamic suffix):# Reuse the single body fetch from process(ISSUE) start (D5). # SHA-gated diff re-fetch: only re-fetch the PR diff when the branch head changed. _current_head=$(gh pr view <PR> --json headRefOid --jq .headRefOid 2>/dev/null || echo "") if [ -z "${_reviewer_pr_diff_file:-}" ] || [ "${_reviewer_last_head:-}" != "$_current_head" ]; then _reviewer_pr_diff_file=$(mktemp -t autospec-pr-diff-XXXXXX.diff) gh pr diff <PR> > "$_reviewer_pr_diff_file" _reviewer_last_head="$_current_head" fi combined_reviewer_prompt=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/gen-reviewer-prompt.sh" \ --pr-diff "$_reviewer_pr_diff_file" \ --issue-body "/tmp/issue-<ISSUE>-body.md" \ --prev-findings "/tmp/guardian-<PR>.md" \ --issue-labels "<ISSUE_LABELS>" \ --repo "<REPO>" \ ${_reuse_flags_file:+--reuse-flags "$_reuse_flags_file"})Pass
combined_reviewer_promptas the reviewer subagent prompt. The static cached prefix is framed by<!-- CACHE BOUNDARY -->markers; pass it withcache_control: { type: "ephemeral" }so Anthropic's prompt cache can reuse it across inner-loop iterations.Dispatch ONE foreground subagent with this brief:
You are the implementation reviewer for PR #<PR> on {repo}, closing issue #<ISSUE>.
Review counter-team as review lens
Read the issue body's Review counter-team section before reviewing. Review from that independent team's perspective and challenge likely blind spots from the implementation team's Team personality. Stay inside the issue scope: do not request unrelated rewrites, but do raise findings when the PR misses risks the counter-team was selected to notice.
Part 1 — Guardian (contract compliance) — skip if
AUTOSPEC_NO_GUARDIAN=1:
- Read AGENTS.md
## Implementation-quality contractfor the RULE_ID table and directive map.- Read issue #<ISSUE> body — note
## Implementation scope,## Implementation outline,## Tests required, and anyGuardian: skip-*lines.- Read deterministic findings in /tmp/guardian-<PR>.md (populated by lint-implementation.sh; may be empty if guardian disabled).
- Run
gh pr diff <PR>andgh pr view <PR> --json files,title,body.- Apply LLM-tier RULE_IDs (HALLUCINATED_API, DUPLICATE_CODE, STRING_MATCH_DOMAIN_LOGIC, REPEATED_STRUCTURE_AS_CODE, DOC_OUT_OF_SYNC semantic pass, INVENTED_CONFIG). For DUPLICATE_CODE, explicitly search for existing components, helpers, validators, API clients, request wrappers, error banners, fixtures, and test utilities before accepting new generated code. For STRING_MATCH_DOMAIN_LOGIC, flag any function that encodes domain meaning via substring-on-name checks when the file already imports a proper-representation library (Python rdkit/ast/urllib.parse/datetime/ipaddress/lxml/jsonschema; JS/TS URL/Date/AST/zod/ajv/joi; Go net/url/time/go/ast/net.ParseIP; Java java.net.URI/java.time/JavaParser/javax.validation; Scala java.net.URI/java.time/scalameta/refined/circe; Rust url::Url/chrono/time/syn/std::net::IpAddr/serde). For REPEATED_STRUCTURE_AS_CODE, flag any function whose body has ≥5 branches sharing identical structural shape (Python if/elif, Java/Scala switch/match, Rust match arms, Go switch cases, JS if/else) — direct the implementer to extract into a table + single dispatcher loop. For each new component, module, function, endpoint, worker, hook, or test helper, require a spec-linked reason, reuse check, public contract, proof test, and "what breaks if wrong" answer. Collect as
RULE_ID:<path>:<line>: <desc>. HonorGuardian: skip-*withINFO:lines.Part 2 — LGTM (correctness review): Using the same diff and issue body already in context: 6. Check correctness, edge cases, missing tests, AGENTS.md compliance (TDD, no mocks, conventional commits), whether every new code unit exists for a concrete issue/spec requirement rather than convenience, and whether deprecated routes, caches, buckets, stores, workers, config keys, UI paths, docs, specs, tests, or fixtures were removed instead of revived to make tests pass. 6a. For anything touching authentication, signing, encryption, or credential handling: ask "Is this crypto/dependency defending against a party who can actually reach this data path, or is it re-proving what the transport already proved?" If TLS to a named host, a process boundary, or an existing gate already establishes the property, flag the re-establishment. The reverse also holds: if a token could arrive by another route (forwarded by a client, read from a header), it must be verified properly, and that boundary must be pinned by a comment at the code. 7. Collect findings as a numbered list. 8. Critical self-question before LGTM: "What else could still pass here while the real user workflow fails, and how could this be better?" Check especially mocked-vs-deployed behavior, external service assumptions, fallback paths, user-visible outcomes, and missing no-mock smoke coverage. If the answer is actionable inside the issue scope, emit it as a finding or required test. <!-- guardian-block:end --> 8a. data-scope invariant lens (diagnostic/filter endpoints): When the issue touches endpoints, dashboards, reports, or diagnostics that accept optional job/sample/filter parameters, verify filters never widen to unrelated records. empty optional filters reject unless documented as a deliberate all-records mode. Require concrete evidence for
job-only,sample-only,job+sample,unsupported-filter, andempty-filterpaths; unsupported-filter and empty-filter cases must prove rejection or a documented scoped response, not silent broadening. 9. Regression gap-check (MANDATORY forregression/priority:highissues; skip otherwise): ask "would the reviewer have caught the original gap?" If the fused review as written would NOT have caught the gap this regression closes, add the missing checklist item(s) toreports/autospec-review/reviewer-lessons.md(one entry per item, with parentgap_idand date) and apply those new checks to this diff before issuing the verdict. This folds the former second-pass regression meta-review into this single reviewer pass — the reviewer-lessons write-path is preserved here; there is no second Tier-A dispatch.Hard limit: max 25 tool calls total (Parts 1 + 2 combined). If budget exhausted, append
RULE_ID:OUT_OF_SCOPE: reviewer budget exhausted; PR needs human reviewand proceed to verdict.Simplicity axis is ADVISE-only (anti-gold-plating): the reuse / build-vs-buy / "how could this be better?" axis may argue only toward less code — reuse a named existing util (
scripts/lib/, repo source), adopt a named library, or delete an unneeded abstraction — and only when tied to a named acceptance criterion. It may NEVER emit aBLOCKthat demands more code, a new abstraction, or speculative generality; such suggestions are at mostADVISEand never halt the commit. Every reuse verdict must name the matched util or library (evidence-bound), never assert a match from belief.Verdict: If Part 1 has ZERO blocking findings (INFO lines OK) AND Part 2 has no findings: return ONLY the token:
LGTM. Otherwise return a numbered findings list — RULE_ID findings first, then LGTM findings. A reuseBLOCKis provisional until it survives the refute pass below.Reuse-BLOCK refute pass (before consuming the verdict): If the findings list contains a build-vs-buy / reuse
BLOCK, do NOT halt on it yet. Dispatch a cheap refute pass — one shortTIER_Bsecond voter (≤5 tool calls) whose only job is to kill the BLOCK:rg-search the repo for the named util/library and confirm the claimed reuse target actually exists, is reachable, and fits this call site. Majority rules: keep the BLOCK only if the refuter also upholds it; if the refuter refutes it (the named target is absent, unreachable, or ill-fitting), demote that BLOCK toADVISE, drop it from the blocking findings, and continue. If demotion leaves no remaining blocking findings, treat the verdict asLGTM. This keeps a hallucinated "library exists" from stalling the merge (feedback_llm_validator_adaptive_retry). Record the outcome of this reuseBLOCKdecision to the reuse-lens ledger HERE (issue #1442) — at the decision point, so precision = upheld ÷ total is computed only over real reuse BLOCKs and never from phantom rows on clean passes. Set_reuse_block_raised=1,_reuse_triggerto the flagged RULE_ID, and_reuse_upheld=truewhen the refuter upheld the BLOCK or_reuse_upheld=falsewhen it was refuted/demoted-to-ADVISE, then: Draw the refuter from a different vendor than the proposer. Two dispatches to the same model family share failure modes and tend to be wrong together, which is the one case this second vote exists to catch — so resolve the refuter's vendor before dispatching it, passing the harness you detected in step 1 of Phase 0 as--proposer:REFUTE_VENDOR=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/verify-voter-vendor.sh" \ --proposer "<HARNESS>") || REFUTE_VENDOR=Run the refute pass on
$REFUTE_VENDOR's ownTIER_B— vendor is the independence lever, tier is the quality lever, and this changes only the former. An emptyREFUTE_VENDOR(exit 3: single-harness host, or every alternative already failed over) means keep the same-harnessTIER_Brefuter: a same-vendor second vote is weaker than a cross-vendor one but still better than none, and the script refuses to name the proposer's own vendor rather than report an independence the host cannot provide. On a quota failure, re-resolve rather than give up. If the refuter's dispatch fails with a 429 / quota / capacity error, call the script again adding--unavailable <that vendor>and dispatch to what it returns; a 429 is the only ground-truth quota signal available, sinceusage-observe.shreportsobservable=falsefor all three harnesses. Repeat until it exits 3, then fall back to the same-harness refuter.if [ "${AUTOSPEC_REUSE_LENS:-}" = "1" ] && [ "${_reuse_block_raised:-0}" = "1" ]; then bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/interrogation-ledger.sh" record \ --issue "<ISSUE>" --pr "<PR>" --trigger "${_reuse_trigger:-REINVENT_REPO_UTIL}" \ --verdict BLOCK --upheld "${_reuse_upheld:-true}" \ || true # write failure is best-effort; never blocks the PR fiIf
LGTM&& det_exit == 0: gh pr comment <PR> --body "<!-- guardian-block --> Review: clean. <!-- /-->" run Full test suite gate and record the exact full-suite command and passing output summary bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/ci-wait.sh" <PR> # fire-and-forget sentinel if [ -f ".autospec/tokens-<ISSUE>-reviewer.json" ]; then bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/record-telemetry.sh"
--dispatch-id "<DISPATCH_ID>-reviewer" --role reviewer --issue "<ISSUE>"
--tokens-json ".autospec/tokens-<ISSUE>-reviewer.json" fiReuse-lens verdict is recorded at the refute-pass decision point above
(issue #1442), not here — recording in this LGTM-only branch produced
phantom BLOCK rows on clean passes and never recorded upheld BLOCKs.
monitor exits to parking state HERE — orchestrator relaunches when ~/.autospec/ci-state/<PR>.signal settles
On relaunch: run ci-wait-poll.sh <PR>; break SUCCESS if exit 0 (pass)
break SUCCESS only if the full suite passed and required checks pass. If
LGTMbut det_exit != 0: Treat deterministic findings as blocking. Comment, fix, recommit, push. Continue inner loop. If findings list: gh pr comment <PR> --edit-last --body "<!-- guardian-block:begin -->\n## Review findings (iter <K>/3)\n<findings>\n<!-- guardian-block:end -->" Append findings to implementer retry context. Continue inner loop (counts toward 3-iter cap). On 3-iter exhaustion with non-LGTM: gh label create guardian-blocked --color e11d21 --force --repo {repo} gh issue edit <ISSUE> --add-label guardian-blocked Run failure cleanup (comment, swap label, close PR). rm -f /tmp/guardian-<PR>.md <!-- guardian-block:end -->Regression coverage for
regression/priority:highissues is handled inside the single fused reviewer brief (Part 2 item 9 above): the reviewer self-asks "would the reviewer have caught the original gap?", writes any missing checks toreports/autospec-review/reviewer-lessons.md, and applies them before its verdict. No second Tier-A dispatch.If LGTM: break SUCCESS.
# Stop-sentinel: abort if an immediate stop flag is present after this step. if ! bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-stop-check.sh" "$ISSUE" "$BRANCH" "$LAST_STEP"; then exit 0 fi- SUCCESS: Run the rebase-and-retest pre-merge gate before admin-merging. Addresses cross-session CI rot (issue #307): two PRs each green against pre-merge main can together break main, so we re-prove this PR against post-merge main. Cap is
AUTOSPEC_REBASE_MAX_ATTEMPTS(default 3). The gate ends with the admin-merge, so do NOT issue a second merge after the block.max_attempts="${AUTOSPEC_REBASE_MAX_ATTEMPTS:-3}" attempt=0 # Advisory checks (e.g. self-hosted TeamCity) are operator-declared via # AUTOSPEC_PR_ADVISORY_CHECKS, defaulting to the same regex the conductor's # main-health gate already honors (AUTOSPEC_MAIN_HEALTH_IGNORE_CHECKS) — # one shared definition. Unset/empty ("^$") matches no real check name, so # default behavior is unchanged: every FAILURE blocks. adv="${AUTOSPEC_PR_ADVISORY_CHECKS:-${AUTOSPEC_MAIN_HEALTH_IGNORE_CHECKS:-^$}}" wait_for_ci_green() { while :; do rollup=$(gh pr view <PR> --json statusCheckRollup --jq '.statusCheckRollup // []') pending=$(printf '%s' "$rollup" | jq --arg adv "$adv" '[.[] | select((((.name // .context // "") as $n | $n != "" and ($n | test($adv)))) | not) | select(.conclusion == null)] | length') bad=$(printf '%s' "$rollup" | jq --arg adv "$adv" '[.[] | select((((.name // .context // "") as $n | $n != "" and ($n | test($adv)))) | not) | select(.conclusion=="FAILURE" or .conclusion=="CANCELLED" or .conclusion=="TIMED_OUT" or .conclusion=="ACTION_REQUIRED")] | length') total=$(printf '%s' "$rollup" | jq 'length') if [ "$bad" != "0" ]; then # Distinguish inherited base-branch CI rot from branch-caused failures # before blocking. Capture the PR head rollup and the current base # commit's check/status contexts as merge evidence, then let # ci-status-compare.sh emit classification plus blocked_branch and # blocked_inherited arrays for the PR comment/final report. base_sha=$(gh pr view <PR> --json baseRefOid --jq .baseRefOid) head_checks="/tmp/autospec-ci-head-<PR>.json" base_checks="/tmp/autospec-ci-base-<PR>.json" base_check_runs="/tmp/autospec-ci-base-check-runs-<PR>.json" base_statuses="/tmp/autospec-ci-base-statuses-<PR>.json" compare_json="/tmp/autospec-ci-compare-<PR>.json" printf '%s\n' "$rollup" > "$head_checks" gh api "repos/{repo}/commits/$base_sha/check-runs" --paginate \ --jq '[.check_runs[] | {name, conclusion, status, detailsUrl: .details_url}]' \ > "$base_check_runs" gh api "repos/{repo}/commits/$base_sha/status" \ --jq '[.statuses[] | {context, state, targetUrl: .target_url}]' \ > "$base_statuses" jq -s 'add' "$base_check_runs" "$base_statuses" > "$base_checks" bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/ci-status-compare.sh" \ --head "$head_checks" --base "$base_checks" > "$compare_json" classification=$(jq -r '.classification' "$compare_json") target_url=$(jq -r '.target_url // ""' "$compare_json") case "$classification" in inherited) gh issue comment <ISSUE> --body "PR #<PR>: required checks are red, but ci-status-compare classified them as inherited base-branch CI rot (blocked_inherited). Merge evidence: ${target_url:-see attached rollup}; compare artifact: \`$compare_json\`. Pausing for operator review instead of marking the branch broken." exit 1 ;; branch_caused) gh issue comment <ISSUE> --body "PR #<PR>: required checks failed and ci-status-compare classified them as branch-caused (blocked_branch). Merge evidence: ${target_url:-see attached rollup}; compare artifact: \`$compare_json\`." exit 1 ;; *) gh issue comment <ISSUE> --body "PR #<PR>: required checks looked bad but ci-status-compare returned \`$classification\`; pausing for operator review. Compare artifact: \`$compare_json\`." exit 1 ;; esac fi if [ "$total" != "0" ] && [ "$pending" = "0" ]; then return 0; fi sleep 30 done } while [ "$attempt" -lt "$max_attempts" ]; do state=$(gh pr view <PR> --json mergeStateStatus --jq .mergeStateStatus) # mergeStateStatus values: CLEAN | BEHIND | BLOCKED | DIRTY | HAS_HOOKS | UNKNOWN | UNSTABLE case "$state" in CLEAN|HAS_HOOKS|UNSTABLE) break ;; BEHIND) if ! gh pr update-branch <PR>; then gh issue comment <ISSUE> --body "PR #<PR>: \`gh pr update-branch\` failed (auth/api/conflict). Pausing for operator review." exit 1 fi wait_for_ci_green ;; DIRTY) gh issue comment <ISSUE> --body "PR #<PR> has a merge conflict against main; needs human resolution." exit 1 ;; BLOCKED) sleep 30; wait_for_ci_green ;; *) sleep 15 ;; esac attempt=$((attempt + 1)) done if [ "$attempt" -ge "$max_attempts" ]; then gh issue comment <ISSUE> --body "PR #<PR>: rebase-and-retest stalled after $max_attempts attempts; main is moving faster than CI completes. Pausing for operator review." exit 1 fi # Mandatory final local proof after the branch is current with main. # Run the **Full test suite gate** here using the same command resolution order # (`AUTOSPEC_FULL_TEST_COMMAND`, Operator/full verification, then `autospec validate` fallback). # If it fails, fix the failure, recommit, push, rerun the full suite and review, and do NOT run `gh pr merge`. # # Final quality gate (pre-merge, fail-closed): after the full suite passes and # before admin-merging, discover repository-specific whole-workspace quality # commands and run each one from the target repo root. Discovery covers every # language whose marker is present, not only Rust: # 1. If AUTOSPEC_FINAL_QUALITY_COMMAND is set it OVERRIDES discovery: that # command is the only one run, and no marker is inspected. # 2. Otherwise `discover-quality-commands.sh` emits one `marker<TAB>command` # line per present marker - Cargo.toml -> `cargo clippy --workspace --all-targets -- -D warnings`, # `*.sh` -> shellcheck, package.json -> `npm run lint`, pyproject.toml -> # `ruff check`, go.mod -> `go vet ./... && golangci-lint run`. # 3. A marker present whose linter is NOT installed is never a silent pass: # the gate fails closed with rule=<language>-unavailable before running anything. # Treat every discovered command as required merge evidence. Do NOT run `gh pr merge` while the final quality gate is failing. # On failure, post/comment a # `FINAL_QUALITY_GATE_FAILED` block that includes the command plus one finding # record with `crate`, `file`, `line`, and `rule` fields (use `unknown` only when # the tool output genuinely omits a field), then return to the fix/recommit/retry # loop and rerun the full suite plus final quality gate before merge. _QG="${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/discover-quality-commands.sh" # Fail closed first: a present marker whose linter is absent blocks the merge. bash "$_QG" --missing-tools > /tmp/autospec-quality-missing.txt if [ -s /tmp/autospec-quality-missing.txt ]; then IFS="$(printf '\t')" read -r _marker _cmd _lang _tool < /tmp/autospec-quality-missing.txt gh issue comment <ISSUE> --body "FINAL_QUALITY_GATE_FAILED command=$_cmd crate=unknown file=$_marker line=1 rule=${_lang}-unavailable tool=$_tool" exit 1 fi # Run every discovered command from the repo root. Cargo.toml is skipped in this # loop because the Rust-specialized block below runs clippy exactly once and # parses its diagnostics into crate/file/line/rule. bash "$_QG" > /tmp/autospec-quality-commands.txt while IFS="$(printf '\t')" read -r _marker _cmd; do if [ "$_marker" = "Cargo.toml" ]; then continue; fi sh -lc "$_cmd" || { gh issue comment <ISSUE> --body "FINAL_QUALITY_GATE_FAILED command=$_cmd crate=unknown file=$_marker line=unknown rule=quality-command-failed" exit 1 } done < /tmp/autospec-quality-commands.txt # The Rust marker keeps its specialized evidence path; the override skips it too. if [ -z "${AUTOSPEC_FINAL_QUALITY_COMMAND:-}" ]; then if [ -f Cargo.toml ]; then if ! command -v cargo >/dev/null 2>&1; then gh issue comment <ISSUE> --body "FINAL_QUALITY_GATE_FAILED command=cargo-metadata crate=unknown file=Cargo.toml line=1 rule=cargo-unavailable" exit 1 fi if ! cargo metadata --no-deps --format-version=1 >/tmp/autospec-cargo-metadata.json 2>/tmp/autospec-cargo-metadata.err; then _metadata_err=$(tr '
' ' ' </tmp/autospec-cargo-metadata.err | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//' | cut -c1-500)
gh issue comment <ISSUE> --body "FINAL_QUALITY_GATE_FAILED command=cargo-metadata crate=unknown file=Cargo.toml line=1 rule=${_metadata_err:-metadata-failed}" exit 1 fi if ! cargo clippy --workspace --all-targets -- -D warnings >/tmp/autospec-final-quality-clippy.log 2>&1; then # Preserve raw clippy output and summarize the first diagnostic in a stable schema for reviewers. _crate=$(jq -r '.packages[0].name // "unknown"' /tmp/autospec-cargo-metadata.json 2>/dev/null || printf 'unknown') _location=$(grep -m1 -E '^[[:space:]]*-->' /tmp/autospec-final-quality-clippy.log | sed -E 's/^[[:space:]]*-->[[:space:]]*//' || true) _file=$(printf '%s' "$_location" | awk -F: '{print $1}') _line=$(printf '%s' "$_location" | awk -F: '{print $2}') _rule=$(grep -m1 -Eo 'clippy::[A-Za-z0-9_]+' /tmp/autospec-final-quality-clippy.log || true) if [ -z "$_rule" ]; then _rule=$(grep -m1 -E 'warning:|error:' /tmp/autospec-final-quality-clippy.log | sed 's/^ *//' | cut -c1-200 || true); fi gh issue comment <ISSUE> --body "FINAL_QUALITY_GATE_FAILED command=cargo-clippy crate=${_crate:-unknown} file=${_file:-unknown} line=${_line:-unknown} rule=${_rule:-unknown}" exit 1 fifi fi <!-- pr-size-final-merge:begin -->
Query GitHub after update-branch, review, and final local proof. The live PR
endpoints are authoritative; stale local OIDs can never create acceptance.
If the shell boundary discarded the helper, redefine it exactly as in step 4a.
pr-size-final-merge-exec:begin
PR_SIZE_REMOTE_OIDS=$(gh pr view <PR> --json baseRefOid,headRefOid
--jq '[.baseRefOid, .headRefOid] | @tsv') || exit 1 [ "$(printf '%s\n' "$PR_SIZE_REMOTE_OIDS" | awk -F '\t'
'NF == 2 && $1 != "" && $2 != "" { print "valid" }')" = "valid" ] || exit 1 PR_SIZE_BASE_OID=$(printf '%s\n' "$PR_SIZE_REMOTE_OIDS" | cut -f1) PR_SIZE_HEAD_OID=$(printf '%s\n' "$PR_SIZE_REMOTE_OIDS" | cut -f2) git fetch --no-tags origin
"+refs/heads/${AUTOSPEC_BASE_BRANCH:-main}:refs/remotes/origin/${AUTOSPEC_BASE_BRANCH:-main}"
"+refs/heads/<BRANCH>:refs/remotes/origin/<BRANCH>" || exit 1 git cat-file -e "${PR_SIZE_BASE_OID}^{commit}" || exit 1 git cat-file -e "${PR_SIZE_HEAD_OID}^{commit}" || exit 1 [ "$(git rev-parse "origin/${AUTOSPEC_BASE_BRANCH:-main}")" = "$PR_SIZE_BASE_OID" ] || exit 1 [ "$(git rev-parse "origin/<BRANCH>")" = "$PR_SIZE_HEAD_OID" ] || exit 1 [ "$(git rev-parse HEAD)" = "$PR_SIZE_HEAD_OID" ] || exit 1 PR_SIZE_PHASE=final-pre-merge PR_SIZE_EVIDENCE=$(run_pr_size_gate) || { printf '%s\n' "$PR_SIZE_EVIDENCE" exit 1 } printf '%s\n' "$PR_SIZE_EVIDENCE"Reviewer evidence is accepted only as this complete line; prefixes,
suffixes, summaries, and inferred approval are not acceptance.
printf '%s\n' "$PR_SIZE_EVIDENCE" | grep -qxF 'INFO:PR_SIZE: acceptance' || exit 1
pr-size-final-merge-exec:end
<!-- pr-size-final-merge:end -->
Blast-radius domain fence at the merge chokepoint (issue #1732). The guarded-merge
wrapper classifies the PR's ACTUAL changed files against the repo's fenced_surfaces
registry and refuses to merge a fenced-surface diff (the wrapper applies the
human-review quarantine label and comments) unless the PR carries the
autospec:fenced-approvedoverride label; otherwiseit performs the same admin squash-merge. Call it INSTEAD of a bare
gh pr merge --adminso "merge without the fence check" requires deliberately bypassing the wrapper.
exit 0 = merged (allowed/overridden); 1 = quarantined (NOT merged); 3 = review quorum refused (NOT merged); 2 = fail-closed error.
This replaces the historical bare
gh pr merge <PR> --admin --squash --delete-branch.pr-size-guarded-merge-exec:begin
run_guarded_pr_size_merge() { bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-guarded-merge.sh"
--pr <PR> --repo {repo}
--merge-args "--admin --squash --delete-branch --match-head-commit $PR_SIZE_HEAD_OID" }pr-size-guarded-merge-exec:end
if run_guarded_pr_size_merge; then : else _gm_rc=$? if [ "$_gm_rc" -eq 1 ] || [ "$_gm_rc" -eq 3 ]; then "$AUTOSPEC_CLAIM_BIN" claim release --issue "<ISSUE>" --repo {repo} --worker-id "$CLAIM_WORKER_ID" --claim-id "$CLAIM_ID" --state needs-human --branch "<BRANCH>" --pr "<PR>" || true if [ "$_gm_rc" -eq 3 ]; then echo "[monitor] #<ISSUE> review quorum REFUSED (rc=3): needs review/quorum or an operator review_quorum policy; PR NOT merged, left needs-human" else echo "[monitor] #<ISSUE> quarantined by blast-radius fence: fenced surface, left needs-human; PR NOT merged" fi elif [ "$_gm_rc" -eq 4 ]; then echo "[monitor] #<ISSUE> review quorum could not be checked (rc=4, gh/transport failure): PR NOT merged; retry later" else echo "[monitor] #<ISSUE> guarded-merge fail-closed (rc=$_gm_rc): PR NOT merged; pausing for operator review" fi rm -f "/tmp/issue-<ISSUE>-body.md" || true exit 0 fi "$AUTOSPEC_CLAIM_BIN" claim release --issue "<ISSUE>" --repo {repo}
--worker-id "$CLAIM_WORKER_ID" --claim-id "$CLAIM_ID"
--state merged --branch "<BRANCH>" --pr "<PR>" || true _parent_slug=$(bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/repo-slug.sh" --canonical "{repo}") export AUTOSPEC_PARENT_STATE_ROOT="${AUTOSPEC_PARENT_STATE_ROOT:-$HOME/.autospec/parent-state/$_parent_slug}" if ! "$AUTOSPEC_CLAIM_BIN" parent reconcile-child --repo {repo} --child "<ISSUE>"; then gh issue comment "<ISSUE>" --repo {repo} --body "Parent reconciliation failed after merge; remote parent state is unknown and will be retried by the recurring parent sweep." echo "[monitor] WARN: parent reconciliation failed for merged child #<ISSUE>" >&2 fiPortfolio lifecycle boundary: a merged child is an authoritative fact
for the typed portfolio graph; the monitor sweep's portfolio
reconciliation (step 2) publishes it to the primary Project before the
next admission decision.
case "$_notify_fired" in :merged:) ;; *) _notify_fired="${_notify_fired}:merged:"; bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/notify.sh" "autospec #<ISSUE>: merged" "PR #<PR> merged on {repo}" || true ;; esac
The block ends with the admin-merge and merged-state claim release; merge auto-closes the issue. ```bash # Stop-sentinel: abort if an immediate stop flag is present after this step. if ! bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-stop-check.sh" "$ISSUE" "$BRANCH" "$LAST_STEP"; then exit 0 fi<!-- token-report:begin --> Post the per-issue token report (best-effort; never fails the run):
# Orchestrator writes .autospec/tokens-<ISSUE>.json from Agent-result usage # (harness-dependent, best-effort; absent fields → null, never blocking). bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/post-token-report.sh" \ --issue "<ISSUE>" --repo "<REPO>" \ --tokens-json ".autospec/tokens-<ISSUE>.json" || true<!-- token-report:end -->
Cleanup single-fetch body temp file on terminal success (D5).
rm -f "/tmp/issue-<ISSUE>-body.md" || true 9. FAILURE (loop exhausted): comment failure on issue, swap label
in-progress-by-bot→auto-implement,gh pr close <PR> --delete-branch. Fire the terminal failure notification:case "$_notify_fired" in *:failed:*) ;; *) _notify_fired="${_notify_fired}:failed:"; bash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/notify.sh" "autospec #<ISSUE>: failed" "Implementation failed on {repo}" || true ;; esacCleanup single-fetch body temp file on terminal failure:rm -f "/tmp/issue-<ISSUE>-body.md" || true10. Cleanup: runautospec runtime env down --repo /tmp/wt-<BRANCH> --mode "${AUTOSPEC_RUNTIME_MODE:-auto}" --purge-maven; then runbash "${AUTOSPEC_SCRIPTS_DIR:-$HOME/.autospec/scripts}/autospec-runtime-worktree-cleanup.sh" /tmp/wt-<BRANCH>; only after both succeed, runcd / && git -C {repo_root} worktree remove /tmp/wt-<BRANCH> --force. 11. Report: PR number, outcome, one-paragraph summary.Hard rules: NEVER push to main, force-push, or bypass hooks. Only
autospec parentmay update or close an umbrella issue. gh CLI only.Hard rules for the monitor: ONE issue at a time, sequential. Only `autospec parent` may update or close an umbrella issue. On transient gh errors retry once. Do NOT ask the user — auto-merge authority is granted in AGENTS.md. Final output when shutdown: numbered list of every processed issue with PR # and outcome.
Capture the agent ID / log path for monitoring.
If your harness lacks background delegation: open a separate terminal/tmux pane, run the monitor prompt in a fresh session there, and have it write progress to a logfile that Phase 5 can tail.
Running concurrent workers
autospec-run is designed for concurrent operation across separate harness sessions (e.g., two Claude Code sessions in different terminals, or two Codex CLI processes). Each session runs an independent monitor. Three layers enforce safety:
- Atomic claim —
autospec claim acquirecheck-and-swapsauto-implement → in-progress-by-botplus writes anautospec-run-stateGitHub comment. The GitHub comment is the cross-machine source of truth, not the local heartbeat. - Worktree isolation — each issue gets its own
/tmp/wt-<branch>so two workers never share a worktree. - Per-session lock — the session-lock (see above) ensures a single harness session never runs two concurrent monitors; separate sessions run independently by design.
To launch a second concurrent worker:
# Terminal 1 — first worker (already running)
AUTOSPEC_WORKER_ID="$(hostname):user:shell:$$" /autospec-run
# Terminal 2 — second worker (fresh session, distinct worker id)
AUTOSPEC_WORKER_ID="$(hostname):user:shell:$$" /autospec-run
Each session derives its own AUTOSPEC_WORKER_ID if not overridden; the default form is host:user:harness:pid. Set it explicitly when two sessions run on the same host to guarantee uniqueness.
Watchdog tuning for long concurrent runs. The watchdog cross-checks the GitHub run-state comment before releasing any claimed heartbeat, so a live sibling's claim is never reclaimed. The tunable env vars (set in every concurrent terminal):
| Variable | Default | Purpose |
|---|---|---|
AUTOSPEC_MAX_CONCURRENT_REPO_WORKERS |
0 (disabled) |
Repo-wide active-worker cap. When active in-progress-by-bot issues meet the cap, autospec queue ready returns an empty .batch while still reporting .ready. Use this to throttle one workstation or a cluster. |
AUTOSPEC_RUN_ONLY_ISSUES |
unset (unconstrained) | Space-separated issue-number allowlist. When set and non-empty, autospec queue ready scopes .ready/.blocked/.batch to only those issue numbers — set by the autonomous conductor's dispatch-time provenance split so the operator and self-originated batches each drain their own subset. Unset or empty keeps the full-queue scan. |
AUTOSPEC_CLAIM_LEASE_SECONDS |
10800 |
Cross-machine claim lease TTL written into the GitHub run-state comment and used by autospec claim acquire stale-reclaim decisions. |
AUTOSPEC_CLAIM_SETTLE_SECONDS |
0.2 |
Short post-upsert readback delay so simultaneous comment creates converge before a worker reports claim success. |
AUTOSPEC_CLAIM_CONFIRM_READS |
5 |
Number of settled lowest-lock readbacks required before autospec claim acquire reports claim success. |
AUTOSPEC_WATCHDOG_CLAIMED_TIMEOUT_SECS |
1800 |
Minimum age before a claimed heartbeat triggers the GitHub cross-check. Set higher (e.g., 3600) for hosts with slow worktree setup. |
AUTOSPEC_WATCHDOG_RECLAIM_SECS |
10800 |
Legacy fallback for claim lease age when AUTOSPEC_CLAIM_LEASE_SECONDS is unset; also used by watchdog stale cleanup. |
AUTOSPEC_WATCHDOG_STALE_SECS |
1800 |
Age at which a heartbeat is considered stale for nudging. |
If a cross-check GitHub API call fails (offline / rate-limited), the watchdog treats the claim as live and skips the reclaim — fail-safe by design.
Recommended starting caps:
| Deployment | AUTOSPEC_MAX_CONCURRENT_REPO_WORKERS |
AUTOSPEC_BATCH_SIZE |
AUTOSPEC_CLAIM_LEASE_SECONDS |
|---|---|---|---|
| 10 workers | 6 |
1 |
10800 |
| 25 workers | 12 |
1 |
10800 |
| 50 workers | 20 |
1 |
14400 |
Phase 5 — Periodic status updates
Set up a recurring check (every ~25 min) using your harness's self-paced wakeup capability. Each tick:
gh issue list --repo {repo} --label auto-implement,in-progress-by-bot,awaiting-merge --state all --json number,state,labels
gh pr list --repo {repo} --state all --json number,state,title --limit 20
Post a one-paragraph delta to the user (newly closed issues, newly merged PRs, failures, blockers). If two consecutive ticks have nothing new, slow to ~50 min cadence to reduce noise. Stop the loop when:
- the monitor agent reports completion, OR
- all child issues are CLOSED, OR
- the user explicitly stops.
If your harness lacks self-paced wakeup: register a local cron/launchd job that runs the same status-check prompt at the chosen cadence, OR ask the user to invoke status-update manually.
Resolving references/ paths. The reference pointers below are written relative to the autospec repo root, which is where the validation gate resolves them. Inside a target repo that path does not exist: resolve against this skill's own installed directory instead — ~/.claude/skills/autospec-run/references/, $CODEX_HOME/skills/autospec-run/references/, or $HOME/.autospec/skills/autospec-run/references/ — and fall back to the ~/.autospec/repo checkout. A pointer that will not resolve is a stop-and-report condition, never a skipped step.
Phase 6 — Final report
MUST read skills/autospec-run/references/end-of-run.md (section "Phase 6 — Final report") and follow it when the monitor terminates: it defines the Done challenge, the final summary, and the .autospec/run-summary.md write via autospec-write-run-summary.sh.
Phase 5.5 — End-of-run gap remediation
MUST read skills/autospec-run/references/end-of-run.md (section "Phase 5.5 — End-of-run gap remediation") and follow it after the last issue in the batch closes/merges (queue drains, ALL_DONE), before the final report: the bounded broad-review → file-survivors → converge loop plus the docs / security / fab-completeness dimensions, and the autospec-gap-miner.sh closeout invocation that mines run evidence into docs/memory/autospec-gap-ledger.md. Skip the whole phase when ~/.autospec/no-review.flag exists or --no-postreview was passed.
Phase 5.6 — Repo quality audit
MUST read skills/autospec-run/references/end-of-run.md (section "Phase 5.6 — Repo quality audit") and run repo-quality-audit.sh after Phase 5.5 converges or hits its cap, before Phase 6 writes the final run summary.
Advisor escalation
MUST read skills/autospec-run/references/end-of-run.md (section "Advisor escalation") before escalating any bounded hard decision to a TIER_A advisor (protocol, gates, and the reviewer gate).
Constraints (apply throughout)
- Cadence: sub-hour polling needs an in-session background subagent or a local cron. Cloud cron services (e.g. Anthropic remote routines) typically have a 1-hour minimum and are not appropriate.
- TDD non-negotiable per AGENTS.md. Real services in tests, no DB mocks.
- Branch-per-issue, conventional commits, no force-push, no hook bypass.
- Auto-merge on success per AGENTS.md. Do not ask.
- Context budget: stay under 60%. Delegate everything possible.
- Failure isolation: a failed issue restores its
auto-implementlabel so the next monitor cycle picks it up; it does not block the cascade unless dependent issues are downstream. - Fresh repo: if Phase 0 created the repo, the very first commit is the scaffold (incl.
AGENTS.md); the spec lands as the second commit; child branches branch off thatmain. - Small-LLM target: child issues are sized and pre-staged for 32B-class local LLMs (e.g. qwen3-32B / Qwen3-30B-A3B on Ollama, 48 GB-class Macs). Bias toward smaller issues, sectional spec anchors, pre-staged file pointers, checkbox AC, and one Primary smoke test.
Autonomous mode
/autospec-run is already autonomous (no operator gates during normal
operation). When invoked with --autonomous, or when
~/.autospec/autonomous.flag exists, the monitor:
- Tags run-state telemetry with
autonomous=trueso Phase 5.5 gap remediation and Phase 6 final reports can distinguish autonomous runs from interactive ones. - Honors the same safety guardrails as
/autospecand/autospec-define: destructive remote actions, out-of-scope file changes, and the token cost gate (AUTOSPEC_AUTONOMOUS_TOKEN_CAP) still surface confirmations viaautospec-autonomy-gate.sh --check all, exit 1 = ask anyway. - Does NOT add any additional user-facing gates. The flag is informational here — the run loop's existing "only ask on hard blocker" rule already matches autonomous semantics.
Pass --autonomous from /autospec-define's handoff (or set it
explicitly) to preserve cross-phase telemetry continuity. Existing
feedback_autospec_autonomy_scope.md rules remain in force.
04 同仓库更多 Skills
trio-pass-skill
暂无简介
duo-pass-skill
暂无简介
duo-divergence-skill
暂无简介
autospec
Use when the user wants to ship a feature end-to-end across multiple commits — bootstraps repo if missing, brainstorms a design spec, decomposes into linked GitHub issues, or splits an existing spec into issues, then runs an autonomous implementation loop with auto-merge.
autospec-upgrade
Use when the user wants to upgrade an Angular, Next.js, or React project to the latest official versions safely — by locking observable behavior before touching versions, upgrading one major at a time with official codemods, and gating completion on mutation score rather than coverage theater.
autospec-ui-audit
Use when a React Router application needs a deterministic whole-site route inventory before UI, UX, navigation, or evidence auditing begins.