@dougharness/cli 1.1.1 → 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 (50) 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/spec-writer.md +23 -0
  11. package/node_modules/@dougharness/flow/agents/tester.md +21 -0
  12. package/node_modules/@dougharness/flow/hooks/hooks.json +4 -1
  13. package/node_modules/@dougharness/flow/hooks/register.tsx +75 -0
  14. package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +117 -0
  15. package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +17 -0
  16. package/node_modules/@dougharness/flow/lib/board.mjs +91 -10
  17. package/node_modules/@dougharness/flow/lib/card-land.mjs +115 -0
  18. package/node_modules/@dougharness/flow/lib/card-loop.mjs +746 -0
  19. package/node_modules/@dougharness/flow/lib/coordinator.mjs +368 -0
  20. package/node_modules/@dougharness/flow/lib/cost.mjs +15 -5
  21. package/node_modules/@dougharness/flow/lib/embeddings.mjs +144 -3
  22. package/node_modules/@dougharness/flow/lib/land.mjs +27 -8
  23. package/node_modules/@dougharness/flow/lib/memory.mjs +48 -12
  24. package/node_modules/@dougharness/flow/lib/plan.mjs +32 -4
  25. package/node_modules/@dougharness/flow/lib/worktrees.mjs +3 -3
  26. package/node_modules/@dougharness/flow/package.json +1 -1
  27. package/node_modules/@dougharness/flow/scripts/autopilot.mjs +407 -0
  28. package/node_modules/@dougharness/flow/scripts/board.mjs +107 -32
  29. package/node_modules/@dougharness/flow/scripts/cost.mjs +16 -3
  30. package/node_modules/@dougharness/flow/scripts/memory.mjs +20 -9
  31. package/node_modules/@dougharness/flow/scripts/plan.mjs +100 -30
  32. package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +8 -2
  33. package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +30 -10
  34. package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +80 -0
  35. package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
  36. package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +6 -6
  37. package/node_modules/@dougharness/flow/workflows/card-queen.js +807 -0
  38. package/node_modules/@dougharness/flow/workflows/doug-implement.js +7 -7
  39. package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
  40. package/node_modules/@dougharness/gates/lib/anchor.mjs +29 -4
  41. package/node_modules/@dougharness/gates/lib/bash-rules.mjs +196 -6
  42. package/node_modules/@dougharness/gates/lib/gate-lock.mjs +40 -8
  43. package/node_modules/@dougharness/gates/lib/proposals.mjs +41 -5
  44. package/node_modules/@dougharness/gates/lib/scope.mjs +43 -4
  45. package/node_modules/@dougharness/gates/package.json +1 -1
  46. package/node_modules/@dougharness/gates/scripts/reanchor.mjs +2 -1
  47. package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +138 -10
  48. package/node_modules/@dougharness/gates/scripts/verify.mjs +8 -4
  49. package/package.json +3 -3
  50. package/templates/board-app.js +46 -0
@@ -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
+ }