@rafinery/cli 0.2.2 → 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.
- package/CHANGELOG.md +105 -0
- package/README.md +17 -7
- package/bin/rafa.mjs +78 -8
- package/blueprint/.claude/agents/atlas.md +28 -20
- package/blueprint/.claude/agents/bloom.md +15 -8
- package/blueprint/.claude/agents/compass.md +46 -0
- package/blueprint/.claude/agents/prism.md +42 -25
- package/blueprint/.claude/commands/rafa.md +190 -13
- package/blueprint/.claude/rafa/contract.md +482 -0
- package/blueprint/.claude/skills/rafa-build/SKILL.md +76 -0
- package/blueprint/.claude/skills/rafa-distill/SKILL.md +87 -0
- package/blueprint/{.rafa/capabilities/improve.md → .claude/skills/rafa-improve/SKILL.md} +10 -5
- package/blueprint/.claude/skills/rafa-insights/SKILL.md +71 -0
- package/blueprint/{.rafa/capabilities/leverage.md → .claude/skills/rafa-leverage/SKILL.md} +9 -4
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +74 -0
- package/blueprint/{.rafa/capabilities/scan.md → .claude/skills/rafa-scan/SKILL.md} +14 -9
- package/blueprint/{.rafa/capabilities/validate.md → .claude/skills/rafa-validate/SKILL.md} +14 -5
- package/lib/blueprint.mjs +22 -12
- package/lib/brain-repo.mjs +100 -0
- package/lib/checkpoint.mjs +138 -0
- package/lib/ci-setup.mjs +109 -0
- package/lib/claude-config.mjs +5 -5
- package/lib/compile.mjs +6 -21
- package/lib/distill.mjs +237 -0
- package/lib/fold.mjs +53 -0
- package/lib/gate/compile.mjs +549 -0
- package/lib/gate/verify-citations.mjs +156 -0
- package/lib/hydrate.mjs +96 -0
- package/lib/init.mjs +69 -35
- package/lib/leverage/adapters/claude-code.mjs +5 -4
- package/lib/mcp-client.mjs +97 -0
- package/lib/mcp-config.mjs +93 -0
- package/lib/migrations/index.mjs +39 -3
- package/lib/pull.mjs +129 -0
- package/lib/push.mjs +93 -14
- package/lib/releases.mjs +36 -0
- package/lib/verify-citations.mjs +9 -0
- package/lib/working-set.mjs +113 -0
- package/package.json +2 -2
- package/blueprint/.rafa/bin/rafa-compile.mjs +0 -390
- package/blueprint/.rafa/bin/rafa-push.mjs +0 -75
- package/blueprint/.rafa/bin/verify-citations.mjs +0 -153
- package/blueprint/.rafa/capabilities/build.md +0 -38
- package/blueprint/.rafa/capabilities/plan.md +0 -38
- package/blueprint/.rafa/contract.md +0 -303
|
@@ -1,390 +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
|
-
// ── assemble ─────────────────────────────────────────────────────────────────
|
|
328
|
-
const gitOpts = { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }; // no stderr leak
|
|
329
|
-
function gitSha() {
|
|
330
|
-
try {
|
|
331
|
-
return execSync("git rev-parse HEAD", gitOpts).trim();
|
|
332
|
-
} catch {
|
|
333
|
-
return arg("codeSha", "");
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
function gitBranch() {
|
|
337
|
-
try {
|
|
338
|
-
return execSync("git rev-parse --abbrev-ref HEAD", gitOpts).trim();
|
|
339
|
-
} catch {
|
|
340
|
-
return arg("branch", "main");
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const notes = [
|
|
345
|
-
...compileNotes("rule", join(ROOT, "brain", "rules")),
|
|
346
|
-
...compileNotes("playbook", join(ROOT, "brain", "playbooks")),
|
|
347
|
-
];
|
|
348
|
-
const improvements = compileImprovements(join(ROOT, "improve", "improvements"));
|
|
349
|
-
const health = compileHealth();
|
|
350
|
-
const ledger = compileLedger();
|
|
351
|
-
const coverage = compileCoverage();
|
|
352
|
-
|
|
353
|
-
// Cross-check: ledger.open / by_priority must match the actual open improvements.
|
|
354
|
-
if (ledger) {
|
|
355
|
-
const open = improvements.filter((i) => i.status === "open");
|
|
356
|
-
const bp = { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
357
|
-
for (const i of open) bp[i.priority]++;
|
|
358
|
-
if (ledger.open !== open.length)
|
|
359
|
-
fail("improve/ledger.md", "open", `says ${ledger.open}, rows have ${open.length}`);
|
|
360
|
-
for (const k of ["P0", "P1", "P2", "P3"])
|
|
361
|
-
if (ledger.byPriority[k] !== bp[k])
|
|
362
|
-
fail("improve/ledger.md", `by_priority.${k}`, `says ${ledger.byPriority[k]}, rows have ${bp[k]}`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (errors.length) {
|
|
366
|
-
console.error(`✗ rafa compile: ${errors.length} contract violation(s)\n`);
|
|
367
|
-
for (const e of errors) console.error(` ${e.path} · ${e.field} · ${e.rule}`);
|
|
368
|
-
console.error(`\nFix these files to match .rafa/contract.md, then re-run compile.`);
|
|
369
|
-
process.exit(1);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const manifest = {
|
|
373
|
-
schemaVersion: SCHEMA_VERSION,
|
|
374
|
-
generatedAt: new Date().toISOString(),
|
|
375
|
-
repo: arg("repo", ""),
|
|
376
|
-
branch: gitBranch(),
|
|
377
|
-
codeSha: gitSha(),
|
|
378
|
-
health,
|
|
379
|
-
ledger,
|
|
380
|
-
coverage,
|
|
381
|
-
notes,
|
|
382
|
-
improvements,
|
|
383
|
-
plans: [],
|
|
384
|
-
};
|
|
385
|
-
writeFileSync(join(ROOT, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
386
|
-
console.log(
|
|
387
|
-
`✓ rafa compile: ${notes.length} notes · ${improvements.length} improvements · ` +
|
|
388
|
-
`health ${health ? "ok" : "—"} · ledger ${ledger ? "ok" : "—"} · ` +
|
|
389
|
-
`coverage ${coverage ? `${coverage.domains.length} domains` : "—"} → .rafa/manifest.json`,
|
|
390
|
-
);
|
|
@@ -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})`);
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Deterministic citation checker for the atlas scan output. Three gates:
|
|
4
|
-
*
|
|
5
|
-
* RESOLUTION (B1) — every cite `- <file>:<line>[-<end>] :: <token>` points at a line
|
|
6
|
-
* that actually contains <token>. Catches off-by-N / wrong-file.
|
|
7
|
-
* COMPLETENESS (B2) — a note may declare `anchor: <token>`; the checker greps it repo-wide
|
|
8
|
-
* (code only, docs excluded) and asserts EVERY hit is a cited site.
|
|
9
|
-
* Catches the "3-place vs 5-place" omission bug.
|
|
10
|
-
* POLICY — every `type: contract` MUST declare `anchor:` — a greppable token,
|
|
11
|
-
* or `anchor: none` for composition/ordering contracts that don't grep
|
|
12
|
-
* as one token. So completeness can't be silently skipped (closes F1:
|
|
13
|
-
* the guarantee is mandatory, not opt-in; exemptions are explicit).
|
|
14
|
-
*
|
|
15
|
-
* Writes a generated report to .rafa/brain/citation-check.md (don't hand-paste it).
|
|
16
|
-
* Exits 1 on any failure. Run from repo root: node .rafa/bin/verify-citations.mjs
|
|
17
|
-
*/
|
|
18
|
-
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
19
|
-
import { execSync } from "node:child_process";
|
|
20
|
-
import { join } from "node:path";
|
|
21
|
-
import { tmpdir } from "node:os";
|
|
22
|
-
|
|
23
|
-
const arg = (k, d) => { const m = process.argv.find((a) => a.startsWith(`--${k}=`)); return m ? m.slice(k.length + 3) : d; };
|
|
24
|
-
const ROOT = arg("root", ".rafa/brain"); // --root=.rafa/improve for the improve ledger
|
|
25
|
-
const NOTE_DIRS = arg("dirs", "rules,playbooks").split(",").filter(Boolean); // --dirs=improvements
|
|
26
|
-
const CITE = /^\s*-\s+(.+?):(\d+)(?:-(\d+))?\s*::\s*(.+?)\s*$/; // - file:start[-end] :: token
|
|
27
|
-
const ANCHOR = /^anchor:\s*(.+?)\s*$/;
|
|
28
|
-
const TYPE = /^type:\s*(.+?)\s*$/;
|
|
29
|
-
|
|
30
|
-
function walk(dir) {
|
|
31
|
-
if (!existsSync(dir)) return [];
|
|
32
|
-
return readdirSync(dir).flatMap((e) => {
|
|
33
|
-
const p = join(dir, e);
|
|
34
|
-
return statSync(p).isDirectory() ? walk(p) : (e.endsWith(".md") && !e.startsWith("_")) ? [p] : []; // ignore _*.md scratch
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function gitGrep(token) {
|
|
39
|
-
try {
|
|
40
|
-
const out = execSync(`git grep -nF -e ${JSON.stringify(token)}`, { encoding: "utf8" });
|
|
41
|
-
return out.split("\n").filter(Boolean).map((l) => {
|
|
42
|
-
const m = l.match(/^(.+?):(\d+):/);
|
|
43
|
-
return m ? { file: m[1], line: +m[2] } : null;
|
|
44
|
-
}).filter(Boolean);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
if (e.status === 1) return []; // git grep: no matches
|
|
47
|
-
throw e;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function citeResolves(file, start, end, token) {
|
|
52
|
-
if (!existsSync(file)) return { ok: false, reason: "file missing" };
|
|
53
|
-
const src = readFileSync(file, "utf8").split("\n");
|
|
54
|
-
if (start < 1 || end > src.length) return { ok: false, reason: `lines ${start}-${end} out of range (${src.length})` };
|
|
55
|
-
if (src.slice(start - 1, end).some((l) => l.includes(token))) return { ok: true, reason: "" };
|
|
56
|
-
return { ok: false, reason: `token not on ${start}${end !== start ? "-" + end : ""}` };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// --selftest: prove the resolution logic fails on a bad cite and passes a good one, on a
|
|
60
|
-
// throwaway temp file (no brain/repo pollution). prism runs this as its mutation probe —
|
|
61
|
-
// re-running the checker is not proof it still works; this is.
|
|
62
|
-
if (process.argv.includes("--selftest")) {
|
|
63
|
-
const dir = mkdtempSync(join(tmpdir(), "vc-selftest-"));
|
|
64
|
-
const f = join(dir, "sample.txt");
|
|
65
|
-
writeFileSync(f, "alpha\nTOKEN beta\ngamma\n");
|
|
66
|
-
const good = citeResolves(f, 2, 2, "TOKEN").ok; // expect true
|
|
67
|
-
const bad = citeResolves(f, 1, 1, "TOKEN").ok; // expect false (off-by-one)
|
|
68
|
-
rmSync(dir, { recursive: true, force: true });
|
|
69
|
-
const sane = good === true && bad === false;
|
|
70
|
-
console.log(`checker self-test: ${sane ? "PASS" : "FAIL"} — good cite=${good} (expect true), bad cite=${bad} (expect false)`);
|
|
71
|
-
process.exit(sane ? 0 : 1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const notes = NOTE_DIRS.flatMap((d) => walk(join(ROOT, d)));
|
|
75
|
-
if (notes.length === 0) {
|
|
76
|
-
console.log(`No notes under ${ROOT}/{${NOTE_DIRS.join(",")}}/ — run a scan first.`);
|
|
77
|
-
process.exit(0);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const resolution = []; // { note, loc, token, ok, reason }
|
|
81
|
-
const completeness = []; // { note, anchor, site, ok, reason }
|
|
82
|
-
const policy = []; // { note, ok, reason }
|
|
83
|
-
|
|
84
|
-
for (const note of notes) {
|
|
85
|
-
const rel = note.replace(ROOT + "/", "");
|
|
86
|
-
const lines = readFileSync(note, "utf8").split("\n");
|
|
87
|
-
const cites = [];
|
|
88
|
-
const anchors = [];
|
|
89
|
-
let noteType = "";
|
|
90
|
-
let fence = 0;
|
|
91
|
-
for (const line of lines) {
|
|
92
|
-
if (line.trim() === "---") { fence++; if (fence >= 2) break; continue; }
|
|
93
|
-
if (fence !== 1) continue;
|
|
94
|
-
const c = line.match(CITE);
|
|
95
|
-
if (c) { cites.push({ file: c[1], start: +c[2], end: c[3] ? +c[3] : +c[2], token: c[4] }); continue; }
|
|
96
|
-
const a = line.match(ANCHOR);
|
|
97
|
-
if (a) { anchors.push(a[1].replace(/\s+#.*$/, "").replace(/^["']|["']$/g, "").trim()); continue; }
|
|
98
|
-
const t = line.match(TYPE);
|
|
99
|
-
if (t) noteType = t[1].replace(/\s+#.*$/, "").trim();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// POLICY — every contract must declare an anchor (token or explicit `none`)
|
|
103
|
-
if (noteType === "contract") {
|
|
104
|
-
const ok = anchors.length > 0;
|
|
105
|
-
policy.push({ note: rel, ok, reason: ok ? "" : "type: contract must declare `anchor: <token>` (or `anchor: none` for composition/ordering contracts)" });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// RESOLUTION
|
|
109
|
-
for (const ct of cites) {
|
|
110
|
-
const { ok, reason } = citeResolves(ct.file, ct.start, ct.end, ct.token);
|
|
111
|
-
resolution.push({ note: rel, loc: `${ct.file}:${ct.start}${ct.end !== ct.start ? "-" + ct.end : ""}`, token: ct.token, ok, reason });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// COMPLETENESS
|
|
115
|
-
for (const anchor of anchors) {
|
|
116
|
-
if (anchor.toLowerCase() === "none") continue; // explicit, visible exemption
|
|
117
|
-
const hits = gitGrep(anchor).filter((h) => !h.file.endsWith(".md")); // docs excluded
|
|
118
|
-
if (hits.length === 0) {
|
|
119
|
-
completeness.push({ note: rel, anchor, site: "(none)", ok: false, reason: "anchor found nowhere in code — wrong token?" });
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
for (const h of hits) {
|
|
123
|
-
const covered = cites.some((ct) => ct.file === h.file && h.line >= ct.start && h.line <= ct.end);
|
|
124
|
-
completeness.push({ note: rel, anchor, site: `${h.file}:${h.line}`, ok: covered, reason: covered ? "" : "grep hit not cited — site omitted" });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const rFail = resolution.filter((r) => !r.ok);
|
|
130
|
-
const cFail = completeness.filter((r) => !r.ok);
|
|
131
|
-
const pFail = policy.filter((r) => !r.ok);
|
|
132
|
-
const fail = rFail.length + cFail.length + pFail.length;
|
|
133
|
-
|
|
134
|
-
const md = [
|
|
135
|
-
"# Citation check (generated — do not hand-edit)",
|
|
136
|
-
"",
|
|
137
|
-
`## Resolution (B1): ${resolution.length - rFail.length}/${resolution.length} ✓`,
|
|
138
|
-
...resolution.map((r) => `${r.ok ? "✓" : "✗"} ${r.loc} :: ${r.token}${r.ok ? "" : " — " + r.reason + " [" + r.note + "]"}`),
|
|
139
|
-
"",
|
|
140
|
-
`## Completeness (B2): ${completeness.length - cFail.length}/${completeness.length} ✓ (${new Set(completeness.map((c) => c.anchor)).size} anchors)`,
|
|
141
|
-
...completeness.map((r) => `${r.ok ? "✓" : "✗"} anchor '${r.anchor}' → ${r.site}${r.ok ? "" : " — " + r.reason + " [" + r.note + "]"}`),
|
|
142
|
-
"",
|
|
143
|
-
`## Policy (contract → anchor declared): ${policy.length - pFail.length}/${policy.length} ✓`,
|
|
144
|
-
...policy.map((r) => `${r.ok ? "✓" : "✗"} ${r.note}${r.ok ? "" : " — " + r.reason}`),
|
|
145
|
-
"",
|
|
146
|
-
fail ? `**${fail} FAILED.**` : "**All pass.**",
|
|
147
|
-
"",
|
|
148
|
-
].join("\n");
|
|
149
|
-
writeFileSync(join(ROOT, "citation-check.md"), md);
|
|
150
|
-
|
|
151
|
-
console.log(md);
|
|
152
|
-
console.log(`resolution ${resolution.length - rFail.length}/${resolution.length} · completeness ${completeness.length - cFail.length}/${completeness.length} · policy ${policy.length - pFail.length}/${policy.length}` + (fail ? ` · ${fail} FAILED` : " · all pass"));
|
|
153
|
-
process.exit(fail ? 1 : 0);
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# build — execute, brain-aware, compounding (DRAFT · capability #4)
|
|
2
|
-
|
|
3
|
-
> Status: **draft / spec.** The work-time loop where **recall + nudge + capture-back** all
|
|
4
|
-
> fire — the mission payoff. Depends on: plan (#3), brain (#1), ledger (#2). Built last.
|
|
5
|
-
|
|
6
|
-
Execute the approved plan with both stores in the loop. Invoke via `/build`.
|
|
7
|
-
|
|
8
|
-
## Why build fits the mission (the flywheel closes here)
|
|
9
|
-
|
|
10
|
-
Each task both **spends** the stores and **feeds** them — the compounding made real:
|
|
11
|
-
|
|
12
|
-
- **RECALL** — per task, load the brain slice for the touched domains: follow
|
|
13
|
-
conventions/contracts; honor **non-exemplars** (don't pattern-match the salient-but-wrong
|
|
14
|
-
example — the audit showed a cold agent paying a 100K-token correction loop for exactly this).
|
|
15
|
-
- **NUDGE** — surface the top-leverage open improvement in the task's blast radius: *"10-min fix
|
|
16
|
-
while you're here?"* Dismissible, never blocking. (bloom's deferred nudge, finally firing.)
|
|
17
|
-
- **LEVERAGE (orchestrate, don't bury)** — before hand-rolling a task, surface the best-fit
|
|
18
|
-
existing capability (skill/tool/MCP — committed or the dev's personal) to the host, and any
|
|
19
|
-
config that would help it fire. Recommend; the host orchestrates. Never reinvent what's present.
|
|
20
|
-
- **CAPTURE-BACK** — as work lands:
|
|
21
|
-
- decisions / scars → propose brain notes (**prism-validated** before they enter the brain),
|
|
22
|
-
**tool-agnostic** — capture *what + why + cited code*, never which (possibly personal) skill produced it,
|
|
23
|
-
- improvements fixed in passing → improve ledger **auto-close** (`status: fixed`),
|
|
24
|
-
- touched code → **incremental re-scan** of that domain so the brain stays fresh.
|
|
25
|
-
|
|
26
|
-
## Procedure (draft)
|
|
27
|
-
1. Resume from `active.md` (plan + active task).
|
|
28
|
-
2. Per task: **recall** brain slice → implement (convention-adherent) → **nudge** a
|
|
29
|
-
blast-radius fix (opt-in) → done-check.
|
|
30
|
-
3. **Capture-back**: distill decisions/scars (prism-validated); auto-close fixed improvements;
|
|
31
|
-
re-scan the touched domain.
|
|
32
|
-
4. Verify (prism-style) before declaring done. Update `active.md`.
|
|
33
|
-
|
|
34
|
-
## Deferred / open
|
|
35
|
-
- **The capture engine** — `Stop`/`PostToolUse` hooks to make capture-back *automatic*, not
|
|
36
|
-
model-discretion (the deterministic-enforcement lesson, applied to capture).
|
|
37
|
-
- **Incremental re-scan** — changed files + seam-neighbors only (keep it cheap).
|
|
38
|
-
- Show-thinking + pivot protocol from the atlas character.
|