v1.91.253.0 feat(captain): /captain skill (also /crew), crew templates and docs/captain.md for running a crew from one controller (#3473)

* feat(captain): /captain skill (also /crew), crew templates and docs/captain.md for running a crew from one controller

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

* chore(captain): regenerate SKILL.md at main's version

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

* feat(captain): AGENTS.md and NOTES.md templates, roles, ship lane sequence, owner device and lessons loop in docs/captain.md; owner-brief closes with the do-it reply

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

* fix(captain): review pass 1: 'do it' parses as rec, READY template matches the tool's lines, slot-2 and freeze wording, mergeChangelogs named, parameter lists and doc numbers aligned

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

* fix(captain): align the slot-call BASE merge, freeze repair and severe-race wording across skill, CREW.md and docs; router routes on phrases; receipt head width as printed

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

* feat(captain): CREW.md template becomes pointers plus parameters; every crew rule gets a canonical home in gstack (lane discipline, outside contributions, stale READYs, standing spend, the lane side of flaky reruns in docs/captain.md; the rule-to-home map)

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

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-26 18:04:22 +00:00
1 parent 8663c3d25f
commit 4d48d39e51
40 files changed
+1923 -22

No files matched your search

+10 -2
View File
@@ -10,7 +10,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { capyRelayLine, renderCaptainBrief } from '../lib/captain-brief';
import { recHash, type GateList } from '../lib/gate-list';
import { parseReply, recHash, type GateList } from '../lib/gate-list';
import { findSection, headings } from '../lib/markdown-sections';
import { loadBriefInputs } from '../lib/owner-brief';
@@ -87,7 +87,7 @@ describe('renderCaptainBrief', () => {
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', 'Reply: **rec**'].map(s => t.indexOf(s));
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(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).');
@@ -104,9 +104,17 @@ describe('renderCaptainBrief', () => {
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).toContain('_Not written for this plan yet._');
});
test('"do it", the advertised closing reply, parses as rec', () => {
const gate = JSON.parse(fs.readFileSync(path.join(run(FULL_PLAN), 'gate.json'), 'utf8')) as GateList;
const parsed = parseReply('do it', gate, gate.gate_rev);
expect(parsed.valid).toBe(true);
expect(parsed.mode).toBe('rec');
});
test('an answered gate says nothing is pending; urgent findings come first', () => {
expect(brief(run(FULL_PLAN, { answered: true })).text).toStartWith('**Nothing pending.** Already decided on 2026-10-20: approved with the recommended plan changes (“rec”).');
expect(brief(run(FULL_PLAN, { urgent: true })).text).toStartWith('## URGENT, outside this plan');
+93
View File
@@ -0,0 +1,93 @@
/**
* /captain: the crew operating model as a skill plus templates.
*
* Pins the contract the skill teaches (one controller merges, the READY block
* fields, the train loop, the digest shape, the approval shapes, the flaky
* pass), the template set a team copies, and that everything shipped is
* generic: no crew codes, no person's name, parameters instead of repo names.
*/
import { describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as path from 'node:path';
const ROOT = path.resolve(import.meta.dir, '..');
const read = (rel: string) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
const skill = read('captain/SKILL.md');
const tmpl = read('captain/SKILL.md.tmpl');
const flat = skill.replace(/\s+/g, ' ');
const TEMPLATES = ['CREW.md', 'AGENTS.md', 'NOTES.md', 'DIGEST.md', 'flaky-ledger.md', 'report.md', 'READY.md', 'slot-call.md', 'plan-ask.md', 'merge-ask.md', 'automation-flaky-pass.md'];
describe('/captain skill', () => {
test('frontmatter names the skill and its /crew alias trigger', () => {
expect(tmpl.startsWith('---\nname: captain\n')).toBe(true);
expect(tmpl).toMatch(/^triggers:\n(?: - .*\n)* - crew\n/m);
expect(flat).toContain('`/crew` is the same skill');
});
test('teaches the one rule and the READY contract fields', () => {
expect(skill).toContain('The controller merges; a lane never merges');
for (const field of ['READY #<n> <sha>', 'STAMP_VERSION', 'PREGATE', 'SHIP_RECEIPT', 'READY_RECEIPT', 'full 40-character head SHA', 'head frozen from now on']) {
expect(skill, field).toContain(field);
}
expect(flat).toContain('A green list state, a passing PR page, a "looks good" in chat and a cheap-tier pass are not READYs');
});
test('teaches the train loop, the red-train rules and the backstop', () => {
for (const step of ['train build', 'train verdict', 'train bisect', 'train rebuild', 'train prune', 'verify --pr', '--match-head-commit', 'FLAKY N', 'exactly one unchanged', 'FREEZE: cleared', 'The tree rule', 'Gate-ahead (option)', 'Severe race (option)', 'beyond `STAMP_FILES`']) {
expect(skill, step).toContain(step);
}
});
test('teaches the digest shape, the three approval shapes and the flaky pass', () => {
for (const s of ['**Needs you**', '**New**', '**In progress**', '**A. The per-thread approval record.**', '**B. The plan ask**', '**C. The merge ask.**', '"do it"', 'flake-records artifacts', 'red on `BASE` and green with the fix', 'KNOWN_INFRA_FLAKES']) {
expect(flat, s).toContain(s);
}
expect(flat).toContain('no thresholds, trial counts, case kinds or budgets change to turn a red green');
});
test('ships every template the skill and docs name', () => {
const doc = read('docs/captain.md');
for (const t of TEMPLATES) {
expect(fs.existsSync(path.join(ROOT, 'captain', 'templates', t)), t).toBe(true);
expect(skill.includes('captain/templates/' + t) || doc.includes('captain/templates/' + t), t + ' is referenced').toBe(true);
}
const digest = read('captain/templates/DIGEST.md');
for (const h of ['## Needs you', '## New', '## In progress']) expect(digest).toContain(h);
const report = read('captain/templates/report.md');
for (const h of ['## Outcome', '## PR', '## Needs', '## Cost', '## Lesson', '## Approvals']) expect(report).toContain(h);
expect(read('captain/templates/READY.md')).toContain('SHIP_RECEIPT: pr=<n> head=<sha12>');
expect(read('captain/templates/slot-call.md')).toContain('STAMP_VERSION=<X>');
expect(read('captain/templates/flaky-ledger.md')).toMatch(/## Open[\s\S]*## Fixed[\s\S]*## Won't fix/);
const crew = read('captain/templates/CREW.md');
expect(crew).toMatch(/^23\. /m);
expect(crew).not.toMatch(/^24\. /m);
for (let n = 1; n <= 23; n++) expect(crew, 'rule ' + n + ' names its home').toMatch(new RegExp('^' + n + '\\. \\*\\*[^]*?Home:', 'm'));
for (let n = 1; n <= 23; n++) expect(doc, 'docs/captain.md maps rule ' + n).toMatch(new RegExp('^\\| ' + n + ' \\| ', 'm'));
for (const h of ['## Lane discipline', '## Contributions from outside the crew']) expect(doc).toContain(h);
expect(read('captain/templates/plan-ask.md')).toContain('**do it**');
expect(read('captain/templates/automation-flaky-pass.md')).toContain('never merge');
});
test('is generic: parameters, not one crew\'s codes, names or repository', () => {
const files = ['captain/SKILL.md.tmpl', 'docs/captain.md', ...TEMPLATES.map(t => 'captain/templates/' + t)];
for (const f of files) {
const text = read(f);
expect(text, f).not.toMatch(/\b(?:GSTA|GBRA)-\d+\b/);
expect(text, f).not.toMatch(/\bGarry\b/);
expect(text, f).not.toMatch(/garrytan\/gstack/);
}
for (const p of ['REPO', 'BASE', 'REQUIRED_CHECKS', 'FULL_GATE_CHECK', 'MERGE_METHOD', 'STAMP_FILES', 'KNOWN_INFRA_FLAKES']) {
expect(read('captain/templates/CREW.md'), p).toContain(p);
expect(skill, p).toContain(p);
}
});
test('docs/captain.md links resolve and the skill is registered where skills are listed', () => {
const doc = read('docs/captain.md');
const links = [...doc.matchAll(/\]\(([^)#\s]+)(?:#[^)]*)?\)/g)].map(m => m[1]).filter(l => !/^https?:/.test(l));
expect(links.length).toBeGreaterThan(5);
for (const l of links) expect(fs.existsSync(path.resolve(ROOT, 'docs', l)), l).toBe(true);
for (const f of ['README.md', 'AGENTS.md', 'docs/skills.md', 'SKILL.md.tmpl', 'docs/PROJECT_STRUCTURE.md']) expect(read(f), f).toMatch(/\/captain\b/);
expect(read('README.md')).toContain('docs/captain.md');
});
});
+9 -1
View File
@@ -46,8 +46,16 @@ import { skillCensus } from './helpers/skill-census';
* New-skill ratchet: previous ceiling 1,194 + ceil(10 / 4) = 1,197
* token-equivalents (4,788 bytes), leaving 2 bytes. Existing descriptions
* are unchanged.
* ref captain addition on base ab9a854b (2026-10-26)
* result pre-addition aggregate 4,786 bytes; captain adds 214 bytes (name
* plus the one-sentence catalog lead naming the /crew alias),
* yielding 5,000 bytes = 1,250 token-equivalents including the root
* router alias.
* New-skill ratchet: previous ceiling 1,197 + ceil(214 / 4) = 1,251
* token-equivalents (5,004 bytes), leaving 4 bytes. Existing descriptions
* are unchanged.
*/
const CATALOG_BUDGET_TOKEN_EQUIVALENTS = 1_197;
const CATALOG_BUDGET_TOKEN_EQUIVALENTS = 1_251;
// Largest today: design-consultation at 229 bytes. A description that needs
// more than 260 bytes is a body paragraph, not a catalog entry.
+2 -1
View File
@@ -8,6 +8,7 @@
"browse": 8943,
"browser-skills/hackernews-frontpage": 371,
"canary": 13441,
"captain": 12011,
"capy/skill/gstack": 3498,
"careful": 1239,
"codex": 15594,
@@ -25,7 +26,7 @@
"document-release": 10092,
"eval-plan": 9811,
"freeze": 1115,
"gstack": 4821,
"gstack": 5326,
"gstack-upgrade": 6212,
"guard": 1000,
"health": 11440,