@rafinery/cli 0.1.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/README.md +23 -0
- package/bin/rafa.mjs +53 -0
- package/blueprint/.claude/agents/atlas.md +81 -0
- package/blueprint/.claude/agents/bloom.md +52 -0
- package/blueprint/.claude/agents/prism.md +39 -0
- package/blueprint/.claude/commands/rafa.md +64 -0
- package/blueprint/.rafa/bin/rafa-compile.mjs +390 -0
- package/blueprint/.rafa/bin/rafa-push.mjs +75 -0
- package/blueprint/.rafa/bin/verify-citations.mjs +153 -0
- package/blueprint/.rafa/capabilities/build.md +38 -0
- package/blueprint/.rafa/capabilities/improve.md +158 -0
- package/blueprint/.rafa/capabilities/plan.md +38 -0
- package/blueprint/.rafa/capabilities/scan.md +252 -0
- package/blueprint/.rafa/capabilities/validate.md +114 -0
- package/blueprint/.rafa/contract.md +303 -0
- package/lib/blueprint.mjs +86 -0
- package/lib/compile.mjs +24 -0
- package/lib/init.mjs +91 -0
- package/lib/push.mjs +24 -0
- package/lib/update.mjs +22 -0
- package/package.json +40 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# rafa brain contract — v1
|
|
2
|
+
|
|
3
|
+
The strict protocol every brain file obeys, and the `manifest.json` the platform
|
|
4
|
+
ingests. This is the **single source of truth** shared by the writers (atlas, bloom,
|
|
5
|
+
prism) and the reader (the platform ingest). If a file violates this, the compile
|
|
6
|
+
step **fails loudly** and the authoring agent must correct it — the platform never
|
|
7
|
+
guesses, never assumes a value.
|
|
8
|
+
|
|
9
|
+
Pipeline:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
agents write .md (frontmatter per this contract)
|
|
13
|
+
→ rafa compile (deterministic: parse frontmatter, VALIDATE, fail loudly)
|
|
14
|
+
├ FAIL → structured error (file · field · rule) → author agent fixes → retry
|
|
15
|
+
└ PASS → emit manifest.json
|
|
16
|
+
→ git push → webhook → INGEST = JSON.parse(manifest) + shape-check → Convex
|
|
17
|
+
→ query APIs (per repo+branch) → platform renders
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Two enforcement points: **compile-time** (structure — this contract) and **prism**
|
|
21
|
+
(semantics — truth of the content). Structure is validated *before* semantics.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## File-type registry — every `.rafa` file has a declared class
|
|
26
|
+
|
|
27
|
+
No consumer ever guesses. **Every** file type is classified here; a `structured` type has
|
|
28
|
+
a schema (parse it), a `verbatim` type is prose (display it, never parse for data), a
|
|
29
|
+
`generated` type is machine-written by a named tool. If a file matching a `structured` path
|
|
30
|
+
doesn't validate, compile fails.
|
|
31
|
+
|
|
32
|
+
| Type | Path glob | Class | Schema | Author |
|
|
33
|
+
|---|---|---|---|---|
|
|
34
|
+
| rule | `brain/rules/*.md` | **structured** | §2 | atlas |
|
|
35
|
+
| playbook | `brain/playbooks/*.md` | **structured** | §2 | atlas |
|
|
36
|
+
| health | `brain/checklist.md` | **structured** | §4 | prism |
|
|
37
|
+
| coverage | `brain/coverage.md` | **structured** | §6 | atlas |
|
|
38
|
+
| improvement | `improve/improvements/*.md` | **structured** | §3 | bloom |
|
|
39
|
+
| ledger | `improve/ledger.md` | **structured** | §5 | bloom |
|
|
40
|
+
| plan | `plans/**/*.md` | **structured** | §7 *(wired: Slice 4)* | plan/build |
|
|
41
|
+
| log | `brain/log.md` | **verbatim** | — (prose trail) | conductor |
|
|
42
|
+
| citation-check | `**/citation-check.md` | **generated** | — (by `verify-citations`) | tool |
|
|
43
|
+
| active pointer | `active.md` | **generated** | — (`# <plan-id>` or "No active plan") | conductor |
|
|
44
|
+
| blueprint | `capabilities/*`, `bin/*`, `contract.md` | **verbatim** | — (shipped SOPs/tools) | rafa |
|
|
45
|
+
|
|
46
|
+
`structured` types are compiled into `manifest.json` (§1) — that JSON is the ONLY thing the
|
|
47
|
+
platform ingests. `verbatim`/`generated` files are shown as-is in the file browser (fetched
|
|
48
|
+
lazily), never parsed. A file under a `structured` path that isn't in this table → compile
|
|
49
|
+
error (no rogue unschema'd data files).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 0. Global rules
|
|
54
|
+
|
|
55
|
+
- **Frontmatter is the only machine-read surface.** The markdown **body is prose**
|
|
56
|
+
and is NEVER parsed for data. Every value the platform shows comes from frontmatter
|
|
57
|
+
(via the compiled manifest), or from the body rendered *verbatim* (never scraped).
|
|
58
|
+
- **`schemaVersion` is required** on every file and in the manifest. Unknown version
|
|
59
|
+
→ ingest reports an error, does not guess.
|
|
60
|
+
- **`id` is required and stable.** It MUST equal the filename stem (`foo.md` → `foo`).
|
|
61
|
+
Renames are safe because rows are keyed by `id`, not path.
|
|
62
|
+
- **Required fields are required.** Missing/empty required field = compile failure.
|
|
63
|
+
The validator **never fills a default for a required field** — that would be
|
|
64
|
+
assuming. Optional fields have documented defaults.
|
|
65
|
+
- **Enums are closed.** A value outside the enum = compile failure (not coerced).
|
|
66
|
+
|
|
67
|
+
### Frontmatter grammar (strict subset — the only shapes allowed)
|
|
68
|
+
|
|
69
|
+
Frontmatter is a leading `---` … `---` block. Within it, only these forms are legal;
|
|
70
|
+
anything else is a compile error:
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
key: scalar # string | number | true | false
|
|
74
|
+
key: "quoted string" # quotes stripped
|
|
75
|
+
key: [a, b, c] # flow list of scalars
|
|
76
|
+
key: { a: x, b: y } # flow map of scalars
|
|
77
|
+
key: # block list …
|
|
78
|
+
- item
|
|
79
|
+
- item
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
No nested block maps, no multiline scalars, no anchors. Deterministic to parse.
|
|
83
|
+
|
|
84
|
+
### cites DSL
|
|
85
|
+
|
|
86
|
+
Each cite is one string: `` `<path>:<line> :: <token>` ``
|
|
87
|
+
|
|
88
|
+
- split on the **first** ` :: ` → (`left`, `token`)
|
|
89
|
+
- in `left`, split on the **last** `:` → (`path`, `line`)
|
|
90
|
+
- `line` is digits (`"42"`) or a range (`"14-18"`)
|
|
91
|
+
|
|
92
|
+
Compile parses each into `{ file, line, token }`. A cite that doesn't match → error.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 1. manifest.json — the ingestion contract
|
|
97
|
+
|
|
98
|
+
Machine-generated by `rafa compile`, committed to the brain repo, read by ingest as
|
|
99
|
+
JSON. This is the exact shape the platform binds to.
|
|
100
|
+
|
|
101
|
+
```jsonc
|
|
102
|
+
{
|
|
103
|
+
"schemaVersion": 1,
|
|
104
|
+
"generatedAt": "2026-07-03T10:00:00Z", // ISO-8601, stamped by compile
|
|
105
|
+
"repo": "owner/repo", // CODE repo full_name (identity)
|
|
106
|
+
"branch": "main", // code branch this brain reflects
|
|
107
|
+
"codeSha": "abc123…", // code commit the brain was built for
|
|
108
|
+
|
|
109
|
+
"health": { // from brain/checklist.md — or null
|
|
110
|
+
"verdict": "PASS", // "PASS" | "ITERATE"
|
|
111
|
+
"score": 95, // 0–100
|
|
112
|
+
"gates": { "fidelity": "pass", "coverage": "pass" },
|
|
113
|
+
"counts": { "blockers": 0, "majors": 0, "minors": 2 }
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
"ledger": { // from improve/ledger.md — or null
|
|
117
|
+
"open": 7,
|
|
118
|
+
"debtScore": 13,
|
|
119
|
+
"byPriority": { "P0": 0, "P1": 1, "P2": 3, "P3": 3 }
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
"coverage": { // from brain/coverage.md — or null
|
|
123
|
+
"domains": [ // one row per domain, its scan status
|
|
124
|
+
{ "domain": "design-system", "status": "mapped" },
|
|
125
|
+
{ "domain": "external-integrations", "status": "thin" }
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
"notes": [
|
|
130
|
+
{
|
|
131
|
+
"id": "agent-name-contract",
|
|
132
|
+
"kind": "rule", // "rule" | "playbook" (from directory)
|
|
133
|
+
"type": "contract", // contract | convention | flow | how-to
|
|
134
|
+
"domain": "web-agent-bridge",
|
|
135
|
+
"title": "…",
|
|
136
|
+
"summary": "…",
|
|
137
|
+
"links": ["other-note-id"], // [] if none
|
|
138
|
+
"failure": "silent", // "silent" | "loud" | omitted
|
|
139
|
+
"cites": [{ "file": "src/x.tsx", "line": "42", "token": "research_agent" }],
|
|
140
|
+
"path": "brain/rules/agent-name-contract.md" // prose body (lazy fetch)
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
|
|
144
|
+
"improvements": [
|
|
145
|
+
{
|
|
146
|
+
"id": "dead-model-options-google-crewai",
|
|
147
|
+
"priority": "P1", // P0 | P1 | P2 | P3
|
|
148
|
+
"lens": "product", // security|correctness|performance|architecture|product|ops
|
|
149
|
+
"status": "open", // open | backlog | fixed | wontfix
|
|
150
|
+
"title": "…",
|
|
151
|
+
"summary": "…",
|
|
152
|
+
"fix": "…",
|
|
153
|
+
"leverage": { "impact": "high", "effort": "low" }, // impact/effort ∈ low|medium|high
|
|
154
|
+
"blastRadius": ["web-agent-bridge"], // [] if none
|
|
155
|
+
"cites": [{ "file": "…", "line": "25", "token": "…" }],
|
|
156
|
+
"path": "improve/improvements/dead-model-options-google-crewai.md"
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
|
|
160
|
+
"plans": [] // v1: may be empty; shape in §6
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Ingest is `JSON.parse` + a shape-check against this schema. Any mismatch (missing
|
|
165
|
+
key, wrong enum, wrong type, unknown `schemaVersion`) → a **surfaced ingest error**
|
|
166
|
+
on the platform, never a guessed value.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 2. Note files — `brain/rules/*.md`, `brain/playbooks/*.md`
|
|
171
|
+
|
|
172
|
+
`kind` is derived from the directory (`rules/` → `rule`, `playbooks/` → `playbook`).
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
---
|
|
176
|
+
schemaVersion: 1
|
|
177
|
+
id: agent-name-contract # required · == filename stem
|
|
178
|
+
type: contract # required · contract | convention | flow | how-to
|
|
179
|
+
domain: web-agent-bridge # required · non-empty
|
|
180
|
+
title: The agent name is a cross-process contract # required
|
|
181
|
+
summary: Only "research_agent" is fully wired end to end # required
|
|
182
|
+
links: [copilotkit-runtime-route-convention] # optional · default []
|
|
183
|
+
failure: silent # optional · silent | loud
|
|
184
|
+
cites: # required · ≥ 1
|
|
185
|
+
- src/lib/model-selector-provider.tsx:42 :: research_agent
|
|
186
|
+
- agents/python/main.py:20 :: research_agent
|
|
187
|
+
---
|
|
188
|
+
(prose body — rendered verbatim, never parsed)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 3. Improvement files — `improve/improvements/*.md`
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
---
|
|
197
|
+
schemaVersion: 1
|
|
198
|
+
id: dead-model-options-google-crewai # required · == filename stem
|
|
199
|
+
priority: P1 # required · P0 | P1 | P2 | P3
|
|
200
|
+
lens: product # required · security|correctness|performance|architecture|product|ops
|
|
201
|
+
status: open # required · open | backlog | fixed | wontfix
|
|
202
|
+
title: Two of the four model options are dead and fail silently # required
|
|
203
|
+
summary: google_genai and crewai route to unwired agents # required
|
|
204
|
+
fix: Wire both agents in main.py, or remove them from the picker # required
|
|
205
|
+
leverage: { impact: high, effort: low } # required · impact/effort ∈ low|medium|high
|
|
206
|
+
blast_radius: [web-agent-bridge, external-integrations] # optional · default []
|
|
207
|
+
cites: # required · ≥ 1
|
|
208
|
+
- src/components/ModelSelector.tsx:25 :: google_genai
|
|
209
|
+
found: 2026-07-02 # optional · ISO date
|
|
210
|
+
---
|
|
211
|
+
(prose body)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Note: `title`, `summary`, `fix` are now **required frontmatter** — previously scraped
|
|
215
|
+
from prose. That scraping is gone; these are authored values.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 4. Health file — `brain/checklist.md`
|
|
220
|
+
|
|
221
|
+
```yaml
|
|
222
|
+
---
|
|
223
|
+
schemaVersion: 1
|
|
224
|
+
verdict: PASS # required · PASS | ITERATE
|
|
225
|
+
score: 95 # required · 0–100
|
|
226
|
+
gates: { fidelity: pass, coverage: pass } # required · each pass | fail
|
|
227
|
+
counts: { blockers: 0, majors: 0, minors: 2 } # required · non-negative ints
|
|
228
|
+
---
|
|
229
|
+
(prism report body)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 5. Ledger file — `improve/ledger.md`
|
|
235
|
+
|
|
236
|
+
The debt figure is **authored in frontmatter** (bloom's number), not parsed from a
|
|
237
|
+
table. `open`/`by_priority` are cross-checked against the improvement rows at ingest.
|
|
238
|
+
Flat keys only — no nesting (compile maps `debt_score`→`debtScore`, `by_priority`→`byPriority`).
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
---
|
|
242
|
+
schemaVersion: 1
|
|
243
|
+
open: 7
|
|
244
|
+
debt_score: 13
|
|
245
|
+
by_priority: { P0: 0, P1: 1, P2: 3, P3: 3 }
|
|
246
|
+
---
|
|
247
|
+
(human-readable trend/tables in the body)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 6. Coverage file — `brain/coverage.md`
|
|
253
|
+
|
|
254
|
+
The scan's breadth report. `domains` is a **flow map** of `domain: status`, one entry per
|
|
255
|
+
domain found (compile emits it as `[{ domain, status }]` in the manifest). Status enum:
|
|
256
|
+
`mapped | thin | stubbed | empty`. The body holds the human per-criterion PASS/FAIL narrative.
|
|
257
|
+
|
|
258
|
+
```yaml
|
|
259
|
+
---
|
|
260
|
+
schemaVersion: 1
|
|
261
|
+
domains: { design-system: mapped, components: mapped, api: thin, external-integrations: empty }
|
|
262
|
+
---
|
|
263
|
+
(per-criterion PASS/FAIL narrative in the body)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 7. Plan files — `plans/**/*.md` (Slice 4)
|
|
269
|
+
|
|
270
|
+
Each child owns exactly one file (so merges never conflict; parent progress is
|
|
271
|
+
derived by counting).
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
---
|
|
275
|
+
schemaVersion: 1
|
|
276
|
+
id: c1-schema # required · == filename stem
|
|
277
|
+
plan: multitenant # required · the parent plan id
|
|
278
|
+
parent: null # required · parent plan id, or null for the root
|
|
279
|
+
kind: parent | child # required
|
|
280
|
+
title: … # required
|
|
281
|
+
status: todo | in-progress | done | blocked # required (child); parent may omit
|
|
282
|
+
branch: feat/mt-c1 # optional · the branch this executes on
|
|
283
|
+
baseSha: abc123 # optional · commit it was cut from
|
|
284
|
+
---
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Parent progress is `count(children where status == done) / count(children)` —
|
|
288
|
+
derived, never stored.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 8. Versioning & the validate-and-correct loop
|
|
293
|
+
|
|
294
|
+
- **`schemaVersion`** bumps when this contract changes incompatibly. Ingest refuses
|
|
295
|
+
an unknown version (surfaced error) rather than mis-reading it.
|
|
296
|
+
- **Validate-and-correct:** `rafa compile` validates every file. On failure it emits
|
|
297
|
+
a structured error — `path · field · rule` — to the **authoring agent** (atlas for
|
|
298
|
+
notes, bloom for improvements/ledger, prism for checklist). The agent edits the
|
|
299
|
+
file and compile re-runs. Bounded retries; if it can't converge, a **hard failure**
|
|
300
|
+
blocks the push (better than a silently-wrong brain).
|
|
301
|
+
- The validator **reports, never auto-fills** a required value.
|
|
302
|
+
|
|
303
|
+
This contract is the thing to test against.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// The blueprint — the exact set of files rafa vendors into a repo (shadcn-style:
|
|
2
|
+
// the client OWNS these copies). This one list is the source of truth for both
|
|
3
|
+
// `bundle-blueprint` (monorepo root → package/blueprint) and `init`/`update`
|
|
4
|
+
// (package/blueprint → your repo).
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
cpSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
statSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
// Files a client owns + tunes (agents, SOPs). Tools + contract move in lockstep.
|
|
17
|
+
export const BLUEPRINT = {
|
|
18
|
+
files: [
|
|
19
|
+
".claude/agents/atlas.md",
|
|
20
|
+
".claude/agents/prism.md",
|
|
21
|
+
".claude/agents/bloom.md",
|
|
22
|
+
".claude/commands/rafa.md",
|
|
23
|
+
".rafa/contract.md",
|
|
24
|
+
".rafa/bin/verify-citations.mjs",
|
|
25
|
+
".rafa/bin/rafa-compile.mjs",
|
|
26
|
+
".rafa/bin/rafa-push.mjs",
|
|
27
|
+
],
|
|
28
|
+
dirs: [".rafa/capabilities"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const HERE = dirname(fileURLToPath(import.meta.url)); // …/packages/cli/lib
|
|
32
|
+
|
|
33
|
+
// Walk up to the monorepo root (the dir that holds the source `.rafa/contract.md`).
|
|
34
|
+
function findRepoRoot() {
|
|
35
|
+
let d = HERE;
|
|
36
|
+
for (let i = 0; i < 8; i++) {
|
|
37
|
+
d = join(d, "..");
|
|
38
|
+
if (existsSync(join(d, ".rafa", "contract.md"))) return d;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Where the blueprint is read FROM when vendoring into a repo:
|
|
44
|
+
// - published package → <package>/blueprint (bundled by `prepare`)
|
|
45
|
+
// - dev in the monorepo → the repo root itself
|
|
46
|
+
export function blueprintSource() {
|
|
47
|
+
const bundled = join(HERE, "..", "blueprint");
|
|
48
|
+
if (existsSync(join(bundled, ".rafa", "contract.md"))) return bundled;
|
|
49
|
+
const root = findRepoRoot();
|
|
50
|
+
if (root) return root;
|
|
51
|
+
throw new Error(
|
|
52
|
+
"blueprint not found — run `npm run bundle` in packages/cli, or reinstall.",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The monorepo root, for `bundle-blueprint` (copies root → package/blueprint).
|
|
57
|
+
export function repoRoot() {
|
|
58
|
+
const root = findRepoRoot();
|
|
59
|
+
if (!root) throw new Error("could not locate the monorepo root");
|
|
60
|
+
return root;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Copy every blueprint file/dir from `from` into `to`. Returns the relative paths written.
|
|
64
|
+
export function copyBlueprint(from, to) {
|
|
65
|
+
const written = [];
|
|
66
|
+
for (const rel of BLUEPRINT.files) {
|
|
67
|
+
const src = join(from, rel);
|
|
68
|
+
if (!existsSync(src)) continue;
|
|
69
|
+
const dst = join(to, rel);
|
|
70
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
71
|
+
cpSync(src, dst);
|
|
72
|
+
written.push(rel);
|
|
73
|
+
}
|
|
74
|
+
for (const rel of BLUEPRINT.dirs) {
|
|
75
|
+
const src = join(from, rel);
|
|
76
|
+
if (!existsSync(src)) continue;
|
|
77
|
+
const dst = join(to, rel);
|
|
78
|
+
mkdirSync(dst, { recursive: true });
|
|
79
|
+
cpSync(src, dst, { recursive: true });
|
|
80
|
+
// list the .md files copied, for a useful summary
|
|
81
|
+
for (const e of readdirSync(src)) {
|
|
82
|
+
if (statSync(join(src, e)).isFile()) written.push(join(rel, e));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return written;
|
|
86
|
+
}
|
package/lib/compile.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// rafa compile — run the vendored contract gate on this repo's brain. Thin wrapper
|
|
2
|
+
// over .rafa/bin/rafa-compile.mjs (the client's own copy = its contract version).
|
|
3
|
+
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
export default async function compile(args) {
|
|
9
|
+
const script = join(process.cwd(), ".rafa", "bin", "rafa-compile.mjs");
|
|
10
|
+
if (!existsSync(script)) {
|
|
11
|
+
console.error(
|
|
12
|
+
"✗ .rafa/bin/rafa-compile.mjs not found — run `rafa init` (or `rafa update`) first.",
|
|
13
|
+
);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
execSync(`node ${JSON.stringify(script)} ${args.join(" ")}`.trim(), {
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
process.exit(1); // compile already printed its path·field·rule errors
|
|
23
|
+
}
|
|
24
|
+
}
|
package/lib/init.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// rafa init — provision a repo: vendor the blueprint, and (with a platform setup
|
|
2
|
+
// URL) wire the brain remote. Run inside your code repo's root.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
appendFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
|
|
13
|
+
|
|
14
|
+
const die = (m) => {
|
|
15
|
+
console.error(`✗ ${m}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
};
|
|
18
|
+
const run = (cmd, cwd) =>
|
|
19
|
+
execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }).trim();
|
|
20
|
+
|
|
21
|
+
export default async function init(args) {
|
|
22
|
+
const url = args.find((a) => !a.startsWith("-"));
|
|
23
|
+
const TARGET = process.cwd();
|
|
24
|
+
if (!existsSync(join(TARGET, ".git")))
|
|
25
|
+
die("Not a git repo — run this inside your code repo's root.");
|
|
26
|
+
|
|
27
|
+
// Optional platform setup (short-code URL → non-secret provisioning payload).
|
|
28
|
+
let p = null;
|
|
29
|
+
if (url) {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(url);
|
|
32
|
+
p = await res.json();
|
|
33
|
+
if (!res.ok || p?.error)
|
|
34
|
+
die(`Couldn't fetch setup: ${p?.error ?? res.status} (setup URLs expire in 15 min).`);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
die(`Couldn't reach the setup URL: ${e instanceof Error ? e.message : e}`);
|
|
37
|
+
}
|
|
38
|
+
if (!p?.repoId || !p?.brainRepoUrl) die("Setup is missing repo/brain details.");
|
|
39
|
+
console.log(`rafa init → ${p.repoName} (${p.repoId})`);
|
|
40
|
+
console.log(` brain remote: ${p.brainRepoUrl}`);
|
|
41
|
+
} else {
|
|
42
|
+
console.log("rafa init — vendoring the blueprint (no setup URL)");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Vendor the blueprint into .claude/ + .rafa/ (you own these copies).
|
|
46
|
+
const written = copyBlueprint(blueprintSource(), TARGET);
|
|
47
|
+
if (!existsSync(join(TARGET, ".rafa", "active.md")))
|
|
48
|
+
writeFileSync(join(TARGET, ".rafa", "active.md"), "# No active plan\n");
|
|
49
|
+
console.log(` ✓ vendored ${written.length} blueprint files → .claude/ + .rafa/`);
|
|
50
|
+
|
|
51
|
+
// Keep the brain out of the code repo's diffs.
|
|
52
|
+
const gi = join(TARGET, ".gitignore");
|
|
53
|
+
const body = existsSync(gi) ? readFileSync(gi, "utf8") : "";
|
|
54
|
+
if (!/^\.rafa\/?\s*$/m.test(body)) {
|
|
55
|
+
appendFileSync(gi, `${body.endsWith("\n") || body === "" ? "" : "\n"}.rafa/\n`);
|
|
56
|
+
console.log(" ✓ .rafa/ → .gitignore");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (p) {
|
|
60
|
+
writeFileSync(
|
|
61
|
+
join(TARGET, "rafa.json"),
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
{
|
|
64
|
+
name: p.repoName,
|
|
65
|
+
repoId: p.repoId,
|
|
66
|
+
codeRepo: p.codeRepoUrl ?? null,
|
|
67
|
+
brainRepo: p.brainRepoUrl,
|
|
68
|
+
provisionedFor: p.owner ?? null,
|
|
69
|
+
},
|
|
70
|
+
null,
|
|
71
|
+
2,
|
|
72
|
+
) + "\n",
|
|
73
|
+
);
|
|
74
|
+
const rafaDir = join(TARGET, ".rafa");
|
|
75
|
+
if (!existsSync(join(rafaDir, ".git"))) run("git init -b main", rafaDir);
|
|
76
|
+
try {
|
|
77
|
+
run("git remote get-url origin", rafaDir);
|
|
78
|
+
run(`git remote set-url origin ${p.brainRepoUrl}`, rafaDir);
|
|
79
|
+
} catch {
|
|
80
|
+
run(`git remote add origin ${p.brainRepoUrl}`, rafaDir);
|
|
81
|
+
}
|
|
82
|
+
console.log(" ✓ .rafa/ is its own git repo → origin = brain remote");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(
|
|
86
|
+
`\n✓ Provisioned.\n\nNext steps:\n` +
|
|
87
|
+
" 1. Restart Claude Code in this repo (reload the /rafa command + agents).\n" +
|
|
88
|
+
" 2. Run /rafa scan — build + validate the brain, then rafa push to sync it.\n" +
|
|
89
|
+
" 3. Open the repo on the rafinery platform — its Overview shows the brain once pushed.",
|
|
90
|
+
);
|
|
91
|
+
}
|
package/lib/push.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// rafa push — compile (contract gate) then push .rafa/ to the brain remote. Thin
|
|
2
|
+
// wrapper over the vendored .rafa/bin/rafa-push.mjs (which runs compile itself).
|
|
3
|
+
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
export default async function push(args) {
|
|
9
|
+
const script = join(process.cwd(), ".rafa", "bin", "rafa-push.mjs");
|
|
10
|
+
if (!existsSync(script)) {
|
|
11
|
+
console.error(
|
|
12
|
+
"✗ .rafa/bin/rafa-push.mjs not found — run `rafa init` (or `rafa update`) first.",
|
|
13
|
+
);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
execSync(`node ${JSON.stringify(script)} ${args.join(" ")}`.trim(), {
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
package/lib/update.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// rafa update — re-sync the blueprint into this repo. shadcn-style: you own your
|
|
2
|
+
// copies, so this is an opt-in refresh (run it to pull the latest agents,
|
|
3
|
+
// capabilities, contract, and gates after upgrading the CLI).
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { blueprintSource, copyBlueprint } from "./blueprint.mjs";
|
|
8
|
+
|
|
9
|
+
export default async function update() {
|
|
10
|
+
const TARGET = process.cwd();
|
|
11
|
+
if (!existsSync(join(TARGET, ".rafa")) && !existsSync(join(TARGET, ".claude"))) {
|
|
12
|
+
console.error("✗ This repo isn't provisioned yet — run `rafa init` first.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const written = copyBlueprint(blueprintSource(), TARGET);
|
|
16
|
+
console.log(`✓ rafa update — synced ${written.length} blueprint files:`);
|
|
17
|
+
for (const f of written) console.log(` ${f}`);
|
|
18
|
+
console.log(
|
|
19
|
+
"\n Your brain data (.rafa/brain, .rafa/improve) was untouched. " +
|
|
20
|
+
"Restart Claude Code to reload the agents.",
|
|
21
|
+
);
|
|
22
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rafinery/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "rafa — the AI engineer CLI. Vendors the rafa blueprint into your repo (shadcn-style) and runs the deterministic contract gates.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rafa": "bin/rafa.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"blueprint"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"bundle": "node scripts/bundle-blueprint.mjs",
|
|
22
|
+
"prepare": "node scripts/bundle-blueprint.mjs"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"rafa",
|
|
26
|
+
"rafinery",
|
|
27
|
+
"ai",
|
|
28
|
+
"codebase",
|
|
29
|
+
"brain",
|
|
30
|
+
"agent",
|
|
31
|
+
"cli"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/rafinery-ai/rafinery.git",
|
|
37
|
+
"directory": "packages/cli"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/rafinery-ai/rafinery/tree/main/packages/cli"
|
|
40
|
+
}
|