Merge remote-tracking branch 'origin/main' into stamp3647-local

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

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
garrytan committed 2026-10-30 16:55:51 +00:00
commit d9eb63afec
43 files changed
+2274 -50

No files matched your search

+6
View File
@@ -188,6 +188,12 @@ with OpenClaw's tool names and pointing at the installed gstack, so the agent ru
workflows and helpers itself. Use ACP when the work should happen in a Claude
Code session with the full install (hooks, Claude tool names); use native
skills when the OpenClaw agent should do it. Both can be installed together.
OpenClaw serves a registered skill's `SKILL.md` whole or refuses it, and its
limit is 32,768 bytes when it does not know the model's context window
(32,000 characters with a 100K-200K-token one), so a skill over 32,000 bytes (review, investigate, ship and plan-eng-review among the five) is
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).
The ACP artifacts in `openclaw/` are generated by `bun run gen:skill-docs --host openclaw`.
+1 -1
View File
@@ -59,7 +59,7 @@ characters, the part of a description Hermes's skills index shows.
| Harness | What gstack registers | Install | Verify | Remove |
|---|---|---|---|---|
| OpenClaw | default `--skills hybrid`: `gstack-review`, `gstack-investigate`, `gstack-qa`, `gstack-ship` and `gstack-plan-eng-review` as top-level skills plus one `gstack` router skill whose body lists every other skill and its path, in `${OPENCLAW_STATE_DIR:-~/.openclaw}/skills`. With OpenClaw 2026.10.6's 19 eligible bundled skills, hybrid's skills section is 10,290 characters, full format, under its 18,000-character budget; `--skills all` (17,971, compact format, descriptions cut) lists every skill as its own folder, `--skills router` (6,701) only the router | `./setup --host openclaw`, or `./setup` inside OpenClaw | `openclaw skills list --json` shows `gstack` and the five task skills (source `openclaw-managed`, eligible) | `gstack-harness unregister openclaw` |
| OpenClaw | default `--skills hybrid`: `gstack-review`, `gstack-investigate`, `gstack-qa`, `gstack-ship` and `gstack-plan-eng-review` as top-level skills plus one `gstack` router skill whose body lists every other skill and its path, in `${OPENCLAW_STATE_DIR:-~/.openclaw}/skills`. With OpenClaw 2026.10.6's 19 eligible bundled skills, hybrid's skills section is 10,290 characters, full format, under its 18,000-character budget; `--skills all` (17,971, compact format, descriptions cut) lists every skill as its own folder, `--skills router` (6,701) only the router. OpenClaw serves a registered skill's `SKILL.md` whole or refuses it ("Skill instructions cannot be partially served"), at most 32,768 bytes when its model catalog has no context window for the model and 32,000 characters with a 100K-200K-token window, so any registered skill over 32,000 bytes (four of the five in hybrid, 26 of 63 in `all` today) is a short entry `SKILL.md` with the same name and description that sends the agent to its full `SKILL.md` in a plain copy of every skill (the router's `gstack/skills/`, or `.gstack-skills/` under `--skills all`, a folder OpenClaw's skill scan skips), which OpenClaw reads in pages. A context window under 100K tokens lowers that budget to 16,000 characters, which this limit does not cover | `./setup --host openclaw`, or `./setup` inside OpenClaw | `openclaw skills list --json` shows `gstack` and the five task skills (source `openclaw-managed`, eligible) | `gstack-harness unregister openclaw` |
| Hermes | every skill, as `gstack-<skill>` in one `gstack` category, in `${HERMES_HOME:-~/.hermes}/skills/gstack`. Hermes's skills index lists only skills registered as folders, so a router would leave the rest out of the listing (`skill_view` still loads an unlisted skill by name, but nothing tells the model it exists) | `./setup --host hermes`, or `./setup` inside Hermes | `hermes skills list` shows the `gstack-*` skills in category `gstack` | `gstack-harness unregister hermes` |
| dots (feder-cr) | the `gstack` router in `/home/dot/skills/gstack`, which the Dot lists every turn | `./setup` on the Dot's computer, or `gstack-harness register dots` | the Dot's Skills page, or `GET /skills` | `gstack-harness unregister dots` |
| QM | **not available yet.** QM imports a skill pack by cloning the whole repository and caps it at 32 MiB and 5,000 files; gstack's repository is about 73 MiB, so `gstack-harness register qm --yes` stops before sending anything ([why](troubleshooting.md#harness-qm-pack-too-large)) | meanwhile, paste `gstack-harness register generic --print` into the agent's instructions | ask it for a gstack skill | delete the pasted text |
+7
View File
@@ -163,6 +163,13 @@ good, edit or delete that line; `gstack-config unset agent_model` clears this
machine only. Copying a newer `capy/skill/gstack/` over the skill folder leaves
`preferences.yaml` alone, because the shipped folder has no such file.
A Capy session with no gstack checkout (a Captain or coordinator that runs
`install.sh --check --for none`) can't run the resolver, so the skill tells it
to read the `agent_model` line itself. There a family means the newest model of
that family the host lists, while the resolver maps each family to the id in
its table (today they agree: `opus` is `anthropic/claude-opus-5-5`). Anything
it can't use means the session keeps its own model and says so.
## Reasoning effort
Codex calls carry a `model_reasoning_effort`. It follows the same shape as the
+5 -2
View File
@@ -456,7 +456,8 @@ receipt for H when the two trees differ only as a stamp does:
stamps, each hunk in them must be the exact old → new rewrite under
`stamp_patterns` (literal `{version}` templates) or the built-in forms
(`(was X)`, `since: X`, `vX`), and the version file, `package.json`'s
`version`, a lockfile's root version keys and a CHANGELOG re-heading have
`version`, a lockfile's root version keys and a CHANGELOG re-heading (under
`changelog_fragments`, also the fold of the fragments the head deletes) have
their own built-in rules. There is no second list and no looser rule here:
the file verdicts `clear` reaches on a pair are the verdicts
`gstack-tree-receipt --gated G --current H` reaches, and its gate rule holds
@@ -475,7 +476,9 @@ receipt for H when the two trees differ only as a stamp does:
the policy label (`origin/<base>@<sha>:.gstack/ship-policy.json`) and every
differing path with the rule that classed it (`version-file`,
`version-manifest`, `manifest-version`, `lockfile-root-version`,
`changelog-heading`, `stamp_files`, `stamp_paths`, `release_outputs`). `clear` prints the same
`changelog-heading`, `changelog-fragments` for the CHANGELOG under the fold,
`changelog_fragments` for a folded fragment's deletion, `stamp_files`,
`stamp_paths`, `release_outputs`). `clear` prints the same
as `PREGATE_RECEIPT_SUBSTITUTED: …`, `verify` repeats it on its ok line, and
the ship receipt carries the receipt string. The receipt's own tests and
failed checks apply as before (its exit status is checked only when
+157 -2
View File
@@ -61,6 +61,7 @@ refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
| `min_gstack_version` | version or `null` | `null` | The oldest gstack that may read the file; see the compatibility rule above. |
| `docs` | `{ caps: [{ path, max_bytes }] }` or `null` | `null` | Documentation files with a byte cap. `/ship` Step 14.5 runs `gstack-ship-receipt docs-caps --base <base>` (the caps come from `origin/<base>`'s policy, never the branch's copy; `--policy <file>` is for a repository bootstrapping its policy) before the verification gate and prints one `DOCS_CAP: <path> <bytes>/<max> ok\|over` line per cap; any `over` (an absent file counts) exits 1 and blocks the ship until the file is trimmed or the cap raised on the base branch (`DOCS_AUDIT_BLOCKED`). At most 64 caps, each path contained. See [the ship receipt](#ship-receipt). Needs `min_gstack_version`. |
| `train` | `{ gate_workflow, free_workflow, full_gate_check, record_artifact, max_size, fill_hours, screen_selection, trial_reservation_usd }` or `null` | `null` | Turns on [merge trains](#run-a-merge-train), read from the base revision only. Every field is required and an unknown field is `POLICY_INVALID`: `max_size` 1 to 6, `fill_hours` above 0 and at most 24, `screen_selection` `gstack-paid` \| `none`, `trial_reservation_usd` above 0 and at most 100. `null` keeps the per-PR queue exactly as it is. Needs `min_gstack_version`. |
| `changelog_fragments` | `{ dir }` or `null` | `null` | PRs carry unversioned [CHANGELOG fragments](#changelog-fragments) in `dir` (one flat, repo-relative directory, no `..`) and the merge step ([`land`](#land)) assigns the version. `null` keeps `[Unreleased]` sections and per-PR stamps. Needs `min_gstack_version`. |
Containment, checked by `validate` and again at write time: paths and globs are
repo-relative with no `..` segment, under 256 characters, at most 64 per list,
@@ -148,7 +149,8 @@ ship receipt into the coordinator protocol, so nobody proves a stamp diff by
hand. Lanes own their PRs and never merge; the coordinator owns the order and
every merge. Each step ends in a line the other side acts on, then
`GSTACK_RESULT: skill=ship-queue status=complete|refused run=<path>`. Nothing
here pushes.
here pushes, except `ready` under `changelog_fragments` (its READY receipt goes
to `refs/notes/gstack-trains`) and [`land`](#land).
**1. Lane: report ready (unstamped).** After the gate and pregate pass on the
pushed head:
@@ -169,8 +171,12 @@ ci: green at <head12> (passed=31 failed=0 pending=0 skipped=2)
PREGATE: pass tree=<tree12> head=<head12> checks=6
gate: command="<gate_command>" full_e2e=no gate-ahead=none
SHIP_RECEIPT: pr=42 head=<head12> …
READY_RECEIPT: <state root>/ship-queue/ready/<repo key>/42-<head>.json (notes pushed to origin refs/notes/gstack-trains)
```
Under `changelog_fragments`, a `READY_RECEIPT:` line follows, written only when
every check above passed; `land` refuses a pin without one.
CI's Version Gate (`scripts/compare-pr-version.ts`) accepts that head as
unstamped when `VERSION` and `package.json`'s version match its merge base, the
CHANGELOG change is confined to `[Unreleased]`, and any changed release output
@@ -788,6 +794,7 @@ which carry the workflow consequences the skill prose would otherwise spell out:
| `ship: preregistration: …` | `gstack-evidence ancestor --from-policy --base <base>` must print only `ANCESTOR: ok` before push |
| `ship: release_tool: …` | the command runs only inside `gstack-restamp` (staging worktree, outputs confined to `release_outputs`) |
| `ship: review_scope …` | Step 9.4 item 0 runs `gstack-review-scope classify` on each pass's findings; see [review_scope](#review-scope) |
| `ship: changelog_fragments <dir>: …` | Step 12 writes no version; Step 13 writes the entry as `<dir>/<branch-slug>.md` (entry text only, `bump:` front matter only when the change warrants more than the policy bump), never an `[Unreleased]` section, linted with `gstack-prose-lint <file> --gate`; `gstack-ship-queue land` assigns the version; see [changelog fragments](#changelog-fragments) |
The PR body's first section is the ship receipt (`gstack-ship-receipt write … --body`),
written by the pr-body section of `/ship` after the body is composed.
@@ -1168,6 +1175,153 @@ Read from the base branch and run only with `--allow-repo-commands`, like
of its own choosing cannot run here; give it a mode that takes the version and
writes only stamps.
The command also gets `GSTACK_RESTAMP_BASE_SHA`, the release predecessor: the
base branch commit the version is allocated after (origin/<base> for a per-PR
restamp and for `land`). A repo counter that must be predecessor + 1 (a
SAVE_VERSION, a rules number) reads its old value from that commit
(`git show "$GSTACK_RESTAMP_BASE_SHA:<file>"`), never from the branch, so two PRs
cut from one base never both claim the same number. Regenerated goldens go in
`release_outputs`.
<a id="changelog-fragments"></a>
## `changelog_fragments`: entries without versions, versioned at merge
With `changelog_fragments: { "dir": "changelog.d" }` on the base branch, a PR
never edits VERSION, the manifest `version` key or a released CHANGELOG
section. It adds one or more fragments, and the merge step assigns the
version, so two PRs cut from one base cannot collide and no lane redoes a
stamp after another PR merges.
A fragment is `<dir>/<slug>.md`, one regular file directly in `dir`
(`README.md` there is skipped). Its body is the entry text, exactly what the
CHANGELOG section will hold, with no `## [x]` heading. It may open with front
matter that raises the bump:
```markdown
---
bump: minor
---
**What changed, in one sentence.**
### Fixed
- ...
```
The effective bump of a stamp is the highest of the policy bump and every
folded fragment's `bump:` (`micro` < `patch` < `minor` < `major`). Fragments
are read as Git blobs at the commit being stamped (`git ls-tree` and
`git cat-file`, never the working tree), so a symlink, a gitlink, a nested
path, a non-`.md` file, an empty fragment, one over 64 KiB, one carrying a
release heading, or one whose name already landed on the base with other text
is `CHANGELOG_FRAGMENT_INVALID`, and nothing is allocated. Only fragments the
branch adds against the base count.
**/ship.** `show-effective` prints a `ship: changelog_fragments` line, so /ship
writes no version in Step 12 and writes Step 13's entry as
`<dir>/<branch-slug>.md`, linted with `gstack-prose-lint <file> --gate`, never an
`[Unreleased]` section.
**The fold.** `gstack-restamp` (and so `land`) concatenates the fragment
bodies in path order above any legacy `[Unreleased]` section, re-heads both as
one `## [<version>] - <date>` section at the top, and deletes the fragment
files, in one stamp. The tree receipt, and so `gstack-pregate clear`'s
stamp-only substitution, calls a deleted fragment a stamp (rule
`changelog_fragments`) only when the new CHANGELOG section is exactly the
deleted fragments' bodies in path order above any legacy `[Unreleased]` text
(rule `changelog-fragments`); an edited fold, or a fragment deleted without
its text, is a change.
**Version Gate.** The workflow runs on every PR (no path filter), so a PR
that touches no release file still has to prove its entry. Under the key, an
unstamped head must keep VERSION, the manifest `version` key and every
released CHANGELOG section at its merge base's values, and add at least one
valid fragment (a legacy `[Unreleased]`-only entry passes during migration;
a `[bot]` author, such as Dependabot, needs none). Other manifest edits, a
dependency or a script, pass. A head made by the merge step, a commit whose
parents are [pin, base], must carry the base's version plus the effective
bump of the fragments its pin adds. Under a train policy `gstack-ship-queue
ready` applies the same rule itself; without one, `ready` requires the head's
checks green, Version Gate included.
<a id="land"></a>
## `gstack-ship-queue land`: the per-PR merge step
`land --pr <n>` merges one READY PR whose head is unstamped. It replaces the
lane's stamp round trip. Nobody stamps by hand; the tool makes and pushes the
stamp commit. Steps, each journaled in
`.gstack/tmp/ship-queue/land/<n>.json` and resumable by rerunning the same
command:
1. **Refuse** a fork head, a closed PR, a PR whose base is another branch, a
base policy with `changelog_fragments` off (`QUEUE_NOT_READY`), a pin
without a READY receipt (`gstack-ship-queue ready --pr <n>` records one
on `refs/notes/gstack-trains` once its pregate, full_e2e and receipt checks
pass; land checks nothing weaker), a /ship receipt that is missing or
bound to another head (`QUEUE_RECEIPT_MISSING`, `QUEUE_RECEIPT_STALE`), and
required checks that are not green at the pin.
2. **Lock.** Take the repository land lock, `refs/notes/gstack-land` (its own
ref, never the trains notes ref), by exact lease. It records the PR, holder,
phase and journal. A held lock is `QUEUE_LAND_LOCKED` naming the holder;
`--break-lock` takes over a crashed holder's lock and is refused while that
holder entered a push or a merge less than 10 minutes ago, and for a drift
lock. A refusal before the push releases the lock.
3. **Merge and stamp.** In a scratch worktree, merge the base into the pin.
Conflicts in release fields only (the version file and `release_outputs`
take the base's side, the CHANGELOG merges by section, a manifest or npm
lockfile is re-merged with every side's release version set to the base's,
so a clean dependency edit survives) are resolved; anything else is
`QUEUE_LAND_CONFLICT`, nothing pushed, back to the lane. A legacy pin that
bumped VERSION itself has its release fields put back to its merge base
first and its entry re-headed `[Unreleased]` (a manifest version source
keeps its other edits; a symlinked release file refuses). A PR with no entry gets a
fragment written from its title. Then `gstack-restamp --next` folds and
allocates (`restamp_command` and `release_tool` need
`--allow-repo-commands`, as everywhere).
4. **Push.** `commit-tree` the stamped tree with parents [pin, base] and push
it to the PR branch under `--force-with-lease=<branch>:<pin>`. The journal
keeps the reviewed pin apart from the last pushed head, so a rerun leases
on what it pushed and never on a head someone else pushed
(`QUEUE_TRAIN_MOVED`, never overwritten).
5. **Wait** for the PR's checks on the stamp commit, polling every 30 seconds
with the same check read the pin passed (`--timeout <min>`, default 60). `--no-wait`
stops after the push and prints `gstack-pr-wait <n> --until green &&
gstack-ship-queue land --pr <n>` to run instead.
CI runs for real on the stamp commit in this mode.
6. **Merge.** Re-read the base first: if it moved, restamp from the new base
and push again under the last-pushed lease, at most twice
(`QUEUE_LAND_BASE_BUSY` on a third move, which releases the lock). Then
`gh pr merge --squash --match-head-commit <stamp>` (`--merge` under
`history: merge-only`) and confirm the base's
tree is the stamped tree on the base land stamped. The stamp date is taken
once and reused by every restamp. A rerun after GitHub merged the PR
(an error reply, a crash) goes straight to that tree check.
7. **Drift.** GitHub's merge API cannot bind the base, so a merge made outside
`land` can slip in between the check and the squash. The post-merge check
then fails with `QUEUE_LAND_DRIFT` and keeps the lock.
`land --pr <n> --repair` opens a PR from the base whose only change is a
fragment for what the outside merge brought (its unfolded fragments, moved,
or one entry naming its commits) and records it on the lock; /ship and land
that PR, from any checkout, and its land takes over the drift lock and
releases it. A rerun of `--repair` finds the PR it already opened.
`--dry-run` merges and stamps in a scratch worktree and prints the version and
tree, taking no lock and pushing nothing.
<a id="migrating-to-fragments"></a>
### Migrating to fragments
Before turning the key on, run `gstack-ship-queue land --census` (read-only).
It prints one line per open PR, saying whether land would fold its fragments, re-head a legacy
`[Unreleased]`, rewrite a tentative per-PR VERSION bump, resolve a
release-field-only conflict, write an entry from the title, or refuse on a
product conflict. Then set `changelog_fragments` and `min_gstack_version` in
one policy PR; a claim-at-open repository switches `queue_mode` to
`stamp-at-merge` in the same PR. Open PRs need no change: `land` handles each
census case. Rollback is removing the key. With it off, `gstack-restamp`
neither folds nor deletes fragment files, so first land every open PR that
carries a fragment (`land --census` lists them) or move each fragment's text
under `[Unreleased]` in its PR; otherwise that entry never reaches CHANGELOG.md.
<a id="tree-receipt"></a>
## The tree receipt
@@ -1191,7 +1345,8 @@ Hunks are classified per file type: the version file (old → new only), the
manifest (`version` key only), npm and bun lockfiles (root `version` keys
only, where `gstack-version-bump` writes; a dependency entry is `changed`),
the CHANGELOG (a heading move or re-heading, compared as an order-independent
multiset of entries), `stamp_paths` and `release_outputs` (every
multiset of entries; under `changelog_fragments`, also exactly the deleted
fragments folded under the new heading, and those deletions are stamps), `stamp_paths` and `release_outputs` (every
removed/added pair must be an exact `(was X)` / `since: X` / `vX` rewrite (a quote around X in the first two forms included) with
X the gated version, or a newer version of the repo's width that the
stamp_paths stage moves (a tentative `*_KEY_VERSION` written as the next
+59
View File
@@ -4347,6 +4347,65 @@ reason and dispatches nothing.
**Fix.** Run the printed `train build --prs <list without it>`; the fork PR
stays on the trusted-branch path.
<a id="changelog-fragment-invalid"></a>
### `CHANGELOG_FRAGMENT_INVALID`
**Meaning.** A CHANGELOG fragment under the policy's `changelog_fragments`
directory is not one regular `.md` file directly in that directory with entry
text, optional `bump:` front matter and no release heading, or it is over
64 KiB, or a fragment of that name already landed with other text. Nothing was
allocated or written. See [changelog fragments](ship-policy.md#changelog-fragments).
**Fix.** Rewrite the named fragment as a flat `<dir>/<slug>.md` holding only
the entry text (and optional `bump: minor|major` front matter); rename it if
the name already landed. Then rerun.
<a id="queue-land-locked"></a>
### `QUEUE_LAND_LOCKED`
**Meaning.** Another `gstack-ship-queue land` holds the repository land lock
(`refs/notes/gstack-land`), or took it from under this run, so this one would
race it for the next version. The line names the holder, its PR and phase.
**Fix.** Wait for the holder to finish. If it crashed, rerun with
`--break-lock`; that is refused while the holder entered a push or a merge
less than 10 minutes ago. A lock in phase `drift` is released by landing its
repair PR ([QUEUE_LAND_DRIFT](#queue-land-drift)).
<a id="queue-land-conflict"></a>
### `QUEUE_LAND_CONFLICT`
**Meaning.** The PR conflicts with the base branch in a product file (or a
release field that cannot be resolved alone, such as a real dependency
conflict in a manifest), so the merge step cannot stamp it. Nothing was
pushed.
**Fix.** The lane merges the base into the PR, resolves the named files, runs
/ship and reports READY again.
<a id="queue-land-drift"></a>
### `QUEUE_LAND_DRIFT`
**Meaning.** After the squash merge, the base branch's tree is not the
stamped tree on the base `land` stamped: a merge made outside `land` slipped
in between the last check and the merge. The lock stays held, in phase
`drift`, so no other land allocates on a release record that is now wrong.
**Fix.** Run `gstack-ship-queue land --pr <n> --repair`. It opens a PR whose
only change is a fragment for what the outside merge brought. Run /ship on it
and land it; that land takes over the lock and releases it. Then find what
merged outside `land`.
<a id="queue-land-base-busy"></a>
### `QUEUE_LAND_BASE_BUSY`
**Meaning.** The base branch moved during the stamp commit's CI on each of
the two allowed restamps, so `land` stopped instead of restamping again.
**Fix.** Find what is merging outside `land` (every merge should go through
it), then rerun `gstack-ship-queue land --pr <n>`; the lock was released and
the rerun restamps from the pin.
<a id="queue-train-moved"></a>
### `QUEUE_TRAIN_MOVED`
+1 -1
View File
@@ -49,7 +49,7 @@ rm -rf ~/.gstack
rm -rf "${CODEX_HOME:-$HOME/.codex}/skills/gstack"* 2>/dev/null
rm -rf ~/.factory/skills/gstack* 2>/dev/null
rm -rf ~/.kiro/skills/gstack* 2>/dev/null
rm -rf "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/skills/gstack"* 2>/dev/null
rm -rf "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/skills/gstack"* "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/skills/.gstack-skills" 2>/dev/null
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