@rafinery/cli 0.5.0 → 0.7.0

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 (35) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/bin/rafa.mjs +37 -5
  3. package/blueprint/.claude/agents/atlas.md +2 -1
  4. package/blueprint/.claude/agents/sage.md +66 -0
  5. package/blueprint/.claude/commands/rafa.md +214 -269
  6. package/blueprint/.claude/rafa/contract.md +204 -115
  7. package/blueprint/.claude/rafa/hooks/post-tool.mjs +62 -0
  8. package/blueprint/.claude/rafa/hooks/pre-push +24 -0
  9. package/blueprint/.claude/rafa/hooks/session-start.mjs +229 -0
  10. package/blueprint/.claude/rafa/hooks/statusline.mjs +113 -0
  11. package/blueprint/.claude/rafa/hooks/user-prompt-submit.mjs +87 -0
  12. package/blueprint/.claude/skills/rafa-build/SKILL.md +20 -5
  13. package/blueprint/.claude/skills/rafa-distill/SKILL.md +6 -1
  14. package/blueprint/.claude/skills/rafa-plan/SKILL.md +7 -0
  15. package/blueprint/.claude/skills/rafa-sage/SKILL.md +201 -0
  16. package/blueprint/.claude/skills/rafa-scan/SKILL.md +55 -5
  17. package/blueprint/.claude/skills/rafa-validate/SKILL.md +15 -2
  18. package/lib/benchmark.mjs +573 -0
  19. package/lib/blueprint.mjs +11 -1
  20. package/lib/brain-repo.mjs +10 -4
  21. package/lib/ci-setup.mjs +2 -0
  22. package/lib/claude-config.mjs +77 -0
  23. package/lib/dirty.mjs +114 -0
  24. package/lib/distill.mjs +4 -0
  25. package/lib/gate/compile.mjs +293 -44
  26. package/lib/gate/verify-citations.mjs +214 -23
  27. package/lib/githook.mjs +54 -0
  28. package/lib/init.mjs +18 -0
  29. package/lib/pull.mjs +7 -0
  30. package/lib/push.mjs +21 -0
  31. package/lib/reflex.mjs +76 -0
  32. package/lib/releases.mjs +35 -0
  33. package/lib/status.mjs +152 -0
  34. package/lib/update.mjs +13 -0
  35. package/package.json +1 -1
@@ -24,7 +24,7 @@ agents write .md (frontmatter per this contract)
24
24
  ```
25
25
 
26
26
  Two enforcement points: **compile-time** (structure — this contract) and **prism**
27
- (semantics — truth of the content). Structure is validated *before* semantics.
27
+ (semantics — truth of the content). Structure is validated _before_ semantics.
28
28
 
29
29
  ---
30
30
 
@@ -35,20 +35,23 @@ a schema (parse it), a `verbatim` type is prose (display it, never parse for dat
35
35
  `generated` type is machine-written by a named tool. If a file matching a `structured` path
36
36
  doesn't validate, compile fails.
37
37
 
38
- | Type | Path glob | Class | Schema | Author |
39
- |---|---|---|---|---|
40
- | rule | `brain/rules/*.md` | **structured** | §2 | atlas |
41
- | playbook | `brain/playbooks/*.md` | **structured** | §2 | atlas |
42
- | health | `brain/checklist.md` | **structured** | §4 | prism |
43
- | coverage | `brain/coverage.md` | **structured** | §6 | atlas |
44
- | improvement | `improve/improvements/*.md` | **structured** | §3 | bloom |
45
- | ledger | `improve/ledger.md` | **structured** | §5 | bloom |
46
- | plan | `plans/**/*.md` | **structured** | §7 | plan/build |
47
- | log | `brain/log.md` | **verbatim** | — (prose trail) | conductor |
48
- | citation-check | `**/citation-check.md` | **generated** | — (by `rafa verify-citations`) | tool |
49
- | active pointer | `active.md` | **generated** | (`# <plan-id>` or "No active plan"; compile emits it as `activePlanId`, §7) | conductor |
50
- | contract copy | `contract.md` | **generated** | — (stamped copy of `.claude/rafa/contract.md`, written by `rafa push`) | tool |
51
- | agent | `.claude/agents/*.md` (code repo) | **structured** (local gate NOT in manifest) | §10 | rafa |
38
+ | Type | Path glob | Class | Schema | Author |
39
+ | --------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
40
+ | rule | `brain/rules/*.md` | **structured** | §2 | atlas |
41
+ | playbook | `brain/playbooks/*.md` | **structured** | §2 | atlas |
42
+ | health | `brain/checklist.md` | **structured** | §4 | prism |
43
+ | coverage | `brain/coverage.md` | **structured** | §6 | atlas |
44
+ | improvement | `improve/improvements/*.md` | **structured** | §3 | bloom |
45
+ | ledger | `improve/ledger.md` | **structured** | §5 | bloom |
46
+ | plan | `plans/**/*.md` | **structured** | §7 | plan/build |
47
+ | log | `brain/log.md` | **verbatim** | — (prose trail) | conductor |
48
+ | citation-check | `**/citation-check.md` | **generated** | — (by `rafa verify-citations`) | tool |
49
+ | citation-check record | `**/citation-check.json` | **generated** | `{ checkerVersion, at, pass, gates, warns }` (by `rafa verify-citations`; compile folds it into `manifest.citations`) | tool |
50
+ | active pointer | `active.md` | **generated** | — (`# <plan-id>` or "No active plan"; compile emits it as `activePlanId`, §7) | conductor |
51
+ | contract copy | `contract.md` | **generated** |(stamped copy of `.claude/rafa/contract.md`, written by `rafa push`) | tool |
52
+ | dirty queue | `dirty.jsonl` | **generated** (local state — transport-excluded, NEVER pushed/ingested) | one `{f, t}` JSON line per code edit (by the PostToolUse sensor hook; read by `rafa dirty` + the SessionStart digest) | tool |
53
+ | reflex queue | `reflex.jsonl` | **generated** (local state — transport-excluded, NEVER pushed/ingested; transcript pointers are LOCAL, raw transcripts never ship) | `{id, p, t, tp}` per detected correction + append-only `{id, done, verdict, at}` markers (by the UserPromptSubmit sensor; read by `rafa reflex` + the digest) | tool |
54
+ | agent | `.claude/agents/*.md` (code repo) | **structured** (local gate — NOT in manifest) | §10 | rafa |
52
55
 
