@dougharness/cli 1.3.0 → 1.5.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 (51) hide show
  1. package/dist/ablate.js +2 -2
  2. package/dist/ablate.js.map +1 -1
  3. package/dist/bin.js +13 -4
  4. package/dist/bin.js.map +1 -1
  5. package/dist/board-page.d.ts +5 -1
  6. package/dist/board-page.js +19 -5
  7. package/dist/board-page.js.map +1 -1
  8. package/dist/board-serve.js +34 -10
  9. package/dist/board-serve.js.map +1 -1
  10. package/dist/board.d.ts +1 -1
  11. package/dist/board.js +160 -13
  12. package/dist/board.js.map +1 -1
  13. package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
  14. package/node_modules/@dougharness/flow/agents/planner.md +1 -0
  15. package/node_modules/@dougharness/flow/hooks/register.tsx +18 -2
  16. package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +94 -7
  17. package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +31 -4
  18. package/node_modules/@dougharness/flow/lib/board.mjs +375 -30
  19. package/node_modules/@dougharness/flow/lib/card-loop.mjs +237 -24
  20. package/node_modules/@dougharness/flow/lib/cost.mjs +9 -0
  21. package/node_modules/@dougharness/flow/lib/decisions.mjs +2 -4
  22. package/node_modules/@dougharness/flow/lib/learn.mjs +95 -54
  23. package/node_modules/@dougharness/flow/lib/memory.mjs +1403 -44
  24. package/node_modules/@dougharness/flow/lib/plan.mjs +40 -0
  25. package/node_modules/@dougharness/flow/package.json +1 -1
  26. package/node_modules/@dougharness/flow/scripts/autopilot.mjs +355 -13
  27. package/node_modules/@dougharness/flow/scripts/board.mjs +160 -27
  28. package/node_modules/@dougharness/flow/scripts/cost.mjs +6 -3
  29. package/node_modules/@dougharness/flow/scripts/learn.mjs +1 -1
  30. package/node_modules/@dougharness/flow/scripts/memory.mjs +668 -39
  31. package/node_modules/@dougharness/flow/scripts/plan.mjs +3 -3
  32. package/node_modules/@dougharness/flow/scripts/session-recall.mjs +11 -4
  33. package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +7 -3
  34. package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
  35. package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +11 -2
  36. package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +9 -8
  37. package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
  38. package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
  39. package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +3 -3
  40. package/node_modules/@dougharness/flow/workflows/card-queen.js +238 -25
  41. package/node_modules/@dougharness/flow/workflows/doug-implement.js +55 -37
  42. package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
  43. package/node_modules/@dougharness/gates/lib/baseline.mjs +32 -0
  44. package/node_modules/@dougharness/gates/lib/block-ledger.mjs +45 -5
  45. package/node_modules/@dougharness/gates/lib/docs-gate.mjs +6 -2
  46. package/node_modules/@dougharness/gates/lib/verified-tree.mjs +33 -0
  47. package/node_modules/@dougharness/gates/package.json +1 -1
  48. package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +130 -42
  49. package/node_modules/@dougharness/gates/scripts/verify.mjs +20 -4
  50. package/package.json +3 -3
  51. package/templates/board-app.js +240 -30
@@ -576,6 +576,46 @@ export function validatePlan(plan) {
576
576
  }
577
577
  });
578
578
  }
