@polymorphism-tech/morph-spec 8.47.0 → 9.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 (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -25,27 +25,11 @@ import { samePathCase } from '../../lib/worktree/docker-audit.js';
25
25
 
26
26
  const isWindows = platform() === 'win32';
27
27
 
28
- /**
29
- * Resolved-path equality for a `cwd` string against the project root —
30
- * `"./src/web"`, `"src/web/"` and (on win32) `"src\\web"` must compare equal
31
- * to `"src/web"`. Case-insensitive on win32 only, through `samePathCase` of
32
- * `src/lib/worktree/docker-audit.js` — the one copy of that rule (#87 F5): NTFS
33
- * itself is case-insensitive, so two spellings that differ only in case are
34
- * the same directory there and nowhere else.
35
- *
36
- * @param {string} root - Project root the cwd is relative to
37
- * @param {string|null|undefined} cwd
38
- * @returns {string}
39
- */
28
+ // NTFS is case-insensitive: case folding goes through `samePathCase`, the one copy of that rule.
40
29
  function normCwd(root, cwd) {
41
30
  return samePathCase(resolve(root, String(cwd ?? '.')).replace(/\\/g, '/'));
42
31
  }
43
32
 
44
- /**
45
- * Check which required Claude Code plugins are installed.
46
- * @param {Object} pluginsData - The `plugins` object from installed_plugins.json
47
- * @returns {{ context7: boolean }}
48
- */
49
33
  export function checkInstalledPlugins(pluginsData) {
50
34
  const REQUIRED = ['context7'];
51
35
  return Object.fromEntries(
@@ -54,9 +38,6 @@ export function checkInstalledPlugins(pluginsData) {
54
38
  }
55
39
 
56
40
 
57
- /**
58
- * Check if morph-spec command is in PATH
59
- */
60
41
  function isMorphSpecInPath() {
61
42
  try {
62
43
  const command = isWindows ? 'where morph-spec' : 'which morph-spec';
@@ -67,9 +48,6 @@ function isMorphSpecInPath() {
67
48
  }
68
49
  }
69
50
 
70
- /**
71
- * Detect if nvm-windows is installed
72
- */
73
51
  function isUsingNvmWindows() {
74
52
  if (!isWindows) return false;
75
53
 
@@ -81,9 +59,6 @@ function isUsingNvmWindows() {
81
59
  }
82
60
  }
83
61
 
84
- /**
85
- * Get npm global prefix
86
- */
87
62
  function getNpmGlobalPrefix() {
88
63
  try {
89
64
  const prefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
@@ -93,12 +68,10 @@ function getNpmGlobalPrefix() {
93
68
  }
94
69
  }
95
70
 
96
- // lib files (v5 — minimal surface)
97
71
  const REQUIRED_LIB_FILES = [
98
72
  'src/lib/stack-filter.js'
99
73
  ];
100
74
 
101
- // command files (v5 CLI surface — installer-only)
102
75
  const REQUIRED_COMMAND_FILES = [
103
76
  'src/commands/project/init.js',
104
77
  'src/commands/project/update.js',
@@ -112,20 +85,6 @@ const REQUIRED_COMMAND_FILES = [
112
85
  'src/commands/context/context-refresh.js',
113
86
  ];
114
87
 
115
- /**
116
- * Validate a parsed `agents.json` against the v5 contract.
117
- *
118
- * Exported, and pure, so the tests can exercise the REAL rules. They used to
119
- * re-implement this logic inline "because doctorCommand doesn't export
120
- * individual check functions" — and the two promptly disagreed: the tests
121
- * asserted a v4 document (version 4.x, `tier` and `active_phases` required)
122
- * while this code treats exactly those fields as deprecated. Both stayed green
123
- * because they never met. A canary that destroyed this whole check left the
124
- * suite at 18/18.
125
- *
126
- * @param {Object} agentsData - parsed agents.json
127
- * @returns {Array<{name: string, status: 'ok'|'warn'|'error', msg?: string}>}
128
- */
129
88
  export function validateAgentsJson(agentsData) {
130
89
  const checks = [];
131
90
  const agentEntries = Object.entries(agentsData.agents || {})
@@ -133,7 +92,6 @@ export function validateAgentsJson(agentsData) {
133
92
  const actualCount = agentEntries.length;
134
93
  const declaredCount = agentsData.total_agents;
135
94
 
136
- // 1: total_agents matches actual count
137
95
  if (actualCount !== declaredCount) {
138
96
  checks.push({
139
97
  name: 'agents.json count',
@@ -144,7 +102,6 @@ export function validateAgentsJson(agentsData) {
144
102
  checks.push({ name: 'agents.json count', status: 'ok', msg: `${actualCount} agents` });
145
103
  }
146
104
 
147
- // 2: required v5 personas present
148
105
  const missingPersonas = REQUIRED_AGENTS.filter(id => !(agentsData.agents || {})[id]);
149
106
  if (missingPersonas.length > 0) {
150
107
  checks.push({
@@ -156,7 +113,6 @@ export function validateAgentsJson(agentsData) {
156
113
  checks.push({ name: 'agents.json required personas', status: 'ok' });
157
114
  }
158
115
 
159
- // 3: v5 schema (flat personas, no tiers)
160
116
  const version = agentsData.version || 'unknown';
161
117
  const isV5 = version.startsWith('5.');
162
118
  const v4Fields = [];
@@ -191,8 +147,7 @@ export function validateAgentsJson(agentsData) {
191
147
  });
192
148
  }
193
149
 
194
- // 4: maxTurns (ADR-004) — the installer's own rule, so this cannot pass a
195
- // value the installer refuses. ERROR, not warn: that persona is not generated.
150
+ // The installer's own rule, so this cannot pass a value it refuses. ERROR: that persona is not generated.
196
151
  const maxTurnsErrors = agentEntries
197
152
  .map(([id, data]) => maxTurnsError(id, data?.maxTurns))
198
153
  .filter(Boolean);
@@ -200,7 +155,6 @@ export function validateAgentsJson(agentsData) {
200
155
  ? { name: 'agents.json maxTurns', status: 'error', msg: maxTurnsErrors.join(' | ') }
201
156
  : { name: 'agents.json maxTurns', status: 'ok' });
202
157
 
203
- // 5: surfaces — same installer rule, same severity: the persona is not generated.
204
158
  const surfacesErrors = agentEntries
205
159
  .map(([id, data]) => surfacesError(id, data?.surfaces, data?.standards))
206
160
  .filter(Boolean);
@@ -208,7 +162,6 @@ export function validateAgentsJson(agentsData) {
208
162
  ? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
209
163
  : { name: 'agents.json surfaces', status: 'ok' });
210
164
 
211
- // 6: effort — same installer rule, same severity.
212
165
  const effortErrors = agentEntries
213
166
  .map(([id, data]) => effortError(id, data?.effort))
214
167
  .filter(Boolean);
@@ -228,12 +181,7 @@ export function validateAgentsJson(agentsData) {
228
181
  return checks;
229
182
  }
230
183
 
231
- // framework standards (stack-relevant only).
232
- // Keep every entry pointing at a file that exists: a check that can never pass
233
- // carries the same information as one that can never fail — none. This list
234
- // still named two standards from a `workflows/` directory that no longer
235
- // exists, which pinned `doctor --full` at a permanent 3/6 and made a real
236
- // regression indistinguishable from the usual red.
184
+ // Every entry must point at an existing file: a check that can never pass reports nothing.
237
185
  const FRAMEWORK_STANDARDS = [
238
186
  'framework/standards/architecture/vertical-slice/vertical-slice.md',
239
187
  'framework/standards/data/nosql/cosmos-db.md',
@@ -242,16 +190,13 @@ const FRAMEWORK_STANDARDS = [
242
190
  'framework/standards/integration/mcp/mcp-tools.md'
243
191
  ];
244
192
 
245
- // required personas expected in agents.json (v5 — the universal, stack-agnostic
246
- // personas: evaluator (domain axis) and scout (stage axis))
247
193
  const REQUIRED_AGENTS = [
248
- 'evaluator',
194
+ 'verifier',
195
+ 'reviewer',
196
+ 'security',
249
197
  'scout'
250
198
  ];
251
199
 
252
- /**
253
- * Run full health checks
254
- */
255
200
  async function doctorFullCommand(frameworkRoot) {
256
201
  console.log(chalk.bold('\n🔬 MORPH-SPEC Full Health Check\n'));
257
202
  console.log('─'.repeat(60));
@@ -270,15 +215,8 @@ async function doctorFullCommand(frameworkRoot) {
270
215
  }
271
216
  };
272
217
 
273
- // Two layouts reach this command and they disagree on where everything is.
274
- //
275
- // framework repo src/, framework/, no .morph/ at all
276
- // installed project .morph/framework/, and the CLI in node_modules
277
- //
278
- // Half the checks assumed one layout and half the other, so `--full` could
279
- // not pass anywhere: red in the repo (no .morph/) and red in a project (no
280
- // src/). A check that can never pass reports exactly as much as one that can
281
- // never fail. Each check now runs only where it means something.
218
+ // Two layouts reach this command (framework repo: src/, no .morph/; installed project:
219
+ // .morph/framework/, CLI in node_modules). Each check runs only where it means something.
282
220
  const installedAgentsPath = join(frameworkRoot, '.morph/framework/agents.json');
283
221
  const canonicalAgentsPath = join(frameworkRoot, 'framework/agents.json');
284
222
  const inFrameworkRepo = !(await pathExists(installedAgentsPath))
@@ -287,9 +225,7 @@ async function doctorFullCommand(frameworkRoot) {
287
225
 
288
226
  console.log(chalk.gray(`\n Contexto: ${inFrameworkRepo ? 'repo do framework' : 'projeto instalado'}`));
289
227
 
290
- // ── 1-2. Source tree (framework repo only) ──────────────────────────────
291
- // An installed project has no src/ — the CLI lives in node_modules, and its
292
- // integrity is npm's job, not the doctor's.
228
+ // An installed project has no src/: the CLI's integrity in node_modules is npm's job.
293
229
  if (inFrameworkRepo) {
294
230
  console.log(chalk.cyan(`\n src/lib/ — Core Libraries (${REQUIRED_LIB_FILES.length} files)`));
295
231
  const missingLibs = [];
@@ -310,9 +246,6 @@ async function doctorFullCommand(frameworkRoot) {
310
246
  missingCmds.length > 0 ? `missing: ${missingCmds.join(', ')}` : '');
311
247
  }
312
248
 
313
- // ── 3. Framework Standards ───────────────────────────────────────────────
314
- // Same files, different root: the repo holds them at framework/standards/,
315
- // an installed project at .morph/framework/standards/.
316
249
  const stdsRoot = inFrameworkRepo ? frameworkRoot : join(frameworkRoot, '.morph');
317
250
  console.log(chalk.cyan(`\n ${inFrameworkRepo ? '' : '.morph/'}framework/standards/ — Framework Standards (${FRAMEWORK_STANDARDS.length} files)`));
318
251
  const missingStds = [];
@@ -323,7 +256,6 @@ async function doctorFullCommand(frameworkRoot) {
323
256
  missingStds.length === 0, false,
324
257
  missingStds.length > 0 ? `missing: ${missingStds.join(', ')}` : '');
325
258
 
326
- // ── 5. Required Agents in agents.json ───────────────────────────────────
327
259
  console.log(chalk.cyan(`\n ${inFrameworkRepo ? 'framework' : '.morph/framework'}/agents.json — Required Agents (${REQUIRED_AGENTS.length})`));
328
260
  if (await pathExists(agentsPath)) {
329
261
  try {
@@ -340,10 +272,7 @@ async function doctorFullCommand(frameworkRoot) {
340
272
  check('agents.json', false, false, 'file not found');
341
273
  }
342
274
 
343
- // ── 6. agents.json version freshness ──────────────────────────────────
344
- // Only meaningful for an installed project: it compares what was installed
345
- // against the canonical source. In the framework repo the two are the same
346
- // file, so the comparison is vacuous and is skipped.
275
+ // Installed project only: in the framework repo source and installed copy are the same file.
347
276
  if (!inFrameworkRepo && await pathExists(agentsPath) && await pathExists(canonicalAgentsPath)) {
348
277
  try {
349
278
  const installed = JSON.parse(await fs.readFile(agentsPath, 'utf8'));
@@ -353,15 +282,10 @@ async function doctorFullCommand(frameworkRoot) {
353
282
  const isFresh = installedVer === canonicalVer;
354
283
  check(`agents.json version (${installedVer})`, isFresh, true,
355
284
  isFresh ? '' : `stale — canonical is v${canonicalVer}, run 'morph-spec update' to sync`);
356
- } catch { /* ignore parse errors — already caught above */ }
285
+ } catch {}
357
286
  }
358
287
 
359
- // ── 6b. Generated personas drift (.claude/agents ↔ agents.json) ─────────
360
- // agents.json is the single source of truth; the .claude/agents/morph-*.md
361
- // files are derived artifacts (ADR-001). Missing = broken dispatch (error);
362
- // stale/hand-edited or orphan = drift (warning).
363
- // Skipped in the framework repo: the generated `.claude/agents/morph-*.md`
364
- // are an artifact of installing into a project, not of the source tree.
288
+ // Skipped in the framework repo: the generated `.claude/agents/morph-*.md` only exist in a project.
365
289
  if (!inFrameworkRepo && await pathExists(agentsPath)) {
366
290
  console.log(chalk.cyan('\n .claude/agents/ — Generated personas (drift vs agents.json)'));
367
291
  try {
@@ -371,7 +295,7 @@ async function doctorFullCommand(frameworkRoot) {
371
295
  try {
372
296
  const rawCfg = (await fs.readFile(cfgPath, 'utf8')).replace(/^/, '');
373
297
  projectTags = getProjectTags(JSON.parse(rawCfg));
374
- } catch { /* no tags */ }
298
+ } catch {}
375
299
  }
376
300
  // The MCP registry comes from the package, like the installer's: the
377
301
  // installed .morph/framework/ has no mcp/, and rendering without it
@@ -397,7 +321,6 @@ async function doctorFullCommand(frameworkRoot) {
397
321
  }
398
322
  }
399
323
 
400
- // ── 6c. GitHub tooling — gh CLI (canonical; morph-spec does not use a GitHub MCP) ──
401
324
  console.log(chalk.cyan('\n Tooling — GitHub via gh CLI'));
402
325
  try {
403
326
  execSync('gh --version', { stdio: 'pipe', timeout: 5000 });
@@ -407,7 +330,6 @@ async function doctorFullCommand(frameworkRoot) {
407
330
  "GitHub ops (issues/PRs/releases) usam o gh CLI — instale https://cli.github.com e rode 'gh auth login'");
408
331
  }
409
332
 
410
- // ── 7. state.json schema version ────────────────────────────────────────
411
333
  console.log(chalk.cyan('\n .morph/state.json — Schema Version'));
412
334
  const statePath = join(frameworkRoot, '.morph/state.json');
413
335
  if (await pathExists(statePath)) {
@@ -423,23 +345,8 @@ async function doctorFullCommand(frameworkRoot) {
423
345
  check('state.json', false, true, 'not found (run: morph-spec init)');
424
346
  }
425
347
 
426
- // ── 8. project.tests[] — o contrato do config, camada 3 ─────────────────
427
- //
428
- // As outras duas camadas são o `config.schema.json` (declarativa) e
429
- // `validateTestNodes()` chamada pelo `verify` (fail-closed, em uso). Esta
430
- // aqui existe para o defeito chegar ANTES do gate: um `id` com typo em
431
- // `tests[]` só se manifesta quando alguém roda `morph-spec verify` e lê um
432
- // nó vermelho — o doctor o mostra na checagem de saúde, que é onde se olha
433
- // primeiro. Reusa `validateTestNodes` (a MESMA autoridade que o verify
434
- // consulta): uma segunda leitura do mesmo campo aqui seria uma segunda
435
- // opinião sobre o que é um nó válido, e as duas divergiriam.
436
- //
437
- // PULA de verdade — sem check e sem cabeçalho — quando não há `config.json`
438
- // ou quando `project.tests` não foi declarado. Um projeto sem declaração usa
439
- // o fallback de quatro passos e não tem contrato a violar; este repositório
440
- // é exatamente esse caso, e um check que não pode passar aqui é ruído, não
441
- // achado (`test/commands/doctor.test.js`: `doctor --full` sem nenhum `✗`).
442
- // WARN e nunca ERROR: a barreira fail-closed é o `verify`; o doctor informa.
348
+ // Mesma autoridade do `verify` (validateTestNodes). Pula sem `config.json` ou sem `project.tests`:
349
+ // não há contrato a violar, e `doctor --full` não pode ter ✗ aqui. WARN, nunca ERROR.
443
350
  const testsConfigPath = morphConfigPath(frameworkRoot);
444
351
  if (await pathExists(testsConfigPath)) {
445
352
  let testsConfig = null;
@@ -458,12 +365,8 @@ async function doctorFullCommand(frameworkRoot) {
458
365
  ? broken.map((n) => n.reason ?? `invalid-test-node: ${n.id}`).join('; ')
459
366
  : '');
460
367
 
461
- // #87 PR B: a vitest/jest package.json declared tests[] never mentions is
462
- // exactly the shape F30 exploited — a real suite sitting outside the gate.
463
- // WARN-only, never ✗: the fail-closed barrier is `verify`, this just informs.
464
- // Compared by RESOLVED path, not raw string: "./src/web", "src/web/" and
465
- // (win32) "src\\web" all address the same node as "src/web" and must not
466
- // produce a spurious warning just because the spelling differs.
368
+ // A vitest/jest package.json that no declared node covers is a suite outside the gate (WARN only).
369
+ // Compared by RESOLVED path so a different spelling of the same dir doesn't warn.
467
370
  const declaredCwds = new Set(declared.map((n) => normCwd(frameworkRoot, n.cwd)));
468
371
  const uncoveredPkgs = discoverFrontendPackages(frameworkRoot, testsConfig)
469
372
  .filter((pkg) => !declaredCwds.has(normCwd(frameworkRoot, pkg.cwd)));
@@ -474,10 +377,7 @@ async function doctorFullCommand(frameworkRoot) {
474
377
  );
475
378
  }
476
379
  } else if (typeof testsConfig?.project?.testCommand === 'string' && testsConfig.project.testCommand.trim()) {
477
- // Mesma checagem, para o caso testCommand-only: o comando é OPACO (uma
478
- // string de shell), então cobertura aqui é só "o comando MENCIONA o
479
- // runner do package.json" — não confirmar isso seria ruído para quem já
480
- // roda vitest por dentro do testCommand.
380
+ // testCommand é opaco: cobertura aqui é só "o comando MENCIONA o runner do package.json".
481
381
  const testCommand = testsConfig.project.testCommand;
482
382
  const uncoveredPkgs = discoverFrontendPackages(frameworkRoot, testsConfig)
483
383
  .filter((pkg) => !testCommand.includes(pkg.runner));
@@ -491,7 +391,6 @@ async function doctorFullCommand(frameworkRoot) {
491
391
  }
492
392
  }
493
393
 
494
- // ── Display ──────────────────────────────────────────────────────────────
495
394
  console.log('\n' + '─'.repeat(60));
496
395
  console.log(chalk.bold('\nResults:\n'));
497
396
  for (const c of checks) {
@@ -518,19 +417,7 @@ async function doctorFullCommand(frameworkRoot) {
518
417
  }
519
418
  }
520
419
 
521
- /**
522
- * MCP health check: reads .mcp.json, settings.json, and settings.local.json
523
- * and validates each configured MCP server's command binary and env var requirements.
524
- *
525
- * Strategy:
526
- * - "ok" : command binary exists (or is a known runtime like npx/node) AND
527
- * all env vars referenced in args/env are set in process.env
528
- * - "warn" : binary found but env vars missing (MCP won't auth)
529
- * - "missing": command binary not found in PATH
530
- *
531
- * NOTE: This does NOT test actual connectivity — that requires spawning the MCP.
532
- * It's a static configuration health check that catches 80% of "MCP not working" cases.
533
- */
420
+ // Static config check only: connectivity would require spawning the MCP.
534
421
  async function doctorMcpCommand(targetPath) {
535
422
  logger.header('MCP Server Health Check');
536
423
  logger.blank();
@@ -538,7 +425,6 @@ async function doctorMcpCommand(targetPath) {
538
425
  const allMcpServers = {};
539
426
  const sourcesFound = [];
540
427
 
541
- // Primary: .mcp.json (Claude Code reads MCPs from here)
542
428
  const mcpJsonPath = join(targetPath, '.mcp.json');
543
429
  if (await pathExists(mcpJsonPath)) {
544
430
  try {
@@ -549,10 +435,9 @@ async function doctorMcpCommand(targetPath) {
549
435
  Object.assign(allMcpServers, mcps);
550
436
  sourcesFound.push(`.mcp.json (${count})`);
551
437
  }
552
- } catch { /* invalid JSON — ignore */ }
438
+ } catch {}
553
439
  }
554
440
 
555
- // Legacy: settings files (for backward compat)
556
441
  const settingsPaths = [
557
442
  join(targetPath, '.claude', 'settings.json'),
558
443
  join(targetPath, '.claude', 'settings.local.json'),
@@ -573,7 +458,7 @@ async function doctorMcpCommand(targetPath) {
573
458
  if (addedCount > 0) {
574
459
  sourcesFound.push(`${relative(targetPath, sp)} (${addedCount})`);
575
460
  }
576
- } catch { /* invalid JSON — ignore */ }
461
+ } catch {}
577
462
  }
578
463
  }
579
464
 
@@ -596,14 +481,11 @@ async function doctorMcpCommand(targetPath) {
596
481
  let status = 'ok';
597
482
  let isRemote = false;
598
483
 
599
- // ── 0. Remote MCP detection ────────────────────────────────────────────
600
- // Remote MCPs use HTTP transport (type: "http" or url property) — no binary to check
601
484
  if (config.type === 'http' || (config.url && !config.command)) {
602
485
  isRemote = true;
603
486
  // Remote MCPs are always "ok" from a config perspective — auth happens at runtime
604
487
  }
605
488
 
606
- // ── 1. Binary check (stdio MCPs only) ──────────────────────────────────
607
489
  const cmd = config.command || '';
608
490
  if (!isRemote && cmd && !KNOWN_RUNTIMES.has(cmd)) {
609
491
  try {
@@ -614,7 +496,6 @@ async function doctorMcpCommand(targetPath) {
614
496
  }
615
497
  }
616
498
 
617
- // ── 2. Env vars from config.env ────────────────────────────────────────
618
499
  for (const [key, val] of Object.entries(config.env || {})) {
619
500
  if (typeof val === 'string' && (val.startsWith('${') || (val.startsWith('$') && !val.includes(' ')))) {
620
501
  const varName = val.replace(/^\$\{?/, '').replace(/\}$/, '');
@@ -625,7 +506,6 @@ async function doctorMcpCommand(targetPath) {
625
506
  }
626
507
  }
627
508
 
628
- // ── 3. Env refs in args array ──────────────────────────────────────────
629
509
  const argsStr = (config.args || []).join(' ');
630
510
  const envRefsInArgs = [...argsStr.matchAll(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g)].map(m => m[1]);
631
511
  for (const varName of envRefsInArgs) {
@@ -638,7 +518,6 @@ async function doctorMcpCommand(targetPath) {
638
518
  checks.push({ name, status, issues, cmd, isRemote });
639
519
  }
640
520
 
641
- // ── Display ────────────────────────────────────────────────────────────────
642
521
  for (const c of checks) {
643
522
  const typeLabel = c.isRemote
644
523
  ? chalk.gray(` [remote: ${allMcpServers[c.name]?.url || 'http'}]`)
@@ -677,23 +556,8 @@ async function doctorMcpCommand(targetPath) {
677
556
  }
678
557
 
679
558
  /**
680
- * `morph-spec doctor --worktrees [--prune]` — reconcile git's worktree registry
681
- * with the directories on disk and the features morph knows (C2).
682
- *
683
- * Orphan worktrees used to accumulate invisibly: `fleet` only reports worktrees
684
- * on a `morph/*` branch, so anything else never showed up anywhere.
685
- *
686
- * `--prune` runs the safe repairs: `git worktree prune` (admin records for
687
- * directories that are already gone) and the removal of EMPTY unregistered
688
- * residue directories, which by definition hold nothing that can be lost.
689
- * Deleting a directory that still HOLDS something is reported with the exact
690
- * command, never performed — same PROVE-or-DECLINE discipline as the finish
691
- * teardown.
692
- *
693
- * `--prune` also applies the retention of the finish backups — the same policy
694
- * every `finish` applies on its own, and the ONLY path that also applies it to
695
- * the backups written before the retention existed (no manifest), counting
696
- * them against the budget. Under `--json` its result rides in the JSON.
559
+ * `--prune` runs only safe repairs (`git worktree prune`, EMPTY unregistered dirs) and the
560
+ * finish-backup retention; a directory that still holds something is reported, never deleted.
697
561
  */
698
562
  export async function doctorWorktreesCommand(targetPath, options = {}) {
699
563
  const { auditWorktrees, pruneWorktrees } = await import('../../lib/worktree/worktree-audit.js');
@@ -758,13 +622,8 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
758
622
  }
759
623
 
760
624
  const issues = auditWorktrees(targetPath);
761
- // `severity: 'info'` entries (a worktree carrying unintegrated work) and
762
- // `severity: 'warn'` entries (a worktree created outside morph's container,
763
- // and the docker findings from `docker-audit.js`: an orphan stack, a port
764
- // held by another compose project) are REPORTED but are not failures — a
765
- // `doctor --worktrees || ...` in CI must not go red because somebody has an
766
- // open PR, a hand-made tree, or a container still up. The human path below
767
- // splits the same way; both surfaces must agree on what counts as broken.
625
+ // `info`/`warn` entries are REPORTED, not failures: CI must not go red for an open PR, a hand-made
626
+ // tree or a running container. The human path below splits the same way.
768
627
  const isProblem = (i) => !['info', 'warn'].includes(i.severity);
769
628
 
770
629
  if (options.json) {
@@ -778,10 +637,8 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
778
637
  return true;
779
638
  }
780
639
 
781
- // `unintegrated-work` is NOT an inconsistency — it is a worktree doing its
782
- // job, reported so the reader knows why no cleanup is offered for it. Counting
783
- // it as a problem is how a diagnostic starts nagging people into destroying
784
- // things to make the warning go away.
640
+ // `unintegrated-work` is a worktree doing its job, not an inconsistency: counting it nags people
641
+ // into destroying work to silence the warning.
785
642
  const informational = issues.filter((i) => !isProblem(i));
786
643
  const problems = issues.filter(isProblem);
787
644
 
@@ -796,9 +653,7 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
796
653
  console.log(` ${i.detail}`);
797
654
  console.log(` → ${i.fix}`);
798
655
  }
799
- // An EMPTY unregistered directory is prunable too — it holds nothing that can
800
- // be lost — so the hint must mention it, or the one issue `--prune` now fixes
801
- // automatically would still read as a manual chore.
656
+ // An EMPTY unregistered dir is prunable too, so the hint must mention it.
802
657
  const prunable = (i) => i.type === 'registered-missing'
803
658
  || i.type === 'dangling-registration'
804
659
  || (i.type === 'unregistered-dir' && i.empty);
@@ -815,29 +670,9 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
815
670
  return false;
816
671
  }
817
672
 
818
- /** Dias a partir dos quais um PR aberto vira aviso. */
819
673
  const PR_STALE_DAYS = 7;
820
674
 
821
- /**
822
- * As inconsistências de PR desta árvore.
823
- *
824
- * pr-awaiting-merge — PR aberto há muito, sem cópia local: trabalho em
825
- * revisão que nenhuma superfície local reivindica.
826
- * pr-merged-not-landed — o PR mergeou mas `archive/{f}` não está aqui. O
827
- * archive viaja no diff do PR, então ou falta um
828
- * `git pull`, ou ele não estava no diff. Branch
829
- * `morph/*` cujo diff nunca tocou
830
- * `.morph/{features,archive}/{f}/` não conta: o nome
831
- * não prova feature (ver pr-files.js).
832
- *
833
- * Offline-safe: índice `null` devolve `null`, NUNCA lista vazia — lista vazia
834
- * afirmaria "perguntei e não achei nada", que é diferente de "não pude
835
- * perguntar". Achado #3: um `[]` disfarçado de "sem problema" é o que fazia
836
- * `doctor` imprimir `✓ pull requests` mesmo sem conseguir consultar o `gh`.
837
- *
838
- * @returns {Array<{feature:string,type:string,detail:string}>|null}
839
- * null = DESCONHECIDO (índice não pôde ser construído); nunca "sem PRs".
840
- */
675
+ /** `null` = DESCONHECIDO (o índice não pôde ser construído), nunca "sem PRs". */
841
676
  function auditPullRequests(root) {
842
677
  const index = buildPrIndex(root);
843
678
  if (!index) return null;
@@ -872,26 +707,8 @@ function auditPullRequests(root) {
872
707
  }
873
708
 
874
709
  /**
875
- * `morph-spec doctor --ai` — as cinco checagens de projeto .NET com agentes.
876
- *
877
- * **Superfície própria, no molde do `--mcp`, e é decisão, não acidente.** O
878
- * `--ai` NÃO entra no `--full` nem no `doctor` default:
879
- *
880
- * - o renderer do `--full` é `ok → ✓`, `warn → ⚠`, **`else → ✗`** — não tem o
881
- * ramo `skip → ○`. Uma checagem de IA lá seria VERMELHA neste repositório
882
- * mesmo dizendo "não verificado", e `test/commands/doctor.test.js` exige
883
- * `doesNotMatch(out, /✗/)` para `doctor --full` aqui, com a justificativa
884
- * escrita no próprio teste: *"every check must be satisfiable here — a
885
- * permanently red one teaches nothing"*;
886
- * - o `doctor` default é o comando mais rodado do dia, e cinco linhas `○`
887
- * permanentes em todo projeto sem .NET é ruído com custo e sem informação.
888
- *
889
- * Exit code: 1 se houver ao menos um `error`; 0 caso contrário. `warn` e `skip`
890
- * nunca mudam o código de saída.
891
- *
892
- * @param {string} targetPath
893
- * @param {object} [options] - repassado a `collectAiChecks` (`config`, `deps`)
894
- * @returns {Promise<Array>} as checagens, para o canário assere-las sem parsear texto
710
+ * Superfície própria, fora do `--full` (cujo renderer mostra `skip` como ✗) e do default.
711
+ * Exit 1 só com algum `error`; `warn` e `skip` nunca mudam o código de saída.
895
712
  */
896
713
  export async function doctorAiCommand(targetPath, options = {}) {
897
714
  const { collectAiChecks, aiExitCode, allSkipped } = await import('../../lib/ai/ai-checks.js');
@@ -904,9 +721,7 @@ export async function doctorAiCommand(targetPath, options = {}) {
904
721
  const checks = collectAiChecks(targetPath, { config, deps: options.deps ?? {} });
905
722
 
906
723
  if (allSkipped(checks)) {
907
- // Condição 4 do silêncio: nenhuma checagem teve insumo. Uma linha, não
908
- // cinco `○` vazios — listar cinco vezes "não verificado" é o ruído que
909
- // esta superfície existe para não produzir.
724
+ // Nenhuma checagem teve insumo: uma linha, não cinco `○` vazios.
910
725
  const reasons = [...new Set(checks.map((c) => c.reason))];
911
726
  for (const reason of reasons) {
912
727
  console.log(chalk.gray(` ○ Nenhuma das ${checks.length} checagens teve insumo — ${reason}.`));
@@ -952,10 +767,8 @@ export async function doctorAiCommand(targetPath, options = {}) {
952
767
  }
953
768
 
954
769
  export async function doctorCommand(options = {}) {
955
- // `--json` and `--prune` only mean something under `--worktrees` or `--procs`. Falling
956
- // through to the human health check would hand a script a banner and a table
957
- // where it asked for JSON — a caller has no way to notice a flag that was
958
- // dropped in silence, so refuse instead of pretending.
770
+ // `--json`/`--prune` only mean something under `--worktrees` or `--procs`: refuse rather than
771
+ // drop the flag in silence and hand a script a banner where it asked for JSON.
959
772
  const scoped = ['json', 'prune'].filter((f) => options[f]);
960
773
  if (scoped.length && !options.worktrees && !options.procs) {
961
774
  const flags = scoped.map((f) => `--${f}`).join(' e ');
@@ -978,10 +791,7 @@ export async function doctorCommand(options = {}) {
978
791
  }
979
792
  }
980
793
 
981
- // `--ai` é superfície própria (D1). Combiná-la com outra superfície pediria
982
- // dois relatórios com dois códigos de saída no mesmo processo — o segundo
983
- // apagaria o primeiro em silêncio. Mesmo molde da guarda acima: recusa em
984
- // vez de fingir.
794
+ // `--ai` com outra superfície daria dois códigos de saída no mesmo processo: recusa.
985
795
  if (options.ai) {
986
796
  const conflicts = ['full', 'mcp', 'worktrees', 'procs'].filter((f) => options[f]);
987
797
  if (conflicts.length) {
@@ -994,7 +804,6 @@ export async function doctorCommand(options = {}) {
994
804
  return doctorAiCommand(process.cwd(), options);
995
805
  }
996
806
 
997
- // Reset mode: remove morph-managed settings entries
998
807
  if (options.reset) {
999
808
  const targetPath = process.cwd();
1000
809
  logger.header('MORPH-SPEC Settings Reset');
@@ -1019,27 +828,18 @@ export async function doctorCommand(options = {}) {
1019
828
  return;
1020
829
  }
1021
830
 
1022
- // full health check mode
1023
831
  if (options.full) {
1024
832
  const frameworkRoot = process.cwd();
1025
833
  return doctorFullCommand(frameworkRoot);
1026
834
  }
1027
835
 
1028
- // MCP health check mode
1029
836
  if (options.mcp) {
1030
837
  return doctorMcpCommand(process.cwd());
1031
838
  }
1032
839
 
1033
- // Worktree reconciliation mode (C2)
1034
840
  if (options.worktrees) {
1035
841
  const clean = await doctorWorktreesCommand(process.cwd(), options);
1036
- // The audit result has to reach the exit code, not just the terminal.
1037
- // Commander discards an action's return value, so this reported success to
1038
- // every script while printing "⚠ 5 inconsistência(s)" to the human — a
1039
- // `doctor --worktrees || echo falhou` in CI or a pre-commit hook saw a
1040
- // clean run with orphaned worktrees piling up. The flag-misuse branch
1041
- // above already treats the exit code as this command's signal channel;
1042
- // only the audit itself was left out of it.
842
+ // The audit result must reach the exit code: Commander discards an action's return value.
1043
843
  if (!clean) process.exitCode = 1;
1044
844
  return clean;
1045
845
  }
@@ -1067,11 +867,9 @@ export async function doctorCommand(options = {}) {
1067
867
  }
1068
868
  };
1069
869
 
1070
- // Check versions first
1071
870
  const cliCheck = await checkCLIOutdated();
1072
871
  const projectCheck = await checkProjectOutdated(targetPath);
1073
872
 
1074
- // CLI Version
1075
873
  if (cliCheck.latest) {
1076
874
  if (cliCheck.isOutdated) {
1077
875
  checks.push({
@@ -1094,7 +892,6 @@ export async function doctorCommand(options = {}) {
1094
892
  });
1095
893
  }
1096
894
 
1097
- // Project MORPH version
1098
895
  if (projectCheck.current) {
1099
896
  if (projectCheck.isOutdated) {
1100
897
  checks.push({
@@ -1118,7 +915,6 @@ export async function doctorCommand(options = {}) {
1118
915
  hasWarnings = true;
1119
916
  }
1120
917
 
1121
- // Check if morph-spec is in PATH
1122
918
  const inPath = isMorphSpecInPath();
1123
919
  if (inPath) {
1124
920
  checks.push({
@@ -1134,7 +930,6 @@ export async function doctorCommand(options = {}) {
1134
930
  hasWarnings = true;
1135
931
  }
1136
932
 
1137
- // Check CLAUDE.md has morph-spec @import
1138
933
  const claudeMd = join(targetPath, 'CLAUDE.md');
1139
934
  const claudeMdStatus = await checkClaudeMdImport(claudeMd);
1140
935
  if (claudeMdStatus === 'ok') {
@@ -1147,7 +942,6 @@ export async function doctorCommand(options = {}) {
1147
942
  hasErrors = true;
1148
943
  }
1149
944
 
1150
- // Check .morph folder
1151
945
  const morphPath = join(targetPath, '.morph');
1152
946
  if (await pathExists(morphPath)) {
1153
947
  checks.push({ name: '.morph/', status: 'ok' });
@@ -1156,7 +950,6 @@ export async function doctorCommand(options = {}) {
1156
950
  hasErrors = true;
1157
951
  }
1158
952
 
1159
- // Check config.json
1160
953
  const configPath = join(morphPath, 'config', 'config.json');
1161
954
  if (await pathExists(configPath)) {
1162
955
  try {
@@ -1175,7 +968,6 @@ export async function doctorCommand(options = {}) {
1175
968
  hasErrors = true;
1176
969
  }
1177
970
 
1178
- // Check agents.json
1179
971
  const agentsPath = join(morphPath, 'framework', 'agents.json');
1180
972
  if (await pathExists(agentsPath)) {
1181
973
  try {
@@ -1194,11 +986,9 @@ export async function doctorCommand(options = {}) {
1194
986
  hasErrors = true;
1195
987
  }
1196
988
 
1197
- // Check standards (either in project .morph/framework/standards/ or in framework package)
1198
989
  const standardsPath = join(morphPath, 'framework', 'standards');
1199
990
  const frameworkStdsRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'standards');
1200
991
  if (await pathExists(standardsPath)) {
1201
- // Check for VSA standard (core to the framework)
1202
992
  const vsaStd = await pathExists(join(standardsPath, 'architecture', 'vertical-slice', 'vertical-slice.md')) ||
1203
993
  await pathExists(join(standardsPath, 'vertical-slice.md'));
1204
994
  if (vsaStd) {
@@ -1208,7 +998,6 @@ export async function doctorCommand(options = {}) {
1208
998
  hasWarnings = true;
1209
999
  }
1210
1000
  } else if (await pathExists(frameworkStdsRoot)) {
1211
- // Framework package has standards (dev/global install mode)
1212
1001
  checks.push({ name: 'standards/', status: 'warn', msg: 'not installed in project (run morph-spec init)' });
1213
1002
  hasWarnings = true;
1214
1003
  } else {
@@ -1216,15 +1005,8 @@ export async function doctorCommand(options = {}) {
1216
1005
  hasErrors = true;
1217
1006
  }
1218
1007
 
1219
- // Check templates — as of the R3 perf plan (Task 2), setup-infra/update no
1220
- // longer pre-populate .morph/framework/templates/: template resolution is
1221
- // layered (template-registry.js / architecture-scanner.js resolveTemplatePath)
1222
- // and falls back to the npm package's own framework/templates/ when the
1223
- // project-local copy is absent, which is now the expected default rather
1224
- // than an error. The project-local copy remains a supported override — if
1225
- // present, report it as such. The "key templates" check further below
1226
- // verifies the package copy itself is intact, which is the check that
1227
- // actually matters now.
1008
+ // A missing project-local templates copy is the default (resolution falls back to the package's
1009
+ // framework/templates/); a local copy is a supported override.
1228
1010
  const templatesPath = join(morphPath, 'framework', 'templates');
1229
1011
  if (await pathExists(templatesPath)) {
1230
1012
  checks.push({ name: 'templates/', status: 'ok', msg: 'local override present' });
@@ -1232,7 +1014,6 @@ export async function doctorCommand(options = {}) {
1232
1014
  checks.push({ name: 'templates/', status: 'ok', msg: 'resolved from package (no local override)' });
1233
1015
  }
1234
1016
 
1235
- // Check .claude folder
1236
1017
  const claudePath = join(targetPath, '.claude');
1237
1018
  if (await pathExists(claudePath)) {
1238
1019
  const commandsPath = join(claudePath, 'commands');
@@ -1244,7 +1025,6 @@ export async function doctorCommand(options = {}) {
1244
1025
  if (hasCommands && hasSkills) {
1245
1026
  checks.push({ name: '.claude/', status: 'ok' });
1246
1027
 
1247
- // Check skills link status
1248
1028
  const skillEntries = await fs.readdir(skillsPath, { withFileTypes: true });
1249
1029
  const skillDirs = skillEntries.filter(e => e.isDirectory() || e.isSymbolicLink());
1250
1030
  let linkedCount = 0;
@@ -1253,7 +1033,7 @@ export async function doctorCommand(options = {}) {
1253
1033
  try {
1254
1034
  const stat = await fs.lstat(join(skillsPath, dir.name));
1255
1035
  if (stat.isSymbolicLink()) linkedCount++;
1256
- } catch { /* ignore */ }
1036
+ } catch {}
1257
1037
  }
1258
1038
 
1259
1039
  if (skillDirs.length > 0) {
@@ -1268,7 +1048,6 @@ export async function doctorCommand(options = {}) {
1268
1048
  }
1269
1049
  }
1270
1050
 
1271
- // Check .claude/rules/
1272
1051
  const rulesPath = join(claudePath, 'rules');
1273
1052
  if (await pathExists(rulesPath)) {
1274
1053
  const ruleFiles = await fs.readdir(rulesPath);
@@ -1279,23 +1058,18 @@ export async function doctorCommand(options = {}) {
1279
1058
  hasWarnings = true;
1280
1059
  }
1281
1060
 
1282
- // Check .claude/agents/
1283
1061
  const agentsPathClaude = join(claudePath, 'agents');
1284
1062
  if (await pathExists(agentsPathClaude)) {
1285
1063
  const agentFiles = await fs.readdir(agentsPathClaude);
1286
1064
  const morphAgents = agentFiles.filter(f => f.startsWith('morph-') && f.endsWith('.md'));
1287
1065
  checks.push({ name: '.claude/agents/', status: 'ok', msg: `${morphAgents.length} agents` });
1288
1066
 
1289
- // Check for missing agents based on stack tags. Empty tags (no config
1290
- // yet) is NOT a skip: personas install-all by default, so with an
1291
- // unknown stack ALL personas are expected (agentEligible semantics) —
1292
- // the exact field failure where only morph-evaluator landed and every
1293
- // domain dispatch fell back to general-purpose.
1067
+ // Empty tags (no config yet) is NOT a skip: personas install-all by default, so ALL are expected.
1294
1068
  try {
1295
1069
  let projectTags = [];
1296
1070
  try {
1297
1071
  projectTags = getProjectTags(await readJson(configPath));
1298
- } catch { /* no config → expected = all personas */ }
1072
+ } catch {}
1299
1073
  const tagsLabel = projectTags.length > 0 ? projectTags.join(', ') : 'all (stack unknown)';
1300
1074
  const frameworkDir = join(import.meta.dirname, '..', '..', '..', 'framework');
1301
1075
  const { missing } = checkMissingAgents(targetPath, frameworkDir, projectTags);
@@ -1310,12 +1084,11 @@ export async function doctorCommand(options = {}) {
1310
1084
  } else {
1311
1085
  checks.push({ name: 'agents completeness', status: 'ok', msg: `all agents for [${tagsLabel}]` });
1312
1086
  }
1313
- } catch { /* framework dir not resolvable — skip completeness check */ }
1087
+ } catch {}
1314
1088
  } else {
1315
1089
  checks.push({ name: '.claude/agents/', status: 'warn', msg: 'missing — run morph-spec update' });
1316
1090
  hasWarnings = true;
1317
1091
  }
1318
- // ── Hook health: verify hook scripts exist ───────────────────────────────
1319
1092
  const settingsLocalPath = join(claudePath, 'settings.local.json');
1320
1093
  if (await pathExists(settingsLocalPath)) {
1321
1094
  try {
@@ -1351,7 +1124,6 @@ export async function doctorCommand(options = {}) {
1351
1124
  hasWarnings = true;
1352
1125
  }
1353
1126
 
1354
- // ── Skills cliVersion: warn if installed skills reference older CLI ──────
1355
1127
  try {
1356
1128
  const cliVersion = getInstalledCLIVersion();
1357
1129
  if (cliVersion) {
@@ -1373,24 +1145,19 @@ export async function doctorCommand(options = {}) {
1373
1145
  hasWarnings = true;
1374
1146
  }
1375
1147
  }
1376
- } catch { /* non-fatal */ }
1148
+ } catch {}
1377
1149
 
1378
- // ── Claude Code Plugins ──────────────────────────────────────────────────
1379
1150
  console.log(chalk.cyan('\n Claude Code Plugins (required by morph-spec init)'));
1380
1151
  const pluginsFile = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
1381
1152
  let pluginData = { plugins: {} };
1382
1153
  try {
1383
1154
  pluginData = JSON.parse(await fs.readFile(pluginsFile, 'utf8'));
1384
- } catch { /* file may not exist */ }
1155
+ } catch {}
1385
1156
  const pluginStatus = checkInstalledPlugins(pluginData.plugins || {});
1386
1157
  check('context7 plugin', pluginStatus.context7, false,
1387
1158
  'Run: morph-spec install-plugin context7');
1388
1159
  } else {
1389
- // `.claude/` exists but lacks commands/ and/or skills/ — ranges from a
1390
- // partial install to the full-wipe incident this command exists to catch
1391
- // (folder survives, 0 items — doctor used to shrug this off as a vague
1392
- // warning with no fix). Count what IS still there: near-total loss is an
1393
- // ERROR with the actionable command, not a warning that reads as optional.
1160
+ // `.claude/` without commands/ or skills/: count what is left; near-total loss is an ERROR with the fix.
1394
1161
  const pieces = ['agents', 'rules', 'skills', 'commands'];
1395
1162
  const present = [];
1396
1163
  for (const piece of pieces) {
@@ -1420,7 +1187,6 @@ export async function doctorCommand(options = {}) {
1420
1187
  hasErrors = true;
1421
1188
  }
1422
1189
 
1423
- // Check state.json
1424
1190
  const statePath = join(targetPath, '.morph', 'state.json');
1425
1191
  if (await pathExists(statePath)) {
1426
1192
  try {
@@ -1441,10 +1207,7 @@ export async function doctorCommand(options = {}) {
1441
1207
  hasWarnings = true;
1442
1208
  }
1443
1209
 
1444
- // Feature-lifecycle consistency — the consolidated inconsistency report:
1445
- // gate-order violations (N approved with N-1 pending), identity-less
1446
- // features (no description), orphan folders and stale index entries. Warn
1447
- // only (advisory); the per-feature detail is printed after the summary.
1210
+ // Warn only (advisory); the per-feature detail is printed after the summary.
1448
1211
  const lifecycleIssues = auditFeatureLifecycle(targetPath);
1449
1212
  if (lifecycleIssues.length === 0) {
1450
1213
  checks.push({ name: 'feature lifecycle', status: 'ok' });
@@ -1457,13 +1220,8 @@ export async function doctorCommand(options = {}) {
1457
1220
  hasWarnings = true;
1458
1221
  }
1459
1222
 
1460
- // Pull requests — o pendurado (aberto há dias sem cópia local) e o mergeado
1461
- // que não pousou (archive não chegou nesta árvore). Offline-safe: sem `gh`,
1462
- // sem origin no GitHub, ou qualquer falha, `auditPullRequests` nunca lança
1463
- // (achado #7 — mesma guarda fail-open que `fleet-scanner.js` já usa para a
1464
- // síntese idêntica) e devolve `null` — achado #3: um índice desconhecido
1465
- // renderiza como "não verificado", nunca como `✓ ok`, porque `[]` e
1466
- // "não pude perguntar" não são a mesma coisa.
1223
+ // Offline-safe: `auditPullRequests` never throws and returns `null` when it cannot ask, which
1224
+ // renders as "não verificado", never `✓`.
1467
1225
  let prIssues;
1468
1226
  try {
1469
1227
  prIssues = auditPullRequests(targetPath);
@@ -1483,8 +1241,7 @@ export async function doctorCommand(options = {}) {
1483
1241
  hasWarnings = true;
1484
1242
  }
1485
1243
 
1486
- // Codebase graph — opcional (ADR-002). Nunca 'missing': um projeto que não
1487
- // usa o grafo tem de continuar com o doctor verde.
1244
+ // Opcional: nunca 'missing', um projeto que não usa o grafo segue com o doctor verde.
1488
1245
  const graphAudit = auditGraph(targetPath);
1489
1246
  if (graphAudit.status === 'warn') {
1490
1247
  checks.push({ name: 'codebase graph', status: 'warn', msg: graphAudit.msg });
@@ -1504,15 +1261,12 @@ export async function doctorCommand(options = {}) {
1504
1261
  checks.push(storeCheck);
1505
1262
  if (storeCheck.status === 'warn') hasWarnings = true;
1506
1263
  }
1507
- } catch { /* fail-open */ }
1264
+ } catch {}
1508
1265
 
1509
- // Check key templates exist (in framework package, not project copy)
1510
1266
  const frameworkTemplatesRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'templates');
1511
1267
  const keyTemplates = [
1512
- { name: 'proposal.md', path: join(frameworkTemplatesRoot, 'docs', 'proposal.md') },
1513
- { name: 'spec.md', path: join(frameworkTemplatesRoot, 'docs', 'spec.md') },
1514
- { name: 'tasks.template.json', path: join(frameworkTemplatesRoot, 'tasks.template.json') },
1515
- { name: 'decisions.md', path: join(frameworkTemplatesRoot, 'feature', 'decisions.md') }
1268
+ { name: 'plan.md', path: join(frameworkTemplatesRoot, 'docs', 'plan.md') },
1269
+ { name: 'ui-mockups.md', path: join(frameworkTemplatesRoot, 'docs', 'ui-mockups.md') }
1516
1270
  ];
1517
1271
  const missingTemplates = [];
1518
1272
  for (const template of keyTemplates) {
@@ -1531,7 +1285,6 @@ export async function doctorCommand(options = {}) {
1531
1285
  hasErrors = true;
1532
1286
  }
1533
1287
 
1534
- // Check Node.js version
1535
1288
  const nodeVersion = process.version;
1536
1289
  const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0], 10);
1537
1290
  if (majorVersion >= 18) {
@@ -1541,7 +1294,6 @@ export async function doctorCommand(options = {}) {
1541
1294
  hasWarnings = true;
1542
1295
  }
1543
1296
 
1544
- // Display results
1545
1297
  for (const check of checks) {
1546
1298
  if (check.status === 'ok') {
1547
1299
  console.log(chalk.green(` ✓ ${check.name}`));
@@ -1552,15 +1304,11 @@ export async function doctorCommand(options = {}) {
1552
1304
  } else if (check.status === 'error') {
1553
1305
  console.log(chalk.red(` ✗ ${check.name}`) + chalk.gray(` (${check.msg})`));
1554
1306
  } else if (check.status === 'skip') {
1555
- // "Não verificado" — nem ok nem falha. Nunca soma em hasWarnings/hasErrors:
1556
- // um `gh` indisponível não pode deixar o doctor mais barulhento nem mudar
1557
- // o exit code (achado #3).
1307
+ // "Não verificado" nunca soma em hasWarnings/hasErrors: um `gh` indisponível não muda o exit code.
1558
1308
  console.log(chalk.gray(` ○ ${check.name}`) + chalk.gray(check.msg ? ` (${check.msg})` : ''));
1559
1309
  }
1560
1310
  }
1561
1311
 
1562
- // Per-feature detail for the lifecycle inconsistencies (item: doctor lists
1563
- // what's already wrong in the repo, e.g. plan approved with proposal pending).
1564
1312
  if (lifecycleIssues.length) {
1565
1313
  logger.blank();
1566
1314
  console.log(chalk.yellow(' Feature lifecycle inconsistencies:'));
@@ -1570,8 +1318,6 @@ export async function doctorCommand(options = {}) {
1570
1318
  console.log(chalk.gray(" Fix: 'morph-spec approve' (order), '--description' on create, or 'morph-spec delete' for dead scaffolds."));
1571
1319
  }
1572
1320
 
1573
- // Per-feature detail for the PR inconsistencies (item: pendurado ou merge
1574
- // que não pousou).
1575
1321
  if (prIssues?.length) {
1576
1322
  logger.blank();
1577
1323
  console.log(chalk.yellow(' Pull requests pendentes:'));