@rafinery/cli 0.4.1 → 0.5.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 CHANGED
@@ -4,6 +4,53 @@ 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.5.0 — plans v2: the work-item tree
8
+
9
+ **Adopt with:** `npx @rafinery/cli@latest update` → it runs the mechanical `rafa migrate`
10
+ (plans data v1→v2: `parent`→`epic`, `child`→`task`, stored `blocked`→`todo` +
11
+ `blocked_reason`). Brain data unchanged; no re-scan. Re-push plans once so the platform
12
+ rows pick up the tree + glimpse fields.
13
+
14
+ - **The tree**: one epic → tasks → subtasks (three ranks — matching Linear/Jira/Asana's
15
+ ceiling so tracker sync stays lossless). Compile validates ranks, single root,
16
+ `blocked_by` resolution + acyclicity.
17
+ - **Vendor-blended vocabulary** (checked against the real APIs): `title` ·
18
+ `description` (1:1 with vendor description/notes) · `approach` (rafa's how-line) ·
19
+ `assignee` · `blocked_by` (a dependency IS a blocker) · `blocked_reason` ·
20
+ `priority` 0–4 (Linear scale) · `estimate` · `external {provider, key, url}`
21
+ (read-only tracker identity; provider-closed ≠ done — K6).
22
+ - **`blocked` is DERIVED, never stored** — the status enum is
23
+ todo|in-progress|done|superseded|abandoned; the platform renders a blocked chip from
24
+ unresolved `blocked_by`/`blocked_reason`.
25
+ - **Decisions are first-class**: `log_decision` (at · actor · context · options ·
26
+ decision · rationale — paraphrase + pivotal quotes) with the `## Decisions` body
27
+ section as the human mirror; rendered as a timeline on the plan detail page.
28
+ - Platform: glimpse cards (what/why/how/who/blocked/deps at a glance), tree detail
29
+ page, board blocked-chip (the Blocked column is gone — it's a condition, not a state).
30
+
31
+ ## 0.4.2
32
+
33
+ **Adopt with:** `npx @rafinery/cli@latest update` — file re-sync only (skills + contract
34
+ moved a few lines); no migration, no re-scan.
35
+
36
+ Owner E2E feedback round (research-canvas):
37
+
38
+ - **`rafa init` offers CI reconciliation** at provisioning (TTY only): yes runs
39
+ `ci-setup`, no prints the session-fallback path. Nothing is silently installed —
40
+ the workflow runs on the org's own secrets.
41
+ - **`rafa distill --headless` is observable**: streams the agent's narration lines and
42
+ prints per-phase timings (brain mirror · agent turns/cost · gates · total), so a CI
43
+ log shows liveness and where the time goes. Failure semantics unchanged: any
44
+ incomplete verdict or failed gate aborts with the working set intact.
45
+ - **Plans link to their brain slice**: optional parent-only `domains:` (contract §7) —
46
+ the blast radius named at plan time rides `push_plan`; the platform's new plan
47
+ detail page renders the plan's related knowledge beside its tasks + `## Log`
48
+ journals. Compile validates the shape (child `domains:` fails loudly).
49
+ - **Live improvement signal** (platform + SOP): bloom's fixed-in-passing during build
50
+ now also calls `report_improvement_status` — event-only (the ledger row keeps its
51
+ single writer, the ingest); the Improvements page shows it as *pending
52
+ reconciliation* until the next brain push confirms.
53
+
7
54
  ## 0.4.1
8
55
 
9
56
  **Adopt with:** `npx @rafinery/cli@latest <cmd>` — no file re-sync needed (the fix is inside
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: atlas
3
- version: 3.5.0
3
+ version: 3.6.0
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: >-
@@ -10,7 +10,7 @@ description: >-
10
10
  when a codebase must be scanned, a brain repaired after validation, a plan
11
11
  drafted from recalled knowledge, or a plan's task implemented. Runs
12
12
  context-isolated; comprehensive, cited, never cherry-picked.
13
- tools: Read, Write, Edit, Bash, Grep, Glob, TodoWrite, mcp__rafinery
13
+ tools: Read, Write, Edit, Bash, Grep, Glob, TodoWrite, Skill, mcp__rafinery
14
14
  color: blue
15
15
  duties:
16
16
  - "scan :: .claude/skills/rafa-scan/SKILL.md :: comprehensive breadth-first cited brain · verify-citations exits 0 · coverage honest (thin/gap stated, never hidden)"
@@ -84,9 +84,13 @@ Never act cold; never over-load.
84
84
  **never open `.env`/`.env.*` or any secret store, and never read or copy a value.** A key's
85
85
  *name* is a contract; its *value* is a secret. If a note genuinely needs a value, stop and ask
86
86
  the dev — don't harvest it. (Enforced in [the scan skill](../skills/rafa-scan/SKILL.md) step 4.)
87
- - **Orchestrate, don't bury.** At work-time, ride the host + the dev's existing skills/tools/MCP
88
- and recommend the best-fit one; never reinvent a capability that already exists. rafa conducts
89
- what's present.
87
+ - **Toolbox-first execution (automatic never an offer).** Before implementing any
88
+ task step, CHECK the repo's installed toolbox committed `.claude/skills/`,
89
+ `.mcp.json` servers, commands — for a capability that already does it, and INVOKE
90
+ it (Skill tool / MCP) instead of hand-rolling. The conductor passes the matching
91
+ inventory slice in your spawn prompt; consult it first. Only what is actually
92
+ installed — never guess a capability into existence. Record the choice in the
93
+ item's `approach` ("how: via the <x> skill").
90
94
 
91
95
  ## Style
92
96
  Dense, no filler, no praise. Short plan before acting. Bracketed status
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: compass
3
- version: 0.2.1
3
+ version: 0.3.0
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: >-
@@ -16,6 +16,8 @@ duties:
16
16
  - "bootstrap :: .claude/skills/rafa-insights/SKILL.md :: native /insights report distilled with judgment (never parsed mechanically) into dev-level candidates — each OFFERED, banked only on yes"
17
17
  - "continuous-refinement :: .claude/skills/rafa-insights/SKILL.md :: session observations routed by the boundary (dev-level here, code-level to the branch working set) · update beats duplicate · every insight legible, correctable, deletable on request"
18
18
  - "steering :: .claude/skills/rafa-insights/SKILL.md :: propose-dev-disposes at natural boundaries · at most one nudge · dismissal is final for the session · never rank, never compare, never nag"
19
+ - "leverage-coaching :: .claude/skills/rafa-insights/SKILL.md :: the one boundary nudge may be sourced from toolbox × patterns (repo inventory + the dev's tooling insights) — always HAS a candidate ready, never raises nudge frequency; only capabilities actually installed"
20
+ - "tooling-capture :: .claude/skills/rafa-insights/SKILL.md :: personal ~/.claude tooling (names + descriptions ONLY) banked as kind:tooling insights under STANDING consent · legible/deletable · never enters org stores"
19
21
  ---
20
22
 
21
23
  # compass — the dev's coach
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 1.3.0
2
+ version: 1.5.0
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
 
@@ -28,6 +28,20 @@ unless explicitly asked (serve knowledge, not gossip). Misroutes the dev correct
28
28
  signal — prefer under-routing (answer plainly) over dragging a question through
29
29
  choreography.
30
30
 
31
+ **Zero-command principle (owner rule, 2026-07-12):** every rafa command — CLI verbs,
32
+ MCP tools, checkpoints, pushes, decisions logging — is AGENT-INTERNAL machinery. The
33
+ dev speaks intent; YOU run the machinery. Never instruct the dev to run a command you
34
+ can run yourself. The only human acts: init (provision) · `npx rafa pull` (once per
35
+ clone) · ci-setup approval · npm publish. **Toolbox is a recall surface:** at
36
+ bootstrap, load the committed toolbox (`.claude/skills/`, `.mcp.json`, commands)
37
+ alongside brain status; pass the matching slice into every atlas/bloom spawn prompt —
38
+ agents USE the fit capability automatically instead of hand-rolling (only what's
39
+ installed, never guessed; personal `~/.claude/` stays live-recommend-only).
40
+ **Actor envelope:** stamp `actorMeta {model, agent, runner: "session"}` on every
41
+ state-plane write (checkpoints, plan pushes, decisions, reports) and export
42
+ `RAFA_ACTOR_MODEL` before CLI calls — the platform records WHO/WHAT executed;
43
+ "unreported" when unknown, never guessed.
44
+
31
45
  **Scope discipline (owner rule):** deliver exactly what the message asked — fix what
32
46
  they're expecting, nothing else. No unsolicited extras beyond bloom's single opt-in
33
47
  nudge. Fix-shaped prompts: recall the failure domain's knowledge → fix the asked
@@ -49,6 +63,10 @@ Three interaction modes by the weight of the action:
49
63
  - session start, local ahead of platform → *"brain is N commits behind — push?"*
50
64
  - reconciliation pending → *"branch <x> merged with N working-set files — distill now?"*
51
65
  - adjudication pending → *"1 file diverged at the <x>→<y> fold — pick a copy?"*
66
+ - user brain EMPTY (first session / founding scan just completed) → *"want me to
67
+ bootstrap your insights from your usage report? (compass distills it; every
68
+ candidate is offered — nothing banked without your yes)"* — accepted = run
69
+ `## insights`. Offered ONCE; a "no" is a standing no until the dev asks.
52
70
 
53
71
  Offer etiquette (hard rules): boundaries only, never mid-flow · once per moment — a
54
72
  "no" is remembered for the session and never re-argued · an unanswered offer never
@@ -105,6 +123,11 @@ improve + push — a cheap knowledge refresh).
105
123
  explicit approval**, push the brain: `npx @rafinery/cli push` — commits `.rafa/` and
106
124
  pushes to the brain remote using the dev's own git auth, stamped `brain-for: <code sha>`.
107
125
  Never push without approval.
126
+ 8. **The coach offer (founding scan only)** — if this was the repo's FIRST scan and the
127
+ dev's user brain is empty (`list_dev_insights` → none), offer ONCE: *"the code side
128
+ is mapped — want me to bootstrap YOUR insights from your usage report?"* Accepted =
129
+ run `## insights` (compass; every candidate offered, banked only on yes). This is
130
+ the offer rung of the consent ladder — never auto-run, never re-asked after a no.
108
131
 
109
132
  atlas scans + fixes; prism judges; bloom improves; you orchestrate + push on approval. You own
110
133
  `log.md`; never edit `checklist.md` or the brain yourself — spawn atlas for that.
@@ -146,12 +169,15 @@ atlas drafts, bloom pulls, prism validates the plan itself:
146
169
  Per [the rafa-build skill](../skills/rafa-build/SKILL.md). Resume from `active.md`
147
170
  (or the platform's `get_active_plan` when connected — "load plan X" materializes it) —
148
171
  never re-derive context.
149
- Per task: **atlas** recalls + implements **prism** validates against the child's
150
- `## Done-check` (**`status: done` only on prism PASS**; FAIL atlas corrects) → **bloom**
151
- sweeps (new opportunities ledger files; fixed-in-passing`status: fixed`; one opt-in
152
- nudge max) update the child file's `status` + `## Log`at CHECKPOINTS (task done ·
153
- cadence under session consent): `push_plan`/`update_plan_status` (plans channel) +
154
- `rafa checkpoint` (working set) so the platform reflects live progress. Terminal
172
+ Per item (work the tree in `blocked_by` order an item with an unmet dependency is
173
+ DERIVED-blocked and waits): **atlas** recalls + implements**prism** validates against
174
+ the item's `## Done-check` (**`status: done` only on prism PASS**; FAIL atlas corrects)
175
+ **bloom** sweeps (new opportunities ledger files; fixed-in-passing`status: fixed`;
176
+ one opt-in nudge max) → update the item file's `status` + `## Log` + `## Decisions` →
177
+ at CHECKPOINTS (task done · cadence under session consent):
178
+ `push_plan`/`update_plan_status`/`log_decision` (plans channel — decisions record what
179
+ the DEV chose and why; paraphrase + pivotal quotes) + `rafa checkpoint` (working set)
180
+ so the platform reflects live progress. Terminal
155
181
  statuses when honest: `superseded` (replaced by a newer plan) · `abandoned` (dropped).
156
182
  If execution invalidates/creates brain knowledge, route per §capture (working set on a
157
183
  branch; full `## scan` on main — never hand-edit main's brain around the gate). When all
@@ -288,63 +288,94 @@ domains: { design-system: mapped, components: mapped, api: thin, external-integr
288
288
 
289
289
  ---
290
290
 
291
- ## 7. Plan files — `plans/**/*.md` (+ the plans channel)
291
+ ## 7. Plan files — `plans/**/*.md` (+ the plans channel) — **v2: the work-item tree**
292
292
 
293
- Each child owns exactly one file (so merges never conflict; parent progress is
294
- derived by counting).
293
+ Plans are a TREE of work items (plans data version 2, ratified 2026-07-12): one
294
+ **epic** (the root) → **tasks** → **subtasks**. Three ranks, deliberately matching
295
+ the ceiling of Linear (Project→Issue→sub-issue), Jira (Epic→Task→Sub-task), and
296
+ Asana (Project→Task→Subtask) so tracker sync is lossless. Vocabulary is
297
+ vendor-blended: field names below are the majority vendor names wherever one exists.
298
+ Each item owns exactly one file (merges never conflict; progress is derived).
295
299
 
296
300
  ```yaml
297
301
  ---
298
302
  schemaVersion: 1
299
- id: multitenant-c1-schema # required · == filename stem · GLOBALLY unique across plans/**
300
- plan: multitenant # required · the parent plan id
301
- parent: multitenant # required · parent plan id (child), or null (root)
302
- kind: parent | child # required
303
- title: # required
304
- status: todo | in-progress | done | blocked | superseded | abandoned # required (child); parent may omit
305
- branch: feat/mt-c1 # optional · the branch this executes on
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
309
+ 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
306
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
307
321
  ---
308
- (prose body — for a child it MUST contain a `## Done-check` section: the expected
309
- outcome prism validates execution against. Compile never parses bodies; a missing
310
- Done-check is rejected by prism's PLAN validation, not by compile.)
322
+ (prose body — a LEAF item (a task with no subtasks, or a subtask) MUST carry a
323
+ `## Done-check`; `## Log` is the execution journal; `## Decisions` mirrors the
324
+ structured decision records (below). Compile never parses bodies.)
311
325
  ```
312
326
 
313
327
  Rules (all compile-enforced unless noted):
314
328
 
315
- - **Global id uniqueness.** `plans/**` is nested, so stems could collide; a duplicate
316
- plan id anywhere under `plans/` is a compile error. Convention: prefix children
317
- with the plan id (`<plan>-c1-….md`).
318
- - **`kind: parent`** `parent` MUST be `null` and `plan` MUST equal `id`.
319
- - **`kind: child`** `plan` MUST resolve to a `kind: parent` file in the same
320
- compile, and `parent` MUST equal `plan` (flat parent→children in v1). A dangling
321
- child is a loud error; a parent with zero children is valid (a plan just created).
322
- - **Progress is never stored.** A `progress` key in plan frontmatter is a compile
323
- error. Parent progress is `count(children where status == done) / count(children)`
324
- derived at read time, everywhere (compile, platform, MCP).
325
- - **Statuses.** `done` exists only on prism PASS (the execution gate). The two
326
- TERMINAL statuses close work honestly without pretending `done`:
327
- `superseded` (a newer plan replaces it) · `abandoned` (deliberately dropped).
328
- `blocked` is waiting, not terminal. Statuses are set in sessions; the platform
329
- board renders them read-only.
330
- - **`active.md` `activePlanId`.** Compile reads the generated pointer: a first line
331
- `# <plan-id>` must resolve to a `kind: parent` plan (else compile error);
332
- `# No active plan` or a missing `active.md` `activePlanId: null` (documented
333
- default the pointer is generated, absence means "none", never a guess of one).
334
- The PLATFORM pointer is set separately through `set_active_plan` (below).
335
-
336
- **Transport the plans channel (0.4.0; plans never ride the brain manifest):**
337
-
338
- - **Authoring**: plans are drafted as the files above (compile-validated
339
- determinism holds; `.rafa/plans/` is the working copy).
340
- - **Push on approval**: the dev's approval IS the trigger — `push_plan` sends the
341
- whole plan (parent + children + bodies) to the platform; `set_active_plan`
342
- points the repo's active pointer at it. Build cadence re-pushes statuses +
343
- `## Log` journals (`push_plan` upsert / `update_plan_status`).
344
- - **Pull-based work**: `list_plans` returns NAMES ONLY (id · title · status ·
345
- progress); **"load plan X"** = `get_plan` (bodies included) materialized back
346
- into `.rafa/plans/` by the session. Nothing plan-shaped is hydrated until
347
- explicitly loaded.
329
+ - **Global id uniqueness** across `plans/**` (duplicate = error). Convention: prefix
330
+ descendants with the epic id.
331
+ - **Tree shape.** Exactly ONE `kind: epic` per plan; `epic` → `parent: null`, `plan == id`.
332
+ A task's `parent` is the epic; a subtask's `parent` is a task. Every item's `plan`
333
+ is the root epic id. Dangling parents and rank violations are loud errors.
334
+ - **`blocked` is DERIVED, never stored.** An item is blocked when it has an
335
+ unresolved `blocked_by` (a listed item not yet `done`) or a `blocked_reason`.
336
+ `status: blocked` is a compile ERROR (removed in v2 `rafa migrate` converts).
337
+ No vendor stores blocked as a state either (Linear: relations · Jira: links +
338
+ Flagged · Asana: dependencies) deriving it keeps sync lossless.
339
+ - **`blocked_by` ids resolve within the plan and are acyclic.**
340
+ - **Progress is never stored.** A `progress` key is a compile error. Progress =
341
+ `count(done LEAVES) / count(leaves)` per subtree derived everywhere.
342
+ - **Statuses.** `done` exists only on prism PASS (the execution gate). Terminal:
343
+ `superseded` (replaced by a newer plan) · `abandoned` (deliberately dropped) —
344
+ mapping losslessly to Linear `canceled` / Jira resolution "Duplicate"/"Won't do".
345
+ Statuses are set in sessions; the platform board renders them read-only.
346
+ - **`external` is read-only to sessions** the sync layer owns it. When a provider
347
+ closes a ticket, the item shows DUAL status (provider's + ours); `done` is never
348
+ imported (K6: done is earned, not synced).
349
+ - **`active.md` → `activePlanId`.** First line `# <plan-id>` must resolve to a
350
+ `kind: epic`; `# No active plan` / missing file null.
351
+
352
+ **Decisions first-class deliberation records (channel-borne, never frontmatter):**
353
+ the frontmatter grammar is flat and bodies are never parsed, so structured decisions
354
+ live on the PLATFORM, pushed via `log_decision` at checkpoint moments:
355
+ `{ item, at, actor, context, options[], decision, rationale }` *actor* is who
356
+ decided (the dev for steering; the agent for proposals it made under standing
357
+ consent). Text fidelity: **paraphrase + short verbatim quotes only where the exact
358
+ wording carries the decision** (transcripts never land in shared stores). Sessions
359
+ mirror each record into the item body's `## Decisions` for human reading. On tracker
360
+ sync, decisions flow OUT as prefixed comments (Jira/Linear comments, Asana stories);
361
+ imported comments are never scraped into decisions.
362
+
363
+ **Transport — the plans channel (plans never ride the brain manifest):**
364
+
365
+ - **Authoring**: files above, compile-validated; `.rafa/plans/` is the working copy.
366
+ - **Push on approval**: approval IS the trigger — `push_plan` sends the whole tree
367
+ (`items[]`, bodies included); `set_active_plan` points the pointer. Build cadence
368
+ re-pushes statuses + `## Log` (`push_plan` / `update_plan_status`) and logs
369
+ decisions (`log_decision`).
370
+ - **Pull-based work**: `list_plans` = NAMES ONLY; **"load plan X"** = `get_plan`
371
+ (bodies + decisions) materialized back into `.rafa/plans/` by the session.
372
+ - **The brain link**: the epic's `domains:` rides the channel; the plan detail page
373
+ renders the related knowledge beside the tree.
374
+ - **Tracker sync (phased)**: `external` identity now → import (a ticket is
375
+ purpose/what, NOT an executable plan — it still flows through plan drafting +
376
+ prism before build) → two-way sync under a field-ownership matrix (provider owns
377
+ ticket-native fields; rafa owns execution fields; conflicts surface, never
378
+ auto-resolve).
348
379
 
349
380
  ---
350
381
 
@@ -435,7 +466,8 @@ state**, marked by `envelope.plane: "state"`:
435
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` |
436
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 |
437
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 |
438
- | `push_plan` / `list_plans` / `update_plan_status` / `set_active_plan` | **plans** — (repo) work objects | the dedicated push-on-approval channel (§7): approval pushes the whole plan (bodies stored); `list_plans` = names only; statuses push back from sessions; the active pointer is explicit, never inferred |
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 |
439
471
 
440
472
  State tools work with or without an ingested brain (a working set can exist —
441
473
  and a plan can be pushed — before the first scan) and are exempt from the
@@ -18,7 +18,7 @@ platform MCP (one read path — the same surface any third-party agent uses).
18
18
  |---|---|---|
19
19
  | **Executor** | atlas | RECALL the task's brain slice via MCP (`search_knowledge` + `get_rule`/`get_playbook`; honor non-exemplars) → implement, convention-adherent |
20
20
  | **Validator** | prism | validate the execution against the child's `## Done-check` — strict, unbiased, against code + brain, never against atlas's claims. **`status: done` only on prism PASS**; FAIL → atlas corrects (validate-and-correct at work time). This gate is conductor-flow SOP — deterministic enforcement is the deferred capture-engine's job |
21
- | **Improver** | bloom | **push**: new improvement opportunities spotted during execution → new ledger files. **close**: improvements fixed in passing → `status: fixed`. **nudge**: top-leverage open item in the task's blast radius — opt-in, never blocking |
21
+ | **Improver** | bloom | **push**: new improvement opportunities spotted during execution → new ledger files. **close**: improvements fixed in passing → `status: fixed` in the ledger file + `report_improvement_status(id, fixed)` so the platform shows it LIVE as pending-reconciliation (the ledger row itself changes only at the next brain push — K1). **nudge**: top-leverage open item in the task's blast radius — opt-in, never blocking |
22
22
 
23
23
  ## Procedure
24
24
 
@@ -40,6 +40,11 @@ platform MCP (one read path — the same surface any third-party agent uses).
40
40
  parsed; the plan files at `.rafa/plans/<plan>/` ARE the local cache) → at the
41
41
  task-done CHECKPOINT (under the session consent): `push_plan` /
42
42
  `update_plan_status` (the plans channel — statuses + Log journals) +
43
+ **`log_decision` for each deliberation since the last checkpoint** — what
44
+ came up, what was considered, what the DEV chose, why (actor = the dev for
45
+ steering, the agent for its own proposals; PARAPHRASE + short verbatim
46
+ quotes only where the wording carries the decision — transcripts never land
47
+ in shared stores; mirror each into the item's `## Decisions` section) +
43
48
  `rafa checkpoint` (the branch working set), so the platform and every MCP
44
49
  consumer reflect live progress. Checkpoint moments: task done · plan
45
50
  approved · explicit ask · cadence · git push/pull — never session-end. A
@@ -58,9 +58,20 @@ code-level → the branch working set (not yours — rafa.md §capture routes it
58
58
  existing insight (`put_dev_insight` with its id) rather than accreting near-
59
59
  copies.
60
60
 
61
+ ### 2b. Personal tooling (ratified 2026-07-12 — the user-plane toolbox)
62
+ Under the dev's STANDING insights consent, inventory the tooling they carry
63
+ across repos — `~/.claude/skills` + user-level MCPs — and bank each as a
64
+ `kind: tooling` insight (NAME + DESCRIPTION only, never file contents). Update
65
+ beats duplicate; tombstones honored. This is what lets rafa orchestrate a
66
+ dev's personal migration skill in a repo that has never seen it. The REPO
67
+ toolbox is atlas's plane (scan's toolbox domain), never yours.
68
+
61
69
  ### 3. Steering (the payoff — propose, the dev disposes)
62
- At natural boundaries only, at most one nudge: surface the insight that changes
63
- what the dev does next — "you've corrected X three times: want it banked as a
70
+ At natural boundaries only, at most one nudge and ALWAYS have a candidate
71
+ ready (leverage-coaching: cross the repo's toolbox inventory with the dev's
72
+ patterns + tooling insights — "you've hand-run this migration three times; the
73
+ repo ships a skill for it"). Availability is always; frequency never rises.
74
+ Surface the insight that changes what the dev does next — "you've corrected X three times: want it banked as a
64
75
  rule?" · "this decomposition worked before, reuse it?" · "you never use plan
65
76
  mode on big changes; it would have caught this." Dismissal is final for the
66
77
  session. Never rank, never compare devs, never nag.
@@ -37,9 +37,17 @@ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
37
37
  1. **Staleness check** — compare the platform envelope's `brainForSha` against the
38
38
  local brain stamp; if the platform is behind, surface "run `rafa push`" (never
39
39
  proceed silently on knowledge you know is stale — never block either).
40
- 2. **Recall** (atlas, via MCP) → **decompose** ADR-style: decision + rationale +
41
- alternatives + risk surface + non-goals; tasks bound to domains with a
42
- `## Done-check` each.
40
+ 2. **Recall** (atlas, via MCP) → **decompose** into the WORK-ITEM TREE (contract
41
+ §7 v2): one epic tasks subtasks (three ranks, never deeper). Every item
42
+ carries the glimpse fields — `title` (what) · `description` (why) ·
43
+ `approach` (how, one line) · `assignee` when known · `blocked_by` for
44
+ intra-plan dependencies (a dependency IS a blocker; blocked is DERIVED,
45
+ never a status) · optional `priority` 0–4 / `estimate`. Every LEAF carries a
46
+ `## Done-check`. The blast radius goes on the EPIC's `domains:` — it rides
47
+ `push_plan` and the platform renders the plan's brain slice beside it.
48
+ ADR material (alternatives, risks, non-goals) lives in the epic body AND
49
+ the pivotal choices are logged as DECISIONS at approval (`log_decision`:
50
+ context · options · decision · rationale; actor = the dev for their calls).
43
51
  3. **Ledger pull** (bloom) → optional leverage tasks in the blast radius.
44
52
  4. **Leverage-match** — recommend existing skills/tools/MCP that fit the tasks;
45
53
  never plan to hand-roll what a capability already does.
@@ -138,6 +138,15 @@ retrieval index. Bodies read like a senior engineer explaining that one concept
138
138
  the internal node/relationship model the next steps reason over. (Not emitted as a
139
139
  file; it feeds the notes.)
140
140
 
141
+ 3b. **Toolbox inventory** [deterministic] — ratified 2026-07-12: the REPO toolbox is a
142
+ first-class brain domain (`toolbox`). Run `npx @rafinery/cli leverage --json` (the
143
+ deterministic extractor) and author cited notes for the committed toolbox — skills
144
+ (`.claude/skills/*/SKILL.md` name+description), commands, `.mcp.json` servers,
145
+ granted permissions — cites into the config files themselves (contract §2; they are
146
+ citable file:line). This makes the toolbox recallable through the SAME MCP surface
147
+ as all knowledge and refreshable like any note. Personal `~/.claude/` is NEVER
148
+ inventoried here (user-profile plane, compass's job, standing consent).
149
+
141
150
  4. **Convention detection** [deterministic + light LLM] — for **each** domain, the local
142
151
  idiom (design tokens, component export pattern + any server/client split such as RSC,
143
152
  data-op shape, naming).
@@ -84,6 +84,13 @@ export default async function checkpoint() {
84
84
  try {
85
85
  payload = await callTool(ROOT, "checkpoint_sync", {
86
86
  branch,
87
+ // WHO/WHAT executed — the CLI knows itself; the MODEL is the session's to
88
+ // report (RAFA_ACTOR_MODEL env, set by the conductor) — never guessed.
89
+ actorMeta: {
90
+ agent: "rafa-cli@" + (process.env.npm_package_version ?? "0.5.0"),
91
+ runner: process.env.CI ? "ci" : "session",
92
+ model: process.env.RAFA_ACTOR_MODEL ?? "unreported",
93
+ },
87
94
  files: candidates.map(({ path, content, baseVersion, baseBrainSha }) => ({
88
95
  path,
89
96
  content,
@@ -13,6 +13,42 @@ export const RAFA_PERMISSIONS = [
13
13
  "Bash(rafa:*)",
14
14
  ];
15
15
 
16
+ // Merge the repo's .mcp.json server names into the OWNED agent cards' tools
17
+ // lines (ratified 2026-07-12: subagents must be able to reach the repo's MCPs
18
+ // — a Convex repo's mcp__convex tools were structurally invisible to atlas).
19
+ // Owned-file edit, idempotent, reviewable in git diff; blueprint stays generic.
20
+ export function mergeRepoMcpsIntoCards(targetDir) {
21
+ const mcp = readClaudeSettingsFile(join(targetDir, ".mcp.json"));
22
+ const servers = Object.keys(mcp?.mcpServers ?? {}).filter((n) => n !== "rafinery");
23
+ if (servers.length === 0) return { updated: [] };
24
+ const updated = [];
25
+ const dir = join(targetDir, ".claude", "agents");
26
+ if (!existsSync(dir)) return { updated };
27
+ for (const card of ["atlas.md", "bloom.md", "prism.md", "compass.md"]) {
28
+ const file = join(dir, card);
29
+ if (!existsSync(file)) continue;
30
+ let text = readFileSync(file, "utf8");
31
+ const m = text.match(/^tools: (.+)$/m);
32
+ if (!m) continue;
33
+ const have = m[1].split(",").map((t) => t.trim());
34
+ const missing = servers.map((s2) => `mcp__${s2}`).filter((t) => !have.includes(t));
35
+ if (missing.length === 0) continue;
36
+ text = text.replace(/^tools: .+$/m, `tools: ${[...have, ...missing].join(", ")}`);
37
+ writeFileSync(file, text);
38
+ updated.push(`${card} (+${missing.join(", ")})`);
39
+ }
40
+ return { updated };
41
+ }
42
+
43
+ function readClaudeSettingsFile(file) {
44
+ if (!existsSync(file)) return null;
45
+ try {
46
+ return JSON.parse(readFileSync(file, "utf8"));
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
16
52
  export function settingsPath(targetDir) {
17
53
  return join(targetDir, ".claude", "settings.json");
18
54
  }
package/lib/distill.mjs CHANGED
@@ -76,6 +76,10 @@ export default async function distill(args = []) {
76
76
  );
77
77
 
78
78
  const ROOT = process.cwd();
79
+ // Telemetry (owner feedback #9): every phase is timed and printed, so a CI
80
+ // log answers "where is it, and where did the time go" at a glance.
81
+ const t0 = Date.now();
82
+ const secs = (t) => ((Date.now() - t) / 1000).toFixed(1);
79
83
 
80
84
  // 1 · collect the branch working set. Adjudication-flagged rows are LEFT for
81
85
  // a human session (CI can flag divergence, never resolve it).
@@ -101,7 +105,9 @@ export default async function distill(args = []) {
101
105
 
102
106
  // 2 · mirror the org brain locally — the authoring target + the base the
103
107
  // survivors fold into. (CI checkout is merged main = the validation target.)
108
+ const tPull = Date.now();
104
109
  await pull(["--full"]);
110
+ console.log(` ⏱ brain mirror: ${secs(tPull)}s`);
105
111
 
106
112
  // 3 · stage the incoming working set for the agent (gitignored inside .rafa).
107
113
  const stagingRoot = join(ROOT, ".rafa", "distill-incoming");
@@ -153,6 +159,7 @@ Staged files:
153
159
  ${fileList}`;
154
160
 
155
161
  console.log("• running the distillation agent (org's own ANTHROPIC_API_KEY) …");
162
+ const tAgent = Date.now();
156
163
  const run = sdk.query({
157
164
  prompt,
158
165
  options: {
@@ -163,14 +170,45 @@ ${fileList}`;
163
170
  maxTurns: 150,
164
171
  },
165
172
  });
173
+ // Stream the agent's narration lines — a silent multi-minute CI step reads
174
+ // as hung; a narrated one reads as working (and shows WHERE it hangs).
175
+ let lastLine = "";
176
+ let agentModel = "unreported";
166
177
  for await (const message of run) {
178
+ if (message.type === "assistant" && typeof message.message?.model === "string")
179
+ agentModel = message.message.model;
180
+ if (message.type === "assistant") {
181
+ const blocks = message.message?.content ?? [];
182
+ for (const b of blocks) {
183
+ if (b.type !== "text" || !b.text?.trim()) continue;
184
+ const line = b.text.trim().split("\n")[0].slice(0, 110);
185
+ if (line && line !== lastLine) {
186
+ console.log(` · ${line}`);
187
+ lastLine = line;
188
+ }
189
+ }
190
+ }
167
191
  if (message.type === "result") {
168
192
  if (message.subtype !== "success")
169
193
  die(`distillation agent did not complete (${message.subtype}) — nothing resolved, nothing pushed`);
170
194
  console.log(
171
- ` agent done (${message.num_turns ?? "?"} turns · ` +
195
+ ` agent: ${secs(tAgent)}s (${message.num_turns ?? "?"} turns · ` +
172
196
  `$${(message.total_cost_usd ?? 0).toFixed(4)} on the org's key)`,
173
197
  );
198
+ // Self-report the metered run (backlog #6) — best-effort, never fatal.
199
+ try {
200
+ await callTool(ROOT, "log_usage", {
201
+ provider: "anthropic",
202
+ model: agentModel,
203
+ inputTokens: message.usage?.input_tokens ?? 0,
204
+ outputTokens: message.usage?.output_tokens ?? 0,
205
+ ...(typeof message.total_cost_usd === "number" ? { costUsd: message.total_cost_usd } : {}),
206
+ purpose: "distill",
207
+ runner: "ci",
208
+ });
209
+ } catch (e) {
210
+ console.log(` ! usage not reported: ${e instanceof Error ? e.message : e}`);
211
+ }
174
212
  }
175
213
  }
176
214
 
@@ -195,10 +233,12 @@ ${fileList}`;
195
233
 
196
234
  // 6 · trust-but-verify: OUR gates re-run regardless of what the agent claims.
197
235
  console.log("• re-running the gates (trust-but-verify) …");
236
+ const tGates = Date.now();
198
237
  if (runVerifyCitations([]) !== 0)
199
238
  die("citation checker failed on the authored brain — nothing resolved, nothing pushed");
200
239
  if (runCompile([]) !== 0)
201
240
  die("compile gate failed on the authored brain — nothing resolved, nothing pushed");
241
+ console.log(` ⏱ gates: ${secs(tGates)}s`);
202
242
 
203
243
  // 7 · ship: brain repo push (webhook → ingest), staging cleaned first.
204
244
  rmSync(stagingRoot, { recursive: true, force: true });
@@ -221,6 +261,7 @@ ${fileList}`;
221
261
  branch,
222
262
  path: f.path,
223
263
  resolution: v.verdict,
264
+ actorMeta: { agent: "rafa-distill", runner: "ci", model: agentModel },
224
265
  ...(v.note ? { note: v.note } : {}),
225
266
  });
226
267
  console.log(` ${v.verdict === "distilled" ? "✓" : "!"} ${f.path} → ${v.verdict}${v.note ? ` (${v.note})` : ""}`);
@@ -232,6 +273,6 @@ ${fileList}`;
232
273
 
233
274
  console.log(
234
275
  `✓ distillation: ${distilledCount} into the org brain · ${refuted} refuted (reported, cited) · ` +
235
- `${adjudication} flagged needs-adjudication (next session's digest)`,
276
+ `${adjudication} flagged needs-adjudication (next session's digest) · total ${secs(t0)}s`,
236
277
  );
237
278
  }
@@ -335,20 +335,13 @@ export function runCompile(argv = []) {
335
335
  return { domains };
336
336
  }
337
337
 
338
- // Plans (contract §7): parent+child files under plans/**. Global id uniqueness,
339
- // parent/child cross-checks, progress never stored, active.md activePlanId.
340
- // VALIDATED here (files are the authoring surface determinism holds) but
341
- // since 0.4.0 plans do NOT ride the manifest: they travel through the
342
- // dedicated push-on-approval plans channel (push_plan). Terminal statuses
343
- // (superseded/abandoned) close a plan honestly without pretending `done`.
344
- const PLAN_STATUSES = [
345
- "todo",
346
- "in-progress",
347
- "done",
348
- "blocked",
349
- "superseded",
350
- "abandoned",
351
- ];
338
+ // Plans (contract §7 v2 the work-item tree, plans data version 2): one epic
339
+ // (root) tasks subtasks. Vendor-blended vocabulary; `blocked` is DERIVED
340
+ // (unresolved blocked_by / blocked_reason), never stored. VALIDATED here
341
+ // (files are the authoring surface determinism holds) but plans do NOT ride
342
+ // the manifest: they travel through the push-on-approval plans channel.
343
+ const PLAN_STATUSES = ["todo", "in-progress", "done", "superseded", "abandoned"];
344
+ const PLAN_KINDS = ["epic", "task", "subtask"];
352
345
  function compilePlans() {
353
346
  const plans = [];
354
347
  const seen = new Map(); // id → path (global uniqueness across plans/**)
@@ -365,43 +358,123 @@ export function runCompile(argv = []) {
365
358
  fail(path, "id", `duplicate plan id "${id}" (also ${seen.get(id)}) · ids are globally unique across plans/**`);
366
359
  seen.set(id, path);
367
360
  if ("progress" in data)
368
- fail(path, "progress", "never stored · parent progress is derived by counting children");
369
- const kind = reqEnum(data, "kind", ["parent", "child"], path);
361
+ fail(path, "progress", "never stored · progress = done leaves / total leaves, derived");
362
+ if (data.kind === "parent" || data.kind === "child")
363
+ fail(path, "kind", `plans v1 shape ("${data.kind}") — run \`rafa migrate\` (v2: epic|task|subtask)`);
364
+ const kind = reqEnum(data, "kind", PLAN_KINDS, path);
370
365
  const plan = reqStr(data, "plan", path);
371
366
  const status = data.status;
372
- if (kind === "child") {
373
- if (!isEnum(status, PLAN_STATUSES))
374
- fail(path, "status", `required (child) · ${PLAN_STATUSES.join("|")}`);
375
- if (data.parent !== plan)
376
- fail(path, "parent", `must equal plan "${plan}" for a child (flat v1)`);
377
- } else {
367
+ if (status === "blocked")
368
+ fail(path, "status", "removed in v2 — blocked is DERIVED from blocked_by/blocked_reason (run `rafa migrate`)");
369
+ if (kind === "epic") {
378
370
  if (data.parent !== null && data.parent !== "null")
379
- fail(path, "parent", "must be null for kind: parent");
380
- if (plan !== id) fail(path, "plan", `must equal id "${id}" for kind: parent`);
371
+ fail(path, "parent", "must be null for kind: epic (the root)");
372
+ if (plan !== id) fail(path, "plan", `must equal id "${id}" for kind: epic`);
381
373
  if (status !== undefined && !isEnum(status, PLAN_STATUSES))
382
- fail(path, "status", `optional (parent) · ${PLAN_STATUSES.join("|")}`);
374
+ fail(path, "status", `optional (epic) · ${PLAN_STATUSES.join("|")}`);
375
+ } else {
376
+ if (!isEnum(status, PLAN_STATUSES))
377
+ fail(path, "status", `required (${kind}) · ${PLAN_STATUSES.join("|")}`);
378
+ if (typeof data.parent !== "string" || data.parent === "")
379
+ fail(path, "parent", `required (${kind}) · the parent item id`);
380
+ if (data.domains !== undefined)
381
+ fail(path, "domains", "epic only · the blast radius belongs to the plan, not an item");
382
+ }
383
+ if (data.domains !== undefined && !Array.isArray(data.domains))
384
+ fail(path, "domains", "optional · flow list of domain strings");
385
+ if (data.blocked_by !== undefined && !Array.isArray(data.blocked_by))
386
+ fail(path, "blocked_by", "optional · flow list of item ids this item waits on");
387
+ if (
388
+ data.priority !== undefined &&
389
+ !(Number.isInteger(data.priority) && data.priority >= 0 && data.priority <= 4)
390
+ )
391
+ fail(path, "priority", "optional · int 0–4 (0 none · 1 urgent · 2 high · 3 medium · 4 low)");
392
+ if (data.estimate !== undefined && !(Number.isInteger(data.estimate) && data.estimate >= 0))
393
+ fail(path, "estimate", "optional · non-negative int (points)");
394
+ if (data.external !== undefined) {
395
+ const ex = data.external;
396
+ if (typeof ex !== "object" || Array.isArray(ex) || typeof ex.provider !== "string" || typeof ex.key !== "string")
397
+ fail(path, "external", "optional · { provider, key, url? } flow map — tracker identity (read-only to sessions)");
383
398
  }
384
399
  plans.push({
385
400
  id,
386
401
  plan,
387
- parent: kind === "parent" ? null : plan,
402
+ parent: kind === "epic" ? null : String(data.parent ?? ""),
388
403
  kind,
389
404
  title: reqStr(data, "title", path),
390
405
  ...(isEnum(status, PLAN_STATUSES) ? { status } : {}),
406
+ ...(typeof data.description === "string" && data.description !== ""
407
+ ? { description: data.description }
408
+ : {}),
409
+ ...(typeof data.approach === "string" && data.approach !== ""
410
+ ? { approach: data.approach }
411
+ : {}),
412
+ ...(typeof data.assignee === "string" && data.assignee !== ""
413
+ ? { assignee: data.assignee }
414
+ : {}),
415
+ ...(Array.isArray(data.blocked_by) ? { blockedBy: data.blocked_by.map(String) } : {}),
416
+ ...(typeof data.blocked_reason === "string" && data.blocked_reason !== ""
417
+ ? { blockedReason: data.blocked_reason }
418
+ : {}),
419
+ ...(Number.isInteger(data.priority) ? { priority: data.priority } : {}),
420
+ ...(Number.isInteger(data.estimate) ? { estimate: data.estimate } : {}),
391
421
  ...(typeof data.branch === "string" && data.branch !== ""
392
422
  ? { branch: data.branch }
393
423
  : {}),
394
424
  ...(typeof data.baseSha === "string" && data.baseSha !== ""
395
425
  ? { baseSha: String(data.baseSha) }
396
426
  : {}),
427
+ ...(Array.isArray(data.domains) && kind === "epic"
428
+ ? { domains: data.domains.map(String) }
429
+ : {}),
430
+ ...(typeof data.external === "object" && !Array.isArray(data.external) && data.external !== null
431
+ ? { external: data.external }
432
+ : {}),
397
433
  path: rel(path),
398
434
  });
399
435
  }
400
- // Cross-check: every child's plan resolves to a kind:parent in this compile.
401
- const parents = new Set(plans.filter((p) => p.kind === "parent").map((p) => p.id));
436
+ // Cross-checks: tree shape + blocked_by resolution, per plan.
437
+ const byId = new Map(plans.map((p) => [p.id, p]));
438
+ const roots = new Map(); // plan id → epic item
439
+ for (const p of plans) if (p.kind === "epic") roots.set(p.id, p);
440
+ for (const p of plans) {
441
+ if (!roots.has(p.plan))
442
+ fail(p.path, "plan", `no kind:epic "${p.plan}" in this compile (dangling item)`);
443
+ if (p.kind === "task") {
444
+ const parent = byId.get(p.parent);
445
+ if (!parent || parent.kind !== "epic" || parent.id !== p.plan)
446
+ fail(p.path, "parent", `a task's parent must be its epic ("${p.plan}")`);
447
+ }
448
+ if (p.kind === "subtask") {
449
+ const parent = byId.get(p.parent);
450
+ if (!parent || parent.kind !== "task")
451
+ fail(p.path, "parent", "a subtask's parent must be a task in the same plan");
452
+ else if (parent.plan !== p.plan)
453
+ fail(p.path, "parent", `parent task belongs to plan "${parent.plan}", not "${p.plan}"`);
454
+ }
455
+ for (const b of p.blockedBy ?? []) {
456
+ const target = byId.get(b);
457
+ if (!target || target.plan !== p.plan)
458
+ fail(p.path, "blocked_by", `"${b}" does not resolve to an item in plan "${p.plan}"`);
459
+ if (b === p.id) fail(p.path, "blocked_by", "an item cannot block itself");
460
+ }
461
+ }
462
+ // blocked_by acyclicity (per plan; DFS with colors).
463
+ const color = new Map();
464
+ const cyc = (p) => {
465
+ color.set(p.id, 1);
466
+ for (const b of p.blockedBy ?? []) {
467
+ const t = byId.get(b);
468
+ if (!t) continue;
469
+ if (color.get(t.id) === 1) return true;
470
+ if (!color.has(t.id) && cyc(t)) return true;
471
+ }
472
+ color.set(p.id, 2);
473
+ return false;
474
+ };
402
475
  for (const p of plans)
403
- if (p.kind === "child" && !parents.has(p.plan))
404
- fail(p.path, "plan", `no parent plan "${p.plan}" in this compile (dangling child)`);
476
+ if (!color.has(p.id) && cyc(p))
477
+ fail(p.path, "blocked_by", "dependency cycle blocked_by must be acyclic");
405
478
  return plans;
406
479
  }
407
480
 
@@ -419,8 +492,8 @@ export function runCompile(argv = []) {
419
492
  }
420
493
  const val = m[1].trim();
421
494
  if (/^no active plan$/i.test(val)) return null;
422
- if (!plans.some((p) => p.kind === "parent" && p.id === val)) {
423
- fail(path, "pointer", `active plan "${val}" is not a kind:parent plan in this compile`);
495
+ if (!plans.some((p) => p.kind === "epic" && p.id === val)) {
496
+ fail(path, "pointer", `active plan "${val}" is not a kind:epic plan in this compile`);
424
497
  return null;
425
498
  }
426
499
  return val;
package/lib/init.mjs CHANGED
@@ -105,6 +105,13 @@ export default async function init(args) {
105
105
  );
106
106
  // Grant rafa's tools their permissions (merge, never overwrite).
107
107
  reportSettings(TARGET);
108
+ // Wire the repo's own MCP servers into the (owned) agent cards so subagents
109
+ // can actually reach them (zero-command orchestration, 2026-07-12).
110
+ {
111
+ const { mergeRepoMcpsIntoCards } = await import("./claude-config.mjs");
112
+ const r = mergeRepoMcpsIntoCards(TARGET);
113
+ for (const u of r.updated) console.log(` ✓ repo MCPs → ${u}`);
114
+ }
108
115
 
109
116
  // Keep the brain out of the code repo's diffs; keep the per-dev MCP key out of git.
110
117
  const ensureIgnored = (line, label) => {
@@ -151,6 +158,26 @@ export default async function init(args) {
151
158
  );
152
159
  }
153
160
 
161
+ // CI reconciliation — OPT-IN at provisioning (owner feedback #5): the workflow
162
+ // runs on the ORG's own secrets (their LLM key never touches our platform), so
163
+ // it is offered, never silently installed. Non-TTY (CI/agent shells) skips the
164
+ // question entirely — explicit `rafa ci-setup` remains the path there.
165
+ if (isInteractive()) {
166
+ const a = await ask(
167
+ " • Wire CI reconciliation now? (GitHub Actions: mechanical fold on branch merges,\n" +
168
+ " distillation on merge-to-main — runs on YOUR CI secrets) [y/N] ",
169
+ );
170
+ if (a === "y" || a === "yes") {
171
+ const { default: ciSetup } = await import("./ci-setup.mjs");
172
+ await ciSetup([]);
173
+ } else {
174
+ console.log(
175
+ " • skipped — wire it anytime with `rafa ci-setup`. Until then, the next dev\n" +
176
+ " session offers the same reconciliation after each merge (nothing is lost).",
177
+ );
178
+ }
179
+ }
180
+
154
181
  // Stamp the versions this repo was provisioned at — the compat anchor `update`/`migrate`
155
182
  // diff against. Carries platform identity too when provisioned via a setup URL.
156
183
  stampCurrent(
package/lib/leverage.mjs CHANGED
@@ -3,12 +3,23 @@
3
3
  // only: it reads, ranks, and reports — it changes nothing. Run from your repo root.
4
4
 
5
5
  import { advise } from "./leverage/engine.mjs";
6
- import { PLANNED } from "./leverage/adapters/index.mjs";
6
+ import { adapters, PLANNED } from "./leverage/adapters/index.mjs";
7
7
 
8
- export default async function leverage() {
8
+ export default async function leverage(args = []) {
9
9
  const cwd = process.cwd();
10
10
  const { detected, tips } = advise(cwd);
11
11
 
12
+ // --json: the DETERMINISTIC toolbox inventory (ratified 2026-07-12) — what
13
+ // the conductor loads at bootstrap and atlas consults during scan's toolbox
14
+ // pass. Machine truth: only what is actually installed, never inferred.
15
+ if (args.includes("--json")) {
16
+ const inventory = {};
17
+ for (const a of adapters)
18
+ if (a.detect(cwd)) inventory[a.id] = a.inspect(cwd);
19
+ console.log(JSON.stringify({ detected, inventory, tips }, null, 2));
20
+ return;
21
+ }
22
+
12
23
  if (detected.length === 0) {
13
24
  console.log(
14
25
  "No supported agent toolchain detected here.\n" +
@@ -28,7 +28,7 @@
28
28
  // run: (cwd) => backfillPlanField(cwd, "owner", ownerOf(cwd)),
29
29
  // };
30
30
 
31
- import { existsSync, rmSync } from "node:fs";
31
+ import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
32
32
  import { join } from "node:path";
33
33
  import { cmpSemver } from "../releases.mjs";
34
34
 
@@ -66,7 +66,62 @@ const blueprint_split_0_4_0 = {
66
66
  },
67
67
  };
68
68
 
69
- export const MIGRATIONS = [blueprint_split_0_4_0];
69
+ // plans v2 (contract §7 v2, ratified 2026-07-12): the work-item tree. Mechanical
70
+ // half: kind parent→epic, child→task (rank semantics unchanged for flat v1 plans),
71
+ // stored `status: blocked` → `status: todo` + a blocked_reason note (blocked is
72
+ // DERIVED in v2). Only frontmatter lines are touched; bodies never.
73
+ const plans_v1_to_v2 = {
74
+ id: "plans-v1-to-v2-work-item-tree",
75
+ kind: "plans",
76
+ mode: "mechanical",
77
+ summary:
78
+ "plans become a work-item tree (epic|task|subtask); blocked becomes derived " +
79
+ "(kind parent→epic, child→task; status blocked→todo + blocked_reason)",
80
+ appliesTo: (from, to) => from.plans < 2 && to.plans >= 2,
81
+ run: (cwd) => {
82
+ const changed = [];
83
+ const notes = [];
84
+ const dir = join(cwd, ".rafa", "plans");
85
+ const walk = (d) => {
86
+ if (!existsSync(d)) return [];
87
+ return readdirSync(d).flatMap((e) => {
88
+ const p = join(d, e);
89
+ return statSync(p).isDirectory() ? walk(p) : e.endsWith(".md") ? [p] : [];
90
+ });
91
+ };
92
+ for (const file of walk(dir)) {
93
+ const text = readFileSync(file, "utf8");
94
+ if (!text.startsWith("---")) continue;
95
+ const end = text.indexOf("\n---", 3);
96
+ if (end === -1) continue;
97
+ let fm = text.slice(0, end);
98
+ const body = text.slice(end);
99
+ const before = fm;
100
+ fm = fm.replace(/^kind:\s*parent\s*$/m, "kind: epic");
101
+ fm = fm.replace(/^kind:\s*child\s*$/m, "kind: task");
102
+ if (/^status:\s*blocked\s*$/m.test(fm)) {
103
+ fm = fm.replace(/^status:\s*blocked\s*$/m, "status: todo");
104
+ if (!/^blocked_reason:/m.test(fm))
105
+ fm += `\nblocked_reason: "(migrated) was 'blocked' in plans v1 — re-state the real blocker"`;
106
+ notes.push(`${file.slice(cwd.length + 1)}: blocked → todo + blocked_reason (re-state the real blocker)`);
107
+ }
108
+ if (fm !== before) {
109
+ writeFileSync(file, fm + body);
110
+ changed.push(file.slice(cwd.length + 1));
111
+ }
112
+ }
113
+ return {
114
+ changed,
115
+ notes: changed.length
116
+ ? notes.length
117
+ ? notes
118
+ : ["kinds re-ranked (parent→epic, child→task); review the diff"]
119
+ : ["no v1 plan files found — nothing to migrate"],
120
+ };
121
+ },
122
+ };
123
+
124
+ export const MIGRATIONS = [blueprint_split_0_4_0, plans_v1_to_v2];
70
125
 
71
126
  // The migrations that apply to a given move (stamped `from` → shipped `to`), in order.
72
127
  export function pendingMigrations(from, to) {
package/lib/releases.mjs CHANGED
@@ -102,6 +102,37 @@ export const RELEASES = [
102
102
  "the working-set scanner returned the directory path instead of each file's path " +
103
103
  "(variable shadowing in scanWorkingFiles). No schema change; no re-scan.",
104
104
  },
105
+ {
106
+ version: "0.4.2",
107
+ contract: 1,
108
+ plans: 1,
109
+ requires: "update",
110
+ summary:
111
+ "Owner E2E feedback round. `rafa init` now OFFERS CI reconciliation at provisioning " +
112
+ "(TTY only; skipping prints the session-fallback path — nothing is silently installed). " +
113
+ "`rafa distill --headless` streams the agent's narration and prints per-phase timings + " +
114
+ "cost (mirror · agent · gates · total), so CI logs show liveness and where time goes. " +
115
+ "Plans gain an optional parent-only `domains:` (contract §7) — the blast radius rides " +
116
+ "push_plan and the platform renders the plan's brain slice on its new detail page. " +
117
+ "No schema migration; no re-scan.",
118
+ },
119
+ {
120
+ version: "0.5.0",
121
+ contract: 1,
122
+ plans: 2,
123
+ requires: "migrate",
124
+ summary:
125
+ "Plans v2 — the work-item tree (contract §7 v2, first plans data-version bump). " +
126
+ "epic → task → subtask with vendor-blended vocabulary (checked against Linear/Jira/Asana " +
127
+ "APIs): title · description · approach · assignee · blocked_by (a dependency IS a " +
128
+ "blocker) · blocked_reason · priority 0–4 · estimate · external {provider,key,url} " +
129
+ "(read-only tracker identity). `blocked` is DERIVED, never stored — the status enum is " +
130
+ "todo|in-progress|done|superseded|abandoned. Decisions become first-class channel " +
131
+ "records (log_decision: at·actor·context·options·decision·rationale; paraphrase + " +
132
+ "pivotal quotes), mirrored as `## Decisions` body prose. `rafa migrate` converts v1 " +
133
+ "files mechanically (parent→epic, child→task, blocked→todo+blocked_reason). Brain data " +
134
+ "schema unchanged; no re-scan.",
135
+ },
105
136
  ];
106
137
 
107
138
  // The release this CLI build ships (last entry).
package/lib/update.mjs CHANGED
@@ -31,6 +31,13 @@ export default async function update(args = []) {
31
31
  await copyBlueprint(blueprintSource(), TARGET, { onConflict: makeConflictResolver(args) }),
32
32
  );
33
33
  reportSettings(TARGET);
34
+ // Wire the repo's own MCP servers into the (owned) agent cards so subagents
35
+ // can actually reach them (zero-command orchestration, 2026-07-12).
36
+ {
37
+ const { mergeRepoMcpsIntoCards } = await import("./claude-config.mjs");
38
+ const r = mergeRepoMcpsIntoCards(TARGET);
39
+ for (const u of r.updated) console.log(` ✓ repo MCPs → ${u}`);
40
+ }
34
41
 
35
42
  // ── Blueprint-side migrations: mechanical/deterministic, run here in the CLI. ──
36
43
  // (Anything needing intelligence is left for the brain-side pass — /rafa update.)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rafinery/cli",
3
- "version": "0.4.1",
4
- "description": "rafa \u2014 the AI engineer CLI. Vendors the rafa blueprint into your repo (shadcn-style) and runs the deterministic contract gates.",
3
+ "version": "0.5.0",
4
+ "description": "rafa the AI engineer CLI. Vendors the rafa blueprint into your repo (shadcn-style) and runs the deterministic contract gates.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "rafa": "bin/rafa.mjs"