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