Files
gstack/scripts/lib/windows-curation.ts
T
Garry Tanandcapy-ai[bot] 0bf9d50808 v1.91.107.0 fix: the GBRA-40 field list (one Bun for install and check, rec gate approval, pregate never silent) (#3190)
* fix(autoplan): empty findings fences, repo-relative rows, PLAN_REALITY flags, pending/--all/provider (Lane B: B3 B6 B7 B8 B9 B10 B11)

B3  parseFences: the body group is optional (lazy), so an LF/CRLF empty fence is zero
    findings in both the canonical and record grammars; opener without closer still
    RESULT_FINDINGS_MISSING.
B6  plan-reality/3: item/section references are not quantities; backtick phrases are
    exempt only from the plan-scope rule-phrase check (B-f); a measured number inside
    backticks stays flagged in review scope. vaguePlanTerms() feeds an advisory
    `flagged: PLAN_REALITY ...` line under the p1 approval item (never blocking).
B7  canonical rows: absolute repo paths become repo-relative; snapshot/active-plan
    citations become the plan file name with the source line mapped (or dropped).
B8  `status` prints `PENDING: <id> phase=<p> voice=<v> result=<file>` per open attempt.
B9  `gstack-gate render --all`; paged output says to relay every page.
B10 `gstack-plan-reality check --plan <snapshot>`: a number in a "quoted" span copied
    verbatim from the reviewed plan needs no receipt (reviewer scope only).
B11 submit defaults the provider to host-subagent when --runner is omitted.

Lane B files: lib/autoplan-reconcile.ts lib/plan-reality.ts lib/autoplan-gate.ts
lib/gate-list.ts lib/autoplan-plan-text.ts lib/autoplan-run.ts bin/gstack-gate
bin/gstack-plan-reality scripts/resolvers/plan-reality.ts autoplan/sections/ceo-phase.md
autoplan/sections/eng-phase.md test/plan-reality.test.ts test/autoplan-gate-artifacts.test.ts

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

* fix(ubicloud): ubi-runner up/run -S accepts only Ubicloud's storage sizes, before any API call (A11)

Files: scripts/ubicloud/ubi-runner.sh, test/ubicloud-runner.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(issue-links): promotion candidates, Related PRs, validated flake issues, body/receipt consistency (Lane D: D1-D3)

D1: the GBRA-40 fix wave's gbrain #6279 body had no `## Issues` section and no
receipt (it was not composed by /ship, so gstack-issue-links never ran), and
#6252 appeared only as a subject parenthetical `(#6252)`, which the closure
rule correctly treats as Refs. The rule is kept. Same-repo references are now
printed as promotion candidates (`(promote: fix #N)`, `candidates` in --json),
and `gstack-ship-receipt write --issues ... --body` refuses a body that lacks a
line its issues object records (ISSUE_LINKS_MISSING), so a /ship body can no
longer drop its closing lines.

D2: open PRs by other authors whose closing set overlaps this PR's Fixes are
`Related: #N (@author)` (paged GraphQL closedByPullRequestsReferences; viewer
and this branch excluded; cross-repo PRs keep owner/repo#N). `Supersedes` and
`Contributed by` only with --supersedes/--credit; --no-auto-links skips the
lookup; the stderr preview names each added line's source. A failed lookup is
ISSUE_LOOKUP_FAILED (installer form with docs anchor) and keeps the other lines.

D3: a --ci-run match becomes a Flake line only for a bot-opened issue of this
repository that names that run (flake:json first_seen.run_id or this repo's
actions/runs URL) and a test; duplicates of one test keep the lowest number;
every refused match prints its reason.

Files: bin/gstack-issue-links, bin/gstack-ship-receipt, lib/issue-links.ts,
lib/ship-receipt.ts, lib/headless-artifacts.ts (ship-receipt issues.related),
lib/result-codes.ts (ISSUE_LOOKUP_FAILED, ISSUE_LINKS_MISSING,
resultLineWithDocs), bin/gstack-doctor-components.sh (regenerated),
docs/pregate.md, docs/troubleshooting.md, test/ship-issue-intent.test.ts,
test/helpers/pregate-fixture.ts (gh shim: repo view, api graphql, full issue rows).

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

* fix(ship): link issues by intent at publish and handoff; Fixes: trailer at commit (Lane D: D1, D2, D-a)

- Step 15: a commit fixing a user-named issue carries a `Fixes: #<n>` trailer
  (offset by tightening Step 15 so the skeleton stays at 80,998 <= 81,000).
- pr-body: an explicit "Issue links" step runs gstack-issue-links before the
  body is composed (previously only a placeholder inside the body template);
  interactive runs ask once about `(promote: fix #N)` references and add
  `--supersedes`/`--credit` only on the owner's confirmation; unattended runs
  keep Refs and list unpromoted candidates, Related lines and lookup failures
  in the completion message; `ISSUE_LINKS_MISSING` from the receipt write.
- handoff (no-PR lanes): runs gstack-issue-links and passes --issues to the
  receipt; the completion message gains an `issues:` line.
- Regenerated ship/SKILL.md and sections; refreshed the four ship goldens.
- Parity: union 309,431 / 187,706 = 1.6485 (> 1.6450 by 655 bytes); maxSizeRatio
  raised to the measured 1.6485 with its justification, as previous waves did.

Files: ship/SKILL.md.tmpl, ship/SKILL.md, ship/sections/pr-body.md.tmpl,
ship/sections/pr-body.md, ship/sections/handoff.md.tmpl, ship/sections/handoff.md,
test/fixtures/golden/{claude,codex,factory,agy}-ship-SKILL.md,
test/helpers/carve-guards.ts (ship maxSizeRatio).

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

* fix(install): one Bun resolution contract, versioned runtime store, verdict per (checkout, project), drift and wrapper rows (A1-A3, A5, A6, A8, A-c, A-d, A-f, A-h, A-i)

- bin/gstack-bun-version.sh: GSTACK_BUN_DEFAULT=1.4.3 beside the 1.4.2 floor,
  gstack_bun_install_version (GSTACK_BUN_INSTALL_VERSION override),
  gstack_bun_store, portable gstack_realpath, and gstack_bun_resolve (pin and
  default in the store, $BUN_INSTALL/bin, ~/.bun/bin, PATH; first candidate
  satisfying every pin; shim-marked files never returned). gstack_bun_launch_dir
  is replaced by it.
- lib/pregate/runtime.ts: resolveBun, the TypeScript twin; launchRuntime runs
  the Bun the install verdict recorded under the admission condition and
  explains the skip otherwise.
- bin/gstack-capy-install: install and --check resolve through the contract;
  verified archives publish atomically to <state root>/runtimes/bun/<v>/bun
  under ~/.gstack-runtime.lock and ~/.bun/bin is never written; the "no Bun
  satisfies" line names the candidates judged; --check reports
  BUN_RESOLUTION_DRIFT with one converging fix.
- bin/gstack-install-verdict.sh, lib/install-verdict.ts: schema 2 verdicts
  keyed by (checkout, project) with pin, bun_path, bun_version, bun_rule;
  schema 1 path still read; admission binds test-running workflows to the
  project and refuses a running Bun the verdict did not record.
- bin/gstack-doctor-check.sh, lib/doctor-components.ts: wrapper core row
  (CAPY_WRAPPER_STALE by active provenance, WARN for stale Drive copies),
  runtime drift row, shim note, install-version note.
- lib/result-codes.ts: BUN_RESOLUTION_DRIFT, BUN_INSTALL_VERSION_INVALID;
  CAPY_WRAPPER_STALE fix names the workaround and the owner action.

Files: bin/gstack-bun-version.sh, bin/gstack-capy-install,
bin/gstack-doctor-check.sh, bin/gstack-doctor-components.sh (generated),
bin/gstack-install-verdict.sh, lib/doctor-components.ts,
lib/install-verdict.ts, lib/pregate/runtime.ts, lib/result-codes.ts,
test/bun-resolve.test.ts, test/gstack-capy-install.test.ts,
test/gstack-doctor.test.ts, test/install-verdict.test.ts,
test/pregate-guards.test.ts

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

* fix(issue-links): --author for the Related lookup when gh runs as an app (Lane D: D2)

On a Capy machine the gh viewer is `capy-ai[bot]`, not the owner (checked live
against garrytan/gbrain #6252), so "other authors" needs the PR author named:
`--author <login>` overrides the viewer; the pr-body Issue links step says to
pass it when gh runs as an app.

Files: bin/gstack-issue-links, docs/pregate.md, ship/sections/pr-body.md.tmpl,
ship/sections/pr-body.md, test/ship-issue-intent.test.ts,
test/helpers/carve-guards.ts (ship maxSizeRatio re-measured: 309,483 / 187,706 = 1.6488).

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

* docs(troubleshooting): BUN_RESOLUTION_DRIFT and BUN_INSTALL_VERSION_INVALID anchors; CAPY_WRAPPER_STALE covers the wrapper row (A5, A8, A-f, G1)

Files: docs/troubleshooting.md, bin/gstack-capy-install, bin/gstack-doctor-check.sh, test/gstack-capy-install.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(install): marked per-directory bun and bunx shims for fresh Capy shells (A4, A-e)

gstack-capy-install (install mode, Capy machines only, GSTACK_BUN_SHIM=0 opts
out) writes ~/.local/bin/bun and bunx under ~/.gstack-runtime.lock. The shim
bakes the store, verdict directory, checkout and install default, walks up to
.git without spawning git, runs the Bun the (checkout, project) verdict
recorded, else a store Bun matching the .bun-version/package.json subset
(stderr names the --check to run when none matches), else the install
default, else the next unmarked bun on PATH. A foreign file or dangling link
is never touched. GSTACK_BUN_SHIM_EXPLAIN=1 prints project, record, choice and
rule; the doctor's runtime row shows the shim and its target.

Files: bin/gstack-capy-install, test/gstack-capy-install.test.ts, test/gstack-doctor.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(pregate): test stage never hangs silently; small machines record the remote gate (Lane C, GBRA-95)

C1 diagnosis (gbrain 92f0ea87, a core edit selecting 3,683 local files, 358
serial; Bun 1.4.2 and 1.4.3 identical): --timeout was applied per runner
spawn (unit group plus one per serial file), so the check was unbounded, and
spawnSync buffered all output until it returned: 360 s with 0 bytes on stdout
and stderr. A backgrounded child holding stdout made spawnSync wait the full
timeout and then report exit 0 (a silent wait, then green). Not the Bun 1.4.2
spawnSync hang.

C2/C-b: test-tier commands (lanes runner, guards, the --remote gate) run via
lib/pregate/supervise.ts: own process group (setsid), stdout/stderr appended
to a log file (no pipes), async supervision; CheckDef.run may return a
promise and runPregate/withFingerprint await it (a rejection is incomplete).
Leftover group members are killed after exit; SIGINT/SIGTERM to pregate
SIGKILLs running groups, writes no pregate.json, exits 130/143. Windows:
taskkill /T /F (reparented or detached descendants can survive; documented).

C3/C-a: --timeout bounds the whole check; groups past the deadline are not
started. --stall-timeout <s> (default stall_timeout_s, else 300; 0 disables)
kills a group whose log stops growing and records PREGATE_STALLED naming the
last file started. Live stderr: PREGATE_TEST per file (bun test adapter; any
runner for one-file runs), PREGATE_HEARTBEAT after 60 s without progress,
PREGATE: tests <verdict>. --json stdout unchanged.

C4/C-c: under 8 cores, a base-branch remote_gate or a non-local test_backend
records the local tier as a requires-remote obligation with its command
(pregate.json/remote_gate or test_backend/<backend>). --remote runs the
declared remote_gate now (needs --allow-repo-commands, never --policy-from
head) under --spend-cap/GSTACK_SPEND_CAP_USD; it clears only on
PREGATE_REMOTE: tree=<tested tree> tests>0 failed=0 with exit 0, and reports
spent/reserved/unknown.

G1: new code PREGATE_STALLED has its result-codes row, troubleshooting anchor
and the installer-form error line (CODE; url#anchor).

Files (G2): lib/pregate/supervise.ts (new), lib/pregate/types.ts,
lib/pregate/context.ts, lib/pregate/index.ts, lib/pregate/checks/lanes.ts,
lib/pregate/checks/guards.ts, lib/pregate/config.ts, lib/headless-artifacts.ts,
lib/result-codes.ts, bin/gstack-pregate, bin/gstack-doctor-components.sh
(generated), docs/pregate.md, docs/troubleshooting.md,
test/pregate-supervise.test.ts (new), test/pregate-remote.test.ts (new).

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

* docs(ship): Step 5 pre-gate text for stalls and the small-machine remote gate (Lane C-c)

The pre-gate paragraph now says `incomplete` blocks (a stall is incomplete)
and that `requires-remote` covers the remote gate under 8 cores, cleared by
`gstack-pregate clear` or `--remote --spend-cap <usd>` only under a cap the
user gave. Kept within the ship parity size cap (union 308,771 of 308,776
bytes; base 308,767), so the `--explain` hint moved to docs/pregate.md. The
handoff's escalation line already names any `requires-remote` lane; unchanged.

Files (G2): ship/sections/tests.md.tmpl, ship/sections/tests.md (generated).

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

* fix(install): one ok-trailer grammar with requested, pins-skipped and latest notes; bootstrapper revision 2 exports origin's VERSION (A9, A-a)

Notes ride only on the ok trailer in fixed order; the failure head is
unchanged, pinned by a test that runs the Capy skill's dispatch examples
against real doctor output. The latest note comes only from
GSTACK_CAPY_REMOTE_VERSION, which the bootstrapper sets from a fetch in this
run and unsets otherwise (a stale FETCH_HEAD never reaches the installer).

Files: bin/gstack-capy-install, bin/gstack-capy-session,
capy/skill/gstack/scripts/install.sh, capy/skill/gstack/SKILL.md,
test/gstack-capy-install.test.ts, test/capy-skill-wrapper.test.ts,
test/gstack-doctor.test.ts

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

* fix(upgrade): every success path certifies the caller's project; a pins failure keeps the move, ends on gstack: fail pins, exit 1, no pending marker (A7, A-b)

gstack-upgrade captures the caller's directory and git toplevel before any
cd. Already-current, fast-forwarded and resumed-pending paths run
bin/gstack-capy-install --project <it> on Capy (installing a verified pinned
Bun) or gstack-doctor --check --require runtime,pins elsewhere. The
reinstall's own trailer is held back when a project certification follows,
so no ok precedes a pins failure; a RUNTIME_PIN_CONFLICT stop becomes
gstack: fail pins — <conflict>. --check and --ff-only-no-reinstall certify
nothing.

Files: bin/gstack-upgrade, test/gstack-upgrade-bin.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(install): fresh-install log lines name the clone and every host installed (A10)

gstack-capy-install prints `cloned <repo> at <sha> (<version>)` for a checkout
it cloned, or one the bootstrapper (revision 2, GSTACK_CAPY_CLONED) cloned in
this run, instead of `reusing`. setup's final line is
`gstack ready (<hosts>)` from the same rows as its summary table, including
the --refresh-registered upgrade summary.

Files: bin/gstack-capy-install, bin/gstack-capy-session,
capy/skill/gstack/scripts/install.sh, setup, test/gstack-capy-install.test.ts,
test/host-conformance.test.ts

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

* fix(pregate): --remote runs the gate only when local tests are selected (Lane C4)

With only platform lanes selected, --remote no longer launches the remote
gate with an empty file list; the existing no-local-lane path applies.

Files (G2): lib/pregate/checks/lanes.ts.

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

* fix(autoplan): one-word rec, verified pairing, sealed attempt results, deferred reopen (Lane B: B1 B2 B4 B5)

B1/B-b/B-c  `rec` (aliases: all, approve all, accept all, take all recommendations)
    takes every recommendation, plan changes included; `as-is` keeps every
    direction. The render offers `rec` first and prints what it resolves to
    with gate_rev and rec_hash. `gstack-autoplan recommend` changes one
    recommendation before relay (provenance recorded, p1 and decisions follow)
    and bumps rec_rev, so a `rec` without the new --rec-hash is refused
    (GATE_REC_STALE). An agent answer needs the owner's words verbatim
    (--owner-reply <text>|-, --owner-reply-file; OWNER_REPLY_REQUIRED); a
    parseable owner reply that resolves differently from the relay is refused
    (GATE_OWNER_REPLY_MISMATCH). Supersedes field-wave R1 per the owner's
    gate_rev 1 decision.
B2/B-a  reconcile associates by title first; a file:line-only match is
    `unverified` (associated, never confirmed). `b` is recommended only for a
    confirmed pair with the same `changes` and one shared proposal (override)
    or the same normalized remedy; the basis says which. Negative control and
    positives copied from run-20261014225954 (test/fixtures/autoplan-pairing).
B4/B-e  each attempt writes <phase>-<voice>.<attempt>.md; submit refuses another
    attempt's file (ATTEMPT_MISMATCH) and binds a sealed 0444
    <phase>-<voice>.<attempt>.bound.md copy, so a late write changes nothing
    bound; reopen keeps sealed copies in place.
B5/B-d  answer records accepted changes as amendments (status amend_pending) and
    reopens nothing; revise reopens what changed. `revise --settle <id|d4>`
    discharges a packaging/ordering change on an unchanged plan
    (AMENDMENT_NOT_SETTLEABLE otherwise). APPLY/NEXT/status lines say which.

DRIVER.md and the reply table move to the rec grammar.

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

* feat(autoplan): durable checkpoint and resume --from; Capy checklist (Lane B: B12)

With GSTACK_AUTOPLAN_CHECKPOINT_DIR set, every command that leaves a run at
rest (gate_pending, amend_pending, complete, incomplete) publishes
<dir>/<run>/: checkpoint.json listing every file's sha256, the run under run/,
the owner's plan file under plan-input/ when it lives outside the run, and
restore.md as the human index. Publication swaps in a fully written sibling.
Checkpointing at every rest state, not only gate_pending, keeps accepted
decisions from rolling back on restore.

`gstack-autoplan resume --out <new dir> --from <checkpoint>` checks
containment (no absolute or .. paths, regular files only) and every hash
before publishing the run with one rename. It restores a missing plan file
into the run, journals `restored` with both hosts, and takes the lock.
CHECKPOINT_INVALID and CHECKPOINT_TARGET_EXISTS each get a result-codes row
and a troubleshooting anchor. Journal and revise staging paths are now
run-relative, so a moved run recovers.

DRIVER.md and the Capy skill's autoplan section get a short Capy checklist:
always drive gstack-autoplan, never present a hand review as an autoplan run,
relay every gate page with rec as the default, pass --owner-reply, and point
the checkpoint at the private project-user Drive folder.

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

* A-g: run the quick free tier on GSTACK_BUN_DEFAULT in CI; drift test pins it

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

* docs(autoplan): unattended contract and reply-unparsed fix move to rec, sealed results, settle and checkpoints (Lane B)

docs/unattended.md: rec/as-is/rec except grammar, --owner-reply and recommend/--rec-hash, unverified pairing and the shared-proposal rule for b, amend_pending and revise --settle, attempt-owned result paths with the sealed .bound.md copy, render --all, and the checkpoint/resume --from paragraph. GATE_REPLY_UNPARSED's fix names rec/as-is (DRIVER.md regenerated).

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

* docs/capy-install: resolver, versioned store, bun shim, trailer notes; wrapper minimum 2

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

* fix(pregate): --timeout bounds each runner process again, not the whole lanes check

Lane C made --timeout a deadline for the whole lanes check, so a repo whose
unit group and serial files each fit in 600 s but exceed it together ended
incomplete and blocked /ship. --timeout is per runner process again (unit
group, each serial file); the stall timeout still catches hung or silent
processes. A timed-out process is incomplete naming its last file, and the
next serial file still runs. Regression: three 1.5 s serial files under
--timeout 3 pass; one 5 s file is incomplete.

Files (G2): lib/pregate/checks/lanes.ts, bin/gstack-pregate (help),
docs/pregate.md, test/pregate-supervise.test.ts.

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

* fix(pins): CI exact pins for this platform are alternatives; the installer prefers a non-CI exact pin, else the install default among CI alternatives, else the highest verified (integration item 1)

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

* fix(pregate): clear validates the remote gate receipt; test_backend run URLs are attested

`gstack-pregate clear` accepted any --run-url for a small machine's
obligation. A pregate.json/remote_gate obligation now clears only with
--receipt <file> holding the gate's PREGATE_REMOTE: line, validated as
--remote validates it (tree equals the tested tree, tests > 0, failed = 0;
shared parseRemoteReport/remoteReportProblem); a run URL is usage (exit 2)
and a bad receipt is PREGATE_RECEIPT_INVALID (exit 1, obligation unchanged).
A test_backend/<backend> obligation has no machine-checkable receipt: its
--run-url is recorded as attested:<url> and the output says attested, not
validated. Platform lanes are unchanged. Hints name the right clear form.

New code PREGATE_RECEIPT_INVALID: result-codes row, troubleshooting anchor,
installer-form error line, regenerated doctor components.

Files (G2): lib/pregate/index.ts, lib/pregate/checks/lanes.ts,
bin/gstack-pregate, lib/result-codes.ts, bin/gstack-doctor-components.sh
(generated), docs/pregate.md, docs/troubleshooting.md,
test/pregate-remote.test.ts.

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

* fix(issue-links): derive this PR's author when gh runs as an app, so handoff and pr-body exclude the owner's own PRs (Lane D fix 1)

The handoff ran gstack-issue-links without --author, and the pr-body step only
asked the agent to pass one, so under Capy's app identity (viewer
`capy-ai[bot]`) the owner's own open PRs were listed as `Related:`. The bin now
derives the author itself: --author, else the gh viewer, else (for a `…[bot]`
viewer) the HEAD commit's GitHub user, else the owner of a user-owned
repository; with none it keeps every PR and reports ISSUE_LOOKUP_FAILED.
`--json` gains `author: {login, source}`; the stderr preview names it. Both ship
steps work unchanged, so the pr-body `--author` clause is dropped (union
309,487 -> 309,435 bytes, under the unchanged 1.6488 cap; skeleton 80,998).

Files: bin/gstack-issue-links, lib/result-codes.ts (ISSUE_LOOKUP_FAILED row),
docs/troubleshooting.md (#issue-lookup-failed), docs/pregate.md,
ship/sections/pr-body.md.tmpl, ship/sections/pr-body.md,
test/helpers/pregate-fixture.ts (gh stub: PR-author query),
test/ship-issue-intent.test.ts.

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

* fix(wrapper): GSTACK_CAPY_MIN_WRAPPER back to 1; an active wrapper below the minimum is a WARN; gstack-upgrade never leaves a pending marker for the wrapper row and maps every installer stop to a trailer (integration items 2, 9)

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

* fix(shim): validate and contain store pins, read the 'none' record outside projects and in the checkout, fall back in the resolver's order, stay quiet without a pin; quote baked paths; publish bunx and replace store entries without a window (integration items 3, 4, 6, 9)

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

* fix(install): --check fails BUN_RESOLUTION_DRIFT only when the recorded Bun is gone, changed or no longer satisfies the pins; a newer candidate is a note and the recorded Bun stays the launch Bun (integration item 5)

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

* fix(install): one final 'gstack ready (claude, codex)' after both host setups (integration item 7)

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

* fix(pregate): lanes and guards validate the install verdict like admission and return incomplete (INSTALL_UNVERIFIED, BUN_RESOLUTION_DRIFT) before launching; the checkout binds the 'none' verdict (integration item 8)

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

* docs: CI pin alternatives, shim order and containment, drift note, wrapper WARN, final ready line

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

* fix(autoplan): settle after or with a revise, title pairs need the same remedy, legacy gates refuse rec (integration items 1, 7, 8)

1. A gate with a scope change (uc1) and a packaging change (uc2) answered
   `rec` deadlocked: after `revise --plan v2 --incorporates uc1` reopened the
   phases, `revise --settle uc2` compared against the first phase snapshot
   (now undefined) and was refused forever. Settle evidence is now the plan
   hash recorded when the owner accepted the change (Amendment.plan_sha256)
   or a later plan_revised hash, plus "the plan file has no unrevised edits".
   `--settle` may ride the same revise as `--plan/--incorporates` and binds
   to the revision's hash. Settling the last amendment after a reopen leaves
   the run `running` (the reopened phases rerun) instead of approving it.
   NEXT and PLAN_AMEND_PENDING list the settle step before the edit step.
7. A title association alone no longer confirms: the same remedy confirms,
   opposite remedies (one keeps, drops or negates what the other changes)
   disagree, different or missing remedies stay `unverified`. The pairing
   fixture gains same-title negative controls (opposite and different
   remedies) and a same-remedy positive, for ordinary findings and user
   challenges.
8. A gate.json without rec_rev (written by 1.91.101.0, whose render said
   `all` leaves b items pending) refuses rec/all/approve all/`all except`
   with GATE_REC_STALE until a re-render binds --rec-hash; explicit tokens
   still answer. New gates carry rec_rev: 0.

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

* fix(autoplan): checkpoints never touch the live run; generations, newest-valid restore, validated staging (integration items 2-6)

2. checkpointRun canonicalizes the collection and the run directory (symlinks
   resolved through the deepest existing ancestor) and refuses, before
   touching anything, when they are the same or one contains the other
   (CHECKPOINT_OVERLAP); restore applies the same check to source, target and
   staging. Before, <root>/<run> equal to the live run replaced it.
3. Publication writes an immutable generation (generations/g<revision>-…),
   renames it into place complete, then switches current.json with one
   rename; the previous generation is kept, so an interruption at the publish
   boundary (fault hook checkpoint-before-pointer) leaves the last checkpoint
   current. A generation never replaces a newer one (higher run revision):
   "CHECKPOINT: skipped … newer". Publication and restore hold the
   collection's .lock; restore also holds <out>.lock.
4. Restore checks every listed file's real path stays inside the generation
   (an ancestor symlink no longer copies outside files), parses run.json and
   runs validateRun on the staged run before publishing; a refusal leaves
   --out absent. readState reroots plan_input and plan_source for moved runs,
   and a restore with the owner's plan file gone points both at the restored
   copy, so a second restore cycle and a later revise work.
5. A checkpoint failure after a committed command prints
   "CHECKPOINT: failed for <run>: … ; the command's result stands" on stderr
   and keeps the command's exit code.
6. `gstack-autoplan checkpoints --from <dir>` lists generations newest first
   (run, gate_rev, status, revision, time, hash ok|bad, current).
   `resume --from <collection or root>` restores the newest generation that
   verifies, printing each corrupt newer one it skipped; a root holding
   several runs must name one.

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

* chore: budget the Capy skill's autoplan checklist and trim its install notes

Files: capy/skill/gstack/SKILL.md, test/fixtures/context-budget.json (capy/skill/gstack ceiling only: 2364 tok x 1.10)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: release 1.91.102.0 (tentative stamp) — GBRA-40 field list

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

* test: supply _setup_ready_line to setup-tail harnesses, register the pairing fixture, make the admission test runner-Bun independent; announce shim writes

Files: test/setup-hook-syntax-gate.test.ts, test/redact-prepush-hook.test.ts, test/setup-playwright-best-effort.test.ts, test/helpers/free-fixtures-data.ts, test/pregate-guards.test.ts, bin/gstack-capy-install
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci: run quick-bun-default on the ubicloud-standard-8 class the free suite uses

The ubuntu-24.04 runner timed out two Swift typecheck tests at 30 s (render @Observable marker, ios-qa-regen); both pass on Bun 1.4.3 on Ubicloud.

Files: .github/workflows/free-tests.yml
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(pregate): read bun test's GitHub Actions group headers; exclude pregate-remote from the Windows subset; give quick-bun-default the free suite's flake policy

Files: lib/pregate/supervise.ts, test/pregate-supervise.test.ts, scripts/lib/windows-curation.ts, .github/workflows/free-tests.yml
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: run the Capy wrapper and shim doctor-row tests on POSIX only (Git Bash prints MSYS paths)

Files: test/gstack-doctor.test.ts
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: restamp 1.91.107.0 (1.91.106.0 is claimed by #3209)

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

* fix(pregate): spawn the supervised shell through lib/script-invocation (#3197 tripwire)

The supervisor now resolves bash via bashCommandInvocation, so Windows uses
Git Bash instead of a PATH bash, and an unresolvable bash reports spawn-failed.
guards.ts and lanes.ts no longer spawn bash (they run through supervise.ts),
so their KNOWN_SITES entries are removed.

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

* test(qa-functional): disable auto gc and maintenance in the fixture repo

On eval-slices (9), qa-functional-webhook-fix failed mid-run with 100+
'directory watch lost: .git/objects/..' entries and forbidden writes to
.git/info/refs and .git/objects/pack. That is git's detached auto
maintenance repacking the fixture under the write observer. This applies the
same gc.auto=0 / maintenance.auto=false config qa-callers-fixture uses.

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

* test(autoplan-guard-pty): say Phase 2 starts without waiting for user input

On eval-slices (1), haiku published Phase 1 and then ended its turn with
'Waiting for next invocation to continue with step 8'. Step 5 tells it to end
its turn while the reviewer runs, so step 8's 'In a later message' read as a
later user turn. Phase 2 never started, the guard logged 2 of the 4 expected
allows, and the run hit its deadline. Step 8 now says to continue in the next
assistant message, still separate from the step 7 publication message.

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-10-16 23:09:23 +00:00

485 lines
28 KiB
TypeScript

/**
* Windows-safe curation for the free suite (`--windows-only`): POSIX-only
* source patterns, explicit known-incompatible files, and force-includes for
* pattern hits that do not apply. Pinned by test/test-free-shards.test.ts and
* test/windows-native-workflows.test.ts.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dir, '../..');
// POSIX-only patterns that indicate a test will fail on windows-latest no
// matter how the runner shards. Codex's v1.18.0.0 review flagged the first
// three as concrete examples in the existing free suite (test/ship-version-sync.test.ts:72,
// test/helpers/providers/claude.ts:22, package.json:12). We scan the test's
// own content here so the filter stays automatic as new tests land. The
// "Windows-incompatible APIs" patterns at the bottom were added after the
// first windows-free-tests CI run surfaced concrete failure modes.
const WINDOWS_FRAGILE_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
// Hardcoded POSIX shells / commands.
{ pattern: /['"`]\/bin\/(?:ba)?sh/, reason: 'hardcoded /bin/sh or /bin/bash' },
{ pattern: /spawnSync\(['"]sh['"],|spawn\(['"]sh['"],|exec\(['"]sh /, reason: 'spawn("sh", ...)' },
{ pattern: /['"]bash -c['"]|['"]sh -c['"]/, reason: 'bash -c / sh -c' },
{ pattern: /['"`]\/tmp\//, reason: 'raw /tmp/ path (use os.tmpdir())' },
{ pattern: /['"]chmod\b/, reason: 'chmod shell command' },
{ pattern: /['"]xargs\b/, reason: 'xargs pipeline' },
{ pattern: /\bwhich claude\b/, reason: 'which claude (use Bun.which)' },
// Windows-incompatible APIs.
{ pattern: /\.mode\s*&\s*0o[0-7]+/, reason: 'POSIX file mode bitmask (mode & 0o600 etc — Windows fakes mode bits)' },
{ pattern: /\.endsWith\(['"]\//, reason: 'hardcoded forward-slash path assertion (Windows uses \\\\)' },
{ pattern: /['"]\.\/[a-zA-Z][^"']*['"]\)\s*\.\s*toBe\(true\)/, reason: 'forward-slash path comparison' },
// Tests that spawn a bash shebang script in bin/ via spawnSync. Git Bash on
// Windows can run `bash /path/to/script` but spawnSync(scriptPath, ...)
// tries to execute the file directly via CreateProcess, which fails on the
// shebang. The pattern matches `, 'bin'` as a path-join argument (closing
// OR followed by another segment), which catches:
// - path.join(ROOT, 'bin', 'script-name') — typical
// - join(import.meta.dir, '..', 'bin', 'name') — destructured (diff-scope)
// - path.join(ROOT, 'bin') — bare BIN constant (brain-sync)
{ pattern: /,\s*['"]bin['"]\s*[,)]|['"]\.?\/?bin\/[a-z][\w-]+['"]/, reason: 'spawns bin/ shebang script (Windows CreateProcess does not parse shebangs)' },
// Tests that spawn the browse server as a subprocess via `bun run server.ts`.
// The Bun → server.ts → Playwright path is the same one that doesn't work
// on Windows (PR #1238 windows-pty-bun-pty-fix). Tests typically set
// BROWSE_HEADLESS_SKIP=1 to skip the browser launch but still need a working
// server, which they don't get on Windows.
{ pattern: /BROWSE_HEADLESS_SKIP|spawn\(\[['"]bun['"],\s*['"]run['"]/, reason: 'spawns the browse server subprocess (Bun-driven path is Windows-broken)' },
];
// Explicit known-Windows-incompatible test files that don't fit a regex
// pattern. Listed here with the precise reason. Prefer adding a pattern above
// when possible; this list is for environment-/runtime-specific tests where
// the failure mode is structural rather than detectable via source-file scan.
export const KNOWN_WINDOWS_INCOMPATIBLE: Array<{ file: string; reason: string }> = [
// C-pregate: these drive bin/gstack-regen, bin/gstack-pregate, bin/gstack-issue-links and
// bin/gstack-ship-receipt through test/helpers/pregate-fixture.ts runBin (spawnSync on the
// shebang file, which CreateProcess cannot exec) and run `node gen.js` / `node --test` fixture
// commands through the pre-gate's POSIX `bash -o pipefail` runner. The pure parsers and the
// import graph are covered by test/pregate-selection.test.ts, which the bin/ pattern excludes too.
{ file: 'test/gstack-regen.test.ts', reason: 'spawns bin/gstack-regen through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and POSIX fixture regen commands' },
{ file: 'test/pregate-guards.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and runs node guard and lane fixture commands through the POSIX runner' },
{ file: 'test/pregate-remote.test.ts', reason: 'spawns bin/gstack-pregate through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and runs POSIX remote-gate fixture commands through the lane runner' },
{ file: 'test/pregate-regate.test.ts', reason: 'spawns bin/gstack-pregate regate through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess)' },
{ file: 'test/stamp-templates.test.ts', reason: 'spawns bin/gstack-tree-receipt through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess)' },
{ file: 'test/pregate-invalidation.test.ts', reason: 'spawns bin/gstack-pregate and bin/gstack-ship-receipt through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) and the pipefail lane runner' },
{ file: 'test/ship-issue-intent.test.ts', reason: 'spawns bin/gstack-issue-links and bin/gstack-ship-receipt through test/helpers/pregate-fixture.ts runBin (shebang via CreateProcess) with a `gh` shell shim on PATH' },
// PR E2: lane ownership and evidence verification run bin/gstack-lane-check and bin/gstack-evidence
// through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess); the lib paths are
// exercised in the same file, so there is no Windows-safe split to keep.
{ file: 'test/lane-coordination.test.ts', reason: 'spawns bin/gstack-lane-check and bin/gstack-evidence through test/helpers/restamp-fixture.ts runBin (shebang via CreateProcess) and reruns POSIX lane commands' },
{
file: 'test/ship-measure-seeded-fixture.test.ts',
reason: 'drives a POSIX shell stub project (`./evals.sh` with a shebang) that CreateProcess cannot exec; the measure runner itself is covered by ship-measure.test.ts with in-process fake runners',
},
{
file: 'test/qa-evidence-producer.test.ts',
reason: 'executes the registered Linux native actor and its inotify observer; portable capture and Windows job behavior are covered by qa-evidence.test.ts',
},
{
file: 'test/qa-functional-fixture.test.ts',
reason: 'executes graceful POSIX signal cancellation; Bun on Windows uses TerminateProcess and cannot run the fixture SIGTERM cleanup handler',
},
{
file: 'test/qa-functional-observer-atomic.test.ts',
reason: 'exercises real Linux inotify inode and directory watches through libc.so.6; Windows has no equivalent kernel interface',
},
{
file: 'test/docsync-report-interface.test.ts',
reason: 'executes registered native documentation callbacks with their real Linux inotify write observer before the model boundary',
},
{
file: 'test/setup-gbrain-fixture.test.ts',
reason: 'the fixture invokes real POSIX detector/verifier helpers through executable shebang wrappers',
},
{
file: 'test/setup-gbrain-windows-mcp.test.ts',
reason: 'drives the Windows MCP registration branch on POSIX by putting uname/claude shebang stubs on a colon-separated PATH and comparing POSIX path spellings; the console behavior it guards needs a manual Windows check',
},
{
file: 'test/hermetic-skills-seeding.test.ts',
reason: 'seeds the POSIX PTY skill runtime, whose embedded shell paths require a POSIX temporary root',
},
{
file: 'test/hermetic-wiring.test.ts',
reason: 'its runtime contract check seeds the POSIX PTY skill runtime; the curated Windows lane does not run that harness',
},
{
file: 'test/pty-workspace-trust.test.ts',
reason: 'launches the POSIX PTY harness with a fake executable and bound skill runtime',
},
{
file: 'test/host-config.test.ts',
reason: 'asserts "claude" binary on PATH (only true when running inside Claude Code, not on bare CI runner)',
},
{
file: 'browse/test/findport.test.ts',
reason: 'asserts Bun.serve.stop() is fire-and-forget — Bun behavior differs on Windows for this polyfill',
},
// First full run of the expanded lane (v1.66, 13 → ~258 files) surfaced
// seven POSIX-bound files the content patterns cannot see (their
// POSIX-ness is what they TEST, or arrives via a variable). Receipts:
// PR #2593 windows-free-tests run 31918591602.
{
file: 'test/codex-under-codex-detection.test.ts',
reason: 'drives the rendered preflight bash under a hardcoded POSIX PATH (/usr/bin:/bin) — bash is unreachable through that PATH on Windows, so every case sees empty output (v1.67 windows lane run 95234224148)',
},
{
file: 'test/regression-pr1169-build-app-sed.test.ts',
reason: 'tests sed escape sequences in build-app.sh — sed/bash are the subject under test',
},
{
file: 'test/setup-conductor-worktree.test.ts',
reason: 'tests ln -snf symlink semantics in the setup script — POSIX ln is the subject under test',
},
{
file: 'test/artifacts-init-migration.test.ts',
reason: 'runs a bash migration script + jq against a scaffolded git state — POSIX toolchain paths break under cmd spawn',
},
{
file: 'test/gstack-decision-semantic.test.ts',
reason: 'installs a fake gbrain SHEBANG SHIM on PATH; Windows spawn cannot exec shebang scripts',
},
{
file: 'test/question-log-hook.test.ts',
reason: 'spawns the PostToolUse hook script (bash shebang) directly; Windows spawn cannot exec it',
},
{
file: 'browse/test/browser-skills-e2e.test.ts',
reason: 'asserts forward-slash tier paths (<repo>/browser-skills/) that resolve with backslashes on Windows',
},
{
file: 'design/test/variants-retry-after.test.ts',
reason: 'wall-clock retry-timing assertions — flaky on the slow windows-latest runner even with widened bounds',
},
// Round-2 census (PR #2593 run 31919227507) after the first seven:
{
file: 'test/skill-census.test.ts',
reason: 'census walk throws at module load on Windows (skill-census.ts:63) — the skills-tree symlink layout needs Developer Mode that CI runners lack',
},
{
file: 'browse/test/browser-manager-unit.test.ts',
reason: 'wedges the shard to its wall deadline on windows-latest (in-flight at kill); needs a Windows repro to diagnose — macOS + Linux lanes cover the file',
},
// Round-3 census (PR #2593 run 31919871680): the round-2 wedge had been
// TRUNCATING its shard, so these seven only surfaced once shard 2 completed.
// All the same POSIX-environment classes: PID/cmdline identity probing,
// bash scripts as the subject under test, env-scrubbed child spawns.
{
file: 'browse/test/server-embedder-terminal-port.test.ts',
reason: 'identity-based terminal-agent kill probes PID/cmdline with POSIX semantics; teardown asserts fail on windows-latest',
},
{
file: 'design/test/daemon-discovery.test.ts',
reason: 'verifyIdentity matches a spawned daemon via /proc-style cmdline probing — POSIX identity semantics',
},
{
file: 'test/context-save-hardening.test.ts',
reason: 'bash context-save/migration scripts (HOME-unset semantics, random-suffix path) are the subject under test',
},
{
file: 'test/eval-list-cli.test.ts',
reason: 'spawns the eval:list CLI via bun with a constructed env — bun resolution fails under Windows spawn',
},
{
file: 'test/memory-cache-injection.test.ts',
reason: 'exercises hook/deny-enforcement shell scripts — POSIX toolchain is the subject under test',
},
{
file: 'test/migrations-v1.65.0.0.test.ts',
reason: 'bash migration script (bunx re-fetch, .done markers) is the subject under test',
},
{
file: 'test/question-preference-hook.test.ts',
reason: 'spawns the PreToolUse preference hook (shebang script) directly; Windows spawn cannot exec it',
},
// Round-4 census (PR #2593 run 31920052810): unhandled errors with no
// (fail) lines — attributed statically (the lane had no log artifact yet).
{
file: 'browse/test/browser-skill-commands.test.ts',
reason: 'spawnSkill spawns bun with a constructed env — bun resolution fails under Windows spawn (unhandled, no (fail) line)',
},
{
file: 'browse/test/security-audit-r2.test.ts',
reason: 'symlink-attack fixtures (evil-link) need Developer Mode CI runners lack; expect(toThrow) fires unhandled on Windows',
},
// CSO comprehensive execution is qualified only for Linux containers behind
// the POSIX watchdog and Unix-domain registry broker. Keep the portable
// static/parser contracts in the Windows lane while leaving these exact
// containment suites to the Linux and macOS gates.
{
file: 'test/cso-preparation-adversarial.test.ts',
reason: 'exercises POSIX prepared-tree and archive-cache containment for qualified Linux Docker execution, which Windows does not admit',
},
{
file: 'test/cso-preparation-container.test.ts',
reason: 'asserts POSIX permission and symlink semantics for inert exports consumed by qualified Linux Docker execution',
},
{
file: 'test/cso-preparation-executor.test.ts',
reason: 'executes the Linux Docker acquisition path and its Unix-domain registry broker; comprehensive execution is unavailable on Windows',
},
{
file: 'test/cso-verification-cleanup.test.ts',
reason: 'spawns the POSIX detached watchdog used by contained repair verification, which Windows intentionally leaves unavailable',
},
{
file: 'test/cso-witness.test.ts',
reason: 'tests the contained repair witness with POSIX private-directory and compiled-helper assumptions; comprehensive execution is unavailable on Windows',
},
{
file: 'test/shard-engine-equivalence.test.ts',
reason: 'its classification golden was recorded from the POSIX runners (process-group wall kill); the win32 engine path is pinned by the mocked-platform case in shard-engine.test.ts',
},
{
file: 'test/claude-overlay-setup-default.test.ts',
reason: 'every case runs the real ./setup under describe.skipIf(win32); its bin/ spawns live in test/helpers/claude-overlay-fixture.ts, so the source scan misses them',
},
{
file: 'test/claude-overlay-setup-installs.test.ts',
reason: 'every case runs the real ./setup under describe.skipIf(win32); its bin/ spawns live in test/helpers/claude-overlay-fixture.ts, so the source scan misses them',
},
{
file: 'test/cso-scanner-cli.test.ts',
reason: 'drives the prebuilt POSIX CSO launcher with /usr/bin/git and a POSIX-only PATH; native Windows launcher behavior is covered by the dedicated cso-windows-launcher gate',
},
// C-stamp (ship policy, restamp, tree receipt, evidence bundle, ship receipt):
// every case spawns bin/ shebang scripts and a #!/bin/sh `gh` shim through
// test/helpers/restamp-fixture.ts, so the source scan misses them.
{
file: 'test/gstack-ship-policy.test.ts',
reason: 'spawns bin/gstack-ship-policy (shebang) through test/helpers/restamp-fixture.ts; the policy schema and loader are pure lib code covered on the POSIX gates',
},
{
file: 'test/restamp-command-boundary.test.ts',
reason: 'spawns bin/gstack-restamp (shebang) and a POSIX release tool through test/helpers/restamp-fixture.ts',
},
{
file: 'test/tree-receipt-stamp-sensitive.test.ts',
reason: 'spawns bin/gstack-tree-receipt, bin/gstack-restamp and bin/gstack-evidence (shebangs) plus a #!/bin/sh gh shim through test/helpers/restamp-fixture.ts',
},
{
file: 'test/evidence-bundle-portability.test.ts',
reason: 'spawns bin/gstack-evidence and bin/gstack-tree-receipt (shebangs) through test/helpers/restamp-fixture.ts',
},
{
file: 'test/gstack-ship-receipt.test.ts',
reason: 'spawns bin/gstack-ship-receipt and bin/gstack-artifact (shebangs) plus a #!/bin/sh gh shim through test/helpers/restamp-fixture.ts',
},
];
// Force-include overrides: files a WINDOWS_FRAGILE_PATTERNS regex excludes for
// a reason that does not actually apply to them. Each entry documents WHY the
// pattern hit is a false positive — the point of these files is Windows
// coverage, so auto-excluding them defeats the regression tests they carry.
export const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [
// Named Windows coverage for fixes whose bug only bites on Windows: kept in
// the curated lane even if a future edit trips a content pattern.
{ file: 'browse/test/cli-chain-stdin.test.ts', reason: 'piped-stdin browse chain against a stub daemon, and no-flow usage errors before any daemon boots; spawns the CLI through Bun argv' },
{ file: 'browse/test/runtime-root-server-bundle.test.ts', reason: 'a runtime root copy without node_modules resolves the checkout bundle via .source-path and refuses on version skew; os.tmpdir layouts only' },
{ file: 'test/cso-windows-docker.test.ts', reason: '/cso docker.exe discovery under known-folder roots (real path, no reparse points) and the native-transport-unsupported outcome; pure win32 path logic' },
{ file: 'test/copilot-windows-bash.test.ts', reason: 'the Copilot glossary line that runs bash blocks in Git for Windows Bash; reads host config only' },
{ file: 'test/ship-hook-windows-paths.test.ts', reason: 'runs bin/ helpers through explicit bash and Bun argv with forward-slash paths; never executes a shebang; the path-spelling simulation is skipIf win32' },
{ file: 'test/gstack-wtree-windows-paths.test.ts', reason: '#3101: runs bin/gstack-wtree through explicit bash in an os.tmpdir linked worktree with forward-slash paths; never executes a shebang; the drive-letter simulation is skipIf win32' },
{ file: 'test/setup-hook-syntax-gate.test.ts', reason: '#3088/#3100: runs bin/gstack-hook-check and the setup hook sections through explicit bash with forward-slash paths; never executes a shebang. Its Bun drive-path spelling is the Windows bug it guards' },
{ file: 'test/path-spelling.test.ts', reason: 'sources bin/gstack-path-spelling.sh through explicit bash and checks lib/path-spelling.ts; on Windows it exercises the real Git Bash spellings' },
{ file: 'test/state-root-parity.test.ts', reason: 'runs the bash twin and lib/state-root.ts over an env table with PATH empty; no shebang execution, raw-string comparison is platform-neutral' },
{ file: 'test/generator-eexist.test.ts',
reason: 'E4: runs the generators through Bun argv with the Windows EEXIST emulation preload; no shebang execution' },
{ file: 'test/gstack-config-gbrain-refresh.test.ts', reason: 'E6: runs bin/gstack-config through explicit bash; its gbrain/python3 shims are found by bash PATH lookup, never launched by CreateProcess' },
{ file: 'test/hook-scripts.test.ts', reason: '#3067 PowerShell and NotebookEdit hook coverage: every hook runs through spawnSync("bash", [script]) with the payload on stdin, scratch state lives under os.tmpdir(); its /tmp/ literals are payload strings, never opened; symlink, newline-path and POSIX-PATH cases are skipIf win32' },
{ file: 'test/gstack-doctor.test.ts', reason: 'runs bin/gstack-doctor and setup --status through explicit bash argv in an os.tmpdir fixture; its bun/codex/claude/probe stubs are found by bash PATH lookup, and expected paths use bash spellings' },
{
file: 'test/qa-evidence.test.ts',
reason: 'invokes the production helper through Bun argv and exercises native Windows job cleanup, private file captures and backpressured receipt output',
},
{
file: 'test/qa-evidence-selection.test.ts',
reason: 'bin/ strings are literal dependency and Windows-selection assertions; no native actor or shebang command is launched',
},
{
file: 'test/qa-deadline.test.ts',
reason: 'launches the guard through Bun argv; mode assertions and POSIX signal cases are platform-gated, while Windows job cleanup must execute natively',
},
{
file: 'test/qa-deadline-selection.test.ts',
reason: 'bin/ strings are dependency-selection inputs; this suite never launches a shebang executable',
},
{
file: 'test/shared-libs-source-reads.test.ts',
reason: 'bin/ literal is a mocked launch assertion; actual worktree fingerprinting explicitly invokes Bash on Windows',
},
{
file: 'test/claude-code-windows-job.test.ts',
reason: 'invokes Bun directly; verifies Windows job containment at the standalone CLI boundary',
},
{
file: 'test/claude-code-runner.test.ts',
// The bin/ path is launched through process.execPath (Bun), never as a shebang; keep taskkill supervision in the Windows lane.
reason: 'invokes the runner via Bun argv; fake CLI and timeout descendant assertions cover native Windows taskkill',
},
{
file: 'test/setup-gbrain-remote-caller.test.ts',
// bin is an expected PATH component; the adapter injects the SDK boundary
// and never launches a shebang. Keep the native delimiter cases in CI.
reason: 'replays the registered SDK callback with fixture-only bin paths; covers native Windows PATH composition',
},
{
file: 'test/cso-windows-build-contract.test.ts',
// bin is a temporary staging directory. The adapter injects spawnSync;
// real PowerShell/native execution remains in cso-windows-launcher.
reason: 'replays the native build callbacks with an injected subprocess; bin paths are staging fixtures, not shebang launches',
},
{
file: 'test/setup-windows-rerun-refresh.test.ts',
// Trips the "spawns bin/ shebang script" pattern via path.join(..., 'bin',
// 'tool.sh') fixture paths, but every spawn goes through test/helpers/bash-script.ts
// (bash <tempfile>) — Git Bash executes it fine on windows-latest, with no argv-length ceiling. This file IS
// the #2444 Windows regression coverage (IS_WINDOWS=1 copy-refresh path);
// excluding it here would keep the bug class unexercised on the one
// platform it bites.
reason: 'bin/ hits are fixture path segments; spawns bash explicitly — the IS_WINDOWS=1 refresh path must run on windows-latest',
},
{
file: 'test/uninstall-windows-copies.test.ts',
// Trips the "spawns bin/ shebang script" pattern via the
// path.join(ROOT, 'bin', 'gstack-uninstall') constant, but the script is
// always spawned through spawnSync('bash', [UNINSTALL, ...]). This file
// carries the #2563 Windows real-dir-copy uninstall coverage — the bug
// ONLY reproduces on the copy install shape windows-latest exercises.
// The symlink-shape describe block self-skips on win32.
reason: 'bin/ hit is a bash-spawned script path; #2563 real-dir uninstall coverage must run on windows-latest',
},
{
file: 'browse/test/file-permissions.test.ts',
// Trips the POSIX-mode-bitmask pattern, but every `mode & 0o777` assertion
// is platform-guarded: win32-only tests return early, POSIX-only tests
// guard the bitmask behind `process.platform !== 'win32'`, and the
// symlink-skip regression test both wraps symlinkSync in try/catch
// (runners without Developer Mode can't create symlinks) and guards its
// bitmask — on win32 it asserts behavior (warns, skips, doesn't throw,
// target stays usable), never fake Windows mode bits (dirs stat 0o777
// there, so a 0o755 expectation fails on runner semantics, not our code).
// This file carries the win32-only icacls-by-SID regression tests, which
// can ONLY execute on windows-latest — excluding it here means the
// machine-account ACL lockout regression is never exercised on the one
// platform it bricks.
reason: 'every mode-bitmask assertion is guarded off win32 (behavior asserted instead); win32-only ACL regression tests must run on windows-latest',
},
{
file: 'browse/test/terminal-agent-owner-watchdog.test.ts',
// Trips the spawn(['bun','run',...]) pattern, whose reason is the
// Playwright-bound browse server. This test spawns terminal-agent.ts,
// which imports only fs/path/crypto + local helpers (no Playwright, no
// PTY at module scope) and boots under Bun on Windows — the owner-PID
// orphan leak it pins was reported on Windows (#2019).
reason: 'spawns terminal-agent (no Playwright), not the browse server; owner-orphan leak is a Windows defect',
},
{
file: 'test/timeline-stop-hook-slug.test.ts',
// Trips the "spawns bin/ shebang script" pattern via the fixture's
// path.join(tree, 'bin', 'gstack-slug') stub, but the hook runs through
// Bun argv and reaches the stub through runBin, which hands it to Git
// Bash on win32. #3103 is a Windows latency defect: this lane is the one
// place the hook's return-within-budget assertion meets real Windows
// process startup.
reason: '#3103: the hook runs through Bun argv and its gstack-slug stub through runBin (Git Bash on win32); the return-within-budget timing must run on windows-latest',
},
];
/**
* Pattern hits disproved by running each excluded file alone on windows-latest
* (temporary probe run 37221995077, Bun 1.4.0, Chromium installed): every case
* passed and at least one case executed. Their /tmp/ literals are fixture
* strings or platform-gated, never opened on Windows.
*/
export const WINDOWS_PROBE_SAFE: string[] = [
'browse/test/data-platform.test.ts',
'browse/test/dual-listener.test.ts',
'browse/test/error-handling.test.ts',
'browse/test/learnings-injection.test.ts',
'browse/test/pdf-flags.test.ts',
'browse/test/security-adversarial.test.ts',
'browse/test/tunnel-gate-unit.test.ts',
'browse/test/xprotect-heal.test.ts',
'design/test/feedback-roundtrip.test.ts',
'ios-qa/daemon/test/tailscale-localapi.test.ts',
'make-pdf/test/asideClient.test.ts',
'make-pdf/test/e2e/diagram-gate.test.ts',
'make-pdf/test/e2e/emoji-gate.test.ts',
'make-pdf/test/e2e/format-gate.test.ts',
'make-pdf/test/e2e/landscape-gate.test.ts',
'test/brain-preflight.test.ts',
'test/ceo-barless-submit.test.ts',
'test/ceo-split-collection.test.ts',
'test/ceo-split-question-policy.test.ts',
'test/ci-paid-coordination.test.ts',
'test/design-md.test.ts',
'test/document-skills-redaction.test.ts',
'test/evals-workflow-wiring.test.ts',
'test/gbrain-guards.test.ts',
'test/global-discover.test.ts',
'test/helpers-unit.test.ts',
'test/helpers/hermetic-env.test.ts',
'test/office-hours-review.test.ts',
'test/paid-run-manifest.test.ts',
'test/paid-shards.test.ts',
'test/plan-count-completion.test.ts',
'test/plan-design-floor-fixture.test.ts',
'test/pty-skill-seeding-wiring.test.ts',
'test/regression-issue2091-bsd-mktemp.test.ts',
'test/regression-pr1169-mktemp-fallbacks.test.ts',
'test/resolvers-gbrain-save-results.test.ts',
'test/skill-preflight-budget.test.ts',
'test/takes-fence-fallback.test.ts',
'test/terse-build.test.ts',
'test/typecheck-test-ratchet.test.ts',
];
/**
* Returns the first POSIX-only pattern hit in the file, or null if Windows-safe.
*/
export function detectWindowsFragility(absolutePath: string): { reason: string } | null {
let content: string;
try {
content = fs.readFileSync(absolutePath, 'utf-8');
} catch {
return null;
}
for (const { pattern, reason } of WINDOWS_FRAGILE_PATTERNS) {
if (pattern.test(content)) return { reason };
}
return null;
}
export interface CurationResult {
safe: string[];
excluded: Array<{ file: string; reason: string }>;
}
export function curateWindowsSafe(files: string[], rootDir = ROOT): CurationResult {
const safe: string[] = [];
const excluded: Array<{ file: string; reason: string }> = [];
const knownBad = new Map(KNOWN_WINDOWS_INCOMPATIBLE.map((e) => [e.file, e.reason]));
const knownSafe = new Set([...KNOWN_WINDOWS_SAFE.map((e) => e.file), ...WINDOWS_PROBE_SAFE]);
for (const relativePath of files) {
const knownReason = knownBad.get(relativePath);
if (knownReason) {
excluded.push({ file: relativePath, reason: knownReason });
continue;
}
if (knownSafe.has(relativePath)) {
safe.push(relativePath);
continue;
}
const absolute = path.join(rootDir, relativePath);
const fragility = detectWindowsFragility(absolute);
if (fragility) {
excluded.push({ file: relativePath, reason: fragility.reason });
} else {
safe.push(relativePath);
}
}
return { safe, excluded };
}