Files
gstack/docs/CONTRIBUTOR_REFERENCE.md
Garry Tanandcapy-ai[bot] 14d7414af3 v1.93.33.0 wave(review): #3705 #3744 #3745 #3764 #3765 #3795 (#3852)
* fix(ship): base merges, adjacent base edits, test-only and audit-requested fixes no longer force a second review loop (GST-407, GST-384, GST-391)

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

* ship driver: trim two phrases for headroom with #3662

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

* ship driver: keep the Next steps heading phrase the handoff check reads; trim elsewhere

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

* test: regenerate ship goldens

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

* fix: pre-landing review fixes

comparePath returns same only when every hunk sits where the base change puts it (patch-ids ignore positions) and treats a file-mode change as changed; the scoped-review packet says when a fence is truncated; --docs-result must be the posted audit's result; the --context-reviewed record goes beside the packet, outside the worktree. Negative controls for each.

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

* fix: pass-2 review fixes

A test edit routes as tests= only when it adds lines with no gate tag; a removed or changed line or an unreadable test diff goes through the full loop. Diffs skip textconv. A hunkless patch (binary, empty, mode-only) is same when its patch-id matches. Without --reviewed-base test files route as before. finish's packet path and attempt's state directory have fallbacks.

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

* fix: Step 11 review fixes; a version-only stamp_files edit after review is release metadata (GBRA-251)

gateEditedPaths reads removed lines only inside hunks; carry refuses when a path listing fails; docs freshness stops on a failed listing; attempt counts an unreadable state file. releasePaths includes stamp_files paths whose edit is a version swap under stamp_patterns.

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

* fix: no test edit skips review; guard packet writes; exclusive attempt state

A test-only edit with no gate tag gets the scoped fix-check of its test hunks instead of a lanes-only rerun. Packet write failures print a STOP or RECEIPT_CARRY_REFUSED. attempt's allowance uses an exclusive create.

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

* fix: attempt prints gstack's install command; carry checks the product diff outside context paths

The attempt STOP no longer echoes the audit child's fix text. A scoped-review carry now requires the product patch-id without the context paths to match at both heads.

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

* fix: oversized packets take the full loop; exact --docs-requested match; name-only test globs only in test dirs

A scoped-review packet section over 200 lines sends that path to the full loop instead of a truncated packet. The docs-result path match is exact. test_*.py, *Test.java and similar count as test code only under a test directory.

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

* docs(unattended): carry's scoped-review refusal and what it keeps

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

* docs: carry's scoped-review route in ship-policy, review-policy and troubleshooting (docs-a-1)

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

* test(windows): curate ship-rereview-triggers out of the Windows lane like ship-release (run 41193336209)

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

* fix(review-log): record a notes-only /ship pass clean; name the cause and repair when a pass's tree moved (GST-430)

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

* fix(receipt): carry and write handle PRs with thousands of files; tree receipt fails closed on a failed diff (GST-448)

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

* fix(ship): preflight routes RECEIPT_CARRY_SCOPED_REVIEW to the scoped review; carry --dry-run writes no packet (GST-442)

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

* docs(ship): Step 1.4 and Step 16 route the scoped-review refusal and the --docs-requested fix; trims keep the cap (GST-442, GST-444)

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

* fix: /ship review pass 1: notes-only floor, scoped carry order, named temp and diff refusals

- A notes-only pass is recorded clean only when every finding is fixed, or noted or deferred below the
  blocking floor (never CRITICAL or blocking), by its logger-stamped action, and the findings account
  for the claimed issue and critical counts (GST-430).
- The preflight scoped carry route commits and pushes the base-merge fix before writing the packet (GST-442).
- patchId refuses by name on an unwritable TMPDIR; the tree receipt names why its diff failed (GST-448).
- Step 16 stage 3 spells out --docs-requested with its --docs-result (GST-444).

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

* test: pin the Step 16 --docs-requested route with its --docs-result

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

