Merge remote-tracking branch 'origin/main' into capy/merge-train-workflow-side

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

# Conflicts:
#	CHANGELOG.md
#	scripts/harness-version.json
This commit is contained in:
garrytan committed 2026-10-20 01:39:30 +00:00
commit afa9fc11fd
420 files changed
+5512 -1912

No files matched your search

+1
View File
@@ -298,3 +298,4 @@ Agents poll `eval:bg:*` logs for the `### gstack-detach EXIT=<code> ###` sentine
- Every Codex run's verdict goes through `lib/outside-review-result.ts`; its reason codes live in `lib/gate-outcomes.ts`, each with a `docs/troubleshooting.md` anchor.
- `test/module-size-ratchet.test.ts` keeps refactored owner modules at or under 800 lines (150 per function) and residual files from growing.
- The `claude` CLI binary resolves via `lib/claude-bin.ts` (re-exported from `browse/src/claude-bin.ts` for browse internals; `Bun.which()` + `GSTACK_CLAUDE_BIN` override). Set `GSTACK_CLAUDE_BIN=wsl` plus `GSTACK_CLAUDE_BIN_ARGS='["claude"]'` to run Claude through WSL on Windows.
- Every Bun gstack starts ignores the working directory's `.env` files and `bunfig.toml`: bun shebangs are `#!/usr/bin/env -S bun --no-env-file --config=/dev/null`, shell and skill callers pass `bun --no-env-file --config=/dev/null`, TypeScript spawns use `bunIsolationArgs()` from `lib/bun-isolation.ts`, and `bun build --compile` adds `--no-compile-autoload-dotenv --no-compile-autoload-bunfig`. `test/bun-entry-isolation.test.ts` enforces all four.
+1 -1
View File
@@ -91,7 +91,7 @@ The server writes `.gstack/browse.json` (atomic write via tmp + rename, mode 0o6
{ "pid": 12345, "port": 34567, "token": "uuid-v4", "startedAt": "...", "binaryVersion": "abc123" }
```
The CLI reads this file to find the server. If the file is missing or the daemon process is dead, the CLI spawns a new server. A process that is alive but not answering `/health` is busy, not dead: the CLI probes for a bounded ~8s, then reports busy with a nonzero exit — only an explicit `--force-restart` kills a live daemon. Process liveness uses signal-0 (`isProcessAlive`, EPERM counts as alive) on every platform, with the health check (GET /health) as the responsiveness signal. Daemon stdout/stderr persists to `<project>/.gstack/browse-daemon.log`.
The CLI reads this file to find the server. If the file is missing or the daemon process is dead, the CLI spawns a new server. A process that is alive but not answering `/health` is busy, not dead: the CLI probes for a bounded ~8s, then reports busy with a nonzero exit — only an explicit `--force-restart` kills a live daemon. Process liveness uses signal-0 (`isProcessAlive`, EPERM counts as alive) on every platform, with the health check (GET /health) as the responsiveness signal. Its `status` covers the daemon and its Chromium connection only; the active tab's main thread is reported separately (`activeTab.responsive`, a probe bounded to 1s), so one page stuck in a long script never makes the CLI refuse commands. Daemon stdout/stderr persists to `<project>/.gstack/browse-daemon.log`.
### Port selection
+9 -1
View File
@@ -584,7 +584,7 @@ SITE=app.example.com
DOMAIN=example.com
$B goto "https://$SITE"
$B cookie-import-browser opera-gx --domain "$DOMAIN" --profile Default # or: opera
$B reload # confirm the intended account
$B reload # view the page; only --verify-auth proves sign-in
```
Run `$B cookie-import-browser` with no flags to see which browsers were detected. `--profile` can be omitted when only one profile has cookies for the domain. If the receipt reports App-Bound Encryption, run `$B handoff`, sign in to the intended account in the window that opens, then `$B resume` (needs a display).
@@ -626,6 +626,14 @@ Storage stays intact by default. With explicit approval on a Chromium target, `-
| `tab-each <command> [args...]` | Fan out a command across every open tab; returns JSON |
| `frame <sel\|@ref\|--name n\|--url pattern\|main>` | Switch to iframe context (or back to main); clears refs |
**A busy tab is not a busy daemon.** `/health` `status` is daemon liveness
(Chromium connected); the active tab's main thread is reported separately as
`activeTab: { id, responsive }`, probed for at most 1s. While one page is stuck
in a long script, `status`, `tabs`, `tab <id>`, `newtab`, `closetab`,
`state save` and `stop` keep working, and `status` and `tabs` mark the tab as
not responding. A command that needs the stuck tab times out after 30s and
names it, so you can switch away or close it without `--force-restart`.
### Extraction
| Command | Description |
+162
View File
@@ -15,6 +15,168 @@
- **Gate integrity (train block only):** under a `train` block, PR pushes no longer start paid slices; trains, screens and the backstop gate instead. Train and bisect runs never reuse a verdict and never bind one for reuse. No threshold, trial count, case kind or selection changed.
- **Gate integrity (always, GSTA-48 P3-1):** when the readiness job blocks a PR head, a new `readiness-comment` job replaces an existing `## E2E Evals` comment with "NOT RUN on <head>", so an earlier PASS no longer stands beside a head that never ran. It posts nothing when no such comment exists. A slice killed before its upload step counts as incomplete.
- **Gate integrity, widening (`plan-design-with-ui-scope` focus detector, GStack Minder-approved):** the Step 0D focus question also counts when its title names the focus (the title ends at the first line break or before `Project/branch/task:`), its body carries the step's own rating sentence ("I('ve) rated this plan N/10 on design completeness"), and it offers at least two options, one of which starts "All 7" or "All seven" (design or review) dimensions or passes. Opus 5.5 titled the question only "Review focus" in both attempts of run 39298269902, so the detector missed it. In the second attempt the finding detector also counted the focus question as the review finding, because its one-line title names hierarchy and panels. A recognized focus question can no longer count as a finding. Free replays of both captured attempts (`test/fixtures/design-ui-focus-capture-39298269902-a1.json` and `-a2.json`, registered in the case's touchfiles) pass. Every existing negative control still fails, as do these new ones: an issue-labeled finding that carries the rating, a single-option focus question, a focus question without a leading all-7 option, and a 4/7 rating. No threshold, trial count, case kind or ceiling changed.
## [1.91.161.0] - 2026-10-19
**`gstack-pregate` works on Django, Dart and Flutter repos, and on branches that only touch assets or tool config.** Before, a Django migration, an `admin.py`, a template, a font, a vendored licence or `.pre-commit-config.yaml` stopped `/ship` with `PREGATE_NO_LANE` in a repo that had never opted in, and every touched `_test.dart` file did the same. `gstack-pregate init` now drafts a policy from the repo instead of writing a fixed example. Contributed by @arlophoenix (#3285) and @szsunyuan (#3290); reported by @dgrant (#3266, #3267, #3268, #3269).
### Fixed
- **Django files loaded by name select tests (#3266).** In a Django project (a manage.py or code importing django, plus Python code that assigns `INSTALLED_APPS` or `ROOT_URLCONF`, or a config file naming `DJANGO_SETTINGS_MODULE`), every Python test depends on the settings module; a dotted string in settings and an `include("app.urls")` argument count as imports; an app's migrations, `admin.py`, `templates/`, `static/`, `templatetags/`, `management/`, `fixtures/` and `locale/` files select that app's tests and the tests that reach its `models.py`; a project-level template selects the suite.
- **Files no test runner loads take an empty lane (#3267).** Images, fonts, media and PDFs (outside snapshot, golden and fixture trees) and allowlisted tool config (`.pre-commit-config.yaml`, `.editorconfig`, lint and format config, `.vscode/`, `.idea/`, `.impeccable/`, GitHub issue templates and CODEOWNERS) print `no-tests (assets and tool config)`, and licence text under any vendored name (`roboto-LICENSE.txt`, `LICENSE-MIT`) is documentation, when no test names the file. Stylesheets, unknown dotfiles (`.rspec`, `.husky/` hooks), CI workflows, `.gstack/`, `.env*` and version pins still need a lane, and so does unmapped code.
- **Dart and Flutter lanes (#3285, @arlophoenix).** The import graph reads `import`/`export`/`part` directives and resolves `package:` URIs through every tracked `pubspec.yaml`. `lanes` runs each package's tests from its own root with `flutter test` or `dart test`, through fvm when the package pins it. flutter, dart, melos and very_good CI jobs are lanes, and a run step's test paths resolve against its `working-directory` and literal `cd`. A missing Dart SDK or a `.dart` file outside any package is `incomplete`, never a pass; device tests are not unit lanes.
- **`--base <sha>` gets a branch suggestion, not an impossible fetch (#3269, #3290, @szsunyuan).**
### Changed
- **`gstack-pregate init` drafts from the repo (#3268).** It prints the languages it found and the runner each language's tests get, maps each file the import graph cannot place to the tests of the nearest directory above it, and lists the rest as `TODO` (as it does past the 256-entry schema limit). It never writes `[]`, which would clear a file without a test.
- **Review fixes on the Dart port.** A `cd` inside a pipeline, an alternative, a background job, shell control flow, a command substitution or a redirected subshell now leaves a CI lane's test set unknown (it may run any test) instead of resolving later paths against the wrong directory, which could drop a platform obligation. Dart directives are read to their `;` whatever the line indentation, metadata before a directive and semicolons inside a URI are handled, and nested block comments are skipped, so an unindented conditional import or a long `show` list no longer loses its edges.
- **Gate integrity:** new free regressions in `test/pregate-non-js.test.ts` (Django, assets and licence, init, and the two review fixes, each with a negative control) and `test/pregate-dart.test.ts`. Each new positive case fails on the tree before this change. No existing test, threshold, trial count or case kind changed; `test/helpers/pregate-fixture.ts` only reports more about a failed git spawn. `test/pregate-dart.test.ts` and `test/pregate-non-js.test.ts` join the other pregate fixture files in `scripts/lib/windows-curation.ts`'s known-incompatible list: the Dart stand-ins are bash scripts on a colon-joined PATH, and the bin cases expect the Linux-host lanes verdict.
## [1.91.160.0] - 2026-10-19
**Dart and Flutter projects get the right reviewers and the right test command.** A Dart change matched no diff-scope category except its test file, so a Flutter widget PR ran only the testing pass and skipped design, performance and security. Contributed by @arlophoenix (#3284).
### Fixed
- **`gstack-diff-scope` counts `.dart` as both frontend and backend.** The extension cannot tell a Flutter widget from a repository class or a pure-Dart server, and a skipped reviewer is invisible, so both specialists run. `pubspec.lock` (at the root or in a workspace package) is config. `lib/frontend-scope.ts` mirrors the frontend half, and `gstack-design-detect` still never hands a `.dart` file to the impeccable engine, which reads web sources only.
- **Test detection knows Dart.** `/ship`'s bootstrap and coverage audit, `/design-review` and `/plan-eng-review` print `RUNTIME:dart` for a `pubspec.yaml` and `FRAMEWORK:flutter` for an indented `sdk: flutter` dependency (a commented-out one or an `environment.sdk` constraint does not count). They treat `dart_test.yaml` and `*_test.dart` files anywhere as existing tests and offer `flutter test` or `dart test`. Review-army reports `STACK: dart` and `TEST_FW: dart-test` or `flutter-test`.
- **Adversarial `DIFF_SIZE` sums `git diff --numstat`,** the same count review-army's `DIFF_LINES` uses, instead of parsing the `--stat` summary line. Binary files count as zero.
### Changed
- The test-bootstrap prose is shorter so the generated skills stay inside the parity size cap and `/design-review` stays at its SKILL.md size pin. The rules it states are unchanged. `/design-review`, `/plan-eng-review` and `/review` shrink, so their pins in `test/fixtures/skill-md-size-allowlist.json` go down (31 entries).
- `test/frontend-scope.test.ts`'s bash parity test gets a 30 s budget. It spawns git six times and the scope script once per sample, and it already ran past Bun's 5 s default on a 4-core machine before this change.
## [1.91.159.0] - 2026-10-19
**Security: the repository gstack runs in can no longer configure gstack's own programs.** Bun loads `.env`, `.env.local` and `.env.<NODE_ENV>` from the directory it starts in. It also runs any `preload` that a `bunfig.toml` there names, before the program's first line. gstack runs inside the repository it audits, so a committed or branch `.env` could set `GSTACK_STATE_ROOT`, `GSTACK_PROJECT_SLUG`, `GSTACK_CLAUDE_BIN` (an attacker-chosen binary then ran as "claude"), `GSTACK_SPEND_CAP_USD` or a provider base URL for every gstack bin, hook and skill step. A `bunfig.toml` preload ran arbitrary code in all of them. Every Bun that gstack starts now passes `--no-env-file` and an empty `--config`. Your shell environment works exactly as before. Found in review by GSTA-54.
### Security
- **Every bun entry point starts isolated:** all 123 bun shebangs are now `#!/usr/bin/env -S bun --no-env-file --config=/dev/null`. `bun run <file>` ignores the shebang, so the callers carry the flags too: skill templates, generated skill docs, resolvers, hook wrappers, `setup` and the bash bins pass `bun --no-env-file --config=/dev/null` for every `run`, `-e`, `-p` and script path.
- **TypeScript spawns use `bunIsolationArgs()`** from `lib/bun-isolation.ts`, which follows browse's `emptyBunConfig()`. This covers `scriptInvocation` (how TypeScript launches gstack's own bins), restamp, qa-deadline, the timeline hook's repair child, gbrain sync's ingest child, the agents-digest regen and the Claude Code migration. Native Windows has no `/dev/null`, so there the helper passes a zero-byte bunfig in a fixed directory under the per-user temp root. It is never removed, because the timeline hook's detached repair child reads it after the hook exits. Under Git Bash, MSYS rewrites the shebang's `/dev/null` to `nul`, which Bun 1.4.2 reads as an empty config. A windows-latest run verified both.
- **Compiled builds:** `build:gates` and `scripts/build-app.sh` now pass `--no-compile-autoload-dotenv --no-compile-autoload-bunfig`, as `scripts/build.sh` already did. Without them, compiled binaries load both files.
- **A ratchet and regressions:**
- `test/bun-entry-isolation.test.ts` fails on any bun shebang, shell or skill caller, TypeScript spawn or `build --compile` that drops the flags.
- `test/bun-dotenv-isolation.test.ts` runs the real bins in a fixture repo with a hostile `.env`, `.env.local` and `bunfig.toml`. The state root, slug, claude binary and pregate spend cap still come from the shell.
- `test/bun-isolation-launch.test.ts` runs the shebang, bash and TypeScript launch forms on every free lane, Windows included.
### Changed
- **`gstack-evidence run` no longer scrubs dotenv values from the child environment,** and `GSTACK_EVIDENCE_KEEP_DOTENV` is gone. Bun no longer loads those values into the wrapper, so the child sees your shell's environment, as it would if you ran the command by hand. The old value-equality scrub (#2652, @namtrok) also dropped shell-exported variables whose value matched the file.
- **Raised SKILL.md size pins:** 80 pins in `test/fixtures/skill-md-size-allowlist.json` grew by 33 to 330 characters each, from the isolation flags alone (8,253 characters in total).
- **Directly executed `./scripts/*.ts` files no longer read gstack's own `.env`.** `bun run <script>` and the package scripts (`bun run test`, `bun run eval:bg:pr`) still load it.
### If you…
- **…add a bin or a hook:** start it with the isolated shebang, and pass `bun --no-env-file --config=/dev/null` (or `bunIsolationArgs()` in TypeScript) wherever you start Bun. If you don't, the ratchet names the line.
- **…relied on a repo `.env` to configure gstack:** export the variable in your shell or set it with `gstack-config`.
## [1.91.158.0] - 2026-10-19
**`gstack-config set plan_mode_preamble off` leaves the plan-mode preamble out of every installed skill, for people who never use plan mode.** The "Plan Mode Safe Operations" and "Skill Invocation During Plan Mode" sections add about 1.6 KB to each of 50 Claude skills. With the key off, the next `./setup` renders every host's skills with `gen-skill-docs --plan-mode=off`, and the default Claude install serves that render instead of the committed files. The default is on, and with it on every render and the committed SKILL.md files are byte-for-byte unchanged.
### Added
- **`gen-skill-docs --plan-mode=off`** drops the two sections at generation time. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" keep their marker. Hosts whose capabilities declare no plan mode already render the sections empty, so in practice the flag matters for Claude, Copilot and OpenCode. Fixes #3168. From #3213 by @xingzihai, finished with the config wiring.
- **The `plan_mode_preamble` config key** (`on` | `off`, default `on`). It is a closed domain like `explain_level`: an unknown value warns and stores `on`. Setting it outside `./setup` says it applies on the next setup run and prints the `./setup --refresh-registered` command that applies it now.
- **Every installed render carries the choice:** all 16 `gen:skill-docs` call sites in `setup`, the Claude render helper, `gstack-config gbrain-refresh`, and the router that `gstack-relink` re-renders in place (as it already does for `disabled_skills`). When setup has to build, `scripts/build.sh` renders Factory, OpenCode, Cursor, Copilot and Antigravity without the flag, so with the key off setup renders those hosts again afterwards.
- **The default Claude install gets a render when the key is off.** Without a working gbrain (absent, or the detector failed), `gstack_claude_render_action` returns `plain` instead of serving the committed files; with gbrain the brain-aware render already runs and now carries the flag too. The activation record (`~/.gstack/render/claude.overlay`) gains `plan_mode=off`. Turning the key back on removes that render on the next setup when gbrain is absent or its detector fails (a failed detector otherwise keeps a render), and re-renders the brain-aware render with the sections when gbrain is present. A failed render with the key off keeps the previous render. Records written with the key on are unchanged.
### Fixed
- **`gstack-relink` refreshes the `_gstack-command` router alias after it re-renders the router.** It used to copy the router before the re-render, so the alias kept the previous router (with `disabled_skills`, the full skill list) until the next relink.
### Changed
- **Gate integrity:** new tests `test/plan-mode-off-build.test.ts` (from #3213, unchanged), `test/plan-mode-off-generation.test.ts`, `test/plan-mode-preamble-config.test.ts`, `test/plan-mode-preamble-render.test.ts` and `test/plan-mode-preamble-setup.test.ts`. No existing test was edited, and no threshold, trial count, case kind or judge changed.
## [1.91.157.0] - 2026-10-19
**Claude SKILL.md files no longer carry a "When to invoke" body section, which saves about 20 KB across 56 skills.** Claude Code reads a skill's body only after the skill is invoked, so routing prose there never routed anything. Routing still comes from the catalog description and the CLAUDE.md routing block, and voice triggers move into the root router's routing block. From #3129 by @thanhnguyentung95.
### Changed
- **Catalog trim (`scripts/gen-skill-docs.ts`):** the Claude render keeps the one-line catalog description and no longer inserts the routing and voice-trigger prose into the body. `--catalog-mode=full` and the other hosts are unchanged: they keep the full description.
- **Voice triggers in the root router:** a new `{{VOICE_TRIGGER_ROUTING}}` resolver (`scripts/resolvers/voice-triggers.ts`) adds one `- Voice trigger "…" → invoke /<skill>` line per skill to the root `SKILL.md` routing block, Claude only. That's 21 lines, and the root router's eager ceiling rises from 4289 to 4821 tokens.
- **Context budget:** 54 per-skill eager ceilings go down (net −5,139 tokens). The `claude/design-review` size pin drops by 437 chars, and the claude ship golden is refreshed.
- **Gate integrity:** the tests that pinned the removed section now assert its absence, or check the template description instead:
- `test/catalog-trim.test.ts` (the three `buildWhenToInvokeSection` tests go with the function);
- `test/catalog-mode-full.test.ts` and `test/cso-preserved.test.ts`;
- `test/gen-skill-docs.test.ts` (the benchmark routing and CSO voice tests read the template);
- `test/skill-validation.test.ts` (trigger and proactive phrases are checked in `SKILL.md.tmpl`);
- `test/helpers/parity-harness.ts` (no `## When to invoke` heading required);
- `test/helpers/skill-fixture.ts` and `test/skill-e2e-retro.test.ts` (the section is dropped from the review, review-army and retro E2E section lists). `scripts/harness-version.json` records the `skill-fixture.ts` change as non-behavioral: `HARNESS_VERSION` stays 6, and the regenerated SKILL.md bytes already start those cases' new series.
- `test/helpers/manual-judge-review-fixture.ts` puts the removed section back when it rebuilds the historical cookie-workflow approval, so the approved prompt hash and line range still match. The approval itself is unchanged.
One new free test checks that the root router routes every voice trigger, and it skips the `connect-chrome` alias symlink. No threshold, trial count, case kind or judge changed. No context-budget ceiling rose except the root router's.
## [1.91.156.0] - 2026-10-19
**The weekly marathon's office-hours case can run its review formatter again, and its validator now checks cap fixes the way the formatter does.** Both defects came from #3164 and were in the test, not the skill. The marathon fixture copied `bin/gstack-office-hours-review` with a hand-kept list of two library files; #3164 added a third import (`lib/spec-review-cap.ts`), so every formatter command in the fixture failed at module load, the agent correctly declared the review UNREVIEWED, and the case went red in marathon run 39032308051 (#3246). With the module restored, a focused rerun showed the second defect: when the review stops at the round cap and the agent applies the remaining blocking fixes with `--fixes`, the formatter records them as verified, but the test never gave the validator that list, so it expected "fixes unconfirmed" and rejected a correct report.
### Fixed
- **Office-hours cap fixes:** `observedSpecFixes()` in `test/helpers/office-hours-completion.ts` returns the last valid `--fixes` list the agent saved with Write, and the marathon test passes it to `validateOfficeHoursReviewArtifacts`. A free test in `test/office-hours-completion.test.ts` runs the real `finalize --fixes` at the cap and checks three cases: the observed list validates, no list fails with the marathon rerun's `Spec Review does not match` error, and a list naming a different finding also fails.
- **Office-hours marathon fixture:** `test/helpers/office-hours-formatter-fixture.ts` copies the formatter together with every local module it imports, so a new import can no longer be left out. A free test in `test/office-hours-review.test.ts` loads the copied formatter and fails on a missing module; the old three-file list reproduces the exact `Cannot find module '../lib/spec-review-cap.ts'` error. The case's touchfiles now include `lib/spec-review-cap.ts` and the new helper.
- **Gate integrity:** no threshold, trial count, case kind or judge changed. The marathon assertions are unchanged; only the fixture's file copy changed.
## [1.91.155.0] - 2026-10-19
**A busy page no longer locks up the browse daemon.** When one tab kept its main thread busy (a heavy dev bundle, a big grid render, a runaway script), every `browse` command, including `status`, `tabs`, `tab <id>`, `closetab` and `stop`, failed with "Daemon busy" after about 9.6 s, and the only way out was `--force-restart`, which threw away cookies, tabs and logins. Fixes #3247. From #3250 by @AndreIntelas.
### Fixed
- **`/health` `status` reports daemon liveness (Chromium connected)**, and the active tab's responsiveness is reported separately as `activeTab: {id, responsive}`. The CLI no longer gates every command on a 2 s `page.evaluate` race against the active tab.
- **`tabs` and `state save` bound their per-tab reads**, so a stuck tab shows as `(not responding)` instead of hanging the command, and one pending probe is shared per stuck tab rather than one per poll. A busy tab keeps its last good storage in saved state.
- **A command that really needs the stuck tab times out with an error naming that tab** and how to get out (switch tabs or close it). The CLI's timeout branch now matches Bun's `TimeoutError`; the old `AbortError` check never fired.
### Changed
- **Gate integrity:** `browse/test/terminal-agent.test.ts` adds `activeTab` to the exact `/health` keys and updates its stub for the now-synchronous `isHealthy` and the new `probeActiveTab`; its no-token assertions are unchanged. `browse/test/busy-active-tab.test.ts` and `browse/test/command-timeout-report.test.ts` are new. No threshold, trial count or case kind changed.
## [1.91.154.0] - 2026-10-19
**/cso can verify Rails repairs in a read-only checkout, its retained Git history no longer brings back credential files the snapshot leaves out, a repair whose external assertions fail is now reported as `ASSERTION_FAILED` instead of a misleading `INCOMPATIBLE_INPUT`, and a security assertion can accept several safe rejection statuses while still requiring the secret to be absent.** The helper keeps `.env`, `master.key`, keys and agent configuration out of the readable tree, but `history.txt` captured `git log -p` in full, so a committed `.env` came back through its diff whenever the value wasn't credential-shaped enough for the redactor. Paid qualification run 39186328569 caught this when the private corpus's planted `.env` value showed up in the judge's helper state.
### Fixed
- **Rails `verify` on read-only sources:** the helper writes its own test `config/database.yml` (plus an initializer and `storage.yml`) into the execution copy. The copy keeps the source's file modes, so in a read-only checkout (the eval producer seals every source file at 0444, and Nix or Bazel trees are the same) the write failed with `EACCES`. That surfaced as the generic `INVALID_SCHEMA` "unexpected or unsafe input", and no Rails repair could reach `runtime_tested`. Each helper file now replaces the copied file instead of opening it for writing, so it also never writes through a copied symlink.
- **`history.txt`:** every diff section that touches a path the snapshot excludes (`exclusion()`, the same rule the readable tree uses) is replaced with `diff withheld: <path> (<reason>)`. This covers either side of a rename and any diff header that can't be parsed. Commit preambles and other files' diffs stay.
- **`verify` with an authenticated witness:** a receipt or observation mismatch is still `INCOMPATIBLE_INPUT`. Receipts that bind correctly but record failed external assertions now raise `ASSERTION_FAILED`, with the phase and the booted, legitimate and security outcome. Nothing new is certified either way. In run 39186328569, three repairs that rejected the attack with a different status than the oracle expected surfaced as `INCOMPATIBLE_INPUT`.
### Changed
- **A security assertion's `expected.status` may list up to eight safe statuses** (for example `[403, 404]`), so a repair that refuses an attack with a different reasonable rejection still passes. A set is accepted only on a security assertion that also carries a `vulnerable` expectation, and only with `excludes` set, so the response must still withhold the vulnerable marker. The fixed and vulnerable oracles must stay provably mutually exclusive: a set that contains the vulnerable status is accepted only when the vulnerable response's `includes` contains the excluded marker. Legitimate controls and `vulnerable` expectations still take exactly one status, so a patch that answers every route with 403 still fails its controls. A no-op patch, a listed rejection that still leaks the marker, and an unlisted status all stay `inconclusive`.
## [1.91.153.0] - 2026-10-19
**An outside review whose findings are untagged prose no longer reads `clean` because a "no issues" phrase appears somewhere in it, and a clean Codex review that says "no actionable regressions" no longer reads `unverified`.** Reported in #3294 by @SchizasNik, with the false-pass half pointed out by @xg-gh-25. `lib/outside-review-result.ts` matched its no-findings vocabulary anywhere in the reply, so "The parser change has no new issues. However, … drops users_email_idx" passed /ship's structured gate. It also lacked `regressions`, so `codex review --uncommitted`'s "I found no actionable regressions in the staged, unstaged, or untracked changes." was missing coverage.
### Fixed
- **A no-findings statement counts only as the review's conclusion.** It must open the reply's last sentence or last ", and" clause (or the `because` clause of a review's `Recommendation:` line) and carry no contrast or exception ("but", "however", "except", "other than", "elsewhere"). When other prose comes first, anything after the statement other than a predicate ("were found", "is evident") or a colon explanation must scope it to the change as a whole, so "No issues in the parser." and "No regressions were found by the existing tests." do not conclude the review. Anything else stays `unverified` / `untagged_review`, the same reason code and troubleshooting anchor as before.
- **`regressions` and `defects` join the vocabulary**, and `NO_FINDINGS` written as inline code still counts.
### Gate integrity
- The structured and review gates' no-findings check (`classifyOutsideReview`) is stricter about position and wider in vocabulary. New free regressions in `test/outside-review-result.test.ts` cover the reporter's exact reply through their CLI invocation, two clean `codex review --uncommitted` replies captured from codex-cli 0.160.0 with gpt-6-sol (both read `unverified` before), fifteen mixed replies that must stay `unverified` under both gates (the two strings from the issue, the phrase before, after and inside untagged findings, exception conclusions, conclusions scoped to one place or to the tests, a quoted `NO_FINDINGS`, a no-issues line inside a code fence), and Recommendation-clause cases including a wrapped clause with "but". A GPT-6.1 Sol outside review probed the classifier; its false passes are among those controls. A known residual, unchanged from before: untagged findings followed by "No issues found in the changed parser." still read `clean`, because "changed" names the change and the parser cannot tell a finding from a summary without tags. All four new tests fail on the previous classifier. Every existing negative control passes unchanged, including the stored Codex clean review that still reads `unverified` ("no actionable correctness issues"). No threshold, trial count or case kind changed.
## [1.91.152.0] - 2026-10-19
**`/cso` now starts and finishes on ordinary repositories that hold a nested Git repository, a long patch history, or a Docker socket on macOS 27.** Three user reports (#3255, #3248) each stopped a run on a normal repo.
### Fixed
- **An untracked nested Git repository or a registered submodule no longer stops `gstack-cso start` with `UNSAFE_PATH`.** Git lists such a directory as one entry (`inner/` for an untracked repo, `sub` for a gitlink). The snapshot now records it as one explicit `excluded: nested Git repository` entry that is never read or copied, and the snapshot coverage lists it under exclusions. The capture pins the boundary's device, inode and mode, so a directory swapped in during capture fails as `SNAPSHOT_RACE`. A symlink to a directory is still refused. Ported from #3256 by Chao Chen (@surlymochan), who keeps authorship of the fix. (#3255, bug 1)
- **A patch history over 1 MiB no longer leaves `history-inputs` not assessed.** The helper reads up to 100 commits of patches, then keeps the newest whole commits that fit the 1 MiB redaction bound. It never cuts inside a commit, so a secret can't be split across the cut and escape redaction. When even the newest patch is too large, it keeps commit metadata only. A truncated `history.txt` starts with a `[gstack-cso history truncated: …]` line, `history-status.json` records `truncated` and the retained commit count, and the history coverage becomes `partial` with that gap. The redaction bound is unchanged. (#3255, bug 2)
### For contributors
- **`test/cso-ordinary-repos.test.ts`** covers each report end to end through the compiled launcher and `capture()`. Its negative controls show that a symlinked directory is still refused, that a nested repository swapped during capture fails as a race (this check fails if the device and inode comparison is removed), that retained and omitted history never shows a credential canary, and that Docker endpoint admission still refuses non-sockets, dangling and looping links, `..` paths and TCP. The #3248 socket fallback (`realpathSocket`) shipped in 1.91.96.0. The new endpoint-level case fails when admission calls plain `realpathSync`.
## [1.91.151.0] - 2026-10-19
**Three contributor fixes: a saved "never ask" now sticks for /plan-eng-review's structure question, /context-save and /context-restore close three checkpoint gaps, and a new how-to covers the first production deploy.** Ported from fork PRs so they can run the paid gate; each commit keeps its author.
### Fixed
- **/plan-eng-review's complexity-gate structure question has a registered id (`plan-eng-review-structure`).** Each run used to invent its own slug, so a `never-ask` saved through /plan-tune never matched the next run and the question came back every time. On `AUTO_DECIDE` the recommended arrangement is taken; feature cuts are still asked one by one. `gstack-question-preference --check` now also falls back to `$GSTACK_HOME/global-question-preferences.json` (the file the question-preference hook already reads) when the project file has no entry; a project entry still wins and the one-way-door and split-chain guards run first. From #3249 by @bezdekt.
- **/context-save keeps provenance markers as English literals and backs file claims in Decisions Made; /context-restore treats a translated or unknown marker as `(path assumed)` and asks when no branch can pick between recent checkpoints with different titles.** Fixes #3217. From #3244 by @surlymochan.
### Added
- **`docs/howto-bring-up-a-first-deploy.md`:** a vendor-neutral walkthrough for when /setup-deploy stops at "Nothing is deployed yet" (host, services, env and secrets, DNS, verification, and what to have ready when you rerun /setup-deploy). Linked from the README and docs/skills.md. Fixes #3150. From #3264 by @szsunyuan.
## [1.91.150.0] - 2026-10-19
**The paid evals that still pinned older models now run on the current generation, and two cases that went red on the move are repaired at their cause.** Garry's direction (2026-10-18) is to stop testing older frontier models, so these files are re-baselined as a fresh measured reference rather than held to a threshold change.
### Changed
- **Old-model pins:** the `claude-opus-4-7` pins in `skill-e2e-plan`, `skill-e2e-plan-prosons`, `skill-e2e-design`, `skill-e2e-office-hours-phase4` and `skill-e2e-qa-bugs` now follow `resolveEvalModel('capture')` (Opus 5.5). The overlay benchmark fixtures pin `claude-opus-5-5` and `claude-sonnet-5-5`, the cookie workflow judge pins `claude-sonnet-5-5`, and `codex-sol-scope-termination` runs on `gpt-6.1-sol` while still rendering the `gpt-5.6-sol` overlay.
- **/setup-browser-cookies:** the workflow is now five ordered steps. It checks for Aside first and ends there on `READY`, since no import is needed. It then asks every decision (source and profile, domains and target, verification, storage reset, all cookies) before any `$B` command, prepares gstack's browser (verification variables before the daemon starts, the fallback, the `Mode: cdp` stop), imports, and reports on three lines: Imported, Storage reset, Sign-in. The `{{BROWSE_SETUP}}` block, which contradicted the Aside probe, is gone from this skill.
- **Gate integrity:** no threshold, trial count, case kind or assertion changed. Free-test edits follow the new cookie template: two phrase and order assertions, the approval fixture's reverse edits, and the positional-literals checksum list, which no longer includes this skill.
### Fixed
- **plan-review-prosons-hardstop-neg:** on Opus 5.5 one trial in three stopped with a provider refusal (`stop_reason: refusal`, category `reasoning_extraction`) on the turn that writes the captured question. Replays of the refused request were stopped 7 of 16 times with the capture prompt's "verbatim" and "Do NOT paraphrase. This is a format-capture test." wording and 0 of 24 without it. The shared prosons capture prompt now asks for "the text of the single AskUserQuestion you would have made", with the same required parts. The decision content and every assertion are unchanged; all four prosons cases pass 3/3.
- **Cookie workflow judge:** on `claude-sonnet-5-5` it scored clarity 3, completeness 4, actionability 3 against the old template. The rewritten template scores 4/4/4 with unchanged thresholds.
## [1.91.149.0] - 2026-10-19
+3
View File
@@ -656,6 +656,7 @@ Other references: [docs/gbrain-sync.md](docs/gbrain-sync.md) (sync-specific guid
|-----|---------------|
| [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes Greptile integration) |
| [Diagrams & Document Formats](docs/howto-diagrams-and-formats.md) | Mermaid/excalidraw fences in PDFs, image sizing and safety defaults, `--to html\|docx`, `/diagram` triplets |
| [First Production Deploy](docs/howto-bring-up-a-first-deploy.md) | What to do when `/setup-deploy` says nothing is deployed yet: hosting, env and secrets, DNS, checking it works, then back to `/setup-deploy` |
| [Builder Ethos](ETHOS.md) | Builder philosophy: Boil the Ocean, Search Before Building, three layers of knowledge |
| [Using GBrain with GStack](USING_GBRAIN_WITH_GSTACK.md) | Every path, flag, bin helper, and troubleshooting step for `/setup-gbrain` |
| [GBrain Sync](docs/gbrain-sync.md) | Cross-machine memory setup, privacy modes, troubleshooting |
@@ -796,6 +797,8 @@ types into that element; bare `browse type <text>` types into whatever has focus
**Don't use some skills?** `gstack-config set disabled_skills make-pdf,pair-agent` stops registering them on every host, so their descriptions stop loading into each session. Claude Code updates right away; other hosts on their next `./setup --host <name>` or `/gstack-upgrade`. Their files stay installed, so a skill that calls a disabled one still works (you get a warning). Typos are rejected with the closest skill name, `gstack-upgrade` can't be disabled, and `gstack-config set disabled_skills ""` re-enables everything.
**Never use plan mode?** `gstack-config set plan_mode_preamble off` leaves the "Plan Mode Safe Operations" and "Skill Invocation During Plan Mode" sections (about 1.6 KB per skill) out of every installed skill on the next `./setup` run; the command prints the exact `./setup --refresh-registered` line that applies it. Claude Code then serves a render under `~/.gstack/render/claude` instead of the committed files. Commands marked "PLAN MODE EXCEPTION — ALWAYS RUN" keep their marker. `gstack-config set plan_mode_preamble on` (the default) and another `./setup` bring the sections back. It matters for Claude Code, Copilot and OpenCode: hosts without plan mode (Codex and others) never carry these sections.
**Want namespaced commands?** `cd ~/.claude/skills/gstack && ./setup --prefix` — switches from `/qa` to `/gstack-qa`. Useful if you run other skill packs alongside gstack.
**Codex says "Skipped loading skill(s) due to invalid SKILL.md"?** Your Codex skill descriptions are stale. `${CODEX_HOME:-~/.codex}/skills/gstack` is a runtime directory, not the checkout: `./setup --status` shows the Codex row's source checkout. Fix: `cd <that source> && git pull && ./setup --host codex` — for a repo-local install, run it from inside the project.
+21 -8
View File
@@ -16,14 +16,6 @@ triggers:
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
## When to invoke this skill
Sends any gstack request to the right skill
(planning, review, QA, shipping, debugging, docs, security, design). Routes QA by
intent and browser interaction to /browse. Use when you invoke gstack without a specific
skill, or ask "which gstack skill fits this?".
## Preamble (run first)
```bash
@@ -230,6 +222,27 @@ Generic “second opinion”, “outside review”, or “cross-model review”
- User asks what gstack has learned, "show learnings" → invoke `/learn`
- User asks to tune question sensitivity, "stop asking me that" → invoke `/plan-tune`
- User asks for code quality dashboard, "health check" → invoke `/health`
- Voice trigger "auto plan", "automatic review" → invoke `/autoplan`
- Voice trigger "speed test", "check performance" → invoke `/benchmark`
- Voice trigger "compare models", "model shootout", "which model is best" → invoke `/benchmark-models`
- Voice trigger "open the browser", "look at this page" → invoke `/browse`
- Voice trigger "code x", "code ex", "get another opinion" → invoke `/codex`
- Voice trigger "see-so", "see so", "security review", "security check", "vulnerability scan", "run security" → invoke `/cso`
- Voice trigger "build the design", "code the mockup", "make it real" → invoke `/design-html`
- Voice trigger "dx audit", "test the developer experience", "try the onboarding", "developer experience test" → invoke `/devex-review`
- Voice trigger "upgrade the tools", "update the tools", "gee stack upgrade", "g stack upgrade" → invoke `/gstack-upgrade`
- Voice trigger "clean the iOS debug bridge", "remove DebugBridge", "strip the gstack iOS instrumentation" → invoke `/ios-clean`
- Voice trigger "review the iOS design", "audit the iPhone app's visuals", "design QA the iPhone app" → invoke `/ios-design-review`
- Voice trigger "fix the iOS bug", "patch the iPhone app", "auto-fix the iOS issue" → invoke `/ios-fix`
- Voice trigger "iOS quality check", "test the iPhone app", "test the iPad app", "run iOS QA" → invoke `/ios-qa`
- Voice trigger "resync the iOS debug bridge", "regenerate iOS accessors", "update the gstack iOS instrumentation" → invoke `/ios-sync`
- Voice trigger "make this a pdf", "make it a pdf", "export to pdf", "turn this into a pdf", "turn this markdown into a pdf", "generate a pdf", "make a pdf from", "pdf this markdown" → invoke `/make-pdf`
- Voice trigger "show me the browser" → invoke `/open-gstack-browser`
- Voice trigger "pair agent", "connect agent", "share my browser", "remote browser access" → invoke `/pair-agent`
- Voice trigger "dx review", "developer experience review", "devex review", "devex audit", "API design review", "onboarding review" → invoke `/plan-devex-review`
- Voice trigger "tech review", "technical review", "plan engineering review" → invoke `/plan-eng-review`
- Voice trigger "quality check", "test the app", "run QA" → invoke `/qa`
- Voice trigger "bug report", "just check for bugs" → invoke `/qa-only`
**Route only to skills in your available-skills list.** A rule above whose skill is
not listed there is turned off on this install; skip it, never try to invoke it.
+1 -1
View File
@@ -97,7 +97,7 @@ quality gates that produce better results than answering inline.
- User asks to plan, preregister, price or pilot a paid eval → invoke `/eval-plan`
- User asks what gstack has learned, "show learnings" → invoke `/learn`
- User asks to tune question sensitivity, "stop asking me that" → invoke `/plan-tune`
- User asks for code quality dashboard, "health check" → invoke `/health`
- User asks for code quality dashboard, "health check" → invoke `/health`{{VOICE_TRIGGER_ROUTING}}
**Route only to skills in your available-skills list.** A rule above whose skill is
not listed there is turned off on this install; skip it, never try to invoke it.
+16 -11
View File
@@ -1645,19 +1645,24 @@ and `test/dx-selected-navigation-ap.test.ts`. One shared table run once against
only after `engFirstReviewAUQ` checks native completion once at entry; today each branch gates it
separately, so the change alters a paid verdict and needs its own paid run.
### P3: Re-pin the five remaining claude-opus-4-7 paid files
### P3: Follow-ups from re-baselining the old-model paid pins on the current generation
**What:** The 2026-09 audit moved seven paid evals to the default capture model (`resolveEvalModel('capture')`).
`skill-e2e-design`, `skill-e2e-office-hours-phase4`, `skill-e2e-plan-prosons` and `skill-e2e-plan` keep
`claude-opus-4-7` because six cases failed on the default model in one run (plan-design-review-plan-mode timeout,
office-hours-phase4-fork format, plan-review-prosons-neutral-neg missing output, plan-ceo-review-selective and
plan-eng-review 600 s timeouts, plan-ceo-review-expansion-energy posture score 3). plan-review-report, the one PR-gate
case in `skill-e2e-plan`, left the pin on 2026-10-18 and follows the capture default. `skill-e2e-qa-bugs` returned
to `claude-opus-4-7` after `qa-b6-static` timed out on the default model in two of three runs (census 36597762183
and a targeted local rerun): each time the stream stopped mid-message, with no pending tool, right after the model
found the disabled submit button, and emitted nothing until the 300 s case deadline. They measure an old model.
**What:** GSTA-49 TM3 (2026-10-18) moved every remaining old-model paid pin to the current generation: the
`claude-opus-4-7` pins in `skill-e2e-{plan,plan-prosons,design,office-hours-phase4,qa-bugs}` follow
`resolveEvalModel('capture')`, the overlay fixtures pin `claude-opus-5-5` and `claude-sonnet-5-5`, the cookie workflow
judge pins `claude-sonnet-5-5`, and `codex-sol-scope-termination` runs on `gpt-6.1-sol` (still rendering the
`gpt-5.6-sol` overlay). Three things remain:
**Re-entry:** fix the prompt, budget or rubric so each case passes on the default model in one run, then drop the pin.
- The base `claude-dedicated-tools-vs-bash` fixture now duplicates `claude-dedicated-tools-vs-bash-opus-5-5`.
- The `opus-4-7-*` fixtures measure `model-overlays/opus-4-7.md` on Opus 5.5, which gets the generic `claude`
overlay in product.
- Opus 5.5 refuses (stop category `reasoning_extraction`) capture prompts that ask for the "verbatim text" of a
question "you would have made" with "Do NOT paraphrase"; that wording stopped 7 of 16 replays of a prosons
request and 0 of 24 without it. `skill-e2e-plan-prosons` dropped it; `skill-e2e-plan-format` and
`skill-e2e-office-hours-phase4` still use it.
**Re-entry:** retire one dedicated-tools fixture, decide the opus-4-7 overlay's future, and drop the extraction
wording from the remaining capture prompts with a paid run each.
### P3: Retire the unused CEO payment seeder
+1 -1
View File
@@ -1 +1 @@
1.91.149.0
1.91.161.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.149.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.161.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+1 -14
View File
@@ -32,19 +32,6 @@ hooks:
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
## When to invoke this skill
Surfaces
taste decisions (close approaches, borderline scope, outside-review disagreements) at a final
approval gate. One command, fully reviewed plan out.
Use when asked to "auto review", "autoplan", "run all reviews", "review this plan
automatically", or "make the decisions for me".
Proactively suggest when the user has a plan file and wants to run the full review
gauntlet without answering 15-30 intermediate questions.
Voice triggers (speech-to-text aliases): "auto plan", "automatic review".
## Preamble (run first)
```bash
@@ -676,7 +663,7 @@ Send phase announcements and the final approval request (the brief) in the conve
Resolve SNAPSHOT_TOOL once:
```bash
bun -e 'console.log(require("fs").realpathSync(process.argv[1]))' "$HOME/.claude/skills/gstack/bin/gstack-autoplan-snapshot.ts"
bun --no-env-file --config=/dev/null -e 'console.log(require("fs").realpathSync(process.argv[1]))' "$HOME/.claude/skills/gstack/bin/gstack-autoplan-snapshot.ts"
```
Fresh RESTORE_PATH, beside its phase artifacts in the git-excluded `.gstack/tmp/autoplan/`:
+1 -1
View File
@@ -9,7 +9,7 @@ _autoplan_backstop() {
}
trap _autoplan_backstop EXIT
_AUTOPLAN_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
_AUTOPLAN_RESULT="$(bun "$_AUTOPLAN_HOOK_DIR/phase-publication-hook.ts")"
_AUTOPLAN_RESULT="$(bun --no-env-file --config=/dev/null "$_AUTOPLAN_HOOK_DIR/phase-publication-hook.ts")"
test -n "$_AUTOPLAN_RESULT"
printf '%s\n' "$_AUTOPLAN_RESULT"
_AUTOPLAN_DECIDED=1
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/** A native parent publication barrier at Autoplan's exact Read boundaries. */
import * as fs from 'node:fs';
import * as path from 'node:path';
+1 -1
View File
@@ -126,7 +126,7 @@ if [ "$_OUTSIDE_EXIT" -eq 124 ]; then
fi
cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; }
_OUTSIDE_RC=0
bun "$HOME/.claude/skills/gstack/lib/outside-review-result.ts" --label 'Codex outside review' --exit "$_OUTSIDE_EXIT" --stderr "$_OUTSIDE_TMP/stderr" --events "$_OUTSIDE_TMP/events" review "$_OUTSIDE_TMP/text" || _OUTSIDE_RC=$?
bun --no-env-file --config=/dev/null "$HOME/.claude/skills/gstack/lib/outside-review-result.ts" --label 'Codex outside review' --exit "$_OUTSIDE_EXIT" --stderr "$_OUTSIDE_TMP/stderr" --events "$_OUTSIDE_TMP/events" review "$_OUTSIDE_TMP/text" || _OUTSIDE_RC=$?
case "$_OUTSIDE_RC" in
0|3) ;;
4) echo 'OUTSIDE_STATUS: unverified provider=codex host=claude'; exit 4 ;;
+1 -1
View File
@@ -85,7 +85,7 @@ if [ "$_OUTSIDE_EXIT" -eq 124 ]; then
fi
cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; }
_OUTSIDE_RC=0
bun "$HOME/.claude/skills/gstack/lib/outside-review-result.ts" --label 'Codex outside review' --exit "$_OUTSIDE_EXIT" --stderr "$_OUTSIDE_TMP/stderr" --events "$_OUTSIDE_TMP/events" review "$_OUTSIDE_TMP/text" || _OUTSIDE_RC=$?
bun --no-env-file --config=/dev/null "$HOME/.claude/skills/gstack/lib/outside-review-result.ts" --label 'Codex outside review' --exit "$_OUTSIDE_EXIT" --stderr "$_OUTSIDE_TMP/stderr" --events "$_OUTSIDE_TMP/events" review "$_OUTSIDE_TMP/text" || _OUTSIDE_RC=$?
case "$_OUTSIDE_RC" in
0|3) ;;
4) echo 'OUTSIDE_STATUS: unverified provider=codex host=claude'; exit 4 ;;
+1 -1
View File
@@ -89,7 +89,7 @@ if [ "$_OUTSIDE_EXIT" -eq 124 ]; then
fi
cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; }
_OUTSIDE_RC=0
bun "$HOME/.claude/skills/gstack/lib/outside-review-result.ts" --label 'Codex outside review' --exit "$_OUTSIDE_EXIT" --stderr "$_OUTSIDE_TMP/stderr" --events "$_OUTSIDE_TMP/events" review "$_OUTSIDE_TMP/text" || _OUTSIDE_RC=$?
bun --no-env-file --config=/dev/null "$HOME/.claude/skills/gstack/lib/outside-review-result.ts" --label 'Codex outside review' --exit "$_OUTSIDE_EXIT" --stderr "$_OUTSIDE_TMP/stderr" --events "$_OUTSIDE_TMP/events" review "$_OUTSIDE_TMP/text" || _OUTSIDE_RC=$?
case "$_OUTSIDE_RC" in
0|3) ;;
4) echo 'OUTSIDE_STATUS: unverified provider=codex host=claude'; exit 4 ;;
+1 -1
View File
@@ -78,7 +78,7 @@ if [ "$_OUTSIDE_EXIT" -eq 124 ]; then
fi
cat "$_OUTSIDE_TMP/stderr" >&2 || { [ "$_OUTSIDE_EXIT" -ne 0 ] || _OUTSIDE_EXIT=1; }
_OUTSIDE_RC=0
bun "$HOME/.claude/skills/gstack/lib/outside-review-result.ts" --label 'Codex outside review' --exit "$_OUTSIDE_EXIT" --stderr "$_OUTSIDE_TMP/stderr" --events "$_OUTSIDE_TMP/events" review "$_OUTSIDE_TMP/text" || _OUTSIDE_RC=$?
bun --no-env-file --config=/dev/null "$HOME/.claude/skills/gstack/lib/outside-review-result.ts" --label 'Codex outside review' --exit "$_OUTSIDE_EXIT" --stderr "$_OUTSIDE_TMP/stderr" --events "$_OUTSIDE_TMP/events" review "$_OUTSIDE_TMP/text" || _OUTSIDE_RC=$?
case "$_OUTSIDE_RC" in
0|3) ;;
4) echo 'OUTSIDE_STATUS: unverified provider=codex host=claude'; exit 4 ;;
-12
View File
@@ -16,18 +16,6 @@ allowed-tools:
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
## When to invoke this skill
Runs the same prompt through Claude,
GPT (via Codex CLI), and Gemini side-by-side — compares latency, tokens, cost,
and optionally quality via LLM judge. Answers "which model is actually best
for this skill?" with data instead of vibes. Separate from /benchmark, which
measures web page performance. Use when: "benchmark models", "compare models",
"which model is best for X", "cross-model comparison", "model shootout".
Voice triggers (speech-to-text aliases): "compare models", "model shootout", "which model is best".
## Preamble (run first)
```bash
-12
View File
@@ -17,18 +17,6 @@ allowed-tools:
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
## When to invoke this skill
Establishes
baselines for page load times, Core Web Vitals, and resource sizes.
Compares before/after on every PR. Tracks performance trends over time.
Use when: "web performance benchmark", "page speed", "lighthouse", "web vitals",
"bundle size", "load time". Not for AI model, agent eval or business
benchmarks; use /benchmark-models for cross-model skill comparisons.
Voice triggers (speech-to-text aliases): "speed test", "check performance".
## Preamble (run first)
```bash
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-approval-classes — changes that need the owner's approval (field list 2, W2).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-artifact — validate, print and acknowledge the unattended artifact contract.
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-autoplan — the unattended /autoplan runner as an explicit, durable state machine (plan B2).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/** Autoplan's blind reviewer inputs contain only the current implementation plan. */
import { createHash } from 'node:crypto';
import { linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-autoplan-timing — wall time per autoplan phase and the pre-run estimate (plan B10).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-banned-terms — terms a repository forbids outside named locations (plan E3).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-brain-cache — three-tier cache for brain-aware planning skills.
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-brain-context-load — V1 retrieval surface (Lane C).
*
+1 -1
View File
@@ -576,7 +576,7 @@ PYEOF
CONSENT_TS="$SCRIPT_DIR/../lib/transcript-consent.ts"
transcript_policy() {
command -v bun >/dev/null 2>&1 || return 2
GSTACK_STATE_ROOT="$GSTACK_HOME" bun run "$CONSENT_TS" "$@"
GSTACK_STATE_ROOT="$GSTACK_HOME" bun --no-env-file --config=/dev/null run "$CONSENT_TS" "$@"
}
# 0 when staged transcript pages may sync at all.
+1 -1
View File
@@ -58,7 +58,7 @@ probe() {
else PROBE_OUT=$(cd "$ROOT" && node -e "$PROBE" </dev/null 2>&1); PROBE_RC=$?; fi
elif command -v bun >/dev/null 2>&1; then
ENGINE=bun
PROBE_OUT=$(cd "$ROOT" && bun -e "$PROBE" </dev/null 2>&1); PROBE_RC=$?
PROBE_OUT=$(cd "$ROOT" && bun --no-env-file --config=/dev/null -e "$PROBE" </dev/null 2>&1); PROBE_RC=$?
else
fail "neither node nor bun is on PATH to launch Chromium" "install Node.js (https://nodejs.org) or Bun, then re-run $0"
fi
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
import { ciGateMain } from '../lib/ci-gate';
process.exit(await ciGateMain(process.argv.slice(2)));
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
// Claude-only outside-review runner. Prompts arrive on stdin, never argv.
// codex_reviews gates the send (lib/outside-consent.ts); --user-invoked marks
// a review the user asked for by name (/claude-code) and runs regardless.
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-code-intelligence — pick a code-intelligence provider and use it to
* index and search this repo. OPTIONAL: with nothing selected, gstack works
+1 -1
View File
@@ -168,7 +168,7 @@ _gstack_codex_select_model() {
[ -n "$_role" ] && set -- "$@" --role "$_role"
[ -n "$_explicit" ] && set -- "$@" --explicit "$_explicit"
[ -n "$_cwd" ] && set -- "$@" --cwd "$_cwd"
_line=$(bun run "$_GSTACK_CODEX_BIN/../scripts/resolve-codex-generation-model.ts" "$@")
_line=$(bun --no-env-file --config=/dev/null run "$_GSTACK_CODEX_BIN/../scripts/resolve-codex-generation-model.ts" "$@")
_rc=$?
_GSTACK_CODEX_SEL="${_line%%$'\t'*}"
_rest="${_line#*$'\t'}"
+1 -1
View File
@@ -101,7 +101,7 @@ IMPORTED=0
SKIPPED_NO_ANSWER=0
for SESSION_FILE in "${SESSION_FILES[@]}"; do
COUNT_LINE=$(SESSION_FILE_PATH="$SESSION_FILE" QLOG_BIN="$SCRIPT_DIR/gstack-question-log" bun -e '
COUNT_LINE=$(SESSION_FILE_PATH="$SESSION_FILE" QLOG_BIN="$SCRIPT_DIR/gstack-question-log" bun --no-env-file --config=/dev/null -e '
const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
+23 -4
View File
@@ -94,6 +94,13 @@ CONFIG_HEADER='# gstack configuration — edit freely, changes take effect on ne
# # (for power users who know the terms)
# # Unknown values default to "default" with a warning.
# # See docs/designs/PLAN_TUNING_V1.md for rationale.
# plan_mode_preamble: on # on = every skill carries the plan-mode preamble
# # off = rendered skills omit the "Plan Mode Safe
# # Operations" and "Skill Invocation During
# # Plan Mode" sections (~1.8 KB each), for
# # users who never enter plan mode
# # Applies on the next ./setup run. Unknown values
# # default to "on" with a warning.
#
# ─── Artifacts sync (renamed from gbrain_sync_mode in v1.27.0.0) ─────
# artifacts_sync_mode: off # off | artifacts-only | full
@@ -279,6 +286,7 @@ lookup_default() {
update_check) echo "true" ;;
skill_prefix) echo "false" ;;
explain_level) echo "default" ;;
plan_mode_preamble) echo "on" ;; # on | off — off drops the plan-mode preamble on the next ./setup (#3168)
codex_reviews) echo "enabled" ;;
design_detector) echo "auto" ;; # auto | off — impeccable engine pre-pass in the design skills
design_detector_install_prompted) echo "false" ;; # true once the user answered the one-time engine install offer with "never ask again"
@@ -734,6 +742,10 @@ $_gstack_legacy_root"
echo "Warning: explain_level '$VALUE' not recognized. Valid values: default, terse. Using default." >&2
VALUE="default"
fi
if [ "$KEY" = "plan_mode_preamble" ] && [ "$VALUE" != "on" ] && [ "$VALUE" != "off" ]; then
echo "Warning: plan_mode_preamble '$VALUE' not recognized. Valid values: on, off. Using on." >&2
VALUE="on"
fi
if [ "$KEY" = "artifacts_sync_mode" ] && [ "$VALUE" != "off" ] && [ "$VALUE" != "artifacts-only" ] && [ "$VALUE" != "full" ]; then
echo "Warning: artifacts_sync_mode '$VALUE' not recognized. Valid values: off, artifacts-only, full. Using off." >&2
VALUE="off"
@@ -985,6 +997,11 @@ $_gstack_legacy_root"
gstack_install_registry_rows | awk -F '\t' '$1 != "claude" { print " apply to " $1 " (" $4 "): cd " $6 " && ./setup --host " $1 }' )
fi
fi
# Installed skills are rendered by ./setup, so the preamble choice lands
# on its next run (every registered host).
if [ "$KEY" = "plan_mode_preamble" ] && [ -z "${GSTACK_SETUP_RUNNING:-}" ]; then
printf 'gstack-config: plan_mode_preamble=%s applies on the next ./setup run. Apply now: cd %q && ./setup --refresh-registered\n' "$VALUE" "$(cd "$(dirname "$0")/.." && pwd)"
fi
;;
unset)
KEY="${2:?Usage: gstack-config unset <key>}"
@@ -1009,7 +1026,7 @@ $_gstack_legacy_root"
echo "# ─── Active values (including defaults for unset keys) ───"
report_root_disagreement
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix explain_level \
skill_prefix explain_level plan_mode_preamble \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted artifacts_sync_removals plan_tune_hooks \
timeline_stop_hook design_detector design_detector_install_prompted memorable_recall \
@@ -1034,7 +1051,7 @@ $_gstack_legacy_root"
defaults)
echo "# gstack-config defaults"
for KEY in proactive routing_declined telemetry auto_upgrade update_check \
skill_prefix explain_level \
skill_prefix explain_level plan_mode_preamble \
codex_reviews gstack_contributor skip_eng_review workspace_root \
artifacts_sync_mode artifacts_sync_mode_prompted artifacts_sync_removals plan_tune_hooks \
timeline_stop_hook design_detector design_detector_install_prompted memorable_recall \
@@ -1065,6 +1082,8 @@ $_gstack_legacy_root"
# generated SKILL.md files match your installation state.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/gstack-render-claude.sh" 2>/dev/null || { echo "$0: $SCRIPT_DIR/gstack-render-claude.sh is missing. fix: reinstall with ./setup or /gstack-upgrade" >&2; exit 1; }
_PLAN_MODE_ARG=""
[ "$("$0" get plan_mode_preamble 2>/dev/null || true)" != off ] || _PLAN_MODE_ARG="--plan-mode=off"
DETECT_BIN="$SCRIPT_DIR/gstack-gbrain-detect"
DETECTION_FILE="$STATE_DIR/gbrain-detection.json"
mkdir -p "$STATE_DIR"
@@ -1133,7 +1152,7 @@ $_gstack_legacy_root"
gstack_claude_overlay "$INSTALL_DIR" "$0"
RENDER_TMP="$RENDER_DIR.tmp.$$"
rm -rf "$RENDER_TMP"
if ( cd "$INSTALL_DIR" && bun run gen:skill-docs:user --host claude --out-dir "$RENDER_TMP" --link-root "$RENDER_DIR" --model "$_GSTACK_OVERLAY" >/dev/null 2>&1 ); then
if ( cd "$INSTALL_DIR" && bun run gen:skill-docs:user --host claude --out-dir "$RENDER_TMP" --link-root "$RENDER_DIR" --model "$_GSTACK_OVERLAY" ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} >/dev/null 2>&1 ); then
gstack_render_lock "$RENDER_DIR"
_swap_in_render "$RENDER_DIR" "$RENDER_TMP"
gstack_render_unlock "$RENDER_DIR"
@@ -1159,7 +1178,7 @@ $_gstack_legacy_root"
# brain blocks, so removing gbrain keeps the chosen overlay.
INSTALL_DIR="$HOME/.claude/skills/gstack"
RENDER_DIR="${GSTACK_USER_RENDER_DIR:-$STATE_DIR/render/claude}"
if { [ -d "$RENDER_DIR" ] || "$0" has claude_overlay_model; } \
if { [ -d "$RENDER_DIR" ] || "$0" has claude_overlay_model || [ -n "$_PLAN_MODE_ARG" ]; } \
&& [ -d "$INSTALL_DIR" ] && [ ! -L "$INSTALL_DIR" ] && [ -f "$INSTALL_DIR/VERSION" ] && [ -f "$INSTALL_DIR/package.json" ]; then
if ! command -v bun >/dev/null 2>&1; then
echo "Skip: bun not on PATH — the Claude render at $RENDER_DIR was not refreshed. Install bun, then re-run 'gstack-config gbrain-refresh'."
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
// gstack-context-bill — token bill-of-materials for an installed skills tree.
// All behavior lives in lib/context-bill.ts; this is the CLI shim.
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-contributor-mode — contributor-PR mode detection for /review (plan E3, decision D10).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-decision-log — append a durable decision (or supersede/redact/compact it).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-decision-search — read active decisions (the curated "what did we decide" view).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
// gstack-design-approved — print the absolute path of the image an
// approved.json names.
//
+3 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-design-detect — find, and run, an impeccable engine the USER installed.
*
@@ -829,7 +829,8 @@ function resolveTargets(args: ScanArgs, p: Probe): { targets: string[]; refusedB
runZ(['diff', '-z', '--name-only', '--diff-filter=ACMR', 'HEAD']);
runZ(['ls-files', '-z', '--others', '--exclude-standard']);
for (const rel of [...files].sort()) {
if (!isFrontendPath(rel)) continue;
// impeccable reads web sources; a Dart file is frontend scope but not a scan target.
if (!isFrontendPath(rel) || rel.endsWith('.dart')) continue;
const abs = path.join(top, rel);
try { if (fs.lstatSync(abs).isSymbolicLink()) { refuse(rel, 'symlink named by git is never scanned'); continue; } } catch { continue; }
const real = realpathOrNull(abs);
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-design-md — inspect, convert, and read DESIGN.md in the open format.
*
+11 -11
View File
@@ -70,7 +70,7 @@ do_migrate() {
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
trap 'rm -f "$TMPOUT"' EXIT
cat "$LEGACY_FILE" | bun -e "
cat "$LEGACY_FILE" | bun --no-env-file --config=/dev/null -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const sessions = [];
const signalsAcc = {};
@@ -127,7 +127,7 @@ do_migrate() {
mv "$LEGACY_FILE" "$LEGACY_FILE.migrated-$TS"
local COUNT
COUNT=$(PROFILE_FILE_PATH="$PROFILE_FILE" bun -e "console.log(JSON.parse(require('fs').readFileSync(process.env.PROFILE_FILE_PATH,'utf-8')).sessions.length)" 2>/dev/null || echo "?")
COUNT=$(PROFILE_FILE_PATH="$PROFILE_FILE" bun --no-env-file --config=/dev/null -e "console.log(JSON.parse(require('fs').readFileSync(process.env.PROFILE_FILE_PATH,'utf-8')).sessions.length)" 2>/dev/null || echo "?")
echo "MIGRATE: ok — migrated $COUNT sessions from builder-profile.jsonl"
}
@@ -184,7 +184,7 @@ do_log_session() {
fi
# Validate: input must be parseable JSON with required fields (date, mode).
if ! printf '%s' "$INPUT" | bun -e "
if ! printf '%s' "$INPUT" | bun --no-env-file --config=/dev/null -e "
const j = JSON.parse(await Bun.stdin.text());
if (!j.date || !j.mode) process.exit(1);
" 2>/dev/null; then
@@ -197,7 +197,7 @@ do_log_session() {
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
trap 'rm -f "$TMPOUT"' EXIT
PROFILE_FILE_PATH="$PROFILE_FILE" RECORD_INPUT="$INPUT" TMPOUT_PATH="$TMPOUT" bun -e "
PROFILE_FILE_PATH="$PROFILE_FILE" RECORD_INPUT="$INPUT" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config=/dev/null -e "
const fs = require('fs');
const entry = JSON.parse(process.env.RECORD_INPUT);
if (!entry.ts) entry.ts = new Date().toISOString();
@@ -244,7 +244,7 @@ do_reconcile() {
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
trap 'rm -f "$TMPOUT"' EXIT
PROFILE_FILE_PATH="$PROFILE_FILE" PROJECTS_DIR="$GSTACK_HOME/projects" TMPOUT_PATH="$TMPOUT" bun -e "
PROFILE_FILE_PATH="$PROFILE_FILE" PROJECTS_DIR="$GSTACK_HOME/projects" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config=/dev/null -e "
const fs = require('fs');
const path = require('path');
const profile = JSON.parse(fs.readFileSync(process.env.PROFILE_FILE_PATH, 'utf-8'));
@@ -314,7 +314,7 @@ do_reconcile() {
# -----------------------------------------------------------------------
do_read() {
ensure_profile
cat "$PROFILE_FILE" | bun -e "
cat "$PROFILE_FILE" | bun --no-env-file --config=/dev/null -e "
const p = JSON.parse(await Bun.stdin.text());
const sessions = p.sessions || [];
@@ -388,7 +388,7 @@ do_profile() {
# -----------------------------------------------------------------------
do_gap() {
ensure_profile
cat "$PROFILE_FILE" | bun -e "
cat "$PROFILE_FILE" | bun --no-env-file --config=/dev/null -e "
const p = JSON.parse(await Bun.stdin.text());
const declared = p.declared || {};
const inferred = (p.inferred && p.inferred.values) || {};
@@ -417,7 +417,7 @@ do_derive() {
fi
cd "$ROOT_DIR"
PROFILE_FILE_PATH="$PROFILE_FILE" EVENTS_PATH="$EVENTS" bun -e "
PROFILE_FILE_PATH="$PROFILE_FILE" EVENTS_PATH="$EVENTS" bun --no-env-file --config=/dev/null -e "
import('./scripts/question-registry.ts').then(async (regmod) => {
const sigmod = await import('./scripts/psychographic-signals.ts');
const fs = require('fs');
@@ -504,7 +504,7 @@ do_trace() {
return 0
fi
cd "$ROOT_DIR"
EVENTS_PATH="$EVENTS" TRACE_DIM="$DIM" bun -e "
EVENTS_PATH="$EVENTS" TRACE_DIM="$DIM" bun --no-env-file --config=/dev/null -e "
import('./scripts/question-registry.ts').then(async (regmod) => {
const sigmod = await import('./scripts/psychographic-signals.ts');
const fs = require('fs');
@@ -542,7 +542,7 @@ do_trace() {
# -----------------------------------------------------------------------
do_check_mismatch() {
ensure_profile
cat "$PROFILE_FILE" | bun -e "
cat "$PROFILE_FILE" | bun --no-env-file --config=/dev/null -e "
const p = JSON.parse(await Bun.stdin.text());
const declared = p.declared || {};
const inferred = (p.inferred && p.inferred.values) || {};
@@ -586,7 +586,7 @@ do_narrative() {
do_vibe() {
ensure_profile
cd "$ROOT_DIR"
cat "$PROFILE_FILE" | PROFILE_DATA="$(cat "$PROFILE_FILE")" bun -e "
cat "$PROFILE_FILE" | PROFILE_DATA="$(cat "$PROFILE_FILE")" bun --no-env-file --config=/dev/null -e "
import('./scripts/archetypes.ts').then(async (mod) => {
const p = JSON.parse(process.env.PROFILE_DATA);
const dims = (p.inferred && p.inferred.values) || {
+9
View File
@@ -104,6 +104,11 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
*.tsx|*.jsx|*.vue|*.svelte|*.astro) m_frontend=true ;;
*.erb|*.haml|*.slim|*.hbs|*.ejs) m_frontend=true ;;
*.html) m_frontend=true ;;
# Dart: the extension cannot tell a Flutter widget from a repository or a
# pure-Dart server, so it is BOTH frontend and backend (BACKEND set below).
# A skipped reviewer is invisible; an extra one (design-lite included) is a
# visible pass that costs a review.
*.dart) m_frontend=true ;;
tailwind.config.*|postcss.config.*) m_frontend=true ;;
app/views/*|*/components/*|styles/*|css/*|app/assets/stylesheets/*) m_frontend=true ;;
esac
@@ -135,6 +140,7 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
*.yml|*.yaml) m_config=true ;;
.github/*) m_config=true ;;
requirements.txt|pyproject.toml|go.mod|Cargo.toml|composer.json) m_config=true ;;
pubspec.lock|*/pubspec.lock) m_config=true ;;
esac
# Migrations: database migration files. Bare migrations/* covers a
@@ -169,6 +175,9 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
*.ts|*.js|*.mjs|*.cjs|*.mts|*.cts) m_backend=true ;;
esac
fi
case "$f" in
*.dart) m_backend=true ;;
esac
[ "$m_frontend" = true ] && FRONTEND=true
[ "$m_prompts" = true ] && PROMPTS=true
+2 -2
View File
@@ -57,7 +57,7 @@ if [ ! -f "$PROPOSAL_FILE" ]; then
fi
if [ "$ACTION" = "list" ]; then
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" bun -e '
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" bun --no-env-file --config=/dev/null -e '
const fs = require("fs");
const p = JSON.parse(fs.readFileSync(process.env.PROPOSAL_FILE_PATH, "utf-8"));
const proposals = p.proposals || [];
@@ -100,7 +100,7 @@ MEMORY_FILE_PATH="$MEMORY_FILE" \
PROFILE_FILE_PATH="$PROFILE_FILE" \
PREF_BIN="$SCRIPT_DIR/gstack-question-preference" \
GBRAIN_PUBLISHED="$GBRAIN_PUBLISHED" \
bun -e '
bun --no-env-file --config=/dev/null -e '
const fs = require("fs");
const { spawnSync } = require("child_process");
const idx = parseInt(process.env.PROPOSAL_IDX, 10);
+5 -5
View File
@@ -59,7 +59,7 @@ esac
# --- Status subcommand --------------------------------------------------
if [ "$MODE" = "status" ]; then
COST_LOG_PATH="$COST_LOG" SLUG_PATH="$SLUG" bun -e '
COST_LOG_PATH="$COST_LOG" SLUG_PATH="$SLUG" bun --no-env-file --config=/dev/null -e '
const fs = require("fs");
const slug = process.env.SLUG_PATH;
const path = process.env.COST_LOG_PATH;
@@ -98,7 +98,7 @@ if [ ! -f "$LOG_FILE" ]; then
exit 0
fi
EVENTS_JSON=$(LOG_FILE_PATH="$LOG_FILE" bun -e '
EVENTS_JSON=$(LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config=/dev/null -e '
const fs = require("fs");
const lines = fs.readFileSync(process.env.LOG_FILE_PATH, "utf-8").trim().split("\n").filter(Boolean);
const out = [];
@@ -119,7 +119,7 @@ EVENTS_JSON=$(LOG_FILE_PATH="$LOG_FILE" bun -e '
process.stdout.write(JSON.stringify(out));
')
EVENT_COUNT=$(printf '%s' "$EVENTS_JSON" | bun -e 'const a = JSON.parse(await Bun.stdin.text()); console.log(a.length);')
EVENT_COUNT=$(printf '%s' "$EVENTS_JSON" | bun --no-env-file --config=/dev/null -e 'const a = JSON.parse(await Bun.stdin.text()); console.log(a.length);')
if [ "$EVENT_COUNT" -eq 0 ]; then
echo "NO_FREE_TEXT: nothing to distill"
exit 0
@@ -172,7 +172,7 @@ if [ "$MODE" = "dry-run" ]; then
echo "$DISTILL_PROMPT"
echo
echo "=== EVENTS ($EVENT_COUNT) ==="
echo "$EVENTS_JSON" | bun -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()), null, 2));'
echo "$EVENTS_JSON" | bun --no-env-file --config=/dev/null -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()), null, 2));'
exit 0
fi
@@ -196,7 +196,7 @@ fi
RESULT=$(EVENTS_JSON="$EVENTS_JSON" DISTILL_PROMPT="$DISTILL_PROMPT" \
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" LOG_FILE_PATH="$LOG_FILE" \
ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
bun --cwd "$ROOT_DIR" -e '
bun --no-env-file --config=/dev/null --cwd "$ROOT_DIR" -e '
const fs = require("fs");
const Anthropic = require("@anthropic-ai/sdk").default;
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S bun --no-env-file run
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-docs-candidate — /ship's documentation-audit snapshot and its later check.
*
+1 -1
View File
@@ -512,7 +512,7 @@ check_windows_bash() {
row warn "windows bash" "not checked: bun not found" "install Bun (see the bun row), then rerun $BIN/gstack-doctor"
return
fi
out=$(bun "$BIN/gstack-windows-bash.ts" ${GSTACK_DOCTOR_BASH_PROBE:+--probe "$GSTACK_DOCTOR_BASH_PROBE"} </dev/null 2>&1)
out=$(bun --no-env-file --config=/dev/null "$BIN/gstack-windows-bash.ts" ${GSTACK_DOCTOR_BASH_PROBE:+--probe "$GSTACK_DOCTOR_BASH_PROBE"} </dev/null 2>&1)
rc=$?
out=$(printf '%s\n' "$out" | tr -d '\000-\010\013-\037')
bash=$(printf '%s\n' "$out" | sed -n 's/^WINDOWS_BASH: //p' | head -1)
+3 -3
View File
@@ -213,7 +213,7 @@ _dc_check_browser() {
out=$(cd "$ROOT" && { command -v timeout >/dev/null 2>&1 && timeout 90 node -e "$script" </dev/null 2>&1 || node -e "$script" </dev/null 2>&1; }); rc=$?
elif command -v bun >/dev/null 2>&1; then
engine=bun
out=$(cd "$ROOT" && bun -e "$script" </dev/null 2>&1); rc=$?
out=$(cd "$ROOT" && bun --no-env-file --config=/dev/null -e "$script" </dev/null 2>&1); rc=$?
else
_dc_row FAIL browser "neither node nor bun is on PATH to launch Chromium" "install Node.js (https://nodejs.org) or Bun, then $fix" BROWSER_UNAVAILABLE
return
@@ -329,7 +329,7 @@ _dc_check_pins() {
return
fi
node=$(node --version </dev/null 2>/dev/null | clamp 'v0-9.' 20); node="${node#v}"
out=$(bun "$BIN/gstack-runtime-pins.ts" --project "$project" ${_dc_bun:+--bun "$_dc_bun"} ${node:+--node "$node"} </dev/null 2>&1); rc=$?
out=$(bun --no-env-file --config=/dev/null "$BIN/gstack-runtime-pins.ts" --project "$project" ${_dc_bun:+--bun "$_dc_bun"} ${node:+--node "$node"} </dev/null 2>&1); rc=$?
verdict=$(printf '%s\n' "$out" | sed -n 's/^PINS_VERDICT: //p' | tail -1 | clamp 'a-z' 8)
summary=$(printf '%s\n' "$out" | sed -n 's/^PIN_SUMMARY: //p' | tr '\n' '|' | sed 's/|$//; s/|/; /g')
case "$verdict" in
@@ -357,7 +357,7 @@ _dc_check_pins() {
fi
fi
if [ -n "$_dc_ln_dir" ] && [ "$_dc_ln_ver" != "$_dc_bun" ] \
&& bun "$BIN/gstack-runtime-pins.ts" --project "$project" --bun "$_dc_ln_ver" ${node:+--node "$node"} </dev/null 2>/dev/null | grep -q '^PINS_VERDICT: pass'; then
&& bun --no-env-file --config=/dev/null "$BIN/gstack-runtime-pins.ts" --project "$project" --bun "$_dc_ln_ver" ${node:+--node "$node"} </dev/null 2>/dev/null | grep -q '^PINS_VERDICT: pass'; then
note="; bun $_dc_ln_ver found at $_dc_ln_dir (not on PATH) satisfies them" fix="$_dc_ln_fix"
fi
_dc_row FAIL pins "$failline$note" "$fix" PROJECT_PIN_MISMATCH ;;
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-egress — the auditor's view of the receipts ledger.
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
// gstack-egress-receipt — bun script that BRIDGES shell callers (the bash
// egress sinks: gstack-telemetry-sync, gstack-update-check, gstack-brain-sync,
// and the sourced helpers in gstack-egress-lib.sh) into lib/egress-receipt.ts.
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
import { dxErrorCaptureMain } from '../lib/dx-error-capture';
process.exit(await dxErrorCaptureMain(process.argv.slice(2)));
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-eval-plan — the tier-1 machinery behind /eval-plan (plan E1).
*
+2 -99
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-evidence — verification-evidence ledger: the mechanical arm of /ship's
* IRON LAW ("no completion claims without fresh verification evidence").
@@ -269,97 +269,6 @@ function openLog(logsDir: string, label: string, cmdSha: string): { fd: number;
return undefined;
}
/**
* Bun AUTO-LOADS `.env`, `.env.<NODE_ENV>` and `.env.local` from the working
* directory into `process.env`. This file has a `#!/usr/bin/env bun` shebang, so
* every command it spawns inherits those variables — and a repo `.env.local`
* routinely holds PRODUCTION credentials.
*
* Two things go wrong, and the second one is worse than the leak:
*
* 1. Secrets reach a child process that would not have had them. `npm test` run
* by hand in the same shell sees none of this; run through the wrapper it sees
* all of it.
* 2. THE COMMAND UNDER TEST BEHAVES DIFFERENTLY, so the evidence ledger
* certifies a run that is not the run CI performs. Observed in a Next.js repo
* 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. The
* wrapper exists to record trustworthy evidence, so silently changing the
* environment defeats its whole purpose.
*
* Verified against 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 is
* exactly 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.
*
* Escape hatch: GSTACK_EVIDENCE_KEEP_DOTENV=1 keeps the old behaviour for anyone
* who really does want the wrapper to supply .env values.
*/
const BUN_DOTENV_FILES = (): string[] => {
const nodeEnv = process.env.NODE_ENV;
// bun's documented precedence, lowest first. `.env.local` is skipped by bun
// when NODE_ENV is "test"; mirror that rather than guessing.
const files = [".env"];
if (nodeEnv) files.push(`.env.${nodeEnv}`);
if (nodeEnv !== "test") files.push(".env.local");
return files;
};
/** Minimal dotenv reader: KEY=VALUE, one per line. Quotes stripped, comments and
* `export ` prefixes tolerated. Multi-line values are not parsed — a key we fail
* to parse is simply left in the child env, which is the safe direction.
* Known limitation: bun EXPANDS ${VAR} references inside dotenv values, but this
* reader compares the raw file text, so an expanded live value never matches and
* that key is left in the child env — the pre-scrub behavior persists for those
* keys (fails open, same safe direction as above). */
function parseDotenv(text: string): Map<string, string> {
const out = new Map<string, string>();
for (const raw of text.split(/\r?\n/)) {
const line = raw.trim();
if (!line || line.startsWith("#")) continue;
const eq = line.indexOf("=");
if (eq <= 0) continue;
let key = line.slice(0, eq).trim();
if (key.startsWith("export ")) key = key.slice(7).trim();
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
let val = line.slice(eq + 1).trim();
if ((val.startsWith('"') && val.endsWith('"') && val.length > 1) ||
(val.startsWith("'") && val.endsWith("'") && val.length > 1)) {
val = val.slice(1, -1);
}
out.set(key, val);
}
return out;
}
/** process.env minus the variables bun injected from the repo's dotenv files.
* Returns the scrubbed env and the KEY NAMES removed (never the values). */
function childEnv(cwd: string): { env: Record<string, string>, scrubbed: string[] } {
const env: Record<string, string> = {};
for (const [k, v] of Object.entries(process.env)) if (v !== undefined) env[k] = v;
if (process.env.GSTACK_EVIDENCE_KEEP_DOTENV === "1") return { env, scrubbed: [] };
const scrubbed: string[] = [];
for (const file of BUN_DOTENV_FILES()) {
let text: string;
try {
text = readFileSync(join(cwd, file), "utf-8");
} catch {
continue; // absent or unreadable — nothing to scrub from it
}
for (const [k, v] of parseDotenv(text)) {
// Only when the live value IS the file's value. A different live value means
// the shell exported its own and bun left it alone, so it is genuinely the
// user's environment and must survive.
if (env[k] !== undefined && env[k] === v) {
delete env[k];
if (!scrubbed.includes(k)) scrubbed.push(k);
}
}
}
return { env, scrubbed };
}
async function cmdRun(argv: string[]): Promise<number> {
let label = "default";
const li = argv.indexOf("--label");
@@ -395,15 +304,9 @@ async function cmdRun(argv: string[]): Promise<number> {
let exitCode: number;
let proc: ReturnType<typeof Bun.spawn> | undefined;
try {
const { env: spawnEnv, scrubbed } = childEnv(process.cwd());
if (scrubbed.length > 0) {
// Names only. Printing values here would defeat the point.
warn(`scrubbed ${scrubbed.length} bun-injected dotenv var(s) from the child env: ${scrubbed.join(", ")} `
+ `(GSTACK_EVIDENCE_KEEP_DOTENV=1 to keep them)`);
}
if (shell && !shell.ok) throw new Error(shell.error);
const spawnArgv = shell?.ok ? [shell.cmd, ...shell.argv] : cmdArgv;
proc = Bun.spawn(spawnArgv, { stdin: "inherit", stdout: "pipe", stderr: "pipe", env: spawnEnv });
proc = Bun.spawn(spawnArgv, { stdin: "inherit", stdout: "pipe", stderr: "pipe" });
} catch (e: any) {
// Spawn failure (ENOENT on argv-direct form): record exit 127, propagate 127.
exitCode = 127;
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-gate — render and parse the one gate list (plan B9).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-gate-diff — list the gate edits in a candidate so a human can
* disposition each one (W1 of docs/designs/HONEST_WORK_GATE_INTEGRITY.md).
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S bun run
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-gbrain-detect — emit current gbrain/gstack-brain state as JSON.
*
+2 -2
View File
@@ -100,7 +100,7 @@ fi
# bun, which this installer already requires. It used to need jq, which it
# never checked for, so a successful install reported failure (#1675).
pkg_field() {
GSTACK_PKG_JSON="$1" GSTACK_PKG_FIELD="$2" bun -e '
GSTACK_PKG_JSON="$1" GSTACK_PKG_FIELD="$2" bun --no-env-file --config=/dev/null -e '
try {
const pkg = JSON.parse(require("fs").readFileSync(process.env.GSTACK_PKG_JSON, "utf8"));
const v = process.env.GSTACK_PKG_FIELD.split(".").reduce((o, k) => (o == null ? o : o[k]), pkg);
@@ -198,7 +198,7 @@ _bun_exe_hint() {
[ "$IS_WINDOWS" -eq 1 ] || return 0
command -v bun.exe >/dev/null 2>&1 && return 0
local real_bun
real_bun=$(bun -e 'console.log(process.execPath)' 2>/dev/null | tr -d '\r' || true)
real_bun=$(bun --no-env-file --config=/dev/null -e 'console.log(process.execPath)' 2>/dev/null | tr -d '\r' || true)
echo " detected: bun was installed via npm — bun.exe is NOT on %PATH%, and the gbrain.exe shim needs it." >&2
if [ -n "$real_bun" ]; then
echo " fix: add bun.exe's directory to PATH (persist it in your shell profile):" >&2
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
import { readFileSync, realpathSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
+1 -1
View File
@@ -222,7 +222,7 @@ ensure_worktree() {
# is not JSON.
source_local_path() {
gbrain sources list --json 2>/dev/null \
| GB_SOURCE_ID="$1" bun -e '
| GB_SOURCE_ID="$1" bun --no-env-file --config=/dev/null -e '
const raw = await Bun.stdin.text();
try {
const s = (JSON.parse(raw).sources || []).find((x) => x.id === process.env.GB_SOURCE_ID);
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S bun run
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-gbrain-supabase-provision — Supabase Management API wrapper for
* /setup-gbrain path 2a (auto-provision). Thin entry: all logic lives in
+3 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-gbrain-sync — V1 unified sync verb.
*
@@ -51,6 +51,7 @@ import {
} from "../lib/gbrain-sources";
import { detectAutopilot, decideSourceRemove, decideCodeSync } from "../lib/gbrain-guards";
import { writeReceipt } from "../lib/egress-receipt";
import { bunIsolationArgs } from "../lib/bun-isolation";
import { dbUnreachableReason, localEngineStatus, localEngineStatusDetail, type LocalEngineStatus } from "../lib/gbrain-local-status";
import { buildGbrainEnv, spawnGbrain, spawnGbrainAsync, execGbrainJson, NEEDS_SHELL_ON_WINDOWS, bashScriptInvocation } from "../lib/gbrain-exec";
import { repoPolicyTier as sharedRepoPolicyTier } from "../lib/gbrain-repo-policy-client";
@@ -1611,7 +1612,7 @@ function runMemoryIngest(args: CliArgs, previous: SyncState, consent: Transcript
}
const ingestPath = join(import.meta.dir, "gstack-memory-ingest.ts");
const ingestArgs = ["run", ingestPath];
const ingestArgs = [...bunIsolationArgs(), "run", ingestPath];
if (args.mode === "full") ingestArgs.push("--bulk");
else ingestArgs.push("--incremental");
if (args.mode === "full" && consent.affirmative && consent.window !== "recent") ingestArgs.push("--all-history");
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-global-discover — Discover AI coding sessions across Claude Code, Codex CLI, and Gemini CLI.
* Resolves each session's working directory to a git repo, deduplicates by normalized remote URL,
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-greptile-early — /ship's early Greptile review (detect, consent, open, wait).
*
+1 -1
View File
@@ -173,7 +173,7 @@ if [ -n "$ENTRIES" ]; then
# up below (lib/path-spelling.ts shellPath; test/path-spelling.test.ts keeps
# the two equal).
# shellcheck disable=SC2086 # newline-split list (IFS is set to a newline below)
(cd "$ROOT" && IFS=$NL && "$BUN" -e '
(cd "$ROOT" && IFS=$NL && "$BUN" --no-env-file --config=/dev/null -e '
const shellPath = (value) => process.platform === "win32"
? String(value).replace(/\\/g, "/").replace(/^([A-Za-z]):(?=\/|$)/, (_, drive) => `/${drive.toLowerCase()}`)
: String(value);
+1 -1
View File
@@ -159,7 +159,7 @@ gstack_install_admission() {
v_bun=$(gstack_install_verdict_field "$file" bun_path)
if [ -z "$detail" ] && [ -n "$v_bun" ]; then
running=$(gstack_realpath "$where" 2>/dev/null)
_gstack_bun_is_shim "$where" && running=$(bun -p process.execPath </dev/null 2>/dev/null | head -1)
_gstack_bun_is_shim "$where" && running=$(bun --no-env-file --config=/dev/null -p process.execPath </dev/null 2>/dev/null | head -1)
if [ -n "$running" ] && [ "$running" != "$v_bun" ]; then
_ia_status=runtime code=BUN_RESOLUTION_DRIFT
detail="bun $found at $running is running, but the install verdict recorded bun $(gstack_install_verdict_field "$file" bun_version) at $v_bun"
+1 -1
View File
@@ -67,4 +67,4 @@ if ! command -v bun >/dev/null 2>&1; then
exit 1
fi
exec bun run "$ENTRY" "$@"
exec bun --no-env-file --config=/dev/null run "$ENTRY" "$@"
+1 -1
View File
@@ -25,4 +25,4 @@ if ! command -v bun >/dev/null 2>&1; then
exit 1
fi
exec bun run "$ENTRY" "$@"
exec bun --no-env-file --config=/dev/null run "$ENTRY" "$@"
+1 -1
View File
@@ -144,7 +144,7 @@ do
fi
done
bun run "$GENERATOR" --input "$APP_SOURCE" --output "$GENERATED_DIR"
bun --no-env-file --config=/dev/null run "$GENERATOR" --input "$APP_SOURCE" --output "$GENERATED_DIR"
# Stamp only after successful accessor generation. ios-sync uses this marker
# to distinguish a complete current install from an interrupted regeneration.
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-issue-guard — fetch tracker text and emit it inside the untrusted
* trust envelope (lib/tracker-guard.ts). The ONLY sanctioned path for reading
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-issue-links — issue links by intent for /ship Step 18 (plan C6).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-lane-check — planned files versus in-flight lanes (plan E2).
*
+1 -1
View File
@@ -27,7 +27,7 @@ INPUT="$1"
TMPERR=$(mktemp "${TMPDIR:-/tmp}/gstack-learnings-log.XXXXXX") || { echo "gstack-learnings-log: could not create a temp file in ${TMPDIR:-/tmp}, so the learning was not recorded. Fix: point TMPDIR at a writable directory." >&2; exit 1; }
trap 'rm -f "$TMPERR"' EXIT
set +e
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun -e "
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config=/dev/null -e "
const { hasInjection } = await import(process.env.GSTACK_LIB_DIR + '/jsonl-store.ts');
const raw = await Bun.stdin.text();
let j;
+1 -1
View File
@@ -69,7 +69,7 @@ emit_tagged_file() {
emit_tagged_file cross "$f"
done
fi
} | GSTACK_SEARCH_TYPE="$TYPE" GSTACK_SEARCH_QUERY="$QUERY" GSTACK_SEARCH_LIMIT="$LIMIT" GSTACK_SEARCH_CROSS="$CROSS_PROJECT" bun -e "
} | GSTACK_SEARCH_TYPE="$TYPE" GSTACK_SEARCH_QUERY="$QUERY" GSTACK_SEARCH_LIMIT="$LIMIT" GSTACK_SEARCH_CROSS="$CROSS_PROJECT" bun --no-env-file --config=/dev/null -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const now = Date.now();
const type = process.env.GSTACK_SEARCH_TYPE || '';
+1 -1
View File
@@ -374,7 +374,7 @@ status_bridge() {
# JSON; a failed query is reported as unknown, never as an empty history.
local egress_json
if egress_json="$("$EGRESS_BIN" list --sink "$SINK" --json 2>/dev/null)"; then
n="$(printf '%s' "$egress_json" | bun -e 'const a=JSON.parse(require("fs").readFileSync(0,"utf8")||"[]");console.log(Array.isArray(a)?a.length:0)' 2>/dev/null)"
n="$(printf '%s' "$egress_json" | bun --no-env-file --config=/dev/null -e 'const a=JSON.parse(require("fs").readFileSync(0,"utf8")||"[]");console.log(Array.isArray(a)?a.length:0)' 2>/dev/null)"
case "$n" in *[!0-9]*|"") n="unknown (could not parse gstack-egress output)" ;; esac
else
n="unknown (gstack-egress list failed; run it yourself)"
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-memory-ingest — V1 memory ingest helper.
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
// Repair existing installs only; never install a new host or invoke an AI CLI.
import * as path from 'node:path';
import { migrateClaudeCodeSkills } from '../lib/claude-code-migration';
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-model-benchmark — run the same prompt across multiple providers
* and compare latency, tokens, cost, quality, and tool-call count.
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
import { modelsMain } from '../lib/model-policy-cli.ts';
const result = modelsMain(process.argv.slice(2));
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
// gstack-next-version — host-aware VERSION allocator for /ship.
//
// Queries the PR queue (GitHub or GitLab), fetches each open PR's VERSION,
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
import * as fs from 'node:fs';
import * as path from 'node:path';
import { atomicWriteSync } from '../lib/fs-atomic.ts';
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-outside-voice — run the outside voice through one pluggable runner (plan B3).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-owner-brief — the one-page owner brief at the autoplan gate (plan E3).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-plan-reality — the reality rows of a plan review (plan D1).
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-post — post PR/MR and issue text from agent-written files.
*
+9 -6
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-pregate — cheap checks before anything paid or remote (plan C8).
*
* gstack-pregate [run] [--stage preflight|tests|all] [--only <check,..>] [--base <ref>]
* gstack-pregate [run] [--stage preflight|tests|all] [--only <check,..>] [--base <branch>]
* [--allow-repo-commands] [--policy-from base|head] [--out <pregate.json>]
* [--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--explain] [--json]
* gstack-pregate verify [--out <pregate.json>] [--json] publication check: current tree, no fail/incomplete, obligations cleared
@@ -27,7 +27,8 @@ import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { EXIT, exitTableHelp, formatResult, type PregateFile } from '../lib/headless-artifacts';
import { PREGATE_CONFIG_PATH, exampleConfigJson, explainConfig, loadConfigFromHead } from '../lib/pregate/config';
import { PREGATE_CONFIG_PATH, explainConfig, loadConfigFromHead } from '../lib/pregate/config';
import { describeDraft, draftConfig } from '../lib/pregate/init-draft';
import { buildContext, describeRefusal } from '../lib/pregate/context';
import { CHECKS, CHECK_IDS, PREGATE_OUT_REL, PREGATE_TIER2, clearDecision, clearObligation, readPregateFile, renderTable, runPregate, verifyPregate, writePregateFile } from '../lib/pregate/index';
import { describeLane, discoverLanes } from '../lib/pregate/workflows';
@@ -60,7 +61,7 @@ const HELP = `gstack-pregate — cheap checks before anything paid or remote (do
--stage preflight|tests|all default all
--only <check,...> restrict to these checks (${CHECK_IDS.join(', ')}; tier 2 later: ${PREGATE_TIER2.join(', ')})
--base <ref> base branch (default: origin/HEAD, else main)
--base <branch> a branch on origin, not a commit (default: origin/HEAD's branch, else main)
--allow-repo-commands run pin-file commands and the test runner (what /ship passes); without it those checks are incomplete
--policy-from base|head base (default, reviewed) or head (unreviewed; needs --allow-repo-commands)
--out <file> pregate.json path (default ${PREGATE_OUT_REL})
@@ -303,9 +304,11 @@ async function main(argv: string[]): Promise<number> {
if (rest.includes('--explain')) { process.stdout.write(explainConfig()); if (rest.length === 1) return EXIT.ok; }
const file = path.join(root, PREGATE_CONFIG_PATH);
if (fs.existsSync(file) && !rest.includes('--force')) { process.stderr.write(resultLine('gstack-pregate init', 'POLICY_EXISTS', file) + '\n'); return EXIT.refused; }
const draft = draftConfig(root);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, exampleConfigJson());
process.stdout.write(`PREGATE_CONFIG_WRITTEN: ${PREGATE_CONFIG_PATH} (edit it, then gstack-pregate validate; commit it on the base branch before /ship reads it)\n`);
fs.writeFileSync(file, draft.json);
process.stdout.write(`PREGATE_CONFIG_WRITTEN: ${PREGATE_CONFIG_PATH} (a draft from this repo: review it, then gstack-pregate validate; commit it on the base branch before /ship reads it)\n`);
for (const line of describeDraft(draft)) process.stdout.write(`${line}\n`);
return EXIT.ok;
}
process.stderr.write(`gstack-pregate: unknown command ${cmd}\n${HELP}\n`);
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
import { qaDeadlineMain } from '../lib/qa-deadline';
const args = process.argv.slice(2);
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
import { qaEvidenceMain } from '../lib/qa-evidence';
process.exit(await qaEvidenceMain(process.argv.slice(2)));
+3 -3
View File
@@ -46,7 +46,7 @@ INPUT="$1"
TMPERR=$(mktemp "${TMPDIR:-/tmp}/gstack-question-log.XXXXXX") || { echo "gstack-question-log: could not create a temp file in ${TMPDIR:-/tmp}, so the question was not logged. Fix: point TMPDIR at a writable directory." >&2; exit 1; }
trap 'rm -f "$TMPERR"' EXIT
set +e
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun -e "
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config=/dev/null -e "
const { hasInjection } = await import(process.env.GSTACK_LIB_DIR + '/jsonl-store.ts');
const path = require('path');
const raw = await Bun.stdin.text();
@@ -218,7 +218,7 @@ LOG_FILE="$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
# Lookup is bounded so the bin stays cheap on hot paths.
DEDUP_SKIP=""
if [ -f "$LOG_FILE" ]; then
DEDUP_SKIP=$(VALIDATED_JSON="$VALIDATED" LOG_FILE_PATH="$LOG_FILE" bun -e '
DEDUP_SKIP=$(VALIDATED_JSON="$VALIDATED" LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config=/dev/null -e '
const fs = require("fs");
const j = JSON.parse(process.env.VALIDATED_JSON);
if (!j.tool_use_id) { console.log(""); process.exit(0); }
@@ -238,7 +238,7 @@ if [ -f "$LOG_FILE" ]; then
fi
if [ "$DEDUP_SKIP" = "dup" ]; then
echo "DEDUP: skipped (source=$(echo "$VALIDATED" | bun -e 'const j=JSON.parse(await Bun.stdin.text()); console.log(j.source);'), tool_use_id duplicate)"
echo "DEDUP: skipped (source=$(echo "$VALIDATED" | bun --no-env-file --config=/dev/null -e 'const j=JSON.parse(await Bun.stdin.text()); console.log(j.source);'), tool_use_id duplicate)"
exit 0
fi
+19 -5
View File
@@ -2,6 +2,9 @@
# gstack-question-preference — read/write/check explicit per-question preferences.
#
# Preference file: ~/.gstack/projects/{SLUG}/question-preferences.json
# Global fallback: ~/.gstack/global-question-preferences.json, the file the
# question-preference hook reads (--check only; a project entry for the same
# id wins). Hand-edited; --write stays per-project.
# Schema: { "<question_id>": "always-ask" | "never-ask" | "ask-only-for-one-way" }
#
# Subcommands:
@@ -42,6 +45,7 @@ GSTACK_HOME="$_gstack_sr_root"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null || true)"
SLUG="${SLUG:-unknown}"
PREF_FILE="$GSTACK_HOME/projects/$SLUG/question-preferences.json"
GLOBAL_PREF_FILE="$GSTACK_HOME/global-question-preferences.json"
EVENT_FILE="$GSTACK_HOME/projects/$SLUG/question-events.jsonl"
mkdir -p "$GSTACK_HOME/projects/$SLUG"
@@ -101,13 +105,23 @@ do_check() {
fi
ensure_file
cd "$ROOT_DIR"
PREF_FILE_PATH="$PREF_FILE" QID="$QID" QSUMMARY="$QSUMMARY" bun -e "
PREF_FILE_PATH="$PREF_FILE" GLOBAL_PREF_FILE_PATH="$GLOBAL_PREF_FILE" QID="$QID" QSUMMARY="$QSUMMARY" bun --no-env-file --config=/dev/null -e "
import('./scripts/one-way-doors.ts').then((oneway) => {
const fs = require('fs');
const qid = process.env.QID;
const summary = process.env.QSUMMARY || undefined;
const prefs = JSON.parse(fs.readFileSync(process.env.PREF_FILE_PATH, 'utf-8'));
const pref = prefs[qid];
let globalPrefs = {};
try {
if (fs.existsSync(process.env.GLOBAL_PREF_FILE_PATH)) {
const parsed = JSON.parse(fs.readFileSync(process.env.GLOBAL_PREF_FILE_PATH, 'utf-8'));
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not an object');
globalPrefs = parsed;
}
} catch (e) {
console.error('NOTE: global-question-preferences.json is unreadable; ignoring it.');
}
const pref = qid in prefs ? prefs[qid] : globalPrefs[qid];
// Always check one-way status first — safety overrides preferences.
// summary (when piped) lets the keyword net catch ad-hoc destructive
@@ -187,7 +201,7 @@ do_write() {
set +e
local RESULT
RESULT=$(cd "$ROOT_DIR" && printf '%s' "$INPUT" | PREF_FILE_PATH="$PREF_FILE" EVENT_FILE_PATH="$EVENT_FILE" FREE_TEXT="$FREE_TEXT" HAS_FREE_TEXT="$HAS_FREE_TEXT" bun -e "
RESULT=$(cd "$ROOT_DIR" && printf '%s' "$INPUT" | PREF_FILE_PATH="$PREF_FILE" EVENT_FILE_PATH="$EVENT_FILE" FREE_TEXT="$FREE_TEXT" HAS_FREE_TEXT="$HAS_FREE_TEXT" bun --no-env-file --config=/dev/null -e "
const fs = require('fs');
const raw = await Bun.stdin.text();
let j;
@@ -306,7 +320,7 @@ do_clear() {
echo '{}' > "$PREF_FILE"
echo "OK: cleared all preferences"
else
PREF_FILE_PATH="$PREF_FILE" QID="$QID" bun -e "
PREF_FILE_PATH="$PREF_FILE" QID="$QID" bun --no-env-file --config=/dev/null -e "
const fs = require('fs');
const prefs = JSON.parse(fs.readFileSync(process.env.PREF_FILE_PATH, 'utf-8'));
if (prefs[process.env.QID] !== undefined) {
@@ -325,7 +339,7 @@ do_clear() {
# -----------------------------------------------------------------------
do_stats() {
ensure_file
(cd "$ROOT_DIR" && PREF_FILE_PATH="$PREF_FILE" bun -e "
(cd "$ROOT_DIR" && PREF_FILE_PATH="$PREF_FILE" bun --no-env-file --config=/dev/null -e "
import('./scripts/one-way-doors.ts').then((oneway) => {
const fs = require('fs');
const prefs = JSON.parse(fs.readFileSync(process.env.PREF_FILE_PATH, 'utf-8'));
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-redact — scan text for secrets/PII/legal content via the shared engine.
*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-redact-prepush — git pre-push hook that scans the diff being pushed for
* HIGH-severity credentials and blocks the push on a hit.
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-regen — the generated-file registry `.gstack/generated.json` (plan C4).
*
+10 -3
View File
@@ -461,19 +461,26 @@ done
# (in place, like gstack-patch-names). Clearing the list restores the committed
# bytes. Only a generated router is rewritten, and only from a checkout that
# carries the generator; a failed render keeps the current router.
# plan_mode_preamble=off (#3168) renders it without the plan-mode preamble the
# same way, and turning it back on restores the committed bytes.
_ROUTER="$INSTALL_DIR/SKILL.md"
_GEN_SRC="$(cd "$SCRIPT_DIR/.." && pwd)"
if { [ "$DISABLED" != " " ] || grep -q '^Disabled on this install' "$_ROUTER" 2>/dev/null; } \
_PLAN_MODE_ARG=""
[ "$("$GSTACK_CONFIG" get plan_mode_preamble 2>/dev/null || true)" != off ] || _PLAN_MODE_ARG="--plan-mode=off"
if { [ "$DISABLED" != " " ] || grep -q '^Disabled on this install' "$_ROUTER" 2>/dev/null \
|| [ -n "$_PLAN_MODE_ARG" ] \
|| { grep -q 'PLAN MODE EXCEPTION' "$_ROUTER" 2>/dev/null && ! grep -q '^## Plan Mode Safe Operations' "$_ROUTER" 2>/dev/null; }; } \
&& grep -q '<!-- AUTO-GENERATED from' "$_ROUTER" 2>/dev/null && [ -f "$_GEN_SRC/scripts/gen-skill-docs.ts" ]; then
_ROUTER_TMP="$(mktemp -d "${TMPDIR:-/tmp}/gstack-router.XXXXXX")" || _ROUTER_TMP=""
_DISABLED_CSV="$(printf '%s' "$DISABLED" | xargs | tr ' ' ',')"
if [ -n "$_ROUTER_TMP" ] && command -v bun >/dev/null 2>&1 \
&& (cd "$_GEN_SRC" && bun run scripts/gen-skill-docs.ts --host claude --out-dir "$_ROUTER_TMP" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} >/dev/null 2>&1) \
&& (cd "$_GEN_SRC" && bun --no-env-file --config=/dev/null run scripts/gen-skill-docs.ts --host claude --out-dir "$_ROUTER_TMP" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} >/dev/null 2>&1) \
&& [ -s "$_ROUTER_TMP/SKILL.md" ] && cp "$_ROUTER_TMP/SKILL.md" "$_ROUTER.tmp.$$" && mv "$_ROUTER.tmp.$$" "$_ROUTER"; then
_link_root_skill_alias
[ -n "$_DISABLED_CSV" ] && echo "Router: disabled skills left out of $_ROUTER"
else
rm -f "$_ROUTER.tmp.$$"
echo "gstack-relink: could not re-render the router for disabled_skills (bun missing or generation failed); $_ROUTER still lists every skill. Fix: install Bun, then run $0" >&2
echo "gstack-relink: could not re-render the router for disabled_skills or plan_mode_preamble (bun missing or generation failed); $_ROUTER is unchanged. Fix: install Bun, then run $0" >&2
fi
[ -z "$_ROUTER_TMP" ] || rm -rf "$_ROUTER_TMP"
fi
+20 -10
View File
@@ -9,10 +9,15 @@
# upgrade falls back to `claude` with the reason printed.
#
# Each render keeps a plain-text activation record beside it
# (<render dir>.overlay: overlay, source, model, version, gbrain, render), so
# (<render dir>.overlay: overlay, source, model, version, gbrain, render, and
# plan_mode=off for a render without the plan-mode preamble), so
# `./setup --status` and the change-only banner never need bun. No record
# means the default `claude` overlay on the committed render.
#
# Callers set _PLAN_MODE_ARG to --plan-mode=off when gstack-config
# plan_mode_preamble is off (#3168); empty or unset means on. The committed
# files carry the preamble, so with it off the default install is rendered.
#
# Callers define `_swap_in_render` (setup and bin/gstack-config each keep
# their tested copy) and may define `log` and `bun_cmd`; plain echo and bun
# are used otherwise.
@@ -81,15 +86,18 @@ gstack_render_unlock() {
# gstack_claude_render_action RENDER_DIR GBRAIN — what the default install's
# render dir needs: `user` (brain-aware :user render), `plain` (pinned
# overlay, no brain blocks), `remove` (serve the committed files) or `keep`.
# overlay or plan-mode preamble off, no brain blocks), `remove` (serve the
# committed files) or `keep`.
# GBRAIN is ok, absent or unknown (detector missing or failed: no brain
# blocks, and a render is left alone unless an overlay change needs it).
# blocks, and a render is left alone unless an overlay or plan-mode change
# needs it).
gstack_claude_render_action() {
local render_dir="$1" gbrain="$2" previous
if [ "$gbrain" = ok ]; then _GSTACK_RENDER_ACTION=user; return 0; fi
if [ "$_GSTACK_OVERLAY" != claude ]; then _GSTACK_RENDER_ACTION=plain; return 0; fi
if [ "$_GSTACK_OVERLAY" != claude ] || [ -n "${_PLAN_MODE_ARG:-}" ]; then _GSTACK_RENDER_ACTION=plain; return 0; fi
previous="$(gstack_claude_overlay_field "$render_dir.overlay" overlay)"
if [ "$gbrain" = unknown ] && [ "${previous:-claude}" = claude ]; then
if [ "$gbrain" = unknown ] && [ "${previous:-claude}" = claude ] \
&& [ -z "$(gstack_claude_overlay_field "$render_dir.overlay" plan_mode)" ]; then
_GSTACK_RENDER_ACTION=keep
else
_GSTACK_RENDER_ACTION=remove
@@ -102,8 +110,8 @@ gstack_claude_render_plain() {
local src="$1" RENDER_DIR="$2" RENDER_TMP out rc=0
RENDER_TMP="$RENDER_DIR.tmp.$$"
rm -rf "$RENDER_TMP"
_gstack_render_say "Rendering Claude skills with the $_GSTACK_OVERLAY overlay into $RENDER_DIR (source checkout stays clean)..."
out="$(cd "$src" && _gstack_render_bun run gen:skill-docs --host claude --out-dir "$RENDER_TMP" --link-root "$RENDER_DIR" --model "$_GSTACK_OVERLAY" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} 2>&1)" || rc=$?
_gstack_render_say "Rendering Claude skills with the $_GSTACK_OVERLAY overlay${_PLAN_MODE_ARG:+ and no plan-mode preamble} into $RENDER_DIR (source checkout stays clean)..."
out="$(cd "$src" && _gstack_render_bun run gen:skill-docs --host claude --out-dir "$RENDER_TMP" --link-root "$RENDER_DIR" --model "$_GSTACK_OVERLAY" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} 2>&1)" || rc=$?
if [ "$rc" -ne 0 ]; then
rm -rf "$RENDER_TMP"
printf '%s\n' "$out" | tail -3
@@ -150,9 +158,11 @@ gstack_claude_render_settle() {
if [ "$render_kind" = committed ] && [ "$_GSTACK_OVERLAY_SOURCE" = default ]; then
rm -f "$record"
else
printf 'overlay=%s\nsource=%s\nmodel=%s\nversion=%s\ngbrain=%s\nrender=%s\n' \
"$served" "$_GSTACK_OVERLAY_SOURCE" "$_GSTACK_OVERLAY_ID" "${version:-unknown}" "$gbrain" "$render_kind" \
> "$record.tmp.$$" && mv "$record.tmp.$$" "$record"
{
printf 'overlay=%s\nsource=%s\nmodel=%s\nversion=%s\ngbrain=%s\nrender=%s\n' \
"$served" "$_GSTACK_OVERLAY_SOURCE" "$_GSTACK_OVERLAY_ID" "${version:-unknown}" "$gbrain" "$render_kind"
[ "$render_kind" != user ] || [ -z "${_PLAN_MODE_ARG:-}" ] || printf 'plan_mode=off\n'
} > "$record.tmp.$$" && mv "$record.tmp.$$" "$record"
fi
gstack_render_unlock "$render_dir"
if [ "$served" != "${previous:-claude}" ]; then
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-render — render a local HTML file through a browser: Aside when it
* is running, otherwise gstack's own headless browser (the browse daemon).
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-restamp — the whole stamp in one step (plan C2).
*
+3 -3
View File
@@ -72,14 +72,14 @@ fi
SESSION_KIND=$("$SCRIPT_DIR/gstack-session-kind" 2>/dev/null || echo interactive)
export GSTACK_STAMP_COMMIT_FULL="$COMMIT_FULL" GSTACK_STAMP_TREE="$TREE" GSTACK_STAMP_WTREE="$WTREE" GSTACK_STAMP_DIRTY="$DIRTY" GSTACK_STAMP_SESSION_KIND="$SESSION_KIND"
if [ "$INPUT" = --start ]; then
GSTACK_REVIEW_SKILL="${2:-}" bun -e '
GSTACK_REVIEW_SKILL="${2:-}" bun --no-env-file --config=/dev/null -e '
const { captureReviewStart } = await import(process.env.GSTACK_REVIEW_LIB);
console.log(captureReviewStart(process.env.GSTACK_REVIEW_SKILL));
'
exit $?
fi
if [ "$INPUT" = --check-shared-libs ] && [ "$#" -eq 2 ]; then
GSTACK_REVIEW_TOKEN="$2" bun -e '
GSTACK_REVIEW_TOKEN="$2" bun --no-env-file --config=/dev/null -e '
const { checkSharedLibsReuse } = await import(process.env.GSTACK_REVIEW_LIB);
console.log(JSON.stringify(checkSharedLibsReuse(JSON.parse(await Bun.stdin.text()), process.env.GSTACK_REVIEW_TOKEN)));
'
@@ -107,7 +107,7 @@ case "$INPUT" in --*) usage ;; esac
# Caller values for the binding keys are dropped before stamping. A binding or
# validation failure reaches stderr as its own line (never folded into
# "invalid JSON"), and exits 1 with nothing written.
STAMPED=$(printf '%s' "$INPUT" | GSTACK_REVIEW_TOKEN="$TOKEN" GSTACK_REVIEW_FINDINGS="$FINDINGS" bun -e "
STAMPED=$(printf '%s' "$INPUT" | GSTACK_REVIEW_TOKEN="$TOKEN" GSTACK_REVIEW_FINDINGS="$FINDINGS" bun --no-env-file --config=/dev/null -e "
const { bindReview } = await import(process.env.GSTACK_REVIEW_LIB);
let rec;
try { rec = JSON.parse(await Bun.stdin.text()); } catch (e) { console.error('gstack-review-log: invalid JSON, skipping (' + e.message + ')'); process.exit(1); }
+1 -1
View File
@@ -19,7 +19,7 @@ gstack_state_root_select
GSTACK_HOME="$_gstack_sr_root"
WTREE=$("$SCRIPT_DIR/gstack-wtree" 2>/dev/null || echo "unknown")
if [ -f "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" ]; then
GSTACK_REVIEW_LIB="$SCRIPT_DIR/../lib/review-evidence.ts" GSTACK_REVIEW_WTREE="$WTREE" bun -e '
GSTACK_REVIEW_LIB="$SCRIPT_DIR/../lib/review-evidence.ts" GSTACK_REVIEW_WTREE="$WTREE" bun --no-env-file --config=/dev/null -e '
const { reviewFreshness } = await import(process.env.GSTACK_REVIEW_LIB);
for (const line of (await Bun.stdin.text()).split("\n").filter(Boolean)) {
try {
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bun
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-runtime-pins — the doctor's `project pins` row (lib/runtime-pins.ts).
*
Loaded 100 of 420 files, more files were not shown because too many files have changed in this diff. Show more