@rafinery/cli 0.4.1 → 0.5.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 +47 -0
- package/blueprint/.claude/agents/atlas.md +9 -5
- package/blueprint/.claude/agents/compass.md +3 -1
- package/blueprint/.claude/commands/rafa.md +33 -7
- package/blueprint/.claude/rafa/contract.md +79 -47
- package/blueprint/.claude/skills/rafa-build/SKILL.md +6 -1
- package/blueprint/.claude/skills/rafa-insights/SKILL.md +13 -2
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +11 -3
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +9 -0
- package/lib/checkpoint.mjs +7 -0
- package/lib/claude-config.mjs +36 -0
- package/lib/distill.mjs +43 -2
- package/lib/gate/compile.mjs +105 -32
- package/lib/init.mjs +27 -0
- package/lib/leverage.mjs +13 -2
- package/lib/migrations/index.mjs +57 -2
- package/lib/releases.mjs +31 -0
- package/lib/update.mjs +7 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ 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.5.0 — plans v2: the work-item tree
|
|
8
|
+
|
|
9
|
+
**Adopt with:** `npx @rafinery/cli@latest update` → it runs the mechanical `rafa migrate`
|
|
10
|
+
(plans data v1→v2: `parent`→`epic`, `child`→`task`, stored `blocked`→`todo` +
|
|
11
|
+
`blocked_reason`). Brain data unchanged; no re-scan. Re-push plans once so the platform
|
|
12
|
+
rows pick up the tree + glimpse fields.
|
|
13
|
+
|
|
14
|
+
- **The tree**: one epic → tasks → subtasks (three ranks — matching Linear/Jira/Asana's
|
|
15
|
+
ceiling so tracker sync stays lossless). Compile validates ranks, single root,
|
|
16
|
+
`blocked_by` resolution + acyclicity.
|
|
17
|
+
- **Vendor-blended vocabulary** (checked against the real APIs): `title` ·
|
|
18
|
+
`description` (1:1 with vendor description/notes) · `approach` (rafa's how-line) ·
|
|
19
|
+
`assignee` · `blocked_by` (a dependency IS a blocker) · `blocked_reason` ·
|
|
20
|
+
`priority` 0–4 (Linear scale) · `estimate` · `external {provider, key, url}`
|
|
21
|
+
(read-only tracker identity; provider-closed ≠ done — K6).
|
|
22
|
+
- **`blocked` is DERIVED, never stored** — the status enum is
|
|
23
|
+
todo|in-progress|done|superseded|abandoned; the platform renders a blocked chip from
|
|
24
|
+
unresolved `blocked_by`/`blocked_reason`.
|
|
25
|
+
- **Decisions are first-class**: `log_decision` (at · actor · context · options ·
|
|
26
|
+
decision · rationale — paraphrase + pivotal quotes) with the `## Decisions` body
|
|
27
|
+
section as the human mirror; rendered as a timeline on the plan detail page.
|
|
28
|
+
- Platform: glimpse cards (what/why/how/who/blocked/deps at a glance), tree detail
|
|
29
|
+
page, board blocked-chip (the Blocked column is gone — it's a condition, not a state).
|
|
30
|
+
|
|
31
|
+
## 0.4.2
|
|
32
|
+
|
|
33
|
+
**Adopt with:** `npx @rafinery/cli@latest update` — file re-sync only (skills + contract
|
|
34
|
+
moved a few lines); no migration, no re-scan.
|
|
35
|
+
|
|
36
|
+
Owner E2E feedback round (research-canvas):
|
|
37
|
+
|
|
38
|
+
- **`rafa init` offers CI reconciliation** at provisioning (TTY only): yes runs
|
|
39
|
+
`ci-setup`, no prints the session-fallback path. Nothing is silently installed —
|
|
40
|
+
the workflow runs on the org's own secrets.
|
|
41
|
+
- **`rafa distill --headless` is observable**: streams the agent's narration lines and
|
|
42
|
+
prints per-phase timings (brain mirror · agent turns/cost · gates · total), so a CI
|
|
43
|
+
log shows liveness and where the time goes. Failure semantics unchanged: any
|
|
44
|
+
incomplete verdict or failed gate aborts with the working set intact.
|
|
45
|
+
- **Plans link to their brain slice**: optional parent-only `domains:` (contract §7) —
|
|
46
|
+
the blast radius named at plan time rides `push_plan`; the platform's new plan
|
|
47
|
+
detail page renders the plan's related knowledge beside its tasks + `## Log`
|
|
48
|
+
journals. Compile validates the shape (child `domains:` fails loudly).
|
|
49
|
+
- **Live improvement signal** (platform + SOP): bloom's fixed-in-passing during build
|
|
50
|
+
now also calls `report_improvement_status` — event-only (the ledger row keeps its
|
|
51
|
+
single writer, the ingest); the Improvements page shows it as *pending
|
|
52
|
+
reconciliation* until the next brain push confirms.
|
|
53
|
+
|
|
7
54
|
## 0.4.1
|
|
8
55
|
|
|
9
56
|
**Adopt with:** `npx @rafinery/cli@latest <cmd>` — no file re-sync needed (the fix is inside
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: atlas
|
|
3
|
-
version: 3.
|
|
3
|
+
version: 3.6.0
|
|
4
4
|
model: opus # authoring is correctness-critical — a hallucinated note poisons the brain; best model, never cheap
|
|
5
5
|
groundTruth: code-at-sha
|
|
6
6
|
description: >-
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
when a codebase must be scanned, a brain repaired after validation, a plan
|
|
11
11
|
drafted from recalled knowledge, or a plan's task implemented. Runs
|
|
12
12
|
context-isolated; comprehensive, cited, never cherry-picked.
|
|
13
|
-
tools: Read, Write, Edit, Bash, Grep, Glob, TodoWrite, mcp__rafinery
|
|
13
|
+
tools: Read, Write, Edit, Bash, Grep, Glob, TodoWrite, Skill, mcp__rafinery
|
|
14
14
|
color: blue
|
|
15
15
|
duties:
|
|
16
16
|
- "scan :: .claude/skills/rafa-scan/SKILL.md :: comprehensive breadth-first cited brain · verify-citations exits 0 · coverage honest (thin/gap stated, never hidden)"
|
|
@@ -84,9 +84,13 @@ Never act cold; never over-load.
|
|
|
84
84
|
**never open `.env`/`.env.*` or any secret store, and never read or copy a value.** A key's
|
|
85
85
|
*name* is a contract; its *value* is a secret. If a note genuinely needs a value, stop and ask
|
|
86
86
|
the dev — don't harvest it. (Enforced in [the scan skill](../skills/rafa-scan/SKILL.md) step 4.)
|
|
87
|
-
- **
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
- **Toolbox-first execution (automatic — never an offer).** Before implementing any
|
|
88
|
+
task step, CHECK the repo's installed toolbox — committed `.claude/skills/`,
|
|
89
|
+
`.mcp.json` servers, commands — for a capability that already does it, and INVOKE
|
|
90
|
+
it (Skill tool / MCP) instead of hand-rolling. The conductor passes the matching
|
|
91
|
+
inventory slice in your spawn prompt; consult it first. Only what is actually
|
|
92
|
+
installed — never guess a capability into existence. Record the choice in the
|
|
93
|
+
item's `approach` ("how: via the <x> skill").
|
|
90
94
|
|
|
91
95
|
## Style
|
|
92
96
|
Dense, no filler, no praise. Short plan before acting. Bracketed status
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: compass
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.3.0
|
|
4
4
|
model: opus # a false note about a PERSON is worse than one about code — best model, never cheap
|
|
5
5
|
groundTruth: sessions-over-time
|
|
6
6
|
description: >-
|
|
@@ -16,6 +16,8 @@ duties:
|
|
|
16
16
|
- "bootstrap :: .claude/skills/rafa-insights/SKILL.md :: native /insights report distilled with judgment (never parsed mechanically) into dev-level candidates — each OFFERED, banked only on yes"
|
|
17
17
|
- "continuous-refinement :: .claude/skills/rafa-insights/SKILL.md :: session observations routed by the boundary (dev-level here, code-level to the branch working set) · update beats duplicate · every insight legible, correctable, deletable on request"
|
|
18
18
|
- "steering :: .claude/skills/rafa-insights/SKILL.md :: propose-dev-disposes at natural boundaries · at most one nudge · dismissal is final for the session · never rank, never compare, never nag"
|
|
19
|
+
- "leverage-coaching :: .claude/skills/rafa-insights/SKILL.md :: the one boundary nudge may be sourced from toolbox × patterns (repo inventory + the dev's tooling insights) — always HAS a candidate ready, never raises nudge frequency; only capabilities actually installed"
|
|
20
|
+
- "tooling-capture :: .claude/skills/rafa-insights/SKILL.md :: personal ~/.claude tooling (names + descriptions ONLY) banked as kind:tooling insights under STANDING consent · legible/deletable · never enters org stores"
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
# compass — the dev's coach
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: 1.
|
|
2
|
+
version: 1.5.0
|
|
3
3
|
description: rafa — the repo's engineering SOP. Use whenever the dev wants to plan a feature or change, build/execute against the active plan, improve code health, or ground work in the repo's brain — intent counts, the dev does NOT need to type /rafa. Also explicit: /rafa <init|scan|improve|plan|build|push|leverage|migrate|update|help> [--brain-only]. Admin verbs (init/scan/push/migrate/update) run ONLY when explicitly invoked.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -28,6 +28,20 @@ unless explicitly asked (serve knowledge, not gossip). Misroutes the dev correct
|
|
|
28
28
|
signal — prefer under-routing (answer plainly) over dragging a question through
|
|
29
29
|
choreography.
|
|
30
30
|
|
|
31
|
+
**Zero-command principle (owner rule, 2026-07-12):** every rafa command — CLI verbs,
|
|
32
|
+
MCP tools, checkpoints, pushes, decisions logging — is AGENT-INTERNAL machinery. The
|
|
33
|
+
dev speaks intent; YOU run the machinery. Never instruct the dev to run a command you
|
|
34
|
+
can run yourself. The only human acts: init (provision) · `npx rafa pull` (once per
|
|
35
|
+
clone) · ci-setup approval · npm publish. **Toolbox is a recall surface:** at
|
|
36
|
+
bootstrap, load the committed toolbox (`.claude/skills/`, `.mcp.json`, commands)
|
|
37
|
+
alongside brain status; pass the matching slice into every atlas/bloom spawn prompt —
|
|
38
|
+
agents USE the fit capability automatically instead of hand-rolling (only what's
|
|
39
|
+
installed, never guessed; personal `~/.claude/` stays live-recommend-only).
|
|
40
|
+
**Actor envelope:** stamp `actorMeta {model, agent, runner: "session"}` on every
|
|
41
|
+
state-plane write (checkpoints, plan pushes, decisions, reports) and export
|
|
42
|
+
`RAFA_ACTOR_MODEL` before CLI calls — the platform records WHO/WHAT executed;
|
|
43
|
+
"unreported" when unknown, never guessed.
|
|
44
|
+
|
|
31
45
|
**Scope discipline (owner rule):** deliver exactly what the message asked — fix what
|
|
32
46
|
they're expecting, nothing else. No unsolicited extras beyond bloom's single opt-in
|
|
33
47
|
nudge. Fix-shaped prompts: recall the failure domain's knowledge → fix the asked
|
|
@@ -49,6 +63,10 @@ Three interaction modes by the weight of the action:
|
|
|
49
63
|
- session start, local ahead of platform → *"brain is N commits behind — push?"*
|
|
50
64
|
- reconciliation pending → *"branch <x> merged with N working-set files — distill now?"*
|
|
51
65
|
- adjudication pending → *"1 file diverged at the <x>→<y> fold — pick a copy?"*
|
|
66
|
+
- user brain EMPTY (first session / founding scan just completed) → *"want me to
|
|
67
|
+
bootstrap your insights from your usage report? (compass distills it; every
|
|
68
|
+
candidate is offered — nothing banked without your yes)"* — accepted = run
|
|
69
|
+
`## insights`. Offered ONCE; a "no" is a standing no until the dev asks.
|
|
52
70
|
|
|
53
71
|
Offer etiquette (hard rules): boundaries only, never mid-flow · once per moment — a
|
|
54
72
|
"no" is remembered for the session and never re-argued · an unanswered offer never
|
|
@@ -105,6 +123,11 @@ improve + push — a cheap knowledge refresh).
|
|
|
105
123
|
explicit approval**, push the brain: `npx @rafinery/cli push` — commits `.rafa/` and
|
|
106
124
|
pushes to the brain remote using the dev's own git auth, stamped `brain-for: <code sha>`.
|
|
107
125
|
Never push without approval.
|
|
126
|
+
8. **The coach offer (founding scan only)** — if this was the repo's FIRST scan and the
|
|
127
|
+
dev's user brain is empty (`list_dev_insights` → none), offer ONCE: *"the code side
|
|
128
|
+
is mapped — want me to bootstrap YOUR insights from your usage report?"* Accepted =
|
|
129
|
+
run `## insights` (compass; every candidate offered, banked only on yes). This is
|
|
130
|
+
the offer rung of the consent ladder — never auto-run, never re-asked after a no.
|
|
108
131
|
|
|
109
132
|
atlas scans + fixes; prism judges; bloom improves; you orchestrate + push on approval. You own
|
|
110
133
|
`log.md`; never edit `checklist.md` or the brain yourself — spawn atlas for that.
|
|
@@ -146,12 +169,15 @@ atlas drafts, bloom pulls, prism validates the plan itself:
|
|
|
146
169
|
Per [the rafa-build skill](../skills/rafa-build/SKILL.md). Resume from `active.md`
|
|
147
170
|
(or the platform's `get_active_plan` when connected — "load plan X" materializes it) —
|
|
148
171
|
never re-derive context.
|
|
149
|
-
Per
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
Per item (work the tree in `blocked_by` order — an item with an unmet dependency is
|
|
173
|
+
DERIVED-blocked and waits): **atlas** recalls + implements → **prism** validates against
|
|
174
|
+
the item's `## Done-check` (**`status: done` only on prism PASS**; FAIL → atlas corrects)
|
|
175
|
+
→ **bloom** sweeps (new opportunities → ledger files; fixed-in-passing → `status: fixed`;
|
|
176
|
+
one opt-in nudge max) → update the item file's `status` + `## Log` + `## Decisions` →
|
|
177
|
+
at CHECKPOINTS (task done · cadence under session consent):
|
|
178
|
+
`push_plan`/`update_plan_status`/`log_decision` (plans channel — decisions record what
|
|
179
|
+
the DEV chose and why; paraphrase + pivotal quotes) + `rafa checkpoint` (working set)
|
|
180
|
+
so the platform reflects live progress. Terminal
|
|
155
181
|
statuses when honest: `superseded` (replaced by a newer plan) · `abandoned` (dropped).
|
|
156
182
|
If execution invalidates/creates brain knowledge, route per §capture (working set on a
|
|
157
183
|
branch; full `## scan` on main — never hand-edit main's brain around the gate). When all
|
|
@@ -288,63 +288,94 @@ domains: { design-system: mapped, components: mapped, api: thin, external-integr
|
|
|
288
288
|
|
|
289
289
|
---
|
|
290
290
|
|
|
291
|
-
## 7. Plan files — `plans/**/*.md` (+ the plans channel)
|
|
291
|
+
## 7. Plan files — `plans/**/*.md` (+ the plans channel) — **v2: the work-item tree**
|
|
292
292
|
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
Plans are a TREE of work items (plans data version 2, ratified 2026-07-12): one
|
|
294
|
+
**epic** (the root) → **tasks** → **subtasks**. Three ranks, deliberately matching
|
|
295
|
+
the ceiling of Linear (Project→Issue→sub-issue), Jira (Epic→Task→Sub-task), and
|
|
296
|
+
Asana (Project→Task→Subtask) so tracker sync is lossless. Vocabulary is
|
|
297
|
+
vendor-blended: field names below are the majority vendor names wherever one exists.
|
|
298
|
+
Each item owns exactly one file (merges never conflict; progress is derived).
|
|
295
299
|
|
|
296
300
|
```yaml
|
|
297
301
|
---
|
|
298
302
|
schemaVersion: 1
|
|
299
|
-
id:
|
|
300
|
-
plan:
|
|
301
|
-
parent:
|
|
302
|
-
kind:
|
|
303
|
-
title:
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
id: payments-c3-webhooks # required · == filename stem · GLOBALLY unique across plans/**
|
|
304
|
+
plan: payments # required · the ROOT (epic) id
|
|
305
|
+
parent: payments-c3 # required · the parent ITEM id (any rank above), or null (epic)
|
|
306
|
+
kind: epic | task | subtask # required · epic = root only · task's parent = the epic · subtask's parent = a task
|
|
307
|
+
title: Handle provider webhooks # required · WHAT (Linear title · Jira summary · Asana name)
|
|
308
|
+
description: >- # optional · WHY + context — syncs 1:1 with vendor description/notes
|
|
309
|
+
reconciliation must be automatic; manual matching doesn't scale
|
|
310
|
+
approach: verify HMAC → enqueue → idempotent apply # optional · HOW, one line — rafa value-add
|
|
311
|
+
status: todo | in-progress | done | superseded | abandoned # required (task/subtask); epic may omit
|
|
312
|
+
assignee: rohik # optional · WHO (free string pre-orgs; unanimous vendor word)
|
|
313
|
+
blocked_by: [payments-c2] # optional · item ids this waits on — a dependency IS a blocker
|
|
314
|
+
blocked_reason: "vendor sandbox access" # optional · external blocks (no dependency id to point at)
|
|
315
|
+
priority: 2 # optional · 0 none · 1 urgent · 2 high · 3 medium · 4 low (Linear scale)
|
|
316
|
+
estimate: 3 # optional · points (Linear estimate / Jira story points)
|
|
317
|
+
branch: feat/webhooks # optional · ↔ Linear branchName
|
|
306
318
|
baseSha: abc123 # optional · commit it was cut from
|
|
319
|
+
domains: [payments] # optional · EPIC only · the blast radius — the brain link
|
|
320
|
+
external: { provider: jira, key: PAY-142 } # optional · tracker identity — READ-ONLY to sessions
|
|
307
321
|
---
|
|
308
|
-
(prose body —
|
|
309
|
-
|
|
310
|
-
|
|
322
|
+
(prose body — a LEAF item (a task with no subtasks, or a subtask) MUST carry a
|
|
323
|
+
`## Done-check`; `## Log` is the execution journal; `## Decisions` mirrors the
|
|
324
|
+
structured decision records (below). Compile never parses bodies.)
|
|
311
325
|
```
|
|
312
326
|
|
|
313
327
|
Rules (all compile-enforced unless noted):
|
|
314
328
|
|
|
315
|
-
- **Global id uniqueness
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
-
|
|
326
|
-
|
|
327
|
-
`
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
329
|
+
- **Global id uniqueness** across `plans/**` (duplicate = error). Convention: prefix
|
|
330
|
+
descendants with the epic id.
|
|
331
|
+
- **Tree shape.** Exactly ONE `kind: epic` per plan; `epic` → `parent: null`, `plan == id`.
|
|
332
|
+
A task's `parent` is the epic; a subtask's `parent` is a task. Every item's `plan`
|
|
333
|
+
is the root epic id. Dangling parents and rank violations are loud errors.
|
|
334
|
+
- **`blocked` is DERIVED, never stored.** An item is blocked when it has an
|
|
335
|
+
unresolved `blocked_by` (a listed item not yet `done`) or a `blocked_reason`.
|
|
336
|
+
`status: blocked` is a compile ERROR (removed in v2 — `rafa migrate` converts).
|
|
337
|
+
No vendor stores blocked as a state either (Linear: relations · Jira: links +
|
|
338
|
+
Flagged · Asana: dependencies) — deriving it keeps sync lossless.
|
|
339
|
+
- **`blocked_by` ids resolve within the plan and are acyclic.**
|
|
340
|
+
- **Progress is never stored.** A `progress` key is a compile error. Progress =
|
|
341
|
+
`count(done LEAVES) / count(leaves)` per subtree — derived everywhere.
|
|
342
|
+
- **Statuses.** `done` exists only on prism PASS (the execution gate). Terminal:
|
|
343
|
+
`superseded` (replaced by a newer plan) · `abandoned` (deliberately dropped) —
|
|
344
|
+
mapping losslessly to Linear `canceled` / Jira resolution "Duplicate"/"Won't do".
|
|
345
|
+
Statuses are set in sessions; the platform board renders them read-only.
|
|
346
|
+
- **`external` is read-only to sessions** — the sync layer owns it. When a provider
|
|
347
|
+
closes a ticket, the item shows DUAL status (provider's + ours); `done` is never
|
|
348
|
+
imported (K6: done is earned, not synced).
|
|
349
|
+
- **`active.md` → `activePlanId`.** First line `# <plan-id>` must resolve to a
|
|
350
|
+
`kind: epic`; `# No active plan` / missing file → null.
|
|
351
|
+
|
|
352
|
+
**Decisions — first-class deliberation records (channel-borne, never frontmatter):**
|
|
353
|
+
the frontmatter grammar is flat and bodies are never parsed, so structured decisions
|
|
354
|
+
live on the PLATFORM, pushed via `log_decision` at checkpoint moments:
|
|
355
|
+
`{ item, at, actor, context, options[], decision, rationale }` — *actor* is who
|
|
356
|
+
decided (the dev for steering; the agent for proposals it made under standing
|
|
357
|
+
consent). Text fidelity: **paraphrase + short verbatim quotes only where the exact
|
|
358
|
+
wording carries the decision** (transcripts never land in shared stores). Sessions
|
|
359
|
+
mirror each record into the item body's `## Decisions` for human reading. On tracker
|
|
360
|
+
sync, decisions flow OUT as prefixed comments (Jira/Linear comments, Asana stories);
|
|
361
|
+
imported comments are never scraped into decisions.
|
|
362
|
+
|
|
363
|
+
**Transport — the plans channel (plans never ride the brain manifest):**
|
|
364
|
+
|
|
365
|
+
- **Authoring**: files above, compile-validated; `.rafa/plans/` is the working copy.
|
|
366
|
+
- **Push on approval**: approval IS the trigger — `push_plan` sends the whole tree
|
|
367
|
+
(`items[]`, bodies included); `set_active_plan` points the pointer. Build cadence
|
|
368
|
+
re-pushes statuses + `## Log` (`push_plan` / `update_plan_status`) and logs
|
|
369
|
+
decisions (`log_decision`).
|
|
370
|
+
- **Pull-based work**: `list_plans` = NAMES ONLY; **"load plan X"** = `get_plan`
|
|
371
|
+
(bodies + decisions) materialized back into `.rafa/plans/` by the session.
|
|
372
|
+
- **The brain link**: the epic's `domains:` rides the channel; the plan detail page
|
|
373
|
+
renders the related knowledge beside the tree.
|
|
374
|
+
- **Tracker sync (phased)**: `external` identity now → import (a ticket is
|
|
375
|
+
purpose/what, NOT an executable plan — it still flows through plan drafting +
|
|
376
|
+
prism before build) → two-way sync under a field-ownership matrix (provider owns
|
|
377
|
+
ticket-native fields; rafa owns execution fields; conflicts surface, never
|
|
378
|
+
auto-resolve).
|
|
348
379
|
|
|
349
380
|
---
|
|
350
381
|
|
|
@@ -435,7 +466,8 @@ state**, marked by `envelope.plane: "state"`:
|
|
|
435
466
|
| `get_working_set` | branch working set | hydration (a session starts with its branch's working set), the branch view, distillation collect; `needs-adjudication` rows carry the incoming copy in `pending` |
|
|
436
467
|
| `resolve_working_file` | branch working set | `distilled` / `refuted` (+ cited note) at merge-to-main distillation; `keep-current` for adjudication decisions; CAS on status — a racing distill fails loudly |
|
|
437
468
|
| `fold_working_set` | branch working set | branch→parent-branch MECHANICAL fold (no LLM, no prism — rigor only at main): absent → re-keyed · identical → merged · divergent → `needs-adjudication` on the parent row |
|
|
438
|
-
| `push_plan` / `list_plans` / `update_plan_status` / `set_active_plan` | **plans** — (repo) work objects | the
|
|
469
|
+
| `push_plan` / `list_plans` / `update_plan_status` / `set_active_plan` / `log_decision` | **plans** — (repo) work objects | the push-on-approval channel (§7 v2): approval pushes the whole work-item TREE (items[], bodies stored); `list_plans` = names only; statuses push back (5-value enum — blocked is derived); `log_decision` records the deliberation trail (paraphrase + pivotal quotes); the active pointer is explicit, never inferred |
|
|
470
|
+
| `report_improvement_status` | **events only** — no store | a LIVE session signal (bloom's fixed-in-passing during build). The improvement ledger row is untouched — it has ONE writer, the ingest (K1); the platform overlays the report as pending-reconciliation until the next brain push confirms it |
|
|
439
471
|
|
|
440
472
|
State tools work with or without an ingested brain (a working set can exist —
|
|
441
473
|
and a plan can be pushed — before the first scan) and are exempt from the
|
|
@@ -18,7 +18,7 @@ platform MCP (one read path — the same surface any third-party agent uses).
|
|
|
18
18
|
|---|---|---|
|
|
19
19
|
| **Executor** | atlas | RECALL the task's brain slice via MCP (`search_knowledge` + `get_rule`/`get_playbook`; honor non-exemplars) → implement, convention-adherent |
|
|
20
20
|
| **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 |
|
|
21
|
-
| **Improver** | bloom | **push**: new improvement opportunities spotted during execution → new ledger files. **close**: improvements fixed in passing → `status: fixed
|
|
21
|
+
| **Improver** | bloom | **push**: new improvement opportunities spotted during execution → new ledger files. **close**: improvements fixed in passing → `status: fixed` in the ledger file + `report_improvement_status(id, fixed)` so the platform shows it LIVE as pending-reconciliation (the ledger row itself changes only at the next brain push — K1). **nudge**: top-leverage open item in the task's blast radius — opt-in, never blocking |
|
|
22
22
|
|
|
23
23
|
## Procedure
|
|
24
24
|
|
|
@@ -40,6 +40,11 @@ platform MCP (one read path — the same surface any third-party agent uses).
|
|
|
40
40
|
parsed; the plan files at `.rafa/plans/<plan>/` ARE the local cache) → at the
|
|
41
41
|
task-done CHECKPOINT (under the session consent): `push_plan` /
|
|
42
42
|
`update_plan_status` (the plans channel — statuses + Log journals) +
|
|
43
|
+
**`log_decision` for each deliberation since the last checkpoint** — what
|
|
44
|
+
came up, what was considered, what the DEV chose, why (actor = the dev for
|
|
45
|
+
steering, the agent for its own proposals; PARAPHRASE + short verbatim
|
|
46
|
+
quotes only where the wording carries the decision — transcripts never land
|
|
47
|
+
in shared stores; mirror each into the item's `## Decisions` section) +
|
|
43
48
|
`rafa checkpoint` (the branch working set), so the platform and every MCP
|
|
44
49
|
consumer reflect live progress. Checkpoint moments: task done · plan
|
|
45
50
|
approved · explicit ask · cadence · git push/pull — never session-end. A
|
|
@@ -58,9 +58,20 @@ code-level → the branch working set (not yours — rafa.md §capture routes it
|
|
|
58
58
|
existing insight (`put_dev_insight` with its id) rather than accreting near-
|
|
59
59
|
copies.
|
|
60
60
|
|
|
61
|
+
### 2b. Personal tooling (ratified 2026-07-12 — the user-plane toolbox)
|
|
62
|
+
Under the dev's STANDING insights consent, inventory the tooling they carry
|
|
63
|
+
across repos — `~/.claude/skills` + user-level MCPs — and bank each as a
|
|
64
|
+
`kind: tooling` insight (NAME + DESCRIPTION only, never file contents). Update
|
|
65
|
+
beats duplicate; tombstones honored. This is what lets rafa orchestrate a
|
|
66
|
+
dev's personal migration skill in a repo that has never seen it. The REPO
|
|
67
|
+
toolbox is atlas's plane (scan's toolbox domain), never yours.
|
|
68
|
+
|
|
61
69
|
### 3. Steering (the payoff — propose, the dev disposes)
|
|
62
|
-
At natural boundaries only, at most one nudge
|
|
63
|
-
|
|
70
|
+
At natural boundaries only, at most one nudge — and ALWAYS have a candidate
|
|
71
|
+
ready (leverage-coaching: cross the repo's toolbox inventory with the dev's
|
|
72
|
+
patterns + tooling insights — "you've hand-run this migration three times; the
|
|
73
|
+
repo ships a skill for it"). Availability is always; frequency never rises.
|
|
74
|
+
Surface the insight that changes what the dev does next — "you've corrected X three times: want it banked as a
|
|
64
75
|
rule?" · "this decomposition worked before, reuse it?" · "you never use plan
|
|
65
76
|
mode on big changes; it would have caught this." Dismissal is final for the
|
|
66
77
|
session. Never rank, never compare devs, never nag.
|
|
@@ -37,9 +37,17 @@ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
|
|
|
37
37
|
1. **Staleness check** — compare the platform envelope's `brainForSha` against the
|
|
38
38
|
local brain stamp; if the platform is behind, surface "run `rafa push`" (never
|
|
39
39
|
proceed silently on knowledge you know is stale — never block either).
|
|
40
|
-
2. **Recall** (atlas, via MCP) → **decompose**
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
2. **Recall** (atlas, via MCP) → **decompose** into the WORK-ITEM TREE (contract
|
|
41
|
+
§7 v2): one epic → tasks → subtasks (three ranks, never deeper). Every item
|
|
42
|
+
carries the glimpse fields — `title` (what) · `description` (why) ·
|
|
43
|
+
`approach` (how, one line) · `assignee` when known · `blocked_by` for
|
|
44
|
+
intra-plan dependencies (a dependency IS a blocker; blocked is DERIVED,
|
|
45
|
+
never a status) · optional `priority` 0–4 / `estimate`. Every LEAF carries a
|
|
46
|
+
`## Done-check`. The blast radius goes on the EPIC's `domains:` — it rides
|
|
47
|
+
`push_plan` and the platform renders the plan's brain slice beside it.
|
|
48
|
+
ADR material (alternatives, risks, non-goals) lives in the epic body AND
|
|
49
|
+
the pivotal choices are logged as DECISIONS at approval (`log_decision`:
|
|
50
|
+
context · options · decision · rationale; actor = the dev for their calls).
|
|
43
51
|
3. **Ledger pull** (bloom) → optional leverage tasks in the blast radius.
|
|
44
52
|
4. **Leverage-match** — recommend existing skills/tools/MCP that fit the tasks;
|
|
45
53
|
never plan to hand-roll what a capability already does.
|
|
@@ -138,6 +138,15 @@ retrieval index. Bodies read like a senior engineer explaining that one concept
|
|
|
138
138
|
the internal node/relationship model the next steps reason over. (Not emitted as a
|
|
139
139
|
file; it feeds the notes.)
|
|
140
140
|
|
|
141
|
+
3b. **Toolbox inventory** [deterministic] — ratified 2026-07-12: the REPO toolbox is a
|
|
142
|
+
first-class brain domain (`toolbox`). Run `npx @rafinery/cli leverage --json` (the
|
|
143
|
+
deterministic extractor) and author cited notes for the committed toolbox — skills
|
|
144
|
+
(`.claude/skills/*/SKILL.md` name+description), commands, `.mcp.json` servers,
|
|
145
|
+
granted permissions — cites into the config files themselves (contract §2; they are
|
|
146
|
+
citable file:line). This makes the toolbox recallable through the SAME MCP surface
|
|
147
|
+
as all knowledge and refreshable like any note. Personal `~/.claude/` is NEVER
|
|
148
|
+
inventoried here (user-profile plane, compass's job, standing consent).
|
|
149
|
+
|
|
141
150
|
4. **Convention detection** [deterministic + light LLM] — for **each** domain, the local
|
|
142
151
|
idiom (design tokens, component export pattern + any server/client split such as RSC,
|
|
143
152
|
data-op shape, naming).
|
package/lib/checkpoint.mjs
CHANGED
|
@@ -84,6 +84,13 @@ export default async function checkpoint() {
|
|
|
84
84
|
try {
|
|
85
85
|
payload = await callTool(ROOT, "checkpoint_sync", {
|
|
86
86
|
branch,
|
|
87
|
+
// WHO/WHAT executed — the CLI knows itself; the MODEL is the session's to
|
|
88
|
+
// report (RAFA_ACTOR_MODEL env, set by the conductor) — never guessed.
|
|
89
|
+
actorMeta: {
|
|
90
|
+
agent: "rafa-cli@" + (process.env.npm_package_version ?? "0.5.0"),
|
|
91
|
+
runner: process.env.CI ? "ci" : "session",
|
|
92
|
+
model: process.env.RAFA_ACTOR_MODEL ?? "unreported",
|
|
93
|
+
},
|
|
87
94
|
files: candidates.map(({ path, content, baseVersion, baseBrainSha }) => ({
|
|
88
95
|
path,
|
|
89
96
|
content,
|
package/lib/claude-config.mjs
CHANGED
|
@@ -13,6 +13,42 @@ export const RAFA_PERMISSIONS = [
|
|
|
13
13
|
"Bash(rafa:*)",
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
+
// Merge the repo's .mcp.json server names into the OWNED agent cards' tools
|
|
17
|
+
// lines (ratified 2026-07-12: subagents must be able to reach the repo's MCPs
|
|
18
|
+
// — a Convex repo's mcp__convex tools were structurally invisible to atlas).
|
|
19
|
+
// Owned-file edit, idempotent, reviewable in git diff; blueprint stays generic.
|
|
20
|
+
export function mergeRepoMcpsIntoCards(targetDir) {
|
|
21
|
+
const mcp = readClaudeSettingsFile(join(targetDir, ".mcp.json"));
|
|
22
|
+
const servers = Object.keys(mcp?.mcpServers ?? {}).filter((n) => n !== "rafinery");
|
|
23
|
+
if (servers.length === 0) return { updated: [] };
|
|
24
|
+
const updated = [];
|
|
25
|
+
const dir = join(targetDir, ".claude", "agents");
|
|
26
|
+
if (!existsSync(dir)) return { updated };
|
|
27
|
+
for (const card of ["atlas.md", "bloom.md", "prism.md", "compass.md"]) {
|
|
28
|
+
const file = join(dir, card);
|
|
29
|
+
if (!existsSync(file)) continue;
|
|
30
|
+
let text = readFileSync(file, "utf8");
|
|
31
|
+
const m = text.match(/^tools: (.+)$/m);
|
|
32
|
+
if (!m) continue;
|
|
33
|
+
const have = m[1].split(",").map((t) => t.trim());
|
|
34
|
+
const missing = servers.map((s2) => `mcp__${s2}`).filter((t) => !have.includes(t));
|
|
35
|
+
if (missing.length === 0) continue;
|
|
36
|
+
text = text.replace(/^tools: .+$/m, `tools: ${[...have, ...missing].join(", ")}`);
|
|
37
|
+
writeFileSync(file, text);
|
|
38
|
+
updated.push(`${card} (+${missing.join(", ")})`);
|
|
39
|
+
}
|
|
40
|
+
return { updated };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readClaudeSettingsFile(file) {
|
|
44
|
+
if (!existsSync(file)) return null;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
16
52
|
export function settingsPath(targetDir) {
|
|
17
53
|
return join(targetDir, ".claude", "settings.json");
|
|
18
54
|
}
|
package/lib/distill.mjs
CHANGED
|
@@ -76,6 +76,10 @@ export default async function distill(args = []) {
|
|
|
76
76
|
);
|
|
77
77
|
|
|
78
78
|
const ROOT = process.cwd();
|
|
79
|
+
// Telemetry (owner feedback #9): every phase is timed and printed, so a CI
|
|
80
|
+
// log answers "where is it, and where did the time go" at a glance.
|
|
81
|
+
const t0 = Date.now();
|
|
82
|
+
const secs = (t) => ((Date.now() - t) / 1000).toFixed(1);
|
|
79
83
|
|
|
80
84
|
// 1 · collect the branch working set. Adjudication-flagged rows are LEFT for
|
|
81
85
|
// a human session (CI can flag divergence, never resolve it).
|
|
@@ -101,7 +105,9 @@ export default async function distill(args = []) {
|
|
|
101
105
|
|
|
102
106
|
// 2 · mirror the org brain locally — the authoring target + the base the
|
|
103
107
|
// survivors fold into. (CI checkout is merged main = the validation target.)
|
|
108
|
+
const tPull = Date.now();
|
|
104
109
|
await pull(["--full"]);
|
|
110
|
+
console.log(` ⏱ brain mirror: ${secs(tPull)}s`);
|
|
105
111
|
|
|
106
112
|
// 3 · stage the incoming working set for the agent (gitignored inside .rafa).
|
|
107
113
|
const stagingRoot = join(ROOT, ".rafa", "distill-incoming");
|
|
@@ -153,6 +159,7 @@ Staged files:
|
|
|
153
159
|
${fileList}`;
|
|
154
160
|
|
|
155
161
|
console.log("• running the distillation agent (org's own ANTHROPIC_API_KEY) …");
|
|
162
|
+
const tAgent = Date.now();
|
|
156
163
|
const run = sdk.query({
|
|
157
164
|
prompt,
|
|
158
165
|
options: {
|
|
@@ -163,14 +170,45 @@ ${fileList}`;
|
|
|
163
170
|
maxTurns: 150,
|
|
164
171
|
},
|
|
165
172
|
});
|
|
173
|
+
// Stream the agent's narration lines — a silent multi-minute CI step reads
|
|
174
|
+
// as hung; a narrated one reads as working (and shows WHERE it hangs).
|
|
175
|
+
let lastLine = "";
|
|
176
|
+
let agentModel = "unreported";
|
|
166
177
|
for await (const message of run) {
|
|
178
|
+
if (message.type === "assistant" && typeof message.message?.model === "string")
|
|
179
|
+
agentModel = message.message.model;
|
|
180
|
+
if (message.type === "assistant") {
|
|
181
|
+
const blocks = message.message?.content ?? [];
|
|
182
|
+
for (const b of blocks) {
|
|
183
|
+
if (b.type !== "text" || !b.text?.trim()) continue;
|
|
184
|
+
const line = b.text.trim().split("\n")[0].slice(0, 110);
|
|
185
|
+
if (line && line !== lastLine) {
|
|
186
|
+
console.log(` · ${line}`);
|
|
187
|
+
lastLine = line;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
167
191
|
if (message.type === "result") {
|
|
168
192
|
if (message.subtype !== "success")
|
|
169
193
|
die(`distillation agent did not complete (${message.subtype}) — nothing resolved, nothing pushed`);
|
|
170
194
|
console.log(
|
|
171
|
-
` agent
|
|
195
|
+
` ⏱ agent: ${secs(tAgent)}s (${message.num_turns ?? "?"} turns · ` +
|
|
172
196
|
`$${(message.total_cost_usd ?? 0).toFixed(4)} on the org's key)`,
|
|
173
197
|
);
|
|
198
|
+
// Self-report the metered run (backlog #6) — best-effort, never fatal.
|
|
199
|
+
try {
|
|
200
|
+
await callTool(ROOT, "log_usage", {
|
|
201
|
+
provider: "anthropic",
|
|
202
|
+
model: agentModel,
|
|
203
|
+
inputTokens: message.usage?.input_tokens ?? 0,
|
|
204
|
+
outputTokens: message.usage?.output_tokens ?? 0,
|
|
205
|
+
...(typeof message.total_cost_usd === "number" ? { costUsd: message.total_cost_usd } : {}),
|
|
206
|
+
purpose: "distill",
|
|
207
|
+
runner: "ci",
|
|
208
|
+
});
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.log(` ! usage not reported: ${e instanceof Error ? e.message : e}`);
|
|
211
|
+
}
|
|
174
212
|
}
|
|
175
213
|
}
|
|
176
214
|
|
|
@@ -195,10 +233,12 @@ ${fileList}`;
|
|
|
195
233
|
|
|
196
234
|
// 6 · trust-but-verify: OUR gates re-run regardless of what the agent claims.
|
|
197
235
|
console.log("• re-running the gates (trust-but-verify) …");
|
|
236
|
+
const tGates = Date.now();
|
|
198
237
|
if (runVerifyCitations([]) !== 0)
|
|
199
238
|
die("citation checker failed on the authored brain — nothing resolved, nothing pushed");
|
|
200
239
|
if (runCompile([]) !== 0)
|
|
201
240
|
die("compile gate failed on the authored brain — nothing resolved, nothing pushed");
|
|
241
|
+
console.log(` ⏱ gates: ${secs(tGates)}s`);
|
|
202
242
|
|
|
203
243
|
// 7 · ship: brain repo push (webhook → ingest), staging cleaned first.
|
|
204
244
|
rmSync(stagingRoot, { recursive: true, force: true });
|
|
@@ -221,6 +261,7 @@ ${fileList}`;
|
|
|
221
261
|
branch,
|
|
222
262
|
path: f.path,
|
|
223
263
|
resolution: v.verdict,
|
|
264
|
+
actorMeta: { agent: "rafa-distill", runner: "ci", model: agentModel },
|
|
224
265
|
...(v.note ? { note: v.note } : {}),
|
|
225
266
|
});
|
|
226
267
|
console.log(` ${v.verdict === "distilled" ? "✓" : "!"} ${f.path} → ${v.verdict}${v.note ? ` (${v.note})` : ""}`);
|
|
@@ -232,6 +273,6 @@ ${fileList}`;
|
|
|
232
273
|
|
|
233
274
|
console.log(
|
|
234
275
|
`✓ distillation: ${distilledCount} into the org brain · ${refuted} refuted (reported, cited) · ` +
|
|
235
|
-
`${adjudication} flagged needs-adjudication (next session's digest)`,
|
|
276
|
+
`${adjudication} flagged needs-adjudication (next session's digest) · total ${secs(t0)}s`,
|
|
236
277
|
);
|
|
237
278
|
}
|
package/lib/gate/compile.mjs
CHANGED
|
@@ -335,20 +335,13 @@ export function runCompile(argv = []) {
|
|
|
335
335
|
return { domains };
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
// Plans (contract §7
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
"todo",
|
|
346
|
-
"in-progress",
|
|
347
|
-
"done",
|
|
348
|
-
"blocked",
|
|
349
|
-
"superseded",
|
|
350
|
-
"abandoned",
|
|
351
|
-
];
|
|
338
|
+
// Plans (contract §7 v2 — the work-item tree, plans data version 2): one epic
|
|
339
|
+
// (root) → tasks → subtasks. Vendor-blended vocabulary; `blocked` is DERIVED
|
|
340
|
+
// (unresolved blocked_by / blocked_reason), never stored. VALIDATED here
|
|
341
|
+
// (files are the authoring surface — determinism holds) but plans do NOT ride
|
|
342
|
+
// the manifest: they travel through the push-on-approval plans channel.
|
|
343
|
+
const PLAN_STATUSES = ["todo", "in-progress", "done", "superseded", "abandoned"];
|
|
344
|
+
const PLAN_KINDS = ["epic", "task", "subtask"];
|
|
352
345
|
function compilePlans() {
|
|
353
346
|
const plans = [];
|
|
354
347
|
const seen = new Map(); // id → path (global uniqueness across plans/**)
|
|
@@ -365,43 +358,123 @@ export function runCompile(argv = []) {
|
|
|
365
358
|
fail(path, "id", `duplicate plan id "${id}" (also ${seen.get(id)}) · ids are globally unique across plans/**`);
|
|
366
359
|
seen.set(id, path);
|
|
367
360
|
if ("progress" in data)
|
|
368
|
-
fail(path, "progress", "never stored ·
|
|
369
|
-
|
|
361
|
+
fail(path, "progress", "never stored · progress = done leaves / total leaves, derived");
|
|
362
|
+
if (data.kind === "parent" || data.kind === "child")
|
|
363
|
+
fail(path, "kind", `plans v1 shape ("${data.kind}") — run \`rafa migrate\` (v2: epic|task|subtask)`);
|
|
364
|
+
const kind = reqEnum(data, "kind", PLAN_KINDS, path);
|
|
370
365
|
const plan = reqStr(data, "plan", path);
|
|
371
366
|
const status = data.status;
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (data.parent !== plan)
|
|
376
|
-
fail(path, "parent", `must equal plan "${plan}" for a child (flat v1)`);
|
|
377
|
-
} else {
|
|
367
|
+
if (status === "blocked")
|
|
368
|
+
fail(path, "status", "removed in v2 — blocked is DERIVED from blocked_by/blocked_reason (run `rafa migrate`)");
|
|
369
|
+
if (kind === "epic") {
|
|
378
370
|
if (data.parent !== null && data.parent !== "null")
|
|
379
|
-
fail(path, "parent", "must be null for kind:
|
|
380
|
-
if (plan !== id) fail(path, "plan", `must equal id "${id}" for kind:
|
|
371
|
+
fail(path, "parent", "must be null for kind: epic (the root)");
|
|
372
|
+
if (plan !== id) fail(path, "plan", `must equal id "${id}" for kind: epic`);
|
|
381
373
|
if (status !== undefined && !isEnum(status, PLAN_STATUSES))
|
|
382
|
-
fail(path, "status", `optional (
|
|
374
|
+
fail(path, "status", `optional (epic) · ${PLAN_STATUSES.join("|")}`);
|
|
375
|
+
} else {
|
|
376
|
+
if (!isEnum(status, PLAN_STATUSES))
|
|
377
|
+
fail(path, "status", `required (${kind}) · ${PLAN_STATUSES.join("|")}`);
|
|
378
|
+
if (typeof data.parent !== "string" || data.parent === "")
|
|
379
|
+
fail(path, "parent", `required (${kind}) · the parent item id`);
|
|
380
|
+
if (data.domains !== undefined)
|
|
381
|
+
fail(path, "domains", "epic only · the blast radius belongs to the plan, not an item");
|
|
382
|
+
}
|
|
383
|
+
if (data.domains !== undefined && !Array.isArray(data.domains))
|
|
384
|
+
fail(path, "domains", "optional · flow list of domain strings");
|
|
385
|
+
if (data.blocked_by !== undefined && !Array.isArray(data.blocked_by))
|
|
386
|
+
fail(path, "blocked_by", "optional · flow list of item ids this item waits on");
|
|
387
|
+
if (
|
|
388
|
+
data.priority !== undefined &&
|
|
389
|
+
!(Number.isInteger(data.priority) && data.priority >= 0 && data.priority <= 4)
|
|
390
|
+
)
|
|
391
|
+
fail(path, "priority", "optional · int 0–4 (0 none · 1 urgent · 2 high · 3 medium · 4 low)");
|
|
392
|
+
if (data.estimate !== undefined && !(Number.isInteger(data.estimate) && data.estimate >= 0))
|
|
393
|
+
fail(path, "estimate", "optional · non-negative int (points)");
|
|
394
|
+
if (data.external !== undefined) {
|
|
395
|
+
const ex = data.external;
|
|
396
|
+
if (typeof ex !== "object" || Array.isArray(ex) || typeof ex.provider !== "string" || typeof ex.key !== "string")
|
|
397
|
+
fail(path, "external", "optional · { provider, key, url? } flow map — tracker identity (read-only to sessions)");
|
|
383
398
|
}
|
|
384
399
|
plans.push({
|
|
385
400
|
id,
|
|
386
401
|
plan,
|
|
387
|
-
parent: kind === "
|
|
402
|
+
parent: kind === "epic" ? null : String(data.parent ?? ""),
|
|
388
403
|
kind,
|
|
389
404
|
title: reqStr(data, "title", path),
|
|
390
405
|
...(isEnum(status, PLAN_STATUSES) ? { status } : {}),
|
|
406
|
+
...(typeof data.description === "string" && data.description !== ""
|
|
407
|
+
? { description: data.description }
|
|
408
|
+
: {}),
|
|
409
|
+
...(typeof data.approach === "string" && data.approach !== ""
|
|
410
|
+
? { approach: data.approach }
|
|
411
|
+
: {}),
|
|
412
|
+
...(typeof data.assignee === "string" && data.assignee !== ""
|
|
413
|
+
? { assignee: data.assignee }
|
|
414
|
+
: {}),
|
|
415
|
+
...(Array.isArray(data.blocked_by) ? { blockedBy: data.blocked_by.map(String) } : {}),
|
|
416
|
+
...(typeof data.blocked_reason === "string" && data.blocked_reason !== ""
|
|
417
|
+
? { blockedReason: data.blocked_reason }
|
|
418
|
+
: {}),
|
|
419
|
+
...(Number.isInteger(data.priority) ? { priority: data.priority } : {}),
|
|
420
|
+
...(Number.isInteger(data.estimate) ? { estimate: data.estimate } : {}),
|
|
391
421
|
...(typeof data.branch === "string" && data.branch !== ""
|
|
392
422
|
? { branch: data.branch }
|
|
393
423
|
: {}),
|
|
394
424
|
...(typeof data.baseSha === "string" && data.baseSha !== ""
|
|
395
425
|
? { baseSha: String(data.baseSha) }
|
|
396
426
|
: {}),
|
|
427
|
+
...(Array.isArray(data.domains) && kind === "epic"
|
|
428
|
+
? { domains: data.domains.map(String) }
|
|
429
|
+
: {}),
|
|
430
|
+
...(typeof data.external === "object" && !Array.isArray(data.external) && data.external !== null
|
|
431
|
+
? { external: data.external }
|
|
432
|
+
: {}),
|
|
397
433
|
path: rel(path),
|
|
398
434
|
});
|
|
399
435
|
}
|
|
400
|
-
// Cross-
|
|
401
|
-
const
|
|
436
|
+
// Cross-checks: tree shape + blocked_by resolution, per plan.
|
|
437
|
+
const byId = new Map(plans.map((p) => [p.id, p]));
|
|
438
|
+
const roots = new Map(); // plan id → epic item
|
|
439
|
+
for (const p of plans) if (p.kind === "epic") roots.set(p.id, p);
|
|
440
|
+
for (const p of plans) {
|
|
441
|
+
if (!roots.has(p.plan))
|
|
442
|
+
fail(p.path, "plan", `no kind:epic "${p.plan}" in this compile (dangling item)`);
|
|
443
|
+
if (p.kind === "task") {
|
|
444
|
+
const parent = byId.get(p.parent);
|
|
445
|
+
if (!parent || parent.kind !== "epic" || parent.id !== p.plan)
|
|
446
|
+
fail(p.path, "parent", `a task's parent must be its epic ("${p.plan}")`);
|
|
447
|
+
}
|
|
448
|
+
if (p.kind === "subtask") {
|
|
449
|
+
const parent = byId.get(p.parent);
|
|
450
|
+
if (!parent || parent.kind !== "task")
|
|
451
|
+
fail(p.path, "parent", "a subtask's parent must be a task in the same plan");
|
|
452
|
+
else if (parent.plan !== p.plan)
|
|
453
|
+
fail(p.path, "parent", `parent task belongs to plan "${parent.plan}", not "${p.plan}"`);
|
|
454
|
+
}
|
|
455
|
+
for (const b of p.blockedBy ?? []) {
|
|
456
|
+
const target = byId.get(b);
|
|
457
|
+
if (!target || target.plan !== p.plan)
|
|
458
|
+
fail(p.path, "blocked_by", `"${b}" does not resolve to an item in plan "${p.plan}"`);
|
|
459
|
+
if (b === p.id) fail(p.path, "blocked_by", "an item cannot block itself");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// blocked_by acyclicity (per plan; DFS with colors).
|
|
463
|
+
const color = new Map();
|
|
464
|
+
const cyc = (p) => {
|
|
465
|
+
color.set(p.id, 1);
|
|
466
|
+
for (const b of p.blockedBy ?? []) {
|
|
467
|
+
const t = byId.get(b);
|
|
468
|
+
if (!t) continue;
|
|
469
|
+
if (color.get(t.id) === 1) return true;
|
|
470
|
+
if (!color.has(t.id) && cyc(t)) return true;
|
|
471
|
+
}
|
|
472
|
+
color.set(p.id, 2);
|
|
473
|
+
return false;
|
|
474
|
+
};
|
|
402
475
|
for (const p of plans)
|
|
403
|
-
if (p.
|
|
404
|
-
fail(p.path, "
|
|
476
|
+
if (!color.has(p.id) && cyc(p))
|
|
477
|
+
fail(p.path, "blocked_by", "dependency cycle — blocked_by must be acyclic");
|
|
405
478
|
return plans;
|
|
406
479
|
}
|
|
407
480
|
|
|
@@ -419,8 +492,8 @@ export function runCompile(argv = []) {
|
|
|
419
492
|
}
|
|
420
493
|
const val = m[1].trim();
|
|
421
494
|
if (/^no active plan$/i.test(val)) return null;
|
|
422
|
-
if (!plans.some((p) => p.kind === "
|
|
423
|
-
fail(path, "pointer", `active plan "${val}" is not a kind:
|
|
495
|
+
if (!plans.some((p) => p.kind === "epic" && p.id === val)) {
|
|
496
|
+
fail(path, "pointer", `active plan "${val}" is not a kind:epic plan in this compile`);
|
|
424
497
|
return null;
|
|
425
498
|
}
|
|
426
499
|
return val;
|
package/lib/init.mjs
CHANGED
|
@@ -105,6 +105,13 @@ export default async function init(args) {
|
|
|
105
105
|
);
|
|
106
106
|
// Grant rafa's tools their permissions (merge, never overwrite).
|
|
107
107
|
reportSettings(TARGET);
|
|
108
|
+
// Wire the repo's own MCP servers into the (owned) agent cards so subagents
|
|
109
|
+
// can actually reach them (zero-command orchestration, 2026-07-12).
|
|
110
|
+
{
|
|
111
|
+
const { mergeRepoMcpsIntoCards } = await import("./claude-config.mjs");
|
|
112
|
+
const r = mergeRepoMcpsIntoCards(TARGET);
|
|
113
|
+
for (const u of r.updated) console.log(` ✓ repo MCPs → ${u}`);
|
|
114
|
+
}
|
|
108
115
|
|
|
109
116
|
// Keep the brain out of the code repo's diffs; keep the per-dev MCP key out of git.
|
|
110
117
|
const ensureIgnored = (line, label) => {
|
|
@@ -151,6 +158,26 @@ export default async function init(args) {
|
|
|
151
158
|
);
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
// CI reconciliation — OPT-IN at provisioning (owner feedback #5): the workflow
|
|
162
|
+
// runs on the ORG's own secrets (their LLM key never touches our platform), so
|
|
163
|
+
// it is offered, never silently installed. Non-TTY (CI/agent shells) skips the
|
|
164
|
+
// question entirely — explicit `rafa ci-setup` remains the path there.
|
|
165
|
+
if (isInteractive()) {
|
|
166
|
+
const a = await ask(
|
|
167
|
+
" • Wire CI reconciliation now? (GitHub Actions: mechanical fold on branch merges,\n" +
|
|
168
|
+
" distillation on merge-to-main — runs on YOUR CI secrets) [y/N] ",
|
|
169
|
+
);
|
|
170
|
+
if (a === "y" || a === "yes") {
|
|
171
|
+
const { default: ciSetup } = await import("./ci-setup.mjs");
|
|
172
|
+
await ciSetup([]);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(
|
|
175
|
+
" • skipped — wire it anytime with `rafa ci-setup`. Until then, the next dev\n" +
|
|
176
|
+
" session offers the same reconciliation after each merge (nothing is lost).",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
154
181
|
// Stamp the versions this repo was provisioned at — the compat anchor `update`/`migrate`
|
|
155
182
|
// diff against. Carries platform identity too when provisioned via a setup URL.
|
|
156
183
|
stampCurrent(
|
package/lib/leverage.mjs
CHANGED
|
@@ -3,12 +3,23 @@
|
|
|
3
3
|
// only: it reads, ranks, and reports — it changes nothing. Run from your repo root.
|
|
4
4
|
|
|
5
5
|
import { advise } from "./leverage/engine.mjs";
|
|
6
|
-
import { PLANNED } from "./leverage/adapters/index.mjs";
|
|
6
|
+
import { adapters, PLANNED } from "./leverage/adapters/index.mjs";
|
|
7
7
|
|
|
8
|
-
export default async function leverage() {
|
|
8
|
+
export default async function leverage(args = []) {
|
|
9
9
|
const cwd = process.cwd();
|
|
10
10
|
const { detected, tips } = advise(cwd);
|
|
11
11
|
|
|
12
|
+
// --json: the DETERMINISTIC toolbox inventory (ratified 2026-07-12) — what
|
|
13
|
+
// the conductor loads at bootstrap and atlas consults during scan's toolbox
|
|
14
|
+
// pass. Machine truth: only what is actually installed, never inferred.
|
|
15
|
+
if (args.includes("--json")) {
|
|
16
|
+
const inventory = {};
|
|
17
|
+
for (const a of adapters)
|
|
18
|
+
if (a.detect(cwd)) inventory[a.id] = a.inspect(cwd);
|
|
19
|
+
console.log(JSON.stringify({ detected, inventory, tips }, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
if (detected.length === 0) {
|
|
13
24
|
console.log(
|
|
14
25
|
"No supported agent toolchain detected here.\n" +
|
package/lib/migrations/index.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// run: (cwd) => backfillPlanField(cwd, "owner", ownerOf(cwd)),
|
|
29
29
|
// };
|
|
30
30
|
|
|
31
|
-
import { existsSync, rmSync } from "node:fs";
|
|
31
|
+
import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
32
32
|
import { join } from "node:path";
|
|
33
33
|
import { cmpSemver } from "../releases.mjs";
|
|
34
34
|
|
|
@@ -66,7 +66,62 @@ const blueprint_split_0_4_0 = {
|
|
|
66
66
|
},
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
// plans v2 (contract §7 v2, ratified 2026-07-12): the work-item tree. Mechanical
|
|
70
|
+
// half: kind parent→epic, child→task (rank semantics unchanged for flat v1 plans),
|
|
71
|
+
// stored `status: blocked` → `status: todo` + a blocked_reason note (blocked is
|
|
72
|
+
// DERIVED in v2). Only frontmatter lines are touched; bodies never.
|
|
73
|
+
const plans_v1_to_v2 = {
|
|
74
|
+
id: "plans-v1-to-v2-work-item-tree",
|
|
75
|
+
kind: "plans",
|
|
76
|
+
mode: "mechanical",
|
|
77
|
+
summary:
|
|
78
|
+
"plans become a work-item tree (epic|task|subtask); blocked becomes derived " +
|
|
79
|
+
"(kind parent→epic, child→task; status blocked→todo + blocked_reason)",
|
|
80
|
+
appliesTo: (from, to) => from.plans < 2 && to.plans >= 2,
|
|
81
|
+
run: (cwd) => {
|
|
82
|
+
const changed = [];
|
|
83
|
+
const notes = [];
|
|
84
|
+
const dir = join(cwd, ".rafa", "plans");
|
|
85
|
+
const walk = (d) => {
|
|
86
|
+
if (!existsSync(d)) return [];
|
|
87
|
+
return readdirSync(d).flatMap((e) => {
|
|
88
|
+
const p = join(d, e);
|
|
89
|
+
return statSync(p).isDirectory() ? walk(p) : e.endsWith(".md") ? [p] : [];
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
for (const file of walk(dir)) {
|
|
93
|
+
const text = readFileSync(file, "utf8");
|
|
94
|
+
if (!text.startsWith("---")) continue;
|
|
95
|
+
const end = text.indexOf("\n---", 3);
|
|
96
|
+
if (end === -1) continue;
|
|
97
|
+
let fm = text.slice(0, end);
|
|
98
|
+
const body = text.slice(end);
|
|
99
|
+
const before = fm;
|
|
100
|
+
fm = fm.replace(/^kind:\s*parent\s*$/m, "kind: epic");
|
|
101
|
+
fm = fm.replace(/^kind:\s*child\s*$/m, "kind: task");
|
|
102
|
+
if (/^status:\s*blocked\s*$/m.test(fm)) {
|
|
103
|
+
fm = fm.replace(/^status:\s*blocked\s*$/m, "status: todo");
|
|
104
|
+
if (!/^blocked_reason:/m.test(fm))
|
|
105
|
+
fm += `\nblocked_reason: "(migrated) was 'blocked' in plans v1 — re-state the real blocker"`;
|
|
106
|
+
notes.push(`${file.slice(cwd.length + 1)}: blocked → todo + blocked_reason (re-state the real blocker)`);
|
|
107
|
+
}
|
|
108
|
+
if (fm !== before) {
|
|
109
|
+
writeFileSync(file, fm + body);
|
|
110
|
+
changed.push(file.slice(cwd.length + 1));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
changed,
|
|
115
|
+
notes: changed.length
|
|
116
|
+
? notes.length
|
|
117
|
+
? notes
|
|
118
|
+
: ["kinds re-ranked (parent→epic, child→task); review the diff"]
|
|
119
|
+
: ["no v1 plan files found — nothing to migrate"],
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const MIGRATIONS = [blueprint_split_0_4_0, plans_v1_to_v2];
|
|
70
125
|
|
|
71
126
|
// The migrations that apply to a given move (stamped `from` → shipped `to`), in order.
|
|
72
127
|
export function pendingMigrations(from, to) {
|
package/lib/releases.mjs
CHANGED
|
@@ -102,6 +102,37 @@ export const RELEASES = [
|
|
|
102
102
|
"the working-set scanner returned the directory path instead of each file's path " +
|
|
103
103
|
"(variable shadowing in scanWorkingFiles). No schema change; no re-scan.",
|
|
104
104
|
},
|
|
105
|
+
{
|
|
106
|
+
version: "0.4.2",
|
|
107
|
+
contract: 1,
|
|
108
|
+
plans: 1,
|
|
109
|
+
requires: "update",
|
|
110
|
+
summary:
|
|
111
|
+
"Owner E2E feedback round. `rafa init` now OFFERS CI reconciliation at provisioning " +
|
|
112
|
+
"(TTY only; skipping prints the session-fallback path — nothing is silently installed). " +
|
|
113
|
+
"`rafa distill --headless` streams the agent's narration and prints per-phase timings + " +
|
|
114
|
+
"cost (mirror · agent · gates · total), so CI logs show liveness and where time goes. " +
|
|
115
|
+
"Plans gain an optional parent-only `domains:` (contract §7) — the blast radius rides " +
|
|
116
|
+
"push_plan and the platform renders the plan's brain slice on its new detail page. " +
|
|
117
|
+
"No schema migration; no re-scan.",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
version: "0.5.0",
|
|
121
|
+
contract: 1,
|
|
122
|
+
plans: 2,
|
|
123
|
+
requires: "migrate",
|
|
124
|
+
summary:
|
|
125
|
+
"Plans v2 — the work-item tree (contract §7 v2, first plans data-version bump). " +
|
|
126
|
+
"epic → task → subtask with vendor-blended vocabulary (checked against Linear/Jira/Asana " +
|
|
127
|
+
"APIs): title · description · approach · assignee · blocked_by (a dependency IS a " +
|
|
128
|
+
"blocker) · blocked_reason · priority 0–4 · estimate · external {provider,key,url} " +
|
|
129
|
+
"(read-only tracker identity). `blocked` is DERIVED, never stored — the status enum is " +
|
|
130
|
+
"todo|in-progress|done|superseded|abandoned. Decisions become first-class channel " +
|
|
131
|
+
"records (log_decision: at·actor·context·options·decision·rationale; paraphrase + " +
|
|
132
|
+
"pivotal quotes), mirrored as `## Decisions` body prose. `rafa migrate` converts v1 " +
|
|
133
|
+
"files mechanically (parent→epic, child→task, blocked→todo+blocked_reason). Brain data " +
|
|
134
|
+
"schema unchanged; no re-scan.",
|
|
135
|
+
},
|
|
105
136
|
];
|
|
106
137
|
|
|
107
138
|
// The release this CLI build ships (last entry).
|
package/lib/update.mjs
CHANGED
|
@@ -31,6 +31,13 @@ export default async function update(args = []) {
|
|
|
31
31
|
await copyBlueprint(blueprintSource(), TARGET, { onConflict: makeConflictResolver(args) }),
|
|
32
32
|
);
|
|
33
33
|
reportSettings(TARGET);
|
|
34
|
+
// Wire the repo's own MCP servers into the (owned) agent cards so subagents
|
|
35
|
+
// can actually reach them (zero-command orchestration, 2026-07-12).
|
|
36
|
+
{
|
|
37
|
+
const { mergeRepoMcpsIntoCards } = await import("./claude-config.mjs");
|
|
38
|
+
const r = mergeRepoMcpsIntoCards(TARGET);
|
|
39
|
+
for (const u of r.updated) console.log(` ✓ repo MCPs → ${u}`);
|
|
40
|
+
}
|
|
34
41
|
|
|
35
42
|
// ── Blueprint-side migrations: mechanical/deterministic, run here in the CLI. ──
|
|
36
43
|
// (Anything needing intelligence is left for the brain-side pass — /rafa update.)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rafinery/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "rafa
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "rafa — the AI engineer CLI. Vendors the rafa blueprint into your repo (shadcn-style) and runs the deterministic contract gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"rafa": "bin/rafa.mjs"
|