@nurix/etna 0.15.7 → 0.15.8

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 (86) hide show
  1. package/bin/etna.js +64 -13
  2. package/data/harness-index.json +7 -5
  3. package/data/harnesses/code/CLAUDE.md +3 -7
  4. package/data/harnesses/code/agents/harness-manager.md +4 -4
  5. package/data/harnesses/code/agents/maintenance.md +1 -1
  6. package/data/harnesses/code/harness.json +4 -3
  7. package/data/harnesses/code/hooks/hooks.json +3 -17
  8. package/data/harnesses/code/hooks/prompt-recall.js +157 -0
  9. package/data/harnesses/code/hooks/session-reflect.js +283 -0
  10. package/data/harnesses/code/skills/distiller/SKILL.md +62 -60
  11. package/data/harnesses/code/skills/distiller/references/docs-mode.md +21 -21
  12. package/data/harnesses/code/skills/distiller/references/method.md +41 -41
  13. package/data/harnesses/code/skills/documentation/references/templates.md +20 -27
  14. package/data/harnesses/code-fable/CLAUDE.md +138 -0
  15. package/data/harnesses/code-fable/agents/brainstorm.md +35 -0
  16. package/data/harnesses/code-fable/agents/change-review.md +249 -0
  17. package/data/harnesses/code-fable/agents/harness-manager.md +193 -0
  18. package/data/harnesses/code-fable/agents/maintenance.md +123 -0
  19. package/data/harnesses/code-fable/agents/performance-review.md +218 -0
  20. package/data/harnesses/code-fable/agents/spec-mode.md +56 -0
  21. package/data/harnesses/code-fable/harness.json +7 -0
  22. package/data/harnesses/code-fable/hooks/hooks.json +27 -0
  23. package/data/harnesses/code-fable/hooks/lib/redact.js +37 -0
  24. package/data/harnesses/code-fable/hooks/prompt-recall.js +157 -0
  25. package/data/harnesses/code-fable/hooks/session-reflect.js +283 -0
  26. package/data/harnesses/code-fable/rules/general.md +96 -0
  27. package/data/harnesses/code-fable/rules/nomenclature.md +34 -0
  28. package/data/harnesses/code-fable/skills/boundary/SKILL.md +262 -0
  29. package/data/harnesses/code-fable/skills/boundary/references/boundary-taxonomy.md +734 -0
  30. package/data/harnesses/code-fable/skills/brainstorm/README.md +136 -0
  31. package/data/harnesses/code-fable/skills/brainstorm/SKILL.md +135 -0
  32. package/data/harnesses/code-fable/skills/brainstorm/harvest.md +26 -0
  33. package/data/harnesses/code-fable/skills/brainstorm/templates/backlog.md +39 -0
  34. package/data/harnesses/code-fable/skills/brainstorm/templates/build.md +37 -0
  35. package/data/harnesses/code-fable/skills/brainstorm/templates/decisions.md +29 -0
  36. package/data/harnesses/code-fable/skills/brainstorm/templates/doctrine.md +31 -0
  37. package/data/harnesses/code-fable/skills/brainstorm/templates/domain-model.md +25 -0
  38. package/data/harnesses/code-fable/skills/brainstorm/templates/glossary.md +19 -0
  39. package/data/harnesses/code-fable/skills/brainstorm/templates/milestones.md +43 -0
  40. package/data/harnesses/code-fable/skills/brainstorm/templates/non-goals.md +21 -0
  41. package/data/harnesses/code-fable/skills/brainstorm/templates/plan-doc.md +61 -0
  42. package/data/harnesses/code-fable/skills/brainstorm/templates/questions.md +35 -0
  43. package/data/harnesses/code-fable/skills/brainstorm/templates/references.md +28 -0
  44. package/data/harnesses/code-fable/skills/brainstorm/templates/rejected.md +29 -0
  45. package/data/harnesses/code-fable/skills/brainstorm/templates/state.md +8 -0
  46. package/data/harnesses/code-fable/skills/brainstorm/templates/tech.md +23 -0
  47. package/data/harnesses/code-fable/skills/brainstorm/templates/thesis.md +21 -0
  48. package/data/harnesses/code-fable/skills/distiller/SKILL.md +219 -0
  49. package/data/harnesses/code-fable/skills/distiller/references/docs-mode.md +526 -0
  50. package/data/harnesses/code-fable/skills/distiller/references/method.md +166 -0
  51. package/data/harnesses/code-fable/skills/documentation/SKILL.md +127 -0
  52. package/data/harnesses/code-fable/skills/documentation/references/examples.md +193 -0
  53. package/data/harnesses/code-fable/skills/documentation/references/style-guide.md +288 -0
  54. package/data/harnesses/code-fable/skills/documentation/references/templates.md +942 -0
  55. package/data/harnesses/code-fable/skills/workflow-mapper/SKILL.md +430 -0
  56. package/data/harnesses/code-fable/skills/workflow-mapper/references/patterns.md +214 -0
  57. package/data/harnesses/{re → discover}/CLAUDE.md +48 -33
  58. package/data/harnesses/{re → discover}/agents/api.md +3 -3
  59. package/data/harnesses/{re → discover}/agents/codebase.md +3 -3
  60. package/data/harnesses/{re → discover}/agents/community.md +3 -3
  61. package/data/harnesses/{re → discover}/agents/deployed-client-bundle.md +5 -5
  62. package/data/harnesses/{re → discover}/agents/distribution-artifacts.md +4 -4
  63. package/data/harnesses/{re → discover}/agents/docs.md +3 -3
  64. package/data/harnesses/{re → discover}/agents/infra-backend-fingerprint.md +2 -2
  65. package/data/harnesses/{re → discover}/agents/packages.md +3 -3
  66. package/data/harnesses/{re → discover}/agents/session.md +19 -11
  67. package/data/harnesses/{re → discover}/agents/website.md +3 -3
  68. package/data/harnesses/{re → discover}/agents/wire-capture.md +9 -7
  69. package/data/harnesses/discover/harness.json +7 -0
  70. package/data/harnesses/{re/rules/re-discovery.md → discover/rules/discovery.md} +54 -17
  71. package/data/harnesses/{re/rules/re-evaluation.md → discover/rules/evaluation.md} +12 -1
  72. package/data/harnesses/{re/rules/re-ingestion.md → discover/rules/ingestion.md} +192 -23
  73. package/data/harnesses/discover/rules/self-correction.md +177 -0
  74. package/data/harnesses/discover/rules/tradecraft.md +532 -0
  75. package/data/harnesses/hr/CLAUDE.md +91 -0
  76. package/data/harnesses/hr/harness.json +7 -0
  77. package/data/harnesses/hr/skills/interview-eval/SKILL.md +118 -0
  78. package/data/harnesses/hr/skills/interview-eval/TEMPLATE.md +124 -0
  79. package/data/harnesses/hr/skills/interview-eval/extract_session.py +418 -0
  80. package/data/harnesses/hr/skills/interview-eval/rubric.md +219 -0
  81. package/data/harnesses/kb-agi/harness.json +1 -0
  82. package/package.json +1 -1
  83. package/data/harnesses/code/hooks/post-commit.js +0 -136
  84. package/data/harnesses/code/hooks/post-tool-failure.js +0 -136
  85. package/data/harnesses/code/hooks/session-stop.js +0 -207
  86. package/data/harnesses/re/harness.json +0 -6
