@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/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 { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
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("..")) die(`unsafe working-set path: ${p}`);
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: normal import → the host
44
- // repo's node_modules (the workflow installs it with `npm i --no-save`).
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
- try {
47
- return await import("@anthropic-ai/claude-agent-sdk");
48
- } catch {
55
+ const roots = [process.env.RAFA_AGENT_SDK_DIR, null, cwd];
56
+ for (const root of roots) {
49
57
  try {
50
- const req = createRequire(join(cwd, "package.json"));
51
- return await import(pathToFileURL(req.resolve("@anthropic-ai/claude-agent-sdk")).href);
52
- } catch {
53
- die(
54
- "@anthropic-ai/claude-agent-sdk is not installed — the reconcile workflow runs\n" +
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 " + branch,
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 = (await callTool(ROOT, "get_working_set", { branch, status: "active" })).files ?? [];
94
- flagged =
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.map((f) => `- ${f.path} (author: ${f.lastAuthor})`).join("\n");
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("• running the distillation agent (org's own ANTHROPIC_API_KEY) …");
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 (message.type === "assistant" && typeof message.message?.model === "string")
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(`distillation agent did not complete (${message.subtype}) — nothing resolved, nothing pushed`);
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" ? { costUsd: message.total_cost_usd } : {}),
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(` ! usage not reported: ${e instanceof Error ? e.message : e}`);
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("agent produced no .rafa/distill-verdicts.json — nothing resolved, nothing pushed; working set left intact");
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("distill-verdicts.json is not valid JSON — nothing resolved, nothing pushed");
262
+ die(
263
+ "distill-verdicts.json is not valid JSON — nothing resolved, nothing pushed",
264
+ );
227
265
  }
228
- if (!Array.isArray(verdicts)) die("distill-verdicts.json has no verdicts[] — aborting");
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(`no valid verdict for ${f.path} — nothing resolved, nothing pushed; working set left intact`);
234
- if (v.verdict !== "distilled" && (typeof v.note !== "string" || v.note.trim() === ""))
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("citation checker failed on the authored brain — nothing resolved, nothing pushed");
286
+ die(
287
+ "citation checker failed on the authored brain — nothing resolved, nothing pushed",
288
+ );
243
289
  if (runCompile([]) !== 0)
244
- die("compile gate failed on the authored brain — nothing resolved, nothing pushed");
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((f) => byPath.get(f.path).verdict === "distilled").length;
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(` ${v.verdict === "distilled" ? "✓" : "!"} ${f.path} → ${v.verdict}${v.note ? ` (${v.note})` : ""}`);
321
+ console.log(
322
+ ` ${v.verdict === "distilled" ? "✓" : "!"} ${f.path} → ${v.verdict}${v.note ? ` (${v.note})` : ""}`,
323
+ );
272
324
  } catch (e) {
273
- console.log(` ✗ ${f.path} — resolve failed: ${e instanceof Error ? e.message : e}`);
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 });