@rafinery/cli 0.8.0 → 0.8.2
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 +20 -0
- package/bin/rafa-distiller.mjs +13 -0
- package/bin/rafa.mjs +4 -0
- package/blueprint/.claude/agents/atlas.md +5 -1
- package/blueprint/.claude/agents/bloom.md +5 -1
- package/blueprint/.claude/agents/compass.md +5 -1
- package/blueprint/.claude/agents/prism.md +5 -1
- package/blueprint/.claude/agents/sage.md +5 -1
- package/blueprint/.claude/commands/rafa.md +19 -1
- package/blueprint/.claude/rafa/contract.md +31 -3
- package/blueprint/.claude/rafa/hooks/brain-commit.mjs +80 -0
- package/blueprint/.claude/rafa/hooks/brain-map.mjs +64 -0
- package/blueprint/.claude/rafa/hooks/brain-switch.mjs +48 -0
- package/blueprint/.claude/rafa/hooks/post-checkout +10 -0
- package/blueprint/.claude/rafa/hooks/post-commit +11 -0
- package/blueprint/.claude/rafa/hooks/post-rewrite +10 -0
- package/blueprint/.claude/rafa/hooks/pre-push +22 -0
- package/blueprint/.claude/rafa/hooks/session-start.mjs +93 -0
- package/blueprint/.claude/skills/rafa-improve/SKILL.md +5 -0
- package/blueprint/.claude/skills/rafa-validate/SKILL.md +6 -0
- package/lib/checkpoint.mjs +42 -0
- package/lib/ci-setup.mjs +48 -3
- package/lib/dirty.mjs +37 -0
- package/lib/distill.mjs +313 -37
- package/lib/distiller/doctrine.mjs +379 -0
- package/lib/distiller/state-plane.mjs +159 -0
- package/lib/distiller.mjs +410 -0
- package/lib/doctor.mjs +110 -0
- package/lib/gate/compile.mjs +31 -0
- package/lib/gate/okf-emit.mjs +12 -0
- package/lib/gate/verify-citations.mjs +9 -2
- package/lib/githook.mjs +75 -33
- package/lib/init.mjs +14 -2
- package/lib/mcp-client.mjs +22 -5
- package/lib/okf.mjs +11 -1
- package/lib/pull.mjs +2 -2
- package/lib/push.mjs +67 -2
- package/lib/releases.mjs +26 -0
- package/lib/sensor-health.mjs +91 -0
- package/lib/status.mjs +47 -1
- package/lib/update.mjs +2 -2
- package/lib/working-set.mjs +19 -1
- package/package.json +11 -9
- package/LICENSE +0 -21
package/lib/githook.mjs
CHANGED
|
@@ -1,54 +1,96 @@
|
|
|
1
|
-
// The git
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The git hook installer — M5 sensor #3 (pre-push checkpoint boundary) plus
|
|
2
|
+
// the capture-engine P1 transport hooks (post-commit 1-1 brain commits ·
|
|
3
|
+
// post-checkout lockstep · post-rewrite rebase maps). "Brain git work is done
|
|
4
|
+
// automatically; the dev never maintains the brain" — these installs are that
|
|
5
|
+
// principle made mechanical.
|
|
4
6
|
//
|
|
5
7
|
// .git/hooks is per-clone (git never versions it), so this runs at init AND
|
|
6
|
-
// pull (the teammate path) AND update — every working copy gets the
|
|
7
|
-
// Never clobbers a foreign hook: ours is recognized by its marker line;
|
|
8
|
-
// else is respected and the chain-line printed for the dev to add
|
|
8
|
+
// pull (the teammate path) AND update — every working copy gets the sensors.
|
|
9
|
+
// Never clobbers a foreign hook: ours is recognized by its marker line;
|
|
10
|
+
// anything else is respected and the chain-line printed for the dev to add
|
|
11
|
+
// themselves.
|
|
9
12
|
|
|
10
13
|
import { chmodSync, copyFileSync, existsSync, readFileSync } from "node:fs";
|
|
11
14
|
import { execSync } from "node:child_process";
|
|
12
15
|
import { join } from "node:path";
|
|
13
16
|
|
|
14
|
-
const
|
|
15
|
-
|
|
17
|
+
const HOOKS = [
|
|
18
|
+
{
|
|
19
|
+
name: "pre-push",
|
|
20
|
+
marker: "rafa pre-push hook",
|
|
21
|
+
chain: "npx -y @rafinery/cli checkpoint || true",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "post-commit",
|
|
25
|
+
marker: "rafa post-commit hook",
|
|
26
|
+
chain: 'node ".claude/rafa/hooks/brain-commit.mjs" || true',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "post-checkout",
|
|
30
|
+
marker: "rafa post-checkout hook",
|
|
31
|
+
chain: 'node ".claude/rafa/hooks/brain-switch.mjs" "$1" "$2" "$3" || true',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "post-rewrite",
|
|
35
|
+
marker: "rafa post-rewrite hook",
|
|
36
|
+
chain: 'node ".claude/rafa/hooks/brain-map.mjs" "$1" || true',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
16
39
|
|
|
17
|
-
|
|
18
|
-
const src = join(targetDir, ".claude", "rafa", "hooks",
|
|
19
|
-
if (!existsSync(src))
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
// --git-path resolves worktrees/submodules correctly; never assume .git is a dir.
|
|
24
|
-
hooksDir = execSync("git rev-parse --git-path hooks", { cwd: targetDir, encoding: "utf8" }).trim();
|
|
25
|
-
hooksDir = join(targetDir, hooksDir);
|
|
26
|
-
} catch {
|
|
27
|
-
return { skipped: "not a git repo" };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const dst = join(hooksDir, "pre-push");
|
|
40
|
+
function installOne(targetDir, hooksDir, { name, marker, chain }) {
|
|
41
|
+
const src = join(targetDir, ".claude", "rafa", "hooks", name);
|
|
42
|
+
if (!existsSync(src))
|
|
43
|
+
return { hook: name, skipped: "no vendored hook template (run `rafa update`)" };
|
|
44
|
+
const dst = join(hooksDir, name);
|
|
31
45
|
if (existsSync(dst)) {
|
|
32
46
|
const body = readFileSync(dst, "utf8");
|
|
33
|
-
if (!body.includes(
|
|
47
|
+
if (!body.includes(marker))
|
|
34
48
|
return {
|
|
49
|
+
hook: name,
|
|
35
50
|
skipped:
|
|
36
|
-
|
|
37
|
-
`
|
|
51
|
+
`a ${name} hook already exists (not rafa's) — to chain ours, add this ` +
|
|
52
|
+
`line to it yourself: ${chain}`,
|
|
38
53
|
};
|
|
39
|
-
|
|
40
|
-
// ours → refresh in place (idempotent update)
|
|
41
|
-
copyFileSync(src, dst);
|
|
54
|
+
copyFileSync(src, dst); // ours → refresh in place (idempotent update)
|
|
42
55
|
chmodSync(dst, 0o755);
|
|
43
|
-
return { updated: true };
|
|
56
|
+
return { hook: name, updated: true };
|
|
44
57
|
}
|
|
45
58
|
copyFileSync(src, dst);
|
|
46
59
|
chmodSync(dst, 0o755);
|
|
47
|
-
return { installed: true };
|
|
60
|
+
return { hook: name, installed: true };
|
|
48
61
|
}
|
|
49
62
|
|
|
63
|
+
export function installGitHooks(targetDir) {
|
|
64
|
+
let hooksDir;
|
|
65
|
+
try {
|
|
66
|
+
// --git-path resolves worktrees/submodules correctly; never assume .git is a dir.
|
|
67
|
+
hooksDir = execSync("git rev-parse --git-path hooks", {
|
|
68
|
+
cwd: targetDir,
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
}).trim();
|
|
71
|
+
hooksDir = join(targetDir, hooksDir);
|
|
72
|
+
} catch {
|
|
73
|
+
return HOOKS.map((h) => ({ hook: h.name, skipped: "not a git repo" }));
|
|
74
|
+
}
|
|
75
|
+
return HOOKS.map((h) => installOne(targetDir, hooksDir, h));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function reportGitHooks(results) {
|
|
79
|
+
const fresh = results.filter((r) => r.installed).map((r) => r.hook);
|
|
80
|
+
const refreshed = results.filter((r) => r.updated).map((r) => r.hook);
|
|
81
|
+
if (fresh.length)
|
|
82
|
+
console.log(
|
|
83
|
+
` ✓ git hooks installed: ${fresh.join(" · ")} — checkpoint at push, 1-1 brain commits, branch lockstep, rebase maps (all non-blocking)`,
|
|
84
|
+
);
|
|
85
|
+
if (refreshed.length) console.log(` ✓ git hooks refreshed: ${refreshed.join(" · ")}`);
|
|
86
|
+
for (const r of results.filter((r) => r.skipped))
|
|
87
|
+
console.log(` ! git ${r.hook} hook not installed — ${r.skipped}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Back-compat for older callers: pre-push only.
|
|
91
|
+
export function installPrePush(targetDir) {
|
|
92
|
+
return installGitHooks(targetDir).find((r) => r.hook === "pre-push");
|
|
93
|
+
}
|
|
50
94
|
export function reportGitHook(r) {
|
|
51
|
-
|
|
52
|
-
else if (r.updated) console.log(" ✓ git pre-push hook refreshed");
|
|
53
|
-
else if (r.skipped) console.log(` ! git pre-push hook not installed — ${r.skipped}`);
|
|
95
|
+
reportGitHooks([r]);
|
|
54
96
|
}
|
package/lib/init.mjs
CHANGED
|
@@ -120,8 +120,8 @@ export default async function init(args) {
|
|
|
120
120
|
}
|
|
121
121
|
// The pre-push checkpoint boundary is per-clone (.git/hooks is never committed).
|
|
122
122
|
{
|
|
123
|
-
const {
|
|
124
|
-
|
|
123
|
+
const { installGitHooks, reportGitHooks } = await import("./githook.mjs");
|
|
124
|
+
reportGitHooks(installGitHooks(TARGET));
|
|
125
125
|
}
|
|
126
126
|
// Wire the repo's own MCP servers into the (owned) agent cards so subagents
|
|
127
127
|
// can actually reach them (zero-command orchestration, 2026-07-12).
|
|
@@ -229,5 +229,17 @@ export default async function init(args) {
|
|
|
229
229
|
: ""),
|
|
230
230
|
);
|
|
231
231
|
|
|
232
|
+
// P0: prove the machinery init just wired — sensors, scripts, and the
|
|
233
|
+
// heartbeat round-trip — instead of hoping. Platform-wired inits run the
|
|
234
|
+
// proof now (advisory: init already succeeded; doctor names any fix);
|
|
235
|
+
// blueprint-only inits get the pointer for after the platform is wired.
|
|
236
|
+
if (p?.mcpUrl) {
|
|
237
|
+
console.log("");
|
|
238
|
+
const { runDoctor } = await import("./doctor.mjs");
|
|
239
|
+
await runDoctor(TARGET);
|
|
240
|
+
} else {
|
|
241
|
+
console.log("\n · verify the capture machinery anytime: npx @rafinery/cli doctor");
|
|
242
|
+
}
|
|
243
|
+
|
|
232
244
|
await notifyIfBehind();
|
|
233
245
|
}
|
package/lib/mcp-client.mjs
CHANGED
|
@@ -41,7 +41,11 @@ export function resolveMcp(cwd) {
|
|
|
41
41
|
const creds = readJson(credentialsPath());
|
|
42
42
|
const repoCreds = creds?.repos?.[repoId] ?? null;
|
|
43
43
|
|
|
44
|
+
// Track WHERE each value came from — auth failures name their sources so a
|
|
45
|
+
// wrong-platform mismatch (env override vs credentials vs .mcp.json) is
|
|
46
|
+
// self-diagnosing instead of a scavenger hunt (owner live-catch 2026-07-17).
|
|
44
47
|
const key = process.env.RAFA_MCP_KEY || repoCreds?.key || null;
|
|
48
|
+
const keySource = process.env.RAFA_MCP_KEY ? "env RAFA_MCP_KEY" : "credentials.json";
|
|
45
49
|
if (!key)
|
|
46
50
|
throw new Error(
|
|
47
51
|
`no agent key — set RAFA_MCP_KEY, or mint one on the platform (repo → Agent access) ` +
|
|
@@ -49,17 +53,21 @@ export function resolveMcp(cwd) {
|
|
|
49
53
|
);
|
|
50
54
|
|
|
51
55
|
let mcpUrl = process.env.RAFA_MCP_URL || repoCreds?.mcpUrl || null;
|
|
56
|
+
let urlSource = process.env.RAFA_MCP_URL ? "env RAFA_MCP_URL" : "credentials.json";
|
|
52
57
|
if (!mcpUrl) {
|
|
53
58
|
const mcpJson = readJson(join(cwd, ".mcp.json"));
|
|
54
59
|
const url = mcpJson?.mcpServers?.rafinery?.url;
|
|
55
|
-
if (typeof url === "string" && url !== "")
|
|
60
|
+
if (typeof url === "string" && url !== "") {
|
|
61
|
+
mcpUrl = url;
|
|
62
|
+
urlSource = ".mcp.json";
|
|
63
|
+
}
|
|
56
64
|
}
|
|
57
65
|
if (!mcpUrl)
|
|
58
66
|
throw new Error(
|
|
59
67
|
"no MCP endpoint — set RAFA_MCP_URL, or ensure .mcp.json carries the `rafinery` server entry",
|
|
60
68
|
);
|
|
61
69
|
|
|
62
|
-
return { repoId, key, mcpUrl };
|
|
70
|
+
return { repoId, key, mcpUrl, keySource, urlSource };
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
let rpcId = 0;
|
|
@@ -67,7 +75,7 @@ let rpcId = 0;
|
|
|
67
75
|
// Call one tool; returns the structured payload. Throws with the server's
|
|
68
76
|
// message on transport or tool errors.
|
|
69
77
|
export async function callTool(cwd, tool, args = {}) {
|
|
70
|
-
const { repoId, key, mcpUrl } = resolveMcp(cwd);
|
|
78
|
+
const { repoId, key, mcpUrl, keySource, urlSource } = resolveMcp(cwd);
|
|
71
79
|
// A localhost platform is only reachable on the machine running the dev
|
|
72
80
|
// server — on a CI runner it is a guaranteed dead end; say so up front
|
|
73
81
|
// instead of a bare "fetch failed".
|
|
@@ -101,10 +109,19 @@ export async function callTool(cwd, tool, args = {}) {
|
|
|
101
109
|
`check the URL (RAFA_MCP_URL / credentials.json / .mcp.json) and that the platform is deployed and reachable from here.`,
|
|
102
110
|
);
|
|
103
111
|
}
|
|
104
|
-
if (res.status === 401)
|
|
112
|
+
if (res.status === 401) {
|
|
113
|
+
let host = mcpUrl;
|
|
114
|
+
try {
|
|
115
|
+
host = new URL(mcpUrl).host;
|
|
116
|
+
} catch {
|
|
117
|
+
/* keep the raw url */
|
|
118
|
+
}
|
|
105
119
|
throw new Error(
|
|
106
|
-
|
|
120
|
+
`agent key rejected (401) by ${host} — the key (from ${keySource}) is not valid on that platform ` +
|
|
121
|
+
`(url from ${urlSource}): revoked, or minted on a DIFFERENT deployment. Keys are per-platform — ` +
|
|
122
|
+
`mint one on THIS platform (repo → Agent access), or point the url back at the platform that minted yours.`,
|
|
107
123
|
);
|
|
124
|
+
}
|
|
108
125
|
if (res.status === 429)
|
|
109
126
|
throw new Error("rate limited (429) — back off and retry");
|
|
110
127
|
if (!res.ok) throw new Error(`MCP endpoint returned HTTP ${res.status}`);
|
package/lib/okf.mjs
CHANGED
|
@@ -36,9 +36,19 @@ export default async function okf(args = []) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (sub === "check") {
|
|
39
|
-
const { conformant, checked, errors, warnings } = validateBundle(ROOT, {
|
|
39
|
+
const { conformant, empty, checked, errors, warnings } = validateBundle(ROOT, {
|
|
40
40
|
exempt: RAFA_EXEMPT,
|
|
41
41
|
});
|
|
42
|
+
// Zero files is ABSENCE, never conformance — a green over an empty root
|
|
43
|
+
// would be the silent-empty failure class (no assumed values, outward).
|
|
44
|
+
if (empty) {
|
|
45
|
+
console.error(
|
|
46
|
+
`✗ rafa okf check: no .md files under ${ROOT} — nothing to check.\n` +
|
|
47
|
+
` Wrong --root? In a brain-repo clone the bundle IS the directory: --root=.\n` +
|
|
48
|
+
` In a code repo the bundle is .rafa/ (lazy — \`rafa pull --full\` mirrors it).`,
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
42
52
|
for (const e of errors) console.log(`✗ ${e.path} — ${e.rule}`);
|
|
43
53
|
for (const w of warnings) console.log(`⚠ ${w.path} — ${w.rule}`);
|
|
44
54
|
console.log(
|
package/lib/pull.mjs
CHANGED
|
@@ -35,8 +35,8 @@ export default async function pull(args = []) {
|
|
|
35
35
|
// Every working copy gets the M5 checkpoint boundary (.git/hooks is per-clone —
|
|
36
36
|
// pull IS the teammate path, so this is where a fresh clone gains the sensor).
|
|
37
37
|
{
|
|
38
|
-
const {
|
|
39
|
-
|
|
38
|
+
const { installGitHooks, reportGitHooks } = await import("./githook.mjs");
|
|
39
|
+
reportGitHooks(installGitHooks(ROOT));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Reachability + default branch — loud if the remote can't be reached.
|
package/lib/push.mjs
CHANGED
|
@@ -23,6 +23,33 @@ export default async function push(args = []) {
|
|
|
23
23
|
const { rafaDir } = ensureBrainRepo(ROOT);
|
|
24
24
|
const inRafa = inDir(rafaDir);
|
|
25
25
|
|
|
26
|
+
// Mirrored-branch era (capture-engine P1): push is a MAIN-branch act and the
|
|
27
|
+
// brain DEFAULT branch is its only target. If the working copy sits on a
|
|
28
|
+
// mirrored session branch (post-checkout lockstep), carry dirty surfaces
|
|
29
|
+
// there and switch first — a session branch's candidates must never ride an
|
|
30
|
+
// org-brain push.
|
|
31
|
+
const defaultBranch = remoteDefaultBranch(rafaDir);
|
|
32
|
+
try {
|
|
33
|
+
const cur = inRafa("git rev-parse --abbrev-ref HEAD");
|
|
34
|
+
if (cur !== defaultBranch) {
|
|
35
|
+
try {
|
|
36
|
+
inRafa("git add -A");
|
|
37
|
+
inRafa(
|
|
38
|
+
`git commit -q -m "brain(switch-carryover)" -m "switch-carryover-from: ${cur}"`,
|
|
39
|
+
);
|
|
40
|
+
} catch {
|
|
41
|
+
/* nothing dirty */
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
inRafa(`git checkout -q ${defaultBranch}`);
|
|
45
|
+
} catch {
|
|
46
|
+
inRafa(`git checkout -q -b ${defaultBranch}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
/* unborn HEAD (fresh brain repo) — the branch -M below names it */
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
// Anchor the brain to the current code HEAD.
|
|
27
54
|
let codeSha = "unknown";
|
|
28
55
|
try {
|
|
@@ -104,12 +131,50 @@ export default async function push(args = []) {
|
|
|
104
131
|
);
|
|
105
132
|
|
|
106
133
|
inRafa("git add -A");
|
|
134
|
+
// Descriptive commit (owner catch 2026-07-17: the history was a wall of
|
|
135
|
+
// identical "brain: scan"). Everything needed is already in scope — the
|
|
136
|
+
// manifest compile just wrote, the staged diff, the verb from the caller
|
|
137
|
+
// (--verb=scan|improve|build|distill; "update" when unstated). The
|
|
138
|
+
// "brain-for:" trailer paragraph is unchanged — provenance consumers key
|
|
139
|
+
// on it.
|
|
140
|
+
const verbArg = args.find((a) => a.startsWith("--verb="));
|
|
141
|
+
const verb = verbArg ? verbArg.slice("--verb=".length) : "update";
|
|
142
|
+
const subjectParts = [];
|
|
143
|
+
try {
|
|
144
|
+
const m = JSON.parse(readFileSync(join(rafaDir, "manifest.json"), "utf8"));
|
|
145
|
+
if (Array.isArray(m.notes)) subjectParts.push(`${m.notes.length} notes`);
|
|
146
|
+
if (Array.isArray(m.improvements)) {
|
|
147
|
+
const open = m.improvements.filter((i) => i.status === "open").length;
|
|
148
|
+
subjectParts.push(`${m.improvements.length} improvements (${open} open)`);
|
|
149
|
+
}
|
|
150
|
+
const h = m.health ?? {};
|
|
151
|
+
if (h.verdict) subjectParts.push(`health ${h.verdict}${typeof h.score === "number" ? ` ${h.score}` : ""}`);
|
|
152
|
+
} catch {
|
|
153
|
+
/* manifest unreadable — the verb + trailer still beat "brain: scan" */
|
|
154
|
+
}
|
|
155
|
+
let stagedStat = "";
|
|
107
156
|
try {
|
|
108
|
-
|
|
157
|
+
const lines = inRafa("git diff --cached --name-status")
|
|
158
|
+
.split("\n")
|
|
159
|
+
.filter(Boolean);
|
|
160
|
+
if (lines.length) {
|
|
161
|
+
const n = (p) => lines.filter((l) => l.startsWith(p)).length;
|
|
162
|
+
stagedStat = `files +${n("A")} ~${n("M")} -${n("D")}`;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
/* stat is garnish */
|
|
166
|
+
}
|
|
167
|
+
const subject = `brain(${verb}): ${
|
|
168
|
+
subjectParts.length ? subjectParts.join(" · ") : "state sync"
|
|
169
|
+
} · brain-for: ${codeSha}`.replace(/"/g, "'");
|
|
170
|
+
try {
|
|
171
|
+
inRafa(
|
|
172
|
+
`git commit -m "${subject}" -m "brain-for: ${codeSha}${stagedStat ? ` · ${stagedStat}` : ""} · cli: ${CLI_VERSION}"`,
|
|
173
|
+
);
|
|
109
174
|
} catch {
|
|
110
175
|
console.log("• nothing new to commit — pushing current state");
|
|
111
176
|
}
|
|
112
|
-
const branch =
|
|
177
|
+
const branch = defaultBranch;
|
|
113
178
|
inRafa(`git branch -M ${branch}`);
|
|
114
179
|
try {
|
|
115
180
|
inRafa(`git push -u origin ${branch}`);
|
package/lib/releases.mjs
CHANGED
|
@@ -203,6 +203,32 @@ export const RELEASES = [
|
|
|
203
203
|
"manifest shape + schemaVersion 1 + checker v2 — all additive " +
|
|
204
204
|
"(§8); no re-scan, adopt with `rafa update`, then `rafa push` materializes the bundle.",
|
|
205
205
|
},
|
|
206
|
+
{
|
|
207
|
+
version: "0.8.1",
|
|
208
|
+
contract: 1,
|
|
209
|
+
plans: 2,
|
|
210
|
+
requires: "update",
|
|
211
|
+
summary:
|
|
212
|
+
"Honest-empty patch: `rafa okf check` on a root with zero .md files now FAILS " +
|
|
213
|
+
"loudly (\"nothing to check\" + root guidance) instead of printing a vacuous " +
|
|
214
|
+
"conformance green — absence is never conformance. `rafa okf` (emit) on an empty " +
|
|
215
|
+
"root materializes nothing and returns 1 (push logs + continues past a lazy " +
|
|
216
|
+
".rafa; standalone exits loud). @rafinery/okf 0.1.1: validateBundle gains the " +
|
|
217
|
+
"additive `empty` flag so library consumers can make the same distinction.",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
version: "0.8.2",
|
|
221
|
+
contract: 1,
|
|
222
|
+
plans: 2,
|
|
223
|
+
requires: "update",
|
|
224
|
+
summary:
|
|
225
|
+
"MCP scope hygiene, fleet-wide. Server: `repo` is now OPTIONAL on every tool — " +
|
|
226
|
+
"derived from the per-repo key (an authenticated fact); explicit mismatch still " +
|
|
227
|
+
"fails loudly with guidance. Cards: every agent (atlas/bloom/prism/sage/compass/" +
|
|
228
|
+
"distiller) + the conductor now carry the calling convention — omit repo; where a " +
|
|
229
|
+
"value is needed it is rafa.json → repoId, never a folder-name guess. `rafa " +
|
|
230
|
+
"update` re-vendors the cards.",
|
|
231
|
+
},
|
|
206
232
|
];
|
|
207
233
|
|
|
208
234
|
// The release this CLI build ships (last entry).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Sensor-health inventory (capture-engine P0) — the same wired∧exists truth
|
|
2
|
+
// `rafa status` prints, shaped for the platform heartbeat. Hooks fail SILENT
|
|
3
|
+
// by design (never block a session); the heartbeat makes the ABSENCE visible
|
|
4
|
+
// to the platform instead of only to a dev who thinks to ask.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const queueLife = (file) => {
|
|
10
|
+
try {
|
|
11
|
+
const rows = readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
12
|
+
const last = rows.length ? JSON.parse(rows[rows.length - 1]) : null;
|
|
13
|
+
const t = last?.t ?? last?.at ?? null;
|
|
14
|
+
return { entries: rows.length, ...(typeof t === "number" ? { lastFireAt: t } : {}) };
|
|
15
|
+
} catch {
|
|
16
|
+
return { entries: 0 };
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function collectSensorHealth(root) {
|
|
21
|
+
const rafaDir = join(root, ".rafa");
|
|
22
|
+
let settings = null;
|
|
23
|
+
try {
|
|
24
|
+
settings = JSON.parse(
|
|
25
|
+
readFileSync(join(root, ".claude", "settings.json"), "utf8"),
|
|
26
|
+
);
|
|
27
|
+
} catch {
|
|
28
|
+
/* unwired repo — every hook reports wired: false below */
|
|
29
|
+
}
|
|
30
|
+
const wired = (event, script) => {
|
|
31
|
+
const entries = settings?.hooks?.[event] ?? [];
|
|
32
|
+
return (
|
|
33
|
+
JSON.stringify(entries).includes(`rafa/hooks/${script}`) &&
|
|
34
|
+
existsSync(join(root, ".claude", "rafa", "hooks", script))
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
// The git hooks are per-clone (installed by init/pull/update), so their
|
|
38
|
+
// wiring is checked in .git/hooks, not settings.json.
|
|
39
|
+
const gitHookWired = (name) => {
|
|
40
|
+
try {
|
|
41
|
+
return readFileSync(join(root, ".git", "hooks", name), "utf8").includes("rafa");
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
// Swallowed failures the wrappers recorded (non-blocking by design, but the
|
|
47
|
+
// heartbeat still carries them — the platform sees what the terminal ate).
|
|
48
|
+
const lastErrorFor = (() => {
|
|
49
|
+
const byHook = new Map();
|
|
50
|
+
try {
|
|
51
|
+
for (const line of readFileSync(join(rafaDir, "sensor-errors.jsonl"), "utf8")
|
|
52
|
+
.split("\n")
|
|
53
|
+
.filter(Boolean)) {
|
|
54
|
+
const e = JSON.parse(line);
|
|
55
|
+
if (e?.hook) byHook.set(e.hook, e);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
/* no recorded errors */
|
|
59
|
+
}
|
|
60
|
+
return (name) => {
|
|
61
|
+
const e = byHook.get(name);
|
|
62
|
+
return e
|
|
63
|
+
? {
|
|
64
|
+
...(typeof e.error === "string" ? { lastError: e.error } : {}),
|
|
65
|
+
...(typeof e.t === "number" ? { lastErrorAt: e.t } : {}),
|
|
66
|
+
}
|
|
67
|
+
: {};
|
|
68
|
+
};
|
|
69
|
+
})();
|
|
70
|
+
return {
|
|
71
|
+
reportedAt: Date.now(),
|
|
72
|
+
hooks: [
|
|
73
|
+
{ name: "session-start", wired: wired("SessionStart", "session-start.mjs") },
|
|
74
|
+
{
|
|
75
|
+
name: "post-tool",
|
|
76
|
+
wired: wired("PostToolUse", "post-tool.mjs"),
|
|
77
|
+
...queueLife(join(rafaDir, "dirty.jsonl")),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "user-prompt-submit",
|
|
81
|
+
wired: wired("UserPromptSubmit", "user-prompt-submit.mjs"),
|
|
82
|
+
...queueLife(join(rafaDir, "reflex.jsonl")),
|
|
83
|
+
},
|
|
84
|
+
{ name: "pre-push", wired: gitHookWired("pre-push"), ...lastErrorFor("pre-push") },
|
|
85
|
+
// Capture-engine P1 transport hooks — the brain's 1-1 mirror machinery.
|
|
86
|
+
{ name: "post-commit", wired: gitHookWired("post-commit"), ...lastErrorFor("post-commit") },
|
|
87
|
+
{ name: "post-checkout", wired: gitHookWired("post-checkout"), ...lastErrorFor("post-checkout") },
|
|
88
|
+
{ name: "post-rewrite", wired: gitHookWired("post-rewrite"), ...lastErrorFor("post-rewrite") },
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
package/lib/status.mjs
CHANGED
|
@@ -105,7 +105,40 @@ export function computeStatus(root = process.cwd()) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
// Sensor health — hooks fail SILENT by design (never block a session), which
|
|
109
|
+
// means a broken sensor is invisible until someone asks. This makes asking
|
|
110
|
+
// one command (live-catch 2026-07-17: "it proves our hooks are not listening"
|
|
111
|
+
// was unfalsifiable without it). wired = settings.json carries the hook AND
|
|
112
|
+
// its script exists; lastWrite = the queue's own evidence of life.
|
|
113
|
+
const sensors = (() => {
|
|
114
|
+
const settings = (() => {
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(readFileSync(join(root, ".claude", "settings.json"), "utf8"));
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
const hookWired = (event, script) => {
|
|
122
|
+
const entries = settings?.hooks?.[event] ?? [];
|
|
123
|
+
const inSettings = JSON.stringify(entries).includes(`rafa/hooks/${script}`);
|
|
124
|
+
return {
|
|
125
|
+
wired: inSettings && existsSync(join(root, ".claude", "rafa", "hooks", script)),
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
const lastLine = (file) => {
|
|
129
|
+
const rows = readJsonl(join(rafaDir, file));
|
|
130
|
+
const t = rows.length ? rows[rows.length - 1].t ?? rows[rows.length - 1].at ?? null : null;
|
|
131
|
+
return { entries: rows.length, lastWrite: t ?? null };
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
settingsFound: settings !== null,
|
|
135
|
+
sessionStart: hookWired("SessionStart", "session-start.mjs"),
|
|
136
|
+
postTool: { ...hookWired("PostToolUse", "post-tool.mjs"), ...lastLine("dirty.jsonl") },
|
|
137
|
+
promptSubmit: { ...hookWired("UserPromptSubmit", "user-prompt-submit.mjs"), ...lastLine("reflex.jsonl") },
|
|
138
|
+
};
|
|
139
|
+
})();
|
|
140
|
+
|
|
141
|
+
return { provisioned, plan, dirty, reflex, conflicts, sensors };
|
|
109
142
|
}
|
|
110
143
|
|
|
111
144
|
// The ambient one-liner — identical semantics wherever it renders.
|
|
@@ -149,4 +182,17 @@ export default async function status(args = []) {
|
|
|
149
182
|
if (s.conflicts) console.log(` conflicts: ${s.conflicts} .theirs.md awaiting your decision`);
|
|
150
183
|
if (!s.plan && !s.dirty && !s.reflex && !s.conflicts)
|
|
151
184
|
console.log(" nothing pending — the loop is closed.");
|
|
185
|
+
// Sensors: broken hooks are silent by design — say their state out loud here.
|
|
186
|
+
const sen = s.sensors;
|
|
187
|
+
const mark = (h) => (h.wired ? "✓" : "✗ NOT WIRED");
|
|
188
|
+
console.log(
|
|
189
|
+
` sensors: digest ${mark(sen.sessionStart)} · dirty-marker ${mark(sen.postTool)}` +
|
|
190
|
+
(sen.postTool.entries ? ` (${sen.postTool.entries} entries, last ${sen.postTool.lastWrite ?? "?"})` : " (queue empty)") +
|
|
191
|
+
` · reflex ${mark(sen.promptSubmit)}` +
|
|
192
|
+
(sen.promptSubmit.entries ? ` (${sen.promptSubmit.entries})` : ""),
|
|
193
|
+
);
|
|
194
|
+
if (!sen.settingsFound || !sen.sessionStart.wired || !sen.postTool.wired || !sen.promptSubmit.wired)
|
|
195
|
+
console.log(
|
|
196
|
+
" ⚠ sensor(s) not listening — edits/corrections are NOT being captured. `rafa update` re-merges the hooks into .claude/settings.json.",
|
|
197
|
+
);
|
|
152
198
|
}
|
package/lib/update.mjs
CHANGED
|
@@ -48,8 +48,8 @@ export default async function update(args = []) {
|
|
|
48
48
|
const sl = mergeStatusLine(TARGET);
|
|
49
49
|
if (sl.skipped) console.log(` ! statusline untouched — ${sl.skipped}`);
|
|
50
50
|
else if (sl.added?.length) console.log(" ✓ loop-state statusline → .claude/settings.json");
|
|
51
|
-
const {
|
|
52
|
-
|
|
51
|
+
const { installGitHooks, reportGitHooks } = await import("./githook.mjs");
|
|
52
|
+
reportGitHooks(installGitHooks(TARGET));
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// ── Blueprint-side migrations: mechanical/deterministic, run here in the CLI. ──
|
package/lib/working-set.mjs
CHANGED
|
@@ -26,7 +26,25 @@ import {
|
|
|
26
26
|
import { dirname, join } from "node:path";
|
|
27
27
|
|
|
28
28
|
// The working-set scan roots inside .rafa/ — brain-shaped authoring surfaces.
|
|
29
|
-
|
|
29
|
+
// P2 typed candidates (capture-engine spec r2 §3): improvements ride as
|
|
30
|
+
// candidates (files only — improve/ledger.md is DERIVED, regenerated at push,
|
|
31
|
+
// never a candidate) and the post-commit hook's intent records travel as
|
|
32
|
+
// provenance. The platform stores paths untyped; kind derives from the path.
|
|
33
|
+
export const WORKING_DIRS = [
|
|
34
|
+
"brain/rules",
|
|
35
|
+
"brain/playbooks",
|
|
36
|
+
"improve/improvements",
|
|
37
|
+
"intent",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// The candidate kind a working-set path carries — the distiller's judge seam
|
|
41
|
+
// and the fold's filters key on this, never on a parallel type field that
|
|
42
|
+
// could drift from the path.
|
|
43
|
+
export function kindOfPath(path) {
|
|
44
|
+
if (path.startsWith("improve/improvements/")) return "improvement";
|
|
45
|
+
if (path.startsWith("intent/")) return "intent";
|
|
46
|
+
return "note";
|
|
47
|
+
}
|
|
30
48
|
|
|
31
49
|
export const hashOf = (text) => createHash("sha256").update(text).digest("hex");
|
|
32
50
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rafinery/cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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": {
|
|
7
|
-
"rafa": "bin/rafa.mjs"
|
|
7
|
+
"rafa": "bin/rafa.mjs",
|
|
8
|
+
"rafa-distiller": "bin/rafa-distiller.mjs"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"bin",
|
|
@@ -16,11 +17,16 @@
|
|
|
16
17
|
"node": ">=18"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@rafinery/okf": "
|
|
20
|
+
"@rafinery/okf": "workspace:*"
|
|
20
21
|
},
|
|
21
22
|
"publishConfig": {
|
|
22
23
|
"access": "public"
|
|
23
24
|
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"bundle": "node scripts/bundle-blueprint.mjs",
|
|
27
|
+
"prepare": "node scripts/bundle-blueprint.mjs",
|
|
28
|
+
"test": "node --test"
|
|
29
|
+
},
|
|
24
30
|
"keywords": [
|
|
25
31
|
"rafa",
|
|
26
32
|
"rafinery",
|
|
@@ -36,9 +42,5 @@
|
|
|
36
42
|
"url": "git+https://github.com/rafinery-ai/rafinery.git",
|
|
37
43
|
"directory": "packages/cli"
|
|
38
44
|
},
|
|
39
|
-
"homepage": "https://github.com/rafinery-ai/rafinery/tree/main/packages/cli"
|
|
40
|
-
|
|
41
|
-
"bundle": "node scripts/bundle-blueprint.mjs",
|
|
42
|
-
"test": "node --test"
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
+
"homepage": "https://github.com/rafinery-ai/rafinery/tree/main/packages/cli"
|
|
46
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
The MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) Atai Barkai
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
|
13
|
-
all copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
THE SOFTWARE.
|