@dougharness/cli 1.2.0 → 1.4.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.
- package/dist/bin.js +13 -4
- package/dist/bin.js.map +1 -1
- package/dist/board-page.d.ts +5 -1
- package/dist/board-page.js +19 -5
- package/dist/board-page.js.map +1 -1
- package/dist/board-serve.js +34 -10
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +160 -13
- package/dist/board.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/implementer.md +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +1 -1
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -1
- package/node_modules/@dougharness/flow/agents/reviewer.md +3 -1
- package/node_modules/@dougharness/flow/hooks/register.tsx +67 -32
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +94 -7
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +31 -4
- package/node_modules/@dougharness/flow/lib/board.mjs +366 -21
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +229 -23
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +25 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +9 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/memory.mjs +382 -65
- package/node_modules/@dougharness/flow/lib/plan.mjs +1 -1
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +228 -14
- package/node_modules/@dougharness/flow/scripts/board.mjs +122 -13
- package/node_modules/@dougharness/flow/scripts/cost.mjs +6 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +27 -8
- package/node_modules/@dougharness/flow/scripts/plan.mjs +5 -1
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +11 -4
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +9 -2
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +2 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +2 -2
- package/node_modules/@dougharness/flow/workflows/card-queen.js +238 -25
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +77 -34
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/baseline.mjs +23 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +41 -3
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +6 -2
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +119 -15
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +107 -20
- package/package.json +3 -3
- package/templates/board-app.js +240 -30
|
@@ -9,23 +9,30 @@
|
|
|
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> writes .doug/.state/reports/<id>/queen-args.json, prints
|
|
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
|
+
// autopilot.mjs agent-types --kind <flow|hand> prints the agent types that queen spawns, one per line
|
|
14
15
|
// 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
|
|
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
|
|
17
21
|
// autopilot.mjs lock-wait <id> --ms <n>
|
|
22
|
+
// autopilot.mjs clear drops an ended or inactive run; a running run is kept
|
|
18
23
|
// autopilot.mjs end
|
|
19
24
|
// --dir <d> names the project (default cwd). Exit 0 done; 1 no run or unknown card; 2 bad usage.
|
|
20
25
|
import { execFileSync } from "node:child_process";
|
|
21
|
-
import { copyFileSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { homedir } from "node:os";
|
|
22
28
|
import { dirname, join, resolve } from "node:path";
|
|
23
29
|
import { fileURLToPath } from "node:url";
|
|
24
30
|
import { loadModels } from "../lib/models.mjs";
|
|
25
|
-
import { AUTOPILOT_FILE, STEPS, readAutopilot } from "../lib/autopilot-view.mjs";
|
|
31
|
+
import { AUTOPILOT_FILE, STEPS, clearable, readAutopilot } from "../lib/autopilot-view.mjs";
|
|
26
32
|
import { contextThreshold } from "../lib/context.mjs";
|
|
27
|
-
import { classifyNotification, pickCards, workersMax } from "../lib/coordinator.mjs";
|
|
33
|
+
import { classifyNotification, pickCards, queenLabel, renameQueenMeta, workersMax } from "../lib/coordinator.mjs";
|
|
28
34
|
import { nextReadyCards } from "../lib/board.mjs";
|
|
35
|
+
import { SEATS } from "../lib/card-loop.mjs";
|
|
29
36
|
import { planFor, reportPathFor } from "../lib/plan.mjs";
|
|
30
37
|
|
|
31
38
|
const argv = process.argv.slice(2);
|
|
@@ -81,10 +88,43 @@ function cardOf(s, cardId) {
|
|
|
81
88
|
if (!c) fail(`unknown card ${cardId}`);
|
|
82
89
|
return c;
|
|
83
90
|
}
|
|
91
|
+
const IN_FLIGHT = ["running", "done", "landing"];
|
|
92
|
+
const RERUN_ALONE = "Rerun alone: no other queen is in flight now.";
|
|
93
|
+
const othersInFlight = (s, id) => s.cards.filter((x) => x.id !== id && IN_FLIGHT.includes(x.state));
|
|
94
|
+
// The saved stop report and queen-args of a card, for the commands that answer an asking stop.
|
|
95
|
+
function savedStop(id, c) {
|
|
96
|
+
const reportDir = join(dir, ".doug/.state/reports", id);
|
|
97
|
+
let rep;
|
|
98
|
+
try {
|
|
99
|
+
rep = JSON.parse(readFileSync(join(reportDir, "card-report.json"), "utf8"));
|
|
100
|
+
} catch (e) {
|
|
101
|
+
fail(`cannot read the saved report for ${id}: ${e.code || e.message}`);
|
|
102
|
+
}
|
|
103
|
+
if (!rep || rep.outcome !== "stopped" || rep.reason === "agent-null" || !SEATS.includes(rep.stoppedAt)) fail(`${id} did not stop on an answerable ask stop`);
|
|
104
|
+
const argsPath = join(reportDir, "queen-args.json");
|
|
105
|
+
let qa;
|
|
106
|
+
try {
|
|
107
|
+
qa = JSON.parse(readFileSync(argsPath, "utf8"));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
fail(`cannot read ${argsPath}: ${e.code || e.message}`);
|
|
110
|
+
}
|
|
111
|
+
const stopped = (Array.isArray(rep.history) ? rep.history : []).filter((h) => h && h.result === "stopped").pop();
|
|
112
|
+
const entry = { seat: rep.stoppedAt, question: rep.detail, answer: "" };
|
|
113
|
+
if (stopped && Number.isInteger(stopped.round) && stopped.round > 0) entry.round = stopped.round;
|
|
114
|
+
entry.run = c.runId;
|
|
115
|
+
return { rep, argsPath, qa, entry };
|
|
116
|
+
}
|
|
84
117
|
const clearStep = (c) => {
|
|
85
118
|
delete c.step;
|
|
86
119
|
delete c.stepStartedAt;
|
|
120
|
+
delete c.seats;
|
|
87
121
|
};
|
|
122
|
+
// Where the queen run's journal lives: the main session's run dir under the user's Claude dir; "" without a session id.
|
|
123
|
+
function journalPath(run) {
|
|
124
|
+
const sid = process.env.CLAUDE_CODE_SESSION_ID;
|
|
125
|
+
if (!sid) return "";
|
|
126
|
+
return join(homedir(), ".claude", "projects", dir.replace(/[^A-Za-z0-9]/g, "-"), sid, "subagents", "workflows", run, "journal.jsonl");
|
|
127
|
+
}
|
|
88
128
|
|
|
89
129
|
function boardCard(cardId) {
|
|
90
130
|
try {
|
|
@@ -168,6 +208,7 @@ switch (cmd) {
|
|
|
168
208
|
const c = cardOf(s, id);
|
|
169
209
|
c.step = arg2;
|
|
170
210
|
c.stepStartedAt = new Date().toISOString();
|
|
211
|
+
c.seats = [...(Array.isArray(c.seats) ? c.seats : []), { step: arg2, startedAt: c.stepStartedAt }];
|
|
171
212
|
save(s);
|
|
172
213
|
say(`${id} ${arg2}`);
|
|
173
214
|
break;
|
|
@@ -255,9 +296,14 @@ switch (cmd) {
|
|
|
255
296
|
const b = boardCard(c.id);
|
|
256
297
|
return b ? shape(b) : { id: c.id, goal: "", track: c.kind === "flow" ? "flow" : "hand" };
|
|
257
298
|
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
299
|
+
if (opts.milestone === "") usage("--milestone needs an id");
|
|
300
|
+
let picked;
|
|
301
|
+
try {
|
|
302
|
+
picked = nextReadyCards(board, { track: "any", batch: Math.max(1, board.cards.length), milestone: opts.milestone });
|
|
303
|
+
} catch (e) {
|
|
304
|
+
fail(e instanceof Error ? e.message : String(e));
|
|
305
|
+
}
|
|
306
|
+
const ready = picked.cards.filter((c) => !flyingIds.has(c.id)).map(shape);
|
|
261
307
|
const plans = {};
|
|
262
308
|
for (const c of [...ready, ...inFlight]) {
|
|
263
309
|
if (c.track !== "flow") continue;
|
|
@@ -279,6 +325,10 @@ switch (cmd) {
|
|
|
279
325
|
if (!id) usage("queen-args needs a card id");
|
|
280
326
|
const kind = need("kind");
|
|
281
327
|
if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
|
|
328
|
+
if (opts.research !== undefined) {
|
|
329
|
+
if (kind === "flow") usage("--research is for a hand card; research for a flow card runs before its plan (doug-next section 1)");
|
|
330
|
+
if (opts.research !== "on" && opts.research !== "off") usage("--research must be on or off");
|
|
331
|
+
}
|
|
282
332
|
let args;
|
|
283
333
|
if (kind === "flow") {
|
|
284
334
|
try {
|
|
@@ -295,15 +345,21 @@ switch (cmd) {
|
|
|
295
345
|
} catch {
|
|
296
346
|
// absent or unreadable: no install command
|
|
297
347
|
}
|
|
348
|
+
let base = "main";
|
|
349
|
+
try {
|
|
350
|
+
base = execFileSync("git", ["-C", dir, "symbolic-ref", "--short", "-q", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || "main";
|
|
351
|
+
} catch {
|
|
352
|
+
// not a git repo or a detached HEAD: main
|
|
353
|
+
}
|
|
298
354
|
args = {
|
|
299
355
|
card: { id: b.id, title: b.title ?? null, size: b.size ?? null, class: b.class ?? null, goal: b.goal ?? "" },
|
|
300
356
|
options: {
|
|
301
357
|
testerAgent: true,
|
|
302
|
-
research:
|
|
358
|
+
research: opts.research === "on",
|
|
303
359
|
install: (config.commands && config.commands.install) || null,
|
|
304
360
|
gateCommand: "node .doug/hooks/scripts/verify.mjs",
|
|
305
361
|
branch: `card/${id}`,
|
|
306
|
-
base
|
|
362
|
+
base,
|
|
307
363
|
},
|
|
308
364
|
models: loadModels(dir),
|
|
309
365
|
repoRoot: dir,
|
|
@@ -313,7 +369,39 @@ switch (cmd) {
|
|
|
313
369
|
const out = join(dir, ".doug/.state/reports", id, "queen-args.json");
|
|
314
370
|
mkdirSync(dirname(out), { recursive: true });
|
|
315
371
|
writeFileSync(out, JSON.stringify(args) + "\n");
|
|
316
|
-
|
|
372
|
+
const flying = (readAutopilot(dir)?.cards ?? []).filter((c) => ["running", "done", "landing"].includes(c.state)).map((c) => c.id);
|
|
373
|
+
const batch = String(opts.with ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
374
|
+
const srcFile = join(dirname(fileURLToPath(import.meta.url)), "..", "workflows", kind === "hand" ? "card-queen.js" : "doug-implement.js");
|
|
375
|
+
let renamed;
|
|
376
|
+
try {
|
|
377
|
+
renamed = renameQueenMeta(readFileSync(srcFile, "utf8"), { kind, label: queenLabel(id, [...flying, ...batch]) });
|
|
378
|
+
} catch (e) {
|
|
379
|
+
fail(`cannot build the per-card queen from ${srcFile}: ${e.message}`);
|
|
380
|
+
}
|
|
381
|
+
const script = join(dirname(out), "queen.js");
|
|
382
|
+
rmSync(script, { force: true });
|
|
383
|
+
writeFileSync(script, renamed);
|
|
384
|
+
process.stdout.write(out + "\n" + script + "\n");
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case "agent-types": {
|
|
388
|
+
const kind = need("kind");
|
|
389
|
+
if (kind !== "flow" && kind !== "hand") usage("--kind must be flow or hand");
|
|
390
|
+
const wf = join(dirname(fileURLToPath(import.meta.url)), "..", "workflows", kind === "hand" ? "card-queen.js" : "doug-implement.js");
|
|
391
|
+
let src;
|
|
392
|
+
try {
|
|
393
|
+
src = readFileSync(wf, "utf8");
|
|
394
|
+
} catch (e) {
|
|
395
|
+
fail(`cannot read ${wf}: ${e.message}`);
|
|
396
|
+
}
|
|
397
|
+
if (kind === "hand") {
|
|
398
|
+
const m = src.match(/const AGENT_TYPES = \{([^}]*)\}/);
|
|
399
|
+
if (!m) fail("AGENT_TYPES not found in card-queen.js");
|
|
400
|
+
src = m[1];
|
|
401
|
+
}
|
|
402
|
+
const types = [...new Set([...src.matchAll(kind === "hand" ? /'(doug-flow:[\w-]+)'/g : /agentType: '([^']+)'/g)].map((x) => x[1]))];
|
|
403
|
+
if (types.length === 0) fail("no agent types found");
|
|
404
|
+
process.stdout.write(types.join("\n") + "\n");
|
|
317
405
|
break;
|
|
318
406
|
}
|
|
319
407
|
case "launched": {
|
|
@@ -337,6 +425,9 @@ switch (cmd) {
|
|
|
337
425
|
c.kind = kind;
|
|
338
426
|
c.worktree = worktree;
|
|
339
427
|
clearStep(c);
|
|
428
|
+
delete c.journals;
|
|
429
|
+
const journal = journalPath(run);
|
|
430
|
+
if (journal) c.journals = [journal];
|
|
340
431
|
save(s);
|
|
341
432
|
say(`${id} launched as ${run}`);
|
|
342
433
|
break;
|
|
@@ -360,6 +451,11 @@ switch (cmd) {
|
|
|
360
451
|
// unreadable: classify says resume
|
|
361
452
|
}
|
|
362
453
|
const kind = c.kind === "flow" ? "flow" : "hand";
|
|
454
|
+
// The queen's task-output file wraps the result: {summary, agentCount, logs, result}.
|
|
455
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.result !== null && typeof parsed.result === "object") {
|
|
456
|
+
const bare = kind === "flow" ? !("ok" in parsed) && !("paused" in parsed) : !("card" in parsed) && !("outcome" in parsed);
|
|
457
|
+
if (bare) parsed = parsed.result;
|
|
458
|
+
}
|
|
363
459
|
if (parsed !== null) {
|
|
364
460
|
const dest = kind === "flow" ? join(dir, reportPathFor(c.id)) : join(dir, ".doug/.state/reports", c.id, "card-report.json");
|
|
365
461
|
try {
|
|
@@ -368,11 +464,94 @@ switch (cmd) {
|
|
|
368
464
|
} catch (e) {
|
|
369
465
|
process.stderr.write(`autopilot: warning: cannot write ${dest}: ${e.code || e.message}\n`);
|
|
370
466
|
}
|
|
467
|
+
if (kind === "hand") {
|
|
468
|
+
const rep = parsed.report ?? parsed;
|
|
469
|
+
const note = rep && typeof rep.researchNote === "string" ? rep.researchNote : "";
|
|
470
|
+
if (note.trim() !== "") {
|
|
471
|
+
const noteDest = join(dir, ".doug/.state/research", `${c.id}.md`);
|
|
472
|
+
try {
|
|
473
|
+
const oneLine = note.trim();
|
|
474
|
+
let src = null;
|
|
475
|
+
if (!oneLine.includes("\n") && oneLine.startsWith("/") && resolve(oneLine) !== resolve(noteDest) && existsSync(oneLine)) src = oneLine;
|
|
476
|
+
if (resolve(oneLine) !== resolve(noteDest)) {
|
|
477
|
+
mkdirSync(dirname(noteDest), { recursive: true });
|
|
478
|
+
if (src) copyFileSync(src, noteDest);
|
|
479
|
+
else writeFileSync(noteDest, note);
|
|
480
|
+
}
|
|
481
|
+
} catch (e) {
|
|
482
|
+
process.stderr.write(`autopilot: warning: cannot write ${noteDest}: ${e.code || e.message}\n`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
371
486
|
}
|
|
372
487
|
const v = classifyNotification(kind, { status: "completed", result: parsed === null ? text : parsed });
|
|
488
|
+
if (v.action === "ask") {
|
|
489
|
+
let answered = false;
|
|
490
|
+
try {
|
|
491
|
+
const saved = JSON.parse(readFileSync(join(dir, ".doug/.state/reports", c.id, "queen-args.json"), "utf8"));
|
|
492
|
+
const rep = parsed?.report ?? parsed;
|
|
493
|
+
const stop = (Array.isArray(rep?.history) ? rep.history : []).filter((h) => h && h.result === "stopped").pop();
|
|
494
|
+
answered =
|
|
495
|
+
Array.isArray(saved?.options?.answers) &&
|
|
496
|
+
saved.options.answers.some(
|
|
497
|
+
(a) => a && a.run === run && a.seat === rep?.stoppedAt && stop && a.round === stop.round && a.question === rep?.detail,
|
|
498
|
+
);
|
|
499
|
+
} catch {
|
|
500
|
+
// no readable args: not answered
|
|
501
|
+
}
|
|
502
|
+
if (answered) {
|
|
503
|
+
say(`${c.id} resume with the answer`);
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (v.action === "ask" && v.reason === "gate-timeout" && !c.resumes && othersInFlight(s, c.id).length > 0) {
|
|
508
|
+
c.state = "stopped";
|
|
509
|
+
c.reason = "gate-timeout: rerun alone pending";
|
|
510
|
+
save(s);
|
|
511
|
+
say(`${c.id} rerun alone gate-timeout`);
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
373
514
|
say(`${c.id} ${v.action}${v.reason ? ` ${v.reason}` : ""}`);
|
|
374
515
|
break;
|
|
375
516
|
}
|
|
517
|
+
case "answer": {
|
|
518
|
+
if (!id) usage("answer needs a card id");
|
|
519
|
+
const answer = need("answer");
|
|
520
|
+
if (answer.trim() === "") usage("--answer needs a non-blank answer");
|
|
521
|
+
const s = load();
|
|
522
|
+
const c = cardOf(s, id);
|
|
523
|
+
const { rep, argsPath, qa, entry } = savedStop(id, c);
|
|
524
|
+
entry.answer = answer;
|
|
525
|
+
qa.options = { ...(qa.options || {}), answers: [...(Array.isArray(qa.options?.answers) ? qa.options.answers : []), entry] };
|
|
526
|
+
writeFileSync(argsPath, JSON.stringify(qa, null, 2) + "\n");
|
|
527
|
+
say(`${id} answer recorded for ${entry.seat}`);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
case "rerun-alone": {
|
|
531
|
+
if (!id) usage("rerun-alone needs a card id");
|
|
532
|
+
const s = load();
|
|
533
|
+
const c = cardOf(s, id);
|
|
534
|
+
const others = othersInFlight(s, id);
|
|
535
|
+
if (others.length > 0) fail(`${others.map((x) => x.id).join(", ")} still in flight; rerun ${id} alone after it lands`);
|
|
536
|
+
if (c.resumes >= 1) fail(`${id} was already resumed once`);
|
|
537
|
+
const { rep, argsPath, qa, entry } = savedStop(id, c);
|
|
538
|
+
entry.answer = RERUN_ALONE;
|
|
539
|
+
qa.options = { ...(qa.options || {}), answers: [...(Array.isArray(qa.options?.answers) ? qa.options.answers : []), entry] };
|
|
540
|
+
writeFileSync(argsPath, JSON.stringify(qa, null, 2) + "\n");
|
|
541
|
+
say(`${id} rerun alone recorded for ${rep.stoppedAt}`);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
case "leave": {
|
|
545
|
+
if (!id) usage("leave needs a card id");
|
|
546
|
+
const reason = need("reason");
|
|
547
|
+
if (reason.trim() === "") usage("--reason needs a non-blank reason");
|
|
548
|
+
const s = load();
|
|
549
|
+
const c = cardOf(s, id);
|
|
550
|
+
c.left = reason;
|
|
551
|
+
save(s);
|
|
552
|
+
say(`${id} left In flow: ${reason}`);
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
376
555
|
case "resumed": {
|
|
377
556
|
if (!id) usage("resumed needs a card id");
|
|
378
557
|
const run = need("run");
|
|
@@ -380,6 +559,9 @@ switch (cmd) {
|
|
|
380
559
|
const c = cardOf(s, id);
|
|
381
560
|
c.resumes = (c.resumes || 0) + 1;
|
|
382
561
|
c.runId = run;
|
|
562
|
+
c.state = "running";
|
|
563
|
+
const journal = journalPath(run);
|
|
564
|
+
if (journal) c.journals = [...(Array.isArray(c.journals) ? c.journals : []), journal];
|
|
383
565
|
save(s);
|
|
384
566
|
say(`${id} resumed as ${run}`);
|
|
385
567
|
break;
|
|
@@ -394,8 +576,40 @@ switch (cmd) {
|
|
|
394
576
|
say(`${id} lock wait ${c.lockWaitMs}ms`);
|
|
395
577
|
break;
|
|
396
578
|
}
|
|
579
|
+
case "clear": {
|
|
580
|
+
if (!existsSync(file)) say("nothing to clear");
|
|
581
|
+
else {
|
|
582
|
+
const s = readAutopilot(dir);
|
|
583
|
+
if (!s) say("kept the unreadable file");
|
|
584
|
+
else if (clearable(s, new Date())) {
|
|
585
|
+
rmSync(file);
|
|
586
|
+
say("cleared");
|
|
587
|
+
} else say("kept the running run");
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
397
591
|
case "end": {
|
|
398
592
|
const s = load();
|
|
593
|
+
if (s.cards.length > 0) {
|
|
594
|
+
let board;
|
|
595
|
+
try {
|
|
596
|
+
board = JSON.parse(readFileSync(join(dir, ".doug/board.json"), "utf8"));
|
|
597
|
+
} catch (e) {
|
|
598
|
+
fail(`cannot read the board to check cards In flow: ${e.code || e.message}`);
|
|
599
|
+
}
|
|
600
|
+
const stuck = s.cards.filter((c) => {
|
|
601
|
+
if ((board.cards || []).find((b) => b.id === c.id)?.column !== "flow") return false;
|
|
602
|
+
if (c.resumes >= 1 || (typeof c.left === "string" && c.left.trim() !== "")) return false;
|
|
603
|
+
let answers = [];
|
|
604
|
+
try {
|
|
605
|
+
answers = JSON.parse(readFileSync(join(dir, ".doug/.state/reports", c.id, "queen-args.json"), "utf8")).options.answers;
|
|
606
|
+
} catch {
|
|
607
|
+
// no readable args: not answered
|
|
608
|
+
}
|
|
609
|
+
return !(Array.isArray(answers) && answers.some((a) => a && a.run === c.runId && a.answer !== RERUN_ALONE));
|
|
610
|
+
});
|
|
611
|
+
if (stuck.length > 0) fail(`cannot end: ${stuck.map((c) => c.id).join(", ")} still In flow, neither resumed nor answered`);
|
|
612
|
+
}
|
|
399
613
|
s.status = "ended";
|
|
400
614
|
s.endedAt = new Date().toISOString();
|
|
401
615
|
save(s);
|
|
@@ -403,5 +617,5 @@ switch (cmd) {
|
|
|
403
617
|
break;
|
|
404
618
|
}
|
|
405
619
|
default:
|
|
406
|
-
usage(`unknown subcommand${cmd ? ` ${cmd}` : ""}; use start, add, card, step, land, stop, filed, caps, pick, launched, report, resumed, lock-wait, or end`);
|
|
620
|
+
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`);
|
|
407
621
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// CLI for the development board, used by the /doug-next and doug-hand skills. Deterministic; no agents.
|
|
3
|
-
// board.mjs next [dir] [--track flow|hand|any] [--tag <tag>]
|
|
3
|
+
// board.mjs next [dir] [--track flow|hand|any] [--tag <tag>] [--epic <id>] [--milestone <id>]
|
|
4
4
|
// the first Ready card on that track (default flow) whose deps are
|
|
5
5
|
// Done, as JSON (exit 1 if none); --tag filters to cards carrying it
|
|
6
|
-
// board.mjs next --batch <n> [dir] [--track flow|hand|any] [--tag <tag>]
|
|
6
|
+
// board.mjs next --batch <n> [dir] [--track flow|hand|any] [--tag <tag>] [--epic <id>] [--milestone <id>]
|
|
7
7
|
// the first n runnable Ready cards, in board order, as a JSON array
|
|
8
8
|
// (exit 1 if none): the cards /doug-next plans together; stderr also
|
|
9
9
|
// carries a `wall cap: <m> min` line (the cards' budgets from the size x class table)
|
|
@@ -14,16 +14,25 @@
|
|
|
14
14
|
// Wall clock rows joined to the board, one line per cell and a final
|
|
15
15
|
// `table:` line; writes nothing (replace WALL_BUDGET_MIN by hand). --usd does the same for the usd
|
|
16
16
|
// table (USD_BUDGET) from the Cost rows of flow and hand entries
|
|
17
|
+
// board.mjs rollup [dir] [--epic <id> | --milestone <id>] [--from <live-runs file>]
|
|
18
|
+
// per epic and milestone: columns, done/total, size-weighted progress,
|
|
19
|
+
// measured cost and wall of Done cards, not-Done budget, as JSON; --from
|
|
20
|
+
// defaults to docs/live-runs.md when present (else measured is null)
|
|
21
|
+
// board.mjs labels --check [dir] one line per label colour under 3:1 against #ffffff or #1c2022; exit 0
|
|
17
22
|
// board.mjs card <id> [dir] one card as JSON
|
|
18
23
|
// board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom
|
|
19
24
|
// reorder a card within its column
|
|
20
25
|
// board.mjs move <id> <column> [dir] [--source <text>]
|
|
21
26
|
// move a card
|
|
22
27
|
// board.mjs edit <id> [dir] [--title <t>] [--goal <g> | --goal-file <path>] [--size S|M|L] [--component <c>]
|
|
23
|
-
// [--track flow|hand] [--class <c>] [--deps a,b] [--tag a,b] [--source <s>] [--force]
|
|
28
|
+
// [--track flow|hand] [--class <c>] [--deps a,b] [--tag a,b] [--source <s>] [--epic <id>] [--milestone <id>] [--force]
|
|
24
29
|
// changes a card's fields through the record's own validation
|
|
25
30
|
// (never the id or the column; move stays the way to change that);
|
|
26
|
-
// --tag "" clears a card's tags
|
|
31
|
+
// --tag "" clears a card's tags; --epic "" / --milestone "" clear those
|
|
32
|
+
// board.mjs epic add <id> --title <t> --goal <g> [dir] [--version <v>] [--date YYYY-MM-DD]
|
|
33
|
+
// board.mjs epic edit <id> [dir] [--title <t>] [--goal <g>] [--version <v>] [--date <d>] [--status planned|active|done] [--force]
|
|
34
|
+
// board.mjs epic list [dir] epics as a JSON array; milestone add|edit|list takes the same flags.
|
|
35
|
+
// --status done is refused while a member card is not Done (--force sets it, with a warning)
|
|
27
36
|
// board.mjs remove <id> [dir] [--force] removes a card from the record: refused always when another card
|
|
28
37
|
// lists it in deps; refused without --force when it is done, in
|
|
29
38
|
// flow, or named by .doug/plan.json (each with --force removes it
|
|
@@ -49,29 +58,42 @@
|
|
|
49
58
|
|
|
50
59
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
51
60
|
import { join, relative, resolve } from "node:path";
|
|
52
|
-
import { loadBoard, saveBoard, findCard, nextReadyCard, nextReadyCards, moveCard, editCard, removeCard, reorderCard, readyCapWarning, wallCapMinutes, WALL_BUDGET_MIN, WALL_BUDGET_MIN_N, parseLiveRunWalls, wallBudgetTable, USD_BUDGET, USD_BUDGET_MIN_N, usdCapDollars, parseLiveRunCosts, usdBudgetTable, runEntry, handEntry, handWallVerdict, runSummary, boardPath, parseAdversaryClasses, filterReportForCard, recordLanding } from "../lib/board.mjs";
|
|
61
|
+
import { loadBoard, saveBoard, findCard, nextReadyCard, nextReadyCards, moveCard, editCard, editEpic, editMilestone, initLabelGroups, addEpic, addMilestone, removeCard, reorderCard, readyCapWarning, wallCapMinutes, WALL_BUDGET_MIN, WALL_BUDGET_MIN_N, parseLiveRunWalls, wallBudgetTable, USD_BUDGET, USD_BUDGET_MIN_N, usdCapDollars, parseLiveRunCosts, usdBudgetTable, rollup, checkFilters, labelContrastFailures, LIVE_RUNS_RELPATH, runEntry, handEntry, handWallVerdict, runSummary, boardPath, parseAdversaryClasses, filterReportForCard, recordLanding } from "../lib/board.mjs";
|
|
53
62
|
import { loadPlan, planFor, unwrapReport, DEFAULT_BUDGET } from "../lib/plan.mjs";
|
|
54
63
|
import { wallForRun } from "../lib/cost.mjs";
|
|
55
64
|
|
|
56
|
-
const EDIT_FIELD_ORDER = ["title", "goal", "size", "component", "track", "class", "tags", "deps", "source"];
|
|
57
|
-
const EDIT_USAGE = "usage: board.mjs edit <id> [dir] [--title <t>] [--goal <g> | --goal-file <path>] [--size S|M|L] [--component <c>] [--track flow|hand] [--class <c>] [--tag a,b] [--deps a,b] [--source <s>] [--force]";
|
|
65
|
+
const EDIT_FIELD_ORDER = ["title", "goal", "size", "component", "track", "class", "tags", "deps", "source", "epic", "milestone"];
|
|
66
|
+
const EDIT_USAGE = "usage: board.mjs edit <id> [dir] [--title <t>] [--goal <g> | --goal-file <path>] [--size S|M|L] [--component <c>] [--track flow|hand] [--class <c>] [--tag a,b] [--deps a,b] [--source <s>] [--epic <id>] [--milestone <id>] [--force]";
|
|
58
67
|
|
|
59
68
|
const argv = process.argv.slice(2);
|
|
60
|
-
const FLAGS = new Set(["hand", "top", "bottom", "force", "usd"]);
|
|
69
|
+
const FLAGS = new Set(["hand", "top", "bottom", "force", "usd", "check"]);
|
|
61
70
|
const REPEATABLE = new Set(["adversary"]);
|
|
62
71
|
|
|
63
72
|
// Each subcommand's allowed flags, derived from what that case actually reads (card board-cli-unknown-flags).
|
|
64
73
|
// `record` has two forms (--hand or not) with different allowed sets, handled separately below.
|
|
65
74
|
const SUBCOMMAND_FLAGS = {
|
|
66
|
-
next: ["track", "batch", "tag"],
|
|
75
|
+
next: ["track", "batch", "tag", "epic", "milestone"],
|
|
67
76
|
card: [],
|
|
68
77
|
move: ["source"],
|
|
69
|
-
edit: ["title", "goal", "goal-file", "size", "component", "track", "class", "deps", "tag", "source", "force"],
|
|
78
|
+
edit: ["title", "goal", "goal-file", "size", "component", "track", "class", "deps", "tag", "source", "epic", "milestone", "force"],
|
|
70
79
|
remove: ["force"],
|
|
71
80
|
reorder: ["before", "after", "top", "bottom"],
|
|
72
81
|
budgets: ["from", "usd"],
|
|
82
|
+
rollup: ["epic", "milestone", "from"],
|
|
83
|
+
labels: ["check"],
|
|
73
84
|
summary: ["summary", "cost", "codex-cost", "wall", "commit", "card", "run", "claude-dir", "note"],
|
|
74
85
|
};
|
|
86
|
+
// epic and milestone: `<kind> add|edit|list`, each action with its own allowed flags.
|
|
87
|
+
const PLAN_ACTION_FLAGS = {
|
|
88
|
+
add: ["title", "goal", "version", "date"],
|
|
89
|
+
edit: ["title", "goal", "version", "date", "status", "force"],
|
|
90
|
+
list: [],
|
|
91
|
+
};
|
|
92
|
+
const planUsage = (kind) => [
|
|
93
|
+
`usage: board.mjs ${kind} add <id> --title <t> --goal <g> [dir] [--version <v>] [--date YYYY-MM-DD]`,
|
|
94
|
+
` board.mjs ${kind} edit <id> [dir] [--title <t>] [--goal <g>] [--version <v>] [--date YYYY-MM-DD] [--status planned|active|done] [--force]`,
|
|
95
|
+
` board.mjs ${kind} list [dir]`,
|
|
96
|
+
].join("\n");
|
|
75
97
|
const RECORD_HAND_FLAGS = ["summary", "hand", "commit", "wall", "gate", "note", "rehearsal", "rehearsal-outcome", "cost"];
|
|
76
98
|
const RECORD_REPORT_FLAGS = ["summary", "cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal", "rehearsal-outcome", "run", "claude-dir"];
|
|
77
99
|
const opts = {};
|
|
@@ -98,13 +120,15 @@ function fail(msg, code = 1) {
|
|
|
98
120
|
}
|
|
99
121
|
|
|
100
122
|
const USAGE_FOR = {
|
|
101
|
-
next: "usage: board.mjs next [dir] [--track flow|hand|any] [--batch <n>] [--tag <tag>]",
|
|
123
|
+
next: "usage: board.mjs next [dir] [--track flow|hand|any] [--batch <n>] [--tag <tag>] [--epic <id>] [--milestone <id>]",
|
|
102
124
|
card: "usage: board.mjs card <id> [dir]",
|
|
103
125
|
move: "usage: board.mjs move <id> <column> [dir] [--source <text>]",
|
|
104
126
|
edit: EDIT_USAGE,
|
|
105
127
|
remove: "usage: board.mjs remove <id> [dir] [--force]",
|
|
106
128
|
reorder: "usage: board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom",
|
|
107
129
|
budgets: "usage: board.mjs budgets [dir] [--usd] [--from <live-runs file>]",
|
|
130
|
+
rollup: "usage: board.mjs rollup [dir] [--epic <id> | --milestone <id>] [--from <live-runs file>]",
|
|
131
|
+
labels: "usage: board.mjs labels --check [dir] | labels init [dir]",
|
|
108
132
|
summary: "usage: board.mjs summary <report.json> --summary \"<one sentence>\" [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>] [--note <text>]",
|
|
109
133
|
};
|
|
110
134
|
const RECORD_HAND_USAGE = "usage: board.mjs record <id> --hand [dir] --summary \"<one sentence>\" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed>] [--cost <usd>]";
|
|
@@ -124,6 +148,12 @@ function dirFrom(args, n) {
|
|
|
124
148
|
return resolve(process.env.CLAUDE_PROJECT_DIR || args[n] || process.cwd());
|
|
125
149
|
}
|
|
126
150
|
|
|
151
|
+
if (cmd === "epic" || cmd === "milestone") {
|
|
152
|
+
const action = rest[0];
|
|
153
|
+
if (!Object.prototype.hasOwnProperty.call(PLAN_ACTION_FLAGS, action)) fail(planUsage(cmd), 2);
|
|
154
|
+
const unknown = Object.keys(opts).find((name) => !PLAN_ACTION_FLAGS[action].includes(name));
|
|
155
|
+
if (unknown !== undefined) fail(`unknown option --${unknown} for board.mjs ${cmd}\n${planUsage(cmd)}`, 2);
|
|
156
|
+
}
|
|
127
157
|
if (cmd === "record" || Object.prototype.hasOwnProperty.call(SUBCOMMAND_FLAGS, cmd)) {
|
|
128
158
|
const unknown = unknownFlagFor(cmd, opts);
|
|
129
159
|
if (unknown !== undefined) {
|
|
@@ -198,7 +228,7 @@ try {
|
|
|
198
228
|
if (batch !== null && !(Number.isInteger(batch) && batch > 0)) fail(`--batch must be a positive integer, got ${JSON.stringify(opts.batch)}`, 2);
|
|
199
229
|
if (opts.tag !== undefined && opts.tag.includes(",")) fail(`--tag takes one tag here; got "${opts.tag}"`, 2);
|
|
200
230
|
const board = loadBoard(dir);
|
|
201
|
-
const { card, cards, skipped } = batch === null ? nextReadyCard(board, { track, tag: opts.tag }) : nextReadyCards(board, { track, batch, tag: opts.tag });
|
|
231
|
+
const { card, cards, skipped } = batch === null ? nextReadyCard(board, { track, tag: opts.tag, epic: opts.epic, milestone: opts.milestone }) : nextReadyCards(board, { track, batch, tag: opts.tag, epic: opts.epic, milestone: opts.milestone });
|
|
202
232
|
for (const s of skipped) process.stderr.write(s.hand ? `skipping ${s.id}: hand track (a by-hand card; /doug-next ${s.id} takes it)\n` : s.flow ? `skipping ${s.id}: flow track (run it with /doug-next)\n` : `skipping ${s.id}: waiting on ${s.waitingOn.join(", ")}\n`);
|
|
203
233
|
const capLine = readyCapWarning(board);
|
|
204
234
|
if (capLine) process.stderr.write(capLine);
|
|
@@ -236,6 +266,42 @@ try {
|
|
|
236
266
|
process.stdout.write(out.join("\n") + "\n");
|
|
237
267
|
break;
|
|
238
268
|
}
|
|
269
|
+
case "rollup": {
|
|
270
|
+
if (opts.epic !== undefined && opts.milestone !== undefined) fail(USAGE_FOR.rollup, 2);
|
|
271
|
+
const dir = dirFrom(rest, 0);
|
|
272
|
+
const board = loadBoard(dir);
|
|
273
|
+
try {
|
|
274
|
+
checkFilters(board, { epic: opts.epic, milestone: opts.milestone });
|
|
275
|
+
} catch (e) {
|
|
276
|
+
fail(e.message, 2);
|
|
277
|
+
}
|
|
278
|
+
let text = null;
|
|
279
|
+
if (opts.from !== undefined) {
|
|
280
|
+
try {
|
|
281
|
+
text = readFileSync(resolve(opts.from), "utf8");
|
|
282
|
+
} catch (e) {
|
|
283
|
+
fail(`cannot read --from file ${opts.from}: ${e.message}`, 2);
|
|
284
|
+
}
|
|
285
|
+
} else if (existsSync(join(dir, LIVE_RUNS_RELPATH))) {
|
|
286
|
+
text = readFileSync(join(dir, LIVE_RUNS_RELPATH), "utf8");
|
|
287
|
+
}
|
|
288
|
+
const r = rollup(board, text);
|
|
289
|
+
const out = opts.epic !== undefined ? r.epics.find((e) => e.id === opts.epic) : opts.milestone !== undefined ? r.milestones.find((m) => m.id === opts.milestone) : r;
|
|
290
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case "labels": {
|
|
294
|
+
if (rest[0] === "init" && opts.check !== true) {
|
|
295
|
+
const dir = dirFrom(rest, 1);
|
|
296
|
+
saveBoard(dir, initLabelGroups(loadBoard(dir)));
|
|
297
|
+
process.stdout.write(`Added label groups type and priority to ${relative(dir, boardPath(dir))}.\n`);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
if (opts.check !== true) fail(USAGE_FOR.labels, 2);
|
|
301
|
+
const lines = labelContrastFailures(loadBoard(dirFrom(rest, 0)));
|
|
302
|
+
if (lines.length) process.stdout.write(lines.join("\n") + "\n");
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
239
305
|
case "card": {
|
|
240
306
|
const dir = dirFrom(rest, 1);
|
|
241
307
|
process.stdout.write(JSON.stringify(findCard(loadBoard(dir), rest[0]), null, 2) + "\n");
|
|
@@ -272,6 +338,8 @@ try {
|
|
|
272
338
|
if (opts.deps !== undefined) fields.deps = opts.deps.split(",").map((d) => d.trim()).filter(Boolean);
|
|
273
339
|
if (opts.tag !== undefined) fields.tags = opts.tag.split(",").map((t) => t.trim()).filter(Boolean);
|
|
274
340
|
if (opts.source !== undefined) fields.source = opts.source;
|
|
341
|
+
if (opts.epic !== undefined) fields.epic = opts.epic === "" ? null : opts.epic;
|
|
342
|
+
if (opts.milestone !== undefined) fields.milestone = opts.milestone === "" ? null : opts.milestone;
|
|
275
343
|
const changed = EDIT_FIELD_ORDER.filter((k) => Object.prototype.hasOwnProperty.call(fields, k));
|
|
276
344
|
if (!changed.length) fail(EDIT_USAGE, 2);
|
|
277
345
|
const dir = dirFrom(rest, 1);
|
|
@@ -282,6 +350,47 @@ try {
|
|
|
282
350
|
process.stdout.write(`Edited ${id} (${changed.join(", ")}) in ${relative(dir, boardPath(dir))}.\n`);
|
|
283
351
|
break;
|
|
284
352
|
}
|
|
353
|
+
case "epic":
|
|
354
|
+
case "milestone": {
|
|
355
|
+
const [action, id] = rest;
|
|
356
|
+
const add = cmd === "epic" ? addEpic : addMilestone;
|
|
357
|
+
const edit = cmd === "epic" ? editEpic : editMilestone;
|
|
358
|
+
const targetOf = () => {
|
|
359
|
+
const t = {};
|
|
360
|
+
if (opts.version !== undefined) t.version = opts.version === "" ? null : opts.version;
|
|
361
|
+
if (opts.date !== undefined) t.date = opts.date === "" ? null : opts.date;
|
|
362
|
+
return t;
|
|
363
|
+
};
|
|
364
|
+
if (action === "list") {
|
|
365
|
+
const board = loadBoard(dirFrom(rest, 1));
|
|
366
|
+
process.stdout.write(JSON.stringify(board[`${cmd}s`] || [], null, 2) + "\n");
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
if (!id) fail(planUsage(cmd), 2);
|
|
370
|
+
if (action === "add") {
|
|
371
|
+
if (!opts.title || !opts.goal) fail(planUsage(cmd), 2);
|
|
372
|
+
const dir = dirFrom(rest, 2);
|
|
373
|
+
const target = {};
|
|
374
|
+
if (opts.version) target.version = opts.version;
|
|
375
|
+
if (opts.date) target.date = opts.date;
|
|
376
|
+
saveBoard(dir, add(loadBoard(dir), { id, title: opts.title, goal: opts.goal, ...(Object.keys(target).length ? { target } : {}) }));
|
|
377
|
+
process.stdout.write(`Added ${cmd} ${id} in ${relative(dir, boardPath(dir))}.\n`);
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
const fields = {};
|
|
381
|
+
if (opts.title !== undefined) fields.title = opts.title;
|
|
382
|
+
if (opts.goal !== undefined) fields.goal = opts.goal;
|
|
383
|
+
if (opts.status !== undefined) fields.status = opts.status;
|
|
384
|
+
const target = targetOf();
|
|
385
|
+
if (Object.keys(target).length) fields.target = target;
|
|
386
|
+
if (!Object.keys(fields).length) fail(planUsage(cmd), 2);
|
|
387
|
+
const dir = dirFrom(rest, 2);
|
|
388
|
+
const { board, warnings } = edit(loadBoard(dir), id, fields, { force: !!opts.force });
|
|
389
|
+
saveBoard(dir, board);
|
|
390
|
+
for (const w of warnings) process.stderr.write(`warning: ${w}\n`);
|
|
391
|
+
process.stdout.write(`Edited ${cmd} ${id} (${Object.keys(fields).join(", ")}) in ${relative(dir, boardPath(dir))}.\n`);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
285
394
|
case "remove": {
|
|
286
395
|
const [id] = rest;
|
|
287
396
|
if (!id) fail("usage: board.mjs remove <id> [dir] [--force]", 2);
|
|
@@ -389,7 +498,7 @@ try {
|
|
|
389
498
|
break;
|
|
390
499
|
}
|
|
391
500
|
default:
|
|
392
|
-
fail("usage: board.mjs <next|card|reorder|move|edit|remove|record|summary> ...", 2);
|
|
501
|
+
fail("usage: board.mjs <next|card|reorder|move|edit|epic|milestone|rollup|labels|remove|record|summary> ...", 2);
|
|
393
502
|
}
|
|
394
503
|
} catch (err) {
|
|
395
504
|
fail(err.message);
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
// --claude-dir <path> where ~/.claude lives (tests point it at a fixture)
|
|
15
15
|
// --report <path> a saved workflow report (card adversary-usage-in-report): prices its adversary relays'
|
|
16
16
|
// Codex usage as a separate line/`codex` key, never mixed into the Claude total; default
|
|
17
|
-
// <dir>/REPORT_RELPATH
|
|
18
|
-
//
|
|
17
|
+
// <dir>/REPORT_RELPATH when no --report is given and that file is readable. A report is
|
|
18
|
+
// attached only when its top-level runId equals the run asked for; a report with no runId
|
|
19
|
+
// is attached only when named with --report (card cost-run-codex-line-mismatch)
|
|
19
20
|
// Exit 1 when the run's journal cannot be found, or an explicit --report cannot be read/parsed; the text output ends
|
|
20
21
|
// with the run total and any unpriced agent. An unreadable/invalid default report is ignored, not a usage error.
|
|
21
22
|
import { readFileSync } from "node:fs";
|
|
22
23
|
import { resolve, join } from "node:path";
|
|
23
|
-
import { costRun, costSession, renderSession, renderCost, costByCard, renderCostByCard } from "../lib/cost.mjs";
|
|
24
|
+
import { reportForRun, costRun, costSession, renderSession, renderCost, costByCard, renderCostByCard } from "../lib/cost.mjs";
|
|
24
25
|
import { planFor, REPORT_RELPATH, unwrapReport } from "../lib/plan.mjs";
|
|
25
26
|
|
|
26
27
|
const argv = process.argv.slice(2);
|
|
@@ -81,6 +82,8 @@ try {
|
|
|
81
82
|
reportPath = null;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
report = reportForRun(report, runId, { named: Boolean(opts.report) });
|
|
86
|
+
if (!report) reportPath = null;
|
|
84
87
|
const result = costRun({ runId, projectDir, claudeDir: opts["claude-dir"] ? resolve(opts["claude-dir"]) : undefined, report, reportPath });
|
|
85
88
|
const byCard = opts["by-card"] ? costByCard(result, planFor(projectDir, opts.card)) : null;
|
|
86
89
|
if (opts.json) process.stdout.write(JSON.stringify(byCard ? { ...result, byCard } : result, null, 2) + "\n");
|