@rafinery/cli 0.2.2 → 0.3.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,32 @@ 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.3.0
8
+
9
+ **Adopt with:** `npx @rafinery/cli@latest update` — no re-scan, no migration required.
10
+ Recommended: re-run `/rafa scan` (or a `rafa push`) so your manifest picks up the new `plans[]`.
11
+
12
+ The read side lands — your brain is now queryable by any MCP client.
13
+
14
+ - **`init` wires the knowledge MCP (secret-free where committed).** With a platform setup URL,
15
+ `init` now registers the platform's read-only knowledge MCP:
16
+ - `.mcp.json` (committed) — a `rafinery` HTTP server with `Authorization: Bearer ${RAFA_MCP_KEY}`
17
+ env expansion, so **no secret is committed**.
18
+ - `.claude/settings.local.json` `env` (gitignored) + `~/.config/rafinery/credentials.json` (0600) —
19
+ where the raw per-dev key lives. The key is minted at setup generation and delivered
20
+ **consume-once** through the setup fetch; a re-fetched (keyless) setup is reported loudly, with
21
+ instructions to mint one on the platform (repo → Agent access).
22
+ - Merge, never clobber: unparseable files are left alone and reported; other servers/env keys are
23
+ preserved; only the `rafinery` entry is ours.
24
+ - **Contract §9 — the read-side twin.** Documents the platform-served knowledge MCP: the mandatory
25
+ response envelope (`source`/`brainForSha`/`ingestedAt`), loud error semantics (no assumed values),
26
+ and the read-only tool set (`get_brain_status`, `search_knowledge`, `get_rule/playbook/improvement`,
27
+ `list_improvements`, `get_plan`, `get_active_plan`, `get_coverage`).
28
+ - **§7 plans wired; compile emits `plans[]` + `activePlanId`.** Plan files are no longer "Slice 4" —
29
+ compile now parses `plans/**/*.md`, enforces global-id uniqueness, parent/child integrity, and
30
+ the no-stored-progress rule, resolves `active.md` → `activePlanId`, and emits them into
31
+ `manifest.json`. Progress stays **derived**, never stored.
32
+
7
33
  ## 0.2.2
8
34
 
9
35
  **Adopt with:** `npx @rafinery/cli@latest update` — no re-scan, no migration.
package/bin/rafa.mjs CHANGED
@@ -40,7 +40,29 @@ Commands:
40
40
  migrate Bring this repo's structured files (plans, config) up to the schema
41
41
  the installed CLI ships. Idempotent; review the diff after.
42
42
 
43
- Then, inside Claude Code: /rafa scan · /rafa improve · /rafa plan
43
+ End to end from zero to a queryable, working brain:
44
+
45
+ 1. PLATFORM Sign in → add a GitHub token → add your brain repo + code repo →
46
+ connect them → "Generate setup command" (URL expires in 15 min;
47
+ it carries a one-time MCP agent key).
48
+ 2. INSTALL In your code repo: npx @rafinery/cli init '<setup-url>'
49
+ → vendors the blueprint (.rafa/ + .claude/), wires the brain remote,
50
+ registers the knowledge MCP in .mcp.json (secret-free) and puts the
51
+ key in .claude/settings.local.json + ~/.config/rafinery/credentials.json.
52
+ 3. SCAN Restart Claude Code, run /rafa scan — atlas maps the codebase into
53
+ cited rules/playbooks, prism validates, bloom writes the improvement
54
+ ledger. Then rafa push → webhook → the platform ingests it.
55
+ 4. SEE IT Open the repo on the platform: Overview (health), Brain (files),
56
+ Improvements (kanban), Plans. "Agent access" mints keys for more
57
+ clients (Slack, incident.io, CI — anything that speaks MCP).
58
+ 5. WORK /rafa plan "<intent>" — a prism-validated plan, grounded in the brain.
59
+ /rafa build — execute it; progress syncs on every push, so
60
+ any teammate or session resumes where you stopped.
61
+ 6. QUERY Any MCP client with a key can read the brain at <platform>/api/mcp:
62
+ get_brain_status · search_knowledge · get_rule/playbook/improvement ·
63
+ get_plan · get_active_plan. Read-only, cited, per-repo scoped.
64
+
65
+ Then, inside Claude Code: /rafa scan · /rafa improve · /rafa plan · /rafa build
44
66
 
