@rafinery/cli 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/bin/rafa.mjs +37 -5
  3. package/blueprint/.claude/agents/atlas.md +2 -1
  4. package/blueprint/.claude/agents/sage.md +66 -0
  5. package/blueprint/.claude/commands/rafa.md +214 -269
  6. package/blueprint/.claude/rafa/contract.md +204 -115
  7. package/blueprint/.claude/rafa/hooks/post-tool.mjs +62 -0
  8. package/blueprint/.claude/rafa/hooks/pre-push +24 -0
  9. package/blueprint/.claude/rafa/hooks/session-start.mjs +229 -0
  10. package/blueprint/.claude/rafa/hooks/statusline.mjs +113 -0
  11. package/blueprint/.claude/rafa/hooks/user-prompt-submit.mjs +87 -0
  12. package/blueprint/.claude/skills/rafa-build/SKILL.md +20 -5
  13. package/blueprint/.claude/skills/rafa-distill/SKILL.md +6 -1
  14. package/blueprint/.claude/skills/rafa-plan/SKILL.md +7 -0
  15. package/blueprint/.claude/skills/rafa-sage/SKILL.md +201 -0
  16. package/blueprint/.claude/skills/rafa-scan/SKILL.md +55 -5
  17. package/blueprint/.claude/skills/rafa-validate/SKILL.md +15 -2
  18. package/lib/benchmark.mjs +573 -0
  19. package/lib/blueprint.mjs +11 -1
  20. package/lib/brain-repo.mjs +10 -4
  21. package/lib/ci-setup.mjs +2 -0
  22. package/lib/claude-config.mjs +77 -0
  23. package/lib/dirty.mjs +114 -0
  24. package/lib/distill.mjs +4 -0
  25. package/lib/gate/compile.mjs +293 -44
  26. package/lib/gate/verify-citations.mjs +214 -23
  27. package/lib/githook.mjs +54 -0
  28. package/lib/init.mjs +18 -0
  29. package/lib/pull.mjs +7 -0
  30. package/lib/push.mjs +21 -0
  31. package/lib/reflex.mjs +76 -0
  32. package/lib/releases.mjs +35 -0
  33. package/lib/status.mjs +152 -0
  34. package/lib/update.mjs +13 -0
  35. package/package.json +1 -1
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafinery/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.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": {