@rafinery/cli 0.8.15 → 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 +104 -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 +2 -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 +62 -16
- 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 +21 -4
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +4 -1
- package/lib/brain-repo.mjs +1 -1
- package/lib/checkpoint.mjs +76 -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 +87 -18
- 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
package/lib/releases.mjs
CHANGED
|
@@ -264,6 +264,36 @@ export const RELEASES = [
|
|
|
264
264
|
"first branch checkout (bounded; a failure never breaks the checkout). " +
|
|
265
265
|
"`rafa update` re-vendors the post-checkout hook.",
|
|
266
266
|
},
|
|
267
|
+
{
|
|
268
|
+
version: "0.8.13",
|
|
269
|
+
contract: 1,
|
|
270
|
+
plans: 2,
|
|
271
|
+
requires: "update",
|
|
272
|
+
summary:
|
|
273
|
+
"LEDGER HYDRATION: `rafa hydrate improvement <id>` faults a ledger item into " +
|
|
274
|
+
".rafa/improve/improvements/<id>.md — the CANONICAL path — so bloom edits the " +
|
|
275
|
+
"same file the org brain carries (never a path twin). SOPs updated: an " +
|
|
276
|
+
"improvement the plan ADOPTS is hydrated AT PLAN TIME (bloom engages from the " +
|
|
277
|
+
"start, not only at build end); ledger status edits always hydrate first. " +
|
|
278
|
+
"Platform fixes ride server-side: set_active_plan now resolves v2 epics " +
|
|
279
|
+
"(was v1 parent-only — the not_found bug), get_improvement serves summary.",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
version: "0.8.14",
|
|
283
|
+
contract: 1,
|
|
284
|
+
plans: 2,
|
|
285
|
+
requires: "update",
|
|
286
|
+
summary:
|
|
287
|
+
"THE PROVENANCE SPINE (published 2026-07-24). Branch manifest: `rafa manifest` " +
|
|
288
|
+
"(zero-command — the post-commit brain hook regenerates it per mirrored commit): " +
|
|
289
|
+
"lenient snapshot with mode:branch · base.brainSha · per-note provenance " +
|
|
290
|
+
"{baseBrainSha, commits[], tasks[]} — the reconciler's three-way handoff doc. " +
|
|
291
|
+
"rafa-commit skill: subjects carry the active task id ([task-id] type: subject) " +
|
|
292
|
+
"and the manifest lifts it into per-note task lineage. Deterministic loop " +
|
|
293
|
+
"events: push gates emit gate-result from real exit codes; reflex --consume " +
|
|
294
|
+
"emits reflex-outcome. Benchmark is workflow-woven (scan's Prove-it step + " +
|
|
295
|
+
"session-start absence sensor) — never a dev-typed command.",
|
|
296
|
+
},
|
|
267
297
|
{
|
|
268
298
|
version: "0.8.15",
|
|
269
299
|
contract: 1,
|
|
@@ -284,34 +314,73 @@ export const RELEASES = [
|
|
|
284
314
|
"aware recall (tier-labeled), compass sitback, gap lifecycle beats.",
|
|
285
315
|
},
|
|
286
316
|
{
|
|
287
|
-
version: "0.8.
|
|
317
|
+
version: "0.8.16",
|
|
288
318
|
contract: 1,
|
|
289
319
|
plans: 2,
|
|
290
320
|
requires: "update",
|
|
291
321
|
summary:
|
|
292
|
-
"THE
|
|
293
|
-
"
|
|
294
|
-
"
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"
|
|
322
|
+
"THE SESSION TAIL NEVER STRANDS (live-catch 2026-07-24: brain notes " +
|
|
323
|
+
"authored AFTER the last code commit sat uncommitted while `rafa " +
|
|
324
|
+
"checkpoint` said clean — capture plane synced, git plane dirty). " +
|
|
325
|
+
"`rafa checkpoint` now owns BOTH planes: after the working-set sync it " +
|
|
326
|
+
"fires the vendored brain-commit worker whenever the brain tree is " +
|
|
327
|
+
"dirty, so the tail commits + pushes at the same beat that creates it. " +
|
|
328
|
+
"The worker is tail-aware: HEAD already mirrored ⇒ a distinct " +
|
|
329
|
+
"`session tail · N file(s)` subject on the same code-commit trailer " +
|
|
330
|
+
"(never a look-alike duplicate), and an empty tail is a no-op. " +
|
|
331
|
+
"review.json (the review gate's scored-ranges artifact) joins the " +
|
|
332
|
+
"local-state exclusion — machine state, never brain knowledge.",
|
|
301
333
|
},
|
|
302
334
|
{
|
|
303
|
-
version: "0.
|
|
335
|
+
version: "0.9.0",
|
|
304
336
|
contract: 1,
|
|
305
337
|
plans: 2,
|
|
306
338
|
requires: "update",
|
|
307
339
|
summary:
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
"
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
"(
|
|
340
|
+
"WAVE 5 — trust plane + transport economics (the QA-dogfood fixes). " +
|
|
341
|
+
"Loop events are STRICT (no legacy path): required actor envelope " +
|
|
342
|
+
"{model, agent, runner} + required verification {method: static|live, " +
|
|
343
|
+
"tool, evidence} on verdict/gate categories + idempotent dedupeKey + " +
|
|
344
|
+
"tier — pre-0.9.0 emits reject loudly (owner: old CLIs discarded). " +
|
|
345
|
+
"One beat per task boundary: patch_plan_item (field-level item patch) " +
|
|
346
|
+
"+ checkpoint_task (patch + loop event + decisions, ONE transaction). " +
|
|
347
|
+
"verify-citations --fix heals drifted cites (unique-grep only — never " +
|
|
348
|
+
"guesses) and hydrate re-bases served cites on arrival. NEW `rafa " +
|
|
349
|
+
"facts`: the session-facts plane (memoized verification, sha-anchored " +
|
|
350
|
+
"invalidation). The SessionStart digest computes 'sage due' from a " +
|
|
351
|
+
"checkpoint-written loop-event cache — sage's trigger is mechanical. " +
|
|
352
|
+
"Plan tasks gain validation_tier (light|standard|full — prism depth " +
|
|
353
|
+
"only; the Done-check never relaxes). Cards/SOPs re-vendored: prism " +
|
|
354
|
+
"0.8.0 (tool authenticity · structured return · bounded invariant " +
|
|
355
|
+
"citation) · atlas 3.9.0 · sage 0.4.0 · conductor 2.1.0.",
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
version: "0.10.0",
|
|
359
|
+
contract: 1,
|
|
360
|
+
plans: 2,
|
|
361
|
+
requires: "update",
|
|
362
|
+
summary:
|
|
363
|
+
"WAVE 6 — the capability arc. NEW: consent-gated SKILL DEPENDENCIES — " +
|
|
364
|
+
"this release declares 7 third-party skills (tdd · frontend-design · " +
|
|
365
|
+
"vercel-composition-patterns · requesting-code-review · " +
|
|
366
|
+
"improve-codebase-architecture · grill-me · grilling), and `rafa " +
|
|
367
|
+
"update`/`init` offers them ONCE with per-skill reasons, installs " +
|
|
368
|
+
"package-manager style into the HARNESS-NEUTRAL .agents/skills/ " +
|
|
369
|
+
"(Claude Code · Codex · Cursor read the same convention), verifies " +
|
|
370
|
+
"(frontmatter · hashes · licenses), and stamps rafa.json. Declined is " +
|
|
371
|
+
"remembered (re-offer: --skills); --yes for CI; a declined set NEVER " +
|
|
372
|
+
"breaks anything (F1: cards never point at .agents). Agent upgrades " +
|
|
373
|
+
"riding it: TDD-DEFAULT builds (atlas 4.0.0 — the Done-check is a " +
|
|
374
|
+
"failing test first at a dev-approved seam; red→green evidence; prism " +
|
|
375
|
+
"verifies tier-scaled), the UI design method (frontend-design invoked, " +
|
|
376
|
+
"brain conventions win), the plan-gate GRILL pass (tier-bounded " +
|
|
377
|
+
"adversarial interview; vacuous Done-checks caught), severity-labeled " +
|
|
378
|
+
"findings (Critical/Important/Minor — Minor never flips a verdict; " +
|
|
379
|
+
"prism 0.9.0), and bloom's architecture method (deletion test · " +
|
|
380
|
+
"Confidence-lined candidates; bloom 0.9.0). Conductor 2.2.0: the " +
|
|
381
|
+
"toolbox recall surface includes .agents/skills; `rafa leverage " +
|
|
382
|
+
"--json` carries the harness-neutral agentSkills + skillDeps blocks; " +
|
|
383
|
+
"doctor gains a capabilities section.",
|
|
315
384
|
},
|
|
316
385
|
];
|
|
317
386
|
|
package/lib/review.mjs
CHANGED
|
@@ -117,6 +117,37 @@ export function relevanceOf(note, diffTokens) {
|
|
|
117
117
|
return { score, engage: score >= 3 ? "deep" : score >= 1 ? "triage" : "skip" };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Summary counters, deduped by id (wave 5 T7): get_code_context runs per
|
|
121
|
+
// changed file, so a note citing N changed files lands in N per-file rows —
|
|
122
|
+
// counting it N times inflated every summary number. Dedupe is DISPLAY-COUNT
|
|
123
|
+
// only: the per-file sections (and review.json's files[]) stay per-file — a
|
|
124
|
+
// note governing two files still prints under both. A note keeps its STRONGEST
|
|
125
|
+
// classification: direct beats file-hit, engage beats skip.
|
|
126
|
+
export function summarizeReview(out) {
|
|
127
|
+
const dedupe = (rows) => {
|
|
128
|
+
const seen = new Map();
|
|
129
|
+
for (const r of rows) if (!seen.has(r.id)) seen.set(r.id, r);
|
|
130
|
+
return [...seen.values()];
|
|
131
|
+
};
|
|
132
|
+
const allNotes = out.flatMap((f) => f.notes);
|
|
133
|
+
const direct = dedupe(allNotes.filter((r) => r.hit === "direct"));
|
|
134
|
+
const directIds = new Set(direct.map((r) => r.id));
|
|
135
|
+
const fileHits = dedupe(allNotes.filter((r) => r.hit === "file")).filter(
|
|
136
|
+
(r) => !directIds.has(r.id),
|
|
137
|
+
);
|
|
138
|
+
const engaged = dedupe(allNotes.filter((r) => r.engage !== "skip"));
|
|
139
|
+
const engagedIds = new Set(engaged.map((r) => r.id));
|
|
140
|
+
const skipped = dedupe(allNotes).filter((r) => !engagedIds.has(r.id));
|
|
141
|
+
return {
|
|
142
|
+
direct,
|
|
143
|
+
fileHits,
|
|
144
|
+
imps: dedupe(out.flatMap((f) => f.improvements)),
|
|
145
|
+
stale: dedupe(allNotes.filter((r) => r.staleCites)),
|
|
146
|
+
engaged,
|
|
147
|
+
skipped,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
120
151
|
export default async function review(args = []) {
|
|
121
152
|
const ROOT = process.cwd();
|
|
122
153
|
if (!existsSync(join(ROOT, "rafa.json"))) {
|
|
@@ -208,12 +239,7 @@ export default async function review(args = []) {
|
|
|
208
239
|
console.log(JSON.stringify(report, null, 2));
|
|
209
240
|
return;
|
|
210
241
|
}
|
|
211
|
-
const direct
|
|
212
|
-
const fileHits = out.flatMap((f) => f.notes.filter((r) => r.hit === "file"));
|
|
213
|
-
const imps = out.flatMap((f) => f.improvements);
|
|
214
|
-
const stale = out.flatMap((f) => f.notes.filter((r) => r.staleCites));
|
|
215
|
-
const engaged = out.flatMap((f) => f.notes.filter((r) => r.engage !== "skip"));
|
|
216
|
-
const skipped = out.flatMap((f) => f.notes.filter((r) => r.engage === "skip"));
|
|
242
|
+
const { direct, fileHits, imps, stale, engaged, skipped } = summarizeReview(out);
|
|
217
243
|
console.log(
|
|
218
244
|
`✓ review pre-pass vs ${base.slice(0, 7)}: ${files.length} changed file(s)` +
|
|
219
245
|
(dropped > 0 ? ` (${dropped} beyond the ${MAX_FILES}-file bound NOT reviewed)` : ""),
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The session-facts plane (wave 5.4 — memoized verification).
|
|
2
|
+
//
|
|
3
|
+
// QA-session finding: six subagents independently re-derived the same
|
|
4
|
+
// environment facts ("no node_modules, workspace:* deps") in one build. The fix
|
|
5
|
+
// is NOT a trust-me cache — that would be the assumed-values disease. Every
|
|
6
|
+
// fact carries its VERIFICATION EVIDENCE (the command + exit code that proved
|
|
7
|
+
// it) and a MECHANICAL invalidation rule: the fact is citable only while
|
|
8
|
+
// `git diff <verifiedAtSha>` (against the working tree, uncommitted edits
|
|
9
|
+
// count) leaves its `dependsOn` paths untouched. Memoization with
|
|
10
|
+
// invalidation — the same move as brain = f(code@sha), at session scale.
|
|
11
|
+
//
|
|
12
|
+
// File: .rafa/session-facts.json — machine/session state, excluded from brain
|
|
13
|
+
// pushes (LOCAL_STATE in brain-commit.mjs + IGNORES in brain-repo.mjs; a
|
|
14
|
+
// defensive self-ignore rides every write). Conductor writes via `rafa facts`;
|
|
15
|
+
// subagents READ FIRST and cite "SF-<n> unchanged" instead of re-deriving.
|
|
16
|
+
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
export const FACTS_REL = "session-facts.json";
|
|
22
|
+
export const factsPath = (cwd) => join(cwd, ".rafa", FACTS_REL);
|
|
23
|
+
|
|
24
|
+
const EMPTY = () => ({ schemaVersion: 1, facts: [] });
|
|
25
|
+
|
|
26
|
+
export function readFacts(cwd = process.cwd()) {
|
|
27
|
+
try {
|
|
28
|
+
const doc = JSON.parse(readFileSync(factsPath(cwd), "utf8"));
|
|
29
|
+
if (doc?.schemaVersion !== 1 || !Array.isArray(doc.facts)) return EMPTY();
|
|
30
|
+
return doc;
|
|
31
|
+
} catch {
|
|
32
|
+
return EMPTY();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A malformed fact is rejected loudly at write time — never stored, never
|
|
37
|
+
// silently dropped at read time. Returns an error string or null.
|
|
38
|
+
export function validateFact(f) {
|
|
39
|
+
if (!f || typeof f !== "object") return "fact must be an object";
|
|
40
|
+
if (typeof f.id !== "string" || !/^SF-\d+$/.test(f.id)) return "id must be SF-<n>";
|
|
41
|
+
if (typeof f.claim !== "string" || f.claim.trim() === "") return "claim is required";
|
|
42
|
+
if (!f.evidence || typeof f.evidence.command !== "string" || f.evidence.command.trim() === "")
|
|
43
|
+
return "evidence.command is required — a fact without the command that proved it is an assumed value";
|
|
44
|
+
if (!Number.isInteger(f.evidence.exitCode)) return "evidence.exitCode must be an integer";
|
|
45
|
+
if (typeof f.verifiedAtSha !== "string" || f.verifiedAtSha.trim() === "")
|
|
46
|
+
return "verifiedAtSha is required";
|
|
47
|
+
if (!Array.isArray(f.dependsOn)) return "dependsOn must be an array of paths (may be empty)";
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeFacts(cwd, doc) {
|
|
52
|
+
const dir = join(cwd, ".rafa");
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
writeFileSync(factsPath(cwd), JSON.stringify(doc, null, 2) + "\n");
|
|
55
|
+
// Defensive self-ignore (post-tool.mjs pattern): the facts file must never
|
|
56
|
+
// ride a brain push even if this repo predates the wave-5 ignore lists.
|
|
57
|
+
try {
|
|
58
|
+
const gi = join(dir, ".gitignore");
|
|
59
|
+
const cur = existsSync(gi) ? readFileSync(gi, "utf8") : "";
|
|
60
|
+
const have = new Set(cur.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
61
|
+
if (!have.has(FACTS_REL))
|
|
62
|
+
writeFileSync(gi, cur + (cur === "" || cur.endsWith("\n") ? "" : "\n") + FACTS_REL + "\n");
|
|
63
|
+
} catch {
|
|
64
|
+
/* ignore-file hygiene is best-effort — the canonical lists also carry it */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const short = (sha) => String(sha).slice(0, 7);
|
|
69
|
+
|
|
70
|
+
// The mechanical invalidation rule. A fact goes stale when:
|
|
71
|
+
// - any dependsOn path changed since verifiedAtSha (working tree INCLUDED —
|
|
72
|
+
// an uncommitted edit invalidates just as hard), or
|
|
73
|
+
// - verifiedAtSha is unknown to this checkout (rebase/gc) → fail-honest.
|
|
74
|
+
// Empty dependsOn = environment-shaped fact, never auto-stale (invalidate by hand).
|
|
75
|
+
export function staleFacts(cwd = process.cwd(), doc = null) {
|
|
76
|
+
const d = doc ?? readFacts(cwd);
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const f of d.facts) {
|
|
79
|
+
try {
|
|
80
|
+
const changed = execSync(`git diff --name-only ${f.verifiedAtSha}`, {
|
|
81
|
+
cwd,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
84
|
+
})
|
|
85
|
+
.split("\n")
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
const hit = (f.dependsOn ?? []).filter((p) =>
|
|
88
|
+
changed.some((c) => c === p || c.startsWith(p.endsWith("/") ? p : p + "/")),
|
|
89
|
+
);
|
|
90
|
+
if (hit.length)
|
|
91
|
+
out.push({ id: f.id, reason: `dependsOn touched since ${short(f.verifiedAtSha)}: ${hit.join(", ")}` });
|
|
92
|
+
} catch {
|
|
93
|
+
out.push({
|
|
94
|
+
id: f.id,
|
|
95
|
+
reason: `verifiedAtSha ${short(f.verifiedAtSha)} unknown to this checkout — re-verify or drop`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function nextFactId(doc) {
|
|
103
|
+
const max = doc.facts.reduce((m, f) => {
|
|
104
|
+
const n = Number(String(f.id).replace(/^SF-/, ""));
|
|
105
|
+
return Number.isFinite(n) && n > m ? n : m;
|
|
106
|
+
}, 0);
|
|
107
|
+
return `SF-${max + 1}`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Skill dependencies (wave 6 — the capability arc).
|
|
2
|
+
//
|
|
3
|
+
// rafa's agents lean on a small set of third-party skills (TDD, design method,
|
|
4
|
+
// composition rules, review taxonomy, the grilling interview). They are NOT
|
|
5
|
+
// baked into the blueprint: the dev owns their toolbox, so the CLI DECLARES
|
|
6
|
+
// them here and `rafa update`/`init` offers them ONCE with per-skill reasons —
|
|
7
|
+
// package-manager style: consent → sequential install with progress → verify
|
|
8
|
+
// (frontmatter parses · hashes match · licenses present) → stamp. A decline is
|
|
9
|
+
// remembered (`rafa.json → rafa.skills.declined`; re-offer with `rafa update
|
|
10
|
+
// --skills`), `--yes` installs unprompted (CI), and a verify failure removes
|
|
11
|
+
// the partial dir and NEVER blocks provisioning — a missing capability
|
|
12
|
+
// degrades the SOPs gracefully, it never bricks a repo.
|
|
13
|
+
//
|
|
14
|
+
// The skills ship inside the npm tarball (skills-bundle/, hashed at prepack —
|
|
15
|
+
// deterministic, offline, license-complete). Install target: .agents/skills/
|
|
16
|
+
// (committed — the repo owns the copies; the harness-neutral home, Codex
|
|
17
|
+
// manifests ride along). Agent adoption is the point: the conductor's toolbox
|
|
18
|
+
// slice + the rafa-* SOPs reference these BY CAPABILITY with when-installed
|
|
19
|
+
// semantics — agent-card duty paths NEVER point here (F1: a declined skill
|
|
20
|
+
// must never break a consumer's compile gate).
|
|
21
|
+
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import {
|
|
24
|
+
cpSync,
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
rmSync,
|
|
30
|
+
statSync,
|
|
31
|
+
} from "node:fs";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
import { readStamp, writeStamp } from "./stamp.mjs";
|
|
35
|
+
import { isInteractive, ask } from "./prompt.mjs";
|
|
36
|
+
|
|
37
|
+
const HERE = dirname(fileURLToPath(import.meta.url)); // …/packages/cli/lib
|
|
38
|
+
|
|
39
|
+
// The declared dependencies. `version` is rafa's pin for the vendored snapshot;
|
|
40
|
+
// `source`/`upstreamHash` mirror skills-lock.json provenance at authoring time;
|
|
41
|
+
// `why` is the one-line reason shown in the consent prompt; `licenseFile` names
|
|
42
|
+
// the in-dir license the verify pass requires (null = license carried in
|
|
43
|
+
// frontmatter/metadata, or rafa-authored).
|
|
44
|
+
export const SKILL_DEPS = [
|
|
45
|
+
{
|
|
46
|
+
name: "tdd",
|
|
47
|
+
version: "1.0.0",
|
|
48
|
+
source: "mattpocock/skills",
|
|
49
|
+
upstreamHash: "81eca2a5b53a",
|
|
50
|
+
targetDir: ".agents/skills/tdd",
|
|
51
|
+
license: "MIT",
|
|
52
|
+
licenseFile: "LICENSE",
|
|
53
|
+
why: "atlas builds test-first — the Done-check becomes an executable test (red → green)",
|
|
54
|
+
requiredBy: "atlas · rafa-build",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "frontend-design",
|
|
58
|
+
version: "1.0.0",
|
|
59
|
+
source: "anthropics/skills",
|
|
60
|
+
upstreamHash: "4eabc6618376",
|
|
61
|
+
targetDir: ".agents/skills/frontend-design",
|
|
62
|
+
license: "Apache-2.0",
|
|
63
|
+
licenseFile: "LICENSE.txt",
|
|
64
|
+
why: "UI tasks get a design method (token system · signature element) instead of template defaults",
|
|
65
|
+
requiredBy: "atlas · rafa-build",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "vercel-composition-patterns",
|
|
69
|
+
version: "1.0.0",
|
|
70
|
+
source: "vercel-labs/agent-skills",
|
|
71
|
+
upstreamHash: "575757e3e257",
|
|
72
|
+
targetDir: ".agents/skills/vercel-composition-patterns",
|
|
73
|
+
license: "MIT",
|
|
74
|
+
licenseFile: null, // license declared in SKILL.md frontmatter + metadata.json
|
|
75
|
+
why: "React composition rules for component tasks (no boolean-prop sprawl · compound components · React 19)",
|
|
76
|
+
requiredBy: "atlas · rafa-build",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "requesting-code-review",
|
|
80
|
+
version: "1.0.0",
|
|
81
|
+
source: "obra/superpowers",
|
|
82
|
+
upstreamHash: "2304d2073c62",
|
|
83
|
+
targetDir: ".agents/skills/requesting-code-review",
|
|
84
|
+
license: "MIT",
|
|
85
|
+
licenseFile: "LICENSE",
|
|
86
|
+
why: "review findings get Critical/Important/Minor severity — Minor never blocks",
|
|
87
|
+
requiredBy: "prism · rafa-review",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "improve-codebase-architecture",
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
source: "mattpocock/skills",
|
|
93
|
+
upstreamHash: "66e8a50c83c3",
|
|
94
|
+
targetDir: ".agents/skills/improve-codebase-architecture",
|
|
95
|
+
license: "MIT",
|
|
96
|
+
licenseFile: "LICENSE",
|
|
97
|
+
why: "bloom's architecture lens gets a real method (deletion test · deep/shallow modules)",
|
|
98
|
+
requiredBy: "bloom · rafa-improve",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "grill-me",
|
|
102
|
+
version: "1.0.0",
|
|
103
|
+
source: "mattpocock/skills",
|
|
104
|
+
upstreamHash: "f361db4e15e6",
|
|
105
|
+
targetDir: ".agents/skills/grill-me",
|
|
106
|
+
license: "MIT",
|
|
107
|
+
licenseFile: "LICENSE",
|
|
108
|
+
why: "/grill-me — adversarial interview of any plan (human-invoked)",
|
|
109
|
+
requiredBy: "dev · prism plan-gate",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "grilling",
|
|
113
|
+
version: "1.0.0",
|
|
114
|
+
source: "rafinery",
|
|
115
|
+
upstreamHash: null,
|
|
116
|
+
targetDir: ".agents/skills/grilling",
|
|
117
|
+
license: "rafinery (ships with the CLI)",
|
|
118
|
+
licenseFile: null,
|
|
119
|
+
why: "the interview procedure grill-me and the plan gate run (one probe at a time · survives-three ends it)",
|
|
120
|
+
requiredBy: "grill-me · prism plan-gate",
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
// ── bundle resolution (mirrors blueprintSource: bundled copy first, monorepo dev fallback) ──
|
|
125
|
+
|
|
126
|
+
const walkFiles = (dir, base = dir) =>
|
|
127
|
+
readdirSync(dir).flatMap((e) => {
|
|
128
|
+
const p = join(dir, e);
|
|
129
|
+
if (statSync(p).isDirectory()) return walkFiles(p, base);
|
|
130
|
+
return [p.slice(base.length + 1)];
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const hashFile = (abs) =>
|
|
134
|
+
createHash("sha256").update(readFileSync(abs)).digest("hex");
|
|
135
|
+
|
|
136
|
+
// {dir, manifest: {skills: {name: {version, files: {rel: sha256}}}}} or null.
|
|
137
|
+
export function loadSkillsBundle() {
|
|
138
|
+
const bundled = join(HERE, "..", "skills-bundle");
|
|
139
|
+
if (existsSync(join(bundled, "skills-manifest.json"))) {
|
|
140
|
+
try {
|
|
141
|
+
return {
|
|
142
|
+
dir: bundled,
|
|
143
|
+
manifest: JSON.parse(readFileSync(join(bundled, "skills-manifest.json"), "utf8")),
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Dev fallback: the monorepo's .agents/skills IS the source — manifest computed live.
|
|
150
|
+
const dev = join(HERE, "..", "..", "..", ".agents", "skills");
|
|
151
|
+
if (!existsSync(dev)) return null;
|
|
152
|
+
const skills = {};
|
|
153
|
+
for (const d of SKILL_DEPS) {
|
|
154
|
+
const src = join(dev, d.name);
|
|
155
|
+
if (!existsSync(src)) continue;
|
|
156
|
+
skills[d.name] = {
|
|
157
|
+
version: d.version,
|
|
158
|
+
files: Object.fromEntries(walkFiles(src).map((rel) => [rel, hashFile(join(src, rel))])),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { dir: dev, manifest: { skills } };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── stamp helpers ──────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export function skillsStamp(cwd) {
|
|
167
|
+
const s = readStamp(cwd).rafa?.skills ?? {};
|
|
168
|
+
return { installed: s.installed ?? {}, declined: s.declined ?? [] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function writeSkillsStamp(cwd, { installed, declined }) {
|
|
172
|
+
writeStamp(cwd, { rafa: { skills: { installed, declined } } });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function neededSkills(cwd, { reoffer = false } = {}) {
|
|
176
|
+
const { installed, declined } = skillsStamp(cwd);
|
|
177
|
+
const declinedSet = new Set(declined);
|
|
178
|
+
return SKILL_DEPS.filter(
|
|
179
|
+
(d) => installed[d.name] === undefined && (reoffer || !declinedSet.has(d.name)),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── verify (also the doctor's capabilities check) ─────────────────────────────
|
|
184
|
+
|
|
185
|
+
// Lenient head-parse for FOREIGN skill files: third-party SKILL.md frontmatter
|
|
186
|
+
// is general YAML (folded scalars, nested maps) that the strict OKF grammar
|
|
187
|
+
// rightly rejects — these are Claude skills, not OKF concepts. Verify only what
|
|
188
|
+
// a skill needs to be loadable: a fenced frontmatter block carrying `name:`.
|
|
189
|
+
export function skillHead(abs) {
|
|
190
|
+
let text;
|
|
191
|
+
try {
|
|
192
|
+
text = readFileSync(abs, "utf8");
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (!text.startsWith("---")) return null;
|
|
197
|
+
const end = text.indexOf("\n---", 3);
|
|
198
|
+
if (end === -1) return null;
|
|
199
|
+
const fm = text.slice(3, end);
|
|
200
|
+
const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim();
|
|
201
|
+
const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
|
|
202
|
+
return name ? { name, description } : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The harness-neutral third-party toolbox inventory: every .agents/skills/<name>
|
|
206
|
+
// with a loadable SKILL.md, as {name, description}. `.agents/` is the
|
|
207
|
+
// cross-harness home (Claude Code · Codex · Cursor — the vendored skills ship
|
|
208
|
+
// Codex `agents/openai.yaml` manifests alongside SKILL.md), so this reader
|
|
209
|
+
// lives HERE, not inside any one toolchain adapter; the leverage engine's
|
|
210
|
+
// repoContext, the conductor's spawn-prompt slice, and the scan's toolbox
|
|
211
|
+
// domain all consume this one implementation.
|
|
212
|
+
export function listAgentSkills(cwd) {
|
|
213
|
+
const root = join(cwd, ".agents", "skills");
|
|
214
|
+
if (!existsSync(root)) return [];
|
|
215
|
+
return readdirSync(root)
|
|
216
|
+
.filter((e) => {
|
|
217
|
+
try {
|
|
218
|
+
return statSync(join(root, e)).isDirectory();
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
.map((e) => {
|
|
224
|
+
const head = skillHead(join(root, e, "SKILL.md"));
|
|
225
|
+
return head ? { name: head.name, description: head.description } : null;
|
|
226
|
+
})
|
|
227
|
+
.filter(Boolean);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function verifySkill(cwd, dep, bundleManifest) {
|
|
231
|
+
const dst = join(cwd, dep.targetDir);
|
|
232
|
+
if (!existsSync(join(dst, "SKILL.md"))) return { ok: false, reason: "SKILL.md missing" };
|
|
233
|
+
const head = skillHead(join(dst, "SKILL.md"));
|
|
234
|
+
if (!head) return { ok: false, reason: "SKILL.md frontmatter does not parse (no fenced name:)" };
|
|
235
|
+
if (dep.licenseFile && !existsSync(join(dst, dep.licenseFile)))
|
|
236
|
+
return { ok: false, reason: `license file ${dep.licenseFile} missing` };
|
|
237
|
+
const expect = bundleManifest?.skills?.[dep.name]?.files;
|
|
238
|
+
if (expect) {
|
|
239
|
+
for (const [rel, sha] of Object.entries(expect)) {
|
|
240
|
+
const abs = join(dst, rel);
|
|
241
|
+
if (!existsSync(abs)) return { ok: false, reason: `${rel} missing` };
|
|
242
|
+
if (hashFile(abs) !== sha) return { ok: false, reason: `${rel} hash mismatch` };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { ok: true };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Doctor's read: every STAMPED-installed skill re-verified against disk.
|
|
249
|
+
export function verifyInstalled(cwd) {
|
|
250
|
+
const { installed } = skillsStamp(cwd);
|
|
251
|
+
const bundle = loadSkillsBundle();
|
|
252
|
+
const results = [];
|
|
253
|
+
for (const [name] of Object.entries(installed)) {
|
|
254
|
+
const dep = SKILL_DEPS.find((d) => d.name === name);
|
|
255
|
+
if (!dep) {
|
|
256
|
+
results.push({ name, ok: false, reason: "stamped but no longer a declared dependency" });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
results.push({ name, ...verifySkill(cwd, dep, bundle?.manifest) });
|
|
260
|
+
}
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── the consent-gated installer (owner's design, wave 6) ──────────────────────
|
|
265
|
+
//
|
|
266
|
+
// prompt is injectable for tests: async (question) => "y" | "n" | … | null
|
|
267
|
+
// (null = non-interactive). Returns { installed, declined, failed, skipped }.
|
|
268
|
+
export async function installSkillDeps(cwd, { prompt = null, args = [] } = {}) {
|
|
269
|
+
const reoffer = args.includes("--skills");
|
|
270
|
+
const yes = args.includes("--yes");
|
|
271
|
+
const needed = neededSkills(cwd, { reoffer });
|
|
272
|
+
const result = { installed: [], declined: [], failed: [], skipped: false };
|
|
273
|
+
if (needed.length === 0) return { ...result, skipped: true };
|
|
274
|
+
|
|
275
|
+
const bundle = loadSkillsBundle();
|
|
276
|
+
if (!bundle) {
|
|
277
|
+
console.log(" ! skill dependencies declared but no skills bundle in this CLI build — skipped");
|
|
278
|
+
return { ...result, skipped: true };
|
|
279
|
+
}
|
|
280
|
+
const available = needed.filter((d) => bundle.manifest.skills?.[d.name]);
|
|
281
|
+
if (available.length === 0) return { ...result, skipped: true };
|
|
282
|
+
|
|
283
|
+
console.log(`\nrafa needs ${available.length} skill(s) for better performance in this release:\n`);
|
|
284
|
+
for (const d of available)
|
|
285
|
+
console.log(
|
|
286
|
+
` ${(d.name + " " + d.version).padEnd(38)}${d.why} [${d.license}]`,
|
|
287
|
+
);
|
|
288
|
+
console.log(
|
|
289
|
+
"\n They install to .agents/skills/ (committed — your repo owns the copies)." +
|
|
290
|
+
"\n Decline with n; re-offer any time with `rafa update --skills`.\n",
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const doAsk = prompt ?? (async (q) => (isInteractive() ? ask(q) : null));
|
|
294
|
+
|
|
295
|
+
let toInstall = [];
|
|
296
|
+
const newlyDeclined = [];
|
|
297
|
+
if (yes) {
|
|
298
|
+
toInstall = available;
|
|
299
|
+
} else if (reoffer) {
|
|
300
|
+
// Per-skill grammar (the makeConflictResolver interaction): y/N/a/q.
|
|
301
|
+
let all = false;
|
|
302
|
+
let quit = false;
|
|
303
|
+
for (const d of available) {
|
|
304
|
+
if (all) {
|
|
305
|
+
toInstall.push(d);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (quit) {
|
|
309
|
+
newlyDeclined.push(d.name);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const a = await doAsk(` • install ${d.name} ${d.version}? [y = yes · N = no · a = all · q = none] `);
|
|
313
|
+
if (a === null) return { ...result, skipped: true }; // non-TTY: record NOTHING
|
|
314
|
+
if (a === "a") {
|
|
315
|
+
all = true;
|
|
316
|
+
toInstall.push(d);
|
|
317
|
+
} else if (a === "q") {
|
|
318
|
+
quit = true;
|
|
319
|
+
newlyDeclined.push(d.name);
|
|
320
|
+
} else if (a === "y" || a === "yes") toInstall.push(d);
|
|
321
|
+
else newlyDeclined.push(d.name);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
const a = await doAsk(` Install all ${available.length}? [Y/n] `);
|
|
325
|
+
if (a === null) {
|
|
326
|
+
// Non-TTY without --yes: skip, stamp NOTHING — a human must decide;
|
|
327
|
+
// CI silently declining for them would be an assumed value.
|
|
328
|
+
console.log(" · non-interactive — skipped (install later: `rafa update --skills`, or pass --yes)");
|
|
329
|
+
return { ...result, skipped: true };
|
|
330
|
+
}
|
|
331
|
+
if (a === "" || a === "y" || a === "yes") toInstall = available;
|
|
332
|
+
else newlyDeclined.push(...available.map((d) => d.name));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const d of toInstall) {
|
|
336
|
+
const src = join(bundle.dir, d.name);
|
|
337
|
+
const dst = join(cwd, d.targetDir);
|
|
338
|
+
try {
|
|
339
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
340
|
+
cpSync(src, dst, { recursive: true });
|
|
341
|
+
const v = verifySkill(cwd, d, bundle.manifest);
|
|
342
|
+
if (!v.ok) {
|
|
343
|
+
rmSync(dst, { recursive: true, force: true });
|
|
344
|
+
console.log(` ✗ ${d.name} ${d.version} — verify failed (${v.reason}); not installed`);
|
|
345
|
+
result.failed.push({ name: d.name, reason: v.reason });
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
console.log(` ✓ ${d.name} ${d.version} — installed (${d.targetDir})`);
|
|
349
|
+
result.installed.push(d.name);
|
|
350
|
+
} catch (e) {
|
|
351
|
+
rmSync(dst, { recursive: true, force: true });
|
|
352
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
353
|
+
console.log(` ✗ ${d.name} ${d.version} — install failed (${reason}); not installed`);
|
|
354
|
+
result.failed.push({ name: d.name, reason });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Stamp: installed adds; declined = explicit human "no" only, deduped.
|
|
359
|
+
const cur = skillsStamp(cwd);
|
|
360
|
+
const installed = { ...cur.installed };
|
|
361
|
+
for (const n of result.installed) {
|
|
362
|
+
const dep = SKILL_DEPS.find((d) => d.name === n);
|
|
363
|
+
installed[n] = dep.version;
|
|
364
|
+
}
|
|
365
|
+
const declined = [...new Set([...cur.declined.filter((n) => !result.installed.includes(n)), ...newlyDeclined])];
|
|
366
|
+
writeSkillsStamp(cwd, { installed, declined });
|
|
367
|
+
result.declined = newlyDeclined;
|
|
368
|
+
|
|
369
|
+
if (result.installed.length)
|
|
370
|
+
console.log(
|
|
371
|
+
` ✓ verified ${result.installed.length}/${toInstall.length} (frontmatter parses · hashes match · licenses present)` +
|
|
372
|
+
`\n → commit .agents/skills/ — the repo owns its toolbox`,
|
|
373
|
+
);
|
|
374
|
+
if (newlyDeclined.length)
|
|
375
|
+
console.log(` · declined ${newlyDeclined.length} — remembered; agents fall back per SOP (re-offer: rafa update --skills)`);
|
|
376
|
+
return result;
|
|
377
|
+
}
|