@rafinery/cli 0.8.1 → 0.8.3

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/bin/rafa-distiller.mjs +13 -0
  3. package/bin/rafa.mjs +9 -8
  4. package/blueprint/.claude/agents/atlas.md +5 -1
  5. package/blueprint/.claude/agents/bloom.md +5 -1
  6. package/blueprint/.claude/agents/compass.md +5 -1
  7. package/blueprint/.claude/agents/prism.md +5 -1
  8. package/blueprint/.claude/agents/sage.md +5 -1
  9. package/blueprint/.claude/commands/rafa.md +20 -2
  10. package/blueprint/.claude/rafa/contract.md +31 -3
  11. package/blueprint/.claude/rafa/hooks/brain-commit.mjs +81 -0
  12. package/blueprint/.claude/rafa/hooks/brain-map.mjs +64 -0
  13. package/blueprint/.claude/rafa/hooks/brain-switch.mjs +48 -0
  14. package/blueprint/.claude/rafa/hooks/post-checkout +10 -0
  15. package/blueprint/.claude/rafa/hooks/post-commit +11 -0
  16. package/blueprint/.claude/rafa/hooks/post-rewrite +10 -0
  17. package/blueprint/.claude/rafa/hooks/pre-push +22 -0
  18. package/blueprint/.claude/rafa/hooks/session-start.mjs +93 -0
  19. package/blueprint/.claude/skills/rafa-improve/SKILL.md +5 -0
  20. package/blueprint/.claude/skills/rafa-scan/SKILL.md +1 -1
  21. package/blueprint/.claude/skills/rafa-validate/SKILL.md +6 -0
  22. package/lib/checkpoint.mjs +42 -0
  23. package/lib/ci-setup.mjs +54 -3
  24. package/lib/dirty.mjs +37 -0
  25. package/lib/distill.mjs +313 -37
  26. package/lib/distiller/doctrine.mjs +379 -0
  27. package/lib/distiller/state-plane.mjs +159 -0
  28. package/lib/distiller.mjs +410 -0
  29. package/lib/doctor.mjs +110 -0
  30. package/lib/gate/compile.mjs +31 -0
  31. package/lib/gate/verify-citations.mjs +9 -2
  32. package/lib/githook.mjs +75 -33
  33. package/lib/init.mjs +22 -23
  34. package/lib/mcp-client.mjs +22 -5
  35. package/lib/pull.mjs +2 -2
  36. package/lib/push.mjs +67 -2
  37. package/lib/releases.mjs +13 -0
  38. package/lib/sensor-health.mjs +91 -0
  39. package/lib/status.mjs +47 -1
  40. package/lib/update.mjs +2 -2
  41. package/lib/working-set.mjs +19 -1
  42. package/package.json +12 -10
  43. package/LICENSE +0 -21
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ The machine-readable version of this lives in [`lib/releases.mjs`](lib/releases.
4
4
  CLI reads it to tell a client, on `rafa update`, exactly what an upgrade requires (nothing,
5
5
  a re-scan, or a `rafa migrate`).
6
6
 
7
+ ## 0.8.2 — MCP scope: derived from the key, never guessed
8
+
9
+ First real client MCP call surfaced it: agents GUESSED the `repo` arg (folder name)
10
+ because nothing told them where the id lives. Server-side, `repo` is now optional on
11
+ every tool — the per-repo key IS the scope and the platform derives it (explicit
12
+ mismatch still fails loudly, teaching both exits). Fleet-side, every agent card +
13
+ the conductor carry the convention: omit `repo`; where a value is explicitly needed
14
+ it is the committed `rafa.json → repoId`, never a guess. Adopt with `rafa update`.
15
+
7
16
  ## 0.8.1 — honest empty: absence is never conformance
8
17
 
9
18
  `rafa okf check` over a root with zero `.md` files used to print a green "conforms" —
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ // rafa-distiller — the distiller loop's binary (reconciliation orchestrator, spec
3
+ // §2.6). boot.sh's DISTILLER_ENTRY seam: the sandbox image's boot.sh wins the claim
4
+ // (claim-on-start, exports RAFA_ATTEMPT) and `exec rafa-distiller`. From here the
5
+ // metered loop owns the run: heartbeat + arbitrate against merged main + author
6
+ // survivors through the gates + push (never force) + reconcile_report.
7
+ //
8
+ // Thin entry only — the loop lives in ../lib/distiller.mjs so it stays importable +
9
+ // unit-testable (runDistiller) without a sandbox.
10
+
11
+ import distiller from "../lib/distiller.mjs";
12
+
13
+ await distiller(process.argv.slice(2));
package/bin/rafa.mjs CHANGED
@@ -12,6 +12,8 @@
12
12
  // rafa checkpoint sync this branch's working set (CAS; conflicts → .theirs.md)
13
13
  // rafa hydrate fault knowledge into the lazy .rafa/
14
14
  // rafa dirty [--json] staleness queue → citing notes; --consume after a refresh
15
+ // rafa doctor prove the capture machinery end to end (sensors ·
16
+ // scripts · heartbeat round-trip); runs inside init too
15
17
  // rafa fold branch→parent mechanical fold of the working set (no LLM)
16
18
  // rafa distill --headless CI merge-to-main distillation (org's own LLM key)
17
19
  // rafa ci-setup write the reconcile workflow (fold + distill on PR merge)
@@ -45,8 +47,10 @@ const COMMANDS = [
45
47
  "dirty",
46
48
  "reflex",
47
49
  "status",
50
+ "doctor",
48
51
  "fold",
49
52
  "distill",
53
+ "distiller",
50
54
  "ci-setup",
51
55
  "leverage",
52
56
  "benchmark",
@@ -144,18 +148,15 @@ End to end — from zero to a queryable, working brain:
144
148
  validates, bloom writes the improvement ledger. Then rafa push
145
149
  → webhook → the platform ingests it (the brain repo only).
146
150
  4. SEE IT Open the repo on the platform: Overview (health), Brain (files),
147
- Improvements (kanban), Plans, Activity (branch changes). "Agent
148
- access" mints keys for more MCP clients (Slack, CI, incident.io).
151
+ Improvements (kanban), Plans, Reconciliations (merge runs).
149
152
  5. WORK /rafa plan "<intent>" — prism-validated, brain-grounded; approval
150
153
  pushes it to the platform (the plans channel). /rafa build —
151
154
  execute it; statuses/journals + the branch WORKING SET sync at
152
155
  checkpoints, so any teammate or session resumes where you stopped.
153
- 6. RECONCILE rafa ci-setup (once)your CI folds branch→branch merges
154
- mechanically and DISTILLS merge-to-main into the org brain with
155
- YOUR OWN LLM key. No CI? The next session offers the same.
156
- 7. QUERY Any MCP client with a key can read the brain at <platform>/api/mcp:
157
- get_brain_status · search_knowledge · get_rule/playbook/improvement ·
158
- list_plans · get_plan · get_active_plan. Read-only, cited, per-repo.
156
+ 6. MERGE Just merge PRsthe platform detects the merge and distills the
157
+ branch's knowledge into the org brain automatically (watch the
158
+ Reconciliations tab). Teams that want the compute in their own
159
+ CI instead: rafa ci-setup (optional, org-CI adapter).
159
160
 
160
161
  Then, inside Claude Code: /rafa scan · /rafa improve · /rafa plan · /rafa build
161
162
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: atlas
3
- version: 3.8.0
3
+ version: 3.8.1
4
4
  model: opus # authoring is correctness-critical — a hallucinated note poisons the brain; best model, never cheap
5
5
  groundTruth: code-at-sha
6
6
  description: >-
@@ -23,6 +23,10 @@ duties:
23
23
 
24
24
  # atlas — senior design engineer
25
25
 
26
+ **MCP scope — every `mcp__rafinery` call:** OMIT `repo`; your key IS the repo
27
+ scope and the server derives it. Where a value is explicitly needed, it is the
28
+ committed `rafa.json → repoId` — NEVER a folder name or repo-name guess.
29
+
26
30
  You are **atlas**, a senior design engineer for the rafa platform. A peer who
27
31
  reasons about systems, not a code-completion assistant.
28
32
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bloom
3
- version: 0.8.0
3
+ version: 0.8.1
4
4
  model: opus # a false flag mutes the whole ledger — best model, never cheap
5
5
  groundTruth: code-trend
6
6
  description: >-
@@ -22,6 +22,10 @@ duties:
22
22
 
23
23
  # bloom — continuous improvement
24
24
 
25
+ **MCP scope — every `mcp__rafinery` call:** OMIT `repo`; your key IS the repo
26
+ scope and the server derives it. Where a value is explicitly needed, it is the
27
+ committed `rafa.json → repoId` — NEVER a folder name or repo-name guess.
28
+
25
29
  You are **bloom**, rafa's improvement engine — a patient staff engineer who raises the bar
26
30
  a little every interaction, relentless but gentle. atlas **knows** the codebase; **prism**
27
31
  doubts it; **you raise its floor.**
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: compass
3
- version: 0.4.0
3
+ version: 0.4.1
4
4
  model: opus # a false note about a PERSON is worse than one about code — best model, never cheap
5
5
  groundTruth: sessions-over-time
6
6
  description: >-
@@ -23,6 +23,10 @@ duties:
23
23
 
24
24
  # compass — the dev's coach
25
25
 
26
+ **MCP scope — every `mcp__rafinery` call:** OMIT `repo`; your key IS the repo
27
+ scope and the server derives it. Where a value is explicitly needed, it is the
28
+ committed `rafa.json → repoId` — NEVER a folder name or repo-name guess.
29
+
26
30
  You are **compass**, the fourth agent: where atlas knows the code, prism doubts
27
31
  the claims, and bloom raises the floor, you know — and refine — **how this
28
32
  developer works**. Your store is the user brain: account-scoped, cross-repo,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: prism
3
- version: 0.7.0
3
+ version: 0.7.1
4
4
  model: opus # the trust anchor — a hallucinated verdict/finding is the worst failure; best model, never cheap
5
5
  groundTruth: code-vs-claim
6
6
  description: >-
@@ -22,6 +22,10 @@ duties:
22
22
 
23
23
  # prism — the validator
24
24
 
25
+ **MCP scope — every `mcp__rafinery` call:** OMIT `repo`; your key IS the repo
26
+ scope and the server derives it. Where a value is explicitly needed, it is the
27
+ committed `rafa.json → repoId` — NEVER a folder name or repo-name guess.
28
+
25
29
  You are **prism**, an independent QA engineer and the trust anchor of every rafa
26
30
  gate. Adversarial by mandate: your job is to find what's wrong, not to bless what's
27
31
  there. You review the **artifact + the ground truth, never the producer's claims**;
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sage
3
- version: 0.2.0
3
+ version: 0.2.1
4
4
  model: opus # a wrong learning re-shapes an agent for every repo — best model, never cheap
5
5
  groundTruth: sessions-over-time
6
6
  description: >-
@@ -24,6 +24,10 @@ duties:
24
24
 
25
25
  # sage — the silent L5 observer
26
26
 
27
+ **MCP scope — every `mcp__rafinery` call:** OMIT `repo`; your key IS the repo
28
+ scope and the server derives it. Where a value is explicitly needed, it is the
29
+ committed `rafa.json → repoId` — NEVER a folder name or repo-name guess.
30
+
27
31
  You are **sage**, rafa's fifth agent and its **self-improvement loop (L5: system → itself)**.
28
32
  Where **atlas** knows the code, **prism** doubts the claims, **bloom** raises the code floor,
29
33
  and **compass** coaches the dev — **you raise the floor of the agents themselves.** Your subject
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 2.0.0
2
+ version: 2.0.2
3
3
  description: rafa — the repo's engineering SOP. Use whenever the dev wants to plan a feature or change, build/execute against the active plan, improve code health, or ground work in the repo's brain — intent counts, the dev does NOT need to type /rafa. Also explicit: /rafa <init|scan|improve|plan|build|push|leverage|migrate|update|help> [--brain-only]. Admin verbs (init/scan/push/migrate/update) run ONLY when explicitly invoked.
4
4
  ---
5
5
 
@@ -123,6 +123,13 @@ Dismissible as a unit; offer fatigue trains reflexive yes, which corrodes every
123
123
  rung above it. Open gaps are one more line in this ONE digest — a pullable source, never
124
124
  a serial prompt and never mid-flow.
125
125
 
126
+ **Exception — a `[rafa · doctor]` digest line is NOT dismissible-later.** It means the
127
+ capture machinery itself is broken (unwired sensor, missing/foreign git hook, a recent
128
+ swallowed failure): run `npx -y @rafinery/cli doctor` FIRST and present its named fixes
129
+ to the dev as concrete next steps before knowledge work — every task done on dead
130
+ sensors is knowledge silently lost. This fires on new sessions AND resumes (the
131
+ SessionStart hook covers both).
132
+
126
133
  **The M5 sensors feed you — you never poll.** Four deterministic instruments run outside
127
134
  the model (blueprint hooks + the git boundary): the SessionStart digest injects
128
135
  staleness/conflicts/corrections/active-plan at session start · every Edit/Write is
@@ -199,6 +206,17 @@ Two destinations, one rule — route by what the observation is ABOUT:
199
206
  > **Every verb that writes `.md` is under the OKF surface** (contract §11): files
200
207
  > self-describe, body links are bundle-relative markdown, `rafa push` materializes
201
208
  > the rest — protocol + the two declared exceptions live in [rafa-okf](../skills/rafa-okf/SKILL.md).
209
+ >
210
+ > **MCP scope — every `mcp__rafinery` call:** omit `repo` (the key IS the scope;
211
+ > the server derives it); where explicitly needed it is `rafa.json → repoId`,
212
+ > never a folder-name guess. Mid-session 401 after a key rotation = this session
213
+ > holds the OLD connect-time key — have the dev reconnect the rafinery MCP (or
214
+ > restart); CLI tools reload credentials per call and keep working.
215
+ >
216
+ > **Fixed-in-passing is REPORTED, always:** any ledger improvement fixed during
217
+ > ANY work — inside or outside a /rafa verb — gets `report_improvement_status`
218
+ > at the fix boundary. Hand-edits never flip ledger status; an unreported fix
219
+ > leaves bloom's ledger lying until the next full pass.
202
220
 
203
221
  Each work/admin verb dispatches to its SOP. Read the skill only when the verb runs; it is
204
222
  self-contained and ADR-shaped. **You orchestrate + push on approval; the subagent follows
@@ -216,7 +234,7 @@ explicitly typed; an ACCEPTED boundary offer counts as the explicit invocation.
216
234
  | `insights` | spawn compass → bootstrap/refresh the dev's private user brain from their native `/insights` report + recent work; every candidate OFFERED, banked only on yes (`put_dev_insight`). Capture during normal work is §capture's job, not this command. | [rafa-insights](../skills/rafa-insights/SKILL.md) |
217
235
  | `leverage` | reason over the committed toolbox (`.claude/settings.json`, `.mcp.json`, `skills/`, `commands/`, the stack) — what's missing/misconfigured/unused; on approval apply the fix EXACTLY (merge permissions, wire an MCP, scaffold a skill). Merge, never clobber; show the diff. The CLI reports, you fix. | [rafa-leverage](../skills/rafa-leverage/SKILL.md) |
218
236
  | `sage` | explicit OVERRIDE of the implicit observer pass (below) | [rafa-sage](../skills/rafa-sage/SKILL.md) |
219
- | `push` | `npx @rafinery/cli push` — commit `.rafa/` and push to the brain remote (the dev's own git auth), stamped `brain-for: <code sha>`. After a scan, or to re-sync a changed brain. Never without approval. | — |
237
+ | `push` | `npx @rafinery/cli push --verb=<scan\|improve\|build\|update>` — commit `.rafa/` and push to the brain remote (the dev's own git auth), stamped `brain-for: <code sha>`. Pass the verb of the work that produced this push (P4 descriptive commits — the brain history should say `brain(scan): …`, never a wall of `update`). After a scan, or to re-sync a changed brain. Never without approval. | — |
220
238
  | `migrate` · `update` | brain-side schema migration — see below | — |
221
239
  | `help` (also no arg / unrecognized) | print this reference verbatim, then stop | — |
222
240
 
@@ -47,6 +47,7 @@ doesn't validate, compile fails.
47
47
  | improvement | `improve/improvements/*.md` | **structured** | §3 | bloom |
48
48
  | ledger | `improve/ledger.md` | **structured** | §5 | bloom |
49
49
  | plan | `plans/**/*.md` | **structured** | §7 | plan/build |
50
+ | reconciliation report | `brain/reconciliations/*.md` | **structured** (validated, NOT in the manifest — travels on the brain-repo transport, like plans) | `run` (merge sha) · `outcome` ∈ `succeeded`\|`needs-attention`\|`superseded` · `tier` ∈ `canonical`\|`provisional`; refutation/deletion records ride INSIDE (body sections — one class); minted via `rafa okf new reconciliation-report` | distiller |
50
51
  | log | `brain/log.md` | **verbatim** | — (prose trail; OKF-reserved name, §11) | conductor |
51
52
  | index | `**/index.md` | **generated** | — (OKF §6 listings, by `rafa okf`; the ROOT index alone carries frontmatter: `okf_version` + provenance) | tool |
52
53
  | citation-check | `**/citation-check.md` | **generated** | — (by `rafa verify-citations`; self-describing frontmatter, §11) | tool |
@@ -500,7 +501,11 @@ match.
500
501
  | `get_plan` | `repo`, `id` (parent or child) | parent + all children **with stored bodies** + **derived** progress; a child id resolves the whole plan with `requestedChild` set — this is what "load plan X" materializes into `.rafa/plans/` |
501
502
  | `get_active_plan` | `repo` | resolves the channel pointer (`set_active_plan`) → `get_plan`, or "no active plan" |
502
503
 
503
- `repo` must match the key's scope exactly; `branch` is reserved (branch-keyed
504
+ `repo` is OPTIONAL on every tool the scope is DERIVED from the per-repo key
505
+ (the key IS the repo identity; a derivation from an authenticated fact, never an
506
+ assumed value). When provided it must match the registered id exactly — loud
507
+ mismatch, never a fallback. Sessions that pass it read the id from the committed
508
+ `rafa.json` `repoId`, never a folder name. `branch` is reserved (branch-keyed
504
509
  instances land in Slice 2 — third-party clients default to the production brain,
505
510
  i.e. the default branch's instance).
506
511
 
@@ -538,7 +543,13 @@ intent degrades to search; a wrong intent would answer the wrong thing.
538
543
  silent throttle.
539
544
 
540
545
  **Auth:** per-repo machine keys, minted on the platform, sent as
541
- `Authorization: Bearer`. Keys are stored hashed platform-side and live client-side in
546
+ `Authorization: Bearer`. **Rotation is zero-downtime by design:** keys COEXIST
547
+ mint the replacement, update the configs (settings.local.json · credentials.json ·
548
+ CI secrets), then revoke the old at leisure; revoke stays an immediate kill (a
549
+ security act, never graced). A live session holds its connect-time key — reconnect
550
+ the rafinery MCP server (or restart the session) after swapping; the CLI tools
551
+ (checkpoint · hydrate · distill) resolve credentials fresh per invocation and are
552
+ never stale. Keys are stored hashed platform-side and live client-side in
542
553
  `~/.config/rafinery/credentials.json` — never inside the code repo or this brain repo.
543
554
 
544
555
  **Client wiring (written by `rafa init`, secret-free where committed):** a key is
@@ -551,7 +562,7 @@ minted at setup generation and delivered consume-once through the setup fetch;
551
562
  ### §9 addendum — state-plane tools (three-store model, working-set architecture)
552
563
 
553
564
  The knowledge plane stays strictly read-only (org-brain writes go ONLY through
554
- files → compile → push → ingest). Seven additional tools write **collaboration
565
+ files → compile → push → ingest). Additional tools write **collaboration
555
566
  state**, marked by `envelope.plane: "state"`:
556
567
 
557
568
  | Tool | Store | Notes |
@@ -563,6 +574,11 @@ state**, marked by `envelope.plane: "state"`:
563
574
  | `fold_working_set` | branch working set | branch→parent-branch MECHANICAL fold (no LLM, no prism — rigor only at main): absent → re-keyed · identical → merged · divergent → `needs-adjudication` on the parent row |
564
575
  | `push_plan` / `list_plans` / `update_plan_status` / `set_active_plan` / `log_decision` | **plans** — (repo) work objects | the push-on-approval channel (§7 v2): approval pushes the whole work-item TREE (items[], bodies stored); `list_plans` = names only; statuses push back (5-value enum — blocked is derived); `log_decision` records the deliberation trail (paraphrase + pivotal quotes); the active pointer is explicit, never inferred |
565
576
  | `report_improvement_status` | **events only** — no store | a LIVE session signal (bloom's fixed-in-passing during build). The improvement ledger row is untouched — it has ONE writer, the ingest (K1); the platform overlays the report as pending-reconciliation until the next brain push confirms it |
577
+ | `reconcile_claim` | **reconciliations queue** — (repo) run rows | executor claim (`plane:"state"`): ACID no-running-sibling assert → returns the attempt token; delegates to `reconciliations.claim`; stamps the actor envelope `{model, agent, runner}` (runner ∈ `sandbox`\|`ci`\|`session`, enum-checked at the boundary) on the row |
578
+ | `reconcile_heartbeat` | reconciliations queue | extends the wall-clock lease + sets the phase; **attempt-fenced** (a non-current attempt is rejected, nothing written); actor envelope re-stamped |
579
+ | `reconcile_log_append` | **reconciliationLogs** — the run log tail | batched chunks, **secret-screened PER LINE** (the same `looksLikeSecret` screen — a credential chunk is rejected by chunk/line index, nothing stored or echoed) + **attempt-fenced**; `seq`-ordered; UNLOGGED (emits its own `reconcile.log` event, counts only) |
580
+ | `reconcile_report` | reconciliations queue (+ knowledge node on success) | terminal outcome + node delta + meter; **attempt-fenced**; success COMPOSES the pointer advance atomically (insert node + move pointer, one mutation); refutation/deletion ride the success outcomes; actor envelope stamped |
581
+ | `list_working_sets` | branch working set (**READ**) | enumerates branches with LIVE candidate rows + counts only (no bodies) — closes the 07-14 sensor gap (`get_working_set` needs a branch arg; nothing listed them); feeds the SessionStart digest pending line |
566
582
 
567
583
  State tools work with or without an ingested brain (a working set can exist —
568
584
  and a plan can be pushed — before the first scan) and are exempt from the
@@ -571,6 +587,18 @@ snapshot/ingestError gates. `get_plan`/`get_active_plan` share this exemption
571
587
  bucket-note tools (`stage/list/resolve_bucket_note`, pre-0.4.0) are superseded
572
588
  by the working-set tools.
573
589
 
590
+ **Typed candidates (capture-engine P2, additive).** A working-set row's KIND
591
+ derives from its path — never from a parallel field that could drift:
592
+ `brain/rules|playbooks/**` = **note** (§2, judged on "does the claim hold?");
593
+ `improve/improvements/**` = **improvement** (§3, judged on the LIFECYCLE —
594
+ "is the defect actually gone / does it actually exist on merged main?"; the
595
+ row resolution stays the standard enum, the verdict note carries
596
+ `confirmed-fixed` / `still-open` / `opened`); `intent/**` = **intent record**
597
+ (the post-commit hook's per-commit provenance — consumed at merge, never
598
+ judged, never authored into the org brain). `improve/ledger.md` is DERIVED
599
+ and never a candidate. schemaVersion stays 1 — this section is purely
600
+ additive (§8).
601
+
574
602
  ## 10. Agent cards — `../.claude/agents/*.md` (local gate)
575
603
 
576
604
  The shipped agents are contract-governed artifacts: every card is validated by
@@ -0,0 +1,81 @@
1
+ // rafa brain-commit — the post-commit worker (capture-engine P1, spec r2 §2.2).
2
+ // ONE brain commit per code commit, strict 1-1 (--allow-empty), on the MIRRORED
3
+ // brain branch — never the brain default branch (the distiller's, single
4
+ // writer). Carries whatever changed under .rafa/ plus the commit's INTENT
5
+ // RECORD (intent/<shortsha>.md); trailers code-commit/code-branch are the join
6
+ // keys. Standalone by design (node built-ins only, like every M5 sensor);
7
+ // non-blocking always — a brain problem must never block a code commit.
8
+
9
+ import { execSync } from "node:child_process";
10
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ const ROOT = process.cwd();
14
+ try {
15
+ if (process.env.RAFA_HOOKS_DISABLED === "1") process.exit(0);
16
+ if (!existsSync(join(ROOT, "rafa.json"))) process.exit(0);
17
+ if (!existsSync(join(ROOT, ".rafa", ".git"))) process.exit(0);
18
+
19
+ const sh = (cmd, cwd = ROOT) =>
20
+ execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
21
+ const rafa = join(ROOT, ".rafa");
22
+ const shR = (cmd) => sh(cmd, rafa);
23
+
24
+ const branch = sh("git rev-parse --abbrev-ref HEAD");
25
+ // Trunk commits never mirror — the brain default branch has one writer, the
26
+ // distiller. Conservative charset guard: a refname git allows but a shell
27
+ // might mangle is skipped, never quoted-and-hoped.
28
+ if (branch === "main" || branch === "master" || branch === "HEAD") process.exit(0);
29
+ if (!/^[A-Za-z0-9._/-]+$/.test(branch)) process.exit(0);
30
+
31
+ // Lockstep ensure (post-checkout's job, repeated here belt-and-braces): dirty
32
+ // surfaces carry to the OLD branch first — deterministic, nothing lost.
33
+ const cur = shR("git rev-parse --abbrev-ref HEAD");
34
+ if (cur !== branch) {
35
+ try {
36
+ shR("git add -A");
37
+ shR(`git commit -q -m "brain(switch-carryover)" -m "switch-carryover-from: ${cur}"`);
38
+ } catch {
39
+ /* nothing dirty */
40
+ }
41
+ try {
42
+ shR(`git rev-parse --verify -q "refs/heads/${branch}"`);
43
+ shR(`git checkout -q "${branch}"`);
44
+ } catch {
45
+ shR(`git checkout -q -b "${branch}"`);
46
+ }
47
+ }
48
+
49
+ // The intent record — the commit's end-to-end intent, mechanically joined.
50
+ // Minimal here (sha · subject · files); the P3 capture worker enriches.
51
+ const fullSha = sh("git rev-parse HEAD");
52
+ const short = fullSha.slice(0, 12);
53
+ const clean = (s) => s.replace(/[`"$\\]/g, "'");
54
+ const subject = clean(sh("git log -1 --pretty=%s")).slice(0, 100);
55
+ const files = sh("git diff-tree --no-commit-id --name-only -r HEAD")
56
+ .split("\n")
57
+ .filter(Boolean)
58
+ .slice(0, 100);
59
+ mkdirSync(join(rafa, "intent"), { recursive: true });
60
+ writeFileSync(
61
+ join(rafa, "intent", `${short}.md`),
62
+ `---\n` +
63
+ `type: IntentRecord\n` +
64
+ `description: "per-commit intent trail (capture-engine P2) — provenance, consumed at merge, never org-brain truth"\n` +
65
+ `code-commit: ${fullSha}\n` +
66
+ `code-branch: ${branch}\n` +
67
+ `timestamp: ${new Date().toISOString()}\n` +
68
+ `---\n\n# ${subject}\n\n## Files\n` +
69
+ files.map((f) => `- ${f}`).join("\n") +
70
+ `\n`,
71
+ );
72
+
73
+ shR("git add -A");
74
+ shR(
75
+ `git commit --allow-empty -q -m "brain(${branch}): ${subject}" ` +
76
+ `-m "code-commit: ${fullSha}" -m "code-branch: ${branch}"`,
77
+ );
78
+ } catch {
79
+ /* silent by design — the heartbeat carries sensor health */
80
+ }
81
+ process.exit(0);
@@ -0,0 +1,64 @@
1
+ // rafa brain-map — the post-rewrite worker (capture-engine P1, spec r2 §2.2).
2
+ // Rebase/amend rewrite the CODE shas; git hands the old→new map on stdin. We
3
+ // APPEND it to the mirrored brain branch (maps/rewrite-<ts>.json) — brain
4
+ // history is never rewritten. Collection keys on the merge event's branch
5
+ // name; the map chain restores per-commit grounding through code-commit
6
+ // trailers. argv[2]: "amend" | "rebase" (git's post-rewrite kind).
7
+
8
+ import { execSync } from "node:child_process";
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ const ROOT = process.cwd();
13
+ try {
14
+ if (process.env.RAFA_HOOKS_DISABLED === "1") process.exit(0);
15
+ if (!existsSync(join(ROOT, "rafa.json"))) process.exit(0);
16
+ if (!existsSync(join(ROOT, ".rafa", ".git"))) process.exit(0);
17
+
18
+ const map = readFileSync(0, "utf8")
19
+ .split("\n")
20
+ .filter(Boolean)
21
+ .map((l) => {
22
+ const [oldSha, newSha] = l.trim().split(/\s+/);
23
+ return { old: oldSha, new: newSha };
24
+ })
25
+ .filter((m) => /^[0-9a-f]{40,64}$/i.test(m.old ?? "") && /^[0-9a-f]{40,64}$/i.test(m.new ?? ""));
26
+ if (map.length === 0) process.exit(0);
27
+
28
+ const sh = (cmd, cwd = ROOT) =>
29
+ execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
30
+ const rafa = join(ROOT, ".rafa");
31
+ const shR = (cmd) => sh(cmd, rafa);
32
+
33
+ const branch = sh("git rev-parse --abbrev-ref HEAD");
34
+ if (branch === "main" || branch === "master" || branch === "HEAD") process.exit(0);
35
+ if (!/^[A-Za-z0-9._/-]+$/.test(branch)) process.exit(0);
36
+
37
+ const cur = shR("git rev-parse --abbrev-ref HEAD");
38
+ if (cur !== branch) {
39
+ try {
40
+ shR("git add -A");
41
+ shR(`git commit -q -m "brain(switch-carryover)" -m "switch-carryover-from: ${cur}"`);
42
+ } catch {
43
+ /* nothing dirty */
44
+ }
45
+ try {
46
+ shR(`git rev-parse --verify -q "refs/heads/${branch}"`);
47
+ shR(`git checkout -q "${branch}"`);
48
+ } catch {
49
+ shR(`git checkout -q -b "${branch}"`);
50
+ }
51
+ }
52
+
53
+ const kind = process.argv[2] === "amend" ? "amend" : "rebase";
54
+ mkdirSync(join(rafa, "maps"), { recursive: true });
55
+ writeFileSync(
56
+ join(rafa, "maps", `rewrite-${Date.now()}.json`),
57
+ JSON.stringify({ kind, at: new Date().toISOString(), branch, map }, null, 2) + "\n",
58
+ );
59
+ shR("git add -A");
60
+ shR(`git commit -q -m "brain(rewrite-map): ${kind} · ${map.length} commit(s)" -m "code-branch: ${branch}"`);
61
+ } catch {
62
+ /* silent by design */
63
+ }
64
+ process.exit(0);
@@ -0,0 +1,48 @@
1
+ // rafa brain-switch — the post-checkout worker (capture-engine P1, spec r2
2
+ // §2.2). Keeps the nested .rafa/ repo's branch in LOCKSTEP with the code
3
+ // branch: code branch cut/switch ⇒ brain branch cut/switch (created lazily
4
+ // from the current local state). Dirty brain surfaces are committed to the OLD
5
+ // branch first (switch-carryover) — deterministic, nothing lost, nothing
6
+ // blocks. argv: <prevHEAD> <newHEAD> <flag> (git's post-checkout contract;
7
+ // flag "1" = branch checkout, "0" = file checkout → no-op).
8
+
9
+ import { execSync } from "node:child_process";
10
+ import { existsSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ const ROOT = process.cwd();
14
+ try {
15
+ if (process.env.RAFA_HOOKS_DISABLED === "1") process.exit(0);
16
+ if (process.argv[4] !== "1") process.exit(0);
17
+ if (!existsSync(join(ROOT, "rafa.json"))) process.exit(0);
18
+ if (!existsSync(join(ROOT, ".rafa", ".git"))) process.exit(0);
19
+
20
+ const sh = (cmd, cwd = ROOT) =>
21
+ execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
22
+ const shR = (cmd) => sh(cmd, join(ROOT, ".rafa"));
23
+
24
+ const branch = sh("git rev-parse --abbrev-ref HEAD");
25
+ if (branch === "HEAD") process.exit(0); // detached — no mirror
26
+ if (!/^[A-Za-z0-9._/-]+$/.test(branch)) process.exit(0);
27
+
28
+ const cur = shR("git rev-parse --abbrev-ref HEAD");
29
+ if (cur === branch) process.exit(0);
30
+ try {
31
+ shR("git add -A");
32
+ shR(`git commit -q -m "brain(switch-carryover)" -m "switch-carryover-from: ${cur}"`);
33
+ } catch {
34
+ /* nothing dirty */
35
+ }
36
+ // Trunk mirrors to the brain repo's SAME-NAMED branch when it exists — the
37
+ // default branch stays the distiller's write target; the session only ever
38
+ // reads there. A missing trunk-named brain branch is left alone.
39
+ try {
40
+ shR(`git rev-parse --verify -q "refs/heads/${branch}"`);
41
+ shR(`git checkout -q "${branch}"`);
42
+ } catch {
43
+ if (branch !== "main" && branch !== "master") shR(`git checkout -q -b "${branch}"`);
44
+ }
45
+ } catch {
46
+ /* silent by design */
47
+ }
48
+ process.exit(0);
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ # rafa post-checkout hook v1 (installed by @rafinery/cli init/pull/update).
3
+ #
4
+ # Capture-engine P1: brain-branch LOCKSTEP — code branch cut/switch mirrors in
5
+ # the nested .rafa/ repo; dirty brain surfaces carry to the old branch first.
6
+ # Non-blocking and silent on every path.
7
+ [ "$RAFA_HOOKS_DISABLED" = "1" ] && exit 0
8
+ [ -f "rafa.json" ] || exit 0
9
+ node ".claude/rafa/hooks/brain-switch.mjs" "$1" "$2" "$3" >/dev/null 2>&1 || true
10
+ exit 0
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ # rafa post-commit hook v1 (installed by @rafinery/cli init/pull/update — safe
3
+ # to delete; offered again, never forced over a foreign hook).
4
+ #
5
+ # Capture-engine P1: ONE brain commit per code commit (strict 1-1) on the
6
+ # MIRRORED brain branch, with the commit's intent record inside. Hook-owned —
7
+ # the dev never maintains the brain. Non-blocking and silent on every path.
8
+ [ "$RAFA_HOOKS_DISABLED" = "1" ] && exit 0
9
+ [ -f "rafa.json" ] || exit 0
10
+ node ".claude/rafa/hooks/brain-commit.mjs" >/dev/null 2>&1 || true
11
+ exit 0
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ # rafa post-rewrite hook v1 (installed by @rafinery/cli init/pull/update).
3
+ #
4
+ # Capture-engine P1: rebase/amend safety — git's old→new sha map is APPENDED to
5
+ # the mirrored brain branch (never rewritten); CI resolves code-commit trailers
6
+ # through the chain. Non-blocking and silent on every path.
7
+ [ "$RAFA_HOOKS_DISABLED" = "1" ] && exit 0
8
+ [ -f "rafa.json" ] || exit 0
9
+ node ".claude/rafa/hooks/brain-map.mjs" "$1" >/dev/null 2>&1 || true
10
+ exit 0
@@ -19,6 +19,28 @@ npx -y @rafinery/cli checkpoint || {
19
19
  echo "rafa · checkpoint CONFLICT — a teammate's newer copy landed as *.theirs.md; decide in your next session (merge/adopt/keep, re-run rafa checkpoint). Push continues."
20
20
  else
21
21
  echo "rafa · checkpoint skipped (exit $code) — push continues; run 'rafa checkpoint' manually when convenient."
22
+ # P0: a swallowed failure must still reach the heartbeat — record it so the
23
+ # next checkpoint's report_sensor_health carries it (best-effort, silent).
24
+ mkdir -p .rafa 2>/dev/null && printf '{"t":%s000,"hook":"pre-push","error":"checkpoint exit %s"}\n' "$(date +%s)" "$code" >> .rafa/sensor-errors.jsonl 2>/dev/null || true
22
25
  fi
23
26
  }
27
+
28
+ # Capture-engine P1 ceremony: durably push the MIRRORED brain branch so capture
29
+ # survives a dead laptop, a closed PR, an abandoned branch. Trunk pushes skip —
30
+ # the brain default branch is the distiller's; sessions never push it. A
31
+ # diverged remote (two clones on one branch) takes a mechanical transport
32
+ # replay; a real conflict is left intact for the next session, never forced.
33
+ BR="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
34
+ if [ -n "$BR" ] && [ "$BR" != "main" ] && [ "$BR" != "master" ] && [ -d ".rafa/.git" ]; then
35
+ if git -C .rafa rev-parse --verify -q "refs/heads/$BR" >/dev/null 2>&1; then
36
+ echo "rafa · brain branch push ($BR) …"
37
+ git -C .rafa push -q -u origin "$BR" 2>/dev/null || {
38
+ git -C .rafa pull -q --rebase origin "$BR" 2>/dev/null && git -C .rafa push -q -u origin "$BR" 2>/dev/null || {
39
+ git -C .rafa rebase --abort >/dev/null 2>&1
40
+ echo "rafa · brain branch push skipped (remote diverged on the same file) — the next session reconciles; code push continues."
41
+ printf '{"t":%s000,"hook":"pre-push","error":"brain branch push diverged"}\n' "$(date +%s)" >> .rafa/sensor-errors.jsonl 2>/dev/null || true
42
+ }
43
+ }
44
+ fi
45
+ fi
24
46
  exit 0