mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-02 15:04:37 +01:00
1a0be11bf4e0abb20cfd46a0a7657eaecbb13bcd
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
+44 |
2db0b3adc8 |
v1.91.19.0 fix: severe fix wave (truthful gates, no lost memory, every host finds gstack) (#3025)
* docs: approved severe fix wave plan (Oct 2026) * D0: keep a project's .env and bunfig.toml out of compiled binaries and daemon children Compiled binaries are built with --no-compile-autoload-dotenv and --no-compile-autoload-bunfig; the browse server and terminal agent are spawned with --no-env-file and an empty config, so a repo's .env cannot set GSTACK_CHROMIUM_NO_SANDBOX and a bunfig preload cannot run code in them. Verified on Bun 1.4.0. * A9: --no-write is a dry run and leaves ingest state untouched --no-write stamped every prepared page ingested_at and saved state, so a later real run skipped those pages forever. It now prepares and reports pages without importing or changing the state file. Tests that used GSTACK_MEMORY_INGEST_NO_WRITE / --no-write to seed state move to an explicit seam (a fake gbrain that accepts every staged page), because the --no-write contract deliberately changes. * H #2885 #2827: vector geometry is not phone or card PII pii.phone.e164 accepts "." as a group separator, so a single float (viewBox="0 0 581.66796875 ...") and a run of path coordinates ("37.6188 101.694") read as phone numbers. pii.cc's \b stops at a decimal point, so the long fraction of an .excalidraw float is a Luhn candidate. Excalidraw's seed/versionNonce/updated integers read as bare phone numbers. A rendered /diagram triplet raised 18 (svg) and 97 (excalidraw) MEDIUM findings on main; it now raises none. - looksLikeDecimalCoordinates (ported from #2885, extended to a single decimal and refusing a leading "+", the E.164 marker) - insideDecimalNumber: a cc span glued to "<digit>." or ".<digit>" - isNumericMetadataValue: an unquoted JSON value under a seed, nonce or timestamp key; the same digits under "phone" still report Controls: E.164 in prose, dotted and spaced phones, "+"-prefixed runs, "phone"/"contact" JSON numbers, Luhn-valid cards in prose and at sentence end. Co-authored-by: Lewis <lewispeel@hotmail.com> * D1: pin the terminal-agent /ws Origin to the gstack extension (#1324) GSTACK_EXTENSION_ID moves to browse/src/extension-id.ts, shared by /extension-token and the terminal agent's /ws gate. /ws now returns 403 for every origin except the pinned extension. Forks override the ID with gstack-config set browse_extension_id <id>, honored by both routes; the BROWSE_EXTENSION_ID env var is no longer read. Test sites that used fake extension IDs now use the pinned origin. * H #2899: env.kv skips a function call assigned to a credential name `session = _FlakySession(...)`, `token = make_token(user, ...)` and `password = getpass.getpass()` raised env.kv MEDIUM, which blocks the fail-closed decision log. The exemption needs the line's evidence, not the span's shape: the value is unquoted, is an identifier or dotted path followed by "(", and the call closes on the line or opens a multi-line argument list. Quoted values, unclosed "(" inside an unquoted secret (API_SECRET=Abc123xyz(9Qm) and calls carrying a high-entropy literal argument still report. The literal check is shared with the #2912 env-read exemption. Co-authored-by: Yugz <yannduzelier@gmail.com> * H #2962: per-machine config filenames are not internal hostnames `CLAUDE.local.md` and `settings.local.json` reported internal.hostname MEDIUM (the pattern's \b stops at the dot before the extension), which fails closed in the decision log. A span immediately followed by a known config extension (.md .json .jsonc .yaml .yml .toml .ini .conf .txt) and a word boundary is a filename. Controls: `ssh build.local`, `printer.local.` at a sentence end, `printer.local/md`, `printer.local.mdx`, a host with a port, `api.corp`, `build-7.internal`. The `.env.local` vectors (#2740) pass on main already via isDotenvFilename and are pinned here as regressions. Co-authored-by: Florian <florian.cleyetmerle@gmail.com> * H #2784: four-part versions need a version declaration, not a shape `"version": "1.128.1.0"`, `<Version>`, `AssemblyVersion("1.0.0.0")` and Keep a Changelog headings raised pii.ip_public MEDIUM on every release, because a four-part version is byte-for-byte a dotted quad. Exempt only when the same line declares it a version immediately before the value (a version key or word, an XML tag, an attribute call, or a `## [x]` heading). The same digits after host:/server =/"upstream", bare on a line, or after prose that merely mentions a version still report. Unlike #2784, this does not exempt a value because the same string is declared a version elsewhere in the text: that let a real address sharing a diff with a matching version slip. The pre-push hook's VERSION-file rule (#2856) is unchanged. Co-authored-by: cdecook23 <decook23@gmail.com> * H #2913: postgres:postgres is exempt only on a loopback host The postgres image's default pair in compose files and CI DATABASE_URLs blocked pushes as db.url_with_password HIGH. It is now exempt only for postgres/postgresql URLs with exactly postgres:postgres on localhost or 127.0.0.1 (optional port). A bare single-label host (`db`, `postgres`), any other host, a host that merely starts with localhost (`localhost.evil...`, `localhost@prod-db`), any other user or password, and other schemes still block. #2913 exempted the pair on any host; this keeps the plan's narrower rule. Co-authored-by: mb1810 <michael.berger1810@gmail.com> * G1: ship without a version when no version source is configured Resolve the version source to four outcomes (valid, absent, ambiguous, broken) in lib/version-source.ts and read that one signal in gstack-version-bump (new NO_VERSION state; write refuses to invent a VERSION), gstack-next-version (no_version, no slot), /ship's bump, CHANGELOG and title steps, /land-and-deploy's drift check and /document-release. A configured version file that is missing, empty, unreadable or malformed stops with its path; 0.0.0.0 is never substituted. docs/skills.md documents .gstack/version-path. Migrates two tests that pinned the old fabricated-0.0.0.0 behavior. Co-authored-by: Koda <koda@edgeviewfinance.com.au> Co-authored-by: Merit Blake <merit@newsprint.com> * H #2949: eval credential screen checks every name segment The hermetic eval env admits GITHUB_* and EVALS_* metadata but rejects credential-shaped names, judged only by the last underscore segment. A trailing qualifier carried a credential through: GITHUB_APP_PRIVATE_KEY_BASE64, GITHUB_TOKEN_1, EVALS_API_KEY_FALLBACK reached eval children. The screen now rejects a name when any segment is a credential word; segments, not substrings, so GITHUB_PATH / GITHUB_TOKENIZER / GITHUB_KEYRING stay metadata, and explicit runner admissions still win. Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com> * H #2915: cover chunked scanning of large multi-file push diffs Test-only coverage for behavior already on main: a clean two-file pushed diff over the engine's 1 MiB cap passes without engine.input_too_large, and a credential at the end of the second file (a later scan chunk) still blocks the push. Co-authored-by: pneumorea-ai <pneumorea@gmail.com> * A3: keep a project's DATABASE_URL away from a PGLite brain With a PGLite config, buildGbrainEnv returned the caller env unchanged, so a project's .env DATABASE_URL (autoloaded by Bun) reached gbrain: a false broken-db at best, gbrain talking to the app's own Postgres at worst. Inherited DATABASE_URL and GBRAIN_DATABASE_URL are now blanked for PGLite configs (GSTACK_RESPECT_ENV_DATABASE_URL=1 still keeps them), and every gbrain child runs from gbrain's config dir instead of the user's project, so it cannot reload the project's .env / .env.local. The old test that pinned pass-through for an engine:pglite config is migrated: pass-through now applies only to configs that name neither a database_url nor PGLite. Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com> * C9: guard destructive gstack-upgrade and spec fences against unset or wrong paths Fresh-shell hosts (and Claude Code, which keeps cwd but not shell variables) ran cd "$INSTALL_DIR" with an empty variable, so git checkout/stash/reset targeted the user's project. Step 2 now prints an absolute path; every destructive fence starts with cd -- "${VAR:?...}" || exit 1 and checks it is gstack's own checkout before any git command. /spec's worktree and spawn fences get the same guard. * F4: keep every paid design variant across regenerate rounds $D variants now reserves a collision-safe round-<N>/ directory per run (atomic mkdir) and writes a manifest.json of what each round saved. The paid image request and the local write retry separately: a write failure retries the write (temp + rename) and, if the path stays unwritable, keeps the bytes in the temp dir instead of buying the image again. Responsive variants land in their round directory too. Readers updated: design-shotgun reserves a round dir and a private temp dir for its parallel agents and records approved_path/round in approved.json; design-html resolves approved_path (legacy letters still work) and finds round-dir variants; design-consultation checks the printed paths; the gallery shows round-dir variants. Variants that were already overwritten before this change cannot be recovered. Migrates the consultation contract test that pinned the flat path. Co-authored-by: Matt Van Horn <mvanhorn@gmail.com> * A2: stage dot-leading slugs gbrain can collect; network errors are db-unreachable A page slug segment starting with a dot (a .claude project) staged a file gbrain's import walker prunes, so the batch was refused on every run. Leading dots become "dot-" per segment, and slugs recorded in state under the old name map through the same function so no page is duplicated. A DNS or connection failure (ENOTFOUND, EAI_AGAIN, ECONNREFUSED, ...) no longer classifies as broken-db, which routed users to move a working config aside. The new db-unreachable status carries the reason "database host unreachable (<code> <host>); your gbrain config is unchanged. Fix: check network or VPN, then re-run /sync-gbrain.", skips local sync stages with that reason, and counts as usable for gstack-gbrain-detect --is-ok like timeout. Co-authored-by: Infiniteyieldai <infiniteyield.ai@gmail.com> * D2: one address classifier for navigation literals, DNS answers, redirects and page-driven navigations (#2811) classifyAddress() in url-validation.ts blocks 169.254.0.0/16 (metadata and container credential endpoints), 100.100.100.200, fe80::/10 and fc00::/7, including IPv4-mapped, IPv4-compatible and NAT64 spellings and decimal, octal and hex IPv4. validateNavigationUrl, the A/AAAA answer check and session-persist's cookie-domain guard all use it. BrowserManager now checks every navigation request (redirect hops, link clicks, scripted navigations): a blocked target resets the tab to about:blank and the write command fails with the reason. Connection-time DNS rebinding remains out of scope. Co-authored-by: Gabriel Rondon <grondon@gmail.com> * C4: external hosts point at carved sections relative to the installed skill Publishes the pointer form setup matches: on every non-Claude host a carved section is `sections/<file>` relative to the installed gstack-<skill> SKILL.md directory (QA's existing form), never $GSTACK_ROOT/<skill>/sections/, which does not exist in external runtime roots. No render changes yet: only QA is carved on external hosts today. * INV-1: gate outcome contract for outside reviews classifyOutsideReview() returns separate execution, findings and verdict fields from text, stderr and exit status; reason codes, anchors and fix actions live in lib/gate-outcomes.ts. validateOutsideReview() stays as the text-only wrapper and the two-argument CLI keeps exit 0/1/2 for importers and installed skills that predate a re-render. The --verdict form (implied by --stderr/--exit) prints VERDICT/FINDINGS/REASON and exits 0/3/4/1; the generated outside-voice caller moves to it and branches on each status (unverified prints OUTSIDE_STATUS: unverified, never completed). Adds docs/troubleshooting.md with one anchored section per reason code, the anchor test, and the gate-tail lint (query scripts may not swallow failures, gate calls may not end in || true, other best-effort tails say why). * G1: quote the base placeholder in document-release title sync The title-sync block is checked with bash -n; an unquoted <base> parses as a redirection. Migrates the test that pinned the old root VERSION read to the shared classifier call. * G3: give every logging-bin mktemp a ${TMPDIR:-/tmp} template Bare mktemp, mktemp -t and literal /tmp templates in the learnings, question and preference logs (plus jsonl-merge, distill, the two dashboards, /ship's PR body and /document-release's run dir) now use "${TMPDIR:-/tmp}/<name>.XXXXXX", the form the INV-3 lint accepts. When the temp file cannot be created, the learnings, question and preference logs now say the entry was not recorded and how to fix TMPDIR instead of exiting with only mktemp's own error. Three tests that pinned the bare PR_BODY_FILE=$(mktemp) and one that pinned the /tmp run dir migrate. GNU and BSD mktemp already honor TMPDIR without a template, so the macOS-sandbox drop in #2952 is unverified here; the template makes the temp location explicit on every mktemp. Co-authored-by: Davon Johnson <d.johnson@dayofshow.net> * B1b: structured outside-review gate fails on P0 as well as P1 validateOutsideReview() derived its structured pass/fail from a [P1] match, so a review reporting only [P0] findings passed. The wrapper now reads the classifier's findings verdict (P0 or P1), and the /review and /ship structured-gate prose requests [P0]-[P3] tags and fails on P0/P1; untagged output (OUTSIDE_STATUS: unverified) is missing coverage. The ship clarity pin moves from 'structured P1' to 'structured P0/P1' with the behavior. * E1: freeze normalizes boundary and target the same way on Windows; hooks spawn through cmd.exe (#2876, #2354) gstack_hook_normalize_path (careful/bin/hook-extract.sh) maps C:\..., c:/..., UNC \\server\share (leading // kept) and /cygdrive/c to one form; check-freeze runs both the saved boundary and file_path through it before the absolute test, after symlink resolution, and compares case-insensitively on Windows bash. freeze, guard and careful register hooks as bash -c "exec bash \"$HOME/...\"" so cmd.exe, which never expands $HOME, no longer turns the hook into a silent exit 127. Co-authored-by: Kevin Grasso <kevin@biblingo.com> Co-authored-by: Xuan-Ce Wang <261591073+xwang4-svg@users.noreply.github.com> * G4: keep CLI-to-daemon loopback calls off HTTP_PROXY (#494) The CLI appends 127.0.0.1, localhost and ::1 to the user's NO_PROXY (and no_proxy), keeping existing entries, before its first fetch. Verified on Bun 1.4.0: fetch honors NO_PROXY when it is set before the first request and ignores later changes, so it is set at the top of main(). * F1: bind the plan audit to this branch's plan, never the newest file Plan File Discovery now prefers an explicit binding (the plan-mode file or /autoplan ACTIVE_PLAN from the conversation, then a "Plan: <path>" line in the PR body), then lists candidates: repo-committed docs/designs/ files this branch changed or that name it, and personal plan files that name the branch. Candidates are offered, never picked silently. The repo-name match and the newest-file-of-the-day fallback, which audited PRs against unrelated plans, are gone. With nothing bound or chosen it prints the exact line 'Plan completion audit: not run (...). Fix: add "Plan: <path>" to the PR body, or run /autoplan.' /ship's parent now binds the plan before dispatching the audit subagent ({{PLAN_COMPLETION_GATE_SHIP:discovery}}), so the child never searches. Migrates three tests that pinned the old markers and notice. Co-authored-by: loulanyue <260355617@qq.com> * G5: browse type --selector <sel> <text> (#2936) --selector focuses the element (sel or @ref) and types into it. The bare form still types its text literally into the focused element; text that starts with -- goes after a -- separator. When the first word looks like a CSS selector, the result adds a hint to use --selector without echoing the typed text, and never refuses based on page contents. Help and command usage updated. Co-authored-by: Thomas Reuser <thomasreuser@gmail.com> * F2: design-doc discovery reads docs/designs, never a root DESIGN.md A root DESIGN.md is the design system /design-consultation writes, so preferring it unconditionally reviewed plans against a token file while printing "Design doc found" and never looked at docs/designs/ feature docs. Discovery now considers only docs/designs/*.md as the repo-local design doc (still freshness-compared with the private ~/.gstack copy). The CEO finding fixture seeds its supplied design as a docs/designs doc and its test follows. Co-authored-by: Bit0ps <alex.dreamscaper@gmail.com> * C4: rename the external carve test (typecheck:test union-order artifact) tsc prints a union's members in type-creation order; a test file sorting before ceo-mode-option.test.ts reordered one baseline diagnostic's text. * C1: every env-var host fence resolves its own gstack root Codex and the other env-var hosts run each block in a fresh shell, so GSTACK_ROOT/BIN, B and D set by the preamble were empty in later blocks (/bin/gstack-slug, B=/browse). scripts/resolvers/runtime-root.ts now owns root resolution: a valid exported GSTACK_ROOT (bin/ and lib/), else the per-install literal root (no git call), else repo-local, else the global root (CODEX_HOME on Codex); no root prints the setup fix line and exits. One post-render pass inserts it (<=400 bytes) into every fence that uses these variables without assigning them; B/D come from the same helper as BROWSE and DESIGN SETUP. Claude output is byte-identical. Migrated pins of the old preamble lines (gen-skill-docs.test.ts) and a fixture whose exported GSTACK_ROOT lacked lib/ (skill-positional-literals). * E8: headless browse honors GSTACK_CHROMIUM_PATH; extensions load in new headless mode (#2771, #1968, #2281) The headless launch passes GSTACK_CHROMIUM_PATH as executablePath, scopes the XProtect self-heal out for that bundle, and names the path in a launch failure with the fix. BROWSE_EXTENSIONS_DIR now launches a persistent context under the project state dir with --headless=new, so the extension runs and no window appears (the off-screen window was visible on macOS and the extension never ran in an isolated newContext). The GPU source guard test moves from the removed useHeadless flag to launch() vs launchHeaded(). Co-authored-by: oli548 <312288720+oli548@users.noreply.github.com> Co-authored-by: Slava <slava.istomin@tuta.io> Co-authored-by: ryanlaiwy <129729476+ryanlaiwy@users.noreply.github.com> Co-authored-by: 李翊楠 <yinanli1917@gmail.com> * C2: link design/dist and make-pdf/dist into every host runtime root and the agents sidecar Codex design and make-pdf skills fall back to $GSTACK_DESIGN and $GSTACK_MAKE_PDF under the runtime root, but setup linked only browse/dist, so both preflights reported NOT_AVAILABLE on a fresh install. One helper links dist/ only (make-pdf/ ships a Claude SKILL.md) into the Codex, Factory, OpenCode, Cursor, Copilot and Kiro roots, the .agents sidecar and the Cursor sidecar. Co-authored-by: Mammad M. <mammad@digit.az> * C3: install router and nested SKILL.md files as real copies; back up hand edits; --status checks Codex skips a symlinked SKILL.md file, so the gstack router linked into ~/.codex/skills/gstack was invisible. Every host runtime root (Codex, Factory, OpenCode, Cursor, Copilot, Kiro) and Kiro's per-skill installs now get real copies, rewritten on every run (the auto-update path runs setup --refresh-registered, so it refreshes them too). setup records each copy's hash in <state root>/skill-copies.tsv; a copy that no longer matches is saved to a uniquely named backup under <state root>/backups/skill-copies/ before _activate_runtime_root replaces the tree, and a failed backup keeps the old tree. setup prints what it refreshed or removed. ./setup --status now prints, per registered install, whether the router is a real file and whether every section pointer resolves, with the fix command. Test migration: setup-codex-model pinned the Kiro router/upgrade links as _link_or_copy; C3 deliberately changes them to copies. * INV-3: install-level host conformance checks (no symlinked SKILL.md, dist present, section pointers resolve) For codex, kiro, factory, opencode, cursor and copilot, the real-setup conformance kit now fails when an installed SKILL.md is a symlink, when design/dist or make-pdf/dist is missing from the runtime root, or when a 'Read `.../sections/<f>.md`' pointer in an installed SKILL.md does not resolve (bare pointers in carved skills, relative-to-installed-skill pointers, and $GSTACK_ROOT/~/absolute/../ pointers everywhere), and ./setup --status must report 'all resolve'. No new setup runs. Expected red until the generator lane lands: design-html's install-offer pointer and plan-eng-review's review-sections pointer name $GSTACK_ROOT/<skill>/sections/... on every external host. * B11: auto-update tracks pull, setup and migrations; announces only on success; retries with backoff gstack-session-update wrote the 'just upgraded' marker after SETUP_FAILED or SETUP_SKIPPED bun_missing, and re-ran setup only when HEAD moved, so a failed install was announced as an upgrade and never retried. Stages are now persisted in <state root>/session-update-pending; the marker is written only when every stage succeeded; a pending setup or migrations stage is retried even with HEAD unchanged after the update-check interval, then 6h, then 24h; and each session start prints one line naming the stage, the reason and the repair command while it is pending. Best-effort tails carry a same-line reason. * D1: gstack-config key browse_extension_id for forks and self-built extensions The browse daemon trusts one Chrome extension ID on /extension-token and the terminal-agent /ws. The override now lives only in gstack-config (never an env var a project .env could set): empty by default (gstack's published extension), a 32-letter a-p Chrome ID when set, "" clears it, and anything else is rejected with the stored value kept. The browse lane reads it. gstack-config-defaults: browse/src counts as a reader directory; the key is listed prose-only until the browse lane's reader lands (the stale-entry check then asks for the entry's removal). * G2: key the sticky slug cache on the git common dir A linked worktree used to resolve its slug fresh from the remote while the main checkout kept its sticky pre-remote identity, so learnings, checkpoints and timelines split into two buckets. gstack-slug and its TS twin in lib/bin-context.ts now key the cache on the git common dir (read from .git files, no git spawn, so cache hits stay git-free) and read entries in one deterministic order: the repository entry, then the main checkout's pre-upgrade entry, then this directory's own. Both the repository and per-path entries are written. When a worktree's own old entry named a different bucket that still holds data, that slug is recorded in projects/<slug>/.former-slugs and reported by --get FORMER_SLUGS. New --adopt-legacy --from <slug> merges it: JSONL logs append missing lines with de-duplication, missing files copy, differing files are listed as conflicts and never overwritten, the source bucket is kept, and it prints migrated/pending/conflicting counts; reruns are idempotent. --adopt-legacy --dismiss <slug> drops the reminder. Co-authored-by: loulanyue <260355617@qq.com> * C5: OpenCode /gstack-* commands load the skill by its frontmatter name OpenCode's skill tool knows gstack-review as `review` (its frontmatter name), and OpenCode's builtin /review shadows that name as a command. The managed /gstack-review command told the agent to load `gstack-review`, a name the skill tool does not list. Every managed command now names the frontmatter name. OpenCode's AskUserQuestion/ExitPlanMode mapping (#2759) already shipped in v1.91.16.0. Test migration: the #2629 commands test pinned the directory name. Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com> * E4: Windows node-server bundle keeps sharp and socks external; CLI resolves its server script lazily (#2260, #2439) build-node-server.sh passes --external sharp and --external socks (Bun on Windows refuses to inline them; on Linux they were silently inlined). The CLI no longer resolves server.ts or server-node.mjs at module load: resolveServerLaunch() runs when a server starts, so a Windows minimal runtime with only server-node.mjs works and a missing script is reported only when one is needed. Co-authored-by: Spooks444 <193448029+Spooks444@users.noreply.github.com> Co-authored-by: punksterlabs <77217235+punksterlabs@users.noreply.github.com> * B1: Codex reviews whose sandbox could not start are unavailable, not a pass In containers without unprivileged user namespaces every Codex command fails, yet the review exits 0 and concludes 'no findings' (captured in test/fixtures/codex-sandbox/). The validator now takes execution evidence: 'codex exec --json' command events and the 'codex review' stderr transcript (a command that succeeded proves execution); sandbox failure lines on stderr or in failed command output return unavailable(sandbox_unavailable); exact 'could not run commands / read the diff' phrases are the last fallback. Review text that merely discusses bwrap or namespaces still passes. A free 'codex sandbox ... true' preflight (Linux, 10 s) runs before the paid model probe and every automatic outside review; CODEX_MODE gains sandbox_unavailable, and an inconclusive model probe reports unverified instead of ready (exit code kept for stale callers). Every Codex site (outside-voice resolver, /codex review, challenge, consult) takes its sandbox from _GSTACK_CODEX_SANDBOX and pipes its result through the validator; a static test parses rendered command positions for both. GSTACK_CODEX_NO_SANDBOX=1 (exactly 1, shell env only) switches to danger-full-access with a warning on every use. Sandbox pins in existing tests move from the literal read-only to the selected-sandbox variable; captured autoplan probes are judged against the probe block they executed. * C6: gstack-relink discovers skills structurally, so /browse survives a skill_prefix toggle relink skipped a hardcoded list of support-directory names that included browse, a real skill, so toggling skill_prefix removed /browse. A top-level directory is now a skill exactly when it carries a SKILL.md. Co-authored-by: Jonathan Zhang <topcoder1@Mac-Studio.local> * C4: carve ship and plan-ceo-review on every host; enforce the 160 KB byte gate The generated ship skill was 202-241 KB on all 10 non-Claude hosts (and Factory plan-ceo-review 160,039 bytes), over the 160,000-byte ceiling, because section carving ran only for Claude. Both skills now carve on every host, pointing at sections relative to the installed skill directory (literal $GSTACK_ROOT/<skill>/sections refs are rewritten the same way). The ceiling is measured in UTF-8 bytes and fails generation in repo mode; under --install-root it warns so setup never leaves a user without skills. qa-lazy-sections.test.ts pinned ship as inline-on-external-hosts; migrated to the carved policy and kept the inline check on review (Claude-only carve). * B5: learnings and timeline reads fail loudly when bun is missing gstack-learnings-search and gstack-timeline-read ended in '2>/dev/null || exit 0', so a missing bun or a crashed embedded script printed nothing and exited 0, identical to 'nothing recorded'. Both now exit 127 with '<script>: bun not found on PATH, so ... could not be read. Fix: install Bun (https://bun.sh), then re-run ./setup.' and let an embedded-script failure exit non-zero with its stderr. A successful run with no matches still exits 0 with empty output. The generated callers that print 'LEARNINGS: unavailable (...)' belong to the generator lane. Co-authored-by: loulanyue <260355617@qq.com> * B2: a retired-model 404 reports MODEL_UNUSABLE, not CODEX_MODE: ready The model probe matched only 400 / 'is not supported', so a retired model's 404 ('does not exist or you do not have access to it') and a custom provider's bare 404 fell through to the inconclusive branch and reported ready. Both now classify as MODEL_UNUSABLE with a hint naming the two causes (retired model, or a wrong base_url) and log codex_model_retired; a 'requires a newer version of Codex' 400 points at upgrading the CLI and logs codex_cli_outdated. Ported from #2843 onto the current probe. Co-authored-by: Marek Cermak <marek.cermak@strv.com> * B6: rank learnings by matched query tokens and disclose truncation A more specific query used to drop the exact match: the token-OR filter recalled every entry sharing one token, the sort used confidence alone, and the default --limit cut silently. Entries now rank by how many distinct query tokens they match, then confidence, then recency, and a query that matched more than fit says '-- N more matched, raise --limit to see them'. The hit count lives in a local map so a field stored on disk can never become a sort key, and the type grouping uses a null-prototype map so a row typed 'constructor' or '__proto__' cannot crash the run. Ranking code ported from #2796; regression vectors adapted from #2799 (its stemmer is not needed). Co-authored-by: loulanyue <260355617@qq.com> Co-authored-by: y$un_ <forrest.sun527@gmail.com> * C7: required team mode also blocks Copilot CLI's lowercase skill tool Copilot CLI applies Claude-format matchers by tool name; its skill tool is `skill`, which no Claude tool name maps to, so the 'Skill' matcher never fired and Copilot ran protected skills without gstack. The hook entry now matches 'Skill|skill', an entry an older team-init wrote is upgraded in place, and the deny is emitted in both decision formats (Copilot reads the top-level permissionDecision, Claude Code reads hookSpecificOutput) with exit 0 so both parse it. Test migration: the settings test pinned matcher 'Skill'. Co-authored-by: neallee <neallee@gmail.com> * B3: Codex auth probe accepts custom providers' env_key credentials The auth probe only knew CODEX_API_KEY, OPENAI_API_KEY and auth.json, so a working self-hosted or OpenAI-compatible provider configured with [model_providers.<id>] env_key = "X" reported AUTH_FAILED and /codex stopped. The probe now reads env_key from model_providers tables only (comments ignored, the name must be a shell identifier before it is dereferenced) and accepts a set variable; when none is set it names the missing key on stderr. Ported from #2192 with the table scoping added. Co-authored-by: hardy <932821449@qq.com> * E8: keep the extensions profile under the state dir at a path lock cleanup accepts cleanSingletonLocks only cleans directories named chromium-profile, so the BROWSE_EXTENSIONS_DIR profile moves to <state>/extensions/chromium-profile. * B10: plan-tune never reports CALIBRATED with zero signal events Stats printed CALIBRATED: true from logged-answer counts alone, while every answer could miss SIGNAL_MAP and leave each dimension at its 0.5 seed (#2623). --derive now records inferred.signal_events, the answers that actually moved a dimension; calibration requires at least one, and the summary prints 'not calibrated: no recorded signals' verbatim when there are none (or says the count is unknown for a profile derived before this change). The psychographic redesign stays out of scope. * A1: verify gbrain landing before stamping transcripts, plus --reconcile gbrain import exits 0 while skipping pages (returned per-file failures, malformed filenames, managed-import Pending receipts), and its failure ledger exists only for git import dirs, so a skipped page was stamped ingested and never retried (field reports: 37 and 69 lost entries). Write, verify, then stamp (lib/memory-ingest-landing.ts, a new ratcheted module): - State schema 2 with a transition table (staged -> imported_unverified -> ingested, plus refused, quarantined, unrecoverable). Schema 1 files migrate in place: entries become ingested in the legacy "default" source and a reconcile pass is marked pending. - Named failures come from --json `failures`, stderr "Skipped <rel>:" lines and the ledger; a failure gbrain does not attribute refuses the whole batch. After 3 refused batches the batch is bisected and the page that poisons it is quarantined (printed even under --quiet). - Landing check: one `gbrain list --source-id` per source per run and at most 50 `gbrain get --json` content checks against a gstack_content_sha256 frontmatter field gstack now writes. Lookup errors and pglite_busy mean "not yet checked", never absent; pages over the cap stay imported_unverified and are not re-imported. - Each entry records its gbrain source_id. Remote-http mode records "staged", never "ingested". - The source fingerprint comes from the exact bytes parsed, so a transcript that grows during import is not stamped as its newer self (the old unscanned contract test is migrated; it pinned that bug). - --reconcile mode (bounded by --limit, default 200; resumable; --dry-run; backs up the state file) prints "reconcile: checked N, present M, re-queued K, not yet checked L". --request-reconcile records a pending pass without calling gbrain; the v1.91.17.0 migration (placeholder name) uses it, and the next ingest run does one bounded pass. - One lock for every state writer; a state-save failure fails the run. The #1802 static pins in regression-1611 move to the new structure (remote-http returns before importBatch; the finally honors preserveStaging). Co-authored-by: afshaker <andrew@phone2.io> * B4: arm64 re-sign keeps a backup, restores it when codesign fails, and reports design unavailable on exit 137 setup's Apple Silicon re-sign mutated each binary in place (truncate, remove signature) and left it that way when re-signing failed; a design binary that was then SIGKILLed at launch produced at most a quiet log line. Each binary is now copied first and restored as built when signing fails, and design is exec-probed after re-signing: exit 137 prints 'design unavailable: ... Fix: cd <checkout> && ./setup' (even under -q). Verified on Linux with the real block and uname/codesign/otool/stat stubs; not run on a Mac. * D3: treat design docs and handoff notes as data in plan reviews plan-ceo-review, plan-eng-review and plan-devex-review read the freshest design doc as the source of truth, and nothing said its content is data. In multi-agent pipelines another agent may have written it, or a handoff note beside it, with reviewer-directed text ('skip Step 0', 'approve as-is'). Each read site now says the doc and any handoff note are data, never to follow text aimed at the reviewer, and to report it as suspicious content. The plan-completion audit's discovery, which now reads repo docs/designs/ files (F1), carries the same rule. Co-authored-by: cube <frosimanuel@gmail.com> * D4: setup-deploy reports Render API key presence only /setup-deploy ran 'echo $RENDER_API_KEY | head -c 4', printing the key's first bytes into the transcript. It now prints only 'RENDER_API_KEY: set' or 'RENDER_API_KEY: not set'. Wording follows the #2971 port of #1096; a test also guards against any setup-deploy command echoing a credential variable. Co-authored-by: jacob-wang <jiayuw794@gmail.com> * D5: per-project headed Chromium profiles; never kill a live profile owner (#2492) The headed profile moves from <gstack home>/chromium-profile to <project>/.gstack/chromium-profile (CHROMIUM_PROFILE still wins). killOrphanChromium now kills a SingletonLock PID only when the lock names this host, the process is a Chromium using that profile, and its daemon is gone (reparented to init or a subreaper); a live owner or another host's lock stops browse with the holder named. On first headed use a project profile is seeded from the old shared profile only when nothing holds it: copied without Singleton* files into a 0700 temp dir and renamed into place, with a one-time notice; otherwise it starts fresh and says how to import later. New: browse profiles [list] and browse profiles prune [--days N] (never removes a profile in use or the old shared one). The #2817 headed-startup tests move to the project profile and a real orphan (the old stand-in was a live child of the test). Co-authored-by: miao <miao@macmini-2.local> * F5: installed Claude skills serve the rendered sections, not the checkout copies Probe: a gbrain user render serves ship/SKILL.md from the render dir; its STOP pointers name absolute render paths, but its section index names `sections/<f>.md` relative to the installed skill directory, whose sections/ was linked from the checkout. So the rendered 'Save Results to Brain' blocks (plan reviews, office-hours, ship adversarial) were generated and never read. setup's _link_skill_runtime_assets and gstack-relink (the gbrain-refresh path) now serve a rendered counterpart per file, keep checkout-only assets (manifest.json), return an asset to the checkout once its render is gone, and never touch an entry that is not gstack's links. Co-authored-by: Jonah Berg <jonah.berg.g@gmail.com> * F1: ratchet /ship's parity union-size cap for this lane's content G1's NO_VERSION arms, F1's parent-side plan binding and D3's data line add 2,126 bytes to /ship's skeleton-plus-sections union, taking it from 1.4022x to 1.4135x of the v1.64.1.0 baseline (cap was 1.405). The cap moves to 1.414 with the measurement recorded, as earlier growth did; no other invariant changes. * E5: feed every Codex prompt on stdin instead of argv Prompts went to Codex as one argv string ('codex exec "$(cat file)"'), which fails over 32 KB on Windows (and over 128 KB on Linux, E2BIG) and mangles quotes. Every codex exec site (outside-voice resolver, /codex review custom instructions, challenge, consult new and resume) now runs 'codex exec -' with the prepared prompt file on stdin; challenge and consult write the prompt with a quoted heredoc so it is never shell-parsed. The bash-native timeout watchdog (stock macOS) started the command in the background, which gives it /dev/null as stdin; it now passes the caller's stdin through. The 'byte for byte' dispatch test drops its old argv trailing-LF exception, the consultation and autoplan fixtures follow the stdin form, and the static site test requires stdin at every rendered codex exec. * G6: prettyscreenshot --hide keeps a trailing image path as the output path (#1419) --hide collected every following non-flag argument as a selector, so the output path was hidden instead of written to. A trailing argument that looks like a path or ends in .png/.jpg/.jpeg/.webp is now the output path. Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com> * INV-3: render-level host conformance and generated-bash lint on every host One render per host checks every fence of every SKILL.md and section file: bash -n with angle-bracket placeholders normalized; the env-var host prelude and its B/D consumers run under env -i and set -u with stub binaries (no empty value, no /bin/, /browse, /design or /gstack- path); and a quote-aware lint (test/helpers/generated-bash-lint.ts) fails on a ~/ word inside double quotes, mktemp without a TMPDIR-respecting template, and cd/rm -r/mv/git -C on a variable the fence never assigned unless written ${VAR:?...} (cd must also handle failure). Negative controls plant each failure. Templated the bare mktemp calls in generator-owned resolvers and the upgrade fence; migrated the two tests that pinned the bare form. * G6: analytics averages quoted duration_s values (#1728) gstack-skill-end writes duration_s as a quoted string, and the per-skill average stripped everything from the leading quote, so every average read 0s. The awk now drops a leading quote before extracting digits. Co-authored-by: Ryan Alberts <ryan.a.alberts@gmail.com> * G6: gstack-ios-qa-daemon --help prints usage instead of starting the daemon (#1932) Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com> * G1: tighten /ship's NO_VERSION wording to fit the union-size cap Same behavior in fewer bytes: the NO_VERSION arm, the broken-source exit, the unprefixed-title rule, the Step 20 null and the CHANGELOG skip. The cap ratchet commit measured this wording. * G6: rejected taste preferences gain confidence (#1777) bumpPref computed every entry's confidence from approved_count, so a rejected entry, which never gains approvals, stayed at 0: show ranked rejections by insertion order and the taste-drift warning could not fire from real use. Confidence now uses the entry's own bucket count. Also salvaged by Garry's #2971. Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com> * E2: Windows /cso build disables only C5105, and a failed MSVC compile is not reported as a missing toolchain The Windows SDK's winbase.h raises C5105, which /W4 /WX promotes to error C2220, so every MSVC build of the native /cso helper failed, and setup mapped every probe failure to 'install Visual Studio 2022 Build Tools'. Both compile lines add /wd5105 (only that diagnostic), and setup keeps the probe's output: a probe that found MSVC but failed to compile reports windows-msvc-compile with the compiler's first error. Not run on Windows: the mapping test stubs powershell.exe; the ps1 assertion runs everywhere. Co-authored-by: AlinValentin7 <AlinValentin7@users.noreply.github.com> * E6: gbrain-refresh hands the detection file to Python on stdin Under Git Bash, a native Windows Python first on PATH cannot open an MSYS /c/... path, so gbrain-refresh always summarized the detection as 'local-status: unknown' and brain-aware blocks were suppressed. bash now opens the file and Python reads stdin; the grep fallback without Python is unchanged. Verified on Linux with a stub Python that refuses path opens; not run on Windows. Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com> * G6: context-save computes session duration on Linux (#2704) The duration step parsed 'ps -o lstart=' with BSD-only 'date -jf', so on Linux session_duration_s was always unknown. It now falls back to GNU 'date -d', and never parses an empty start (GNU reads that as midnight). Ported onto the current block instead of #2704's ~/.gstack/sessions mtime, which bypassed the state root. Co-authored-by: Evgenii Lopatin <e75533@gmail.com> * Q2: one-time notice naming the Codex provider and account for outside reviews Outside reviews are on by default and use whatever Codex login exists (#965). The first automatic Codex review on a machine now prints one non-blocking notice: the provider (config.toml model_provider, else openai), where the credential comes from (CODEX_API_KEY, OPENAI_API_KEY, the ChatGPT login or the key saved by codex login; never the value), that reviews stay on, and how to turn them off (gstack-config set codex_reviews disabled). A marker in the gstack state root keeps it to once per machine. Tests that run the generated command now use private state roots. Also shortens the no-sandbox provenance line. * Q1: validate browser output paths the way the kernel resolves them (#2971 salvage) validateOutputPath walks the destination one component at a time, resolving each existing component before the next, so link/../x, a dangling symlink and a dangling symlinked parent can no longer route a write outside the safe directories, while a new multi-level directory inside them is allowed. scrape validates its manifest and every downloaded file, not only --dir. Ported from PR #2971 onto current code. * G6: retro test census counts non-JS suites, skips generated output The census recognized only JS/TS-style names, so pytest and minitest (test_*.py|rb), XCTest (Tests/ dirs, FooTests.swift), Terraform (*.tftest.hcl) and Bats (*.bats) suites counted as zero, and a committed build directory counted as authored code (and its Test/ subdirs as test LOC). is_test now knows those conventions and never treats prose as a test, generated output is skipped in the numstat pass, TEST_FILES_CHANGED uses is_test, and TEST_FILES_TOTAL uses the same filename conventions minus generated paths. Merges #2812, #2809 and #2037's patterns (#2037's retro template edits target a block the metrics bin replaced). Co-authored-by: Jino <dgfrty00@gmail.com> Co-authored-by: snig-17 <santosh.m.tiwari@gmail.com> Co-authored-by: Sholto McNeilage <sholto@nodenarrative.com.au> * Q3: setup stops auto-installing the AskUserQuestion preference hook in Conductor and removes one it added The PreToolUse question-preference hook breaks Conductor's native AskUserQuestion round-trip even when it only defers (#2207), and setup auto-installed it on every Conductor workspace setup. Now a Conductor setup without an explicit opt-in never installs it and removes an existing one (the question-log and AUQ-error hooks stay), printing how to keep it (gstack-config set plan_tune_hooks yes). An explicit opt-in still installs it. gstack-settings-hook remove-source gains --event to scope the removal. Test migration: setup-plan-tune-hooks-noninteractive pinned the retired Conductor auto-opt-in. Co-authored-by: Phil Hie <philhie@outlook.com> * E5: watchdog stdin test feeds the wrapper through a redirect Callers run '_gstack_codex_timeout_wrapper ... codex exec - < file'. Bash keeps stdin for a backgrounded command inside a pipeline but not behind a redirect, so the pipe form passed on the old watchdog; the redirect form reproduces the lost prompt. * F4: type the variant-round fetch stub like the existing stubs The test-typecheck ratchet flagged the zero-argument stub cast to typeof fetch; it now takes the fetch arguments like variants-retry-after's stub. Test behavior unchanged. * A4: import transcripts into per-repository, non-federated gbrain sources `gbrain import` ran without --source-id, so transcripts from every project landed in the shared (often federated) default source. - Each transcript partition imports with --source-id into its originating repository's own source (gstack-transcripts-<repo>, built with constrainSourceId, now exported from lib/gbrain-sources.ts). The source is registered machine-local and --no-federated before its first import, and the registration is cached in state. Partitions import one after another (PGLite is a single writer); a failing source no longer blocks the others, except a timeout, whose checkpoint must survive. - If gbrain cannot register the source, that partition stays local and unstamped, and the run says why (even under --quiet). - --include-unattributed sends repo-less transcripts only to the machine-local gstack-transcripts-unattributed source, keeps them on the machine when the brain is remote (Postgres, Supabase or remote-HTTP), and never writes them to the publishable ~/.gstack/transcripts staging. - A page's recorded source is sticky: pages stamped before this release ("default") keep importing there, so no source gains a duplicate. - The staging dir name carries -src-<id> so a resumed import keeps its source. Test fakes that recorded only the last import now record every import of one run, and answer `gbrain sources`; remote-http scan tests use an attributed transcript, since unattributed ones no longer stage there. Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> * F3: load the taste profile, and report a failed slug lookup The taste-profile probe double-quoted ~/.claude/.../gstack-slug, so the tilde never expanded and /design-consultation and /design-shotgun always printed NO_TASTE_PROFILE. Use quoteSafePath(); stop hiding gstack-slug's stderr and print TASTE_PROFILE_UNAVAILABLE with the fix when the slug cannot be resolved. * B4: DESIGN_READY only when the design binary actually starts DESIGN SETUP (and office-hours' visual exploration) printed DESIGN_READY for any executable binary, so one SIGKILLed at launch by an invalid macOS code signature still passed. Launch it under the portable 10-second deadline (gtimeout, timeout, or perl alarm); otherwise print DESIGN_NOT_AVAILABLE with the exit code, 'timed out after 10s', or on 137 the code-signature hint and 'Fix: cd <checkout> && ./setup'. A success is cached by inode and mtime under the state root so Gatekeeper's first-launch scan is paid once. The probe launches the binary with no arguments (usage, exit 0): the design CLI has no --version command today. * C2: name the runtime root every way a generated preflight may read it in the dist test The generator lane's C1 resolves D from $GSTACK_ROOT instead of $GSTACK_DESIGN, so the test now exports GSTACK_ROOT as well; it still fails on main (no dist in the roots) and passes with and without the C1 change. * A5: brain-sync stage fails loud when the artifacts source indexed zero pages The stage reported "curated artifacts pushed" whenever gstack-brain-sync exited 0, but a git push is not gbrain indexing. After a successful push the stage now reads the maintained artifacts source's page count (GSTACK_BRAIN_SOURCE_ID, else the source whose path is the artifacts worktree or state root): zero pages is ERR with the fix command; a missing source or an unavailable count says so instead of claiming indexing. Co-authored-by: Jimmy Zeisweiss <4261258+jzeisweiss@users.noreply.github.com> * B1: state the effective read-only sandbox in /codex review prose The codex parity invariant requires the skill to state sandbox_mode="read-only"; the command now takes it from _GSTACK_CODEX_SANDBOX, so the prose names the effective default explicitly. * A6: report deleted-worktree gbrain sources; prune only with proof, on request A deleted Conductor worktree leaves its gstack-code-<hash> source behind and gbrain's autopilot fails on it forever. Local absence is not proof a source is dead (machines sharing a federated brain register their own paths, and its pages may be the only copy of uncommitted work), so gstack never removes one automatically. - Each sync counts consecutive misses for gstack code sources whose path is missing, prints "path unavailable" once per source with the review command, and records which repository each synced source belongs to. - New gstack-gbrain-sync --prune-gone-worktrees (with --dry-run, whose plan it always prints first) removes a source only when its path was missing on two consecutive syncs, its parent is readable, recomputing deriveCodeSourceId() for that path on this host yields its id, and the repository's git common dir still exists and no longer lists the worktree. It re-checks the path right before removing (through the #1734-guarded safeSourcesRemove) and logs each removal. * D0: pass the Bun child flags after 'run' so wrappers that rewrite 'bun run' argv keep working bun run --no-env-file --config=/dev/null <script> has the same effect (verified on Bun 1.4.0) and keeps argv[1] === 'run', which the publication-lock fixture's preload injection relies on. The sidebar-tabs source guard now expects the flags in the terminal-agent spawn. * E8: count the BROWSE_EXTENSIONS_DIR persistent launch in the signal-flags guard The new launch site carries handleSIGINT/SIGTERM/SIGHUP false like the other three, so the guard's expected count moves from 3 to 4. * D2: give the memory-leak reproducer's fake request isNavigationRequest() wirePageEvents now runs the navigation guard on every request event; the fake request adapter supplies the method real Playwright requests have. * A7: run only the resolve_symbol_edges dream phase, detected first /sync-gbrain --dream and the --full auto-build ran gbrain's full dream cycle (~35 minutes, LLM phases) when the call graph needs only the resolve_symbol_edges phase. The stage now runs gbrain dream [--source <id>] --phase resolve_symbol_edges when the installed gbrain accepts that phase (detected by behavior: gbrain validates --phase before printing --help). Otherwise it skips the forced dream and says the full cycle costs about 35 minutes and how to run it, instead of starting it. Co-authored-by: liutiming <timingliu@outlook.com> * D0: keep the dotenv isolation test runnable on the Windows lane The compiled probe gets its .exe name on Windows, and the terminal-agent case (owner identity comes from ps) is skipped there. * B9: read cycle_freshness from a doctor run that includes it /sync-gbrain Step 3.5 and the --full auto-dream decision read CYCLE from gbrain doctor --json --fast, which skips the DB checks that carry cycle_freshness, so the answer was always "unknown". Both now read gbrain doctor --json --scope=brain (DB checks, no skill-file walk), parse its JSON even when doctor exits 1 for a failing check, and match the issue that names this source: "never completed" is never, "last cycled Nh ago" is completed. When the installed gbrain does not report the check, they say "unknown: installed gbrain does not expose cycle_freshness" (lib readCycleStatus carries the reason to the --full SKIP row). The source-probe fixture that pinned the --fast call and a made-up message format now answers --scope=brain with gbrain's real message. * B4: compact the launch probe (cache keyed on inode, valid while newer than the binary) Keeps design-consultation inside its parity size budget. * C4: migrate the ship docs-dispatch test to the carved external ship On Codex and Factory the documentation dispatch now lives in sections/documentation.md behind a pointer relative to the installed skill, like Claude; the test pinned the old inlined layout. * B5: learnings callers report a failed search instead of reading as empty Every generated gstack-learnings-search caller ended in 2>/dev/null || true, so a missing bun or a failed search looked like 'nothing recorded'. Callers now keep stdout, capture stderr and the exit status, and print 'LEARNINGS: unavailable (<first stderr line>)' on failure (resolver callers in learnings.ts and review-army.ts, plus investigate, qa and ship's adversarial section). * F1: ship and review count unverified/unavailable outside reviews as missing coverage INV-1 consumers in this lane's templates: /ship's readiness note, its PR body Pre-Landing Review section and /review's completion rule now say an outside review reporting unverified or unavailable is missing coverage, never a pass. The PR body's Plan Completion section prints F1's not-run line instead of the old 'No plan file detected.' The /ship union-size cap moves to 1.415 for these lines (measured 1.4148). * A8: rename the artifacts repo with OWNER/REPO, rewrite the remote only after it The v1.27.0.0 migration passed a bare repo name to gh repo rename / gh repo edit, which always fails (gh wants [HOST/]OWNER/REPO), then step 2 rewrote ~/.gstack-artifacts-remote.txt to the never-renamed repository. - Qualify both names with the owner from the recorded remote URL (else the signed-in gh user) and surface gh's error. - Step 2 moves the remote file but rewrites the URL only once the rename is confirmed; a later successful retry rewrites it then. - A pending journal now counts as state to migrate, so the #1383 retry actually re-runs step 1 (it used to see no legacy file and declare the migration done). - The v1.91.17.0 migration (placeholder name) repairs installs already hit: when the remote names gstack-artifacts-<user> that does not exist but gstack-brain-<user> does, it restores the old URL in the remote file and ~/.gstack's origin and says so. It only calls gh for such a remote and never fails the upgrade. Co-authored-by: Brandon Pugsley <aibpaem@gmail.com> * B8: read gbrain JSON with bun instead of jq in install and wireup gstack-gbrain-install read package.json with jq but never checked for it, so on a machine without jq a successful install failed with "cannot read version". gstack-gbrain-source-wireup hard-failed without jq ("brew install jq" is not actionable on git-bash), so the wireup was skipped on every run. Both now read the JSON with bun, which they already require. Co-authored-by: benjamin beres <benjamin.beres@bienpreter.com> * B4: shorter probe messages (design-consultation parity budget) * B5: shorter capture variable names (parity budget) * E7: find the Aside CLI in ~/.local/bin when it is not on PATH (#2902) The installer puts aside in ~/.local/bin, which is not on the default macOS login PATH, so the probe said NEEDS_ASIDE ('download Aside') for a healthy install. The probe now falls back to ~/.local/bin/aside, drives the resolved binary, and prints READY: <path> when it is off PATH so later blocks run that path. Ported from PR #2965 onto the current _gs_d probe. Co-authored-by: Som Samantray <som.samantray@gmail.com> * A2: key the dot-slug catalog entry on the message users actually saw * E1: cmd.exe-safe investigate freeze hook On Windows Claude Code runs hook commands through cmd.exe, which cannot parse the single-quoted bash -c argument, so /investigate's scope-lock hook never ran. Use one double-quoted bash -c script that names the hook path directly (no S= variable: POSIX sh would expand $S to empty and skip the hook). * F4: design resolver reads per-round variant paths $D variants now writes $_DESIGN_DIR/round-<N>/variant-*.png and prints JSON with roundDir, manifest and paths. The mockup flow and comparison-board loops build the board from those printed paths instead of hardcoded variant-{A,B,C}.png, and approved.json records approved_path and round so a round-1 choice made after round 2 still resolves. * E4: generators tolerate Bun's spurious EEXIST on Windows (#2329) Bun on Windows throws EEXIST from a recursive mkdir of an existing directory, so gen:skill-docs and the llms.txt writer crashed once a second file landed in a directory. Both now use lib/fs-utils' mkdirpSync (ported from PR #2329 onto the existing helper instead of a new scripts/fs-utils.ts). Co-authored-by: Vaughan <vaughan@cairnsitguy.com.au> * C7: keep the required-mode hook's exit-2 deny Copilot CLI treats a preToolUse exit 2 as deny (its hooks reference), and #2413 established exit 2 plus hookSpecificOutput as the shape Claude Code enforces, so the deny output stays as on main; only the matcher and the in-place upgrade of older 'Skill' entries change. * C3: guard Kiro's router copies on a present source; give the QA pointer adapter the copy and dist helpers A copy fails where a symlink dangled, so the Kiro router and upgrade copies are written only when their render exists (as every other runtime root already does). test/qa-lazy-sections' extracted Kiro installer now supplies _link_runtime_dists (C2) and the SKILL.md copy helpers (C3). * Q3: office-hours asks open-ended questions in a Q<N> prose form (#2719) In Conductor, and whenever AskUserQuestion fails, prose is the only channel, and the prose fallback defined only the D<N> decision brief. Phase 2A/2B diagnostic questions have no option set, so the model dropped the phase. Office-hours' preamble now defines the Q<N> open-question form (question, why I'm asking, what a strong answer sounds like) and routes free-text replies to it; the diagnostic sections name it. Scoped to office-hours so every other skill's preamble is unchanged. Ported from PR #2729. Co-authored-by: Bharadwaj Mittapelli <bharadwajmittapelli@gmail.com> * A2: render db-unreachable like timeout gbrain's new db-unreachable status is a transient network failure; a setup run while offline must not strip brain-aware blocks from every skill. * A2: treat db-unreachable like timeout in gbrain-refresh The memory lane's new db-unreachable status (network or DNS failure, config intact) is transient, so gbrain-refresh keeps rendering brain-aware blocks for it exactly as it does for timeout instead of suppressing them. * C8: a render given disabled skills drops them from the router (#3018) The root router named every skill and ends 'When in doubt, invoke the skill', so it routed to skills the user disabled. gen-skill-docs takes --disabled-skills (runGeneration disabledSkills): the router drops their routing rules, keeps the other side of an 'A or B' rule, and names them as disabled. The router and gstack-upgrade cannot be disabled. With no disabled skills the render is byte-identical. * G2/G3/INV-1: skill-start former-bucket notice and visible learnings failure; TMPDIR mktemp in gbrain bins; best-effort reasons * C9: Step 2 falls back to the install registry's source checkout (DX-7) Codex users who installed from the README's ~/gstack clone got 'ERROR: gstack not found' from /gstack-upgrade: Step 2 only looked under host skill directories. It now falls back to the source checkout the install registry recorded for the current host. * B4: design --version prints the version, and the probe uses it The design CLI treated --version as an unknown command (exit 1), so the probe launched the binary with no arguments instead. Add --version (prints the gstack version, exit 0, before auth or network) and probe with it, so messages read '<path> --version exited <code>' as planned. * C4: no external render points at a section file that is not installed design-html's detector install offer and plan-eng-review's self-check pointed at $GSTACK_ROOT/<skill>/sections/<file> on every external host, where neither skill is carved and no section tree exists. design-html now inlines the offer off Claude, and a skill's pointer to its own {{SECTION}}-inlined section names the inlined text. * C1: skillify finds the browse SDK under its own host's install root The template hardcoded ~/.claude/skills/gstack for the canonical browse-client.ts, so external hosts read another host's install or nothing. A new INSTALLED_ROOT resolver (sibling of INSTALLED_BIN_DIR) names the host's global root, or the per-install root when setup renders for one. * C4: rewrite section refs on hosts whose root stays a literal ~/<globalRoot> Kiro, OpenCode, Cursor and Copilot render ~/.claude/skills/gstack as their own literal root, not $GSTACK_ROOT, so plan-eng-review's self-check pointer escaped the rewrite (found by the setup lane's installed-pointer check). * Integrator: outside-voice fences use the shared runtime prelude; TMPDIR mktemp and per-round variant paths in design templates * Integrator: C8 router omits disabled skills on every render (relink renders the served Claude router in place); Windows-safe E4/E6 entries; investigate hooks under the cmd.exe check; browse_extension_id reader; order-insensitive test typecheck identities tsc printed one unchanged ceo-mode-option diagnostic's object members in a different order (content?/isError?) once lib/fs-utils joined the generator's import graph; identities now sort object-type members, and old baselines normalize on read. * Q1 (#2971 salvage): bound iOS proxy responses One wall-clock deadline covers headers and body (stalled and dribbling bodies time out), the whole body is capped at 64 MiB (upstream_response_too_large), every terminal path clears its timer, removes listeners and destroys the request, and the daemon threads the deadline through both listeners. Ambiguous mutations are still never replayed after a timeout or abort. Ported from #2971's proxy-transport work plus the size bound. * I1: grade the AUQ matrix's (recommended) marker on option labels, the field the auto-decide hook reads The decision-brief format puts a Pros / cons block with 'A) ... (recommended)' in the question text and the same marker on the option label. The matrix counted both and failed all six skills in periodic run 37151477069 with 'expected exactly one (recommended) option, found 2'. The question-preference hook reads labels only, so the gate now checks the native question: a Recommendation line in the question text and exactly one label ending in (recommended). Fixtures move to the native question shape and add the real spec capture plus a marker-only-in-text negative control. * Q1: type the proxy-transport test doubles (typecheck:test ratchet) * INV-1: gate outcome rows and troubleshooting sections for every lane's new messages * Release v1.91.19.0 (next free patch in the queue: #3022 holds 1.91.17.0, #3023 holds 1.91.18.0); migration renamed; engines.bun >=1.4.0 to match the CI pin (#2815) * v1.91.19.0 docs and metadata: CHANGELOG, TODOS, README troubleshooting/upgrade/type --selector, BROWSER.md type and profiles, AGENTS.md conventions, CONTRIBUTING #2949, touchfiles for the wave's new lib/bin files * I1: give hermetic Codex runs a temp dir beside CODEX_HOME so Codex can create its PATH aliases Codex 0.160 refuses to create its helper aliases (codex-linux-sandbox among them) when CODEX_HOME sits under its temp dir, and the runner put the hermetic home under the shard TMPDIR. Periodic run 37151477069 shows the warning and 'Unable to spawn codex-linux-sandbox ... No viable candidates found in PATH' for every exec_command in codex-sol-scope-termination. * I1: Codex evals fail a review whose commands never ran and stop reading tool errors as skill-load problems validateCodexReview accepted codex-review-findings in periodic run 37151477069 although its only command failed in bwrap ('I couldn't run the review'); it now requires execution evidence from classifyOutsideReview (B1's INV-1 classifier). codex-sol-scope-termination failed on 'apply_patch verification failed: invalid hunk' because the skill-load check matched any 'invalid'; it now matches Codex 0.160's skill-loader diagnostics only. * I1: run paid eval containers with seccomp unconfined so Codex's sandbox can start Docker's default seccomp profile blocks the user namespaces bubblewrap needs, so every Codex command in a paid eval failed with 'bwrap: No permissions to create a new namespace' (codex-review-findings, run 37151477069). Reproduced in the CI image locally: default profile fails, seccomp=unconfined runs the command. The runner stays non-root. * I1: shared-libs plan actor reads one exclusion grammar for scope lists Periodic run 37151477069 refused 'Existing-caller migration and helper hardening stay out of scope' as a hardening proposal: the actor had two partial exclusion rules (unanchored is/are/remains outside, and an anchored list that only stays unchanged), the third refusal at this boundary after 36633323521 and 36776104571. One anchored predicate now covers a bare list of scope nouns (hyphenated compounds included) with is/are/stays/remains and unchanged/untouched/excluded/out of scope/outside/not part; any other verb in the clause is still read as a proposal. Adds the real capture and two adversarial controls. * Parity caps: measured v1.91.19.0 union/skeleton sizes for ship, review, design-consultation, office-hours and design-shotgun, with the reason beside each * Regenerate generated output for v1.91.19.0 (gen:skill-docs --host all, goldens, llms.txt, context-budget fixture) * I1: plan-eng-review writes the Test Plan Artifact even while test choices are pending Periodic run 37151477069 (plan-eng-review-artifact): a non-interactive review recorded every Test review choice as pending and never wrote the QA test plan, because Step 5 said to write the artifact 'when these test and eval choices are resolved' while the artifact section says to list unresolved choices as pending. Both now say to write it after the decision gate, with unanswered choices under Pending Decisions. The pinned sentence becomes a meaning-level check. * I3: plan-eng-review states when findings first enter the report and that the scope menu's recommendation needs no repo probe Judge rationales for 'plan-eng-review/SKILL.md sections' (actionability 3.67 in periodic run 37151477069, clarity 3 in 36385945043 and 35567915613) cite two concrete tensions: 'do not add findings before Scope Challenge C' versus the ledger saves C itself performs, and 'Recommendation: A when a branch diff exists' versus 'do not probe for session state'. The write policy now says findings first enter through Scope Challenge C's ledger saves; the selector says its Recommendation line advises the user and needs no repository inspection. The pinned sentence becomes the new meaning-level regex. * I3: plan-ceo-review defines the closing sequence's cleanup step Two of three judge samples for 'plan-ceo-review/SKILL.md modes' in periodic run 37151477069 (actionability at the 4.0 gate) flag 'perform permitted cleanup' as never defined. The only cleanup the skill has is archiving old CEO plans, which needs approval; step 5 now says so. * I3: /qa-only's value-card rule names only its own card location qa-only rendered the /qa line 'Put it in the 8e.5 record (/qa) or under each proposed test (/qa-only)'; judge samples for 'qa-only/SKILL.md workflow' (actionability at the 4.0 gate in run 37151477069, 3.67 in 36907895813) flag the /qa-only-irrelevant 8e.5 record. The qa-mode bar now renders the invoking skill's location; both skills get shorter. * test-of-test ratchet: a repo script or setup read by path counts as product code codex-invocation-sites (runs scripts/gen-skill-docs.ts), gstack-config-gbrain-refresh (copies bin/<name> through a variable) and setup-cso-windows-probe (reads setup and scripts/build-cso-windows.ps1) exercise product code the detector could not see. * I1: DX floor actor strips the editor hint from the footer line only (from #2971) The custom-field reply normalization removed 'ctrl+g to edit in <editor>' from every line, so question text that resembled the menu footer could be rewritten and still authenticate. Only the last non-empty line is the footer. Ported from Garry's #2971 with its negative control. * I4: attribute every census testcase; deselections never count as SKIPPED The paid report counted every skipped JUnit testcase as SKIPPED and every testcase whose name was not a registry id as unattributed. On the 2026-10-03 census (run 37151477069) that was SKIPPED 82 / unattributed 72 (periodic) and SKIPPED 282 / unattributed 24 (gate), and every case-shard sibling also wrote a skipped history record that reset its pass-rate series. - attributeJUnitCase: a name is its registry id or CASE_TEST_NAMES label, else the only id its file registers, else the keyless file itself. Skipped other-tier cases, case-shard siblings, isolated or unselected cases and Bun's '(unnamed)' hook placeholders are zero-credit deselections with a reason and write no history record. Anything else is listed by name as unattributed. - One result and one history record per case per shard; a genuinely skipped selected case counts as SKIPPED and is listed with its reason, also in report-summary.md and collector-outcomes.json. SKIPPED panels show their reason and skipped trials are no longer called assertions. - CASE_TEST_NAMES labels for the three /cso cases. - tierSkipReason: a computed registration whose lane-tier ids are all registered to other test files is excluded at plan time, so the four gate and two periodic files that ran with every test skipped no longer take a runner. Census rerun: periodic SKIPPED 0 / unattributed 0, gate SKIPPED 0 / unattributed 0; verdicts unchanged. * I5: split the deploy file per case; guard PR-lane shard walls at 600 s test/skill-e2e-deploy.test.ts ran 668 s as one gate shard in the 2026-10-03 census (six sequential cases, 51-201 s each). It is now case-sharded, so each case is its own shard with its own (smaller) supervision budget; no timeout or budget was raised. - CASE_SHARDED_FILES gains the deploy file (names already equal ids). - Gate duration seeds for the six case keys from the census JUnit walls, so packing stays at 12 gate-census slices (CI cap 230m -> 200m). - shardDurationViolations + PR_LANE_SHARD_LIMIT_MS: a free test fails when a planned gate shard's recorded wall (committed seed, and the last gate census in test/fixtures/paid-report-census/gate-census-durations.json) exceeds 600 s. - Tests that pinned deploy as one shard follow the split (cookie planner keys, reuse fixture key, release/PR floors). * TODOS: /review fingerprint under Codex's read-only .git sandbox (found by the live Codex check) * Free-suite repairs from the first integrated Ubicloud run Product: - Outside-voice fences carry runtimeRootPrelude(ctx) explicitly again, so the resolver output runs on its own (the post-render pass skips fences that already assign GSTACK_ROOT); outsideVoiceRuntime stays deleted. - D5: an explicit CHROMIUM_PROFILE (gbrowser's per-workspace gbd) is never seeded from the shared profile or registered for `browse profiles prune`; this was also the free suite's write to the real ~/.gstack/browse-profiles.json. - gstack-ios-qa-daemon: BASH_COMPAT=50 for its >512-byte --help heredoc. Tests migrated to the wave's deliberate contract changes: - B1 heading admits `unverified`; sandbox flag is ${_GSTACK_CODEX_SANDBOX:?}; verdict-form output (VERDICT/FINDINGS/REASON); fake codex follows the preflight + stdin + -o form; Q2 notice marked shown in a private state root. - C1: an exported root needs bin/ and lib/; a lone GSTACK_BIN is not honored. - C4: plan-ceo-review is carved on every host (carrier, discovery and methodology fixtures read sections/ when the render has it). - E7: READY prints the resolved Aside path. - INV-3: the cookie-approval reconstruction reverses the bun-install mktemp template. - F1's `Plan:` binding read gets a reasoned tracker-guard exemption. - Browse stubs stop their server instead of scheduling process.exit; the headless launch check only counts browsers its own launch started. * Regenerate generated output after the outside-voice prelude repair * I1: revert the seccomp-only eval container option; it does not let Codex's sandbox start Run 37158847998 on this branch and a stock Ubicloud VM both show that seccomp=unconfined only moves bubblewrap's failure from 'No permissions to create a new namespace' to 'Failed to make / slave: Permission denied'. Codex's sandbox started only with the host sysctl kernel.apparmor_restrict_unprivileged_userns=0 plus seccomp and AppArmor unconfined on the container, which a container job cannot set from its steps. The relaxation bought nothing, so it goes; the fix is filed as a TODO for the integrator. This reverts commit |
||
|
|
ad8400543c |
v1.69.0.0 fix: the silent-failure wave — 6 fixes, 5 community PRs absorbed, tracker closed with receipts (#2666)
* test(wireup): make gbrain-missing PATH fixture hermetic The gbrain-missing test appended the host PATH (and a hardcoded /opt/homebrew/bin) to the fixture PATH, so on any machine with a real gbrain installed the 'missing' case saw it, exited 0 instead of 2, and could never fail where the bug exists — a false green for a whole machine class. The fixture now keeps only root-owned OS dirs on the child PATH, and a new determinism check plants a host-like gbrain to prove it is unreachable. Absorbed from PR #2615 with authorship preserved; the PR-thread liveness screenshot (docs/images/gstack-pr-liveness-2255.png) is dropped — referenced by nothing in the tree. Fixes #2255 Co-authored-by: CommandCodeBot <noreply@commandcode.ai> * fix(evidence): stop bun's dotenv autoload from reaching the spawned command `bin/gstack-evidence` has a `#!/usr/bin/env bun` shebang, and bun AUTO-LOADS `.env`, `.env.<NODE_ENV>` and `.env.local` from the cwd into `process.env`. The wrapper then spawned the command with no `env` override, so every command run through it inherited those variables — and a repo `.env.local` routinely holds production credentials. Two things go wrong, and the second is worse than the leak: 1. Secrets reach a child that would not otherwise have them. `npm test` run by hand in the same shell sees none of them; the same command through the wrapper sees all of them. 2. THE COMMAND UNDER TEST BEHAVES DIFFERENTLY, so the ledger certifies a run that is not the run CI performs. Observed in a Next.js repo on 2026-08-20: four tests failed 4/4 through the wrapper and passed 5/5 without it, because app code branched on env vars only the wrapper supplied. Nearly an hour went into chasing a "flake" that was the measuring instrument. The wrapper exists to record trustworthy evidence, so silently altering the environment defeats its purpose. The fix builds the child env from `process.env` minus the keys bun injected, and detection is exact rather than heuristic: verified on bun 1.3.11, a dotenv file does NOT override a variable the shell already exported (the shell's value wins). So a key whose live value equals the dotenv file's value was injected by bun, and dropping it restores the environment the user's own shell would have given the command. A key whose live value differs is genuinely the caller's and survives. `BUN_DOTENV_FILES()` mirrors bun's precedence, including that `.env.local` is skipped when NODE_ENV is "test" — scrubbing a key bun never loaded would strip a variable the caller legitimately provided. Escape hatch: GSTACK_EVIDENCE_KEEP_DOTENV=1 keeps the old behaviour. When keys are scrubbed the wrapper warns with the KEY NAMES ONLY, so the diagnostic cannot become the leak it prevents. Tests: 6 cases, mutation-verified — removing `env: spawnEnv` reddens exactly the two leak tests and restoring it gives 30/30. Every leak test asserts the scrub warning fired, because `bun test` sets NODE_ENV=test and the first version of these tests passed vacuously against a `.env.local` bun had never loaded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Absorbed from PR #2652 with authorship preserved. Wave additions: a doc-comment on the ${VAR}-expansion limitation (bun expands refs, the reader compares raw text — those keys are left in the child env, failing open) and a regression pin for the unreadable-.env fail-open path with a functional DAC-override skip guard. Fixes #2624 * fix(setup): reap dangling skill dirs when the payload is gone cleanup_old_claude_symlinks derived its work list from the payload directory, so when the payload was gone — precisely when orphans exist — the glob matched nothing and the loop never ran; the -f guard also followed symlinks, hiding dangling SKILL.md links even with a payload present. The cleanup now scans the DESTINATION skills dir (-e/-L, so dangling symlinks are visible) and anchors SKILL.md provenance to path segments (gstack/*, */gstack/*, */.gstack/render/claude/*) instead of a bare *gstack* substring that would eat a user skill under ~/tools/gstack-fork/. The Windows real-file arm stays payload-gated: a real file has no provable owner. Absorbed from PR #2634 (2 commits squashed) with authorship preserved. The symmetric cleanup_prefixed_claude_symlinks hole is filed as a TODOS.md residual in this wave. Fixes #2204 * fix(redact): tolerate EEXIST from recursive mkdir in install-prepush-hook on bun/Windows (#2635) fs.mkdirSync(dir, { recursive: true }) is a no-op on an existing directory in Node, but bun on Windows throws EEXIST - crashing hook install on any repo whose .git/hooks already existed, leaving the repo unprotected. Add lib/fs-utils.ts mkdirpSync: swallow EEXIST only when statSync confirms the path is an existing directory; a regular file occupying the path, a stat failure, or any other errno still rethrows. Use it in installPrepushHook(). The regression test emulates the Windows bun fs semantics via a bun --preload fixture, so the exact crash path runs (and fails on the old code) on any platform, including CI Linux. Absorbed from PR #2641 with authorship preserved. Fixes #2635 * fix(bin): route remaining Windows-reachable mkdirSync sites through mkdirpSync Sweep follow-up to #2641's lib/fs-utils.ts helper: bun on Windows throws EEXIST from a recursive mkdir on an existing dir, so every unguarded recursive mkdirSync on a Windows-reachable path is a latent crash. Converted: bin/gstack-decision-log (unguarded, runs on every decision log — the second call on any machine hits the pre-existing projects dir), bin/gstack-evidence logsDir + ledger dir sites, and bin/gstack-redact-prepush's skip-log site (already try-wrapped, so its failure mode was a silent skip-log loss rather than a crash — the fix makes the log survive). The ~15 remaining gbrain/mac-lane sites are deliberately left alone. Regression: fs-utils.test.ts drives gstack-decision-log twice, the second run under the bun-Windows EEXIST preload fixture — the pre-sweep code exits 1 with EEXIST there; verified red against v1.68.3.0. * fix(setup-gbrain): warn about the ZeroEntropy sunset before Sept 4 ZeroEntropy was acquired by Notion and sunsets its hosted API on September 4, 2026. A gbrain configured with the zeroentropyai embedding recipe keeps importing pages after that date but embedding silently fails — pages land structurally with no semantic search, this repo's tracker P1 (TODOS.md NEXT PRIORITY). Nothing in gstack ever recommended ZeroEntropy (the dependency is gbrain-internal), so the gstack side is detection + advisory: the wireup helper warns when ~/.gbrain/config.json names the recipe (fail-open grep — a missing, unreadable, or other-provider config stays silent and never blocks a working setup), the setup-gbrain provider-default comments say never to select the legacy recipe for a new brain, and USING_GBRAIN_WITH_GSTACK.md gains a troubleshooting entry. The gbrain-side provider migration stays open upstream. Refs #2365 * fix(gbrain-source-wireup): first sync targets the registered source, not --repo The wireup registered a federated source by id, then ran 'gbrain sync --repo $WORKTREE' — which resolves against the brain's DEFAULT source and (on gbrain 0.46.x) rewrites that source's local_path anchor to our worktree. Net effect: the user's primary knowledge source silently repointed at the gstack brain worktree while the just-registered source got zero pages, and pages_synced still reported success. The sync now targets the registered id ('gbrain sync --source $id', the same form the repo's own troubleshooting documents). Because the script's stated floor is gbrain >= 0.18.0 and nothing proves --source exists there, support is probed via 'gbrain sync --help' first: an older gbrain keeps the wrong-but-working --repo call with an upgrade warning instead of converting it into a hard failure. The probe sits after the GSTACK_BRAIN_NO_SYNC early-exit and is unreachable in --probe mode. Regression tests (fail on v1.68.3.0): a no-skip sync case asserting the call log shows 'sync --source gstack-brain-<id>' and never 'sync --repo', and an old-gbrain fallback case (fake sync --help without --source) asserting --repo plus the upgrade warning. Fixes #2662 * fix(setup): --host slate exits informatively instead of silently installing nothing slate passed --host validation (added to the accept-list in v1.64.1.0) but never got a dispatch arm, and the all-INSTALL_*-zero fallback lives inside the auto branch — so './setup --host slate' configured nothing and exited 0, a silent no-op strictly worse than the original hard rejection. slate is now an informational arm (per docs/designs/SLATE_HOST.md it is blocked on the host-config refactor; Slate reads .claude/skills as a compatibility fallback, so the arm points at './setup --host claude'), and a defensive guard after the dispatch chain errors loudly (naming the host, the missing arm, and the valid targets, exit 1) if a future host is ever accepted without being wired. Regression tests (fail on v1.68.3.0): a dispatch-arm ratchet asserting every accept-listed install target has a matching dispatch branch — the exact drift class; a registry cross-check deriving both sides from hosts/index.ts and setup's case arms; a behavioral slate probe (exit 0, points at --host claude, never reaches the installer — on unfixed code it fell through into the installer); and a static pin on the guard's shape. Fixes #2361 * fix(make-pdf): resolve the sibling browse binary from execPath, not argv[0] In a bun-compiled binary process.argv[0] is the raw invocation string — often relative ('./pdf', 'pdf') — so dirname(argv[0]) yielded '.' and the sibling candidates (../browse/dist/browse etc.) resolved against the CWD instead of the install dir. Resolution was cwd-dependent: correct-by-luck when the fallbacks rescued it, wrong when a cwd-relative path matched. process.execPath is always the absolute binary path. The resolution step takes an injectable selfPath (defaulted) because under bun test the process path is the bun runtime and the compiled-binary shapes are otherwise unreachable. The issue's other half — pdf setup failing on newtab('about:blank') — was already fixed on main in v1.64.0.0 (browse/src/url-validation.ts exact-match allows about:blank; its comment names this exact smoke). This commit closes what remains. Regression tests (the sibling-via-selfPath case fails on v1.68.3.0 — pre-fix code ignores the seam and either resolves the global install or throws): sibling resolution from an install-shaped tree, and a decoy-browse-DIRECTORY case pinning that a directory never wins resolution. Fixes #2156 * fix(memory-ingest): store the normalized git_remote so unattributed pages hit the policy filter buildTranscriptPage wrote the normalized '_unattributed' sentinel into the page FRONTMATTER but stored the raw resolved remote ('' when unresolvable) on the page object. The policy filter fast-paths !p.git_remote, so under --include-unattributed an explicit '_unattributed → deny' (or read-only) policy never applied to exactly the pages it names — they ingested unpoliced. The stored value now matches the frontmatter. Regression test (fails on v1.68.3.0): seeds the REAL bin/gstack-gbrain-repo-policy store with '_unattributed → deny' through its own set verb, ingests an unresolvable-remote session with --include-unattributed, and asserts nothing reaches gbrain — pre-fix the '' remote bypassed the filter and the import ran. A fake echoing tiers would pass on both sides of the fix; the real helper prints 'none' for unknown keys, so only a genuinely applied deny distinguishes the two. Fixes #2353 * fix(land-and-deploy): MERGED recovery reconciles and reports remote-branch cleanup Step 4's merge commands carry --delete-branch, and the success path tells the user 'The branch has been cleaned up.' When gh exits non-zero AFTER GitHub already merged (routine in worktree layouts: gh's local cleanup runs git checkout <base> and fails), the §4a-postfail MERGED recovery re-established everything EXCEPT the branch deletion — and said nothing about it, so the discrepancy was invisible. The MERGED path now reconciles: git ls-remote --heads distinguishes branch-already-gone (exit 0, empty → 'already cleaned up', idempotent on re-runs) from branch-survived (offer confirm-first deletion, matching the section's worktree posture; -d not -D for any local branch) from check-itself-failed (non-zero exit → 'couldn't verify', skip the offer — never read a failed check as a clean branch). Template + regenerated SKILL.md + test extensions land in one commit (the md-sync assertion goes red otherwise). Regression assertions (fail on v1.68.3.0: no delete-branch reconciliation existed in test/ at all) pin the ls-remote check, the confirm-first delete, and the absent-vs-failed distinction. Fixes #2656 * fix(scripts): stop heredoc bodies deadlocking under Homebrew bash `./setup --help` can hang forever on macOS, printing nothing, with no way to tell it apart from a slow install. Eleven scripts carry the same latent hang, `setup` itself being the one every user hits first. bash 5.2+ delivers a heredoc body of 64KiB or less through a pipe: the forked child writes the entire body before exec, and nothing reads the other end until the command starts. Under macOS pipe-KVA pressure the kernel hands a fresh pipe a 512-byte buffer instead of the usual 16-64KiB, so any body of 512 bytes or more blocks write() permanently. The capacity check bash would need to notice (F_GETPIPE_SZ) is Linux-only, so it never fires here. It is pressure-dependent, which is why it reads as "worked on my machine" — the same script runs fine all day and then wedges. Homebrew bash is what `#!/usr/bin/env bash` resolves to on a Mac with brew on PATH, which is most of them. Apple's /bin/bash 3.2 predates the pipe path and is unaffected, so the bug is invisible to anyone testing with the system shell. The fix is `BASH_COMPAT=50` in each affected script, which restores the pre-5.2 tempfile path: $ bash -c 'probe() { [ -p /dev/stdin ] && echo PIPE || echo TEMPFILE; } probe <<EOF $(printf "x%.0s" $(seq 1 1000)) EOF' PIPE $ BASH_COMPAT=50 bash -c '...same...' TEMPFILE - Not a `#!/bin/bash` shebang swap: that pins the script to whatever bash lives at /bin (3.2 on macOS, absent on some Linux distributions) and is bypassed entirely by `bash script.sh` call sites. The variable survives both. - Not exported, so child processes keep their own compat level. - Placed below any `--help` sed range that reads $0, so usage output is unchanged (verified on all eleven). - Every guarded script is bash-3.2-clean — no associative arrays, case conversion, or mapfile — so compat level 50 costs them nothing. test/heredoc-pipe-deadlock.test.ts scans every tracked shell script for a heredoc body in the 512B-64KiB window and fails without the guard, and proves the mechanism at runtime on bash 5.2+ by asserting the body moves from PIPE to TEMPFILE. On older bash the runtime half is skipped, since the pipe path does not exist there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Absorbed from PR #2640 with authorship preserved. Wave adaptations: the pipe-probe test skips on minimal-/dev environments without /dev/stdin (it would report OTHER for an unobservable fd), and one caveat verified during review: on bash 4.3/4.4 (e.g. Git Bash), assigning BASH_COMPAT=50 prints a non-fatal 'invalid value' warning to stderr — those bashes are already on tempfiles, so the guard is a no-op there; windows-setup-e2e exercises this empirically. * docs: TODOS.md v1.69 wave close-out Move the slate P4 entry and the ZeroEntropy P1's gstack-side half to Completed (v1.69.0.0); reframe the ZeroEntropy NEXT PRIORITY entry around the remaining gbrain-side work; file the wave's four residuals with rationale — the prefixed-cleanup symmetric conversion, the #2163 legacy-slug checkpoint heal, the invited #2657 --reconcile contribution, and the table-driven setup host dispatch behind the new cross-check ratchet. * chore: bump version and changelog (v1.69.0.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Som Samantray <som.samantray@gmail.com> Co-authored-by: CommandCodeBot <noreply@commandcode.ai> Co-authored-by: Connex Client Access <paul@paulkortman.com> Co-authored-by: y$un_ <forrest.sun527@gmail.com> Co-authored-by: Lockyer <135391289+Lockyer228@users.noreply.github.com> Co-authored-by: Benjamin D. Smith <benjamin.smith@binarysword.com> Co-authored-by: Claude Fable 5 <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> |
||
|
|
f44de365c5 |
v1.27.0.0 feat: /setup-gbrain Path 4 (remote MCP) + brain → artifacts rename (#1351)
* feat: gstack-gbrain-mcp-verify helper for remote MCP probe
Probes a remote gbrain MCP endpoint with bearer auth. POSTs initialize,
classifies failures into NETWORK / AUTH / MALFORMED with one-line
remediation hints, and runs a tools/list capability probe to detect
sources_add MCP support (forward-compat for when gbrain ships URL ingest).
Token consumed from GBRAIN_MCP_TOKEN env, never argv. Required to set
both 'application/json' AND 'text/event-stream' in Accept; that gotcha
costs 10 minutes of debugging when missed (regression-tested).
Live-verified against wintermute (gbrain v0.27.1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: gstack-artifacts-init + gstack-artifacts-url helpers
artifacts-init replaces brain-init with provider choice (gh / glab /
manual), per-user gstack-artifacts-$USER repo, HTTPS-canonical storage in
~/.gstack-artifacts-remote.txt, and a "send this to your brain admin"
hookup printout. Always prints the command, never auto-executes — gbrain
v0.26.x has no admin-scope MCP probe (codex Finding #3).
artifacts-url centralizes HTTPS↔SSH/host/owner-repo conversion so callers
don't each string-mangle (codex Finding #10). The remote-conflict check in
artifacts-init compares at the canonical level so re-running with HTTPS
input doesn't trip on a stored SSH URL for the same logical repo.
The "URL form not supported" branch prints a two-line clone-then-path
form for gbrain v0.26.x; the supported branch is a one-liner with --url
ready for when gbrain ships URL ingest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: extend gstack-gbrain-detect with mcp_mode + artifacts_remote
Adds two new fields to detect's JSON output:
- gbrain_mcp_mode: local-stdio | remote-http | none
Resolved via 3-tier fallback (codex Finding D3): claude mcp get --json
→ claude mcp list text-grep → ~/.claude.json jq read. If Anthropic moves
the file format, the first two tiers absorb it.
- gstack_artifacts_remote: HTTPS URL from ~/.gstack-artifacts-remote.txt
Falls back to ~/.gstack-brain-remote.txt during the v1.27.0.0 migration
window so detect doesn't return empty between upgrade and migration.
Existing detect tests still pass (15/15). New 19 tests cover every fallback
tier independently, plus a schema regression for /sync-gbrain compat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: setup-gbrain Path 4 (remote MCP) + artifacts rename
Path 4 lets users paste an HTTPS MCP URL + bearer token and registers it
as an HTTP-transport MCP without needing a local gbrain CLI install. The
flow:
- Step 2 gains a fourth option (Remote gbrain MCP)
- Step 4 adds Path 4 sub-flow: collect URL, secret-read bearer, verify
via gstack-gbrain-mcp-verify (NETWORK / AUTH / MALFORMED classifier)
- Step 5 (local doctor), Step 7.5 (transcript ingest), Step 5a's stdio
branch all skip on Path 4
- Step 5a adds an HTTP+bearer registration form: claude mcp add
--transport http --header "Authorization: Bearer ..."
- Step 7 renamed "session memory sync" → "artifacts sync" and now calls
gstack-artifacts-init (which always prints the brain-admin hookup
command — no auto-execute, codex Finding #3)
- Step 8 CLAUDE.md block branches: remote-http includes URL + server
version (never the token); local-stdio keeps engine + config-file
- Step 9 smoke test on Path 4 prints the curl-equivalent for
post-restart verification (MCP tools aren't visible mid-session)
- Step 10 verdict block has separate templates per mode
Idempotency: re-running with gbrain_mcp_mode=remote-http already in
detect output skips Step 2 entirely and goes to verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: rename gbrain_sync_mode → artifacts_sync_mode (v1.27.0.0 prep)
Hard rename, no dual-read alias (codex Finding D4). The on-disk migration
script (Phase C, separate commit) renames the config key in users'
~/.gstack/config.yaml and any CLAUDE.md blocks.
Touched call sites:
- bin/gstack-config defaults + validation + list/defaults output
- bin/gstack-gbrain-detect (gstack_brain_sync_mode field still emitted
with the same name for downstream-tool compat; reads new key)
- bin/gstack-brain-sync, bin/gstack-brain-enqueue, bin/gstack-brain-uninstall
- bin/gstack-timeline-log (comment ref)
- scripts/resolvers/preamble/generate-brain-sync-block.ts: renames key,
branches on gbrain_mcp_mode=remote-http to emit "ARTIFACTS_SYNC:
remote-mode (managed by brain server <host>)" instead of the local
mode/queue/last_push line (codex Finding #11)
- bin/gstack-brain-restore + bin/gstack-gbrain-source-wireup: read
~/.gstack-artifacts-remote.txt with ~/.gstack-brain-remote.txt fallback
during the migration window
- bin/gstack-artifacts-init: tolerant of unrecognized URL forms (local
paths, file://, self-hosted gitea) so test infrastructure and unusual
remotes work without canonicalization
- test/brain-sync.test.ts: gstack-brain-init → gstack-artifacts-init
- test/skill-e2e-brain-privacy-gate.test.ts: artifacts_sync_mode keys
- test/gen-skill-docs.test.ts: budget 35K → 36.5K for the new MCP-mode
probe in the preamble resolver
- health/SKILL.md.tmpl, sync-gbrain/SKILL.md.tmpl: comment + verdict line
Hard delete:
- bin/gstack-brain-init (replaced by bin/gstack-artifacts-init in v1.27.0.0)
- test/gstack-brain-init-gh-mock.test.ts (replaced by gstack-artifacts-init.test.ts)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: regenerate SKILL.md files after artifacts-sync rename
Mechanical regen via \`bun run gen:skill-docs --host all\`. All */SKILL.md
files reflect the renamed config key (gbrain_sync_mode →
artifacts_sync_mode), the renamed remote-helper file
(~/.gstack-artifacts-remote.txt with brain fallback), the renamed init
script (gstack-artifacts-init), and the new ARTIFACTS_SYNC: remote-mode
status line that fires when a remote-http MCP is registered.
Golden fixtures (test/fixtures/golden/*-ship-SKILL.md) refreshed to match
the regenerated default-ship output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: v1.27.0.0 migration — gstack-brain → gstack-artifacts rename
Journaled, interruption-safe migration. Six steps, each writes to
~/.gstack/.migrations/v1.27.0.0.journal on success; re-entry resumes
from the next un-done step. On final success, journal is replaced by
~/.gstack/.migrations/v1.27.0.0.done.
Steps:
1. gh_repo_renamed gh/glab repo rename gstack-brain-$USER →
gstack-artifacts-$USER (idempotent: detects
already-renamed and skips)
2. remote_txt_renamed mv ~/.gstack-brain-remote.txt → artifacts file,
rewriting URL path to match the new repo name
3. config_key_renamed sed -i in ~/.gstack/config.yaml flips
gbrain_sync_mode → artifacts_sync_mode
4. claude_md_block sed flips "- Memory sync:" → "- Artifacts sync:"
in cwd CLAUDE.md and ~/.gstack/CLAUDE.md
5. sources_swapped gbrain sources add NEW (verify) → remove OLD
(codex Finding #6: add-before-remove ordering,
no downtime window). On remote-MCP mode, prints
commands for the brain admin instead of executing.
6. done touchfile + delete journal
User opt-out: any "n" or "skip-for-now" answer at the initial prompt
writes a marker file that prevents re-prompting; user can re-invoke
via /setup-gbrain --rerun-migration.
11 unit tests cover: nothing-to-migrate, GitHub happy path, idempotent
re-run, journal-resume mid-flight, remote-MCP print-only path,
add-before-remove ordering verification, add-fail → old source stays
registered, CLAUDE.md field rewrite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: regression suite + E2E for v1.27.0.0 rename
Three new regression tests guard the rename's blast radius (per codex
Findings #1, #8, #9, #12):
- test/no-stale-gstack-brain-refs.test.ts: greps bin/, scripts/, *.tmpl,
test/ for forbidden identifiers (gstack-brain-init, gbrain_sync_mode);
fails CI if any non-allowlisted file references them.
- test/post-rename-doc-regen.test.ts: confirms gen-skill-docs output has
no stale references in any */SKILL.md (the cross-product blind spot).
- test/setup-gbrain-path4-structure.test.ts: structural lint over the
Path 4 prose contract — STOP gates after verify failure, never-write-
token rules, mode-aware CLAUDE.md block, bearer always via env-var.
Two new gate-tier E2E tests (deterministic stub HTTP server, fixed inputs):
- test/skill-e2e-setup-gbrain-remote.test.ts: Path 4 happy path. Stubs
an HTTP MCP server, drives the skill via Agent SDK with a stubbed
bearer, asserts claude.json gets the http MCP entry, CLAUDE.md gets
the remote-http block, the secret token NEVER leaks to CLAUDE.md.
- test/skill-e2e-setup-gbrain-bad-token.test.ts: stub server returns 401;
asserts the AUTH classifier hint surfaces, no MCP registration occurs,
CLAUDE.md is unchanged. Regression guard for the "verify failed → STOP"
rule.
touchfiles.ts: setup-gbrain-remote and setup-gbrain-bad-token added at
gate-tier so CI catches Path 4 regressions on every PR.
Plus a few comment refs flipped: bin/gstack-jsonl-merge, bin/gstack-timeline-log
(legacy gstack-brain-init mentions in headers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* release: v1.27.0.0 — /setup-gbrain Path 4 + brain → artifacts rename
Bumps VERSION 1.26.4.0 → 1.27.0.0 (MINOR per CLAUDE.md scale-aware bump
guidance: ~1500 line net change including a new path in /setup-gbrain,
two new bin helpers, a journaled migration, 59 new tests, and a config
key rename across the codebase).
CHANGELOG entry covers: Path 4 (Remote MCP) end-to-end, the brain →
artifacts rename, the journaled migration, the verify-helper error
classifier, the artifacts-init multi-host provider choice. Includes
the canonical Garry-voice headline + numbers table + audience close
per the release-summary format.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: demote setup-gbrain Path 4 E2E to periodic-tier
The Agent SDK E2E tests for Path 4 (skill-e2e-setup-gbrain-remote and
skill-e2e-setup-gbrain-bad-token) are inherently non-deterministic —
the model interprets "follow Path 4 only" prompts flexibly and can
skip Step 8 (CLAUDE.md write) or shortcut past the verify helper, which
makes the gate-tier assertions flaky.
The deterministic gate coverage for Path 4 is in
test/setup-gbrain-path4-structure.test.ts: a fast structural lint that
catches AUQ-pacing regressions and prose contract drift in <200ms with
zero token spend. That test is the right tool for catching the failure
mode the gate-tier was meant to guard against.
The Agent SDK E2E tests stay available on-demand for periodic-tier runs
(EVALS=1 EVALS_TIER=periodic bun test test/skill-e2e-setup-gbrain-*.test.ts).
Also tightened the verify-error assertion to the literal field shape
("error_class": "AUTH") instead of a substring match that false-matches
the parent claude session's "needs-auth" MCP discovery markers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: sync package.json version to 1.27.0.0
VERSION was bumped to 1.27.0.0 in
|
||
|
|
9dbaf906cf |
feat(v1.9.0.0): gbrain-sync — cross-machine gstack memory (#1151)
* feat(gbrain-sync): queue primitives + writer shims
Adds bin/gstack-brain-enqueue (atomic append to sync queue) and
bin/gstack-jsonl-merge (git merge driver, ts-sort with SHA-256 fallback).
Wires one backgrounded enqueue call into learnings-log, timeline-log,
review-log, and developer-profile --migrate. question-log and
question-preferences stay local per Codex v2 decision.
gstack-config gains gbrain_sync_mode (off/artifacts-only/full) and
gbrain_sync_mode_prompted keys, plus GSTACK_HOME env alignment so
tests don't leak into real ~/.gstack/config.yaml.
* feat(gbrain-sync): --once drain + secret scan + push
bin/gstack-brain-sync is the core sync binary. Subcommands: --once
(drain queue, allowlist-filter, privacy-class-filter, secret-scan
staged diff, commit with template, push with fetch+merge retry),
--status, --skip-file <path>, --drop-queue --yes, --discover-new
(cursor-based detection of artifact writes that skip the shim).
Secret regex families: AWS keys, GitHub tokens (ghp_/gho_/ghu_/ghs_/
ghr_/github_pat_), OpenAI sk-, PEM blocks, JWTs, bearer-token-in-JSON.
On hit: unstage, preserve queue, print remediation hint (--skip-file
or edit), exit clean. No daemon — invoked by preamble at skill
boundaries.
* feat(gbrain-sync): init, restore, uninstall, consumer registry
bin/gstack-brain-init: idempotent first-run. git init ~/.gstack/,
.gitignore=*, canonical .brain-allowlist + .brain-privacy-map.json,
pre-commit secret-scan hook (defense-in-depth), merge driver registration
via git config, gh repo create --private OR arbitrary --remote <url>,
initial push, ~/.gstack-brain-remote.txt for new-machine discovery,
GBrain consumer registration via HTTP POST.
bin/gstack-brain-restore: safe new-machine bootstrap. Refuses clobber
of existing allowlisted files, clones to staging, rsync-copies tracked
files, re-registers merge drivers (required — not cloned from remote),
rehydrates consumers.json, prompts for per-consumer tokens.
bin/gstack-brain-uninstall: clean off-ramp. Removes .git + .brain-*
files + consumers.json + config keys. Preserves user data (learnings,
plans, retros, profile). Optional --delete-remote for GitHub repos.
bin/gstack-brain-consumer + bin/gstack-brain-reader (symlink alias):
registry management. Internal 'consumer' term; user-facing 'reader'
per DX review decision.
* feat(gbrain-sync): preamble block — privacy gate + boundary sync
scripts/resolvers/preamble/generate-brain-sync-block.ts emits bash that
runs at every skill invocation:
- Detects ~/.gstack-brain-remote.txt on machines without local .git
and surfaces a restore-available hint (does NOT auto-run restore).
- Runs gstack-brain-sync --once at skill start to drain any pending
writes (and at skill end via prose instruction).
- Once-per-day auto-pull (cached via .brain-last-pull) for append-only
JSONL files.
- Emits BRAIN_SYNC: status line every skill run.
Also emits prose for the host LLM to fire the one-time privacy
stop-gate (full / artifacts-only / off) when gbrain is detected and
gbrain_sync_mode_prompted is false. Wired into preamble.ts composition.
* test(gbrain-sync): 27-test consolidated suite
test/brain-sync.test.ts covers:
- Config: validation, defaults, GSTACK_HOME env isolation
- Enqueue: no-op gates, skip list, concurrent atomicity, JSON escape
- JSONL merge driver: 3-way + ts-sort + SHA-256 fallback
- Init + sync: canonical file creation, merge driver registration,
push-reject + fetch+merge retry path
- Init refuses different remote (idempotency)
- Cross-machine restore round-trip (machine A write → machine B sees)
- Secret scan across all 6 regex families (AWS, GH, OpenAI, PEM, JWT,
bearer-JSON). --skip-file unblock remediation
- Uninstall removes sync config, preserves user data
- --discover-new idempotence via mtime+size cursor
Behaviors verified via integration smokes during implementation. Known
follow-up: bun-test 5s default timeout needs 30s wrapper for
spawnSync-heavy tests.
* docs(gbrain-sync): user guide + error lookup + README section
docs/gbrain-sync.md: setup walkthrough, privacy modes, cross-machine
workflow, secret protection, two-machine conflict handling, uninstall,
troubleshooting reference.
docs/gbrain-sync-errors.md: problem/cause/fix index for every
user-visible error. Patterned on Rust's error docs + Stripe's API
error reference.
README.md: 'Cross-machine memory with GBrain sync' section near the
top (discovery moment), plus docs-table entry.
* chore: bump version and changelog (v1.7.0.0)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: regenerate SKILL.md files for gbrain-sync preamble block
Re-runs bun run gen:skill-docs after adding generateBrainSyncBlock
to scripts/resolvers/preamble.ts in
|