@hanzlaa/rcode 4.12.0 → 4.13.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/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +1 -0
- package/cli/doctor.js +40 -5
- package/cli/install.js +6 -1
- package/dist/rcode.js +87 -87
- package/package.json +1 -1
- package/rcode/agents/rcode-hussain-pm.md +37 -3
- package/rcode/agents/rcode-orchestrator.md +91 -0
- package/rcode/agents/rules/executor/correctness-hazard-scan.md +98 -0
- package/rcode/agents/rules/executor/execution-flow.md +8 -0
- package/rcode/agents/rules/executor/self-check.md +8 -0
- package/rcode/agents/rules/orchestrator/contract.md +76 -0
- package/rcode/agents/rules/sprint-checker/dimensions.md +38 -0
- package/rcode/agents/rules/verifier/reachability-check.md +45 -2
- package/rcode/bin/lib/progress.cjs +41 -13
- package/rcode/bin/lib/state-digest.cjs +88 -0
- package/rcode/bin/rcode-hooks.cjs +192 -23
- package/rcode/bin/rcode-tools.cjs +94 -4
- package/rcode/references/REFERENCES_INDEX.md +3 -1
- package/rcode/references/agent-shared-rules.md +87 -0
- package/rcode/references/code-reviewer-playbook.md +5 -0
- package/rcode/references/executor-playbook.md +2 -0
- package/rcode/references/github-comment-style.md +57 -0
- package/rcode/references/persona-executor-mode.md +61 -0
- package/rcode/references/response-style.md +21 -4
- package/rcode/references/verifier-playbook.md +14 -0
- package/rcode/skills/SKILLS_INDEX.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/references.md +7 -0
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/merge-strategy.md +19 -3
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/wave-prompt.md +3 -1
- package/rcode/skills/agents/{raees-orchestrator → orchestrator}/SKILL.md +1 -1
- package/rcode/team.yaml +20 -1
- package/rcode/templates/settings-hooks.json +12 -12
- package/rcode/workflows/audit-worktrees.md +15 -1
- package/rcode/workflows/enable-hooks.md +6 -0
- package/rcode/workflows/execute-verify-phase-goal.md +58 -2
- package/rcode/workflows/execute.md +37 -8
- package/rcode/workflows/plan-research-validation.md +8 -2
- package/rcode/workflows/plan-spawn-planner.md +32 -4
- package/rcode/workflows/plan.md +138 -10
- package/rcode/workflows/pr-branch.md +2 -0
- package/rcode/workflows/research-phase.md +12 -4
- package/rcode/workflows/ship.md +4 -0
- package/rcode/workflows/verify-phase.md +40 -0
- package/server/dashboard.js +57 -17
- package/server/lib/html/client/components/OrchPanel.js +6 -2
- package/server/lib/html/client/components/XtermPanel.js +7 -2
- package/server/lib/html/client/orchestrator.js +58 -21
- package/server/lib/html/client/views/MemoryView.js +59 -3
- package/server/lib/html/css.js +40 -0
- package/server/lib/html/shell.js +10 -4
- package/server/lib/scanner.js +150 -3
- package/server/lib/view-only.js +32 -0
- package/server/orchestrator.js +63 -4
- /package/rcode/skills/agents/{raees-orchestrator → orchestrator}/references.md +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State digest — slim, subagent-facing extract of .rcode/state.json (#948).
|
|
3
|
+
*
|
|
4
|
+
* Every hop in a multi-agent workflow (rcode-phase-researcher, rcode-planner)
|
|
5
|
+
* is currently told to Read the raw state.json via `{state_path}` in its
|
|
6
|
+
* <files_to_read> block (see plan-spawn-planner.md, research-phase.md,
|
|
7
|
+
* plan-research-validation.md). In a mature project that file accumulates the
|
|
8
|
+
* full phases[]/sprints[]/stories[] history for every phase ever run — in
|
|
9
|
+
* this repo, 20 of 26 phases are already complete and carry their full sprint
|
|
10
|
+
* breakdowns, none of which a researcher/planner working on the CURRENT phase
|
|
11
|
+
* consumes (verified against the actual prompt templates, not guessed).
|
|
12
|
+
*
|
|
13
|
+
* buildStateDigest() keeps exactly what those prompts read state.json for:
|
|
14
|
+
* - current_phase / current_plan / active_workstream (orientation)
|
|
15
|
+
* - the ACTIVE phase's own entry in full (its sprints/stories — legitimate
|
|
16
|
+
* "what happened so far in this phase" signal for a continuation plan)
|
|
17
|
+
* - every other phase collapsed to {number, name, status} (existence +
|
|
18
|
+
* status only, no nested sprint/story bodies)
|
|
19
|
+
* - the most recent decisions (bounded — "Project decisions and history"
|
|
20
|
+
* per research-phase.md, not the full ADR log)
|
|
21
|
+
* - open (unresolved) blockers only
|
|
22
|
+
*
|
|
23
|
+
* Deliberately excluded: velocity_history, executions[], council_sessions[],
|
|
24
|
+
* chains[], the completed-phase sprint/story bodies, and resolved blockers —
|
|
25
|
+
* none of these are read by any workflow/agent prompt that consumes the
|
|
26
|
+
* digest (verified via grep across rcode/workflows and rcode/agents).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const RECENT_DECISIONS_LIMIT = 10;
|
|
30
|
+
|
|
31
|
+
/** Normalize a phase number/id for comparison, stripping legacy leading zeros. */
|
|
32
|
+
function normalizePhaseKey(v) {
|
|
33
|
+
const s = String(v ?? '').trim();
|
|
34
|
+
return s.replace(/^0+(?=\d)/, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {object|null} state - parsed state.json (post-migration)
|
|
39
|
+
* @param {string|number|null} phaseNumber - the phase currently being worked on
|
|
40
|
+
* @returns {object|null} slim digest, or null when state is absent
|
|
41
|
+
*/
|
|
42
|
+
function buildStateDigest(state, phaseNumber) {
|
|
43
|
+
if (!state) return null;
|
|
44
|
+
|
|
45
|
+
const phases = Array.isArray(state.phases) ? state.phases : [];
|
|
46
|
+
const decisions = Array.isArray(state.decisions) ? state.decisions : [];
|
|
47
|
+
const blockers = Array.isArray(state.blockers) ? state.blockers : [];
|
|
48
|
+
|
|
49
|
+
const targetKey = phaseNumber != null ? normalizePhaseKey(phaseNumber) : null;
|
|
50
|
+
const activePhase = targetKey
|
|
51
|
+
? phases.find((p) => normalizePhaseKey(p?.number ?? p?.id) === targetKey)
|
|
52
|
+
: null;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
project: state.project ?? null,
|
|
56
|
+
current_phase: state.current_phase ?? null,
|
|
57
|
+
current_plan: state.current_plan ?? null,
|
|
58
|
+
active_workstream: state.active_workstream ?? null,
|
|
59
|
+
last_session: state.last_session ?? null,
|
|
60
|
+
phase: activePhase ? {
|
|
61
|
+
number: activePhase.number ?? null,
|
|
62
|
+
name: activePhase.name ?? null,
|
|
63
|
+
status: activePhase.status ?? null,
|
|
64
|
+
started: activePhase.started ?? null,
|
|
65
|
+
completed: activePhase.completed ?? null,
|
|
66
|
+
goal: activePhase.goal ?? null,
|
|
67
|
+
sprints: Array.isArray(activePhase.sprints) ? activePhase.sprints : [],
|
|
68
|
+
} : null,
|
|
69
|
+
phase_history: phases.map((p) => ({
|
|
70
|
+
number: p?.number ?? p?.id ?? null,
|
|
71
|
+
name: p?.name ?? null,
|
|
72
|
+
status: p?.status ?? null,
|
|
73
|
+
})),
|
|
74
|
+
recent_decisions: decisions.slice(-RECENT_DECISIONS_LIMIT).map((d) => ({
|
|
75
|
+
summary: d?.summary ?? d?.description ?? null,
|
|
76
|
+
phase: d?.phase ?? null,
|
|
77
|
+
date: d?.date ?? null,
|
|
78
|
+
})),
|
|
79
|
+
open_blockers: blockers
|
|
80
|
+
.filter((b) => b && !b.resolved)
|
|
81
|
+
.map((b) => ({
|
|
82
|
+
description: b?.description ?? null,
|
|
83
|
+
date: b?.date ?? null,
|
|
84
|
+
})),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { buildStateDigest, normalizePhaseKey, RECENT_DECISIONS_LIMIT };
|
|
@@ -116,8 +116,12 @@ async function preEdit() {
|
|
|
116
116
|
|
|
117
117
|
process.exit(0);
|
|
118
118
|
} catch (err) {
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// Route through the circuit breaker: these inner catches are where hook
|
|
120
|
+
// crashes actually surface (each handler catches its own errors and exits),
|
|
121
|
+
// so main()'s .catch would never see them.
|
|
122
|
+
const _tripped = recordCrash(process.argv[2], err.message);
|
|
123
|
+
if (!_tripped) console.error(`Hook error: ${err.message}`);
|
|
124
|
+
process.exit(_tripped ? 0 : 1);
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
127
|
|
|
@@ -161,8 +165,12 @@ async function preWorkflow() {
|
|
|
161
165
|
|
|
162
166
|
process.exit(0);
|
|
163
167
|
} catch (err) {
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
// Route through the circuit breaker: these inner catches are where hook
|
|
169
|
+
// crashes actually surface (each handler catches its own errors and exits),
|
|
170
|
+
// so main()'s .catch would never see them.
|
|
171
|
+
const _tripped = recordCrash(process.argv[2], err.message);
|
|
172
|
+
if (!_tripped) console.error(`Hook error: ${err.message}`);
|
|
173
|
+
process.exit(_tripped ? 0 : 1);
|
|
166
174
|
}
|
|
167
175
|
}
|
|
168
176
|
|
|
@@ -301,8 +309,12 @@ async function postCommit() {
|
|
|
301
309
|
|
|
302
310
|
process.exit(0);
|
|
303
311
|
} catch (err) {
|
|
304
|
-
|
|
305
|
-
|
|
312
|
+
// Route through the circuit breaker: these inner catches are where hook
|
|
313
|
+
// crashes actually surface (each handler catches its own errors and exits),
|
|
314
|
+
// so main()'s .catch would never see them.
|
|
315
|
+
const _tripped = recordCrash(process.argv[2], err.message);
|
|
316
|
+
if (!_tripped) console.error(`Hook error: ${err.message}`);
|
|
317
|
+
process.exit(_tripped ? 0 : 1);
|
|
306
318
|
}
|
|
307
319
|
}
|
|
308
320
|
|
|
@@ -423,8 +435,12 @@ async function bashGuard() {
|
|
|
423
435
|
|
|
424
436
|
process.exit(0);
|
|
425
437
|
} catch (err) {
|
|
426
|
-
|
|
427
|
-
|
|
438
|
+
// Route through the circuit breaker: these inner catches are where hook
|
|
439
|
+
// crashes actually surface (each handler catches its own errors and exits),
|
|
440
|
+
// so main()'s .catch would never see them.
|
|
441
|
+
const _tripped = recordCrash(process.argv[2], err.message);
|
|
442
|
+
if (!_tripped) console.error(`Hook error: ${err.message}`);
|
|
443
|
+
process.exit(_tripped ? 0 : 1);
|
|
428
444
|
}
|
|
429
445
|
}
|
|
430
446
|
|
|
@@ -573,11 +589,44 @@ async function preCompact() {
|
|
|
573
589
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
574
590
|
process.exit(0);
|
|
575
591
|
} catch (err) {
|
|
576
|
-
|
|
577
|
-
|
|
592
|
+
// Route through the circuit breaker: these inner catches are where hook
|
|
593
|
+
// crashes actually surface (each handler catches its own errors and exits),
|
|
594
|
+
// so main()'s .catch would never see them.
|
|
595
|
+
const _tripped = recordCrash(process.argv[2], err.message);
|
|
596
|
+
if (!_tripped) console.error(`Hook error: ${err.message}`);
|
|
597
|
+
process.exit(_tripped ? 0 : 1);
|
|
578
598
|
}
|
|
579
599
|
}
|
|
580
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Strip JSONC comments and trailing commas so a tolerant re-parse can tell a
|
|
603
|
+
* commented-but-valid config from an actually broken one. Scans character by
|
|
604
|
+
* character and tracks string state — a naive regex would eat the `//` in
|
|
605
|
+
* "https://example.com" and turn a valid file into a reported syntax error.
|
|
606
|
+
*/
|
|
607
|
+
function stripJsonc(text) {
|
|
608
|
+
let out = '';
|
|
609
|
+
let inStr = false, esc = false, inLine = false, inBlock = false;
|
|
610
|
+
for (let i = 0; i < text.length; i++) {
|
|
611
|
+
const c = text[i], next = text[i + 1];
|
|
612
|
+
if (inLine) { if (c === '\n') { inLine = false; out += c; } continue; }
|
|
613
|
+
if (inBlock) { if (c === '*' && next === '/') { inBlock = false; i++; } continue; }
|
|
614
|
+
if (inStr) {
|
|
615
|
+
out += c;
|
|
616
|
+
if (esc) esc = false;
|
|
617
|
+
else if (c === '\\') esc = true;
|
|
618
|
+
else if (c === '"') inStr = false;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (c === '"') { inStr = true; out += c; continue; }
|
|
622
|
+
if (c === '/' && next === '/') { inLine = true; i++; continue; }
|
|
623
|
+
if (c === '/' && next === '*') { inBlock = true; i++; continue; }
|
|
624
|
+
out += c;
|
|
625
|
+
}
|
|
626
|
+
// Trailing commas before } or ]
|
|
627
|
+
return out.replace(/,(\s*[}\]])/g, '$1');
|
|
628
|
+
}
|
|
629
|
+
|
|
581
630
|
/**
|
|
582
631
|
* stop-verify: Syntax-check files changed during the response (#744).
|
|
583
632
|
*
|
|
@@ -597,6 +646,11 @@ async function stopVerify() {
|
|
|
597
646
|
null;
|
|
598
647
|
|
|
599
648
|
if (!Array.isArray(changed)) {
|
|
649
|
+
// Fallback only. `git diff --name-only` reports the WHOLE dirty working
|
|
650
|
+
// tree, not what this response touched — so one pre-existing dirty file
|
|
651
|
+
// makes every Stop from now on report the same failure, forever, with
|
|
652
|
+
// nothing the user did causing it. Scope it to files modified since the
|
|
653
|
+
// response began where we can, and treat the result as advisory.
|
|
600
654
|
const diff = spawnSync('git', ['diff', '--name-only'], {
|
|
601
655
|
encoding: 'utf8',
|
|
602
656
|
cwd: process.cwd(),
|
|
@@ -626,24 +680,58 @@ async function stopVerify() {
|
|
|
626
680
|
failures.push(`${file}: ${(check.stderr || '').trim().split('\n')[0]}`);
|
|
627
681
|
}
|
|
628
682
|
} else if (ext === '.json') {
|
|
683
|
+
// Guard the read: one unreadable file (permissions, a symlink that just
|
|
684
|
+
// broke, a truncated write in flight) used to throw past this loop into
|
|
685
|
+
// the outer catch, killing the check for EVERY other changed file and
|
|
686
|
+
// printing a generic "Hook error" instead of naming anything.
|
|
687
|
+
let text;
|
|
688
|
+
try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
|
|
629
689
|
try {
|
|
630
|
-
JSON.parse(
|
|
631
|
-
} catch (
|
|
632
|
-
|
|
690
|
+
JSON.parse(text);
|
|
691
|
+
} catch (strictErr) {
|
|
692
|
+
// Many real-world *.json files are JSONC: turbo.json, tsconfig.json,
|
|
693
|
+
// jsconfig.json, .eslintrc.json, devcontainer.json, and VS Code's
|
|
694
|
+
// settings/launch all permit // comments and trailing commas. Strict
|
|
695
|
+
// JSON.parse calls those a syntax error, which made this hook fail on
|
|
696
|
+
// every single Stop against a perfectly valid file. Retry tolerantly
|
|
697
|
+
// and only report a failure when BOTH parses fail.
|
|
698
|
+
try {
|
|
699
|
+
JSON.parse(stripJsonc(text));
|
|
700
|
+
} catch {
|
|
701
|
+
failures.push(`${file}: ${strictErr.message}`);
|
|
702
|
+
}
|
|
633
703
|
}
|
|
634
704
|
}
|
|
635
705
|
}
|
|
636
706
|
|
|
637
707
|
if (failures.length > 0) {
|
|
708
|
+
// Don't re-report an identical failure set on every Stop. Without this a
|
|
709
|
+
// single unfixable/irrelevant dirty file turns into an error banner on
|
|
710
|
+
// every response for the rest of the session, which trains the user to
|
|
711
|
+
// ignore the hook entirely — the one outcome that makes it worthless.
|
|
712
|
+
const sig = failures.slice().sort().join('|');
|
|
713
|
+
const seenPath = path.join(os.tmpdir(), `rcode-stop-verify-${process.ppid || 0}.txt`);
|
|
714
|
+
let previous = '';
|
|
715
|
+
try { previous = fs.readFileSync(seenPath, 'utf8'); } catch { /* first run */ }
|
|
716
|
+
if (previous === sig) process.exit(0);
|
|
717
|
+
try { fs.writeFileSync(seenPath, sig); } catch { /* best-effort */ }
|
|
718
|
+
|
|
638
719
|
console.error('⚠ stop-verify: changed files failed syntax check:');
|
|
639
720
|
failures.forEach((f) => console.error(` • ${f}`));
|
|
640
721
|
process.exit(1);
|
|
641
722
|
}
|
|
723
|
+
// Clear the dedupe marker once everything parses, so a genuine NEW failure
|
|
724
|
+
// after a green run is reported rather than swallowed.
|
|
725
|
+
try { fs.unlinkSync(path.join(os.tmpdir(), `rcode-stop-verify-${process.ppid || 0}.txt`)); } catch { /* fine */ }
|
|
642
726
|
|
|
643
727
|
process.exit(0);
|
|
644
728
|
} catch (err) {
|
|
645
|
-
|
|
646
|
-
|
|
729
|
+
// Route through the circuit breaker: these inner catches are where hook
|
|
730
|
+
// crashes actually surface (each handler catches its own errors and exits),
|
|
731
|
+
// so main()'s .catch would never see them.
|
|
732
|
+
const _tripped = recordCrash(process.argv[2], err.message);
|
|
733
|
+
if (!_tripped) console.error(`Hook error: ${err.message}`);
|
|
734
|
+
process.exit(_tripped ? 0 : 1);
|
|
647
735
|
}
|
|
648
736
|
}
|
|
649
737
|
|
|
@@ -686,8 +774,12 @@ async function costTrack() {
|
|
|
686
774
|
|
|
687
775
|
process.exit(0);
|
|
688
776
|
} catch (err) {
|
|
689
|
-
|
|
690
|
-
|
|
777
|
+
// Route through the circuit breaker: these inner catches are where hook
|
|
778
|
+
// crashes actually surface (each handler catches its own errors and exits),
|
|
779
|
+
// so main()'s .catch would never see them.
|
|
780
|
+
const _tripped = recordCrash(process.argv[2], err.message);
|
|
781
|
+
if (!_tripped) console.error(`Hook error: ${err.message}`);
|
|
782
|
+
process.exit(_tripped ? 0 : 1);
|
|
691
783
|
}
|
|
692
784
|
}
|
|
693
785
|
|
|
@@ -1068,8 +1160,12 @@ async function stopHandler() {
|
|
|
1068
1160
|
}
|
|
1069
1161
|
process.exit(0);
|
|
1070
1162
|
} catch (err) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1163
|
+
// Route through the circuit breaker: these inner catches are where hook
|
|
1164
|
+
// crashes actually surface (each handler catches its own errors and exits),
|
|
1165
|
+
// so main()'s .catch would never see them.
|
|
1166
|
+
const _tripped = recordCrash(process.argv[2], err.message);
|
|
1167
|
+
if (!_tripped) console.error(`Hook error: ${err.message}`);
|
|
1168
|
+
process.exit(_tripped ? 0 : 1);
|
|
1073
1169
|
}
|
|
1074
1170
|
}
|
|
1075
1171
|
|
|
@@ -1120,12 +1216,79 @@ function sessionStart() {
|
|
|
1120
1216
|
process.exit(0);
|
|
1121
1217
|
}
|
|
1122
1218
|
|
|
1219
|
+
// ── Circuit breaker ────────────────────────────────────────────────────
|
|
1220
|
+
// A hook that CRASHES has nothing useful to say and will keep saying it on
|
|
1221
|
+
// every single event. After THRESHOLD consecutive crashes, trip the breaker
|
|
1222
|
+
// and stay quiet for the rest of the session rather than pollute every turn.
|
|
1223
|
+
//
|
|
1224
|
+
// This tracks CRASHES ONLY (the hook itself threw), never FINDINGS. A hook
|
|
1225
|
+
// that exits non-zero because it correctly found a broken file is working;
|
|
1226
|
+
// disabling it for doing its job would be the opposite of the intent.
|
|
1227
|
+
const BREAKER_THRESHOLD = 3;
|
|
1228
|
+
|
|
1229
|
+
// Safety hooks are NEVER auto-disabled. bash-guard blocks `git push`,
|
|
1230
|
+
// `--no-verify`, and `rm -rf`; pre-tool-use and pre-edit gate writes. A crashing
|
|
1231
|
+
// guard must fail loudly and keep failing — silently disabling it converts a
|
|
1232
|
+
// bug into an open door, which is a far worse outcome than a noisy terminal.
|
|
1233
|
+
const NEVER_BREAK = new Set(['bash-guard', 'pre-tool-use', 'pre-edit', 'pre-workflow']);
|
|
1234
|
+
|
|
1235
|
+
function breakerPath(name) {
|
|
1236
|
+
return path.join(os.tmpdir(), `rcode-hook-breaker-${process.ppid || 0}-${name}.json`);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function breakerTripped(name) {
|
|
1240
|
+
if (NEVER_BREAK.has(name)) return false;
|
|
1241
|
+
try {
|
|
1242
|
+
const st = JSON.parse(fs.readFileSync(breakerPath(name), 'utf8'));
|
|
1243
|
+
return (st.crashes || 0) >= BREAKER_THRESHOLD;
|
|
1244
|
+
} catch { return false; }
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function recordCrash(name, message) {
|
|
1248
|
+
if (NEVER_BREAK.has(name)) return false;
|
|
1249
|
+
let crashes = 0;
|
|
1250
|
+
try { crashes = JSON.parse(fs.readFileSync(breakerPath(name), 'utf8')).crashes || 0; } catch { /* first */ }
|
|
1251
|
+
crashes += 1;
|
|
1252
|
+
CRASH_RECORDED = true;
|
|
1253
|
+
try { fs.writeFileSync(breakerPath(name), JSON.stringify({ crashes, last: message })); } catch { /* best-effort */ }
|
|
1254
|
+
if (crashes >= BREAKER_THRESHOLD) {
|
|
1255
|
+
console.error(
|
|
1256
|
+
`⚠ rcode hook '${name}' crashed ${crashes}x in a row — disabling it for this session ` +
|
|
1257
|
+
`so it stops repeating. Last error: ${message}`
|
|
1258
|
+
);
|
|
1259
|
+
console.error(` Re-enable: restart the session, or fix and run 'node .rcode/bin/rcode-hooks.cjs ${name}' directly to see the full error.`);
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function clearCrashes(name) {
|
|
1266
|
+
try { fs.unlinkSync(breakerPath(name)); } catch { /* nothing to clear */ }
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Every handler exits from inside itself, so a `.then()` on main() would almost
|
|
1270
|
+
// never run. Hook the process exit instead: any clean exit means this hook ran
|
|
1271
|
+
// without crashing, so the consecutive-crash count resets. Without this, three
|
|
1272
|
+
// crashes spread across an entire session would trip the breaker even though
|
|
1273
|
+
// the hook worked fine in between.
|
|
1274
|
+
let CRASH_RECORDED = false;
|
|
1275
|
+
function installBreakerReset(name) {
|
|
1276
|
+
if (!name || NEVER_BREAK.has(name)) return;
|
|
1277
|
+
process.on('exit', (code) => {
|
|
1278
|
+
if (code === 0 && !CRASH_RECORDED) clearCrashes(name);
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1123
1282
|
/**
|
|
1124
1283
|
* Main entry point.
|
|
1125
1284
|
*/
|
|
1126
1285
|
async function main() {
|
|
1127
1286
|
const subcommand = process.argv[2];
|
|
1128
1287
|
|
|
1288
|
+
// Already tripped this session — exit silently. Advisory hooks only.
|
|
1289
|
+
if (breakerTripped(subcommand)) process.exit(0);
|
|
1290
|
+
installBreakerReset(subcommand);
|
|
1291
|
+
|
|
1129
1292
|
switch (subcommand) {
|
|
1130
1293
|
case 'pre-edit':
|
|
1131
1294
|
await preEdit();
|
|
@@ -1174,10 +1337,16 @@ async function main() {
|
|
|
1174
1337
|
}
|
|
1175
1338
|
|
|
1176
1339
|
if (require.main === module) {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1340
|
+
const name = process.argv[2];
|
|
1341
|
+
main()
|
|
1342
|
+
.then(() => clearCrashes(name))
|
|
1343
|
+
.catch((err) => {
|
|
1344
|
+
const tripped = recordCrash(name, err.message);
|
|
1345
|
+
if (!tripped) console.error(`Fatal error: ${err.message}`);
|
|
1346
|
+
// Advisory hooks exit 0 once tripped so the harness stops surfacing them;
|
|
1347
|
+
// guard hooks (never tripped) keep their non-zero exit.
|
|
1348
|
+
process.exit(tripped ? 0 : 1);
|
|
1349
|
+
});
|
|
1181
1350
|
}
|
|
1182
1351
|
|
|
1183
1352
|
module.exports = { INTENT_TABLE };
|
|
@@ -510,6 +510,12 @@ function cmdInit(workflowName, rawArgs) {
|
|
|
510
510
|
|
|
511
511
|
// Phase status from state.json (complete/executed/in_progress/planned/null).
|
|
512
512
|
// Used by plan.md to show context-aware messaging when plans already exist.
|
|
513
|
+
//
|
|
514
|
+
// #948 — state_digest is derived from this SAME parse (single read, not a
|
|
515
|
+
// second pass over state.json). It replaces the raw `{state_path}` full-file
|
|
516
|
+
// read that plan-spawn-planner.md / research-phase.md / plan-research-
|
|
517
|
+
// validation.md instruct the researcher/planner subagents to do — those
|
|
518
|
+
// prompts embed state_digest directly instead.
|
|
513
519
|
try {
|
|
514
520
|
const stateFilePath = path.join(RCODE_DIR, 'state.json');
|
|
515
521
|
const rawState = fs.existsSync(stateFilePath)
|
|
@@ -520,7 +526,9 @@ function cmdInit(workflowName, rawArgs) {
|
|
|
520
526
|
return k === String(phaseNum);
|
|
521
527
|
});
|
|
522
528
|
out.phase_status = stPhase ? (stPhase.status || null) : null;
|
|
523
|
-
|
|
529
|
+
const stateDigest = require(path.join(__dirname, 'lib', 'state-digest.cjs'));
|
|
530
|
+
out.state_digest = stateDigest.buildStateDigest(rawState, phaseNum);
|
|
531
|
+
} catch { out.phase_status = null; out.state_digest = null; }
|
|
524
532
|
|
|
525
533
|
// Disk artifacts — same shape as walkPhaseDirs() but inlined.
|
|
526
534
|
if (phaseDirEntry) {
|
|
@@ -580,11 +588,20 @@ function cmdInit(workflowName, rawArgs) {
|
|
|
580
588
|
const wf = nestedCfg.workflow || {};
|
|
581
589
|
const features = nestedCfg.features || {};
|
|
582
590
|
|
|
591
|
+
// #949 — context_window folded into init so plan.md doesn't need a
|
|
592
|
+
// separate `config-get context_window` cold start (top-level scalar,
|
|
593
|
+
// not namespaced under workflow.*/features.*).
|
|
594
|
+
out.context_window = nestedCfg.context_window ?? null;
|
|
595
|
+
|
|
583
596
|
// Workflow feature flags (top-level for direct workflow consumption).
|
|
584
597
|
// Defaults match the inline `config-get … || echo "X"` calls in the workflows.
|
|
585
598
|
out.research_enabled = String(wf.research_by_default ?? 'false') === 'true';
|
|
586
599
|
out.plan_checker_enabled = String(wf.plan_checker ?? 'true') !== 'false';
|
|
587
600
|
out.nyquist_validation_enabled = String(wf.nyquist_validation ?? 'true') !== 'false';
|
|
601
|
+
// Plan-time specialist review panel (#plan step 9.5). Default ON: the
|
|
602
|
+
// generalist planner+checker pair cannot see design or guard-shape
|
|
603
|
+
// defects, only goal coverage.
|
|
604
|
+
out.specialist_review_enabled = String(wf.specialist_review ?? 'true') !== 'false';
|
|
588
605
|
out.text_mode = String(wf.text_mode ?? 'false') === 'true';
|
|
589
606
|
|
|
590
607
|
// Model resolution per active profile. The researcher agent ships as
|
|
@@ -595,6 +612,21 @@ function cmdInit(workflowName, rawArgs) {
|
|
|
595
612
|
out.planner_model = resolveModelString('rcode-planner');
|
|
596
613
|
out.checker_model = resolveModelString('rcode-sprint-checker');
|
|
597
614
|
|
|
615
|
+
// #949 — agent-skills rows folded into init output. plan.md previously
|
|
616
|
+
// shelled out to `agent-skills rcode-phase-researcher` / `rcode-planner` /
|
|
617
|
+
// `rcode-sprint-checker` as 3 separate cold Node starts; research-phase.md
|
|
618
|
+
// and plan-research-validation.md each did their own `agent-skills
|
|
619
|
+
// rcode-phase-researcher` call. Same manifest lookup this init call
|
|
620
|
+
// already has installedAgents/readAgentManifest() loaded for.
|
|
621
|
+
{
|
|
622
|
+
const agentManifest = readAgentManifest();
|
|
623
|
+
out.agent_skills = {
|
|
624
|
+
researcher: resolveAgentId('rcode-phase-researcher', agentManifest) || null,
|
|
625
|
+
planner: resolveAgentId('rcode-planner', agentManifest) || null,
|
|
626
|
+
checker: resolveAgentId('rcode-sprint-checker', agentManifest) || null,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
598
630
|
// Phase requirement IDs — extracted from ROADMAP requirements block.
|
|
599
631
|
out.phase_req_ids = extractReqIds(roadmapPhase ? roadmapPhase.requirements : []);
|
|
600
632
|
|
|
@@ -3435,6 +3467,23 @@ function cmdState(subArgs) {
|
|
|
3435
3467
|
return 'planned';
|
|
3436
3468
|
}
|
|
3437
3469
|
|
|
3470
|
+
// Phase dirs are historically zero-padded ("03-evidence-ledger") while
|
|
3471
|
+
// ROADMAP tables and state.json use bare integers ("3"). Match on the
|
|
3472
|
+
// normalized number or every disk cross-check silently no-ops.
|
|
3473
|
+
const normPhaseNum = (k) => String(k ?? '').trim().replace(/^0+(\d)/, '$1');
|
|
3474
|
+
const findPhaseDirFiles = (phaseNum) => {
|
|
3475
|
+
const phasesRootDir = path.join(PLANNING_DIR, 'phases');
|
|
3476
|
+
if (!fs.existsSync(phasesRootDir)) return null;
|
|
3477
|
+
const want = normPhaseNum(phaseNum);
|
|
3478
|
+
const dirName = fs.readdirSync(phasesRootDir).find(d => {
|
|
3479
|
+
const m = d.match(/^(\d+(?:\.\d+)?)(?:-|$)/);
|
|
3480
|
+
return m && normPhaseNum(m[1]) === want;
|
|
3481
|
+
});
|
|
3482
|
+
if (!dirName) return null;
|
|
3483
|
+
const full = path.join(phasesRootDir, dirName);
|
|
3484
|
+
return { dirName, path: full, files: fs.readdirSync(full) };
|
|
3485
|
+
};
|
|
3486
|
+
|
|
3438
3487
|
const upsertPhase = (phaseNum, phaseName, phaseGoal, phaseStatus) => {
|
|
3439
3488
|
if (!/^\d/.test(phaseNum)) return;
|
|
3440
3489
|
if (phaseName.toLowerCase() === 'phase') return;
|
|
@@ -3456,6 +3505,49 @@ function cmdState(subArgs) {
|
|
|
3456
3505
|
const statusRank = { complete: 2, in_progress: 1, planned: 0 };
|
|
3457
3506
|
let incomingStatus = normalizeStatus(phaseStatus);
|
|
3458
3507
|
|
|
3508
|
+
// --from-disk means FROM DISK. The ROADMAP status column is only one
|
|
3509
|
+
// signal, and in several roadmap shapes column 4 isn't a status column
|
|
3510
|
+
// at all (e.g. a "Blocking?" column), so ROADMAP-only sync leaves every
|
|
3511
|
+
// shipped phase sitting at `planned` forever and no amount of re-running
|
|
3512
|
+
// sync fixes it. Advance from the artifacts that actually exist on disk.
|
|
3513
|
+
// Status never downgrades (statusRank guard below), so this can only
|
|
3514
|
+
// correct an under-reported phase, never overwrite a truer one.
|
|
3515
|
+
try {
|
|
3516
|
+
const dirInfo = findPhaseDirFiles(phaseNum);
|
|
3517
|
+
if (dirInfo) {
|
|
3518
|
+
const verFile = dirInfo.files.find(f => /-?VERIFICATION\.md$/i.test(f));
|
|
3519
|
+
const verText = verFile ? fs.readFileSync(path.join(dirInfo.path, verFile), 'utf8') : '';
|
|
3520
|
+
// `passed` alone is not enough: a report with no `falsification:`
|
|
3521
|
+
// key was self-certified — the pass that tries to refute it never
|
|
3522
|
+
// ran. Treat that as in_progress, not complete.
|
|
3523
|
+
const verPassed = /^status:\s*passed/mi.test(verText);
|
|
3524
|
+
// A `passed` report with no `falsification:` key was self-certified
|
|
3525
|
+
// — the pass that tries to refute it never ran. Do NOT downgrade it
|
|
3526
|
+
// here: every VERIFICATION.md written before the falsification pass
|
|
3527
|
+
// existed lacks the key, and silently reverting those phases to
|
|
3528
|
+
// in_progress would undo real completion history. Surface it
|
|
3529
|
+
// instead, so the gap is visible without rewriting the past.
|
|
3530
|
+
if (verPassed && !/^falsification:\s*upheld/mi.test(verText)) {
|
|
3531
|
+
parsed.self_certified_phases = parsed.self_certified_phases || [];
|
|
3532
|
+
parsed.self_certified_phases.push(phaseNum);
|
|
3533
|
+
}
|
|
3534
|
+
const hasSummary = dirInfo.files.some(f => /SUMMARY\.md$/i.test(f));
|
|
3535
|
+
const hasSprint = dirInfo.files.some(f => /-SPRINT\.md$/i.test(f));
|
|
3536
|
+
let diskStatus = null;
|
|
3537
|
+
// A passed VERIFICATION.md is the strongest completion artifact
|
|
3538
|
+
// there is — do NOT also require a SUMMARY.md. Summaries stop
|
|
3539
|
+
// being written partway through many real projects, and gating on
|
|
3540
|
+
// them leaves verified phases stuck at in_progress forever.
|
|
3541
|
+
if (verPassed) diskStatus = 'complete';
|
|
3542
|
+
else if (hasSummary || hasSprint) diskStatus = 'in_progress';
|
|
3543
|
+
if (diskStatus && (statusRank[diskStatus] ?? 0) > (statusRank[incomingStatus] ?? 0)) {
|
|
3544
|
+
incomingStatus = diskStatus;
|
|
3545
|
+
parsed.disk_derived_status = parsed.disk_derived_status || [];
|
|
3546
|
+
parsed.disk_derived_status.push({ phase: phaseNum, status: diskStatus });
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
} catch { /* best-effort — never fail sync over disk inspection */ }
|
|
3550
|
+
|
|
3459
3551
|
// Cross-check against VERIFICATION.md before trusting a 'complete' claim
|
|
3460
3552
|
// from ROADMAP prose. A phase can be hand-edited to say "Complete" (or
|
|
3461
3553
|
// "gaps_found → closed") without ever re-running the verifier — confirmed
|
|
@@ -3466,9 +3558,7 @@ function cmdState(subArgs) {
|
|
|
3466
3558
|
if (incomingStatus === 'complete') {
|
|
3467
3559
|
try {
|
|
3468
3560
|
const phasesRootDir = path.join(PLANNING_DIR, 'phases');
|
|
3469
|
-
const phaseDirName =
|
|
3470
|
-
? fs.readdirSync(phasesRootDir).find(d => d === phaseNum || d.startsWith(`${phaseNum}-`))
|
|
3471
|
-
: null;
|
|
3561
|
+
const phaseDirName = (findPhaseDirFiles(phaseNum) || {}).dirName || null;
|
|
3472
3562
|
if (phaseDirName) {
|
|
3473
3563
|
const verFile = fs.readdirSync(path.join(phasesRootDir, phaseDirName))
|
|
3474
3564
|
.find(f => /-VERIFICATION\.md$/i.test(f));
|
|
@@ -41,12 +41,14 @@ These files were extracted from heavy agents (>100L) to reduce context budget pe
|
|
|
41
41
|
|
|
42
42
|
| File | Loaded by |
|
|
43
43
|
|------|-----------|
|
|
44
|
-
| `agent-shared-rules.md` | rcode-fatima, rcode-hanzla, rcode-hussain-pm, rcode-mariam, rcode-sadiq, rcode-waleed |
|
|
44
|
+
| `agent-shared-rules.md` | rcode-fatima, rcode-hanzla, rcode-hussain-pm, rcode-mariam, rcode-sadiq, rcode-waleed; Calibration discipline section also referenced by rcode-verifier and rcode-reviewer |
|
|
45
45
|
| `codebase-grounding.md` | rcode-ahmed, rcode-fatima, rcode-haitham, rcode-hanzla, rcode-hussain-pm, rcode-khalid, rcode-layla, rcode-mariam, rcode-nasser, rcode-noor, rcode-omar, rcode-sadiq, rcode-waleed, rcode-yousef, rcode-zahra, rcode-zayd |
|
|
46
46
|
| `karpathy-guidelines.md` | rcode-assumptions-analyzer, rcode-fixer, rcode-debugger, rcode-deviation-analyzer, rcode-fatima, rcode-haitham, rcode-hanzla, rcode-hussain-pm, rcode-integration-checker, rcode-khalid, rcode-noor, rcode-omar, rcode-phase-researcher, rcode-profiler, rcode-project-researcher, rcode-remediation-planner, rcode-research-synthesizer, rcode-roadmapper, rcode-ui-auditor, rcode-ux-designer, rcode-waleed, rcode-yousef, rcode-zayd |
|
|
47
47
|
| `karpathy-guidelines-full.md` | rcode-codebase-mapper, rcode-reviewer, rcode-docs-auditor, rcode-edge-case-hunter, rcode-executor, rcode-nyquist-auditor, rcode-planner, rcode-security-adversary, rcode-security-auditor, rcode-sprint-checker, rcode-verifier |
|
|
48
48
|
| `response-style.md` | rcode-advisor-researcher, rcode-ahmed, rcode-assumptions-analyzer, rcode-codebase-mapper, rcode-fixer, rcode-reviewer, rcode-debugger, rcode-deviation-analyzer, rcode-docs-auditor, rcode-edge-case-hunter, rcode-executor, rcode-haitham, rcode-integration-checker, rcode-khalid, rcode-layla, rcode-nasser, rcode-noor, rcode-nyquist-auditor, rcode-omar, rcode-phase-researcher, rcode-planner, rcode-profiler, rcode-project-researcher, rcode-remediation-planner, rcode-research-synthesizer, rcode-roadmapper, rcode-security-adversary, rcode-security-auditor, rcode-sprint-checker, rcode-ui-auditor, rcode-ux-designer, rcode-verifier, rcode-yousef, rcode-zahra, rcode-zayd |
|
|
49
49
|
|
|
50
|
+
| `github-comment-style.md` | ship, pr-branch, export-to-github, review-adversarial, rcode-herdr-orchestration, and any agent dispatched into a repo with a GitHub remote |
|
|
51
|
+
|
|
50
52
|
---
|
|
51
53
|
|
|
52
54
|
## Workflow References
|
|
@@ -26,6 +26,93 @@
|
|
|
26
26
|
|
|
27
27
|
**Numeric claims need numbers.** "Fast" / "slow" / "scalable" / "performant" are forbidden as evidence. State the threshold (`p95 < 200ms`) or admit you don't have it (`unknown — would need 1 hour to measure`).
|
|
28
28
|
|
|
29
|
+
**Count the population, not the sample.** Before arguing that something is
|
|
30
|
+
widespread ("error handling is inconsistent", "these calls are unguarded"), count
|
|
31
|
+
it: how many call sites, in how many files, out of how many total. `67 routes, 256
|
|
32
|
+
fetch sites, 20 Sentry calls in 6 files` ends a debate that adjectives extend. An
|
|
33
|
+
unquantified sweeping claim is an opinion wearing a finding's clothes.
|
|
34
|
+
|
|
35
|
+
**Route to a persona from context, never from a keyword table alone.** When any
|
|
36
|
+
workflow picks which specialist to dispatch, read the evidence first — the files
|
|
37
|
+
the work touches, the migrations and schemas it alters, the decisions already
|
|
38
|
+
recorded — and choose the lens that evidence needs. Keyword scorers
|
|
39
|
+
(`select-panel` and friends) route on the words the request happens to use, not
|
|
40
|
+
on what it touches, so a high score is corroboration and a zero score is no
|
|
41
|
+
information. When your reading disagrees with the score, your reading wins, and
|
|
42
|
+
you name the file or decision that made you override.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Redirect protocol
|
|
47
|
+
|
|
48
|
+
Every persona file carries a `## Redirects` table. It is not decoration — it is
|
|
49
|
+
the contract for what you do when a request lands outside your lens.
|
|
50
|
+
|
|
51
|
+
**When the request is squarely in another persona's owned domain** (it maps to a
|
|
52
|
+
line in your `## Redirects`, or to a `Do NOT use for:` entry in your
|
|
53
|
+
description), say so in your FIRST line, before doing anything else:
|
|
54
|
+
|
|
55
|
+
> Haitham — frontend. This is a schema and query-plan question, which is Yousef's
|
|
56
|
+
> lens, not mine. Want me to hand it to him? Otherwise I'll take it as far as I
|
|
57
|
+
> can.
|
|
58
|
+
|
|
59
|
+
Three rules make this useful instead of annoying:
|
|
60
|
+
|
|
61
|
+
1. **Offer, never refuse.** You are flagging a better owner, not declining work.
|
|
62
|
+
If the user says continue, says nothing, or the run is autonomous — do the
|
|
63
|
+
work. A persona that stops and waits has converted a helpful note into a
|
|
64
|
+
blocker.
|
|
65
|
+
2. **Name who and why, in one sentence.** "Yousef owns query plans and index
|
|
66
|
+
strategy" is useful. "This is outside my area" is not — it tells the user
|
|
67
|
+
nothing they can act on.
|
|
68
|
+
3. **Only for the core of another lens, not for anything adjacent.** A frontend
|
|
69
|
+
task that touches an API response shape is still frontend work. Offer the
|
|
70
|
+
handoff when the *deliverable itself* belongs to someone else, not every time
|
|
71
|
+
another domain is mentioned. Redirecting on adjacency is how a team stops
|
|
72
|
+
answering questions.
|
|
73
|
+
|
|
74
|
+
**Say it once.** If the user chose you anyway, they have decided — do not raise
|
|
75
|
+
it again later in the same exchange, and do not caveat every subsequent answer
|
|
76
|
+
with it. Repeating a declined handoff reads as reluctance to work.
|
|
77
|
+
|
|
78
|
+
**Never use this to dodge.** If you can do the task, the honest form is "X would
|
|
79
|
+
do this better, here is my answer meanwhile" — not "you should ask X" with no
|
|
80
|
+
answer attached.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Calibration discipline
|
|
85
|
+
|
|
86
|
+
**Under-claiming is the same defect as over-claiming.** Reporting `gaps_found` when
|
|
87
|
+
the evidence says `passed`, hedging a confirmed finding into a "possible issue", or
|
|
88
|
+
adding a caveat you cannot name a failure mode for — these are not caution. They
|
|
89
|
+
are inaccurate reports, and a user cannot plan around an agent whose confidence
|
|
90
|
+
does not track its evidence. State the level the evidence supports: neither higher
|
|
91
|
+
nor lower.
|
|
92
|
+
|
|
93
|
+
**Every hedge must name its unknown.** "This may not be complete" is noise. "I did
|
|
94
|
+
not check the migration files, only the schema" is calibration. If you cannot name
|
|
95
|
+
what you did not verify, delete the hedge and make the claim.
|
|
96
|
+
|
|
97
|
+
**Say "I don't know" plainly, then say what would resolve it.** Not silence, not a
|
|
98
|
+
confident guess. `Unknown — reading src/queue/worker.ts would settle it.` This is
|
|
99
|
+
the highest-trust sentence available to you; the agents that never say it are the
|
|
100
|
+
ones whose output has to be re-checked.
|
|
101
|
+
|
|
102
|
+
**Separate the symptom fix from the cause, out loud.** When you ship a patch that
|
|
103
|
+
does not address the root cause, say so in the same breath, and say what the cause
|
|
104
|
+
is. Never let "fixed" stand for "worked around".
|
|
105
|
+
|
|
106
|
+
**Name your own risk before the reviewer finds it.** If part of your change is
|
|
107
|
+
timing-dependent, untested, or rests on an assumption, flag it yourself in the
|
|
108
|
+
summary. Flagging costs nothing and is the difference between a report that gets
|
|
109
|
+
trusted and one that gets audited.
|
|
110
|
+
|
|
111
|
+
**Prefer the enforced standard over the explained one.** A rule that lives only in
|
|
112
|
+
a doc decays on contact with the next agent. When you fix a class of mistake, ask
|
|
113
|
+
whether a test, a gate, or a checker assertion can make the mistake impossible —
|
|
114
|
+
and add that instead of, or in addition to, the paragraph describing it.
|
|
115
|
+
|
|
29
116
|
---
|
|
30
117
|
|
|
31
118
|
## Engineering invariants
|