@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
@@ -9,6 +9,13 @@ import { isModelName, EFFORTS } from "./models.mjs";
9
9
 
10
10
  export const PLAN_RELPATH = ".doug/plan.json";
11
11
  export const REPORT_RELPATH = ".doug/.state/last-report.json";
12
+ // Per-card plans (card parallel-card-queens): a card run as its own queen keeps its plan at the tracked
13
+ // .doug/plans/<id>.json and its report at .doug/.state/reports/<id>/last-report.json.
14
+ export const PLANS_RELPATH = ".doug/plans";
15
+ export const REPORTS_RELPATH = ".doug/.state/reports";
16
+ export const DEFAULT_INTEGRATION_WORKTREE = ".claude/worktrees/doug-integration";
17
+ export const planPathFor = (card) => `${PLANS_RELPATH}/${card}.json`;
18
+ export const reportPathFor = (card) => `${REPORTS_RELPATH}/${card}/last-report.json`;
12
19
  // One draft per card of a batch (card parallel-cards): the planner for card <id> writes .doug/.state/drafts/<id>.json
13
20
  // and `plan.mjs merge` joins them into the plan file.
14
21
  export const DRAFTS_RELPATH = ".doug/.state/drafts";
@@ -42,7 +49,8 @@ export function crewOf(plan, task) {
42
49
  // feature, never more than eight. The validator warns; it does not refuse, since the planner may have a reason.
43
50
  export const FEATURE_TASKS = 5;
44
51
  export const TASK_CAP = 8;
45
- export const DEFAULT_BUDGET = { agents: 12, tokens: 400000, wallMinutes: 40 };
52
+ // agents 26 is the minimum that lets the default fixAttempts 5 run with one adversary seat when Codex is down and the Claude fallback runs on every pass (card flow-budget-after-parallel-adversary); a supervisor seat or a fix retry is not covered.
53
+ export const DEFAULT_BUDGET = { agents: 26, tokens: 400000, wallMinutes: 40 };
46
54
 
47
55
  export function loadPlan(dir, relpath = PLAN_RELPATH) {
48
56
  const file = join(dir, relpath);
@@ -54,8 +62,24 @@ export function draftPath(card) {
54
62
  return `${DRAFTS_RELPATH}/${card}.json`;
55
63
  }
56
64
 
57
- export function savePlan(dir, plan) {
58
- const file = join(dir, PLAN_RELPATH);
65
+ // The plan a card's commands read: its per-card plan when one exists; else the single .doug/plan.json, but only
66
+ // when no card is given or that plan is this card's (plan.card equals it, or plan.cards includes it), so a stale
67
+ // plan for another card is never read. Null otherwise, and when neither exists.
68
+ export function planFor(dir, card) {
69
+ const hasCard = typeof card === "string" && card !== "";
70
+ if (hasCard && existsSync(join(dir, planPathFor(card)))) return loadPlan(dir, planPathFor(card));
71
+ const plan = loadPlan(dir);
72
+ if (!plan || !hasCard) return plan;
73
+ return plan.card === card || (Array.isArray(plan.cards) && plan.cards.includes(card)) ? plan : null;
74
+ }
75
+
76
+ // The plan's integration worktree: its own when set (a per-card run), else the fixed shared path.
77
+ export function integrationWorktreeFor(plan) {
78
+ return (plan && plan.integrationWorktree) || DEFAULT_INTEGRATION_WORKTREE;
79
+ }
80
+
81
+ export function savePlan(dir, plan, relpath = PLAN_RELPATH) {
82
+ const file = join(dir, relpath);
59
83
  mkdirSync(dirname(file), { recursive: true });
60
84
  const tmp = file + "." + process.pid + ".tmp";
61
85
  writeFileSync(tmp, JSON.stringify(plan, null, 2) + "\n");
@@ -75,8 +99,8 @@ export function withSpecHashes(plan) {
75
99
  return { ...plan, tasks: plan.tasks.map((t) => ({ ...t, specHash: specHash(t) })) };
76
100
  }
77
101
 
78
- export function loadReport(dir) {
79
- const file = join(dir, REPORT_RELPATH);
102
+ export function loadReport(dir, relpath = REPORT_RELPATH) {
103
+ const file = join(dir, relpath);
80
104
  if (!existsSync(file)) return null;
81
105
  try {
82
106
  return unwrapReport(JSON.parse(readFileSync(file, "utf8")));
@@ -395,6 +419,11 @@ export function validatePlan(plan) {
395
419
  });
396
420
  }
397
421
  if (!Array.isArray(plan.verify) || !plan.verify.every((v) => typeof v === "string" && v.trim())) errors.push("verify must be an array of command strings");
422
+ if (plan.integrationWorktree !== undefined) {
423
+ const w = plan.integrationWorktree;
424
+ const under = typeof w === "string" && w.startsWith(".claude/worktrees/") && w.length > ".claude/worktrees/".length && !w.split("/").includes("..");
425
+ if (!under) errors.push("integrationWorktree must be a path under .claude/worktrees/ (the gate only recognises worktrees there)");
426
+ }
398
427
  if (plan.card !== undefined && (typeof plan.card !== "string" || !plan.card.trim())) errors.push("card must be a non-empty string when present");
399
428
  // A batch (several cards in one plan) lists its cards; every task then names the card it belongs to.
400
429
  let cards = null;
@@ -198,15 +198,15 @@ export function retireTaskWorktrees(dir, { tasks = [], saveDir }) {
198
198
  return result;
199
199
  }
200
200
 
201
- // Retires the integration worktree at .claude/worktrees/doug-integration, if one is registered (card
201
+ // Retires the integration worktree at `path` (default .claude/worktrees/doug-integration; a per-card run passes its own), if one is registered (card
202
202
  // integration-worktree-stale, 2026-09-08): a leftover from an abandoned run must not survive into the next one,
203
203
  // since the workflow's own leftover-vs-this-run's-own check (integratePrompt's firstIntegration) only runs inside
204
204
  // a run already underway. Removed with --force; the branch is never renamed or deleted (the workflow's
205
205
  // `git worktree add -B` on the next run's first integration resets it from the base branch). A worktree that is
206
206
  // not there is not an error.
207
207
  // Returns { removed: string | null } - the path removed, or null when none was registered.
208
- export function retireIntegrationWorktree(dir) {
209
- const target = join(realpathOrSelf(dir), ".claude", "worktrees", "doug-integration");
208
+ export function retireIntegrationWorktree(dir, path = ".claude/worktrees/doug-integration") {
209
+ const target = resolve(realpathOrSelf(dir), path);
210
210
  const match = taskWorktrees(dir).find((wt) => realpathOrSelf(wt.path) === realpathOrSelf(target));
211
211
  if (!match) return { removed: null };
212
212
  const removal = tryGit(dir, ["worktree", "remove", "--force", match.path]);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougharness/flow",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+ // The lead's autopilot state file writer (card autopilot-live-queue): .doug/.state/autopilot.json, which the live pane
3
+ // and the served board's autopilot panel read. Each subcommand prints one line `autopilot: <what>`.
4
+ // autopilot.mjs start --cards <id,id,...> --usd-cap <n> --wall-cap <min> [--context-threshold <pct>]
5
+ // autopilot.mjs add <id> [--usd-cap <n>] [--wall-cap <min>]
6
+ // autopilot.mjs card <id>
7
+ // autopilot.mjs step <id> <research|tester|coder|reviewer|gate|commit|record>
8
+ // autopilot.mjs land <id> --commit <sha>
9
+ // autopilot.mjs stop <id> --reason "<why>"
10
+ // autopilot.mjs filed <id> --kind <follow-up|user> [--from <card>] --position "<text>"
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> writes .doug/.state/reports/<id>/queen-args.json, prints its path
14
+ // autopilot.mjs launched <id> --run <wf_id> --kind <flow|hand> --worktree <abs> --args-file <path>
15
+ // autopilot.mjs report --run <wf_id> --file <result.json> saves the queen's report, prints land|resume|ask
16
+ // autopilot.mjs resumed <id> --run <wf_id>
17
+ // autopilot.mjs lock-wait <id> --ms <n>
18
+ // autopilot.mjs end
19
+ // --dir <d> names the project (default cwd). Exit 0 done; 1 no run or unknown card; 2 bad usage.
20
+ import { execFileSync } from "node:child_process";
21
+ import { copyFileSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
22
+ import { dirname, join, resolve } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { loadModels } from "../lib/models.mjs";
25
+ import { AUTOPILOT_FILE, STEPS, readAutopilot } from "../lib/autopilot-view.mjs";
26
+ import { contextThreshold } from "../lib/context.mjs";
27
+ import { classifyNotification, pickCards, workersMax } from "../lib/coordinator.mjs";
28
+ import { nextReadyCards } from "../lib/board.mjs";
29
+ import { planFor, reportPathFor } from "../lib/plan.mjs";
30
+
31
+ const argv = process.argv.slice(2);
32
+ const opts = {};
33
+ const pos = [];
34
+ for (let i = 0; i < argv.length; i++) {
35
+ if (argv[i].startsWith("--")) {
36
+ opts[argv[i].slice(2)] = argv[i + 1] ?? "";
37
+ i++;
38
+ } else pos.push(argv[i]);
39
+ }
40
+ const [cmd, id, arg2] = pos;
41
+ const dir = resolve(opts.dir || process.cwd());
42
+ const file = join(dir, AUTOPILOT_FILE);
43
+
44
+ const usage = (msg) => {
45
+ process.stderr.write(`autopilot: ${msg}\n`);
46
+ process.exit(2);
47
+ };
48
+ const fail = (msg) => {
49
+ process.stderr.write(`autopilot: ${msg}\n`);
50
+ process.exit(1);
51
+ };
52
+ const number = (flag, v) => {
53
+ const n = Number(v);
54
+ if (v === undefined || v === "" || !Number.isFinite(n) || n < 0) usage(`--${flag} needs a non-negative number`);
55
+ return n;
56
+ };
57
+ const need = (flag) => {
58
+ if (opts[flag] === undefined || opts[flag] === "") usage(`--${flag} is required`);
59
+ return opts[flag];
60
+ };
61
+
62
+ // tmp + rename, so a reader never sees a half-written file; the tmp name is not left behind.
63
+ function save(state) {
64
+ state.updatedAt = new Date().toISOString();
65
+ mkdirSync(dirname(file), { recursive: true });
66
+ const tmp = `${file}.${process.pid}.tmp`;
67
+ writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
68
+ renameSync(tmp, file);
69
+ }
70
+ const say = (what) => process.stdout.write(`autopilot: ${what}\n`);
71
+ const NO_RUN = "no autopilot run; run autopilot.mjs start first";
72
+
73
+ function load() {
74
+ const s = readAutopilot(dir);
75
+ if (!s) fail(NO_RUN);
76
+ if (!Array.isArray(s.cards)) s.cards = [];
77
+ return s;
78
+ }
79
+ function cardOf(s, cardId) {
80
+ const c = s.cards.find((x) => x.id === cardId);
81
+ if (!c) fail(`unknown card ${cardId}`);
82
+ return c;
83
+ }
84
+ const clearStep = (c) => {
85
+ delete c.step;
86
+ delete c.stepStartedAt;
87
+ };
88
+
89
+ function boardCard(cardId) {
90
+ try {
91
+ const b = JSON.parse(readFileSync(join(dir, ".doug/board.json"), "utf8"));
92
+ return b.cards.find((c) => c.id === cardId) || null;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ // A swarm card carries the tag "swarm", a field swarm: true, or an explicit request in its goal ("run it as a
98
+ // swarm", "swarm: on"); the word swarm alone in a goal does not make one.
99
+ function isSwarmCard(c) {
100
+ if (c.swarm === true || (Array.isArray(c.tags) && c.tags.includes("swarm"))) return true;
101
+ return /\b(?:run|build|land)s?\b[^.]{0,20}\bas a swarm\b|\bswarm:\s*on\b/i.test(c.goal ?? "");
102
+ }
103
+ function queued(cardId) {
104
+ const b = boardCard(cardId);
105
+ const c = { id: cardId };
106
+ for (const k of ["title", "size", "class"]) if (b && b[k] !== undefined) c[k] = b[k];
107
+ c.state = "queued";
108
+ return c;
109
+ }
110
+
111
+ switch (cmd) {
112
+ case "start": {
113
+ const ids = need("cards").split(",").map((x) => x.trim()).filter(Boolean);
114
+ if (!ids.length) usage("--cards names no card");
115
+ const usdCap = number("usd-cap", opts["usd-cap"]);
116
+ const wallCap = number("wall-cap", opts["wall-cap"]);
117
+ let threshold;
118
+ if (opts["context-threshold"] !== undefined) threshold = number("context-threshold", opts["context-threshold"]);
119
+ else {
120
+ let config = {};
121
+ try {
122
+ config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
123
+ } catch {
124
+ // absent or unreadable: the default
125
+ }
126
+ threshold = contextThreshold(config);
127
+ }
128
+ const now = new Date().toISOString();
129
+ save({
130
+ version: 1,
131
+ status: "running",
132
+ startedAt: now,
133
+ updatedAt: now,
134
+ endedAt: null,
135
+ caps: { usd: usdCap, wallMin: wallCap, contextPct: threshold },
136
+ cards: ids.map(queued),
137
+ filed: [],
138
+ });
139
+ say(`started with ${ids.length} card${ids.length === 1 ? "" : "s"}`);
140
+ break;
141
+ }
142
+ case "add": {
143
+ if (!id) usage("add needs a card id");
144
+ const s = load();
145
+ if (!s.caps || typeof s.caps !== "object") s.caps = {};
146
+ if (opts["usd-cap"] !== undefined) s.caps.usd = number("usd-cap", opts["usd-cap"]);
147
+ if (opts["wall-cap"] !== undefined) s.caps.wallMin = number("wall-cap", opts["wall-cap"]);
148
+ const dup = s.cards.some((c) => c.id === id);
149
+ if (!dup) s.cards.push(queued(id));
150
+ save(s);
151
+ say(dup ? `${id} already queued` : `added ${id}`);
152
+ break;
153
+ }
154
+ case "card": {
155
+ if (!id) usage("card needs a card id");
156
+ const s = load();
157
+ const c = cardOf(s, id);
158
+ c.state = "running";
159
+ clearStep(c);
160
+ save(s);
161
+ say(`${id} running`);
162
+ break;
163
+ }
164
+ case "step": {
165
+ if (!id || !arg2) usage("step needs a card id and a step");
166
+ if (!STEPS.includes(arg2)) usage(`step must be one of ${STEPS.join(", ")}`);
167
+ const s = load();
168
+ const c = cardOf(s, id);
169
+ c.step = arg2;
170
+ c.stepStartedAt = new Date().toISOString();
171
+ save(s);
172
+ say(`${id} ${arg2}`);
173
+ break;
174
+ }
175
+ case "land": {
176
+ if (!id) usage("land needs a card id");
177
+ const commit = need("commit");
178
+ const s = load();
179
+ const c = cardOf(s, id);
180
+ c.state = "landed";
181
+ c.commit = commit;
182
+ delete c.reason;
183
+ clearStep(c);
184
+ save(s);
185
+ say(`${id} landed ${commit}`);
186
+ break;
187
+ }
188
+ case "stop": {
189
+ if (!id) usage("stop needs a card id");
190
+ const reason = need("reason");
191
+ const s = load();
192
+ const c = cardOf(s, id);
193
+ c.state = "stopped";
194
+ c.reason = reason;
195
+ clearStep(c);
196
+ save(s);
197
+ say(`${id} stopped`);
198
+ break;
199
+ }
200
+ case "filed": {
201
+ if (!id) usage("filed needs a card id");
202
+ const kind = need("kind");
203
+ if (kind !== "follow-up" && kind !== "user") usage("--kind must be follow-up or user");
204
+ const position = need("position");
205
+ const s = load();
206
+ if (!Array.isArray(s.filed)) s.filed = [];
207
+ s.filed.push({ id, kind, ...(opts.from ? { from: opts.from } : {}), position });
208
+ save(s);
209
+ say(`filed ${id} (${kind})`);
210
+ break;
211
+ }
212
+ case "caps": {
213
+ const keys = ["usd", "wall", "context"].filter((k) => opts[k] !== undefined);
214
+ if (!keys.length) usage("caps needs --usd, --wall, or --context");
215
+ const vals = Object.fromEntries(keys.map((k) => [k, number(k, opts[k])]));
216
+ const s = load();
217
+ s.used = {
218
+ ...(s.used || {}),
219
+ ...(vals.usd !== undefined ? { usd: vals.usd } : {}),
220
+ ...(vals.wall !== undefined ? { wallMin: vals.wall } : {}),
221
+ ...(vals.context !== undefined ? { contextPct: vals.context } : {}),
222
+ checkedAt: new Date().toISOString(),
223
+ };
224
+ save(s);
225
+ say("caps recorded");
226
+ break;
227
+ }
228
+ case "pick": {
229
+ const s = load();
230
+ let board;
231
+ try {
232
+ board = JSON.parse(readFileSync(join(dir, ".doug/board.json"), "utf8"));
233
+ } catch {
234
+ fail("no readable .doug/board.json");
235
+ }
236
+ let config = {};
237
+ try {
238
+ config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
239
+ } catch {
240
+ // absent or unreadable: the default
241
+ }
242
+ const max = opts.max !== undefined ? Math.floor(number("max", opts.max)) : workersMax(config, (m) => process.stderr.write(`autopilot: ${m}\n`));
243
+ const trackOf = (c) => (c.track === "hand" ? "hand" : "flow");
244
+ const shape = (c) => ({
245
+ id: c.id,
246
+ goal: c.goal ?? "",
247
+ component: c.component,
248
+ track: trackOf(c),
249
+ class: c.class,
250
+ swarm: isSwarmCard(c),
251
+ });
252
+ const flying = s.cards.filter((c) => ["running", "done", "landing"].includes(c.state));
253
+ const flyingIds = new Set(flying.map((c) => c.id));
254
+ const inFlight = flying.map((c) => {
255
+ const b = boardCard(c.id);
256
+ return b ? shape(b) : { id: c.id, goal: "", track: c.kind === "flow" ? "flow" : "hand" };
257
+ });
258
+ const ready = nextReadyCards(board, { track: "any", batch: Math.max(1, board.cards.length) })
259
+ .cards.filter((c) => !flyingIds.has(c.id))
260
+ .map(shape);
261
+ const plans = {};
262
+ for (const c of [...ready, ...inFlight]) {
263
+ if (c.track !== "flow") continue;
264
+ let p = null;
265
+ try {
266
+ p = planFor(dir, c.id);
267
+ } catch {
268
+ // an unreadable plan counts as no plan
269
+ }
270
+ if (p) plans[c.id] = p;
271
+ }
272
+ const roots = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
273
+ const out = pickCards({ ready, inFlight, slots: Math.max(0, max - inFlight.length), plans, roots });
274
+ for (const h of out.held) process.stderr.write(`held ${h.id}: overlaps ${h.with} (${h.why})\n`);
275
+ process.stdout.write(JSON.stringify(out) + "\n");
276
+ break;
277
+ }
278
+ case "queen-args": {
279
+ if (!id) usage("queen-args needs a card id");
280
+ const kind = need("kind");
281
+ if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
282
+ let args;
283
+ if (kind === "flow") {
284
+ try {
285
+ args = JSON.parse(execFileSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), "plan.mjs"), "json", "--card", id], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 }));
286
+ } catch (e) {
287
+ fail(`plan.mjs json --card ${id} failed: ${String(e.stderr || e.message).trim()}`);
288
+ }
289
+ } else {
290
+ const b = boardCard(id);
291
+ if (!b) fail(`unknown card ${id}`);
292
+ let config = {};
293
+ try {
294
+ config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
295
+ } catch {
296
+ // absent or unreadable: no install command
297
+ }
298
+ args = {
299
+ card: { id: b.id, title: b.title ?? null, size: b.size ?? null, class: b.class ?? null, goal: b.goal ?? "" },
300
+ options: {
301
+ testerAgent: true,
302
+ research: false,
303
+ install: (config.commands && config.commands.install) || null,
304
+ gateCommand: "node .doug/hooks/scripts/verify.mjs",
305
+ branch: `card/${id}`,
306
+ base: "main",
307
+ },
308
+ models: loadModels(dir),
309
+ repoRoot: dir,
310
+ worktreePath: join(dir, ".claude/worktrees", `card-${id}`),
311
+ };
312
+ }
313
+ const out = join(dir, ".doug/.state/reports", id, "queen-args.json");
314
+ mkdirSync(dirname(out), { recursive: true });
315
+ writeFileSync(out, JSON.stringify(args) + "\n");
316
+ process.stdout.write(out + "\n");
317
+ break;
318
+ }
319
+ case "launched": {
320
+ if (!id) usage("launched needs a card id");
321
+ const run = need("run");
322
+ const kind = need("kind");
323
+ if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
324
+ const worktree = need("worktree");
325
+ const argsFile = need("args-file");
326
+ const s = load();
327
+ const c = cardOf(s, id);
328
+ const reportDir = join(dir, ".doug/.state/reports", id);
329
+ mkdirSync(reportDir, { recursive: true });
330
+ try {
331
+ copyFileSync(argsFile, join(reportDir, "queen-args.json"));
332
+ } catch (e) {
333
+ fail(`cannot read --args-file ${argsFile}: ${e.code || e.message}`);
334
+ }
335
+ c.state = "running";
336
+ c.runId = run;
337
+ c.kind = kind;
338
+ c.worktree = worktree;
339
+ clearStep(c);
340
+ save(s);
341
+ say(`${id} launched as ${run}`);
342
+ break;
343
+ }
344
+ case "report": {
345
+ const run = need("run");
346
+ const resultFile = need("file");
347
+ const s = load();
348
+ const c = s.cards.find((x) => x.runId === run);
349
+ if (!c) fail(`unknown run ${run}`);
350
+ let text;
351
+ try {
352
+ text = readFileSync(resultFile, "utf8");
353
+ } catch (e) {
354
+ fail(`cannot read --file ${resultFile}: ${e.code || e.message}`);
355
+ }
356
+ let parsed = null;
357
+ try {
358
+ parsed = JSON.parse(text);
359
+ } catch {
360
+ // unreadable: classify says resume
361
+ }
362
+ const kind = c.kind === "flow" ? "flow" : "hand";
363
+ if (parsed !== null) {
364
+ const dest = kind === "flow" ? join(dir, reportPathFor(c.id)) : join(dir, ".doug/.state/reports", c.id, "card-report.json");
365
+ try {
366
+ mkdirSync(dirname(dest), { recursive: true });
367
+ writeFileSync(dest, JSON.stringify(kind === "hand" && parsed.report ? parsed.report : parsed, null, 2) + "\n");
368
+ } catch (e) {
369
+ process.stderr.write(`autopilot: warning: cannot write ${dest}: ${e.code || e.message}\n`);
370
+ }
371
+ }
372
+ const v = classifyNotification(kind, { status: "completed", result: parsed === null ? text : parsed });
373
+ say(`${c.id} ${v.action}${v.reason ? ` ${v.reason}` : ""}`);
374
+ break;
375
+ }
376
+ case "resumed": {
377
+ if (!id) usage("resumed needs a card id");
378
+ const run = need("run");
379
+ const s = load();
380
+ const c = cardOf(s, id);
381
+ c.resumes = (c.resumes || 0) + 1;
382
+ c.runId = run;
383
+ save(s);
384
+ say(`${id} resumed as ${run}`);
385
+ break;
386
+ }
387
+ case "lock-wait": {
388
+ if (!id) usage("lock-wait needs a card id");
389
+ const ms = number("ms", opts.ms);
390
+ const s = load();
391
+ const c = cardOf(s, id);
392
+ c.lockWaitMs = (c.lockWaitMs || 0) + ms;
393
+ save(s);
394
+ say(`${id} lock wait ${c.lockWaitMs}ms`);
395
+ break;
396
+ }
397
+ case "end": {
398
+ const s = load();
399
+ s.status = "ended";
400
+ s.endedAt = new Date().toISOString();
401
+ save(s);
402
+ say("ended");
403
+ break;
404
+ }
405
+ default:
406
+ usage(`unknown subcommand${cmd ? ` ${cmd}` : ""}; use start, add, card, step, land, stop, filed, caps, pick, launched, report, resumed, lock-wait, or end`);
407
+ }