@rafinery/cli 0.8.0 → 0.8.2
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/CHANGELOG.md +20 -0
- package/bin/rafa-distiller.mjs +13 -0
- package/bin/rafa.mjs +4 -0
- package/blueprint/.claude/agents/atlas.md +5 -1
- package/blueprint/.claude/agents/bloom.md +5 -1
- package/blueprint/.claude/agents/compass.md +5 -1
- package/blueprint/.claude/agents/prism.md +5 -1
- package/blueprint/.claude/agents/sage.md +5 -1
- package/blueprint/.claude/commands/rafa.md +19 -1
- package/blueprint/.claude/rafa/contract.md +31 -3
- package/blueprint/.claude/rafa/hooks/brain-commit.mjs +80 -0
- package/blueprint/.claude/rafa/hooks/brain-map.mjs +64 -0
- package/blueprint/.claude/rafa/hooks/brain-switch.mjs +48 -0
- package/blueprint/.claude/rafa/hooks/post-checkout +10 -0
- package/blueprint/.claude/rafa/hooks/post-commit +11 -0
- package/blueprint/.claude/rafa/hooks/post-rewrite +10 -0
- package/blueprint/.claude/rafa/hooks/pre-push +22 -0
- package/blueprint/.claude/rafa/hooks/session-start.mjs +93 -0
- package/blueprint/.claude/skills/rafa-improve/SKILL.md +5 -0
- package/blueprint/.claude/skills/rafa-validate/SKILL.md +6 -0
- package/lib/checkpoint.mjs +42 -0
- package/lib/ci-setup.mjs +48 -3
- package/lib/dirty.mjs +37 -0
- package/lib/distill.mjs +313 -37
- package/lib/distiller/doctrine.mjs +379 -0
- package/lib/distiller/state-plane.mjs +159 -0
- package/lib/distiller.mjs +410 -0
- package/lib/doctor.mjs +110 -0
- package/lib/gate/compile.mjs +31 -0
- package/lib/gate/okf-emit.mjs +12 -0
- package/lib/gate/verify-citations.mjs +9 -2
- package/lib/githook.mjs +75 -33
- package/lib/init.mjs +14 -2
- package/lib/mcp-client.mjs +22 -5
- package/lib/okf.mjs +11 -1
- package/lib/pull.mjs +2 -2
- package/lib/push.mjs +67 -2
- package/lib/releases.mjs +26 -0
- package/lib/sensor-health.mjs +91 -0
- package/lib/status.mjs +47 -1
- package/lib/update.mjs +2 -2
- package/lib/working-set.mjs +19 -1
- package/package.json +11 -9
- package/LICENSE +0 -21
package/lib/distill.mjs
CHANGED
|
@@ -14,9 +14,21 @@
|
|
|
14
14
|
// the checkers itself (trust-but-verify), pushes, and resolves. The agent
|
|
15
15
|
// judges; it never ships.
|
|
16
16
|
//
|
|
17
|
+
// Collection is branch-match PLUS ancestry sweep (capture-engine spec P1,
|
|
18
|
+
// 2026-07-17): any still-active row on ANY branch whose capturedAtSha is
|
|
19
|
+
// reachable from this checkout's HEAD *off the first-parent mainline* is
|
|
20
|
+
// provably-merged backlog — an earlier merge whose distill never ran — and
|
|
21
|
+
// folds into this run. Mainline-grounded rows (a checkpoint before the
|
|
22
|
+
// branch's first commit) and squash/rebase-merged commits are NOT provable;
|
|
23
|
+
// those rows stay active for their own merge event or the platform
|
|
24
|
+
// reconciler. Same-path collisions: the merged branch's copy is judged
|
|
25
|
+
// (backlog never displaces the triggering merge); other copies stay active
|
|
26
|
+
// for their own later judgment — a verdict never applies to unjudged content.
|
|
27
|
+
//
|
|
17
28
|
// Fallback chain when this can't run (no key, no CI): the dev-session flow —
|
|
18
29
|
// /rafa distill <branch> in Claude Code (the bootstrap digest offers it).
|
|
19
30
|
|
|
31
|
+
import { execSync } from "node:child_process";
|
|
20
32
|
import {
|
|
21
33
|
existsSync,
|
|
22
34
|
mkdirSync,
|
|
@@ -74,6 +86,62 @@ async function loadAgentSdk(cwd) {
|
|
|
74
86
|
|
|
75
87
|
const VERDICTS = ["distilled", "refuted", "needs-adjudication"];
|
|
76
88
|
|
|
89
|
+
// ── collection helpers (pure; exported for tests) ───────────────────────────
|
|
90
|
+
|
|
91
|
+
// Same-path collision across branches. The MERGED branch's own row always
|
|
92
|
+
// wins — this run exists to judge the merge that triggered it; a swept
|
|
93
|
+
// backlog row must never displace it. Between swept rows, newest updatedAt
|
|
94
|
+
// wins with a deterministic branch-name tiebreak (platform iteration order
|
|
95
|
+
// must never pick the winner). Superseded copies are NOT resolved here —
|
|
96
|
+
// each copy is judged on its own merits in a later run (a verdict never
|
|
97
|
+
// applies to content the judge did not see).
|
|
98
|
+
export function latestWinsByPath(rows, preferredBranch) {
|
|
99
|
+
const beats = (a, b) => {
|
|
100
|
+
const aPref = a.branch === preferredBranch,
|
|
101
|
+
bPref = b.branch === preferredBranch;
|
|
102
|
+
if (aPref !== bPref) return aPref;
|
|
103
|
+
const aAt = a.updatedAt ?? 0,
|
|
104
|
+
bAt = b.updatedAt ?? 0;
|
|
105
|
+
if (aAt !== bAt) return aAt > bAt;
|
|
106
|
+
return String(a.branch) > String(b.branch);
|
|
107
|
+
};
|
|
108
|
+
const byPath = new Map();
|
|
109
|
+
for (const r of rows) {
|
|
110
|
+
const prev = byPath.get(r.path);
|
|
111
|
+
if (!prev) byPath.set(r.path, { winner: r, superseded: [] });
|
|
112
|
+
else if (beats(r, prev.winner)) {
|
|
113
|
+
prev.superseded.push(prev.winner);
|
|
114
|
+
prev.winner = r;
|
|
115
|
+
} else prev.superseded.push(r);
|
|
116
|
+
}
|
|
117
|
+
return [...byPath.values()];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Rows whose capture commit git can PROVE belongs to merged branch work:
|
|
121
|
+
// reachable from HEAD AND off the first-parent mainline. Reachability alone
|
|
122
|
+
// is not proof — a session that checkpoints before its branch's first commit
|
|
123
|
+
// grounds at a MAINLINE sha (the branch point), which is reachable forever;
|
|
124
|
+
// sweeping it would steal in-flight rows from open branches. Off-mainline
|
|
125
|
+
// reachable = the commit arrived via a merge. Rebase/squash-merged work is
|
|
126
|
+
// unprovable here and stays active (safety over recall — the platform
|
|
127
|
+
// reconciler and the session digest remain its path). Only full shas match
|
|
128
|
+
// (checkpoint stamps `git rev-parse HEAD`); anything else counts unproven,
|
|
129
|
+
// never guessed.
|
|
130
|
+
export function sweepByAncestry(rows, provenMerged) {
|
|
131
|
+
const swept = [];
|
|
132
|
+
let unproven = 0;
|
|
133
|
+
for (const r of rows) {
|
|
134
|
+
if (
|
|
135
|
+
typeof r.capturedAtSha === "string" &&
|
|
136
|
+
/^[0-9a-f]{40,64}$/i.test(r.capturedAtSha) &&
|
|
137
|
+
provenMerged(r.capturedAtSha.toLowerCase())
|
|
138
|
+
)
|
|
139
|
+
swept.push(r);
|
|
140
|
+
else unproven++;
|
|
141
|
+
}
|
|
142
|
+
return { swept, unproven };
|
|
143
|
+
}
|
|
144
|
+
|
|
77
145
|
export default async function distill(args = []) {
|
|
78
146
|
// U7 recursion guard, belt-and-braces: the Agent SDK worker must never fire
|
|
79
147
|
// the M5 session sensors (a worker's own tool calls re-triggering hooks would
|
|
@@ -99,31 +167,200 @@ export default async function distill(args = []) {
|
|
|
99
167
|
// log answers "where is it, and where did the time go" at a glance.
|
|
100
168
|
const t0 = Date.now();
|
|
101
169
|
const secs = (t) => ((Date.now() - t) / 1000).toFixed(1);
|
|
170
|
+
let agentModel = "unreported";
|
|
102
171
|
|
|
103
|
-
//
|
|
104
|
-
// a
|
|
172
|
+
// Run record (capture-engine P5): the Reconciliations tab renders CI runs
|
|
173
|
+
// too — a terminal report_distill_run per NON-EMPTY run, keyed by the merge
|
|
174
|
+
// commit (RAFA_MERGE_SHA from the workflow; HEAD of the merged checkout as
|
|
175
|
+
// the fallback). Telemetry — best-effort, never fatal, and empty merges
|
|
176
|
+
// write no row (honest no-op stays row-less).
|
|
177
|
+
const mergeSha = (() => {
|
|
178
|
+
const env = process.env.RAFA_MERGE_SHA ?? "";
|
|
179
|
+
if (/^[0-9a-f]{7,64}$/i.test(env)) return env;
|
|
180
|
+
try {
|
|
181
|
+
return execSync("git rev-parse HEAD", {
|
|
182
|
+
cwd: ROOT,
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
185
|
+
}).trim();
|
|
186
|
+
} catch {
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
})();
|
|
190
|
+
const reportRun = async (status, summary) => {
|
|
191
|
+
if (!mergeSha) return;
|
|
192
|
+
try {
|
|
193
|
+
await callTool(ROOT, "report_distill_run", {
|
|
194
|
+
branch,
|
|
195
|
+
toBranch: process.env.RAFA_TARGET_BRANCH || "main",
|
|
196
|
+
mergeCommitSha: mergeSha,
|
|
197
|
+
status,
|
|
198
|
+
note: summary,
|
|
199
|
+
actorMeta: { agent: "rafa-distill", runner: "ci", model: agentModel },
|
|
200
|
+
});
|
|
201
|
+
} catch {
|
|
202
|
+
/* the run itself is the deliverable; its record is telemetry */
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const failRun = async (m) => {
|
|
206
|
+
await reportRun("failed", m);
|
|
207
|
+
die(m);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Merged-work oracle: TWO subprocesses total — every commit reachable from
|
|
211
|
+
// HEAD, and the first-parent mainline. provenMerged = reachable ∧
|
|
212
|
+
// off-mainline (see sweepByAncestry). A shallow checkout truncates both
|
|
213
|
+
// sets, which only ever UNDER-sweeps (rows stay active) — safe.
|
|
214
|
+
const revList = (args) => {
|
|
215
|
+
try {
|
|
216
|
+
return new Set(
|
|
217
|
+
execSync(`git rev-list ${args} HEAD`, {
|
|
218
|
+
cwd: ROOT,
|
|
219
|
+
encoding: "utf8",
|
|
220
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
221
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
222
|
+
})
|
|
223
|
+
.trim()
|
|
224
|
+
.split("\n")
|
|
225
|
+
.filter(Boolean),
|
|
226
|
+
);
|
|
227
|
+
} catch {
|
|
228
|
+
return new Set(); // no git history readable → sweep proves nothing
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const reachable = revList("");
|
|
232
|
+
const mainline = revList("--first-parent");
|
|
233
|
+
const provenMerged = (sha) => reachable.has(sha) && !mainline.has(sha);
|
|
234
|
+
let shallow = false;
|
|
235
|
+
try {
|
|
236
|
+
shallow =
|
|
237
|
+
execSync("git rev-parse --is-shallow-repository", {
|
|
238
|
+
cwd: ROOT,
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
241
|
+
}).trim() === "true";
|
|
242
|
+
} catch {
|
|
243
|
+
/* not a git repo would have failed checkout long before this */
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 1 · collect: the merged branch's own rows PLUS the ancestry sweep over
|
|
247
|
+
// every other branch's active rows (provably-merged backlog folds in).
|
|
248
|
+
// Adjudication-flagged rows are LEFT for a human session (CI can flag
|
|
249
|
+
// divergence, never resolve it). The sweep is AUXILIARY: its failure warns
|
|
250
|
+
// and narrows the run to the merged branch — it never kills the primary
|
|
251
|
+
// distillation the merge event exists for.
|
|
105
252
|
console.log(`• collecting working set for ${branch} …`);
|
|
106
|
-
let
|
|
253
|
+
let branchAll;
|
|
107
254
|
try {
|
|
108
|
-
|
|
109
|
-
(await callTool(ROOT, "get_working_set", { branch
|
|
110
|
-
.files ?? [];
|
|
111
|
-
flagged =
|
|
112
|
-
(
|
|
113
|
-
await callTool(ROOT, "get_working_set", {
|
|
114
|
-
branch,
|
|
115
|
-
status: "needs-adjudication",
|
|
116
|
-
})
|
|
117
|
-
).files ?? [];
|
|
255
|
+
branchAll =
|
|
256
|
+
(await callTool(ROOT, "get_working_set", { branch })).files ?? [];
|
|
118
257
|
} catch (e) {
|
|
119
258
|
die(e instanceof Error ? e.message : String(e));
|
|
120
259
|
}
|
|
260
|
+
const branchRows = branchAll.filter((r) => r.status === "active");
|
|
261
|
+
const flagged = branchAll.filter((r) => r.status === "needs-adjudication");
|
|
262
|
+
const sweptRows = [];
|
|
263
|
+
let unproven = 0,
|
|
264
|
+
sweptBranches = 0;
|
|
265
|
+
try {
|
|
266
|
+
const { branches } = await callTool(ROOT, "list_working_sets", {});
|
|
267
|
+
const others = (branches ?? [])
|
|
268
|
+
.map((b) => (typeof b === "string" ? b : b.branch))
|
|
269
|
+
.filter((other) => other && other !== branch);
|
|
270
|
+
const perBranch = await Promise.all(
|
|
271
|
+
others.map(async (other) => {
|
|
272
|
+
const rows =
|
|
273
|
+
(
|
|
274
|
+
await callTool(ROOT, "get_working_set", {
|
|
275
|
+
branch: other,
|
|
276
|
+
status: "active",
|
|
277
|
+
})
|
|
278
|
+
).files ?? [];
|
|
279
|
+
return sweepByAncestry(rows, provenMerged);
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
for (const { swept, unproven: u } of perBranch) {
|
|
283
|
+
if (swept.length) sweptBranches++;
|
|
284
|
+
sweptRows.push(...swept);
|
|
285
|
+
unproven += u;
|
|
286
|
+
}
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.log(
|
|
289
|
+
` ! ancestry sweep skipped (${e instanceof Error ? e.message : e}) — distilling ${branch}'s own rows only`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
121
292
|
if (flagged.length)
|
|
122
293
|
console.log(
|
|
123
294
|
` ! ${flagged.length} file(s) already need adjudication — left for the next dev session's digest`,
|
|
124
295
|
);
|
|
296
|
+
if (sweptRows.length)
|
|
297
|
+
console.log(
|
|
298
|
+
` + ancestry sweep: ${sweptRows.length} provably-merged row(s) from ${sweptBranches} other branch(es) folded into this run`,
|
|
299
|
+
);
|
|
300
|
+
if (unproven > 0)
|
|
301
|
+
console.log(
|
|
302
|
+
` · ${unproven} row(s) on other branches not provably merged — left intact` +
|
|
303
|
+
(shallow
|
|
304
|
+
? `\n (shallow checkout limits the proof — re-run \`npx @rafinery/cli ci-setup --overwrite\` for full history)`
|
|
305
|
+
: ""),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const allGroups = latestWinsByPath([...branchRows, ...sweptRows], branch);
|
|
309
|
+
// P2 typed candidates: intent records are PROVENANCE, never truth claims —
|
|
310
|
+
// they resolve as consumed without staging or judgment; notes and
|
|
311
|
+
// improvements are the judged kinds (per-kind question in the prompt).
|
|
312
|
+
const kindOf = (p) =>
|
|
313
|
+
p.startsWith("improve/improvements/")
|
|
314
|
+
? "improvement"
|
|
315
|
+
: p.startsWith("intent/")
|
|
316
|
+
? "intent"
|
|
317
|
+
: "note";
|
|
318
|
+
const intentGroups = allGroups.filter((g) => kindOf(g.winner.path) === "intent");
|
|
319
|
+
const groups = allGroups.filter((g) => kindOf(g.winner.path) !== "intent");
|
|
320
|
+
const active = groups.map((g) => g.winner);
|
|
321
|
+
if (intentGroups.length)
|
|
322
|
+
console.log(
|
|
323
|
+
` · ${intentGroups.length} intent record(s) — provenance, consumed without judgment`,
|
|
324
|
+
);
|
|
325
|
+
let intentConsumed = 0;
|
|
326
|
+
for (const g of intentGroups) {
|
|
327
|
+
for (const row of [g.winner, ...g.superseded]) {
|
|
328
|
+
try {
|
|
329
|
+
await callTool(ROOT, "resolve_working_file", {
|
|
330
|
+
branch: row.branch ?? branch,
|
|
331
|
+
path: row.path,
|
|
332
|
+
resolution: "distilled",
|
|
333
|
+
actorMeta: { agent: "rafa-distill", runner: "ci", model: agentModel },
|
|
334
|
+
note: "intent record — provenance consumed by the merge run (never a truth claim)",
|
|
335
|
+
...(typeof row.version === "number" ? { baseVersion: row.version } : {}),
|
|
336
|
+
});
|
|
337
|
+
intentConsumed++;
|
|
338
|
+
} catch {
|
|
339
|
+
/* stays active for a later run — the brain branch keeps the trail */
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
125
344
|
if (active.length === 0) {
|
|
126
|
-
|
|
345
|
+
// Diagnostic empty (owner catch 2026-07-17): silence must explain itself —
|
|
346
|
+
// "never captured" and "already reconciled" are different states.
|
|
347
|
+
if (branchAll.length === 0) {
|
|
348
|
+
console.log(
|
|
349
|
+
`✓ nothing to distill on ${branch} — 0 rows were ever captured on this branch.\n` +
|
|
350
|
+
` Capture never fired during its dev sessions (see \`rafa status\` → SENSORS; ` +
|
|
351
|
+
`candidates land via \`rafa checkpoint\`).`,
|
|
352
|
+
);
|
|
353
|
+
} else {
|
|
354
|
+
const counts = {};
|
|
355
|
+
for (const r of branchAll) counts[r.status] = (counts[r.status] ?? 0) + 1;
|
|
356
|
+
const parts = Object.entries(counts)
|
|
357
|
+
.map(([s, n]) => `${n} ${s}`)
|
|
358
|
+
.join(" · ");
|
|
359
|
+
console.log(
|
|
360
|
+
`✓ nothing to distill on ${branch} — ${branchAll.length} row(s) exist, none active: ${parts}\n` +
|
|
361
|
+
` (distilled/refuted = already reconciled by an earlier run · folded = moved to the parent branch)`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
127
364
|
return;
|
|
128
365
|
}
|
|
129
366
|
console.log(` ${active.length} file(s) to distill`);
|
|
@@ -142,8 +379,9 @@ export default async function distill(args = []) {
|
|
|
142
379
|
mkdirSync(dirname(abs), { recursive: true });
|
|
143
380
|
writeFileSync(
|
|
144
381
|
abs,
|
|
145
|
-
`<!-- incoming working-set file · branch: ${branch} · author: ${f.lastAuthor} · v${f.version}` +
|
|
146
|
-
`${f.baseBrainSha ? ` · base: ${f.baseBrainSha}` : ""}
|
|
382
|
+
`<!-- incoming working-set file · branch: ${f.branch ?? branch} · author: ${f.lastAuthor} · v${f.version}` +
|
|
383
|
+
`${f.baseBrainSha ? ` · base: ${f.baseBrainSha}` : ""}` +
|
|
384
|
+
`${f.capturedAtSha ? ` · captured-at: ${f.capturedAtSha}` : ""} -->\n` +
|
|
147
385
|
f.content,
|
|
148
386
|
);
|
|
149
387
|
}
|
|
@@ -153,18 +391,25 @@ export default async function distill(args = []) {
|
|
|
153
391
|
// 4 · the judgment pass (org's own key; gates stay deterministic below).
|
|
154
392
|
const sdk = await loadAgentSdk(ROOT);
|
|
155
393
|
const fileList = active
|
|
156
|
-
.map(
|
|
394
|
+
.map(
|
|
395
|
+
(f) =>
|
|
396
|
+
`- ${f.path} (kind: ${kindOf(f.path)} · branch: ${f.branch ?? branch} · author: ${f.lastAuthor})`,
|
|
397
|
+
)
|
|
157
398
|
.join("\n");
|
|
158
399
|
const prompt = `You are running rafa's CI DISTILLATION — merge-time reconciliation of branch "${branch}"'s
|
|
159
|
-
working set into the org brain.
|
|
400
|
+
working set into the org brain. Some staged files were swept in from OTHER already-merged
|
|
401
|
+
branches (their provenance comments name the source branch) — judge them identically.
|
|
402
|
+
The checked-out repo IS merged main (the ground truth).
|
|
160
403
|
The current org brain is mirrored at .rafa/brain/. The incoming candidate-grade files are
|
|
161
404
|
staged under .rafa/distill-incoming/ (brain-relative paths mirrored; a provenance comment
|
|
162
405
|
tops each file).
|
|
163
406
|
|
|
164
407
|
Read .claude/skills/rafa-distill/SKILL.md (the SOP) and .claude/rafa/contract.md §2 (the
|
|
165
|
-
note schema) first.
|
|
408
|
+
note schema) + §3 (the improvement schema) first.
|
|
409
|
+
|
|
410
|
+
Each staged file names its KIND in the list below — the judge question differs:
|
|
166
411
|
|
|
167
|
-
For
|
|
412
|
+
For each kind:note file:
|
|
168
413
|
1. VALIDATE like prism — skeptical, code-grounded: does every claim hold against the code
|
|
169
414
|
as it now stands on main? Confirm each citation resolves (the cited line contains the
|
|
170
415
|
token; run \`git grep\` yourself). A claim you cannot confirm with a file:line is
|
|
@@ -174,8 +419,20 @@ For EACH staged file:
|
|
|
174
419
|
per the contract: valid frontmatter, real cites into main, \`anchor:\` on contracts;
|
|
175
420
|
fold into an existing note when one already covers the topic (supersede, never
|
|
176
421
|
duplicate). Never copy the provenance comment into the brain file.
|
|
177
|
-
|
|
178
|
-
|
|
422
|
+
|
|
423
|
+
For each kind:improvement file — the LIFECYCLE question, not the claim question:
|
|
424
|
+
1. If it reports a FIX (status: fixed): is the cited defect actually GONE on merged main?
|
|
425
|
+
Gone → verdict "distilled", note "confirmed-fixed — <cite>"; author the status change
|
|
426
|
+
into .rafa/improve/improvements/<id>.md. Still present → verdict "refuted", note
|
|
427
|
+
"still-open — <cite>" (the ledger keeps saying open, truthfully).
|
|
428
|
+
2. If it proposes a NEW improvement: does the defect actually EXIST on merged main, at the
|
|
429
|
+
cited lines? Exists → verdict "distilled", note "opened"; author the file under
|
|
430
|
+
.rafa/improve/improvements/. Not reproducible → "refuted" with the cited reason.
|
|
431
|
+
3. After ANY improvement change, regenerate .rafa/improve/ledger.md counts to match the
|
|
432
|
+
rows — the ledger is DERIVED and compile cross-checks it.
|
|
433
|
+
|
|
434
|
+
When all survivors are authored, run \`npx @rafinery/cli verify-citations\` and
|
|
435
|
+
\`npx @rafinery/cli compile\` and fix your authored files until BOTH exit 0.
|
|
179
436
|
|
|
180
437
|
Finally write .rafa/distill-verdicts.json:
|
|
181
438
|
{"verdicts":[{"path":"<staged brain-relative path>","verdict":"distilled|refuted|needs-adjudication","note":"<cited reason — required for refuted and needs-adjudication>"}]}
|
|
@@ -202,7 +459,6 @@ ${fileList}`;
|
|
|
202
459
|
// Stream the agent's narration lines — a silent multi-minute CI step reads
|
|
203
460
|
// as hung; a narrated one reads as working (and shows WHERE it hangs).
|
|
204
461
|
let lastLine = "";
|
|
205
|
-
let agentModel = "unreported";
|
|
206
462
|
for await (const message of run) {
|
|
207
463
|
if (
|
|
208
464
|
message.type === "assistant" &&
|
|
@@ -222,7 +478,7 @@ ${fileList}`;
|
|
|
222
478
|
}
|
|
223
479
|
if (message.type === "result") {
|
|
224
480
|
if (message.subtype !== "success")
|
|
225
|
-
|
|
481
|
+
await failRun(
|
|
226
482
|
`distillation agent did not complete (${message.subtype}) — nothing resolved, nothing pushed`,
|
|
227
483
|
);
|
|
228
484
|
console.log(
|
|
@@ -252,42 +508,42 @@ ${fileList}`;
|
|
|
252
508
|
|
|
253
509
|
// 5 · verdicts must be complete — a missing entry is a hard stop, never a guess.
|
|
254
510
|
if (!existsSync(verdictsPath))
|
|
255
|
-
|
|
511
|
+
await failRun(
|
|
256
512
|
"agent produced no .rafa/distill-verdicts.json — nothing resolved, nothing pushed; working set left intact",
|
|
257
513
|
);
|
|
258
514
|
let verdicts;
|
|
259
515
|
try {
|
|
260
516
|
verdicts = JSON.parse(readFileSync(verdictsPath, "utf8")).verdicts;
|
|
261
517
|
} catch {
|
|
262
|
-
|
|
518
|
+
await failRun(
|
|
263
519
|
"distill-verdicts.json is not valid JSON — nothing resolved, nothing pushed",
|
|
264
520
|
);
|
|
265
521
|
}
|
|
266
522
|
if (!Array.isArray(verdicts))
|
|
267
|
-
|
|
523
|
+
await failRun("distill-verdicts.json has no verdicts[] — aborting");
|
|
268
524
|
const byPath = new Map(verdicts.map((v) => [v.path, v]));
|
|
269
525
|
for (const f of active) {
|
|
270
526
|
const v = byPath.get(f.path);
|
|
271
527
|
if (!v || !VERDICTS.includes(v.verdict))
|
|
272
|
-
|
|
528
|
+
await failRun(
|
|
273
529
|
`no valid verdict for ${f.path} — nothing resolved, nothing pushed; working set left intact`,
|
|
274
530
|
);
|
|
275
531
|
if (
|
|
276
532
|
v.verdict !== "distilled" &&
|
|
277
533
|
(typeof v.note !== "string" || v.note.trim() === "")
|
|
278
534
|
)
|
|
279
|
-
|
|
535
|
+
await failRun(`${f.path}: verdict "${v.verdict}" requires a cited note — aborting`);
|
|
280
536
|
}
|
|
281
537
|
|
|
282
538
|
// 6 · trust-but-verify: OUR gates re-run regardless of what the agent claims.
|
|
283
539
|
console.log("• re-running the gates (trust-but-verify) …");
|
|
284
540
|
const tGates = Date.now();
|
|
285
541
|
if (runVerifyCitations([]) !== 0)
|
|
286
|
-
|
|
542
|
+
await failRun(
|
|
287
543
|
"citation checker failed on the authored brain — nothing resolved, nothing pushed",
|
|
288
544
|
);
|
|
289
545
|
if (runCompile([]) !== 0)
|
|
290
|
-
|
|
546
|
+
await failRun(
|
|
291
547
|
"compile gate failed on the authored brain — nothing resolved, nothing pushed",
|
|
292
548
|
);
|
|
293
549
|
console.log(` ⏱ gates: ${secs(tGates)}s`);
|
|
@@ -298,39 +554,59 @@ ${fileList}`;
|
|
|
298
554
|
(f) => byPath.get(f.path).verdict === "distilled",
|
|
299
555
|
).length;
|
|
300
556
|
if (distilledCount > 0) {
|
|
301
|
-
await push([]);
|
|
557
|
+
await push(["--verb=distill"]);
|
|
302
558
|
} else {
|
|
303
559
|
console.log("• no survivors — nothing to push to the brain repo");
|
|
304
560
|
}
|
|
305
561
|
|
|
306
562
|
// 8 · resolve the rows (CAS — a racing session's resolve loses loudly).
|
|
563
|
+
// ONLY judged winners resolve, each under ITS OWN branch key and fenced on
|
|
564
|
+
// the version this run READ — a checkpoint that landed mid-judgment wins,
|
|
565
|
+
// never the stale verdict. Superseded same-path copies stay active: a
|
|
566
|
+
// verdict must never apply to content the judge did not see; each copy is
|
|
567
|
+
// judged on its own merits in a later run.
|
|
307
568
|
let refuted = 0,
|
|
308
569
|
adjudication = 0;
|
|
309
|
-
for (const
|
|
570
|
+
for (const g of groups) {
|
|
571
|
+
const f = g.winner;
|
|
310
572
|
const v = byPath.get(f.path);
|
|
311
573
|
if (v.verdict === "refuted") refuted++;
|
|
312
574
|
if (v.verdict === "needs-adjudication") adjudication++;
|
|
313
575
|
try {
|
|
314
576
|
await callTool(ROOT, "resolve_working_file", {
|
|
315
|
-
branch,
|
|
577
|
+
branch: f.branch ?? branch,
|
|
316
578
|
path: f.path,
|
|
317
579
|
resolution: v.verdict,
|
|
318
580
|
actorMeta: { agent: "rafa-distill", runner: "ci", model: agentModel },
|
|
319
581
|
...(v.note ? { note: v.note } : {}),
|
|
582
|
+
...(typeof f.version === "number" ? { baseVersion: f.version } : {}),
|
|
320
583
|
});
|
|
321
584
|
console.log(
|
|
322
|
-
` ${v.verdict === "distilled" ? "✓" : "!"} ${f.path} → ${v.verdict}${v.note ? ` (${v.note})` : ""}`,
|
|
585
|
+
` ${v.verdict === "distilled" ? "✓" : "!"} ${f.branch ?? branch} · ${f.path} → ${v.verdict}${v.note ? ` (${v.note})` : ""}`,
|
|
323
586
|
);
|
|
324
587
|
} catch (e) {
|
|
325
588
|
console.log(
|
|
326
|
-
` ✗ ${f.path} — resolve failed: ${e instanceof Error ? e.message : e}`,
|
|
589
|
+
` ✗ ${f.branch ?? branch} · ${f.path} — resolve failed: ${e instanceof Error ? e.message : e}`,
|
|
327
590
|
);
|
|
328
591
|
}
|
|
592
|
+
for (const s of g.superseded)
|
|
593
|
+
console.log(
|
|
594
|
+
` · ${s.branch ?? branch} · ${s.path} — older same-path copy left active for its own judgment`,
|
|
595
|
+
);
|
|
329
596
|
}
|
|
330
597
|
rmSync(verdictsPath, { force: true });
|
|
331
598
|
|
|
599
|
+
await reportRun(
|
|
600
|
+
"succeeded",
|
|
601
|
+
`${distilledCount} banked · ${refuted} refuted · ${adjudication} needs-adjudication` +
|
|
602
|
+
(sweptRows.length ? ` · ${sweptRows.length} swept from earlier merges` : "") +
|
|
603
|
+
(intentConsumed ? ` · ${intentConsumed} intent record(s) consumed` : ""),
|
|
604
|
+
);
|
|
332
605
|
console.log(
|
|
333
606
|
`✓ distillation: ${distilledCount} into the org brain · ${refuted} refuted (reported, cited) · ` +
|
|
334
|
-
`${adjudication} flagged needs-adjudication (next session's digest)
|
|
607
|
+
`${adjudication} flagged needs-adjudication (next session's digest)` +
|
|
608
|
+
(sweptRows.length ? ` · ${sweptRows.length} swept from earlier merges` : "") +
|
|
609
|
+
(intentConsumed ? ` · ${intentConsumed} intent consumed` : "") +
|
|
610
|
+
` · total ${secs(t0)}s`,
|
|
335
611
|
);
|
|
336
612
|
}
|