@dougharness/cli 1.0.2 → 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 -0
- package/dist/generate/config.js +4 -1
- package/dist/generate/config.js.map +1 -1
- package/dist/generate/proposal.js +15 -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/embeddings.mjs +185 -23
- package/node_modules/@dougharness/flow/lib/land.mjs +69 -10
- package/node_modules/@dougharness/flow/lib/learn.mjs +4 -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 +190 -34
- 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/{core-next → doug-hand}/SKILL.md +31 -18
- 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 +8 -9
- 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/test-totals.mjs +40 -7
- 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/stop-gate.mjs +551 -113
- 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/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
|
|
@@ -165,7 +182,7 @@
|
|
|
165
182
|
// lesson add and import embed only the row(s) they just wrote, best-effort, when a provider is configured: a
|
|
166
183
|
// provider failure prints one stderr line and never turns into a nonzero exit for that add/import. A pending
|
|
167
184
|
// row from an earlier add is left alone until `reembed` (or a later add/import) picks it up.
|
|
168
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
185
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from "node:fs";
|
|
169
186
|
import { resolve, join, dirname } from "node:path";
|
|
170
187
|
import { createHash } from "node:crypto";
|
|
171
188
|
import { spawnSync } from "node:child_process";
|
|
@@ -187,7 +204,7 @@ import {
|
|
|
187
204
|
|
|
188
205
|
const argv = process.argv.slice(2);
|
|
189
206
|
const FLAGS = new Set(["json", "all", "hand", "dry-run", "check", "no-check"]);
|
|
190
|
-
const REPEATABLE = new Set(["adversary"]);
|
|
207
|
+
const REPEATABLE = new Set(["adversary", "review"]);
|
|
191
208
|
const opts = {};
|
|
192
209
|
const positional = [];
|
|
193
210
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -596,26 +613,88 @@ try {
|
|
|
596
613
|
case "reflect": {
|
|
597
614
|
const usage =
|
|
598
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]';
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if (!reportFile) fail(usage, 2);
|
|
602
|
-
const dir = dirFrom(rest, 1);
|
|
603
|
-
const reportPath = resolve(reportFile);
|
|
604
|
-
if (!existsSync(reportPath)) fail(`no such report file: ${reportPath}`);
|
|
616
|
+
let dir;
|
|
617
|
+
let reportPath;
|
|
605
618
|
let reportText;
|
|
606
|
-
try {
|
|
607
|
-
reportText = readFileSync(reportPath, "utf8");
|
|
608
|
-
} catch (err) {
|
|
609
|
-
fail(`cannot read report file ${reportPath}: ${err.message}`);
|
|
610
|
-
}
|
|
611
619
|
let report;
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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 || []);
|
|
616
696
|
}
|
|
617
697
|
const reportHash = createHash("sha256").update(reportText).digest("hex");
|
|
618
|
-
const classes = parseAdversaryClasses(opts.adversary || []);
|
|
619
698
|
const plan = loadPlan(dir);
|
|
620
699
|
const models = loadModels(dir);
|
|
621
700
|
const model = textOrNull(opts.model) || (models.tiers.cheap && models.tiers.cheap.model) || "haiku";
|
|
@@ -640,7 +719,7 @@ try {
|
|
|
640
719
|
classes,
|
|
641
720
|
plan,
|
|
642
721
|
run: opts.run ?? null,
|
|
643
|
-
card: opts.card ?? null,
|
|
722
|
+
card: handCard ?? opts.card ?? null,
|
|
644
723
|
commit: opts.commit ?? null,
|
|
645
724
|
model,
|
|
646
725
|
claudeDir,
|
|
@@ -843,14 +922,18 @@ try {
|
|
|
843
922
|
}
|
|
844
923
|
const provider = createProvider(readMemoryConfig(dir));
|
|
845
924
|
const staleDays = readMemoryStaleDays(dir);
|
|
846
|
-
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();
|
|
847
928
|
const m = openMemory(dir);
|
|
848
929
|
let result;
|
|
849
930
|
try {
|
|
850
|
-
result = await recallLessons(m, query, { files, k, provider, checkoutDir: dir, staleDays });
|
|
931
|
+
result = await recallLessons(m, query, { files, k, provider, checkoutDir: dir, staleDays, detectedProvider });
|
|
851
932
|
} finally {
|
|
852
933
|
m.close();
|
|
853
934
|
}
|
|
935
|
+
const recallWarning = degradedWarning(result, { what: "memory recall", detectedProvider });
|
|
936
|
+
if (recallWarning) process.stderr.write(recallWarning + "\n");
|
|
854
937
|
if (opts.json) {
|
|
855
938
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
856
939
|
} else {
|
|
@@ -863,6 +946,67 @@ try {
|
|
|
863
946
|
}
|
|
864
947
|
break;
|
|
865
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
|
+
}
|
|
866
1010
|
case "doctor": {
|
|
867
1011
|
const dir = dirFrom(rest, 0);
|
|
868
1012
|
const cfg = readMemoryConfig(dir);
|
|
@@ -919,10 +1063,12 @@ try {
|
|
|
919
1063
|
if (opts.check && opts["no-check"]) {
|
|
920
1064
|
fail(`${usage}\n--check and --no-check are mutually exclusive.`, 2);
|
|
921
1065
|
}
|
|
922
|
-
const { openMemory, embedLessons } = await import("../lib/memory.mjs");
|
|
1066
|
+
const { openMemory, embedLessons, lessonsNeedingEmbedding } = await import("../lib/memory.mjs");
|
|
923
1067
|
const m = openMemory(dir);
|
|
924
1068
|
let result;
|
|
925
1069
|
let checkStatus;
|
|
1070
|
+
let stillMissing = 0;
|
|
1071
|
+
let liveTotal = 0;
|
|
926
1072
|
try {
|
|
927
1073
|
if (opts["no-check"]) {
|
|
928
1074
|
process.stderr.write("embed-check: skipped (--no-check)\n");
|
|
@@ -944,6 +1090,8 @@ try {
|
|
|
944
1090
|
batch,
|
|
945
1091
|
onProgress: ({ embedded, total }) => process.stderr.write(`embedded ${embedded}/${total}\n`),
|
|
946
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;
|
|
947
1095
|
} finally {
|
|
948
1096
|
m.close();
|
|
949
1097
|
}
|
|
@@ -954,6 +1102,11 @@ try {
|
|
|
954
1102
|
`Embedded ${result.embedded}/${result.total} lesson(s) with ${provider.model}/${provider.dims} (${result.failed} failed).\n`
|
|
955
1103
|
);
|
|
956
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
|
+
}
|
|
957
1110
|
break;
|
|
958
1111
|
}
|
|
959
1112
|
case "index": {
|
|
@@ -1076,15 +1229,18 @@ try {
|
|
|
1076
1229
|
}
|
|
1077
1230
|
}
|
|
1078
1231
|
const provider = createProvider(readMemoryConfig(dir));
|
|
1079
|
-
const { openMemory } = await import("../lib/memory.mjs");
|
|
1232
|
+
const { openMemory, degradedWarning, detectLocalProvider } = await import("../lib/memory.mjs");
|
|
1080
1233
|
const { searchIndex } = await import("../lib/code-index.mjs");
|
|
1234
|
+
const detectedProvider = provider ? null : await detectLocalProvider();
|
|
1081
1235
|
const m = openMemory(dir);
|
|
1082
1236
|
let result;
|
|
1083
1237
|
try {
|
|
1084
|
-
result = await searchIndex(m, query, { k, files, provider });
|
|
1238
|
+
result = await searchIndex(m, query, { k, files, provider, detectedProvider });
|
|
1085
1239
|
} finally {
|
|
1086
1240
|
m.close();
|
|
1087
1241
|
}
|
|
1242
|
+
const searchWarning = degradedWarning(result, { what: "code index search", code: true, detectedProvider });
|
|
1243
|
+
if (searchWarning) process.stderr.write(searchWarning + "\n");
|
|
1088
1244
|
if (opts.json) {
|
|
1089
1245
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1090
1246
|
} else {
|
|
@@ -1268,7 +1424,7 @@ try {
|
|
|
1268
1424
|
break;
|
|
1269
1425
|
}
|
|
1270
1426
|
default:
|
|
1271
|
-
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);
|
|
1272
1428
|
}
|
|
1273
1429
|
} catch (err) {
|
|
1274
1430
|
fail(err.message);
|