Files
gstack/docs/autoplan-guard-troubleshooting.md
Garry Tanandcapy-ai[bot] 90a170c941 v1.92.50.0 wave(autoplan): #3781 #3743 #3751 #3762 (#3788)
* fix(eval-plan): per-arm pilot projection and --seat-metered arms (#3653)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): guard tolerates Claude Code's cwd-reset footer and recovers after an unreadable init (#3651)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (journal reader strips the cwd-reset footer; an init with no text block no longer locks the session)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: journal reader skips a failed or unreadable init, as invocation() does

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(autoplan): --ceo-only runs the CEO review alone with zero questions (GST-537)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(eval-plan): all-seat pilot, single-attempt arm and --seat-metered CLI wiring

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(eval-plan): summed deviation adds arm deviations, matching the summed band (arms share rows)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (test pins an unpriced seat-metered arm)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (seat-metered declaration fixed per --out, price row still required, a real charge settles as spent)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: classify autoplan-ceo-only in the Codex host metadata

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: a seat-metered arm that settles a charge is projected as a priced arm

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: select autoplan-ceo-only on the skills it runs; say it needs Claude Code

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(skills): /eval-plan --seat-metered and per-arm projection

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(eval-plan): skill prose stays under its context budget and carries no $N token

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: run autoplan-ceo-only in the marathon lane

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(skills): a seat-metered arm leaves the seat set once it settles a charge

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: changelog entry for /autoplan --ceo-only

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): init_failed covers unreadable init output and the cwd-reset line (#3651)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: count the autoplan-ceo-only paid file in the shard inventory

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(autoplan): #3651 tests pass the test-code type ratchet

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(dx): CAP finding titles carry a one-line description (GBRA-223 claim 2)

The DX error contract told both autoplan DX voices to title capped findings
exactly CAP4:<path>:<item>, so gates, tasks and reconcile saw a bare code.
The title is now CAP4:<path>:<item> <what fails>; the grader already parses
that shape.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): reconcile pairs same-file findings that carry no line (GBRA-223 claim 1)

Plan reviews name files, rarely lines, and reconcile associated by location
only when both findings had a line, so findings both voices raised on one
file were filed new and native-only. A same-file pair with a missing line is
now associated as unverified (basis file), never confirmed; a CAP code no
longer counts in title comparison. DRIVER.md names submit --reconcile.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): open Critical and High findings reach the gate as apply items (GBRA-223 claim 3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a spend cap binds on every unattended host; --estimate-usd persists; DRIVER names --usage-usd (GBRA-223 claim 4)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): run binds complete api results, names a truncated response, --max-tokens; DRIVER documents run (GBRA-223 claim 6)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(autoplan): carry an item-by-item answer when the later gate asks nothing new (GBRA-223 claim 7)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(dx): trim the compact error contract so plan-devex-review stays under its size pin

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (p1b applies finding fixes, api attempts reserve no host default, cross-file title pairing, paired severity counted)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: review fixes (priced api reservations, no file-only twins, two-pass pairing, partial replies keep p1-dependent defaults open, outside refusal degrades)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* P7: record whose words answered a gate (--words-by agent:<id>), never carry an agent's own words, import each row under its own answerer (GEVA-2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: review pass 2 (p1b needs --rec-hash after recommend; api calls send the output limit they reserve for)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: fragment for the /autoplan field-report wave (GST-672)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: captain record notes --words-by agent attribution

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: follow finding apply items in approval-class and Captain flow tests; bind run-api limit to DEFAULT_MAX_TOKENS

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* wave autoplan: lower the gemini plan-devex-review pin to its exact size after #3781's DX trim

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): CAP titles on different paths never match by title in reconcile or confirm rounds (wave review)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a truncated api call settles its cost and a retry is admitted afresh against the cap

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): an applied finding stays open until the revised plan is reviewed; a shared location alone never twins two findings

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(approval): a plan gate answered in an agent's own words is not owner approval

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(gate): a new answer replaces the earlier answer's authorship and words

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a failed api call with no usage keeps its attempt open; reservations price the bound prompt (wave fix-check)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a spend cap fails closed: only a call refused before sending keeps its attempt; a sent call with unknown usage settles at its reservation

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): the 16384 output default applies to both providers; changelog for the wave's integrator fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): how run settles a failed api call; changelog scopes it to the native voice

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a spend cap fails closed on every exit of run; an unpriced model reserves the default once a cap is set

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): every sent call settles; an unpriced model under a cap reserves the default

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): DRIVER and help say a cap reserves in any session kind and how run settles a sent call

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-02 01:35:47 +00:00

