mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
* docs(designs): gstack field wave plan (2026-10-12) and the shared simulated Capy host fixture
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(pregate): price_command config key for the non-executing call plan a paid gate is priced from
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(pregate): repo guards a lane must pass, verified launch Bun and per-file serial runs in lanes, unknown flags exit 2
The guards check runs each .gstack/pregate.json guard whose globs match a
touched file in its own process with its own timeout (PREGATE_GUARD_FAILED,
never downgradable, incomplete without --allow-repo-commands). The lanes
runner and guards get the first Bun in $BUN_INSTALL/bin or ~/.bun/bin that
satisfies gstack's and the project's engines.bun; serial_patterns files run
one process each. price_file replaces the unreleased price_command key: a
paid gate is priced from a call-plan data file, never by running a command.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(pricing): Sonnet 5.5, Gemini 3.x and gbrain embedding rows from fetched pages; Opus 4.7 at its current list price
Each new row carries its source URL and fetch date (2026-10-12). Embedding
models bill input only, so they live in EMBEDDING_PRICING beside PRICING
(eval arms never resolve to them); pricingRow also accepts gbrain's
provider:model ids (voyage:voyage-4). claude-opus-4-7 moves from 15/75 to the
5/25 the Anthropic pricing page lists; benchmark-runner's expected costs follow.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(eval-plan): price-gate prices a paid gate from a call-plan data file, never executing it
gstack-eval-plan price-gate --gate "<exact command>" reads a GSTACK_EVAL_CALLS
v1 file (--plan-file, else the base policy's price_file at HEAD), checks its
binding (gate command string equality, head with only the plan file changed
since, every input's sha256 at HEAD), prices each call purpose (aggregate
totals) and each one-call reservation, and writes a receipt binding the price
to the gate, head, inputs and plan bytes. Unpriced models print unknown, never
zero (EVAL_PRICE_MISSING); a missing plan or no call lines is
EVAL_GATE_UNPRICED unless --allow-unpriced records an owner override; over
--cap-usd / --smoke-cap-usd is EVAL_GATE_OVER_CAP; malformed or partial files
refuse EVAL_DRY_RUN_FAILED and stale ones EVAL_PLAN_STALE. dry-run --cap adds
the same reservation-fits-cap check. Wire format, receipt and the reference
emitter (formatEvalCalls): docs/eval-calls.md.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(eval-plan): the approval step prices every paid gate with price-gate
Step 4 runs gstack-eval-plan price-gate for each paid gate the plan names and
puts its GATE_PRICE line in the ask; the dry-run checklist documents
reservation-fits-cap under --cap.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(sealed): sealed-output guard over every published commit, in the pre-push hook and gstack-pregate sealed
A repo with a sealed key in .gstack/pregate.json (base branch union the
pushed commit, so a push can tighten but never relax) refuses publishing
sealed paths, transcripts, quoted answers, seeds or local home paths to a
public or unknown destination (SEALED_OUTPUT_REFUSED); private warns.
Path rules see every added, renamed or copied path in each published
commit (git rev-list <local> --not --remotes=<remote>), text rules each
commit's added lines; waivers only from the base branch allow file; a
malformed policy fails closed; visibility caches public 24 h, private 1 h.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(eval-plan): only an undeclared plan is unpriced; a missing price_file is stale, a bad base policy refuses
price-gate's EVAL_GATE_UNPRICED (and --allow-unpriced) now covers only no
plan declared at all or a plan with zero call lines. A declared price_file
missing from the HEAD commit is EVAL_PLAN_STALE (exit 1, not overridable); an
unresolvable origin/<base> is POLICY_SOURCE_UNAVAILABLE and an invalid
.gstack/pregate.json is PREGATE_CONFIG_INVALID (both exit 3).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(restamp): the repo's stamp rule (stamp_files, literal stamp_patterns) and restamp_command in the staging worktree
stamp_files is an allowlist applied before every stamp rule; stamp_patterns
are bounded literal {version} templates shared by the restamp writer and the
tree receipt. restamp_command runs first in the staging worktree with the
target version (argument and GSTACK_RESTAMP_VERSION), needs a clean caller
tree, skips gstack's version writer and keeps output confinement; a failure
or a wrong version writes nothing. migrations { dir, id } is the policy key
gstack-pregate regate reads.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(pregate): gstack-pregate regate --against <gated-head> with per-lane reuse and migration collisions
Reuse comes only from a tree receipt evaluated now, with a bundle produced
on the gated tree, the current execution identity and an unmoved, known
predecessor head. Each lane prints reusable/rerun/not-reusable; REGATE:
not-needed needs stamp-only, every bundle and --require lane reusable (a
stamp-sensitive lane reruns) and a finished migration lookup. Migration
numbers the branch adds are checked against the base tree and every other
open PR (gh pr list --limit 1000; 100-file lists refetched with gh api
--paginate); any lookup that cannot finish is unknown and blocks
not-needed (MIGRATION_COLLISION). Bare --against routes to the verb.
docs/pregate.md gains the regate section and the gbrain worked configs.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(contract): lanes run gstack-pregate (and the sealed guard where configured) before done; handoff carries the gstack trailer and PREGATE: line
The coordinator contract gains a 'Before done' paragraph and the handoff
order GSTACK_RESULT, gstack: ok|fail <rev>, PREGATE:, gate summary; the
spec section is regenerated and docs/unattended.md states the same rule.
A new test pins the terms and their order; the byte-identity pins stay.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* v1.91.98.0: pregate guards, priced paid gates, regate and the sealed-output guard
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: drop the helper-only Capy host fixture test (test-of-test ratchet); lane tests exercise the helper
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: price-gate --gate-file keeps free text out of the /eval-plan fence; shortest dependency traces; bounded test spawns
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci(windows): curate out the new pregate/restamp tests that spawn bin/ shebang scripts through runBin
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>
570 lines
33 KiB
TypeScript
570 lines
33 KiB
TypeScript
/**
|
||
* Result codes for the unattended workflow commands (gstack-artifact,
|
||
* gstack-gate, gstack-autoplan-timing, gstack-review-log --findings, the
|
||
* skill-start guard line) and the install side (gstack-doctor --check,
|
||
* gstack-capy-install, gstack-browser-ensure; the Bash side reads the same
|
||
* table from the generated bin/gstack-doctor-components.sh). Sibling of lib/gate-outcomes.ts, whose `state`
|
||
* enum is for gate outcomes only: a result code has no state, just one
|
||
* stable anchor in docs/troubleshooting.md, a summary and a `fix:` clause.
|
||
* Every error line a workflow command prints ends with `(<CODE>)` so a parent
|
||
* agent greps the code, never the prose. test/troubleshooting-anchors.test.ts
|
||
* checks every anchor exists; add a row here before printing a new code.
|
||
*/
|
||
export interface ResultCode {
|
||
/** Stable `<a id>` in docs/troubleshooting.md. */
|
||
anchor: string;
|
||
/** What happened, in plain words; `<detail>` adds the real value. */
|
||
summary: string;
|
||
fix: string;
|
||
}
|
||
|
||
export const RESULT_CODES = {
|
||
ARTIFACT_INVALID_JSON: {
|
||
anchor: 'artifact-invalid-json',
|
||
summary: 'the manifest or a JSON artifact is not valid JSON',
|
||
fix: 'regenerate the run with the tool that writes it; never hand-edit a manifest',
|
||
},
|
||
ARTIFACT_UNSUPPORTED_VERSION: {
|
||
anchor: 'artifact-unsupported-version',
|
||
summary: 'the artifact declares a schema_version this gstack does not read',
|
||
fix: 'run /gstack-upgrade on the consumer, or regenerate the run with this gstack',
|
||
},
|
||
ARTIFACT_SCHEMA: {
|
||
anchor: 'artifact-schema',
|
||
summary: 'a required field is missing or holds a value outside its enum',
|
||
fix: 'compare the row with `gstack-artifact schema <name>` and regenerate it',
|
||
},
|
||
ARTIFACT_MISSING: {
|
||
anchor: 'artifact-missing',
|
||
summary: 'an artifact named by the manifest is not in the run directory',
|
||
fix: 'copy the whole run directory, not single files; rerun the phase that writes the missing file',
|
||
},
|
||
ARTIFACT_STALE: {
|
||
anchor: 'artifact-stale',
|
||
summary: 'an artifact’s bytes no longer match the hash the manifest recorded',
|
||
fix: 'treat the run as tampered or partially rewritten; regenerate it or re-bind the file through the tool that wrote it',
|
||
},
|
||
ARTIFACT_PATH_ESCAPE: {
|
||
anchor: 'artifact-path-escape',
|
||
summary: 'an artifact path resolves outside the run directory (a `..` segment, an absolute path or a symlink)',
|
||
fix: 'keep every artifact inside the run directory and name it with a plain relative path',
|
||
},
|
||
ARTIFACT_MALFORMED_JSONL: {
|
||
anchor: 'artifact-malformed-jsonl',
|
||
summary: 'a JSONL artifact has a line (usually the tail) that is not one JSON object',
|
||
fix: 'the writer was interrupted mid-append; rerun the phase that writes the file',
|
||
},
|
||
ARTIFACT_DUPLICATE_ID: {
|
||
anchor: 'artifact-duplicate-id',
|
||
summary: 'two rows share one id',
|
||
fix: 'ids are run-bound and unique per file; regenerate the rows with the tool that assigns them',
|
||
},
|
||
ARTIFACT_UNBOUND_ID: {
|
||
anchor: 'artifact-unbound-id',
|
||
summary: 'a row id does not start with the run id',
|
||
fix: 'ids are `<run>-...`; regenerate the row with the run id the manifest names',
|
||
},
|
||
ARTIFACT_DANGLING_REF: {
|
||
anchor: 'artifact-dangling-ref',
|
||
summary: 'a row references an id no artifact in this run defines',
|
||
fix: 'write the referenced finding, decision or task first, or drop the reference',
|
||
},
|
||
ARTIFACT_DEPENDENCY_CYCLE: {
|
||
anchor: 'artifact-dependency-cycle',
|
||
summary: 'tasks depend on each other in a cycle',
|
||
fix: 'break the cycle in `depends_on`; a lane cannot start a task that waits on itself',
|
||
},
|
||
ARTIFACT_COUNT_MISMATCH: {
|
||
anchor: 'artifact-count-mismatch',
|
||
summary: 'a count in run.json disagrees with the rows in the file it summarizes',
|
||
fix: 'regenerate run.json after the last row was written; a count never replaces the file',
|
||
},
|
||
ARTIFACT_RUN_INTERRUPTED: {
|
||
anchor: 'artifact-run-interrupted',
|
||
summary: 'the run did not reach a terminal status or a reviewer never returned',
|
||
fix: 'resume the run (`gstack-autoplan resume --out <dir>`) or treat its artifacts as partial',
|
||
},
|
||
ARTIFACT_REVIEWER_MISSING: {
|
||
anchor: 'artifact-reviewer-missing',
|
||
summary: 'a required phase or one of its voices has no terminal outcome or no bound output file',
|
||
fix: 'finish the phase; a missing voice is missing coverage, never N/A by default',
|
||
},
|
||
ARTIFACT_GUARD_LINE_MISSING: {
|
||
anchor: 'artifact-guard-line-missing',
|
||
summary: 'an unattended run’s review-record.md does not state that the publication guard was not enforced',
|
||
fix: 'start the run through gstack-skill-start so the GUARD_NOT_INSTALLED line is recorded',
|
||
},
|
||
GUARD_NOT_INSTALLED: {
|
||
anchor: 'guard-not-installed',
|
||
summary: 'this host does not execute the autoplan publication hook, so phase publication order is unverified',
|
||
fix: 'nothing to do on this host; the run record carries this line, and the snapshot hashes per phase stand in for the guard',
|
||
},
|
||
REVIEW_STATUS_MISMATCH: {
|
||
anchor: 'review-status-mismatch',
|
||
summary: 'the review-log row claims a status or count that its findings file contradicts',
|
||
fix: 'log through `--findings <file>` and let the row be derived; fix the findings file, not the claim',
|
||
},
|
||
GATE_REV_STALE: {
|
||
anchor: 'gate-rev-stale',
|
||
summary: 'the reply answers an older revision of the gate list',
|
||
fix: 'render the current gate list, read it, and reply with its gate_rev',
|
||
},
|
||
GATE_REPLY_UNPARSED: {
|
||
anchor: 'gate-reply-unparsed',
|
||
summary: 'part of the reply matched no gate item or option, so nothing was applied',
|
||
fix: 'reply with `all`, `all as-is`, `<id><option>` tokens (for example `d3b uc1a`), or `all except <tokens>`; bare yes/no is ambiguous',
|
||
},
|
||
// PR A (multi-agent wave): gstack-doctor --check, gstack-capy-install, gstack-browser-ensure.
|
||
COMPONENT_MISSING: {
|
||
anchor: 'doctor-component-missing',
|
||
summary: 'a component the selected workflow needs is not installed, not built or not current',
|
||
fix: 'run the command in the row\'s fix clause, then re-run gstack-doctor --check',
|
||
},
|
||
RUNTIME_BELOW_MINIMUM: {
|
||
anchor: 'doctor-runtime-below-minimum',
|
||
summary: 'the Bun on PATH is absent, below gstack\'s security floor, or below its supported minimum',
|
||
fix: 'install the supported Bun through setup\'s checksum-verified recipe (bin/gstack-capy-install does this on Capy), then re-run ./setup',
|
||
},
|
||
PROJECT_PIN_MISMATCH: {
|
||
anchor: 'doctor-project-pin-mismatch',
|
||
summary: 'the running Bun or Node is outside a version the target project pins (package.json engines, .tool-versions, .nvmrc, .bun-version or a CI workflow)',
|
||
fix: 'install the pinned version for this platform, or change the pin in the project',
|
||
},
|
||
REVISION_UNMET: {
|
||
anchor: 'doctor-revision-unmet',
|
||
summary: 'the installed gstack checkout is not at the revision --revision requested',
|
||
fix: 'run /gstack-upgrade (or git -C <checkout> fetch && git checkout <ref>, then ./setup) to move the checkout',
|
||
},
|
||
RUNTIME_PIN_CONFLICT: {
|
||
anchor: 'capy-install-runtime-conflict',
|
||
summary: 'gstack\'s supported Bun minimum and the project\'s Bun pin cannot both be satisfied, so the installer changed nothing',
|
||
fix: 'raise the project\'s pin to gstack\'s supported minimum, or install gstack on a machine whose Bun the project accepts',
|
||
},
|
||
BROWSER_UNAVAILABLE: {
|
||
anchor: 'browser-ensure-failed',
|
||
summary: 'Chromium could not be installed or launched by gstack-browser-ensure',
|
||
fix: 'read the installer output above (offline, proxy, AppArmor); on Ubuntu 24.04+ try GSTACK_CHROMIUM_NO_SANDBOX=1, then re-run gstack-browser-ensure',
|
||
},
|
||
INSTALL_PREFLIGHT_FAILED: {
|
||
anchor: 'capy-install-preflight-failed',
|
||
summary: 'a tool the installer needs before ./setup (git, curl, node, jq) is missing, or the Bun install recipe failed its checksum',
|
||
fix: 'install the named tool, then re-run bin/gstack-capy-install',
|
||
},
|
||
// C-stamp (plan C1, C2, C3, C5, C7): ship policy, restamp, tree receipt, ship receipt.
|
||
POLICY_SOURCE_UNAVAILABLE: {
|
||
anchor: 'policy-source-unavailable',
|
||
summary: 'policy source unavailable: origin/<base> is not a resolvable commit, so the reviewed .gstack/ship-policy.json cannot be read',
|
||
fix: 'git fetch origin <base> --depth=1 (the working-tree copy is never used silently)',
|
||
},
|
||
POLICY_INVALID: {
|
||
anchor: 'policy-invalid',
|
||
summary: '.gstack/ship-policy.json has an unknown key, a wrong type or a value outside its enum',
|
||
fix: 'run `gstack-ship-policy validate`, compare with `gstack-ship-policy init --explain`, and fix the file on the base branch',
|
||
},
|
||
POLICY_CONTAINMENT: {
|
||
anchor: 'policy-containment',
|
||
summary: 'a repo-controlled path, glob, command or list in the policy escapes the repository or exceeds its size cap',
|
||
fix: 'use repo-relative paths without `..`, globs under 256 characters, lists under 64 entries and a release_tool under 512 characters',
|
||
},
|
||
POLICY_EXISTS: {
|
||
anchor: 'policy-exists',
|
||
summary: '.gstack/ship-policy.json already exists; nothing written',
|
||
fix: 'edit the existing file, or pass --force to overwrite it',
|
||
},
|
||
REPO_COMMANDS_NOT_ALLOWED: {
|
||
anchor: 'repo-commands-not-allowed',
|
||
summary: 'repo commands not executed: the policy declares commands and this invocation did not opt in',
|
||
fix: 'pass --allow-repo-commands (what /ship does), or run without the release tool',
|
||
},
|
||
QUEUE_STALE: {
|
||
anchor: 'queue-stale',
|
||
summary: 'the base SHA or queue order the allocation assumed has moved',
|
||
fix: 'recompute with the printed command (gstack-restamp --next ... with the current --expect-base) before merging',
|
||
},
|
||
RESTAMP_VERSION_SOURCE: {
|
||
anchor: 'restamp-version-source',
|
||
summary: 'the configured version source is absent, ambiguous or broken at the base revision or on the branch',
|
||
fix: 'create VERSION or pin .gstack/version-path; gstack never invents a version',
|
||
},
|
||
RESTAMP_MERGE_CONFLICT: {
|
||
anchor: 'restamp-merge-conflict',
|
||
summary: 'the gate-ahead merge of the branch and its predecessor conflicts; nothing written',
|
||
fix: 'merge the predecessor into the branch under /ship Step 3’s conflict rules, then run --after again',
|
||
},
|
||
RESTAMP_RELEASE_TOOL_FAILED: {
|
||
anchor: 'restamp-release-tool-failed',
|
||
summary: 'the policy’s release_tool exited non-zero in the staging worktree; nothing written to the branch',
|
||
fix: 'run the release tool by hand in the repo, fix it, and rerun gstack-restamp',
|
||
},
|
||
RESTAMP_OUTPUT_OUTSIDE_ALLOWED: {
|
||
anchor: 'restamp-output-outside-allowed',
|
||
summary: 'the release tool wrote a path outside the enumerated allowed-output set; nothing written to the branch',
|
||
fix: 'list the path in `release_outputs` or `stamp_paths` on the base branch, or stop the tool from writing it',
|
||
},
|
||
RESTAMP_CONFLICT: {
|
||
anchor: 'restamp-conflict',
|
||
summary: 'a file to write or roll back no longer holds the bytes the journal recorded (a later edit); the restamp stopped',
|
||
fix: 'inspect .gstack/tmp/restamp-journal.json, reconcile the file by hand, then rerun gstack-restamp',
|
||
},
|
||
RESTAMP_PREDECESSOR_MOVED: {
|
||
anchor: 'restamp-predecessor-moved',
|
||
summary: 'PREDECESSOR MOVED: the predecessor head the gate assumed is no longer that PR’s head',
|
||
fix: 'gstack-restamp --after <pr> again and regate on the new synthetic tree',
|
||
},
|
||
CHANGELOG_DUPLICATE_HEADING: {
|
||
anchor: 'changelog-duplicate-heading',
|
||
summary: 'two CHANGELOG sections carry the same version heading',
|
||
fix: 'merge the duplicate sections into one entry under the new version',
|
||
},
|
||
CHANGELOG_TOP_MISMATCH: {
|
||
anchor: 'changelog-top-mismatch',
|
||
summary: 'the first CHANGELOG entry is not the version being stamped',
|
||
fix: 'move the PR’s entry to the top (gstack-restamp does this) or fix the heading to match VERSION',
|
||
},
|
||
CHANGELOG_SECTION_MISSING: {
|
||
anchor: 'changelog-section-missing',
|
||
summary: 'the CHANGELOG has no `[Unreleased]` or old-version section for this PR to re-head',
|
||
fix: 'write the PR’s entry under `## [Unreleased]` (stamp-at-merge) and rerun',
|
||
},
|
||
GATE_REUSE_NOT_ELIGIBLE: {
|
||
anchor: 'gate-reuse-not-eligible',
|
||
summary: 'the earlier gate ran under a different identity (tree, runtime pins, lockfile, CI config, gate command or selection)',
|
||
fix: 'rerun the named lanes on the final stamped tree with the printed command',
|
||
},
|
||
EVIDENCE_IDENTITY_UNKNOWN: {
|
||
anchor: 'evidence-identity-unknown',
|
||
summary: 'the evidence bundle lacks the execution identity (committed tree, runtime, lockfile or command hash) a reuse decision needs',
|
||
fix: 'rebuild the bundle on the gate machine with `gstack-evidence bundle` after the lanes ran on a committed head',
|
||
},
|
||
EVIDENCE_BUNDLE_INVALID: {
|
||
anchor: 'evidence-bundle-invalid',
|
||
summary: 'the evidence bundle is not valid JSON or not the schema this gstack reads',
|
||
fix: 'rebuild it with `gstack-evidence bundle`; never hand-edit a bundle',
|
||
},
|
||
PREREGISTRATION_NOT_ANCESTOR: {
|
||
anchor: 'preregistration-not-ancestor',
|
||
summary: 'a preregistration_shas entry is not an ancestor of HEAD',
|
||
fix: 'rebase or merge so the preregistered commit is in the branch history, or remove the entry on the base branch',
|
||
},
|
||
HISTORY_POLICY_VIOLATION: {
|
||
anchor: 'history-policy-violation',
|
||
summary: 'the policy says history: merge-only and the plan squashes, rebases or rewrites the branch',
|
||
fix: 'merge the base branch instead (git merge origin/<base>) and keep every commit',
|
||
},
|
||
RECEIPT_MISSING: {
|
||
anchor: 'receipt-missing',
|
||
summary: 'no ```gstack-ship-receipt``` block was found in the PR body',
|
||
fix: 'write it with `gstack-ship-receipt write ... --pr <n>` (/ship Step 18) before reading it',
|
||
},
|
||
RECEIPT_INVALID: {
|
||
anchor: 'receipt-invalid',
|
||
summary: 'the ship receipt block does not validate against the ship-receipt schema',
|
||
fix: 'regenerate it with `gstack-ship-receipt write`; never hand-edit the block',
|
||
},
|
||
// B-runner (gstack-autoplan, gstack-outside-voice, lib/spend-ledger.ts).
|
||
EXECUTION_UNKNOWN: {
|
||
anchor: 'execution-unknown',
|
||
summary: 'an attempt was dispatched but never reached a terminal record, so the provider may have finished and charged',
|
||
fix: 'submit the result file if the reviewer finished (`gstack-autoplan submit ... --attempt <id>`), or redispatch explicitly with `gstack-autoplan next --redispatch <id>`; nothing is redispatched automatically',
|
||
},
|
||
RUN_LOCKED: {
|
||
anchor: 'run-locked',
|
||
summary: 'another gstack-autoplan process owns this run directory',
|
||
fix: 'wait for it to exit, or if its pid is gone run `gstack-autoplan resume --out <dir>` to take ownership',
|
||
},
|
||
RUN_NOT_INITIALIZED: {
|
||
anchor: 'run-not-initialized',
|
||
summary: 'the run directory has no run.json written by gstack-autoplan',
|
||
fix: 'start with `gstack-autoplan next --plan <file> --out <dir>` (the first call initializes the run)',
|
||
},
|
||
PHASE_NOT_AWAITING: {
|
||
anchor: 'phase-not-awaiting',
|
||
summary: 'the submitted phase or voice is not waiting for a result',
|
||
fix: 'run `gstack-autoplan status --out <dir>` and submit for the attempt `next` printed',
|
||
},
|
||
ATTEMPT_MISMATCH: {
|
||
anchor: 'attempt-mismatch',
|
||
summary: 'the attempt id does not name the open attempt for this phase and voice, or it was already submitted',
|
||
fix: 'pass the attempt id `next` printed for this voice; a result is bound exactly once',
|
||
},
|
||
RESULT_RECEIPT_MISSING: {
|
||
anchor: 'result-receipt-missing',
|
||
summary: 'the result file does not start with the full-read receipt `INPUT: <phase> <snapshot sha256>` for this attempt',
|
||
fix: 'the reviewer must read the prompt file through EOF and begin its result with the INPUT line the prompt names; a result for another snapshot is refused',
|
||
},
|
||
RESULT_FINDINGS_MISSING: {
|
||
anchor: 'result-findings-missing',
|
||
summary: 'the result file has no valid canonical findings block (a ```gstack-findings fence of JSONL rows)',
|
||
fix: 'end the review with one ```gstack-findings fence, one JSON object per line with at least severity and title; `[]` rows are not findings, an empty fence means no findings',
|
||
},
|
||
MODEL_FAMILY_CONFLICT: {
|
||
anchor: 'model-family-conflict',
|
||
summary: 'the outside model is in the same family as the native reviewer, or its family is unknown',
|
||
fix: 'pass `--model <id>` of a different family (anthropic, openai, google, xai, deepseek, qwen, zai, moonshot, meta, mistral); a same-family result is not an outside voice',
|
||
},
|
||
SPEND_CAP_EXCEEDED: {
|
||
anchor: 'spend-cap-exceeded',
|
||
summary: 'spent + reserved + the next estimate would exceed the spend cap',
|
||
fix: 'raise `--spend-cap`, settle or release unsettled attempts, or stop; unknown charges count as their reservation',
|
||
},
|
||
CONSENSUS_MISSING: {
|
||
anchor: 'consensus-missing',
|
||
summary: 'a closed prior phase has no consensus record to include in the Eng dispatch',
|
||
fix: 'resume the run so the phase closes through reconciliation; the Eng prompt is never sent without every prior phase summary',
|
||
},
|
||
OUTSIDE_RUNNER_UNAVAILABLE: {
|
||
anchor: 'outside-runner-unavailable',
|
||
summary: 'the selected outside-voice runner cannot run here (CLI missing, no API key, or no result file for host-subagent)',
|
||
fix: 'install or log in the CLI (`gstack-codex-login` from OPENAI_API_KEY), set an API key for `--runner api`, or pass `--result <file> --model <id>` for `--runner host-subagent`',
|
||
},
|
||
// PR D (gstack-plan-reality check).
|
||
REALITY_ROW_MISSING: {
|
||
anchor: 'reality-row-missing',
|
||
summary: 'a plan review carries no `REALITY:` line for an applicable reality row, or marks a tier 1 row n/a',
|
||
fix: 'work the row (`gstack-plan-reality rows --phase <p>` prints its check) and emit `REALITY: <row> pass|finding <summary> <file:line>`; unattended, the phase is incomplete until it does',
|
||
},
|
||
REALITY_RECEIPT_MISSING: {
|
||
anchor: 'reality-receipt-missing',
|
||
summary: 'a `REALITY:` line names its row but carries no file:line or commit receipt',
|
||
fix: 'cite where the check was made (`path:line`, `path:line-line` or a commit sha); a row without a receipt is a claim, not a row',
|
||
},
|
||
// C-pregate (plan C4, C6, C8): generated-file registry, issue links, the cheap pre-gate.
|
||
REGEN_REGISTRY_INVALID: {
|
||
anchor: 'regen-registry-invalid',
|
||
summary: '.gstack/generated.json is not valid JSON, has an unknown key, a missing field or a duplicate id',
|
||
fix: 'run `gstack-regen validate`, compare with `gstack-regen init --explain`, and fix the file on the base branch',
|
||
},
|
||
REGEN_CONTAINMENT: {
|
||
anchor: 'regen-containment',
|
||
summary: 'a registry glob escapes the repository or a command is not one line under 512 bytes',
|
||
fix: 'use repo-relative globs without `..`, at most 64 per list, and a single-line command',
|
||
},
|
||
REGEN_STALE: {
|
||
anchor: 'regen-stale',
|
||
summary: 'a generated output differs from what its registered command produces (stale, or hand-edited)',
|
||
fix: 'run the printed regen command and commit the outputs; never hand-merge a generated file',
|
||
},
|
||
REGEN_COMMAND_FAILED: {
|
||
anchor: 'regen-command-failed',
|
||
summary: 'a registered regen command exited non-zero in the scratch worktree',
|
||
fix: 'run the command by hand in the repository, read its output, fix the generator or its inputs',
|
||
},
|
||
PREGATE_CONFIG_INVALID: {
|
||
anchor: 'pregate-config-invalid',
|
||
summary: '.gstack/pregate.json has an unknown key, a wrong type, a non-downgradable check in `warn`, or an uncontained glob',
|
||
fix: 'run `gstack-pregate validate` and fix the file on the base branch (secrets and required lanes cannot be downgraded)',
|
||
},
|
||
PREGATE_SECRETS: {
|
||
anchor: 'pregate-secrets',
|
||
summary: 'gstack-redact found a HIGH or MEDIUM finding in the branch’s added lines',
|
||
fix: 'rotate the credential, remove it from the added lines (git history included), then rerun; `secrets` cannot be downgraded',
|
||
},
|
||
PREGATE_STRAYS: {
|
||
anchor: 'pregate-strays',
|
||
summary: 'a file was added at the repository root or outside the usual directories (scratch, tmp, zz-*)',
|
||
fix: 'delete the stray or move it under a tracked directory; declare an intentional root file in .gstack/pregate.json `strays.allow`',
|
||
},
|
||
PREGATE_LITERALS: {
|
||
anchor: 'pregate-literals',
|
||
summary: 'a changed exported constant’s old value still appears in the test tree (nightly- and Heavy-only files included)',
|
||
fix: 'update every listed test to the new value, or keep the old constant; a nightly-only file is still a consumer',
|
||
},
|
||
PREGATE_NO_LANE: {
|
||
anchor: 'pregate-no-lane',
|
||
summary: 'a touched file maps to no test lane through imports, repo-path literals, the bin/<name> convention or a declaration',
|
||
fix: 'add a test that names the file, or declare its lane (or an explicit empty lane) under `dependencies` in .gstack/pregate.json',
|
||
},
|
||
PREGATE_LANE_FAILED: {
|
||
anchor: 'pregate-lane-failed',
|
||
summary: 'a local test lane the touched files select failed or ran zero tests under pipefail',
|
||
fix: 'read the lane log, fix the in-branch failure, rerun `gstack-pregate --stage tests`; a `| tail`-hidden exit is still a failure',
|
||
},
|
||
PREGATE_INCOMPLETE: {
|
||
anchor: 'pregate-incomplete',
|
||
summary: 'a check timed out, its tool is unavailable, or the tree changed while it ran; the result is incomplete, never pass',
|
||
fix: 'install the named tool or raise --timeout, keep the tree still during the run, then rerun the check',
|
||
},
|
||
PREGATE_STALE: {
|
||
anchor: 'pregate-stale',
|
||
summary: 'pregate.json was recorded against a different tree than the one being published',
|
||
fix: 'rerun `gstack-pregate` after the final generation and stamp; publication needs results that are current',
|
||
},
|
||
PREGATE_REMOTE_UNCLEARED: {
|
||
anchor: 'pregate-remote-uncleared',
|
||
summary: 'a requires-remote lane obligation (a platform lane this machine cannot run) has no matching receipt',
|
||
fix: 'dispatch the named workflow job, then pass its run id with `gstack-pregate clear --remote <workflow/job> --run-url <url>` (or an evidence bundle lane) before publishing',
|
||
},
|
||
ISSUE_UNREADABLE: {
|
||
anchor: 'issue-unreadable',
|
||
summary: 'an issue a closure trailer or --issue names could not be read through gstack-issue-guard, so its link is Refs, never Fixes',
|
||
fix: 'check `gh auth status` and the issue number; rerun gstack-issue-links once the issue reads',
|
||
},
|
||
// PR E (multi-agent wave): gstack-eval-plan, gstack-lane-check, gstack-evidence verify,
|
||
// gstack-owner-brief, gstack-contributor-mode, gstack-banned-terms.
|
||
PREREG_INCOMPLETE: {
|
||
anchor: 'prereg-incomplete',
|
||
summary: 'the preregistration file lacks a required section or a section is empty',
|
||
fix: 'fill every section `gstack-eval-plan prereg --check` names (bars, arms, decoys, stop rules, held-out exposure) before any priced call',
|
||
},
|
||
EVAL_PRICE_MISSING: {
|
||
anchor: 'eval-price-missing',
|
||
summary: 'an arm names a model with no row in the price table, so its admission cannot be estimated',
|
||
fix: 'add the model to lib/pricing.ts (provider page, USD per million tokens, as_of) or drop the arm; a $0 estimate is never an admission',
|
||
},
|
||
EVAL_DRY_RUN_FAILED: {
|
||
anchor: 'eval-dry-run-failed',
|
||
summary: 'the $0 stub-model dry run found an input or checklist problem (input shape, output budget, retry prompt)',
|
||
fix: 'repair the named input rows or settings and rerun `gstack-eval-plan dry-run`; the pilot refuses to start on a failed dry run',
|
||
},
|
||
EVAL_ARMS_UNRESOLVED: {
|
||
anchor: 'eval-arms-unresolved',
|
||
summary: 'no eval arm could be resolved from the model policy or the repo policy document',
|
||
fix: 'run `gstack-models resolve --role eval-arms`, or pass `--arms <model,model>` / `--policy-doc <file>` naming models in the price table',
|
||
},
|
||
EVAL_BUDGET_EXHAUSTED: {
|
||
anchor: 'eval-budget-exhausted',
|
||
summary: 'the pilot or run stopped because spent + reserved + unknown charges reached the cap',
|
||
fix: 'read the spend table, settle or release open attempts, then raise `--cap` deliberately or stop; never rerun to make room',
|
||
},
|
||
LANE_CONFLICT: {
|
||
anchor: 'lane-conflict',
|
||
summary: 'a planned file is already changed by an in-flight branch or pull request',
|
||
fix: 'assign the file one owner: wait for that PR, stack on its branch, or drop the file from this lane',
|
||
},
|
||
LANE_REF_UNAVAILABLE: {
|
||
anchor: 'lane-ref-unavailable',
|
||
summary: 'a branch or pull request named for the lane check could not be resolved',
|
||
fix: 'fetch the branch (`git fetch origin <branch>`) or check `gh auth status`; an unresolved lane is reported, never assumed clear',
|
||
},
|
||
EVIDENCE_UNVERIFIED: {
|
||
anchor: 'evidence-unverified',
|
||
summary: 'a bundle claim could not be verified on this machine (tree moved, log missing, or the probe was not rerun)',
|
||
fix: 'rerun the lane here with `gstack-evidence verify <bundle> --rerun`, or treat the claim as an imported assertion',
|
||
},
|
||
BANNED_TERM_FOUND: {
|
||
anchor: 'banned-term-found',
|
||
summary: 'the diff adds a banned term outside its allowed locations',
|
||
fix: 'remove the term or move the text to a path `.gstack/banned-terms.json` allows for it',
|
||
},
|
||
BANNED_TERMS_INVALID: {
|
||
anchor: 'banned-terms-invalid',
|
||
summary: '`.gstack/banned-terms.json` is malformed or an allowed path escapes the repository',
|
||
fix: 'keep `terms` a non-empty string list and every `allowed_paths` entry a plain repo-relative glob (no `..`, no absolute path)',
|
||
},
|
||
CONTRIBUTOR_SOURCE_UNAVAILABLE: {
|
||
anchor: 'contributor-source-unavailable',
|
||
summary: 'neither a `maintainers` list in .gstack/ship-policy.json nor the collaborators API could classify the PR author',
|
||
fix: 'add `maintainers` to the ship policy, or run `gh auth login`; the review proceeds with contributor mode off and records the source',
|
||
},
|
||
BRIEF_INPUT_MISSING: {
|
||
anchor: 'brief-input-missing',
|
||
summary: 'the run directory lacks a file the owner brief renders from (gate.json, decisions.jsonl or findings.jsonl)',
|
||
fix: 'finish the run to the gate (`gstack-autoplan next`) so the gate list and decisions exist, then render the brief',
|
||
},
|
||
DRAFT_DIRECTION_UNRESOLVED: {
|
||
anchor: 'draft-direction-unresolved',
|
||
summary: 'a `draft direction stands until the owner decides` line names a decision that is still pending',
|
||
fix: 'answer the gate (`gstack-autoplan answer`) before closing; the close rewrites only decided lines',
|
||
},
|
||
PREGATE_GUARD_FAILED: {
|
||
anchor: 'pregate-guard-failed',
|
||
summary: 'a repo guard from .gstack/pregate.json whose files the branch touched exited non-zero',
|
||
fix: 'run the guard command printed in the table, apply the guard\'s fix line, commit, then rerun `gstack-pregate run --allow-repo-commands`',
|
||
},
|
||
MIGRATION_COLLISION: {
|
||
anchor: 'migration-collision',
|
||
summary: 'a migration number this branch adds is already used on the base branch or by another open PR',
|
||
fix: 'take the next free migration number when this PR is next to merge (rename the file and its id), commit, then rerun `gstack-pregate regate --against <gated-head> --bundle <file>`',
|
||
},
|
||
SEALED_OUTPUT_REFUSED: {
|
||
anchor: 'sealed-output-refused',
|
||
summary: 'a push or PR would publish sealed or eval output (a sealed path, a transcript, a quoted answer, a seed or a local home path) to a public or unknown destination, or the sealed policy is unreadable',
|
||
fix: 'remove it from every published commit, or add one reviewed `rule<TAB>path<TAB>sha<TAB>reason` line to the base branch\'s .gstack/sealed-allow.tsv; a private destination sets sealed.visibility "private" in the base branch\'s .gstack/pregate.json',
|
||
},
|
||
EVAL_GATE_UNPRICED: {
|
||
anchor: 'eval-gate-unpriced',
|
||
summary: 'the paid gate has no call-plan file (no `--plan-file`, no base-policy `price_file`) or the file has no GSTACK_EVAL_CALLS lines, so its cost is unknown',
|
||
fix: 'have the repo commit a call-plan file (docs/eval-calls.md) and set `price_file` in .gstack/pregate.json, or pass `--allow-unpriced "<reason>"` only on the owner\'s say-so',
|
||
},
|
||
EVAL_GATE_OVER_CAP: {
|
||
anchor: 'eval-gate-over-cap',
|
||
summary: 'the paid gate\'s priced total, or one call\'s reservation (input plus maximum output), is above the cap',
|
||
fix: 'cut questions, arms or `max_out_tokens` in the call plan, or raise `--cap-usd` / `--smoke-cap-usd` deliberately with the owner; never split calls to slip under it',
|
||
},
|
||
EVAL_PLAN_STALE: {
|
||
anchor: 'eval-plan-stale',
|
||
summary: 'the call-plan file\'s binding does not match (a different gate command, a head behind HEAD, an input whose sha256 at HEAD differs) or the declared price_file is not in HEAD',
|
||
fix: 'regenerate the call-plan file at HEAD for the exact gate command you are approving, commit it, and rerun `gstack-eval-plan price-gate`',
|
||
},
|
||
PLAN_SECTIONS_INVALID: {
|
||
anchor: 'plan-sections-invalid',
|
||
summary: 'the plan does not have exactly one non-empty `## Implementation plan` section followed by one `## Review record` section',
|
||
fix: 'add the missing heading, or rerun the first `gstack-autoplan next` with --scaffold to review a scaffolded copy (your plan file is never edited); duplicate, reversed or empty sections are fixed by hand',
|
||
},
|
||
PLAN_AMEND_PENDING: {
|
||
anchor: 'plan-amend-pending',
|
||
summary: 'the owner accepted plan changes at the gate that the plan has not absorbed yet',
|
||
fix: 'edit the plan, then `gstack-autoplan revise --out <dir> --plan <file> --incorporates <decision id,...>`; or drop one with `gstack-autoplan revise --out <dir> --withdraw <decision id>`',
|
||
},
|
||
AMENDMENT_UNKNOWN: {
|
||
anchor: 'amendment-unknown',
|
||
summary: '`revise --incorporates` or `--withdraw` named a decision that is not a pending amendment (never accepted as a plan change, or already incorporated or withdrawn)',
|
||
fix: 'name only the ids the refusal lists after `pending:`; with none pending, drop --incorporates and run `gstack-autoplan revise --out <dir> --plan <file>`',
|
||
},
|
||
GSTACK_VERSION_CHANGED: {
|
||
anchor: 'gstack-version-changed',
|
||
summary: 'the run directory was written by a different gstack version',
|
||
fix: 'nothing to do for an older run (journaled forward migrations ran); a run written by a newer gstack needs that gstack: upgrade with bin/gstack-upgrade',
|
||
},
|
||
ARTIFACT_GATE_UNSETTLED: {
|
||
anchor: 'artifact-gate-unsettled',
|
||
summary: 'the run says complete while an approval item of its last gate revision or an accepted plan amendment is still pending',
|
||
fix: 'answer the gate until every approval settles and revise (or withdraw) every accepted change with `gstack-autoplan`; never hand-edit status',
|
||
},
|
||
// Field wave lane A (items 1-4): gstack-upgrade, the Capy skill bootstrapper, install admission.
|
||
UPGRADE_REFUSED: {
|
||
anchor: 'upgrade-refused',
|
||
summary: 'bin/gstack-upgrade changed nothing: the checkout is dirty, diverged, pinned (detached or off the remote default branch), not a gstack checkout, or the incoming release holds',
|
||
fix: 'resolve the named cause (commit or move the listed changes, check out the default branch), then re-run bin/gstack-upgrade; it never stashes or resets',
|
||
},
|
||
UPGRADE_FETCH_FAILED: {
|
||
anchor: 'upgrade-fetch-failed',
|
||
summary: 'bin/gstack-upgrade could not fetch the remote default branch, so it cannot tell whether the checkout is current',
|
||
fix: 'check network access to the checkout\'s origin (git -C <checkout> remote -v), then re-run bin/gstack-upgrade',
|
||
},
|
||
REVISION_BELOW_MINIMUM: {
|
||
anchor: 'revision-below-minimum',
|
||
summary: 'the installed gstack revision is below the supported minimum declared in capy/support.env on the remote default branch',
|
||
fix: 'run the Capy skill installer without --check (it fast-forwards a clean, unpinned checkout), or bin/gstack-upgrade; a pinned, dirty or diverged checkout is named in the row',
|
||
},
|
||
CAPY_WRAPPER_STALE: {
|
||
anchor: 'capy-wrapper-stale',
|
||
summary: 'the Capy skill copy that ran is older than the checkout\'s GSTACK_CAPY_MIN_WRAPPER',
|
||
fix: 'ask the owner to copy <checkout>/capy/skill/gstack/ over the Drive skill folder; the checkout wrapper keeps working meanwhile',
|
||
},
|
||
INSTALL_UNVERIFIED: {
|
||
anchor: 'install-unverified',
|
||
summary: 'a Capy session (GSTACK_CAPY=1) has no passing install verdict for this checkout, HEAD and workflow, so the workflow refused to start',
|
||
fix: 'run the Capy skill installer with --for <workflow> on this machine and launch with the GSTACK_LAUNCH line it prints',
|
||
},
|
||
} as const satisfies Record<string, ResultCode>;
|
||
|
||
export type ResultCodeName = keyof typeof RESULT_CODES;
|
||
|
||
/** Alias kept for the doctor/installer side. */
|
||
export type ResultCodeId = ResultCodeName;
|
||
|
||
export const TROUBLESHOOTING_DOC = 'https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md';
|
||
|
||
/** `(CODE; <doc>#anchor)` — the suffix the doctor and installer rows carry. */
|
||
export function resultCodeSuffix(code: ResultCodeName): string {
|
||
return `(${code}; ${TROUBLESHOOTING_DOC}#${RESULT_CODES[code].anchor})`;
|
||
}
|
||
|
||
/** One error line: `<context>: <summary>[ (<detail>)]; fix: <fix> (<CODE>)`. */
|
||
export function resultLine(context: string, code: ResultCodeName, detail?: string): string {
|
||
const row: ResultCode = RESULT_CODES[code];
|
||
const what = detail ? `${row.summary} (${detail})` : row.summary;
|
||
return `${context}: ${what}; fix: ${row.fix} (${code})`;
|
||
}
|