@hanzlaa/rcode 4.3.3 → 4.4.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 (113) hide show
  1. package/CONTRIBUTING.md +1 -1
  2. package/README.md +10 -4
  3. package/cli/doctor.js +21 -3
  4. package/cli/index.js +5 -1
  5. package/cli/install.js +2 -2
  6. package/cli/postinstall.js +1 -1
  7. package/cli/uninstall.js +48 -6
  8. package/dist/rcode.js +174 -170
  9. package/package.json +12 -22
  10. package/rcode/agents/rcode-codebase-mapper.md +12 -0
  11. package/rcode/agents/rcode-phase-researcher.md +13 -0
  12. package/rcode/agents/rcode-project-researcher.md +32 -0
  13. package/rcode/agents/rcode-roadmapper.md +15 -0
  14. package/rcode/agents/rules/planner/common-patterns.md +40 -0
  15. package/rcode/bin/lib/roadmap.cjs +184 -11
  16. package/rcode/bin/rcode-hooks.cjs +102 -151
  17. package/rcode/bin/rcode-tools.cjs +669 -55
  18. package/rcode/data/intent-table.json +87 -0
  19. package/rcode/modules/seo.yaml +22 -0
  20. package/rcode/references/project-types.yaml +75 -0
  21. package/rcode/skills/SKILLS_INDEX.md +17 -0
  22. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/SKILL.md +4 -21
  23. package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/heartbeat.sh +0 -0
  24. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/SKILL.md +60 -1
  25. package/rcode/skills/core/rcode-init/scripts/__pycache__/rcode_init.cpython-38.pyc +0 -0
  26. package/rcode/skills/core/rcode-init/scripts/tests/__pycache__/test_rcode_init.cpython-38.pyc +0 -0
  27. package/rcode/skills/seo/module.yaml +20 -0
  28. package/rcode/skills/seo/on-page-seo-auditor/SKILL.md +308 -0
  29. package/rcode/skills/seo/on-page-seo-auditor/references/audit-example.md +109 -0
  30. package/rcode/skills/seo/on-page-seo-auditor/references/audit-templates.md +427 -0
  31. package/rcode/skills/seo/on-page-seo-auditor/references/bulk-audit-playbook.md +116 -0
  32. package/rcode/skills/seo/on-page-seo-auditor/references/scoring-rubric.md +525 -0
  33. package/rcode/skills/seo/rank-and-rent-local-seo/SKILL.md +94 -0
  34. package/rcode/skills/seo/rank-and-rent-local-seo/rules/city-matrix-and-pages.md +76 -0
  35. package/rcode/skills/seo/rank-and-rent-local-seo/rules/monetization.md +70 -0
  36. package/rcode/skills/seo/rank-and-rent-local-seo/rules/niche-selection.md +60 -0
  37. package/rcode/skills/seo/rank-and-rent-local-seo/rules/subniche-discovery.md +66 -0
  38. package/rcode/skills/seo/rank-and-rent-local-seo/templates/service-city-page.md +36 -0
  39. package/rcode/skills/seo/rank-and-rent-local-seo/templates/subniche-research-prompt.md +34 -0
  40. package/rcode/skills/seo/seo-audit/SKILL.md +412 -0
  41. package/rcode/skills/seo/seo-audit/evals/evals.json +136 -0
  42. package/rcode/skills/seo/seo-audit/references/ai-writing-detection.md +200 -0
  43. package/rcode/skills/seo/seo-content-factory/SKILL.md +130 -0
  44. package/rcode/skills/seo/seo-content-factory/rules/agents.md +80 -0
  45. package/rcode/skills/seo/seo-content-factory/rules/herdr-wave-mapping.md +80 -0
  46. package/rcode/skills/seo/seo-content-factory/rules/pipeline.md +102 -0
  47. package/rcode/skills/seo/seo-content-factory/rules/programmatic-pages.md +107 -0
  48. package/rcode/skills/seo/seo-content-factory/rules/quality-gates.md +68 -0
  49. package/rcode/skills/seo/seo-content-factory/rules/weekly-cadence.md +61 -0
  50. package/rcode/skills/seo/seo-content-factory/templates/BACKLOG.md +23 -0
  51. package/rcode/skills/seo/seo-content-factory/templates/agent-prompts/01-funnel-A1-A4.md +63 -0
  52. package/rcode/skills/seo/seo-content-factory/templates/agent-prompts/02-fanout-editorial-A5-A6-A8.md +28 -0
  53. package/rcode/skills/seo/seo-content-factory/templates/agent-prompts/03-fanout-programmatic-A7-A6-A8.md +25 -0
  54. package/rcode/skills/seo/seo-content-factory/templates/agent-prompts/04-loop-A9-A10.md +34 -0
  55. package/rcode/skills/seo/seo-content-factory/templates/article.mdx +39 -0
  56. package/rcode/skills/seo/seo-content-factory/templates/cluster.schema.json +52 -0
  57. package/rcode/skills/seo/seo-content-factory/templates/content-brief.md +51 -0
  58. package/rcode/skills/seo/seo-content-factory/templates/jsonld-builders.ts +49 -0
  59. package/rcode/skills/seo/seo-content-factory/templates/keywords.csv +13 -0
  60. package/rcode/skills/seo/seo-content-factory/templates/programmatic-page.tsx +58 -0
  61. package/rcode/skills/seo/seo-content-writer/SKILL.md +263 -0
  62. package/rcode/skills/seo/seo-content-writer/references/content-structure-templates.md +875 -0
  63. package/rcode/skills/seo/seo-content-writer/references/instructions-detail.md +239 -0
  64. package/rcode/skills/seo/seo-content-writer/references/seo-writing-checklist.md +250 -0
  65. package/rcode/skills/seo/seo-content-writer/references/title-formulas.md +339 -0
  66. package/rcode/skills/seo/seo-growth-orchestrator/SKILL.md +79 -0
  67. package/rcode/skills/seo/seo-growth-orchestrator/rules/backlinks.md +29 -0
  68. package/rcode/skills/seo/seo-growth-orchestrator/rules/content-engine.md +27 -0
  69. package/rcode/skills/seo/seo-growth-orchestrator/rules/dos-and-donts.md +40 -0
  70. package/rcode/skills/seo/seo-growth-orchestrator/rules/goals-protocol.md +34 -0
  71. package/rcode/skills/seo/seo-growth-orchestrator/rules/local-seo-stack.md +35 -0
  72. package/rcode/skills/seo/seo-growth-orchestrator/rules/mrr-sprint.md +27 -0
  73. package/rcode/skills/seo/seo-growth-orchestrator/rules/tools.md +19 -0
  74. package/rcode/skills/seo/seo-growth-orchestrator/templates/backlink-research.md +29 -0
  75. package/rcode/skills/seo/seo-growth-orchestrator/templates/goal-protocol.md +35 -0
  76. package/rcode/skills/seo/seo-growth-orchestrator/templates/local-citations.md +24 -0
  77. package/rcode/skills/seo/seo-growth-orchestrator/templates/local-city-pages.md +27 -0
  78. package/rcode/skills/seo/seo-growth-orchestrator/templates/local-gbp-categories.md +23 -0
  79. package/rcode/skills/seo/seo-growth-orchestrator/templates/local-gsc-goldmine.md +27 -0
  80. package/rcode/skills/seo/seo-growth-orchestrator/templates/local-review-mining.md +26 -0
  81. package/rcode/skills/seo/seo-growth-orchestrator/templates/mrr-30day-plan.md +19 -0
  82. package/rcode/skills/seo/seo-growth-orchestrator/templates/mrr-cut.md +13 -0
  83. package/rcode/skills/seo/seo-growth-orchestrator/templates/mrr-opportunity-finder.md +17 -0
  84. package/rcode/skills/seo/seo-growth-orchestrator/templates/mrr-role-model.md +18 -0
  85. package/rcode/skills/seo/seo-site-builder/SKILL.md +149 -0
  86. package/rcode/skills/seo/seo-site-builder/rules/01-niche-discovery.md +64 -0
  87. package/rcode/skills/seo/seo-site-builder/rules/02-deep-research.md +164 -0
  88. package/rcode/skills/seo/seo-site-builder/rules/03-keyword-strategy.md +82 -0
  89. package/rcode/skills/seo/seo-site-builder/rules/04-content-plan.md +173 -0
  90. package/rcode/skills/seo/seo-site-builder/rules/05-site-build.md +187 -0
  91. package/rcode/skills/seo/seo-site-builder/rules/06-yolo.md +127 -0
  92. package/rcode/skills/seo/seo-site-builder/templates/article-stub.mdx +62 -0
  93. package/rcode/skills/seo/seo-site-builder/templates/image-prompt.md +97 -0
  94. package/rcode/skills/seo/seo-site-builder/templates/keyword-export.md +45 -0
  95. package/rcode/skills/seo/seo-site-builder/templates/niche-scorecard.md +28 -0
  96. package/rcode/skills/seo/technical-seo-checker/SKILL.md +329 -0
  97. package/rcode/skills/seo/technical-seo-checker/references/bulk-audit-playbook.md +118 -0
  98. package/rcode/skills/seo/technical-seo-checker/references/ecommerce-platform-patterns.md +166 -0
  99. package/rcode/skills/seo/technical-seo-checker/references/http-status-codes.md +705 -0
  100. package/rcode/skills/seo/technical-seo-checker/references/llm-crawler-handling.md +257 -0
  101. package/rcode/skills/seo/technical-seo-checker/references/pre-migration-playbook.md +137 -0
  102. package/rcode/skills/seo/technical-seo-checker/references/robots-txt-reference.md +717 -0
  103. package/rcode/skills/seo/technical-seo-checker/references/technical-audit-example.md +169 -0
  104. package/rcode/skills/seo/technical-seo-checker/references/technical-audit-templates.md +436 -0
  105. package/rcode/templates/settings-hooks.json +4 -0
  106. package/rcode/workflows/do.md +16 -0
  107. package/rcode/workflows/execute-sprint.md +22 -0
  108. package/rcode/workflows/execute.md +17 -3
  109. package/rcode/workflows/new-milestone.md +21 -9
  110. package/rcode/workflows/new-project-create-roadmap.md +2 -0
  111. package/rcode/workflows/new-project-roadmap.md +2 -0
  112. package/rcode/workflows/new-project.md +3 -0
  113. package/rcode/workflows/plan.md +15 -2
