@dougharness/cli 1.1.0 → 1.2.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.
Files changed (59) hide show
  1. package/dist/bin.js +10 -3
  2. package/dist/bin.js.map +1 -1
  3. package/dist/board-serve.js +45 -10
  4. package/dist/board-serve.js.map +1 -1
  5. package/dist/board.d.ts +1 -1
  6. package/dist/board.js +33 -8
  7. package/dist/board.js.map +1 -1
  8. package/dist/init.js +1 -1
  9. package/dist/init.js.map +1 -1
  10. package/dist/memory-setup.d.ts +7 -0
  11. package/dist/memory-setup.js +75 -48
  12. package/dist/memory-setup.js.map +1 -1
  13. package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +6 -2
  14. package/node_modules/@dougharness/flow/agents/coder.md +23 -0
  15. package/node_modules/@dougharness/flow/agents/lead.md +1 -1
  16. package/node_modules/@dougharness/flow/agents/planner.md +2 -2
  17. package/node_modules/@dougharness/flow/agents/spec-writer.md +23 -0
  18. package/node_modules/@dougharness/flow/agents/tester.md +21 -0
  19. package/node_modules/@dougharness/flow/hooks/hooks.json +4 -1
  20. package/node_modules/@dougharness/flow/hooks/register.tsx +75 -0
  21. package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +117 -0
  22. package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +17 -0
  23. package/node_modules/@dougharness/flow/lib/board.mjs +107 -10
  24. package/node_modules/@dougharness/flow/lib/card-land.mjs +115 -0
  25. package/node_modules/@dougharness/flow/lib/card-loop.mjs +746 -0
  26. package/node_modules/@dougharness/flow/lib/coordinator.mjs +368 -0
  27. package/node_modules/@dougharness/flow/lib/cost.mjs +44 -11
  28. package/node_modules/@dougharness/flow/lib/embeddings.mjs +147 -4
  29. package/node_modules/@dougharness/flow/lib/land.mjs +33 -9
  30. package/node_modules/@dougharness/flow/lib/memory.mjs +96 -33
  31. package/node_modules/@dougharness/flow/lib/plan.mjs +34 -5
  32. package/node_modules/@dougharness/flow/lib/worktrees.mjs +3 -3
  33. package/node_modules/@dougharness/flow/package.json +1 -1
  34. package/node_modules/@dougharness/flow/scripts/autopilot.mjs +407 -0
  35. package/node_modules/@dougharness/flow/scripts/board.mjs +128 -32
  36. package/node_modules/@dougharness/flow/scripts/context.mjs +4 -2
  37. package/node_modules/@dougharness/flow/scripts/cost.mjs +16 -3
  38. package/node_modules/@dougharness/flow/scripts/memory.mjs +29 -14
  39. package/node_modules/@dougharness/flow/scripts/plan.mjs +122 -30
  40. package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +10 -4
  41. package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +34 -12
  42. package/node_modules/@dougharness/flow/skills/doug-plan/SKILL.md +1 -1
  43. package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +80 -0
  44. package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +2 -2
  45. package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +12 -10
  46. package/node_modules/@dougharness/flow/workflows/card-queen.js +807 -0
  47. package/node_modules/@dougharness/flow/workflows/doug-implement.js +33 -21
  48. package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
  49. package/node_modules/@dougharness/gates/lib/anchor.mjs +29 -4
  50. package/node_modules/@dougharness/gates/lib/bash-rules.mjs +217 -20
  51. package/node_modules/@dougharness/gates/lib/gate-lock.mjs +126 -0
  52. package/node_modules/@dougharness/gates/lib/proposals.mjs +41 -5
  53. package/node_modules/@dougharness/gates/lib/scope.mjs +43 -4
  54. package/node_modules/@dougharness/gates/package.json +1 -1
  55. package/node_modules/@dougharness/gates/scripts/reanchor.mjs +2 -1
  56. package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +148 -97
  57. package/node_modules/@dougharness/gates/scripts/verify.mjs +64 -5
  58. package/package.json +4 -4
  59. package/templates/board-app.js +48 -1
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // CLI for the plan file. Used by the doug-plan, doug-approve, and doug-implement skills.
3
+ // Any command takes --card <id> (right after the command): it works on the card's plan .doug/plans/<id>.json
4
+ // and report .doug/.state/reports/<id>/last-report.json instead of .doug/plan.json (card parallel-card-queens)
5
+ // plan.mjs anchor --cards [dir]
6
+ // writes .doug/anchor.md as one section per approved per-card plan, keeping the
7
+ // "## Autopilot handoff" section of the old anchor
3
8
  // plan.mjs validate [dir] [--file <path>]
4
9
  // exit 0 if .doug/plan.json (or the plan at --file, a card's draft) is valid, 1 with errors otherwise
5
10
  // plan.mjs show [dir] [--file <path>]
@@ -29,8 +34,12 @@
29
34
  // land.deleteBranches: true in .doug/config.json) also deletes the plan's
30
35
  // task branches after the merge with `git branch -d`; the integration
31
36
  // branch is always kept
37
+ // plan.mjs land-card <id> --branch <b> --worktree <abs> [--base main]
38
+ // land a hand card with no plan file: rebase its worktree on the base, gate the rebased
39
+ // tree through verify.mjs, fast-forward the base, remove the card worktree; prints the
40
+ // commit, the gate line, lockWaitMs and the removed worktrees; exit 1 on a refusal
32
41
  // plan.mjs set <key> <value> [dir]
33
- // set install, baseBranch, integrationBranch, adversary.command,
42
+ // set install, baseBranch, integrationBranch, integrationWorktree (under .claude/worktrees/), adversary.command,
34
43
  // adversary.fallback (<model>[/<effort>], or off), card, fixAttempts, swarm (on|off),
35
44
  // or workerCheck (on|off) in the plan file
36
45
  // (the file is what the workflow and land read; never pass these as launch args only)
@@ -44,13 +53,15 @@
44
53
 
45
54
  import { writeFileSync } from "node:fs";
46
55
  import { join, resolve } from "node:path";
47
- import { loadPlan, savePlan, validatePlan, setStatus, renderPlan, levelize, withSpecHashes, loadReport, reusePlan, staleReuseErrors, REPORT_RELPATH, acceptanceEntries, humanGateLevels, planWarnings, mergePlans, draftPath, PLAN_RELPATH } from "../lib/plan.mjs";
56
+ import { integrationWorktreeFor, loadPlan, savePlan, validatePlan, setStatus, renderPlan, levelize, withSpecHashes, loadReport, reusePlan, staleReuseErrors, REPORT_RELPATH, acceptanceEntries, humanGateLevels, planWarnings, mergePlans, draftPath, PLAN_RELPATH, PLANS_RELPATH, planPathFor, reportPathFor } from "../lib/plan.mjs";
48
57
  import { loadBoard, boardPath } from "../lib/board.mjs";
49
58
  import { loadModels, validateModelRefs, resolvePlanModels, renderModels, isModelName, EFFORTS } from "../lib/models.mjs";
50
59
  import { landPlan, INTEGRATION_WORKTREE } from "../lib/land.mjs";
60
+ import { landCard } from "../lib/card-land.mjs";
51
61
  import { removeFinishedWorktrees, retireTaskWorktrees, retireIntegrationWorktree, branchHead, isGitRepo } from "../lib/worktrees.mjs";
52
62
  import { readMemoryConfig, readMemoryStaleDays, readIndexConfig, createProvider } from "../lib/embeddings.mjs";
