@dougharness/cli 1.0.1 → 1.1.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 +8 -0
- package/dist/bin.js.map +1 -1
- package/dist/board-serve.js +9 -3
- package/dist/board-serve.js.map +1 -1
- package/dist/board.js +10 -2
- package/dist/board.js.map +1 -1
- package/dist/detect/repo.js +14 -1
- package/dist/detect/repo.js.map +1 -1
- package/dist/detect/types.d.ts +2 -0
- package/dist/doctor.d.ts +46 -0
- package/dist/doctor.js +491 -0
- package/dist/doctor.js.map +1 -0
- package/dist/generate/agents.d.ts +7 -0
- package/dist/generate/agents.js +62 -12
- package/dist/generate/agents.js.map +1 -1
- package/dist/generate/claude-md.js +4 -0
- package/dist/generate/claude-md.js.map +1 -1
- package/dist/generate/config.d.ts +8 -1
- package/dist/generate/config.js +5 -12
- package/dist/generate/config.js.map +1 -1
- package/dist/generate/proposal.js +24 -6
- package/dist/generate/proposal.js.map +1 -1
- package/dist/generate/settings.d.ts +2 -0
- package/dist/generate/settings.js +17 -2
- package/dist/generate/settings.js.map +1 -1
- package/dist/init.d.ts +14 -0
- package/dist/init.js +94 -8
- package/dist/init.js.map +1 -1
- package/dist/memory-setup.d.ts +28 -0
- package/dist/memory-setup.js +323 -0
- package/dist/memory-setup.js.map +1 -0
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +16 -4
- package/node_modules/@dougharness/flow/agents/planner.md +2 -2
- package/node_modules/@dougharness/flow/agents/researcher.md +2 -2
- package/node_modules/@dougharness/flow/hooks/hooks.json +16 -0
- package/node_modules/@dougharness/flow/lib/board.mjs +154 -12
- package/node_modules/@dougharness/flow/lib/code-index.mjs +18 -3
- package/node_modules/@dougharness/flow/lib/context.mjs +57 -0
- package/node_modules/@dougharness/flow/lib/cost.mjs +129 -4
- package/node_modules/@dougharness/flow/lib/data/pairs.json +103 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +2 -2
- package/node_modules/@dougharness/flow/lib/embeddings.mjs +189 -27
- package/node_modules/@dougharness/flow/lib/land.mjs +69 -10
- package/node_modules/@dougharness/flow/lib/learn.mjs +61 -4
- package/node_modules/@dougharness/flow/lib/memory-import.mjs +22 -4
- package/node_modules/@dougharness/flow/lib/memory.mjs +326 -35
- package/node_modules/@dougharness/flow/lib/plan.mjs +9 -0
- package/node_modules/@dougharness/flow/package.json +2 -1
- package/node_modules/@dougharness/flow/scripts/board.mjs +60 -20
- package/node_modules/@dougharness/flow/scripts/context.mjs +48 -0
- package/node_modules/@dougharness/flow/scripts/cost.mjs +15 -3
- package/node_modules/@dougharness/flow/scripts/memory.mjs +196 -39
- package/node_modules/@dougharness/flow/scripts/plan.mjs +32 -64
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +132 -0
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +105 -0
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +44 -10
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +5 -5
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +4 -2
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +10 -11
- package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +119 -72
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/hooks/hooks.json +11 -0
- package/node_modules/@dougharness/gates/lib/baseline.mjs +166 -1
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +58 -1
- package/node_modules/@dougharness/gates/lib/checkpoint.mjs +1 -1
- package/node_modules/@dougharness/gates/lib/config.mjs +20 -0
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +95 -0
- package/node_modules/@dougharness/gates/lib/evidence.mjs +2 -0
- package/node_modules/@dougharness/gates/lib/run.mjs +4 -0
- package/node_modules/@dougharness/gates/lib/state.mjs +5 -0
- package/node_modules/@dougharness/gates/lib/test-totals.mjs +139 -0
- package/node_modules/@dougharness/gates/lib/trace.mjs +24 -7
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +225 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/research-cap.mjs +133 -5
- package/node_modules/@dougharness/gates/scripts/session-start-baseline.mjs +14 -0
- package/node_modules/@dougharness/gates/scripts/statusline.mjs +33 -7
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +794 -67
- package/node_modules/@dougharness/gates/scripts/trace.mjs +5 -1
- package/node_modules/@dougharness/gates/scripts/turn-start.mjs +17 -0
- package/node_modules/@dougharness/gates/scripts/verify.mjs +146 -0
- package/package.json +3 -3
- package/templates/board-app.js +2 -2
- package/node_modules/@dougharness/flow/lib/rehearse.mjs +0 -825
- package/node_modules/@dougharness/flow/scripts/rehearse.mjs +0 -129
- package/node_modules/@dougharness/flow/skills/core-next/SKILL.md +0 -92
- package/node_modules/@dougharness/flow/skills/harness-fix/SKILL.md +0 -85
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// CLI for the development board, used by the /doug-next and
|
|
3
|
-
// board.mjs next [dir] [--track flow|hand] [--tag <tag>]
|
|
2
|
+
// CLI for the development board, used by the /doug-next and doug-hand skills. Deterministic; no agents.
|
|
3
|
+
// board.mjs next [dir] [--track flow|hand|any] [--tag <tag>]
|
|
4
4
|
// the first Ready card on that track (default flow) whose deps are
|
|
5
5
|
// Done, as JSON (exit 1 if none); --tag filters to cards carrying it
|
|
6
|
-
// board.mjs next --batch <n> [dir] [--track flow|hand] [--tag <tag>]
|
|
6
|
+
// board.mjs next --batch <n> [dir] [--track flow|hand|any] [--tag <tag>]
|
|
7
7
|
// the first n runnable Ready cards, in board order, as a JSON array
|
|
8
|
-
// (exit 1 if none): the cards /doug-next plans together
|
|
8
|
+
// (exit 1 if none): the cards /doug-next plans together; stderr also
|
|
9
|
+
// carries a `wall cap: <m> min` line (the cards' budgets from the size x class table)
|
|
10
|
+
// board.mjs budgets [dir] [--from <live-runs file>]
|
|
11
|
+
// without --from, the shipped wall budget table as `table: <JSON>`;
|
|
12
|
+
// with --from, recomputes it (p80 per size/class cell) from the file's
|
|
13
|
+
// 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)
|
|
9
15
|
// board.mjs card <id> [dir] one card as JSON
|
|
10
16
|
// board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom
|
|
11
17
|
// reorder a card within its column
|
|
@@ -21,7 +27,7 @@
|
|
|
21
27
|
// flow, or named by .doug/plan.json (each with --force removes it
|
|
22
28
|
// and prints a warning to stderr); prints the removed card as JSON
|
|
23
29
|
// to stdout so a mistaken removal can be re-added by hand
|
|
24
|
-
// 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>]
|
|
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>]
|
|
25
31
|
// append a run entry for the card to docs/live-runs.md; every adversary block
|
|
26
32
|
// in the report is classified real, marginal, or false (repeat the flag per block).
|
|
27
33
|
// When .doug/plan.json lists the card among its `cards` (a batch), the entry
|
|
@@ -34,13 +40,14 @@
|
|
|
34
40
|
// Either record form also promotes .doug/.state/research/<id>.md to docs/research/<id>.md when that note
|
|
35
41
|
// exists and differs from the docs copy, copying it and printing one line saying so; a card with no note, or
|
|
36
42
|
// whose docs copy is already byte-identical, is untouched, silently.
|
|
37
|
-
// board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>]
|
|
43
|
+
// board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>] [--run <run-id>] [--claude-dir <dir>]
|
|
38
44
|
// the three-line chat summary of a report (--card: that card's tasks only)
|
|
39
45
|
|
|
40
46
|
import { readFileSync } from "node:fs";
|
|
41
47
|
import { relative, resolve } from "node:path";
|
|
42
|
-
import { loadBoard, saveBoard, findCard, nextReadyCard, nextReadyCards, moveCard, editCard, removeCard, reorderCard, runEntry, handEntry, runSummary, boardPath, parseAdversaryClasses, filterReportForCard, recordLanding } from "../lib/board.mjs";
|
|
43
|
-
import { loadPlan, unwrapReport } from "../lib/plan.mjs";
|
|
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 { wallForRun } from "../lib/cost.mjs";
|
|
44
51
|
|
|
45
52
|
const EDIT_FIELD_ORDER = ["title", "goal", "size", "component", "track", "class", "tags", "deps", "source"];
|
|
46
53
|
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]";
|
|
@@ -58,10 +65,11 @@ const SUBCOMMAND_FLAGS = {
|
|
|
58
65
|
edit: ["title", "goal", "goal-file", "size", "component", "track", "class", "deps", "tag", "source", "force"],
|
|
59
66
|
remove: ["force"],
|
|
60
67
|
reorder: ["before", "after", "top", "bottom"],
|
|
61
|
-
|
|
68
|
+
budgets: ["from"],
|
|
69
|
+
summary: ["cost", "codex-cost", "wall", "commit", "card", "run", "claude-dir"],
|
|
62
70
|
};
|
|
63
71
|
const RECORD_HAND_FLAGS = ["hand", "commit", "wall", "gate", "note", "rehearsal"];
|
|
64
|
-
const RECORD_REPORT_FLAGS = ["cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal"];
|
|
72
|
+
const RECORD_REPORT_FLAGS = ["cost", "codex-cost", "wall", "commit", "adversary", "shared-cost", "note", "rehearsal", "run", "claude-dir"];
|
|
65
73
|
const opts = {};
|
|
66
74
|
const positional = [];
|
|
67
75
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -86,16 +94,17 @@ function fail(msg, code = 1) {
|
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
const USAGE_FOR = {
|
|
89
|
-
next: "usage: board.mjs next [dir] [--track flow|hand] [--batch <n>] [--tag <tag>]",
|
|
97
|
+
next: "usage: board.mjs next [dir] [--track flow|hand|any] [--batch <n>] [--tag <tag>]",
|
|
90
98
|
card: "usage: board.mjs card <id> [dir]",
|
|
91
99
|
move: "usage: board.mjs move <id> <column> [dir] [--source <text>]",
|
|
92
100
|
edit: EDIT_USAGE,
|
|
93
101
|
remove: "usage: board.mjs remove <id> [dir] [--force]",
|
|
94
102
|
reorder: "usage: board.mjs reorder <id> [dir] --before <other> | --after <other> | --top | --bottom",
|
|
95
|
-
|
|
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>]",
|
|
96
105
|
};
|
|
97
106
|
const RECORD_HAND_USAGE = "usage: board.mjs record <id> --hand [dir] [--commit <sha>] [--wall <text>] [--gate <text>] [--note <text>] [--rehearsal <scenario>]";
|
|
98
|
-
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>]";
|
|
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>]";
|
|
99
108
|
|
|
100
109
|
// An unknown --flag for the given subcommand: exit 2, name the flag and the subcommand's usage.
|
|
101
110
|
// Runs on flag names only (parse order doesn't matter), before any work or write.
|
|
@@ -123,18 +132,45 @@ try {
|
|
|
123
132
|
switch (cmd) {
|
|
124
133
|
case "next": {
|
|
125
134
|
const dir = dirFrom(rest, 0);
|
|
126
|
-
const track = opts.track === "hand" ? "hand" : "flow";
|
|
127
|
-
if (opts.track !== undefined && opts.track !== "flow" && opts.track !== "hand") fail(`unknown track "${opts.track}"; tracks are flow, hand`, 2);
|
|
135
|
+
const track = opts.track === "hand" ? "hand" : opts.track === "any" ? "any" : "flow";
|
|
136
|
+
if (opts.track !== undefined && opts.track !== "flow" && opts.track !== "hand" && opts.track !== "any") fail(`unknown track "${opts.track}"; tracks are flow, hand, any`, 2);
|
|
128
137
|
const batch = opts.batch !== undefined ? Number(opts.batch) : null;
|
|
129
138
|
if (batch !== null && !(Number.isInteger(batch) && batch > 0)) fail(`--batch must be a positive integer, got ${JSON.stringify(opts.batch)}`, 2);
|
|
130
139
|
if (opts.tag !== undefined && opts.tag.includes(",")) fail(`--tag takes one tag here; got "${opts.tag}"`, 2);
|
|
131
140
|
const board = loadBoard(dir);
|
|
132
141
|
const { card, cards, skipped } = batch === null ? nextReadyCard(board, { track, tag: opts.tag }) : nextReadyCards(board, { track, batch, tag: opts.tag });
|
|
133
|
-
for (const s of skipped) process.stderr.write(s.hand ? `skipping ${s.id}: hand track (
|
|
142
|
+
for (const s of skipped) process.stderr.write(s.hand ? `skipping ${s.id}: hand track (a by-hand card; /doug-next ${s.id} takes it)\n` : s.flow ? `skipping ${s.id}: flow track (run it with /doug-next)\n` : `skipping ${s.id}: waiting on ${s.waitingOn.join(", ")}\n`);
|
|
143
|
+
const capLine = readyCapWarning(board);
|
|
144
|
+
if (capLine) process.stderr.write(capLine);
|
|
134
145
|
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
|
+
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`);
|
|
135
147
|
process.stdout.write(JSON.stringify(batch === null ? card : cards, null, 2) + "\n");
|
|
136
148
|
break;
|
|
137
149
|
}
|
|
150
|
+
case "budgets": {
|
|
151
|
+
if (opts.from === undefined) {
|
|
152
|
+
process.stdout.write(`table: ${JSON.stringify(WALL_BUDGET_MIN)}\n`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
const dir = dirFrom(rest, 0);
|
|
156
|
+
let text;
|
|
157
|
+
try {
|
|
158
|
+
text = readFileSync(resolve(opts.from), "utf8");
|
|
159
|
+
} catch (e) {
|
|
160
|
+
fail(`cannot read --from file ${opts.from}: ${e.message}`, 2);
|
|
161
|
+
}
|
|
162
|
+
const { table, cells } = wallBudgetTable(parseLiveRunWalls(text), loadBoard(dir));
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const size of ["S", "M", "L"]) {
|
|
165
|
+
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 ${WALL_BUDGET_MIN[size].all})` : `${size}/all n=${c.n} p80 ${c.p80}`);
|
|
167
|
+
else out.push(`${size}/${c.cls} n=${c.n} p80 ${c.p80}${c.n < WALL_BUDGET_MIN_N ? ` (fewer than ${WALL_BUDGET_MIN_N}; falls back to ${size}/all ${table[size].all})` : ""}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
out.push(`table: ${JSON.stringify(table)}`);
|
|
171
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
138
174
|
case "card": {
|
|
139
175
|
const dir = dirFrom(rest, 1);
|
|
140
176
|
process.stdout.write(JSON.stringify(findCard(loadBoard(dir), rest[0]), null, 2) + "\n");
|
|
@@ -226,7 +262,7 @@ try {
|
|
|
226
262
|
break;
|
|
227
263
|
}
|
|
228
264
|
const [id, reportFile] = rest;
|
|
229
|
-
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>]", 2);
|
|
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);
|
|
230
266
|
const dir = dirFrom(rest, 2);
|
|
231
267
|
const card = findCard(loadBoard(dir), id);
|
|
232
268
|
const report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
|
|
@@ -234,7 +270,8 @@ try {
|
|
|
234
270
|
const plan = loadPlan(dir);
|
|
235
271
|
const batch = plan && Array.isArray(plan.cards) && plan.cards.includes(id) ? plan.cards : null;
|
|
236
272
|
const taskIds = batch ? plan.tasks.filter((t) => t.card === id).map((t) => t.id) : [];
|
|
237
|
-
const
|
|
273
|
+
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 });
|
|
238
275
|
const { file, promoted } = recordLanding(dir, id, entry);
|
|
239
276
|
if (promoted) process.stdout.write(`Promoted the research note for ${id} to ${promoted}.\n`);
|
|
240
277
|
process.stdout.write(`Appended a run entry for ${id} to ${file}.\n`);
|
|
@@ -242,14 +279,17 @@ try {
|
|
|
242
279
|
}
|
|
243
280
|
case "summary": {
|
|
244
281
|
const [reportFile] = rest;
|
|
245
|
-
if (!reportFile) fail("usage: board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>]", 2);
|
|
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);
|
|
246
283
|
let report = unwrapReport(JSON.parse(readFileSync(resolve(reportFile), "utf8")));
|
|
247
284
|
if (opts.card !== undefined) {
|
|
248
285
|
// With the plan at hand the filter also covers a report whose tasks carry no card.
|
|
249
286
|
const plan = loadPlan(dirFrom(rest, 1));
|
|
250
287
|
report = filterReportForCard(report, opts.card, plan && Array.isArray(plan.tasks) ? plan.tasks.filter((t) => t.card === opts.card).map((t) => t.id) : []);
|
|
251
288
|
}
|
|
252
|
-
|
|
289
|
+
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 ? loadPlan(dirFrom(rest, 1)) : null;
|
|
291
|
+
const sumBatch = sumPlan && Array.isArray(sumPlan.cards) && sumPlan.cards.includes(opts.card) ? sumPlan.cards : null;
|
|
292
|
+
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 }));
|
|
253
293
|
break;
|
|
254
294
|
}
|
|
255
295
|
default:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The lead's own context percentage (card autopilot-unlimited), read from the session transcript.
|
|
3
|
+
// context.mjs [dir] [--session <id|latest>] [--claude-dir <path>]
|
|
4
|
+
// dir the project directory (default cwd); <dir>/.doug/config.json supplies autopilot.contextThreshold
|
|
5
|
+
// --session a session id or latest (default latest), as cost.mjs --session
|
|
6
|
+
// --claude-dir where ~/.claude lives (tests point it at a fixture)
|
|
7
|
+
// Prints one line and exits 0; exits 1 with a stderr message when there is no transcript or no usage line.
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { resolve, join } from "node:path";
|
|
10
|
+
import { sessionTranscriptPath } from "../lib/cost.mjs";
|
|
11
|
+
import { atThreshold, contextReading, contextThreshold } from "../lib/context.mjs";
|
|
12
|
+
|
|
13
|
+
const argv = process.argv.slice(2);
|
|
14
|
+
const opts = {};
|
|
15
|
+
const positional = [];
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
if (!argv[i].startsWith("--")) {
|
|
18
|
+
positional.push(argv[i]);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
opts[argv[i].slice(2)] = argv[i + 1] ?? "";
|
|
22
|
+
i++;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const dir = resolve(positional[0] || process.cwd());
|
|
26
|
+
let config = {};
|
|
27
|
+
try {
|
|
28
|
+
config = JSON.parse(readFileSync(join(dir, ".doug/config.json"), "utf8"));
|
|
29
|
+
} catch {
|
|
30
|
+
// absent or unreadable: defaults
|
|
31
|
+
}
|
|
32
|
+
const threshold = contextThreshold(config, (m) => process.stderr.write(`[doug] ${m}\n`));
|
|
33
|
+
|
|
34
|
+
let file;
|
|
35
|
+
try {
|
|
36
|
+
file = sessionTranscriptPath({ session: opts.session || "latest", ...(opts["claude-dir"] ? { claudeDir: resolve(opts["claude-dir"]) } : {}), projectDir: dir });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
process.stderr.write(`context: ${err.message}\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const r = contextReading(file);
|
|
42
|
+
if (!r) {
|
|
43
|
+
process.stderr.write(`context: no usage line in ${file}\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
process.stdout.write(
|
|
47
|
+
`context: ${r.pct}% (${r.tokens} of ${r.window} tokens, ${r.model}); threshold ${threshold}% (autopilot.contextThreshold) -> ${atThreshold(r.pct, threshold) ? "stop" : "continue"}\n`,
|
|
48
|
+
);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Real cost of one doug-implement run from the local Claude Code transcripts; no network, nothing estimated.
|
|
3
|
+
// cost.mjs --session <id|latest> [dir] [--json] [--claude-dir <path>]
|
|
4
|
+
// --session price one Claude Code session: its transcript plus every subagent transcript under it
|
|
5
|
+
// (latest = the most recently modified one in the project); exit 1 when none is found
|
|
3
6
|
// cost.mjs <run-id> [dir] [--json] [--claude-dir <path>]
|
|
4
7
|
// <run-id> the Workflow run id (wf_...)
|
|
5
8
|
// [dir] the project directory whose ~/.claude/projects slug is searched first (default cwd)
|
|
@@ -15,7 +18,7 @@
|
|
|
15
18
|
// with the run total and any unpriced agent. An unreadable/invalid default report is ignored, not a usage error.
|
|
16
19
|
import { readFileSync } from "node:fs";
|
|
17
20
|
import { resolve, join } from "node:path";
|
|
18
|
-
import { costRun, renderCost, costByCard, renderCostByCard } from "../lib/cost.mjs";
|
|
21
|
+
import { costRun, costSession, renderSession, renderCost, costByCard, renderCostByCard } from "../lib/cost.mjs";
|
|
19
22
|
import { loadPlan, REPORT_RELPATH, unwrapReport } from "../lib/plan.mjs";
|
|
20
23
|
|
|
21
24
|
const argv = process.argv.slice(2);
|
|
@@ -35,13 +38,22 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
35
38
|
opts[name] = argv[i + 1] ?? "";
|
|
36
39
|
i++;
|
|
37
40
|
}
|
|
38
|
-
const [runId, dirArg] = positional;
|
|
39
|
-
if (
|
|
41
|
+
const [runId, dirArg] = opts.session !== undefined ? [null, positional[0]] : positional;
|
|
42
|
+
if (opts.session === "") {
|
|
43
|
+
process.stderr.write("usage: cost.mjs --session <id|latest> [dir] [--json] [--claude-dir <path>]\n");
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
if (!runId && opts.session === undefined) {
|
|
40
47
|
process.stderr.write("usage: cost.mjs <run-id> [dir] [--json] [--claude-dir <path>]\n");
|
|
41
48
|
process.exit(2);
|
|
42
49
|
}
|
|
43
50
|
try {
|
|
44
51
|
const projectDir = resolve(process.env.CLAUDE_PROJECT_DIR || dirArg || process.cwd());
|
|
52
|
+
if (opts.session !== undefined) {
|
|
53
|
+
const result = costSession({ session: opts.session, projectDir, claudeDir: opts["claude-dir"] ? resolve(opts["claude-dir"]) : undefined });
|
|
54
|
+
process.stdout.write(opts.json ? JSON.stringify(result, null, 2) + "\n" : renderSession(result));
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
45
57
|
let reportPath = opts.report ? resolve(opts.report) : join(projectDir, REPORT_RELPATH);
|
|
46
58
|
let report = null;
|
|
47
59
|
if (opts.report) {
|
|
@@ -60,14 +60,24 @@
|
|
|
60
60
|
// memory.mjs lesson unstale <id> [dir] [--json]
|
|
61
61
|
// clear a lesson's stale mark (set by recall's citation
|
|
62
62
|
// re-check) so it is a candidate again; refuses on an unknown id
|
|
63
|
+
// memory.mjs brief-lessons <card-id> [dir] [--k <n>] [--json]
|
|
64
|
+
// (card memory-every-session) recall for a hand-track card's
|
|
65
|
+
// briefs: query = the card's title and goal, k 8. Prints the
|
|
66
|
+
// workflow's lessons header, the rendered `- [id] ...` lines
|
|
67
|
+
// (capped at 2000 chars like plan.mjs), then `memoryUsed: <ids>`;
|
|
68
|
+
// writes .doug/.state/reports/<card-id>/lessons.json. No database
|
|
69
|
+
// -> "No lessons recalled for <id>." (database not created);
|
|
70
|
+
// a database or provider error -> one stderr line, exit 0;
|
|
71
|
+
// unknown card -> exit 1
|
|
63
72
|
// memory.mjs recall <query> [dir] [--files <glob,glob>] [--k <n>] [--json]
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
73
|
+
// dense recall (card memory-recall): with a provider and
|
|
74
|
+
// stored vectors, every live lesson is ranked by cosine x
|
|
75
|
+
// recency weight x scope boost (--files), ties by lesson id,
|
|
76
|
+
// then BM25 hits with no vector are appended in BM25 order;
|
|
77
|
+
// without a provider or vectors the ranking is BM25 x recency
|
|
78
|
+
// x scope, a keyword-only fallback that says so. Each returned
|
|
79
|
+
// lesson's citation is re-checked against [dir] only when
|
|
80
|
+
// [dir] is a real project root (contains .doug)
|
|
71
81
|
// memory.mjs reflect <report.json> [dir] [--run <wf-id>] [--card <id>] [--commit <sha>]
|
|
72
82
|
// [--adversary "<id>=<class>[: <reason>]"]... [--claude-bin <path>] [--claude-dir <path>]
|
|
73
83
|
// [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
@@ -82,10 +92,17 @@
|
|
|
82
92
|
// helpful instead of appending); idempotent on the report's
|
|
83
93
|
// sha256 (a second reflect on the same report is a no-op, exit
|
|
84
94
|
// 0); --dry-run applies nothing but still runs the LLM pass and
|
|
85
|
-
// prints what would happen
|
|
86
|
-
// landing has no report. Cost is measured from the same local
|
|
95
|
+
// prints what would happen. Cost is measured from the same local
|
|
87
96
|
// transcripts cost.mjs reads, falling back to the CLI's own
|
|
88
97
|
// cost estimate. Needs Node >=22.16 (node:sqlite).
|
|
98
|
+
// memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "R1=<class>[: <reason>]"]...
|
|
99
|
+
// [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
100
|
+
// (card memory-every-session) the same pass for a hand landing:
|
|
101
|
+
// assembles .doug/.state/reports/<card-id>/hand-report.json from
|
|
102
|
+
// the saved briefs/, reviews/ and lessons.json (memoryUsed = its
|
|
103
|
+
// ids; verified true; passes = --rounds, default 1; one fixed
|
|
104
|
+
// blocker ledger entry per --review id), then reflects on it
|
|
105
|
+
// exactly as above. --commit is required (exit 2). No schema change.
|
|
89
106
|
// memory.mjs doctor [dir] [--json]
|
|
90
107
|
// probe the configured embedding provider (memory.embeddings in
|
|
91
108
|
// .doug/config.json), or a local Ollama at localhost:11434 when
|
|
@@ -128,8 +145,9 @@
|
|
|
128
145
|
// path; keyword-only, and says so, with no provider or no vectors
|
|
129
146
|
// memory.mjs embed-check [dir] [--pairs <file>] [--json]
|
|
130
147
|
// cosine sanity check (card memory-measure): embeds the dozen
|
|
131
|
-
// paraphrase and dozen unrelated lesson pairs in
|
|
132
|
-
// pairs.json (or --pairs) against the
|
|
148
|
+
// paraphrase and dozen unrelated lesson pairs in
|
|
149
|
+
// plugins/doug-flow/lib/data/pairs.json (or --pairs) against the
|
|
150
|
+
// configured provider and
|
|
133
151
|
// confirms paraphrases score higher than unrelated pairs by the
|
|
134
152
|
// file's stated margin; exit 0 ok, 1 on a failed check or an
|
|
135
153
|
// unreachable/unconfigured provider, 2 on usage; refuses when
|
|
@@ -164,7 +182,7 @@
|
|
|
164
182
|
// lesson add and import embed only the row(s) they just wrote, best-effort, when a provider is configured: a
|
|
165
183
|
// provider failure prints one stderr line and never turns into a nonzero exit for that add/import. A pending
|
|
166
184
|
// row from an earlier add is left alone until `reembed` (or a later add/import) picks it up.
|
|
167
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
185
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from "node:fs";
|
|
168
186
|
import { resolve, join, dirname } from "node:path";
|
|
169
187
|
import { createHash } from "node:crypto";
|
|
170
188
|
import { spawnSync } from "node:child_process";
|
|
@@ -186,7 +204,7 @@ import {
|
|
|
186
204
|
|
|
187
205
|
const argv = process.argv.slice(2);
|
|
188
206
|
const FLAGS = new Set(["json", "all", "hand", "dry-run", "check", "no-check"]);
|
|
189
|
-
const REPEATABLE = new Set(["adversary"]);
|
|
207
|
+
const REPEATABLE = new Set(["adversary", "review"]);
|
|
190
208
|
const opts = {};
|
|
191
209
|
const positional = [];
|
|
192
210
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -469,8 +487,8 @@ function printProposalResult(result, jsonOut) {
|
|
|
469
487
|
}
|
|
470
488
|
|
|
471
489
|
// `index build`'s own pairs loader: an explicit --pairs that is missing fails clearly (same as loadPairsOrFail,
|
|
472
|
-
// same exit code), but with no --pairs given at all, a missing default file (
|
|
473
|
-
//
|
|
490
|
+
// same exit code), but with no --pairs given at all, a missing default file (DEFAULT_SANITY_PAIRS lives inside
|
|
491
|
+
// plugins/doug-flow/lib/data, so this only happens if that file was removed) is not a hard failure: build
|
|
474
492
|
// still runs, and buildIndex itself reports a check failure only if a model/dims pair actually new to the code
|
|
475
493
|
// index shows up needing one.
|
|
476
494
|
function loadIndexPairs(opts) {
|
|
@@ -595,26 +613,88 @@ try {
|
|
|
595
613
|
case "reflect": {
|
|
596
614
|
const usage =
|
|
597
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]';
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (!reportFile) fail(usage, 2);
|
|
601
|
-
const dir = dirFrom(rest, 1);
|
|
602
|
-
const reportPath = resolve(reportFile);
|
|
603
|
-
if (!existsSync(reportPath)) fail(`no such report file: ${reportPath}`);
|
|
616
|
+
let dir;
|
|
617
|
+
let reportPath;
|
|
604
618
|
let reportText;
|
|
605
|
-
try {
|
|
606
|
-
reportText = readFileSync(reportPath, "utf8");
|
|
607
|
-
} catch (err) {
|
|
608
|
-
fail(`cannot read report file ${reportPath}: ${err.message}`);
|
|
609
|
-
}
|
|
610
619
|
let report;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
620
|
+
let classes;
|
|
621
|
+
let handCard = null;
|
|
622
|
+
if (opts.hand) {
|
|
623
|
+
const handUsage =
|
|
624
|
+
'usage: memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "R1=<real|marginal|false>[: <reason>]"]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
625
|
+
const [id] = rest;
|
|
626
|
+
if (!id || !opts.commit) fail(handUsage, 2);
|
|
627
|
+
handCard = id;
|
|
628
|
+
dir = dirFrom(rest, 1);
|
|
629
|
+
let rounds = 1;
|
|
630
|
+
if (opts.rounds !== undefined) {
|
|
631
|
+
rounds = Number(opts.rounds);
|
|
632
|
+
if (!Number.isInteger(rounds) || rounds < 1) fail(`${handUsage}\n--rounds must be a positive integer, got ${JSON.stringify(opts.rounds)}.`, 2);
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
classes = parseAdversaryClasses(opts.review || []);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
fail(`${handUsage}\n${err.message}`, 2);
|
|
638
|
+
}
|
|
639
|
+
const base = join(dir, ".doug", ".state", "reports", id);
|
|
640
|
+
const readDirFiles = (sub) => {
|
|
641
|
+
const d = join(base, sub);
|
|
642
|
+
if (!existsSync(d)) return [];
|
|
643
|
+
return readdirSync(d)
|
|
644
|
+
.filter((n) => !n.startsWith("."))
|
|
645
|
+
.sort()
|
|
646
|
+
.map((name) => {
|
|
647
|
+
try {
|
|
648
|
+
return { name, text: readFileSync(join(d, name), "utf8") };
|
|
649
|
+
} catch {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
})
|
|
653
|
+
.filter(Boolean);
|
|
654
|
+
};
|
|
655
|
+
let memoryUsed = [];
|
|
656
|
+
try {
|
|
657
|
+
const saved = JSON.parse(readFileSync(join(base, "lessons.json"), "utf8"));
|
|
658
|
+
if (saved && Array.isArray(saved.ids)) memoryUsed = saved.ids.filter((x) => typeof x === "string");
|
|
659
|
+
} catch {
|
|
660
|
+
memoryUsed = [];
|
|
661
|
+
}
|
|
662
|
+
const ledger = classes.map((c) => {
|
|
663
|
+
return { id: c.id, stage: "adversary", seat: "reviewer", severity: "blocker", status: "fixed", description: c.reason || `reviewer finding ${c.id}` };
|
|
664
|
+
});
|
|
665
|
+
const sortKeys = (v) =>
|
|
666
|
+
Array.isArray(v) ? v.map(sortKeys) : v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeys(v[k])])) : v;
|
|
667
|
+
const handReport = sortKeys({
|
|
668
|
+
track: "hand",
|
|
669
|
+
card: id,
|
|
670
|
+
commit: opts.commit,
|
|
671
|
+
levels: [{ index: 0, tasks: [{ id, card: id, commit: opts.commit, verified: true, passes: rounds, stopReason: null, memoryUsed, ledger }] }],
|
|
672
|
+
hand: { briefs: readDirFiles("briefs"), reviews: readDirFiles("reviews"), note: textOrNull(opts.note) },
|
|
673
|
+
});
|
|
674
|
+
reportText = JSON.stringify(handReport, null, 2) + "\n";
|
|
675
|
+
reportPath = join(base, "hand-report.json");
|
|
676
|
+
mkdirSync(base, { recursive: true });
|
|
677
|
+
writeFileSync(reportPath, reportText, "utf8");
|
|
678
|
+
report = handReport;
|
|
679
|
+
} else {
|
|
680
|
+
const [reportFile] = rest;
|
|
681
|
+
if (!reportFile) fail(usage, 2);
|
|
682
|
+
dir = dirFrom(rest, 1);
|
|
683
|
+
reportPath = resolve(reportFile);
|
|
684
|
+
if (!existsSync(reportPath)) fail(`no such report file: ${reportPath}`);
|
|
685
|
+
try {
|
|
686
|
+
reportText = readFileSync(reportPath, "utf8");
|
|
687
|
+
} catch (err) {
|
|
688
|
+
fail(`cannot read report file ${reportPath}: ${err.message}`);
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
report = unwrapReport(JSON.parse(reportText));
|
|
692
|
+
} catch (err) {
|
|
693
|
+
fail(`cannot parse report file ${reportPath} as JSON: ${err.message}`);
|
|
694
|
+
}
|
|
695
|
+
classes = parseAdversaryClasses(opts.adversary || []);
|
|
615
696
|
}
|
|
616
697
|
const reportHash = createHash("sha256").update(reportText).digest("hex");
|
|
617
|
-
const classes = parseAdversaryClasses(opts.adversary || []);
|
|
618
698
|
const plan = loadPlan(dir);
|
|
619
699
|
const models = loadModels(dir);
|
|
620
700
|
const model = textOrNull(opts.model) || (models.tiers.cheap && models.tiers.cheap.model) || "haiku";
|
|
@@ -639,7 +719,7 @@ try {
|
|
|
639
719
|
classes,
|
|
640
720
|
plan,
|
|
641
721
|
run: opts.run ?? null,
|
|
642
|
-
card: opts.card ?? null,
|
|
722
|
+
card: handCard ?? opts.card ?? null,
|
|
643
723
|
commit: opts.commit ?? null,
|
|
644
724
|
model,
|
|
645
725
|
claudeDir,
|
|
@@ -817,7 +897,7 @@ try {
|
|
|
817
897
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
818
898
|
} else {
|
|
819
899
|
process.stdout.write(
|
|
820
|
-
`Imported ${result.added + result.updated} lesson(s) from ${result.dir}: ${result.added} added, ${result.updated} updated (old row superseded), ${result.unchanged} unchanged, ${result.skipped} skipped (empty).\n`
|
|
900
|
+
`Imported ${result.added + result.updated} lesson(s) from ${result.dir}: ${result.added} added, ${result.updated} updated (old row superseded), ${result.unchanged} unchanged, ${result.skipped} skipped (empty), ${result.retired} retired (file deleted).\n`
|
|
821
901
|
);
|
|
822
902
|
}
|
|
823
903
|
break;
|
|
@@ -842,14 +922,18 @@ try {
|
|
|
842
922
|
}
|
|
843
923
|
const provider = createProvider(readMemoryConfig(dir));
|
|
844
924
|
const staleDays = readMemoryStaleDays(dir);
|
|
845
|
-
const { openMemory, recallLessons } = await import("../lib/memory.mjs");
|
|
925
|
+
const { openMemory, recallLessons, degradedWarning, detectLocalProvider } = await import("../lib/memory.mjs");
|
|
926
|
+
// Probed once, and only when no provider is configured; a failed probe is just "not detected".
|
|
927
|
+
const detectedProvider = provider ? null : await detectLocalProvider();
|
|
846
928
|
const m = openMemory(dir);
|
|
847
929
|
let result;
|
|
848
930
|
try {
|
|
849
|
-
result = await recallLessons(m, query, { files, k, provider, checkoutDir: dir, staleDays });
|
|
931
|
+
result = await recallLessons(m, query, { files, k, provider, checkoutDir: dir, staleDays, detectedProvider });
|
|
850
932
|
} finally {
|
|
851
933
|
m.close();
|
|
852
934
|
}
|
|
935
|
+
const recallWarning = degradedWarning(result, { what: "memory recall", detectedProvider });
|
|
936
|
+
if (recallWarning) process.stderr.write(recallWarning + "\n");
|
|
853
937
|
if (opts.json) {
|
|
854
938
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
855
939
|
} else {
|
|
@@ -862,6 +946,67 @@ try {
|
|
|
862
946
|
}
|
|
863
947
|
break;
|
|
864
948
|
}
|
|
949
|
+
case "brief-lessons": {
|
|
950
|
+
const usage = "usage: memory.mjs brief-lessons <card-id> [dir] [--k <n>] [--json]";
|
|
951
|
+
const [id, ...extra] = rest;
|
|
952
|
+
if (!id) fail(usage, 2);
|
|
953
|
+
const dir = dirFrom(extra, 0);
|
|
954
|
+
let k = 8;
|
|
955
|
+
if (opts.k !== undefined) {
|
|
956
|
+
k = Number(opts.k);
|
|
957
|
+
if (!Number.isInteger(k) || k < 1) fail(`${usage}\n--k must be an integer >= 1, got ${JSON.stringify(opts.k)}.`, 2);
|
|
958
|
+
}
|
|
959
|
+
let card;
|
|
960
|
+
try {
|
|
961
|
+
card = findCard(loadBoard(dir), id);
|
|
962
|
+
} catch (err) {
|
|
963
|
+
fail(`no card ${id}: ${err.message}`);
|
|
964
|
+
}
|
|
965
|
+
const query = `${card.title}\n${card.goal ?? ""}`;
|
|
966
|
+
const header = "Lessons from earlier runs on this repository (memory; each cites where it was learned):";
|
|
967
|
+
const saved = { card: id, query, mode: null, reason: null, ids: [], text: null, recalledAt: new Date().toISOString() };
|
|
968
|
+
let error = null;
|
|
969
|
+
if (existsSync(join(dir, ".doug", ".state", "memory", "memory.db"))) {
|
|
970
|
+
try {
|
|
971
|
+
const provider = createProvider(readMemoryConfig(dir));
|
|
972
|
+
const staleDays = readMemoryStaleDays(dir);
|
|
973
|
+
const { openMemory, recallLessons, degradedWarning, degradedReason, detectLocalProvider, renderLessons } = await import("../lib/memory.mjs");
|
|
974
|
+
const detectedProvider = provider ? null : await detectLocalProvider();
|
|
975
|
+
const m = openMemory(dir);
|
|
976
|
+
try {
|
|
977
|
+
const result = await recallLessons(m, query, { k, checkoutDir: dir, provider, staleDays, detectedProvider });
|
|
978
|
+
const warning = degradedWarning(result, { what: "memory recall", detectedProvider });
|
|
979
|
+
if (warning) process.stderr.write(warning + "\n");
|
|
980
|
+
const rendered = renderLessons(result.lessons);
|
|
981
|
+
saved.mode = result.mode;
|
|
982
|
+
saved.reason = result.degraded ? degradedReason(result, detectedProvider) : null;
|
|
983
|
+
saved.ids = rendered.ids;
|
|
984
|
+
saved.text = rendered.text === null ? null : result.degraded ? `(recall was keyword-only: ${saved.reason})\n${rendered.text}` : rendered.text;
|
|
985
|
+
} finally {
|
|
986
|
+
m.close();
|
|
987
|
+
}
|
|
988
|
+
} catch (err) {
|
|
989
|
+
error = err.message;
|
|
990
|
+
saved.mode = null;
|
|
991
|
+
saved.reason = null;
|
|
992
|
+
saved.ids = [];
|
|
993
|
+
saved.text = null;
|
|
994
|
+
saved.error = error;
|
|
995
|
+
process.stderr.write(`[doug] memory recall skipped: ${error}\n`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const reportDir = join(dir, ".doug", ".state", "reports", id);
|
|
999
|
+
mkdirSync(reportDir, { recursive: true });
|
|
1000
|
+
writeFileSync(join(reportDir, "lessons.json"), JSON.stringify(saved, null, 2) + "\n", "utf8");
|
|
1001
|
+
if (opts.json) {
|
|
1002
|
+
process.stdout.write(JSON.stringify(saved, null, 2) + "\n");
|
|
1003
|
+
} else if (saved.ids.length) {
|
|
1004
|
+
process.stdout.write([header, saved.text, `memoryUsed: ${saved.ids.join(", ")}`].join("\n") + "\n");
|
|
1005
|
+
} else {
|
|
1006
|
+
process.stdout.write(`No lessons recalled for ${id}.\n`);
|
|
1007
|
+
}
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
865
1010
|
case "doctor": {
|
|
866
1011
|
const dir = dirFrom(rest, 0);
|
|
867
1012
|
const cfg = readMemoryConfig(dir);
|
|
@@ -918,10 +1063,12 @@ try {
|
|
|
918
1063
|
if (opts.check && opts["no-check"]) {
|
|
919
1064
|
fail(`${usage}\n--check and --no-check are mutually exclusive.`, 2);
|
|
920
1065
|
}
|
|
921
|
-
const { openMemory, embedLessons } = await import("../lib/memory.mjs");
|
|
1066
|
+
const { openMemory, embedLessons, lessonsNeedingEmbedding } = await import("../lib/memory.mjs");
|
|
922
1067
|
const m = openMemory(dir);
|
|
923
1068
|
let result;
|
|
924
1069
|
let checkStatus;
|
|
1070
|
+
let stillMissing = 0;
|
|
1071
|
+
let liveTotal = 0;
|
|
925
1072
|
try {
|
|
926
1073
|
if (opts["no-check"]) {
|
|
927
1074
|
process.stderr.write("embed-check: skipped (--no-check)\n");
|
|
@@ -943,6 +1090,8 @@ try {
|
|
|
943
1090
|
batch,
|
|
944
1091
|
onProgress: ({ embedded, total }) => process.stderr.write(`embedded ${embedded}/${total}\n`),
|
|
945
1092
|
});
|
|
1093
|
+
stillMissing = lessonsNeedingEmbedding(m, { model: provider.model, dims: provider.dims }).length;
|
|
1094
|
+
liveTotal = m.prepare("SELECT COUNT(*) AS n FROM lessons WHERE superseded_by IS NULL AND stale IS NULL").get().n;
|
|
946
1095
|
} finally {
|
|
947
1096
|
m.close();
|
|
948
1097
|
}
|
|
@@ -953,6 +1102,11 @@ try {
|
|
|
953
1102
|
`Embedded ${result.embedded}/${result.total} lesson(s) with ${provider.model}/${provider.dims} (${result.failed} failed).\n`
|
|
954
1103
|
);
|
|
955
1104
|
}
|
|
1105
|
+
if (stillMissing > 0) {
|
|
1106
|
+
process.stderr.write(
|
|
1107
|
+
`WARNING: ${stillMissing} of ${liveTotal} lessons still have no vector for ${provider.model}/${provider.dims}; retry with memory.mjs reembed --batch 4\n`
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
956
1110
|
break;
|
|
957
1111
|
}
|
|
958
1112
|
case "index": {
|
|
@@ -1075,15 +1229,18 @@ try {
|
|
|
1075
1229
|
}
|
|
1076
1230
|
}
|
|
1077
1231
|
const provider = createProvider(readMemoryConfig(dir));
|
|
1078
|
-
const { openMemory } = await import("../lib/memory.mjs");
|
|
1232
|
+
const { openMemory, degradedWarning, detectLocalProvider } = await import("../lib/memory.mjs");
|
|
1079
1233
|
const { searchIndex } = await import("../lib/code-index.mjs");
|
|
1234
|
+
const detectedProvider = provider ? null : await detectLocalProvider();
|
|
1080
1235
|
const m = openMemory(dir);
|
|
1081
1236
|
let result;
|
|
1082
1237
|
try {
|
|
1083
|
-
result = await searchIndex(m, query, { k, files, provider });
|
|
1238
|
+
result = await searchIndex(m, query, { k, files, provider, detectedProvider });
|
|
1084
1239
|
} finally {
|
|
1085
1240
|
m.close();
|
|
1086
1241
|
}
|
|
1242
|
+
const searchWarning = degradedWarning(result, { what: "code index search", code: true, detectedProvider });
|
|
1243
|
+
if (searchWarning) process.stderr.write(searchWarning + "\n");
|
|
1087
1244
|
if (opts.json) {
|
|
1088
1245
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1089
1246
|
} else {
|
|
@@ -1267,7 +1424,7 @@ try {
|
|
|
1267
1424
|
break;
|
|
1268
1425
|
}
|
|
1269
1426
|
default:
|
|
1270
|
-
fail("usage: memory.mjs <record|reflect|outcomes|lessons|lesson|import|recall|doctor|reembed|embed-check|index|condition|decision|rule|claude-md> ...", 2);
|
|
1427
|
+
fail("usage: memory.mjs <record|reflect|outcomes|lessons|lesson|import|recall|brief-lessons|doctor|reembed|embed-check|index|condition|decision|rule|claude-md> ...", 2);
|
|
1271
1428
|
}
|
|
1272
1429
|
} catch (err) {
|
|
1273
1430
|
fail(err.message);
|