@dougharness/cli 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +13 -4
- package/dist/bin.js.map +1 -1
- package/dist/board-page.d.ts +5 -1
- package/dist/board-page.js +19 -5
- package/dist/board-page.js.map +1 -1
- package/dist/board-serve.js +34 -10
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +160 -13
- package/dist/board.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/implementer.md +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +1 -1
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -1
- package/node_modules/@dougharness/flow/agents/reviewer.md +3 -1
- package/node_modules/@dougharness/flow/hooks/register.tsx +67 -32
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +94 -7
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +31 -4
- package/node_modules/@dougharness/flow/lib/board.mjs +366 -21
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +229 -23
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +25 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +9 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/memory.mjs +382 -65
- package/node_modules/@dougharness/flow/lib/plan.mjs +1 -1
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +228 -14
- package/node_modules/@dougharness/flow/scripts/board.mjs +122 -13
- package/node_modules/@dougharness/flow/scripts/cost.mjs +6 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +27 -8
- package/node_modules/@dougharness/flow/scripts/plan.mjs +5 -1
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +11 -4
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +9 -2
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +2 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +2 -2
- package/node_modules/@dougharness/flow/workflows/card-queen.js +238 -25
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +77 -34
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/baseline.mjs +23 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +41 -3
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +6 -2
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +119 -15
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +107 -20
- package/package.json +3 -3
- package/templates/board-app.js +240 -30
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
// (card memory-every-session) recall for a hand-track card's
|
|
65
65
|
// briefs: query = the card's title and goal, k 8. Prints the
|
|
66
66
|
// workflow's lessons header, the rendered `- [id] ...` lines
|
|
67
|
-
// (capped at 2000 chars like plan.mjs), then
|
|
67
|
+
// (capped at 2000 chars like plan.mjs), then the window line
|
|
68
|
+
// when lessons were hidden, then `memoryUsed: <ids>`;
|
|
68
69
|
// writes .doug/.state/reports/<card-id>/lessons.json. No database
|
|
69
70
|
// -> "No lessons recalled for <id>." (database not created);
|
|
70
71
|
// a database or provider error -> one stderr line, exit 0;
|
|
@@ -97,7 +98,11 @@
|
|
|
97
98
|
// 0); --dry-run applies nothing but still runs the LLM pass and
|
|
98
99
|
// prints what would happen. Cost is measured from the same local
|
|
99
100
|
// transcripts cost.mjs reads, falling back to the CLI's own
|
|
100
|
-
// cost estimate.
|
|
101
|
+
// cost estimate. For each appended lesson with no CLAUDE.md home
|
|
102
|
+
// it prints `claude-md candidate for lesson <id>: <command>`
|
|
103
|
+
// (edits file under .doug/.state/learn/claude-md-candidates/,
|
|
104
|
+
// nothing under --dry-run) or `... dropped: <reason>`; --json
|
|
105
|
+
// carries claudeMd and claudeMdDropped. Needs Node >=22.16 (node:sqlite).
|
|
101
106
|
// memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "<R<n> or <card-id>/R<n>>=<class>[: <reason>]"]...
|
|
102
107
|
// [--harmful <id>]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
103
108
|
// (card memory-every-session) the same pass for a hand landing:
|
|
@@ -776,6 +781,14 @@ try {
|
|
|
776
781
|
for (const r of result.rejected) {
|
|
777
782
|
process.stdout.write(` rejected ${r.text ? JSON.stringify(r.text) : "(no text)"}: ${r.reason}\n`);
|
|
778
783
|
}
|
|
784
|
+
for (const c of result.claudeMd || []) {
|
|
785
|
+
process.stdout.write(
|
|
786
|
+
result.dryRun
|
|
787
|
+
? `claude-md candidate for lesson ${c.lessonId} (dry run, not written)\n`
|
|
788
|
+
: `claude-md candidate for lesson ${c.lessonId}: ${c.command}\n`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
for (const d of result.claudeMdDropped || []) process.stdout.write(`claude-md candidate for lesson ${d.lessonId} dropped: ${d.reason}\n`);
|
|
779
792
|
}
|
|
780
793
|
break;
|
|
781
794
|
}
|
|
@@ -937,7 +950,7 @@ try {
|
|
|
937
950
|
}
|
|
938
951
|
const provider = createProvider(readMemoryConfig(dir));
|
|
939
952
|
const staleDays = readMemoryStaleDays(dir);
|
|
940
|
-
const { openMemory, recallLessons, degradedWarning, detectLocalProvider } = await import("../lib/memory.mjs");
|
|
953
|
+
const { openMemory, recallLessons, degradedWarning, detectLocalProvider, windowNote } = await import("../lib/memory.mjs");
|
|
941
954
|
// Probed once, and only when no provider is configured; a failed probe is just "not detected".
|
|
942
955
|
const detectedProvider = provider ? null : await detectLocalProvider();
|
|
943
956
|
const m = openMemory(dir);
|
|
@@ -956,7 +969,8 @@ try {
|
|
|
956
969
|
result.mode === "hybrid"
|
|
957
970
|
? `recall: hybrid via ${result.provider} ${provider.model}/${provider.dims}`
|
|
958
971
|
: `recall: keyword-only (${result.reason})`;
|
|
959
|
-
const
|
|
972
|
+
const note = windowNote(result.hidden, staleDays);
|
|
973
|
+
const lines = [header, ...(note ? [note] : []), ...result.lessons.map((l) => `${l.id} ${l.score.toFixed(4)} ${l.kind} ${l.text}`)];
|
|
960
974
|
process.stdout.write(lines.join("\n") + "\n");
|
|
961
975
|
}
|
|
962
976
|
break;
|
|
@@ -979,13 +993,14 @@ try {
|
|
|
979
993
|
}
|
|
980
994
|
const query = `${card.title}\n${card.goal ?? ""}`;
|
|
981
995
|
const header = "Lessons from earlier runs on this repository (memory; each cites where it was learned):";
|
|
982
|
-
const saved = { card: id, query, mode: null, reason: null, ids: [], text: null, recalledAt: new Date().toISOString() };
|
|
996
|
+
const saved = { card: id, query, mode: null, reason: null, ids: [], text: null, recalledAt: new Date().toISOString(), hidden: null };
|
|
997
|
+
let note = null;
|
|
983
998
|
let error = null;
|
|
984
999
|
if (existsSync(join(dir, ".doug", ".state", "memory", "memory.db"))) {
|
|
985
1000
|
try {
|
|
986
1001
|
const provider = createProvider(readMemoryConfig(dir));
|
|
987
1002
|
const staleDays = readMemoryStaleDays(dir);
|
|
988
|
-
const { openMemory, recallLessons, degradedWarning, degradedReason, detectLocalProvider, renderLessons } = await import("../lib/memory.mjs");
|
|
1003
|
+
const { openMemory, recallLessons, degradedWarning, degradedReason, detectLocalProvider, renderLessons, windowNote } = await import("../lib/memory.mjs");
|
|
989
1004
|
const detectedProvider = provider ? null : await detectLocalProvider();
|
|
990
1005
|
const m = openMemory(dir);
|
|
991
1006
|
try {
|
|
@@ -994,6 +1009,8 @@ try {
|
|
|
994
1009
|
if (warning) process.stderr.write(warning + "\n");
|
|
995
1010
|
const rendered = renderLessons(result.lessons);
|
|
996
1011
|
saved.mode = result.mode;
|
|
1012
|
+
saved.hidden = result.hidden ?? null;
|
|
1013
|
+
note = windowNote(result.hidden, staleDays);
|
|
997
1014
|
saved.reason = result.degraded ? degradedReason(result, detectedProvider) : null;
|
|
998
1015
|
saved.ids = rendered.ids;
|
|
999
1016
|
saved.text = rendered.text === null ? null : result.degraded ? `(recall was keyword-only: ${saved.reason})\n${rendered.text}` : rendered.text;
|
|
@@ -1006,6 +1023,8 @@ try {
|
|
|
1006
1023
|
saved.reason = null;
|
|
1007
1024
|
saved.ids = [];
|
|
1008
1025
|
saved.text = null;
|
|
1026
|
+
saved.hidden = null;
|
|
1027
|
+
note = null;
|
|
1009
1028
|
saved.error = error;
|
|
1010
1029
|
process.stderr.write(`[doug] memory recall skipped: ${error}\n`);
|
|
1011
1030
|
}
|
|
@@ -1016,9 +1035,9 @@ try {
|
|
|
1016
1035
|
if (opts.json) {
|
|
1017
1036
|
process.stdout.write(JSON.stringify(saved, null, 2) + "\n");
|
|
1018
1037
|
} else if (saved.ids.length) {
|
|
1019
|
-
process.stdout.write([header, saved.text, `memoryUsed: ${saved.ids.join(", ")}`].join("\n") + "\n");
|
|
1038
|
+
process.stdout.write([header, saved.text, ...(note ? [note] : []), `memoryUsed: ${saved.ids.join(", ")}`].join("\n") + "\n");
|
|
1020
1039
|
} else {
|
|
1021
|
-
process.stdout.write(`No lessons recalled for ${id}.\n`);
|
|
1040
|
+
process.stdout.write(`No lessons recalled for ${id}.\n${note ? note + "\n" : ""}`);
|
|
1022
1041
|
}
|
|
1023
1042
|
break;
|
|
1024
1043
|
}
|
|
@@ -265,7 +265,7 @@ async function attachCodeContext(plan, dir, provider) {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
const SETTABLE = ["install", "baseBranch", "integrationBranch", "integrationWorktree", "adversary.command", "adversary.fallback", "card", "fixAttempts", "swarm", "workerCheck"];
|
|
268
|
+
const SETTABLE = ["install", "baseBranch", "integrationBranch", "integrationWorktree", "adversary.command", "adversary.fallback", "card", "fixAttempts", "swarm", "workerCheck", "crew.testers"];
|
|
269
269
|
|
|
270
270
|
const [cmd, ...argsRaw] = process.argv.slice(2);
|
|
271
271
|
// --card=<id> is the same as --card <id>.
|
|
@@ -520,6 +520,10 @@ switch (cmd) {
|
|
|
520
520
|
// Card swarm-topology (candidate 3): a deterministic per-worker check before the merge, behind the swarm opt-in.
|
|
521
521
|
if (!/^(on|off|true|false)$/i.test(value.trim())) fail(`plan.mjs set workerCheck: the value must be on or off, got ${JSON.stringify(value)}`, 2);
|
|
522
522
|
next.workerCheck = /^(on|true)$/i.test(value.trim());
|
|
523
|
+
} else if (key === "crew.testers") {
|
|
524
|
+
// Card crew-tester-seat: how many verifier seats run in parallel per full-shape task.
|
|
525
|
+
if (!/^[1-9]\d*$/.test(value.trim())) fail(`plan.mjs set crew.testers: the value must be a positive integer, got ${JSON.stringify(value)}`, 2);
|
|
526
|
+
next.crew = { ...(plan.crew && typeof plan.crew === "object" && !Array.isArray(plan.crew) ? plan.crew : {}), testers: Number(value.trim()) };
|
|
523
527
|
} else if (key === "fixAttempts") {
|
|
524
528
|
if (!/^\d+$/.test(value)) fail(`plan.mjs set fixAttempts: the value must be a non-negative integer, got ${JSON.stringify(value)}`, 2);
|
|
525
529
|
next.fixAttempts = Number(value);
|
|
@@ -63,7 +63,7 @@ async function run() {
|
|
|
63
63
|
// Checked before anything opens it: a missing database is never created here.
|
|
64
64
|
if (!existsSync(join(dir, ".doug", ".state", "memory", "memory.db"))) return;
|
|
65
65
|
|
|
66
|
-
const { openMemory, recallLessons, topLessons, renderLessons, degradedReason } = await import("../lib/memory.mjs");
|
|
66
|
+
const { openMemory, recallLessons, topLessons, renderLessons, degradedReason, windowHidden, windowNote } = await import("../lib/memory.mjs");
|
|
67
67
|
const { readMemoryConfig, readMemoryStaleDays, createProvider } = await import("../lib/embeddings.mjs");
|
|
68
68
|
const { loadBoard } = await import("../lib/board.mjs");
|
|
69
69
|
|
|
@@ -78,6 +78,7 @@ async function run() {
|
|
|
78
78
|
let lessons;
|
|
79
79
|
let header;
|
|
80
80
|
let scope;
|
|
81
|
+
let hidden = null;
|
|
81
82
|
try {
|
|
82
83
|
if (cards.length) {
|
|
83
84
|
const query = cards.map((c) => `${c.title}\n${c.goal || ""}`).join("\n");
|
|
@@ -101,10 +102,12 @@ async function run() {
|
|
|
101
102
|
? `recall: hybrid via ${result.provider} ${provider.model}/${provider.dims}`
|
|
102
103
|
: `recall: keyword-only (${degradedReason(result)})`;
|
|
103
104
|
lessons = result.lessons;
|
|
105
|
+
hidden = result.hidden;
|
|
104
106
|
} else {
|
|
105
107
|
scope = "the repository: top by helpful-harmful";
|
|
106
108
|
header = null;
|
|
107
109
|
lessons = topLessons(m, { k: K, staleDays });
|
|
110
|
+
hidden = windowHidden(m, { staleDays });
|
|
108
111
|
}
|
|
109
112
|
} finally {
|
|
110
113
|
// A raced-out hybrid recall may still be running; closing the handle makes it fail into its catch.
|
|
@@ -115,10 +118,14 @@ async function run() {
|
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
const rendered = renderLessons(lessons);
|
|
118
|
-
|
|
121
|
+
const note = windowNote(hidden, staleDays);
|
|
122
|
+
if (rendered.text === null && !note) return;
|
|
119
123
|
const additionalContext =
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
rendered.text === null
|
|
125
|
+
? `[doug] ${note}`
|
|
126
|
+
: `[doug] Lessons from this repository's memory (${header ? `${header}; ` : ""}${scope}):\n${rendered.text}\n` +
|
|
127
|
+
(note ? `${note}\n` : "") +
|
|
128
|
+
"Each line is [lesson id] text (citation). When you brief an agent, paste the lessons that apply and list their ids as memoryUsed.";
|
|
122
129
|
await write({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext } });
|
|
123
130
|
}
|
|
124
131
|
|
|
@@ -68,7 +68,7 @@ Then recall memory for the card: `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs
|
|
|
68
68
|
|
|
69
69
|
On the lean lane and on a tests-only card, every brief carries the same `brief-lessons` block.
|
|
70
70
|
|
|
71
|
-
Then find the test file that covers each module the goal touches: the project's test skill when it has one, otherwise grep for the module's name under the test directories. Follow any procedure CLAUDE.md names for changes of this kind (its test table, its rules for generated or copied files, its type declarations), and put those rules into the brief: read the goal as the acceptance list, name the test file you found — when the project has a tester agent, the tester writes or extends that file first, from the goal; the coder runs only that file while iterating and does not write tests — and the mutation list step 3 demands (one mutation per case the card's goal names, each with the test that must fail). A card whose acceptance is that a mechanism exists is proved by mutating it: the coder applies each listed mutation, confirms the named test fails, reverts it, and reports which assertion failed; a card that only changes prose, a message, or a docs line needs none. The reviewer, on every card, reruns the brief's mutation list and adds its own. A tests-only card's tester never edits a production file in the live checkout. The coder and the reviewer apply each mutation in a scratch copy or git worktree under `.doug/.state`, run the one test file there, and remove the copy. Keep the change to what the card asks: a defect found on the way becomes its own hand-track card. On facts: a fact the brief states about the workflow, a hook contract, or a model row is copied verbatim from CLAUDE.md, README, docs, or the code with its path, never paraphrased; a claim the lead cannot cite goes into the brief as unverified.
|
|
71
|
+
Then find the test file that covers each module the goal touches: the project's test skill when it has one, otherwise grep for the module's name under the test directories. Follow any procedure CLAUDE.md names for changes of this kind (its test table, its rules for generated or copied files, its type declarations), and put those rules into the brief: read the goal as the acceptance list, name the test file you found — when the project has a tester agent, the tester writes or extends that file first, from the goal; the coder runs only that file while iterating and does not write tests — and the mutation list step 3 demands (one mutation per case the card's goal names, each with the test that must fail). A mutation is a code change to a file in the card worktree that a test catches; a check that is not a code mutation (a git diff, a file-absence check) goes in reviewerChecks, never in mutations. A card whose acceptance is that a mechanism exists is proved by mutating it: the coder applies each listed mutation, confirms the named test fails, reverts it, and reports which assertion failed; a card that only changes prose, a message, or a docs line needs none. The reviewer, on every card, reruns the brief's mutation list and adds its own. A tests-only card's tester never edits a production file in the live checkout. The coder and the reviewer apply each mutation in a scratch copy or git worktree under `.doug/.state`, run the one test file there, and remove the copy. Keep the change to what the card asks: a defect found on the way becomes its own hand-track card. On facts: a fact the brief states about the workflow, a hook contract, or a model row is copied verbatim from CLAUDE.md, README, docs, or the code with its path, never paraphrased; a claim the lead cannot cite goes into the brief as unverified. Every tester, coder, and reviewer brief carries the no-CPU-burners rule verbatim: A burner is any process started only to load the machine (yes > /dev/null, stress, a busy loop), bounded or not; concurrent runs of the project's test command (the commands in `stopGate.commands`) are not burners. Never start a burner without the user's yes: a seat that wants one stops and returns a designQuestion naming the load it would start (the process, how many, the timeout, and why), and starts it only after an answer from the user says yes. Before hand-back, confirm with ps that no load process you started survives, orphans reparented to PID 1 included, and kill any that does. When a seat returns a question asking to start a burner, the lead asks the user and resumes the seat with the answer; no burner starts before a yes.
|
|
72
72
|
|
|
73
73
|
Next: before the coder, spawn the project's `tester` agent (`.claude/agents/tester.md`, on the `implement` row) with the goal as the acceptance list and the test file step 3 found, so the failing tests are written from the card, not from the code — the Agent tool naming the project's `tester` agent, `model` set to the `implement` row's model, the same way the coder runs on that row. Tell the tester to end its report with the `tests_red_by_design` claim naming the test files it left red, so the SubagentStop gate lets it stop; the lead's own Stop stays red until the coder's change lands. The claim may sit in the tester's final plain-text message or in its hand-back call; the gate reads the plain text first, then the last hand-back. The tester is spawned in the foreground, never in the background: the coder starts from its red tests.
|
|
74
74
|
|
|
@@ -98,16 +98,19 @@ Ask with `AskUserQuestion` whether to take the next Ready card. Yes: go to step
|
|
|
98
98
|
|
|
99
99
|
`/doug-next --auto [n]` takes the first n runnable Ready cards of either track in board order (`board.mjs next --batch <n> --track any`). With n, the queue is fixed at the approval. Without n, autopilot is unlimited: whenever a slot is free it re-reads Ready with `autopilot.mjs pick` and takes the next runnable Ready cards in board order. It never takes a Backlog card: follow-ups still go to Backlog, and a card the user promotes to Ready during the run is picked up. It shows the queue once, each card's id, size, track, and class, with the caps: cards, wall clock, and USD. The caps default to the queue's length in cards, the wall cap that board.mjs next --batch prints (the sum of each card's budget from the size x class wall budget table in board.mjs, at the p80 of measured landings: a size/class cell with fewer than 5 landings falls back to its size's all-class figure, a card with no class uses its size's all-class figure, and a card with no size counts as M), and the usd cap it prints beside it (the sum of each card's budget from the size x class usd budget table in board.mjs, at the p80 of measured cost over flow and hand landings, with the same fallbacks), and the user may change them at the approval. The USD cap has no fixed default and no floor: it is the sum of the per-card budgets, and the user may raise it at the approval. Unlimited autopilot shows the runnable Ready cards at the approval, and the approval covers every card it later takes from Ready. Unlimited autopilot has no card cap: its wall cap and its USD cap grow as each card is taken, by that card's budget from the same size x class tables (`board.mjs budgets` and `board.mjs budgets --usd` print them); only the totals are checked on every queen notification. When the measured walls or costs drift, the lead reruns `board.mjs budgets --from docs/live-runs.md` and `board.mjs budgets --usd --from docs/live-runs.md`, which recompute the tables and print each cell's n, and replaces each table in the `board.mjs` source with the printed one. Autopilot asks one approval, for the whole queue, and no other gate: no start gate, no shape question, and no continue gate.
|
|
100
100
|
|
|
101
|
+
`/doug-next --milestone <id>` passes the id to `board.mjs next --milestone <id>`, and `--auto [n] --milestone <id>` adds `--milestone <id>` to that `board.mjs next --batch <n> --track any` command and to `autopilot.mjs pick`, so only that milestone's Ready cards launch. The closing summary also prints `board.mjs rollup --milestone <id>`. Autopilot never changes a milestone's status.
|
|
102
|
+
|
|
101
103
|
A card the user asks for during the run is not a gate: the lead scopes and files it as "Cards the user asks for" says, and the queue resumes.
|
|
102
104
|
|
|
103
105
|
Each card still runs its track unchanged (research when needed, tester, coder, reviewer, mutations, gate, commit, record, Done; a lean-lane card has no tester and no mutation list). A hand card runs card-queen, whose queen follows doug-hand from step 3, with step 2's start gate question, 2b's question, and step 6's continue gate skipped; a hand card that asks for a swarm takes 2b's swarm branch instead of step 3, as below. A flow card runs doug-implement per card, following sections 2 to 6 above, with section 3's approval replaced as below and section 7 skipped. Only the questions are skipped, not the actions behind them. Under autopilot a card still gets everything the skipped gates' Start or Approve branches do: a hand card gets doug-hand step 2's class, move to flow, and condition open, and the wall clock is noted when the card starts; a flow card gets section 1's move and section 3's Approve steps.
|
|
104
106
|
|
|
105
107
|
The queue approval stands for each flow-track card's plan approval: the lead puts plan.mjs show in the chat and runs plan.mjs approve itself. A card runs by hand unless its goal asks for a swarm. A hand card whose goal asks for a swarm follows doug-hand's 2b swarm branch, with the queue approval standing for its plan approval. Follow-ups are filed to Backlog as section 6 says, without a question. On every queen notification the lead measures the session's cost with cost.mjs --session <id> and checks every cap.
|
|
106
108
|
|
|
107
|
-
Parallel cards: up to `workers.max` cards (default 3) run at once, each under its own queen, a background run in the card's own worktree, and main only coordinates. Main picks with `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" pick` up to workers.max cards that are disjoint: for hand cards, goals that name no common path and components that differ; for flow cards, plans whose owned files do not overlap. Flow cards in `needsPlan` get one `doug-flow:planner` each, all in one message, on the plan row, each writing `.doug/plans/<id>.json` and validating it with `plan.mjs validate --card <id>`. For each planned card main runs `plan.mjs set card <id> --card <id>`, `plan.mjs set integrationBranch doug/<id> --card <id>`, and `plan.mjs set integrationWorktree .claude/worktrees/doug-integration-<id> --card <id>`, puts `plan.mjs show --card <id>` in the chat, and runs `plan.mjs approve --card <id>`, because the queue approval stands for it; then main picks again. A flow card gets section 1's move and a hand card gets doug-hand step 2's class, move, and condition open, before its launch. A flow queen is `Workflow({
|
|
109
|
+
Parallel cards: up to `workers.max` cards (default 3) run at once, each under its own queen, a background run in the card's own worktree, and main only coordinates. Main picks with `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" pick` up to workers.max cards that are disjoint: for hand cards, goals that name no common path and components that differ; for flow cards, plans whose owned files do not overlap. Flow cards in `needsPlan` get one `doug-flow:planner` each, all in one message, on the plan row, each writing `.doug/plans/<id>.json` and validating it with `plan.mjs validate --card <id>`. For each planned card main runs `plan.mjs set card <id> --card <id>`, `plan.mjs set integrationBranch doug/<id> --card <id>`, and `plan.mjs set integrationWorktree .claude/worktrees/doug-integration-<id> --card <id>`, puts `plan.mjs show --card <id>` in the chat, and runs `plan.mjs approve --card <id>`, because the queue approval stands for it; then main picks again. A flow card gets section 1's move and a hand card gets doug-hand step 2's class, move, and condition open, before its launch. A flow queen is `Workflow({ scriptPath: <the queen.js path>, args: <plan.mjs json --card <id>> })`, the queen.js being the per-card copy of `workflows/doug-implement.js` that `autopilot.mjs queen-args` writes to `.doug/.state/reports/<id>/queen.js`; a hand queen is `Workflow({ scriptPath: <the queen.js path>, args })`, the per-card copy of `workflows/card-queen.js` at the same path, with args `{ card, options, models, repoRoot, worktreePath }`, the worktreePath being `<repoRoot>/.claude/worktrees/card-<id>`. Before any queen launch, main runs `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" agent-types --kind <flow|hand>`, which prints the agent types that queen spawns, one per line, and compares each name with the agent types its own session lists; when one is missing it launches no queen and tells the user to run /reload-plugins or restart. To launch a queen, main runs `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" queen-args <id> --kind <flow|hand> --with <the other ids launched in the same message>`, which writes `.doug/.state/reports/<id>/queen-args.json` and the per-card `.doug/.state/reports/<id>/queen.js`, whose run name is `<short id> card-queen` or `<short id> flow-queen`, and prints both paths; then `Workflow({ scriptPath: <the queen.js path>, args })` with that file's JSON as args; then `autopilot.mjs launched <id> --run <wf_id> --kind <flow|hand> --worktree <abs> --args-file <path>`, the flow worktree being `<repoRoot>/<the plan's integrationWorktree>`, because that value is relative. When a hand card's goal depends on a fact outside the repository (a Claude Code hook contract, a CLI's output format, a third-party API), main adds `--research on` to its queen-args line, so its queen runs the research seat before the lead seat; queen-args takes the queen's base branch from the current branch, or main when there is none. Main starts all the Workflow calls in one message, and it never briefs, builds, or reviews a queen card. Each queen's report arrives in that run's task-notification `<result>`, not in the Workflow tool's return, and main pipes it through `autopilot.mjs report --run <wf_id> --file .doug/.state/briefs/<id>-result.json`, the queen's result saved there, which prints land, resume, rerun alone, or ask. Land means the landing steps below; resume relaunches the same `.doug/.state/reports/<id>/queen.js` with `resumeFromRunId` and the saved `queen-args.json`, then runs `autopilot.mjs resumed <id> --run <new wf_id>`, once, and a second resume is ask; ask stops that card and asks the user. For any asking stop with a stoppedAt seat (a design-decision, seat-blocked, or reviewer-major-open stop among them), main asks the user, records the answer with `autopilot.mjs answer <id> --answer "<the user's answer>"`, which adds it to `options.answers` in the saved `queen-args.json`, and `autopilot.mjs report` then prints resume with the answer: main relaunches the same queen.js with `resumeFromRunId` and that `queen-args.json` and runs `autopilot.mjs resumed`; the seats before the asking seat replay from the cache, the asking seat re-runs with the answer in its brief, and a second stop from that seat stops the card again. A gate-timeout stop is load, not a red, when another queen is in flight: `autopilot.mjs report` then prints rerun alone, and main launches no new queen, waits until no other queen is running, runs `autopilot.mjs rerun-alone <id>`, which adds a rerun answer for the gate seat to the saved `queen-args.json`, relaunches the same queen.js with `resumeFromRunId` and that `queen-args.json`, and runs `autopilot.mjs resumed`; that is the card's one resume, so a second gate timeout is ask. Main lands one card at a time: a flow card with `plan.mjs done --card <id>` then `plan.mjs land --card <id>`, a hand card with `plan.mjs land-card <id> --branch card/<id> --worktree <abs>`. After each landing main runs `board.mjs move`, the run-report skill with `cost.mjs <wf_id> --report <per-card report path>`, which is `.doug/.state/reports/<id>/last-report.json` for a flow card and `.doug/.state/reports/<id>/card-report.json` for a hand card, and `autopilot.mjs lock-wait <id> --ms <n>`, where n is the `lockWaitMs` line that `plan.mjs land` or `land-card` printed, and the landing commit carries the card's plan `.doug/plans/<id>.json`. A resume never replans. When a queen's run needs the user, main asks while the other queens keep running. A cap reached launches no new queen and lets the in-flight ones finish. In manual or accept-edits mode every queen launch prompts, and the Workflow allow rule or "Yes, and don't ask again" runs n queens unattended (docs/research/parallel-cards-research.md section 4). A card tagged swarm (or `swarm: true`, or whose goal says to run it as a swarm) and a decision card run alone, as today, after the in-flight queens finish; `/doug-next <id>` by hand is unchanged.
|
|
108
110
|
|
|
109
111
|
The lead keeps the run's state file, `.doug/.state/autopilot.json` (uncommitted), current through `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs"`, which the live pane and the board's Autopilot panel (`doug board serve`) read:
|
|
110
112
|
|
|
113
|
+
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" clear` first when `--auto` is invoked, before the queue approval question, so an ended or inactive run drops from the pane and the board's panel; a running run is kept.
|
|
111
114
|
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" start --cards <id,id,...> --usd-cap <n> --wall-cap <min>` at the queue approval.
|
|
112
115
|
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" add <id> [--usd-cap <n>] [--wall-cap <min>]` for each card unlimited autopilot takes from Ready later, with the grown caps.
|
|
113
116
|
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" card <id>` when a card that main runs itself starts.
|
|
@@ -115,7 +118,9 @@ The lead keeps the run's state file, `.doug/.state/autopilot.json` (uncommitted)
|
|
|
115
118
|
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" land <id> --commit <sha>` when the card lands, or `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" stop <id> --reason "<why>"` when it stops.
|
|
116
119
|
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" filed <id> --kind <follow-up|user> [--from <card>] --position "<text>"` for each follow-up or user card filed to Backlog.
|
|
117
120
|
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" caps --usd <n> --wall <min> --context <pct>` after each cap check on a queen notification.
|
|
118
|
-
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" end` at the closing summary.
|
|
121
|
+
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" end` at the closing summary; it refuses while a card of the run is In flow and was neither resumed nor answered, naming each such card.
|
|
122
|
+
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" rerun-alone <id>` for a gate-timeout stop that report printed as rerun alone.
|
|
123
|
+
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" leave <id> --reason "<the user's answer>"` when the user chooses to leave a card In flow.
|
|
119
124
|
|
|
120
125
|
A write that fails never stops the run. The live pane (a Claude Code pane, drawn by the doug-flow plugin's hooks module) opens on its own when the run starts and `/doug-autopilot` reopens it; the same run shows on the board page; with no run the pane shows a "No autopilot run." line and the board shows no panel; a finished run shows its last summary.
|
|
121
126
|
|
|
@@ -123,6 +128,7 @@ Stop conditions, one per line:
|
|
|
123
128
|
|
|
124
129
|
- It stops and asks when a card needs a design decision its goal does not answer.
|
|
125
130
|
- It stops and asks when the gate is red twice on one card.
|
|
131
|
+
- It stops and asks when a gate seat times out with no other queen in flight, or times out again on its rerun alone.
|
|
126
132
|
- It stops and asks when a hook blocks a step.
|
|
127
133
|
- It stops and asks when a reviewer blocker stays open after one fix pass.
|
|
128
134
|
- It stops and asks when a cap is reached.
|
|
@@ -136,6 +142,7 @@ It ends with the summary below, one condition per line:
|
|
|
136
142
|
- It ends when Ready has no runnable card, under unlimited autopilot.
|
|
137
143
|
- It ends when the user says stop: the in-flight cards are finished or left In flow.
|
|
138
144
|
- It ends when the lead's context is at or above autopilot.contextThreshold in .doug/config.json (default 60): on every queen notification the lead runs `node "${CLAUDE_PLUGIN_ROOT}/scripts/context.mjs"`, which prints the context percentage of the lead's own session transcript, and treats a run that prints no reading as at the threshold.
|
|
145
|
+
- Before it ends, every card of the run still In flow is resumed once, alone, from its saved `queen-args.json` (after `autopilot.mjs rerun-alone <id>` unless it stopped agent-null); only when that resume also stops does it ask, and the user's choice to leave a card In flow is recorded with `autopilot.mjs leave <id> --reason "<the user's answer>"`.
|
|
139
146
|
- Before that summary it refreshes the anchor handoff: a `## Autopilot handoff` section in .doug/anchor.md naming the landed cards, any card left In flow, and that a new session resumes with /doug-next --auto.
|
|
140
147
|
|
|
141
148
|
An edge case or input the goal does not name is not a design decision: the lead takes the conservative behaviour, rejecting the input or, where rejecting is not possible, preserving it, never guessing, has whoever writes the test pin it, and names it in the card's landing note and the closing summary. A design decision is a choice, for inputs the goal names, between behaviours the goal's acceptance would both satisfy and a user would notice, or one that changes another card's or module's contract.
|
|
@@ -21,7 +21,7 @@ The session model never researches itself; it asks the questions and writes the
|
|
|
21
21
|
|
|
22
22
|
## One researcher or several
|
|
23
23
|
|
|
24
|
-
One question, one researcher. Several independent questions get several researchers in the same message, in parallel, each with one distinct question and told not to answer the others'; their answers merge into one note. A request or card that names a crew of researchers (`crew: { "researchers": 2 }` in the plan's terms) gets that many, each with its own question. Do not give two researchers the same question: a disagreement between two guesses is not a check. Each question names the budget (at most 6 WebSearch plus WebFetch calls, research.maxFetches) and lists the raw URLs to try, in priority order.
|
|
24
|
+
One question, one researcher. Several independent questions get several researchers in the same message, in parallel, each with one distinct question and told not to answer the others'; their answers merge into one note. A request or card that names a crew of researchers (`crew: { "researchers": 2 }` in the plan's terms) gets that many, each with its own question. Do not give two researchers the same question: a disagreement between two guesses is not a check. Each question names the budget (at most 6 WebSearch plus WebFetch calls, research.maxFetches) and lists the raw URLs to try, in priority order. Each researcher brief asks exactly one question; a brief that reads an external system and then compares it with this repository asks two. A comparison with this repository's code is its own question for its own researcher, never appended to a question about an external system. Every researcher brief names where to read, the raw URLs in priority order or the directories and files of a downloaded tree, and its turn budget: at most 40 tool calls (the researcher agent's maxTurns), stopping with at least 5 to spare to return what it has. On 2026-10-13 two of the three memory-research-tencentdb researchers, each briefed to read an external codebase and then compare it with Doug, ran into the 30-turn cap and returned a half sentence with no findings.
|
|
25
25
|
|
|
26
26
|
## The note
|
|
27
27
|
|
|
@@ -33,7 +33,7 @@ Write the findings to `.doug/.state/research/<card-id>.md` (session state, ignor
|
|
|
33
33
|
|
|
34
34
|
A fact the design depends on is marked verified only when it is quoted verbatim from the raw source with its line number, fetched and searched (for example `curl -sL <url>` then `grep -n`) by the plugin's researcher agent or by a claude-code-guide asked for exact quotes; a guide answer that paraphrases, or cannot give the line, stays `unverified` in the note. The confirming fetch counts against the same research.maxFetches budget. On 2026-10-01 a guide answer quoted a sentence the raw sub-agents page does not contain, and only the grep caught it.
|
|
35
35
|
|
|
36
|
-
The note is written under the card's id, not the question's, because the id is also the promotion key: a note for a card that lands is promoted to `docs/research/<card-id>.md`, `board.mjs record` copying it there when it records the landing so a commit that cites the note has something to read afterward. A note for a card that never lands (dropped, replanned under another id) stays in state and is eventually swept with the rest of `.doug/.state/`.
|
|
36
|
+
The note is written under the card's id, not the question's, because the id is also the promotion key: a note for a card that lands is promoted to `docs/research/<card-id>.md`, `board.mjs record` copying it there when it records the landing so a commit that cites the note has something to read afterward. A queen run's inline note rides the queen report as `researchNote`; `autopilot.mjs report` writes it to `.doug/.state/research/<card-id>.md` so record promotes it as for a by-hand card. A note for a card that never lands (dropped, replanned under another id) stays in state and is eventually swept with the rest of `.doug/.state/`.
|
|
37
37
|
|
|
38
38
|
Then hand it on:
|
|
39
39
|
|
|
@@ -14,7 +14,7 @@ Every `board.mjs record` and `board.mjs summary` takes a required `--summary "<o
|
|
|
14
14
|
|
|
15
15
|
With the card id, the report (`.doug/.state/last-report.json` unless another path is given), the run id the Workflow tool printed (`wf_...`), the wall clock, and the landing commit:
|
|
16
16
|
|
|
17
|
-
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/cost.mjs" <run-id>`: the run's real cost, summed from the local Claude Code transcripts of every agent in the run and priced from the dated table in the script (no network). Its last lines are the run total and any agent it could not price. The number after `Run total:` is the cost; when the run cannot be found or every agent is unpriced, there is no cost. The workflow itself does not report cost; never estimate one. The Codex line after it, when present, is the Codex adversary's cost: pass its USD as --codex-cost <usd> to record and summary; it is never added to --cost.
|
|
17
|
+
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/cost.mjs" <run-id> --report <report.json>`: the run's real cost, summed from the local Claude Code transcripts of every agent in the run and priced from the dated table in the script (no network). Its last lines are the run total and any agent it could not price. The number after `Run total:` is the cost; when the run cannot be found or every agent is unpriced, there is no cost. The workflow itself does not report cost; never estimate one. The Codex line after it, when present, is the Codex adversary's cost: pass its USD as --codex-cost <usd> to record and summary; it is never added to --cost.
|
|
18
18
|
2. Classify every adversary block in the report. A block is a finding the adversary raised as a blocker (its ledger id, `F1`; for a report from before the ledger, `pass-<n>`); `board.mjs record` refuses an id the report does not have and lists the ones it has. This is judgment work and runs on the `review` row: read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.review.model`, `roles.review.effort`), and spawn `Agent({ subagent_type: "doug-flow:reviewer", model: "<review row model>", prompt: "There is no worktree and no diff for this job: read the report at <report path> (repository root, read-only) and classify every adversary block in it as real (a defect a user would hit), marginal (true to the spec, no user impact), or false (wrong), each with a one-line reason drawn from the fix passes and the checks in the report, not from the adversary's summary. Return exactly one line per block, nothing else: F1=<real|marginal|false>: <one-line reason> (pass-<n>=... for a pre-ledger report). Edit nothing." })`. The lead passes each returned line into `--adversary` verbatim and never reclassifies.
|
|
19
19
|
3. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --run <run-id> --wall "<m> min" --commit <sha> --cost <usd> --summary "<one sentence>" [--codex-cost <usd>] --adversary "F1=<class>: <reason>"` with `--adversary` repeated once per block, leaving `--cost` out when step 1 gave no number and `--codex-cost` out when step 1 showed no Codex line. This appends the entry to `docs/live-runs.md`: the measures table with the precision line (n real / n marginal / n false), the per-level, per-task table, and one line per classified block. It also promotes `.doug/.state/research/<id>.md` to `docs/research/<id>.md` when that note exists, printing one line saying so; add that path to the landing commit alongside `docs/live-runs.md`.
|
|
20
20
|
4. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" import`: rolls any auto-memory file written since the last import into the lessons store — it is idempotent, a rewritten file supersedes its old row, and it never writes under the auto-memory directory. Its one-line output goes in the chat; an exit 1 naming Node is said in the chat and the landing goes on, the same as record below.
|
|
@@ -28,7 +28,7 @@ A run that stopped is recorded the same way, without `--commit`; the summary's t
|
|
|
28
28
|
|
|
29
29
|
When `.doug/plan.json` carries `cards` (made by `plan.mjs merge`), the run is recorded once per card:
|
|
30
30
|
|
|
31
|
-
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/cost.mjs" <run-id> --by-card`: after the usual tables, a per-card table with each card's tasks' agents summed for that card and one row for the shared integration agents (the integrate and level-adversary agents, task `level-<n>`). The card rows are each card's `--cost`; the shared row is `--shared-cost`, the same number for every card. Pass the Codex line's USD as --codex-cost to the first card's record only.
|
|
31
|
+
1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/cost.mjs" <run-id> --report <report.json> --by-card`: after the usual tables, a per-card table with each card's tasks' agents summed for that card and one row for the shared integration agents (the integrate and level-adversary agents, task `level-<n>`). The card rows are each card's `--cost`; the shared row is `--shared-cost`, the same number for every card. Pass the Codex line's USD as --codex-cost to the first card's record only.
|
|
32
32
|
2. Classify the adversary blocks per card: `board.mjs record` lists only that card's tasks' blocks and refuses the others' ids.
|
|
33
33
|
3. For each card, in the plan's `cards` order: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --run <run-id> --wall "<m> min" --commit <sha> --summary "<one sentence>" --cost <that card's usd> --shared-cost <shared usd> [--codex-cost <usd>, first card only] --adversary ...`. Record reads the batch from the plan file: the entry names the other cards, shows only that card's tasks and the integration rows of their levels, and counts the shared cost once, under the first card.
|
|
34
34
|
4. `node "${CLAUDE_PLUGIN_ROOT}/scripts/memory.mjs" import` once for the whole report, the same as the workflow-run form's step 4: its one-line output goes in the chat, an exit 1 naming Node is said in the chat and the landing goes on, and it is idempotent, a rewritten file supersedes its old row, and it never writes under the auto-memory directory.
|