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