@rafinery/cli 0.8.1 → 0.8.3
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 +9 -0
- package/bin/rafa-distiller.mjs +13 -0
- package/bin/rafa.mjs +9 -8
- package/blueprint/.claude/agents/atlas.md +5 -1
- package/blueprint/.claude/agents/bloom.md +5 -1
- package/blueprint/.claude/agents/compass.md +5 -1
- package/blueprint/.claude/agents/prism.md +5 -1
- package/blueprint/.claude/agents/sage.md +5 -1
- package/blueprint/.claude/commands/rafa.md +20 -2
- package/blueprint/.claude/rafa/contract.md +31 -3
- package/blueprint/.claude/rafa/hooks/brain-commit.mjs +81 -0
- package/blueprint/.claude/rafa/hooks/brain-map.mjs +64 -0
- package/blueprint/.claude/rafa/hooks/brain-switch.mjs +48 -0
- package/blueprint/.claude/rafa/hooks/post-checkout +10 -0
- package/blueprint/.claude/rafa/hooks/post-commit +11 -0
- package/blueprint/.claude/rafa/hooks/post-rewrite +10 -0
- package/blueprint/.claude/rafa/hooks/pre-push +22 -0
- package/blueprint/.claude/rafa/hooks/session-start.mjs +93 -0
- package/blueprint/.claude/skills/rafa-improve/SKILL.md +5 -0
- package/blueprint/.claude/skills/rafa-scan/SKILL.md +1 -1
- package/blueprint/.claude/skills/rafa-validate/SKILL.md +6 -0
- package/lib/checkpoint.mjs +42 -0
- package/lib/ci-setup.mjs +54 -3
- package/lib/dirty.mjs +37 -0
- package/lib/distill.mjs +313 -37
- package/lib/distiller/doctrine.mjs +379 -0
- package/lib/distiller/state-plane.mjs +159 -0
- package/lib/distiller.mjs +410 -0
- package/lib/doctor.mjs +110 -0
- package/lib/gate/compile.mjs +31 -0
- package/lib/gate/verify-citations.mjs +9 -2
- package/lib/githook.mjs +75 -33
- package/lib/init.mjs +22 -23
- package/lib/mcp-client.mjs +22 -5
- package/lib/pull.mjs +2 -2
- package/lib/push.mjs +67 -2
- package/lib/releases.mjs +13 -0
- package/lib/sensor-health.mjs +91 -0
- package/lib/status.mjs +47 -1
- package/lib/update.mjs +2 -2
- package/lib/working-set.mjs +19 -1
- package/package.json +12 -10
- package/LICENSE +0 -21
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// (get_code_context batch, ≤ 2s, fail-soft to local counts). Never blocks a
|
|
13
13
|
// session: every path exits 0. Honors RAFA_HOOKS_DISABLED=1.
|
|
14
14
|
|
|
15
|
+
import { execSync } from "node:child_process";
|
|
15
16
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
16
17
|
import { homedir } from "node:os";
|
|
17
18
|
import { join } from "node:path";
|
|
@@ -45,6 +46,22 @@ function dirtyEntries(rafaDir) {
|
|
|
45
46
|
return [...files.keys()];
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// OPEN improvements whose cited files are dirty — fixed-in-passing candidates
|
|
50
|
+
// (the ledger lies until each is verified + report_improvement_status'd; this
|
|
51
|
+
// nag is deterministic code, not SOP adherence). null = no local manifest.
|
|
52
|
+
function openFixCandidates(rafaDir, files) {
|
|
53
|
+
const m = readJson(join(rafaDir, "manifest.json"));
|
|
54
|
+
if (!m || !Array.isArray(m.improvements)) return null;
|
|
55
|
+
const want = new Set(files);
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const i of m.improvements) {
|
|
58
|
+
if (i.status !== "open") continue;
|
|
59
|
+
const hits = [...new Set((i.cites ?? []).filter((c) => want.has(c.file)).map((c) => c.file))];
|
|
60
|
+
if (hits.length) out.push({ id: i.id, priority: i.priority });
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
// file → citing note ids, from the LOCAL manifest (a full pull / prior compile).
|
|
49
66
|
function citersFromManifest(rafaDir, files) {
|
|
50
67
|
const m = readJson(join(rafaDir, "manifest.json"));
|
|
@@ -149,6 +166,58 @@ function conflictCopies(rafaDir) {
|
|
|
149
166
|
return out;
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
// Capture-machinery self-check (P0, owner ask 2026-07-18): a session that
|
|
170
|
+
// starts — or resumes — on broken sensors must SAY so and hand the dev next
|
|
171
|
+
// steps, not silently lose its knowledge. Wiring + recorded failures only;
|
|
172
|
+
// never a network call inside a hook (doctor does the round-trip on demand).
|
|
173
|
+
function machineryProblems(root, rafaDir) {
|
|
174
|
+
const problems = [];
|
|
175
|
+
let settings = null;
|
|
176
|
+
try {
|
|
177
|
+
settings = JSON.parse(readFileSync(join(root, ".claude", "settings.json"), "utf8"));
|
|
178
|
+
} catch {
|
|
179
|
+
/* handled by the wired() checks below */
|
|
180
|
+
}
|
|
181
|
+
const wired = (event, script) =>
|
|
182
|
+
JSON.stringify(settings?.hooks?.[event] ?? []).includes(`rafa/hooks/${script}`) &&
|
|
183
|
+
existsSync(join(root, ".claude", "rafa", "hooks", script));
|
|
184
|
+
if (!wired("PostToolUse", "post-tool.mjs")) problems.push("dirty-marker unwired");
|
|
185
|
+
if (!wired("UserPromptSubmit", "user-prompt-submit.mjs")) problems.push("steering unwired");
|
|
186
|
+
let hooksDir = join(root, ".git", "hooks");
|
|
187
|
+
try {
|
|
188
|
+
// --git-path resolves worktrees correctly; plain .git/hooks is the fallback.
|
|
189
|
+
hooksDir = join(
|
|
190
|
+
root,
|
|
191
|
+
execSync("git rev-parse --git-path hooks", {
|
|
192
|
+
cwd: root,
|
|
193
|
+
encoding: "utf8",
|
|
194
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
195
|
+
}).trim(),
|
|
196
|
+
);
|
|
197
|
+
} catch {
|
|
198
|
+
/* plain .git/hooks fallback */
|
|
199
|
+
}
|
|
200
|
+
for (const h of ["pre-push", "post-commit", "post-checkout", "post-rewrite"]) {
|
|
201
|
+
try {
|
|
202
|
+
if (!readFileSync(join(hooksDir, h), "utf8").includes("rafa"))
|
|
203
|
+
problems.push(`git ${h} hook is foreign (chain line missing)`);
|
|
204
|
+
} catch {
|
|
205
|
+
problems.push(`git ${h} hook missing`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const rows = readFileSync(join(rafaDir, "sensor-errors.jsonl"), "utf8")
|
|
210
|
+
.split("\n")
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
const last = rows.length ? JSON.parse(rows[rows.length - 1]) : null;
|
|
213
|
+
if (last?.t && Date.now() - last.t < 48 * 3600 * 1000)
|
|
214
|
+
problems.push(`recent swallowed failure: ${last.error ?? last.hook}`);
|
|
215
|
+
} catch {
|
|
216
|
+
/* no recorded failures */
|
|
217
|
+
}
|
|
218
|
+
return problems;
|
|
219
|
+
}
|
|
220
|
+
|
|
152
221
|
try {
|
|
153
222
|
if (process.env.RAFA_HOOKS_DISABLED === "1") process.exit(0);
|
|
154
223
|
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
@@ -157,6 +226,16 @@ try {
|
|
|
157
226
|
|
|
158
227
|
const lines = [];
|
|
159
228
|
|
|
229
|
+
const machinery = machineryProblems(root, rafaDir);
|
|
230
|
+
if (machinery.length)
|
|
231
|
+
lines.push(
|
|
232
|
+
`[rafa · doctor] capture machinery is NOT fully live: ` +
|
|
233
|
+
machinery.slice(0, 4).join(" · ") +
|
|
234
|
+
(machinery.length > 4 ? ` (+${machinery.length - 4} more)` : "") +
|
|
235
|
+
` — run \`npx -y @rafinery/cli doctor\` FIRST and relay its named fixes to the dev as ` +
|
|
236
|
+
`concrete next steps; until fixed, this session's knowledge may be silently lost.`,
|
|
237
|
+
);
|
|
238
|
+
|
|
160
239
|
const dirty = dirtyEntries(rafaDir);
|
|
161
240
|
if (dirty.length) {
|
|
162
241
|
let byNote = citersFromManifest(rafaDir, dirty);
|
|
@@ -179,6 +258,20 @@ try {
|
|
|
179
258
|
);
|
|
180
259
|
}
|
|
181
260
|
|
|
261
|
+
if (dirty.length) {
|
|
262
|
+
const candidates = openFixCandidates(rafaDir, dirty);
|
|
263
|
+
if (candidates && candidates.length)
|
|
264
|
+
lines.push(
|
|
265
|
+
`[rafa · ledger] ${candidates.length} OPEN improvement(s) cite dirty files — possible fixed-in-passing: ` +
|
|
266
|
+
candidates
|
|
267
|
+
.slice(0, 6)
|
|
268
|
+
.map((c) => `${c.priority} ${c.id}`)
|
|
269
|
+
.join(" · ") +
|
|
270
|
+
(candidates.length > 6 ? " …" : "") +
|
|
271
|
+
` — verify each; fixed → report_improvement_status(id, fixed). An unreported fix leaves the ledger lying.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
182
275
|
const corrections = pendingCorrections(rafaDir);
|
|
183
276
|
if (corrections.length)
|
|
184
277
|
lines.push(
|
|
@@ -5,6 +5,11 @@ description: "rafa SOP — bloom's continuous-improvement pass: multi-lens, cite
|
|
|
5
5
|
|
|
6
6
|
# improve — continuous improvement (capability #2)
|
|
7
7
|
|
|
8
|
+
> **First worklist, every pass:** `list_improvements` and treat every `reported`
|
|
9
|
+
> overlay (a live session's fixed-in-passing signal) as a verification candidate —
|
|
10
|
+
> confirm against the code, update the row through the gates. A report stays
|
|
11
|
+
> pending until the ROW catches up; only this pass makes it converge.
|
|
12
|
+
|
|
8
13
|
rafa's second mission: **drive the codebase worst → well-managed, compounding via work
|
|
9
14
|
the dev is already doing.** A living, prioritized, cited **improvement ledger** — not a throwaway
|
|
10
15
|
report. Kept in `.rafa/improve/`, **never** in `.rafa/brain/` (knowledge and assessment
|
|
@@ -254,7 +254,7 @@ pipeline; **`--brain-only`** stops after the brain is validated (step 5 PASS)
|
|
|
254
254
|
spawn `bloom` → `.rafa/improve/`. It reads the *validated* brain as its index, so it only
|
|
255
255
|
runs after PASS.
|
|
256
256
|
7. **Push** — present the full summary (brain verdict/score + top improvements). **On the
|
|
257
|
-
dev's explicit approval**, `npx @rafinery/cli push` — commits `.rafa/` and pushes to the
|
|
257
|
+
dev's explicit approval**, `npx @rafinery/cli push --verb=scan` — commits `.rafa/` and pushes to the
|
|
258
258
|
brain remote using the dev's own git auth, stamped `brain-for: <code sha>`. Never without approval.
|
|
259
259
|
8. **The coach offer (founding scan only)** — if this was the repo's FIRST scan and the dev's
|
|
260
260
|
user brain is empty (`list_dev_insights` → none), offer ONCE: *"the code side is mapped —
|
|
@@ -31,6 +31,12 @@ agent. So:
|
|
|
31
31
|
|
|
32
32
|
## Procedure (run in order; ground everything in code)
|
|
33
33
|
|
|
34
|
+
0. **Prove the machinery first** — `npx -y @rafinery/cli doctor` (capture-engine P0). A
|
|
35
|
+
brain can only stay valid if capture is alive: sensors wired, hook scripts parse, the
|
|
36
|
+
heartbeat lands. Doctor failing is NOT a brain blocker (the brain may still be
|
|
37
|
+
faithful) — record it as a named finding in the checklist's health notes with
|
|
38
|
+
doctor's own fixes as the next steps; the dev must see a dead capture chain in the
|
|
39
|
+
report card, not discover it at the next empty distill.
|
|
34
40
|
1. **Re-run the checker yourself** — `npx @rafinery/cli verify-citations`. Record exit
|
|
35
41
|
code + counts (resolution / completeness / policy / **absence / inventory** — checker
|
|
36
42
|
v2 mechanizes the 2026-06-08 ratchet: declared `absent:` tokens re-grepped, coverage
|
package/lib/checkpoint.mjs
CHANGED
|
@@ -40,6 +40,16 @@ export default async function checkpoint() {
|
|
|
40
40
|
} catch {
|
|
41
41
|
die("not a git repo — run from your code repo root");
|
|
42
42
|
}
|
|
43
|
+
// P0 heartbeat — sensor liveness must reach the PLATFORM, not just a dev who
|
|
44
|
+
// thinks to ask (`rafa status`). Fires on every checkpoint, every branch,
|
|
45
|
+
// BEFORE the trunk guard; best-effort — telemetry never blocks the job.
|
|
46
|
+
try {
|
|
47
|
+
const { collectSensorHealth } = await import("./sensor-health.mjs");
|
|
48
|
+
await callTool(ROOT, "report_sensor_health", collectSensorHealth(ROOT));
|
|
49
|
+
} catch {
|
|
50
|
+
/* unprovisioned repo or platform unreachable — the checkpoint continues */
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
// The working set is BRANCH state. On the trunk, knowledge changes go
|
|
44
54
|
// through scan → compile → rafa push (the org brain's one writer surface).
|
|
45
55
|
if (branch === "main" || branch === "master") {
|
|
@@ -49,6 +59,21 @@ export default async function checkpoint() {
|
|
|
49
59
|
return;
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
// The commit this capture is grounded against (contract: capturedAtSha —
|
|
63
|
+
// absent = unknown grounding). One HEAD per checkpoint push; the merge-time
|
|
64
|
+
// ancestry sweep keys on it, so a missing sha only narrows that sweep,
|
|
65
|
+
// never blocks the checkpoint.
|
|
66
|
+
let capturedAtSha = "";
|
|
67
|
+
try {
|
|
68
|
+
capturedAtSha = execSync("git rev-parse HEAD", {
|
|
69
|
+
cwd: ROOT,
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
72
|
+
}).trim();
|
|
73
|
+
} catch {
|
|
74
|
+
/* unborn HEAD (fresh repo) — sync without grounding */
|
|
75
|
+
}
|
|
76
|
+
|
|
52
77
|
const sidecar = readSidecar(ROOT);
|
|
53
78
|
const syncBranch = (sidecar.sync[branch] = sidecar.sync[branch] ?? {});
|
|
54
79
|
|
|
@@ -76,6 +101,22 @@ export default async function checkpoint() {
|
|
|
76
101
|
|
|
77
102
|
if (candidates.length === 0) {
|
|
78
103
|
console.log(`✓ working set clean on ${branch} — nothing to checkpoint`);
|
|
104
|
+
// Knowledge-absence nudge (live-catch 2026-07-17: a session can do real
|
|
105
|
+
// work and bank ZERO knowledge, silently — no sensor fires on absence).
|
|
106
|
+
// Deterministic signal only: code edits recorded, no brain delta on this
|
|
107
|
+
// branch. Non-blocking, one line — the human decides if it's a real gap.
|
|
108
|
+
try {
|
|
109
|
+
const { readDirty } = await import("./dirty.mjs");
|
|
110
|
+
const dirtyCount = readDirty(ROOT).files.length;
|
|
111
|
+
if (dirtyCount > 0)
|
|
112
|
+
console.log(
|
|
113
|
+
`⚑ ${dirtyCount} code edit(s) recorded this cycle, ZERO brain files captured on ${branch} — ` +
|
|
114
|
+
`if decisions/gotchas/contracts were learned, they die with the session. ` +
|
|
115
|
+
`Worth a note? (notes, improvement files, and intent records all ride \`rafa checkpoint\`)`,
|
|
116
|
+
);
|
|
117
|
+
} catch {
|
|
118
|
+
/* nudge is best-effort — never block a checkpoint */
|
|
119
|
+
}
|
|
79
120
|
return;
|
|
80
121
|
}
|
|
81
122
|
|
|
@@ -84,6 +125,7 @@ export default async function checkpoint() {
|
|
|
84
125
|
try {
|
|
85
126
|
payload = await callTool(ROOT, "checkpoint_sync", {
|
|
86
127
|
branch,
|
|
128
|
+
...(capturedAtSha ? { capturedAtSha } : {}),
|
|
87
129
|
// WHO/WHAT executed — the CLI knows itself; the MODEL is the session's to
|
|
88
130
|
// report (RAFA_ACTOR_MODEL env, set by the conductor) — never guessed.
|
|
89
131
|
actorMeta: {
|
package/lib/ci-setup.mjs
CHANGED
|
@@ -23,9 +23,13 @@ const WORKFLOW = `# rafa reconcile — knowledge propagates exactly like the cod
|
|
|
23
23
|
# RAFA_BRAIN_TOKEN a token with push access to the BRAIN repo (distill only)
|
|
24
24
|
#
|
|
25
25
|
# Fires on the merge EVENT itself (PR closed+merged), so squash/rebase merges are
|
|
26
|
-
# detected correctly
|
|
27
|
-
#
|
|
28
|
-
#
|
|
26
|
+
# detected correctly. The distill job ALSO runs an ancestry sweep: any still-active
|
|
27
|
+
# working-set row (any branch) whose capturedAtSha is reachable from merged main is
|
|
28
|
+
# provably-merged backlog — an earlier merge whose distill never ran, or a direct
|
|
29
|
+
# push — and folds into the same run. The sweep needs full history (fetch-depth: 0).
|
|
30
|
+
# If distillation cannot run (missing secret, failed gate) it fails LOUD and the
|
|
31
|
+
# branch's working set stays intact — the next dev session gets the standard
|
|
32
|
+
# distill offer (fallback chain).
|
|
29
33
|
|
|
30
34
|
name: rafa reconcile
|
|
31
35
|
|
|
@@ -58,6 +62,7 @@ jobs:
|
|
|
58
62
|
- uses: actions/checkout@v4
|
|
59
63
|
with:
|
|
60
64
|
ref: \${{ github.event.repository.default_branch }} # merged main = the validation target
|
|
65
|
+
fetch-depth: 0 # full history — the ancestry sweep proves prior merges
|
|
61
66
|
- uses: actions/setup-node@v4
|
|
62
67
|
with:
|
|
63
68
|
node-version: 20
|
|
@@ -76,13 +81,52 @@ jobs:
|
|
|
76
81
|
RAFA_BRAIN_TOKEN: \${{ secrets.RAFA_BRAIN_TOKEN }}
|
|
77
82
|
RAFA_AGENT_SDK_DIR: \${{ runner.temp }}/rafa-sdk
|
|
78
83
|
RAFA_HOOKS_DISABLED: "1" # headless — the M5 sensors are session instruments
|
|
84
|
+
RAFA_MERGE_SHA: \${{ github.event.pull_request.merge_commit_sha }} # keys the run record
|
|
85
|
+
RAFA_TARGET_BRANCH: \${{ github.event.pull_request.base.ref }}
|
|
79
86
|
run: npx -y @rafinery/cli distill --headless "\${{ github.event.pull_request.head.ref }}"
|
|
80
87
|
`;
|
|
81
88
|
|
|
89
|
+
// Pristine-detection for upgrades: a byte-diff alone cannot tell "user tuned
|
|
90
|
+
// this" from "our template moved on" — and mislabeling a stale pristine
|
|
91
|
+
// install as tuned strands it on old behavior forever (the ancestry sweep is
|
|
92
|
+
// inert without fetch-depth: 0). Comparison ignores comments/blank lines;
|
|
93
|
+
// PREVIOUS_NORMALIZED holds every historical pristine body, so a file that
|
|
94
|
+
// matches one is upgraded in place and only a genuinely tuned file is left
|
|
95
|
+
// alone. (Comment-only tuning is knowingly not preserved.)
|
|
96
|
+
const normalize = (s) =>
|
|
97
|
+
s
|
|
98
|
+
.split("\n")
|
|
99
|
+
.map((l) => l.trimEnd())
|
|
100
|
+
.filter((l) => {
|
|
101
|
+
const t = l.trim();
|
|
102
|
+
return t !== "" && !t.startsWith("#");
|
|
103
|
+
})
|
|
104
|
+
.join("\n");
|
|
105
|
+
const PREVIOUS_NORMALIZED = [
|
|
106
|
+
// v1 (pre ancestry-sweep, pre run-record): identical body minus the lines
|
|
107
|
+
// those two features added. Derived, not duplicated — v1 never shipped a
|
|
108
|
+
// byte that differs otherwise.
|
|
109
|
+
normalize(WORKFLOW)
|
|
110
|
+
.split("\n")
|
|
111
|
+
.filter(
|
|
112
|
+
(l) =>
|
|
113
|
+
!l.includes("fetch-depth: 0") &&
|
|
114
|
+
!l.includes("RAFA_MERGE_SHA") &&
|
|
115
|
+
!l.includes("RAFA_TARGET_BRANCH"),
|
|
116
|
+
)
|
|
117
|
+
.join("\n"),
|
|
118
|
+
];
|
|
119
|
+
|
|
82
120
|
export default async function ciSetup(args = []) {
|
|
83
121
|
const ROOT = process.cwd();
|
|
84
122
|
const target = join(ROOT, WORKFLOW_PATH);
|
|
85
123
|
|
|
124
|
+
console.log(
|
|
125
|
+
"OPTIONAL — the platform reconciles merges automatically (no CI, no repo secrets).\n" +
|
|
126
|
+
"This wires the org-CI ADAPTER instead: reconciliation compute runs in YOUR\n" +
|
|
127
|
+
"GitHub Actions on YOUR secrets. Most teams should skip this.\n",
|
|
128
|
+
);
|
|
129
|
+
|
|
86
130
|
if (existsSync(target)) {
|
|
87
131
|
const current = readFileSync(target, "utf8");
|
|
88
132
|
if (current === WORKFLOW) {
|
|
@@ -90,6 +134,11 @@ export default async function ciSetup(args = []) {
|
|
|
90
134
|
} else if (args.includes("--overwrite")) {
|
|
91
135
|
writeFileSync(target, WORKFLOW);
|
|
92
136
|
console.log(`✓ ${WORKFLOW_PATH} updated (--overwrite)`);
|
|
137
|
+
} else if (PREVIOUS_NORMALIZED.includes(normalize(current))) {
|
|
138
|
+
writeFileSync(target, WORKFLOW);
|
|
139
|
+
console.log(
|
|
140
|
+
`✓ ${WORKFLOW_PATH} upgraded from an older pristine template (no local tuning detected)`,
|
|
141
|
+
);
|
|
93
142
|
} else {
|
|
94
143
|
console.log(
|
|
95
144
|
`! ${WORKFLOW_PATH} exists and differs — left untouched (yours may be tuned).\n` +
|
|
@@ -113,6 +162,8 @@ Next steps (repo Settings → Secrets and variables → Actions):
|
|
|
113
162
|
4. RAFA_BRAIN_TOKEN — a token with push access to the BRAIN repo (the distill
|
|
114
163
|
job pushes validated knowledge there).
|
|
115
164
|
5. Commit the workflow via your normal MR flow.
|
|
165
|
+
6. Verify the whole capture chain: npx @rafinery/cli doctor (sensors, scripts,
|
|
166
|
+
platform round-trip — exit 1 names any fix).
|
|
116
167
|
|
|
117
168
|
The rigor gradient this wires: dev↔dev = CAS + session prompt · branch↔branch =
|
|
118
169
|
free mechanical fold · branch→main = full distillation + gates. Cost tracks
|
package/lib/dirty.mjs
CHANGED
|
@@ -34,6 +34,31 @@ export function readDirty(root = process.cwd()) {
|
|
|
34
34
|
return { file, files: [...files.entries()].map(([f, t]) => ({ f, t })) };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// OPEN improvements whose cited files are dirty — the fixed-in-passing detector
|
|
38
|
+
// (live-catch 2026-07-17: a session hand-fixed five P2s and the ledger kept
|
|
39
|
+
// saying "open"; SOP text alone is the proven weak link, so the nag is CODE).
|
|
40
|
+
// A dirty file cited by an open improvement is a strong signal the fix may
|
|
41
|
+
// already exist — the session verifies and reports; the queue never guesses.
|
|
42
|
+
// null = no local manifest here (honest unknown, not zero).
|
|
43
|
+
export function resolveOpenFixCandidates(root, dirtyFiles) {
|
|
44
|
+
const path = join(root, ".rafa", "manifest.json");
|
|
45
|
+
if (!existsSync(path)) return null;
|
|
46
|
+
let m;
|
|
47
|
+
try {
|
|
48
|
+
m = JSON.parse(readFileSync(path, "utf8"));
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const want = new Set(dirtyFiles);
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const i of m.improvements ?? []) {
|
|
55
|
+
if (i.status !== "open") continue;
|
|
56
|
+
const files = [...new Set((i.cites ?? []).filter((c) => want.has(c.file)).map((c) => c.file))];
|
|
57
|
+
if (files.length) out.push({ id: i.id, priority: i.priority, title: i.title, files });
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
37
62
|
// file → citing notes from the local manifest; null = no manifest here (lazy .rafa).
|
|
38
63
|
export function resolveCiters(root, dirtyFiles) {
|
|
39
64
|
const path = join(root, ".rafa", "manifest.json");
|
|
@@ -72,6 +97,7 @@ export default async function dirty(args = []) {
|
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
const notes = resolveCiters(ROOT, files.map((e) => e.f));
|
|
100
|
+
const fixCandidates = resolveOpenFixCandidates(ROOT, files.map((e) => e.f));
|
|
75
101
|
const alarm =
|
|
76
102
|
files.length >= DIRTY_FILES_ALARM || (notes !== null && notes.length >= CITED_NOTES_ALARM);
|
|
77
103
|
|
|
@@ -81,6 +107,7 @@ export default async function dirty(args = []) {
|
|
|
81
107
|
{
|
|
82
108
|
files: files.map((e) => e.f),
|
|
83
109
|
notes, // null = unresolved here (no local manifest) — an honest unknown, not zero
|
|
110
|
+
openImprovements: fixCandidates, // dirty ∩ open-improvement cites — verify & report_improvement_status
|
|
84
111
|
thresholds: { files: DIRTY_FILES_ALARM, notes: CITED_NOTES_ALARM },
|
|
85
112
|
alarm,
|
|
86
113
|
},
|
|
@@ -106,6 +133,16 @@ export default async function dirty(args = []) {
|
|
|
106
133
|
console.log(`${notes.length} brain note(s) cite them:`);
|
|
107
134
|
for (const n of notes) console.log(` · ${n.id} ← ${n.files.join(", ")}`);
|
|
108
135
|
}
|
|
136
|
+
if (fixCandidates && fixCandidates.length) {
|
|
137
|
+
console.log(
|
|
138
|
+
`⚑ ${fixCandidates.length} OPEN improvement(s) cite files you changed — possible fixed-in-passing:`,
|
|
139
|
+
);
|
|
140
|
+
for (const c of fixCandidates)
|
|
141
|
+
console.log(` · ${c.priority} ${c.id} ← ${c.files.join(", ")}`);
|
|
142
|
+
console.log(
|
|
143
|
+
" verify each: fixed → `report_improvement_status(id, fixed)` (bloom updates the row at its next pass); still open → leave it. An unreported fix leaves the ledger lying.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
109
146
|
console.log(
|
|
110
147
|
alarm
|
|
111
148
|
? `⚠ drift past threshold — recommend a brain refresh from main (/rafa scan --brain-only), then \`rafa dirty --consume\`.`
|