@rafinery/cli 0.8.12 → 0.8.13

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
@@ -53,6 +53,7 @@ const COMMANDS = [
53
53
  "ci-setup",
54
54
  "leverage",
55
55
  "benchmark",
56
+ "manifest",
56
57
  "migrate",
57
58
  ];
58
59
  const [cmd, ...rest] = process.argv.slice(2);
@@ -72,6 +73,10 @@ Commands:
72
73
  --keep keep all changed owned files without asking
73
74
  (default: ask per file; in a non-TTY it keeps them)
74
75
  compile Validate the brain against the contract → .rafa/manifest.json.
76
+ manifest The BRANCH manifest (lenient snapshot — WIP files land under
77
+ drafts, never fail). You never need to run this: the post-commit
78
+ brain hook regenerates it on every mirrored commit (zero-command
79
+ rule). The standalone form exists for debugging a snapshot.
75
80
  verify-citations Deterministic citation checker (v2): every cite resolves (B1),
76
81
  every contract anchor's code occurrences are cited (B2), policy
77
82
  holds, declared absences re-grepped (B3), coverage inventories
@@ -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");
@@ -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
@@ -33,7 +33,10 @@ platform MCP (one read path — the same surface any third-party agent uses).
33
33
  anytime ("stop pushing"). On "no": journal locally only, push at the end on
34
34
  approval. Dev-level insights are NEVER under this consent — each is its own
35
35
  offer.
36
- 2. **Per task:** atlas recalls → implements → prism validates vs `## Done-check` →
36
+ 2. **Per task:** atlas recalls → implements → **commits use the
37
+ [rafa-commit](../rafa-commit/SKILL.md) format — `[<task-id>] <type>:
38
+ <subject>` (the id join-key; intent records + the branch manifest lift it
39
+ into per-note provenance)** → prism validates vs `## Done-check` →
37
40
  bloom sweeps (push new / close fixed / nudge) → update the child file's `status`
38
41
  **and append a dated entry to the child's `## Log`** (body links: markdown,
39
42
  per [rafa-okf](../rafa-okf/SKILL.md)) — what was done, what was
@@ -69,14 +72,25 @@ platform MCP (one read path — the same surface any third-party agent uses).
69
72
  - **On any other branch:** the org brain is NEVER written from a branch —
70
73
  it describes main, and a branch-state scan would poison it for everyone.
71
74
  Invalidated/learned knowledge → the branch **working set**: hydrate the
72
- affected note (`rafa hydrate <rule|playbook> <id>`) and edit it, or author
73
- a new note file under `.rafa/brain/**` — `rafa checkpoint` syncs it. It
75
+ affected note (`rafa hydrate <rule|playbook|improvement> <id>`) and edit
76
+ it, or author a new note file under `.rafa/brain/**` — `rafa checkpoint`
77
+ syncs it. Ledger status edits (bloom's `fixed`) ALWAYS hydrate first. It
74
78
  enters the org brain at merge-to-main, through distillation. This is the
75
79
  knowledge-propagates-like-code rule, enforced.
76
80
  The working-set files ARE the sanctioned branch authoring surface — what is
77
81
  never allowed is editing main's brain around the scan/compile/push gates.
82
+ **Gap close-out:** authored knowledge that answers an in-scope knowledge
83
+ gap (adopted at plan time via `get_knowledge_gaps`) closes the loop —
84
+ `set_gap_status(q, "closed")` at the same beat the note is authored.
78
85
  4. **Verify** (prism-style) before declaring the plan done; final `push_plan` +
79
- `set_active_plan` (clear) + `rafa checkpoint`. A plan that stops being worth
86
+ `set_active_plan` (clear) + `rafa checkpoint`. **Dual status (single/double
87
+ tick):** `status: done` is the dev ✓ — prism-earned, session-set, for
88
+ leaves AND the epic (the epic's ✓ = every leaf verified done). DELIVERY is
89
+ the separate ✓✓: the platform stamps `merged` per item when the branch
90
+ merges to main (the reconciliation is the receipt; sessions can never
91
+ write it; intermediate merges re-point the plan to the target branch so
92
+ stacked branches converge). The board shows "awaiting merge" between ✓ and
93
+ ✓✓. A plan that stops being worth
80
94
  finishing closes honestly: `superseded` or `abandoned`, never fake-`done`.
81
95
  Plan-done is also a **staleness boundary**: read `rafa dirty --json` — if the
82
96
  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)?
@@ -54,7 +63,11 @@ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
54
63
  5. **prism plan-validation** → REJECT/fix loop until clean.
55
64
  6. **Approval gate** (owner). Then materialize `plans/<plan>/*.md` per contract §7
56
65
  (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) + `active.md` pointer
66
+ (parent + child-owned files, globally-unique prefixed ids; **the EPIC's
67
+ frontmatter stamps `branch:` = the current git branch** — the platform
68
+ joins merge events on this field to stamp the delivery ✓✓ (`merged`) per
69
+ item, and re-points it through intermediate merges)
70
+ + `active.md` pointer
58
71
  → `rafa compile` (validate the files) → **`push_plan` + `set_active_plan`
59
72
  immediately, no second prompt — plan approval IS the push trigger** (the
60
73
  dedicated plans channel; plans never ride the brain manifest). The dev just
@@ -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. **Land it**present the full summary (brain verdict/score + top improvements). **On the
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
- 8. **The coach offer (founding scan only)** — if this was the repo's FIRST scan and the dev's
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,7 @@ 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",
41
42
  ".claude/skills/rafa-improve",
42
43
  ".claude/skills/rafa-distill",
43
44
  ".claude/skills/rafa-insights",
@@ -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> one org-brain note → .rafa/brain/rules/<id>.md
5
- // rafa hydrate playbook <id> one org-brain note → .rafa/brain/playbooks/<id>.md
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(ROOT, kind === "rule" ? "get_rule" : "get_playbook", { id });
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 = materializeNote(ROOT, kind, payload, brainForSha);
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)})` : "") +
@@ -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,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.13",
269
+ contract: 1,
270
+ plans: 2,
271
+ requires: "update",
272
+ summary:
273
+ "LEDGER HYDRATION: `rafa hydrate improvement <id>` faults a ledger item into " +
274
+ ".rafa/improve/improvements/<id>.md — the CANONICAL path — so bloom edits the " +
275
+ "same file the org brain carries (never a path twin). SOPs updated: an " +
276
+ "improvement the plan ADOPTS is hydrated AT PLAN TIME (bloom engages from the " +
277
+ "start, not only at build end); ledger status edits always hydrate first. " +
278
+ "Platform fixes ride server-side: set_active_plan now resolves v2 epics " +
279
+ "(was v1 parent-only — the not_found bug), get_improvement serves summary.",
280
+ },
267
281
  ];
268
282
 
269
283
  // The release this CLI build ships (last entry).
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafinery/cli",
3
- "version": "0.8.12",
3
+ "version": "0.8.13",
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": {