@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.
- package/CHANGELOG.md +97 -0
- package/bin/rafa.mjs +37 -5
- package/blueprint/.claude/agents/atlas.md +2 -1
- package/blueprint/.claude/agents/sage.md +66 -0
- package/blueprint/.claude/commands/rafa.md +214 -269
- package/blueprint/.claude/rafa/contract.md +204 -115
- package/blueprint/.claude/rafa/hooks/post-tool.mjs +62 -0
- package/blueprint/.claude/rafa/hooks/pre-push +24 -0
- package/blueprint/.claude/rafa/hooks/session-start.mjs +229 -0
- package/blueprint/.claude/rafa/hooks/statusline.mjs +113 -0
- package/blueprint/.claude/rafa/hooks/user-prompt-submit.mjs +87 -0
- package/blueprint/.claude/skills/rafa-build/SKILL.md +20 -5
- package/blueprint/.claude/skills/rafa-distill/SKILL.md +6 -1
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +7 -0
- package/blueprint/.claude/skills/rafa-sage/SKILL.md +201 -0
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +55 -5
- package/blueprint/.claude/skills/rafa-validate/SKILL.md +15 -2
- package/lib/benchmark.mjs +573 -0
- package/lib/blueprint.mjs +11 -1
- package/lib/brain-repo.mjs +10 -4
- package/lib/ci-setup.mjs +2 -0
- package/lib/claude-config.mjs +77 -0
- package/lib/dirty.mjs +114 -0
- package/lib/distill.mjs +4 -0
- package/lib/gate/compile.mjs +293 -44
- package/lib/gate/verify-citations.mjs +214 -23
- package/lib/githook.mjs +54 -0
- package/lib/init.mjs +18 -0
- package/lib/pull.mjs +7 -0
- package/lib/push.mjs +21 -0
- package/lib/reflex.mjs +76 -0
- package/lib/releases.mjs +35 -0
- package/lib/status.mjs +152 -0
- package/lib/update.mjs +13 -0
- 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
|
|
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
|
|
39
|
-
|
|
40
|
-
| rule
|
|
41
|
-
| playbook
|
|
42
|
-
| health
|
|
43
|
-
| coverage
|
|
44
|
-
| improvement
|
|
45
|
-
| ledger
|
|
46
|
-
| plan
|
|
47
|
-
| log
|
|
48
|
-
| citation-check
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
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
|
|
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**
|
|
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",
|
|
125
|
-
"repo": "owner/repo",
|
|
126
|
-
"branch": "main",
|
|
127
|
-
"codeSha": "abc123…",
|
|
128
|
-
|
|
129
|
-
"health": {
|
|
130
|
-
|
|
131
|
-
"
|
|
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": {
|
|
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": {
|
|
143
|
-
|
|
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",
|
|
153
|
-
"type": "contract",
|
|
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"],
|
|
158
|
-
"failure": "silent",
|
|
159
|
-
"
|
|
160
|
-
|
|
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",
|
|
168
|
-
"lens": "product",
|
|
169
|
-
"status": "open",
|
|
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" },
|
|
174
|
-
"blastRadius": ["web-agent-bridge"],
|
|
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
|
|
201
|
-
type: contract
|
|
202
|
-
domain: web-agent-bridge
|
|
203
|
-
title: The agent name is a cross-process contract
|
|
204
|
-
summary: Only "research_agent" is fully wired end to end
|
|
205
|
-
links: [copilotkit-runtime-route-convention]
|
|
206
|
-
failure: silent
|
|
207
|
-
|
|
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
|
|
222
|
-
priority: P1
|
|
223
|
-
lens: product
|
|
224
|
-
status: open
|
|
225
|
-
title: Two of the four model options are dead and fail silently
|
|
226
|
-
summary: google_genai and crewai route to unwired agents
|
|
227
|
-
fix: Wire both agents in main.py, or remove them from the picker
|
|
228
|
-
leverage: { impact: high, effort: low }
|
|
229
|
-
blast_radius: [web-agent-bridge, external-integrations]
|
|
230
|
-
cites:
|
|
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
|
|
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
|
|
248
|
-
score: 95
|
|
249
|
-
gates: { fidelity: pass, coverage: pass }
|
|
250
|
-
counts: { blockers: 0, majors: 0, minors: 2 }
|
|
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
|
|
304
|
-
plan: payments
|
|
305
|
-
parent: payments-c3
|
|
306
|
-
kind: epic | task | subtask
|
|
307
|
-
title: Handle provider webhooks
|
|
308
|
-
description:
|
|
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
|
|
311
|
-
status: todo | in-progress | done | superseded | abandoned
|
|
312
|
-
assignee: rohik
|
|
313
|
-
blocked_by: [payments-c2]
|
|
314
|
-
blocked_reason: "vendor sandbox access"
|
|
315
|
-
priority: 2
|
|
316
|
-
estimate: 3
|
|
317
|
-
branch: feat/webhooks
|
|
318
|
-
baseSha: abc123
|
|
319
|
-
domains: [payments]
|
|
320
|
-
external: { provider: jira, key: PAY-142 }
|
|
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 }` —
|
|
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
|
|
400
|
-
the
|
|
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>",
|
|
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
|
|
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
|
|
423
|
-
|
|
424
|
-
| `get_brain_status`
|
|
425
|
-
| `get_coverage`
|
|
426
|
-
| `search_knowledge`
|
|
427
|
-
| `
|
|
428
|
-
| `
|
|
429
|
-
| `
|
|
430
|
-
| `
|
|
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).
|
|
443
|
-
|
|
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
|
|
463
|
-
|
|
464
|
-
| `list_dev_insights` / `put_dev_insight` / `remove_dev_insight`
|
|
465
|
-
| `checkpoint_sync`
|
|
466
|
-
| `get_working_set`
|
|
467
|
-
| `resolve_working_file`
|
|
468
|
-
| `fold_working_set`
|
|
469
|
-
| `push_plan` / `list_plans` / `update_plan_status` / `set_active_plan` / `log_decision` | **plans** — (repo) work objects
|
|
470
|
-
| `report_improvement_status`
|
|
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
|
|
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
|
|
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
|