17 KiB

/autoplan publication guard: troubleshooting

/autoplan installs a PreToolUse hook (autoplan/bin/phase-publication-hook) at its phase boundaries. Before it lets a phase-entry Read or a reviewer Agent dispatch run, it reads this session's own Claude Code journal (~/.claude/projects/<project>/<session>.jsonl) and checks that the previous phase's report was published as parent assistant text.

Every guard message names a code, the Claude Code version that wrote the journal, and a link to the section below for that code. Include the code and version when you report a problem. The guard's decision log (see The decision log) holds the same information.

Each code has one of four dispositions:

Disposition What it means What you do
Unverified allow Claude Code left this call uncheckable. The call runs, with a visible warning. Nothing, unless it keeps happening (then report it).
Fallback Retrying cannot help. Run the fallback below.
Corrective One named action must happen first. Take that action.
Transient Something is still in progress. Wait for the named thing, then continue.

The fallback (printed in every fallback denial):

  1. Run the reviews by hand: /plan-ceo-review, then /plan-devex-review, then /plan-eng-review.
  2. Or move to a fresh session and keep your work: /context-save, start a new Claude Code session in the project (not --resume), /context-restore, then /autoplan <plan path>.

There is no environment variable that turns the guard off.

How a phase transition is published

Claude Code can write a tool call to the journal after the hook has already run, and text in the same assistant message as a tool call can be invisible to the hook. So the guard counts a phase report only when a later journaled record follows it: a tool result, or a record from another assistant message.

The phase-close procedure therefore publishes each report in a message whose only tool call is the Bash no-op true autoplan-published <phase> (it prints nothing and needs no permission; Claude Code 2.1.292 asks for approval of a bare : command, so the no-op is true), and makes the next phase's Read or Agent call in a later message. Sessions that put the report and the next phase's call in one message get one publication_unflushed denial naming that fix.

