@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
@@ -1,69 +1,24 @@
1
- /**
2
- * Agents Installer (v5)
3
- *
4
- * Installs morph-spec domain personas as native Claude Code subagents
5
- * in .claude/agents/morph-{id}.md.
6
- *
7
- * agents.json is the SINGLE SOURCE OF TRUTH (ADR-001, docs/specs/
8
- * dispatchable-personas/decisions.md). The .md files are DERIVED artifacts:
9
- * generated with a MANAGED header, regenerated on update/--reinstall, and
10
- * drift-checked by `morph-spec doctor` (checkAgentsDrift). Never hand-edit
11
- * them — that is exactly the v4 dual-source drift the v5 migration removed.
12
- *
13
- * Canonical naming: subagent_type = `morph-{id}` — the frontmatter `name`
14
- * matches the filename 1:1, and the skills dispatch that exact type.
15
- *
16
- * In v5 the harness has 8 personas (no tiers) across two axes — domain
17
- * (dotnet-senior, ef-modeler, maf-expert, nextjs-expert, ui-designer,
18
- * infra-engineer, verifier, reviewer, security) and stage (scout, read-only diagnosis before
19
- * implementation). Each persona is a subagent the Specialist (Claude in
20
- * implementer role) can dispatch. The 3 roles (Orchestrator/Specialist/
21
- * Evaluator) are assumed by Claude itself per MORPH.md, not as separate agents.
22
- */
23
-
24
1
  import { readFileSync, existsSync, readdirSync } from 'node:fs';
25
2
  import { join } from 'path';
26
3
  import { shouldInstall } from '../lib/stack-filter.js';
27
4
  import { realEffects } from '../lib/installers/effects.js';
28
5
 
29
- /** Default tool scope for domain personas (override per agent via agents.json `tools`). */
30
6
  export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
31
7
 
32
- /**
33
- * A persona that can change files: its effective tool scope grants Write or Edit.
34
- * Derived from the tools, so a persona made read-only stops being one.
35
- *
36
- * @param {object} agent - agents.json entry
37
- * @returns {boolean}
38
- */
8
+ /** Derived from the effective tool scope (Write or Edit), so a persona made read-only stops being one. */
39
9
  export function isImplementer(agent) {
40
10
  const tools = String(agent?.tools ?? DEFAULT_AGENT_TOOLS).split(',').map((t) => t.trim());
41
11
  return tools.includes('Write') || tools.includes('Edit');
42
12
  }
43
13
 
44
- /**
45
- * Turn ceiling for a persona that omits agents.json `maxTurns` (ADR-004).
46
- *
47
- * 20 is the value the generator hardcoded before the field existed — the v4
48
- * Tier-2 default (commit 93aad05f) that survived the v5 flattening with no
49
- * recorded rationale. Keeping it as the default is what lets an old
50
- * agents.json render byte-for-byte the same .md as before.
51
- */
14
+ // The value hardcoded before the field existed: an agents.json without `maxTurns` renders byte-for-byte as before.
52
15
  export const DEFAULT_MAX_TURNS = 20;
53
16
 
54
- /** Share of the ceiling (percent) at which a sub-agent checkpoints (partial report). */
55
17
  const CHECKPOINT_PERCENT = 70;
56
18
 
57
19
  /**
58
- * The ONE rule for a valid `maxTurns`. Four readers — installAgents,
59
- * checkAgentsDrift, doctor's validateAgentsJson and validate-framework.js
60
- * Check 6 — so the same agents.json can never pass in one and fail in another.
61
- *
62
- * Absent (`undefined`) is valid: it means "use the default". Anything present
63
- * must be an integer >= 1. `null` is NOT absent — whoever wants the default
64
- * removes the field.
65
- *
66
- * @param {unknown} value - the raw agents.json value
20
+ * The ONE validity rule, shared by installAgents, checkAgentsDrift, doctor and validate-framework.
21
+ * `undefined` means default; `null` is NOT absent.
67
22
  * @returns {string|null} what was expected, or null when valid
68
23
  */
