@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.
Files changed (89) hide show
  1. package/dist/bin.js +8 -0
  2. package/dist/bin.js.map +1 -1
  3. package/dist/board-serve.js +9 -3
  4. package/dist/board-serve.js.map +1 -1
  5. package/dist/board.js +10 -2
  6. package/dist/board.js.map +1 -1
  7. package/dist/detect/repo.js +14 -1
  8. package/dist/detect/repo.js.map +1 -1
  9. package/dist/detect/types.d.ts +2 -0
  10. package/dist/doctor.d.ts +46 -0
  11. package/dist/doctor.js +491 -0
  12. package/dist/doctor.js.map +1 -0
  13. package/dist/generate/agents.d.ts +7 -0
  14. package/dist/generate/agents.js +62 -12
  15. package/dist/generate/agents.js.map +1 -1
  16. package/dist/generate/claude-md.js +4 -0
  17. package/dist/generate/claude-md.js.map +1 -1
  18. package/dist/generate/config.d.ts +8 -1
  19. package/dist/generate/config.js +5 -12
  20. package/dist/generate/config.js.map +1 -1
  21. package/dist/generate/proposal.js +24 -6
  22. package/dist/generate/proposal.js.map +1 -1
  23. package/dist/generate/settings.d.ts +2 -0
  24. package/dist/generate/settings.js +17 -2
  25. package/dist/generate/settings.js.map +1 -1
  26. package/dist/init.d.ts +14 -0
  27. package/dist/init.js +94 -8
  28. package/dist/init.js.map +1 -1
  29. package/dist/memory-setup.d.ts +28 -0
  30. package/dist/memory-setup.js +323 -0
  31. package/dist/memory-setup.js.map +1 -0
  32. package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +16 -4
  33. package/node_modules/@dougharness/flow/agents/planner.md +2 -2
  34. package/node_modules/@dougharness/flow/agents/researcher.md +2 -2
  35. package/node_modules/@dougharness/flow/hooks/hooks.json +16 -0
  36. package/node_modules/@dougharness/flow/lib/board.mjs +154 -12
  37. package/node_modules/@dougharness/flow/lib/code-index.mjs +18 -3
  38. package/node_modules/@dougharness/flow/lib/context.mjs +57 -0
  39. package/node_modules/@dougharness/flow/lib/cost.mjs +129 -4
  40. package/node_modules/@dougharness/flow/lib/data/pairs.json +103 -0
  41. package/node_modules/@dougharness/flow/lib/decisions.mjs +2 -2
  42. package/node_modules/@dougharness/flow/lib/embeddings.mjs +189 -27
  43. package/node_modules/@dougharness/flow/lib/land.mjs +69 -10
  44. package/node_modules/@dougharness/flow/lib/learn.mjs +61 -4
  45. package/node_modules/@dougharness/flow/lib/memory-import.mjs +22 -4
  46. package/node_modules/@dougharness/flow/lib/memory.mjs +326 -35
  47. package/node_modules/@dougharness/flow/lib/plan.mjs +9 -0
  48. package/node_modules/@dougharness/flow/package.json +2 -1
  49. package/node_modules/@dougharness/flow/scripts/board.mjs +60 -20
  50. package/node_modules/@dougharness/flow/scripts/context.mjs +48 -0
  51. package/node_modules/@dougharness/flow/scripts/cost.mjs +15 -3
  52. package/node_modules/@dougharness/flow/scripts/memory.mjs +196 -39
  53. package/node_modules/@dougharness/flow/scripts/plan.mjs +32 -64
  54. package/node_modules/@dougharness/flow/scripts/session-recall.mjs +132 -0
  55. package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +105 -0
  56. package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
  57. package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +44 -10
  58. package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +5 -5
  59. package/node_modules/@dougharness/flow/skills/research/SKILL.md +4 -2
  60. package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +10 -11
  61. package/node_modules/@dougharness/flow/skills/swarm-launch/SKILL.md +1 -1
  62. package/node_modules/@dougharness/flow/workflows/doug-implement.js +119 -72
  63. package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
  64. package/node_modules/@dougharness/gates/hooks/hooks.json +11 -0
  65. package/node_modules/@dougharness/gates/lib/baseline.mjs +166 -1
  66. package/node_modules/@dougharness/gates/lib/block-ledger.mjs +58 -1
  67. package/node_modules/@dougharness/gates/lib/checkpoint.mjs +1 -1
  68. package/node_modules/@dougharness/gates/lib/config.mjs +20 -0
  69. package/node_modules/@dougharness/gates/lib/docs-gate.mjs +95 -0
  70. package/node_modules/@dougharness/gates/lib/evidence.mjs +2 -0
  71. package/node_modules/@dougharness/gates/lib/run.mjs +4 -0
  72. package/node_modules/@dougharness/gates/lib/state.mjs +5 -0
  73. package/node_modules/@dougharness/gates/lib/test-totals.mjs +139 -0
  74. package/node_modules/@dougharness/gates/lib/trace.mjs +24 -7
  75. package/node_modules/@dougharness/gates/lib/verified-tree.mjs +225 -0
  76. package/node_modules/@dougharness/gates/package.json +1 -1
  77. package/node_modules/@dougharness/gates/scripts/research-cap.mjs +133 -5
  78. package/node_modules/@dougharness/gates/scripts/session-start-baseline.mjs +14 -0
  79. package/node_modules/@dougharness/gates/scripts/statusline.mjs +33 -7
  80. package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +794 -67
  81. package/node_modules/@dougharness/gates/scripts/trace.mjs +5 -1
  82. package/node_modules/@dougharness/gates/scripts/turn-start.mjs +17 -0
  83. package/node_modules/@dougharness/gates/scripts/verify.mjs +146 -0
  84. package/package.json +3 -3
  85. package/templates/board-app.js +2 -2
  86. package/node_modules/@dougharness/flow/lib/rehearse.mjs +0 -825
  87. package/node_modules/@dougharness/flow/scripts/rehearse.mjs +0 -129
  88. package/node_modules/@dougharness/flow/skills/core-next/SKILL.md +0 -92
  89. 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 /core-next skills. Deterministic; no agents.
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
- summary: ["cost", "codex-cost", "wall", "commit", "card"],
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
- summary: "usage: board.mjs summary <report.json> [--cost <usd>] [--codex-cost <usd>] [--wall <text>] [--commit <sha>] [--card <id>]",
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 (built by hand with /core-next, not by doug-next)\n` : s.flow ? `skipping ${s.id}: flow track (run it with /doug-next)\n` : `skipping ${s.id}: waiting on ${s.waitingOn.join(", ")}\n`);
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 entry = runEntry({ card, report, 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 });
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
- process.stdout.write(runSummary({ report, 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 }));
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 (!runId) {
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
- // hybrid recall (card memory-recall): BM25 over FTS5 fused by
65
- // reciprocal rank fusion with cosine over stored vectors when a
66
- // provider is configured and vectors exist for it, recency-
67
- // weighted, scope-boosted by --files, each returned lesson's
68
- // citation re-checked against [dir] only when [dir] is a real
69
- // project root (contains .doug); keyword-only, and says so, when
70
- // no provider is configured or no vectors match it
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; --hand is refused, since a hand
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 evals/memory/
132
- // pairs.json (or --pairs) against the configured provider and
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 (a standalone-published copy of
473
- // this plugin outside this checkout - DEFAULT_SANITY_PAIRS lives at the repo root) is not a hard failure: build
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
- if (opts.hand) fail(`${usage}\n--hand is not accepted: a hand landing has no report.`, 2);
599
- const [reportFile] = rest;
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
- try {
612
- report = unwrapReport(JSON.parse(reportText));
613
- } catch (err) {
614
- fail(`cannot parse report file ${reportPath} as JSON: ${err.message}`);
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);