@rafinery/cli 0.8.16 → 0.10.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 +81 -0
- package/bin/rafa.mjs +11 -0
- package/blueprint/.claude/agents/atlas.md +21 -3
- package/blueprint/.claude/agents/bloom.md +2 -2
- package/blueprint/.claude/agents/prism.md +42 -3
- package/blueprint/.claude/agents/sage.md +1 -1
- package/blueprint/.claude/commands/rafa.md +24 -13
- package/blueprint/.claude/rafa/contract.md +6 -0
- package/blueprint/.claude/rafa/hooks/brain-commit.mjs +4 -0
- package/blueprint/.claude/rafa/hooks/session-start.mjs +66 -0
- package/blueprint/.claude/skills/rafa-build/SKILL.md +111 -20
- package/blueprint/.claude/skills/rafa-improve/SKILL.md +22 -0
- package/blueprint/.claude/skills/rafa-leverage/SKILL.md +6 -1
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +36 -1
- package/blueprint/.claude/skills/rafa-review/SKILL.md +16 -2
- package/blueprint/.claude/skills/rafa-sage/SKILL.md +8 -2
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +4 -1
- package/lib/brain-repo.mjs +1 -1
- package/lib/checkpoint.mjs +12 -10
- package/lib/distiller/doctrine.mjs +5 -16
- package/lib/doctor.mjs +19 -1
- package/lib/facts.mjs +130 -0
- package/lib/gate/compile.mjs +12 -0
- package/lib/gate/verify-citations.mjs +78 -1
- package/lib/hydrate.mjs +45 -0
- package/lib/init.mjs +9 -1
- package/lib/leverage/engine.mjs +32 -3
- package/lib/leverage.mjs +11 -2
- package/lib/loop-cache.mjs +67 -0
- package/lib/mcp-client.mjs +12 -0
- package/lib/push.mjs +9 -1
- package/lib/reflex.mjs +5 -1
- package/lib/releases.mjs +51 -0
- package/lib/review.mjs +32 -6
- package/lib/session-facts.mjs +108 -0
- package/lib/skill-deps.mjs +377 -0
- package/lib/update.mjs +9 -0
- package/package.json +3 -2
- package/skills-bundle/frontend-design/LICENSE.txt +177 -0
- package/skills-bundle/frontend-design/SKILL.md +55 -0
- package/{LICENSE → skills-bundle/grill-me/LICENSE} +6 -6
- package/skills-bundle/grill-me/SKILL.md +7 -0
- package/skills-bundle/grill-me/agents/openai.yaml +5 -0
- package/skills-bundle/grilling/SKILL.md +53 -0
- package/skills-bundle/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/skills-bundle/improve-codebase-architecture/LICENSE +21 -0
- package/skills-bundle/improve-codebase-architecture/SKILL.md +71 -0
- package/skills-bundle/improve-codebase-architecture/agents/openai.yaml +5 -0
- package/skills-bundle/requesting-code-review/LICENSE +21 -0
- package/skills-bundle/requesting-code-review/SKILL.md +95 -0
- package/skills-bundle/requesting-code-review/code-reviewer.md +172 -0
- package/skills-bundle/skills-manifest.json +72 -0
- package/skills-bundle/tdd/LICENSE +21 -0
- package/skills-bundle/tdd/SKILL.md +36 -0
- package/skills-bundle/tdd/agents/openai.yaml +3 -0
- package/skills-bundle/tdd/mocking.md +59 -0
- package/skills-bundle/tdd/tests.md +77 -0
- package/skills-bundle/vercel-composition-patterns/AGENTS.md +946 -0
- package/skills-bundle/vercel-composition-patterns/README.md +60 -0
- package/skills-bundle/vercel-composition-patterns/SKILL.md +89 -0
- package/skills-bundle/vercel-composition-patterns/metadata.json +11 -0
- package/skills-bundle/vercel-composition-patterns/rules/_sections.md +29 -0
- package/skills-bundle/vercel-composition-patterns/rules/_template.md +24 -0
- package/skills-bundle/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/skills-bundle/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
- package/skills-bundle/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/skills-bundle/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/skills-bundle/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/skills-bundle/vercel-composition-patterns/rules/state-context-interface.md +191 -0
- package/skills-bundle/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/skills-bundle/vercel-composition-patterns/rules/state-lift-state.md +125 -0
|
@@ -91,6 +91,24 @@ convention — see [RSC client boundary](/brain/rules/rsc-client-boundary.md)).
|
|
|
91
91
|
performance · architecture · product · ops**. Weight the **silent** issues (directive 2):
|
|
92
92
|
convention violations the brain documents, framework boundary breaches (e.g. RSC), missing
|
|
93
93
|
authz, drifting contracts. The brain's conventions are your oracle for "what's a violation here."
|
|
94
|
+
|
|
95
|
+
**The architecture lens has a METHOD (wave 6)** — criteria adapted from the installed
|
|
96
|
+
`improve-codebase-architecture` skill (READ as criteria, never invoked — it is
|
|
97
|
+
`disable-model-invocation`; the method below stands whether or not the skill is installed):
|
|
98
|
+
- **Scope by heat, not by sweep** (YAGNI): unless the dev named a direction, walk
|
|
99
|
+
`git log --oneline` for the hot spots — churn is where depth pays; cold code waits.
|
|
100
|
+
- **Deep vs shallow judgment**: a module earns its interface when the interface is smaller
|
|
101
|
+
than what it hides. Bouncing between many small modules to follow one behavior, pure
|
|
102
|
+
functions extracted only for testability, and leaky seams are the friction signals.
|
|
103
|
+
- **The deletion test**: would deleting this module CONCENTRATE complexity somewhere honest,
|
|
104
|
+
or just smear it around? "Concentrates" marks a candidate.
|
|
105
|
+
- **Candidates are normal §3 improvement files** (zero schema change): before/after as body
|
|
106
|
+
prose + a `Confidence: Strong | Worth exploring | Speculative` body line; inside these
|
|
107
|
+
notes use the depth vocabulary — module · interface · implementation · seam · adapter ·
|
|
108
|
+
depth · leverage · locality — and keep it INSIDE the architecture lens (the rest of the
|
|
109
|
+
ledger speaks the repo's own language).
|
|
110
|
+
- **The big-bang guard is sovereign** (directive 1): a `Strong` candidate lands as its first
|
|
111
|
+
10-minute slice or a P2 backlog item — never a grand refactor plan.
|
|
94
112
|
3. **Delegate security.** Run `pnpm audit` (dep CVEs). Recommend/run the **`security-review`**
|
|
95
113
|
skill and semgrep/CodeQL if available. Record *tool* results as improvements; the LLM pass is
|
|
96
114
|
observational only — never present it as an authoritative audit.
|
|
@@ -166,3 +184,7 @@ the ledger the nudge will read.
|
|
|
166
184
|
beyond the deterministic cite-check.
|
|
167
185
|
- Debt-score weighting calibrated from real runs.
|
|
168
186
|
- Per-lens diverse validators (a security lens checked by a security-minded validator).
|
|
187
|
+
- A visual before/after artifact for architecture candidates (the
|
|
188
|
+
improve-codebase-architecture skill's HTML report) — deliberately SKIPPED for now: its
|
|
189
|
+
CDN-loaded Tailwind/Mermaid + temp-file-and-open mechanics violate the deterministic,
|
|
190
|
+
no-network posture; the ledger IS the report.
|
|
@@ -44,7 +44,12 @@ at the fix. This is platform DNA: **leverage what's present; never reinvent it.*
|
|
|
44
44
|
|
|
45
45
|
The engine ([`lib/leverage/engine.mjs`](../../../packages/cli/lib/leverage/engine.mjs)) gathers
|
|
46
46
|
light, tool-agnostic repo signals (`repoContext`: stack deps, `.env` files + whether they're
|
|
47
|
-
ignored, declared MCP servers
|
|
47
|
+
ignored, declared MCP servers, **and the harness-neutral `.agents/skills/` inventory +
|
|
48
|
+
skill-dependency status (wave 6)** — `.agents/` is ONE convention Claude Code · Codex ·
|
|
49
|
+
Cursor all read, so it lives in the engine, never inside a per-toolchain adapter), then runs
|
|
50
|
+
every **detected** adapter and ranks the tips. The engine emits its own rafa-level tip —
|
|
51
|
+
`skill-deps-missing` (`P2`, fix = `rafa update --skills`) — because rafa's declared skill
|
|
52
|
+
dependencies are harness-neutral by definition.
|
|
48
53
|
|
|
49
54
|
Each adapter implements one contract ([`lib/leverage/adapters/index.mjs`](../../../packages/cli/lib/leverage/adapters/index.mjs)):
|
|
50
55
|
|
|
@@ -41,6 +41,34 @@ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
|
|
|
41
41
|
A plan whose children lack Done-checks is REJECTED here — compile never parses
|
|
42
42
|
bodies (invariant #3); this gate is prism's.
|
|
43
43
|
|
|
44
|
+
**The grill pass (wave 6 — tier-bounded adversarial interview).** On top of
|
|
45
|
+
the two invariants, prism GRILLS the draft — the same procedure the installed
|
|
46
|
+
`grilling` skill carries (`.agents/skills/grilling/SKILL.md`; prism READS it
|
|
47
|
+
as criteria when installed, and this SOP carries the method regardless —
|
|
48
|
+
never invoked mid-validation, per its card). Per task, up the probe ladder:
|
|
49
|
+
**grounding** (which claims rest on assumption? name it, demand its
|
|
50
|
+
evidence — a session-fact id counts, vibes don't) · **unstated dependencies**
|
|
51
|
+
(what must exist for this task that no `blocked_by` declares?) · **vacuous
|
|
52
|
+
Done-check** (can it pass while the feature is broken? a check that
|
|
53
|
+
recomputes its expectation the way the code will, or that a no-op satisfies,
|
|
54
|
+
is TAUTOLOGICAL — criteria per the tdd skill's anti-patterns when installed)
|
|
55
|
+
· **YAGNI** (does the INTENT need this task, or did thoroughness invent it?).
|
|
56
|
+
Budget scales with the task's `validation_tier`: **light** = the two
|
|
57
|
+
invariants only (exactly today) · **standard** = ≤2 probes per task, ≤10 per
|
|
58
|
+
plan · **full** = the complete grilling procedure (one probe at a time,
|
|
59
|
+
survives-three-consecutive ends it). Every probe finding cites the plan line
|
|
60
|
+
it hits — an uncited probe is dropped, and findings feed the existing
|
|
61
|
+
REJECT-with-cited-reasons loop. Plan-lite drafts grill at light.
|
|
62
|
+
|
|
63
|
+
**The seam ritual (wave 6 — TDD's consent moment, no new prompt).** For code
|
|
64
|
+
tasks in TDD-eligible repos (the tdd skill installed + a test harness
|
|
65
|
+
detected — the build SOP's gate), every LEAF's `## Done-check` NAMES the seam
|
|
66
|
+
under test: *"`<runner>` on `<test-file>` at seam `<the public interface>`
|
|
67
|
+
exits 0"*. Seams come from recall (the brain's contracts/flows) — never
|
|
68
|
+
invented at build time. The plan-approval summary lists the seams as their
|
|
69
|
+
own line, so **the dev approving the plan IS the tdd skill's "confirm seams
|
|
70
|
+
with the user" step** — consent rides the existing approval gate.
|
|
71
|
+
|
|
44
72
|
## Procedure
|
|
45
73
|
|
|
46
74
|
1. **Staleness check** — compare the platform envelope's `brainForSha` against the
|
|
@@ -58,7 +86,14 @@ Planning is a choreography, not one agent (spec: knowledge-mcp-build-agent):
|
|
|
58
86
|
carries the glimpse fields — `title` (what) · `description` (why) ·
|
|
59
87
|
`approach` (how, one line) · `assignee` when known · `blocked_by` for
|
|
60
88
|
intra-plan dependencies (a dependency IS a blocker; blocked is DERIVED,
|
|
61
|
-
never a status) · optional `priority` 0–4 / `estimate
|
|
89
|
+
never a status) · optional `priority` 0–4 / `estimate` ·
|
|
90
|
+
**`validation_tier` (wave 5 — rigor scales with blast radius, not with
|
|
91
|
+
process): assign per LEAF from what the task touches — contract/state-shape/
|
|
92
|
+
graph surface → `full` · pure types/docs/leaf-UI → `light` · everything else
|
|
93
|
+
→ `standard` (the default when omitted). The tier bounds prism's
|
|
94
|
+
RE-DERIVATION DEPTH only; the Done-check gate itself never relaxes, and the
|
|
95
|
+
tier rides the task's loop events so sage can audit the tiering against
|
|
96
|
+
escaped-bug reality instead of anyone guessing.** Every LEAF carries a
|
|
62
97
|
`## Done-check`. The blast radius goes on the EPIC's `domains:` — it rides
|
|
63
98
|
`push_plan` and the platform renders the plan's brain slice beside it.
|
|
64
99
|
ADR material (alternatives, risks, non-goals) lives in the epic body AND
|
|
@@ -104,8 +104,16 @@ its OWN question:
|
|
|
104
104
|
7. **The active task's Done-check** (from `get_active_plan`): does the diff
|
|
105
105
|
satisfy what the plan said done means?
|
|
106
106
|
8. Verdict: **PASS** or **ITERATE** with per-finding citations (note id +
|
|
107
|
-
file:line).
|
|
108
|
-
|
|
107
|
+
file:line). **Severity rides every finding (wave 6 — the
|
|
108
|
+
requesting-code-review taxonomy): `Critical (Must Fix)` · `Important
|
|
109
|
+
(Should Fix)` · `Minor (Nice to Have)`. ITERATE iff ≥1 Critical or
|
|
110
|
+
Important; Minor NEVER flips the verdict** (advisory noise must not
|
|
111
|
+
masquerade as a gate — and "not everything is Critical" is the taxonomy's
|
|
112
|
+
own first rule). Findings feed atlas (validate-and-correct — atlas fixes,
|
|
113
|
+
the judge re-checks; the reviewer never edits). The fresh-context reviewer
|
|
114
|
+
discipline the installed requesting-code-review skill prescribes
|
|
115
|
+
(precisely-crafted context, never the session's history) IS rafa's spawn
|
|
116
|
+
model already — confirmation, not new machinery.
|
|
109
117
|
9. Emit **`report_loop_event(category: "review-verdict", outcome:
|
|
110
118
|
PASS|ITERATE, subject: <task id or branch>)`** at the ruling — sage's
|
|
111
119
|
evidence, monotonic, never a session-end sweep.
|
|
@@ -124,3 +132,9 @@ its OWN question:
|
|
|
124
132
|
- Trusting the diff author's summary — judge code against rules.
|
|
125
133
|
- Blocking on the gate — it is advisory; the merge reconciler is the hard gate.
|
|
126
134
|
- Findings without citations (rule id + file:line) — uncited = unactionable.
|
|
135
|
+
- **Mutating anything while judging** (wave 6, from the code-reviewer's
|
|
136
|
+
read-only rule): the review never touches the working tree, the index, HEAD,
|
|
137
|
+
or branch state — inspect another revision via `git show`/a throwaway
|
|
138
|
+
worktree, never a checkout on this one.
|
|
139
|
+
- **Severity inflation** — a verdict where everything is Critical has stopped
|
|
140
|
+
discriminating; that is itself a review failure.
|
|
@@ -58,8 +58,14 @@ boundary; its product is a ledger reviewed *later*, never a live nudge. No mid-f
|
|
|
58
58
|
### 2. evidence-cited — SHAPES only, via `get_loop_events`
|
|
59
59
|
Every learning cites its evidence, and evidence is **loop-event shapes and categories only**:
|
|
60
60
|
verdict TYPES, gap CLASSES, miss TAXONOMIES, and the aggregate counts/rates over them — read via
|
|
61
|
-
`get_loop_events` (category ∈ `prism-verdict | gate-result | reflex-outcome | distill-refutation
|
|
62
|
-
outcome enum per category; `subject` is a shape reference, never content).
|
|
61
|
+
`get_loop_events` (category ∈ `prism-verdict | gate-result | reflex-outcome | distill-refutation |
|
|
62
|
+
review-verdict`; outcome enum per category; `subject` is a shape reference, never content). Wave 5
|
|
63
|
+
enriched every event with THREE new analysis dimensions — split every rate by them: **verification
|
|
64
|
+
method** (`static` vs `live` — a PASS-rate that holds only on static rulings is a coverage gap, not
|
|
65
|
+
health), **actor envelope** (model · agent@version · runner — attribute a miss class to the agent
|
|
66
|
+
structure that produced it), and **tier** (`light|standard|full` — audit the tiering itself: a
|
|
67
|
+
light-tier task class that later produced escaped bugs is evidence the planner's tier rule needs a
|
|
68
|
+
diff; a full-tier class whose findings-density is ~zero is evidence of ceremony). A learning with no
|
|
63
69
|
event-shape evidence is a hunch — drop it. sage never cites a customer artifact (no `file:line`
|
|
64
70
|
into customer code; that field does not exist in the schema below).
|
|
65
71
|
|
|
@@ -155,7 +155,10 @@ retrieval index. Bodies read like a senior engineer explaining that one concept
|
|
|
155
155
|
3b. **Toolbox inventory** [deterministic] — ratified 2026-07-12: the REPO toolbox is a
|
|
156
156
|
first-class brain domain (`toolbox`). Run `npx @rafinery/cli leverage --json` (the
|
|
157
157
|
deterministic extractor) and author cited notes for the committed toolbox — skills
|
|
158
|
-
(`.claude/skills/*/SKILL.md` name+description),
|
|
158
|
+
(`.claude/skills/*/SKILL.md` name+description), **the harness-neutral
|
|
159
|
+
`.agents/skills/*/SKILL.md` (wave 6 — the `agentSkills` block in the JSON: one
|
|
160
|
+
convention Claude Code · Codex · Cursor all read; consent-installed by
|
|
161
|
+
`rafa update`)**, commands, `.mcp.json` servers,
|
|
159
162
|
granted permissions — cites into the config files themselves (contract §2; they are
|
|
160
163
|
citable file:line). This makes the toolbox recallable through the SAME MCP surface
|
|
161
164
|
as all knowledge and refreshable like any note. Personal `~/.claude/` is NEVER
|
package/lib/brain-repo.mjs
CHANGED
|
@@ -77,7 +77,7 @@ export function ensureBrainRepo(cwd, { requireRemote = true } = {}) {
|
|
|
77
77
|
// (not knowledge). MERGED line-by-line so clones ignored under an older CLI
|
|
78
78
|
// still gain new exclusions.
|
|
79
79
|
const ignore = join(rafaDir, ".gitignore");
|
|
80
|
-
const IGNORES = ["hydration.json", "*.theirs.md", "distill-incoming/", "distill-verdicts.json", "dirty.jsonl", "reflex.jsonl", "sensor-errors.jsonl", "benchmark.demo.json"];
|
|
80
|
+
const IGNORES = ["hydration.json", "*.theirs.md", "distill-incoming/", "distill-verdicts.json", "dirty.jsonl", "reflex.jsonl", "sensor-errors.jsonl", "benchmark.demo.json", "session-facts.json", "loop-events-tail.json"];
|
|
81
81
|
const cur = existsSync(ignore) ? readFileSync(ignore, "utf8") : "";
|
|
82
82
|
const have = new Set(cur.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
83
83
|
const missing = IGNORES.filter((l) => !have.has(l));
|
package/lib/checkpoint.mjs
CHANGED
|
@@ -248,22 +248,24 @@ export default async function checkpoint() {
|
|
|
248
248
|
// durable mirror of the dev's current copies; .theirs.md files are ignored.)
|
|
249
249
|
mirrorTail(ROOT, branch);
|
|
250
250
|
|
|
251
|
-
// prism-verdict backstop (harness-arc wave 0)
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
251
|
+
// prism-verdict backstop (harness-arc wave 0) + the sage-due cache (wave
|
|
252
|
+
// 5.5): the SAME single best-effort call, enriched. It now fetches recent
|
|
253
|
+
// events (no category filter) and caches their SHAPES locally
|
|
254
|
+
// (.rafa/loop-events-tail.json — category/at only, subjects excluded) so the
|
|
255
|
+
// SessionStart digest computes "sage due" with ZERO added platform calls.
|
|
256
|
+
// The old prism-verdict reminder derives from the same response.
|
|
255
257
|
try {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
});
|
|
260
|
-
if ((
|
|
258
|
+
const { writeLoopCache } = await import("./loop-cache.mjs");
|
|
259
|
+
const evts = await callTool(ROOT, "get_loop_events", { limit: 200 });
|
|
260
|
+
const events = evts.events ?? [];
|
|
261
|
+
writeLoopCache(ROOT, { fetchedAt: new Date().toISOString(), events });
|
|
262
|
+
if (events.filter((e) => e.category === "prism-verdict").length === 0)
|
|
261
263
|
console.log(
|
|
262
264
|
" ℹ no prism-verdict loop events recorded yet — the build SOP emits " +
|
|
263
265
|
"report_loop_event(prism-verdict, PASS|ITERATE, <task id>) at each Done-check ruling.",
|
|
264
266
|
);
|
|
265
267
|
} catch {
|
|
266
|
-
/* unprovisioned/unreachable — the reminder
|
|
268
|
+
/* unprovisioned/unreachable — the reminder + cache are best-effort */
|
|
267
269
|
}
|
|
268
270
|
if (conflicts) process.exit(2);
|
|
269
271
|
}
|
|
@@ -164,22 +164,11 @@ export function citesGround(content, cwd = process.cwd()) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// Rewrite a survivor's frontmatter `cites:` DSL so a re-grounded citation points
|
|
167
|
-
// at where the token NOW lives on main
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
export
|
|
172
|
-
let out = content;
|
|
173
|
-
for (const r of regroundings ?? []) {
|
|
174
|
-
const fromLoc = r.cite.loc; // file:line[-end]
|
|
175
|
-
const toLoc = r.to.loc; // file:line
|
|
176
|
-
// Match the DSL line ` - <fromLoc> :: <token>` and swap the locator only.
|
|
177
|
-
const esc = fromLoc.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
178
|
-
const re = new RegExp(`(-\\s+)${esc}(\\s*::\\s*)`, "g");
|
|
179
|
-
out = out.replace(re, `$1${toLoc}$2`);
|
|
180
|
-
}
|
|
181
|
-
return out;
|
|
182
|
-
}
|
|
167
|
+
// at where the token NOW lives on main. MOVED to the gate (wave 5.3) so
|
|
168
|
+
// `verify-citations --fix` can use it without an import cycle (this module
|
|
169
|
+
// imports the gate's primitives, never the reverse); re-exported here so the
|
|
170
|
+
// doctrine's callers keep their import path.
|
|
171
|
+
export { rewriteCites } from "../gate/verify-citations.mjs";
|
|
183
172
|
|
|
184
173
|
// ── folding N branch working sets into ONE run (case 1 · case 5) ────────────────
|
|
185
174
|
// Input: [{ branch, capturedAtSha, mergedAt, files: [{ path, content }] }].
|
package/lib/doctor.mjs
CHANGED
|
@@ -79,7 +79,25 @@ export async function runDoctor(ROOT = process.cwd()) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// 3 ·
|
|
82
|
+
// 3 · capabilities — every STAMPED-installed skill dependency re-verified
|
|
83
|
+
// against disk (wave 6): dir present, frontmatter parses, hashes match the
|
|
84
|
+
// bundle, license file where required. Declined/never-offered = silent.
|
|
85
|
+
{
|
|
86
|
+
const { verifyInstalled, neededSkills } = await import("./skill-deps.mjs");
|
|
87
|
+
const results = verifyInstalled(ROOT);
|
|
88
|
+
const pending = neededSkills(ROOT).length;
|
|
89
|
+
if (results.length || pending) {
|
|
90
|
+
console.log("• capabilities (skill dependencies)");
|
|
91
|
+
for (const r of results) {
|
|
92
|
+
if (r.ok) ok(`${r.name} installed + verified`);
|
|
93
|
+
else bad(`${r.name} ${r.reason}`, "re-install: `rafa update --skills`");
|
|
94
|
+
}
|
|
95
|
+
if (pending)
|
|
96
|
+
console.log(` · ${pending} declared dep(s) not installed — offer: \`rafa update --skills\``);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 4 · the round-trip — the heartbeat must LAND, not just send. A recorded
|
|
83
101
|
// response proves key + url + repo scoping + the platform handler, end to end.
|
|
84
102
|
console.log("• platform round-trip");
|
|
85
103
|
if (provisioned) {
|
package/lib/facts.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// rafa facts — the session-facts surface (wave 5.4, memoized verification).
|
|
2
|
+
//
|
|
3
|
+
// rafa facts list facts + staleness (the conductor's read)
|
|
4
|
+
// rafa facts --json machine read (spawn-prompt slices)
|
|
5
|
+
// rafa facts add --claim="…" --command="…" --exit=0 [--depends=a,b] [--tool=…]
|
|
6
|
+
// bank ONE verified fact (stamps HEAD sha)
|
|
7
|
+
// rafa facts invalidate <SF-n> drop a fact deliberately
|
|
8
|
+
// rafa facts prune --stale drop every mechanically-stale fact
|
|
9
|
+
//
|
|
10
|
+
// A fact is never an assumption: `add` REQUIRES the command + exit code that
|
|
11
|
+
// proved it, and staleness is mechanical (dependsOn ∩ git diff since the
|
|
12
|
+
// verifying sha, working tree included). Subagents read first and cite
|
|
13
|
+
// "SF-<n> unchanged" instead of re-deriving — prism's bounded exception.
|
|
14
|
+
|
|
15
|
+
import { execSync } from "node:child_process";
|
|
16
|
+
import {
|
|
17
|
+
factsPath,
|
|
18
|
+
nextFactId,
|
|
19
|
+
readFacts,
|
|
20
|
+
staleFacts,
|
|
21
|
+
validateFact,
|
|
22
|
+
writeFacts,
|
|
23
|
+
} from "./session-facts.mjs";
|
|
24
|
+
|
|
25
|
+
const die = (m) => {
|
|
26
|
+
console.error(`✗ ${m}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default async function facts(args = []) {
|
|
31
|
+
const ROOT = process.cwd();
|
|
32
|
+
const arg = (k) => {
|
|
33
|
+
const m = args.find((a) => a.startsWith(`--${k}=`));
|
|
34
|
+
return m ? m.slice(k.length + 3) : undefined;
|
|
35
|
+
};
|
|
36
|
+
const [sub, subArg] = args.filter((a) => !a.startsWith("-"));
|
|
37
|
+
|
|
38
|
+
if (sub === "add") {
|
|
39
|
+
const claim = arg("claim");
|
|
40
|
+
const command = arg("command");
|
|
41
|
+
const exit = arg("exit");
|
|
42
|
+
if (!claim || !command || exit === undefined)
|
|
43
|
+
die('usage: rafa facts add --claim="…" --command="…" --exit=0 [--depends=a,b]');
|
|
44
|
+
let sha = "";
|
|
45
|
+
try {
|
|
46
|
+
sha = execSync("git rev-parse HEAD", {
|
|
47
|
+
cwd: ROOT,
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
50
|
+
}).trim();
|
|
51
|
+
} catch {
|
|
52
|
+
die("not a git repo — session facts anchor to a verifying sha");
|
|
53
|
+
}
|
|
54
|
+
const doc = readFacts(ROOT);
|
|
55
|
+
const fact = {
|
|
56
|
+
id: nextFactId(doc),
|
|
57
|
+
claim,
|
|
58
|
+
evidence: { command, exitCode: Number(exit) },
|
|
59
|
+
verifiedAtSha: sha,
|
|
60
|
+
dependsOn: (arg("depends") ?? "").split(",").map((s) => s.trim()).filter(Boolean),
|
|
61
|
+
at: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
const bad = validateFact(fact);
|
|
64
|
+
if (bad) die(bad);
|
|
65
|
+
doc.facts.push(fact);
|
|
66
|
+
writeFacts(ROOT, doc);
|
|
67
|
+
console.log(
|
|
68
|
+
`✓ ${fact.id} banked — "${claim}"\n` +
|
|
69
|
+
` evidence: \`${command}\` → exit ${fact.evidence.exitCode} @ ${sha.slice(0, 7)}` +
|
|
70
|
+
(fact.dependsOn.length
|
|
71
|
+
? `\n citable while untouched: ${fact.dependsOn.join(", ")}` +
|
|
72
|
+
(fact.dependsOn.every((p) => !p.endsWith("/"))
|
|
73
|
+
? "\n note: invalidation is NOT transitive — if this fact spans a subsystem, declare the" +
|
|
74
|
+
"\n enclosing directory (e.g. --depends=src/) so indirect changes stale it. Breadth is the conservative move."
|
|
75
|
+
: "")
|
|
76
|
+
: "\n no dependsOn — environment-shaped; invalidate by hand when the env changes"),
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (sub === "invalidate") {
|
|
82
|
+
if (!subArg) die("usage: rafa facts invalidate <SF-n>");
|
|
83
|
+
const doc = readFacts(ROOT);
|
|
84
|
+
const before = doc.facts.length;
|
|
85
|
+
doc.facts = doc.facts.filter((f) => f.id !== subArg);
|
|
86
|
+
if (doc.facts.length === before) die(`no fact ${subArg} (see \`rafa facts\`)`);
|
|
87
|
+
writeFacts(ROOT, doc);
|
|
88
|
+
console.log(`✓ ${subArg} invalidated`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (sub === "prune") {
|
|
93
|
+
if (!args.includes("--stale")) die("usage: rafa facts prune --stale");
|
|
94
|
+
const doc = readFacts(ROOT);
|
|
95
|
+
const stale = new Set(staleFacts(ROOT, doc).map((s) => s.id));
|
|
96
|
+
const before = doc.facts.length;
|
|
97
|
+
doc.facts = doc.facts.filter((f) => !stale.has(f.id));
|
|
98
|
+
writeFacts(ROOT, doc);
|
|
99
|
+
console.log(`✓ pruned ${before - doc.facts.length} stale fact(s) · ${doc.facts.length} remain`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Default: list.
|
|
104
|
+
const doc = readFacts(ROOT);
|
|
105
|
+
if (args.includes("--json")) {
|
|
106
|
+
console.log(JSON.stringify({ ...doc, stale: staleFacts(ROOT, doc) }, null, 2));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (doc.facts.length === 0) {
|
|
110
|
+
console.log(
|
|
111
|
+
"no session facts banked — `rafa facts add` after verifying something expensive\n" +
|
|
112
|
+
` (${factsPath(ROOT).replace(ROOT + "/", "")} · read-first for every spawned agent)`,
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const stale = new Map(staleFacts(ROOT, doc).map((s) => [s.id, s.reason]));
|
|
117
|
+
for (const f of doc.facts) {
|
|
118
|
+
const s = stale.get(f.id);
|
|
119
|
+
console.log(
|
|
120
|
+
`${s ? "!" : "✓"} ${f.id} — ${f.claim}\n` +
|
|
121
|
+
` \`${f.evidence.command}\` → exit ${f.evidence.exitCode} @ ${f.verifiedAtSha.slice(0, 7)}` +
|
|
122
|
+
(s ? `\n STALE: ${s}` : ""),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const staleCount = stale.size;
|
|
126
|
+
console.log(
|
|
127
|
+
`${doc.facts.length} fact(s)` +
|
|
128
|
+
(staleCount ? ` · ${staleCount} STALE — re-verify or \`rafa facts prune --stale\`` : " · all fresh"),
|
|
129
|
+
);
|
|
130
|
+
}
|
package/lib/gate/compile.mjs
CHANGED
|
@@ -579,6 +579,15 @@ export function runCompile(argv = []) {
|
|
|
579
579
|
!(Number.isInteger(data.estimate) && data.estimate >= 0)
|
|
580
580
|
)
|
|
581
581
|
fail(path, "estimate", "optional · non-negative int (points)");
|
|
582
|
+
if (
|
|
583
|
+
data.validation_tier !== undefined &&
|
|
584
|
+
!["light", "standard", "full"].includes(data.validation_tier)
|
|
585
|
+
)
|
|
586
|
+
fail(
|
|
587
|
+
path,
|
|
588
|
+
"validation_tier",
|
|
589
|
+
"optional · light|standard|full — bounds prism re-derivation depth ONLY (the Done-check gate never relaxes)",
|
|
590
|
+
);
|
|
582
591
|
if (data.external !== undefined) {
|
|
583
592
|
const ex = data.external;
|
|
584
593
|
if (
|
|
@@ -618,6 +627,9 @@ export function runCompile(argv = []) {
|
|
|
618
627
|
: {}),
|
|
619
628
|
...(Number.isInteger(data.priority) ? { priority: data.priority } : {}),
|
|
620
629
|
...(Number.isInteger(data.estimate) ? { estimate: data.estimate } : {}),
|
|
630
|
+
...(["light", "standard", "full"].includes(data.validation_tier)
|
|
631
|
+
? { validationTier: data.validation_tier }
|
|
632
|
+
: {}),
|
|
621
633
|
...(typeof data.branch === "string" && data.branch !== ""
|
|
622
634
|
? { branch: data.branch }
|
|
623
635
|
: {}),
|
|
@@ -119,6 +119,31 @@ function gitLsGlob(glob, cwd = process.cwd()) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// EXPORTED — moved here from distiller/doctrine.mjs (wave 5.3; doctrine
|
|
123
|
+
// re-exports it) so the gate's `--fix` can use it without an import cycle
|
|
124
|
+
// (doctrine imports citeResolves/gitGrep FROM this file). Rewrites frontmatter
|
|
125
|
+
// `cites:` DSL locators so a re-grounded citation points at where the token
|
|
126
|
+
// NOW lives. Token-anchored when the cite carries one — two cites sharing
|
|
127
|
+
// file:line with different tokens must not clobber each other; falls back to
|
|
128
|
+
// the locator-only match for callers that pass no token.
|
|
129
|
+
export function rewriteCites(content, regroundings) {
|
|
130
|
+
let out = content;
|
|
131
|
+
for (const r of regroundings ?? []) {
|
|
132
|
+
const fromLoc = r.cite.loc; // file:line[-end]
|
|
133
|
+
const toLoc = r.to.loc; // file:line
|
|
134
|
+
const esc = fromLoc.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
135
|
+
if (r.cite.token !== undefined) {
|
|
136
|
+
const escTok = String(r.cite.token).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
137
|
+
const re = new RegExp(`(-\\s+)${esc}(\\s*::\\s*${escTok}\\s*)$`, "gm");
|
|
138
|
+
out = out.replace(re, `$1${toLoc}$2`);
|
|
139
|
+
} else {
|
|
140
|
+
const re = new RegExp(`(-\\s+)${esc}(\\s*::\\s*)`, "g");
|
|
141
|
+
out = out.replace(re, `$1${toLoc}$2`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
122
147
|
// EXPORTED (refinery-arc-p1-distiller): the doctrine validates a candidate's
|
|
123
148
|
// citations against merged main through the exact same predicate the gate uses,
|
|
124
149
|
// so "resolves for the checker" and "resolves for arbitration" can never drift.
|
|
@@ -160,6 +185,14 @@ export function runVerifyCitations(argv = []) {
|
|
|
160
185
|
const arg = (k, d) => { const m = argv.find((a) => a.startsWith(`--${k}=`)); return m ? m.slice(k.length + 3) : d; };
|
|
161
186
|
const ROOT = arg("root", ".rafa/brain"); // --root=.rafa/improve for the improve ledger
|
|
162
187
|
const NOTE_DIRS = arg("dirs", "rules,playbooks").split(",").filter(Boolean); // --dirs=improvements
|
|
188
|
+
// --fix (wave 5.3): heal DRIFTED line-cites in place — a failing cite whose
|
|
189
|
+
// token still lives at exactly ONE grep site gets its locator rewritten, then
|
|
190
|
+
// the whole verification re-runs clean. gone (0 hits) and ambiguous (>1)
|
|
191
|
+
// stay untouched and keep failing — the checker never guesses.
|
|
192
|
+
const FIX = argv.includes("--fix");
|
|
193
|
+
const fixDrifted = []; // { notePath, cite: {loc, token}, to: {loc} }
|
|
194
|
+
const fixGone = []; // { note, loc, token }
|
|
195
|
+
const fixAmbiguous = []; // { note, loc, token, hits }
|
|
163
196
|
|
|
164
197
|
// --selftest: prove each gate's logic on throwaway fixtures (no brain/repo pollution).
|
|
165
198
|
// prism runs this as its mutation probe — re-running the checker is not proof it still
|
|
@@ -264,7 +297,22 @@ export function runVerifyCitations(argv = []) {
|
|
|
264
297
|
// RESOLUTION
|
|
265
298
|
for (const ct of cites) {
|
|
266
299
|
const { ok, reason } = citeResolves(ct.file, ct.start, ct.end, ct.token);
|
|
267
|
-
|
|
300
|
+
const loc = `${ct.file}:${ct.start}${ct.end !== ct.start ? "-" + ct.end : ""}`;
|
|
301
|
+
resolution.push({ note: rel, loc, token: ct.token, ok, reason });
|
|
302
|
+
if (!ok && FIX) {
|
|
303
|
+
// Classify with the gate's OWN primitives — deliberately NOT
|
|
304
|
+
// arbitrateCite, which takes hits[0] blindly; the gate never guesses.
|
|
305
|
+
const hits = gitGrep(ct.token);
|
|
306
|
+
if (hits.length === 1)
|
|
307
|
+
fixDrifted.push({
|
|
308
|
+
notePath: note,
|
|
309
|
+
cite: { loc, token: ct.token },
|
|
310
|
+
to: { loc: `${hits[0].file}:${hits[0].line}` },
|
|
311
|
+
ranged: ct.end !== ct.start,
|
|
312
|
+
});
|
|
313
|
+
else if (hits.length === 0) fixGone.push({ note: rel, loc, token: ct.token });
|
|
314
|
+
else fixAmbiguous.push({ note: rel, loc, token: ct.token, hits: hits.length });
|
|
315
|
+
}
|
|
268
316
|
}
|
|
269
317
|
|
|
270
318
|
// COMPLETENESS
|
|
@@ -355,6 +403,35 @@ export function runVerifyCitations(argv = []) {
|
|
|
355
403
|
}
|
|
356
404
|
}
|
|
357
405
|
|
|
406
|
+
// ── --fix apply + re-run (wave 5.3) ────────────────────────────────────────
|
|
407
|
+
if (FIX && (fixDrifted.length || fixGone.length || fixAmbiguous.length)) {
|
|
408
|
+
const byNote = new Map();
|
|
409
|
+
for (const f of fixDrifted) {
|
|
410
|
+
if (!byNote.has(f.notePath)) byNote.set(f.notePath, []);
|
|
411
|
+
byNote.get(f.notePath).push(f);
|
|
412
|
+
}
|
|
413
|
+
for (const [notePath, fixes] of byNote) {
|
|
414
|
+
const before = readFileSync(notePath, "utf8");
|
|
415
|
+
const after = rewriteCites(before, fixes);
|
|
416
|
+
if (after !== before) writeFileSync(notePath, after);
|
|
417
|
+
}
|
|
418
|
+
for (const f of fixDrifted)
|
|
419
|
+
console.log(
|
|
420
|
+
` ✓ fixed drifted ${f.cite.loc} → ${f.to.loc} :: ${f.cite.token}` +
|
|
421
|
+
(f.ranged ? " (range collapsed to the found line)" : ""),
|
|
422
|
+
);
|
|
423
|
+
for (const g of fixGone)
|
|
424
|
+
console.log(` ✗ gone ${g.loc} :: ${g.token} — token nowhere in the tree, not fixable [${g.note}]`);
|
|
425
|
+
for (const a2 of fixAmbiguous)
|
|
426
|
+
console.log(` · ambiguous ${a2.loc} :: ${a2.token} — ${a2.hits} grep sites, never guessed [${a2.note}]`);
|
|
427
|
+
console.log(
|
|
428
|
+
`--fix: ${fixDrifted.length} drifted healed · ${fixGone.length} gone · ${fixAmbiguous.length} ambiguous (untouched)`,
|
|
429
|
+
);
|
|
430
|
+
// Re-run WITHOUT --fix so the report + exit code reflect the healed state
|
|
431
|
+
// (gone/ambiguous cites still fail — honestly).
|
|
432
|
+
return runVerifyCitations(argv.filter((a2) => a2 !== "--fix"));
|
|
433
|
+
}
|
|
434
|
+
|
|
358
435
|
const rFail = resolution.filter((r) => !r.ok);
|
|
359
436
|
const cFail = completeness.filter((r) => !r.ok);
|
|
360
437
|
const pFail = policy.filter((r) => !r.ok);
|
package/lib/hydrate.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { execSync } from "node:child_process";
|
|
|
22
22
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { dirname, join } from "node:path";
|
|
24
24
|
import { callTool } from "./mcp-client.mjs";
|
|
25
|
+
import { citeResolves, gitGrep } from "./gate/verify-citations.mjs";
|
|
25
26
|
import {
|
|
26
27
|
hashOf,
|
|
27
28
|
materializeImprovement,
|
|
@@ -35,6 +36,42 @@ const die = (m) => {
|
|
|
35
36
|
process.exit(1);
|
|
36
37
|
};
|
|
37
38
|
|
|
39
|
+
// Wave 5.3 — hydrate-time citation re-basing. Served cites were authored
|
|
40
|
+
// against the brain's base sha; the LOCAL checkout may be newer. Before
|
|
41
|
+
// materializing, re-ground each {file, line, token} with the gate's own
|
|
42
|
+
// primitives: resolves → keep; exactly ONE grep hit → re-cite there;
|
|
43
|
+
// gone/ambiguous → keep the original + a ⚠ (the push gate still catches it).
|
|
44
|
+
// Fail-soft by construction: any error (not a git repo, missing file) keeps
|
|
45
|
+
// the served original untouched. Exported for tests.
|
|
46
|
+
export function rebaseCites(cites, cwd = process.cwd()) {
|
|
47
|
+
const out = [];
|
|
48
|
+
const notes = [];
|
|
49
|
+
for (const c of cites ?? []) {
|
|
50
|
+
try {
|
|
51
|
+
const [startRaw, endRaw] = String(c.line).split("-");
|
|
52
|
+
const start = Number(startRaw);
|
|
53
|
+
const end = Number(endRaw ?? startRaw);
|
|
54
|
+
if (!Number.isFinite(start) || citeResolves(join(cwd, c.file), start, end, c.token).ok) {
|
|
55
|
+
out.push(c);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const hits = gitGrep(c.token, cwd);
|
|
59
|
+
if (hits.length === 1) {
|
|
60
|
+
out.push({ ...c, file: hits[0].file, line: hits[0].line });
|
|
61
|
+
notes.push(`✓ re-based ${c.file}:${c.line} → ${hits[0].file}:${hits[0].line} :: ${c.token}`);
|
|
62
|
+
} else {
|
|
63
|
+
out.push(c);
|
|
64
|
+
notes.push(
|
|
65
|
+
`⚠ ${c.file}:${c.line} did not re-base (${hits.length === 0 ? "gone" : `ambiguous — ${hits.length} sites`}) — served line may be stale`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
out.push(c);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { cites: out, notes };
|
|
73
|
+
}
|
|
74
|
+
|
|
38
75
|
export default async function hydrate(args = []) {
|
|
39
76
|
const ROOT = process.cwd();
|
|
40
77
|
|
|
@@ -94,6 +131,14 @@ export default async function hydrate(args = []) {
|
|
|
94
131
|
die(e instanceof Error ? e.message : String(e));
|
|
95
132
|
}
|
|
96
133
|
const brainForSha = payload.envelope?.brainForSha ?? null;
|
|
134
|
+
// Wave 5.3 — arrive pre-healed: re-base served cites against THIS checkout
|
|
135
|
+
// before the file is written (drifted-but-unique tokens re-cite; gone/
|
|
136
|
+
// ambiguous keep the original + a ⚠ so nothing is ever guessed).
|
|
137
|
+
if (Array.isArray(payload.cites) && payload.cites.length > 0) {
|
|
138
|
+
const { cites, notes } = rebaseCites(payload.cites, ROOT);
|
|
139
|
+
payload.cites = cites;
|
|
140
|
+
for (const n of notes) console.log(` ${n}`);
|
|
141
|
+
}
|
|
97
142
|
const rel =
|
|
98
143
|
kind === "improvement"
|
|
99
144
|
? materializeImprovement(ROOT, payload, brainForSha)
|
package/lib/init.mjs
CHANGED
|
@@ -131,6 +131,13 @@ export default async function init(args) {
|
|
|
131
131
|
for (const u of r.updated) console.log(` ✓ repo MCPs → ${u}`);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Wave 6 — offer the declared skill dependencies (consent-gated, verified,
|
|
135
|
+
// stamped; declined remembered). .agents/skills is COMMITTED dev toolbox.
|
|
136
|
+
{
|
|
137
|
+
const { installSkillDeps } = await import("./skill-deps.mjs");
|
|
138
|
+
await installSkillDeps(TARGET, { args });
|
|
139
|
+
}
|
|
140
|
+
|
|
134
141
|
// Keep the brain out of the code repo's diffs; keep the per-dev MCP key out of git.
|
|
135
142
|
const ensureIgnored = (line, label) => {
|
|
136
143
|
const gi = join(TARGET, ".gitignore");
|
|
@@ -200,7 +207,8 @@ export default async function init(args) {
|
|
|
200
207
|
|
|
201
208
|
console.log(
|
|
202
209
|
`\n✓ Provisioned.\n\nNext steps:\n` +
|
|
203
|
-
" 1. Commit the vendored files (agents, /rafa command, skills, contract, rafa.json
|
|
210
|
+
" 1. Commit the vendored files (agents, /rafa command, skills, contract, rafa.json,\n" +
|
|
211
|
+
" and .agents/skills/ if you installed the skill dependencies)\n" +
|
|
204
212
|
" — protected-main teams: via a normal MR. The brain remote lives in rafa.json,\n" +
|
|
205
213
|
" so any teammate's clone bootstraps with npx rafa pull (no init needed).\n" +
|
|
206
214
|
" 2. Restart Claude Code in this repo (reload the /rafa command + agents + skills" +
|