Merge origin/main and stamp v1.91.316.0 (train 46 slot 3, #3593)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-29 10:13:28 +00:00
commit 99730eaf68
48 files changed
+1749 -174

No files matched your search

+8 -5
View File
@@ -138,8 +138,9 @@ this rule shipped cites this section by name.
**What the lane sends.** The plan and its gate in plain language a person outside the
project can follow: what changes, why now, what the change does, what it costs, the lane's
pick, and the recommended one-word reply. `gstack-owner-brief render --captain --out <run dir>`
prints that from the plan's `## Before → after`, `## Why it matters`,
`## What caused this work` and `## Big decisions` sections, with the cost lines
prints that from the plan's `## ELI5`, `## Before → after`, `## Why it matters`,
`## What caused this work` and `## Big decisions` sections (also saved as `eli5` in
`gate.json`, so `gstack-gate render` opens with them), with the cost lines
(`GATE_SPEND:`, `GATE_REC_COST:`) and a last line for the controller; a plan missing a
section exits `CAPTAIN_BRIEF_INCOMPLETE`, so the lane adds the section and renders again
([capy/skill/gstack/captain.md](../capy/skill/gstack/captain.md) § 3 lists them). The lane
@@ -156,10 +157,12 @@ controller and been answered into the run
every accepted plan change has been incorporated (`revise`), every reopened phase has
finished, and the run reports `APPROVED` (`status=complete` after `export`). An answered
gate that still prints `PLAN_AMEND_PENDING` or `REOPENED:` is not a completed plan, and an
imported approval row alone is not completion.
imported approval row alone is not completion. A later gate revision that asks nothing new
is answered with the owner's recorded words and says `CARRIED:`; that is the owner's word,
not a new ask ([carry-forward](unattended.md#carry-forward)).
**The record.** `gstack-approvals import --autoplan <run dir> --by "<owner>"` writes the
approval row (date, the ask, the owner's exact words, the lane that answered as relayer,
**The record.** `gstack-approvals import --autoplan <run dir> --by "<owner>"` (with
`--stack <b1,b2,...>` for a stacked PR set) writes the approval row (date, the ask, the owner's exact words, the lane that answered as relayer,
what it covers); `gstack-approvals render` puts it in the report and the PR body. Before
training the PR the controller compares that row's words and `gate_rev` with the ask it
relayed and the reply it carried back; a row that does not match is not READY (shape A).
+10 -1
View File
@@ -155,7 +155,16 @@ and `/proc` start time; elsewhere `ps -o lstart`), because a pid is reused after
a reboot or a machine wake. `bin/gstack-upgrade`, and so the session-start
self-upgrade, refuses with [UPGRADE_BUSY](troubleshooting.md#upgrade-busy)
while a lease held by a live process outside its own chain exists, naming each
holder as `pid <p> (<age>m <command>)`. The team-mode SessionStart hook
holder as `pid <p> (<age>m <command>)`. The install-mode self-upgrade also
defers while its own chain holds a live lease under 120 minutes old. An install
run from inside a workflow (/ship's docs step running `install.sh --for
document-release`, #3571) prints `SELF_UPGRADE: skipped (a gstack run in this
session holds a workflow lease on this installation: ... (UPGRADE_BUSY))`,
leaves the checkout where the run started and stays ok with `behind=`. Each
`gstack-skill-start` renews the lease. On Capy the leased pid is the machine's
long-lived `capy-server`, so the deferral ends when the lease turns 120
minutes old, not when the process exits; `bin/gstack-upgrade` run by hand
moves the checkout sooner. The team-mode SessionStart hook
(`bin/gstack-session-update`) honors the same leases: it logs
`SKIP leased pid=<p>/<age>m` to `analytics/session-update.log` and tries again at
the next session start, without writing its hourly throttle stamp. A lease whose
+35 -3
View File
@@ -768,11 +768,13 @@ with a timing flake: never raise the bound.
```json
{ "sealed": {
"paths": ["heldout/**"],
"paths": ["heldout/**", "**/sealed/**"],
"transcripts": ["runs/**/transcript*.jsonl"],
"allow_paths": ["**/sealed/*/MANIFEST.json", "**/protected/MANIFEST.json"],
"answer_markers": ["SEALED ANSWER:"],
"seed_patterns": ["shuffle_seed=\\d+"],
"local_paths": true } }
"local_paths": true,
"case_insensitive": true } }
```
A repo with a `sealed` key gets a configured local check against accidental
@@ -792,8 +794,38 @@ gstack-pregate sealed [--range <a..b>] [--remote origin] [--base <branch>] [--js
in a later commit); `sealed.answer`, `sealed.seed` and `sealed.local_path`
(`/home/<user>/`, `/Users/<user>/`, `C:\Users\<user>\`) check each commit's added
lines. Findings print rule, `file[:line]` and commit, never the text.
- **Globs.** `paths`, `transcripts` and `allow_paths` match the whole
repo-relative path from the repository root. `*` and `?` stay inside one
path segment; `**/` spans zero or more directories; a `**` not followed by
`/` matches across directories (`heldout/**` covers every depth under
`heldout/`); `[` and `{` are literal. So `sealed/**` misses `tasks/x/sealed/a.json`:
to guard a `sealed/` directory wherever it sits, write `**/sealed/**`.
- **Case.** Matching is case-sensitive. `case_insensitive: true` folds case
for `paths` and `transcripts` only, so `**/heldout/**` also catches
`HELDOUT/` and `HeldOut/`. Separator and spelling variants (`hold-out/`,
`held_out/`, `heldout-v2/`) are not folded: list each as its own glob.
The effective value is the base branch's OR the pushed one (it only tightens).
- **Exceptions.** `allow_paths` exempts publishable files that sit next to
sealed data (a `MANIFEST.json` of hashes and counts) from `sealed.path` and
`sealed.transcript`, applied after the deny globs and before waivers. They
are read from the destination base branch only: an exception a push adds
takes effect once it lands on the base branch, so a branch can never widen
its own. They never fold case. An entry whose file name contains `**`, or
is only wildcards before its extension (`**`, `sealed/*`, `**/*.json`,
`sealed/**.json`), is refused; a partial wildcard such as `MANIFEST*.json`
is accepted and matches every file it spells, so name the file. An exempt file is checked by the
text rules only, so configure `answer_markers` when you exempt files next to
sealed data. Each exempt path is listed on every push (`exempt <rule>
<file> commit <sha> (sealed.allow_paths <glob>)`) and counted as
`exempt=<n>` on the `SEALED:` line.
- **Version skew.** A gstack older than the release whose CHANGELOG adds
`sealed.allow_paths` refuses `allow_paths` and `case_insensitive` as
unknown keys, so every push and `gstack-pregate run` in the repository
fail on that machine. Raise `min_gstack_version` in
`.gstack/ship-policy.json` to that release before landing either key.
- **Policy.** The union of the destination base branch's `sealed` and the
pushed commit's: a push can tighten it, never relax it. A malformed or
pushed commit's: a push can tighten it, never relax it (`allow_paths` come
from the base branch alone, and a pushed deny glob cannot override one). A malformed or
unreadable policy refuses. Waivers come only from the base branch's
`.gstack/sealed-allow.tsv` (`rule<TAB>path<TAB>sha<TAB>reason`) and are
listed on every push.
+16
View File
@@ -953,6 +953,22 @@ receipt when the base moves:
| a merge of the base whose resolution changed product code | a fresh /ship (`RECEIPT_CARRY_PRODUCT_CHANGED`) |
| a rebase or force-push onto a tree the receipt covers, product diff against the new base unchanged | carries (`carry.ancestry: rewritten`) |
| any other rebase or force-push | a fresh /ship (`RECEIPT_CARRY_TREE_MISMATCH`) |
| a stacked PR whose parent landed (squash or merge), retargeted to the base with the base merged in, HEAD's product diff exactly the one reviewed against the parent branch | carries (reason `stack parent <branch> landed`) |
**Stacked PRs (#3570).** A child PR's receipt is written against its parent
branch, so its `base` names that branch and its `product_patch_id` is the
child's own range. After the parent lands, the PR is retargeted to the base, and
/ship's carry check runs with `--base <base>`. When that base differs from the
receipt's, the carry compares the reviewed range (the recorded
`product_patch_id`, or for an older receipt the range against
`origin/<parent>` while that branch still resolves) with HEAD's whole product
diff against the base, computed the way `write` records it, so a parent's
release-metadata change that never reached the base counts too. Equal means
the parent fully landed and the child is unchanged, so the receipt carries
and from then on names the base it was carried to. A parent that has not landed, a child edited
since, or a parent that landed with changes the child never merged all leave a
different diff and refuse with `RECEIPT_CARRY_PRODUCT_CHANGED`. An older
receipt whose parent branch is gone refuses with `RECEIPT_CARRY_REFUSED`.
Since the release that added `review_scope`. A `/ship` receipt names the head it
reviewed, so a conflict-only merge of the base or a test-only fix used to
+60 -15
View File
@@ -1566,11 +1566,19 @@ process has exited never blocks, and neither does a stale one: each lease
records its process's start identity, so a pid reused after a reboot or a
machine wake (GBRA-105's lease named pid 379, which had become dbus-daemon)
is reclaimed instead of blocking. The upgrader's own session (the pid chain it
runs under) never counts.
runs under) never counts for `bin/gstack-upgrade` run by hand. A Capy install
in install mode (`bin/gstack-capy-session`) counts it while that lease is under
120 minutes old: an install run from inside a workflow that holds the lease
(/ship's docs step, #3571) prints `SELF_UPGRADE: skipped (a gstack run in this
session holds a workflow lease on this installation: pid <p> (<age>m
<command>), so the upgrade waits until the lease is 120 minutes old; fix:
<checkout>/bin/gstack-upgrade once the run ends (UPGRADE_BUSY))` and leaves the
checkout where the run started. Each `gstack-skill-start` renews the lease.
**Fix.** Wait for those sessions to finish (`bin/gstack-lease list` shows
every lease with its age and `alive|dead|stale`; a holder is named as
`pid <p> (<age>m <command>)`), then re-run `bin/gstack-upgrade`.
`pid <p> (<age>m <command>)`), then re-run `bin/gstack-upgrade`. For the
session's own run, run the printed `bin/gstack-upgrade` after the run ends.
When you know they are idle, `GSTACK_UPGRADE_FORCE=1 bin/gstack-upgrade`
proceeds. At a Capy session start the same refusal appears as
`SELF_UPGRADE: skipped (live gstack sessions hold this installation: ...)`,
@@ -1729,8 +1737,11 @@ restate them. Rotate the secret if it was real.
**Meaning.** The record is kept per branch (one Capy thread ships one branch), and
HEAD is detached or the working directory is not a git repository. Exit 3.
From `import --autoplan <dir> --stack <b1,b2,...>` it means a listed branch exists
neither locally nor on origin; the line names it, and no branch's record was written.
**Fix.** Check out the branch the work ships from, or pass `--branch <name>`.
**Fix.** Check out the branch the work ships from, or pass `--branch <name>`. For
`--stack`, correct or fetch the named branch and import again.
<a id="approval-record-unavailable"></a>
### `gstack-approvals: ... (APPROVAL_RECORD_UNAVAILABLE)`
@@ -1769,7 +1780,9 @@ import again. Imports are idempotent, so a retry never records an answer twice.
### `gstack-owner-brief render --captain: ... (CAPTAIN_BRIEF_INCOMPLETE)`
**Meaning.** The plan is missing at least one section the brief needs: `## Before → after`,
`## Why it matters`, `## What caused this work`, `## Big decisions`. The brief still prints,
`## Why it matters`, `## What caused this work`, `## Big decisions`, and `## ELI5` (alias
`## In plain words`) on a run whose `gate.json` carries an `eli5` field (gstack 1.91.305 and
later; older runs render as before). The brief still prints,
but it opens with "Not ready for approval" and has no reply invitation. Exit 1.
**Fix.** Add the named sections to the plan (`gstack-autoplan revise --plan <file>` if
@@ -2474,6 +2487,14 @@ and an E.164 `+14155551234` still report, so the finding means the id stands
without a run word (`the id is 40269992077`, `40269992077 failed`) or follows
a word that is not one (`see 40323989742`).
Separately, the scanner skips a bare number that is exactly a power of two
from 2^30 through 2^49, one less than such a power, or one of eleven 32-bit
hash and PRNG constants (Knuth's multiplicative hash, the golden-ratio
constant, the glibc and Numerical Recipes LCGs, FNV-1a, MurmurHash2 and 3, the
MT19937 seeder and the largest 32-bit prime; `isNumericConstant` in
`lib/redact-patterns.ts`). Only the exact digits with no separator are exempt:
the same digits with a `+`, spaces, dashes, dots or parentheses still report.
**Fix.** Write the id next to a run word (`run 40269992077`) or as an
`/actions/runs/` link. For anything else, rerun with `--confirm <token>`
using the `TOKEN:` line the refusal printed; MEDIUM findings do not block
@@ -2514,6 +2535,15 @@ An address only another remote knows still reports. If the hook also printed
`existing-email suppression was limited for this push`, it could not read the
history in 5 seconds, so known authors may be listed too.
A filename with an `@` in the package@version style is not an address and
never reports: the part after the `@` is a version or tag (one dotless label
or a dotted version, never a domain name) followed by file extensions no
top-level domain uses, or a dotted all-digit version followed by
an extension that doubles as a top-level domain (`looksLikeAtFilename` in
`lib/redact-patterns.ts`). A file named after a real address, an address at an
all-digit domain and an address whose top-level domain doubles as a file
extension still report.
**Fix.** Remove the address, or allow it for this repo:
`git config --add gstack.redact.allowEmail <address>`. If it is your own
address, check `git config user.email`. The allowlist only covers `pii.email`;
@@ -3238,7 +3268,10 @@ cannot be downgraded; every other check id in `warn` is honoured only from the
base branch's copy or the user-owned file. For the user-owned file, edit it
directly at the path the message names. A user file inside the repository, or
reached through a symlink into it, is ignored rather than read (`--explain`
names it).
names it). An unknown `sealed` key that a newer gstack defines (such as
`allow_paths` or `case_insensitive`) means this machine's gstack is older than
the policy: upgrade gstack here, and raise `min_gstack_version` in the
repository's `.gstack/ship-policy.json` before landing such a key.
<a id="pregate-secrets"></a>
### `PREGATE_SECRETS`
@@ -3744,7 +3777,11 @@ finding: rule, `file[:line]`, commit. They never print the matched text.
**Fix.** Remove the output from every published commit before the branch
reaches the public repository (a later commit that deletes it still publishes
it). A reviewed false positive gets one line in the base branch's
it). A publishable file kept next to sealed data (a `MANIFEST.json` of hashes
and counts) can be exempted from the path rules with a `sealed.allow_paths`
glob in the base branch's `.gstack/pregate.json`; the text rules still read it
([docs/pregate.md#sealed](pregate.md#sealed)). A reviewed false positive gets
one line in the base branch's
`.gstack/sealed-allow.tsv`: `rule<TAB>path<TAB>sha<TAB>reason`. A private
destination sets `"visibility": "private"` under `sealed` in the base branch's
`.gstack/pregate.json`. Capy's pull-request tool may not run local hooks, so
@@ -4819,8 +4856,10 @@ to HEAD: the PR body has no valid receipt, the working tree has uncommitted or
untracked changes (a carry certifies a commit, never a tree that differs from
it), the receipt's head left HEAD's history and the receipt records no tree to
compare (a receipt written before `head_tree`, whose head is not in this
clone), 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
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
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
@@ -4831,9 +4870,10 @@ ready` refuses `QUEUE_RECEIPT_STALE`.
**Fix.** Commit or stash every working-tree change, then rerun
`gstack-ship-receipt carry --pr <n>` at the new head. When the history is
already rewritten and the receipt predates `head_tree`, run /ship on the new
head; next time merge the base in instead of rebasing. For a depth or
exception refusal, run /ship on the new head (or check out `--head` and carry
there).
head; next time merge the base in instead of rebasing. For a retargeted stacked
PR whose older receipt lost its parent branch, or a depth or exception
refusal, run /ship on the new head. When `--head` is not the checked-out HEAD,
check it out and carry there.
<a id="review-loop-exception-invalid"></a>
### `REVIEW_LOOP_EXCEPTION_INVALID`
@@ -4872,13 +4912,18 @@ registered output the pregate's regen check proved (listed under `proven`,
checked against the same base registry), nor part of the base merge has
a different patch than the reviewed head had. After a rebase it means HEAD has
the reviewed tree but the base moved under it, so the diff a reviewer would see
is not the reviewed one. The reviewed verdict does not cover HEAD, so the
receipt is not carried. The refusal names the changed product paths.
is not the reviewed one. For a stacked PR retargeted to the base after its
parent landed, it means HEAD's product diff against the base is not the range
reviewed against the parent branch: the parent has not fully landed, or the
branch changed since. The reviewed verdict does not cover HEAD, so the
receipt is not carried. The refusal names the changed product paths, or for a
retargeted stacked PR the parent branch and the base.
**Fix.** Run /ship on the new head. The receipt carries only across a
conflict-only base merge, a test-only change, release metadata, a registered
output the pregate's regen check proved, or a rebase onto a covered tree with
the recorded product diff, which the
output the pregate's regen check proved, a rebase onto a covered tree with
the recorded product diff, or a stacked PR's retarget once its parent fully
landed on the base, which the
`carry` object in the carried receipt records with the product patch-id and
the range-diff evidence ([unattended.md](unattended.md#the-terminal-line)).
+47
View File
@@ -504,6 +504,53 @@ by its runner. Result codes: `EXECUTION_UNKNOWN`, `RUN_LOCKED`,
decision ids are renamed and journaled); one written by a newer gstack is
refused.
### Carry-forward
A gate revision after the first (`gate_rev` 2 and later, written after a
`revise`) is answered with the owner's recorded words from the revision before
it when nothing new needs their decision. `gstack-autoplan` prints
`CARRIED: gate_rev=<n> <- gate_rev=<m> words="<their words>" by=<relayer> reason=<why>`,
then the usual `APPROVED:` lines, and journals `gate_carried`. The carry needs
all of: the earlier gate approved (`p1a` or `p1b`) with `rec` or "do it"
(not item tokens), no item there left pending, and the owner's words
recorded; that gate was not itself carried; every accepted change incorporated
or settled; the plan file's bytes equal those recorded at the gate or at the
`revise` that took the changes (any later edit refuses, an ELI5-only one
included); no urgent or
outside-plan finding open; and every new gate item one of the cases below.
Anything else prints `CARRY: no (<why>)` (journal `carry_refused`) and the gate
goes to the owner as before. `--no-carry-forward` on the first `next` turns it
off for the run.
- **"do it".** When the owner's words were "do it" (their words, not a
`captain:` relay of them), a new `auto` item that changes no scope, cost or
direction, whose recommended option changes no plan text, and is no round
or budget grant takes its recommendation. The
decision row gets `carried_under: do it`, the output prints
`CARRIED_UNDER_DO_IT:` per item, and the approval record lists them as
"taken under "do it", not seen by the owner". `rec` carries no new item.
- **Unreviewed post-cap delta.** A "plan changed after <phase> cap" item
recommending `a` (accept) is read by one reviewer before it carries. `CARRY_CHECK:` names the attempt and
the prompt (the delta against `plan-at-gate.r<m>.md`, the accepted changes,
the full plan); the run reserves the estimate in `spend.json` and refuses
when it is over `--carry-check-cap <usd>` (default 5) or the run's cap.
Dispatch it to a model family other than the plan reviewers', then
`check --out <dir> --attempt <id> --result <file> --model <id>`
(`--same-family "<why>"` when no other family is available). A High or
Critical finding, or `--unavailable "<reason>"`, leaves the gate for the
owner. The verdict is saved as `carry_check` on `gate.json`, on the
decision rows (`carry_check`) and in `<dir>/decisions.tsv`. A pending
check is void, its reservation released and the gate left for the owner,
when the owner answers the gate, the plan is revised, a new gate is
written, or the delta changed after dispatch.
Carried rows keep `answered_by: agent:gstack-autoplan` and name their source
(`carried_gate_from`, `carried_gate_rev`, `carried_answered_by`);
`gstack-approvals import --autoplan` writes the carried gate as its own row,
and `gstack-owner-brief render --captain` opens with "Nothing pending.
Carried forward from gate_rev <m>", followed by any items taken under "do it"
without being shown to the owner.
## Owner brief
`gstack-owner-brief render --out <dir>` is what the owner sees at the final