579
+ errors.push(...signatureErrors(plan));
580
+ return errors;
581
+ }
582
+
583
+ // Card planner-keeps-stated-signatures (2026-10-23, ts-orders swarm-skill): a signature the goal or an acceptance
584
+ // line quotes in backticks (`name(...)...`) must reach every task spec that quotes the same name, character for
585
+ // character after an optional `export `, `async `, `function ` prefix. Two different signatures for one name are
586
+ // rejected, never guessed between. Non-string goal, spec, and acceptance entries are skipped.
587
+ function signatureErrors(plan) {
588
+ const spans = (s) => (typeof s === "string" ? [...s.matchAll(/`([^`\n]+)`/g)].map((m) => m[1].trim()) : []);
589
+ const strip = (s) => s.replace(/^export\s+/, "").replace(/^async\s+/, "").replace(/^function\s+/, "").replace(/\s*;\s*$/, "").trim();
590
+ // A stated signature is a call form that carries a return type (its last `)` is followed by `:`); an untyped call
591
+ // is an example, never compared.
592
+ const nameOf = (s) => (/\)\s*:/.test(s.slice(s.lastIndexOf(")"))) ? /^([A-Za-z_$][\w$]*)\(/.exec(s)?.[1] : undefined);
593
+ const texts = [plan.goal, ...(Array.isArray(plan.acceptance) ? plan.acceptance.map((a) => (a && typeof a === "object" ? a.text : a)) : [])];
594
+ const stated = new Map(); // name -> signatures (distinct)
595
+ for (const text of texts) {
596
+ for (const span of spans(text)) {
597
+ const sig = strip(span);
598
+ const name = nameOf(sig);
599
+ if (!name) continue;
600
+ if (!stated.has(name)) stated.set(name, []);
601
+ if (!stated.get(name).includes(sig)) stated.get(name).push(sig);
602
+ }
603
+ }
604
+ const errors = [];
605
+ for (const [name, sigs] of stated) {
606
+ if (sigs.length > 1) errors.push(`the goal and acceptance state ${name} two ways, ${sigs.map((s) => `\`${s}\``).join(" and ")}: a stated signature reaches the plan verbatim, so state one`);
607
+ }
608
+ if (!Array.isArray(plan.tasks)) return errors;
609
+ for (const t of plan.tasks) {
610
+ if (!t || typeof t !== "object") continue;
611
+ for (const span of spans(t.spec)) {
612
+ const sig = strip(span);
613
+ const name = nameOf(sig);
614
+ const want = name && stated.get(name);
615
+ if (!want || want.length !== 1 || want[0] === sig) continue;
616
+ errors.push(`task ${t.id} spec states \`${sig}\` but the plan states \`${want[0]}\`: a stated signature reaches the task spec verbatim`);
617
+ }
618
+ }
579
619
  return errors;
580
620
  }
581
621
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougharness/flow",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "private": true,
5
5
  "description": "Plan, approve, implement one task per worktree, verify, review: a Workflow-tool template and subagents for Claude Code.",
6
6
  "type": "module",
@@ -9,25 +9,33 @@
9
9
  // autopilot.mjs stop <id> --reason "<why>"
10
10
  // autopilot.mjs filed <id> --kind <follow-up|user> [--from <card>] --position "<text>"
11
11
  // autopilot.mjs caps [--usd <n>] [--wall <min>] [--context <pct>]
12
- // autopilot.mjs pick [--max <n>] JSON { launch, needsPlan, held, alone }; a held line per held card on stderr
13
- // autopilot.mjs queen-args <id> --kind <flow|hand> [--with <id,id,...>] writes .doug/.state/reports/<id>/queen-args.json and the per-card queen.js beside it, prints both paths (args, then queen.js)
12
+ // autopilot.mjs pick [--max <n>] [--milestone <id>] JSON { launch, needsPlan, held, alone }; a held line per held card on stderr
13
+ // autopilot.mjs queen-args <id> --kind <flow|hand> [--with <id,id,...>] [--research on|off] writes .doug/.state/reports/<id>/queen-args.json and the per-card queen.js beside it, prints both paths (args, then queen.js)
14
14
  // autopilot.mjs agent-types --kind <flow|hand> prints the agent types that queen spawns, one per line
15
15
  // autopilot.mjs launched <id> --run <wf_id> --kind <flow|hand> --worktree <abs> --args-file <path>
16
- // autopilot.mjs report --run <wf_id> --file <result.json> saves the queen's report, prints land|resume|ask
17
- // autopilot.mjs resumed <id> --run <wf_id>
16
+ // autopilot.mjs report --run <wf_id> --file <result.json> saves the queen's report (a hand card's inline researchNote is written to .doug/.state/research/<id>.md so record promotes it), prints land|resume|ask
17
+ // autopilot.mjs answer <id> --answer "<text>" records the user's answer to any asking stop in queen-args.json options.answers
18
+ // autopilot.mjs resumed <id> --run <wf_id> counts every resume, plain or with the answer, against the single plain resume
19
+ // autopilot.mjs rerun-alone <id> records a rerun-alone answer for a gate-timeout stop; refuses while another queen is in flight or after the card's one resume
20
+ // autopilot.mjs leave <id> --reason "<text>" records the user's choice to leave a card In flow
18
21
  // autopilot.mjs lock-wait <id> --ms <n>
