@rafinery/cli 0.8.13 → 0.8.15
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 +8 -0
- package/blueprint/.claude/rafa/hooks/brain-commit.mjs +27 -0
- package/blueprint/.claude/skills/rafa-build/SKILL.md +10 -1
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +8 -1
- package/blueprint/.claude/skills/rafa-review/SKILL.md +126 -0
- package/lib/blueprint.mjs +1 -0
- package/lib/brain-repo.mjs +1 -1
- package/lib/releases.mjs +35 -0
- package/lib/review.mjs +244 -0
- package/package.json +1 -1
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,13 @@ 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 → everything the
|
|
139
|
+
change touches — rules AND playbooks (flow-still-true), open
|
|
140
|
+
improvements, stale-cite risk flags, open knowledge gaps,
|
|
141
|
+
related plans (.rafa/review.json + summary). The rafa-review
|
|
142
|
+
SOP's judge rules on precisely that list — generic "review
|
|
143
|
+
this PR" never happens. Advisory; never blocks.
|
|
136
144
|
migrate Bring this repo's structured files (plans, config) up to the schema
|
|
137
145
|
the installed CLI ships. Idempotent; review the diff after.
|
|
138
146
|
|
|
@@ -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
|
|
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)
|
|
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,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rafa-review
|
|
3
|
+
description: "rafa SOP — the brain-grounded review gate: `rafa review` computes exactly what a diff touches (cite-intersection, zero LLM): rules AND playbooks, open improvements, stale-cite risk, open gaps, related plans' decisions; 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), BOTH note kinds:
|
|
20
|
+
|
|
21
|
+
- **◆ direct** — a changed line falls inside a note's cited anchor range: the
|
|
22
|
+
code the note 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 note governs it.
|
|
25
|
+
- **⚠ stale cites** — the freshness sweep already flagged this note's anchors
|
|
26
|
+
as drifted: a RISK marker (verify the note against code before relying on
|
|
27
|
+
it in judgment — it may describe a world that no longer exists).
|
|
28
|
+
- **! open improvements** citing a touched file — is this change making the
|
|
29
|
+
debt better, worse, or is it the fix itself (then bloom's close flow)?
|
|
30
|
+
- **open knowledge gaps** (top-missed queries) — live demand near the change.
|
|
31
|
+
|
|
32
|
+
Output: `.rafa/review.json` + summary. A bounded pass SAYS what it dropped
|
|
33
|
+
(never a silent cap); a mid-pass transport failure marks the list PARTIAL.
|
|
34
|
+
|
|
35
|
+
## Token economy — BINDING (the gate must cost less than the bugs it catches)
|
|
36
|
+
|
|
37
|
+
The judge's spend is proportional to the DIFF, never to the brain. Owner rule
|
|
38
|
+
(2026-07-24): review checks only the diff; hydrated fields serve the judgment;
|
|
39
|
+
querying files wholesale is a waste. Concretely:
|
|
40
|
+
|
|
41
|
+
1. **The pre-pass is free AND selective** — zero LLM; every note carries a
|
|
42
|
+
deterministic RELEVANCE score (direct-hit +3 · failure:silent +2 · cite
|
|
43
|
+
anchor token appears in the diff's added lines +2 · title/summary keyword
|
|
44
|
+
overlap +1 each, capped) and an **`engage` tier**:
|
|
45
|
+
- `deep` — body-worthy: hydrate/fetch and judge fully;
|
|
46
|
+
- `triage` — summary-only: rule in/out from metadata + diff, fetch a body
|
|
47
|
+
ONLY if that makes a violation plausible;
|
|
48
|
+
- `skip` — governs the file but nothing in the change relates: NEVER
|
|
49
|
+
judged, never fetched — listed in review.json so nothing drops silently.
|
|
50
|
+
(Semantic relevance stays contract-reserved via `matchKind` — keywords +
|
|
51
|
+
citations are the v1 scorer; vectors arrive on evidence, never blindly.)
|
|
52
|
+
2. **Honor the tiers.** The judge never upgrades a `skip`, and upgrades a
|
|
53
|
+
`triage` to a body fetch only with a stated reason (the finding cites it).
|
|
54
|
+
3. **Local-first, never re-serve.** A note already hydrated under
|
|
55
|
+
`.rafa/brain/**` (plan/build recall put it there) is read FROM DISK —
|
|
56
|
+
no MCP round-trip, no double-serving. Fetch via `get_rule`/`get_playbook`
|
|
57
|
+
only for shortlist notes absent locally; `rafa hydrate <kind> <id>` them
|
|
58
|
+
so a re-judge after atlas's fix re-reads the disk copy (disposable-cache
|
|
59
|
+
semantics keep them out of commits).
|
|
60
|
+
**Frontmatter-first (the OKF leverage):** every file type has a strict
|
|
61
|
+
format (contract §11), so the MACHINE fields — `cites` · `anchor` ·
|
|
62
|
+
`failure` · `links` · `type` — parse without touching the prose body.
|
|
63
|
+
Judge from frontmatter + diff first; read the body only when they can't
|
|
64
|
+
settle it. A `contract` note's check is half-mechanical before any prose:
|
|
65
|
+
does the diff touch its `anchor` token anywhere without updating every
|
|
66
|
+
cited site (the B2 completeness question)?
|
|
67
|
+
4. **Deep-judge cap: 10 notes**, leverage-ranked (◆ direct > failure:silent >
|
|
68
|
+
⚠ stale-and-direct > · file). Anything beyond the cap is ruled on metadata
|
|
69
|
+
only and the verdict SAYS SO ("N notes judged shallow") — an honest bound,
|
|
70
|
+
never a silent one.
|
|
71
|
+
5. **Metadata-only lanes stay metadata-only**: gaps, decisions, relatedPlans,
|
|
72
|
+
improvements are judged from what the pre-pass/`get_plan` already carries —
|
|
73
|
+
no body fetches, no extra searches. NEVER load the whole brain, never
|
|
74
|
+
fetch a note the pre-pass didn't flag.
|
|
75
|
+
|
|
76
|
+
## Pass 2 — the judge (prism-style, scoped)
|
|
77
|
+
|
|
78
|
+
For the work list ONLY — never the whole diff, never taste. Each data kind has
|
|
79
|
+
its OWN question:
|
|
80
|
+
|
|
81
|
+
1. Per **rule hit** — the OKF `type` names the violation shape:
|
|
82
|
+
- `convention` → does the new code FOLLOW the local pattern the rule names?
|
|
83
|
+
- `contract` → the cross-file class: does the diff change any occurrence of
|
|
84
|
+
the rule's `anchor` without updating EVERY cited site (B2 completeness —
|
|
85
|
+
half-mechanical from frontmatter, see the economy rules)? `failure:
|
|
86
|
+
silent` contracts get the deepest scrutiny — that class never announces
|
|
87
|
+
itself.
|
|
88
|
+
Judge against code + rule, never against the author's claims.
|
|
89
|
+
2. Per **playbook hit** (`flow` / `how-to` types): is the documented
|
|
90
|
+
FLOW/how-to still TRUE after this change (steps, ordering, file routes)?
|
|
91
|
+
A diff that silently invalidates a playbook ships a lie to the next dev —
|
|
92
|
+
that's an ITERATE finding (or a working-set edit updating the playbook,
|
|
93
|
+
authored the branch way).
|
|
94
|
+
3. Per **⚠ stale-cite note**: verify the note against current code BEFORE
|
|
95
|
+
using it in judgment; if the diff is the thing that staled it further,
|
|
96
|
+
surface the scoped-refresh offer.
|
|
97
|
+
4. Per **open improvement**: touched for better or worse? Fixed-in-passing →
|
|
98
|
+
bloom's close flow (hydrate first, `status: fixed`, report).
|
|
99
|
+
5. **Related plans' decisions** (`get_plan` for each `relatedPlans` entry):
|
|
100
|
+
does the diff contradict a recorded decision? Contradicting one is the
|
|
101
|
+
owner's call to reopen — an ITERATE finding, never silently overridden.
|
|
102
|
+
6. **Open gaps**: does this change ANSWER one (author the note, close the
|
|
103
|
+
gap) — or deepen it?
|
|
104
|
+
7. **The active task's Done-check** (from `get_active_plan`): does the diff
|
|
105
|
+
satisfy what the plan said done means?
|
|
106
|
+
8. Verdict: **PASS** or **ITERATE** with per-finding citations (note id +
|
|
107
|
+
file:line). Findings feed atlas (validate-and-correct — atlas fixes, the
|
|
108
|
+
judge re-checks; the reviewer never edits).
|
|
109
|
+
9. Emit **`report_loop_event(category: "review-verdict", outcome:
|
|
110
|
+
PASS|ITERATE, subject: <task id or branch>)`** at the ruling — sage's
|
|
111
|
+
evidence, monotonic, never a session-end sweep.
|
|
112
|
+
|
|
113
|
+
## When it runs
|
|
114
|
+
|
|
115
|
+
- **The pre-push beat** — before `git push`, offer the gate ("review the
|
|
116
|
+
branch against the brain? <n> rules touched"). Opt-in, advisory, never
|
|
117
|
+
blocks a push (a declined offer is honored, not re-asked this session).
|
|
118
|
+
- **Plan-done verify** — rafa-build's final verify runs it as part of
|
|
119
|
+
declaring the build complete.
|
|
120
|
+
- **On demand** — `/rafa review` any time.
|
|
121
|
+
|
|
122
|
+
## Anti-patterns
|
|
123
|
+
- Judging files the pre-pass didn't flag (scope creep = generic review).
|
|
124
|
+
- Trusting the diff author's summary — judge code against rules.
|
|
125
|
+
- Blocking on the gate — it is advisory; the merge reconciler is the hard gate.
|
|
126
|
+
- Findings without citations (rule id + file:line) — uncited = unactionable.
|
package/lib/blueprint.mjs
CHANGED
package/lib/brain-repo.mjs
CHANGED
|
@@ -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,41 @@ 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.15",
|
|
269
|
+
contract: 1,
|
|
270
|
+
plans: 2,
|
|
271
|
+
requires: "update",
|
|
272
|
+
summary:
|
|
273
|
+
"THE REVIEW GATE + ZERO-LEAK MIRRORS. `rafa review`: deterministic brain-" +
|
|
274
|
+
"grounded review pre-pass — diff's changed lines ∩ the cite graph → rules AND " +
|
|
275
|
+
"playbooks + open improvements + stale-cite risk + open gaps, each note " +
|
|
276
|
+
"relevance-scored (OKF type-aware: contract class weighs heavier) into " +
|
|
277
|
+
"deep/triage/skip engage tiers so the judge's tokens scale with the DIFF, " +
|
|
278
|
+
"never the brain (rafa-review SOP: local-first + frontmatter-first reads, " +
|
|
279
|
+
"10-note deep cap, skips listed never judged). LOCAL-STATE EXCLUSION: the " +
|
|
280
|
+
"post-commit hook now ensures .rafa/.gitignore AND untracks previously " +
|
|
281
|
+
"tracked machine state (hydration.json · sensor queues · demo benchmark · " +
|
|
282
|
+
"distill staging · *.theirs.md) — the next mirror commit cleans the branch. " +
|
|
283
|
+
"Plus wave-2 SOPs: delta briefing + decision recall at plan time, branch-" +
|
|
284
|
+
"aware recall (tier-labeled), compass sitback, gap lifecycle beats.",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
version: "0.8.14",
|
|
288
|
+
contract: 1,
|
|
289
|
+
plans: 2,
|
|
290
|
+
requires: "update",
|
|
291
|
+
summary:
|
|
292
|
+
"THE PROVENANCE SPINE (published 2026-07-24). Branch manifest: `rafa manifest` " +
|
|
293
|
+
"(zero-command — the post-commit brain hook regenerates it per mirrored commit): " +
|
|
294
|
+
"lenient snapshot with mode:branch · base.brainSha · per-note provenance " +
|
|
295
|
+
"{baseBrainSha, commits[], tasks[]} — the reconciler's three-way handoff doc. " +
|
|
296
|
+
"rafa-commit skill: subjects carry the active task id ([task-id] type: subject) " +
|
|
297
|
+
"and the manifest lifts it into per-note task lineage. Deterministic loop " +
|
|
298
|
+
"events: push gates emit gate-result from real exit codes; reflex --consume " +
|
|
299
|
+
"emits reflex-outcome. Benchmark is workflow-woven (scan's Prove-it step + " +
|
|
300
|
+
"session-start absence sensor) — never a dev-typed command.",
|
|
301
|
+
},
|
|
267
302
|
{
|
|
268
303
|
version: "0.8.13",
|
|
269
304
|
contract: 1,
|
package/lib/review.mjs
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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 *and* PLAYBOOKS and improvements that know about it), producing the
|
|
5
|
+
// exact, 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 notes
|
|
7
|
+
// against THESE lines. Also surfaced: per-note stale-cite RISK flags (the
|
|
8
|
+
// freshness sweep's signal) and the repo's open knowledge gaps — the judge
|
|
9
|
+
// weighs whether this change answers or worsens one.
|
|
10
|
+
//
|
|
11
|
+
// rafa review [--base <ref>] [--json]
|
|
12
|
+
//
|
|
13
|
+
// Base defaults to merge-base(HEAD, origin/<default>) — the review covers
|
|
14
|
+
// everything this branch changed (committed + working tree). Advisory always:
|
|
15
|
+
// exit 0, never blocks. Output: .rafa/review.json + a human summary.
|
|
16
|
+
//
|
|
17
|
+
// hit: "direct" — a changed line falls INSIDE a cite's line range (the rule's
|
|
18
|
+
// anchor itself moved — highest review priority)
|
|
19
|
+
// hit: "file" — the file changed elsewhere; the rule still governs it
|
|
20
|
+
//
|
|
21
|
+
// Open improvements citing a touched file + stale-risk flags ride along. The
|
|
22
|
+
// SOP (rafa-review) judges the work list, feeds findings to atlas, and emits
|
|
23
|
+
// report_loop_event(review-verdict, PASS|ITERATE).
|
|
24
|
+
|
|
25
|
+
import { execSync } from "node:child_process";
|
|
26
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { callTool } from "./mcp-client.mjs";
|
|
29
|
+
|
|
30
|
+
const MAX_FILES = 50; // bounded fan-out — anything dropped is SAID, never silent
|
|
31
|
+
|
|
32
|
+
const flag = (args, name) => {
|
|
33
|
+
const i = args.indexOf(name);
|
|
34
|
+
return i !== -1 && args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const sh = (cmd, cwd) => {
|
|
38
|
+
try {
|
|
39
|
+
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Parse `git diff -U0` → path → changed NEW-side line ranges [{start, end}].
|
|
46
|
+
export function changedRanges(diffText) {
|
|
47
|
+
const byFile = new Map();
|
|
48
|
+
let current = null;
|
|
49
|
+
for (const line of diffText.split("\n")) {
|
|
50
|
+
const f = line.match(/^\+\+\+ b\/(.+)$/);
|
|
51
|
+
if (f) {
|
|
52
|
+
current = f[1];
|
|
53
|
+
if (!byFile.has(current)) byFile.set(current, []);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const h = current && line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
57
|
+
if (h) {
|
|
58
|
+
const start = Number(h[1]);
|
|
59
|
+
const count = h[2] !== undefined ? Number(h[2]) : 1;
|
|
60
|
+
if (count > 0) byFile.get(current).push({ start, end: start + count - 1 });
|
|
61
|
+
else byFile.get(current).push({ start, end: start }); // pure deletion → the seam line
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return byFile;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A cite line ("12" | "10-14") vs the changed ranges → direct hit?
|
|
68
|
+
export function citeHits(citeLine, ranges) {
|
|
69
|
+
const m = String(citeLine ?? "").match(/^(\d+)(?:-(\d+))?$/);
|
|
70
|
+
if (!m) return false;
|
|
71
|
+
const a = Number(m[1]);
|
|
72
|
+
const b = m[2] !== undefined ? Number(m[2]) : a;
|
|
73
|
+
return ranges.some((r) => a <= r.end && b >= r.start);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Identifier tokens from the diff's ADDED lines, per file — the relevance
|
|
77
|
+
// vocabulary a note must intersect to earn the judge's attention.
|
|
78
|
+
export function addedTokens(diffText) {
|
|
79
|
+
const byFile = new Map();
|
|
80
|
+
let current = null;
|
|
81
|
+
for (const line of diffText.split("\n")) {
|
|
82
|
+
const f = line.match(/^\+\+\+ b\/(.+)$/);
|
|
83
|
+
if (f) {
|
|
84
|
+
current = f[1];
|
|
85
|
+
if (!byFile.has(current)) byFile.set(current, new Set());
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (current && line.startsWith("+") && !line.startsWith("+++"))
|
|
89
|
+
for (const t of line.slice(1).toLowerCase().match(/[a-z_][a-z0-9_-]{2,}/g) ?? [])
|
|
90
|
+
byFile.get(current).add(t);
|
|
91
|
+
}
|
|
92
|
+
return byFile;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Deterministic relevance (owner 2026-07-24: never hydrate/judge blindly —
|
|
96
|
+
// keywords + citations decide whether a note ADDS VALUE to this diff; the OKF
|
|
97
|
+
// type registry is leveraged: every type has a format, so every type has a
|
|
98
|
+
// DEFINED violation shape and a deterministic weight):
|
|
99
|
+
// direct anchor hit +3 (the cited code itself moved)
|
|
100
|
+
// type: contract +2 (the cross-file class — breaks span files)
|
|
101
|
+
// failure: silent +2 (the class that never announces itself)
|
|
102
|
+
// cite anchor token in diff +2 (the rule's own token appears in new code)
|
|
103
|
+
// title/summary keyword +1 each, capped at +3
|
|
104
|
+
// → engage: "deep" (≥3: body-worthy) · "triage" (1–2: summary-only) ·
|
|
105
|
+
// "skip" (0: governs the file but nothing relates — listed, never judged).
|
|
106
|
+
export function relevanceOf(note, diffTokens) {
|
|
107
|
+
let score = 0;
|
|
108
|
+
if (note.hit === "direct") score += 3;
|
|
109
|
+
if (note.type === "contract") score += 2;
|
|
110
|
+
if (note.failure === "silent") score += 2;
|
|
111
|
+
const anchor = String(note.token ?? "").toLowerCase();
|
|
112
|
+
if (anchor && [...diffTokens].some((t) => anchor.includes(t) || t.includes(anchor))) score += 2;
|
|
113
|
+
let kw = 0;
|
|
114
|
+
const text = `${note.title ?? ""} ${note.summary ?? ""}`.toLowerCase();
|
|
115
|
+
for (const t of diffTokens) if (text.includes(t)) kw += 1;
|
|
116
|
+
score += Math.min(kw, 3);
|
|
117
|
+
return { score, engage: score >= 3 ? "deep" : score >= 1 ? "triage" : "skip" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default async function review(args = []) {
|
|
121
|
+
const ROOT = process.cwd();
|
|
122
|
+
if (!existsSync(join(ROOT, "rafa.json"))) {
|
|
123
|
+
console.error("✗ not a rafa repo (no rafa.json)");
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const defaultBranch =
|
|
127
|
+
sh("git symbolic-ref --short refs/remotes/origin/HEAD", ROOT).replace(/^origin\//, "") || "main";
|
|
128
|
+
const base =
|
|
129
|
+
flag(args, "--base") ?? sh(`git merge-base HEAD "origin/${defaultBranch}"`, ROOT) ?? "";
|
|
130
|
+
if (!base) {
|
|
131
|
+
console.error(`✗ cannot resolve a review base (no origin/${defaultBranch}?) — pass --base <ref>`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const diff = sh(`git diff -U0 ${base}`, ROOT);
|
|
136
|
+
const ranges = changedRanges(diff);
|
|
137
|
+
const tokensByFile = addedTokens(diff);
|
|
138
|
+
// Code files only — the brain plane reviews itself at reconcile, not here.
|
|
139
|
+
const files = [...ranges.keys()].filter(
|
|
140
|
+
(p) => !p.startsWith(".rafa/") && !p.startsWith(".claude/") && p !== "rafa.json",
|
|
141
|
+
);
|
|
142
|
+
const reviewed = files.slice(0, MAX_FILES);
|
|
143
|
+
const dropped = files.length - reviewed.length;
|
|
144
|
+
|
|
145
|
+
const out = [];
|
|
146
|
+
let unreachable = null;
|
|
147
|
+
for (const path of reviewed) {
|
|
148
|
+
let ctx;
|
|
149
|
+
try {
|
|
150
|
+
ctx = await callTool(ROOT, "get_code_context", { path });
|
|
151
|
+
} catch (e) {
|
|
152
|
+
unreachable = e instanceof Error ? e.message : String(e);
|
|
153
|
+
break; // one transport failure fails them all — say so, don't half-report
|
|
154
|
+
}
|
|
155
|
+
const fileRanges = ranges.get(path) ?? [];
|
|
156
|
+
// BOTH note kinds engage: a rule hit asks "does the diff still honor the
|
|
157
|
+
// convention/contract"; a playbook hit asks "is the documented flow/how-to
|
|
158
|
+
// still true after this change". staleCites = freshness RISK (the note may
|
|
159
|
+
// already describe drifted code — verify before relying on it).
|
|
160
|
+
const fileTokens = tokensByFile.get(path) ?? new Set();
|
|
161
|
+
const notes = (ctx.notes ?? []).map((n) => {
|
|
162
|
+
const base = {
|
|
163
|
+
id: n.id,
|
|
164
|
+
kind: n.kind,
|
|
165
|
+
...(n.type ? { type: n.type } : {}),
|
|
166
|
+
title: n.title,
|
|
167
|
+
domain: n.domain,
|
|
168
|
+
line: n.line,
|
|
169
|
+
token: n.token,
|
|
170
|
+
...(n.failure ? { failure: n.failure } : {}),
|
|
171
|
+
// The one-line summary IS the triage surface — the judge rules most
|
|
172
|
+
// notes in/out from this alone, fetching bodies only for the shortlist.
|
|
173
|
+
...(n.summary ? { summary: n.summary } : {}),
|
|
174
|
+
...(n.staleCites ? { staleCites: n.staleCites } : {}),
|
|
175
|
+
hit: citeHits(n.line, fileRanges) ? "direct" : "file",
|
|
176
|
+
};
|
|
177
|
+
const { score, engage } = relevanceOf(base, fileTokens);
|
|
178
|
+
return { ...base, relevance: score, engage };
|
|
179
|
+
});
|
|
180
|
+
const improvements = (ctx.improvements ?? []).filter((i) => i.status === "open");
|
|
181
|
+
if (notes.length > 0 || improvements.length > 0)
|
|
182
|
+
out.push({ path, ranges: fileRanges, notes, improvements, relatedPlans: ctx.relatedPlans ?? [] });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Open knowledge gaps — live demand near this change (queries + counts only).
|
|
186
|
+
// The judge weighs: does this diff ANSWER one (close it) or deepen it?
|
|
187
|
+
let gaps = [];
|
|
188
|
+
try {
|
|
189
|
+
const g = await callTool(ROOT, "get_knowledge_gaps", { status: "open", limit: 5 });
|
|
190
|
+
gaps = (g.gaps ?? []).map((x) => ({ q: x.q, misses: x.misses }));
|
|
191
|
+
} catch {
|
|
192
|
+
/* gaps are context, not a gate — absence never fails the pass */
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const report = {
|
|
196
|
+
schemaVersion: 1,
|
|
197
|
+
base,
|
|
198
|
+
filesChanged: files.length,
|
|
199
|
+
filesReviewed: reviewed.length,
|
|
200
|
+
...(dropped > 0 ? { dropped } : {}),
|
|
201
|
+
...(unreachable ? { unreachable } : {}),
|
|
202
|
+
files: out,
|
|
203
|
+
...(gaps.length > 0 ? { openGaps: gaps } : {}),
|
|
204
|
+
};
|
|
205
|
+
writeFileSync(join(ROOT, ".rafa", "review.json"), JSON.stringify(report, null, 2) + "\n");
|
|
206
|
+
|
|
207
|
+
if (args.includes("--json")) {
|
|
208
|
+
console.log(JSON.stringify(report, null, 2));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const direct = out.flatMap((f) => f.notes.filter((r) => r.hit === "direct"));
|
|
212
|
+
const fileHits = out.flatMap((f) => f.notes.filter((r) => r.hit === "file"));
|
|
213
|
+
const imps = out.flatMap((f) => f.improvements);
|
|
214
|
+
const stale = out.flatMap((f) => f.notes.filter((r) => r.staleCites));
|
|
215
|
+
const engaged = out.flatMap((f) => f.notes.filter((r) => r.engage !== "skip"));
|
|
216
|
+
const skipped = out.flatMap((f) => f.notes.filter((r) => r.engage === "skip"));
|
|
217
|
+
console.log(
|
|
218
|
+
`✓ review pre-pass vs ${base.slice(0, 7)}: ${files.length} changed file(s)` +
|
|
219
|
+
(dropped > 0 ? ` (${dropped} beyond the ${MAX_FILES}-file bound NOT reviewed)` : ""),
|
|
220
|
+
);
|
|
221
|
+
if (unreachable) console.log(` ! platform unreachable mid-pass — the work list is PARTIAL: ${unreachable}`);
|
|
222
|
+
if (out.length === 0) {
|
|
223
|
+
console.log(" no governed lines touched — nothing for the judge (the brain has no rules citing these files)");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (const f of out) {
|
|
227
|
+
console.log(` ${f.path}`);
|
|
228
|
+
for (const r of f.notes.filter((n) => n.engage !== "skip"))
|
|
229
|
+
console.log(
|
|
230
|
+
` ${r.hit === "direct" ? "◆" : "·"} [${r.engage}] ${r.kind} ${r.id} [${r.domain}] ${r.title}` +
|
|
231
|
+
`${r.failure ? ` (failure: ${r.failure})` : ""}${r.staleCites ? ` ⚠ ${r.staleCites} stale cite(s)` : ""}`,
|
|
232
|
+
);
|
|
233
|
+
for (const i of f.improvements) console.log(` ! open ${i.priority}: ${i.id} ${i.title}`);
|
|
234
|
+
}
|
|
235
|
+
if (gaps.length)
|
|
236
|
+
console.log(` open knowledge gaps (does this change answer one?): ${gaps.map((g) => `"${g.q}"×${g.misses}`).join(" · ")}`);
|
|
237
|
+
console.log(
|
|
238
|
+
`→ engage ${engaged.length}/${engaged.length + skipped.length} note(s) (${direct.length} direct · ${fileHits.length} same-file` +
|
|
239
|
+
(skipped.length ? ` · ${skipped.length} governs-the-file-but-unrelated SKIPPED, listed in review.json` : "") +
|
|
240
|
+
`) · ${imps.length} open improvement(s)` +
|
|
241
|
+
(stale.length ? ` · ⚠ ${stale.length} stale-cite note(s) — verify before relying` : "") +
|
|
242
|
+
`\n The judge rules on exactly the engaged set (rafa-review SOP): deep = body-worthy, triage = summary-only.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
package/package.json
CHANGED