@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 +26 -0
- package/bin/rafa.mjs +23 -1
- package/blueprint/.claude/commands/rafa.md +53 -1
- 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/init.mjs +57 -8
- package/lib/mcp-config.mjs +93 -0
- package/lib/releases.mjs +13 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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/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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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