@hanzlaa/rcode 2.1.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 (676) hide show
  1. package/AGENTS.md +120 -0
  2. package/CLAUDE.md +120 -0
  3. package/CONTRIBUTING.md +298 -0
  4. package/README.md +436 -0
  5. package/cli/config.js +142 -0
  6. package/cli/context.js +213 -0
  7. package/cli/dashboard.js +38 -0
  8. package/cli/digest.js +66 -0
  9. package/cli/doctor.js +283 -0
  10. package/cli/github-sync.js +1016 -0
  11. package/cli/index.js +113 -0
  12. package/cli/install.js +946 -0
  13. package/cli/lib/config.cjs +334 -0
  14. package/cli/lib/fsutil.cjs +76 -0
  15. package/cli/lib/github.cjs +365 -0
  16. package/cli/lib/manifest.cjs +240 -0
  17. package/cli/lib/memory-bank.cjs +348 -0
  18. package/cli/lib/model-profiles.cjs +169 -0
  19. package/cli/lib/prompts.cjs +355 -0
  20. package/cli/postinstall.js +32 -0
  21. package/cli/set-mode.js +94 -0
  22. package/cli/set-profile.js +80 -0
  23. package/cli/show-model.js +82 -0
  24. package/cli/team.js +35 -0
  25. package/cli/tiers.js +49 -0
  26. package/cli/uninstall.js +600 -0
  27. package/cli/update.js +373 -0
  28. package/package.json +60 -0
  29. package/rihal/agents/rihal-advisor-researcher.md +116 -0
  30. package/rihal/agents/rihal-ahmed.md +66 -0
  31. package/rihal/agents/rihal-architect.md +79 -0
  32. package/rihal/agents/rihal-assumptions-analyzer.md +117 -0
  33. package/rihal/agents/rihal-code-fixer.md +74 -0
  34. package/rihal/agents/rihal-code-reviewer.md +75 -0
  35. package/rihal/agents/rihal-codebase-mapper.md +170 -0
  36. package/rihal/agents/rihal-debugger.md +140 -0
  37. package/rihal/agents/rihal-deviation-analyzer.md +74 -0
  38. package/rihal/agents/rihal-docs-auditor.md +77 -0
  39. package/rihal/agents/rihal-edge-case-hunter.md +75 -0
  40. package/rihal/agents/rihal-executor.md +113 -0
  41. package/rihal/agents/rihal-fatima.md +68 -0
  42. package/rihal/agents/rihal-haitham.md +75 -0
  43. package/rihal/agents/rihal-hanzla.md +59 -0
  44. package/rihal/agents/rihal-hussain-pm.md +82 -0
  45. package/rihal/agents/rihal-integration-checker.md +455 -0
  46. package/rihal/agents/rihal-khalid.md +59 -0
  47. package/rihal/agents/rihal-layla.md +57 -0
  48. package/rihal/agents/rihal-mariam.md +58 -0
  49. package/rihal/agents/rihal-nasser.md +57 -0
  50. package/rihal/agents/rihal-noor.md +60 -0
  51. package/rihal/agents/rihal-nyquist-auditor.md +182 -0
  52. package/rihal/agents/rihal-omar.md +57 -0
  53. package/rihal/agents/rihal-phase-researcher.md +84 -0
  54. package/rihal/agents/rihal-planner.md +176 -0
  55. package/rihal/agents/rihal-profiler.md +74 -0
  56. package/rihal/agents/rihal-project-researcher.md +80 -0
  57. package/rihal/agents/rihal-remediation-planner.md +78 -0
  58. package/rihal/agents/rihal-research-synthesizer.md +253 -0
  59. package/rihal/agents/rihal-roadmapper.md +73 -0
  60. package/rihal/agents/rihal-sadiq.md +72 -0
  61. package/rihal/agents/rihal-security-adversary.md +82 -0
  62. package/rihal/agents/rihal-security-auditor.md +78 -0
  63. package/rihal/agents/rihal-sprint-checker.md +124 -0
  64. package/rihal/agents/rihal-tech-writer.md +80 -0
  65. package/rihal/agents/rihal-ui-auditor.md +81 -0
  66. package/rihal/agents/rihal-ui-designer.md +6 -0
  67. package/rihal/agents/rihal-ux-designer.md +83 -0
  68. package/rihal/agents/rihal-verifier.md +124 -0
  69. package/rihal/agents/rihal-waleed.md +60 -0
  70. package/rihal/agents/rihal-yousef.md +78 -0
  71. package/rihal/agents/rihal-zahra.md +62 -0
  72. package/rihal/agents/rihal-zayd.md +78 -0
  73. package/rihal/agents/rules/codebase-mapper/detailed-guide.md +615 -0
  74. package/rihal/agents/rules/debugger/checkpoint-recovery.md +272 -0
  75. package/rihal/agents/rules/debugger/debug-session-state.md +261 -0
  76. package/rihal/agents/rules/debugger/hypothesis-templates.md +330 -0
  77. package/rihal/agents/rules/debugger/investigation-protocol.md +298 -0
  78. package/rihal/agents/rules/debugger/scientific-method.md +317 -0
  79. package/rihal/agents/rules/executor/authentication-gates.md +202 -0
  80. package/rihal/agents/rules/executor/deviation-rules.md +191 -0
  81. package/rihal/agents/rules/executor/execution-flow.md +116 -0
  82. package/rihal/agents/rules/executor/self-check.md +241 -0
  83. package/rihal/agents/rules/executor/stub-detection.md +267 -0
  84. package/rihal/agents/rules/executor/summary-creation.md +76 -0
  85. package/rihal/agents/rules/executor/task-commit-protocol.md +283 -0
  86. package/rihal/agents/rules/executor/tdd-flow.md +294 -0
  87. package/rihal/agents/rules/phase-researcher/detailed-guide.md +628 -0
  88. package/rihal/agents/rules/planner/common-patterns.md +373 -0
  89. package/rihal/agents/rules/planner/dependency-analysis.md +195 -0
  90. package/rihal/agents/rules/planner/goal-backward-thinking.md +220 -0
  91. package/rihal/agents/rules/planner/sprint-verification.md +202 -0
  92. package/rihal/agents/rules/planner/task-templates.md +296 -0
  93. package/rihal/agents/rules/project-researcher/detailed-guide.md +589 -0
  94. package/rihal/agents/rules/roadmapper/detailed-guide.md +620 -0
  95. package/rihal/agents/rules/sprint-checker/dimensions.md +414 -0
  96. package/rihal/agents/rules/sprint-checker/process.md +377 -0
  97. package/rihal/agents/rules/verifier/anti-patterns.md +94 -0
  98. package/rihal/agents/rules/verifier/artifact-verification.md +69 -0
  99. package/rihal/agents/rules/verifier/behavioral-spot-checks.md +49 -0
  100. package/rihal/agents/rules/verifier/context-loading.md +84 -0
  101. package/rihal/agents/rules/verifier/data-flow-trace.md +65 -0
  102. package/rihal/agents/rules/verifier/gap-output.md +51 -0
  103. package/rihal/agents/rules/verifier/key-links.md +56 -0
  104. package/rihal/agents/rules/verifier/requirements-coverage.md +28 -0
  105. package/rihal/agents/rules/verifier/verification-report.md +131 -0
  106. package/rihal/bin/lib/code-references.cjs +196 -0
  107. package/rihal/bin/lib/config.cjs +146 -0
  108. package/rihal/bin/lib/council-panel.cjs +501 -0
  109. package/rihal/bin/lib/roadmap.cjs +256 -0
  110. package/rihal/bin/lib/verify.cjs +118 -0
  111. package/rihal/bin/rihal-hooks.cjs +204 -0
  112. package/rihal/bin/rihal-tools.cjs +3554 -0
  113. package/rihal/brain/README.md +38 -0
  114. package/rihal/brain/best-practices/no-autonomous-bypass.md +37 -0
  115. package/rihal/brain/best-practices/research-citation-rule.md +39 -0
  116. package/rihal/brain/best-practices/state-sync-rule.md +43 -0
  117. package/rihal/brain/sources.yaml +59 -0
  118. package/rihal/commands/add-phase.md +18 -0
  119. package/rihal/commands/add-tests.md +18 -0
  120. package/rihal/commands/add-todo.md +8 -0
  121. package/rihal/commands/analyze-dependencies.md +11 -0
  122. package/rihal/commands/audit-fix.md +14 -0
  123. package/rihal/commands/audit-milestone.md +12 -0
  124. package/rihal/commands/audit-uat.md +18 -0
  125. package/rihal/commands/autonomous.md +19 -0
  126. package/rihal/commands/brainstorm.md +11 -0
  127. package/rihal/commands/chain.md +8 -0
  128. package/rihal/commands/check-implementation-readiness.md +8 -0
  129. package/rihal/commands/check-todos.md +18 -0
  130. package/rihal/commands/cleanup.md +18 -0
  131. package/rihal/commands/code-review-fix.md +14 -0
  132. package/rihal/commands/code-review.md +14 -0
  133. package/rihal/commands/complete-milestone.md +12 -0
  134. package/rihal/commands/config.md +8 -0
  135. package/rihal/commands/correct-course.md +8 -0
  136. package/rihal/commands/council.md +25 -0
  137. package/rihal/commands/create-epics-and-stories.md +8 -0
  138. package/rihal/commands/create-story.md +8 -0
  139. package/rihal/commands/dashboard.md +10 -0
  140. package/rihal/commands/debug.md +8 -0
  141. package/rihal/commands/decisions.md +10 -0
  142. package/rihal/commands/dev-story.md +8 -0
  143. package/rihal/commands/diff.md +10 -0
  144. package/rihal/commands/discuss-phase-power.md +11 -0
  145. package/rihal/commands/discuss-phase.md +19 -0
  146. package/rihal/commands/discuss.md +23 -0
  147. package/rihal/commands/do.md +22 -0
  148. package/rihal/commands/docs-update.md +14 -0
  149. package/rihal/commands/document-project.md +8 -0
  150. package/rihal/commands/enable-hooks.md +11 -0
  151. package/rihal/commands/execute-sprint.md +13 -0
  152. package/rihal/commands/execute.md +19 -0
  153. package/rihal/commands/explore.md +14 -0
  154. package/rihal/commands/export-to-github.md +11 -0
  155. package/rihal/commands/forensics.md +11 -0
  156. package/rihal/commands/from-template.md +11 -0
  157. package/rihal/commands/health.md +10 -0
  158. package/rihal/commands/help.md +8 -0
  159. package/rihal/commands/import.md +12 -0
  160. package/rihal/commands/inbox.md +12 -0
  161. package/rihal/commands/init.md +14 -0
  162. package/rihal/commands/insert-phase.md +11 -0
  163. package/rihal/commands/install.md +10 -0
  164. package/rihal/commands/karpathy-audit.md +12 -0
  165. package/rihal/commands/list-plans.md +11 -0
  166. package/rihal/commands/list-workspaces.md +10 -0
  167. package/rihal/commands/map-codebase.md +14 -0
  168. package/rihal/commands/milestone-summary.md +11 -0
  169. package/rihal/commands/new-milestone.md +12 -0
  170. package/rihal/commands/new-project-research.md +11 -0
  171. package/rihal/commands/new-project-roadmap.md +11 -0
  172. package/rihal/commands/new-project.md +13 -0
  173. package/rihal/commands/new-workspace.md +12 -0
  174. package/rihal/commands/next.md +19 -0
  175. package/rihal/commands/note.md +12 -0
  176. package/rihal/commands/notify-test.md +10 -0
  177. package/rihal/commands/pause-work.md +8 -0
  178. package/rihal/commands/plan-milestone-gaps.md +18 -0
  179. package/rihal/commands/plan.md +19 -0
  180. package/rihal/commands/plant-seed.md +18 -0
  181. package/rihal/commands/pr-branch.md +18 -0
  182. package/rihal/commands/profile-user.md +8 -0
  183. package/rihal/commands/progress.md +19 -0
  184. package/rihal/commands/quick.md +14 -0
  185. package/rihal/commands/remove-phase.md +18 -0
  186. package/rihal/commands/remove-workspace.md +11 -0
  187. package/rihal/commands/replay.md +11 -0
  188. package/rihal/commands/report.md +10 -0
  189. package/rihal/commands/rerun.md +11 -0
  190. package/rihal/commands/research-phase.md +18 -0
  191. package/rihal/commands/resume-work.md +8 -0
  192. package/rihal/commands/review-adversarial.md +8 -0
  193. package/rihal/commands/review-edge-case-hunter.md +8 -0
  194. package/rihal/commands/review.md +18 -0
  195. package/rihal/commands/scan.md +14 -0
  196. package/rihal/commands/secure-phase.md +14 -0
  197. package/rihal/commands/session-report.md +10 -0
  198. package/rihal/commands/settings.md +8 -0
  199. package/rihal/commands/ship.md +18 -0
  200. package/rihal/commands/show.md +10 -0
  201. package/rihal/commands/sprint-planning.md +20 -0
  202. package/rihal/commands/sprint-status.md +21 -0
  203. package/rihal/commands/stats.md +10 -0
  204. package/rihal/commands/status.md +21 -0
  205. package/rihal/commands/ui-phase.md +8 -0
  206. package/rihal/commands/ui-review.md +8 -0
  207. package/rihal/commands/undo.md +14 -0
  208. package/rihal/commands/update.md +11 -0
  209. package/rihal/commands/validate-phase.md +18 -0
  210. package/rihal/commands/verify-phase.md +18 -0
  211. package/rihal/commands/verify-work.md +19 -0
  212. package/rihal/commands/why.md +10 -0
  213. package/rihal/commands/workstream.md +11 -0
  214. package/rihal/config/model-profiles.json +226 -0
  215. package/rihal/config/model-profiles.schema.json +36 -0
  216. package/rihal/config.yaml +39 -0
  217. package/rihal/digests/README.md +50 -0
  218. package/rihal/digests/fatima.md +24 -0
  219. package/rihal/digests/hussain-pm.md +24 -0
  220. package/rihal/digests/mariam.md +24 -0
  221. package/rihal/digests/sadiq.md +24 -0
  222. package/rihal/digests/waleed.md +24 -0
  223. package/rihal/modules/core.yaml +101 -0
  224. package/rihal/modules/discovery.yaml +50 -0
  225. package/rihal/modules/execution.yaml +66 -0
  226. package/rihal/references/brain-methods.csv +9 -0
  227. package/rihal/references/checklist-architect.md +146 -0
  228. package/rihal/references/checklist-change.md +136 -0
  229. package/rihal/references/checklist-pm.md +154 -0
  230. package/rihal/references/checklist-po-master.md +100 -0
  231. package/rihal/references/checklist-story-dod.md +75 -0
  232. package/rihal/references/checklist-story-draft.md +53 -0
  233. package/rihal/references/checkpoints-index.md +53 -0
  234. package/rihal/references/checkpoints.md +778 -0
  235. package/rihal/references/codebase-grounding.md +76 -0
  236. package/rihal/references/command-redirect-format.md +62 -0
  237. package/rihal/references/commit-conventions.md +125 -0
  238. package/rihal/references/common-bug-patterns-index.md +44 -0
  239. package/rihal/references/common-bug-patterns.md +621 -0
  240. package/rihal/references/context-budget.md +104 -0
  241. package/rihal/references/continuation-format.md +249 -0
  242. package/rihal/references/council-protocol.md +91 -0
  243. package/rihal/references/domain-probes.md +213 -0
  244. package/rihal/references/elicitation-methods.csv +16 -0
  245. package/rihal/references/execution-protocol.md +155 -0
  246. package/rihal/references/gate-prompts.md +212 -0
  247. package/rihal/references/gates.md +127 -0
  248. package/rihal/references/git-integration.md +159 -0
  249. package/rihal/references/git-planning-commit.md +185 -0
  250. package/rihal/references/karpathy-guidelines.md +79 -0
  251. package/rihal/references/model-profiles.md +90 -0
  252. package/rihal/references/no-unauthorized-git-ops.md +73 -0
  253. package/rihal/references/output-format.md +319 -0
  254. package/rihal/references/output-realism.md +52 -0
  255. package/rihal/references/project-types.yaml +270 -0
  256. package/rihal/references/questioning.md +163 -0
  257. package/rihal/references/response-style.md +81 -0
  258. package/rihal/references/state-schema.md +366 -0
  259. package/rihal/references/tdd.md +263 -0
  260. package/rihal/references/thinking-models-debug.md +126 -0
  261. package/rihal/references/thinking-models-planning.md +127 -0
  262. package/rihal/references/ui-brand.md +254 -0
  263. package/rihal/references/verification-patterns-index.md +76 -0
  264. package/rihal/references/verification-patterns.md +612 -0
  265. package/rihal/references/workstream-flag.md +166 -0
  266. package/rihal/skills/SKILLS_INDEX.md +114 -0
  267. package/rihal/skills/_shared/no-autonomous-bypass.md +37 -0
  268. package/rihal/skills/_shared/research-citation-rule.md +39 -0
  269. package/rihal/skills/_shared/state-sync-rule.md +43 -0
  270. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/SKILL.md +31 -0
  271. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/domain-steps/step-01-init.md +137 -0
  272. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/domain-steps/step-02-domain-analysis.md +229 -0
  273. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/domain-steps/step-03-competitive-landscape.md +238 -0
  274. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/domain-steps/step-04-regulatory-focus.md +206 -0
  275. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/domain-steps/step-05-technical-trends.md +234 -0
  276. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/domain-steps/step-06-research-synthesis.md +444 -0
  277. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/research.template.md +29 -0
  278. package/rihal/skills/actions/1-analysis/research/rihal-domain-research/workflow.md +49 -0
  279. package/rihal/skills/actions/1-analysis/research/rihal-market-research/SKILL.md +30 -0
  280. package/rihal/skills/actions/1-analysis/research/rihal-market-research/research.template.md +29 -0
  281. package/rihal/skills/actions/1-analysis/research/rihal-market-research/steps/step-01-init.md +184 -0
  282. package/rihal/skills/actions/1-analysis/research/rihal-market-research/steps/step-02-customer-behavior.md +239 -0
  283. package/rihal/skills/actions/1-analysis/research/rihal-market-research/steps/step-03-customer-pain-points.md +251 -0
  284. package/rihal/skills/actions/1-analysis/research/rihal-market-research/steps/step-04-customer-decisions.md +261 -0
  285. package/rihal/skills/actions/1-analysis/research/rihal-market-research/steps/step-05-competitive-analysis.md +173 -0
  286. package/rihal/skills/actions/1-analysis/research/rihal-market-research/steps/step-06-research-completion.md +478 -0
  287. package/rihal/skills/actions/1-analysis/research/rihal-market-research/workflow.md +49 -0
  288. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/SKILL.md +31 -0
  289. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/research.template.md +29 -0
  290. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/technical-steps/step-01-init.md +137 -0
  291. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/technical-steps/step-02-technical-overview.md +239 -0
  292. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/technical-steps/step-03-integration-patterns.md +248 -0
  293. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/technical-steps/step-04-architectural-patterns.md +202 -0
  294. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/technical-steps/step-05-implementation-research.md +233 -0
  295. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/technical-steps/step-06-research-synthesis.md +487 -0
  296. package/rihal/skills/actions/1-analysis/research/rihal-technical-research/workflow.md +50 -0
  297. package/rihal/skills/actions/1-analysis/rihal-document-project/SKILL.md +30 -0
  298. package/rihal/skills/actions/1-analysis/rihal-document-project/checklist.md +245 -0
  299. package/rihal/skills/actions/1-analysis/rihal-document-project/documentation-requirements.csv +12 -0
  300. package/rihal/skills/actions/1-analysis/rihal-document-project/instructions.md +128 -0
  301. package/rihal/skills/actions/1-analysis/rihal-document-project/templates/deep-dive-template.md +345 -0
  302. package/rihal/skills/actions/1-analysis/rihal-document-project/templates/index-template.md +169 -0
  303. package/rihal/skills/actions/1-analysis/rihal-document-project/templates/project-overview-template.md +103 -0
  304. package/rihal/skills/actions/1-analysis/rihal-document-project/templates/project-scan-report-schema.json +160 -0
  305. package/rihal/skills/actions/1-analysis/rihal-document-project/templates/source-tree-template.md +135 -0
  306. package/rihal/skills/actions/1-analysis/rihal-document-project/workflow.md +27 -0
  307. package/rihal/skills/actions/1-analysis/rihal-document-project/workflows/deep-dive-instructions.md +299 -0
  308. package/rihal/skills/actions/1-analysis/rihal-document-project/workflows/deep-dive-workflow.md +34 -0
  309. package/rihal/skills/actions/1-analysis/rihal-document-project/workflows/full-scan-instructions.md +1107 -0
  310. package/rihal/skills/actions/1-analysis/rihal-document-project/workflows/full-scan-workflow.md +34 -0
  311. package/rihal/skills/actions/1-analysis/rihal-prfaq/SKILL.md +120 -0
  312. package/rihal/skills/actions/1-analysis/rihal-prfaq/agents/artifact-analyzer.md +60 -0
  313. package/rihal/skills/actions/1-analysis/rihal-prfaq/agents/web-researcher.md +49 -0
  314. package/rihal/skills/actions/1-analysis/rihal-prfaq/assets/prfaq-template.md +62 -0
  315. package/rihal/skills/actions/1-analysis/rihal-prfaq/references/customer-faq.md +55 -0
  316. package/rihal/skills/actions/1-analysis/rihal-prfaq/references/internal-faq.md +51 -0
  317. package/rihal/skills/actions/1-analysis/rihal-prfaq/references/press-release.md +60 -0
  318. package/rihal/skills/actions/1-analysis/rihal-prfaq/references/verdict.md +79 -0
  319. package/rihal/skills/actions/1-analysis/rihal-prfaq/rihal-manifest.json +16 -0
  320. package/rihal/skills/actions/1-analysis/rihal-product-brief/SKILL.md +112 -0
  321. package/rihal/skills/actions/1-analysis/rihal-product-brief/agents/artifact-analyzer.md +60 -0
  322. package/rihal/skills/actions/1-analysis/rihal-product-brief/agents/opportunity-reviewer.md +44 -0
  323. package/rihal/skills/actions/1-analysis/rihal-product-brief/agents/skeptic-reviewer.md +44 -0
  324. package/rihal/skills/actions/1-analysis/rihal-product-brief/agents/web-researcher.md +49 -0
  325. package/rihal/skills/actions/1-analysis/rihal-product-brief/prompts/contextual-discovery.md +57 -0
  326. package/rihal/skills/actions/1-analysis/rihal-product-brief/prompts/draft-and-review.md +86 -0
  327. package/rihal/skills/actions/1-analysis/rihal-product-brief/prompts/finalize.md +75 -0
  328. package/rihal/skills/actions/1-analysis/rihal-product-brief/prompts/guided-elicitation.md +70 -0
  329. package/rihal/skills/actions/1-analysis/rihal-product-brief/resources/brief-template.md +60 -0
  330. package/rihal/skills/actions/1-analysis/rihal-product-brief/rihal-manifest.json +17 -0
  331. package/rihal/skills/actions/2-plan/rihal-create-epics-and-stories/SKILL.md +30 -0
  332. package/rihal/skills/actions/2-plan/rihal-create-epics-and-stories/steps/step-01-validate-prerequisites.md +255 -0
  333. package/rihal/skills/actions/2-plan/rihal-create-epics-and-stories/steps/step-02-design-epics.md +212 -0
  334. package/rihal/skills/actions/2-plan/rihal-create-epics-and-stories/steps/step-03-create-stories.md +255 -0
  335. package/rihal/skills/actions/2-plan/rihal-create-epics-and-stories/steps/step-04-final-validation.md +131 -0
  336. package/rihal/skills/actions/2-plan/rihal-create-epics-and-stories/templates/epics-template.md +61 -0
  337. package/rihal/skills/actions/2-plan/rihal-create-epics-and-stories/workflow.md +54 -0
  338. package/rihal/skills/actions/2-plan/rihal-create-milestone/SKILL.md +39 -0
  339. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/README.md +30 -0
  340. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-01-init.md +62 -0
  341. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-02-outcomes.md +64 -0
  342. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-03-sequencing.md +65 -0
  343. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-04-windows.md +60 -0
  344. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-05-kill-criteria.md +59 -0
  345. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-06-phase-stubs.md +56 -0
  346. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-07-backlog.md +44 -0
  347. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-08-write-roadmap.md +58 -0
  348. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-09-state-sync.md +62 -0
  349. package/rihal/skills/actions/2-plan/rihal-create-milestone/steps/step-10-complete.md +56 -0
  350. package/rihal/skills/actions/2-plan/rihal-create-milestone/workflow.md +93 -0
  351. package/rihal/skills/actions/2-plan/rihal-create-prd/SKILL.md +40 -0
  352. package/rihal/skills/actions/2-plan/rihal-create-prd/data/domain-complexity.csv +15 -0
  353. package/rihal/skills/actions/2-plan/rihal-create-prd/data/prd-purpose.md +197 -0
  354. package/rihal/skills/actions/2-plan/rihal-create-prd/data/project-types.csv +11 -0
  355. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-01-init.md +178 -0
  356. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-01b-continue.md +161 -0
  357. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-02-discovery.md +208 -0
  358. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-02b-vision.md +142 -0
  359. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-02c-executive-summary.md +158 -0
  360. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-03-success.md +214 -0
  361. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-04-journeys.md +201 -0
  362. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-05-domain.md +194 -0
  363. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-06-innovation.md +211 -0
  364. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-07-project-type.md +222 -0
  365. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-08-scoping.md +216 -0
  366. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-09-functional.md +219 -0
  367. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-10-nonfunctional.md +230 -0
  368. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-11-polish.md +221 -0
  369. package/rihal/skills/actions/2-plan/rihal-create-prd/steps-c/step-12-complete.md +115 -0
  370. package/rihal/skills/actions/2-plan/rihal-create-prd/templates/prd-template.md +10 -0
  371. package/rihal/skills/actions/2-plan/rihal-create-prd/workflow.md +64 -0
  372. package/rihal/skills/actions/2-plan/rihal-create-story/SKILL.md +31 -0
  373. package/rihal/skills/actions/2-plan/rihal-create-story/checklist.md +357 -0
  374. package/rihal/skills/actions/2-plan/rihal-create-story/discover-inputs.md +88 -0
  375. package/rihal/skills/actions/2-plan/rihal-create-story/template.md +49 -0
  376. package/rihal/skills/actions/2-plan/rihal-create-story/workflow.md +380 -0
  377. package/rihal/skills/actions/2-plan/rihal-create-ux-design/SKILL.md +31 -0
  378. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-01-init.md +135 -0
  379. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-01b-continue.md +127 -0
  380. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-02-discovery.md +190 -0
  381. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-03-core-experience.md +217 -0
  382. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-04-emotional-response.md +220 -0
  383. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-05-inspiration.md +235 -0
  384. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-06-design-system.md +253 -0
  385. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-07-defining-experience.md +255 -0
  386. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-08-visual-foundation.md +225 -0
  387. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-09-design-directions.md +225 -0
  388. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-10-user-journeys.md +242 -0
  389. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-11-component-strategy.md +249 -0
  390. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-12-ux-patterns.md +238 -0
  391. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-13-responsive-accessibility.md +265 -0
  392. package/rihal/skills/actions/2-plan/rihal-create-ux-design/steps/step-14-complete.md +171 -0
  393. package/rihal/skills/actions/2-plan/rihal-create-ux-design/ux-design-template.md +13 -0
  394. package/rihal/skills/actions/2-plan/rihal-create-ux-design/workflow.md +36 -0
  395. package/rihal/skills/actions/2-plan/rihal-edit-prd/SKILL.md +30 -0
  396. package/rihal/skills/actions/2-plan/rihal-edit-prd/steps-e/step-e-01-discovery.md +242 -0
  397. package/rihal/skills/actions/2-plan/rihal-edit-prd/steps-e/step-e-01b-legacy-conversion.md +204 -0
  398. package/rihal/skills/actions/2-plan/rihal-edit-prd/steps-e/step-e-02-review.md +245 -0
  399. package/rihal/skills/actions/2-plan/rihal-edit-prd/steps-e/step-e-03-edit.md +250 -0
  400. package/rihal/skills/actions/2-plan/rihal-edit-prd/steps-e/step-e-04-complete.md +165 -0
  401. package/rihal/skills/actions/2-plan/rihal-edit-prd/workflow.md +63 -0
  402. package/rihal/skills/actions/2-plan/rihal-frontend-design/SKILL.md +169 -0
  403. package/rihal/skills/actions/2-plan/rihal-validate-prd/SKILL.md +29 -0
  404. package/rihal/skills/actions/2-plan/rihal-validate-prd/data/domain-complexity.csv +15 -0
  405. package/rihal/skills/actions/2-plan/rihal-validate-prd/data/prd-purpose.md +197 -0
  406. package/rihal/skills/actions/2-plan/rihal-validate-prd/data/project-types.csv +11 -0
  407. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-01-discovery.md +221 -0
  408. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-02-format-detection.md +188 -0
  409. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-02b-parity-check.md +206 -0
  410. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-03-density-validation.md +171 -0
  411. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-04-brief-coverage-validation.md +211 -0
  412. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-05-measurability-validation.md +225 -0
  413. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-06-traceability-validation.md +214 -0
  414. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-07-implementation-leakage-validation.md +202 -0
  415. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-08-domain-compliance-validation.md +240 -0
  416. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-09-project-type-validation.md +260 -0
  417. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-10-smart-validation.md +206 -0
  418. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-11-holistic-quality-validation.md +261 -0
  419. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-12-completeness-validation.md +239 -0
  420. package/rihal/skills/actions/2-plan/rihal-validate-prd/steps-v/step-v-13-report-complete.md +229 -0
  421. package/rihal/skills/actions/2-plan/rihal-validate-prd/workflow.md +62 -0
  422. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/SKILL.md +30 -0
  423. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/steps/step-01-document-discovery.md +179 -0
  424. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/steps/step-02-prd-analysis.md +168 -0
  425. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/steps/step-03-epic-coverage-validation.md +169 -0
  426. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/steps/step-04-ux-alignment.md +129 -0
  427. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/steps/step-05-epic-quality-review.md +241 -0
  428. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/steps/step-06-final-assessment.md +126 -0
  429. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/templates/readiness-report-template.md +4 -0
  430. package/rihal/skills/actions/3-solutioning/rihal-check-implementation-readiness/workflow.md +49 -0
  431. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/SKILL.md +32 -0
  432. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/architecture-decision-template.md +12 -0
  433. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/data/domain-complexity.csv +13 -0
  434. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/data/project-types.csv +7 -0
  435. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/steps/step-01-init.md +153 -0
  436. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/steps/step-01b-continue.md +173 -0
  437. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/steps/step-02-context.md +224 -0
  438. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/steps/step-03-starter.md +329 -0
  439. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/steps/step-04-decisions.md +318 -0
  440. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/steps/step-05-patterns.md +359 -0
  441. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/steps/step-06-structure.md +379 -0
  442. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/steps/step-07-validation.md +359 -0
  443. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/steps/step-08-complete.md +76 -0
  444. package/rihal/skills/actions/3-solutioning/rihal-create-architecture/workflow.md +38 -0
  445. package/rihal/skills/actions/3-solutioning/rihal-generate-project-context/SKILL.md +31 -0
  446. package/rihal/skills/actions/3-solutioning/rihal-generate-project-context/project-context-template.md +21 -0
  447. package/rihal/skills/actions/3-solutioning/rihal-generate-project-context/steps/step-01-discover.md +186 -0
  448. package/rihal/skills/actions/3-solutioning/rihal-generate-project-context/steps/step-02-generate.md +321 -0
  449. package/rihal/skills/actions/3-solutioning/rihal-generate-project-context/steps/step-03-complete.md +278 -0
  450. package/rihal/skills/actions/3-solutioning/rihal-generate-project-context/workflow.md +43 -0
  451. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/SKILL.md +48 -0
  452. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/generate-trail.md +38 -0
  453. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/step-01-orientation.md +105 -0
  454. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/step-02-walkthrough.md +89 -0
  455. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/step-03-detail-pass.md +106 -0
  456. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/step-04-testing.md +74 -0
  457. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/step-05-wrapup.md +24 -0
  458. package/rihal/skills/actions/4-implementation/rihal-code-review/SKILL.md +31 -0
  459. package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-01-gather-context.md +62 -0
  460. package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-02-review.md +34 -0
  461. package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-03-triage.md +49 -0
  462. package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-04-present.md +129 -0
  463. package/rihal/skills/actions/4-implementation/rihal-code-review/workflow.md +55 -0
  464. package/rihal/skills/actions/4-implementation/rihal-correct-course/SKILL.md +29 -0
  465. package/rihal/skills/actions/4-implementation/rihal-correct-course/checklist.md +288 -0
  466. package/rihal/skills/actions/4-implementation/rihal-correct-course/workflow.md +267 -0
  467. package/rihal/skills/actions/4-implementation/rihal-dev-story/SKILL.md +36 -0
  468. package/rihal/skills/actions/4-implementation/rihal-dev-story/checklist.md +80 -0
  469. package/rihal/skills/actions/4-implementation/rihal-dev-story/workflow.md +450 -0
  470. package/rihal/skills/actions/4-implementation/rihal-qa-generate-e2e-tests/SKILL.md +31 -0
  471. package/rihal/skills/actions/4-implementation/rihal-qa-generate-e2e-tests/checklist.md +33 -0
  472. package/rihal/skills/actions/4-implementation/rihal-qa-generate-e2e-tests/workflow.md +136 -0
  473. package/rihal/skills/actions/4-implementation/rihal-retrospective/SKILL.md +30 -0
  474. package/rihal/skills/actions/4-implementation/rihal-retrospective/workflow.md +1479 -0
  475. package/rihal/skills/actions/4-implementation/rihal-scaffold-project/SKILL.md +77 -0
  476. package/rihal/skills/actions/4-implementation/rihal-scaffold-project/steps/step-01-target.md +17 -0
  477. package/rihal/skills/actions/4-implementation/rihal-scaffold-project/steps/step-02-safety.md +35 -0
  478. package/rihal/skills/actions/4-implementation/rihal-scaffold-project/steps/step-03-clone.md +50 -0
  479. package/rihal/skills/actions/4-implementation/rihal-scaffold-project/steps/step-04-post-setup.md +44 -0
  480. package/rihal/skills/actions/4-implementation/rihal-sprint-planning/SKILL.md +35 -0
  481. package/rihal/skills/actions/4-implementation/rihal-sprint-planning/checklist.md +43 -0
  482. package/rihal/skills/actions/4-implementation/rihal-sprint-planning/sprint-status-template.yaml +56 -0
  483. package/rihal/skills/actions/4-implementation/rihal-sprint-planning/workflow.md +284 -0
  484. package/rihal/skills/actions/4-implementation/rihal-sprint-status/SKILL.md +30 -0
  485. package/rihal/skills/actions/4-implementation/rihal-sprint-status/workflow.md +261 -0
  486. package/rihal/skills/agents/ahmed-hassani-director/SKILL.md +121 -0
  487. package/rihal/skills/agents/fatima-qa/SKILL.md +106 -0
  488. package/rihal/skills/agents/fatima-qa/skill-manifest.yaml +11 -0
  489. package/rihal/skills/agents/haitham-frontend/SKILL.md +120 -0
  490. package/rihal/skills/agents/hanzla-engineer/SKILL.md +109 -0
  491. package/rihal/skills/agents/hanzla-engineer/skill-manifest.yaml +11 -0
  492. package/rihal/skills/agents/hussain-pm/SKILL.md +107 -0
  493. package/rihal/skills/agents/hussain-pm/skill-manifest.yaml +11 -0
  494. package/rihal/skills/agents/hussain-sm/SKILL.md +104 -0
  495. package/rihal/skills/agents/hussain-sm/skill-manifest.yaml +11 -0
  496. package/rihal/skills/agents/layla-designer/SKILL.md +96 -0
  497. package/rihal/skills/agents/layla-designer/skill-manifest.yaml +11 -0
  498. package/rihal/skills/agents/majlis-council/SKILL.md +179 -0
  499. package/rihal/skills/agents/mariam-marketing/SKILL.md +133 -0
  500. package/rihal/skills/agents/nasser-eng-manager/SKILL.md +125 -0
  501. package/rihal/skills/agents/noor-writer/SKILL.md +104 -0
  502. package/rihal/skills/agents/noor-writer/explain-concept.md +20 -0
  503. package/rihal/skills/agents/noor-writer/mermaid-gen.md +20 -0
  504. package/rihal/skills/agents/noor-writer/skill-manifest.yaml +11 -0
  505. package/rihal/skills/agents/noor-writer/validate-doc.md +19 -0
  506. package/rihal/skills/agents/noor-writer/write-document.md +20 -0
  507. package/rihal/skills/agents/raees-orchestrator/SKILL.md +154 -0
  508. package/rihal/skills/agents/sadiq-analyst/SKILL.md +106 -0
  509. package/rihal/skills/agents/sadiq-analyst/skill-manifest.yaml +11 -0
  510. package/rihal/skills/agents/waleed-architect/SKILL.md +106 -0
  511. package/rihal/skills/agents/waleed-architect/skill-manifest.yaml +11 -0
  512. package/rihal/skills/agents/yousef-backend/SKILL.md +136 -0
  513. package/rihal/skills/agents/zahra-branding/SKILL.md +141 -0
  514. package/rihal/skills/agents/zayd-ml/SKILL.md +124 -0
  515. package/rihal/skills/core/module-help.csv +11 -0
  516. package/rihal/skills/core/module.yaml +25 -0
  517. package/rihal/skills/core/rihal-advanced-elicitation/SKILL.md +155 -0
  518. package/rihal/skills/core/rihal-advanced-elicitation/methods.csv +51 -0
  519. package/rihal/skills/core/rihal-advanced-elicitation/rihal-advanced-elicitation/SKILL.md +148 -0
  520. package/rihal/skills/core/rihal-advanced-elicitation/rihal-advanced-elicitation/methods.csv +51 -0
  521. package/rihal/skills/core/rihal-brainstorming/SKILL.md +82 -0
  522. package/rihal/skills/core/rihal-brainstorming/brain-methods.csv +62 -0
  523. package/rihal/skills/core/rihal-brainstorming/steps/step-01-session-setup.md +214 -0
  524. package/rihal/skills/core/rihal-brainstorming/steps/step-01b-continue.md +124 -0
  525. package/rihal/skills/core/rihal-brainstorming/steps/step-02a-user-selected.md +229 -0
  526. package/rihal/skills/core/rihal-brainstorming/steps/step-02b-ai-recommended.md +239 -0
  527. package/rihal/skills/core/rihal-brainstorming/steps/step-02c-random-selection.md +211 -0
  528. package/rihal/skills/core/rihal-brainstorming/steps/step-02d-progressive-flow.md +266 -0
  529. package/rihal/skills/core/rihal-brainstorming/steps/step-03-technique-execution.md +401 -0
  530. package/rihal/skills/core/rihal-brainstorming/steps/step-04-idea-organization.md +305 -0
  531. package/rihal/skills/core/rihal-brainstorming/template.md +15 -0
  532. package/rihal/skills/core/rihal-brainstorming/workflow.md +53 -0
  533. package/rihal/skills/core/rihal-clone-website/SKILL.md +395 -0
  534. package/rihal/skills/core/rihal-distillator/SKILL.md +205 -0
  535. package/rihal/skills/core/rihal-distillator/agents/distillate-compressor.md +116 -0
  536. package/rihal/skills/core/rihal-distillator/agents/round-trip-reconstructor.md +68 -0
  537. package/rihal/skills/core/rihal-distillator/resources/compression-rules.md +51 -0
  538. package/rihal/skills/core/rihal-distillator/resources/distillate-format-reference.md +227 -0
  539. package/rihal/skills/core/rihal-distillator/resources/splitting-strategy.md +78 -0
  540. package/rihal/skills/core/rihal-distillator/scripts/analyze_sources.py +300 -0
  541. package/rihal/skills/core/rihal-distillator/scripts/tests/test_analyze_sources.py +204 -0
  542. package/rihal/skills/core/rihal-editorial-review-prose/SKILL.md +108 -0
  543. package/rihal/skills/core/rihal-editorial-review-structure/SKILL.md +193 -0
  544. package/rihal/skills/core/rihal-help/SKILL.md +91 -0
  545. package/rihal/skills/core/rihal-index-docs/SKILL.md +80 -0
  546. package/rihal/skills/core/rihal-init/SKILL.md +119 -0
  547. package/rihal/skills/core/rihal-init/resources/core-module.yaml +25 -0
  548. package/rihal/skills/core/rihal-init/scripts/rihal_init.py +593 -0
  549. package/rihal/skills/core/rihal-init/scripts/tests/test_rihal_init.py +329 -0
  550. package/rihal/skills/core/rihal-party-mode/SKILL.md +77 -0
  551. package/rihal/skills/core/rihal-party-mode/steps/step-01-agent-loading.md +138 -0
  552. package/rihal/skills/core/rihal-party-mode/steps/step-02-discussion-orchestration.md +187 -0
  553. package/rihal/skills/core/rihal-party-mode/steps/step-03-graceful-exit.md +167 -0
  554. package/rihal/skills/core/rihal-party-mode/workflow.md +190 -0
  555. package/rihal/skills/core/rihal-review-adversarial-general/SKILL.md +55 -0
  556. package/rihal/skills/core/rihal-review-edge-case-hunter/SKILL.md +81 -0
  557. package/rihal/skills/core/rihal-shard-doc/SKILL.md +119 -0
  558. package/rihal/skills/core/rihal-shard-doc/rihal-shard-doc/SKILL.md +122 -0
  559. package/rihal/team.yaml +343 -0
  560. package/rihal/templates/UI-SPEC.md +127 -0
  561. package/rihal/templates/documentation-requirements.csv +11 -0
  562. package/rihal/templates/github/bug-template.md +53 -0
  563. package/rihal/templates/github/epic-template.md +57 -0
  564. package/rihal/templates/github/feature-template.md +55 -0
  565. package/rihal/templates/github/task-template.md +52 -0
  566. package/rihal/templates/milestone.md +147 -0
  567. package/rihal/templates/projects/api-backend/PROJECT.md +37 -0
  568. package/rihal/templates/projects/api-backend/REQUIREMENTS.md +38 -0
  569. package/rihal/templates/projects/api-backend/ROADMAP.md +92 -0
  570. package/rihal/templates/projects/api-backend/template.yaml +17 -0
  571. package/rihal/templates/projects/mobile-app/PROJECT.md +37 -0
  572. package/rihal/templates/projects/mobile-app/REQUIREMENTS.md +32 -0
  573. package/rihal/templates/projects/mobile-app/ROADMAP.md +93 -0
  574. package/rihal/templates/projects/mobile-app/template.yaml +17 -0
  575. package/rihal/templates/projects/saas-b2b/PROJECT.md +40 -0
  576. package/rihal/templates/projects/saas-b2b/REQUIREMENTS.md +38 -0
  577. package/rihal/templates/projects/saas-b2b/ROADMAP.md +95 -0
  578. package/rihal/templates/projects/saas-b2b/template.yaml +18 -0
  579. package/rihal/templates/settings-hooks.json +36 -0
  580. package/rihal/templates/sprint.md +70 -0
  581. package/rihal/workflows/add-phase.md +112 -0
  582. package/rihal/workflows/add-tests.md +351 -0
  583. package/rihal/workflows/add-todo.md +181 -0
  584. package/rihal/workflows/analyze-dependencies.md +138 -0
  585. package/rihal/workflows/audit-fix.md +190 -0
  586. package/rihal/workflows/audit-milestone.md +155 -0
  587. package/rihal/workflows/audit-uat.md +109 -0
  588. package/rihal/workflows/autonomous.md +992 -0
  589. package/rihal/workflows/brainstorm.md +203 -0
  590. package/rihal/workflows/chain.md +188 -0
  591. package/rihal/workflows/check-implementation-readiness.md +193 -0
  592. package/rihal/workflows/check-todos.md +177 -0
  593. package/rihal/workflows/cleanup.md +152 -0
  594. package/rihal/workflows/code-review-fix.md +529 -0
  595. package/rihal/workflows/code-review.md +566 -0
  596. package/rihal/workflows/complete-milestone.md +836 -0
  597. package/rihal/workflows/config.md +105 -0
  598. package/rihal/workflows/correct-course.md +190 -0
  599. package/rihal/workflows/council.md +565 -0
  600. package/rihal/workflows/create-epics-and-stories.md +373 -0
  601. package/rihal/workflows/create-story.md +297 -0
  602. package/rihal/workflows/dashboard.md +102 -0
  603. package/rihal/workflows/debug.md +256 -0
  604. package/rihal/workflows/decisions.md +107 -0
  605. package/rihal/workflows/dev-story.md +432 -0
  606. package/rihal/workflows/diff.md +74 -0
  607. package/rihal/workflows/discuss-phase-power.md +325 -0
  608. package/rihal/workflows/discuss-phase.md +1201 -0
  609. package/rihal/workflows/discuss.md +227 -0
  610. package/rihal/workflows/do.md +175 -0
  611. package/rihal/workflows/docs-update.md +261 -0
  612. package/rihal/workflows/document-project.md +180 -0
  613. package/rihal/workflows/enable-hooks.md +102 -0
  614. package/rihal/workflows/execute-sprint.md +514 -0
  615. package/rihal/workflows/execute.md +1478 -0
  616. package/rihal/workflows/explore.md +171 -0
  617. package/rihal/workflows/export-to-github.md +174 -0
  618. package/rihal/workflows/forensics.md +201 -0
  619. package/rihal/workflows/from-template.md +173 -0
  620. package/rihal/workflows/health.md +194 -0
  621. package/rihal/workflows/help.md +318 -0
  622. package/rihal/workflows/import.md +306 -0
  623. package/rihal/workflows/inbox.md +418 -0
  624. package/rihal/workflows/init.md +245 -0
  625. package/rihal/workflows/insert-phase.md +116 -0
  626. package/rihal/workflows/install.md +85 -0
  627. package/rihal/workflows/karpathy-audit.md +409 -0
  628. package/rihal/workflows/list-plans.md +146 -0
  629. package/rihal/workflows/list-workspaces.md +115 -0
  630. package/rihal/workflows/map-codebase.md +449 -0
  631. package/rihal/workflows/milestone-summary.md +206 -0
  632. package/rihal/workflows/new-milestone.md +616 -0
  633. package/rihal/workflows/new-project-research.md +262 -0
  634. package/rihal/workflows/new-project-roadmap.md +446 -0
  635. package/rihal/workflows/new-project.md +1503 -0
  636. package/rihal/workflows/new-workspace.md +167 -0
  637. package/rihal/workflows/next.md +162 -0
  638. package/rihal/workflows/note.md +156 -0
  639. package/rihal/workflows/notify-test.md +113 -0
  640. package/rihal/workflows/pause-work.md +243 -0
  641. package/rihal/workflows/plan-milestone-gaps.md +273 -0
  642. package/rihal/workflows/plan.md +1262 -0
  643. package/rihal/workflows/plant-seed.md +169 -0
  644. package/rihal/workflows/pr-branch.md +129 -0
  645. package/rihal/workflows/profile-user.md +162 -0
  646. package/rihal/workflows/progress.md +184 -0
  647. package/rihal/workflows/quick.md +105 -0
  648. package/rihal/workflows/remove-phase.md +155 -0
  649. package/rihal/workflows/remove-workspace.md +158 -0
  650. package/rihal/workflows/replay.md +160 -0
  651. package/rihal/workflows/rerun.md +77 -0
  652. package/rihal/workflows/research-phase.md +82 -0
  653. package/rihal/workflows/resume-work.md +326 -0
  654. package/rihal/workflows/review-adversarial.md +180 -0
  655. package/rihal/workflows/review-edge-case-hunter.md +214 -0
  656. package/rihal/workflows/review.md +281 -0
  657. package/rihal/workflows/scan.md +135 -0
  658. package/rihal/workflows/secure-phase.md +196 -0
  659. package/rihal/workflows/session-report.md +187 -0
  660. package/rihal/workflows/settings.md +185 -0
  661. package/rihal/workflows/ship.md +237 -0
  662. package/rihal/workflows/show.md +63 -0
  663. package/rihal/workflows/sprint-planning.md +166 -0
  664. package/rihal/workflows/sprint-status.md +124 -0
  665. package/rihal/workflows/stats.md +141 -0
  666. package/rihal/workflows/status.md +116 -0
  667. package/rihal/workflows/ui-phase.md +148 -0
  668. package/rihal/workflows/ui-review.md +130 -0
  669. package/rihal/workflows/undo.md +426 -0
  670. package/rihal/workflows/update.md +185 -0
  671. package/rihal/workflows/validate-phase.md +174 -0
  672. package/rihal/workflows/verify-phase.md +375 -0
  673. package/rihal/workflows/verify-work.md +717 -0
  674. package/rihal/workflows/why.md +130 -0
  675. package/rihal/workflows/workstream.md +197 -0
  676. package/server/dashboard.js +632 -0