package/package.json CHANGED
@@ -1,21 +1,11 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "4.3.3",
3
+ "version": "4.4.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": {
7
7
  "rcode": "dist/rcode.js"
8
8
  },
9
- "scripts": {
10
- "dashboard": "node server/dashboard.js",
11
- "test": "node --test",
12
- "test:ci": "node --test --test-reporter=spec",
13
- "postinstall": "node cli/postinstall.js",
14
- "build:cli": "node scripts/build.cjs",
15
- "build": "node scripts/build.cjs",
16
- "prepack": "node scripts/build.cjs",
17
- "dogfood": "bash scripts/dogfood-check.sh"
18
- },
19
9
  "files": [
20
10
  "cli/",
21
11
  "rcode/",
@@ -61,16 +51,6 @@
61
51
  "engines": {
62
52
  "node": ">=18.0.0"
63
53
  },
64
- "devDependencies": {
65
- "@clack/prompts": "^0.9.1",
66
- "diff": "^8.0.4",
67
- "esbuild": "^0.25.0",
68
- "fast-glob": "^3.3.3",
69
- "nanospinner": "^1.2.2",
70
- "picocolors": "^1.1.1",
71
- "semver": "^7.7.1",
72
- "zod": "^3.24.0"
73
- },
74
54
  "publishConfig": {
75
55
  "access": "public"
76
56
  },
@@ -79,5 +59,15 @@
79
59
  },
