@dougharness/cli 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/board-serve.js +40 -6
- package/dist/board-serve.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 +70 -46
- 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/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 +91 -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 +15 -5
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +144 -3
- package/node_modules/@dougharness/flow/lib/land.mjs +27 -8
- package/node_modules/@dougharness/flow/lib/memory.mjs +48 -12
- package/node_modules/@dougharness/flow/lib/plan.mjs +32 -4
- 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 +107 -32
- package/node_modules/@dougharness/flow/scripts/cost.mjs +16 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +20 -9
- package/node_modules/@dougharness/flow/scripts/plan.mjs +100 -30
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +8 -2
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +30 -10
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +80 -0
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +6 -6
- package/node_modules/@dougharness/flow/workflows/card-queen.js +807 -0
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +7 -7
- 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 +196 -6
- package/node_modules/@dougharness/gates/lib/gate-lock.mjs +40 -8
- 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 +138 -10
- package/node_modules/@dougharness/gates/scripts/verify.mjs +8 -4
- package/package.json +3 -3
- package/templates/board-app.js +46 -0
|
@@ -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] --summary "<one sentence>" [--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] --summary "<one sentence>" [--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> --summary "<one sentence>" [--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, handWallVerdict, 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: ["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 = ["summary", "hand", "commit", "wall", "gate", "note", "rehearsal"];
|
|
72
|
-
const RECORD_REPORT_FLAGS = ["summary", "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> --summary \"<one sentence>\" [--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] --summary \"<one sentence>\" [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]";
|
|
107
|
-
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>] [--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.
|
|
@@ -134,11 +138,56 @@ if (cmd === "record" || Object.prototype.hasOwnProperty.call(SUBCOMMAND_FLAGS, c
|
|
|
134
138
|
let summarySentence = null;
|
|
135
139
|
if (cmd === "record" || cmd === "summary") {
|
|
136
140
|
const text = typeof opts.summary === "string" ? opts.summary.trim() : "";
|
|
137
|
-
if (!text) fail(`--summary "<one sentence>" is required for board.mjs ${cmd}: one sentence, in plain words, of what changed for the user`, 2);
|
|
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);
|
|
138
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);
|
|
139
143
|
summarySentence = text;
|
|
140
144
|
}
|
|
141
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
|
+
|
|
142
191
|
try {
|
|
143
192
|
switch (cmd) {
|
|
144
193
|
case "next": {
|
|
@@ -155,12 +204,14 @@ try {
|
|
|
155
204
|
if (capLine) process.stderr.write(capLine);
|
|
156
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.");
|
|
157
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`);
|
|
158
208
|
process.stdout.write(JSON.stringify(batch === null ? card : cards, null, 2) + "\n");
|
|
159
209
|
break;
|
|
160
210
|
}
|
|
161
211
|
case "budgets": {
|
|
212
|
+
const usd = opts.usd === true;
|
|
162
213
|
if (opts.from === undefined) {
|
|
163
|
-
process.stdout.write(`table: ${JSON.stringify(WALL_BUDGET_MIN)}\n`);
|
|
214
|
+
process.stdout.write(`table: ${JSON.stringify(usd ? USD_BUDGET : WALL_BUDGET_MIN)}\n`);
|
|
164
215
|
break;
|
|
165
216
|
}
|
|
166
217
|
const dir = dirFrom(rest, 0);
|
|
@@ -170,12 +221,15 @@ try {
|
|
|
170
221
|
} catch (e) {
|
|
171
222
|
fail(`cannot read --from file ${opts.from}: ${e.message}`, 2);
|
|
172
223
|
}
|
|
173
|
-
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;
|
|
174
228
|
const out = [];
|
|
175
229
|
for (const size of ["S", "M", "L"]) {
|
|
176
230
|
for (const c of cells.filter((x) => x.size === size)) {
|
|
177
|
-
if (c.cls === "all") out.push(c.n === 0 ? `${size}/all n=0 (no landings; keeps the shipped ${
|
|
178
|
-
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)})` : ""}`);
|
|
179
233
|
}
|
|
180
234
|
}
|
|
181
235
|
out.push(`table: ${JSON.stringify(table)}`);
|
|
@@ -194,6 +248,11 @@ try {
|
|
|
194
248
|
const moved = moveCard(loadBoard(dir), id, column, { source: opts.source });
|
|
195
249
|
saveBoard(dir, moved);
|
|
196
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
|
+
}
|
|
197
256
|
const capLine = column === "ready" ? readyCapWarning(moved) : null;
|
|
198
257
|
if (capLine) process.stderr.write(capLine);
|
|
199
258
|
break;
|
|
@@ -266,10 +325,12 @@ try {
|
|
|
266
325
|
case "record": {
|
|
267
326
|
if (opts.hand) {
|
|
268
327
|
const [id] = rest;
|
|
269
|
-
if (!id) fail("usage: board.mjs record <id> --hand [dir] --summary \"<one sentence>\" [--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);
|
|
270
330
|
const dir = dirFrom(rest, 1);
|
|
271
331
|
const card = findCard(loadBoard(dir), id);
|
|
272
|
-
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 });
|
|
273
334
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
274
335
|
process.stdout.write(`${summarySentence}\n`);
|
|
275
336
|
if (!opts.rehearsal) {
|
|
@@ -278,39 +339,53 @@ try {
|
|
|
278
339
|
}
|
|
279
340
|
if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
|
|
280
341
|
process.stdout.write(`Appended a hand-track entry for ${id} to ${file}.\n`);
|
|
342
|
+
writeLandingBlock(dir, id, block, false, !!opts.rehearsal);
|
|
281
343
|
break;
|
|
282
344
|
}
|
|
283
345
|
const [id, reportFile] = rest;
|
|
284
|
-
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>] [--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);
|
|
285
347
|
const dir = dirFrom(rest, 2);
|
|
286
348
|
const card = findCard(loadBoard(dir), id);
|
|
287
349
|
const report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
|
|
288
350
|
// A batch plan (its `cards` list this card) makes the entry that card's part of the run.
|
|
289
|
-
const plan =
|
|
351
|
+
const plan = planFor(dir, id);
|
|
290
352
|
const batch = plan && Array.isArray(plan.cards) && plan.cards.includes(id) ? plan.cards : null;
|
|
291
353
|
const taskIds = batch ? plan.tasks.filter((t) => t.card === id).map((t) => t.id) : [];
|
|
292
354
|
const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: dir, wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
|
|
293
|
-
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 });
|
|
294
357
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
295
358
|
process.stdout.write(`${summarySentence}\n`);
|
|
296
359
|
if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
|
|
297
360
|
process.stdout.write(`Appended a run entry for ${id} to ${file}.\n`);
|
|
361
|
+
writeLandingBlock(dir, id, block, false, !!opts.rehearsal);
|
|
298
362
|
break;
|
|
299
363
|
}
|
|
300
364
|
case "summary": {
|
|
301
365
|
const [reportFile] = rest;
|
|
302
|
-
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>]", 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);
|
|
303
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
|
|
304
369
|
if (opts.card !== undefined) {
|
|
305
370
|
// With the plan at hand the filter also covers a report whose tasks carry no card.
|
|
306
|
-
const plan =
|
|
371
|
+
const plan = planFor(dirFrom(rest, 1), opts.card);
|
|
307
372
|
report = filterReportForCard(report, opts.card, plan && Array.isArray(plan.tasks) ? plan.tasks.filter((t) => t.card === opts.card).map((t) => t.id) : []);
|
|
308
373
|
}
|
|
309
374
|
const wall = opts.run ? wallForRun({ runId: opts.run, claudeDir: opts["claude-dir"], projectDir: process.cwd(), wallMinutes: report.budget?.wallMinutes ?? DEFAULT_BUDGET.wallMinutes }) : null;
|
|
310
|
-
const sumPlan = opts.card !== undefined ?
|
|
375
|
+
const sumPlan = opts.card !== undefined ? planFor(dirFrom(rest, 1), opts.card) : null;
|
|
311
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;
|
|
312
386
|
process.stdout.write(`${summarySentence}\n`);
|
|
313
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);
|
|
314
389
|
break;
|
|
315
390
|
}
|
|
316
391
|
default:
|
|
@@ -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;
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
// prints what would happen. Cost is measured from the same local
|
|
99
99
|
// transcripts cost.mjs reads, falling back to the CLI's own
|
|
100
100
|
// cost estimate. Needs Node >=22.16 (node:sqlite).
|
|
101
|
-
// memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "
|
|
101
|
+
// memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "<R<n> or <card-id>/R<n>>=<class>[: <reason>]"]...
|
|
102
102
|
// [--harmful <id>]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
103
103
|
// (card memory-every-session) the same pass for a hand landing:
|
|
104
104
|
// assembles .doug/.state/reports/<card-id>/hand-report.json from
|
|
@@ -190,7 +190,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
190
190
|
import { createHash } from "node:crypto";
|
|
191
191
|
import { spawnSync } from "node:child_process";
|
|
192
192
|
import { parseAdversaryClasses, loadBoard, findCard } from "../lib/board.mjs";
|
|
193
|
-
import {
|
|
193
|
+
import { planFor, unwrapReport } from "../lib/plan.mjs";
|
|
194
194
|
import { costRun } from "../lib/cost.mjs";
|
|
195
195
|
import { loadModels } from "../lib/models.mjs";
|
|
196
196
|
import {
|
|
@@ -508,7 +508,7 @@ try {
|
|
|
508
508
|
case "record": {
|
|
509
509
|
if (opts.hand) {
|
|
510
510
|
const handUsage =
|
|
511
|
-
'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]';
|
|
512
512
|
const [id, ...extra] = rest;
|
|
513
513
|
if (!id || !opts.commit) fail(handUsage, 2);
|
|
514
514
|
if (id.endsWith(".json") || existsSync(resolve(id))) {
|
|
@@ -517,6 +517,7 @@ try {
|
|
|
517
517
|
if (opts.run !== undefined || opts.adversary !== undefined || opts["claude-dir"] !== undefined || extra.length > 1) {
|
|
518
518
|
fail(handUsage, 2);
|
|
519
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);
|
|
520
521
|
const dir = dirFrom(rest, 1);
|
|
521
522
|
// condition.json (card outcomes-condition-columns): read when `condition open` wrote one before the
|
|
522
523
|
// brief; a stamp missing entirely (the common case until core-next-hand-report wires this in) is not a
|
|
@@ -531,7 +532,7 @@ try {
|
|
|
531
532
|
}
|
|
532
533
|
}
|
|
533
534
|
const { openMemory, recordOutcomes, handOutcomeRow } = await import("../lib/memory.mjs");
|
|
534
|
-
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 });
|
|
535
536
|
const m = openMemory(dir);
|
|
536
537
|
let result;
|
|
537
538
|
try {
|
|
@@ -580,7 +581,7 @@ try {
|
|
|
580
581
|
}
|
|
581
582
|
const reportHash = createHash("sha256").update(reportText).digest("hex");
|
|
582
583
|
const classes = parseAdversaryClasses(opts.adversary || []);
|
|
583
|
-
const plan =
|
|
584
|
+
const plan = planFor(dir, opts.card);
|
|
584
585
|
const run = opts.run || null;
|
|
585
586
|
const runLabel = run || `report:${reportHash.slice(0, 12)}`;
|
|
586
587
|
let cost = null;
|
|
@@ -624,7 +625,7 @@ try {
|
|
|
624
625
|
let handCard = null;
|
|
625
626
|
if (opts.hand) {
|
|
626
627
|
const handUsage =
|
|
627
|
-
'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]';
|
|
628
629
|
const [id] = rest;
|
|
629
630
|
if (!id || !opts.commit) fail(handUsage, 2);
|
|
630
631
|
handCard = id;
|
|
@@ -635,7 +636,17 @@ try {
|
|
|
635
636
|
if (!Number.isInteger(rounds) || rounds < 1) fail(`${handUsage}\n--rounds must be a positive integer, got ${JSON.stringify(opts.rounds)}.`, 2);
|
|
636
637
|
}
|
|
637
638
|
try {
|
|
638
|
-
|
|
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()];
|
|
639
650
|
} catch (err) {
|
|
640
651
|
fail(`${handUsage}\n${err.message}`, 2);
|
|
641
652
|
}
|
|
@@ -698,7 +709,7 @@ try {
|
|
|
698
709
|
classes = parseAdversaryClasses(opts.adversary || []);
|
|
699
710
|
}
|
|
700
711
|
const reportHash = createHash("sha256").update(reportText).digest("hex");
|
|
701
|
-
const plan =
|
|
712
|
+
const plan = planFor(dir, opts.card);
|
|
702
713
|
const models = loadModels(dir);
|
|
703
714
|
const model = textOrNull(opts.model) || (models.tiers.cheap && models.tiers.cheap.model) || "haiku";
|
|
704
715
|
let maxUsd = 0.5;
|