@dougharness/cli 1.1.1 → 1.3.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 (57) hide show
  1. package/dist/board-serve.js +40 -6
  2. package/dist/board-serve.js.map +1 -1
  3. package/dist/init.js +1 -1
  4. package/dist/init.js.map +1 -1
  5. package/dist/memory-setup.d.ts +7 -0
  6. package/dist/memory-setup.js +70 -46
  7. package/dist/memory-setup.js.map +1 -1
  8. package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +6 -2
  9. package/node_modules/@dougharness/flow/agents/coder.md +23 -0
  10. package/node_modules/@dougharness/flow/agents/implementer.md +1 -1
  11. package/node_modules/@dougharness/flow/agents/planner.md +1 -1
  12. package/node_modules/@dougharness/flow/agents/researcher.md +2 -1
  13. package/node_modules/@dougharness/flow/agents/reviewer.md +3 -1
  14. package/node_modules/@dougharness/flow/agents/spec-writer.md +23 -0
  15. package/node_modules/@dougharness/flow/agents/tester.md +21 -0
  16. package/node_modules/@dougharness/flow/hooks/hooks.json +4 -1
  17. package/node_modules/@dougharness/flow/hooks/register.tsx +94 -0
  18. package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +117 -0
  19. package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +17 -0
  20. package/node_modules/@dougharness/flow/lib/board.mjs +91 -10
  21. package/node_modules/@dougharness/flow/lib/card-land.mjs +115 -0
  22. package/node_modules/@dougharness/flow/lib/card-loop.mjs +778 -0
  23. package/node_modules/@dougharness/flow/lib/coordinator.mjs +393 -0
  24. package/node_modules/@dougharness/flow/lib/cost.mjs +15 -5
  25. package/node_modules/@dougharness/flow/lib/decisions.mjs +4 -2
  26. package/node_modules/@dougharness/flow/lib/embeddings.mjs +144 -3
  27. package/node_modules/@dougharness/flow/lib/land.mjs +27 -8
  28. package/node_modules/@dougharness/flow/lib/memory.mjs +406 -76
  29. package/node_modules/@dougharness/flow/lib/plan.mjs +33 -5
  30. package/node_modules/@dougharness/flow/lib/worktrees.mjs +3 -3
  31. package/node_modules/@dougharness/flow/package.json +1 -1
  32. package/node_modules/@dougharness/flow/scripts/autopilot.mjs +440 -0
  33. package/node_modules/@dougharness/flow/scripts/board.mjs +107 -32
  34. package/node_modules/@dougharness/flow/scripts/cost.mjs +16 -3
  35. package/node_modules/@dougharness/flow/scripts/memory.mjs +33 -10
  36. package/node_modules/@dougharness/flow/scripts/plan.mjs +104 -30
  37. package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +8 -2
  38. package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +30 -10
  39. package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +80 -0
  40. package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
  41. package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
  42. package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +6 -6
  43. package/node_modules/@dougharness/flow/workflows/card-queen.js +846 -0
  44. package/node_modules/@dougharness/flow/workflows/doug-implement.js +63 -23
  45. package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
  46. package/node_modules/@dougharness/gates/lib/anchor.mjs +29 -4
  47. package/node_modules/@dougharness/gates/lib/bash-rules.mjs +196 -6
  48. package/node_modules/@dougharness/gates/lib/gate-lock.mjs +40 -8
  49. package/node_modules/@dougharness/gates/lib/proposals.mjs +41 -5
  50. package/node_modules/@dougharness/gates/lib/scope.mjs +43 -4
  51. package/node_modules/@dougharness/gates/package.json +1 -1
  52. package/node_modules/@dougharness/gates/scripts/reanchor.mjs +2 -1
  53. package/node_modules/@dougharness/gates/scripts/research-cap.mjs +119 -15
  54. package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +156 -14
  55. package/node_modules/@dougharness/gates/scripts/verify.mjs +8 -4
  56. package/package.json +3 -3
  57. package/templates/board-app.js +46 -0
@@ -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";
@@ -17,7 +24,7 @@ export const SIZES = ["S", "M", "L"];
17
24
  export const GATES = ["auto", "human"];
