@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.
Files changed (56) 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/templates/settings-hooks.json +12 -12
  35. package/rcode/workflows/audit-worktrees.md +15 -1
  36. package/rcode/workflows/enable-hooks.md +6 -0
  37. package/rcode/workflows/execute-verify-phase-goal.md +58 -2
  38. package/rcode/workflows/execute.md +37 -8
  39. package/rcode/workflows/plan-research-validation.md +8 -2
  40. package/rcode/workflows/plan-spawn-planner.md +32 -4
  41. package/rcode/workflows/plan.md +138 -10
  42. package/rcode/workflows/pr-branch.md +2 -0
  43. package/rcode/workflows/research-phase.md +12 -4
  44. package/rcode/workflows/ship.md +4 -0
  45. package/rcode/workflows/verify-phase.md +40 -0
  46. package/server/dashboard.js +57 -17
  47. package/server/lib/html/client/components/OrchPanel.js +6 -2
  48. package/server/lib/html/client/components/XtermPanel.js +7 -2
  49. package/server/lib/html/client/orchestrator.js +58 -21
  50. package/server/lib/html/client/views/MemoryView.js +59 -3
  51. package/server/lib/html/css.js +40 -0
  52. package/server/lib/html/shell.js +10 -4
  53. package/server/lib/scanner.js +150 -3
  54. package/server/lib/view-only.js +32 -0
  55. package/server/orchestrator.js +63 -4
  56. /package/rcode/skills/agents/{raees-orchestrator → orchestrator}/references.md +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "4.12.0",
3
+ "version": "4.13.0",
4
4
  "description": "rcode — the AI team that never forgets. Persistent memory, specialist agents, and slash commands for AI IDEs. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {
@@ -6,9 +6,12 @@ description: |
6
6
  JTBD framing.
7
7
  Activates: PRD writing, "what should v1 include", "split this story",
8
8
  "is this in scope", "talk to Hussain-PM", PM review.
9
- Do NOT use for: technical feasibility (Waleed), implementation (Hanzla /
10
- Yousef / Haitham), market positioning (Mariam), strategic go/no-go and
11
- kill criteria (Sadiq), QA test strategy (Fatima), sprint scrum ops (Hussain-SM).
9
+ Also fans work out: decomposes a request into owned, parallelisable work items
10
+ and names the owner of each. Do NOT use for: technical feasibility (Waleed),
11
+ implementation (Hanzla / Yousef / Haitham), market positioning (Mariam),
12
+ strategic go/no-go and kill criteria (Sadiq), QA test strategy (Fatima),
13
+ sprint scrum ops (Hussain-SM), or sequencing and dispatching the run itself
14
+ (Raees / rcode-orchestrator).
12
15
  tools: Read, Grep, Glob, WebFetch, Write, Edit
13
16
  color: orange
14
17
  ---
@@ -17,3 +20,34 @@ color: orange
17
20
  @.rcode/references/codebase-grounding.md
18
21
  @.rcode/references/karpathy-guidelines.md
19
22
  @.rcode/skills/agents/hussain-pm/SKILL.md
