@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
@@ -1,478 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * rafa compile — the deterministic gate between the agents' markdown and the
4
- * platform. Reads every brain file, parses its frontmatter with a STRICT grammar
5
- * (see .rafa/contract.md §0), validates it against the contract, and on success
6
- * emits .rafa/manifest.json — the exact JSON the platform ingests.
7
- *
8
- * NO ASSUMPTIONS: a missing required field, a bad enum, or a malformed cite is a
9
- * hard error (path · field · rule). It never fills a default for a required field.
10
- * Exits 1 with a structured error list so the authoring agent can correct + retry.
11
- *
12
- * Usage: node .rafa/bin/rafa-compile.mjs [--repo=owner/repo] [--branch=main] [--codeSha=<sha>]
13
- */
14
- import {
15
- readFileSync,
16
- writeFileSync,
17
- readdirSync,
18
- existsSync,
19
- statSync,
20
- } from "node:fs";
21
- import { join } from "node:path";
22
- import { execSync } from "node:child_process";
23
-
24
- const SCHEMA_VERSION = 1;
25
- const arg = (k, d) => {
26
- const m = process.argv.find((a) => a.startsWith(`--${k}=`));
27
- return m ? m.slice(k.length + 3) : d;
28
- };
29
- const ROOT = arg("root", ".rafa");
30
- // Paths in the manifest are relative to the brain-repo root (`.rafa/` IS the root),
31
- // so the platform can fetch a prose body straight from the brain repo.
32
- const rel = (p) => (p.startsWith(ROOT + "/") ? p.slice(ROOT.length + 1) : p);
33
-
34
- const errors = [];
35
- const fail = (path, field, rule) => errors.push({ path, field, rule });
36
-
37
- // ── strict frontmatter parser (contract §0 grammar) ─────────────────────────
38
- // Legal value forms only: scalar | "quoted" | [flow,list] | { flow: map }.
39
- // Plus `key:` followed by ` - item` block lists. Anything else → error.
40
- function parseScalar(raw) {
41
- const s = raw.trim();
42
- if (s === "") return "";
43
- if (/^".*"$/.test(s) || /^'.*'$/.test(s)) return s.slice(1, -1);
44
- if (s === "true") return true;
45
- if (s === "false") return false;
46
- if (/^-?\d+$/.test(s)) return Number(s);
47
- return s;
48
- }
49
- // Strip a trailing YAML comment ( whitespace + # … ) from an UNQUOTED top-level
50
- // scalar. Not applied to block-list items (cites/links are exact strings that may
51
- // legitimately contain `#`).
52
- function stripComment(s) {
53
- if (/^["']/.test(s.trim())) return s;
54
- const i = s.search(/\s#/);
55
- return i === -1 ? s : s.slice(0, i);
56
- }
57
- function parseFlowList(raw) {
58
- const inner = raw.trim().slice(1, -1).trim();
59
- if (inner === "") return [];
60
- return inner.split(",").map((x) => parseScalar(x));
61
- }
62
- function parseFlowMap(raw) {
63
- const inner = raw.trim().slice(1, -1).trim();
64
- const out = {};
65
- if (inner === "") return out;
66
- for (const pair of inner.split(",")) {
67
- const i = pair.indexOf(":");
68
- if (i === -1) return null; // malformed
69
- out[pair.slice(0, i).trim()] = parseScalar(pair.slice(i + 1));
70
- }
71
- return out;
72
- }
73
- // Returns { data } or { error: "reason" }.
74
- function parseFrontmatter(text) {
75
- if (!text.startsWith("---")) return { error: "no frontmatter block" };
76
- const end = text.indexOf("\n---", text.indexOf("\n"));
77
- if (end === -1) return { error: "unterminated frontmatter" };
78
- const lines = text.slice(text.indexOf("\n") + 1, end).split("\n");
79
- const data = {};
80
- for (let i = 0; i < lines.length; i++) {
81
- const line = lines[i];
82
- if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
83
- const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*):(.*)$/);
84
- if (!m) return { error: `illegal line: ${JSON.stringify(line)}` };
85
- const key = m[1];
86
- // Strip a trailing comment first, so `cites: # note` reads as an empty value
87
- // (block list follows), not as the comment being the value.
88
- const rest = stripComment(m[2]).trim();
89
- if (rest === "") {
90
- // block list follows
91
- const items = [];
92
- while (i + 1 < lines.length && /^\s*-\s+/.test(lines[i + 1])) {
93
- items.push(parseScalar(lines[++i].replace(/^\s*-\s+/, "")));
94
- }
95
- data[key] = items;
96
- } else if (rest.startsWith("[")) {
97
- const close = rest.lastIndexOf("]"); // ignore any trailing comment after ]
98
- if (close === -1) return { error: `unclosed flow list at ${key}` };
99
- data[key] = parseFlowList(rest.slice(0, close + 1));
100
- } else if (rest.startsWith("{")) {
101
- const close = rest.lastIndexOf("}");
102
- if (close === -1) return { error: `unclosed flow map at ${key}` };
103
- const fm = parseFlowMap(rest.slice(0, close + 1));
104
- if (fm === null) return { error: `malformed flow map at ${key}` };
105
- data[key] = fm;
106
- } else {
107
- data[key] = parseScalar(rest);
108
- }
109
- }
110
- return { data };
111
- }
112
-
113
- // ── validators (contract §2–§6) ──────────────────────────────────────────────
114
- const CITE_RE = /^(.+):(\d+(?:-\d+)?)\s*::\s*(.+)$/;
115
- function parseCites(list, path) {
116
- if (!Array.isArray(list) || list.length === 0) {
117
- fail(path, "cites", "required · ≥ 1");
118
- return [];
119
- }
120
- const out = [];
121
- for (const c of list) {
122
- const m = String(c).match(CITE_RE);
123
- if (!m) {
124
- fail(path, "cites", `malformed cite: ${JSON.stringify(c)}`);
125
- continue;
126
- }
127
- out.push({ file: m[1].trim(), line: m[2], token: m[3].trim() });
128
- }
129
- return out;
130
- }
131
- const isEnum = (v, allowed) => allowed.includes(v);
132
- function reqStr(d, key, path) {
133
- const v = d[key];
134
- if (typeof v !== "string" || v.trim() === "") {
135
- fail(path, key, "required · non-empty string");
136
- return "";
137
- }
138
- return v;
139
- }
140
- function reqEnum(d, key, allowed, path) {
141
- const v = d[key];
142
- if (!isEnum(v, allowed)) {
143
- fail(path, key, `required · one of ${allowed.join("|")}`);
144
- return allowed[0];
145
- }
146
- return v;
147
- }
148
- function checkVersion(d, path) {
149
- if (d.schemaVersion !== SCHEMA_VERSION)
150
- fail(path, "schemaVersion", `must be ${SCHEMA_VERSION}`);
151
- }
152
- function checkId(d, path, name) {
153
- const stem = name.replace(/\.md$/, "");
154
- if (d.id !== stem) fail(path, "id", `must equal filename stem "${stem}"`);
155
- return stem;
156
- }
157
-
158
- function walk(dir) {
159
- if (!existsSync(dir)) return [];
160
- return readdirSync(dir).flatMap((e) => {
161
- const p = join(dir, e);
162
- return statSync(p).isDirectory()
163
- ? walk(p)
164
- : e.endsWith(".md") && !e.startsWith("_")
165
- ? [p]
166
- : [];
167
- });
168
- }
169
- const read = (p) => readFileSync(p, "utf8");
170
-
171
- function compileNotes(kind, dir) {
172
- const notes = [];
173
- for (const path of walk(dir)) {
174
- const name = path.split("/").pop();
175
- const { data, error } = parseFrontmatter(read(path));
176
- if (error) {
177
- fail(path, "frontmatter", error);
178
- continue;
179
- }
180
- checkVersion(data, path);
181
- const id = checkId(data, path, name);
182
- notes.push({
183
- id,
184
- kind,
185
- type: reqEnum(data, "type", ["contract", "convention", "flow", "how-to"], path),
186
- domain: reqStr(data, "domain", path),
187
- title: reqStr(data, "title", path),
188
- summary: reqStr(data, "summary", path),
189
- links: Array.isArray(data.links) ? data.links.map(String) : [],
190
- ...(data.failure === "silent" || data.failure === "loud"
191
- ? { failure: data.failure }
192
- : {}),
193
- cites: parseCites(data.cites, path),
194
- path: rel(path),
195
- });
196
- }
197
- return notes;
198
- }
199
-
200
- function compileImprovements(dir) {
201
- const out = [];
202
- for (const path of walk(dir)) {
203
- const name = path.split("/").pop();
204
- const { data, error } = parseFrontmatter(read(path));
205
- if (error) {
206
- fail(path, "frontmatter", error);
207
- continue;
208
- }
209
- checkVersion(data, path);
210
- const id = checkId(data, path, name);
211
- const lev = data.leverage;
212
- if (
213
- typeof lev !== "object" ||
214
- !isEnum(lev?.impact, ["low", "medium", "high"]) ||
215
- !isEnum(lev?.effort, ["low", "medium", "high"])
216
- )
217
- fail(path, "leverage", "required · { impact, effort } ∈ low|medium|high");
218
- out.push({
219
- id,
220
- priority: reqEnum(data, "priority", ["P0", "P1", "P2", "P3"], path),
221
- lens: reqEnum(
222
- data,
223
- "lens",
224
- ["security", "correctness", "performance", "architecture", "product", "ops"],
225
- path,
226
- ),
227
- status: reqEnum(data, "status", ["open", "backlog", "fixed", "wontfix"], path),
228
- title: reqStr(data, "title", path),
229
- summary: reqStr(data, "summary", path),
230
- fix: reqStr(data, "fix", path),
231
- leverage: {
232
- impact: lev?.impact ?? "low",
233
- effort: lev?.effort ?? "low",
234
- },
235
- blastRadius: Array.isArray(data.blast_radius)
236
- ? data.blast_radius.map(String)
237
- : [],
238
- cites: parseCites(data.cites, path),
239
- path: rel(path),
240
- });
241
- }
242
- return out;
243
- }
244
-
245
- function compileHealth() {
246
- const path = join(ROOT, "brain", "checklist.md");
247
- if (!existsSync(path)) return null;
248
- const { data, error } = parseFrontmatter(read(path));
249
- if (error) {
250
- fail(path, "frontmatter", error);
251
- return null;
252
- }
253
- checkVersion(data, path);
254
- const g = data.gates,
255
- c = data.counts;
256
- if (!isEnum(g?.fidelity, ["pass", "fail"]) || !isEnum(g?.coverage, ["pass", "fail"]))
257
- fail(path, "gates", "required · { fidelity, coverage } ∈ pass|fail");
258
- if (
259
- typeof c?.blockers !== "number" ||
260
- typeof c?.majors !== "number" ||
261
- typeof c?.minors !== "number"
262
- )
263
- fail(path, "counts", "required · { blockers, majors, minors } ints");
264
- if (typeof data.score !== "number") fail(path, "score", "required · 0–100 int");
265
- return {
266
- verdict: reqEnum(data, "verdict", ["PASS", "ITERATE"], path),
267
- score: typeof data.score === "number" ? data.score : 0,
268
- gates: { fidelity: g?.fidelity ?? "fail", coverage: g?.coverage ?? "fail" },
269
- counts: {
270
- blockers: c?.blockers ?? 0,
271
- majors: c?.majors ?? 0,
272
- minors: c?.minors ?? 0,
273
- },
274
- };
275
- }
276
-
277
- function compileLedger() {
278
- const path = join(ROOT, "improve", "ledger.md");
279
- if (!existsSync(path)) return null;
280
- const { data, error } = parseFrontmatter(read(path));
281
- if (error) {
282
- fail(path, "frontmatter", error);
283
- return null;
284
- }
285
- checkVersion(data, path);
286
- const bp = data.by_priority;
287
- const okBp =
288
- bp &&
289
- ["P0", "P1", "P2", "P3"].every((k) => typeof bp[k] === "number");
290
- if (!okBp) fail(path, "by_priority", "required · { P0, P1, P2, P3 } ints");
291
- if (typeof data.open !== "number") fail(path, "open", "required · int");
292
- if (typeof data.debt_score !== "number")
293
- fail(path, "debt_score", "required · int");
294
- return {
295
- open: typeof data.open === "number" ? data.open : 0,
296
- debtScore: typeof data.debt_score === "number" ? data.debt_score : 0,
297
- byPriority: okBp
298
- ? { P0: bp.P0, P1: bp.P1, P2: bp.P2, P3: bp.P3 }
299
- : { P0: 0, P1: 0, P2: 0, P3: 0 },
300
- };
301
- }
302
-
303
- function compileCoverage() {
304
- const path = join(ROOT, "brain", "coverage.md");
305
- if (!existsSync(path)) return null;
306
- const { data, error } = parseFrontmatter(read(path));
307
- if (error) {
308
- fail(path, "frontmatter", error);
309
- return null;
310
- }
311
- checkVersion(data, path);
312
- const d = data.domains;
313
- if (typeof d !== "object" || Array.isArray(d)) {
314
- fail(path, "domains", "required · { domain: status } flow map");
315
- return { domains: [] };
316
- }
317
- const STATUS = ["mapped", "thin", "stubbed", "empty"];
318
- const domains = [];
319
- for (const [domain, status] of Object.entries(d)) {
320
- if (!STATUS.includes(status))
321
- fail(path, `domains.${domain}`, `status ∈ ${STATUS.join("|")}`);
322
- domains.push({ domain, status: String(status) });
323
- }
324
- return { domains };
325
- }
326
-
327
- // Plans (contract §7): parent+child files under plans/**. Global id uniqueness,
328
- // parent/child cross-checks, progress never stored, active.md → activePlanId.
329
- function compilePlans() {
330
- const plans = [];
331
- const seen = new Map(); // id → path (global uniqueness across plans/**)
332
- for (const path of walk(join(ROOT, "plans"))) {
333
- const name = path.split("/").pop();
334
- const { data, error } = parseFrontmatter(read(path));
335
- if (error) {
336
- fail(path, "frontmatter", error);
337
- continue;
338
- }
339
- checkVersion(data, path);
340
- const id = checkId(data, path, name);
341
- if (seen.has(id))
342
- fail(path, "id", `duplicate plan id "${id}" (also ${seen.get(id)}) · ids are globally unique across plans/**`);
343
- seen.set(id, path);
344
- if ("progress" in data)
345
- fail(path, "progress", "never stored · parent progress is derived by counting children");
346
- const kind = reqEnum(data, "kind", ["parent", "child"], path);
347
- const plan = reqStr(data, "plan", path);
348
- const status = data.status;
349
- if (kind === "child") {
350
- if (!isEnum(status, ["todo", "in-progress", "done", "blocked"]))
351
- fail(path, "status", "required (child) · todo|in-progress|done|blocked");
352
- if (data.parent !== plan)
353
- fail(path, "parent", `must equal plan "${plan}" for a child (flat v1)`);
354
- } else {
355
- if (data.parent !== null && data.parent !== "null")
356
- fail(path, "parent", "must be null for kind: parent");
357
- if (plan !== id) fail(path, "plan", `must equal id "${id}" for kind: parent`);
358
- if (
359
- status !== undefined &&
360
- !isEnum(status, ["todo", "in-progress", "done", "blocked"])
361
- )
362
- fail(path, "status", "optional (parent) · todo|in-progress|done|blocked");
363
- }
364
- plans.push({
365
- id,
366
- plan,
367
- parent: kind === "parent" ? null : plan,
368
- kind,
369
- title: reqStr(data, "title", path),
370
- ...(isEnum(status, ["todo", "in-progress", "done", "blocked"])
371
- ? { status }
372
- : {}),
373
- ...(typeof data.branch === "string" && data.branch !== ""
374
- ? { branch: data.branch }
375
- : {}),
376
- ...(typeof data.baseSha === "string" && data.baseSha !== ""
377
- ? { baseSha: String(data.baseSha) }
378
- : {}),
379
- path: rel(path),
380
- });
381
- }
382
- // Cross-check: every child's plan resolves to a kind:parent in this compile.
383
- const parents = new Set(plans.filter((p) => p.kind === "parent").map((p) => p.id));
384
- for (const p of plans)
385
- if (p.kind === "child" && !parents.has(p.plan))
386
- fail(p.path, "plan", `no parent plan "${p.plan}" in this compile (dangling child)`);
387
- return plans;
388
- }
389
-
390
- // active.md (generated pointer) → activePlanId. First line `# <plan-id>` must
391
- // resolve to a kind:parent plan; `# No active plan` or a missing file → null
392
- // (documented default: the pointer is generated, absence means "none").
393
- function compileActivePlanId(plans) {
394
- const path = join(ROOT, "active.md");
395
- if (!existsSync(path)) return null;
396
- const first = read(path).split("\n")[0].trim();
397
- const m = first.match(/^#\s+(.+)$/);
398
- if (!m) {
399
- fail(path, "pointer", 'first line must be "# <plan-id>" or "# No active plan"');
400
- return null;
401
- }
402
- const val = m[1].trim();
403
- if (/^no active plan$/i.test(val)) return null;
404
- if (!plans.some((p) => p.kind === "parent" && p.id === val)) {
405
- fail(path, "pointer", `active plan "${val}" is not a kind:parent plan in this compile`);
406
- return null;
407
- }
408
- return val;
409
- }
410
-
411
- // ── assemble ─────────────────────────────────────────────────────────────────
412
- const gitOpts = { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }; // no stderr leak
413
- function gitSha() {
414
- try {
415
- return execSync("git rev-parse HEAD", gitOpts).trim();
416
- } catch {
417
- return arg("codeSha", "");
418
- }
419
- }
420
- function gitBranch() {
421
- try {
422
- return execSync("git rev-parse --abbrev-ref HEAD", gitOpts).trim();
423
- } catch {
424
- return arg("branch", "main");
425
- }
426
- }
427
-
428
- const notes = [
429
- ...compileNotes("rule", join(ROOT, "brain", "rules")),
430
- ...compileNotes("playbook", join(ROOT, "brain", "playbooks")),
431
- ];
432
- const improvements = compileImprovements(join(ROOT, "improve", "improvements"));
433
- const health = compileHealth();
434
- const ledger = compileLedger();
435
- const coverage = compileCoverage();
436
- const plans = compilePlans();
437
- const activePlanId = compileActivePlanId(plans);
438
-
439
- // Cross-check: ledger.open / by_priority must match the actual open improvements.
440
- if (ledger) {
441
- const open = improvements.filter((i) => i.status === "open");
442
- const bp = { P0: 0, P1: 0, P2: 0, P3: 0 };
443
- for (const i of open) bp[i.priority]++;
444
- if (ledger.open !== open.length)
445
- fail("improve/ledger.md", "open", `says ${ledger.open}, rows have ${open.length}`);
446
- for (const k of ["P0", "P1", "P2", "P3"])
447
- if (ledger.byPriority[k] !== bp[k])
448
- fail("improve/ledger.md", `by_priority.${k}`, `says ${ledger.byPriority[k]}, rows have ${bp[k]}`);
449
- }
450
-
451
- if (errors.length) {
452
- console.error(`✗ rafa compile: ${errors.length} contract violation(s)\n`);
453
- for (const e of errors) console.error(` ${e.path} · ${e.field} · ${e.rule}`);
454
- console.error(`\nFix these files to match .rafa/contract.md, then re-run compile.`);
455
- process.exit(1);
456
- }
457
-
458
- const manifest = {
459
- schemaVersion: SCHEMA_VERSION,
460
- generatedAt: new Date().toISOString(),
461
- repo: arg("repo", ""),
462
- branch: gitBranch(),
463
- codeSha: gitSha(),
464
- health,
465
- ledger,
466
- coverage,
467
- notes,
468
- improvements,
469
- activePlanId,
470
- plans,
471
- };
472
- writeFileSync(join(ROOT, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
473
- console.log(
474
- `✓ rafa compile: ${notes.length} notes · ${improvements.length} improvements · ` +
475
- `${plans.length} plans${activePlanId ? ` (active: ${activePlanId})` : ""} · ` +
476
- `health ${health ? "ok" : "—"} · ledger ${ledger ? "ok" : "—"} · ` +
477
- `coverage ${coverage ? `${coverage.domains.length} domains` : "—"} → .rafa/manifest.json`,
478
- );
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * rafa push — commit `.rafa/` and push it to the brain remote.
4
- *
5
- * `.rafa/` is its own git repo (set up by `rafa init`, remote `origin` = your brain
6
- * repo). This commits the current brain/improve state and pushes it, stamped
7
- * `brain-for: <code HEAD sha>` so the brain stays anchored to the code it describes.
8
- * Uses YOUR git auth — no credential passes through rafa. Run from the code repo root.
9
- */
10
- import { execSync } from "node:child_process";
11
- import { existsSync } from "node:fs";
12
- import { join } from "node:path";
13
-
14
- const die = (m) => {
15
- console.error(`✗ ${m}`);
16
- process.exit(1);
17
- };
18
-
19
- const ROOT = process.cwd();
20
- const RAFA = join(ROOT, ".rafa");
21
- if (!existsSync(join(RAFA, ".git")))
22
- die("No `.rafa/` git repo here — run `rafa init` first, from your code repo root.");
23
-
24
- const inRafa = (cmd) =>
25
- execSync(cmd, { cwd: RAFA, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }).trim();
26
-
27
- try {
28
- inRafa("git remote get-url origin");
29
- } catch {
30
- die("`.rafa/` has no `origin` remote (the brain repo). Re-run `rafa init`.");
31
- }
32
-
33
- // Anchor the brain to the current code HEAD.
34
- let codeSha = "unknown";
35
- try {
36
- codeSha = execSync("git rev-parse --short HEAD", { cwd: ROOT, encoding: "utf8" }).trim();
37
- } catch {
38
- /* code repo may have no commits yet — fine */
39
- }
40
-
41
- // The CODE repo's full_name — stamped into the manifest as its identity.
42
- let repoFull = "";
43
- try {
44
- const origin = execSync("git remote get-url origin", { cwd: ROOT, encoding: "utf8" }).trim();
45
- const m = origin.match(/[/:]([^/:]+)\/([^/]+?)(?:\.git)?\/?$/);
46
- if (m) repoFull = `${m[1]}/${m[2]}`;
47
- } catch {
48
- /* no origin — manifest.repo stays "" */
49
- }
50
-
51
- // ── contract gate ──
52
- // Compile + validate the brain. A schema-invalid brain NEVER leaves the machine —
53
- // the platform ingests JSON, so what we push must already conform to the contract.
54
- console.log("• rafa compile — validating against .rafa/contract.md …");
55
- try {
56
- execSync(`node ${join(RAFA, "bin", "rafa-compile.mjs")} --repo=${repoFull}`, {
57
- cwd: ROOT,
58
- stdio: "inherit",
59
- });
60
- } catch {
61
- die(
62
- "brain failed the contract (see errors above). Fix the files, or have the " +
63
- "authoring agent (atlas/bloom/prism) correct them, then re-push.",
64
- );
65
- }
66
-
67
- inRafa("git add -A");
68
- try {
69
- inRafa(`git commit -m "brain: scan" -m "brain-for: ${codeSha}"`);
70
- } catch {
71
- console.log("• nothing new to commit — pushing current state");
72
- }
73
- inRafa("git branch -M main");
74
- inRafa("git push -u origin main");
75
- console.log(`✓ Pushed .rafa/ → brain remote (brain-for: ${codeSha})`);