@rafinery/cli 0.7.1 → 0.8.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 +58 -0
- package/LICENSE +21 -0
- package/bin/rafa.mjs +18 -3
- package/blueprint/.claude/agents/atlas.md +4 -3
- package/blueprint/.claude/agents/bloom.md +2 -1
- package/blueprint/.claude/agents/compass.md +2 -1
- package/blueprint/.claude/agents/prism.md +2 -1
- package/blueprint/.claude/agents/sage.md +2 -1
- package/blueprint/.claude/commands/rafa.md +4 -0
- package/blueprint/.claude/rafa/contract.md +86 -4
- package/blueprint/.claude/skills/rafa-build/SKILL.md +2 -1
- package/blueprint/.claude/skills/rafa-distill/SKILL.md +2 -1
- package/blueprint/.claude/skills/rafa-improve/SKILL.md +1 -1
- package/blueprint/.claude/skills/rafa-okf/SKILL.md +101 -0
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +1 -0
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +12 -7
- package/blueprint/.claude/skills/rafa-validate/SKILL.md +2 -1
- package/lib/blueprint.mjs +1 -0
- package/lib/ci-setup.mjs +7 -3
- package/lib/gate/compile.mjs +131 -86
- package/lib/gate/okf-emit.mjs +323 -0
- package/lib/gate/verify-citations.mjs +89 -3
- package/lib/mcp-client.mjs +43 -16
- package/lib/okf.mjs +110 -0
- package/lib/push.mjs +33 -17
- package/lib/releases.mjs +35 -0
- package/package.json +10 -7
|
@@ -38,6 +38,11 @@ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdtemp
|
|
|
38
38
|
import { execSync } from "node:child_process";
|
|
39
39
|
import { join } from "node:path";
|
|
40
40
|
import { tmpdir } from "node:os";
|
|
41
|
+
// Format utilities only (splitConcept/extractLinks/unresolvedLinks) — the five
|
|
42
|
+
// GATES above stay independently implemented; the LINKS lane below is a
|
|
43
|
+
// non-failing warn lane, so sharing the format package does not couple a gate
|
|
44
|
+
// to the producer it checks.
|
|
45
|
+
import { splitConcept, extractLinks, unresolvedLinks, OKF_RESERVED } from "@rafinery/okf";
|
|
41
46
|
|
|
42
47
|
export const CHECKER_VERSION = 2; // v1 = the 3-gate era (resolution/completeness/policy)
|
|
43
48
|
|
|
@@ -60,7 +65,26 @@ function walk(dir) {
|
|
|
60
65
|
if (!existsSync(dir)) return [];
|
|
61
66
|
return readdirSync(dir).flatMap((e) => {
|
|
62
67
|
const p = join(dir, e);
|
|
63
|
-
|
|
68
|
+
if (statSync(p).isDirectory()) return walk(p);
|
|
69
|
+
// ignore _*.md scratch + OKF-reserved listings (index.md is generated, never
|
|
70
|
+
// a note) + *.theirs.md conflict copies (compile gates those with a typed error)
|
|
71
|
+
return e.endsWith(".md") &&
|
|
72
|
+
!e.startsWith("_") &&
|
|
73
|
+
!e.endsWith(".theirs.md") &&
|
|
74
|
+
!OKF_RESERVED.has(e)
|
|
75
|
+
? [p]
|
|
76
|
+
: [];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Every .md in the bundle (reserved included — they are legitimate mdlink
|
|
81
|
+
// targets), as "/bundle-relative" paths for the LINKS lane's resolution set.
|
|
82
|
+
function walkBundleMd(dir, base = dir) {
|
|
83
|
+
if (!existsSync(dir)) return [];
|
|
84
|
+
return readdirSync(dir).flatMap((e) => {
|
|
85
|
+
const p = join(dir, e);
|
|
86
|
+
if (statSync(p).isDirectory()) return e === ".git" ? [] : walkBundleMd(p, base);
|
|
87
|
+
return e.endsWith(".md") ? ["/" + p.slice(base.length + 1)] : [];
|
|
64
88
|
});
|
|
65
89
|
}
|
|
66
90
|
|
|
@@ -178,12 +202,24 @@ export function runVerifyCitations(argv = []) {
|
|
|
178
202
|
return 0;
|
|
179
203
|
}
|
|
180
204
|
|
|
205
|
+
// LINKS lane resolution sets — BUNDLE-wide (a note may link across dirs):
|
|
206
|
+
// ids = concept stems of every knowledge class; paths = every bundle .md as
|
|
207
|
+
// "/bundle-relative" (reserved files included — legitimate mdlink targets).
|
|
208
|
+
const BUNDLE_ROOT = join(ROOT, "..");
|
|
209
|
+
const bundleIds = new Set(
|
|
210
|
+
["brain/rules", "brain/playbooks", "improve/improvements"]
|
|
211
|
+
.flatMap((d) => walk(join(BUNDLE_ROOT, d)))
|
|
212
|
+
.map((p) => p.split("/").pop().replace(/\.md$/, "")),
|
|
213
|
+
);
|
|
214
|
+
const bundlePaths = new Set(walkBundleMd(BUNDLE_ROOT));
|
|
215
|
+
|
|
181
216
|
const resolution = []; // { note, loc, token, ok, reason }
|
|
182
217
|
const completeness = []; // { note, anchor, site, ok, reason }
|
|
183
218
|
const policy = []; // { note, ok, reason }
|
|
184
219
|
const absence = []; // { note, token, site, ok, reason }
|
|
185
220
|
const inventory = []; // { name, glob, declared, found, ok, reason, paths }
|
|
186
221
|
const warns = []; // { note, phrase } — heuristic, never fails the gate
|
|
222
|
+
const linkWarns = []; // { note, kind, target } — dangling links (OKF §5.3), never fails the gate
|
|
187
223
|
|
|
188
224
|
for (const note of notes) {
|
|
189
225
|
const rel = note.replace(ROOT + "/", "");
|
|
@@ -256,6 +292,40 @@ export function runVerifyCitations(argv = []) {
|
|
|
256
292
|
const m = (title + " · " + summary).match(ABSENCE_HEURISTIC);
|
|
257
293
|
if (m) warns.push({ note: rel, phrase: m[0] });
|
|
258
294
|
}
|
|
295
|
+
|
|
296
|
+
// LINKS lane (non-failing, OKF §5.3): every frontmatter `links:` id, body
|
|
297
|
+
// [[wikilink]], and body bundle-relative markdown link should resolve to a
|
|
298
|
+
// bundle concept/file. A dangling link may be not-yet-written knowledge —
|
|
299
|
+
// prism's worklist, never a gate failure.
|
|
300
|
+
{
|
|
301
|
+
const raw = lines.join("\n");
|
|
302
|
+
const split = splitConcept(raw);
|
|
303
|
+
const body = split ? split.body : raw;
|
|
304
|
+
const fmLinks = [];
|
|
305
|
+
let inLinks = false;
|
|
306
|
+
for (const l of split?.fmLines ?? []) {
|
|
307
|
+
const flow = l.match(/^links:\s*\[(.*)\]\s*$/);
|
|
308
|
+
if (flow) {
|
|
309
|
+
for (const item of flow[1].split(",")) {
|
|
310
|
+
const v = strip(item);
|
|
311
|
+
if (v) fmLinks.push(v);
|
|
312
|
+
}
|
|
313
|
+
inLinks = false;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (/^links:\s*$/.test(l)) { inLinks = true; continue; }
|
|
317
|
+
if (inLinks) {
|
|
318
|
+
const item = l.match(/^\s*-\s+(.+?)\s*$/);
|
|
319
|
+
if (item) { fmLinks.push(strip(item[1])); continue; }
|
|
320
|
+
if (/^\S/.test(l)) inLinks = false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const dangling = unresolvedLinks(extractLinks({ links: fmLinks }, body), {
|
|
324
|
+
ids: bundleIds,
|
|
325
|
+
paths: bundlePaths,
|
|
326
|
+
});
|
|
327
|
+
for (const d of dangling) linkWarns.push({ note: rel, kind: d.kind, target: d.target });
|
|
328
|
+
}
|
|
259
329
|
}
|
|
260
330
|
|
|
261
331
|
// INVENTORY — coverage.md declared counts vs tracked reality
|
|
@@ -309,10 +379,24 @@ export function runVerifyCitations(argv = []) {
|
|
|
309
379
|
`## Warns (heuristic, non-failing — existence-shaped title/summary with no \`absent:\` declared): ${warns.length}`,
|
|
310
380
|
...warns.map((w) => `⚠ ${w.note} — reads as an absence claim ("${w.phrase}") — declare \`absent: <token>\` so the gate can re-grep it, or reword`),
|
|
311
381
|
"",
|
|
382
|
+
`## Links (non-failing, OKF §5.3 — dangling cross-links, bundle-wide resolution): ${linkWarns.length}`,
|
|
383
|
+
...linkWarns.map((w) => `⚠ ${w.note} — ${w.kind} → ${w.target} does not resolve in the bundle (not-yet-written knowledge, a typo, or a moved file)`),
|
|
384
|
+
"",
|
|
312
385
|
fail ? `**${fail} FAILED.**` : "**All pass.**",
|
|
313
386
|
"",
|
|
314
387
|
].join("\n");
|
|
315
|
-
|
|
388
|
+
// The report is itself a generated concept (contract §11): it self-describes.
|
|
389
|
+
const at = new Date().toISOString();
|
|
390
|
+
const reportFm = [
|
|
391
|
+
"---",
|
|
392
|
+
'type: "Citation Check"',
|
|
393
|
+
'title: "Citation check report"',
|
|
394
|
+
`description: "checker v${CHECKER_VERSION} — ${fail ? `${fail} failed` : "all gates pass"} · ${warns.length + linkWarns.length} warn(s)"`,
|
|
395
|
+
`timestamp: ${at}`,
|
|
396
|
+
"---",
|
|
397
|
+
"",
|
|
398
|
+
].join("\n");
|
|
399
|
+
writeFileSync(join(ROOT, "citation-check.md"), reportFm + md);
|
|
316
400
|
|
|
317
401
|
// Machine record of THIS run — compile folds it into manifest.citations so the
|
|
318
402
|
// platform knows the gate level a brain passed. Recorded, never assumed.
|
|
@@ -321,7 +405,7 @@ export function runVerifyCitations(argv = []) {
|
|
|
321
405
|
JSON.stringify(
|
|
322
406
|
{
|
|
323
407
|
checkerVersion: CHECKER_VERSION,
|
|
324
|
-
at
|
|
408
|
+
at,
|
|
325
409
|
pass: fail === 0,
|
|
326
410
|
gates: {
|
|
327
411
|
resolution: { pass: resolution.length - rFail.length, total: resolution.length },
|
|
@@ -331,6 +415,7 @@ export function runVerifyCitations(argv = []) {
|
|
|
331
415
|
inventory: { pass: inventory.length - iFail.length, total: inventory.length },
|
|
332
416
|
},
|
|
333
417
|
warns: warns.length,
|
|
418
|
+
linkWarns: linkWarns.length, // additive optional — compile only reads { checkerVersion, pass, at }
|
|
334
419
|
},
|
|
335
420
|
null,
|
|
336
421
|
2,
|
|
@@ -341,6 +426,7 @@ export function runVerifyCitations(argv = []) {
|
|
|
341
426
|
console.log(
|
|
342
427
|
`resolution ${resolution.length - rFail.length}/${resolution.length} · completeness ${completeness.length - cFail.length}/${completeness.length} · policy ${policy.length - pFail.length}/${policy.length} · absence ${absence.length - aFail.length}/${absence.length} · inventory ${inventory.length - iFail.length}/${inventory.length}` +
|
|
343
428
|
(warns.length ? ` · ${warns.length} warn(s)` : "") +
|
|
429
|
+
(linkWarns.length ? ` · ${linkWarns.length} dangling link(s)` : "") +
|
|
344
430
|
(fail ? ` · ${fail} FAILED` : " · all pass"),
|
|
345
431
|
);
|
|
346
432
|
return fail ? 1 : 0;
|
package/lib/mcp-client.mjs
CHANGED
|
@@ -26,7 +26,9 @@ const readJson = (file) => {
|
|
|
26
26
|
// The platform repo id this working copy is provisioned for.
|
|
27
27
|
export function repoIdOf(cwd) {
|
|
28
28
|
const stamp = readStamp(cwd);
|
|
29
|
-
return typeof stamp.repoId === "string" && stamp.repoId !== ""
|
|
29
|
+
return typeof stamp.repoId === "string" && stamp.repoId !== ""
|
|
30
|
+
? stamp.repoId
|
|
31
|
+
: null;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function resolveMcp(cwd) {
|
|
@@ -66,26 +68,51 @@ let rpcId = 0;
|
|
|
66
68
|
// message on transport or tool errors.
|
|
67
69
|
export async function callTool(cwd, tool, args = {}) {
|
|
68
70
|
const { repoId, key, mcpUrl } = resolveMcp(cwd);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
// A localhost platform is only reachable on the machine running the dev
|
|
72
|
+
// server — on a CI runner it is a guaranteed dead end; say so up front
|
|
73
|
+
// instead of a bare "fetch failed".
|
|
74
|
+
if (
|
|
75
|
+
process.env.CI &&
|
|
76
|
+
/\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)[:/]/.test(mcpUrl)
|
|
77
|
+
)
|
|
78
|
+
throw new Error(
|
|
79
|
+
`platform MCP url is ${mcpUrl} — a localhost dev server is not reachable from CI.\n` +
|
|
80
|
+
` Set the RAFA_MCP_URL secret to your deployed platform's /api/mcp URL, or run the\n` +
|
|
81
|
+
` session fallback instead (/rafa distill <branch> in Claude Code).`,
|
|
82
|
+
);
|
|
83
|
+
let res;
|
|
84
|
+
try {
|
|
85
|
+
res = await fetch(mcpUrl, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"content-type": "application/json",
|
|
89
|
+
authorization: `Bearer ${key}`,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
jsonrpc: "2.0",
|
|
93
|
+
id: ++rpcId,
|
|
94
|
+
method: "tools/call",
|
|
95
|
+
params: { name: tool, arguments: { repo: repoId, ...args } },
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`cannot reach the platform MCP at ${mcpUrl} (${e instanceof Error ? e.message : e}) — ` +
|
|
101
|
+
`check the URL (RAFA_MCP_URL / credentials.json / .mcp.json) and that the platform is deployed and reachable from here.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
82
104
|
if (res.status === 401)
|
|
83
|
-
throw new Error(
|
|
105
|
+
throw new Error(
|
|
106
|
+
"agent key rejected (401) — revoked or wrong platform; mint a new one (repo → Agent access)",
|
|
107
|
+
);
|
|
84
108
|
if (res.status === 429)
|
|
85
109
|
throw new Error("rate limited (429) — back off and retry");
|
|
86
110
|
if (!res.ok) throw new Error(`MCP endpoint returned HTTP ${res.status}`);
|
|
87
111
|
const body = await res.json();
|
|
88
|
-
if (body.error)
|
|
112
|
+
if (body.error)
|
|
113
|
+
throw new Error(
|
|
114
|
+
`MCP error: ${body.error.message ?? JSON.stringify(body.error)}`,
|
|
115
|
+
);
|
|
89
116
|
const result = body.result ?? {};
|
|
90
117
|
const payload = result.structuredContent ?? null;
|
|
91
118
|
if (result.isError) {
|
package/lib/okf.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// rafa okf — the OKF surface commands (contract §11). The format machinery is
|
|
2
|
+
// @rafinery/okf; the rafa profile decides where classes live and what stamps
|
|
3
|
+
// are derivable.
|
|
4
|
+
//
|
|
5
|
+
// rafa okf materialize the surface on .rafa (stamps · link
|
|
6
|
+
// transpile · citations sections · index tree).
|
|
7
|
+
// Runs automatically inside `rafa push`.
|
|
8
|
+
// rafa okf check OKF §9 conformance walk of the bundle — errors
|
|
9
|
+
// fail (exit 1), soft guidance warns. Exemptions
|
|
10
|
+
// (active.md) are explicit and surfaced.
|
|
11
|
+
// rafa okf new <class> <id> [--key=value …] [--cite="f:l :: tok" …] [--link=id …]
|
|
12
|
+
// mint a conformant concept skeleton in the class's
|
|
13
|
+
// directory. Nothing is invented: flags you omit are
|
|
14
|
+
// absent, and `rafa compile` fails required ones
|
|
15
|
+
// loudly — that is the validate-and-correct loop.
|
|
16
|
+
|
|
17
|
+
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { runOkfEmit } from "./gate/okf-emit.mjs";
|
|
20
|
+
import { SCHEMA_VERSION } from "./gate/compile.mjs";
|
|
21
|
+
import {
|
|
22
|
+
OKF_VERSION,
|
|
23
|
+
validateBundle,
|
|
24
|
+
newConcept,
|
|
25
|
+
FILE_CLASSES,
|
|
26
|
+
RAFA_EXEMPT,
|
|
27
|
+
} from "@rafinery/okf";
|
|
28
|
+
|
|
29
|
+
export default async function okf(args = []) {
|
|
30
|
+
const [sub, ...rest] = args[0]?.startsWith("--") ? [undefined, ...args] : args;
|
|
31
|
+
const ROOT =
|
|
32
|
+
(args.find((a) => a.startsWith("--root=")) ?? "").slice(7) || ".rafa";
|
|
33
|
+
|
|
34
|
+
if (sub === undefined || sub === "emit") {
|
|
35
|
+
process.exit(runOkfEmit(args.filter((a) => a.startsWith("--"))));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (sub === "check") {
|
|
39
|
+
const { conformant, checked, errors, warnings } = validateBundle(ROOT, {
|
|
40
|
+
exempt: RAFA_EXEMPT,
|
|
41
|
+
});
|
|
42
|
+
for (const e of errors) console.log(`✗ ${e.path} — ${e.rule}`);
|
|
43
|
+
for (const w of warnings) console.log(`⚠ ${w.path} — ${w.rule}`);
|
|
44
|
+
console.log(
|
|
45
|
+
`${conformant ? "✓" : "✗"} rafa okf check: ${checked} files · ` +
|
|
46
|
+
`${errors.length} error(s) · ${warnings.length} warning(s) — ` +
|
|
47
|
+
(conformant ? `bundle conforms to OKF v${OKF_VERSION}` : "bundle is NOT conformant"),
|
|
48
|
+
);
|
|
49
|
+
process.exit(conformant ? 0 : 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (sub === "new") {
|
|
53
|
+
const [cls, id] = rest.filter((a) => !a.startsWith("--"));
|
|
54
|
+
const registry = FILE_CLASSES[cls];
|
|
55
|
+
if (!registry || registry.singleton) {
|
|
56
|
+
console.error(
|
|
57
|
+
`✗ rafa okf new: <class> must be one of ${Object.entries(FILE_CLASSES)
|
|
58
|
+
.filter(([, c]) => !c.singleton)
|
|
59
|
+
.map(([k]) => k)
|
|
60
|
+
.join(" | ")} (singletons are authored at their fixed path)`,
|
|
61
|
+
);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (!id) {
|
|
65
|
+
console.error("✗ rafa okf new: missing <id> (kebab-case, becomes the filename stem)");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const fields = { schemaVersion: SCHEMA_VERSION }; // never a literal — moves with the contract
|
|
69
|
+
const listFields = { cites: [], links: [] };
|
|
70
|
+
for (const a of rest.filter((a) => a.startsWith("--"))) {
|
|
71
|
+
const m = a.match(/^--([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
72
|
+
if (!m) continue;
|
|
73
|
+
const [, key, value] = m;
|
|
74
|
+
if (key === "cite") listFields.cites.push(value);
|
|
75
|
+
else if (key === "link") listFields.links.push(value);
|
|
76
|
+
else if (key !== "root") fields[key] = value;
|
|
77
|
+
}
|
|
78
|
+
// The improvement class's OKF type is constant; notes author their own
|
|
79
|
+
// contract enum via --type (compile owns the enum — no default here).
|
|
80
|
+
const type = fields.type ?? registry.okfType ?? "";
|
|
81
|
+
delete fields.type;
|
|
82
|
+
let content;
|
|
83
|
+
try {
|
|
84
|
+
content = newConcept({ type, id, fields, listFields });
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(`✗ rafa okf new: ${e instanceof Error ? e.message : e}`);
|
|
87
|
+
console.error(
|
|
88
|
+
cls === "improvement"
|
|
89
|
+
? ""
|
|
90
|
+
: " notes require --type=<contract|convention|flow|how-to> (the compile enum)",
|
|
91
|
+
);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const path = join(ROOT, registry.dir, `${id}.md`);
|
|
95
|
+
if (existsSync(path)) {
|
|
96
|
+
console.error(`✗ rafa okf new: ${path} already exists — refusing to overwrite`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
100
|
+
writeFileSync(path, content);
|
|
101
|
+
console.log(
|
|
102
|
+
`✓ rafa okf new: ${path} — now write the body + fill required fields, ` +
|
|
103
|
+
`then \`rafa verify-citations\` + \`rafa compile\` gate it.`,
|
|
104
|
+
);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.error(`✗ rafa okf: unknown subcommand "${sub}" (emit | check | new)`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
package/lib/push.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { runCompile } from "./gate/compile.mjs";
|
|
16
16
|
import { runVerifyCitations } from "./gate/verify-citations.mjs";
|
|
17
|
+
import { runOkfEmit } from "./gate/okf-emit.mjs";
|
|
17
18
|
import { ensureBrainRepo, remoteDefaultBranch, inDir, die } from "./brain-repo.mjs";
|
|
18
19
|
import { CLI_VERSION } from "./releases.mjs";
|
|
19
20
|
|
|
@@ -40,6 +41,38 @@ export default async function push(args = []) {
|
|
|
40
41
|
/* no origin — manifest.repo stays "" */
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// ── stamped contract copy rides the push (before emit so the root index
|
|
45
|
+
// lists it from the very first push) ──
|
|
46
|
+
const canonical = join(ROOT, ".claude", "rafa", "contract.md");
|
|
47
|
+
if (existsSync(canonical)) {
|
|
48
|
+
// The copy is itself a generated concept (contract §11): it self-describes.
|
|
49
|
+
const stampFm =
|
|
50
|
+
`---\n` +
|
|
51
|
+
`type: Reference\n` +
|
|
52
|
+
`title: "rafa brain contract (stamped copy)"\n` +
|
|
53
|
+
`description: "the file protocol this bundle conforms to — canonical: .claude/rafa/contract.md in the code repo"\n` +
|
|
54
|
+
`timestamp: ${new Date().toISOString()}\n` +
|
|
55
|
+
`---\n\n`;
|
|
56
|
+
const stampHeader =
|
|
57
|
+
`<!-- STAMPED COPY — canonical: .claude/rafa/contract.md in the code repo.\n` +
|
|
58
|
+
` cli: ${CLI_VERSION} · brain-for: ${codeSha} · stamped: ${new Date().toISOString()} -->\n\n`;
|
|
59
|
+
writeFileSync(join(rafaDir, "contract.md"), stampFm + stampHeader + readFileSync(canonical, "utf8"));
|
|
60
|
+
} else {
|
|
61
|
+
console.log(
|
|
62
|
+
" ! no .claude/rafa/contract.md in this repo — pushing without the stamped contract copy " +
|
|
63
|
+
"(run `rafa update` to vendor it)",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── OKF surface (in-process, contract §11) ──
|
|
68
|
+
// Materialize the interchange surface BEFORE the gates so everything the
|
|
69
|
+
// emitter writes is checked like any authored file: stamps (type/description/
|
|
70
|
+
// timestamp/tags where derivable), wikilink→markdown-link transpile, generated
|
|
71
|
+
// `# Citations` sections, and the index.md tree. A pushed brain repo is a
|
|
72
|
+
// conformant OKF v0.1 bundle any foreign consumer can read without rafa.
|
|
73
|
+
console.log("• rafa okf — materializing the OKF surface (stamps · links · citations · indexes) …");
|
|
74
|
+
runOkfEmit([`--repo=${repoFull}`]);
|
|
75
|
+
|
|
43
76
|
// ── citation gate (in-process) ──
|
|
44
77
|
// Re-run the checker HERE so the record that rides the push is the record of
|
|
45
78
|
// THIS push — a stale/hand-stamped citation-check.json can never ship
|
|
@@ -70,23 +103,6 @@ export default async function push(args = []) {
|
|
|
70
103
|
"authoring agent (atlas/bloom/prism) correct them, then re-push.",
|
|
71
104
|
);
|
|
72
105
|
|
|
73
|
-
// ── stamped contract copy rides the push ──
|
|
74
|
-
// The canonical contract lives in the CODE repo (.claude/rafa/contract.md);
|
|
75
|
-
// the brain repo carries a stamped copy so any reader of the brain repo knows
|
|
76
|
-
// exactly which contract version this brain conforms to.
|
|
77
|
-
const canonical = join(ROOT, ".claude", "rafa", "contract.md");
|
|
78
|
-
if (existsSync(canonical)) {
|
|
79
|
-
const stampHeader =
|
|
80
|
-
`<!-- STAMPED COPY — canonical: .claude/rafa/contract.md in the code repo.\n` +
|
|
81
|
-
` cli: ${CLI_VERSION} · brain-for: ${codeSha} · stamped: ${new Date().toISOString()} -->\n\n`;
|
|
82
|
-
writeFileSync(join(rafaDir, "contract.md"), stampHeader + readFileSync(canonical, "utf8"));
|
|
83
|
-
} else {
|
|
84
|
-
console.log(
|
|
85
|
-
" ! no .claude/rafa/contract.md in this repo — pushing without the stamped contract copy " +
|
|
86
|
-
"(run `rafa update` to vendor it)",
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
106
|
inRafa("git add -A");
|
|
91
107
|
try {
|
|
92
108
|
inRafa(`git commit -m "brain: scan" -m "brain-for: ${codeSha}"`);
|
package/lib/releases.mjs
CHANGED
|
@@ -168,6 +168,41 @@ export const RELEASES = [
|
|
|
168
168
|
"never ship). Brain data schema unchanged; no re-scan; adopt with `rafa update` " +
|
|
169
169
|
"(re-vendors hooks, merges settings + statusline, installs the pre-push boundary).",
|
|
170
170
|
},
|
|
171
|
+
{
|
|
172
|
+
version: "0.8.0",
|
|
173
|
+
contract: 1,
|
|
174
|
+
plans: 2,
|
|
175
|
+
requires: "update",
|
|
176
|
+
recommends: ["push"],
|
|
177
|
+
summary:
|
|
178
|
+
"THE OKF SURFACE (contract §11): every generated .md self-describes, and a pushed " +
|
|
179
|
+
"brain repo is a conformant Open Knowledge Format v0.1 bundle any foreign agent can " +
|
|
180
|
+
"read without rafa tooling. New @rafinery/okf package (zero-dep: strict parser · " +
|
|
181
|
+
"concept primitives · links · citations · indexes · §9 validator · generator) — the " +
|
|
182
|
+
"gate now imports the parser from it. New `rafa okf` (emit — runs inside push, before " +
|
|
183
|
+
"the gates, so its output is gate-checked) · `rafa okf check` (conformance walk; " +
|
|
184
|
+
"documented exemptions surface as warnings) · `rafa okf new <class> <id>` (mint a " +
|
|
185
|
+
"conformant concept skeleton; the rafa-okf skill is the SOP). Emit stamps ONLY " +
|
|
186
|
+
"derivable values (description from summary · singleton type/title from the class " +
|
|
187
|
+
"registry · timestamp from authored created:/found: → brain-repo git history → the " +
|
|
188
|
+
"emit run; never a guess), renders `# Citations` (sha-pinned GitHub URLs when the " +
|
|
189
|
+
"repo is known) inside okf:citations markers, transpiles resolvable [[wikilinks]] to " +
|
|
190
|
+
"bundle-relative markdown links (the authored form going forward — SOPs updated), and " +
|
|
191
|
+
"generates the index.md tree (root index carries okf_version + repo + codeSha). " +
|
|
192
|
+
"Plan items carry the surface too (type: Plan Epic|Task|Subtask derived from kind; " +
|
|
193
|
+
"per-epic plans/index.md). " +
|
|
194
|
+
"Checker: non-failing LINKS warn lane (dangling link = not-yet-written knowledge, " +
|
|
195
|
+
"OKF §5.3 — prism's worklist; closes the 2026-06-19 [[…]]-lint ratchet) and the " +
|
|
196
|
+
"citation-check.md report now self-describes. index.md/log.md are reserved names " +
|
|
197
|
+
"(skipped by every walker). Agent cards: okf-surface duty on atlas · bloom · prism · " +
|
|
198
|
+
"sage; okf-awareness on compass (bundles are PORTABLE — person-scoped content never " +
|
|
199
|
+
"lands in one); the conductor verb map carries the protocol note. TOTALITY: "
|
|
200
|
+
+ "*.theirs.md conflicts fail compile with a typed rule (walkers skip them); "
|
|
201
|
+
+ "shipped SOPs (rafa-* skills + conductor) and sage learnings are compile-gated "
|
|
202
|
+
+ "local classes beside the agent cards. WIRE UNCHANGED: " +
|
|
203
|
+
"manifest shape + schemaVersion 1 + checker v2 — all additive " +
|
|
204
|
+
"(§8); no re-scan, adopt with `rafa update`, then `rafa push` materializes the bundle.",
|
|
205
|
+
},
|
|
171
206
|
];
|
|
172
207
|
|
|
173
208
|
// The release this CLI build ships (last entry).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rafinery/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "rafa — the AI engineer CLI. Vendors the rafa blueprint into your repo (shadcn-style) and runs the deterministic contract gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,13 +15,12 @@
|
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=18"
|
|
17
17
|
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@rafinery/okf": "0.1.0"
|
|
20
|
+
},
|
|
18
21
|
"publishConfig": {
|
|
19
22
|
"access": "public"
|
|
20
23
|
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"bundle": "node scripts/bundle-blueprint.mjs",
|
|
23
|
-
"prepare": "node scripts/bundle-blueprint.mjs"
|
|
24
|
-
},
|
|
25
24
|
"keywords": [
|
|
26
25
|
"rafa",
|
|
27
26
|
"rafinery",
|
|
@@ -37,5 +36,9 @@
|
|
|
37
36
|
"url": "git+https://github.com/rafinery-ai/rafinery.git",
|
|
38
37
|
"directory": "packages/cli"
|
|
39
38
|
},
|
|
40
|
-
"homepage": "https://github.com/rafinery-ai/rafinery/tree/main/packages/cli"
|
|
41
|
-
|
|
39
|
+
"homepage": "https://github.com/rafinery-ai/rafinery/tree/main/packages/cli",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"bundle": "node scripts/bundle-blueprint.mjs",
|
|
42
|
+
"test": "node --test"
|
|
43
|
+
}
|
|
44
|
+
}
|