@rafinery/cli 0.5.0 → 0.6.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 +97 -0
- package/bin/rafa.mjs +30 -5
- package/blueprint/.claude/agents/atlas.md +2 -1
- package/blueprint/.claude/commands/rafa.md +71 -6
- package/blueprint/.claude/rafa/contract.md +32 -0
- package/blueprint/.claude/rafa/hooks/post-tool.mjs +62 -0
- package/blueprint/.claude/rafa/hooks/pre-push +24 -0
- package/blueprint/.claude/rafa/hooks/session-start.mjs +229 -0
- package/blueprint/.claude/rafa/hooks/statusline.mjs +113 -0
- package/blueprint/.claude/rafa/hooks/user-prompt-submit.mjs +87 -0
- package/blueprint/.claude/skills/rafa-build/SKILL.md +11 -4
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +7 -0
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +14 -3
- package/blueprint/.claude/skills/rafa-validate/SKILL.md +15 -2
- package/lib/blueprint.mjs +9 -1
- package/lib/brain-repo.mjs +10 -4
- package/lib/ci-setup.mjs +2 -0
- package/lib/claude-config.mjs +77 -0
- package/lib/dirty.mjs +114 -0
- package/lib/distill.mjs +4 -0
- package/lib/gate/compile.mjs +40 -0
- package/lib/gate/verify-citations.mjs +214 -23
- package/lib/githook.mjs +54 -0
- package/lib/init.mjs +18 -0
- package/lib/pull.mjs +7 -0
- package/lib/push.mjs +21 -0
- package/lib/reflex.mjs +76 -0
- package/lib/releases.mjs +35 -0
- package/lib/status.mjs +152 -0
- package/lib/update.mjs +13 -0
- package/package.json +1 -1
package/lib/githook.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The git pre-push hook installer — M5 sensor #3 (the deterministic checkpoint
|
|
2
|
+
// boundary). `git push` of the code branch is a ratified natural checkpoint
|
|
3
|
+
// moment; this hook makes it MECHANICAL instead of SOP-remembered.
|
|
4
|
+
//
|
|
5
|
+
// .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 sensor.
|
|
7
|
+
// Never clobbers a foreign hook: ours is recognized by its marker line; anything
|
|
8
|
+
// else is respected and the chain-line printed for the dev to add themselves.
|
|
9
|
+
|
|
10
|
+
import { chmodSync, copyFileSync, existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
const MARKER = "rafa pre-push hook";
|
|
15
|
+
const CHAIN_LINE = "npx -y @rafinery/cli checkpoint || true";
|
|
16
|
+
|
|
17
|
+
export function installPrePush(targetDir) {
|
|
18
|
+
const src = join(targetDir, ".claude", "rafa", "hooks", "pre-push");
|
|
19
|
+
if (!existsSync(src)) return { skipped: "no vendored hook template (run `rafa update`)" };
|
|
20
|
+
|
|
21
|
+
let hooksDir;
|
|
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");
|
|
31
|
+
if (existsSync(dst)) {
|
|
32
|
+
const body = readFileSync(dst, "utf8");
|
|
33
|
+
if (!body.includes(MARKER)) {
|
|
34
|
+
return {
|
|
35
|
+
skipped:
|
|
36
|
+
"a pre-push hook already exists (not rafa's) — to chain the checkpoint boundary, " +
|
|
37
|
+
`add this line to it yourself: ${CHAIN_LINE}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// ours → refresh in place (idempotent update)
|
|
41
|
+
copyFileSync(src, dst);
|
|
42
|
+
chmodSync(dst, 0o755);
|
|
43
|
+
return { updated: true };
|
|
44
|
+
}
|
|
45
|
+
copyFileSync(src, dst);
|
|
46
|
+
chmodSync(dst, 0o755);
|
|
47
|
+
return { installed: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function reportGitHook(r) {
|
|
51
|
+
if (r.installed) console.log(" ✓ git pre-push hook → checkpoint runs at every code push (non-blocking)");
|
|
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}`);
|
|
54
|
+
}
|
package/lib/init.mjs
CHANGED
|
@@ -105,6 +105,24 @@ export default async function init(args) {
|
|
|
105
105
|
);
|
|
106
106
|
// Grant rafa's tools their permissions (merge, never overwrite).
|
|
107
107
|
reportSettings(TARGET);
|
|
108
|
+
// Wire the M5 sensor hooks (SessionStart digest + PostToolUse dirty-marker)
|
|
109
|
+
// into .claude/settings.json — merge, never clobber the dev's own hooks.
|
|
110
|
+
{
|
|
111
|
+
const { mergeClaudeHooks, mergeStatusLine } = await import("./claude-config.mjs");
|
|
112
|
+
const h = mergeClaudeHooks(TARGET);
|
|
113
|
+
if (h.skipped) console.log(` ! hooks untouched — ${h.skipped}`);
|
|
114
|
+
else if (h.added?.length) console.log(` ✓ sensor hooks → .claude/settings.json (${h.added.join(", ")})`);
|
|
115
|
+
else console.log(" ✓ sensor hooks already wired");
|
|
116
|
+
const sl = mergeStatusLine(TARGET);
|
|
117
|
+
if (sl.skipped) console.log(` ! statusline untouched — ${sl.skipped}`);
|
|
118
|
+
else if (sl.added?.length) console.log(" ✓ loop-state statusline → .claude/settings.json");
|
|
119
|
+
else console.log(" ✓ loop-state statusline already wired");
|
|
120
|
+
}
|
|
121
|
+
// The pre-push checkpoint boundary is per-clone (.git/hooks is never committed).
|
|
122
|
+
{
|
|
123
|
+
const { installPrePush, reportGitHook } = await import("./githook.mjs");
|
|
124
|
+
reportGitHook(installPrePush(TARGET));
|
|
125
|
+
}
|
|
108
126
|
// Wire the repo's own MCP servers into the (owned) agent cards so subagents
|
|
109
127
|
// can actually reach them (zero-command orchestration, 2026-07-12).
|
|
110
128
|
{
|
package/lib/pull.mjs
CHANGED
|
@@ -32,6 +32,13 @@ export default async function pull(args = []) {
|
|
|
32
32
|
const inRafa = inDir(rafaDir);
|
|
33
33
|
if (bootstrapped) console.log(`• bootstrapped lazy .rafa/ from rafa.json (remote: ${remote})`);
|
|
34
34
|
|
|
35
|
+
// Every working copy gets the M5 checkpoint boundary (.git/hooks is per-clone —
|
|
36
|
+
// pull IS the teammate path, so this is where a fresh clone gains the sensor).
|
|
37
|
+
{
|
|
38
|
+
const { installPrePush, reportGitHook } = await import("./githook.mjs");
|
|
39
|
+
reportGitHook(installPrePush(ROOT));
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
// Reachability + default branch — loud if the remote can't be reached.
|
|
36
43
|
let branch = "main";
|
|
37
44
|
try {
|
package/lib/push.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { execSync } from "node:child_process";
|
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { runCompile } from "./gate/compile.mjs";
|
|
16
|
+
import { runVerifyCitations } from "./gate/verify-citations.mjs";
|
|
16
17
|
import { ensureBrainRepo, remoteDefaultBranch, inDir, die } from "./brain-repo.mjs";
|
|
17
18
|
import { CLI_VERSION } from "./releases.mjs";
|
|
18
19
|
|
|
@@ -39,6 +40,26 @@ export default async function push(args = []) {
|
|
|
39
40
|
/* no origin — manifest.repo stays "" */
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// ── citation gate (in-process) ──
|
|
44
|
+
// Re-run the checker HERE so the record that rides the push is the record of
|
|
45
|
+
// THIS push — a stale/hand-stamped citation-check.json can never ship
|
|
46
|
+
// (mechanized 2026-07-13 from prism's live-run recommendation: the stale-record
|
|
47
|
+
// class dies at the transport layer, not in review).
|
|
48
|
+
console.log("• rafa verify-citations — re-grounding every cite/absence/inventory …");
|
|
49
|
+
if (runVerifyCitations([]) !== 0)
|
|
50
|
+
die(
|
|
51
|
+
"citations failed the checker (see report above). Fix the notes against the " +
|
|
52
|
+
"code (or have atlas repair them), then re-push.",
|
|
53
|
+
);
|
|
54
|
+
if (
|
|
55
|
+
existsSync(join(ROOT, ".rafa", "improve", "improvements")) &&
|
|
56
|
+
runVerifyCitations(["--root=.rafa/improve", "--dirs=improvements"]) !== 0
|
|
57
|
+
)
|
|
58
|
+
die(
|
|
59
|
+
"improvement citations failed the checker (see report above). Fix the ledger " +
|
|
60
|
+
"files against the code (or have bloom correct them), then re-push.",
|
|
61
|
+
);
|
|
62
|
+
|
|
42
63
|
// ── contract gate (in-process) ──
|
|
43
64
|
// Compile + validate the brain. A schema-invalid brain NEVER leaves the machine —
|
|
44
65
|
// the platform ingests JSON, so what we push must already conform to the contract.
|
package/lib/reflex.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// rafa reflex — the correction queue's query/consume surface (M5, agent-internal;
|
|
2
|
+
// the UserPromptSubmit sensor writes the queue, the conductor works it).
|
|
3
|
+
//
|
|
4
|
+
// rafa reflex list unprocessed corrections
|
|
5
|
+
// rafa reflex --json machine shape (bootstrap digest / spawn prompts)
|
|
6
|
+
// rafa reflex --consume <id> [reason] mark one processed — AFTER it was banked
|
|
7
|
+
// through the gates, or judged session-only
|
|
8
|
+
//
|
|
9
|
+
// Consume APPENDS a done-marker line ({id, done:true, verdict, at}) — the queue
|
|
10
|
+
// is append-only like every M5 sensor stream (monotonic, torn-line tolerant);
|
|
11
|
+
// readers treat the latest line per id as truth. Nothing here ever ships: the
|
|
12
|
+
// queue is transport-excluded; only distilled cited notes pass the gates.
|
|
13
|
+
|
|
14
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
export function readReflex(root = process.cwd()) {
|
|
18
|
+
const file = join(root, ".rafa", "reflex.jsonl");
|
|
19
|
+
const byId = new Map(); // id → latest entry (done-markers override)
|
|
20
|
+
if (!existsSync(file)) return { file, pending: [], done: 0 };
|
|
21
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
22
|
+
if (!line.trim()) continue;
|
|
23
|
+
try {
|
|
24
|
+
const e = JSON.parse(line);
|
|
25
|
+
if (e?.id) byId.set(e.id, { ...(byId.get(e.id) ?? {}), ...e });
|
|
26
|
+
} catch {
|
|
27
|
+
/* torn line — skip */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const all = [...byId.values()];
|
|
31
|
+
return {
|
|
32
|
+
file,
|
|
33
|
+
pending: all.filter((e) => !e.done),
|
|
34
|
+
done: all.filter((e) => e.done).length,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default async function reflex(args = []) {
|
|
39
|
+
const ROOT = process.cwd();
|
|
40
|
+
const { file, pending, done } = readReflex(ROOT);
|
|
41
|
+
|
|
42
|
+
const ci = args.indexOf("--consume");
|
|
43
|
+
if (ci !== -1) {
|
|
44
|
+
const id = args[ci + 1];
|
|
45
|
+
if (!id || id.startsWith("-")) {
|
|
46
|
+
console.error("✗ usage: rafa reflex --consume <id> [banked|session-only|refuted]");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (!pending.some((e) => e.id === id)) {
|
|
50
|
+
console.error(`✗ no unprocessed correction with id ${id} (see \`rafa reflex\`)`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const verdict = args[ci + 2] && !args[ci + 2].startsWith("-") ? args[ci + 2] : "banked";
|
|
54
|
+
appendFileSync(
|
|
55
|
+
file,
|
|
56
|
+
JSON.stringify({ id, done: true, verdict, at: new Date().toISOString() }) + "\n",
|
|
57
|
+
);
|
|
58
|
+
console.log(`✓ correction ${id} consumed (${verdict})`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (args.includes("--json")) {
|
|
63
|
+
console.log(JSON.stringify({ pending, done }, null, 2));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (pending.length === 0) {
|
|
68
|
+
console.log(`✓ reflex queue clean${done ? ` (${done} processed)` : ""} — no unbanked corrections`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
console.log(`${pending.length} unprocessed correction(s):`);
|
|
72
|
+
for (const e of pending) console.log(` · ${e.id} [${e.t}] "${e.p}"`);
|
|
73
|
+
console.log(
|
|
74
|
+
"→ for each: bank it through the gates if it's durable repo knowledge (branch: note + checkpoint · main: full gates), then `rafa reflex --consume <id>` — or consume with `session-only` if it isn't.",
|
|
75
|
+
);
|
|
76
|
+
}
|
package/lib/releases.mjs
CHANGED
|
@@ -133,6 +133,41 @@ export const RELEASES = [
|
|
|
133
133
|
"files mechanically (parent→epic, child→task, blocked→todo+blocked_reason). Brain data " +
|
|
134
134
|
"schema unchanged; no re-scan.",
|
|
135
135
|
},
|
|
136
|
+
{
|
|
137
|
+
version: "0.6.0",
|
|
138
|
+
contract: 1,
|
|
139
|
+
plans: 2,
|
|
140
|
+
requires: "update",
|
|
141
|
+
summary:
|
|
142
|
+
"The core-solidity release — checker v2 + the M5 capture engine. RATCHET: " +
|
|
143
|
+
"verify-citations gains the two 2026-06-08 catches as gates — ABSENCE (declared " +
|
|
144
|
+
"`absent:` tokens re-grepped every run; a stale absence claim fails at gate 1) and " +
|
|
145
|
+
"INVENTORY (coverage `inventory: <name> :: <glob> :: <count>` recomputed via git " +
|
|
146
|
+
"ls-files; surface drift fails) — plus a non-failing WARN heuristic for undeclared " +
|
|
147
|
+
"absence-shaped claims (prism's worklist). The checker records its run " +
|
|
148
|
+
"(citation-check.json) and compile folds it into manifest.citations {checkerVersion, " +
|
|
149
|
+
"pass, at} — the platform learns which gate level a brain passed. M5 SENSORS " +
|
|
150
|
+
"(deterministic capture moments — no more SOP-only checkpoints): SessionStart state " +
|
|
151
|
+
"digest (staleness · conflicts · active plan), PostToolUse dirty-marking " +
|
|
152
|
+
"(.rafa/dirty.jsonl, monotonic, no session-end dependency), git pre-push runs " +
|
|
153
|
+
"`rafa checkpoint` (non-blocking; installed per-clone at init/pull/update). New " +
|
|
154
|
+
"`rafa dirty [--json|--consume]` — the cite-graph invalidator's query surface: dirty " +
|
|
155
|
+
"files → citing notes → scoped refresh offer at boundaries; drift threshold → " +
|
|
156
|
+
"fire-alarm (/rafa scan --brain-only). Hooks vendor LOCKSTEP at .claude/rafa/hooks/ " +
|
|
157
|
+
"and are disabled in headless/CI runs (RAFA_HOOKS_DISABLED=1 — the recursion guard). " +
|
|
158
|
+
"THE CORRECTION REFLEX: correction-shaped prompts are queued (.rafa/reflex.jsonl) and " +
|
|
159
|
+
"steer the session to validate + bank them through the gates SAME-SESSION (rafa reflex " +
|
|
160
|
+
"--consume closes each with a verdict; ungroundable claims never bank). " +
|
|
161
|
+
"THE FRICTIONLESS LOOP: loop-state statusline (rafa ▸ plan 2/5 · 3 stale · 1 correction; " +
|
|
162
|
+
"never replaces a dev's own statusline; `rafa status --line|--json` is the harness-" +
|
|
163
|
+
"neutral core — Claude Code today, Cursor/Codex adapters later) · a deterministic " +
|
|
164
|
+
"'suggested next' line in the session digest · DIRECT-DO routing (small work = no plan " +
|
|
165
|
+
"files, no approval; escalates only when it grows) · brainstorm mode (grounded " +
|
|
166
|
+
"participant, one crystallization offer). " +
|
|
167
|
+
"`rafa push` now re-runs the checker itself (stale/hand-stamped check records can " +
|
|
168
|
+
"never ship). Brain data schema unchanged; no re-scan; adopt with `rafa update` " +
|
|
169
|
+
"(re-vendors hooks, merges settings + statusline, installs the pre-push boundary).",
|
|
170
|
+
},
|
|
136
171
|
];
|
|
137
172
|
|
|
138
173
|
// The release this CLI build ships (last entry).
|
package/lib/status.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// rafa status — the loop-state read, HARNESS-NEUTRAL CORE (portability
|
|
2
|
+
// directive, 2026-07-13: built for Claude Code now, ships for Cursor/Codex
|
|
3
|
+
// later — the logic lives HERE; harness adapters stay thin mirrors).
|
|
4
|
+
//
|
|
5
|
+
// rafa status human view of where the loop stands
|
|
6
|
+
// rafa status --line the one-line ambient form (statuslines, prompts, TUIs)
|
|
7
|
+
// rafa status --json machine shape (adapters, conductors, other harnesses)
|
|
8
|
+
//
|
|
9
|
+
// Reads ONLY local state (the queues + plans + active pointer) — fast, offline,
|
|
10
|
+
// no network. The Claude Code statusline adapter (.claude/rafa/hooks/
|
|
11
|
+
// statusline.mjs) mirrors this computation as a self-contained script; any
|
|
12
|
+
// drift between the two is a bug against THIS file's semantics.
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
function readJsonl(file, pick) {
|
|
18
|
+
if (!existsSync(file)) return [];
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
21
|
+
if (!line.trim()) continue;
|
|
22
|
+
try {
|
|
23
|
+
const e = JSON.parse(line);
|
|
24
|
+
if (e) out.push(e);
|
|
25
|
+
} catch {
|
|
26
|
+
/* torn line */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return pick ? pick(out) : out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function walk(dir, depth, hit, out = []) {
|
|
33
|
+
if (depth < 0 || !existsSync(dir)) return out;
|
|
34
|
+
for (const e of readdirSync(dir)) {
|
|
35
|
+
if (e === ".git") continue;
|
|
36
|
+
const p = join(dir, e);
|
|
37
|
+
try {
|
|
38
|
+
if (statSync(p).isDirectory()) walk(p, depth - 1, hit, out);
|
|
39
|
+
else if (hit(e)) out.push(p);
|
|
40
|
+
} catch {
|
|
41
|
+
/* races are fine */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Frontmatter line-scan (the hooks' shared idiom — never a YAML parser).
|
|
48
|
+
function fmField(path, key) {
|
|
49
|
+
try {
|
|
50
|
+
const lines = readFileSync(path, "utf8").split("\n");
|
|
51
|
+
let fence = 0;
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
if (line.trim() === "---") { fence++; if (fence >= 2) break; continue; }
|
|
54
|
+
if (fence !== 1) continue;
|
|
55
|
+
const m = line.match(new RegExp(`^${key}:\\s*(.+?)\\s*$`));
|
|
56
|
+
if (m) return m[1].replace(/\s+#.*$/, "").replace(/^["']|["']$/g, "").trim();
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
/* unreadable = absent */
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The whole loop state, from local files only.
|
|
65
|
+
export function computeStatus(root = process.cwd()) {
|
|
66
|
+
const rafaDir = join(root, ".rafa");
|
|
67
|
+
const provisioned = existsSync(join(root, "rafa.json"));
|
|
68
|
+
|
|
69
|
+
// dirty: unique files
|
|
70
|
+
const dirty = new Set(
|
|
71
|
+
readJsonl(join(rafaDir, "dirty.jsonl")).filter((e) => typeof e.f === "string").map((e) => e.f),
|
|
72
|
+
).size;
|
|
73
|
+
|
|
74
|
+
// reflex: pending = latest-per-id without a done marker
|
|
75
|
+
const byId = new Map();
|
|
76
|
+
for (const e of readJsonl(join(rafaDir, "reflex.jsonl"))) {
|
|
77
|
+
if (e.id) byId.set(e.id, { ...(byId.get(e.id) ?? {}), ...e });
|
|
78
|
+
}
|
|
79
|
+
const reflex = [...byId.values()].filter((e) => !e.done).length;
|
|
80
|
+
|
|
81
|
+
// checkpoint conflicts awaiting a human
|
|
82
|
+
const conflicts = walk(rafaDir, 4, (n) => n.endsWith(".theirs.md")).length;
|
|
83
|
+
|
|
84
|
+
// active plan + task progress (plans v2: kind task|subtask, status done|…)
|
|
85
|
+
let plan = null;
|
|
86
|
+
const activePath = join(rafaDir, "active.md");
|
|
87
|
+
if (existsSync(activePath)) {
|
|
88
|
+
const first = readFileSync(activePath, "utf8").split("\n")[0].trim();
|
|
89
|
+
const id = first.startsWith("# ") && first !== "# No active plan" ? first.slice(2).trim() : null;
|
|
90
|
+
if (id) {
|
|
91
|
+
let done = 0;
|
|
92
|
+
let total = 0;
|
|
93
|
+
let current = null;
|
|
94
|
+
for (const f of walk(join(rafaDir, "plans"), 3, (n) => n.endsWith(".md") && !n.startsWith("_"))) {
|
|
95
|
+
if (fmField(f, "plan") !== id) continue;
|
|
96
|
+
const kind = fmField(f, "kind");
|
|
97
|
+
if (kind !== "task" && kind !== "subtask") continue;
|
|
98
|
+
total++;
|
|
99
|
+
const status = fmField(f, "status");
|
|
100
|
+
if (status === "done") done++;
|
|
101
|
+
else if (status === "in-progress" && !current)
|
|
102
|
+
current = fmField(f, "title") ?? fmField(f, "id");
|
|
103
|
+
}
|
|
104
|
+
plan = { id, done, total, current };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { provisioned, plan, dirty, reflex, conflicts };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The ambient one-liner — identical semantics wherever it renders.
|
|
112
|
+
export function formatLine(s) {
|
|
113
|
+
if (!s.provisioned) return "";
|
|
114
|
+
const parts = [];
|
|
115
|
+
if (s.plan)
|
|
116
|
+
parts.push(
|
|
117
|
+
`plan ${s.plan.id} ${s.plan.done}/${s.plan.total}` +
|
|
118
|
+
(s.plan.current ? ` → ${s.plan.current}` : ""),
|
|
119
|
+
);
|
|
120
|
+
if (s.dirty) parts.push(`${s.dirty} stale`);
|
|
121
|
+
if (s.reflex) parts.push(`${s.reflex} correction${s.reflex === 1 ? "" : "s"}`);
|
|
122
|
+
if (s.conflicts) parts.push(`${s.conflicts} conflict${s.conflicts === 1 ? "" : "s"}`);
|
|
123
|
+
return parts.length ? `rafa ▸ ${parts.join(" · ")}` : "rafa ▸ in sync";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default async function status(args = []) {
|
|
127
|
+
const s = computeStatus(process.cwd());
|
|
128
|
+
if (args.includes("--json")) {
|
|
129
|
+
console.log(JSON.stringify(s, null, 2));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (args.includes("--line")) {
|
|
133
|
+
const line = formatLine(s);
|
|
134
|
+
if (line) console.log(line);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!s.provisioned) {
|
|
138
|
+
console.log("not a rafa repo (no rafa.json) — `npx @rafinery/cli init` to provision");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
console.log(formatLine(s));
|
|
142
|
+
if (s.plan)
|
|
143
|
+
console.log(
|
|
144
|
+
` plan: ${s.plan.id} — ${s.plan.done}/${s.plan.total} tasks done` +
|
|
145
|
+
(s.plan.current ? ` · in progress: ${s.plan.current}` : ""),
|
|
146
|
+
);
|
|
147
|
+
if (s.dirty) console.log(` stale: ${s.dirty} code file(s) touched since last reconcile (rafa dirty)`);
|
|
148
|
+
if (s.reflex) console.log(` corrections: ${s.reflex} unprocessed (rafa reflex)`);
|
|
149
|
+
if (s.conflicts) console.log(` conflicts: ${s.conflicts} .theirs.md awaiting your decision`);
|
|
150
|
+
if (!s.plan && !s.dirty && !s.reflex && !s.conflicts)
|
|
151
|
+
console.log(" nothing pending — the loop is closed.");
|
|
152
|
+
}
|
package/lib/update.mjs
CHANGED
|
@@ -38,6 +38,19 @@ export default async function update(args = []) {
|
|
|
38
38
|
const r = mergeRepoMcpsIntoCards(TARGET);
|
|
39
39
|
for (const u of r.updated) console.log(` ✓ repo MCPs → ${u}`);
|
|
40
40
|
}
|
|
41
|
+
// M5 sensors: hooks config (merge, never clobber) + the per-clone pre-push
|
|
42
|
+
// boundary + the ambient loop-state statusline.
|
|
43
|
+
{
|
|
44
|
+
const { mergeClaudeHooks, mergeStatusLine } = await import("./claude-config.mjs");
|
|
45
|
+
const h = mergeClaudeHooks(TARGET);
|
|
46
|
+
if (h.skipped) console.log(` ! hooks untouched — ${h.skipped}`);
|
|
47
|
+
else if (h.added?.length) console.log(` ✓ sensor hooks → .claude/settings.json (${h.added.join(", ")})`);
|
|
48
|
+
const sl = mergeStatusLine(TARGET);
|
|
49
|
+
if (sl.skipped) console.log(` ! statusline untouched — ${sl.skipped}`);
|
|
50
|
+
else if (sl.added?.length) console.log(" ✓ loop-state statusline → .claude/settings.json");
|
|
51
|
+
const { installPrePush, reportGitHook } = await import("./githook.mjs");
|
|
52
|
+
reportGitHook(installPrePush(TARGET));
|
|
53
|
+
}
|
|
41
54
|
|
|
42
55
|
// ── Blueprint-side migrations: mechanical/deterministic, run here in the CLI. ──
|
|
43
56
|
// (Anything needing intelligence is left for the brain-side pass — /rafa update.)
|
package/package.json
CHANGED