@dougharness/cli 1.3.0 → 1.5.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/ablate.js +2 -2
- package/dist/ablate.js.map +1 -1
- package/dist/bin.js +13 -4
- package/dist/bin.js.map +1 -1
- package/dist/board-page.d.ts +5 -1
- package/dist/board-page.js +19 -5
- package/dist/board-page.js.map +1 -1
- package/dist/board-serve.js +34 -10
- package/dist/board-serve.js.map +1 -1
- package/dist/board.d.ts +1 -1
- package/dist/board.js +160 -13
- package/dist/board.js.map +1 -1
- package/node_modules/@dougharness/flow/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/flow/agents/planner.md +1 -0
- package/node_modules/@dougharness/flow/hooks/register.tsx +18 -2
- package/node_modules/@dougharness/flow/lib/autopilot-core.mjs +94 -7
- package/node_modules/@dougharness/flow/lib/autopilot-view.mjs +31 -4
- package/node_modules/@dougharness/flow/lib/board.mjs +375 -30
- package/node_modules/@dougharness/flow/lib/card-loop.mjs +237 -24
- package/node_modules/@dougharness/flow/lib/cost.mjs +9 -0
- package/node_modules/@dougharness/flow/lib/decisions.mjs +2 -4
- package/node_modules/@dougharness/flow/lib/learn.mjs +95 -54
- package/node_modules/@dougharness/flow/lib/memory.mjs +1403 -44
- package/node_modules/@dougharness/flow/lib/plan.mjs +40 -0
- package/node_modules/@dougharness/flow/package.json +1 -1
- package/node_modules/@dougharness/flow/scripts/autopilot.mjs +355 -13
- package/node_modules/@dougharness/flow/scripts/board.mjs +160 -27
- package/node_modules/@dougharness/flow/scripts/cost.mjs +6 -3
- package/node_modules/@dougharness/flow/scripts/learn.mjs +1 -1
- package/node_modules/@dougharness/flow/scripts/memory.mjs +668 -39
- package/node_modules/@dougharness/flow/scripts/plan.mjs +3 -3
- package/node_modules/@dougharness/flow/scripts/session-recall.mjs +11 -4
- package/node_modules/@dougharness/flow/skills/doug-hand/SKILL.md +7 -3
- package/node_modules/@dougharness/flow/skills/doug-learn/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/doug-next/SKILL.md +11 -2
- package/node_modules/@dougharness/flow/skills/doug-start/SKILL.md +9 -8
- package/node_modules/@dougharness/flow/skills/doug-swarm/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/research/SKILL.md +1 -1
- package/node_modules/@dougharness/flow/skills/run-report/SKILL.md +3 -3
- package/node_modules/@dougharness/flow/workflows/card-queen.js +238 -25
- package/node_modules/@dougharness/flow/workflows/doug-implement.js +55 -37
- package/node_modules/@dougharness/gates/.claude-plugin/plugin.json +1 -1
- package/node_modules/@dougharness/gates/lib/baseline.mjs +32 -0
- package/node_modules/@dougharness/gates/lib/block-ledger.mjs +45 -5
- package/node_modules/@dougharness/gates/lib/docs-gate.mjs +6 -2
- package/node_modules/@dougharness/gates/lib/verified-tree.mjs +33 -0
- package/node_modules/@dougharness/gates/package.json +1 -1
- package/node_modules/@dougharness/gates/scripts/stop-gate.mjs +130 -42
- package/node_modules/@dougharness/gates/scripts/verify.mjs +20 -4
- package/package.json +3 -3
- package/templates/board-app.js +240 -30
|
@@ -19,13 +19,26 @@
|
|
|
19
19
|
// condition columns from it; without one the row still records
|
|
20
20
|
// (exit 0) and an extra stdout line says the landing is
|
|
21
21
|
// unstamped (--json: "stamped": true|false instead)
|
|
22
|
+
// memory.mjs record <card-id> --hand --stop <reason> [dir] [--detail <text>] [--json]
|
|
23
|
+
// record a stopped by-hand run as exactly one stop row (run
|
|
24
|
+
// stop:<12 hex>, commit null, stop_class <reason>, stop_reason
|
|
25
|
+
// --detail or the reason); the same stop recorded again updates
|
|
26
|
+
// that row. Condition columns and memory_config come from
|
|
27
|
+
// condition.json when present, else the row is unstamped (extra
|
|
28
|
+
// stdout line; --json: "stamped": false). --stop with no value
|
|
29
|
+
// or with --commit exits 2 and writes nothing
|
|
22
30
|
// memory.mjs condition open <card-id> [dir] [--json]
|
|
23
31
|
// (card outcomes-condition-columns) writes
|
|
24
32
|
// .doug/.state/reports/<card-id>/condition.json: harnessCommit
|
|
25
33
|
// (git HEAD), configHash (conditionConfigHash: .doug/config.json
|
|
26
34
|
// plus the CLAUDE.md Models table text), class and arm
|
|
27
35
|
// (<class>/default) from the card's board record, assignedBy
|
|
28
|
-
// "policy", exploreProbability null, openedAt
|
|
36
|
+
// "policy", exploreProbability null, openedAt, memoryConfig (card
|
|
37
|
+
// memory-outcome-arms: memoryConfig() from the project's
|
|
38
|
+
// embeddings config and staleDays, track "hand": delivery
|
|
39
|
+
// index+fetch; a flow row from `record <report.json>` is
|
|
40
|
+
// stamped with track "flow": delivery index+fetch, fetch top-k,
|
|
41
|
+
// fetchTokens 4000). Refuses (exit 1,
|
|
29
42
|
// nothing written) when the file already exists, the card is
|
|
30
43
|
// missing or has no class, or there is no git HEAD
|
|
31
44
|
// memory.mjs condition backfill [dir] [--json]
|
|
@@ -39,9 +52,22 @@
|
|
|
39
52
|
// outcomes rows with a harness commit." (--json: {"backfilled": n})
|
|
40
53
|
// memory.mjs outcomes [dir] [--run <id>] [--task <id>] [--json]
|
|
41
54
|
// list recorded outcomes, filtered by run and/or task
|
|
55
|
+
// memory.mjs arms [dir] [--min <n>] [--json]
|
|
56
|
+
// (card memory-outcome-arms) compares outcome arms by the memory
|
|
57
|
+
// configuration stamped on each row (outcomes.memory_config):
|
|
58
|
+
// landings, stops, then stop-or-land rate, fix rounds, harmful
|
|
59
|
+
// marks per landing and cost per landing once an arm has --min
|
|
60
|
+
// landings (default 12; an integer >= 1), else "not compared: <n>
|
|
61
|
+
// of <min> landings"; a last line says whether two arms are
|
|
62
|
+
// comparable. Reads existing rows only: no database -> "No
|
|
63
|
+
// outcomes recorded." (exit 0, database not created); no claude
|
|
64
|
+
// call, no network request
|
|
42
65
|
// memory.mjs lessons [dir] [--search <text>] [--k <n>] [--all] [--json]
|
|
43
66
|
// list lessons, or full-text search them (--search); --all
|
|
44
67
|
// includes superseded rows
|
|
68
|
+
// memory.mjs lessons --unconfirmed [dir] [--json]
|
|
69
|
+
// read-only review list: live lessons with helpful 0, harmful 0
|
|
70
|
+
// and no `lesson show --card` fetch; id, kind, created, title
|
|
45
71
|
// memory.mjs lesson add --text <text> --kind <feedback|project|pitfall|pattern> [dir]
|
|
46
72
|
// [--scope <glob,glob>] [--citation <text>] [--card <id>]
|
|
47
73
|
// [--commit <sha>] [--source-agent <name>] [--source-model <name>] [--json]
|
|
@@ -60,11 +86,35 @@
|
|
|
60
86
|
// memory.mjs lesson unstale <id> [dir] [--json]
|
|
61
87
|
// clear a lesson's stale mark (set by recall's citation
|
|
62
88
|
// re-check) so it is a candidate again; refuses on an unknown id
|
|
89
|
+
// memory.mjs lesson unarchive <id> [dir] [--json]
|
|
90
|
+
// restore an archived lesson (clears archived and
|
|
91
|
+
// archived_reason); refuses on an unknown or non-archived id
|
|
92
|
+
// memory.mjs lesson show <id> [dir] [--card <card-id>] [--json]
|
|
93
|
+
// (card memory-progressive-access) one lesson in full: cost
|
|
94
|
+
// line, citation line, superseded/stale status when set, a
|
|
95
|
+
// blank line, then the whole text, never cut. Read-only unless
|
|
96
|
+
// --card <card-id> (a plain id, else exit 2): then, after the
|
|
97
|
+
// lesson is found and printed, one JSON line {id, at} is
|
|
98
|
+
// appended to .doug/.state/reports/<card-id>/fetched.jsonl
|
|
99
|
+
// (reflect --hand credits only those). No
|
|
100
|
+
// store or unknown id -> one stderr line, exit 1 (nothing
|
|
101
|
+
// created); no id -> usage, exit 2. --json: the row with
|
|
102
|
+
// hasVector and tokens instead of the embedding blob
|
|
103
|
+
// memory.mjs history show <ref> [dir] [--json]
|
|
104
|
+
// (card memory-progressive-access) fetch run history (L0) by
|
|
105
|
+
// id: <card> lists its runs, <card>/<run> lists the run's
|
|
106
|
+
// entries with token costs, <card>/<run>:<line> prints one
|
|
107
|
+
// entry whole (a leading history: and a .jsonl suffix are
|
|
108
|
+
// accepted). Needs no memory database; bad id -> exit 2,
|
|
109
|
+
// missing -> one stderr line, exit 1
|
|
63
110
|
// memory.mjs brief-lessons <card-id> [dir] [--k <n>] [--json]
|
|
64
111
|
// (card memory-every-session) recall for a hand-track card's
|
|
65
112
|
// briefs: query = the card's title and goal, k 8. Prints the
|
|
66
|
-
// workflow's lessons header, the
|
|
67
|
-
// (capped at 2000 chars
|
|
113
|
+
// workflow's lessons header, the index `- [id] title (kind,
|
|
114
|
+
// ~n tokens)` lines (capped at 2000 chars; the lead fetches
|
|
115
|
+
// full text with `lesson show`), an `index: omitted` line
|
|
116
|
+
// naming lessons the cap dropped, then the window line
|
|
117
|
+
// when lessons were hidden, then `memoryUsed: <ids>`;
|
|
68
118
|
// writes .doug/.state/reports/<card-id>/lessons.json. No database
|
|
69
119
|
// -> "No lessons recalled for <id>." (database not created);
|
|
70
120
|
// a database or provider error -> one stderr line, exit 0;
|
|
@@ -91,11 +141,13 @@
|
|
|
91
141
|
// filtered in code (a pitfall needs a block classed real, a
|
|
92
142
|
// pattern needs a first-pass verified task, the citation must be
|
|
93
143
|
// checkable, the text must not already be derivable from the
|
|
94
|
-
// checkout, and a
|
|
95
|
-
//
|
|
144
|
+
// checkout, and a proposal may name one listed lesson
|
|
145
|
+
// it repeats (that lesson's seen rises, nothing is appended) or
|
|
146
|
+
// supersedes (the new lesson is written and the old retired); a
|
|
147
|
+
// near-duplicate with neither is rejected); idempotent on the report's
|
|
96
148
|
// sha256 (a second reflect on the same report is a no-op, exit
|
|
97
|
-
// 0); --dry-run applies nothing
|
|
98
|
-
// prints
|
|
149
|
+
// 0); --dry-run applies nothing, makes no model call, and
|
|
150
|
+
// prints the counters it would credit and the prompt it would send. Cost is measured from the same local
|
|
99
151
|
// transcripts cost.mjs reads, falling back to the CLI's own
|
|
100
152
|
// cost estimate. For each appended lesson with no CLAUDE.md home
|
|
101
153
|
// it prints `claude-md candidate for lesson <id>: <command>`
|
|
@@ -107,9 +159,48 @@
|
|
|
107
159
|
// (card memory-every-session) the same pass for a hand landing:
|
|
108
160
|
// assembles .doug/.state/reports/<card-id>/hand-report.json from
|
|
109
161
|
// the saved briefs/, reviews/ and lessons.json (memoryUsed = its
|
|
110
|
-
// ids; verified true; passes = --rounds, default 1; one fixed
|
|
162
|
+
// ids that the card's fetched.jsonl lists; verified true; passes = --rounds, default 1; one fixed
|
|
111
163
|
// blocker ledger entry per --review id), then reflects on it
|
|
112
164
|
// exactly as above. --commit is required (exit 2). No schema change.
|
|
165
|
+
// memory.mjs reflect --failures <card-id> [dir] [--session <id>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
166
|
+
// (card memory-failure-lessons) the failure pass: reads the card's
|
|
167
|
+
// run history for stopped reports, blocks classed real and
|
|
168
|
+
// denials, prints the item count with the blocks it hid by class
|
|
169
|
+
// and the entries it could not read, then a cheap-row pass proposes
|
|
170
|
+
// pitfall lessons citing a history ref of one item; a proposal's
|
|
171
|
+
// `repeats` names a live pitfall and records an occurrence in
|
|
172
|
+
// .doug/.state/memory/pitfalls.jsonl (no new lesson, no helpful).
|
|
173
|
+
// Exclusive with --hand and a report path (exit 2); idempotent on
|
|
174
|
+
// the slice; --dry-run makes no model call and prints the prompt it would send;
|
|
175
|
+
// not wired into any loop (its spend cap is open)
|
|
176
|
+
// memory.mjs ideas [dir] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--cap <n>] [--dry-run] [--json]
|
|
177
|
+
// (card memory-idea-proposals, ADR 0019 L3) one cheap-row `claude
|
|
178
|
+
// -p --json-schema` call (--max-usd default 0.5) over the live
|
|
179
|
+
// lessons, run history, pitfall occurrences and the Backlog and
|
|
180
|
+
// Ready cards files at most --cap (default 3) idea cards to
|
|
181
|
+
// Backlog only, source `memory-proposed: <citations>`, and appends
|
|
182
|
+
// priority reviews naming Backlog cards to
|
|
183
|
+
// .doug/.state/memory/priority-reviews.jsonl. An item with an
|
|
184
|
+
// unresolved citation is dropped; a duplicate or invalid idea is
|
|
185
|
+
// skipped. Never touches Ready. No store or no board exits 1 with
|
|
186
|
+
// no call; a failed call exits 1 and writes nothing. --dry-run
|
|
187
|
+
// makes no model call, writes neither file and prints the prompt
|
|
188
|
+
// it would send and the files it would write
|
|
189
|
+
// memory.mjs model regen [dir] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]
|
|
190
|
+
// (card memory-project-model, ADR 0019 L2) one cheap-row `claude
|
|
191
|
+
// -p --json-schema` call (--max-usd default 0.5) over the live
|
|
192
|
+
// lessons and the run-history index writes .doug/project-model.md:
|
|
193
|
+
// failure patterns, conventions and open risks, at most 2000
|
|
194
|
+
// characters in all. A statement with any citation that does not
|
|
195
|
+
// resolve in code (a live lesson id, or a run-history ref whose
|
|
196
|
+
// file and line exist) is dropped whole; over the cap the most
|
|
197
|
+
// cited statements stay. Prints one line with the counts, the
|
|
198
|
+
// drops and the cost. A failed call or zero surviving statements
|
|
199
|
+
// exits 1 and leaves an existing file untouched; no store exits 1
|
|
200
|
+
// with no call. --dry-run makes no model call, writes nothing and prints the prompt
|
|
201
|
+
// memory.mjs model show [dir]
|
|
202
|
+
// prints .doug/project-model.md verbatim, read-only; missing ->
|
|
203
|
+
// one stderr line, exit 1
|
|
113
204
|
// memory.mjs doctor [dir] [--json]
|
|
114
205
|
// probe the configured embedding provider (memory.embeddings in
|
|
115
206
|
// .doug/config.json), or a local Ollama at localhost:11434 when
|
|
@@ -189,11 +280,11 @@
|
|
|
189
280
|
// lesson add and import embed only the row(s) they just wrote, best-effort, when a provider is configured: a
|
|
190
281
|
// provider failure prints one stderr line and never turns into a nonzero exit for that add/import. A pending
|
|
191
282
|
// row from an earlier add is left alone until `reembed` (or a later add/import) picks it up.
|
|
192
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from "node:fs";
|
|
283
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, appendFileSync, statSync } from "node:fs";
|
|
193
284
|
import { resolve, join, dirname } from "node:path";
|
|
194
285
|
import { createHash } from "node:crypto";
|
|
195
286
|
import { spawnSync } from "node:child_process";
|
|
196
|
-
import { parseAdversaryClasses, loadBoard, findCard } from "../lib/board.mjs";
|
|
287
|
+
import { parseAdversaryClasses, loadBoard, saveBoard, findCard } from "../lib/board.mjs";
|
|
197
288
|
import { planFor, unwrapReport } from "../lib/plan.mjs";
|
|
198
289
|
import { costRun } from "../lib/cost.mjs";
|
|
199
290
|
import { loadModels } from "../lib/models.mjs";
|
|
@@ -210,7 +301,7 @@ import {
|
|
|
210
301
|
} from "../lib/embeddings.mjs";
|
|
211
302
|
|
|
212
303
|
const argv = process.argv.slice(2);
|
|
213
|
-
const FLAGS = new Set(["json", "all", "hand", "dry-run", "check", "no-check"]);
|
|
304
|
+
const FLAGS = new Set(["json", "all", "hand", "dry-run", "check", "no-check", "unconfirmed"]);
|
|
214
305
|
const REPEATABLE = new Set(["adversary", "review", "harmful"]);
|
|
215
306
|
const opts = {};
|
|
216
307
|
const positional = [];
|
|
@@ -283,7 +374,8 @@ async function bestEffortEmbed(m, dir, ids) {
|
|
|
283
374
|
if (!ids || !ids.length) return;
|
|
284
375
|
const provider = createProvider(readMemoryConfig(dir));
|
|
285
376
|
if (!provider) return;
|
|
286
|
-
|
|
377
|
+
const { LIVE_LESSON_SQL } = await import("../lib/memory.mjs");
|
|
378
|
+
if (embeddingChangeDetected(m, provider, LIVE_LESSON_SQL)) {
|
|
287
379
|
process.stderr.write(
|
|
288
380
|
`Note: ${provider.model}/${provider.dims} is new to this store; leaving ${ids.length} lesson(s) pending. Run \`memory.mjs reembed\` first — it runs the cosine sanity check before this store commits to a new model.\n`
|
|
289
381
|
);
|
|
@@ -349,10 +441,10 @@ function parseLastJsonObject(text) {
|
|
|
349
441
|
// stdin, a 180s timeout, and a 64MB stdout buffer (a report's prompt plus a model's proposals can run long).
|
|
350
442
|
// Throws on any failure the spec names (spawn error, timeout, unparseable stdout, is_error, a missing
|
|
351
443
|
// structured_output.lessons array) so runReflect's single catch handles every case uniformly.
|
|
352
|
-
function makePropose({ claudeBin, model, maxUsd, dir }) {
|
|
444
|
+
function makePropose({ claudeBin, model, maxUsd, dir, schema = null, resultKey = "lessons", extraKeys = [] }) {
|
|
353
445
|
return async (prompt) => {
|
|
354
446
|
const { REFLECT_SCHEMA } = await import("../lib/memory.mjs");
|
|
355
|
-
const args = ["-p", "--output-format", "json", "--json-schema", JSON.stringify(REFLECT_SCHEMA), "--model", model, "--max-budget-usd", String(maxUsd)];
|
|
447
|
+
const args = ["-p", "--output-format", "json", "--json-schema", JSON.stringify(schema ?? REFLECT_SCHEMA), "--model", model, "--max-budget-usd", String(maxUsd)];
|
|
356
448
|
const res = spawnSync(claudeBin, args, { input: prompt, encoding: "utf8", cwd: dir, timeout: 180000, maxBuffer: 64 * 1024 * 1024 });
|
|
357
449
|
if (res.error) throw new Error(`claude spawn failed: ${res.error.message}`);
|
|
358
450
|
if (res.signal) throw new Error(`claude was killed by signal ${res.signal} (likely the 180s timeout)`);
|
|
@@ -364,10 +456,11 @@ function makePropose({ claudeBin, model, maxUsd, dir }) {
|
|
|
364
456
|
throw new Error(`claude printed no parseable JSON: ${err.message}`);
|
|
365
457
|
}
|
|
366
458
|
if (envelope.is_error) throw new Error(`claude reported is_error${envelope.result ? `: ${envelope.result}` : ""}`);
|
|
367
|
-
const
|
|
368
|
-
if (!Array.isArray(
|
|
459
|
+
const items = envelope.structured_output && envelope.structured_output[resultKey];
|
|
460
|
+
if (!Array.isArray(items)) throw new Error(`claude's structured_output.${resultKey} is missing`);
|
|
369
461
|
return {
|
|
370
|
-
|
|
462
|
+
[resultKey]: items,
|
|
463
|
+
...Object.fromEntries(extraKeys.map((k) => [k, Array.isArray(envelope.structured_output[k]) ? envelope.structured_output[k] : []])),
|
|
371
464
|
sessionId: envelope.session_id || null,
|
|
372
465
|
totalCostUsd: typeof envelope.total_cost_usd === "number" ? envelope.total_cost_usd : null,
|
|
373
466
|
};
|
|
@@ -383,6 +476,10 @@ function idsList(ids) {
|
|
|
383
476
|
return ids.length ? ids.join(", ") : "none";
|
|
384
477
|
}
|
|
385
478
|
|
|
479
|
+
function supersededList(pairs) {
|
|
480
|
+
return pairs.length ? pairs.map((p) => `${p.old}->${p.new}`).join(", ") : "none";
|
|
481
|
+
}
|
|
482
|
+
|
|
386
483
|
function reviewIssueCountText(count) {
|
|
387
484
|
return count === null || count === undefined ? "null" : count;
|
|
388
485
|
}
|
|
@@ -404,7 +501,7 @@ function outcomeLine(row) {
|
|
|
404
501
|
`verified ${verifiedWord(row.verified)}`,
|
|
405
502
|
...(row.partial === 1 || row.partial === 0 ? [`partial ${partialWord(row.partial)}`] : []),
|
|
406
503
|
`review ${reviewIssueCountText(row.review_issue_count)} issue(s)`,
|
|
407
|
-
`adversary ${adv} (${blocksText(row.blocks)} block(s))`,
|
|
504
|
+
String(row.run).startsWith("stop:") ? `gate blocks ${blocksText(row.blocks)}` : `adversary ${adv} (${blocksText(row.blocks)} block(s))`,
|
|
408
505
|
`passes ${row.passes ?? "null"}`,
|
|
409
506
|
`tokens ${tokensText(row.tokens)}`,
|
|
410
507
|
`usd ${usdText(row.usd)}`,
|
|
@@ -443,11 +540,11 @@ function sanityLine(result) {
|
|
|
443
540
|
// embedded under provider.model/provider.dims — a model or dims change, including the very first embed ever
|
|
444
541
|
// under any model. Mirrors lessonsNeedingEmbedding's WHERE clause in lib/memory.mjs so "no lesson matches"
|
|
445
542
|
// means the same thing here as it does there.
|
|
446
|
-
function embeddingChangeDetected(m, provider) {
|
|
447
|
-
const total = m.prepare(
|
|
543
|
+
function embeddingChangeDetected(m, provider, LIVE_LESSON_SQL) {
|
|
544
|
+
const total = m.prepare(`SELECT COUNT(*) AS n FROM lessons WHERE ${LIVE_LESSON_SQL}`).get().n;
|
|
448
545
|
if (total === 0) return false;
|
|
449
546
|
const matching = m
|
|
450
|
-
.prepare(
|
|
547
|
+
.prepare(`SELECT COUNT(*) AS n FROM lessons WHERE ${LIVE_LESSON_SQL} AND embedding_model = ? AND embedding_dims = ?`)
|
|
451
548
|
.get(provider.model, provider.dims).n;
|
|
452
549
|
return matching === 0;
|
|
453
550
|
}
|
|
@@ -514,6 +611,56 @@ try {
|
|
|
514
611
|
const handUsage =
|
|
515
612
|
'usage: memory.mjs record <card-id> --hand [dir] --commit <sha> [--wall <text>] [--gate <text>] [--note <text>] [--cost <usd>] [--json]';
|
|
516
613
|
const [id, ...extra] = rest;
|
|
614
|
+
if (opts.stop !== undefined) {
|
|
615
|
+
// Card memory-hand-stop-rows: a stopped by-hand run records exactly one stop row.
|
|
616
|
+
const stopUsage =
|
|
617
|
+
'usage: memory.mjs record <card-id> --hand --stop <reason> [dir] [--detail <text>] [--json]';
|
|
618
|
+
if (!id || String(opts.stop).trim() === "" || opts.commit !== undefined) fail(stopUsage, 2);
|
|
619
|
+
if (id.endsWith(".json") || existsSync(resolve(id))) fail(`${stopUsage}\n--hand takes a card id, not a report.`, 2);
|
|
620
|
+
if (opts.run !== undefined || opts.adversary !== undefined || opts["claude-dir"] !== undefined || extra.length > 1) {
|
|
621
|
+
fail(stopUsage, 2);
|
|
622
|
+
}
|
|
623
|
+
const dir = dirFrom(rest, 1);
|
|
624
|
+
const conditionFile = join(dir, ".doug", ".state", "reports", id, "condition.json");
|
|
625
|
+
let condition = null;
|
|
626
|
+
if (existsSync(conditionFile)) {
|
|
627
|
+
try {
|
|
628
|
+
condition = JSON.parse(readFileSync(conditionFile, "utf8"));
|
|
629
|
+
} catch (err) {
|
|
630
|
+
fail(`cannot read ${conditionFile}: ${err.message}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const reason = String(opts.stop).trim();
|
|
634
|
+
const detail = textOrNull(opts.detail);
|
|
635
|
+
const reportHash = createHash("sha256")
|
|
636
|
+
.update(JSON.stringify({ card: id, reason, detail, openedAt: condition?.openedAt ?? null }))
|
|
637
|
+
.digest("hex");
|
|
638
|
+
const { openMemory, recordOutcomes, stopOutcomeRow } = await import("../lib/memory.mjs");
|
|
639
|
+
const row = stopOutcomeRow({ card: id, report: { outcome: "stopped", reason, detail }, reportHash, blocks: [], condition });
|
|
640
|
+
const m = openMemory(dir);
|
|
641
|
+
let result;
|
|
642
|
+
try {
|
|
643
|
+
result = recordOutcomes(m, [row]);
|
|
644
|
+
} finally {
|
|
645
|
+
m.close();
|
|
646
|
+
}
|
|
647
|
+
const stamped = !!condition;
|
|
648
|
+
if (opts.json) {
|
|
649
|
+
process.stdout.write(
|
|
650
|
+
JSON.stringify({ run: row.run, card: id, inserted: result.inserted, updated: result.updated, stamped }, null, 2) + "\n"
|
|
651
|
+
);
|
|
652
|
+
} else {
|
|
653
|
+
process.stdout.write(
|
|
654
|
+
`Recorded a hand-track stop for ${id} (${reason}) in .doug/.state/memory/memory.db (${result.inserted} new, ${result.updated} updated).\n`
|
|
655
|
+
);
|
|
656
|
+
if (!stamped) {
|
|
657
|
+
process.stdout.write(
|
|
658
|
+
`No condition.json for ${id}: this stop is unstamped (memory.mjs condition open ${id} writes one before the brief).\n`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
517
664
|
if (!id || !opts.commit) fail(handUsage, 2);
|
|
518
665
|
if (id.endsWith(".json") || existsSync(resolve(id))) {
|
|
519
666
|
fail(`${handUsage}\n--hand takes a card id, not a report.`, 2);
|
|
@@ -599,8 +746,9 @@ try {
|
|
|
599
746
|
}
|
|
600
747
|
}
|
|
601
748
|
// Lazy: a Node without node:sqlite must fail loudly here, not with a stack trace, and never before this point.
|
|
602
|
-
const { openMemory, recordOutcomes, outcomeRows } = await import("../lib/memory.mjs");
|
|
603
|
-
const
|
|
749
|
+
const { openMemory, recordOutcomes, outcomeRows, memoryConfig } = await import("../lib/memory.mjs");
|
|
750
|
+
const flowConfig = memoryConfig({ embeddings: readMemoryConfig(dir).embeddings, staleDays: readMemoryStaleDays(dir), track: "flow" });
|
|
751
|
+
const rows = outcomeRows(report, { plan, classes, run, reportHash, reportPath, card: opts.card ?? null, commit: opts.commit ?? null, cost, memoryConfig: flowConfig });
|
|
604
752
|
const m = openMemory(dir);
|
|
605
753
|
let result;
|
|
606
754
|
try {
|
|
@@ -621,12 +769,85 @@ try {
|
|
|
621
769
|
case "reflect": {
|
|
622
770
|
const usage =
|
|
623
771
|
'usage: memory.mjs reflect <report.json> [dir] [--run <wf-id>] [--card <id>] [--commit <sha>] [--adversary "<id>=<class>[: <reason>]"]... [--harmful <id>]... [--claude-bin <path>] [--claude-dir <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
772
|
+
if (opts.failures !== undefined) {
|
|
773
|
+
const failuresUsage =
|
|
774
|
+
"usage: memory.mjs reflect --failures <card-id> [dir] [--session <id>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]";
|
|
775
|
+
const card = textOrNull(opts.failures);
|
|
776
|
+
if (!card || opts.hand) fail(`${failuresUsage}\n--failures takes a card id and is exclusive with --hand and a report path.`, 2);
|
|
777
|
+
if (rest.length > 1) fail(failuresUsage, 2);
|
|
778
|
+
if (rest.length === 1) {
|
|
779
|
+
// A lone positional that is a file, or names a .json report, is a report path, not a dir.
|
|
780
|
+
let isFile = /\.json$/i.test(rest[0]);
|
|
781
|
+
try {
|
|
782
|
+
if (statSync(resolve(rest[0])).isFile()) isFile = true;
|
|
783
|
+
} catch {
|
|
784
|
+
// missing path: judged by its name above
|
|
785
|
+
}
|
|
786
|
+
if (isFile) fail(`${failuresUsage}\n--failures takes a card id and is exclusive with --hand and a report path.`, 2);
|
|
787
|
+
}
|
|
788
|
+
const fdir = dirFrom(rest, 0);
|
|
789
|
+
const fmodels = loadModels(fdir);
|
|
790
|
+
const fmodel = textOrNull(opts.model) || (fmodels.tiers.cheap && fmodels.tiers.cheap.model) || "haiku";
|
|
791
|
+
let fmaxUsd = 0.5;
|
|
792
|
+
if (opts["max-usd"] !== undefined) {
|
|
793
|
+
fmaxUsd = Number(opts["max-usd"]);
|
|
794
|
+
if (!Number.isFinite(fmaxUsd) || fmaxUsd <= 0) fail(`${failuresUsage}\n--max-usd must be a positive number, got ${JSON.stringify(opts["max-usd"])}.`, 2);
|
|
795
|
+
}
|
|
796
|
+
const fClaudeBin = textOrNull(opts["claude-bin"]) || process.env.DOUG_CLAUDE_BIN || "claude";
|
|
797
|
+
const fDry = !!opts["dry-run"];
|
|
798
|
+
const { openMemory, runFailureReflect, FAILURE_REFLECT_SCHEMA } = await import("../lib/memory.mjs");
|
|
799
|
+
const m = openMemory(fdir);
|
|
800
|
+
let fres;
|
|
801
|
+
try {
|
|
802
|
+
fres = await runFailureReflect(m, {
|
|
803
|
+
dir: fdir,
|
|
804
|
+
card,
|
|
805
|
+
session: textOrNull(opts.session),
|
|
806
|
+
model: fmodel,
|
|
807
|
+
claudeDir: opts["claude-dir"] ? resolve(opts["claude-dir"]) : undefined,
|
|
808
|
+
dryRun: fDry,
|
|
809
|
+
propose: fDry ? null : makePropose({ claudeBin: fClaudeBin, model: fmodel, maxUsd: fmaxUsd, dir: fdir, schema: FAILURE_REFLECT_SCHEMA }),
|
|
810
|
+
});
|
|
811
|
+
if (!fDry && fres.ok && fres.appended && fres.appended.length) await bestEffortEmbed(m, fdir, fres.appended);
|
|
812
|
+
} finally {
|
|
813
|
+
m.close();
|
|
814
|
+
}
|
|
815
|
+
if (fres.alreadyReflected) {
|
|
816
|
+
if (opts.json) process.stdout.write(JSON.stringify(fres.row, null, 2) + "\n");
|
|
817
|
+
else process.stdout.write(`Already reflected on ${fres.row.recorded} for the failures of ${card}; nothing to do.\n`);
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
if (fres.ok === false) {
|
|
821
|
+
process.stderr.write(`reflect: the failure pass failed for ${card}: ${fres.reason}\n`);
|
|
822
|
+
if (opts.json) process.stdout.write(JSON.stringify(fres, null, 2) + "\n");
|
|
823
|
+
process.exit(1);
|
|
824
|
+
}
|
|
825
|
+
if (opts.json) {
|
|
826
|
+
process.stdout.write(JSON.stringify(fres, null, 2) + "\n");
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
const hiddenText = Object.entries(fres.hidden).map(([k, n]) => `${k} ${n}`).join(", ") || "none";
|
|
830
|
+
const prefix = fDry ? "(dry run) " : "";
|
|
831
|
+
process.stdout.write(`${prefix}Failures of ${card}: ${fres.items} item(s); hidden blocks: ${hiddenText}; unreadable: ${fres.unreadable}\n`);
|
|
832
|
+
if (fres.items === 0) break;
|
|
833
|
+
if (fDry && fres.proposed === null) {
|
|
834
|
+
process.stdout.write(`${prefix}no model call; the prompt that would be sent:\n${fres.prompt}\n`);
|
|
835
|
+
process.stdout.write(`${prefix}Would append pitfall lessons to the memory store, .doug/.state/memory/pitfalls.jsonl and a reflections row; ${costLine(fres.usd, fres.usdSource)}\n`);
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
process.stdout.write(
|
|
839
|
+
`${prefix}Reflected: proposed ${fres.proposed}, ${fDry ? "would append" : "appended"} ${fres.appended.length} (${idsList(fres.appended)}), repeated ${fres.repeated.length} (${idsList(fres.repeated)}), rejected ${fres.rejected.length}; model ${fres.model}; ${costLine(fres.usd, fres.usdSource)}\n`
|
|
840
|
+
);
|
|
841
|
+
for (const r of fres.rejected) process.stdout.write(` rejected ${r.text ? JSON.stringify(r.text) : "(no text)"}: ${r.reason}\n`);
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
624
844
|
let dir;
|
|
625
845
|
let reportPath;
|
|
626
846
|
let reportText;
|
|
627
847
|
let report;
|
|
628
848
|
let classes;
|
|
629
849
|
let handCard = null;
|
|
850
|
+
let rounds = 1;
|
|
630
851
|
if (opts.hand) {
|
|
631
852
|
const handUsage =
|
|
632
853
|
'usage: memory.mjs reflect --hand <card-id> [dir] --commit <sha> [--rounds <n>] [--review "<R<n> or <card-id>/R<n>>=<real|marginal|false>[: <reason>]"]... [--harmful <id>]... [--note <text>] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json]';
|
|
@@ -634,7 +855,6 @@ try {
|
|
|
634
855
|
if (!id || !opts.commit) fail(handUsage, 2);
|
|
635
856
|
handCard = id;
|
|
636
857
|
dir = dirFrom(rest, 1);
|
|
637
|
-
let rounds = 1;
|
|
638
858
|
if (opts.rounds !== undefined) {
|
|
639
859
|
rounds = Number(opts.rounds);
|
|
640
860
|
if (!Number.isInteger(rounds) || rounds < 1) fail(`${handUsage}\n--rounds must be a positive integer, got ${JSON.stringify(opts.rounds)}.`, 2);
|
|
@@ -677,6 +897,16 @@ try {
|
|
|
677
897
|
} catch {
|
|
678
898
|
memoryUsed = [];
|
|
679
899
|
}
|
|
900
|
+
const fetched = new Set();
|
|
901
|
+
try {
|
|
902
|
+
for (const line of readFileSync(join(base, "fetched.jsonl"), "utf8").split("\n")) {
|
|
903
|
+
try {
|
|
904
|
+
const rec = JSON.parse(line);
|
|
905
|
+
if (rec && typeof rec.id === "string") fetched.add(rec.id);
|
|
906
|
+
} catch {}
|
|
907
|
+
}
|
|
908
|
+
} catch {}
|
|
909
|
+
memoryUsed = [...new Set(memoryUsed.filter((x) => fetched.has(x)))];
|
|
680
910
|
const ledger = classes.map((c) => {
|
|
681
911
|
return { id: c.id, stage: "adversary", seat: "reviewer", severity: "blocker", status: "fixed", description: c.reason || `reviewer finding ${c.id}` };
|
|
682
912
|
});
|
|
@@ -694,6 +924,23 @@ try {
|
|
|
694
924
|
mkdirSync(base, { recursive: true });
|
|
695
925
|
writeFileSync(reportPath, reportText, "utf8");
|
|
696
926
|
report = handReport;
|
|
927
|
+
if (!opts["dry-run"]) {
|
|
928
|
+
try {
|
|
929
|
+
const { historyScanner, writeRunHistory } = await import("../lib/memory.mjs");
|
|
930
|
+
const entries = [{ kind: "report", source: "hand-report.json", value: handReport }];
|
|
931
|
+
for (const f of readDirFiles("briefs")) {
|
|
932
|
+
const mm = /^(\d+)-(.+)\.md$/.exec(f.name);
|
|
933
|
+
entries.push({ kind: "brief", seat: mm ? mm[2] : null, round: mm ? Number(mm[1]) : null, source: `briefs/${f.name}`, value: f.text });
|
|
934
|
+
}
|
|
935
|
+
for (const f of readDirFiles("reviews")) {
|
|
936
|
+
const mm = /^(\d+)\.md$/.exec(f.name);
|
|
937
|
+
entries.push({ kind: "review", seat: "reviewer", round: mm ? Number(mm[1]) : null, source: `reviews/${f.name}`, value: f.text });
|
|
938
|
+
}
|
|
939
|
+
writeRunHistory(dir, { card: id, run: `hand:${opts.commit}`, entries, scan: await historyScanner(dir) });
|
|
940
|
+
} catch (err) {
|
|
941
|
+
process.stderr.write(`reflect: warning: cannot write the run history for ${id}: ${err.message || err}\n`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
697
944
|
} else {
|
|
698
945
|
const [reportFile] = rest;
|
|
699
946
|
if (!reportFile) fail(usage, 2);
|
|
@@ -727,7 +974,7 @@ try {
|
|
|
727
974
|
const runLabel = opts.run || `report:${reportHash.slice(0, 12)}`;
|
|
728
975
|
|
|
729
976
|
const { openMemory, runReflect } = await import("../lib/memory.mjs");
|
|
730
|
-
const propose = makePropose({ claudeBin, model, maxUsd, dir });
|
|
977
|
+
const propose = dryRun ? null : makePropose({ claudeBin, model, maxUsd, dir });
|
|
731
978
|
const m = openMemory(dir);
|
|
732
979
|
let result;
|
|
733
980
|
try {
|
|
@@ -748,6 +995,10 @@ try {
|
|
|
748
995
|
if (!dryRun && result.ok && result.appended && result.appended.length) {
|
|
749
996
|
await bestEffortEmbed(m, dir, result.appended);
|
|
750
997
|
}
|
|
998
|
+
// Card memory-outcome-arms: only an explicit --rounds is a measurement; the default of 1 is never written.
|
|
999
|
+
if (handCard && !dryRun && opts.rounds !== undefined && (result.ok || result.alreadyReflected)) {
|
|
1000
|
+
m.prepare("UPDATE outcomes SET passes = ?, fix_passes = ? WHERE run = ? AND task = ?").run(rounds, rounds - 1, `hand:${opts.commit}`, handCard);
|
|
1001
|
+
}
|
|
751
1002
|
} finally {
|
|
752
1003
|
m.close();
|
|
753
1004
|
}
|
|
@@ -772,11 +1023,20 @@ try {
|
|
|
772
1023
|
} else {
|
|
773
1024
|
const c = result.counters;
|
|
774
1025
|
const prefix = result.dryRun ? "(dry run) " : "";
|
|
1026
|
+
if (result.dryRun && result.proposed === null) {
|
|
1027
|
+
process.stdout.write(
|
|
1028
|
+
`${prefix}Reflected on ${runLabel}: counters helpful +${c.helpful.length} (${idsList(c.helpful)}) harmful +${c.harmful.length} (${idsList(c.harmful)}); ` +
|
|
1029
|
+
`cheap-row pass not run (dry run, no model call); ${costLine(result.usd, result.usdSource)}\n`
|
|
1030
|
+
);
|
|
1031
|
+
process.stdout.write(`${prefix}the prompt that would be sent:\n${result.prompt}\n`);
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
775
1034
|
process.stdout.write(
|
|
776
1035
|
`${prefix}Reflected on ${runLabel}: counters helpful +${c.helpful.length} (${idsList(c.helpful)}) harmful +${c.harmful.length} (${idsList(c.harmful)}); ` +
|
|
777
1036
|
`proposed ${result.proposed}, appended ${result.appended.length} (${idsList(result.appended)}), bumped ${result.bumped.length} (${idsList(result.bumped)}), ` +
|
|
778
|
-
`rejected ${result.rejected.length}; model ${result.model}; ${costLine(result.usd, result.usdSource)}\n`
|
|
1037
|
+
`superseded ${result.superseded.length} (${supersededList(result.superseded)}), rejected ${result.rejected.length}; model ${result.model}; ${costLine(result.usd, result.usdSource)}\n`
|
|
779
1038
|
);
|
|
1039
|
+
for (const n of result.notes || []) process.stdout.write(` note: ${n}\n`);
|
|
780
1040
|
for (const r of result.rejected) {
|
|
781
1041
|
process.stdout.write(` rejected ${r.text ? JSON.stringify(r.text) : "(no text)"}: ${r.reason}\n`);
|
|
782
1042
|
}
|
|
@@ -810,8 +1070,70 @@ try {
|
|
|
810
1070
|
}
|
|
811
1071
|
break;
|
|
812
1072
|
}
|
|
1073
|
+
case "arms": {
|
|
1074
|
+
const armsUsage = "usage: memory.mjs arms [dir] [--min <n>] [--json]";
|
|
1075
|
+
let min = 12;
|
|
1076
|
+
if (opts.min !== undefined) {
|
|
1077
|
+
min = /^\d+$/.test(String(opts.min)) ? Number(opts.min) : NaN;
|
|
1078
|
+
if (!Number.isInteger(min) || min < 1) fail(`${armsUsage}\n--min must be an integer >= 1, got ${JSON.stringify(opts.min)}.`, 2);
|
|
1079
|
+
}
|
|
1080
|
+
const dir = dirFrom(rest, 0);
|
|
1081
|
+
const { memoryPath, openMemory, compareMemoryArms } = await import("../lib/memory.mjs");
|
|
1082
|
+
if (!existsSync(memoryPath(dir))) {
|
|
1083
|
+
process.stdout.write("No outcomes recorded.\n");
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
const m = openMemory(dir);
|
|
1087
|
+
let res;
|
|
1088
|
+
try {
|
|
1089
|
+
res = compareMemoryArms(m, { min });
|
|
1090
|
+
} finally {
|
|
1091
|
+
m.close();
|
|
1092
|
+
}
|
|
1093
|
+
if (opts.json) {
|
|
1094
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
const num = (v, d = 2) => (typeof v === "number" ? v.toFixed(d) : "n/a");
|
|
1098
|
+
const label = (cfg) => {
|
|
1099
|
+
try {
|
|
1100
|
+
const c = JSON.parse(cfg);
|
|
1101
|
+
const e = c.embeddings ? `${c.embeddings.provider}/${c.embeddings.model}/${c.embeddings.dims}` : "keyword-only";
|
|
1102
|
+
return `${c.stage} ${e} staleDays ${c.staleDays}`;
|
|
1103
|
+
} catch {
|
|
1104
|
+
return cfg;
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
const lines = res.arms.map((a) => {
|
|
1108
|
+
const head = `${label(a.config)}: ${a.landings} landings, ${a.stops} stops`;
|
|
1109
|
+
if (!a.ready) return `${head}; not compared: ${a.landings} of ${res.min} landings`;
|
|
1110
|
+
const fix = a.fixRounds.n ? `${num(a.fixRounds.mean)} (n ${a.fixRounds.n})` : "n/a (none measured)";
|
|
1111
|
+
return `${head}; land rate ${num(a.landRate)}; fix rounds ${fix}; harmful per landing ${num(a.harmfulPerLanding)}; cost per landing ${typeof a.costPerLanding === "number" ? `$${a.costPerLanding.toFixed(2)}` : "n/a"} (${a.priced} priced)`;
|
|
1112
|
+
});
|
|
1113
|
+
if (res.unstamped) lines.push(`${res.unstamped} rows carry no memory configuration and are in no arm.`);
|
|
1114
|
+
lines.push(res.comparable ? "Two or more arms are comparable." : "Not comparable yet: fewer than two arms have enough landings.");
|
|
1115
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
813
1118
|
case "lessons": {
|
|
814
1119
|
const dir = dirFrom(rest, 0);
|
|
1120
|
+
if (opts.unconfirmed) {
|
|
1121
|
+
if (opts.search !== undefined || opts.all) fail("usage: memory.mjs lessons --unconfirmed [dir] [--json] (not with --search or --all)", 2);
|
|
1122
|
+
let list = [];
|
|
1123
|
+
if (existsSync(join(dir, ".doug", ".state", "memory", "memory.db"))) {
|
|
1124
|
+
const { openMemory, listUnconfirmedLessons, readFetchedLessonIds } = await import("../lib/memory.mjs");
|
|
1125
|
+
const m = openMemory(dir);
|
|
1126
|
+
try {
|
|
1127
|
+
list = listUnconfirmedLessons(m, { fetchedIds: readFetchedLessonIds(dir) });
|
|
1128
|
+
} finally {
|
|
1129
|
+
m.close();
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (opts.json) process.stdout.write(JSON.stringify(list, null, 2) + "\n");
|
|
1133
|
+
else if (!list.length) process.stdout.write("no unconfirmed lessons\n");
|
|
1134
|
+
else process.stdout.write(list.map((r) => `${r.id} ${r.kind} ${r.created} ${r.title}`).join("\n") + "\n");
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
815
1137
|
const { openMemory, listLessons, searchLessons } = await import("../lib/memory.mjs");
|
|
816
1138
|
const m = openMemory(dir);
|
|
817
1139
|
let rows;
|
|
@@ -838,7 +1160,9 @@ try {
|
|
|
838
1160
|
"usage: memory.mjs lesson add --text <text> --kind <feedback|project|pitfall|pattern> [dir]\n" +
|
|
839
1161
|
" [--scope <glob,glob>] [--citation <text>] [--card <id>]\n" +
|
|
840
1162
|
" [--commit <sha>] [--source-agent <name>] [--source-model <name>] [--json]\n" +
|
|
841
|
-
" memory.mjs lesson unstale <id> [dir] [--json]"
|
|
1163
|
+
" memory.mjs lesson unstale <id> [dir] [--json]\n" +
|
|
1164
|
+
" memory.mjs lesson unarchive <id> [dir] [--json]\n" +
|
|
1165
|
+
" memory.mjs lesson show <id> [dir] [--card <card-id>] [--json]";
|
|
842
1166
|
const [sub, ...subrest] = rest;
|
|
843
1167
|
if (sub === "unstale") {
|
|
844
1168
|
const [id] = subrest;
|
|
@@ -859,6 +1183,57 @@ try {
|
|
|
859
1183
|
}
|
|
860
1184
|
break;
|
|
861
1185
|
}
|
|
1186
|
+
if (sub === "unarchive") {
|
|
1187
|
+
const [id] = subrest;
|
|
1188
|
+
if (!id) fail(usage, 2);
|
|
1189
|
+
const dir = dirFrom(subrest, 1);
|
|
1190
|
+
const { openMemory, unarchiveLesson } = await import("../lib/memory.mjs");
|
|
1191
|
+
const m = openMemory(dir);
|
|
1192
|
+
let row;
|
|
1193
|
+
try {
|
|
1194
|
+
row = unarchiveLesson(m, id);
|
|
1195
|
+
} finally {
|
|
1196
|
+
m.close();
|
|
1197
|
+
}
|
|
1198
|
+
if (opts.json) {
|
|
1199
|
+
process.stdout.write(JSON.stringify(row, null, 2) + "\n");
|
|
1200
|
+
} else {
|
|
1201
|
+
process.stdout.write(`Restored archived lesson ${row.id}.\n`);
|
|
1202
|
+
}
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
if (sub === "show") {
|
|
1206
|
+
const [id] = subrest;
|
|
1207
|
+
if (!id) fail(usage, 2);
|
|
1208
|
+
if (opts.card !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(String(opts.card))) fail(usage, 2);
|
|
1209
|
+
const dir = dirFrom(subrest, 1);
|
|
1210
|
+
if (!existsSync(join(dir, ".doug", ".state", "memory", "memory.db"))) fail(`no lesson ${id}: no memory store`);
|
|
1211
|
+
const { openMemory, getLesson, lessonTokens } = await import("../lib/memory.mjs");
|
|
1212
|
+
const m = openMemory(dir);
|
|
1213
|
+
let row;
|
|
1214
|
+
try {
|
|
1215
|
+
row = getLesson(m, id);
|
|
1216
|
+
} finally {
|
|
1217
|
+
m.close();
|
|
1218
|
+
}
|
|
1219
|
+
if (!row) fail(`no lesson ${id}`);
|
|
1220
|
+
const tokens = lessonTokens(row.text);
|
|
1221
|
+
if (opts.json) {
|
|
1222
|
+
const { embedding, ...rest2 } = row;
|
|
1223
|
+
process.stdout.write(JSON.stringify({ ...rest2, hasVector: embedding != null, tokens }, null, 2) + "\n");
|
|
1224
|
+
} else {
|
|
1225
|
+
const head = [`lesson ${row.id} (${row.kind}, ~${tokens} tokens)`, `citation: ${row.citation || "none"}`];
|
|
1226
|
+
if (row.superseded_by) head.push(`superseded by ${row.superseded_by}`);
|
|
1227
|
+
if (row.stale) head.push(`stale since ${row.stale}`);
|
|
1228
|
+
process.stdout.write(head.join("\n") + "\n\n" + row.text + "\n");
|
|
1229
|
+
}
|
|
1230
|
+
if (opts.card !== undefined) {
|
|
1231
|
+
const fdir = join(dir, ".doug", ".state", "reports", String(opts.card));
|
|
1232
|
+
mkdirSync(fdir, { recursive: true });
|
|
1233
|
+
appendFileSync(join(fdir, "fetched.jsonl"), JSON.stringify({ id: row.id, at: new Date().toISOString() }) + "\n");
|
|
1234
|
+
}
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
862
1237
|
if (sub !== "add") fail(usage, 2);
|
|
863
1238
|
const dir = dirFrom(subrest, 0);
|
|
864
1239
|
const sourceAgent = String(opts["source-agent"] !== undefined ? opts["source-agent"] : "lead").trim();
|
|
@@ -949,7 +1324,7 @@ try {
|
|
|
949
1324
|
}
|
|
950
1325
|
const provider = createProvider(readMemoryConfig(dir));
|
|
951
1326
|
const staleDays = readMemoryStaleDays(dir);
|
|
952
|
-
const { openMemory, recallLessons, degradedWarning, detectLocalProvider } = await import("../lib/memory.mjs");
|
|
1327
|
+
const { openMemory, recallLessons, degradedWarning, detectLocalProvider, windowNote } = await import("../lib/memory.mjs");
|
|
953
1328
|
// Probed once, and only when no provider is configured; a failed probe is just "not detected".
|
|
954
1329
|
const detectedProvider = provider ? null : await detectLocalProvider();
|
|
955
1330
|
const m = openMemory(dir);
|
|
@@ -968,7 +1343,8 @@ try {
|
|
|
968
1343
|
result.mode === "hybrid"
|
|
969
1344
|
? `recall: hybrid via ${result.provider} ${provider.model}/${provider.dims}`
|
|
970
1345
|
: `recall: keyword-only (${result.reason})`;
|
|
971
|
-
const
|
|
1346
|
+
const note = windowNote(result.hidden, staleDays);
|
|
1347
|
+
const lines = [header, ...(note ? [note] : []), ...result.lessons.map((l) => `${l.id} ${l.score.toFixed(4)} ${l.kind} ${l.text}`)];
|
|
972
1348
|
process.stdout.write(lines.join("\n") + "\n");
|
|
973
1349
|
}
|
|
974
1350
|
break;
|
|
@@ -991,21 +1367,25 @@ try {
|
|
|
991
1367
|
}
|
|
992
1368
|
const query = `${card.title}\n${card.goal ?? ""}`;
|
|
993
1369
|
const header = "Lessons from earlier runs on this repository (memory; each cites where it was learned):";
|
|
994
|
-
const saved = { card: id, query, mode: null, reason: null, ids: [], text: null, recalledAt: new Date().toISOString() };
|
|
1370
|
+
const saved = { card: id, query, mode: null, reason: null, ids: [], text: null, omitted: [], recalledAt: new Date().toISOString(), hidden: null };
|
|
1371
|
+
let note = null;
|
|
995
1372
|
let error = null;
|
|
996
1373
|
if (existsSync(join(dir, ".doug", ".state", "memory", "memory.db"))) {
|
|
997
1374
|
try {
|
|
998
1375
|
const provider = createProvider(readMemoryConfig(dir));
|
|
999
1376
|
const staleDays = readMemoryStaleDays(dir);
|
|
1000
|
-
const { openMemory, recallLessons, degradedWarning, degradedReason, detectLocalProvider,
|
|
1377
|
+
const { openMemory, recallLessons, degradedWarning, degradedReason, detectLocalProvider, renderLessonIndex, windowNote } = await import("../lib/memory.mjs");
|
|
1001
1378
|
const detectedProvider = provider ? null : await detectLocalProvider();
|
|
1002
1379
|
const m = openMemory(dir);
|
|
1003
1380
|
try {
|
|
1004
1381
|
const result = await recallLessons(m, query, { k, checkoutDir: dir, provider, staleDays, detectedProvider });
|
|
1005
1382
|
const warning = degradedWarning(result, { what: "memory recall", detectedProvider });
|
|
1006
1383
|
if (warning) process.stderr.write(warning + "\n");
|
|
1007
|
-
const rendered =
|
|
1384
|
+
const rendered = renderLessonIndex(result.lessons);
|
|
1385
|
+
saved.omitted = rendered.omitted;
|
|
1008
1386
|
saved.mode = result.mode;
|
|
1387
|
+
saved.hidden = result.hidden ?? null;
|
|
1388
|
+
note = windowNote(result.hidden, staleDays);
|
|
1009
1389
|
saved.reason = result.degraded ? degradedReason(result, detectedProvider) : null;
|
|
1010
1390
|
saved.ids = rendered.ids;
|
|
1011
1391
|
saved.text = rendered.text === null ? null : result.degraded ? `(recall was keyword-only: ${saved.reason})\n${rendered.text}` : rendered.text;
|
|
@@ -1018,6 +1398,9 @@ try {
|
|
|
1018
1398
|
saved.reason = null;
|
|
1019
1399
|
saved.ids = [];
|
|
1020
1400
|
saved.text = null;
|
|
1401
|
+
saved.omitted = [];
|
|
1402
|
+
saved.hidden = null;
|
|
1403
|
+
note = null;
|
|
1021
1404
|
saved.error = error;
|
|
1022
1405
|
process.stderr.write(`[doug] memory recall skipped: ${error}\n`);
|
|
1023
1406
|
}
|
|
@@ -1028,10 +1411,255 @@ try {
|
|
|
1028
1411
|
if (opts.json) {
|
|
1029
1412
|
process.stdout.write(JSON.stringify(saved, null, 2) + "\n");
|
|
1030
1413
|
} else if (saved.ids.length) {
|
|
1031
|
-
|
|
1414
|
+
const omittedLine = saved.omitted.length
|
|
1415
|
+
? [`index: omitted ${saved.omitted.length} lessons over the 2000-character index budget: ${saved.omitted.join(", ")}`]
|
|
1416
|
+
: [];
|
|
1417
|
+
process.stdout.write([header, saved.text, ...omittedLine, ...(note ? [note] : []), `memoryUsed: ${saved.ids.join(", ")}`].join("\n") + "\n");
|
|
1032
1418
|
} else {
|
|
1033
|
-
process.stdout.write(`No lessons recalled for ${id}.\n`);
|
|
1419
|
+
process.stdout.write(`No lessons recalled for ${id}.\n${note ? note + "\n" : ""}`);
|
|
1420
|
+
}
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
case "history": {
|
|
1424
|
+
const usage = "usage: memory.mjs history show <card>[/<run>[:<line>]] [dir] [--json]";
|
|
1425
|
+
const [sub, ref, ...extra] = rest;
|
|
1426
|
+
if (sub !== "show" || !ref) fail(usage, 2);
|
|
1427
|
+
const dir = dirFrom(extra, 0);
|
|
1428
|
+
const { parseHistoryRef, readRunHistory } = await import("../lib/memory.mjs");
|
|
1429
|
+
let parsed;
|
|
1430
|
+
try {
|
|
1431
|
+
parsed = parseHistoryRef(ref);
|
|
1432
|
+
} catch (err) {
|
|
1433
|
+
fail(`${err.message}\n${usage}`, 2);
|
|
1434
|
+
}
|
|
1435
|
+
let data;
|
|
1436
|
+
try {
|
|
1437
|
+
data = readRunHistory(dir, parsed);
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
fail(err.message);
|
|
1440
|
+
}
|
|
1441
|
+
if (opts.json) {
|
|
1442
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
1443
|
+
} else if (parsed.line !== null) {
|
|
1444
|
+
const e = data;
|
|
1445
|
+
const out = [`history ${parsed.card}/${parsed.run}:${parsed.line} (${e.kind}, seat ${e.seat ?? "-"}, round ${e.round ?? "-"}, ${e.source}, ~${e.tokens} tokens)`];
|
|
1446
|
+
if (e.truncated) out.push(`truncated at capture: ${e.bytes} bytes, head kept`);
|
|
1447
|
+
if (e.redacted) out.push(`redacted: ${e.redacted}`);
|
|
1448
|
+
process.stdout.write(out.join("\n") + "\n\n" + (e.text ?? "") + "\n");
|
|
1449
|
+
} else if (parsed.run !== null) {
|
|
1450
|
+
const out = [`history ${data.card}/${data.run}: ${data.entries.length} entries, ~${data.tokens} tokens`];
|
|
1451
|
+
for (const e of data.entries) {
|
|
1452
|
+
if (e.kind === "cap") {
|
|
1453
|
+
out.push(`${e.line} cap dropped ${e.dropped} entries over ${e.maxRunBytes} bytes`);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
out.push(
|
|
1457
|
+
`${e.line} ${e.kind} ${e.seat ?? "-"} ${e.round ?? "-"} ${e.source} ~${e.tokens} tokens` +
|
|
1458
|
+
(e.truncated ? ` truncated from ${e.bytes} bytes` : "") +
|
|
1459
|
+
(e.redacted ? ` redacted: ${e.redacted}` : "")
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
1463
|
+
} else {
|
|
1464
|
+
const out = [`history ${data.card}: ${data.runs.length} runs`];
|
|
1465
|
+
for (const r of data.runs) out.push(`${data.card}/${r.run} ${r.entries} entries ~${r.tokens} tokens`);
|
|
1466
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
1467
|
+
}
|
|
1468
|
+
break;
|
|
1469
|
+
}
|
|
1470
|
+
case "ideas": {
|
|
1471
|
+
const usage = "usage: memory.mjs ideas [dir] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--cap <n>] [--dry-run] [--json]";
|
|
1472
|
+
const dir = dirFrom(rest, 0);
|
|
1473
|
+
const lib = await import("../lib/memory.mjs");
|
|
1474
|
+
const models = loadModels(dir);
|
|
1475
|
+
const model = textOrNull(opts.model) || (models.tiers.cheap && models.tiers.cheap.model) || "haiku";
|
|
1476
|
+
let maxUsd = 0.5;
|
|
1477
|
+
if (opts["max-usd"] !== undefined) {
|
|
1478
|
+
maxUsd = Number(opts["max-usd"]);
|
|
1479
|
+
if (!Number.isFinite(maxUsd) || maxUsd <= 0) fail(`${usage}\n--max-usd must be a positive number, got ${JSON.stringify(opts["max-usd"])}.`, 2);
|
|
1480
|
+
}
|
|
1481
|
+
let cap = lib.IDEA_CAP;
|
|
1482
|
+
if (opts.cap !== undefined) {
|
|
1483
|
+
cap = Number(opts.cap);
|
|
1484
|
+
if (!Number.isInteger(cap) || cap < 1) fail(`${usage}\n--cap must be an integer of at least 1, got ${JSON.stringify(opts.cap)}.`, 2);
|
|
1485
|
+
}
|
|
1486
|
+
const claudeBin = textOrNull(opts["claude-bin"]) || process.env.DOUG_CLAUDE_BIN || "claude";
|
|
1487
|
+
const dryRun = !!opts["dry-run"];
|
|
1488
|
+
if (!existsSync(lib.memoryPath(dir))) fail("no memory store: nothing to propose ideas from");
|
|
1489
|
+
let board;
|
|
1490
|
+
try {
|
|
1491
|
+
board = loadBoard(dir);
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
fail(`ideas: ${err.message}`);
|
|
1494
|
+
}
|
|
1495
|
+
const m = lib.openMemory(dir);
|
|
1496
|
+
let call;
|
|
1497
|
+
let ideaCheck;
|
|
1498
|
+
let reviewCheck;
|
|
1499
|
+
let dryPrompt = null;
|
|
1500
|
+
try {
|
|
1501
|
+
const prompt = lib.buildIdeasPrompt({
|
|
1502
|
+
lessons: lib.listLessons(m),
|
|
1503
|
+
runs: lib.listRunHistoryRefs(dir),
|
|
1504
|
+
pitfalls: lib.readPitfallOccurrences(dir),
|
|
1505
|
+
cards: board.cards.filter((c) => c.column === "backlog" || c.column === "ready"),
|
|
1506
|
+
});
|
|
1507
|
+
if (dryRun) {
|
|
1508
|
+
dryPrompt = prompt;
|
|
1509
|
+
} else {
|
|
1510
|
+
try {
|
|
1511
|
+
call = await makePropose({ claudeBin, model, maxUsd, dir, schema: lib.IDEAS_SCHEMA, resultKey: "ideas", extraKeys: ["reviews"] })(prompt);
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
fail(`ideas: the model call failed: ${err.message}`);
|
|
1514
|
+
}
|
|
1515
|
+
ideaCheck = lib.checkIdeaCitations(m, dir, call.ideas);
|
|
1516
|
+
reviewCheck = lib.checkIdeaCitations(m, dir, call.reviews);
|
|
1517
|
+
}
|
|
1518
|
+
} finally {
|
|
1519
|
+
m.close();
|
|
1520
|
+
}
|
|
1521
|
+
if (dryRun) {
|
|
1522
|
+
const writes = [".doug/board.json", lib.PRIORITY_REVIEWS_RELPATH];
|
|
1523
|
+
if (opts.json) {
|
|
1524
|
+
process.stdout.write(JSON.stringify({ dryRun: true, prompt: dryPrompt, wouldWrite: writes, usd: 0, usdSource: "none" }, null, 2) + "\n");
|
|
1525
|
+
} else {
|
|
1526
|
+
process.stdout.write(`(dry run) no model call; the prompt that would be sent:\n${dryPrompt}\n`);
|
|
1527
|
+
for (const w of writes) process.stdout.write(`Would write ${w}\n`);
|
|
1528
|
+
process.stdout.write(`(dry run) Would file idea cards and reviews from the model's proposals; ${costLine(0, "none")}\n`);
|
|
1529
|
+
}
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
const cost = lib.computeReflectCost({ dir, sessionId: call.sessionId, totalCostUsd: call.totalCostUsd });
|
|
1533
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
1534
|
+
try {
|
|
1535
|
+
board = loadBoard(dir);
|
|
1536
|
+
} catch (err) {
|
|
1537
|
+
fail(`ideas: ${err.message}`);
|
|
1538
|
+
}
|
|
1539
|
+
const filed = lib.fileIdeaCards(board, ideaCheck.kept, { cap, date });
|
|
1540
|
+
const reviewsChecked = lib.checkPriorityReviews(board, reviewCheck.kept, { cap, recorded: lib.readPriorityReviews(dir) });
|
|
1541
|
+
const at = new Date().toISOString();
|
|
1542
|
+
const reviewLines = reviewsChecked.kept.map((r) => ({ at, text: r.text, cards: r.cards, citations: r.citations }));
|
|
1543
|
+
const droppedReviews = [...reviewCheck.dropped, ...reviewsChecked.dropped];
|
|
1544
|
+
if (!dryRun) {
|
|
1545
|
+
if (filed.filed.length) saveBoard(dir, filed.board);
|
|
1546
|
+
if (reviewLines.length) {
|
|
1547
|
+
const file = join(dir, lib.PRIORITY_REVIEWS_RELPATH);
|
|
1548
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
1549
|
+
appendFileSync(file, reviewLines.map((l) => JSON.stringify(l)).join("\n") + "\n", "utf8");
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
const filedCards = filed.filed.map((id) => {
|
|
1553
|
+
const c = filed.board.cards.find((x) => x.id === id);
|
|
1554
|
+
return { id, title: c.title };
|
|
1555
|
+
});
|
|
1556
|
+
if (opts.json) {
|
|
1557
|
+
process.stdout.write(
|
|
1558
|
+
JSON.stringify(
|
|
1559
|
+
{ filed: filedCards, skipped: filed.skipped, reviews: reviewLines, droppedReviews, unresolved: ideaCheck.dropped, usd: cost.usd, usdSource: cost.usdSource, dryRun },
|
|
1560
|
+
null,
|
|
1561
|
+
2
|
|
1562
|
+
) + "\n"
|
|
1563
|
+
);
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1566
|
+
const out = [];
|
|
1567
|
+
for (const c of filedCards) out.push(`${dryRun ? "would file" : "filed"} ${c.id}: ${c.title}`);
|
|
1568
|
+
for (const s2 of filed.skipped) out.push(`skipped ${s2.id}: ${s2.reason}`);
|
|
1569
|
+
for (const u of ideaCheck.dropped) out.push(`skipped ${u.id}: unresolved citation`);
|
|
1570
|
+
for (const r of reviewLines) out.push(`${dryRun ? "would record" : "recorded"} review: ${r.text} (${r.cards.join(", ")})`);
|
|
1571
|
+
out.push(
|
|
1572
|
+
`${dryRun ? "Would file" : "Filed"} ${filedCards.length} idea cards to Backlog, ${filed.skipped.length + ideaCheck.dropped.length} skipped, ${reviewLines.length} reviews kept, ${droppedReviews.length} dropped; ${costLine(cost.usd, cost.usdSource)}`
|
|
1573
|
+
);
|
|
1574
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
1575
|
+
break;
|
|
1576
|
+
}
|
|
1577
|
+
case "model": {
|
|
1578
|
+
const usage = "usage: memory.mjs model regen [dir] [--claude-bin <path>] [--model <name>] [--max-usd <n>] [--dry-run] [--json] | memory.mjs model show [dir]";
|
|
1579
|
+
const [sub, ...subrest] = rest;
|
|
1580
|
+
if (sub !== "regen" && sub !== "show") fail(usage, 2);
|
|
1581
|
+
const dir = dirFrom(subrest, 0);
|
|
1582
|
+
const lib = await import("../lib/memory.mjs");
|
|
1583
|
+
const modelPath = join(dir, lib.PROJECT_MODEL_RELPATH);
|
|
1584
|
+
if (sub === "show") {
|
|
1585
|
+
if (!existsSync(modelPath)) fail(`no project model at ${lib.PROJECT_MODEL_RELPATH}: run \`memory.mjs model regen\``);
|
|
1586
|
+
process.stdout.write(readFileSync(modelPath, "utf8"));
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
const models = loadModels(dir);
|
|
1590
|
+
const model = textOrNull(opts.model) || (models.tiers.cheap && models.tiers.cheap.model) || "haiku";
|
|
1591
|
+
let maxUsd = 0.5;
|
|
1592
|
+
if (opts["max-usd"] !== undefined) {
|
|
1593
|
+
maxUsd = Number(opts["max-usd"]);
|
|
1594
|
+
if (!Number.isFinite(maxUsd) || maxUsd <= 0) fail(`${usage}\n--max-usd must be a positive number, got ${JSON.stringify(opts["max-usd"])}.`, 2);
|
|
1595
|
+
}
|
|
1596
|
+
const claudeBin = textOrNull(opts["claude-bin"]) || process.env.DOUG_CLAUDE_BIN || "claude";
|
|
1597
|
+
const dryRun = !!opts["dry-run"];
|
|
1598
|
+
if (!existsSync(lib.memoryPath(dir))) fail("no memory store: nothing to build a project model from");
|
|
1599
|
+
const m = lib.openMemory(dir);
|
|
1600
|
+
let call;
|
|
1601
|
+
let checked;
|
|
1602
|
+
let dryPrompt = null;
|
|
1603
|
+
try {
|
|
1604
|
+
const lessons = lib.listLessons(m);
|
|
1605
|
+
const prompt = lib.buildProjectModelPrompt({ lessons, runs: lib.listRunHistoryRefs(dir) });
|
|
1606
|
+
if (dryRun) {
|
|
1607
|
+
dryPrompt = prompt;
|
|
1608
|
+
} else {
|
|
1609
|
+
try {
|
|
1610
|
+
call = await makePropose({ claudeBin, model, maxUsd, dir, schema: lib.PROJECT_MODEL_SCHEMA, resultKey: "statements" })(prompt);
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
fail(`model regen: the model call failed: ${err.message}`);
|
|
1613
|
+
}
|
|
1614
|
+
checked = lib.checkProjectModelCitations(m, dir, call.statements);
|
|
1615
|
+
}
|
|
1616
|
+
} finally {
|
|
1617
|
+
m.close();
|
|
1618
|
+
}
|
|
1619
|
+
if (dryRun) {
|
|
1620
|
+
if (opts.json) {
|
|
1621
|
+
process.stdout.write(JSON.stringify({ path: modelPath, dryRun: true, prompt: dryPrompt, usd: 0, usdSource: "none" }, null, 2) + "\n");
|
|
1622
|
+
} else {
|
|
1623
|
+
process.stdout.write(`(dry run) no model call; the prompt that would be sent:\n${dryPrompt}\n`);
|
|
1624
|
+
process.stdout.write(`Would write ${lib.PROJECT_MODEL_RELPATH} from the model's statements; ${costLine(0, "none")}\n`);
|
|
1625
|
+
}
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
const cost = lib.computeReflectCost({ dir, sessionId: call.sessionId, totalCostUsd: call.totalCostUsd });
|
|
1629
|
+
const rendered = lib.renderProjectModel(checked.kept);
|
|
1630
|
+
const counts = `dropped ${checked.dropped.length} unresolved, ${rendered.overCap.length} over cap`;
|
|
1631
|
+
if (!rendered.kept.length) {
|
|
1632
|
+
fail(`model regen: nothing was written: no statement survived (${call.statements.length} proposed; ${counts}); ${costLine(cost.usd, cost.usdSource)}`);
|
|
1633
|
+
}
|
|
1634
|
+
if (!dryRun) {
|
|
1635
|
+
mkdirSync(dirname(modelPath), { recursive: true });
|
|
1636
|
+
writeFileSync(modelPath, rendered.text, "utf8");
|
|
1637
|
+
}
|
|
1638
|
+
if (opts.json) {
|
|
1639
|
+
process.stdout.write(
|
|
1640
|
+
JSON.stringify(
|
|
1641
|
+
{
|
|
1642
|
+
path: modelPath,
|
|
1643
|
+
statements: rendered.kept.length,
|
|
1644
|
+
chars: rendered.chars,
|
|
1645
|
+
cap: lib.PROJECT_MODEL_CHAR_CAP,
|
|
1646
|
+
unresolved: checked.dropped.length,
|
|
1647
|
+
overCap: rendered.overCap.length,
|
|
1648
|
+
dropped: checked.dropped,
|
|
1649
|
+
usd: cost.usd,
|
|
1650
|
+
usdSource: cost.usdSource,
|
|
1651
|
+
dryRun,
|
|
1652
|
+
},
|
|
1653
|
+
null,
|
|
1654
|
+
2
|
|
1655
|
+
) + "\n"
|
|
1656
|
+
);
|
|
1657
|
+
break;
|
|
1034
1658
|
}
|
|
1659
|
+
if (dryRun) process.stdout.write(rendered.text + "\n");
|
|
1660
|
+
process.stdout.write(
|
|
1661
|
+
`${dryRun ? "Would write" : "Wrote"} ${lib.PROJECT_MODEL_RELPATH}: ${rendered.kept.length} statements, ${rendered.chars} characters (cap ${lib.PROJECT_MODEL_CHAR_CAP}); ${counts}; ${costLine(cost.usd, cost.usdSource)}\n`
|
|
1662
|
+
);
|
|
1035
1663
|
break;
|
|
1036
1664
|
}
|
|
1037
1665
|
case "doctor": {
|
|
@@ -1090,7 +1718,7 @@ try {
|
|
|
1090
1718
|
if (opts.check && opts["no-check"]) {
|
|
1091
1719
|
fail(`${usage}\n--check and --no-check are mutually exclusive.`, 2);
|
|
1092
1720
|
}
|
|
1093
|
-
const { openMemory, embedLessons, lessonsNeedingEmbedding } = await import("../lib/memory.mjs");
|
|
1721
|
+
const { openMemory, embedLessons, lessonsNeedingEmbedding, LIVE_LESSON_SQL } = await import("../lib/memory.mjs");
|
|
1094
1722
|
const m = openMemory(dir);
|
|
1095
1723
|
let result;
|
|
1096
1724
|
let checkStatus;
|
|
@@ -1100,7 +1728,7 @@ try {
|
|
|
1100
1728
|
if (opts["no-check"]) {
|
|
1101
1729
|
process.stderr.write("embed-check: skipped (--no-check)\n");
|
|
1102
1730
|
checkStatus = "skipped";
|
|
1103
|
-
} else if (opts.check || embeddingChangeDetected(m, provider)) {
|
|
1731
|
+
} else if (opts.check || embeddingChangeDetected(m, provider, LIVE_LESSON_SQL)) {
|
|
1104
1732
|
const pairs = loadPairsOrFail(opts);
|
|
1105
1733
|
const check = await checkEmbeddingSanity(provider, pairs);
|
|
1106
1734
|
if (!check.ok) {
|
|
@@ -1118,7 +1746,7 @@ try {
|
|
|
1118
1746
|
onProgress: ({ embedded, total }) => process.stderr.write(`embedded ${embedded}/${total}\n`),
|
|
1119
1747
|
});
|
|
1120
1748
|
stillMissing = lessonsNeedingEmbedding(m, { model: provider.model, dims: provider.dims }).length;
|
|
1121
|
-
liveTotal = m.prepare(
|
|
1749
|
+
liveTotal = m.prepare(`SELECT COUNT(*) AS n FROM lessons WHERE ${LIVE_LESSON_SQL}`).get().n;
|
|
1122
1750
|
} finally {
|
|
1123
1751
|
m.close();
|
|
1124
1752
|
}
|
|
@@ -1359,7 +1987,7 @@ try {
|
|
|
1359
1987
|
if (headRes.status !== 0 || !headRes.stdout.trim()) {
|
|
1360
1988
|
fail(`cannot open condition for "${id}": no git HEAD in ${dir}${headRes.stderr && headRes.stderr.trim() ? ` (${headRes.stderr.trim()})` : " (not a git repository, or no commits yet)"}.`);
|
|
1361
1989
|
}
|
|
1362
|
-
const { conditionConfigHash } = await import("../lib/memory.mjs");
|
|
1990
|
+
const { conditionConfigHash, memoryConfig } = await import("../lib/memory.mjs");
|
|
1363
1991
|
const written = {
|
|
1364
1992
|
harnessCommit: headRes.stdout.trim(),
|
|
1365
1993
|
configHash: conditionConfigHash(dir),
|
|
@@ -1368,6 +1996,7 @@ try {
|
|
|
1368
1996
|
assignedBy: "policy",
|
|
1369
1997
|
exploreProbability: null,
|
|
1370
1998
|
openedAt: new Date().toISOString(),
|
|
1999
|
+
memoryConfig: memoryConfig({ embeddings: readMemoryConfig(dir).embeddings, staleDays: readMemoryStaleDays(dir), track: "hand" }),
|
|
1371
2000
|
};
|
|
1372
2001
|
mkdirSync(dirname(file), { recursive: true });
|
|
1373
2002
|
writeFileSync(file, JSON.stringify(written, null, 2) + "\n");
|
|
@@ -1451,7 +2080,7 @@ try {
|
|
|
1451
2080
|
break;
|
|
1452
2081
|
}
|
|
1453
2082
|
default:
|
|
1454
|
-
fail("usage: memory.mjs <record|reflect|outcomes|lessons|lesson|import|recall|brief-lessons|doctor|reembed|embed-check|index|condition|decision|rule|claude-md> ...", 2);
|
|
2083
|
+
fail("usage: memory.mjs <record|reflect|outcomes|arms|lessons|lesson|history|model|import|recall|brief-lessons|doctor|reembed|embed-check|index|condition|decision|rule|claude-md> ...", 2);
|
|
1455
2084
|
}
|
|
1456
2085
|
} catch (err) {
|
|
1457
2086
|
fail(err.message);
|