When Claude Code has not yet written the current call, the guard evaluates the call from the hook payload instead (Claude Code's own parsed input), placed after the last journaled record. The previous phase's report must still be in the journal.

Unverified allows

These states are caused by Claude Code, not by the model. The guard lets the call run without a permission decision (Claude Code's own permission check still runs), prints a warning to you that names the code, and logs the allow with disposition: "unverified". For that one call it does not check that the previous phase's report was published.

[autoplan] phase publication was NOT verified for this session (journal_lag): ...

unrecognized_shape:<detail>

The journal root has a shape the guard does not know, usually because Claude Code changed its journal format. The guard reads the journal twice and allows only when both reads agree. unrecognized_shape:cwd_spelling means the journal's directory is the same folder as the project, spelled differently. unrecognized_shape:compaction_unreachable means records after a Claude Code compaction hang from a compact boundary whose logical parent loops back instead of reaching the session root, in a layout gstack does not read yet (Claude Code 2.1.300's preserved segment is read). Earlier gstack versions dropped those records and denied the next phase with close_incomplete. Please report the detail and version.

journal_lag

Claude Code had not written the previous assistant message to the journal (a tool in an earlier message still has no result, or an ancestor record is missing) and did not catch up within 2 seconds.

rewritten

The journal bytes the guard had already read changed during one hook call: a later read's sha256 of bytes [0, first size) differs from the first read's, or a record pass two reloads no longer parses to the record pass one indexed. Claude Code normally only appends, and appends never fail a read. Rewrites between hook calls are not detected (an accepted limit).

too_large

A single journal record is over the 32 MiB record limit (OWNED_RECORD_MAX_BYTES). The guard reads long journals record by record, so a large journal alone never triggers this; one enormous record (for example a huge pasted file) does. A record still being written (no newline yet) is not read at all.

unseen_version

A Claude Code release newer than the one gstack has checked passes the hook a reviewer dispatch without a key the journal has (a schema strip). The daily autoplan-schema-canary case catches these within a day of a release.

Fallback denials

competing_root

The journal has more than one conversation root, or two records share one UUID.

foreign_cwd

The journal's root was written in another project directory, after spelling and symlinks are taken into account. Linked git worktrees of the project are accepted.

sidechain

The first turn or its ancestry is a sidechain (subagent) record, not the parent session.

agent

The conversation ancestry passes through a subagent record.

cycle

The journal's parent links form a loop.

identity

The journal path, its directories or the file failed the identity checks: a symlink, a foreign session, the wrong directory layout, a different file, or a journal that shrank. A rerun hits the same check.

oversized_invocation

The current /autoplan invocation needs more journal records read in full than the 32 MiB retained-data bound (OWNED_RETAINED_MAX_BYTES). The guard indexes every record without its content and reloads in full only what it evaluates: snapshot init results, guarded and close-packet Reads, structured Bash results, error results, assistant text after the first close-packet Read and edits of the active plan. A rerun reads the same records; use the fallback.

malformed

A complete journal line is not valid JSON or UTF-8, or its records contradict each other.

journal_missing

The session journal named by the hook does not exist after 2 seconds.

event_order

The journal's events could not be put in a consistent order for this session.

tool_identity

Two journal records claim the same tool call identity.

current_mismatch

The journal records the current call with a different tool or input than Claude Code passed to the hook. Only run_in_background (which Claude Code's fork-subagent schema drops) and the spelling of file_path may differ.

agent_key

The reviewer dispatch carries a key outside prompt, description, subagent_type (general-purpose only) and run_in_background. A model override is not allowed for /autoplan reviewer dispatch. The log records the key names, never their values.

cross_phase_batch

One assistant message enters two different phases. Calls that enter the same phase may share a message; another phase needs its own message after the previous report.

init_mismatch

Snapshot init returned paths that do not match the canonical form of its own command arguments, or its artifacts (restore point, active plan header) do not match this invocation. The guard canonicalizes the <active> and <restore> arguments the way init does (see Symlinked plan directories) before comparing, so a retargeted or forged init result is denied here. Zero results or duplicate results for one init use also deny as init_mismatch. Do not re-run the same call: run init with the paths the guard expects.

snapshot

A phase snapshot, methodology or other immutable artifact is missing, aliased, writable or changed.

dispatch_prompt

The reviewer prompt differs from the snapshot's exact nativeDispatchPrompt.

evidence

An artifact the guard needed could not be parsed.

publication_repeat

The report was published separately once in this invocation and still cannot be verified, so Claude Code is not writing it to the journal in time.

Corrective denials

publication_missing

No filled report for the previous phase appears after its close packet Read. Publish it as your own assistant text in a message whose only tool call is true autoplan-published <phase>, then enter the next phase in a later message.

publication_unflushed

The report shares the current call's message, or nothing journaled follows it yet. Publish it in its own message with the no-op call, then enter the next phase in a later message. A second occurrence in one invocation becomes publication_repeat.

close_required

Finish the previous phase's close procedure and Read its close packet.

close_incomplete

Read every line of the current close packet.

close_stale

The close packet or its checkpoint no longer matches the current phase input. Prepare a fresh packet and read it.

close_edits

The plan changed after the close Read in a way the guard cannot reconstruct, or it fails its phase check. Repeat the close procedure.

init_required

No successful snapshot init for this invocation. Run the existing init step.

init_own

A new /autoplan turn needs its own init (it may answer reused: true).

init_failed

The most recent snapshot init call failed; re-run the same init command with the same three paths. An errored init is skipped, so a later successful init still binds. This denial applies when no init ever succeeded, or when an errored init follows a good one.

An init whose output is not its JSON result (extra text, or no single text block) also counts as failed, and the message adds "without a leading cd" to the re-run hint. The one exception is the Shell cwd was reset to <dir> line Claude Code adds as the last line of a Bash result when the command's cd left the session's folder: the guard removes exactly that line before reading an init or checkpoint result (#3651).

init_unbindable

Init ran through a shell variable, substitution, chaining, a pipe or a redirect. Re-run it as one Bash call with literal absolute paths.

phase_order

Enter Phase 1 (CEO) before a later phase.

hook_input

The hook could not read its input. Restore the installation (./setup).

installation

The hook failed unexpectedly. Restore the installation (./setup).

foreign_install

The phase file belongs to another gstack installation than the hook. Restore this installation (./setup).

Transient denials

entry_pending

A phase-entry call from an earlier message has no result yet. Wait for it.

mutation_pending

An edit to the active plan after the close Read has no result yet. Wait for it.

Removed codes

changing

Before the October 7, 2026 release, changing denied a call when Claude Code appended to the journal while the guard read it. The bounded reader reads the bytes present at open and ignores a trailing record still being written, so appends never fail a read; a changed prefix is rewritten.

Before the October 7, 2026 release, "Native parent evidence has not reached the journal yet" (no code) and "Current native phase-entry identity is required before entering a new phase" denied calls Claude Code had not yet journaled. The payload path replaced both.

Claude Code settings

CLAUDE_CODE_FORK_SUBAGENT=false is no longer needed (keeping it is harmless). With the fork-subagent gate on, reviewer dispatches may run in the background: the dispatch returns a launch receipt and the review arrives later as a completion notice. Wait for that notice before closing the phase. A background reviewer keeps the invocation armed, so a typed message while it runs does not end /autoplan.

Permission cards after a background reviewer

A skill's allowed-tools applies only to the turn that invoked it, and a background reviewer's completion notice starts a new turn. From then on Claude Code's normal permission rules apply, so Reads outside the session's working directory can show a card, and in Manual mode so do Bash commands that are not read-only. Skill hooks stay registered for the whole session but do not run inside subagents.

/autoplan therefore keeps its own files readable without a card:

  • The restore point and every phase artifact (methodology, reviewer input, snapshots, close packet) live in the project's .gstack/tmp/autoplan/, which Step 1 adds to .git/info/exclude. Claude Code reads files in the working directory without a card, for /autoplan and for its reviewer subagents. A git clean -x deletes them, restore point included.
  • The publication hook approves /autoplan's own Reads outside the working directory: this installation's autoplan/sections/*.md, and the immutable artifacts in <repo>/.gstack/tmp/autoplan/ when the session started in a subdirectory of the repository. It approves nothing else, and never a call the guard denies.

A session started in a repository subdirectory still shows a card when a reviewer reads its input: subagents run without the skill's hook. Start the session at the repository root, or answer the card with "Yes, allow reading from ... during this session". In Manual mode, /autoplan's Bash commands after the first background reviewer also show cards; auto mode, the default starting mode on current Claude Code for supported models, sends them to its classifier instead.

Supported journal roots

The guard accepts exactly one root per journal file:

  • A user message with no parent (sessions without hooks).
  • An unbroken chain of message-less SessionStart hook attachments (hook_success, hook_additional_context, ...) that starts at a null parent and ends at the first user turn. Claude Code writes this when any SessionStart hook is installed, including hooks that plugins add. It also writes one after /clear.
  • A compact_boundary record whose logical parent is not in the file, but only when it is the file's first record. Claude Code writes this when you resume a compacted session with --fork-session.

--continue, --resume and /compact append to the same journal, and the guard follows them through their existing parent links. The real journals these rules were checked against came from Claude Code 2.1.284 (test/fixtures/claude-native-journal-roots-2.1.284.json); the periodic autoplan-journal-drift canary re-checks the pinned Claude Code.

The decision log

Every guarded decision appends one line to <state root>/analytics/autoplan-guard.jsonl (normally ~/.gstack/analytics/autoplan-guard.jsonl): schema, time, decision (allow or deny), disposition, code, path (journal, payload or none) and the Claude Code version. It never holds content; an agent_key denial adds the key names, and an unrecognized shape adds record type names. The file keeps the newest 1,000 lines once it passes 256 KiB. A failure to write it never changes a decision.

Windows paths

Claude Code and Git Bash can spell one path as C:\x, c:\x, C:/x or /c/x. The guard treats these as the same path. It does not resolve . or .., so a path containing them is still rejected.

Symlinked plan directories (macOS /tmp, /var)

The guard compares plan paths canonically, not by argv spelling. On macOS /tmp and /var are symlinks to /private/tmp and /private/var, so a plan under /tmp or /var/folders is expected to appear as /private/... in the init JSON. That is by design: init reports canonical paths (the deepest existing ancestor is resolved with realpath and the missing segments are re-joined, canonicalDestination in bin/gstack-autoplan-snapshot.ts), and the guard canonicalizes the init command's <active> and <restore> the same way. Writes and Edits to the active plan are matched by canonical path too, so editing the plan through an alias spelling is recognized. Symlinked plan directories are allowed; only a result that differs from the canonical form of its own arguments is denied (init_mismatch).