@@ -0,0 +1,283 @@
1
+ // plugins/code/hooks/session-reflect.js — SessionEnd hook: reflect the session into durable lessons (headless haiku pass) appended to the actor's memory inbox.
2
+
3
+ "use strict";
4
+
5
+ const fs = require("fs");
6
+ const os = require("os");
7
+ const path = require("path");
8
+ const { execFileSync, spawn } = require("child_process");
9
+ const { redact } = require("./lib/redact.js");
10
+
11
+ // A session below all three thresholds carries no lesson worth a model call.
12
+ const MIN_TURNS = 8;
13
+ const MAX_MESSAGE_CHARS = 1500;
14
+ const MAX_DIGEST_CHARS = 80000;
15
+ const MAX_LESSONS = 6;
16
+
17
+ // Read all of stdin synchronously; returns "" if nothing is piped.
18
+ function readStdin() {
19
+ try {
20
+ return fs.readFileSync(0, "utf8");
21
+ } catch (e) {
22
+ return "";
23
+ }
24
+ }
25
+
26
+ // Resolve the project root: CLAUDE_PROJECT_DIR env, else payload cwd, else process cwd.
27
+ function resolveProjectDir(payload) {
28
+ return process.env.CLAUDE_PROJECT_DIR || (payload && payload.cwd) || process.cwd();
29
+ }
30
+
31
+ // Run a git command in projectDir; returns trimmed stdout or fallback on any error.
32
+ function git(projectDir, args, fallback) {
33
+ try {
34
+ return execFileSync("git", ["-C", projectDir, ...args], {
35
+ encoding: "utf8",
36
+ stdio: ["ignore", "pipe", "ignore"],
37
+ }).trim() || fallback;
38
+ } catch (e) {
39
+ return fallback;
40
+ }
41
+ }
42
+
43
+ // Slug a raw actor name into kebab-case ASCII.
44
+ function slugActor(name) {
45
+ return String(name || "")
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, "-")
48
+ .replace(/^-+|-+$/g, "");
49
+ }
50
+
51
+ // Determine the actor: git user.name (slugged) → $USER → "agent".
52
+ function resolveActor(projectDir) {
53
+ const fromGit = slugActor(git(projectDir, ["config", "user.name"], ""));
54
+ if (fromGit) return fromGit;
55
+ const fromEnv = slugActor(process.env.USER || "");
56
+ if (fromEnv) return fromEnv;
57
+ return "agent";
58
+ }
59
+
60
+ // Truncate a single line to n chars with an ellipsis marker.
61
+ function clip(text, n) {
62
+ const clean = String(text || "").replace(/\s+/g, " ").trim();
63
+ return clean.length > n ? clean.slice(0, n) + "…" : clean;
64
+ }
65
+
66
+ // Extract plain text from a message whose content is a string or content-block array.
67
+ function extractText(msg) {
68
+ if (!msg || !msg.content) return "";
69
+ if (typeof msg.content === "string") return msg.content;
70
+ if (Array.isArray(msg.content)) {
71
+ return msg.content
72
+ .filter((b) => b && b.type === "text" && typeof b.text === "string")
73
+ .map((b) => b.text)
74
+ .join(" ");
75
+ }
76
+ return "";
77
+ }
78
+
79
+ // Strip harness-injected wrappers so only what the user actually typed survives.
80
+ function stripInjected(text) {
81
+ return String(text || "")
82
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
83
+ .replace(/<(local-command-caveat|local-command-stdout|local-command-stderr|command-name|command-message|command-args|command-contents)>[\s\S]*?<\/\1>/g, "")
84
+ .trim();
85
+ }
86
+
87
+ // Scan an assistant message's tool_use blocks for edited files and git commits.
88
+ function collectToolUse(msg, summary) {
89
+ if (!msg || !Array.isArray(msg.content)) return;
90
+ for (const block of msg.content) {
91
+ if (!block || block.type !== "tool_use") continue;
92
+ const name = block.name || "";
93
+ const input = block.input || {};
94
+ if (/^(Edit|Write|MultiEdit|NotebookEdit)$/.test(name) && input.file_path) {
95
+ summary.files.add(input.file_path);
96
+ }
97
+ if (name === "Bash" && typeof input.command === "string" && /\bgit\s+commit\b/.test(input.command)) {
98
+ summary.sawCommit = true;
99
+ }
100
+ }
101
+ }
102
+
103
+ // Walk the JSONL transcript once: turn/edit/commit counts plus a USER/ASSISTANT text digest.
104
+ function digestTranscript(transcriptPath) {
105
+ const out = { turns: 0, files: new Set(), sawCommit: false, lines: [] };
106
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return out;
107
+ for (const line of fs.readFileSync(transcriptPath, "utf8").split("\n")) {
108
+ if (!line.trim()) continue;
109
+ let row;
110
+ try {
111
+ row = JSON.parse(line);
112
+ } catch (e) {
113
+ continue;
114
+ }
115
+ if (row.isSidechain) continue;
116
+ const msg = row && row.message ? row.message : row;
117
+ const role = (msg && msg.role) || row.type;
118
+ if (role === "user") {
119
+ const text = stripInjected(extractText(msg));
120
+ if (!text || text.startsWith("Caveat: The messages below")) continue;
121
+ out.lines.push("USER: " + clip(text, MAX_MESSAGE_CHARS));
122
+ } else if (role === "assistant") {
123
+ out.turns += 1;
124
+ collectToolUse(msg, out);
125
+ const text = extractText(msg);
126
+ if (text) out.lines.push("ASSISTANT: " + clip(text, MAX_MESSAGE_CHARS));
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+
132
+ // Compute the per-actor inbox file path.
133
+ function inboxPath(projectDir, actor) {
134
+ const dir = path.join(projectDir, "docs", "memory", "inbox");
135
+ return { dir, file: path.join(dir, `${actor}.md`) };
136
+ }
137
+
138
+ // True when this session already wrote an inbox entry (SessionEnd re-fires on resume).
139
+ function alreadyReflected(file, sessionId) {
140
+ try {
141
+ return Boolean(sessionId) && fs.existsSync(file) && fs.readFileSync(file, "utf8").includes(`session ${sessionId}`);
142
+ } catch (e) {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ // Build the inbox file header block.
148
+ function inboxHeader(actor) {
149
+ return `# Memory inbox — ${actor}\nSession lessons awaiting distillation — appended by the session-reflect hook, consumed by \`distiller facts\`.\n`;
150
+ }
151
+
152
+ // The full output contract sits AFTER the transcript — with it only up front, the model
153
+ // continues the conversation in the transcript instead of reflecting on it.
154
+ const PROMPT_HEADER = [
155
+ "You are a memory reflector. Below is the transcript of a FINISHED coding session,",
156
+ "delimited by <transcript> tags. Do not reply to it or continue it — you will be asked",
157
+ "to extract durable lessons from it after the closing tag.",
158
+ "",
159
+ "<transcript>",
160
+ "",
161
+ ].join("\n");
162
+
163
+ const PROMPT_FOOTER = [
164
+ "",
165
+ "</transcript>",
166
+ "",
167
+ "The session above is over. Extract ONLY durable lessons a future session in this",
168
+ "repository would benefit from knowing:",
169
+ "- corrections or preferences the user stated",
170
+ "- failures whose root cause was found, with the fix",
171
+ "- decisions made, with their rationale",
172
+ "- discovered constraints or non-obvious facts about the project or its tooling",
173
+ "Rules:",
174
+ "- Most sessions contain none. If nothing qualifies, output exactly: NONE",
175
+ `- Otherwise output at most ${MAX_LESSONS} bullets and nothing else — no preamble, no headers.`,
176
+ '- Each bullet starts with "- [project]" (true of this repo or its tooling) or "- [user]" (true of this user).',
177
+ "- Each bullet is one complete, self-contained sentence with names and identifiers spelled out.",
178
+ "- Routine work (commits made, files edited, tasks completed) is NOT a lesson. Never include secrets.",
179
+ ].join("\n");
180
+
181
+ // Hook mode: stash the payload and hand off to a detached worker so session exit is never delayed.
182
+ function hook() {
183
+ const raw = readStdin();
184
+ let payload = {};
185
+ try {
186
+ payload = raw ? JSON.parse(raw) : {};
187
+ } catch (e) {
188
+ payload = {};
189
+ }
190
+ if (!payload.transcript_path) return;
191
+
192
+ const stash = {
193
+ projectDir: resolveProjectDir(payload),
194
+ transcriptPath: payload.transcript_path,
195
+ sessionId: payload.session_id || "",
196
+ };
197
+ const stashFile = path.join(os.tmpdir(), `etna-reflect-${stash.sessionId || process.pid}.json`);
198
+ fs.writeFileSync(stashFile, JSON.stringify(stash), "utf8");
199
+ spawn(process.execPath, [__filename, "--worker", stashFile], { detached: true, stdio: "ignore" }).unref();
200
+ }
201
+
202
+ // Worker mode: filter, digest, run the reflection model, append surviving lessons to the inbox.
203
+ function worker(stashFile) {
204
+ let stash;
205
+ try {
206
+ stash = JSON.parse(fs.readFileSync(stashFile, "utf8"));
207
+ } finally {
208
+ try {
209
+ fs.rmSync(stashFile, { force: true });
210
+ } catch (e) {
211
+ // Stale stash files are harmless.
212
+ }
213
+ }
214
+
215
+ const { projectDir, transcriptPath, sessionId } = stash;
216
+ const digest = digestTranscript(transcriptPath);
217
+ if (digest.turns < MIN_TURNS && digest.files.size === 0 && !digest.sawCommit) return;
218
+
219
+ const actor = resolveActor(projectDir);
220
+ const { dir, file } = inboxPath(projectDir, actor);
221
+ if (alreadyReflected(file, sessionId)) return;
222
+
223
+ let text = redact(digest.lines.join("\n"));
224
+ if (text.length > MAX_DIGEST_CHARS) {
225
+ text = text.slice(0, 8000) + "\n[... middle of session elided ...]\n" + text.slice(-(MAX_DIGEST_CHARS - 8000));
226
+ }
227
+
228
+ const output = execFileSync("claude", ["-p", "--model", "haiku"], {
229
+ input: PROMPT_HEADER + text + PROMPT_FOOTER,
230
+ encoding: "utf8",
231
+ cwd: os.tmpdir(),
232
+ env: Object.assign({}, process.env, { ETNA_REFLECT: "1" }),
233
+ timeout: 240000,
234
+ maxBuffer: 4 * 1024 * 1024,
235
+ stdio: ["pipe", "pipe", "ignore"],
236
+ });
237
+
238
+ const lessons = output
239
+ .split("\n")
240
+ .map((l) => l.trim())
241
+ .filter((l) => /^- \[(project|user)\]\s+\S/.test(l))
242
+ .slice(0, MAX_LESSONS)
243
+ .map((l) => redact(l));
244
+ if (!lessons.length) return;
245
+
246
+ const repo = path.basename(projectDir);
247
+ const branch = git(projectDir, ["rev-parse", "--abbrev-ref", "HEAD"], "-");
248
+ const now = new Date();
249
+ const pad = (n) => String(n).padStart(2, "0");
250
+ const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
251
+
252
+ fs.mkdirSync(dir, { recursive: true });
253
+ if (alreadyReflected(file, sessionId)) return;
254
+ const exists = fs.existsSync(file);
255
+ const block =
256
+ (exists ? "\n" : inboxHeader(actor) + "\n") +
257
+ `## ${stamp} · session ${sessionId || "unknown"} · ${repo}@${branch}\n` +
258
+ lessons.join("\n") +
259
+ "\n";
260
+ fs.appendFileSync(file, block, "utf8");
261
+ }
262
+
263
+ function main() {
264
+ if (process.env.ETNA_REFLECT) return;
265
+ const workerIdx = process.argv.indexOf("--worker");
266
+ if (workerIdx !== -1) {
267
+ worker(process.argv[workerIdx + 1]);
268
+ } else {
269
+ hook();
270
+ }
271
+ }
272
+
273
+ try {
274
+ main();
275
+ } catch (e) {
276
+ try {
277
+ process.stderr.write(`session-reflect hook: ${e && e.message ? e.message : e}\n`);
278
+ } catch (e2) {
279
+ // Nothing more we can do.
280
+ }
281
+ } finally {
282
+ process.exit(0);
283
+ }
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: distiller
3
- description: The one place the raw event ledger is distilled into colder, higher-signal knowledge. One skill, two modes — `facts` (ledger → atomic provenance-linked fact-cards in `docs/memory/facts/`) and `docs` (fact-cards → proposed, human-approved doc deltas). Both modes read one shared algorithm spec, `references/method.md` (surprisal filter, EDU fact-cards, reconciliation, bi-temporal supersession — all stolen, all cited), so they can never drift apart. NEVER edits the ledger (append-only audit substrate); NEVER auto-runs (invoked by the user, or by accepting the maintenance agent's threshold proposal). Triggers on "distill the ledger", "distill facts", "run the distiller", "distill into fact-cards", "facts from the ledger", "distill docs", "draft doc deltas", "distiller docs", "/distiller".
3
+ description: The one place captured session lessons are distilled into colder, higher-signal knowledge. One skill, two modes — `facts` (memory inbox → atomic provenance-linked fact-cards in `docs/memory/facts/`) and `docs` (fact-cards → proposed, human-approved doc deltas). Both modes read one shared algorithm spec, `references/method.md` (surprisal filter, EDU fact-cards, reconciliation, bi-temporal supersession — all stolen, all cited), so they can never drift apart. Consumes the inbox entries it digests (the inbox is a queue, not an audit log — git history and commit/session provenance are the audit trail); NEVER auto-runs (invoked by the user, or by accepting the maintenance agent's threshold proposal). Triggers on "distill the inbox", "distill facts", "run the distiller", "distill into fact-cards", "facts from the inbox", "distill docs", "draft doc deltas", "distiller docs", "/distiller".
4
4
  ---
5
5
 
6
6
  # Distiller Skill
7
7
 
8
- The one place a repo's **raw event ledger** is distilled into colder,
9
- higher-signal knowledge. The ledger (`docs/memory/ledger/`) is a high-volume,
10
- append-only stream of what happened; the distiller turns the *surprising* part of
11
- it into atomic, self-contained, provenance-linked **fact-cards**
12
- (`docs/memory/facts/`). Volume falls, signal rises — one hop down the
13
- distillation gradient.
8
+ The one place a repo's **captured session lessons** are distilled into colder,
9
+ higher-signal knowledge. The memory inbox (`docs/memory/inbox/{actor}.md`) is a
10
+ per-actor queue of lessons the `session-reflect` hook extracted at session end;
11
+ the distiller reconciles the *surprising* part of it into atomic, self-contained,
12
+ provenance-linked **fact-cards** (`docs/memory/facts/`) and consumes the entries
13
+ it digests. Volume falls, signal rises — one hop down the distillation gradient.
14
14
 
15
15
  The methods this skill runs are **stolen, not invented** — surprisal-based
16
16
  retention (Nemori), non-lossy EDU fact-cards (EMem), per-fact ADD/UPDATE/
@@ -19,8 +19,8 @@ Graphiti). They live in exactly one place:
19
19
 
20
20
  > **Read [`references/method.md`](references/method.md) first, in every mode.**
21
21
  > It carries the four mechanisms verbatim-grade (with their arXiv ids — provenance
22
- > matters here of all places), the fact-card format, and the high-water-mark
23
- > protocol. This SKILL.md only orchestrates those mechanisms over the ledger.
22
+ > matters here of all places), the fact-card format, and the inbox-consumption
23
+ > protocol. This SKILL.md only orchestrates those mechanisms over the inbox.
24
24
 
25
25
  The reason the two modes are one skill: **both read one algorithm root.** Two
26
26
  components would mean two copies of the surprisal/reconciliation/supersession
@@ -32,7 +32,7 @@ model that drift apart. One skill, one spec, zero drift.
32
32
 
33
33
  | You want to… | Mode | Ships |
34
34
  |---|---|---|
35
- | Distill new ledger entries into fact-cards | **`facts`** | now |
35
+ | Distill new inbox entries into fact-cards | **`facts`** | now |
36
36
  | Distill fact-cards into proposed doc deltas | **`docs`** | now — procedure in [`references/docs-mode.md`](references/docs-mode.md) |
37
37
 
38
38
  Both modes read [`references/method.md`](references/method.md); each mode's
@@ -43,57 +43,55 @@ procedure that sequences it lives in its own section below.
43
43
  Runs **in the main session** — no standing sub-agents, no daemon, no cron, no
44
44
  watch. The harness **proposes**, the user **disposes**: this skill runs only when
45
45
  the user asks, when the **Completion flow** invokes it (the kit `CLAUDE.md`
46
- Completion step distils the just-finished plan's trail — `facts` if the ledger
47
- advanced past the high-water mark, then `docs` which still drafts for approval),
48
- or when the user accepts the `maintenance` agent's threshold proposal (≥20
49
- undigested ledger entries). It never schedules itself, never runs on a hook,
50
- never gates anything.
46
+ Completion step distils the just-finished plan's trail — `facts` if the inbox
47
+ holds entries, then `docs` which still drafts for approval), or when the user
48
+ accepts the `maintenance` agent's threshold proposal (≥5 undigested inbox
49
+ entries). It never schedules itself, never runs on a hook, never gates anything.
51
50
 
52
51
  ---
53
52
 
54
- ## Mode: `facts` (ledger → fact-cards)
53
+ ## Mode: `facts` (inbox → fact-cards)
55
54
 
56
- Distill ledger entries newer than the high-water mark into fact-cards. The
57
- algorithm is in [`references/method.md`](references/method.md) — follow it; this
58
- section is the procedure that sequences it.
55
+ Distill the inbox's undigested entries into fact-cards. The algorithm is in
56
+ [`references/method.md`](references/method.md) — follow it; this section is the
57
+ procedure that sequences it.
59
58
 
60
- 1. **Read the high-water mark.** Read `docs/memory/facts/.digest-state.md`
61
- (the frontmatter records the last-digested file + entry timestamp **per
62
- actor**). Absent ⇒ start of history — digest everything. Then collect ledger
63
- entries from `docs/memory/ledger/YYYY-MM-DD--{actor}.md` that are **newer**
64
- than the per-actor mark. This is what makes the run idempotent — an immediate
65
- second run finds nothing new and is a clean NOOP.
59
+ 1. **Collect the inbox.** Read every entry block
60
+ (`## <timestamp> · session <id> · <repo>@<branch>`) across
61
+ `docs/memory/inbox/*.md`. The inbox is a consume-on-digest queue — what is
62
+ present is exactly what is undigested, so there is no separate high-water
63
+ mark. An immediate second run finds an empty inbox and is a clean NOOP.
66
64
 
67
- **Thin or empty ledger ⇒ not a NOOP — distill the decision record.** A
68
- commit-less project, or one whose ledger holds only mechanical entries the
69
- surprisal filter would drop (a transient exit-1, a bare echo), is **not** a
70
- project with nothing to distill. Architecture decisions and reversals are
65
+ **Thin or empty inbox ⇒ not a NOOP — distill the decision record.** A
66
+ project whose sessions produced no reflected lessons is **not** a project
67
+ with nothing to distill. Architecture decisions and reversals are
71
68
  high-surprisal by nature — they exist precisely because they were *not*
72
- predictable. So when the ledger is thin/empty, also ingest, as surprisal
69
+ predictable. So when the inbox is thin/empty, also ingest, as surprisal
73
70
  sources for steps 2–5:
74
71
  - `docs/architecture/adr/**` — each accepted ADR is a surprising decision
75
72
  (its `Decision` + **Options Considered** — the rejected-alternatives section
76
73
  in the `documentation` §10 ADR template — carry the prediction error).
77
74
  - `docs/memory/brainstorm.md` — owner pronouncements, reversals, and
78
- discovered constraints recorded outside the ledger.
79
- Provenance points at the ADR file / brainstorm section, not a ledger
80
- `#HH:MM`. These sources sit outside the high-water mark (which tracks ledger
81
- entries only) — reconciliation (step 4) is what keeps a re-run idempotent.
75
+ discovered constraints recorded outside the inbox.
76
+ Provenance points at the ADR file / brainstorm section, not a session id.
77
+ These sources are **never consumed** — reconciliation (step 4) is what keeps
78
+ a re-run over them idempotent.
82
79
 
83
80
  2. **Surprisal filter → survivors** (method §1). Present the LLM with (a) a
84
- summary of the *current* fact-set, (b) the new undigested entries. Keep only
81
+ summary of the *current* fact-set, (b) the undigested entries. Keep only
85
82
  what is **surprising** relative to (a). Predictable ⇒ dropped — no importance
86
- scores, no templates, prediction error is the only retention signal. (First
87
- run, empty fact-set: "predictable" = mechanical routine — a bare commit echo,
88
- a transient exit-1 with no lesson; keep decisions, reversals, discovered
89
- constraints, and failures with a cause worth remembering.)
83
+ scores, no templates, prediction error is the only retention signal. Inbox
84
+ entries arrive semi-distilled (the reflection hook already dropped routine
85
+ work), so the filter's job here is narrower: drop what the existing fact-set
86
+ already predicts. (First run, empty fact-set: keep decisions, reversals,
87
+ discovered constraints, and failures with a cause worth remembering.)
90
88
 
91
89
  3. **Decompose survivors → EDU candidates** (method §2). Each survivor becomes
92
90
  **one** atomic, self-contained statement — entity names normalized, minimal
93
- context baked into the sentence — with `provenance` pointing back at the
94
- ledger entries/commits it derives from. Decomposition, **not** summarization:
91
+ context baked into the sentence — with `provenance` pointing at the session
92
+ ids and commits its inbox entry names. Decomposition, **not** summarization:
95
93
  one survivor may yield several cards; no information is destroyed; everything
96
- stays auditable back to raw.
94
+ stays auditable back to its session and commits.
97
95
 
98
96
  4. **Reconcile each candidate against existing cards** (method §3). Query the
99
97
  existing facts for semantic neighbors, then decide
@@ -132,35 +130,39 @@ section is the procedure that sequences it.
132
130
  See method.md §4 "Self-contained reversal."
133
131
  - **NOOP** → the fact is already known; write nothing.
134
132
 
135
- 6. **Update the high-water mark LAST.** Only after every card is written, update
136
- `docs/memory/facts/.digest-state.md` to the newest entry timestamp processed
137
- per actor. Last, so a crash mid-run re-digests rather than silently skips.
133
+ 6. **Consume the digested entries LAST.** Only after every card is written,
134
+ delete exactly the entry blocks this run digested from
135
+ `docs/memory/inbox/*.md` (keep each file's header; never touch undigested
136
+ blocks). Last, so a crash mid-run leaves the entries in place and the re-run
137
+ re-digests them — reconciliation NOOPs the already-written cards. This
138
+ consumption is the only bookkeeping; there is no `.digest-state.md`.
138
139
 
139
140
  7. **If qmd present, refresh the lens** (skip silently if `command -v qmd`
140
141
  finds nothing):
141
142
  - `qmd collection add docs/memory/facts --name {repo}-facts` — register the
142
143
  collection. Re-adding an existing name exits non-zero with "already exists";
143
144
  **treat that as success.** Never register `docs/memory` as a whole — that
144
- would sweep the raw ledger into the index.
145
+ would sweep the raw inbox queue into the index.
145
146
  - `qmd context add qmd://{repo}-facts/ "Distilled recent decisions/observations, provenance-linked"`
146
147
  — upserts, safe to re-run.
147
148
  - `qmd update` — **re-scan the collection for the cards you just wrote.** In
148
149
  qmd 2.5.3 `collection add` on an already-registered name does NOT re-index
149
150
  new files; `qmd update` is what picks them up. Skipping this leaves the new
150
- cards invisible to retrieval (and to distill②).
151
+ cards invisible to retrieval (to the `prompt-recall` hook, and to distill②).
151
152
  - `qmd embed -c {repo}-facts` — regenerate vectors for the new/changed cards.
152
153
  Always scope with `-c`; a bare `qmd embed` re-embeds every repo on the
153
- machine.
154
+ machine. The `prompt-recall` hook's vector fallback only sees embedded
155
+ cards, so skipping this hides new cards from prompt-time recall.
154
156
 
155
157
  8. **Report** (in chat). Cards added / updated / superseded / NOOP'd, the
156
- high-water mark advance per actor, and — if reconciliation fell back to
158
+ entries consumed per actor, and — if reconciliation fell back to
157
159
  filename/grep (qmd absent, **or** `{repo}-facts` registered but holding no
158
160
  cards yet — the first-run-on-a-scaffolded-facts-dir case) — the one advisory
159
161
  line saying so. Fire this advisory whenever semantic retrieval was effectively
160
162
  unavailable, even if the collection appeared in `qmd collection list`: a
161
163
  registered-but-empty collection is not semantic retrieval. A run that finds
162
164
  nothing surprising is **valid**: say
163
- "no surprising entries since the mark," don't pad.
165
+ "no surprising entries in the inbox," don't pad.
164
166
 
165
167
  ---
166
168
 
@@ -179,14 +181,14 @@ It NEVER writes a doc on its own — it **drafts and presents**; the human dispo
179
181
 
180
182
  - **One algorithm root.** Every mode reads `references/method.md`. Extend the
181
183
  spec there — never inline a divergent copy of the four methods in a mode.
182
- - **Never edit the ledger.** `docs/memory/ledger/` is the append-only audit
183
- substrate. The distiller is **read-only** on it — no deletes, no rewrites, no
184
- reordering, not even archiving. It writes only under `docs/memory/facts/`.
185
- - **Idempotent via the mark.** The high-water mark is the only thing standing
186
- between this skill and reprocessing. Read it first, advance it last; a second
187
- run with no new entries is a NOOP.
184
+ - **Consume, never rewrite.** `docs/memory/inbox/` is a queue owned by the
185
+ `session-reflect` capture hook. The distiller deletes exactly the entry blocks
186
+ it digested this run — no edits, no reordering, no touching undigested blocks.
187
+ Beyond that consumption it writes only under `docs/memory/facts/`.
188
+ - **Idempotent via consumption.** What is in the inbox is what is undigested.
189
+ Consume last; a second run finds an empty inbox and is a NOOP.
188
190
  - **Cards are atomic and provenance-linked.** One self-contained statement per
189
- card; every card resolves back to the ledger entries/commits it came from.
191
+ card; every card resolves back to the session ids and commits it came from.
190
192
  Decompose — never lossily summarize.
191
193
  - **Supersede ≠ delete.** Bi-temporal only: set `invalid_from:` +
192
194
  `superseded_by:` on the old card and leave the file in place. Nothing is ever
@@ -208,9 +210,9 @@ It NEVER writes a doc on its own — it **drafts and presents**; the human dispo
208
210
 
209
211
  ## Output locations
210
212
 
211
- - `facts` writes fact-cards to `docs/memory/facts/{kebab-slug}.md` and the
212
- high-water mark to `docs/memory/facts/.digest-state.md`; the report goes to
213
- chat. It reads `docs/memory/ledger/` but never writes there.
213
+ - `facts` reads `docs/memory/inbox/`, writes fact-cards to
214
+ `docs/memory/facts/{kebab-slug}.md`, and deletes the consumed entry blocks
215
+ from the inbox; the report goes to chat.
214
216
  - `docs` presents doc deltas in chat and, **only on approval**, writes the accepted
215
217
  prose into the target doc under `docs/` (+ a new `docs/history/changelog/` entry if
216
218
  drafted) and appends `digested_into:` to each consumed card in `docs/memory/facts/`.
@@ -8,7 +8,7 @@ from `docs/memory/facts/` (warm, distilled cards) into proposed edits to `docs/`
8
8
  human-owned SSoT). It NEVER writes a doc on its own — it **drafts and presents**; the human
9
9
  disposes (P7).
10
10
 
11
- > `facts` mode = ledger → cards. `docs` mode = cards → *presented* doc deltas. One component,
11
+ > `facts` mode = inbox → cards. `docs` mode = cards → *presented* doc deltas. One component,
12
12
  > two modes, one algorithm root. distill② is the greenfield hop no surveyed tool has built
13
13
  > (prior art stops at index/retrieval); the discipline below is what keeps it from corrupting
14
14
  > documentation.
@@ -23,14 +23,14 @@ mechanically check every claim:
23
23
 
24
24
  > **Two-way traceability.** Every drafted delta *line* names the fact-card(s) it decomposes
25
25
  > from (forward: delta → card). Every consumed card names the doc(s) it was digested into
26
- > (backward: card → delta). Every card already names the ledger entries/commits it came from
27
- > (`provenance`, from `facts` mode). The chain **doc-line → card → ledger entry → commit**
26
+ > (backward: card → delta). Every card already names the sessions/commits it came from
27
+ > (`provenance`, from `facts` mode). The chain **doc-line → card → session/commit**
28
28
  > must resolve end to end with no orphan link. A delta line with no card, or a card marked
29
29
  > digested into a doc that does not contain its content, is a **defect**: the run fails its
30
30
  > acceptance check, the line is not presented.
31
31
 
32
32
  This mode introduces **no prose that is not a decomposition of a card** — decomposition-with-
33
- citation, exactly as `facts` mode decomposes ledger entries into cards. If the agent cannot
33
+ citation, exactly as `facts` mode decomposes inbox entries into cards. If the agent cannot
34
34
  cite a card for a sentence, that sentence is not written.
35
35
 
36
36
  ---
@@ -90,7 +90,7 @@ independently.
90
90
  > Each step names the failure mode(s) it closes. Ordering is load-bearing: the pillar check
91
91
  > precedes drafting (a belief-changing card never reaches the draft); the digestion mark is
92
92
  > written **last** (a crash re-presents rather than silently swallows — the mirror of `facts`
93
- > mode's "advance the high-water mark last").
93
+ > mode's "consume the digested entries last").
94
94
 
95
95
  ### 0. Read the roots
96
96
 
@@ -173,20 +173,20 @@ The output of this step is a **single presented delta document** (in chat, not w
173
173
  disk):
174
174
 
175
175
  ```
176
- TARGET: docs/features/memory-ledger.md
176
+ TARGET: docs/features/memory-capture.md
177
177
 
178
178
  ── PROPOSED DELTA ────────────────────────────────────────
179
179
  § Current Behavior (EXTEND)
180
180
  + The qmd retrieval lens is organized into per-subtree collections, with the
181
- raw ledger excluded from indexing by construction. [from: fact-qmd-optional-retrieval-lens]
181
+ memory inbox excluded from indexing by construction. [from: fact-qmd-optional-retrieval-lens]
182
182
 
183
183
  § Changelog entry (new — docs/history/changelog/2026-06-04-...md)
184
184
  ### Modified
185
185
  - capture-hook merge is now idempotent across re-install [from: fact-etna-hook-merge-idempotent]
186
186
 
187
187
  ── QUESTIONS (D7 phrasing — answer before accept) ────────
188
- Q1. docs/features/memory-ledger.md §Current Behavior says the index spans all of
189
- docs/memory; fact-qmd-optional-retrieval-lens says the raw ledger is excluded.
188
+ Q1. docs/features/memory-capture.md §Current Behavior says the index spans all of
189
+ docs/memory; fact-qmd-optional-retrieval-lens says the memory inbox is excluded.
190
190
  Confirm the doc is stale before I revise it — I will not overwrite it silently.
191
191
 
192
192
  ── PILLAR-CONFLICT ESCAPE (NOT a doc edit) ───────────────
@@ -238,7 +238,7 @@ Append the declined delta to `docs/history/backlog.md` at the finding's own P0
238
238
  (same scale as code work), deduped:
239
239
 
240
240
  ```
241
- - **[P2]** distiller-docs proposed updating docs/features/memory-ledger.md §Current Behavior from fact-qmd-optional-retrieval-lens (declined) — discovered by distiller docs
241
+ - **[P2]** distiller-docs proposed updating docs/features/memory-capture.md §Current Behavior from fact-qmd-optional-retrieval-lens (declined) — discovered by distiller docs
242
242
  ```
243
243
 
244
244
  Dedupe by (target doc § section, contributing card-ids): if an identical pending item exists,
@@ -266,11 +266,11 @@ broad capture, conservative gating.
266
266
 
267
267
  **(a) Structural intersection (exact, always evaluated).** True when ANY of:
268
268
 
269
- 1. **Provenance match (STRONG signal).** A path in the card's `provenance` matches a glob in
270
- the target's `owns`/`touches`. A `provenance` entry may be a `ledger/...#HH:MM` reference
271
- (the entry names files) or a `commit <sha>`. Resolve a commit's changed files with
272
- `git show --name-only <sha>` when the commit is local; a changed file under the doc's globs
273
- is a match. This is strong because the card derives from a commit that edited code the doc
269
+ 1. **Provenance match (STRONG signal).** A `commit <sha>` entry in the card's `provenance`
270
+ resolves to changed files (`git show --name-only <sha>` when the commit is local); a changed
271
+ file under the target's `owns`/`touches` globs is a match. (`session <id>` entries carry no
272
+ paths and do not participate; an ADR/brainstorm path matches the globs directly.) This is
273
+ strong because the card derives from a commit that edited code the doc
274
274
  owns.
275
275
  2. **Entity match (SUPPORTING signal).** A normalized name in the card's `entities` equals
276
276
  (case-insensitively, after kebab/camel normalization) a module/feature stem in an `owns`
@@ -338,10 +338,10 @@ valid_from: 2026-06-04
338
338
  invalid_from: null
339
339
  superseded_by: null
340
340
  digested_into: # ← added by docs mode, on accept, never on draft
341
- - docs/features/memory-ledger.md
341
+ - docs/features/memory-capture.md
342
342
  - docs/history/changelog/2026-06-04-qmd-lens.md
343
- entities: [qmd, retrieval-lens, per-subtree-collections, ledger]
344
- provenance: [ledger/2026-06-04--joyjeet-sarkar.md#00:31, commit d262e18]
343
+ entities: [qmd, retrieval-lens, per-subtree-collections, inbox]
344
+ provenance: [session e3c4a623, commit d262e18]
345
345
  ---
346
346
  ```
347
347
 
@@ -434,12 +434,12 @@ does not re-teach (the procedure lives in this file). Verbatim:
434
434
  > On Completion, after flipping the spec to `Active`, consult `distiller docs` for the just-completed feature's doc — it drafts any pending fact-card deltas for your approval (presented, never auto-written).
435
435
 
436
436
  **(b) Maintenance threshold — ONE line in `agents/maintenance.md` Step 7**, sibling to the
437
- existing ledger-distillation threshold (the `facts` analogue: ≥20 undigested *ledger entries*
437
+ existing inbox-distillation threshold (the `facts` analogue: ≥5 undigested *inbox entries*
438
438
  proposes a `facts` run). Verbatim:
439
439
 
440
440
  > **Docs-distillation threshold.** Per target doc, count undigested fact-cards (`invalid_from: null`, the doc's path not in their `digested_into:`) that **touch** it by Resolution 1's structural intersection (exact, qmd-independent). If **≥10** map to **one** doc, add one line to the report **proposing** a `distiller docs --doc <path>` run on that doc — propose only, never auto-run, no cron. Declined/ignored ⇒ the count keeps growing and the proposal reappears next pass; that recurrence is the dedupe, so never backlog the proposal itself.
441
441
 
442
- `N = 10` default (vs the ledger threshold's 20 — one hop colder, a lower bar). "Touch" uses the
442
+ `N = 10` default (vs the inbox threshold's 5 — inbox entries are rare and rich; cards accrue per doc). "Touch" uses the
443
443
  **structural** retriever only (deterministic, no qmd dependency in the maintenance loop).
444
444
  Per-doc count, not repo-wide: the proposal names the one doc, so the run is already scoped. This
445
445
  lands as a proposal emitter only — maintenance does not run the distiller (P19: distinct axis).
@@ -519,7 +519,7 @@ resulting doc edit re-enter normally.
519
519
  nothing.
520
520
  8. **Idempotent via the digestion mark.** An immediate re-run on the same doc finds its touching
521
521
  cards already `digested_into:` that doc and is a clean NOOP — the digestion mark is to `docs`
522
- mode what the high-water mark is to `facts` mode (F3).
522
+ mode what inbox consumption is to `facts` mode (F3).
523
523
  9. **Trigger budget.** Exactly one line added to `CLAUDE.md` Completion; exactly one line added to
524
524
  maintenance Step 7. No procedure inlined into either (P10).
525
525
  10. **Not autonomous.** No cron/daemon/hook; invoked by the Completion step, by accepting