18
25
  // Crew per role (card crew-sizing): researchers before planning, reviewers and adversaries after implementation,
19
26
  // default one each. More coders is a split into more tasks, so there is no coders row.
20
- export const CREW_ROLES = ["researchers", "reviewers", "adversaries"];
27
+ export const CREW_ROLES = ["researchers", "reviewers", "adversaries", "testers"];
21
28
 
22
29
  // Error strings for a crew object at `where` (the plan or a task); [] when valid or absent.
23
30
  function crewErrors(crew, where) {
@@ -55,8 +62,24 @@ export function draftPath(card) {
55
62
  return `${DRAFTS_RELPATH}/${card}.json`;
56
63
  }
57
64
 
58
- export function savePlan(dir, plan) {
59
- 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);
60
83
  mkdirSync(dirname(file), { recursive: true });
61
84
  const tmp = file + "." + process.pid + ".tmp";
62
85
  writeFileSync(tmp, JSON.stringify(plan, null, 2) + "\n");
@@ -76,8 +99,8 @@ export function withSpecHashes(plan) {
76
99
  return { ...plan, tasks: plan.tasks.map((t) => ({ ...t, specHash: specHash(t) })) };
77
100
  }
78
101
 
79
- export function loadReport(dir) {
80
- const file = join(dir, REPORT_RELPATH);
102
+ export function loadReport(dir, relpath = REPORT_RELPATH) {
103
+ const file = join(dir, relpath);
81
104
  if (!existsSync(file)) return null;
82
105
  try {
83
106
  return unwrapReport(JSON.parse(readFileSync(file, "utf8")));
@@ -396,6 +419,11 @@ export function validatePlan(plan) {
396
419
  });
397
420
  }
398
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
+ }
399
427
  if (plan.card !== undefined && (typeof plan.card !== "string" || !plan.card.trim())) errors.push("card must be a non-empty string when present");
400
428
  // A batch (several cards in one plan) lists its cards; every task then names the card it belongs to.
401
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.1",
3
+ "version": "1.3.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,440 @@
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> [--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)
14
+ // autopilot.mjs agent-types --kind <flow|hand> prints the agent types that queen spawns, one per line
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>
18
+ // autopilot.mjs lock-wait <id> --ms <n>
19
+ // autopilot.mjs end
20
+ // --dir <d> names the project (default cwd). Exit 0 done; 1 no run or unknown card; 2 bad usage.
21
+ import { execFileSync } from "node:child_process";
22
+ import { copyFileSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
23
+ import { dirname, join, resolve } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { loadModels } from "../lib/models.mjs";
26
+ import { AUTOPILOT_FILE, STEPS, readAutopilot } from "../lib/autopilot-view.mjs";
27
+ import { contextThreshold } from "../lib/context.mjs";
28
+ import { classifyNotification, pickCards, queenLabel, renameQueenMeta, workersMax } from "../lib/coordinator.mjs";
29
+ import { nextReadyCards } from "../lib/board.mjs";
30
+ import { planFor, reportPathFor } from "../lib/plan.mjs";
31
+
32
+ const argv = process.argv.slice(2);
33
+ const opts = {};
34
+ const pos = [];
35
+ for (let i = 0; i < argv.length; i++) {
36
+ if (argv[i].startsWith("--")) {
37
+ opts[argv[i].slice(2)] = argv[i + 1] ?? "";
38
+ i++;
39
+ } else pos.push(argv[i]);
40
+ }
41
+ const [cmd, id, arg2] = pos;
42
+ const dir = resolve(opts.dir || process.cwd());
43
+ const file = join(dir, AUTOPILOT_FILE);
44
+
45
+ const usage = (msg) => {
46
+ process.stderr.write(`autopilot: ${msg}\n`);
47
+ process.exit(2);
48
+ };
49
+ const fail = (msg) => {
50
+ process.stderr.write(`autopilot: ${msg}\n`);
51
+ process.exit(1);
52
+ };
53
+ const number = (flag, v) => {
54
+ const n = Number(v);
55
+ if (v === undefined || v === "" || !Number.isFinite(n) || n < 0) usage(`--${flag} needs a non-negative number`);
56
+ return n;
57
+ };
58
+ const need = (flag) => {
59
+ if (opts[flag] === undefined || opts[flag] === "") usage(`--${flag} is required`);
60
+ return opts[flag];
61
+ };
62
+
63
+ // tmp + rename, so a reader never sees a half-written file; the tmp name is not left behind.
64
+ function save(state) {
65
+ state.updatedAt = new Date().toISOString();
66
+ mkdirSync(dirname(file), { recursive: true });
67
+ const tmp = `${file}.${process.pid}.tmp`;
68
+ writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
69
+ renameSync(tmp, file);
70
+ }
71
+ const say = (what) => process.stdout.write(`autopilot: ${what}\n`);
72
+ const NO_RUN = "no autopilot run; run autopilot.mjs start first";
73
+
74
+ function load() {
75
+ const s = readAutopilot(dir);
76
+ if (!s) fail(NO_RUN);
77
+ if (!Array.isArray(s.cards)) s.cards = [];
78
+ return s;
79
+ }
80
+ function cardOf(s, cardId) {
81
+ const c = s.cards.find((x) => x.id === cardId);
82
+ if (!c) fail(`unknown card ${cardId}`);
83
+ return c;
84
+ }
85
+ const clearStep = (c) => {
86
+ delete c.step;
87
+ delete c.stepStartedAt;
88
+ };
89
+
90
+ function boardCard(cardId) {
91
+ try {
92
+ const b = JSON.parse(readFileSync(join(dir, ".doug/board.json"), "utf8"));
93
+ return b.cards.find((c) => c.id === cardId) || null;
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ // A swarm card carries the tag "swarm", a field swarm: true, or an explicit request in its goal ("run it as a
99
+ // swarm", "swarm: on"); the word swarm alone in a goal does not make one.
100
+ function isSwarmCard(c) {
101
+ if (c.swarm === true || (Array.isArray(c.tags) && c.tags.includes("swarm"))) return true;
102
+ return /\b(?:run|build|land)s?\b[^.]{0,20}\bas a swarm\b|\bswarm:\s*on\b/i.test(c.goal ?? "");
103
+ }
104
+ function queued(cardId) {
105
+ const b = boardCard(cardId);
106
+ const c = { id: cardId };
107
+ for (const k of ["title", "size", "class"]) if (b && b[k] !== undefined) c[k] = b[k];
108
+ c.state = "queued";
109
+ return c;
110
+ }
111
+
112
+ switch (cmd) {
113
+ case "start": {
114
+ const ids = need("cards").split(",").map((x) => x.trim()).filter(Boolean);
115
+ if (!ids.length) usage("--cards names no card");
116
+ const usdCap = number("usd-cap", opts["usd-cap"]);
117
+ const wallCap = number("wall-cap", opts["wall-cap"]);
118
+ let threshold;
119
+ if (opts["context-threshold"] !== undefined) threshold = number("context-threshold", opts["context-threshold"]);
120
+ else {
121
+ let config = {};
122
+ try {
123
+ config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
124
+ } catch {
125
+ // absent or unreadable: the default
126
+ }
127
+ threshold = contextThreshold(config);
128
+ }
129
+ const now = new Date().toISOString();
130
+ save({
131
+ version: 1,
132
+ status: "running",
133
+ startedAt: now,
134
+ updatedAt: now,
135
+ endedAt: null,
136
+ caps: { usd: usdCap, wallMin: wallCap, contextPct: threshold },
137
+ cards: ids.map(queued),
138
+ filed: [],
139
+ });
140
+ say(`started with ${ids.length} card${ids.length === 1 ? "" : "s"}`);
141
+ break;
142
+ }
143
+ case "add": {
144
+ if (!id) usage("add needs a card id");
145
+ const s = load();
146
+ if (!s.caps || typeof s.caps !== "object") s.caps = {};
147
+ if (opts["usd-cap"] !== undefined) s.caps.usd = number("usd-cap", opts["usd-cap"]);
148
+ if (opts["wall-cap"] !== undefined) s.caps.wallMin = number("wall-cap", opts["wall-cap"]);
149
+ const dup = s.cards.some((c) => c.id === id);
150
+ if (!dup) s.cards.push(queued(id));
151
+ save(s);
152
+ say(dup ? `${id} already queued` : `added ${id}`);
153
+ break;
154
+ }
155
+ case "card": {
156
+ if (!id) usage("card needs a card id");
157
+ const s = load();
158
+ const c = cardOf(s, id);
159
+ c.state = "running";
160
+ clearStep(c);
161
+ save(s);
162
+ say(`${id} running`);
163
+ break;
164
+ }
165
+ case "step": {
166
+ if (!id || !arg2) usage("step needs a card id and a step");
167
+ if (!STEPS.includes(arg2)) usage(`step must be one of ${STEPS.join(", ")}`);
168
+ const s = load();
169
+ const c = cardOf(s, id);
170
+ c.step = arg2;
171
+ c.stepStartedAt = new Date().toISOString();
172
+ save(s);
173
+ say(`${id} ${arg2}`);
174
+ break;
175
+ }
176
+ case "land": {
177
+ if (!id) usage("land needs a card id");
178
+ const commit = need("commit");
179
+ const s = load();
180
+ const c = cardOf(s, id);
181
+ c.state = "landed";
182
+ c.commit = commit;
183
+ delete c.reason;
184
+ clearStep(c);
185
+ save(s);
186
+ say(`${id} landed ${commit}`);
187
+ break;
188
+ }
189
+ case "stop": {
190
+ if (!id) usage("stop needs a card id");
191
+ const reason = need("reason");
192
+ const s = load();
193
+ const c = cardOf(s, id);
194
+ c.state = "stopped";
195
+ c.reason = reason;
196
+ clearStep(c);
197
+ save(s);
198
+ say(`${id} stopped`);
199
+ break;
200
+ }
201
+ case "filed": {
202
+ if (!id) usage("filed needs a card id");
203
+ const kind = need("kind");
204
+ if (kind !== "follow-up" && kind !== "user") usage("--kind must be follow-up or user");
205
+ const position = need("position");
206
+ const s = load();
207
+ if (!Array.isArray(s.filed)) s.filed = [];
208
+ s.filed.push({ id, kind, ...(opts.from ? { from: opts.from } : {}), position });
209
+ save(s);
210
+ say(`filed ${id} (${kind})`);
211
+ break;
212
+ }
213
+ case "caps": {
214
+ const keys = ["usd", "wall", "context"].filter((k) => opts[k] !== undefined);
215
+ if (!keys.length) usage("caps needs --usd, --wall, or --context");
216
+ const vals = Object.fromEntries(keys.map((k) => [k, number(k, opts[k])]));
217
+ const s = load();
218
+ s.used = {
219
+ ...(s.used || {}),
220
+ ...(vals.usd !== undefined ? { usd: vals.usd } : {}),
221
+ ...(vals.wall !== undefined ? { wallMin: vals.wall } : {}),
222
+ ...(vals.context !== undefined ? { contextPct: vals.context } : {}),
223
+ checkedAt: new Date().toISOString(),
224
+ };
225
+ save(s);
226
+ say("caps recorded");
227
+ break;
228
+ }
229
+ case "pick": {
230
+ const s = load();
231
+ let board;
232
+ try {
233
+ board = JSON.parse(readFileSync(join(dir, ".doug/board.json"), "utf8"));
234
+ } catch {
235
+ fail("no readable .doug/board.json");
236
+ }
237
+ let config = {};
238
+ try {
239
+ config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
240
+ } catch {
241
+ // absent or unreadable: the default
242
+ }
243
+ const max = opts.max !== undefined ? Math.floor(number("max", opts.max)) : workersMax(config, (m) => process.stderr.write(`autopilot: ${m}\n`));
244
+ const trackOf = (c) => (c.track === "hand" ? "hand" : "flow");
245
+ const shape = (c) => ({
246
+ id: c.id,
247
+ goal: c.goal ?? "",
248
+ component: c.component,
249
+ track: trackOf(c),
250
+ class: c.class,
251
+ swarm: isSwarmCard(c),
252
+ });
253
+ const flying = s.cards.filter((c) => ["running", "done", "landing"].includes(c.state));
254
+ const flyingIds = new Set(flying.map((c) => c.id));
255
+ const inFlight = flying.map((c) => {
256
+ const b = boardCard(c.id);
257
+ return b ? shape(b) : { id: c.id, goal: "", track: c.kind === "flow" ? "flow" : "hand" };
258
+ });
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);
262
+ const plans = {};
263
+ for (const c of [...ready, ...inFlight]) {
264
+ if (c.track !== "flow") continue;
265
+ let p = null;
266
+ try {
267
+ p = planFor(dir, c.id);
268
+ } catch {
269
+ // an unreadable plan counts as no plan
270
+ }
271
+ if (p) plans[c.id] = p;
272
+ }
273
+ const roots = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
274
+ const out = pickCards({ ready, inFlight, slots: Math.max(0, max - inFlight.length), plans, roots });
275
+ for (const h of out.held) process.stderr.write(`held ${h.id}: overlaps ${h.with} (${h.why})\n`);
276
+ process.stdout.write(JSON.stringify(out) + "\n");
277
+ break;
278
+ }
279
+ case "queen-args": {
280
+ if (!id) usage("queen-args needs a card id");
281
+ const kind = need("kind");
282
+ if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
283
+ let args;
284
+ if (kind === "flow") {
285
+ try {
286
+ 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 }));
287
+ } catch (e) {
288
+ fail(`plan.mjs json --card ${id} failed: ${String(e.stderr || e.message).trim()}`);
289
+ }
290
+ } else {
291
+ const b = boardCard(id);
292
+ if (!b) fail(`unknown card ${id}`);
293
+ let config = {};
294
+ try {
295
+ config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
296
+ } catch {
297
+ // absent or unreadable: no install command
298
+ }
299
+ args = {
300
+ card: { id: b.id, title: b.title ?? null, size: b.size ?? null, class: b.class ?? null, goal: b.goal ?? "" },
301
+ options: {
302
+ testerAgent: true,
303
+ research: false,
304
+ install: (config.commands && config.commands.install) || null,
305
+ gateCommand: "node .doug/hooks/scripts/verify.mjs",
306
+ branch: `card/${id}`,
307
+ base: "main",
308
+ },
309
+ models: loadModels(dir),
310
+ repoRoot: dir,
311
+ worktreePath: join(dir, ".claude/worktrees", `card-${id}`),
312
+ };
313
+ }
314
+ const out = join(dir, ".doug/.state/reports", id, "queen-args.json");
315
+ mkdirSync(dirname(out), { recursive: true });
316
+ writeFileSync(out, JSON.stringify(args) + "\n");
317
+ const flying = (readAutopilot(dir)?.cards ?? []).filter((c) => ["running", "done", "landing"].includes(c.state)).map((c) => c.id);
318
+ const batch = String(opts.with ?? "").split(",").map((x) => x.trim()).filter(Boolean);
319
+ const srcFile = join(dirname(fileURLToPath(import.meta.url)), "..", "workflows", kind === "hand" ? "card-queen.js" : "doug-implement.js");
320
+ let renamed;
321
+ try {
322
+ renamed = renameQueenMeta(readFileSync(srcFile, "utf8"), { kind, label: queenLabel(id, [...flying, ...batch]) });
323
+ } catch (e) {
324
+ fail(`cannot build the per-card queen from ${srcFile}: ${e.message}`);
325
+ }
326
+ const script = join(dirname(out), "queen.js");
327
+ rmSync(script, { force: true });
328
+ writeFileSync(script, renamed);
329
+ process.stdout.write(out + "\n" + script + "\n");
330
+ break;
331
+ }
332
+ case "agent-types": {
333
+ const kind = need("kind");
334
+ if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
335
+ const wf = join(dirname(fileURLToPath(import.meta.url)), "..", "workflows", kind === "hand" ? "card-queen.js" : "doug-implement.js");
336
+ let src;
337
+ try {
338
+ src = readFileSync(wf, "utf8");
339
+ } catch (e) {
340
+ fail(`cannot read ${wf}: ${e.message}`);
341
+ }
342
+ if (kind === "hand") {
343
+ const m = src.match(/const AGENT_TYPES = \{([^}]*)\}/);
344
+ if (!m) fail("AGENT_TYPES not found in card-queen.js");
345
+ src = m[1];
346
+ }
347
+ const types = [...new Set([...src.matchAll(kind === "hand" ? /'(doug-flow:[\w-]+)'/g : /agentType: '([^']+)'/g)].map((x) => x[1]))];
348
+ if (types.length === 0) fail("no agent types found");
349
+ process.stdout.write(types.join("\n") + "\n");
350
+ break;
351
+ }
352
+ case "launched": {
353
+ if (!id) usage("launched needs a card id");
354
+ const run = need("run");
355
+ const kind = need("kind");
356
+ if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
357
+ const worktree = need("worktree");
358
+ const argsFile = need("args-file");
359
+ const s = load();
360
+ const c = cardOf(s, id);
361
+ const reportDir = join(dir, ".doug/.state/reports", id);
362
+ mkdirSync(reportDir, { recursive: true });
363
+ try {
364
+ copyFileSync(argsFile, join(reportDir, "queen-args.json"));
365
+ } catch (e) {
366
+ fail(`cannot read --args-file ${argsFile}: ${e.code || e.message}`);
367
+ }
368
+ c.state = "running";
369
+ c.runId = run;
370
+ c.kind = kind;
371
+ c.worktree = worktree;
372
+ clearStep(c);
373
+ save(s);
374
+ say(`${id} launched as ${run}`);
375
+ break;
376
+ }
377
+ case "report": {
378
+ const run = need("run");
379
+ const resultFile = need("file");
380
+ const s = load();
381
+ const c = s.cards.find((x) => x.runId === run);
382
+ if (!c) fail(`unknown run ${run}`);
383
+ let text;
384
+ try {
385
+ text = readFileSync(resultFile, "utf8");
386
+ } catch (e) {
387
+ fail(`cannot read --file ${resultFile}: ${e.code || e.message}`);
388
+ }
389
+ let parsed = null;
390
+ try {
391
+ parsed = JSON.parse(text);
392
+ } catch {
393
+ // unreadable: classify says resume
394
+ }
395
+ const kind = c.kind === "flow" ? "flow" : "hand";
396
+ if (parsed !== null) {
397
+ const dest = kind === "flow" ? join(dir, reportPathFor(c.id)) : join(dir, ".doug/.state/reports", c.id, "card-report.json");
398
+ try {
399
+ mkdirSync(dirname(dest), { recursive: true });
400
+ writeFileSync(dest, JSON.stringify(kind === "hand" && parsed.report ? parsed.report : parsed, null, 2) + "\n");
401
+ } catch (e) {
402
+ process.stderr.write(`autopilot: warning: cannot write ${dest}: ${e.code || e.message}\n`);
403
+ }
404
+ }
405
+ const v = classifyNotification(kind, { status: "completed", result: parsed === null ? text : parsed });
406
+ say(`${c.id} ${v.action}${v.reason ? ` ${v.reason}` : ""}`);
407
+ break;
408
+ }
409
+ case "resumed": {
410
+ if (!id) usage("resumed needs a card id");
411
+ const run = need("run");
412
+ const s = load();
413
+ const c = cardOf(s, id);
414
+ c.resumes = (c.resumes || 0) + 1;
415
+ c.runId = run;
416
+ save(s);
417
+ say(`${id} resumed as ${run}`);
418
+ break;
419
+ }
420
+ case "lock-wait": {
421
+ if (!id) usage("lock-wait needs a card id");
422
+ const ms = number("ms", opts.ms);
423
+ const s = load();
424
+ const c = cardOf(s, id);
425
+ c.lockWaitMs = (c.lockWaitMs || 0) + ms;
426
+ save(s);
427
+ say(`${id} lock wait ${c.lockWaitMs}ms`);
428
+ break;
429
+ }
430
+ case "end": {
431
+ const s = load();
432
+ s.status = "ended";
433
+ s.endedAt = new Date().toISOString();
434
+ save(s);
435
+ say("ended");
436
+ break;
437
+ }
438
+ default:
439
+ usage(`unknown subcommand${cmd ? ` ${cmd}` : ""}; use start, add, card, step, land, stop, filed, caps, pick, launched, report, resumed, lock-wait, or end`);
440
+ }