45
67
  Docs: ${pkg.homepage ?? "https://github.com/rafinery-ai/rafinery"}`);
46
68
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: rafa — init · scan · improve · push · leverage · migrate · update · help. Usage: /rafa <init|scan|improve|push|leverage|migrate|update|help> [--brain-only]
2
+ description: rafa — init · scan · improve · plan · build · push · leverage · migrate · update · help. Usage: /rafa <init|scan|improve|plan|build|push|leverage|migrate|update|help> [--brain-only]
3
3
  ---
4
4
 
5
5
  You are the **conductor** of the rafa agent loop, running in the **main session**.
@@ -52,6 +52,38 @@ cite-check them (`--root=.rafa/improve --dirs=improvements`, drop unresolved), r
52
52
  `ledger.md` + debt trend."* When it returns, read `ledger.md` and **surface only the top few
53
53
  high-leverage P0/P1s — don't nag; the dev triages.**
54
54
 
55
+ ## `plan <intent>` — brain-grounded, prism-validated decomposition
56
+ Per [`.rafa/capabilities/plan.md`](../../.rafa/capabilities/plan.md). The trio at plan time —
57
+ atlas drafts, bloom pulls, prism validates the plan itself:
58
+ 1. **Staleness check** — if this repo is platform-connected, compare the MCP envelope's
59
+ `brainForSha` to the local brain stamp; if the platform is behind, surface "run `rafa push`"
60
+ (never proceed silently, never block).
61
+ 2. **Spawn `atlas`** context-isolated: *"Draft a plan for `<intent>` per
62
+ `.rafa/capabilities/plan.md`: RECALL the brain slice for the touched domains (coverage →
63
+ search → rules/playbooks, honor non-exemplars), name the blast radius, decompose into
64
+ contract §7 files — one parent + child-owned tasks, globally-unique prefixed ids, each child
65
+ body carrying a `## Done-check`. Return the draft file contents."*
66
+ 3. **Spawn `bloom`**: *"List open improvements in the blast radius `<domains>`; return the
67
+ top-leverage few as optional child tasks."* Fold them in, marked optional.
68
+ 4. **Spawn `prism`** context-isolated: *"Validate this DRAFT plan against the brain + code per
69
+ `.rafa/capabilities/plan.md`: every task grounded (not hallucinated), every child has a
70
+ `## Done-check`. REJECT with reasons or PASS."* Fix-and-revalidate until PASS (max 3; then
71
+ surface and stop).
72
+ 5. **Approval gate** (ExitPlanMode / the dev). On approval: write `plans/<plan>/*.md`, set
73
+ `active.md` = `# <plan-id>`, run `node .rafa/bin/rafa-compile.mjs` to exit 0, offer
74
+ `rafa push` (the plan becomes resumable from any session/machine via the platform).
75
+
76
+ ## `build` — execute the active plan, trio-choreographed
77
+ Per [`.rafa/capabilities/build.md`](../../.rafa/capabilities/build.md). Resume from `active.md`
78
+ (or the platform's `get_active_plan` when connected) — never re-derive context.
79
+ Per task: **atlas** recalls + implements → **prism** validates against the child's
80
+ `## Done-check` (**`status: done` only on prism PASS**; FAIL → atlas corrects) → **bloom**
81
+ sweeps (new opportunities → ledger files; fixed-in-passing → `status: fixed`; one opt-in
82
+ nudge max) → update the child file's `status` → periodically compile + push so the platform
83
+ reflects live progress. If execution invalidates/creates brain knowledge, run a **full
84
+ `## scan`** (v1 rule — never hand-edit brain files around the gate). When all children are
85
+ done: prism-style final verify, set `active.md` = `# No active plan`, compile + push.
86
+
55
87
  ## `push` — (re-)push the brain to the brain remote
56
88
  `node .rafa/bin/rafa-push.mjs` — commit `.rafa/` and push to `origin` (the brain repo) with the
57
89
  dev's own git auth. Use after a scan, or to re-sync a brain that changed.
@@ -103,6 +135,24 @@ Never hand-edit around a migration or discard tuned files.
103
135
  ## `help` (also: no argument, or an unrecognized command)
104
136
  Print this reference verbatim and stop.
105
137
 
138
+ **End to end — from zero to a queryable, working brain:**
139
+
140
+ 1. **Platform** — sign in → add a GitHub token → add brain repo + code repo → connect →
141
+ *Generate setup command* (15-min URL carrying a one-time MCP agent key).
142
+ 2. **Install** — in the code repo: `npx @rafinery/cli init '<setup-url>'` — vendors the
143
+ blueprint, wires the brain remote, registers the knowledge MCP in `.mcp.json`
144
+ (secret-free) + puts the key in `.claude/settings.local.json` (gitignored).
145
+ 3. **Scan** — restart Claude Code → `/rafa scan` (atlas maps → prism validates → bloom
146
+ ledgers) → `rafa push` → webhook → the platform ingests the manifest.
147
+ 4. **See it** — platform repo pages: Overview · Brain · Improvements · Plans; *Agent
148
+ access* mints keys for more MCP clients (Slack, incident.io, CI).
149
+ 5. **Work** — `/rafa plan "<intent>"` (prism-validated, brain-grounded) → `/rafa build`
150
+ (atlas executes, prism gates each Done-check, bloom sweeps); every push syncs
151
+ progress so any session/teammate resumes exactly where work stopped.
152
+ 6. **Query** — any MCP client with a key reads the brain at `<platform>/api/mcp`:
153
+ `get_brain_status` · `search_knowledge` · `get_rule/playbook/improvement` ·
154
+ `get_plan` · `get_active_plan`. Read-only, cited, per-repo scoped.
155
+
106
156
  **rafa has two surfaces.** `/rafa <cmd>` runs the intelligent, in-editor passes; the terminal
107
157
  `rafa` CLI (run via `npx @rafinery/cli@latest <cmd>`) does the deterministic plumbing. Several
108
158
  names exist on both — the CLI does the mechanical half, `/rafa` the intelligent half.
@@ -113,6 +163,8 @@ names exist on both — the CLI does the mechanical half, `/rafa` the intelligen
113
163
  | `/rafa init` | First run: ensure structure, then run the full scan pass. |
114
164
  | `/rafa scan [--brain-only]` | The full pass: know → verify → improve → push. `--brain-only` stops after the brain is validated (a cheap knowledge refresh). |
115
165
  | `/rafa improve` | The improvement pass — bloom writes a cited, prioritized (P0–P3) ledger. |
166
+ | `/rafa plan <intent>` | Trio planning: atlas drafts (brain-grounded, contract §7 files, Done-checks), bloom pulls blast-radius improvements, prism validates the plan — then approval, compile, push. |
167
+ | `/rafa build` | Execute the active plan: per task atlas implements → prism gates `done` on the Done-check → bloom sweeps the ledger; compile + push progress as you go. |
116
168
  | `/rafa push` | (Re-)push the brain to the brain remote (your git auth). |
117
169
  | `/rafa leverage` | Tune your toolbox: reason over config and, on approval, apply fixes exactly — merge settings, wire an MCP, scaffold a skill. |
118
170
  | `/rafa migrate` | Semantic migration — rewrite plans to a new schema preserving meaning, then compile-gate. |
@@ -324,6 +324,90 @@ function compileCoverage() {
324
324
  return { domains };
325
325
  }
326
326
 
327
+ // Plans (contract §7): parent+child files under plans/**. Global id uniqueness,
328
+ // parent/child cross-checks, progress never stored, active.md → activePlanId.
329
+ function compilePlans() {
330
+ const plans = [];
331
+ const seen = new Map(); // id → path (global uniqueness across plans/**)
332
+ for (const path of walk(join(ROOT, "plans"))) {
333
+ const name = path.split("/").pop();
334
+ const { data, error } = parseFrontmatter(read(path));
335
+ if (error) {
336
+ fail(path, "frontmatter", error);
337
+ continue;
338
+ }
339
+ checkVersion(data, path);
340
+ const id = checkId(data, path, name);
341
+ if (seen.has(id))
342
+ fail(path, "id", `duplicate plan id "${id}" (also ${seen.get(id)}) · ids are globally unique across plans/**`);
343
+ seen.set(id, path);
344
+ if ("progress" in data)
345
+ fail(path, "progress", "never stored · parent progress is derived by counting children");
346
+ const kind = reqEnum(data, "kind", ["parent", "child"], path);
347
+ const plan = reqStr(data, "plan", path);
348
+ const status = data.status;
349
+ if (kind === "child") {
350
+ if (!isEnum(status, ["todo", "in-progress", "done", "blocked"]))
351
+ fail(path, "status", "required (child) · todo|in-progress|done|blocked");
352
+ if (data.parent !== plan)
353
+ fail(path, "parent", `must equal plan "${plan}" for a child (flat v1)`);
354
+ } else {
355
+ if (data.parent !== null && data.parent !== "null")
356
+ fail(path, "parent", "must be null for kind: parent");
357
+ if (plan !== id) fail(path, "plan", `must equal id "${id}" for kind: parent`);
358
+ if (
359
+ status !== undefined &&
360
+ !isEnum(status, ["todo", "in-progress", "done", "blocked"])
361
+ )
362
+ fail(path, "status", "optional (parent) · todo|in-progress|done|blocked");
363
+ }
364
+ plans.push({
365
+ id,
366
+ plan,
367
+ parent: kind === "parent" ? null : plan,
368
+ kind,
369
+ title: reqStr(data, "title", path),
370
+ ...(isEnum(status, ["todo", "in-progress", "done", "blocked"])
371
+ ? { status }
372
+ : {}),
373
+ ...(typeof data.branch === "string" && data.branch !== ""
374
+ ? { branch: data.branch }
375
+ : {}),
376
+ ...(typeof data.baseSha === "string" && data.baseSha !== ""
377
+ ? { baseSha: String(data.baseSha) }
378
+ : {}),
379
+ path: rel(path),
380
+ });
381
+ }
382
+ // Cross-check: every child's plan resolves to a kind:parent in this compile.
383
+ const parents = new Set(plans.filter((p) => p.kind === "parent").map((p) => p.id));
384
+ for (const p of plans)
385
+ if (p.kind === "child" && !parents.has(p.plan))
386
+ fail(p.path, "plan", `no parent plan "${p.plan}" in this compile (dangling child)`);
387
+ return plans;
388
+ }
389
+
390
+ // active.md (generated pointer) → activePlanId. First line `# <plan-id>` must
391
+ // resolve to a kind:parent plan; `# No active plan` or a missing file → null
392
+ // (documented default: the pointer is generated, absence means "none").
393
+ function compileActivePlanId(plans) {
394
+ const path = join(ROOT, "active.md");
395
+ if (!existsSync(path)) return null;
396
+ const first = read(path).split("\n")[0].trim();
397
+ const m = first.match(/^#\s+(.+)$/);
398
+ if (!m) {
399
+ fail(path, "pointer", 'first line must be "# <plan-id>" or "# No active plan"');
400
+ return null;
401
+ }
402
+ const val = m[1].trim();
403
+ if (/^no active plan$/i.test(val)) return null;
404
+ if (!plans.some((p) => p.kind === "parent" && p.id === val)) {
405
+ fail(path, "pointer", `active plan "${val}" is not a kind:parent plan in this compile`);
406
+ return null;
407
+ }
408
+ return val;
409
+ }
410
+
327
411
  // ── assemble ─────────────────────────────────────────────────────────────────
328
412
  const gitOpts = { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }; // no stderr leak
329
413
  function gitSha() {
@@ -349,6 +433,8 @@ const improvements = compileImprovements(join(ROOT, "improve", "improvements"));
349
433
  const health = compileHealth();
350
434
  const ledger = compileLedger();
351
435
  const coverage = compileCoverage();
436
+ const plans = compilePlans();
437
+ const activePlanId = compileActivePlanId(plans);
352
438
 
353
439
  // Cross-check: ledger.open / by_priority must match the actual open improvements.
354
440
  if (ledger) {
@@ -380,11 +466,13 @@ const manifest = {
380
466
  coverage,
381
467
  notes,
382
468
  improvements,
383
- plans: [],
469
+ activePlanId,
470
+ plans,
384
471
  };
385
472
  writeFileSync(join(ROOT, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
386
473
  console.log(
387
474
  `✓ rafa compile: ${notes.length} notes · ${improvements.length} improvements · ` +
475
+ `${plans.length} plans${activePlanId ? ` (active: ${activePlanId})` : ""} · ` +
388
476
  `health ${health ? "ok" : "—"} · ledger ${ledger ? "ok" : "—"} · ` +
389
477
  `coverage ${coverage ? `${coverage.domains.length} domains` : "—"} → .rafa/manifest.json`,
390
478
  );
@@ -1,38 +1,39 @@
1
- # build — execute, brain-aware, compounding (DRAFT · capability #4)
1
+ # build — execute the plan, trio-choreographed, compounding (capability #4)
2
2
 
3
- > Status: **draft / spec.** The work-time loop where **recall + nudge + capture-back** all
4
- > fire — the mission payoff. Depends on: plan (#3), brain (#1), ledger (#2). Built last.
3
+ > Status: **active.** The work-time loop where recall + validation + improvement all
4
+ > fire — the mission payoff. Depends on: plan (#3), brain (#1), ledger (#2).
5
+ > Invoke via `/rafa build`.
5
6
 
6
- Execute the approved plan with both stores in the loop. Invoke via `/build`.
7
+ Execute the approved plan with all three agents in the loop, knowledge served by the
8
+ platform MCP (one read path — the same surface any third-party agent uses).
7
9
 
8
- ## Why build fits the mission (the flywheel closes here)
10
+ ## The trio at build time
9
11
 
10
- Each task both **spends** the stores and **feeds** them — the compounding made real:
12
+ | Role | Agent | Job per task |
13
+ |---|---|---|
14
+ | **Executor** | atlas | RECALL the task's brain slice via MCP (`search_knowledge` + `get_rule`/`get_playbook`; honor non-exemplars) → implement, convention-adherent |
15
+ | **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 |
16
+ | **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 |
11
17
 
12
- - **RECALL** — per task, load the brain slice for the touched domains: follow
13
- conventions/contracts; honor **non-exemplars** (don't pattern-match the salient-but-wrong
14
- example — the audit showed a cold agent paying a 100K-token correction loop for exactly this).
15
- - **NUDGE** — surface the top-leverage open improvement in the task's blast radius: *"10-min fix
16
- while you're here?"* Dismissible, never blocking. (bloom's deferred nudge, finally firing.)
17
- - **LEVERAGE (orchestrate, don't bury)** — before hand-rolling a task, surface the best-fit
18
- existing capability (skill/tool/MCP — committed or the dev's personal) to the host, and any
19
- config that would help it fire. Recommend; the host orchestrates. Never reinvent what's present.
20
- - **CAPTURE-BACK** — as work lands:
21
- - decisions / scars → propose brain notes (**prism-validated** before they enter the brain),
22
- **tool-agnostic** — capture *what + why + cited code*, never which (possibly personal) skill produced it,
23
- - improvements fixed in passing → improve ledger **auto-close** (`status: fixed`),
24
- - touched code → **incremental re-scan** of that domain so the brain stays fresh.
18
+ ## Procedure
25
19
 
26
- ## Procedure (draft)
27
- 1. Resume from `active.md` (plan + active task).
28
- 2. Per task: **recall** brain sliceimplement (convention-adherent) **nudge** a
29
- blast-radius fix (opt-in) → done-check.
30
- 3. **Capture-back**: distill decisions/scars (prism-validated); auto-close fixed improvements;
31
- re-scan the touched domain.
32
- 4. Verify (prism-style) before declaring done. Update `active.md`.
20
+ 1. **Resume** — `get_active_plan` (platform) or local `active.md`; staleness check
21
+ (envelope `brainForSha` vs local stamp → prompt `rafa push` if behind).
22
+ 2. **Per task:** atlas recalls implements prism validates vs `## Done-check`
23
+ bloom sweeps (push new / close fixed / nudge) → update the child file's `status`
24
+ periodic `rafa compile` + `rafa push` so the platform (and every MCP consumer)
25
+ reflects live progress.
26
+ 3. **Brain changes mid-build** execution may invalidate rules or teach new ones.
27
+ v1: run a full `/rafa scan` (regenerate → prism → compile → push); the whole
28
+ brain re-stamps at the new sha, so `brain = f(code@sha)` stays exact. NEVER
29
+ hand-edit brain files around the gate.
30
+ 4. **Verify** (prism-style) before declaring the plan done; final compile + push.
33
31
 
34
32
  ## Deferred / open
35
- - **The capture engine** — `Stop`/`PostToolUse` hooks to make capture-back *automatic*, not
36
- model-discretion (the deterministic-enforcement lesson, applied to capture).
37
- - **Incremental re-scan** — changed files + seam-neighbors only (keep it cheap).
33
+ - **The capture engine** — `Stop`/`PostToolUse` hooks making the gates + capture
34
+ automatic rather than conductor-driven SOP (deterministic-enforcement lesson).
35
+ - **Incremental re-scan** — cite-graph invalidation (diff invalidate notes citing
36
+ changed files → re-verify/regenerate only those). Needs: partial-brain cache-key
37
+ semantics + the seam-neighbor scope rule. Designed (see
38
+ .fable/sessions/2026-07-07-brain-versioning-and-incremental.md); first post-loop item.
38
39
  - Show-thinking + pivot protocol from the atlas character.
@@ -1,38 +1,47 @@
1
- # plan — brain-grounded ADR decomposition (DRAFT · capability #3)
2
-
3
- > Status: **draft / spec.** The first *consumer* of the stores — where the brain starts to pay
4
- > off. Depends on: brain (#1), ledger (#2). Built after the stores are proven in real use.
5
-
6
- Turn an intent into an approval-gated **ADR plan**, *grounded* in the brain (don't re-derive)
7
- and *aware* of the ledger (fold improvement into the work). Invoke via `/plan <intent>`.
8
-
9
- ## Why plan fits the mission (the integration)
10
-
11
- plan is the first place knowledge + improvement meet real work, all keyed on **domain**:
12
-
13
- - **RECALL** — load the brain slice for the domains the intent touches: conventions,
14
- contracts, flows, and **non-exemplars** (don't copy the grandfathered demo the audit's
15
- exact lesson). The plan starts from known ground, not a cold read. *This is the 100×.*
16
- - **BLAST RADIUS** name the domains/nodes the change touches (from the brain's map), so risk
17
- is known up front and `build` can recall/nudge per task.
18
- - **NUDGE** — query the improve ledger for open improvements in that blast radius; surface the
19
- **top-leverage** ones as optional *"while-you're-here"* tasks (leverage-ranked, dismissible).
20
- - **LEVERAGE (orchestrate, don't bury)** — match the intent against available capabilities
21
- (committed skills/agents/MCP **+** the dev's personal ones, discovered live) and recommend the
22
- best-fit to the host: *"there's a `tdd-workflow` skill for this use it."* Never plan to
23
- hand-roll what a capability already does; flag config that would help. Recommend; the host
24
- orchestrates (a hook can't auto-invoke).
25
-
26
- ## Procedure (draft)
27
- 1. **Recall** the brain slice for the intent's domains (notes + contracts + non-exemplars).
28
- 2. **Decompose** ADR: decision + rationale + alternatives + tradeoffs + risk surface + non-goals.
29
- 3. **Tasks** — each bound to domains/nodes (so `build` can recall + nudge per task) with a done-check.
30
- 4. **Ledger nudge** surface open improvements in the blast radius as optional, leverage-ranked tasks.
31
- 4b. **Leverage-match** recommend the existing skills/tools/MCP that fit the tasks; flag tooling-fit
32
- improvements. Don't plan to reinvent a present capability.
33
- 5. **Approval gate** (ExitPlanMode). Materialize `.rafa/plans/<id>.md` + `active.md {plan, task:1}`.
1
+ # plan — brain-grounded, prism-validated decomposition (capability #3)
2
+
3
+ > Status: **active.** The first *consumer* of the stores — where the brain starts to
4
+ > pay off. Depends on: brain (#1), ledger (#2). Invoke via `/rafa plan <intent>`.
5
+
6
+ Turn an intent into an approval-gated plan (contract §7 files), *grounded* in the
7
+ brain (don't re-derive), *aware* of the ledger (fold improvement into the work), and
8
+ **validated by prism before the owner ever sees it**.
9
+
10
+ ## The trio at plan time
11
+
12
+ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
13
+
14
+ - **atlas drafts** RECALL the brain slice for the intent's domains through the
15
+ knowledge MCP (`get_coverage` to navigate `search_knowledge` `get_rule` /
16
+ `get_playbook`, including non-exemplars), name the BLAST RADIUS from coverage,
17
+ decompose into parent + child plan files.
18
+ - **bloom pulls** — `list_improvements` in the blast radius; surface the
19
+ top-leverage open items as optional *"while-you're-here"* child tasks
20
+ (leverage-ranked, dismissible, never blocking).
21
+ - **prism validates the plan itself** before the approval gate: is every task
22
+ grounded in real brain/code (not hallucinated ground)? does every child carry a
23
+ `## Done-check` (the expected outcome prism will validate execution against)?
24
+ A plan whose children lack Done-checks is REJECTED here — compile never parses
25
+ bodies (invariant #3); this gate is prism's.
26
+
27
+ ## Procedure
28
+
29
+ 1. **Staleness check** — compare the platform envelope's `brainForSha` against the
30
+ local brain stamp; if the platform is behind, surface "run `rafa push`" (never
31
+ proceed silently on knowledge you know is stale never block either).
32
+ 2. **Recall** (atlas, via MCP) **decompose** ADR-style: decision + rationale +
33
+ alternatives + risk surface + non-goals; tasks bound to domains with a
34
+ `## Done-check` each.
35
+ 3. **Ledger pull** (bloom) → optional leverage tasks in the blast radius.
36
+ 4. **Leverage-match** — recommend existing skills/tools/MCP that fit the tasks;
37
+ never plan to hand-roll what a capability already does.
38
+ 5. **prism plan-validation** → REJECT/fix loop until clean.
39
+ 6. **Approval gate** (owner). Then materialize `plans/<plan>/*.md` per contract §7
40
+ (parent + child-owned files, globally-unique prefixed ids) + `active.md` pointer
41
+ → `rafa compile` → `rafa push` (the plan is now resumable from ANY session,
42
+ machine, or teammate via `get_active_plan`).
34
43
 
35
44
  ## Deferred / open
36
- - Capture-back of plan-time decisions → brain (prism-validated) — needs the capture engine (hooks).
37
- - Pivot detection (conversation-as-steering; mark superseded, the path is data).
38
- - Plan lifecycle + tiering (cap, archive→remote, restore) — designed in `.arohi/architecture.md`.
45
+ - Capture-back of plan-time decisions → brain (needs the capture engine).
46
+ - Pivot detection (mark superseded; the path is data).
47
+ - Plan lifecycle + tiering (cap, archive→remote, restore).
@@ -37,10 +37,10 @@ doesn't validate, compile fails.
37
37
  | coverage | `brain/coverage.md` | **structured** | §6 | atlas |
38
38
  | improvement | `improve/improvements/*.md` | **structured** | §3 | bloom |
39
39
  | ledger | `improve/ledger.md` | **structured** | §5 | bloom |
40
- | plan | `plans/**/*.md` | **structured** | §7 *(wired: Slice 4)* | plan/build |
40
+ | plan | `plans/**/*.md` | **structured** | §7 | plan/build |
41
41
  | log | `brain/log.md` | **verbatim** | — (prose trail) | conductor |
42
42
  | citation-check | `**/citation-check.md` | **generated** | — (by `verify-citations`) | tool |
43
- | active pointer | `active.md` | **generated** | — (`# <plan-id>` or "No active plan") | conductor |
43
+ | active pointer | `active.md` | **generated** | — (`# <plan-id>` or "No active plan"; compile emits it as `activePlanId`, §7) | conductor |
44
44
  | blueprint | `capabilities/*`, `bin/*`, `contract.md` | **verbatim** | — (shipped SOPs/tools) | rafa |
45
45
 
46
46
  `structured` types are compiled into `manifest.json` (§1) — that JSON is the ONLY thing the
@@ -157,7 +157,20 @@ JSON. This is the exact shape the platform binds to.
157
157
  }
158
158
  ],
159
159
 
160
- "plans": [] // v1: may be empty; shape in §6
160
+ "activePlanId": null, // from active.md: plan id, or null
161
+ "plans": [
162
+ {
163
+ "id": "multitenant", // == filename stem, globally unique
164
+ "plan": "multitenant", // the parent plan id
165
+ "parent": null, // parent plan id, or null for the root
166
+ "kind": "parent", // "parent" | "child"
167
+ "title": "…",
168
+ "status": "in-progress", // omitted when absent (parent may omit)
169
+ "branch": "feat/mt-c1", // omitted when absent
170
+ "baseSha": "abc123", // omitted when absent
171
+ "path": "plans/multitenant/multitenant.md" // prose body (lazy fetch)
172
+ }
173
+ ]
161
174
  }
162
175
  ```
163
176
 
@@ -265,7 +278,7 @@ domains: { design-system: mapped, components: mapped, api: thin, external-integr
265
278
 
266
279
  ---
267
280
 
268
- ## 7. Plan files — `plans/**/*.md` (Slice 4)
281
+ ## 7. Plan files — `plans/**/*.md`
269
282
 
270
283
  Each child owns exactly one file (so merges never conflict; parent progress is
271
284
  derived by counting).
@@ -273,19 +286,36 @@ derived by counting).
273
286
  ```yaml
274
287
  ---
275
288
  schemaVersion: 1
276
- id: c1-schema # required · == filename stem
289
+ id: multitenant-c1-schema # required · == filename stem · GLOBALLY unique across plans/**
277
290
  plan: multitenant # required · the parent plan id
278
- parent: null # required · parent plan id, or null for the root
291
+ parent: multitenant # required · parent plan id (child), or null (root)
279
292
  kind: parent | child # required
280
293
  title: … # required
281
294
  status: todo | in-progress | done | blocked # required (child); parent may omit
282
295
  branch: feat/mt-c1 # optional · the branch this executes on
283
296
  baseSha: abc123 # optional · commit it was cut from
284
297
  ---
298
+ (prose body — for a child it MUST contain a `## Done-check` section: the expected
299
+ outcome prism validates execution against. Compile never parses bodies; a missing
300
+ Done-check is rejected by prism's PLAN validation, not by compile.)
285
301
  ```
286
302
 
287
- Parent progress is `count(children where status == done) / count(children)` —
288
- derived, never stored.
303
+ Rules (all compile-enforced unless noted):
304
+
305
+ - **Global id uniqueness.** `plans/**` is nested, so stems could collide; a duplicate
306
+ plan id anywhere under `plans/` is a compile error. Convention: prefix children
307
+ with the plan id (`<plan>-c1-….md`).
308
+ - **`kind: parent`** → `parent` MUST be `null` and `plan` MUST equal `id`.
309
+ - **`kind: child`** → `plan` MUST resolve to a `kind: parent` file in the same
310
+ compile, and `parent` MUST equal `plan` (flat parent→children in v1). A dangling
311
+ child is a loud error; a parent with zero children is valid (a plan just created).
312
+ - **Progress is never stored.** A `progress` key in plan frontmatter is a compile
313
+ error. Parent progress is `count(children where status == done) / count(children)`
314
+ — derived at read time, everywhere (compile, platform, MCP).
315
+ - **`active.md` → `activePlanId`.** Compile reads the generated pointer: a first line
316
+ `# <plan-id>` must resolve to a `kind: parent` plan (else compile error);
317
+ `# No active plan` or a missing `active.md` → `activePlanId: null` (documented
318
+ default — the pointer is generated, absence means "none", never a guess of one).
289
319
 
290
320
  ---
291
321
 
@@ -300,4 +330,67 @@ derived, never stored.
300
330
  blocks the push (better than a silently-wrong brain).
301
331
  - The validator **reports, never auto-fills** a required value.
302
332
 
333
+ ---
334
+
335
+ ## 9. The read side — knowledge MCP (platform-served)
336
+
337
+ The platform serves the **ingested** brain to any MCP client (the build agent, a
338
+ teammate's session, Slack/incident.io agents) — the read-side twin of this contract.
339
+ One backend: the platform. Local files are the *authoring* surface; the platform is
340
+ the *serving* surface, and it serves only what passed compile + ingest.
341
+
342
+ **Envelope — mandatory on every tool response:**
343
+
344
+ ```jsonc
345
+ {
346
+ "source": "platform",
347
+ "brainForSha": "<snapshot codeSha>", // the ingested manifest's codeSha — one source
348
+ "ingestedAt": "<ISO time of ingest>",
349
+ "schemaVersion": 1,
350
+ "synthesized": false // raw tools: always false
351
+ }
352
+ ```
353
+
354
+ **Error semantics (no assumed values, outward):** snapshot has an `ingestError` →
355
+ every tool returns that error loudly, never partial data. Repo connected but never
356
+ pushed (no snapshot) → `"no brain ingested — run rafa push"`. Invalid/mismatched key
357
+ → loud auth error, never a fallback. Empty search → empty result, never a stretched
358
+ match.
359
+
360
+ **Tools (read-only — ALL writes flow files → compile → push → ingest):**
361
+
362
+ | Tool | Args | Returns |
363
+ |---|---|---|
364
+ | `get_brain_status` | `repo` | envelope + per-type counts, health, coverage summary, `activePlanId`, `ingestError?` |
365
+ | `get_coverage` | `repo` | coverage rows (the domain map — how an agent navigates) |
366
+ | `search_knowledge` | `q`, `types?`, `domain?`, `limit?` | ranked candidates (fields per type below) |
367
+ | `get_rule` / `get_playbook` / `get_improvement` | `repo`, `id` | full frontmatter + lazy-fetched body + cites as objects |
368
+ | `list_improvements` | `repo`, `status?`, `priority?`, `domain?` | ledger rows |
369
+ | `get_plan` | `repo`, `id` (parent or child) | parent + all children + **derived** progress; a child id resolves the whole plan with `requestedChild` set |
370
+ | `get_active_plan` | `repo` | resolves manifest `activePlanId` → `get_plan`, or "no active plan" |
371
+
372
+ `repo` must match the key's scope exactly; `branch` is reserved (branch-keyed
373
+ instances land in Slice 2 — third-party clients default to the production brain,
374
+ i.e. the default branch's instance).
375
+
376
+ **Search = deterministic lexical retrieval; the server retrieves, the agent decides.**
377
+ Fields searched per type — rule/playbook: `title, summary, domain, type` ·
378
+ improvement: `title, summary, lens, blast_radius` · plan: `title, plan, parent`.
379
+ Score = Σ over matched case-folded query tokens of field weight (`title` 3 ·
380
+ `domain`/`lens`/`blast_radius`/`type`/`plan`/`parent` 2 · `summary` 1); tie-break
381
+ score desc then id asc; fields a type lacks are omitted, never defaulted. Candidates
382
+ carry `matchKind: "lexical"` (`"semantic"` reserved). `ask_knowledge` is a reserved
383
+ tool id for the synthesis layer (responses will carry `synthesized: true`).
384
+
385
+ **Auth:** per-repo machine keys, minted on the platform, sent as
386
+ `Authorization: Bearer`. Keys are stored hashed platform-side and live client-side in
387
+ `~/.config/rafinery/credentials.json` — never inside the code repo or this brain repo.
388
+
389
+ **Client wiring (written by `rafa init`, secret-free where committed):** a key is
390
+ minted at setup generation and delivered consume-once through the setup fetch;
391
+ `.mcp.json` (committed) registers the `rafinery` server with
392
+ `Authorization: Bearer ${RAFA_MCP_KEY}` env expansion; the raw key lands in
393
+ `.claude/settings.local.json` `env` (gitignored) and `~/.config/rafinery/credentials.json`
394
+ (0600). Teammates mint their own key on the platform (repo → Agent access).
395
+
303
396
  This contract is the thing to test against.
package/lib/init.mjs CHANGED
@@ -11,6 +11,12 @@ import { execSync } from "node:child_process";
11
11
  import { join } from "node:path";
12
12
  import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
13
13
  import { mergeClaudeSettings } from "./claude-config.mjs";
14
+ import {
15
+ credentialsPath,
16
+ mergeLocalEnv,
17
+ mergeMcpJson,
18
+ saveCredentials,
19
+ } from "./mcp-config.mjs";
14
20
  import { stampCurrent } from "./stamp.mjs";
15
21
  import { notifyIfBehind } from "./version-check.mjs";
16
22
  import { isInteractive, ask } from "./prompt.mjs";
@@ -108,12 +114,49 @@ export default async function init(args) {
108
114
  // Grant rafa's vendored tools their permissions (merge, never overwrite).
109
115
  reportSettings(TARGET);
110
116
 
111
- // Keep the brain out of the code repo's diffs.
112
- const gi = join(TARGET, ".gitignore");
113
- const body = existsSync(gi) ? readFileSync(gi, "utf8") : "";
114
- if (!/^\.rafa\/?\s*$/m.test(body)) {
115
- appendFileSync(gi, `${body.endsWith("\n") || body === "" ? "" : "\n"}.rafa/\n`);
116
- console.log(" ✓ .rafa/ → .gitignore");
117
+ // Keep the brain out of the code repo's diffs; keep the per-dev MCP key out of git.
118
+ const ensureIgnored = (line, label) => {
119
+ const gi = join(TARGET, ".gitignore");
120
+ const body = existsSync(gi) ? readFileSync(gi, "utf8") : "";
121
+ const re = new RegExp(`^${line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\/?\\s*$`, "m");
122
+ if (!re.test(body)) {
123
+ appendFileSync(gi, `${body.endsWith("\n") || body === "" ? "" : "\n"}${line}\n`);
124
+ console.log(` ✓ ${label} → .gitignore`);
125
+ }
126
+ };
127
+ ensureIgnored(".rafa/", ".rafa/");
128
+
129
+ // Wire the platform's knowledge MCP (contract §9): committed .mcp.json entry
130
+ // (secret-free, ${RAFA_MCP_KEY} expansion) + the key in gitignored local config
131
+ // + the per-machine canonical copy. The key was minted at setup generation and
132
+ // is consume-once: a re-fetched setup arrives keyless and we say so loudly.
133
+ if (p?.mcpUrl) {
134
+ if (p.agentKey) {
135
+ const cred = saveCredentials(p.repoId, p.agentKey, p.mcpUrl);
136
+ console.log(
137
+ cred.skipped
138
+ ? ` ! MCP key NOT saved to ${credentialsPath()} — ${cred.skipped}`
139
+ : ` ✓ MCP key → ${cred.file} (0600)`,
140
+ );
141
+ const env = mergeLocalEnv(TARGET, p.agentKey);
142
+ console.log(
143
+ env.skipped
144
+ ? ` ! ${env.skipped} — export RAFA_MCP_KEY yourself (key is in ${credentialsPath()})`
145
+ : " ✓ RAFA_MCP_KEY → .claude/settings.local.json (gitignored)",
146
+ );
147
+ ensureIgnored(".claude/settings.local.json", ".claude/settings.local.json");
148
+ } else {
149
+ console.log(
150
+ ` ! this setup's MCP key was already consumed${p.agentKeyConsumed ? "" : " or absent"} — ` +
151
+ "mint one on the platform (repo → Agent access) and set RAFA_MCP_KEY in .claude/settings.local.json",
152
+ );
153
+ }
154
+ const mcp = mergeMcpJson(TARGET, p.mcpUrl);
155
+ console.log(
156
+ mcp.skipped
157
+ ? ` ! ${mcp.skipped} — add the "rafinery" server pointing at ${p.mcpUrl} yourself`
158
+ : ` ✓ knowledge MCP → .mcp.json (rafinery · ${p.mcpUrl})`,
159
+ );
117
160
  }
118
161
 
119
162
  // Stamp the versions this repo was provisioned at — the compat anchor `update`/`migrate`
@@ -145,9 +188,15 @@ export default async function init(args) {
145
188
 
146
189
  console.log(
147
190
  `\n✓ Provisioned.\n\nNext steps:\n` +
148
- " 1. Restart Claude Code in this repo (reload the /rafa command + agents).\n" +
191
+ " 1. Restart Claude Code in this repo (reload the /rafa command + agents" +
192
+ (p?.mcpUrl ? " + the rafinery MCP server" : "") +
193
+ ").\n" +
149
194
  " 2. Run /rafa scan — build + validate the brain, then rafa push to sync it.\n" +
150
- " 3. Open the repo on the rafinery platform — its Overview shows the brain once pushed.",
195
+ " 3. Open the repo on the rafinery platform — its Overview shows the brain once pushed." +
196
+ (p?.mcpUrl
197
+ ? "\n 4. After the first push, any MCP client with a key can query this repo's knowledge\n" +
198
+ ` (endpoint: ${p.mcpUrl} · keys: repo → Agent access on the platform).`
199
+ : ""),
151
200
  );
152
201
 
153
202
  await notifyIfBehind();
@@ -0,0 +1,93 @@
1
+ // MCP wiring for `rafa init` — connect the client repo to the platform's
2
+ // knowledge MCP (.rafa/contract.md §9) with zero secrets in committed files:
3
+ //
4
+ // .mcp.json committed · server entry, key via ${RAFA_MCP_KEY}
5
+ // .claude/settings.local.json gitignored · env.RAFA_MCP_KEY = the raw key
6
+ // ~/.config/rafinery/credentials.json per-machine canonical copy (0600)
7
+ //
8
+ // Merge, never clobber: unparseable files are left alone (reported), other
9
+ // servers/env keys are preserved; only the `rafinery` entry is ours to replace.
10
+
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ writeFileSync,
16
+ chmodSync,
17
+ } from "node:fs";
18
+ import { homedir } from "node:os";
19
+ import { join, dirname } from "node:path";
20
+
21
+ const readJson = (file) => {
22
+ if (!existsSync(file)) return null;
23
+ try {
24
+ return JSON.parse(readFileSync(file, "utf8"));
25
+ } catch (e) {
26
+ return { error: e instanceof Error ? e.message : String(e) };
27
+ }
28
+ };
29
+ const writeJson = (file, data) => {
30
+ mkdirSync(dirname(file), { recursive: true });
31
+ writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
32
+ };
33
+
34
+ export function credentialsPath() {
35
+ return join(homedir(), ".config", "rafinery", "credentials.json");
36
+ }
37
+
38
+ // Canonical per-machine store: { repos: { <repoId>: { key, mcpUrl, savedAt } } }.
39
+ // Never inside the code repo or the brain repo — nothing to gitignore, nothing
40
+ // for `rafa push` to publish.
41
+ export function saveCredentials(repoId, key, mcpUrl) {
42
+ const file = credentialsPath();
43
+ const existing = readJson(file);
44
+ if (existing?.error)
45
+ return { skipped: `credentials.json is not valid JSON (${existing.error})` };
46
+ const data = existing ?? {};
47
+ data.repos = { ...(data.repos ?? {}) };
48
+ data.repos[repoId] = { key, mcpUrl, savedAt: new Date().toISOString() };
49
+ writeJson(file, data);
50
+ try {
51
+ chmodSync(file, 0o600);
52
+ } catch {
53
+ /* best-effort on non-POSIX */
54
+ }
55
+ return { file };
56
+ }
57
+
58
+ // .mcp.json — the committed, shareable server registration. The Authorization
59
+ // header uses ${RAFA_MCP_KEY} env expansion, so the file carries NO secret; each
60
+ // teammate's key comes from their own settings.local.json / shell env.
61
+ export function mergeMcpJson(targetDir, mcpUrl) {
62
+ const file = join(targetDir, ".mcp.json");
63
+ const existing = readJson(file);
64
+ if (existing?.error)
65
+ return { skipped: `.mcp.json is not valid JSON (${existing.error})` };
66
+ const data = existing ?? {};
67
+ data.mcpServers = { ...(data.mcpServers ?? {}) };
68
+ const entry = {
69
+ type: "http",
70
+ url: mcpUrl,
71
+ headers: { Authorization: "Bearer ${RAFA_MCP_KEY}" },
72
+ };
73
+ const same = JSON.stringify(data.mcpServers.rafinery) === JSON.stringify(entry);
74
+ data.mcpServers.rafinery = entry;
75
+ if (!same) writeJson(file, data);
76
+ return { file, changed: !same };
77
+ }
78
+
79
+ // .claude/settings.local.json — Claude Code's gitignored per-checkout settings;
80
+ // its `env` map is applied to the session, which is exactly where a per-dev
81
+ // secret belongs. Everything else in the file is preserved.
82
+ export function mergeLocalEnv(targetDir, key) {
83
+ const file = join(targetDir, ".claude", "settings.local.json");
84
+ const existing = readJson(file);
85
+ if (existing?.error)
86
+ return {
87
+ skipped: `.claude/settings.local.json is not valid JSON (${existing.error})`,
88
+ };
89
+ const data = existing ?? {};
90
+ data.env = { ...(data.env ?? {}), RAFA_MCP_KEY: key };
91
+ writeJson(file, data);
92
+ return { file };
93
+ }
package/lib/releases.mjs CHANGED
@@ -56,6 +56,19 @@ export const RELEASES = [
56
56
  "(no more `.new` clutter; stale `.new` cleaned up; --overwrite/--keep flags). New `/rafa help` " +
57
57
  "with a full command reference for both surfaces. (Completes 0.2.1, published before these landed.)",
58
58
  },
59
+ {
60
+ version: "0.3.0",
61
+ contract: 1,
62
+ plans: 1,
63
+ requires: "update",
64
+ summary:
65
+ "Knowledge MCP — the read side. `init` now wires the platform's read-only knowledge MCP: " +
66
+ "a secret-free `.mcp.json` entry (Bearer ${RAFA_MCP_KEY}), the per-dev key into gitignored " +
67
+ "`.claude/settings.local.json` + `~/.config/rafinery/credentials.json` (0600), delivered " +
68
+ "consume-once via the setup URL. Contract §9 (read-side tools + envelope) and §7 plans are " +
69
+ "wired: compile now emits `plans[]` + `activePlanId` with global-id/parent/progress rules. " +
70
+ "Re-run `/rafa scan` (or push) to surface plans in your manifest; no re-scan required to adopt.",
71
+ },
59
72
  ];
60
73
 
61
74
  // The release this CLI build ships (last entry).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafinery/cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
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": {