* fix: a claimed clean review row with a logger-stamped open gate finding reads UNVERIFIED (red-team, pass 1)

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

* fix: Step 11 findings: gate rows from --findings are stamped, one open rule, stale patch-id dirs swept

- deriveFromFindings stamps gate rows with the session kind before deriving status, so a findings file cannot
  dispose of a gate finding the unattended logger forces open (outside-voice#1).
- reviewFreshness reads a claimed row's findings with isOpenByAction, the rule the notes-only rewrite uses;
  noted and deferred clear a finding only when it is marked blocking: false and is not CRITICAL.
- patchId sweeps gstack-patch-id-* directories over an hour old that a killed carry left in TMPDIR.

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

* fix: Step 11 round 2: stamped-open gates win over status and disposition; resolved gate, skipped and below-floor actions stay CURRENT

- isOpenFinding treats action 'open' (the logger's forced-open stamp) as open before a row's status or disposition.
- reviewFreshness flags a claimed clean row only for an 'open' action or a CRITICAL or blocking deferral or note, so
  interactive kept and restored gates, skipped advisories and INFORMATIONAL deferrals read CURRENT as before.

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

* fix: carry dry run offers the scoped route last; patch-id diff under the git dir; changelog matches the review-log floor

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

* fix: a claimed clean review row with any unresolved non-gate CRITICAL finding reads UNVERIFIED (pass 2 red-team)

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

* docs: carry dry-run scoped route, patch-id directory, tree-receipt refusal (docs-d-1 audit)

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

* fix(ship): Step 3 names merge conflicts in its STOP sentence again (ship-workflow-clarity pin)

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

* fix(tree-receipt): keep the gated-to-current diff at the file helper timeout (60 s); pregate literals check

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

* test(carry): large-diff fixture sets core.longpaths so Git for Windows can add its long receipt paths (windows-free-shard 1, run 41210080055)

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

* fix(review): a stale local base branch yields to its origin twin in gate-diff and diff-scope (GST-462)

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

* fix(ship): Step 11 saves the targeted record and the receipt writes carry it; Step 9 clears it (GST-464)

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

* changelog: GST-464 and GST-462

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

* fix(review): pass 1: ahead and diverged labels, diff-scope needs shared history; preflight refreshes origin/<base> and stops on a diff far beyond the PR (GST-462, HARN-15)

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

* fix: pass-2 review fixes for base twin, prSized stop and targeted record

Resolve the tracking twin by refs/remotes/<remote>/<ref> and only for a bare
branch name, keep a local base that is ahead or diverged, quote the prSized
stop's base and name the fork case, and have every Step 9 entry create,
exclude and clear .gstack/tmp/targeted.json.

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

* fix: origin's tracking base wins whenever related; prSized lower bound

The fix-check found that keeping a local base ahead of origin left unpushed
base commits out of the gate scan while review-plan listed them. The tracking
branch now wins whenever it shares history with HEAD, an explicit origin/main
reads refs/remotes/origin/main, the preflight also stops when the diff lists
fewer files than the PR, and the Step 9 exclude line survives a missing
.git/info and an exclude file without a final newline.

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

* changelog: the tracking-ref claim holds while the tracking branch exists

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

* test: repin the two ship section briefs under no-loss replay GST-464

GST-464 brief-identity check: every reviewer brief lib/review-brief.ts cuts
from ship/sections/review-army.md and adversarial.md stays byte-identical to
the bodies the L and GST-257 replays admitted; only lead instructions outside
the cut spans changed. The check is a test CI runs.

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

* ship: keep Step 9's PASS_HEAD sentence whole; repin review-army under GST-464

The targeted-record line follows the PASS_HEAD/RANGE_BASE sentence instead of
splitting it (test/review-fix-range.test.ts pins that sentence). The brief-body
check still holds: every reviewer brief is byte-identical.

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

* fix: restore bin/gstack-ship-receipt after the main merge

The merge commit b10e2db wrote a truncated copy of the file. This is the
three-way merge of both sides with the refused() union: main's codes plus
RECEIPT_CARRY_SCOPED_REVIEW.

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

* review-collect: narrow the consumer scan and give it one 300 s deadline (GST-544)

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

* review-collect: keep the consumer scan deadline under the 180 s plan timeout (review fix)

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

* review-collect: plain paths under --literal-pathspecs; whole-millisecond timeout override (review fix)

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

* review-collect: whole-tree grep for non-UTF-8 names, no argument spread, name spawn errors (review fix)

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

* review-collect: cap the consumer grep output at 256 MB across calls; per-scan deadline wording (review fix, cycle 4)

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

* review-collect: read a missing spawn status as null (Bun 1.4.2 returns undefined on ENOENT)

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

* test: review-collect whole-tree helper reads a null status as -1 (typecheck:test ratchet)

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

* review-bots: /review and /ship triage every configured review bot, not only Greptile (#3122)

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

* fix: pre-landing review fixes (gh before 2.48 pages without --slurp; driver routes Step 10; tests for guards, review-bot fence, REMOTE_SLUG owner)

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

* fix: drop the review-bot fence test that could not fail (the tool-fence WARN degrade is unreachable: placeholder spans are skipped first)

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

* fix: Step 11 review fixes (record by comment id so a filename never reaches a shell; lookup failures are errors; tojson pages; longest bot prefix wins)

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

* fix: pass-2 review fixes (record refuses * paths, colliding ids and a missing fetch dir; parent requires id and dir; botOwning doc)

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

* review: classify the PR type and always check repo conventions (#356)

The checklist now opens with a PR-type line (application, CI/infra,
scripts/tooling, docs-only or mixed). Application and mixed diffs run
every category; other types always run five categories, including the
new Repo Conventions & Platform Fit, and report the rest as not
applicable in one line instead of skipping silently.

No-loss replay: GSTA-224 H1 preregistered no-loss run (run id
gsta224-h1-20261031, Opus 5.5, 81 calls, $9.53) against main's checklist
with a no-gstack arm; every preregistered bar passed. The
review/checklist.md pin in test/review-packets-removed.test.ts moves to
a32963ef with replay 'GSTA-224 H1'.

Refs GSTA-224

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

* review-bots: allowlist the record placeholders and match Step 10's id rule in tests

The record fence uses <comment-id>, <fix|fp|already-fixed> and <category>,
each with an enum or numeric grammar; <greptile-dir> becomes <bot-dir>.
review/greptile-triage.md also lists every REVIEW_BOTS skip reason.

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

* docs: review-bot triage covers every configured bot (#3122)

README, CONTRIBUTING, the contributor reference and standalone-binaries
name gstack-review-bots and the review-bot fence; TODOS drops the #3122
item; the changelog fragment covers record-by-id and lookup errors.

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

* test: the PR-type test also pins the compose, Terraform and README/CHANGELOG phrases

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

* test: follow the review-bot rename in the remote free tier's three reds

The cross-skill consistency judge describes review-bot-history.md and its
record helper; the tracker-guard exemption for the removed raw Greptile
fetch in review/greptile-triage.md is dropped (the fetch now lives in
gstack-review-bots, whose bodies still reach context only through
gstack-issue-guard); the Step 10 PR-check test expects 'Review bots: not
run'. HARNESS_VERSION stays 6: skill-fixture.ts and llm-judge.ts are
recorded non-behavioral for every other case.

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

* test: keep the cross-skill judge's rubric to one renamed bullet

Name review-bot-history.md beside the older greptile-history.md so the
calibration corpus still matches, and keep the REMOTE_SLUG bullet. One
live 3-sample panel at this tree: consistent 3/3, scores 4/4/4.

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

* changelog: H1 fragment passes prose lint

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

* changelog: name the H1 fragment after its branch; TODOS drops #356

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

* docs: /review's PR type step in docs/skills.md (#356)

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

* ship-policy: review_exclude_paths key and loadPolicyAt (GST-508)

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

* review: apply the base policy's review_exclude_paths and the reviewer diff ceiling (GST-508)

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

* receipt: bind review_exclude in write, carry and the queue (GST-508)

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

* docs, tests and changelog for review_exclude_paths (GST-508)

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

* fix: pre-landing review fixes (binary by bytes, links kept, untracked files out of the selection)

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

* fix: keep binary files at code paths under review_exclude_paths

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

* docs: name the reasons that keep a binary file under review_exclude_paths

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

* fix: keep every review_exclude_paths candidate when the gate scan is unknown; docs example names its version

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

* test: keep the NUL fixture clear of the sync-spawn tripwire

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

* changelog: split the ceiling sentence

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

* fix: keep a live binary file whose bytes cannot be read; tests render outside voices from a clean checkout and read the ship fixture text

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

* test(review-exclude): explicit +x for the census script; skip the chmod-000 read case on Windows and root

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

* refactor: move loadPolicyAt to lib/ship-policy-at.ts so lib/ship-policy.ts stays under the 800-line module cap

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

* fix: refuse a base that contains HEAD; skip a self-pointing origin/HEAD (GST-774)

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

* chore: regenerate gstack-doctor-components.sh for BASE_CONTAINS_HEAD

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

* fix: land resumes a merged merge-only PR instead of refusing BASE_CONTAINS_HEAD (GST-774 review)

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

* docs: changelog fragment for GST-774

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

* docs: troubleshooting names diff-scope base_contains_head and the self-pointing no_base case (GST-774 docs audit)

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

* test: repin the reviewer briefs to GST-508's no-loss-tested bytes

ship/sections/review-army.md, ship/sections/adversarial.md and lib/review-depth.ts now pin
the bytes the GST-508 preregistered no-loss replay ran at ef2760f (handoffs/GSTA-202/noloss-results.md
in the GStack Minder drive): Arm V 33/41 vs R 33/41, delta 4, exact sign flip, p_NI 0.0279 (pass).
The merge of main 83ae96e leaves these three files byte-identical to ef2760f, and an Arm P unit
(3593) renders byte-identical briefs at ef2760f and at the merge.

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

* fix: GST-774 review round 1 — merged resume depth, aliased and detached PR checkouts, develop/trunk defaults

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

* ship: Step 20 says a metrics row, keeping DRIVER.md at 12 KB after the main merge

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

* test: ship-parts resend case leaves room for the first phase-3 part after the Step 9 targeted-record sentence

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

* fix: /ship delivers review/checklist.md in two parts so checklist v1 fits the 24576-byte part cap (#3670)

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

* changelog: drop the GST-407 and GST-430 fragments, already released on main (stacked history)

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

* wave review-53cfb5bfe011: lib/ship-policy.ts back to 800 lines (module-size ratchet after merging #3744 and #3795)

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

* wave review-53cfb5bfe011: /ship pass 1 fixes

- lib/ship-receipt-depth.ts: a merged resume (land after a true merge) no longer recomputes the review_exclude digest over the emptied range, so it lands instead of RECEIPT_INVALID (failing-first land test with review_exclude_paths active).
- bin/gstack-review-dispatch render: a standalone targeted render under an active review_exclude_paths refuses and points at the pass manifest voices stage, instead of writing a brief with no selection that leaves verify dirty (failing-first test).
- test/default-base.test.ts: #3795 expectations follow main R51 depth (light for an 80-line code diff; runtime_lines=80 still proves the real base).
- test/review-dispatch.test.ts: the GST-897 fingerprint test renders from the clean checkout, like its neighbor, because CI rebuilds the tracked lib/diagram-render/dist/diagram-render.html and the reviewer diff ceiling refused the ROOT render.

* wave review-53cfb5bfe011: /ship pass 2 fixes

- lib/review-exclude.ts: binaryContent reads each blob's first 8,000 bytes (git cat-file blob, 8 KB buffer) instead of one --batch read capped at 256 MB, so head mode (receipt, carry, queue) and the plan agree past 256 MB of blobs; an unreadable blob is kept, as in live mode. Failing-first test with two 270 MB blobs.
- lib/review-bots.ts: a file-level bot review comment (subject_type file, no line position) is triaged instead of read as outdated. Failing-first test.
- test/ship-queue-land.test.ts: the merged-resume exclusion test runs its gate scan under the fixture state root, not the real home (CI free-suite 6 home containment).

* fix: merge follow-ups for main 3d99c22: origin/* stays qualified in diff-scope's twin step, retro skeleton back under its cap, combined no-loss pin GST-508+R51+GST-911

* fix(review): receipt write gate-scans whenever the base declares review_exclude_paths; collectLive never re-qualifies origin/<base> (#3852 /ship red-team#1, outside-voice#1)

* test: the docs-only receipt digest test keeps its gate scan out of the real home (free-suite shard 5 FREE_SUBSHARD_REJECTED)

* fix(merge fc292e22f): tentative REVIEW_EXCLUDE_KEY_VERSION, classify targeted.json, render main's new outside-voice test from a clean checkout

- lib/ship-policy.ts: REVIEW_EXCLUDE_KEY_VERSION 1.93.0.0 -> 1.94.0.0, the next-minor tentative form stamp_paths moves (docs/ship-policy.md, #3512); 1.93.0.0 is a released heading that never names the key.
- test/gstack-clean-producers.test.ts (new from main): KEPT gains targeted.json, the wave's --review-targeted input.
- test/review-dispatch.test.ts: main's wrong-tree test rendered the outside voice from ROOT; a CI build that dirties ROOT pushes that diff past the wave's REVIEW_DIFF_MAX_BYTES ceiling, so render refused and verify saw no brief (exit 3). It now runs in cleanCheckout like the wave's other outside-voice tests.

* test(merge 68410828f): GST-967's brief-body pin carries the wave's GST-508 placeholders

The merged sectionTexts differ from main's by exactly the wave's GST-508 exclude-pathspec lines (merged minus main equals wave minus fc292e22f); the 1,440-case plan grid still hashes to main's MAIN_PLANS. Checks: GStack Minder drive handoffs/GSTA-202/merge-6841-checks.md.

* fix(merge e85005482): targeted classes since the read accept an empty range; the wave's receipt tests pass main's targeted record

- lib/ship-receipt-depth.ts targetedClassesSince (main, GST-986) reads the range from the commit the targeted reader read to HEAD through headInputs, which since #3795 runs gstack-diff-scope's BASE_CONTAINS_HEAD check. When the reader read HEAD itself the range is empty, diff-scope reported base_contains_head, and every write with --review-targeted was refused (the product patch matched). It now passes merged=true (--merged), which skips only that check; an ancestor check already guards the range.
- test/review-exclude.test.ts and test/review-targeted.test.ts: since #3887 write refuses a money head with no targeted record and requires the record's head, so the wave's two writes that must succeed pass one, as main's own tests do (targetedAtHead in test/ship-receipt-depth.test.ts).

* test(merge e85005482): drop the wave's duplicate targeted.json KEPT entry

Main (#3887) classified .gstack/tmp/targeted.json in KEPT too; the auto-merge kept both keys, which typecheck:test rejects (TS1117). Main's entry stays.

* test(merge e85005482): ship size ledger entry for the merged tree (-232 B)

Main's ship size ledger plus the wave's entries summed to 345,334 B; the merged tree measures 345,102 B because DRIVER.md takes main's shorter Step 19 line. Recorded with scripts/size-ledger.ts record ship.

---------

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

13 KiB

Contributor reference

Contributor-only detail moved verbatim from CLAUDE.md, which Claude Code loads into every session and caps at 40,000 characters (#2096, test/claude-md-size.test.ts). CLAUDE.md keeps a one-line pointer to each section here.

Skill size budgets

Token ceiling: Generated SKILL.md files trip a warning above 160KB (~40K tokens). This is a "watch for feature bloat" guardrail, not a hard gate. Modern flagship models have 200K-1M context windows, so 40K is 4-20% of window, and prompt caching makes the marginal cost of larger skills small. The ceiling exists to catch runaway preamble/resolver growth, not to force compression on carefully-tuned big skills (ship, plan-ceo-review, office-hours legitimately pack 25-35K tokens of behavior). If you blow past 40K, the right fix is usually: (1) look at WHAT grew, (2) if one resolver added 10K+ in a single PR, question whether it belongs inline or as a reference doc, (3) only compress carefully-tuned prose as a last resort — cuts to the coverage audit, review army, or voice directive have real quality cost.

A second, harder ceiling guards the DISCOVERY surface: test/catalog-budget.test.ts caps the aggregate frontmatter name + description across all skills at CATALOG_BUDGET_TOKEN_EQUIVALENTS (1,423 today; each new skill ratchets it) token-equivalents (260-byte per-skill sub-cap), counted through the shared census in test/helpers/skill-census.ts. This one is enforced, not a warning — every host loads the full catalog every session, so growth here taxes every conversation. The failure message carries the re-measure + ratchet protocol. bin/gstack-context-bill shows the full token bill-of-materials for a skills tree (always-on vs per-invocation, --diff, --budget; --exact opts into the real tokenizer and POSTs file text to api.anthropic.com with an egress receipt).

The context-budget ratchet (test/context-budget-ratchet.test.ts, free, runs in bun run test) pins ABSOLUTE ceilings on two more ledgers: the always-on FULL-frontmatter aggregate (catalog-budget counts only name+description) and each skill's per-invocation eager tokens (SKILL.md + forced-read references — size floors and parity ratios guard these relatively, not absolutely), graded against test/fixtures/context-budget.json. A skill that grows past its ceiling fails; a new skill fails until it's consciously budgeted. For legitimate growth or a landed reduction, re-run bun test/helpers/capture-context-budget.ts and commit the refreshed fixture in the same commit, so ceilings ratchet down and every win is locked.

Egress receipts

Egress receipts at every off-machine sink (v1.63.0.0+). Every gstack-initiated send off the machine MUST write a hash-chained receipt to ~/.gstack/security/egress.jsonl BEFORE the send: TypeScript callers use writeReceipt from lib/egress-receipt.ts; shell scripts source bin/gstack-egress-lib.sh and use _receipted_curl / _receipted_git. Failure polarity is per-class: fail-closed for sensitive sinks (brain-sync, memory-ingest, gbrain-sync, telemetry, ngrok tunnels, mcp-verify, supabase-provision, and the Memorable bridge's per-prompt memorable-recall hand-off), fail-open

  • stderr warning for user-facing ones (design OpenAI and Gemini calls, update-check, dashboards, git-class ops). The new-sink scanner in test/egress-receipt-wiring.test.ts fails CI on an unreceipted curl / git push / fetch to a non-loopback host unless the file carries a reasoned entry in its SCANNER_EXEMPT list (user-directed page fetches, reachability probes, instruction strings, skill prose) — if you add a new off-machine sink, wire it through the helpers and add it to the enumerated sink list. aside exec (a gstack-composed prompt sent to Aside's agent) is a fail-open user-facing sink: skills call it through the _aside_exec wrapper that scripts/resolvers/aside.ts renders, never bare. Inspect with bin/gstack-egress (list | verify, exit 3 on tamper | grants). Threat model: forensic observability of ATTEMPTED egress, not an exfiltration control.

Skill linking and ownership

Prefix setting: Setup creates real directories (not symlinks) at the top level with a SKILL.md symlink inside (e.g., qa/SKILL.md -> gstack/qa/SKILL.md), plus links to each skill's runtime assets (sections/, templates, checklists — everything except SKILL.md, tests, build output, and .tmpl sources). Alias skills (_gstack-command, connect-chrome) install as rewritten copies, never symlinks. This ensures Claude discovers them as top-level skills, not nested under gstack/. Names are either short (qa) or namespaced (gstack-qa), controlled by skill_prefix in ~/.gstack/config.yaml. Pass --no-prefix or --prefix to skip the interactive prompt.

Ownership gate (#2119): setup writes a .gstack-owned marker into every skill directory it creates, and setup (the linker, the alias installer, both prefix-flip cleanups, and the retired-skill prune) and bin/gstack-relink only delete or link over an entry they can prove is gstack's. Strong proof (a symlink resolving into gstack, or the marker) allows deleting or refreshing the whole directory. Weak proof (a real SKILL.md byte-identical to the source, or carrying gen-skill-docs' two-line banner) covers only that one file, and a weakly-proven file that differs is moved to ~/.gstack/backups/skills/<ts>/<skill>/SKILL.md before gstack links over it. Anything else is a foreign skill: skipped, and named in setup's final summary. The rule lives in two copies (setup and bin/gstack-relink); keep them in sync until the shared helper filed in TODOS.md lands. The retired-skill prune (_prune_stale_generated) applies the same strong/weak split to renders of skills that no longer exist, through its own gate (_owned_for_windows_refresh: a real host directory is a candidate only when its SKILL.md carries the generated banner; the marker and byte identity are not consulted): it scans the render tree and every host skills dir, deletes a real render directory, removes a host symlink only when it resolves into gstack, cleans a bannered real directory through _cleanup_weak_dir, never follows a symlink inside the render tree, and recognizes a skill renamed through its frontmatter name:. Pinned by test/setup-link-ownership.test.ts, test/setup-cleanup-orphans.test.ts, test/setup-prune-stale-generated.test.ts, and test/relink.test.ts.

Redaction guard

Shared redaction engine catches credentials, PII, and legal/damaging content before it reaches an external sink (codex dispatch, GitHub issue/PR body, pushed commit). It is a guardrail, not airtight enforcement — git push --no-verify, direct gh issue create, and GSTACK_REDACT_PREPUSH=skip all bypass it. It catches accidents and carelessness, the 99% case. Do not claim it stops a determined leaker (a CHANGELOG line that does would fail a hostile screenshotter).

  • Engine + taxonomy: lib/redact-patterns.ts (the single source of truth — 3 tiers; HIGH = genuinely-secret credentials that block, MEDIUM = PII/legal/ internal + high-FP credential shapes that confirm via AskUserQuestion, LOW = FYI) and lib/redact-engine.ts (scan() + applyRedactions(); no I/O, and scan()'s one ambient input is the credential-named variables of process.env, read by value for the env.value HIGH finding unless ScanOptions.processEnv names another environment). Calibration matters: a gate that cries wolf gets ignored, so context-variable shapes (Stripe pk_live_, Google AIza, JWT, env *_KEY=) sit at MEDIUM.
  • CLI: bin/gstack-redact (exit 0 clean / 2 MEDIUM / 3 HIGH; --json, --auto-redact, --repo-visibility, --from-file). bin/gstack-redact-prepush is the opt-in git hook.
  • Skill docs are generated from scripts/resolvers/redact-doc.ts ({{REDACT_INVOCATION_BLOCK:<sink>}}) so /spec, /cso, /ship, /document-release, /document-generate never drift from the engine.
  • Scan-at-sink: always scan the EXACT bytes that will be sent — write to a temp file, scan that file, pass the SAME file to gh/git. Never scan a string then re-render (that reopens a scan-vs-send gap).
  • Visibility (no tier promotion): resolve once per run, order = local config (gstack-config get redact_repo_visibility, ~/.gstack so never committed) → gh → glab → unknown(=public-strict). Public repos get STERNER per-finding confirmation (no batch-acknowledge, no silent-proceed); MEDIUM is never auto-promoted to HIGH.
  • Tool-attributed fences: wrap Codex/review-bot/Greptile/eval output in ```codex-review / ```review-bot / ```greptile fences so example credentials those tools quote WARN-degrade instead of blocking. A live-format credential inside the fence still blocks.
  • Config keys: redact_repo_visibility (public|private|unknown, local-only override for repos gh/glab can't read), redact_prepush_hook (true|false). There is intentionally NO key to disable HIGH blocking.
  • Committed allowlist: .gstack-redact-allowlist at the root of the pushed commit lists exact matched spans reviewed as benign; the pre-push hook suppresses only a finding whose span equals an entry and lists every suppression on each push.
  • Audit: the /spec semantic pass appends a content-free record (categories + body sha256, no spec text) to ~/.gstack/security/semantic-reviews.jsonl (0600).

Checking out PRs from garrytan-agents

When the user says "check out " and the PR is from garrytan-agents/gstack (or any other fork that is NOT a collaborator on garrytan/gstack), do NOT just gh pr checkout. Fork PRs don't receive base-repo secrets (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.), so the eval/E2E CI jobs fail with empty-env auth errors regardless of what's set on the base repo.

Workflow: push the branch to garrytan/gstack (the base repo) and re-target the PR from there.

Concretely, after gh pr checkout <N>:

  1. Note the original PR number and head branch name.
  2. Push the same branch to the base repo: git push origin HEAD:<branch-name> (origin = garrytan/gstack, since the worktree is set up with that remote).
  3. Close the fork PR (gh pr close <N> --comment "moving to base-repo branch for secret access").
  4. Open a new PR from the base-repo branch: gh pr create --base main --head <branch-name>.
  5. New PR's workflows will get secrets automatically.

Why not fix it on the fork side? garrytan-agents isn't a collaborator on garrytan/gstack. Adding it as a collaborator (option A) or flipping the repo-wide "send secrets to fork PRs" toggle (option B) would let secrets reach fork PRs from anyone — broader blast radius than just moving this one branch. Option C (this section) keeps secret-distribution scope tight.

If the user asks you to skip the move (e.g., "just leave it as a fork PR"), respect that — eval CI will fail with empty-env auth, but check-freshness, workflow-lint, and windows-tests will still pass on the fork PR.

Auditing Conductor worktrees

Conductor workspaces are git worktrees, so after a few weeks of parallel lanes the checkout carries worktrees nobody remembers. bin/gstack-worktree-audit [-b origin/<base>] [--no-fetch] [repo-path] prints one TSV row per worktree beside the main one, largest first: size, age of the last commit, whether its head is merged into the base, clean / clean,ignored:<n> (only gitignored entries such as .env or build output) / wip:<n> (tracked edits) / scratch:<n> (untracked only) / missing (registered, directory gone) / broken (below) / unknown (git status failed), pushed / ahead<n> / no-remote / detached (against the local origin/<branch> tracking ref as last fetched), the PR on its branch through gh (#n/open|merged|closed, or -; a fork's PR on a same-named branch is ignored, so in a fork workflow where origin is your fork the column is -, and an open PR wins over an older closed one on a reused branch name), the date of the newest Claude Code chat that ran in it, and a bucket: prunable (registered, directory gone, not locked), then review for a locked entry whose directory is gone or a broken worktree whose own .git link is missing, then hold-wip, then hold-open-pr, then verify-recent-chat (a chat in the last four days), then safe, else review. safe means nothing tracked or untracked exists only in that worktree: a clean tree whose head is merged into the base, or whose PR is closed or merged with every commit pushed; an unpushed commit or an untracked file on a merged branch is review. Ignored entries do not change the bucket, but removing the worktree removes them too, so read the ignored:<n> count before you do. A squash-merged branch is never an ancestor of the base, so read MERGED together with PR. The audit only reads, apart from a best-effort fetch of the base ref that --no-fetch skips; removing a worktree (git worktree remove <path>) stays your call after the table. /landing-report points at it when the sibling table shows stale worktrees.