@hanzlaa/rcode 4.12.1 → 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.
Files changed (54) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/CONTRIBUTING.md +1 -0
  4. package/cli/doctor.js +40 -5
  5. package/cli/install.js +6 -1
  6. package/dist/rcode.js +87 -87
  7. package/package.json +1 -1
  8. package/rcode/agents/rcode-hussain-pm.md +37 -3
  9. package/rcode/agents/rcode-orchestrator.md +91 -0
  10. package/rcode/agents/rules/executor/correctness-hazard-scan.md +98 -0
  11. package/rcode/agents/rules/executor/execution-flow.md +8 -0
  12. package/rcode/agents/rules/executor/self-check.md +8 -0
  13. package/rcode/agents/rules/orchestrator/contract.md +76 -0
  14. package/rcode/agents/rules/sprint-checker/dimensions.md +38 -0
  15. package/rcode/agents/rules/verifier/reachability-check.md +45 -2
  16. package/rcode/bin/lib/progress.cjs +41 -13
  17. package/rcode/bin/lib/state-digest.cjs +88 -0
  18. package/rcode/bin/rcode-hooks.cjs +192 -23
  19. package/rcode/bin/rcode-tools.cjs +94 -4
  20. package/rcode/references/REFERENCES_INDEX.md +3 -1
  21. package/rcode/references/agent-shared-rules.md +87 -0
  22. package/rcode/references/code-reviewer-playbook.md +5 -0
  23. package/rcode/references/executor-playbook.md +2 -0
  24. package/rcode/references/github-comment-style.md +57 -0
  25. package/rcode/references/persona-executor-mode.md +61 -0
  26. package/rcode/references/response-style.md +21 -4
  27. package/rcode/references/verifier-playbook.md +14 -0
  28. package/rcode/skills/SKILLS_INDEX.md +1 -1
  29. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/references.md +7 -0
  30. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/merge-strategy.md +19 -3
  31. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/wave-prompt.md +3 -1
  32. package/rcode/skills/agents/{raees-orchestrator → orchestrator}/SKILL.md +1 -1
  33. package/rcode/team.yaml +20 -1
  34. package/rcode/workflows/audit-worktrees.md +15 -1
  35. package/rcode/workflows/execute-verify-phase-goal.md +58 -2
  36. package/rcode/workflows/execute.md +37 -8
  37. package/rcode/workflows/plan-research-validation.md +8 -2
  38. package/rcode/workflows/plan-spawn-planner.md +32 -4
  39. package/rcode/workflows/plan.md +138 -10
  40. package/rcode/workflows/pr-branch.md +2 -0
  41. package/rcode/workflows/research-phase.md +12 -4
  42. package/rcode/workflows/ship.md +4 -0
  43. package/rcode/workflows/verify-phase.md +40 -0
  44. package/server/dashboard.js +57 -17
  45. package/server/lib/html/client/components/OrchPanel.js +6 -2
  46. package/server/lib/html/client/components/XtermPanel.js +7 -2
  47. package/server/lib/html/client/orchestrator.js +58 -21
  48. package/server/lib/html/client/views/MemoryView.js +59 -3
  49. package/server/lib/html/css.js +40 -0
  50. package/server/lib/html/shell.js +10 -4
  51. package/server/lib/scanner.js +150 -3
  52. package/server/lib/view-only.js +32 -0
  53. package/server/orchestrator.js +63 -4
  54. /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
- console.error(`Hook error: ${err.message}`);
120
- process.exit(1);
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
- console.error(`Hook error: ${err.message}`);
165
- process.exit(1);
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
- console.error(`Hook error: ${err.message}`);
305
- process.exit(1);
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
- console.error(`Hook error: ${err.message}`);
427
- process.exit(1);
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
- console.error(`Hook error: ${err.message}`);
577
- process.exit(1);
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(fs.readFileSync(abs, 'utf8'));
631
- } catch (e) {
632
- failures.push(`${file}: ${e.message}`);
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
- console.error(`Hook error: ${err.message}`);
646
- process.exit(1);
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
- console.error(`Hook error: ${err.message}`);
690
- process.exit(1);
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
- console.error(`Hook error: ${err.message}`);
1072
- process.exit(1);
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
- main().catch((err) => {
1178
- console.error(`Fatal error: ${err.message}`);
1179
- process.exit(1);
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
- } catch { out.phase_status = null; }
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 = fs.existsSync(phasesRootDir)
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