mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
Merge origin/main (5225e35, v1.92.42.0) into gsta215-ceo-only-driver
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
commit
f4547e07cc
249 files changed
+15292
-6073
No files matched your search
+53
-9
@@ -2,7 +2,7 @@
|
||||
|
||||
gstack uses a declarative host config system. Each supported AI coding agent
|
||||
(Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Hermes,
|
||||
GBrain, Copilot, Antigravity) is defined as a typed TypeScript config object built by the
|
||||
GBrain, Copilot, Antigravity, Gemini CLI) is defined as a typed TypeScript config object built by the
|
||||
`defineHost()` factory. Rendering a new host means creating one file and
|
||||
re-exporting it: the generator, tests, and validation pick it up. Installing
|
||||
it is not free: setup still has one install arm per installable host (see
|
||||
@@ -76,13 +76,13 @@ Inventory of every setup side effect, by phase (line numbers are approximate):
|
||||
|---|---|---|
|
||||
| Build | `browse/dist`, `design/dist`, `make-pdf/dist`, `bin/gstack-cso-*` in the source checkout | source |
|
||||
| Hosts record | `.gstack-installed-hosts` in the source checkout: every host setup installed from it, additive (#1694) | source |
|
||||
| Generation | renders for the recorded hosts only (`.agents/`, `.kiro/`, `.factory/`, `.opencode/`, `.cursor/`, `.copilot/`, `.agy/`) in the source checkout; `gstack-patch-names` rewrites Claude `name:` fields in place | source |
|
||||
| Generation | renders for the recorded hosts only (`.agents/`, `.kiro/`, `.factory/`, `.opencode/`, `.cursor/`, `.copilot/`, `.agy/`, `.gemini-cli/`) in the source checkout; `gstack-patch-names` rewrites Claude `name:` fields in place | source |
|
||||
| Render prune | renders of hosts the record does not name: links into the checkout removed, generated files moved to `$GSTACK_STATE_ROOT/backups/host-renders/<ts>-<id>/`, other files kept | source / state root |
|
||||
| Chromium | Playwright cache (`~/.cache/ms-playwright`), lock under the state root | machine |
|
||||
| Config | `skill_prefix`, `timeline_stop_hook`, team-mode keys in `$GSTACK_STATE_ROOT/config.yaml` | state root |
|
||||
| Claude arm | `~/.claude/skills/gstack` link, one dir per skill with a `SKILL.md` link, alias copies | selected host |
|
||||
| Codex arm | `${CODEX_HOME:-~/.codex}/skills/gstack` runtime root, `gstack-*` links into `.agents/skills`, `.agents/skills/gstack` sidecar | selected host |
|
||||
| Kiro, Factory, OpenCode, Cursor, Copilot, Antigravity arms | the host's `skills/gstack` runtime root (built beside the live one and swapped in) and `gstack-*` links | selected host |
|
||||
| Kiro, Factory, OpenCode, Cursor, Copilot, Antigravity, Gemini CLI arms | the host's `skills/gstack` runtime root (built beside the live one and swapped in) and `gstack-*` links | selected host |
|
||||
| OpenCode commands | `~/.config/opencode/commands/gstack-*.md` with a managed marker (#2629); user command files are never touched | selected host |
|
||||
| Registry | one row per activated install in `installs.tsv` | state root |
|
||||
| Migrations | whatever each `v*.sh` repairs; marker in the state root | state root |
|
||||
@@ -245,12 +245,15 @@ which Google's docs do not state, so the rendered text names no number.
|
||||
|
||||
**Shared `~/.gemini`.** Antigravity CLI and Gemini CLI both live under
|
||||
`~/.gemini`. Gemini CLI reads `~/.gemini/skills` (and `~/.agents/skills`);
|
||||
Antigravity CLI reads `~/.gemini/antigravity-cli/skills`. Setup writes only
|
||||
gstack entries in the latter, uninstall removes only gstack-managed entries
|
||||
there, and `--host auto` selects Antigravity only for `agy` on `PATH` or an
|
||||
existing `~/.gemini/antigravity-cli`, never for a bare `~/.gemini`.
|
||||
`test/setup-install-registry.test.ts` holds both directions. Gemini CLI as a
|
||||
host is not supported. Two overlaps remain: workspace `.agents/skills` is also
|
||||
Antigravity CLI reads `~/.gemini/antigravity-cli/skills`, and Google's
|
||||
migration table lists `~/.gemini/skills` as an Antigravity global path too.
|
||||
Each host's arm writes only gstack entries in its own directory, uninstall
|
||||
removes only gstack-managed entries in each, and `--host auto` selects
|
||||
Antigravity only for `agy` on `PATH` or an existing
|
||||
`~/.gemini/antigravity-cli`, and Gemini CLI only for `gemini` on `PATH`,
|
||||
never either for a bare `~/.gemini`. `test/setup-install-registry.test.ts`
|
||||
holds every direction, including both hosts installed together. Gemini CLI is
|
||||
its own host ("Gemini CLI" below). Two overlaps remain: workspace `.agents/skills` is also
|
||||
where a project-local Codex install puts its Codex-flavored `gstack-*`
|
||||
skills, which Antigravity will list in that repo, and the Antigravity 2.0 app
|
||||
and IDE read `~/.gemini/config/skills`, which this host does not install
|
||||
@@ -277,6 +280,46 @@ root's extra markdown files (`ETHOS.md`, `review/*.md`) as skills, the
|
||||
partial-read behavior of `view_file` on long skills, `ask_question` option
|
||||
handling, and an upgrade from an existing install.
|
||||
|
||||
## Gemini CLI (#1247)
|
||||
|
||||
Shipped as `experimental` (`hosts/gemini.ts`, `./setup --host gemini`, alias
|
||||
`--host gemini-cli`). Verified against Gemini CLI 0.66.0 (its bundled
|
||||
`docs/cli/skills.md` and `SkillManager.discoverSkills`) on 2026-10-31:
|
||||
|
||||
| Fact | Value | Source |
|
||||
|---|---|---|
|
||||
| User skills | `~/.gemini/skills/<skill>/SKILL.md`, and the `~/.agents/skills` alias, which wins inside the user tier | [Agent Skills](https://geminicli.com/docs/cli/skills/), `packages/core/src/skills/skillManager.ts` |
|
||||
| Workspace skills | `.gemini/skills` and the `.agents/skills` alias, loaded only in a trusted folder (`/trust`) | same |
|
||||
| Precedence | built-in < extension < user < workspace; same name: the higher tier wins | same |
|
||||
| Discovery | `<dir>/<name>/SKILL.md`, one level deep; symlinked skill dirs are followed; the name comes from frontmatter `name` | same |
|
||||
| Invocation | the model calls `activate_skill` after you approve; `/skills list` and `gemini skills list` show them | same |
|
||||
| Tool names | `run_shell_command`, `read_file`, `read_many_files`, `write_file`, `replace`, `grep_search`, `glob`, `list_directory`, `ask_user`, `invoke_agent`, `activate_skill`, `enter_plan_mode`, `exit_plan_mode`, `write_todos`, `web_fetch`, `google_web_search` | 0.66.0 bundle |
|
||||
| Context file | `GEMINI.md` | 0.66.0 bundle |
|
||||
|
||||
What the host does: skills in `~/.gemini/skills/gstack-*`, runtime root
|
||||
`~/.gemini/skills/gstack` with `.source-path` for `/gstack-upgrade`, `name:`
|
||||
equal to the `gstack-<skill>` directory, frontmatter limited to `name` and
|
||||
`description` (1024-character limit), the Gemini model overlay, and a
|
||||
one-paragraph tool glossary ahead of the preamble's STATUS rules. The glossary
|
||||
also names Antigravity's `run_command`, `view_file` and `ask_question`,
|
||||
because an Antigravity CLI that reads `~/.gemini/skills` sees these skills.
|
||||
The render directory is `.gemini-cli/` (gitignored), not `.gemini/`, so this
|
||||
repo's own trusted workspace never lists render output as skills. `--local`
|
||||
is refused: it installs only Claude, and Gemini CLI reads workspace skills only
|
||||
in trusted folders.
|
||||
|
||||
Overlaps: inside a trusted repo, a project-local Codex install's `.agents/skills`
|
||||
(Codex-flavored `gstack-*` skills) is the workspace tier, which outranks
|
||||
`~/.gemini/skills`, so Gemini CLI uses those there. gstack never writes
|
||||
`~/.agents/skills`: that alias is shared with other tools and outranks
|
||||
`~/.gemini/skills`. Antigravity's own install stays in
|
||||
`~/.gemini/antigravity-cli/skills`.
|
||||
|
||||
Unverified without a Gemini API key on the build machines (why it is not
|
||||
`full`): a live model run of any skill, `ask_user` option handling, and
|
||||
plan mode with gstack's plan-review skills. Discovery is verified:
|
||||
`gemini skills list` after `./setup --host gemini` lists the `gstack-*` skills.
|
||||
|
||||
## Instruction-only hosts and agent harnesses
|
||||
|
||||
Slate and GBrain have no install arm. OpenClaw and Hermes are harness hosts:
|
||||
@@ -313,6 +356,7 @@ hosts/
|
||||
├── gbrain.ts # GBrain
|
||||
├── copilot.ts # GitHub Copilot CLI
|
||||
├── agy.ts # Google Antigravity CLI
|
||||
├── gemini.ts # Google Gemini CLI
|
||||
└── index.ts # Registry: imports all, derives Host type
|
||||
```
|
||||
|
||||
|
||||
@@ -195,6 +195,11 @@ registered as a short entry that tells the agent to read the skill's full
|
||||
`SKILL.md` in `gstack/skills/`, a plain copy of every skill that OpenClaw
|
||||
reads in pages (`.gstack-skills/` under `--skills all`).
|
||||
Install, verify and remove: [hosts.md](hosts.md#agent-harnesses).
|
||||
Agents in OpenClaw read gstack's skills but rarely run their start command, so
|
||||
there is an opt-in [start hook](hosts.md#start-hook): `gstack-harness register
|
||||
openclaw --start-hook` adds a `gstack-start` plugin that puts a one-line note on
|
||||
every user turn, and grants it the conversation access OpenClaw requires for
|
||||
`before_prompt_build` through `openclaw config set`.
|
||||
|
||||
The ACP artifacts in `openclaw/` are generated by `bun run gen:skill-docs --host openclaw`.
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ gstack/
|
||||
│ ├── codex.ts, factory.ts, kiro.ts # Existing hosts
|
||||
│ ├── opencode.ts, slate.ts, cursor.ts, openclaw.ts # IDE hosts
|
||||
│ ├── hermes.ts, gbrain.ts # Agent runtime hosts
|
||||
│ ├── copilot.ts, agy.ts, gemini.ts # GitHub Copilot CLI, Antigravity CLI, Gemini CLI
|
||||
│ └── index.ts # Registry: exports all, derives Host type
|
||||
├── scripts/ # Build + DX tooling
|
||||
│ ├── gen-skill-docs.ts # Template → SKILL.md generator (config-driven)
|
||||
|
||||
@@ -795,7 +795,9 @@ When a judge's abort signal cancels its request, `callJudge`
|
||||
(`test/helpers/llm-judge.ts`) prints one `llm-judge-aborted` JSON line to
|
||||
stderr: elapsed time, wrapper `attempts` and `retryWaitsMs` (429 waits),
|
||||
`httpRequests` and `httpStatuses` (so SDK retries show), `connectedMs`,
|
||||
`firstEventMs` and `lastEventMs`, the open `block` type, `thinkingChars` and
|
||||
`firstEventMs` and `lastEventMs`, `bytesReceived` and `lastByteMs` (every body
|
||||
byte, pings included, so a silent socket differs from a model thinking on a live
|
||||
connection), the open `block` type, `thinkingChars` and
|
||||
`textChars`, `messageId`, `requestId` and the `reason` (at most 200
|
||||
characters). It holds counts and ids only, never content, and changes no
|
||||
request, score or verdict.
|
||||
@@ -1018,7 +1020,9 @@ first (`UBI_REAP=0` skips it). Names outside the `ubirun-` prefix are never reap
|
||||
quota preflight adds the size's vCPUs to the project's running VMs and refuses
|
||||
before the create request when that exceeds the known ceiling
|
||||
(`UBI_QUOTA_VCPUS`, or the maximum the last refusal printed, remembered in the
|
||||
state root; `UBI_QUOTA_WAIT=<seconds>` waits for room instead). `run`, `verify`
|
||||
state root; `UBI_QUOTA_WAIT=<seconds>` waits for room instead). When the usage read fails (`usage` exits 1 on an
|
||||
unreadable VM list or size and skips a VM that 404s), the preflight retries
|
||||
within `UBI_QUOTA_WAIT`, then logs and leaves the decision to the API. `run`, `verify`
|
||||
and `ssh NAME COMMAND` stream the remote output through a watchdog: a heartbeat
|
||||
line every `UBI_HEARTBEAT` seconds (300), and a command silent for
|
||||
`UBI_IDLE_TIMEOUT` seconds (1800) or older than `UBI_RUN_TIMEOUT` (0, unlimited)
|
||||
@@ -1031,7 +1035,7 @@ repository content: its setup script and pull destinations must resolve inside
|
||||
`--src`, and a name it lists under `pass` is forwarded only when the command
|
||||
line grants it (`--pass NAME`, or all of them with `--trust-config`); `--pass`
|
||||
never forwards `UBICLOUD_*`. All of this is pinned offline in
|
||||
`test/ubicloud-runner.test.ts` against a fake API and a fake `ssh`, including
|
||||
`test/ubicloud-runner*.test.ts` against a fake API and a fake `ssh`, including
|
||||
a fixture whose fake API creates, so `run` and `verify` execute end to end.
|
||||
|
||||
`run` and `verify` start their setup, command and tier phases detached on the
|
||||
@@ -1047,7 +1051,7 @@ start/pass/fail per tier under `UBI_CHECKPOINT_DIR/<run key>/`, so a resume on
|
||||
a fresh VM reruns setup and only the tiers without a pass; `pregate-remote.sh`
|
||||
adds its tree and planned-set digest to the key, so a rerun of
|
||||
`gstack-pregate --stage tests --remote` reattaches and bills from the original
|
||||
`created_at`. The fake VM in `test/ubicloud-runner.test.ts` runs these phases
|
||||
`created_at`. The fake VM in `test/helpers/ubi-runner-fake-vm.ts` runs these phases
|
||||
for real in a directory.
|
||||
|
||||
Under 8 cores (`gstack-doctor`'s `cores` row says so), run the full unit and
|
||||
|
||||
@@ -96,7 +96,7 @@ The plan-ceo-review-plan-mode (:89, :108), plan-devex-review-plan-mode (:181-222
|
||||
| Case | Check (file:line) | Matches | Structured source | User-visible | Disposition |
|
||||
|---|---|---|---|---|---|
|
||||
| ship-docsync-store | test/skill-e2e-ship-docsync.test.ts:74, :78 | ship report ~ `Documentation…blocked`; not `Documentation: current` | no receipt, no publish call (:75-77) | yes (report status) | (c) |
|
||||
| ship-docsync-completion, ship-docsync-current | test/skill-e2e-ship-docsync.test.ts:81, :97 | parsed docs contract, then report contains `contract.documentation_section` | docs dispatch JSON (ship/sections/documentation.md.tmpl:52-56) | yes | (a) |
|
||||
| ship-docsync-completion, ship-docsync-current | test/skill-e2e-ship-docsync.test.ts:85, :105 | parsed docs contract, then report contains `contract.documentation_section` | docs dispatch JSON (ship/sections/documentation.md.tmpl:52-56) | yes | (a) |
|
||||
| ship-docsync-late-result, -missing-marker, -missing-asset, -launch-failure, -timeout-unsettled, -failure | test/helpers/docsync-fault-eval.ts:28, :30 | non-success report ~ `Documentation…blocked`, no `Documentation: current` outside inline code | actor event log decides dispatch count and publication first (:22-27) | yes | (c) |
|
||||
| ship-docsync-recovery, -stale-before, -stale-after | test/helpers/docsync-fault-eval.ts:31 | report contains the accepted audit id | actor state `acceptedId` | yes | (a) |
|
||||
| docsync-spawned | test/skill-e2e-docsync-spawned.test.ts:60 | `contract.blockers` ~ security/sensitive | parsed docs contract | no | (a) |
|
||||
|
||||
@@ -107,7 +107,11 @@ After fixing auth, run any skill to retry sync automatically.
|
||||
git's error appears after the colon.
|
||||
|
||||
**Cause.** Could be network issue, rejected push (remote ahead), server 500,
|
||||
or repo access revoked.
|
||||
or repo access revoked. The line is git's own error, not its `To <remote>`
|
||||
banner or `hint:` lines. After a rejected push, brain-sync fetches and merges
|
||||
the other machine's commits; when that merge fails, the status adds
|
||||
`; merging origin failed: <reason>` (for example `CONFLICT (content): Merge
|
||||
conflict in developer-profile.json`).
|
||||
|
||||
**Fix.** Look at `~/.gstack/.brain-sync-status.json` for more detail, or run:
|
||||
```bash
|
||||
|
||||
@@ -191,6 +191,15 @@ append commits. Git's default would conflict at the file tail, but the
|
||||
You shouldn't see conflict prompts. If you do (a real semantic conflict,
|
||||
like two machines editing the same plan), git will stop and prompt.
|
||||
|
||||
When a push is rejected, brain-sync fetches and merges the other machine's
|
||||
commits itself. Local edits to files that merge would change are set aside
|
||||
and put back afterward, and a failed merge is aborted, so `~/.gstack` never
|
||||
holds conflict markers. If a local edit conflicts with the other machine's
|
||||
change, the other machine's version stays in the working tree and your edit
|
||||
is kept in a git stash; `gstack-brain-sync --status` names the stash and the
|
||||
`git -C ~/.gstack --literal-pathspecs restore --source=<stash> -- <path>`
|
||||
command that brings it back.
|
||||
|
||||
## Cross-machine pull cadence
|
||||
|
||||
The preamble runs `git fetch` + `git merge --ff-only` once per 24 hours
|
||||
|
||||
@@ -24,6 +24,8 @@ neither does `./setup --local`.
|
||||
|
||||
**What it writes.** Copies in the harness's own skills folder, never edits to
|
||||
`openclaw.json`, Hermes's `config.yaml` or any context file you did not name.
|
||||
The one exception is the [start hook](#start-hook), which you turn on yourself:
|
||||
it adds a plugin and enables it through the harness's own commands.
|
||||
A context block is opt-in: `gstack-harness register openclaw|hermes|generic
|
||||
--context <file>` writes the marked block into that file only, after printing
|
||||
the file and its scope (`profile` inside the harness's own home, else
|
||||
@@ -67,6 +69,30 @@ characters, the part of a description Hermes's skills index shows.
|
||||
| Grok Bot, any chat-only skill library | nothing on disk | `gstack-harness register generic --print`, then paste the router and block into the app | ask it for a gstack skill | delete the pasted text |
|
||||
| any other harness | the router in an Agent Skills folder you name, and/or a 600-byte routing block between `<!-- gstack:begin harness-routing -->` markers in a context file you name | `gstack-harness register generic --skills-dir <dir>` and/or `--context <file>`; `gstack-harness manifest generic` prints every skill as JSON | the harness's own skill list | `gstack-harness unregister generic` |
|
||||
|
||||
<a id="start-hook"></a>
|
||||
### Start hook (OpenClaw and Hermes, opt-in)
|
||||
|
||||
Every gstack skill starts with a command in its Preamble (`gstack-skill-start`) that sets up the session: whether it runs unattended, questions, consent, upgrade checks. In OpenClaw and Hermes, agents read gstack's skills but almost never run that command (1 start in 29 chances, HARN-18). The start hook is a small `gstack-start` plugin that puts one note of about 210 characters on every user turn, telling the agent to run that command first, exactly as written, with no pipe.
|
||||
|
||||
It is off unless you turn it on, because turning it on changes the harness's own settings:
|
||||
|
||||
| | OpenClaw | Hermes |
|
||||
|---|---|---|
|
||||
| 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) |
|
||||
| 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.
|
||||
|
||||
**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.
|
||||
|
||||
**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`.
|
||||
|
||||
**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.
|
||||
|
||||
**One owner.** A `gstack-start` folder this registration did not write stops the hook (`HARNESS_HOOK_OWNED_ELSEWHERE`): turn it off from the gstack install that wrote it, or move it aside.
|
||||
|
||||
OpenClaw's ACP route below is unchanged: native registration gives OpenClaw's
|
||||
own agent gstack's skills; ACP spawns Claude Code with the full install.
|
||||
|
||||
@@ -137,6 +163,14 @@ Antigravity only when `agy` is on `PATH` or `~/.gemini/antigravity-cli` exists,
|
||||
never for a Gemini-CLI-only `~/.gemini`. The Antigravity 2.0 app and IDE read
|
||||
`~/.gemini/config/skills`, which this host does not install.
|
||||
|
||||
Gemini CLI loads gstack skills itself when a task matches (`activate_skill`,
|
||||
after you approve); ask for `gstack-office-hours`, or list them with
|
||||
`/skills list`. Setup writes only `~/.gemini/skills/gstack*` and never
|
||||
`~/.agents/skills`, and `--host auto` selects Gemini CLI only when `gemini` is on
|
||||
`PATH`. Inside a repo with a project-local Codex install, Gemini CLI uses the
|
||||
Codex skills in `.agents/skills` (the workspace tier wins), and workspace skills
|
||||
load only in trusted folders (`/trust`).
|
||||
|
||||
Outside reviews require the selected CLI to be installed and authenticated: Claude Code when using gstack in Codex, or Codex on other harnesses. External harnesses discover these commands as `/gstack-claude-code` and `/gstack-codex`; each harness omits its own wrapper. Explicit provider requests keep that provider. The existing `codex_reviews` setting controls automatic outside reviews where supported, regardless of the provider selected.
|
||||
|
||||
`/claude` has been renamed to `/claude-code`. Re-run `./setup --host <name>` to migrate managed installations, including other harnesses sharing the checkout. Setup preserves the previous installation if replacement generation or installation fails and prints repair instructions.
|
||||
|
||||
+114
-16
@@ -75,10 +75,10 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`).
|
||||
| Key | Meaning |
|
||||
|---|---|
|
||||
| `runner` | the local test command with `{files}` (gstack: `bun test {files}`); unset, it is detected (bun, vitest, jest, pytest, `node --test`, and `flutter test`/`dart test` per Dart package root, through fvm when that package or an ancestor pins it with `.fvmrc` or `.fvm/fvm_config.json` and fvm is on PATH; `cargo test -p <name>` per Rust crate, run from the root of the workspace the crate is a member of (Cargo's `members` globs, `exclude` and `package.workspace`), else the crate directory; a JS/TS test whose nearest package.json is a nested package declaring vitest or jest (a dependency or its `test` script) runs from that package, with package-relative paths, through the package manager its `packageManager` field or the nearest lockfile up to the repo root names: `pnpm exec`, `yarn`, `bun x`, else `npx`; every other JS/TS test keeps the root detection); nothing detected, a Dart SDK, cargo or package manager missing from PATH, a `.dart` file with no `pubspec.yaml` above it, a `.rs` file outside any crate, or one in a crate whose name Cargo would reject (anything but letters, digits, `_` and `-`), is `incomplete` (the runnable groups still run, so a real failure still fails) |
|
||||
| `dependencies` | glob → test files for subprocess targets, runtime reads and generated inputs the import graph cannot see; `[]` declares "no lane needed" (docs, licences) |
|
||||
| `dependencies` | glob → test files for subprocess targets, runtime reads and generated inputs the import graph cannot see; `[]` declares "no lane needed" (docs, licences). A `[]` that matches a deleted test does not skip it: the deleted test still runs its owning suite, and a file naming it that `[]` covers (a durations list) counts as covered. With this key present, even empty, the policy [declares lanes](#lane-requirements) |
|
||||
| `empty_lanes` | extra globs for the docs/no-tests empty lane. Any Markdown file already qualifies with no pin file when no test depends on it (the same import graph as other files: a quoted path, a `join()` of path segments, an import, or a `dependencies` entry). reStructuredText and AsciiDoc count as Markdown here. README, CHANGELOG, CONTRIBUTING, LICENSE, AGENTS and CLAUDE qualify even without `.md`, and so does licence text under any vendored name (`roboto-LICENSE.txt`, `LICENSE-MIT`, `licenses/*.txt`). Source code and files under a test directory never qualify. A test that reads the file still selects that test |
|
||||
| `selection` | `workflow.yml/job` → a command printing the lane's file list, for matrices a planner computes (gstack: `scripts/test-free-shards.ts --list`) |
|
||||
| `warn` | check ids downgraded to warn; `secrets` and required lanes never |
|
||||
| `warn` | check ids downgraded to warn; `secrets` and required lanes never. `"lanes"` downgrades only a touched file with no lane: the selected tests still run, a red lane stays `fail` (`PREGATE_LANE_FAILED`) and an unfinished one `incomplete`; a green run is `warn` `PREGATE_NO_LANE` naming the unmapped files |
|
||||
| `strays.allow` | intentional root files or new top-level directories |
|
||||
| `local` | `workflow.yml/job` lanes a local run satisfies: a platform-agnostic toolchain (an `ubuntu-latest` suite that runs the same on a Mac), or a repo whose CI is off. Their tests run here instead of becoming `requires-remote`, except that on a machine under 8 cores with a `remote_gate` or a remote `test_backend` the small-machine rule still records the test tier as a `requires-remote` obligation. `--explain` notes an id that matches no workflow lane |
|
||||
| `guards` | `[{ id, when: [globs], command, fix?, timeout_s? }]`: repo commands a lane must pass when a touched file matches `when` (manifest checks, subprocess goldens the import graph cannot see); see [guards](#guards) |
|
||||
@@ -88,7 +88,7 @@ the byte-pinned `/ship` renders in `test/fixtures/golden/`).
|
||||
| `stall_timeout_s` | seconds a test command may print nothing 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)) |
|
||||
| `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)) |
|
||||
| `remote_gate_runs` | `{ profiles, inventory }`: the run profiles that together clear `pregate.json/remote_gate`, and the command that prints the remote gate's test files, one per line, which their runs must cover (see [run evidence](#run-evidence)) |
|
||||
| `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)) |
|
||||
|
||||
@@ -141,6 +141,19 @@ unmapped file is `PREGATE_NO_LANE` again: the warning is for the first /ship
|
||||
in a new repository (three Captains lost their first pass to it), never a way
|
||||
to hide missing coverage. `--policy-from head` with a head policy is strict too.
|
||||
|
||||
<a id="lane-requirements"></a>
|
||||
**Lane requirements.** "Any policy" above means a policy that declares lanes:
|
||||
one with a `dependencies`, `empty_lanes`, `selection` or `local` key, even an
|
||||
empty one (`gstack-pregate init` writes the first three). A policy with none of
|
||||
them, such as
|
||||
`{"timeout_s": 1800}` to give long tests more time, keeps the no-policy lane
|
||||
verdicts: an unmapped file is the `warn` `PREGATE_NO_POLICY`, the selected
|
||||
tests run, a red lane is `fail`, and the starter says the active policy
|
||||
declares no lanes instead of claiming there is none. So raising
|
||||
`timeout_s` never needs a `"**": []` catch-all. Add `dependencies` when you
|
||||
want unmapped files to fail. The same rule applies to the user-owned file and to
|
||||
a `--policy-from head` bootstrap (gbrain-evals #272, GST-775).
|
||||
|
||||
`gstack-pregate init` writes a draft built from the repo, not a fixed example.
|
||||
It prints the languages it found and the runner each language's tests would
|
||||
get, then maps every file the import graph cannot place to the tests of the
|
||||
@@ -160,7 +173,7 @@ The list lives in its own file, not in `pregate.json`, because older pregate rea
|
||||
|
||||
```
|
||||
gstack-pregate [--stage preflight|tests|all] [--only <check,..>] [--allow-repo-commands] [--policy-from base|head]
|
||||
[--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--explain] [--json]
|
||||
[--timeout <s>] [--stall-timeout <s>] [--remote --spend-cap <usd>] [--run <id>] [--from-run <id|url>] [--explain] [--json]
|
||||
gstack-pregate verify | clear --remote <workflow.yml/job> --run-url <url> | lanes | validate | init
|
||||
```
|
||||
|
||||
@@ -266,9 +279,10 @@ Two stages with separate clocks, both printed:
|
||||
`.cjs`, `.mts`, `.cts`, then `index.ts`, `index.tsx`, `index.js`, `index.jsx`
|
||||
and `index.mjs` inside the directory; the first file that exists wins.
|
||||
A touched source file with no lane is `FAIL lanes — <file> has no test lane`
|
||||
once any policy exists (the base copy, the user-owned file or `--policy-from
|
||||
head`); with no policy anywhere it is the `warn` `PREGATE_NO_POLICY` with a
|
||||
starter printed ([no policy](#no-policy)); never a pass. Markdown that no
|
||||
once a policy that [declares lanes](#lane-requirements) exists (the base copy,
|
||||
the user-owned file or `--policy-from head`); with no policy anywhere, or one
|
||||
that declares no lanes, it is the `warn` `PREGATE_NO_POLICY` with a starter
|
||||
printed ([no policy](#no-policy)); never a pass. Markdown that no
|
||||
test depends on is a docs/no-tests empty lane
|
||||
instead (`docs/no-tests: N documentation file(s)`), including when the base
|
||||
branch has no `.gstack/pregate.json`. The dependency is the same import graph
|
||||
@@ -654,15 +668,20 @@ old the run may be, and `os` (`Linux`, `macOS`, `Windows`) requires
|
||||
`tree.json` to record that runner OS. 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.
|
||||
run executed), `tests` (the count) and, with `os`, the runner OS or a list of
|
||||
every OS the run's test jobs ran on (the profile's `os` must be one of them).
|
||||
|
||||
**What `clear --run` checks** (`lib/run-evidence.ts`), for every run, online,
|
||||
once:
|
||||
|
||||
- The run belongs to this repository and ran for a same-repository head. Its
|
||||
workflow file is the profile's, its event is `push` or `workflow_dispatch`
|
||||
(a `pull_request` run tests the merge commit, not the head tree, so no
|
||||
profile may accept it), and it completed with success. The API answers with
|
||||
workflow file is the profile's, its event is one the profile lists (`push`,
|
||||
`workflow_dispatch` or `pull_request`), and it completed with success. A
|
||||
`pull_request` run tested GitHub's merge commit, not its head, so its
|
||||
`tree.json` must name the head (`head_sha`) and the merge commit (`sha`), and
|
||||
the tree git computes for that merge commit must equal the head's tree: the
|
||||
head already contained its base, so the merge tested exactly the head tree
|
||||
(`tree_mismatch` otherwise). The API answers with
|
||||
the newest attempt, so an earlier green attempt under a newer red one is
|
||||
refused, and the run is read again after the evidence is collected: a new
|
||||
attempt meanwhile is `run_changed`.
|
||||
@@ -730,9 +749,21 @@ recording. When the base config or the GitHub origin cannot be read, `--run`
|
||||
fails, and so do `--run-url` and `--bundle-lane` unless the base was read and
|
||||
shows the lane has no profile; then the lane is recorded as before, with a
|
||||
warning naming the cause. `gstack-ship-queue ready --dispatch-ci` still
|
||||
records its dispatched runs by URL; routing it through the verifier is the next
|
||||
change (P3b), so a repository that declares lane profiles clears those lanes
|
||||
with `gstack-pregate clear --run`.
|
||||
records its dispatched runs by URL; routing it through the verifier is a
|
||||
follow-up (GST-555), so a repository that declares lane profiles clears those
|
||||
lanes with `gstack-pregate clear --run`.
|
||||
|
||||
**gstack's own profiles.** gstack's `.gstack/pregate.json` declares two, and
|
||||
names neither after a lane: `ci-reuse/free-tests` (the complete Free Tests run:
|
||||
every non-matrix job and the `free-tests` aggregate, `os` Linux) and
|
||||
`ci-reuse/windows-free-tests` (`windows-plan` and the `windows-free-tests`
|
||||
aggregate, `os` Windows). Each accepts `pull_request` and `workflow_dispatch`
|
||||
runs and reads the `tested-tree` artifact, which both aggregates upload only
|
||||
after their strict verification passes. The main-push reuse job and
|
||||
`--from-run` read them. No lane has a profile, so every macOS and Windows lane
|
||||
still clears by `--run-url` as before. A pull request cut before these
|
||||
workflows uploaded `tested-tree` has no record a lane profile could verify, so
|
||||
lane profiles wait until open pull requests carry the producer (GST-555).
|
||||
|
||||
**`verify` stays offline.** The check ran at `clear`. `gstack-pregate verify`,
|
||||
and `gstack-ship-queue ready` through it, refuse a run receipt whose `tree=`
|
||||
@@ -742,8 +773,75 @@ merge path.
|
||||
|
||||
**What is never reusable.** Security and advisory scans (osv, audit,
|
||||
gitleaks) depend on the day, not on the tree, so they are not run profiles:
|
||||
they run on every head as before. Pull-request required checks always run
|
||||
fresh.
|
||||
they run on every head as before, and `--from-run` never skips them.
|
||||
Pull-request required checks and dispatched runs always run fresh; only a
|
||||
push to main reuses ([below](#ci-reuse)).
|
||||
|
||||
<a id="from-run"></a>
|
||||
### The tests stage from a Free Tests run (`--from-run`)
|
||||
|
||||
`gstack-pregate --stage tests --from-run <run id|url>` takes a Free Tests run
|
||||
that already passed on this exact tree, for example the one CI ran on the
|
||||
pushed head, instead of running its files again here. The run is verified
|
||||
online, once, against the base branch's run profiles for this machine's OS
|
||||
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 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>;
|
||||
<k> still run`.
|
||||
- The rest run as they would have: locally, or recorded as the remote gate
|
||||
obligation on a small machine, or through `--remote`.
|
||||
- A run that does not verify prints `REUSE: refused reason=<code> — …; every
|
||||
selected test runs here` and changes nothing else. The codes are
|
||||
verifyRun's, plus `tree_mismatch` (another tree), `no_tests`, `no_profile`
|
||||
(no profile for this OS covers a local lane's workflow), `no_verified_run`
|
||||
(more than one profile applied and each refused; the detail lists every
|
||||
refusal) and `profiles_unavailable` (the base config or the GitHub origin
|
||||
could not be read).
|
||||
|
||||
`--from-run` applies to the tests stage only, so `--stage preflight` refuses it
|
||||
as a usage error. It changes nothing in the preflight stage: with `--stage all`
|
||||
the preflight checks, the secrets scan included, run as before, and
|
||||
`--stage tests` runs no preflight check in that invocation, with or without it.
|
||||
|
||||
<a id="ci-reuse"></a>
|
||||
### Main pushes reuse the merged pull request's run
|
||||
|
||||
A squash merge whose pull request head already held main produces a push with
|
||||
the identical tree, and Free Tests and Windows Free Tests used to run on it a
|
||||
second time. Now each workflow starts with a `reuse` job that runs only on a
|
||||
push to `main`, with job-level `contents: read` and `actions: read` (every other
|
||||
job keeps the workflow's `contents: read`). It runs
|
||||
`scripts/ci-tree-reuse.ts find`, which:
|
||||
|
||||
1. finds the pull request whose merge commit is the pushed commit
|
||||
(`repos/{repo}/commits/{sha}/pulls`, one page of at most 10), merged into
|
||||
main from a same-repository head (`no_pr`, `ambiguous_pr`, `fork_head`);
|
||||
2. requires that head's tree to equal the pushed tree (`tree_mismatch`);
|
||||
3. lists that head's `pull_request` and `workflow_dispatch` runs of the same
|
||||
workflow, newest first, at most 10 (`no_candidate`); and
|
||||
4. accepts the first one `verifyRun` accepts under the profile in the pushed
|
||||
tree's `.gstack/pregate.json` (`no_verified_run` lists each refusal; a newer
|
||||
red run on the head vetoes an older green one).
|
||||
|
||||
It writes `reused=true` with the run's id and link, or `reused=false` and the
|
||||
reason, and prints one `REUSE:` line in the step summary. Every suite job
|
||||
needs `reuse` and runs unless it succeeded with `reused=true`; their
|
||||
conditions start with `!cancelled()` because GitHub otherwise skips a job whose
|
||||
needed job was skipped, and `reuse` is skipped on every pull request and
|
||||
dispatch run. The required aggregates (`free-tests`, `windows-free-tests`)
|
||||
choose a path in their first step, before any download. A verified reuse writes
|
||||
the reused run's link to the summary and passes; anything else, a failed or
|
||||
skipped `reuse` job included, takes the strict path unchanged. Only the strict
|
||||
path records and uploads `tested-tree`, after its verification passes, so a
|
||||
red, partial or reused run never leaves a record and a reuse is never reused.
|
||||
The pull request lookup needs no `pull-requests` permission on a public
|
||||
repository; a private fork of this setup would need `pull-requests: read`,
|
||||
and without it every push falls back to the full run.
|
||||
|
||||
<a id="guards"></a>
|
||||
## Guards and the lane-done rule
|
||||
|
||||
@@ -82,7 +82,7 @@ a time; each is independent.
|
||||
|
||||
| File | Why | Without it |
|
||||
|---|---|---|
|
||||
| `.gstack/pregate.json` | Maps changed paths to test lanes, so `/ship` runs the tests that cover the diff and the remote gate selects correctly ([docs/pregate.md](pregate.md)) | `/ship` fails every touched file with `PREGATE_NO_LANE`. Write it once, from one owner, before the crew starts. |
|
||||
| `.gstack/pregate.json` | Maps changed paths to test lanes, so `/ship` runs the tests that cover the diff and the remote gate selects correctly ([docs/pregate.md](pregate.md)) | `/ship` warns on every touched file with no test lane (`PREGATE_NO_POLICY`) and prints a starter policy; the selected tests still run. A policy that sets only `timeout_s` or other keys without `dependencies`, `empty_lanes`, `selection` or `local` behaves the same. Write it once, from one owner, before the crew starts. |
|
||||
| `.gstack/ship-policy.json` | `queue_mode`, the `train` block (gate and free workflow names, the full-gate check, the train size and fill wait), the stamp files ([docs/ship-policy.md](ship-policy.md)) | Per-PR queue; `train …` refuses with `QUEUE_TRAIN_OFF`. |
|
||||
| `CHANGELOG.md` + `VERSION` | A stamp per merge and a twice-weekly release with notes ([docs/releases.md](releases.md)) | Skip the version and changelog steps; the train still works on the tree receipt. |
|
||||
| `AGENTS.md` validation discipline | Diagnose before fixing, focused checks during repair, one full suite at the end on a frozen tree | Threads retry blindly and report passes from older revisions. |
|
||||
|
||||
+19
-2
@@ -462,7 +462,11 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
literal diff base, the plan's `DIFF_CMD` excludes, stack, test framework,
|
||||
learnings, checklist paths, the Red Team's merged findings and specialist count,
|
||||
and the outside voice's branch diff. A section that lost an anchor or placeholder
|
||||
refuses (`BRIEF_RENDER_FAILED`) instead of rendering something else.
|
||||
refuses (`BRIEF_RENDER_FAILED`) instead of rendering something else, and so does
|
||||
a slot the section leaves unfilled (every reader but fix-check and the outside voice).
|
||||
That check runs before the findings and learnings go in, and they reach the reader
|
||||
as written, so a finding that quotes `origin/<base>` neither fails the render nor
|
||||
gets filled (GST-797).
|
||||
2. The dispatch program passes that file's content unchanged as the reader's
|
||||
prompt and runs `sent` on what it passed: a hash that differs from the rendered
|
||||
one is `BRIEF_MISMATCH`, and that reader's review does not count.
|
||||
@@ -478,6 +482,15 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
adversarial voice's FIXABLE collects as CRITICAL (normalizer v2, GSTA-167). An unnumbered, unlabeled bullet with no file:line
|
||||
under a paragraph finding whose Severity or Blocks line has not come yet is that finding's detail, so the field line after it
|
||||
lands on the finding; a numbered item is its own finding.
|
||||
A negated severity word still collects at its label ("found nothing CRITICAL", "No P0/P1 issues"), so a reader's
|
||||
closing summary is a known phantom: it stays at that label in the judged set, lead judgment can fold it only into a
|
||||
finding at least as severe, and it counts toward what blocks. GST-766 skipped negated labels and lost
|
||||
real blockers named in an exception clause ("No CRITICAL issues except the token leak in log.ts:3"); GSTA-167
|
||||
reverted that. Each prose finding's line also
|
||||
records the Severity and Blocks its own text states as `stated`, beside the collected label and never in its place;
|
||||
a classified finding that carries `stated` shows it in its deferred-review issue next to the label ("CRITICAL (the
|
||||
reviewer wrote Severity: Low, Blocks: no)"). /ship's Step 9.4 does not copy `stated` into its findings file yet: that
|
||||
instruction lives in a reviewer brief that changes only after a no-loss replay. What blocks is unchanged.
|
||||
A phantom finding costs a fold; a lost one costs the gate.
|
||||
A reply with no finding counts as a review only with the clean marker its own brief asks for: `NO FINDINGS` alone on a
|
||||
line for the specialists, the batched specialist, the targeted reader, the Red Team and fix-check, the `Recommendation:`
|
||||
@@ -515,7 +528,10 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
create whose result was lost by its title and never starts a reader twice; a
|
||||
missing or doubled title stops the pass. On Capy the pass runs as one call to the
|
||||
checked-in program `lib/capy-review-pass.js`, served by `program` and loaded
|
||||
unchanged by the gstack Capy skill.
|
||||
unchanged by the gstack Capy skill. A full repeat pass takes `--diff-base` (RANGE_BASE; a scoped pass, none), a
|
||||
fix-check plan renders its reader from `--fix-list`, `--hunks`, `--tests` and `--callers`,
|
||||
`--audit-only` runs just the `--extras` readers (Step 14.5's docs audit), and the fork
|
||||
point is found against `origin/<base>` first, as preflight does.
|
||||
7. Every brief, audit readers' included, ends with a workspace paragraph naming the
|
||||
checkout the bins run in (GST-482): a reader on a shared machine starts in the
|
||||
machine's working directory, so it is told to `cd` there on every command, to give
|
||||
@@ -531,6 +547,7 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends:
|
||||
## Where this is enforced
|
||||
|
||||
- `lib/review-brief.ts`, `lib/review-ledger.ts`, `lib/review-pass.ts`, `lib/capy-review-pass.js`, `bin/gstack-review-dispatch`: [verbatim dispatch](#verbatim-dispatch) (`test/review-dispatch.test.ts`, `test/review-pass.test.ts`).
|
||||
- `lib/ship-parts.ts`, `bin/gstack-ship-parts`: the section text each /ship phase uses, returned once per run inside the phase program before it; `next --resend` sends an unreceived batch again (`test/ship-parts.test.ts`).
|
||||
|
||||
- `scripts/resolvers/review-army.ts`: the floor and `NO FINDINGS` in every specialist and Red Team prompt (rendered into `review/sections/review-army.md` and `ship/sections/review-army.md`), and the lead judgment's "On record" line that turns a [ruled class](#rulings-below-the-floor) into a note; in `/ship` every specialist and Red Team prompt it renders carries its diff base as a literal sha the parent writes in (`DIFF_BASE=<diff-base>`: the merge base on pass 1, `RANGE_BASE` from pass 2 on) beside its plan's `DIFF_CMD` excludes, because a reviewer is its own process and never sees the parent's shell variables; the fan-out batches above.
|
||||
- `ship/sections/review-army.md` Step 9: the plan's `REVIEW_DEPTH`, `GATE_SCAN` and `--depth-floor` on every run, the depth fields of the review-log row, passes, `RANGE_BASE`, classify, defer, render, the cap check before any edit, the Step 11 late-defer row, scratch files under `.gstack/tmp/`, the cap decisions; `ship/sections/adversarial.md` Step 11: the outside voice reads the same range and scopes queued fixes the same way.
|
||||
|
||||
+10
-3
@@ -41,7 +41,7 @@ unless `--label` is given.
|
||||
|
||||
**Counting calls.** `gstack-ship-budget calls --events <file> [--since <iso>] [--until <iso>]` reads
|
||||
the host's transcript items (a JSON array, or `{ "items": [...] }` as Capy's `tools.thread.get`
|
||||
transcript returns) and prints
|
||||
transcript returns; a leading `~` or `~/` in `<file>` means the home directory) and prints
|
||||
`CALLS: total= decision= keepalive= idle_gaps_over_300s= max_gap_s= first= last= source=`. Every tool
|
||||
item is one parent call; a `wait`, or a program that only reads status (plus at most one bash for its
|
||||
note), is a keep-alive, and a gap over 300 s between calls outlives the prompt cache
|
||||
@@ -76,8 +76,14 @@ one program measure nothing: Capy books a call after its program ends.
|
||||
|
||||
A jam's own counter includes its own subagents, outside voices too (measured 2026-10-30: +10.92M
|
||||
across a six-reader dispatch with two ~0.35M parent calls between). `"this"` is the root thread,
|
||||
whose counter also holds every sibling tree, so a subagent running /ship passes its own jam id;
|
||||
a root thread's own jam id is its `"this"`. A Capy handoff child cannot read its own jam id, so it
|
||||
whose counter also holds every sibling tree. A subagent running /ship cannot read its own jam id, so it
|
||||
reads `"this"` and starts with `--subagent --reading-from this --meter-scope tree` (`GSTACK_SHIP_SUBAGENT=1` in its
|
||||
brief stands in for `--subagent` only; `--reading-from this` is still required): the run is tree-scoped, and every BUDGET, NOTE, BUDGET_FINAL
|
||||
and BUDGET_TIMELINE line it prints is followed by `METER: tree (root counter, upper bound)`. A
|
||||
subagent that asks for `--meter-scope self` while reading `"this"` is refused
|
||||
(`METER_SCOPE_PARENT`). A `--token-cap` on a tree-scoped run stops on that upper bound, so sibling
|
||||
lanes count against it. Exact per-run cost comes from a root replay thread: a root thread that runs
|
||||
only that /ship, whose `"this"` is its own counter. A root thread's own jam id is its `"this"`. A Capy handoff child cannot read its own jam id, so it
|
||||
verifies with no reading and prints `METER: parent`; the parent's handoff and reconcile readings,
|
||||
taken on its own counter (which holds the child), measure it.
|
||||
|
||||
@@ -286,6 +292,7 @@ stop rules) before claiming a saving from the cap or from delegation.
|
||||
|
||||
[BUDGET_CAP_REACHED](troubleshooting.md#budget-cap-reached) ·
|
||||
[BUDGET_READING_MISSING](troubleshooting.md#budget-reading-missing) ·
|
||||
[METER_SCOPE_PARENT](troubleshooting.md#meter-scope-parent) ·
|
||||
[BUDGET_READING_REJECTED](troubleshooting.md#budget-reading-rejected) ·
|
||||
[BUDGET_METER_UNAVAILABLE](troubleshooting.md#budget-meter-unavailable) ·
|
||||
[BUDGET_LEDGER_INVALID](troubleshooting.md#budget-ledger-invalid) ·
|
||||
|
||||
+36
-9
@@ -563,7 +563,7 @@ of required checks before it merges, one slot after another, so a wave of
|
||||
three saves two of those rounds and two versions.
|
||||
|
||||
```bash
|
||||
gstack-ship-queue wave plan # read-only: QUEUE, WAVE_ADVISED, WAVE_GROUP lines
|
||||
gstack-ship-queue wave plan # read-only (merge-tree scratch objects aside): QUEUE, WAVE_ADVISED, WAVE_GROUP lines
|
||||
gstack-ship-queue wave build --area ship --prs 41@<sha>,42@<sha>,43@<sha>
|
||||
gstack-ship-queue wave build --wave <nonce> --continue # after resolving a conflict in the printed worktree
|
||||
gstack-ship-queue wave build --wave <nonce> --drop 42 # rebuild without #42 as a new wave
|
||||
@@ -573,9 +573,19 @@ gstack-ship-queue wave settle --wave <nonce> [--dry-run] # after the wave
|
||||
```
|
||||
|
||||
- **When.** `wave plan` counts the live READY PRs (open, head equal to their
|
||||
READY receipt). Over the trigger (6) it prints `WAVE_ADVISED` and suggested
|
||||
groups of 3 to 6: PRs that change a common file always share a group (their
|
||||
conflicts are resolved once), each group takes its largest PR's functional
|
||||
READY receipt, CI green at that head; a READY head whose CI is red, still
|
||||
running or absent prints a `NOT_GREEN` line and stays out of the plan). A
|
||||
READY PR whose CI, head or diff cannot be read fails the whole plan with
|
||||
`QUEUE_PROOF_MISSING`. Over the
|
||||
trigger (6) it prints `WAVE_ADVISED` and suggested groups of 3 to 6: PRs
|
||||
whose changes to a common code file conflict (`git merge-tree` cannot merge
|
||||
the two heads, or the check itself fails) always share a group, so that
|
||||
conflict is resolved once. Hub
|
||||
files never link PRs: anything under `docs/`, Markdown (generated skill
|
||||
renders included; their `.tmpl` sources still link) and every `.json` or
|
||||
`.jsonl` file (ledgers and registries). Most PRs append to them, and
|
||||
linking through them once chained a 55-PR queue into a single area
|
||||
(GST-770). Each group takes its largest PR's functional
|
||||
area (a skill directory, the first word of a `lib/`, `bin/` or `test/`
|
||||
module name, or `docs`), smaller groups ride solo, and the integrator is
|
||||
the PR with the most changed lines. `train build` handed more than 6 pins
|
||||
@@ -1335,13 +1345,19 @@ falling back to the whitespace-stripping default, and a `git diff` or
|
||||
patch-ids differ, the refusal names up to `CARRY_NAMED_PATHS` (20) product
|
||||
paths whose patch against the base changed, walking only the paths the branch
|
||||
itself touched at either head (a base merge brings in every path the base
|
||||
changed, none of which can differ between the two base diffs). Product means every changed path that is not test code (`test/**`,
|
||||
changed, none of which can differ between the two base diffs), and of those
|
||||
only the paths that changed between the two heads or the two bases (a path with
|
||||
the same blob on both sides has the same patch). The product diff streams
|
||||
through a file under the git directory (`.git/gstack-patch-id`) into
|
||||
`git patch-id --verbatim`, so a PR with thousands of paths is not held in
|
||||
memory; a directory a killed run left there is removed after an hour. Product means every changed path that is not test code (`test/**`,
|
||||
`spec/**`, `e2e/**`, `**/*.test.<code ext>`, `**/*.spec.<code ext>` and the
|
||||
other `CARRY_TEST_GLOBS` in `lib/ship-receipt.ts`; an `openapi.spec.yaml` is
|
||||
product) or release metadata (`CHANGELOG.md`, `agents-digest/**`, each
|
||||
`<changelog_fragments.dir>/<slug>.md` fragment when the base policy sets
|
||||
`changelog_fragments` (not the directory's README.md or a nested path; a README
|
||||
change also makes the range's fragments product), and `VERSION`
|
||||
change the branch itself makes also makes the range's fragments product, while one
|
||||
a base merge brings in does not), and `VERSION`
|
||||
or `package.json` only while the change is a version stamp and nothing else: a
|
||||
dependency, a script or a new key makes them product). A path the policy's
|
||||
`stamp_files`, `stamp_paths` or `release_outputs` name is metadata only while
|
||||
@@ -1372,7 +1388,11 @@ along the receipt's own history, `rewritten` once any hop was a rebase;
|
||||
`CARRY: carried from <old> to <new> (<reason>)` and the `GSTACK_RESULT:
|
||||
skill=ship status=complete` line, and its summary line ends with
|
||||
`carried_from=<old12>`. `CARRY: current` means the receipt already names HEAD;
|
||||
`--dry-run` prints `CARRY: would carry …` and writes nothing.
|
||||
`--dry-run` prints `CARRY: would carry …` and writes nothing. When the only
|
||||
difference is base context next to the branch's unchanged hunks and none of
|
||||
carry's other checks would refuse, `--dry-run` refuses
|
||||
`RECEIPT_CARRY_SCOPED_REVIEW` without writing the packet; carry without
|
||||
`--dry-run` writes it.
|
||||
|
||||
A changed product path, or a product patch-id that differs, is
|
||||
[`RECEIPT_CARRY_PRODUCT_CHANGED`](troubleshooting.md#receipt-carry-product-changed)
|
||||
@@ -1405,7 +1425,12 @@ the carry when the branch has an open PR, in this order, and never reruns the
|
||||
review loop for a carried receipt:
|
||||
|
||||
1. `gstack-ship-receipt carry --pr <n> --base <base> --dry-run`. `CARRY: would
|
||||
carry` is eligibility only; a refusal continues the full ship.
|
||||
carry` is eligibility only; a refusal continues the full ship, except
|
||||
`RECEIPT_CARRY_SCOPED_REVIEW`, for which `gstack-ship-preflight` prints
|
||||
`NEXT: carry route`: fix what the base merge broke, commit and push, run carry
|
||||
without `--dry-run` to write the packet, have one fix-check reviewer read it,
|
||||
then carry with `--scoped-review <record.json>` and `--pregate`. A blocking
|
||||
finding continues the full ship.
|
||||
2. At that pinned HEAD: the CHANGELOG check, the size-cap check the policy
|
||||
declares, and `gstack-pregate` (the remote tests stage when
|
||||
`requires_remote` asks for it). A failing check, or HEAD moving, continues
|
||||
@@ -1695,7 +1720,9 @@ hand edit), and the replacement the current one, so a dependency pin
|
||||
that happens to equal the old version is `changed`; `stamp_patterns`
|
||||
templates count as stamp forms too). With `stamp_files` set, a file outside it
|
||||
is `changed (stamp_files: not in the repo's stamp_files allowlist)` whatever
|
||||
its diff. Any other byte is a change.
|
||||
its diff. Any other byte is a change. A diff git cannot produce between the two
|
||||
heads fails closed with `EVIDENCE_IDENTITY_UNKNOWN` and git's reason; it never
|
||||
reads as no change.
|
||||
|
||||
Equivalence is decided per gate. A lane in `stamp_sensitive_gates` always
|
||||
`rerun`s; a verified lane is `reusable` only when the tree is same-modulo-stamps
|
||||
|
||||
+127
-10
@@ -550,6 +550,25 @@ local `origin/<base>` at `<rev>`.
|
||||
**Fix.** Run `git fetch origin <base>` outside the sandbox and rerun /review if
|
||||
the base has moved.
|
||||
|
||||
<a id="review-content-changed"></a>
|
||||
### `review_freshness: STALE content changed during review: the tree moved from <start> to <end> while <skill> ran ...` / **Review records missing or mismatched**
|
||||
|
||||
**Meaning.** The working tree changed between the pass's `gstack-review-log --start` and
|
||||
its logged row: an edit, commit or merge landed while reviewers were still reading. The
|
||||
reviewers may not have seen that change, so the row's `review_binding.state` is `changed`.
|
||||
Step 11.5 cannot bind it and inserts `9 → 10 → 11 → 11.5`. Logging the same results again
|
||||
cannot repair it.
|
||||
|
||||
**Fix.** Keep the checkout frozen (no edits, commits, merges or pushes) from a pass's start
|
||||
until every reviewer in it has returned and its row is logged. If the mid-pass change was
|
||||
your own fix to that pass's findings, log the pass with `"converged":false` and the fix
|
||||
as `"action":"fixed"`. `gstack-review-plan` then picks the repeat: one fix-check reading
|
||||
the touched hunks when every fix is below the blocking floor, and 11.5 accepts that
|
||||
fix-check record as Step 9's. Rerun Step 11's native pass on the same frozen tree.
|
||||
|
||||
**Expected result.** `gstack-review-read` shows both records at one tree with
|
||||
`review_freshness` `CURRENT`, and Step 11.5 continues to Step 12.
|
||||
|
||||
<a id="review-fingerprint-tmpdir"></a>
|
||||
### `gstack-wtree: cannot create a temp index under <dir>; set TMPDIR to a writable directory` / `gstack-review-log: cannot create a private object directory under <dir>; no working-tree fingerprint. Set TMPDIR to a writable directory.`
|
||||
|
||||
@@ -1239,8 +1258,8 @@ git -C ~/.claude/skills/gstack update-ref -d 'refs/gstack/rescue/<oid>'
|
||||
<a id="setup-runtime-root-contains-checkout"></a>
|
||||
### `gstack setup: refusing to replace <root> for <host>: this runtime directory contains the source checkout setup runs from (<checkout>), so replacing it would delete the checkout. Nothing was changed.`
|
||||
|
||||
**Meaning.** Setup installs Codex, Factory, OpenCode, Cursor, Copilot and
|
||||
Antigravity by building a runtime root (such as `~/.cursor/skills/gstack`) and
|
||||
**Meaning.** Setup installs Codex, Factory, OpenCode, Cursor, Copilot,
|
||||
Antigravity and Gemini CLI by building a runtime root (such as `~/.cursor/skills/gstack`) and
|
||||
swapping it in for the old one, and Kiro by writing into `~/.kiro/skills/gstack`.
|
||||
Your gstack checkout lives at that root or inside it, so the swap would delete
|
||||
the checkout (#3084), and Kiro would link the checkout into itself. Setup refuses before
|
||||
@@ -1942,6 +1961,18 @@ owner's call: run the printed `RECOVER:` line; its resume needs
|
||||
**Fix.** In the execute program that runs the command, read
|
||||
`(await tools.thread.get({ thread_id: "<your own jam id>" })).cost.tokens` and pass it as `--reading <n>`.
|
||||
|
||||
<a id="meter-scope-parent"></a>
|
||||
### `gstack-ship-budget: ... (METER_SCOPE_PARENT)`
|
||||
|
||||
**Meaning.** `start` was told the run is a subagent (`--subagent` or `GSTACK_SHIP_SUBAGENT=1`) reading
|
||||
`"this"` (`--reading-from this`), the root thread's counter, which also holds every sibling lane, and
|
||||
asked for `--meter-scope self`. A self scope would print that counter as the run's own cost. Exit 2;
|
||||
no run started.
|
||||
|
||||
**Fix.** Start with `--meter-scope tree`. Every cost line then prints
|
||||
`METER: tree (root counter, upper bound)`. Exact per-run cost comes from a root replay thread
|
||||
(`docs/ship-budget.md`).
|
||||
|
||||
<a id="budget-reading-rejected"></a>
|
||||
### `gstack-ship-budget: ... (BUDGET_READING_REJECTED)`
|
||||
|
||||
@@ -2206,6 +2237,25 @@ anything.
|
||||
|
||||
---
|
||||
|
||||
<a id="setup-deps-migrate-failed"></a>
|
||||
### `gstack: WARN kept the full dependency install (SETUP_DEPS_MIGRATE_FAILED; ...): <cause>`
|
||||
|
||||
**Meaning.** Your install was made before v1.91.231.0, so it still carries the
|
||||
test and eval packages (about 520 MB, mostly the Claude Agent SDK). setup moved
|
||||
`node_modules` aside to install production dependencies only, that install
|
||||
failed, and setup put the full `node_modules` back. Nothing was removed, and
|
||||
gstack keeps working on the full install.
|
||||
|
||||
**Fix.** Fix the cause the warning names (no network, a proxy, a full disk),
|
||||
then run `./setup` again. Developing gstack in this checkout? Mark it, and
|
||||
setup keeps the dev packages from then on:
|
||||
|
||||
```bash
|
||||
git config gstack.contributor true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="chromium-install-exit-127"></a>
|
||||
### `Chromium bootstrap: chromium-install — <bun> x playwright install chromium exited 127 (command not found); install Bun from bun.sh (winget Bun ships no bunx), or skip with ./setup --no-browser (Git Bash) or .\setup.ps1 --no-browser (PowerShell)`
|
||||
|
||||
@@ -2896,6 +2946,34 @@ fetching it cannot create that ref, so pass a branch that exists on `origin`
|
||||
(`git branch -r --list 'origin/*'` lists them). `gstack-pregate` says so in
|
||||
its fix instead of suggesting the fetch.
|
||||
|
||||
<a id="history-read-failed"></a>
|
||||
### `cannot read <path> at <ref>. <cause> (git version <v>). Fix: see docs/troubleshooting.md#history-read-failed (HISTORY_READ_FAILED)`
|
||||
|
||||
**Meaning.** A CI job or a local release tool read a file at an older commit
|
||||
and git could not produce it. The PR and train path jobs (Free Tests' plan, the Version Gate,
|
||||
and E2E Evals' route, screen summary, planner, slices, reruns, bisect and
|
||||
report) check out blobless clones (`filter: blob:none`): every commit and
|
||||
tree is there, but an older file's contents are fetched from GitHub the first
|
||||
time something reads them. When that fetch fails three times (a GitHub outage,
|
||||
a network drop), or the ref does not resolve at all, the read fails here
|
||||
instead of counting as "the file is absent". A file the commit really does not
|
||||
have still reads as absent, as before. No ship policy at the base means no
|
||||
train, and no VERSION means `0.0.0.0`. The route and backstop policy reads (inline
|
||||
in their workflows), `.github/scripts/history-read.sh` (the Version Gate's base
|
||||
`VERSION`) and `lib/history-read.ts` (the Version Gate's train classification)
|
||||
print this line. The Version Gate prints the same code when
|
||||
`git fetch origin <base>` fails three times. `gstack-restamp` and
|
||||
`gstack-ship-queue land` print it too when they cannot read the base branch's
|
||||
CHANGELOG, which they check fragments against (`lib/changelog-fragments.ts`
|
||||
reads it through `lib/history-read.ts`); outside a partial clone they do not
|
||||
retry.
|
||||
|
||||
**Fix.** Rerun the failed job once GitHub is reachable. If it fails the same
|
||||
way on a healthy network, check that the ref exists on `origin` and that the
|
||||
runner's git supports partial clone (`git --version` 2.22 or newer; the eval
|
||||
image asserts it at build time in `.github/docker/Dockerfile.ci`). For a local
|
||||
tool, run `git fetch origin <base>` and rerun it.
|
||||
|
||||
<a id="policy-invalid"></a>
|
||||
### `POLICY_INVALID`
|
||||
|
||||
@@ -3081,9 +3159,14 @@ or pass `--force-gate <reason>` in `/ship` to run the full gate again.
|
||||
**Meaning.** The bundle (or ledger record) lacks a committed tree id, runtime
|
||||
versions, lockfile hashes or the command hash, so no reuse decision can rest on
|
||||
it. Imported assertions without verification count as unknown too.
|
||||
`gstack-tree-receipt` also fails with this code when git cannot produce the
|
||||
diff between the gated and current heads (a missing object, a timeout): the
|
||||
error names git's reason, and a failed diff is never read as no change.
|
||||
|
||||
**Fix.** Rebuild the bundle on the gate machine after the lanes ran on a
|
||||
committed head: `gstack-evidence bundle --out <file> --label <lane>...`.
|
||||
committed head: `gstack-evidence bundle --out <file> --label <lane>...`. For
|
||||
the tree receipt, fix what git's reason names (fetch a missing head with
|
||||
`git fetch origin <sha>`), then rerun.
|
||||
|
||||
<a id="evidence-bundle-invalid"></a>
|
||||
### `EVIDENCE_BUNDLE_INVALID`
|
||||
@@ -3550,9 +3633,12 @@ never runs them: declare `"**/integration_test/**": []` (and
|
||||
them. A `[]` declaration added only on the branch is refused under
|
||||
`--policy-from head` ([`PREGATE_HEAD_POLICY_LOOSENS`](#pregate-head-policy-loosens)),
|
||||
so land it in its own PR first.
|
||||
With no policy anywhere (no base copy, no user-owned file) an unmapped file is
|
||||
With no policy anywhere (no base copy, no user-owned file), or a policy with no
|
||||
`dependencies`, `empty_lanes`, `selection` or `local` key (a timeout-only file), an unmapped file is
|
||||
the warning [`PREGATE_NO_POLICY`](#pregate-no-policy) with a starter policy
|
||||
printed, not this error; `gstack-pregate init` writes that draft and lists the
|
||||
printed, not this error. Under `warn: ["lanes"]` it is a `warn` with this code,
|
||||
given only after the selected tests ran green (a red lane is still
|
||||
`PREGATE_LANE_FAILED`); `gstack-pregate init` writes that draft and lists the
|
||||
files it could not place. `--explain` prints every mapping and why. For a file with no lane it also prints the import
|
||||
specifiers in test files that resolved to no repo file (for example a
|
||||
`tsconfig.json` `paths` alias such as `@/lib/x` whose target is missing) and any
|
||||
@@ -3690,8 +3776,9 @@ differing path is a change.
|
||||
### `PREGATE_NO_POLICY`
|
||||
|
||||
**Meaning.** The repository has no pregate policy yet (no `.gstack/pregate.json`
|
||||
on the base branch and no user-owned `<state root>/projects/<slug>/pregate.json`)
|
||||
and a touched file maps to no test lane. Under no policy that is a `warn`, not
|
||||
on the base branch and no user-owned `<state root>/projects/<slug>/pregate.json`),
|
||||
or its policy declares no lanes (no `dependencies`, `empty_lanes`, `selection` or `local` key, a
|
||||
timeout-only file for example), and a touched file maps to no test lane. Under no policy that is a `warn`, not
|
||||
`PREGATE_NO_LANE`: the selected tests still ran (a red lane is still `fail`),
|
||||
and the table printed a starter policy drafted from the repository
|
||||
(`PREGATE_STARTER:` lines: runner, test directories, CI lanes, the JSON between
|
||||
@@ -3702,7 +3789,8 @@ Details: [docs/pregate.md](pregate.md#no-policy).
|
||||
overwrite an existing file without `--force`), edit each `TODO` (a test that
|
||||
names the file, a `dependencies` entry, or `[]` for a file with no behaviour),
|
||||
run `gstack-pregate validate`, then commit `.gstack/pregate.json` on the base
|
||||
branch. From then on an unmapped file is `PREGATE_NO_LANE`.
|
||||
branch. When a policy already exists, add the starter's `dependencies` to it
|
||||
instead. From then on an unmapped file is `PREGATE_NO_LANE`.
|
||||
|
||||
<a id="pregate-environment"></a>
|
||||
### `PREGATE_ENVIRONMENT`
|
||||
@@ -5502,7 +5590,9 @@ compare (a receipt written before `head_tree`, whose head is not in this
|
||||
clone), the PR was retargeted to the base after its stack parent landed but
|
||||
the receipt records no `product_patch_id` and `origin/<parent>` no longer
|
||||
resolves, so the reviewed range cannot be recomputed, or git could not
|
||||
compare the two product diffs or read the working tree's status. It also refuses when an owner exception the receipt records
|
||||
compare the two product diffs (including when carry cannot create its temp
|
||||
directory under `.git/gstack-patch-id`; the message names it) or read the
|
||||
working tree's status. It also refuses when an owner exception the receipt records
|
||||
no longer stands, and, for a receipt from a gstack that records review
|
||||
depth, when HEAD needs a deeper review than the receipt records, `--head` is
|
||||
not the checked-out HEAD, or the receipt records `gstack_version` without
|
||||
@@ -5595,6 +5685,10 @@ the tree it is bound to. Finish also stops this way for test code changed since
|
||||
review with no gate edit (`tests=N`): the packet holds those test hunks, and the
|
||||
reviewer checks that each test still tests what it did. A packet that cannot be
|
||||
written (an unwritable `.git`) is a STOP or `RECEIPT_CARRY_REFUSED` naming the path.
|
||||
`carry --dry-run` reports this code without writing the packet, and only when
|
||||
none of carry's other checks would refuse; carry without `--dry-run` writes it.
|
||||
In /ship Step 1, `gstack-ship-preflight` prints the route as `NEXT: carry
|
||||
route (Step 1.4)`.
|
||||
|
||||
<a id="receipt-prose-mismatch"></a>
|
||||
### `RECEIPT_PROSE_MISMATCH`
|
||||
@@ -5650,7 +5744,10 @@ over` line names (or raise its cap on the base branch). When the owner accepts
|
||||
the named risk instead, record it as `docs.exception: { accepted_by, date, via,
|
||||
reason }` and carry the `Documentation exception: accepted by <owner> <date> via
|
||||
<ref>: <reason>` line in the PR body's `## Documentation` section, word for word
|
||||
([ship-policy.md](ship-policy.md#ship-receipt)).
|
||||
([ship-policy.md](ship-policy.md#ship-receipt)). Then save the post-audit snapshot with
|
||||
`gstack-docs-candidate snapshot --exception .gstack/tmp/docs-audit.json`;
|
||||
`gstack-ship-release finish` stops on a blocked audit whose snapshot lacks the
|
||||
exception.
|
||||
<a id="gstack-pr-wait"></a>
|
||||
## gstack-pr-wait
|
||||
|
||||
@@ -6008,6 +6105,26 @@ The router's description (at most 300 bytes) and the context block (at most 600
|
||||
|
||||
There is no ledger for that harness or id under `$GSTACK_STATE_ROOT/harness`. `gstack-harness status` lists every registration on this machine with its id.
|
||||
|
||||
<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.
|
||||
|
||||
<a id="harness-hook-choice-invalid"></a>
|
||||
### `gstack-harness: GSTACK_HARNESS_START_HOOK="<value>" is not on or off` (HARNESS_HOOK_CHOICE_INVALID)
|
||||
|
||||
The start hook switch takes `on` or `off` (`GSTACK_HARNESS_START_HOOK` also takes `1` or `0`). Nothing was registered. Fix the value, or unset it so each registration keeps its saved choice.
|
||||
|
||||
<a id="harness-hook-owned-elsewhere"></a>
|
||||
### `gstack-harness: a gstack-start plugin already exists at <dir> and this registration did not write it` (HARNESS_HOOK_OWNED_ELSEWHERE)
|
||||
|
||||
Each harness profile has one start hook, owned by the gstack registration that wrote it, so that removing one install can never take another install's hook away. This folder is not in this registration's ledger: another gstack checkout or state root wrote it, or someone made it by hand. Nothing was written. Run `gstack-harness register <harness> --no-start-hook` from the gstack install that wrote it (its `gstack-harness status` shows the hook), or move the folder aside, then register again.
|
||||
|
||||
<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.
|
||||
|
||||
<a id="harness-qm-credentials"></a>
|
||||
### `gstack-harness: no QM admin credential in the environment` / `... needs --yes` (HARNESS_QM_CREDENTIALS)
|
||||
|
||||
|
||||
+3
-2
@@ -242,8 +242,9 @@ now names HEAD with a `carry` object holding the `git range-diff` evidence, and
|
||||
counters are kept, because the reviewed product diff is unchanged or its only
|
||||
difference passed a scoped review. `CARRY: current` means nothing moved.
|
||||
`CARRY: refused (RECEIPT_CARRY_SCOPED_REVIEW)` means the base edited context
|
||||
next to reviewed hunks: one fix-check reviewer reads the packet the refusal
|
||||
names, and a clean record reruns carry with `--scoped-review <record.json>`
|
||||
next to reviewed hunks. Carry without `--dry-run` writes the packet the
|
||||
refusal names (a dry run writes none), one fix-check reviewer reads it, and a
|
||||
clean record reruns carry with `--scoped-review <record.json>`
|
||||
([troubleshooting](troubleshooting.md#receipt-carry-scoped-review)). `CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)`,
|
||||
`(RECEIPT_CARRY_TREE_MISMATCH)` or `(RECEIPT_CARRY_REFUSED)` with the refused
|
||||
result line means the product diff changed, or history was rewritten onto a
|
||||
|
||||
+9
-1
@@ -8,7 +8,7 @@ If gstack is installed on your machine:
|
||||
~/.claude/skills/gstack/bin/gstack-uninstall
|
||||
```
|
||||
|
||||
This handles skills, symlinks, harness registrations (the files `gstack-harness` wrote into OpenClaw, Hermes, a Dot or another harness, from their ledgers), global state (`~/.gstack/`), project-local state, browse daemons, and temp files. Use `--keep-state` to preserve config and analytics. Use `--force` to skip confirmation.
|
||||
This handles skills, symlinks, harness registrations (the files `gstack-harness` wrote into OpenClaw, Hermes, a Dot or another harness, from their ledgers, plus the opt-in [start hook](hosts.md#start-hook), which it turns off through the harness's own CLI), global state (`~/.gstack/`), project-local state, browse daemons, and temp files. Use `--keep-state` to preserve config and analytics. Use `--force` to skip confirmation.
|
||||
|
||||
## Option 2: Manual removal (no local repo)
|
||||
|
||||
@@ -54,6 +54,7 @@ rm -rf "${HERMES_HOME:-$HOME/.hermes}/skills/gstack" 2>/dev/null
|
||||
rm -rf ~/.cursor/skills/gstack* 2>/dev/null
|
||||
rm -rf ~/.config/opencode/skills/gstack* 2>/dev/null
|
||||
rm -rf ~/.gemini/antigravity-cli/skills/gstack* 2>/dev/null
|
||||
rm -rf ~/.gemini/skills/gstack* 2>/dev/null
|
||||
|
||||
# 6. Remove temp files
|
||||
rm -f /tmp/gstack-* 2>/dev/null
|
||||
@@ -71,6 +72,13 @@ hook, the AskUserQuestion PreToolUse/PostToolUse hooks, and the Stop hooks
|
||||
(session timeline, plus verify-gate if you opted in). Left in place, they error
|
||||
on every matching event once the install directory is gone.
|
||||
|
||||
If you turned on the OpenClaw or Hermes [start hook](hosts.md#start-hook),
|
||||
manual removal also leaves its `gstack-start` plugin turned on. Turn it off
|
||||
with `openclaw config unset plugins.entries.gstack-start` or `hermes plugins
|
||||
disable gstack-start`, then delete
|
||||
`${OPENCLAW_STATE_DIR:-~/.openclaw}/extensions/gstack-start` or
|
||||
`${HERMES_HOME:-~/.hermes}/plugins/gstack-start`.
|
||||
|
||||
## Clean up CLAUDE.md
|
||||
|
||||
The uninstall script does not edit CLAUDE.md. In each project where gstack was added, remove the `## gstack` and `## Skill routing` sections.
|
||||
|
||||
Reference in new issue
Block a user