mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 16:34:00 +01:00
Merge main 1ccab0d8 into gsta258-l4-ready-receipts (rule 19 grant)
test/review-packets-removed.test.ts takes main's version (#3903 moved the review pins to test/fixtures/review-pins/). This branch's two pin entries become pin files with GSTA-202's recipe: a new replays/GST-508+GST-464+GST-967+GSTA-258-L4.json and the files/ship/sections/review-army.md.json repin to 22c37a64. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
commit
04a02bce2b
180 files changed
+3123
-433
No files matched your search
@@ -1001,8 +1001,10 @@ paid test file, its fixtures and its skills' prompt files, including the
|
||||
generated SKILL.md) plus `HARNESS_VERSION`, keyed with model, Claude CLI version
|
||||
and policy version (`scripts/eval-trial-series.ts`). Each trial also records the
|
||||
full consumed-input fingerprint as provenance. `scripts/harness-version.json`
|
||||
pins every shared-harness file (`GLOBAL_TOUCHFILES` plus the session runners, the
|
||||
PTY harness, the e2e gate and the LLM judge) by blob hash. When
|
||||
holds the version and the harness patterns, and `scripts/harness-pins/<path>.json`
|
||||
pins each shared-harness file (`GLOBAL_TOUCHFILES` plus the session runners, the
|
||||
PTY harness, the e2e gate and the LLM judge) by blob hash, one file per pin, so
|
||||
two PRs that record decisions for different harness files never conflict. When
|
||||
`test/harness-version.test.ts` fails, run one of:
|
||||
|
||||
```bash
|
||||
@@ -1010,8 +1012,8 @@ bun run scripts/bump-harness-version.ts --non-behavioral "<why no verdict can ch
|
||||
bun run scripts/bump-harness-version.ts --bump "<what behavior changed>" # new series for every case
|
||||
```
|
||||
|
||||
On a merge conflict in the manifest, take the higher version, then rerun the
|
||||
script.
|
||||
On a merge conflict in the manifest or a pin file, take the higher version, then
|
||||
rerun the script.
|
||||
|
||||
## Ubicloud VMs (`bun run test:ubicloud`)
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ run from inside a workflow (/ship's docs step running `install.sh --for
|
||||
document-release`, #3571) prints `SELF_UPGRADE: skipped (a gstack run in this
|
||||
session holds a workflow lease on this installation: ... (UPGRADE_BUSY))`,
|
||||
leaves the checkout where the run started and stays ok with `behind=`. Each
|
||||
`gstack-skill-start` renews the lease. On Capy the leased pid is the machine's
|
||||
`gstack-skill-start` renews the lease and records its `SESSION_ID` as an open run; `gstack-skill-end` closes that run, and a lease whose runs are all closed is idle and blocks nothing, so a finished /ship no longer holds the upgrade back (GST-1205). Each run counts for 120 minutes from its own start, whatever other sessions renew. A lease with no recorded run (written without `--run`, or by an older gstack) counts as before. On Capy the leased pid is the machine's
|
||||
long-lived `capy-server`, so the deferral ends when the lease turns 120
|
||||
minutes old, not when the process exits. Every session on the machine runs
|
||||
under that same `capy-server`, so `bin/gstack-upgrade` run by hand also
|
||||
@@ -231,7 +231,7 @@ identities, a taker stalled over 2 minutes between its `mkdir` and its owner
|
||||
rename can be broken and then publish into its successor's lock. While a live process
|
||||
holds the lock, or it cannot be taken at all, `touch` exits 3 without writing
|
||||
(`gstack-skill-start` reports that the session holds no lease) and a reclaim
|
||||
keeps the lease. `bin/gstack-lease list` shows `alive|dead|stale`, and
|
||||
keeps the lease. `bin/gstack-lease list` shows `alive|idle|dead|stale`, and
|
||||
`GSTACK_UPGRADE_FORCE=1` overrides.
|
||||
|
||||
## What the installer does
|
||||
|
||||
+6
-4
@@ -80,14 +80,16 @@ It is off unless you turn it on, because turning it on changes the harness's own
|
||||
|---|---|---|
|
||||
| Plugin folder | `${OPENCLAW_STATE_DIR:-~/.openclaw}/extensions/gstack-start` (index.js, openclaw.plugin.json, package.json, note.txt) | `${HERMES_HOME:-~/.hermes}/plugins/gstack-start` (plugin.yaml, __init__.py, note.txt) |
|
||||
| Hook | `before_prompt_build`, returning the note as `prependContext` | `pre_llm_call`, returning the note as `context` |
|
||||
| Settings it changes, through the harness's own CLI | `openclaw config set plugins.entries.gstack-start.hooks.allowConversationAccess true` and `...enabled true` in `openclaw.json`. OpenClaw runs `before_prompt_build` from a plugin it did not bundle only with that grant, which also lets the plugin's hooks see conversation content (the plugin reads only its own note.txt) | `hermes plugins enable gstack-start` adds it to `plugins.enabled` in `config.yaml` (and creates `config.yaml` when there is none) |
|
||||
| Settings it changes, through the harness's own CLI | `openclaw config set plugins.entries.gstack-start.hooks.allowConversationAccess true` and `...enabled true` in `openclaw.json`. OpenClaw runs `before_prompt_build` from a plugin it did not bundle only with that grant, which also lets the plugin's hooks see conversation content (the plugin reads only its own note.txt) | `hermes plugins enable gstack-start` adds it to `plugins.enabled` in `config.yaml` |
|
||||
| Check | `openclaw plugins inspect gstack-start --runtime --json` lists `before_prompt_build` under `typedHooks` | `hermes plugins list` shows `gstack-start` enabled |
|
||||
|
||||
**Turn it on.** `gstack-harness register openclaw --start-hook` (or `hermes`), or `gstack-config set harness_start_hook on` followed by `./setup`, or `GSTACK_HARNESS_START_HOOK=1 ./setup --host openclaw`. The first of these that is set decides: the `--start-hook`/`--no-start-hook` flag, then `GSTACK_HARNESS_START_HOOK` (`1`/`on` or `0`/`off`), then `harness_start_hook`, then the choice saved in the registration's ledger. Whatever decides, the choice is saved, so refresh and upgrades keep it. The hook goes only into the harness profile's own plugin folder; a registration with a custom `--skills-dir`, or OpenClaw run with `OPENCLAW_CONFIG_PATH` pointing at another config, gets no hook (`HARNESS_HOOK_UNSUPPORTED` when you ask for one). A hook you already turned on stays on when a later run happens to have `OPENCLAW_CONFIG_PATH` set.
|
||||
**Turn it on.** `gstack-harness register openclaw --start-hook` (or `hermes`), or `gstack-config set harness_start_hook on` followed by `./setup`, or `GSTACK_HARNESS_START_HOOK=1 ./setup --host openclaw`. The first of these that is set decides: the `--start-hook`/`--no-start-hook` flag, then `GSTACK_HARNESS_START_HOOK` (`1`/`on` or `0`/`off`), then `harness_start_hook`, then the choice saved in the registration's ledger. Whatever decides, the choice is saved, so refresh and upgrades keep it. The hook goes only into the harness profile's own plugin folder; a registration with a custom `--skills-dir`, or OpenClaw run with `OPENCLAW_CONFIG_PATH` pointing at another config, gets no hook (`HARNESS_HOOK_UNSUPPORTED` with `--start-hook`; with `GSTACK_HARNESS_START_HOOK` or `harness_start_hook`, `register` and `./setup --host` say why and exit 4). `OPENCLAW_CONFIG_PATH` naming the profile's own `openclaw.json`, by any path that resolves to it, is fine. A hook you already turned on stays on when a later run happens to have `OPENCLAW_CONFIG_PATH` set.
|
||||
|
||||
**Check it.** The receipt and `gstack-harness status` show `start hook: active` (configured for new sessions; a running OpenClaw gateway or Hermes gateway picks it up when it reloads or restarts), `written` (files in place but not active, with the reason and the command that fixes it), `disabled` (you turned it off in the harness), `unchecked` (a run that left a hook already on alone, or `status --no-check`; `gstack-harness status` checks it) or `off`. When the run that turns the hook on (or `--start-hook`) leaves it inactive, `gstack-harness register` and `./setup --host` exit 4 after registering the skills (`HARNESS_HOOK_INACTIVE`). Later refreshes and upgrades try once more each time, without failing, until the hook is active or you disable the plugin in the harness.
|
||||
**Run the harness's own setup first.** gstack turns the hook on only in a harness that already has its config (`openclaw.json` from `openclaw onboard`, `config.yaml` from `hermes setup`). Without one, the harness CLI would create a config holding nothing but gstack's plugin setting, with no model, provider or terminal settings, so gstack runs no harness command: the plugin's files are written, the hook reports `written` with the setup to run, and `register` and `./setup --host` exit 4. Once the harness is set up, the next `./setup` or `gstack-harness refresh` turns the hook on, changing only the `gstack-start` plugin entry.
|
||||
|
||||
**Turn it off.** `gstack-harness register openclaw --no-start-hook` (or `hermes`), or `gstack-config set harness_start_hook off` followed by `./setup`. Either turns the plugin off through the harness's CLI and removes its folder. You can also switch it off in the harness (`openclaw config set plugins.entries.gstack-start.enabled false`, `hermes plugins disable gstack-start`); gstack then leaves it off and never turns it back on unless you pass `--start-hook` again. `gstack-harness unregister` and `gstack-uninstall` remove it with the rest (Hermes only disables it when its folder holds an edited file or a kept `.edited-*` copy, so your edit survives), and print the harness command to run when the harness CLI is not on `PATH`.
|
||||
**Check it.** The receipt and `gstack-harness status` show `start hook: active` (configured for new sessions; a running OpenClaw gateway or Hermes gateway picks it up when it reloads or restarts), `written` (files in place but not active, with the reason and the command that fixes it), `disabled` (you turned it off in the harness), `unknown` (the harness CLI is not on `PATH`, as inside OpenClaw's or Hermes's own terminal tool, and the config file cannot settle it; gstack reads `plugins.enabled` and `plugins.disabled` from Hermes's `config.yaml` and the plugin entry from `openclaw.json` instead), `unchecked` (a run that left a hook already on alone, or `status --no-check`; `gstack-harness status` checks it) or `off`. When the run that turns the hook on (or `--start-hook`) leaves it inactive, `gstack-harness register` and `./setup --host` exit 4 after registering the skills (`HARNESS_HOOK_INACTIVE`). Later refreshes and upgrades try once more each time, without failing, until the hook is active or you disable the plugin in the harness.
|
||||
|
||||
**Turn it off.** `gstack-harness register openclaw --no-start-hook` (or `hermes`), or `gstack-config set harness_start_hook off` followed by `./setup`. Either turns the plugin off through the harness's CLI and removes its folder. You can also switch it off in the harness (`openclaw config set plugins.entries.gstack-start.enabled false`, `hermes plugins disable gstack-start`); gstack then leaves it off and never turns it back on unless you pass `--start-hook` again. `gstack-harness unregister` and `gstack-uninstall` remove it with the rest (Hermes only disables it when its folder holds an edited file or a kept `.edited-*` copy, so your edit survives), and print the harness command to run when the harness CLI is not on `PATH`. In a profile with no harness config yet (no `openclaw.json` or `config.yaml`), the hook was never turned on, so turning it off or unregistering runs no harness command and leaves the config alone.
|
||||
|
||||
**What it costs.** The note is about 50 tokens and lands on every user turn, and each copy stays in the session's history, so a long session pays more (mostly cache reads). HARN-18's panel measures the added cost per trial. OpenClaw also logs that it cannot verify where the plugin came from, because gstack writes it instead of installing it from npm or ClawHub.
|
||||
|
||||
|
||||
+185
-13
@@ -88,9 +88,9 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`).
|
||||
| `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 |
|
||||
| `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? }`: 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)) |
|
||||
| `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)) |
|
||||
| `ship_lanes_from` | maps each exact /ship lane command to `{ profiles, inventory }`: the run profiles whose verified runs at HEAD's clean tree carry that lane instead of running it, and the command listing its test files, which their runs must cover (see [CI carry](#ci-carry)); read from the base branch only |
|
||||
| `ship_lanes_from` | maps each exact /ship lane command to `{ profiles, inventory }` or `{ profiles, jobs }`, with optional `aliases` (and, on an inventory entry, an optional `per_file` runner for files CI skipped entirely, see [skipped files](#skipped-files)): the run profiles whose verified runs at HEAD's clean tree carry that lane instead of running it, and either the command listing its test files, which their runs must cover, or the CI jobs that must have succeeded (see [CI carry](#ci-carry)); read from the base branch only |
|
||||
| `allowlist` | `[{ finding, paths?, pattern?, reason }]`: benign findings the `secrets` check accepts and names as allowlisted, by gstack-redact finding id or non-secret category, path globs and a pattern anchored at the finding; never `env.value` (see [allowlist](#allowlist)) |
|
||||
|
||||
Only the `origin/<base>` copy or the user-owned file below is honoured, so a
|
||||
@@ -201,9 +201,12 @@ Two stages with separate clocks, both printed:
|
||||
file is read whole from disk. A file over the size limit, or whose full diff
|
||||
does not parse, is scanned on its added lines alone, without that context.
|
||||
- `strays`: a new file at the repository root, a new top-level directory, or a
|
||||
scratch name (`zz-*`, `tmp*`, `scratch*`) anywhere.
|
||||
scratch name (`zz-*`, `tmp*`, `scratch*`) or editor leftover (`*.orig`,
|
||||
`*.rej`, `*.swp`, `*.swo`, `*.bak`, `*.tmp`, `*~`, `.DS_Store`, `Thumbs.db`)
|
||||
anywhere.
|
||||
- `literals`: for every changed exported constant or named timeout/retry
|
||||
literal, the whole test tree (nightly- and Heavy-only files included) is
|
||||
literal (a named literal the branch only adds, such as `limit: 300` in a new
|
||||
object, is not a change), the whole test tree (nightly- and Heavy-only files included) is
|
||||
searched for the old value in files that name the constant or, when the old
|
||||
value is longer than one character, its module (the module's file name not
|
||||
followed by another extension, or a path ending
|
||||
@@ -259,7 +262,8 @@ Two stages with separate clocks, both printed:
|
||||
`--allow-repo-commands` the selected guards are listed and the check is
|
||||
`incomplete`. Guards are read from the base branch's copy (or the
|
||||
user-owned file when the base carries none) and cannot be
|
||||
downgraded to warn. See [guards](#guards).
|
||||
downgraded to warn. A guard whose exact command already passed at this tree
|
||||
is not run again ([guard carry](#guard-carry)). See [guards](#guards).
|
||||
|
||||
**Stage 2, test execution:**
|
||||
|
||||
@@ -710,7 +714,22 @@ other gate change. Each profile names the workflow file, the events it
|
||||
accepts, every job that must have passed (each exactly once), and the artifact
|
||||
holding the run's `tree.json`. `max_age_hours` (1–168, default 24) bounds how
|
||||
old the run may be, and `os` (`Linux`, `macOS`, `Windows`) requires
|
||||
`tree.json` to record that runner OS. Each workflow uploads `tree.json` from
|
||||
`tree.json` to record that runner OS. <a id="skippable"></a>`skippable`
|
||||
(`{ jobs, gate, scripts }`, GST-1190) lets named jobs be skipped by a path
|
||||
filter: a skipped job counts only in a `pull_request` run (whose gate diffs
|
||||
against the pull request's base; a push or dispatch gate diffs against the last
|
||||
push, which the branch's push history controls), when the `gate` job (the one
|
||||
whose output decides the skip) succeeded in the same attempt, and the workflow
|
||||
file plus the gate's inputs are byte-identical to `origin/<base>`. Those inputs
|
||||
are the declared `scripts`, which must list every repo file the gate reads,
|
||||
plus every path the gate job's steps name in the base copy of the workflow
|
||||
(also read under the job's `working-directory` and after a `${{ … }}`
|
||||
prefix), plus `package.json` when the gate runs a package script. gstack
|
||||
cannot see what a gate script imports or what a package script runs, so list
|
||||
those files in `scripts`. Otherwise the pull request controlled the skip
|
||||
decision, so the job refuses as `job_skipped` and the lane runs here. A lane
|
||||
whose every named job was skipped this way prints `LANE: <label> not owed: CI
|
||||
skipped at this tree (gate job <name>, workflow unchanged) run=…`. Each workflow uploads `tree.json` from
|
||||
its own job: `run_id`, `run_attempt`, `event`, `sha` (or `head_sha`),
|
||||
`tree` (`git rev-parse HEAD^{tree}` as tested), `files` (every test file the
|
||||
run executed), `tests` (the count) and, with `os`, the runner OS or a list of
|
||||
@@ -737,6 +756,28 @@ once:
|
||||
record (a reuse is never evidence). Its tree equals the tree git computes for
|
||||
the run's head commit. `files` is a list of paths and `tests` a
|
||||
non-negative integer when present; without `tests` the file count stands in.
|
||||
<a id="skipped-files"></a>`skipped_files`, when present, lists the files whose every test skipped on
|
||||
the runner (a Docker image, a drive path or an installed gstack it lacks;
|
||||
GST-1190). A carry treats them as uncovered: the pregate tests stage runs
|
||||
them here. A /ship lane or guard whose inventory names one carries the
|
||||
covered files and runs only the skipped ones here, through the runner its
|
||||
`ship_lanes_from` entry declares as `per_file` (a command with `{files}`,
|
||||
such as `"per_file": "bun test --timeout=30000 {files}"`, the same form as the
|
||||
tests stage's `runner`). That run is recorded under `<label>.skipped`, the
|
||||
`LANE:` line adds `skipped_here=<n> file(s) tests_ran_here=<n>`, and the CI row
|
||||
is written only after it passes with a test count above zero and every carried
|
||||
run still stands (`run_changed` otherwise). A failure there is
|
||||
`skipped_failed` and a run of zero tests is `skipped_zero_run`; either way the
|
||||
whole lane runs here. An entry with no `per_file` refuses as
|
||||
`inventory_skipped`, naming the files, and the lane runs here whole. A partly
|
||||
skipped file ran tests and stays covered. gstack's own Free Tests writer does
|
||||
not emit the list yet: it starts in the same follow-up pull request that
|
||||
declares `per_file` for gstack's `bun run test` lane, so gstack's carry does not
|
||||
change until both switch on together. A `tree.json`
|
||||
without the key (older writers, other repos) is read as before, with every
|
||||
listed file counted as run; add the list to your writer to close that gap.
|
||||
The remote gate's clearance does not use it: that gate runs on a Linux runner
|
||||
like CI, where the same files skip.
|
||||
- No run of the same workflow on that head, or on pregate.json's own head,
|
||||
whose newest attempt started at or after the second this run was created
|
||||
failed, was cancelled or is still running (`run_started_at`, so a red re-run
|
||||
@@ -785,7 +826,7 @@ Then, for the lane:
|
||||
Any API or git error refuses. Each refusal prints `REUSE: refused
|
||||
reason=<code> — …` as `PREGATE_RECEIPT_INVALID`, and a run that fails
|
||||
verification is named with its own reason (`fork_run`, `event_mismatch`,
|
||||
`run_not_success`, `job_missing`, `artifact_mismatch`, `artifact_reused`,
|
||||
`run_not_success`, `job_missing`, `job_skipped`, `artifact_mismatch`, `artifact_reused`,
|
||||
`tree_mismatch`, `newer_run_vetoes`, `run_changed`, `run_stale`,
|
||||
`os_mismatch`, `api_error`, …); the clear-level codes are `base_mismatch`,
|
||||
`no_verified_run`, `tree_mismatch` (runs on different trees),
|
||||
@@ -895,7 +936,8 @@ whose workflow holds a lane this machine runs (gstack's `ci-reuse/free-tests`
|
||||
on Linux), with pregate.json's own head as a further veto head. Its tree must be this tree exactly (no
|
||||
stamp-only substitution). Then:
|
||||
|
||||
- The selected local test files the run's `tree.json` lists count as passed.
|
||||
- The selected local test files the run's `tree.json` lists count as passed,
|
||||
except those in its `skipped_files` ([above](#skipped-files)), which run here.
|
||||
The lanes check records them under `from_run` with the run receipt
|
||||
(`run tree=… tests=… runs=<id>/<attempt> profiles=… heads=… verified=…`) and
|
||||
prints `REUSE: accepted <n> of <m> selected local test file(s) from <receipt>;
|
||||
@@ -933,11 +975,14 @@ it accepts a run only when all of these hold:
|
||||
tree. A `pull_request` run counts only when the pull request already
|
||||
contained its base.
|
||||
- Every run executed tests, and with an inventory command, the runs'
|
||||
`tree.json` files cover every file it prints. Each run is read once more
|
||||
afterward (`run_changed`).
|
||||
`tree.json` files cover every file it prints, none of them a file whose every
|
||||
test skipped there (`skipped_files`). A lane mapped by `jobs`
|
||||
instead needs no test count: each named job succeeded in the run's newest
|
||||
attempt. Each run is read once more afterward (`run_changed`).
|
||||
|
||||
Security and advisory scans, the sealed guard, guards and every preflight-stage
|
||||
check always run; only test lanes carry.
|
||||
Security and advisory scans, the sealed guard and every other preflight-stage
|
||||
check always run; only test lanes carry, plus guards whose exact command
|
||||
already passed at this tree ([guard carry](#guard-carry)).
|
||||
|
||||
**The tests stage.** Without `--from-run`, when the base branch declares
|
||||
`run_profiles`, `gstack-pregate` looks up the newest verified run at HEAD under
|
||||
@@ -986,8 +1031,101 @@ it and the command that lists its test files:
|
||||
}
|
||||
```
|
||||
|
||||
<a id="ship-lanes-jobs"></a>A lane with no test files to list, such as a typecheck or a verify script,
|
||||
names the CI jobs that run it instead (GST-1189). Declare exactly one of
|
||||
`inventory` or `jobs`:
|
||||
|
||||
```json
|
||||
"ship_lanes_from": {
|
||||
"bun run verify": { "profiles": ["ci/test"], "jobs": ["verify", "typecheck"] }
|
||||
}
|
||||
```
|
||||
|
||||
It carries only when every named job concluded `success` in the newest attempt
|
||||
of the verified run at HEAD's tree, read by the same `verifyRun` job check the
|
||||
profile's own `jobs` use. A job that is missing refuses as `job_missing`, a
|
||||
skipped one as `job_skipped`, any other conclusion as `job_not_success`, and a
|
||||
run of another tree as `tree_mismatch`. The receipt reads `ci-carry tree=…
|
||||
jobs=verify,typecheck runs=…`, and the ledger row records the jobs. gbrain run
|
||||
41564913688 (tree `13570abc`) is the case: its verify and typecheck jobs passed
|
||||
at the tree, but no inventory could name their files, so they ran again. The
|
||||
same form replaces fake `check:<command>` entries some repos wrote into
|
||||
`tree.json` to give a typecheck, a doc-freshness check or a lint an inventory
|
||||
(gstack-evals #180): name the job instead. A gstack older than the version that
|
||||
adds `jobs` rejects the key as invalid config, so a repo declaring it should
|
||||
first raise its `min_gstack_version` to that version.
|
||||
|
||||
<a id="ship-lanes-aliases"></a>**Aliases.** A lane is keyed on its exact command, so
|
||||
`env -u ANTHROPIC_API_KEY bun test tests/` never matches a `bun test tests/` key.
|
||||
Declare the other spellings on the entry, read from the base like the rest of
|
||||
the map (GST-1190):
|
||||
|
||||
```json
|
||||
"bun test tests/": { "profiles": ["ci/test"], "inventory": "…", "aliases": ["env -u ANTHROPIC_API_KEY bun test tests/"] }
|
||||
```
|
||||
|
||||
An alias is a second exact command, never a pattern: gstack does not strip env
|
||||
prefixes on its own, because `env -i HOME=… PATH=… bun test tests/` runs in a
|
||||
different environment than the key. An alias that repeats a key or another
|
||||
entry's alias is invalid config. The ledger row binds the command the lane
|
||||
actually ran.
|
||||
|
||||
**What `tests_ran` means on a carried lane.** No carried lane reports the whole
|
||||
run's count. A lane mapped by `inventory` reports how many of its inventory
|
||||
files the runs' `tree.json` covers, labeled as a file count (`tests_ran=14
|
||||
unit=files` on the ledger line, `tests_ran=14 (files)` on the `LANE:` line);
|
||||
alphaclaw's 14-file, 46-test guards lane used to record the run's 9677.
|
||||
`tree.json` has no per-file test counts yet, so the lane's own test count is
|
||||
not available. A lane mapped by `jobs` reports `tests_ran=n/a`. Only the
|
||||
pregate tests stage and `gstack-ci-carry find` without `--command`, which carry
|
||||
the whole run, report its test count.
|
||||
|
||||
**`tree.json` counts from Bun's JUnit.** A writer that builds `tree.json` from
|
||||
Bun's JUnit report must take the root `testsuite` count. Nested `describe`
|
||||
suites repeat the file attribute, so summing every `testsuite` counts tests
|
||||
twice. harness-evals #240's `scripts/ci-tree-json.ts` does it right.
|
||||
|
||||
<a id="prove-a-mapping"></a>**Proving a new mapping.** `ship_lanes_from` is read from `origin/<base>`
|
||||
only, so a branch that adds a mapping cannot carry from it, and running
|
||||
against `--base <PR branch>` fails (`BASE_CONTAINS_HEAD`). Prove it with the dry
|
||||
run instead, once per lane command:
|
||||
|
||||
```bash
|
||||
gstack-ci-carry find --base main --policy-from head --simulate --command 'bun run verify' --allow-repo-commands
|
||||
```
|
||||
|
||||
It reads the branch's own `ship_lanes_from` and `run_profiles`, checks the lane
|
||||
against the verified CI run at this tree exactly as a carry would, and prints
|
||||
`SIMULATION:` at the start of every line (`SIMULATION: CI_CARRY: accepted …` or
|
||||
`SIMULATION: CI_CARRY: none (<reason>)`). It writes no evidence or ledger row
|
||||
and clears no obligation. To check every /ship lane at once, the same dry run
|
||||
takes lane flags: `gstack-ship-preflight --tests-only --base main --policy-from
|
||||
head --simulate --allow-repo-commands --lane 'verify=bun run verify' …` prints `SIMULATION: LANE:
|
||||
<label> would carry run=…`, `SIMULATION: CI_CARRY: none label=<label> (<reason>)`
|
||||
or `not mapped` per lane, runs no lane and no pregate, and exits 0 when every
|
||||
mapped lane would carry. Both dry runs run the branch's inventory commands to
|
||||
check coverage, which is running the branch's code, so both need
|
||||
`--allow-repo-commands`; without it `gstack-ci-carry find --command` stops
|
||||
with a usage error (exit 2), and the preflight dry run prints
|
||||
`inventory_not_allowed` for an inventory-mapped lane. truther's adoption proved its map this way by hand
|
||||
before the dry run existed: run 41585332512 accepted 7 of 7 lanes. A branch's own map never carries,
|
||||
even when the base has no policy at all, because the pull request controls both
|
||||
the map and the CI that would satisfy it. So the first pull request that adopts
|
||||
a mapping pays its full lanes; the carry starts on the pull requests after it
|
||||
merges. `ship_lanes_from` itself needs `min_gstack_version` 1.94.0.0 or later in
|
||||
`.gstack/ship-policy.json`, and `jobs` and `aliases` need the release that ships
|
||||
them. With that floor set, a gstack that has this check but not the key prints
|
||||
`needs gstack >= <version>; upgrade` (`PREGATE_GSTACK_TOO_OLD`) instead of a
|
||||
generic invalid-config error. A gstack released before the check (1.94.0.0
|
||||
included) still reports the unknown key as `PREGATE_CONFIG_INVALID`.
|
||||
|
||||
As a second guard, every `record-ci` row names the policy that mapped it
|
||||
(`origin/<base>@<sha>:.gstack/pregate.json`), and `gstack-evidence carried`, which
|
||||
the ship receipt reads, lists only rows whose policy is `origin/<the receipt's
|
||||
base>`. A row written by a run against another base never lands in a receipt.
|
||||
|
||||
A carried lane prints `LANE: <label> carried run=<id>/<attempt> tests_ran=<n>`
|
||||
and writes an explicit CI-sourced ledger row (`gstack-evidence record-ci
|
||||
(see above for what `<n>` counts) and writes an explicit CI-sourced ledger row (`gstack-evidence record-ci
|
||||
--label <label> --base <base> -- <command>`, which finds and verifies the run
|
||||
itself, so no caller hands it run ids or a test count: `source: "ci"`, its runs
|
||||
and profiles, the run's finish time as `ts`, bound to the clean tree), never a
|
||||
@@ -1021,6 +1159,8 @@ them (`ci_carried=<lane>:<run>/<attempt>`).
|
||||
--allow-repo-commands]` prints the verdict for HEAD; `wait [--timeout <s>]`
|
||||
polls the newest run at HEAD until it completes (run it in the background) and
|
||||
prints `CI_WAIT: green|red|running|none`, then `find`'s verdict on green.
|
||||
`find --policy-from head --simulate` is the dry run in
|
||||
[proving a new mapping](#prove-a-mapping).
|
||||
|
||||
<a id="ci-reuse"></a>
|
||||
### Main pushes reuse the merged pull request's run
|
||||
@@ -1165,6 +1305,38 @@ guard is a *command* with its own fix line. Older gstack refuses unknown keys
|
||||
that reads them ships. The worked gbrain configuration is in
|
||||
[gbrain worked configs](#gbrain-worked-configs).
|
||||
|
||||
<a id="guard-carry"></a>
|
||||
### A guard that already passed at this tree does not run again
|
||||
|
||||
A guard is skipped as passed when its exact command already passed at this tree
|
||||
(GST-1186). Two sources count, checked in this order:
|
||||
|
||||
1. **CI.** When the base branch's `ship_lanes_from` maps the guard's exact
|
||||
command, the guard is carried from the verified CI run at HEAD's clean tree,
|
||||
through the same `gstack-evidence record-ci` and `findCarry` path a /ship
|
||||
lane uses ([ci-carry](#ci-carry)). It prints `GUARD: <id> carried
|
||||
runs=<id>/<attempt>`, or `GUARD: <id> not owed: CI skipped at this tree …`
|
||||
when every job it maps was skipped by an unchanged gate
|
||||
([skippable](#skippable)). When no run carries, it prints `CI_CARRY: none
|
||||
guard=<id> (<reason>)` and goes on to the ledger.
|
||||
2. **The evidence ledger.** When the newest local ledger row for the exact
|
||||
command at this working tree, under any label, is a counted pass (`tests_ran`
|
||||
above zero under the runtime the guard would launch with, within 24 hours),
|
||||
the guard prints `GUARD: <id> reused label=<L> source=local ts=<ts>`. CI rows
|
||||
never count here: a CI pass counts only through `record-ci`'s live check
|
||||
against the base, so an old CI row does not outlive a refusal.
|
||||
`gstack-evidence passed -- <command>` is the same lookup by hand. A newer
|
||||
failure of the command at that tree vetoes an older pass.
|
||||
|
||||
Anything else runs the guard as before, with the same timeout, stall rule and
|
||||
fix line. This matters for repos whose suites are guards rather than lanes
|
||||
(founderstar declares `check-quick`, `play-through` and `assets` as guards),
|
||||
which otherwise reran every suite /ship had just carried from CI.
|
||||
|
||||
Known limit: `ship_lanes_from` is read from `origin/<base>` only, so the pull
|
||||
request that adds a mapping cannot carry from it. Prove it with the dry run in
|
||||
[proving a new mapping](#prove-a-mapping) until it lands.
|
||||
|
||||
<a id="allowlist"></a>
|
||||
## Allowlist: benign findings the secrets check accepts
|
||||
|
||||
|
||||
+5
-1
@@ -53,6 +53,7 @@ refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
|
||||
| `maintainers` | GitHub logins | `[]` | Maintainer set for `/review` contributor mode; empty means repository push permission. |
|
||||
| `stamp_files` | globs | `[]` | The repo's stamp rule: when set, only these files may differ by a stamp (list the version file, manifests, plugin trees and the files holding the PR's own `since:` rows). It applies before every other stamp rule, so a doc `vX` mention outside it is a difference. |
|
||||
| `stamp_patterns` | literal templates | `[]` | At most 16 templates of at most 128 characters, each with exactly one `{version}` slot (`"version": "{version}"`, `since: '{version}'`, `<!-- x-stamp: {version} -->`); matched literally at a version boundary, never as a regex. The restamp writer, its mention scanner and the tree receipt use them beside the built-in forms. |
|
||||
| `stamp_placeholders` | literal placeholders | `[]` | At most 16 one-line literals of at most 32 characters, never `{version}` and never a version (`["TBD"]`). On a line the branch added itself, a placeholder inside a `stamp_patterns` template (`since: 'TBD'` under `since: '{version}'`) may become that template at the new version, and finish's route, the receipt carry and the tree receipt read the change as a stamp. The built-in forms never take a placeholder, and neither does a base line, even one whose text duplicates a branch line. The writer does not fill placeholders: the repo's `restamp_command` does. A newer key: set `min_gstack_version` to the release `gstack-ship-policy validate` names. Empty accepts no placeholder. |
|
||||
| `restamp_command` | command or `null` | `null` | The repo's own restamp, run first in the staging worktree; see [restamp_command](#restamp-command). |
|
||||
| `migrations` | `{ dir, id }` or `null` | `null` | The migration directory and a regex with exactly one capture group for the number in a file name (`"^v(\\d+)-"`); `gstack-pregate regate` reports `MIGRATION_COLLISION`. |
|
||||
| `gate_command` | command or `null` | `null` | The repo's full pre-merge gate. `gstack-ship-queue gate-ahead` and `ready` print it for the caller to run (in the background or in CI); gstack never executes it. Needs `min_gstack_version`. |
|
||||
@@ -1535,7 +1536,10 @@ the reviewed range (the reviewed head excluded, at most 100 commits) whose
|
||||
product change the current base already holds: `git merge-tree` of that commit
|
||||
into the base changes no product path. A conflicted product path never counts,
|
||||
so a parent that never landed is never treated as one, even when the child's
|
||||
merge dropped its change. The comparison then runs from that commit: an equal
|
||||
merge dropped its change. The comparison then runs from that commit, leaving out
|
||||
every path whose content at the reviewed head is the base's (a base merge the
|
||||
child made before its receipt brought it in) unless HEAD changed it against the
|
||||
base (GST-1214): an equal
|
||||
patch-id carries with reason `stack parent <sha12> landed`, base context next
|
||||
to the child's unchanged hunks is `RECEIPT_CARRY_SCOPED_REVIEW`, and any other
|
||||
difference refuses with `RECEIPT_CARRY_PRODUCT_CHANGED`.
|
||||
|
||||
@@ -50,14 +50,14 @@ per-failure-class breakdown from `test:health`.
|
||||
full consumed-input fingerprint (all touchfiles, globals included) as
|
||||
`series_fingerprint`, plus `harness_version`, as provenance.
|
||||
(`caseSeriesIdentitiesV2` in `scripts/eval-trial-series.ts`.)
|
||||
- **Harness guard.** `scripts/harness-version.json` pins the blob of every
|
||||
shared execution-harness file: `GLOBAL_TOUCHFILES` plus the session runners,
|
||||
the PTY harness, the e2e gate and the LLM judge (36 files). A free test
|
||||
- **Harness guard.** `scripts/harness-pins/<path>.json` pins the blob of every
|
||||
shared execution-harness file (`scripts/harness-version.json` keeps the version and patterns): `GLOBAL_TOUCHFILES` plus the session runners,
|
||||
the PTY harness, the e2e gate and the LLM judge (42 files). A free test
|
||||
(`test/harness-version.test.ts`) fails when one changes without a decision.
|
||||
`bun run scripts/bump-harness-version.ts --bump "<reason>"` raises
|
||||
`HARNESS_VERSION` and starts a new series for every case;
|
||||
`--non-behavioral "<reason>"` records the new blobs as compatible with no
|
||||
reset. Entries are one line per file so concurrent PRs that touch different
|
||||
reset. Each pin is its own file so concurrent PRs that touch different
|
||||
files merge cleanly; on a conflict, take the higher version, then rerun.
|
||||
- **Weekly history (W4b).** Weeks for quarantine expiry and drift are
|
||||
`evals-periodic.yml` runs on `main`: scheduled runs and main dispatches.
|
||||
|
||||
+28
-5
@@ -1705,10 +1705,12 @@ session holds a workflow lease on this installation: pid <p> (<age>m
|
||||
<checkout>/bin/gstack-upgrade once the lease is 120 minutes old, or
|
||||
GSTACK_UPGRADE_FORCE=1 <checkout>/bin/gstack-upgrade when no other session uses
|
||||
this checkout (UPGRADE_BUSY))` and leaves the
|
||||
checkout where the run started. Each `gstack-skill-start` renews the lease.
|
||||
checkout where the run started. Each `gstack-skill-start` renews the lease
|
||||
and opens a run that its `gstack-skill-end` closes; a lease whose runs are all
|
||||
closed is idle and never counts (GST-1205).
|
||||
|
||||
**Fix.** Wait for those sessions to finish (`bin/gstack-lease list` shows
|
||||
every lease with its age and `alive|dead|stale`; a holder is named as
|
||||
every lease with its age and `alive|idle|dead|stale`; a holder is named as
|
||||
`pid <p> (<age>m <command>)`), then re-run `bin/gstack-upgrade`. For the
|
||||
session's own run, or a shared-harness lease, re-run once the lease is 120
|
||||
minutes old. When you know no other session uses the checkout, `GSTACK_UPGRADE_FORCE=1 bin/gstack-upgrade`
|
||||
@@ -3733,6 +3735,23 @@ the entry is reported as unverified, not `REGEN_STALE`.
|
||||
**Fix.** Run the printed install command (`bun install`, or the one matching the
|
||||
repository's lockfile), then rerun `gstack-regen check` or the pre-gate.
|
||||
|
||||
<a id="pregate-gstack-too-old"></a>
|
||||
### `PREGATE_GSTACK_TOO_OLD`
|
||||
|
||||
**Meaning.** `.gstack/pregate.json` uses a key this gstack does not know, and
|
||||
the same tree's `.gstack/ship-policy.json` sets `min_gstack_version` above this
|
||||
gstack. The key belongs to a newer gstack (one a later release adds), so the
|
||||
line reads `needs gstack >= <version>; upgrade`. The policy still refuses,
|
||||
exactly as `PREGATE_CONFIG_INVALID` would. Only a gstack that has this check
|
||||
prints it; one released before it (1.94.0.0 included) reports the same key as
|
||||
`PREGATE_CONFIG_INVALID`.
|
||||
|
||||
**Fix.** Upgrade gstack on this machine to at least the version named
|
||||
(`bin/gstack-upgrade`, or the session install with `--for <skill>`), then
|
||||
rerun. A repo adopting a new key raises `min_gstack_version` first, so an older
|
||||
gstack that has this check prints this instead of a generic invalid-config
|
||||
error.
|
||||
|
||||
<a id="pregate-config-invalid"></a>
|
||||
### `PREGATE_CONFIG_INVALID`
|
||||
|
||||
@@ -3770,7 +3789,9 @@ anchored pattern and a reason ([docs/pregate.md](pregate.md#allowlist));
|
||||
### `PREGATE_STRAYS`
|
||||
|
||||
**Meaning.** A file was added at the repository root, in a new top-level
|
||||
directory, or under a scratch name (`zz-*`, `tmp*`, `scratch*`).
|
||||
directory, or under a scratch name (`zz-*`, `tmp*`, `scratch*`) or an editor
|
||||
leftover name (`*.orig`, `*.rej`, `*.swp`, `*.swo`, `*.bak`, `*.tmp`, `*~`,
|
||||
`.DS_Store`, `Thumbs.db`).
|
||||
|
||||
**Fix.** Delete or move it. An intentional root file goes in `strays.allow`.
|
||||
|
||||
@@ -6475,7 +6496,7 @@ There is no ledger for that harness or id under `$GSTACK_STATE_ROOT/harness`. `g
|
||||
<a id="harness-hook-unsupported"></a>
|
||||
### `gstack-harness: <harness> has no start hook` / `the start hook goes into the <harness> profile <dir>, and this registration uses another skills folder` (HARNESS_HOOK_UNSUPPORTED)
|
||||
|
||||
The [start hook](hosts.md#start-hook) exists for OpenClaw and Hermes only, and it goes into the harness profile's own plugin folder, the one the harness scans. A registration with `--skills-dir` somewhere else has no profile gstack can trust, and with `OPENCLAW_CONFIG_PATH` set OpenClaw reads a config outside the profile, where gstack's settings change would not land, so `--start-hook` is refused and nothing is written. Register without `--skills-dir` (set `OPENCLAW_STATE_DIR` or `HERMES_HOME` to pick the profile), or drop `--start-hook`. When the choice comes from `GSTACK_HARNESS_START_HOOK` or `harness_start_hook` instead of the flag, such a registration just gets no hook, and its receipt says so.
|
||||
The [start hook](hosts.md#start-hook) exists for OpenClaw and Hermes only, and it goes into the harness profile's own plugin folder, the one the harness scans. A registration with `--skills-dir` somewhere else has no profile gstack can trust, and with `OPENCLAW_CONFIG_PATH` naming another config OpenClaw reads settings outside the profile, where gstack's settings change would not land, so `--start-hook` is refused and nothing is written. `OPENCLAW_CONFIG_PATH` naming the profile's own `openclaw.json` (compared after resolving symlinks) is not refused. Register without `--skills-dir` (set `OPENCLAW_STATE_DIR` or `HERMES_HOME` to pick the profile), unset `OPENCLAW_CONFIG_PATH` or point it at the profile's own `openclaw.json`, or drop `--start-hook`. When the choice comes from `GSTACK_HARNESS_START_HOOK` or `harness_start_hook` instead of the flag, the registration gets no hook, and `gstack-harness register` and `./setup --host` exit 4 with the reason (`HARNESS_HOOK_INACTIVE`, `start hook is off`); refreshes only print it.
|
||||
|
||||
<a id="harness-hook-choice-invalid"></a>
|
||||
### `gstack-harness: GSTACK_HARNESS_START_HOOK="<value>" is not on or off` (HARNESS_HOOK_CHOICE_INVALID)
|
||||
@@ -6490,7 +6511,9 @@ Each harness profile has one start hook, owned by the gstack registration that w
|
||||
<a id="harness-hook-inactive"></a>
|
||||
### `gstack-harness: <harness> start hook is written: <why>. Turn it on: <command>` (HARNESS_HOOK_INACTIVE, exit 4)
|
||||
|
||||
The skills are registered and the start hook's files are in place, but the hook is not active in the harness. gstack turns the hook on through the harness's own CLI and then reads it back, so this means the CLI was not on `PATH`, one of its commands failed or timed out (the message quotes its last output), or the harness's settings block the plugin: OpenClaw's `plugins.allow` set without `gstack-start`, `gstack-start` in `plugins.deny`, or `hooks.allowPromptInjection: false`. gstack never edits your allow or deny lists. Run the printed command, then `gstack-harness status`; `--start-hook` again retries the whole step. `./setup --host` exits 4 for the same reason. Refreshes and upgrades retry on their own without failing, and stop once you disable the plugin in the harness.
|
||||
The skills are registered and the start hook's files are in place, but the hook is not active in the harness. gstack turns the hook on through the harness's own CLI and then reads it back, so this means the harness has no config yet (`no OpenClaw config at <path> yet` or `no Hermes config at <path> yet`: run `openclaw onboard` or `hermes setup` first, then `gstack-harness refresh`; gstack runs no harness command until then, because the CLI would create a config holding only gstack's plugin setting), the CLI was not on `PATH`, one of its commands failed or timed out (the message quotes its last output), or the harness's settings block the plugin: OpenClaw's `plugins.allow` set without `gstack-start`, `gstack-start` in `plugins.deny`, or `hooks.allowPromptInjection: false`. gstack never edits your allow or deny lists. Run the printed command, then `gstack-harness status`; `--start-hook` again retries the whole step. `./setup --host` exits 4 for the same reason. Refreshes and upgrades retry on their own without failing, and stop once you disable the plugin in the harness.
|
||||
|
||||
The same code with `start hook is off: <why>` means you asked for the hook through `GSTACK_HARNESS_START_HOOK` or `harness_start_hook` but this registration cannot take it (a custom `--skills-dir`, `OPENCLAW_CONFIG_PATH` naming another config, or an unsafe plugin folder); the message names the fix.
|
||||
|
||||
<a id="harness-qm-credentials"></a>
|
||||
### `gstack-harness: no QM admin credential in the environment` / `... needs --yes` (HARNESS_QM_CREDENTIALS)
|
||||
|
||||
Reference in new issue
Block a user