80
60
  "optionalDependencies": {
81
61
  "@lydell/node-pty": "1.2.0-beta.12"
62
+ },
63
+ "scripts": {
64
+ "dashboard": "node server/dashboard.js",
65
+ "test": "node --test",
66
+ "test:ci": "node --test --test-reporter=spec",
67
+ "postinstall": "node cli/postinstall.js",
68
+ "build:cli": "node scripts/build.cjs",
69
+ "build": "node scripts/build.cjs",
70
+ "dogfood": "bash scripts/dogfood-check.sh",
71
+ "test:skills": "node test/skill-snapshot.test.cjs"
82
72
  }
83
- }
73
+ }
@@ -30,6 +30,18 @@ Your job: Explore thoroughly across ALL source roots (never assume `src/` is the
30
30
  If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context.
31
31
  </role>
32
32
 
33
+ ## Timeout Strategy
34
+
35
+ If this scan takes longer than expected (many large files, deep directories):
36
+
37
+ 1. **After 3 minutes without writing an output file:** Write a partial CONVENTIONS.md or TESTING.md with what has been discovered so far, clearly marked `## [PARTIAL — scan timed out]` at the top. A partial file is better than no file.
38
+
39
+ 2. **Scope reduction:** If a directory scan is taking too long, limit depth: scan top-level + src/ + lib/ + app/ only. Skip node_modules, dist, .next, .git, coverage automatically.
40
+
41
+ 3. **Never emit only a greeting and go silent.** If you cannot complete the full scan, write whatever you have found to the output file immediately and explain what was skipped.
42
+
43
+ 4. **On stall recovery:** If restarted after a stall, check which output files already exist under `.planning/maps/`. Skip any that are already written (non-empty). Only write the missing ones.
44
+
33
45
  <why_this_matters>
34
46
  **These documents are consumed by other rcode commands:**
35
47
 
@@ -83,6 +83,19 @@ Named rules. Cite by name when applying.
83
83
  6. **Write RESEARCH.md** — sections in order: User Constraints → Standard Stack → Architecture Patterns → Don't Hand-Roll → Common Pitfalls → Code Examples.
84
84
  7. **Return to orchestrator** — RESEARCH.md path in the return message.
85
85
 
86
+ ## Iterative Retrieval Protocol
87
+
88
+ After your first search pass, evaluate what's still unknown:
89
+ 1. List the open questions your initial searches did NOT answer
90
+ 2. If there are ≥2 open questions, run a second search pass targeting those gaps
91
+ 3. After the second pass, if critical questions remain (marked MUST-KNOW in your output), run a third pass
92
+ 4. Stop when: (a) all MUST-KNOW gaps are filled, (b) 3 passes are complete, or (c) the same result appears in 2+ searches (diminishing returns signal)
93
+
94
+ Log each pass as:
95
+ Pass 1: [query list] → [results summary]
96
+ Pass 2: [gap-filling queries] → [results summary]
97
+ Pass 3 (if needed): [remaining gaps] → [results summary]
98
+
86
99
  ## Anti-Patterns / Refuse List
87
100
 
88
101
  - **Never omit User Constraints** — the planner enforces them; missing constraints cause plan/user conflicts.
@@ -28,6 +28,25 @@ Your files feed the roadmap:
28
28
  **Be comprehensive but opinionated.** "Use X because Y" not "Options are X, Y, Z."
29
29
  </role>
30
30
 
31
+ ## content-site Mode
32
+
33
+ When `project_type` is `content-site` (detected from PROJECT.md or the new-project classification step), skip the generic STACK.md / FEATURES.md / ARCHITECTURE.md research flow. Instead produce:
34
+
35
+ **KEYWORDS.md** — seed keyword list with intent classification per keyword (informational / commercial / transactional / navigational). Call `claude-seo:seo-cluster` if available to generate the SERP-overlap cluster map. Minimum: 20 keywords across at least 3 intent categories.
36
+
37
+ **CLUSTERS.md** — hub/spoke content architecture. Each cluster = one hub page + N spoke pages. Derive from keyword intent groupings. Include: page type per node (hub / spoke / programmatic / location), primary keyword, secondary keywords (≤3), and estimated internal link targets (hub links to all spokes; each spoke links back to hub + 2 sibling spokes).
38
+
39
+ **PITFALLS.md** — SEO-specific pitfalls for this project. Must include at minimum:
40
+ 1. Thin-content risk (pages under word floor indexed before enrichment)
41
+ 2. Canonical drift (programmatic pages with near-duplicate URLs getting split authority)
42
+ 3. Noindex strategy (when to gate pages as noindex until enriched vs. index immediately)
43
+ 4. Duplicate title/H1 trap (city×service matrix producing identical meta across locations)
44
+ 5. Crawl budget waste (low-value pages consuming crawl allocation)
45
+
46
+ Reference `seo-content-factory` for the full production pipeline and `seo-growth-orchestrator` for strategy orchestration.
47
+
48
+ Do NOT produce STACK.md, FEATURES.md, or ARCHITECTURE.md for content-site projects. Technology stack research is secondary — the content pipeline is the product.
49
+
31
50
  <philosophy>
32
51
 
33
52
  ## Honest Reporting
@@ -81,6 +100,19 @@ Named rules. Cite by name when applying.
81
100
  - `PITFALLS.md` — risk flags for deeper research
82
101
  6. **Return to orchestrator** — list all written files.
83
102
 
103
+ ## Iterative Retrieval Protocol
104
+
105
+ After your first search pass, evaluate what's still unknown:
106
+ 1. List the open questions your initial searches did NOT answer
107
+ 2. If there are ≥2 open questions, run a second search pass targeting those gaps
108
+ 3. After the second pass, if critical questions remain (marked MUST-KNOW in your output), run a third pass
109
+ 4. Stop when: (a) all MUST-KNOW gaps are filled, (b) 3 passes are complete, or (c) the same result appears in 2+ searches (diminishing returns signal)
110
+
111
+ Log each pass as:
112
+ Pass 1: [query list] → [results summary]
113
+ Pass 2: [gap-filling queries] → [results summary]
114
+ Pass 3 (if needed): [remaining gaps] → [results summary]
115
+
84
116
  ## Anti-Patterns / Refuse List
85
117
 
86
118
  - **Never present a menu of options** when a clear recommendation can be made. Per Confident-but-honest.
@@ -46,3 +46,18 @@ Named rules. Cite by name when applying.
46
46
  - **Never over-phase** — for a solo developer project, 3-7 phases is typical. 15 phases is corporate theater.
47
47
  - **Never start planning before reading the research files** — phases without research produce wrong phase structures.
48
48
 
49
+ ## Scope Constraint
50
+
51
+ **rcode-roadmapper creates ONLY:**
52
+ - `ROADMAP.md` — the project roadmap with phases, requirements, and success criteria
53
+ - One `PHASE.md` per phase directory (if phase-level detail files are requested)
54
+ - `STATE.md` — project memory initialization
55
+ - `REQUIREMENTS.md` traceability updates
56
+
57
+ **rcode-roadmapper MUST NOT create:**
58
+ - `SPRINT.md` — sprint planning is handled exclusively by `/rcode-plan`
59
+ - `PLAN.md` — plan files are out of scope for this agent
60
+ - Any other sprint-level or task-level planning files
61
+
62
+ If you find yourself about to write a `SPRINT.md` file, **STOP** — that file is out of scope for this agent. Sprint planning is the responsibility of `/rcode-plan`, not `/rcode-new-project` or the roadmapper.
63
+
@@ -371,3 +371,43 @@ If your work doesn't match any pattern:
371
371
  4. Document the rationale in the plan
372
372
 
373
373
  Novel patterns are fine — just ensure they're still decomposed correctly.
374
+
375
+ ---
376
+
377
+ ## Known Stack Constraints
378
+
379
+ Hard-won version constraints and format requirements that the planner must honour
380
+ when generating `devDependencies`, scaffold files, or code snippets. Getting these
381
+ wrong causes runtime crashes or build failures that are hard to diagnose.
382
+
383
+ ### Mobile / Expo
384
+
385
+ **Expo SDK 55+ requires TypeScript ≥ 5.4**
386
+
387
+ `tsconfig.base.json` in Expo SDK 55 sets `"module": "preserve"`, which TypeScript
388
+ < 5.4 does not support and will error on at compile time.
389
+
390
+ - When generating `devDependencies` for any Expo SDK 50+ project, always specify
391
+ `"typescript": "~5.8.0"` (or at minimum `"~5.4.0"`).
392
+ - **Never** emit `"typescript": "~5.3.x"` for an Expo project.
393
+
394
+ ### Drizzle ORM — Migrations Barrel Format (v0.30.x+)
395
+
396
+ The migrations barrel passed to `drizzle-orm/expo-sqlite` (and similar drivers)
397
+ must be an **object**, not an array.
398
+
399
+ ```ts
400
+ // CORRECT — object spread
401
+ import * as m0000 from './migrations/0000_bored_microchip.sql';
402
+ export const migrations = { m0000 };
403
+
404
+ // WRONG — array causes a runtime crash
405
+ export const migrations = [m0000];
406
+ ```
407
+
408
+ Additional rules:
409
+ - SQL filenames are auto-generated by `drizzle-kit generate` (e.g. `0000_bored_microchip.sql`).
410
+ Never hardcode SQL filenames in plans — instruct the executor to match actual
411
+ `drizzle-kit` output or use a dynamic import glob.
412
+ - The barrel file must be re-generated (or updated) whenever `drizzle-kit generate`
413
+ produces a new migration file.
@@ -14,6 +14,69 @@ function roadmapPathFor(projectRoot) {
14
14
  return path.join(projectRoot, '.planning', 'ROADMAP.md');
15
15
  }
16
16
 
17
+ /**
18
+ * Resolve the active ROADMAP file path for a project.
19
+ *
20
+ * Convention detection (#734):
21
+ * 1. Single file — .planning/ROADMAP.md (primary / existing behaviour)
22
+ * 2. Per-milestone — .planning/ROADMAP-M<N>.md, e.g. ROADMAP-M3.md
23
+ * Selects the file that matches state.current_milestone when present;
24
+ * falls back to the lexicographically last file when no state is set.
25
+ *
26
+ * Returns { path, convention, milestone } where convention is
27
+ * 'single' | 'per-milestone'.
28
+ */
29
+ function resolveRoadmapPath(projectRoot) {
30
+ const single = path.join(projectRoot, '.planning', 'ROADMAP.md');
31
+ if (fs.existsSync(single)) {
32
+ return { path: single, convention: 'single', milestone: null };
33
+ }
34
+
35
+ // Scan for ROADMAP-M*.md files
36
+ const planningDir = path.join(projectRoot, '.planning');
37
+ let milestoneFiles = [];
38
+ if (fs.existsSync(planningDir)) {
39
+ milestoneFiles = fs.readdirSync(planningDir)
40
+ .filter(f => /^ROADMAP-M\d+\.md$/i.test(f))
41
+ .sort();
42
+ }
43
+
44
+ if (milestoneFiles.length === 0) {
45
+ // Neither convention found — return the canonical path (callers handle missing file)
46
+ return { path: single, convention: 'single', milestone: null };
47
+ }
48
+
49
+ // Read state.json to find current_milestone
50
+ const stateJsonPath = path.join(projectRoot, '.rcode', 'state.json');
51
+ let currentMilestone = null;
52
+ if (fs.existsSync(stateJsonPath)) {
53
+ try {
54
+ const st = JSON.parse(fs.readFileSync(stateJsonPath, 'utf8'));
55
+ currentMilestone = st.current_milestone || null;
56
+ } catch { /* non-fatal */ }
57
+ }
58
+
59
+ // Try to match by milestone id (e.g. "M3", "3", "milestone-3")
60
+ let matched = null;
61
+ if (currentMilestone) {
62
+ const numMatch = String(currentMilestone).match(/(\d+)/);
63
+ const milestoneNum = numMatch ? numMatch[1] : null;
64
+ if (milestoneNum) {
65
+ matched = milestoneFiles.find(f => {
66
+ const m = f.match(/^ROADMAP-M(\d+)\.md$/i);
67
+ return m && m[1] === milestoneNum;
68
+ });
69
+ }
70
+ }
71
+
72
+ // Fall back to the last file in sorted order
73
+ if (!matched) matched = milestoneFiles[milestoneFiles.length - 1];
74
+
75
+ const resolvedPath = path.join(planningDir, matched);
76
+ const milestoneLabel = matched.replace(/^ROADMAP-M(\d+)\.md$/i, 'M$1');
77
+ return { path: resolvedPath, convention: 'per-milestone', milestone: milestoneLabel };
78
+ }
79
+
17
80
  /**
18
81
  * Extract all phase sections. Each returned entry includes:
19
82
  * number, name, goal, section (raw markdown slice), headerIndex, sectionEnd
@@ -66,11 +129,30 @@ function extractPhases(content) {
66
129
 
67
130
  function parseRequirements(section) {
68
131
  // Matches both bold-style (**Requirements:**) and heading-style (### Requirements)
69
- const match = section.match(/(?:\*\*Requirements(?::\*\*|\*\*:)|#{1,4}\s*Requirements\s*:?)[^\n]*\n((?:\s*(?:\d+\.|[-*])\s+[^\n]+\n?)+)/i);
70
- if (!match) return [];
71
- return match[1].split('\n')
72
- .map((l) => l.replace(/^\s*(?:\d+\.|[-*])\s+/, '').trim())
73
- .filter(Boolean);
132
+ // followed by a list block.
133
+ const listMatch = section.match(/(?:\*\*Requirements(?::\*\*|\*\*:)|#{1,4}\s*Requirements\s*:?)[^\n]*\n((?:\s*(?:\d+\.|[-*])\s+[^\n]+\n?)+)/i);
134
+ if (listMatch) {
135
+ return listMatch[1].split('\n')
136
+ .map((l) => l.replace(/^\s*(?:\d+\.|[-*])\s+/, '').trim())
137
+ .filter(Boolean);
138
+ }
139
+
140
+ // Also capture REQ-IDs from inline lines like:
141
+ // **REQs:** REQ-004, REQ-010, REQ-020
142
+ // Requirements: REQ-001, REQ-002
143
+ // **Covers:** REQ-001, REQ-003
144
+ // Collect every line in the section that contains REQ-\d+ patterns.
145
+ const seen = new Set();
146
+ const out = [];
147
+ const reqIdRe = /\bREQ-[A-Z0-9][A-Z0-9-]*\b/g;
148
+ for (const line of section.split('\n')) {
149
+ if (!/REQ-/i.test(line)) continue;
150
+ const ids = line.match(reqIdRe) || [];
151
+ for (const id of ids) {
152
+ if (!seen.has(id)) { seen.add(id); out.push(id); }
153
+ }
154
+ }
155
+ return out;
74
156
  }
75
157
 
76
158
  function parseSuccessCriteria(section) {
@@ -124,7 +206,7 @@ function phaseStatus(section) {
124
206
  function normalizePhaseNum(n) { return parseInt(String(n).replace(/^0+/, '') || '0', 10); }
125
207
 
126
208
  function cmdGetPhase(projectRoot, phaseNum, opts = {}) {
127
- const rp = roadmapPathFor(projectRoot);
209
+ const { path: rp } = resolveRoadmapPath(projectRoot);
128
210
  if (!fs.existsSync(rp)) {
129
211
  return { found: false, error: 'ROADMAP.md not found', phase_number: phaseNum };
130
212
  }
@@ -169,19 +251,67 @@ function cmdGetPhase(projectRoot, phaseNum, opts = {}) {
169
251
  }
170
252
 
171
253
  function cmdListPhases(projectRoot) {
172
- const rp = roadmapPathFor(projectRoot);
254
+ const { path: rp } = resolveRoadmapPath(projectRoot);
173
255
  if (!fs.existsSync(rp)) return [];
174
256
  const content = fs.readFileSync(rp, 'utf8');
175
257
  // Phase 10 / #466 — prefer the parsed Status field from extractPhases over
176
258
  // the legacy phaseStatus() heuristic, which only matched literal "completed"
177
259
  // in the header and missed our **Status:** Complete convention. Fall back to
178
260
  // phaseStatus() only when extractPhases couldn't parse a Status line.
179
- return extractPhases(content).map((p) => ({
261
+ const phases = extractPhases(content).map((p) => ({
180
262
  number: p.number,
181
263
  name: p.name,
182
264
  status: p.status === 'unknown' ? phaseStatus(p.section) : p.status,
183
265
  status_raw: p.status_raw,
184
266
  }));
267
+
268
+ // Fix #856 — state.current_phase is authoritative for which phase is in_progress.
269
+ // ROADMAP.md may carry a stale "Active" marker from a previous phase after
270
+ // `state set-phase` has moved forward. Read state.json and override statuses so
271
+ // exactly one phase (the current one) gets in_progress, and any other phase that
272
+ // ROADMAP still labels active/in_progress is demoted to planned.
273
+ const stateJsonPath = path.join(projectRoot, '.rcode', 'state.json');
274
+ let currentPhase = null;
275
+ if (fs.existsSync(stateJsonPath)) {
276
+ try {
277
+ const st = JSON.parse(fs.readFileSync(stateJsonPath, 'utf8'));
278
+ currentPhase = st.current_phase || null;
279
+ } catch { /* non-fatal — fall through to ROADMAP-only statuses */ }
280
+ }
281
+
282
+ if (!currentPhase) return phases;
283
+
284
+ // Extract the numeric prefix from current_phase (e.g. "02-correctness-fixes" → "2").
285
+ const numPrefixMatch = String(currentPhase).match(/^(\d+)/);
286
+ const leadingNum = numPrefixMatch ? String(parseInt(numPrefixMatch[1], 10)) : null;
287
+
288
+ let matched = false;
289
+ return phases.map((p) => {
290
+ // Normalize the ROADMAP phase number (strip leading zeros) for comparison.
291
+ const normNum = String(parseInt(String(p.number), 10));
292
+ const isCurrentPhase =
293
+ !matched && (
294
+ p.name === currentPhase ||
295
+ (leadingNum !== null && normNum === leadingNum) ||
296
+ String(p.number) === String(currentPhase)
297
+ );
298
+ if (isCurrentPhase) {
299
+ matched = true;
300
+ // Only promote to in_progress if not already complete/closed.
301
+ const s = String(p.status || '').toLowerCase();
302
+ if (s !== 'complete' && s !== 'closed') {
303
+ return { ...p, status: 'in_progress' };
304
+ }
305
+ return p;
306
+ }
307
+ // Demote any other phase that ROADMAP still marks active/in_progress —
308
+ // state.current_phase is the single source of truth for the active phase.
309
+ const s = String(p.status || '').toLowerCase();
310
+ if (s === 'active' || s === 'in_progress') {
311
+ return { ...p, status: 'planned' };
312
+ }
313
+ return p;
314
+ });
185
315
  }
