mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 00:03:15 +01:00
v1.93.1.0 stamp: merge main 9d9d4c31bb into #3865 (land)
This commit is contained in:
commit
da34529dae
155 files changed
+7490
-703
No files matched your search
@@ -94,7 +94,7 @@ gstack/
|
||||
│ ├── test/ # Integration tests
|
||||
│ └── dist/ # Compiled binary
|
||||
├── agents-digest/ # Committed 2KB instruction-tier rules digest (gstack-AGENTS.md) for rules-reading hosts
|
||||
├── modules/ # loadouts.json (+ loadouts.schema.json): model rungs, pinned modules and named loadouts; read by scripts/modules.ts, rendered by bin/gstack-modules (docs/loadouts.md)
|
||||
├── modules/ # loadouts.json (+ loadouts.schema.json): model rungs, the model map, pinned modules and named loadouts; read by scripts/modules.ts and lib/model-detect.ts, rendered by bin/gstack-modules (docs/loadouts.md)
|
||||
├── extension/ # Chrome extension (side panel + activity feed + CSS inspector)
|
||||
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-lint/ = the static and render tiers behind bin/gstack-design-lint, ported from hallmark's sloplint; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
|
||||
│ └── diagram-render/ # Vendored mermaid + excalidraw runtimes, built into one offline bundle the renderer loads
|
||||
|
||||
@@ -154,7 +154,7 @@ CSS selectors. Always `snapshot -i` first, then use the refs.
|
||||
|
||||
Paired agents get `read+write+admin+meta` by default; the pairing ceremony is the trust boundary. `--restrict` narrows the list (it can never grant `control`). `--control` adds the control scope (`--admin` is a legacy alias). Over the tunnel, the `js`/`cookies`/`storage` commands are blocked by the command allowlist regardless of scope; `eval` works. Pair with `--restrict "read,write"` when the agent will read untrusted web content — scope caps the prompt-injection blast radius.
|
||||
|
||||
To tighten an already-paired agent, re-pair it with the **same `--client` name** and the narrower `--restrict`/`--domain`: a reducing re-pair revokes the previous session and releases its tabs immediately (the agent must reconnect with the new key), so the old wide access never lingers. Broadening or refreshing keeps the working session with no outage. Re-pairing without `--client` mints a new agent instead. `root` is a reserved client name.
|
||||
To tighten an already-paired agent, re-pair it with the **same `--client` name** and the narrower `--restrict`/`--domain`: a reducing re-pair revokes the previous session and releases its tabs immediately (the agent must reconnect with the new key), so the old wide access never lingers. Broadening or refreshing keeps the working session with no outage. Re-pairing without `--client` mints a new agent instead. `root` and names starting with `session:` (kept for local agent sessions) are reserved client names.
|
||||
|
||||
## Tab Isolation
|
||||
|
||||
|
||||
+38
-17
@@ -195,15 +195,18 @@ fallback; unknown files get 99th-percentile pessimism, and both full-suite and
|
||||
the long pole. Packed
|
||||
shards get duration-aware walls (`max(base, predicted × 3, files × 5s)`). The
|
||||
legacy `--shards N --shard i` path keeps stable hash indices. Required CI uses
|
||||
one duration-packed `--ci-plan`, 20 ordinary `--ci-run` shards plus a separate
|
||||
one duration-packed `--ci-plan`, 10 ordinary `--ci-run` shards plus a separate
|
||||
exclusive-fixture shard, and a `--ci-verify` aggregate. CI shards still run on
|
||||
independent machines without ordering unrelated jobs. Two workflow-level
|
||||
independent machines without ordering unrelated jobs. Three workflow-level
|
||||
constants in `free-tests.yml` set that size for every event: `FREE_SUITE_RUNNER`
|
||||
(`ubicloud-standard-4`) and `FREE_SUITE_SHARDS` (`'20'`), which `free-plan`
|
||||
resolves into the shards' runner and shard count. A `workflow_dispatch` run may
|
||||
override them with `free_runner` (`ubicloud-standard-2`, `-4` or `-8`) and
|
||||
`free_shards` (1–40) for runner-sizing experiments only; its job summary says
|
||||
so, and it is never cited as a PR or train Free Tests result. Each shard prints
|
||||
(`ubicloud-standard-8`), `FREE_SUITE_SHARDS` (`'10'`) and `FREE_SUITE_JOBS`
|
||||
(`'4'`, the [sub-shards](#sub-shards-inside-one-ci-job) each shard runs side by
|
||||
side), which `free-plan` resolves into the shards' runner, shard count and plan.
|
||||
GSTA-203 chose them by a 3-run A/B against 20 × `ubicloud-standard-4` at k = 1
|
||||
(the PR that set them has the table). A `workflow_dispatch` run may
|
||||
override them with `free_runner` (`ubicloud-standard-2`, `-4` or `-8`),
|
||||
`free_shards` (1–40) and `free_jobs` (1–8) for runner-sizing experiments only;
|
||||
its job summary says so, and it is never cited as a PR or train Free Tests result. Each shard prints
|
||||
`RESOURCES: peak_used_mib=… min_free_disk_mib=… samples=…` from a memory and
|
||||
disk sample every 2 seconds, even when the suite fails. The public
|
||||
`TREE_MUTATING` map now classifies exclusive host-state fixtures; its sole
|
||||
@@ -1553,7 +1556,7 @@ refresh to a maintainer.
|
||||
`scripts/ubicloud/setup-free-suite.sh` and the Windows seed through
|
||||
`windows-free-tests.yml`'s `record_durations` path. The recorder stays on
|
||||
`ubicloud-standard-8` while the free-suite shards run on `FREE_SUITE_RUNNER`
|
||||
(`ubicloud-standard-4`); both are Ubicloud standard runners. When either recording adds
|
||||
(`ubicloud-standard-8`); both are Ubicloud standard runners. When either recording adds
|
||||
or drops a test file, it force-updates the `bot/free-duration-seeds` branch,
|
||||
opens or refreshes one seed PR whose body states the ratchet result, and
|
||||
dispatches Free Tests, Windows Smoke and Quality gate on it. Those
|
||||
@@ -1575,14 +1578,24 @@ k sub-shards side by side on the same machine (`scripts/lib/shard-jobs.ts`). Eac
|
||||
sub-shard is an ordinary free shard child: one `bun test` process, serial
|
||||
inside, with its own sandbox, process-group kill and log. Bun's worker
|
||||
`--parallel` mode is still not used (decision V3/D6 in
|
||||
`scripts/test-free-shards.ts`). The default is `k = 1`, which runs today's one
|
||||
child, so no workflow changes behavior until it passes `--jobs`.
|
||||
`scripts/test-free-shards.ts`). Without `--jobs`, a shard runs at the plan's
|
||||
per-shard k (below); a shard with no planned k runs one child (`k = 1`).
|
||||
|
||||
- `--jobs` applies only to `--ci-run`. It takes a positive whole number up to the
|
||||
shard's file count (an empty shard takes 1); anything else stops with
|
||||
`FREE_JOBS_INVALID`. `GSTACK_FREE_JOBS` sets the local full-suite process count
|
||||
and never changes `--ci-run`. The exclusive host-state shard has one file, so
|
||||
a workflow passes it `--jobs 1` or no `--jobs`.
|
||||
- Workflows set k in the plan, per shard: `--ci-plan <plan> --shards N
|
||||
--shard-jobs k` stores `jobs = min(k, the shard's files that may run in
|
||||
parallel)` on each shard, and only when that is above 1. The exclusive
|
||||
host-state shard holds only `TREE_MUTATING` files, so it stays one child, and
|
||||
a plan made at k = 1 is byte-identical to one made without `--shard-jobs`.
|
||||
`--ci-run` runs the shard at the plan's k, and `--ci-verify` rejects any
|
||||
shard result that ran at a different k. `free-tests.yml` reads k from
|
||||
`FREE_SUITE_JOBS` and `windows-free-tests.yml` from `WINDOWS_FREE_JOBS`; the
|
||||
`free_jobs` and `windows_jobs` dispatch inputs override them for experiments
|
||||
only.
|
||||
- `--jobs` applies only to `--ci-run` and overrides the plan's k, for local
|
||||
reproduction. It takes a positive whole number up to the shard's file count
|
||||
(an empty shard takes 1); anything else stops with `FREE_JOBS_INVALID`, as
|
||||
does a bad `--shard-jobs` or one without `--ci-plan`. `GSTACK_FREE_JOBS` sets
|
||||
the local full-suite process count and never changes `--ci-run`.
|
||||
- Files are split into disjoint sub-shards packed by the duration seed. Each
|
||||
sub-shard has an id (`<plan id prefix>:<shard>.<n>`) bound to its file list.
|
||||
Files in `TREE_MUTATING` or `SLOW_UNDER_LOAD` run after the parallel
|
||||
@@ -1606,6 +1619,13 @@ reproduce command. If the file passes serially, add it to `SLOW_UNDER_LOAD` in
|
||||
`scripts/lib/shard-jobs.ts` with that run id and a reason. Never raise the
|
||||
timeout. `SLOW_UNDER_LOAD` is empty today.
|
||||
|
||||
The nightly Windows lane stays at `WINDOWS_FREE_JOBS: '1'`. In GSTA-203's A/B, k = 2
|
||||
made the slowest Windows shard faster, but two of three runs needed the flaky
|
||||
retry for file-lock cleanup errors (`EBUSY` in
|
||||
`browse/test/stealth-webdriver.test.ts` and a native fixture cleanup in
|
||||
`browse/test/cookie-import-native-job.test.ts`), which k = 1 never showed. Fix
|
||||
those cleanups before raising it.
|
||||
|
||||
Every `--ci-run` result, at any k, records per-file test and `expect()` counts
|
||||
in `outcome.counts` (`tests`, `pass`, `fail`, `skip`, `todo`, `expects` per
|
||||
file, plus shard totals). They come from bun's JUnit reporter, which the runner
|
||||
@@ -1624,8 +1644,9 @@ test list in its last step, where each file's reason sits beside it.
|
||||
`test/windows-native-workflows.test.ts` keeps that list Windows-safe and fast.
|
||||
|
||||
The curated suite below runs nightly on main and on dispatch, never on a PR.
|
||||
`windows-free-tests.yml` plans on Linux (`--windows-only --ci-plan --shards 6`),
|
||||
runs one strict serial shard per `windows-latest` job
|
||||
`windows-free-tests.yml` plans on Linux (`--windows-only --ci-plan --shards 6
|
||||
--shard-jobs $WINDOWS_FREE_JOBS`), runs one strict serial shard per
|
||||
`windows-latest` job
|
||||
(`bun run test:windows --ci-run`), and verifies every shard receipt on Linux
|
||||
(`--windows-only --ci-verify`). Packing uses Windows-measured durations from
|
||||
`scripts/free-test-durations-windows.json`; the weekly
|
||||
|
||||
@@ -472,6 +472,13 @@ make <dir> writable, or set GSTACK_STATE_ROOT to a writable directory
|
||||
(LAUNCH_FILE_UNWRITABLE)`, and a failing run prints no `GSTACK_LAUNCH_FILE` line
|
||||
and removes the file an earlier passing run left beside the verdict it rewrites
|
||||
(a profile check that keeps a fuller ok verdict leaves that verdict's file).
|
||||
With `--model <id>`, or `GSTACK_MODEL=<id>` in its environment (what the Capy
|
||||
skill passes), an ok run other than `--for review-voice` also prints `LOADOUT: …`
|
||||
and `SKILL_ROOT: <dir>` just before `GSTACK_LAUNCH`. For a loadout other than
|
||||
`full`, `GSTACK_LAUNCH_FILE` then names `<verdict file without .json>.loadout-<name>-<digest>.env`,
|
||||
a copy of the verdict's launch file that adds `GSTACK_LOADOUT_SKILL_ROOT`; the
|
||||
verdict and its own launch file do not change, and any failure prints
|
||||
`LOADOUT_FALLBACK` and serves `full` ([loadouts](loadouts.md#capy)).
|
||||
`. <that file>` at the start of a shell applies them without ever printing them,
|
||||
which is the only way to hand them to a child agent: pass the path, never the
|
||||
values. None of the assignments is a secret, but during GSTA-96's unattended
|
||||
|
||||
@@ -105,7 +105,7 @@ After weighing Codex's argument, we chose to roll back CEO EXPANSION and ship an
|
||||
}
|
||||
```
|
||||
|
||||
`signal_events` counts the logged answers that moved any dimension, and `contributing_events` counts, per dimension, the logged answers that moved it (an answer that writes two deltas to one dimension counts once), so a dimension with no evidence (0) is visible beside one whose evidence balances out; both print 0.5 (#2623). Each value is the sigmoid of its accumulated deltas, and each UTC day's sum per dimension (by the row's `ts`) is clamped to the largest single delta in the signal map (currently ±0.06), so one burst of answers can't pin a dimension; `--trace <dim>` lists every contributing answer, each clamped day and the total. The bound keys on the day, not on `session_id`: the PostToolUse hook and the agent log different ids for the same review (the host's conversation id and the preamble's run id), and an agent can leave the `SESSION_ID` placeholder unsubstituted, so no logged id reliably marks one review. The cost is that several reviews on one day count as one. `contributing_events` still counts an answer logged by both writers twice. Only an ISO 8601 date or date-time `ts` counts as dated (a time without a zone is read as UTC), every other row shares one undated bucket, and `diversity.days_span` counts the same UTC days. Nothing displays `contributing_events` yet (#3436).
|
||||
`signal_events` counts the logged answers that moved any dimension, and `contributing_events` counts, per dimension, the logged answers that moved it (an answer that writes two deltas to one dimension counts once), so a dimension with no evidence (0) is visible beside one whose evidence balances out; both print 0.5 (#2623). Each value is the sigmoid of its accumulated deltas, and each UTC day's sum per dimension (by the row's `ts`) is clamped to the largest single delta in the signal map (currently ±0.06), so one burst of answers can't pin a dimension; `--trace <dim>` lists every contributing answer, each clamped day and the total. The bound keys on the day, not on `session_id`: the PostToolUse hook and the agent log different ids for the same review (the host's conversation id and the preamble's run id), and an agent can leave the `SESSION_ID` placeholder unsubstituted, so no logged id reliably marks one review. The cost is that several reviews on one day count as one. `contributing_events` still counts an answer logged by both writers twice. Only an ISO 8601 date or date-time `ts` counts as dated (a time without a zone is read as UTC), every other row shares one undated bucket, and `diversity.days_span` counts the same UTC days. `--check-mismatch`, `--gap`, the stored `gap` and /plan-tune's profile view compare a dimension only when its `contributing_events` is at least 3; `--gap` lists the others under `low_evidence` and the stored `gap` omits them (#3436).
|
||||
|
||||
**Diversity check** (Codex #13): `inferred` is considered "enough data" only when `sample_size >= 20 AND skills_covered >= 3 AND question_ids_covered >= 8 AND days_span >= 7`. Below this, `/plan-tune profile` shows "not enough observed data yet" instead of a potentially-misleading inferred value.
|
||||
|
||||
|
||||
+49
-4
@@ -1,8 +1,8 @@
|
||||
# Module loadouts
|
||||
|
||||
gstack can render a smaller or larger set of its instructions for a given model. Each toggleable piece is a **module** with an id. A **loadout** is a named set of modules, and `modules/loadouts.json` maps each model **rung** to a loadout. Today every rung loads `full`, which is the same render gstack has always shipped. A loadout changes only when the shared skill eval (GSTA-2) measures a smaller one that matches `full` within its preregistered margin.
|
||||
gstack can render a smaller or larger set of its instructions for a given model. Each toggleable piece is a **module** with an id. A **loadout** is a named set of modules, and `modules/loadouts.json` maps each model **rung** to a loadout. Every rung loads `full`, which is the same render gstack has always shipped. A loadout changes only when a preregistered eval measures a smaller one that matches `full` within its margin.
|
||||
|
||||
Install-time model detection, which picks a loadout for the model you run, ships with the first loadout that is not `full`. Until then, loadouts are an eval tool: `bin/gstack-modules render` produces any loadout as a byte-exact tree with a lock file.
|
||||
One model has its own loadout: Claude Opus 5.5 gets `lite` ([below](#lite-on-claude-opus-55)). Every other model, and any model gstack is not told about, gets `full`. `bin/gstack-modules render` also produces any loadout as a byte-exact tree with a lock file for evals.
|
||||
|
||||
## Terms
|
||||
|
||||
@@ -12,6 +12,7 @@ Install-time model detection, which picks a loadout for the model you run, ships
|
||||
| Pinned | A module no loadout can remove where `full` renders it: the start command, plan-mode rules, the question format, completion status, the unattended profile, /ship's review army and adversarial pass, the router and `/gstack-upgrade`. A skill's `preamble-tier` still applies, so a tier-1 skill has no question-format section in any loadout. |
|
||||
| Rung | How much scaffolding a model gets: `frontier`, `strong`, `mid`, `light`, or `unknown`. Model ids are matched after lowercasing and stripping a `provider/` prefix (`anthropic/claude-haiku-5-5` is `mid`). Rungs are separate from the frontier/smart tiers in `lib/model-catalog.ts`, which choose which model runs a role. |
|
||||
| Loadout | `{ base, exclude, include, preamble_tier_max, note }`. `full` (include `*`) is always present. An unknown model gets `default.unknown`, which is `full`. |
|
||||
| Model map | `models` in `modules/loadouts.json`: an exact model id to a loadout, checked before rungs. The id also matches its dated snapshot (`-YYYYMMDD`) and `-latest`, never a point release or any other suffix (`claude-opus-5-5` matches `claude-opus-5-5-20261101`, not `claude-opus-5-50` or `claude-opus-5-5-1`). |
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -22,9 +23,11 @@ bin/gstack-modules render --host claude --loadout full --out-dir ~/arms/control
|
||||
bin/gstack-modules render --host claude \
|
||||
--modules 'full,-section:office-hours/second-opinion,-section:office-hours/visual-design' \
|
||||
--out-dir ~/arms/oh-lean
|
||||
bin/gstack-modules resolve --model claude-opus-5-5 # the loadout an install would serve, and why
|
||||
bin/gstack-modules install --full-root "$HOME/.claude/skills" --model claude-opus-5-5 # publish it and print SKILL_ROOT
|
||||
```
|
||||
|
||||
`render` writes the whole host tree into a staging directory beside `--out-dir`. It checks that no rendered file still points at an excluded module, writes `loadout.lock.json` last (loadout name or `adhoc`, the normalized spec and its sha256, host, model, every excluded id, and the sha256 and byte count of every file), then swaps the result into place. A failure leaves an earlier render at `--out-dir` untouched. Claude section links name `--out-dir`, so point the eval harness at that directory. Pass the same `--model`, `--install-root` and `--plan-mode` the install under test uses, so the arms differ only by the modules. Codex renders default to the host's generic `gpt` overlay, while `./setup` uses `gpt-6-astra`.
|
||||
`render` writes the whole host tree into a staging directory beside `--out-dir`. It checks that no rendered file still points at an excluded module, writes `loadout.lock.json` last (loadout name or `adhoc`, the normalized spec and its sha256, host, model, every excluded id, and the sha256 and byte count of every file), then swaps the result into place. A failure leaves an earlier render at `--out-dir` untouched. Claude section links name `--out-dir`, so point the eval harness at that directory. Pass the same `--model`, `--install-root` and `--plan-mode` the install under test uses, so the arms differ only by the modules. Codex renders default to the host's generic `gpt` overlay, while `./setup` uses `gpt-6-astra`. `--bind-model <id> --full-root <abs>` (both or neither, on a loadout that excludes something) bakes the start lines an install for that model serves ([below](#the-start-line-carries-the-model)).
|
||||
|
||||
`bun run gen:skill-docs --loadout <name> --out-dir <dir>` and `--modules <spec> --out-dir <dir>` apply the same filter to a generation run without the staging, dangling-reference check or lock file; a selection with exclusions and no `--out-dir` is refused with `MODULE_SPEC_INVALID`, because the committed tree is always the `full` render. A non-`full` render adds `<!-- loadout: <name> -->` to each SKILL.md header.
|
||||
|
||||
@@ -35,6 +38,48 @@ bin/gstack-modules render --host claude \
|
||||
3. Excluding a section removes its STOP pointer, its Section-index row and its file. The step becomes `> Not loaded in the <loadout> loadout: skip <trigger>. Do not offer it, and treat it as not run.` Skeleton text that belongs to the section, such as office-hours' Phase 3.5 offer, sits inside `{{IF_SECTION:<id>}}…{{/IF_SECTION}}` in the template and goes with it.
|
||||
4. The autoplan runner (`gstack-autoplan`) reads review methodology from the source tree, not from a rendered loadout, so autoplan-runner arms always measure `full` methodology.
|
||||
|
||||
## Lite on Claude Opus 5.5
|
||||
|
||||
`lite` is `full` minus eleven coaching pieces of the shared preamble: voice, writing style, completeness, confusion, evidence, context health, question tuning, search before building, repo mode, brain health and context recovery. It keeps every pinned piece, the upgrade check (the PROACTIVE and skill-prefix rules) and brain sync (the privacy stop-gate). Only `claude-opus-5-5` maps to it, because only Opus 5.5 has been measured: office-hours showed no loss on Opus 5.5 (GSTA-161 preamble run), and the land-and-deploy confirmation (GSTA-161, PR 1b) compared full, lite and no gstack on the same tasks. Haiku 5.5 leaned slightly against a larger cut on the same office-hours run, so small models stay on `full`. Two skills point at a preamble section lite drops: office-hours and plan-ceo-review name ETHOS.md through the "Search Before Building" section, so under lite they find ETHOS.md in the gstack checkout instead.
|
||||
|
||||
### How gstack picks a loadout
|
||||
|
||||
gstack never guesses the model. It uses, in order:
|
||||
|
||||
1. An explicit loadout: `./setup --loadout <name>` on Claude Code, or `bin/gstack-modules install --loadout <name>`. It is served as asked and bound to no model.
|
||||
2. An explicit model id: `--model <id>` on the Capy installer (`scripts/install.sh --model <id>`, which passes it to `bin/gstack-capy-install`), else `GSTACK_MODEL`, which the installer also reads.
|
||||
3. Nothing: `full`.
|
||||
|
||||
A model id is lowercased, its `provider/` prefix and a trailing `[…]` are dropped, and it must then be a plain id (`^[a-z0-9][a-z0-9._-]{0,79}$`). An unmapped id serves `full`; an id that is not plain also serves `full` and prints `LOADOUT_INVALID_MODEL`. Nothing reads Claude Code settings or an inherited loadout variable, so a Sonnet subagent of an Opus session gets `full` unless its own install names Opus.
|
||||
|
||||
### Capy
|
||||
|
||||
The Capy skill passes the thread's model id in the environment: `GSTACK_MODEL=<id> bash scripts/install.sh --check --for <skill>`. The variable, unlike `--model`, is safe on a checkout whose installer predates loadouts: that installer ignores it instead of refusing an unknown argument. The installer prints two lines before the launch lines:
|
||||
|
||||
```
|
||||
LOADOUT: lite model=claude-opus-5-5 source=GSTACK_MODEL digest=3f2a9c01d4e7 reused=yes
|
||||
SKILL_ROOT: /home/user/.gstack/loadouts/lite-3f2a9c01d4e7
|
||||
```
|
||||
|
||||
Read skills from `SKILL_ROOT` (`<root>/gstack/SKILL.md` routes, `<root>/<skill>/SKILL.md` runs) and source the printed `GSTACK_LAUNCH_FILE`, which adds `GSTACK_LOADOUT_SKILL_ROOT`. For `full`, `SKILL_ROOT` is `~/.claude/skills` and the launch file is the install verdict's own.
|
||||
|
||||
The lite tree is published by `bin/gstack-modules install` under `<state root>/loadouts/<loadout>-<digest12>/`, beside the full install; the checkout and its committed SKILL.md files stay the full render. The digest covers the selection, the bound model, the full root, host, overlay, plan mode and the source tree (the git HEAD tree plus any uncommitted change), so an upgrade or a different model gets a new folder and an unchanged one is reused after its files are re-hashed against `loadout.lock.json`. Each folder is a complete skill tree: the rendered SKILL.md and section files plus every tracked runtime asset of each skill (templates, references, scripts). A published folder is never replaced. Pruning removes only folders it published (a `<loadout>-<digest12>` name with an install lock) beyond the three most recently used per loadout, and never one used in the last day. When `--full-root` is not `~/.claude/skills`, the tree's command paths point at that install's `gstack` folder. Two Capy threads with different models on one machine share one install verdict and get one launch file each (`<verdict>.loadout-<name>-<digest>.env` for lite). Any failure serves `full` and prints `LOADOUT_FALLBACK`.
|
||||
|
||||
### The start line carries the model
|
||||
|
||||
A lite tree chosen for a model bakes that model and the full root into each skill's start command, so `gstack-skill-start` prints:
|
||||
|
||||
```
|
||||
LOADOUT: lite model=claude-opus-5-5 full_root=/home/user/.claude/skills
|
||||
LOADOUT_RULE: this file was rendered for claude-opus-5-5. If you are not running claude-opus-5-5, read /home/user/.claude/skills/qa/SKILL.md now and follow it instead of this file.
|
||||
```
|
||||
|
||||
When `GSTACK_MODEL` is set to a different model, it adds `LOADOUT_MISMATCH`. A preamble already read cannot be unread, so the remedy is always to read the full file. An explicit loadout (`./setup --loadout lite`) carries no model and prints neither line.
|
||||
|
||||
### Claude Code
|
||||
|
||||
`./setup --loadout lite` saves `claude_loadout` and renders the Claude skills into the untracked render dir (`<state root>/render/claude`) through setup's own render, the same path a pinned overlay or `plan_mode_preamble=off` takes, so later upgrades and `gstack-config gbrain-refresh` keep it. `./setup --loadout full` returns to the committed files. A plain `./setup` never selects lite.
|
||||
|
||||
## Changing a loadout
|
||||
|
||||
Add or edit an entry under `loadouts` and cite the GSTA-2 run (preregistration, margin, per-rung result) in its `note` and in the PR body. A model-specific loadout that wins a preregistered comparison is allowed even if it breaks the expected nesting (light ⊇ mid ⊇ strong). Committed SKILL.md files are always the `full` render, so the parity suite, the SKILL.md size guard, the size ledger and the catalog and context budgets keep measuring `full`.
|
||||
Add or edit an entry under `loadouts` (and `models`, for a model-specific one) and cite the measurement (preregistration, margin, per-rung result) in its `note` and in the PR body. A model-specific loadout that wins a preregistered comparison is allowed even if it breaks the expected nesting (light ⊇ mid ⊇ strong). Committed SKILL.md files are always the `full` render, so the parity suite, the SKILL.md size guard, the size ledger and the catalog and context budgets keep measuring `full`.
|
||||
+21
-1
@@ -1123,13 +1123,33 @@ publication whenever the managed pre-push hook (`gstack-redact
|
||||
install-prepush-hook`, no reinstall needed) or the standalone preflight runs:
|
||||
|
||||
```
|
||||
gstack-pregate sealed [--range <a..b>] [--remote origin] [--base <branch>] [--json]
|
||||
gstack-pregate sealed [--range <a..b>] [--remote origin] [--base <branch>] [--no-skip] [--json]
|
||||
```
|
||||
|
||||
- **Boundary.** Every commit the push publishes: `git rev-list <local sha>
|
||||
--not --remotes=<remote>` (and the remote tip when the ref exists); a
|
||||
destination with no refs means everything reachable. `--range` names the
|
||||
commits explicitly.
|
||||
- <a id="sealed-skip"></a>**Skipped commits.** When the base branch resolves, a
|
||||
listed commit is not rescanned when the destination already holds it or its
|
||||
patch already landed. Held: an ancestor of the destination's own
|
||||
`refs/pull/<n>/head` for a squash commit `(#n)` on the base since the
|
||||
merge-base; the subject is only a hint, the fetched ref is the proof. That
|
||||
lookup lists and fetches at most 50 refs within 20 seconds (receipted, sink
|
||||
`sealed-guard`) and deletes the fetched refs afterwards. Landed: the commit's
|
||||
own `git patch-id --verbatim` equals a base commit's since the merge-base and
|
||||
so does its diff, apart from blob ids and line numbers; a merge never matches,
|
||||
and above 2,000 base or 400 listed commits this check is off. A landed commit
|
||||
skips only the path and text rules: `sealed.label_join` judges the whole file
|
||||
at that commit, which the base may never have held, so it still runs. Nothing
|
||||
matches a combined diff, so a parent whose commits were never published is
|
||||
scanned whole even when its squash landed. A lookup that fails, runs out of
|
||||
time or exceeds a limit prints one line and scans everything. Each PR group
|
||||
and landed commit is listed (`skipped <n> commit(s) the destination already
|
||||
has as refs/pull/<n>/head`, `skipped commit <sha> (its patch landed on the
|
||||
base as <sha>)`), the `SEALED:` line counts them as `skipped=<n>` (its
|
||||
`commits=` counts the commits scanned), and `--no-skip` scans every listed
|
||||
commit. The pre-push hook and `gstack-pregate sealed` skip the same way.
|
||||
- **Rules.** `sealed.path` and `sealed.transcript` check every path a commit
|
||||
adds, renames or copies, whatever its content (binary, empty, deleted again
|
||||
in a later commit); `sealed.answer`, `sealed.seed` and `sealed.local_path`
|
||||
|
||||
+20
-2
@@ -429,7 +429,14 @@ rules hash, base and registry commits, the gate scan bound to the head and the r
|
||||
the queue refuses a receipt below the depth its head needs
|
||||
([ship-policy.md#ship-receipt](ship-policy.md#ship-receipt)). A stop the owner
|
||||
accepted after a fix-check edit goes in `review_loop_exception`, never in prose
|
||||
under `REVIEW_LOOP:`.
|
||||
under `REVIEW_LOOP:`. Step 16 takes the same record:
|
||||
`gstack-ship-release finish --review-loop-exception <json|@file>` routes the
|
||||
paths the branch touched at `fix_check.head` (narrowed to the record's
|
||||
`fix_check.files` when it lists them) and changed since as `accepted=` in
|
||||
`ROUTE:`, prints them on a `REVIEW_LOOP_EXCEPTION:` line, and carries them
|
||||
through the docs stage. A path outside that scope and a gate-edited test still
|
||||
route as behavior, with `--reviewed-base` (as /ship passes it) so does a file
|
||||
too large for the scoped review, and without the record nothing changes.
|
||||
|
||||
The receipt's summary line prints `passes=<n> blocking_found=<k> deferred=<m>`,
|
||||
which is what a scorecard tracks (target: a median of 1 pass per PR). A
|
||||
@@ -572,7 +579,18 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
missing coverage leaves the review incomplete. A mismatched send re-sent
|
||||
byte-exact before anything was collected is void and counted (`voided=N` on the
|
||||
verdict line); one that produced an output stays `MISMATCH`. Then output hashes
|
||||
against the merged lines they produced, and the judgment rerun against the
|
||||
against the merged lines they produced, and each merged line against the finding collect
|
||||
parses at that id's index from the unchanged output file, label and raw text byte for byte
|
||||
(`merged_line_differs` on the OUTPUT and JUDGE rows otherwise, GST-541), so an edited
|
||||
merged.jsonl cannot lower a finding or swap its text. A recorded output file that is missing
|
||||
(`output_file_missing`), a judgment run on a merged file collect did not write
|
||||
(`judge_merged_not_collected`) and merged rows collect never recorded
|
||||
(`merged_rows_not_collected`) are dirty too, and so is an output whose re-parsed findings
|
||||
outnumber or undercount the ids collect recorded (`merged_count_differs`). The one exception
|
||||
is a reply collect refused for its tree: verify re-checks the reply's `REVIEWED_TREE` line
|
||||
against the render's tree, and such an output stays `NOT_REVIEWED` until a re-sent brief is
|
||||
answered by a reply that passes the same check. Deleting a whole output event with its rows is
|
||||
not caught yet, because the ledger has no anchor for it. The judgment is then rerun against the
|
||||
recorded judged set, covering every collected id. A ledger from a run that kept no
|
||||
prompts prints `VERIFY: no brief records` (exit 3), never clean.
|
||||
6. `pass --base <b> --out <dir> --pass <id> -- <plan flags>` runs one pass from a
|
||||
|
||||
+1
-1
@@ -656,7 +656,7 @@ I want the model imagining the production incident before it happens.
|
||||
|
||||
When something is broken and you don't know why, `/investigate` is your systematic debugger. It follows the Iron Law: **no fixes without root cause investigation first.**
|
||||
|
||||
Instead of guessing and patching, it traces data flow, matches against known bug patterns, and tests hypotheses one at a time. If three fix attempts fail, it stops and questions the architecture instead of thrashing. This prevents the "let me try one more thing" spiral that wastes hours.
|
||||
It leaves the order and tools to the model, keeps a few guards (stop and ask after three failed hypotheses, ask before a fix touches more than five files, lock edits to the affected module), reads a recipe for flakes, order-dependent failures, races and eval regressions, and ends with a structured debug report. It used to walk a five-phase procedure; Opus 5.5 followed none of it in 46 recorded sessions, so the procedure was dropped (GST-938).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ Beyond the slash-command skills, gstack ships standalone CLIs for workflows that
|
||||
| `gstack-memorable` | **Memorable recall bridge (opt-in, third party, Claude Code only)** — connects Claude Code to the external [Memorable](https://memorable.sh) CLI *through gstack* instead of the vendor's own installer, so the hook gets gstack's guarantees: an explicit consent key (`memorable_recall`, off by default, listed by `gstack-egress grants`), a fail-closed egress receipt for every prompt handed over (`gstack-egress list --sink memorable-recall`), a HIGH-tier secret pre-scan, a trust envelope and 8 KiB cap on whatever comes back, an allowlisted environment and process-group containment for the vendor process, and clean removal. `enable` registers the hook at the stable install with a 5 s timeout and never runs the vendor's own consent command; `disable` revokes the gate first and removes the entry by identity even after Claude Code strips the tag; `status` is read-only. gstack never installs Memorable, and what its binary sends is the vendor's claim, not gstack's. Not available on Windows yet. [Full guide](memorable-workflow-memory.md). |
|
||||
| `gstack-decision-trail` | **Decision trail** — append one row (ts, phase, decision, why, evidence, result) to a run's TSV trail so a reviewer reads what was chosen and why without the transcript. Header once, append-only, cells flattened to one line, and a cell starting with `=`, `+`, `-` or `@` quoted so a spreadsheet cannot run it. `start <file> <run-id>` opens a run's stretch of a shared trail (idempotent; `start` is reserved as a phase name), and `--run <id>` refuses an append until that start row is the newest. Ported from pstack's `log.sh` (MIT, NOTICE.md). [Rule](unattended.md#the-decision-trail). |
|
||||
| `gstack-worktree-audit` | **Worktree audit** — read-only table of every git worktree beside the main one, largest first: size, age, merged into the base or not, `clean`/`clean,ignored:<n>`/`wip:<n>`/`scratch:<n>`, `pushed`/`ahead<n>`/`no-remote`/`detached`, the PR on its branch through `gh` (fork PRs ignored), the newest Claude Code chat in it, and a bucket (`prunable` for an unlocked registered worktree whose directory is gone, `review` for a locked or `broken` one, `hold-wip`, `hold-open-pr`, `verify-recent-chat`, `safe` only for a clean tree that is merged into the base or whose PR is closed or merged with every commit pushed, else `review`). `-b origin/<branch>` sets the base, `--no-fetch` skips the best-effort fetch of the base ref (its one write). Never deletes. Ported from pstack's `worktree-audit.sh` (MIT, NOTICE.md). [Guide](CONTRIBUTOR_REFERENCE.md#auditing-conductor-worktrees). |
|
||||
| `gstack-clean` | **Per-repo `.gstack/` cleanup** — run inside a repository to see what it would reclaim from that repository's `.gstack/` folder; `--apply` does it. It removes `tmp/codex-*` scratch files older than 7 days, cuts the browse logs (`browse-console.log`, `browse-network.log`, `browse-dialog.log`, `browse-daemon.log`, `browse-startup-error.log`) to their last 256 KB once they pass 1 MB, and trims `browse-audit.jsonl` only with `--audit-days N`. It never touches `browse.json`, `session-state.json`, `chromium-profile/`, `browser-skills/` or `browse-states/`, skips browse files while the daemon runs and `tmp/` while codex runs, and refuses (exit 3) outside git, when `.gstack` is a symlink, when `BROWSE_STATE_FILE` names another folder, and when the folder is, holds or sits inside the gstack state root. `--json` prints one versioned object. Exit 0 done, 1 an action failed (rerunning is safe), 2 usage, 3 refused. [Error codes](troubleshooting.md#gstack-clean-usage). |
|
||||
| `gstack-wtree` | **Working-tree fingerprint** — prints a content hash of what's actually on disk (temp index seeded from the stat cache, ~40x cheaper than a full re-hash; untracked source counts, gitignored scratch doesn't). Identical content fingerprints identically through commits, rebases, amends, and squashes — it's what binds reviews and test evidence to content instead of commit SHAs. |
|
||||
| `gstack-review-log` | **Review-pass receipts** — `--start <skill>` captures the working-tree fingerprint before a diff review; `'<JSON>' --finish <token>` consumes that single-use, repository/branch/skill-scoped receipt. Binding requires matching start/end content and reviewer-reported `completed:true` and `converged:true`; it is not independent proof that a model read the source. `--check-shared-libs <token>` reads a current finding as JSON on stdin and checks prior Skip decisions against the actual branch, capture and eligible raw source blobs. It returns `reusable:false` when proof is missing or unsafe, including older records without logger-versioned coverage. Final review logging computes shared-code fingerprints and coverage rather than trusting supplied proof. |
|
||||
| `gstack-review-read` | **Review freshness** — emits review records with computed `review_freshness.status` and `reason`: CURRENT, STALE, or UNVERIFIED for diff reviews. `/ship` and `/land-and-deploy` use the same grade; a matching commit alone never certifies a diff review. [Dashboard rules](skills.md#review-readiness-dashboard). |
|
||||
|
||||
+125
-1
@@ -2184,6 +2184,78 @@ command again.
|
||||
|
||||
**Fixed in.** The first release after v1.91.101.0 (#3167).
|
||||
|
||||
<a id="browse-session-invalid"></a>
|
||||
### ``browse: the session name is not 1-64 letters, digits, dots, colons, underscores or dashes ("<name>"); fix: pick a name like agent-1 ... (BROWSE_SESSION_INVALID)``
|
||||
|
||||
**Meaning.** `BROWSE_SESSION` or `--session` holds a name browse will not use
|
||||
as a tab owner.
|
||||
|
||||
**Fix.** Use a short name such as `agent-1`. An empty `BROWSE_SESSION=` turns
|
||||
sessions off and uses the shared active tab.
|
||||
|
||||
<a id="browse-session-root-only"></a>
|
||||
### ``browse: only the local root token may name a session ... (BROWSE_SESSION_ROOT_ONLY)``
|
||||
|
||||
**Meaning.** A paired or scoped agent sent a session name. Sessions are for
|
||||
local agents that share the root token; scoped agents already get their own
|
||||
tabs.
|
||||
|
||||
**Fix.** Run `newtab` and pass the returned `tabId` on every command.
|
||||
|
||||
<a id="browse-session-tab-reset"></a>
|
||||
### ``browse: this session's tab was closed ... (session <name>'s tab <id> was closed; it was on <url>); fix: open the page again ... (BROWSE_SESSION_TAB_RESET)``
|
||||
|
||||
**Meaning.** The tab this session was working in is gone: someone ran
|
||||
`closetab` on it, closed the window, or it sat idle past
|
||||
`BROWSE_SESSION_IDLE_MINUTES` (default 120). browse refuses reads instead of
|
||||
answering from a blank page.
|
||||
|
||||
**Fix.** `browse goto <url>` in the same session opens a new tab for it, then
|
||||
`snapshot` again before using `@refs`.
|
||||
|
||||
<a id="browse-session-limit"></a>
|
||||
### ``browse: every session slot is running a command ... (BROWSE_SESSION_LIMIT)``
|
||||
|
||||
**Meaning.** `BROWSE_MAX_SESSIONS` sessions (default 16) are all mid-command,
|
||||
so browse could not close an idle one to open a tab for a new session.
|
||||
|
||||
**Fix.** Retry in a moment, or raise `BROWSE_MAX_SESSIONS`.
|
||||
|
||||
<a id="browse-device-unknown"></a>
|
||||
### ``browse: no Playwright device has that name, so nothing on the tab changed ("<name>"; nearest: ...); fix: $B device --list (BROWSE_DEVICE_UNKNOWN)``
|
||||
|
||||
**Meaning.** `$B device <name>` matches names from Playwright's `devices`
|
||||
table, case-insensitively. The name you gave is not in it, so the tab was not
|
||||
touched. The line lists the three nearest names.
|
||||
|
||||
**Fix.** `$B device --list` prints every name; quote names with spaces
|
||||
(`$B device "iPhone 15"`). Add `--landscape` for the rotated profile.
|
||||
|
||||
**Fixed in.** The first release after v1.92.61.0 (#3058).
|
||||
|
||||
<a id="browse-device-cdp-failed"></a>
|
||||
### ``browse: Chromium refused a device emulation call on this tab (<method>: <error>; the tab is partly emulated); fix: $B device off (BROWSE_DEVICE_CDP_FAILED)``
|
||||
|
||||
**Meaning.** Phone emulation is three DevTools calls on the tab's own session
|
||||
(size, touch, user agent). One failed; the line names it and says whether the
|
||||
calls before it already changed the tab.
|
||||
|
||||
**Fix.** `$B device off` clears all three, then try the device again. If it
|
||||
keeps failing, the tab or browser is gone: `$B restart`.
|
||||
|
||||
**Fixed in.** The first release after v1.92.61.0 (#3058).
|
||||
|
||||
<a id="browse-touch-no-device"></a>
|
||||
### ``browse: touch input needs a phone profile on this tab first ($B tap on this tab); fix: $B device "iPhone 15" (BROWSE_TOUCH_NO_DEVICE)``
|
||||
|
||||
**Meaning.** `tap`, `swipe`, `pinch` and `touch` send real touch events,
|
||||
which pages only see on a touch device. The current tab has no phone profile.
|
||||
|
||||
**Fix.** `$B device "iPhone 15"` (or any name from `$B device --list`) on
|
||||
the same tab, then repeat the touch command.
|
||||
|
||||
**Fixed in.** The first release after v1.92.61.0 (#3058).
|
||||
|
||||
---
|
||||
|
||||
## Windows
|
||||
@@ -3317,6 +3389,19 @@ work: they are how a budget-stopped CEO-only run recovers.)
|
||||
full review in a new directory: `gstack-autoplan next --out <new dir> --plan
|
||||
<file>` without `--ceo-only`.
|
||||
|
||||
<a id="shadow-before-answer"></a>
|
||||
### `SHADOW_BEFORE_ANSWER`
|
||||
|
||||
**Meaning.** `gstack-autoplan shadow` skipped a gate revision whose plan
|
||||
approval (`p1`) is unanswered or was answered `c` (interrogate). The export
|
||||
pairs each hidden prediction with the owner's answer, so a revision he has not
|
||||
answered has nothing to pair it with. Answered revisions of the same run still
|
||||
export; the command exits 3 only when nothing was exported.
|
||||
|
||||
**Fix.** Answer the gate (`gstack-autoplan answer --out <dir> --gate-rev <n>
|
||||
...`), then run `gstack-autoplan shadow` again. See
|
||||
[Shadow mode](unattended.md#shadow-mode).
|
||||
|
||||
<a id="attempt-mismatch"></a>
|
||||
### `ATTEMPT_MISMATCH`
|
||||
|
||||
@@ -6245,7 +6330,7 @@ QM registration does not work yet. QM's skill-pack import clones the whole repos
|
||||
|
||||
## Module loadouts (gstack-modules)
|
||||
|
||||
These codes come from `bin/gstack-modules` and `gen-skill-docs --loadout/--modules` (guide: [loadouts.md](loadouts.md)). Each one exits 2 before anything is written, and none falls back to the `full` loadout: an eval arm that silently rendered `full` would measure the control.
|
||||
These codes come from `bin/gstack-modules` and `gen-skill-docs --loadout/--modules` (guide: [loadouts.md](loadouts.md)). Each one exits 2 before anything is written, and none falls back to the `full` loadout: an eval arm that silently rendered `full` would measure the control. The exceptions are the install-time notices `LOADOUT_INVALID_MODEL`, `LOADOUT_MISMATCH` and `LOADOUT_FALLBACK` (from `bin/gstack-modules install`, the Capy installer and `gstack-skill-start`): an install never fails on them and serves `full` instead.
|
||||
|
||||
<a id="loadouts-invalid"></a>
|
||||
### `LOADOUTS_INVALID: modules/loadouts.json …`
|
||||
@@ -6257,6 +6342,21 @@ These codes come from `bin/gstack-modules` and `gen-skill-docs --loadout/--modul
|
||||
|
||||
**Meaning.** `--loadout` or the first token of `--modules` names no loadout in `modules/loadouts.json`. **Fix.** Use a name from the `valid:` list (`bin/gstack-modules list` prints them); `full` is always present.
|
||||
|
||||
<a id="loadout-invalid-model"></a>
|
||||
### `LOADOUT_INVALID_MODEL: "<id>" is not a plain model id; using full`
|
||||
|
||||
**Meaning.** The model id given to the installer (`--model`) or in `GSTACK_MODEL` is not a plain id after normalizing (lowercase letters, digits, `.`, `_` and `-`, at most 80 characters), so it never reaches a launch file or a start line. The full skills are served; nothing failed. **Fix.** Pass the model id as your model list names it, for example `--model claude-opus-5-5` or `--model anthropic/claude-opus-5-5` ([loadouts](loadouts.md#how-gstack-picks-a-loadout)).
|
||||
|
||||
<a id="loadout-mismatch"></a>
|
||||
### `LOADOUT_MISMATCH: <baked id> vs <running id>: read <full root>/<skill>/SKILL.md now`
|
||||
|
||||
**Meaning.** The skill you opened is the lite render made for one model, and `GSTACK_MODEL` says this session runs another. Lite is measured only on the baked model. **Fix.** Read the named full SKILL.md and follow it instead of the lite one. To stop it recurring, run the installer with this session's model (`scripts/install.sh --model <id>`) and read skills from the `SKILL_ROOT` it prints.
|
||||
|
||||
<a id="loadout-fallback"></a>
|
||||
### `LOADOUT_FALLBACK: <reason>; serving full`
|
||||
|
||||
**Meaning.** A lite tree could not be published (a render or dangling-reference failure, an unwritable state root, or a published folder whose files no longer match its `loadout.lock.json`), so the installer served the full skills and printed `SKILL_ROOT` for them. Nothing else changed. **Fix.** Fix the named reason; for a folder whose files changed, delete `<state root>/loadouts/<loadout>-<digest>` and rerun the installer, which publishes it again.
|
||||
|
||||
<a id="module-spec-invalid"></a>
|
||||
### `MODULE_SPEC_INVALID: …`
|
||||
|
||||
@@ -6286,3 +6386,27 @@ These codes come from `bin/gstack-modules` and `gen-skill-docs --loadout/--modul
|
||||
### `RENDER_DEST_NOT_OWNED: <dir> exists and holds no loadout.lock.json`
|
||||
|
||||
**Meaning.** `--out-dir` is a non-empty directory that an earlier `gstack-modules render` did not write, so replacing it could delete someone's files. **Fix.** Pass a new or empty directory, or the directory of an earlier render.
|
||||
|
||||
## Per-repo cleanup (gstack-clean)
|
||||
|
||||
These codes come from `bin/gstack-clean`, which reclaims old codex scratch in `<repo>/.gstack/tmp/`, browse logs over 1 MB and, with `--audit-days N`, old browse audit entries. It changes nothing unless you pass `--apply`, and `gstack-clean --help` lists what it touches.
|
||||
|
||||
<a id="gstack-clean-usage"></a>
|
||||
### `gstack-clean: an unknown flag or a bad --audit-days value … (GSTACK_CLEAN_USAGE)`
|
||||
|
||||
**Meaning.** A flag is not one of `--apply`, `--audit-days N`, `--json` or `--help`, or `--audit-days` got something other than a whole number of days from 1 to 99999. The line quotes the bad value and the last line is the usage line. Exit 2; nothing was read or changed. **Fix.** `gstack-clean --audit-days 30`, for example.
|
||||
|
||||
<a id="gstack-clean-target-refused"></a>
|
||||
### `gstack-clean: the folder to clean is not a safe per-repo .gstack folder … (GSTACK_CLEAN_TARGET_REFUSED)`
|
||||
|
||||
**Meaning.** gstack-clean only cleans `<git toplevel>/.gstack` for the directory you run it in. It refuses outside a git checkout, when `.gstack` is a symlink, when `BROWSE_STATE_FILE` points at a different folder's `browse.json`, and when the folder is, holds or sits inside the gstack state root (`~/.gstack` by default), where your learnings and settings live. Exit 3; nothing was touched. **Fix.** Run it from inside the repository, unset `BROWSE_STATE_FILE` (or point it at this repository's `.gstack/browse.json`), and replace a symlinked `.gstack` with a real folder.
|
||||
|
||||
<a id="gstack-clean-partial"></a>
|
||||
### `FAILED <path> <errno>` … `(GSTACK_CLEAN_PARTIAL)`
|
||||
|
||||
**Meaning.** One or more removals or cuts failed, usually `EACCES` or `EPERM` (a read-only file or folder) or `EBUSY` (another program holds the file). Every other action finished. Exit 1. **Fix.** Rerunning is safe: fix the permissions on each FAILED path, or wait for the program holding it to finish, then run `gstack-clean --apply` again.
|
||||
|
||||
<a id="gstack-clean-internal"></a>
|
||||
### `gstack-clean: unexpected gstack-clean failure … (GSTACK_CLEAN_INTERNAL)`
|
||||
|
||||
**Meaning.** gstack-clean hit an error it does not expect; the line names it. Exit 1. Changes made before the error stay made, and each was checked right before it was made. **Fix.** Rerun once; if it repeats, report the printed error to the gstack maintainers.
|
||||
+90
-1
@@ -268,7 +268,8 @@ tree no review saw, so /ship runs again;
|
||||
| File | Schema | Contents |
|
||||
|------|--------|----------|
|
||||
| `run.json` | `run` v1 | The manifest: `run` id, `status`, `required_phases`, each phase's `native` and `outside` outcome (`status`, `model`, `provider`), input `snapshot` hashes, `counts`, every artifact's `path` (relative to the run directory) and `sha256`, `session_kind`, `gate_rev`, `deviations`, `consumed_by`. |
|
||||
| `decisions.jsonl` | `decisions` v1 | One row per decision: `id` (`<run>-<label>` at gate revision 1, `<run>-r<rev>-<label>` after a reopen, since labels restart at each gate), `title`, `options`, `recommended`, `kind` (`auto`, `approval`, `user_challenge`), `status` (`pending`, `approved`, `overridden`, `rejected`, or `auto_kept` in a [CEO-only run](#ceo-only-runs)), `gate_rev`, and once answered `chosen`, `answered_at`, `reply`. |
|
||||
| `decisions.jsonl` | `decisions` v1 | One row per decision: `id` (`<run>-<label>` at gate revision 1, `<run>-r<rev>-<label>` after a reopen, since labels restart at each gate), `title`, `options`, `recommended`, `kind` (`auto`, `approval`, `user_challenge`), `status` (`pending`, `approved`, `overridden`, `rejected`, or `auto_kept` in a [CEO-only run](#ceo-only-runs)), `gate_rev`, and once answered `chosen`, `answered_at`, `reply`, plus how the owner answered: `owner_mode` (`rec`, `rec_except`, `tokens`, `as_is` or `unparsed`, parsed from the owner's own words), `owner_named` (his words named this item), `rec_hash` (the recommendations answered against) and `answered_role` (`human`, or `agent` relaying his words). |
|
||||
| `shadow/` | private, not an artifact | [Shadow mode](#shadow-mode) only: `ceo-only.r<rev>.jsonl` (one hidden prediction per gate item, written before the answer), `r<rev>.prompt.md` and one `<call>.reply.txt` per model call, the audit `salt`, and `r<rev>.pending` while a call is in flight. Never in `export --public`, the gate render or the owner brief. |
|
||||
| `findings.jsonl` | `findings` v1 | One row per reviewer finding: `id`, `phase`, `voice` (`native`/`outside`), `model`, `severity`, `title`, `disposition`, `plan_items`, `native_counterpart` (string or null), `source`. Rows share the review-log row shape, so `gstack-review-log --findings` derives review status from this file. |
|
||||
| `tasks.jsonl` | `tasks` v2 | Implementation tasks with `id`, `title`, `status`, `depends_on`, `findings` (ids in `findings.jsonl`), `blocked_by`, `acceptance`, optional `pr`/`item`/`tier`. |
|
||||
| `timing.json` | `timing` v1 | Per-phase `started_at`, `ended_at`, `wall_s`, `outside_s`, `native_s`; `started_at` and `total_wall_s` for the run; the Phase 0 `estimate`. |
|
||||
@@ -660,6 +661,94 @@ approves, so `gstack-artifact validate` accepts the run as it stands.
|
||||
does: `budget` then `resume`, or `close --phase ceo --deferred`, which ends
|
||||
it `reviewed`.
|
||||
|
||||
### Shadow mode
|
||||
|
||||
Shadow mode measures how often a model decider would pick what the owner
|
||||
picks, before the CEO-only mode is allowed to decide for him. In a driver run
|
||||
that turned it on, every gate the driver publishes gets one hidden prediction
|
||||
per question, written before anyone answers: `action` (`decide`,
|
||||
`send_back` or `hold`), `pick` (one of that item's options), `confidence`
|
||||
(0 to 1), `cites` (finding ids from that item) and `reason` (at most 400
|
||||
characters). The owner still gets the gate and answers it as usual; nothing
|
||||
routes differently. Taste-replay round 2 (R2, in gstack-evals) compares the
|
||||
predictions with his answers.
|
||||
|
||||
- **Off by default.** A crew turns it on with `gstack-config set
|
||||
autoplan_shadow on`; `GSTACK_AUTOPLAN_SHADOW=1` or `0` overrides it for one
|
||||
run. It is read once, at the run's first `next`, and stored in `run.json`
|
||||
(`shadow`), so a resumed run cannot flip it. The first `next` prints
|
||||
`SHADOW: on model=<m> ...`, or `SHADOW: off (<reason>)` when it was asked for
|
||||
but cannot run: no API key for the model's family, a model with no price in
|
||||
`lib/pricing.ts`, `--no-outside`, or a CEO-only run. R2's collection window
|
||||
starts at the first recorded row on a crew gate, not at this release: check
|
||||
one `SHADOW: recorded` line on a real gate before counting days.
|
||||
- **The model and the cost.** `autoplan_shadow_model` (default
|
||||
`claude-opus-5-5`) gets one call per gate: about $0.07 on your own API key.
|
||||
The call sends the reviewed plan's Implementation plan section (cut to 40,000
|
||||
characters) and the gate questions to that provider, with an egress receipt
|
||||
of sink `autoplan-shadow` and consent `autoplan_shadow on`. Each call is
|
||||
reserved against `--spend-cap` under its own ledger id labeled `shadow` and
|
||||
shows as `shadow=<usd>` on `GATE_SPEND`; it never changes the per-voice price
|
||||
on `GATE_REC_COST`. When the cap cannot cover a call and its retry, the
|
||||
shadow is skipped (`SHADOW: failed budget`).
|
||||
- **The decider.** `autoplan/shadow-decider.md` is a frozen, hand-written
|
||||
prediction prompt (it is not generated from the CEO-only section). It shows
|
||||
the reviewers' recommendation as one labeled input and gives the six decision
|
||||
principles as the owner's tendencies, not as rules, so the model can
|
||||
disagree with the recommendation. Its sha256 and version are on every row;
|
||||
an edit bumps both.
|
||||
- **The reply.** Exactly one JSON object per question. A duplicate, missing
|
||||
or invalid object marks that item malformed and the page is asked once more;
|
||||
items still malformed are recorded malformed with no pick. A reply cut at the
|
||||
4,000-token output limit is asked once more at 8,000 instead. One 90-second
|
||||
deadline covers both calls.
|
||||
- **When it runs.** Every driver command that prints a gate (`next`,
|
||||
`submit`, `close`, `resume`, `revise`, `run` and `check`) runs the
|
||||
shadow first, prints `SHADOW: recorded model=<m> usd=<x>` or `SHADOW: failed
|
||||
<code>` (status only, never a pick), then the gate, then checkpoints, so a
|
||||
checkpoint taken at a gate holds its record. A whole-gate carry asks nothing,
|
||||
so it gets no shadow. Interactive `/autoplan` and CEO-only runs write none.
|
||||
A shadow failure (`budget`, `no_key`, `timeout`, `http`, `network`,
|
||||
`truncated`, `unsafe_dir`, `locked`, `internal`) never blocks the gate or the answer. A
|
||||
failure while publishing the record (`locked` or `internal`) is journaled
|
||||
(`shadow_failed`) under the run lock and never throws; if the lock stays
|
||||
taken, the lease expires and the next command settles that revision.
|
||||
- **The lease.** The run lock is not held during the call: the command takes
|
||||
a lease (`shadow/r<rev>.pending`, with its pid, host and deadline) under the
|
||||
lock, calls the model, then retakes the lock (waiting up to 30 seconds) to
|
||||
publish. Another writer that finds a live lease prints `SHADOW: pending` and
|
||||
pays nothing, and `answer` waits for the record. A lease whose process died
|
||||
or whose deadline passed is a crash: its sent calls settle `unknown` at
|
||||
their reservation (they may have been billed), and `answer` records that
|
||||
revision first with every row `retrospective: true`, which R2 excludes from
|
||||
prospective evidence.
|
||||
- **The record.** `shadow/ceo-only.r<rev>.jsonl`, one row per gate item: the
|
||||
frozen item, `item_class` (`challenge2`, `challenge1`, `disagree`, `agent`,
|
||||
`cap_deferred`, `cap_blocker`, `cap_delta`, `finding`, `p1`), `ceo_eligible`
|
||||
(false for a `finding` item, which a CEO-only run leaves to the owner), the
|
||||
prediction, `rule_pick` (the driver's keep-direction pick, the free
|
||||
baseline), `rec_hash`, the plan and prompt hashes, the model, the gstack
|
||||
version, the page's tokens and cost, and `audit`. It is written once per
|
||||
revision with a link, so a second writer never overwrites it, and its sha256
|
||||
is journaled (`shadow_recorded`). `gstack-artifact validate <run.json>`
|
||||
checks each record against that hash and its schema, and reports a
|
||||
journaled revision whose record file is missing as `ARTIFACT_STALE`.
|
||||
- **The 10% audit mark.** A `decide` row carries `audit: true` when
|
||||
sha256(salt + decision id) mod 1000 < 100, with the salt drawn once per run
|
||||
into `shadow/salt`. Nothing routes on it yet; the live switch that lets the
|
||||
mode decide alone will send these items to the owner.
|
||||
- **The reader.** `gstack-autoplan shadow --out <dir> [--json]`, or `--runs
|
||||
<file>` with one run directory per line, exports one row per item of each
|
||||
answered gate revision: the shadow status (`recorded`, `malformed`,
|
||||
`failed` with its code, `missing`, `retrospective`, or `off` when the run
|
||||
never turned it on), the prediction, and the owner's answer with its
|
||||
provenance. It prints counts by status, item class, `owner_mode` and
|
||||
`ceo_eligible` and computes no agreement number. A revision whose plan
|
||||
approval is unanswered or answered `c` (interrogate) refuses
|
||||
`SHADOW_BEFORE_ANSWER`, and a CEO-only run `CEO_ONLY_NO_GATE`. The refusal is
|
||||
a courtesy for honest tools, not access control: the journaled hash is the
|
||||
tamper evidence.
|
||||
|
||||
## Owner brief
|
||||
|
||||
`gstack-owner-brief render --out <dir>` is what the owner sees at the final
|
||||
|
||||
Reference in new issue
Block a user