@rafinery/cli 0.8.12 → 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 +11 -0
- package/blueprint/.claude/rafa/hooks/brain-commit.mjs +40 -0
- package/blueprint/.claude/rafa/hooks/session-start.mjs +10 -0
- package/blueprint/.claude/skills/rafa-build/SKILL.md +28 -5
- package/blueprint/.claude/skills/rafa-commit/SKILL.md +45 -0
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +23 -3
- package/blueprint/.claude/skills/rafa-review/SKILL.md +63 -0
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +14 -2
- package/lib/blueprint.mjs +2 -0
- package/lib/brain-repo.mjs +1 -1
- package/lib/checkpoint.mjs +18 -0
- package/lib/hydrate.mjs +17 -6
- package/lib/manifest.mjs +208 -0
- package/lib/push.mjs +28 -3
- package/lib/reflex.mjs +15 -0
- package/lib/releases.mjs +28 -0
- package/lib/review.mjs +160 -0
- package/lib/working-set.mjs +43 -0
- package/package.json +1 -1
package/bin/rafa.mjs
CHANGED
|
@@ -53,6 +53,8 @@ const COMMANDS = [
|
|
|
53
53
|
"ci-setup",
|
|
54
54
|
"leverage",
|
|
55
55
|
"benchmark",
|
|
56
|
+
"manifest",
|
|
57
|
+
"review",
|
|
56
58
|
"migrate",
|
|
57
59
|
];
|
|
58
60
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
@@ -72,6 +74,10 @@ Commands:
|
|
|
72
74
|
--keep keep all changed owned files without asking
|
|
73
75
|
(default: ask per file; in a non-TTY it keeps them)
|
|
74
76
|
compile Validate the brain against the contract → .rafa/manifest.json.
|
|
77
|
+
manifest The BRANCH manifest (lenient snapshot — WIP files land under
|
|
78
|
+
drafts, never fail). You never need to run this: the post-commit
|
|
79
|
+
brain hook regenerates it on every mirrored commit (zero-command
|
|
80
|
+
rule). The standalone form exists for debugging a snapshot.
|
|
75
81
|
verify-citations Deterministic citation checker (v2): every cite resolves (B1),
|
|
76
82
|
every contract anchor's code occurrences are cited (B2), policy
|
|
77
83
|
holds, declared absences re-grepped (B3), coverage inventories
|
|
@@ -128,6 +134,11 @@ Commands:
|
|
|
128
134
|
count harness tokens, emit a MEASURED benchmark.json. --dry-run
|
|
129
135
|
exercises the scaffold from fixture counts; --help documents the
|
|
130
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.
|
|
131
142
|
migrate Bring this repo's structured files (plans, config) up to the schema
|
|
132
143
|
the installed CLI ships. Idempotent; review the diff after.
|
|
133
144
|
|
|
@@ -105,6 +105,19 @@ try {
|
|
|
105
105
|
/* not a merge / rev-list unavailable — normal 1-1 path */
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// The BRANCH MANIFEST (harness-arc wave 1, manifest-as-handoff): every brain
|
|
109
|
+
// commit carries a lenient snapshot of the branch's knowledge state, so the
|
|
110
|
+
// reconciler (or any agent) reads "what this branch believes" at any ref.
|
|
111
|
+
// Delegated to the CLI (`rafa manifest` — okf-parsed, never a second parser);
|
|
112
|
+
// best-effort + bounded: a missing/slow CLI must never block a code commit.
|
|
113
|
+
try {
|
|
114
|
+
const localRafa = join(ROOT, "node_modules", ".bin", "rafa");
|
|
115
|
+
const runner = existsSync(localRafa) ? `"${localRafa}"` : "npx -y @rafinery/cli";
|
|
116
|
+
sh(`${runner} manifest`, ROOT, 30000);
|
|
117
|
+
} catch {
|
|
118
|
+
/* snapshot skipped — the reconciler treats a stale/absent branch manifest as null */
|
|
119
|
+
}
|
|
120
|
+
|
|
108
121
|
// The intent record — the commit's end-to-end intent, mechanically joined.
|
|
109
122
|
// Minimal here (sha · subject · files); the P3 capture worker enriches.
|
|
110
123
|
const fullSha = sh("git rev-parse HEAD");
|
|
@@ -129,6 +142,33 @@ try {
|
|
|
129
142
|
`\n`,
|
|
130
143
|
);
|
|
131
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
|
+
|
|
132
172
|
shR("git add -A");
|
|
133
173
|
disposeHydrations(branch);
|
|
134
174
|
shR(
|
|
@@ -300,6 +300,16 @@ try {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
// Benchmark liveness (harness-arc: the proof engine is workflow-woven, never a
|
|
304
|
+
// dev-typed command) — a scanned brain with no MEASURED benchmark is a gap the
|
|
305
|
+
// scan SOP's Prove-it step closes. Absence sensor only; never a nag mid-flow.
|
|
306
|
+
const hasBrain = existsSync(join(rafaDir, "brain", "rules"));
|
|
307
|
+
const measuredBenchmark = readJson(join(rafaDir, "benchmark.json"))?.measured === true;
|
|
308
|
+
if (hasBrain && !measuredBenchmark)
|
|
309
|
+
lines.push(
|
|
310
|
+
`[rafa · benchmark] no measured token-proof for this repo yet — the next /rafa scan runs the Prove-it step (brain-vs-cold, harness-counted) automatically.`,
|
|
311
|
+
);
|
|
312
|
+
|
|
303
313
|
// Suggested next — ONE deterministic recommendation, ranked by consequence:
|
|
304
314
|
// a teammate blocked by a conflict > an unbanked correction > resuming the
|
|
305
315
|
// active plan > staleness repair. Guidance is front-loaded here (and ambient
|
|
@@ -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
|
|
@@ -33,7 +39,10 @@ platform MCP (one read path — the same surface any third-party agent uses).
|
|
|
33
39
|
anytime ("stop pushing"). On "no": journal locally only, push at the end on
|
|
34
40
|
approval. Dev-level insights are NEVER under this consent — each is its own
|
|
35
41
|
offer.
|
|
36
|
-
2. **Per task:** atlas recalls → implements →
|
|
42
|
+
2. **Per task:** atlas recalls → implements → **commits use the
|
|
43
|
+
[rafa-commit](../rafa-commit/SKILL.md) format — `[<task-id>] <type>:
|
|
44
|
+
<subject>` (the id join-key; intent records + the branch manifest lift it
|
|
45
|
+
into per-note provenance)** → prism validates vs `## Done-check` →
|
|
37
46
|
bloom sweeps (push new / close fixed / nudge) → update the child file's `status`
|
|
38
47
|
**and append a dated entry to the child's `## Log`** (body links: markdown,
|
|
39
48
|
per [rafa-okf](../rafa-okf/SKILL.md)) — what was done, what was
|
|
@@ -69,14 +78,28 @@ platform MCP (one read path — the same surface any third-party agent uses).
|
|
|
69
78
|
- **On any other branch:** the org brain is NEVER written from a branch —
|
|
70
79
|
it describes main, and a branch-state scan would poison it for everyone.
|
|
71
80
|
Invalidated/learned knowledge → the branch **working set**: hydrate the
|
|
72
|
-
affected note (`rafa hydrate <rule|playbook> <id>`) and edit
|
|
73
|
-
a new note file under `.rafa/brain/**` — `rafa checkpoint`
|
|
81
|
+
affected note (`rafa hydrate <rule|playbook|improvement> <id>`) and edit
|
|
82
|
+
it, or author a new note file under `.rafa/brain/**` — `rafa checkpoint`
|
|
83
|
+
syncs it. Ledger status edits (bloom's `fixed`) ALWAYS hydrate first. It
|
|
74
84
|
enters the org brain at merge-to-main, through distillation. This is the
|
|
75
85
|
knowledge-propagates-like-code rule, enforced.
|
|
76
86
|
The working-set files ARE the sanctioned branch authoring surface — what is
|
|
77
87
|
never allowed is editing main's brain around the scan/compile/push gates.
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
**Gap close-out:** authored knowledge that answers an in-scope knowledge
|
|
89
|
+
gap (adopted at plan time via `get_knowledge_gaps`) closes the loop —
|
|
90
|
+
`set_gap_status(q, "closed")` at the same beat the note is authored.
|
|
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` +
|
|
95
|
+
`set_active_plan` (clear) + `rafa checkpoint`. **Dual status (single/double
|
|
96
|
+
tick):** `status: done` is the dev ✓ — prism-earned, session-set, for
|
|
97
|
+
leaves AND the epic (the epic's ✓ = every leaf verified done). DELIVERY is
|
|
98
|
+
the separate ✓✓: the platform stamps `merged` per item when the branch
|
|
99
|
+
merges to main (the reconciliation is the receipt; sessions can never
|
|
100
|
+
write it; intermediate merges re-point the plan to the target branch so
|
|
101
|
+
stacked branches converge). The board shows "awaiting merge" between ✓ and
|
|
102
|
+
✓✓. A plan that stops being worth
|
|
80
103
|
finishing closes honestly: `superseded` or `abandoned`, never fake-`done`.
|
|
81
104
|
Plan-done is also a **staleness boundary**: read `rafa dirty --json` — if the
|
|
82
105
|
build's edits dirtied notes this session didn't already refresh, surface the
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rafa-commit
|
|
3
|
+
description: "rafa SOP — generate the commit message with the id join-key: the active task id rides the subject ([task-id] type: subject), making commit → task → plan → brain-delta lineage mechanical. Loaded at commit moments during /rafa build or on request."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# commit — the id join-key (the commit contract)
|
|
7
|
+
|
|
8
|
+
> Status: **active.** The commit message is a JOIN KEY, not prose: the task id
|
|
9
|
+
> in the subject is what lets the platform walk rule ← commit ← task ← plan
|
|
10
|
+
> mechanically (intent records capture the subject per commit; the branch
|
|
11
|
+
> manifest lifts task ids into per-note `provenance.tasks`). Invoked at commit
|
|
12
|
+
> moments in /rafa build, or explicitly.
|
|
13
|
+
|
|
14
|
+
## The format
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
[<task-id>] <type>: <subject>
|
|
18
|
+
|
|
19
|
+
<body — what and why, wrapped>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `<task-id>` — the ACTIVE plan item this commit advances (the in-progress
|
|
23
|
+
leaf from `get_active_plan` / local `active.md`). Exactly one; the deepest
|
|
24
|
+
in-progress leaf wins when nested. Work outside any plan (direct-do) omits
|
|
25
|
+
the bracket entirely — `<type>: <subject>` — never a fake id.
|
|
26
|
+
- `<type>` — conventional: feat | fix | refactor | perf | test | docs | chore.
|
|
27
|
+
- `<subject>` — imperative, ≤ 72 chars including the bracket.
|
|
28
|
+
|
|
29
|
+
## Procedure
|
|
30
|
+
|
|
31
|
+
1. Resolve the active task: `get_active_plan` (platform) or `active.md` +
|
|
32
|
+
child statuses (local). No active plan / no in-progress leaf → no bracket.
|
|
33
|
+
2. Compose from the STAGED DIFF (never from memory of the session): what
|
|
34
|
+
changed, why, in the repo's own voice.
|
|
35
|
+
3. On task completion commits, the body's last paragraph notes the Done-check
|
|
36
|
+
outcome one-line ("Done-check: prism PASS") — the receipt travels with the
|
|
37
|
+
code.
|
|
38
|
+
|
|
39
|
+
## Why this exists (don't skip the bracket)
|
|
40
|
+
|
|
41
|
+
The brain-commit hook captures every subject into `intent/<sha>.md`;
|
|
42
|
+
`rafa manifest` parses `[task-id]` back out into each note's
|
|
43
|
+
`provenance.tasks`. Skip the bracket and the lineage chain (rule ← commit ←
|
|
44
|
+
task ← plan) breaks at its first link — the platform can still join
|
|
45
|
+
plan ↔ merge via the branch, but per-task attribution is lost for that commit.
|
|
@@ -25,7 +25,16 @@ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
|
|
|
25
25
|
knowledge, just unserved.*
|
|
26
26
|
- **bloom pulls** — `list_improvements` in the blast radius; surface the
|
|
27
27
|
top-leverage open items as optional *"while-you're-here"* child tasks
|
|
28
|
-
(leverage-ranked, dismissible, never blocking).
|
|
28
|
+
(leverage-ranked, dismissible, never blocking). **Any improvement the plan
|
|
29
|
+
ADOPTS (as a task or the plan's very subject) is hydrated NOW** —
|
|
30
|
+
`rafa hydrate improvement <id>` — so the canonical ledger file is in the
|
|
31
|
+
branch working set from the start and the eventual `status: fixed` edit lands
|
|
32
|
+
on that same file, never a path-drifted twin authored at build's end.
|
|
33
|
+
**Also pull `get_knowledge_gaps`** (the open backlog — what devs asked that
|
|
34
|
+
the brain couldn't serve): a top-missed gap inside the blast radius is live
|
|
35
|
+
demand — offer it as a while-you're-here task; on adoption call
|
|
36
|
+
`set_gap_status(q, "in-scope")` (and `out-of-scope` with a one-line note is
|
|
37
|
+
an honest dismissal — silence is not).
|
|
29
38
|
- **prism validates the plan itself** — before the approval gate: is every task
|
|
30
39
|
grounded in real brain/code (not hallucinated ground)? does every child carry a
|
|
31
40
|
`## Done-check` (the expected outcome prism will validate execution against)?
|
|
@@ -37,7 +46,14 @@ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
|
|
|
37
46
|
1. **Staleness check** — compare the platform envelope's `brainForSha` against the
|
|
38
47
|
local brain stamp; if the platform is behind, surface "run `rafa push`" (never
|
|
39
48
|
proceed silently on knowledge you know is stale — never block either).
|
|
40
|
-
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
|
|
41
57
|
§7 v2): one epic → tasks → subtasks (three ranks, never deeper). Every item
|
|
42
58
|
carries the glimpse fields — `title` (what) · `description` (why) ·
|
|
43
59
|
`approach` (how, one line) · `assignee` when known · `blocked_by` for
|
|
@@ -54,7 +70,11 @@ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
|
|
|
54
70
|
5. **prism plan-validation** → REJECT/fix loop until clean.
|
|
55
71
|
6. **Approval gate** (owner). Then materialize `plans/<plan>/*.md` per contract §7
|
|
56
72
|
(plan files ride the OKF surface — markdown links in bodies; see [rafa-okf](../rafa-okf/SKILL.md))
|
|
57
|
-
(parent + child-owned files, globally-unique prefixed ids
|
|
73
|
+
(parent + child-owned files, globally-unique prefixed ids; **the EPIC's
|
|
74
|
+
frontmatter stamps `branch:` = the current git branch** — the platform
|
|
75
|
+
joins merge events on this field to stamp the delivery ✓✓ (`merged`) per
|
|
76
|
+
item, and re-points it through intermediate merges)
|
|
77
|
+
+ `active.md` pointer
|
|
58
78
|
→ `rafa compile` (validate the files) → **`push_plan` + `set_active_plan`
|
|
59
79
|
immediately, no second prompt — plan approval IS the push trigger** (the
|
|
60
80
|
dedicated plans channel; plans never ride the brain manifest). The dev just
|
|
@@ -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.
|
|
@@ -253,7 +253,19 @@ pipeline; **`--brain-only`** stops after the brain is validated (step 5 PASS)
|
|
|
253
253
|
6. **Improve** *(skip if `--brain-only`)* — run the improve pass ([rafa-improve](../rafa-improve/SKILL.md)):
|
|
254
254
|
spawn `bloom` → `.rafa/improve/`. It reads the *validated* brain as its index, so it only
|
|
255
255
|
runs after PASS.
|
|
256
|
-
7. **
|
|
256
|
+
7. **Prove it — the measured benchmark** *(skip if `--brain-only`; skip if
|
|
257
|
+
`.rafa/benchmark.json` already carries `measured: true` for this repo)* —
|
|
258
|
+
the proof engine is WORKFLOW-WOVEN, never a dev-typed command (owner
|
|
259
|
+
2026-07-24): the conductor drives `rafa benchmark` itself right after the
|
|
260
|
+
brain PASSes — cut the two worktrees (cold vs brain; the scaffold does
|
|
261
|
+
this), run the task fixture in EACH (the conductor IS the driving loop the
|
|
262
|
+
scaffold documents), collect the harness token counts, then
|
|
263
|
+
`rafa benchmark --counts <file> --push` so the Efficiency page's
|
|
264
|
+
"measured on this repo: N×" widget carries a real row. Announce-and-proceed
|
|
265
|
+
("measuring the brain-vs-cold token proof — say stop to skip"); a decline
|
|
266
|
+
is honored and not re-asked this session. `--dry-run` fixture numbers are
|
|
267
|
+
demo-only and can never land as proof (the handler refuses them).
|
|
268
|
+
8. **Land it** — present the full summary (brain verdict/score + top improvements). **On the
|
|
257
269
|
dev's explicit approval**, land the brain the way EVERY brain change lands — through the
|
|
258
270
|
commit hooks, **never a direct trunk push** (the reconciler is the org brain's only writer).
|
|
259
271
|
The dev MUST be on a feature branch (the post-commit hook no-ops on `main`). Then:
|
|
@@ -263,7 +275,7 @@ pipeline; **`--brain-only`** stops after the brain is validated (step 5 PASS)
|
|
|
263
275
|
`git merge` + push — both work) → detection enqueues and the **reconciler** authors the org
|
|
264
276
|
brain at the merge sha, minting the node (brain ↔ code). Do **not** run `rafa push` (retired:
|
|
265
277
|
it wrote the trunk directly, bypassing the reconciler). Never land without approval.
|
|
266
|
-
|
|
278
|
+
9. **The coach offer (founding scan only)** — if this was the repo's FIRST scan and the dev's
|
|
267
279
|
user brain is empty (`list_dev_insights` → none), offer ONCE: *"the code side is mapped —
|
|
268
280
|
want me to bootstrap YOUR insights from your usage report?"* Accepted = run `## insights`
|
|
269
281
|
(compass; every candidate offered, banked only on yes). The offer rung of the consent
|
package/lib/blueprint.mjs
CHANGED
|
@@ -38,6 +38,8 @@ export const BLUEPRINT = {
|
|
|
38
38
|
".claude/skills/rafa-validate",
|
|
39
39
|
".claude/skills/rafa-plan",
|
|
40
40
|
".claude/skills/rafa-build",
|
|
41
|
+
".claude/skills/rafa-commit",
|
|
42
|
+
".claude/skills/rafa-review",
|
|
41
43
|
".claude/skills/rafa-improve",
|
|
42
44
|
".claude/skills/rafa-distill",
|
|
43
45
|
".claude/skills/rafa-insights",
|
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/checkpoint.mjs
CHANGED
|
@@ -183,5 +183,23 @@ export default async function checkpoint() {
|
|
|
183
183
|
`${conflicts ? "!" : "✓"} checkpoint: ${accepted}/${results.length} synced on ${branch}` +
|
|
184
184
|
(conflicts ? ` · ${conflicts} conflict(s) need YOUR decision (see above)` : ""),
|
|
185
185
|
);
|
|
186
|
+
|
|
187
|
+
// prism-verdict backstop (harness-arc wave 0): gate-result/reflex-outcome are
|
|
188
|
+
// emitted mechanically by the CLI, but prism verdicts are LLM judgments only
|
|
189
|
+
// the session can report. The checkpoint beat — the SOP's own emit moment —
|
|
190
|
+
// checks the store and reminds while it's still empty. Best-effort, one line.
|
|
191
|
+
try {
|
|
192
|
+
const evts = await callTool(ROOT, "get_loop_events", {
|
|
193
|
+
category: "prism-verdict",
|
|
194
|
+
limit: 1,
|
|
195
|
+
});
|
|
196
|
+
if ((evts.events ?? []).length === 0)
|
|
197
|
+
console.log(
|
|
198
|
+
" ℹ no prism-verdict loop events recorded yet — the build SOP emits " +
|
|
199
|
+
"report_loop_event(prism-verdict, PASS|ITERATE, <task id>) at each Done-check ruling.",
|
|
200
|
+
);
|
|
201
|
+
} catch {
|
|
202
|
+
/* unprovisioned/unreachable — the reminder is best-effort */
|
|
203
|
+
}
|
|
186
204
|
if (conflicts) process.exit(2);
|
|
187
205
|
}
|
package/lib/hydrate.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// rafa hydrate — fault knowledge INTO the lazy .rafa/ instance (working-set
|
|
2
2
|
// architecture, ratified 2026-07-10). Two sources, one bookkeeping rule:
|
|
3
3
|
//
|
|
4
|
-
// rafa hydrate rule <id>
|
|
5
|
-
// rafa hydrate playbook <id>
|
|
4
|
+
// rafa hydrate rule <id> one org-brain note → .rafa/brain/rules/<id>.md
|
|
5
|
+
// rafa hydrate playbook <id> one org-brain note → .rafa/brain/playbooks/<id>.md
|
|
6
|
+
// rafa hydrate improvement <id> one ledger item → .rafa/improve/improvements/<id>.md
|
|
7
|
+
// (the CANONICAL ledger path — a status edit lands on the same file the org
|
|
8
|
+
// brain carries; bloom hydrates BEFORE editing, never authors a path twin)
|
|
6
9
|
// Stamped in the sidecar with its main-brain base version (envelope
|
|
7
10
|
// brainForSha) + content hash — an UNEDITED hydration is a disposable
|
|
8
11
|
// cache `rafa checkpoint` will never push; editing it makes it working set.
|
|
@@ -21,6 +24,7 @@ import { dirname, join } from "node:path";
|
|
|
21
24
|
import { callTool } from "./mcp-client.mjs";
|
|
22
25
|
import {
|
|
23
26
|
hashOf,
|
|
27
|
+
materializeImprovement,
|
|
24
28
|
materializeNote,
|
|
25
29
|
readSidecar,
|
|
26
30
|
writeSidecar,
|
|
@@ -76,17 +80,24 @@ export default async function hydrate(args = []) {
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
const [kind, id] = args.filter((a) => !a.startsWith("-"));
|
|
79
|
-
if ((kind !== "rule" && kind !== "playbook") || !id)
|
|
80
|
-
die("usage: rafa hydrate <rule|playbook> <id> · rafa hydrate --working-set");
|
|
83
|
+
if ((kind !== "rule" && kind !== "playbook" && kind !== "improvement") || !id)
|
|
84
|
+
die("usage: rafa hydrate <rule|playbook|improvement> <id> · rafa hydrate --working-set");
|
|
81
85
|
|
|
82
86
|
let payload;
|
|
83
87
|
try {
|
|
84
|
-
payload = await callTool(
|
|
88
|
+
payload = await callTool(
|
|
89
|
+
ROOT,
|
|
90
|
+
kind === "rule" ? "get_rule" : kind === "playbook" ? "get_playbook" : "get_improvement",
|
|
91
|
+
{ id },
|
|
92
|
+
);
|
|
85
93
|
} catch (e) {
|
|
86
94
|
die(e instanceof Error ? e.message : String(e));
|
|
87
95
|
}
|
|
88
96
|
const brainForSha = payload.envelope?.brainForSha ?? null;
|
|
89
|
-
const rel =
|
|
97
|
+
const rel =
|
|
98
|
+
kind === "improvement"
|
|
99
|
+
? materializeImprovement(ROOT, payload, brainForSha)
|
|
100
|
+
: materializeNote(ROOT, kind, payload, brainForSha);
|
|
90
101
|
console.log(
|
|
91
102
|
`✓ hydrated ${kind} "${id}" → .rafa/${rel}` +
|
|
92
103
|
(brainForSha ? ` (base: ${String(brainForSha).slice(0, 7)})` : "") +
|
package/lib/manifest.mjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// rafa manifest — the BRANCH manifest, lenient snapshot mode (harness-arc wave 1,
|
|
2
|
+
// manifest-as-handoff, owner 2026-07-23). The trunk manifest is compile's (strict,
|
|
3
|
+
// gate-fed); every mirrored BRANCH commit carries this snapshot instead so any
|
|
4
|
+
// agent reading the brain repo at a ref knows exactly what that branch's knowledge
|
|
5
|
+
// state is — parseable entries index normally, WIP files land under `drafts`
|
|
6
|
+
// (never fail a dev's commit; strictness stays at the reconcile gate).
|
|
7
|
+
//
|
|
8
|
+
// ZERO-COMMAND (owner 2026-07-24): devs never run this — the post-commit brain
|
|
9
|
+
// hook (brain-commit.mjs) regenerates it on every mirrored commit, exactly like
|
|
10
|
+
// okf emit rides `rafa push`. The standalone form exists for debugging only.
|
|
11
|
+
//
|
|
12
|
+
// rafa manifest write .rafa/manifest.json for the current branch
|
|
13
|
+
//
|
|
14
|
+
// Additive contract-§1 fields (schemaVersion stays 1):
|
|
15
|
+
// mode: "branch" trunk manifests never carry these
|
|
16
|
+
// base: { brainSha } | null the org-brain version most hydrations
|
|
17
|
+
// diverged from (hydration sidecar)
|
|
18
|
+
// drafts: [{ path, error }] unparseable WIP files, listed not fatal
|
|
19
|
+
// per-entry provenance: { baseBrainSha?, commits[] }
|
|
20
|
+
// commits = CODE shas that touched the
|
|
21
|
+
// note (brain-commit trailers, 1-1 law)
|
|
22
|
+
//
|
|
23
|
+
// Parsing goes through @rafinery/okf + the distiller doctrine — never a second
|
|
24
|
+
// frontmatter parser. The manifest describes what IS at a commit; the knowledge
|
|
25
|
+
// node describes what HAPPENED between commits (that split is doctrine).
|
|
26
|
+
|
|
27
|
+
import { execSync } from "node:child_process";
|
|
28
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { join, relative } from "node:path";
|
|
30
|
+
import { parseFrontmatter } from "@rafinery/okf/frontmatter";
|
|
31
|
+
import { citesOf } from "./distiller/doctrine.mjs";
|
|
32
|
+
|
|
33
|
+
const SCHEMA_VERSION = 1;
|
|
34
|
+
const PRIORITIES = new Set(["P0", "P1", "P2", "P3"]);
|
|
35
|
+
const IMP_STATUSES = new Set(["open", "backlog", "fixed", "wontfix"]);
|
|
36
|
+
|
|
37
|
+
const str = (v, fallback = "") => (typeof v === "string" ? v : fallback);
|
|
38
|
+
const strArr = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === "string") : []);
|
|
39
|
+
const idOfPath = (p) => (p.split("/").pop() ?? p).replace(/\.md$/, "");
|
|
40
|
+
|
|
41
|
+
const toCites = (content) =>
|
|
42
|
+
citesOf(content).map((c) => ({
|
|
43
|
+
file: c.file,
|
|
44
|
+
line: c.start === c.end ? String(c.start) : `${c.start}-${c.end}`,
|
|
45
|
+
token: c.token,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
function mdFilesUnder(dir) {
|
|
49
|
+
if (!existsSync(dir)) return [];
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const entry of readdirSync(dir, { recursive: true })) {
|
|
52
|
+
const p = join(dir, String(entry));
|
|
53
|
+
if (p.endsWith(".md") && statSync(p).isFile()) out.push(p);
|
|
54
|
+
}
|
|
55
|
+
return out.sort();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default async function manifest() {
|
|
59
|
+
const ROOT = process.cwd();
|
|
60
|
+
const rafa = join(ROOT, ".rafa");
|
|
61
|
+
if (!existsSync(join(rafa, ".git"))) {
|
|
62
|
+
console.error("✗ no .rafa brain instance here — run `rafa pull` first");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const sh = (cmd, cwd = ROOT) => {
|
|
66
|
+
try {
|
|
67
|
+
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
68
|
+
} catch {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const branch = sh("git rev-parse --abbrev-ref HEAD");
|
|
73
|
+
if (branch === "main" || branch === "master") {
|
|
74
|
+
// The trunk manifest is compile's (strict) — this command is branch-only.
|
|
75
|
+
console.log("• on the trunk — the strict compile gate owns main's manifest; nothing to do");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const codeSha = sh("git rev-parse HEAD");
|
|
79
|
+
|
|
80
|
+
// Hydration sidecar → per-path divergence base + the branch-level base
|
|
81
|
+
// (the most common hydration base across files; null when nothing hydrated).
|
|
82
|
+
let sidecarFiles = {};
|
|
83
|
+
try {
|
|
84
|
+
sidecarFiles = JSON.parse(readFileSync(join(rafa, "hydration.json"), "utf8")).files ?? {};
|
|
85
|
+
} catch {
|
|
86
|
+
/* no sidecar — branch-new everything */
|
|
87
|
+
}
|
|
88
|
+
const baseCounts = new Map();
|
|
89
|
+
for (const rec of Object.values(sidecarFiles))
|
|
90
|
+
if (rec?.baseBrainSha) baseCounts.set(rec.baseBrainSha, (baseCounts.get(rec.baseBrainSha) ?? 0) + 1);
|
|
91
|
+
const baseBrainSha =
|
|
92
|
+
[...baseCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
|
|
93
|
+
|
|
94
|
+
// provenance.commits — the CODE shas that touched a brain path, read off the
|
|
95
|
+
// 1-1 brain commits' `code-commit:` trailers (bounded; oldest dropped).
|
|
96
|
+
const commitsFor = (relPath) =>
|
|
97
|
+
sh(`git log -n 10 --format="%(trailers:key=code-commit,valueonly)" -- "${relPath}"`, rafa)
|
|
98
|
+
.split("\n")
|
|
99
|
+
.map((s) => s.trim())
|
|
100
|
+
.filter((s) => /^[0-9a-f]{7,64}$/i.test(s));
|
|
101
|
+
|
|
102
|
+
// The commit contract's task lift: intent/<shortsha>.md captures each code
|
|
103
|
+
// commit's subject; a `[task-id]` bracket there joins the note to the plan
|
|
104
|
+
// item that produced it (rule ← commit ← task ← plan, mechanical).
|
|
105
|
+
const taskOfCommit = (sha) => {
|
|
106
|
+
try {
|
|
107
|
+
const subject = readFileSync(join(rafa, "intent", `${sha.slice(0, 12)}.md`), "utf8")
|
|
108
|
+
.split("\n")
|
|
109
|
+
.find((l) => l.startsWith("# "));
|
|
110
|
+
return subject?.match(/^# \[([A-Za-z0-9][\w.-]*)\]/)?.[1] ?? null;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const drafts = [];
|
|
117
|
+
const provenanceOf = (relPath) => {
|
|
118
|
+
const commits = commitsFor(relPath);
|
|
119
|
+
const base = sidecarFiles[relPath]?.baseBrainSha;
|
|
120
|
+
const tasks = [...new Set(commits.map(taskOfCommit).filter(Boolean))];
|
|
121
|
+
return {
|
|
122
|
+
...(base ? { baseBrainSha: base } : {}),
|
|
123
|
+
...(commits.length > 0 ? { commits } : {}),
|
|
124
|
+
...(tasks.length > 0 ? { tasks } : {}),
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const noteEntry = (abs) => {
|
|
129
|
+
const relPath = relative(rafa, abs);
|
|
130
|
+
const content = readFileSync(abs, "utf8");
|
|
131
|
+
const parsed = parseFrontmatter(content);
|
|
132
|
+
if (parsed.error || !parsed.data) {
|
|
133
|
+
drafts.push({ path: relPath, error: parsed.error ?? "unparseable frontmatter" });
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const d = parsed.data;
|
|
137
|
+
const id = str(d.id, idOfPath(relPath));
|
|
138
|
+
const prov = provenanceOf(relPath);
|
|
139
|
+
return {
|
|
140
|
+
id,
|
|
141
|
+
kind: relPath.includes("playbooks/") ? "playbook" : "rule",
|
|
142
|
+
type: str(d.type, "note"),
|
|
143
|
+
domain: str(d.domain, "general"),
|
|
144
|
+
title: str(d.title, id),
|
|
145
|
+
summary: str(d.summary),
|
|
146
|
+
links: strArr(d.links),
|
|
147
|
+
cites: toCites(content),
|
|
148
|
+
path: relPath,
|
|
149
|
+
...(Object.keys(prov).length > 0 ? { provenance: prov } : {}),
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const improvementEntry = (abs) => {
|
|
154
|
+
const relPath = relative(rafa, abs);
|
|
155
|
+
const content = readFileSync(abs, "utf8");
|
|
156
|
+
const parsed = parseFrontmatter(content);
|
|
157
|
+
if (parsed.error || !parsed.data) {
|
|
158
|
+
drafts.push({ path: relPath, error: parsed.error ?? "unparseable frontmatter" });
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const d = parsed.data;
|
|
162
|
+
const id = str(d.id, idOfPath(relPath));
|
|
163
|
+
const leverage = typeof d.leverage === "object" && d.leverage !== null ? d.leverage : {};
|
|
164
|
+
const prov = provenanceOf(relPath);
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
priority: PRIORITIES.has(d.priority) ? d.priority : "P2",
|
|
168
|
+
lens: str(d.lens, "general"),
|
|
169
|
+
status: IMP_STATUSES.has(d.status) ? d.status : "open",
|
|
170
|
+
title: str(d.title, id),
|
|
171
|
+
summary: str(d.summary),
|
|
172
|
+
fix: str(d.fix),
|
|
173
|
+
leverage: { impact: str(leverage.impact), effort: str(leverage.effort) },
|
|
174
|
+
blastRadius: strArr(d.blastRadius),
|
|
175
|
+
cites: toCites(content),
|
|
176
|
+
path: relPath,
|
|
177
|
+
...(Object.keys(prov).length > 0 ? { provenance: prov } : {}),
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const notes = [
|
|
182
|
+
...mdFilesUnder(join(rafa, "brain", "rules")),
|
|
183
|
+
...mdFilesUnder(join(rafa, "brain", "playbooks")),
|
|
184
|
+
]
|
|
185
|
+
.map(noteEntry)
|
|
186
|
+
.filter(Boolean);
|
|
187
|
+
const improvements = mdFilesUnder(join(rafa, "improve", "improvements"))
|
|
188
|
+
.map(improvementEntry)
|
|
189
|
+
.filter(Boolean);
|
|
190
|
+
|
|
191
|
+
const byPath = (a, b) => a.path.localeCompare(b.path);
|
|
192
|
+
const out = {
|
|
193
|
+
schemaVersion: SCHEMA_VERSION,
|
|
194
|
+
mode: "branch",
|
|
195
|
+
codeSha,
|
|
196
|
+
branch,
|
|
197
|
+
base: baseBrainSha ? { brainSha: baseBrainSha } : null,
|
|
198
|
+
notes: notes.sort(byPath),
|
|
199
|
+
improvements: improvements.sort(byPath),
|
|
200
|
+
...(drafts.length > 0 ? { drafts: drafts.sort(byPath) } : {}),
|
|
201
|
+
};
|
|
202
|
+
writeFileSync(join(rafa, "manifest.json"), JSON.stringify(out, null, 2) + "\n");
|
|
203
|
+
console.log(
|
|
204
|
+
`✓ branch manifest: ${notes.length} note(s) · ${improvements.length} improvement(s)` +
|
|
205
|
+
(drafts.length ? ` · ${drafts.length} draft(s)` : "") +
|
|
206
|
+
(baseBrainSha ? ` · base ${baseBrainSha.slice(0, 7)}` : " · no hydration base"),
|
|
207
|
+
);
|
|
208
|
+
}
|
package/lib/push.mjs
CHANGED
|
@@ -122,30 +122,55 @@ export default async function push(args = []) {
|
|
|
122
122
|
// THIS push — a stale/hand-stamped citation-check.json can never ship
|
|
123
123
|
// (mechanized 2026-07-13 from prism's live-run recommendation: the stale-record
|
|
124
124
|
// class dies at the transport layer, not in review).
|
|
125
|
+
// Deterministic gate-result loop events (harness-arc wave 0): the push
|
|
126
|
+
// boundary is where the gates ACTUALLY run, so the event rides the real exit
|
|
127
|
+
// code — never the SOP's memory of it. Best-effort: telemetry never blocks
|
|
128
|
+
// a push; an unprovisioned repo just skips.
|
|
129
|
+
const emitGate = async (subject, ok) => {
|
|
130
|
+
try {
|
|
131
|
+
const { callTool } = await import("./mcp-client.mjs");
|
|
132
|
+
await callTool(ROOT, "report_loop_event", {
|
|
133
|
+
category: "gate-result",
|
|
134
|
+
outcome: ok ? "exit0" : "failed",
|
|
135
|
+
subject,
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
/* platform unreachable / unprovisioned — the local exit code is still the truth */
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
125
142
|
console.log("• rafa verify-citations — re-grounding every cite/absence/inventory …");
|
|
126
|
-
if (runVerifyCitations([]) !== 0)
|
|
143
|
+
if (runVerifyCitations([]) !== 0) {
|
|
144
|
+
await emitGate("verify-citations", false);
|
|
127
145
|
die(
|
|
128
146
|
"citations failed the checker (see report above). Fix the notes against the " +
|
|
129
147
|
"code (or have atlas repair them), then re-push.",
|
|
130
148
|
);
|
|
149
|
+
}
|
|
131
150
|
if (
|
|
132
151
|
existsSync(join(ROOT, ".rafa", "improve", "improvements")) &&
|
|
133
152
|
runVerifyCitations(["--root=.rafa/improve", "--dirs=improvements"]) !== 0
|
|
134
|
-
)
|
|
153
|
+
) {
|
|
154
|
+
await emitGate("verify-citations", false);
|
|
135
155
|
die(
|
|
136
156
|
"improvement citations failed the checker (see report above). Fix the ledger " +
|
|
137
157
|
"files against the code (or have bloom correct them), then re-push.",
|
|
138
158
|
);
|
|
159
|
+
}
|
|
160
|
+
await emitGate("verify-citations", true);
|
|
139
161
|
|
|
140
162
|
// ── contract gate (in-process) ──
|
|
141
163
|
// Compile + validate the brain. A schema-invalid brain NEVER leaves the machine —
|
|
142
164
|
// the platform ingests JSON, so what we push must already conform to the contract.
|
|
143
165
|
console.log("• rafa compile — validating against the contract …");
|
|
144
|
-
if (runCompile([`--repo=${repoFull}`]) !== 0)
|
|
166
|
+
if (runCompile([`--repo=${repoFull}`]) !== 0) {
|
|
167
|
+
await emitGate("compile", false);
|
|
145
168
|
die(
|
|
146
169
|
"brain failed the contract (see errors above). Fix the files, or have the " +
|
|
147
170
|
"authoring agent (atlas/bloom/prism) correct them, then re-push.",
|
|
148
171
|
);
|
|
172
|
+
}
|
|
173
|
+
await emitGate("compile", true);
|
|
149
174
|
|
|
150
175
|
inRafa("git add -A");
|
|
151
176
|
// Descriptive commit (owner catch 2026-07-17: the history was a wall of
|
package/lib/reflex.mjs
CHANGED
|
@@ -56,6 +56,21 @@ export default async function reflex(args = []) {
|
|
|
56
56
|
JSON.stringify({ id, done: true, verdict, at: new Date().toISOString() }) + "\n",
|
|
57
57
|
);
|
|
58
58
|
console.log(`✓ correction ${id} consumed (${verdict})`);
|
|
59
|
+
// Deterministic reflex-outcome loop event (harness-arc wave 0): the consume
|
|
60
|
+
// IS the outcome moment, so the event rides it mechanically — shape only
|
|
61
|
+
// (the correction id, never its text). Best-effort, never blocks.
|
|
62
|
+
try {
|
|
63
|
+
const { callTool } = await import("./mcp-client.mjs");
|
|
64
|
+
const outcome =
|
|
65
|
+
verdict === "banked" ? "durable" : verdict === "refuted" ? "refuted" : "session-only";
|
|
66
|
+
await callTool(process.cwd(), "report_loop_event", {
|
|
67
|
+
category: "reflex-outcome",
|
|
68
|
+
outcome,
|
|
69
|
+
subject: id,
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
/* unprovisioned repo / platform unreachable — the queue marker is the local truth */
|
|
73
|
+
}
|
|
59
74
|
return;
|
|
60
75
|
}
|
|
61
76
|
|
package/lib/releases.mjs
CHANGED
|
@@ -264,6 +264,34 @@ 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
|
+
},
|
|
281
|
+
{
|
|
282
|
+
version: "0.8.13",
|
|
283
|
+
contract: 1,
|
|
284
|
+
plans: 2,
|
|
285
|
+
requires: "update",
|
|
286
|
+
summary:
|
|
287
|
+
"LEDGER HYDRATION: `rafa hydrate improvement <id>` faults a ledger item into " +
|
|
288
|
+
".rafa/improve/improvements/<id>.md — the CANONICAL path — so bloom edits the " +
|
|
289
|
+
"same file the org brain carries (never a path twin). SOPs updated: an " +
|
|
290
|
+
"improvement the plan ADOPTS is hydrated AT PLAN TIME (bloom engages from the " +
|
|
291
|
+
"start, not only at build end); ledger status edits always hydrate first. " +
|
|
292
|
+
"Platform fixes ride server-side: set_active_plan now resolves v2 epics " +
|
|
293
|
+
"(was v1 parent-only — the not_found bug), get_improvement serves summary.",
|
|
294
|
+
},
|
|
267
295
|
];
|
|
268
296
|
|
|
269
297
|
// The release this CLI build ships (last entry).
|
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/lib/working-set.mjs
CHANGED
|
@@ -139,3 +139,46 @@ export function renderNote(d) {
|
|
|
139
139
|
];
|
|
140
140
|
return lines.join("\n") + (d.body ?? "") + "\n";
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
// The improvement-ledger twin of materializeNote — faults ONE improvement into
|
|
144
|
+
// `.rafa/improve/improvements/<id>.md` (the CANONICAL ledger path, so a status
|
|
145
|
+
// edit lands on the same file the org brain carries — never a path-drifted twin).
|
|
146
|
+
// Same sidecar rule: unedited hydration = disposable cache.
|
|
147
|
+
export function materializeImprovement(cwd, data, brainForSha) {
|
|
148
|
+
const rel = `improve/improvements/${data.id}.md`;
|
|
149
|
+
const abs = join(cwd, ".rafa", rel);
|
|
150
|
+
const content = renderImprovement(data);
|
|
151
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
152
|
+
writeFileSync(abs, content);
|
|
153
|
+
const sidecar = readSidecar(cwd);
|
|
154
|
+
sidecar.files[rel] = {
|
|
155
|
+
...(brainForSha ? { baseBrainSha: brainForSha } : {}),
|
|
156
|
+
hash: hashOf(content),
|
|
157
|
+
};
|
|
158
|
+
writeSidecar(cwd, sidecar);
|
|
159
|
+
return rel;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Deterministic ledger-file writer for hydrated improvements — mirrors the shape
|
|
163
|
+
// bloom authors (see any improve/improvements/*.md): priority · lens · status ·
|
|
164
|
+
// fix · leverage · blast_radius · cites, then the prose body.
|
|
165
|
+
export function renderImprovement(d) {
|
|
166
|
+
const lines = [
|
|
167
|
+
"---",
|
|
168
|
+
"schemaVersion: 1",
|
|
169
|
+
`id: ${d.id}`,
|
|
170
|
+
`priority: ${d.priority}`,
|
|
171
|
+
...(d.lens ? [`lens: ${d.lens}`] : []),
|
|
172
|
+
`status: ${d.status}`,
|
|
173
|
+
`title: ${q(d.title)}`,
|
|
174
|
+
...(d.summary ? [`summary: ${q(d.summary)}`] : []),
|
|
175
|
+
...(d.fix ? [`fix: ${q(d.fix)}`] : []),
|
|
176
|
+
...(d.leverage ? [`leverage: { impact: ${d.leverage.impact}, effort: ${d.leverage.effort} }`] : []),
|
|
177
|
+
`blast_radius: [${(d.blastRadius ?? []).join(", ")}]`,
|
|
178
|
+
"cites:",
|
|
179
|
+
...(d.cites ?? []).map((c) => ` - ${c.file}:${c.line} :: ${c.token}`),
|
|
180
|
+
"---",
|
|
181
|
+
"",
|
|
182
|
+
];
|
|
183
|
+
return lines.join("\n") + (d.body ?? "") + "\n";
|
|
184
|
+
}
|
package/package.json
CHANGED