@nurix/etna 0.15.7 → 0.16.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/bin/etna.js +64 -13
- package/data/harness-index.json +7 -5
- package/data/harnesses/code/CLAUDE.md +3 -7
- package/data/harnesses/code/agents/harness-manager.md +4 -4
- package/data/harnesses/code/agents/maintenance.md +1 -1
- package/data/harnesses/code/harness.json +4 -3
- package/data/harnesses/code/hooks/hooks.json +3 -17
- package/data/harnesses/code/hooks/prompt-recall.js +157 -0
- package/data/harnesses/code/hooks/session-reflect.js +283 -0
- package/data/harnesses/code/skills/distiller/SKILL.md +62 -60
- package/data/harnesses/code/skills/distiller/references/docs-mode.md +21 -21
- package/data/harnesses/code/skills/distiller/references/method.md +41 -41
- package/data/harnesses/code/skills/documentation/references/templates.md +20 -27
- package/data/harnesses/code-fable/CLAUDE.md +138 -0
- package/data/harnesses/code-fable/agents/brainstorm.md +35 -0
- package/data/harnesses/code-fable/agents/change-review.md +249 -0
- package/data/harnesses/code-fable/agents/harness-manager.md +193 -0
- package/data/harnesses/code-fable/agents/maintenance.md +123 -0
- package/data/harnesses/code-fable/agents/performance-review.md +218 -0
- package/data/harnesses/code-fable/agents/spec-mode.md +56 -0
- package/data/harnesses/code-fable/harness.json +7 -0
- package/data/harnesses/code-fable/hooks/hooks.json +27 -0
- package/data/harnesses/code-fable/hooks/lib/redact.js +37 -0
- package/data/harnesses/code-fable/hooks/prompt-recall.js +157 -0
- package/data/harnesses/code-fable/hooks/session-reflect.js +283 -0
- package/data/harnesses/code-fable/rules/general.md +96 -0
- package/data/harnesses/code-fable/rules/nomenclature.md +34 -0
- package/data/harnesses/code-fable/skills/boundary/SKILL.md +262 -0
- package/data/harnesses/code-fable/skills/boundary/references/boundary-taxonomy.md +734 -0
- package/data/harnesses/code-fable/skills/brainstorm/README.md +136 -0
- package/data/harnesses/code-fable/skills/brainstorm/SKILL.md +135 -0
- package/data/harnesses/code-fable/skills/brainstorm/harvest.md +26 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/backlog.md +39 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/build.md +37 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/decisions.md +29 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/doctrine.md +31 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/domain-model.md +25 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/glossary.md +19 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/milestones.md +43 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/non-goals.md +21 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/plan-doc.md +61 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/questions.md +35 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/references.md +28 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/rejected.md +29 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/state.md +8 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/tech.md +23 -0
- package/data/harnesses/code-fable/skills/brainstorm/templates/thesis.md +21 -0
- package/data/harnesses/code-fable/skills/distiller/SKILL.md +219 -0
- package/data/harnesses/code-fable/skills/distiller/references/docs-mode.md +526 -0
- package/data/harnesses/code-fable/skills/distiller/references/method.md +166 -0
- package/data/harnesses/code-fable/skills/documentation/SKILL.md +127 -0
- package/data/harnesses/code-fable/skills/documentation/references/examples.md +193 -0
- package/data/harnesses/code-fable/skills/documentation/references/style-guide.md +288 -0
- package/data/harnesses/code-fable/skills/documentation/references/templates.md +942 -0
- package/data/harnesses/code-fable/skills/workflow-mapper/SKILL.md +430 -0
- package/data/harnesses/code-fable/skills/workflow-mapper/references/patterns.md +214 -0
- package/data/harnesses/{re → discover}/CLAUDE.md +48 -33
- package/data/harnesses/{re → discover}/agents/api.md +3 -3
- package/data/harnesses/{re → discover}/agents/codebase.md +3 -3
- package/data/harnesses/{re → discover}/agents/community.md +3 -3
- package/data/harnesses/{re → discover}/agents/deployed-client-bundle.md +5 -5
- package/data/harnesses/{re → discover}/agents/distribution-artifacts.md +4 -4
- package/data/harnesses/{re → discover}/agents/docs.md +3 -3
- package/data/harnesses/{re → discover}/agents/infra-backend-fingerprint.md +2 -2
- package/data/harnesses/{re → discover}/agents/packages.md +3 -3
- package/data/harnesses/{re → discover}/agents/session.md +19 -11
- package/data/harnesses/{re → discover}/agents/website.md +3 -3
- package/data/harnesses/{re → discover}/agents/wire-capture.md +9 -7
- package/data/harnesses/discover/harness.json +7 -0
- package/data/harnesses/{re/rules/re-discovery.md → discover/rules/discovery.md} +54 -17
- package/data/harnesses/{re/rules/re-evaluation.md → discover/rules/evaluation.md} +12 -1
- package/data/harnesses/{re/rules/re-ingestion.md → discover/rules/ingestion.md} +192 -23
- package/data/harnesses/discover/rules/self-correction.md +177 -0
- package/data/harnesses/discover/rules/tradecraft.md +532 -0
- package/data/harnesses/hr/CLAUDE.md +91 -0
- package/data/harnesses/hr/harness.json +7 -0
- package/data/harnesses/hr/skills/interview-eval/SKILL.md +118 -0
- package/data/harnesses/hr/skills/interview-eval/TEMPLATE.md +124 -0
- package/data/harnesses/hr/skills/interview-eval/extract_session.py +418 -0
- package/data/harnesses/hr/skills/interview-eval/rubric.md +219 -0
- package/data/harnesses/kb-agi/harness.json +1 -0
- package/package.json +1 -1
- package/data/harnesses/code/hooks/post-commit.js +0 -136
- package/data/harnesses/code/hooks/post-tool-failure.js +0 -136
- package/data/harnesses/code/hooks/session-stop.js +0 -207
- 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
|
|
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 **
|
|
9
|
-
higher-signal knowledge. The
|
|
10
|
-
|
|
11
|
-
it into atomic, self-contained,
|
|
12
|
-
(`docs/memory/facts/`)
|
|
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
|
|
23
|
-
> protocol. This SKILL.md only orchestrates those mechanisms over the
|
|
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
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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` (
|
|
53
|
+
## Mode: `facts` (inbox → fact-cards)
|
|
55
54
|
|
|
56
|
-
Distill
|
|
57
|
-
|
|
58
|
-
|
|
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. **
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
79
|
-
Provenance points at the ADR file / brainstorm section, not a
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
94
|
-
|
|
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
|
|
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. **
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
- **
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
run
|
|
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
|
|
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`
|
|
212
|
-
|
|
213
|
-
|
|
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 =
|
|
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
|
|
27
|
-
> (`provenance`, from `facts` mode). The chain **doc-line → card →
|
|
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
|
|
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 "
|
|
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-
|
|
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
|
-
|
|
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-
|
|
189
|
-
docs/memory; fact-qmd-optional-retrieval-lens says the
|
|
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-
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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-
|
|
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,
|
|
344
|
-
provenance: [
|
|
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
|
|
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
|
|
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
|
|
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
|