@rafinery/cli 0.3.0 → 0.4.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +17 -7
  3. package/bin/rafa.mjs +68 -20
  4. package/blueprint/.claude/agents/atlas.md +28 -20
  5. package/blueprint/.claude/agents/bloom.md +15 -8
  6. package/blueprint/.claude/agents/compass.md +46 -0
  7. package/blueprint/.claude/agents/prism.md +42 -25
  8. package/blueprint/.claude/commands/rafa.md +151 -26
  9. package/blueprint/{.rafa → .claude/rafa}/contract.md +108 -22
  10. package/blueprint/.claude/skills/rafa-build/SKILL.md +76 -0
  11. package/blueprint/.claude/skills/rafa-distill/SKILL.md +87 -0
  12. package/blueprint/{.rafa/capabilities/improve.md → .claude/skills/rafa-improve/SKILL.md} +10 -5
  13. package/blueprint/.claude/skills/rafa-insights/SKILL.md +71 -0
  14. package/blueprint/{.rafa/capabilities/leverage.md → .claude/skills/rafa-leverage/SKILL.md} +9 -4
  15. package/blueprint/{.rafa/capabilities/plan.md → .claude/skills/rafa-plan/SKILL.md} +30 -3
  16. package/blueprint/{.rafa/capabilities/scan.md → .claude/skills/rafa-scan/SKILL.md} +14 -9
  17. package/blueprint/{.rafa/capabilities/validate.md → .claude/skills/rafa-validate/SKILL.md} +14 -5
  18. package/lib/blueprint.mjs +22 -12
  19. package/lib/brain-repo.mjs +100 -0
  20. package/lib/checkpoint.mjs +138 -0
  21. package/lib/ci-setup.mjs +109 -0
  22. package/lib/claude-config.mjs +5 -5
  23. package/lib/compile.mjs +6 -21
  24. package/lib/distill.mjs +237 -0
  25. package/lib/fold.mjs +53 -0
  26. package/lib/gate/compile.mjs +549 -0
  27. package/lib/gate/verify-citations.mjs +156 -0
  28. package/lib/hydrate.mjs +96 -0
  29. package/lib/init.mjs +15 -30
  30. package/lib/leverage/adapters/claude-code.mjs +5 -4
  31. package/lib/mcp-client.mjs +97 -0
  32. package/lib/mcp-config.mjs +1 -1
  33. package/lib/migrations/index.mjs +39 -3
  34. package/lib/pull.mjs +129 -0
  35. package/lib/push.mjs +93 -14
  36. package/lib/releases.mjs +23 -0
  37. package/lib/verify-citations.mjs +9 -0
  38. package/lib/working-set.mjs +113 -0
  39. package/package.json +2 -2
  40. package/blueprint/.rafa/bin/rafa-compile.mjs +0 -478
  41. package/blueprint/.rafa/bin/rafa-push.mjs +0 -75
  42. package/blueprint/.rafa/bin/verify-citations.mjs +0 -153
  43. package/blueprint/.rafa/capabilities/build.md +0 -39