69
24
  export function maxTurnsProblem(value) {
@@ -71,14 +26,6 @@ export function maxTurnsProblem(value) {
71
26
  return Number.isInteger(value) && value >= 1 ? null : 'esperado inteiro >= 1';
72
27
  }
73
28
 
74
- /**
75
- * The full message for an invalid `maxTurns` — persona, value received, what
76
- * is expected and how to get the default back. Every reader prints this one.
77
- *
78
- * @param {string} id - persona id (agents.json key)
79
- * @param {unknown} value - the raw agents.json value
80
- * @returns {string|null} null when valid
81
- */
82
29
  export function maxTurnsError(id, value) {
83
30
  const problem = maxTurnsProblem(value);
84
31
  if (!problem) return null;
@@ -89,41 +36,20 @@ export function maxTurnsError(id, value) {
89
36
  `Remova o campo para usar o default ${DEFAULT_MAX_TURNS}.`;
90
37
  }
91
38
 
92
- /**
93
- * Resolve the ceiling a persona is generated with: its `maxTurns`, or the
94
- * default when absent. Throws `maxTurnsError()` on an invalid value.
95
- *
96
- * @param {object} agent - agents.json entry with `id` merged in
97
- * @returns {number}
98
- */
99
39
  export function resolveMaxTurns(agent) {
100
40
  const error = maxTurnsError(agent.id, agent.maxTurns);
101
41
  if (error) throw new Error(error);
102
42
  return agent.maxTurns ?? DEFAULT_MAX_TURNS;
103
43
  }
104
44
 
105
- /** Values Claude Code accepts in a subagent's `effort` frontmatter. */
106
45
  export const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
107
46
 
108
- /**
109
- * The ONE rule for a valid `effort` — same four readers as `maxTurnsProblem`.
110
- * Absent means the frontmatter carries no `effort:` line and the sub-agent
111
- * inherits the session's effort, byte-for-byte what the generator emitted
112
- * before the field existed.
113
- *
114
- * @param {unknown} value - the raw agents.json value
115
- * @returns {string|null} what was expected, or null when valid
116
- */
47
+ // Absent: no `effort:` line, so the sub-agent inherits the session effort.
117
48
  export function effortProblem(value) {
118
49
  if (value === undefined) return null;
119
50
  return VALID_EFFORTS.includes(value) ? null : `esperado um de ${VALID_EFFORTS.join('|')}`;
120
51
  }
121
52
 
122
- /**
123
- * @param {string} id - persona id (agents.json key)
124
- * @param {unknown} value - the raw agents.json value
125
- * @returns {string|null} null when valid
126
- */
127
53
  export function effortError(id, value) {
128
54
  const problem = effortProblem(value);
129
55
  if (!problem) return null;
@@ -131,26 +57,12 @@ export function effortError(id, value) {
131
57
  'Remova o campo para herdar o effort da sessão.';
132
58
  }
133
59
 
134
- /** A surface id is the key the mockup checklist names (`Superfície: admin-app`). */
135
60
  const SURFACE_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
136
61
 
137
62
  const SURFACE_KEYS = new Set(['when', 'standards']);
138
63
 
139
- /**
140
- * The ONE rule for a valid `surfaces` — the optional map that splits a
141
- * persona's standards by the kind of screen it works on
142
- * (`{ "admin-app": { when, standards: [ids] } }`). Same four readers as
143
- * `maxTurnsProblem`, for the same reason.
144
- *
145
- * Absent (`undefined`) is valid: the persona reads only `standards`. A standard
146
- * listed both in `standards` (read on every surface) and in a surface is
147
- * refused: that is exactly how a "separated" persona keeps loading marketing
148
- * standards on a CRUD screen without anyone noticing.
149
- *
150
- * @param {unknown} surfaces - the raw agents.json value
151
- * @param {unknown} [standards] - the persona's universal `standards`
152
- * @returns {string|null} what is wrong, or null when valid
153
- */
64
+ // A standard both universal and in a surface is refused: the persona would keep
65
+ // loading it on every surface without anyone noticing.
154
66
  export function surfacesProblem(surfaces, standards = []) {
155
67
  if (surfaces === undefined) return null;
156
68
  if (surfaces === null || typeof surfaces !== 'object' || Array.isArray(surfaces)) {
@@ -185,14 +97,6 @@ export function surfacesProblem(surfaces, standards = []) {
185
97
  return null;
186
98
  }
187
99
 
188
- /**
189
- * The full message for an invalid `surfaces`. Every reader prints this one.
190
- *
191
- * @param {string} id - persona id (agents.json key)
192
- * @param {unknown} surfaces
193
- * @param {unknown} [standards]
194
- * @returns {string|null} null when valid
195
- */
196
100
  export function surfacesError(id, surfaces, standards) {
197
101
  const problem = surfacesProblem(surfaces, standards);
198
102
  if (!problem) return null;
@@ -200,14 +104,7 @@ export function surfacesError(id, surfaces, standards) {
200
104
  'Remova o campo para a persona ler só `standards`.';
201
105
  }
202
106
 
203
- /**
204
- * Every standard id a persona can cite: the universal list plus each
205
- * surface's. Reference checks read this, so a surface id that does not exist
206
- * fails like any other.
207
- *
208
- * @param {object} persona - agents.json entry
209
- * @returns {string[]} unique ids, universal first
210
- */
107
+ /** @returns {string[]} unique ids of `standards` plus every surface's, universal first */
211
108
  export function personaStandardIds(persona) {
212
109
  const ids = [...(Array.isArray(persona?.standards) ? persona.standards : [])];
213
110
  const surfaces = persona?.surfaces;
@@ -219,48 +116,22 @@ export function personaStandardIds(persona) {
219
116
  return [...new Set(ids)];
220
117
  }
221
118
 
222
- /** The first validity error of a persona entry, or null — what drift reports as `invalid`. */
223
119
  function personaError(agent) {
224
120
  return maxTurnsError(agent.id, agent.maxTurns)
225
121
  ?? surfacesError(agent.id, agent.surfaces, agent.standards)
226
122
  ?? effortError(agent.id, agent.effort);
227
123
  }
228
124
 
229
- /**
230
- * Tools every persona gets on top of its scope. A `tools:` line is a strict
231
- * allowlist: without `Skill` the sub-agent cannot invoke a single skill, and
232
- * without `ToolSearch` it cannot load an MCP tool the session defers.
233
- */
125
+ // `tools:` is a strict allowlist: without Skill no skill runs, without ToolSearch no deferred MCP tool loads.
234
126
  export const PLATFORM_TOOLS = ['Skill', 'ToolSearch'];
235
127
 
236
- /**
237
- * The server names an MCP registry entry is granted under. Claude Code names a
238
- * server's tools after WHERE it is configured — `mcp__context7__*` from
239
- * `.mcp.json`, `mcp__plugin_context7_context7__*` from the plugin — and a grant
240
- * only matches its own spelling (measured 2026-09-24: `mcp__context7` gave a
241
- * plugin-installed context7 nothing). So every spelling becomes a grant; one
242
- * the project does not configure is inert.
243
- *
244
- * @param {string} id - registry key
245
- * @param {object} entry - registry entry
246
- * @returns {string[]}
247
- */
128
+ // A grant matches only its own spelling (`mcp__context7` vs `mcp__plugin_context7_context7`),
129
+ // so every spelling becomes a grant; one the project does not configure is inert.
248
130
  export function mcpServerNames(id, entry) {
249
- // A malformed list falls back to the id instead of breaking the render:
250
- // installAgents and checkAgentsDrift both go through here (fail-open).
251
131
  return serverNamesProblem(id, entry?.serverNames) === null ? entry?.serverNames ?? [id] : [id];
252
132
  }
253
133
 
254
- /**
255
- * The ONE rule for a registry entry's `serverNames` (validate-framework Check 8
256
- * reads it). Absent is valid (`[id]`). Present: non-empty, only the characters
257
- * Claude Code keeps in a tool name, and it must include the id itself — that is
258
- * the name the MCP installer writes into `.mcp.json`.
259
- *
260
- * @param {string} id - registry key
261
- * @param {unknown} value - the raw `serverNames`
262
- * @returns {string|null} what was expected, or null when valid
263
- */
134
+ // Must include the id itself: that is the name the MCP installer writes into `.mcp.json`.
264
135
  export function serverNamesProblem(id, value) {
265
136
  if (value === undefined) return null;
266
137
  if (!Array.isArray(value) || value.length === 0) return 'esperado array não vazio';
@@ -269,15 +140,7 @@ export function serverNamesProblem(id, value) {
269
140
  return value.includes(id) ? null : `precisa incluir o próprio id "${id}"`;
270
141
  }
271
142
 
272
- /**
273
- * Load the MCP servers known to the framework registry
274
- * (framework/mcp/registry.json) with the server names each is granted under.
275
- * Only registry-known ids in agents.json `mcp` become tool grants (fail-open
276
- * on any error).
277
- *
278
- * @param {string} frameworkDir
279
- * @returns {Map<string, string[]>|null} id → server names; null = no registry found (accept nothing)
280
- */
143
+ /** @returns {Map<string, string[]>|null} id → server names; null = no registry (grant nothing) */
281
144
  export function loadKnownMcpIds(frameworkDir) {
282
145
  try {
283
146
  const registryPath = join(frameworkDir, 'mcp', 'registry.json');
@@ -289,21 +152,8 @@ export function loadKnownMcpIds(frameworkDir) {
289
152
  }
290
153
  }
291
154
 
292
- /**
293
- * Personas install-all default (friction fix, MORPH_GHLBrain 2026-07-11):
294
- * with NO known stack tags (greenfield / install before config.json exists)
295
- * EVERY persona is generated — a project that dispatches `morph-{id}` must
296
- * find the subagent registered, and an install that only lands
297
- * morph-verifier forces every domain dispatch into the general-purpose
298
- * fallback. Stack filtering only applies when the project's stack is KNOWN
299
- * (tags non-empty): the fallback stays reserved for stacks the project
300
- * genuinely does not have. Note this deliberately differs from rules/skills
301
- * filtering (stack-filter.js), which stays minimal on greenfield.
302
- *
303
- * @param {object} agent - agents.json entry (with `stack`)
304
- * @param {string[]} projectTags
305
- * @returns {boolean}
306
- */
155
+ // With no known stack tags every persona is generated: a dispatch of `morph-{id}` must find
156
+ // its subagent. Deliberately differs from rules/skills filtering.
307
157
  export function agentEligible(agent, projectTags = []) {
308
158
  if (projectTags.length === 0) return true;
309
159
  return shouldInstall(agent.stack ?? ['*'], projectTags);
@@ -314,17 +164,6 @@ const MANAGED_HEADER =
314
164
  ' Do NOT edit by hand: changes are overwritten on `morph-spec update` / `setup-infra --reinstall`.\n' +
315
165
  ' To change this persona, edit agents.json and regenerate. Drift is flagged by `morph-spec doctor`. -->';
316
166
 
317
- /**
318
- * Install morph-spec domain personas as native Claude Code subagents.
319
- *
320
- * @param {string} projectDir - Target project directory
321
- * @param {string} frameworkDir - Path to morph-spec framework/ directory
322
- * @param {object} options
323
- * @param {string[]} [options.projectTags=[]] - Stack tags from config (e.g. ['dotnet', 'nextjs'])
324
- * @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [options.fx]
325
- * Coletor de efeitos; o default executa e registra numa lista descartada.
326
- * @returns {Promise<{ installed: number }>}
327
- */
328
167
  export async function installAgents(projectDir, frameworkDir = 'framework', options = {}) {
329
168
  const { projectTags = [], fx = realEffects() } = options;
330
169
 
@@ -336,8 +175,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
336
175
  .filter(([key]) => !key.startsWith('_comment'))
337
176
  .map(([id, data]) => ({ id, ...data }));
338
177
 
339
- // Validate EVERY persona before the first write: refusing halfway through
340
- // would leave some personas regenerated and others stale.
178
+ // Validate every persona before the first write: refusing halfway leaves a mix of stale and fresh.
341
179
  for (const agent of agents) {
342
180
  const error = personaError(agent);
343
181
  if (error) throw new Error(error);
@@ -348,10 +186,8 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
348
186
 
349
187
  const knownMcpIds = loadKnownMcpIds(frameworkDir);
350
188
 
351
- // Filter by stack — install-all when the stack is unknown (empty tags).
352
189
  const eligible = agents.filter(a => agentEligible(a, projectTags));
353
190
 
354
- // Build set of all possible filenames for orphan cleanup
355
191
  const allPossible = new Set(agents.map(a => `morph-${a.id}.md`));
356
192
 
357
193
  let installed = 0;
@@ -373,12 +209,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
373
209
  installed++;
374
210
  }
375
211
 
376
- // Cleanup orphan morph-* files filtered out by stack.
377
- //
378
- // Calado em `dryRun` pelo mesmo motivo do laço gêmeo em skills-installer.js
379
- // (D6): numa execução real do `update` este diretório acabou de ser apagado
380
- // inteiro por `cleanFrameworkDirs()`, então o laço nunca remove nada. Num
381
- // dry ingênuo ele veria o diretório antigo e preveria remoções inexistentes.
212
+ // Silent in dryRun: the real update's cleanFrameworkDirs() already emptied the dir.
382
213
  if (!fx.dryRun) {
383
214
  for (const file of readdirSync(targetDir)) {
384
215
  if (allPossible.has(file) && !installedFilenames.has(file)) {
@@ -390,20 +221,10 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
390
221
  return { installed, agents: installedAgents };
391
222
  }
392
223
 
393
- /**
394
- * Render the full .md content for a persona — deterministic, so the same
395
- * function serves the installer AND the doctor drift check (a hand-edited or
396
- * stale file differs byte-for-byte from this output).
397
- *
398
- * @param {object} agent - agents.json entry with `id` merged in
399
- * @param {Map<string, string[]>|null} [knownMcpIds] - registry-known MCP ids and
400
- * their server names; agents.json `mcp` entries outside it are ignored (null = ignore all)
401
- * @returns {string} complete file content
402
- */
224
+ // Deterministic: the doctor drift check compares files byte-for-byte against this output.
403
225
  export function renderAgentMd(agent, knownMcpIds = null) {
404
226
  const description = buildDescription(agent);
405
- // Resolved ONCE: the frontmatter ceiling and the budget the body announces
406
- // must be the same number — the sub-agent never sees its own frontmatter.
227
+ // One value for the frontmatter and the body: the sub-agent never sees its own frontmatter.
407
228
  const maxTurns = resolveMaxTurns(agent);
408
229
  const surfaces = surfacesError(agent.id, agent.surfaces, agent.standards);
409
230
  if (surfaces) throw new Error(surfaces);
@@ -417,9 +238,6 @@ export function renderAgentMd(agent, knownMcpIds = null) {
417
238
  function buildToolsLine(agent, knownMcpIds) {
418
239
  const base = String(agent.tools ?? DEFAULT_AGENT_TOOLS).split(',').map(t => t.trim()).filter(Boolean);
419
240
  const platform = PLATFORM_TOOLS.filter(t => !base.includes(t));
420
- // Domain-relevant MCP servers (agents.json `mcp: [ids]`) become server-level
421
- // grants, one per server name (`mcp__{name}`). Only registry-known ids are
422
- // emitted; in projects without that server configured the grant is inert.
423
241
  const mcpGrants = (agent.mcp ?? [])
424
242
  .filter(id => knownMcpIds?.has(id))
425
243
  .flatMap(id => knownMcpIds.get(id).map(name => `mcp__${name}`));
@@ -428,16 +246,11 @@ function buildToolsLine(agent, knownMcpIds) {
428
246
 
429
247
  function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
430
248
  const lines = [
431
- // Canonical 1:1 naming: frontmatter name === filename === subagent_type.
432
249
  `name: morph-${agent.id}`,
433
250
  `description: ${description}`,
434
- // The verifier is sonnet here; the dispatch routes it to opus when the risk
435
- // detector flags the diff. A persona without the field inherits the session model.
436
251
  `model: ${agent.model ?? 'inherit'}`,
437
252
  `tools: ${buildToolsLine(agent, knownMcpIds)}`,
438
- // Per persona in agents.json (ADR-004); absent → DEFAULT_MAX_TURNS. The
439
- // line keeps its position so an agents.json without the field renders
440
- // exactly what the hardcoded `maxTurns: 20` rendered.
253
+ // Position fixed so an agents.json without the field renders byte-for-byte as before.
441
254
  `maxTurns: ${maxTurns}`,
442
255
  ];
443
256
  if (agent.effort) lines.push(`effort: ${agent.effort}`);
@@ -453,12 +266,6 @@ function buildDescription(agent) {
453
266
  return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
454
267
  }
455
268
 
456
- /**
457
- * The fixed half of every implementer dispatch. The orchestrator paraphrases
458
- * a prompt template in the field; the generated body is the only text a
459
- * dispatch is guaranteed to carry, so the contract lives here and the
460
- * dispatch carries only what changes per slice.
461
- */
462
269
  const NEON_CAUTION = 'NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável ' +
463
270
  '(prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — ' +
464
271
  'nunca chame tool destrutiva sem confirmação humana explícita.';
@@ -476,7 +283,7 @@ export const DISPATCH_CONTRACT = [
476
283
  'ANTES de encerrar — é o retorno autoritativo, não a sua mensagem final. `SendMessage` não é substituto do relatório e não deve ser chamado.',
477
284
  '2. ESTADO: NÃO toque em `plan.md`, `checks.json`, `feature.json`, `state.json` nem no board de sessão — todo estado é gravado exclusivamente pelo orquestrador.',
478
285
  '3. TESTE: a prova é do orquestrador (`morph-spec verify {feature}`, depois do seu retorno), não sua. Escreva os testes pelo orçamento: ' +
479
- 'um por pós-condição, um de regressão por bug, variantes numa tabela (`[Theory]`/`it.each`). NÃO rode teste a cada marco nem faça ciclo red→green: ' +
286
+ 'um por Cn `prova: teste`, variantes em tabela (`[Theory]`/`it.each`); `leitura` sem teste. NÃO rode teste a cada marco nem faça ciclo red→green: ' +
480
287
  'escreva código e testes, compile, e rode SÓ o filtro dos seus testes UMA vez, em primeiro plano, antes do relatório; se falhar, corrija e rode só os que falharam. ' +
481
288
  'Grave a saída em arquivo (`… > .morph/logs/test-last.log 2>&1`, sem nada depois) e leia o arquivo — um pipe troca o exit do teste pelo do último comando. ' +
482
289
  'Em batch paralelo SEM isolamento, só compile. Com `isolation: "worktree"`, o mesmo vale no seu worktree, e o último ato é o commit na branch dele.',
@@ -518,7 +325,7 @@ function buildBody(agent, maxTurns) {
518
325
  '',
519
326
  ];
520
327
 
521
- // The 5.3.0 shape (no effort, no surfaces, implementer) renders byte-for-byte as before.
328
+ // The shape without effort or surfaces renders byte-for-byte as the old fixture.
522
329
  const legacyShape = !agent.effort && !agent.surfaces && isImplementer(agent);
523
330
 
524
331
  if (legacyShape && agent.domains?.length) {
@@ -560,7 +367,6 @@ function buildBody(agent, maxTurns) {
560
367
  '',
561
368
  );
562
369
  } else {
563
- // A read-only persona has no slice to implement nor scenario to report.
564
370
  lines.push(
565
371
  '## Como você opera',
566
372
  '',
@@ -571,22 +377,17 @@ function buildBody(agent, maxTurns) {
571
377
  }
572
378
 
573
379
  lines.push(
574
- // `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps an
575
- // review partial from carrying a Verdict the gate would read as final.
380
+ // `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps a partial
381
+ // review from carrying a Verdict the gate would read as final.
576
382
  '## Turn budget',
577
383
  '',
578
- // Pasted into EVERY dispatch of every persona — kept to the minimum that
579
- // still carries the contract (test/framework/maf-expert-persona.test.js
580
- // pins the rendered size).
581
384
  `You have ${maxTurns} turns (\`maxTurns\`); at the ceiling you are cut off mid-work, without warning. ` +
582
385
  'One response is one turn, however many parallel tool calls it makes. ' +
583
386
  `By turn ${checkpointTurn(maxTurns)}, if your dispatch names a report file, save a partial there — ` +
584
387
  '`PARCIAL` on line 1, done, remaining, files, no verdict (final report only) — then continue or stop, your call; the final report replaces it.',
585
388
  );
586
389
 
587
- // The dispatch template is paraphrased by the orchestrator in the field; this
588
- // body is the only text every implementer dispatch is guaranteed to carry.
589
- // Gated on `effort` so a persona without it renders byte-for-byte as before.
390
+ // The only text every implementer dispatch is guaranteed to carry.
590
391
  if (agent.effort && isImplementer(agent)) {
591
392
  lines.push(
592
393
  '',
@@ -603,30 +404,15 @@ function buildBody(agent, maxTurns) {
603
404
  return lines.join('\n');
604
405
  }
605
406
 
606
- /** The turn at which a sub-agent with `maxTurns` saves its partial report. */
607
407
  function checkpointTurn(maxTurns) {
608
- // Integer arithmetic: 0.7 × N in floating point is not always exact.
408
+ // Integer arithmetic: 0.7 × N in floating point is not exact.
609
409
  return Math.max(1, Math.floor((maxTurns * CHECKPOINT_PERCENT) / 100));
610
410
  }
611
411
 
612
- /**
613
- * Compatibility shim for older callers that expected installDomainAgents.
614
- * In v5 there is no separate "domain" install step — installAgents handles
615
- * everything from agents.json.
616
- */
617
412
  export async function installDomainAgents(projectDir, frameworkDir, options) {
618
- // No-op: domain content is now defined inline in agents.json (persona field).
619
413
  return { specialists: 0 };
620
414
  }
621
415
 
622
- /**
623
- * Check which agents SHOULD be installed but are NOT.
624
- *
625
- * @param {string} projectDir
626
- * @param {string} frameworkDir
627
- * @param {string[]} projectTags
628
- * @returns {{ missing: Array<{ name: string, stacks: string[] }>, installed: number, expected: number }}
629
- */
630
416
  export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
631
417
  const targetDir = join(projectDir, '.claude', 'agents');
632
418
  const installedFiles = new Set();
@@ -661,26 +447,8 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
661
447
  }
662
448
 
663
449
  /**
664
- * Drift check between agents.json (source of truth) and the generated
665
- * .claude/agents/morph-*.md files. Consumed by `morph-spec doctor`.
666
- *
667
- * - missing: eligible persona with no .md installed → run setup-infra --reinstall
668
- * - stale: .md exists but differs from renderAgentMd() output (hand-edited
669
- * or generated by an older CLI) → regenerate. `staleFields` names
670
- * what differs per file: the frontmatter keys, and `body`.
671
- * - invalid: persona whose agents.json entry cannot be rendered (an invalid
672
- * `maxTurns` or `surfaces`) — reported with the reason instead of throwing, so
673
- * the doctor prints the cause and not "failed to compute"
674
- * - orphans: morph-*.md files with no matching agents.json entry
675
- *
676
- * @param {string} projectDir
677
- * @param {string} frameworkDir - directory containing agents.json
678
- * @param {string[]} projectTags
679
- * @param {object} [options]
680
- * @param {string} [options.mcpRegistryDir=frameworkDir] - where `mcp/registry.json`
681
- * lives. It must be the directory the INSTALLER read: an installed project's
682
- * `.morph/framework/` carries agents.json but no `mcp/`, so rendering against
683
- * it drops every `mcp__*` grant and flags a pristine install as stale.
450
+ * `mcpRegistryDir` must be where the INSTALLER read the registry: the installed
451
+ * `.morph/framework/` has no `mcp/`, and rendering against it flags a clean install as stale.
684
452
  * @returns {{ missing: string[], stale: string[], staleFields: Object<string, string[]>,
685
453
  * invalid: Array<{ file: string, reason: string }>, orphans: string[], expected: number }}
686
454
  */
@@ -739,10 +507,6 @@ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = [], opt
739
507
  return { missing, stale, staleFields, invalid, orphans, expected };
740
508
  }
741
509
 
742
- /**
743
- * Split a generated .md into its frontmatter map and the rest.
744
- * @returns {{ fields: Map<string, string>, body: string }}
745
- */
746
510
  function splitAgentMd(content) {
747
511
  const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
748
512
  if (!match) return { fields: new Map(), body: content };
@@ -754,11 +518,6 @@ function splitAgentMd(content) {
754
518
  return { fields, body: match[2] };
755
519
  }
756
520
 
757
- /**
758
- * Name what differs between a .md on disk and its render: the frontmatter
759
- * keys whose value (or presence) differs, in render order, then `body`.
760
- * A file with no parseable frontmatter differs in everything → `frontmatter`.
761
- */
762
521
  function diffAgentMd(onDisk, rendered) {
763
522
  const disk = splitAgentMd(onDisk);
764
523
  const want = splitAgentMd(rendered);
@@ -766,16 +525,10 @@ function diffAgentMd(onDisk, rendered) {
766
525
  const keys = [...new Set([...want.fields.keys(), ...disk.fields.keys()])];
767
526
  const fields = keys.filter(k => disk.fields.get(k) !== want.fields.get(k));
768
527
  if (disk.body !== want.body) fields.push('body');
769
- // Bytes differ but no key/value does (e.g. a stray blank line between the
770
- // fences): still drift, and it lives in the frontmatter.
528
+ // Bytes differ but no key/value does (a stray blank line between the fences).
771
529
  return fields.length > 0 ? fields : ['frontmatter'];
772
530
  }
773
531
 
774
- /**
775
- * Compatibility export — kept so existing tests/CLI code that imports
776
- * formatStandardRef or parseSkillFile / collectSkillFiles still work,
777
- * but they are no-ops (will be removed in v5.1).
778
- */
779
532
  export function formatStandardRef(standard) {
780
533
  if (typeof standard === 'string') return `- ${standard}`;
781
534
  const id = standard.id || String(standard);
@@ -1,4 +1,3 @@
1
- // src/utils/banner.js
2
1
  import chalk from "chalk";
3
2
  import { type as osType, release as osRelease, homedir } from "os";
4
3
  import { join } from "path";
@@ -7,7 +6,6 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
7
6
  const BRAND = chalk.hex("#7C3AED");
8
7
  const DIM = chalk.dim;
9
8
 
10
- /** Purple gradient, one tone per logo line (top → bottom). */
11
9
  const GRADIENT = [
12
10
  "#6D28D9",
13
11
  "#7C3AED",
@@ -26,16 +24,8 @@ const LOGO_LINES = [
26
24
  "╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝",
27
25
  ];
28
26
 
29
- const LOGO_WIDTH = LOGO_LINES[0].length; // 44
27
+ const LOGO_WIDTH = LOGO_LINES[0].length;
30
28
 
31
- /**
32
- * The morph-spec logo: gradient "MORPH" block art with a S P E C + version
33
- * rule underneath. Single source for the CLI help banner and the install
34
- * banner — one drawing, everywhere.
35
- *
36
- * @param {string} version - e.g. "7.7.2"
37
- * @returns {string}
38
- */
39
29
  export function renderLogo(version) {
40
30
  const art = LOGO_LINES.map(
41
31
  (line, i) => " " + chalk.hex(GRADIENT[i])(line),
@@ -56,19 +46,8 @@ export function renderLogo(version) {
56
46
  return `\n${art}\n${rule}\n`;
57
47
  }
58
48
 
59
- /**
60
- * Print the install banner once per version change, on the first CLI run
61
- * after an install/upgrade. npm ≥7 swallows lifecycle-script output, so the
62
- * postinstall banner is invisible on a normal `npm i -g` — this is the
63
- * fallback that guarantees the next-steps guidance is actually seen.
64
- *
65
- * Marker: ~/.claude/morph-spec-banner-version (last version shown).
66
- * Skips silently when stdout is not a TTY (piped/parsed output) or when
67
- * MORPH_QUIET=1. Fail-open: any fs error just skips the banner.
68
- *
69
- * @param {{version: string, nodeVersion: string, markerDir?: string}} opts
70
- * @returns {boolean} true if the banner was printed
71
- */
49
+ // npm >=7 swallows postinstall output, so the first CLI run after an upgrade
50
+ // shows the banner instead. Skips without a TTY or with MORPH_QUIET=1.
72
51
  export function maybePrintFirstRunBanner({ version, nodeVersion, markerDir }) {
73
52
  try {
74
53
  if (!process.stdout.isTTY) return false;
@@ -117,7 +96,7 @@ export function printInstallBanner({ version, nodeVersion, results = [] }) {
117
96
  sep("Next · new project"),
118
97
  ` ${chalk.cyan("1.")} ${DIM("cd")} ${chalk.white("your-project")} ${DIM("(git repo)")}`,
119
98
  ` ${chalk.cyan("2.")} Run ${chalk.bold.white("morph-spec init")}`,
120
- ` ${chalk.cyan("3.")} Open it in Claude Code and run ${chalk.bold.white("/morph-proposal <feature>")}`,
99
+ ` ${chalk.cyan("3.")} Open it in Claude Code and run ${chalk.bold.white("/morph-plan <feature>")}`,
121
100
  "",
122
101
  sep("Next · project already on morph-spec"),
123
102
  ` ${chalk.cyan("1.")} Run ${chalk.bold.white("morph-spec update")} ${DIM("— after a CLI upgrade or a project.stack change: full stack-aware resync")}`,