@rafinery/cli 0.8.13 → 0.8.14

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/bin/rafa.mjs CHANGED
@@ -54,6 +54,7 @@ const COMMANDS = [
54
54
  "leverage",
55
55
  "benchmark",
56
56
  "manifest",
57
+ "review",
57
58
  "migrate",
58
59
  ];
59
60
  const [cmd, ...rest] = process.argv.slice(2);
@@ -133,6 +134,11 @@ Commands:
133
134
  count harness tokens, emit a MEASURED benchmark.json. --dry-run
134
135
  exercises the scaffold from fixture counts; --help documents the
135
136
  agent-driven run procedure. Measured — never the estimated baseline.
137
+ review Brain-grounded review pre-pass (deterministic, zero LLM): the
138
+ diff's changed lines ∩ the brain's cite graph → the exact rules/
139
+ improvements the change touches (.rafa/review.json + summary).
140
+ The rafa-review SOP's judge then rules on precisely that list —
141
+ generic "review this PR" never happens. Advisory; never blocks.
136
142
  migrate Bring this repo's structured files (plans, config) up to the schema
137
143
  the installed CLI ships. Idempotent; review the diff after.
138
144
 
@@ -142,6 +142,33 @@ try {
142
142
  `\n`,
143
143
  );
144
144
 
145
+ // Local-state exclusion (owner 2026-07-24): the hydration sidecar + sensor
146
+ // queues are MACHINE state, never knowledge — ensure the ignore entries and
147
+ // UNTRACK anything an earlier CLI's window let git track (a .gitignore entry
148
+ // alone never untracks; `git add -A` would keep committing it forever).
149
+ // Best-effort: exclusion must never block a code commit.
150
+ try {
151
+ const LOCAL_STATE = [
152
+ "hydration.json",
153
+ "dirty.jsonl",
154
+ "reflex.jsonl",
155
+ "sensor-errors.jsonl",
156
+ "distill-verdicts.json",
157
+ "benchmark.demo.json",
158
+ ];
159
+ const gi = join(rafa, ".gitignore");
160
+ const cur = existsSync(gi) ? readFileSync(gi, "utf8") : "";
161
+ const have = new Set(cur.split("\n").map((l) => l.trim()).filter(Boolean));
162
+ const missing = [...LOCAL_STATE, "*.theirs.md", "distill-incoming/"].filter((l) => !have.has(l));
163
+ if (missing.length)
164
+ writeFileSync(gi, cur + (cur === "" || cur.endsWith("\n") ? "" : "\n") + missing.join("\n") + "\n");
165
+ for (const p of LOCAL_STATE) shR(`git rm -q --cached --ignore-unmatch "${p}"`);
166
+ shR('git rm -q -r --cached --ignore-unmatch distill-incoming');
167
+ shR('git rm -q --cached --ignore-unmatch "*.theirs.md"');
168
+ } catch {
169
+ /* exclusion is best-effort */
170
+ }
171
+
145
172
  shR("git add -A");
146
173
  disposeHydrations(branch);