53
- import { existsSync, readFileSync, statSync } from "node:fs";
63
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
64
+ import { execFileSync } from "node:child_process";
54
65
 
55
66
  // The project's install command from .doug/config.json, used when the plan does not name one.
56
67
  function configInstall(d) {
@@ -88,8 +99,29 @@ function configDeleteBranches(d) {
88
99
  return false;
89
100
  }
90
101
  }
102
+ // card plan-base-branch-default: the branch checked out in d, or on a detached HEAD its full sha (noted on
103
+ // stderr); null when d is not a git repository or git fails.
104
+ function currentBase(d) {
105
+ const git = (...args) => execFileSync("git", args, { cwd: d, encoding: "utf8", stdio: "pipe" }).trim();
106
+ try {
107
+ const b = git("symbolic-ref", "--short", "-q", "HEAD");
108
+ if (b) return b;
109
+ } catch {}
110
+ try {
111
+ const sha = git("rev-parse", "--verify", "-q", "HEAD");
112
+ if (!sha) return null;
113
+ process.stderr.write(`[doug] plan.mjs: detached HEAD; baseBranch set to ${sha}\n`);
114
+ return sha;
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
91
119
  function withDefaults(p, d) {
92
120
  let out = p;
121
+ if (typeof out.baseBranch !== "string" || !out.baseBranch.trim()) {
122
+ const base = currentBase(d);
123
+ if (base) out = { ...out, baseBranch: base };
124
+ }
93
125
  if (!out.install && configInstall(d)) out = { ...out, install: configInstall(d) };
94
126
  const hasCommand = out.adversary && typeof out.adversary === "object" && typeof out.adversary.command === "string" && out.adversary.command.trim();
95
127
  if (out.adversary !== false && !hasCommand && configAdversary(d)) out = { ...out, adversary: { ...(out.adversary && typeof out.adversary === "object" ? out.adversary : {}), command: configAdversary(d) } };
@@ -233,9 +265,24 @@ async function attachCodeContext(plan, dir, provider) {
233
265
  }
234
266
  }
235
267
 
236
- const SETTABLE = ["install", "baseBranch", "integrationBranch", "adversary.command", "adversary.fallback", "card", "fixAttempts", "swarm", "workerCheck"];
268
+ const SETTABLE = ["install", "baseBranch", "integrationBranch", "integrationWorktree", "adversary.command", "adversary.fallback", "card", "fixAttempts", "swarm", "workerCheck"];
237
269
 
238
- const [cmd, ...rawArgs] = process.argv.slice(2);
270
+ const [cmd, ...argsRaw] = process.argv.slice(2);
271
+ // --card=<id> is the same as --card <id>.
272
+ const argsWithCard = argsRaw.flatMap((a) => (a.startsWith("--card=") ? ["--card", a.slice("--card=".length)] : [a]));
273
+ // --card <id> (card parallel-card-queens): the command works on that card's plan .doug/plans/<id>.json and report
274
+ // .doug/.state/reports/<id>/last-report.json instead of the single .doug/plan.json. The flag and its value are
275
+ // stripped before any positional is read.
276
+ const cardFlag = argsWithCard.indexOf("--card");
277
+ const cardId = cardFlag >= 0 ? argsWithCard[cardFlag + 1] : undefined;
278
+ const rawArgs = cardFlag >= 0 ? argsWithCard.filter((_, i) => i !== cardFlag && i !== cardFlag + 1) : argsWithCard;
279
+ if (cardFlag >= 0 && (!cardId || cardId.startsWith("--") || !/^[A-Za-z0-9._-]+$/.test(cardId) || cardId.startsWith("."))) {
280
+ process.stderr.write("plan.mjs --card needs a card id (letters, digits, . _ -)\n");
281
+ process.exit(2);
282
+ }
283
+ const cardPlanRel = cardId ? planPathFor(cardId) : PLAN_RELPATH;
284
+ const reportRel = cardId ? reportPathFor(cardId) : REPORT_RELPATH;
285
+ const anchorCards = cmd === "anchor" && rawArgs.includes("--cards");
239
286
  // --file <path>: validate or show a plan file other than .doug/plan.json (a card's draft), relative to the project dir.
240
287
  const fileFlag = rawArgs.indexOf("--file");
241
288
  const planFile = fileFlag >= 0 && (cmd === "validate" || cmd === "show") ? rawArgs[fileFlag + 1] : undefined;
@@ -247,7 +294,7 @@ const lastDirArg = (args) => {
247
294
  const last = positional[positional.length - 1];
248
295
  return last && existsSync(last) && statSync(last).isDirectory() ? last : undefined;
249
296
  };
250
- const dirArg = cmd === "set" || cmd === "gate" ? restArgs.slice(2).find((a) => !a.startsWith("--")) : cmd === "merge" ? lastDirArg(restArgs) : firstNonFlag(restArgs);
297
+ const dirArg = cmd === "land-card" ? undefined : cmd === "set" || cmd === "gate" ? restArgs.slice(2).find((a) => !a.startsWith("--")) : cmd === "merge" ? lastDirArg(restArgs) : firstNonFlag(restArgs);
251
298
  const dir = resolve(process.env.CLAUDE_PROJECT_DIR || dirArg || process.cwd());
252
299
  const deleteBranchesFlag = cmd === "land" && restArgs.includes("--delete-branches");
253
300
  const forceDoneFlag = cmd === "done" && restArgs.includes("--force");
@@ -264,8 +311,8 @@ function fail(msg, code = 1) {
264
311
  process.exit(code);
265
312
  }
266
313
 
267
- const plan = loadPlan(dir, planFile);
268
- if (!plan && cmd !== undefined && cmd !== "models" && cmd !== "merge") fail(`No plan at ${join(dir, planFile || PLAN_RELPATH)}. Run /doug-plan first.`, 2);
314
+ const plan = anchorCards ? null : loadPlan(dir, planFile || cardPlanRel);
315
+ if (!plan && cmd !== undefined && cmd !== "models" && cmd !== "merge" && cmd !== "land-card" && !anchorCards) fail(`No plan at ${join(dir, planFile || cardPlanRel)}. Run /doug-plan first.`, 2);
269
316
  const models = loadModels(dir);
270
317
 
271
318
  function allErrors(p) {
@@ -314,7 +361,7 @@ switch (cmd) {
314
361
  const errors = allErrors(plan);
315
362
  if (errors.length) fail("Refusing to approve an invalid plan:\n" + errors.map((e) => " - " + e).join("\n"));
316
363
  if (plan.tasks.some((t) => t.reuse !== undefined)) {
317
- const reuseErrors = staleReuseErrors(plan, loadReport(dir), { git: gitResolver });
364
+ const reuseErrors = staleReuseErrors(plan, loadReport(dir, reportRel), { git: gitResolver });
318
365
  if (reuseErrors.length) fail("Refusing to approve: stale reuse marks:\n" + reuseErrors.map((e) => " - " + e).join("\n"));
319
366
  }
320
367
  if (plan.status === "approved") {
@@ -339,19 +386,19 @@ switch (cmd) {
339
386
  for (const r of retired.renamed) process.stdout.write(` renamed branch ${r.from} to ${r.to}\n`);
340
387
  }
341
388
  printWarnings(plan);
342
- savePlan(dir, setStatus(plan, "approved", new Date().toISOString()));
389
+ savePlan(dir, setStatus(plan, "approved", new Date().toISOString()), cardPlanRel);
343
390
  process.stdout.write(`Approved: ${plan.title}. Run /doug-implement to execute it.\n`);
344
391
  break;
345
392
  }
346
393
  case "reject": {
347
- savePlan(dir, setStatus(plan, "rejected"));
394
+ savePlan(dir, setStatus(plan, "rejected"), cardPlanRel);
348
395
  process.stdout.write("Plan rejected. Edit it or run /doug-plan again.\n");
349
396
  break;
350
397
  }
351
398
  case "done": {
352
399
  // A run paused at a human gate has levels still to run; landing it would merge half a plan. --force does not
353
400
  // override this: a paused run genuinely has work left, unlike a stopped one that merely failed.
354
- const lastReport = loadReport(dir);
401
+ const lastReport = loadReport(dir, reportRel);
355
402
  if (lastReport && lastReport.paused && typeof lastReport.paused.level === "number" && lastReport.plan === plan.title && plan.status === "approved") fail(`Refusing to mark the plan done: the last run is paused at the human gate after level ${lastReport.paused.level} (next: ${(lastReport.paused.next || []).join(", ")}). Open it with plan.mjs gate open ${lastReport.paused.level} and resume the run with its id, or reject the plan.`);
356
403
  // A run that stopped short (a failed level, a level adversary still blocked) or otherwise finished not-ok must
357
404
  // not be markable done: that would let land merge a broken run. --force overrides with a printed warning.
@@ -367,48 +414,89 @@ switch (cmd) {
367
414
  if (!forceDoneFlag) fail(`Refusing to mark the plan done: the last run is not ok; ${levelPart}${reasonsPart}. Re-plan with plan.mjs replan and run again, or plan.mjs done --force to override.`);
368
415
  process.stderr.write(`Warning: marking the plan done although the last run is not ok (${typeof lastReport.stoppedAtLevel === "number" ? `stopped at level ${lastReport.stoppedAtLevel}` : "finished not ok with no stopped level"}).\n`);
369
416
  }
370
- savePlan(dir, setStatus(plan, "done", new Date().toISOString()));
417
+ savePlan(dir, setStatus(plan, "done", new Date().toISOString()), cardPlanRel);
371
418
  process.stdout.write("Plan marked done.\n");
372
419
  break;
373
420
  }
374
421
  case "anchor": {
375
- const cardLines = typeof plan.card === "string" && plan.card.trim() ? [`Card: ${plan.card}`, ""] : Array.isArray(plan.cards) && plan.cards.length ? [`Cards: ${plan.cards.join(", ")}`, ""] : [];
376
- const acceptanceLines = acceptanceEntries(plan.acceptance).flatMap(({ text, command }) => (command ? [`- ${text}`, ` $ ${command}`] : [`- ${text}`]));
377
- const lines = [`# ${plan.title}`, "", ...cardLines, plan.goal, "", "Acceptance:", ...acceptanceLines, "", "Tasks and owned files:"];
378
- for (const t of plan.tasks) lines.push(`- ${t.id}${t.card ? ` [${t.card}]` : ""}: ${t.title} -> ${t.files.join(", ")}${t.reuse ? ` (reuse ${t.reuse})` : ""}`);
379
- writeFileSync(join(dir, ".doug/anchor.md"), lines.join("\n") + "\n");
422
+ const anchorLines = (p) => {
423
+ const cardLines = typeof p.card === "string" && p.card.trim() ? [`Card: ${p.card}`, ""] : Array.isArray(p.cards) && p.cards.length ? [`Cards: ${p.cards.join(", ")}`, ""] : [];
424
+ const acceptanceLines = acceptanceEntries(p.acceptance).flatMap(({ text, command }) => (command ? [`- ${text}`, ` $ ${command}`] : [`- ${text}`]));
425
+ const lines = [`# ${p.title}`, "", ...cardLines, p.goal, "", "Acceptance:", ...acceptanceLines, "", "Tasks and owned files:"];
426
+ for (const t of p.tasks) lines.push(`- ${t.id}${t.card ? ` [${t.card}]` : ""}: ${t.title} -> ${t.files.join(", ")}${t.reuse ? ` (reuse ${t.reuse})` : ""}`);
427
+ return lines;
428
+ };
429
+ if (anchorCards) {
430
+ // One section per approved, not-landed per-card plan; the "## Autopilot handoff" section of the old anchor is kept.
431
+ const plansDir = join(dir, PLANS_RELPATH);
432
+ const sections = [];
433
+ for (const name of existsSync(plansDir) ? readdirSync(plansDir).filter((f) => f.endsWith(".json")).sort() : []) {
434
+ const p = loadPlan(dir, `${PLANS_RELPATH}/${name}`);
435
+ if (p && p.status === "approved" && !p.landed) sections.push(anchorLines(p).join("\n"));
436
+ }
437
+ const anchorFile = join(dir, ".doug/anchor.md");
438
+ let handoff = "";
439
+ if (existsSync(anchorFile)) {
440
+ const old = readFileSync(anchorFile, "utf8");
441
+ const at = old.startsWith("## Autopilot handoff") ? 0 : old.indexOf("\n## Autopilot handoff");
442
+ if (at >= 0) {
443
+ const from = at === 0 ? 0 : at + 1;
444
+ const next = old.indexOf("\n## ", from + 1);
445
+ handoff = old.slice(from, next >= 0 ? next + 1 : undefined).replace(/\n*$/, "\n");
446
+ }
447
+ }
448
+ writeFileSync(anchorFile, [...sections.map((x) => x + "\n"), ...(handoff ? [handoff] : [])].join("\n"));
449
+ process.stdout.write(`Wrote .doug/anchor.md (${sections.length} card plans)\n`);
450
+ break;
451
+ }
452
+ writeFileSync(join(dir, ".doug/anchor.md"), anchorLines(plan).join("\n") + "\n");
380
453
  process.stdout.write("Wrote .doug/anchor.md\n");
381
454
  break;
382
455
  }
383
456
  case "land": {
384
457
  const deleteBranches = deleteBranchesFlag || configDeleteBranches(dir);
385
- const r = landPlan(dir, withDefaults(plan, dir), { deleteBranches });
458
+ const r = landPlan(dir, withDefaults(plan, dir), { deleteBranches, ...(cardId ? { rebase: true, ownOnly: true, planRelpath: cardPlanRel } : {}) });
386
459
  if (!r.ok) fail(`Refusing to land: ${r.reason}`);
387
460
  process.stdout.write(`Landed ${r.branch} into ${r.base} as ${r.mergeCommit}.\n`);
461
+ process.stdout.write(`lockWaitMs ${r.lockWaitMs ?? 0}\n`);
388
462
  for (const v of r.verify) process.stdout.write(` verify ${v.ok ? "ok" : "failed"}: ${v.command} (${v.durationMs} ms)\n`);
389
463
  for (const a of r.acceptance) process.stdout.write(` acceptance ${a.ok ? "ok" : "failed"}: ${a.text} (${a.durationMs} ms)\n`);
390
464
  for (const p of r.worktrees.removed) process.stdout.write(` removed worktree ${p}\n`);
391
465
  for (const k of r.worktrees.kept) process.stdout.write(` kept worktree ${k.path}: ${k.reason}\n`);
392
466
  for (const n of r.branches.deleted) process.stdout.write(` deleted branch ${n}\n`);
393
467
  for (const k of r.branches.kept) process.stdout.write(` kept branch ${k.name}: ${k.reason}\n`);
394
- process.stdout.write(`The branch ${r.branch} is kept; delete it when you no longer need it. Recorded in .doug/plan.json as landed.\n`);
468
+ process.stdout.write(`The branch ${r.branch} is kept; delete it when you no longer need it. Recorded in ${cardPlanRel} as landed.\n`);
469
+ break;
470
+ }
471
+ case "land-card": {
472
+ // plan.mjs land-card <id> --branch <b> --worktree <abs> [--base main]: land a hand card (no plan file needed).
473
+ const opt = (name) => {
474
+ const i = restArgs.indexOf(name);
475
+ return i >= 0 ? restArgs[i + 1] : undefined;
476
+ };
477
+ const id = restArgs.find((a, i) => !a.startsWith("--") && !(i > 0 && restArgs[i - 1].startsWith("--")));
478
+ const r = landCard(dir, { id, branch: opt("--branch"), worktree: opt("--worktree"), base: opt("--base") || "main" });
479
+ if (!r.ok) fail(`Refusing to land: ${r.reason}`);
480
+ process.stdout.write(`Landed ${r.id || opt("--branch")} as ${r.commit}.\n${r.gateLine}\nlockWaitMs ${r.lockWaitMs}\n`);
481
+ for (const p of r.removed) process.stdout.write(` removed worktree ${p}\n`);
482
+ for (const k of r.kept) process.stdout.write(` kept worktree ${k.path}: ${k.reason}\n`);
395
483
  break;
396
484
  }
397
485
  case "gate": {
398
486
  // plan.mjs gate open <level>: release the human gate after that level so a resumed run continues past it.
399
- const [, action, levelArg] = process.argv.slice(2);
487
+ const [, action, levelArg] = [cmd, ...rawArgs];
400
488
  const gated = humanGateLevels(plan);
401
489
  if (action !== "open" || !/^\d+$/.test(String(levelArg))) fail(`usage: plan.mjs gate open <level> [dir]${gated.length ? `\nlevels with a human gate: ${gated.join(", ")}` : "\nthis plan has no human gate"}`, 2);
402
490
  if (plan.landed) fail("Refusing to change a landed plan.");
403
491
  const level = Number(levelArg);
404
492
  if (!gated.includes(level)) fail(`plan.mjs gate open: level ${level} has no human gate${gated.length ? ` (levels with one: ${gated.join(", ")})` : " (this plan has none)"}`, 2);
405
493
  const opened = [...new Set([...(plan.gatesOpened || []), level])].sort((a, b) => a - b);
406
- savePlan(dir, { ...plan, gatesOpened: opened });
494
+ savePlan(dir, { ...plan, gatesOpened: opened }, cardPlanRel);
407
495
  process.stdout.write(`Opened the human gate after level ${level}. Resume the run with its id; levels already run replay, and the run continues with level ${level + 1}.\n`);
408
496
  break;
409
497
  }
410
498
  case "set": {
411
- const [, key, value] = process.argv.slice(2);
499
+ const [, key, value] = [cmd, ...rawArgs];
412
500
  if (!SETTABLE.includes(key) || typeof value !== "string" || !value.trim()) fail(`usage: plan.mjs set <${SETTABLE.join("|")}> <value> [dir]`, 2);
413
501
  if (existsSync(value) && statSync(value).isDirectory()) fail(`plan.mjs set ${key}: the value ${JSON.stringify(value)} is a directory; the value comes before the optional [dir] argument.\nusage: plan.mjs set <${SETTABLE.join("|")}> <value> [dir]`, 2);
414
502
  if (plan.landed) fail("Refusing to change a landed plan.");
@@ -436,8 +524,12 @@ switch (cmd) {
436
524
  if (!/^\d+$/.test(value)) fail(`plan.mjs set fixAttempts: the value must be a non-negative integer, got ${JSON.stringify(value)}`, 2);
437
525
  next.fixAttempts = Number(value);
438
526
  } else next[key] = value;
439
- savePlan(dir, next);
440
- process.stdout.write(`Set ${key} in .doug/plan.json.\n`);
527
+ if (key === "integrationWorktree") {
528
+ const bad = validatePlan(next).filter((e) => e.includes("integrationWorktree"));
529
+ if (bad.length) fail(`plan.mjs set integrationWorktree: ${bad.join("; ")}`, 2);
530
+ }
531
+ savePlan(dir, next, cardPlanRel);
532
+ process.stdout.write(`Set ${key} in ${cardPlanRel}.\n`);
441
533
  break;
442
534
  }
443
535
  case "merge": {
@@ -479,10 +571,10 @@ switch (cmd) {
479
571
  }
480
572
  case "replan": {
481
573
  if (plan.landed) fail("Refusing to replan a landed plan.");
482
- const report = loadReport(dir);
483
- if (report === null) fail(`No report at ${REPORT_RELPATH}; run the workflow first (doug-next saves its report there).`);
574
+ const report = loadReport(dir, reportRel);
575
+ if (report === null) fail(`No report at ${reportRel}; run the workflow first (doug-next saves its report there).`);
484
576
  const { plan: next, decisions } = reusePlan(plan, report, { git: gitResolver });
485
- savePlan(dir, next);
577
+ savePlan(dir, next, cardPlanRel);
486
578
  process.stdout.write(`Plan "${plan.title}" is a draft again.\n`);
487
579
  for (const d of decisions) {
488
580
  if (d.reuse) process.stdout.write(d.renamed ? ` ${d.id}: reuse ${d.reuse} (${d.reason})\n` : ` ${d.id}: reuse ${d.reuse}\n`);
@@ -505,11 +597,11 @@ switch (cmd) {
505
597
  // The integration worktree is cleared too: a leftover from the plan's last run must not be reused as is by
506
598
  // the next one (card integration-worktree-stale). Its branch is untouched; the workflow's first integration
507
599
  // recreates the worktree from the base branch.
508
- const integrationRetired = retireIntegrationWorktree(dir);
600
+ const integrationRetired = retireIntegrationWorktree(dir, integrationWorktreeFor(plan));
509
601
  if (integrationRetired.removed) process.stdout.write(` removed integration worktree ${integrationRetired.removed}\n`);
510
602
  process.stdout.write("Review it with plan.mjs show, edit .doug/plan.json if a fresh task needs a new spec, then approve.\n");
511
603
  break;
512
604
  }
513
605
  default:
514
- fail("usage: plan.mjs <validate|show|json|models|approve|reject|done|anchor|land|set|replan|gate|merge> [dir]", 2);
606
+ fail("usage: plan.mjs <validate|show|json|models|approve|reject|done|anchor|land|land-card|set|replan|gate|merge> [dir]", 2);
515
607
  }
@@ -38,15 +38,19 @@ Batch: one `AskUserQuestion` names every remaining card of the batch (id, title,
38
38
  - **Skip**: go back to step 1 and take the next hand-track card after this one.
39
39
  - **Stop**: end.
40
40
 
41
+ A size L card with no `docs/specs/<card-id>/spec.md` is offered feature mode first (ADR 0012): after Start and before 2b, ask with `AskUserQuestion` whether to run `/doug-start <id>` (specification, optional pseudocode, architecture, refinement) or build now; `/doug-start <id>` ends this loop and the card stays In flow. S and M cards go straight on, and any card can still run `/doug-start <id>` on request. Under autopilot the offer is not asked and the card goes straight on.
42
+
41
43
  ## 2b. Shape: by hand, or a swarm
42
44
 
43
45
  Size S, or a card with no size at all: by hand, no question.
44
46
 
45
47
  Size M or L: by hand without asking when the goal names a file the running workflow or hooks execute: the workflow file (`workflows/doug-implement.js`), anything under `.doug/hooks/scripts/`, the plugin's agents, plus any list the project's CLAUDE.md, or a procedure it names, gives. The workflow cannot rewrite itself, its gate hooks, or its agents while it runs them, so those stay by hand.
46
48
 
47
- Batch: a batch runs only as a swarm, so for two or more cards this step does not ask swarm-or-by-hand; it says so and goes straight to spawning one planner per card in the same message so they run in parallel, on the `plan` row — the same `plan.mjs models` row and prompt shape below, each told to write its draft to `.doug/.state/drafts/<id>.json` (not `.doug/plan.json`) and validate it with `plan.mjs validate --file .doug/.state/drafts/<id>.json`, each also told this plan runs as a swarm: set `"swarm": true` and size every task M or L unless it is truly one bounded change. When every planner has returned, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" merge <id> <id>...` joins the drafts into `.doug/plan.json` (its output in the chat verbatim), then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`. Never `plan.mjs set card` for a batch: merge's `cards` replaces it. Then `plan.mjs show` (it prints `Cards:` and each task's card) and the approval gate below; Stop on a batch moves every card back to Ready with `board.mjs move <id> ready` (mirroring `/doug-next` section 3's Stop).
49
+ Batch: a batch runs only as a swarm, so for two or more cards this step does not ask swarm-or-by-hand; it says so and goes straight to spawning one planner per card in the same message so they run in parallel, on the `plan` row — the same `plan.mjs models` row and prompt shape below (including its `Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts.` sentence), each told to write its draft to `.doug/.state/drafts/<id>.json` (not `.doug/plan.json`) and validate it with `plan.mjs validate --file .doug/.state/drafts/<id>.json`, each also told this plan runs as a swarm: set `"swarm": true` and size every task M or L unless it is truly one bounded change. When every planner has returned, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" merge <id> <id>...` joins the drafts into `.doug/plan.json` (its output in the chat verbatim), then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`. Never `plan.mjs set card` for a batch: merge's `cards` replaces it. Then `plan.mjs show` (it prints `Cards:` and each task's card) and the approval gate below; Stop on a batch moves every card back to Ready with `board.mjs move <id> ready` (mirroring `/doug-next` section 3's Stop).
50
+
51
+ Recommend a swarm only when the goal asks for two or more independent deliverables: separate pieces of work, each with its own test file, where none uses a function, type, or file another one adds (the 2026-09-19 split probe's four report commands, docs/live-runs.md); otherwise recommend by hand. Say in the question, in one clause, which side of this rule the card is on. The recommendation is the lead's reading of the goal, not a default.
48
52
 
49
- Otherwise, for one card, ask with `AskUserQuestion`: run it as a swarm, or by hand. Swarm: spawn the planner the way `doug-plan` does, on the `plan` row: read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`), and pass that row's model (omit `model` when the row says `inherit`), but tell it this plan runs as a swarm the way `/doug-swarm` does: `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<title>\": <goal>. This plan runs as a swarm: set \"swarm\": true in the plan file, and size every task M or L unless it is truly one bounded change (a size-S task keeps a single implementer). Write .doug/plan.json and return the rendered plan." })`. Then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set card <id>` and `plan.mjs set swarm on` (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`) guarantee the card and the opt-in even if the planner forgot, put `plan.mjs show` in the chat, and ask the approval gate with `AskUserQuestion`: approve as written, edit first, or stop. Approval is never automatic.
53
+ Otherwise, for one card, ask with `AskUserQuestion`: run it as a swarm, or by hand. Swarm: spawn the planner the way `doug-plan` does, on the `plan` row: read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`), and pass that row's model (omit `model` when the row says `inherit`), but tell it this plan runs as a swarm the way `/doug-swarm` does: `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<title>\": <goal>. This plan runs as a swarm: set \"swarm\": true in the plan file, and size every task M or L unless it is truly one bounded change (a size-S task keeps a single implementer). Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write .doug/plan.json and return the rendered plan." })`. Then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set card <id>` and `plan.mjs set swarm on` (`node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set swarm on`) guarantee the card and the opt-in even if the planner forgot, put `plan.mjs show` in the chat, and ask the approval gate with `AskUserQuestion`: approve as written, edit first, or stop. Approval is never automatic.
50
54
 
51
55
  On approve: `plan.mjs approve`, `plan.mjs anchor`, invoke the `swarm-launch` skill. Note the wall clock when it starts and ends. When it returns, save the `result` object of the Workflow tool's output to `.doug/.state/last-report.json` with the Write tool and note the run id the Workflow tool printed (`wf_...`), the way `/doug-next` step 4 does: `plan.mjs replan`, `plan.mjs done`, `board.mjs record`, and `cost.mjs <run-id>` all read those two. If the report carries `paused`, follow `/doug-next` step 4's paused paragraph: name the tasks in `next` and ask with `AskUserQuestion` whether to open the gate; yes reopens with `plan.mjs gate open <level>` and a relaunch with `resumeFromRunId`, no leaves the card In flow and stops — `plan.mjs done` refuses while the last report is paused. A paused or failed batch changes nothing in this mechanism: the paused paragraph and `plan.mjs replan` both work from the plan file, which keeps `cards` through replan, and a second pass is detected the way `/doug-next` section 2 is, by the plan's `cards` matching this batch. Otherwise read the report as `swarm-launch` says, then `plan.mjs done` and `plan.mjs land`; a report that is not ok goes back through `plan.mjs replan` and the approval gate above, or by hand if the user says so — for a batch, "by hand" dissolves it: the first card continues by hand at step 3, and every other card of the batch goes back to Ready with `board.mjs move <id> ready`.
52
56
 
@@ -84,10 +88,12 @@ Stay within the card's goal. A defect found on the way that the goal does not co
84
88
 
85
89
  With the commit sha (the merge sha for a swarmed card) and the wall clock since step 1 (or since 2b's launch, for a swarmed card):
86
90
 
87
- 1. Invoke the `run-report` skill. By hand: its hand-track-landing form, `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>"` (the gate line is run-report's own rule: one `<command> <exit>` clause per entry of `.doug/config.json` `stopGate.commands`, joined by `; `; for example `typecheck 0; test:unit 0 (<n> passed)`), and `--note "<one line>"` when something about the build is worth keeping (what was harder than the goal said, what was left out and why), Hand the form's step 4, `memory.mjs reflect --hand`, two more inputs; they go to that command only, never to `board.mjs record` or `memory.mjs record`, which reject them: `--rounds <n>` (coder rounds until the reviewer passed) and one `--review "R<n>=<real|marginal|false>: <reason>"` per reviewer blocker finding a later round fixed, classed by the lead. Swarmed: its workflow-run form, `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json>` (the file 2b saved, `.doug/.state/last-report.json`) `--commit <merge sha> --wall "<m> min" --cost <usd> --codex-cost <usd>` (add --codex-cost <usd> when cost.mjs printed a Codex line, next to the run total) with one `--adversary "<id>=<real|marginal|false>: <reason>"` per adversary block the report carries (run-report requires every block classified), then `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --wall "<m> min" --commit <merge sha>` and put its three lines in the chat verbatim. Batch: the `run-report` skill's batch form — `cost.mjs <run-id> --by-card` for each card's cost with the shared integration agents counted once under the first, then one `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --commit <merge sha> --wall "<m> min" --cost <that card's usd> --shared-cost <shared usd> --adversary ...` per card in the plan's `cards` order, then `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --card <id> --wall "<m> min" --commit <merge sha>` per card with each card's three lines in the chat verbatim.
91
+ 1. Invoke the `run-report` skill. By hand: its hand-track-landing form, `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> --hand --commit <sha> --wall "<m> min" --gate "<gate line>" --summary "<one sentence>" --cost <usd>` (`--cost` is the measured session cost run-report's hand form takes with `cost.mjs --session <session> --since <openedAt>`, left out when nothing was measured; the gate line is run-report's own rule: one `<command> <exit>` clause per entry of `.doug/config.json` `stopGate.commands`, joined by `; `; for example `typecheck 0; test:unit 0 (<n> passed)`), and `--note "<one line>"` when something about the build is worth keeping (what was harder than the goal said, what was left out and why), Hand the form's step 4, `memory.mjs reflect --hand`, three more inputs; they go to that command only, never to `board.mjs record` or `memory.mjs record`, which reject them: `--rounds <n>` (coder rounds until the reviewer passed) and one `--review "<key>=<real|marginal|false>: <reason>"` (the key is "`R<n>` or `<card>/R<n>`") per reviewer blocker finding a later round fixed, classed by the lead, and, third, `--harmful <id>` once per recalled lesson the lead judges actually misled the work (a real block no longer bumps harmful). Swarmed: its workflow-run form, `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json>` (the file 2b saved, `.doug/.state/last-report.json`) `--commit <merge sha> --wall "<m> min" --cost <usd> --codex-cost <usd> --summary "<one sentence>"` (add --codex-cost <usd> when cost.mjs printed a Codex line, next to the run total) with one `--adversary "<id>=<real|marginal|false>: <reason>"` per adversary block the report carries (run-report requires every block classified), then `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --wall "<m> min" --commit <merge sha> --summary "<one sentence>"` and put its sentence, then its three lines, in the chat verbatim. Batch: the `run-report` skill's batch form — `cost.mjs <run-id> --by-card` for each card's cost with the shared integration agents counted once under the first, then one `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" record <id> <report.json> --commit <merge sha> --wall "<m> min" --cost <that card's usd> --shared-cost <shared usd> --summary "<one sentence>" --adversary ...` per card in the plan's `cards` order, then `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" summary <report.json> --card <id> --wall "<m> min" --commit <merge sha> --summary "<one sentence>"` per card with each card's sentence and three lines in the chat verbatim. A batch ends with one overall sentence, in plain words, of what the batch completed, after each card's own summary line. The landing block is printed by `board.mjs record` (both forms) and by `board.mjs summary --card` as their last three lines (Changed, Outcome, Learned; at most 100 words, `--note` feeds Learned); plain `summary` (no `--card`) prints no block. Your final message carries it verbatim, and the Stop gate blocks until it does.
88
92
  2. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" move <id> done --source "commit <sha>"`. Batch: once per card of the batch, same merge sha.
89
93
  3. `git add <record> docs/live-runs.md && git commit -m "Board: <id> done as <sha>"` (swarmed: add `.doug/plan.json` too); when step 1's `run-report` invocation printed that it promoted a research note, add `docs/research/<id>.md` too. No trailers. Batch: one commit `"Board: <id>, <id> done as <sha>"` including `.doug/plan.json`.
90
94
 
95
+ After the landing block, the lead prints the card's recap, a few lines in plain words: what the task was, what the outcome was, any other important facts (a follow-up worth filing, an edge case taken conservatively, a cost or wall-clock surprise), and, for a new feature, how to use it. In a batch, each card's recap follows that card's landing block.
96
+
91
97
  ## 5. Follow-up cards
92
98
 
93
99
  Before the continue gate, gather the follow-up candidates this landing produced: the coder's and reviewer's "left out", "not closed", and "found on the way" items, the landing note, and any defect the reviewer marked out of scope. Suggestions come only from what the run actually reported, never invented; each carries the one-line reason and cites the pass that found it. A reviewer's out-of-scope finding is filed only when it is realistic drift (a form a model or a person would plausibly write) or when one card would close the whole class; otherwise it goes in the landing note and is not filed. The card's goal text carries both: the goal names the pass that found it (the reviewer's finding, the coder's report, the adversary block id) and the landed commit.
@@ -98,7 +104,7 @@ The lead says in the chat what it filed and where: each new card's id, title, an
98
104
 
99
105
  ## Cards the user asks for
100
106
 
101
- A card the user asks for is a mini ADR, not a one-line goal. Before filing it, the lead asks the scoping questions in one `AskUserQuestion` of up to 4 questions, each with a Recommended option, and writes the answers into the goal. The goal carries the fixed headings Context / Decision / Options / Questions; `docs/decisions` stays for real ADRs. Only a user-requested card takes this form: a follow-up the lead files keeps step 5's lighter form, so autopilot follow-ups stay question-free.
107
+ A card the user asks for is a mini ADR, not a one-line goal. Before filing it, the lead asks the scoping questions in one `AskUserQuestion` of up to 4 questions, and writes the answers into the goal. The goal carries the fixed headings Context / Decision / Options / Questions; `docs/decisions` stays for real ADRs. Only a user-requested card takes this form: a follow-up the lead files keeps step 5's lighter form, so autopilot follow-ups stay question-free. The goal opens with the user's request quoted verbatim under Context, and each Decision line is marked (user answer) or (lead, not asked). One question names the broadest word in the user's request and offers its readings. An option is marked Recommended only when its description cites the fact it rests on, checked before asking; an option resting on an unchecked fact says (unverified) and is not Recommended, and a question with no checked option has no Recommended. When the user asked for research, "Let the research decide" is the Recommended option. A card the user asks for while autopilot runs is scoped at once with the same questions, filed to Backlog, and the queue resumes at the card it was on: the request is input, not a gate, and the running queue is unchanged.
102
108
 
103
109
  ## 6. Human gate: continue
104
110
 
@@ -31,17 +31,19 @@ Move it: `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" move <id> flow` prints
31
31
 
32
32
  If `.doug/plan.json` is already a draft whose `card` is this id (or whose `cards` are this batch) and any task carries `reuse`, it is the second pass of a blocked run (section 4 made it): do not spawn the planner; skip to the `plan.mjs show` paragraph below.
33
33
 
34
+ A size L card with no `docs/specs/<card-id>/spec.md` is offered feature mode first (ADR 0012): before the planner, ask with `AskUserQuestion` whether to run `/doug-start <id>` (specification, optional pseudocode, architecture, refinement) or plan now; `/doug-start <id>` ends this loop and the card stays In flow. S and M cards go straight to the plan, and any card can still run `/doug-start <id>` on request. Under autopilot the offer is not asked and the card goes straight to the plan.
35
+
34
36
  If the card's goal depends on a fact outside the repository (a Claude Code hook contract, a CLI's output format, a third-party API), invoke the `research` skill first and add `Research note: <path>` to the planner's prompt; the planner writes the facts it relied on into the plan's goal with their sources.
35
37
 
36
38
  The planner runs on the `plan` row (see "Who does the work" above): read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models` (`roles.plan.model`, `roles.plan.effort`), and pass that row's model on every planner spawn below (omit `model` when the row says `inherit`).
37
39
 
38
40
  Spawn the planner with the card's goal text verbatim as the request:
39
- `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<card.title>\": <card.goal>. Write .doug/plan.json and return the rendered plan." })`
41
+ `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<card.title>\": <card.goal>. Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write .doug/plan.json and return the rendered plan." })`
40
42
 
41
43
  Then record the card on the plan: `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" set card <id>` (the planner also writes it; setting it here is what guarantees the plan file carries the id even if the planner forgot).
42
44
 
43
45
  Batch: spawn one planner per card in the same message so they run in parallel, each told where its draft goes, each carrying the same plan row's model:
44
- `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<card.title>\": <card.goal>. Write the plan to .doug/.state/drafts/<id>.json (a draft for a batch; not .doug/plan.json), validate it with plan.mjs validate --file .doug/.state/drafts/<id>.json, and return the rendered plan." })`
46
+ `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "Board card <id>, title \"<card.title>\": <card.goal>. Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write the plan to .doug/.state/drafts/<id>.json (a draft for a batch; not .doug/plan.json), validate it with plan.mjs validate --file .doug/.state/drafts/<id>.json, and return the rendered plan." })`
45
47
  When all have returned, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" merge <id> <id>...` joins the drafts into `.doug/plan.json`: one draft plan, `cards` listing the batch in board order, every task carrying its `card`, level k of the batch being the union of each card's level k. A file two cards own is not refused: the later card's task (board order) gets a dependency edge to the earlier card's, so the file is edited in sequence and each card keeps its own spec, verify, and acceptance; merge prints each such edge (`<file>: <card>/<task> runs after <card>/<task>`), any task id it had to rename, and any plan-level setting the drafts disagreed on. Only a dependency cycle across cards is refused, by card name; then edit the drafts (or drop a card from the batch) and merge again. Put merge's output in the chat verbatim. Do not run `plan.mjs set card` for a batch; `plan.mjs show` prints `Cards:` and each task's card.
46
48
 
47
49
  Then `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" show` and put its complete output in the chat, verbatim, in a fenced code block, before anything else: the title and goal, every acceptance line, the verify commands, every level with each task's owned files and verify command, and the models table. The approval prompt that follows is a small dialog and hides the plan, so the user must be able to read the whole plan in the conversation above it. After the block, add at most a few lines of your own: any assumption the planner stated, any task that owns many files, and any task whose implementer is not on the implement row (a `tier`, `model`, or `effort` on the task): the planner sets none on its own, so one that is there came from the plan file and the user decides whether it stays. Do not summarize the plan instead of showing it.
@@ -56,7 +58,7 @@ Ask with `AskUserQuestion`, only after the full plan is in the chat: approve as
56
58
 
57
59
  ## 4. Implement
58
60
 
59
- `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" json` and pass its output verbatim as the args (do not add fields at launch): `Workflow({ name: "doug-flow:doug-implement", args: <plan object> })`; the registered name carries the plugin prefix. If it is not found, use `scriptPath: "${CLAUDE_PLUGIN_ROOT}/workflows/doug-implement.js"`. Note the wall clock when it starts and ends. When it returns, save the `result` object of the Workflow tool's output to `.doug/.state/last-report.json` with the Write tool (that directory is ignored by git and always allowed by the scope gate; anything else under `.doug/` would block the turn) and summarize it as `/doug-implement` does: per level, what was implemented, verified, reviewed, adversarially reviewed, and integrated, with the integration verify results. A task whose `attempts` array has more than one entry was fixed in the run: say how many passes it took and quote the `retriable.reason` of each blocked pass; a task with a `stopReason` gets it quoted verbatim.
61
+ `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" json` and pass its output verbatim as the args (do not add fields at launch): `Workflow({ name: "doug-flow:doug-implement", args: <plan object> })`; the registered name carries the plugin prefix. If it is not found, use `scriptPath: "${CLAUDE_PLUGIN_ROOT}/workflows/doug-implement.js"`. Note the wall clock when it starts and ends. A background run's result arrives in its task-notification `<result>`. When it returns, save the `result` object of the Workflow tool's output to `.doug/.state/last-report.json` with the Write tool (that directory is ignored by git and always allowed by the scope gate; anything else under `.doug/` would block the turn) and summarize it as `/doug-implement` does: per level, what was implemented, verified, reviewed, adversarially reviewed, and integrated, with the integration verify results. A task whose `attempts` array has more than one entry was fixed in the run: say how many passes it took and quote the `retriable.reason` of each blocked pass; a task with a `stopReason` gets it quoted verbatim.
60
62
 
61
63
  If the report carries `paused` (`{ level, gate: "human", next }`), the run stopped on purpose at a human gate after that level integrated: nothing failed, and the levels after it have not run. Summarize the levels that ran as above, name the tasks in `next`, and ask with `AskUserQuestion` whether to open the gate. Yes: `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" gate open <level>`, then relaunch the same run with the plan file's fresh JSON: `Workflow({ scriptPath: "${CLAUDE_PLUGIN_ROOT}/workflows/doug-implement.js", resumeFromRunId: "<the wf_ id>", args: <plan.mjs json> })`; the levels already run replay from the runtime's cache and the run continues with the next level; then read the new report from the top of this section. No: leave the card In flow and the plan approved and stop; `plan.mjs done` refuses while the last report is paused, and also refuses a report that is not ok (naming the stopped level and each task's `stopReason`, overridable with `--force`), so a paused or failed run cannot be skipped by landing.
62
64
 
@@ -69,10 +71,12 @@ If `ok` is not true: the workflow has already relaunched the implementer for eve
69
71
  Then, with the merge commit it printed:
70
72
 
71
73
  1. `node "${CLAUDE_PLUGIN_ROOT}/scripts/board.mjs" move <id> done --source "commit <sha>"`
72
- 2. Invoke the `run-report` skill for the card with `.doug/.state/last-report.json`, the run id the Workflow tool printed (`wf_...`), the wall clock, and the merge commit: it measures the run's cost with `cost.mjs` from the local transcripts (the workflow does not report cost; never estimate one), passes `cost.mjs`'s Codex line, when present, as `--codex-cost` to `board.mjs record` and `board.mjs summary` (never folded into `--cost`), appends the run entry to `docs/live-runs.md` with `board.mjs record`, and prints the three-line summary with `board.mjs summary`; put those three lines in the chat verbatim.
74
+ 2. Invoke the `run-report` skill for the card with `.doug/.state/last-report.json`, the run id the Workflow tool printed (`wf_...`), the wall clock, and the merge commit: it measures the run's cost with `cost.mjs` from the local transcripts (the workflow does not report cost; never estimate one), passes `cost.mjs`'s Codex line, when present, as `--codex-cost` to `board.mjs record` and `board.mjs summary` (never folded into `--cost`), appends the run entry to `docs/live-runs.md` with `board.mjs record`, and prints the three-line summary with `board.mjs summary`, each given the lead's `--summary "<one sentence>"` (run-report); put the sentence the commands print first in the chat, then those three lines verbatim. The landing block is printed by `board.mjs record` (both forms) and by `board.mjs summary --card` as their last three lines (Changed, Outcome, Learned; at most 100 words, `--note` feeds Learned); plain `summary` (no `--card`) prints no block. Your final message carries it verbatim, and the Stop gate blocks until it does.
73
75
  3. `git add <record> docs/live-runs.md .doug/plan.json && git commit -m "Board: <id> landed as <sha>"`, with `<record>` the path `board.mjs move` printed in item 1; when item 2's `run-report` invocation printed that it promoted a research note, add `docs/research/<id>.md` too. No trailers.
74
76
 
75
- Batch: every card of the batch moves to Done with the same `--source "commit <sha>"` (item 1, once per card), the `run-report` skill's batch section records one entry per card (that card's tasks only, its own measured cost, the shared integration agents counted once) and prints one summary per card, and item 3 is one commit: `"Board: <id>, <id> landed as <sha>"`.
77
+ Batch: every card of the batch moves to Done with the same `--source "commit <sha>"` (item 1, once per card), the `run-report` skill's batch section records one entry per card (that card's tasks only, its own measured cost, the shared integration agents counted once) and prints one summary per card, and item 3 is one commit: `"Board: <id>, <id> landed as <sha>"`. A batch ends with one overall sentence, in plain words, of what the batch completed, after each card's own summary line. In a batch, each card's landing block (one per card) goes in the final message verbatim.
78
+
79
+ After the landing block, the lead prints the card's recap, a few lines in plain words: what the task was, what the outcome was, any other important facts (a follow-up worth filing, an edge case taken conservatively, a cost or wall-clock surprise), and, for a new feature, how to use it. In a batch, each card's recap follows that card's landing block.
76
80
 
77
81
  ## 6. Follow-up cards
78
82
 
@@ -84,7 +88,7 @@ The lead says in the chat what it filed and where: each new card's id, title, an
84
88
 
85
89
  ## Cards the user asks for
86
90
 
87
- A card the user asks for is a mini ADR, not a one-line goal. Before filing it, the lead asks the scoping questions in one `AskUserQuestion` of up to 4 questions, each with a Recommended option, and writes the answers into the goal. The goal carries the fixed headings Context / Decision / Options / Questions; `docs/decisions` stays for real ADRs. Only a user-requested card takes this form: a follow-up the lead files keeps section 6's lighter form, so autopilot follow-ups stay question-free.
91
+ A card the user asks for is a mini ADR, not a one-line goal. Before filing it, the lead asks the scoping questions in one `AskUserQuestion` of up to 4 questions, and writes the answers into the goal. The goal carries the fixed headings Context / Decision / Options / Questions; `docs/decisions` stays for real ADRs. Only a user-requested card takes this form: a follow-up the lead files keeps section 6's lighter form, so autopilot follow-ups stay question-free. The goal opens with the user's request quoted verbatim under Context, and each Decision line is marked (user answer) or (lead, not asked). One question names the broadest word in the user's request and offers its readings. An option is marked Recommended only when its description cites the fact it rests on, checked before asking; an option resting on an unchecked fact says (unverified) and is not Recommended, and a question with no checked option has no Recommended. When the user asked for research, "Let the research decide" is the Recommended option. A card the user asks for while autopilot runs is scoped at once with the same questions, filed to Backlog, and the queue resumes at the card it was on: the request is input, not a gate, and the running queue is unchanged.
88
92
 
89
93
  ## 7. Human gate: continue
90
94
 
@@ -92,11 +96,28 @@ Ask with `AskUserQuestion` whether to take the next Ready card. Yes: go to step
92
96
 
93
97
  ## Autopilot (--auto)
94
98
 
95
- `/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: before each card it re-reads Ready with `board.mjs next --track any` and takes the next runnable Ready card 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 $40, and the user may change them 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 USD cap defaults to $40, and its wall cap grows as each card is taken, by that card's budget from the same size x class table (`board.mjs budgets` prints it); only the totals are checked between cards. When the measured walls drift, the lead reruns `board.mjs budgets --from docs/live-runs.md`, which recomputes the table and prints each cell's n, and replaces the 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.
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
+
101
+ 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
+
103
+ 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
+
105
+ 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
+
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({ name: "doug-flow:doug-implement", args: <plan.mjs json --card <id>> })`; a hand queen is `Workflow({ scriptPath: "${CLAUDE_PLUGIN_ROOT}/workflows/card-queen.js", args })` with args `{ card, options, models, repoRoot, worktreePath }`, the worktreePath being `<repoRoot>/.claude/worktrees/card-<id>`. To launch a queen, main runs `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" queen-args <id> --kind <flow|hand>`, which writes `.doug/.state/reports/<id>/queen-args.json` and prints its path; then the Workflow call 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. 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, or ask. Land means the landing steps below; resume relaunches the queen 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. 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
+
109
+ 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:
96
110
 
97
- 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 follows doug-hand from step 3 when it runs by hand, 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 follows 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.
111
+ - `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" start --cards <id,id,...> --usd-cap <n> --wall-cap <min>` at the queue approval.
112
+ - `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
+ - `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" card <id>` when a card that main runs itself starts.
114
+ - `node "${CLAUDE_PLUGIN_ROOT}/scripts/autopilot.mjs" step <id> <step>` as a card that main runs itself enters each of `research`, `tester`, `coder`, `reviewer`, `gate`, `commit`, and `record`, never for a queen card.
115
+ - `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
+ - `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
+ - `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.
98
119
 
99
- 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. Between cards the lead measures the session's cost with cost.mjs --session latest and checks every cap.
120
+ 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.
100
121
 
101
122
  Stop conditions, one per line:
102
123
 
@@ -108,14 +129,15 @@ Stop conditions, one per line:
108
129
  - It stops and asks when a workflow run's report is not ok, on either track.
109
130
  - It stops and asks when a workflow run pauses at a human gate, on either track.
110
131
  - It stops and asks when cost.mjs reports an unpriced transcript, because the USD cap cannot be checked.
132
+ - It stops and asks when a card queen's run stops; the other cards keep running.
111
133
 
112
134
  It ends with the summary below, one condition per line:
113
135
 
114
136
  - It ends when Ready has no runnable card, under unlimited autopilot.
115
- - It ends when the user says stop: the current card is finished or left In flow.
116
- - It ends when the lead's context is at or above autopilot.contextThreshold in .doug/config.json (default 60): between cards 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.
137
+ - It ends when the user says stop: the in-flight cards are finished or left In flow.
138
+ - 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.
117
139
  - 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.
118
140
 
119
141
  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.
120
142
 
121
- It waits for the answer, then resumes the queue at the card it stopped on. It ends with one summary: each landed card's commit, wall clock, and gate line; each stopped card and why; each edge case taken conservatively; and the follow-ups filed.
143
+ It waits for the answer, then resumes the queue at the card it stopped on; the other queens keep running while main waits. Its closing summary starts with one overall sentence, in plain words, of what the run completed; each landed card's own summary line was printed at its landing. It ends with one summary: each landed card's commit, wall clock, gate line, and recap; each stopped card and why; each edge case taken conservatively; and the follow-ups filed.
@@ -12,7 +12,7 @@ Produce a plan file the user can approve. No code is written by this skill.
12
12
  1. If the request depends on a fact outside the repository (a Claude Code hook contract, a CLI's output format, a third-party API), invoke the `research` skill first: it writes one cited note to `.doug/.state/research/<id>.md` (one researcher, or several in parallel with distinct questions).
13
13
 
14
14
  The planner runs on the `plan` row: read it with the `plan.mjs models` command, `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" models`, which prints the parsed table as JSON (`roles.plan.model`, `roles.plan.effort`). Spawn the planner subagent with the user's request verbatim plus any constraints they gave, and the note's path when there is one, passing that row's model (omit `model` when the row says `inherit`):
15
- `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "<request and constraints>. Research note: <path>. Write .doug/plan.json and return the rendered plan." })`
15
+ `Agent({ subagent_type: "doug-flow:planner", model: "<the plan row's model>", prompt: "<request and constraints>. Research note: <path>. Scripts: ${CLAUDE_PLUGIN_ROOT}/scripts. Write .doug/plan.json and return the rendered plan." })`
16
16
  The Agent tool carries no effort parameter; the row's effort applies through the planner agent's own frontmatter (`effort: high`). The session model never plans itself: it hands the request over and shows the plan.
17
17
  2. When it returns, validate and render the plan:
18
18
  `node "${CLAUDE_PLUGIN_ROOT}/scripts/plan.mjs" show`