@rafinery/cli 0.11.0 → 0.12.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/bin/rafa.mjs +10 -0
- package/blueprint/.claude/agents/atlas.md +2 -2
- package/blueprint/.claude/agents/bloom.md +3 -2
- package/blueprint/.claude/commands/rafa.md +1 -1
- package/blueprint/.claude/rafa/contract.md +10 -4
- package/blueprint/.claude/skills/rafa-build/SKILL.md +5 -1
- package/blueprint/.claude/skills/rafa-improve/SKILL.md +17 -6
- package/blueprint/.claude/skills/rafa-okf/SKILL.md +1 -1
- package/blueprint/.claude/skills/rafa-plan/SKILL.md +14 -1
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +9 -1
- package/blueprint/.claude/skills/rafa-security/SKILL.md +96 -0
- package/lib/audit/core.mjs +488 -0
- package/lib/audit.mjs +279 -0
- package/lib/blueprint.mjs +1 -0
- package/lib/distiller/doctrine.mjs +4 -4
- package/lib/doctor.mjs +16 -0
- package/lib/gate/compile.mjs +17 -13
- package/lib/init.mjs +3 -2
- package/lib/manifest.mjs +2 -1
- package/lib/mcp-client.mjs +3 -2
- package/lib/working-set.mjs +3 -2
- package/package.json +1 -1
- package/skills-bundle/skills-manifest.json +1 -1
package/lib/audit.mjs
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// rafa audit — the deterministic security engine (finding shape rafa.audit/v1).
|
|
2
|
+
//
|
|
3
|
+
// SELF-CONTAINED (owner, 2026-07-26: "rafa audit should already have what it
|
|
4
|
+
// needs — it should just run"): dependency CVEs via the built-in pnpm-lock v9
|
|
5
|
+
// parser + the free keyless OSV.dev API (merged with `pnpm audit --json` as a
|
|
6
|
+
// secondary source), secrets via the built-in curated ruleset over TRACKED
|
|
7
|
+
// files only (`.env*` never opened; fingerprints, never secret bytes). semgrep
|
|
8
|
+
// is the ONE optional external (SAST, pinned config) — used if present,
|
|
9
|
+
// honestly absent otherwise; gitleaks enhances the secrets tier the same way.
|
|
10
|
+
// NO binaries are installed by this command, ever.
|
|
11
|
+
//
|
|
12
|
+
// Exit codes: 0 clean · 1 findings present · 2 engine error.
|
|
13
|
+
|
|
14
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import {
|
|
20
|
+
AUDIT_SCHEMA,
|
|
21
|
+
SECRET_PATTERNS,
|
|
22
|
+
buildOsvQueries,
|
|
23
|
+
buildReport,
|
|
24
|
+
classifyReachability,
|
|
25
|
+
dedupeFindings,
|
|
26
|
+
entropyOf,
|
|
27
|
+
fromGitleaks,
|
|
28
|
+
fromOsvBatch,
|
|
29
|
+
fromPnpmAudit,
|
|
30
|
+
fromRegexSecrets,
|
|
31
|
+
fromSemgrepSarif,
|
|
32
|
+
mapConcurrent,
|
|
33
|
+
parsePnpmLock,
|
|
34
|
+
} from "./audit/core.mjs";
|
|
35
|
+
|
|
36
|
+
const OSV_BATCH = "https://api.osv.dev/v1/querybatch";
|
|
37
|
+
const OSV_VULN = "https://api.osv.dev/v1/vulns/";
|
|
38
|
+
const SEMGREP_CONFIG = "p/security-audit"; // pinned — never `--config auto` (it phones home to pick rules)
|
|
39
|
+
|
|
40
|
+
export const hasBinary = (name) => {
|
|
41
|
+
try {
|
|
42
|
+
execSync(`command -v ${name}`, { stdio: "ignore", shell: "/bin/sh" });
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
async function osvFetch(url, body) {
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
method: body ? "POST" : "GET",
|
|
52
|
+
headers: body ? { "content-type": "application/json" } : undefined,
|
|
53
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) throw new Error(`OSV ${res.status} on ${url}`);
|
|
56
|
+
return res.json();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Tier a — dependency CVEs. Built-in OSV primary; `pnpm audit` secondary.
|
|
60
|
+
async function dependencyTier(root) {
|
|
61
|
+
const lockPath = join(root, "pnpm-lock.yaml");
|
|
62
|
+
if (!existsSync(lockPath))
|
|
63
|
+
return { tier: { ran: false, tool: null, reason: "no pnpm-lock.yaml here" }, findings: [] };
|
|
64
|
+
|
|
65
|
+
const parsed = parsePnpmLock(readFileSync(lockPath, "utf8"));
|
|
66
|
+
const reachability = classifyReachability(parsed);
|
|
67
|
+
const findings = [];
|
|
68
|
+
let osvOk = false;
|
|
69
|
+
let reason = null;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const chunks = buildOsvQueries(parsed.packages);
|
|
73
|
+
const results = [];
|
|
74
|
+
for (const chunk of chunks) {
|
|
75
|
+
const res = await osvFetch(OSV_BATCH, { queries: chunk });
|
|
76
|
+
results.push(...(res.results ?? []));
|
|
77
|
+
}
|
|
78
|
+
const ids = [...new Set(results.flatMap((r) => (r?.vulns ?? []).map((v) => v.id)))];
|
|
79
|
+
const details = new Map();
|
|
80
|
+
await mapConcurrent(ids, 8, async (id) => {
|
|
81
|
+
try {
|
|
82
|
+
details.set(id, await osvFetch(OSV_VULN + id));
|
|
83
|
+
} catch {
|
|
84
|
+
/* a detail we cannot fetch yields a shallower finding, never a guessed one */
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
findings.push(...fromOsvBatch(results, parsed.packages, details, reachability));
|
|
88
|
+
osvOk = true;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
reason = `OSV unreachable (${e.message})`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Secondary source — pnpm audit builds its tree from the lockfile and asks
|
|
94
|
+
// the registry; it exits non-zero WHEN VULNS EXIST, so parse stdout
|
|
95
|
+
// regardless of exit code. Run without --prod: dev deps ride with dev:true
|
|
96
|
+
// and the priority map downgrades them — never silent omission.
|
|
97
|
+
let pnpmOk = false;
|
|
98
|
+
if (hasBinary("pnpm")) {
|
|
99
|
+
try {
|
|
100
|
+
let stdout;
|
|
101
|
+
try {
|
|
102
|
+
stdout = execSync("pnpm audit --json", { cwd: root, maxBuffer: 64 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
|
|
103
|
+
} catch (e) {
|
|
104
|
+
stdout = e.stdout; // non-zero exit = findings, not failure
|
|
105
|
+
}
|
|
106
|
+
const json = JSON.parse(String(stdout));
|
|
107
|
+
findings.push(...fromPnpmAudit(json, reachability));
|
|
108
|
+
pnpmOk = true;
|
|
109
|
+
} catch {
|
|
110
|
+
/* unparseable output = tool failure; OSV may still have run */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!osvOk && !pnpmOk)
|
|
115
|
+
return { tier: { ran: false, tool: null, reason: reason ?? "registry unreachable" }, findings: [] };
|
|
116
|
+
const tool = osvOk && pnpmOk ? "osv-api+pnpm-audit" : osvOk ? "osv-api" : "pnpm-audit";
|
|
117
|
+
return { tier: { ran: true, tool }, findings: dedupeFindings(findings) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isEnvFile = (p) => /(^|\/)\.env(\.|$)/.test(p);
|
|
121
|
+
const looksBinary = (buf) => buf.includes(0);
|
|
122
|
+
|
|
123
|
+
// Tier b — secrets over tracked files only. The matched text NEVER leaves the
|
|
124
|
+
// process: findings carry a sha256 fingerprint of (file:line:rule:match).
|
|
125
|
+
function secretsTier(root) {
|
|
126
|
+
let files;
|
|
127
|
+
try {
|
|
128
|
+
files = execSync("git ls-files", { cwd: root, maxBuffer: 64 * 1024 * 1024 })
|
|
129
|
+
.toString()
|
|
130
|
+
.split("\n")
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.filter((f) => !isEnvFile(f));
|
|
133
|
+
} catch {
|
|
134
|
+
return { tier: { ran: false, tool: null, reason: "not a git repository" }, findings: [] };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const rows = [];
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
const abs = join(root, file);
|
|
140
|
+
let buf;
|
|
141
|
+
try {
|
|
142
|
+
if (statSync(abs).size > 1024 * 1024) continue; // generated/vendored blobs
|
|
143
|
+
buf = readFileSync(abs);
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (looksBinary(buf)) continue;
|
|
148
|
+
const lines = buf.toString("utf8").split("\n");
|
|
149
|
+
for (let i = 0; i < lines.length; i++) {
|
|
150
|
+
for (const p of SECRET_PATTERNS) {
|
|
151
|
+
const m = lines[i].match(p.re);
|
|
152
|
+
if (!m) continue;
|
|
153
|
+
if (p.entropy && entropyOf(m[1] ?? m[0]) < p.entropy) continue;
|
|
154
|
+
rows.push({
|
|
155
|
+
rule: p.rule,
|
|
156
|
+
title: p.title,
|
|
157
|
+
severity: p.severity,
|
|
158
|
+
file,
|
|
159
|
+
line: i + 1,
|
|
160
|
+
fingerprint: createHash("sha256").update(`${file}:${i + 1}:${p.rule}:${m[0]}`).digest("hex").slice(0, 16),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
let findings = fromRegexSecrets(rows);
|
|
166
|
+
let tool = "regex-fallback";
|
|
167
|
+
|
|
168
|
+
// gitleaks enhancer — merged in when present, keyed off its own report file.
|
|
169
|
+
if (hasBinary("gitleaks")) {
|
|
170
|
+
const report = join(tmpdir(), `rafa-gitleaks-${process.pid}.json`);
|
|
171
|
+
try {
|
|
172
|
+
try {
|
|
173
|
+
execFileSync("gitleaks", ["detect", "--source", root, "--no-banner", "--report-format", "json", "--report-path", report], { stdio: "ignore" });
|
|
174
|
+
} catch {
|
|
175
|
+
/* gitleaks exits 1 when it finds leaks — the report still writes */
|
|
176
|
+
}
|
|
177
|
+
if (existsSync(report)) {
|
|
178
|
+
const rowsGl = JSON.parse(readFileSync(report, "utf8"));
|
|
179
|
+
findings = dedupeFindings([...findings, ...fromGitleaks(Array.isArray(rowsGl) ? rowsGl.filter((r) => !isEnvFile(r.File ?? "")) : [])]);
|
|
180
|
+
tool = "regex-fallback+gitleaks";
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
/* enhancer failure never fails the built-in tier */
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { tier: { ran: true, tool }, findings };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Tier c — SAST via semgrep, the one optional external.
|
|
190
|
+
function sastTier(root) {
|
|
191
|
+
if (!hasBinary("semgrep"))
|
|
192
|
+
return { tier: { ran: false, tool: null, reason: "semgrep not installed (optional — `brew install semgrep`)" }, findings: [] };
|
|
193
|
+
try {
|
|
194
|
+
let stdout;
|
|
195
|
+
try {
|
|
196
|
+
stdout = execFileSync("semgrep", ["scan", "--config", SEMGREP_CONFIG, "--sarif", "--quiet"], { cwd: root, maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] });
|
|
197
|
+
} catch (e) {
|
|
198
|
+
stdout = e.stdout; // semgrep exits non-zero on findings with some flags
|
|
199
|
+
}
|
|
200
|
+
const findings = fromSemgrepSarif(JSON.parse(String(stdout)));
|
|
201
|
+
return { tier: { ran: true, tool: "semgrep" }, findings };
|
|
202
|
+
} catch (e) {
|
|
203
|
+
return { tier: { ran: false, tool: "semgrep", reason: `semgrep failed (${e.message})` }, findings: [] };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const SEV_ICON = { critical: "✗", high: "✗", moderate: "~", low: "·", info: "·" };
|
|
208
|
+
|
|
209
|
+
function renderHuman(report) {
|
|
210
|
+
const lines = [`rafa audit — ${report.summary.total} finding(s)`, ""];
|
|
211
|
+
for (const [t, s] of Object.entries(report.tiers)) {
|
|
212
|
+
lines.push(s.ran ? ` ✓ ${t} ran (${s.tool})` : ` ○ ${t} did not run — ${s.reason}`);
|
|
213
|
+
}
|
|
214
|
+
lines.push("", ` critical ${report.summary.critical} · high ${report.summary.high} · moderate ${report.summary.moderate} · low ${report.summary.low} · info ${report.summary.info}`, "");
|
|
215
|
+
for (const f of report.findings.slice(0, 20)) {
|
|
216
|
+
const where = f.package ? `${f.package.name}@${f.package.version}${f.chain?.direct ? "" : " (transitive)"}${f.dev ? " [dev]" : ""}` : f.location ? `${f.location.file}:${f.location.line ?? "?"}` : "";
|
|
217
|
+
lines.push(` ${SEV_ICON[f.severity]} [${f.severity}] ${f.title}${where ? ` — ${where}` : ""}${f.fixedIn ? ` → fixed in ${f.fixedIn}` : ""}`);
|
|
218
|
+
}
|
|
219
|
+
if (report.findings.length > 20) lines.push(` … ${report.findings.length - 20} more in security-audit.json`);
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderRecord(report) {
|
|
224
|
+
const md = [
|
|
225
|
+
"---",
|
|
226
|
+
'type: "Security Audit"',
|
|
227
|
+
`title: Security audit — ${report.summary.total} finding(s)`,
|
|
228
|
+
`description: "critical ${report.summary.critical} · high ${report.summary.high} · moderate ${report.summary.moderate} · low ${report.summary.low}"`,
|
|
229
|
+
"---",
|
|
230
|
+
"",
|
|
231
|
+
"# Security audit",
|
|
232
|
+
"",
|
|
233
|
+
`Generated by \`rafa audit\` (${AUDIT_SCHEMA}). Machine record: \`security-audit.json\`.`,
|
|
234
|
+
"",
|
|
235
|
+
"| severity | tier | finding | subject | fixed in |",
|
|
236
|
+
"|---|---|---|---|---|",
|
|
237
|
+
...report.findings.map((f) => {
|
|
238
|
+
const subject = f.package ? `${f.package.name}@${f.package.version}` : f.location ? `${f.location.file}:${f.location.line ?? "?"}` : "—";
|
|
239
|
+
return `| ${f.severity} | ${f.tier} | ${f.vulnId ?? f.ruleId ?? f.id} | ${subject} | ${f.fixedIn ?? "—"} |`;
|
|
240
|
+
}),
|
|
241
|
+
"",
|
|
242
|
+
];
|
|
243
|
+
return md.join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function runAudit(args = []) {
|
|
247
|
+
const rootArg = args.find((a) => a.startsWith("--root="));
|
|
248
|
+
const root = rootArg ? rootArg.slice("--root=".length) : process.cwd();
|
|
249
|
+
const json = args.includes("--json");
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const [dep, sec, sast] = [await dependencyTier(root), secretsTier(root), sastTier(root)];
|
|
253
|
+
const report = buildReport({
|
|
254
|
+
at: new Date().toISOString(),
|
|
255
|
+
ref: null,
|
|
256
|
+
tiers: { dependency: dep.tier, secrets: sec.tier, sast: sast.tier },
|
|
257
|
+
findings: dedupeFindings([...dep.findings, ...sec.findings, ...sast.findings]),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Dual record beside the ledger (the verify-citations convention) — only
|
|
261
|
+
// when a .rafa/ bundle exists; a bare repo still gets the stdout report.
|
|
262
|
+
const improveDir = join(root, ".rafa", "improve");
|
|
263
|
+
if (existsSync(join(root, ".rafa"))) {
|
|
264
|
+
mkdirSync(improveDir, { recursive: true });
|
|
265
|
+
writeFileSync(join(improveDir, "security-audit.json"), JSON.stringify(report, null, 2) + "\n");
|
|
266
|
+
writeFileSync(join(improveDir, "security-audit.md"), renderRecord(report));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log(json ? JSON.stringify(report, null, 2) : renderHuman(report));
|
|
270
|
+
return report.summary.total > 0 ? 1 : 0;
|
|
271
|
+
} catch (e) {
|
|
272
|
+
console.error(`✗ rafa audit: ${e.message}`);
|
|
273
|
+
return 2;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default async function audit(args = []) {
|
|
278
|
+
process.exit(await runAudit(args));
|
|
279
|
+
}
|
package/lib/blueprint.mjs
CHANGED
|
@@ -46,6 +46,7 @@ export const BLUEPRINT = {
|
|
|
46
46
|
".claude/skills/rafa-leverage",
|
|
47
47
|
".claude/skills/rafa-sage",
|
|
48
48
|
".claude/skills/rafa-okf",
|
|
49
|
+
".claude/skills/rafa-security",
|
|
49
50
|
],
|
|
50
51
|
lockstep: [".claude/rafa/contract.md"],
|
|
51
52
|
// The M5 sensor hooks — deterministic protocol machinery (like the contract):
|
|
@@ -275,7 +275,7 @@ export function mergeRangeSweep({ changedFiles, orgNotes, cwd = process.cwd() })
|
|
|
275
275
|
|
|
276
276
|
// ── failure classification (case 3) ─────────────────────────────────────────────
|
|
277
277
|
// Deterministic (config / gate rejection / schema): retrying only burns spend →
|
|
278
|
-
//
|
|
278
|
+
// terminal-report failure class "deterministic" → needs-attention immediately.
|
|
279
279
|
// Transient (network / boot / OOM / clone / timeout): backoff retry while a later
|
|
280
280
|
// branch proceeds. An error may carry an explicit `errorClass`; else heuristics.
|
|
281
281
|
const TRANSIENT_RE =
|
|
@@ -288,7 +288,7 @@ export function classifyError(err) {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
// Tagged-error helpers so the loop can raise a failure of a known class and the
|
|
291
|
-
//
|
|
291
|
+
// terminal-report path (reconciliations.recordResult) carries it verbatim (never a re-guess at the boundary).
|
|
292
292
|
export function deterministicFailure(message) {
|
|
293
293
|
const e = new Error(message);
|
|
294
294
|
e.errorClass = "deterministic";
|
|
@@ -314,7 +314,7 @@ export function outcomeRow({ candidate, outcome, detail, reconciliationId, super
|
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
// The
|
|
317
|
+
// The terminal report’s `delta` shape (knowledgeGraph.deltaValidator): four buckets
|
|
318
318
|
// of { noteId, cites }. Built from the run's dispositions so the node the pointer
|
|
319
319
|
// advance mints carries exactly what this run refined. `folded` losers do not
|
|
320
320
|
// enter the delta (they were superseded, not banked) but ARE in the outcomes.
|
|
@@ -339,7 +339,7 @@ function toWireCites(cites = []) {
|
|
|
339
339
|
}));
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
// Per-branch outcome rows for
|
|
342
|
+
// Per-branch outcome rows for the terminal report (perBranchOutcomeValidator:
|
|
343
343
|
// { branch, capturedAtSha, verdict }). One row per source branch, the verdict a
|
|
344
344
|
// short mechanical summary of that branch's fate in the fold.
|
|
345
345
|
export function perBranchOutcomes(branchSets, outcomeRows) {
|
package/lib/doctor.mjs
CHANGED
|
@@ -97,6 +97,22 @@ export async function runDoctor(ROOT = process.cwd()) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// 3b · security tools — the audit engine's core (lockfile parser + OSV +
|
|
101
|
+
// secrets ruleset) is BUILT IN and always on; gitleaks/semgrep are optional
|
|
102
|
+
// enhancers. Detection only — rafa never installs binaries.
|
|
103
|
+
{
|
|
104
|
+
const { hasBinary } = await import("./audit.mjs");
|
|
105
|
+
console.log("• security tools (rafa audit)");
|
|
106
|
+
ok("core engine built in (dep CVEs via OSV · secrets ruleset)");
|
|
107
|
+
for (const [name, hint] of [
|
|
108
|
+
["gitleaks", "brew install gitleaks"],
|
|
109
|
+
["semgrep", "brew install semgrep"],
|
|
110
|
+
]) {
|
|
111
|
+
if (hasBinary(name)) ok(`${name} present (optional enhancer)`);
|
|
112
|
+
else console.log(` · ${name} not installed — optional enhancer (\`${hint}\`)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
// 4 · the round-trip — the heartbeat must LAND, not just send. A recorded
|
|
101
117
|
// response proves key + url + repo scoping + the platform handler, end to end.
|
|
102
118
|
console.log("• platform round-trip");
|
package/lib/gate/compile.mjs
CHANGED
|
@@ -268,22 +268,26 @@ export function runCompile(argv = []) {
|
|
|
268
268
|
"leverage",
|
|
269
269
|
"required · { impact, effort } ∈ low|medium|high",
|
|
270
270
|
);
|
|
271
|
+
// `category` is canonical; `lens` is the pre-2026-07-26 name, accepted
|
|
272
|
+
// as a deprecated alias so already-authored ledgers keep compiling.
|
|
273
|
+
const category = reqEnum(
|
|
274
|
+
{ category: data.category ?? data.lens },
|
|
275
|
+
"category",
|
|
276
|
+
[
|
|
277
|
+
"security",
|
|
278
|
+
"correctness",
|
|
279
|
+
"performance",
|
|
280
|
+
"architecture",
|
|
281
|
+
"product",
|
|
282
|
+
"ops",
|
|
283
|
+
],
|
|
284
|
+
path,
|
|
285
|
+
);
|
|
271
286
|
out.push({
|
|
272
287
|
id,
|
|
273
288
|
priority: reqEnum(data, "priority", ["P0", "P1", "P2", "P3"], path),
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"lens",
|
|
277
|
-
[
|
|
278
|
-
"security",
|
|
279
|
-
"correctness",
|
|
280
|
-
"performance",
|
|
281
|
-
"architecture",
|
|
282
|
-
"product",
|
|
283
|
-
"ops",
|
|
284
|
-
],
|
|
285
|
-
path,
|
|
286
|
-
),
|
|
289
|
+
category,
|
|
290
|
+
lens: category, // transitional dual-key — dropped with the backfill arc
|
|
287
291
|
status: reqEnum(
|
|
288
292
|
data,
|
|
289
293
|
"status",
|
package/lib/init.mjs
CHANGED
|
@@ -183,8 +183,9 @@ export default async function init(args) {
|
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
// Reconciliation runs on the PLATFORM (App-only era, owner call 2026-07-18
|
|
187
|
-
// merged PRs are detected and
|
|
186
|
+
// Reconciliation runs on the PLATFORM (App-only era, owner call 2026-07-18;
|
|
187
|
+
// agent-native since 2026-07-21): merged PRs are detected and reconciled by
|
|
188
|
+
// the server-side LangGraph agent — no
|
|
188
189
|
// CI workflow, no repo secrets, nothing to wire here. The org-CI adapter
|
|
189
190
|
// stays available as an explicit opt-out for teams that want the compute in
|
|
190
191
|
// their own CI: `rafa ci-setup`. Init no longer asks — one fewer question,
|
package/lib/manifest.mjs
CHANGED
|
@@ -165,7 +165,8 @@ export default async function manifest() {
|
|
|
165
165
|
return {
|
|
166
166
|
id,
|
|
167
167
|
priority: PRIORITIES.has(d.priority) ? d.priority : "P2",
|
|
168
|
-
|
|
168
|
+
category: str(d.category ?? d.lens, "general"),
|
|
169
|
+
lens: str(d.category ?? d.lens, "general"), // transitional mirror of category
|
|
169
170
|
status: IMP_STATUSES.has(d.status) ? d.status : "open",
|
|
170
171
|
title: str(d.title, id),
|
|
171
172
|
summary: str(d.summary),
|
package/lib/mcp-client.mjs
CHANGED
|
@@ -19,8 +19,9 @@ import { CLI_VERSION } from "./releases.mjs";
|
|
|
19
19
|
// The CLI's own actor envelope for state-plane writes (wave 5: REQUIRED on every
|
|
20
20
|
// loop event — strict, no legacy path). model is the explicit "mechanical"
|
|
21
21
|
// sentinel: no LLM ruled here, and the split stays grep-able for sage. The
|
|
22
|
-
// runner honors RAFA_ACTOR_RUNNER first (
|
|
23
|
-
//
|
|
22
|
+
// runner honors RAFA_ACTOR_RUNNER first (platform tiers export it —
|
|
23
|
+
// sandbox|ci|session; "sandbox" is a legacy wire value kept for historical
|
|
24
|
+
// rows), else CI env, else session.
|
|
24
25
|
export function cliActorMeta() {
|
|
25
26
|
const declared = process.env.RAFA_ACTOR_RUNNER;
|
|
26
27
|
return {
|
package/lib/working-set.mjs
CHANGED
|
@@ -160,15 +160,16 @@ export function materializeImprovement(cwd, data, brainForSha) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// Deterministic ledger-file writer for hydrated improvements — mirrors the shape
|
|
163
|
-
// bloom authors (see any improve/improvements/*.md): priority ·
|
|
163
|
+
// bloom authors (see any improve/improvements/*.md): priority · category · status ·
|
|
164
164
|
// fix · leverage · blast_radius · cites, then the prose body.
|
|
165
165
|
export function renderImprovement(d) {
|
|
166
|
+
const category = d.category ?? d.lens;
|
|
166
167
|
const lines = [
|
|
167
168
|
"---",
|
|
168
169
|
"schemaVersion: 1",
|
|
169
170
|
`id: ${d.id}`,
|
|
170
171
|
`priority: ${d.priority}`,
|
|
171
|
-
...(
|
|
172
|
+
...(category ? [`category: ${category}`] : []),
|
|
172
173
|
`status: ${d.status}`,
|
|
173
174
|
`title: ${q(d.title)}`,
|
|
174
175
|
...(d.summary ? [`summary: ${q(d.summary)}`] : []),
|
package/package.json
CHANGED