@rafinery/cli 0.2.0 → 0.2.2
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 +31 -1
- package/bin/rafa.mjs +4 -0
- package/blueprint/.claude/commands/rafa.md +75 -3
- package/blueprint/.rafa/capabilities/leverage.md +10 -4
- package/lib/blueprint.mjs +54 -25
- package/lib/init.mjs +38 -6
- package/lib/leverage.mjs +3 -1
- package/lib/migrate.mjs +7 -1
- package/lib/migrations/index.mjs +8 -0
- package/lib/prompt.mjs +21 -0
- package/lib/releases.mjs +29 -4
- package/lib/stamp.mjs +9 -1
- package/lib/update.mjs +45 -28
- package/lib/version-check.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,39 @@ 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.2.2
|
|
8
|
+
|
|
9
|
+
**Adopt with:** `npx @rafinery/cli@latest update` — no re-scan, no migration.
|
|
10
|
+
|
|
11
|
+
Completes 0.2.1 (which was published before these two landed):
|
|
12
|
+
|
|
13
|
+
- **shadcn-style overwrite, no more `.new` clutter.** When a vendored file the dev owns already
|
|
14
|
+
exists and differs, `init`/`update` **ask** "overwrite? [y/N/a/q]" and act on the answer —
|
|
15
|
+
instead of silently writing `<file>.new`. Flags `--overwrite` / `--keep` skip the prompt; a
|
|
16
|
+
non-TTY (CI / agent shell) safely keeps files. Stale `.new` files from older CLIs are cleaned up.
|
|
17
|
+
- **`/rafa help`** — a full command reference for both surfaces (in-editor `/rafa` vs terminal
|
|
18
|
+
`rafa` CLI), also shown on no argument or an unrecognized command.
|
|
19
|
+
|
|
20
|
+
## 0.2.1
|
|
21
|
+
|
|
22
|
+
**Adopt with:** `npx @rafinery/cli@latest update` (or `/rafa update` in Claude Code) — no re-scan,
|
|
23
|
+
no migration. Recommended: `/rafa leverage` to tune your toolbox.
|
|
24
|
+
|
|
25
|
+
- **Intelligence where it belongs.** `leverage` and `migrate` are now `/rafa` commands: the CLI
|
|
26
|
+
*detects* (deterministic), the LLM *acts* — `/rafa leverage` edits your config exactly (merges
|
|
27
|
+
settings, wires the MCP, scaffolds a skill, on approval); `/rafa migrate` rewrites plans to a new
|
|
28
|
+
schema preserving meaning, then compile-gates them.
|
|
29
|
+
- **Two-layer update — one job each, no double work.**
|
|
30
|
+
- `rafa update` (CLI) = the **blueprint side**: pulls the latest blueprint into your repo and runs
|
|
31
|
+
the blueprint/mechanical migrations. It prints the **release notes** since your version and
|
|
32
|
+
**exactly what the brain needs** next (computed from the contract/plans schema deltas).
|
|
33
|
+
- `/rafa update` (Claude Code) = the **brain side**: runs the migrations that need intelligence
|
|
34
|
+
(re-scan a stale brain, rewrite plans to a new shape). It does **not** re-run the CLI.
|
|
35
|
+
- `/rafa` redirects the terminal-only `compile` instead of erroring on it.
|
|
36
|
+
|
|
7
37
|
## 0.2.0
|
|
8
38
|
|
|
9
|
-
**Adopt with:** `
|
|
39
|
+
**Adopt with:** `npx @rafinery/cli@latest update` — no re-scan, no migration.
|
|
10
40
|
Your brain data is still valid (contract v1, plans v1 unchanged).
|
|
11
41
|
|
|
12
42
|
- **Non-destructive vendoring.** `init`/`update` never clobber tuned files: owned files
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: rafa — init
|
|
2
|
+
description: rafa — init · scan · improve · push · leverage · migrate · update · help. Usage: /rafa <init|scan|improve|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**.
|
|
@@ -56,8 +56,80 @@ high-leverage P0/P1s — don't nag; the dev triages.**
|
|
|
56
56
|
`node .rafa/bin/rafa-push.mjs` — commit `.rafa/` and push to `origin` (the brain repo) with the
|
|
57
57
|
dev's own git auth. Use after a scan, or to re-sync a brain that changed.
|
|
58
58
|
|
|
59
|
-
## (
|
|
60
|
-
|
|
59
|
+
## `leverage` — tune the dev's toolbox (rafa's 3rd mission; intelligence + exact edits)
|
|
60
|
+
Detection is cheap and deterministic; **acting needs intelligence — that's why this is a
|
|
61
|
+
`/rafa` command, not just the `rafa leverage` CLI.** The CLI *reports*; you *fix*.
|
|
62
|
+
1. **Inspect** the committed toolbox: `.claude/settings.json`, `.mcp.json`, `.claude/skills/`,
|
|
63
|
+
`.claude/commands/`, and the stack (`package.json` / dirs). (`rafa leverage` gives the fast
|
|
64
|
+
deterministic signal; here you reason over the real files.)
|
|
65
|
+
2. **Reason** per [`.rafa/capabilities/leverage.md`](../../.rafa/capabilities/leverage.md): what's
|
|
66
|
+
missing / misconfigured / unused — permission gaps, an MCP the stack needs but isn't wired, a
|
|
67
|
+
repeatable flow worth capturing as a skill, the best-fit *existing* skill for THIS repo. P1→P3.
|
|
68
|
+
3. **Act — on the dev's approval, edit exactly.** Merge the precise permission entries, write the
|
|
69
|
+
correct `.mcp.json` server block, scaffold the skill. **Merge, never clobber** the dev's config;
|
|
70
|
+
show the diff. Propose, don't force. Never open `.env*` or read a secret value (scan's guardrail).
|
|
71
|
+
|
|
72
|
+
## `migrate` — bring structured files to the current schema (the intelligent half)
|
|
73
|
+
Two channels. **Mechanical** changes (field rename, path move) run deterministically via the
|
|
74
|
+
terminal `rafa migrate`. **Semantic** changes — a plan's *shape* changed and each file must be
|
|
75
|
+
understood to be rewritten correctly — need intelligence, and that's this command.
|
|
76
|
+
1. Read the target schema in [`.rafa/contract.md`](../../.rafa/contract.md) (§6/§7 for plans) and
|
|
77
|
+
the `from` versions in `rafa.json`.
|
|
78
|
+
2. For each affected file under `.rafa/plans/` (and any other structured dir), **rewrite it to
|
|
79
|
+
conform** — preserve meaning, remap fields, fill new required fields sensibly.
|
|
80
|
+
3. Run `node .rafa/bin/rafa-compile.mjs` until it **exits 0** (validate-and-correct loop), surface
|
|
81
|
+
the diff for approval, then offer `rafa push` if this is a connected repo.
|
|
82
|
+
|
|
83
|
+
## `update` — the BRAIN-side of an upgrade (the CLI already did the blueprint side)
|
|
84
|
+
**Two layers, two actors — don't re-run the CLI here.**
|
|
85
|
+
- The terminal **`npx @rafinery/cli@latest update`** already pulled the latest blueprint and ran
|
|
86
|
+
the **blueprint-side** (mechanical/structural) migrations. It also *reported* what the brain needs.
|
|
87
|
+
- **`/rafa update` (this command)** does the **brain-side** migrations — the part that needs
|
|
88
|
+
intelligence — against the blueprint the CLI just installed. It does **not** pull the blueprint
|
|
89
|
+
or re-run the CLI.
|
|
90
|
+
1. **Find the gap.** The brain/plans conform to a *data* version; the blueprint just moved to a
|
|
91
|
+
newer one. Compare `.rafa/manifest.json`'s `schemaVersion` (and `rafa.json`'s `contract`/`plans`)
|
|
92
|
+
to the current `.rafa/contract.md`. That gap is exactly what the CLI update flagged. (If the CLI
|
|
93
|
+
update hasn't run yet, tell the dev to run `npx @rafinery/cli@latest update` first, then return.)
|
|
94
|
+
2. **Migrate the brain side:**
|
|
95
|
+
- **Contract bumped → the brain is a stale cache:** re-scan (`## scan`) to regenerate it, or for
|
|
96
|
+
a targeted change, intelligently rewrite the affected notes to the new schema and re-validate.
|
|
97
|
+
- **Plans shape changed:** rewrite each plan per `## migrate` (preserve meaning), compile to exit 0.
|
|
98
|
+
3. **Record + summarize.** Run `node .rafa/bin/rafa-compile.mjs` to exit 0, then advance the data
|
|
99
|
+
version in `rafa.json` (`contract`/`plans` → the versions you migrated to). Summarize what changed;
|
|
100
|
+
on the dev's approval, `rafa push` if this is a connected repo.
|
|
101
|
+
Never hand-edit around a migration or discard tuned files.
|
|
102
|
+
|
|
103
|
+
## `help` (also: no argument, or an unrecognized command)
|
|
104
|
+
Print this reference verbatim and stop.
|
|
105
|
+
|
|
106
|
+
**rafa has two surfaces.** `/rafa <cmd>` runs the intelligent, in-editor passes; the terminal
|
|
107
|
+
`rafa` CLI (run via `npx @rafinery/cli@latest <cmd>`) does the deterministic plumbing. Several
|
|
108
|
+
names exist on both — the CLI does the mechanical half, `/rafa` the intelligent half.
|
|
109
|
+
|
|
110
|
+
**`/rafa` — in the editor (LLM does the work):**
|
|
111
|
+
| Command | What it does |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `/rafa init` | First run: ensure structure, then run the full scan pass. |
|
|
114
|
+
| `/rafa scan [--brain-only]` | The full pass: know → verify → improve → push. `--brain-only` stops after the brain is validated (a cheap knowledge refresh). |
|
|
115
|
+
| `/rafa improve` | The improvement pass — bloom writes a cited, prioritized (P0–P3) ledger. |
|
|
116
|
+
| `/rafa push` | (Re-)push the brain to the brain remote (your git auth). |
|
|
117
|
+
| `/rafa leverage` | Tune your toolbox: reason over config and, on approval, apply fixes exactly — merge settings, wire an MCP, scaffold a skill. |
|
|
118
|
+
| `/rafa migrate` | Semantic migration — rewrite plans to a new schema preserving meaning, then compile-gate. |
|
|
119
|
+
| `/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). |
|
|
120
|
+
|
|
121
|
+
**`rafa` — in the terminal (`npx @rafinery/cli@latest <cmd>`; deterministic):**
|
|
122
|
+
| Command | What it does |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `init [<setup-url>]` | Vendor the blueprint into the repo (+ wire the brain remote from a platform setup URL). |
|
|
125
|
+
| `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. |
|
|
126
|
+
| `compile` | Run the contract gate → `.rafa/manifest.json`. |
|
|
127
|
+
| `push` | Compile, then push `.rafa/` to the brain remote. |
|
|
128
|
+
| `leverage` | Detect toolbox gaps (deterministic) and print prioritized tips — the detector for `/rafa leverage`. |
|
|
129
|
+
| `migrate` | Run the mechanical (deterministic) migrations. |
|
|
130
|
+
|
|
131
|
+
If the dev typed a **terminal-only** command (`compile`) as a slash command, point them to the
|
|
132
|
+
shell (`rafa compile`) as well. Then stop.
|
|
61
133
|
|
|
62
134
|
---
|
|
63
135
|
Token discipline: glob/grep/AST before reading; scoped reads; deterministic extraction
|
|
@@ -6,8 +6,13 @@ config layer (permissions, skills, MCP servers, hooks, commands) that decides ho
|
|
|
6
6
|
the AI engineer actually is. It surfaces what's missing, misconfigured, or unused, and points
|
|
7
7
|
at the fix. This is platform DNA: **leverage what's present; never reinvent it.**
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
ranks, and reports
|
|
9
|
+
**Two layers — detection is deterministic, action needs intelligence:**
|
|
10
|
+
- **`rafa leverage`** (CLI) — the cheap **detector**. Reads the repo, ranks, and *reports*; it
|
|
11
|
+
**changes nothing**. Good for a notify/CI signal, and as structured input to the LLM.
|
|
12
|
+
- **`/rafa leverage`** (in Claude Code) — the **actor**. rafa reasons over the real config and,
|
|
13
|
+
on the dev's approval, **applies the fix exactly** — merges the precise permission entries,
|
|
14
|
+
writes the correct `.mcp.json` block, scaffolds the skill. The CLI can't do this; only the
|
|
15
|
+
intelligence can edit correctly. The detector finds the gap; the LLM closes it.
|
|
11
16
|
|
|
12
17
|
---
|
|
13
18
|
|
|
@@ -21,8 +26,9 @@ ranks, and reports; it **changes nothing**.
|
|
|
21
26
|
advisor. Few, relevant, dismissible.
|
|
22
27
|
3. **Leverage-ranked** — `P1` security/correctness → `P2` friction/leverage → `P3` nice-to-have.
|
|
23
28
|
Lead with impact × ease. Never dump an unranked wall.
|
|
24
|
-
4. **
|
|
25
|
-
|
|
29
|
+
4. **Approval-gated, never authoritarian** — the dev owns their setup. `rafa leverage` (CLI) only
|
|
30
|
+
proposes. `/rafa leverage` *applies*, but **only on the dev's approval and always as a merge** —
|
|
31
|
+
show the diff, never clobber, propose don't force.
|
|
26
32
|
5. **Tool-agnostic by construction** — the advice engine knows nothing about any specific tool;
|
|
27
33
|
all tool knowledge lives in an **adapter**. This is what makes "leverage what's available"
|
|
28
34
|
true for Codex, Cursor, or any other toolchain, not just Claude Code.
|
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
|
@@ -13,16 +13,46 @@ import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
|
|
|
13
13
|
import { mergeClaudeSettings } from "./claude-config.mjs";
|
|
14
14
|
import { stampCurrent } from "./stamp.mjs";
|
|
15
15
|
import { notifyIfBehind } from "./version-check.mjs";
|
|
16
|
+
import { isInteractive, ask } from "./prompt.mjs";
|
|
16
17
|
|
|
17
|
-
//
|
|
18
|
+
// Decide what to do when an owned file the dev may have tuned already exists and differs.
|
|
19
|
+
// shadcn-style: ask before clobbering. Honors `--overwrite`/`-o` (yes to all) and `--keep`
|
|
20
|
+
// (no to all); in a non-TTY (CI / agent shell) it defaults to KEEP so it never blocks or clobbers.
|
|
21
|
+
export function makeConflictResolver(args = []) {
|
|
22
|
+
const force = args.includes("--overwrite") || args.includes("-o");
|
|
23
|
+
const keepAll = args.includes("--keep");
|
|
24
|
+
const interactive = isInteractive();
|
|
25
|
+
let all = false; // overwrite everything remaining
|
|
26
|
+
let quit = false; // keep everything remaining
|
|
27
|
+
return async (rel) => {
|
|
28
|
+
if (force || all) return "overwrite";
|
|
29
|
+
if (keepAll || quit || !interactive) return "keep";
|
|
30
|
+
const a = await ask(
|
|
31
|
+
` • ${rel} exists and differs — overwrite? [y = yes · N = keep · a = all · q = keep rest] `,
|
|
32
|
+
);
|
|
33
|
+
if (a === "a") {
|
|
34
|
+
all = true;
|
|
35
|
+
return "overwrite";
|
|
36
|
+
}
|
|
37
|
+
if (a === "q") {
|
|
38
|
+
quit = true;
|
|
39
|
+
return "keep";
|
|
40
|
+
}
|
|
41
|
+
return a === "y" || a === "yes" ? "overwrite" : "keep";
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// One-line summary of a copyBlueprint report + per-file notes for kept/cleaned/errored files.
|
|
18
46
|
export function reportBlueprint(r) {
|
|
19
47
|
const n = (a) => a.length;
|
|
20
48
|
console.log(
|
|
21
49
|
` ✓ blueprint: ${n(r.created)} new · ${n(r.updated)} updated · ` +
|
|
22
|
-
`${n(r.unchanged)} unchanged · ${n(r.
|
|
50
|
+
`${n(r.unchanged)} unchanged · ${n(r.kept)} kept`,
|
|
23
51
|
);
|
|
24
|
-
for (const rel of r.
|
|
25
|
-
console.log(` • kept your ${rel}
|
|
52
|
+
for (const rel of r.kept)
|
|
53
|
+
console.log(` • kept your ${rel} (re-run with --overwrite to replace, or /rafa update to merge)`);
|
|
54
|
+
for (const rel of r.cleaned) console.log(` • removed stale ${rel}`);
|
|
55
|
+
for (const err of r.errors) console.log(` ! ${err}`);
|
|
26
56
|
}
|
|
27
57
|
|
|
28
58
|
// Union rafa's required permissions into .claude/settings.json without clobbering it.
|
|
@@ -69,8 +99,10 @@ export default async function init(args) {
|
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
// Vendor the blueprint into .claude/ + .rafa/ (you own these copies) — non-destructive:
|
|
72
|
-
// owned files (agents, capabilities)
|
|
73
|
-
reportBlueprint(
|
|
102
|
+
// tools/contract move in lockstep; owned files (agents, capabilities) prompt before overwrite.
|
|
103
|
+
reportBlueprint(
|
|
104
|
+
await copyBlueprint(blueprintSource(), TARGET, { onConflict: makeConflictResolver(args) }),
|
|
105
|
+
);
|
|
74
106
|
if (!existsSync(join(TARGET, ".rafa", "active.md")))
|
|
75
107
|
writeFileSync(join(TARGET, ".rafa", "active.md"), "# No active plan\n");
|
|
76
108
|
// Grant rafa's vendored tools their permissions (merge, never overwrite).
|
package/lib/leverage.mjs
CHANGED
|
@@ -31,6 +31,8 @@ export default async function leverage() {
|
|
|
31
31
|
console.log("");
|
|
32
32
|
}
|
|
33
33
|
console.log(
|
|
34
|
-
`${tips.length} suggestion(s), most impactful first.
|
|
34
|
+
`${tips.length} suggestion(s), most impactful first. This is the detector — it changed nothing.\n` +
|
|
35
|
+
"To have rafa apply these exactly (merge settings, wire the MCP, scaffold a skill), run\n" +
|
|
36
|
+
" /rafa leverage in Claude Code — it edits with your approval.",
|
|
35
37
|
);
|
|
36
38
|
}
|
package/lib/migrate.mjs
CHANGED
|
@@ -14,9 +14,15 @@ export default async function migrate() {
|
|
|
14
14
|
|
|
15
15
|
if (pending.length === 0) {
|
|
16
16
|
console.log(
|
|
17
|
-
`✓ Nothing to migrate — plans v${from.plans}, contract v${from.contract} ` +
|
|
17
|
+
`✓ Nothing mechanical to migrate — plans v${from.plans}, contract v${from.contract} ` +
|
|
18
18
|
`already match ${CURRENT.version}.`,
|
|
19
19
|
);
|
|
20
|
+
if (CURRENT.plans > from.plans || CURRENT.contract > from.contract) {
|
|
21
|
+
console.log(
|
|
22
|
+
" ↳ but the schema version advanced. If a plan/brain SHAPE changed and files need\n" +
|
|
23
|
+
" rewriting, that's a semantic migration — run /rafa migrate in Claude Code.",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
20
26
|
// Still advance the stamp so the recorded CLI version reflects reality.
|
|
21
27
|
stampCurrent(cwd);
|
|
22
28
|
return;
|
package/lib/migrations/index.mjs
CHANGED
|
@@ -6,10 +6,18 @@
|
|
|
6
6
|
// Contract:
|
|
7
7
|
// id string stable identifier, shown in the log
|
|
8
8
|
// kind "plans" | "contract" | "config"
|
|
9
|
+
// mode "mechanical" | "assisted" default "mechanical"
|
|
9
10
|
// summary string one line: what it changes + why
|
|
10
11
|
// appliesTo(from, to) => boolean from = stampedVersions(cwd), to = CURRENT
|
|
11
12
|
// run(cwd) => { changed: string[], notes: string[] } idempotent; report what it touched
|
|
12
13
|
//
|
|
14
|
+
// mode distinguishes the two migration channels:
|
|
15
|
+
// "mechanical" — a deterministic transform (field rename, path move, add-default). `run` does
|
|
16
|
+
// it here in the CLI, no LLM needed.
|
|
17
|
+
// "assisted" — the shape changed and each file must be UNDERSTOOD to be rewritten. The CLI
|
|
18
|
+
// can't do this correctly; `run` is a no-op that flags it, and `/rafa migrate`
|
|
19
|
+
// (the LLM) rewrites the files against the contract. Intelligence lives there.
|
|
20
|
+
//
|
|
13
21
|
// Empty by design until a release actually changes a shape — the SEAM is what matters,
|
|
14
22
|
// so the first breaking change ships its migration here and `rafa migrate` just works.
|
|
15
23
|
// Example (kept as a reference, not registered):
|
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
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
// package.json). It declares the contract + plans schema versions that release ships and
|
|
4
4
|
// the strongest action a client must take to adopt it:
|
|
5
5
|
//
|
|
6
|
-
// requires:
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// requires: "update" | "migrate" | "rescan" the strongest structural action
|
|
7
|
+
// ("update" → just re-sync files; "migrate"/"rescan" are also computed
|
|
8
|
+
// deterministically from the contract/plans version deltas below).
|
|
9
|
+
// recommends: string[] soft follow-ups this release suggests (e.g. ["leverage"]) — shown
|
|
10
|
+
// by `update` as recommended, not required.
|
|
9
11
|
//
|
|
10
|
-
// `update` reads this to tell the client — transparently — what an upgrade means for them
|
|
12
|
+
// `update` reads this to tell the client — transparently — what an upgrade means for them:
|
|
13
|
+
// the release notes since their version, and exactly which actions the upgrade needs.
|
|
11
14
|
|
|
12
15
|
import { readFileSync } from "node:fs";
|
|
13
16
|
import { dirname, join } from "node:path";
|
|
@@ -31,6 +34,28 @@ export const RELEASES = [
|
|
|
31
34
|
"Non-destructive vendoring (merge settings, never clobber tuned agents); secrets " +
|
|
32
35
|
"guardrail in scan; `rafa leverage` toolbox advisor; version-aware updates + `rafa migrate`.",
|
|
33
36
|
},
|
|
37
|
+
{
|
|
38
|
+
version: "0.2.1",
|
|
39
|
+
contract: 1,
|
|
40
|
+
plans: 1,
|
|
41
|
+
requires: "update",
|
|
42
|
+
recommends: ["leverage"],
|
|
43
|
+
summary:
|
|
44
|
+
"leverage & migrate are now intelligent `/rafa` commands — the LLM applies toolbox fixes " +
|
|
45
|
+
"and rewrites plans exactly. Two-layer update: `rafa update` (CLI) does the blueprint side + " +
|
|
46
|
+
"blueprint migrations and reports what the brain needs; new `/rafa update` does the brain-side " +
|
|
47
|
+
"migrations (re-scan / rewrite). `rafa update` now prints release notes + required actions.",
|
|
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
|
+
},
|
|
34
59
|
];
|
|
35
60
|
|
|
36
61
|
// The release this CLI build ships (last entry).
|
package/lib/stamp.mjs
CHANGED
|
@@ -46,10 +46,18 @@ export function stampedVersions(cwd) {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Re-stamp to the versions this CLI build ships (called after init
|
|
49
|
+
// Re-stamp to the versions this CLI build ships (called after init, or after update when NO
|
|
50
|
+
// brain-side migration remains — i.e. blueprint AND data are current together).
|
|
50
51
|
export function stampCurrent(cwd, extra = {}) {
|
|
51
52
|
return writeStamp(cwd, {
|
|
52
53
|
...extra,
|
|
53
54
|
rafa: { cli: CLI_VERSION, contract: CURRENT.contract, plans: CURRENT.plans },
|
|
54
55
|
});
|
|
55
56
|
}
|
|
57
|
+
|
|
58
|
+
// Advance ONLY the blueprint/CLI version — the data (brain/plans) version is left where it is,
|
|
59
|
+
// so `/rafa update` can still see the gap and run the brain-side migrations. Use after
|
|
60
|
+
// `rafa update` when brain-side work remains.
|
|
61
|
+
export function stampCli(cwd) {
|
|
62
|
+
return writeStamp(cwd, { rafa: { cli: CLI_VERSION } });
|
|
63
|
+
}
|
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";
|
|
12
|
-
import { CURRENT, releasesAfter } from "./releases.mjs";
|
|
13
|
-
import { stampedVersions, stampCurrent } from "./stamp.mjs";
|
|
11
|
+
import { reportBlueprint, reportSettings, makeConflictResolver } from "./init.mjs";
|
|
12
|
+
import { CURRENT, CLI_VERSION, releasesAfter } from "./releases.mjs";
|
|
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,42 +27,59 @@ 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. ──
|
|
36
|
+
// (Anything needing intelligence is left for the brain-side pass — /rafa update.)
|
|
37
|
+
const pending = pendingMigrations(from, CURRENT);
|
|
38
|
+
const mechanical = pending.filter((m) => (m.mode ?? "mechanical") !== "assisted");
|
|
39
|
+
for (const m of mechanical) {
|
|
40
|
+
try {
|
|
41
|
+
const r = m.run(TARGET);
|
|
42
|
+
console.log(` ✓ migrated ${m.id}: ${r.changed.length} file(s)`);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.log(` ! migration ${m.id} failed: ${e instanceof Error ? e.message : e}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Release notes: what changed since the client's version. ──
|
|
34
49
|
const news = releasesAfter(from.cli);
|
|
35
50
|
if (news.length) {
|
|
36
|
-
console.log(`\n
|
|
37
|
-
for (const r of news) console.log(`
|
|
51
|
+
console.log(`\n Release notes (${from.cli} → ${CLI_VERSION}):`);
|
|
52
|
+
for (const r of news) console.log(` ${r.version} — ${r.summary}`);
|
|
38
53
|
}
|
|
39
54
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
` ⚠ Brain contract v${from.contract} → v${CURRENT.contract}: your brain is now stale.\n` +
|
|
48
|
-
" Run /rafa scan to regenerate it, then rafa push.",
|
|
55
|
+
// ── Brain-side follow-ups: the CLI can't do these — they need intelligence. Route to /rafa update. ──
|
|
56
|
+
// The gap is measured against the DATA version (from.contract/plans), which only advances when the
|
|
57
|
+
// brain/plans are actually regenerated — not by this blueprint sync.
|
|
58
|
+
const brain = [];
|
|
59
|
+
if (CURRENT.contract > from.contract) {
|
|
60
|
+
brain.push(
|
|
61
|
+
`Re-scan the brain (contract v${from.contract} → v${CURRENT.contract}; the brain is a stale cache)`,
|
|
49
62
|
);
|
|
50
63
|
}
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
` ⚠ Plan/structure schema changed (plans v${from.plans} → v${CURRENT.plans}).\n` +
|
|
54
|
-
" Run rafa migrate to bring your files forward.",
|
|
55
|
-
);
|
|
64
|
+
if (CURRENT.plans > from.plans || pending.some((m) => m.mode === "assisted")) {
|
|
65
|
+
brain.push(`Rewrite plans to the new shape (plans v${from.plans} → v${CURRENT.plans})`);
|
|
56
66
|
}
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
|
|
68
|
+
console.log("");
|
|
69
|
+
if (brain.length) {
|
|
70
|
+
console.log(" Brain-side follow-ups — run /rafa update in Claude Code (needs intelligence):");
|
|
71
|
+
for (const b of brain) console.log(` • ${b}`);
|
|
72
|
+
// Blueprint is current, but the data still lags — keep the gap visible for /rafa update.
|
|
73
|
+
stampCli(TARGET);
|
|
74
|
+
} else {
|
|
75
|
+
console.log(" ✓ No brain migration needed — your brain data is still valid.");
|
|
76
|
+
stampCurrent(TARGET);
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
const recs = new Set(news.flatMap((r) => r.recommends ?? []));
|
|
80
|
+
if (recs.has("leverage")) console.log(" ↳ recommended: /rafa leverage (tune your toolbox).");
|
|
63
81
|
|
|
64
82
|
console.log(
|
|
65
|
-
"\n
|
|
66
|
-
"Review any `.new` files, then restart Claude Code to reload the agents.",
|
|
83
|
+
"\n Owned files you tuned were preserved (review any `.new`). Restart Claude Code to reload the agents.",
|
|
67
84
|
);
|
|
68
85
|
}
|
package/lib/version-check.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export async function notifyIfBehind(installed = CLI_VERSION) {
|
|
|
27
27
|
if (!latest || cmpSemver(latest, installed) <= 0) return latest;
|
|
28
28
|
console.log(
|
|
29
29
|
`\n ⬆ rafinery ${latest} is available (you're on ${installed}).\n` +
|
|
30
|
-
` Update:
|
|
30
|
+
` Update: npx @rafinery/cli@latest update (fetches the latest CLI + vendors it into your repo)\n`,
|
|
31
31
|
);
|
|
32
32
|
return latest;
|
|
33
33
|
}
|
package/package.json
CHANGED