147
174
  shR(
@@ -19,6 +19,7 @@ platform MCP (one read path — the same surface any third-party agent uses).
19
19
  | **Executor** | atlas | RECALL the task's brain slice via MCP (`search_knowledge` + `get_rule`/`get_playbook`; honor non-exemplars) → implement, convention-adherent |
20
20
  | **Validator** | prism | validate the execution against the child's `## Done-check` — strict, unbiased, against code + brain, never against atlas's claims. **`status: done` only on prism PASS**; FAIL → atlas corrects (validate-and-correct at work time). Plan-done adds one line to the verdict: **working set reviewed — captured, or clean-with-reason** (a build that learned nothing SAYS so; a build that learned something SHOWS the files) |
21
21
  | **Improver** | bloom | **push**: new improvement opportunities spotted during execution → new ledger files. **close**: improvements fixed in passing → `status: fixed` in the ledger file + `report_improvement_status(id, fixed)` so the platform shows it LIVE as pending-reconciliation (the ledger row itself changes only at the next brain push — K1). **nudge**: top-leverage open item in the task's blast radius — opt-in, never blocking |
22
+ | **Coach** | compass | **sitback** (harness-arc): after each task's verdict + sweep, one beat of reflection — did THIS task reveal something about how this DEV works (a preference, a recurring friction, a steering pattern)? Repo knowledge goes to the working set, never here. A genuine dev-level observation becomes its OWN opt-in offer (consent doctrine: insights are NEVER under session consent) → `put_dev_insight` on yes. No observation = no offer — silence is the honest default |
22
23
 
23
24
  ## Procedure
24
25
 
@@ -26,6 +27,11 @@ platform MCP (one read path — the same surface any third-party agent uses).
26
27
  (envelope `brainForSha` vs local stamp → prompt `rafa push` if behind). MCP
27
28
  recall is automatic throughout — SOP-driven, never dev-invoked; a repo without
28
29
  the `rafinery` MCP connected falls back to local `.rafa/` file reads.
30
+ **On a feature branch, pass `branch: <current git branch>` to
31
+ search_knowledge/get_rule/get_playbook/get_improvement** — recall then
32
+ overlays the branch's live working set on canon, every non-canonical result
33
+ tier-labeled (the alert rule: a `source: {tier: "candidate"}` or
34
+ `branchOverlay` is branch state, not org truth — say so when you rely on it).
29
35
  **Session consent (asked ONCE, verbs ENUMERATED):** *"keep the platform
30
36
  updated as I work? That means exactly: (1) plan status + Log pushes on
31
37
  cadence, (2) checkpointing this branch's working set (edited/new brain
@@ -82,7 +88,10 @@ platform MCP (one read path — the same surface any third-party agent uses).
82
88
  **Gap close-out:** authored knowledge that answers an in-scope knowledge
83
89
  gap (adopted at plan time via `get_knowledge_gaps`) closes the loop —
84
90
  `set_gap_status(q, "closed")` at the same beat the note is authored.
85
- 4. **Verify** (prism-style) before declaring the plan done; final `push_plan` +
91
+ 4. **Verify** (prism-style) before declaring the plan done including the
92
+ [rafa-review](../rafa-review/SKILL.md) gate: `rafa review` scopes the exact
93
+ rules/improvements the branch's diff touches; the judge rules on that list
94
+ + each Done-check, emits `review-verdict`; final `push_plan` +
86
95
  `set_active_plan` (clear) + `rafa checkpoint`. **Dual status (single/double
87
96
  tick):** `status: done` is the dev ✓ — prism-earned, session-set, for
88
97
  leaves AND the epic (the epic's ✓ = every leaf verified done). DELIVERY is
@@ -46,7 +46,14 @@ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
46
46
  1. **Staleness check** — compare the platform envelope's `brainForSha` against the
47
47
  local brain stamp; if the platform is behind, surface "run `rafa push`" (never
48
48
  proceed silently on knowledge you know is stale — never block either).
49
- 2. **Recall** (atlas, via MCP) **decompose** into the WORK-ITEM TREE (contract
49
+ 2. **Recall** (atlas, via MCP) `get_coverage` now carries **`recentDeltas`**
50
+ (the last trunk merges' knowledge changes + which plan delivered each):
51
+ open the plan draft with a TWO-LINE BRIEFING of what changed in the blast
52
+ radius since it was last touched — planning starts from the deltas, never a
53
+ stale mental model. `search_knowledge` may return a **`decisions`** block
54
+ (prior recorded calls matching the query): read it BEFORE re-litigating a
55
+ settled decision — reopening one is the owner's move, not the plan's.
56
+ Then **decompose** into the WORK-ITEM TREE (contract
50
57
  §7 v2): one epic → tasks → subtasks (three ranks, never deeper). Every item
51
58
  carries the glimpse fields — `title` (what) · `description` (why) ·
52
59
  `approach` (how, one line) · `assignee` when known · `blocked_by` for
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: rafa-review
3
+ description: "rafa SOP — the brain-grounded review gate: `rafa review` computes the exact rules/improvements a diff touches (cite-intersection, zero LLM); a prism-style judge rules ONLY on that list + the active task's Done-check; findings feed atlas; the verdict emits a review-verdict loop event. Loaded before push/merge or on request."
4
+ ---
5
+
6
+ # review — the brain-grounded gate (harness-arc wave 2)
7
+
8
+ > Status: **active.** The at-the-gate moment: defects caught BEFORE merge, by
9
+ > the repo's own rules — not generic review taste. Depends on: brain (#1),
10
+ > plans (#3, for the Done-check). Runs at the pre-push beat (offer, never
11
+ > block) and on demand.
12
+
13
+ Generic "review this PR" is banned here. The gate is two passes with a hard
14
+ split:
15
+
16
+ ## Pass 1 — mechanical (the CLI, zero LLM)
17
+
18
+ `rafa review [--json]` — diff vs merge-base(origin default) → changed line
19
+ ranges ∩ the brain's cite graph (`get_code_context` per file):
20
+
21
+ - **◆ direct** — a changed line falls inside a rule's cited anchor range: the
22
+ code the rule stands on MOVED. Highest priority; also a staleness signal
23
+ (the note may need a scoped refresh — the dirty queue already tracks it).
24
+ - **· file** — the file changed elsewhere but the rule governs it.
25
+ - **! open improvements** citing a touched file — is this change making the
26
+ debt better, worse, or is it the fix itself (then bloom's close flow)?
27
+
28
+ Output: `.rafa/review.json` + summary. A bounded pass SAYS what it dropped
29
+ (never a silent cap); a mid-pass transport failure marks the list PARTIAL.
30
+
31
+ ## Pass 2 — the judge (prism-style, scoped)
32
+
33
+ For the work list ONLY — never the whole diff, never taste:
34
+
35
+ 1. Per **rule hit**: fetch the rule (`get_rule`, with `branch:` for the
36
+ working-set overlay) → does the diff still honor it? Judge against code +
37
+ rule, never against the author's claims. A `failure: silent` rule gets
38
+ extra scrutiny — that class never announces itself.
39
+ 2. Per **open improvement**: touched for better or worse? Fixed-in-passing →
40
+ bloom's close flow (hydrate first, `status: fixed`, report).
41
+ 3. **The active task's Done-check** (from `get_active_plan`): does the diff
42
+ satisfy what the plan said done means?
43
+ 4. Verdict: **PASS** or **ITERATE** with per-finding citations (rule id +
44
+ file:line). Findings feed atlas (validate-and-correct — atlas fixes, the
45
+ judge re-checks; the reviewer never edits).
46
+ 5. Emit **`report_loop_event(category: "review-verdict", outcome:
47
+ PASS|ITERATE, subject: <task id or branch>)`** at the ruling — sage's
48
+ evidence, monotonic, never a session-end sweep.
49
+
50
+ ## When it runs
51
+
52
+ - **The pre-push beat** — before `git push`, offer the gate ("review the
53
+ branch against the brain? <n> rules touched"). Opt-in, advisory, never
54
+ blocks a push (a declined offer is honored, not re-asked this session).
55
+ - **Plan-done verify** — rafa-build's final verify runs it as part of
56
+ declaring the build complete.
57
+ - **On demand** — `/rafa review` any time.
58
+
59
+ ## Anti-patterns
60
+ - Judging files the pre-pass didn't flag (scope creep = generic review).
61
+ - Trusting the diff author's summary — judge code against rules.
62
+ - Blocking on the gate — it is advisory; the merge reconciler is the hard gate.
63
+ - Findings without citations (rule id + file:line) — uncited = unactionable.
package/lib/blueprint.mjs CHANGED
@@ -39,6 +39,7 @@ export const BLUEPRINT = {
39
39
  ".claude/skills/rafa-plan",
40
40
  ".claude/skills/rafa-build",
41
41
  ".claude/skills/rafa-commit",
42
+ ".claude/skills/rafa-review",
42
43
  ".claude/skills/rafa-improve",
43
44
  ".claude/skills/rafa-distill",
44
45
  ".claude/skills/rafa-insights",
@@ -77,7 +77,7 @@ export function ensureBrainRepo(cwd, { requireRemote = true } = {}) {
77
77
  // (not knowledge). MERGED line-by-line so clones ignored under an older CLI
78
78
  // still gain new exclusions.
79
79
  const ignore = join(rafaDir, ".gitignore");
80
- const IGNORES = ["hydration.json", "*.theirs.md", "distill-incoming/", "distill-verdicts.json", "dirty.jsonl", "reflex.jsonl"];
80
+ const IGNORES = ["hydration.json", "*.theirs.md", "distill-incoming/", "distill-verdicts.json", "dirty.jsonl", "reflex.jsonl", "sensor-errors.jsonl", "benchmark.demo.json"];
81
81
  const cur = existsSync(ignore) ? readFileSync(ignore, "utf8") : "";
82
82
  const have = new Set(cur.split("\n").map((l) => l.trim()).filter(Boolean));
83
83
  const missing = IGNORES.filter((l) => !have.has(l));
package/lib/releases.mjs CHANGED
@@ -264,6 +264,20 @@ export const RELEASES = [
264
264
  "first branch checkout (bounded; a failure never breaks the checkout). " +
265
265
  "`rafa update` re-vendors the post-checkout hook.",
266
266
  },
267
+ {
268
+ version: "0.8.14",
269
+ contract: 1,
270
+ plans: 2,
271
+ requires: "update",
272
+ summary:
273
+ "LOCAL-STATE EXCLUSION (zero-leak mirrors): the hydration sidecar + sensor " +
274
+ "queues (hydration.json · dirty/reflex/sensor-errors.jsonl · benchmark.demo.json " +
275
+ "· distill staging · *.theirs.md) are machine state, never knowledge — the " +
276
+ "post-commit brain hook now ENSURES the .rafa/.gitignore entries AND untracks " +
277
+ "anything an earlier window let git track (gitignore alone never untracks), so " +
278
+ "the next mirror commit cleans the branch. ensureBrainRepo's ignore list gains " +
279
+ "sensor-errors.jsonl + benchmark.demo.json. `rafa update` re-vendors the hook.",
280
+ },
267
281
  {
268
282
  version: "0.8.13",
269
283
  contract: 1,
package/lib/review.mjs ADDED
@@ -0,0 +1,160 @@
1
+ // rafa review — the brain-grounded review gate's DETERMINISTIC pre-pass
2
+ // (harness-arc wave 2). Zero LLM here: the diff's changed line ranges are
3
+ // intersected with the brain's cite graph (get_code_context — file → the
4
+ // rules/playbooks/improvements that know about it), producing the exact,
5
+ // scoped work list the rafa-review SOP's judge then rules on. Generic
6
+ // "review this PR" never happens — the judge only ever sees THESE rules
7
+ // against THESE lines.
8
+ //
9
+ // rafa review [--base <ref>] [--json]
10
+ //
11
+ // Base defaults to merge-base(HEAD, origin/<default>) — the review covers
12
+ // everything this branch changed (committed + working tree). Advisory always:
13
+ // exit 0, never blocks. Output: .rafa/review.json + a human summary.
14
+ //
15
+ // hit: "direct" — a changed line falls INSIDE a cite's line range (the rule's
16
+ // anchor itself moved — highest review priority)
17
+ // hit: "file" — the file changed elsewhere; the rule still governs it
18
+ //
19
+ // Open improvements citing a touched file + stale-risk flags ride along. The
20
+ // SOP (rafa-review) judges the work list, feeds findings to atlas, and emits
21
+ // report_loop_event(review-verdict, PASS|ITERATE).
22
+
23
+ import { execSync } from "node:child_process";
24
+ import { existsSync, writeFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { callTool } from "./mcp-client.mjs";
27
+
28
+ const MAX_FILES = 50; // bounded fan-out — anything dropped is SAID, never silent
29
+
30
+ const flag = (args, name) => {
31
+ const i = args.indexOf(name);
32
+ return i !== -1 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : null;
33
+ };
34
+
35
+ const sh = (cmd, cwd) => {
36
+ try {
37
+ return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
38
+ } catch {
39
+ return "";
40
+ }
41
+ };
42
+
43
+ // Parse `git diff -U0` → path → changed NEW-side line ranges [{start, end}].
44
+ export function changedRanges(diffText) {
45
+ const byFile = new Map();
46
+ let current = null;
47
+ for (const line of diffText.split("\n")) {
48
+ const f = line.match(/^\+\+\+ b\/(.+)$/);
49
+ if (f) {
50
+ current = f[1];
51
+ if (!byFile.has(current)) byFile.set(current, []);
52
+ continue;
53
+ }
54
+ const h = current && line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
55
+ if (h) {
56
+ const start = Number(h[1]);
57
+ const count = h[2] !== undefined ? Number(h[2]) : 1;
58
+ if (count > 0) byFile.get(current).push({ start, end: start + count - 1 });
59
+ else byFile.get(current).push({ start, end: start }); // pure deletion → the seam line
60
+ }
61
+ }
62
+ return byFile;
63
+ }
64
+
65
+ // A cite line ("12" | "10-14") vs the changed ranges → direct hit?
66
+ export function citeHits(citeLine, ranges) {
67
+ const m = String(citeLine ?? "").match(/^(\d+)(?:-(\d+))?$/);
68
+ if (!m) return false;
69
+ const a = Number(m[1]);
70
+ const b = m[2] !== undefined ? Number(m[2]) : a;
71
+ return ranges.some((r) => a <= r.end && b >= r.start);
72
+ }
73
+
74
+ export default async function review(args = []) {
75
+ const ROOT = process.cwd();
76
+ if (!existsSync(join(ROOT, "rafa.json"))) {
77
+ console.error("✗ not a rafa repo (no rafa.json)");
78
+ process.exit(1);
79
+ }
80
+ const defaultBranch =
81
+ sh("git symbolic-ref --short refs/remotes/origin/HEAD", ROOT).replace(/^origin\//, "") || "main";
82
+ const base =
83
+ flag(args, "--base") ?? sh(`git merge-base HEAD "origin/${defaultBranch}"`, ROOT) ?? "";
84
+ if (!base) {
85
+ console.error(`✗ cannot resolve a review base (no origin/${defaultBranch}?) — pass --base <ref>`);
86
+ process.exit(1);
87
+ }
88
+
89
+ const diff = sh(`git diff -U0 ${base}`, ROOT);
90
+ const ranges = changedRanges(diff);
91
+ // Code files only — the brain plane reviews itself at reconcile, not here.
92
+ const files = [...ranges.keys()].filter(
93
+ (p) => !p.startsWith(".rafa/") && !p.startsWith(".claude/") && p !== "rafa.json",
94
+ );
95
+ const reviewed = files.slice(0, MAX_FILES);
96
+ const dropped = files.length - reviewed.length;
97
+
98
+ const out = [];
99
+ let unreachable = null;
100
+ for (const path of reviewed) {
101
+ let ctx;
102
+ try {
103
+ ctx = await callTool(ROOT, "get_code_context", { path });
104
+ } catch (e) {
105
+ unreachable = e instanceof Error ? e.message : String(e);
106
+ break; // one transport failure fails them all — say so, don't half-report
107
+ }
108
+ const fileRanges = ranges.get(path) ?? [];
109
+ const rules = (ctx.notes ?? []).map((n) => ({
110
+ id: n.id,
111
+ kind: n.kind,
112
+ title: n.title,
113
+ domain: n.domain,
114
+ line: n.line,
115
+ ...(n.failure ? { failure: n.failure } : {}),
116
+ hit: citeHits(n.line, fileRanges) ? "direct" : "file",
117
+ }));
118
+ const improvements = (ctx.improvements ?? []).filter((i) => i.status === "open");
119
+ if (rules.length > 0 || improvements.length > 0)
120
+ out.push({ path, ranges: fileRanges, rules, improvements, relatedPlans: ctx.relatedPlans ?? [] });
121
+ }
122
+
123
+ const report = {
124
+ schemaVersion: 1,
125
+ base,
126
+ filesChanged: files.length,
127
+ filesReviewed: reviewed.length,
128
+ ...(dropped > 0 ? { dropped } : {}),
129
+ ...(unreachable ? { unreachable } : {}),
130
+ files: out,
131
+ };
132
+ writeFileSync(join(ROOT, ".rafa", "review.json"), JSON.stringify(report, null, 2) + "\n");
133
+
134
+ if (args.includes("--json")) {
135
+ console.log(JSON.stringify(report, null, 2));
136
+ return;
137
+ }
138
+ const direct = out.flatMap((f) => f.rules.filter((r) => r.hit === "direct"));
139
+ const fileHits = out.flatMap((f) => f.rules.filter((r) => r.hit === "file"));
140
+ const imps = out.flatMap((f) => f.improvements);
141
+ console.log(
142
+ `✓ review pre-pass vs ${base.slice(0, 7)}: ${files.length} changed file(s)` +
143
+ (dropped > 0 ? ` (${dropped} beyond the ${MAX_FILES}-file bound NOT reviewed)` : ""),
144
+ );
145
+ if (unreachable) console.log(` ! platform unreachable mid-pass — the work list is PARTIAL: ${unreachable}`);
146
+ if (out.length === 0) {
147
+ console.log(" no governed lines touched — nothing for the judge (the brain has no rules citing these files)");
148
+ return;
149
+ }
150
+ for (const f of out) {
151
+ console.log(` ${f.path}`);
152
+ for (const r of f.rules)
153
+ console.log(` ${r.hit === "direct" ? "◆" : "·"} ${r.id} [${r.domain}] ${r.title}${r.failure ? ` (failure: ${r.failure})` : ""}`);
154
+ for (const i of f.improvements) console.log(` ! open ${i.priority}: ${i.id} ${i.title}`);
155
+ }
156
+ console.log(
157
+ `→ ${direct.length} direct anchor hit(s) · ${fileHits.length} same-file rule(s) · ${imps.length} open improvement(s)` +
158
+ `\n The judge rules on exactly these (rafa-review SOP): does the diff still honor each rule; does it satisfy the active task's Done-check.`,
159
+ );
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafinery/cli",
3
- "version": "0.8.13",
3
+ "version": "0.8.14",
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": {