@polymorphism-tech/morph-spec 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +4 -62
  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 +1 -1
  9. package/framework/MORPH.md +1 -1
  10. package/framework/agents.json +12 -4
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +6 -17
  14. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  15. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  16. package/framework/hooks/claude-code/core/chain.js +4 -65
  17. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  18. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  19. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  20. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  21. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  22. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  23. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  24. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  25. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  26. package/framework/hooks/claude-code/core/state-sync.js +11 -67
  27. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  29. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  30. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  31. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  32. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  33. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  34. package/framework/hooks/shared/activity-logger.js +9 -89
  35. package/framework/hooks/shared/agent-reap.js +2 -17
  36. package/framework/hooks/shared/build-check.js +6 -75
  37. package/framework/hooks/shared/claim-registry.js +9 -119
  38. package/framework/hooks/shared/command-tokens.js +6 -51
  39. package/framework/hooks/shared/comment-rules.js +5 -20
  40. package/framework/hooks/shared/comment-scanner.js +6 -25
  41. package/framework/hooks/shared/comment-stripper.js +11 -85
  42. package/framework/hooks/shared/e2e-check.js +0 -0
  43. package/framework/hooks/shared/errata.js +2 -15
  44. package/framework/hooks/shared/feature-locator.js +11 -95
  45. package/framework/hooks/shared/frontend-locations.js +1 -3
  46. package/framework/hooks/shared/gate-question.js +1 -7
  47. package/framework/hooks/shared/git-exclude.js +3 -20
  48. package/framework/hooks/shared/git-paths.js +8 -104
  49. package/framework/hooks/shared/graph-freshness.js +22 -166
  50. package/framework/hooks/shared/graph-refresh.js +21 -149
  51. package/framework/hooks/shared/heavy-command.js +4 -12
  52. package/framework/hooks/shared/hook-response.js +8 -83
  53. package/framework/hooks/shared/morph-cli.js +8 -52
  54. package/framework/hooks/shared/payload-utils.js +10 -132
  55. package/framework/hooks/shared/phase-utils.js +1 -42
  56. package/framework/hooks/shared/redact-secrets.js +4 -15
  57. package/framework/hooks/shared/reparse-scan.js +6 -66
  58. package/framework/hooks/shared/slice-progress.js +4 -23
  59. package/framework/hooks/shared/source-files.js +4 -17
  60. package/framework/hooks/shared/state-reader.js +13 -183
  61. package/framework/hooks/shared/state-writer.js +8 -52
  62. package/framework/hooks/shared/stdin-reader.js +1 -11
  63. package/framework/hooks/shared/telemetry-logger.js +7 -67
  64. package/framework/hooks/shared/transcript-usage.js +7 -29
  65. package/framework/hooks/shared/worktree-root.js +7 -29
  66. package/framework/rules/csharp-standards.md +1 -1
  67. package/framework/rules/frontend-standards.md +1 -0
  68. package/framework/rules/hangfire-standards.md +1 -1
  69. package/framework/rules/neon-standards.md +1 -1
  70. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  71. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  72. package/framework/skills/core/morph-apply/SKILL.md +2 -2
  73. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  74. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  75. package/framework/skills/core/morph-media/SKILL.md +1 -1
  76. package/framework/skills/core/morph-plan/SKILL.md +2 -2
  77. package/framework/skills/core/morph-review/SKILL.md +1 -1
  78. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  79. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  80. package/framework/standards/STANDARDS.json +34 -12
  81. package/framework/standards/STANDARDS.meta.json +16 -2
  82. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  83. package/framework/standards/ai-agents/agent-session.md +1 -1
  84. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  85. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  86. package/framework/standards/ai-agents/guardrails.md +1 -1
  87. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  88. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  89. package/framework/standards/ai-agents/production.md +1 -1
  90. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  91. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  92. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  93. package/framework/standards/ai-agents/setup.md +2 -2
  94. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  95. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  96. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  97. package/framework/standards/ai-agents/workflows.md +1 -1
  98. package/framework/standards/architecture/code-comments.md +1 -1
  99. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  100. package/framework/standards/backend/api/validation.md +1 -1
  101. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  102. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  103. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  104. package/framework/standards/data/nosql/cache/redis.md +1 -1
  105. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  106. package/framework/standards/frontend/admin-app/forms.md +1 -1
  107. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  108. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  109. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  110. package/framework/templates/CLAUDE.md.core.template +3 -3
  111. package/framework/templates/REGISTRY.json +9 -9
  112. package/framework/templates/docs/ui-mockups.md +4 -0
  113. package/package.json +1 -1
  114. package/src/commands/context/context-refresh.js +0 -16
  115. package/src/commands/dashboard/dashboard.js +2 -8
  116. package/src/commands/e2e/e2e.js +17 -104
  117. package/src/commands/graph/affected.js +0 -14
  118. package/src/commands/graph/build.js +11 -50
  119. package/src/commands/graph/explain.js +0 -16
  120. package/src/commands/graph/status.js +1 -12
  121. package/src/commands/mcp/mcp-setup.js +4 -62
  122. package/src/commands/observability/cost.js +5 -57
  123. package/src/commands/observability/telemetry.js +1 -14
  124. package/src/commands/project/branch.js +4 -45
  125. package/src/commands/project/claim.js +4 -48
  126. package/src/commands/project/doctor.js +48 -302
  127. package/src/commands/project/env.js +1 -27
  128. package/src/commands/project/fleet.js +3 -32
  129. package/src/commands/project/init.js +4 -68
  130. package/src/commands/project/install-plugin-cmd.js +0 -1
  131. package/src/commands/project/standards.js +1 -62
  132. package/src/commands/project/status.js +0 -53
  133. package/src/commands/project/update.js +31 -239
  134. package/src/commands/project/worktree.js +108 -882
  135. package/src/commands/scaffold/scaffold.js +0 -25
  136. package/src/commands/state/advance.js +14 -85
  137. package/src/commands/state/approve.js +14 -96
  138. package/src/commands/state/archive.js +12 -64
  139. package/src/commands/state/auto-approval.js +2 -5
  140. package/src/commands/state/create.js +22 -109
  141. package/src/commands/state/delete.js +11 -63
  142. package/src/commands/state/finish.js +98 -475
  143. package/src/commands/state/gate-check.js +22 -132
  144. package/src/commands/state/gate-decision.js +3 -39
  145. package/src/commands/state/handoff.js +3 -30
  146. package/src/commands/state/list.js +14 -84
  147. package/src/commands/state/report-check.js +8 -58
  148. package/src/commands/state/retype.js +4 -26
  149. package/src/commands/validation/index.js +0 -3
  150. package/src/commands/validation/mutate.js +2 -5
  151. package/src/commands/validation/validate-feature.js +5 -94
  152. package/src/commands/validation/verify.js +15 -98
  153. package/src/core/paths/output-schema.js +2 -101
  154. package/src/core/state/feature-identity.js +1 -25
  155. package/src/core/state/gate-order.js +0 -27
  156. package/src/core/state/lifecycle-audit.js +8 -100
  157. package/src/core/state/state-manager.js +22 -304
  158. package/src/core/templates/template-registry.js +2 -97
  159. package/src/core/templates/template-renderer.js +13 -93
  160. package/src/core/templates/template-seed.js +0 -1
  161. package/src/core/work-types.js +1 -36
  162. package/src/dashboard/app/App.jsx +1 -8
  163. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  164. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  165. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  166. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  167. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  168. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  169. package/src/dashboard/app/components/Markdown.jsx +0 -9
  170. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  171. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  172. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  173. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  174. package/src/dashboard/app/hooks/usePref.js +1 -5
  175. package/src/dashboard/app/lib/format.js +0 -2
  176. package/src/dashboard/app/lib/phases.js +1 -5
  177. package/src/dashboard/app/lib/project.js +0 -12
  178. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  179. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  180. package/src/dashboard/app/pages/Features.jsx +1 -8
  181. package/src/dashboard/app/pages/Guide.jsx +0 -1
  182. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  183. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  184. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  185. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  186. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  187. package/src/dashboard/app/pages/Standards.jsx +2 -17
  188. package/src/dashboard/scanner/activity-scanner.js +10 -69
  189. package/src/dashboard/scanner/agents-scanner.js +1 -23
  190. package/src/dashboard/scanner/docs-scanner.js +0 -6
  191. package/src/dashboard/scanner/feature-status.js +2 -5
  192. package/src/dashboard/scanner/features-scanner.js +10 -66
  193. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  194. package/src/dashboard/scanner/index.js +0 -4
  195. package/src/dashboard/scanner/portfolio.js +5 -29
  196. package/src/dashboard/scanner/project-api.js +8 -54
  197. package/src/dashboard/scanner/rules-scanner.js +1 -2
  198. package/src/dashboard/scanner/skills-scanner.js +2 -12
  199. package/src/dashboard/scanner/standards-scanner.js +2 -6
  200. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  201. package/src/lib/ai/ai-checks.js +7 -52
  202. package/src/lib/ai/ai-project-scan.js +14 -108
  203. package/src/lib/ai/code-audit.js +7 -63
  204. package/src/lib/ai/pin-drift.js +8 -51
  205. package/src/lib/ai/registry-audit.js +11 -90
  206. package/src/lib/ci/ci-verify.js +4 -19
  207. package/src/lib/ci/github-ci.js +9 -37
  208. package/src/lib/ci/workflow-scaffold.js +2 -10
  209. package/src/lib/detectors/claude-config-detector.js +13 -74
  210. package/src/lib/detectors/index.js +4 -50
  211. package/src/lib/e2e/compose-env.js +5 -66
  212. package/src/lib/e2e/stack-teardown.js +6 -40
  213. package/src/lib/generators/architecture-scanner.js +18 -156
  214. package/src/lib/generators/handoff-generator.js +9 -53
  215. package/src/lib/generators/scaffold-generator.js +5 -101
  216. package/src/lib/generators/settings-generator.js +4 -37
  217. package/src/lib/git/branch-classifier.js +11 -84
  218. package/src/lib/git/default-branch.js +14 -169
  219. package/src/lib/git/evidence-staleness.js +1 -0
  220. package/src/lib/git/gh.js +2 -17
  221. package/src/lib/git/pending-prs.js +6 -37
  222. package/src/lib/git/pr-files.js +3 -28
  223. package/src/lib/git/pr-index.js +3 -34
  224. package/src/lib/git/tree-fingerprint.js +3 -15
  225. package/src/lib/git/tree-state.js +6 -33
  226. package/src/lib/graph/god-nodes.js +2 -22
  227. package/src/lib/graph/graph-health.js +3 -24
  228. package/src/lib/graph/graph-loader.js +5 -56
  229. package/src/lib/graph/graphify-runtime.js +7 -37
  230. package/src/lib/graph/graphifyignore.js +8 -74
  231. package/src/lib/graph/sanitize.js +16 -136
  232. package/src/lib/heavy/cleanup.js +1 -1
  233. package/src/lib/installers/effects.js +8 -93
  234. package/src/lib/installers/mcp-installer.js +9 -169
  235. package/src/lib/installers/memory-installer.js +4 -27
  236. package/src/lib/installers/templates-reconcile.js +5 -60
  237. package/src/lib/mutation/dotnet-introspection.js +20 -106
  238. package/src/lib/mutation/subset.js +1 -4
  239. package/src/lib/plan/artifact-vocabulary.js +5 -25
  240. package/src/lib/plan/checks.js +5 -31
  241. package/src/lib/plan/plan-parser.js +1 -6
  242. package/src/lib/projects/registry.js +7 -44
  243. package/src/lib/review-reports.js +5 -23
  244. package/src/lib/risk-detector.js +39 -173
  245. package/src/lib/specs/adr-index.js +6 -34
  246. package/src/lib/specs/frontmatter.js +2 -15
  247. package/src/lib/specs/gfm-table.js +1 -7
  248. package/src/lib/specs/revocations.js +1 -14
  249. package/src/lib/specs/screen-checklist.js +47 -124
  250. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  251. package/src/lib/stack/stack-profile.js +1 -28
  252. package/src/lib/stack/stack-scan.js +1 -8
  253. package/src/lib/stack-filter.js +4 -27
  254. package/src/lib/standards/registry.js +1 -7
  255. package/src/lib/standards/sections.js +2 -13
  256. package/src/lib/standards/verified-against.js +19 -191
  257. package/src/lib/tasks/eval-node.js +21 -169
  258. package/src/lib/tasks/linux-runner.js +18 -142
  259. package/src/lib/tasks/test-nodes.js +43 -240
  260. package/src/lib/tasks/test-runner.js +43 -341
  261. package/src/lib/telemetry/metrics.js +7 -36
  262. package/src/lib/telemetry/telemetry-reader.js +13 -125
  263. package/src/lib/telemetry/verify-event.js +2 -30
  264. package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
  265. package/src/lib/validators/code-budget/index.js +0 -7
  266. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  267. package/src/lib/validators/contracts/index.js +0 -6
  268. package/src/lib/validators/nextjs/index.js +0 -5
  269. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  270. package/src/lib/validators/packages/index.js +0 -6
  271. package/src/lib/validators/packages/package-validator.js +6 -56
  272. package/src/lib/validators/reuse/index.js +0 -8
  273. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  274. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  275. package/src/lib/validators/traceability-check.js +5 -63
  276. package/src/lib/validators/validation-runner.js +20 -125
  277. package/src/lib/validators/verify-runner.js +52 -358
  278. package/src/lib/work-type-detector.js +5 -67
  279. package/src/lib/worktree/docker-audit.js +17 -136
  280. package/src/lib/worktree/env-allocator.js +10 -98
  281. package/src/lib/worktree/feature-truth.js +12 -115
  282. package/src/lib/worktree/finish-backup.js +32 -242
  283. package/src/lib/worktree/fleet-scanner.js +13 -95
  284. package/src/lib/worktree/ignored-triage.js +15 -91
  285. package/src/lib/worktree/vscode-settings.js +2 -3
  286. package/src/lib/worktree/worktree-audit.js +21 -213
  287. package/src/lib/worktree/worktree-include.js +3 -5
  288. package/src/scripts/global-install.js +3 -9
  289. package/src/scripts/install-plugin.js +1 -13
  290. package/src/scripts/setup-infra.js +17 -147
  291. package/src/utils/agents-installer.js +28 -275
  292. package/src/utils/banner.js +4 -25
  293. package/src/utils/claude-md-injector.js +1 -37
  294. package/src/utils/claude-settings-manager.js +1 -52
  295. package/src/utils/file-copier.js +11 -95
  296. package/src/utils/hooks-installer.js +21 -298
  297. package/src/utils/skills-installer.js +4 -101
  298. package/src/utils/version-checker.js +4 -83
  299. package/framework/commands/morph-proposal.md +0 -9
  300. package/src/commands/utils/index.js +0 -3
  301. package/src/lib/detectors/design-system-detector.js +0 -188
  302. package/src/lib/generators/design-system-generator.js +0 -508
  303. package/src/lib/specs/old-format.js +0 -68
  304. package/src/utils/agent-id-mapper.js +0 -11