@@ -0,0 +1,3554 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * rihal-tools.cjs — the helper binary every Rihal v2 workflow shells out to.
4
+ *
5
+ * Design goal: one Bash call per workflow step returns a single JSON blob
6
+ * with every path, flag, and config value the orchestrator needs. This
7
+ * replaces what would otherwise be 5-10 Read calls in the parent context
8
+ * and keeps the orchestrator's context window small.
9
+ *
10
+ * Installed at: {project-root}/.rihal/bin/rihal-tools.cjs
11
+ *
12
+ * Subcommands:
13
+ * init <workflow-name> "<raw-args>" → JSON context blob for a workflow
14
+ * select-panel "<question>" [flags] → JSON { panel, scores, question, flags }
15
+ * classify-question "<question>" → JSON { type, signals } (codebase|discovery|market|greenfield)
16
+ * agent-info <agent-id> → JSON row from agent-manifest.csv
17
+ * list-agents → JSON array of installed agent ids
18
+ * version → package version
19
+ *
20
+ * Zero external dependencies. Pure Node stdlib. Runs offline.
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ // Resolve project root. This file is installed at {project-root}/.rihal/bin/,
27
+ // so two levels up is the project.
28
+ // PROJECT_ROOT detection: when installed, this binary lives at <project>/.rihal/bin/rihal-tools.cjs
29
+ // When running from source (rihal/bin/), warn but allow — tests need this path.
30
+ const _maybeRoot = path.resolve(__dirname, '..', '..');
31
+ const _isInstalled = path.basename(path.dirname(__dirname)) === '.rihal';
32
+ if (!_isInstalled && !process.env.RIHAL_DEV_MODE && !process.env.NODE_TEST_CONTEXT) {
33
+ // Source dir, not installed location — warn but proceed (tests run from here)
34
+ if (process.stderr.isTTY) {
35
+ console.error('Note: rihal-tools.cjs running from source. For full features install with: node cli/install-v2.js <target> --yes');
36
+ }
37
+ }
38
+ const PROJECT_ROOT = _maybeRoot;
39
+ const RIHAL_DIR = path.join(PROJECT_ROOT, '.rihal');
40
+ const CONFIG_DIR = path.join(RIHAL_DIR, '_config');
41
+ const REFS_DIR = path.join(RIHAL_DIR, 'references');
42
+ const WORKFLOWS_DIR = path.join(RIHAL_DIR, 'workflows');
43
+ const PLANNING_DIR = path.join(PROJECT_ROOT, '.planning');
44
+ const SESSIONS_DIR = path.join(PLANNING_DIR, 'council-sessions');
45
+
46
+ /**
47
+ * Parse a minimal YAML subset for our flat config.yaml shape.
48
+ * Only supports `key: value` lines — no nesting, no lists, no flow syntax.
49
+ */
50
+ function parseSimpleYaml(text) {
51
+ const out = {};
52
+ for (const raw of text.split('\n')) {
53
+ const line = raw.replace(/#.*$/, '').trim();
54
+ if (!line) continue;
55
+ const colonAt = line.indexOf(':');
56
+ if (colonAt === -1) continue;
57
+ const key = line.slice(0, colonAt).trim();
58
+ let val = line.slice(colonAt + 1).trim();
59
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
60
+ if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
61
+ out[key] = val;
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function readConfig() {
67
+ const configPath = path.join(RIHAL_DIR, 'config.yaml');
68
+ if (!fs.existsSync(configPath)) {
69
+ return {
70
+ user_name: 'User',
71
+ project_name: path.basename(PROJECT_ROOT),
72
+ language: 'English',
73
+ mode: 'guided',
74
+ };
75
+ }
76
+ try {
77
+ const parsed = parseSimpleYaml(fs.readFileSync(configPath, 'utf8'));
78
+ return {
79
+ ...parsed, // spread all parsed keys (model_profile, branching_strategy, etc.)
80
+ user_name: parsed.user_name || 'User',
81
+ project_name: parsed.project_name || path.basename(PROJECT_ROOT),
82
+ language: parsed.communication_language || parsed.language || 'English',
83
+ mode: parsed.mode || 'guided',
84
+ };
85
+ } catch (e) {
86
+ throw new Error(`Failed to read config.yaml: ${e.message}`);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Parse CSV with quoted-field support. Expects the first row to be headers.
92
+ * Returns array of objects keyed by header.
93
+ */
94
+ function parseCsv(text) {
95
+ const rows = [];
96
+ let row = [];
97
+ let field = '';
98
+ let inQuotes = false;
99
+ for (let i = 0; i < text.length; i++) {
100
+ const ch = text[i];
101
+ if (inQuotes) {
102
+ if (ch === '"' && text[i + 1] === '"') { field += '"'; i++; }
103
+ else if (ch === '"') inQuotes = false;
104
+ else field += ch;
105
+ } else {
106
+ if (ch === '"') inQuotes = true;
107
+ else if (ch === ',') { row.push(field); field = ''; }
108
+ else if (ch === '\n') { row.push(field); rows.push(row); row = []; field = ''; }
109
+ else if (ch === '\r') { /* skip */ }
110
+ else field += ch;
111
+ }
112
+ }
113
+ if (field.length > 0 || row.length > 0) { row.push(field); rows.push(row); }
114
+ if (rows.length === 0) return [];
115
+ const headers = rows[0];
116
+ return rows.slice(1).filter((r) => r.length >= headers.length && r.some((c) => c !== '')).map((r) => {
117
+ const obj = {};
118
+ headers.forEach((h, idx) => { obj[h] = r[idx] || ''; });
119
+ return obj;
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Recursively walk a directory and return absolute file paths.
125
+ */
126
+ function walkFiles(dir) {
127
+ const out = [];
128
+ if (!fs.existsSync(dir)) return out;
129
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
130
+ const full = path.join(dir, entry.name);
131
+ if (entry.isDirectory()) out.push(...walkFiles(full));
132
+ else if (entry.isFile()) out.push(full);
133
+ }
134
+ return out;
135
+ }
136
+
137
+ /**
138
+ * Parse YAML frontmatter from a markdown file. Returns { frontmatter, body }.
139
+ * Minimal subset — supports `key: value` and quoted strings only. Good
140
+ * enough for our agent and command files.
141
+ */
142
+ function parseFrontmatter(text) {
143
+ if (!text.startsWith('---\n')) return { frontmatter: {}, body: text };
144
+ const end = text.indexOf('\n---\n', 4);
145
+ if (end === -1) return { frontmatter: {}, body: text };
146
+ const block = text.slice(4, end);
147
+ const body = text.slice(end + 5);
148
+ const fm = {};
149
+ for (const raw of block.split('\n')) {
150
+ const line = raw.replace(/^#.*$/, '').trimEnd();
151
+ if (!line) continue;
152
+ const colonAt = line.indexOf(':');
153
+ if (colonAt === -1) continue;
154
+ const key = line.slice(0, colonAt).trim();
155
+ let val = line.slice(colonAt + 1).trim();
156
+ if (!key || !val) continue;
157
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
158
+ if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
159
+ fm[key] = val;
160
+ }
161
+ return { frontmatter: fm, body };
162
+ }
163
+
164
+ function readAgentManifest() {
165
+ const manifestPath = path.join(CONFIG_DIR, 'agent-manifest.csv');
166
+ if (!fs.existsSync(manifestPath)) return [];
167
+ return parseCsv(fs.readFileSync(manifestPath, 'utf8'));
168
+ }
169
+
170
+ function listInstalledAgents() {
171
+ // Local agents from project manifest
172
+ const local = readAgentManifest().map((row) => row.id).filter(Boolean);
173
+
174
+ // Global agents from ~/.rihal/agents/
175
+ let global = [];
176
+ const globalDir = path.join(process.env.HOME || '', '.rihal', 'agents');
177
+ if (fs.existsSync(globalDir)) {
178
+ global = fs.readdirSync(globalDir)
179
+ .filter(f => f.startsWith('rihal-') && f.endsWith('.md'))
180
+ .map(f => f.replace('rihal-', '').replace('.md', ''));
181
+ }
182
+
183
+ // Merge and deduplicate: local takes precedence if defined in both
184
+ return [...new Set([...local, ...global])];
185
+ }
186
+
187
+ /**
188
+ * Load the council-panel scoring function. Installed at
189
+ * .rihal/bin/lib/council-panel.cjs alongside this helper.
190
+ */
191
+ function loadPanelScorer() {
192
+ const scorerPath = path.join(__dirname, 'lib', 'council-panel.cjs');
193
+ if (!fs.existsSync(scorerPath)) {
194
+ throw new Error(`Panel scorer missing at ${scorerPath}. Reinstall: see README install command, or re-run install-v2.js with --force.`);
195
+ }
196
+ return require(scorerPath);
197
+ }
198
+
199
+ /**
200
+ * Parse raw workflow args. Returns { question, flags }.
201
+ *
202
+ * Flag grammar:
203
+ * --full → flags.full = true
204
+ * --agents=a,b,c → flags.agents = ['a','b','c']
205
+ * --explain → flags.explain = true
206
+ * --top N or --top=N → flags.top = N (integer)
207
+ *
208
+ * Everything else becomes part of the question.
209
+ */
210
+ function parseArgs(raw) {
211
+ const flags = { full: false, agents: [], explain: false, top: null };
212
+ const words = [];
213
+ const tokens = (raw || '').trim().split(/\s+/).filter(Boolean);
214
+ for (let i = 0; i < tokens.length; i++) {
215
+ const tok = tokens[i];
216
+ if (tok === '--full') flags.full = true;
217
+ else if (tok === '--explain') flags.explain = true;
218
+ else if (tok.startsWith('--agents=')) {
219
+ flags.agents = tok.slice('--agents='.length).split(',').map((s) => s.trim()).filter(Boolean);
220
+ } else if (tok.startsWith('--top=')) {
221
+ flags.top = parseInt(tok.slice('--top='.length), 10) || null;
222
+ } else if (tok === '--top' && i + 1 < tokens.length && /^\d+$/.test(tokens[i + 1])) {
223
+ flags.top = parseInt(tokens[++i], 10);
224
+ } else {
225
+ words.push(tok);
226
+ }
227
+ }
228
+ return { question: words.join(' '), flags };
229
+ }
230
+
231
+ /**
232
+ * Take the scorer's ideal panel and reduce it to agents actually installed
233
+ * on disk. If the ideal panel references uninstalled ids (e.g. hussain-pm
234
+ * in the v2 prototype), they are dropped and the panel is padded from the
235
+ * remaining installed agents in their canonical order — so the user still
236
+ * sees a reasonable panel size instead of a 1- or 2-agent stub.
237
+ *
238
+ * Minimum panel size is min(3, installedAgents.length) — if the project
239
+ * only has 2 installed agents we return both, not a broken padded panel.
240
+ *
241
+ * Padding is SKIPPED when the user explicitly passed --agents=, because
242
+ * that flag is a direct user intent and we must not add agents they
243
+ * didn't ask for.
244
+ */
245
+ function filterPanelToInstalled(idealPanel, installedAgents, { pad = true } = {}) {
246
+ const kept = idealPanel.filter((id) => installedAgents.includes(id));
247
+ if (!pad) return kept;
248
+
249
+ const minTarget = Math.min(3, installedAgents.length);
250
+ if (kept.length >= minTarget) return kept;
251
+
252
+ const already = new Set(kept);
253
+ const padded = [...kept];
254
+ for (const id of installedAgents) {
255
+ if (padded.length >= minTarget) break;
256
+ if (!already.has(id)) padded.push(id);
257
+ }
258
+ return padded;
259
+ }
260
+
261
+ function cmdInit(workflowName, rawArgs) {
262
+ const config = readConfig();
263
+ const installedAgents = listInstalledAgents();
264
+ const { question, flags } = parseArgs(rawArgs);
265
+
266
+ let panel = [];
267
+ let scores = {};
268
+
269
+ let agent_id = null;
270
+
271
+ if (workflowName === 'council') {
272
+ const COUNCIL_EXCLUDED = ['executor', 'planner'];
273
+ const councilAgents = installedAgents.filter((id) => !COUNCIL_EXCLUDED.includes(id));
274
+ const scorer = loadPanelScorer();
275
+ const opts = {};
276
+ if (flags.full) opts.full = true;
277
+ if (flags.agents.length > 0) opts.agents = flags.agents;
278
+ const ideal = scorer.selectPanel(question, opts);
279
+ // Don't pad when user explicitly specified the agent list — their
280
+ // choice is the final word.
281
+ panel = filterPanelToInstalled(ideal, councilAgents, { pad: flags.agents.length === 0 });
282
+ if (flags.explain) {
283
+ const explained = scorer.explainSelection(question, opts);
284
+ scores = explained.scores || {};
285
+ }
286
+ }
287
+
288
+ if (workflowName === 'discuss') {
289
+ // Check if the first token of the question is a known agent id.
290
+ // If so, extract it and shorten the question.
291
+ const qWords = question.split(/\s+/).filter(Boolean);
292
+ if (qWords.length > 0 && installedAgents.includes(qWords[0])) {
293
+ agent_id = qWords[0];
294
+ }
295
+ }
296
+
297
+ const questionClassification = cmdClassifyQuestion(
298
+ agent_id ? question.slice(agent_id.length).trim() : question
299
+ );
300
+
301
+ // For discuss, strip agent_id from the question in the output
302
+ const outputQuestion = (workflowName === 'discuss' && agent_id)
303
+ ? question.slice(agent_id.length).trim()
304
+ : question;
305
+
306
+ const out = {
307
+ workflow: workflowName,
308
+ question: outputQuestion,
309
+ agent_id,
310
+ flags,
311
+ panel,
312
+ scores,
313
+ question_type: questionClassification.type,
314
+ question_signals: questionClassification.signals,
315
+ config,
316
+ installed_agents: installedAgents,
317
+ paths: {
318
+ project_root: PROJECT_ROOT,
319
+ rihal: RIHAL_DIR,
320
+ config_dir: CONFIG_DIR,
321
+ refs: REFS_DIR,
322
+ workflows: WORKFLOWS_DIR,
323
+ planning_root: PLANNING_DIR,
324
+ sessions_dir: SESSIONS_DIR,
325
+ state: path.join(RIHAL_DIR, 'state.json'),
326
+ },
327
+ state_exists: fs.existsSync(path.join(RIHAL_DIR, 'state.json')),
328
+ };
329
+
330
+ return out;
331
+ }
332
+
333
+ function cmdSelectPanel(rawArgs) {
334
+ const { question, flags } = parseArgs(rawArgs);
335
+ const scorer = loadPanelScorer();
336
+ const opts = {};
337
+ if (flags.full) opts.full = true;
338
+ if (flags.agents.length > 0) opts.agents = flags.agents;
339
+ const ideal = scorer.selectPanel(question, opts);
340
+ const explained = scorer.explainSelection(question, opts);
341
+ const installed = listInstalledAgents();
342
+ let panel = filterPanelToInstalled(ideal, installed, { pad: flags.agents.length === 0 });
343
+
344
+ // --top N: return only the top N agents by score
345
+ if (flags.top && flags.top > 0) {
346
+ // Sort panel by score descending, then slice
347
+ const scoreMap = explained.scores || {};
348
+ panel = [...panel].sort((a, b) => (scoreMap[b] || 0) - (scoreMap[a] || 0)).slice(0, flags.top);
349
+ }
350
+
351
+ // Filter scores to installed-only agents
352
+ const installedSet = new Set(installed);
353
+ const filteredScores = Object.fromEntries(
354
+ Object.entries(explained.scores || {}).filter(([id]) => installedSet.has(id))
355
+ );
356
+
357
+ return {
358
+ question,
359
+ flags,
360
+ panel,
361
+ scores: filteredScores,
362
+ installed,
363
+ };
364
+ }
365
+
366
+ function cmdAgentInfo(agentId) {
367
+ const row = readAgentManifest().find((r) => r.id === agentId);
368
+ if (!row) {
369
+ console.error(`Unknown agent: ${agentId}`);
370
+ process.exit(1);
371
+ }
372
+ return row;
373
+ }
374
+
375
+ function cmdListAgents() {
376
+ return { agents: listInstalledAgents() };
377
+ }
378
+
379
+ /**
380
+ * Classify a question into one of four types so the workflow can decide
381
+ * whether to run a codebase scan or a research/discovery pre-step.
382
+ *
383
+ * Types:
384
+ * codebase — question is about existing code, architecture, tests, commits, bugs
385
+ * discovery — question is about choosing what to build (new project, sector, market)
386
+ * market — question is about external context (plans, regulations, competitors, geography)
387
+ * greenfield — question is about starting from scratch with no existing artifacts
388
+ * team — question is about people, hiring, org structure, process, culture
389
+ * release — question is about shipping, deploy, rollback, incident, production
390
+ * design — question is about UX, brand, visual, user journey, interface
391
+ *
392
+ * Returns { type, signals: string[] } where signals are the matched phrases.
393
+ */
394
+ function cmdClassifyQuestion(raw) {
395
+ const normalized = (raw || '').toLowerCase().replace(/[.,;:!?"()\[\]{}]/g, ' ').replace(/\s+/g, ' ').trim();
396
+
397
+ const SIGNAL_GROUPS = {
398
+ discovery: [
399
+ 'what project', 'which project', 'what should we build', 'what to build',
400
+ 'which market', 'what market', 'where should we start', 'what business',
401
+ 'which idea', 'what idea', 'start a company', 'start a business',
402
+ 'new venture', 'new startup', 'what opportunity',
403
+ // Roman Urdu discovery signals
404
+ 'research kar', 'pata karo', 'batao', 'kaisa', 'kya karo', 'suggest karo',
405
+ 'plan karo', 'soche', 'plan karna',
406
+ // Roman Urdu strategic signals (what-should-I-do questions)
407
+ 'kya karna', 'worth hai', 'sahi hai', 'kya sochte', 'kya lagta',
408
+ // Urdu unicode discovery signals
409
+ 'ریسرچ', 'بتاؤ', 'ماذا', 'أفضل', 'کیف',
410
+ // Arabic discovery signals
411
+ 'كيف أبدأ', 'ابدأ مشروع', 'مشروع جديد',
412
+ ],
413
+ market: [
414
+ '2040', '2030', '2050', 'vision plan', 'national plan', 'government plan',
415
+ 'strategy plan', 'economic plan', 'development plan', 'five year plan',
416
+ 'oman', 'saudi', 'uae', 'gulf', 'gcc', 'mena', 'bahrain', 'qatar', 'kuwait',
417
+ 'market opportunity', 'market size', 'competitor', 'industry trend',
418
+ 'regulation', 'compliance', 'sector', 'economy',
419
+ // Roman Urdu market signals
420
+ 'dubai', 'affiliate', 'karobar', 'business karna', 'market research kar',
421
+ // Urdu unicode market signals
422
+ 'دبئی', 'مارکیٹ', 'کاروبار', 'خلیج', 'ہل', 'سوق', 'مشروع',
423
+ // Arabic market signals
424
+ 'سوق', 'بحث', 'دبئي',
425
+ ],
426
+ greenfield: [
427
+ 'start fresh', 'from scratch', 'new project', 'blank slate', 'greenfield',
428
+ 'build something new', 'start building', 'no existing', 'haven\'t started',
429
+ 'bootstrap', 'kickoff',
430
+ // Business-launch patterns (overloaded "launch" word — these disambiguate to business intent, not release)
431
+ 'launch a website', 'launch a site', 'launch a business', 'launch a startup',
432
+ 'launch an app', 'launch a product', 'launch a service', 'launch the website',
433
+ 'launch the site', 'launch the business', 'website launch', 'site launch',
434
+ 'business launch', 'product launch', 'startup launch',
435
+ 'launch karna', 'launch karo', 'website launch karna', 'site launch karna',
436
+ 'rent website', 'rental site', 'rental marketplace', 'rental platform',
437
+ 'quick bucks', 'side hustle', 'make money',
438
+ // Roman Urdu greenfield signals
439
+ 'bnanai', 'banana', 'app banana', 'shuru', 'start karna', 'naya project', 'project banana', 'build karna',
440
+ 'chahiye', 'banana hai', 'website chahiye', 'app chahiye', 'rank and rent', 'banaiye', 'bana do',
441
+ 'banai', 'banaye', 'tayyar karna',
442
+ // Urdu unicode greenfield signals
443
+ 'سائٹ بنانا', 'ایپ بنانا',
444
+ ],
445
+ team: [
446
+ 'hiring', 'hire', 'fire', 'team size', 'squad', 'org structure', 'burnout',
447
+ 'morale', 'retrospective', 'culture', 'process', 'onboarding', 'offboarding',
448
+ 'performance review', 'raise', 'promotion', 'conflict', 'burning out', 'burn out',
449
+ 'overwork', 'overworked', 'retention', 'turnover',
450
+ ],
451
+ release: [
452
+ 'deploy', 'deployment', 'ship to prod', 'shipping', 'rollback', 'incident', 'production issue',
453
+ 'hotfix', 'feature flag', 'canary', 'blue green', 'downtime', 'outage',
454
+ 'monitoring', 'alert', 'on call', 'oncall',
455
+ 'production launch', 'release launch',
456
+ ],
457
+ design: [
458
+ 'ux', 'user experience', 'user journey', 'wireframe', 'prototype', 'figma',
459
+ 'brand', 'visual identity', 'color palette', 'typography', 'logo',
460
+ 'design system', 'component library', 'accessibility', 'a11y', 'ui design',
461
+ 'redesign', 'onboarding flow', 'landing page', 'interface', 'layout',
462
+ ],
463
+ codebase: [
464
+ 'rewrite', 'refactor', 'migrate', 'this code', 'this function', 'this file',
465
+ 'this component', 'this api', 'this service', 'this database', 'this schema',
466
+ 'the auth', 'the tests', 'the build', 'the deploy', 'the pipeline',
467
+ 'production ready', 'ready to ship', 'test coverage', 'bug', 'error',
468
+ 'performance', 'should i rewrite', 'auth layer', 'db migration',
469
+ 'pull request', 'code review', 'technical debt', 'tech debt',
470
+ 'feature', 'ci/cd', 'cicd', 'pipeline', 'documentation', 'docs',
471
+ // Tech choice signals
472
+ 'astro', 'nextjs', 'next.js', 'remix', 'nuxt', 'svelte', 'vue', 'angular',
473
+ 'should i use', 'which framework', 'compare framework',
474
+ // Roman Urdu codebase/fix signals
475
+ 'fix karo', 'theek karo', 'sahi karo',
476
+ 'إعادة', 'کود',
477
+ // Arabic execution signals
478
+ 'إصلاح', 'كود', 'برنامج', 'نفذ', 'شغل',
479
+ ],
480
+ };
481
+
482
+ const matchedSignals = (signals) => signals.filter((s) => normalized.includes(s));
483
+ const matched = {};
484
+ for (const [type, signals] of Object.entries(SIGNAL_GROUPS)) {
485
+ matched[type] = matchedSignals(signals);
486
+ }
487
+
488
+ // Weights per type
489
+ const WEIGHTS = { discovery: 3, market: 2, greenfield: 2, team: 3, release: 3, design: 3, codebase: 3 };
490
+ const scores = {};
491
+ for (const [type, hits] of Object.entries(matched)) {
492
+ scores[type] = hits.length * WEIGHTS[type];
493
+ }
494
+
495
+ // discovery + market together = market-research question
496
+ if (matched.discovery.length > 0 && matched.market.length > 0) {
497
+ return { type: 'market', signals: [...matched.discovery, ...matched.market], scores };
498
+ }
499
+
500
+ const winner = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
501
+ const type = winner[1] > 0 ? winner[0] : 'discovery'; // default to discovery
502
+ const allSignals = Object.values(matched).flat();
503
+
504
+ return { type, signals: allSignals, scores };
505
+ }
506
+
507
+ /**
508
+ * init execute — returns context blob for the /rihal:execute workflow.
509
+ * Resolves plan_path (single file or phase directory), reads the plan
510
+ * frontmatter, and returns dependency wave groupings.
511
+ */
512
+ function cmdInitExecute(rawArgs) {
513
+ const config = readConfig();
514
+ const tokens = (rawArgs || '').trim().split(/\s+/).filter(Boolean);
515
+ const flags = { wave: null, interactive: false, continue: false, option: null, 'skip-gates': false };
516
+ const positional = [];
517
+
518
+ for (let i = 0; i < tokens.length; i++) {
519
+ const t = tokens[i];
520
+ if (t === '--interactive') flags.interactive = true;
521
+ else if (t === '--continue') flags.continue = true;
522
+ else if (t === '--skip-gates') flags['skip-gates'] = true;
523
+ else if (t.startsWith('--wave=')) flags.wave = t.slice('--wave='.length);
524
+ else if (t.startsWith('--option=')) flags.option = t.slice('--option='.length);
525
+ else positional.push(t);
526
+ }
527
+
528
+ const target = positional[0] || '';
529
+ let planPath = null;
530
+ let phaseDir = null;
531
+ let plans = [];
532
+
533
+ // Resolve target: could be a .md file or a phase dir/name
534
+ if (target && target.length > 5000) {
535
+ throw new Error('Target path exceeds maximum length (5000 chars)');
536
+ }
537
+ const asAbsolute = path.isAbsolute(target) ? target : path.join(PROJECT_ROOT, target);
538
+ if (!asAbsolute.startsWith(PROJECT_ROOT)) {
539
+ throw new Error(`Path outside project root: ${target}`);
540
+ }
541
+ if (target.endsWith('.md') && fs.existsSync(asAbsolute)) {
542
+ planPath = asAbsolute;
543
+ plans = [{ path: planPath, depends_on: [], wave: 0 }];
544
+ } else {
545
+ // Look for a phase directory
546
+ const candidates = [
547
+ asAbsolute,
548
+ path.join(PLANNING_DIR, target),
549
+ path.join(PLANNING_DIR, 'phases', target),
550
+ ];
551
+ for (const c of candidates) {
552
+ if (fs.existsSync(c) && fs.statSync(c).isDirectory()) {
553
+ phaseDir = c;
554
+ break;
555
+ }
556
+ }
557
+ if (phaseDir) {
558
+ const planFiles = walkFiles(phaseDir).filter((f) => path.basename(f) === 'SPRINT.md' || path.basename(f) === 'PLAN.md');
559
+ plans = planFiles.map((f) => {
560
+ const text = fs.readFileSync(f, 'utf8');
561
+ const { frontmatter } = parseFrontmatter(text);
562
+ const depends = frontmatter.depends_on
563
+ ? frontmatter.depends_on.split(',').map((s) => s.trim()).filter(Boolean)
564
+ : [];
565
+ return { path: f, depends_on: depends, wave: 0, plan: frontmatter.plan || path.basename(path.dirname(f)) };
566
+ });
567
+ // Simple wave assignment: wave 0 = no deps, wave 1 = depends on wave 0, etc.
568
+ const assigned = new Set();
569
+ let wave = 0;
570
+ while (assigned.size < plans.length) {
571
+ const prev = assigned.size;
572
+ for (const p of plans) {
573
+ if (assigned.has(p.path)) continue;
574
+ const depsResolved = p.depends_on.every((dep) =>
575
+ plans.some((q) => q.plan === dep && assigned.has(q.path))
576
+ );
577
+ if (depsResolved) { p.wave = wave; assigned.add(p.path); }
578
+ }
579
+ if (assigned.size === prev) break; // circular or missing dep — break
580
+ wave++;
581
+ }
582
+ }
583
+ }
584
+
585
+ return {
586
+ workflow: 'execute',
587
+ target,
588
+ flags,
589
+ plan_path: planPath,
590
+ phase_dir: phaseDir,
591
+ plans,
592
+ config,
593
+ paths: {
594
+ project_root: PROJECT_ROOT,
595
+ rihal: RIHAL_DIR,
596
+ planning_root: PLANNING_DIR,
597
+ state: path.join(RIHAL_DIR, 'state.json'),
598
+ },
599
+ state_exists: fs.existsSync(path.join(RIHAL_DIR, 'state.json')),
600
+ };
601
+ }
602
+
603
+ /**
604
+ * state <subcommand> — read/write .rihal/state.json for execution tracking.
605
+ *
606
+ * Subcommands:
607
+ * read → print full state.json as formatted JSON
608
+ * get → alias for read
609
+ * init --project <name> → create state.json if missing
610
+ * set-phase <name> → set current_phase, reset current_plan, append to phases[]
611
+ * advance-plan → increment current_plan
612
+ * record-execution --plan <name> --tasks <n> --duration <ms> --hash <git-hash>
613
+ * add-decision "<summary>" → append to decisions[]
614
+ * add-blocker "<description>" → append to blockers[]
615
+ * resolve-blocker <index> → set blockers[index].resolved = true
616
+ * record-session → update last_session timestamp
617
+ * record-council --slug <s> --panel <csv> --artifact <path>
618
+ */
619
+ function cmdState(subArgs) {
620
+ const statePath = path.join(RIHAL_DIR, 'state.json');
621
+ const sub = subArgs[0];
622
+
623
+ /** Parse --key value flags from subArgs starting at index. */
624
+ function parseFlags(startIdx) {
625
+ const flags = {};
626
+ for (let i = startIdx; i < subArgs.length; i++) {
627
+ if (subArgs[i].startsWith('--')) {
628
+ const key = subArgs[i].slice(2);
629
+ flags[key] = subArgs[i + 1] || '';
630
+ i++;
631
+ }
632
+ }
633
+ return flags;
634
+ }
635
+
636
+ /** Cross-project decision log at ~/.rihal/decisions.jsonl. One JSON record per line. */
637
+ function globalDecisionsPath() {
638
+ const os = require('os');
639
+ return path.join(os.homedir(), '.rihal', 'decisions.jsonl');
640
+ }
641
+
642
+ function appendGlobalDecision(record) {
643
+ const file = globalDecisionsPath();
644
+ fs.mkdirSync(path.dirname(file), { recursive: true });
645
+ fs.appendFileSync(file, JSON.stringify(record) + '\n', 'utf8');
646
+ }
647
+
648
+ function readGlobalDecisions() {
649
+ const file = globalDecisionsPath();
650
+ if (!fs.existsSync(file)) return [];
651
+ const raw = fs.readFileSync(file, 'utf8');
652
+ const out = [];
653
+ for (const line of raw.split('\n')) {
654
+ const t = line.trim();
655
+ if (!t) continue;
656
+ try { out.push(JSON.parse(t)); } catch (_) { /* skip malformed */ }
657
+ }
658
+ return out;
659
+ }
660
+
661
+ /** Read state or return default skeleton. */
662
+ function readState() {
663
+ if (!fs.existsSync(statePath)) return null;
664
+ const stats = fs.statSync(statePath);
665
+ if (stats.size > 10 * 1024 * 1024) {
666
+ throw new Error('state.json exceeds 10 MB limit — possible corruption');
667
+ }
668
+ try {
669
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
670
+ } catch (e) {
671
+ throw new Error(`Invalid JSON in state.json: ${e.message}`);
672
+ }
673
+ }
674
+
675
+ /** Atomic write: write to temp file then rename. */
676
+ function writeState(state) {
677
+ function isProcessAlive(pid) {
678
+ try { process.kill(pid, 0); return true; } catch { return false; }
679
+ }
680
+
681
+ state.updated = new Date().toISOString();
682
+ fs.mkdirSync(RIHAL_DIR, { recursive: true });
683
+ const lockPath = statePath + '.lock';
684
+ let attempts = 0;
685
+ while (fs.existsSync(lockPath) && attempts < 50) {
686
+ // Check if lock holder is alive
687
+ const lockPid = parseInt(fs.readFileSync(lockPath, 'utf8'), 10);
688
+ if (lockPid && !isProcessAlive(lockPid)) {
689
+ console.error(`Stale lock from PID ${lockPid} — removing`);
690
+ try { fs.unlinkSync(lockPath); } catch {}
691
+ break;
692
+ }
693
+ require('child_process').execSync('sleep 0.05'); // 50ms backoff
694
+ attempts++;
695
+ }
696
+ if (attempts >= 50) throw new Error('state.json locked too long');
697
+
698
+ try {
699
+ fs.writeFileSync(lockPath, String(process.pid));
700
+ const tmp = statePath + '.tmp';
701
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n');
702
+ fs.renameSync(tmp, statePath);
703
+ } finally {
704
+ try { fs.unlinkSync(lockPath); } catch {}
705
+ }
706
+ return { ok: true, state };
707
+ }
708
+
709
+ /** Write state and return compact result (hides full state from output) */
710
+ function writeStateCompact(state, meta) {
711
+ writeState(state);
712
+ return { ok: true, ...meta };
713
+ }
714
+
715
+ function defaultState(projectName) {
716
+ const now = new Date().toISOString();
717
+ return {
718
+ version: '1',
719
+ project: projectName || path.basename(PROJECT_ROOT),
720
+ created: now,
721
+ updated: now,
722
+ current_phase: null,
723
+ current_plan: 0,
724
+ current_sprint: null,
725
+ phases: [],
726
+ velocity_history: [],
727
+ executions: [],
728
+ decisions: [],
729
+ blockers: [],
730
+ council_sessions: [],
731
+ last_session: null,
732
+ workstreams: [],
733
+ active_workstream: null,
734
+ };
735
+ }
736
+
737
+ // --- read / get ---
738
+ if (sub === 'read' || sub === 'get') {
739
+ if (!fs.existsSync(statePath)) {
740
+ // Auto-init with defaults if config.yaml exists (install happened).
741
+ // Removes the "run /rihal:init first" friction — any workflow can
742
+ // call `state read` and get a usable state back.
743
+ const configPath = path.join(RIHAL_DIR, 'config.yaml');
744
+ if (fs.existsSync(configPath)) {
745
+ let projectName = path.basename(PROJECT_ROOT);
746
+ try {
747
+ const cfg = fs.readFileSync(configPath, 'utf8');
748
+ const match = cfg.match(/^project_name:\s*"?([^"\n]+)"?/m);
749
+ if (match) projectName = match[1].trim();
750
+ } catch { /* use basename fallback */ }
751
+ const state = defaultState(projectName);
752
+ writeState(state);
753
+ return state;
754
+ }
755
+ return {
756
+ ok: false,
757
+ error: 'No state.json yet. Run /rihal:install to set up this project, or `state init --project <name>` directly.'
758
+ };
759
+ }
760
+ const state = readState();
761
+ if (!state) return { state: null };
762
+ return state;
763
+ }
764
+
765
+ // --- init ---
766
+ if (sub === 'init') {
767
+ let existing;
768
+ try {
769
+ existing = fs.existsSync(statePath) ? readState() : null;
770
+ } catch (e) {
771
+ console.error(`Warning: existing state.json corrupted (${e.message}). Initializing fresh state.`);
772
+ existing = null;
773
+ }
774
+ if (existing && !parseFlags(1).force) {
775
+ return { ok: true, state: existing, message: 'state.json already exists; pass --force to reinitialize' };
776
+ }
777
+ const flags = parseFlags(1);
778
+ const state = defaultState(flags.project);
779
+ return writeState(state);
780
+ }
781
+
782
+ // --- set-phase ---
783
+ if (sub === 'set-phase') {
784
+ const name = subArgs[1];
785
+ if (!name) throw new Error('set-phase requires a phase name argument');
786
+ const state = readState() || defaultState();
787
+ state.current_phase = name;
788
+ state.current_plan = 0;
789
+ if (!state.phases) state.phases = [];
790
+ if (!state.phases.some(p => p.name === name)) {
791
+ state.phases.push({ name, started: new Date().toISOString(), completed: null, plan_count: 0 });
792
+ }
793
+ return writeState(state);
794
+ }
795
+
796
+ // --- advance-plan ---
797
+ if (sub === 'advance-plan') {
798
+ const state = readState() || defaultState();
799
+ if (typeof state.current_plan !== 'number') state.current_plan = 0;
800
+ state.current_plan += 1;
801
+ // Update plan_count on current phase if tracked
802
+ if (state.phases && state.phases.length > 0) {
803
+ const current = state.phases[state.phases.length - 1];
804
+ current.plan_count = state.current_plan;
805
+ }
806
+ return writeState(state);
807
+ }
808
+
809
+ // =====================================================================
810
+ // Sprint & Story Management
811
+ // =====================================================================
812
+
813
+ // --- sprint add --phase NN --goal "Sprint goal" ---
814
+ if (sub === 'sprint' && subArgs[1] === 'add') {
815
+ const flags = parseFlags(2);
816
+ const state = readState() || defaultState();
817
+ if (!flags.phase) throw new Error('sprint add requires --phase <NN>');
818
+ if (!flags.goal) throw new Error('sprint add requires --goal "Sprint goal"');
819
+
820
+ const phaseIdx = state.phases.findIndex(p =>
821
+ String(p.number) === String(flags.phase) ||
822
+ String(p.id) === String(flags.phase) ||
823
+ p.name === flags.phase
824
+ );
825
+ if (phaseIdx === -1) throw new Error(`Phase "${flags.phase}" not found in state`);
826
+ const phase = state.phases[phaseIdx];
827
+
828
+ // Derive phase number: prefer explicit .number, fallback to array position
829
+ // Prefer explicit .number, then .id (zero-padded string like "01"),
830
+ // then array position
831
+ const phaseNum = phase.number != null
832
+ ? phase.number
833
+ : phase.id != null
834
+ ? parseInt(phase.id, 10) || (phaseIdx + 1)
835
+ : phaseIdx + 1;
836
+ if (!phase.sprints) phase.sprints = [];
837
+ const sprintNum = phase.sprints.length + 1;
838
+ const padPhase = String(phaseNum).padStart(2, '0');
839
+ const sprintId = `${padPhase}.${sprintNum}`;
840
+
841
+ const sprint = {
842
+ id: sprintId,
843
+ number: sprintNum,
844
+ goal: flags.goal,
845
+ status: 'planned',
846
+ velocity_target: flags.velocity ? parseInt(flags.velocity, 10) : null,
847
+ velocity_actual: null,
848
+ started_at: null,
849
+ completed_at: null,
850
+ stories: [],
851
+ };
852
+ phase.sprints.push(sprint);
853
+ state.current_sprint = sprintId;
854
+ return writeStateCompact(state, { sprint_id: sprintId, phase: padPhase });
855
+ }
856
+
857
+ // --- sprint list [--phase NN] ---
858
+ if (sub === 'sprint' && subArgs[1] === 'list') {
859
+ const flags = parseFlags(2);
860
+ const state = readState() || defaultState();
861
+ const results = [];
862
+ for (const phase of (state.phases || [])) {
863
+ if (flags.phase && String(phase.number) !== String(flags.phase)) continue;
864
+ for (const s of (phase.sprints || [])) {
865
+ const done = (s.stories || []).filter(t => t.status === 'done').length;
866
+ const total = (s.stories || []).length;
867
+ const points_done = (s.stories || []).filter(t => t.status === 'done').reduce((a, t) => a + (t.points || 0), 0);
868
+ const points_total = (s.stories || []).reduce((a, t) => a + (t.points || 0), 0);
869
+ results.push({
870
+ id: s.id, goal: s.goal, status: s.status,
871
+ stories: `${done}/${total}`, points: `${points_done}/${points_total}`,
872
+ velocity_target: s.velocity_target,
873
+ });
874
+ }
875
+ }
876
+ return results;
877
+ }
878
+
879
+ // --- sprint status [--sprint NN.S] ---
880
+ if (sub === 'sprint' && subArgs[1] === 'status') {
881
+ const flags = parseFlags(2);
882
+ const state = readState() || defaultState();
883
+ const targetId = flags.sprint || state.current_sprint;
884
+ if (!targetId) throw new Error('No current sprint. Use --sprint NN.S or run sprint add first.');
885
+
886
+ let found = null;
887
+ for (const phase of (state.phases || [])) {
888
+ for (const s of (phase.sprints || [])) {
889
+ if (s.id === targetId) { found = s; break; }
890
+ }
891
+ if (found) break;
892
+ }
893
+ if (!found) throw new Error(`Sprint "${targetId}" not found`);
894
+
895
+ const stories = found.stories || [];
896
+ const byStatus = { todo: [], in_progress: [], review: [], done: [] };
897
+ for (const st of stories) (byStatus[st.status] || byStatus.todo).push(st);
898
+ const points_done = byStatus.done.reduce((a, t) => a + (t.points || 0), 0);
899
+ const points_total = stories.reduce((a, t) => a + (t.points || 0), 0);
900
+
901
+ return {
902
+ sprint: found.id, goal: found.goal, status: found.status,
903
+ velocity_target: found.velocity_target, velocity_actual: found.velocity_actual,
904
+ stories: { todo: byStatus.todo.length, in_progress: byStatus.in_progress.length,
905
+ review: byStatus.review.length, done: byStatus.done.length, total: stories.length },
906
+ points: { done: points_done, total: points_total,
907
+ remaining: points_total - points_done },
908
+ };
909
+ }
910
+
911
+ // --- sprint start [--sprint NN.S] ---
912
+ if (sub === 'sprint' && subArgs[1] === 'start') {
913
+ const flags = parseFlags(2);
914
+ const state = readState() || defaultState();
915
+ const targetId = flags.sprint || state.current_sprint;
916
+ if (!targetId) throw new Error('No sprint to start. Use --sprint NN.S.');
917
+
918
+ for (const phase of (state.phases || [])) {
919
+ for (const s of (phase.sprints || [])) {
920
+ if (s.id === targetId) {
921
+ s.status = 'active';
922
+ s.started_at = new Date().toISOString();
923
+ state.current_sprint = targetId;
924
+ return writeStateCompact(state, { started: targetId });
925
+ }
926
+ }
927
+ }
928
+ throw new Error(`Sprint "${targetId}" not found`);
929
+ }
930
+
931
+ // --- sprint complete [--sprint NN.S] ---
932
+ if (sub === 'sprint' && subArgs[1] === 'complete') {
933
+ const flags = parseFlags(2);
934
+ const state = readState() || defaultState();
935
+ const targetId = flags.sprint || state.current_sprint;
936
+ if (!targetId) throw new Error('No sprint to complete. Use --sprint NN.S.');
937
+
938
+ for (const phase of (state.phases || [])) {
939
+ for (const s of (phase.sprints || [])) {
940
+ if (s.id === targetId) {
941
+ const points_done = (s.stories || []).filter(t => t.status === 'done').reduce((a, t) => a + (t.points || 0), 0);
942
+ s.status = 'completed';
943
+ s.completed_at = new Date().toISOString();
944
+ s.velocity_actual = points_done;
945
+ if (!state.velocity_history) state.velocity_history = [];
946
+ state.velocity_history.push({ sprint: targetId, points: points_done, completed_at: s.completed_at });
947
+ state.current_sprint = null;
948
+ return writeStateCompact(state, { completed: targetId, velocity: points_done });
949
+ }
950
+ }
951
+ }
952
+ throw new Error(`Sprint "${targetId}" not found`);
953
+ }
954
+
955
+ // --- sprint velocity ---
956
+ if (sub === 'sprint' && subArgs[1] === 'velocity') {
957
+ const state = readState() || defaultState();
958
+ const history = state.velocity_history || [];
959
+ const avg = history.length > 0
960
+ ? Math.round(history.reduce((a, v) => a + v.points, 0) / history.length)
961
+ : 0;
962
+ return { history, average_velocity: avg, sprint_count: history.length };
963
+ }
964
+
965
+ // --- story add --sprint NN.S --title "Story title" --points N ---
966
+ if (sub === 'story' && subArgs[1] === 'add') {
967
+ const flags = parseFlags(2);
968
+ const state = readState() || defaultState();
969
+ const sprintId = flags.sprint || state.current_sprint;
970
+ if (!sprintId) throw new Error('story add requires --sprint NN.S or an active sprint');
971
+ if (!flags.title) throw new Error('story add requires --title "Story title"');
972
+
973
+ for (const phase of (state.phases || [])) {
974
+ for (const s of (phase.sprints || [])) {
975
+ if (s.id === sprintId) {
976
+ if (!s.stories) s.stories = [];
977
+ const storyNum = s.stories.length + 1;
978
+ const storyId = `${sprintId}.${String(storyNum).padStart(2, '0')}`;
979
+ const story = {
980
+ id: storyId,
981
+ title: flags.title,
982
+ points: flags.points ? parseInt(flags.points, 10) : 0,
983
+ status: 'todo',
984
+ acceptance: flags.acceptance || null,
985
+ };
986
+ s.stories.push(story);
987
+ return writeStateCompact(state, { story_id: storyId, sprint: sprintId });
988
+ }
989
+ }
990
+ }
991
+ throw new Error(`Sprint "${sprintId}" not found`);
992
+ }
993
+
994
+ // --- story move --id NN.S.TT --status done ---
995
+ if (sub === 'story' && subArgs[1] === 'move') {
996
+ const flags = parseFlags(2);
997
+ const state = readState() || defaultState();
998
+ if (!flags.id) throw new Error('story move requires --id NN.S.TT');
999
+ if (!flags.status) throw new Error('story move requires --status <todo|in_progress|review|done>');
1000
+ const validStatuses = ['todo', 'in_progress', 'review', 'done'];
1001
+ if (!validStatuses.includes(flags.status)) throw new Error(`Invalid status "${flags.status}". Valid: ${validStatuses.join(', ')}`);
1002
+
1003
+ for (const phase of (state.phases || [])) {
1004
+ for (const s of (phase.sprints || [])) {
1005
+ for (const story of (s.stories || [])) {
1006
+ if (story.id === flags.id) {
1007
+ const prev = story.status;
1008
+ story.status = flags.status;
1009
+ return writeStateCompact(state, { story: flags.id, from: prev, to: flags.status });
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ throw new Error(`Story "${flags.id}" not found`);
1015
+ }
1016
+
1017
+ // --- story list [--sprint NN.S] [--status todo|in_progress|done] ---
1018
+ if (sub === 'story' && subArgs[1] === 'list') {
1019
+ const flags = parseFlags(2);
1020
+ const state = readState() || defaultState();
1021
+ const sprintId = flags.sprint || state.current_sprint;
1022
+ const results = [];
1023
+ for (const phase of (state.phases || [])) {
1024
+ for (const s of (phase.sprints || [])) {
1025
+ if (sprintId && s.id !== sprintId) continue;
1026
+ for (const story of (s.stories || [])) {
1027
+ if (flags.status && story.status !== flags.status) continue;
1028
+ results.push({ ...story, sprint: s.id });
1029
+ }
1030
+ }
1031
+ }
1032
+ return results;
1033
+ }
1034
+
1035
+ // --- record-execution ---
1036
+ if (sub === 'record-execution') {
1037
+ const flags = parseFlags(1);
1038
+ const state = readState() || defaultState();
1039
+ if (!state.executions) state.executions = [];
1040
+ state.executions.push({
1041
+ plan: flags.plan || '',
1042
+ tasks: parseInt(flags.tasks || '0', 10),
1043
+ duration_ms: flags.duration ? parseInt(flags.duration, 10) : null,
1044
+ commit_hash: flags.hash || null,
1045
+ committed_at: new Date().toISOString(),
1046
+ });
1047
+ return writeState(state);
1048
+ }
1049
+
1050
+ // --- add-decision ---
1051
+ if (sub === 'add-decision') {
1052
+ const summary = subArgs.slice(1).join(' ');
1053
+ if (!summary) throw new Error('add-decision requires a summary argument');
1054
+ const state = readState() || defaultState();
1055
+ if (!state.decisions) state.decisions = [];
1056
+ const record = {
1057
+ summary,
1058
+ phase: state.current_phase,
1059
+ plan: state.current_plan,
1060
+ date: new Date().toISOString(),
1061
+ };
1062
+ state.decisions.push(record);
1063
+ const result = writeState(state);
1064
+ // Mirror to cross-project store (best-effort, never fails the local write).
1065
+ try {
1066
+ appendGlobalDecision({
1067
+ ts: record.date,
1068
+ project: state.project || path.basename(PROJECT_ROOT),
1069
+ project_root: PROJECT_ROOT,
1070
+ phase: record.phase,
1071
+ plan: record.plan,
1072
+ summary: record.summary,
1073
+ });
1074
+ } catch (_) { /* silent — local commit must not break on home-dir issues */ }
1075
+ return result;
1076
+ }
1077
+
1078
+ // --- decisions-global: query ~/.rihal/decisions.jsonl across all projects ---
1079
+ if (sub === 'decisions-global') {
1080
+ const flags = parseFlags(1);
1081
+ const limit = Math.max(1, parseInt(flags.limit || '20', 10));
1082
+ const sinceMs = flags.since ? Date.parse(flags.since) : null;
1083
+ const lines = readGlobalDecisions();
1084
+ const filtered = lines.filter((d) => {
1085
+ if (flags.project && d.project !== flags.project) return false;
1086
+ if (sinceMs && Date.parse(d.ts) < sinceMs) return false;
1087
+ return true;
1088
+ });
1089
+ // newest first
1090
+ filtered.sort((a, b) => (a.ts < b.ts ? 1 : -1));
1091
+ return { decisions: filtered.slice(0, limit), total: filtered.length };
1092
+ }
1093
+
1094
+ // --- add-blocker ---
1095
+ if (sub === 'add-blocker') {
1096
+ const description = subArgs.slice(1).join(' ');
1097
+ if (!description) throw new Error('add-blocker requires a description argument');
1098
+ const state = readState() || defaultState();
1099
+ if (!state.blockers) state.blockers = [];
1100
+ state.blockers.push({
1101
+ description,
1102
+ phase: state.current_phase,
1103
+ plan: state.current_plan,
1104
+ date: new Date().toISOString(),
1105
+ resolved: null,
1106
+ });
1107
+ return writeState(state);
1108
+ }
1109
+
1110
+ // --- resolve-blocker ---
1111
+ if (sub === 'resolve-blocker') {
1112
+ const index = parseInt(subArgs[1], 10);
1113
+ const state = readState();
1114
+ if (!state) throw new Error('No state.json found');
1115
+ if (!state.blockers || index < 0 || index >= state.blockers.length) {
1116
+ throw new Error(`Invalid blocker index: ${subArgs[1]}. Valid range: 0-${(state.blockers || []).length - 1}`);
1117
+ }
1118
+ state.blockers[index].resolved = new Date().toISOString();
1119
+ return writeState(state);
1120
+ }
1121
+
1122
+ // --- record-session ---
1123
+ if (sub === 'record-session') {
1124
+ const state = readState() || defaultState();
1125
+ state.last_session = new Date().toISOString();
1126
+ return writeState(state);
1127
+ }
1128
+
1129
+ // --- record-council ---
1130
+ if (sub === 'record-council') {
1131
+ const flags = parseFlags(1);
1132
+ if (!flags.slug) throw new Error('record-council requires --slug <value>');
1133
+ const state = readState() || defaultState();
1134
+ if (!state.council_sessions) state.council_sessions = [];
1135
+ state.council_sessions.push({
1136
+ date: new Date().toISOString(),
1137
+ question_slug: flags.slug || '',
1138
+ panel: (flags.panel || '').split(',').map((s) => s.trim()).filter(Boolean),
1139
+ artifact_path: flags.artifact || '',
1140
+ });
1141
+ return writeState(state);
1142
+ }
1143
+
1144
+ // --- record-chain ---
1145
+ if (sub === 'record-chain') {
1146
+ const flags = parseFlags(1);
1147
+ if (!flags.slug) throw new Error('record-chain requires --slug <value>');
1148
+ const state = readState() || defaultState();
1149
+ if (!state.chains) state.chains = [];
1150
+ state.chains.push({
1151
+ date: new Date().toISOString(),
1152
+ slug: flags.slug || '',
1153
+ agents: (flags.agents || '').split(',').map((s) => s.trim()).filter(Boolean),
1154
+ artifacts_dir: flags.artifacts || '',
1155
+ });
1156
+ return writeState(state);
1157
+ }
1158
+
1159
+ // --- insert-phase ---
1160
+ if (sub === 'insert-phase') {
1161
+ const flags = parseFlags(1);
1162
+ const phaseNumber = flags.number || '';
1163
+ const phaseName = flags.name || '';
1164
+
1165
+ // Validate N.M format
1166
+ const phaseRegex = /^\d+\.\d+$/;
1167
+ if (!phaseRegex.test(phaseNumber)) {
1168
+ throw new Error(`Invalid phase number format: ${phaseNumber}. Expected N.M (e.g., 2.1, 3.2)`);
1169
+ }
1170
+
1171
+ if (!phaseName) {
1172
+ throw new Error('insert-phase requires --name <phase-name>');
1173
+ }
1174
+
1175
+ // Generate slug from name: lowercase, hyphenate spaces/underscores
1176
+ const slug = phaseName
1177
+ .toLowerCase()
1178
+ .replace(/[^a-z0-9\s-]/g, '')
1179
+ .replace(/\s+/g, '-')
1180
+ .replace(/-+/g, '-')
1181
+ .replace(/^-+|-+$/g, '');
1182
+
1183
+ if (!slug) {
1184
+ throw new Error('Phase name must contain at least one alphanumeric character');
1185
+ }
1186
+
1187
+ const state = readState() || defaultState();
1188
+ if (!state.phases) state.phases = [];
1189
+
1190
+ // Check if phase already exists
1191
+ if (state.phases.some(p => p.number === phaseNumber)) {
1192
+ throw new Error(`Phase ${phaseNumber} already exists`);
1193
+ }
1194
+
1195
+ // Helper to convert phase number to comparable tuple
1196
+ function phaseTuple(s) {
1197
+ const [maj, min] = s.split('.').map(x => parseInt(x, 10));
1198
+ return [maj, min || 0];
1199
+ }
1200
+
1201
+ // Helper to compare phase tuples
1202
+ function cmpPhase(a, b) {
1203
+ const [a1, a2] = phaseTuple(a);
1204
+ const [b1, b2] = phaseTuple(b);
1205
+ return a1 - b1 || a2 - b2;
1206
+ }
1207
+
1208
+ // Insert phase in sorted order
1209
+ const newPhase = {
1210
+ number: phaseNumber,
1211
+ name: phaseName,
1212
+ slug: slug,
1213
+ created: new Date().toISOString(),
1214
+ started: null,
1215
+ completed: null,
1216
+ };
1217
+
1218
+ const insertIdx = state.phases.findIndex(p => {
1219
+ return cmpPhase(p.number, phaseNumber) > 0;
1220
+ });
1221
+
1222
+ if (insertIdx === -1) {
1223
+ state.phases.push(newPhase);
1224
+ } else {
1225
+ state.phases.splice(insertIdx, 0, newPhase);
1226
+ }
1227
+
1228
+ writeState(state);
1229
+ return {
1230
+ ok: true,
1231
+ phase_number: phaseNumber,
1232
+ name: phaseName,
1233
+ slug: slug,
1234
+ directory: path.join(PLANNING_DIR, 'phases', `${phaseNumber}-${slug}`),
1235
+ };
1236
+ }
1237
+
1238
+ // --- workstream-validate ---
1239
+ if (sub === 'workstream-validate') {
1240
+ const subcommand = subArgs[1];
1241
+ const flags = parseFlags(2);
1242
+ const name = flags.name || '';
1243
+
1244
+ if (!subcommand || !['create', 'switch', 'list', 'status', 'complete'].includes(subcommand)) {
1245
+ throw new Error(`Invalid workstream subcommand: ${subcommand}. Valid: create, switch, list, status, complete`);
1246
+ }
1247
+
1248
+ if (['create', 'switch', 'complete'].includes(subcommand) && !name) {
1249
+ throw new Error(`workstream ${subcommand} requires --name <name>`);
1250
+ }
1251
+
1252
+ const state = readState() || defaultState();
1253
+ if (!state.workstreams) state.workstreams = [];
1254
+
1255
+ if (subcommand === 'create') {
1256
+ if (state.workstreams.some((w) => w.name === name)) {
1257
+ throw new Error(`Workstream already exists: ${name}`);
1258
+ }
1259
+ } else if (['switch', 'complete'].includes(subcommand)) {
1260
+ if (!state.workstreams.some((w) => w.name === name)) {
1261
+ throw new Error(`Workstream not found: ${name}`);
1262
+ }
1263
+ }
1264
+
1265
+ return { ok: true, valid: true };
1266
+ }
1267
+
1268
+ // --- workstream-create ---
1269
+ if (sub === 'workstream-create') {
1270
+ const flags = parseFlags(1);
1271
+ const name = flags.name || '';
1272
+ if (!name) throw new Error('workstream-create requires --name <name>');
1273
+
1274
+ const state = readState() || defaultState();
1275
+ if (!state.workstreams) state.workstreams = [];
1276
+ if (state.workstreams.some((w) => w.name === name)) {
1277
+ throw new Error(`Workstream already exists: ${name}`);
1278
+ }
1279
+
1280
+ // Create new workstream
1281
+ const now = new Date().toISOString();
1282
+ const id = `ws-${Date.now().toString(36).slice(-8)}`;
1283
+ const newWorkstream = {
1284
+ name,
1285
+ id,
1286
+ created: now,
1287
+ active: true,
1288
+ completed: false,
1289
+ phases: [],
1290
+ };
1291
+
1292
+ // Deactivate other workstreams
1293
+ state.workstreams.forEach((w) => { w.active = false; });
1294
+ state.workstreams.push(newWorkstream);
1295
+ state.active_workstream = name;
1296
+
1297
+ return writeState(state);
1298
+ }
1299
+
1300
+ // --- workstream-switch ---
1301
+ if (sub === 'workstream-switch') {
1302
+ const flags = parseFlags(1);
1303
+ const name = flags.name || '';
1304
+ if (!name) throw new Error('workstream-switch requires --name <name>');
1305
+
1306
+ const state = readState() || defaultState();
1307
+ if (!state.workstreams) state.workstreams = [];
1308
+
1309
+ const ws = state.workstreams.find((w) => w.name === name);
1310
+ if (!ws) throw new Error(`Workstream not found: ${name}`);
1311
+
1312
+ // Deactivate others, activate target
1313
+ state.workstreams.forEach((w) => { w.active = w.name === name; });
1314
+ state.active_workstream = name;
1315
+
1316
+ return writeState(state);
1317
+ }
1318
+
1319
+ // --- workstream-list ---
1320
+ if (sub === 'workstream-list') {
1321
+ const state = readState() || defaultState();
1322
+ if (!state.workstreams) state.workstreams = [];
1323
+
1324
+ return {
1325
+ ok: true,
1326
+ workstreams: state.workstreams.map((w) => ({
1327
+ name: w.name,
1328
+ id: w.id || '',
1329
+ active: w.active || false,
1330
+ completed: w.completed || false,
1331
+ phases: (w.phases || []).length,
1332
+ created: w.created || '',
1333
+ })),
1334
+ };
1335
+ }
1336
+
1337
+ // --- workstream-status ---
1338
+ if (sub === 'workstream-status') {
1339
+ const state = readState() || defaultState();
1340
+ if (!state.workstreams) state.workstreams = [];
1341
+
1342
+ const active = state.workstreams.find((w) => w.active) || state.workstreams[0];
1343
+ if (!active) {
1344
+ return { ok: true, workstream: null, message: 'No workstreams exist' };
1345
+ }
1346
+
1347
+ return {
1348
+ ok: true,
1349
+ workstream: {
1350
+ name: active.name,
1351
+ id: active.id || '',
1352
+ active: active.active || false,
1353
+ completed: active.completed || false,
1354
+ phases: (active.phases || []).length,
1355
+ created: active.created || '',
1356
+ },
1357
+ };
1358
+ }
1359
+
1360
+ // --- workstream-complete ---
1361
+ if (sub === 'workstream-complete') {
1362
+ const flags = parseFlags(1);
1363
+ const name = flags.name || '';
1364
+ if (!name) throw new Error('workstream-complete requires --name <name>');
1365
+
1366
+ const state = readState() || defaultState();
1367
+ if (!state.workstreams) state.workstreams = [];
1368
+
1369
+ const ws = state.workstreams.find((w) => w.name === name);
1370
+ if (!ws) throw new Error(`Workstream not found: ${name}`);
1371
+ if (ws.completed) throw new Error(`Workstream already completed: ${name}`);
1372
+
1373
+ ws.completed = true;
1374
+ ws.active = false;
1375
+
1376
+ // If this was the active workstream, switch to first incomplete
1377
+ if (state.active_workstream === name) {
1378
+ const next = state.workstreams.find((w) => !w.completed);
1379
+ if (next) {
1380
+ next.active = true;
1381
+ state.active_workstream = next.name;
1382
+ } else {
1383
+ state.active_workstream = null;
1384
+ }
1385
+ }
1386
+
1387
+ return writeState(state);
1388
+ }
1389
+
1390
+ // --- set-user-profile / write-profile ---
1391
+ if (sub === 'set-user-profile' || sub === 'write-profile') {
1392
+ const flags = parseFlags(1);
1393
+ if (!flags.json) throw new Error('write-profile requires --json <json-blob>');
1394
+ const state = readState() || defaultState();
1395
+ if (!state.user_profile) state.user_profile = {};
1396
+ try {
1397
+ state.user_profile = JSON.parse(flags.json);
1398
+ } catch (e) {
1399
+ throw new Error(`Invalid JSON in --json flag: ${e.message}`);
1400
+ }
1401
+ return writeState(state);
1402
+ }
1403
+
1404
+ // --- next-phase-id ---
1405
+ if (sub === 'next-phase-id') {
1406
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
1407
+ let maxNum = 0;
1408
+ if (fs.existsSync(phasesDir)) {
1409
+ const entries = fs.readdirSync(phasesDir);
1410
+ for (const entry of entries) {
1411
+ const match = entry.match(/^(\d{2})-/);
1412
+ if (match) {
1413
+ const num = parseInt(match[1], 10);
1414
+ maxNum = Math.max(maxNum, num);
1415
+ }
1416
+ }
1417
+ }
1418
+ const nextId = String(maxNum + 1).padStart(2, '0');
1419
+ return { ok: true, next_phase_id: nextId };
1420
+ }
1421
+
1422
+ // --- next-plan-id <phase-id> ---
1423
+ if (sub === 'next-plan-id') {
1424
+ const phaseId = subArgs[1];
1425
+ if (!phaseId) throw new Error('next-plan-id requires a phase ID argument (NN format)');
1426
+ const phaseMatch = phaseId.match(/^(\d{2})(?:\.(\d+))?$/);
1427
+ if (!phaseMatch) throw new Error(`Invalid phase ID format: ${phaseId}. Expected NN or NN.M`);
1428
+
1429
+ const phasePart = phaseMatch[1];
1430
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
1431
+
1432
+ // Find the phase directory matching NN-*
1433
+ let phaseDir = null;
1434
+ if (fs.existsSync(phasesDir)) {
1435
+ const entries = fs.readdirSync(phasesDir);
1436
+ for (const entry of entries) {
1437
+ const match = entry.match(/^(\d{2})(?:\.\d+)?-/);
1438
+ if (match && match[1] === phasePart) {
1439
+ phaseDir = path.join(phasesDir, entry);
1440
+ break;
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ // If no phase dir found, default to 01 plan
1446
+ if (!phaseDir) {
1447
+ return { ok: true, next_plan_id: `${phasePart}.01` };
1448
+ }
1449
+
1450
+ // Scan phase dir for numbered subdirs (MM-*) to find max plan number
1451
+ let maxPlanNum = 0;
1452
+ const entries = fs.readdirSync(phaseDir);
1453
+ for (const entry of entries) {
1454
+ const match = entry.match(/^(\d{2})-/);
1455
+ if (match && fs.statSync(path.join(phaseDir, entry)).isDirectory()) {
1456
+ const num = parseInt(match[1], 10);
1457
+ maxPlanNum = Math.max(maxPlanNum, num);
1458
+ }
1459
+ }
1460
+
1461
+ const nextPlanNum = String(maxPlanNum + 1).padStart(2, '0');
1462
+ // First plan in empty phase gets .01 not .02
1463
+ return { ok: true, next_plan_id: maxPlanNum === 0 ? `${phasePart}.01` : `${phasePart}.${nextPlanNum}` };
1464
+ }
1465
+
1466
+ // --- next-task-id <plan-id> ---
1467
+ if (sub === 'next-task-id') {
1468
+ const planId = subArgs[1];
1469
+ if (!planId) throw new Error('next-task-id requires a plan ID argument (NN.MM format)');
1470
+ const match = planId.match(/^(\d{2})\.(\d{2})$/);
1471
+ if (!match) throw new Error(`Invalid plan ID format: ${planId}. Expected NN.MM`);
1472
+
1473
+ const phasePart = match[1];
1474
+ const planPart = match[2];
1475
+
1476
+ // Construct plan file path
1477
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
1478
+ let planFile = null;
1479
+
1480
+ if (fs.existsSync(phasesDir)) {
1481
+ const entries = fs.readdirSync(phasesDir);
1482
+ for (const entry of entries) {
1483
+ const phaseMatch = entry.match(/^(\d{2})(?:\.\d+)?-/);
1484
+ if (phaseMatch && phaseMatch[1] === phasePart) {
1485
+ const phaseDir = path.join(phasesDir, entry);
1486
+
1487
+ // Check for subdirectory named planPart-*
1488
+ const subentries = fs.readdirSync(phaseDir);
1489
+ for (const subentry of subentries) {
1490
+ const subMatch = subentry.match(/^(\d{2})-/);
1491
+ if (subMatch && subMatch[1] === planPart) {
1492
+ const planDir = path.join(phaseDir, subentry);
1493
+ const candidate = path.join(planDir, 'SPRINT.md');
1494
+ if (fs.existsSync(candidate)) {
1495
+ planFile = candidate;
1496
+ break;
1497
+ }
1498
+ }
1499
+ }
1500
+
1501
+ // If no subdir found, check phase-level PLAN.md
1502
+ if (!planFile && planPart === '01') {
1503
+ const candidate = path.join(phaseDir, 'SPRINT.md');
1504
+ if (fs.existsSync(candidate)) {
1505
+ planFile = candidate;
1506
+ }
1507
+ }
1508
+ break;
1509
+ }
1510
+ }
1511
+ }
1512
+
1513
+ if (!planFile) {
1514
+ throw new Error(`Plan ${planId} not found. Ensure phase and plan directories exist.`);
1515
+ }
1516
+
1517
+ // Read PLAN.md and count existing tasks
1518
+ const planContent = fs.readFileSync(planFile, 'utf8');
1519
+ const taskMatches = planContent.match(/^### Task \d+\.\d+\.\d+ —/gm) || [];
1520
+ const nextTaskNum = String(taskMatches.length + 1).padStart(2, '0');
1521
+
1522
+ return { ok: true, next_task_id: `${planId}.${nextTaskNum}` };
1523
+ }
1524
+
1525
+ // --- resolve-id <id> ---
1526
+ if (sub === 'resolve-id') {
1527
+ const id = subArgs[1];
1528
+ if (!id) throw new Error('resolve-id requires an ID argument (NN, NN.MM, NN.MM.TT, or M{N})');
1529
+
1530
+ // Parse ID pattern
1531
+ let idType = null;
1532
+ let phaseId = null, planId = null, taskId = null, milestoneId = null;
1533
+
1534
+ if (/^M\d+$/.test(id)) {
1535
+ idType = 'milestone';
1536
+ milestoneId = id;
1537
+ } else if (/^\d{2}$/.test(id)) {
1538
+ idType = 'phase';
1539
+ phaseId = id;
1540
+ } else if (/^\d{2}\.\d+$/.test(id)) {
1541
+ const parts = id.split('.');
1542
+ phaseId = parts[0];
1543
+
1544
+ // Determine if this is a decimal phase or a plan
1545
+ // Check if directory ends in .M pattern
1546
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
1547
+ let isDecimalPhase = false;
1548
+ if (fs.existsSync(phasesDir)) {
1549
+ const entries = fs.readdirSync(phasesDir);
1550
+ for (const entry of entries) {
1551
+ if (entry.match(/^\d{2}\.\d+-/)) {
1552
+ isDecimalPhase = true;
1553
+ break;
1554
+ }
1555
+ }
1556
+ }
1557
+
1558
+ if (isDecimalPhase) {
1559
+ idType = 'decimal-phase';
1560
+ } else {
1561
+ idType = 'plan';
1562
+ planId = id;
1563
+ }
1564
+ } else if (/^\d{2}\.\d+\.\d+$/.test(id)) {
1565
+ idType = 'task';
1566
+ const parts = id.split('.');
1567
+ phaseId = parts[0];
1568
+ planId = `${parts[0]}.${parts[1]}`;
1569
+ taskId = id;
1570
+ } else {
1571
+ throw new Error(`Invalid ID format: ${id}. Valid formats: NN (phase), NN.MM (plan), NN.MM.TT (task), MN (milestone)`);
1572
+ }
1573
+
1574
+ // Build response
1575
+ const result = {
1576
+ id,
1577
+ type: idType,
1578
+ phase_id: phaseId,
1579
+ plan_id: planId,
1580
+ task_id: taskId,
1581
+ milestone_id: milestoneId,
1582
+ path: null,
1583
+ phase_dir: null,
1584
+ plan_dir: null,
1585
+ status: 'pending',
1586
+ };
1587
+
1588
+ // Resolve paths
1589
+ if (phaseId) {
1590
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
1591
+ if (fs.existsSync(phasesDir)) {
1592
+ const entries = fs.readdirSync(phasesDir);
1593
+ for (const entry of entries) {
1594
+ const match = entry.match(/^(\d{2})-/);
1595
+ if (match && match[1] === phaseId) {
1596
+ const phaseDir = path.join(phasesDir, entry);
1597
+ result.phase_dir = phaseDir;
1598
+
1599
+ // Resolve plan path if plan_id is set
1600
+ if (planId) {
1601
+ const planNum = planId.split('.')[1];
1602
+
1603
+ // Check for subdirectory
1604
+ const subentries = fs.readdirSync(phaseDir);
1605
+ for (const subentry of subentries) {
1606
+ const subMatch = subentry.match(/^(\d{2})-/);
1607
+ if (subMatch && subMatch[1] === planNum) {
1608
+ const planDir = path.join(phaseDir, subentry);
1609
+ const planPath = path.join(planDir, 'SPRINT.md');
1610
+ if (fs.existsSync(planPath)) {
1611
+ result.plan_dir = planDir;
1612
+ result.path = planPath;
1613
+ }
1614
+ break;
1615
+ }
1616
+ }
1617
+
1618
+ // If no subdir and planNum is 01, check phase-level PLAN.md
1619
+ if (!result.path && planNum === '01') {
1620
+ const candidate = path.join(phaseDir, 'SPRINT.md');
1621
+ if (fs.existsSync(candidate)) {
1622
+ result.plan_dir = phaseDir;
1623
+ result.path = candidate;
1624
+ }
1625
+ }
1626
+ }
1627
+ break;
1628
+ }
1629
+ }
1630
+ }
1631
+ }
1632
+
1633
+ // Resolve milestone path if milestone_id is set
1634
+ if (milestoneId) {
1635
+ const milestonesDir = path.join(PLANNING_DIR, 'milestones');
1636
+ if (fs.existsSync(milestonesDir)) {
1637
+ const entries = fs.readdirSync(milestonesDir);
1638
+ for (const entry of entries) {
1639
+ if (entry.match(new RegExp(`^${milestoneId}-`))) {
1640
+ result.path = path.join(milestonesDir, entry, 'ROADMAP.md');
1641
+ break;
1642
+ }
1643
+ }
1644
+ }
1645
+ }
1646
+
1647
+ // Determine status
1648
+ let status = 'not_found';
1649
+ if (result.phase_dir && fs.existsSync(result.phase_dir)) {
1650
+ status = 'found';
1651
+ // Check if SUMMARY exists for "complete"
1652
+ if (result.plan_dir) {
1653
+ const summaryFiles = fs.existsSync(result.plan_dir) ?
1654
+ fs.readdirSync(result.plan_dir).filter(f => f.endsWith('-SUMMARY.md')) : [];
1655
+ if (summaryFiles.length > 0) status = 'complete';
1656
+ else if (fs.existsSync(path.join(result.plan_dir, 'SPRINT.md'))) status = 'planned';
1657
+ }
1658
+ }
1659
+ result.status = status;
1660
+
1661
+ return result;
1662
+ }
1663
+
1664
+ // --- set-ids-in-state ---
1665
+ if (sub === 'set-ids-in-state') {
1666
+ const state = readState() || defaultState();
1667
+ if (!state.phases) state.phases = [];
1668
+ if (!state.plans) state.plans = [];
1669
+ if (!state.milestones) state.milestones = [];
1670
+
1671
+ // Scan phases directory
1672
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
1673
+ if (fs.existsSync(phasesDir)) {
1674
+ const entries = fs.readdirSync(phasesDir);
1675
+ for (const entry of entries) {
1676
+ const match = entry.match(/^(\d{2})(?:\.\d+)?-(.+)$/);
1677
+ if (match) {
1678
+ const phaseId = match[1];
1679
+ const slug = match[2];
1680
+ const phaseDir = path.join(phasesDir, entry);
1681
+
1682
+ // Add phase if not already present
1683
+ if (!state.phases.some(p => p.id === phaseId)) {
1684
+ state.phases.push({
1685
+ id: phaseId,
1686
+ slug,
1687
+ path: phaseDir,
1688
+ created: new Date().toISOString(),
1689
+ });
1690
+ }
1691
+
1692
+ // Scan for plans within phase
1693
+ const subentries = fs.readdirSync(phaseDir);
1694
+ for (const subentry of subentries) {
1695
+ const subMatch = subentry.match(/^(\d{2})-(.+)$/);
1696
+ if (subMatch && fs.statSync(path.join(phaseDir, subentry)).isDirectory()) {
1697
+ const planNum = subMatch[1];
1698
+ const planId = `${phaseId}.${planNum}`;
1699
+ const planSlug = subMatch[2];
1700
+ const planDir = path.join(phaseDir, subentry);
1701
+ const planPath = path.join(planDir, 'SPRINT.md');
1702
+
1703
+ if (fs.existsSync(planPath)) {
1704
+ if (!state.plans.some(p => p.id === planId)) {
1705
+ state.plans.push({
1706
+ id: planId,
1707
+ phase_id: phaseId,
1708
+ slug: planSlug,
1709
+ path: planPath,
1710
+ created: new Date().toISOString(),
1711
+ });
1712
+ }
1713
+ }
1714
+ }
1715
+ }
1716
+
1717
+ // Check for phase-level PLAN.md (01 plan)
1718
+ const phasePlanPath = path.join(phaseDir, 'SPRINT.md');
1719
+ if (fs.existsSync(phasePlanPath)) {
1720
+ const planId = `${phaseId}.01`;
1721
+ if (!state.plans.some(p => p.id === planId)) {
1722
+ state.plans.push({
1723
+ id: planId,
1724
+ phase_id: phaseId,
1725
+ slug: 'default',
1726
+ path: phasePlanPath,
1727
+ created: new Date().toISOString(),
1728
+ });
1729
+ }
1730
+ }
1731
+ }
1732
+ }
1733
+ }
1734
+
1735
+ // Scan milestones directory
1736
+ const milestonesDir = path.join(PLANNING_DIR, 'milestones');
1737
+ if (fs.existsSync(milestonesDir)) {
1738
+ const entries = fs.readdirSync(milestonesDir);
1739
+ for (const entry of entries) {
1740
+ const match = entry.match(/^(M\d+)-(.+)$/);
1741
+ if (match) {
1742
+ const milestoneId = match[1];
1743
+ const slug = match[2];
1744
+ const milestonePath = path.join(milestonesDir, entry, 'ROADMAP.md');
1745
+
1746
+ if (!state.milestones.some(m => m.id === milestoneId)) {
1747
+ state.milestones.push({
1748
+ id: milestoneId,
1749
+ slug,
1750
+ path: milestonePath,
1751
+ created: new Date().toISOString(),
1752
+ });
1753
+ }
1754
+ }
1755
+ }
1756
+ }
1757
+
1758
+ return writeState(state);
1759
+ }
1760
+
1761
+ // --- migrate-ids ---
1762
+ if (sub === 'migrate-ids') {
1763
+ const state = readState() || defaultState();
1764
+ let migratedCount = 0;
1765
+
1766
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
1767
+ if (fs.existsSync(phasesDir)) {
1768
+ const entries = fs.readdirSync(phasesDir).sort();
1769
+ let phaseNum = 1;
1770
+
1771
+ for (const entry of entries) {
1772
+ const match = entry.match(/^(\d{2})-/);
1773
+ if (match) {
1774
+ phaseNum = parseInt(match[1], 10);
1775
+ }
1776
+
1777
+ const phaseDir = path.join(phasesDir, entry);
1778
+
1779
+ // Check for PLAN.md at phase level
1780
+ const phasePlanPath = path.join(phaseDir, 'SPRINT.md');
1781
+ if (fs.existsSync(phasePlanPath)) {
1782
+ try {
1783
+ let content = fs.readFileSync(phasePlanPath, 'utf8');
1784
+ const phaseIdStr = String(phaseNum).padStart(2, '0');
1785
+
1786
+ // Check if it has frontmatter with phase/plan fields
1787
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
1788
+ if (frontmatterMatch) {
1789
+ const fm = frontmatterMatch[1];
1790
+ if (!fm.match(/^id:/m)) {
1791
+ // Only add id if missing; preserve existing phase/plan if present
1792
+ let newFrontmatter = fm.trimEnd() + `\nid: "${phaseIdStr}.01"`;
1793
+ if (!fm.match(/^phase:/m)) newFrontmatter += `\nphase: "${phaseIdStr}"`;
1794
+ if (!fm.match(/^plan:/m)) newFrontmatter += `\nplan: "01"`;
1795
+ newFrontmatter += '\n';
1796
+ content = content.replace(/^---\n([\s\S]*?)\n---\n/, `---\n${newFrontmatter}---\n`);
1797
+ const tmp = phasePlanPath + '.tmp';
1798
+ fs.writeFileSync(tmp, content, 'utf8');
1799
+ fs.renameSync(tmp, phasePlanPath);
1800
+ migratedCount++;
1801
+ }
1802
+ } else {
1803
+ // No frontmatter found — prepend minimal frontmatter
1804
+ const assignedId = `${phaseIdStr}.01`;
1805
+ const minimal = `---\nid: "${assignedId}"\nphase: "${phaseIdStr}"\nplan: "01"\ntype: auto\n---\n`;
1806
+ fs.writeFileSync(phasePlanPath, minimal + content);
1807
+ migratedCount++;
1808
+ }
1809
+ } catch (e) {
1810
+ // Log but continue on file read/write errors
1811
+ if (process.env.DEBUG) console.error(`Warning: Could not migrate ${phasePlanPath}: ${e.message}`);
1812
+ }
1813
+ }
1814
+
1815
+ // Check for plan subdirs
1816
+ const subentries = fs.readdirSync(phaseDir);
1817
+ let planNum = 1;
1818
+ for (const subentry of subentries) {
1819
+ const subMatch = subentry.match(/^(\d{2})-/);
1820
+ if (subMatch && fs.statSync(path.join(phaseDir, subentry)).isDirectory()) {
1821
+ planNum = parseInt(subMatch[1], 10);
1822
+ const planDir = path.join(phaseDir, subentry);
1823
+ const planPath = path.join(planDir, 'SPRINT.md');
1824
+
1825
+ if (fs.existsSync(planPath)) {
1826
+ try {
1827
+ let content = fs.readFileSync(planPath, 'utf8');
1828
+ const phaseIdStr = String(phaseNum).padStart(2, '0');
1829
+ const planIdStr = String(planNum).padStart(2, '0');
1830
+
1831
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
1832
+ if (frontmatterMatch) {
1833
+ const fm = frontmatterMatch[1];
1834
+ if (!fm.match(/^id:/m)) {
1835
+ // Only add id if missing; preserve existing phase/plan if present
1836
+ let newFrontmatter = fm.trimEnd() + `\nid: "${phaseIdStr}.${planIdStr}"`;
1837
+ if (!fm.match(/^phase:/m)) newFrontmatter += `\nphase: "${phaseIdStr}"`;
1838
+ if (!fm.match(/^plan:/m)) newFrontmatter += `\nplan: "${planIdStr}"`;
1839
+ newFrontmatter += '\n';
1840
+ content = content.replace(/^---\n([\s\S]*?)\n---\n/, `---\n${newFrontmatter}---\n`);
1841
+ const tmp = planPath + '.tmp';
1842
+ fs.writeFileSync(tmp, content, 'utf8');
1843
+ fs.renameSync(tmp, planPath);
1844
+ migratedCount++;
1845
+ }
1846
+ } else {
1847
+ // No frontmatter found — prepend minimal frontmatter
1848
+ const assignedId = `${phaseIdStr}.${planIdStr}`;
1849
+ const minimal = `---\nid: "${assignedId}"\nphase: "${phaseIdStr}"\nplan: "${planIdStr}"\ntype: auto\n---\n`;
1850
+ fs.writeFileSync(planPath, minimal + content);
1851
+ migratedCount++;
1852
+ }
1853
+ } catch (e) {
1854
+ // Log but continue on file read/write errors
1855
+ if (process.env.DEBUG) console.error(`Warning: Could not migrate ${planPath}: ${e.message}`);
1856
+ }
1857
+ }
1858
+ }
1859
+ }
1860
+ }
1861
+ }
1862
+
1863
+ return { ok: true, migrated: migratedCount, message: `Migrated ${migratedCount} PLAN.md files with IDs` };
1864
+ }
1865
+
1866
+ // =====================================================================
1867
+ // Execution-lifecycle phase state
1868
+ // =====================================================================
1869
+
1870
+ if (sub === 'planned-phase') {
1871
+ const flags = parseFlags(1);
1872
+ if (!flags.phase) throw new Error('planned-phase requires --phase <N>');
1873
+ const state = readState() || defaultState();
1874
+ if (!state.phases) state.phases = [];
1875
+ const phaseKey = String(flags.phase);
1876
+ let entry = state.phases.find((p) => String(p.number || p.id || p.name) === phaseKey);
1877
+ const previousStatus = entry ? (entry.status || null) : null;
1878
+ if (!entry) {
1879
+ entry = { number: phaseKey, name: flags.name || phaseKey, plans: Number(flags.plans || 0) };
1880
+ state.phases.push(entry);
1881
+ }
1882
+ entry.status = 'planned';
1883
+ entry.name = flags.name || entry.name;
1884
+ if (flags.plans !== undefined) entry.plans = Number(flags.plans);
1885
+ entry.planned_at = new Date().toISOString();
1886
+ writeState(state);
1887
+ return { updated: true, phase: phaseKey, status: 'planned', previous_status: previousStatus, name: entry.name, plans: entry.plans };
1888
+ }
1889
+
1890
+ if (sub === 'begin-phase') {
1891
+ const flags = parseFlags(1);
1892
+ if (!flags.phase) throw new Error('begin-phase requires --phase <N>');
1893
+ const state = readState() || defaultState();
1894
+ if (!state.phases) state.phases = [];
1895
+ const phaseKey = String(flags.phase);
1896
+ let entry = state.phases.find((p) => String(p.number || p.id || p.name) === phaseKey);
1897
+ const previousStatus = entry ? (entry.status || null) : null;
1898
+ if (!entry) {
1899
+ entry = { number: phaseKey, name: flags.name || phaseKey, plans: Number(flags.plans || 0) };
1900
+ state.phases.push(entry);
1901
+ }
1902
+ entry.status = 'executing';
1903
+ if (flags.name) entry.name = flags.name;
1904
+ if (flags.plans !== undefined) entry.plans = Number(flags.plans);
1905
+ entry.started = entry.started || new Date().toISOString();
1906
+ state.current_phase = entry.name;
1907
+ writeState(state);
1908
+ return { updated: true, phase: phaseKey, status: 'executing', previous_status: previousStatus };
1909
+ }
1910
+
1911
+ if (sub === 'complete-phase') {
1912
+ const flags = parseFlags(1);
1913
+ if (!flags.phase) throw new Error('complete-phase requires --phase <N>');
1914
+ const state = readState() || defaultState();
1915
+ if (!state.phases) state.phases = [];
1916
+ const phaseKey = String(flags.phase);
1917
+ const entry = state.phases.find((p) => String(p.number || p.id || p.name) === phaseKey);
1918
+ if (!entry) throw new Error(`Phase ${phaseKey} not found in state`);
1919
+ const previousStatus = entry.status || null;
1920
+ entry.status = 'complete';
1921
+ entry.completed = new Date().toISOString();
1922
+ writeState(state);
1923
+ return { updated: true, phase: phaseKey, status: 'complete', previous_status: previousStatus };
1924
+ }
1925
+
1926
+ // Truncates execution state but preserves decisions, council_sessions, and workstreams.
1927
+ if (sub === 'reset') {
1928
+ const state = readState() || defaultState();
1929
+ const preserved = {
1930
+ version: state.version || '1',
1931
+ project: state.project || path.basename(PROJECT_ROOT),
1932
+ created: state.created || new Date().toISOString(),
1933
+ current_phase: null,
1934
+ current_plan: 0,
1935
+ current_sprint: null,
1936
+ phases: [],
1937
+ velocity_history: [],
1938
+ executions: [],
1939
+ decisions: state.decisions || [],
1940
+ blockers: [],
1941
+ council_sessions: state.council_sessions || [],
1942
+ last_session: state.last_session || null,
1943
+ workstreams: state.workstreams || [],
1944
+ active_workstream: state.active_workstream || null,
1945
+ };
1946
+ writeState(preserved);
1947
+ return { updated: true, status: 'reset', preserved_decisions: preserved.decisions.length };
1948
+ }
1949
+
1950
+ // --- promote-backlog <from> --to <target> ---
1951
+ // Promote a 999.x parking-lot phase to a real phase number.
1952
+ // Mutates state.phases[]; renames the on-disk directory if present.
1953
+ // Tracks issue #159 (M2.5 — GSD-parity 999.x convention).
1954
+ if (sub === 'promote-backlog') {
1955
+ const from = subArgs[1];
1956
+ const flags = parseFlags(2);
1957
+ const to = flags.to;
1958
+ if (!from || !to) {
1959
+ throw new Error('Usage: state promote-backlog <999.x> --to <NN>');
1960
+ }
1961
+ if (!/^999\.\d+$/.test(from)) {
1962
+ throw new Error(`Source must be 999.x parking-lot number, got: ${from}`);
1963
+ }
1964
+ if (!/^\d{1,3}(\.\d+)?$/.test(to)) {
1965
+ throw new Error(`Target must be NN or NN.M, got: ${to}`);
1966
+ }
1967
+ const state = readState() || defaultState();
1968
+ if (!state.phases) state.phases = [];
1969
+ const idx = state.phases.findIndex(p => String(p.number) === from);
1970
+ if (idx < 0) {
1971
+ throw new Error(`Parking-lot phase ${from} not found in state.phases`);
1972
+ }
1973
+ if (state.phases.some(p => String(p.number) === to)) {
1974
+ throw new Error(`Target phase ${to} already exists`);
1975
+ }
1976
+ const phase = state.phases[idx];
1977
+ const oldNumber = phase.number;
1978
+ phase.number = to;
1979
+ phase.promoted_from = oldNumber;
1980
+ phase.promoted_at = new Date().toISOString();
1981
+
1982
+ // Rename on-disk directory if present
1983
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
1984
+ let renamed = false;
1985
+ if (fs.existsSync(phasesDir)) {
1986
+ for (const entry of fs.readdirSync(phasesDir)) {
1987
+ if (entry.startsWith(`${oldNumber}-`) || entry === oldNumber) {
1988
+ const oldPath = path.join(phasesDir, entry);
1989
+ const newPath = path.join(phasesDir, entry.replace(oldNumber, to));
1990
+ fs.renameSync(oldPath, newPath);
1991
+ renamed = true;
1992
+ break;
1993
+ }
1994
+ }
1995
+ }
1996
+
1997
+ writeState(state);
1998
+ return { ok: true, promoted: { from: oldNumber, to }, renamed_disk: renamed };
1999
+ }
2000
+
2001
+ // --- sync --from-disk ---
2002
+ // Parse ROADMAP.md + epics.md and upsert milestones/phases/epics into state.json.
2003
+ // Preserves existing statuses on matching phase names/numbers.
2004
+ // Tracks: issue #126 (state desync between planning artifacts and state.json).
2005
+ if (sub === 'sync') {
2006
+ const flags = parseFlags(1);
2007
+ if (!flags['from-disk'] && flags['from-disk'] !== '') {
2008
+ // Support both "--from-disk" (flag) and "--from-disk true"
2009
+ // parseFlags consumes the next token as value; accept empty-string value.
2010
+ }
2011
+ const roadmapPath = path.join(PLANNING_DIR, 'ROADMAP.md');
2012
+ const epicsPath = path.join(PLANNING_DIR, 'epics.md');
2013
+ const state = readState() || defaultState();
2014
+
2015
+ const parsed = {
2016
+ milestones_found: 0,
2017
+ phases_found: 0,
2018
+ phases_upserted: 0,
2019
+ epics_found: 0,
2020
+ roadmap_exists: fs.existsSync(roadmapPath),
2021
+ epics_exists: fs.existsSync(epicsPath),
2022
+ };
2023
+
2024
+ // Parse ROADMAP.md for phase tables.
2025
+ // Expected row format: | 01 | Phase Name | Goal text | ... |
2026
+ // First cell is phase number (1-3 chars of digits or digits+letter).
2027
+ if (parsed.roadmap_exists) {
2028
+ const roadmap = fs.readFileSync(roadmapPath, 'utf8');
2029
+ parsed.milestones_found = (roadmap.match(/^##\s+Milestone\s+M\d+/gim) || []).length;
2030
+ const rowRe = /^\|\s*(\d{1,3}(?:\.\d+)?)\s*\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|/gm;
2031
+ let m;
2032
+ if (!state.phases) state.phases = [];
2033
+ while ((m = rowRe.exec(roadmap)) !== null) {
2034
+ const phaseNum = m[1].trim();
2035
+ const phaseName = m[2].trim();
2036
+ const phaseGoal = m[3].trim();
2037
+ // Skip header rows like "| # | Phase | Goal |"
2038
+ if (!/^\d/.test(phaseNum)) continue;
2039
+ if (phaseName.toLowerCase() === 'phase') continue;
2040
+ parsed.phases_found += 1;
2041
+ const existingIdx = state.phases.findIndex(p =>
2042
+ String(p.number) === phaseNum || p.name === phaseName
2043
+ );
2044
+ if (existingIdx >= 0) {
2045
+ // Preserve existing status fields; update metadata.
2046
+ state.phases[existingIdx].number = state.phases[existingIdx].number || phaseNum;
2047
+ state.phases[existingIdx].name = phaseName;
2048
+ if (phaseGoal) state.phases[existingIdx].goal = phaseGoal;
2049
+ } else {
2050
+ state.phases.push({
2051
+ number: phaseNum,
2052
+ name: phaseName,
2053
+ goal: phaseGoal,
2054
+ status: 'planned',
2055
+ started: null,
2056
+ completed: null,
2057
+ plan_count: 0,
2058
+ });
2059
+ parsed.phases_upserted += 1;
2060
+ }
2061
+ }
2062
+ }
2063
+
2064
+ // Parse epics.md for epics AND stories (issue #135 — story-level sync).
2065
+ // Supports both whole-document "## EPIC-NN" and sharded "epics/epic-N.md" layouts.
2066
+ parsed.stories_found = 0;
2067
+ parsed.stories_upserted = 0;
2068
+ parsed.stories_preserved_status = 0;
2069
+ parsed.sprints_found = 0;
2070
+ parsed.sprints_upserted = 0;
2071
+
2072
+ if (parsed.epics_exists) {
2073
+ const epics = fs.readFileSync(epicsPath, 'utf8');
2074
+ parsed.epics_found = (epics.match(/^##\s+EPIC-\d+/gim) || epics.match(/^##\s+Epic\s+\d+/gim) || []).length;
2075
+ state.epics_count = parsed.epics_found;
2076
+
2077
+ // Parse epic blocks and their stories.
2078
+ // Epic heading examples: "## EPIC-01 — Setup" or "## Epic 1: User Auth"
2079
+ // Story heading examples: "### Story 01.03 — Schema" or "### Story 1.3: Foo"
2080
+ if (!state.epics) state.epics = [];
2081
+ const epicBlocks = epics.split(/^##\s+(?:EPIC-\d+|Epic\s+\d+)/im);
2082
+ const epicHeaders = epics.match(/^##\s+(?:EPIC-\d+|Epic\s+\d+)[^\n]*$/gim) || [];
2083
+ for (let i = 0; i < epicHeaders.length; i++) {
2084
+ const header = epicHeaders[i];
2085
+ const body = epicBlocks[i + 1] || '';
2086
+ const numMatch = header.match(/(\d+)/);
2087
+ if (!numMatch) continue;
2088
+ const epicNum = numMatch[1].padStart(2, '0');
2089
+ const nameMatch = header.match(/[—\-:]\s*(.+?)\s*$/);
2090
+ const epicName = nameMatch ? nameMatch[1].trim() : `Epic ${epicNum}`;
2091
+
2092
+ // Upsert epic with story-level preservation.
2093
+ let epicEntry = state.epics.find(e => String(e.number) === epicNum);
2094
+ if (!epicEntry) {
2095
+ epicEntry = { number: epicNum, name: epicName, status: 'planned', stories: [] };
2096
+ state.epics.push(epicEntry);
2097
+ } else {
2098
+ epicEntry.name = epicName;
2099
+ if (!epicEntry.stories) epicEntry.stories = [];
2100
+ }
2101
+
2102
+ // Parse stories inside this epic's body.
2103
+ const storyRe = /^###\s+Story\s+(\d+[\.-]\d+)[^\n]*?(?:[—\-:]\s*(.+?))?$/gim;
2104
+ let sm;
2105
+ while ((sm = storyRe.exec(body)) !== null) {
2106
+ const storyId = sm[1].replace('-', '.');
2107
+ const storyName = (sm[2] || '').trim() || `Story ${storyId}`;
2108
+ parsed.stories_found += 1;
2109
+ const existing = epicEntry.stories.find(s => String(s.id) === storyId);
2110
+ if (existing) {
2111
+ // Preserve status — state is authoritative for "completed" / "in_progress"
2112
+ existing.name = storyName;
2113
+ parsed.stories_preserved_status += 1;
2114
+ } else {
2115
+ epicEntry.stories.push({
2116
+ id: storyId,
2117
+ name: storyName,
2118
+ status: 'pending',
2119
+ });
2120
+ parsed.stories_upserted += 1;
2121
+ }
2122
+ }
2123
+ }
2124
+ }
2125
+
2126
+ // Walk .rihal/phases/*/sprint-*.md — parse sprints into state.sprints[] (issue #135).
2127
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
2128
+ const rihalPhasesDir = path.join(RIHAL_DIR, 'phases');
2129
+ const sprintRoot = fs.existsSync(phasesDir) ? phasesDir : (fs.existsSync(rihalPhasesDir) ? rihalPhasesDir : null);
2130
+ if (sprintRoot) {
2131
+ if (!state.sprints) state.sprints = [];
2132
+ for (const phaseEntry of fs.readdirSync(sprintRoot)) {
2133
+ const phaseDir = path.join(sprintRoot, phaseEntry);
2134
+ if (!fs.statSync(phaseDir).isDirectory()) continue;
2135
+ const phaseNumMatch = phaseEntry.match(/^(\d{1,3}(?:\.\d+)?)/);
2136
+ const phaseNum = phaseNumMatch ? phaseNumMatch[1] : phaseEntry;
2137
+ for (const file of fs.readdirSync(phaseDir)) {
2138
+ const sprintMatch = file.match(/^sprint-(\d+)\.md$/);
2139
+ if (!sprintMatch) continue;
2140
+ const sprintNum = sprintMatch[1];
2141
+ const sprintKey = `${phaseNum}/${sprintNum}`;
2142
+ parsed.sprints_found += 1;
2143
+ const sprintPath = path.join(phaseDir, file);
2144
+ const sprintText = fs.readFileSync(sprintPath, 'utf8');
2145
+ const goalMatch = sprintText.match(/(?:^goal:\s*(.+)$|\*\*Sprint Goal:\*\*\s*(.+))/im);
2146
+ const goal = goalMatch ? (goalMatch[1] || goalMatch[2] || '').trim() : '';
2147
+ const existing = state.sprints.find(s => s.key === sprintKey);
2148
+ if (existing) {
2149
+ existing.phase = phaseNum;
2150
+ existing.number = sprintNum;
2151
+ if (goal) existing.goal = goal;
2152
+ existing.file = path.relative(PROJECT_ROOT, sprintPath);
2153
+ } else {
2154
+ state.sprints.push({
2155
+ key: sprintKey,
2156
+ phase: phaseNum,
2157
+ number: sprintNum,
2158
+ goal,
2159
+ status: 'planned',
2160
+ file: path.relative(PROJECT_ROOT, sprintPath),
2161
+ });
2162
+ parsed.sprints_upserted += 1;
2163
+ }
2164
+ }
2165
+ }
2166
+ }
2167
+
2168
+ if (!parsed.roadmap_exists && !parsed.epics_exists && parsed.sprints_found === 0) {
2169
+ throw new Error(`state sync --from-disk: no ROADMAP.md, epics.md, or sprint files found`);
2170
+ }
2171
+
2172
+ writeState(state);
2173
+ return { ok: true, synced: true, ...parsed };
2174
+ }
2175
+
2176
+ throw new Error(`Unknown state subcommand: ${sub}.\nCommon: read, set-phase, advance-plan, add-decision, decisions-global, add-blocker, sync, promote-backlog\nRun 'rihal-tools.cjs help' for the full list of state subcommands.`);
2177
+ }
2178
+
2179
+ /**
2180
+ * Classify the scope of input based on keywords and length.
2181
+ * Returns one of: 'ticket', 'feature', 'phase', 'initiative'
2182
+ *
2183
+ * Priority order:
2184
+ * 1. Initiative keywords (highest)
2185
+ * 2. Phase keywords
2186
+ * 3. Feature keywords (add, implement, build)
2187
+ * 4. Ticket keywords
2188
+ * 5. Length-based fallback
2189
+ */
2190
+ function classifyScope(input) {
2191
+ const text = (input || '').toLowerCase();
2192
+ const len = text.length;
2193
+
2194
+ // Initiative signals — highest priority
2195
+ if (/\b(milestone|initiative|roadmap|multi-team|multi-sprint|q[1-4]\s*\d{4})\b/.test(text)) {
2196
+ return 'initiative';
2197
+ }
2198
+ if (len > 800) {
2199
+ return 'initiative';
2200
+ }
2201
+
2202
+ // Phase signals
2203
+ if (/\b(phase|epic|sprint)\b/.test(text)) {
2204
+ return 'phase';
2205
+ }
2206
+ if (len > 300 && len <= 800) {
2207
+ return 'phase';
2208
+ }
2209
+
2210
+ // Feature signals (add, implement, build)
2211
+ if (/\b(add|implement|build|create|develop|design)\b/.test(text)) {
2212
+ return 'feature';
2213
+ }
2214
+
2215
+ // Ticket signals
2216
+ if (/\b(fix|bug|typo|quick|small)\b/.test(text)) {
2217
+ return 'ticket';
2218
+ }
2219
+ if (/github\.com\/[^/]+\/[^/]+\/issues\/\d+/.test(text)) {
2220
+ return 'ticket';
2221
+ }
2222
+
2223
+ // Length-based fallback
2224
+ if (len < 100) {
2225
+ return 'ticket';
2226
+ }
2227
+
2228
+ // Default to feature
2229
+ return 'feature';
2230
+ }
2231
+
2232
+ /** init plan — context blob for /rihal:plan workflow. */
2233
+ function cmdInitPlan(rawArgs) {
2234
+ const config = readConfig();
2235
+ const tokens = (rawArgs || '').trim().split(/\s+/).filter(Boolean);
2236
+ const flags = { phase: null, output: null, research: false };
2237
+ const positional = [];
2238
+ for (let i = 0; i < tokens.length; i++) {
2239
+ const t = tokens[i];
2240
+ if (t === '--phase' && tokens[i + 1]) { flags.phase = tokens[++i]; }
2241
+ else if (t.startsWith('--phase=')) { flags.phase = t.slice('--phase='.length); }
2242
+ else if (t === '--output' && tokens[i + 1]) { flags.output = tokens[++i]; }
2243
+ else if (t.startsWith('--output=')) { flags.output = t.slice('--output='.length); }
2244
+ else if (t === '--research') { flags.research = true; }
2245
+ else positional.push(t);
2246
+ }
2247
+ const arg = positional.join(' ').trim();
2248
+ let inputType = 'description';
2249
+ let resolvedPath = null;
2250
+ let description = arg;
2251
+ if (arg) {
2252
+ if (!arg || arg.length === 0) {
2253
+ throw new Error('Plan argument cannot be empty');
2254
+ }
2255
+ if (arg.length > 5000) {
2256
+ throw new Error('Plan argument exceeds maximum length (5000 chars)');
2257
+ }
2258
+ const asAbs = path.isAbsolute(arg) ? arg : path.join(PROJECT_ROOT, arg);
2259
+ const normalized = path.resolve(asAbs);
2260
+ if (!normalized.startsWith(PROJECT_ROOT + path.sep) && normalized !== PROJECT_ROOT) {
2261
+ throw new Error(`Path outside project root: ${arg}`);
2262
+ }
2263
+ try {
2264
+ if (arg.endsWith('.md') && fs.existsSync(asAbs)) {
2265
+ resolvedPath = asAbs;
2266
+ // Check if this is already an executable plan (ends in -PLAN.md)
2267
+ if (/-PLAN\.md$/.test(resolvedPath)) {
2268
+ inputType = 'executable_plan';
2269
+ description = null;
2270
+ } else if (path.basename(asAbs).startsWith('council-')) {
2271
+ inputType = 'session';
2272
+ description = null;
2273
+ } else {
2274
+ inputType = 'file';
2275
+ description = null;
2276
+ }
2277
+ } else if (fs.existsSync(asAbs) && fs.statSync(asAbs).isDirectory()) {
2278
+ const sessions = walkFiles(asAbs).filter((f) => f.endsWith('.md')).sort().reverse();
2279
+ if (sessions.length > 0) { resolvedPath = sessions[0]; inputType = 'session'; description = null; }
2280
+ }
2281
+ } catch (e) {
2282
+ throw new Error(`Failed to resolve plan path: ${e.message}`);
2283
+ }
2284
+ }
2285
+
2286
+ if (!description && !resolvedPath) {
2287
+ console.error('rihal-tools warning: no description provided; plan will be named "unnamed". Re-run with a description.');
2288
+ }
2289
+
2290
+ const phaseSlug = flags.phase || (resolvedPath
2291
+ ? path.basename(resolvedPath, '.md').replace(/^council-\d{4}-\d{2}-\d{2}-/, '').slice(0, 40)
2292
+ : (arg || 'unnamed').toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40));
2293
+ const outputDir = flags.output || path.join(PLANNING_DIR, 'plans', phaseSlug);
2294
+ if (flags.output) {
2295
+ const absOutput = path.isAbsolute(flags.output) ? flags.output : path.join(PROJECT_ROOT, flags.output);
2296
+ if (!absOutput.startsWith(PROJECT_ROOT)) {
2297
+ throw new Error(`Output path outside project root: ${flags.output}`);
2298
+ }
2299
+ }
2300
+
2301
+ // Classify scope based on description or resolved path content
2302
+ let scopeInput = description || '';
2303
+ if (!scopeInput && resolvedPath) {
2304
+ try {
2305
+ const content = fs.readFileSync(resolvedPath, 'utf8');
2306
+ // Extract Follow-ups section if it's a council session
2307
+ const followUpsMatch = content.match(/## Follow-ups\s*\n([\s\S]*?)(?:##|$)/);
2308
+ if (followUpsMatch) {
2309
+ scopeInput = followUpsMatch[1].slice(0, 500); // Use first 500 chars of follow-ups
2310
+ } else {
2311
+ scopeInput = content.slice(0, 500); // Use first 500 chars of content
2312
+ }
2313
+ } catch (e) {
2314
+ // Ignore read errors, default to 'feature'
2315
+ }
2316
+ }
2317
+ const scope = classifyScope(scopeInput);
2318
+
2319
+ // If input is already an executable plan, redirect to execute workflow
2320
+ if (inputType === 'executable_plan') {
2321
+ return {
2322
+ workflow: 'plan',
2323
+ input_type: 'executable_plan',
2324
+ resolved_path: resolvedPath,
2325
+ suggestion: `This file is already an executable plan. Run: /rihal:execute ${path.relative(PROJECT_ROOT, resolvedPath)}`,
2326
+ config,
2327
+ paths: { project_root: PROJECT_ROOT, rihal: RIHAL_DIR, planning_root: PLANNING_DIR, state: path.join(RIHAL_DIR, 'state.json') },
2328
+ };
2329
+ }
2330
+
2331
+ return {
2332
+ workflow: 'plan', input_type: inputType, resolved_path: resolvedPath, description,
2333
+ phase_slug: phaseSlug, output_dir: outputDir, scope, flags, config,
2334
+ paths: { project_root: PROJECT_ROOT, rihal: RIHAL_DIR, planning_root: PLANNING_DIR, state: path.join(RIHAL_DIR, 'state.json') },
2335
+ };
2336
+ }
2337
+
2338
+ /** plan list — glob .planning/plans/ for plan files. */
2339
+ function cmdPlanList() {
2340
+ const plansDir = path.join(PLANNING_DIR, 'plans');
2341
+ if (!fs.existsSync(plansDir)) return { plans: [] };
2342
+ const files = walkFiles(plansDir).filter((f) => f.endsWith('.md'));
2343
+ return {
2344
+ plans: files.map((f) => {
2345
+ const text = fs.readFileSync(f, 'utf8');
2346
+ const { frontmatter, body } = parseFrontmatter(text);
2347
+ const objMatch = body.match(/^## Objective\s*\n(.+)/m);
2348
+ return {
2349
+ path: path.relative(PROJECT_ROOT, f), phase: frontmatter.phase || '',
2350
+ plan: frontmatter.plan || '', type: frontmatter.type || 'auto',
2351
+ depends_on: frontmatter.depends_on ? frontmatter.depends_on.split(',').map((s) => s.trim()) : [],
2352
+ objective: objMatch ? objMatch[1].trim() : '',
2353
+ };
2354
+ }),
2355
+ };
2356
+ }
2357
+
2358
+ /** init chain — context blob for /rihal:chain workflow. */
2359
+ function cmdInitChain(rawArgs) {
2360
+ const config = readConfig();
2361
+ const installedAgents = listInstalledAgents();
2362
+ const tokens = (rawArgs || '').trim().split(/\s+/).filter(Boolean);
2363
+
2364
+ const PRESETS = {
2365
+ 'research-plan': ['mariam', 'hussain-pm', 'planner'],
2366
+ 'feasibility': ['waleed', 'fatima'],
2367
+ 'gtm-to-build': ['mariam', 'hussain-pm', 'waleed'],
2368
+ 'full-discovery': ['mariam', 'sadiq', 'hussain-pm', 'waleed', 'planner'],
2369
+ };
2370
+
2371
+ let chain = [];
2372
+ let preset = null;
2373
+ let topicTokens = [];
2374
+
2375
+ if (tokens.length > 0) {
2376
+ const first = tokens[0];
2377
+ if (PRESETS[first]) {
2378
+ preset = first;
2379
+ chain = PRESETS[first];
2380
+ topicTokens = tokens.slice(1);
2381
+ } else if (first.includes(',')) {
2382
+ // Custom comma-separated agent list
2383
+ chain = first.split(',').map((s) => s.trim()).filter(Boolean);
2384
+ topicTokens = tokens.slice(1);
2385
+ } else {
2386
+ // Treat as topic with default research-plan preset
2387
+ preset = 'research-plan';
2388
+ chain = PRESETS['research-plan'];
2389
+ topicTokens = tokens;
2390
+ }
2391
+ }
2392
+
2393
+ const topic = topicTokens.join(' ').trim();
2394
+ const slug = (topic || preset || 'unnamed').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
2395
+ .split('-')
2396
+ .reduce((acc, word) => {
2397
+ const next = acc ? acc + '-' + word : word;
2398
+ return next.length <= 40 ? next : acc;
2399
+ }, '');
2400
+ const date = new Date().toISOString().slice(0, 10);
2401
+ const chainDir = path.join(PLANNING_DIR, 'chains', `${date}-${slug}`);
2402
+
2403
+ // Normalize: if user passed "mariam", check both "mariam" and "rihal-mariam"
2404
+ chain = chain.map(id => {
2405
+ if (installedAgents.includes(id)) return id;
2406
+ if (installedAgents.includes('rihal-' + id)) return 'rihal-' + id;
2407
+ // Try without prefix if user passed full
2408
+ if (id.startsWith('rihal-') && installedAgents.includes(id.slice(6))) return id.slice(6);
2409
+ return id; // will fail validation downstream with proper error
2410
+ });
2411
+
2412
+ // Validate agents are installed
2413
+ const unknownAgents = chain.filter((id) => !installedAgents.includes(id));
2414
+
2415
+ return {
2416
+ workflow: 'chain',
2417
+ preset,
2418
+ chain,
2419
+ topic,
2420
+ slug,
2421
+ chain_dir: chainDir,
2422
+ config,
2423
+ installed_agents: installedAgents,
2424
+ unknown_agents: unknownAgents,
2425
+ presets: PRESETS,
2426
+ paths: { project_root: PROJECT_ROOT, rihal: RIHAL_DIR, planning_root: PLANNING_DIR, sessions_dir: SESSIONS_DIR, state: path.join(RIHAL_DIR, 'state.json') },
2427
+ };
2428
+ }
2429
+
2430
+ /** init discuss — context blob for /rihal:discuss workflow. */
2431
+ function cmdInitDiscuss(rawArgs) {
2432
+ const config = readConfig();
2433
+ const installedAgents = listInstalledAgents();
2434
+ const tokens = (rawArgs || '').trim().split(/\s+/).filter(Boolean);
2435
+ let agentId = null;
2436
+ let question = rawArgs || '';
2437
+ if (tokens.length > 0 && installedAgents.includes(tokens[0])) {
2438
+ agentId = tokens[0];
2439
+ question = tokens.slice(1).join(' ');
2440
+ }
2441
+ const questionClassification = cmdClassifyQuestion(question);
2442
+ return {
2443
+ workflow: 'discuss', agent_id: agentId, question,
2444
+ question_type: questionClassification.type, question_signals: questionClassification.signals,
2445
+ config, installed_agents: installedAgents,
2446
+ paths: { project_root: PROJECT_ROOT, rihal: RIHAL_DIR, planning_root: PLANNING_DIR, sessions_dir: SESSIONS_DIR, state: path.join(RIHAL_DIR, 'state.json') },
2447
+ };
2448
+ }
2449
+
2450
+ /**
2451
+ * module <subcommand> — module system helpers.
2452
+ * list → available modules from package
2453
+ * installed → modules listed in .rihal/_config/manifest.yaml
2454
+ * check-requires → verify a module's dependencies are installed
2455
+ */
2456
+ function cmdModule(subArgs) {
2457
+ const sub = subArgs[0];
2458
+
2459
+ if (sub === 'list') {
2460
+ // Hardcoded available modules (known at build time)
2461
+ return {
2462
+ modules: [
2463
+ { name: 'core', description: 'Council agents, /rihal:council, /rihal:discuss, /rihal:status, /rihal:do router, /rihal:help, and state management' },
2464
+ { name: 'execution', description: 'Plan execution — /rihal:execute, /rihal:plan, /rihal:quick, /rihal:debug, /rihal:audit-fix, /rihal:undo' },
2465
+ { name: 'discovery', description: 'Project discovery — /rihal:new-project, /rihal:map-codebase, /rihal:scan, /rihal:explore, /rihal:code-review, /rihal:docs-update' },
2466
+ ]
2467
+ };
2468
+ }
2469
+
2470
+ if (sub === 'installed') {
2471
+ const manifestPath = path.join(CONFIG_DIR, 'manifest.yaml');
2472
+ if (!fs.existsSync(manifestPath)) return { installed: [] };
2473
+ const text = fs.readFileSync(manifestPath, 'utf8');
2474
+ const modules = [];
2475
+ let inModules = false;
2476
+ for (const line of text.split('\n')) {
2477
+ if (line.startsWith('modules:')) { inModules = true; continue; }
2478
+ if (inModules && line.trim().startsWith('-')) {
2479
+ modules.push(line.trim().slice(1).trim());
2480
+ } else if (inModules && !line.startsWith(' ')) {
2481
+ inModules = false;
2482
+ }
2483
+ }
2484
+ return { installed: modules };
2485
+ }
2486
+
2487
+ if (sub === 'check-requires') {
2488
+ const REQUIRES = { core: [], execution: ['core'], discovery: ['core'] };
2489
+ const modName = subArgs[1];
2490
+ if (!modName) return { ok: false, error: 'check-requires requires a module name argument (core|execution|discovery)' };
2491
+ if (!REQUIRES[modName]) return { ok: false, error: `Unknown module: ${modName}. Valid: core, execution, discovery` };
2492
+ const requires = REQUIRES[modName];
2493
+ if (requires.length === 0) return { ok: true, requires: [], missing: [] };
2494
+ const { installed } = cmdModule(['installed']);
2495
+ const missing = requires.filter((r) => !installed.includes(r));
2496
+ return { ok: missing.length === 0, requires, missing };
2497
+ }
2498
+
2499
+ throw new Error(`Unknown module subcommand: ${sub}. Valid: list, installed, check-requires`);
2500
+ }
2501
+
2502
+ function readPackageVersion() {
2503
+ try {
2504
+ const manifestPath = path.join(CONFIG_DIR, 'manifest.yaml');
2505
+ if (fs.existsSync(manifestPath)) {
2506
+ const parsed = parseSimpleYaml(fs.readFileSync(manifestPath, 'utf8'));
2507
+ if (parsed.version) return parsed.version;
2508
+ }
2509
+ } catch { /* fall through */ }
2510
+ return 'unknown';
2511
+ }
2512
+
2513
+ /**
2514
+ * resolve-model <agent-id> — return the model string for the given agent
2515
+ * under the current model profile in config.yaml.
2516
+ *
2517
+ * Model profiles (defined in references/model-profiles.md):
2518
+ * - quality: opus for reasoning agents, sonnet for executor, haiku for utilities
2519
+ * - balanced: sonnet for all agents
2520
+ * - budget: haiku for all agents
2521
+ * - inherit: no override, return null
2522
+ *
2523
+ * If the agent id is unknown, exit with error.
2524
+ */
2525
+ function cmdResolveModel(agentId) {
2526
+ if (!agentId || agentId.trim() === '') {
2527
+ throw new Error('resolve-model requires an agent-id argument');
2528
+ }
2529
+
2530
+ const config = readConfig();
2531
+ const profile = config.model_profile || 'balanced';
2532
+ const installedAgents = listInstalledAgents();
2533
+
2534
+ if (!installedAgents.includes(agentId)) {
2535
+ throw new Error(`Unknown agent: ${agentId}. Valid agents: ${installedAgents.join(', ')}`);
2536
+ }
2537
+
2538
+ // Model assignments per profile
2539
+ const QUALITY_AGENTS = {
2540
+ 'rihal-sadiq': 'claude-3-5-opus-20241022',
2541
+ 'rihal-waleed': 'claude-3-5-opus-20241022',
2542
+ 'rihal-planner': 'claude-3-5-opus-20241022',
2543
+ 'rihal-sprint-checker': 'claude-3-5-opus-20241022',
2544
+ 'rihal-fatima': 'claude-3-5-sonnet-20241022',
2545
+ 'rihal-executor': 'claude-3-5-sonnet-20241022',
2546
+ 'rihal-verifier': 'claude-3-5-sonnet-20241022',
2547
+ };
2548
+
2549
+ if (profile === 'inherit') {
2550
+ return { model: null, profile: 'inherit', note: 'No override; use parent session model' };
2551
+ }
2552
+
2553
+ if (profile === 'budget') {
2554
+ return { model: 'claude-3-5-haiku-20241022', profile: 'budget', agent: agentId };
2555
+ }
2556
+
2557
+ if (profile === 'balanced') {
2558
+ return { model: 'claude-3-5-sonnet-20241022', profile: 'balanced', agent: agentId };
2559
+ }
2560
+
2561
+ if (profile === 'quality') {
2562
+ const model = QUALITY_AGENTS[agentId] || 'claude-3-5-haiku-20241022';
2563
+ return { model, profile: 'quality', agent: agentId };
2564
+ }
2565
+
2566
+ // Unknown profile, default to balanced
2567
+ return { model: 'claude-3-5-sonnet-20241022', profile: 'balanced', agent: agentId, warning: `Unknown profile '${profile}'; using balanced` };
2568
+ }
2569
+
2570
+ /**
2571
+ * config set --key <k> --value <v> — update a key in .rihal/config.yaml
2572
+ * Writes YAML-style `key: value` (quotes strings with spaces).
2573
+ */
2574
+ function cmdConfigSet(subArgs) {
2575
+ const flags = {};
2576
+ for (let i = 0; i < subArgs.length; i++) {
2577
+ if (subArgs[i].startsWith('--')) {
2578
+ const key = subArgs[i].slice(2);
2579
+ flags[key] = subArgs[i + 1] || '';
2580
+ i++;
2581
+ }
2582
+ }
2583
+
2584
+ const key = flags.key || '';
2585
+ const value = flags.value || '';
2586
+
2587
+ if (!key) throw new Error('config set requires --key <key> --value <value>\n e.g. config set --key language --value Arabic');
2588
+ if (!value) throw new Error('config set requires --key <key> --value <value>\n e.g. config set --key language --value Arabic');
2589
+
2590
+ const configPath = path.join(RIHAL_DIR, 'config.yaml');
2591
+ fs.mkdirSync(RIHAL_DIR, { recursive: true });
2592
+
2593
+ let content = '';
2594
+ if (fs.existsSync(configPath)) {
2595
+ content = fs.readFileSync(configPath, 'utf8');
2596
+ }
2597
+
2598
+ // Parse current config
2599
+ const config = parseSimpleYaml(content);
2600
+
2601
+ // Update the key
2602
+ config[key] = value;
2603
+
2604
+ // Serialize back to YAML
2605
+ const lines = [];
2606
+ for (const [k, v] of Object.entries(config)) {
2607
+ const needsQuotes = /\s/.test(v);
2608
+ const yamlValue = needsQuotes ? `"${v.replace(/"/g, '\\"')}"` : v;
2609
+ lines.push(`${k}: ${yamlValue}`);
2610
+ }
2611
+
2612
+ const newContent = lines.join('\n') + '\n';
2613
+ const tmp = configPath + '.tmp';
2614
+ fs.writeFileSync(tmp, newContent, 'utf8');
2615
+ fs.renameSync(tmp, configPath);
2616
+
2617
+ return { ok: true, key, value, path: configPath };
2618
+ }
2619
+
2620
+ /**
2621
+ * notify send — post a message to configured webhook URLs.
2622
+ *
2623
+ * Config keys read from .rihal/config.yaml (top-level, flat):
2624
+ * slack_webhook_url — Slack incoming webhook
2625
+ * discord_webhook_url — Discord webhook
2626
+ * teams_webhook_url — Microsoft Teams incoming webhook (MessageCard format)
2627
+ *
2628
+ * Flags:
2629
+ * --title <t> required headline
2630
+ * --body <b> optional detail text
2631
+ * --event <e> optional short event tag (e.g. "execute-done", "council-done")
2632
+ * --only slack|discord|teams restrict to one platform (for /rihal:notify-test)
2633
+ *
2634
+ * Returns: { sent: [...], skipped: [...], failed: [...] }
2635
+ * Never throws on webhook failure — this runs at the tail of workflows and
2636
+ * must not break the primary pipeline.
2637
+ */
2638
+ async function cmdNotify(subArgs) {
2639
+ const sub = subArgs[0];
2640
+ if (sub !== 'send') {
2641
+ throw new Error("Unknown notify subcommand. Valid: send");
2642
+ }
2643
+ const flags = {};
2644
+ for (let i = 1; i < subArgs.length; i++) {
2645
+ if (subArgs[i].startsWith('--')) {
2646
+ flags[subArgs[i].slice(2)] = subArgs[i + 1] || '';
2647
+ i++;
2648
+ }
2649
+ }
2650
+ const title = flags.title || '';
2651
+ const body = flags.body || '';
2652
+ const event = flags.event || 'rihal';
2653
+ const only = flags.only || '';
2654
+ if (!title) throw new Error('notify send requires --title <text>');
2655
+
2656
+ // Read config
2657
+ const configPath = path.join(RIHAL_DIR, 'config.yaml');
2658
+ const config = fs.existsSync(configPath)
2659
+ ? parseSimpleYaml(fs.readFileSync(configPath, 'utf8'))
2660
+ : {};
2661
+
2662
+ const targets = [
2663
+ { name: 'slack', url: config.slack_webhook_url, shape: buildSlackPayload },
2664
+ { name: 'discord', url: config.discord_webhook_url, shape: buildDiscordPayload },
2665
+ { name: 'teams', url: config.teams_webhook_url, shape: buildTeamsPayload },
2666
+ ];
2667
+
2668
+ const sent = [], skipped = [], failed = [];
2669
+ for (const t of targets) {
2670
+ if (only && t.name !== only) continue;
2671
+ if (!t.url) { skipped.push({ platform: t.name, reason: 'no webhook configured' }); continue; }
2672
+ try {
2673
+ const payload = t.shape({ title, body, event, project: path.basename(PROJECT_ROOT) });
2674
+ const res = await fetch(t.url, {
2675
+ method: 'POST',
2676
+ headers: { 'Content-Type': 'application/json' },
2677
+ body: JSON.stringify(payload),
2678
+ });
2679
+ if (!res.ok) {
2680
+ failed.push({ platform: t.name, status: res.status, text: (await res.text()).slice(0, 200) });
2681
+ } else {
2682
+ sent.push({ platform: t.name });
2683
+ }
2684
+ } catch (e) {
2685
+ failed.push({ platform: t.name, error: String(e.message || e) });
2686
+ }
2687
+ }
2688
+ return { sent, skipped, failed };
2689
+ }
2690
+
2691
+ function buildSlackPayload({ title, body, event, project }) {
2692
+ const lines = [`*${title}*`];
2693
+ if (body) lines.push(body);
2694
+ lines.push(`_project: ${project} · event: ${event}_`);
2695
+ return { text: lines.join('\n') };
2696
+ }
2697
+
2698
+ function buildDiscordPayload({ title, body, event, project }) {
2699
+ return {
2700
+ embeds: [{
2701
+ title: title.slice(0, 256),
2702
+ description: (body || '').slice(0, 4000),
2703
+ footer: { text: `project: ${project} · event: ${event}` },
2704
+ }],
2705
+ };
2706
+ }
2707
+
2708
+ // Teams legacy MessageCard — still accepted by Incoming Webhook connectors.
2709
+ function buildTeamsPayload({ title, body, event, project }) {
2710
+ return {
2711
+ '@type': 'MessageCard',
2712
+ '@context': 'https://schema.org/extensions',
2713
+ themeColor: '0076D7',
2714
+ summary: title.slice(0, 256),
2715
+ title,
2716
+ text: body || '',
2717
+ sections: [{
2718
+ facts: [
2719
+ { name: 'Project', value: project },
2720
+ { name: 'Event', value: event },
2721
+ ],
2722
+ }],
2723
+ };
2724
+ }
2725
+
2726
+ /**
2727
+ * notes list — glob .rihal/notes/*.md and ~/.rihal-notes/*.md,
2728
+ * parse frontmatter, return sorted array of {path, date, slug, summary}
2729
+ * (10 most recent).
2730
+ */
2731
+ function cmdNotesList() {
2732
+ const noteDirs = [
2733
+ path.join(RIHAL_DIR, 'notes'),
2734
+ path.join(process.env.HOME || '', '.rihal-notes'),
2735
+ ];
2736
+
2737
+ const notes = [];
2738
+ for (const dir of noteDirs) {
2739
+ if (!fs.existsSync(dir)) continue;
2740
+ try {
2741
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
2742
+ for (const file of files) {
2743
+ const filePath = path.join(dir, file);
2744
+ const content = fs.readFileSync(filePath, 'utf8');
2745
+ const { frontmatter, body } = parseFrontmatter(content);
2746
+ const summary = body.trim().split('\n')[0].slice(0, 50);
2747
+ notes.push({
2748
+ path: filePath,
2749
+ date: frontmatter.date || file.slice(0, 10),
2750
+ slug: frontmatter.slug || file.replace(/^[\d-]+/, '').replace(/\.md$/, ''),
2751
+ summary,
2752
+ });
2753
+ }
2754
+ } catch (err) {
2755
+ // Silently skip if directory cannot be read
2756
+ }
2757
+ }
2758
+
2759
+ // Sort by date descending, take 10 most recent
2760
+ notes.sort((a, b) => b.date.localeCompare(a.date));
2761
+ return notes.slice(0, 10);
2762
+ }
2763
+
2764
+ /**
2765
+ * notes count — return count of unpromoted notes in both .rihal/notes
2766
+ * and ~/.rihal-notes.
2767
+ */
2768
+ function cmdNotesCount() {
2769
+ const noteDirs = [
2770
+ path.join(RIHAL_DIR, 'notes'),
2771
+ path.join(process.env.HOME || '', '.rihal-notes'),
2772
+ ];
2773
+
2774
+ let count = 0;
2775
+ for (const dir of noteDirs) {
2776
+ if (!fs.existsSync(dir)) continue;
2777
+ try {
2778
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
2779
+ for (const file of files) {
2780
+ const filePath = path.join(dir, file);
2781
+ const content = fs.readFileSync(filePath, 'utf8');
2782
+ const { frontmatter } = parseFrontmatter(content);
2783
+ if (frontmatter.promoted !== 'true') count++;
2784
+ }
2785
+ } catch (err) {
2786
+ // Silently skip if directory cannot be read
2787
+ }
2788
+ }
2789
+
2790
+ return { count };
2791
+ }
2792
+
2793
+ /**
2794
+ * cmdBrain — pull Rihal brain content from configured sources.
2795
+ *
2796
+ * Subcommands:
2797
+ * brain pull Fetch all configured sources into rihal/brain/
2798
+ * brain pull <name> Fetch a single named source
2799
+ * brain status Report cache freshness and placeholder status
2800
+ * brain list Print configured sources
2801
+ *
2802
+ * Uses git sparse-checkout so we pull only the paths listed per source.
2803
+ * Placeholder URLs (containing `<PLACEHOLDER`) are skipped with a clear
2804
+ * message — useful in v2.0 before M5 lands real Rihal repo URLs.
2805
+ */
2806
+ function cmdBrain(args) {
2807
+ const sub = args[0] || 'help';
2808
+ const sourcesPath = path.join(PROJECT_ROOT, 'rihal', 'brain', 'sources.yaml');
2809
+ const brainDir = path.join(PROJECT_ROOT, 'rihal', 'brain');
2810
+
2811
+ if (!fs.existsSync(sourcesPath)) {
2812
+ return {
2813
+ ok: false,
2814
+ error: `sources.yaml missing at ${sourcesPath}. Run install or see issue #158.`,
2815
+ };
2816
+ }
2817
+
2818
+ // Minimal YAML reader specifically for sources.yaml — not a general parser.
2819
+ // Handles: `version: 1`, `defaults:` block, `sources:` list where each
2820
+ // entry is a `- name: X` block with sibling key: value lines and an
2821
+ // `paths:` sub-list of strings.
2822
+ function parseSourcesYaml(text) {
2823
+ const root = { version: null, defaults: {}, sources: [] };
2824
+ const lines = text.split('\n');
2825
+ let section = null;
2826
+ let current = null; // current source map
2827
+ let inPaths = false;
2828
+ let inDescription = false;
2829
+ let descLines = [];
2830
+
2831
+ function unquote(s) { return s.replace(/^['"]|['"]$/g, ''); }
2832
+
2833
+ for (const raw of lines) {
2834
+ if (!raw.trim() || raw.trim().startsWith('#')) continue;
2835
+
2836
+ // Flush description if we were collecting
2837
+ if (inDescription && raw.match(/^ {4}\S/) && !raw.trim().startsWith('-')) {
2838
+ // still inside the description block
2839
+ const m = raw.match(/^ *(.*)$/);
2840
+ if (m) descLines.push(m[1]);
2841
+ continue;
2842
+ } else if (inDescription) {
2843
+ current.description = descLines.join(' ').trim();
2844
+ inDescription = false;
2845
+ descLines = [];
2846
+ }
2847
+
2848
+ // Top-level keys
2849
+ const top = raw.match(/^(\w+):\s*(.*)$/);
2850
+ if (top) {
2851
+ const key = top[1], val = top[2].trim();
2852
+ if (key === 'version') { root.version = unquote(val); section = null; continue; }
2853
+ if (key === 'defaults') { section = 'defaults'; continue; }
2854
+ if (key === 'sources') { section = 'sources'; continue; }
2855
+ }
2856
+
2857
+ // defaults: indented key-value
2858
+ if (section === 'defaults') {
2859
+ const m = raw.match(/^ +([\w_]+):\s*(.*)$/);
2860
+ if (m) root.defaults[m[1]] = unquote(m[2]);
2861
+ continue;
2862
+ }
2863
+
2864
+ // sources: list items
2865
+ if (section === 'sources') {
2866
+ const startItem = raw.match(/^ *- ([\w_-]+):\s*(.*)$/);
2867
+ if (startItem) {
2868
+ current = {};
2869
+ current[startItem[1]] = unquote(startItem[2]);
2870
+ root.sources.push(current);
2871
+ inPaths = false;
2872
+ continue;
2873
+ }
2874
+ // paths: list-of-strings under current
2875
+ const pathsStart = raw.match(/^ +paths:\s*$/);
2876
+ if (pathsStart) { current.paths = []; inPaths = true; continue; }
2877
+ if (inPaths) {
2878
+ const pItem = raw.match(/^ *- (.*)$/);
2879
+ if (pItem) { current.paths.push(unquote(pItem[1])); continue; }
2880
+ inPaths = false;
2881
+ }
2882
+ // description: block scalar `>`
2883
+ const descStart = raw.match(/^ +description:\s*>\s*$/);
2884
+ if (descStart) { inDescription = true; descLines = []; continue; }
2885
+ // Regular key: value on current item
2886
+ const kv = raw.match(/^ +([\w_-]+):\s*(.*)$/);
2887
+ if (kv && current) {
2888
+ current[kv[1]] = unquote(kv[2]);
2889
+ }
2890
+ }
2891
+ }
2892
+ // final flush
2893
+ if (inDescription && current) current.description = descLines.join(' ').trim();
2894
+ return root;
2895
+ }
2896
+
2897
+ const cfg = parseSourcesYaml(fs.readFileSync(sourcesPath, 'utf8'));
2898
+ const sources = Array.isArray(cfg.sources) ? cfg.sources : [];
2899
+
2900
+ if (sub === 'list') {
2901
+ return {
2902
+ ok: true,
2903
+ version: cfg.version,
2904
+ sources: sources.map(s => ({
2905
+ name: s.name,
2906
+ repo: s.repo,
2907
+ dest: s.dest,
2908
+ placeholder: String(s.repo || '').includes('<PLACEHOLDER'),
2909
+ })),
2910
+ };
2911
+ }
2912
+
2913
+ if (sub === 'status') {
2914
+ const report = { ok: true, sources: [] };
2915
+ for (const s of sources) {
2916
+ const destPath = path.join(PROJECT_ROOT, s.dest || '');
2917
+ const exists = fs.existsSync(destPath);
2918
+ report.sources.push({
2919
+ name: s.name,
2920
+ dest: s.dest,
2921
+ fetched: exists,
2922
+ placeholder: String(s.repo || '').includes('<PLACEHOLDER'),
2923
+ });
2924
+ }
2925
+ return report;
2926
+ }
2927
+
2928
+ if (sub !== 'pull') {
2929
+ return {
2930
+ ok: false,
2931
+ error: `Unknown brain subcommand: ${sub}. Try: pull | status | list`,
2932
+ };
2933
+ }
2934
+
2935
+ // sub === 'pull'
2936
+ const onlyName = args[1];
2937
+ const report = { ok: true, pulled: [], skipped: [], errors: [] };
2938
+
2939
+ for (const s of sources) {
2940
+ if (onlyName && s.name !== onlyName) continue;
2941
+ const repo = String(s.repo || '');
2942
+
2943
+ if (repo.includes('<PLACEHOLDER')) {
2944
+ report.skipped.push({ name: s.name, reason: 'placeholder URL — fill in via issue #162 (M5)' });
2945
+ continue;
2946
+ }
2947
+
2948
+ if (repo === 'self') {
2949
+ // In-repo copy — use rsync-ish node copy from paths under project root.
2950
+ const destPath = path.join(PROJECT_ROOT, s.dest);
2951
+ fs.mkdirSync(destPath, { recursive: true });
2952
+ const paths = Array.isArray(s.paths) ? s.paths : [];
2953
+ let copied = 0;
2954
+ for (const pattern of paths) {
2955
+ // Very simple glob: expand ** to recursive copy.
2956
+ const base = pattern.split('**')[0].replace(/\/$/, '');
2957
+ const srcDir = path.join(PROJECT_ROOT, base);
2958
+ if (!fs.existsSync(srcDir)) continue;
2959
+ // Recursive copy of .md files
2960
+ function walk(dir) {
2961
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
2962
+ const full = path.join(dir, e.name);
2963
+ if (e.isDirectory()) { walk(full); continue; }
2964
+ if (!e.isFile()) continue;
2965
+ if (!full.endsWith('.md')) continue;
2966
+ const rel = path.relative(srcDir, full);
2967
+ const out = path.join(destPath, rel);
2968
+ fs.mkdirSync(path.dirname(out), { recursive: true });
2969
+ fs.copyFileSync(full, out);
2970
+ copied++;
2971
+ }
2972
+ }
2973
+ walk(srcDir);
2974
+ }
2975
+ report.pulled.push({ name: s.name, kind: 'self', files: copied });
2976
+ continue;
2977
+ }
2978
+
2979
+ // External git source — use sparse checkout into a tmp dir then copy.
2980
+ const { execSync } = require('child_process');
2981
+ const os = require('os');
2982
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rihal-brain-'));
2983
+ const branch = s.branch || cfg.defaults?.branch || 'main';
2984
+ try {
2985
+ execSync(
2986
+ `git clone --depth=1 --filter=blob:none --sparse --branch="${branch}" "${repo}" "${tmp}"`,
2987
+ { stdio: 'pipe' }
2988
+ );
2989
+ const paths = Array.isArray(s.paths) ? s.paths : [];
2990
+ execSync(`git -C "${tmp}" sparse-checkout set ${paths.map(p => `"${p}"`).join(' ')}`, { stdio: 'pipe' });
2991
+
2992
+ const destPath = path.join(PROJECT_ROOT, s.dest);
2993
+ fs.mkdirSync(destPath, { recursive: true });
2994
+ // Copy everything the sparse checkout materialized.
2995
+ function copyTree(src, dst) {
2996
+ for (const e of fs.readdirSync(src, { withFileTypes: true })) {
2997
+ if (e.name === '.git') continue;
2998
+ const sp = path.join(src, e.name);
2999
+ const dp = path.join(dst, e.name);
3000
+ if (e.isDirectory()) { fs.mkdirSync(dp, { recursive: true }); copyTree(sp, dp); }
3001
+ else if (e.isFile()) fs.copyFileSync(sp, dp);
3002
+ }
3003
+ }
3004
+ copyTree(tmp, destPath);
3005
+ report.pulled.push({ name: s.name, kind: 'git', repo, branch });
3006
+ } catch (e) {
3007
+ report.errors.push({ name: s.name, error: String(e.message || e).slice(0, 200) });
3008
+ } finally {
3009
+ try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
3010
+ }
3011
+ }
3012
+
3013
+ if (report.errors.length) report.ok = false;
3014
+ return report;
3015
+ }
3016
+
3017
+ /**
3018
+ * cmdProgress — single pre-computed progress blob (GSD-parity, issue #159).
3019
+ *
3020
+ * Subcommands:
3021
+ * progress init Full snapshot — everything /rihal:progress needs.
3022
+ * progress bar --raw ASCII bar only (e.g. "[████░░░░] 50%").
3023
+ * progress insights insights[] array (drift warnings, between-milestone detection).
3024
+ * progress routes intent-tree routes[] for Next Up menu.
3025
+ *
3026
+ * Pushing logic into the CLI lets the workflow file shrink to pure
3027
+ * rendering — no ROADMAP.md parsing, no SUMMARY.md walking, no grep.
3028
+ */
3029
+ function cmdProgress(args) {
3030
+ const sub = args[0] || 'init';
3031
+ const rawMode = args.includes('--raw');
3032
+
3033
+ // Resolve paths — workflow files may run this from any subdirectory.
3034
+ const statePath = path.join(RIHAL_DIR, 'state.json');
3035
+ const roadmapPath = path.join(PLANNING_DIR, 'ROADMAP.md');
3036
+ const phasesDir = path.join(PLANNING_DIR, 'phases');
3037
+
3038
+ function readState() {
3039
+ if (!fs.existsSync(statePath)) return null;
3040
+ try { return JSON.parse(fs.readFileSync(statePath, 'utf8')); }
3041
+ catch { return null; }
3042
+ }
3043
+
3044
+ function parseRoadmapPhases() {
3045
+ if (!fs.existsSync(roadmapPath)) return [];
3046
+ const text = fs.readFileSync(roadmapPath, 'utf8');
3047
+ const phases = [];
3048
+ const rowRe = /^\|\s*(\d{1,3}(?:\.\d+)?)\s*\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|/gm;
3049
+ let m;
3050
+ while ((m = rowRe.exec(text)) !== null) {
3051
+ const num = m[1].trim();
3052
+ const name = m[2].trim();
3053
+ const goal = m[3].trim();
3054
+ if (!/^\d/.test(num)) continue;
3055
+ if (name.toLowerCase() === 'phase') continue;
3056
+ phases.push({ number: num, name, goal });
3057
+ }
3058
+ return phases;
3059
+ }
3060
+
3061
+ function extractMilestoneName() {
3062
+ if (!fs.existsSync(roadmapPath)) return null;
3063
+ const text = fs.readFileSync(roadmapPath, 'utf8');
3064
+ const m = text.match(/^##\s+Milestone\s+(M\d+\S*)\s*[—\-:]?\s*(.*)$/m);
3065
+ return m ? `${m[1]} ${m[2]}`.trim() : null;
3066
+ }
3067
+
3068
+ function walkPhaseDirs() {
3069
+ if (!fs.existsSync(phasesDir)) return {};
3070
+ const byNum = {};
3071
+ for (const entry of fs.readdirSync(phasesDir)) {
3072
+ const full = path.join(phasesDir, entry);
3073
+ if (!fs.statSync(full).isDirectory()) continue;
3074
+ const numMatch = entry.match(/^(\d{1,3}(?:\.\d+)?)/);
3075
+ if (!numMatch) continue;
3076
+ const num = numMatch[1];
3077
+ const files = fs.readdirSync(full);
3078
+ byNum[num] = {
3079
+ path: full,
3080
+ dirName: entry,
3081
+ plan_count: files.filter(f => /PLAN\.md$|-PLAN\.md$|SPRINT\.md$/.test(f)).length,
3082
+ summary_count: files.filter(f => /SUMMARY\.md$|-SUMMARY\.md$/.test(f)).length,
3083
+ has_research: files.includes('RESEARCH.md'),
3084
+ has_context: files.includes('CONTEXT.md'),
3085
+ has_verification: files.includes('VERIFICATION.md'),
3086
+ };
3087
+ }
3088
+ return byNum;
3089
+ }
3090
+
3091
+ function detectInsights(state, roadmapPhases, diskByNum) {
3092
+ const insights = [];
3093
+ const statePhases = (state && (state.state?.phases || state.phases)) || [];
3094
+
3095
+ // Drift: ROADMAP phase count vs state.json phase count
3096
+ if (roadmapPhases.length > 0 && statePhases.length !== roadmapPhases.length) {
3097
+ insights.push({
3098
+ kind: 'drift',
3099
+ severity: 'warn',
3100
+ message: `ROADMAP.md has ${roadmapPhases.length} phases, state.json has ${statePhases.length}. Run: node .rihal/bin/rihal-tools.cjs state sync --from-disk`,
3101
+ });
3102
+ }
3103
+
3104
+ // Undercount: phases that exist on disk but not in state
3105
+ const statePhaseNums = new Set(statePhases.map(p => String(p.number)));
3106
+ const diskPhaseNums = Object.keys(diskByNum);
3107
+ const missingFromState = diskPhaseNums.filter(n => !statePhaseNums.has(n));
3108
+ if (missingFromState.length > 0) {
3109
+ insights.push({
3110
+ kind: 'undercount',
3111
+ severity: 'warn',
3112
+ message: `${missingFromState.length} phase dir(s) on disk not registered in state.json: ${missingFromState.slice(0, 5).join(', ')}`,
3113
+ });
3114
+ }
3115
+
3116
+ // Between-milestones heuristic: no current_phase + previous milestone's last phase is complete
3117
+ if (state && state.current_phase === null && statePhases.length > 0) {
3118
+ const allComplete = statePhases.every(p => p.status === 'complete' || p.completed);
3119
+ if (allComplete) {
3120
+ insights.push({
3121
+ kind: 'between-milestones',
3122
+ severity: 'info',
3123
+ message: 'All registered phases complete — effectively between milestones. Consider /rihal:audit-milestone or /rihal:new-milestone.',
3124
+ });
3125
+ }
3126
+ }
3127
+
3128
+ return insights;
3129
+ }
3130
+
3131
+ function deriveRoutes(state, roadmapPhases, diskByNum) {
3132
+ const routes = [];
3133
+ const statePhases = (state && (state.state?.phases || state.phases)) || [];
3134
+
3135
+ // Route A — phases with pending plans (ready to execute)
3136
+ const pendingExec = statePhases.filter(p => {
3137
+ const disk = diskByNum[String(p.number)];
3138
+ return disk && disk.plan_count > disk.summary_count;
3139
+ }).slice(0, 3);
3140
+ for (const p of pendingExec) {
3141
+ routes.push({
3142
+ letter: 'A',
3143
+ label: `Execute phase ${p.number} — unfinished plans`,
3144
+ command: `/rihal:execute-phase ${p.number}`,
3145
+ });
3146
+ }
3147
+
3148
+ // Route B — phases with research but no plans
3149
+ const researchOnly = Object.entries(diskByNum)
3150
+ .filter(([num, d]) => d.has_research && d.plan_count === 0)
3151
+ .slice(0, 3);
3152
+ for (const [num, d] of researchOnly) {
3153
+ routes.push({
3154
+ letter: 'B',
3155
+ label: `Plan phase ${num} — researched, awaiting plan`,
3156
+ command: `/rihal:plan-phase ${num}`,
3157
+ });
3158
+ }
3159
+
3160
+ // Route C — close out milestone if everything seems done
3161
+ const allDone = statePhases.length > 0 && statePhases.every(p => p.status === 'complete' || p.completed);
3162
+ if (allDone) {
3163
+ routes.push({ letter: 'C', label: 'Audit current milestone', command: '/rihal:audit-milestone' });
3164
+ routes.push({ letter: 'C', label: 'Complete current milestone', command: '/rihal:complete-milestone' });
3165
+ }
3166
+
3167
+ // Fallback — nothing obvious: offer status
3168
+ if (routes.length === 0) {
3169
+ routes.push({ letter: 'A', label: 'Check progress detail', command: '/rihal:progress' });
3170
+ routes.push({ letter: 'B', label: 'Start a council on what to do next', command: '/rihal:council' });
3171
+ }
3172
+
3173
+ return routes;
3174
+ }
3175
+
3176
+ function buildBar(completed, total) {
3177
+ if (!total) return '[░░░░░░░░░░░░░░░░░░░░] 0/0 (0%)';
3178
+ const pct = Math.round((completed / total) * 100);
3179
+ const width = 20;
3180
+ const filled = Math.min(width, Math.round((completed / total) * width));
3181
+ const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
3182
+ return `[${bar}] ${completed}/${total} (${pct}%)`;
3183
+ }
3184
+
3185
+ // Build the core snapshot once — all subcommands derive from it.
3186
+ const state = readState();
3187
+ const roadmapPhases = parseRoadmapPhases();
3188
+ const diskByNum = walkPhaseDirs();
3189
+ const statePhases = (state && (state.state?.phases || state.phases)) || [];
3190
+ const completedCount = statePhases.filter(p => p.status === 'complete' || p.completed).length;
3191
+ const phaseCount = Math.max(statePhases.length, roadmapPhases.length);
3192
+
3193
+ if (sub === 'bar') {
3194
+ const bar = buildBar(completedCount, phaseCount);
3195
+ if (rawMode) { console.log(bar); process.exit(0); }
3196
+ return { ok: true, bar, completed: completedCount, total: phaseCount };
3197
+ }
3198
+
3199
+ if (sub === 'insights') {
3200
+ return { ok: true, insights: detectInsights(state, roadmapPhases, diskByNum) };
3201
+ }
3202
+
3203
+ if (sub === 'routes') {
3204
+ return { ok: true, routes: deriveRoutes(state, roadmapPhases, diskByNum) };
3205
+ }
3206
+
3207
+ // sub === 'init' (default) — full snapshot
3208
+ const currentPhase = state && state.current_phase;
3209
+ const insights = detectInsights(state, roadmapPhases, diskByNum);
3210
+ const routes = deriveRoutes(state, roadmapPhases, diskByNum);
3211
+
3212
+ return {
3213
+ ok: true,
3214
+ project: state && state.project,
3215
+ milestone: extractMilestoneName(),
3216
+ current_phase: currentPhase,
3217
+ phase_count: phaseCount,
3218
+ completed_count: completedCount,
3219
+ bar: buildBar(completedCount, phaseCount),
3220
+ phases: roadmapPhases.map(p => ({
3221
+ ...p,
3222
+ disk: diskByNum[p.number] || null,
3223
+ in_state: statePhases.some(sp => String(sp.number) === p.number),
3224
+ })),
3225
+ decisions: state ? (state.decisions || []).slice(-3) : [],
3226
+ blockers: state ? (state.blockers || []).filter(b => !b.resolved).slice(0, 5) : [],
3227
+ insights,
3228
+ routes,
3229
+ updated: state && state.updated,
3230
+ };
3231
+ }
3232
+
3233
+ /**
3234
+ * cmdSummaryExtract — surgically pull named fields from a SUMMARY.md.
3235
+ * Avoids whole-file loads when the caller only wants one or two headings.
3236
+ * Usage: summary-extract <path> --fields one_liner,status
3237
+ */
3238
+ function cmdSummaryExtract(args) {
3239
+ const filePath = args[0];
3240
+ const fieldsFlag = args.indexOf('--fields');
3241
+ const fields = fieldsFlag >= 0 ? (args[fieldsFlag + 1] || '').split(',').map(s => s.trim()).filter(Boolean) : ['one_liner'];
3242
+
3243
+ if (!filePath) return { ok: false, error: 'Usage: summary-extract <path> [--fields a,b,c]' };
3244
+ if (!fs.existsSync(filePath)) return { ok: false, error: `file not found: ${filePath}` };
3245
+
3246
+ const text = fs.readFileSync(filePath, 'utf8');
3247
+ const out = { ok: true, path: filePath };
3248
+
3249
+ const fieldToPatterns = {
3250
+ one_liner: [/^##\s+One[-\s]?liner\s*\n([\s\S]*?)(?=\n##|\n---|$)/im, /^##\s+Summary\s*\n([\s\S]*?)(?=\n##|\n---|$)/im],
3251
+ status: [/^##\s+Status\s*\n([\s\S]*?)(?=\n##|\n---|$)/im, /^status:\s*(.+)$/im],
3252
+ outcomes: [/^##\s+Outcomes?\s*\n([\s\S]*?)(?=\n##|\n---|$)/im],
3253
+ decisions: [/^##\s+Decisions?\s*\n([\s\S]*?)(?=\n##|\n---|$)/im],
3254
+ blockers: [/^##\s+Blockers?\s*\n([\s\S]*?)(?=\n##|\n---|$)/im],
3255
+ followups: [/^##\s+Follow[-\s]?ups?\s*\n([\s\S]*?)(?=\n##|\n---|$)/im, /^##\s+Next[-\s]?steps?\s*\n([\s\S]*?)(?=\n##|\n---|$)/im],
3256
+ };
3257
+
3258
+ for (const f of fields) {
3259
+ const patterns = fieldToPatterns[f] || [new RegExp(`^##\\s+${f.replace(/_/g, '[ _-]?')}\\s*\\n([\\s\\S]*?)(?=\\n##|\\n---|$)`, 'im')];
3260
+ let value = null;
3261
+ for (const re of patterns) {
3262
+ const m = text.match(re);
3263
+ if (m && m[1]) { value = m[1].trim().split('\n').map(l => l.trim()).filter(Boolean).join('\n'); break; }
3264
+ }
3265
+ // Fallback for one_liner: first non-empty paragraph after H1
3266
+ if (f === 'one_liner' && !value) {
3267
+ const afterH1 = text.replace(/^#[^\n]*\n/, '');
3268
+ const firstPara = afterH1.match(/^[^\n#][^\n]*(?:\n(?!\n)[^\n#][^\n]*)*/m);
3269
+ if (firstPara) value = firstPara[0].trim();
3270
+ }
3271
+ out[f] = value;
3272
+ }
3273
+
3274
+ return out;
3275
+ }
3276
+
3277
+ /**
3278
+ * cmdStateSnapshot — compact, display-friendly state extract.
3279
+ * Hides internal machinery (lock metadata, full history) from callers
3280
+ * that only need a render-ready summary.
3281
+ */
3282
+ function cmdStateSnapshot() {
3283
+ const statePath = path.join(RIHAL_DIR, 'state.json');
3284
+ if (!fs.existsSync(statePath)) return { ok: true, state: null };
3285
+ let state;
3286
+ try { state = JSON.parse(fs.readFileSync(statePath, 'utf8')); }
3287
+ catch (e) { return { ok: false, error: `invalid state.json: ${e.message}` }; }
3288
+
3289
+ return {
3290
+ ok: true,
3291
+ project: state.project,
3292
+ current_phase: state.current_phase,
3293
+ current_plan: state.current_plan,
3294
+ current_sprint: state.current_sprint,
3295
+ phase_count: (state.phases || []).length,
3296
+ decisions_count: (state.decisions || []).length,
3297
+ blockers_open: (state.blockers || []).filter(b => !b.resolved).length,
3298
+ last_session: state.last_session,
3299
+ updated: state.updated,
3300
+ active_workstream: state.active_workstream,
3301
+ };
3302
+ }
3303
+
3304
+ function cmdFindFiles(rawArgs) {
3305
+ const flags = {};
3306
+ const parts = rawArgs.split(/\s+/).filter(p => p);
3307
+ for (let i = 0; i < parts.length; i++) {
3308
+ if (parts[i].startsWith('--')) {
3309
+ const key = parts[i].slice(2);
3310
+ flags[key] = parts[i + 1] || true;
3311
+ if (parts[i + 1] && !parts[i + 1].startsWith('--')) i++;
3312
+ }
3313
+ }
3314
+ const type = flags.type || 'all';
3315
+ const patterns = {
3316
+ 'design-tokens': ['tailwind.config.*','tokens.*','design-tokens*','**/theme.*','**/colors.*'],
3317
+ 'colors': ['**/colors.*','**/palette.*','**/theme.*'],
3318
+ 'fonts': ['**/fonts.*','**/typography.*','**/font.css'],
3319
+ 'all': ['**/*'],
3320
+ }[type] || ['**/*'];
3321
+ const matches = [];
3322
+ function walk(dir) {
3323
+ if (!fs.existsSync(dir)) return;
3324
+ try {
3325
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
3326
+ if (e.name.startsWith('.') || e.name === 'node_modules') continue;
3327
+ const p = path.join(dir, e.name);
3328
+ if (e.isDirectory()) walk(p);
3329
+ else if (e.isFile()) {
3330
+ for (const pat of patterns) {
3331
+ const re = new RegExp(pat.replace(/\*\*/g,'.*').replace(/\*/g,'[^/]*').replace(/\./g,'\\.'));
3332
+ if (re.test(e.name) || re.test(p)) { matches.push(p); break; }
3333
+ }
3334
+ }
3335
+ }
3336
+ } catch (err) {
3337
+ // Silently skip directories we can't read
3338
+ }
3339
+ }
3340
+ walk(PROJECT_ROOT);
3341
+ return { ok: true, type, matches };
3342
+ }
3343
+
3344
+ async function main() {
3345
+ const [, , subcommand, ...args] = process.argv;
3346
+ try {
3347
+ let result;
3348
+ switch (subcommand) {
3349
+ case 'init':
3350
+ if (args[0] === 'execute') {
3351
+ result = cmdInitExecute(args.slice(1).join(' '));
3352
+ } else if (args[0] === 'plan') {
3353
+ result = cmdInitPlan(args.slice(1).join(' '));
3354
+ } else if (args[0] === 'discuss') {
3355
+ result = cmdInitDiscuss(args.slice(1).join(' '));
3356
+ } else if (args[0] === 'chain') {
3357
+ result = cmdInitChain(args.slice(1).join(' '));
3358
+ } else {
3359
+ result = cmdInit(args[0] || '', args.slice(1).join(' '));
3360
+ }
3361
+ break;
3362
+ case 'plan':
3363
+ if (args[0] === 'list') { result = cmdPlanList(); }
3364
+ else { console.error('Unknown plan subcommand. Valid: list'); process.exit(1); }
3365
+ break;
3366
+ case 'notes':
3367
+ if (args[0] === 'list') { result = cmdNotesList(); }
3368
+ else if (args[0] === 'count') { result = cmdNotesCount(); }
3369
+ else { console.error('Unknown notes subcommand. Valid: list, count'); process.exit(1); }
3370
+ break;
3371
+ case 'select-panel':
3372
+ result = cmdSelectPanel(args.join(' '));
3373
+ break;
3374
+ case 'agent-info':
3375
+ result = cmdAgentInfo(args[0]);
3376
+ break;
3377
+ case 'list-agents':
3378
+ result = cmdListAgents();
3379
+ break;
3380
+ case 'classify-question':
3381
+ result = cmdClassifyQuestion(args.join(' '));
3382
+ break;
3383
+ case 'state':
3384
+ result = cmdState(args);
3385
+ break;
3386
+ case 'module':
3387
+ result = cmdModule(args);
3388
+ break;
3389
+ case 'resolve-model':
3390
+ result = cmdResolveModel(args[0]);
3391
+ break;
3392
+ case 'config':
3393
+ if (args[0] === 'set') {
3394
+ result = cmdConfigSet(args.slice(1));
3395
+ } else {
3396
+ console.error('Unknown config subcommand. Valid: set');
3397
+ process.exit(1);
3398
+ }
3399
+ break;
3400
+ case 'notify':
3401
+ result = await cmdNotify(args);
3402
+ break;
3403
+ case 'find-files':
3404
+ result = cmdFindFiles(args.join(' '));
3405
+ break;
3406
+ case 'verify-references': {
3407
+ const planPath = args[0];
3408
+ if (!planPath) { console.error('Usage: verify-references <plan-path>'); process.exit(1); }
3409
+ const cr = require(path.join(__dirname, 'lib', 'code-references.cjs'));
3410
+ const text = fs.readFileSync(planPath, 'utf8');
3411
+ const refs = cr.extractReferences(text);
3412
+ const result = cr.verifyReferences(refs, PROJECT_ROOT);
3413
+ console.log(JSON.stringify(result, null, 2));
3414
+ return;
3415
+ }
3416
+ case 'roadmap': {
3417
+ const roadmap = require(path.join(__dirname, 'lib', 'roadmap.cjs'));
3418
+ const r = roadmap.dispatch(PROJECT_ROOT, args);
3419
+ if (r && typeof r === 'object' && '__raw' in r) {
3420
+ console.log(r.__raw);
3421
+ return;
3422
+ }
3423
+ result = r;
3424
+ break;
3425
+ }
3426
+ case 'config-get': {
3427
+ const cfg = require(path.join(__dirname, 'lib', 'config.cjs'));
3428
+ const val = cfg.cmdGet(PROJECT_ROOT, args[0]);
3429
+ if (val !== null && val !== undefined) console.log(val);
3430
+ return;
3431
+ }
3432
+ case 'config-set': {
3433
+ const cfg = require(path.join(__dirname, 'lib', 'config.cjs'));
3434
+ result = cfg.cmdSet(PROJECT_ROOT, args[0], args.slice(1).join(' '));
3435
+ break;
3436
+ }
3437
+ case 'verify': {
3438
+ const verify = require(path.join(__dirname, 'lib', 'verify.cjs'));
3439
+ result = verify.dispatch(PROJECT_ROOT, args);
3440
+ break;
3441
+ }
3442
+ case 'brain': {
3443
+ result = cmdBrain(args);
3444
+ break;
3445
+ }
3446
+ case 'progress': {
3447
+ result = cmdProgress(args);
3448
+ break;
3449
+ }
3450
+ case 'summary-extract': {
3451
+ result = cmdSummaryExtract(args);
3452
+ break;
3453
+ }
3454
+ case 'state-snapshot': {
3455
+ result = cmdStateSnapshot();
3456
+ break;
3457
+ }
3458
+ case 'agent-skills':
3459
+ result = cmdAgentInfo(args[0]);
3460
+ break;
3461
+ case 'version':
3462
+ console.log(readPackageVersion());
3463
+ return;
3464
+ case 'help':
3465
+ case '--help':
3466
+ case '-h':
3467
+ case undefined:
3468
+ console.log('Usage: rihal-tools.cjs <init|select-panel|classify-question|agent-info|agent-skills|list-agents|state|module|plan|notes|config|config-get|config-set|roadmap|verify|notify|resolve-model|version|help> [args]');
3469
+ console.log('');
3470
+ console.log('Top-level subcommands:');
3471
+ console.log(' init → initialize .rihal directory structure');
3472
+ console.log(' select-panel → choose council panel members');
3473
+ console.log(' classify-question → categorize user questions');
3474
+ console.log(' agent-info <name> → show agent metadata and skills');
3475
+ console.log(' agent-skills <name> → alias for agent-info');
3476
+ console.log(' list-agents → list all available Rihal agents');
3477
+ console.log(' state <subcommand> [args] → manage .rihal/state.json');
3478
+ console.log(' module <subcommand> [args] → module system helpers');
3479
+ console.log(' plan <subcommand> [args] → phase/plan operations');
3480
+ console.log(' notes <subcommand> [args] → manage project notes');
3481
+ console.log(' config <subcommand> [args] → read/write project config');
3482
+ console.log(' notify send --title "<t>" [--body "<b>"] [--event <e>] [--only slack|discord|teams] → post to configured webhooks');
3483
+ console.log(' roadmap <get-phase|list-phases|update-plan-progress|clear> → .planning/ROADMAP.md operations');
3484
+ console.log(' config-get <dotted.key> → read scalar from .rihal/config.yaml');
3485
+ console.log(' config-set <dotted.key> <value> → atomically set a value in .rihal/config.yaml');
3486
+ console.log(' verify schema-drift <phase> [--block] → detect schema vs migration drift across phase commits');
3487
+ console.log(' resolve-model <profile> → resolve model name from profile');
3488
+ console.log(' version → print rihal-tools version');
3489
+ console.log(' help → print this help text');
3490
+ console.log('');
3491
+ console.log('State subcommands:');
3492
+ console.log(' state read → print full state.json');
3493
+ console.log(' state get → alias for state read');
3494
+ console.log(' state init --project <name> → create state.json if missing');
3495
+ console.log(' state set-phase <name> → set current phase, reset plan counter');
3496
+ console.log(' state advance-plan → increment current_plan counter');
3497
+ console.log(' state record-execution --plan <p> --tasks <n> --duration <ms> --hash <h>');
3498
+ console.log(' state add-decision "<summary>" → append to decisions[] + ~/.rihal/decisions.jsonl');
3499
+ console.log(' state decisions-global [--limit N] [--project <name>] [--since <ISO>] → query ~/.rihal/decisions.jsonl across all projects');
3500
+ console.log(' state add-blocker "<description>" → append to blockers[]');
3501
+ console.log(' state resolve-blocker <index> → mark blocker as resolved');
3502
+ console.log(' state record-session → update last_session timestamp');
3503
+ console.log(' state record-council --slug <s> --panel <csv> --artifact <path>');
3504
+ console.log(' state record-chain --slug <s> --agents <csv> --artifacts <path>');
3505
+ console.log(' state insert-phase --number <N.M> --name <slug>');
3506
+ console.log(' state next-phase-id → return next available 2-digit phase ID');
3507
+ console.log(' state next-plan-id <phase-id> → return next plan ID within phase');
3508
+ console.log(' state next-task-id <plan-id> → return next task ID within plan');
3509
+ console.log(' state resolve-id <id> → resolve ID to paths and metadata');
3510
+ console.log(' state set-ids-in-state → scan .planning/ and populate state arrays');
3511
+ console.log(' state migrate-ids → migrate existing PLAN.md files with IDs');
3512
+ console.log(' state workstream-create --name <name> → create a new workstream');
3513
+ console.log(' state workstream-switch --name <name> → switch active workstream');
3514
+ console.log(' state workstream-list → list all workstreams');
3515
+ console.log(' state workstream-status → show active workstream');
3516
+ console.log(' state workstream-complete --name <name> → mark workstream done');
3517
+ console.log(' state workstream-validate → validate workstream schema');
3518
+ console.log('');
3519
+ console.log('Sprint subcommands:');
3520
+ console.log(' state sprint add --phase <NN> --goal "..." → create sprint under phase');
3521
+ console.log(' state sprint list [--phase <NN>] → list all sprints');
3522
+ console.log(' state sprint status [--sprint <NN.S>] → sprint progress + points');
3523
+ console.log(' state sprint start [--sprint <NN.S>] → mark sprint active');
3524
+ console.log(' state sprint complete [--sprint <NN.S>] → complete sprint, record velocity');
3525
+ console.log(' state sprint velocity → velocity history + average');
3526
+ console.log('');
3527
+ console.log('Story subcommands:');
3528
+ console.log(' state story add --sprint <NN.S> --title "..." [--points N]');
3529
+ console.log(' state story move --id <NN.S.TT> --status <todo|in_progress|review|done>');
3530
+ console.log(' state story list [--sprint <NN.S>] [--status <status>]');
3531
+ return;
3532
+ default: {
3533
+ const stateSubs = ['read','get','init','set-phase','advance-plan','record-execution','record-council','record-chain','add-decision','decisions-global','add-blocker','resolve-blocker','record-session','set-ids-in-state','migrate-ids','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'];
3534
+ if (stateSubs.includes(subcommand)) {
3535
+ console.error(`Did you mean: state ${subcommand}? Run 'rihal-tools.cjs help' for full usage.`);
3536
+ } else {
3537
+ console.error(`Unknown subcommand: ${subcommand}. Run 'rihal-tools.cjs help' for full usage.`);
3538
+ }
3539
+ process.exit(1);
3540
+ }
3541
+ }
3542
+ console.log(JSON.stringify(result, null, 2));
3543
+ } catch (err) {
3544
+ console.error(`rihal-tools error: ${err.message}`);
3545
+ if (process.env.DEBUG) console.error(err.stack);
3546
+ process.exit(1);
3547
+ }
3548
+ }
3549
+
3550
+ main().catch((err) => {
3551
+ console.error(`rihal-tools error: ${err.message}`);
3552
+ if (process.env.DEBUG) console.error(err.stack);
3553
+ process.exit(1);
3554
+ });