@@ -0,0 +1,138 @@
1
+ // rafa checkpoint — sync this branch's WORKING SET to the platform (working-set
2
+ // architecture, ratified 2026-07-10).
3
+ //
4
+ // Deterministic by construction: the hydration sidecar makes "unedited hydrated
5
+ // file = disposable cache" a hash comparison. What syncs is exactly the edited
6
+ // or new brain files under .rafa/brain/**, keyed by the CURRENT code branch,
7
+ // under base-version CAS:
8
+ //
9
+ // accepted → sidecar records the new row version
10
+ // conflict → the newer copy lands beside the file as `<file>.theirs.md`
11
+ // (attributed) and this command exits 2 — the HUMAN in this
12
+ // session decides (merge/adopt/overwrite the local file), then
13
+ // re-runs `rafa checkpoint`, which pushes against the seen
14
+ // version. CI can flag staleness; it never resolves it.
15
+ //
16
+ // Checkpoint moments (the conductor invokes this): task done · plan approved ·
17
+ // explicit ask · cadence under session consent · git push/pull. Never session-end.
18
+
19
+ import { execSync } from "node:child_process";
20
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { callTool } from "./mcp-client.mjs";
23
+ import { hashOf, readSidecar, scanWorkingFiles, writeSidecar } from "./working-set.mjs";
24
+
25
+ const die = (m) => {
26
+ console.error(`✗ ${m}`);
27
+ process.exit(1);
28
+ };
29
+
30
+ export default async function checkpoint() {
31
+ const ROOT = process.cwd();
32
+
33
+ let branch = "";
34
+ try {
35
+ branch = execSync("git rev-parse --abbrev-ref HEAD", {
36
+ cwd: ROOT,
37
+ encoding: "utf8",
38
+ stdio: ["ignore", "pipe", "ignore"],
39
+ }).trim();
40
+ } catch {
41
+ die("not a git repo — run from your code repo root");
42
+ }
43
+ // The working set is BRANCH state. On the trunk, knowledge changes go
44
+ // through scan → compile → rafa push (the org brain's one writer surface).
45
+ if (branch === "main" || branch === "master") {
46
+ console.log(
47
+ `• on ${branch} — the working set is branch-scoped; org-brain changes on the trunk go through /rafa scan + rafa push. Nothing to checkpoint.`,
48
+ );
49
+ return;
50
+ }
51
+
52
+ const sidecar = readSidecar(ROOT);
53
+ const syncBranch = (sidecar.sync[branch] = sidecar.sync[branch] ?? {});
54
+
55
+ const candidates = [];
56
+ for (const { path, abs } of scanWorkingFiles(ROOT)) {
57
+ const content = readFileSync(abs, "utf8");
58
+ const hash = hashOf(content);
59
+ const hydrated = sidecar.files[path];
60
+ const synced = syncBranch[path];
61
+
62
+ if (synced?.hash === hash && synced.conflictSeen === undefined) continue; // already checkpointed
63
+ if (!synced && hydrated?.hash === hash) continue; // unedited hydration = disposable cache
64
+
65
+ candidates.push({
66
+ path,
67
+ content,
68
+ hash,
69
+ // CAS base: the version this client last saw. After a surfaced conflict,
70
+ // conflictSeen IS the seen version — pushing against it is the human's
71
+ // decision made in this session.
72
+ baseVersion: synced?.conflictSeen ?? synced?.version ?? null,
73
+ ...(hydrated?.baseBrainSha ? { baseBrainSha: hydrated.baseBrainSha } : {}),
74
+ });
75
+ }
76
+
77
+ if (candidates.length === 0) {
78
+ console.log(`✓ working set clean on ${branch} — nothing to checkpoint`);
79
+ return;
80
+ }
81
+
82
+ console.log(`• checkpointing ${candidates.length} file(s) on ${branch} …`);
83
+ let payload;
84
+ try {
85
+ payload = await callTool(ROOT, "checkpoint_sync", {
86
+ branch,
87
+ files: candidates.map(({ path, content, baseVersion, baseBrainSha }) => ({
88
+ path,
89
+ content,
90
+ baseVersion,
91
+ ...(baseBrainSha ? { baseBrainSha } : {}),
92
+ })),
93
+ });
94
+ } catch (e) {
95
+ die(e instanceof Error ? e.message : String(e));
96
+ }
97
+
98
+ const results = payload.results ?? [];
99
+ let conflicts = 0;
100
+ for (const r of results) {
101
+ const cand = candidates.find((c) => c.path === r.path);
102
+ if (r.ok) {
103
+ syncBranch[r.path] = { version: r.version, hash: cand?.hash ?? "" };
104
+ const theirs = join(ROOT, ".rafa", `${r.path}.theirs.md`);
105
+ if (existsSync(theirs)) rmSync(theirs);
106
+ console.log(` ✓ ${r.path} (v${r.version})`);
107
+ } else if (r.conflict) {
108
+ conflicts++;
109
+ const theirs = join(ROOT, ".rafa", `${r.path}.theirs.md`);
110
+ writeFileSync(
111
+ theirs,
112
+ `<!-- CONFLICT copy from the platform — v${r.conflict.version} by ${r.conflict.author ?? r.conflict.lastAuthor} at ${new Date(r.conflict.updatedAt).toISOString()}.\n` +
113
+ ` Your checkpoint was based on an older version. Decide: merge/adopt/keep, edit the\n` +
114
+ ` real file accordingly, then re-run \`rafa checkpoint\` (it will push YOUR file as\n` +
115
+ ` the decision). Delete this copy when done. -->\n\n` +
116
+ r.conflict.content,
117
+ );
118
+ syncBranch[r.path] = {
119
+ ...(syncBranch[r.path] ?? { version: 0, hash: "" }),
120
+ conflictSeen: r.conflict.version,
121
+ };
122
+ console.log(
123
+ ` ✗ ${r.path} — CONFLICT: v${r.conflict.version} by ${r.conflict.lastAuthor} is newer.\n` +
124
+ ` their copy: .rafa/${r.path}.theirs.md — decide, edit your file, re-run rafa checkpoint`,
125
+ );
126
+ } else {
127
+ console.log(` ✗ ${r.path} — ${r.error ?? "rejected"}`);
128
+ }
129
+ }
130
+ writeSidecar(ROOT, sidecar);
131
+
132
+ const accepted = results.filter((r) => r.ok).length;
133
+ console.log(
134
+ `${conflicts ? "!" : "✓"} checkpoint: ${accepted}/${results.length} synced on ${branch}` +
135
+ (conflicts ? ` · ${conflicts} conflict(s) need YOUR decision (see above)` : ""),
136
+ );
137
+ if (conflicts) process.exit(2);
138
+ }
@@ -0,0 +1,109 @@
1
+ // rafa ci-setup — install the reconciliation workflow (working-set
2
+ // architecture, ratified 2026-07-10, rules 6–7): the org's own CI folds
3
+ // branch→branch merges mechanically and DISTILLS merge-to-main with the org's
4
+ // own LLM key. Writes .github/workflows/rafa-reconcile.yml and names the
5
+ // secrets to configure — it configures nothing itself (secrets are the org's;
6
+ // nothing sensitive ever transits rafinery).
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+
11
+ const WORKFLOW_PATH = ".github/workflows/rafa-reconcile.yml";
12
+
13
+ const WORKFLOW = `# rafa reconcile — knowledge propagates exactly like the code it describes.
14
+ #
15
+ # branch → parent branch merge = MECHANICAL FOLD (no LLM, no prism)
16
+ # branch → default branch merge = DISTILLATION (prism-validated vs merged main,
17
+ # compile-gated, pushed to the brain repo)
18
+ #
19
+ # Secrets (Settings → Secrets and variables → Actions):
20
+ # RAFA_MCP_KEY an agent key for this repo (platform → repo → Agent access)
21
+ # ANTHROPIC_API_KEY the ORG'S OWN LLM key — used only inside this CI for
22
+ # distillation; never stored on the rafinery platform
23
+ # RAFA_BRAIN_TOKEN a token with push access to the BRAIN repo (distill only)
24
+ #
25
+ # Fires on the merge EVENT itself (PR closed+merged), so squash/rebase merges are
26
+ # detected correctly — git ancestry is never consulted. If distillation cannot run
27
+ # (missing secret, failed gate) it fails LOUD and the branch's working set stays
28
+ # intact — the next dev session gets the standard distill offer (fallback chain).
29
+
30
+ name: rafa reconcile
31
+
32
+ on:
33
+ pull_request:
34
+ types: [closed]
35
+
36
+ jobs:
37
+ fold:
38
+ name: fold working set (branch → parent)
39
+ if: github.event.pull_request.merged == true && github.event.pull_request.base.ref != github.event.repository.default_branch
40
+ runs-on: ubuntu-latest
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+ - uses: actions/setup-node@v4
44
+ with:
45
+ node-version: 20
46
+ - name: Mechanical fold (no LLM — rigor lives at main)
47
+ env:
48
+ RAFA_MCP_KEY: \${{ secrets.RAFA_MCP_KEY }}
49
+ run: npx -y @rafinery/cli fold --from "\${{ github.event.pull_request.head.ref }}" --to "\${{ github.event.pull_request.base.ref }}"
50
+
51
+ distill:
52
+ name: distill working set (merge to main)
53
+ if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == github.event.repository.default_branch
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ with:
58
+ ref: \${{ github.event.repository.default_branch }} # merged main = the validation target
59
+ - uses: actions/setup-node@v4
60
+ with:
61
+ node-version: 20
62
+ - name: Install the Agent SDK (runs on the org's own key)
63
+ run: npm i --no-save @anthropic-ai/claude-agent-sdk
64
+ - name: Distill into the org brain (prism-validated, compile-gated)
65
+ env:
66
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
67
+ RAFA_MCP_KEY: \${{ secrets.RAFA_MCP_KEY }}
68
+ RAFA_BRAIN_TOKEN: \${{ secrets.RAFA_BRAIN_TOKEN }}
69
+ run: npx -y @rafinery/cli distill --headless "\${{ github.event.pull_request.head.ref }}"
70
+ `;
71
+
72
+ export default async function ciSetup(args = []) {
73
+ const ROOT = process.cwd();
74
+ const target = join(ROOT, WORKFLOW_PATH);
75
+
76
+ if (existsSync(target)) {
77
+ const current = readFileSync(target, "utf8");
78
+ if (current === WORKFLOW) {
79
+ console.log(`✓ ${WORKFLOW_PATH} is already current`);
80
+ } else if (args.includes("--overwrite")) {
81
+ writeFileSync(target, WORKFLOW);
82
+ console.log(`✓ ${WORKFLOW_PATH} updated (--overwrite)`);
83
+ } else {
84
+ console.log(
85
+ `! ${WORKFLOW_PATH} exists and differs — left untouched (yours may be tuned).\n` +
86
+ " Re-run with --overwrite to replace it.",
87
+ );
88
+ }
89
+ } else {
90
+ mkdirSync(dirname(target), { recursive: true });
91
+ writeFileSync(target, WORKFLOW);
92
+ console.log(`✓ wrote ${WORKFLOW_PATH}`);
93
+ }
94
+
95
+ console.log(`
96
+ Next steps (repo Settings → Secrets and variables → Actions):
97
+ 1. RAFA_MCP_KEY — mint on the platform (repo → Agent access); scope: this repo.
98
+ 2. ANTHROPIC_API_KEY — the ORG'S OWN LLM key. Used only inside YOUR CI for
99
+ merge-to-main distillation; never stored on the rafinery
100
+ platform (hard rule).
101
+ 3. RAFA_BRAIN_TOKEN — a token with push access to the BRAIN repo (the distill
102
+ job pushes validated knowledge there).
103
+ 4. Commit the workflow via your normal MR flow.
104
+
105
+ The rigor gradient this wires: dev↔dev = CAS + session prompt · branch↔branch =
106
+ free mechanical fold · branch→main = full distillation + gates. Cost tracks
107
+ consequence. Without CI, nothing is lost — the next dev session's bootstrap
108
+ digest offers the same reconciliation (/rafa distill).`);
109
+ }
@@ -4,13 +4,13 @@
4
4
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
 
