@dougharness/cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +10 -3
- package/dist/bin.js.map +1 -1
- package/dist/board-serve.js +45 -10
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +33 -8
- package/dist/board.js.map +1 -1
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +7 -0
- package/dist/memory-setup.js +75 -48
- package/dist/memory-setup.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +6 -2
- package/node_modules/@dougharness/flow/agents/coder.md +23 -0
- package/node_modules/@dougharness/flow/agents/lead.md +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +2 -2
- package/node_modules/@dougharness/flow/agents/spec-writer.md +23 -0
- package/node_modules/@dougharness/flow/agents/tester.md +21 -0
- package/node_modules/@dougharness/flow/hooks/hooks.json +4 -1
- package/node_modules/@dougharness/flow/hooks/register.tsx +75 -0
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +117 -0
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +17 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +107 -10
- package/node_modules/@dougharness/flow/lib/card-land.mjs +115 -0
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +746 -0
- package/node_modules/@dougharness/flow/lib/coordinator.mjs +368 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +44 -11
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +147 -4
- package/node_modules/@dougharness/flow/lib/land.mjs +33 -9
- package/node_modules/@dougharness/flow/lib/memory.mjs +96 -33
- package/node_modules/@dougharness/flow/lib/plan.mjs +34 -5
- package/node_modules/@dougharness/flow/lib/worktrees.mjs +3 -3
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +407 -0
- package/node_modules/@dougharness/flow/scripts/board.mjs +128 -32
- package/node_modules/@dougharness/flow/scripts/context.mjs +4 -2
- package/node_modules/@dougharness/flow/scripts/cost.mjs +16 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +29 -14
- package/node_modules/@dougharness/flow/scripts/plan.mjs +122 -30
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +10 -4
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +34 -12
- package/node_modules/@dougharness/flow/skills/doug-plan/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +80 -0
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +2 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +12 -10
- package/node_modules/@dougharness/flow/workflows/card-queen.js +807 -0
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +33 -21
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/anchor.mjs +29 -4
- package/node_modules/@dougharness/gates/lib/bash-rules.mjs +217 -20
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +126 -0
- package/node_modules/@dougharness/gates/lib/proposals.mjs +41 -5
- package/node_modules/@dougharness/gates/lib/scope.mjs +43 -4
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/reanchor.mjs +2 -1
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +148 -97
- package/node_modules/@dougharness/gates/scripts/verify.mjs +64 -5
- package/package.json +4 -4
- package/templates/board-app.js +48 -1
|
@@ -7,11 +7,13 @@
|
|
|
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)
|
|
10
|
-
//
|
|
10
|
+
// and a `usd cap: $<x>` line after it (the cards' budgets from the usd table)
|
|
11
|
+
// board.mjs budgets [dir] [--usd] [--from <live-runs file>]
|
|
11
12
|
// without --from, the shipped wall budget table as `table: <JSON>`;
|
|
12
13
|
// with --from, recomputes it (p80 per size/class cell) from the file's
|
|
13
14
|
// Wall clock rows joined to the board, one line per cell and a final
|
|
14
|
-
// `table:` line; writes nothing (replace WALL_BUDGET_MIN by hand)
|
|
15
|
+
// `table:` line; writes nothing (replace WALL_BUDGET_MIN by hand). --usd does the same for the usd
|
|
16
|
+
// table (USD_BUDGET) from the Cost rows of flow and hand entries
|
|
15
17
|
// board.mjs card <id> [dir] one card as JSON
|
|
16
18
|
// board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom
|
|
17
19
|
// reorder a card within its column
|
|
@@ -27,33 +29,35 @@
|
|
|
27
29
|
// flow, or named by .doug/plan.json (each with --force removes it
|
|
28
30
|
// and prints a warning to stderr); prints the removed card as JSON
|
|
29
31
|
// to stdout so a mistaken removal can be re-added by hand
|
|
30
|
-
// board.mjs record <id> <report.json> [dir] [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--note <text>] [--rehearsal <scenario>] [--run <run-id>] [--claude-dir <dir>]
|
|
32
|
+
// 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>]
|
|
31
33
|
// append a run entry for the card to docs/live-runs.md; every adversary block
|
|
32
34
|
// in the report is classified real, marginal, or false (repeat the flag per block).
|
|
33
35
|
// When .doug/plan.json lists the card among its `cards` (a batch), the entry
|
|
34
36
|
// shows only that card's tasks, --cost is that card's tasks' cost, and
|
|
35
37
|
// --shared-cost <usd> the shared integration agents', counted once
|
|
36
|
-
// board.mjs record <id> --hand [dir] [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]
|
|
38
|
+
// 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>]
|
|
37
39
|
// append a hand-track entry (no report) for the card
|
|
38
|
-
// --rehearsal <scenario> (either form) marks the entry as a rehearse.mjs run on the ts-basic fixture: the first
|
|
40
|
+
// --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
|
|
39
41
|
// prose line and the commit sha (abbreviated to 7 characters) read as the fixture's, not this repository's.
|
|
42
|
+
// --rehearsal-outcome <passed|failed> is required with --rehearsal (and refused without it): the entry's Outcome reads
|
|
43
|
+
// "rehearsal <scenario> <outcome>", never "landed", and a rehearsal record writes no landing-block files (it is no landing).
|
|
40
44
|
// Either record form also promotes .doug/.state/research/<id>.md to docs/research/<id>.md when that note
|
|
41
45
|
// exists and differs from the docs copy, copying it and printing one line saying so; a card with no note, or
|
|
42
46
|
// whose docs copy is already byte-identical, is untouched, silently.
|
|
43
|
-
// board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]
|
|
47
|
+
// 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>]
|
|
44
48
|
// the three-line chat summary of a report (--card: that card's tasks only)
|
|
45
49
|
|
|
46
|
-
import { readFileSync } from "node:fs";
|
|
47
|
-
import { relative, resolve } from "node:path";
|
|
48
|
-
import { loadBoard, saveBoard, findCard, nextReadyCard, nextReadyCards, moveCard, editCard, removeCard, reorderCard, readyCapWarning, wallCapMinutes, WALL_BUDGET_MIN, WALL_BUDGET_MIN_N, parseLiveRunWalls, wallBudgetTable, runEntry, handEntry, runSummary, boardPath, parseAdversaryClasses, filterReportForCard, recordLanding } from "../lib/board.mjs";
|
|
49
|
-
import { loadPlan, unwrapReport, DEFAULT_BUDGET } from "../lib/plan.mjs";
|
|
50
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
51
|
+
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";
|
|
53
|
+
import { loadPlan, planFor, unwrapReport, DEFAULT_BUDGET } from "../lib/plan.mjs";
|
|
50
54
|
import { wallForRun } from "../lib/cost.mjs";
|
|
51
55
|
|
|
52
56
|
const EDIT_FIELD_ORDER = ["title", "goal", "size", "component", "track", "class", "tags", "deps", "source"];
|
|
53
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]";
|
|
54
58
|
|
|
55
59
|
const argv = process.argv.slice(2);
|
|
56
|
-
const FLAGS = new Set(["hand", "top", "bottom", "force"]);
|
|
60
|
+
const FLAGS = new Set(["hand", "top", "bottom", "force", "usd"]);
|
|
57
61
|
const REPEATABLE = new Set(["adversary"]);
|
|
58
62
|
|
|
59
63
|
// Each subcommand's allowed flags, derived from what that case actually reads (card board-cli-unknown-flags).
|
|
@@ -65,11 +69,11 @@ const SUBCOMMAND_FLAGS = {
|
|
|
65
69
|
edit: ["title", "goal", "goal-file", "size", "component", "track", "class", "deps", "tag", "source", "force"],
|
|
66
70
|
remove: ["force"],
|
|
67
71
|
reorder: ["before", "after", "top", "bottom"],
|
|
68
|
-
budgets: ["from"],
|
|
69
|
-
summary: ["cost", "codex-cost", "wall", "commit", "card", "run", "claude-dir"],
|
|
72
|
+
budgets: ["from", "usd"],
|
|
73
|
+
summary: ["summary", "cost", "codex-cost", "wall", "commit", "card", "run", "claude-dir", "note"],
|
|
70
74
|
};
|
|
71
|
-
const RECORD_HAND_FLAGS = ["hand", "commit", "wall", "gate", "note", "rehearsal"];
|
|
72
|
-
const RECORD_REPORT_FLAGS = ["cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal", "run", "claude-dir"];
|
|
75
|
+
const RECORD_HAND_FLAGS = ["summary", "hand", "commit", "wall", "gate", "note", "rehearsal", "rehearsal-outcome", "cost"];
|
|
76
|
+
const RECORD_REPORT_FLAGS = ["summary", "cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal", "rehearsal-outcome", "run", "claude-dir"];
|
|
73
77
|
const opts = {};
|
|
74
78
|
const positional = [];
|
|
75
79
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -100,11 +104,11 @@ const USAGE_FOR = {
|
|
|
100
104
|
edit: EDIT_USAGE,
|
|
101
105
|
remove: "usage: board.mjs remove <id> [dir] [--force]",
|
|
102
106
|
reorder: "usage: board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom",
|
|
103
|
-
budgets: "usage: board.mjs budgets [dir] [--from <live-runs file>]",
|
|
104
|
-
summary: "usage: board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]",
|
|
107
|
+
budgets: "usage: board.mjs budgets [dir] [--usd] [--from <live-runs file>]",
|
|
108
|
+
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>]",
|
|
105
109
|
};
|
|
106
|
-
const RECORD_HAND_USAGE = "usage: board.mjs record <id> --hand [dir] [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]";
|
|
107
|
-
const RECORD_REPORT_USAGE = "usage: board.mjs record <id> <report.json> [dir] [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario>] [--run <run-id>] [--claude-dir <dir>]";
|
|
110
|
+
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>]";
|
|
111
|
+
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>]";
|
|
108
112
|
|
|
109
113
|
// An unknown --flag for the given subcommand: exit 2, name the flag and the subcommand's usage.
|
|
110
114
|
// Runs on flag names only (parse order doesn't matter), before any work or write.
|
|
@@ -128,6 +132,62 @@ if (cmd === "record" || Object.prototype.hasOwnProperty.call(SUBCOMMAND_FLAGS, c
|
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
// --summary: the lead's one sentence of what changed for the user, required by record and summary (card
|
|
136
|
+
// landing-one-sentence-summary). Trimmed, non-empty, one line, no sentence end followed by more text.
|
|
137
|
+
// Checked before anything is appended or promoted; printed as stdout's first line, never written to a file.
|
|
138
|
+
let summarySentence = null;
|
|
139
|
+
if (cmd === "record" || cmd === "summary") {
|
|
140
|
+
const text = typeof opts.summary === "string" ? opts.summary.trim() : "";
|
|
141
|
+
if (!text) fail(`--summary "<one sentence>" is required for board.mjs ${cmd}: one sentence, in plain words, of what changed for the user${cmd === "record" ? `\n${opts.hand ? RECORD_HAND_USAGE : RECORD_REPORT_USAGE}` : ""}`, 2);
|
|
142
|
+
if (/[\r\n]/.test(text) || /[.!?]\s+\S/.test(text)) fail(`--summary must be one sentence for board.mjs ${cmd}: no newline, and no sentence end followed by more text`, 2);
|
|
143
|
+
summarySentence = text;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --rehearsal-outcome (card rehearsal-failure-not-landed): required with --rehearsal, refused without it; checked before any write.
|
|
147
|
+
if (cmd === "record") {
|
|
148
|
+
const ro = opts["rehearsal-outcome"];
|
|
149
|
+
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);
|
|
150
|
+
if (opts.rehearsal === undefined && ro !== undefined) fail(`--rehearsal-outcome is only valid with --rehearsal\n${opts.hand ? RECORD_HAND_USAGE : RECORD_REPORT_USAGE}`, 2);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The landing block (card landing-block-enforced): three lines (Changed / Outcome / Learned), at most 100 words,
|
|
154
|
+
// printed last by record and summary, kept under .doug/.state/reports/<id>/ for the Stop gate to check against the
|
|
155
|
+
// lead's final message. Built and checked before anything is written, so a refusal leaves no trace.
|
|
156
|
+
const LANDING_BLOCK_WORDS = 100;
|
|
157
|
+
function landingBlock({ id, ok, commit, wall, gate, cost, codexCost, note, rehearsal = null, rehearsalOutcome = null }) {
|
|
158
|
+
const money = (v) => `$${Number(v).toFixed(2)}`;
|
|
159
|
+
const costText = cost !== undefined && cost !== null ? `${money(cost)}${codexCost !== undefined && codexCost !== null ? ` + Codex ${money(codexCost)}` : ""}` : codexCost !== undefined && codexCost !== null ? `Codex ${money(codexCost)}` : "";
|
|
160
|
+
const verdict = rehearsal ? `rehearsal ${rehearsal} ${rehearsalOutcome}` : `${ok ? "landed" : "stopped"} ${id}${commit ? ` at ${commit}` : ""}`;
|
|
161
|
+
const outcome = [verdict, wall, gate, costText].filter(Boolean).join(", ");
|
|
162
|
+
const learned = typeof note === "string" && note.trim() ? note.trim().replace(/\s+/g, " ") : "nothing new";
|
|
163
|
+
const lines = [`Changed: ${summarySentence}`, `Outcome: ${outcome}`, `Learned: ${learned}`];
|
|
164
|
+
const words = lines.join("\n").split(/\s+/).filter(Boolean).length;
|
|
165
|
+
if (words > LANDING_BLOCK_WORDS) fail(`landing block is ${words} words; the limit is ${LANDING_BLOCK_WORDS}. Shorten --summary or --note.`);
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
// The pending marker holds the session that landed, so only that session's Stop checks it (empty when unset).
|
|
169
|
+
function arm(d) {
|
|
170
|
+
writeFileSync(join(d, "landing-block.pending"), process.env.CLAUDE_CODE_SESSION_ID || "");
|
|
171
|
+
}
|
|
172
|
+
function writeLandingBlock(dir, id, lines, keepFile = false, printOnly = false) {
|
|
173
|
+
// The block files only arm the Stop gate's check; a state dir that cannot be written must not fail a landing that
|
|
174
|
+
// is already recorded, so the block still prints and the gate simply has nothing to check.
|
|
175
|
+
if (!printOnly) try {
|
|
176
|
+
const d = join(dir, ".doug/.state/reports", id);
|
|
177
|
+
mkdirSync(d, { recursive: true });
|
|
178
|
+
if (!keepFile) writeFileSync(join(d, "landing-block.txt"), lines.join("\n") + "\n");
|
|
179
|
+
arm(d);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
process.stderr.write(`warning: landing block not saved for the Stop gate: ${e.message}\n`);
|
|
182
|
+
}
|
|
183
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
184
|
+
}
|
|
185
|
+
function acceptanceGate(report) {
|
|
186
|
+
const levels = (report.levels || []).filter((l) => l && l.integration);
|
|
187
|
+
const acc = levels.length && Array.isArray(levels[levels.length - 1].integration.acceptance) ? levels[levels.length - 1].integration.acceptance : [];
|
|
188
|
+
return `acceptance ${acc.filter((a) => a && a.ok).length}/${acc.length}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
131
191
|
try {
|
|
132
192
|
switch (cmd) {
|
|
133
193
|
case "next": {
|
|
@@ -144,12 +204,14 @@ try {
|
|
|
144
204
|
if (capLine) process.stderr.write(capLine);
|
|
145
205
|
if (batch === null ? !card : !cards.length) fail(track === "hand" ? "No Ready hand-track card whose dependencies are Done." : "No Ready card whose dependencies are Done.");
|
|
146
206
|
if (batch !== null) process.stderr.write(`wall cap: ${wallCapMinutes(cards)} min (size x class table at p80, board.mjs budgets; no size counts as M)\n`);
|
|
207
|
+
if (batch !== null) process.stderr.write(`usd cap: $${usdCapDollars(cards).toFixed(2)} (size x class table at p80 of measured cost, board.mjs budgets --usd; no size counts as M)\n`);
|
|
147
208
|
process.stdout.write(JSON.stringify(batch === null ? card : cards, null, 2) + "\n");
|
|
148
209
|
break;
|
|
149
210
|
}
|
|
150
211
|
case "budgets": {
|
|
212
|
+
const usd = opts.usd === true;
|
|
151
213
|
if (opts.from === undefined) {
|
|
152
|
-
process.stdout.write(`table: ${JSON.stringify(WALL_BUDGET_MIN)}\n`);
|
|
214
|
+
process.stdout.write(`table: ${JSON.stringify(usd ? USD_BUDGET : WALL_BUDGET_MIN)}\n`);
|
|
153
215
|
break;
|
|
154
216
|
}
|
|
155
217
|
const dir = dirFrom(rest, 0);
|
|
@@ -159,12 +221,15 @@ try {
|
|
|
159
221
|
} catch (e) {
|
|
160
222
|
fail(`cannot read --from file ${opts.from}: ${e.message}`, 2);
|
|
161
223
|
}
|
|
162
|
-
const { table, cells } = wallBudgetTable(parseLiveRunWalls(text), loadBoard(dir));
|
|
224
|
+
const { table, cells } = usd ? usdBudgetTable(parseLiveRunCosts(text), loadBoard(dir)) : wallBudgetTable(parseLiveRunWalls(text), loadBoard(dir));
|
|
225
|
+
const shown = (v) => (usd ? `$${v.toFixed(2)}` : String(v));
|
|
226
|
+
const shipped = usd ? USD_BUDGET : WALL_BUDGET_MIN;
|
|
227
|
+
const minN = usd ? USD_BUDGET_MIN_N : WALL_BUDGET_MIN_N;
|
|
163
228
|
const out = [];
|
|
164
229
|
for (const size of ["S", "M", "L"]) {
|
|
165
230
|
for (const c of cells.filter((x) => x.size === size)) {
|
|
166
|
-
if (c.cls === "all") out.push(c.n === 0 ? `${size}/all n=0 (no landings; keeps the shipped ${
|
|
167
|
-
else out.push(`${size}/${c.cls} n=${c.n} p80 ${c.p80}${c.n <
|
|
231
|
+
if (c.cls === "all") out.push(c.n === 0 ? `${size}/all n=0 (no landings; keeps the shipped ${shown(shipped[size].all)})` : `${size}/all n=${c.n} p80 ${shown(c.p80)}`);
|
|
232
|
+
else out.push(`${size}/${c.cls} n=${c.n} p80 ${shown(c.p80)}${c.n < minN ? ` (fewer than ${minN}; falls back to ${size}/all ${shown(table[size].all)})` : ""}`);
|
|
168
233
|
}
|
|
169
234
|
}
|
|
170
235
|
out.push(`table: ${JSON.stringify(table)}`);
|
|
@@ -180,8 +245,16 @@ try {
|
|
|
180
245
|
const [id, column] = rest;
|
|
181
246
|
if (!id || !column) fail("usage: board.mjs move <id> <column> [dir] [--source <text>]", 2);
|
|
182
247
|
const dir = dirFrom(rest, 2);
|
|
183
|
-
|
|
248
|
+
const moved = moveCard(loadBoard(dir), id, column, { source: opts.source });
|
|
249
|
+
saveBoard(dir, moved);
|
|
184
250
|
process.stdout.write(`Moved ${id} to ${column} in ${relative(dir, boardPath(dir))}.\n`);
|
|
251
|
+
// A card moved to done re-arms the Stop gate's landing-block check, only when record left a block (never creates one).
|
|
252
|
+
if (column === "done" && /^[a-z0-9._-]+$/.test(id)) {
|
|
253
|
+
const d = join(dir, ".doug/.state/reports", id);
|
|
254
|
+
if (existsSync(join(d, "landing-block.txt"))) arm(d);
|
|
255
|
+
}
|
|
256
|
+
const capLine = column === "ready" ? readyCapWarning(moved) : null;
|
|
257
|
+
if (capLine) process.stderr.write(capLine);
|
|
185
258
|
break;
|
|
186
259
|
}
|
|
187
260
|
case "edit": {
|
|
@@ -252,44 +325,67 @@ try {
|
|
|
252
325
|
case "record": {
|
|
253
326
|
if (opts.hand) {
|
|
254
327
|
const [id] = rest;
|
|
255
|
-
if (!id) fail("usage: board.mjs record <id> --hand [dir] [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]", 2);
|
|
328
|
+
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);
|
|
329
|
+
if (opts.cost !== undefined && !/^\d+(\.\d+)?$/.test(opts.cost)) fail(`--cost takes a non-negative number of dollars, got ${JSON.stringify(opts.cost)}`, 2);
|
|
256
330
|
const dir = dirFrom(rest, 1);
|
|
257
331
|
const card = findCard(loadBoard(dir), id);
|
|
258
|
-
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 });
|
|
332
|
+
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 });
|
|
333
|
+
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 });
|
|
259
334
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
335
|
+
process.stdout.write(`${summarySentence}\n`);
|
|
336
|
+
if (!opts.rehearsal) {
|
|
337
|
+
const verdict = handWallVerdict(card, opts.wall ?? null);
|
|
338
|
+
if (verdict.over) process.stdout.write(`Wall budget: ${verdict.text}\n`);
|
|
339
|
+
}
|
|
260
340
|
if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
|
|
261
341
|
process.stdout.write(`Appended a hand-track entry for ${id} to ${file}.\n`);
|
|
342
|
+
writeLandingBlock(dir, id, block, false, !!opts.rehearsal);
|
|
262
343
|
break;
|
|
263
344
|
}
|
|
264
345
|
const [id, reportFile] = rest;
|
|
265
|
-
if (!id || !reportFile) fail("usage: board.mjs record <id> <report.json> [dir] [--cost <usd>] [--codex-cost <usd>] [--shared-cost <usd>] [--wall <text>] [--commit <sha>] [--adversary \"<id>=<class>[: <reason>]\"]... [--note <text>] [--rehearsal <scenario>] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
346
|
+
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);
|
|
266
347
|
const dir = dirFrom(rest, 2);
|
|
267
348
|
const card = findCard(loadBoard(dir), id);
|
|
268
349
|
const report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
|
|
269
350
|
// A batch plan (its `cards` list this card) makes the entry that card's part of the run.
|
|
270
|
-
const plan =
|
|
351
|
+
const plan = planFor(dir, id);
|
|
271
352
|
const batch = plan && Array.isArray(plan.cards) && plan.cards.includes(id) ? plan.cards : null;
|
|
272
353
|
const taskIds = batch ? plan.tasks.filter((t) => t.card === id).map((t) => t.id) : [];
|
|
273
354
|
const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: dir, wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
|
|
274
|
-
const entry = runEntry({ 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 });
|
|
355
|
+
const entry = runEntry({ 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 });
|
|
356
|
+
const block = landingBlock({ id, ok: report.ok === true, commit: opts.commit, wall: opts.wall, gate: acceptanceGate(report), cost: opts.cost, codexCost: opts["codex-cost"], note: opts.note, rehearsal: opts.rehearsal ?? null, rehearsalOutcome: opts["rehearsal-outcome"] ?? null });
|
|
275
357
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
358
|
+
process.stdout.write(`${summarySentence}\n`);
|
|
276
359
|
if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
|
|
277
360
|
process.stdout.write(`Appended a run entry for ${id} to ${file}.\n`);
|
|
361
|
+
writeLandingBlock(dir, id, block, false, !!opts.rehearsal);
|
|
278
362
|
break;
|
|
279
363
|
}
|
|
280
364
|
case "summary": {
|
|
281
365
|
const [reportFile] = rest;
|
|
282
|
-
if (!reportFile) fail("usage: board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]", 2);
|
|
366
|
+
if (!reportFile) fail("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>]", 2);
|
|
283
367
|
let report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
|
|
368
|
+
const wholeReport = report; // the block's gate reads the final level's acceptance, which the --card filter drops
|
|
284
369
|
if (opts.card !== undefined) {
|
|
285
370
|
// With the plan at hand the filter also covers a report whose tasks carry no card.
|
|
286
|
-
const plan =
|
|
371
|
+
const plan = planFor(dirFrom(rest, 1), opts.card);
|
|
287
372
|
report = filterReportForCard(report, opts.card, plan && Array.isArray(plan.tasks) ? plan.tasks.filter((t) => t.card === opts.card).map((t) => t.id) : []);
|
|
288
373
|
}
|
|
289
374
|
const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: process.cwd(), wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
|
|
290
|
-
const sumPlan = opts.card !== undefined ?
|
|
375
|
+
const sumPlan = opts.card !== undefined ? planFor(dirFrom(rest, 1), opts.card) : null;
|
|
291
376
|
const sumBatch = sumPlan && Array.isArray(sumPlan.cards) && sumPlan.cards.includes(opts.card) ? sumPlan.cards : null;
|
|
377
|
+
// A block record already saved for this card is reprinted verbatim and re-armed, never recomputed or rewritten.
|
|
378
|
+
let sumSaved = null;
|
|
379
|
+
if (opts.card !== undefined && /^[a-z0-9._-]+$/.test(opts.card)) {
|
|
380
|
+
try {
|
|
381
|
+
const saved = readFileSync(join(dirFrom(rest, 1), ".doug/.state/reports", opts.card, "landing-block.txt"), "utf8").split("\n").filter(Boolean);
|
|
382
|
+
if (saved.length === 3) sumSaved = saved;
|
|
383
|
+
} catch {}
|
|
384
|
+
}
|
|
385
|
+
const sumBlock = sumSaved ? sumSaved : opts.card !== undefined ? landingBlock({ id: opts.card, ok: report.ok === true, commit: opts.commit, wall: opts.wall, gate: acceptanceGate(wholeReport), cost: opts.cost, codexCost: opts["codex-cost"], note: opts.note }) : null;
|
|
386
|
+
process.stdout.write(`${summarySentence}\n`);
|
|
292
387
|
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 }));
|
|
388
|
+
if (sumBlock) writeLandingBlock(dirFrom(rest, 1), opts.card, sumBlock, sumSaved !== null);
|
|
293
389
|
break;
|
|
294
390
|
}
|
|
295
391
|
default:
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// The lead's own context percentage (card autopilot-unlimited), read from the session transcript.
|
|
3
3
|
// context.mjs [dir] [--session <id|latest>] [--claude-dir <path>]
|
|
4
4
|
// dir the project directory (default cwd); <dir>/.doug/config.json supplies autopilot.contextThreshold
|
|
5
|
-
// --session a session id or latest
|
|
5
|
+
// --session a session id or latest, as cost.mjs --session. Default: the CLAUDE_CODE_SESSION_ID environment
|
|
6
|
+
// variable (the lead's own session, so a newer non-lead transcript, such as a reflect pass, is not read);
|
|
7
|
+
// latest only when it is unset or empty. A variable naming a session with no transcript exits 1.
|
|
6
8
|
// --claude-dir where ~/.claude lives (tests point it at a fixture)
|
|
7
9
|
// Prints one line and exits 0; exits 1 with a stderr message when there is no transcript or no usage line.
|
|
8
10
|
import { readFileSync } from "node:fs";
|
|
@@ -33,7 +35,7 @@ const threshold = contextThreshold(config, (m) => process.stderr.write(`[doug] $
|
|
|
33
35
|
|
|
34
36
|
let file;
|
|
35
37
|
try {
|
|
36
|
-
file = sessionTranscriptPath({ session: opts.session || "latest", ...(opts["claude-dir"] ? { claudeDir: resolve(opts["claude-dir"]) } : {}), projectDir: dir });
|
|
38
|
+
file = sessionTranscriptPath({ session: opts.session || process.env.CLAUDE_CODE_SESSION_ID || "latest", ...(opts["claude-dir"] ? { claudeDir: resolve(opts["claude-dir"]) } : {}), projectDir: dir });
|
|
37
39
|
} catch (err) {
|
|
38
40
|
process.stderr.write(`context: ${err.message}\n`);
|
|
39
41
|
process.exit(1);
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// cost.mjs <run-id> [dir] [--json] [--claude-dir <path>]
|
|
7
7
|
// <run-id> the Workflow run id (wf_...)
|
|
8
8
|
// [dir] the project directory whose ~/.claude/projects slug is searched first (default cwd)
|
|
9
|
+
// --since <iso>, --until <iso> with --session: price only the assistant messages whose timestamp is in the
|
|
10
|
+
// window (card usd-budget-size-class: a hand landing's cost, openedAt to the landing)
|
|
9
11
|
// --json print the full result as JSON instead of the text tables
|
|
10
12
|
// --by-card a batch run (the plan at [dir] lists `cards`): add the per-card table, each card's tasks'
|
|
11
13
|
// agents summed for that card and the shared integration agents listed once (`byCard` in --json)
|
|
@@ -19,7 +21,7 @@
|
|
|
19
21
|
import { readFileSync } from "node:fs";
|
|
20
22
|
import { resolve, join } from "node:path";
|
|
21
23
|
import { costRun, costSession, renderSession, renderCost, costByCard, renderCostByCard } from "../lib/cost.mjs";
|
|
22
|
-
import {
|
|
24
|
+
import { planFor, REPORT_RELPATH, unwrapReport } from "../lib/plan.mjs";
|
|
23
25
|
|
|
24
26
|
const argv = process.argv.slice(2);
|
|
25
27
|
const FLAGS = new Set(["json", "by-card"]);
|
|
@@ -43,6 +45,17 @@ if (opts.session === "") {
|
|
|
43
45
|
process.stderr.write("usage: cost.mjs --session <id|latest> [dir] [--json] [--claude-dir <path>]\n");
|
|
44
46
|
process.exit(2);
|
|
45
47
|
}
|
|
48
|
+
const USAGE_SESSION = "usage: cost.mjs --session <id|latest> [dir] [--since <iso>] [--until <iso>] [--json] [--claude-dir <path>]\n";
|
|
49
|
+
const window = {};
|
|
50
|
+
for (const flag of ["since", "until"]) {
|
|
51
|
+
if (opts[flag] === undefined) continue;
|
|
52
|
+
const at = opts[flag] === "" ? NaN : Date.parse(opts[flag]);
|
|
53
|
+
if (opts.session === undefined || Number.isNaN(at)) {
|
|
54
|
+
process.stderr.write(`${USAGE_SESSION}--${flag} takes an ISO date with --session, got ${JSON.stringify(opts[flag])}.\n`);
|
|
55
|
+
process.exit(2);
|
|
56
|
+
}
|
|
57
|
+
window[flag] = at;
|
|
58
|
+
}
|
|
46
59
|
if (!runId && opts.session === undefined) {
|
|
47
60
|
process.stderr.write("usage: cost.mjs <run-id> [dir] [--json] [--claude-dir <path>]\n");
|
|
48
61
|
process.exit(2);
|
|
@@ -50,7 +63,7 @@ if (!runId && opts.session === undefined) {
|
|
|
50
63
|
try {
|
|
51
64
|
const projectDir = resolve(process.env.CLAUDE_PROJECT_DIR || dirArg || process.cwd());
|
|
52
65
|
if (opts.session !== undefined) {
|
|
53
|
-
const result = costSession({ session: opts.session, projectDir, claudeDir: opts["claude-dir"] ? resolve(opts["claude-dir"]) : undefined });
|
|
66
|
+
const result = costSession({ session: opts.session, projectDir, claudeDir: opts["claude-dir"] ? resolve(opts["claude-dir"]) : undefined, since: window.since ?? null, until: window.until ?? null });
|
|
54
67
|
process.stdout.write(opts.json ? JSON.stringify(result, null, 2) + "\n" : renderSession(result));
|
|
55
68
|
process.exit(0);
|
|
56
69
|
}
|
|
@@ -69,7 +82,7 @@ try {
|
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
const result = costRun({ runId, projectDir, claudeDir: opts["claude-dir"] ? resolve(opts["claude-dir"]) : undefined, report, reportPath });
|
|
72
|
-
const byCard = opts["by-card"] ? costByCard(result,
|
|
85
|
+
const byCard = opts["by-card"] ? costByCard(result, planFor(projectDir, opts.card)) : null;
|
|
73
86
|
if (opts.json) process.stdout.write(JSON.stringify(byCard ? { ...result, byCard } : result, null, 2) + "\n");
|
|
74
87
|
else process.stdout.write(renderCost(result) + (byCard ? "\n" + renderCostByCard(byCard) : ""));
|
|
75
88
|
} catch (err) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// run+task); with --run, per-task tokens and USD come from the
|
|
9
9
|
// same local transcripts lib/cost.mjs reads (null, with a note on
|
|
10
10
|
// stderr, when the run's journal cannot be found)
|
|
11
|
-
// memory.mjs record <card-id> --hand [dir] --commit <sha> [--wall <text>] [--gate <text>] [--note <text>] [--json]
|
|
11
|
+
// memory.mjs record <card-id> --hand [dir] --commit <sha> [--wall <text>] [--gate <text>] [--note <text>] [--cost <usd>] [--json]
|
|
12
12
|
// record a hand-track landing (card id, commit, wall clock, gate
|
|
13
13
|
// line, note; nothing else is invented) as one outcomes row keyed
|
|
14
14
|
// hand:<commit> (upsert on the same commit); --commit is required;
|
|
@@ -79,11 +79,14 @@
|
|
|
79
79
|
// lesson's citation is re-checked against [dir] only when
|
|
80
80
|
// [dir] is a real project root (contains .doug)
|
|
81
81
|
// memory.mjs reflect <report.json> [dir] [--run <wf-id>] [--card <id>] [--commit <sha>]
|
|
82
|
-
// [--adversary "<id>=<class>[: <reason>]"]... [--claude-bin <path>] [--claude-dir <path>]
|
|
82
|
+
// [--adversary "<id>=<class>[: <reason>]"]... [--harmful <id>]... [--claude-bin <path>] [--claude-dir <path>]
|
|
83
83
|
// [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
84
84
|
// runs after record: applies deterministic helpful/harmful
|
|
85
85
|
// counters to lessons named in each task's memoryUsed (committed
|
|
86
|
-
// even if the LLM pass below fails
|
|
86
|
+
// even if the LLM pass below fails; a real block alone bumps no
|
|
87
|
+
// harmful: each --harmful <id>, a recalled lesson that misled the
|
|
88
|
+
// work, gets harmful +1 and no helpful; an id outside every
|
|
89
|
+
// task's memoryUsed exits 1 before any write), then a cheap-row `claude -p
|
|
87
90
|
// --json-schema` pass proposes at most 3 lessons from the report,
|
|
88
91
|
// filtered in code (a pitfall needs a block classed real, a
|
|
89
92
|
// pattern needs a first-pass verified task, the citation must be
|
|
@@ -95,8 +98,8 @@
|
|
|
95
98
|
// prints what would happen. Cost is measured from the same local
|
|
96
99
|
// transcripts cost.mjs reads, falling back to the CLI's own
|
|
97
100
|
// cost estimate. Needs Node >=22.16 (node:sqlite).
|
|
98
|
-
// memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "
|
|
99
|
-
// [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
101
|
+
// memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "<R<n> or <card-id>/R<n>>=<class>[: <reason>]"]...
|
|
102
|
+
// [--harmful <id>]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
100
103
|
// (card memory-every-session) the same pass for a hand landing:
|
|
101
104
|
// assembles .doug/.state/reports/<card-id>/hand-report.json from
|
|
102
105
|
// the saved briefs/, reviews/ and lessons.json (memoryUsed = its
|
|
@@ -187,7 +190,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
187
190
|
import { createHash } from "node:crypto";
|
|
188
191
|
import { spawnSync } from "node:child_process";
|
|
189
192
|
import { parseAdversaryClasses, loadBoard, findCard } from "../lib/board.mjs";
|
|
190
|
-
import {
|
|
193
|
+
import { planFor, unwrapReport } from "../lib/plan.mjs";
|
|
191
194
|
import { costRun } from "../lib/cost.mjs";
|
|
192
195
|
import { loadModels } from "../lib/models.mjs";
|
|
193
196
|
import {
|
|
@@ -204,7 +207,7 @@ import {
|
|
|
204
207
|
|
|
205
208
|
const argv = process.argv.slice(2);
|
|
206
209
|
const FLAGS = new Set(["json", "all", "hand", "dry-run", "check", "no-check"]);
|
|
207
|
-
const REPEATABLE = new Set(["adversary", "review"]);
|
|
210
|
+
const REPEATABLE = new Set(["adversary", "review", "harmful"]);
|
|
208
211
|
const opts = {};
|
|
209
212
|
const positional = [];
|
|
210
213
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -505,7 +508,7 @@ try {
|
|
|
505
508
|
case "record": {
|
|
506
509
|
if (opts.hand) {
|
|
507
510
|
const handUsage =
|
|
508
|
-
'usage: memory.mjs record <card-id> --hand [dir] --commit <sha> [--wall <text>] [--gate <text>] [--note <text>] [--json]';
|
|
511
|
+
'usage: memory.mjs record <card-id> --hand [dir] --commit <sha> [--wall <text>] [--gate <text>] [--note <text>] [--cost <usd>] [--json]';
|
|
509
512
|
const [id, ...extra] = rest;
|
|
510
513
|
if (!id || !opts.commit) fail(handUsage, 2);
|
|
511
514
|
if (id.endsWith(".json") || existsSync(resolve(id))) {
|
|
@@ -514,6 +517,7 @@ try {
|
|
|
514
517
|
if (opts.run !== undefined || opts.adversary !== undefined || opts["claude-dir"] !== undefined || extra.length > 1) {
|
|
515
518
|
fail(handUsage, 2);
|
|
516
519
|
}
|
|
520
|
+
if (opts.cost !== undefined && !/^\d+(\.\d+)?$/.test(opts.cost)) fail(`${handUsage}\n--cost takes a non-negative number of dollars, got ${JSON.stringify(opts.cost)}.`, 2);
|
|
517
521
|
const dir = dirFrom(rest, 1);
|
|
518
522
|
// condition.json (card outcomes-condition-columns): read when `condition open` wrote one before the
|
|
519
523
|
// brief; a stamp missing entirely (the common case until core-next-hand-report wires this in) is not a
|
|
@@ -528,7 +532,7 @@ try {
|
|
|
528
532
|
}
|
|
529
533
|
}
|
|
530
534
|
const { openMemory, recordOutcomes, handOutcomeRow } = await import("../lib/memory.mjs");
|
|
531
|
-
const row = handOutcomeRow({ card: id, commit: opts.commit, wallClock: textOrNull(opts.wall), gate: textOrNull(opts.gate), note: textOrNull(opts.note), condition });
|
|
535
|
+
const row = handOutcomeRow({ card: id, commit: opts.commit, wallClock: textOrNull(opts.wall), gate: textOrNull(opts.gate), note: textOrNull(opts.note), condition, cost: opts.cost !== undefined ? Number(opts.cost) : null });
|
|
532
536
|
const m = openMemory(dir);
|
|
533
537
|
let result;
|
|
534
538
|
try {
|
|
@@ -577,7 +581,7 @@ try {
|
|
|
577
581
|
}
|
|
578
582
|
const reportHash = createHash("sha256").update(reportText).digest("hex");
|
|
579
583
|
const classes = parseAdversaryClasses(opts.adversary || []);
|
|
580
|
-
const plan =
|
|
584
|
+
const plan = planFor(dir, opts.card);
|
|
581
585
|
const run = opts.run || null;
|
|
582
586
|
const runLabel = run || `report:${reportHash.slice(0, 12)}`;
|
|
583
587
|
let cost = null;
|
|
@@ -612,7 +616,7 @@ try {
|
|
|
612
616
|
}
|
|
613
617
|
case "reflect": {
|
|
614
618
|
const usage =
|
|
615
|
-
'usage: memory.mjs reflect <report.json> [dir] [--run <wf-id>] [--card <id>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--claude-bin <path>] [--claude-dir <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
619
|
+
'usage: memory.mjs reflect <report.json> [dir] [--run <wf-id>] [--card <id>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--harmful <id>]... [--claude-bin <path>] [--claude-dir <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
616
620
|
let dir;
|
|
617
621
|
let reportPath;
|
|
618
622
|
let reportText;
|
|
@@ -621,7 +625,7 @@ try {
|
|
|
621
625
|
let handCard = null;
|
|
622
626
|
if (opts.hand) {
|
|
623
627
|
const handUsage =
|
|
624
|
-
'usage: memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "
|
|
628
|
+
'usage: memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "<R<n> or <card-id>/R<n>>=<real|marginal|false>[: <reason>]"]... [--harmful <id>]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
625
629
|
const [id] = rest;
|
|
626
630
|
if (!id || !opts.commit) fail(handUsage, 2);
|
|
627
631
|
handCard = id;
|
|
@@ -632,7 +636,17 @@ try {
|
|
|
632
636
|
if (!Number.isInteger(rounds) || rounds < 1) fail(`${handUsage}\n--rounds must be a positive integer, got ${JSON.stringify(opts.rounds)}.`, 2);
|
|
633
637
|
}
|
|
634
638
|
try {
|
|
635
|
-
|
|
639
|
+
const byKey = new Map();
|
|
640
|
+
for (const c of parseAdversaryClasses(opts.review || [])) {
|
|
641
|
+
let key = c.id;
|
|
642
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
643
|
+
if (/^R\d+$/.test(key)) key = `${id}/${key}`;
|
|
644
|
+
else if (!new RegExp(`^${escaped}/R\\d+$`).test(key)) throw new Error(`--review key ${JSON.stringify(c.id)} must be R<n> or ${id}/R<n>`);
|
|
645
|
+
const prev = byKey.get(key);
|
|
646
|
+
if (prev && prev.class !== c.class) throw new Error(`--review names ${key} twice with different classes (${prev.class}, ${c.class})`);
|
|
647
|
+
if (!prev) byKey.set(key, { ...c, id: key });
|
|
648
|
+
}
|
|
649
|
+
classes = [...byKey.values()];
|
|
636
650
|
} catch (err) {
|
|
637
651
|
fail(`${handUsage}\n${err.message}`, 2);
|
|
638
652
|
}
|
|
@@ -695,7 +709,7 @@ try {
|
|
|
695
709
|
classes = parseAdversaryClasses(opts.adversary || []);
|
|
696
710
|
}
|
|
697
711
|
const reportHash = createHash("sha256").update(reportText).digest("hex");
|
|
698
|
-
const plan =
|
|
712
|
+
const plan = planFor(dir, opts.card);
|
|
699
713
|
const models = loadModels(dir);
|
|
700
714
|
const model = textOrNull(opts.model) || (models.tiers.cheap && models.tiers.cheap.model) || "haiku";
|
|
701
715
|
let maxUsd = 0.5;
|
|
@@ -724,6 +738,7 @@ try {
|
|
|
724
738
|
model,
|
|
725
739
|
claudeDir,
|
|
726
740
|
dryRun,
|
|
741
|
+
harmful: opts.harmful || [],
|
|
727
742
|
propose,
|
|
728
743
|
});
|
|
729
744
|
if (!dryRun && result.ok && result.appended && result.appended.length) {
|