* refactor(resolvers): split review.ts into MECE resolver modules (pure move)
Move every function from scripts/resolvers/review.ts, unchanged, into:
- review-dashboard.ts: review dashboard, plan-file review report
- plan-gates.ts: approval check, exit-plan-mode gate, plan-file discovery,
plan-completion audit/gate (ship + review), plan verification exec
- spec-review.ts: both spec review loops, benefits-from, anti-shortcut clause
- outside-voice-steps.ts: Codex second opinion, adversarial step, Codex plan
review, Codex doc review, disabled-outside record
- review-scope.ts: scope drift, cross-review dedup, shared-code reuse
review.ts is deleted; index.ts imports the new modules. gen-skill-docs
output is byte-identical for every host (--host all). Test imports and
source-path references are re-pointed; the two source-text report/gate
tests in gen-skill-docs.test.ts become behavioral renders across every
consuming skill and host. All 46 touchfile entries that named review.ts
now name all five modules, guarded by a recorded selection golden.
* test(browse): black-box auth matrix for every server route and both surfaces
Drives buildFetchHandler fetchLocal/fetchTunnel with no token, wrong token,
root token, scoped token and the SSE cookie for all 33 routes, plus unmatched
paths and wrong methods. Denials assert today's exact status, body and content
type; allowed credentials assert the handler was reached. Written against the
unchanged if-chain server so the W3 route-table refactor must keep it green.
* refactor(shard-engine): move scripts/test-strict-output.ts to scripts/lib/shard-engine.ts
The shared shard engine grows from the existing strict-output module
(runShardChild, killProcessGroup, signal forwarding, strict classifier).
scripts/test-strict-output.ts stays as a re-export so existing importers,
mock.module paths and the strict-output/run-shard-child tests are unchanged.
The engine inherits the global touchfile entry; the free runner's CLI-routing
fixture copies the new module.
* refactor(resolvers): decompose the three >150-line review resolvers (output-neutral)
Split generateAdversarialStep, generateCodexPlanReview and
generatePlanCompletionAuditInner into per-section helpers whose template
literals are copied verbatim, so every function in the new modules is at
or under 150 lines. gen-skill-docs output is byte-identical for every host
(--host all, compared against 96764e80 with a fixed --link-root).
* refactor(resolvers): one outside-voice failure policy (deliberate prose unification)
outsideVoiceFailurePolicy(ctx, opts) in outside-voice.ts now renders the
auth / timeout / empty-response bullets for all four call sites that
hand-typed them (Codex second opinion, adversarial step, Codex plan
review, design outside voices). Options are explicit per site
(timeoutMinutes, onTimeout, stderrOnEmpty, fallback, escape) with no
defaults.
Deliberate generated-prose changes (every host):
- office-hours: 'Fall back to <native> subagent.' becomes
'Fall back to the <native> subagent below.'
- plan-devex-review: the plain 'Auth failure (stderr contains ...)'
bullets become the canonical bold bullets; auth also triggers on
'API key'; 'auth failed' becomes 'authentication failed'.
- review/ship adversarial: 'exceeded 9 minutes and was terminated'
becomes 'timed out after 9 minutes and was terminated'; the timeout
is still MISSING COVERAGE.
- design outside voices: unchanged.
Adds ratchet (d) (test/outside-voice-failure-policy.test.ts) with a
reasoned allowlist for /codex's own CLI errors, the MISSING COVERAGE
retention test, refreshed codex/factory ship goldens, and outside-voice.ts
in every touchfile entry of review.ts and design.ts (selection golden
extended).
* test(pty): fake PTY session driver with an injectable clock through the runner launch seam
The three plan-skill runners take an optional PtyDriver (launch, now,
monotonic, sleep); omitted, they use the real launcher and clocks exactly as
before. test/helpers/pty/fake-session.ts feeds scripted frames through that
seam, and claude-pty-runner.runners.unit.test.ts runs observation, counting
and floor for success, deadline timeout, permission prompt and plan-ready
outcomes with no CLI or real timers. These cases must stay green unchanged
through the W4 split and the runPtySession extraction.
Touchfiles: every entry that lists claude-pty-runner.ts or pty-screen.ts now
also lists test/helpers/pty/**.
* refactor(shard-engine): run both lanes on the shared engine; lane policy injected
Engine (scripts/lib/shard-engine.ts) gains the W2 primitives: per-shard
tmp/Chromium sandbox + async cleanup backstop, log-path allocation and
full-stream log capture, one duration-seed reader/writer with a lane
predicate, LanePolicy (seed predicate + zero-execution verdict),
strictShardStatus, and the shared CLI flag loop. runShardChild takes an
optional companion (signal/settle) and waits a bounded 250ms to reap a
wall-killed child.
Free lane stops spawning shards itself: runFreeShard uses runShardChild
with trackShardBrowser as the companion (win32 path unchanged: no process
group, no negative-pid kill). Its sync state-dir removal stays lane policy.
Paid lane uses the sandbox, log, seed, verdict and flag primitives; the
hollow-shard guard applies PAID_LANE_POLICY. Lane outcomes are unchanged
(free keeps >= 0 seeds and file-count zero-exec rule; paid keeps > 0 seeds,
warning under selection and passed-empty under EVALS_ALL).
paid-free-boundary's closure assertion now names the engine module, where
the strict classifier lives.
* test(shard-engine): engine unit tests, fixture-corpus equivalence, per-lane CLI parity
- test/shard-engine.test.ts: failing/unhandled/module-load output fails both
lanes, per-lane zero-execution and seed rules, whole-group kill on a wall
timeout (both lanes), mocked-win32 path with no negative-pid kill,
companion settle order, log capture, sandbox isolation, flag loop.
- test/shard-engine-equivalence.test.ts + test/fixtures/shard-equivalence:
seven outcome fixtures plus one real shard, run through both lanes and
compared with classifications recorded from the base runners (96764e80).
- test/shard-cli-parity.test.ts + test/fixtures/shard-cli-parity: flag set,
defaults, validation errors and the Unknown argument error per lane match
the base runners.
* refactor(shard-engine): decompose runFreeShard and runPaidShard to <= 150 lines
Output-neutral extraction under the fixture-corpus equivalence and runner
tests: captureFreeStream, explainFreeVerdict and logFreeRecovery (free);
paidShardCommand, settleShardSpool, settleBootstrapRetention and
printLogTail (paid). The bootstrap scope-creation block that
bootstrap-retention.test.ts evaluates stays verbatim.
* refactor(pty): split claude-pty-runner.ts into test/helpers/pty/* behind a barrel
Pure move: every line of the former 5,047-line runner lands verbatim in one
module (four private helpers gain `export` for cross-module use):
binary, screen (absorbs test/helpers/pty-screen.ts, which now re-exports it),
launch, session (PtyDriver), judge, classify, auq, plan-native, boundaries,
runners/{observation,counting,floor}. claude-pty-runner.ts re-exports the
original public surface by name; pty/ modules import siblings directly.
Tests that read the runner's source text:
- rewritten as behavioral: the unit test's model-pin tripwire (fake CLI argv:
fallback chain, --model before extraArgs, hermetic --strict-mcp-config),
pty-skill-seeding-wiring (runners through the fake driver; launcher through
a fake CLI reporting CLAUDE_CONFIG_DIR). The "three wrappers forward model"
grep is replaced by the runners' fake-driver launch assertions.
- pty-screen-session / pty-screen-supervision: stop copying runner source;
they mock.module the real pty/screen.ts (and the fixture cleanup) instead.
- re-pointed to the owning module (they execute a sliced runner body with
injected boundaries; no seam exists for those boundaries yet):
eng-seeded-completion-ai, plan-floor-permission, plan-create-prepublication,
plan-count-completion; hermetic-wiring's source guard now reads pty/launch.ts
and scans every pty/ module for raw process.env spreads.
- plan-count-timeout and pty-output-wake mock the viewport at pty/screen.ts.
* test(ratchet-c): enforcing module/function size ratchet and moved-code touchfile coverage
Ratchet (c) ships enforcing: test/helpers/module-size.ts counts file and
top-level function lengths by brace matching over masked source (strings,
comments, regex literals and template text masked; ${} expressions kept),
covering function declarations, arrow functions assigned to consts and
route-table handler properties, with no parser dependency. Its self-test
uses template literals and code-fence braces copied from
scripts/resolvers/review.ts and design.ts. test/fixtures/module-size-ratchet.json
binds scripts/lib/shard-engine.ts (<= 800 lines, <= 150 per function) and
records the residual runner sizes (free 2352, paid 1921) as non-growth caps;
allowlist entries are keyed on file plus matched text and need a reason.
Failure output lists file:line, the rule, Fix: and the allowlist path.
touchfiles.test.ts gains the moved-code superset check over
test/fixtures/touchfile-move-goldens/ (W2 golden recorded at 96764e80:
test-strict-output.ts and test-paid-shards.ts global, test-free-shards.ts none).
* refactor(browse): declared route table replaces the buildFetchHandler if-chain
The ~1,300-line if-chain in buildFetchHandler becomes a route table:
each entry declares method, path, auth kind and surfaces, and one auth
gate in browse/src/routes/table.ts returns the per-kind denial (root-bearer,
scoped, root-or-sse-cookie: 401 Unauthorized; root-token: 403 Root token
required; extension-origin: 403 Forbidden). Unmatched requests take the
declared fallthrough (root-bearer check, then plain-text 404). Handlers move
to browse/src/routes/{core,pairing,pty,tokens,tunnel,activity,commands,files,
inspector}.ts and receive a RouteContext with auth checks as functions
instead of closing over factory locals. Dispatch order is unchanged:
tunnel filter, beforeRoute overlay, gate, handler. TUNNEL_PATHS stays a
literal in server.ts.
Behavior-preserving: the black-box auth matrix from the previous commit
passes unchanged. /memory and /inspector/events are declared root-bearer
because the blanket check always ran before their SSE-cookie branch.
Source-text route tests are rewritten as behavioral tests through
buildFetchHandler or a route's real handler with a stub RouteContext
(browse/test/route-test-harness.ts). Checks with no runtime seam are
re-pointed to the route modules: Surface type, /inspector/events SSE
helper, sanitizeReplacer imports, /pty-inject-scan sidecar-client import,
and the ngrok config lookup and startTunnel wiring that stay in server.ts.
* test(browse): stubbed-handler auth matrix and route inventory for the route table
Every ROUTES entry runs through the real dispatcher and gate with stub
handlers on each declared surface and six credentials; denials assert the
exact status and body each auth kind returned at 96764e8, admitted
credentials assert the handler ran (with the gate's TokenInfo for scoped
routes). Also pins the reviewed route inventory (method, path, auth kind,
surfaces), that every entry declares auth and surfaces, that the table's
tunnel paths equal the TUNNEL_PATHS literal with GET /connect admitted, the
unmatched fallthrough, and that the root token is rejected on every tunnel
route through buildFetchHandler.
* test(browse): ratchet (b) keeps route dispatch inside the route table
Scans browse/src/server.ts and browse/src/routes/*.ts for pathname
comparisons; only the table matcher and the tunnel-surface filter are
allowed, listed with reasons in browse/test/fixtures/route-dispatch-allowlist.json
(keyed on file plus line text). Also checks every entry declares auth and
surfaces and that gstack registers no beforeRoute overlay itself. Self-tests
plant a violation and assert the file:line, Fix: and allowlist path in the
message, that a shifted line stays allowlisted, and that a reasonless entry
is rejected.
* test: touchfile superset check for modules moved out of browse/src/server.ts
Records the paid evals selected by touching browse/src/server.ts at 96764e80
(17 E2E, 1 LLM judge) and asserts every browse/src/routes/*.ts module selects
a superset. The test reads every golden in test/fixtures/moved-module-selection/
so other moved-code goldens can sit beside it.
* test(shard-engine): give non-timeout corpus fixtures CI headroom; keep the POSIX golden off the Windows lane
Only the wall-timeout fixture keeps a 3s wall; the rest get 60s so a loaded
host cannot turn a pass into a timeout. Base and branch runners still agree
on every classification under the new walls. The Windows exclusion entry
moves the free runner's ratchet (c) residual cap to 2356 lines.
* refactor(pty): one runPtySession loop drives observation, counting and floor
test/helpers/pty/session.ts owns launch -> start -> (poll -> tick)* ->
timeout and the failure contract the three runners each hand-rolled: the
run's own error wins over capture and close errors, close always runs, owned
fixture cleanup runs last (also when launch fails). Each runner now supplies a
PtySessionPlan: its boot/command step, poll cadence (2s observation/floor
sleep; counting's output wake + 250ms coalesce), tick policy (permission
handling, native identity, terminal rules stay per runner because they differ)
and capture hooks. The runner bodies are decomposed into top-level steps so no
function exceeds 150 lines; behavior is unchanged and the fake-driver cases
from the first W4 commit pass unmodified.
The counting capture step and the native completion-summary predicate are now
named functions (countingCapture, isNativeCompletionSummary), so
plan-create-prepublication and plan-count-completion call them directly
instead of executing sliced source. The two harnesses that still execute a
sliced runner body with injected boundaries (eng-seeded-completion-ai,
plan-floor-permission) pass the PtyDriver seam instead of overriding
Date/Bun.sleep.
* test(ratchet-c): register route modules, review resolver modules and server.ts residual cap
* refactor(pty): decompose launchClaudePty and engNumberedFindingAUQ under 150 lines
launchClaudePty (349 lines) becomes launch preparation (args, hermetic
child env, owned state roots), recorder creation, spawn, the trust-dialog
watcher, close, and the session handle over one PtyProcess state object. The
failure order is unchanged: abort the viewport, dispose any recorders created
so far, dispose the viewport, rethrow. The --model / --strict-mcp-config
ordering and seedSkills wiring stay pinned by the behavioral fake-CLI tests.
engNumberedFindingAUQ (345 lines) keeps its guards and dispatch; each
self-contained issue family (declared cache, library retry hooks, cache
owner, injected singleton, shared writers, injected export) moves verbatim
into its own function. Every pty/ module is now <= 800 lines and every
top-level function <= 150 lines.
* test(pty): split claude-pty-runner.unit.test.ts along the pty/ module seams
The 188 unit tests move verbatim into claude-pty-runner.{screen,classify,
auq,launch,plan-native,boundaries}.unit.test.ts (test names unchanged; each
file imports only what it uses from the barrel). The five files that no longer
read a SKILL.md template join the test-of-test ratchet baseline with a reason.
* test(touchfiles): moved PTY modules keep their paid-eval selection
test/fixtures/touchfile-selection/w4-pty.json records, at 96764e8, the paid
evals selected by touching test/helpers/claude-pty-runner.ts (20) and
test/helpers/pty-screen.ts (20). touchfiles.test.ts now asserts every .ts file
under test/helpers/pty/ (and pty/screen.ts for both sources) selects a
superset, reading every golden in that directory so later moves can add one;
a planted-violation case pins the report and its Fix line.
* fix(browse): unexchanged pair setup keys no longer authenticate bearer requests
validateToken accepted a gsk_setup_ key as a bearer on /command, /batch and
/file (found while building the W3 auth matrix). A setup key now only
authenticates the /connect exchange.
* W1: one state-root owner (lib/state-root.ts + bin/gstack-state-root.sh), gstack-paths --explain and fail-stop, parity tests
* W1: guarded migration of every executable state-root site; uninstall deletes only ~/.gstack
Bins, careful/freeze hooks, setup, upgrade migrations, browse/src, design,
ios-qa daemon, lib and scripts resolve the state root through
bin/gstack-state-root.sh (bash) or lib/state-root.ts (TS). Bins source the
twin and stop with a reinstall message when it is missing; hooks source it
and never spawn gstack-paths. browse/src/config.ts and lib/cso/state.ts
delegate to resolveStateRoot. Analytics writers and readers move together
so the usage log stays one file. gstack-uninstall deletes state only at
~/.gstack, refuses (exit 2) when it resolves to /, $HOME or an ancestor,
the checkout or the git root, and leaves any other resolved root in place
with the removal command. Fixtures that copy single bins now copy the twin.
* W1: privacy keys and trust-policy deny tiers merge across state roots; gstack-config reporting; test hermeticity
readConfigKey / gstack_read_config_key return the most restrictive
telemetry, memorable_recall, codex_reviews and update_check across the
resolved root and ~/.gstack; other keys read the resolved root only.
gstack-config set reports an overriding root with the exact override
command, list shows the winning root and a root-variable disagreement line.
gstack-gbrain-repo-policy get merges deny/read-only tiers. gstack-egress
reads through readConfigKey. test-setup.ts strips inherited
GSTACK_STATE_ROOT/GSTACK_STATE_DIR and redirects the legacy root.
* W1: shared hook logging helper (hosts/claude/hooks/hook-log.ts)
One hook-errors.log writer: root from resolveStateRoot, 0600 on every
append, opt-in rate limit used only by memorable-user-prompt. The five
hooks route through it.
* W1: docs/state-root.md and README troubleshooting pointer
Precedence table, a real --explain example, the move-your-state recipe,
merged privacy keys, the uninstall rule, the resolver-failure fix, and the
plugin-mode note (evidence gate: no official plugin distribution).
* W1b: template and resolver prose resolve state through guarded gstack-paths; ratchet (a)
Every gstack-paths eval in templates and resolvers carries the fail-stop
guard; executable ~/.gstack paths in bash blocks (context recovery preamble,
eureka log, analytics, project artifacts, upgrade snooze, setup-gbrain lock,
retro snapshots, ship consent marker) use $GSTACK_STATE_ROOT, and the writer
prose that pairs with them points at the printed PROJECT_DIR / RETRO_FILE.
ship drops export GSTACK_STATE_ROOT. SKILL.md regenerated (claude + codex),
ship goldens re-pinned, parity and context-budget caps raised to the measured
sizes with notes. test/state-root-ratchet.test.ts enforces the rule with a
reasoned allowlist; W1 touchfile entries plus a superset golden.
* refactor: apply W1 state-root edits in W2/W3/W5-owned files; one moved-code touchfile golden for all workstreams
* test: fold the moved-code touchfile golden into touchfiles.test.ts; fix integration fixture closure and caps
* v1.91.11.0: CHANGELOG, TODOS, docs and conventions for the refactor wave
* test: re-measure plan-ceo/design-consultation caps and ship goldens after the guarded plan-discovery and spec-review blocks; add the state-root twin to the workflow-boundaries fixture
* fix(windows): migrations resolve their directory with either path separator; state-root parity compares under the HOME Git Bash actually sees
* fix(review,ship): state plan-check timing after smoke expiry and test_stub Skip semantics (review workflow judge clarity)
* test(qa-eval): webhook fix eval asks for the fix loop's post-repair probes; eight-scenario coverage stays in the report-only case and the harness recheck
* test(qa-eval): re-pin the webhook prompt contract to the fix-loop stage; R29 coverage omissions stay bound by the report-only case
* fix(review,ship): plan checks publish a checkpoint before each probe; only the smoke expiry stop is skipped
* fix(qa): carry #2999's checkpoint receipt link, report-template line and full-revision placeholder (identical hunks)
* test(qa-callers): disable git auto maintenance in the caller fixture
Git 2.47+ runs auto maintenance detached after commit; on the CI runner's git
2.55 it rewrote .git/objects fan-out directories while the write observer was
running, which surfaced as unauthorized mutations. Same gc.auto=0 /
maintenance.auto=false guard the shared-libs fixture already uses.
* test(plan-mode-no-op): require prose evidence for the prose-fallback members so a spinner-frame judge verdict cannot end the run as asked
* test(ship-docsync): carry #2999's seeded-attempt docsync harness (identical files)
The doc-sync fault cases replayed attempt 1 before reaching their gate and ran
out of their 285s budget. The fixture now seeds attempt 1 and the parent starts
at the gate under test. Taken byte-identical from origin/capy/audit-fix-wave
(fb526898, e6ac813d, 6ce10ff7, d0c53577, 77cce3be). Local: stale-before,
recovery and late-result 6/6 PASS (97-164s); the whole file 12/12 PASS.
* feat: add optional Memorable workflow memory
(cherry picked from commit 6bd5d477b2)
* fix: refuse the second registration, and say what leaves the machine
Two things the first cut got wrong.
Memorable's own installer registers the same UserPromptSubmit hook, under its
own name and outside gstack's table. `memorable start`, `memorable setup` and
`memorable install-hooks` all do it, and that is the documented way to install
the CLI, so on most machines it is already there before gstack is asked.
Registering ours beside it ran the same command twice on every prompt: context
injected twice, and the session captured twice against the user's own
extraction allowance. `enable` now looks for it and refuses, naming the entry
and the file it lives in; `status` says who registered it rather than reporting
none. Matched on the command rather than on a tag, for the reason the hook
table already gives: Claude Code rewrites settings and private tags do not
survive it.
The removal instruction says to delete the entry by hand because Memorable has
no command that removes its own hook. `uninstall-hooks` is not a command in
0.5.18; it answers "unknown command".
The README said "Memorable, not gstack, owns the captured data and any network
access", which answers the question by pointing away from it. It now carries a
per-command table of exactly what leaves the machine, in the shape the adopted
gbrain section uses, and it is explicit that the hook makes no network call of
its own, that every row is the third-party CLI acting under its own consent,
and that `gstack-egress` will therefore not show any of it. Under it, the split
between what gstack pin-tests (the gating and the wiring) and what is
Memorable's claim (storage, sending, and what disable and forget erase).
The CHANGELOG entry is removed. This file has never carried an [Unreleased]
heading; every entry is a version and a date, written at release. The text is
in the pull request for whoever cuts the next one.
Three tests added: enable refuses and touches neither consent nor settings when
Memorable already holds the hook, status names that registration, and a foreign
UserPromptSubmit hook is not mistaken for Memorable's.
(cherry picked from commit e0899afa8c)
* docs: a guide for the Memorable bridge, in the gbrain-sync shape
README carries the section and the egress table; this is the page it links to
for anyone who wants the whole thing. Same shape as docs/gbrain-sync.md, which
is the closest thing in the repo: an optional integration whose interesting
questions are all about what leaves the machine and how to turn it off.
What it covers that the README cannot at that length: that the hook sees every
Claude Code prompt rather than only the ones a skill produced; that capture is
a separate consent from this bridge, so turning the bridge off does not turn
capture off; what to do when Memorable has already registered the hook itself,
which is the common case because its own installer does it; and why the hook
has no loud failure path.
The egress table is repeated here rather than linked, because the sentence it
is answering ("what does this send") is the one somebody arrives on this page
already asking.
Co-authored-by: Advaiyt Sane <advaiyt.sane@gmail.com>
Co-authored-by: Nikhil Krishnaswamy <krishnaswamynikhil@gmail.com>
(cherry picked from commit 5c108cc0f7)
* feat(settings-hook): identity-aware remove-source + read-only list-items
remove-source used to inspect only entries still carrying the
_gstack_source tag. Claude Code strips that tag when it rewrites
settings.json, so an off switch built on remove-source alone silently
no-oped on exactly the entries it was written for. Removal is now driven
by KNOWN_HOOKS identity for the requested source (tagged or not), keeps
the tagged-single-item legacy-stray rule, never touches another source's
items, and leaves entries with nothing of ours byte-identical.
list-items is the read-only view of the same identity table: one JSON
string literal per matching hook command, filters (--owned-by,
--command-regex as a JavaScript RegExp) applied inside the JS, empty
stdout for no match, and the mutating verbs' exit codes (1 usage, 3
unparseable settings, 4 unexpected shape) so callers can decide
mutations from its output without parsing raw command strings.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(config): memorable_recall consent key (on|off, default off, reject-and-preserve)
The gstack-side gate for the Memorable UserPromptSubmit bridge. `on` lets
a Claude Code hook hand every prompt to a third-party binary, so the key
follows the codex_reviews rule: an invalid value is rejected and the
stored value kept, never coerced in either direction. Registered in all
four places gstack-config keeps in sync (annotated header, DEFAULTS
table, the set validator, and both enumeration loops). Memorable's own
capture consent (`memorable enable`) is a separate thing gstack never
sets.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(hooks): memorable-user-prompt-hook.ts — consent gate, deny veto, HIGH-tier pre-scan, fail-closed receipt, trust envelope; runExternal in spawn-bin
The PR's hook exec'd the vendor binary with the full environment and
passed its stdout to Claude verbatim. It is now the house pattern: a
fail-open bash shim over a .ts twin that (1) gates on the memorable_recall
consent key, (2) skips repos whose trust policy is deny or read-only,
(3) scans the prompt (raw bytes and decoded string leaves) and refuses to
hand over a HIGH-tier credential shape, (4) writes a fail-closed egress
receipt naming the local executable it ran, (5) spawns the vendor in its
own process group with an allowlisted environment and group-kills it on
timeout, (6) accepts only a string additionalContext back, caps it at
8 KiB on a UTF-8 boundary and wraps it in the trust envelope, and (7)
records an `output-written` outcome after the stdout write completes.
One deadline clock (4.5 s) undercuts Claude Code's 5 s kill and bounds
both ledger writes through the new lockBudgetMs option on
writeReceipt/writeOutcome (default unchanged).
spawn-bin gains runExternal for external executables (detached group,
stderr drained, stdin EPIPE handled, stdout capped, win32 refused).
The wiring test pins the sink fail-closed and sweeps hosts/.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(gstack-memorable): canonical hook path, no vendor consent, --timeout 5, identity-based status, verified disable, lifecycle lock
enable used to bake the hook path from whatever tree the CLI ran in and
to run the vendor's own `memorable enable` (its consent for storing AND
uploading session traces) before registering anything. It now resolves
the canonical install like setup does and refuses when that install does
not carry this bridge (version and hook-twin check), registers through
the canonical hook manager with --timeout 5, records gstack's own consent
in memorable_recall, never executes the vendor, and restores the captured
prior state if consent cannot be recorded. disable flips the gate first,
removes the entry by identity (tag or no tag), verifies both states and
reports partial failure instead of a blended success. status reads only:
resolution path, gate, registration by identity (gstack / vendor-own /
both / unknown), mismatch lines, receipt count, recent hook errors. enable
and disable serialise under a lock with stale takeover. Windows is
refused (TODOS.md D21). Exit codes mirror the hook manager (3/4/5).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(setup): --no-team sweep keeps the opt-in gstack-memorable hook
`./setup --no-team` finishes its teardown with `prune-stale --all`, which
removes every KNOWN_HOOKS item. The Memorable bridge hook is a
user-registered opt-in unrelated to team mode, exactly like verify-gate,
so it joins the sweep exclusion list. The verify-gate pin now accepts the
comma-extended list; a schema-aware case proves the exclusion keeps both
opt-ins (tagged or tag-stripped) while the uninstall sweep still removes
them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(uninstall): named gstack-memorable arm, vendor-consent notice, honest kept config
The identity sweep already removed the Memorable bridge hook as an
unnamed stray. It now has a named arm like every other source, so the
summary says what went, and says plainly that Memorable's own consent
(if the user granted it) is theirs to revoke. Under --keep-state the kept
config is set memorable_recall=off so it never claims a hook that is
gone. The canonical-paths pins cover the sixth KNOWN_HOOKS row and the
new uninstall source.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(egress): memorable-recall row in gstack-egress grants
`gstack-egress grants` promises every standing consent in force with the
command that revokes it. The Memorable bridge's memorable_recall key is
one, so it gets a row: off by default, granted only when
`gstack-memorable enable` set it, revoked by `gstack-memorable disable`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(memorable): accurate bridge guide; README row, Docs table, privacy pointer; PROJECT_STRUCTURE
The README section becomes one row in the Standalone binaries table (the
shape every other binary uses) plus a Docs-table row and one Privacy
bullet saying that optional third-party bridges are off by default and
receipted. The guide now separates the two consents, says what gstack
hands to the vendor binary and what the receipt can and cannot attest,
attributes every statement about the vendor's network activity to the
vendor, describes the hook manager accurately (identity via KNOWN_HOOKS,
list-items vs list-sources, rollback is whole-file), states the Claude
Code hot-reload behaviour, adds a troubleshooting runbook, and keeps the
contributors' credit in the CHANGELOG idiom.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* chore(todos): Memorable bridge follow-ups
Filed from the CEO and eng reviews of the bridge fix-up: the generic
third-party hook seam, Windows support (deferred whole), the envelope
kind parameter, a vendor payload-minimization contract, a latency and
timeout revisit, resolver and canonical-root consolidation, a
non-interactive MEDIUM-tier redaction policy for hooks, and adopting
list-items at setup's plan-tune check.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(settings-hook): list-items --owned-by with --command-regex intersects
When both filters are given, an item must satisfy both: owned by the
requested source AND matching the pattern. Before, the regex branch
skipped every owned row, so the combination could never match.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(gstack-memorable): compat_check reads list-items output before grepping
Under pipefail, piping the probe straight into grep -q let a non-zero
probe exit mask the match, so a hook manager without list-items was
sometimes reported as compatible. Capture the output, then grep.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: coverage for the memorable bridge (remove-source regression for every KNOWN_HOOKS source)
- settings-hook: identity removal pinned for each source in KNOWN_HOOKS;
list-items unknown flag and combined --owned-by/--command-regex
- gstack-memorable: enable/disable failure paths (lock give-up exit 5
with the test-only lock timeout override, consent-write failures
guarded by canRevokeWrites, canonical-version mismatch, no-bun status)
- hook: non-object JSON, missing cwd, non-ASCII bytes, held-open stdin,
shim without bun, stripControl, resolveVendor, runExternal ENOENT
- egress-receipt: lockBudgetMs 0 and writeOutcome on garbage input
- uninstall: no memorable entry present reports nothing removed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(memorable): gbrain backend note and the settings-rewrite race
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* perf(redact-engine): line/col by binary search over a per-scan line index
lineColAt walked the input from offset 0 for every finding, so a match-dense
input (a pasted log full of emails and IPs) cost O(findings x bytes): 128 KiB
took ~400 ms and 900 KiB tens of seconds. The line starts are now indexed once
per scan, on the first finding, and each finding is a binary search.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(gbrain-repo-policy-client): repoPolicyTier accepts a spawn timeout
The policy script spawn was fixed at 10 s, more than twice the memorable
hook's whole budget. Callers on their own deadline pass what they can afford;
a timeout reads as unreadable and polarity stays the caller's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(spawn-bin): runExternal resolves on the child's exit, keeps stdin errors advisory
- A vendor that exits 0 but leaves a background child holding its pipes was
held to the deadline, group-killed and reported as a timeout with its
answer dropped. 'exit' now starts a short stdout drain, then resolves with
the real exit code and kills whatever still holds the group.
- EPIPE on the child's stdin (it answered before reading a large input) is
reported as stdinError, separate from error, so a delivered answer is not
classified as a spawn failure.
- Stdio streams are destroyed and the child unref'd on resolve so a straggler
cannot pin the hook process; tail/grace/drain sizes are named constants.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hooks): memorable hook closes the review army's gaps
- Vendor failures are logged even with empty stderr (a silently hanging
vendor taxed every prompt invisibly); the stderr tail is withheld when the
redaction engine finds a credential or PII shape in it; hook-errors.log is
created 0600.
- Trust-policy veto fails closed when git cannot run or answer in time (it
read as 'no remote' before); the policy script spawn is bounded by the
hook's clock; a payload cwd that is not a directory falls back.
- Each secret scan is admitted by the deadline clock (the engine's cost grows
with match density); stdin is decoded once.
- The pre-spawn gate re-check logs a config failure instead of swallowing it;
an incomplete stdin read is named as such, not as 'not JSON'.
- Carriage returns are stripped with the other controls.
- The vendor env allowlist adds the standard proxy, TLS and XDG variables so
a vendor behind a corporate proxy or private CA still reaches its service.
- A stdin EPIPE on a delivered answer is recorded in the outcome, not treated
as a spawn error.
- Stage caps and the truncation marker are named constants; a test-only
GSTACK_MEMORABLE_TEST_BUDGET_MS can shorten (never widen) the budget.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(gstack-memorable): lock staleness from the directory mtime; honest messages
- A contender that looked between the holder's mkdir and its ts write read a
missing ts as 0, called the lock stale and reclaimed it; staleness now comes
from the lock directory's own mtime (the settings-hook idiom).
- The ensure-event failure is no longer labelled 'warning'; the consent-write
rollback message says what was actually kept; a removal that left no entry
is reported on stdout, not as an error; receipts are counted from the
filtered JSON array, not a formatting artefact; the resolution order and
lock tuning are named once.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(uninstall): memorable_recall goes off whether or not state is kept
gstack-config resolves its root through GSTACK_STATE_ROOT/GSTACK_HOME, which
can differ from the STATE_DIR uninstall removes; a full uninstall could leave
memorable_recall=on in a config that survived. Flipped only when currently on.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: pin the review-army fixes for the memorable bridge
Nonce-scoped orphan checks (the system-wide ps grep could see another
shard's sleeper); exit-with-lingering-grandchild; advisory stdin EPIPE;
withheld stderr; vendor timeout logged with empty stderr; CR stripping;
budget seam; rate-limit expiry and 0600 log; unreadable policy store fails
closed; file-as-cwd fallback; mtime-based lock staleness and the mkdir gap.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(memorable): vendor environment allowlist and stderr policy; two follow-ups
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hooks): memorable hook second-pass review fixes
- Trust-policy lookup fails closed on any git failure that is not 'no such
remote' or 'not a git repository' (a corrupt or unreadable .git/config and
dubious ownership exit 128 and used to read as 'no remote').
- pickAdditionalContext takes the first complete top-level JSON object, so a
vendor whose background helper appends a line to stdout (or prints a banner
first) does not lose its answer.
- The hook-errors.log rate limiter keys on a stable string (a vendor's
timestamped stderr no longer defeats it); the log is chmod 0600 on every
append because sibling hooks create the same file without a mode.
- Scan admission is sized by payload bytes (scan() is uninterruptible).
- The receipt payload class is a stable token; the prose moved to the docs.
- Header, constants and comments match the behaviour (silent skips vs logged
refusals; HIGH/MEDIUM withholding; STAGE_CAP_MS scope; runExternal in the
spawn-bin header; the ledger lock budget doc).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(gstack-memorable): stale-lock takeover by atomic rename; comments match behaviour
Two contenders that both saw a stale lock could both reclaim it with rm -rf;
the settings-hook idiom (mv to a private name, exactly one winner) is used
instead. The hook-manager fallback comment now says every verb falls back.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: second-pass coverage for the memorable bridge
Policy lookup outcomes (plain dir, repo without policy, corrupt .git/config
fails closed); repoPolicyTier timeoutMs; line/col at line starts, after blank
lines, CRLF and first char; tolerant first-JSON-object parsing; keyed rate
limit; uninstall never creates a config just to say off and flips consent in
a GSTACK_STATE_ROOT outside the removed state dir.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(memorable): silent skips vs logged refusals; payload class token; D21/D24 anchors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hooks): memorable hook survives host termination and brace-bearing banners
- The bash shim runs bun as a job and forwards SIGTERM/SIGINT/SIGHUP (bash
holds a signal until a foreground child exits); the .ts kills the in-flight
vendor's process group on the way out (runExternal exposes the group kill
through onSpawn), so a hook the host terminates cannot leave the vendor
running with the prompt on its stdin.
- The tolerant stdout parser tries every complete top-level object (bounded)
and takes the first carrying a string additionalContext, so a banner with
braces or quotes, or a progress object, no longer costs the answer.
- git runs with LC_ALL=C and the not-a-repository check is anchored to the
start of its message: a localized git or a repository path containing the
phrase can no longer flip the lookup.
- The rate limiter remembers up to 32 live keys, so alternating failures cost
two lines, not one per prompt.
- Unicode format characters (bidi overrides, zero-width spaces) are stripped
from vendor text at egress; the zero-width joiner stays for emoji.
- A killed child (timeout, ENOBUFS) resolves on exit without the stdout drain,
and the post-kill grace is 100 ms, so the timeout outcome fits the reserve.
- The ledger size warning, which the host discards from an exit-0 hook's
stderr, is logged where status looks.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(gstack-memorable): failed stale-lock takeover reaches the give-up; disable runs without gstack-config; status shows ledger size
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: host termination kills the vendor group; brace banners, decoys and format characters; non-reclaimable stale lock gives up; ledger line in status
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(memorable): the vendor dies with a terminated hook
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: deterministic stdin EPIPE case for runExternal (child closes stdin, stays alive)
Under parallel shard load a child that merely exits fast raced the write and
the EPIPE was not always observed; closing the read end first makes it so.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hooks): outside-model review fixes for the memorable hook and runExternal
- Nothing in the vendor's process group outlives the call: the group is
killed on every resolve, the clean 'close' path included (a helper the
vendor forked with redirected stdio ran on unsupervised before).
- A child that already exited when the deadline fires keeps its result; the
deadline only ends the drain instead of rewriting a completed exit as a
timeout.
- The decoded-leaf scan reports when its node/depth bound cut the walk short
and the hook refuses the hand-off as unscanned; object keys are scanned too.
- git for the trust-policy lookup runs without inherited GIT_* selectors
(GIT_DIR, GIT_WORK_TREE, GIT_CONFIG_*), so it inspects the session's
repository and no other; a host cancellation kills that git as well.
- An unmatched brace in a vendor banner no longer hides the answer after it.
- The stderr tail is scanned whole before it is cropped for the log, so a
credential's prefix cannot be cropped away from its secret half.
- The vendor override reads an empty GSTACK_MEMORABLE_BIN as unset, exactly
as bin/gstack-memorable does, so enable checks the binary the hook runs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(gstack-memorable): stale-lock reclaim checks the inode it judged and the owner's liveness; status reports a failed receipt query as unknown
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(uninstall): revoke memorable_recall independently of the hook manager and name a failed revocation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: group kill on clean close, walk exhaustion refused, GIT_DIR cannot bypass the deny, unmatched-brace banner, whole-tail scan, empty-override parity, uninstall revocation without the hook manager
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(memorable): the process-group guarantee and its setsid boundary
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* chore(release): v1.83.0.0 — Memorable recall bridge, identity-aware hook removal, faster redaction line/col
CHANGELOG entry for the Memorable workflow-memory bridge (opt-in, off by
default, Claude Code only), the identity-aware remove-source and read-only
list-items in the hook manager, the memorable-recall egress sink, runExternal,
lockBudgetMs, and the binary-search line/col index in the redaction engine.
No migration. Contributed by @AdvaiytSane and @NIkhil-cmd-cmd (#2831).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: stdin EPIPE observation is scheduling-dependent under shard load; pin the invariant instead
A delivered answer is never reclassified as a spawn error; when the EPIPE is
observed it is reported as stdinError. Whether it is observed before the
child's exit resolves the call is not something the test can force.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs: list the memorable-recall sink among the fail-closed egress sinks
CLAUDE.md and ARCHITECTURE.md enumerate the receipt sinks that refuse to
send when the ledger cannot be written; the Memorable bridge's per-prompt
hand-off is pinned fail-closed in test/egress-receipt-wiring.test.ts but
was missing from both lists.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* test: memorable bridge tests pass on a runner that enforces file modes; scanner-proof key fixture
- The consent-write-failure tests make the state dir read-only, which also
blocked the bridge lock directory (exit 5 before the path under test);
the locks dir is pre-created so only the consent write fails.
- The unreadable-store test leaves a 0600 directory behind (the policy
script chmods the store path); cleanup restores the search bit and the
suite's afterEach reopens directories before removing.
- The AWS-key-shaped fixture is built by concatenation, as every sibling
test does, so the CI credential gate does not read it as a live key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(gstack-memorable): name an unwritable state directory when the lock cannot be created
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: AdvaiytSane <advaiyt.sane@gmail.com>
Co-authored-by: Nikhil Krishnaswamy <krishnaswamynikhil@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix(redact): block real all-caps URL passwords, not just shape-match
urlPasswordIsPlaceholder skipped any password matching /^[A-Z][A-Z0-9_]*$/,
so a real DSN like postgres://admin:PROD2026SECRET@db-prod.internal/app slipped
the HIGH pre-push block. Replace the shape rule with an anchored, exact-match
set of doc-convention placeholder tokens (PASSWORD, PASS, CHANGEME, ...),
compared case-sensitively and never as a substring (PROD2026SECRET must not
match SECRET). The USER:PASSWORD doc convention still suppresses; real all-caps
and lowercase passwords block. Regression cases pinned both directions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browse): write self-contained .gstack/.gitignore unconditionally
ensureStateDir only appended .gstack/ to the project .gitignore when that file
already existed, skipped silently on ENOENT, and swallowed other append
failures. With BROWSE_PERSIST_STATE=1, session-state.json (live cookies +
localStorage/sessionStorage tokens) and browse-network.log / browse-audit.jsonl
(request headers) then sat git-add-able under <git-root>/.gstack/. Write a
self-contained <stateDir>/.gitignore containing "*" unconditionally, before
return, so the state dir's contents can never be committed regardless of the
project .gitignore. The project-.gitignore append is kept as redundant safety.
The no-import-side-effects guard is relaxed to allow exactly this lone
.gitignore guard file (still fails on browse.json / session-state.json / logs /
listener binds) — the guard is written eagerly by ensureStateDir at import and
is not leaked state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(browse): restore Bun.spawn exited/drain/OOM-cap contract on Node polyfill
The v1.65 fork-port squash silently dropped the `exited` promise, eager
stdout/stderr drain, and 16MB GSTACK_SPAWN_MAX_BUFFER cap that v1.64 added
(#2571), plus the five tests pinning them. On the Windows Node fallback,
`await proc.exited` then resolved to undefined immediately — cookie-import,
isBrowserRunning, and browser-skill children all read stdout before the child
produced it, a silent failure. Re-land the block (keeping v1.65's windowsHide
comment improvements) and re-add the pinning tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ios-qa): compile the private-API touch bridge out of Release builds
PR #2264 claimed DebugBridgeTouch.m (KIF-derived in-process touch synthesis
using private UIKit/IOKit symbols: _touchesEvent, IOHIDEventCreateDigitizer*,
_AXSSetAutomationEnabled) was "compiled out in Release," but the body was gated
only by TARGET_OS_IOS, so a Release iOS build carried the private symbols (App
Store rejection risk). The safety half of the fix (closed PR #2269) never
landed. Gate the body on `#if TARGET_OS_IOS && DEBUG` and add the cSettings
DEBUG define to the DebugBridgeTouch target so `#if DEBUG` is true in debug and
false in release (mirrors the Core/UI swiftSettings). A free static tripwire
pins both halves; the nm/strings symbol proof needs an iOS-SDK build and belongs
in the device/periodic tier.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(egress): state truncation/deletion of the ledger are out of scope
gstack-egress verify catches in-place edits, reordering, and mid-chain deletion
(the hash chain breaks) but not tail-truncation, whole-file re-fabrication, or
deletion — a same-user local actor who owns the ledger defeats those and verify
still exits 0. That matches the stated threat model (forensic observability, not
an exfiltration control). Document it in the header threat model and the usage
text rather than adding a count-sidecar, which would false-positive on every
legitimate rotation and barely raise the bar. Head-anchoring stays the tracked
rotation TODO in lib/egress-receipt.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ship): scope the App Store Connect key to one app and disclose it at exit
The release flow minted a non-expiring APP_MANAGER key with allAppsVisible:true
(standing authority over every app on the team) and was told never to mention
any credential to the user, so the durable key never reached their revocation
checklist. Scope the key to the app being released via the apps relationship
(allAppsVisible:false + an explicit apps association — required, since a
no-app key can see nothing and uploads fail), and disclose the key once in the
closing report with its ASC revocation path. Carve the exit disclosure as the
explicit exception to the mid-run no-credential-talk rule so the
one-authorization-moment contract still holds. Edited the .tmpl source and
regenerated the section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* harden(browse): constant-time bearer-token comparison in validateAuth
The loopback auth check compared the Authorization header with `===`, whose
byte-by-byte early exit leaks the token prefix through response timing. Use
crypto.timingSafeEqual with a length gate (the length is not secret). Behavior
is unchanged for valid/invalid tokens; auth tests unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: pin the security-property regression guards from pre-landing review
The pre-landing review found the fixes were correct but three regression guards
were missing — each pins a property whose silent revert would keep behavior
identical while reopening the hole:
- validateAuth: a static tripwire asserting crypto.timingSafeEqual + the
got.length===want.length gate + the null-header guard (a revert to `===`
keeps accept/reject green but restores the timing side-channel).
- redact: a table-driven loop over the exported URL_PASSWORD_PLACEHOLDER_WORDS
so a typo or dropped entry can't silently start blocking a doc placeholder;
plus a substring-can't-rescue-a-real-secret assertion.
- config: assert the self-contained .gitignore is written even when git already
ignores .gstack/, proving the write precedes the isIgnoredByGit early return.
- bun-polyfill: cover the 128+signal exit branch (POSIX only).
URL_PASSWORD_PLACEHOLDER_WORDS is exported so the table test can't drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump version and changelog (v1.66.2.0)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: sync egress-verify scope and layered iOS Release guard into user docs
ARCHITECTURE.md and README.md now carry the same gstack-egress verify
scope disclosure the CLI ships (edits/reordering/mid-chain deletion
detected; tail-truncation and ledger deletion out of scope for a
forensic log). docs/howto-ios-testing-with-gstack.md documents the
second Release-build guard: DebugBridgeTouch.m compiles out behind
#if TARGET_OS_IOS && DEBUG via the cSettings DEBUG define.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(ios-qa): call the DebugBridge targets SwiftPM targets, not Swift targets
DebugBridgeTouch is Objective-C (the same sentence says so); "Swift
targets" was the wrong word. Cross-model doc review catch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): describe the all-caps DSN examples without a scannable URL shape
The v1.66.2.0 entry quoted its own headline fix as three literal
postgres://user:PASSWORD@host examples — which the branch's stricter HIGH
gate now correctly flags, failing CI's quality scan on this very PR (the
local pre-push hook passed because the installed gstack still runs the old
engine). Rewrite the three mentions: the reproduce command uses a
fully-braced shell interpolation (suppressed in the diff scan by design,
expands to the real all-caps password at runtime, still exits 3 — verified),
and the table row + Fixed bullet name the password token without the URL
shape. Gate scan on the amended diff: 0 high.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci(evals): pre-seed one-time preamble markers for PTY smokes
Root cause of the documented intermittent scope-gate-question-NOT-observed
failure (test/skill-e2e-plan-mode-no-op.test.ts, also PR #2593 rounds 3/11):
on a fresh runner every one-time preamble marker is missing, so each PTY
child runs first-run feature discovery before the behavior under test, and
touching .feature-prompted-model-overlay under ~/.claude/skills/gstack/
trips Claude Code's sensitive-file permission prompt — the run stalls on
that dialog (classified outcome=asked) and the scope gate never renders.
Dev machines never reproduce it because the operator's markers exist.
Seed ~/.gstack one-time markers (.activated, .first-loop-tip-shown,
.telemetry-prompted, .proactive-prompted, .completeness-intro-seen,
.plan-tune-nudge-shown) and both .feature-prompted-* markers (via the
gstack root symlink into the checkout) in the PTY-smoke registration step,
so no first-run prompt can preempt the assertion under test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: re-version release as v1.67.1.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: restore main's dependency manifest clobbered by the merge resolution
The v1.67.0.0 merge resolved the package.json conflict wholesale --ours,
which kept this branch's version stamp but erased main's dependency work
(playwright 1.58->1.62 + its patchedDependencies entry, transformers 4.1->4.2,
cross-spawn added, puppeteer-core removed — which is also why main dropped the
basic-ftp pin test: the pinned package left the tree with it — marked/socks
bumps, adm-zip override) while bun.lock auto-merged to main's side. Every CI
job that runs `bun install --frozen-lockfile` failed on the mismatch
(check-freshness, quality, free-tests, gate, windows x2).
Take main's package.json + bun.lock verbatim, re-stamp the version through
gstack-version-bump (1.67.1.0). bun.lock is now byte-identical to main's;
frozen install verified locally; full free suite green for the branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* test(helpers): shared skill-census helper with three explicit counts
physicalSkillFiles (symlinked dirs included, root router included),
authoredSkills (realpath-deduped, router excluded), registryEntries
(what ./setup registers: unique frontmatter names + _gstack-command).
One counting authority for the hermetic seeder, context-bill ground
truth, and the catalog-budget test — connect-chrome's dir symlink and
the root router otherwise produce three subtly different hand-rolled
censuses. Ported-wave foundation (C11).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(evals): stop the harness grading itself
findPreviousRun excluded only the file being written, by name, so every
suite compared against _partial-e2e.json — the current run's own
accumulator, relabelled with the current tier just before each flush.
That is why every block read '+$0.00, +0s, Stable run, no regressions.'
This harness has never been able to detect a regression, and reassuring
output that cannot fail is worse than none. In-progress runs are now
excluded by role, and a run with nothing to compare against says NO
BASELINE instead of claiming stability.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit f3140b5245221fff7fb9411c7ec07c2ca11587b5)
* refactor(evals): shared partial-run predicate + finalized-run lookup
isPartialEval(data, filename) is the one place that decides what counts
as an in-progress accumulator (the _partial flag OR a _partial-prefixed
filename), and findLatestFinalizedRun(evalDir, tier) is the one place
that finds the newest real run — scanning the eval dir plus one level of
shards/<slug>/ subdirs, where the sharded paid runner points each
shard's collector. skill-budget-regression.test.ts's hand-rolled
findLatestRun (flag-blind: a flagged-but-renamed accumulator passed its
name check) is replaced by the shared helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit b55fcf6966366fd21a8cdc46de61aab6e1b1d100)
* feat(evals): register shipped skills for hermetic PTY children
Hermetic children get a config dir that deliberately seeds no skills —
right for children that install their own, fatal for the PTY family that
TYPES /office-hours or /plan-ceo-review: claude rejects the command as
Unknown before any model turn, so the plan-family gate smokes measure
nothing. hermeticSkillsConfigDir() is a second, opt-in config dir under
the same runRoot that mirrors ./setup's registration exactly (real dir
per registry name, SKILL.md + sections/ symlinks, frontmatter-name
resolution, _gstack-command root alias), driven by the shared
skill-census so connect-chrome's dir symlink collapses the same way
setup's idempotent overwrite does.
Ported from fork commit 03c4eca2, tree walk rewritten for the upstream
layout (top-level <skill>/SKILL.md dirs, no skills/ tree). Unit tests
are new: seed shape, census parity, symlink resolution, connect-chrome
collapse, idempotence, no-API-key seed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 93dae6107b30ce453a07c2d342b60262bba6ce0b)
* feat(evals): seedSkills opt-in for PTY slash-command tests + tripwire
Wire ClaudePtyOptions.seedSkills through launchClaudePty: when set (and
hermetic, and no per-test CLAUDE_CONFIG_DIR override), the child gets
hermeticSkillsConfigDir() so typed /skill slash commands resolve instead
of dying as Unknown command before any model turn. Opted in at the three
runPlanSkill* helpers and the four direct-launch slash-command tests
(plan-design-with-ui, plan-ceo-mode-routing, autoplan-chain,
ship-idempotency).
New static tripwire (test/pty-skill-seeding-wiring.test.ts): any test
file that sends a slash command over the PTY must route through a
runPlanSkill* helper or pass seedSkills: true — an unseeded slash-command
test spends money and measures nothing. hermetic-wiring.test.ts now
blesses the repo-tree seeding path explicitly (config dir under runRoot,
symlinks into the repo checkout, never operator ~/.claude).
The CI "Register gstack skills for PTY smoke" step keeps a keep-me note:
container cross-mount symlinks defeat the TUI scanner and HOME is not
hermeticized, so the real-file copies there must survive this change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 63c52269daaffb833b3105ea9b4b99be6df8fec7)
* refactor(evals): single shared paid-test-set module
test/helpers/paid-test-set.ts is now the one definition of which test
files are paid (the exact globs package.json's test:gate expands).
scripts/test-free-shards.ts derives its free/paid exclusion from it
instead of a private regex list, dropping the dead
browse/test/security-review-fullstack.test.ts pattern (file no longer
exists). The sharded paid runner derives its enumeration from the same
module, so a file added to one list can no longer silently miss the
other.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit a7f36479a6a1f3656452370f5883371f3cb65623)
* feat(evals): env-driven lazy eval dir + shard-aware store and tooling
Importing eval-store no longer spawns the gstack-slug subprocess: the
module-level DEFAULT_EVAL_DIR constant is now a memoized defaultEvalDir()
resolved at collector construction. Resolution order: explicit
constructor arg, then GSTACK_EVAL_DIR, then slug detection — so the
sharded paid runner can point each shard child at its own
<evalDir>/shards/<slug>/ dir with plain env, no --preload.
Runs collected under a shards/ subdir record their slug in the eval
JSON (EvalResult.shard). findPreviousRun scans one shards/<slug>/ level
and prefers same-slug priors, so each shard baselines against its own
history instead of whichever shard flushed last. eval:list,
eval:summary, and eval:compare enumerate the same one level of shard
subdirs; eval:compare's no-arg mode also stops picking an in-progress
accumulator as the after-run.
eval-watch stays flat (documented follow-up): it tails a single dir for
live progress and gains nothing from per-shard baselines until the
runner emits a merged stream.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit e1f53f7d9c7fe6b65877d843f2e25bd2e2d12ffd)
* feat(evals): sharded paid tier runner
scripts/test-paid-shards.ts runs the gate/periodic tier one Bun process
per test file, with an EXTERNAL wall-clock timeout that SIGKILLs the
shard's detached process group and an aggregate that distinguishes
passed / failed / timed-out / never-started — partial execution can no
longer read as a pass. Bun's native --shard/--isolate covers none of
this: no process-group kill (hung claude/codex PTY grandchildren
survive in-process isolation), no never-started taxonomy, no per-shard
env. Each shard child gets GSTACK_EVAL_DIR=<evalDir>/shards/<slug>/
(slug = test filename sans extension, stable across runs) so shard
baselines compare against their own prior runs.
Output classification lives in scripts/test-strict-output.ts (strict
exit-code derivation, incremental fail-line classifier, child signal
forwarding) so the runner and any future strict bun-test wrapper share
one implementation. Enumeration derives from the shared paid-test-set
module; tier exclusion fires only on an explicit whole-file
EVALS_TIER === '<other>' guard.
package.json gains test:gate:sharded / test:periodic:sharded, and
eval:bg:gate / eval:bg:periodic now run the sharded scripts with detach
timeouts sized to the worst case (gate: 49 shards x 30min / 4 jobs ~
6.2h -> 25200s; periodic: 59 -> 28800s).
test/paid-shards.test.ts pins enumeration, tier classification, and the
kill-and-continue property with a real busy-loop shard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5e76bd5931836257f896cedfe4e93912cb759c70)
* feat(security): hash-chained egress receipt ledger (core)
Port lib/egress-receipt from the v2 fork as TypeScript: writeReceipt
(sync, fail-closed via typed EGRESS_RECEIPT_FAILED), best-effort
writeOutcome, readLedger/listReceipts/verifyLedger, GSTACK_HOME ->
GSTACK_STATE_DIR -> ~/.gstack resolution, 0600 ledger under a 0700
security dir, and an mkdir spin lock (2.5s budget) with documented
>10s-mtime stale-lock reclaim.
Changes vs the fork:
- lastRawLine tail-reads the final 4KB instead of loading the whole
ledger, so appends stay O(1) as the file grows.
- WARN-at-size: past 25MB writeReceipt emits one self-explanatory
stderr warning per process (what the ledger is, how to inspect it,
rotation TODO); verifyLedger gains a sizeWarning field. Rotation
TODO carries the chain-genesis sketch (new generation's first record
embeds the prior file's tail hash).
bin/gstack-egress-receipt is a bun script bridging shell callers:
write|outcome subcommands, exit 3 + EGRESS_RECEIPT_FAILED on stderr on
failure; --no-payload records sha256:null for git-class ops.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 619726a3d77d987a2e50151a5727b3faaaf5fc6a)
* chore(bin): delete dead brain-consumer/reader scripts
bin/gstack-brain-consumer and bin/gstack-brain-reader are byte-identical
dead scripts that POST the repo URL + a Bearer token to a /ingest-repo
endpoint gbrain removed (docs/gbrain-sync.md already documents the
removal in past tense). No live references remain; CHANGELOG mentions
are historical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 254ddc69fc5a0270fcc973e36b6a81766d835d2d)
* feat(security): shared shell receipt helpers
bin/gstack-egress-lib.sh (sourced library, gstack-gbrain-lib.sh
precedent) provides _receipted_curl and _receipted_git: write the
egress receipt BEFORE the send via gstack-egress-receipt, hand curl the
SAME payload file via --data-binary @file so the receipt hash matches
the wire bytes exactly, then append a best-effort outcome. Per-call
fail policy: 'closed' refuses the send (return 3, problem/cause/fix
message on stderr) and 'open' warns and proceeds. Payload temp files
are consumed immediately per call — no EXIT traps, since callers like
gstack-telemetry-sync own their own EXIT trap and a sourced trap would
clobber it.
Tested end-to-end against a local Bun.serve listener: receipt sha256
equals the sha256 of the bytes the listener received, fail-closed
refusal never touches the network and carries the problem/cause/fix
stderr shape, fail-open warns and proceeds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 6d067dce2d4c8815dec98be551763c85a3671357)
* feat(security): receipt core shell sinks
Wire the three core bash egress sinks through gstack-egress-lib.sh:
- gstack-telemetry-sync: the batch POST now writes the payload to a
temp file, receipts those exact bytes fail-closed, and hands curl the
SAME file. On refusal nothing is sent and the cursor does not
advance, so the batch stays buffered for the next run. The HTTP
status is recorded as the receipt outcome.
- gstack-update-check: fail-open receipts (warn + proceed) on the
Supabase ping POST, both VERSION curls (via a local
_receipted_version_fetch helper that skips non-network schemes), and
git ls-remote. The ping receipt is written inside the backgrounded
subshell, so it can never block the script's exit.
- gstack-brain-sync: fail-closed git-class receipts. The push receipt
is written BEFORE the commit consumes the queue, so a refused receipt
leaves the queue intact and the next run retries the whole drain
(pinned by a new queue-intact-on-refusal test, including the
problem/cause/fix refusal message shape). The retry-path fetch and
retry push carry their own fail-closed receipts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 3c60f699acceaf1c92a218874711e05fc17dca5d)
* feat(security): receipt TS module sinks + tunnel
writeReceipt (fail-closed, sha256:null — a subprocess or SDK owns the
wire bytes) before every TS-module network-bearing operation:
- bin/gstack-gbrain-sync.ts: before the gbrain code walk that ships
repo content to the user's gbrain DB (may be remote Postgres). A
refused receipt fails the stage with status refused-egress-receipt.
- bin/gstack-memory-ingest.ts: before the gbrain batch import of
transcript pages. A refused receipt returns a system_error verdict
without spawning the import.
- browse/src/server.ts: before both ngrok.forward call sites (start-up
BROWSE_TUNNEL=1 path and the /tunnel/start endpoint). A receipt
failure lands in the existing catch that tears the tunnel listener
back down and refuses the start.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5677d618a48fcd0ae2b068bf868781d90f809cb5)
* feat(design): receipted fetch for OpenAI calls
design/src/receipted-fetch.ts wraps every api.openai.com call: a
content-free egress receipt (sink design-openai, sha256 of the JSON
body — hash only, never the body) is written BEFORE the send. Polarity
is FAIL-OPEN: user-facing generation must not die because an audit log
hiccuped, so a receipt failure warns on stderr and the call proceeds.
Streams pass through untouched (response bodies returned as-is;
non-string request bodies receipted as sha256:null rather than drained
to hash).
All ten call sites converted with per-command payload classes:
generate, variants (injected fetchFn passes through), iterate (both
threaded and fresh paths), evolve (image + screenshot analysis), check,
diff, design-to-code, memory.
Unit-tested with injected fetch: receipt-before-send ordering, stream
passthrough, and fail-open on an unwritable ledger.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c0e5ff6639414ac2fd98e8ac3affb51401746b55)
* feat(security): receipt admin scripts + user git-ops (zero exceptions)
Wire the remaining shell egress through gstack-egress-lib.sh:
- gstack-gbrain-mcp-verify: both JSON-RPC probe POSTs (initialize +
tools/list) receipted fail-closed via payload files (hash == wire
bytes). A refused receipt lands in the NETWORK class — no send.
- gstack-security-dashboard / gstack-community-dashboard: the
community-pulse GETs receipted fail-open (read-only stats must not
break over an audit hiccup).
- gstack-gbrain-supabase-provision: api_call receipted fail-closed.
Each retry attempt hands the helper a fresh copy of the body file
(the helper consumes its payload). The receipt hashes the request
body only — the PAT never reaches the ledger or any log. Refusal
exits 8 without retrying.
- git-class sha256:null receipts, fail-open: gstack-artifacts-init
(ls-remote, initial push, fetch/pull recovery, retry push),
gstack-brain-restore (staging clone, existing-repo fetch),
gstack-session-update (self-update pull).
gstack-team-init needs no wiring: every git clone in it is inside an
echoed instruction string, not an executed command.
The lib now self-locates with shell builtins only (no dirname), so
sourcing works under the whitelist-PATH test harnesses.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit b8c5e2055b21ab72878b3e46f8047782ee65a11c)
* test(security): egress wiring tripwire + polarity contract
Static-grep tripwire pinning the egress-receipt wiring (threat model in
the header: the ledger is forensic observability of ATTEMPTED egress,
not an exfiltration control):
- Per-sink assertions: every wired TS module imports egress-receipt and
calls writeReceipt; every wired shell sink sources
gstack-egress-lib.sh with each network op under a receipt;
ngrok-proximity check for server.ts; every design api.openai.com call
routes through receiptedFetch.
- Absence assertions: the dead brain-consumer/reader scripts stay
deleted (lstat, so a dangling symlink also fails).
- Polarity table pinned as data (fail-closed: brain-sync,
memory-ingest, gbrain-sync, telemetry-sync, ngrok, mcp-verify,
supabase-provision; fail-open: design-openai, update-check,
dashboards, git-class user ops, context-bill --exact) plus per-file
polarity spot-checks.
- NEW-SINK SCANNER with zero KNOWN_UNWIRED: sweeps bin/, lib/,
scripts/, design/src, browse/src for curl, absolute-URL fetch(, and
git remote ops (never local rev-parse/get-url; heredoc bodies and
message strings excluded) and requires every hit to be receipted or
in a REASONED exemption list where each entry carries its why.
Preamble-generated skill prose documented out-of-scope in the header.
- Shebang tripwire: no bin/gstack-* file may carry a node shebang.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit ff69ceeafaf9c017d539b6ad77ff8f95b680b979)
* feat(cli): gstack-egress reader
bin/gstack-egress (bun) — the auditor's view of the receipts ledger:
- list: one row per receipt (what gstack ATTEMPTED to send), with
--since/--host/--sink filters and --json.
- verify: recompute the hash chain; exit 3 on tamper naming the first
broken line; prints the sizeWarning when the ledger passes 25MB.
- grants: what CAN leave, built on the upstream config keys only
(telemetry, artifacts_sync_mode, redact_repo_visibility,
redact_prepush_hook via gstack-config get) — each grant names its
file, key, and the exact revoke command.
CLI smoke tests spawn the real bin against a temp GSTACK_HOME,
including a broken-chain fixture asserting exit 3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 9e24eca0f1069fea2ea69e7df4e9b256e93d59a3)
* feat(cli): context-bill — token bill-of-materials (stripped port)
lib/context-bill.ts, ported from the v2 fork and STRIPPED to the tiers
this repo's skills can exercise: ALWAYS-ON (per-skill frontmatter bytes
with dead-key and foreign-host-file flags), EAGER (SKILL.md + any
forced 'for every invocation' references), on-disk totals, --diff,
--budget, and --exact with the calibration table. The fork's
CONDITIONAL/TRANSITIVE/LAZY/FAST-PATH parsers understand only its
dispatcher layout and were dropped; the tier fields stay in the report
shape (empty/zero/null) so re-adding a parser is additive.
TOKEN_DIVISORS and their provenance docblock kept; --help notes
recalibration via --exact's calibration block.
Three upstream fixes over the fork:
(a) findSkillDirs treats the walk ROOT as a container — the repo root's
router SKILL.md is billed AND its children are walked (the fork
short-circuited and billed one skill); walkMd skips node_modules
and dot-directories.
(b) installed-tree layout: subdirs that are their own repo checkout
(a gstack/ clone inside ~/.claude/skills, detected by .git) are
skipped, and directory symlinks (connect-chrome) are followed with
a container-recursion cycle guard.
(c) ROUTER_KEYS widened to the upstream frontmatter contract {name,
description, version, allowed-tools, triggers, preamble-tier}.
--exact writes an egress receipt (sink context-bill-exact, host
api.anthropic.com) BEFORE any count_tokens POST; if the receipt cannot
be written the run degrades to the offline estimate with a warning —
nothing is sent unrecorded. bin/gstack-context-bill is the bun shim.
Tests: fixture-tree ledgers, the three fixes, --diff/--budget exit
codes, --exact with injected fetch (envelope subtraction, receipt
ordering, fail-open degradation), CLI smoke test, and ground truth
against THIS repo via test/helpers/skill-census.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 675c19876b87ec927b555f5f64c7f93130b3de90)
* test(catalog): aggregate discovery-surface budget with ratchet protocol
Every host loads every skill's frontmatter name + description at
discovery, every session. applyCatalogTrim in scripts/gen-skill-docs.ts
shapes each description and the 160KB per-file warn covers body size,
but nothing capped the aggregate frontmatter — the catalog could grow
one reasonable-looking description at a time. This test is that
enforcement layer.
Measures the catalog via test/helpers/skill-census.ts authoredSkills
(symlink-deduped, root router counted separately as the _gstack-command
alias line item): 53 skills + router = 4,420 bytes = 1,105
token-equivalents today, asserted <= 1,150 (~4% headroom). Per-skill
sub-cap of 260 bytes (largest today: design-consultation at 229), plus
a non-empty-description check.
Failure messages are self-service ratchets: they print the new total,
the delta, and the update protocol (bump the constant AND the
derivation comment in the same commit; trim instead of grow for
existing descriptions). Parser handles folded block scalars
(description: >-) for fork parity; import-free by design so it
survives generator refactors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c106fb36f768181b80c257e5cff1cde4f435f9c0)
* fix(browse): extension token bootstrap moves to pinned-origin POST; /health carries no token
GET /health is now liveness/status only in every mode — both token
carve-outs (headed-mode disjunct AND chrome-extension:// Origin
disjunct) are removed. Token bootstrap is POST /extension-token on the
local listener: the Origin header must be exactly
chrome-extension://<GSTACK_EXTENSION_ID> and the Host header's hostname
must parse to 127.0.0.1 or localhost (parsed via new URL, never literal
equality — Host arrives as '127.0.0.1:34567'). Wrong origin/host → 403
with no detail. The tunnel surface 404s the endpoint (not in
TUNNEL_PATHS, verified by test).
The extension ID is pinned by a new "key" field (RSA public key) in
extension/manifest.json; browse/scripts/extension-id.ts reproduces the
ID derivation (first 16 bytes of SHA-256 of the DER public key, hex
mapped 0-9a-f → a-p). The private key is not committed anywhere —
unpacked/baked-in loads only need the public key.
Extension side: background.js bootstraps and refreshes the token via
POST /extension-token (403 → disconnected state); sidepanel.js direct
connect path does the same; sidepanel-terminal.js's dead /health token
fallback (read AUTH_TOKEN/authToken keys the server never sent,
hardcoded port) is replaced with the window.gstackAuthToken path.
MIGRATION NOTE: the manifest key pins the extension ID, so existing
installs' side-panel local state (saved port, snoozes) resets once —
explained in-product via a one-time notice (flag
gstack_id_migrated_v162). After upgrading the server, restart the
browser so the old service worker stops polling for a token GET /health
no longer serves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit e9a0b6847a2d17fe6656a4686b4efd0c8380eb09)
* docs: correct stale compiled-binaries claim; file three egress/eval follow-ups
CLAUDE.md's compiled-binaries section claimed browse/dist binaries are
tracked by git and appear as modified in git status — false since
64d5a3e4 (v0.11.16.0) untracked them, and actively harmful: it trained
agents to ignore dist binaries in git status. The section now states
the truth (untracked + gitignored; a dist binary in git status means
someone force-added it) and covers make-pdf/dist too.
TODOS.md gains the three follow-ups filed by the v1.62 port-wave
reviews: ledger rotation with chain-genesis records, launch-nonce
token bootstrap, and eval-watch shard-awareness.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: pre-landing review fixes for the v2 port wave
Review army (checklist + 5 specialists) + coverage/plan audits on the
assembled branch. Genuine correctness/security/hygiene fixes:
- test-paid-shards: strictTestExitCode now receives expectedFiles on the
real bun path, so a shard that runs fewer files than planned (harness
crash, nothing loaded) with exit 0 is no longer recorded 'passed' — the
invisible-non-execution class the runner exists to kill. Pinned by the
new test/strict-output.test.ts (also covers the chunk-boundary classifier).
- test-paid-shards: EVALS_TIER env is validated (gate|periodic) like the
--tier flag, so a typo can't self-skip every test and exit 0 green.
- package.json: test:periodic:sharded sets EVALS_ALL=1, restoring the
full-tier semantics the pre-shard script had (CI already set it; local
eval:bg:periodic silently under-measured without it).
- brain-sync.test: run() pins HOME to the temp home so gstack-artifacts-init
stops writing/clobbering the operator's real ~/.gstack-artifacts-remote.txt
every free-suite run; afterEach now also scrubs the current filename.
- egress-receipt: cap each receipt field at 512B so a serialized line always
fits the 4KB tail-read window — a longer line would make the next append
hash a truncated prior line and verifyLedger report a permanent false
TAMPER. warnLedgerSize short-circuits before statSync once fired (append
hot path).
- gstack-egress: import.meta.dir (Windows-safe) instead of new URL().pathname
so grants doesn't silently report defaults on Windows; strip control chars
from ledger-derived fields on render so a crafted receipt can't spoof the
auditor's view.
- extension/background.js + CLAUDE.md: renumber the identity-pin migration
refs v1.62 -> v1.63 (main claimed 1.62.0.0; this wave queue-advances).
- egress-receipt-wiring: pin lib/context-bill.ts unconditionally (both land
together now); drop the dead RunShardsOptions.tier field.
All fix-affected test files green; gate failures triaged as external-env
(codex/gemini CLI drift) or pre-existing (hermetic-canary fails identically
on base). Deferred polish tracked in the PR body + decision store.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump version and changelog (v1.63.0.0)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: file TODO to harden plan-design-with-ui PTY detection
The v1.63 seedSkills change made this gate test execute for the first
time; it reliably times out because its terminal scraper can't parse the
(correctly-rendered) scope-gate AskUserQuestion out of a spinner-mangled
PTY buffer. Shipped skill behavior is correct — test-harness limitation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: re-slot release as v1.62.1.0 (PATCH per user)
Main claimed 1.62.0.0 while the wave was in flight; the user chose the
PATCH slot over queue-advancing MINOR. Renumbers the identity-pin
migration notice (now version-free flag name so a re-slot never orphans
an already-set flag), the CLAUDE.md /health note, the CHANGELOG heading,
and the TODOS section titles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(test): stop hard-requiring the literal ok) case label in gbrain-refresh guards
The extractor grepped for 'ok)' but the case label grew to
ok|timeout|thin-client) (#1964, #2051), so the whole file errored on
import — the free suite's only red for months. The extractor now matches
any label starting with ok and its alternations; all 7 guard assertions
run again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(test): hermetic-canary probes with ${VAR:-} so nounset shells can't fail success
The probe echoed bare $CONDUCTOR_WORKSPACE_PATH — when scrubbing WORKS
the var is unset, and under a nounset shell the echo errors, failing the
canary exactly when isolation succeeds. Defaulted expansions assert
identically under any shell. Fails identically on base; fixed here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(evals): absorb codex/gemini CLI drift; external-service tests go periodic-tier
- codex exec gains --skip-git-repo-check: newer CLIs refuse exec in an
untrusted non-git dir (our temp skill dirs) — empirically verified.
- gemini: --skip-trust was removed in gemini-cli 0.34 (argv parse error);
dropped from the session runner and the benchmark adapter. A present-
but-unusable CLI (deprecated individual code-assist auth path) now
classifies as SKIP, not a false adapter failure; the benchmark live
smoke skips on auth/rate_limit error codes (environmental) while still
failing on timeout/unknown (the drift classes it exists to catch).
- codex-e2e, gemini-e2e, and benchmark-providers gain the canonical
whole-file EVALS_TIER === 'periodic' guard per CLAUDE.md tiering rule 3
(external service -> periodic) — the sharded gate runner now excludes
all three (gate: 45 -> 42 shards).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(evals): parse single-logical-line AskUserQuestions in the PTY runner
When the PTY reflows a boxed AUQ, ALL options land on ONE logical line
after stripAnsi — parseNumberedOptions parsed one option per line, found
only '1.', and the >=2 check failed forever while the correct question
sat on screen (plan-design-with-ui timed out this way twice, with the
rendered scope-gate AUQ visible in both failure buffers). The cursor
line is now parsed as a stream of ascending N. tokens; DEC cursor-
visibility residue is stripped before matching; plan-design-with-ui's
budgets grow to fit observed ~6min preamble+thinking latency. Pinned by
test/pty-auq-single-line.test.ts using the real failure buffers; all 142
existing parser-consumer unit tests still green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: restore v1.63.0.0 (MINOR — user-confirmed final slot)
The wave ships new capability (egress receipts + two CLIs, sharded paid
runner, hermetic skill seeding) at ~8K lines — MINOR scale per the
scale-aware bump rules. Supersedes the brief v1.62.1.0 re-slot; the
version-free migration flag means no state churn from the renumber.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: spell out AskUserQuestion in the PTY single-line fixture
Rename test/pty-auq-single-line.test.ts to
test/pty-askuserquestion-single-line.test.ts and expand the AUQ
abbreviation in identifiers and comments. House style writes
AskUserQuestion in full in filenames, identifiers, and comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: sync every doc surface with the v1.63 release
/document-release audit (4-lane, all claims verified against branch code):
- README: gstack-egress + gstack-context-bill rows in the standalone-binaries
table; Privacy & Telemetry gains the receipted-egress bullet (attempted-
egress framing per the shipped threat model).
- ARCHITECTURE: /health is liveness-only, POST /extension-token endpoint row
+ bootstrap mechanics paragraph; new Egress receipt ledger subsection under
Security model; eval persistence covers the sharded runner, GSTACK_EVAL_DIR,
and the finalized-run baseline rule.
- CLAUDE.md: sharded test scripts in Commands; sharded semantics in the
detached-evals section; PTY skill seeding in the hermetic section; egress
invariant block beside the other server-egress invariants; catalog-budget
ceiling beside the 160KB token ceiling; project-tree entries for
lib/egress-receipt.ts, lib/context-bill.ts, scripts/test-paid-shards.ts.
- CONTRIBUTING: seedSkills + live-tree seeding in the hermetic paragraph;
sharded runner in detached runs; catalog-budget in the Tier 1 list.
- BROWSER: extension token bootstrap section, tunnel egress receipts section,
identity-pin migration note in manual install.
- REMOTE_BROWSER_ACCESS: tunnel-start receipt bullet in the security model.
- gbrain docs: /sync-gbrain + brain-sync egress-receipt behavior documented;
dead consumer-token instructions removed (consumer machinery deleted this
release); new fail-closed refusal added to the error catalog.
- CHANGELOG: measured-vs-ceiling catalog numbers, contributor notes for the
external-service tier move and the PTY single-line AskUserQuestion parser,
release date.
- TODOS: /health token-distribution TODO resolved by this release, removed;
port-wave follow-up sections re-labeled to the shipped version.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: sweep drift that predates this release
Surfaced by the /document-release audit; every fix verified against the
current binaries:
- gstack-brain-init was replaced by gstack-artifacts-init in v1.27.0.0
(hard-delete, no compat shim), but README, USING_GBRAIN_WITH_GSTACK,
docs/gbrain-sync.md, and docs/gbrain-sync-errors.md still instructed
users to run it — command-not-found on every follow. Same sweep updates
~/.gstack-brain-remote.txt to the canonical ~/.gstack-artifacts-remote.txt
(legacy name still honored on restore, noted where users copy the file).
- gbrain-sync-errors.md headings re-matched to the literal messages the
binaries print today (the doc's whole value is grep-by-exact-message):
'gstack-artifacts-init: ~/.gstack/ is already a git repo pointing at:',
'Remote not reachable via SSH:', 'Failed to create or find ...'. The
already-a-repo fix now leads with the command's own set-url suggestion.
- docs/gbrain-sync.md 'Under the hood' linked a plan file that does not
exist in the repo; replaced with the decisions themselves.
- SIDEBAR_MESSAGE_FLOW startup timeline: /pty-session responds with
{terminalPort, sessionId, attachToken, leaseExpiresAt} (v1.44 shape,
verified at browse/src/server.ts:1860), not the retired
{terminalPort, ptySessionToken} pair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: fold the Codex accuracy review of the release docs
Six findings, all verified against source before fixing:
1. 'Every send writes a receipt' overclaimed — fail-open sinks proceed with
a stderr warning when the receipt write fails, so a fail-open send can go
unrecorded (lib/egress-receipt.ts:8-14). Descriptive prose now says so;
the receipted framing keeps 'attempted'.
2. 'Receipts hash the request body' is wrong for subprocess-owned sends —
git pushes record sha256: null (lib/egress-receipt.ts:71).
3. 'grants shows every consent in force' overclaimed — it reports the four
standing config settings (bin/gstack-egress:139-181). Reworded in
README, ARCHITECTURE, and the CHANGELOG entry.
4. 'Zero-exception scanner' vs reality: the new-sink scanner carries a
reasoned SCANNER_EXEMPT list (user-directed fetches, probes, instruction
strings, skill prose). CLAUDE.md now names it.
5. Error-catalog cause/fix for the receipt refusal: the writer mkdirs the
ledger dir itself, so 'missing' isn't a cause and bare chmod fails when
it is absent — cause reworded, fix is mkdir -p && chmod.
6. gbrain-sync first-run steps described the retired binary's behavior:
default repo is gstack-artifacts-$USER, and init PRINTS the gbrain
hookup command (never auto-executes; bin/gstack-artifacts-init:384-419).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>