22
+ // autopilot.mjs clear drops an ended or inactive run; a running run is kept
19
23
  // autopilot.mjs end
20
24
  // --dir <d> names the project (default cwd). Exit 0 done; 1 no run or unknown card; 2 bad usage.
21
25
  import { execFileSync } from "node:child_process";
22
- import { copyFileSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
26
+ import { createHash } from "node:crypto";
27
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
28
+ import { homedir } from "node:os";
23
29
  import { dirname, join, resolve } from "node:path";
24
- import { fileURLToPath } from "node:url";
30
+ import { fileURLToPath, pathToFileURL } from "node:url";
25
31
  import { loadModels } from "../lib/models.mjs";
26
- import { AUTOPILOT_FILE, STEPS, readAutopilot } from "../lib/autopilot-view.mjs";
32
+ import { AUTOPILOT_FILE, STEPS, clearable, readAutopilot } from "../lib/autopilot-view.mjs";
27
33
  import { contextThreshold } from "../lib/context.mjs";
28
34
  import { classifyNotification, pickCards, queenLabel, renameQueenMeta, workersMax } from "../lib/coordinator.mjs";
29
35
  import { nextReadyCards } from "../lib/board.mjs";
36
+ import { SEATS } from "../lib/card-loop.mjs";
30
37
  import { planFor, reportPathFor } from "../lib/plan.mjs";
38
+ import { readMemoryConfig, readMemoryStaleDays } from "../lib/embeddings.mjs";
31
39
 
32
40
  const argv = process.argv.slice(2);
33
41
  const opts = {};
@@ -82,10 +90,43 @@ function cardOf(s, cardId) {
82
90
  if (!c) fail(`unknown card ${cardId}`);
83
91
  return c;
84
92
  }
93
+ const IN_FLIGHT = ["running", "done", "landing"];
94
+ const RERUN_ALONE = "Rerun alone: no other queen is in flight now.";
95
+ const othersInFlight = (s, id) => s.cards.filter((x) => x.id !== id && IN_FLIGHT.includes(x.state));
96
+ // The saved stop report and queen-args of a card, for the commands that answer an asking stop.
97
+ function savedStop(id, c) {
98
+ const reportDir = join(dir, ".doug/.state/reports", id);
99
+ let rep;
100
+ try {
101
+ rep = JSON.parse(readFileSync(join(reportDir, "card-report.json"), "utf8"));
102
+ } catch (e) {
103
+ fail(`cannot read the saved report for ${id}: ${e.code || e.message}`);
104
+ }
105
+ if (!rep || rep.outcome !== "stopped" || rep.reason === "agent-null" || !SEATS.includes(rep.stoppedAt)) fail(`${id} did not stop on an answerable ask stop`);
106
+ const argsPath = join(reportDir, "queen-args.json");
107
+ let qa;
108
+ try {
109
+ qa = JSON.parse(readFileSync(argsPath, "utf8"));
110
+ } catch (e) {
111
+ fail(`cannot read ${argsPath}: ${e.code || e.message}`);
112
+ }
113
+ const stopped = (Array.isArray(rep.history) ? rep.history : []).filter((h) => h && h.result === "stopped").pop();
114
+ const entry = { seat: rep.stoppedAt, question: rep.detail, answer: "" };
115
+ if (stopped && Number.isInteger(stopped.round) && stopped.round > 0) entry.round = stopped.round;
116
+ entry.run = c.runId;
117
+ return { rep, argsPath, qa, entry };
118
+ }
85
119
  const clearStep = (c) => {
86
120
  delete c.step;
87
121
  delete c.stepStartedAt;
122
+ delete c.seats;
88
123
  };
124
+ // Where the queen run's journal lives: the main session's run dir under the user's Claude dir; "" without a session id.
125
+ function journalPath(run) {
126
+ const sid = process.env.CLAUDE_CODE_SESSION_ID;
127
+ if (!sid) return "";
128
+ return join(homedir(), ".claude", "projects", dir.replace(/[^A-Za-z0-9]/g, "-"), sid, "subagents", "workflows", run, "journal.jsonl");
129
+ }
89
130
 
90
131
  function boardCard(cardId) {
91
132
  try {
@@ -169,6 +210,7 @@ switch (cmd) {
169
210
  const c = cardOf(s, id);
170
211
  c.step = arg2;
171
212
  c.stepStartedAt = new Date().toISOString();
213
+ c.seats = [...(Array.isArray(c.seats) ? c.seats : []), { step: arg2, startedAt: c.stepStartedAt }];
172
214
  save(s);
173
215
  say(`${id} ${arg2}`);
174
216
  break;
@@ -256,9 +298,14 @@ switch (cmd) {
256
298
  const b = boardCard(c.id);
257
299
  return b ? shape(b) : { id: c.id, goal: "", track: c.kind === "flow" ? "flow" : "hand" };
258
300
  });
259
- const ready = nextReadyCards(board, { track: "any", batch: Math.max(1, board.cards.length) })
260
- .cards.filter((c) => !flyingIds.has(c.id))
261
- .map(shape);
301
+ if (opts.milestone === "") usage("--milestone needs an id");
302
+ let picked;
303
+ try {
304
+ picked = nextReadyCards(board, { track: "any", batch: Math.max(1, board.cards.length), milestone: opts.milestone });
305
+ } catch (e) {
306
+ fail(e instanceof Error ? e.message : String(e));
307
+ }
308
+ const ready = picked.cards.filter((c) => !flyingIds.has(c.id)).map(shape);
262
309
  const plans = {};
263
310
  for (const c of [...ready, ...inFlight]) {
264
311
  if (c.track !== "flow") continue;
@@ -280,6 +327,10 @@ switch (cmd) {
280
327
  if (!id) usage("queen-args needs a card id");
281
328
  const kind = need("kind");
282
329
  if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
330
+ if (opts.research !== undefined) {
331
+ if (kind === "flow") usage("--research is for a hand card; research for a flow card runs before its plan (doug-next section 1)");
332
+ if (opts.research !== "on" && opts.research !== "off") usage("--research must be on or off");
333
+ }
283
334
  let args;
284
335
  if (kind === "flow") {
285
336
  try {
@@ -296,15 +347,21 @@ switch (cmd) {
296
347
  } catch {
297
348
  // absent or unreadable: no install command
298
349
  }
350
+ let base = "main";
351
+ try {
352
+ base = execFileSync("git", ["-C", dir, "symbolic-ref", "--short", "-q", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "main";
353
+ } catch {
354
+ // not a git repo or a detached HEAD: main
355
+ }
299
356
  args = {
300
357
  card: { id: b.id, title: b.title ?? null, size: b.size ?? null, class: b.class ?? null, goal: b.goal ?? "" },
301
358
  options: {
302
359
  testerAgent: true,
303
- research: false,
360
+ research: opts.research === "on",
304
361
  install: (config.commands && config.commands.install) || null,
305
362
  gateCommand: "node .doug/hooks/scripts/verify.mjs",
306
363
  branch: `card/${id}`,
307
- base: "main",
364
+ base,
308
365
  },
309
366
  models: loadModels(dir),
310
367
  repoRoot: dir,
@@ -370,6 +427,9 @@ switch (cmd) {
370
427
  c.kind = kind;
371
428
  c.worktree = worktree;
372
429
  clearStep(c);
430
+ delete c.journals;
431
+ const journal = journalPath(run);
432
+ if (journal) c.journals = [journal];
373
433
  save(s);
374
434
  say(`${id} launched as ${run}`);
375
435
  break;
@@ -393,6 +453,11 @@ switch (cmd) {
393
453
  // unreadable: classify says resume
394
454
  }
395
455
  const kind = c.kind === "flow" ? "flow" : "hand";
456
+ // The queen's task-output file wraps the result: {summary, agentCount, logs, result}.
457
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.result !== null && typeof parsed.result === "object") {
458
+ const bare = kind === "flow" ? !("ok" in parsed) && !("paused" in parsed) : !("card" in parsed) && !("outcome" in parsed);
459
+ if (bare) parsed = parsed.result;
460
+ }
396
461
  if (parsed !== null) {
397
462
  const dest = kind === "flow" ? join(dir, reportPathFor(c.id)) : join(dir, ".doug/.state/reports", c.id, "card-report.json");
398
463
  try {
@@ -401,11 +466,253 @@ switch (cmd) {
401
466
  } catch (e) {
402
467
  process.stderr.write(`autopilot: warning: cannot write ${dest}: ${e.code || e.message}\n`);
403
468
  }
469
+ if (kind === "hand") {
470
+ const rep = parsed.report ?? parsed;
471
+ const note = rep && typeof rep.researchNote === "string" ? rep.researchNote : "";
472
+ if (note.trim() !== "") {
473
+ const noteDest = join(dir, ".doug/.state/research", `${c.id}.md`);
474
+ try {
475
+ const oneLine = note.trim();
476
+ let src = null;
477
+ if (!oneLine.includes("\n") && oneLine.startsWith("/") && resolve(oneLine) !== resolve(noteDest) && existsSync(oneLine)) src = oneLine;
478
+ if (resolve(oneLine) !== resolve(noteDest)) {
479
+ mkdirSync(dirname(noteDest), { recursive: true });
480
+ if (src) copyFileSync(src, noteDest);
481
+ else writeFileSync(noteDest, note);
482
+ }
483
+ } catch (e) {
484
+ process.stderr.write(`autopilot: warning: cannot write ${noteDest}: ${e.code || e.message}\n`);
485
+ }
486
+ }
487
+ }
488
+ }
489
+ if (kind === "hand" && parsed !== null) {
490
+ const rep = parsed.report ?? parsed;
491
+ if (rep && typeof rep === "object" && rep.outcome === "stopped") {
492
+ try {
493
+ const mem = await import("../lib/memory.mjs");
494
+ const written = JSON.stringify(rep, null, 2) + "\n";
495
+ const readOr = (p) => {
496
+ try {
497
+ return readFileSync(p, "utf8");
498
+ } catch {
499
+ return "";
500
+ }
501
+ };
502
+ const ids = mem.runAgentIds((Array.isArray(c.journals) ? c.journals : []).map(readOr));
503
+ const blocks = mem.attributeBlocks(readOr(join(dir, ".doug/.state/blocks.jsonl")), ids);
504
+ let condition = null;
505
+ try {
506
+ const parsedCondition = JSON.parse(readFileSync(join(dir, ".doug/.state/reports", c.id, "condition.json"), "utf8"));
507
+ if (parsedCondition && typeof parsedCondition === "object" && !Array.isArray(parsedCondition)) condition = parsedCondition;
508
+ } catch {
509
+ condition = null;
510
+ }
511
+ const row = mem.stopOutcomeRow({
512
+ card: c.id,
513
+ report: rep,
514
+ reportHash: createHash("sha256").update(written).digest("hex"),
515
+ reportPath: join(dir, ".doug/.state/reports", c.id, "card-report.json"),
516
+ run,
517
+ blocks,
518
+ condition,
519
+ });
520
+ const m = mem.openMemory(dir);
521
+ try {
522
+ mem.recordOutcomes(m, [row]);
523
+ } finally {
524
+ m.close();
525
+ }
526
+ } catch (e) {
527
+ process.stderr.write(`autopilot: warning: cannot record the stop row for ${c.id}: ${e.message || e}\n`);
528
+ }
529
+ }
530
+ }
531
+ if (kind === "hand" && parsed !== null) {
532
+ try {
533
+ const mem = await import("../lib/memory.mjs");
534
+ const rep = parsed.report ?? parsed;
535
+ const readOr = (p) => {
536
+ try {
537
+ return readFileSync(p, "utf8");
538
+ } catch {
539
+ return "";
540
+ }
541
+ };
542
+ const sid = process.env.CLAUDE_CODE_SESSION_ID;
543
+ const jp = journalPath(run);
544
+ const journalText = jp ? readOr(jp) : "";
545
+ let queenArgs = null;
546
+ try {
547
+ queenArgs = JSON.parse(readFileSync(join(dir, ".doug/.state/reports", c.id, "queen-args.json"), "utf8"));
548
+ } catch {
549
+ // no saved args: no briefs
550
+ }
551
+ const seatRuns = mem.journalSeatRuns(journalText);
552
+ const briefs = mem.replayBriefs(queenArgs, seatRuns);
553
+ const ids = mem.runAgentIds([journalText]);
554
+ const blocks = mem.attributeBlocks(readOr(join(dir, ".doug/.state/blocks.jsonl")), ids);
555
+ const denials = mem.attributeDenials(sid ? readOr(join(dir, ".doug/.state/trace", `${sid.replace(/[^A-Za-z0-9_.-]/g, "_")}.jsonl`)) : "", ids);
556
+ const scan = await mem.historyScanner(dir);
557
+ const entries = [{ kind: "report", source: "card-report.json", value: rep }];
558
+ for (const b of blocks) entries.push({ kind: "block", source: "blocks.jsonl", value: b });
559
+ for (const d of denials) entries.push({ kind: "denial", source: "trace", value: d });
560
+ seatRuns.forEach((r, i) => {
561
+ if (briefs[i] !== null) entries.push({ kind: "brief", seat: r.seat, round: r.round, source: "replay:card-loop.mjs", value: briefs[i] });
562
+ entries.push({ kind: "hand-back", seat: r.seat, round: r.round, source: "journal.jsonl", value: r.result });
563
+ });
564
+ mem.writeRunHistory(dir, { card: c.id, run, entries, scan });
565
+ } catch (e) {
566
+ process.stderr.write(`autopilot: warning: cannot write the run history for ${c.id}: ${e.message || e}\n`);
567
+ }
568
+ }
569
+ if (kind === "flow" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.ok === false && !parsed.paused) {
570
+ try {
571
+ const mem = await import("../lib/memory.mjs");
572
+ const readOr = (p) => {
573
+ try {
574
+ return readFileSync(p, "utf8");
575
+ } catch {
576
+ return "";
577
+ }
578
+ };
579
+ const written = JSON.stringify(parsed, null, 2) + "\n";
580
+ const ids = mem.runAgentIds((Array.isArray(c.journals) ? c.journals : []).map(readOr));
581
+ const blocks = mem.attributeBlocks(readOr(join(dir, ".doug/.state/blocks.jsonl")), ids);
582
+ const levels = Array.isArray(parsed.levels) ? parsed.levels : [];
583
+ let stopClass = null;
584
+ let stopReason = null;
585
+ for (const lv of levels) {
586
+ const t = (Array.isArray(lv?.tasks) ? lv.tasks : []).find((x) => x && typeof x.stopReason === "string" && x.stopReason.trim() !== "");
587
+ if (t) {
588
+ stopClass = t.stopClass ?? null;
589
+ stopReason = t.stopReason;
590
+ break;
591
+ }
592
+ }
593
+ if (stopReason === null) {
594
+ stopReason = "the run stopped";
595
+ if (parsed.stoppedAtLevel !== undefined && parsed.stoppedAtLevel !== null) {
596
+ stopReason += ` at level ${parsed.stoppedAtLevel}`;
597
+ const lv = levels.find((x) => x && x.index === parsed.stoppedAtLevel) ?? levels[parsed.stoppedAtLevel];
598
+ const why = lv?.integration?.reason;
599
+ if (typeof why === "string" && why.trim() !== "") stopReason += `: ${why}`;
600
+ }
601
+ }
602
+ const row = {
603
+ ...mem.stopOutcomeRow({
604
+ card: c.id,
605
+ report: { outcome: "stopped", reason: stopClass, detail: stopReason },
606
+ reportHash: createHash("sha256").update(written).digest("hex"),
607
+ reportPath: join(dir, reportPathFor(c.id)),
608
+ run,
609
+ blocks,
610
+ memoryConfig: mem.memoryConfig({ embeddings: readMemoryConfig(dir).embeddings, staleDays: readMemoryStaleDays(dir), track: "flow" }),
611
+ }),
612
+ track: "flow",
613
+ };
614
+ const m = mem.openMemory(dir);
615
+ try {
616
+ mem.recordOutcomes(m, [row]);
617
+ } finally {
618
+ m.close();
619
+ }
620
+ } catch (e) {
621
+ process.stderr.write(`autopilot: warning: cannot record the stop row for ${c.id}: ${e.message || e}\n`);
622
+ }
623
+ }
624
+ if (kind === "flow" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && (parsed.ok === true || parsed.ok === false) && !parsed.paused) {
625
+ try {
626
+ const mem = await import("../lib/memory.mjs");
627
+ const readOr = (p) => {
628
+ try {
629
+ return readFileSync(p, "utf8");
630
+ } catch {
631
+ return "";
632
+ }
633
+ };
634
+ const sid = process.env.CLAUDE_CODE_SESSION_ID;
635
+ const jp = journalPath(run);
636
+ const ids = mem.runAgentIds([jp ? readOr(jp) : ""]);
637
+ const blocks = mem.attributeBlocks(readOr(join(dir, ".doug/.state/blocks.jsonl")), ids);
638
+ const denials = mem.attributeDenials(sid ? readOr(join(dir, ".doug/.state/trace", `${sid.replace(/[^A-Za-z0-9_.-]/g, "_")}.jsonl`)) : "", ids);
639
+ const scan = await mem.historyScanner(dir);
640
+ const entries = [{ kind: "report", source: "last-report.json", value: parsed }];
641
+ for (const b of blocks) entries.push({ kind: "block", source: "blocks.jsonl", value: b });
642
+ for (const d of denials) entries.push({ kind: "denial", source: "trace", value: d });
643
+ mem.writeRunHistory(dir, { card: c.id, run, entries, scan });
644
+ } catch (e) {
645
+ process.stderr.write(`autopilot: warning: cannot write the run history for ${c.id}: ${e.message || e}\n`);
646
+ }
404
647
  }
405
648
  const v = classifyNotification(kind, { status: "completed", result: parsed === null ? text : parsed });
649
+ if (v.action === "ask") {
650
+ let answered = false;
651
+ try {
652
+ const saved = JSON.parse(readFileSync(join(dir, ".doug/.state/reports", c.id, "queen-args.json"), "utf8"));
653
+ const rep = parsed?.report ?? parsed;
654
+ const stop = (Array.isArray(rep?.history) ? rep.history : []).filter((h) => h && h.result === "stopped").pop();
655
+ answered =
656
+ Array.isArray(saved?.options?.answers) &&
657
+ saved.options.answers.some(
658
+ (a) => a && a.run === run && a.seat === rep?.stoppedAt && stop && a.round === stop.round && a.question === rep?.detail,
659
+ );
660
+ } catch {
661
+ // no readable args: not answered
662
+ }
663
+ if (answered) {
664
+ say(`${c.id} resume with the answer`);
665
+ break;
666
+ }
667
+ }
668
+ if (v.action === "ask" && v.reason === "gate-timeout" && !c.resumes && othersInFlight(s, c.id).length > 0) {
669
+ c.state = "stopped";
670
+ c.reason = "gate-timeout: rerun alone pending";
671
+ save(s);
672
+ say(`${c.id} rerun alone gate-timeout`);
673
+ break;
674
+ }
406
675
  say(`${c.id} ${v.action}${v.reason ? ` ${v.reason}` : ""}`);
407
676
  break;
408
677
  }
678
+ case "answer": {
679
+ if (!id) usage("answer needs a card id");
680
+ const answer = need("answer");
681
+ if (answer.trim() === "") usage("--answer needs a non-blank answer");
682
+ const s = load();
683
+ const c = cardOf(s, id);
684
+ const { rep, argsPath, qa, entry } = savedStop(id, c);
685
+ entry.answer = answer;
686
+ qa.options = { ...(qa.options || {}), answers: [...(Array.isArray(qa.options?.answers) ? qa.options.answers : []), entry] };
687
+ writeFileSync(argsPath, JSON.stringify(qa, null, 2) + "\n");
688
+ say(`${id} answer recorded for ${entry.seat}`);
689
+ break;
690
+ }
691
+ case "rerun-alone": {
692
+ if (!id) usage("rerun-alone needs a card id");
693
+ const s = load();
694
+ const c = cardOf(s, id);
695
+ const others = othersInFlight(s, id);
696
+ if (others.length > 0) fail(`${others.map((x) => x.id).join(", ")} still in flight; rerun ${id} alone after it lands`);
697
+ if (c.resumes >= 1) fail(`${id} was already resumed once`);
698
+ const { rep, argsPath, qa, entry } = savedStop(id, c);
699
+ entry.answer = RERUN_ALONE;
700
+ qa.options = { ...(qa.options || {}), answers: [...(Array.isArray(qa.options?.answers) ? qa.options.answers : []), entry] };
701
+ writeFileSync(argsPath, JSON.stringify(qa, null, 2) + "\n");
702
+ say(`${id} rerun alone recorded for ${rep.stoppedAt}`);
703
+ break;
704
+ }
705
+ case "leave": {
706
+ if (!id) usage("leave needs a card id");
707
+ const reason = need("reason");
708
+ if (reason.trim() === "") usage("--reason needs a non-blank reason");
709
+ const s = load();
710
+ const c = cardOf(s, id);
711
+ c.left = reason;
712
+ save(s);
713
+ say(`${id} left In flow: ${reason}`);
714
+ break;
715
+ }
409
716
  case "resumed": {
410
717
  if (!id) usage("resumed needs a card id");
411
718
  const run = need("run");
@@ -413,6 +720,9 @@ switch (cmd) {
413
720
  const c = cardOf(s, id);
414
721
  c.resumes = (c.resumes || 0) + 1;
415
722
  c.runId = run;
723
+ c.state = "running";
724
+ const journal = journalPath(run);
725
+ if (journal) c.journals = [...(Array.isArray(c.journals) ? c.journals : []), journal];
416
726
  save(s);
417
727
  say(`${id} resumed as ${run}`);
418
728
  break;
@@ -427,8 +737,40 @@ switch (cmd) {
427
737
  say(`${id} lock wait ${c.lockWaitMs}ms`);
428
738
  break;
429
739
  }
740
+ case "clear": {
741
+ if (!existsSync(file)) say("nothing to clear");
742
+ else {
743
+ const s = readAutopilot(dir);
744
+ if (!s) say("kept the unreadable file");
745
+ else if (clearable(s, new Date())) {
746
+ rmSync(file);
747
+ say("cleared");
748
+ } else say("kept the running run");
749
+ }
750
+ break;
751
+ }
430
752
  case "end": {
431
753
  const s = load();
754
+ if (s.cards.length > 0) {
755
+ let board;
756
+ try {
757
+ board = JSON.parse(readFileSync(join(dir, ".doug/board.json"), "utf8"));
758
+ } catch (e) {
759
+ fail(`cannot read the board to check cards In flow: ${e.code || e.message}`);
760
+ }
761
+ const stuck = s.cards.filter((c) => {
762
+ if ((board.cards || []).find((b) => b.id === c.id)?.column !== "flow") return false;
763
+ if (c.resumes >= 1 || (typeof c.left === "string" && c.left.trim() !== "")) return false;
764
+ let answers = [];
765
+ try {
766
+ answers = JSON.parse(readFileSync(join(dir, ".doug/.state/reports", c.id, "queen-args.json"), "utf8")).options.answers;
767
+ } catch {
768
+ // no readable args: not answered
769
+ }
770
+ return !(Array.isArray(answers) && answers.some((a) => a && a.run === c.runId && a.answer !== RERUN_ALONE));
771
+ });
772
+ if (stuck.length > 0) fail(`cannot end: ${stuck.map((c) => c.id).join(", ")} still In flow, neither resumed nor answered`);
773
+ }
432
774
  s.status = "ended";
433
775
  s.endedAt = new Date().toISOString();
434
776
  save(s);
@@ -436,5 +778,5 @@ switch (cmd) {
436
778
  break;
437
779
  }
438
780
  default:
439
- usage(`unknown subcommand${cmd ? ` ${cmd}` : ""}; use start, add, card, step, land, stop, filed, caps, pick, launched, report, resumed, lock-wait, or end`);
781
+ usage(`unknown subcommand${cmd ? ` ${cmd}` : ""}; use start, add, card, step, land, stop, filed, caps, pick, launched, report, answer, rerun-alone, leave, resumed, lock-wait, clear, or end`);
440
782
  }