v1.91.344.0 stamp: train gate.train-cab8ef12d747-53522b1bbc60-acd74d S2 (#3656)

This commit is contained in:
garrytan committed 2000-01-01 00:00:00 +00:00
commit 475f2b88d6
20 files changed
+215 -107

No files matched your search

+22
View File
@@ -1,5 +1,27 @@
# Changelog
## [1.91.344.0] - 2026-10-31
**A PR that moves its CHANGELOG entry into `changelog.d/` keeps its /ship receipt (GST-343).** Since #3654 turned on changelog fragments for gstack, a base merge that moved a PR's `[Unreleased]` entry into `changelog.d/<slug>.md` made `gstack-ship-receipt carry` refuse with RECEIPT_CARRY_PRODUCT_CHANGED. The PR then needed a new review pass for a changelog edit.
### Fixed
- **Carry counts a changelog fragment as release metadata** (`lib/ship-receipt.ts`, `bin/gstack-ship-receipt`). When the base policy sets `changelog_fragments`, each `<dir>/<slug>.md` directly under the directory is metadata, as `CHANGELOG.md` is. The directory's README.md, a nested path, a non-Markdown file and a fragment under a policy without `changelog_fragments` stay product. docs/ship-policy.md lists the fragment under release metadata.
### Tests
- `test/gstack-ship-receipt.test.ts`: a merge-only head that moves the entry into `changelog.d/feat.md` carries as "base merge + release metadata". A README edit in the same merge is refused and named, and so is the same move when the base policy has no `changelog_fragments`.
## [1.91.343.0] - 2026-10-31
**Every ask to the owner now uses one lettered format (R41): the Captain's name, then one line per decision with the problem, the options and the recommendation, so the owner can answer "do it" or a letter like "B2" from a phone. When the owner has to do something, the ask links the exact page and lists the steps (R43).**
### Changed
- **The plan ask lists each gate choice as a lettered line** (`lib/captain-brief.ts`, `gstack-owner-brief render --captain`): "What we need from you" prints one line per choice with the recommended option first and the plan approval as the last letter. The header and the closing line invite **do it** or a named option, and a new `--captain-name <name>` heads the list. The relaying agent's last line maps every letter to its gate token next to `rec resolves to …` and says a named option answers as `rec except <token>`, so a reply like "B2" translates exactly. A plan that is missing a section still invites no reply.
- **An ask that needs the owner to do something carries the exact link and simple steps (R43).** The ask format says so: the full URL of the page where it happens (never a menu path like "Settings → Tokens"; a Capy app page gets its exact path and field), numbered steps saying what to click and type, and the one word to reply when done. `docs/captain.md`, `plan-ask.md`, `merge-ask.md`, `captain/SKILL.md` and `factory.md` point 11 carry it.
- **docs/captain.md has a new section, The ask format**, and the plan ask, the controller's relay and the digest's Needs you point to it. The captain templates (`plan-ask.md`, `DIGEST.md`, `merge-ask.md`, `AGENTS.md`, `report.md`), `captain/SKILL.md`, the Capy gstack skill (`SKILL.md`, `captain.md`, `factory.md`) and `docs/recommended-harness.md` state the same format.
### Tests
- `test/captain-brief.test.ts`: lettered lines with the recommendation first and "A1 to take recommendation", the letter-to-token map, the `--captain-name` heading, carried answers in the lettered lines, and no reply invitation on a plan that is missing a section.
## [1.91.342.0] - 2026-10-30
**gstack now versions its own PRs at merge time: a PR adds `changelog.d/<branch-slug>.md` instead of a version, and trains merge through `train land` instead of slot calls.** `.gstack/ship-policy.json` turns `changelog_fragments` on with the checks every PR runs as `required_checks`, and `min_gstack_version` moves to 1.91.340.0, the first release that reads `required_checks` and counts a stamped slot as merged only when GitHub reports it MERGED, so an older gstack refuses the file by name.
+1 -1
View File
@@ -1 +1 @@
1.91.342.0
1.91.344.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.342.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.91.344.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows,
+6 -5
View File
@@ -3,7 +3,7 @@
* gstack-owner-brief — the one-page owner brief at the autoplan gate (plan E3).
*
* gstack-owner-brief render --out <dir> [--page N] [--summary <file>] [--write] [--json]
* gstack-owner-brief render --captain --out <dir> [--allow-incomplete] [--write] [--json]
* gstack-owner-brief render --captain --out <dir> [--captain-name <name>] [--allow-incomplete] [--write] [--json]
* gstack-owner-brief close --out <dir> --plan <file> [--date YYYY-MM-DD] [--json]
*
* `render` prints the plain-language summary (--summary <file>, else <dir>/summary.md
@@ -12,8 +12,9 @@
* taken if unanswered, approval items stay pending) and points at review-record.md
* for the full review; --write also saves brief.md in the run directory.
* `render --captain` prints the plan ask in the shape Capy Captains relay (lib/captain-brief.ts): header with
* the one-word reply, Before → after, Why it matters, What caused this work, Big decisions, What we need from
* you, and one trailing line for the relaying agent; a plan missing a section exits 1
* the "do it" reply, Before → after, Why it matters, What caused this work, Big decisions, What we need from
* you (the lettered ask lines of docs/captain.md § The ask format, headed by --captain-name), and one
* trailing line for the relaying agent; a plan missing a section exits 1
* (CAPTAIN_BRIEF_INCOMPLETE) unless --allow-incomplete. --write saves captain-brief.md.
* `close` rewrites every `draft direction stands until the owner decides` line in
* the plan to the decided option and date; pending lines stay and are reported
@@ -29,7 +30,7 @@ import { resultLine } from '../lib/result-codes';
const HELP = `gstack-owner-brief — one page for the owner: summary, URGENT block, numbered decisions (docs/unattended.md#owner-brief)
render --out <dir> [--page N] [--summary <file>] [--write] [--json]
render --captain --out <dir> [--allow-incomplete] [--write] [--json] the plan ask for a Capy Captain (capy/skill/gstack/captain.md)
render --captain --out <dir> [--captain-name <name>] [--allow-incomplete] [--write] [--json] the plan ask for a Capy Captain (capy/skill/gstack/captain.md); --captain-name heads the lettered ask
close --out <dir> --plan <file> [--date YYYY-MM-DD] [--json]
${exitTableHelp()}`;
@@ -49,7 +50,7 @@ function main(argv: string[]): number {
const json = rest.includes('--json');
if (cmd === 'render' && rest.includes('--captain')) {
const brief = renderCaptainBrief(loaded.inputs, { outDir: path.resolve(out) });
const brief = renderCaptainBrief(loaded.inputs, { outDir: path.resolve(out), captainName: flag(rest, '--captain-name') });
if (rest.includes('--write')) fs.writeFileSync(path.join(path.resolve(out), 'captain-brief.md'), brief.text);
if (json) process.stdout.write(JSON.stringify({ run: loaded.inputs.gate.run, gate_rev: loaded.inputs.gate.gate_rev, rec_hash: brief.recHash, pending: brief.pending, missing: brief.missing, brief: brief.text }) + '\n');
else process.stdout.write(brief.text);
+16 -7
View File
@@ -324,8 +324,8 @@ to be outside the merged PR. Reruns on `BASE` are never a way to get a green.
The controller keeps one digest file (`captain/templates/DIGEST.md`), rewritten at every
turn, three sections in this order, each row with its thread, one line, and an age:
1. **Needs you**: decisions only the owner can make, each with the plain-language ask and
the one-word default. Empty means the owner has nothing to do.
1. **Needs you**: decisions only the owner can make, in the ask format below (the
Captain's name, one lettered line per decision). Empty means the owner has nothing to do.
2. **New**: what landed or changed since the last digest (merges with versions, READYs
received, trains built, incidents), newest first.
3. **In progress**: every working lane, what it owns, its state (building, READY, in
@@ -352,11 +352,20 @@ says so instead of looking empty.
**B. The plan ask**, in this order, written for someone outside the project (no gate codes
in the body): before → after with concrete examples; why it matters, with numbers; what
caused the work (issue, run, report, or the owner's words quoted); the big decisions
(options, pick, why, what would flip it); what we need from you, one decision with each
option's cost and the recommended one-word reply as the default; and a closing
**"do it"** reply that takes every recommendation at full scope, follow-ons included,
with one line saying what it covers. `gstack-owner-brief render --captain` prints this shape from
a plan; relay it whole. Template: `captain/templates/plan-ask.md`.
(options, pick, why, what would flip it); what we need from you in the ask format; and a
closing **"do it"** reply that takes every recommendation at full scope, follow-ons
included. `gstack-owner-brief render --captain --captain-name "<Captain name>"` prints this
shape from a plan; relay it whole. Template: `captain/templates/plan-ask.md`.
**The ask format** (every ask to the owner, the digest's Needs you included), explained like
the owner is five: the Captain's name, then one lettered line per decision, letters unique
across the list: `- A: <code> hit problem p, which happens when h. We could fix it in these
ways: 1) x; 2) y; 3) z. Recommendation: x because why. A1 to take recommendation.` The
recommendation is option 1; "do it" takes every recommendation in the last list, and a named
option ("A2") takes that option instead. When the owner has to do something, the line
carries the exact link to the page (never a menu path; a Capy app page gets its exact path
and field), numbered steps in the simplest words, and the one word to reply when done.
Home: `docs/captain.md`, The ask format.
**Plan before build.** A lane that builds something runs a sized `/autoplan` on its brief as
its first act after the session-start check and sends the controller the plan ask above; the controller relays it, and the
+16 -7
View File
@@ -252,8 +252,8 @@ to be outside the merged PR. Reruns on `BASE` are never a way to get a green.
The controller keeps one digest file (`captain/templates/DIGEST.md`), rewritten at every
turn, three sections in this order, each row with its thread, one line, and an age:
1. **Needs you**: decisions only the owner can make, each with the plain-language ask and
the one-word default. Empty means the owner has nothing to do.
1. **Needs you**: decisions only the owner can make, in the ask format below (the
Captain's name, one lettered line per decision). Empty means the owner has nothing to do.
2. **New**: what landed or changed since the last digest (merges with versions, READYs
received, trains built, incidents), newest first.
3. **In progress**: every working lane, what it owns, its state (building, READY, in
@@ -280,11 +280,20 @@ says so instead of looking empty.
**B. The plan ask**, in this order, written for someone outside the project (no gate codes
in the body): before → after with concrete examples; why it matters, with numbers; what
caused the work (issue, run, report, or the owner's words quoted); the big decisions
(options, pick, why, what would flip it); what we need from you, one decision with each
option's cost and the recommended one-word reply as the default; and a closing
**"do it"** reply that takes every recommendation at full scope, follow-ons included,
with one line saying what it covers. `gstack-owner-brief render --captain` prints this shape from
a plan; relay it whole. Template: `captain/templates/plan-ask.md`.
(options, pick, why, what would flip it); what we need from you in the ask format; and a
closing **"do it"** reply that takes every recommendation at full scope, follow-ons
included. `gstack-owner-brief render --captain --captain-name "<Captain name>"` prints this
shape from a plan; relay it whole. Template: `captain/templates/plan-ask.md`.
**The ask format** (every ask to the owner, the digest's Needs you included), explained like
the owner is five: the Captain's name, then one lettered line per decision, letters unique
across the list: `- A: <code> hit problem p, which happens when h. We could fix it in these
ways: 1) x; 2) y; 3) z. Recommendation: x because why. A1 to take recommendation.` The
recommendation is option 1; "do it" takes every recommendation in the last list, and a named
option ("A2") takes that option instead. When the owner has to do something, the line
carries the exact link to the page (never a menu path; a Capy app page gets its exact path
and field), numbered steps in the simplest words, and the one word to reply when done.
Home: `docs/captain.md`, The ask format.
**Plan before build.** A lane that builds something runs a sized `/autoplan` on its brief as
its first act after the session-start check and sends the controller the plan ask above; the controller relays it, and the
+5 -3
View File
@@ -25,9 +25,11 @@ a standing approval instead of asking again. Append; never rewrite an owner's wo
rerun: <amount>. Over the cap is a fresh ask.
## Asks to the owner (<owner>, <date>: "<words>")
- Plain language, each option with what it is, why it matters, what each answer does and
the cost. The recommended one-word reply is the default. Every ask ends with "do it":
every recommendation at full scope, follow-ons included, no further ask.
- The ask format (gstack docs/captain.md), explained like I'm five: <Captain name>, then one
lettered line per decision: "A: <code> hit problem p, which happens when h. We could fix it
in these ways: 1) x; 2) y; 3) z. Recommendation: x because why. A1 to take recommendation."
"do it" takes every recommendation in the list at full scope, follow-ons included, no
further ask; a named option ("A2") takes that option instead.
## Parallelism (<owner>, <date>: "<words>")
- Keep about <N> lanes working on disjoint files whenever there is backlog. Serial steps
+6 -1
View File
@@ -7,7 +7,12 @@ every row with its thread and an age. Codes are the thread codes the host assign
<empty when the owner has nothing to do>
- <thread> · <age since asked> · <one-line plain-language ask> · default reply: **<one word>** · cost: <amount or none>
<CREW_NAME>
- A: <thread> hit problem <p>, which happens when <h>. We could fix it in these ways: 1) <x>; 2) <y>; 3) <z>. Recommendation: <x> because <why> (cost: <amount or none>; asked <age> ago). A1 to take recommendation.
- B: …
"do it" takes every recommendation above; a named option ("A2") takes that option instead. The
ask format is gstack docs/captain.md, The ask format.
## New
+5 -1
View File
@@ -24,4 +24,8 @@ flaky: <cases, both runs, ledger date | none> · pregate: pass tree=<tree12>
```
The controller's merge report: the PR link, the key image inline, the version it will
take, the slot, and the one-word reply if a decision is attached.
take, the slot, and, if a decision is attached, its lettered line in the ask format
(gstack docs/captain.md): "A: … Recommendation: x because why. A1 to take recommendation."
If the merge needs the owner to act (approve a deployment, grant access), the line carries
the exact link to that page and numbered steps in the simplest words, ending with the one
word to reply when done (a Capy app page: its exact path and field instead of a link).
+13 -4
View File
@@ -5,7 +5,7 @@ gate codes in the body. `gstack-owner-brief render --captain --out <run dir>` pr
shape from an autoplan run; write it by hand for any other plan.
```text
Decision needed on <thread>: <one-line title>. Reply **rec** to take every recommendation.
Decision needed on <thread>: <one-line title>. Reply **do it** to take every recommendation.
## ELI5
<5-10 plain sentences: what changes, for whom, and what it costs>
@@ -25,10 +25,19 @@ output, a screen, a cost or time number>
2. …
## What we need from you
<one decision>: <option 1> (<cost>) · <option 2> (<cost>) · our pick: <option>, reply **rec**.
<Captain name>
- A: <code> hit problem <p>, which happens when <h>. We could fix it in these ways: 1) <x> (<cost>); 2) <y> (<cost>); 3) <z> (<cost>). Recommendation: <x> because <why>. A1 to take recommendation.
- B: …
**do it** = take every recommendation above at full scope, including <the follow-ons
named: docs, trains, cleanups>, and carry them through to done with no further ask.
**do it** = take every recommendation in this list at full scope, including <the follow-ons
named: docs, trains, cleanups>, and carry them through to done with no further ask. A named
option ("A2") takes that option instead.
<only when the owner must act; the exact link, never a menu path (a Capy app page: its exact path and field)>
- C: <code> needs you to <action>, because <why>. Open <exact URL>, then:
1. Click <button>.
2. Type <value> in <field>.
3. Reply **done**.
```
When the answer comes back, record it before the work starts. For an autoplan gate:
+1 -1
View File
@@ -16,7 +16,7 @@ READY block sent: <yes, date> · SHIP_RECEIPT: pr=<n> head=<sha12 of the pin, as
## Needs
<what the owner must do, each with the one-word default; or "nothing">
<"nothing", or the owner's decisions in the ask format (gstack docs/captain.md): one lettered line each, "A1 to take recommendation">
## Cost
+2 -2
View File
@@ -49,7 +49,7 @@ Token caps on /ship: pass a brief's cap as `--token-cap <n>`. /ship reads `cost.
Every Capy thread that uses gstack follows these for new work, under a Captain or on its own. The rule in full is `docs/captain.md`, "Plan before build"; the lane side is section 3 of captain.md in this folder.
- **Plan first.** Before building new functionality, run `/autoplan` on it as the first act after the install check, sized to the work (scope flags and `--max-rounds`; a one-file fix gets `--no-ui --no-dx --max-rounds 1` with `--estimate-usd` and a small `--spend-cap`, never no run).
- **ELI5, then the word.** Send the plan and gate to the person, or the Captain, in plain language: `gstack-owner-brief render --captain --out <run dir>`, with `rec` as the one-word default and a "do it" reply that takes every recommendation at full scope. Build only after their word is answered into the run and it reports `APPROVED`; then `gstack-approvals import --autoplan <run dir> --by "<owner>"`.
- **ELI5, then the word.** Send the plan and gate to the person, or the Captain, in plain language: `gstack-owner-brief render --captain --out <run dir> --captain-name "<Captain name>"`, its choices as lettered lines in the ask format (`docs/captain.md`) and "do it" taking every recommendation at full scope. Build only after their word is answered into the run and it reports `APPROVED`; then `gstack-approvals import --autoplan <run dir> --by "<owner>"`.
- **/ship before ready.** Run /ship at the final pushed head before a PR is called ready; `SHIP_RECEIPT: pr=<n> head=<head>` must name that head, and a later push needs a new /ship. The review inside /ship, like the autoplan, is sized to the change.
- **No merge without the owner's word.** A thread never merges on its own; under a Captain the controller merges on a recorded standing approval, and a thread with no Captain asks the person.
@@ -63,7 +63,7 @@ When you work for a Capy Captain (a crew thread) or straight for a person, gstac
- **Standing approvals need no ask.** These are the ones your Captain lists in its rules (for example "merge when green, through a train"). Cite the rule instead of asking again.
- **These always need a fresh ask:** a product or behaviour decision, spend over a stated cap, anything published outside the repository, a change to a threshold, budget or trial count, closing someone's contribution without landing it, and anything destructive.
- **Record every answer.** Use `gstack-approvals add --ask … --words "<their exact words>" --by … --scope …`, or after an autoplan answer, `gstack-approvals import --autoplan <run dir> --by <name>`. `gstack-approvals render` prints the `## Approvals` table for your report and the PR body. Rows are secret-scanned, and on Capy they are mirrored to the private Drive folder.
- **Plan approvals** use `gstack-owner-brief render --captain --out <run dir>`. It prints before → after, why it matters, what caused this work, the big decisions (options, the pick, why, what would flip it) and what we need, with `rec` as the recommended one-word reply. Relay it whole. Gate codes appear only on its last line, which is for you. If it exits `CAPTAIN_BRIEF_INCOMPLETE`, add the missing plan sections first.
- **Plan approvals** use `gstack-owner-brief render --captain --out <run dir>`. It prints before → after, why it matters, what caused this work, the big decisions (options, the pick, why, what would flip it) and what we need, as lettered ask lines closed by "do it". Relay it whole. Gate codes appear only on its last line, which is for you. If it exits `CAPTAIN_BRIEF_INCOMPLETE`, add the missing plan sections first.
- **Merge asks.** /ship's PR body opens with `## What changed` (captioned before/after images from `gstack-pr-shots` or a UI screenshot, uploaded with Capy's PR asset tool), `## Why`, `## Blast radius` and `## Approvals`. Your READY report attaches the key image.
## Before any push or pull request
+13 -11
View File
@@ -87,23 +87,24 @@ project can follow it:
3. **What caused this work.** The trigger: an issue or PR link, a failed run, a user report, or the
person's request, quoted.
4. **Big decisions.** For each: the options considered, the pick and why, and what would flip it.
5. **What we need from you.** One decision, the options and their costs, the pick, and the one-word reply.
The recommended reply is the default (`rec` for an autoplan gate). Never offer "the plan as first
written" as the default.
6. **"do it".** Every ask ends with a "do it" reply: take every recommendation at full scope, the follow-ons
the plan names (docs, trains, cleanups) included, and carry them through to done with no further ask.
Say in one line what it covers. For an autoplan gate, "do it" answers as `rec`; record the words.
5. **What we need from you.** The ask format (`docs/captain.md`, The ask format): the Captain's name, then one
lettered line per decision, letters unique across the list: "A: <code> hit problem p, which happens when h.
We could fix it in these ways: 1) x; 2) y; 3) z. Recommendation: x because why. A1 to take recommendation."
The recommendation is option 1. Never offer "the plan as first written" as the default.
6. **"do it".** Every ask ends with "do it": take every recommendation in the list at full scope, the follow-ons
the plan names (docs, trains, cleanups) included, and carry them through to done with no further ask. A named
option ("A2") takes that option instead. For an autoplan gate, "do it" answers as `rec`; record the words.
For an autoplan run, open the plan with `## ELI5` (5-10 plain sentences), then write parts 1 to 4 as
`## Before → after`, `## Why it matters`, `## What caused this work` and `## Big decisions` sections. Then, at the gate:
```bash
gstack-owner-brief render --captain --out <run dir>
gstack-owner-brief render --captain --out <run dir> --captain-name "<Captain name>"
```
It prints a header line ("Decision needed … Reply **rec** …"), the `### ELI5` summary (on a run whose gate
carries `eli5`), the four plan sections (each capped, with a pointer to the plan for the rest), and "What we need from you", which lists every choice `rec` takes, in
words, and closes with the "do it" reply, then a `### Cost` section (spent so far, worst case, and the estimate for
It prints a header line ("Decision needed … Reply **do it** …"), the `### ELI5` summary (on a run whose gate
carries `eli5`), the four plan sections (each capped, with a pointer to the plan for the rest), and "What we need from you", one lettered
line per choice in the ask format, closed by "do it", then a `### Cost` section (spent so far, worst case, and the estimate for
the phases `rec` can reopen). The last line, "For the relaying agent", carries the run, `gate_rev`, `rec_hash` and the exact
`gstack-autoplan answer` command. Relay everything above that line. When the reply comes back:
@@ -119,7 +120,8 @@ one reviewer from another model family reads the unreviewed delta first (`gstack
else prints `CARRY: no (<why>)` and goes to the person as today (`docs/unattended.md#carry-forward`).
If the person answers in words ("split it into two PRs"), map the words to tokens, answer, and confirm in one
line what you recorded ("Recorded: split into two PRs; everything else as recommended"). A plan missing a
line what you recorded ("Recorded: split into two PRs; everything else as recommended"). A named option ("B2")
answers as `--reply "rec except <token>"`, with the token from the last line's letter map. A plan missing a
section renders "Not ready for approval" with no reply invitation, and exits 1 (`CAPTAIN_BRIEF_INCOMPLETE`):
add the sections, run `gstack-autoplan revise --plan <file>`, and render again. For a decision that is not an
autoplan gate, write the same five parts by hand.
+11 -11
View File
@@ -1,17 +1,17 @@
# How we work: the gstack software factory on Capy
What seven Captain crews on gstack and Capy converged on (2026-10-29). Your crew's CREW.md and recorded approvals decide who may merge, update a base, spend or touch production; urgency changes priority, never permissions or gates. With no Captain, ask the person and never merge without their word. Story and numbers: capy/README.md; evidence and examples: your organization Drive's software-factory/README.md, when one exists.
What seven Captain crews converged on (2026-10-29). Your crew's CREW.md and recorded approvals decide who may merge, update a base, spend or touch production; urgency changes priority, never permissions or gates. With no Captain, ask the person and never merge without their word. Story: capy/README.md; examples: your org Drive's software-factory/README.md, if any.
1. **Plan, then a one-word yes.** New functionality starts with /autoplan sized to the work, and the build waits for "rec" or "do it". [SKILL.md: Plan, approve, ship]
2. **Someone else merges.** Run /ship to a receipt on your final pushed head, send one READY that quotes gstack's verify output word for word, and stop. The merge owner merges on green, and versions are assigned at merge. [docs/captain.md: The READY contract, Merge trains]
1. **Plan, then a yes.** New functionality starts with /autoplan sized to the work; the build waits for "do it". [SKILL.md: Plan, approve, ship]
2. **Someone else merges.** Run /ship to a receipt on your final pushed head, send one READY quoting gstack's verify output word for word, and stop. The merge owner merges on green and assigns versions. [docs/captain.md: The READY contract]
3. **Block only on the floor.** A finding blocks if it would fail a test, break production, or lose data or secrets; the rest goes in the PR body or the next wave. [docs/review-policy.md]
4. **Fix waves.** When main goes red, hold /ship while one owner finds the root cause; bring main in once, as your crew's rules allow. Fold deferred findings and test follow-ups into one wave PR. [docs/triage.md]
5. **Severe first, same gates.** A severe user-facing bug jumps the queue; the plan can be short, but the receipt, checks and merge owner stay.
4. **Fix waves.** When main goes red, hold /ship while one owner finds the root cause; bring main in once, as your rules allow. Fold deferred findings and test follow-ups into one wave PR. [docs/triage.md]
5. **Severe first, same gates.** A severe user-facing bug jumps the queue; the plan can be short, but receipt, checks and merge owner stay.
6. **Trade findings, not small talk.** A finding that belongs to another crew goes to its Captain, through yours, with the evidence.
7. **Measure before you act.** Time it, count it, run the real system, on the merged tree you ship; a real run outranks a reviewed plan. Each number names its run, commit or uncapped query; say what you did not verify.
8. **Evals decide, and losses get reported.** Preregister bars, cap and stop rule before the first paid call [/eval-plan]. Cheap proof first: a keyless fake, a small smoke, then the paid run. Use controls: no-skill and placebo arms, decoys verified clean. Report use and pass separately and quote spend from the meter.
9. **Flakes: one rerun, a ledger, a daily fix.** Only the rerun the policy allows, never a harness or infra red; a second red is real, and no test is loosened. [AGENTS.md: Validation discipline; docs/captain.md: The daily flaky pass]
10. **Quiet.** Message on a decision, a blocker, a READY or a finished phase. A broadcast that needs no reply goes in the Captain's notes, not to every lane; never reply just to acknowledge.
11. **Ask once, plainly.** One open ask per decision, on an issue: recommendation first, one line per choice, everyday words, links inline. Never re-send it or re-ask a standing rule. Settle what you can check yourself.
12. **Fresh threads, closed out.** One thread per unit of work (crew threads ask their Captain for new ones); past the context threshold, hand off. Its Captain archives each thread it, its crew or its automations start, by its plan for it. Size reviews to the diff; read the token counter, don't estimate. [docs/ship-budget.md]
7. **Measure before you act.** Run the real system on the merged tree you ship; a real run outranks a reviewed plan. Each number names its run, commit or uncapped query; say what you did not verify.
8. **Evals decide, and losses get reported.** Preregister bars, cap and stop rule before paying [/eval-plan]. Cheap proof (a keyless fake, a smoke) before the paid run. Use controls (no-skill and placebo arms, clean decoys). Report use and pass separately; quote spend from the meter.
9. **Flakes: one rerun, a ledger, a daily fix.** Only the rerun the policy allows, never a harness or infra red; a second red is real; no test is loosened. [docs/captain.md: The daily flaky pass]
10. **Quiet.** Message on a decision, a blocker, a READY or a finished phase. A broadcast that needs no reply goes in the Captain's notes; never reply just to acknowledge.
11. **Ask once, plainly.** One ask per decision, explained like the owner is five: your Captain's name, then a lettered line each: "A: <code> hit problem p, which happens when h. We could do 1) x, 2) y or 3) z. Recommendation x because why. A1 to take recommendation." "do it" takes every recommendation; "A2" takes option 2. Owner tasks: the exact link (in Capy: path and field), numbered simple steps, the reply word. Never re-ask a standing rule; check what you can yourself. [docs/captain.md: The ask format]
12. **Fresh threads, closed out.** One thread per unit of work (crew threads ask their Captain); past the context threshold, hand off. Its Captain archives the threads it, its crew or automations start. Size reviews to the diff; read the token counter. [docs/ship-budget.md]
13. **The owner's model preference.** `gstack-models resolve --role agent` picks agent models, except cross-family voices and eval subjects. [docs/model-policy.md: Your model preference]
-7
View File
@@ -1,7 +0,0 @@
**A PR that moves its CHANGELOG entry into `changelog.d/` keeps its /ship receipt (GST-343).** Since #3654 turned on changelog fragments for gstack, a base merge that moved a PR's `[Unreleased]` entry into `changelog.d/<slug>.md` made `gstack-ship-receipt carry` refuse with RECEIPT_CARRY_PRODUCT_CHANGED. The PR then needed a new review pass for a changelog edit.
### Fixed
- **Carry counts a changelog fragment as release metadata** (`lib/ship-receipt.ts`, `bin/gstack-ship-receipt`). When the base policy sets `changelog_fragments`, each `<dir>/<slug>.md` directly under the directory is metadata, as `CHANGELOG.md` is. The directory's README.md, a nested path, a non-Markdown file and a fragment under a policy without `changelog_fragments` stay product. docs/ship-policy.md lists the fragment under release metadata.
### Tests
- `test/gstack-ship-receipt.test.ts`: a merge-only head that moves the entry into `changelog.d/feat.md` carries as "base merge + release metadata". A README edit in the same merge is refused and named, and so is the same move when the base policy has no `changelog_fragments`.
+41 -9
View File
@@ -144,7 +144,7 @@ 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>`
pick, and the choices as lettered ask lines ([the ask format](#the-ask-format)). `gstack-owner-brief render --captain --out <run dir>`
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
@@ -153,10 +153,10 @@ section exits `CAPTAIN_BRIEF_INCOMPLETE`, so the lane adds the section and rende
([capy/skill/gstack/captain.md](../capy/skill/gstack/captain.md) § 3 lists them). The lane
sends it whole, as a needs-you report, and builds nothing yet.
**What the controller relays.** One ask to the owner in shape B below: the recommended
reply (every recommendation, plan changes included) as the one-word default and a "do it"
reply that takes every recommendation at full scope, follow-ons included. The plan as
first written is never the default.
**What the controller relays.** One ask to the owner in shape B below, its choices in
[the ask format](#the-ask-format): "do it" (every recommendation, plan changes included,
at full scope, follow-ons included) is the default, and a named option such as "A2" takes
that option instead. The plan as first written is never the default.
**The build gate.** The lane builds only after the owner's word has come back through the
controller and been answered into the run
@@ -329,19 +329,51 @@ owner pre-approved (train gates, screens, bisects, the backstop, focused paid ca
the owner's date and words and the cap per rerun; anything over the cap is a fresh ask.
**B. The plan ask** ([template](../captain/templates/plan-ask.md)): before → after, why it
matters, what caused this work, the big decisions, what we need from you with the
recommended one-word reply as the default, and a closing "do it" reply that takes every
recommendation at full scope. `gstack-owner-brief render --captain` prints it from an
matters, what caused this work, the big decisions, and what we need from you in the ask
format below, closed by "do it", which takes every recommendation at full scope.
`gstack-owner-brief render --captain --captain-name "<Captain name>"` prints it from an
autoplan run.
**C. The merge ask** ([template](../captain/templates/merge-ask.md)): captioned
before/after images, why, blast radius, the Approvals table, and the evidence. /ship writes the PR
body in this shape on Capy; the controller's merge report shows the key image inline.
## The ask format
Every ask to the owner, from a plan gate to a merge question to the digest, is written so a
five-year-old could follow it: the Captain's name, then one lettered line per decision, with
letters unique across the list.
```text
<Captain name>
- A: <code> hit problem <p>, which happens when <h>. We could fix it in these ways: 1) <x>; 2) <y>; 3) <z>. Recommendation: <x> because <why>. A1 to take recommendation.
- B: …
```
The recommendation is always option 1. "do it" takes every recommendation in the last
list; a named option ("A2") takes that option instead. `<code>` is the issue, PR or thread
the decision is about. An ask that cannot be put this way is not ready to send.
**When the owner has to do something** (create a token, approve a deployment, accept an
invite, change a setting), the line carries the exact link to the page where it happens, a
full URL and never a menu path like "Settings → Tokens", then numbered steps in the
simplest words: what to click, what to type, what to name it, and the one word to reply
when it is done. A page inside the Capy app is the one exception, because it has no stable
link: name the exact path and field instead ("Settings → Integrations → Slack, the Channel
field").
```text
<Captain name>
- A: <code> needs you to <action>, because <why>. Open <exact URL>, then:
1. Click <button>.
2. Type <value> in <field> and name it <name>.
3. Reply **done**.
```
## The digest
`DIGEST.md` is rewritten every controller turn: **Needs you** (decisions only the owner
can make, each with its one-word default; empty is the goal), **New** (what landed or
can make, in [the ask format](#the-ask-format); empty is the goal), **New** (what landed or
changed, newest first), **In progress** (every lane, what it owns, its state, the age of
its last report). Ages turn stalls into the controller's next action.
+1 -1
View File
@@ -119,7 +119,7 @@ explains how the Captain runs a day. Drop the templates in, replace the
placeholders (repository, crew login list, gate workflow name, minimum
revision, who may approve what), and the crew has its rules before its first
thread starts. The approvals shape itself (plan asks with before → after, why,
cause, big decisions and a one-word default; a `## Approvals` table in every
cause, big decisions, and the choices as lettered ask lines closed by "do it"; a `## Approvals` table in every
report and PR body) is the harness-generic part and is documented in
[capy/skill/gstack/captain.md](../capy/skill/gstack/captain.md).
+27 -16
View File
@@ -3,9 +3,12 @@
* their person (capy/skill/gstack/captain.md): a two-line header with the
* one-word reply, then Before → after, Why it matters, What caused this work,
* Big decisions (each read from the reviewed plan through the shared heading
* parser) and What we need from you (built from gate.json: every choice `rec`
* takes, in words, closed by the "do it" reply: rec at full scope, follow-ons
* included, carried through with no further ask). Gate ids, gate_rev and rec_hash appear only on the last
* parser) and What we need from you: the owner's ask format (docs/captain.md
* § The ask format), one lettered line per gate choice with the Captain's name
* first when --captain-name gives it, options numbered with the recommendation
* as 1 ("A1 to take recommendation"), closed by "do it": every recommendation
* at full scope, follow-ons included, carried through with no further ask. Gate
* ids, gate_rev, rec_hash and the letter-to-token map appear only on the last
* line, which is for the relaying agent. A plan missing a section yields a
* brief that says it is not ready for approval and has no reply invitation
* (CAPTAIN_BRIEF_INCOMPLETE). Called by bin/gstack-owner-brief render --captain.
@@ -47,7 +50,7 @@ const OPTION_WORDS: Record<string, string> = {
};
const plainOption = (t: string) => {
const s = t.replace(/\s*\([^)]*gstack-autoplan[^)]*\)/g, '').trim();
if (/^approve with the recommended changes\b/.test(s)) return 'approve, with the plan changes marked below';
if (/^approve with the recommended changes\b/.test(s)) return 'approve, with the plan changes marked above';
if (s === 'approve as-is') return 'approve the plan as reviewed';
return OPTION_WORDS[s] ?? s;
};
@@ -71,7 +74,7 @@ export function buildEli5(planText: string, planFile: string): GateEli5 {
export interface CaptainBrief { text: string; missing: string[]; pending: boolean; recHash: string }
export function renderCaptainBrief(inputs: BriefInputs, o: { outDir: string }): CaptainBrief {
export function renderCaptainBrief(inputs: BriefInputs, o: { outDir: string; captainName?: string }): CaptainBrief {
const plan = reviewedPlan(o.outDir, inputs.run);
const title = firstTitle(plan.text) ?? 'this plan';
const list = inputs.gate;
@@ -86,6 +89,7 @@ export function renderCaptainBrief(inputs: BriefInputs, o: { outDir: string }):
const missing = [...(eli5 && !eli5.summary.trim() ? [ELI5_SECTION.title] : []), ...sections.filter(s => !s.found || !s.found.body.trim()).map(s => s.title)];
const urgent = inputs.findings.some(r => r.urgent === true || r.outside_plan === true) ? [renderUrgentBlock(inputs.findings).trimEnd(), ''] : [];
const items = list.items.filter(i => i.id !== 'p1');
const asked = [...items, ...(p1 ? [p1] : [])];
const prior = priorChoices(o.outDir, list.gate_rev);
const rec = parseReply('rec', list, undefined, prior).choices;
const recKey = (i: GateItem) => rec.find(c => c.id === i.id)?.chosen ?? i.recommended;
@@ -104,27 +108,34 @@ export function renderCaptainBrief(inputs: BriefInputs, o: { outDir: string }):
header = [`**Not ready for approval:** the plan "${title}" is missing ${missing.join(', ')}. The thread will add them and send this again; do not approve from this draft.`];
} else {
header = [
`**Decision needed:** approve the plan "${title}". Reply **rec** to ${kept.length ? 'keep the answers you already gave and take every other recommendation' : 'take every recommendation'} (recommended), or say in words what to change.`,
`${items.length ? `${items.length} choice${items.length === 1 ? '' : 's'} below; rec ${kept.length ? `keeps the ${kept.length} you already answered and takes the recommended answer for the rest` : 'takes the recommended answer for each'}.` : 'No open choices besides the approval itself.'} Nothing is built until you answer.`,
`**Decision needed:** approve the plan "${title}". Reply **do it** to ${kept.length ? 'keep the answers you already gave and take every other recommendation' : 'take every recommendation'} (recommended), or name an option from the lettered list.`,
`${asked.length} lettered choice${asked.length === 1 ? '' : 's'} below; do it ${kept.length ? `keeps the ${kept.length} you already answered and takes the recommendation for the rest` : 'takes the recommendation for each'}. Nothing is built until you answer.`,
];
}
const summary = eli5?.summary.trim() ? [`### ${ELI5_SECTION.title}`, '', eli5.summary.trim(), ''] : [];
const body = [...summary, ...sections.flatMap(s => [`### ${s.title}`, '', s.found?.body.trim() ? (eli5 ? s.found.body.trim() : capped(s.found.body.trim(), plan.file ?? 'plan.md')) : '_Not written for this plan yet._', ''])];
const need: string[] = ['### What we need from you', ''];
const letter = (n: number) => String.fromCharCode(65 + n);
const ordered = (i: GateItem) => [...i.options.filter(x => x.key === recKey(i)), ...i.options.filter(x => x.key !== recKey(i))];
const example = asked.findIndex(i => i.options.length > 1);
if (!pending) need.push('Nothing. This gate is answered.', '');
else if (missing.length) need.push('Nothing to answer yet: the plan goes out again once it is complete.', '');
else {
need.push(`One decision: approve the plan.`, '');
need.push(`- **rec (recommended):** ${p1 ? plainOption(recOption(p1)?.text ?? 'approve') : 'approve'}${items.length ? `, taking these ${kept.length ? 'answers' : 'recommendations'}:` : '.'}`);
items.forEach((i, n) => need.push(` ${n + 1}. ${stripRef(i.title)}: ${plainOption(recOption(i)?.text ?? recKey(i))}${kept.includes(i) ? keptNote(i) : recOption(i)?.changes_plan ? ' (changes the plan)' : ''}.`));
const example = items.find(i => i.options.length > 1);
const alt = example?.options.find(x => x.key !== recKey(example));
need.push(`- **Change something:** say which choice and what you want instead, in words${example && alt ? ` (for example "${clip(stripRef(example.title), 60)}: ${plainOption(alt.text)}")` : ''}. A change that alters the plan reopens its review before anything is built.`);
need.push('- **Reject:** nothing is built.', '');
if (!missing.length) need.push(`- **do it:** rec at full scope: every ${kept.length ? 'choice' : 'recommendation'} above, the follow-ons the plan names (docs, trains, cleanups) included, carried through to done with no further ask.`, '', 'Reply: **rec**, or **do it**', '');
if (o.captainName?.trim()) need.push(o.captainName.trim());
asked.forEach((i, n) => {
const opts = ordered(i);
const ask = i === p1 ? `Approve the plan "${title}"` : stripRef(i.title);
const note = kept.includes(i) ? keptNote(i) : opts[0]?.changes_plan ? ' (changes the plan)' : '';
const because = i.recommended_basis?.trim() ? ` (why: ${i.recommended_basis.trim()})` : '';
need.push(`- ${letter(n)}: ${ask}. We could: ${opts.map((x, k) => `${k + 1}) ${plainOption(x.text)}`).join('; ')}. Recommendation: ${plainOption(opts[0]?.text ?? recKey(i))}${note}${because}. ${letter(n)}1 to take recommendation.`);
});
need.push('', `**do it** takes every recommendation in this list at full scope, the follow-ons the plan names (docs, trains, cleanups) included, carried through to done with no further ask. A named option${example >= 0 ? ` (for example "${letter(example)}2")` : ''} takes that option instead. Anything else, say it in words: a change that alters the plan reopens its review before anything is built.`, '');
need.push(`Reply: **do it**${example >= 0 ? `, or name options such as **${letter(example)}2**` : ''}`, '');
}
const tokens = rec.map(c => `${c.id}${c.chosen}`).join(' ');
const trailer = `_For the relaying agent, not the person:_ run ${list.run}, gate_rev=${list.gate_rev}, rec_hash=${hash}; rec resolves to ${tokens}; plan ${plan.file ?? '(missing)'} (sha256 ${plan.sha}). Answer with ` +
const letters = pending && !missing.length ? asked.flatMap((i, n) => ordered(i).map((x, k) => `${letter(n)}${k + 1}=${i.id}${x.key}`)).join(' ') : '';
const trailer = `_For the relaying agent, not the person:_ run ${list.run}, gate_rev=${list.gate_rev}, rec_hash=${hash}; rec resolves to ${tokens}; "do it" answers as rec;${letters ? ` letters: ${letters}; a named option answers as \`--reply "rec except <token>"\`${example >= 0 ? ` (${letter(example)}2 = rec except ${asked[example]!.id}${ordered(asked[example]!)[1]!.key})` : ''};` : ''} plan ${plan.file ?? '(missing)'} (sha256 ${plan.sha}). Answer with ` +
`\`gstack-autoplan answer --out ${o.outDir} --gate-rev ${list.gate_rev} --reply rec --rec-hash ${hash} --by agent:<id> --owner-reply "<their exact words>"\`, then \`gstack-approvals import --autoplan ${o.outDir} --by <name>\`.`;
// The price sits above the trailer: the relaying agent sends everything above that line, so the person approving sees it.
const cost = inputs.run ? ['### Cost', '', gateCostSentence(gateCost(o.outDir, list, { phase_order: inputs.run.phase_order ?? [], outside_enabled: inputs.run.outside_enabled !== false, spend_cap_usd: inputs.run.spend_cap_usd ?? null, scope_flags: inputs.run.scope_flags })), ''] : [];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.91.342",
"version": "1.91.344",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+27 -18
View File
@@ -1,7 +1,7 @@
/**
* The Captain brief (lib/captain-brief.ts, gstack-owner-brief render --captain)
* and the shared heading parser (lib/markdown-sections.ts): the five-part shape,
* every choice rec takes shown in words, gate ids only on the agent line, the
* every choice as a lettered ask line (docs/captain.md § The ask format), gate ids only on the agent line, the
* rec_hash binding, and the incomplete and answered states.
*/
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
@@ -66,10 +66,10 @@ function run(plan: string, o: { answered?: boolean; urgent?: boolean } = {}): st
return dir;
}
function brief(dir: string) {
function brief(dir: string, captainName?: string) {
const loaded = loadBriefInputs(dir);
if ('error' in loaded) throw new Error(loaded.error.message);
return { ...renderCaptainBrief(loaded.inputs, { outDir: dir }), gate: loaded.inputs.gate };
return { ...renderCaptainBrief(loaded.inputs, { outDir: dir, captainName }), gate: loaded.inputs.gate };
}
describe('heading parser', () => {
@@ -84,29 +84,38 @@ describe('heading parser', () => {
});
describe('renderCaptainBrief', () => {
test('five parts in order, header first, every choice rec takes in words, ids only on the agent line', () => {
test('five parts in order, header first, one lettered ask line per choice with the recommendation as option 1, ids only on the agent line', () => {
const b = brief(run(FULL_PLAN));
expect(b.missing).toEqual([]);
const t = b.text;
expect(t.split('\n')[0]).toStartWith('**Decision needed:** approve the plan "Widget export fix". Reply **rec**');
const order = ['### Before → after', '### Why it matters', '### What caused this work', '### Big decisions', '### What we need from you', '- **do it:** rec at full scope', 'Reply: **rec**, or **do it**'].map(s => t.indexOf(s));
expect(t.split('\n')[0]).toStartWith('**Decision needed:** approve the plan "Widget export fix". Reply **do it**');
const order = ['### Before → after', '### Why it matters', '### What caused this work', '### Big decisions', '### What we need from you', '- A: ', '- B: ', '- C: ', '**do it** takes every recommendation in this list', 'Reply: **do it**, or name options such as **A2**'].map(s => t.indexOf(s));
expect(order.every((v, i) => v > 0 && (i === 0 || v > order[i - 1]!))).toBe(true);
expect(t).toContain('1. Split the exporter into its own module: keep the plan as written (a reviewer suggested changing it).');
expect(t).toContain('2. Cap memory at 256 MB: cap at 256 MB (changes the plan).');
expect(t).toContain('- A: Split the exporter into its own module. We could: 1) keep the plan as written (a reviewer suggested changing it); 2) make the reviewer\'s change. Recommendation: keep the plan as written (a reviewer suggested changing it). A1 to take recommendation.');
expect(t).toContain('- B: Cap memory at 256 MB. We could: 1) cap at 256 MB; 2) no cap. Recommendation: cap at 256 MB (changes the plan). B1 to take recommendation.');
expect(t).toContain('- C: Approve the plan "Widget export fix". We could: 1) approve, with the plan changes marked above; 2) approve the plan as reviewed;');
expect(t).toContain('A named option (for example "A2") takes that option instead.');
expect(t).not.toMatch(/^GStack Minder$/m);
expect(brief(run(FULL_PLAN), 'GStack Minder').text).toMatch(/^GStack Minder\n- A: /m);
const [ownerPart, agentPart] = t.split('\n---\n');
for (const item of b.gate.items) expect(ownerPart).not.toMatch(new RegExp(`\\b${item.id}[a-e]?\\b`));
expect(ownerPart).not.toContain('gate_rev');
expect(ownerPart).not.toContain('gstack-autoplan revise');
expect(agentPart).toContain(`rec_hash=${recHash(b.gate)}`);
expect(agentPart).toContain('rec resolves to d1a d2b p1b');
expect(agentPart).toContain('rec resolves to d1a d2b p1b; "do it" answers as rec; letters: A1=d1a A2=d1b B1=d2b B2=d2a C1=p1b C2=p1a C3=p1c C4=p1d C5=p1e; a named option answers as `--reply "rec except <token>"` (A2 = rec except d1b);');
const named = parseReply('rec except d1b', b.gate);
expect(named.valid).toBe(true);
expect(named.pending).toEqual([]);
expect(named.choices.find(c => c.id === 'd1')?.chosen).toBe('b');
});
test('a plan missing a section is not ready for approval and invites no reply', () => {
const b = brief(run(FULL_PLAN.replace('## Why it matters', '## Motivation')));
expect(b.missing).toEqual(['Why it matters']);
expect(b.text).toStartWith('**Not ready for approval:** the plan "Widget export fix" is missing Why it matters.');
expect(b.text).not.toContain('Reply: **rec**');
expect(b.text).not.toContain('**do it:**');
expect(b.text).not.toContain('Reply: **do it**');
expect(b.text).not.toContain('**do it** takes');
expect(b.text).not.toContain('- A: ');
expect(b.text).toContain('_Not written for this plan yet._');
});
@@ -201,11 +210,11 @@ describe('carried decisions at a cap gate (#3546)', () => {
gate.items.filter(i => i.id !== 'p1').forEach((item, n) => {
const text = item.options.find(o => o.key === gateRec[item.id])!.text;
expect(gateText).toContain(` ${item.id}${gateRec[item.id]}) ${text}`);
expect(briefText).toContain(` ${n + 1}. ${item.title.replace(/\s*\[[^\]]*\]\s*$/, '')}: ${WORDS[text] ?? text}`);
expect(briefText).toContain(`- ${String.fromCharCode(65 + n)}: ${item.title.replace(/\s*\[[^\]]*\]\s*$/, '')}. We could: 1) ${WORDS[text] ?? text};`);
});
expect(briefText).toContain("Use a queue table: make the reviewer's change (your answer at gate_rev 1, carried forward).");
expect(briefText).toContain('rec keeps the 3 you already answered and takes the recommended answer for the rest.');
expect(briefText).toContain('Reply **rec** to keep the answers you already gave and take every other recommendation (recommended)');
expect(briefText).toContain("- A: Use a queue table. We could: 1) make the reviewer's change; 2) keep the plan as written (a reviewer suggested changing it). Recommendation: make the reviewer's change (your answer at gate_rev 1, carried forward). A1 to take recommendation.");
expect(briefText).toContain('do it keeps the 3 you already answered and takes the recommendation for the rest.');
expect(briefText).toContain('Reply **do it** to keep the answers you already gave and take every other recommendation (recommended)');
});
test('an answer already given at this gate is what rec keeps, as gstack-autoplan answer --reply rec records it', () => {
@@ -215,7 +224,7 @@ describe('carried decisions at a cap gate (#3546)', () => {
expect(partial.stdout).toMatch(/^GATE_PENDING: /m);
const { briefText, briefRec } = renders(copy);
expect(briefRec).toMatchObject({ d1: 'b', d2: 'a', d3: 'b' });
expect(briefText).toContain('Add metrics: keep the plan as written (a reviewer suggested changing it) (your answer at this gate).');
expect(briefText).toContain('Recommendation: keep the plan as written (a reviewer suggested changing it) (your answer at this gate). B1 to take recommendation.');
const answered = r.ap(['answer', '--by', 'human', '--out', copy, '--gate-rev', '2', '--reply', 'rec']);
expect(answered.stdout).toMatch(/^APPROVED: gate_rev=2/m);
const recorded = fs.readFileSync(path.join(copy, 'decisions.jsonl'), 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)).filter(d => d.gate_rev === 2);
@@ -234,8 +243,8 @@ describe('carried decisions at a cap gate (#3546)', () => {
const { briefText, gateRec, briefRec } = renders(copy);
expect(briefRec).toEqual(gateRec);
expect(gateRec).toEqual(Object.fromEntries(gate.items.map(i => [i.id, i.recommended])));
expect(briefText).toContain('Use a queue table: keep the plan as written (a reviewer suggested changing it).');
expect(briefText).toContain('- A: Use a queue table. We could: 1) keep the plan as written (a reviewer suggested changing it); 2) make the reviewer\'s change. Recommendation: keep the plan as written (a reviewer suggested changing it). A1 to take recommendation.');
expect(briefText).not.toContain('carried forward');
expect(briefText).toContain('rec takes the recommended answer for each.');
expect(briefText).toContain('do it takes the recommendation for each.');
});
});