@@ -2,11 +2,8 @@ import { planAutoApprovalBlock } from './gate-check.js';
2
2
  import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
3
3
 
4
4
  /**
5
- * Why an "auto" signature may not stamp `gate`, or null when it may. The ONE
6
- * rule `approve` and `advance` both apply: they write the same
7
- * `approvalGates` entry, so they must refuse the same inputs.
8
- *
9
- * @returns {{ title: string, hints: string[] } | null}
5
+ * Why an "auto" signature may not stamp `gate`, or null when it may. The one rule
6
+ * `approve` and `advance` share: they write the same `approvalGates` entry.
10
7
  */
11
8
  export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedBy }) {
12
9
  if (approvedBy !== 'auto' || !gate) return null;
@@ -10,29 +10,9 @@ import { detectDefaultBranch } from '../../lib/git/default-branch.js';
10
10
  import { writePlanSkeleton } from '../../lib/plan/plan-template.js';
11
11
  import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
12
12
 
13
- /**
14
- * Common flow (no `--worktree`): still isolate the feature on its own branch
15
- * `morph/{feature}`, branched from the DEFAULT branch (never HEAD — so it never
16
- * inherits another feature branch's commits) and checked out in place, so
17
- * `morph-spec finish` can later merge/PR it back. Symmetric with the worktree
18
- * flow, which gets its branch from `runWorktreeSetup`.
19
- *
20
- * That checkout moves the dev's PRIMARY working tree, and `stdio: 'pipe'`
21
- * swallows even git's own message — so the move is reported by the caller
22
- * (`branchMoveWarning`), never silent. `checkout: false` (`--no-checkout`)
23
- * still CREATES the branch — `finish --pr|--merge` depends on it existing in
24
- * both flows — and only skips moving the tree.
25
- *
26
- * Fail-open: outside a git repo, with no resolvable default, or on any git
27
- * error, the feature is simply left on the current branch — never a failed birth.
28
- *
29
- * @param {string} featureName
30
- * @param {{ checkout?: boolean }} [opts] - `checkout: false` keeps the tree put
31
- * @returns {{ branched: boolean, checkedOut: boolean, branch?: string, from?: string,
32
- * movedFrom?: string, stayedOn?: string, reason?: string }}
33
- * `movedFrom` is present ONLY when the working tree actually moved (it names
34
- * the branch left behind); `stayedOn` only when the move was declined.
35
- */
13
+ // Branches from the DEFAULT branch, never HEAD, so it never inherits another feature's commits.
14
+ // `checkout: false` still CREATES the branch, which `finish` depends on. Fail-open: any git error
15
+ // leaves the feature on the current branch, never a failed birth.
36
16
  function branchCommonFlow(featureName, { checkout = true } = {}) {
37
17
  const cwd = process.cwd();
38
18
  try {
@@ -44,7 +24,6 @@ function branchCommonFlow(featureName, { checkout = true } = {}) {
44
24
  execFileSync('git', ['branch', branch, defaultBranch], { cwd, stdio: 'pipe' });
45
25
  }
46
26
  const current = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
47
- // Already there: nothing moved, so nothing to warn about.
48
27
  if (current === branch) return { branched: true, checkedOut: true, branch, from: defaultBranch };
49
28
  if (!checkout) return { branched: true, checkedOut: false, branch, from: defaultBranch, stayedOn: current };
50
29
  execFileSync('git', ['checkout', branch], { cwd, stdio: 'pipe' });
@@ -54,14 +33,7 @@ function branchCommonFlow(featureName, { checkout = true } = {}) {
54
33
  }
55
34
  }
56
35
 
57
- /**
58
- * The alert for a checkout that MOVED. Built once and rendered on both
59
- * surfaces — stdout (human) and stderr (under `--json`, so it survives a
60
- * stdout redirect) — so the two can never drift apart.
61
- *
62
- * @param {{ branch: string, movedFrom: string }} branchResult
63
- * @returns {string[]} lines, unstyled and unindented
64
- */
36
+ // One builder for stdout and (under --json) stderr, so the two surfaces never drift.
65
37
  function branchMoveWarning({ branch, movedFrom }) {
66
38
  return [
67
39
  `⚠️ Working tree MOVED: ${movedFrom} → ${branch}`,
@@ -72,21 +44,12 @@ function branchMoveWarning({ branch, movedFrom }) {
72
44
  ];
73
45
  }
74
46
 
75
- /** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
76
47
  const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
77
48
 
78
49
  const VALID_STATUSES = ['draft', 'in_progress', 'completed'];
79
50
  const VALID_DETECT_SOURCES = ['deterministic', 'llm', 'user'];
80
51
 
81
- /**
82
- * Resolve the work type for a NEW feature from `--type`/`--request`, or fall
83
- * back to the default. Never touches the filesystem/state — pure control
84
- * flow so it can `process.exit` before any side effect happens.
85
- *
86
- * @param {{ type?: string, request?: string, detectSource?: string }} options
87
- * @param {string} now - ISO timestamp, stamped into workTypeDetection.decidedAt
88
- * @returns {{ workType: string, workTypeDetection?: object }}
89
- */
52
+ // Pure control flow, so it can `process.exit` before any side effect happens.
90
53
  function resolveWorkType(options, now) {
91
54
  if (options.type !== undefined) {
92
55
  if (!isValidWorkType(options.type)) {
@@ -112,10 +75,8 @@ function resolveWorkType(options, now) {
112
75
  if (options.request !== undefined) {
113
76
  const d = detectWorkType({ text: options.request });
114
77
  if (d.confidence !== 'high') {
115
- // Router contract: determinism first, ambiguity escalates to the
116
- // LLM/HCOP caller (which re-runs `create --type <chosen>`). The
117
- // feature must NOT be created on this path — always JSON, regardless
118
- // of --json, since this is the machine-consumable half of the contract.
78
+ // Ambiguity escalates to the caller, which re-runs `create --type`: nothing is created here, and
79
+ // the output is always JSON since this is the machine-consumable half of the contract.
119
80
  console.log(JSON.stringify({ needsDecision: true, candidates: d.candidates, matched: d.matched }));
120
81
  process.exit(2);
121
82
  }
@@ -133,20 +94,8 @@ function resolveWorkType(options, now) {
133
94
  return { workType: DEFAULT_WORK_TYPE };
134
95
  }
135
96
 
136
- /**
137
- * `morph-spec create <feature>` — explicit feature birth: classifies the
138
- * work type (`--type`/`--request`), creates `.morph/features/{feature}/`
139
- * scaffolded per the type's dimensioned pipeline, stamps policy-approved
140
- * gates (`approvedBy: "policy:{type}"`) for the gates that type skips, and
141
- * (with `--worktree`) births the feature already inside its own git
142
- * worktree on branch `morph/{feature}`.
143
- *
144
- * Replaces the implicit birth via the state-sync hook on the first Write —
145
- * the skills call this at plan start so the authoritative state exists
146
- * with the full canonical shape before any artifact lands. Idempotent: an
147
- * existing feature is reported and left untouched — no re-stamping, no
148
- * re-scaffolding, no worktree setup.
149
- */
97
+ // Idempotent: an existing feature is reported and left untouched (no re-stamping, no
98
+ // re-scaffolding, no worktree setup).
150
99
  export async function createCommand(featureName, options = {}) {
151
100
  try {
152
101
  if (!FEATURE_NAME_RE.test(featureName)) {
@@ -161,15 +110,8 @@ export async function createCommand(featureName, options = {}) {
161
110
  process.exit(1);
162
111
  }
163
112
 
164
- // `--no-checkout` pede que o checkout PRINCIPAL não se mova; `--worktree`
165
- // nunca o move — a feature nasce numa árvore separada, e o caminho de
166
- // worktree sequer chama `branchCommonFlow`, que é o único leitor do flag.
167
- // Aceitar a combinação em silêncio é dizer que o CLI honrou um pedido que
168
- // ele ignorou — o mesmo anti-padrão que esta série de fixes tirou do
169
- // `finish --pr --push`. Recusa ALTA, e AQUI: junto das outras validações
170
- // de forma, ANTES de `createFeature` — a primeira mutação do `create`.
171
- // Uma recusa depois dela deixaria a feature nascida por um comando que
172
- // falhou, pior que o no-op que ela substitui.
113
+ // `--worktree` nunca move o checkout principal: aceitar `--no-checkout` junto em silêncio é fingir
114
+ // que o pedido foi honrado. Recusa AQUI, antes de `createFeature`, a primeira mutação do `create`.
173
115
  if (options.worktree && options.checkout === false) {
174
116
  console.error(chalk.red(`\n❌ no-checkout-with-worktree: --no-checkout não se combina com --worktree`));
175
117
  console.error(chalk.gray(` O --worktree já não move o checkout desta árvore: a feature nasce em worktrees/${featureName},`));
@@ -182,16 +124,10 @@ export async function createCommand(featureName, options = {}) {
182
124
  const now = new Date().toISOString();
183
125
  const { workType, workTypeDetection } = resolveWorkType(options, now);
184
126
 
185
- // Identity at birth: every feature is stamped name (slug by default) +
186
- // description, so it is never an anonymous {status,updatedAt} shell.
187
- // `--request` (which the router already consumes) doubles as the
188
- // description seed. A NEW feature with no description source is refused —
189
- // the description is the one identity field a human must supply. An
190
- // idempotent re-run on an EXISTING feature never demands it.
127
+ // A NEW feature with no description source is refused: it is the one identity field a human
128
+ // must supply. An idempotent re-run never demands it.
191
129
  const description = options.description ?? options.request ?? null;
192
- // Existence check via the filesystem, NOT getFeature/loadState — loadState
193
- // throws when no state exists yet (a feature can be the first morph action
194
- // in a fresh project), which would break birth-from-nothing.
130
+ // Filesystem, not loadState: loadState throws when no state exists yet (birth from nothing).
195
131
  const alreadyExists = existsSync(join(process.cwd(), '.morph', 'features', featureName, 'feature.json'));
196
132
  if (!alreadyExists && (description === null || String(description).trim() === '')) {
197
133
  console.error(chalk.red(`\n❌ A feature needs a description at birth: "${featureName}"`));
@@ -217,14 +153,11 @@ export async function createCommand(featureName, options = {}) {
217
153
  let gitignoreAdded = [];
218
154
 
219
155
  if (created) {
220
- // Scaffold the phase folders this work type starts with.
221
156
  for (const folder of wt.scaffold) {
222
157
  mkdirSync(join(featureDir, folder), { recursive: true });
223
158
  }
224
159
 
225
- // Stamp the gates this type skips by policy — NEVER "auto" (the
226
- // gate-guard hook only special-cases approvedBy === "auto"; policy:{type}
227
- // keeps the audit trail honest while staying silent to the guard).
160
+ // NEVER "auto": the gate-guard hook special-cases it; `policy:{type}` keeps the audit honest.
228
161
  for (const gate of wt.skippedGates) {
229
162
  await setApprovalGate(featureName, gate, true, {
230
163
  approvedBy: `${POLICY_APPROVER_PREFIX}${workType}`,
@@ -236,19 +169,10 @@ export async function createCommand(featureName, options = {}) {
236
169
  worktreeResult = await runWorktreeSetup(featureName, {});
237
170
  if (worktreeResult?.created && worktreeResult.path) {
238
171
  try {
239
- // feature.json in the root is NOT committed yet (create just
240
- // wrote it), so the branch `runWorktreeSetup` forked off the
241
- // DEFAULT branch cannot have it — copy the freshly-scaffolded
242
- // folder into the worktree and re-materialize its local state.json.
172
+ // The root feature.json is not committed yet, so the worktree branch cannot have it: copy it in.
243
173
  cpSync(featureDir, join(worktreeResult.path, '.morph', 'features', featureName), { recursive: true });
244
- // The `.worktree` registration is ROOT-OWNED metadata (runWorktreeSetup
245
- // stamped it onto the root's feature.json, which cpSync just copied in).
246
- // Strip it from the worktree's branch-local copy so the branch commits a
247
- // clean feature.json — otherwise it merges back into the root at
248
- // `morph-spec finish` and the post-merge cleanup leaves the root dirty.
249
- // verify.decidePersistence reads the registration from the ROOT's copy,
250
- // never this branch-local one (see verify.js), so stripping is safe and
251
- // restores the invariant that comment relies on.
174
+ // `.worktree` is ROOT-OWNED: left in the branch copy it merges back at `finish` and leaves the
175
+ // root dirty. verify reads the registration from the root copy only.
252
176
  try {
253
177
  const wtFeatureJson = join(worktreeResult.path, '.morph', 'features', featureName, 'feature.json');
254
178
  const fj = JSON.parse(readFileSync(wtFeatureJson, 'utf8'));
@@ -257,25 +181,17 @@ export async function createCommand(featureName, options = {}) {
257
181
  writeFileSync(wtFeatureJson, JSON.stringify(fj, null, 2), 'utf8');
258
182
  }
259
183
  } catch { /* fail-open — root copy still carries the registration for verify */ }
260
- // Re-materialize now that the feature.json exists in the worktree, and
261
- // report THIS result — runWorktreeSetup's earlier materialize ran before
262
- // the copy (no features yet), so its `materialized:false` was stale.
184
+ // Re-materialize after the copy: the setup's earlier materialize ran with no features yet.
263
185
  const remat = await materializeState(worktreeResult.path);
264
186
  worktreeResult.materialized = remat.materialized;
265
- // Same reason the setup path generates one: the new instance's first
266
- // read. Regenerated here because setup ran before the copy above.
267
187
  try {
268
188
  const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
269
- // Same reason as runWorktreeSetup's own call: `worktree` was just
270
- // stripped from this branch-local copy a few lines up, so the doc
271
- // must be told explicitly where it actually lives.
189
+ // `worktree` was stripped from this copy above, so pass where it actually lives.
272
190
  writeHandoff(featureName, worktreeResult.path, { worktree: { path: worktreeResult.path, branch: worktreeResult.branch } });
273
191
  } catch { /* fail-open */ }
274
192
  } catch { /* fail-open — the feature already exists in root */ }
275
193
  }
276
194
  } else {
277
- // Common flow: still isolate the feature on its own branch so `finish`
278
- // can merge/PR it back. Fail-open (see branchCommonFlow).
279
195
  // Commander's `--no-checkout` sets options.checkout === false.
280
196
  branchResult = branchCommonFlow(featureName, { checkout: options.checkout !== false });
281
197
  }
@@ -296,9 +212,7 @@ export async function createCommand(featureName, options = {}) {
296
212
  gitignoreAdded = seedDotnetBuildIgnores(home, { featureName, configRoot: process.cwd() });
297
213
  }
298
214
 
299
- // `feature` is a snapshot from BEFORE the gate-stamping/worktree steps —
300
- // setApprovalGate/runWorktreeSetup each do their own loadState()/saveState()
301
- // round-trip on disk, so re-read the authoritative copy for reporting.
215
+ // `feature` predates the gate-stamping and worktree steps, which each round-trip state on disk.
302
216
  const finalFeature = created ? (getFeature(featureName) || feature) : feature;
303
217
 
304
218
  if (options.json) {
@@ -307,8 +221,7 @@ export async function createCommand(featureName, options = {}) {
307
221
  if (branchResult) out.branch = branchResult;
308
222
  if (planSkeleton) out.planSkeleton = planSkeleton;
309
223
  if (gitignoreAdded.length) out.gitignoreAdded = gitignoreAdded;
310
- // The move of the PRIMARY checkout is never silent — under --json it goes
311
- // to stderr so it survives `... --json > out.json` instead of vanishing.
224
+ // Under --json the move goes to stderr, so it survives `... --json > out.json`.
312
225
  if (branchResult?.movedFrom) {
313
226
  console.error(chalk.yellow(branchMoveWarning(branchResult).join('\n')));
314
227
  }
@@ -6,33 +6,10 @@ import { isValidFeatureName, cwdIsInsideWorktree, teardownWorktree, resolvePrima
6
6
  import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
7
7
  import { tearDownFeatureStack } from '../../lib/e2e/stack-teardown.js';
8
8
 
9
- /**
10
- * `morph-spec delete <feature>` — remove a feature. Hard by default (folder is
11
- * gone), soft via `--archive` (moved to .morph/archive with status
12
- * "abandoned"). The complement of `archive`: archive = completed work you keep;
13
- * delete = dead scaffolds and abandoned drafts you clear out.
14
- *
15
- * The critical guarantee — mirrored from `archive` — is that the deletion also
16
- * drops the feature from the thin root index (.morph/state.json). Without that,
17
- * the next `loadState` resurrects the folder from the stale index entry
18
- * (turning a delete into a zombie). We edit the index directly rather than
19
- * calling saveState, which would re-materialize .morph/features/{name}.
20
- *
21
- * Refuses when any gate is approved (real work, not a dead shell), unless
22
- * `--force`.
23
- *
24
- * SECURITY: `featureName` is an unsanitized CLI argument that flows straight
25
- * into `rmSync(..., { recursive: true })` — the same class of bug the worktree
26
- * teardown guards against, but here the danger is path TRAVERSAL rather than a
27
- * followed symlink. `join(cwd, '.morph', 'features', '../../.claude')`
28
- * resolves to `cwd/.claude`, so an unvalidated name can point the hard-delete
29
- * at the shared `.claude/` install (or anything else relative to cwd). Gated
30
- * by `isValidFeatureName` (same kebab-slug rule `create` stamps at birth) PLUS
31
- * a resolved-path containment assert — validated by path, not by the
32
- * convention that a real feature name never contains `..`.
33
- */
34
-
35
- /** Refuse a delete/archive whose resolved target escapes `.morph/features/{scope}/{name}`. */
9
+ // `featureName` flows into a recursive rmSync and `../../.claude` would resolve to the shared
10
+ // install: the name is validated AND the resolved path must stay contained. Dropping the root
11
+ // index entry matters too, or the next loadState resurrects the folder.
12
+
36
13
  function assertContained(scopeDir, name, target) {
37
14
  const scopeAbs = resolve(scopeDir) + sep;
38
15
  const targetAbs = resolve(target);
@@ -57,7 +34,6 @@ function approvedGates(feature) {
57
34
  .map(([name]) => name);
58
35
  }
59
36
 
60
- /** Drop the feature from the thin, derivable root index (like archive does). */
61
37
  function dropFromIndex(cwd, featureName) {
62
38
  const rootPath = join(cwd, '.morph', 'state.json');
63
39
  if (!existsSync(rootPath)) return;
@@ -76,11 +52,7 @@ export async function deleteCommand(featureName, options = {}) {
76
52
  try {
77
53
  const cwd = options.cwd || process.cwd();
78
54
 
79
- // Gate 0: valid kebab name — refuses BEFORE any path is built from it, so a
80
- // traversal attempt (`../../.claude`) never reaches existsSync/rmSync. Same
81
- // gate `create` stamps at birth and `finish` re-checks; unlike those two,
82
- // delete/archive take a FRESH argument every invocation, so this cannot rely
83
- // on "every real feature folder is already a valid slug".
55
+ // Validated BEFORE any path is built: delete takes a fresh argument on every invocation.
84
56
  if (!isValidFeatureName(featureName)) {
85
57
  console.error(chalk.red(`\n❌ Invalid feature name: "${featureName}"`));
86
58
  console.error(chalk.gray('Use kebab-case: lowercase letters, digits and hyphens (e.g. "user-onboarding")\n'));
@@ -97,14 +69,12 @@ export async function deleteCommand(featureName, options = {}) {
97
69
  process.exit(1);
98
70
  }
99
71
 
100
- // Read the authoritative feature.json directly (not loadState — a read that
101
- // mutates state has no place inside a delete).
72
+ // Not loadState: a read that mutates state has no place inside a delete.
102
73
  let feature = {};
103
74
  if (existsSync(fjPath)) {
104
75
  try { feature = JSON.parse(readFileSync(fjPath, 'utf8')); } catch { feature = {}; }
105
76
  }
106
77
 
107
- // Precondition: no approved gate (real work) unless --force.
108
78
  const approved = approvedGates(feature);
109
79
  if (approved.length && !options.force) {
110
80
  console.error(chalk.red(`\n❌ "${featureName}" has approved gate(s): ${approved.join(', ')}.`));
@@ -113,14 +83,8 @@ export async function deleteCommand(featureName, options = {}) {
113
83
  process.exit(1);
114
84
  }
115
85
 
116
- // A live worktree elsewhere owns this feature's REAL state — deleting the
117
- // root's registration here would orphan the worktree + branch silently
118
- // (no `no-feature` audit hit either: featureCommittedOnBranch stays true).
119
- // Distinct consent from --force on purpose: --force answers "approved gate
120
- // present, delete anyway" (a STATE risk); a live worktree is a FILESYSTEM
121
- // risk (orphan directory, junctions into the primary root) — the same
122
- // reasoning that keeps `worktree remove`'s PROVE-or-DECLINE separate from
123
- // gate policy.
86
+ // A live worktree owns the real state; deleting the root registration would orphan it. A distinct
87
+ // consent from --force on purpose: --force answers a state risk, this one is a filesystem risk.
124
88
  const owner = resolveFeatureTree(featureName, cwd);
125
89
  const liveElsewhere = (owner.source === 'registry' || owner.source === 'pointer') && owner.tree;
126
90
  if (liveElsewhere) {
@@ -141,14 +105,9 @@ export async function deleteCommand(featureName, options = {}) {
141
105
  process.exit(1);
142
106
  }
143
107
 
144
- // PROVE-or-DECLINE first — only delete the feature's own copy if the
145
- // worktree actually came down. A refusal here must leave EVERYTHING
146
- // untouched, same contract as `worktree remove`.
108
+ // PROVE-or-DECLINE: a refusal here must leave everything untouched.
147
109
  const root = resolvePrimaryRoot(cwd);
148
- // The feature's verification stack (containers, ports, volumes, stamp)
149
- // comes down BEFORE its worktree: the compose file and the bind mount
150
- // live there, and a deleted feature has nothing to keep a volume for.
151
- // Fail-open by contract — a stubborn container never blocks the delete.
110
+ // The stack comes down BEFORE the worktree, where its compose file and bind mount live. Fail-open.
152
111
  try {
153
112
  tearDownFeatureStack(featureName, { root, worktreePath: owner.tree, volumes: true });
154
113
  } catch { /* fail-open */ }
@@ -166,15 +125,11 @@ export async function deleteCommand(featureName, options = {}) {
166
125
  console.error(chalk.gray(` ${teardown.declineReason || 'motivo desconhecido'}\n`));
167
126
  process.exit(1);
168
127
  }
169
- // Fall through: the feature's own .morph/features/{featureName} copy in
170
- // `cwd` (the primary root, by construction of `liveElsewhere`) is deleted
171
- // by the normal flow below, exactly as if there had never been a worktree.
172
128
  }
173
129
 
174
130
  const useGit = isGitRepo(cwd) && options.git !== false;
175
131
 
176
132
  if (options.archive) {
177
- // Soft delete: move to .morph/archive with status "abandoned".
178
133
  const archiveDir = join(cwd, '.morph', 'archive');
179
134
  const archivePath = join(archiveDir, featureName);
180
135
  if (existsSync(archivePath)) {
@@ -209,7 +164,6 @@ export async function deleteCommand(featureName, options = {}) {
209
164
  process.exit(0);
210
165
  }
211
166
 
212
- // Hard delete: remove the folder (git-aware), then drop from index.
213
167
  let staged = false;
214
168
  if (useGit) {
215
169
  try {
@@ -218,13 +172,7 @@ export async function deleteCommand(featureName, options = {}) {
218
172
  } catch {
219
173
  // Untracked (never committed) — nothing for git rm to act on.
220
174
  }
221
- // `git rm` only ever removes TRACKED files — a scaffolded phase folder
222
- // with no artifact in it (2-plan/ with nothing written yet) has no
223
- // tracked blob and survives as an empty directory on disk. This bit
224
- // real after --with-worktree: teardownWorktree's own registration-clear
225
- // commits the root's feature.json on its way through, so `git rm`
226
- // SUCCEEDS on that one file while the sibling empty folders it never
227
- // tracked are left behind. Unconditional sweep closes both paths at once.
175
+ // `git rm` only removes tracked files: scaffolded empty phase folders survive it.
228
176
  if (existsSync(featuresPath)) rmSync(featuresPath, { recursive: true, force: true });
229
177
  try { execSync(`git add -A "${featuresPath}"`, { cwd, stdio: 'pipe' }); staged = true; } catch { /* not fatal */ }
230
178
  } else {