53
56
  The `agent` type is the one structured type outside `.rafa/`: the shipped agent cards
54
57
  in the code repo. Compile validates them (§10) so a malformed card fails the gate like
@@ -69,7 +72,7 @@ error (no rogue unschema'd data files).
69
72
 
70
73
  - **Frontmatter is the only machine-read surface.** The markdown **body is prose**
71
74
  and is NEVER parsed for data. Every value the platform shows comes from frontmatter
72
- (via the compiled manifest), or from the body rendered *verbatim* (never scraped).
75
+ (via the compiled manifest), or from the body rendered _verbatim_ (never scraped).
73
76
  - **`schemaVersion` is required** on every file and in the manifest. Unknown version
74
77
  → ingest reports an error, does not guess.
75
78
  - **`id` is required and stable.** It MUST equal the filename stem (`foo.md` → `foo`).
@@ -105,7 +108,7 @@ Deterministic to parse: indented non-empty lines after `>-` join with a single s
105
108
 
106
109
  Each cite is one string: `` `<path>:<line> :: <token>` ``
107
110
 
108
- - split on the **first** ` :: ` → (`left`, `token`)
111
+ - split on the **first** `::` → (`left`, `token`)
109
112
  - in `left`, split on the **last** `:` → (`path`, `line`)
110
113
  - `line` is digits (`"42"`) or a range (`"14-18"`)
111
114
 
@@ -121,61 +124,82 @@ JSON. This is the exact shape the platform binds to.
121
124
  ```jsonc
122
125
  {
123
126
  "schemaVersion": 1,
124
- "generatedAt": "2026-07-03T10:00:00Z", // ISO-8601, stamped by compile
125
- "repo": "owner/repo", // CODE repo full_name (identity)
126
- "branch": "main", // code branch this brain reflects
127
- "codeSha": "abc123…", // code commit the brain was built for
128
-
129
- "health": { // from brain/checklist.md — or null
130
- "verdict": "PASS", // "PASS" | "ITERATE"
131
- "score": 95, // 0–100
127
+ "generatedAt": "2026-07-03T10:00:00Z", // ISO-8601, stamped by compile
128
+ "repo": "owner/repo", // CODE repo full_name (identity)
129
+ "branch": "main", // code branch this brain reflects
130
+ "codeSha": "abc123…", // code commit the brain was built for
131
+
132
+ "health": {
133
+ // from brain/checklist.md — or null
134
+ "verdict": "PASS", // "PASS" | "ITERATE"
135
+ "score": 95, // 0–100
132
136
  "gates": { "fidelity": "pass", "coverage": "pass" },
133
- "counts": { "blockers": 0, "majors": 0, "minors": 2 }
137
+ "counts": { "blockers": 0, "majors": 0, "minors": 2 },
134
138
  },
135
139
 
136
- "ledger": { // from improve/ledger.md — or null
140
+ "ledger": {
141
+ // from improve/ledger.md — or null
137
142
  "open": 7,
138
143
  "debtScore": 13,
139
- "byPriority": { "P0": 0, "P1": 1, "P2": 3, "P3": 3 }
144
+ "byPriority": { "P0": 0, "P1": 1, "P2": 3, "P3": 3 },
140
145
  },
141
146
 
142
- "coverage": { // from brain/coverage.md — or null
143
- "domains": [ // one row per domain, its scan status
147
+ "coverage": {
148
+ // from brain/coverage.md or null
149
+ "domains": [
150
+ // one row per domain, its scan status
144
151
  { "domain": "design-system", "status": "mapped" },
145
- { "domain": "external-integrations", "status": "thin" }
146
- ]
152
+ { "domain": "external-integrations", "status": "thin" },
153
+ ],
147
154
  },
148
155
 
156
+ "citations": {
157
+ // from brain/citation-check.json — or null
158
+ "checkerVersion": 2, // the gate level this brain passed
159
+ "pass": true, // that run's verdict (recorded, never assumed)
160
+ "at": "2026-07-12T10:00:00Z", // when the checker last ran
161
+ }, // null = no recorded checker run rode this push
162
+
149
163
  "notes": [
150
164
  {
151
165
  "id": "agent-name-contract",
152
- "kind": "rule", // "rule" | "playbook" (from directory)
153
- "type": "contract", // contract | convention | flow | how-to
166
+ "kind": "rule", // "rule" | "playbook" (from directory)
167
+ "type": "contract", // contract | convention | flow | how-to
154
168
  "domain": "web-agent-bridge",
155
169
  "title": "…",
156
170
  "summary": "…",
157
- "links": ["other-note-id"], // [] if none
158
- "failure": "silent", // "silent" | "loud" | omitted
159
- "cites": [{ "file": "src/x.tsx", "line": "42", "token": "research_agent" }],
160
- "path": "brain/rules/agent-name-contract.md" // prose body (lazy fetch)
161
- }
171
+ "links": ["other-note-id"], // [] if none
172
+ "failure": "silent", // "silent" | "loud" | omitted
173
+ "bodyTokens": 180, // OPTIONAL · ceil(bodyChars/4) size estimate of the
174
+ // prose body — deterministic (no LLM), for recall-savings
175
+ "cites": [
176
+ {
177
+ "file": "src/x.tsx",
178
+ "line": "42",
179
+ "token": "research_agent",
180
+ "targetTokens": 940,
181
+ },
182
+ ], // targetTokens OPTIONAL · ceil(fileChars/4) of the CITED
183
+ // file — OMITTED when unreadable at compile (never 0)
184
+ "path": "brain/rules/agent-name-contract.md", // prose body (lazy fetch)
185
+ },
162
186
  ],
163
187
 
164
188
  "improvements": [
165
189
  {
166
190
  "id": "dead-model-options-google-crewai",
167
- "priority": "P1", // P0 | P1 | P2 | P3
168
- "lens": "product", // security|correctness|performance|architecture|product|ops
169
- "status": "open", // open | backlog | fixed | wontfix
191
+ "priority": "P1", // P0 | P1 | P2 | P3
192
+ "lens": "product", // security|correctness|performance|architecture|product|ops
193
+ "status": "open", // open | backlog | fixed | wontfix
170
194
  "title": "…",
171
195
  "summary": "…",
172
196
  "fix": "…",
173
- "leverage": { "impact": "high", "effort": "low" }, // impact/effort ∈ low|medium|high
174
- "blastRadius": ["web-agent-bridge"], // [] if none
197
+ "leverage": { "impact": "high", "effort": "low" }, // impact/effort ∈ low|medium|high
198
+ "blastRadius": ["web-agent-bridge"], // [] if none
175
199
  "cites": [{ "file": "…", "line": "25", "token": "…" }],
176
- "path": "improve/improvements/dead-model-options-google-crewai.md"
177
- }
178
- ]
200
+ "path": "improve/improvements/dead-model-options-google-crewai.md",
201
+ },
202
+ ],
179
203
  }
180
204
  ```
181
205
 
@@ -188,6 +212,16 @@ Ingest is `JSON.parse` + a shape-check against this schema. Any mismatch (missin
188
212
  key, wrong enum, wrong type, unknown `schemaVersion`) → a **surfaced ingest error**
189
213
  on the platform, never a guessed value.
190
214
 
215
+ **Size stamps (`bodyTokens`, `cites[].targetTokens`) are ADDITIVE OPTIONAL.** compile
216
+ stamps each note's body-token estimate and, per cite, the cited file's token estimate —
217
+ both `ceil(chars/4)`, computed deterministically at compile time (no LLM, no network) to
218
+ feed the platform's recall-savings estimator. `targetTokens` is **omitted** for any cite
219
+ whose target is unreadable at compile (honest absence — never a guessed `0`). Because
220
+ these fields are purely additive and optional, `schemaVersion` **stays 1**: ingest never
221
+ refuses a manifest for lacking them, so a legacy manifest produced without them (or with
222
+ some cites carrying no `targetTokens`) remains valid. The shape-check treats them as
223
+ optional and tolerates their absence.
224
+
191
225
  ---
192
226
 
193
227
  ## 2. Note files — `brain/rules/*.md`, `brain/playbooks/*.md`
@@ -197,20 +231,36 @@ on the platform, never a guessed value.
197
231
  ```yaml
198
232
  ---
199
233
  schemaVersion: 1
200
- id: agent-name-contract # required · == filename stem
201
- type: contract # required · contract | convention | flow | how-to
202
- domain: web-agent-bridge # required · non-empty
203
- title: The agent name is a cross-process contract # required
204
- summary: Only "research_agent" is fully wired end to end # required
205
- links: [copilotkit-runtime-route-convention] # optional · default []
206
- failure: silent # optional · silent | loud
207
- cites: # required · ≥ 1
234
+ id: agent-name-contract # required · == filename stem
235
+ type: contract # required · contract | convention | flow | how-to
236
+ domain: web-agent-bridge # required · non-empty
237
+ title: The agent name is a cross-process contract # required
238
+ summary: Only "research_agent" is fully wired end to end # required
239
+ links: [copilotkit-runtime-route-convention] # optional · default []
240
+ failure: silent # optional · silent | loud
241
+ anchor:
242
+ research_agent # optional · checker gate B2: EVERY code hit of this
243
+ # token must be a cited site (`anchor: none` = explicit
244
+ # exemption for composition/ordering contracts)
245
+ absent:
246
+ legacy_agent_name # optional · checker gate B3: this token must appear
247
+ # NOWHERE in code (docs/.md excluded) — declare one for
248
+ # every claim that depends on something NOT existing;
249
+ # the checker re-greps it every run, so the claim can
250
+ # never silently go stale. Repeatable (one per line).
251
+ cites: # required · ≥ 1
208
252
  - src/lib/model-selector-provider.tsx:42 :: research_agent
209
253
  - agents/python/main.py:20 :: research_agent
210
254
  ---
211
255
  (prose body — rendered verbatim, never parsed)
212
256
  ```
213
257
 
258
+ `anchor:`/`absent:` are **checker declarations** (consumed by `rafa verify-citations`
259
+ gates B2/B3, not emitted into the manifest). A note whose title/summary reads as an
260
+ absence claim without declaring `absent:` is listed as a checker WARN — prism's
261
+ worklist, never a gate failure (a heuristic that fails the gate would be an assumed
262
+ value).
263
+
214
264
  ---
215
265
 
216
266
  ## 3. Improvement files — `improve/improvements/*.md`
@@ -218,18 +268,18 @@ cites: # required · ≥ 1
218
268
  ```yaml
219
269
  ---
220
270
  schemaVersion: 1
221
- id: dead-model-options-google-crewai # required · == filename stem
222
- priority: P1 # required · P0 | P1 | P2 | P3
223
- lens: product # required · security|correctness|performance|architecture|product|ops
224
- status: open # required · open | backlog | fixed | wontfix
225
- title: Two of the four model options are dead and fail silently # required
226
- summary: google_genai and crewai route to unwired agents # required
227
- fix: Wire both agents in main.py, or remove them from the picker # required
228
- leverage: { impact: high, effort: low } # required · impact/effort ∈ low|medium|high
229
- blast_radius: [web-agent-bridge, external-integrations] # optional · default []
230
- cites: # required · ≥ 1
271
+ id: dead-model-options-google-crewai # required · == filename stem
272
+ priority: P1 # required · P0 | P1 | P2 | P3
273
+ lens: product # required · security|correctness|performance|architecture|product|ops
274
+ status: open # required · open | backlog | fixed | wontfix
275
+ title: Two of the four model options are dead and fail silently # required
276
+ summary: google_genai and crewai route to unwired agents # required
277
+ fix: Wire both agents in main.py, or remove them from the picker # required
278
+ leverage: { impact: high, effort: low } # required · impact/effort ∈ low|medium|high
279
+ blast_radius: [web-agent-bridge, external-integrations] # optional · default []
280
+ cites: # required · ≥ 1
231
281
  - src/components/ModelSelector.tsx:25 :: google_genai
232
- found: 2026-07-02 # optional · ISO date
282
+ found: 2026-07-02 # optional · ISO date
233
283
  ---
234
284
  (prose body)
235
285
  ```
@@ -244,10 +294,10 @@ from prose. That scraping is gone; these are authored values.
244
294
  ```yaml
245
295
  ---
246
296
  schemaVersion: 1
247
- verdict: PASS # required · PASS | ITERATE
248
- score: 95 # required · 0–100
249
- gates: { fidelity: pass, coverage: pass } # required · each pass | fail
250
- counts: { blockers: 0, majors: 0, minors: 2 } # required · non-negative ints
297
+ verdict: PASS # required · PASS | ITERATE
298
+ score: 95 # required · 0–100
299
+ gates: { fidelity: pass, coverage: pass } # required · each pass | fail
300
+ counts: { blockers: 0, majors: 0, minors: 2 } # required · non-negative ints
251
301
  ---
252
302
  (prism report body)
253
303
  ```
@@ -282,10 +332,19 @@ domain found (compile emits it as `[{ domain, status }]` in the manifest). Statu
282
332
  ---
283
333
  schemaVersion: 1
284
334
  domains: { design-system: mapped, components: mapped, api: thin, external-integrations: empty }
335
+ inventory: # optional · declared surface inventories, re-counted by
336
+ - route-pages :: apps/web/app/**/page.tsx :: 23 # the checker every run
337
+ - api-routes :: apps/web/app/api/**/route.ts :: 2
285
338
  ---
286
339
  (per-criterion PASS/FAIL narrative in the body)
287
340
  ```
288
341
 
342
+ `inventory:` entries are `<name> :: <glob> :: <count>` — the checker recomputes each
343
+ count via `git ls-files ':(glob)<glob>'` and FAILS on drift (coverage claiming a
344
+ surface inventory the repo has outgrown). atlas declares one per framework surface it
345
+ maps (route pages, API routes, workflows — whatever the repo's shape makes load-bearing);
346
+ compile validates the grammar, the checker owns the truth.
347
+
289
348
  ---
290
349
 
291
350
  ## 7. Plan files — `plans/**/*.md` (+ the plans channel) — **v2: the work-item tree**
@@ -300,24 +359,25 @@ Each item owns exactly one file (merges never conflict; progress is derived).
300
359
  ```yaml
301
360
  ---
302
361
  schemaVersion: 1
303
- id: payments-c3-webhooks # required · == filename stem · GLOBALLY unique across plans/**
304
- plan: payments # required · the ROOT (epic) id
305
- parent: payments-c3 # required · the parent ITEM id (any rank above), or null (epic)
306
- kind: epic | task | subtask # required · epic = root only · task's parent = the epic · subtask's parent = a task
307
- title: Handle provider webhooks # required · WHAT (Linear title · Jira summary · Asana name)
308
- description: >- # optional · WHY + context — syncs 1:1 with vendor description/notes
362
+ id: payments-c3-webhooks # required · == filename stem · GLOBALLY unique across plans/**
363
+ plan: payments # required · the ROOT (epic) id
364
+ parent: payments-c3 # required · the parent ITEM id (any rank above), or null (epic)
365
+ kind: epic | task | subtask # required · epic = root only · task's parent = the epic · subtask's parent = a task
366
+ title: Handle provider webhooks # required · WHAT (Linear title · Jira summary · Asana name)
367
+ description:
368
+ >- # optional · WHY + context — syncs 1:1 with vendor description/notes
309
369
  reconciliation must be automatic; manual matching doesn't scale
310
- approach: verify HMAC → enqueue → idempotent apply # optional · HOW, one line — rafa value-add
311
- status: todo | in-progress | done | superseded | abandoned # required (task/subtask); epic may omit
312
- assignee: rohik # optional · WHO (free string pre-orgs; unanimous vendor word)
313
- blocked_by: [payments-c2] # optional · item ids this waits on — a dependency IS a blocker
314
- blocked_reason: "vendor sandbox access" # optional · external blocks (no dependency id to point at)
315
- priority: 2 # optional · 0 none · 1 urgent · 2 high · 3 medium · 4 low (Linear scale)
316
- estimate: 3 # optional · points (Linear estimate / Jira story points)
317
- branch: feat/webhooks # optional · ↔ Linear branchName
318
- baseSha: abc123 # optional · commit it was cut from
319
- domains: [payments] # optional · EPIC only · the blast radius — the brain link
320
- external: { provider: jira, key: PAY-142 } # optional · tracker identity — READ-ONLY to sessions
370
+ approach: verify HMAC → enqueue → idempotent apply # optional · HOW, one line — rafa value-add
371
+ status: todo | in-progress | done | superseded | abandoned # required (task/subtask); epic may omit
372
+ assignee: rohik # optional · WHO (free string pre-orgs; unanimous vendor word)
373
+ blocked_by: [payments-c2] # optional · item ids this waits on — a dependency IS a blocker
374
+ blocked_reason: "vendor sandbox access" # optional · external blocks (no dependency id to point at)
375
+ priority: 2 # optional · 0 none · 1 urgent · 2 high · 3 medium · 4 low (Linear scale)
376
+ estimate: 3 # optional · points (Linear estimate / Jira story points)
377
+ branch: feat/webhooks # optional · ↔ Linear branchName
378
+ baseSha: abc123 # optional · commit it was cut from
379
+ domains: [payments] # optional · EPIC only · the blast radius — the brain link
380
+ external: { provider: jira, key: PAY-142 } # optional · tracker identity — READ-ONLY to sessions
321
381
  ---
322
382
  (prose body — a LEAF item (a task with no subtasks, or a subtask) MUST carry a
323
383
  `## Done-check`; `## Log` is the execution journal; `## Decisions` mirrors the
@@ -352,7 +412,7 @@ Rules (all compile-enforced unless noted):
352
412
  **Decisions — first-class deliberation records (channel-borne, never frontmatter):**
353
413
  the frontmatter grammar is flat and bodies are never parsed, so structured decisions
354
414
  live on the PLATFORM, pushed via `log_decision` at checkpoint moments:
355
- `{ item, at, actor, context, options[], decision, rationale }` — *actor* is who
415
+ `{ item, at, actor, context, options[], decision, rationale }` — _actor_ is who
356
416
  decided (the dev for steering; the agent for proposals it made under standing
357
417
  consent). Text fidelity: **paraphrase + short verbatim quotes only where the exact
358
418
  wording carries the decision** (transcripts never land in shared stores). Sessions
@@ -382,7 +442,10 @@ imported comments are never scraped into decisions.
382
442
  ## 8. Versioning & the validate-and-correct loop
383
443
 
384
444
  - **`schemaVersion`** bumps when this contract changes incompatibly. Ingest refuses
385
- an unknown version (surfaced error) rather than mis-reading it.
445
+ an unknown version (surfaced error) rather than mis-reading it. **Additive OPTIONAL
446
+ fields do NOT bump it** — e.g. the note size stamps (`bodyTokens`,
447
+ `cites[].targetTokens`, §1): ingest tolerates their absence, so a legacy manifest
448
+ without them stays valid at the same `schemaVersion`.
386
449
  - **Validate-and-correct:** `rafa compile` validates every file. On failure it emits
387
450
  a structured error — `path · field · rule` — to the **authoring agent** (atlas for
388
451
  notes, bloom for improvements/ledger, prism for checklist). The agent edits the
@@ -396,18 +459,18 @@ imported comments are never scraped into decisions.
396
459
 
397
460
  The platform serves the **ingested** brain to any MCP client (the build agent, a
398
461
  teammate's session, Slack/incident.io agents) — the read-side twin of this contract.
399
- One backend: the platform. Local files are the *authoring* surface; the platform is
400
- the *serving* surface, and it serves only what passed compile + ingest.
462
+ One backend: the platform. Local files are the _authoring_ surface; the platform is
463
+ the _serving_ surface, and it serves only what passed compile + ingest.
401
464
 
402
465
  **Envelope — mandatory on every tool response:**
403
466
 
404
467
  ```jsonc
405
468
  {
406
469
  "source": "platform",
407
- "brainForSha": "<snapshot codeSha>", // the ingested manifest's codeSha — one source
470
+ "brainForSha": "<snapshot codeSha>", // the ingested manifest's codeSha — one source
408
471
  "ingestedAt": "<ISO time of ingest>",
409
472
  "schemaVersion": 1,
410
- "synthesized": false // raw tools: always false
473
+ "synthesized": false, // raw tools: always false
411
474
  }
412
475
  ```
413
476
 
@@ -419,15 +482,16 @@ match.
419
482
 
420
483
  **Tools (read-only — ALL writes flow files → compile → push → ingest):**
421
484
 
422
- | Tool | Args | Returns |
423
- |---|---|---|
424
- | `get_brain_status` | `repo` | envelope + per-type counts, health, coverage summary, `activePlanId`, `ingestError?` |
425
- | `get_coverage` | `repo` | coverage rows (the domain map — how an agent navigates) |
426
- | `search_knowledge` | `q`, `types?`, `domain?`, `limit?` | ranked candidates (fields per type below) |
427
- | `get_rule` / `get_playbook` / `get_improvement` | `repo`, `id` | full frontmatter + lazy-fetched body + cites as objects |
428
- | `list_improvements` | `repo`, `status?`, `priority?`, `domain?` | ledger rows |
429
- | `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/` |
430
- | `get_active_plan` | `repo` | resolves the channel pointer (`set_active_plan`) `get_plan`, or "no active plan" |
485
+ | Tool | Args | Returns |
486
+ | ----------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
487
+ | `get_brain_status` | `repo` | envelope + per-type counts, health, coverage summary, `activePlanId`, `ingestError?` |
488
+ | `get_coverage` | `repo` | coverage rows (the domain map — how an agent navigates) |
489
+ | `search_knowledge` | `q`, `types?`, `domain?`, `limit?` | ranked candidates (fields per type below) |
490
+ | `ask_knowledge` | `repo`, `q` | the guarded synthesis read (below) `{answer, synthesized, citations[], retrieval, usage?}` |
491
+ | `get_rule` / `get_playbook` / `get_improvement` | `repo`, `id` | full frontmatter + lazy-fetched body + cites as objects |
492
+ | `list_improvements` | `repo`, `status?`, `priority?`, `domain?` | ledger rows |
493
+ | `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/` |
494
+ | `get_active_plan` | `repo` | resolves the channel pointer (`set_active_plan`) → `get_plan`, or "no active plan" |
431
495
 
432
496
  `repo` must match the key's scope exactly; `branch` is reserved (branch-keyed
433
497
  instances land in Slice 2 — third-party clients default to the production brain,
@@ -439,8 +503,32 @@ improvement: `title, summary, lens, blast_radius` · plan: `title, plan, parent`
439
503
  Score = Σ over matched case-folded query tokens of field weight (`title` 3 ·
440
504
  `domain`/`lens`/`blast_radius`/`type`/`plan`/`parent` 2 · `summary` 1); tie-break
441
505
  score desc then id asc; fields a type lacks are omitted, never defaulted. Candidates
442
- carry `matchKind: "lexical"` (`"semantic"` reserved). `ask_knowledge` is a reserved
443
- tool id for the synthesis layer (responses will carry `synthesized: true`).
506
+ carry `matchKind: "lexical"` (`"semantic"` reserved).
507
+
508
+ **`ask_knowledge` — the guarded synthesis read (the reserved id, filled 2026-07-13
509
+ per the served-brain spec).** For THIN clients (Slack bots, dashboards); full agents
510
+ should prefer `search_knowledge` + `get_*` and compose with their own model. The
511
+ guarantees, in order: 0. **Enumerable intents route deterministically BEFORE retrieval** — command-shaped
512
+ asks ("list improvements [open|P0]", "list plans", "status/health", "coverage")
513
+ return the actual rows (`retrieval.matchKind: "intent"`, `items[]`), no LLM
514
+ involved. Interrogatives (how/why/where/when) always retrieve instead — a missed
515
+ intent degrades to search; a wrong intent would answer the wrong thing.
516
+
517
+ 1. **Retrieval first, deterministic** — the same lexical scorer, top-K candidates.
518
+ 2. **Empty = honest empty**: `{answer: null, synthesized: false, found: 0}` with a
519
+ "no knowledge found" message — never composed from general knowledge; the empty
520
+ ask is logged into the same knowledge-gaps feed as an empty search
521
+ (`payload.via: "ask"`).
522
+ 3. **Composition uses retrieved cited notes ONLY**; answers are marked
523
+ `synthesized: true` and carry `citations[]` ({id, title, kind, domain?, path}).
524
+ 4. **Raw mode**: with no org synthesis key configured, returns `rawMode: true` +
525
+ the top cited candidates (no LLM call) — the surface degrades to search, never
526
+ to silence or invention.
527
+ 5. **Custody**: synthesis runs on the ORG'S OWN LLM key (a platform tokens row,
528
+ provider `anthropic`, encrypted at rest, decrypted per call server-side, never
529
+ logged). Usage is metered to `usageEvents` (purpose `ask`, runner `platform`).
530
+ 6. **Caps are loud**: a per-repo daily cap returns an explicit error, never a
531
+ silent throttle.
444
532
 
445
533
  **Auth:** per-repo machine keys, minted on the platform, sent as
446
534
  `Authorization: Bearer`. Keys are stored hashed platform-side and live client-side in
@@ -459,15 +547,15 @@ The knowledge plane stays strictly read-only (org-brain writes go ONLY through
459
547
  files → compile → push → ingest). Seven additional tools write **collaboration
460
548
  state**, marked by `envelope.plane: "state"`:
461
549
 
462
- | Tool | Store | Notes |
463
- |---|---|---|
464
- | `list_dev_insights` / `put_dev_insight` / `remove_dev_insight` | **user brain** — account-scoped, cross-repo, PRIVATE | consent-gated by the conductor; never logged to repo activity; nothing person-scoped is ever served to another user |
465
- | `checkpoint_sync` | **branch working set** — (repo, branch, file path) rows | pushes edited/new brain files from the lazy `.rafa/` instance at CHECKPOINTS (task done · plan approved · explicit ask · cadence · git push/pull — never session-end) under **base-version CAS**: each file carries the row `version` last seen (null = create); a stale base returns the newer copy as a per-file conflict for the HUMAN in that session to resolve |
466
- | `get_working_set` | branch working set | hydration (a session starts with its branch's working set), the branch view, distillation collect; `needs-adjudication` rows carry the incoming copy in `pending` |
467
- | `resolve_working_file` | branch working set | `distilled` / `refuted` (+ cited note) at merge-to-main distillation; `keep-current` for adjudication decisions; CAS on status — a racing distill fails loudly |
468
- | `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 |
469
- | `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 |
470
- | `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 |
550
+ | Tool | Store | Notes |
551
+ | -------------------------------------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
552
+ | `list_dev_insights` / `put_dev_insight` / `remove_dev_insight` | **user brain** — account-scoped, cross-repo, PRIVATE | consent-gated by the conductor; never logged to repo activity; nothing person-scoped is ever served to another user |
553
+ | `checkpoint_sync` | **branch working set** — (repo, branch, file path) rows | pushes edited/new brain files from the lazy `.rafa/` instance at CHECKPOINTS (task done · plan approved · explicit ask · cadence · git push/pull — never session-end) under **base-version CAS**: each file carries the row `version` last seen (null = create); a stale base returns the newer copy as a per-file conflict for the HUMAN in that session to resolve |
554
+ | `get_working_set` | branch working set | hydration (a session starts with its branch's working set), the branch view, distillation collect; `needs-adjudication` rows carry the incoming copy in `pending` |
555
+ | `resolve_working_file` | branch working set | `distilled` / `refuted` (+ cited note) at merge-to-main distillation; `keep-current` for adjudication decisions; CAS on status — a racing distill fails loudly |
556
+ | `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 |
557
+ | `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 |
558
+ | `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 |
471
559
 
472
560
  State tools work with or without an ingested brain (a working set can exist —
473
561
  and a plan can be pushed — before the first scan) and are exempt from the
@@ -497,7 +585,8 @@ duties: # required · block list, ≥ 1 · each a duty DSL s
497
585
  ---
498
586
  ```
499
587
 
500
- **Duty DSL** (one string per duty, split on ` :: `, exactly three parts):
588
+ **Duty DSL** (one string per duty, split on `::`, exactly three parts):
589
+
501
590
  - `duty` — kebab-case name of the responsibility.
502
591
  - `sop-path` — path relative to the CODE repo root (e.g.
503
592
  `.claude/skills/rafa-validate/SKILL.md`); the file MUST exist at compile time (a
@@ -506,7 +595,7 @@ duties: # required · block list, ≥ 1 · each a duty DSL s
506
595
  - `bar` — the explicit pass/fail sentence for this duty. Never empty: a duty whose
507
596
  bar can't be stated can't be validated, so it doesn't ship.
508
597
 
509
- Versioning: bump `version:` with any card change; record the *why* outside the card
598
+ Versioning: bump `version:` with any card change; record the _why_ outside the card
510
599
  (cards ship to end users and stay clean of build-log clutter).
511
600
 
512
601
  ---
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // rafa hook · PostToolUse (Edit|Write|MultiEdit|NotebookEdit) — the dirty-marker.
3
+ //
4
+ // M5 capture engine, sensor #1: every code edit is recorded the moment it happens
5
+ // (monotonic, event-driven — never at session end, because there is no session end).
6
+ // Appends {f, t} to .rafa/dirty.jsonl; the SessionStart digest and `rafa dirty`
7
+ // resolve entries to the brain notes that cite those files (the codeRefs plane).
8
+ //
9
+ // Hard rules: NEVER block the session (exit 0 on every path, all errors swallowed),
10
+ // no network, no LLM, O(append). Honors RAFA_HOOKS_DISABLED=1 (headless workers /
11
+ // CI distillation set it — the recursion guard).
12
+
13
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { join, relative, isAbsolute, sep } from "node:path";
15
+
16
+ try {
17
+ if (process.env.RAFA_HOOKS_DISABLED === "1") process.exit(0);
18
+
19
+ let input = "";
20
+ try {
21
+ input = readFileSync(0, "utf8"); // stdin: the hook event JSON
22
+ } catch {
23
+ process.exit(0);
24
+ }
25
+ let evt = null;
26
+ try {
27
+ evt = JSON.parse(input);
28
+ } catch {
29
+ process.exit(0);
30
+ }
31
+
32
+ const root = process.env.CLAUDE_PROJECT_DIR || evt?.cwd || process.cwd();
33
+ if (!existsSync(join(root, "rafa.json"))) process.exit(0); // not a rafa-provisioned repo
34
+
35
+ const fp = evt?.tool_input?.file_path || evt?.tool_input?.notebook_path;
36
+ if (!fp || typeof fp !== "string") process.exit(0);
37
+
38
+ const rel = isAbsolute(fp) ? relative(root, fp) : fp;
39
+ // Outside the repo, or not code-plane: knowledge/state/tooling edits don't dirty the brain.
40
+ if (rel.startsWith("..") || isAbsolute(rel)) process.exit(0);
41
+ const top = rel.split(sep)[0];
42
+ if ([".rafa", ".claude", ".git", "node_modules"].includes(top) || rel === "rafa.json") process.exit(0);
43
+
44
+ const rafaDir = join(root, ".rafa");
45
+ mkdirSync(rafaDir, { recursive: true });
46
+
47
+ // Transport exclusion, belt-and-braces: the queue is session-local state and must
48
+ // never ride a brain push (`rafa push` commits .rafa/ wholesale). ensureBrainRepo
49
+ // writes this too — this covers the window before any pull/push ran here.
50
+ const gi = join(rafaDir, ".gitignore");
51
+ const body = existsSync(gi) ? readFileSync(gi, "utf8") : "";
52
+ if (!/^dirty\.jsonl$/m.test(body))
53
+ writeFileSync(gi, body + (body === "" || body.endsWith("\n") ? "" : "\n") + "dirty.jsonl\n");
54
+
55
+ appendFileSync(
56
+ join(rafaDir, "dirty.jsonl"),
57
+ JSON.stringify({ f: rel.split(sep).join("/"), t: new Date().toISOString() }) + "\n",
58
+ );
59
+ } catch {
60
+ /* a sensor must never take down the session */
61
+ }
62
+ process.exit(0);
@@ -0,0 +1,24 @@
1
+ #!/bin/sh
2
+ # rafa pre-push hook v1 (installed by @rafinery/cli init/pull/update — safe to
3
+ # delete; it will be offered again, never forced over a foreign hook).
4
+ #
5
+ # M5 capture engine, sensor #3: `git push` of the CODE branch is a ratified
6
+ # natural checkpoint boundary — so the branch WORKING SET (edited/new brain
7
+ # files) syncs to the platform deterministically here, not only when a session
8
+ # remembers its SOP. Non-blocking by design: a checkpoint problem must never
9
+ # stop a code push (exit 0 on every path; conflicts land as .theirs.md copies
10
+ # for the next session to decide — printed, never auto-resolved).
11
+
12
+ [ "$RAFA_HOOKS_DISABLED" = "1" ] && exit 0
13
+ [ -f "rafa.json" ] || exit 0
14
+
15
+ echo "rafa · checkpoint (pre-push boundary) …"
16
+ npx -y @rafinery/cli checkpoint || {
17
+ code=$?
18
+ if [ "$code" = "2" ]; then
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
+ else
21
+ echo "rafa · checkpoint skipped (exit $code) — push continues; run 'rafa checkpoint' manually when convenient."
22
+ fi
23
+ }
24
+ exit 0