@rafinery/cli 0.2.1 → 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 +39 -0
- package/bin/rafa.mjs +27 -1
- package/blueprint/.claude/commands/rafa.md +82 -7
- package/blueprint/.rafa/bin/rafa-compile.mjs +89 -1
- package/blueprint/.rafa/capabilities/build.md +30 -29
- package/blueprint/.rafa/capabilities/plan.md +45 -36
- package/blueprint/.rafa/contract.md +101 -8
- package/lib/blueprint.mjs +54 -25
- package/lib/init.mjs +95 -14
- package/lib/mcp-config.mjs +93 -0
- package/lib/prompt.mjs +21 -0
- package/lib/releases.mjs +23 -0
- package/lib/update.mjs +5 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@ 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
|
+
|
|
33
|
+
## 0.2.2
|
|
34
|
+
|
|
35
|
+
**Adopt with:** `npx @rafinery/cli@latest update` — no re-scan, no migration.
|
|
36
|
+
|
|
37
|
+
Completes 0.2.1 (which was published before these two landed):
|
|
38
|
+
|
|
39
|
+
- **shadcn-style overwrite, no more `.new` clutter.** When a vendored file the dev owns already
|
|
40
|
+
exists and differs, `init`/`update` **ask** "overwrite? [y/N/a/q]" and act on the answer —
|
|
41
|
+
instead of silently writing `<file>.new`. Flags `--overwrite` / `--keep` skip the prompt; a
|
|
42
|
+
non-TTY (CI / agent shell) safely keeps files. Stale `.new` files from older CLIs are cleaned up.
|
|
43
|
+
- **`/rafa help`** — a full command reference for both surfaces (in-editor `/rafa` vs terminal
|
|
44
|
+
`rafa` CLI), also shown on no argument or an unrecognized command.
|
|
45
|
+
|
|
7
46
|
## 0.2.1
|
|
8
47
|
|
|
9
48
|
**Adopt with:** `npx @rafinery/cli@latest update` (or `/rafa update` in Claude Code) — no re-scan,
|
package/bin/rafa.mjs
CHANGED
|
@@ -29,6 +29,10 @@ Commands:
|
|
|
29
29
|
init [<setup-url>] Provision this repo — vendor the blueprint (agents, capabilities,
|
|
30
30
|
contract, gates) and, with a platform setup URL, wire the brain remote.
|
|
31
31
|
update Re-sync the blueprint into this repo (you own your copy; opt-in updates).
|
|
32
|
+
|
|
33
|
+
Flags (init/update): --overwrite replace changed owned files without asking
|
|
34
|
+
--keep keep all changed owned files without asking
|
|
35
|
+
(default: ask per file; in a non-TTY it keeps them)
|
|
32
36
|
compile Validate the brain against the contract → .rafa/manifest.json.
|
|
33
37
|
push Compile, then push .rafa/ to your brain remote.
|
|
34
38
|
leverage Inspect your agent toolbox (permissions, skills, MCP) and print
|
|
@@ -36,7 +40,29 @@ Commands:
|
|
|
36
40
|
migrate Bring this repo's structured files (plans, config) up to the schema
|
|
37
41
|
the installed CLI ships. Idempotent; review the diff after.
|
|
38
42
|
|
|
39
|
-
|
|
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
|
|
40
66
|
|
|
41
67
|
Docs: ${pkg.homepage ?? "https://github.com/rafinery-ai/rafinery"}`);
|
|
42
68
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: rafa — init · scan · improve · push · leverage · migrate · update. Usage: /rafa <init|scan|improve|push|leverage|migrate|update> [--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.
|
|
@@ -100,13 +132,56 @@ understood to be rewritten correctly — need intelligence, and that's this comm
|
|
|
100
132
|
on the dev's approval, `rafa push` if this is a connected repo.
|
|
101
133
|
Never hand-edit around a migration or discard tuned files.
|
|
102
134
|
|
|
103
|
-
## (no argument, or unrecognized)
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
## `help` (also: no argument, or an unrecognized command)
|
|
136
|
+
Print this reference verbatim and stop.
|
|
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
|
+
|
|
156
|
+
**rafa has two surfaces.** `/rafa <cmd>` runs the intelligent, in-editor passes; the terminal
|
|
157
|
+
`rafa` CLI (run via `npx @rafinery/cli@latest <cmd>`) does the deterministic plumbing. Several
|
|
158
|
+
names exist on both — the CLI does the mechanical half, `/rafa` the intelligent half.
|
|
159
|
+
|
|
160
|
+
**`/rafa` — in the editor (LLM does the work):**
|
|
161
|
+
| Command | What it does |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `/rafa init` | First run: ensure structure, then run the full scan pass. |
|
|
164
|
+
| `/rafa scan [--brain-only]` | The full pass: know → verify → improve → push. `--brain-only` stops after the brain is validated (a cheap knowledge refresh). |
|
|
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. |
|
|
168
|
+
| `/rafa push` | (Re-)push the brain to the brain remote (your git auth). |
|
|
169
|
+
| `/rafa leverage` | Tune your toolbox: reason over config and, on approval, apply fixes exactly — merge settings, wire an MCP, scaffold a skill. |
|
|
170
|
+
| `/rafa migrate` | Semantic migration — rewrite plans to a new schema preserving meaning, then compile-gate. |
|
|
171
|
+
| `/rafa update` | The brain-side of an upgrade: after the CLI syncs the blueprint, run the migrations that need intelligence (re-scan a stale brain / rewrite plans). |
|
|
172
|
+
|
|
173
|
+
**`rafa` — in the terminal (`npx @rafinery/cli@latest <cmd>`; deterministic):**
|
|
174
|
+
| Command | What it does |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `init [<setup-url>]` | Vendor the blueprint into the repo (+ wire the brain remote from a platform setup URL). |
|
|
177
|
+
| `update [--overwrite\|--keep]` | Blueprint-side of an upgrade: re-sync the blueprint (asks before overwriting files you tuned), run mechanical migrations, and report what the brain needs next. |
|
|
178
|
+
| `compile` | Run the contract gate → `.rafa/manifest.json`. |
|
|
179
|
+
| `push` | Compile, then push `.rafa/` to the brain remote. |
|
|
180
|
+
| `leverage` | Detect toolbox gaps (deterministic) and print prioritized tips — the detector for `/rafa leverage`. |
|
|
181
|
+
| `migrate` | Run the mechanical (deterministic) migrations. |
|
|
106
182
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
anything else unrecognized, print this usage line and stop.
|
|
183
|
+
If the dev typed a **terminal-only** command (`compile`) as a slash command, point them to the
|
|
184
|
+
shell (`rafa compile`) as well. Then stop.
|
|
110
185
|
|
|
111
186
|
---
|
|
112
187
|
Token discipline: glob/grep/AST before reading; scoped reads; deterministic extraction
|
|
@@ -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
|
-
|
|
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,
|
|
1
|
+
# build — execute the plan, trio-choreographed, compounding (capability #4)
|
|
2
2
|
|
|
3
|
-
> Status: **
|
|
4
|
-
> fire — the mission payoff. Depends on: plan (#3), brain (#1), ledger (#2).
|
|
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
|
|
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
|
-
##
|
|
10
|
+
## The trio at build time
|
|
9
11
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
2. Per task
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
- **Incremental re-scan** —
|
|
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
|
|
2
|
-
|
|
3
|
-
> Status: **
|
|
4
|
-
> off. Depends on: brain (#1), ledger (#2).
|
|
5
|
-
|
|
6
|
-
Turn an intent into an approval-gated
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 (
|
|
37
|
-
- Pivot detection (
|
|
38
|
-
- Plan lifecycle + tiering (cap, archive→remote, restore)
|
|
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
|
|
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
|
-
"
|
|
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`
|
|
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
|
|
289
|
+
id: multitenant-c1-schema # required · == filename stem · GLOBALLY unique across plans/**
|
|
277
290
|
plan: multitenant # required · the parent plan id
|
|
278
|
-
parent:
|
|
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
|
-
|
|
288
|
-
|
|
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/blueprint.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
readFileSync,
|
|
11
11
|
readdirSync,
|
|
12
12
|
statSync,
|
|
13
|
+
rmSync,
|
|
13
14
|
} from "node:fs";
|
|
14
15
|
import { dirname, join } from "node:path";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -97,39 +98,67 @@ function sameBytes(a, b) {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
// Copy the blueprint from `from` into `to`.
|
|
100
|
-
// - mode "vendor" (default): non-destructive. lockstep files
|
|
101
|
-
// created if absent, left untouched if identical, and
|
|
102
|
-
//
|
|
101
|
+
// - mode "vendor" (default): non-destructive. lockstep files (tools + contract) overwrite;
|
|
102
|
+
// owned files are created if absent, left untouched if identical, and — if the dev has
|
|
103
|
+
// changed them — resolved via `onConflict(rel)` which returns "overwrite" | "keep"
|
|
104
|
+
// (shadcn-style: ask before clobbering). No `.new` files; any stale `.new` left by an older
|
|
105
|
+
// CLI is cleaned up.
|
|
103
106
|
// - mode "bundle": force-copy everything (building the package tarball into a scratch dir).
|
|
104
|
-
// Returns { created, updated, unchanged,
|
|
105
|
-
export function copyBlueprint(from, to, { mode = "vendor" } = {}) {
|
|
107
|
+
// Returns { created, updated, unchanged, kept, cleaned, errors } — arrays of relative paths.
|
|
108
|
+
export async function copyBlueprint(from, to, { mode = "vendor", onConflict } = {}) {
|
|
106
109
|
const { owned, lockstep } = blueprintFiles(from);
|
|
107
|
-
const report = { created: [], updated: [], unchanged: [],
|
|
110
|
+
const report = { created: [], updated: [], unchanged: [], kept: [], cleaned: [], errors: [] };
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
// Remove a stale `<dst>.new` written by an older CLI — it's our own debris, now obsolete.
|
|
113
|
+
const dropStaleNew = (rel, dst) => {
|
|
114
|
+
const stale = `${dst}.new`;
|
|
115
|
+
if (existsSync(stale)) {
|
|
116
|
+
try {
|
|
117
|
+
rmSync(stale);
|
|
118
|
+
report.cleaned.push(`${rel}.new`);
|
|
119
|
+
} catch {
|
|
120
|
+
/* leave it; not worth failing the update over */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const put = async (rel, isOwned) => {
|
|
110
126
|
const src = join(from, rel);
|
|
111
127
|
if (!existsSync(src)) return;
|
|
112
128
|
const dst = join(to, rel);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
try {
|
|
130
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
131
|
+
if (!existsSync(dst)) {
|
|
132
|
+
cpSync(src, dst);
|
|
133
|
+
report.created.push(rel);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (sameBytes(src, dst)) {
|
|
137
|
+
dropStaleNew(rel, dst);
|
|
138
|
+
report.unchanged.push(rel);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// exists AND differs
|
|
142
|
+
if (mode === "bundle" || !isOwned) {
|
|
143
|
+
cpSync(src, dst); // lockstep, or a scratch bundle: moves with the CLI
|
|
144
|
+
report.updated.push(rel);
|
|
145
|
+
} else {
|
|
146
|
+
// owned + modified: ask before clobbering the dev's tuning (default: keep).
|
|
147
|
+
const decision = onConflict ? await onConflict(rel) : "keep";
|
|
148
|
+
if (decision === "overwrite") {
|
|
149
|
+
cpSync(src, dst);
|
|
150
|
+
report.updated.push(rel);
|
|
151
|
+
} else {
|
|
152
|
+
report.kept.push(rel);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
dropStaleNew(rel, dst);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
report.errors.push(`${rel}: ${e instanceof Error ? e.message : e}`);
|
|
129
158
|
}
|
|
130
159
|
};
|
|
131
160
|
|
|
132
|
-
for (const rel of lockstep) put(rel, false);
|
|
133
|
-
for (const rel of owned) put(rel, true);
|
|
161
|
+
for (const rel of lockstep) await put(rel, false);
|
|
162
|
+
for (const rel of owned) await put(rel, true);
|
|
134
163
|
return report;
|
|
135
164
|
}
|
package/lib/init.mjs
CHANGED
|
@@ -11,18 +11,54 @@ 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";
|
|
22
|
+
import { isInteractive, ask } from "./prompt.mjs";
|
|
23
|
+
|
|
24
|
+
// Decide what to do when an owned file the dev may have tuned already exists and differs.
|
|
25
|
+
// shadcn-style: ask before clobbering. Honors `--overwrite`/`-o` (yes to all) and `--keep`
|
|
26
|
+
// (no to all); in a non-TTY (CI / agent shell) it defaults to KEEP so it never blocks or clobbers.
|
|
27
|
+
export function makeConflictResolver(args = []) {
|
|
28
|
+
const force = args.includes("--overwrite") || args.includes("-o");
|
|
29
|
+
const keepAll = args.includes("--keep");
|
|
30
|
+
const interactive = isInteractive();
|
|
31
|
+
let all = false; // overwrite everything remaining
|
|
32
|
+
let quit = false; // keep everything remaining
|
|
33
|
+
return async (rel) => {
|
|
34
|
+
if (force || all) return "overwrite";
|
|
35
|
+
if (keepAll || quit || !interactive) return "keep";
|
|
36
|
+
const a = await ask(
|
|
37
|
+
` • ${rel} exists and differs — overwrite? [y = yes · N = keep · a = all · q = keep rest] `,
|
|
38
|
+
);
|
|
39
|
+
if (a === "a") {
|
|
40
|
+
all = true;
|
|
41
|
+
return "overwrite";
|
|
42
|
+
}
|
|
43
|
+
if (a === "q") {
|
|
44
|
+
quit = true;
|
|
45
|
+
return "keep";
|
|
46
|
+
}
|
|
47
|
+
return a === "y" || a === "yes" ? "overwrite" : "keep";
|
|
48
|
+
};
|
|
49
|
+
}
|
|
16
50
|
|
|
17
|
-
// One-line summary of a copyBlueprint report
|
|
51
|
+
// One-line summary of a copyBlueprint report + per-file notes for kept/cleaned/errored files.
|
|
18
52
|
export function reportBlueprint(r) {
|
|
19
53
|
const n = (a) => a.length;
|
|
20
54
|
console.log(
|
|
21
55
|
` ✓ blueprint: ${n(r.created)} new · ${n(r.updated)} updated · ` +
|
|
22
|
-
`${n(r.unchanged)} unchanged · ${n(r.
|
|
56
|
+
`${n(r.unchanged)} unchanged · ${n(r.kept)} kept`,
|
|
23
57
|
);
|
|
24
|
-
for (const rel of r.
|
|
25
|
-
console.log(` • kept your ${rel}
|
|
58
|
+
for (const rel of r.kept)
|
|
59
|
+
console.log(` • kept your ${rel} (re-run with --overwrite to replace, or /rafa update to merge)`);
|
|
60
|
+
for (const rel of r.cleaned) console.log(` • removed stale ${rel}`);
|
|
61
|
+
for (const err of r.errors) console.log(` ! ${err}`);
|
|
26
62
|
}
|
|
27
63
|
|
|
28
64
|
// Union rafa's required permissions into .claude/settings.json without clobbering it.
|
|
@@ -69,19 +105,58 @@ export default async function init(args) {
|
|
|
69
105
|
}
|
|
70
106
|
|
|
71
107
|
// Vendor the blueprint into .claude/ + .rafa/ (you own these copies) — non-destructive:
|
|
72
|
-
// owned files (agents, capabilities)
|
|
73
|
-
reportBlueprint(
|
|
108
|
+
// tools/contract move in lockstep; owned files (agents, capabilities) prompt before overwrite.
|
|
109
|
+
reportBlueprint(
|
|
110
|
+
await copyBlueprint(blueprintSource(), TARGET, { onConflict: makeConflictResolver(args) }),
|
|
111
|
+
);
|
|
74
112
|
if (!existsSync(join(TARGET, ".rafa", "active.md")))
|
|
75
113
|
writeFileSync(join(TARGET, ".rafa", "active.md"), "# No active plan\n");
|
|
76
114
|
// Grant rafa's vendored tools their permissions (merge, never overwrite).
|
|
77
115
|
reportSettings(TARGET);
|
|
78
116
|
|
|
79
|
-
// Keep the brain out of the code repo's diffs.
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
);
|
|
85
160
|
}
|
|
86
161
|
|
|
87
162
|
// Stamp the versions this repo was provisioned at — the compat anchor `update`/`migrate`
|
|
@@ -113,9 +188,15 @@ export default async function init(args) {
|
|
|
113
188
|
|
|
114
189
|
console.log(
|
|
115
190
|
`\n✓ Provisioned.\n\nNext steps:\n` +
|
|
116
|
-
" 1. Restart Claude Code in this repo (reload the /rafa command + agents
|
|
191
|
+
" 1. Restart Claude Code in this repo (reload the /rafa command + agents" +
|
|
192
|
+
(p?.mcpUrl ? " + the rafinery MCP server" : "") +
|
|
193
|
+
").\n" +
|
|
117
194
|
" 2. Run /rafa scan — build + validate the brain, then rafa push to sync it.\n" +
|
|
118
|
-
" 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
|
+
: ""),
|
|
119
200
|
);
|
|
120
201
|
|
|
121
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/prompt.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Minimal interactive prompt helpers. Used to ask before overwriting a file the dev owns —
|
|
2
|
+
// shadcn-style — instead of silently writing `.new` clutter.
|
|
3
|
+
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { stdin, stdout } from "node:process";
|
|
6
|
+
|
|
7
|
+
// True only when we can actually prompt (both ends are a TTY). In a pipe / CI / an
|
|
8
|
+
// agent-spawned shell we must NOT block on input — callers fall back to a safe default.
|
|
9
|
+
export function isInteractive() {
|
|
10
|
+
return Boolean(stdin.isTTY && stdout.isTTY);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Ask a free-form question, return the trimmed lowercased answer ("" if the dev just hits enter).
|
|
14
|
+
export async function ask(question) {
|
|
15
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
16
|
+
try {
|
|
17
|
+
return (await rl.question(question)).trim().toLowerCase();
|
|
18
|
+
} finally {
|
|
19
|
+
rl.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
package/lib/releases.mjs
CHANGED
|
@@ -46,6 +46,29 @@ export const RELEASES = [
|
|
|
46
46
|
"blueprint migrations and reports what the brain needs; new `/rafa update` does the brain-side " +
|
|
47
47
|
"migrations (re-scan / rewrite). `rafa update` now prints release notes + required actions.",
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
version: "0.2.2",
|
|
51
|
+
contract: 1,
|
|
52
|
+
plans: 1,
|
|
53
|
+
requires: "update",
|
|
54
|
+
summary:
|
|
55
|
+
"shadcn-style overwrite prompt on init/update — asks before replacing files you tuned " +
|
|
56
|
+
"(no more `.new` clutter; stale `.new` cleaned up; --overwrite/--keep flags). New `/rafa help` " +
|
|
57
|
+
"with a full command reference for both surfaces. (Completes 0.2.1, published before these landed.)",
|
|
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
|
+
},
|
|
49
72
|
];
|
|
50
73
|
|
|
51
74
|
// The release this CLI build ships (last entry).
|
package/lib/update.mjs
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
|
|
11
|
-
import { reportBlueprint, reportSettings } from "./init.mjs";
|
|
11
|
+
import { reportBlueprint, reportSettings, makeConflictResolver } from "./init.mjs";
|
|
12
12
|
import { CURRENT, CLI_VERSION, releasesAfter } from "./releases.mjs";
|
|
13
13
|
import { stampedVersions, stampCurrent, stampCli } from "./stamp.mjs";
|
|
14
14
|
import { pendingMigrations } from "./migrations/index.mjs";
|
|
15
15
|
import { notifyIfBehind } from "./version-check.mjs";
|
|
16
16
|
|
|
17
|
-
export default async function update() {
|
|
17
|
+
export default async function update(args = []) {
|
|
18
18
|
const TARGET = process.cwd();
|
|
19
19
|
if (!existsSync(join(TARGET, ".rafa")) && !existsSync(join(TARGET, ".claude"))) {
|
|
20
20
|
console.error("✗ This repo isn't provisioned yet — run `rafa init` first.");
|
|
@@ -27,7 +27,9 @@ export default async function update() {
|
|
|
27
27
|
await notifyIfBehind();
|
|
28
28
|
|
|
29
29
|
console.log("rafa update — syncing the blueprint (non-destructive)");
|
|
30
|
-
reportBlueprint(
|
|
30
|
+
reportBlueprint(
|
|
31
|
+
await copyBlueprint(blueprintSource(), TARGET, { onConflict: makeConflictResolver(args) }),
|
|
32
|
+
);
|
|
31
33
|
reportSettings(TARGET);
|
|
32
34
|
|
|
33
35
|
// ── Blueprint-side migrations: mechanical/deterministic, run here in the CLI. ──
|
package/package.json
CHANGED