diff --git a/.gstack/pregate.json b/.gstack/pregate.json index 1dded7b5f..737703bbe 100644 --- a/.gstack/pregate.json +++ b/.gstack/pregate.json @@ -4,6 +4,7 @@ "remote_gate": "bash scripts/ubicloud/pregate-remote.sh", "dependencies": { ".github/workflows/**": ["test/pregate-selection.test.ts", "test/free-tests-workflow-wiring.test.ts"], + "patches/**": ["browse/test/playwright-core-patch.test.ts"], "scripts/gen-ship-goldens.ts": ["test/host-config.test.ts"], "bun.lock": [], ".gitignore": [], @@ -31,5 +32,14 @@ "bun run test": { "profiles": ["ci-reuse/free-tests"], "inventory": "bun --no-env-file --config=/dev/null run scripts/test-free-shards.ts --list | sed -n 's/^ //p'" } }, "warn": [], + "guards": [ + { "id": "typecheck", "when": ["**/*.ts", "tsconfig.json", "package.json", "bun.lock"], "command": "bun run typecheck", "fix": "fix the type errors bun run typecheck prints" }, + { "id": "skill-size", "when": ["SKILL.md", "*/SKILL.md", "*/sections/*.md", "SKILL.md.tmpl", "*/SKILL.md.tmpl", "*/sections/*.tmpl", "*/sections/manifest.json", "*/templates/*.tmpl", "hosts/**", "model-overlays/**", "scripts/gen-skill-docs.ts", "scripts/resolvers/**", "test/skill-md-size-guard.test.ts", "test/skill-size-budget.test.ts", "test/catalog-budget.test.ts", "test/fixtures/skill-md-size-allowlist.json", "test/fixtures/parity-baseline-*.json", "test/helpers/skill-census.ts", "test/helpers/skill-startup.ts"], "command": "bun test test/skill-md-size-guard.test.ts test/skill-size-budget.test.ts test/catalog-budget.test.ts", "fix": "shorten the skill text the failure names; move a pin only when the growth is wanted, with the reason in the PR body" }, + { "id": "parity-suite", "when": ["SKILL.md", "*/SKILL.md", "*/sections/*.md", "SKILL.md.tmpl", "*/SKILL.md.tmpl", "*/sections/*.tmpl", "*/sections/manifest.json", "*/templates/*.tmpl", "hosts/**", "model-overlays/**", "scripts/gen-skill-docs.ts", "scripts/resolvers/**", "test/parity-suite.test.ts", "test/fixtures/parity-baseline-*.json", "test/helpers/parity-harness.ts"], "command": "bun test test/parity-suite.test.ts", "fix": "restore the missing phrase or heading, or shorten the skill back under its size ratio" }, + { "id": "context-budget", "when": ["SKILL.md", "*/SKILL.md", "*/sections/*.md", "SKILL.md.tmpl", "*/SKILL.md.tmpl", "*/sections/*.tmpl", "*/sections/manifest.json", "*/templates/*.tmpl", "hosts/**", "model-overlays/**", "scripts/gen-skill-docs.ts", "scripts/resolvers/**", "test/context-budget-ratchet.test.ts", "test/fixtures/context-budget*.json"], "command": "bun test test/context-budget-ratchet.test.ts", "fix": "shorten the skill under its token ceiling, or lower the ceiling when the skill shrank" }, + { "id": "module-size", "when": ["lib/**/*.ts", "bin/**", "scripts/**/*.ts", "browse/src/**/*.ts", "test/helpers/**/*.ts", "test/module-size-ratchet.test.ts", "test/fixtures/module-size-ratchet.json"], "command": "bun test test/module-size-ratchet.test.ts", "fix": "split the module or function the failure names; it may not grow past its recorded size" }, + { "id": "goldens", "when": ["SKILL.md", "*/SKILL.md", "*/sections/*.md", "SKILL.md.tmpl", "*/SKILL.md.tmpl", "*/sections/*.tmpl", "*/sections/manifest.json", "*/templates/*.tmpl", "hosts/**", "model-overlays/**", "scripts/gen-skill-docs.ts", "scripts/resolvers/**", "ship/**", "scripts/gen-ship-goldens.ts", "test/host-config.test.ts", "test/fixtures/golden/**"], "command": "bun test test/host-config.test.ts", "fix": "when the render change is intended, run bun scripts/gen-ship-goldens.ts and commit the updated goldens" }, + { "id": "doc-caps", "when": ["CLAUDE.md", "test/claude-md-size.test.ts"], "command": "bun test test/claude-md-size.test.ts", "fix": "move contributor-only detail from CLAUDE.md into docs/ and leave a one-line pointer" } + ], "strays": { "allow": ["modules/loadouts.json", "modules/loadouts.schema.json"] } } diff --git a/BROWSER.md b/BROWSER.md index 69be0410d..5b773d782 100644 --- a/BROWSER.md +++ b/BROWSER.md @@ -1380,7 +1380,7 @@ browser's trust store and updates. eight eMudhra intermediate CAs by CRLSet on 2026-10-17, the day their key compromise was reported, and scheduled the removal of their root, emSign ECC Root CA - G3, from the Chrome Root Store for 2026-10-27. The root store built -into gstack's pinned Playwright 1.62.1 (Chromium 151) still trusts that root, so +into gstack's pinned Playwright 1.64.1 (Chromium 156) still trusts that root, so the headless browser accepts certificates those intermediates issued. That lasts until gstack moves to the first Playwright release whose Chromium is 158.0.8115.0 or later, the first Chrome for Testing build measured to refuse diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f411c10c..5850abca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,115 @@ # Changelog +## [1.94.34.0] - 2026-11-03 + +### Fixed + +- **The autoplan-guard-pty eval no longer fails when its test actor mistypes a hash.** The live guard test has a Haiku model act out an /autoplan phase boundary, and it had to copy each reviewer dispatch, including two 64-character hashes, into its Agent call. Every so often it got a hash wrong (in PR screen 41247097050 it made up a tail for the Design dispatch's hash), and the guard rightly refused the dispatch. Now the actor sends a short declared token, `DISPATCH ceo` or `DISPATCH design`, and a loopback relay in the test driver puts that phase's exact dispatch in place of the token before Claude Code sees the tool call. The relay also puts the token back in the history the actor sees, because in the first panel's trial 1 the actor copied the substituted CEO dispatch for its Design call. The guard, the hash in the dispatch, the Haiku actor and every assertion are unchanged. The first preregistered panel stopped at that trial 1 failure. After the history fix, the second panel passed 3 of 3 (4 of 4 guarded allows in each trial, no permission cards). +- **The plan-design-with-ui-scope eval accepts both ways the skill writes the outside-voices menu.** The question format says option descriptions use literal ✅/❌ bullets, and models read that two ways: earlier captures kept the bullets in the descriptions, while train gate 41470212877 moved them into the question text and dropped them from the descriptions, so the test's fixture user refused the menu. The fixture now declares both spellings, and the picker accepts exactly the declared description, or that description with every bullet removed. The red question is saved as a fixture. Restoring only some bullets, flipping one, swapping or rewording a description still fail. A focused paid run passed. + +## [1.94.33.0] - 2026-11-03 + +Follow-ups to the skill routing and contract hooks (GST-1088). `gstack-ship-queue ready` no longer misreads files a PR adds as deleted when the installed gstack is older than the checkout: the paid-case selector now derives dependencies from the checkout under review. Each of the two hook switches now has one default, `SWITCH_DEFAULT` in `lib/skill-route.ts` (both still off), which setup, both hooks, `gstack-config get|list|defaults` and the new `gstack-skill-route --default ` all read, so changing a default is a one-line edit; `gstack-config list` and `defaults` now show both switches, an unrecognized value takes the default in the hooks as it already did in setup, and a config file that exists but cannot be read turns both hooks off in setup and in the hooks whatever the default. The contract checker no longer counts installs, builds or version probes (`bun install`, `go build`, `node --version`, `make clean`) as a test run after an edit, while option values (`make -C web test`, `pnpm --filter app test`) and `node --test` still count, and the Stop hook lets a final stop through while Agent dispatches are still running, recording the run as `deferred`, then checks the contract once the same agents have been running for 10 minutes; `gstack-skill-contract report` re-checks deferred runs. + +## [1.94.32.0] - 2026-11-03 + +### Fixed + +- **A RED paid screen now names its rule cases.** `gstack-ship-queue ready`, `verify --screen` and `train build` took a screen's red cases only from its behavior and judge panels, so rule cases that failed twice under the flaky-rerun rule never showed up. #3947's screen run 41662815506 printed `SCREEN_VERDICT: … RED flaky=ship-docsync-stale-after` with no `red=` list, and the refusal said "RED on its gate binding". That made it look like the one flaky case had turned the run red, when seven rule cases had actually failed twice. The verdict line and the `QUEUE_SCREEN_RED` refusal now list every case the report marks with ✗ (qa-quick, browse-basic and five more in that run). The verdict itself doesn't change. A case that passes on its one rerun is still green with a FLAKY line, and a case that fails twice is still red. + +### Tests + +- `test/ship-queue-screen-verdict.test.ts` reads the real gate binding and collector outcomes from run 41662815506. The seven twice-failed rule cases are named red, and the rerun-pass case stays only flaky. Negative control: with just the rerun-pass case left, the run reads GREEN with its FLAKY line. A red that never reran (INFRA, an unreported shard) is still named, and quarantined or passing split panel lines are not. + +## [1.94.31.0] - 2026-11-03 + +**gstack's browser now runs Chromium 156, which fixes CVE-2026-106378, a macOS sandbox escape ([#3907](https://github.com/garrytan/gstack/issues/3907)).** Playwright 1.62.1 bundled Chromium 151, which predates the fix. Your next `./setup` or `/gstack-upgrade` downloads the new browser. + +### Fixed +- **Playwright 1.64.1** (Chromium 156.0.8078.4, revision 1248). Chromium's fix is commit `0c2f3e54b265` ("Disclaim TCC responsibility for GPU process", crbug.com/507596239). Its code is in the tagged source for 156.0.8078.4 and missing from 151.0.7922.34. We checked the source; nobody ran the exploit. A new test fails if the bundled Chromium ever drops below 155.0.8059.12, the first tag with the fix. +- **`browse handoff` opens the browser `GSTACK_CHROMIUM_PATH` names**, as the headless and headed launches already did. Before, handoff always fell back to Playwright's bundled Chromium. It never probes or self-heals a custom browser, and it doesn't load the sidebar extension into a build that already bakes it in. + +### For contributors +- `patches/playwright-core@1.64.1.patch` keeps the launch `windowsHide` hunk and the `TcpPortRandomizationWin` backport. Playwright 1.64 passes `windowsHide` on its `taskkill` call itself, so that hunk is gone; the installed-tree test still checks it. +- `.gstack/pregate.json` maps `patches/**` to `browse/test/playwright-core-patch.test.ts`, so the next Playwright bump gets a test lane without renaming anything. +- Linux arm64 now downloads the Chrome for Testing build (`chrome-linux-arm64.zip`). + +## [1.94.30.0] - 2026-11-03 + +**/ship sends more diffs to full review: spend-binding code, eval scorers, a repo's own risk paths and docs that print secrets now count as risk, and a lane can declare a class the matchers miss (GSTA-258 build C).** Today's reviewer panel stays the default. The one-reader shape for non-risk code ships as an opt-in (`review.panel risk`) because its no-loss replay didn't pass: with only the diff and no repository, the reader found 2 of 68 non-risk blockers, so the replay is being redone with readers working in a checkout. A repo with platform review on can skip /ship's readers on plain docs. + +### Added +- **A `review` key in `.gstack/ship-policy.json`**, read from the base branch only (needs `min_gstack_version` at the release that adds it): `panel` (`all` by default, today's panel; `risk` or `off` opt in to one reader), `reader_model` (`lane` by default), `platform` (`off` by default) and `risk_paths` (globs of code paths that count as risk class `policy`). +- **`gstack-review-plan` prints `REVIEW_MODE:`** and, under `review.platform on`, `PLATFORM_REVIEW:`. `DISPATCH:` lines name the reader's `model=` and `classes=`. New flags: `--fix-checks ` and `--pr `. +- **The platform depth.** Under `review.platform on`, a docs or tests diff with no numeric, results or prereg claims, whose PR carries capy-ai[bot]'s review at the exact head, plans `NEXT: platform` with a planned `SKIP:` for every reader. Step 11.5 accepts it, and `gstack-ship-receipt write --platform-review ` records `review_depth: platform` with that review's id and head. It never applies to runtime code, and never where platform review is off. +- **New risk classes**: `eval` (scorer, judge and grader paths) and `policy` (`review.risk_paths`). A docs line that can print a credential (an `Authorization:` header, a bearer value, a key-shaped literal, `mcp get`) is security risk (gbrain#8417). +- **The receipt records `review_depth.mode`** (lean, panel or platform). +- **Declared risk classes** (GST-1129). `gstack-review-plan --declare-risk ` and `gstack-ship-receipt write --declare-risk ` add classes the matchers missed. A declaration only raises the depth, the plan prints `RISK_DECLARED:`, and the receipt keeps `review_depth.declared` beside `review_depth.detected`. +- **Code that reserves, binds, leases or settles spend is money** (GST-1129): `bindCheck(ledger, …)`, `reserveSpend` or `bindCap` in a code file now plans full depth. gstack-evals#170 got its full review only because an unrelated `UBICLOUD_API_KEY` line was in the diff. + +### Changed +- **Opt-in one-reader review** (`review.panel risk`): non-risk code gets one native adversarial reader at the lane's reasoning, carrying the code checklists and the Red Team checklist, and risk class keeps the full panel. One fix-check follows a fixed blocker. It isn't the default until its no-loss replay passes. +- **Fewer false risk triggers** (harness-evals#231): committed `*.sqlite` and other binary data files count as data, never code; JSON, YAML and other config files are judged by their path, so a `lease_usd` value no longer means money; analysis scripts under `docs/`, `results/` or `benchmarks/` are judged by their path. +- The outside voice reads its diff from a file the brief names with its sha256, instead of a diff pasted into the prompt. Past 2,000,000 bytes it reads a path-scoped slice whose brief lists the files it left out, instead of stopping the pass. + +## [1.94.29.0] - 2026-11-03 + +**Every part of gstack now has to earn its place.** Garry, 2026-11-02: "Also work this principle of only include it if it is beneficial and good into gstack". A new skill, step, instruction, check or default needs evidence that it helps on frontier models, or a reason, an owner and a dated plan to measure it. The first inventory reads untested almost everywhere: one part, `/investigate`, has a fair no-effect result (a CUT-CANDIDATE, measured on Opus 5.5 only), 3 have a fair test under way, 14 more have one proposed, and 5 safety guards stay on their stated reason. + +### Added +- **ETHOS.md principle 9, Earn Its Place.** A fair test is Claude Code's own `claude_code` system prompt, frontier models (today Opus 5.5 and GPT-6.1 Sol) and a masked no-gstack arm. A part a fair test shows doesn't help gets one "Why null?" cycle, then is kept, fixed or cut. Measure the biggest parts first. The Capy factory page cites it in item 8, still under its 3,300-byte budget. +- **`docs/inventory/evidence.json`, the evidence ledger, and `bin/gstack-inventory`.** One row per part: every skill folder plus seven shared parts (preamble, catalog, the two Capy files, the /ship review panel and docs audit, the /autoplan phases), 71 keys in all. Each status has its own required fields (`helps`, `no-effect` and `hurts` need sources, frontier models, the preset prompt, a scope and a masked no-gstack arm, and the last two an R48 verdict; a new `untested` part needs a reason, a concrete plan, an owner and a `measure_by` date; `safety` needs a reason). Rows present today are grandfathered, seeded from the gstack-evals evidence table with their sources, plans and earlier results that don't count yet; no part added later can claim that (`test/fixtures/inventory-grandfathered.json`). `gstack-inventory` writes `docs/inventory.md` (parts ranked by load per run, a "measure next" order, preamble tiers, paid dispatches per run for /ship and /autoplan quoted from their own docs); `--check` validates the ledger; `--json` prints it. It works on the git repo you run it in and never writes into a gstack install. +- **`gstack-skill-bill` prints `EVIDENCE:`**, the skill's ledger row, and `/write-skill`'s edit ladder step 2 is now the evidence rule, with an `Evidence:` commit line beside `Skill-Size:`. +- **`/autoplan`'s CEO phase lists each part a plan adds to gstack** with its evidence or owned, dated plan; a missing one is a finding. + +### Changed +- **No-gstack arms must be masked.** `gstack-eval-plan prereg --check` now also requires `masking:`, `install:` and `audit:` in `## No-gstack arm` and refuses a blank or `none` value (`PREREG_NO_GSTACK_ARM`). On Capy, run agents inherit the instruction to load the user-drive gstack skill, so the arm hides the Capy drives and checks that no gstack is installed. An arm that read a gstack skill file is contaminated and gets zero credit (docs/TESTING_INTERNALS.md#no-gstack-arm). + +### For contributors +- CONTRIBUTING.md "Adding a new skill" adds the ledger row; `test/inventory.test.ts` fails on a skill without a valid row. + +## [1.94.28.0] - 2026-11-03 + +**`gstack-ship-preflight --timeout` reaches Step 5's pregate (GST-1196).** Preflight ran `gstack-pregate` with no way to pass `--timeout`, so on a repo whose `.gstack/pregate.json` sets no `timeout_s`, a test group longer than 600 s always stopped with `PREGATE_INCOMPLETE`. The only workaround was a separate `gstack-pregate --timeout 1800` before finish (gstack-evals #176 and #179). + +### Fixed +- **`gstack-ship-preflight`:** `--timeout ` (whole seconds, 1 to 3600) is passed to Step 5's pregate as its `--timeout` and overrides the policy's `timeout_s`. When it isn't set, `timeout_s` applies, and the default is 600 s, as the help now says. A bad value exits with usage (2). +- **`docs/pregate.md`:** the `timeout_s` row says preflight now forwards `--timeout`. + +### Tests +- `test/ship-preflight.test.ts`: `--timeout 2` bounds a 4 s test under a 600 s policy and `PREGATE_DETAIL` names `--timeout`; 0, 3601, 1.5 and x are usage errors. + +## [1.94.27.0] - 2026-11-03 + +- Housekeeping checks now run before every push instead of after a CI round (GST-1138, ship-90-90 plan). `gstack-pregate --stage guards --allow-repo-commands` runs only generated-file freshness (`regen`) and the repo's `guards`, each only for the files the branch touched, and writes `.gstack/tmp/pregate-guards.json` so it never replaces the full run's record. `gstack-ship-release push` runs it before every push, early or final, prints `GUARDS:`, and stops with the guard's fix when one fails, pushing nothing. A pass already recorded at HEAD's exact tree, by push itself or by `finish`'s preflight, is reused instead of rerun. See docs/pregate.md#prepush. +- gstack declares its own guards in `.gstack/pregate.json`: typecheck, the skill size guards, the parity suite, the context-budget ratchet, the module-size ratchet, the /ship goldens and the CLAUDE.md size cap. gbrain had 64 of 93 red heads from checks like these, and #3908 went red on the context-budget ratchet. Every gstack at or above this repo's version floor already reads the `guards` key, so the floor does not move. + +### Tests +- `test/pregate-guards.test.ts`: `--stage guards` runs only `regen` and `guards`, writes its own record and refuses `--only` and `--from-run`; gstack's guard list validates, names test files that exist and fires only on its own inputs. +- `test/ship-release.test.ts`: a failing guard stops the push with its fix and pushes nothing; a pass at the same tree is reused, and a new commit runs the guards again. + +On main, all five new tests fail. + +## [1.94.26.0] - 2026-11-03 + +- A lane whose command runs several `bun test --shard=i/N` processes now records the sum of the shards' counts, not the last shard's (GBEV-3, gbrain-evals#317: 3336 recorded against 6,095 run across 2 shards). The sum needs shards 1/N to N/N, in order, at the end of the log; any other set records the last summary as before, so a command that runs a suite twice, or reruns one shard, still reports its last run. The shard lines and count lines that scroll out of the 64 KiB output tail are kept for this, so a long shard run still sums (a real 2-shard, 2000-test bun run recorded 1000 before and 2000 after). +- docs/pregate.md#dispatch-carry warns that a `workflow_dispatch` run reports under the same job names on the head commit, so without strict branch protection a green dispatch run can hide a red merge-commit check. It recommends strict protection or distinct job names for dispatch runs, and notes that gstack's own merge trains test the merged tree. +- When a pull_request run tested a merge whose base moved after the run, `CI_CARRY: none (… tree_mismatch …)` now says so and prints the one-command fix: `gh workflow run --ref , then gstack-ci-carry wait` (TRUT-7). +- docs/pregate.md#skipped-files names the order of record-ci's checks after a skipped-file remainder, including the final `tree_changed` recheck. + +### Tests +- `test/ship-ci-carry.test.ts`: a pull_request run whose merge commit has another tree prints the base-moved reason and `gh workflow run free.yml --ref feat, then gstack-ci-carry wait`. It fails without the change. +- `test/evidence.test.ts`: two `--shard=i/2` processes in one log record 6095, two shards with more than 64 KiB of per-test lines record 4000, and an incomplete, repeated or rerun shard set records the last summary. The sum cases fail without the change (3336 and 2000). + +## [1.94.25.0] - 2026-11-03 + +- A lane label with a space or a colon, such as `pnpm lint` or `db:test`, no longer goes MISSING right after it carries (TRUT-7, truther#1170). `gstack-evidence` writes a label to the ledger with unsafe characters turned into `_`, but `check`, which `gstack-ship-release finish` and `accept` call, looked it up under the raw label, so /ship stopped with "not FRESH at the pushed head". `run`, `record-ci` and `check` now share one label spelling. +- A carry that doesn't reach FRESH no longer leaves a `ci_carried` entry in the ship receipt (TRUT-7: truther's receipt kept `pnpm_lint:41603968341/1` from the attempt that stopped). `gstack-evidence carried` now lists a CI row only when `check`'s own grading, at the 24-hour age /ship uses, calls it FRESH. + +### Tests +- `test/ship-ci-carry.test.ts`: `accept` with the lanes `pnpm lint` and `db:test` carries both and checks them FRESH, and `gstack-evidence check` finds both. It failed before the fix (`EVIDENCE: MISSING label=pnpm lint`). +- `test/ship-ci-carry.test.ts`: a carried `pnpm lint` row aged past 24 hours grades STALE and leaves `carried` empty. It failed before the fix (the stale row was still listed). + ## [1.94.24.0] - 2026-11-03 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3df1c01ab..db050d961 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -814,6 +814,7 @@ Write or change skill prose with `/write-skill` (edit ladder, levels, `gstack-sk 3. The dynamic template discovery picks it up for every host render and for `gstack/llms.txt`. Two things it does not fill in: a row in `SKILL_REQUIREMENTS` in `lib/doctor-components.ts` (the doctor's `--for ` table; `test/doctor-components.test.ts` fails on a skill directory without one, and the same gen run rerenders `bin/gstack-doctor-components.sh`), and the catalog rows readers find the skill through (README.md, AGENTS.md, `docs/skills.md`, `docs/PROJECT_STRUCTURE.md`, the root `SKILL.md.tmpl` router) 4. Budget it: run `bun test/helpers/capture-context-budget.ts` and commit the refreshed `test/fixtures/context-budget.json` — the context-budget ratchet fails any skill without a ceiling; the catalog budget (`test/catalog-budget.test.ts`) fails until its cap is ratcheted by the protocol in its failure message, and the number is quoted in CLAUDE.md, this file and `docs/CONTRIBUTOR_REFERENCE.md` 5. Commit `{skill}/SKILL.md` (and the generated `{skill}/sections/preamble.md` when the template's `preamble-tier` is 2 or higher), external host output is generated at setup time and gitignored +6. Give it a row in the evidence ledger, `docs/inventory/evidence.json` ([ETHOS.md: 9. Earn Its Place](ETHOS.md#9-earn-its-place)): evidence that it helps on frontier models (`sources`, `models`, `prompt: preset`, `scope`, `masked: true`), or `status: untested` with its `reason`, a concrete `plan` (a `test/fixtures/skill-evals//tasks.json` or an /eval-plan preregistration), an `owner` and a `measure_by` date. `test/inventory.test.ts` fails on a skill without a valid row; `bin/gstack-inventory --check` names the problem, and `bin/gstack-inventory` refreshes the dated snapshot in `docs/inventory.md` ## Conductor workspaces diff --git a/ETHOS.md b/ETHOS.md index 0c631da19..aaac79291 100644 --- a/ETHOS.md +++ b/ETHOS.md @@ -273,6 +273,42 @@ the eval that jumps twenty points usually found a shortcut, not a capability. --- +## 9. Earn Its Place + +Garry, 2026-11-02: +"Also work this principle of only include it if it is beneficial and good into gstack". + +Every skill, step, instruction, check and default costs tokens and attention +on every run that loads it. So each one earns its place. A new part comes with +evidence that it helps on frontier models, or with a stated reason, an owner +and a date by which a fair test will measure it. + +A fair test runs Claude Code's own system prompt (the `claude_code` preset, +not the Agent SDK's stub), on frontier models (today Opus 5.5 and GPT-6.1 Sol, +plus at most one named low-cost model), against a no-gstack arm that is masked +so it cannot read gstack. A result from the stub, from Haiku or Sonnet alone, +or from an unmasked arm is a lead, not a verdict. + +When a fair test shows an existing part doesn't help, it gets one "Why null?" +cycle: did the model use the part, did the task fit, was there enough power and +room below the ceiling, and does today's model do the job without it. Then +keep it, fix the method, or cut it, and say which plainly. + +Safety and security guards (careful, freeze, guard, unfreeze, cso) stay on +their stated reason: they stop the rare bad run that an average score can't see. + +Measure the biggest parts first, the ones that load the most on the most runs. +[docs/inventory.md](docs/inventory.md) ranks every part by load and shows its +evidence; `docs/inventory/evidence.json` is the ledger, and +`bin/gstack-inventory --check` validates it. + +**Anti-patterns:** +- A new step because it sounds wise, with no test planned. +- Keeping a part because it looked good on the stub or a small model. +- Cutting a part after one null result, without the Why-null cycle. + +--- + ## How They Work Together Boil the Ocean says: **cover the whole requirement with the least code.** @@ -282,6 +318,7 @@ Attack the Premise says: **when fixes keep failing, test the belief they share.* Separate Before Serializing says: **remove the race before you referee it.** Make Operations Idempotent says: **design for the run that happens twice.** Explain the Number says: **a result you cannot explain is not yet a result.** +Earn Its Place says: **every part shows it helps, or has a dated plan to.** Together: search first, then build the complete version of the right thing. The worst outcome is building a complete version of something that already diff --git a/VERSION b/VERSION index 7a11af84d..cbc2db0c2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.94.24.0 +1.94.34.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index c74798cc8..5e9c80c97 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.94.24.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.94.34.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, diff --git a/autoplan/sections/ceo-phase.md b/autoplan/sections/ceo-phase.md index 318f3ec15..3ba3f3ca0 100644 --- a/autoplan/sections/ceo-phase.md +++ b/autoplan/sections/ceo-phase.md @@ -11,11 +11,12 @@ announcement. Dispatching a reviewer never completes its step. - Premises: accept reasonable ones (P6). Queue clearly-wrong/challenged premises as User Challenges for Phase 4: assumption, reason and cost of proceeding. The user decides there; never stop mid-pipeline. +- List each part the plan adds to gstack (skill, step, instruction, check, default) with its + evidence or owned, dated test plan; a missing one is a finding. - Alternatives: pick highest completeness (P1). If tied, pick simplest (P5). If top 2 are close → mark TASTE DECISION. - Scope expansion: in blast radius + <1d CC → approve (P2). Outside → defer to TODOS.md (P3). Duplicates → reject (P4). Borderline (3-5 files) → mark TASTE DECISION. -- All 11 review sections: run fully, auto-decide each issue, log every decision. **Required execution checklist (CEO):** @@ -38,8 +39,7 @@ Before every spec dispatch, including after each accepted spec fix, run: ```bash bun "" amend-input ceo "" "" "" "" ``` -This applies the recorded requirements and exports the complete current -`Implementation plan`. Keep returned `checkpointPath` as the amendment baseline; +Keep returned `checkpointPath` as the amendment baseline; use returned `reviewInputPath` as ``. Read that file at every returned `readRanges` offset/limit through EOF, then read the CEO scope summary in full. Reconcile dispositions, scope counts, proposal IDs and actual heading/test references diff --git a/autoplan/sections/ceo-phase.md.tmpl b/autoplan/sections/ceo-phase.md.tmpl index a4e0e1311..151a737fe 100644 --- a/autoplan/sections/ceo-phase.md.tmpl +++ b/autoplan/sections/ceo-phase.md.tmpl @@ -9,11 +9,12 @@ announcement. Dispatching a reviewer never completes its step. - Premises: accept reasonable ones (P6). Queue clearly-wrong/challenged premises as User Challenges for Phase 4: assumption, reason and cost of proceeding. The user decides there; never stop mid-pipeline. +- List each part the plan adds to gstack (skill, step, instruction, check, default) with its + evidence or owned, dated test plan; a missing one is a finding. - Alternatives: pick highest completeness (P1). If tied, pick simplest (P5). If top 2 are close → mark TASTE DECISION. - Scope expansion: in blast radius + <1d CC → approve (P2). Outside → defer to TODOS.md (P3). Duplicates → reject (P4). Borderline (3-5 files) → mark TASTE DECISION. -- All 11 review sections: run fully, auto-decide each issue, log every decision. **Required execution checklist (CEO):** @@ -36,8 +37,7 @@ Before every spec dispatch, including after each accepted spec fix, run: ```bash bun "" amend-input ceo "" "" "" "" ``` -This applies the recorded requirements and exports the complete current -`Implementation plan`. Keep returned `checkpointPath` as the amendment baseline; +Keep returned `checkpointPath` as the amendment baseline; use returned `reviewInputPath` as ``. Read that file at every returned `readRanges` offset/limit through EOF, then read the CEO scope summary in full. Reconcile dispositions, scope counts, proposal IDs and actual heading/test references diff --git a/bin/gstack-config b/bin/gstack-config index c9e4f72fc..c27f155a9 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -26,6 +26,7 @@ # reports which root received the value and which root still overrides it. # Docs: https://github.com/garrytan/gstack/blob/main/docs/state-root.md set -euo pipefail +GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac [ -r "$(dirname "$0")/gstack-state-root.sh" ] && . "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; } gstack_state_root_select @@ -344,6 +345,8 @@ lookup_default() { artifacts_sync_removals) echo "off" ;; # on | off — publish removals of synced files gone from disk plan_tune_hooks) echo "prompt" ;; # prompt | yes | no — controls ./setup plan-tune hook install timeline_stop_hook) echo "yes" ;; # yes | no — controls ./setup timeline Stop hook registration (#2677) + skill_routing) bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$(dirname "$0")/gstack-skill-route" --default skill_routing ;; # on | off — the skill-routing UserPromptSubmit hook; default: SWITCH_DEFAULT in lib/skill-route.ts (docs/skill-routing-and-contracts.md) + skill_contracts) bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$(dirname "$0")/gstack-skill-route" --default skill_contracts ;; # on | off — the skill-contract Stop hook; default: SWITCH_DEFAULT in lib/skill-route.ts (docs/skill-routing-and-contracts.md) redact_repo_visibility) echo "" ;; # empty → fall through to gh/glab detection redact_prepush_hook) echo "false" ;; @@ -1139,7 +1142,8 @@ $_gstack_legacy_root" ship_measure_judge_outputs ship_measure_ask_per_trial_usd ship_measure_budget_usd \ ship_measure_max_rounds ship_measure_sweep_cases ship_measure_sweep_budget_usd ship_rerun_backend test_backend \ plan_review_tier implementation_tier model_frontier_claude model_frontier_openai \ - model_smart_claude model_smart_openai codex_effort review_effort_tiers agent_model harness_start_hook; do + model_smart_claude model_smart_openai codex_effort review_effort_tiers agent_model harness_start_hook \ + skill_routing skill_contracts; do gstack_config_select "$KEY" VALUE="$_gstack_cfg_value" SOURCE="default" @@ -1164,7 +1168,8 @@ $_gstack_legacy_root" ship_measure_judge_outputs ship_measure_ask_per_trial_usd ship_measure_budget_usd \ ship_measure_max_rounds ship_measure_sweep_cases ship_measure_sweep_budget_usd ship_rerun_backend test_backend \ plan_review_tier implementation_tier model_frontier_claude model_frontier_openai \ - model_smart_claude model_smart_openai codex_effort review_effort_tiers agent_model harness_start_hook; do + model_smart_claude model_smart_openai codex_effort review_effort_tiers agent_model harness_start_hook \ + skill_routing skill_contracts; do printf ' %-24s %s\n' "$KEY:" "$(lookup_default "$KEY")" done ;; diff --git a/bin/gstack-evidence b/bin/gstack-evidence index e3d2bbab1..d87f76e28 100755 --- a/bin/gstack-evidence +++ b/bin/gstack-evidence @@ -37,7 +37,9 @@ * the tail is the stdout tail then the stderr tail, so a line's position says * nothing about when it was printed across streams, and bun's own counts (on * stderr) would otherwise outrank a 10k-test run whichever of the two ran - * first. Any summary when it did not speak follows the last-summary-wins rule; + * first. Any summary when it did not speak follows the last-summary-wins rule, + * except that a complete set of `bun test --shard=i/N` processes ending the log + * is summed (GBEV-3); * anything else is "unknown". * Three states, never merged: * a count, 0 (ZERO-RUN, graded like STALE by `check`) and unknown (non-blocking). @@ -95,7 +97,7 @@ import { isUnavailable, loadPolicyFromBase, resolveBaseBranch } from "../lib/shi import { versionAtRef } from "../lib/restamp"; import { bashCommandInvocation, runScriptSync, wtreeFingerprint } from "../lib/script-invocation"; import { findCarry, laneMapping, mappingCoverage, perFileCommand } from "../lib/ci-carry"; -import { runChanged } from "../lib/run-evidence"; +import { DEFAULT_MAX_AGE_HOURS, runChanged } from "../lib/run-evidence"; import { isConfigUnavailable, loadConfigFromBase } from "../lib/pregate/config"; import { buildRunCheck } from "../lib/pregate/run-check"; @@ -146,30 +148,48 @@ const TAIL_MAX_BYTES = 64 * 1024; * Ring of the last TAIL_MAX_BYTES of each child stream, kept per stream: the * pumps interleave by arrival, so a stderr summary read while stdout still has * pipe-buffered bytes behind it would otherwise be pushed out of a shared ring. + * Lines leaving the ring that bun's shard sum reads (a "--shard=i/N: running" + * line, " N pass", " N fail", "Ran N tests across") are kept in order ahead of + * the tail, so every shard of a long sharded run still counts (GBEV-3). */ class OutputTail { private readonly decoders = { stdout: new TextDecoder("utf-8"), stderr: new TextDecoder("utf-8") }; private readonly text = { stdout: "", stderr: "" }; + private readonly kept = { stdout: [] as string[], stderr: [] as string[] }; + private trim(origin: "stdout" | "stderr", text: string): string { + const cut = text.lastIndexOf("\n", text.length - TAIL_MAX_BYTES); + if (cut < 0) return text.slice(-TAIL_MAX_BYTES); + const lines = text.slice(0, cut).split("\n").filter((l) => /^(?:--shard=\d+\/\d+: running |\s*\d+ (?:pass|fail)\s*$|Ran \d+ tests? across )/.test(strip(l))); + this.kept[origin] = [...this.kept[origin], ...lines].slice(-4096); + return text.slice(cut + 1); + } push(chunk: Uint8Array, origin: "stdout" | "stderr"): void { this.text[origin] += this.decoders[origin].decode(chunk, { stream: true }); - if (this.text[origin].length > TAIL_MAX_BYTES * 2) this.text[origin] = this.text[origin].slice(-TAIL_MAX_BYTES); + if (this.text[origin].length > TAIL_MAX_BYTES * 2) this.text[origin] = this.trim(origin, this.text[origin]); } end(): string { const tails = (["stdout", "stderr"] as const).map((origin) => { const text = this.text[origin] + this.decoders[origin].decode(); - return text.length > TAIL_MAX_BYTES ? text.slice(-TAIL_MAX_BYTES) : text; + const tail = text.length > TAIL_MAX_BYTES ? this.trim(origin, text) : text; + return [...this.kept[origin], tail].join("\n"); }); return tails.join("\n"); } } +/** The one spelling a lane label has in the ledger: run and record-ci write it, check reads it (`pnpm lint` is `pnpm_lint`, TRUT-7). */ +const ledgerLabel = (raw: string): string => raw.replace(/[^a-zA-Z0-9._-]/g, "_"); + const ANSI = /\u001B\[[0-?]*[ -/]*[@-~]/g; const strip = (l: string) => l.replace(ANSI, "").replace(/\r$/, ""); const count = (text: string, word: string): number => Number(new RegExp(`(\\d+) ${word}\\b`).exec(text)?.[1] ?? 0); /** * Bun: the last "Ran N tests across M files" block; executed = its " N pass" + - * " N fail" lines. Bun always prints that counts block (" 0 pass" / " 0 fail" + * " N fail" lines. When that block ends a `bun test --shard=i/N` process (bun's + * "--shard=i/N: running" line since the previous block), the blocks of shards + * 1/N to N/N, in that order, that end the log are summed (a sharded runner prints + * every shard, GBEV-3); any other shard set counts the last block alone. Bun always prints that counts block (" 0 pass" / " 0 fail" * even for an all-skip or empty run), so a terminal line with no " N pass" * above it is a line gstack's quiet shard runner forwarded (it forwards the * terminal line without the " N pass" line), not a bun summary: @@ -186,7 +206,9 @@ async function bunTestsRan(lines: string[]): Promise<{ at: number; ran: number } } catch { warn("shard-engine classifier not importable; using the built-in bun summary parser"); } - for (let i = lines.length - 1; i >= 0; i--) { + const blocks: { at: number; ran: number; shard?: string }[] = []; + let since = 0; + for (let i = 0; i < lines.length; i++) { if (!isTerminal(lines[i])) continue; let pass: number | null = null; let fail = 0; @@ -197,13 +219,17 @@ async function bunTestsRan(lines: string[]): Promise<{ at: number; ran: number } if (p) pass = Number(p[1]); if (f) fail = Number(f[1]); } - if (pass === null) { - if (/^Ran 0 tests? across/.test(lines[i])) return { at: i, ran: 0 }; - continue; - } - return { at: i, ran: pass + fail }; + const shard = lines.slice(since, i).map((l) => /^--shard=(\d+\/\d+): running /.exec(l)?.[1]).findLast(Boolean); + since = i + 1; + if (pass !== null) blocks.push({ at: i, ran: pass + fail, shard }); + else if (/^Ran 0 tests? across/.test(lines[i])) blocks.push({ at: i, ran: 0 }); } - return null; + const last = blocks.at(-1); + if (!last) return null; + const total = Number(last.shard?.split("/")[1] ?? 1); + const set = blocks.slice(-total); + const complete = total > 1 && set.length === total && set.every((b, k) => b.shard === `${k + 1}/${total}`); + return { at: last.at, ran: complete ? set.reduce((n, b) => n + b.ran, 0) : last.ran }; } /** @@ -428,7 +454,7 @@ async function cmdRun(argv: string[]): Promise { const commandString = cmdArgv.length === 1 ? cmdArgv[0] : cmdArgv.join(" "); const shell = cmdArgv.length === 1 ? bashCommandInvocation(cmdArgv[0]) : null; const cmdSha = sha256(commandString); - label = label.replace(/[^a-zA-Z0-9._-]/g, "_"); + label = ledgerLabel(label); // Bookkeeping context — every piece is optional; failures only warn. let paths: ReturnType | undefined; @@ -675,7 +701,8 @@ async function cmdRecordCi(argv: string[]): Promise { const sep = argv.indexOf("--"); const opts = sep < 0 ? argv : argv.slice(0, sep); const get = (name: string) => { const i = opts.indexOf(name); return i >= 0 ? opts[i + 1] : undefined; }; - const label = get("--label")?.replace(/[^a-zA-Z0-9._-]/g, "_"); + const raw = get("--label"); + const label = raw === undefined ? undefined : ledgerLabel(raw); const base = get("--base"); const command = sep < 0 ? "" : argv.slice(sep + 1).join(" "); if (!label || !base || !command || opts.length !== 4) { @@ -754,13 +781,76 @@ function cmdCarried(argv: string[]): number { const wtree = currentWtree(); const latest = new Map(); for (const r of records) latest.set(r.label, r); - const carried = [...latest.values()].filter((r) => r.source === "ci" && !!wtree && r.wtree === wtree && Array.isArray(r.runs) && typeof r.policy === "string" && r.policy.startsWith(policyRef)) + const carried = [...latest.values()].filter((r) => r.source === "ci" && !!wtree && r.wtree === wtree && Array.isArray(r.runs) && typeof r.policy === "string" && r.policy.startsWith(policyRef) + && gradeRecord(r, { maxAgeHours: DEFAULT_MAX_AGE_HOURS, allowPaths: [], wtreeNow: wtree }).verdict === "FRESH") .flatMap((r) => r.runs!.map((ref) => { const [run, attempt] = ref.split("/").map(Number); return { lane: r.label, run, attempt, tree: r.wtree! }; })); if (argv.includes("--json")) console.log(JSON.stringify(carried)); else for (const c of carried) console.log(`CI_CARRIED: ${c.lane} run=${c.run}/${c.attempt} tree=${c.tree.slice(0, 12)}`); return 0; } +/** check's verdict for one ledger row; `carried` lists a CI row only when it grades FRESH here, so a carry that never reached FRESH leaves no receipt entry. */ +function gradeRecord(latest: EvidenceRecord, o: { maxAgeHours?: number; expectCmd?: string; allowPaths: string[]; wtreeNow: string | undefined }): { verdict: "FRESH" | "STALE" | "ZERO-RUN"; reason: string } { + const { maxAgeHours, expectCmd, allowPaths, wtreeNow } = o; + const testsRan = typeof latest.tests_ran === "number" || latest.tests_ran === "n/a" ? latest.tests_ran : "unknown"; + const changedDuringRun = Array.isArray(latest.wtree_changed) ? latest.wtree_changed.filter((p): p is string => typeof p === "string" && p !== "" && !/[\u0000-\u001f\u007f]/.test(p)).slice(0, 5) : []; + let verdict: "FRESH" | "STALE" | "ZERO-RUN" = "FRESH"; + let reason = ""; + if (latest.exit !== 0) { + verdict = "STALE"; + reason = "recorded run failed"; + } else if (testsRan === 0) { + // Zero-run green: exit 0 with nothing executed (all skipped, empty + // selection, "No tests found"). Graded like STALE under its own label; + // "unknown" (no recognised runner summary) stays non-blocking. + verdict = "ZERO-RUN"; + reason = "runner reported 0 executed tests"; + } else if (maxAgeHours !== undefined) { + const ageMs = Date.now() - Date.parse(latest.ts); + if (!(ageMs >= 0 && ageMs <= maxAgeHours * 3600 * 1000)) { + verdict = "STALE"; + reason = `older than ${maxAgeHours}h`; + } + } + if (verdict === "FRESH" && expectCmd !== undefined && sha256(expectCmd) !== latest.cmd_sha256) { + verdict = "STALE"; + reason = "command changed (cmd_sha256 mismatch)"; + } + if (verdict === "FRESH") { + // Content binding: identical working-tree fingerprint, or a diff confined + // to the allow-list. Any git failure (gc'd tree, not a repo) → STALE — + // never an error into the calling flow. + if (!latest.wtree || !/^[0-9a-f]{40}$/.test(latest.wtree) || !wtreeNow) { + // Stored fingerprints are re-validated before reaching git argv — a + // forged/corrupt ledger line must degrade, never inject options. + verdict = "STALE"; + reason = !latest.wtree + ? changedDuringRun.length + ? `content changed during the run: ${changedDuringRun.join(", ")}` + : "record has no content fingerprint" + : !/^[0-9a-f]{40}$/.test(latest.wtree) + ? "record has malformed fingerprint" + : "current fingerprint unavailable"; + } else if (latest.wtree !== wtreeNow) { + const diff = git(["diff", "--name-only", latest.wtree, wtreeNow]); + if (diff === undefined) { + verdict = "STALE"; + reason = "content changed (fingerprint diff unavailable)"; + } else { + const changed = diff.split("\n").map((s) => s.trim()).filter(Boolean); + const outside = changed.filter((f) => !allowPaths.some((a) => f === a || f.startsWith(a.replace(/\/$/, "") + "/"))); + if (changed.length === 0 || outside.length === 0) { + reason = changed.length ? `diff confined to allow-paths (${changed.length} file(s))` : ""; + } else { + verdict = "STALE"; + reason = `content changed: ${outside.slice(0, 5).join(", ")}${outside.length > 5 ? ", ..." : ""}`; + } + } + } + } + return { verdict, reason }; +} + function cmdCheck(argv: string[]): number { // Parse: repeated --label, each optionally followed (anywhere later) by its // own --expect-cmd; pairing is positional — an --expect-cmd binds to the most @@ -771,7 +861,7 @@ function cmdCheck(argv: string[]): number { let allowPaths: string[] = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; - if (a === "--label") wanted.push({ label: argv[++i] ?? "" }); + if (a === "--label") wanted.push({ label: ledgerLabel(argv[++i] ?? "") }); else if (a === "--expect-cmd") { if (wanted.length === 0) { console.error("gstack-evidence: --expect-cmd requires a preceding --label"); @@ -820,63 +910,9 @@ function cmdCheck(argv: string[]): number { continue; } const testsRan = typeof latest.tests_ran === "number" || latest.tests_ran === "n/a" ? latest.tests_ran : "unknown"; - const changedDuringRun = Array.isArray(latest.wtree_changed) ? latest.wtree_changed.filter((p): p is string => typeof p === "string" && p !== "" && !/[\u0000-\u001f\u007f]/.test(p)).slice(0, 5) : []; const ci = latest.source === "ci" && Array.isArray(latest.runs) ? ` source=ci runs=${latest.runs.filter((s) => /^\d+\/\d+$/.test(s)).join(",")}` : ""; const detail = `label=${label} exit=${latest.exit} tests_ran=${testsRan}${latest.tests_unit === "files" ? " unit=files" : ""} ts=${latest.ts}${latest.log_path ? ` log=${latest.log_path}` : ""}${ci}`; - let verdict: "FRESH" | "STALE" | "ZERO-RUN" = "FRESH"; - let reason = ""; - if (latest.exit !== 0) { - verdict = "STALE"; - reason = "recorded run failed"; - } else if (testsRan === 0) { - // Zero-run green: exit 0 with nothing executed (all skipped, empty - // selection, "No tests found"). Graded like STALE under its own label; - // "unknown" (no recognised runner summary) stays non-blocking. - verdict = "ZERO-RUN"; - reason = "runner reported 0 executed tests"; - } else if (maxAgeHours !== undefined) { - const ageMs = Date.now() - Date.parse(latest.ts); - if (!(ageMs >= 0 && ageMs <= maxAgeHours * 3600 * 1000)) { - verdict = "STALE"; - reason = `older than ${maxAgeHours}h`; - } - } - if (verdict === "FRESH" && expectCmd !== undefined && sha256(expectCmd) !== latest.cmd_sha256) { - verdict = "STALE"; - reason = "command changed (cmd_sha256 mismatch)"; - } - if (verdict === "FRESH") { - // Content binding: identical working-tree fingerprint, or a diff confined - // to the allow-list. Any git failure (gc'd tree, not a repo) → STALE — - // never an error into the calling flow. - if (!latest.wtree || !/^[0-9a-f]{40}$/.test(latest.wtree) || !wtreeNow) { - // Stored fingerprints are re-validated before reaching git argv — a - // forged/corrupt ledger line must degrade, never inject options. - verdict = "STALE"; - reason = !latest.wtree - ? changedDuringRun.length - ? `content changed during the run: ${changedDuringRun.join(", ")}` - : "record has no content fingerprint" - : !/^[0-9a-f]{40}$/.test(latest.wtree) - ? "record has malformed fingerprint" - : "current fingerprint unavailable"; - } else if (latest.wtree !== wtreeNow) { - const diff = git(["diff", "--name-only", latest.wtree, wtreeNow]); - if (diff === undefined) { - verdict = "STALE"; - reason = "content changed (fingerprint diff unavailable)"; - } else { - const changed = diff.split("\n").map((s) => s.trim()).filter(Boolean); - const outside = changed.filter((f) => !allowPaths.some((a) => f === a || f.startsWith(a.replace(/\/$/, "") + "/"))); - if (changed.length === 0 || outside.length === 0) { - reason = changed.length ? `diff confined to allow-paths (${changed.length} file(s))` : ""; - } else { - verdict = "STALE"; - reason = `content changed: ${outside.slice(0, 5).join(", ")}${outside.length > 5 ? ", ..." : ""}`; - } - } - } - } + const { verdict, reason } = gradeRecord(latest, { maxAgeHours, expectCmd, allowPaths, wtreeNow }); console.log(`EVIDENCE: ${verdict} ${detail}${reason ? ` reason=${reason}` : ""}`); if (verdict !== "FRESH") allFresh = false; } diff --git a/bin/gstack-inventory b/bin/gstack-inventory new file mode 100755 index 000000000..a8f770afc --- /dev/null +++ b/bin/gstack-inventory @@ -0,0 +1,54 @@ +#!/usr/bin/env -S bun --no-env-file --config=/dev/null +// gstack-inventory — every gstack part, its load per run and its evidence (ETHOS.md: 9. Earn Its Place). Logic: lib/inventory.ts. +import { spawnSync } from "node:child_process"; +import { existsSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { buildInventory, checkInventory, DOC_PATH, installReason, LEDGER_PATH, renderInventory } from "../lib/inventory"; + +const USAGE = `Usage: gstack-inventory [--root ] [--check | --json] + gstack-inventory --help + +Joins the evidence ledger (${LEDGER_PATH}) with gstack-context-bill's sizes and writes ${DOC_PATH}: +parts ranked by load per run, each part's evidence status, a "measure next" order (largest untested +first), shared-part sizes, paid dispatches per run for /ship and /autoplan, and the key list. +--check validates the ledger (every key has one row; each row has its status's fields), the quoted +dispatch sources and the shared-part files, and writes nothing. --json prints the inventory. +The root defaults to the git repo you run it in; it never writes into a gstack install. +Exit 0 when clean; 1 with one ": ()" line per problem; 2 on a bad argument.`; + +const args = process.argv.slice(2); +if (args.includes("--help") || args.includes("-h")) { console.log(USAGE); process.exit(0); } +const rootAt = args.indexOf("--root"); +const rootArg = rootAt >= 0 ? args[rootAt + 1] : undefined; +const unknown = args.find((a, i) => !["--check", "--json", "--root"].includes(a) && !(rootAt >= 0 && i === rootAt + 1)); +if (unknown !== undefined || (rootAt >= 0 && (!rootArg || rootArg.startsWith("--"))) || (args.includes("--check") && args.includes("--json"))) { + console.error(unknown !== undefined ? `gstack-inventory: unknown argument '${unknown}'; valid: --root --check --json --help` : USAGE); + process.exit(2); +} +const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", timeout: 10_000 }); +const root = resolve(rootArg ?? (top.status === 0 ? top.stdout.trim() : process.cwd())); +if (rootArg === undefined && !existsSync(join(root, LEDGER_PATH))) { + console.error(`gstack-inventory: ${root} is not a gstack checkout (no ${LEDGER_PATH}); run it in your gstack checkout or pass --root `); + process.exit(2); +} +const report = (problems: { key: string; code: string; message: string }[]) => { + for (const p of problems) console.error(`gstack-inventory: ${p.key}: ${p.message} (${p.code})`); +}; +if (args.includes("--check")) { + const checked = checkInventory(root); + report(checked.problems); + if (checked.problems.length) process.exit(1); + console.log(`INVENTORY: ok keys=${checked.keys.length} ledger=${LEDGER_PATH} dispatch_sources=${checked.dispatches.length}`); + process.exit(0); +} +const inv = buildInventory(root); +report(inv.problems); +if (args.includes("--json")) { console.log(JSON.stringify(inv, null, 2)); process.exit(inv.problems.length ? 1 : 0); } +if (inv.problems.length) { console.error(`gstack-inventory: ${DOC_PATH} not written; fix the problems above, then rerun`); process.exit(1); } +const installed = installReason(root); +if (installed) { + console.error(`gstack-inventory: refusing to write ${DOC_PATH} into ${root}: it is ${installed}, which gstack-upgrade owns; run it in your gstack checkout or pass --root `); + process.exit(2); +} +writeFileSync(join(root, DOC_PATH), renderInventory(inv)); +console.log(`INVENTORY: wrote ${DOC_PATH} keys=${inv.keys.length} commit=${inv.commit}`); diff --git a/bin/gstack-pregate b/bin/gstack-pregate index c14279033..3fdf66be3 100755 --- a/bin/gstack-pregate +++ b/bin/gstack-pregate @@ -2,7 +2,7 @@ /** * gstack-pregate — cheap checks before anything paid or remote (plan C8). * - * gstack-pregate [run] [--stage preflight|tests|all] [--only ] [--base ] + * gstack-pregate [run] [--stage preflight|tests|guards|all] [--only ] [--base ] * [--allow-repo-commands] [--policy-from base|head] [--out ] * [--timeout ] [--stall-timeout ] [--remote --spend-cap ] [--run ] [--from-run ] [--no-ci-carry] [--explain] [--json] * gstack-pregate verify [--out ] [--json] publication check: current tree, no fail/incomplete, obligations cleared @@ -33,7 +33,7 @@ import { PREGATE_CONFIG_PATH, explainConfig, loadConfigFromHead } from '../lib/p import { describeDraft, draftConfig } from '../lib/pregate/init-draft'; import { buildRunCheck } from '../lib/pregate/run-check'; import { buildContext, defaultUserConfigPath, describeRefusal, loadedDotenvFiles } 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 { CHECKS, CHECK_IDS, PREGATE_GUARDS_OUT_REL, PREGATE_OUT_REL, PREGATE_TIER2, PREPUSH_CHECK_IDS, clearDecision, clearObligation, readPregateFile, renderTable, runPregate, verifyPregate, writePregateFile } from '../lib/pregate/index'; import { describeLane, discoverLanes } from '../lib/pregate/workflows'; import { REMOTE_GATE_LANE } from '../lib/pregate/checks/lanes'; import { describeSubstitution } from '../lib/pregate/remote-receipt'; @@ -75,7 +75,8 @@ const HELP = `gstack-pregate — cheap checks before anything paid or remote (do a fresh tree receipt, each lane's reuse verdict, migration collisions (base branch and open PRs), then REGATE: needed|not-needed (docs/pregate.md#regate); \`gstack-pregate --against \` is the same verb; exit 1 = needed - --stage preflight|tests|all default all + --stage preflight|tests|guards|all default all; guards is the pre-push housekeeping run (docs/pregate.md#prepush): + only ${PREPUSH_CHECK_IDS.join(' and ')}, each for the files the diff touched, written to ${PREGATE_GUARDS_OUT_REL} --only restrict to these checks (${CHECK_IDS.join(', ')}; tier 2 later: ${PREGATE_TIER2.join(', ')}) --base a branch on origin, not a commit (default: origin/HEAD's branch unless it is the checked-out one, else the PR's base, origin/main, origin/master; lib/default-base.ts) @@ -151,10 +152,11 @@ function repoRoot(): string | null { function outPath(root: string, rest: string[]): string { const o = flag(rest, '--out'); return o ? path.resolve(o) : path.join(root, PREGATE_OUT_REL); } async function cmdRun(root: string, rest: string[], json: boolean): Promise { - const stage = (flag(rest, '--stage') ?? 'all') as 'preflight' | 'tests' | 'all'; - if (!['preflight', 'tests', 'all'].includes(stage)) { process.stderr.write('gstack-pregate: --stage needs preflight, tests or all\n'); return EXIT.usage; } + const stage = (flag(rest, '--stage') ?? 'all') as 'preflight' | 'tests' | 'guards' | 'all'; + if (!['preflight', 'tests', 'guards', 'all'].includes(stage)) { process.stderr.write('gstack-pregate: --stage needs preflight, tests, guards or all\n'); return EXIT.usage; } + if (stage === 'guards' && (flag(rest, '--only') !== undefined || rest.includes('--remote'))) { process.stderr.write(`gstack-pregate: --stage guards runs only ${PREPUSH_CHECK_IDS.join(' and ')}; drop --only and --remote\n`); return EXIT.usage; } const fromRun = flag(rest, '--from-run'); - if (fromRun !== undefined && stage === 'preflight') { process.stderr.write('gstack-pregate: --from-run applies to the tests stage (--stage tests or all)\n'); return EXIT.usage; } + if (fromRun !== undefined && (stage === 'preflight' || stage === 'guards')) { process.stderr.write('gstack-pregate: --from-run applies to the tests stage (--stage tests or all)\n'); return EXIT.usage; } const only = flag(rest, '--only')?.split(',').map(s => s.trim()).filter(Boolean); const unknown = (only ?? []).filter(id => !CHECK_IDS.includes(id)); if (unknown.length) { process.stderr.write(`gstack-pregate: unknown check(s) ${unknown.join(', ')}${unknown.some(u => (PREGATE_TIER2 as readonly string[]).includes(u)) ? ' (tier 2, not in this gstack)' : ''}; known: ${CHECK_IDS.join(', ')}\n`); return EXIT.usage; } @@ -197,7 +199,7 @@ async function cmdRun(root: string, rest: string[], json: boolean): Promise REVIEW_DIFF_MAX_BYTES) return { exit: EXIT.refused, line: tooLargeLine(context, r.reader, d, ceilingContext(process.cwd(), base, known)).line }; + if (d.bytes > REVIEW_DIFF_MAX_BYTES && r.reader !== 'outside-voice') return { exit: EXIT.refused, line: tooLargeLine(context, r.reader, d, ceilingContext(process.cwd(), base, known)).line }; } return null; } @@ -329,19 +339,21 @@ function program(): number { * collected, and it was rendered on a committed diff (uncommitted never proves what was read). `head` names the commit it read: * a later commit does not undo the read, and gstack-ship-receipt write binds the record only while that commit's product patch is * the receipt's (GST-986: Step 11 fixes after the read printed status=failed findings=0 for a completed reader). + * R65's merged reader (an adversarial-native brief rendered with --classes) is a targeted dispatch too (design C7). */ function targetedRecord(ledger: string, rows: VerifyRow[]): { classes: string[]; rules: string; brief_sha: string; status: 'done' | 'failed'; findings: number; raised_by: string; head: string | null } | null { const events = readLedger(ledger); - const render = events.filter(e => e.event === 'rendered' && e.reader === 'targeted').at(-1); + const render = events.filter(e => e.event === 'rendered' && (e.reader === 'targeted' || (e.reader === 'adversarial-native' && typeof e.brief_sha === 'string'))).at(-1); if (!render) return null; - const label = rows.filter(r => r.kind === 'BRIEF' && r.reader.split('@')[0] === 'targeted').at(-1)?.reader; + const key = String(render.reader); + const label = rows.filter(r => r.kind === 'BRIEF' && r.reader.split('@')[0] === key).at(-1)?.reader; const brief = rows.filter(r => r.kind === 'BRIEF' && r.reader === label).at(-1); const output = rows.filter(r => r.kind === 'OUTPUT' && r.reader === label).at(-1); const head = typeof render.head === 'string' && /^[0-9a-f]{40,64}$/.test(render.head) ? render.head : null; const committed = typeof render.patch_id === 'string' && !['uncommitted', 'none'].includes(render.patch_id) && head !== null; const done = brief?.status === 'MATCH' && output?.status === 'MATCH' && committed; - const ids = events.filter(e => e.event === 'output' && e.reader === 'targeted').at(-1)?.ids; - return { classes: (render.classes as string[]) ?? [], rules: String(render.rules), brief_sha: String(render.brief_sha), status: done ? 'done' : 'failed', findings: Array.isArray(ids) ? ids.length : 0, raised_by: 'targeted', head }; + const ids = events.filter(e => e.event === 'output' && e.reader === key).at(-1)?.ids; + return { classes: (render.classes as string[]) ?? [], rules: String(render.rules), brief_sha: String(render.brief_sha), status: done ? 'done' : 'failed', findings: Array.isArray(ids) ? ids.length : 0, raised_by: key, head }; } /** diff --git a/bin/gstack-review-plan b/bin/gstack-review-plan index b74496475..ab4cd4eec 100755 --- a/bin/gstack-review-plan +++ b/bin/gstack-review-plan @@ -5,7 +5,7 @@ * gstack-review-plan --base [--pass ] [--last none|full|fix-check] [--fixed-blocking] [--fixed-below] * [--blocking-open] [--touched-gate] [--critical-found] [--adversarial-ran] [--gate-tagged ] [--gated a,b] [--force a,b|all] * [--reviewed-numbers ] [--depth-floor standard|full] [--range-base ] [--pass1-depth light|standard|full] [--no-tiers] - * [--targeted-ran ] [--grant ] [--json] + * [--targeted-ran ] [--grant ] [--fix-checks ] [--pr ] [--json] * gstack-review-plan --fix-check-prompt --fix-list --hunks --tests [--callers ] * * Reads the diff against the merge base with (committed, tracked uncommitted @@ -32,9 +32,12 @@ import { baseNote } from '../lib/gate-diff/git'; import { collectCommit, collectLive } from '../lib/review-collect'; import { excludeLines, reviewExclude, type Exclusion } from '../lib/review-exclude'; import { isUnavailable } from '../lib/ship-policy'; +import { reviewPolicy } from '../lib/ship-policy-review'; +import { platformReviewAt } from '../lib/review-platform'; import { loadPolicyAt } from '../lib/ship-policy-at'; import { classifyDiff, diffCommand, keepCommand, planReview, reviewDepthLine, reviewPlanLine, scanNumbers, SCOPED_RANGE_LINES, type Depth, type History } from '../lib/review-depth'; import { classList, targetedBriefSha, targetedClasses, targetedPatchId } from '../lib/review-targeted'; +import { RISK_CLASSES, type RiskClass } from '../lib/review-risk'; import { renderFixCheckPrompt } from '../scripts/resolvers/fix-check'; const HELP = `gstack-review-plan — the review depth of the next /ship step (docs/review-policy.md#review-depth) @@ -67,9 +70,19 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do its ask or scope naming review_loop_grant and the stop head's sha, at or after --range-base and at or before HEAD; needs --range-base): the stop after a round-2 full pass with a fixed blocking finding or a gate-class edit plans that fix-check instead; a fix-check never earns another; any other row is refused (exit 3) + --fix-checks fix-checks already run in this invocation (R65's lean loop: the owner's grant adds one only after the first) + --pr the PR whose platform review counts under review.platform on (default: this branch's PR) + --declare-risk risk classes the lane declares for the diff (comma list of ${RISK_CLASSES.join(', ')}): they join + the detected classes and only raise the depth; RISK_DECLARED: prints both (GST-1129) --fix-check-prompt print the fix-check reviewer's prompt from --fix-list, --hunks, --tests and --callers files -Depth (R51, risk not size): full on gate class, a risk class (money, auth, secret, security, data) or any DEPTH_ERROR; +Mode (R65, the base policy's review key): review.panel all (default) keeps today's panel on every class. review.panel risk +(opt-in; its one-reader shape has not passed a no-loss replay) runs lean off risk class, and review.panel off everywhere: lean runs one reader: risk class gets the merged reader on review.risk_model +(the native adversarial brief plus the checklists, the Red Team and the targeted brief), non-risk code the single reader at the +lane's reasoning (cross-family when it publishes numbers), docs and tests the core read; one fix-check after fixes, never more +than 2 rounds. review.platform on (any panel): a light diff with no numeric claims whose PR +carries capy-ai[bot]'s review at HEAD plans NEXT: platform and a planned SKIP for every reader. +Depth (R51, risk not size): full on gate class, a risk class (money, auth, secret, security, data, review.risk_paths) or any DEPTH_ERROR; standard only by --depth-floor or a raise; otherwise light. TARGETED: the money, gate, auth and secret classes the diff touches; any class dispatches the targeted reader at every depth. NEXT: full, scoped (a full pass on the range: fix-check, Red Team, native adversarial), fix-check, voices (gate class: Step 11 before any stop, @@ -102,6 +115,8 @@ function main(): number { if (!['none', 'full', 'fix-check'].includes(last)) { process.stderr.write('gstack-review-plan: --last must be none, full or fix-check\n'); return EXIT.usage; } const pass1Depth = (flag('--pass1-depth') ?? 'full') as Depth; if (!['light', 'standard', 'full'].includes(pass1Depth)) { process.stderr.write('gstack-review-plan: --pass1-depth must be light, standard or full\n'); return EXIT.usage; } + const fixChecks = Number(flag('--fix-checks') ?? (last === 'fix-check' ? '1' : '0')); + if (!Number.isInteger(fixChecks) || fixChecks < 0) { process.stderr.write('gstack-review-plan: --fix-checks needs an integer >= 0\n'); return EXIT.usage; } const pass = Number(flag('--pass') ?? '0'); if (!Number.isInteger(pass) || pass < 0) { process.stderr.write('gstack-review-plan: --pass needs an integer >= 0\n'); return EXIT.usage; } const floor = flag('--depth-floor') as Depth | undefined; @@ -111,6 +126,8 @@ function main(): number { const rangeBase = flag('--range-base'); if (has('--range-base') && (!rangeBase || rangeBase.startsWith('-'))) { process.stderr.write('gstack-review-plan: --range-base needs the fix range\'s base commit (this pass\'s PASS_HEAD)\n'); return EXIT.usage; } if (grant && !rangeBase) { process.stderr.write('gstack-review-plan: --grant needs --range-base (round 2\'s PASS_HEAD), which binds the grant to the stop it answers\n'); return EXIT.usage; } + const declared = list('--declare-risk'); + if (has('--declare-risk') && (!declared.length || declared.some(c => !(RISK_CLASSES as readonly string[]).includes(c)))) { process.stderr.write(`gstack-review-plan: --declare-risk ${flag('--declare-risk') ?? ''}: each class is one of ${RISK_CLASSES.join(', ')} (a declaration only raises the depth)\n`); return EXIT.usage; } const root = git(['rev-parse', '--show-toplevel'])?.trim(); if (!root) { process.stderr.write('gstack-review-plan: run it inside a git checkout\n'); return EXIT.fail; } process.chdir(root); @@ -152,6 +169,9 @@ function main(): number { const baseSha = collected.registry.sha ?? git(['rev-parse', '--verify', '-q', `${ref}^{commit}`])?.trim() ?? ''; const policy = baseSha ? loadPolicyAt(root, baseSha, ref) : null; const declares = !!policy && !isUnavailable(policy) && policy.policy.review_exclude_paths.length > 0; + const review = reviewPolicy(policy && !isUnavailable(policy) ? policy.policy : null); + const riskPaths = review.risk_paths.length ? { riskPaths: review.risk_paths } : {}; + const platform = review.platform === 'on' ? platformReviewAt(root, head, { pr: flag('--pr') }) : null; let tagged: Set | null = null; let gateTagged: number | null; if (declares) { @@ -179,7 +199,7 @@ function main(): number { else if (git(['diff', '--quiet', 'HEAD', '--']) === null || collected.untracked.length) fixRange = unread('uncommitted or untracked edits: commit the fixes first'); else { const commit = collectCommit(root, resolved, 'HEAD'); - const range = classifyDiff({ ...commit, registry, scope: scopeOf([resolved, '--head', 'HEAD']), gateTagged: gateTagsOf([resolved]), ...(active ? { excluded: active.excluded } : {}) }); + const range = classifyDiff({ ...commit, registry, scope: scopeOf([resolved, '--head', 'HEAD']), gateTagged: gateTagsOf([resolved]), ...riskPaths, ...(active ? { excluded: active.excluded } : {}) }); fixRange = range.errors.length ? unread(range.errors.map(e => `${e.code} ${e.input}`).join('; ')) : !commit.paths.length ? unread('the fix range is empty: commit the fix after the range base') : { base: rangeBase, runtime_lines: range.runtimeLines, ...(range.class === 'gate' ? { touched_gate: true as const } : {}) }; @@ -189,26 +209,29 @@ function main(): number { const patch = collected.mergeBase ? targetedPatchId(process.cwd(), collected.mergeBase) : 'none'; const brief = targetedBriefSha(); const plan = planReview({ - ...collected, ...exclusionInput, scope, gateTagged, targeted: { classes: targeted.classes, ran: !['uncommitted', 'none'].includes(patch) && flag('--targeted-ran') === patch }, numbers: scanNumbers(kept(added)), force, gated: list('--gated'), floor, tiers: auditEffort !== 'host', + ...collected, ...exclusionInput, ...riskPaths, declared: declared as RiskClass[], shape: { panel: review.panel, readerModel: review.reader_model, platform: review.platform, head, platformReview: platform?.ok ? platform.review : null }, scope, gateTagged, targeted: { classes: targeted.classes, ran: !['uncommitted', 'none'].includes(patch) && flag('--targeted-ran') === patch }, numbers: scanNumbers(kept(added)), force, gated: list('--gated'), floor, tiers: auditEffort !== 'host', criticalFound: has('--critical-found'), adversarialRan: has('--adversarial-ran'), reviewedNumbers: flag('--reviewed-numbers'), - history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: has('--fixed-below') && last !== 'fix-check', blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') || !!fixRange?.touched_gate, pass1Depth, rangeRuntimeLines: fixRange?.runtime_lines, granted: grant }, + history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: has('--fixed-below') && last !== 'fix-check', blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') || !!fixRange?.touched_gate, pass1Depth, rangeRuntimeLines: fixRange?.runtime_lines, granted: grant, fixChecks: has('--fix-checks') ? fixChecks : undefined }, }); const gateScan = { tagged: gateTagged, head }; if (has('--json')) { const { runtimeLines, stepDepth, depthReasons, ...rest } = plan; - process.stdout.write(JSON.stringify({ ...rest, targeted: { ...targeted, brief, targeted_patch_id: patch }, step_depth: stepDepth, runtime_lines: runtimeLines, depth_reasons: depthReasons, depth_floor: floor ?? null, gate_scan: gateScan, fix_range: fixRange ?? null, registry: { sha: registry.sha, outputs: registry.outputs }, per_path: perPath, diff_lines: diffLines, base: collected.base, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly, consumed: collected.consumed, drawings: collected.drawings, + process.stdout.write(JSON.stringify({ ...rest, review_policy: review, platform_review: platform ? (platform.ok ? platform.review : { none: platform.reason }) : null, targeted: { ...targeted, brief, targeted_patch_id: patch }, step_depth: stepDepth, runtime_lines: runtimeLines, depth_reasons: depthReasons, depth_floor: floor ?? null, gate_scan: gateScan, fix_range: fixRange ?? null, registry: { sha: registry.sha, outputs: registry.outputs }, per_path: perPath, diff_lines: diffLines, base: collected.base, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly, consumed: collected.consumed, drawings: collected.drawings, ...(exclusion.state === 'head-only' ? { review_exclude: exclusion } : active ? { review_exclude: { state: 'active', ...active.record, specs: active.specs, keep: active.keep, kept_files: active.kept, content: active.content, references: active.references } } : {}) }) + '\n'); return EXIT.ok; } const out = [`REVIEW_CLASS: ${plan.class} (${plan.reasons.slice(0, 3).join('; ') || 'no signals'})`, reviewDepthLine(plan), `GATE_SCAN: ${gateTagged === null ? 'unavailable' : `tagged=${gateTagged}`} head=${head}`, `REVIEW_BASE: ${ref} ${baseSha.slice(0, 12)}`, `TARGETED: classes=${classList(targeted.classes)} rules=${targeted.rules} brief=${brief} targeted_patch_id=${patch}${targeted.reasons.length ? ` reasons=${targeted.reasons.join('; ')}` : ''}`]; out.push(...excludeLines(exclusion, base)); + out.push(`REVIEW_MODE: ${plan.mode} panel=${review.panel} reader_model=${review.reader_model} platform=${review.platform}`); + if (declared.length) out.push(`RISK_DECLARED: classes=${plan.declared.join(',')} detected=${plan.detected.join(',') || 'none'}`); + if (platform) out.push(platform.ok ? `PLATFORM_REVIEW: ${platform.review.id} reviewer=${platform.review.reviewer} head=${platform.review.head}` : `PLATFORM_REVIEW: none (${platform.reason})`); if (fixRange) out.push(`FIX_RANGE: ${fixRange.runtime_lines === null ? `unread (${fixRange.error})` : `runtime_lines=${fixRange.runtime_lines}`} base=${fixRange.base}`); out.push(reviewPlanLine(plan, pass + (plan.next.step === 'full' || plan.next.step === 'scoped' ? 1 : 0)), `NEXT: ${plan.next.step} (${plan.next.reason})`); for (const e of plan.errors) out.push(`DEPTH_ERROR: ${e.code} input=${e.input} detail=${e.detail}; fix: ${e.repair}${e.result ? ` (${e.result})` : ''}`); for (const d of plan.dispatches) { const key = d.name.split(' ')[0]; - out.push(`DISPATCH: ${d.role} ${d.name} effort=${d.effort}${d.checklists ? ` checklists=${d.checklists.join(',')}` : ''}${d.reason ? ` reason=${d.reason}` : ''}`, `DIFF_CMD: ${key} ${diffCommand(d)}`); + out.push(`DISPATCH: ${d.role} ${d.name} effort=${d.effort}${d.model ? ` model=${d.model}` : ''}${d.checklists ? ` checklists=${d.checklists.join(',')}` : ''}${d.classes ? ` classes=${d.classes.join(',')}` : ''}${d.reason ? ` reason=${d.reason}` : ''}`, `DIFF_CMD: ${key} ${diffCommand(d)}`); if (!d.exclude.length) out.push(`DIFF_EXCLUDE: ${key} none`); const keepCmd = keepCommand(d); if (keepCmd) out.push(`DIFF_KEEP: ${key} ${keepCmd}`); diff --git a/bin/gstack-ship-receipt b/bin/gstack-ship-receipt index e45094d57..b9695b568 100755 --- a/bin/gstack-ship-receipt +++ b/bin/gstack-ship-receipt @@ -50,7 +50,8 @@ import { } from '../lib/ship-receipt'; import { checkDocsCaps, docsCapLine, docsExceptionLine, parseReceiptDocs } from '../lib/ship-receipt-docs'; import { comparePath, contextPacket, PACKET_SECTION_CAP, readScopedReview, touchedPaths } from '../lib/ship-patch'; -import { deeper, depthBelow, exceptionProblems, parseReviewLoopException, parseReviewTargeted, receiptExclude, receiptReviewAtHead, receiptTargetedAtHead, reviewLoopExceptionLine, targetedClassesSince } from '../lib/ship-receipt-depth'; +import { deeper, depthBelow, exceptionProblems, parseReviewLoopException, parseReviewTargeted, platformReceiptDepth, receiptExclude, receiptReviewAtHead, receiptTargetedAtHead, reviewLoopExceptionLine, targetedClassesSince } from '../lib/ship-receipt-depth'; +import { RISK_CLASSES, type RiskClass } from '../lib/review-risk'; import { checkReceiptProse, proseLine } from '../lib/ship-receipt-prose'; import { resolveStateRoot } from '../lib/state-root'; import { readPause, receiptBaseRed } from '../lib/base-red'; @@ -102,6 +103,11 @@ write flags: --pr --head --base --version --gated-tree --predecessor --gate (the same list gstack-review-plan --declare-risk took): joins review_depth's risk and only raises + its depth; review_depth.declared and .detected record both lists (GST-1129). An unknown class is RECEIPT_INVALID. + --platform-review (NEXT: platform's PLATFORM_REVIEW: id): records review_depth platform with capy-ai[bot]'s review at HEAD; + refused (RECEIPT_INVALID) unless the base policy has review.platform on, HEAD needs light review of a docs or tests diff with + no numeric, results or prereg claims, and that review is on the PR at HEAD. review_depth.mode records lean, panel or platform. --review-targeted ({classes, rules, brief_sha, status, findings, raised_by, head}: gstack-review-dispatch verify --targeted-out); recorded as review_targeted bound to the receipt's product_patch_id, and refused (RECEIPT_INVALID) when the commit the reader read (head) is not an ancestor or a change since it carries a targeted class (gate scan included). write and carry @@ -232,8 +238,16 @@ function cmdWrite(rest: string[], json: boolean): number { const line = reviewLoopExceptionLine(loopException.exception); if (bodyFile && !bodyText.split(/\r?\n/).some(l => l.trim() === line)) return refuse(`${bodyFile} does not carry the exception line verbatim: ${JSON.stringify(line)}`); } - const review = receiptReviewAtHead(repoRoot, base, path.dirname(Bun.fileURLToPath(import.meta.url)), headSha ?? head); - const reviewDepth = review.depth; + const declared = (flag(rest, '--declare-risk') ?? '').split(',').map(s => s.trim()).filter(Boolean); + if (rest.includes('--declare-risk') && (!declared.length || declared.some(c => !(RISK_CLASSES as readonly string[]).includes(c)))) { process.stderr.write(resultLine('gstack-ship-receipt write', 'RECEIPT_INVALID', `--declare-risk ${flag(rest, '--declare-risk') ?? ''}: each class is one of ${RISK_CLASSES.join(', ')}; nothing written`) + '\n'); return EXIT.fail; } + const review = receiptReviewAtHead(repoRoot, base, path.dirname(Bun.fileURLToPath(import.meta.url)), headSha ?? head, declared as RiskClass[]); + let reviewDepth = review.depth; + const platformId = flag(rest, '--platform-review'); + if (platformId !== undefined) { + const p = platformReceiptDepth(repoRoot, base, reviewDepth, headSha ?? head, platformId, flag(rest, '--pr')); + if (!p.ok) { process.stderr.write(resultLine('gstack-ship-receipt write', 'RECEIPT_INVALID', `${p.message}; nothing written`) + '\n'); return EXIT.fail; } + reviewDepth = p.depth; + } if (review.exclude.state === 'refused') { for (const l of review.exclude.lines) process.stderr.write(`${l}; nothing written\n`); return EXIT.refused; } const excludeNow = review.exclude.state === 'active' ? review.exclude.record : null; const excludeGiven = flag(rest, '--review-exclude') ?? 'none'; diff --git a/bin/gstack-skill-bill b/bin/gstack-skill-bill index 86aa1c657..bb6e6d755 100755 --- a/bin/gstack-skill-bill +++ b/bin/gstack-skill-bill @@ -11,7 +11,8 @@ Names the skill's kind: gstack-checkout (edit SKILL.md.tmpl, then bun run gen:sk gstack-install (never edit; gstack-upgrade overwrites it) or user. BASELINE is the newest commit with a "Skill-Diet: " trailer; DUE means the authored file grew over 10% since then. In a gstack checkout, EVAL TASKS names test/fixtures/skill-evals//tasks.json (or none) and -EVAL_TASKS_INVALID flags a file that breaks docs/designs/SKILL_EVAL_CONTRACT.md's task format. +EVAL_TASKS_INVALID flags a file that breaks docs/designs/SKILL_EVAL_CONTRACT.md's task format, and +EVIDENCE prints the skill's row in docs/inventory/evidence.json: its status and source, plan or reason. A name must match exactly; pass a path when it names more than one skill. Exit 0 on a report, 2 on SKILL_NOT_FOUND, SKILL_AMBIGUOUS or SKILL_MD_MISSING (each with a fix line).`; diff --git a/bin/gstack-skill-route b/bin/gstack-skill-route index 5af371c41..c0273bb4d 100755 --- a/bin/gstack-skill-route +++ b/bin/gstack-skill-route @@ -4,19 +4,30 @@ * * Usage: * gstack-skill-route --explain "" [--json] [--threshold ] + * gstack-skill-route --default * * Prints the switch state, the threshold, every candidate skill with its * score and best trigger, the route the hook would take and the exact line it * would inject. Works with the switch off (a dry run): it never injects - * anything. Exit 0, or 2 on a usage error. Docs: docs/skill-routing-and-contracts.md. + * anything. --default prints a switch's SWITCH_DEFAULT (on or off), the value + * setup and gstack-config use when the switch is unset. Exit 0, or 2 on a usage error. Docs: docs/skill-routing-and-contracts.md. */ import { - ROUTE_THRESHOLD, hintLine, installedSkillsDir, readSkillSwitch, routePrompt, scanInstalledSkills, + ROUTE_THRESHOLD, SWITCH_DEFAULT, hintLine, installedSkillsDir, readSkillSwitch, routePrompt, scanInstalledSkills, } from '../lib/skill-route'; -const USAGE = 'Usage: gstack-skill-route --explain "" [--json] [--threshold ]'; +const USAGE = 'Usage: gstack-skill-route --explain "" [--json] [--threshold ]\n gstack-skill-route --default '; const args = process.argv.slice(2); if (args[0] === '--help' || args[0] === '-h') { process.stdout.write(`${USAGE}\n`); process.exit(0); } +if (args[0] === '--default') { + const name = args[1] ?? ''; + if (!Object.hasOwn(SWITCH_DEFAULT, name)) { + process.stderr.write(`SKILL_ROUTE_USAGE: --default needs skill_routing or skill_contracts\n${USAGE}\n`); + process.exit(2); + } + process.stdout.write(`${SWITCH_DEFAULT[name as keyof typeof SWITCH_DEFAULT]}\n`); + process.exit(0); +} const at = args.indexOf('--explain'); const prompt = at >= 0 ? args[at + 1] : undefined; const tAt = args.indexOf('--threshold'); diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index 058590a71..d672b0ff7 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -2215,7 +2215,11 @@ export class BrowserManager { // STEALTH_LAUNCH_ARGS the handed-off browser kept the AutomationControlled // tell that the other two paths strip. const launchArgs: string[] = ['--hide-crash-restore-bubble', ...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs(), ...this.proxyLaunchArgs()]; - if (extensionPath) { + if (extensionPath && isCustomChromium()) { + // Same rule as launchHeaded(): a build that bakes the extension in + // crashes when it is loaded twice. + console.log('[browse] Handoff: custom Chromium bakes in the extension — skipping --load-extension'); + } else if (extensionPath) { launchArgs.push(`--disable-extensions-except=${extensionPath}`); launchArgs.push(`--load-extension=${extensionPath}`); // Auth token is served via POST /extension-token (pinned-origin @@ -2234,27 +2238,31 @@ export class BrowserManager { fs.mkdirSync(userDataDir, { recursive: true }); cleanSingletonLocks(userDataDir); - // Self-heal probe (#2242): handoff always launches the Playwright-cache - // bundle (this launchPersistentContext call passes no executablePath), - // so a bundle poisoned by the old in-place rebrand would GPU-crash here - // exactly like launchHeaded(). Same probe, same contract: a - // GSTACK_CHROMIUM_PATH bundle is never passed in. The rethrown typed - // error surfaces through the outer catch as the actionable - // "Cannot open headed browser" message, headless browser untouched. - try { - probePoisonedChromiumBundle(chromium.executablePath()); - } catch (err: unknown) { - if (err instanceof PoisonedBundleError) throw err; - // Probe failures (no bundle yet, EACCES) fall through to launch. + // #3907: same custom-binary support as launch()/launchHeaded(). + const executablePath = process.env.GSTACK_CHROMIUM_PATH || undefined; + + // Self-heal probe (#2242): without a custom path, handoff launches the + // Playwright-cache bundle, so a bundle poisoned by the old in-place + // rebrand would GPU-crash here exactly like launchHeaded(). Same probe, + // same contract: a GSTACK_CHROMIUM_PATH bundle is never probed. The + // rethrown typed error surfaces through the outer catch as the + // actionable "Cannot open headed browser" message, headless browser + // untouched. + if (!executablePath) { + try { + probePoisonedChromiumBundle(chromium.executablePath()); + } catch (err: unknown) { + if (err instanceof PoisonedBundleError) throw err; + // Probe failures (no bundle yet, EACCES) fall through to launch. + } } // T1: same automation-tell-stripping defaults as launchHeaded(). // The handoff path (headless → headed re-launch) takes the same // anti-detection posture. const { STEALTH_IGNORE_DEFAULT_ARGS } = await import('./stealth'); - // XProtect self-heal wrapper (P0 #2554): handoff always launches the - // Playwright-cache bundle (no executablePath), so the heal applies - // exactly as in launch()/launchHeaded(). + // XProtect self-heal wrapper (P0 #2554), scoped exactly as in + // launch()/launchHeaded(): never heals a GSTACK_CHROMIUM_PATH bundle. newContext = await launchWithXProtectHeal(() => chromium.launchPersistentContext(userDataDir, { headless: false, ...(this.xvfb ? { env: { ...process.env, DISPLAY: this.xvfb.display } } : {}), @@ -2268,10 +2276,11 @@ export class BrowserManager { chromiumSandbox: shouldEnableChromiumSandbox(), args: launchArgs, viewport: null, + ...(executablePath ? { executablePath } : {}), ...(this.proxyConfig ? { proxy: this.proxyConfig } : {}), ignoreDefaultArgs: STEALTH_IGNORE_DEFAULT_ARGS, timeout: 15000, - })); + }), { usesCustomExecutable: Boolean(executablePath) }); if (this.closing) { await newContext.close().catch(() => {}); throw new Error('Browser is shutting down'); diff --git a/browse/test/browser-manager-custom-chromium.test.ts b/browse/test/browser-manager-custom-chromium.test.ts index 782a8eece..4cd167320 100644 --- a/browse/test/browser-manager-custom-chromium.test.ts +++ b/browse/test/browser-manager-custom-chromium.test.ts @@ -1,5 +1,9 @@ -import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { isCustomChromium } from '../src/browser-manager'; +import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { chromium } from 'playwright'; +import { BrowserManager, isCustomChromium } from '../src/browser-manager'; describe('browser-manager: isCustomChromium', () => { let origPath: string | undefined; @@ -65,3 +69,72 @@ describe('browser-manager: isCustomChromium', () => { expect(isCustomChromium()).toBe(false); }); }); + + +// #3907: handoff() must launch the browser GSTACK_CHROMIUM_PATH names, like +// launch() and launchHeaded(). The Playwright launch is a spy that rejects, so +// no window opens and handoff() returns its "Cannot open headed browser" error. +describe('handoff: GSTACK_CHROMIUM_PATH', () => { + const EXTENSION = '/opt/gstack/extension'; + const ENV_KEYS = ['GSTACK_CHROMIUM_PATH', 'GSTACK_CHROMIUM_KIND', 'CHROMIUM_PROFILE'] as const; + const saved: Record = {}; + let tmpDir: string; + let launch: ReturnType; + let bundledPath: ReturnType; + + function promotable(): BrowserManager { + const manager = new BrowserManager(); + Object.assign(manager as unknown as Record, { + browser: {}, + context: {}, + saveState: async () => ({ cookies: [], pages: [] }), + ensureHeadedDisplay: async () => {}, + findExtensionPath: () => EXTENSION, + }); + return manager; + } + + const launchOptions = () => (launch.mock.calls[0] as [string, { executablePath?: string; args: string[] }])[1]; + + beforeEach(() => { + for (const key of ENV_KEYS) saved[key] = process.env[key]; + delete process.env.GSTACK_CHROMIUM_PATH; + delete process.env.GSTACK_CHROMIUM_KIND; + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'handoff-custom-chromium-')); + process.env.CHROMIUM_PROFILE = path.join(tmpDir, 'profile'); + launch = spyOn(chromium, 'launchPersistentContext').mockRejectedValue(new Error('launch spy')); + bundledPath = spyOn(chromium, 'executablePath').mockReturnValue(path.join(tmpDir, 'no-bundle', 'chrome')); + }); + + afterEach(() => { + launch.mockRestore(); + bundledPath.mockRestore(); + for (const key of ENV_KEYS) { + if (saved[key] === undefined) delete process.env[key]; + else process.env[key] = saved[key]; + } + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + test('launches the GSTACK_CHROMIUM_PATH binary and never probes the Playwright bundle', async () => { + process.env.GSTACK_CHROMIUM_PATH = '/opt/chrome/chrome'; + expect(await promotable().handoff('custom')).toContain('Cannot open headed browser'); + expect(launch).toHaveBeenCalledTimes(1); + expect(launchOptions().executablePath).toBe('/opt/chrome/chrome'); + expect(bundledPath).not.toHaveBeenCalled(); + }); + + test('without GSTACK_CHROMIUM_PATH it launches the probed Playwright bundle', async () => { + expect(await promotable().handoff('bundled')).toContain('Cannot open headed browser'); + expect('executablePath' in launchOptions()).toBe(false); + expect(bundledPath).toHaveBeenCalled(); + expect(launchOptions().args).toContain(`--load-extension=${EXTENSION}`); + }); + + test('a build that bakes the extension in never gets it loaded twice', async () => { + process.env.GSTACK_CHROMIUM_PATH = '/Applications/GBrowser.app/Contents/MacOS/GBrowser'; + await promotable().handoff('baked'); + expect(launchOptions().executablePath).toBe('/Applications/GBrowser.app/Contents/MacOS/GBrowser'); + expect(launchOptions().args.filter((arg) => arg.includes('extension'))).toEqual([]); + }); +}); diff --git a/browse/test/cookie-import-native-qualification.ts b/browse/test/cookie-import-native-qualification.ts index 8c703c9fe..5ba173c60 100644 --- a/browse/test/cookie-import-native-qualification.ts +++ b/browse/test/cookie-import-native-qualification.ts @@ -66,7 +66,7 @@ const nodeInfo = JSON.parse(nodeProbe.stdout); if (nodeInfo.architecture !== process.arch || !['x64', 'arm64'].includes(process.arch)) incomplete('runtime_architecture_mismatch'); const require = createRequire(import.meta.url); const playwrightVersion = require('playwright/package.json').version; -if (playwrightVersion !== '1.62.1') incomplete('playwright_1_62_1_required'); +if (playwrightVersion !== '1.64.1') incomplete('playwright_1_64_1_required'); const versionProbe = spawnSync(path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'), [ '-NoProfile', '-NonInteractive', '-Command', NATIVE_BROWSER_VERSION_COMMAND, ], { env: { ...environment, GSTACK_QUALIFY_BROWSER_EXE: executable }, encoding: 'utf8', timeout: 10_000, windowsHide: true }); diff --git a/browse/test/cookie-import-native.test.ts b/browse/test/cookie-import-native.test.ts index 11c462355..013d00da1 100644 --- a/browse/test/cookie-import-native.test.ts +++ b/browse/test/cookie-import-native.test.ts @@ -213,7 +213,7 @@ async function qualifiedAdapterFixture() { const receipt = { browserName: 'Edge', architecture: runtime.architecture, windowsRelease: release(), executableSha256: '0'.repeat(64), nodeVersion: runtime.version, bunVersion: Bun.version, - playwrightVersion: '1.62.1', sourceHashes, + playwrightVersion: '1.64.1', sourceHashes, }; const virtualRoot = `C:\\gstack-${path.basename(fixture)}`; const windowsEnv = { LOCALAPPDATA: `${virtualRoot}\\Local`, PROGRAMFILES: `${virtualRoot}\\Apps`, 'PROGRAMFILES(X86)': `${virtualRoot}\\Apps32` }; diff --git a/browse/test/playwright-core-patch.test.ts b/browse/test/playwright-core-patch.test.ts index f0974cbd3..eb34ed1d2 100644 --- a/browse/test/playwright-core-patch.test.ts +++ b/browse/test/playwright-core-patch.test.ts @@ -1,8 +1,10 @@ /** - * #2160/#1989: playwright-core is bun-patched to pass windowsHide at its two + * #2160/#1989: playwright-core must pass windowsHide at its two * Windows-visible child_process sites — the browser launch spawn (Node * defaults windowsHide to FALSE for spawn, so browser children could flash a - * console window) and the force-kill taskkill spawnSync. This is the repo's + * console window) and the force-kill taskkill spawnSync. The launch site is + * bun-patched; the taskkill site is upstream since Playwright 1.64 (#3907), + * so only the installed-tree check below covers it. This is the repo's * first patchedDependencies entry; these static checks pin the three-legged * coherence (patch file ↔ package.json ↔ installed tree) so a playwright * bump that forgets to re-target the patch fails CI instead of silently @@ -35,15 +37,14 @@ describe('playwright-core windowsHide patch (#2160, #1989)', () => { expect(pkg.patchedDependencies[key]).toBe(`patches/playwright-core@${version}.patch`); }); - test('the patch file exists and carries both windowsHide sites', () => { + test('the patch file exists and carries the launch windowsHide site', () => { const version = installedPlaywrightCoreVersion(); const patchPath = path.join(ROOT, 'patches', `playwright-core@${version}.patch`); expect(fs.existsSync(patchPath)).toBe(true); + expect(fs.existsSync(path.join(ROOT, 'patches', 'playwright-core@1.62.1.patch'))).toBe(false); const patch = fs.readFileSync(patchPath, 'utf-8'); // Launch spawnOptions site. expect(patch).toContain('+ windowsHide: true,'); - // taskkill force-kill site. - expect(patch).toContain('shell: true, windowsHide: true'); }); test('bun.lock is coherent: the lockfile records the patched dependency', () => { @@ -63,13 +64,28 @@ describe('playwright-core windowsHide patch (#2160, #1989)', () => { // Windows Free Tests 40656299153 and 41056150287 (cookie-picker-ui, serial retry passed): goto failed with // net::ERR_NO_BUFFER_SPACE on 127.0.0.1. Chromium's TcpPortRandomizationWin raises WSAENOBUFS with ports free; -// microsoft/playwright#43605 disables it in 1.65, and the 1.62.1 patch backports it; drop this block at 1.65. +// microsoft/playwright#43605 disables it in 1.65, and the 1.64.1 patch backports it; drop this block at 1.65. describe('playwright-core TcpPortRandomizationWin backport (microsoft/playwright#43605)', () => { test('the patch and the installed tree disable the feature in the --disable-features list', () => { - expect(fs.readFileSync(path.join(ROOT, 'patches', 'playwright-core@1.62.1.patch'), 'utf-8')).toContain('+ "TcpPortRandomizationWin"'); + expect(fs.readFileSync(path.join(ROOT, 'patches', 'playwright-core@1.64.1.patch'), 'utf-8')).toContain('+ "TcpPortRandomizationWin"'); const bundle = fs.readFileSync(path.join(ROOT, 'node_modules', 'playwright-core', 'lib', 'coreBundle.js'), 'utf-8'); const list = bundle.slice(bundle.indexOf('disabledFeatures = ['), bundle.indexOf('].filter(Boolean);', bundle.indexOf('disabledFeatures = ['))); expect(list).toContain('"TcpPortRandomizationWin"'); expect(bundle).toContain('"--disable-features=" + disabledFeatures.join(",")'); }); }); + +// #3907: CVE-2026-106378 (macOS GPU-process sandbox escape, crbug.com/507596239) is fixed by Chromium +// commit 0c2f3e54b265 (main@{#1695786}), first tagged in 155.0.8059.12. Playwright 1.62.1 shipped +// 151.0.7922.34, which lacks it. A Playwright pin whose bundled Chromium is older reopens the escape. +describe('bundled Chromium carries the CVE-2026-106378 fix (#3907)', () => { + test('playwright-core pins Chromium 155.0.8059.12 or later', () => { + const browsers = JSON.parse(fs.readFileSync( + path.join(ROOT, 'node_modules', 'playwright-core', 'browsers.json'), 'utf-8', + )) as { browsers: Array<{ name: string; browserVersion: string }> }; + const version = browsers.browsers.find((b) => b.name === 'chromium')!.browserVersion.split('.').map(Number); + const floor = [155, 0, 8059, 12]; + const cmp = version.map((part, i) => part - floor[i]).find((d) => d !== 0) ?? 0; + expect(cmp).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/browse/test/security-audit-r2.test.ts b/browse/test/security-audit-r2.test.ts index cd123f5db..9db45e9a1 100644 --- a/browse/test/security-audit-r2.test.ts +++ b/browse/test/security-audit-r2.test.ts @@ -251,7 +251,7 @@ describe('cookie-import domain validation', () => { reset: 'not_requested', verification: { verified: false, reason: 'not_requested' }, message: 'Cookie copy complete.', }); let currentUrl = 'https://example.test'; - const page = { url: () => currentUrl, isClosed: () => false }; + const page = { url: () => currentUrl, isClosed: () => false } as any; const session = { getPage: () => page, getActiveFrameOrPage: () => page, getFrame: () => null } as any; const manager = { trackCookieImportDomains() {} } as any; try { diff --git a/bun.lock b/bun.lock index a32b356bc..d3d3ffbb5 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "html-to-docx": "1.8.0", "html-to-vdom": "0.7.0", "marked": "^18.1.1", - "playwright": "^1.62.1", + "playwright": "^1.64.1", "sharp": "^0.35.4", "socks": "^2.8.10", "xterm": "^5.3.0", @@ -28,7 +28,7 @@ }, }, "patchedDependencies": { - "playwright-core@1.62.1": "patches/playwright-core@1.62.1.patch", + "playwright-core@1.64.1": "patches/playwright-core@1.64.1.patch", }, "overrides": { "@hono/node-server": "2.1.6", @@ -335,8 +335,6 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -469,9 +467,9 @@ "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], - "playwright": ["playwright@1.62.1", "", { "dependencies": { "playwright-core": "1.62.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg=="], + "playwright": ["playwright@1.64.1", "", { "dependencies": { "playwright-core": "1.64.1" }, "bin": { "playwright": "cli.js" } }, "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA=="], - "playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="], + "playwright-core": ["playwright-core@1.64.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ=="], "prettier": ["prettier@3.9.10", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA=="], diff --git a/capy/skill/gstack/factory.md b/capy/skill/gstack/factory.md index 7e472397c..3a334a6bd 100644 --- a/capy/skill/gstack/factory.md +++ b/capy/skill/gstack/factory.md @@ -1,17 +1,17 @@ -# How we work: the gstack software factory on Capy +# How we work: gstack's factory on Capy -What seven Captain crews converged on (2026-10-29). Your crew's CREW.md and recorded approvals decide who may merge, update a base, spend or touch production; urgency changes priority, never permissions or gates. With no Captain, ask the person and never merge without their word. Story: capy/README.md; examples: your org Drive's software-factory/README.md, if any. +What seven Captain crews agreed on. Your crew's CREW.md and recorded approvals decide who may merge, update a base, spend or touch production; urgency changes priority, never permissions or gates. With no Captain, ask the person; never merge without their word. Story: capy/README.md; examples: org Drive software-factory/README.md. 1. **Plan, then a yes.** New functionality starts with /autoplan sized to the work; the build waits for "do it". [SKILL.md: Plan, approve, ship] 2. **Someone else merges.** Run /ship to a receipt on your final pushed head, send one READY quoting gstack's verify output verbatim, and stop. The merge owner merges on green and assigns versions. [docs/captain.md: The READY contract] 3. **Block only on the floor.** A finding blocks if it would fail a test, break production, or lose data or secrets; the rest goes in the PR body or next wave. [docs/review-policy.md] 4. **Fix waves.** When main goes red, hold /ship while one owner finds the root cause; bring main in once, as your rules allow. Fold deferred findings and test follow-ups into one PR. [docs/triage.md] -5. **Severe first, same gates.** A severe user-facing bug jumps the queue; the plan can be short; receipt, checks and merge owner stay. -6. **Trade findings, not small talk.** Another crew's finding goes to its Captain, through yours, with the evidence. +5. **Severe first, same gates.** A severe user-facing bug jumps the queue with a short plan; receipt, checks and merge owner stay. +6. **Trade findings, not small talk.** Another crew's finding goes to its Captain, through yours, with evidence. 7. **Measure before you act.** Run the real system on the merged tree you ship; a real run outranks a reviewed plan. Each number names its run, commit or uncapped query; say what you did not verify. -8. **Evals decide, and losses get reported.** Preregister bars, cap and stop rule before paying [/eval-plan]. Cheap proof (a keyless fake, a smoke) before the paid run. Use controls (no-skill and placebo arms, clean decoys). Report use and pass apart; quote spend from the meter. -9. **Flakes: one rerun, a ledger, a daily fix.** Only the rerun the policy allows, never a harness or infra red; a second red is real; no test is loosened. [docs/captain.md: The daily flaky pass] +8. **Evals decide, and losses get reported.** Preregister bars, cap and stop rule before paying [/eval-plan]. Cheap proof (a keyless fake, a smoke) first. Controls: no-skill and placebo arms, clean decoys. Report use and pass apart; quote spend from the meter. A new part needs proof it helps or a dated test plan [ETHOS.md: 9. Earn Its Place]. +9. **Flakes: one rerun, a ledger, a daily fix.** Only the policy's rerun, never for a harness or infra red; a second red is real; never loosen a test. [docs/captain.md: The daily flaky pass] 10. **Quiet.** Message on a decision, a blocker, a READY or a finished phase. A broadcast needing no reply goes in the Captain's notes; never reply just to acknowledge. -11. **Ask once, plainly.** One ask per decision, explained like the owner is five: your Captain's name, then a lettered line each: "A: hit problem p, which happens when h. We could do 1) x, 2) y or 3) z. Recommendation x because why. A1 to take recommendation." "do it" takes every recommendation; "A2" takes option 2. Owner tasks: the exact link (in Capy: path and field), numbered simple steps, the reply word. Never re-ask a standing rule; check what you can yourself. [docs/captain.md: The ask format] +11. **Ask once, plainly.** One ask per decision, explained like the owner is five: your Captain's name, then a lettered line each: "A: hit problem p, which happens when h. We could do 1) x, 2) y or 3) z. Recommendation x because why. A1 to take recommendation." "do it" takes every recommendation; "A2" takes option 2. Owner tasks: the exact link (in Capy: path and field), numbered steps, the reply word. Never re-ask a standing rule; check what you can yourself. [docs/captain.md: The ask format] 12. **Fresh threads, closed out.** One thread per unit of work (crew threads ask their Captain); past the context limit, hand off. Its Captain archives only its crew's threads; threads outside every crew are the owner's, even its automation's. Size reviews to the diff; read the token counter. [docs/ship-budget.md] 13. **The owner's model preference.** `gstack-models resolve --role agent` picks agent models, except cross-family voices and eval subjects. [docs/model-policy.md: Your model preference] diff --git a/docs/TESTING_INTERNALS.md b/docs/TESTING_INTERNALS.md index 38ce1e467..f4cc39a74 100644 --- a/docs/TESTING_INTERNALS.md +++ b/docs/TESTING_INTERNALS.md @@ -1871,6 +1871,24 @@ own attempts. The arm gets: - the same budget treatment: it is priced and reserved in the pilot like every other arm. +**Masking.** On Capy, the owner's user-scope instructions tell every session to +load the user-drive gstack skill, and eval run agents inherit them: in one +checkpoint (24 runs, 3 arms) every run in every arm tried to read that skill in +its first minute. So `## No-gstack arm` also needs three lines, and `prereg +--check` refuses a missing, blank or `none` one (`PREREG_NO_GSTACK_ARM`): + +- `masking:` an empty directory bind-mounted over `~/.capy/drive/user-garry-tan` + and `~/.capy/drive/org-gstack-gbrain` before the run agent starts, or a + container that sees no Capy drive; +- `install:` no gstack on the arm's machine (`~/.local/share/gstack`, + `~/.claude/skills`), checked before the run; +- `audit:` a per-arm audit of drive and skill reads. + +An arm that read a gstack skill file is contaminated and gets zero credit. A +no-gstack result that ran before this rule without a mask is not evidence for +an R48 verdict until it is re-checked; the evidence ledger records it under +`prior` with `unmasked` ([docs/inventory.md](inventory.md)). + **Reporting.** The no-gstack result is one row beside the variants in every results table and every verdict, including when it wins. A comparison that drops the row has not reported its result. `gstack-eval-plan results --check diff --git a/docs/inventory.md b/docs/inventory.md new file mode 100644 index 000000000..5cef90170 --- /dev/null +++ b/docs/inventory.md @@ -0,0 +1,249 @@ +# gstack inventory: every part, its load per run and its evidence + +Generated by `bin/gstack-inventory` from [evidence.json](inventory/evidence.json), the hand-kept ledger, and lib/context-bill.ts. Measured at commit cd67c41d8 (2026-11-02). This is a dated snapshot: rerun `bin/gstack-inventory` to refresh it; `bin/gstack-inventory --check` validates the ledger. The rule: [ETHOS.md: 9. Earn Its Place](../ETHOS.md#9-earn-its-place). + +Token counts are offline estimates (estimate: calibrated bytes/token per content class, about ±40%), not billed tokens. + +| Status | Parts | +|---|---| +| helps | 0 | +| no-effect | 1 | +| hurts | 0 | +| untested, with a plan | 17 | +| untested, plan none | 48 | +| safety | 5 | + +## Parts by load per run + +Load per run: a skill's SKILL.md plus the files it must read on every use (`perInvocationTokens` in lib/context-bill.ts); a shared part's files. Tier 2+ skills also read the shared preamble once per session (its own row). On demand: the skill's other sections/ and references/ files, read when a step asks for them; files a shared row also counts, such as /ship's review sections, appear in both. Always-on: frontmatter every session's skill scanner reads. + +| Rank | Part | Kind | Load per run (tokens) | On demand (tokens) | Always-on (tokens) | Loads | Status | Source, plan or reason | +|---|---|---|---|---|---|---|---|---| +| 1 | ship-review-panel | shared | 22,830 | n/a | n/a | every /ship run, by depth | untested | plan: GEVA-2 registry row R56-1 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 2 | design-review | skill | 20,159 | 7,778 | 101 | every use | untested | plan: GEVA-2 registry row R56-9 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 3 | ship | skill | 14,587 | 69,678 | 91 | every use | untested | plan: GEVA-2 registry row R56-4 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 4 | plan-ceo-review | skill | 14,394 | 21,203 | 313 | every use | untested | plan: GEVA-2 registry row R56-5 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 5 | review | skill | 13,990 | 41,146 | 75 | every use | untested | plan: R2 (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-02-geva-122-r2-amendment (#129, main eae6efc), told /review vs a no-gstack arm, Opus 5.5, preset on both arms (grandfathered) | +| 6 | devex-review | skill | 13,911 | 0 | 74 | every use | untested | plan: GEVA-2 registry row R56-11 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 7 | plan-design-review | skill | 13,351 | 12,986 | 79 | every use | untested | plan: GEVA-2 registry row R56-7 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 8 | retro | skill | 12,257 | 1,514 | 225 | every use | untested | plan: GEVA-2 registry row R56-12 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 9 | plan-devex-review | skill | 11,104 | 20,474 | 80 | every use | untested | plan: GEVA-2 registry row R56-6 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 10 | autoplan-phases | shared | 10,709 | n/a | n/a | every /autoplan run, by scope | untested | plan: folded into GEVA-2 registry row R56-3 (autoplan); proposed, not yet preregistered (grandfathered) | +| 11 | office-hours | skill | 10,424 | 23,078 | 280 | every use | untested | plan: none (grandfathered) | +| 12 | autoplan | skill | 10,288 | 18,701 | 394 | every use | untested | plan: GEVA-2 registry row R56-3 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 13 | plan-eng-review | skill | 9,837 | 25,963 | 74 | every use | untested | plan: GEVA-2 registry row R56-2 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 14 | design-consultation | skill | 9,557 | 10,864 | 282 | every use | untested | plan: GEVA-2 registry row R56-10 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 15 | setup-gbrain | skill | 9,099 | 10,921 | 106 | every use | untested | plan: none (grandfathered) | +| 16 | codex | skill | 8,991 | 10,358 | 68 | every use | untested | plan: none (grandfathered) | +| 17 | sync-gbrain | skill | 8,863 | 0 | 85 | every use | untested | plan: none (grandfathered) | +| 18 | design-shotgun | skill | 8,470 | 1,208 | 248 | every use | untested | plan: none (grandfathered) | +| 19 | plan-tune | skill | 8,377 | 0 | 107 | every use | untested | plan: none (grandfathered) | +| 20 | spec | skill | 8,305 | 7,759 | 92 | every use | untested | plan: none (grandfathered) | +| 21 | browse | skill | 8,125 | 5,597 | 81 | every use | untested | plan: none (grandfathered) | +| 22 | design-html | skill | 8,096 | 5,844 | 82 | every use | untested | plan: none (grandfathered) | +| 23 | land-and-deploy | skill | 7,652 | 16,188 | 63 | every use | untested | plan: GEVA-2 registry row R56-8 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 24 | benchmark | skill | 7,430 | 961 | 72 | every use | untested | plan: none (grandfathered) | +| 25 | catalog | shared | 7,291 | n/a | 7,291 | every session | untested | plan: Run D (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-01-geva-125-untold-discovery-prereg (#125, main 9820b14), untold gstack vs placebo vs no gstack, preset vs stub, Opus 5.5, GPT-6.1 Sol and particle-1; also measures /investigate's description (grandfathered) | +| 26 | investigate | skill | 7,086 | 3,729 | 490 | every use | no-effect | sources: GST-814 (HARN-1, 2026-11-02), org-gstack-gbrain handoffs/HARN-14/hard-follow-2026-11/decision-rule.md; R48 CUT-CANDIDATE | +| 27 | canary | skill | 7,034 | 0 | 66 | every use | untested | plan: none (grandfathered) | +| 28 | qa | skill | 6,951 | 13,877 | 74 | every use | untested | plan: none (grandfathered) | +| 29 | scrape | skill | 6,647 | 0 | 80 | every use | untested | plan: none (grandfathered) | +| 30 | captain | skill | 6,522 | 7,856 | 127 | every use | untested | plan: none (grandfathered) | +| 31 | preamble | shared | 5,743 | n/a | n/a | every tier 2+ skill run, inside that skill's load | untested | plan: I1 ceremony cut (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-01-geva-108-ceremony-cut-prereg (#122, main 409ff47) with its G3 amendment (#128), preset, Opus 5.5 and particle-1 (grandfathered) | +| 32 | gstack-upgrade | skill | 5,726 | 0 | 59 | every use | untested | plan: none (grandfathered) | +| 33 | context-restore | skill | 5,715 | 1,734 | 84 | every use | untested | plan: none (grandfathered) | +| 34 | document-generate | skill | 5,466 | 0 | 110 | every use | untested | plan: none (grandfathered) | +| 35 | ios-qa | skill | 5,368 | 1,378 | 84 | every use | untested | plan: none (grandfathered) | +| 36 | skillify | skill | 5,317 | 0 | 80 | every use | untested | plan: none (grandfathered) | +| 37 | capy-skill | shared | 5,309 | n/a | n/a | every Capy session | untested | plan: none (grandfathered) | +| 38 | health | skill | 5,287 | 0 | 68 | every use | untested | plan: none (grandfathered) | +| 39 | setup-deploy | skill | 5,216 | 0 | 73 | every use | untested | plan: none (grandfathered) | +| 40 | qa-only | skill | 5,205 | 3,460 | 68 | every use | untested | plan: none (grandfathered) | +| 41 | eval-plan | skill | 5,021 | 0 | 108 | every use | untested | plan: none (grandfathered) | +| 42 | gstack | skill | 4,951 | n/a | 59 | every use | untested | plan: none (grandfathered) | +| 43 | pair-agent | skill | 4,811 | 0 | 63 | every use | untested | plan: none (grandfathered) | +| 44 | make-pdf | skill | 4,730 | 1,767 | 65 | every use | untested | plan: none (grandfathered) | +| 45 | context-save | skill | 4,637 | 0 | 65 | every use | untested | plan: none (grandfathered) | +| 46 | cso | skill | 4,456 | 4,759 | 95 | every use | safety | reason: security: supported security findings with explicit coverage; kept on its security reason (GEVA-2 marks it exempt; GStack Minder may reclassify it) | +| 47 | open-gstack-browser | skill | 4,429 | 0 | 74 | every use | untested | plan: none (grandfathered) | +| 48 | design-motion | skill | 4,295 | 4,995 | 86 | every use | untested | plan: none (grandfathered) | +| 49 | test-audit | skill | 4,201 | 0 | 82 | every use | untested | plan: none (grandfathered) | +| 50 | document-release | skill | 4,049 | 10,260 | 72 | every use | untested | plan: none (grandfathered) | +| 51 | diagram | skill | 3,895 | 0 | 112 | every use | untested | plan: none (grandfathered) | +| 52 | benchmark-models | skill | 3,822 | 0 | 77 | every use | untested | plan: none (grandfathered) | +| 53 | ubicloud | skill | 3,593 | 0 | 83 | every use | untested | plan: none (grandfathered) | +| 54 | modernize | skill | 3,429 | 0 | 106 | every use | untested | plan: none (grandfathered) | +| 55 | setup-browser-cookies | skill | 3,184 | 0 | 79 | every use | untested | plan: none (grandfathered) | +| 56 | deslop-shared-libs | skill | 3,100 | 0 | 64 | every use | untested | plan: none (grandfathered) | +| 57 | landing-report | skill | 2,971 | 0 | 71 | every use | untested | plan: none (grandfathered) | +| 58 | ship-docs-audit | shared | 2,730 | n/a | n/a | every /ship run | untested | plan: folded into GEVA-2 registry row R56-4 (ship); proposed, not yet preregistered (grandfathered) | +| 59 | verify | skill | 2,679 | 0 | 91 | every use | untested | plan: none (grandfathered) | +| 60 | ios-design-review | skill | 2,367 | 0 | 76 | every use | untested | plan: none (grandfathered) | +| 61 | ios-sync | skill | 2,240 | 0 | 90 | every use | untested | plan: none (grandfathered) | +| 62 | ios-clean | skill | 2,214 | 0 | 86 | every use | untested | plan: none (grandfathered) | +| 63 | write-skill | skill | 2,158 | 795 | 100 | every use | untested | plan: none (grandfathered) | +| 64 | ios-fix | skill | 2,096 | 0 | 69 | every use | untested | plan: none (grandfathered) | +| 65 | learn | skill | 1,989 | 0 | 66 | every use | untested | plan: none (grandfathered) | +| 66 | careful | skill | 1,126 | 0 | 171 | every use | safety | reason: safety guard: warns before destructive commands (rm -rf, DROP TABLE, force-push); kept on that reason, an average score cannot see the rare bad run it stops | +| 67 | freeze | skill | 1,043 | 0 | 234 | every use | safety | reason: safety guard: hard-blocks edits outside one directory; kept on that reason | +| 68 | guard | skill | 908 | 0 | 348 | every use | safety | reason: safety guard: careful and freeze together; kept on that reason | +| 69 | capy-factory | shared | 846 | n/a | n/a | every Capy session | untested | plan: none (grandfathered) | +| 70 | unfreeze | skill | 372 | 0 | 65 | every use | safety | reason: safety guard: the off switch for freeze's edit restriction; kept with the guard it releases | +| 71 | claude-code | skill | n/a | n/a | n/a | every use | untested | plan: none (grandfathered) | + +## Measure next + +Untested parts, largest load per run first. A fair test runs the claude_code preset, frontier models (Opus 5.5 and GPT-6.1 Sol, plus at most one named low-cost primary) and a masked no-gstack arm ([TESTING_INTERNALS.md](TESTING_INTERNALS.md#no-gstack-arm)). Safety rows are not on this list. + +| Order | Part | Load per run (tokens) | Plan | +|---|---|---|---| +| 1 | ship-review-panel | 22,830 | GEVA-2 registry row R56-1 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 2 | design-review | 20,159 | GEVA-2 registry row R56-9 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 3 | ship | 14,587 | GEVA-2 registry row R56-4 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 4 | plan-ceo-review | 14,394 | GEVA-2 registry row R56-5 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 5 | review | 13,990 | R2 (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-02-geva-122-r2-amendment (#129, main eae6efc), told /review vs a no-gstack arm, Opus 5.5, preset on both arms (grandfathered) | +| 6 | devex-review | 13,911 | GEVA-2 registry row R56-11 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 7 | plan-design-review | 13,351 | GEVA-2 registry row R56-7 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 8 | retro | 12,257 | GEVA-2 registry row R56-12 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 9 | plan-devex-review | 11,104 | GEVA-2 registry row R56-6 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 10 | autoplan-phases | 10,709 | folded into GEVA-2 registry row R56-3 (autoplan); proposed, not yet preregistered (grandfathered) | +| 11 | office-hours | 10,424 | none (grandfathered) | +| 12 | autoplan | 10,288 | GEVA-2 registry row R56-3 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 13 | plan-eng-review | 9,837 | GEVA-2 registry row R56-2 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 14 | design-consultation | 9,557 | GEVA-2 registry row R56-10 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 15 | setup-gbrain | 9,099 | none (grandfathered) | +| 16 | codex | 8,991 | none (grandfathered) | +| 17 | sync-gbrain | 8,863 | none (grandfathered) | +| 18 | design-shotgun | 8,470 | none (grandfathered) | +| 19 | plan-tune | 8,377 | none (grandfathered) | +| 20 | spec | 8,305 | none (grandfathered) | +| 21 | browse | 8,125 | none (grandfathered) | +| 22 | design-html | 8,096 | none (grandfathered) | +| 23 | land-and-deploy | 7,652 | GEVA-2 registry row R56-8 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered) (grandfathered) | +| 24 | benchmark | 7,430 | none (grandfathered) | +| 25 | catalog | 7,291 | Run D (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-01-geva-125-untold-discovery-prereg (#125, main 9820b14), untold gstack vs placebo vs no gstack, preset vs stub, Opus 5.5, GPT-6.1 Sol and particle-1; also measures /investigate's description (grandfathered) | +| 26 | canary | 7,034 | none (grandfathered) | +| 27 | qa | 6,951 | none (grandfathered) | +| 28 | scrape | 6,647 | none (grandfathered) | +| 29 | captain | 6,522 | none (grandfathered) | +| 30 | preamble | 5,743 | I1 ceremony cut (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-01-geva-108-ceremony-cut-prereg (#122, main 409ff47) with its G3 amendment (#128), preset, Opus 5.5 and particle-1 (grandfathered) | +| 31 | gstack-upgrade | 5,726 | none (grandfathered) | +| 32 | context-restore | 5,715 | none (grandfathered) | +| 33 | document-generate | 5,466 | none (grandfathered) | +| 34 | ios-qa | 5,368 | none (grandfathered) | +| 35 | skillify | 5,317 | none (grandfathered) | +| 36 | capy-skill | 5,309 | none (grandfathered) | +| 37 | health | 5,287 | none (grandfathered) | +| 38 | setup-deploy | 5,216 | none (grandfathered) | +| 39 | qa-only | 5,205 | none (grandfathered) | +| 40 | eval-plan | 5,021 | none (grandfathered) | +| 41 | gstack | 4,951 | none (grandfathered) | +| 42 | pair-agent | 4,811 | none (grandfathered) | +| 43 | make-pdf | 4,730 | none (grandfathered) | +| 44 | context-save | 4,637 | none (grandfathered) | +| 45 | open-gstack-browser | 4,429 | none (grandfathered) | +| 46 | design-motion | 4,295 | none (grandfathered) | +| 47 | test-audit | 4,201 | none (grandfathered) | +| 48 | document-release | 4,049 | none (grandfathered) | +| 49 | diagram | 3,895 | none (grandfathered) | +| 50 | benchmark-models | 3,822 | none (grandfathered) | +| 51 | ubicloud | 3,593 | none (grandfathered) | +| 52 | modernize | 3,429 | none (grandfathered) | +| 53 | setup-browser-cookies | 3,184 | none (grandfathered) | +| 54 | deslop-shared-libs | 3,100 | none (grandfathered) | +| 55 | landing-report | 2,971 | none (grandfathered) | +| 56 | ship-docs-audit | 2,730 | folded into GEVA-2 registry row R56-4 (ship); proposed, not yet preregistered (grandfathered) | +| 57 | verify | 2,679 | none (grandfathered) | +| 58 | ios-design-review | 2,367 | none (grandfathered) | +| 59 | ios-sync | 2,240 | none (grandfathered) | +| 60 | ios-clean | 2,214 | none (grandfathered) | +| 61 | write-skill | 2,158 | none (grandfathered) | +| 62 | ios-fix | 2,096 | none (grandfathered) | +| 63 | learn | 1,989 | none (grandfathered) | +| 64 | capy-factory | 846 | none (grandfathered) | +| 65 | claude-code | n/a | none (grandfathered) | + +## Shared parts and preamble tiers + +| Tier | Skills | What loads | +|---|---|---| +| 1 | benchmark, benchmark-models, browse, diagram, gstack, make-pdf, open-gstack-browser, scrape, setup-browser-cookies, verify | a short preamble inline in SKILL.md | +| 2 | canary, captain, context-restore, context-save, design-html, design-motion, design-shotgun, document-generate, document-release, eval-plan, health, investigate, ios-clean, ios-design-review, ios-fix, ios-sync, landing-report, learn, modernize, pair-agent, plan-tune, retro, setup-deploy, setup-gbrain, skillify, sync-gbrain, test-audit, ubicloud, write-skill | the shared sections/preamble.md (row preamble), once per session | +| 3 | autoplan, claude-code, codex, design-consultation, devex-review, ios-qa, office-hours, plan-ceo-review, plan-design-review, plan-devex-review, plan-eng-review, spec | the shared sections/preamble.md (row preamble), once per session | +| 4 | design-review, land-and-deploy, qa, qa-only, review, ship | the shared sections/preamble.md (row preamble), once per session | +| none | careful, cso, deslop-shared-libs, freeze, gstack-upgrade, guard, unfreeze | no preamble | + +- **preamble**: shared preamble (sections/preamble.md, identical in every tier 2+ skill) (ship/sections/preamble.md); loads every tier 2+ skill run, inside that skill's load. +- **catalog**: skill catalog (every skill's frontmatter); loads every session. +- **capy-skill**: capy/skill/gstack/SKILL.md (capy/skill/gstack/SKILL.md); loads every Capy session. +- **capy-factory**: capy/skill/gstack/factory.md (capy/skill/gstack/factory.md); loads every Capy session. +- **ship-review-panel**: /ship review panel (review-army and adversarial sections) (ship/sections/review-army.md, ship/sections/adversarial.md); loads every /ship run, by depth. +- **ship-docs-audit**: /ship docs audit (Step 14.5) (ship/sections/documentation.md); loads every /ship run. +- **autoplan-phases**: /autoplan CEO, design, DX and eng phases (autoplan/sections/ceo-phase.md, autoplan/sections/design-phase.md, autoplan/sections/dx-phase.md, autoplan/sections/eng-phase.md); loads every /autoplan run, by scope. + +## Paid dispatches per run + +Each count is quoted from the file and line named; `bin/gstack-inventory --check` fails when a quoted line moves away. + +| Skill | What | Count | Source | +|---|---|---|---| +| ship | Step 9 review, pass 1, light depth, docs or tests only | 1 reader (the core checklist) | docs/review-policy.md:194 | +| ship | Step 9 review, pass 1, light depth, code | 3 readers (core, one batched specialist, native adversarial), plus the outside voice on a diff that publishes numbers | docs/review-policy.md:195 | +| ship | Step 9 review, pass 1, standard depth | the batched specialist, the Red Team at 200 runtime lines, adversarial and outside voice | docs/review-policy.md:196 | +| ship | Step 9 review, pass 1, full depth | the batched specialist on the whole diff and the Red Team always | docs/review-policy.md:197 | +| ship | targeted reader on money, gate, auth or secret code | +1 at every depth | docs/review-policy.md:319 | +| ship | audits (Steps 7, 8 and 14.5) | 3 | lib/ship-budget-calibration.json:3 | +| ship | measured: one /ship child (gstack-evals#31) | 10 review dispatches | lib/ship-budget-calibration.json:43 | +| ship | prior cost of one dispatch on the Capy meter | 7,430,000 tokens | lib/ship-budget-calibration.json:56 | +| autoplan | voices per phase (native and outside) | 2 | autoplan/DRIVER.md.tmpl:25 | +| autoplan | phases per round: CEO and Eng always, Design with UI scope, DX with developer-facing scope | 2 to 4, so 4 to 8 voice dispatches a round | autoplan/SKILL.md.tmpl:99 | +| autoplan | CEO spec review loop | up to 3 launches | autoplan/sections/ceo-phase.md.tmpl:50 | +| autoplan | review rounds per phase (new unattended runs) | up to 2 | lib/autoplan-budget.ts:23 | +| autoplan | reserved per voice on Capy or under a spend cap | $5 | lib/autoplan-budget.ts:25 | + +## Earlier results that do not count yet + +- `stub`: ran on the Agent SDK stub prompt, not the claude_code preset. +- `small-tier`: Haiku, Sonnet or another non-frontier model only. +- `unmasked`: no mask recorded: the no-gstack arm predates the masking rule and was not re-checked. +- `ceiling`: control at the ceiling, so no difference could show. +- `no-true-no-gstack-arm`: the none arm still had gstack installed. +- `no-exposure`: the model never used the part. +- `not-claude-code`: ran outside Claude Code (Messages API, Codex or Capy). +- `variant-only`: variant against variant, no no-gstack arm. + +| Part | Result | Why it does not count | Note | +|---|---|---|---| +| investigate | gstack-evals #109 stacks round 1 TB2 (GEVA-89, GEVA-93), wiki/board/2026-11-01-geva-93-stacks-round-1-tb2-results | stub, no-exposure, unmasked | Opus +3 points [-3, +9] at 1.9x cost; debug report in 0 of 111 trials; particle-1 -5 [-14, +1] | +| investigate | GSTA-12 Lane B quick look 2, wiki/board/2026-10-29-gsta-12-laneb-quick-2-results | stub, unmasked | Opus gstack minus none -7 [-13, -2] at 1.7x cost; did not replicate | +| investigate | GSTA-30 Lane B quick look 3 (#89), wiki/board/2026-10-30-gsta-30-laneb-quick-3-results | stub, small-tier | gstack arms on Sonnet, particle-1, DeepSeek and Haiku; Opus ran the none arm only | +| investigate | HARN-14 replication (harness-evals #181, unmerged) | stub, small-tier | Haiku 5.5: gstack minus none +3.5 [-12.3, +20.0] | +| learn | GSTA-13, GEVA-54 and GEVA-65 Lane A runs (wiki/board/2026-10-28-gsta-13-quick-look-3-results and the GEVA-54 and GEVA-65 posts) | stub, no-true-no-gstack-arm, ceiling | Opus contract tasks 27/30 vs 27/30 | +| learn | GEVA-65 Lane A rerun row 5 (GPT-6.1 Sol in Codex) | not-claude-code, no-true-no-gstack-arm | Sol failed the no-code-change safety task in 5 of 9 skill trials vs 0 of 3 without; small n, a lead to check, not a hurts verdict | +| office-hours | GSTA-13 quick look 3 Lane A, wiki/board/2026-10-28-gsta-13-quick-look-3-results | stub, no-true-no-gstack-arm | contract tasks on Opus: gstack 74% vs none 28%; the tasks grade the skill's own contract, a manipulation check, not lift | +| office-hours | GEVA-65 Lane A rerun, wiki/board/2026-10-31-geva-65-lanea-shakedown-rerun-results | stub, no-true-no-gstack-arm | Opus before 0.67, after 0.76, none 0.36 | +| plan-ceo-review | CEO panel v2 admission (GEVA-142), wiki/board/2026-11-02-geva-142-ceo-panel-v2-admission-results (open PR #131) | ceiling, unmasked | preset, Opus 5.5 only: must-fix recall 1.00 in all three arms; plain invocation ran the procedure in 0 of 16 | +| plan-ceo-review | Planning R1a pilot (GEVA-102), wiki/board/2026-11-01-geva-102-planning-r1a-pilot-stop (#114) | stub, ceiling, unmasked | none-opus 8/8; gstack-opus read the sections file in 0 of 4 | +| review | gstack-evals #108 stacks round 1 review (GEVA-89), wiki/board/2026-11-01-geva-89-stacks-round-1-review-results | stub, no-exposure, unmasked | Opus 5.5 blocking recall 9.5% vs 7.1%, +2.4 points [-6.4, +11.1]; specialists and adversarial pass ran in 0 of 66 sessions | +| review | gstack-evals #120 R1 blind re-judge (GEVA-113), wiki/board/2026-11-01-geva-113-r1-blind-rejudge-results | stub, unmasked | gstack-opus minus none-opus +3.5 points [-9.6, +16.7] | +| review | GSTA-11 quick look 1 review replay, wiki/board/2026-10-28-gsta-11-review-replay-results | not-claude-code, unmasked | /review's prompts through the Messages API; Opus recall 44% vs 39% plain, 12 bugs | +| preamble | GEVA-100 exposure counts, org-gstack-gbrain/research/why-null-stacks-r1.md (GEVA-100) | stub, no-exposure | ceremony sections ran in 0 of 66 Opus review sessions and 6 of 111 Opus TB2 trials; no effect estimate | +| catalog | GSTA-12 Lane B quick look 1, wiki/board/2026-10-29-gsta-12-laneb-quick-results | stub, no-exposure, unmasked | untold skill opened in 0 of 180 gstack-arm trials; Opus gstack minus none +5 [-3, +17] | +| ship-review-panel | gstack-evals #120 R1 blind re-judge (GEVA-113), wiki/board/2026-11-01-geva-113-r1-blind-rejudge-results | stub, not-claude-code, unmasked | arm C minus none +56.9 points [40.5, 72.2]; the none arm ran on the stub, arm C in Capy at another gstack version, on a cohort mined from gstack's own catches | +| ship-review-panel | GSTA-48 review no-loss arm D (#77), wiki/board/2026-10-30-gsta-48-review-noloss-arm-how-to | variant-only, not-claude-code | arm D vs pooled arm C, no no-gstack arm | + +## Key list and aliases + +71 keys: 64 skills (the folders `discoverTemplates()` finds, the root skill as `gstack`) and 7 shared parts. + +- Skills: autoplan, benchmark, benchmark-models, browse, canary, captain, careful, claude-code, codex, context-restore, context-save, cso, design-consultation, design-html, design-motion, design-review, design-shotgun, deslop-shared-libs, devex-review, diagram, document-generate, document-release, eval-plan, freeze, gstack, gstack-upgrade, guard, health, investigate, ios-clean, ios-design-review, ios-fix, ios-qa, ios-sync, land-and-deploy, landing-report, learn, make-pdf, modernize, office-hours, open-gstack-browser, pair-agent, plan-ceo-review, plan-design-review, plan-devex-review, plan-eng-review, plan-tune, qa, qa-only, retro, review, scrape, setup-browser-cookies, setup-deploy, setup-gbrain, ship, skillify, spec, sync-gbrain, test-audit, ubicloud, unfreeze, verify, write-skill. +- Shared: preamble, catalog, capy-skill, capy-factory, ship-review-panel, ship-docs-audit, autoplan-phases. +- Installed names: `gstack-` is key `` (`gstack-ship` is `ship`); `gstack-upgrade` keeps its name; the root skill installs as `gstack`. +- Other names (the ledger's `aliases`): gstack (root skill) → gstack; upgrade → gstack-upgrade; preamble (ceremony) → preamble; always-on skill catalog (descriptions) → catalog; capy/skill/gstack/SKILL.md (Capy context) → capy-skill; capy/skill/gstack/factory.md (Capy context) → capy-factory; ship: review panel (specialist, adversarial, targeted, outside voice, red team) → ship-review-panel; ship: docs audit (Step 14.5) → ship-docs-audit; autoplan: CEO, design, DX and eng phases, outside voice → autoplan-phases. + +## Not in this inventory + +- Out of scope: openclaw/skills/* and browser-skills/* (not gstack skills an agent loads from this checkout), and connect-chrome (a symlink to open-gstack-browser, counted once). +- Evidence-table row "review: Review Army and adversarial pass": a section inside /review, not its own key; R2 measures it as part of the review row's plan. +- Evidence-table row "ship: QA probes (Step 9.2.1)": a step inside /ship with no separate size; folded into the ship row (GEVA-2 registry row R56-4). +- Evidence-table row "gstack as a whole (installed, any skill)": not a single text part; GEVA-2's real-world outcome lane measures it (gstack-evals plan #111), not a measurement plan for any one part. diff --git a/docs/inventory/evidence.json b/docs/inventory/evidence.json new file mode 100644 index 000000000..6600f7b7e --- /dev/null +++ b/docs/inventory/evidence.json @@ -0,0 +1,661 @@ +{ + "schema_version": 1, + "note": "R56 evidence ledger (ETHOS.md: 9. Earn Its Place). One row per inventory key (docs/inventory.md lists them); bin/gstack-inventory --check validates it and test/inventory.test.ts runs that check. Statuses and their required fields: helps, no-effect, hurts need sources, models (frontier per R54), prompt: preset, scope and masked: true, and no-effect and hurts also r48 {verdict, ref}; untested needs plan (a reference, or none only when grandfathered), and a new part also reason, owner and measure_by; safety needs reason. prior lists earlier results that do not count yet, with why. Seeded 2026-11-02 from GEVA-2's table (org-gstack-gbrain/research/gstack-part-evidence.json, gstack dac71371a); every row present at that base is grandfathered.", + "seed_source": "org-gstack-gbrain/research/gstack-part-evidence.json (GEVA-156 for GST-915, 2026-11-02, gstack-evals main 409ff47)", + "parts": { + "autoplan": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-3 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: autoplan: untested (never measured)" + }, + "benchmark": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: benchmark: untested (never measured)" + }, + "benchmark-models": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: benchmark-models: untested (never measured)" + }, + "browse": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: browse: untested (never measured)" + }, + "canary": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: canary: untested (never measured)" + }, + "captain": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: captain: untested (never measured)" + }, + "careful": { + "status": "safety", + "grandfathered": true, + "reason": "safety guard: warns before destructive commands (rm -rf, DROP TABLE, force-push); kept on that reason, an average score cannot see the rare bad run it stops", + "seed": "GEVA-2, 2026-11-02: careful: exempt (safety guard)" + }, + "claude-code": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: claude-code: untested (never measured)" + }, + "codex": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "not in GEVA-2's table (2026-11-02); never measured" + }, + "context-restore": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: context-restore: untested (never measured)" + }, + "context-save": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: context-save: untested (never measured)" + }, + "cso": { + "status": "safety", + "grandfathered": true, + "reason": "security: supported security findings with explicit coverage; kept on its security reason (GEVA-2 marks it exempt; GStack Minder may reclassify it)", + "seed": "GEVA-2, 2026-11-02: cso: exempt (security audit skill, treated as exempt; GStack Minder may reclassify)" + }, + "design-consultation": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-10 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: design-consultation: untested (never measured)" + }, + "design-html": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: design-html: untested (never measured)" + }, + "design-motion": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: design-motion: untested (never measured)" + }, + "design-review": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-9 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: design-review: untested (never measured)" + }, + "design-shotgun": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: design-shotgun: untested (never measured)" + }, + "deslop-shared-libs": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: deslop-shared-libs: untested (never measured)" + }, + "devex-review": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-11 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: devex-review: untested (never measured)" + }, + "diagram": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: diagram: untested (never measured)" + }, + "document-generate": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: document-generate: untested (never measured)" + }, + "document-release": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: document-release: untested (never measured)" + }, + "eval-plan": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: eval-plan: untested (never measured)" + }, + "freeze": { + "status": "safety", + "grandfathered": true, + "reason": "safety guard: hard-blocks edits outside one directory; kept on that reason", + "seed": "GEVA-2, 2026-11-02: freeze: exempt (safety guard)" + }, + "gstack": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: gstack (root skill): untested (never measured)", + "aliases": [ + "gstack (root skill)" + ] + }, + "gstack-upgrade": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: upgrade: untested (never measured)", + "aliases": [ + "upgrade" + ] + }, + "guard": { + "status": "safety", + "grandfathered": true, + "reason": "safety guard: careful and freeze together; kept on that reason", + "seed": "GEVA-2, 2026-11-02: guard: exempt (safety guard)" + }, + "health": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: health: untested (never measured)" + }, + "investigate": { + "status": "no-effect", + "grandfathered": true, + "sources": [ + "GST-814 (HARN-1, 2026-11-02)", + "org-gstack-gbrain handoffs/HARN-14/hard-follow-2026-11/decision-rule.md" + ], + "models": [ + "claude-opus-5-5" + ], + "prompt": "preset", + "scope": "skill step procedure follow rate: 0 of 46 gstack sessions ran any step (0 of 30 on the claude_code preset); body loaded every time; the outcome check voided at ceiling (no-gstack fixed 8 of 8)", + "masked": true, + "r48": { + "verdict": "CUT-CANDIDATE", + "ref": "GST-938 (GSTA-251 plans hooks-or-drop)" + }, + "plan": "R3 (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-01-geva-101-r48-follow-ups-prereg (#112, main f0b81c0) and its amendment (open PR #133), told /investigate vs no gstack on SWE-bench Verified, preset, Opus 5.5 then GPT-6.1 Sol", + "prior": [ + { + "ref": "gstack-evals #109 stacks round 1 TB2 (GEVA-89, GEVA-93), wiki/board/2026-11-01-geva-93-stacks-round-1-tb2-results", + "why": [ + "stub", + "no-exposure", + "unmasked" + ], + "note": "Opus +3 points [-3, +9] at 1.9x cost; debug report in 0 of 111 trials; particle-1 -5 [-14, +1]" + }, + { + "ref": "GSTA-12 Lane B quick look 2, wiki/board/2026-10-29-gsta-12-laneb-quick-2-results", + "why": [ + "stub", + "unmasked" + ], + "note": "Opus gstack minus none -7 [-13, -2] at 1.7x cost; did not replicate" + }, + { + "ref": "GSTA-30 Lane B quick look 3 (#89), wiki/board/2026-10-30-gsta-30-laneb-quick-3-results", + "why": [ + "stub", + "small-tier" + ], + "note": "gstack arms on Sonnet, particle-1, DeepSeek and Haiku; Opus ran the none arm only" + }, + { + "ref": "HARN-14 replication (harness-evals #181, unmerged)", + "why": [ + "stub", + "small-tier" + ], + "note": "Haiku 5.5: gstack minus none +3.5 [-12.3, +20.0]" + } + ], + "seed": "GEVA-2, 2026-11-02: investigate: measurement under way (R3)", + "note": "Only Opus 5.5 (claude-opus-5-5) was measured; GPT-6.1 Sol was not, so this verdict covers one of the two frontier models ETHOS.md 9 names." + }, + "ios-clean": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: ios-clean: untested (never measured)" + }, + "ios-design-review": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: ios-design-review: untested (never measured)" + }, + "ios-fix": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: ios-fix: untested (never measured)" + }, + "ios-qa": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: ios-qa: untested (never measured)" + }, + "ios-sync": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: ios-sync: untested (never measured)" + }, + "land-and-deploy": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-8 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: land-and-deploy: untested (never measured)" + }, + "landing-report": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: landing-report: untested (never measured)" + }, + "learn": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "prior": [ + { + "ref": "GSTA-13, GEVA-54 and GEVA-65 Lane A runs (wiki/board/2026-10-28-gsta-13-quick-look-3-results and the GEVA-54 and GEVA-65 posts)", + "why": [ + "stub", + "no-true-no-gstack-arm", + "ceiling" + ], + "note": "Opus contract tasks 27/30 vs 27/30" + }, + { + "ref": "GEVA-65 Lane A rerun row 5 (GPT-6.1 Sol in Codex)", + "why": [ + "not-claude-code", + "no-true-no-gstack-arm" + ], + "note": "Sol failed the no-code-change safety task in 5 of 9 skill trials vs 0 of 3 without; small n, a lead to check, not a hurts verdict" + } + ], + "seed": "GEVA-2, 2026-11-02: learn: untested (unfair test: SDK stub; no true no-gstack arm; Opus at ceiling; Sol ran in Codex, not Claude Code)" + }, + "make-pdf": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: make-pdf: untested (never measured)" + }, + "modernize": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: modernize: untested (never measured)" + }, + "office-hours": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "prior": [ + { + "ref": "GSTA-13 quick look 3 Lane A, wiki/board/2026-10-28-gsta-13-quick-look-3-results", + "why": [ + "stub", + "no-true-no-gstack-arm" + ], + "note": "contract tasks on Opus: gstack 74% vs none 28%; the tasks grade the skill's own contract, a manipulation check, not lift" + }, + { + "ref": "GEVA-65 Lane A rerun, wiki/board/2026-10-31-geva-65-lanea-shakedown-rerun-results", + "why": [ + "stub", + "no-true-no-gstack-arm" + ], + "note": "Opus before 0.67, after 0.76, none 0.36" + } + ], + "seed": "GEVA-2, 2026-11-02: office-hours: untested (unfair test: SDK stub; no true no-gstack arm; tasks grade the skill's own contract, a manipulation check, not lift)" + }, + "open-gstack-browser": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: open-gstack-browser: untested (never measured)" + }, + "pair-agent": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: pair-agent: untested (never measured)" + }, + "plan-ceo-review": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-5 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "prior": [ + { + "ref": "CEO panel v2 admission (GEVA-142), wiki/board/2026-11-02-geva-142-ceo-panel-v2-admission-results (open PR #131)", + "why": [ + "ceiling", + "unmasked" + ], + "note": "preset, Opus 5.5 only: must-fix recall 1.00 in all three arms; plain invocation ran the procedure in 0 of 16" + }, + { + "ref": "Planning R1a pilot (GEVA-102), wiki/board/2026-11-01-geva-102-planning-r1a-pilot-stop (#114)", + "why": [ + "stub", + "ceiling", + "unmasked" + ], + "note": "none-opus 8/8; gstack-opus read the sections file in 0 of 4" + } + ], + "r48": { + "verdict": "FIX-METHOD", + "ref": "CEO panel v2 admission (GEVA-142, open PR #131): FIX-METHOD on the panel, a harder panel is planned" + }, + "seed": "GEVA-2, 2026-11-02: plan-ceo-review: untested (unfair test: ceiling, control 12 of 12 so no MDD; plain invocation ran the procedure in 0 of 16)" + }, + "plan-design-review": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-7 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: plan-design-review: untested (never measured)" + }, + "plan-devex-review": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-6 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: plan-devex-review: untested (never measured)" + }, + "plan-eng-review": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-2 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: plan-eng-review: untested (never measured)" + }, + "plan-tune": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: plan-tune: untested (never measured)" + }, + "qa": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: qa: untested (never measured)" + }, + "qa-only": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: qa-only: untested (never measured)" + }, + "retro": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-12 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: retro: untested (never measured)" + }, + "review": { + "status": "untested", + "grandfathered": true, + "plan": "R2 (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-02-geva-122-r2-amendment (#129, main eae6efc), told /review vs a no-gstack arm, Opus 5.5, preset on both arms", + "prior": [ + { + "ref": "gstack-evals #108 stacks round 1 review (GEVA-89), wiki/board/2026-11-01-geva-89-stacks-round-1-review-results", + "why": [ + "stub", + "no-exposure", + "unmasked" + ], + "note": "Opus 5.5 blocking recall 9.5% vs 7.1%, +2.4 points [-6.4, +11.1]; specialists and adversarial pass ran in 0 of 66 sessions" + }, + { + "ref": "gstack-evals #120 R1 blind re-judge (GEVA-113), wiki/board/2026-11-01-geva-113-r1-blind-rejudge-results", + "why": [ + "stub", + "unmasked" + ], + "note": "gstack-opus minus none-opus +3.5 points [-9.6, +16.7]" + }, + { + "ref": "GSTA-11 quick look 1 review replay, wiki/board/2026-10-28-gsta-11-review-replay-results", + "why": [ + "not-claude-code", + "unmasked" + ], + "note": "/review's prompts through the Messages API; Opus recall 44% vs 39% plain, 12 bugs" + } + ], + "r48": { + "verdict": "FIX-METHOD", + "ref": "org-gstack-gbrain/research/why-null-stacks-r1.md (GEVA-100); R2 is the fix" + }, + "seed": "GEVA-2, 2026-11-02: review: measurement under way (R2)" + }, + "scrape": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: scrape: untested (never measured)" + }, + "setup-browser-cookies": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: setup-browser-cookies: untested (never measured)" + }, + "setup-deploy": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: setup-deploy: untested (never measured)" + }, + "setup-gbrain": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: setup-gbrain: untested (never measured)" + }, + "ship": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-4 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "seed": "GEVA-2, 2026-11-02: ship: untested (never measured)" + }, + "skillify": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: skillify: untested (never measured)" + }, + "spec": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: spec: untested (never measured)" + }, + "sync-gbrain": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: sync-gbrain: untested (never measured)" + }, + "test-audit": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: test-audit: untested (never measured)" + }, + "ubicloud": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: ubicloud: untested (never measured)" + }, + "unfreeze": { + "status": "safety", + "grandfathered": true, + "reason": "safety guard: the off switch for freeze's edit restriction; kept with the guard it releases", + "seed": "GEVA-2, 2026-11-02: unfreeze: exempt (safety guard (undoes freeze))" + }, + "verify": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "not in GEVA-2's table (2026-11-02); never measured" + }, + "write-skill": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: write-skill: untested (never measured)" + }, + "preamble": { + "status": "untested", + "grandfathered": true, + "plan": "I1 ceremony cut (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-01-geva-108-ceremony-cut-prereg (#122, main 409ff47) with its G3 amendment (#128), preset, Opus 5.5 and particle-1", + "prior": [ + { + "ref": "GEVA-100 exposure counts, org-gstack-gbrain/research/why-null-stacks-r1.md (GEVA-100)", + "why": [ + "stub", + "no-exposure" + ], + "note": "ceremony sections ran in 0 of 66 Opus review sessions and 6 of 111 Opus TB2 trials; no effect estimate" + } + ], + "r48": { + "verdict": "CUT-CANDIDATE", + "ref": "org-gstack-gbrain/research/why-null-stacks-r1.md (GEVA-100); taken by Garry as the I1 ceremony-cut eval" + }, + "seed": "GEVA-2, 2026-11-02: preamble (ceremony): measurement under way (I1, ceremony-cut eval)", + "aliases": [ + "preamble (ceremony)" + ] + }, + "catalog": { + "status": "untested", + "grandfathered": true, + "plan": "Run D (GEVA-2, 2026-11-02): gstack-evals wiki/plans/2026-11-01-geva-125-untold-discovery-prereg (#125, main 9820b14), untold gstack vs placebo vs no gstack, preset vs stub, Opus 5.5, GPT-6.1 Sol and particle-1; also measures /investigate's description", + "prior": [ + { + "ref": "GSTA-12 Lane B quick look 1, wiki/board/2026-10-29-gsta-12-laneb-quick-results", + "why": [ + "stub", + "no-exposure", + "unmasked" + ], + "note": "untold skill opened in 0 of 180 gstack-arm trials; Opus gstack minus none +5 [-3, +17]" + } + ], + "seed": "GEVA-2, 2026-11-02: always-on skill catalog (descriptions): measurement under way (Run D)", + "aliases": [ + "always-on skill catalog (descriptions)" + ] + }, + "capy-skill": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: capy/skill/gstack/SKILL.md (Capy context): untested (never measured)", + "aliases": [ + "capy/skill/gstack/SKILL.md (Capy context)" + ], + "note": "loads only in Capy sessions, which the claude_code preset fair test cannot reach yet; needs a Captain call on how to test it" + }, + "capy-factory": { + "status": "untested", + "grandfathered": true, + "plan": "none", + "seed": "GEVA-2, 2026-11-02: capy/skill/gstack/factory.md (Capy context): untested (never measured)", + "aliases": [ + "capy/skill/gstack/factory.md (Capy context)" + ], + "note": "loads only in Capy sessions, which the claude_code preset fair test cannot reach yet; needs a Captain call on how to test it" + }, + "ship-review-panel": { + "status": "untested", + "grandfathered": true, + "plan": "GEVA-2 registry row R56-1 (org-gstack-gbrain/research/gstack-part-evidence.json registry_rows; proposed, not yet preregistered)", + "prior": [ + { + "ref": "gstack-evals #120 R1 blind re-judge (GEVA-113), wiki/board/2026-11-01-geva-113-r1-blind-rejudge-results", + "why": [ + "stub", + "not-claude-code", + "unmasked" + ], + "note": "arm C minus none +56.9 points [40.5, 72.2]; the none arm ran on the stub, arm C in Capy at another gstack version, on a cohort mined from gstack's own catches" + }, + { + "ref": "GSTA-48 review no-loss arm D (#77), wiki/board/2026-10-30-gsta-48-review-noloss-arm-how-to", + "why": [ + "variant-only", + "not-claude-code" + ], + "note": "arm D vs pooled arm C, no no-gstack arm" + } + ], + "seed": "GEVA-2, 2026-11-02: ship: review panel (specialist, adversarial, targeted, outside voice, red team): untested (unfair test: none arm on the SDK stub; arms from different harnesses and gstack versions; cohort mined from gstack's own catches)", + "aliases": [ + "ship: review panel (specialist, adversarial, targeted, outside voice, red team)" + ] + }, + "ship-docs-audit": { + "status": "untested", + "grandfathered": true, + "plan": "folded into GEVA-2 registry row R56-4 (ship); proposed, not yet preregistered", + "seed": "GEVA-2, 2026-11-02: ship: docs audit (Step 14.5): untested (never measured)", + "aliases": [ + "ship: docs audit (Step 14.5)" + ] + }, + "autoplan-phases": { + "status": "untested", + "grandfathered": true, + "plan": "folded into GEVA-2 registry row R56-3 (autoplan); proposed, not yet preregistered", + "seed": "GEVA-2, 2026-11-02: autoplan: CEO, design, DX and eng phases, outside voice: untested (never measured)", + "aliases": [ + "autoplan: CEO, design, DX and eng phases, outside voice" + ] + } + }, + "unmapped": [ + { + "row": "review: Review Army and adversarial pass", + "why": "a section inside /review, not its own key; R2 measures it as part of the review row's plan" + }, + { + "row": "ship: QA probes (Step 9.2.1)", + "why": "a step inside /ship with no separate size; folded into the ship row (GEVA-2 registry row R56-4)" + }, + { + "row": "gstack as a whole (installed, any skill)", + "why": "not a single text part; GEVA-2's real-world outcome lane measures it (gstack-evals plan #111), not a measurement plan for any one part" + } + ] +} diff --git a/docs/pregate.md b/docs/pregate.md index 66771f548..c8fb847a5 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -86,7 +86,7 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`). | `sealed` | sealed or eval output that must never reach a public destination; read by the pre-push hook and `gstack-pregate sealed` (see [sealed output](#sealed)) | | `price_file` | repo-relative path of the paid gate's call-plan data file (`GSTACK_EVAL_CALLS v1`); `gstack-eval-plan price-gate` prices from it and never executes anything ([docs/eval-calls.md](eval-calls.md)) | | `stall_timeout_s` | seconds a test command may print nothing while using under a tenth of one CPU before its process group is killed and the check is `incomplete` (`PREGATE_STALLED`); default 300, `0` disables; `--stall-timeout` overrides; the `--remote` gate is exempt (see [supervision](#supervision)) | -| `timeout_s` | seconds each runner process may take (the unit-file group, each serial file, each guard without its own `timeout_s`, and the remote gate's deadline) when `--timeout` is not passed; 1 to 3600, default 600. `gstack-ship-preflight` and the `finish` fallback run pregate without `--timeout`, so this is how a repo with long lanes sets theirs | +| `timeout_s` | seconds each runner process may take (the unit-file group, each serial file, each guard without its own `timeout_s`, and the remote gate's deadline) when `--timeout` is not passed; 1 to 3600, default 600. `gstack-ship-preflight` forwards its own `--timeout`; without it, and in the `finish` fallback, this is how a repo with long lanes sets theirs | | `remote_gate` | the repo's remote test gate command; under 8 cores the local test tier becomes a `requires-remote` obligation naming it, and `--remote` runs it (see [remote gate](#remote-gate)) | | `run_profiles` | name → `{ workflow, events, jobs, artifact, max_age_hours?, os?, skippable? }`: the GitHub Actions runs `clear --run` verifies as evidence on the identical tree; a profile named after a lane (`free-tests.yml/macos-named-regressions`) clears that lane (see [run evidence](#run-evidence)). `--from-run` and the main-push reuse job verify runs against them too ([from-run](#from-run), [CI reuse](#ci-reuse)) | | `remote_gate_runs` | `{ profiles, inventory, skipped? }`: the run profiles that together clear `pregate.json/remote_gate`, the command that prints the remote gate's test files, one per line, which their runs must cover, and an optional command that prints which selected files the gate itself never runs (see [run evidence](#run-evidence)) | @@ -173,7 +173,7 @@ The list lives in its own file, not in `pregate.json`, because older pregate rea ## `gstack-pregate` ``` -gstack-pregate [--stage preflight|tests|all] [--only ] [--allow-repo-commands] [--policy-from base|head] +gstack-pregate [--stage preflight|tests|guards|all] [--only ] [--allow-repo-commands] [--policy-from base|head] [--timeout ] [--stall-timeout ] [--remote --spend-cap ] [--run ] [--from-run ] [--no-ci-carry] [--explain] [--json] gstack-pregate verify | clear --remote --run-url | lanes | validate | init ``` @@ -765,10 +765,12 @@ once: such as `"per_file": "bun test --timeout=30000 {files}"`, the same form as the tests stage's `runner`). That run is recorded under `