7
- // The permissions rafa's installed tooling needs to run without a prompt each time.
8
- // Kept minimal + specific to what `rafa init` actually vendors into `.rafa/bin`.
7
+ // The permissions rafa's tooling needs to run without a prompt each time.
8
+ // Blueprint split (0.4.0): the gate tools live inside @rafinery/cli, so the
9
+ // grant is the CLI itself (deterministic commands), not vendored scripts.
9
10
  export const RAFA_PERMISSIONS = [
10
11
  "Read(.rafa/**)",
11
- "Bash(node .rafa/bin/rafa-compile.mjs:*)",
12
- "Bash(node .rafa/bin/rafa-push.mjs:*)",
13
- "Bash(node .rafa/bin/verify-citations.mjs:*)",
12
+ "Bash(npx @rafinery/cli:*)",
13
+ "Bash(rafa:*)",
14
14
  ];
15
15
 
16
16
  export function settingsPath(targetDir) {
package/lib/compile.mjs CHANGED
@@ -1,24 +1,9 @@
1
- // rafa compile — run the vendored contract gate on this repo's brain. Thin wrapper
2
- // over .rafa/bin/rafa-compile.mjs (the client's own copy = its contract version).
1
+ // rafa compile — run the contract gate on this repo's brain. The gate lives
2
+ // INSIDE the CLI (blueprint split, 0.4.0) it moves with the CLI version,
3
+ // never vendored, so a repo is gate-current the moment the CLI is.
3
4
 
4
- import { existsSync } from "node:fs";
5
- import { execSync } from "node:child_process";
6
- import { join } from "node:path";
5
+ import { runCompile } from "./gate/compile.mjs";
7
6
 
8
- export default async function compile(args) {
9
- const script = join(process.cwd(), ".rafa", "bin", "rafa-compile.mjs");
10
- if (!existsSync(script)) {
11
- console.error(
12
- "✗ .rafa/bin/rafa-compile.mjs not found — run `rafa init` (or `rafa update`) first.",
13
- );
14
- process.exit(1);
15
- }
16
- try {
17
- execSync(`node ${JSON.stringify(script)} ${args.join(" ")}`.trim(), {
18
- stdio: "inherit",
19
- cwd: process.cwd(),
20
- });
21
- } catch {
22
- process.exit(1); // compile already printed its path·field·rule errors
23
- }
7
+ export default async function compile(args = []) {
8
+ process.exit(runCompile(args));
24
9
  }
@@ -0,0 +1,237 @@
1
+ // rafa distill --headless <branch> — merge-to-main DISTILLATION in the org's
2
+ // own CI (working-set architecture, ratified 2026-07-10, rule 7).
3
+ //
4
+ // The rigor moment of the gradient: every working-set file the branch captured
5
+ // is validated against the CHECKED-OUT MERGED MAIN (prism-shaped skepticism),
6
+ // survivors are authored into the org brain (atlas-shaped), and only what
7
+ // passes verify-citations + compile is pushed to the brain repo. Contested or
8
+ // low-confidence items are NEVER guessed — flagged needs-adjudication for the
9
+ // next dev session's digest offer.
10
+ //
11
+ // The judgment runs on the Agent SDK, driven by the ORG'S OWN LLM key
12
+ // (ANTHROPIC_API_KEY from CI secrets — never stored on the rafinery platform).
13
+ // The gates stay deterministic and OURS: this CLI collects, stages, re-runs
14
+ // the checkers itself (trust-but-verify), pushes, and resolves. The agent
15
+ // judges; it never ships.
16
+ //
17
+ // Fallback chain when this can't run (no key, no CI): the dev-session flow —
18
+ // /rafa distill <branch> in Claude Code (the bootstrap digest offers it).
19
+
20
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
21
+ import { createRequire } from "node:module";
22
+ import { dirname, join } from "node:path";
23
+ import { pathToFileURL } from "node:url";
24
+ import { callTool } from "./mcp-client.mjs";
25
+ import { runCompile } from "./gate/compile.mjs";
26
+ import { runVerifyCitations } from "./gate/verify-citations.mjs";
27
+ import pull from "./pull.mjs";
28
+ import push from "./push.mjs";
29
+
30
+ const die = (m) => {
31
+ console.error(`✗ ${m}`);
32
+ process.exit(1);
33
+ };
34
+
35
+ // A brain-relative path from the platform must stay inside .rafa/ — a row that
36
+ // would escape is refused loudly (never written, never guessed).
37
+ const safeRel = (p) => {
38
+ if (p.startsWith("/") || p.split("/").includes("..")) die(`unsafe working-set path: ${p}`);
39
+ return p;
40
+ };
41
+
42
+ // 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`).
45
+ async function loadAgentSdk(cwd) {
46
+ try {
47
+ return await import("@anthropic-ai/claude-agent-sdk");
48
+ } catch {
49
+ 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).",
56
+ );
57
+ }
58
+ }
59
+ }
60
+
61
+ const VERDICTS = ["distilled", "refuted", "needs-adjudication"];
62
+
63
+ export default async function distill(args = []) {
64
+ const branch = args.filter((a) => !a.startsWith("-"))[0];
65
+ if (!args.includes("--headless"))
66
+ die(
67
+ "session distillation runs in Claude Code (/rafa distill <branch> — offer-driven at bootstrap).\n" +
68
+ " This CLI runs the CI path: rafa distill --headless <branch>",
69
+ );
70
+ if (!branch) die("usage: rafa distill --headless <branch>");
71
+ if (!process.env.ANTHROPIC_API_KEY)
72
+ die(
73
+ "ANTHROPIC_API_KEY is not set — CI distillation runs on the ORG'S OWN LLM key from CI\n" +
74
+ " secrets (never stored on the rafinery platform). Add the secret (rafa ci-setup names\n" +
75
+ " it), or fall back to the dev session: /rafa distill " + branch,
76
+ );
77
+
78
+ const ROOT = process.cwd();
79
+
80
+ // 1 · collect the branch working set. Adjudication-flagged rows are LEFT for
81
+ // a human session (CI can flag divergence, never resolve it).
82
+ console.log(`• collecting working set for ${branch} …`);
83
+ let active, flagged;
84
+ try {
85
+ active = (await callTool(ROOT, "get_working_set", { branch, status: "active" })).files ?? [];
86
+ flagged =
87
+ (await callTool(ROOT, "get_working_set", { branch, status: "needs-adjudication" }))
88
+ .files ?? [];
89
+ } catch (e) {
90
+ die(e instanceof Error ? e.message : String(e));
91
+ }
92
+ if (flagged.length)
93
+ console.log(
94
+ ` ! ${flagged.length} file(s) already need adjudication — left for the next dev session's digest`,
95
+ );
96
+ if (active.length === 0) {
97
+ console.log(`✓ nothing to distill on ${branch}`);
98
+ return;
99
+ }
100
+ console.log(` ${active.length} file(s) to distill`);
101
+
102
+ // 2 · mirror the org brain locally — the authoring target + the base the
103
+ // survivors fold into. (CI checkout is merged main = the validation target.)
104
+ await pull(["--full"]);
105
+
106
+ // 3 · stage the incoming working set for the agent (gitignored inside .rafa).
107
+ const stagingRoot = join(ROOT, ".rafa", "distill-incoming");
108
+ rmSync(stagingRoot, { recursive: true, force: true });
109
+ for (const f of active) {
110
+ const abs = join(stagingRoot, safeRel(f.path));
111
+ mkdirSync(dirname(abs), { recursive: true });
112
+ writeFileSync(
113
+ abs,
114
+ `<!-- incoming working-set file · branch: ${branch} · author: ${f.lastAuthor} · v${f.version}` +
115
+ `${f.baseBrainSha ? ` · base: ${f.baseBrainSha}` : ""} -->\n` +
116
+ f.content,
117
+ );
118
+ }
119
+ const verdictsPath = join(ROOT, ".rafa", "distill-verdicts.json");
120
+ rmSync(verdictsPath, { force: true });
121
+
122
+ // 4 · the judgment pass (org's own key; gates stay deterministic below).
123
+ const sdk = await loadAgentSdk(ROOT);
124
+ const fileList = active.map((f) => `- ${f.path} (author: ${f.lastAuthor})`).join("\n");
125
+ const prompt = `You are running rafa's CI DISTILLATION — merge-time reconciliation of branch "${branch}"'s
126
+ working set into the org brain. The checked-out repo IS merged main (the ground truth).
127
+ The current org brain is mirrored at .rafa/brain/. The incoming candidate-grade files are
128
+ staged under .rafa/distill-incoming/ (brain-relative paths mirrored; a provenance comment
129
+ tops each file).
130
+
131
+ Read .claude/skills/rafa-distill/SKILL.md (the SOP) and .claude/rafa/contract.md §2 (the
132
+ note schema) first.
133
+
134
+ For EACH staged file:
135
+ 1. VALIDATE like prism — skeptical, code-grounded: does every claim hold against the code
136
+ as it now stands on main? Confirm each citation resolves (the cited line contains the
137
+ token; run \`git grep\` yourself). A claim you cannot confirm with a file:line is
138
+ REFUTED (note the cited reason). Contested or genuinely uncertain → verdict
139
+ "needs-adjudication" (NEVER guess; a wrong note is worse than none).
140
+ 2. For survivors, AUTHOR like atlas — write/update the org-brain file under .rafa/brain/
141
+ per the contract: valid frontmatter, real cites into main, \`anchor:\` on contracts;
142
+ fold into an existing note when one already covers the topic (supersede, never
143
+ duplicate). Never copy the provenance comment into the brain file.
144
+ 3. When all survivors are authored, run \`npx @rafinery/cli verify-citations\` and
145
+ \`npx @rafinery/cli compile\` and fix your authored files until BOTH exit 0.
146
+
147
+ Finally write .rafa/distill-verdicts.json:
148
+ {"verdicts":[{"path":"<staged brain-relative path>","verdict":"distilled|refuted|needs-adjudication","note":"<cited reason — required for refuted and needs-adjudication>"}]}
149
+ with EXACTLY one entry per staged file listed below. Do not modify anything outside
150
+ .rafa/ except nothing — the code repo is read-only ground truth.
151
+
152
+ Staged files:
153
+ ${fileList}`;
154
+
155
+ console.log("• running the distillation agent (org's own ANTHROPIC_API_KEY) …");
156
+ const run = sdk.query({
157
+ prompt,
158
+ options: {
159
+ cwd: ROOT,
160
+ permissionMode: "bypassPermissions",
161
+ allowedTools: ["Read", "Grep", "Glob", "Bash", "Write", "Edit"],
162
+ settingSources: [],
163
+ maxTurns: 150,
164
+ },
165
+ });
166
+ for await (const message of run) {
167
+ if (message.type === "result") {
168
+ if (message.subtype !== "success")
169
+ die(`distillation agent did not complete (${message.subtype}) — nothing resolved, nothing pushed`);
170
+ console.log(
171
+ ` agent done (${message.num_turns ?? "?"} turns · ` +
172
+ `$${(message.total_cost_usd ?? 0).toFixed(4)} on the org's key)`,
173
+ );
174
+ }
175
+ }
176
+
177
+ // 5 · verdicts must be complete — a missing entry is a hard stop, never a guess.
178
+ if (!existsSync(verdictsPath))
179
+ die("agent produced no .rafa/distill-verdicts.json — nothing resolved, nothing pushed; working set left intact");
180
+ let verdicts;
181
+ try {
182
+ verdicts = JSON.parse(readFileSync(verdictsPath, "utf8")).verdicts;
183
+ } catch {
184
+ die("distill-verdicts.json is not valid JSON — nothing resolved, nothing pushed");
185
+ }
186
+ if (!Array.isArray(verdicts)) die("distill-verdicts.json has no verdicts[] — aborting");
187
+ const byPath = new Map(verdicts.map((v) => [v.path, v]));
188
+ for (const f of active) {
189
+ const v = byPath.get(f.path);
190
+ if (!v || !VERDICTS.includes(v.verdict))
191
+ die(`no valid verdict for ${f.path} — nothing resolved, nothing pushed; working set left intact`);
192
+ if (v.verdict !== "distilled" && (typeof v.note !== "string" || v.note.trim() === ""))
193
+ die(`${f.path}: verdict "${v.verdict}" requires a cited note — aborting`);
194
+ }
195
+
196
+ // 6 · trust-but-verify: OUR gates re-run regardless of what the agent claims.
197
+ console.log("• re-running the gates (trust-but-verify) …");
198
+ if (runVerifyCitations([]) !== 0)
199
+ die("citation checker failed on the authored brain — nothing resolved, nothing pushed");
200
+ if (runCompile([]) !== 0)
201
+ die("compile gate failed on the authored brain — nothing resolved, nothing pushed");
202
+
203
+ // 7 · ship: brain repo push (webhook → ingest), staging cleaned first.
204
+ rmSync(stagingRoot, { recursive: true, force: true });
205
+ const distilledCount = active.filter((f) => byPath.get(f.path).verdict === "distilled").length;
206
+ if (distilledCount > 0) {
207
+ await push([]);
208
+ } else {
209
+ console.log("• no survivors — nothing to push to the brain repo");
210
+ }
211
+
212
+ // 8 · resolve the rows (CAS — a racing session's resolve loses loudly).
213
+ let refuted = 0,
214
+ adjudication = 0;
215
+ for (const f of active) {
216
+ const v = byPath.get(f.path);
217
+ if (v.verdict === "refuted") refuted++;
218
+ if (v.verdict === "needs-adjudication") adjudication++;
219
+ try {
220
+ await callTool(ROOT, "resolve_working_file", {
221
+ branch,
222
+ path: f.path,
223
+ resolution: v.verdict,
224
+ ...(v.note ? { note: v.note } : {}),
225
+ });
226
+ console.log(` ${v.verdict === "distilled" ? "✓" : "!"} ${f.path} → ${v.verdict}${v.note ? ` (${v.note})` : ""}`);
227
+ } catch (e) {
228
+ console.log(` ✗ ${f.path} — resolve failed: ${e instanceof Error ? e.message : e}`);
229
+ }
230
+ }
231
+ rmSync(verdictsPath, { force: true });
232
+
233
+ console.log(
234
+ `✓ distillation: ${distilledCount} into the org brain · ${refuted} refuted (reported, cited) · ` +
235
+ `${adjudication} flagged needs-adjudication (next session's digest)`,
236
+ );
237
+ }
package/lib/fold.mjs ADDED
@@ -0,0 +1,53 @@
1
+ // rafa fold — branch→parent-branch MECHANICAL fold of the working set
2
+ // (working-set architecture, ratified 2026-07-10, rule 6). No LLM, no prism —
3
+ // rigor lives only at main. The platform re-keys rows onto the parent:
4
+ //
5
+ // absent on parent → re-keyed silently (knowledge rides with the code)
6
+ // identical content → merged silently
7
+ // true divergence → needs-adjudication flag on the parent row
8
+ // (incoming preserved + attributed) — the next dev
9
+ // session's digest offers the decision; CI never
10
+ // resolves a human's divergence.
11
+ //
12
+ // Run by the reconciliation workflow on branch→branch merges (rafa ci-setup),
13
+ // or by hand: rafa fold --from <branch> --to <parent>
14
+
15
+ import { callTool } from "./mcp-client.mjs";
16
+
17
+ const die = (m) => {
18
+ console.error(`✗ ${m}`);
19
+ process.exit(1);
20
+ };
21
+
22
+ const flag = (args, name) => {
23
+ const i = args.indexOf(name);
24
+ return i !== -1 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : null;
25
+ };
26
+
27
+ export default async function fold(args = []) {
28
+ const from = flag(args, "--from");
29
+ const to = flag(args, "--to");
30
+ if (!from || !to) die("usage: rafa fold --from <branch> --to <parent-branch>");
31
+ if (from === to) die("--from and --to are the same branch");
32
+
33
+ let res;
34
+ try {
35
+ res = await callTool(process.cwd(), "fold_working_set", {
36
+ fromBranch: from,
37
+ toBranch: to,
38
+ });
39
+ } catch (e) {
40
+ die(e instanceof Error ? e.message : String(e));
41
+ }
42
+
43
+ console.log(
44
+ `✓ fold ${from} → ${to}: ${res.folded} file(s) · ` +
45
+ `${res.rekeyed} re-keyed · ${res.merged} identical (merged) · ` +
46
+ `${res.needsAdjudication} divergent → needs-adjudication`,
47
+ );
48
+ if (res.needsAdjudication > 0)
49
+ console.log(
50
+ " ! divergent copies were preserved + flagged — the next dev session's bootstrap digest\n" +
51
+ " offers the decision (CI detects divergence, never resolves it).",
52
+ );
53
+ }