@hanzlaa/rcode 4.12.1 → 4.14.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 (63) 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/rcode-project-researcher.md +19 -1
  11. package/rcode/agents/rules/executor/correctness-hazard-scan.md +98 -0
  12. package/rcode/agents/rules/executor/execution-flow.md +8 -0
  13. package/rcode/agents/rules/executor/self-check.md +8 -0
  14. package/rcode/agents/rules/orchestrator/contract.md +76 -0
  15. package/rcode/agents/rules/sprint-checker/dimensions.md +38 -0
  16. package/rcode/agents/rules/verifier/reachability-check.md +45 -2
  17. package/rcode/bin/lib/progress.cjs +41 -13
  18. package/rcode/bin/lib/roadmap.cjs +62 -22
  19. package/rcode/bin/lib/state-digest.cjs +88 -0
  20. package/rcode/bin/rcode-hooks.cjs +192 -23
  21. package/rcode/bin/rcode-tools.cjs +278 -7
  22. package/rcode/references/REFERENCES_INDEX.md +3 -1
  23. package/rcode/references/agent-shared-rules.md +123 -0
  24. package/rcode/references/code-reviewer-playbook.md +5 -0
  25. package/rcode/references/executor-playbook.md +2 -0
  26. package/rcode/references/github-comment-style.md +57 -0
  27. package/rcode/references/persona-executor-mode.md +61 -0
  28. package/rcode/references/planner-playbook.md +11 -0
  29. package/rcode/references/questioning.md +100 -2
  30. package/rcode/references/response-style.md +21 -4
  31. package/rcode/references/roadmapper-playbook.md +14 -0
  32. package/rcode/references/verifier-playbook.md +26 -0
  33. package/rcode/skills/SKILLS_INDEX.md +1 -1
  34. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/references.md +7 -0
  35. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/rules/merge-strategy.md +19 -3
  36. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/wave-prompt.md +3 -1
  37. package/rcode/skills/agents/{raees-orchestrator → orchestrator}/SKILL.md +1 -1
  38. package/rcode/team.yaml +20 -1
  39. package/rcode/workflows/audit-worktrees.md +15 -1
  40. package/rcode/workflows/execute-verify-phase-goal.md +58 -2
  41. package/rcode/workflows/execute.md +43 -8
  42. package/rcode/workflows/new-project-define-requirements.md +36 -0
  43. package/rcode/workflows/new-project-research-decision.md +61 -1
  44. package/rcode/workflows/new-project.md +95 -6
  45. package/rcode/workflows/plan-research-validation.md +8 -2
  46. package/rcode/workflows/plan-spawn-planner.md +32 -4
  47. package/rcode/workflows/plan.md +208 -18
  48. package/rcode/workflows/pr-branch.md +2 -0
  49. package/rcode/workflows/research-phase.md +12 -4
  50. package/rcode/workflows/resume-work.md +18 -0
  51. package/rcode/workflows/ship.md +4 -0
  52. package/rcode/workflows/verify-phase.md +40 -0
  53. package/server/dashboard.js +57 -17
  54. package/server/lib/html/client/components/OrchPanel.js +6 -2
  55. package/server/lib/html/client/components/XtermPanel.js +7 -2
  56. package/server/lib/html/client/orchestrator.js +58 -21
  57. package/server/lib/html/client/views/MemoryView.js +59 -3
  58. package/server/lib/html/css.js +40 -0
  59. package/server/lib/html/shell.js +10 -4
  60. package/server/lib/scanner.js +150 -3
  61. package/server/lib/view-only.js +32 -0
  62. package/server/orchestrator.js +63 -4
  63. /package/rcode/skills/agents/{raees-orchestrator → orchestrator}/references.md +0 -0
