@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 +5 -0
- package/blueprint/.claude/rafa/hooks/brain-commit.mjs +13 -0
- package/blueprint/.claude/rafa/hooks/session-start.mjs +10 -0
- package/blueprint/.claude/skills/rafa-build/SKILL.md +18 -4
- package/blueprint/.claude/skills/rafa-commit/SKILL.md +45 -0
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +15 -2
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +14 -2
- package/lib/blueprint.mjs +1 -0
- 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 +14 -0
- package/lib/working-set.mjs +43 -0
- package/package.json +1 -1
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 →
|
|
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
|
|
73
|
-
a new note file under `.rafa/brain/**` — `rafa checkpoint`
|
|
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`.
|
|
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
|
|
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. **
|
|
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
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,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).
|
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