186
316
 
187
317
  /**
@@ -226,7 +356,7 @@ function cmdUpdatePlanProgress(projectRoot, phaseNum, planId, status) {
226
356
  if (!planId || !status) {
227
357
  throw new Error('Usage: roadmap update-plan-progress <phase> <plan-id> <status>');
228
358
  }
229
- const rp = roadmapPathFor(projectRoot);
359
+ const { path: rp } = resolveRoadmapPath(projectRoot);
230
360
  if (!fs.existsSync(rp)) {
231
361
  return { updated: false, error: 'ROADMAP.md not found' };
232
362
  }
@@ -278,7 +408,7 @@ function cmdUpdatePlanProgress(projectRoot, phaseNum, planId, status) {
278
408
  * roadmap summary — overview of phase counts and active phase.
279
409
  */
280
410
  function cmdSummary(projectRoot) {
281
- const rp = roadmapPathFor(projectRoot);
411
+ const { path: rp } = resolveRoadmapPath(projectRoot);
282
412
  if (!fs.existsSync(rp)) return { found: false, error: 'ROADMAP.md not found' };
283
413
  const phases = cmdListPhases(projectRoot);
284
414
  const total = phases.length;
@@ -297,6 +427,8 @@ function cmdSummary(projectRoot) {
297
427
  * roadmap clear — archive and scaffold.
298
428
  */
299
429
  function cmdClear(projectRoot) {
430
+ // Clear always targets the canonical single-file path — per-milestone files
431
+ // are managed per-milestone and not cleared globally.
300
432
  const rp = roadmapPathFor(projectRoot);
301
433
  // Derive current version from config.yaml or default v1.0
302
434
  const configPath = path.join(projectRoot, '.rcode', 'config.yaml');
@@ -322,6 +454,43 @@ function cmdClear(projectRoot) {
322
454
  return { cleared: true, archived, version, roadmap: rp };
323
455
  }
324
456
 
457
+ /**
458
+ * roadmap detect — report which ROADMAP convention is in use (#734).
459
+ *
460
+ * Outputs a human-readable line and returns structured JSON.
461
+ * Examples:
462
+ * single: .planning/ROADMAP.md
463
+ * per-milestone: .planning/ROADMAP-M3.md (current)
464
+ */
465
+ function cmdDetect(projectRoot) {
466
+ const resolved = resolveRoadmapPath(projectRoot);
467
+ const exists = fs.existsSync(resolved.path);
468
+ const rel = path.relative(projectRoot, resolved.path);
469
+
470
+ if (resolved.convention === 'single') {
471
+ const label = exists ? `single: ${rel}` : `single: ${rel} (not found)`;
472
+ return { __raw: label, convention: 'single', path: rel, exists, milestone: null };
473
+ }
474
+
475
+ // per-milestone — also list all available milestone files
476
+ const planningDir = path.join(projectRoot, '.planning');
477
+ const allFiles = fs.existsSync(planningDir)
478
+ ? fs.readdirSync(planningDir).filter(f => /^ROADMAP-M\d+\.md$/i.test(f)).sort()
479
+ : [];
480
+
481
+ const label = exists
482
+ ? `per-milestone: ${rel} (current)`
483
+ : `per-milestone: ${rel} (current, not found)`;
484
+ return {
485
+ __raw: label,
486
+ convention: 'per-milestone',
487
+ path: rel,
488
+ exists,
489
+ milestone: resolved.milestone,
490
+ all_milestone_files: allFiles.map(f => path.join('.planning', f)),
491
+ };
492
+ }
493
+
325
494
  function dispatch(projectRoot, subArgs) {
326
495
  const sub = subArgs[0];
327
496
  const rest = subArgs.slice(1);
@@ -344,8 +513,10 @@ function dispatch(projectRoot, subArgs) {
344
513
  return cmdClear(projectRoot);
345
514
  case 'summary':
346
515
  return cmdSummary(projectRoot);
516
+ case 'detect':
517
+ return cmdDetect(projectRoot);
347
518
  default:
348
- throw new Error(`Unknown roadmap subcommand: ${sub}. Valid: get-phase, list-phases, update-plan-progress, clear, summary`);
519
+ throw new Error(`Unknown roadmap subcommand: ${sub}. Valid: get-phase, list-phases, update-plan-progress, clear, summary, detect`);
349
520
  }
350
521
  }
351
522
 
@@ -356,4 +527,6 @@ module.exports = {
356
527
  cmdUpdatePlanProgress,
357
528
  cmdClear,
358
529
  cmdSummary,
530
+ cmdDetect,
531
+ resolveRoadmapPath,
359
532
  };