@rafinery/cli 0.8.16 → 0.11.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 +130 -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 +23 -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-commit/SKILL.md +22 -0
- package/blueprint/.claude/skills/rafa-distill/SKILL.md +37 -4
- 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 +26 -15
- package/lib/distill.mjs +56 -3
- package/lib/distiller/doctrine.mjs +5 -16
- package/lib/distiller/schema-ladder.mjs +186 -0
- 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 +19 -0
- package/lib/push.mjs +18 -4
- package/lib/reflex.mjs +5 -1
- package/lib/releases.mjs +78 -0
- package/lib/review.mjs +32 -6
- package/lib/session-facts.mjs +108 -0
- package/lib/skill-deps.mjs +377 -0
- package/lib/stamp.mjs +24 -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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// The schema ladder (0.11.0 — schema-aware reconciliation, owner's 4-case
|
|
2
|
+
// doctrine, 2026-07-25). Distillation is where knowledge from two lineages
|
|
3
|
+
// meets, so it is ALSO where brain schema versions meet — the reconciler
|
|
4
|
+
// resolves the version lattice BEFORE any claim-level judging, and the merged
|
|
5
|
+
// result always lands on the newest schema the RUNNER understands:
|
|
6
|
+
//
|
|
7
|
+
// 1. target < source → REWRITE-TARGET: the whole target brain
|
|
8
|
+
// lifts to the newer schema (the incoming side already speaks it).
|
|
9
|
+
// 2. target > source → UPGRADE-SOURCE: incoming old-schema
|
|
10
|
+
// candidates lift into the target's newer schema before judging.
|
|
11
|
+
// 3. target == source < latest → REWRITE-MERGED: both sides lift; the
|
|
12
|
+
// merged target is a full rewrite onto the latest schema.
|
|
13
|
+
// 4. target == source == latest → DIFF: the happy flow, exactly today.
|
|
14
|
+
// ∅. either side > latest → ABORT LOUDLY: a newer CLI authored this;
|
|
15
|
+
// this runner cannot judge a schema it does not know — update the
|
|
16
|
+
// runner, NEVER downgrade knowledge.
|
|
17
|
+
//
|
|
18
|
+
// Every non-abort mode converges in ONE pass: toVersion is always the
|
|
19
|
+
// runner's latest (which case 1/2's higher side already equals or precedes —
|
|
20
|
+
// the runner can never know less than what it authored). Transforms are
|
|
21
|
+
// REGISTERED per step (v(N-1)→vN) in LADDER below; a missing step throws —
|
|
22
|
+
// the ladder never guesses a schema (no assumed values). v1 is the baseline
|
|
23
|
+
// and the only schema in existence today, so the registry ships empty: this
|
|
24
|
+
// module is the SEAM, proven by tests, that makes a future v2 a one-entry
|
|
25
|
+
// change instead of a migration crisis.
|
|
26
|
+
|
|
27
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
|
|
30
|
+
// Per-step, PER-OKF-TYPE transforms (owner 2026-07-26: each file class has its
|
|
31
|
+
// own shape in the contract's type registry, so each ladder step declares each
|
|
32
|
+
// class EXPLICITLY):
|
|
33
|
+
//
|
|
34
|
+
// LADDER[2] = {
|
|
35
|
+
// describe: "what v2 changes",
|
|
36
|
+
// rule: (content) => content2, // a real shape change for rules
|
|
37
|
+
// playbook: "unchanged", // EXPLICIT identity — visible, auditable
|
|
38
|
+
// improvement: (content) => …,
|
|
39
|
+
// }
|
|
40
|
+
//
|
|
41
|
+
// Only the three DURABLE knowledge classes ladder (rule · playbook ·
|
|
42
|
+
// improvement — the classes distillation judges and the brain keeps). Derived/
|
|
43
|
+
// generated files (coverage, checklist, ledger, reconciliation reports,
|
|
44
|
+
// manifest) are NEVER migrated — their producers regenerate them at the new
|
|
45
|
+
// schema. A step missing a class entry is a LOUD error, not implicit identity:
|
|
46
|
+
// "unchanged" must be declared, never assumed. Every applied step (identity
|
|
47
|
+
// included) re-stamps the note's own `schemaVersion:` line.
|
|
48
|
+
export const LADDER = {};
|
|
49
|
+
|
|
50
|
+
export const LADDER_CLASSES = ["rule", "playbook", "improvement"];
|
|
51
|
+
|
|
52
|
+
// Class resolution chain (owner 2026-07-26): DIRECTORY first — inside rafa's
|
|
53
|
+
// transport the path is governed identity (the CAS row key, the registry
|
|
54
|
+
// glob, the hydrate target; it cannot drift without becoming a different
|
|
55
|
+
// row) — then the PORTABLE filename suffix (`.rule.md` · `.playbook.md` ·
|
|
56
|
+
// `.improvement.md`), which travels WITH a file that leaves the canonical
|
|
57
|
+
// tree (foreign OKF bundles, out-of-context shares) while staying outside
|
|
58
|
+
// the corruptible content. When BOTH are present they must AGREE — a
|
|
59
|
+
// contradiction throws loudly, never a guess. Returns null for non-laddered
|
|
60
|
+
// paths (intent records, reports, …) — those pass through untouched.
|
|
61
|
+
const SUFFIX_CLASS = {
|
|
62
|
+
".rule.md": "rule",
|
|
63
|
+
".playbook.md": "playbook",
|
|
64
|
+
".improvement.md": "improvement",
|
|
65
|
+
};
|
|
66
|
+
export function classOf(path) {
|
|
67
|
+
const p = String(path).replace(/\\/g, "/");
|
|
68
|
+
const byDir = p.includes("brain/rules/")
|
|
69
|
+
? "rule"
|
|
70
|
+
: p.includes("brain/playbooks/")
|
|
71
|
+
? "playbook"
|
|
72
|
+
: p.includes("improve/improvements/")
|
|
73
|
+
? "improvement"
|
|
74
|
+
: null;
|
|
75
|
+
const bySuffix =
|
|
76
|
+
Object.entries(SUFFIX_CLASS).find(([s]) => p.endsWith(s))?.[1] ?? null;
|
|
77
|
+
if (byDir && bySuffix && byDir !== bySuffix)
|
|
78
|
+
throw new Error(
|
|
79
|
+
`schema ladder: directory class "${byDir}" contradicts filename class "${bySuffix}" (${p}) — ` +
|
|
80
|
+
"fix the file's home or its name; the ladder never guesses",
|
|
81
|
+
);
|
|
82
|
+
return byDir ?? bySuffix;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A note's OWN declared schema (first-fence `schemaVersion:`). Default 1 — the
|
|
86
|
+
// founding schema, same convention as stampedVersions defaulting to the first
|
|
87
|
+
// release (a pre-stamp file predates versioning, it isn't unversioned).
|
|
88
|
+
export function noteSchemaVersion(content) {
|
|
89
|
+
if (typeof content !== "string" || !content.startsWith("---")) return 1;
|
|
90
|
+
const end = content.indexOf("\n---", 3);
|
|
91
|
+
const fm = end === -1 ? content : content.slice(0, end);
|
|
92
|
+
const m = fm.match(/^schemaVersion:\s*(\d+)\s*$/m);
|
|
93
|
+
return m ? Number(m[1]) : 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const NOTE_DIRS = ["brain/rules", "brain/playbooks", "improve/improvements"];
|
|
97
|
+
|
|
98
|
+
export function walkBrainNotes(rafaDir) {
|
|
99
|
+
const out = [];
|
|
100
|
+
const walk = (dir) => {
|
|
101
|
+
if (!existsSync(dir)) return;
|
|
102
|
+
for (const e of readdirSync(dir)) {
|
|
103
|
+
const p = join(dir, e);
|
|
104
|
+
if (statSync(p).isDirectory()) walk(p);
|
|
105
|
+
else if (e.endsWith(".md") && !e.startsWith("_") && !e.endsWith(".theirs.md"))
|
|
106
|
+
out.push(p);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
for (const d of NOTE_DIRS) walk(join(rafaDir, d));
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The TARGET brain's schema: manifest.json first (the compiled truth), else
|
|
114
|
+
// the max over its notes' own declarations, else the founding schema.
|
|
115
|
+
export function brainSchemaVersion(rafaDir) {
|
|
116
|
+
try {
|
|
117
|
+
const m = JSON.parse(readFileSync(join(rafaDir, "manifest.json"), "utf8"));
|
|
118
|
+
if (Number.isInteger(m.schemaVersion)) return m.schemaVersion;
|
|
119
|
+
} catch {
|
|
120
|
+
/* no manifest — fall through to the notes */
|
|
121
|
+
}
|
|
122
|
+
const versions = walkBrainNotes(rafaDir).map((f) =>
|
|
123
|
+
noteSchemaVersion(readFileSync(f, "utf8")),
|
|
124
|
+
);
|
|
125
|
+
return versions.length ? Math.max(...versions) : 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The SOURCE side's schema: the max over the incoming candidates' own
|
|
129
|
+
// declarations (a working set may be mixed if it straddled an upgrade).
|
|
130
|
+
export function sourceSchemaVersion(contents) {
|
|
131
|
+
const versions = (contents ?? []).map(noteSchemaVersion);
|
|
132
|
+
return versions.length ? Math.max(...versions) : 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The lattice. Returns {mode, toVersion, rewriteTarget, upgradeSource, reason}.
|
|
136
|
+
export function resolveSchemaPlan({ target, source, latest }) {
|
|
137
|
+
if (target > latest || source > latest)
|
|
138
|
+
return {
|
|
139
|
+
mode: "abort-newer-than-runner",
|
|
140
|
+
toVersion: null,
|
|
141
|
+
rewriteTarget: false,
|
|
142
|
+
upgradeSource: false,
|
|
143
|
+
reason:
|
|
144
|
+
`brain schema v${Math.max(target, source)} is NEWER than this runner's v${latest} — ` +
|
|
145
|
+
"a newer CLI authored it. Update @rafinery/cli and re-run; knowledge is never downgraded.",
|
|
146
|
+
};
|
|
147
|
+
const base = { toVersion: latest, rewriteTarget: target < latest, upgradeSource: source < latest };
|
|
148
|
+
if (target < source)
|
|
149
|
+
return { mode: "rewrite-target", ...base, reason: `target v${target} < source v${source} — full target rewrite onto v${latest}` };
|
|
150
|
+
if (source < target)
|
|
151
|
+
return { mode: "upgrade-source", ...base, reason: `source v${source} < target v${target} — incoming candidates lift into v${latest}` };
|
|
152
|
+
if (target < latest)
|
|
153
|
+
return { mode: "rewrite-merged", ...base, reason: `both sides v${target} < runner v${latest} — merged result rewrites onto v${latest}` };
|
|
154
|
+
return { mode: "diff", ...base, reason: `both sides already v${latest} — plain diff reconcile` };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const stampVersion = (content, v) =>
|
|
158
|
+
content.replace(/^schemaVersion:\s*\d+\s*$/m, `schemaVersion: ${v}`);
|
|
159
|
+
|
|
160
|
+
// Lift ONE note of ONE class from its own declared version to `to`, one
|
|
161
|
+
// registered step at a time. A missing step OR a missing class entry within a
|
|
162
|
+
// step is a loud error, never a guess; the explicit "unchanged" is the only
|
|
163
|
+
// legal identity, and even it re-stamps the note's schemaVersion line.
|
|
164
|
+
export function applyLadder(content, to, cls) {
|
|
165
|
+
if (!LADDER_CLASSES.includes(cls))
|
|
166
|
+
throw new Error(
|
|
167
|
+
`schema ladder: "${cls}" is not a laddered class (${LADDER_CLASSES.join("|")}) — ` +
|
|
168
|
+
"derived/generated files regenerate at the new schema, they never migrate",
|
|
169
|
+
);
|
|
170
|
+
let from = noteSchemaVersion(content);
|
|
171
|
+
let out = content;
|
|
172
|
+
while (from < to) {
|
|
173
|
+
const step = LADDER[from + 1];
|
|
174
|
+
const t = step?.[cls];
|
|
175
|
+
if (t === undefined)
|
|
176
|
+
throw new Error(
|
|
177
|
+
`schema ladder: no registered ${cls} transform v${from}→v${from + 1} — every step ` +
|
|
178
|
+
'declares every class (a function, or the EXPLICIT "unchanged"); ' +
|
|
179
|
+
"update @rafinery/cli to a release that ships it (the ladder never guesses a schema)",
|
|
180
|
+
);
|
|
181
|
+
out = t === "unchanged" ? out : t(out);
|
|
182
|
+
out = stampVersion(out, from + 1);
|
|
183
|
+
from += 1;
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
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" +
|
package/lib/leverage/engine.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { adapters } from "./adapters/index.mjs";
|
|
9
|
+
import { listAgentSkills, neededSkills, skillsStamp } from "../skill-deps.mjs";
|
|
9
10
|
|
|
10
11
|
// Merge dependencies from the cwd package.json and any apps/*/package.json (monorepo).
|
|
11
12
|
function readDeps(cwd) {
|
|
@@ -66,17 +67,45 @@ export function repoContext(cwd) {
|
|
|
66
67
|
envFiles,
|
|
67
68
|
envGitignored: envFiles.length === 0 || envIgnored(cwd),
|
|
68
69
|
mcp: readMcp(cwd),
|
|
70
|
+
// The cross-harness toolbox (wave 6): `.agents/skills/` is harness-NEUTRAL
|
|
71
|
+
// (Claude Code · Codex · Cursor read the same SKILL.md convention; the
|
|
72
|
+
// vendored deps ship Codex manifests alongside). Every adapter sees the
|
|
73
|
+
// same inventory — never a per-toolchain re-read.
|
|
74
|
+
agentSkills: listAgentSkills(cwd),
|
|
75
|
+
skillDeps: {
|
|
76
|
+
needed: neededSkills(cwd).map((d) => ({ name: d.name, why: d.why })),
|
|
77
|
+
...skillsStamp(cwd),
|
|
78
|
+
},
|
|
69
79
|
};
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
const RANK = { P1: 0, P2: 1, P3: 2 };
|
|
73
83
|
|
|
84
|
+
// Engine-level tips — rafa's OWN dependency system, harness-neutral by
|
|
85
|
+
// definition (the fix is a rafa command, whatever the host toolchain).
|
|
86
|
+
function engineTips(ctx) {
|
|
87
|
+
const tips = [];
|
|
88
|
+
const missing = ctx.skillDeps?.needed ?? [];
|
|
89
|
+
if (missing.length) {
|
|
90
|
+
tips.push({
|
|
91
|
+
id: "skill-deps-missing",
|
|
92
|
+
priority: "P2",
|
|
93
|
+
title: `${missing.length} declared skill dependenc${missing.length === 1 ? "y" : "ies"} not installed`,
|
|
94
|
+
detail: missing.map((d) => `${d.name} — ${d.why}`).join(" · "),
|
|
95
|
+
fix: "run `rafa update --skills` (consent-gated; declined is remembered)",
|
|
96
|
+
tool: "rafa",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return tips;
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
// Run every detected adapter, tag tips with their tool, and rank by priority.
|
|
75
103
|
export function advise(cwd) {
|
|
76
104
|
const ctx = repoContext(cwd);
|
|
77
105
|
const active = adapters.filter((a) => a.detect(cwd));
|
|
78
|
-
const tips =
|
|
79
|
-
|
|
80
|
-
.
|
|
106
|
+
const tips = [
|
|
107
|
+
...engineTips(ctx),
|
|
108
|
+
...active.flatMap((a) => a.recommend(cwd, ctx).map((t) => ({ ...t, tool: a.name }))),
|
|
109
|
+
].sort((x, y) => (RANK[x.priority] ?? 9) - (RANK[y.priority] ?? 9));
|
|
81
110
|
return { ctx, detected: active.map((a) => a.name), tips };
|
|
82
111
|
}
|
package/lib/leverage.mjs
CHANGED
|
@@ -7,16 +7,25 @@ import { adapters, PLANNED } from "./leverage/adapters/index.mjs";
|
|
|
7
7
|
|
|
8
8
|
export default async function leverage(args = []) {
|
|
9
9
|
const cwd = process.cwd();
|
|
10
|
-
const { detected, tips } = advise(cwd);
|
|
10
|
+
const { ctx, detected, tips } = advise(cwd);
|
|
11
11
|
|
|
12
12
|
// --json: the DETERMINISTIC toolbox inventory (ratified 2026-07-12) — what
|
|
13
13
|
// the conductor loads at bootstrap and atlas consults during scan's toolbox
|
|
14
14
|
// pass. Machine truth: only what is actually installed, never inferred.
|
|
15
|
+
// `agentSkills` (wave 6) is the HARNESS-NEUTRAL `.agents/skills/` inventory
|
|
16
|
+
// — top-level, not under any one toolchain: Claude Code, Codex, and Cursor
|
|
17
|
+
// all read the same skill convention.
|
|
15
18
|
if (args.includes("--json")) {
|
|
16
19
|
const inventory = {};
|
|
17
20
|
for (const a of adapters)
|
|
18
21
|
if (a.detect(cwd)) inventory[a.id] = a.inspect(cwd);
|
|
19
|
-
console.log(
|
|
22
|
+
console.log(
|
|
23
|
+
JSON.stringify(
|
|
24
|
+
{ detected, agentSkills: ctx.agentSkills, skillDeps: ctx.skillDeps, inventory, tips },
|
|
25
|
+
null,
|
|
26
|
+
2,
|
|
27
|
+
),
|
|
28
|
+
);
|
|
20
29
|
return;
|
|
21
30
|
}
|
|
22
31
|
|