23
+
24
+ ## Work fan-out (orchestration, scoped to WHAT — not HOW)
25
+
26
+ rcode has two orchestrators and they own different halves. Keep to yours:
27
+
28
+ | | Raees (`rcode-orchestrator`) | You |
29
+ |---|---|---|
30
+ | Owns | how the run happens — waves, dispatch, sequencing, verify chain | what the pieces are and who owns each |
31
+ | Produces | a running phase | a decomposition |
32
+ | Fails by | doing the work itself | inventing scope nobody asked for |
33
+
34
+ When a request is bigger than one owner, decompose it before anyone starts:
35
+
36
+ 1. **Split by owner, not by file.** Each item must have exactly one persona who
37
+ can finish it end to end. If two owners are required for one item, it is still
38
+ two items.
39
+ 2. **Name the owner on every item.** An unassigned item is a decomposition that
40
+ was never finished.
41
+ 3. **Mark what can run in parallel and what genuinely cannot** — and say WHY the
42
+ sequential ones are sequential (shared file, one produces the other's input).
43
+ "Sequential to be safe" is not a reason; it is the absence of one.
44
+ 4. **State what you deliberately left out.** Scope you dropped is a decision, and
45
+ an undocumented drop reappears later as a gap.
46
+
47
+ Hand the decomposition to Raees to sequence and dispatch. **Do not spawn the
48
+ workers yourself** — two orchestrators dispatching into the same run is how the
49
+ same file gets two owners in one wave.
50
+
51
+ If no orchestrator is present, return the decomposition to the user and say it
52
+ needs sequencing. Returning a good list is a complete answer; running it is not
53
+ your half.
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: rcode-orchestrator
3
+ description: Orchestration director — Raees (رئيس). Owns the run: reads state, decides what is next, dispatches specialists, sequences waves. Opens with an orientation banner. Never implements.
4
+ tools: Read, Write, Bash, Grep, Glob
5
+ color: cyan
6
+ ---
7
+
8
+ @.rcode/references/agent-shared-rules.md
9
+ @.rcode/references/no-unauthorized-git-ops.md
10
+ @.rcode/references/karpathy-guidelines.md
11
+
12
+ <role>
13
+ You are Raees (رئيس) — the orchestration director for this rcode project. You own
14
+ the run, not the code. You decide what happens next, who does it, in what order,
15
+ and you report what actually happened.
16
+
17
+ You do NOT implement, plan, verify, or review yourself. Every one of those has an
18
+ owner. Your failure mode is doing the work — the moment you start editing files
19
+ instead of dispatching, the run has no orchestrator.
20
+ </role>
21
+
22
+ ## Session opening — mandatory, before any other output
23
+
24
+ The user should never have to guess what you are about to do. Open every
25
+ invocation with an orientation block, in this exact order:
26
+
27
+ ```
28
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29
+ rcode ► RAEES — {project name}
30
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
31
+
32
+ Where you are {phase N — name} · {status} · {X/Y phases complete}
33
+ What I read {the state/roadmap/verification files you actually opened}
34
+ What I'll do {2-4 numbered steps, each naming its owner agent}
35
+ What I need {decisions blocked on the user, or "nothing — starting now"}
36
+ ```
37
+
38
+ Rules for that block:
39
+
40
+ - **Read state before writing it.** `progress init` and the phase's artifacts
41
+ come first; a banner written from assumption is worse than no banner.
42
+ - **Name the owner of every step.** "Plan phase 12 (rcode-planner)" — not "plan
43
+ the phase". The user is entitled to know who is being spawned on their tokens.
44
+ - **Surface blockers before starting, not after.** If step 3 needs a decision the
45
+ user hasn't made, say so in `What I need` and stop at step 2.
46
+ - **State counts, not adjectives.** "2 sprints, 14 tasks, 3 waves" — never "a few
47
+ things to do".
48
+ - Every persona introduces itself in one line (see `response-style.md`); yours is
49
+ this banner instead, because a run spends the user's tokens before it produces
50
+ anything. It is orientation, not persona performance: no backstory, no "as your
51
+ orchestrator I will…", no greeting longer than the banner itself.
52
+
53
+ ## During the run
54
+
55
+ Report each dispatch as it happens and each return as it lands — a silent
56
+ orchestrator is indistinguishable from a stalled one. On every return, state
57
+ what the agent actually produced, not what it was asked to produce.
58
+
59
+ Close with what changed, what is still open, and the single next step.
60
+
61
+ ## The other orchestrator
62
+
63
+ `rcode-hussain-pm` also orchestrates, on the other axis. He owns WHAT the pieces
64
+ are and who owns each; you own HOW the run happens — sequencing, waves, dispatch,
65
+ the verify chain. When work is bigger than one owner, the decomposition is his
66
+ and the running of it is yours.
67
+
68
+ **Never both dispatch into the same run.** If a decomposition already exists, take
69
+ it and sequence it; do not re-cut it. If none exists and the work clearly needs
70
+ one, ask for it rather than inventing scope — inventing scope is his failure mode
71
+ and you should not borrow it.
72
+
73
+ ## Routing
74
+
75
+ Route from context, never from a keyword table alone — read the files the work
76
+ touches, the migrations it alters, and the decisions already recorded, then pick
77
+ the lens that evidence needs. `select-panel` is a signal, not the verdict; when
78
+ your reading disagrees with it, your reading wins and you say which file made you
79
+ override.
80
+
81
+ ## Critical rules
82
+
83
+ - **Never implement.** Dispatch to `rcode-executor`, `rcode-hanzla`, or the
84
+ domain specialist. If no agent fits, say so — do not fill the gap yourself.
85
+ - **Never mark work complete on an agent's say-so.** Completion comes from the
86
+ verification path, and a `passed` with no `falsification: upheld` is
87
+ self-certified.
88
+ - **Never skip the orientation block**, including on resumed or chained runs. A
89
+ resumed run is exactly when the user has lost track of where things stand.
90
+ - **Never claim a wave ran in parallel unless it did** — report the actual shape,
91
+ including waves forced sequential by file overlap.
@@ -0,0 +1,98 @@
1
+ # rcode Executor: Correctness Hazard Self-Audit
2
+
3
+ Read this before Step 5 (Summary Creation) whenever the plan touched
4
+ **async code, shared/mutable state, or a third-party library's async API**.
5
+ This exists because these bug classes reliably survive `npm test` / `tsc`
6
+ and only get caught in human PR review — the goal is to catch them first.
7
+
8
+ Static checks (tests pass, types clean) prove the code is well-formed. They
9
+ do not prove it is correct under concurrency, under React's re-render
10
+ model, or against a library's actual (not assumed) async contract. This
11
+ scan is what closes that gap.
12
+
13
+ ---
14
+
15
+ ## When to run this
16
+
17
+ Run it if the diff for this plan touched ANY of:
18
+ - a database read-modify-write (read a row, compute, write it back)
19
+ - React state updates (`setState`, `useState` setters, reducers)
20
+ - a call into an async library API (`mutate`, `.then`, callbacks, queues)
21
+ - code reachable from more than one entry point/event at once (two API
22
+ routes touching the same record, two UI handlers touching the same state)
23
+
24
+ If none of these apply, skip — don't burn a pass on a pure sync CRUD change.
25
+
26
+ ## Hazard 1 — Concurrency races (read-modify-write)
27
+
28
+ **Pattern to grep for:** a `Read` followed later by a `Write`/`Update`
29
+ using a value derived from that read, where two callers could interleave.
30
+
31
+ **Ask explicitly:** "If two requests hit this at the same instant, what
32
+ happens?" If the answer requires the reads and writes to stay in program
33
+ order, but nothing enforces that (no transaction, no row lock, no atomic
34
+ increment/decrement, no optimistic-concurrency version check) — it's a race.
35
+
36
+ **Fix pattern:** push the read-modify-write into the database as one
37
+ atomic operation (`UPDATE ... SET x = x + 1`, `INSERT ... ON CONFLICT`,
38
+ a transaction with the correct isolation level) instead of
39
+ read-in-app-then-write.
40
+
41
+ ## Hazard 2 — React state updater purity
42
+
43
+ **Pattern to grep for:** any `setX(prev => ...)` / reducer function body
44
+ that calls something other than a pure computation on `prev` — a side
45
+ effect (`deleteFileObject`, a network call, a mutation of an object
46
+ that also exists elsewhere) invoked *inside* the updater.
47
+
48
+ **Why it matters:** React may call an updater function more than once per
49
+ state change (Strict Mode, concurrent rendering, batched replays). Any
50
+ side effect inside it runs that many times too.
51
+
52
+ **Fix pattern:** the updater computes and returns the next state only.
53
+ Side effects happen in the event handler or an effect, outside the
54
+ updater, exactly once.
55
+
56
+ **Recurrence note:** if this fix has already been applied once in this
57
+ codebase in a similar handler, grep for the same shape (`set\w+\(\s*\w+\s*=>` followed
58
+ by a call other than array/object spread) across the whole diff, not just
59
+ the file that was fixed — the same author habit tends to repeat in sibling
60
+ handlers.
61
+
62
+ ## Hazard 3 — Async library footguns (assumed vs. actual contract)
63
+
64
+ **Pattern to grep for:** consecutive/rapid calls into an async library
65
+ API each with their own inline callback (`mutate(x, { onSuccess })`
66
+ called twice before the first resolves; `.then()` chains fired in a loop).
67
+
68
+ **Do not assume intuitive behavior** — check the library's actual
69
+ contract before shipping. Known real-world case: TanStack Query's
70
+ `mutate()` per-call `onSuccess`/`onError` callbacks are dropped for a
71
+ mutation if a **newer** call to the same mutation starts before the
72
+ earlier one resolves — only the latest call's callbacks run. If code
73
+ depends on every call's callback firing, that's already broken.
74
+
75
+ **Fix pattern:** either use `mutateAsync` + `await` in sequence, or read
76
+ the docs for exactly how "concurrent calls to the same mutation" are
77
+ handled before relying on per-call callbacks — do not assume the naive
78
+ "each call gets its own independent completion" model.
79
+
80
+ ---
81
+
82
+ ## What to do if a hazard is found
83
+
84
+ Treat it as **Deviation Rule 1 (auto-fix bugs)** — fix it now, before
85
+ SUMMARY.md, same as any other logic error. Do not defer it to PR review;
86
+ that is the exact failure mode this file exists to close.
87
+
88
+ ## Self-Check addition
89
+
90
+ Add to the SUMMARY.md Self-Check block:
91
+
92
+ ```
93
+ - [ ] Correctness hazard scan run (concurrency / state-updater purity / async-library contract) — N/A or PASSED
94
+ ```
95
+
96
+ If a hazard was found and fixed, note it under deviations with the
97
+ hazard name (e.g. "Hazard 2: state-updater purity — `deleteFileObject`
98
+ moved out of `setItems` updater in `FileList.tsx`").
@@ -86,6 +86,14 @@ For each task in sequence:
86
86
  This enables resume-work to see progress if executor crashes
87
87
  - Continue to next
88
88
 
89
+ ### Step 4.5: Correctness Hazard Self-Audit
90
+ If this plan's diff touched async code, shared/mutable state, or a
91
+ third-party library's async API, read
92
+ `.rcode/agents-rules/executor/correctness-hazard-scan.md` and run the scan
93
+ BEFORE Summary Creation — not after, and not left for PR review. Fix any
94
+ hazard found under Deviation Rule 1 and note it in SUMMARY.md deviations.
95
+ Skip this step entirely for plans that never touched those surfaces.
96
+
89
97
  ### Step 5: Summary Creation (Pattern A only)
90
98
  After all auto tasks complete:
91
99
  1. Create SUMMARY.md at `.planning/phases/XX-name/{phase}-{plan}-SUMMARY.md`
@@ -35,6 +35,12 @@ Before finalizing SUMMARY.md, verify plan execution was complete and correct.
35
35
  - [ ] All imports resolve (no broken paths)
36
36
  - [ ] All tests passing (if tests were modified)
37
37
 
38
+ ### Correctness Hazards (if diff touched async/state/mutation code)
39
+ - [ ] Read `.rcode/agents-rules/executor/correctness-hazard-scan.md` and ran the scan, or confirmed N/A (no async/shared-state/library-async surface touched)
40
+ - [ ] No read-modify-write against shared state without a transaction/atomic op/lock
41
+ - [ ] No side effects inside React state updater/reducer functions
42
+ - [ ] No reliance on an async library's per-call callback firing without checking its actual concurrent-call contract
43
+
38
44
  ### Success Criteria
39
45
  - [ ] Original plan `<success_criteria>` met
40
46
  - [ ] All `<verify>` checks pass
@@ -191,6 +197,7 @@ git stash
191
197
  - [ ] Task names in SUMMARY don't match plan
192
198
  - [ ] Deviations list is incomplete
193
199
  - [ ] You're unsure whether code works
200
+ - [ ] Diff touched async/shared-state/library-async code and the correctness hazard scan wasn't run
194
201
 
195
202
  **If any flag raised:**
196
203
  1. Don't create SUMMARY yet
@@ -217,6 +224,7 @@ Verification performed:
217
224
  - [ ] Success criteria verified
218
225
  - [ ] Stubs documented or resolved
219
226
  - [ ] Deviations documented
227
+ - [ ] Correctness hazard scan run (or N/A — no async/shared-state/library-async surface touched)
220
228
 
221
229
  **Issues encountered and resolved:**
222
230
  [List any red flags caught and how you fixed them]
@@ -0,0 +1,76 @@
1
+ # Orchestrator Contract — Raees (رئيس)
2
+
3
+ `@`-included by `plan.md` and `execute.md`. The session running those workflows
4
+ IS the orchestrator — rcode has no separate process dispatching on your behalf,
5
+ which is exactly why this role has to be adopted explicitly rather than assumed.
6
+ Raees is not spawned as a subagent here; a subagent cannot reliably spawn the
7
+ executors and planners these workflows need.
8
+
9
+ The persona file (`rcode/agents/rcode-orchestrator.md`) is the same contract for
10
+ direct `/rcode-orchestrator` invocation. Keep the two in step.
11
+
12
+ ## Open with the orientation banner
13
+
14
+ Before the first subagent is spawned, always:
15
+
16
+ ```
17
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18
+ rcode ► RAEES — {project}
19
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
20
+
21
+ Where you are {phase N — name} · {status} · {X/Y phases complete}
22
+ What I read {the files you actually opened this run}
23
+ What I'll do {2-4 numbered steps, each naming its owner agent}
24
+ What I need {decisions blocked on the user, or "nothing — starting now"}
25
+ ```
26
+
27
+ - **Fill it from the run's own pre-flight/INIT data, never from memory.** A banner
28
+ written from assumption is worse than no banner.
29
+ - **Name the owner of every step** — "Execute wave 1 (rcode-executor ×3)", not
30
+ "run the plans". The user is entitled to know who is spawned on their tokens.
31
+ - **Surface blockers before starting, not after.** If a later step needs a decision
32
+ the user hasn't made, put it in `What I need` and stop before that step.
33
+ - **Counts, not adjectives.** "2 sprints, 14 tasks, 3 waves" — never "a few things".
34
+ - **Banner on resumed and chained runs too.** A resume is exactly when the user has
35
+ lost track of where things stand.
36
+
37
+ ## Never implement
38
+
39
+ Your job is to dispatch, monitor, checkpoint, and report. The moment you edit a
40
+ file instead of dispatching, the run has no orchestrator.
41
+
42
+ > **STOP.** Spawn the owning agent with the plan as context.
43
+
44
+ Bypassing this produces a built project with no execution trace, no SUMMARY.md,
45
+ and a dashboard frozen at `planned` (issue #915). The same applies to planning: a
46
+ SPRINT.md with no `rcode-planner` Task() behind it was written by an orchestrator
47
+ that started doing the work.
48
+
49
+ ## Report every dispatch and every return
50
+
51
+ A silent orchestrator is indistinguishable from a stalled one. On each return,
52
+ state what the agent actually produced — not what it was asked to produce. Close
53
+ with what changed, what is still open, and the single next step.
54
+
55
+ Never claim a wave ran in parallel unless it did; report waves forced sequential
56
+ by file overlap as sequential.
57
+
58
+ ## Routing
59
+
60
+ Route from context, never from a keyword table alone: read the files the work
61
+ touches, the migrations it alters, and the decisions already recorded, then pick
62
+ the lens that evidence needs. `select-panel` is a signal, not the verdict — when
63
+ your reading disagrees with it, your reading wins and you name the file that made
64
+ you override.
65
+
66
+ ## The other orchestrator
67
+
68
+ `rcode-hussain-pm` owns WHAT the pieces are and who owns each; you own HOW the run
69
+ happens. Never both dispatch into the same run — if a decomposition exists, take
70
+ it and sequence it rather than re-cutting it.
71
+
72
+ ## Completion is not yours to declare
73
+
74
+ A phase is complete when the verification path says so. A `passed` VERIFICATION.md
75
+ with no `falsification: upheld` is self-certified — do not mark the phase complete
76
+ on it.
@@ -330,6 +330,44 @@ For each `<task>` in each plan:
330
330
  - If `<automated>` is absent with no Wave 0 dependency → **BLOCKING FAIL**
331
331
  - If `<automated>` says "MISSING", a Wave 0 task must reference the same test file path → **BLOCKING FAIL** if link broken
332
332
 
333
+ ### Check 8a2 — Always-Green Assertions
334
+
335
+ Scan every `<automated>` block for commands that can never fail:
336
+
337
+ ```bash
338
+ grep -nE 'grep +-[a-zA-Z]*v[a-zA-Z]* ' "${PHASE_DIR}"/*-SPRINT.md
339
+ ```
340
+
341
+ - Any `grep -qv` / `grep -vq` used as an "X must not appear" check → **BLOCKING FAIL**.
342
+ `-v` inverts per-line matching, so it exits 0 whenever ANY line fails to match — it
343
+ passes on almost every file, including one that DOES contain the forbidden pattern.
344
+ Required form: `! grep -q PATTERN FILE`.
345
+ - `test ! -f` is fine; `! test -f` is fine; a bare `test -f` used to assert absence → **BLOCKING FAIL**.
346
+
347
+ ### Check 8a3 — Red-First Evidence for Guard Tests
348
+
349
+ A **guard test** is any test whose purpose is to make a class of mistake
350
+ impossible: meta-tests, schema invariant tests, forbidden-pattern scans,
351
+ coverage/manifest tests, lint-rule tests. Identify guard-test tasks by their
352
+ `<files>` (a `meta-*`, `*-guard*`, `*-invariant*`, or `tests/unit/*-scan*` path)
353
+ or by an `<action>` that describes asserting a rule across a file set.
354
+
355
+ For each guard-test task:
356
+
357
+ - The `<action>` must state the red step — run the new test against the current
358
+ unfixed tree and record that it FAILS, before applying the fix. If it does not
359
+ → **BLOCKING FAIL**.
360
+ - If the task both adds the guard and fixes the violation, the red step must come
361
+ first in the action text, not after.
362
+
363
+ **Why this is blocking, not a warning:** a guard test that was never observed red
364
+ proves nothing. It may glob the wrong path, match nothing, or assert a condition
365
+ that is already true. It then reports green forever and the invariant it was
366
+ written to protect rots silently underneath it. Confirmed live: a scope-coverage
367
+ meta-test globbed only `src/app/api/**/route.ts`, so every Server Action was
368
+ invisible to it and an authorization chokepoint went unenforced across nine
369
+ phases with a fully green CI.
370
+
333
371
  ### Check 8b — Feedback Latency Assessment
334
372
 
335
373
  For each `<automated>` command:
@@ -6,8 +6,11 @@ entry point, can ever land on it. That gap is how a phase gets marked "passed"
6
6
  while the delivered page has no nav link, no sidebar entry, and no way in
7
7
  except typing its exact URL — code-complete, unusable.
8
8
 
9
- **When to run:** Any phase whose must-haves include a user-facing route, page,
10
- or screen (not API-only, not CLI-only, not backend/schema-only phases).
9
+ **When to run:** Steps 6b/10b apply to any phase whose must-haves include a
10
+ user-facing route, page, or screen. **Step 6c below applies to EVERY phase,
11
+ including API-only, CLI-only, and backend/schema-only ones** — the same failure
12
+ has a backend shape, and it is the more dangerous one because no missing nav
13
+ link makes it visible.
11
14
 
12
15
  ## Step 6b — Reachability (static)
13
16
 
@@ -29,6 +32,46 @@ gap, not a reason to skip this check. A phase that ships pages with nowhere to
29
32
  click from IS the gap. Report it as: "No app shell/navigation exists — every
30
33
  delivered page is orphaned from UI by definition."
31
34
 
35
+ ## Step 6c — Production reachability (every phase, no exceptions)
36
+
37
+ The UI version of this check asks "can a user reach this page". The backend
38
+ version asks the same question about code: **is the delivered module reachable
39
+ from a production entrypoint, or only from tests?**
40
+
41
+ For each non-UI artifact this phase delivered (a service, repository, engine,
42
+ job, or the function that is the phase's actual deliverable):
43
+
44
+ 1. List every importer of the module:
45
+ ```bash
46
+ grep -rn "from ['\"].*<module-basename>" src/ tests/ app/ lib/ 2>/dev/null
47
+ ```
48
+ 2. Classify each importer as production or test (`tests/`, `*.test.*`,
49
+ `*.spec.*`, `__tests__/` are tests).
50
+ 3. **If every importer is a test file → BLOCKING FAIL, classify as
51
+ ORPHANED-FROM-PRODUCTION.** The phase's own tests pass because they import
52
+ the implementation directly; the application calls something else, or
53
+ nothing.
54
+
55
+ Then verify the opposite direction, which is how the dangerous version hides:
56
+
57
+ 4. Find what production actually calls for this phase's behaviour (the action,
58
+ route handler, or command the user triggers) and read it. Does it call the
59
+ delivered module, or does it re-implement the behaviour inline?
60
+ 5. **Two implementations of the same behaviour side by side — one tested and
61
+ unreachable, one shipped and untested — is a BLOCKING FAIL**, even when the
62
+ suite is fully green. Name both paths in VERIFICATION.md.
63
+
64
+ **Why this is blocking:** confirmed live on a real project — a phase whose whole
65
+ purpose was "closing a cycle produces an immutable snapshot" shipped with the
66
+ production close-action setting a status field and returning, while the
67
+ snapshot-producing service had exactly one importer in the repo: its own test.
68
+ Green suite, verified phase, feature that never ran. The audit-attribution seam
69
+ failed the same way, in the same commit range, for the same reason.
70
+
71
+ **This check subsumes "the tests pass".** A passing test that imports the
72
+ implementation directly proves the implementation works. It proves nothing about
73
+ whether anything calls it.
74
+
32
75
  ## Step 10b — Live smoke check (dynamic)
33
76
 
34
77
  Static reachability (6b) can be fooled by a nav link that renders but is
@@ -104,6 +104,13 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
104
104
  return String(p?.number ?? p?.id ?? p?.name ?? '').trim();
105
105
  }
106
106
 
107
+ // Phase-number normalizer. Disk dirs are historically zero-padded
108
+ // ("03-evidence-ledger") while ROADMAP tables and state.json use bare
109
+ // integers ("3"), so every diskByNum lookup MUST go through this or the
110
+ // phase silently reports "disk state unavailable" forever — no amount of
111
+ // `state sync --from-disk` fixes it, because sync is not what's broken.
112
+ const normNum = (k) => String(k ?? '').trim().replace(/^0+(\d)/, '$1');
113
+
107
114
  function walkPhaseDirs() {
108
115
  if (!fs.existsSync(phasesDir)) return {};
109
116
  const byNum = {};
@@ -113,7 +120,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
113
120
  // Phase 14 / #476 — \d+ supports high-N phase dirs (1000+).
114
121
  const numMatch = entry.match(/^(\d+(?:\.\d+)?)/);
115
122
  if (!numMatch) continue;
116
- const num = numMatch[1];
123
+ const num = normNum(numMatch[1]);
117
124
  const files = fs.readdirSync(full);
118
125
  byNum[num] = {
119
126
  path: full,
@@ -133,7 +140,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
133
140
  function enforceStrictGate(insightsList) {
134
141
  if (!strictMode) return;
135
142
  const blocking = (insightsList || []).filter(i =>
136
- i && (i.kind === 'drift' || i.kind === 'undercount') && i.severity !== 'info'
143
+ i && (i.kind === 'drift' || i.kind === 'undercount' || i.kind === 'stale-state') && i.severity !== 'info'
137
144
  );
138
145
  if (blocking.length === 0) return;
139
146
  process.stderr.write('✖ State drift detected — state.json is out of sync with disk.\n');
@@ -159,7 +166,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
159
166
  // Undercount: phases that exist on disk but not in state.
160
167
  // Accept any of `number`, `id`, or `name` as the phase identifier — the codebase historically writes different fields.
161
168
  // Also normalize "07" / "7" / 7 to a comparable form.
162
- const norm = (k) => String(k ?? '').replace(/^0+(\d)/, '$1');
169
+ const norm = normNum;
163
170
  const statePhaseNums = new Set(statePhases.map(p => norm(phaseKey(p))));
164
171
  const diskPhaseNums = Object.keys(diskByNum);
165
172
  const missingFromState = diskPhaseNums.filter(n => !statePhaseNums.has(norm(n)));
@@ -171,6 +178,28 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
171
178
  });
172
179
  }
173
180
 
181
+ // Stale-state: the INVERSE of phantom-complete — the phase has a passing
182
+ // VERIFICATION.md on disk but state.json still says planned/in_progress.
183
+ // Every other detector here is tuned to catch OVER-claiming; nothing
184
+ // caught under-reporting, which is why a real project ran nine phases with
185
+ // `executions: 0` and a dashboard reading 1/13 while the disk said 10/13.
186
+ // Silence in the one direction is not safety, it is a blind spot.
187
+ const staleState = [];
188
+ for (const p of statePhases) {
189
+ const st = String(p.status ?? '').toLowerCase();
190
+ if (st === 'complete' || st === 'completed' || st === 'done' || st === 'verified' || p.completed) continue;
191
+ const disk = diskByNum[normNum(phaseKey(p))];
192
+ if (!disk || !disk.has_verification) continue;
193
+ staleState.push(normNum(phaseKey(p)));
194
+ }
195
+ if (staleState.length > 0) {
196
+ insights.push({
197
+ kind: 'stale-state',
198
+ severity: 'warn',
199
+ message: `${staleState.length} phase(s) have a VERIFICATION.md on disk but are not marked complete in state.json: ${staleState.slice(0, 5).join(', ')}. Completion was never written back — your real progress is higher than this dashboard shows. Run: node .rcode/bin/rcode-tools.cjs state sync --from-disk`,
200
+ });
201
+ }
202
+
174
203
  // Phantom-complete: phase claimed Complete (in ROADMAP or state) but missing
175
204
  // PLAN.md AND SUMMARY.md on disk. User-visible bug: /rcode-status would
176
205
  // happily report 'all complete' while /rcode-audit correctly flagged the
@@ -188,7 +217,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
188
217
  for (const p of roadmapPhases) if (claimedComplete(p)) completeKeys.add(norm(phaseKey(p)));
189
218
  for (const p of statePhases) if (claimedComplete(p)) completeKeys.add(norm(phaseKey(p)));
190
219
  for (const k of completeKeys) {
191
- const disk = diskByNum[k] || diskByNum[k.padStart(2, '0')];
220
+ const disk = diskByNum[normNum(k)];
192
221
  // Only flag when the phase dir EXISTS — purely-state-only entries are a
193
222
  // separate problem (drift/undercount above). Here we want claim-vs-files.
194
223
  if (!disk) continue;
@@ -224,7 +253,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
224
253
  });
225
254
  for (const p of inProgressPhases) {
226
255
  const key = norm(phaseKey(p));
227
- const disk = diskByNum[key] || diskByNum[key.padStart(2, '0')];
256
+ const disk = diskByNum[normNum(key)];
228
257
  if (!disk) continue;
229
258
  const dirName = disk.dirName;
230
259
  const gitArgs = ['log', '--oneline', '--since=7 days ago', '--', `.planning/phases/${dirName}/`];
@@ -265,7 +294,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
265
294
  };
266
295
  const pendingExec = statePhases.filter(p => {
267
296
  if (isPhaseDone(p)) return false;
268
- const disk = diskByNum[phaseKey(p)];
297
+ const disk = diskByNum[normNum(phaseKey(p))];
269
298
  return disk && disk.plan_count > disk.summary_count;
270
299
  }).slice(0, 3);
271
300
  for (const p of pendingExec) {
@@ -285,7 +314,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
285
314
  const inProgressNoPlan = statePhases
286
315
  .filter(p => (p.status === 'in_progress' || p.status === 'in-progress'))
287
316
  .filter(p => {
288
- const disk = diskByNum[phaseKey(p)];
317
+ const disk = diskByNum[normNum(phaseKey(p))];
289
318
  return !disk || disk.plan_count === 0;
290
319
  })
291
320
  .slice(0, 2);
@@ -299,7 +328,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
299
328
  if (allDone) {
300
329
  // Count unverified phases (complete but no VERIFICATION.md on disk)
301
330
  const unverifiedCount = statePhases.filter(p => {
302
- const disk = diskByNum[phaseKey(p)];
331
+ const disk = diskByNum[normNum(phaseKey(p))];
303
332
  return (p.status === 'complete' || p.completed) && disk && !disk.has_verification;
304
333
  }).length;
305
334
  const hasDrift = (insights || []).some(i => i.kind === 'roadmap-drift' || (i.message && i.message.includes('ROADMAP')));
@@ -339,12 +368,11 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
339
368
  */
340
369
  function computeWeightedProgress(stPhases, diskMap) {
341
370
  if (!stPhases.length) return { weighted: 0, pct: 0 };
342
- const norm = (k) => String(k ?? '').replace(/^0+(\d)/, '$1');
343
371
  let sum = 0;
344
372
  for (const p of stPhases) {
345
- const k = norm(phaseKey(p));
373
+ const k = normNum(phaseKey(p));
346
374
  if (p.status === 'complete' || p.completed) { sum += 1; continue; }
347
- const disk = diskMap[k] || diskMap[phaseKey(p)];
375
+ const disk = diskMap[k];
348
376
  if (!disk) continue;
349
377
  if (disk.summary_count > 0) { sum += 1; continue; }
350
378
  if (disk.has_verification) { sum += 0.85; continue; }
@@ -410,7 +438,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
410
438
  phases: (() => {
411
439
  // Prefer ROADMAP-parsed phases when available; fall back to state.phases
412
440
  // when the roadmap doesn't use a parseable format. Normalize "07" / "7" / 7.
413
- const norm = (k) => String(k ?? '').replace(/^0+(\d)/, '$1');
441
+ const norm = normNum;
414
442
  const source = roadmapPhases.length > 0 ? roadmapPhases : statePhases.map(p => ({
415
443
  number: phaseKey(p),
416
444
  name: p.name || '',
@@ -424,7 +452,7 @@ function cmdProgress(args, { PROJECT_ROOT, RCODE_DIR, PLANNING_DIR }) {
424
452
  ...p,
425
453
  number: k,
426
454
  status: p.status || (sp && sp.status) || null,
427
- disk: diskByNum[k] || null,
455
+ disk: diskByNum[normNum(k)] || null,
428
456
  in_state: !!sp,
429
457
  };
430
458
  });