@@ -193,7 +193,13 @@ function extractReqIds(requirements) {
193
193
  if (!Array.isArray(requirements) || requirements.length === 0) return [];
194
194
  const seen = new Set();
195
195
  const out = [];
196
- const re = /\bREQ-[A-Z0-9][A-Z0-9-]*\b/g;
196
+ // Two shapes, because real projects rarely use the REQ- prefix:
197
+ // REQ-AUTH, REQ-FOO-BAR — the documented convention
198
+ // FOUND-01, RENT-04, AUTHZ-04, OBJ-06, CITY-02 — what projects actually write
199
+ // Matching only the first shape returned an empty phase_req_ids on every
200
+ // domain-prefixed project, and plan.md's Requirements Coverage Gate skips
201
+ // itself when that array is empty. The gate was silently off, not passing.
202
+ const re = /\bREQ-[A-Z0-9][A-Z0-9-]*\b|\b[A-Z][A-Z0-9]{1,15}-\d+[a-z]?\b/g;
197
203
  for (const line of requirements) {
198
204
  const matches = String(line).match(re) || [];
199
205
  for (const m of matches) {
@@ -510,6 +516,12 @@ function cmdInit(workflowName, rawArgs) {
510
516
 
511
517
  // Phase status from state.json (complete/executed/in_progress/planned/null).
512
518
  // Used by plan.md to show context-aware messaging when plans already exist.
519
+ //
520
+ // #948 — state_digest is derived from this SAME parse (single read, not a
521
+ // second pass over state.json). It replaces the raw `{state_path}` full-file
522
+ // read that plan-spawn-planner.md / research-phase.md / plan-research-
523
+ // validation.md instruct the researcher/planner subagents to do — those
524
+ // prompts embed state_digest directly instead.
513
525
  try {
514
526
  const stateFilePath = path.join(RCODE_DIR, 'state.json');
515
527
  const rawState = fs.existsSync(stateFilePath)
@@ -520,7 +532,9 @@ function cmdInit(workflowName, rawArgs) {
520
532
  return k === String(phaseNum);
521
533
  });
522
534
  out.phase_status = stPhase ? (stPhase.status || null) : null;
523
- } catch { out.phase_status = null; }
535
+ const stateDigest = require(path.join(__dirname, 'lib', 'state-digest.cjs'));
536
+ out.state_digest = stateDigest.buildStateDigest(rawState, phaseNum);
537
+ } catch { out.phase_status = null; out.state_digest = null; }
524
538
 
525
539
  // Disk artifacts — same shape as walkPhaseDirs() but inlined.
526
540
  if (phaseDirEntry) {
@@ -580,11 +594,20 @@ function cmdInit(workflowName, rawArgs) {
580
594
  const wf = nestedCfg.workflow || {};
581
595
  const features = nestedCfg.features || {};
582
596
 
597
+ // #949 — context_window folded into init so plan.md doesn't need a
598
+ // separate `config-get context_window` cold start (top-level scalar,
599
+ // not namespaced under workflow.*/features.*).
600
+ out.context_window = nestedCfg.context_window ?? null;
601
+
583
602
  // Workflow feature flags (top-level for direct workflow consumption).
584
603
  // Defaults match the inline `config-get … || echo "X"` calls in the workflows.
585
604
  out.research_enabled = String(wf.research_by_default ?? 'false') === 'true';
586
605
  out.plan_checker_enabled = String(wf.plan_checker ?? 'true') !== 'false';
587
606
  out.nyquist_validation_enabled = String(wf.nyquist_validation ?? 'true') !== 'false';
607
+ // Plan-time specialist review panel (#plan step 9.5). Default ON: the
608
+ // generalist planner+checker pair cannot see design or guard-shape
609
+ // defects, only goal coverage.
610
+ out.specialist_review_enabled = String(wf.specialist_review ?? 'true') !== 'false';
588
611
  out.text_mode = String(wf.text_mode ?? 'false') === 'true';
589
612
 
590
613
  // Model resolution per active profile. The researcher agent ships as
@@ -595,6 +618,21 @@ function cmdInit(workflowName, rawArgs) {
595
618
  out.planner_model = resolveModelString('rcode-planner');
596
619
  out.checker_model = resolveModelString('rcode-sprint-checker');
597
620
 
621
+ // #949 — agent-skills rows folded into init output. plan.md previously
622
+ // shelled out to `agent-skills rcode-phase-researcher` / `rcode-planner` /
623
+ // `rcode-sprint-checker` as 3 separate cold Node starts; research-phase.md
624
+ // and plan-research-validation.md each did their own `agent-skills
625
+ // rcode-phase-researcher` call. Same manifest lookup this init call
626
+ // already has installedAgents/readAgentManifest() loaded for.
627
+ {
628
+ const agentManifest = readAgentManifest();
629
+ out.agent_skills = {
630
+ researcher: resolveAgentId('rcode-phase-researcher', agentManifest) || null,
631
+ planner: resolveAgentId('rcode-planner', agentManifest) || null,
632
+ checker: resolveAgentId('rcode-sprint-checker', agentManifest) || null,
633
+ };
634
+ }
635
+
598
636
  // Phase requirement IDs — extracted from ROADMAP requirements block.
599
637
  out.phase_req_ids = extractReqIds(roadmapPhase ? roadmapPhase.requirements : []);
600
638
 
@@ -1382,6 +1420,26 @@ function cmdState(subArgs) {
1382
1420
  // --- set-phase ---
1383
1421
  if (sub === 'set-phase') {
1384
1422
  const name = subArgs[1];
1423
+ // A flag-looking argument is never a phase name. Without this,
1424
+ // `state set-phase --phase 99 --status complete` created a phase literally
1425
+ // NAMED "--phase", set current_phase to "--phase", and returned ok:true.
1426
+ // Silent state corruption reported from a live project.
1427
+ if (typeof name === 'string' && name.startsWith('--')) {
1428
+ throw new Error(
1429
+ `set-phase takes a phase NAME as a positional argument, not flags. ` +
1430
+ `Got "${name}". Did you mean:\n` +
1431
+ ` state set-phase "Phase name" (set the current phase pointer)\n` +
1432
+ ` phase complete <N> (mark a phase complete)\n` +
1433
+ ` state planned-phase --phase <N> (record a phase as planned)`
1434
+ );
1435
+ }
1436
+ const strayFlags = subArgs.slice(2).filter(a => typeof a === 'string' && a.startsWith('--'));
1437
+ if (strayFlags.length > 0) {
1438
+ throw new Error(
1439
+ `set-phase does not accept flags (${strayFlags.join(', ')}). It sets the ` +
1440
+ `current-phase pointer only. Use 'phase complete <N>' to change a phase's status.`
1441
+ );
1442
+ }
1385
1443
  if (!name) throw new Error('set-phase requires a phase name argument');
1386
1444
  const state = readState() || defaultState();
1387
1445
  // Fix #854 — mark the previously active phase as completed before switching.
@@ -3224,6 +3282,29 @@ function cmdState(subArgs) {
3224
3282
  // instead. Its stale-executing-phase hygiene warning was ported there.
3225
3283
  // Kept only for backward compatibility with anyone scripting against it
3226
3284
  // directly; do not wire new callers to this — use `phase complete`.
3285
+ // Records what the user actually authorized this session — 'plan', 'build',
3286
+ // 'research', 'audit'. `resume-work` reads it so "resume" restores POSITION
3287
+ // AND SCOPE, not position alone. Without it, a resume after a planning
3288
+ // session reads as "keep going" and starts building work nobody asked for.
3289
+ if (sub === 'set-intent') {
3290
+ const flags = parseFlags(1);
3291
+ const intent = flags.intent || subArgs[1];
3292
+ const ALLOWED = ['plan', 'build', 'research', 'audit', 'review'];
3293
+ if (!intent) throw new Error(`set-intent requires an intent (${ALLOWED.join('|')})`);
3294
+ if (!ALLOWED.includes(intent)) {
3295
+ throw new Error(`unknown intent "${intent}" — expected one of: ${ALLOWED.join(', ')}`);
3296
+ }
3297
+ const state = readState() || defaultState();
3298
+ const previous = state.last_intent ? state.last_intent.intent : null;
3299
+ state.last_intent = {
3300
+ intent,
3301
+ recorded_at: new Date().toISOString(),
3302
+ source: flags.source || 'workflow',
3303
+ };
3304
+ writeState(state);
3305
+ return { ok: true, intent, previous };
3306
+ }
3307
+
3227
3308
  if (sub === 'complete-phase') {
3228
3309
  const flags = parseFlags(1);
3229
3310
  if (!flags.phase) throw new Error('complete-phase requires --phase <N>');
@@ -3435,6 +3516,23 @@ function cmdState(subArgs) {
3435
3516
  return 'planned';
3436
3517
  }
3437
3518
 
3519
+ // Phase dirs are historically zero-padded ("03-evidence-ledger") while
3520
+ // ROADMAP tables and state.json use bare integers ("3"). Match on the
3521
+ // normalized number or every disk cross-check silently no-ops.
3522
+ const normPhaseNum = (k) => String(k ?? '').trim().replace(/^0+(\d)/, '$1');
3523
+ const findPhaseDirFiles = (phaseNum) => {
3524
+ const phasesRootDir = path.join(PLANNING_DIR, 'phases');
3525
+ if (!fs.existsSync(phasesRootDir)) return null;
3526
+ const want = normPhaseNum(phaseNum);
3527
+ const dirName = fs.readdirSync(phasesRootDir).find(d => {
3528
+ const m = d.match(/^(\d+(?:\.\d+)?)(?:-|$)/);
3529
+ return m && normPhaseNum(m[1]) === want;
3530
+ });
3531
+ if (!dirName) return null;
3532
+ const full = path.join(phasesRootDir, dirName);
3533
+ return { dirName, path: full, files: fs.readdirSync(full) };
3534
+ };
3535
+
3438
3536
  const upsertPhase = (phaseNum, phaseName, phaseGoal, phaseStatus) => {
3439
3537
  if (!/^\d/.test(phaseNum)) return;
3440
3538
  if (phaseName.toLowerCase() === 'phase') return;
@@ -3456,6 +3554,49 @@ function cmdState(subArgs) {
3456
3554
  const statusRank = { complete: 2, in_progress: 1, planned: 0 };
3457
3555
  let incomingStatus = normalizeStatus(phaseStatus);
3458
3556
 
3557
+ // --from-disk means FROM DISK. The ROADMAP status column is only one
3558
+ // signal, and in several roadmap shapes column 4 isn't a status column
3559
+ // at all (e.g. a "Blocking?" column), so ROADMAP-only sync leaves every
3560
+ // shipped phase sitting at `planned` forever and no amount of re-running
3561
+ // sync fixes it. Advance from the artifacts that actually exist on disk.
3562
+ // Status never downgrades (statusRank guard below), so this can only
3563
+ // correct an under-reported phase, never overwrite a truer one.
3564
+ try {
3565
+ const dirInfo = findPhaseDirFiles(phaseNum);
3566
+ if (dirInfo) {
3567
+ const verFile = dirInfo.files.find(f => /-?VERIFICATION\.md$/i.test(f));
3568
+ const verText = verFile ? fs.readFileSync(path.join(dirInfo.path, verFile), 'utf8') : '';
3569
+ // `passed` alone is not enough: a report with no `falsification:`
3570
+ // key was self-certified — the pass that tries to refute it never
3571
+ // ran. Treat that as in_progress, not complete.
3572
+ const verPassed = /^status:\s*passed/mi.test(verText);
3573
+ // A `passed` report with no `falsification:` key was self-certified
3574
+ // — the pass that tries to refute it never ran. Do NOT downgrade it
3575
+ // here: every VERIFICATION.md written before the falsification pass
3576
+ // existed lacks the key, and silently reverting those phases to
3577
+ // in_progress would undo real completion history. Surface it
3578
+ // instead, so the gap is visible without rewriting the past.
3579
+ if (verPassed && !/^falsification:\s*upheld/mi.test(verText)) {
3580
+ parsed.self_certified_phases = parsed.self_certified_phases || [];
3581
+ parsed.self_certified_phases.push(phaseNum);
3582
+ }
3583
+ const hasSummary = dirInfo.files.some(f => /SUMMARY\.md$/i.test(f));
3584
+ const hasSprint = dirInfo.files.some(f => /-SPRINT\.md$/i.test(f));
3585
+ let diskStatus = null;
3586
+ // A passed VERIFICATION.md is the strongest completion artifact
3587
+ // there is — do NOT also require a SUMMARY.md. Summaries stop
3588
+ // being written partway through many real projects, and gating on
3589
+ // them leaves verified phases stuck at in_progress forever.
3590
+ if (verPassed) diskStatus = 'complete';
3591
+ else if (hasSummary || hasSprint) diskStatus = 'in_progress';
3592
+ if (diskStatus && (statusRank[diskStatus] ?? 0) > (statusRank[incomingStatus] ?? 0)) {
3593
+ incomingStatus = diskStatus;
3594
+ parsed.disk_derived_status = parsed.disk_derived_status || [];
3595
+ parsed.disk_derived_status.push({ phase: phaseNum, status: diskStatus });
3596
+ }
3597
+ }
3598
+ } catch { /* best-effort — never fail sync over disk inspection */ }
3599
+
3459
3600
  // Cross-check against VERIFICATION.md before trusting a 'complete' claim
3460
3601
  // from ROADMAP prose. A phase can be hand-edited to say "Complete" (or
3461
3602
  // "gaps_found → closed") without ever re-running the verifier — confirmed
@@ -3466,9 +3607,7 @@ function cmdState(subArgs) {
3466
3607
  if (incomingStatus === 'complete') {
3467
3608
  try {
3468
3609
  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;
3610
+ const phaseDirName = (findPhaseDirFiles(phaseNum) || {}).dirName || null;
3472
3611
  if (phaseDirName) {
3473
3612
  const verFile = fs.readdirSync(path.join(phasesRootDir, phaseDirName))
3474
3613
  .find(f => /-VERIFICATION\.md$/i.test(f));
@@ -3492,6 +3631,32 @@ function cmdState(subArgs) {
3492
3631
  }
3493
3632
 
3494
3633
  if (existingIdx >= 0) {
3634
+ // Identity check BEFORE anything is carried over. Sync matched this
3635
+ // entry by NUMBER, but a number is a slot, not an identity. When a
3636
+ // roadmap is replaced, slot 3 can go from "Location Template" to
3637
+ // "Competitor Gap Analysis" — two unrelated pieces of work. Carrying
3638
+ // the old status across told a live project that competitor analysis
3639
+ // was "complete" when it had never been started, and only a manual
3640
+ // disk audit caught it.
3641
+ const priorName = String(state.phases[existingIdx].name || '').trim();
3642
+ const incomingName = String(phaseName || '').trim();
3643
+ const normName = (n) => n.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
3644
+ const identityChanged = priorName && incomingName
3645
+ && normName(priorName) !== normName(incomingName);
3646
+ if (identityChanged) {
3647
+ // Different work in the same slot. Drop the inherited status and
3648
+ // completion, and let the disk-derived pass below decide afresh.
3649
+ state.phases[existingIdx].status = 'planned';
3650
+ delete state.phases[existingIdx].completed;
3651
+ delete state.phases[existingIdx].started;
3652
+ parsed.identity_changed = parsed.identity_changed || [];
3653
+ parsed.identity_changed.push({
3654
+ phase: phaseNum,
3655
+ was: priorName,
3656
+ now: incomingName,
3657
+ carried_status_dropped: true,
3658
+ });
3659
+ }
3495
3660
  // Backfill both id and number so future readers using either schema find it.
3496
3661
  state.phases[existingIdx].number = state.phases[existingIdx].number || phaseNum;
3497
3662
  state.phases[existingIdx].id = state.phases[existingIdx].id || phaseNum;
@@ -4378,6 +4543,70 @@ function cmdPhase(subArgs) {
4378
4543
  // Closes #731. No --names arg required — reads the ROADMAP table directly.
4379
4544
  // Only creates directories; does NOT create .md files inside them.
4380
4545
  // =====================================================================
4546
+ // phase rename-dir <N> — align a phase directory's slug with its ROADMAP name.
4547
+ // Dry-run by default: renaming a directory moves artifacts and, without git mv,
4548
+ // detaches their history. There was no mechanism for this at all, so a roadmap
4549
+ // rewrite left every directory carrying the name of whatever it used to be.
4550
+ if (sub === 'rename-dir') {
4551
+ // cmdPhase has no shared flag parser (parseFlags is local to cmdState), so
4552
+ // read the two flags this needs directly.
4553
+ const argvIdx = subArgs.findIndex((a, i) => i > 0 && !String(a).startsWith('--'));
4554
+ const phaseFlagIdx = subArgs.indexOf('--phase');
4555
+ const target = argvIdx > 0 ? subArgs[argvIdx]
4556
+ : (phaseFlagIdx !== -1 ? subArgs[phaseFlagIdx + 1] : null);
4557
+ if (!target) throw new Error('phase rename-dir requires a phase number');
4558
+ const apply = subArgs.includes('--apply');
4559
+
4560
+ const found = cmdFindPhase([String(target)]);
4561
+ if (!found.exists) throw new Error(`No phase directory on disk for phase ${target}`);
4562
+
4563
+ const roadmapLib = require(path.join(__dirname, 'lib', 'roadmap.cjs'));
4564
+ const rp = roadmapLib.dispatch(PROJECT_ROOT, ['get-phase', String(target)]);
4565
+ if (!rp || !rp.found || !rp.name) {
4566
+ throw new Error(`Phase ${target} not found in ROADMAP.md — nothing to rename toward`);
4567
+ }
4568
+
4569
+ const slugify = (t) => String(t).toLowerCase()
4570
+ .replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').replace(/-+/g, '-');
4571
+ const newSlug = slugify(rp.name);
4572
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
4573
+ const oldDirName = path.basename(found.dir);
4574
+ const newDirName = `${target}-${newSlug}`;
4575
+
4576
+ if (oldDirName === newDirName) {
4577
+ return { ok: true, renamed: false, reason: 'directory already matches the roadmap name', dir: found.dir };
4578
+ }
4579
+ const newPath = path.join(phasesDir, newDirName);
4580
+ if (fs.existsSync(newPath)) {
4581
+ throw new Error(`Target directory already exists: ${newDirName}. Resolve by hand — two phase dirs for one number is worse than a stale name.`);
4582
+ }
4583
+
4584
+ if (!apply) {
4585
+ return {
4586
+ ok: true,
4587
+ renamed: false,
4588
+ dry_run: true,
4589
+ from: oldDirName,
4590
+ to: newDirName,
4591
+ note: 'Dry run. Re-run with --apply to rename. Check first that the artifacts in this directory belong to the phase the roadmap now describes — if the phase was REPLACED rather than renamed, renaming hides that instead of fixing it.',
4592
+ };
4593
+ }
4594
+
4595
+ // Prefer `git mv` so the artifacts keep their history.
4596
+ const oldPath = path.join(phasesDir, oldDirName);
4597
+ let method = 'fs';
4598
+ const { spawnSync } = require('child_process');
4599
+ const gitMv = spawnSync('git', ['mv', oldPath, newPath], { cwd: PROJECT_ROOT, encoding: 'utf8' });
4600
+ if (gitMv.status === 0) { method = 'git mv'; }
4601
+ else { fs.renameSync(oldPath, newPath); }
4602
+
4603
+ return {
4604
+ ok: true, renamed: true, method,
4605
+ from: oldDirName, to: newDirName,
4606
+ warning: 'Any file referencing the old path (SPRINT frontmatter, SUMMARY links, notes) still points at it. Grep for the old slug.',
4607
+ };
4608
+ }
4609
+
4381
4610
  if (sub === 'scaffold-all') {
4382
4611
  const roadmapPath = path.join(PLANNING_DIR, 'ROADMAP.md');
4383
4612
  const phasesDir = path.join(PLANNING_DIR, 'phases');
@@ -5711,11 +5940,52 @@ function cmdFindPhase(args) {
5711
5940
  .map((d) => path.relative(PROJECT_ROOT, path.join(phasesDir, d)));
5712
5941
  if (!exact) return { number: target, exists: false, dir: null, slug: null, decimal_children };
5713
5942
  const slugMatch = exact.match(/^\d+(?:\.\d+)?[-](.+)$/);
5943
+ const slug = slugMatch ? slugMatch[1] : '';
5944
+
5945
+ // Name drift: the directory keeps the slug it was created with, but ROADMAP.md
5946
+ // can be rewritten under it. Resolving to the existing directory is correct —
5947
+ // that is where the artifacts and the git history live, and auto-renaming would
5948
+ // orphan both. Staying SILENT about the divergence is not: an agent reading
5949
+ // `slug: foundation-contact-loop` while the roadmap says "Rentable Contact
5950
+ // Layer" has no way to tell whether it is looking at the same work.
5951
+ // Same class as the phase-identity drift in `state sync --from-disk`.
5952
+ let name_drift = null;
5953
+ try {
5954
+ const roadmapPath = path.join(PLANNING_DIR, 'ROADMAP.md');
5955
+ if (slug && fs.existsSync(roadmapPath)) {
5956
+ const roadmapLib = require(path.join(__dirname, 'lib', 'roadmap.cjs'));
5957
+ const rp = roadmapLib.dispatch(PROJECT_ROOT, ['get-phase', String(target)]);
5958
+ const roadmapName = rp && rp.found ? String(rp.name || '') : '';
5959
+ if (roadmapName) {
5960
+ const slugify = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
5961
+ const fromRoadmap = slugify(roadmapName);
5962
+ // Compare on word sets, not exact slugs — a truncated or reordered slug
5963
+ // is normal, a different subject is the signal.
5964
+ const words = (t) => new Set(String(t).split('-').filter(w => w.length > 3));
5965
+ const dirWords = words(slug);
5966
+ const roadWords = words(fromRoadmap);
5967
+ const shared = [...dirWords].filter(w => roadWords.has(w)).length;
5968
+ if (dirWords.size > 0 && roadWords.size > 0 && shared === 0) {
5969
+ name_drift = {
5970
+ dir_slug: slug,
5971
+ roadmap_name: roadmapName,
5972
+ note: 'Directory name and ROADMAP name share no significant words. '
5973
+ + 'The phase may have been replaced under the same number. '
5974
+ + 'The directory is NOT auto-renamed: its artifacts and git history '
5975
+ + 'belong to whatever was built there. Confirm they are the same work '
5976
+ + 'before planning or executing against it.',
5977
+ };
5978
+ }
5979
+ }
5980
+ }
5981
+ } catch { /* drift detection is advisory — never fail a lookup over it */ }
5982
+
5714
5983
  return {
5715
5984
  number: target,
5716
5985
  exists: true,
5717
5986
  dir: path.relative(PROJECT_ROOT, path.join(phasesDir, exact)),
5718
- slug: slugMatch ? slugMatch[1] : '',
5987
+ slug,
5988
+ name_drift,
5719
5989
  decimal_children,
5720
5990
  };
5721
5991
  }
@@ -7344,6 +7614,7 @@ async function main() {
7344
7614
  console.log(' phase next-range [count] → return next N contiguous free phase numbers (#730)');
7345
7615
  console.log(' phase scaffold-milestone --names "n1|n2|..." → bulk-create phase folders for a milestone (#731)');
7346
7616
  console.log(' phase scaffold-all → create missing phase folders for all phases in ROADMAP.md (#731)');
7617
+ console.log(' phase rename-dir <N> [--apply] → align a phase dir slug with its ROADMAP name (dry-run by default)');
7347
7618
  console.log(' workflow-config-audit → find workflows still referencing .planning/config.json (#733)');
7348
7619
  console.log(' commit "<msg>" [--files p1 p2 ...] → atomic git commit with conventional-commits validation (no AI attribution, no --no-verify, no auto-push)');
7349
7620
  console.log(' commit-to-subrepo --subrepo <p> "<msg>" → atomic commit inside a git subrepo (same validation as commit)');
@@ -7434,7 +7705,7 @@ async function main() {
7434
7705
  console.log(' state story list [--sprint <NN.S>] [--status <status>]');
7435
7706
  return;
7436
7707
  default: {
7437
- const stateSubs = ['read','get','init','set-phase','advance-plan','snapshot','update-progress','record-execution','record-council','record-chain','add-decision','decisions-global','add-blocker','resolve-blocker','record-session','set-ids-in-state','migrate-ids','migrate-schema','next-phase-id','next-plan-id','next-task-id','resolve-id','workstream-create','workstream-switch','workstream-list','workstream-status','workstream-complete','workstream-validate','insert-phase','planned-phase','begin-phase','complete-phase','reset'];
7708
+ const stateSubs = ['read','get','init','set-phase','advance-plan','snapshot','update-progress','record-execution','record-council','record-chain','add-decision','decisions-global','add-blocker','resolve-blocker','record-session','set-ids-in-state','migrate-ids','migrate-schema','next-phase-id','next-plan-id','next-task-id','resolve-id','workstream-create','workstream-switch','workstream-list','workstream-status','workstream-complete','workstream-validate','insert-phase','planned-phase','begin-phase','complete-phase','set-intent','reset'];
7438
7709
  // Issue #656 — top-level aliases for intuitive guesses.
7439
7710
  const intuitionAliases = {
7440
7711
  blocker: 'state resolve-blocker',
@@ -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,129 @@
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
+ **Elicitation is not authoring — hand the pen back.** When gathering what the user
45
+ wants, the moment you catch yourself naming the stack, picking the MVP cut, or
46
+ proposing the phase breakdown, stop and hand it back to them. Infer-and-confirm
47
+ ("I'm assuming the maintainer is you, not a client — right?") is fine; presenting
48
+ your conclusion as a finding is not. Every inferred value that reaches an artifact
49
+ carries an inline `[ASSUMPTION]` tag, and every tag gets walked with the user
50
+ before that artifact is final. An untriaged assumption in a finished document is a
51
+ decision nobody made.
52
+
53
+ **Never decide the stack for the user.** Technology choice — language, framework,
54
+ CMS, database, hosting model — is the user's call, always. Research produces a
55
+ suggestion; only the user turns it into a decision. Present it with the ONE reason
56
+ that actually drove it, in their terms, and offer three ways out: confirm it, name
57
+ their own, or ask for more comparison. Then stop. No default, no auto-selection,
58
+ no "the obvious choice for this domain" — a wrong stack is the single most
59
+ expensive thing in a project to reverse.
60
+
61
+ **Write the premise into the decision, and re-open it when the premise dies.** "X
62
+ because a non-technical client updates content themselves" stays true only while
63
+ there is a non-technical client. When the project pivots, every decision whose
64
+ stated reason the pivot invalidated goes back to the user. A decision whose reason
65
+ has expired is not locked, it is stale — and treating it as locked is how a
66
+ codebase gets built twice.
67
+
68
+ **Planning never authorizes building.** If the user asked you to plan, design,
69
+ research, or audit, the deliverable is the plan, the design, the findings — not
70
+ the implementation. Finishing the plan and continuing into code is not
71
+ thoroughness, it is doing work nobody approved, and it costs more to unwind than
72
+ it saved. The same applies to an ambiguous continuation like "resume", "carry
73
+ on", or "next": it restores POSITION, never SCOPE. When the standing instruction
74
+ was to plan, a resume continues planning.
75
+
76
+ If you believe the next step is obvious and valuable, say so in one line and stop.
77
+ The user typing the next command takes two seconds; undoing an unrequested build
78
+ took a whole session.
79
+
80
+ ---
81
+
82
+ ## Redirect protocol
83
+
84
+ Every persona file carries a `## Redirects` table. It is not decoration — it is
85
+ the contract for what you do when a request lands outside your lens.
86
+
87
+ **When the request is squarely in another persona's owned domain** (it maps to a
88
+ line in your `## Redirects`, or to a `Do NOT use for:` entry in your
89
+ description), say so in your FIRST line, before doing anything else:
90
+
91
+ > Haitham — frontend. This is a schema and query-plan question, which is Yousef's
92
+ > lens, not mine. Want me to hand it to him? Otherwise I'll take it as far as I
93
+ > can.
94
+
95
+ Three rules make this useful instead of annoying:
96
+
97
+ 1. **Offer, never refuse.** You are flagging a better owner, not declining work.
98
+ If the user says continue, says nothing, or the run is autonomous — do the
99
+ work. A persona that stops and waits has converted a helpful note into a
100
+ blocker.
101
+ 2. **Name who and why, in one sentence.** "Yousef owns query plans and index
102
+ strategy" is useful. "This is outside my area" is not — it tells the user
103
+ nothing they can act on.
104
+ 3. **Only for the core of another lens, not for anything adjacent.** A frontend
105
+ task that touches an API response shape is still frontend work. Offer the
106
+ handoff when the *deliverable itself* belongs to someone else, not every time
107
+ another domain is mentioned. Redirecting on adjacency is how a team stops
108
+ answering questions.
109
+
110
+ **Say it once.** If the user chose you anyway, they have decided — do not raise
111
+ it again later in the same exchange, and do not caveat every subsequent answer
112
+ with it. Repeating a declined handoff reads as reluctance to work.
113
+
114
+ **Never use this to dodge.** If you can do the task, the honest form is "X would
115
+ do this better, here is my answer meanwhile" — not "you should ask X" with no
116
+ answer attached.
117
+
118
+ ---
119
+
120
+ ## Calibration discipline
121
+
122
+ **Under-claiming is the same defect as over-claiming.** Reporting `gaps_found` when
123
+ the evidence says `passed`, hedging a confirmed finding into a "possible issue", or
124
+ adding a caveat you cannot name a failure mode for — these are not caution. They
125
+ are inaccurate reports, and a user cannot plan around an agent whose confidence
126
+ does not track its evidence. State the level the evidence supports: neither higher
127
+ nor lower.
128
+
129
+ **Every hedge must name its unknown.** "This may not be complete" is noise. "I did
130
+ not check the migration files, only the schema" is calibration. If you cannot name
131
+ what you did not verify, delete the hedge and make the claim.
132
+
133
+ **Say "I don't know" plainly, then say what would resolve it.** Not silence, not a
134
+ confident guess. `Unknown — reading src/queue/worker.ts would settle it.` This is
135
+ the highest-trust sentence available to you; the agents that never say it are the
136
+ ones whose output has to be re-checked.
137
+
138
+ **Separate the symptom fix from the cause, out loud.** When you ship a patch that
139
+ does not address the root cause, say so in the same breath, and say what the cause
140
+ is. Never let "fixed" stand for "worked around".
141
+
142
+ **Name your own risk before the reviewer finds it.** If part of your change is
143
+ timing-dependent, untested, or rests on an assumption, flag it yourself in the
144
+ summary. Flagging costs nothing and is the difference between a report that gets
145
+ trusted and one that gets audited.
146
+
147
+ **Prefer the enforced standard over the explained one.** A rule that lives only in
148
+ a doc decays on contact with the next agent. When you fix a class of mistake, ask
149
+ whether a test, a gate, or a checker assertion can make the mistake impossible —
150
+ and add that instead of, or in addition to, the paragraph describing it.
151
+
29
152
  ---
30
153
 
31
154
  ## Engineering invariants
@@ -6,6 +6,11 @@ framework, specialization descriptions, workflow steps, and worked examples.
6
6
  The agent stub holds the role identity, response format, principles,
7
7
  anti-patterns, redirects, and constraints.
8
8
 
9
+ **Calibration:** follow the Calibration discipline section of
10
+ `@rcode/references/agent-shared-rules.md`. Reporting a gap the evidence does not
11
+ support is the same defect as missing one — report the level the evidence supports,
12
+ and every hedge must name the specific thing you did not check.
13
+
9
14
  ---
10
15
 
11
16
  ## How you think
@@ -54,6 +54,7 @@ For detailed deviation rules with examples, read `.rcode/agents-rules/executor/d
54
54
  - **Authentication gates:** "Not authenticated", "401", "403", "Set ENV_VAR" are gates (human-action checkpoints), not failures.
55
55
  - **Auto mode detection:** Check `workflow._auto_chain_active` and `workflow.auto_advance`. If true, auto-approve human-verify and auto-select first decision.
56
56
  - **Checkpoint protocol:** Automate first. Users never run CLI, only visit URLs, click UI, provide secrets.
57
+ - **Correctness hazard self-audit:** if this plan's diff touched async code, shared/mutable state, or a third-party library's async API, read `.rcode/agents-rules/executor/correctness-hazard-scan.md` BEFORE writing SUMMARY.md. Concurrency races, React state-updater purity, and async-library footguns pass `npm test`/`tsc` but reliably get caught in human PR review — catch them here instead.
57
58
 
58
59
  ---
59
60
 
@@ -115,5 +116,6 @@ For detailed deviation rules with examples, read `.rcode/agents-rules/executor/d
115
116
  | TDD RED/GREEN/REFACTOR flow | `.rcode/agents-rules/executor/tdd-flow.md` |
116
117
  | Stub detection and tagging | `.rcode/agents-rules/executor/stub-detection.md` |
117
118
  | Pre-SUMMARY verification checklist | `.rcode/agents-rules/executor/self-check.md` |
119
+ | Correctness hazard scan (concurrency/state/async-library) | `.rcode/agents-rules/executor/correctness-hazard-scan.md` |
118
120
 
119
121
  Read these ONLY when the current task needs them. Don't preemptively load.
@@ -0,0 +1,57 @@
1
+ # GitHub Comment & PR Body Style
2
+
3
+ Shared reference for every workflow or agent that writes text a human will read on
4
+ GitHub: PR bodies, PR review comments, issue comments, issue bodies. Hard contract,
5
+ not a suggestion.
6
+
7
+ ## The principle
8
+
9
+ Write like the engineer who did the work, not like a tool reporting on it. A reviewer
10
+ opens the comment to learn three things: what this changes, what was decided and why,
11
+ and what is still open. Everything else is noise they have to scroll past.
12
+
13
+ ## Never include
14
+
15
+ - **Em-dashes (`—`) or en-dashes used as punctuation.** They are the single clearest
16
+ tell that text was machine-written. Use a comma, a colon, parentheses, or a full
17
+ stop. (This applies to the GitHub surface only, not to rcode's own planning docs.)
18
+ - **A gates / verification / CI block.** `Gates: biome clean, typecheck 12/12, server
19
+ 2100 passed / 367 skipped` and anything shaped like it. CI already reports pass/fail
20
+ on the PR; restating it is pure noise, and it goes stale the moment a commit lands.
21
+ - **Internal git process talk.** "merged, not rebased, per repo convention", "branch
22
+ updated from main", "squashed the fixup commits". The reviewer reviews the diff, not
23
+ how the branch got there. The one exception: branch staleness when it is the actual
24
+ cause of a red check or a conflict the reviewer will hit.
25
+ - **Self-blame or status filler.** "This is on me", "Apologies for the churn",
26
+ "Working on it now", "Let me know if you'd like anything changed".
27
+ - **AI attribution of any kind.** No "Generated with", no "Co-Authored-By: Claude",
28
+ no bot emoji sign-off.
29
+ - **Emoji-decorated section headers** (🚀 ✨ 🎯). One plain heading is enough.
30
+
31
+ ## Always include
32
+
33
+ - What the change does, in the reviewer's terms (behaviour, not file inventory).
34
+ - Any non-obvious decision, with the reason it was chosen over the alternative.
35
+ - What is still open, blocked, or deliberately out of scope.
36
+
37
+ ## Agent dispatch rule
38
+
39
+ When dispatching a subagent that touches a repo with a GitHub remote, **explicitly
40
+ forbid every `gh` write operation** in the dispatch prompt: `gh pr create`,
41
+ `gh pr comment`, `gh pr edit`, `gh pr merge`, `gh issue create`, `gh issue comment`,
42
+ `gh api` with a non-GET method. "Do not push" is NOT sufficient — an agent can post a
43
+ comment or open a PR without ever pushing, and that reaches other humans immediately.
44
+ The orchestrator posts to GitHub, after the user approves the text.
45
+
46
+ Related: agents' scratch notes (`REPLY.md`, `STATUS.md`, `NOTES.md`) must never be
47
+ committed to a branch that becomes a PR; they show up in the diff and read as leaked
48
+ machine output. Keep them in the scratchpad directory.
49
+
50
+ ## Self-check before posting
51
+
52
+ ```bash
53
+ # must print nothing
54
+ grep -n '—' "$BODY_FILE"
55
+ grep -niE '^ *(gates|verification|checks) *:' "$BODY_FILE"
56
+ grep -niE 'rebase|not rebased|merged from (main|master)|co-authored-by|generated with' "$BODY_FILE"
57
+ ```