@rafinery/cli 0.6.0 → 0.7.1
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/rafa.mjs +7 -0
- package/blueprint/.claude/agents/sage.md +66 -0
- package/blueprint/.claude/commands/rafa.md +205 -325
- package/blueprint/.claude/rafa/contract.md +188 -131
- package/blueprint/.claude/skills/rafa-build/SKILL.md +9 -1
- package/blueprint/.claude/skills/rafa-distill/SKILL.md +6 -1
- package/blueprint/.claude/skills/rafa-sage/SKILL.md +201 -0
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +41 -2
- package/lib/benchmark.mjs +573 -0
- package/lib/blueprint.mjs +2 -0
- package/lib/ci-setup.mjs +8 -2
- package/lib/distill.mjs +87 -33
- package/lib/gate/compile.mjs +258 -49
- package/package.json +1 -1
package/lib/distill.mjs
CHANGED
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
// Fallback chain when this can't run (no key, no CI): the dev-session flow —
|
|
18
18
|
// /rafa distill <branch> in Claude Code (the bootstrap digest offers it).
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs";
|
|
21
27
|
import { createRequire } from "node:module";
|
|
22
28
|
import { dirname, join } from "node:path";
|
|
23
29
|
import { pathToFileURL } from "node:url";
|
|
@@ -35,27 +41,35 @@ const die = (m) => {
|
|
|
35
41
|
// A brain-relative path from the platform must stay inside .rafa/ — a row that
|
|
36
42
|
// would escape is refused loudly (never written, never guessed).
|
|
37
43
|
const safeRel = (p) => {
|
|
38
|
-
if (p.startsWith("/") || p.split("/").includes(".."))
|
|
44
|
+
if (p.startsWith("/") || p.split("/").includes(".."))
|
|
45
|
+
die(`unsafe working-set path: ${p}`);
|
|
39
46
|
return p;
|
|
40
47
|
};
|
|
41
48
|
|
|
42
49
|
// The Agent SDK is deliberately NOT a dependency of this CLI (it is heavy and
|
|
43
|
-
// only CI distillation needs it). Resolution order:
|
|
44
|
-
//
|
|
50
|
+
// only CI distillation needs it). Resolution order: $RAFA_AGENT_SDK_DIR (the
|
|
51
|
+
// workflow's ISOLATED install — `npm i` inside the client checkout breaks on
|
|
52
|
+
// pnpm workspaces, "workspace:*" is not an npm protocol) → normal import →
|
|
53
|
+
// the host repo's node_modules.
|
|
45
54
|
async function loadAgentSdk(cwd) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} catch {
|
|
55
|
+
const roots = [process.env.RAFA_AGENT_SDK_DIR, null, cwd];
|
|
56
|
+
for (const root of roots) {
|
|
49
57
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"@anthropic-ai/claude-agent-sdk
|
|
55
|
-
" `npm i --no-save @anthropic-ai/claude-agent-sdk` before this command (see rafa ci-setup).",
|
|
58
|
+
if (root === null) return await import("@anthropic-ai/claude-agent-sdk");
|
|
59
|
+
if (!root) continue;
|
|
60
|
+
const req = createRequire(join(root, "package.json"));
|
|
61
|
+
return await import(
|
|
62
|
+
pathToFileURL(req.resolve("@anthropic-ai/claude-agent-sdk")).href
|
|
56
63
|
);
|
|
64
|
+
} catch {
|
|
65
|
+
// try the next root
|
|
57
66
|
}
|
|
58
67
|
}
|
|
68
|
+
die(
|
|
69
|
+
"@anthropic-ai/claude-agent-sdk is not installed — the reconcile workflow installs it\n" +
|
|
70
|
+
" into an isolated dir and points RAFA_AGENT_SDK_DIR at it (see rafa ci-setup;\n" +
|
|
71
|
+
" re-run `npx @rafinery/cli ci-setup --overwrite` if your workflow predates this).",
|
|
72
|
+
);
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
const VERDICTS = ["distilled", "refuted", "needs-adjudication"];
|
|
@@ -76,7 +90,8 @@ export default async function distill(args = []) {
|
|
|
76
90
|
die(
|
|
77
91
|
"ANTHROPIC_API_KEY is not set — CI distillation runs on the ORG'S OWN LLM key from CI\n" +
|
|
78
92
|
" secrets (never stored on the rafinery platform). Add the secret (rafa ci-setup names\n" +
|
|
79
|
-
" it), or fall back to the dev session: /rafa distill " +
|
|
93
|
+
" it), or fall back to the dev session: /rafa distill " +
|
|
94
|
+
branch,
|
|
80
95
|
);
|
|
81
96
|
|
|
82
97
|
const ROOT = process.cwd();
|
|
@@ -90,10 +105,16 @@ export default async function distill(args = []) {
|
|
|
90
105
|
console.log(`• collecting working set for ${branch} …`);
|
|
91
106
|
let active, flagged;
|
|
92
107
|
try {
|
|
93
|
-
active =
|
|
94
|
-
|
|
95
|
-
(await callTool(ROOT, "get_working_set", { branch, status: "needs-adjudication" }))
|
|
108
|
+
active =
|
|
109
|
+
(await callTool(ROOT, "get_working_set", { branch, status: "active" }))
|
|
96
110
|
.files ?? [];
|
|
111
|
+
flagged =
|
|
112
|
+
(
|
|
113
|
+
await callTool(ROOT, "get_working_set", {
|
|
114
|
+
branch,
|
|
115
|
+
status: "needs-adjudication",
|
|
116
|
+
})
|
|
117
|
+
).files ?? [];
|
|
97
118
|
} catch (e) {
|
|
98
119
|
die(e instanceof Error ? e.message : String(e));
|
|
99
120
|
}
|
|
@@ -131,7 +152,9 @@ export default async function distill(args = []) {
|
|
|
131
152
|
|
|
132
153
|
// 4 · the judgment pass (org's own key; gates stay deterministic below).
|
|
133
154
|
const sdk = await loadAgentSdk(ROOT);
|
|
134
|
-
const fileList = active
|
|
155
|
+
const fileList = active
|
|
156
|
+
.map((f) => `- ${f.path} (author: ${f.lastAuthor})`)
|
|
157
|
+
.join("\n");
|
|
135
158
|
const prompt = `You are running rafa's CI DISTILLATION — merge-time reconciliation of branch "${branch}"'s
|
|
136
159
|
working set into the org brain. The checked-out repo IS merged main (the ground truth).
|
|
137
160
|
The current org brain is mirrored at .rafa/brain/. The incoming candidate-grade files are
|
|
@@ -162,7 +185,9 @@ with EXACTLY one entry per staged file listed below. Do not modify anything outs
|
|
|
162
185
|
Staged files:
|
|
163
186
|
${fileList}`;
|
|
164
187
|
|
|
165
|
-
console.log(
|
|
188
|
+
console.log(
|
|
189
|
+
"• running the distillation agent (org's own ANTHROPIC_API_KEY) …",
|
|
190
|
+
);
|
|
166
191
|
const tAgent = Date.now();
|
|
167
192
|
const run = sdk.query({
|
|
168
193
|
prompt,
|
|
@@ -179,7 +204,10 @@ ${fileList}`;
|
|
|
179
204
|
let lastLine = "";
|
|
180
205
|
let agentModel = "unreported";
|
|
181
206
|
for await (const message of run) {
|
|
182
|
-
if (
|
|
207
|
+
if (
|
|
208
|
+
message.type === "assistant" &&
|
|
209
|
+
typeof message.message?.model === "string"
|
|
210
|
+
)
|
|
183
211
|
agentModel = message.message.model;
|
|
184
212
|
if (message.type === "assistant") {
|
|
185
213
|
const blocks = message.message?.content ?? [];
|
|
@@ -194,7 +222,9 @@ ${fileList}`;
|
|
|
194
222
|
}
|
|
195
223
|
if (message.type === "result") {
|
|
196
224
|
if (message.subtype !== "success")
|
|
197
|
-
die(
|
|
225
|
+
die(
|
|
226
|
+
`distillation agent did not complete (${message.subtype}) — nothing resolved, nothing pushed`,
|
|
227
|
+
);
|
|
198
228
|
console.log(
|
|
199
229
|
` ⏱ agent: ${secs(tAgent)}s (${message.num_turns ?? "?"} turns · ` +
|
|
200
230
|
`$${(message.total_cost_usd ?? 0).toFixed(4)} on the org's key)`,
|
|
@@ -206,32 +236,46 @@ ${fileList}`;
|
|
|
206
236
|
model: agentModel,
|
|
207
237
|
inputTokens: message.usage?.input_tokens ?? 0,
|
|
208
238
|
outputTokens: message.usage?.output_tokens ?? 0,
|
|
209
|
-
...(typeof message.total_cost_usd === "number"
|
|
239
|
+
...(typeof message.total_cost_usd === "number"
|
|
240
|
+
? { costUsd: message.total_cost_usd }
|
|
241
|
+
: {}),
|
|
210
242
|
purpose: "distill",
|
|
211
243
|
runner: "ci",
|
|
212
244
|
});
|
|
213
245
|
} catch (e) {
|
|
214
|
-
console.log(
|
|
246
|
+
console.log(
|
|
247
|
+
` ! usage not reported: ${e instanceof Error ? e.message : e}`,
|
|
248
|
+
);
|
|
215
249
|
}
|
|
216
250
|
}
|
|
217
251
|
}
|
|
218
252
|
|
|
219
253
|
// 5 · verdicts must be complete — a missing entry is a hard stop, never a guess.
|
|
220
254
|
if (!existsSync(verdictsPath))
|
|
221
|
-
die(
|
|
255
|
+
die(
|
|
256
|
+
"agent produced no .rafa/distill-verdicts.json — nothing resolved, nothing pushed; working set left intact",
|
|
257
|
+
);
|
|
222
258
|
let verdicts;
|
|
223
259
|
try {
|
|
224
260
|
verdicts = JSON.parse(readFileSync(verdictsPath, "utf8")).verdicts;
|
|
225
261
|
} catch {
|
|
226
|
-
die(
|
|
262
|
+
die(
|
|
263
|
+
"distill-verdicts.json is not valid JSON — nothing resolved, nothing pushed",
|
|
264
|
+
);
|
|
227
265
|
}
|
|
228
|
-
if (!Array.isArray(verdicts))
|
|
266
|
+
if (!Array.isArray(verdicts))
|
|
267
|
+
die("distill-verdicts.json has no verdicts[] — aborting");
|
|
229
268
|
const byPath = new Map(verdicts.map((v) => [v.path, v]));
|
|
230
269
|
for (const f of active) {
|
|
231
270
|
const v = byPath.get(f.path);
|
|
232
271
|
if (!v || !VERDICTS.includes(v.verdict))
|
|
233
|
-
die(
|
|
234
|
-
|
|
272
|
+
die(
|
|
273
|
+
`no valid verdict for ${f.path} — nothing resolved, nothing pushed; working set left intact`,
|
|
274
|
+
);
|
|
275
|
+
if (
|
|
276
|
+
v.verdict !== "distilled" &&
|
|
277
|
+
(typeof v.note !== "string" || v.note.trim() === "")
|
|
278
|
+
)
|
|
235
279
|
die(`${f.path}: verdict "${v.verdict}" requires a cited note — aborting`);
|
|
236
280
|
}
|
|
237
281
|
|
|
@@ -239,14 +283,20 @@ ${fileList}`;
|
|
|
239
283
|
console.log("• re-running the gates (trust-but-verify) …");
|
|
240
284
|
const tGates = Date.now();
|
|
241
285
|
if (runVerifyCitations([]) !== 0)
|
|
242
|
-
die(
|
|
286
|
+
die(
|
|
287
|
+
"citation checker failed on the authored brain — nothing resolved, nothing pushed",
|
|
288
|
+
);
|
|
243
289
|
if (runCompile([]) !== 0)
|
|
244
|
-
die(
|
|
290
|
+
die(
|
|
291
|
+
"compile gate failed on the authored brain — nothing resolved, nothing pushed",
|
|
292
|
+
);
|
|
245
293
|
console.log(` ⏱ gates: ${secs(tGates)}s`);
|
|
246
294
|
|
|
247
295
|
// 7 · ship: brain repo push (webhook → ingest), staging cleaned first.
|
|
248
296
|
rmSync(stagingRoot, { recursive: true, force: true });
|
|
249
|
-
const distilledCount = active.filter(
|
|
297
|
+
const distilledCount = active.filter(
|
|
298
|
+
(f) => byPath.get(f.path).verdict === "distilled",
|
|
299
|
+
).length;
|
|
250
300
|
if (distilledCount > 0) {
|
|
251
301
|
await push([]);
|
|
252
302
|
} else {
|
|
@@ -268,9 +318,13 @@ ${fileList}`;
|
|
|
268
318
|
actorMeta: { agent: "rafa-distill", runner: "ci", model: agentModel },
|
|
269
319
|
...(v.note ? { note: v.note } : {}),
|
|
270
320
|
});
|
|
271
|
-
console.log(
|
|
321
|
+
console.log(
|
|
322
|
+
` ${v.verdict === "distilled" ? "✓" : "!"} ${f.path} → ${v.verdict}${v.note ? ` (${v.note})` : ""}`,
|
|
323
|
+
);
|
|
272
324
|
} catch (e) {
|
|
273
|
-
console.log(
|
|
325
|
+
console.log(
|
|
326
|
+
` ✗ ${f.path} — resolve failed: ${e instanceof Error ? e.message : e}`,
|
|
327
|
+
);
|
|
274
328
|
}
|
|
275
329
|
}
|
|
276
330
|
rmSync(verdictsPath, { force: true });
|