mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
feat(captain): every ask to the owner uses the lettered ask format (R41)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
53522b1bbc
commit
5373aaa275
15 files changed
+150
-93
No files matched your search
@@ -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);
|
||||
|
||||
+13
-7
@@ -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,17 @@ 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. 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
|
||||
|
||||
+13
-7
@@ -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,17 @@ 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. 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -24,4 +24,5 @@ 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."
|
||||
@@ -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,13 @@ 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.
|
||||
```
|
||||
|
||||
When the answer comes back, record it before the work starts. For an autoplan gate:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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, Merge trains]
|
||||
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.
|
||||
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.** 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 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. [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; 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. 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, don't estimate. [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]
|
||||
+25
-9
@@ -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,35 @@ 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.
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -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
@@ -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))];
|
||||
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.`);
|
||||
});
|
||||
const example = asked.findIndex(i => i.options.length > 1);
|
||||
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};` : ''} 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 })), ''] : [];
|
||||
|
||||
+21
-16
@@ -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';
|
||||
@@ -84,29 +84,34 @@ 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(renderCaptainBrief(loadBriefInputs(run(FULL_PLAN)).inputs, { outDir: run(FULL_PLAN), captainName: '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;');
|
||||
});
|
||||
|
||||
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 +206,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 +220,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 +239,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.');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user