mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-21 13:37:14 +02:00
2be6c06ba83de82dcdd37a28d68d2787cfe8c63e
19
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2be6c06ba8 |
v1.65.0.0 feat: fork port wave 2 — feature fixes, session persistence, Apple releases, supply-chain CI (#2577)
* fix(memory-ingest): pass --include-gitignored to gbrain import gstack-artifacts-init writes an ignore-everything .gitignore (a bare `*`, headed "Do not edit") at the root of ~/.gstack. The memory ingest stages pages into ~/.gstack/.staging-ingest-<pid>-<ts>/, which is inside that repo, and gbrain's markdown collector honours .gitignore. The collector therefore matches every staged file against `*` and collects zero. The failure is silent. gbrain import exits 0 having imported nothing while the ingest prints `written: N` from the STAGED count rather than the imported count, so a run that indexes nothing looks identical to a healthy one and the memory corpus quietly stops growing. Reproduction, using git's own ignore machinery (no gbrain needed): git init . mkdir -p .staging-ingest-12345/learnings echo x > .staging-ingest-12345/learnings/page.md printf '*\n' > .gitignore git ls-files --others --exclude-standard # -> empty Passing --include-gitignored makes the import independent of whatever .gitignore sits above the staging directory. Adding a negation to the generated .gitignore is the alternative, but that file is gstack-owned and marked "Do not edit", so any regeneration silently reintroduces the bug. Adds a regression pin in the shape of memory-ingest-no-put_page.test.ts, plus a behavioural test for the collision itself. Both source pins fail against the unpatched file. * fix(memory-ingest): GIT_CEILING_DIRECTORIES defense-in-depth on the import child (#2144) Second layer under #2560's --include-gitignored: a realpath'd ceiling at the staging dir's parent pushes any git-enumerating collector off the git fast path (which sees zero files under ~/.gstack's ignore-everything root) onto its plain FS walk, even on gbrain builds whose flag semantics drift. Ceiling is realpath'd because git compares canonicalized directories during discovery — a staging dir reached through a symlink (macOS /var -> /private/var, symlinked $GSTACK_HOME) otherwise never matches. Behavioral tests prove discovery stops at the ceiling from the staging dir, including through a symlinked path, using git itself — no gbrain required. Mechanism ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(autoplan): Phase 4 task aggregator emitted zero tasks on every run (#2018) The branch+commit jq filter piped to the split commit array and then referenced .commit — jq rebinds context across a pipe, so .commit indexed the ARRAY with a string, every input line errored into 2>/dev/null, and || true swallowed the exit. The aggregate table has been empty for every user since the feature shipped. Bind .commit to a variable before the pipe. Functional pin extracts the ACTUAL emitted jq program from the resolver and runs it against fixture JSONL (verified RED against the broken filter), plus a source-shape guard against reintroducing a context-rebinding reference. Fix mechanism from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(codex): BSD mktemp templates broke /codex on every macOS install (#2091) macOS mktemp requires the X's to end the template; the five "codex-*-XXXXXX.txt" sites failed with "mkstemp failed ... File exists" before Codex ever ran (reproduced live on this machine). Same class fixed in claude/SKILL.md.tmpl's three sites. bin/gstack-paths now strips macOS's trailing slash from TMPDIR so TMP_ROOT-built paths stop carrying "//". Static tripwire scans every tracked .tmpl for characters after the X-run in a mktemp template (longer X-runs stay valid), plus a live portability check of the emitted shape. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(decision-log): --supersede silently discarded the replacement decision The supersede/redact branch appended the retirement event and exited before the JSON argument was ever read — a user recording a reversal WITH its replacement lost the replacement, and the payload finder's first-non-flag-arg predicate would have mistaken the target id for JSON anyway. Payloads are now identified by their leading brace, validated BEFORE any write, and appended FIRST (retirement second), so the only visible interleaving under a crash is both-active — recoverable, never lost. The replacement carries supersedes:<old-id> provenance. Bare --supersede <id> (the documented reversal-without-replacement) stays legal; --redact with a payload now refuses instead of dropping it. Ported from time-attack/gstack (GStack 2), tests included. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(brain-context): cold-start probe latency permanently disabled gbrain context gbrainAvailable() spawned gbrain --version under a 500ms budget; a cold CLI start on a loaded machine blew the timeout, misclassified gbrain as missing, and every skill session silently ran brainless — plus the per-query re-probe burned 3x the budget before any real work. Replaced with a memoized stat-based PATH scan (PATHEXT-aware on Windows) and made the query timeout overridable via GSTACK_BRAIN_TIMEOUT_MS for loaded CI environments. Also picks up the fork's manifest-filter coverage (#1687 shape) against the fake-gbrain harness — passes against our existing filter support. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(setup-gbrain): voyage-code-3 flags were silently dropped under zsh (#1798) zsh does not word-split an unquoted $VAR, so all three PGLite-init sites passed the entire flag string as ONE argv word — gbrain ignored it and silently fell back to its default embedding model, downgrading code retrieval for every zsh user (macOS default shell). Flags now ride the positional params (set -- ...; "$@"). Tests run the shape under BOTH bash and zsh against the fake-gbrain argv recorder (per-word argc log distinguishes one-blob from split), include a demonstration of the zsh collision on the old shape, and pin the template's three sites statically. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(model-benchmark): recognize macOS Keychain auth in the claude adapter (#1890) The default macOS Claude Code install stores OAuth under the Keychain generic-password service "Claude Code-credentials" and never writes ~/.claude/.credentials.json, so available()'s file-or-env sniff reported "No Claude auth found" while claude -p worked fine. On darwin the sniff now also probes the Keychain entry — metadata only (no -w, the secret is never read), 5s timeout, any security(1) failure degrades to not-found. Verified live on this machine (subscription install, no creds file, Keychain entry present). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(upgrade): v1.27 migration no longer auto-proceeds without a TTY or records a failed rename as done (#1383) Two silent-failure shapes in one script. Non-interactive runs (Claude Code Bash tool, CI) blanket-auto-proceeded into a REMOTE repo rename — now they skip-for-now by default and ask again next upgrade; unattended runs opt in with GSTACK_MIGRATE_ASSUME_YES=1. And a failed gh rename was journaled as done and the done-touchfile written, permanently stranding a half-renamed install — the failed step now stays PENDING with the manual command printed, finalize refuses the done-marker while any step is unjournaled, and the migration exits 1 with a re-run pointer while completed steps still skip on retry. Harness updated to opt in explicitly; new tests pin the default-skip and failure-stays-pending-then-retry-succeeds contracts (13/13). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ship): REST fallback when gh pr edit hits the Projects-classic GraphQL deprecation (#1079) On repos where GitHub enforces the Projects-classic sunset, gh pr edit hard-errors on repository.pullRequest.projectCards and Step 19's PR body update dies. The template now names the error shape, says it is not an auth problem, and falls back to the REST endpoint (gh api pulls/N -X PATCH) with the SAME already-redaction-scanned temp file for body and title. Generated SKILL.md regen rides the cluster regen commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ship): test-command detection was blind to Django and config-less-but-tested projects The Test Framework Bootstrap detected Python only via requirements.txt or pyproject.toml and treated missing config files as no-tests, so a green 'python manage.py test' Django app, a Go project with *_test.go beside the source, in-source Rust #[test] blocks, or a package.json with only a test script all got offered a SECOND test framework over a working one. Detection now enumerates definitive per-ecosystem markers (manage.py, tox.ini/setup.cfg, pom.xml/gradle, Makefile test targets, a tracked-file test census, in-source Rust tests) as EVIDENCE for the question it asks — never a command to run blind — preserving the read-CLAUDE.md-or-ask contract, with a marker→candidate-command table and ask-once persistence. The shared coverage-audit detection block gains the same markers. Test runs the resolver's emitted detection bash against Django / Go / Rust / Node fixtures in throwaway git repos. Ported from time-attack/gstack commit e3259078 (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: regenerate SKILL.md files for cluster A (autoplan jq, codex mktemp, setup-gbrain zsh, ship detection + REST fallback) Atomic regen of the 9 generated files whose templates/resolvers changed in the A-cluster commits. bun run gen:skill-docs, no hand edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: refresh ship goldens + parity ratios for cluster A growth Codex/Factory hosts render single-file ship skills whose committed goldens must track template changes; refreshed from the regenerated renders. Parity size guards bumped with the growth itemized — ship (carve-guards) 1.08 -> 1.10 for the detection-evidence + REST-fallback growth measured at 1.090x, qa (parity-harness monolith invariant) 1.07 -> 1.12 for the shared coverage-audit markers measured at 1.111x. Kept tight so the next growth is a deliberate decision, not drift; the Apple adapter raises ship again with its own justification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gbrain-sync): enforce the per-repo policy at the code-import chokepoint (#2140 sync path) The deny/read-only tiers in ~/.gstack/gbrain-repo-policy.json were stored by gstack-gbrain-repo-policy but enforced only in /sync-gbrain skill prose — a direct or cron invocation of gstack-gbrain-sync ingested repo code regardless. Worse: the code stage's egress receipt has cited 'per-repo policy chokepoint (repoPolicyTier)' as its consent since v1.63 while no such function existed. repoPolicyTier() now gates the stage before the dry-run branch: deny → refused-policy-deny (exit 1, loud), read-only → clean skipped-policy-read-only (code ingest writes pages), unreadable store → fail-closed refused-policy-unreadable, no store → unchanged fail-open. Subprocess tests pin all four paths against real git repos and a permission-blocked store (verified RED against the ungated binary). The receipt's consent string is truthful from this commit. #2140's ingest-path source-isolation ask remains open — partial-progress comment at ship. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios-qa): /auth/sessions no longer hands raw bearer tokens to any local process The loopback sessions list echoed live tokens — a harvest-and-replay primitive for anything on the machine (same class as the /health token leak fixed in v1.63). The list now returns a device-salted 16-hex token_id plus metadata; the salt is shared with the attempts log so identifiers correlate. /auth/revoke keeps the list→revoke workflow alive by accepting token_id alongside the caller's own raw token and identity. saltedHash() is exported from audit.ts and writeAttempt now reuses it (was inlined). Integration tests pin raw-token absence, the id shape/metadata, and the token_id revoke round-trip (verified RED against the leaking handler). List fix ported from time-attack/gstack (GStack 2); token_id revoke is ours. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios-qa): boot token out of os_log entirely; IPv4 listener pinned to loopback at the socket The StateServer's bootstrap announce logged the live boot token with privacy: .public — and nothing consumed it: the daemon has read the token from the 0600 app-container file since the devicectl copy flow landed. The log line handed a credential to anything reading the unified log during the launch window. It now announces port/build only. The IPv4 listener bound the wildcard interface and relied on the per-connection peer check alone; IPv4 has no CoreDevice tunnel path, so it now binds 127.0.0.1 via requiredLocalEndpoint at the socket level. IPv6 keeps the wildcard bind for CoreDevice ULA peers by design. Static pins cover both the template and the fixture app copy. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(make-pdf): close the offline-gate bypass via raw-HTML fetch vectors With --allow-network off, the sanitizer stripped script/iframe/link but let Chromium fetch remote resources at print time through four raw-HTML vectors: <style> @import (any form), remote url() in <style> blocks and inline style attributes (incl. protocol-relative //), srcset with a remote candidate (Chromium prefers srcset over the inlined src), and remote src/poster on video/audio/source/track. All neutralized at the sanitizer; remote <img src> is deliberately left for the image inliner so its blocked-remote placeholder still fires, and url() mentions in prose/code spans stay untouched. Fork's test suite ported verbatim (12 cases incl. the end-to-end render assertion), verified RED against the old sanitizer. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pair-agent): tunnel activation is consent-gated — and the receipt's consent claim is now real The tunnel egress receipts have claimed consent: 'pair_agent=on' since v1.63 while no such key or gate existed — ngrok installed+authed was enough for the CLI to auto-start an internet-facing tunnel. isPairAgentEnabled() (fail- closed, env-overridable) now gates all three activation points: CLI auto-start, POST /tunnel/start (refuses with the enable hint), and the BROWSE_TUNNEL=1 startup bind. Consent-on-first-use, not silent breakage: the /pair-agent skill asks once (one-way-door posture), sets pair_agent via gstack-config (registered with on|off validation, default off), and never asks again; direct API callers get the same hint in the refusal. Adapted from the fork's gate: their reader targeted config.json, which on main would have made the gate silently un-enableable — ours reads the canonical ~/.gstack/config.yaml with the JSON shape as fallback, pinned by tests either way (11 cases, gate wiring tripwires included). Ported from time-attack/gstack (GStack 2), store adaptation ours. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: regenerate pair-agent SKILL.md for cluster B (consent gate) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): cancel the parent watchdog when handoff promotes a daemon to headed The parent-process watchdog assumes connection mode is fixed at boot: headless daemons outlive their parent, headed ones do not. The env guards (BROWSE_PARENT_PID=0, BROWSE_HEADED=1) only cover daemons that were headed when they started. handoff breaks that assumption. It swaps in a headed context on a RUNNING daemon and sets connectionMode = 'headed' without a restart, so a daemon that legitimately registered a watchdog lands on the fatal side of the branch. The parent is usually a short-lived shell, and Claude Code's Bash tool kills one after every invocation, so the next 15s poll shuts the daemon down. The user-visible effect is that handoff destroys the thing it just created. It exists so a human can log in, solve a CAPTCHA, or clear an MFA prompt; the browser disappears about fifteen seconds later and takes the session with it. Observed while driving two registrar control panels: five daemon deaths and three logins, each one discarding the authenticated session. BrowserManager now exposes onHeadedPromotion, fired only on runtime promotion and not on a headed boot, and the server binds it to a canceller for the interval it already owned but previously discarded. Bound on both the module-level manager and any embedder-supplied one, since the watchdog reads activeBrowserManager and binding only the default would let embedders promote silently. The binding sits next to the browserManager declaration rather than next to clearParentWatchdog. Placing it with the function, which lives with the watchdog it cancels, reads better but touches browserManager in its temporal dead zone, which aborts module evaluation and leaves every later const uninitialized. findport tests catch that immediately. Tests: watchdog.test.ts already noted in its header that its three cases all fix mode via env at spawn time, so none reaches the headed branch. Driving a real handoff needs a headed Chromium, so the wiring is pinned with static tripwires instead, matching cdp-session-cleanup.test.ts and server-auth.test.ts. Verified they fail when the notification call is removed and pass when restored. Full `bun test` shows the same 6 pre-existing failures on this branch and on main (gstack-gbrain-detect, gstack-artifacts-init), which pass in isolation on both, so they are test-order pollution rather than a regression here. * fix(browse): pass windowsHide so the daemon stops popping console windows On Windows, `browse` leaves empty black console windows on top of whatever the user is doing — they pop up every few minutes for as long as any browser skill is alive, and outlive the process that created them. Cause: `bun-polyfill.cjs` maps `Bun.spawn`/`Bun.spawnSync` onto node's `child_process`, and node defaults `windowsHide` to **false**. Bun never creates these windows, so nothing in the daemon's own code looks wrong — the behaviour only appears on the node fallback path. The one users notice is `spawnTerminalAgent()`, which launches `bun run terminal-agent.ts` through this shim. The daemon respawns it on a watchdog, so closing the window is not enough — a new one arrives shortly after. Ten `bun.exe` processes were live on the machine this was diagnosed on. Why they linger after the child exits: with the default terminal application set to "Let Windows decide", the console is brokered through Windows Terminal via svchost, and WT leaves the empty frame behind when its only child exits. The frame has no child process at all, which is why it looks like a dead terminal. Setting `windowsHide: true` on both wrappers fixes every console child routed through the shim — the bun agent plus the `tasklist`, `git` and `powershell` calls elsewhere in the daemon. No behaviour change on macOS or Linux, where the option is ignored. Not covered by this commit: `chromium.launch()` goes through playwright's own process launcher rather than this shim, so it still creates one window per daemon start. Worth a follow-up. * test(browse): make bun-polyfill tests runnable on Windows, and cover windowsHide `bun test browse/test/bun-polyfill.test.ts` was **0 pass / 4 fail on Windows** before this — every test in the file, on the platform the polyfill exists to support. Each test interpolates the polyfill's absolute path into a single-quoted JS string passed to `node -e`. On Windows that path has backslashes, so JS eats them as escapes: 'C:\Users\jwilk\dev\gstack-fork\browse\src\bun-polyfill.cjs' -> C:Usersjwilkdevgstack-forkrowsesrcun-polyfill.cjs (`\b` is a real escape, so it deletes a character too.) `require()` throws, the subprocess dies, stdout is empty, and every assertion compares against "". The tests pass on macOS and Linux purely because those paths have no backslashes. Fixed by interpolating with `JSON.stringify(polyfillPath)`, which quotes and escapes correctly on all platforms. Also adds a regression test for the windowsHide fix in the previous commit. It stubs `child_process.spawn`/`spawnSync` *before* the polyfill destructures them and asserts the captured options, so it is deterministic and needs no window — it verifies the contract on macOS and Linux too, where the option is a no-op. Verified on Windows: 5 pass / 0 fail with the fix, and the new test alone fails ("VISIBLE" instead of "HIDDEN") when the previous commit is reverted. * fix(browse): forward windowsHide through the Bun polyfill spawn shims The Node fallback shim accepts a Bun.spawn options object and forwards only stdio, env and cwd to child_process.spawn. windowsHide is dropped, and because Node defaults it to false while Bun.spawn hides the console window, the omission inverts the behavior on the one platform the shim exists to support. Symptom: the terminal-agent respawn in server.ts (60s watchdog ticker) pops a visible bun.exe console window on Windows every time it fires, so the window keeps coming back with no scheduled task or startup entry behind it. stdio:'ignore' silences the child's output but does not suppress its window. Both shims now forward the option and default it to true, matching the Bun API being emulated; an explicit windowsHide:false still passes through. spawnTerminalAgent also sets it explicitly at the call site. Tests: three cases in browse/test/bun-polyfill.test.ts assert the default for spawn and spawnSync and that an explicit false is honored. Each was confirmed to fail against the unpatched shim. Drive-by, required to run the suite at all on Windows: the tests interpolated an absolute path into a JS string literal, so backslashes were consumed as escapes and every require() failed with MODULE_NOT_FOUND. The path is now normalized to forward slashes. On Windows this file went from 0/4 passing to 7/7. * fix(browse): headed mode on macOS 26 — stop mutating the signed Chromium bundle, heal the ones we already broke (#2242, #2138, #2139) The in-place rebrand rewrote the Chrome-for-Testing bundle's Info.plist (global name replace — which also renamed CFBundleExecutable to a binary that doesn't exist) and overwrote its Resources/*.icns, breaking the codesign seal: GPU process exit_code=5, headed mode dead on macOS 26. The mutation lived in the SHARED Playwright cache, so it also poisoned the user's other Playwright projects. Three layers land together: (1) the rebrand block is gone — branding lives in the GStack Browser.app wrapper via GSTACK_CHROMIUM_PATH, with a tombstone and a static tripwire (no plist/icns writes into the bundle; the tripwire allows the read-only probe below); (2) a launch-time self-heal detects an already-poisoned cache bundle, removes it, and errors with the exact re-fetch command — covering deploy paths that never run migrations; (3) migration v1.64.0.0 sweeps every cached bundle, removes poisoned ones, and re-fetches clean Chromium immediately (migrations run after ./setup, so without the re-fetch an upgrade would end with zero working browser). Functionally verified against fixture caches: poisoned removed, clean untouched, rerun no-op. Migration filename tracks the final VERSION at ship. The #2242 watchdog half is the absorbed PR #2565 (thanks @Screddyice). Tombstone/tripwire ported from time-attack/gstack (GStack 2); self-heal and migration are ours. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): 'browse stop' no longer restarts the daemon it was asked to stop The stop handler awaited shutdown() — which ends in process.exit — before returning, so the acknowledgement never egressed. The CLI's fetch reset, which its crash path reasonably interpreted as a dead daemon: it relaunched Chromium, re-sent stop, watched the daemon exit again, and errored 'Server crashed twice in a row'. Every stop cost a wasted Chromium launch and a nonzero exit. The ack now returns first; shutdown fires on a 25ms unref'd timer. Same fix for restart. Fork's test pins ack-before-teardown for both. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): lock acquisition reports real errors instead of phantom contention (#1084) acquireServerLock's bare catch treated EVERY failure as 'another process holds the lock' — a missing state dir, EACCES, or ENOSPC read as permanent phantom contention with nothing to debug. Now only EEXIST is contention: ENOENT self-heals with one mkdirSecure retry, everything else throws ServerLockError carrying the real errno, and the stale-lock unlink/retry loop is depth-capped so it can't livelock. Fork's five-case test ported. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(browse): integration coverage for #1781 busy-vs-dead recovery Fork's wedged-daemon fixture: first /command connection drops, daemon PID stays alive. Pins the whole contract — CLI retries the same daemon instance without a kill, state file untouched, no restart, exactly two command requests. Message-text assertion adapted: our CLI retries silently at the probe layer where the fork announces on stderr; the behavior, not the message, is the invariant. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): windowsHide on every Windows-reachable spawn (#1835) Console windows flashed (and stole focus) on every daemon relaunch, taskkill, tasklist poll, and powershell DPAPI call — node-level spawns default windowsHide to false. Covered: the node -e launcher (outer spawnSync AND the inner detached daemon spawn inside the launcher string), the dev-mode bun fallback, killServer's taskkill, isProcessAlive's tasklist, and cookie-import's powershell + tasklist. The Bun-polyfill shims were covered by absorbed PRs #2523 + #2539 (thanks @jwilk-hrep, @jerrynicholsai); this closes the sites those PRs didn't reach. The icacls sites land with the #1605 DACL commit alongside the static tripwire that pins all of them. R8's planned spawnHidden() helper is deliberately NOT built: the polyfill default plus the tripwire achieve the no-drift goal without indirection over seven heterogeneous call shapes. The polyfill + spawn-hide tests join the Windows CI shard. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(browse): self-repair broken Windows DACLs on state dirs (#1605) icacls '/inheritance:r /grant:r' can partially fail on localized or domain accounts: inheritance strips but the user grant doesn't resolve, leaving a machine-SID-only DACL the owner can't even list — the sidebar/PTY failure chain in #1605, caused by the very hardening call meant to protect the dir. mkdirSecure now verifies listability after hardening (a real readdir — fs.accessSync doesn't consult NTFS ACLs) and repairs via icacls /reset, re-hardens, and if hardening breaks access again leaves inherited ACLs: functional-but-unhardened beats hardened-but-unusable. The icacls calls carry windowsHide (#1835's last two sites) and the fork's static spawn-hide tripwire lands here, pinning every covered site. file-permissions.test.ts is already in the windows-free-tests curated shard, so the DACL contract executes on windows-latest. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(browse): opt-in session persistence — auth survives daemon restarts (#778, #2193) BROWSE_PERSIST_STATE=1 snapshots cookies + per-tab URL/localStorage/ sessionStorage to <stateDir>/session-state.json (0600) on a 30s unref'd interval and at clean shutdown, and restores on the next launch — killing the top-complained auth-lost-on-restart class (#778, #2193, #1128, #1129). Security invariants mirror state save|load: loadedHtml and owner are never persisted and never accepted from disk; restored cookies pass the same hygiene filter (localhost/.internal/metadata domains dropped); restoreState re-validates every URL. Default OFF; headed mode excluded (the persistent profile owns that state). Hardened past the fork's shape per review R3: corrupt state quarantines to .corrupt (forensic artifact, boots fresh, one log line), snapshot failures warn once and never kill the daemon, and the boot log reports restored counts or fresh-session status. Module + 10 tests ported (MIT header retained); server wiring at launch, interval, and shutdown; skill docs section added (regen rides the cluster regen commit). Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: regenerate browse SKILL.md for cluster C (session persistence docs) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): third-party web-actions contract — offer to drive vendor-site steps, never just dump a manual list When a workflow needs something done on an external website the user controls (register an API key, create a vendor account, configure a dashboard/webhook/OAuth app), five skills (ship, spec, office-hours, setup-deploy, land-and-deploy) now follow one contract: offer to drive it in a visible browser via gstack's own stack ($B headed + handoff/resume, GStack Browser) behind ONE per-task consent question naming the exact site and actions; passwords, payment, CAPTCHA, and identity stay user-performed; captured secrets go to owner-only files or the user's secret store, never chat/logs/history; and the credential is verified with one non-mutating API call before any success claim — dashboards show masked placeholders, and a 401 catches them. Declining yields manual steps and a blocked-on-user mark; nothing new is ever installed to close the gap. New resolver token {{THIRD_PARTY_ACTIONS}} (adapted from the fork's contract — their Aside-browser detection swapped for our own driver stack; MIT portions noted). Parity guards bumped with growth itemized (ship 1.10->1.12 at measured 1.103x; office-hours skeleton 101K / 1.09 at measured 1.079x); ship goldens refreshed. Ported from time-attack/gstack (GStack 2), driver adaptation ours. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(office-hours): design docs land in the repo, written as decision records (#703, #2000) Office-hours Phase 5 now dual-writes the design doc: the docs/designs/ copy is what teammates and plan reviews read (committable, visible), while the ~/.gstack copy keeps memory ingest and cross-session discovery working. The repo copy leaves the private store, so it passes the redaction scan-at-sink first (HIGH blocks the repo copy, MEDIUM confirms per finding), and any failure — read-only checkout, non-git dir, unconfirmed finding — degrades to the private copy with a one-line reason, never blocking the handoff. The doc itself is now a decision record, not a transcript: one bullet per decision with its why, ruled-out approaches collapsed to a single line with the rejection reason, settled/empty template sections omitted. No page cap; extra length must come from genuinely open questions. Plan reviews (ceo/eng/devex + the shared review resolver) prefer the repo-local doc (DESIGN.md, then newest docs/designs/*.md) when it's at least as fresh as the private copy — a stale old repo doc never shadows a newer session. Parity guards bumped with measured values (three plan-review skeletons +~0.7KB each; office-hours 1.092x). Judgment ported from time-attack/gstack (GStack 2); scan-at-sink and freshness-preference adaptations ours. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(office-hours): 'never show me these again' for the founder-resources pitch (#538) The Phase 6 resources offer (34 PG essays + Garry/YC videos) had no permanent decline — the reporter showed memory instructions kept being overridden on every update, so people who said no got re-pitched forever. The offer now closes with a standing choice; opting out runs gstack-config set founder_resources false (new key, default true, true|false validated), the write is VERIFIED before any promise (a failed write says so and skips this session only), and every future session skips the entire section silently — no resources, no 'skipped as requested' mention. Config outlives session context, so never means never. Re-enable anytime: gstack-config set founder_resources true. The pitch stays default-ON for everyone who never opted out. Tests pin the key's default/persistence/validation through the real config bin and the generated section's gate-before-content + write-verify contract. Approved as a promo-surface change (CEO review D3.4, 2026-08-14). Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ship): the Apple App Store release journey — working tree to Submit for Review Point /ship at a repo with an .xcodeproj, .xcworkspace, or app-product Swift package and ask to release: the adapter runs the whole journey with ONE authorization moment (membership + pricing + in-session sign-in, decision- store persisted so repeat releases ask nothing) and one store-assets question only when assets are missing. fastlane is the single tool (produce/cert/ sigh/gym/pilot/deliver/frameit); credential vocabulary never reaches the user. The adapter carries 21 live releases' worth of paid-for Apple knowledge: the web session mints the permanent upload key itself (iris POST /v1/apiKeys; privateKey is base64-of-PEM, downloadable only at creation) so nobody ever types an app-specific password; error -22938 is Transporter asking for a key, not a user task; errors are CLASSIFIED before credentials are touched (validation/UnexpectedResponse = metadata, incl. Apple's expanded age-rating attributes); pricing goes through POST /v1/appPriceSchedules because fastlane's price_tier is broken against the current API; and store distribution NEVER routes through the branch gate — a clean tree on main is the solo shipper's normal case (Step 0.9 loads the adapter BEFORE the gate, pinned by test with the non-Apple gate byte-unchanged and unique). Uploads/submissions follow an idempotency-log contract (inspect App Store Connect before any re-run). Non-Mac hosts get the honest split: build legs via a macOS CI runner with the minted key as a secret, API legs local. Browser use inside the journey is banned except the named paid-app banking/tax residue. Redaction dry-run clean. Ship's parity ratio raised 1.12 -> 1.22 deliberately: the 14.8KB section is on-demand (Apple store targets only), one manifest line otherwise. Ported from time-attack/gstack (GStack 2), refined across its 21 live releases; architecture adaptation (carved section, decision-store paths, idempotency log, third-party-actions handoff) ours. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(code-intelligence): provider contract Phase 1 — GBrain, Sourcebot, Graphify behind one ask-once offer Open a large repo (1,000+ tracked files) and gstack can offer code intelligence ONCE, with the trade-offs stated: GBrain (semantic memory + code, sends content to YOUR gbrain DB, per-repo consent), Sourcebot (self-hosted whole-repo search, local on localhost), Graphify (local tree-sitter graph, nothing leaves the machine, user-installed), or No indexing — a decline persists machine-wide so no skill ever asks again. Small repos never see the question; grep stays the always-working default and provider-OFF degrades silently (PROVIDER_UNAVAILABLE -> file-only). Ported: lib/code-intelligence/ (contract + 3 verified adapters + picker + selection + suggest, MIT headers), the gstack-code-intelligence CLI (suggest/select/consent/index/search/status), 31 offline tests (fake CLI shims + injected fetch), and the provider-contract design doc. Verified live on this repo: suggest fires at 1,233 files with real availability detail per provider. Hardened per review: the per-remote trust store is the SINGLE consent authority — a gstack-gbrain-repo-policy deny tier vetoes any recorded code-intelligence consent (fail-closed on an unreadable store, pinned by three tests); both send-capable adapters are registered as fail-closed MODULE_SINKS in the egress tripwire so a refactor can't drop their receipts; and local-compute vs remote-send consents are never bundled. setup-gbrain gains the provider-choice Step 0. The fork's Phases 2-4 glue-collapse is explicitly NOT ported. Ported from time-attack/gstack (GStack 2); consent unification ours. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ci): supply-chain hygiene — secret gate on every PR diff, dependency review, OSV, dependabot, evidence-bar PR template The repo owned a redaction engine and had zero CI-side secret scanning. quality-gate.yml now pipes every PR diff's ADDED lines through our own bin/gstack-redact (gate-secret-scan.mjs, taken from the fork — it dogfoods the engine): HIGH findings fail the check, MEDIUM prints an advisory count only (no human in CI to confirm), planted-bug fixtures excluded by pathspec. Live-verified both directions: PEM key fails, clean diff and MEDIUM shapes pass; ShellCheck (errors) covers the setup/build shell boundary and passes today; bun audit gates critical advisories. Trigger is pull_request, never pull_request_target. dependency-review.yml adopts the hardened never-merged prior-art branch (fail-on-severity high, workflow paths watched, tight perms) — verify the dependency graph parses bun.lock with a canary bump before trusting the gate. dependabot: weekly, grouped per ecosystem, capped PR counts; and evals.yml image build/push now skips dependabot actors, whose read-only GITHUB_TOKEN made every lockfile bump a permanently red check. OSV scans weekly with a reasoned ignore file. All new workflow actions SHA-pinned. Scorecard deliberately not taken (no consumer for the score). The PR template front-loads the evidence bar (live proof, liveness screenshot, no-ETHOS/voice-changes checklist); the unenforced DCO line is dropped. bin/gstack-verify-gate ships OPT-IN (never registered by ./setup — a Stop hook running the project's verify command after every turn is the user's call), with the fork's tests adapted to pin exactly that. Ported from time-attack/gstack (GStack 2) + our own prior-art branch. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: remove dead bins; extend the stale-ref scan to docs (the 36-release gap) bin/chrome-cdp, bin/gstack-open-url, and bin/gstack-platform-detect were referenced only by an audit test and CHANGELOG history — dead weight that the stale-ref scanner should police, which required removing them FIRST. The scanner now also sweeps docs/, README.md, and USING_GBRAIN_WITH_GSTACK — the deliberate exclusion that let a dead command survive ~36 releases as a command-not-found instruction. Scan is green on the extended surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bins): detect the default branch instead of hardcoding main gstack-diff-scope fell to an empty diff (all-false SCOPE_*) and gstack-next-version mis-based its bump math on any repo whose default branch isn't main (trunk, master, local-only). Both now resolve origin/HEAD -> origin/main -> origin/master -> main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: housekeeping sweep — telemetry integrity, persistent opt-out, context-bill accuracy, setup hang, dev-server discovery, model resolution (#2136 + v1.63 polish) Seven small fixes, one theme (claims matching code): - telemetry-sync strips local-only fields with jq del() (structural) instead of quote-fragile sed regexes; unparseable lines are dropped, never forwarded unstripped. Sed survives only as a jq-less fallback. - telemetry-log rejects non-integer durations BEFORE the range caps, whose test(1) comparisons silently no-op on non-numerics — a malformed duration spliced raw text into the JSONL stream. - browse's local telemetry honors the persistent tier (config.yaml telemetry: off), not just the preamble's env hint — direct $B use and embedders now respect the opt-out. - gstack-context-bill --exact sees GSTACK_-promoted keys inside Conductor (conductor-env-shim wired at the CLI entry), and the TOTAL line no longer double-counts every nested skill through the root skill's walk (v1.63 deferred polish; the telemetry-sync HTTP-status outcome deferred alongside it turned out already shipped). - setup's Chromium probe is deadline-bounded (90s, background + poll-kill — macOS has no GNU timeout) and prefers Node for the launch probe everywhere (the bun --eval hang family behind #2136); the install is single-flight behind a lock dir with an actionable stale-lock message. Probe verified live on this Mac. - the review resolver's dev-server check reads CLAUDE.md and the plan file before falling back to an expanded port probe, and says how to make itself smarter next time. - eval/harness model IDs resolve through lib/eval-model.ts (GSTACK_EVAL_MODEL[_KIND] env overrides, per-kind defaults, tested) at the SDK-capture and PTY-warmup sites; the bash-embedded distill snippet mirrors the resolution inline. - memory-ingest's silent-zero shape (staged>0, imported+unchanged==0, errors==0) warns even under --quiet — a run that indexes nothing must never look healthy again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: wire ios-qa/daemon/test into the free suite and shard runner (E2) The daemon's 5 test files (allowlist, audit, auth-mint, cli-mint, daemon-integration — now 6 with session hardening) were invisible to every runner: not in the bun test glob, not in TEST_ROOTS. The same silent-coverage-hole class as the tracked design/test P2 — and it meant B2's auth regression tests would never have gated. All files are hermetic (stub state-servers on ephemeral ports, no devices); verified green in the shard census. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): claimed limitations now require evidence, everywhere + wave follow-ups filed Every tier-2+ skill's preamble gains one directive distilled from nine live release failures in two days on the fork: a claimed limitation or requirement ('the API can't do this', 'X requires a credential', 'impossible on this platform') is a material claim, stated only with the verbatim error, the documented statement, or a live probe in hand — pattern-matching a failure to a familiar story is not evidence, and a cheap probe runs BEFORE asking the user or declaring a step blocked. ONE directive adapted into the preamble resolver; the fork's full judgment contract is deliberately not imported. Full regen (46 files), ship goldens refreshed, parity guards bumped with the measured ~0.45KB/skill (investigate, autoplan, plan-design-review, office-hours), Step 0.9 registered as an intentional sub-step. Approved deferrals filed: persona-fleet hostile-user harness + answer-key methodology in TODOS; the fork's question-budget ACCOUNTING judgment (never its 5/8/12 constants) folded into the V1.1 pacing design doc; the Apple adapter added to #1882's coverage note. Ported from time-attack/gstack (GStack 2). Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(make-pdf): close offline-gate bypasses via unquoted style attrs, CSS-escape and HTML-entity obfuscation Three live vectors found by the ship review army, all red-first tested: unquoted style attributes skipped the remote-url neutralizer entirely; CSS ident/string escapes (@\69mport, url(\68ttps://…)) defeated the literal-match patterns Chromium happily decodes; and HTML entities in style attribute values (https) decoded to fetchable schemes before CSS parsing. Style-attr values are now entity-decoded in one browser- faithful pass, escape-bearing at-rules and function tokens are dropped fail-closed, and output is re-encoded double-quoted. 21 new test rows. * fix(migrations): v1.65 Chromium re-fetch actually re-downloads, and success is verified before .done The migration (renamed from the provisional v1.64.0.0 slot, which open PR #2564 claims) deleted only the poisoned .app while Playwright's INSTALLATION_COMPLETE marker survived in the revision dir — so the advertised 'bunx playwright install chromium' re-fetch no-opped and the user finished the upgrade with no browser and a success message. Now: the whole chromium-<rev> dir goes, bunx runs cwd-pinned to the install root, .done is gated on a verified executable, and a needs-refetch sentinel makes re-runs retry a failed download. Stranded rev dirs (markers without .app) also re-trigger. 6 hermetic tests, red-first. * fix(migrations): v1.27 remediation prints a real command instead of a fictional flag Every skip/failure path referenced '/setup-gbrain --rerun-migration', which is implemented nowhere, and promised the migration 'will ask again next upgrade', which the version-window runners make false. All five sites now print the direct GSTACK_MIGRATE_ASSUME_YES=1 bash invocation. Runner-side re-offer tracking is filed in TODOS. * fix(browse): poisoned-bundle self-heal removes the revision dir, probes handoff too, and throws typed Same marker flaw as the migration: rmSync of the .app alone left INSTALLATION_COMPLETE behind, so the error message's own remediation no-opped and the user was hard-stuck. The probe is now an exported, unit-tested helper (probePoisonedChromiumBundle) that removes the whole chromium-<rev> dir, never touches GSTACK_CHROMIUM_PATH custom bundles, throws PoisonedBundleError (instanceof, not string-match), and runs on BOTH headed entry points — launchHeaded and handoff. 7 tests. * fix(browse): session snapshots are atomic and the cookie filter drops loopback IP literals A crash mid-write destroyed the previous good snapshot — the exact scenario persistence exists to survive; writes now go tmp+rename. The internal-network cookie filter gains 127.*/::1/169.254.* (a tampered state file could previously hand loopback-service cookies back to the browser), and 'state load' imports the shared filter instead of maintaining a comment-synced copy. Test cleanup made exception-safe. * fix(browse): server runtime — restore off the boot path, shutdown that cannot hang, watchdog that still reaps tunnels Four review findings on the wave's own new wiring: session restore ran before Bun.serve with sequential 15s gotos while the CLI gives up at 8s (one slow saved URL bricked every $B command) — restore now runs in the background after bind; the shutdown snapshot gets a 2s deadline so a wedged page.evaluate can't hold the port forever behind the new ack-first stop; the persistence ticker gets in-flight + shutdown gates and is cleared before the final snapshot; and the absorbed #2565 handoff fix no longer clears the whole parent watchdog — a suppress flag keeps the tunnel-orphan reaper alive (handoff→resume→tunnel is no longer an unreapable internet-exposed daemon). pair-agent with consent off now names the real remedy instead of ngrok install instructions. Lock-acquisition edge branches (garbage pidfile, vanish-race depth cap) pinned. * fix(browse): telemetry defaults to off like every other surface The persistent tier defaulted ON when the config key was absent, while gstack-config's DEFAULTS table answers 'off' for the same question — preamble-spawned daemons and direct $B daemons disagreed about consent. Absent key/file now means disabled; community/anonymous enable; env kill-switch still beats everything. Both config.yaml consumers now share one readGstackConfigYamlKey reader. 12-case consent suite. * fix(code-intelligence): consent that means what it says — polarity, receipts, read-only veto Four review findings on the wave's own Phase 1 port, all red-first: 'consent <repo> no' recorded consent GRANTED (the CLI ignored the argument and always wrote true) — yes|no is now required and garbage records nothing; Sourcebot egress receipts claimed consented=true on paths that never checked consent — the actual consent state is threaded into every receipt, search is fail-closed on non-loopback, and the liveness probe's receipt says truthfully that it sends no repo content; repoPolicyVeto only honored the deny tier while gbrain refresh writes pages — write-class ops now veto on read-only too, matching the sync chokepoint, via one shared lib/gbrain-repo-policy-client.ts (win32 bash invocation, spawn-vs-unreadable error distinction) used by both call sites. Also: source ids get a host+path hash (same-name repos no longer collide), refresh timeout raised to 120s, availability probes run concurrently at 3s, graphify status stops JSON.parsing 100MB graphs for a count, and every ported file carries the fork MIT notice. +15 tests across the two suites. * fix(verify-gate): trust before eval, re-check on re-entry, audit every grant The opt-in Stop hook eval'd whatever command the first CLAUDE.md up the tree declared — any cloned repo got arbitrary shell at turn end. Now a per-repo trust store (path+command hash, 0600) gates execution: an untrusted or changed command never runs (exit 0 with the --trust invocation printed), stop_hook_active re-entry re-runs the trusted check instead of rubber-stamping (bounded at 3 blocks per episode), and every grant appends a forensic line to ~/.gstack/security/verify-gate-trust-grants.jsonl. 20 tests, red-first. * fix(setup): EXIT traps chain instead of clobbering; timed-out probes reap their whole tree The Playwright-lock trap replaced the copied-bun cleanup trap and then cleared ALL exit handling, leaking .tmp-bun-bin on every Chromium install; and _wait_with_deadline killed only the subshell, orphaning the wedged node→Chromium tree it exists to escape — re-creating the #2136 pile-up on every timed-out re-run. Traps now chain; timeouts walk pgrep -P descendants leaves-first. * refactor(resolvers): one source for the design-doc discovery block The #703 repo-doc-preference bash was pasted byte-identically into three plan-review templates and a fourth copy embedded in review.ts — drift there means plan reviews disagree about which design doc wins. Now a {{DESIGN_DOC_DISCOVERY}} resolver; generated output is byte-identical, so no SKILL.md changes ride along. * fix(ship): finish the Apple upload idempotency sentence The durable-effect contract dropped its consequence clause mid-sentence — the instruction for what to DO when the idempotency key already exists (treat the upload as possibly-done, never re-run it) was missing from the one rule governing whether a binary uploads twice. * fix(ci): SHA-pin dependency-review; the secret gate fails closed without a report dependency-review.yml rode mutable refs (@v4 resolves to a BRANCH on that repo) inside the one workflow whose job is supply-chain hygiene — now commit-pinned like its siblings, with dependabot keeping the pins fresh. gate-secret-scan.mjs crashed with an unhandled EPIPE on oversize diffs (the designed report.oversize branch was unreachable: the scanner emits no JSON on refusal) — the pipe write now tolerates early exit and a missing report is an explicit fail-closed exit 1. Oversize + broken-scanner legs pinned. * fix(bins): Windows-safe GIT_CEILING join; next-version probes the full default-base chain GIT_CEILING_DIRECTORIES was joined with ':' — git on Windows splits on ';' and drive letters contain ':', silently disabling the #2144 second-layer defense there; now path.delimiter. next-version's default-base detection only tried origin/HEAD then 'main', diverging from the canonical 4-step chain diff-scope uses — origin/main and origin/master probes added, pinned by fixture repos. * fix(eval-model): kinds are a literal union, not string Record<string,string> widened EvalModelKind to string, so a typo'd kind only failed at runtime; as const satisfies keeps the closed set the doc comment promises. * test: coverage backfill from the ship review The telemetry-strip invariant only validated the sed FALLBACK while the live jq path went unchecked — the jq del() lists are now held to the same every-emitted-field bar, plus a behavioral pipe-through. The context-bill nested-skill double-count fix gets a regression pin (a revert shipped green before). The windowsHide tripwire gains terminal-agent-control.ts — the exact file the fix commit names. The ios-qa revoke-by-token_id branch gets its negative case: unknown ids revoke nothing and leave live sessions alone. * docs: SLATE_HOST no longer cites the deleted platform-detect bin Host detection lives in the hosts/ registry via host-config-export.ts; the doc's known-gaps list now says so instead of pointing at a bin this branch removed. * test(e2e): headroom for the two plan-ceo-review budget-edge tests Both rode their 360s runner budget at the edge (main clears at 243s of 360s), and the wave legitimately adds work to the review: the evidence directive tells the agent to probe before claiming, and the design-doc discovery block adds bash steps. Under concurrent in-file children the API queuing tipped all retry attempts past the ceiling — the runner then reports $0.00/0 turns for a timed-out child, which reads like a dead spawn but is a healthy child killed at the deadline. 540s runner / 660s test for these two only; verified 2/2 green at 228s and 315s. * fix(code-intelligence): gbrain search/export are consent-gated and receipted The Sourcebot side got this in the last round; gbrain had the same hole — search() and export() sent repo-derived query text into a possibly-remote DATABASE_URL with no consent check and no egress receipt, bypassing the deny-tier veto. Both now assert consent before any bytes move, receipts record the actual consent state (never a hardcoded true), and search receipts carry the query's sha256. gbrain stays fail-closed: the adapter cannot see where DATABASE_URL points, so every send requires consent. 7 new tests, red-first. * fix(make-pdf): SVG remote refs and image-set can no longer fetch offline <svg><image href=https://…> and <use xlink:href=…> survived the gate (only javascript: schemes were stripped from svg hrefs), and bare-string image-set("https://…" 1x) dodged the url()-shaped neutralizer. Remote svg hrefs rewrite to '#' (entity-decode-aware, unclosed-svg smuggle closed) and remote image-set args neutralize to url(#). Local fragments, local image-set, and plain <a> links pinned intact. 12 new rows, red-first. * fix(browse): duplicate config keys read last-wins, matching gstack-config readGstackConfigYamlKey took the FIRST match while gstack-config's get takes the LAST — a duplicated pair_agent or telemetry line made the two consent surfaces disagree about what the user chose. * fix(setup): stale Chromium-install lock self-heals The mkdir mutex had no owner: a SIGKILL'd setup left the lock behind and every later run exited with manual rmdir instructions. The holder pid is recorded in the lock; a dead holder is reclaimed automatically. * fix(setup-gbrain): the code-intelligence offer gate skips when the bin is absent The new Step 1.7 told the agent to run gstack-code-intelligence before the path pick — on installs predating the CLI (and hermetic E2E children) the bin doesn't exist and setup derailed before doing any setup. The gate now probes for the bin and reports offer:false reason:bin-absent, with explicit instructions to proceed: the user asked for gbrain, so set up gbrain. Never block setup on an optional gate. * test(e2e): periodic-tier repairs from the failure triage Each fix traces to a receipt: brain-privacy-gate staged config never reached the hermetic child (ambient GSTACK_HOME is scrubbed) and the operator's remote-mode gbrain suppressed the gate — both now injected per-test; ship-idempotency threw away its evidence on the timeout path and ran a 600s budget its own subject can exceed (now 900s, evidence captured); auto-decide-preserved gets the same headroom its sibling plan-ceo tests got; context-skills' hides-checks scanned bash output where an ls legitimately names old checkpoints (final-text scope now); design names the missing section instead of a bare count and learns the easing/duration/micro-interaction synonyms; qa-workflow's collector afterAll gets an explicit 60s hook timeout. * fix(eval-harness): eng-review phase boundary fires on qid-tagged questions The Step 0 boundary only matched two prose phrases, but plan-eng-review may legitimately reach the review phase without either — every per-finding AskUserQuestion then counted as pre-review and the batching regression test read 0 questions while watching the agent ask them one by one. The boundary now also fires on the first answered question carrying a gstack-qid:eng-review- marker. Additive only; 119 runner unit tests green. * chore: bump version and changelog (v1.65.0.0) Fork port wave 2: the release-summary entry credits Sina Matian (time-attack/gstack) and the four absorbed community PRs. TODOS gains three review-round follow-ups (dual-write E2E, migration runner re-offer, gbrain-adapter op coverage). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(eval-harness): eng-review qid boundary matches the real skill-name prefix Live qids render as gstack-qid:plan-eng-review-<slug> ({skill}-{slug} convention); the boundary anchored eng-review- immediately after the colon and never matched, leaving the batching counter blind while the transcript showed per-finding questions being asked one by one. * fix(setup-gbrain): never ask the provider question inside /setup-gbrain Invoking /setup-gbrain IS the provider choice. Step 1.7 now records 'select gbrain' best-effort and proceeds straight to setup; the offer ceremony is reserved for entry points where no provider was named. On machines where the code-intelligence CLI exists, the offer:true path was hijacking setup into the provider ceremony and the E2E child never reached MCP registration. * chore: file the three documented-red periodic tests as structural-repair TODOs Sidebar trio exercises endpoints removed on every tree; ship-idempotency's PTY child never receives its typed command; brain-privacy-gate has never been green anywhere. Each carries its triage receipt in the entry. * test(e2e): setup-gbrain remote — hermetic env via opts, evidence on failure, output-scoped classifier Three separate defects stacked on this one test: the ambient GBRAIN_MCP_TOKEN/GSTACK_HOME/PATH mutations never reached the child (hermetic-env scrubs them by allowlist — broken since hermetic env landed; the child correctly stopped at Step 4c with NEEDS_CONTEXT), failures discarded the in-memory transcript so every triage started blind, and the wrote-findings-before-asking classifier scanned the full event stream where the child's own Read of the skill file always contains the review-report phrase. Env now goes via opts.env, failures dump bash commands + final text, and the classifier scans assistant output only. Green in 67s with all seven asserts. * test: final coverage pass — CLI rendering, revert traps, keychain probe, gbrain doc ops The user-directed third generation pass closes the audit's remaining tail: the code-intelligence CLI's options/status/suggest surfaces get behavioral coverage through the fake-shim chain; brain-context-load gains an argv-logging trap that goes red if anyone reverts the memoized PATH scan back to the spawn probe (receipt: simulated revert failed exactly these tests); the darwin Keychain auth branch (#1890) gets its first free-tier tests via a PATH-shimmed security binary; and the gbrain add/delete/export ops are pinned (body piped byte-for-byte, receipt sha256, stdin-EOF prompt guard, PROVIDER_UNAVAILABLE degradation) — retiring their TODOS entry. * test: assemble the planted PEM at runtime so the fixture never trips the prepush guard The repo's own credential guard scans pushed diffs and correctly blocked these fixtures: the engine flags any one-line BEGIN…END spelling regardless of body. Header, body, and footer are now joined at runtime, so the file and every diff of it stay clean while the scanner under test still receives the true live shape. * docs: update project documentation for v1.65.0.0 README gains the two wave-2 CLIs (gstack-code-intelligence, gstack-verify-gate) in the standalone-binaries table, BROWSER.md documents BROWSE_PERSIST_STATE next to manual state save/load, CONTRIBUTING's CI section lists the new supply-chain gates, and CLAUDE.md's project tree reflects lib/code-intelligence/ and the added workflows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: apply cross-model doc-review fixes for v1.65.0.0 Findings from the release doc review, verified against source: verify-gate's README row gains the actual install one-liner (setup never registers the Stop hook; test/verify-gate.test.ts pins that) and the 3-blocked-re-entries yield behavior; code-intelligence's row gains the suggest subcommand and the search-side consent gate; CONTRIBUTING scopes the SHA-pin claim to the supply-chain workflows and widens the dependency-review trigger; BROWSER.md's restore-time cookie drop list matches isInternalCookieDomain; CLAUDE.md's workflows comment stops implying six workflows are all of them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CHANGELOG accuracy pass — scope the SHA-pin claim, restore-time cookie filter, exact test counts * test: env restore runs per-test, not per-suite — the leak that failed 30 strangers gstack-memory-helpers saved HOME/GSTACK_HOME/PATH in beforeEach but restored in afterAll, so the last beforeEach's snapshot won and a gstack-test-engine temp dir leaked into every later file in the same process: gstack-config read the wrong store, make-pdf's child resolved Chromium under the temp cache, update-check and artifacts-init lost their real homes. afterAll is now afterEach; the config and update-check harnesses also strip GSTACK_HOME/GSTACK_STATE_ROOT from child env as a belt. * fix(browse): restore the #1846 start-timeout resolution the merge dropped The v1.64.1.0 merge kept this branch's lock design in cli.ts and silently lost main's resolveStartTimeout + late health re-check while their test survived — ported both back in alongside the kept design. * test: adapt main's diagnostics tests to the merged designs cli-lock asserts typed ServerLockError (errno + lock path) instead of the log-and-return shape the merge didn't keep, dropping only the one duplicate of server-lock-errors coverage; the liveness tripwire exempts error-handling.ts as the sanctioned tasklist site; snapshot and compare-board wrappers pass the now-mandatory browser-manager arg; background.js's test pins that the retired sidebar-command type is rejected pre-gate with no response fields. * chore: gitignore the gen-accessors tool's SPM build output skill-e2e-ios-swift-build compiles the Swift package in place, leaving .build/ (2,800+ files) and Package.resolved untracked after every periodic run — the workspace read as ~100 dirty changes with a clean tree. Same class as the dist/ binaries: build output, never committed. * test(browse): subprocess budget for the polyfill suite on Windows CI Every test here spawnSync's a node child; cold-start on the Windows runner (AV scan, first node.exe touch) blew bun's 5s default by 7ms on a 50ms sleep test. File-level 20s default — subprocess budget, not assertion looseness. * test: make the Darwin migration path and the query-timeout SKIP deterministic on Linux CI The v1.65 migration suite relied on the host being macOS — on the ubicloud runner the script's uname gate early-exited every test with empty output; a Darwin uname shim in the shared setup runs the real path everywhere (the non-Darwin test still overrides it with Linux). The 1ms-budget brain-context test assumed 1ms is always too short; the runner's fake gbrain answered in 0ms and no SKIP printed — the fake now sleeps 300ms so the timeout is a certainty, while --version stays instant for the detection assertion. --------- Co-authored-by: Gawie van Blerk <gawievanblerk@gmail.com> Co-authored-by: Sina Matian <sina@time-attack.dev> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Shawn Reddy <19191746+Screddyice@users.noreply.github.com> Co-authored-by: Jake Wilk <jwilk@highlinerepartners.com> Co-authored-by: Jerry Nichols <jerrynicholsai@users.noreply.github.com> |
||
|
|
1d41ee3ab3 |
v1.63.0.0 feat: GStack 2 fork port wave — egress receipts, context-bill, sharded gate, /health fix (#2541)
* 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
|
||
|
|
94993f7401 |
v1.61.0.0 fix wave: guards failing open / silent failures (9 fixes, 4 community PRs absorbed) (#2472)
* fix(careful): warn on chained rm even when the last target is safe The safe-exception block whitelisted rm -rf of build artifacts by extracting targets with a single greedy match (.*rm ...), which only ever inspects the LAST rm in the command. A chain like 'rm -rf /; rm -rf node_modules' was therefore judged solely by its trailing safe target and allowed without warning, waving through the destructive 'rm -rf /'. Gate the shortcut to single rm invocations: when any shell separator (; | & newline, incl. JSON-escaped \n/\r from the grep extraction path) is present, fall through to the destructive-pattern check, which warns on any recursive rm. Single-command artifact cleanups still allow. Adds 3 regression tests covering semicolon and && chains in both orders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(careful): substitution separators + capital -R recursive flag (#2039) Two residual fail-opens in the same guard PR #2040 hardened, both verified by executing the script pre-fix: - rm -rf $(./wipe-all)/node_modules silently allowed: the substitution token ends in a whitelisted suffix and the safe-exception early exit skipped ALL downstream checks. $( and backtick now count as chain separators; plain $VAR expansion stays allowed. - rm -R / silently allowed: both greps required a lowercase r in the flag cluster; capital -R is the documented BSD/macOS recursive flag. Both greps now match -[a-zA-Z]*[rR]. Six new tests: substitution x2 -> ask, capital-R x2 -> ask, rm -Rf node_modules single-command -> still allowed, escaped-newline branch (existing code, previously untested), and a pinned deliberate FP (cd app && rm -rf node_modules -> ask) documenting the fail-closed direction on chains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(context-restore): prefer the current branch's own checkpoint (#2052) All worktrees of a repo share one origin-derived slug, so they share one `~/.gstack/projects/<slug>/checkpoints/` dir. `/context-restore` loaded the newest checkpoint across the whole dir, so in one worktree it could silently restore a *sibling worktree's* newer checkpoint. Step 1 now orders candidates current-branch-first (read from each file's `branch:` frontmatter), keeping other branches as a fallback. A branch is checked out in at most one worktree, so this stops cross-worktree contamination while preserving Conductor cross-branch handoff: when the current branch has no checkpoint of its own, the full newest-first set is still used. - scan the 200 newest before partitioning so a current-branch checkpoint sitting below a burst of sibling saves is still found; output still capped at 20 - non-git / detached HEAD / branchless legacy saves fall back to the old newest-first behavior (back-compat) - +5 regression tests in context-save-hardening.test.ts (the #2052 bug case fails on the old pipeline); regenerated SKILL.md + proactive-suggestions.json Fixes #2052 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gbrain): pass --confirm-destructive on drift re-register (#1985) ensureSourceRegistered() handles match-but-different-path by removing the old source then re-adding it at the new path. The remove was issued as `gbrain sources remove <id> --yes`, but gbrain >= 0.42 gates `sources remove` behind `--confirm-destructive` (`--yes` alone no longer suppresses the data-loss prompt). The remove therefore fails with "To proceed, pass --confirm-destructive", which ensureSourceRegistered surfaces as "source registration failed" — aborting the entire /sync-gbrain code stage for any already-registered source whose path has drifted. The memory and brain-sync stages still pass, so the code index silently stops refreshing. The orchestrator's own safeSourcesRemove() already passes --confirm-destructive; this brings the lib helper in line with that convention. Keeps --yes for older gbrain. Tests: extend the fake gbrain shim in gbrain-sources.test.ts to simulate the gbrain >= 0.42 guard (remove without --confirm-destructive exits 1), update the drift re-register assertion, and add a regression test that proves the drift path no longer throws. Both fail on main with the exact "To proceed, pass --confirm-destructive" error and pass with the fix. Fixes #1985 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(gbrain-sources): route drift remove through #1734 guards + realpath drift check Absorbing #2031 un-blocked a destructive remove that bypassed the #1734 data-loss guards: ensureSourceRegistered's drift path issued `gbrain sources remove` directly, without the detectAutopilot + decideSourceRemove checks every other remove routes through via safeSourcesRemove. gbrain >= 0.42's own prompt was accidentally blocking that path; with --confirm-destructive passed it is live again. - Drift remove now refuses LOUDLY (throws, actionable message) while an autopilot is active or when decideSourceRemove disallows; a silent changed=false would hide the drifted registration. - decideSourceRemove's extraArgs (--keep-storage when supported) propagate to the remove call, matching safeSourcesRemove. - Drift is realpath-normalized before being declared: a symlink alias of the same directory (macOS /tmp -> /private/tmp) is a match, not drift — the probable cause of #1985's reporter hitting the remove on an unmoved repo. - Drift fires a loud stderr line (old -> new path); perpetual drift in logs is the trigger for promoting #1985's reindex-in-place design. Tests: autopilot-active refusal (no remove in call log), fail-closed refusal on unreadable sources list, --keep-storage propagation, symlink-alias no-drift; existing drift tests pin the guard probes so a live autopilot on the dev machine can't flip them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(developer-profile): exclude mode:resources rows from SESSION_COUNT, TIER, NUDGE_ELIGIBLE (#2067) Every /office-hours run appends a mode:"resources" bookkeeping row alongside the real session row, so --read double-counted sessions (~2x): tiers promoted early and the builder-to-founder nudge armed prematurely. The file already filtered resources rows for LAST_*/CROSS_PROJECT; the same realSessions filter now feeds SESSION_COUNT/TIER, and the nudge predicate is the faithful allowlist (mode === 'builder') so a future mode #4 fails closed instead of re-opening this bug. 8 regression tests: count vs resources noise, tier boundaries both sides, nudge false-with-noise / true-at-3-builders, cross-project trailing row. Absorbed from PR #1991 by @mvann (fix + tests commits; the PR's version-bump commit is superseded by this wave's consolidated release commit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hooks): passThrough() two-branch contract — never emit permissionDecision:'defer' (#2035, #2006) Every AskUserQuestion died with "Tool result missing due to internal error" on current Claude Code builds (Desktop 1.14271.0, CC 2.1.177). Root cause: the question-preference-hook emitted permissionDecision:'defer' on every pass-through path. 'defer' is a real PreToolUse value, but since CC v2.1.89 its semantics are "pause this tool call for external resumption" (headless resume) — never "abstain". Interactive sessions have nothing to resume the paused call, so the tool orphaned. Pre-2.1.89 builds ignored the unknown value, which is why the hook worked when it shipped and broke later. The fix is the two-branch pass-through contract: - no context -> exit 0 with EXACTLY empty stdout - memory nuggets present -> hookSpecificOutput with hookEventName + additionalContext ONLY (the documented shape; plan-tune Layer 8 memory injection ships through this branch and keeps working) defer() is renamed passThrough() so the function says what it does, and docs/spikes/claude-code-hook-mutation.md's protocol contract (cited by the hook header) is corrected in the same commit — it taught '"defer" — let permission flow continue' and was the reintroduction vector. Test contract rewritten in the same commit (13 assertions across 3 files, verified fail-first against the unfixed hook): pass-through paths assert exact-empty stdout (a garbage/partial write cannot slip past an optional-chained parse), the nugget path asserts permissionDecision is ABSENT while additionalContext survives, and a new tripwire asserts no non-deny path ever puts the string "permissionDecision" on stdout. The deny (auto-decide) and Conductor prose-redirect paths are unchanged. Deployment: no migration needed — settings.json points at the absolute bash shim which execs the .ts live; /gstack-upgrade delivers the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(one-way-doors): unify credential noun net + wire it into the runtime (#2024) Library fix: revoke/reset/rotate now share ONE noun alternation (api key, token, secret, credential, access key, password) with optional plural s?. Pre-fix leaks: "reset my secret", "reset my access key", "revoke my secret" (mismatched per-verb lists) and every plural form ("rotate the credentials", "revoke all tokens" — \b(...)\b cannot match a trailing s). Runtime wiring — the regexes could never fire in production before: - gstack-question-preference --check gains --summary-stdin: the question text pipes via stdin (never argv — summaries carry quotes/newlines/shell metacharacters) and feeds isOneWayDoor alongside the id, so an ad-hoc destructive question with a stored never-ask preference now forces ASK_NORMALLY. Empty/absent stdin keeps exact id-only semantics. - question-preference-hook falls back to classifyQuestion(question text) when the registry lookup misses, so unregistered destructive questions pass through to a human instead of auto-deciding. - question-tuning resolver prose shows the piped form (SKILL.md regen lands in the wave's release commit). Tripwires (verified fail-first): full verbs x nouns x singular/plural matrix with the #2024 repro rows, benign-summary no-over-match rows, stdin transport survival (quotes/newlines), empty-stdin fail-safe, and hook fallback both directions (destructive -> pass-through, benign -> deny). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(design): loud integer-flag contract for --count/--retry/--timeout (#2032) design variants --count abc silently generated ZERO variants and exited 0: parseInt(NaN) flowed through Math.min into the generation loop bound. The same NaN class was live on the two sibling flags in the same file: --retry abc made generate() a silent no-op (attempt <= NaN never true, null output, exit 0) and --timeout abc killed the serve board ~immediately (setTimeout(NaN)). New design/src/flag-utils.ts: parseIntFlag (pure, unit-testable) + normalizeIntFlag (CLI wrapper). Contract matches the --viewports precedent (error loudly on nonsense — these commands spend real image-API money, a silent fixup hides typos from calling agents): undefined -> default; bare flag/empty/non-integer ("3.7" rejected, not truncated)/below-min -> exit 1 with usage hint; above-max -> clamp with stderr warning. --count normalizes at the variants() consumption site so programmatic callers are covered, with the ceiling derived from STYLE_VARIATIONS.length instead of a magic 7; the CLI passes the raw flag through (a pre-parseInt would truncate "3.7"). Tripwires live in test/design-flag-utils.test.ts — deliberately under test/, not design/test/, which is invisible to the bun test glob, TEST_ROOTS, and every workflow (wiring design/test/ into CI is a captured TODO). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gbrain): thin-client state — remote-MCP brains no longer classify as broken-config (#2051) A thin client (remote-HTTP MCP brain, no local engine by design) probed `gbrain sources list`, which gbrain's dispatch guard REFUSES on thin clients (exit 1, no recognized error string), so the classifier fell to its defensive broken-config default and every suppression gate silently hid brain-aware blocks from exactly the users on a shared team brain. New 'thin-client' state, detected PRE-probe from gbrain's own remote_mcp config marker via the existing gbrainConfigPath() helper (mirrors gbrain's isThinClient(); honors GBRAIN_HOME; zero network, immune to error-string drift), with a /thin[- ]client/ stderr backstop in the probe catch. Remote reachability is deliberately NOT probed by the classifier — that is the #1964 pathology; gbrain calls degrade gracefully at use time, and the detect JSON says so honestly (gbrain_thin_client: {probed: false}). The state is admitted at every suppression gate — gstack-gbrain-detect --is-ok (drives setup + gbrain-refresh), gen-skill-docs' detection override, gstack-config gbrain-refresh — while the sync stages (code/memory/dream) SKIP with an accurate reason: code indexing runs on the brain server, memory syncs via the remote brain's artifacts pull. The two consumer classes need opposite answers, which is why this is a distinct state and not a skip-the-probe special case. sync-gbrain Step 1.5 and setup-gbrain prose route thin-client to proceed, never into broken-config remediation. detectMcpMode secondary generalization: url-match against the config's remote_mcp.mcp_url (deterministic — gbrain mounts at the generic /mcp path) -> name pattern gbrain[-_]* -> stdio command token; gbrain_mcp_mode stays a 3-value enum. Tripwires: end-to-end --is-ok exits 0 on a thin-client fixture AND still exits 1 on broken-config (the gate didn't widen); pre-probe + stderr-fallback classifier paths; 4 detectMcpMode identification cases incl. a non-matching url that must NOT false-positive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * release: v1.60.0.0 — regen SKILL.md, VERSION, CHANGELOG, TODOS follow-ups - Regenerate all SKILL.md from templates (question-tuning --summary-stdin prose from #2024, context-restore branch preference from PR #2054, sync-gbrain/setup-gbrain thin-client prose from #2051) + llms.txt. - VERSION + package.json -> 1.60.0.0 (bin/gstack-next-version, queue-aware: #1815 claims 1.59.0.0, #2213 claims 1.59.1.0). - CHANGELOG release summary + itemized entry crediting @jbetala7 (x3) and @mvann. - TODOS.md: three eng-review follow-ups (design/test CI wiring + documented pre-existing retry-after flake, /context-save worktree identity, gbrain reindex-in-place conditional on the new drift log). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(resolvers): compress --summary-stdin preamble prose to fit parity budget; re-bless ship goldens The v1.57.7.0 parity suite caps investigate's generated size at 1.09x baseline; the #2024 question-tuning prose (duplicated into every tier->=2 skill) tipped it to 1.092. Compressed to a single inline command + short pointer (the full rationale lives in bin/gstack-question-preference's header and the one-way-doors module docs). Ship goldens re-blessed against the final resolver text (conscious template-change acknowledgment, per the golden-file regression contract). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(e2e): office-hours-spec-review turn budget fits the carved skill layout (#2473) The test failed deterministically with error_max_turns at 9 turns on main and this branch alike (CI attempt logs + local main repro). Root cause from the failing transcript: the Spec Review Loop content is carved out of office-hours/SKILL.md into office-hours/sections/, so the agent needs discovery hops (grep SKILL.md -> ls sections/ -> read the section) before it can write — 8 tool turns + the closing text turn = 9 > the 8-turn budget, which predates the carve. Observed failures wrote a CORRECT summary on tool turn 8 and died on the closing turn. maxTurns 8 -> 12. Verified: PASS locally post-fix (7 turns this run — the extra headroom absorbs discovery-path nondeterminism). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(e2e): review-dashboard-via session budget survives runner contention (#2473) The test failed on CI (and its baseline run) with the timeout signature: 0 turns, $0.00, exactly 183s, 3/3 attempts — the spawned claude -p session never emitted a single stream event before the 180s inner timeout. The file's tests run concurrently on one runner; session startup queues behind sibling sessions, and this test had the tightest budget in the file (the 240s-budget tests in the same job passed). A clean local run takes 270s wall for 4 turns, confirming 180s was too tight even without contention. Inner timeout 180s -> 300s; outer bun timeout 240s -> 360s to keep headroom over the inner budget. Verified: PASS locally post-fix (4 turns, 270s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(e2e): retro-base-branch session budget survives runner contention (#2473) Same class as review-dashboard-via, one test over in the same file: /retro is a long multi-step flow whose clean pass measures 225-239s — a coin flip against the 240s inner budget. First CI run passed at 225s; the rerun timed out at the 240s line on all 3 attempts (exitReason "timeout"); the local verification run passed at 239s, ONE second under the old cap. Inner timeout 240s -> 360s; outer bun timeout 300s -> 480s for headroom. Verified: PASS locally post-fix (17 turns, 239s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Michael Vann <9221873+mvann@users.noreply.github.com> |
||
|
|
cbf8a2bec2 | fix(gbrain): classify PGLite connect locks | ||
|
|
9fd03fae9e |
v1.58.4.0 fix: high-priority community bug wave + PTY plan-mode smoke gate (#2077)
* fix(gbrain): stop forcing GBRAIN_PREPARE on transaction-mode poolers (#1965) buildGbrainEnv auto-set GBRAIN_PREPARE=true whenever DATABASE_URL targeted port 6543, and the /sync-gbrain capability check exported it for the rest of the skill run. Both had the semantics inverted: gbrain auto-disables prepared statements on transaction-mode poolers because they break every write there ("prepared statement does not exist"); GBRAIN_PREPARE=true is gbrain's documented override for SESSION-mode poolers on 6543, not a requirement for transaction mode. The #1435 search symptom the auto-set worked around was fixed gbrain-side. Remove both force-sets. A caller-set GBRAIN_PREPARE (either value) still passes through untouched, preserving the session-mode-on-6543 escape hatch. isTransactionModePooler stays exported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gbrain): classify probe timeout as its own status; sync proceeds instead of skipping (#1964) The 5s engine probe misclassified healthy-but-slow engines (cold Supabase pooler connections measured at 6.9-10.7s) as broken-config, so /sync-gbrain silently skipped code+memory and told the user their config was malformed. - New "timeout" status: probe killed at the deadline with no recognized stderr pattern. Default deadline is now 15s, overridable via GSTACK_GBRAIN_PROBE_TIMEOUT_MS (tests set 300ms against a fake that sleeps 2s). - Sync stages PROCEED on timeout with a stderr warning naming the env knob; a genuinely-dead engine surfaces its real error at the first operation instead of a false config diagnosis. - Consistency everywhere "ok" gated behavior: gstack-gbrain-detect --is-ok exits 0 on timeout, and gen-skill-docs' detection gate accepts it, so a slow engine no longer silently suppresses brain-aware features. - Status cache: key now includes the effective probe timeout (raising it invalidates a cached timeout) and GBRAIN_HOME; config detection honors GBRAIN_HOME so relocated-home users stop being misclassified as missing-config. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bins): cygpath-normalize SCRIPT_DIR for bun imports; surface learnings-log errors (#1950) Under Windows git-bash, pwd yields a POSIX path (/c/Users/...) that Bun on Windows cannot resolve as an ES module specifier. gstack-learnings-log interpolates SCRIPT_DIR into a bun -e import, so every invocation died with "Cannot find module" — and 2>/dev/null swallowed the error, silently dropping every AI-logged learning for Windows users. - 3-line cygpath -m guard in gstack-learnings-log and gstack-question-log (which gains the same import shape in the next commit). Matches the duplicated IS_WINDOWS convention in setup; no shared shell lib exists. - learnings-log adopts question-log's set +e / TMPERR capture pattern wholesale: validation errors now print to stderr. The old `if [ $? -ne 0 ]` check was dead code under set -euo pipefail — the script exited at the failing assignment before reaching it. - New test/bin-windows-bun-import-paths.test.ts: static invariant (any bash bin interpolating $SCRIPT_DIR into a bun -e import must carry the guard) + behavioral end-to-end run invoked via `bash <bin>` — added to the windows-free-tests workflow list so the conversion is proven on the only platform where the bug exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(question-log): dedupe INJECTION_PATTERNS via lib/jsonl-store (#1934) bin/gstack-question-log carried a local copy of the injection-pattern list, so pattern fixes to lib/jsonl-store.ts never propagated — including the /override[:\s]/i false-positive fix arriving via community PR #1940. Import the shared hasInjection instead (enabled by the previous commit's cygpath guard). question-log also gets the lib's stricter superset (human:, disregard, from-now-on, approve-all patterns). Tests pin the contract in a #1940-order-independent way: an "Override: ignore all previous instructions" header is rejected, "prose overrides the deterministic table" is accepted, and a static invariant keeps local INJECTION_PATTERNS duplicates out of the bin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): community-pulse + both dashboards never report fake zeros (#1947) The security-signaling surface failed open at three layers — every failure mode read as a reassuring "0 attacks" / "0 installs": - community-pulse edge function: supabase-js returns {data,error} without throwing, and all five queries discarded `error` — a DB outage produced real-looking zeros via the SUCCESS path, and the catch (also returning zeros with HTTP 200) was unreachable for query failures. Every query now destructures and throws; the catch serves the stale cache (marked "stale": true) when one exists, else 503 {"error":"pulse_unavailable"}. Success responses carry "status":"ok" so clients can distinguish authoritative data from legacy backends. NOTE: the edge function deploys out-of-band (supabase functions deploy community-pulse). - gstack-security-dashboard: captures the HTTP status; non-200 / network failure / error body / missing section → "unknown — backend error"; jq missing → "unknown — install jq" (the lossy grep fallback broke on nested arrays and under-reported attacks as zero — removed); a 200 without the new marker shows figures with an "unverified (legacy backend)" note. Also fixes a latent display bug: the TOTAL grep matched the digit 7 inside "attacks_last_7_days" and misreported every count. - gstack-community-dashboard: same class — curl || echo "{}" plus grep || echo "0" printed "Weekly active installs: 0" on any failure. Now "unknown — backend error (HTTP N)". test/security-dashboard-fallback.test.ts pins the matrix (200+marker, 200-legacy, 503, network failure) x (jq present, jq absent) for both bins: "unknown" states never render as 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(telemetry): redact error_message spans before they leave the machine (#1947) error_message was uploaded with only quote/newline escaping — stack traces and failed-API errors can embed credentials, private paths, and hostnames, and the sync path strips only _repo_slug/_branch. New lib/redact-engine.ts export redactFindingSpans(): replaces EVERY finding's span with <REDACTED-{id}> regardless of tier (applyRedactions is the interactive PII-only path and exits nonzero on credential findings, so it can't serve machine egress). Returns null when a span can't be located — callers drop the whole payload rather than risk a leak. gstack-telemetry-log pipes error_message through it at LOG time, so the local JSONL at rest is clean too; surrounding text survives for crash triage. FAIL CLOSED: bun missing, engine error, or non-JSON-string output all null the field. Tests pin: embedded ghp_ token → <REDACTED-github.pat> with context intact; redactor unavailable → null; raw bytes on disk never contain the token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(redact): prepush guard fails closed on git failure; /ship owns hook install (#1946) Two gaps closed: 1. Fail closed. The git() helper returned "" on ANY non-zero exit or maxBuffer overflow (status null), addedLinesFor produced an empty string, and the push sailed through unscanned — fail-open on exactly the oversized-diff case where a large secret-bearing blob is most likely. The diff call now uses a strict variant that throws; main blocks with a clear message naming the GSTACK_REDACT_PREPUSH=skip escape valve. Probe calls (symbolic-ref, rev-parse, merge-base) keep the permissive helper — their failures are normal control flow. 2. Install path. The hook was installed by nothing ("opt-in, installed by nothing" was the issue's words). ./setup runs in the gstack checkout — the wrong repo for a per-project hook — so it gets a one-line hint only. /ship owns per-repo install: config redact_prepush_hook=true + hook missing → silent install (consent already given); config unset + no ~/.gstack/.redact-prepush-prompted marker → one-time machine-wide AskUserQuestion offer, answer persisted. ship/SKILL.md regenerated in this same commit (check-freshness bisect discipline). Tests: unscannable diff (bogus SHAs) → exit 1 + valve named; empty-but- successful diff → exit 0; static asserts pin setup as hint-only and the ship template as the installer surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(redact): six new credential patterns — GitLab, HuggingFace, npm, DigitalOcean, Bearer, GCP SA (#1946) Coverage gaps from the #1946 security review, including token types for tooling gstack itself drives (glab): HIGH (block): gitlab.token (glpat-/glptt-/gldt-), huggingface.token (hf_), npm.token (npm_), digitalocean.token (dop_v1_), gcp.service_account (the JSON-escaped "private_key" form that dodges pem.private_key's literal-block match when minified, confirmed by "private_key_id" proximity). MEDIUM (warn): auth.bearer — the most FP-prone shape in the set (docs are full of "Authorization: Bearer <token>"), so it requires header-context proximity and the same entropy>=3.0 + placeholder validator recipe as env.kv. "Bearer YOUR_TOKEN_HERE" never fires; calibration over coverage, per the cries-wolf principle. All shapes are linear-time; test/redact-pattern-lint.test.ts covers them automatically. Engine tests add positive + placeholder-negative cases per pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: coverage-audit additions for the fix wave Ship Step 7 gap-fill (all passing, 248 tests across the touched suites): memory + dream stage probe-timeout proceeds, gbrain-detect override paths, stale-flag passthrough, 200-body-missing-.security fail-closed case, telemetry redaction edges, and credential-pattern edge cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review fixes Review army findings (1 critical, auto-fixed with regression tests): - CRITICAL (security specialist, verified live): redactFindingSpans spliced only the regex capture span, and pem.private_key / gcp.service_account capture just the BEGIN-header — the key body survived "redaction" and shipped via telemetry. Marker-only patterns now drop the whole payload (null, fail closed). Overlapping spans (Bearer+JWT on the same bytes) are coalesced before splicing so stale offsets can't leave partial secret bytes behind. - gitStrict: drop the dead `|| r.status === null` disjunct (null !== 0 already covers it); add the signal-kill/null-status regression test the docstring promised. - security-dashboard human mode flags stale snapshots ("figures may be out of date") instead of presenting frozen counts as current. - community-dashboard marker check uses jq when available — the grep-only variant misclassified whitespaced/reserialized bodies as legacy. - telemetry fail-closed test now shadows bun with a failing stub (deterministic on any host layout); stale "five status cases" describe title renamed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adversarial review fixes (Claude + Codex cross-model passes) Both adversarial passes ran against the wave; every FIXABLE finding landed with a regression test: - probeTimeoutMs clamps to >=1ms: a fractional override floored to 0, and execFileSync treats timeout:0 as NO timeout — the probe that exists to bound hangs could hang forever (found by both models independently). - /ship silent hook install now requires the hooks dir to live inside .git: with core.hooksPath (husky's COMMITTED .husky/), the chaining installer would have renamed the team's committed pre-push and written a machine-local wrapper into the working tree (found by both models). - gstack-config gbrain-refresh accepts the "timeout" status — the last consumer still gating on literal "ok" (Codex); gstack-gbrain-detect's config-derived fields honor GBRAIN_HOME so the detection JSON can't report status ok alongside config_exists false (Codex). - prepush: a remote sha absent locally (shallow clone / stale fetch) falls back to the merge-base/empty-tree range — scans MORE, never blocks a legitimate push into training users toward --no-verify. - dashboards: curl's own 000 no longer doubles to "HTTP 000000"; the community dashboard flags stale snapshots like the security one; array sections parse via jq (the sed/grep loops truncated at the first ']'); the no-jq marker grep tolerates whitespace. - telemetry: multi-line redactor output nulls the field instead of corrupting the JSONL record; setup's hint fires only when the config key is genuinely unset (an explicit false is a recorded decline); the /ship prompt marker honors GSTACK_HOME. Kept as designed (cross-model tension noted): Bearer stays MEDIUM in the prepush gate — a HIGH Bearer would block every docs example; the entropy validator can't eliminate that FP class, and MEDIUM warns visibly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.57.11.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: P1 TODO — eval harness live progress + incremental persistence Root-caused during this ship: a killed eval run was indistinguishable from a healthy one for hours (per-file output buffering across mega test files, no incremental eval-store writes, no honest liveness signal). Full context and starting points in the entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: fix operational-learning E2E fixture — copy lib/jsonl-store.ts Pre-existing breakage, proven on main: gstack-learnings-log has imported lib/jsonl-store.ts (shared injection patterns) since v1.57.5.0 / #1910, but the fixture copies only the bin scripts — the bin exits 1 before writing anything, on main silently (stderr swallowed) and on this branch loudly (the #1950 error-surfacing made the four-day-old failure visible). A real install always ships bin/ and lib/ together; the fixture now does too. Verified: the fixture-shaped invocation writes the learning (exit 0) with lib present, exits 1 on both main and this branch without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ios-qa): isolate E2E tests under --concurrent (3 real races) The ios-qa E2E file failed intermittently under `bun test --concurrent` (the eval harness default). Three distinct shared-state races, all fixed: 1. Shared pidfile: a module-level `workDir` reassigned in beforeEach was clobbered by parallel tests, so concurrent daemons collided on the same pidfile and the loser returned `already_running`. Each test now gets its own dir via makeWorkDir(). 2. process.env path globals: tests set GSTACK_IOS_AUDIT_PATH / _ATTEMPTS_PATH / _ALLOWLIST_PATH on the shared process env; concurrent tests stomped each other's audit/attempts destinations. Threaded auditPath/attemptsPath/allowlistPath through DaemonOptions (and mintForCaller) as explicit args — env is no longer load-bearing. 3. afterEach cleanup race: the per-test cleanup drained a shared dir array, so the first test to finish deleted still-running tests' workDirs mid-assertion. Moved to afterAll (cleans once, after all settle). Verified: 5/5 clean full-suite runs at --max-concurrency 15 (was intermittent); daemon unit suite 91/91; daemon source compiles. The paths default to the env-derived locations when options are omitted, so the production CLI path is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(pty): pin spawned claude to EVALS model chain (default claude-sonnet-4-6) launchClaudePty spawned the interactive `claude` TUI with no --model flag, so the child inherited the operator's ~/.claude/settings.json model. On a slow-thinking model that meant 5+ min of extended thinking on empty plan-mode context, timing out the plan-mode smoke tests regardless of contention. Pin the model via opts.model ?? EVALS_MODEL ?? 'claude-sonnet-4-6' — byte-identical to session-runner.ts:144, so PTY and `claude -p` evals always agree. Pushed before extraArgs (last flag wins, so a per-test --model still overrides). Placement leaves the spawn region byte-stable for a clean merge with the in-flight hermetic-env branch. Plumbed model through the three plan-skill wrappers. Static-grep tripwires guard the pin, its fallback chain, the before-extraArgs ordering, and all three wrapper forwards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(pty): detect markdown bold-bullet prose AUQs (fixes office-hours smoke) office-hours auto-mode renders its mode question as `- **Building a startup**` markdown bullets (office-hours/SKILL.md.tmpl:102) with no letter/number marker. isProseAUQVisible only matched `A)`-style lettered or `1.`-style numbered options, so the question went undetected: the model surfaced it at ~2m19s (well under the 300s budget) but the harness kept scoring the run "working" off the spinner glyphs and timed out — a false timeout on a question that was already on screen. Add Pattern 3: when an interrogative line ('?') is present AND 3+ bold-bullet markers (`- **`) appear in the 4KB tail, classify as a prose AUQ. Bold is the discriminator vs incidental prose bullets; the line anchor is dropped (stripAnsi can collapse option lines) and the existing `❯ 1.` cursor gate still defers to a live native list. Wires through the existing classifyVisible 'asked' path and the timeout high-water-mark, so office-hours now classifies 'asked' instead of 'timeout'. Five unit cases: the office-hours render passes; no-'?', <3-bullet, plain-bullet, and native-cursor cases stay false. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(pty): detect stripAnsi-collapsed prose AUQs + judge spinner-precedence The plan-eng/plan-design plan-mode + finding-floor smokes timed out even when the skill HAD rendered a complete prose AskUserQuestion and was waiting: the PTY strips cursor-positioning escapes, collapsing the option newlines/spaces so "A) ..." arrives as "A(recommended)" / "-B:" and "Reply with A, B, or C" as "ReplywithA,B,orC". Every line-anchored detector (Patterns 1-3) returns false on those bytes, so proseAUQEverObserved never latched and the run timed out on a question that was already on screen. Add Pattern 4/5: a two-signal collapsed-form detector — a reply/recommendation marker (space-insensitive "reply with [A-D]", "Recommendation:", or "(recommended)") AND 2+ distinct A-D letters each punctuated by ) : or (. The conjunction is what separates a real AUQ from incidental report prose; verified true on the verbatim failing-run buffers where Patterns 1-3 return false. Also fix the Haiku judge spinner bias: of 614 verdicts, 569 were 'working' and 95 of those noted a question was visible — Claude Code keeps the spinner animating at an idle prose decision, so the judge coin-flipped. Add a precedence override: when an option list AND a Recommendation/Reply instruction are both visible, classify WAITING even with spinner glyphs. Kept the strict dual-signal gate (never option-list-alone) so auto-decide-preserved doesn't flip. 5 unit tests pin the two-signal contract (2 true on real collapsed bytes, 3 false guards). 90 -> 95 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(plan-review): ask-first scope gate for plan-eng + plan-design review On an empty/cold invocation, plan-eng-review and plan-design-review would dive straight into repo exploration (plan-eng) or a 7-pass mockup+audit (plan-design) and only ask the user much later, if at all. plan-ceo-review already asks first via an unconditional Step-0 gate and behaves well; these two did not. Add a hard-STOP scope gate as the FIRST operational instruction in each skill (above the design-doc check / pre-review audit / mockup defaults it explicitly overrides): the first tool call must be AskUserQuestion confirming the review target, before any git/Read/Grep/Glob/Bash or mockup generation. Under --disallowedTools the options render as plain column-0 lettered prose with a Recommendation + "Reply with A, B, or C" line so the answer is detectable. This is correct cold-start UX (confirm what to review before grinding a full review on nothing) and it is the product half of the plan-mode smoke fix; the harness collapsed-form detector is the deterministic half that catches the ask however it renders. Templates + regenerated SKILL.md (default variant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(tiers): reclassify stochastic plan-eng/plan-design ask-first smokes as periodic plan-eng-review and plan-design-review run a long explore/audit before their first AskUserQuestion, so whether the plan-mode + finding-floor smokes reach a terminal outcome within the 300s/600s budget depends on stochastic ask-first compliance (measured ~50-67%/run even with the hardened gate). Per the "non-deterministic -> periodic" tiering rule, move the four affected smokes (plan-eng/plan-design review-plan-mode + finding-floor) to periodic. The deterministic harness fix (collapsed-form detector + judge precedence) and the ask-first gate lift these from always-failing to mostly-passing and are the real product+harness improvements; periodic monitoring tracks the rate weekly without blocking PRs on an LLM coin-flip. plan-ceo/plan-devex ask-first reliably and stay gate-tier. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(evals): gate the deterministic PTY plan-mode smokes in CI The real-PTY plan-mode smokes never ran in CI — the gate was local-only. Add an e2e-pty-plan-smoke matrix suite running the two deterministically-reliable ones (office-hours-auto-mode, plan-mode-no-op) so a regression there blocks PRs. The stochastic plan-eng/plan-design ask-first smokes stay periodic (touchfiles E2E_TIERS) and are not CI-gated. A fresh CI container has no ~/.claude.json, so the spawned interactive `claude` would wedge on the onboarding + API-key-approval dialog. Add a scoped seed step (hasCompletedOnboarding + key approval, its own ANTHROPIC_API_KEY env) before the run — mirrors what the hermetic E2E child env seeds. Per-suite timeout override (35 min) via matrix.suite.timeout so the PTY suite has headroom for --retry 2 without bumping the other 12 suites. Report runner count 12 -> 13. Validate via workflow_dispatch before relying on the gate (PTY-in-CI is new). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(evals): install gstack skill registry for the PTY smoke suite The first dry-run of e2e-pty-plan-smoke failed: the spawned interactive `claude` printed "Unknown command: /plan-ceo-review". .claude/skills is gitignored, so a fresh CI checkout has no gstack skill registry and the TUI can't resolve /office-hours or /plan-ceo-review. Add a Register step (scoped to the suite, after Seed, before Run) that mirrors setup's --no-prefix user-scoped registry minimally: $HOME/.claude/skills/gstack -> repo (resolves the preambles' absolute ~/.claude/skills/gstack/bin/* and <skill>/sections/* paths) + per-skill SKILL.md/sections symlinks for the two skills these tests invoke. HOME is /github/home in this container and the runner adds no HOME/CLAUDE_CONFIG_DIR override (no hermetic mode), so $HOME is the right anchor — the Seed step already proved claude reads it. No ./setup (binary build + Chromium + fonts + /dev/tty prompt); SKILL.md + bin/ + sections/ are committed. Self-validating: fails the step loudly on a dangling symlink or missing `name:` frontmatter, so a moved target surfaces here instead of as a silent 35-min "Unknown command" timeout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.58.4.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
45cc95d5f4 |
v1.57.5.0 feat: cross-session decision memory + gbrain dream-stage call graph (#1910)
* feat(gbrain-sync): add cycleCompleted() cycle-state probe Reads `gbrain doctor` cycle_freshness to classify whether a source has completed a full cycle (completed/never/unknown). A fail naming this source -> never; a fail naming only other sources -> completed; an absent or unparseable check -> unknown, so an unrelated doctor failure never masks a real state. Gates the automatic call-graph build on --full. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(gbrain-sync): --dream call-graph stage with lock-free gate + honest outcome guard Adds a source-scoped `gbrain dream --source <id>` stage that builds this worktree's call graph (code-callers/code-callees). Runs lock-free after the sync lock releases so it never blocks sibling worktrees; a .dream-in-progress marker dedupes concurrent dreams. --full auto-runs it only when the cycle was never built; explicit --dream always forces; --no-dream opts out. The stage parses the cycle's own output and reports the truth, not a flat "built": a WARN when the schema pack can't extract code symbols, when the embed phase failed for a missing key, or when 0 edges resolved; OK with the resolved-edge count otherwise. gbrain exits 0 even when it skips on a held cycle lock (e.g. autopilot), so that case reports SKIP, not success. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: ignore gbrain .sources/ local staging dir gbrain writes per-source staging and capability-check artifacts under .sources/ in the repo root. It's machine-local runtime state, not source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(gbrain): honest call-graph guidance in /sync-gbrain + pin works on gbrain>=0.41.38 sync-gbrain frames the --dream offer honestly: building a call graph requires a code-aware schema pack, and the dream stage reports a WARN when it can't. The verdict's Call graph row mirrors the dream stage's real outcome instead of assuming a completed cycle means edges exist. The ## GBrain Search Guidance block written into CLAUDE.md drops the old code-callers --source caveat: gbrain >=0.41.38.0 honors the .gbrain-source pin for code-callers/code-callees. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(jsonl-store): shared audited JSONL plumbing (injection-reject + atomic append + tolerant read) Single source of truth extracted for D2A: gstack-learnings-* and the upcoming gstack-decision-* bins share one injection-pattern list, one atomic single-line appender, and one tolerant reader. No more drift between stores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(learnings-log): use shared hasInjection from lib/jsonl-store (D2A) Replace the inline injection-pattern copy with the shared list. One audited write-path rejection across learnings + the upcoming decision store. Behavior unchanged (35/35 learnings tests green); learnings-search keeps its inline copy because a structural test pins its bash/bun shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(decision): event-sourced decision-memory model (lib/gstack-decision) decide/supersede/redact events on lib/jsonl-store; active set is computed (no mutable status), dangling refs tolerated. Free-text is injection-checked and redact-scanned on write (HIGH secret -> reject). Scope filter (repo/branch/issue) for relevant resurfacing. File-only + reliable; gbrain not required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(decision): bounded active snapshot + compaction (redact expunges, supersede archives) writeSnapshot/readSnapshot/rebuildSnapshot give an O(active) bounded read for the session-start hot path (D1A). compact() rewrites the log to active, archives superseded decisions for history, and EXPUNGES redacted ones (dropped, never archived) so an accidentally-captured secret leaves the store for good. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(decision): gstack-decision-log + gstack-decision-search bins (non-interactive) Two bins mirroring gstack-learnings-* (D3A). log writes decide/--supersede/--redact/ --compact events + refreshes the bounded snapshot + enqueues for cross-machine sync; search reads the O(active) snapshot, scope-filtered to current branch, newest-first, --all to include superseded, --json for machines. Empty store returns silently (no snapshot write on an empty read). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): surface active decisions at session start + capture nudge (Context Recovery) Context Recovery now shows recent scope-relevant active decisions (bounded read of decisions.active.json via gstack-decision-search) and instructs the agent to treat them as settled calls and to log durable decisions/reversals. Closes the Phase-1 capture->curate->resurface loop, reliable + file-only. Regen across all hosts folded in (squash-with-regen); parity 10/10, freshness green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: refresh ship golden baselines for the memory-loop preamble change Context Recovery now emits the cross-session-decisions block, so ship's preamble (all hosts) changed. Golden baselines are hand-maintained copies (gen does not write them); refresh them from the fresh gen so golden-file regression passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(memory): document the cross-session decision-memory loop in CLAUDE.md Adds a '## Cross-session decision memory' section: how to resurface (gstack-decision-search) and capture (gstack-decision-log) durable decisions, the supersede/redact/compact verbs, and a crisp durable-vs-trivial definition so the store stays signal. Reliable file-only path; gbrain not required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): emit durable decisions from ship/ceo/eng/spec at structured points Wires the four skills that finalize real decisions to capture them in the cross-session decision store, from their STRUCTURED outputs (never free-text scraping): - ship: the version bump (level + why) at write time - plan-ceo-review: accepted scope + verdict (branch-scoped) - plan-eng-review: the architecture verdict + key call (branch-scoped) - spec: the filed issue's core approach (issue-scoped) All emits are non-interactive, schema-correct (content in decision/rationale, source=skill, confidence 1-10), and best-effort (|| true) so a decision-log failure never blocks the workflow. Includes regen across hosts + refreshed ship golden baselines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): optional gbrain --semantic recall for decision search Adds gstack-decision-search --semantic (with --query): appends a 'Related from memory' block from gbrain semantic search, scoped to the curated-memory source. Pure enhancement, reliability-first: a new lib/gstack-decision-semantic.ts is the ONLY decision module that touches gbrain and is imported lazily only on --semantic, so the reliable file path never loads gbrain code. Every path degrades to the reliable file results when gbrain is off, unconfigured, empty, or errors (never throws, 10s timeout). Built against the verified gbrain 0.42.x surface (text output [score] slug -- snippet, NOT JSON; curated-memory source resolved by worktree path, not a gstack-brain-<user> id). Deterministic-contract tests only: parser units, degrade-to-null when gbrain absent, and a fake-gbrain shim proving scope+search end-to-end. find-contradictions deferred (no verifiable CLI surface yet + curated memory not indexed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(gbrain-sync): self-heal stale autopilot lock (dead-pid) detectAutopilot treated a lock FILE as proof of life, so a crashed gbrain daemon left a stale lock that wedged every sync forever (observed: a dead pid refused --full indefinitely). Now read the holder pid (bare or JSON body) and check liveness via signal-0: ESRCH=dead → ignore the stale signal and keep checking; EPERM=alive (other user) → active. A stale lock never masks a live autopilot process. Pure decision function — does not delete the file; the caller may clean it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(review): drop stray trailing code fence in TODOS-format Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): align section-loading E2E testNames with their TOUCHFILES keys Pre-existing on main (v1.56.x): the two section-loading E2E tests used human-label testNames ('/ship section-loading') that don't match their slug keys ('ship-section-loading') in E2E_TOUCHFILES/E2E_TIERS. Every other E2E test uses the slug as its testName, and the TOUCHFILES completeness gate requires testName to be a registered key — so the gate was red. Align both testNames to their slug keys (also fixes tier lookup for these two periodic tests). Verified failing on a clean origin/main checkout before the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: pre-landing review fixes (datamark, DRY, compact, coverage) Addresses the pre-landing review findings (all INFORMATIONAL, no criticals): - security: datamark resurfaced decision text at the render boundary (lib/gstack-decision.ts datamark() — neutralizes code fences, --- banners, <|role|>/</system> markers, control chars, newlines). Applied in gstack-decision-search human output so stored text can't masquerade as instructions in Context Recovery (codex hardening #3 / AC #7). --json stays raw. - DRY: extract resolveSlug/gitBranch/flagValue to lib/bin-context.ts; both decision bins use it instead of duplicating the helpers. - compact(): batch the archive append (one write, not N) and shrink the mid-compact crash window; simplify the opaque branch/issue ternary. - coverage: learnings-log injection rejection (D2A wiring), search --recent/ --scope + NaN-safe --recent, datamark-applied, unparseable lock body, compact-empty, corrupt-snapshot degrade. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(security): close adversarial-review findings in decision memory Adversarial review (Claude subagent) found a CRITICAL the specialist pass missed: - F1 (CRITICAL): 'Human:'/'Assistant:' turn-prefixes bypassed BOTH the write-time denylist AND datamark(), landing verbatim in agent context inside the trusted ACTIVE DECISIONS fence. Add 'human:' (+ 'disregard previous', 'from now on') to the shared denylist, and have datamark() neutralize Human:/Assistant:/System:/User: turn-prefixes (ZWSP) at the render boundary. - F2: datamark() only stripped ASCII C0; extend to Unicode line terminators (U+0085/2028/2029) and U+007F so 'strip newlines' actually holds. - F3: validateDecide blocked only HIGH secrets; MEDIUM-tier PII (e.g. SSN) persisted silently and synced cross-machine. The store is non-interactive (no confirm path), so fail closed on MEDIUM too. - F4: compact() was a lock-free read-modify-rewrite that could clobber a concurrent append (lost decision). Add an O_EXCL compact lock + a pre-rename size recheck that aborts untouched (skipped=true) if an append landed; caller re-runs. - F7: filterByScope unknown/garbage scope fell through to 'return true' (leaked into every context); fail conservative (false). F5 (pid reuse) and F6 (pgrep over-match) are intentionally left as-is: both fail SAFE (over-refuse sync); making them precise would introduce a fail-DANGEROUS path (allowing sync during a real autopilot). True disambiguation needs gbrain to stamp the lock with a start-time, which gstack doesn't own. F8 (compact moves history to archive) is by design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(security): close cross-model (Codex) adversarial findings Codex adversarial review found a HIGH the Claude pass missed plus 3 mediums: - C1 (HIGH): gstack-decision-search --all returned every decide and IGNORED redact events, so a redacted secret still resurfaced via --all until compact ran. --all now excludes redacted (redact = expunge from every read path), still showing superseded history. - C-med: semantic (external gbrain) slug/snippet were printed raw — datamark them too so a gbrain hit can't spoof role markers / fences into agent context. - C4: semanticRecall fell back to an UNSCOPED gbrain search when no curated-memory source resolved, pulling code/doc corpora mislabeled as 'related decisions'. Now returns null (degrade) when there's no worktree-backed memory source. - C5: validateDecide scanned only decision/rationale/alternatives; branch and issue are stored + surfaced (raw via --json), so include them in the injection+secret scan. C2 (snapshot staleness) / C3 (compact TOCTOU residual): accepted for a single-user store — atomic appends never lose the event, rebuilds self-heal, and the compact size-recheck leaves only a sub-ms window; full append-locking would break the lock-free append design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.57.5.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
476b0ec597 |
v1.56.1.0 fix(sync): staging-dir ownership guard + resume-correctness fixes (#1802) (#1856)
* fix(sync): fail-closed staging-dir ownership guard — prevent rm -rf of repo (#1802) Adopts community fix #1827 by @diazMelgarejo (cyre). New lib/staging-guard.ts exports checkOwnedStagingDir(), the single fail-closed predicate for 'safe to recurse-delete or resume into', wired at cleanupStagingDir() (the deletion chokepoint), decideResume(), the ingest entry point, and makeStagingDir() (mints the .gstack-staging marker). Fixes #1802. Co-Authored-By: cyre <diazMelgarejo@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): don't route the remote-http persistent transcript dir through cleanup (#1802) The ingest finally ran cleanupStagingDir() unconditionally, but in remote-http mode stagingDir is the PERSISTENT transcript dir (~/.gstack/transcripts/) that gstack-brain-sync push must consume. The remote-http branch documents the intent to skip cleanup, but a finally runs on its return. Gate the call on !remoteHttpMode so the ownership guard only ever sees .staging-ingest-* dirs. Pre-gate this dir was deleted outright (broken artifacts handoff); post-#1827 it produced a false 'prevent data loss' warning every sync. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): preserve staging dir on internal import timeout (#1802 C3) The import-timeout branch printed 'checkpoint preserved' but the finally then deleted the staging dir: the SIGTERM forwarder's preserve branch only runs when the PARENT is signalled, and an internal runGbrainImport timeout kills just the child and returns normally. So #1611 resume-after-timeout never actually worked. Mirror the forwarder in the timeout branch: set preserveStaging only when gbrain checkpointed against this dir (finally then skips cleanup); otherwise clean up and tell the user it restages instead of falsely promising a resume. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): resume must not mark failed files as ingested (#1802 C4) On resume, stagedPathToSource was rebuilt as an empty Map, so readNewFailures() could not map gbrain's per-file failures back to source paths. Every failure fell through to state recording — failed files were silently marked ingested and never retried. Reconstruct the map from the prepared pages via a shared stagedRelPath() helper (single source of truth with writeStaged, so the keys can never drift). Exports stagedRelPath + readNewFailures for a behavioral test proving the reconstructed map recovers the failure the empty map dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(sync): close staging-guard TOCTOU + fail hard on marker write (#1802 C5) checkOwnedStagingDir() now returns the realpath-resolved canonicalPath on a pass, and cleanupStagingDir() rmSync's that instead of the raw input — closing the gap where the input is a symlink swapped between the ownership check and the delete. makeStagingDir() tears down the partial dir and rethrows if the marker write fails, so a marker-less dir (which the guard would refuse forever) can never leak. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: v1.56.1.0 — staging-dir ownership guard + resume-correctness fixes (#1802) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: grant the eval report job issues:write so PR comment upsert stops 401ing Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: cyre <diazMelgarejo@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3bef43bc5a |
v1.55.0.0 fix wave: gbrain data-loss guards + browser crash-loop + 6 more (#1808)
* fix(jsonl-merge): make equal-ts resolution converge across machines The JSONL append merge driver sorted timestamped entries by (0, ts) with no further tiebreaker. Equal-ts entries then fell back to stable-sort insertion order (base, ours, theirs), but git assigns the local side to "ours", so two machines resolving the same conflict emitted equal-ts lines in opposite order. The merged files diverged and never converged. gstack-telemetry-log uses second-granularity timestamps, so same-ts collisions are routine. Add the line content as the final sort tiebreaker so the order is total and side-independent. Add a regression test that runs the driver with the two sides swapped and asserts identical output. * fix(gen-skill-docs): quote frontmatter descriptions with interior colons (#1778) Generated SKILL.md frontmatter emitted the catalog-trimmed description: as a plain YAML scalar. A description with an interior ": " (e.g. "Ship workflow: detect...") parses as a nested mapping under strict YAML loaders, so Codex/OpenAI skill loading rejected those skills. applyCatalogTrim now routes the value through toYamlInlineScalar, which quotes (via JSON.stringify) only when a plain scalar would be invalid — interior ": ", inline " #", leading indicator char, or surrounding whitespace. Strings that are already valid plain scalars pass through unchanged to keep regen diffs small. The frontmatter test now parses every generated block (Claude + Codex hosts) with Bun.YAML.parse instead of string-checking that name:/description: substrings exist, so the regression can't reappear. Runs under `bun test` (already in CI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(skills): regenerate SKILL.md after frontmatter quoting fix (#1778) 9 catalog-trimmed descriptions whose values contain an interior colon or inline- comment marker are now quoted. Generated output only; rerun of bun run gen:skill-docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(gbrain-sources): centralize sources-list shape handling in parseSourcesList (#1576) #1576's crash in sourceLocalPath was already fixed in v1.42.0.0 (dual-shape handling). But the readers disagreed: sourceLocalPath accepted both the wrapped {sources:[...]} object (v0.20+) and a bare array, while probeSource and sourcePageCount accepted only the wrapped shape. Extract one parseSourcesList() normalizer and route all three through it, so the shape assumption lives in a single place. This is also the base the #1734 remote_url audit builds on. parseSourcesList returns [] for null/garbage rather than throwing; callers treat 'no rows' as absent. New test/gbrain-sources-parse.test.ts pins both shapes plus the garbage paths and confirms config.remote_url survives for the audit. #1576 is closeable as already-fixed in v1.42.0.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gbrain): spawn gbrain + brain-sync through a shell on Windows (#1731) On Windows, bun/npm install gbrain as a gbrain.cmd/.ps1 shim and gstack-brain-sync is a bash shebang script. spawnSync/spawn/execFileSync resolve neither without a shell, so the child spawn failed ENOENT — on the sync orchestrator this surfaced as 'brain-sync exited undefined' (#1731). Add NEEDS_SHELL_ON_WINDOWS (process.platform === 'win32') in gbrain-exec and pass it as shell: to every gbrain/brain-sync child spawn: spawnGbrain, spawnGbrainAsync, execGbrainText (gbrain-exec), the two sources-list/remove/add spawns (gbrain-sources), the version + probe spawns (gbrain-local-status), and the two brain-sync spawns in the orchestrator. POSIX keeps the cheaper no-shell path. macOS/Linux CI can't exercise the Windows path, so test/gbrain-spawn-windows-shell.ts is a static-grep tripwire: it fails CI if a gbrain/brain-sync spawn is added without the shell flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(catalog-trim): expect YAML-quoted descriptions with interior colons (#1778) The quoting fix wraps colon-bearing catalog descriptions in double quotes; two catalog-trim assertions still pinned the old unquoted form. Tolerate the optional quotes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gbrain-sync): defensive guards against destructive gbrain ops (#1734) The orchestrator shelled out to gbrain's destructive subcommands as if they were safe. gbrain can rm-rf a user's working tree during an autopilot race (its own bug, upstream gbrain #1526); gstack now defends itself. New lib/gbrain-guards.ts gates the two destructive reach points, all checked immediately before the op: - Autopilot refuse (multi-signal, affirmative-only): refuse a destructive op when a live 'gbrain autopilot' process (primary) or a known autopilot lock file (secondary; checked under both GBRAIN_HOME and ~/.gbrain since gbrain #1226 ignores GBRAIN_HOME) is present. No signal → proceed; inability to introspect never bricks a normal sync. - sources remove: routed through safeSourcesRemove → decideSourceRemove. Fail CLOSED — refuse to remove a user-managed source (remote_url set, local_path outside gbrain's clones) when gbrain has no --keep-storage to protect the files (it doesn't in 0.41.x). Also fail closed when the source list can't be read. Path containment uses realpath so a symlink can't smuggle a delete out of clones. - sync --strategy code: decideCodeSync refuses URL-managed sources (remote_url set) unless --allow-reclone is passed, since the walk can auto-reclone (rm-rf). Capability detection memoizes per process keyed to gbrain's identity (no stale persistent cache); --keep-storage can't be probed (generic help) so it defaults unsupported → fail closed. Every guard surfaces a visible reason; autopilot/reclone refusals fail the code stage (verdict ERR) rather than silently skipping protection. test/gbrain-guards.test.ts covers all branches hermetically (injected rows + probe overrides): autopilot signals, fail-closed remove, keep-storage path, reclone gate, realpath/symlink containment. Supersedes #1736 (which guarded a nonexistent path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(sync-gbrain): warn against running during autopilot; prefer --path sources (#1734) Adds a Safety note to the /sync-gbrain guidance (template + regenerated SKILL.md + this repo's CLAUDE.md): don't run while autopilot is active, and prefer `gbrain sources add --path` over URL-managed sources, which can auto-reclone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory-ingest): configurable import timeout + resume-on-timeout messaging (#1611) The gbrain import (the long pole on big brains) had a hardcoded 30-min timeout, so large memory corpora got SIGTERM'd mid-import on /sync-gbrain --full. Make it configurable via GSTACK_INGEST_TIMEOUT_MS (default 30 min, validated 1min–24h). gstack can't drive gbrain's internal resume, but the existing SIGTERM forwarder already preserves gbrain's import-checkpoint.json, so the next run resumes. On a timeout we now say so explicitly ('checkpoint preserved — re-run /sync-gbrain to resume, raise GSTACK_INGEST_TIMEOUT_MS for big brains') instead of surfacing a bare 'exited null'. True gstack-driven ingest-resume is deferred to gbrain (.context/gbrain-asks.md). Also guards the module's main() behind import.meta.main so resolveImportTimeoutMs is unit-testable; the orchestrator runs it as a subprocess where main still fires. New test/memory-ingest-timeout.test.ts pins default/override/invalid resolution. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(browse): stop the headed daemon crash-loop + silent headless downgrade (#1781) A headed session against a beacon-heavy page (analytics/extension load) could tip the single-threaded daemon into a self-inflicted crash-loop: a brief HTTP stall was read as a crash, the restart didn't clear the dead Chromium's SingletonLock, the relaunch failed, and the session silently came back headless. Four fixes: 1. Busy-vs-dead (sendCommand): on a connection error, if the process is alive give /health a bounded probe (3x/250ms) and just retry the command — never kill+restart a live-but-busy server. A 30s timeout now reports 'busy, not restarting' when the process is alive instead of exiting into a kill cycle. 2. Profile-lock cleanup on (re)start: startServer reaps the orphaned Chromium holding the SingletonLock and clears Singleton{Lock,Socket,Cookie} before relaunch, so the auto-restart path gets the same clean profile the manual connect preamble did. 3. Headed persistence: the restart env reapplies BROWSE_HEADED from this invocation OR the persisted server state (mode==='headed'), so a restart from a plain command never downgrades a headed window to invisible headless. Extracted to buildRestartEnv. 4. Force-clean disconnect reaps the Chromium child tree (via the SingletonLock PID) so the next connect starts clean instead of fighting an orphan. Plus macOS window surfacing: connect + focus raise 'Google Chrome for Testing' to the active Space (best-effort osascript) with a Mission Control hint — the first thing users read as 'I can't see the browser'. Shared lock helpers (chromiumProfileDir / cleanChromiumProfileLocks / killOrphanChromium) dedupe the connect, disconnect, and restart paths. browse/test/restart-env.test.ts pins the headed-persistence decision; the full crash-loop repro is an E2E (periodic). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(gbrain-install): remove the v0.18.2 pin, install latest + version floor + doctor self-test (#1744) The installer pinned gbrain at v0.18.2 while gbrain shipped v0.41.x — ~23 versions behind. Remove the hard pin: a fresh clone now stays on the latest default-branch HEAD. --pinned-commit <sha> still pins for reproducibility. Unpinning removes the version gate the pin provided, so add two install-time gates that fail closed (exit 3, matching the existing PATH-shadow/version-mismatch posture): - MIN_GBRAIN_VERSION floor (0.20.0, the sources-list/federated surface gstack needs): refuse an install below it. - gbrain doctor --fast self-test when a brain config already exists (re-install / detected clone): refuse to leave a broken gbrain in place. Pre-init installs skip it; the full /sync-gbrain --dry-run self-test runs from /setup-gbrain after init. Docs updated (USING_GBRAIN_WITH_GSTACK.md no longer says 'edit PINNED_COMMIT'). Detect-install tests bump the success-path fixtures above the floor and add a below-floor exit-3 test. The gbrain-side asks (root #1526 fix, --keep-storage, remove-lease, capability command, ingest-resume, integration CI) are written to .context/gbrain-asks.md for filing against garrytan/gbrain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(#1778): update claude-ship golden + catalog-mode assertions for quoted descriptions ship's catalog description ('Ship workflow: detect...') has an interior colon, so the #1778 fix now YAML-quotes it. Refresh the claude-ship golden baseline to the quoted output and make the catalog-mode-full trim/restore assertions quote-tolerant. codex/factory ship goldens are unaffected (they use block-scalar descriptions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gen-skill-docs): use function replacer so a $ in a description can't corrupt frontmatter (#1778) String.prototype.replace treats $&/$1/$` in the replacement as patterns. A future skill description containing $ (e.g. referencing $B/$D) would silently corrupt the generated frontmatter. Use a function replacer. Behavior-preserving for all current descriptions (regen produces no diff). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.55.0.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(gbrain): document configurable memory-ingest timeout for v1.55.0.0 USING_GBRAIN_WITH_GSTACK.md: note GSTACK_INGEST_TIMEOUT_MS (default 30 min, 1 min-24h range) on the /sync-gbrain memory stage, plus checkpoint-resume on timeout. Fills the reference gap left by the configurable-import-timeout fix (#1611) shipped in v1.55.0.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
+1 |
66f3a180d3 |
v1.43.2.0 fix wave: post-Daegu paper-cut — 18 fixes, 28 bisect commits (#1642)
* fix(gbrain-sync): --full produces an empty code index on first run of a new repo
`gbrain reindex-code` only RE-EMBEDS pages that already exist; it never walks
the filesystem. On a freshly-registered source (0 pages), a --full run that
called reindex-code alone found nothing ("No code pages to reindex"), finished
in ~1s, and left the code index permanently empty while still reporting OK.
Fix: --full now runs `sync --strategy code` FIRST to create pages via the file
walk, then runs `reindex-code` to honor the documented "full walk + reindex"
contract for both fresh and populated sources.
Contributed by @jetsetterfl via #1584.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gbrain-local-status): classifier falsely reports broken-db inside repos with their own DATABASE_URL
The freshClassify probe ran `gbrain sources list --json` with the inherited
process env. When the probe ran from inside a repo with its own .env (an app
DATABASE_URL on a different port), Bun autoloaded the project's .env, gbrain
connected to the wrong database, and the classifier reported broken-db on
otherwise-healthy brains.
Fix: route the probe env through `buildGbrainEnv` from lib/gbrain-exec, the
same helper the sync orchestrator uses. DATABASE_URL is seeded from
~/.gbrain/config.json so the result is cwd-independent. The 60s cache can no
longer propagate a poisoned negative to clean directories.
Contributed by @jetsetterfl via #1583.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(retro): stale-base + bad-today-anchor pre-flight guard (#1624)
/retro silently produced confidently-wrong output when "today" drifted (model
session-context error) or when origin/<default> was materially behind the
actual remote — git log --since returned zero or near-zero commits and the
narrative was fabricated from nothing.
Adds Step 0.5 with four ordered pre-check branches before any window analysis:
A. No 'origin' remote → skip with "base freshness not verified" note
B. Detached HEAD → skip with "base freshness not verified" note
C. `git fetch origin <default>` fails (offline) → warn, proceed against
last-known origin/<default>
D. Fetch succeeded → compare today vs latest origin/<default> commit; if
gap > window-days, BLOCK with explicit citation of latest-commit date.
Skip paths still proceed to Step 1, but the disclosure is carried into the
retro narrative ("offline run, window not freshness-verified") so the output
is never silently confidently-wrong.
Atomic .tmpl + gen:skill-docs regen commit (T-Codex-3 pattern).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(retro): regression for #1624 stale-base pre-flight guard
13 static-invariant tests pinning the four ordered pre-check branches in
retro/SKILL.md.tmpl:Step 0.5:
A. no-remote skip — must check origin presence + set verdict
B. detached-HEAD skip — must gate behind prior verdict (ordering)
C. fetch-fail warn — must match `if !` or `||` shape, gate by verdict
D. stale-base BLOCK — must read latest-commit ISO date, cite remediation
Plus a disclosure-survives-to-narrative invariant: skip-path verdicts must be
named in prose so the retro output carries the cited reason rather than
silently misreporting.
Failing build if Step 0.5 is removed, branches re-ordered (no-remote no longer
wins), or the BLOCK message stops citing today/latest-commit/remediation
path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gbrain-sync): configurable timeouts + resume from gbrain checkpoint (#1611)
The memory and code stages hardcoded a 35-min spawn timeout. On brains with
~2000+ staged files, /sync-gbrain --full reliably SIGTERM'd the child at
exactly 35 minutes with exit 143. gbrain left ~/.gbrain/import-checkpoint.json
pointing at the staging dir, but gstack-memory-ingest's SIGTERM handler
unconditionally cleaned the dir up — so the next run found a checkpoint
pointing at nothing and restaged from scratch, repeating the SIGTERM forever.
Three changes:
1. Configurable timeouts via env (bounds 60_000ms - 86_400_000ms, default
2_100_000ms = 35min unchanged):
GSTACK_SYNC_MEMORY_TIMEOUT_MS
GSTACK_SYNC_CODE_TIMEOUT_MS
Out-of-range or non-numeric values warn and fall back to the default.
2. SIGTERM in gstack-memory-ingest no longer always cleans up the staging
dir. If gbrain has written ~/.gbrain/import-checkpoint.json pointing at
the active staging dir, the dir is PRESERVED for next-run resume.
Otherwise (no checkpoint pointing here, crash before gbrain ever
touched it) it's cleaned up as before.
3. Next /sync-gbrain run detects gbrain's checkpoint via decideResume() in
gstack-gbrain-sync.ts:
- no checkpoint → fresh ingest pass
- checkpoint + staging ok → set GSTACK_INGEST_RESUME_DIR; child
reuses staging dir and skips
writeStaged; gbrain import resumes
from processedIndex+1
- checkpoint + staging gone → warn "previous checkpoint stale
(staging dir gone), restaging from
scratch" and proceed
Reuses gbrain's own checkpoint as the source of truth (D1 — no double-store
state). Detect-then-fallback semantics per C1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(gbrain-sync): regression for #1611 timeouts + resume
19 tests across three surfaces:
- resolveStageTimeoutMs (10 tests): undefined/empty → default; non-numeric,
zero, negative, below-floor, above-ceiling → warn + default; at-floor,
at-ceiling, valid mid-range → accepted as-is.
- decideResume (6 tests): no checkpoint, corrupt JSON, checkpoint + staging
ok, checkpoint + staging missing, checkpoint with no dir, checkpoint with
empty dir.
- SIGTERM staging preservation (3 static invariants): memory-ingest signal
handler must check stagingDirIsCheckpointed BEFORE cleanup; preserve
branch must come before cleanup branch (ordering); orchestrator must
pass GSTACK_INGEST_RESUME_DIR to the grandchild on resume.
Also threads process.env.HOME through readGbrainCheckpoint and
stagingDirIsCheckpointed so tests can redirect home. os.homedir() caches
at process start and ignores later mutation, so the env override is the
only reliable test injection point.
Failing build if the timeout bounds are removed, the resume detection
short-circuits incorrectly, or the SIGTERM handler regresses to
unconditional cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(review): pre-emit verification gate kills Django-shape FP class (#1539)
External user filed 4/8 false positives on a /review run against a Django +
DRF + PostgreSQL repo (Sprint 2.5). Every FP class was the same shape:
"resolvable in <5 minutes by viewing the actual code or running a simple
grep" — fields that don't exist on the model, dict.get()-might-be-None on a
form that returns {}-initialized cleaned_data, standard ORM save behavior
called out as data loss.
Extends the Confidence Calibration resolver (consumed by review, cso,
plan-eng-review, ship) with a Pre-emit verification gate:
Every finding MUST quote the specific code line that motivates it
(file:line + verbatim text). If the reviewer cannot produce the quote,
the finding is unverified — its confidence is forced to 4-5 so the
existing "Suppress from main report" rule fires automatically. The
finding still goes to the appendix for calibration audit, but the user
does not see it in the critical-pass output.
Reuses the existing suppression mechanism — no new code path. The FP
classes the gate kills are enumerated in the resolver text so reviewers
see the named patterns.
Framework-meta nudge included for Django Meta, Rails associations,
SQLAlchemy relationships, TypeORM decorators, Sequelize init, Prisma
generated client — the reviewer must quote the meta-construct that
generates the symbol, not just grep for the literal name. Deeper
framework-aware ORM verification (model introspection, migration-history-
aware checks) is deliberately deferred to a future wave per T-Codex-2.
Atomic .tmpl-equivalent (resolver) edit + gen:skill-docs regen commit
per T-Codex-3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(review): regression for #1539 pre-emit verification gate
12 tests pinning the gate behavior:
- Resolver emits the gate header + #1539 reference
- Gate requires quoting file:line + verbatim text
- Unverified findings forced to confidence 4-5 (auto-suppress via
existing <7-rule, no new mechanism)
- Framework-meta nudge names Django, Rails, SQLAlchemy, TypeORM,
Sequelize, Prisma
- Deferred design doc reference present (1539-framework-aware-review.md)
- Four named FP classes from #1539 enumerated:
* field doesn't exist on model
* dict.get() might be None
* save() might lose fields
* update_fields might miss X
- All four downstream SKILL.md consumers (review, cso, plan-eng-review,
ship) carry the gate text after gen:skill-docs
- Existing confidence 9-10 'Show normally' + 3-4 'Suppress' rows
unchanged (regression on existing behavior)
Failing build if the gate is removed, the suppression mechanism is
re-invented separately, the framework-meta nudge drops a framework, or
gen:skill-docs stops propagating the gate to consumers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(config): expose explain_level default
* fix(benchmark): parse positional prompt after flags
* fix(artifacts): reject malformed remote paths
* fix(learnings): preserve current entries in cross-project search
* fix(setup): register root gstack slash alias
* fix(memory): probe gitleaks without shell builtin
* fix(gbrain-lib): pin LC_ALL=C in varname validator (macOS locale guard)
In many macOS shells the default locale (e.g. en_US.UTF-8) makes bash
glob brackets like `[A-Z]` match lowercase letters too, so the existing
`case "$name" in [A-Z_][A-Z0-9_]*)` branch lets names like `lower-case`
through validation. The function then trips `printf -v "$varname"` and
`export "$varname"` with `not a valid identifier` errors that surface
mid-prompt, which is exactly what the validator was supposed to prevent.
Pinning `LC_ALL=C` inside the function gives ASCII-only bracket semantics
on both macOS and Linux, matching the documented `[A-Z_][A-Z0-9_]*`
contract. Declared `local` so it doesn't leak to the calling shell —
`gstack-gbrain-lib.sh` is documented as a sourced helper, so a bare
assignment would mutate the caller's locale for the rest of the process
(silently affecting downstream `sort`, `tr`, locale-aware globs in the
same shell, etc.).
The existing regression test
`test/gbrain-lib-verify.test.ts:'rejects invalid var names'`
already covers the macOS repro shape (passes `lower-case` and expects
the validator to reject + emit `invalid var name`). On Linux CI the
test silently passed because `LC_ALL=C` is the typical default; on
macOS dev boxes it fails.
Verified:
- `bun test test/gbrain-lib-verify.test.ts`: 22 pass, 0 fail (on macOS).
- `_gstack_gbrain_validate_varname lower-case; echo $?` → 2.
- `_gstack_gbrain_validate_varname FOO_BAR; echo $?` → 0.
- Caller's LC_ALL preserved across calls (confirmed via sourced bash).
* fix(land-and-deploy): detect merged PR after gh failure
After `gh pr merge` exits non-zero, the PR may already be MERGED server-side
(concurrent merge landed, or local cleanup phase failed AFTER the merge
succeeded). Calling `gh pr merge` a second time then errors with a confusing
"already merged" — and worse, the deploy workflow never runs because we
stopped on the first failure.
Adds a Post-failure PR-state check (§4a-postfail) that runs after ANY
non-zero exit from `gh pr merge`:
- state == MERGED → record MERGE_PATH=direct, OFFER (don't force)
stale-worktree cleanup on the base branch with
uncommitted-work guard, proceed to §4a CI watch
- state == OPEN → check autoMergeRequest; if non-null treat as
merge-queue wait; if null surface both errors and STOP
- state == CLOSED → STOP
Hard invariant: never retry `gh pr merge` after a non-zero exit. Server
state is authoritative.
Re-authored from PR #1620 into land-and-deploy/SKILL.md.tmpl (the source of
truth) instead of the generated SKILL.md, so the next gen:skill-docs run
preserves the change. Original diff by @davidfoy via #1620.
Related: cli/cli#3442, cli/cli#13380.
Contributed by @davidfoy via #1620.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: detect PgBouncer transaction-mode pooler and set GBRAIN_PREPARE=true (#1435)
When gbrain connects through a PgBouncer transaction-mode pooler (port
6543), it auto-disables prepared statements. This breaks `gbrain search`
silently — the /sync-gbrain capability check fails and the GBrain Search
Guidance block never gets written to CLAUDE.md.
Three-layer fix:
1. **lib/gbrain-exec.ts** — `buildGbrainEnv()` now detects port 6543 in
the effective DATABASE_URL and sets `GBRAIN_PREPARE=true` in the env
passed to every gbrain spawn. This is the single chokepoint — all
gstack gbrain invocations inherit the fix. Caller can opt out with
`GBRAIN_PREPARE=false`.
2. **sync-gbrain/SKILL.md{,.tmpl}** — capability check now exports
`GBRAIN_PREPARE=true` explicitly and retries search up to 3x with 1s
delay for async index propagation under connection pooling.
3. **bin/gstack-gbrain-detect** — surfaces `gbrain_pooler_mode` field
("transaction" | "session" | null) in the preamble probe JSON so
/setup-gbrain and /sync-gbrain can advise users about pooler state.
Closes #1435
Built with [ClosedLoop.AI](https://closedloop.ai) | [GitHub](https://github.com/closedloop-ai/claude-plugins)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(supabase-provision): rewrite transaction/6543 -> session/5432 for new projects
- Single-object pooler API responses default to transaction-mode at 6543,
but the shared pooler tenant on new projects only listens on session/5432
- Add a `pool_mode == transaction && db_port == 6543` rewrite + stderr note
- Escape hatch via `GSTACK_SUPABASE_TRUST_API_PORT=1` for forward-compat
- 5 new tests covering rewrite, no-op shapes, env opt-out, array path
Fixes #1301.
* fix(browse): GSTACK_CHROMIUM_NO_SANDBOX opt-out for Ubuntu/AppArmor (#1562)
Ubuntu/AppArmor configurations often block unprivileged Chromium sandboxing
for headless agent sessions even for normal users — /qa hangs without
--no-sandbox. The kernel policy denies the unprivileged user namespaces
Chromium needs.
Adds GSTACK_CHROMIUM_NO_SANDBOX=1 as an explicit user override that forces
the sandbox off without changing the default for everyone else. Re-authored
from PR #1562 onto v1.42.2.0's shouldEnableChromiumSandbox() helper —
purely additive, preserves the headed-launch sandbox-on-by-default behavior
that v1.42.2.0 shipped to kill the --no-sandbox yellow infobar.
Three new regression tests cover:
- linux + override=1 → false (the named use case)
- darwin + override=1 → false (env wins on any platform)
- override=0 → does NOT trigger (must be exactly "1")
Original diff by @techcenter68 via #1562.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(browse): mirror isCustomChromium() guard in headless launch()
When BROWSE_EXTENSIONS_DIR is set alongside GSTACK_CHROMIUM_PATH pointing
at a baked-extension build (GBrowser / GStack Browser), the headless launch()
path was unconditionally adding --disable-extensions-except / --load-extension.
This causes the same ServiceWorkerState::SetWorkerId DCHECK crash that
launchHeaded() already guards against via isCustomChromium().
Mirror the existing guard: skip --load-extension flags when isCustomChromium()
returns true; always push the off-screen window geometry args.
* fix(browse): daemonize macOS/Linux server via setsid()
`Bun.spawn().unref()` only releases the child from Bun's event loop —
it does NOT call setsid(). The spawned bun server inherits the spawning
shell's process session. When the CLI runs inside a session-managed shell
that exits shortly after the CLI returns (Claude Code's per-command Bash
sandbox, Conductor, OpenClaw, CI step runners), the session leader's exit
sends SIGHUP to every PID in the session — killing the bun server and
its Chromium grandchildren within seconds of a successful `connect`.
Setting `BROWSE_PARENT_PID=0` (already done by the `connect` command and
pair-agent) disables the parent-process watchdog but does NOT save the
server here: SIGHUP from session teardown still reaps it.
Replace the macOS/Linux `Bun.spawn().unref()` with Node's
`child_process.spawn({ detached: true })`, which calls setsid() and
gives the server its own session leader role (PPID=1, STAT=Ss). This
mirrors the Windows path's rationale (PR #191 by @fqueiro) — same root
cause, different OS surface.
Verified on macOS in Conductor: pre-fix the server dies ~10–15s after
connect across separate Bash invocations; post-fix the same PID stays
alive (PPID=1, SESS=0, STAT=Ss) and responds to `status`/`goto`/
`snapshot` across many separate shell calls.
The `proc?.stderr` startup-error branch is removed since both platforms
now spawn with `stdio: 'ignore'`; both fall through to the on-disk
`browse-startup-error.log` written by `server.ts`'s start().catch.
* fix(design): bump image-gen timeout to 240s + pin gpt-image-2
The design binary calls /v1/responses (gpt-4o + image_generation tool,
quality:high, 1536x1024) but aborted the request after a hardcoded 120s.
That class of request consistently takes ~140-160s end-to-end, so every
generate/variants/evolve/iterate call aborted before the image returned.
In /design-shotgun this cascades: Step 3c launches N parallel agents,
each calling `$D generate`, each aborts at 120s and retries, all fail,
the comparison board never opens — the skill appears to hang indefinitely.
Reproduced the exact API call with a longer budget: HTTP 200, valid
image, 143.5s. A real /design-shotgun run after the patch generated 3
variants in parallel at 150.0s / 161.0s / 152.1s, all exit 0 — note the
161s case, which a naive 150s bump would still have failed.
- Bump AbortController timeout 120_000 -> 240_000 in generate.ts,
variants.ts, evolve.ts, iterate.ts (both call sites)
- Pin the image_generation tool to model "gpt-image-2"
design/test/variants-retry-after.test.ts: 5 pass, 0 fail. The
feedback-roundtrip.test.ts failures are a pre-existing browse-module
breakage (session.clearLoadedHtml undefined), unrelated to this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: fill coverage gaps for PRs #1606, #1612, #1620
Three cherry-picked PRs in this wave landed without unit-test coverage for
the specific invariant they protect:
#1606 (@andrey-esipov) — LC_ALL=C pin in _gstack_gbrain_validate_varname
8 tests by sourcing bin/gstack-gbrain-lib.sh and calling the validator
directly. Asserts uppercase/digit/underscore accepted, lowercase
REJECTED (the macOS-locale regression case), mixed-case rejected,
LC_ALL=C scoping is local (doesn't leak to caller).
#1612 (@bharat2913) — setsid daemonize via Node child_process.spawn
4 static-invariant tests on browse/src/cli.ts. The actual setsid
syscall is hard to assert without a real spawn, so we pin the source
shape: nodeSpawn imported from child_process; non-Windows branch uses
nodeSpawn(...) with detached:true and .unref(); comment documents
setsid/SIGHUP root cause; Bun.spawn() is NOT used on macOS/Linux.
#1620 (@davidfoy, re-authored into .tmpl per A3) — §4a-postfail
12 static invariants on land-and-deploy/SKILL.md.tmpl + generated
SKILL.md. Pins all three state branches (MERGED/OPEN/CLOSED), the
authoritative state query, the merge-SHA capture, non-destructive
worktree cleanup with uncommitted-work guard, autoMergeRequest probe
on OPEN, hard "never retry gh pr merge" rule, and atomic regen
propagation.
Failing build if any of the three invariants regresses.
Note: gbrain-lib-validate-varname.test.ts also surfaces a pre-existing
glob-pattern overpermissiveness (hyphens + dots accepted) — not in
#1606's scope; documented inline as a separate cleanup target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(learnings): align injection-prevention tests with PR #1619 tagged-line shape
PR #1619 (preserve current entries in cross-project search) refactored
gstack-learnings-search to tag rows inline (`current\t<json>` vs
`cross\t<json>`) instead of filtering inside the bun block via
process.env.GSTACK_SEARCH_SLUG. The bun block no longer reads SLUG or
CROSS env vars — it parses the per-line tag and sets a per-entry
_crossProject flag.
The pre-existing test/learnings-injection.test.ts still asserted on the
old SLUG + CROSS env var shape. Updates:
- Remove the SLUG env var assertion (no longer set on bash command line)
- Remove the bun-block CROSS env var assertion (block reads the tag now,
not the env)
- Add a new positive assertion that the bun block parses the tag
(sourceTag | tabIndex | crossProject)
- Keep the shell-interpolation safety assertion unchanged — that's
independent of the SLUG refactor
The CROSS env var is still SET on the bash command line (it controls
whether the cross-project find runs at all), but the bun child no longer
reads it. The existing "env vars set on bash command line" test continues
to pin that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(fixtures): regenerate ship-SKILL.md golden baselines
ship/SKILL.md consumes the Confidence Calibration resolver via the
preamble pipeline. This wave's #1539 pre-emit verification gate extends
the resolver text, which propagated to ship/SKILL.md via gen:skill-docs.
The golden fixtures in test/fixtures/golden/ matched the pre-#1539 shape
and failed the host-config regression check.
Refreshes claude-ship-SKILL.md, codex-ship-SKILL.md, and factory-ship-SKILL.md
to match the current generated output. Matches the Daegu wave's bisect
commit 23 ("test(fixtures): regenerate ship-SKILL.md golden baselines").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(gbrain-detect): include gbrain_pooler_mode in schema regression (PR #1591)
PR #1591 (PgBouncer transaction-mode detection, @mikeangstadt) added
gbrain_pooler_mode to the gstack-gbrain-detect JSON output but did not
update the schema regression check in
test/gstack-gbrain-detect-mcp-mode.test.ts. Adding the key in alphabetical
order matching the rest of the schema array. Downstream sync-gbrain ignores
unknown keys, so this is forward-compat.
Without this, the test fails with a diff:
+ "gbrain_pooler_mode"
because keys is the actual set returned and the expected array was
pre-#1591.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): v1.43.0.0 — post-Daegu paper-cut wave
Bumps VERSION 1.42.2.0 → 1.43.0.0 (MINOR per scale-aware bump rules: new
env-var surface GSTACK_SYNC_*_TIMEOUT_MS + GSTACK_CHROMIUM_NO_SANDBOX,
behavior expansion in browse/src/browser-manager.ts headless launch,
three skill-template prompt changes affecting /retro, /review,
/sync-gbrain).
CHANGELOG entry leads with what stopped happening: /retro stops
fabricating retros against stale bases, /sync-gbrain stops SIGTERM-looping
35-min restarts on big brains, /review stops shipping framework FPs the
reviewer never grep'd.
18 fixes total — 15 community PRs + 3 self-filed silent-failure issues
(#1624, #1611, #1539) — in one bundled PR with 26 bisect commits and 7
new regression test files. Every wave-touched test file passes in
isolation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): bump v1.43.0.0 → v1.43.2.0 for queue collision
CI check-version-stale flagged v1.43.0.0 already claimed by PR #1574
(garrytan/colombo-v3). PR #1639 (garrytan/muscat-v3) claims v1.43.1.0.
Next available MINOR slot is v1.43.2.0.
Bump VERSION + package.json + CHANGELOG entry header. No behavior
changes — purely re-versioning to clear the queue collision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com>
Co-authored-by: Andrey Esipov <andrey.esipov@outlook.com>
Co-authored-by: David Foy <davidfoy@users.noreply.github.com>
Co-authored-by: mikeangstadt <mike.angstadt@closedloop.ai>
Co-authored-by: 0xDevNinja <manmit0x@gmail.com>
Co-authored-by: techcenter68 <techcenter68@users.noreply.github.com>
Co-authored-by: shohu <shohu33@gmail.com>
Co-authored-by: Bharat <bharat@theysaid.io>
Co-authored-by: Matteo Hertel <info@matteohertel.com>
|
||
|
|
7ca04d8ef0 |
v1.42.0.0 Daegu wave: 23 community-filed bugs + PTY classifier enforcement (24 bisect commits) (#1594)
* fix(gstack-paths): guard CLAUDE_PLUGIN_DATA against cross-plugin contamination (#1569) gstack-paths previously trusted CLAUDE_PLUGIN_DATA as a fallback for GSTACK_STATE_ROOT whenever GSTACK_HOME was unset. When another plugin (e.g. Codex) persists its own CLAUDE_PLUGIN_DATA into the session env via CLAUDE_ENV_FILE, gstack picked it up and wrote checkpoints, analytics, and learnings into that plugin's directory. Anyone with the Codex plugin installed alongside gstack hit this silently. Fix: guard the CLAUDE_PLUGIN_DATA branch so it only fires when CLAUDE_PLUGIN_ROOT confirms we're running as the gstack plugin (path contains "gstack"). Skill installs fall through to \$HOME/.gstack. Contributed by @ElliotDrel via #1570. Closes #1569. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gbrain-sync): sourceLocalPath handles wrapped {sources:[...]} shape from gbrain v0.20+ gbrain v0.20+ changed `gbrain sources list --json` to return {sources: [...]} instead of a flat array. sourceLocalPath crashed upstream with `list.find is not a function` on every /sync-gbrain invocation against modern gbrain. Accept both shapes for forward/backward compat, matching probeSource/sourcePageCount in lib/gbrain-sources.ts. Contributed by @jakehann11 via #1571. Closes #1567. Supersedes #1564 (@tonyjzhou, same fix, different shape — credit retained). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(brain-context-load): probe gbrain via execFile, not shell builtin (#1559) gbrainAvailable() used `execFileSync("command", ["-v", "gbrain"])`, which fails in any environment where the `command` builtin isn't on the spawned process's PATH (most non-interactive shells). The probe then reported gbrain as missing even when it was installed, and context-load silently skipped vector/list queries. Fix: probe `gbrain --version` directly with a 500ms timeout (matching the rest of the file's MCP_TIMEOUT_MS). Same semantics, works everywhere execFile works. Contributed by @jbetala7 via #1560. Closes #1559. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(gbrain-doctor): pin schema_version:2 doctor parse path (#1418) Adds an exec-path regression test that runs a fake gbrain shim emitting the v0.25+ doctor JSON shape (schema_version: 2, status: "warnings", exit 1 for health_score < 100, no top-level `engine` field). Confirms freshDetectEngineTier recovers stdout from the non-zero exit and falls back to GBRAIN_HOME/config.json for the engine label. The pre-existing test for #1415 only stripped gbrain from PATH; this test exercises the actual doctor parse path, closing the gap that codex's plan review flagged. Also documents the schema_version separation in lib/gbrain-local-status.ts: the local CacheEntry stays at version 1, distinct from the doctor-output schema_version which we accept across versions in gstack-memory-helpers. Closes #1418 (credit @mvanhorn for surfacing the doctor + schema_v2 collapse). The fix landed pre-emptively in v1.29.x; this commit pins it with a stronger test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(memory-ingest): pin put_page regression + scrub stale name from --help and comments (#1346) #1346 reported that gstack-memory-ingest still called the renamed gbrain put_page subcommand on gbrain v0.18+. The actual code migrated to `gbrain put` and later to batch `gbrain import <dir>` before this report landed — only documentation lag remained. This commit: - Updates the --help string ("Skip gbrain put calls (still updates state file)") so user-facing docs match the shipped subcommand - Updates two inline comments that still referenced the old name - Adds test/memory-ingest-no-put_page.test.ts: a regression pin that strips comments from bin/gstack-memory-ingest.ts and fails the build if "put_page" appears in any active code or string literal, plus a sanity check that the file still calls a supported gbrain page-write verb (put or import) Closes #1346. Reporter @kylma-code surfaced the doc lag; the original code migration credit is on the v1.27.x wave. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(resolvers): rewrite all gbrain put_page instructions to canonical put <slug> scripts/resolvers/gbrain.ts emitted user-facing copy-paste instructions using the renamed `gbrain put_page` subcommand across 10 skills (office-hours, investigate, plan-ceo-review, retro, plan-eng-review, ship, cso, design-consultation, fallback, entity-stub). Every gstack user copying those snippets hit "unknown command: put_page" on gbrain v0.18+. This commit: - Rewrites all 10 instruction templates to use `gbrain put <slug> --content "$(cat <<EOF...EOF)"` with title/tags moved into YAML frontmatter inside --content, matching the v0.18+ subcommand shape - Updates README.md and USING_GBRAIN_WITH_GSTACK.md "common commands" table to reference `gbrain put` and `gbrain get` - Adds test/resolvers-gbrain-put-rewrite.test.ts pinning two invariants: (a) resolver source ships only canonical instructions, (b) every tracked SKILL.md file is free of `gbrain put_page` CHANGELOG entries are deliberately left untouched (historical record). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(build): extract package.json build to scripts/build.sh for Windows Bun compat (#1538, #1537, #1530, #1457, #1561) Bun's Windows shell parser rejects multiple constructs the inline package.json build chain used: brace groups `{ cmd; }`, subshells with redirection `( git ... ) > path/.version`, and (in Bun 1.3.x) subshells near redirections in general. Every Windows install + every auto-upgrade since v1.34.2.0 has failed on `bun run build`. Extracts the build chain to scripts/build.sh and the .version writes to scripts/write-version-files.sh. POSIX-portable, no Bun shell parsing involved. Also adds Windows-specific bun.exe handling for non-ASCII PATHs (a separate Windows footgun where Bun's --compile fails when the binary lives under a path with non-ASCII chars). Updates test/build-script-shell-compat.test.ts to assert the new shape: no subshells with redirections anywhere in the build chain, and build delegates to scripts/build.sh which delegates .version writes. Contributed by @Charlie-El via #1544. Supersedes #1531 (@scarson, fixed in build helper), #1480 (@mikepsinn, partial overlap), #1460 (@realcarsonterry, brace-group fix subsumed) — credit retained. Closes #1538, #1537, #1530, #1457, #1561. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(windows): .exe glob in .gitignore + .exe extension resolution in find-browse (#1554) bun build --compile on Windows appends .exe to the output filename, producing browse.exe instead of browse. find-browse's existsSync probe only checked the bare path and returned null on Windows even when the binary was correctly built. .gitignore similarly only excluded the bare bin/gstack-global-discover path, leaving the .exe variant tracked. This commit: - .gitignore: changes `bin/gstack-global-discover` → `bin/gstack-global-discover*` so the Windows .exe variant is ignored - browse/src/find-browse.ts: adds isExecutable + findExecutable helpers that fall back to .exe/.cmd/.bat probing on Windows, mirroring the same helper already in make-pdf/src/browseClient.ts and pdftotext.ts Contributed by @Mike-E-Log via #1554. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(windows): add fresh-install E2E gate that runs bun run build on windows-latest Adds .github/workflows/windows-setup-e2e.yml as the gate that catches Bun shell-parser regressions in the build chain before they reach users. Triggers on PRs touching package.json, scripts/build.sh, scripts/write-version-files.sh, setup, browse cli/find-browse, or gstack-paths. What it verifies: 1. bun run build completes on Windows (the previously-broken path that #1538/#1537/#1530/#1457/#1561 reported) 2. All compiled binaries land on disk (browse.exe, find-browse.exe, design.exe, gstack-global-discover.exe) 3. find-browse resolves to the .exe variant on Windows (regression gate for #1554) 4. gstack-paths returns non-empty GSTACK_STATE_ROOT/PLAN_ROOT/TMP_ROOT on Windows (regression gate for #1570) Complements the existing windows-free-tests.yml (curated unit subset); this new workflow exercises the install path itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(codex): move diff scope into prompt instead of --base (Codex CLI 0.130+ argv conflict) (#1209) Codex CLI ≥ 0.130.0 rejects passing a custom prompt and --base together (mutually exclusive at argv level). Every /codex review, /review, and /ship structured Codex review call ended with an argv error before the model ran. Fix: scope the diff in prompt text using "Run git diff origin/<base>...HEAD 2>/dev/null || git diff <base>...HEAD" instead of `--base <base>`. Preserves the filesystem boundary instruction across all invocations and keeps Codex's review prompt tuning. Touches: - codex/SKILL.md.tmpl + regenerated codex/SKILL.md - scripts/resolvers/review.ts + regenerated review/SKILL.md, ship/SKILL.md - test/gen-skill-docs.test.ts: new regression that fails if any of the five known files still contain the prompt+--base shape - test/skill-validation.test.ts: corresponding negative + positive pin on the rendered SKILL.md files Contributed by @jbetala7 via #1209. Closes #1479. Supersedes #1527 (@mvanhorn — same intent, different patch shape, CONFLICTING) and #1449 (@Gujiassh — broader refactor, CONFLICTING). Credit retained in CHANGELOG. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(review): diff from git merge-base, not git diff origin/<base> (#1492) git diff origin/<base> shows everything since the common ancestor in both directions — it includes commits that landed on origin/<base> after this branch was created as deletions. That made /review and /ship's pre-landing structured review report inflated diff totals and flagged "removed" code that was actually still present in the working tree. Fix: compute DIFF_BASE via git merge-base origin/<base> HEAD and diff the working tree against that point. Same coverage of uncommitted edits, no phantom deletions from out-of-order base advancement. Applies to /review's Step 1 (diff existence check), Step 3 (get the diff), the build-on-intent scope-creep check, the structured review DIFF_INS/DIFF_DEL stats, and the Claude adversarial subagent prompt. Same change flows into ship/SKILL.md via the shared resolver. Touches: - review/SKILL.md.tmpl + regenerated review/SKILL.md, ship/SKILL.md - scripts/resolvers/review.ts - scripts/resolvers/review-army.ts Contributed by @mvanhorn via #1492. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(codex): pin filesystem-boundary preservation across all codex review surfaces (#1503, #1522) #1503 reported that the bare codex review --base path stripped the filesystem boundary instruction, letting Codex spend tokens reading .claude/skills/ and agents/. #1522 proposed adding a skill-path detector that switched to the custom-instructions route when the diff touched skill files. After C10 (#1209) restructured codex review to always carry the boundary in the prompt (the prompt+--base argv conflict forced the restructure), the skill-path detector becomes redundant — every default call already preserves the boundary. This commit pins the post-#1209 invariant with a test that fails the build if any future refactor strips the boundary from codex/SKILL.md, review/SKILL.md, or ship/SKILL.md. Closes #1503 by regression test. #1522 (@genisis0x) is superseded by #1209 (the prompt rewrite covers its safety concern); credit retained in CHANGELOG. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(skills): use command -v instead of which for codex detection (#1197) `which` is not on PATH in every shell — some Windows shells, BusyBox- only containers, and minimal CI images all fail when skills probe codex availability via `which codex`. `command -v` is a POSIX builtin and always available where the skill is running. Touched: - codex/SKILL.md.tmpl: CODEX_BIN=$(command -v codex || echo "") - scripts/resolvers/review.ts and scripts/resolvers/design.ts: 3 + 3 sites each rewritten to `command -v codex >/dev/null 2>&1` - Regenerated all 10 affected SKILL.md files (codex, review, ship, design-consultation, design-review, office-hours, plan-ceo-review, plan-design-review, plan-devex-review, plan-eng-review) - test/skill-validation.test.ts: updated pin + defensive regression test that fails if `which codex` returns to codex/SKILL.md - test/skill-e2e-plan.test.ts: updated summary regex Contributed by @mvanhorn via #1197. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(codex): surface non-zero exits so wrappers stop reading as silent stalls (#1467, #1327) When codex exits non-zero (parse errors, arg-shape breaks, model API errors that propagate as non-zero status), the calling agent previously saw an empty output and burned 30-60 minutes misdiagnosing as a silent model/API stall. The hang-detection block only caught exit 124 (the timeout-wrapper signal). Adds elif blocks in all four codex invocation sites (Review default, Challenge, Consult new-session, Consult resume) that: - Echo "[codex exit N] <stderr first line>" to stdout - Indent the first 20 stderr lines for inline context - Log codex_nonzero_exit telemetry tagged with the call site Contributed by @genisis0x via #1467. Closes #1327. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(design): disclose OpenAI key source + warn on cwd .env match (#1278, closes #1248) The design binary previously called process.env.OPENAI_API_KEY without checking where the key came from. If a user ran $D inside someone else's project that had OPENAI_API_KEY in its .env, the resulting generation billed that project's account. Silent and irreversible. Fix: resolveApiKeyInfo() returns both the key and its source. When the env-var path matches an OPENAI_API_KEY entry in the current directory's .env, .env.<NODE_ENV>, or .env.local file, we set a warning. requireApiKey() prints "Using OpenAI key from <source>" plus the warning before the run — never the key itself. Adds 6 unit tests covering: config-vs-env precedence, env-only (no match), env+cwd .env match, quoted/exported values, value-mismatch (no false positive), and the no-leak invariant for requireApiKey stderr output. Contributed by @jbetala7 via #1278. Closes #1248. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(browse): guard full-page screenshots against Anthropic vision API >2000px brick (#1214) Full-page screenshots of tall pages routinely exceeded 2000px on the longest dimension, silently bricking the agent's session: the resulting base64 reached the Anthropic vision API which rejected the oversized image, leaving the agent burning turns on a useless blob with no stderr trace from the browse side. Adds browse/src/screenshot-size-guard.ts as a shared helper: - guardScreenshotBuffer(buf) → downscales in-memory if max(w,h) > 2000 - guardScreenshotPath(path) → file-mode variant that rewrites in place - Aspect ratio preserved via sharp's resize fit:inside - Stderr diagnostic on any downscale so callers can see when it fired - Lazy sharp import so non-screenshot paths pay no startup cost Wires the guard into all three full-page callsites codex review flagged: - browse/src/snapshot.ts: annotated + heatmap fullPage captures - browse/src/meta-commands.ts: screenshot command (path + base64 fullPage modes) plus the responsive 3-viewport sweep - browse/src/write-commands.ts: prettyscreenshot fullPage path Covers seven unit cases (pass-through, downscale, aspect ratio, exactly-2000px edge, file-mode rewrite) plus a static invariant test that fails the build if any of the three callsites stops importing the guard. Closes #1214. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(security): add Node sidecar entry for L4 prompt-injection classifier (#1370) The L4 TestSavant classifier in browse/src/security-classifier.ts can't be imported into the compiled browse server (onnxruntime-node dlopen fails from Bun's compile extract dir per CLAUDE.md). The agent that used to host it (sidebar-agent.ts) was removed when the PTY proved out — leaving the classifier file shipped but with zero callers. Exactly the gap codex flagged in #1370. Adds browse/src/security-sidecar-entry.ts: a Node script that runs the classifier as a subprocess of the browse server. It reads NDJSON requests from stdin and writes id-correlated NDJSON responses to stdout, supporting: - op: "scan-page-content" — full L4 classifier scan - op: "ping" — liveness probe for the client's health check - op: "status" — classifier readiness (used by /pty-inject-scan to surface l4 { available: bool } in its response) Plus browse/src/find-security-sidecar.ts: a resolver that locates node + the bundled JS entry (browse/dist/security-sidecar.js, built in a follow-up package.json change) or falls back to the dev TS entry. Returns null cleanly when node isn't on PATH so the calling endpoint can degrade per D7 (extension WARN + user confirm). C17 of the security-stack wave. C18 adds the IPC client + lifecycle management; C19 wires the endpoint; C20 routes the extension through it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(security): sidecar IPC client with lifecycle + circuit breaker (#1370) Adds browse/src/security-sidecar-client.ts to manage the Node L4 classifier subprocess from the compiled browse server: - Lazy spawn on first scan; reuses the same process across requests - Id-correlated request/response via NDJSON over stdio - 5s default per-scan timeout; 64KB payload cap (short-circuits before spawn so oversized requests don't waste a process) - 3-in-10-minutes respawn cap → trips circuit breaker; subsequent scans throw immediately so the /pty-inject-scan endpoint can surface l4 { available: false } to the extension and degrade to WARN+confirm - process.on('exit') sends SIGTERM to the child for clean teardown - isSidecarAvailable() lets the endpoint probe before scan calls so the response shape reflects degraded mode honestly Unit tests cover the payload cap, the availability probe, and the breaker-doesn't-crash invariant under repeated rejected calls. C18 of the security-stack wave. C19 adds POST /pty-inject-scan; C20 routes the extension through it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(security): add POST /pty-inject-scan endpoint for pre-PTY-inject scans (#1370) The sidebar's gstackInjectToTerminal callers (toolbar Cleanup, Inspector "Send to Code") were piping page-derived text directly into the live claude PTY with ZERO classifier processing — the gap codex flagged in #1370. The documented sidebar security stack had a hole the size of every Cleanup-button click. Adds POST /pty-inject-scan to browse/src/server.ts: - Local-only binding (NOT in TUNNEL_PATHS — tunnel attempts get the general 404 path; never reaches the scan logic) - Root-token auth via existing validateAuth() — 401 on unauth - 64KB request cap → 413 + payload-too-large body - 5s scan timeout via sidecar client - URL-blocklist forced to BLOCK in PTY context (page-derived REPL input is higher-risk than ordinary tool output) - L4 ML classifier via the sidecar when available; degrades to WARN per D7 when sidecar is unavailable - Response goes through JSON.stringify(..., sanitizeReplacer) per v1.38.0.0 Unicode-egress hardening - Imports only from security-sidecar-client.ts, never directly from security-classifier.ts (which would brick the compiled Bun binary) Seven static-invariant tests pin the POST verb, auth gate, 64KB cap, tunnel-listener exclusion, sanitizeReplacer wrapping, l4 availability shape, and the no-direct-classifier-import rule. C19 of the security-stack wave. C20 routes the extension through it; C21 adds the invariant AST check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(extension): route gstackInjectToTerminal through /pty-inject-scan (#1370) Closes the documented-vs-shipped gap codex flagged in #1370. The sidebar's two PTY-injection call sites (Inspector "Send to Code" and toolbar Cleanup) now pre-scan via the new /pty-inject-scan endpoint before writing to the live claude REPL. Adds window.gstackScanForPTYInject(text, origin) to extension/sidepanel-terminal.js: - Async, returns { allow, verdict, reasons, l4 } - POST to /pty-inject-scan with the existing root-token auth - WARN+confirm on scan failure (network down, sidecar absent, etc.) rather than silent PASS — D7 honest-degradation gstackInjectToTerminal stays synchronous, returns boolean. Per D6: keeping the inject sync means existing `const ok = ...?.()` callers don't break, and the invariant test in test/extension-pty-inject-invariant.test.ts can statically pin that every call goes through the scan first. extension/sidepanel.js call sites updated: - inspectorSendBtn click → await scan, BLOCK drops + WARN prompts via window.confirm, PASS injects silently - runCleanup() → same flow. Static cleanup prompt always PASSes but still routes through scan to honor the invariant. C20 of the security-stack wave. C21 adds the static invariant test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(security): invariant — extension PTY inject must be scan-gated (#1370) Static-analysis invariant test that fails the build if any extension/*.js path calls window.gstackInjectToTerminal without a preceding window.gstackScanForPTYInject in the same enclosing function. Closes the documented-vs-shipped gap codex demanded a machine check on. Rules: - Rule 1: any file that calls inject must also reference scan - Rule 2: in the enclosing function (function declaration, arrow, async (), event handler), a scan call must appear before the inject call by source position - Exemption: sidepanel-terminal.js (the file that DEFINES the inject function) is exempt from Rule 2 since the definition is not a call Plus two structural checks: - sidepanel-terminal.js defines both the inject and scan functions - inject stays SYNCHRONOUS (no `async` modifier) per D6 — async would silently break the `const ok = ...?.()` pattern at every caller C21 of the security-stack wave. The sidecar architecture (#1370) is complete: server-side L1-L3 + L4-via-sidecar (C17+C18+C19), extension pre-scan wiring (C20), and now the regression gate (C21). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(browse): opt-in extended stealth mode with 6 detection-vector patches (#1112) Rebases @garrytan's PR #1112 (Apr 2026, abandoned) onto the current browse/src/stealth.ts contract. The existing minimal "codex narrowed" stealth (webdriver-mask + AutomationControlled launch arg) stays the default. PR #1112's six additional patches are added behind an opt-in GSTACK_STEALTH=extended env flag. Extended-mode patches (applied AFTER the default mask, in order): 1. delete navigator.webdriver from prototype (not just the getter — detectors check `"webdriver" in navigator`) 2. WebGL renderer spoof to Apple M1 Pro (SwiftShader was the #1 software-GPU tell in containers) 3. navigator.plugins returns a PluginArray-prototype-passing array with MimeType objects and namedItem() 4. window.chrome populated with chrome.app, chrome.runtime, chrome.loadTimes(), chrome.csi() with realistic shapes 5. navigator.mediaDevices backfilled when headless drops it 6. CDP cdc_*-prefixed window globals cleared Why opt-in: the default mode's contract is fingerprint CONSISTENCY, which protects against detectors that flag spoofing mismatch. Extended mode actively lies about the environment; sites that reflect on these properties can break. Users who hit detection in default mode can flip GSTACK_STEALTH=extended for SannySoft 100% pass-rate. Twenty unit tests pin the env-flag semantics, all six patches' code presence, and the applyStealth wiring order. Live SannySoft pass-rate verification stays in the periodic-tier E2E suite. Contributed by @garrytan via #1112 (rebased — original PR opened before the codex-narrowed minimum landed; rebase preserves the narrowed default while adding the SannySoft-passing path as opt-in). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(fixtures): regenerate ship-SKILL.md golden baselines after C10-C13 + C16 templates Updates the three ship-SKILL.md golden baselines (claude, codex, factory hosts) to match the new shape produced by: - C10 #1209 codex argv (prompt + diff scope, no --base) - C11 #1492 merge-base diff (DIFF_BASE= preamble) - C13 #1197 command -v for codex detection - C12 + boundary preservation per regen-enforcing test Per CLAUDE.md SKILL.md workflow: edit the .tmpl, run gen:skill-docs, commit the regenerated outputs together. Goldens are part of the regen contract — without this commit, test/host-config.test.ts' golden-baseline checks fail with the diff codex review surfaced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): v1.41.0.0 — Daegu wave (24 bisect commits, 14 user-facing fixes) Bumps VERSION 1.40.0.0 → 1.41.0.0. CHANGELOG entry follows the release-summary format in CLAUDE.md: two-line headline, lead paragraph, "The numbers that matter" table, "What this means for builders" closer, then itemized Added/Changed/Fixed/For contributors with inline credit to every PR author and original issue reporter. Scale-aware bump per CLAUDE.md: 24 commits, ~6000 LOC net, substantial new capability across security (PTY sidecar wiring), install (Windows build chain), compat (gbrain 0.18-0.35, Codex CLI 0.130+), and quality (screenshot guard, design key disclosure, extended stealth opt-in). MINOR is the right call. Closes for users: #1567, #1559, #1569, #1346, #1418, #1538, #1537, #1530, #1457, #1561, #1554, #1479, #1503, #1248, #1214, #1370, #1327, #1193 pattern, #1152 pattern. Credit retained inline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(find-browse): resolve source-checkout layout <git-root>/browse/dist/browse[.exe] windows-setup-e2e.yml runs `bun browse/src/find-browse.ts` against a freshly-built repo where binaries land at browse/dist/browse.exe (no .claude/skills/gstack/ install layout). The previous markers chain only matched .codex/.agents/.claude prefixed paths, so find-browse exited "not found" even when the binary was present. Adds a source-checkout fallback after the marker scan: if no installed layout resolves but <git-root>/browse/dist/browse[.exe] exists, return that. Three real callers hit this path: - gstack repo dev workflow before `./setup` runs - windows-setup-e2e.yml CI (the breakage that surfaced this) - make-pdf consumers running from a sibling source checkout Smoke-verified: a fresh git repo with browse/dist/browse on disk now resolves through the source-checkout branch (was returning null before this commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): bump v1.41.0.0 → v1.42.0.0 to clear queue collision with #1574 The version-gate workflow flagged a collision: PR #1574 (garrytan/colombo-v3) already claims v1.41.0.0, and #1592 (fix/audit-critical-high-bugs) claims v1.41.1.0. Per CLAUDE.md's workspace-aware ship rule, queue-advancing past a claimed version within the same bump level is permitted — MINOR work landing on top of a queued MINOR still reads as MINOR relative to main. Util's suggested next slot is v1.42.0.0; taking it. CHANGELOG entry header bumped + dated 2026-05-19; entry body unchanged (same wave content, same credit list). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
026751ea20 |
v1.40.0.0 fix wave: gbrain sync hardening (8 community PRs + migration) (#1547)
* fix(gbrain-sync): fold hostname into code-source id hash + migration (#1414) Cherry-picked from #1468 by 0xDevNinja and extended with the hostname-fold migration that codex review surfaced. Pre-fix `deriveCodeSourceId` hashed the absolute repo path alone, so two machines with identical home-dir layouts (chezmoi-managed dotfiles, ansible-provisioned VMs) derived the same id and clobbered each other's `local_path` in a federated brain. Last-writer-wins, with cryptic "Not a git repository" errors on the loser. Hash key is now `\${hostname}::\${path}`. Conductor worktrees on a single host stay distinct (path entropy unchanged within a host); cross-machine federations stop colliding. Migration (D1=B + codex refinements): every existing user has a pre-#1468 path-only-hash source id in their brain that no longer matches what `deriveCodeSourceId` produces. Without migration, the next sync registers a fresh source and orphans the old one. This commit adds: - \`derivePathOnlyHashLegacyId\` — separate helper for the pre-#1468 form. Distinct from \`deriveLegacyCodeSourceId\` (pre-pathhash v1.x form); both probes run. - \`planHostnameFoldMigration\` — feature-checks \`gbrain sources rename <old> <new>\` (exact argument shape, not just \`--help\`), gates on path-drift (skip migration if old source's \`local_path\` differs from current repo root), and falls back to register-new + sync-OK + remove-old when rename is unsupported. As of gbrain 0.35.0.0 the rename subcommand does not exist, so users go through the cleanup path; the rename path stays dormant until gbrain ships it. - \`removeOrphanedSource\` — called only AFTER new-source sync verifies page_count > 0. Closes the data-loss window codex flagged where "register new, remove old before sync" can wipe pages if sync fails. - \`sourceLocalPath\` — looks up a source's \`local_path\` from \`gbrain sources list --json\` for the drift gate. - Helpers accept an optional \`env\` parameter so tests can inject a gbrain shim via PATH without process-wide PATH mutation (Bun's spawnSync doesn't pick up runtime PATH changes). Pre-positions for commit 4's centralized gbrain-exec helper. - \`if (import.meta.main)\` guard around \`main()\` so the helpers can be imported for in-process unit tests. Tests cover: pure derivation, ids-match degenerate case, no-legacy short-circuit, path-drift skip path, rename path with shim, cleanup fallback when rename unsupported, cleanup fallback when rename call itself fails, source-lookup happy/missing/error paths. \`GSTACK_HOSTNAME\` env var is a test-only knob; production uses \`os.hostname()\`. Fixes #1414 Co-Authored-By: Claude <noreply@anthropic.com> * fix(gbrain-sync): cut source-id slugs on hyphen boundaries (+ #1357) Cherry-picked from #1481 by drummerms and extended with the explicit HTTPS-remote regression case for #1357 (decision D2=A). `constrainSourceId` truncated the slug with `slug.slice(-tailBudget)`, which cut mid-word when the boundary fell inside a token. For a repo where the combined `prefix-org-repo-pathhash` exceeded 32 chars, this produced embarrassing artifacts like `gstack-code-kill-270c0001-c32152` (from `drummerms-av-sow-wiz-skill-270c0001`). Two changes carried from #1481, adapted for the #1468 hostpathhash: 1. `constrainSourceId` now walks hyphen-separated tokens from the right, accumulating whole tokens until adding the next would exceed `tailBudget`. When no token fits, falls through to the existing `${prefix}-${hash}` form. 2. `deriveCodeSourceId` now retries with `repo-only-hostpathhash` (dropping the org segment) when the full `org-repo-hostpathhash` triggers truncation. Keeps the repo name readable when it fits at all. Plus a new test asserting the source id is period-free for the exact HTTPS-with-.git remote shape from #1357 (`https://github.com/foo/bar.git`). canonicalizeRemote strips `.git`; the sanitizer strips any residual non-alnum. The test closes #1357 by pinning the property. Closes #1357 Co-Authored-By: Claude <noreply@anthropic.com> * fix(gbrain): probe CLI without command builtin * fix(gbrain-sync): centralize gbrain spawn surface + seed DATABASE_URL Cherry-picked from #1508 by jasshultz, restructured per codex review #4 and #7 to widen scope and centralize the spawn surface. The bug: gbrain auto-loads .env.local from cwd via dotenv. When /sync-gbrain runs inside a Next.js / Prisma / Rails project whose .env.local defines its own DATABASE_URL (pointing at the app's local DB), gbrain reads that value instead of its own ~/.gbrain/config.json — auth fails, code + memory stages crash. This commit: - Adds lib/gbrain-exec.ts: buildGbrainEnv, spawnGbrain, execGbrainJson, execGbrainText, spawnGbrainAsync (the last one for memory-ingest's streaming gbrain import call). buildGbrainEnv seeds DATABASE_URL from ${GBRAIN_HOME:-$HOME/.gbrain}/config.json, returns a fresh env object (never the caller's by identity — codex review #11), and honors the GSTACK_RESPECT_ENV_DATABASE_URL=1 escape hatch. - Routes every gbrain spawn in bin/gstack-gbrain-sync.ts and bin/gstack-memory-ingest.ts through the helpers. Both files now own zero direct spawnSync("gbrain"|spawn("gbrain"|execFileSync("gbrain" call sites. - Threads buildGbrainEnv into the spawnSync("bun", [memory-ingest], ...) grandchild in runMemoryIngest (codex review #7). Without this, the parent fix is half-baked — the bun child inherits a clean env but needs DATABASE_URL pre-seeded too. spawnGbrainAsync inside memory-ingest provides defense in depth for standalone invocations. - Adds GBRAIN_HOME support — aligns with detectEngineTier (already honors GBRAIN_HOME) so all gstack-side gbrain calls agree on which config file matters. Resolves baseEnv.HOME first, then homedir(), so test injection works without process-wide HOME mutation. - Adds test/build-gbrain-env.test.ts: 10 unit tests covering all five env-seeding branches (seed from config / override caller / GSTACK_RESPECT escape hatch / missing config / unparseable config / no database_url field / GBRAIN_HOME path / object-identity guard / unrelated-vars preservation / idempotent-when-matches). - Adds test/gbrain-exec-invariant.test.ts: static-source check that greps both bin/gstack-gbrain-sync.ts and bin/gstack-memory-ingest.ts for direct spawnSync("gbrain"|spawn("gbrain"|execFileSync("gbrain"| execSync(...gbrain matches and fails the build if any are found. Refactor-proof against future contributors adding a new gbrain spawn without env threading. The invariant is intentionally narrow — only the two files where the DATABASE_URL bug actually hurts users are guarded. Migrating the spawn sites in lib/gbrain-local-status.ts, lib/gstack-memory-helpers.ts, and bin/gstack-brain-context-load.ts is a follow-up. Co-Authored-By: Jason Shultz <jasshultz@gmail.com> Co-Authored-By: Claude <noreply@anthropic.com> * fix(gbrain-sync): add .gbrain-source to consumer repo .gitignore (#1384) The v1.29.0.0 changelog promised .gbrain-source would be added to the consuming repo's .gitignore so the per-worktree pin stays local, but the change actually only added it to gstack's own .gitignore. Without the consumer-side entry, the pin gets committed and Conductor sibling worktrees of the same repo + branch step on each other's pin every time anyone commits. Add ensureGbrainSourceGitignored after a successful gbrain sources attach in runCodeImport. Idempotent on repeat runs (line-trim match), creates .gitignore if missing, logs a warning and continues on permission errors so a read-only checkout doesn't fail the sync. Gate the top-level main() call behind import.meta.main so tests can import the helper without triggering a full sync run on module load. Tests in test/gbrain-source-gitignore.test.ts cover: create-when-missing, append-without-trailing-newline, append-with-trailing-newline, idempotent on repeat, recognize whitespace-surrounded entry, no-throw on read-only file. 6 pass. * fix(gbrain-sources): bump gbrain sources list --json timeout 10s → 30s Supabase free-tier cold-starts can push `gbrain sources list --json` past 10s (observed 14.5s in the wild), causing probeSource() to throw ETIMEDOUT during /sync-gbrain code stage even though the underlying CLI was healthy. Matches the 30s ceiling already used by `sources add` / `sources remove` in the same file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(brain-allowlist): sync project-root eng-review-test-plan artifacts (#1452) Cherry-picked from #1465 by genisis0x and extended with the v1.40.0.0 upgrade migration that codex review #5 surfaced. #1465 alone only patches bin/gstack-artifacts-init, which means fresh installs and re-inits pick up the new pattern. But existing users who already ran v1.38.1.0 have a `.migrations/v1.38.1.0.done` marker — that migration won't re-run no matter what we change. So their installed `.brain-allowlist`, `.brain-privacy-map.json`, and `.gitattributes` stay without the new pattern, and `/plan-eng-review` artifacts continue to silently drop out of their federation queue. This commit: - bin/gstack-artifacts-init: adds projects/*/*-eng-review-test-plan-*.md to the three managed blocks. v1.38.1.0 covered design + test-plan; this completes the set for /plan-eng-review. - gstack-upgrade/migrations/v1.40.0.0.sh: targeted in-place repair for existing installs. Same idempotent jq-based shape as v1.38.1.0. Adds the new pattern to .brain-allowlist (before the USER ADDITIONS marker), .brain-privacy-map.json (as class=artifact), and .gitattributes (as merge=union). NEVER commits + pushes — the user controls when the patches ship to their federated artifacts repo. - test/artifacts-init-migration.test.ts: 5 new tests covering the v1.40.0.0 migration applied on top of a post-v1.38.1.0 state, jq patching, gitattributes append, idempotent re-run, and done-marker write when files are missing entirely. Co-Authored-By: Claude <noreply@anthropic.com> * fix(gbrain-install): skip postinstall on Windows MSYS/MINGW + post-install probe Cherry-picked from #1487 by genisis0x and extended with the post-install subcommand probe per T6 / codex review #19. `bun install` in $INSTALL_DIR fails on Windows MSYS/MINGW/Cygwin shells because gbrain's native postinstall script mis-parses path arguments and aborts with a non-zero exit, breaking gstack-gbrain-install for Windows users running git-bash/MSYS2. The package installs cleanly without scripts. This commit: - Adds Windows shell detection via `uname -s` matching MINGW*/MSYS*/CYGWIN*/Windows_NT (#1487's case statement already covers all four — codex review #18 confirmed MINGW* is included). Windows paths get `bun install --ignore-scripts`; macOS and Linux unchanged. - Adds a post-install probe of `gbrain sources --help`. `gbrain --version` already runs (D19 PATH-shadowing validation), but version success doesn't prove the subcommand surface is reachable — and `--ignore-scripts` may have skipped artifacts that subcommands need. Probe failure logs a clear warning (with Windows-specific remediation pointing at re-running `bun install` outside MSYS) but does NOT exit non-zero; users may still get value from gbrain even if the probe fails transiently. Refs #1271 Co-Authored-By: Claude <noreply@anthropic.com> * chore: v1.40.0.0 — gbrain sync hardening wave Bumps VERSION 1.39.2.0 → 1.40.0.0 (MINOR — substantial gbrain capability hardening across sync pipeline, install path, federation allowlist; ~600 net LOC added across 8 community PRs + plan-review refinements). CHANGELOG entry follows the release-summary format: two-line headline, lead paragraph, "numbers that matter" with before/after table across 8 user-visible surfaces, "what this means for builders" closer, itemized Added/Changed/Fixed/NOT fixed/For contributors sections. Per-commit contributor credits: 0xDevNinja, drummerms, Jayesh Betala, Jason Shultz, genisis0x. Also names NikhileshNanduri and realcarsonterry in the wave's "Fixed" section for independent submissions of the .gbrain-source gitignore bug. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: 0xDevNinja <manmit0x@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: drummerms <mike@av2o.com> Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com> Co-authored-by: Jason Shultz <jasshultz@gmail.com> Co-authored-by: genisis0x <manietdavv@gmail.com> |
||
|
|
33cb4715ef |
v1.39.2.0 feat: GSTACK_* env-shim for Conductor + gbrain/gstack setup docs (#1534)
* feat: GSTACK_* env-key shim for Conductor workspaces New lib/conductor-env-shim.ts promotes GSTACK_ANTHROPIC_API_KEY and GSTACK_OPENAI_API_KEY to canonical names when canonical is empty. Wired into the four TS entry points that hit paid APIs or gbrain embeddings: gstack-gbrain-sync.ts, gstack-model-benchmark, preflight-agent-sdk.ts, test/helpers/e2e-helpers.ts. Side-effect-only import, 15 lines total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: gbrain+gstack setup, Conductor env mapping (v1.39.2.0) USING_GBRAIN_WITH_GSTACK.md: new "What you get after setup" section, Path 4 (remote MCP / split-engine), /sync-gbrain workflow stages + watermark mechanics, "Conductor + GSTACK_* env vars" section, env vars table extended, two troubleshooting entries (silent embedding failure and FILE_TOO_LARGE watermark block). CONTRIBUTING.md "Conductor workspaces": new paragraph on the GSTACK_* prefix pattern and the four entry points importing the shim. VERSION 1.39.1.0 → 1.39.2.0 and CHANGELOG entry covering the shim + docs (full release-summary format with before/after table). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: unit coverage for conductor-env-shim Refactor lib/conductor-env-shim.ts to export promoteConductorEnv() so unit tests can manipulate env and call it directly (a bare side- effect IIFE on import isn't reachable from bun:test once cached). The on-import IIFE still runs — existing four-entry-point imports keep working unchanged. test/conductor-env-shim.test.ts covers all three branches: GSTACK_FOO present + FOO empty → promotion; FOO already set → no-overwrite; nothing in env → no-op. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: Conductor strips canonical API keys (not just "doesn't inherit") The prior docs framed the GSTACK_* prefix as collision-avoidance: "Conductor exposes API keys under a GSTACK_ prefix so it never collides with whatever the host system has set." That understates the mechanism — Conductor actively strips ANTHROPIC_API_KEY and OPENAI_API_KEY from every workspace's process env, so setting them in ~/.zshrc or .env doesn't help. The fix path is to set the GSTACK_-prefixed forms in Conductor's workspace env config; Conductor passes those through untouched. Three docs updated to reflect the strip, not the polite framing: USING_GBRAIN_WITH_GSTACK.md (Conductor section), CONTRIBUTING.md (Conductor workspaces paragraph), CHANGELOG.md (release summary). README.md gains a "Running gstack in Conductor?" callout in the GBrain section pointing at the canonical doc's anchor, plus a fourth path entry (remote gbrain MCP / split-engine) that was already documented in USING_GBRAIN but missing from the README summary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e362b0ae2f |
v1.37.0.0 feat: split-engine gbrain (remote MCP brain + local PGLite for code) (#1500)
* feat(gbrain): add lib/gbrain-local-status classifier with 5-state engine status + 60s cache
Foundation for split-engine gbrain: shared classifier used by both
bin/gstack-gbrain-detect (preamble probe) and bin/gstack-gbrain-sync.ts
(orchestrator SKIP-when-not-ok). Single source of truth.
Probes via `gbrain sources list --json` and classifies stderr against the
same patterns lib/gbrain-sources.ts:66-67 already uses ("Cannot connect to
database", "config.json"). Returns one of: ok, no-cli, missing-config,
broken-config, broken-db. Defensive default: unrecognized failures
classify as broken-config so the raw stderr can be surfaced upstream.
Cache at ~/.gstack/.gbrain-local-status-cache.json keyed on
{home, path_hash, gbrain_bin_path, gbrain_version, config_mtime, config_size}
with 60s TTL. Cache invalidates on any invariant change. --no-cache option
busts the cache for callers that just mutated state (/setup-gbrain,
/sync-gbrain after init/migration).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(gbrain): rewrite gstack-gbrain-detect bash→TS + add gbrain_local_status field
Replaces the bash detect helper with a bun shebang script sharing the
gbrain_local_status classifier from lib/gbrain-local-status.ts with the
sync orchestrator. Single source of truth for engine-status classification
between preamble-probe and orchestrator-skip paths.
Filename stays gstack-gbrain-detect (no .ts extension) so existing skill
preamble callers shell out unchanged. Shebang `#!/usr/bin/env -S bun run`
resolves bun at runtime.
Output is key/type backward-compatible with the bash version per plan
codex #5: the 9 pre-existing keys (gbrain_on_path, gbrain_version,
gbrain_config_exists, gbrain_engine, gbrain_doctor_ok, gbrain_mcp_mode,
gstack_brain_sync_mode, gstack_brain_git, gstack_artifacts_remote) stay
identical in name + type + value semantics. One new key added:
gbrain_local_status (5-state string enum).
Updates the existing schema regression at test/gstack-gbrain-detect-mcp-mode.test.ts
to include the new key. Adds test/gbrain-detect-shape.test.ts asserting
the regression contract for future changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gbrain): orchestrator SKIP when local engine not ok + remote-http transcripts via artifacts pipeline
Two changes in the sync orchestrator, both per plan D11/D12:
1. bin/gstack-gbrain-sync.ts: runCodeImport + runMemoryIngest call
localEngineStatus() (shared classifier from lib/gbrain-local-status.ts).
When status is not 'ok', return a SKIP stage result with a clear reason
instead of crashing with "source registration failed: gbrain not
configured". Brain-sync stage runs regardless — it doesn't depend on
local engine. dry-run preview path is gated above the check so it
continues to show would-do steps even when the engine is broken.
2. bin/gstack-memory-ingest.ts: when gbrain MCP is registered as
remote-http (Path 4), persist staged transcripts to
~/.gstack/transcripts/run-<pid>-<ts>/ instead of the ephemeral
~/.gstack/.staging-ingest-<pid>-<ts>/ tmp dir, and SKIP the local
`gbrain import` call entirely. The artifacts pipeline (gstack-brain-sync
push to git, brain admin pulls and indexes) handles routing to the
remote brain. Local PGLite (when present via Step 4.5) stays code-only.
State recording still happens — prepared pages get their mtime+sha256
stamped under remote-http mode so the next /sync-gbrain doesn't
re-stage them. Cleanup is skipped intentionally so the persisted dir
survives until gstack-brain-sync moves it.
Adds test/gbrain-sync-skip.test.ts covering 5 SKIP scenarios (broken-db,
broken-config, no-cli, missing-config, ok pass-through). All 25
sync-related unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gbrain): v1.34.0.0 migration notice + transcripts allowlist for artifacts pipeline
Per plan D5 + D11. Two pieces of the split-engine rollout:
1. gstack-upgrade/migrations/v1.34.0.0.sh — prints a one-time
discoverability notice for existing Path 4 (remote-http MCP) users
whose machine has no local engine yet. Tells them about /setup-gbrain
Step 4.5 (the new local-PGLite opt-in). Silent for everyone else.
User can suppress permanently via `gstack-config set
local_code_index_offered true`. Touchfile at
~/.gstack/.migrations/v1.34.0.0.done makes it idempotent.
2. bin/gstack-artifacts-init — adds `transcripts/run-*/*.md` and
`transcripts/run-*/**/*.md` to the managed allowlist so the
gstack-memory-ingest persistent staging dir (used in remote-http
mode per D11) gets pushed to the artifacts repo. Brain admin's
pull job then indexes transcripts into the remote brain.
Privacy class: behavioral (matches transcript content).
Adds test/gstack-upgrade-migration-v1_34_0_0.test.ts with 5 cases:
state match, no-MCP, local-config-present, opt-out, and idempotency.
All 5 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gbrain): /setup-gbrain Step 1.5/4.5 + /sync-gbrain Step 1.5 templates
Per plan D4, D10, D11, D12. Wires the skill prose to the new
split-engine flow + classifier introduced in earlier commits.
setup-gbrain/SKILL.md.tmpl:
- Step 1: detect output description now includes the v1.34.0.0
gbrain_local_status field (5 values).
- Step 1.5 (NEW): broken-db / broken-config remediation. AskUserQuestion
with 4 options — Retry / Switch to PGLite / Switch brain mode / Quit
(plan D4). Retry is recommended first since broken-db often = transient
Postgres outage. PGLite is explicitly one-way + destructive (moves
existing config to ~/.gbrain/config.json.gstack-bak-<ts>); rollback on
init failure restores the .bak (plan D7).
- Step 4d → Step 4.5 (NEW): in Path 4, after the verify step, offer
local PGLite for code search. AskUserQuestion Yes/No (plan D10/D11).
Yes path runs gstack-gbrain-install + `gbrain init --pglite --json`
with the same rollback-safe sequence. No path skips Steps 3/4/5/7.5.
- Step 10 verdict (Path 4): adds "Code search" row reflecting Step 4.5
choice. Updates "Transcripts" row to describe the new D11 routing
(artifacts repo → remote brain).
sync-gbrain/SKILL.md.tmpl:
- Step 1 split-engine prose: corrects the prior misleading claim that
"memory routes through whatever setup-gbrain configured, including
remote-MCP" (codex finding #3). Memory stage shells out to local
`gbrain import` in local-stdio mode; in remote-http mode it persists
to ~/.gstack/transcripts/ for the artifacts pipeline.
- Step 1.5 (NEW): local-engine pre-flight. STOP on no-cli, broken-config,
broken-db. Soft skip (continue with code+memory SKIP) on
missing-config + remote-http per plan D12. Surfaces actionable user
remediation message instead of the orchestrator crashing two stages
with ERR.
Regenerated SKILL.md for all hosts (claude, kiro, opencode, slate,
cursor, openclaw, hermes, gbrain). All 712 skill-validation + gen-skill-docs
tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(gbrain): .bak-rollback contract for Step 1.5 / 4.5 init failure path
Per plan D7 (rollback semantics) and codex #10 (rollback scope). The
/setup-gbrain skill instructs the model to follow a specific shell
sequence when running `gbrain init --pglite` against an existing
config:
1. mv ~/.gbrain/config.json ~/.gbrain/config.json.gstack-bak-<ts>
2. gbrain init --pglite --json
3. on non-zero exit: mv .bak back; surface error
This test verifies that contract using a fake `gbrain` binary that
fails on init. Three cases:
- FAILURE: gbrain init exits non-zero → broken config restored to
original path, no leftover .bak.
- SUCCESS: gbrain init exits 0 → new config in place, .bak survives
for audit (user reviews + deletes manually).
- SCOPE: any partial PGLite directory at ~/.gbrain/pglite/ is NOT
auto-cleaned. We only promise to restore config.json; PGLite
cleanup is the user's call (codex #10).
If the skill template rewrites this sequence in a future change, this
test should fail until the test's shell is updated too. That's the
point — keep the test and the skill template aligned.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(gbrain): periodic E2E for /setup-gbrain Path 4 + Step 4.5 Yes flow
End-to-end coverage of the new opt-in question via runAgentSdkTest.
Stubs the MCP endpoint at /tools/list with a 200 response carrying a
fake gbrain v0.32.3.0 serverInfo, and fakes the gbrain + claude CLIs
so init writes a PGLite config and mcp add succeeds. Asserts the model:
1. invokes gstack-gbrain-install (Step 4.5 Yes branch)
2. invokes `gbrain init --pglite --json`
3. writes a working ~/.gbrain/config.json with engine=pglite
4. registers the remote MCP via `claude mcp add --transport http`
5. never leaks the bearer token to CLAUDE.md
Classified as periodic-tier per plan D6 (codex #12 flagged AgentSDK
flakiness; gate-tier coverage of the split-engine behavior lives in the
deterministic unit tests at gbrain-local-status.test.ts and
gbrain-sync-skip.test.ts). Touchfile fires the test when the skill
template, install/verify/init helpers, the local-status classifier, or
the agent-sdk-runner harness changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(gbrain): bump migration to v1.35.0.0 after main merge
main shipped v1.34.0.0 (factory-export submodule) + v1.34.1.0 (update-check
hardening) while this branch was in flight. The migration file I named
v1.34.0.0.sh now belongs at v1.35.0.0 — the next minor on top of main,
matching the scale of split-engine work (new lib + orchestrator skip +
template overhaul + transcripts routing).
Renames the migration script and its test file; updates all internal
version references in both files. Behavior unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* perf(gbrain): memoize gbrain resolution + use --fast doctor in detect
Cuts detect's wall time substantially by sharing fork-exec results
between the helper that walks the JSON output and the localEngineStatus
classifier from lib/gbrain-local-status.ts.
Before: detect made 2x `command -v gbrain` calls (one in detect's
detectGbrain, one in the classifier's resolveGbrainBin) and 2x
`gbrain --version` calls. With memoization keyed on PATH, both
collapse to one fork each (~400ms saved per skill preamble).
Also adds `--fast` to the `gbrain doctor --json` call in detect so a
broken-db config (Garry's repro) doesn't burn a full 5s timeout on the
doctor's DB-connection check. The classifier still probes the DB
directly via `gbrain sources list --json` for engine reachability —
that's `gbrain_local_status`, separate from the coarse
`gbrain_doctor_ok` summary flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(gbrain): relax E2E assertions to smoke-test contract
Per codex #12 (AgentSDK harness is non-deterministic): the E2E now
asserts the model followed the split-engine path WITHOUT requiring a
specific subcommand sequence. Three assertions:
1. AskUserQuestion was called (model reached interactive branches)
2. At least one of {gstack-gbrain-install, `gbrain init --pglite`,
`claude mcp add`} fired (model followed the skill, not a no-op)
3. The fake bearer token never leaked to CLAUDE.md (security regression)
Deterministic per-step coverage of the same flow lives in the gate-tier
unit tests (gbrain-local-status, gbrain-sync-skip, init-rollback,
upgrade-migration). The E2E exists to catch the "model can't follow
the skill at all" regression class, not to pin the exact tool sequence.
Test passes in 280s against the live Agent SDK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(version): bump CLI smoke-test timeout to 15s (flaky at 5s under load)
The gstack-next-version integration smoke test spawns a child process
that does git operations + sibling-worktree probing. Wall time hovers
4-5s on M-series Macs; flakes at exactly 5001-5002ms when the test
suite runs under load (bun's parallel scheduling). Bumping per-test
timeout to 15s eliminates the flake without changing test logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v1.37.0.0)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d21ba06b5a |
v1.33.0.0 feat: /sync-gbrain memory-stage batch-import refactor (D1-D8) + F6/F9 + signal cleanup (#1432)
* refactor: batch-import architecture (D1-D8) + F6 atomic state + F9 full-file hash
bin/gstack-memory-ingest.ts: rewrite memory ingest around `gbrain import <dir>`
batch path. Replaces per-file gbrainPutPage loop (~470s of subprocess startup
per cold run) with prepare-then-batch:
walkAllSources
-> preparePages: mtime-skip + optional gitleaks (--scan-secrets) + parse
-> writeStaged: mkdir -p per slug segment, hierarchical (D1)
-> snapshot ~/.gbrain/sync-failures.jsonl byte offset
-> runGbrainImport (async spawn) -> parseImportJson
-> readNewFailures: read appended bytes, map back to source paths (D7)
-> state.sessions[path] = {...} for files NOT in failed set
-> saveStateAtomic (F6) + cleanupStagingDir
Architecture decisions:
D1 hierarchical staging dir
D2 cut over, deleted gbrainPutPage entirely
D3 source-file gitleaks made opt-in via --scan-secrets (gstack-brain-sync
owns the cross-machine boundary; per-file scan was redundant ~470s tax)
D4 OK/ERR verdict (no DEGRADED tri-state)
D5 unified state schema (no separate skip-list)
D6 trust gbrain content_hash idempotency (no skip_reason bookkeeping)
D7 byte-offset snapshot of sync-failures.jsonl + per-source mapping
F6 saveState uses tmp+rename atomic write
F9 fileSha256 removes 1MB cap; full-file hash (no more silent tail-edit
misses on long partial transcripts)
Signal handling: installSignalForwarder propagates SIGTERM/SIGINT to the
gbrain child process AND synchronously cleans the staging dir before
process.exit. Pre-fix, orchestrator timeouts left gbrain processes
orphaned holding the PGLite write lock (observed: 15-hour-CPU-time
orphan still alive a day later).
parseImportJson returns null on unparseable output (treated as ERR by
caller) instead of silently zeroing through.
gbrainAvailable() probes for the `import` subcommand instead of `put`.
Plan + review chain at /Users/garrytan/.claude/plans/purrfect-tumbling-quiche.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: orchestrator OK/ERR verdict parser for batch memory ingest
gstack-gbrain-sync.ts: memory-stage parser now picks [memory-ingest] ERR
lines preferentially over the latest [memory-ingest] line, strips the
prefix and any leading 'ERR: ' for cleaner summary output, and surfaces
'(killed by signal / timeout)' when the child exits with status=null.
Matches D6's OK/ERR contract: per-file failures (FILE_TOO_LARGE etc.)
show in the summary count but only system-level failures (gbrain crash,
process kill, missing CLI) mark the stage ERR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: batch-ingest writer regressions + refresh golden ship fixtures
test/gstack-memory-ingest.test.ts: 5 new tests for the batch-import
architecture:
1. D1 hierarchical staging slug round-trip — asserts staged file lives
in transcripts/claude-code/<dir>/*.md, not flat at staging root
2. Frontmatter injection — asserts title/type/tags written into the
staged page's YAML block
3. D7 sync-failures.jsonl exclusion — files listed as failed by
gbrain do NOT get state-recorded; one of two test sessions lands,
the other stays un-ingested for retry next run
4. Missing-`import`-subcommand error path — when gbrain only advertises
legacy `put`, memory-ingest exits 1 with [memory-ingest] ERR
5. --scan-secrets opt-in path — verifies a dirty-source file is
skipped via the secret-scan match when the flag is on, while a
clean session in the same run still gets staged
Replaces the prior put-per-file shim with an import-batch shim. The
shim fails loudly (exit 99) if the new code ever regresses to per-file
`gbrain put` calls.
test/fixtures/golden/{claude,codex,factory}-ship-SKILL.md: refresh
golden baselines to match the current generated SKILL.md content after
the v1.31.0.0 AskUserQuestion fallback-clause deletion. Goldens were
stale from that release; test was failing on origin/main before this
PR. Caught by the /ship test pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* v1.33.0.0 docs: design doc, P2 perf TODOs, gbrain guidance block, changelog
docs/designs/SYNC_GBRAIN_BATCH_INGEST.md: full design doc with the 8
decisions (D1-D8), source-verified gbrain behaviors (content_hash
idempotency, frontmatter parity, path-authoritative slug, per-file
failure surface), measured performance vs plan target, F9 hash
migration one-time cliff note, and follow-up TODOs.
CLAUDE.md: append `## GBrain Search Guidance` block from /sync-gbrain
indicating this worktree's pin and how the agent should prefer gbrain
search over Grep for semantic queries.
TODOS.md: P2 `gbrain import` perf-on-large-staging-dirs investigation
(5,131 files takes >10min in gbrain when 501 takes 10s — likely N+1
SQL or auto-link reconciliation). P3 cache-no-changes-since-last-import
at the prepare-batch level for true no-op fast paths.
VERSION + package.json: bump to 1.33.0.0 (queue-aware via
bin/gstack-next-version — skipped v1.32.0.0 which is claimed by
sibling worktree garrytan/wellington / PR #1431).
CHANGELOG.md: v1.33.0.0 entry per the release-summary format.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: setup-gbrain/memory.md reflects opt-in per-file gitleaks
Per-file gitleaks scanning during memory ingest is now opt-in via
--scan-secrets (or GSTACK_MEMORY_INGEST_SCAN_SECRETS=1). Update the
user-facing reference doc so it stops claiming "every page passes
through gitleaks." Also corrects the /gbrain-sync → /sync-gbrain
command typo and the post-incident recovery section.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
06605477e2 |
v1.29.0.0 feat: worktree-aware gbrain code sources via path-hash IDs and CWD pin (#1382)
* feat: worktree-aware gbrain code sources via path-hash IDs and CWD pin Conductor sibling worktrees of the same repo no longer collide on a shared gstack-code-<slug> source ID. /sync-gbrain now derives a path-hashed source ID per worktree, runs gbrain sources attach to write .gbrain-source in the worktree root, and removes the legacy unsuffixed source on first new-format sync to prevent orphan accumulation. Bug fixes surfaced by /codex during /ship: - Silent attach failure now treated as stage failure (no more ok:true while pin is missing → unqualified code-def hits wrong source). - Startup preamble checks .gbrain-source in the cwd worktree, not global state, so an unsynced worktree no longer claims "indexed" because a sibling synced. - Code stage no longer skipped on remote-MCP (Path 4); the early-exit was in the SKILL template, not the orchestrator. - Source registration routes through lib/gbrain-sources.ts only; deleted the near-duplicate ensureSourceRegisteredSync from the orchestrator. Requires gbrain v0.30.0+ (uses sources attach). Phase 0 spike report: ~/.gstack/projects/garrytan-gstack/2026-05-08-gbrain-split-engine-spike.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: bump version and changelog (v1.29.0.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c7aefc1abd |
v1.26.5.0 fix wave: gbrain ingest writer (hybrid frontmatter) + gbrain-valid source ids (#1344)
* fix: use correct `gbrain put <slug>` CLI verb in memory ingest
`put_page` is the MCP tool name, not a CLI subcommand. The actual
gbrain verb is `put <slug>` with content via stdin and tags in YAML
frontmatter. Every transcript / memory ingest fails today on clean
installs.
Switch to the right verb and inject title/type/tags into the
frontmatter that buildTranscriptPage / buildArtifactPage already
produce.
Bundled in the same function:
- timeout: 30s → 60s. Auto-link reconciliation hits 30s once the
brain has a few hundred pages.
- maxBuffer: 1MB → 16MB. Without it Node truncates gbrain's stderr
and callers see only `Command failed:` with no detail.
- Surface stderr/stdout in the returned error instead of the bare
exception.
Verified: bun test test/gstack-memory-ingest.test.ts -> 15/15 pass.
bun test on the three test files touching this path -> 362/362.
* fix(sync-gbrain): generate gbrain-valid source ids for repos with dots or long names
`deriveCodeSourceId` previously concatenated the canonicalized remote with only `/`
and whitespace stripped, leaving dots from hostnames (`github.com`) and no length
cap. gbrain rejects any source id containing characters outside [a-z0-9-] or longer
than 32 chars, so `github.com/<org>/<repo>` produced `gstack-code-github.com-<org>-<repo>`
(40 chars, plus dots) and registration failed:
code source registration failed: Invalid source id
"gstack-code-github.com-radubach-platform". Must be 1-32 lowercase alnum
chars with optional interior hyphens.
Fix:
- Drop the host segment (`github.com` is the same for nearly every user and just
consumes the 32-char budget). Use only the last two path segments (org-repo).
- Sanitize any remaining non-alnum to hyphens, then collapse and trim.
- For genuinely long org/repo names that still exceed the budget, keep the tail
(most distinctive end of the slug) and append a 6-char sha1 hash for collision
resistance.
Adds a regression test that spawns the CLI in temp git repos with controlled
remotes (dot in hostname, SCP-style, multi-dot host, long names forcing
hash-truncation) and asserts every derived id is ≤32 chars and matches the
gbrain validator regex.
* fix(memory-ingest): hybrid frontmatter writer + tightened gbrain availability probe
PR #1328 (merged in the prior commit) correctly injects title/type/tags
into the YAML frontmatter that buildTranscriptPage already prepends. But
buildArtifactPage emits raw markdown without frontmatter, so design-docs,
learnings, and builder-profile-entries were landing in gbrain with empty
title/type/tags. Add the no-frontmatter wrap branch so artifact pages get
the same metadata the inject branch provides for transcripts.
Also bring in gbrainAvailable()'s --help probe (originally proposed in
PR #1341 by Alex Medina), with the regex tightened from /(^|\s)put(\s|$)/m
to /^\s+put\s/m. Anchoring on the indented subcommand format gbrain's
help actually uses keeps the probe from matching "put" appearing as
prose in help text, while still failing fast with one clean error if a
future gbrain renames or removes the put subcommand.
Updates the V1.5 NOTE doc block at the top of the file to describe the
current put-via-stdin shape rather than the legacy put_page flag form.
Co-Authored-By: Alex Medina <oficina@puntoverdemc.com>
* test+fix(memory-ingest): strengthen regression tests, fix inject for malformed-close frontmatter
Imports the shim-based regression tests from PR #1341 (Alex Medina) and
strengthens them to assert title, type, and tags actually arrive in put
stdin — not just `agent: claude-code`. Asserting the metadata fields
matches the regression class that's caused this fix wave: writers can
"succeed" while metadata is silently lost. The original PR #1341 tests
would have passed even with title/type/tags missing.
Strengthening the test surfaced a deeper issue. buildTranscriptPage joins
frontmatter array elements with "\n" and does not append a trailing
newline, so the close fence is "\n---<content>" directly, not "\n---\n".
PR #1328's inject branch searched for "\n---\n" and never matched —
which means even with PR #1328 alone, transcript pages were landing in
gbrain with no title/type/tags. Two-line fix: search for "\n---" only,
since the inject lands before the close fence regardless of what
follows it.
Also imports PR #1341's V1.5 NOTE doc-block update and the section
comment refresh so the prose stays accurate against the new writer
shape.
Co-Authored-By: Alex Medina <oficina@puntoverdemc.com>
* fix+test(gbrain-sync): handle empty-slug edge in constrainSourceId, add no-origin and basename-empty regression tests
PR #1330 (merged in the prior commit) addressed the dot-in-host and
length-overflow cases for source-id derivation, but constrainSourceId
silently returned "${prefix}-" when the input sanitized to an empty
slug — invalid per gbrain's `^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$`
validator on the trailing hyphen. Adds an explicit empty-slug branch
that falls back to a sha1-prefixed id ("gstack-code-<6hex>") so the
output stays gbrain-valid for every input shape.
Two new regression tests cover the corners PR #1330's coverage left
exposed:
- no-origin fallback: a cwd repo with no `origin` remote configured
must still derive a valid id from the basename.
- basename-sanitizes-to-empty: a repo whose path basename is all
non-alnum (e.g. "___") must produce the hash-only fallback, not
an invalid trailing-hyphen id.
Both run the CLI inside temp git repos for genuine end-to-end
coverage (matches the pattern PR #1330 established for its own four
remote-shape cases).
Co-Authored-By: Richard Dubach <radubach@gmail.com>
* chore: bump VERSION to 1.26.5.0 + CHANGELOG entry for fix wave
PATCH bump. Three bug fixes (memory-ingest put_page CLI verb mismatch,
hybrid frontmatter writer for transcripts AND artifacts, gbrain-valid
source-id derivation for github-hosted repos), no new user capability.
CHANGELOG release-summary leads with what users can now do (clean-
install transcripts populate the brain, github-hosted repos register
code sources) and tabulates before/after numbers from real gbrain
v0.25.1 smoke output. Itemized changes credit @smithjoshua, @AZ-1224,
and @radubach for the originating PRs plus the additional hybrid
branch + strengthened tests added on top per Codex plan-review.
* docs(todos): file P2 (gbrain install-pin staleness) + P3 (source-id host-collision) follow-ups
Two follow-ups surfaced during the v1.26.5.0 fix-wave plan review.
P2 — Issue #1305 part 2: bin/gstack-gbrain-install pins gbrain to
v0.18.2 (commit 08b3698) but doesn't move when gstack ships features
that depend on newer gbrain ops or schema. Fresh /setup-gbrain on
v1.26.x lands users on schema 24 with v1.26 features expecting 32+.
Captured for a future fix-wave.
P3 — Codex P1.3 from the v1.26.5.0 plan review: deriveCodeSourceId
drops the host segment to fit gbrain's 32-char source-id budget,
which means github.com/acme/foo and gitlab.com/acme/foo collapse to
the same source id. Real but rare; PR #1330 author explicitly
considered this and chose budget over cross-host uniqueness. Captured
as a long-tail concern.
---------
Co-authored-by: Joshua Smith <joshualowellsmith@gmail.com>
Co-authored-by: Richard Dubach <radubach@gmail.com>
Co-authored-by: Alex Medina <oficina@puntoverdemc.com>
|
||
|
|
db9447c333 |
v1.26.3.0 feat: /sync-gbrain skill + native code-surface orchestrator (#1314)
* feat: native gbrain code-surface orchestrator + ensureSourceRegistered helper Replaces gbrain import (markdown only) with gbrain sources add + sync --strategy code (or reindex-code on --full). Adds lib/gbrain-sources.ts exporting ensureSourceRegistered/probeSource/sourcePageCount, plus lock file + tmp-rename atomicity + dry-run write skip in the orchestrator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: setup-gbrain Step 8 writes ## GBrain Search Guidance after smoke test Extends Step 8 to write a machine-agnostic guidance block that teaches the agent when to prefer gbrain CLI (search/query/code-def/code-refs/ code-callers/code-callees) over Grep. Gated on smoke test pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: /sync-gbrain skill — keep gbrain current and refresh agent guidance New top-level skill that wraps gstack-gbrain-sync with state probing, capability check (write+search round-trip, not gbrain doctor), CLAUDE.md guidance lifecycle (write iff healthy, remove iff broken), and a per-source verdict block. Re-runnable, idempotent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: preamble emits gbrain-availability block when capability ok Extends generate-brain-sync-block.ts to emit Variant A (steady-state, 4 lines) when cwd page_count > 0 or Variant B (empty-corpus emergency, 3 lines) when 0; empty string otherwise. Reads cached page_count from .gbrain-sync-state.json (handles pretty + compact JSON). Refreshes ship golden fixtures and bumps the plan-review preamble byte budget to 35K to absorb the new block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: register /sync-gbrain in AGENTS.md and docs/skills.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: regenerate SKILL.md across all hosts (gen:skill-docs) Mechanical regeneration after preamble + setup-gbrain template + new sync-gbrain skill. Run via: bun run gen:skill-docs --host all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.26.3.0) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add /sync-gbrain to README skills table and gbrain section Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a0bfa001d3 |
v1.26.1.0 fix: gbrain-sync orchestrator resolves sibling via import.meta.dir (#1312)
* fix: gbrain-sync orchestrator resolves brain-sync sibling via import.meta.dir Codex M9: runBrainSyncPush hardcoded ~/.claude/skills/gstack/bin/gstack-brain-sync, so any host that wasn't Claude Code (Codex CLI, dev workspace) hit the existsSync guard and silently skipped curated-artifact push. Replace with the sibling-resolution pattern already in runMemoryIngest at line 193. Regression test asserts the orchestrator no longer takes the lying-skip path when HOME has no ~/.claude/skills/gstack tree. * chore: bump plan-review preamble ratchet + regenerate ship goldens The 33 KB preamble byte budget hadn't been bumped through v1.25.1.0 (AskUserQuestion recommendation pattern) and v1.26.0.0 (gbrain sync block). plan-ceo-review SKILL.md sat at 33,018 bytes — 18 over the ratchet. Comment in the test already authorizes this kind of intentional-growth bump. Lifted to 34 KB which gives ~700 B of headroom for the next preamble change. claude-ship-SKILL.md and factory-ship-SKILL.md golden fixtures regenerated against the live /ship template — v1.25.1.0 added the canonical "Recommendation: <action> because ..." line to the adversarial subagent prompts but the goldens were never re-baked. * chore: bump version and changelog (v1.26.1.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
bf65487162 |
v1.26.0.0 feat: V1 transcript ingest + per-skill gbrain manifests + retrieval surface (#1298)
* feat: lib/gstack-memory-helpers shared module for V1 memory ingest pipeline Lane 0 foundation per plan §"Eng review additions". 5 public functions imported by the V1 helpers (Lanes A/B/C): canonicalizeRemote(url) — normalize git remote → host/org/repo secretScanFile(path) — gitleaks wrapper with discriminated return detectEngineTier() — cached 60s in ~/.gstack/.gbrain-engine-cache.json parseSkillManifest(path) — extract gbrain.context_queries: from frontmatter withErrorContext(op,fn,caller) — async-aware error logging 22 unit tests, all passing. State files use schema_version: 1 + last_writer field per Section 2A standardization. Manifest parser handles all three kinds (vector/list/filesystem) and ignores incomplete items. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: bin/gstack-memory-ingest — V1 unified memory ingest helper Lane A. Walks coding-agent transcripts (Claude Code + Codex; Cursor V1.0.1 follow-up) AND ~/.gstack/ curated artifacts (eureka, learnings, timeline, ceo-plans, design-docs, retros, builder-profile). Calls gbrain put_page with type-tagged frontmatter. Uses gstack-memory-helpers (Lane 0): - Modes: --probe / --incremental (default, mtime fast-path) / --bulk - Default 90-day window; --all-history opts into full archive - --sources subset filter; --include-unattributed opt-in for no-remote sessions - --limit N for smoke testing; --benchmark for throughput reporting - Tolerant JSONL parser handles truncated last lines (D10 partial-flag) - State file at ~/.gstack/.transcript-ingest-state.json (LOCAL per ED1) - schema_version: 1 with backup-on-mismatch + JSON-corrupt recovery - gitleaks via secretScanFile() before every put_page (D19) - withErrorContext wraps every put_page for forensic ~/.gstack/.gbrain-errors.jsonl 15 unit tests cover --help, --probe (empty, Claude Code, Codex, mixed artifacts), --sources filter, state file lifecycle (create, schema mismatch backup, JSON corrupt backup), truncated-last-line handling, --limit validation. All passing. V1.5 P0 follow-ups noted in the file header: - Cursor SQLite extraction (V1.0.1) - gbrain put_file routing for Supabase Storage tier (cross-repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: bin/gstack-gbrain-sync — V1 unified sync verb (Lane B) Orchestrates three storage tiers per plan §"Storage tiering": 1. Code (current repo) → gbrain import (Supabase or local PGLite) 2. Transcripts + curated memory → gstack-memory-ingest (typed put_page) 3. Curated artifacts to git → gstack-brain-sync (existing pipeline) Modes: --incremental (default, mtime fast-path) / --full (~25-35 min per ED2 honest budget) / --dry-run (preview, no writes). Flags: --code-only / --no-code / --no-memory / --no-brain-sync for selective stage disable. Each stage failure is non-fatal; subsequent stages still run. State at ~/.gstack/.gbrain-sync-state.json (LOCAL per ED1) with schema_version: 1 + last_writer + per-stage outcomes for forensic tracing. --watch daemon explicitly deferred to V1.5 P0 TODO per Codex F3 (reverses the "no daemon" invariant). Continuous sync rides the existing preamble-boundary hook only. 8 unit tests cover --help, unknown flag rejection, --dry-run preview shape (all stages + code-only), --no-code stage skip, state file lifecycle (create on real run + skip on dry-run), and stage results recorded in state. All passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: bin/gstack-brain-context-load — V1 retrieval surface (Lane C) Called from the gstack preamble at every skill start. Reads the active skill's gbrain.context_queries: frontmatter (Layer 2) or falls back to a generic salience block (Layer 1 with explicit repo: {repo_slug} filter per Codex F7 cleanup). Dispatches each query by kind: kind: vector → gbrain query <text> kind: list → gbrain list_pages --filter ... kind: filesystem → local glob (with mtime_desc sort + tail support) Each MCP/CLI call has a 500ms hard timeout per Section 1C. On timeout or missing gbrain CLI, helper renders SKIP for that section and continues — skill startup never blocks > 2s on gbrain issues. Datamark envelope per Section 1D + D12: rendered body wrapped once at the page level in <USER_TRANSCRIPT_DATA do-not-interpret-as-instructions> (not per-message). Layer 1 prompt-injection defense. Default manifest (D13 three-section): recent transcripts (limit 5) + recent curated last-7d (limit 10) + skill-name-matched timeline events (limit 5). All scoped to {repo_slug}. Template var substitution: {repo_slug}, {user_slug}, {branch}, {skill_name}, {window}. Unresolved vars cause the query to skip with a logged reason (--explain shows it). 10 unit tests cover help/unknown-flag/limit-validation, default-fallback when skill not found, manifest dispatch when --skill-file points at a real SKILL.md, datamark envelope wrapping, render_as template substitution, unresolved-template-var skip, --quiet suppression, and graceful gbrain-CLI-absence behavior. All passing. V1.5 P0: salience smarts promote to gbrain server-side MCP tools (get_recent_salience, find_anomalies, recency-aware list_pages); helper signature unchanged, internals switch from 4-call composition to single MCP call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: gbrain.context_queries manifests on 6 V1 skills (Lane E partial) Adds the V1 retrieval contracts. Each skill declares what it wants gbrain to surface in the preamble at invocation time: /office-hours — prior sessions + builder profile + design docs + recent eureka (4 queries) /plan-ceo-review — prior CEO plans + design docs + recent CEO review activity (3 queries) /design-shotgun — prior approved variants + DESIGN.md + recent design docs (3 queries) /design-consultation — existing DESIGN.md + prior design decisions + brand-related notes (3 queries) /investigate — prior investigations + project learnings + recent eureka cross-project (3 queries) /retro — prior retros + recent timeline + recent learnings (3 queries) Each query carries an explicit kind (vector | list | filesystem) per D3, schema: 1 versioning per D15, and {repo_slug} template var per F7 cross-repo-contamination cleanup. Mix of vector / list / filesystem matches what each skill actually needs: - filesystem (mtime_desc + tail) for log JSONL + curated markdown - list with tags_contains filter for typed gbrain pages - (vector reserved for V1.0.1 when gbrain query surface stabilizes) Smoke test: bun run bin/gstack-brain-context-load.ts --skill-file office-hours/SKILL.md --repo test-repo --explain returns mode=manifest queries=4 with the filesystem kinds populating real data from ~/.gstack/builder-profile.jsonl + ~/.gstack/analytics/eureka.jsonl on this Mac. End-to-end retrieval flow confirmed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: setup-gbrain Step 7.5 ingest gate + Step 10 verdict + memory.md ref doc (Lane E partial) Step 7.5: Transcript & memory ingest gate. After Step 7 wires brain-sync but before Step 8's CLAUDE.md persist, runs gstack-memory-ingest --probe, then either silent-bulks (small) or AskUserQuestion-gates with the exact counts + value promise + 5 options (this-repo-90d, all-history, multi-repo, incremental-from-now, never). Decision persists to gstack-config set transcript_ingest_mode <choice>. Step 10: GREEN/YELLOW/RED verdict block. Re-running /setup-gbrain on a configured Mac is now a first-class doctor path — every step's detection + repair logic feeds into a single verdict at the end. Rows: CLI / Engine / doctor / MCP / Repo policy / Code import / Memory sync / Transcripts / CLAUDE.md / Smoke. Tells the user "Run /setup-gbrain again any time gbrain feels off; it's safe and idempotent." setup-gbrain/memory.md: user-facing reference doc covering what gets ingested + what stays local + secret scanning via gitleaks + storage tiering + querying + deleting + how the agent auto-loads context per skill + common recovery cases. Linked from Step 8's CLAUDE.md persist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: V1 E2E pipeline + --no-write flag for ingest helper (Lane F) E2E pipeline test exercises the full Lane A → B → C value loop: 1. Set up fake $HOME with all 8 memory source types as fixtures 2. gstack-memory-ingest --probe verifies counts match disk 3. gstack-memory-ingest --incremental writes state with schema_version: 1 4. Idempotency: re-run reports 0 changes 5. --probe distinguishes new vs unchanged after first incremental 6. gstack-gbrain-sync --dry-run previews 3 stages 7. --no-code --no-brain-sync --quiet writes sync state with 1 stage entry 8. office-hours/SKILL.md V1 manifest dispatches 4 queries (mode=manifest) 9. Datamark envelope wraps every loaded section (Section 1D + D12) 10. Layer 1 fallback when no skill specified — default 3-section manifest 11. plan-ceo-review/SKILL.md manifest also dispatches (regression for V1 manifest authoring across all 6 V1 skills) Side effect: bin/gstack-memory-ingest.ts gains --no-write flag (also honored via GSTACK_MEMORY_INGEST_NO_WRITE=1 env var). Skips gbrain put_page calls while still updating the state file. Used by tests + dry-runs to avoid real ingest churn when verifying state-file lifecycle. The --bulk and --incremental modes still call gbrain by default — only explicit opt-in suppresses writes. V1 lane test totals (covering all 5 helpers + 6 skill manifests): test/gstack-memory-helpers.test.ts 22 tests test/gstack-memory-ingest.test.ts 15 tests test/gstack-gbrain-sync.test.ts 8 tests test/gstack-brain-context-load.test.ts 10 tests test/skill-e2e-memory-pipeline.test.ts 10 tests ────────────────────────────────────── ───────── TOTAL 65 passing Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v1.26.0.0) V1 of memory ingest + retrieval surface. Coding-agent transcripts (Claude Code + Codex) on disk become first-class queryable pages in gbrain. Six high-leverage skills auto-load per-skill context manifests at every invocation. Datamark envelopes wrap loaded pages as Layer 1 prompt- injection defense. Storage tiering: curated memory rides existing brain-sync git pipeline; code+transcripts route to Supabase Storage when configured else local PGLite — never double-store. Net branch size vs main: +4174/-849 across 39 files. 65 V1 tests, all green. Goldilocks scope per CEO D18; V1.5 P0 follow-ups documented in the plan's V1.5 TODOs section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |