@dougharness/cli 1.4.0 → 1.6.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/generate/agents.js +4 -0
- package/dist/generate/agents.js.map +1 -1
- package/dist/generate/claude-md.js +1 -1
- package/dist/generate/claude-md.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/coder.md +2 -0
- package/node_modules/@dougharness/flow/agents/implementer.md +2 -0
- package/node_modules/@dougharness/flow/agents/planner.md +2 -1
- package/node_modules/@dougharness/flow/agents/reviewer.md +2 -0
- package/node_modules/@dougharness/flow/agents/tester.md +2 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +9 -9
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +54 -5
- package/node_modules/@dougharness/flow/lib/code-index.mjs +2 -2
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +4 -2
- package/node_modules/@dougharness/flow/lib/decisions.mjs +3 -5
- package/node_modules/@dougharness/flow/lib/learn.mjs +105 -57
- package/node_modules/@dougharness/flow/lib/memory.mjs +1418 -57
- 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 +178 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +43 -15
- package/node_modules/@dougharness/flow/scripts/learn.mjs +4 -2
- package/node_modules/@dougharness/flow/scripts/memory.mjs +870 -39
- package/node_modules/@dougharness/flow/scripts/plan.mjs +3 -3
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +8 -2
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +4 -0
- 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/run-report/SKILL.md +6 -3
- package/node_modules/@dougharness/flow/workflows/card-queen.js +54 -5
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +34 -19
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/baseline.mjs +9 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +6 -3
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +175 -4
- package/node_modules/@dougharness/gates/lib/test-totals.mjs +1 -1
- 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 +110 -125
- package/node_modules/@dougharness/gates/scripts/verify.mjs +47 -13
- package/package.json +3 -3
|
@@ -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
|
|
|
@@ -23,10 +23,11 @@
|
|
|
23
23
|
// autopilot.mjs end
|
|
24
24
|
// --dir <d> names the project (default cwd). Exit 0 done; 1 no run or unknown card; 2 bad usage.
|
|
25
25
|
import { execFileSync } from "node:child_process";
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
26
27
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
27
28
|
import { homedir } from "node:os";
|
|
28
29
|
import { dirname, join, resolve } from "node:path";
|
|
29
|
-
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
30
31
|
import { loadModels } from "../lib/models.mjs";
|
|
31
32
|
import { AUTOPILOT_FILE, STEPS, clearable, readAutopilot } from "../lib/autopilot-view.mjs";
|
|
32
33
|
import { contextThreshold } from "../lib/context.mjs";
|
|
@@ -34,6 +35,21 @@ import { classifyNotification, pickCards, queenLabel, renameQueenMeta, workersMa
|
|
|
34
35
|
import { nextReadyCards } from "../lib/board.mjs";
|
|
35
36
|
import { SEATS } from "../lib/card-loop.mjs";
|
|
36
37
|
import { planFor, reportPathFor } from "../lib/plan.mjs";
|
|
38
|
+
import { readMemoryConfig, readMemoryStaleDays } from "../lib/embeddings.mjs";
|
|
39
|
+
|
|
40
|
+
// Card memory-failures-wiring (user decision, 2026-10-28): a stopped card starts `memory.mjs reflect --failures
|
|
41
|
+
// <card> --max-usd 0.5 --detach` (job failures-<card>, cheap row), after its run history is written. The queue
|
|
42
|
+
// approval is the approval. A start that fails (a live job holds the name) warns and never fails the report.
|
|
43
|
+
function startFailuresReflect(dir, id) {
|
|
44
|
+
try {
|
|
45
|
+
execFileSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), "memory.mjs"), "reflect", "--failures", id, dir, "--max-usd", "0.5", "--detach"], {
|
|
46
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
47
|
+
env: process.env,
|
|
48
|
+
});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
process.stderr.write(`autopilot: warning: cannot start reflect --failures for ${id}: ${String(e.stderr || e.message || e).trim().split("\n")[0]}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
37
53
|
|
|
38
54
|
const argv = process.argv.slice(2);
|
|
39
55
|
const opts = {};
|
|
@@ -484,6 +500,167 @@ switch (cmd) {
|
|
|
484
500
|
}
|
|
485
501
|
}
|
|
486
502
|
}
|
|
503
|
+
if (kind === "hand" && parsed !== null) {
|
|
504
|
+
const rep = parsed.report ?? parsed;
|
|
505
|
+
if (rep && typeof rep === "object" && rep.outcome === "stopped") {
|
|
506
|
+
try {
|
|
507
|
+
const mem = await import("../lib/memory.mjs");
|
|
508
|
+
const written = JSON.stringify(rep, null, 2) + "\n";
|
|
509
|
+
const readOr = (p) => {
|
|
510
|
+
try {
|
|
511
|
+
return readFileSync(p, "utf8");
|
|
512
|
+
} catch {
|
|
513
|
+
return "";
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const ids = mem.runAgentIds((Array.isArray(c.journals) ? c.journals : []).map(readOr));
|
|
517
|
+
const blocks = mem.attributeBlocks(readOr(join(dir, ".doug/.state/blocks.jsonl")), ids);
|
|
518
|
+
let condition = null;
|
|
519
|
+
try {
|
|
520
|
+
const parsedCondition = JSON.parse(readFileSync(join(dir, ".doug/.state/reports", c.id, "condition.json"), "utf8"));
|
|
521
|
+
if (parsedCondition && typeof parsedCondition === "object" && !Array.isArray(parsedCondition)) condition = parsedCondition;
|
|
522
|
+
} catch {
|
|
523
|
+
condition = null;
|
|
524
|
+
}
|
|
525
|
+
const row = mem.stopOutcomeRow({
|
|
526
|
+
card: c.id,
|
|
527
|
+
report: rep,
|
|
528
|
+
reportHash: createHash("sha256").update(written).digest("hex"),
|
|
529
|
+
reportPath: join(dir, ".doug/.state/reports", c.id, "card-report.json"),
|
|
530
|
+
run,
|
|
531
|
+
blocks,
|
|
532
|
+
condition,
|
|
533
|
+
});
|
|
534
|
+
const m = mem.openMemory(dir);
|
|
535
|
+
try {
|
|
536
|
+
mem.recordOutcomes(m, [row]);
|
|
537
|
+
} finally {
|
|
538
|
+
m.close();
|
|
539
|
+
}
|
|
540
|
+
} catch (e) {
|
|
541
|
+
process.stderr.write(`autopilot: warning: cannot record the stop row for ${c.id}: ${e.message || e}\n`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (kind === "hand" && parsed !== null) {
|
|
546
|
+
try {
|
|
547
|
+
const mem = await import("../lib/memory.mjs");
|
|
548
|
+
const rep = parsed.report ?? parsed;
|
|
549
|
+
const readOr = (p) => {
|
|
550
|
+
try {
|
|
551
|
+
return readFileSync(p, "utf8");
|
|
552
|
+
} catch {
|
|
553
|
+
return "";
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
const sid = process.env.CLAUDE_CODE_SESSION_ID;
|
|
557
|
+
const jp = journalPath(run);
|
|
558
|
+
const journalText = jp ? readOr(jp) : "";
|
|
559
|
+
let queenArgs = null;
|
|
560
|
+
try {
|
|
561
|
+
queenArgs = JSON.parse(readFileSync(join(dir, ".doug/.state/reports", c.id, "queen-args.json"), "utf8"));
|
|
562
|
+
} catch {
|
|
563
|
+
// no saved args: no briefs
|
|
564
|
+
}
|
|
565
|
+
const seatRuns = mem.journalSeatRuns(journalText);
|
|
566
|
+
const briefs = mem.replayBriefs(queenArgs, seatRuns);
|
|
567
|
+
const ids = mem.runAgentIds([journalText]);
|
|
568
|
+
const blocks = mem.attributeBlocks(readOr(join(dir, ".doug/.state/blocks.jsonl")), ids);
|
|
569
|
+
const denials = mem.attributeDenials(sid ? readOr(join(dir, ".doug/.state/trace", `${sid.replace(/[^A-Za-z0-9_.-]/g, "_")}.jsonl`)) : "", ids);
|
|
570
|
+
const scan = await mem.historyScanner(dir);
|
|
571
|
+
const entries = [{ kind: "report", source: "card-report.json", value: rep }];
|
|
572
|
+
for (const b of blocks) entries.push({ kind: "block", source: "blocks.jsonl", value: b });
|
|
573
|
+
for (const d of denials) entries.push({ kind: "denial", source: "trace", value: d });
|
|
574
|
+
seatRuns.forEach((r, i) => {
|
|
575
|
+
if (briefs[i] !== null) entries.push({ kind: "brief", seat: r.seat, round: r.round, source: "replay:card-loop.mjs", value: briefs[i] });
|
|
576
|
+
entries.push({ kind: "hand-back", seat: r.seat, round: r.round, source: "journal.jsonl", value: r.result });
|
|
577
|
+
});
|
|
578
|
+
mem.writeRunHistory(dir, { card: c.id, run, entries, scan });
|
|
579
|
+
} catch (e) {
|
|
580
|
+
process.stderr.write(`autopilot: warning: cannot write the run history for ${c.id}: ${e.message || e}\n`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (kind === "hand" && parsed !== null && (parsed.report ?? parsed)?.outcome === "stopped") startFailuresReflect(dir, c.id);
|
|
584
|
+
if (kind === "flow" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.ok === false && !parsed.paused) {
|
|
585
|
+
try {
|
|
586
|
+
const mem = await import("../lib/memory.mjs");
|
|
587
|
+
const readOr = (p) => {
|
|
588
|
+
try {
|
|
589
|
+
return readFileSync(p, "utf8");
|
|
590
|
+
} catch {
|
|
591
|
+
return "";
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
const written = JSON.stringify(parsed, null, 2) + "\n";
|
|
595
|
+
const ids = mem.runAgentIds((Array.isArray(c.journals) ? c.journals : []).map(readOr));
|
|
596
|
+
const blocks = mem.attributeBlocks(readOr(join(dir, ".doug/.state/blocks.jsonl")), ids);
|
|
597
|
+
const levels = Array.isArray(parsed.levels) ? parsed.levels : [];
|
|
598
|
+
let stopClass = null;
|
|
599
|
+
let stopReason = null;
|
|
600
|
+
for (const lv of levels) {
|
|
601
|
+
const t = (Array.isArray(lv?.tasks) ? lv.tasks : []).find((x) => x && typeof x.stopReason === "string" && x.stopReason.trim() !== "");
|
|
602
|
+
if (t) {
|
|
603
|
+
stopClass = t.stopClass ?? null;
|
|
604
|
+
stopReason = t.stopReason;
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (stopReason === null) {
|
|
609
|
+
stopReason = "the run stopped";
|
|
610
|
+
if (parsed.stoppedAtLevel !== undefined && parsed.stoppedAtLevel !== null) {
|
|
611
|
+
stopReason += ` at level ${parsed.stoppedAtLevel}`;
|
|
612
|
+
const lv = levels.find((x) => x && x.index === parsed.stoppedAtLevel) ?? levels[parsed.stoppedAtLevel];
|
|
613
|
+
const why = lv?.integration?.reason;
|
|
614
|
+
if (typeof why === "string" && why.trim() !== "") stopReason += `: ${why}`;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const row = {
|
|
618
|
+
...mem.stopOutcomeRow({
|
|
619
|
+
card: c.id,
|
|
620
|
+
report: { outcome: "stopped", reason: stopClass, detail: stopReason },
|
|
621
|
+
reportHash: createHash("sha256").update(written).digest("hex"),
|
|
622
|
+
reportPath: join(dir, reportPathFor(c.id)),
|
|
623
|
+
run,
|
|
624
|
+
blocks,
|
|
625
|
+
memoryConfig: mem.memoryConfig({ embeddings: readMemoryConfig(dir).embeddings, staleDays: readMemoryStaleDays(dir), track: "flow" }),
|
|
626
|
+
}),
|
|
627
|
+
track: "flow",
|
|
628
|
+
};
|
|
629
|
+
const m = mem.openMemory(dir);
|
|
630
|
+
try {
|
|
631
|
+
mem.recordOutcomes(m, [row]);
|
|
632
|
+
} finally {
|
|
633
|
+
m.close();
|
|
634
|
+
}
|
|
635
|
+
} catch (e) {
|
|
636
|
+
process.stderr.write(`autopilot: warning: cannot record the stop row for ${c.id}: ${e.message || e}\n`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (kind === "flow" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && (parsed.ok === true || parsed.ok === false) && !parsed.paused) {
|
|
640
|
+
try {
|
|
641
|
+
const mem = await import("../lib/memory.mjs");
|
|
642
|
+
const readOr = (p) => {
|
|
643
|
+
try {
|
|
644
|
+
return readFileSync(p, "utf8");
|
|
645
|
+
} catch {
|
|
646
|
+
return "";
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
const sid = process.env.CLAUDE_CODE_SESSION_ID;
|
|
650
|
+
const jp = journalPath(run);
|
|
651
|
+
const ids = mem.runAgentIds([jp ? readOr(jp) : ""]);
|
|
652
|
+
const blocks = mem.attributeBlocks(readOr(join(dir, ".doug/.state/blocks.jsonl")), ids);
|
|
653
|
+
const denials = mem.attributeDenials(sid ? readOr(join(dir, ".doug/.state/trace", `${sid.replace(/[^A-Za-z0-9_.-]/g, "_")}.jsonl`)) : "", ids);
|
|
654
|
+
const scan = await mem.historyScanner(dir);
|
|
655
|
+
const entries = [{ kind: "report", source: "last-report.json", value: parsed }];
|
|
656
|
+
for (const b of blocks) entries.push({ kind: "block", source: "blocks.jsonl", value: b });
|
|
657
|
+
for (const d of denials) entries.push({ kind: "denial", source: "trace", value: d });
|
|
658
|
+
mem.writeRunHistory(dir, { card: c.id, run, entries, scan });
|
|
659
|
+
} catch (e) {
|
|
660
|
+
process.stderr.write(`autopilot: warning: cannot write the run history for ${c.id}: ${e.message || e}\n`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (kind === "flow" && parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && parsed.ok === false && !parsed.paused) startFailuresReflect(dir, c.id);
|
|
487
664
|
const v = classifyNotification(kind, { status: "completed", result: parsed === null ? text : parsed });
|
|
488
665
|
if (v.action === "ask") {
|
|
489
666
|
let answered = false;
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
// flow, or named by .doug/plan.json (each with --force removes it
|
|
39
39
|
// and prints a warning to stderr); prints the removed card as JSON
|
|
40
40
|
// to stdout so a mistaken removal can be re-added by hand
|
|
41
|
-
// board.mjs record <id> <report.json> [dir] --summary "<one sentence>" [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed>] [--run <run-id>] [--claude-dir <dir>]
|
|
41
|
+
// board.mjs record <id> <report.json> [dir] --summary "<one sentence>" [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--run <run-id>] [--claude-dir <dir>]
|
|
42
42
|
// append a run entry for the card to docs/live-runs.md; every adversary block
|
|
43
43
|
// in the report is classified real, marginal, or false (repeat the flag per block).
|
|
44
44
|
// When .doug/plan.json lists the card among its `cards` (a batch), the entry
|
|
45
45
|
// shows only that card's tasks, --cost is that card's tasks' cost, and
|
|
46
46
|
// --shared-cost <usd> the shared integration agents', counted once
|
|
47
|
-
// 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>]
|
|
47
|
+
// board.mjs record <id> --hand [dir] --summary "<one sentence>" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--cost <usd>]
|
|
48
48
|
// append a hand-track entry (no report) for the card
|
|
49
49
|
// --rehearsal <scenario> (either form) marks the entry as a rehearse.mjs run on the ts-basic fixture (the start scenario: an empty git repo): the first
|
|
50
50
|
// prose line and the commit sha (abbreviated to 7 characters) read as the fixture's, not this repository's.
|
|
@@ -94,8 +94,8 @@ const planUsage = (kind) => [
|
|
|
94
94
|
` board.mjs ${kind} edit <id> [dir] [--title <t>] [--goal <g>] [--version <v>] [--date YYYY-MM-DD] [--status planned|active|done] [--force]`,
|
|
95
95
|
` board.mjs ${kind} list [dir]`,
|
|
96
96
|
].join("\n");
|
|
97
|
-
const RECORD_HAND_FLAGS = ["summary", "hand", "commit", "wall", "gate", "note", "rehearsal", "rehearsal-outcome", "cost"];
|
|
98
|
-
const RECORD_REPORT_FLAGS = ["summary", "cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal", "rehearsal-outcome", "run", "claude-dir"];
|
|
97
|
+
const RECORD_HAND_FLAGS = ["summary", "hand", "commit", "wall", "gate", "note", "rehearsal", "rehearsal-outcome", "rehearsal-fixture", "cost"];
|
|
98
|
+
const RECORD_REPORT_FLAGS = ["summary", "cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal", "rehearsal-outcome", "rehearsal-fixture", "run", "claude-dir"];
|
|
99
99
|
const opts = {};
|
|
100
100
|
const positional = [];
|
|
101
101
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -131,8 +131,8 @@ const USAGE_FOR = {
|
|
|
131
131
|
labels: "usage: board.mjs labels --check [dir] | labels init [dir]",
|
|
132
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>]",
|
|
133
133
|
};
|
|
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>]";
|
|
135
|
-
const RECORD_REPORT_USAGE = "usage: board.mjs record <id> <report.json> [dir] --summary \"<one sentence>\" [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed>] [--run <run-id>] [--claude-dir <dir>]";
|
|
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> [--rehearsal-fixture <name>]] [--cost <usd>]";
|
|
135
|
+
const RECORD_REPORT_USAGE = "usage: board.mjs record <id> <report.json> [dir] --summary \"<one sentence>\" [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--run <run-id>] [--claude-dir <dir>]";
|
|
136
136
|
|
|
137
137
|
// An unknown --flag for the given subcommand: exit 2, name the flag and the subcommand's usage.
|
|
138
138
|
// Runs on flag names only (parse order doesn't matter), before any work or write.
|
|
@@ -145,7 +145,11 @@ function unknownFlagFor(cmd, opts) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
function dirFrom(args, n) {
|
|
148
|
-
|
|
148
|
+
const d = resolve(process.env.CLAUDE_PROJECT_DIR || args[n] || process.cwd());
|
|
149
|
+
if (!existsSync(join(d, ".doug")) && !existsSync(join(d, "docs", "board.json"))) {
|
|
150
|
+
fail(`not a Doug project: ${d} has no .doug/`, 1);
|
|
151
|
+
}
|
|
152
|
+
return d;
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
if (cmd === "epic" || cmd === "milestone") {
|
|
@@ -177,6 +181,7 @@ if (cmd === "record" || cmd === "summary") {
|
|
|
177
181
|
if (cmd === "record") {
|
|
178
182
|
const ro = opts["rehearsal-outcome"];
|
|
179
183
|
if (opts.rehearsal !== undefined && ro !== "passed" && ro !== "failed") fail(`--rehearsal needs --rehearsal-outcome passed|failed, got ${ro === undefined ? "none" : JSON.stringify(ro)}\n${opts.hand ? RECORD_HAND_USAGE : RECORD_REPORT_USAGE}`, 2);
|
|
184
|
+
if (opts.rehearsal === undefined && opts["rehearsal-fixture"] !== undefined) fail(`--rehearsal-fixture is only valid with --rehearsal\n${opts.hand ? RECORD_HAND_USAGE : RECORD_REPORT_USAGE}`, 2);
|
|
180
185
|
if (opts.rehearsal === undefined && ro !== undefined) fail(`--rehearsal-outcome is only valid with --rehearsal\n${opts.hand ? RECORD_HAND_USAGE : RECORD_REPORT_USAGE}`, 2);
|
|
181
186
|
}
|
|
182
187
|
|
|
@@ -192,7 +197,8 @@ function landingBlock({ id, ok, commit, wall, gate, cost, codexCost, note, rehea
|
|
|
192
197
|
const learned = typeof note === "string" && note.trim() ? note.trim().replace(/\s+/g, " ") : "nothing new";
|
|
193
198
|
const lines = [`Changed: ${summarySentence}`, `Outcome: ${outcome}`, `Learned: ${learned}`];
|
|
194
199
|
const words = lines.join("\n").split(/\s+/).filter(Boolean).length;
|
|
195
|
-
|
|
200
|
+
// A rehearsal's block is print-only (no landing-block files, so the Stop gate checks nothing): no cap, so a failed stage's long note still records.
|
|
201
|
+
if (!rehearsal && words > LANDING_BLOCK_WORDS) fail(`landing block is ${words} words; the limit is ${LANDING_BLOCK_WORDS}. Shorten --summary or --note.`);
|
|
196
202
|
return lines;
|
|
197
203
|
}
|
|
198
204
|
// The pending marker holds the session that landed, so only that session's Stop checks it (empty when unset).
|
|
@@ -212,6 +218,25 @@ function writeLandingBlock(dir, id, lines, keepFile = false, printOnly = false)
|
|
|
212
218
|
}
|
|
213
219
|
process.stdout.write(lines.join("\n") + "\n");
|
|
214
220
|
}
|
|
221
|
+
// A plan landed after `plan.mjs done --force` (landed set, report not ok, --commit naming the land merge or absent,
|
|
222
|
+
// not a rehearsal) is a landed card whatever the report says; its gate is land's own acceptance. In a batch plan
|
|
223
|
+
// only the entries tagged `[<card>] ` count, or the whole list labelled as the plan's when none carries the tag.
|
|
224
|
+
function forcedLanding({ plan, report, opts, card }) {
|
|
225
|
+
const lc = plan && plan.landed && typeof plan.landed === "object" ? plan.landed : null;
|
|
226
|
+
const lm = lc && typeof lc.mergeCommit === "string" ? lc.mergeCommit : "";
|
|
227
|
+
const forced = report.ok !== true && !opts.rehearsal && lc !== null && (opts.commit === undefined || (lm !== "" && (lm.startsWith(String(opts.commit)) || String(opts.commit).startsWith(lm))));
|
|
228
|
+
if (!forced) return { forced: false, gate: null };
|
|
229
|
+
if (!Array.isArray(lc.acceptance)) return { forced, gate: "acceptance checked at land" };
|
|
230
|
+
const count = (list) => `${list.filter((a) => a && a.ok).length}/${list.length}`;
|
|
231
|
+
if (plan && Array.isArray(plan.cards) && plan.cards.includes(card)) {
|
|
232
|
+
const tag = `[${card}] `;
|
|
233
|
+
const own = lc.acceptance.filter((a) => a && (typeof a === "string" ? a : String(a.text ?? "")).startsWith(tag));
|
|
234
|
+
if (own.length) return { forced, gate: `acceptance ${count(own)} at land` };
|
|
235
|
+
return { forced, gate: `acceptance ${count(lc.acceptance)} at land (the plan's)` };
|
|
236
|
+
}
|
|
237
|
+
return { forced, gate: `acceptance ${count(lc.acceptance)} at land` };
|
|
238
|
+
}
|
|
239
|
+
|
|
215
240
|
function acceptanceGate(report) {
|
|
216
241
|
const levels = (report.levels || []).filter((l) => l && l.integration);
|
|
217
242
|
const acc = levels.length && Array.isArray(levels[levels.length - 1].integration.acceptance) ? levels[levels.length - 1].integration.acceptance : [];
|
|
@@ -434,11 +459,11 @@ try {
|
|
|
434
459
|
case "record": {
|
|
435
460
|
if (opts.hand) {
|
|
436
461
|
const [id] = rest;
|
|
437
|
-
if (!id) fail("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>]", 2);
|
|
462
|
+
if (!id) fail("usage: board.mjs record <id> --hand [dir] --summary \"<one sentence>\" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--cost <usd>]", 2);
|
|
438
463
|
if (opts.cost !== undefined && !/^\d+(\.\d+)?$/.test(opts.cost)) fail(`--cost takes a non-negative number of dollars, got ${JSON.stringify(opts.cost)}`, 2);
|
|
439
464
|
const dir = dirFrom(rest, 1);
|
|
440
465
|
const card = findCard(loadBoard(dir), id);
|
|
441
|
-
const entry = handEntry({ card, commit: opts.commit ?? null, wallClock: opts.wall ?? null, gate: opts.gate ?? null, note: opts.note ?? null, record: relative(dir, boardPath(dir)), rehearsal: opts.rehearsal ?? null, rehearsalOutcome: opts["rehearsal-outcome"] ?? null, cost: opts.cost !== undefined ? Number(opts.cost) : null });
|
|
466
|
+
const entry = handEntry({ card, commit: opts.commit ?? null, wallClock: opts.wall ?? null, gate: opts.gate ?? null, note: opts.note ?? null, record: relative(dir, boardPath(dir)), rehearsal: opts.rehearsal ?? null, rehearsalOutcome: opts["rehearsal-outcome"] ?? null, fixture: opts["rehearsal-fixture"] ?? null, cost: opts.cost !== undefined ? Number(opts.cost) : null });
|
|
442
467
|
const block = landingBlock({ id, ok: true, commit: opts.commit, wall: opts.wall, gate: opts.gate, cost: opts.cost, note: opts.note, rehearsal: opts.rehearsal ?? null, rehearsalOutcome: opts["rehearsal-outcome"] ?? null });
|
|
443
468
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
444
469
|
process.stdout.write(`${summarySentence}\n`);
|
|
@@ -452,7 +477,7 @@ try {
|
|
|
452
477
|
break;
|
|
453
478
|
}
|
|
454
479
|
const [id, reportFile] = rest;
|
|
455
|
-
if (!id || !reportFile) fail("usage: board.mjs record <id> <report.json> [dir] --summary \"<one sentence>\" [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed>] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
480
|
+
if (!id || !reportFile) fail("usage: board.mjs record <id> <report.json> [dir] --summary \"<one sentence>\" [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario> --rehearsal-outcome <passed|failed> [--rehearsal-fixture <name>]] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
456
481
|
const dir = dirFrom(rest, 2);
|
|
457
482
|
const card = findCard(loadBoard(dir), id);
|
|
458
483
|
const report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
|
|
@@ -461,8 +486,10 @@ try {
|
|
|
461
486
|
const batch = plan && Array.isArray(plan.cards) && plan.cards.includes(id) ? plan.cards : null;
|
|
462
487
|
const taskIds = batch ? plan.tasks.filter((t) => t.card === id).map((t) => t.id) : [];
|
|
463
488
|
const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: dir, wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
|
|
464
|
-
|
|
465
|
-
const
|
|
489
|
+
// A plan landed after `plan.mjs done --force` is recorded as landed, with land's own acceptance.
|
|
490
|
+
const { forced, gate: forcedGate } = forcedLanding({ plan, report, opts, card: id });
|
|
491
|
+
const entry = runEntry({ forcedLanding: forced, card, report, wall, cost: opts.cost !== undefined ? Number(opts.cost) : null, codexCost: opts["codex-cost"] !== undefined ? Number(opts["codex-cost"]) : null, wallClock: opts.wall ?? null, mergeCommit: opts.commit ?? null, record: relative(dir, boardPath(dir)), adversary: parseAdversaryClasses(opts.adversary || []), batch, taskIds, sharedCost: opts["shared-cost"] !== undefined ? Number(opts["shared-cost"]) : null, note: opts.note ?? null, rehearsal: opts.rehearsal ?? null, rehearsalOutcome: opts["rehearsal-outcome"] ?? null, fixture: opts["rehearsal-fixture"] ?? null });
|
|
492
|
+
const block = landingBlock({ id, ok: report.ok === true || forced, commit: opts.commit, wall: opts.wall, gate: forced ? forcedGate : acceptanceGate(report), cost: opts.cost, codexCost: opts["codex-cost"], note: opts.note, rehearsal: opts.rehearsal ?? null, rehearsalOutcome: opts["rehearsal-outcome"] ?? null });
|
|
466
493
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
467
494
|
process.stdout.write(`${summarySentence}\n`);
|
|
468
495
|
if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
|
|
@@ -491,9 +518,10 @@ try {
|
|
|
491
518
|
if (saved.length === 3) sumSaved = saved;
|
|
492
519
|
} catch {}
|
|
493
520
|
}
|
|
494
|
-
const
|
|
521
|
+
const sumForced = opts.card !== undefined ? forcedLanding({ plan: sumPlan, report: wholeReport, opts, card: opts.card }) : { forced: false, gate: null };
|
|
522
|
+
const sumBlock = sumSaved ? sumSaved : opts.card !== undefined ? landingBlock({ id: opts.card, ok: report.ok === true || sumForced.forced, commit: opts.commit, wall: opts.wall, gate: sumForced.forced ? sumForced.gate : acceptanceGate(wholeReport), cost: opts.cost, codexCost: opts["codex-cost"], note: opts.note }) : null;
|
|
495
523
|
process.stdout.write(`${summarySentence}\n`);
|
|
496
|
-
process.stdout.write(runSummary({ report, wall, batch: sumBatch, card: opts.card ?? null, cost: opts.cost !== undefined ? Number(opts.cost) : null, codexCost: opts["codex-cost"] !== undefined ? Number(opts["codex-cost"]) : null, wallClock: opts.wall ?? null, mergeCommit: opts.commit ?? null }));
|
|
524
|
+
process.stdout.write(runSummary({ report, forcedLanding: sumForced.forced, wall, batch: sumBatch, card: opts.card ?? null, cost: opts.cost !== undefined ? Number(opts.cost) : null, codexCost: opts["codex-cost"] !== undefined ? Number(opts["codex-cost"]) : null, wallClock: opts.wall ?? null, mergeCommit: opts.commit ?? null }));
|
|
497
525
|
if (sumBlock) writeLandingBlock(dirFrom(rest, 1), opts.card, sumBlock, sumSaved !== null);
|
|
498
526
|
break;
|
|
499
527
|
}
|
|
@@ -43,7 +43,9 @@ for (const a of argv) {
|
|
|
43
43
|
const [cmd, ...rest] = positional;
|
|
44
44
|
|
|
45
45
|
function dirFrom(args, n) {
|
|
46
|
-
|
|
46
|
+
const d = resolve(process.env.CLAUDE_PROJECT_DIR || args[n] || process.cwd());
|
|
47
|
+
if (!existsSync(join(d, ".doug"))) fail(`not a Doug project: ${d} has no .doug/`, 1);
|
|
48
|
+
return d;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
function readClaudeMd(dir) {
|
|
@@ -95,7 +97,7 @@ function readOwnSkills() {
|
|
|
95
97
|
function signalsText(s) {
|
|
96
98
|
return [
|
|
97
99
|
`outcomes: ${s.outcomes.rows.length} rows, ${s.outcomes.gateFailures} gate failures, blocks real=${s.outcomes.blocksByClass.real} marginal=${s.outcomes.blocksByClass.marginal} false=${s.outcomes.blocksByClass.false}, fixPasses sum=${s.outcomes.fixPasses.sum} max=${s.outcomes.fixPasses.max}, usd sum=${s.outcomes.usd.sum.toFixed(2)} n=${s.outcomes.usd.n}, cards=${Object.keys(s.outcomes.byCard).length}`,
|
|
98
|
-
`lessons: ${s.lessons.total} total, ${s.lessons.live} live, ${s.lessons.helpful} helpful, ${s.lessons.harmful} harmful, ${s.lessons.stale} stale, ${s.lessons.
|
|
100
|
+
`lessons: ${s.lessons.total} total, ${s.lessons.live} live, ${s.lessons.helpful} helpful, ${s.lessons.harmful} harmful, ${s.lessons.stale} stale, ${s.lessons.pitfalls.length} pitfalls with occurrences`,
|
|
99
101
|
`trace: ${s.trace.files.length} files, ${s.trace.denials.length} denials, ${s.trace.unmatched.length} unmatched (reported only, never a proposal signal), ${s.trace.skills.length} skills invoked, ${s.trace.instructionsLoaded.length} instruction files loaded`,
|
|
100
102
|
].join("\n");
|
|
101
103
|
}
|