Companion to the gen-skill-docs parsePathFlag extraction: the pin still
asserts the same invariant (LINK_ROOT defaults to OUT_DIR, so an in-place
render stays a byte-exact no-op) against the new expression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins the >145KB truncation regression (process.exit before the pipe
drained): 900 MEDIUM findings -> 259KB JSON report through a sleep-first
POSIX consumer that holds the 64KiB kernel buffer full at child exit;
asserts complete parseable JSON with matching counts and exit 2, plus an
--auto-redact mirror (700 redactions, final sentinel byte arrives).
Harness proven red against a copy of the bin with process.exit restored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The parser's turn.completed/turn.failed/disconnect semantics were pinned
by shape only — now the python block is extracted from both RENDERED
sections and run against synthetic event streams (tokens line, FAILED +
not-a-disconnect, silence -> disconnect warning, SESSION_ID echo), with
byte-equivalence safety pins on the bash double-quote extraction. The
fence scanner also gains CommonMark opener-length tracking: a 4-backtick
fence wrapping a 3-backtick example no longer false-positives, with a
self-test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GSTACK_GBRAIN_VERSION_PROBE_TIMEOUT_MS set to 'abc', '-1', or '0' must use
the default budget — exercised behaviorally through probeGbrainBin with a
fresh PATH per case (the memo keys on PATH).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SPAWNED_ESCAPE_SENTENCE's tightened wording had no pin: positive pins on
the explicit-declaration clause, negative pins on the retired v1.76 loose
parentheticals ('e.g. your dispatch prompt says', 'marks this session as
spawned'), and a drift guard that both hook directives embed the constant
verbatim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The only migration without a dedicated test. Covers copy-when-absent
(script must mkdir GSTACK_HOME itself), destination-wins (never
overwrites), clean no-op, and two-run idempotence — asserting file
existence and contents, not just exit codes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both cfg() helpers defaulted a null spawn status to 0 — a child killed by
signal would read as success and mask real failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite used a FIXED tmpdir name, so concurrent runs (sharded runner,
sibling worktrees) tore down each other's trees mid-flight — now a
per-run mkdtemp root with the apostrophe dir inside. gstack-telemetry-log
was the one bin named in the suite header with no test: it now must append
a real row under the hostile path with the credential span redacted
(<REDACTED-github.pat>) and the rest of the message preserved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #2724 disambiguateSlugs block was inserted between preparePages'
docblock and the function, orphaning the secret-scanning policy doc onto
the wrong symbol. Reordered. A call-site pin now asserts the prepare→stage
flow actually invokes disambiguateSlugs, so a refactor can't drop the call
while every unit test stays green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both the gh and glab send blocks now guard [ -s "$PR_BODY_FILE" ] and the
prose restates that the variable comes from the scan block — bash blocks
run in separate shells, and an unset/empty path would previously send an
empty body (gh) or cat's error output (glab) instead of the scanned bytes.
Codex/factory ship goldens regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two pinned-container periodic rounds showed that ANY dispatch-prompt
declaration channel in rule 1 keeps question counts unstable (round 1, fence
with behavioral clause: paired control 5>4, band 8>7; round 2, bare fence:
intermittent 0s return, paired control breaks both directions). The stable
regime CI was calibrated against had no spawned prose in the eager path at
all. Rule 1 now keys on exactly one machine-verifiable thing: the preamble's
own SESSION_KIND: spawned STATUS echo. No text from a dispatch prompt, file,
or page can flip a session to auto-choose (the strongest anti-injection
form). Subagents that missed the env marker are caught at FAILURE time by
the AUQ hooks' spawned escape (explicit declaration, never inference) — a
channel that never enters an interactive session's eager reasoning.
This reverses the wave's earlier explicit-declaration middle ground (and
adopts the outside voice's twice-made echo-only argument) on the new
evidence. #2733 protected: skill-e2e-docsync-spawned (gate) passes 1/1 on
this prose — the ship Step-18 dispatch forces the env prefix, so the echo
fires there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pinned-container periodic lane proved the collapse dead (reviewCount 0 →
7/8/5 across the AUQ suite) but flagged the fence's remaining behavioral
clause: 'never adds, removes, or batches the skill's decision points' broke
the paired-finding control (5 > 4 — it suppressed the batching that fixture
expects), and the band overshot its ceiling (8 > 7). Every behavioral tail
tried so far skewed counts somewhere ('when unsure, ask' → 8; 'HOW MANY
questions' → 1; 'never batches' → paired control red). The fence now ends at
'When unsure, default to interactive.' — classification only, zero behavior
words. Pins forbid every tried-and-failed phrasing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wave polish on the #2699 absorption: the hand-built JSONL interpolated the
raw tmpdir into a JSON string — on Windows (D:\a\...) that's an invalid
escape, the user line was silently dropped, and the body started at
'## Assistant' (Windows Free Tests red). JSON.stringify the path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wave's PR quality gate failed closed: gate-secret-scan.mjs pipes the
diff's added lines into gstack-redact and parses the JSON report, but
process.exit() discards stdout still buffered in the pipe — this wave's
646-finding report (202 KB) is the first big enough to arrive truncated
(~145 KB) at node's collector, so JSON.parse failed and the gate read
'no report' as HIGH. The report and auto-redact body paths now set
process.exitCode and let the runtime drain stdout; exit-code contract
unchanged (verified 0/2/3 end-to-end). Also: the C1 test's stdin payload no
longer uses a provider-prefix credential shape (the gate correctly flagged
it; the content was never read on the error path under test).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v1.74 racy-git fix carries the real index's mtime onto the temp copy —
but its 'touch -r … || true' meant a FAILED touch silently kept the copy's
fresh stamp, marking every entry non-racy and reopening the exact same-size-
rewrite hole. A failed touch now discards the copy and seeds from read-tree
HEAD (slower; every entry re-hashed; fingerprint stays honest).
Verification for #2687 itself: the reporter's same-size-rewrite repro run 20
iterations against this tree — 0 misses (the underlying race was fixed by
v1.74's b1485d88 with its own regression test; this wave verifies and closes,
it does not claim that fix). Receipt: the stubbed-touch test fails on a
v1.77.0.0 scratch worktree.
Fixes#2687
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--no-team is a one-shot teardown, so every later bare ./setup (including the
ones /gstack-upgrade runs) re-registered the timeline Stop hook with no way
to say 'never'. New gate mirrors the plan_tune_hooks pattern: flag
(--timeline-stop-hook/--no-timeline-stop-hook) > env
(GSTACK_TIMELINE_STOP_HOOK) > saved config (timeline_stop_hook) > default
yes. An explicit flag persists to config so the decision survives upgrades;
an explicit 'no' also removes a live registration (reconciliation), so the
opt-out works against installs registered by an older setup. --no-team
semantics unchanged (NO_TEAM_MODE is never initialized from config). Full
gstack-config surface: DEFAULTS entry, header docs, list/defaults
enumeration, warn-and-default validation.
Fixes#2677
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gen-skill-docs bakes its --out-dir into rendered CONTENT (rewriteSectionBase),
and both swap-in callers (setup, gstack-config gbrain-refresh) render into
claude.tmp.<pid> before the #2569 atomic rename — so every rendered skill
carried ~9 dead section Read paths that pointed at a directory the swap had
just deleted. New --link-root flag names the final serving dir (defaults to
--out-dir for direct-render callers: bin/dev-setup, dev-skill.ts, mkdtemp
tests — full caller audit in the wave notes); the rewrite now uses a
replacement callback so a $-bearing configured path can't expand as $& in a
replacement string. The swap logic itself stays byte-identical. Tests pin the
generator contract (tmp out-dir files reference the final dir, $-bearing
path included) and both callers' wiring.
Fixes#2692
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gstack-relink linked SKILL.md from RENDER_DIR when a gbrain render was active
but ran gstack-patch-names only on INSTALL_DIR, so the served frontmatter kept
the unprefixed name and skill_prefix=true silently no-oped for every
brain-aware skill. The render tree (user-owned, untracked) is now patched too;
gstack-patch-names is idempotent so repeat relinks never double-prefix. The
gen-skill-docs note that pointed users at relink now describes what relink
actually covers.
Receipt: the new test fails on a v1.77.0.0 scratch worktree (served render
keeps 'name: qa').
Fixes#2738
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When Codex is unavailable, the plan-review outside voice falls back to a
Claude subagent and the copy sold it as 'cross-model coverage' with 'genuine
independence'. Fresh context is real; cross-model validation is not — a user
weighing 'both reviewers agree' deserves to know both reviewers share a model
family. Six canonical strings fixed at the resolver source (constants.ts
not_installed/not_authed, review.ts outside-voice bullet + three dispatch
paragraphs); ~10 generated docs and the ship goldens regenerated. Printing
the resolved fallback model at dispatch time is descoped as a functional
change (follow-up in the wave dispositions).
Fixes#2735
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three defects in the codex skill sections:
- The resumed-session bash block never closed its fence; every fenced region
after it inverted (prose rendered as code, the synthesis-recommendation tail
rendered inert). A repo-wide fence-pairing test now scans every generated
SKILL.md and sections/*.md with a CommonMark-faithful state machine (an
info-string opener inside a fence is literal content — nested template
examples in document-generate/make-pdf stay legal; a file ending inside a
fence fails).
- The JSONL parsers had no turn.failed branch: a turn that STATED its failure
was reported as 'possible mid-stream disconnect'. Challenge and consult now
print the event's error and run a three-way completeness check (failed-with-
reason / silent-disconnect / ok); consult previously had no completeness
check at all.
- ${PIPESTATUS[0]} is empty under zsh, so hang detection never fired and
every clean run printed a spurious '[codex exit ]'. All three capture sites
use ${PIPESTATUS[0]:-${pipestatus[1]}}, pinned statically and EXECUTED
under real bash and zsh in the new test. Expect a step-change in
codex_timeout telemetry — the counter starts firing for zsh users.
Receipt: the portability pin fails on a v1.77.0.0 scratch worktree; the fence
fix is structural (17 → 18 fence lines, tail no longer inside a block).
Fixes#2671Fixes#2669
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolveGbrainBin's bare catch collapsed 'gbrain missing' and 'gbrain present
but the 2s --version budget expired' into the same null — freshClassify then
said no-cli, which the --is-ok whitelist from #1964 does NOT forgive, so a
bun-shim install on a loaded POSIX box silently lost every brain-aware block.
The probe now returns a discriminated result (cached per-process, same
lifetime the old null had) using the same killed/SIGTERM/ETIMEDOUT
discrimination the sources-list probe below already uses; timeout routes to
the forgiven 'timeout' status. GSTACK_GBRAIN_VERSION_PROBE_TIMEOUT_MS test
override added (same precedent as the sources-probe override).
Receipt: the slow-but-present sibling test fails on a v1.77.0.0 scratch
worktree (classifies no-cli there).
Fixes#2716
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The absorbed community tests (#2748, #2676, #2714, #2720) were authored
before the v1.77 tripwire required a timeout on every sync spawn in the test
trees.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wave-amended: gh leaves .headRepository.nameWithOwner empty (verified live against gh 2.83) — owner/name now composed from headRepositoryOwner.login + headRepository.name so reconciliation is not a permanent no-op; fork branches get report-not-delete (maintainers lack fork push rights); pins updated
Wave-amended: seeding relocation re-applied to the composite action (v1.77 moved CI seeding out of the inline workflow steps the original commit edited); wiring tripwire re-pointed accordingly; stale marker comment updated
Two independent bugs made transcript pages silently fail to reach the brain.
1. Frontmatter fence gluing. buildTranscriptPage() built the closing "---"
with no trailing newline, and session bodies always start with "## ", so
the rendered page ended "...---## User". gbrain's frontmatter matcher
(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/ in src/core/markdown.ts) requires
the closing "---" to end its own line, so it skipped the glued fence,
latched onto the next standalone "---" in the transcript body, parsed the
prose between as YAML, and dropped the page with "Invalid YAML frontmatter".
Transcripts with no later "---" fell back to body-only, silently losing
their frontmatter. Fix: emit the fence on its own line with a blank
separator, matching renderPageBody()'s artifact branch.
2. Slug collisions. Two source files can map to one path-derived slug (a
session resumed under the same id on one day, or two ids sharing a 12-char
prefix). writeStaged() names each file "${slug}.md", so the second
overwrote the first; gbrain collected N-1 of N staged files and the
reconciliation guard failed the whole batch every run. Fix:
disambiguateSlugs() keeps the first occurrence and gives each later collider
a stable "-<sha8(source_path)>" suffix (deterministic, and slug + page_slug
move together so writeStaged, the failure mapping, and state recording agree).
Exports buildTranscriptPage, renderPageBody, and disambiguateSlugs for tests.
Adds regression tests for both failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave-amended: contributor's local-workaround docblock note removed; issue refs retargeted #2653 (closed by its author) -> #2724 (the live 887-staged-to-0-ingested report)
Two independent Windows git-bash bugs in the bin writers, both silent
because callers invoke these scripts with 2>/dev/null and do not check
the exit status — a hard failure was indistinguishable from success.
Bug 1 — apostrophe in the checkout path breaks the bun -e program.
gstack-learnings-log, gstack-question-log and gstack-telemetry-log build
a bun -e program as a double-quoted shell string and interpolate
SCRIPT_DIR into a single-quoted JS import specifier. A path such as
C:/Users/Someone's PC/... closes the JS string literal early and Bun
fails to parse ("Expected ; but found s"). Every learning write and every
plan-tune question event no-oped; telemetry error redaction fell to its
fail-closed null path. The #1950 cygpath -m guard did not cover this —
cygpath normalises the drive form but does not remove the apostrophe.
Fixed by not interpolating the path at all: cd into the module root and
use a relative import specifier, which is immune to apostrophes, spaces,
backslashes and MSYS paths alike. The one remaining interpolated data
path in gstack-developer-profile (readFileSync of PROFILE_FILE) is passed
via the environment instead, matching do_log_session in the same file.
Bug 2 — gstack-developer-profile --derive fails on an MSYS-form
GSTACK_HOME. GSTACK_HOME defaults to $HOME/.gstack, which under git-bash
is /c/Users/..., and Bun on Windows cannot open that form (ENOENT). This
script carried no cygpath guard at all. Fixed by normalising GSTACK_HOME
once, before PROFILE_FILE / LEGACY_FILE / the events path are derived
from it, so all three pick up the normalised value.
Adds test/hostile-path-writers.test.ts, which runs the bins from a
directory whose name contains an apostrophe and asserts that rows are
ACTUALLY WRITTEN (not merely that the exit code is 0 — exit-code-only
checks are what masked bug 1). The apostrophe repro is OS-independent:
SCRIPT_DIR derives from the script's own location, so a copied checkout
under a hostile directory name reproduces bug 1 on Linux/macOS CI too.
Wave-amended: all four writers unified on the env-var import pattern the PR already used in gstack-developer-profile (no CWD-dependent module resolution)
Wave-amended: all four writers unified on the env-var import pattern the PR already used in gstack-developer-profile (apostrophe-safe without CWD-dependent module resolution); import-shape pin updated
Burn-in calibration: run 1 (fence tail 'when unsure, ask') overshot the
plan-ceo review band at reviewCount=8; run 2 (tail mentioning 'HOW MANY
questions') undershot at 1. Any ask-count language in the fence anchors the
model in one direction or the other. The tail now says only: classify as
interactive, then follow the skill's own decision-point instructions exactly
as written. Pins updated to forbid count language in either direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 0 read the old pid with `grep -o '"pid":[0-9]*'` and Step 2 read the port
the same way. Neither can match. Every writer of that file in
browse/src/server.ts serializes with `JSON.stringify(state, null, 2)`, so the
bytes on disk are `"pid": 12060` — colon, space, digits.
The failure was silent in the worst way. `_OLD_PID` came back empty, the kill
never ran, browse.json was deleted anyway, and the next `connect` died with
"existing daemon has different config (proxy/headed mismatch)" — an error
pointing at proxy/headed flags rather than at the cleanup that no-opped. Caught
against a daemon left over from a reboot: the operator was told to check flags
they had never passed.
Both patterns now accept optional whitespace. The new tripwire does not match
strings — it RUNS the snippets the skill hands the agent, against a state file
written exactly the way the server writes one, and asserts pid and port come
back out. A third case pins the coupling to `JSON.stringify(state, null, 2)`,
so a switch to compact JSON surfaces as a failing expectation rather than as
silence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111Mq3JGwZDcstn5wYcbhSw
`gitleaksAvailable()` cached every failure the same way, so a 2s timeout on
`gitleaks version` was recorded as "the binary is absent" for the rest of the
process. One busy moment and the whole ingest ran unscanned behind a single
stderr line — a fail-open outcome decided by machine load rather than by
anything about the machine's setup. The caller only acts on
`scanner === "gitleaks"`, so every later file was written with no scan and no
second warning.
The probe now classifies three outcomes. ENOENT (and a present-but-unusable
binary: bad exit, EACCES) stays cached — that is a fact about the box, and
re-probing it per file would be waste. A timeout gets one retry on a 10s
budget, and if that also expires nothing is cached: the file is reported
unscanned, the warning says so in those words, and the next file probes again.
Observed under the 7-way sharded free-test runner, where spawning a shell
script inside a temp bin dir took longer than the 2s budget.
Tests: the retry path, the no-cache-on-timeout path (the second call must
re-probe), and the cached-absent path. The fake gitleaks hangs for 30s rather
than racing a short sleep against a short budget, and the budgets are chosen so
load cannot flip an outcome: 30s where the retry MUST answer, 800ms where the
probe MUST expire. An earlier draft used 1s/5s and flaked under the same shard
runner this commit is about. The existing probe test pinned `detect` to
calls[1], which a retry breaks; it now asserts the order instead of the index.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0111Mq3JGwZDcstn5wYcbhSw
gbrain 0.43+ refuses a held PGLite lock with exit 1 and the message
"GBrain's local database is already open through `gbrain serve` (MCP,
PID N)" instead of the pre-0.43 exit 124 + "connect timed out" that
the #2194 branch matches. The message matches no known pattern, so the
classifier falls through to the defensive broken-config default — and
Step 1.5 of /setup-gbrain and /sync-gbrain then tell the user to move a
perfectly healthy config.json aside and re-init the engine.
Reproduced live on gbrain 0.43.0.0, 0.44.0.0 and 0.46.30.0: with a
serve holding the lock, gstack-gbrain-detect reports
gbrain_local_status=broken-config; after stopping the serve it reports
ok with the same untouched config.
Match on the stable substring "already open through", mirroring the
existing #2194 branch semantics: engine-locked for pglite, broken-db
otherwise. Adds a fake-gbrain behavior for the 0.43+ refusal plus two
cases (pglite -> engine-locked, postgres -> broken-db).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A typo was stored with exit 0, so the feature stayed off and the first-run prompt never returned. Reject like codex_reviews; do not coerce.
Co-authored-by: Cursor <cursoragent@cursor.com>
The marker check returned before the only writer, so once a repo had the
hook, no later change to the wrapper could ever reach it. The `printf x`
fail-open fix (v1.64.0.0) has still not landed in any repo that received
the hook before it, and a wrapper naming a gstack that has since moved
stays pointed at a dead path for the same reason.
Compare the body against what this version generates: rewrite on drift,
stay a no-op when identical. The chained pre-push.local is untouched on
both paths.
The existing trailing-newline regression test cannot catch this — it
installs into a repo with no prior managed hook, the one case that was
never broken.
Wave-amended: spawnSync timeouts added to the new tests (v1.77 sync-spawn tripwire)
The marker check returned before the only writer, so once a repo had the
hook, no later change to the wrapper could ever reach it. The `printf x`
fail-open fix (v1.64.0.0) has still not landed in any repo that received
the hook before it, and a wrapper naming a gstack that has since moved
stays pointed at a dead path for the same reason.
Compare the body against what this version generates: rewrite on drift,
stay a no-op when identical. The chained pre-push.local is untouched on
both paths.
The existing trailing-newline regression test cannot catch this — it
installs into a repo with no prior managed hook, the one case that was
never broken.
`pii.email` matches the `git@github.com` inside
`git@github.com:acme/widgets.git`. That is a transport user@host, not a
person's address, so any diff touching a clone URL -- a deploy config's
repo URL, a submodule entry, a README clone line -- draws a spurious
MEDIUM from the pre-push hook.
Suppressed by URL shape rather than by adding `git` to
EMAIL_ALLOW_LOCALPARTS. A bare `git@` allowlist entry would also
suppress a genuine address at a domain that merely begins with "git"
(git@gitmail.com), converting a false positive into a false negative --
the worse failure for a guardrail. Two shapes are accepted:
- `<user>@<host>:<path>.git` for ANY host, covering self-hosted
remotes, plus the equivalent ssh:// URL form.
- `git@<known-host>` for github.com, gitlab.com, bitbucket.org and
ssh.dev.azure.com, whose bare form appears in docs and in
`ssh -T git@github.com` connectivity checks with no path at all.
Matched exactly, so gitmail.com is unaffected.
emailAllowed now receives the normalized text and the span offset so it
can see that surrounding shape; it had only ever been passed the matched
span.
Tests pin both directions: the SSH remotes go quiet, and a real address
still fires -- including at a git host (alex@github.com) and at a
git-prefixed domain (git@gitmail.com).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`internal.hostname` ends in `.local|.prod|.staging|…`, so `.env.local`
matches on `env.local` and a dotenv FILENAME is reported as a leaked
internal host.
The collision is not exotic. It fires on `--env-file=.env.local` in an npm
script, `.env.staging` in a README, `.env.prod` in a .gitignore — ordinary
lines on branches that leak nothing. Measured on one private repo, three of
four MEDIUM findings in a routine push were this, and the fourth was a
deleted localhost URL. That ratio is the real cost: a scanner that reports
package.json is one people learn to skim, and skimming is how the HIGH
finding it exists for gets missed.
The guard follows the `insideUuid` precedent and stays deliberately narrow —
it exempts only a span beginning `env.` immediately preceded by a dot, i.e.
the literal `.env.<suffix>` form. `api.corp.local`, `build-7.internal` and
`myenv.local` all still report.
The test pins both directions, and the negative controls are the point: an
exemption written as "any span ending .local" would pass the dotenv half
while quietly gutting the pattern for every real host. Verified red/green —
with the validate hook removed, exactly the 6 dotenv cases fail and all 9
real-host controls still pass.
Follow-up to #2477. The model probe it added does a real round trip, but its
final branch is the `else` of a "model 400" grep, so it swallowed spawn ENOENT,
non-executable binaries and missing vendor payloads alongside genuine network
timeouts. All three are deterministic — retrying never helps — yet they landed
in the fail-open bucket and resolved to `ready`, so every Codex pass was
skipped in silence and the review reported itself complete.
Observed live: @openai/codex was on PATH with an empty
vendor/aarch64-apple-darwin/codex/ directory. gstack said `ready` for two
months while no Codex pass ran.
Three changes:
- `_gstack_codex_model_probe` classifies deterministic install failures (exit
126/127, or stderr matching ENOENT/ENOEXEC/EACCES/"cannot execute binary
file") as MODEL_UNUSABLE_INSTALL, exit 2, never cached — a reinstall is
picked up on the next probe. Exit 124 and genuine transients still fail open,
which is what #2477 intended.
- The preflight chain captures the probe's code instead of testing it for
truthiness, so exit 2 routes to a new `broken_install` mode whose remedy is
`npm install -g @openai/codex` rather than "check your model pin". A missing
binary and an unusable model are different problems with different fixes.
- `_gstack_codex_version_check` no longer reads a broken CLI as healthy. It ran
`codex --version 2>/dev/null | head -1`, which captures head's status, not
codex's — and 2>/dev/null discarded the one diagnostic available. It now
captures the real exit code and warns on non-zero. Empty-but-successful
output stays silent, per the existing "empty output → OK" case.
Tests: 6 added to test/codex-hardening.test.ts covering both broken-install
shapes, the exit-2 contract, no caching, the transient still failing open, the
model 400 still classifying as MODEL_UNUSABLE, and the version-check warning.
845 pass / 0 fail across all 8 suites touching the changed files.
Closes#2742
Wave-amended: autoplan hand-maintained preflight chain completed (tmpl+render); install-signature grep gated on failed spawn only; goldens regenerated against the wave tree (author's golden commit 5797d326 superseded); +2 tests
The /ship Design Review step skipped the checklist because the generated path omitted the gstack/ install segment. Sync the generated skill doc and pin a regression assertion.
Co-authored-by: Cursor <cursoragent@cursor.com>
Wave-amended: goldens regenerated against the wave tree (author's golden commit 8e7a03ca superseded)
Lock paths are checked before pgrep. Spreading process.env let a runner
GBRAIN_HOME with a live lock refuse the case before the stub ran.
Co-authored-by: Cursor <cursoragent@cursor.com>
The only non-dry-run --code-only child hits #1734's PATH-resolved
autopilot probe. A live host daemon is a correct refuse; the test
cannot inject processRunning. Neutralize pgrep in the fixture bindir
instead of adding a production env hatch.
Co-authored-by: Cursor <cursoragent@cursor.com>
The ignore file was inert from v1.65.0.0: OSV-Scanner only auto-discovers
configs named osv-scanner.toml (no leading dot) and applies them
per-directory, so the root config never covered lib/diagram-render/bun.lock
either way. The workflow now passes --config=.osv-scanner.toml globally.
Every IgnoredVulns entry carries a reason with an upgrade trigger and an
ignoreUntil expiry (~90 days) so suppressions must be re-justified. A wiring
test pins flag ↔ filename ↔ entry hygiene so the file can never silently go
inert again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Burn-in run 1 of the periodic repro overshot the review band (reviewCount=8 >
CEILING=7) with the fence's 'when unsure, ask' tail: that phrasing is a quota
nudge, not a classification default. The fence now states it only classifies
the session and never changes how many questions the skill asks. Pin added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An empty $(mktemp) result silently disabled the redaction pass (redact-doc
resolver, ship pr-body) and made /gstack-upgrade's vendored path destructive:
clone lands at "/gstack", the swap mv fails, and rm -rf then deletes BOTH the
live install's backup and "". All three sites now guard the assignment with a
loud exit; the vendored block additionally restores the backup when the swap
fails (same failure class — backup deletion after a failed mv) and the GitLab
MR path sends the SCANNED file's bytes instead of re-rendering an unscanned
heredoc. bin/gstack-redact rejects an explicit empty --from-file path instead
of silently falling through to stdin.
Receipts: 6 of 8 new regression checks fail on a v1.77.0.0 scratch worktree.
Fixes#2679
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v1.76 spawned rule's parenthetical '(or your dispatch prompt marks this
session as spawned)' let the model INFER spawned status from a scripted-looking
prompt in a CI-looking session and silently auto-choose every review-phase
question: reviewCount=0 across the plan-review periodic E2Es (weekly run
33363624506, 9 of 14 failed shards; reproduced locally, zero AUQ fingerprints).
Env and hook paths were excluded by inspection: hermetic children echo
SESSION_KIND: interactive (CLAUDE_CODE_ENTRYPOINT=cli beats CI markers) and the
question-preference hook isn't installed there.
The trigger is now objective: the echoed SESSION_KIND: spawned STATUS line, or
an EXPLICIT dispatch-prompt declaration ("you are a SPAWNED subagent") —
declared, never inferred — with an absence-safe interactive fence: CI env vars,
scripted-looking or pasted prompts, and write-to-this-exact-file instructions
are NOT spawned markers. The prose channel stays because Task-tool subagents
inherit the parent env (no spawned prefix) — their dispatch prompt is the only
signal; #2733's env-prefix channel is untouched.
19 carve skeleton ceilings re-pinned with measured values (+~440 bytes/skill);
ship goldens refreshed for all three hosts; resolver pins extended with the
no-inference regression tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: pin the claude CLI to an exact version in the CI image + tripwire
The image installed @anthropic-ai/claude-code UNPINNED and rebuilt weekly
'to pick up CLI updates' — while bun sat carefully pinned at 1.3.13 two RUN
lines above. The PTY harness screen-scrapes this CLI's TUI, and that drift
broke it three separate times (welcome-screen wedge on 2.1.233, skillify
HOME discovery on 2.1.237, guard/freeze hooks on 2.1.162), each debugged as
a flake first. Pin 2.1.251 (current latest), bump deliberately via a PR
that runs the PTY gate, and enforce with test/ci-image-cli-pin.test.ts:
any global npm install in Dockerfile.ci without an exact @X.Y.Z pin fails
the free suite. The weekly ci-image cron stays as a cheap tag self-heal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: stamp the claude CLI version into every eval-store run record
Three harness breakages were traced to claude-CLI TUI drift only after long
flake hunts, because no run record said which CLI it actually exercised.
EvalCollector now stamps claude_cli_version (claude --version, cached once
per process, 'unknown' when the binary is absent) into both partial and
finalized records — schema-additive optional field, no SCHEMA_VERSION bump.
Correlating a flake wave with a CLI release becomes a grep over
~/.gstack/projects/<slug>/evals/ instead of archaeology.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: give the spinning-shard kill test load headroom (30s -> 90s)
The test spawns and group-kills three real children (one a busy-loop
burning a full core) while five sibling shard processes compete for eight
vCPUs. Under full-suite load it blew bun's default 30s per-test ceiling at
30,009ms — while passing in isolation in 1.4s — and red the only required
lane. Every assertion in it is event-based (statuses, group-kill proof,
heartbeat lines); the sole latency claim is the <30s kill-deadline sanity
bound, which stays. Explicit 90s headroom, not a weakened oracle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: green-by-skip census — skip counts in the classifier, all-skipped labeling in the paid runner
bun's 'Ran N tests' line COUNTS skipped tests, so a codex/gemini shard
whose every test self-skipped (binary absent on the runner — true of every
CI runner today) exits 0, dodges the hollow-shard guard, and reads as
coverage in the weekly census. The classifier now parses bun's ' N skip' /
' N pass' recap lines; ShardOutcome carries skippedTests; formatSummary and
the fail-closed slices report label an all-skipped pass explicitly:
'all N tests SKIPPED — verified nothing'. Status stays 'passed' (external
service availability is host state, not a repo regression) but the census
can no longer mistake absence for coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: extract composite actions for eval-lane setup; surviving lanes gain the fail-fast registry verification
'Fix bun temp' x3, 'Restore deps' x5, 'Seed claude interactive config' x3,
and 'Register gstack skills' x3 were byte-near-identical copies across the
legacy matrix, the sliced lane, and the periodic lane — and only the MATRIX
copy of register-skills carried the 19-line dangling-symlink + frontmatter
fail-fast loop written after a silent 'Unknown command' + 35-min-timeout
incident. Extract all four into .github/actions/ composites; the register
composite carries the verification loop (generalized over the skill list),
so the sliced and periodic lanes — the lanes that SURVIVE the matrix
deletion — now inherit the check they had silently dropped. Matrix-job
inline copies are left untouched: that job is deleted next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: delete the legacy 17-row eval matrix — the sliced lane is the only paid lane
Every PR paid twice: the hand-enumerated matrix (18 test files, 22.6 min,
~$21 API measured on run 33263204465) ran serialized AHEAD of the strictly
superior sliced lane via 'needs: evals' — 35.5 min wall and ~2x paid spend
for the same diff. 14 of 17 rows carried no tier:, so periodic Opus
benchmarks leaked into every PR (the e2e-plan row alone: 12/12 tests,
21.7 min, $7.28 — the wall-clock bound of ALL of CI).
Parity receipt (static, pre-deletion): the sliced lane's gate census (49
files, derived from the runner itself) strictly contains all 18 matrix test
files, plus 31 files the matrix never ran. Pure deletion — one revert
restores it. The PR comment moved into slices-report (same '## E2E Evals'
upsert marker, now sourced from slice artifacts + carrying the fail-closed
reconciliation verdict). plan-slices loses the needs edge; the dead
workflow-level EVALS_TIER env goes with it.
test/evals-workflow-matrix.test.ts (and its KNOWN_MATRIX_GAPS /
KNOWN_TIER_UNSET burn-down ratchets — retired: the sliced census makes
'every gate file runs' true by construction) is rewritten as
test/evals-workflow-wiring.test.ts: matrix stays deleted, planner/executor/
report tier + slice-count agreement, both surviving lanes on the shared
register-skills composite with its fail-fast verification loop, PR comment
survival. Expected: PR eval wall 35.5 -> ~13 min, per-PR paid spend ~halved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: provider-runner timeouts kill the whole process GROUP; codex/gemini inherit the orphan-drain hardening
All three provider runners (claude/codex/gemini) killed only the direct
child on timeout: tool subprocesses the CLI spawned survived as orphans
holding our pipes open and burning shared API rate (observed: a 600s
timeout stretching past 1400s; a stalled run once burned a core for 15
hours). gstack-detach's watchdog had the same shape one level up — killpg
SIGTERM, 5s grace, then a direct-child proc.kill() that orphaned
grandchildren.
Fix: spawn provider children via node:child_process with detached (own
process group) and killProcessGroup(SIGKILL) in the timeout handler —
runShardChild's proven pattern, EPERM/ESRCH fallbacks included. The codex
and gemini copies also gain the reader.cancel() + stderr Promise.race
hardening only the claude copy had (they still carried the blocked-drain
hang it fixed). gstack-detach's watchdog now group-SIGKILLs after the
grace.
Regression net: test/session-runner-groupkill.test.ts drives the REAL
runSkillTest against a fake claude shim (PATH override) that spawns a
grandchild and wedges — the run must classify timeout within budget and
leave neither shim nor grandchild alive — plus source pins on all three
runners (detached + killProcessGroup, no bare timeout kill, no Bun.spawn
reversion).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: skill-e2e-opus-47 renders SKILL.md fixtures into a mkdtemp — never the live tree
mkEvalRoot ran gen-skill-docs with cwd=ROOT, regenerating every in-repo
SKILL.md mid-run while concurrent paid shards copyFileSync those same files
in their beforeAll (EVALS_JOBS>=4 locally, 2 per CI slice) — a sibling
could capture a half-regenerated or opus-rendered SKILL.md, and a timeout
before afterAll stranded the whole tree at the wrong model for every later
shard. A cross-shard race that could flake ANY concurrent paid test.
Render via the --out-dir flag gen-skill-docs grew for exactly this reason
(mirrors the repo layout, which is all the fixture reads), read the skill
heads from the render dir, delete it, and drop the afterAll restore-regen
entirely.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: claude CLI version resolves in the runner parent, never on a test thread
Eng-review finding: getClaudeCliVersion's fallback is a SYNCHRONOUS
spawnSync on the same thread that polls concurrent PTY/session tests — the
judgePtyState blocking class this overhaul kills elsewhere. The paid runner
parent now resolves it once (cached) and stamps GSTACK_CLAUDE_CLI_VERSION
into every shard's env; eval-store short-circuits on the env var, and the
fallback spawn's budget tightens 10s -> 3s (bounded one-time stall, records
'unknown' on a slow CLI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: wire skippedTests end-to-end through runPaidShard
The census unit tests hand-built outcomes and the classifier tests parsed
strings; nothing proved a real child's ' N skip' recap flows into
outcome.skippedTests and the formatSummary label. A commandFor fake now
prints the recap shape and the test asserts the parsed counts, the
all-skipped predicate, and the 'verified nothing' label.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: make the setup composites rerun-safe (codex diff-review hardenings)
restore-deps: 'cp -r SRC node_modules' with an existing node_modules NESTS
the copy and leaves stale deps active — rm first. register-gstack-skills:
'ln -snf' hard-errors under set -eu when a REAL directory occupies the
gstack slot — clear a non-symlink leftover first. CI workspaces are fresh
today; a reusable composite must survive dirty reruns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: sweep — every sync spawn in the test trees carries a timeout (436 sites, 157 files)
spawnSync/execSync/Bun.spawnSync BLOCK the main thread, so bun's in-process
per-test timeout can never fire while one waits — a hung child (stdin read,
network probe, dead daemon) wedges the whole shard until the runner's
external wall-clock SIGKILL. This exact class reached main: free-tests run
33262077256, test/gstack-memory-ingest.test.ts (normally 2.3s) held shard 2
at the 360s wall while its five siblings finished in ~65s.
Mechanical sweep in two waves (12 + 4 fan-out agents, every edit verified
against its call site): default timeout: 30_000 (matches the free runner's
per-test budget), 120_000 for genuinely slow ops (installs, builds,
playwright, provider CLIs), helper wrappers fixed ONCE where call sites
route through them. Sites that only LOOK like calls (string fixtures, grep
needles, comments) were skipped with reasons — the enforcement commit that
follows marks them exempt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: sync-spawn timeout tripwire — the wedge class stays extinct
Free scanner over all test trees (test/, browse/test/, design/test/,
make-pdf/test/, ios-qa, browser-skills): every spawnSync/execSync/
Bun.spawnSync call site must carry a timeout within a 30-line options
window, or an explicit '// tripwire-exempt: <reason>' marker. Comment
lines are skipped; exemptions are counted and ratcheted shrink-only
(ceiling 6 = the 6 string-fixture/grep-needle sites where the pattern is
CONTENT, not a call — marked in this commit). A scan-sanity test pins that
the scanner still sees >100 real call sites so it can never rot to a
vacuous green. Companion to the 436-site sweep in the previous commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: paid-lane flake telemetry — record-level attempts, flaky_retries, report surfacing
bun --retry leaves a retried pass INVISIBLE in its output: a fail-then-pass
prints the error detail but no (fail) result line and recaps as a clean
pass (probed live on 1.3.10). So attempts are recorded where they cannot
lie: EvalCollector.addTest stamps a 1-based attempt on same-name re-records
(a retried test runs its body again and re-records), finalized runs carry
flaky_retries, printSummary warns loudly, and the fail-closed slices report
lists every passed-only-on-retry test — recorded and ranked, never blocking
and never silent. Cross-model confirmed (codex reached the same don't-parse
-the-stream conclusion independently).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: free-lane flake ledger — retry ON in CI, flaky-passes recorded and uploaded
The runner's attribution-gated flaky-retry pass (cap 5, truncation veto)
was OFF in the required lane and its FLAKY-PASS evidence was console-only —
so a single timing flake red the merge gate while repeat offenders stayed
unenumerable. free-tests.yml now sets GSTACK_FREE_RETRY_FLAKY=1 and points
GSTACK_FLAKE_LEDGER at runner.temp; every flaky-pass appends a JSONL entry
(SINGLE writer: the parent runner — no concurrent-append hazard by
construction; fail-open with a loud warning so a broken ledger can never
red the lane) and the artifact uploads UNCONDITIONALLY — a flaky-pass run
is green, which is exactly when the evidence matters. Wiring pinned by
free-tests-workflow-wiring; ledger behavior unit-tested incl. the fail-open
path. Matches 2026 industry practice (retry for data, quarantine out of
merge-blocking but never out of logging) with the repo's own receipts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: eval:flake-rank — the flake-telemetry dial
Aggregates per-test series across every finalized eval-store run (shard
dirs included) plus the free flake ledger: runs, fails, RETRIED PASSES
(the flake signature), avg duration — ranked retries-first. This is the
readable dial behind two policies: a flaky pass never blocks a merge but
is always ranked here, and the WS16 required-check promotion needs weeks
of clean flake-rank, not vibes. --json for machines, --dir for downloaded
CI artifacts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: two-phase session timeout — silent APIs die at the startup grace, named
The single spawn-armed timer charged API queue latency to the work budget:
the recurring '0 turns / $0.00 / x3 attempts' failure with four budget-bump
receipts (180->300s, 240->360s, 300->420s, 90->300s). Split: startup phase
(no NDJSON byte yet) kills EARLY at min(grace, timeout) with the distinct
exitReason 'timeout_startup' — an availability verdict, not transcript
archaeology — and the work phase arms on the first byte for the REMAINING
budget, so total wall never exceeds the timeout (tier envelopes are
margin-free: tests pass timeout: CAPTURE_MS and bun-budget the same tier).
Local grace 90s (observed queue latency 60-90s), CI floor 300s (TODOS-filed;
shared runners queue harder), both pinned by the new grace tests with fake
-claude shims covering the late-first-byte and silent-API paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: census integrity — 17 phantom selection keys deleted, reverse invariant added, gitignored dep patterns replaced, local map forks derived
The merge-blocking gate census counted tests that could not run. Deleted
(critic-verified against both quoted-occurrence and dep-registration
liveness): 7 *-prosons-format keys with no declaring test, ship-plan-
completion/-verification, review-plan-completion, design-shotgun-path/
session/full, autoplan-core (dead ~10 months), e2e-harness-audit (its
namesake is a FREE-suite file), plus 2 dead LLM-judge keys and 2 free-file
keys (budget-regression-pty, global-discover) misplaced in the PAID maps.
Census: 191 -> 174 keys, gate 86 -> 78 honest.
The new reverse invariant in touchfiles.test.ts makes the class structurally
impossible: every key must be quoted in a living paid test file OR
registered to an existing paid test file via its dep list (the constructed-
name binding the 2026-08 self-registration sweep established) — zero
exceptions needed today, with a live-file check on any future exception.
Also: '.agents/skills/**' dep patterns replaced with the generator
(scripts/gen-skill-docs.ts) — .agents/ is gitignored, so those patterns
could NEVER match a git diff and review-template edits silently stopped
selecting codex/gemini tests; the codex/gemini local touchfile maps are now
DERIVED from the canonical map (loud throw if a key vanishes) instead of
hand-forked copies that had already drifted. ios-qa-e2e demoted gate ->
periodic: its gate declaration was never executable in CI (hardware
exclusion only applies at tier=periodic), so every Linux PR planned a
hollow shard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: routing journeys lose their answer key and end at the routing decision
The journey tests exist to catch skill-DESCRIPTION regressions (touchfiles:
*/SKILL.md.tmpl), but the fixture CLAUDE.md shipped an explicit
prompt->skill lookup table — with the answer key in context, a badly
regressed frontmatter description still routed correctly, so the tests
could not fail on the exact class they select for. The fixture now carries
only the generic invoke-skills nudge; the frontmatter carries the routing
load. Also capped all 10 journeys at maxTurns 2 / tools [Skill, Read]:
only the FIRST Skill call is asserted, so 5 turns of Read/Bash/Glob/Grep
was pure spend — roughly halves each journey's cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: retire decided A/B experiments; vendor the pre-cut fixture; ban raw-SHA fixtures
Three one-shot decision experiments kept re-running weekly as N=1
stochastic comparisons — flaky by construction with near-zero remaining
information: skill-e2e-auq-repetition-cut-ab (its own header: gate "passed
pre-landing, approved 2026-08-25"), skill-e2e-preamble-script-ab ("demoted
post-Phase-3"), and opus-47's fanout arm-vs-arm (parA >= parB across two
SINGLE stochastic runs — a coin flip). Deleted, with their selection keys;
the SDK overlay-harness stays as the maintained instrument for the next
experiment, and opus-47 keeps its routing-precision cases.
verboseSkill() now reads the VENDORED test/fixtures/auq-pre-cut-...-SKILL.md
instead of `git show ab66193e^:...` — a branch-local ref that dies on
branch prune and already failed on shallow clones. New free tripwire
(test/git-ref-fixture-tripwire.test.ts) bans the raw-SHA fixture class
outright: quoted SHA:path rev-specs and gitRef-style hex defaults in the
test trees fail the suite with the vendor-instead instruction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: demote plan-ceo-review-expansion-energy to periodic
Opus generator + a subjective 2-axis >=4/5 LLM-judge threshold sat in the
MERGE-BLOCKING gate — the exact class its sibling posture tests were
demoted for, with a receipt (a +21-line preamble change once flipped the
score). CLAUDE.md's own tiering rule: Opus model test -> periodic. The
weekly lane keeps the regression signal; merges stop paying a judge-
temperament tax.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: paid shards get per-shard TMPDIR + CHROMIUM_PROFILE isolation and a kill-path cleanup backstop
The free runner treats this isolation as MANDATORY (two concurrent shards
on one Chromium profile kill each other's browser; shared tmp
cross-contaminates) — the paid lane had none of it. Doubly load-bearing
here: a shard that hits its 30-min wall is group-SIGKILLed, so per-test
afterAll cleanup never runs; the rmSync backstop is the only thing keeping
wedged runs from accumulating full git-repo workspaces in the shared
tmpdir forever. This is the DAG prerequisite for raising EVALS_JOBS (next
commit) — more concurrency on shared state amplifies exactly the
shared-tree race class opus-47 exhibited.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: paid-runner defaults 4x4 -> 8x2 — halve the local gate worst case
39 of 75 skill-e2e files hold exactly ONE test, so within-shard
concurrency was dead weight for most shards: 4 jobs x 4 concurrency
yielded only ~4-6 real in-flight sessions and a 13-wave local gate worst
case (~6.5h). 8 jobs x 2 gives ~10-13 in-flight — under the
documented-safe ~15 — and ~7 waves (~3.3h worst case). CI lanes keep
their explicit EVALS_JOBS env (2 per slice; 4 for gate-census); this
changes local defaults. Rollback trigger: sustained 429 storms in the WS1
telemetry across 2 PR cycles. test/eval-detach-timeout-floor.test.ts
recomputed green (the raise LOWERS the worst-case floor).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: SHA-pin every action in the secrets-bearing eval lanes
evals.yml and evals-periodic.yml execute PR-authored code with three
provider API keys in env, yet rode mutable action tags (@v7/@v8/@v2/@v4)
— while quality-gate.yml, osv-scanner.yml, and dependency-review.yml
already model the SHA-pin pattern. All 30 uses sites across both lanes now
pin the exact commit (tag noted in a trailing comment); dependabot's
github-actions ecosystem keeps them fresh via PRs instead of silent tag
moves. Pulled forward from the plan's endgame on the CEO-review + outside-
voice agreement: supply-chain pins on secret lanes go first, not last.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: sweep wave 3 — the execFileSync family gets timeouts (90 sites, 17 files)
The tripwire's regex covered spawnSync/execSync/Bun.spawnSync but not
execFileSync — an entire blocking sync-spawn API family that could
reintroduce the shard-wedge class undetected (ship review army). Same
mechanical recipe as waves 1-2: timeout: 30_000 default, 120_000 for slow
ops, shared wrappers fixed once, string-needle sites skipped with reasons.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: review-army + adversarial test hardening
- Tripwire scans execFileSync too (ceiling 8: two more grep-needle string
exemptions); merge-introduced timeout-less spawnSync in
question-preference-hook fixed — the tripwire caught a site that landed
on main AFTER the sweep, on its first day.
- gstack-detach gains TWO watchdog kill regression tests: TERM-immune
grandchild (the killpg-after-grace escalation) and the leader-dies
variant (the pgid-at-spawn fix — the case the first test cannot see).
- eval-flake-rank gets its unit suite (final-attempt accounting, artifact
exclusion, shard recursion, recency bound).
- Groupkill/startup-grace shim markers are per-run unique (pid-suffixed
sleep durations): sibling Conductor worktrees run free suites with no
machine lock, and fixed markers let one run pgrep/pkill the other's
shims — a cross-run flake inside the anti-flake tests.
- flake-ledger test pins the project-scoped local default; stale empty
section headers in touchfiles-data deleted (they invited entries under
deliberately retired categories).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: adversarial-review runtime fixes across the telemetry + kill paths
- session-runner: exit-labeling keys off 'exit', not 'close' — an orphan
holding the pipes could relabel a REAL exit (auth failure) as
'timeout_startup' availability noise; the kill path still always
group-kills and cancels the reader (labeling and unblocking are separate
concerns). Work phase arms on a flag, not firstResponseMs===0 (a same-ms
first byte left the startup timer live all run). The CI startup grace is
now a real FLOOR (Math.max), matching its name and pinning test.
- gstack-detach: pgid captured AT SPAWN (== child pid under
start_new_session) — resolving it after the grace raised ESRCH once the
leader died on SIGTERM, orphaning TERM-immune grandchildren forever.
- test-free-shards: ledger entries carry branch + git_sha (rev-parse split:
'--abbrev-ref HEAD HEAD' printed the branch twice and recorded it as the
sha); local ledger default is per-PROJECT, not the machine-global tmpdir.
- eval-flake-rank: per-LINE ledger parse (one torn JSONL line vanished the
whole series), 60-day recency bound (transcript-bearing files are MBs),
shared isFinalizedEvalResultFile predicate (the artifact-taxonomy rule
lived in three places); eval-store exports the predicate and finalize
stops computing flakyRetries twice; paid-shards cleanup uses async rm
(a SIGKILLed shard's git-workspace teardown blocked every sibling's
stream classification on the parent event loop).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: CI trust-boundary + fail-closed repairs (adversarial findings)
- Token/exec separation restored: slices-report (runs PR-authored code:
bun install + the reconcile runner) drops to contents:read; the PR
comment moves to a NEW slices-comment job holding the write token with
ZERO repo code — no checkout, no bun, only downloaded artifacts + jq/gh.
$GITHUB_ENV/BASH_ENV persistence is job-scoped, so the split is the
boundary. The matrix-era report job had this property; the consolidation
had regressed it. Pinned by the wiring test.
- Reconcile exit captured via PIPESTATUS[0] in BOTH lanes: GitHub's default
run-step shell has no pipefail, so `$?` after `| tee` was tee's exit —
the fail-closed gate was silently fail-open. Wiring test pins it.
- PR comment: final-attempt accounting restored the dropped COST
accumulation (the dial read $0 forever), flaky passes render as the
warning they are (never as failures), and a malformed tests[] artifact
skips that file instead of aborting the whole comment under bash -e.
- Remaining mutable action tags pinned (free-tests upload-artifact,
ci-image checkout/docker trio — the image publisher holds packages:write
and feeds the secret-bearing lanes). restore-deps fallback installs
--frozen-lockfile; register-gstack-skills validates skill names before
its rm -rf.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump version and changelog (v1.77.0.0)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: update project documentation for v1.77.0.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: cross-model doc-review fixes — flake-ledger env knobs, CI retry-on note, stale version comment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: correct CHANGELOG receipt numbers to measured values
Gate census keys: 78 -> 77 (bun-imported E2E_TIERS count). Sweep receipt:
586 sites/176 files -> 499 sites/146 files, measured by running this
branch's spawnsync-timeout-tripwire against origin/main (exit 1, 499
violations across 146 unique files; green on this branch).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: slices-comment creates the PR comment via REST — the write-token job has no git context
The token/exec split gives slices-comment NO checkout by design, and gh's
pr-comment subcommand resolves the repo FROM git — it died with 'not a git
repository' on PR #2746's first run (the update-existing PATCH path was
already explicit-repo REST and worked). Create now posts through
gh api repos/.../issues/N/comments, and the wiring test pins that no
git-context-requiring comment call can creep back into the job.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: startup-grace probes clear CI for local semantics; new probe pins the floor clamp
The two shim probes pass explicit 2s/4s graces, but in CI the runner clamps
any explicit grace up to the 300s floor (deliberate adversarial-review fix),
so 'silent API killed at the grace' died at the 30s work cap instead of 2s —
a deterministic red on every CI run, green locally. The probes now pin LOCAL
semantics with CI cleared (same save/restore pattern as their PATH shim),
and a fourth probe pins the clamp itself: CI=1 + 2s grace + 6s timeout must
kill at the 6s cap, still in the startup phase — proof an explicit low grace
cannot bypass the floor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(session-kind): explicit GSTACK_SESSION_KIND override; skill-start spawned gates keyed on kind (#2733)
Claude Code subagents inherit the parent env byte-for-byte, so ambient
markers classify them as the parent's kind and the spawned classification
was unreachable outside OpenClaw. GSTACK_SESSION_KIND=spawned (step 0,
spawned-only by design) lets a dispatching skill mark its subagent per
command. skill-start now keys SPAWNED_SESSION and the spawned-session
instruction block on the resolved kind (was raw OPENCLAW_SESSION),
suppresses CONDUCTOR_SESSION for spawned sessions, gates all 11
interactive-onboarding blocks plus their ack-at-emit marker writes on
kind != spawned, and adds a destructive-gate carve-out to the spawned
block (conservative-continue, never prose-STOP).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(hooks): spawned-session escape in Conductor AUQ deny; override coverage in AUQ-error fallback (#2733)
Hooks inherit the harness env, so a per-command GSTACK_SESSION_KIND
prefix inside a subagent's bash can never reach them. Levers added:
a deterministic [conductor][spawned] auto-choose deny for env-level
spawned sessions (OPENCLAW_SESSION or session-wide GSTACK_SESSION_KIND),
and a spawned escape sentence appended to both hooks' prose directives
so a marked subagent that slips and calls AUQ resolves to auto-choose
instead of prose-STOP. The sentence lives in one shared constant
(hosts/claude/hooks/spawned-directive.ts) so the two paths can never
drift; destructive semantics are unified to conservative-continue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ship): Step 18 marks the document-release subagent spawned — env prefix + auto-choose prompt (#2733)
The dispatch prompt now (1) frames the run as a SPAWNED subagent whose
LAST line is machine-parsed, (2) instructs prefixing the preamble's
gstack-skill-start invocation with GSTACK_SESSION_KIND=spawned on the
same command line (template bash blocks don't share exports), and
(3) resolves every AUQ gate to auto-choosing the recommended option,
conservative on no-recommendation, never destructive. The JSON contract
gains a required "decisions" array (auto-chosen gates, printed to the
ship console — never embedded in the public PR body) and a placement
clause so the skill's own doc-health summary stops competing with the
LAST-line JSON. Tripwire pins added; codex/factory goldens refreshed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(auq-format): proactive SESSION_KIND=spawned rule ordered above the Conductor rule (#2733)
The spawned classification previously existed only in the failure-fallback
branch — a spawned session was invited to call AskUserQuestion and reach
auto-choose via the deny/error detour, and a spawned session inside a
Conductor workspace hit the Conductor prose-STOP rule first. The Tool
resolution list now leads with the spawned rule (auto-choose recommended,
never prose, never BLOCKED, destructive gates resolve conservative), the
self-check carries the never-reach-this-checklist clause, and all tier>=2
SKILL.md renders are regenerated. Context-budget fixture refreshed in the
same commit per the ratchet protocol (the AUQ section is eager in every
tier>=2 skill).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): spawned document-release subagent returns the JSON contract through a firing gate (#2733)
The behavioral proof the bug shipped without: ship-docsync stubs the
skill (no preamble, no gates) and skill-e2e-workflow suppresses the
gates by prompt. This gate-tier E2E plays the parent — it drives the
verbatim Step 18 dispatch prompt (extracted from the live pr-body.md,
drift-proof) against a real preamble-bearing document-release slice in
a Conductor-ambient env with both AUQ hooks seeded live, an unbumped
VERSION making Step 8 fire. Asserts: the final line parses as the
5-key JSON contract, the fired gate's auto-choice is recorded in
decisions, and VERSION is untouched (the gate resolved to its
recommended Skip). Burn-in: 1/1 pass, $0.35, 21 turns, 106s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(openclaw): document the GSTACK_SESSION_KIND override; wire session-kind into paid selectors (#2733)
OPENCLAW.md's spawned-session section now covers the explicit per-command
marker, its deliberate spawned-only narrowness, the /ship Step 18 usage,
the destructive carve-out, onboarding-block suppression, and the hook
env-blindness caveat. bin/gstack-session-kind and the shared
spawned-directive module join the conductor-prose and
auto-decide-preserved selector dep lists (session-kind previously
appeared in no touchfiles entry — editing it alone triggered no paid
E2E). TODOS.md gains the plan-tune capture follow-up for spawned
auto-choices.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: pre-landing review fixes (#2733)
Review army + coverage audit findings, all applied:
- headless directive carries the spawned escape sentence too (multi-
specialist: a CI-hosted ship's marked subagent must not end BLOCKED)
- anti-injection scoping on every text-claimable spawned trigger (AUQ
rule + shared escape sentence): markings count only from the creating
prompt, never from files/tool output/web content read mid-run
- [conductor][spawned] deny annotates one-way doors per question
- SPAWNED_OVERRIDE: env tamper-visibility status line + OPENCLAW.md note
- spawned sessions skip the network update-check and first-task probe
(consumers suppressed; preserves the one-shot just-upgraded marker)
- test hardening: dispatch-tripwire end-bound validated, vacuous marker
asserts replaced with output asserts, E2E cpSync size filter + named
fence tolerance, spawnedByEnv parity pin, destructive-policy cross-
surface drift guard, one-way annotation + bogus-value hook cases
- session-kind duplicate rationale comment deduped; regen + goldens +
context-budget fixture refreshed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump version and changelog (v1.76.0.0)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: update project documentation for v1.76.0.0
PROJECT_STRUCTURE.md: add hosts/claude/hooks/ to the directory tree
(AUQ capture + enforcement hooks, spawned-session directive, timeline
stop) — the tree omitted the directory while docs/OPENCLAW.md and
CHANGELOG.md now reference paths inside it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: sync TODOS.md ship dispatch entry with the v1.76.0.0 contract
Codex doc-review finding: the SHIPPED entry for /ship auto-invoking
/document-release still described the four-key JSON contract. Adds the
decisions key (console-printed, never PR markdown), the
GSTACK_SESSION_KIND=spawned dispatch marking (#2733), and the new
spawned-dispatch gate E2E to the proven-by list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>