@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
@@ -1,11 +1,3 @@
1
- /**
2
- * Scaffold Command - Generate feature files from templates.
3
- *
4
- * Sub-commands:
5
- * - scaffold dotnet <feature> <slice-name> — VSA feature slice (.NET)
6
- * - scaffold nextjs <feature> — Feature page + components (Next.js)
7
- */
8
-
9
1
  import chalk from 'chalk';
10
2
  import { relative } from 'path';
11
3
  import { logger } from '../../utils/logger.js';
@@ -17,11 +9,6 @@ import {
17
9
 
18
10
  const KEBAB_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
19
11
 
20
- /**
21
- * Validate that a name is valid kebab-case.
22
- * @param {string} name
23
- * @param {string} label
24
- */
25
12
  function validateKebabCase(name, label) {
26
13
  if (!KEBAB_REGEX.test(name)) {
27
14
  logger.error(`Invalid ${label}: "${name}"`);
@@ -30,11 +17,6 @@ function validateKebabCase(name, label) {
30
17
  }
31
18
  }
32
19
 
33
- /**
34
- * Log the result of a scaffold operation.
35
- * @param {Array<{path: string, skipped: boolean}>} files
36
- * @param {string} outputDir
37
- */
38
20
  function logResult(files, outputDir) {
39
21
  let created = 0;
40
22
  let skipped = 0;
@@ -56,16 +38,12 @@ function logResult(files, outputDir) {
56
38
  logger.info(parts.join(', '));
57
39
  }
58
40
 
59
- /**
60
- * Scaffold .NET VSA feature slice.
61
- */
62
41
  export async function scaffoldDotnetCommand(feature, sliceName, options) {
63
42
  validateKebabCase(feature, 'feature name');
64
43
  validateKebabCase(sliceName, 'slice name');
65
44
 
66
45
  const outputDir = options.outputDir || process.cwd();
67
46
 
68
- // Detect or use provided namespace
69
47
  const namespace = options.namespace || detectNamespace(outputDir);
70
48
  if (!namespace) {
71
49
  logger.error('Could not detect namespace');
@@ -100,9 +78,6 @@ export async function scaffoldDotnetCommand(feature, sliceName, options) {
100
78
  }
101
79
  }
102
80
 
103
- /**
104
- * Scaffold Next.js feature page with components and hooks.
105
- */
106
81
  export async function scaffoldNextjsCommand(feature, options) {
107
82
  validateKebabCase(feature, 'feature name');
108
83
 
@@ -14,44 +14,17 @@ import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js'
14
14
 
15
15
  const FRAMEWORK_DIR = fileURLToPath(new URL('../../../framework/', import.meta.url));
16
16
 
17
- /**
18
- * `morph-spec advance <feature>` — approve the current phase's gate AND scaffold
19
- * the next phase's folder in one step. This is the sanctioned, one-command way
20
- * to move a feature forward: it removes the need to hand-edit state.json (gate)
21
- * AND the need to `mkdir` the next phase folder by hand. Once the gate is
22
- * approved, the enforce-phase-writes hook permits writing the next phase's first
23
- * canonical artifact (which then advances the derived phase).
24
- */
25
-
26
- // The gate approved when *completing* a given phase. `implement` has no gate
27
- // (it ends when all slices are done), so advancing from implement just
28
- // scaffolds the review folder.
17
+ // `implement` has no gate: advancing from it only scaffolds the review folder.
29
18
  const PHASE_GATE = { plan: 'plan', uiux: 'uiux', review: 'review' };
30
19
 
31
- // The next phase folder to scaffold when advancing FROM a given phase. The
32
- // default forward path skips the optional design phase (plan → implement); pass
33
- // --design to take the uiux branch (plan → 1-design).
34
20
  const NEXT_PHASE_DIR = {
35
21
  plan: '3-implement',
36
22
  uiux: '3-implement',
37
23
  implement: '4-review',
38
24
  };
39
25
 
40
- /**
41
- * The canonical artifacts the phase being CLOSED owes, per
42
- * `framework/phases.json → phases[phase].requiredOutputs`, resolved to real
43
- * filenames through OUTPUT_SCHEMA.
44
- *
45
- * Data-driven on purpose: adding an artifact to a phase must not need a code
46
- * change here, and the list must never drift from the phase definition.
47
- * Fail-open — an unreadable phases.json/OUTPUT_SCHEMA yields an empty list, so
48
- * a broken install degrades to today's behaviour instead of blocking every
49
- * advance.
50
- *
51
- * @param {string} phase
52
- * @param {string} featurePath
53
- * @returns {string[]} relative paths of the MISSING required artifacts
54
- */
26
+ // Data-driven from phases.json so the list never drifts from the phase definition. Fail-open:
27
+ // an unreadable phases.json yields an empty list instead of blocking every advance.
55
28
  function missingPhaseArtifacts(phase, featurePath) {
56
29
  try {
57
30
  const phasesPath = join(FRAMEWORK_DIR, 'phases.json');
@@ -59,8 +32,7 @@ function missingPhaseArtifacts(phase, featurePath) {
59
32
  const required = phases[phase]?.requiredOutputs || [];
60
33
  const missing = [];
61
34
  for (const outputType of required) {
62
- // requiredOutputs already carries OUTPUT_SCHEMA's camelCase keys — an
63
- // unknown key is skipped rather than guessed at.
35
+ // An unknown key is skipped rather than guessed at.
64
36
  const entry = OUTPUT_SCHEMA[outputType];
65
37
  if (!entry?.filename || !entry?.phaseDir) continue;
66
38
  const rel = `${entry.phaseDir}/${entry.filename}`;
@@ -79,20 +51,8 @@ function unfinishedSliceReports(featurePath) {
79
51
  return progress.slices.filter((s) => s.status !== 'feito').map((s) => `3-implement/reports/${s.id}.md`);
80
52
  }
81
53
 
82
- /**
83
- * When a required artifact is absent HERE, check whether it exists in the
84
- * worktree that owns this feature. `feature.json` travels committed on
85
- * `morph/{feature}`, so a feature developed in a worktree has its artifacts
86
- * there and only a scaffold in the primary root — running `advance` from the
87
- * root then demanded a file that already exists, one directory over.
88
- *
89
- * Message-only: this never reads state for a decision and never writes across
90
- * trees. It replaces a misleading error with an accurate one.
91
- *
92
- * Fail-open — no git, no worktree, or any error yields null (today's message).
93
- *
94
- * @returns {Promise<{path: string, branch: string|null, present: string[]}|null>}
95
- */
54
+ // Message-only and fail-open: names the worktree that holds the artifacts instead of demanding
55
+ // a file that exists one directory over. Never writes across trees.
96
56
  async function artifactsLiveInWorktree(featureName, missing) {
97
57
  try {
98
58
  const { findWorktreeForFeature } = await import('../project/worktree.js');
@@ -107,7 +67,6 @@ async function artifactsLiveInWorktree(featureName, missing) {
107
67
  }
108
68
  }
109
69
 
110
- /** Resolve who approved, honestly (mirrors approve.js — no fake human signatures). */
111
70
  function resolveApprovedBy(options) {
112
71
  if (options.approver) return options.approver;
113
72
  if (options.mode === 'auto') return 'auto';
@@ -133,9 +92,7 @@ export async function advanceCommand(featureName, options = {}) {
133
92
  process.exit(1);
134
93
  }
135
94
 
136
- // Owner-tree guard: a live worktree elsewhere owns this feature's state —
137
- // never stamp a gate onto the root's birth-instant skeleton (branch-drift
138
- // created by this command, not merely left behind by one).
95
+ // A live worktree elsewhere owns the state: never stamp a gate onto the root skeleton.
139
96
  const owner = assertOwnerTree(featureName, process.cwd());
140
97
  if (owner.blocked) {
141
98
  for (const { tone, text } of ownerTreeRefusalLines(featureName, owner.tree, owner.branch, `morph-spec advance ${featureName}`)) {
@@ -169,17 +126,10 @@ export async function advanceCommand(featureName, options = {}) {
169
126
  return;
170
127
  }
171
128
 
172
- // Phase-exit artifact check: a phase transition must not step over what the
173
- // phase it is CLOSING owes. `implement` has no gate, so this is the only
174
- // thing that keeps a feature with unfinished slice reports out of review.
175
- //
176
- // `--force` bypasses (same deliberate-recovery role it already plays for
177
- // gate order) but still names what was skipped — a silent bypass would be
178
- // the defect again.
129
+ // `implement` has no gate, so this is what keeps unfinished slice reports out of review.
130
+ // `--force` bypasses it but still names what was skipped.
179
131
  const missingArtifacts = missingPhaseArtifacts(phase, featurePath);
180
132
  if (missingArtifacts.length && !options.force) {
181
- // Divergence, not absence: name the tree that actually holds the work
182
- // instead of asking for a file that exists in it.
183
133
  const elsewhere = await artifactsLiveInWorktree(featureName, missingArtifacts);
184
134
  if (elsewhere) {
185
135
  console.error(chalk.red(`\n❌ A fase "${phase}" de "${featureName}" não pode ser encerrada A PARTIR DESTA ÁRVORE.`));
@@ -199,16 +149,10 @@ export async function advanceCommand(featureName, options = {}) {
199
149
  process.exit(1);
200
150
  }
201
151
 
202
- // 1) Approve the gate that completes the current phase.
203
152
  const gate = PHASE_GATE[phase];
204
153
  const approvedBy = resolveApprovedBy(options);
205
154
 
206
- // Gate monotonicity (mirrors approve.js): advance is a gate write-path too,
207
- // so it must not stamp gate N while a predecessor is pending. In the
208
- // sanctioned flow advance is always in-order, but an anomalous filesystem
209
- // state — e.g. 1-design/design-system.md present without the plan gate —
210
- // would otherwise let advance deepen the violation by stamping uiux.
211
- // Escape via --force for deliberate recovery.
155
+ // Gate monotonicity: never stamp gate N while a predecessor is pending (`--force` to recover).
212
156
  if (gate) {
213
157
  const unmet = unmetPrereqs(gate, feature.approvalGates);
214
158
  if (unmet.length && !options.force) {
@@ -218,19 +162,8 @@ export async function advanceCommand(featureName, options = {}) {
218
162
  }
219
163
  }
220
164
 
221
- // Gate idempotency (F15): `setApprovalGate` REPLACES the gate object whole
222
- // (state-manager.js), so re-writing an already-approved gate does not
223
- // refresh it — it destroys `approvedBy`, `timestamp`, `notes` and
224
- // `selfAssessed` in one assignment. And `resolveApprovedBy` above answers
225
- // "auto" in every headless context, so `approve f plan --approver
226
- // lucas` followed by `advance f` used to rewrite a human's signature as
227
- // "auto" — the one stamp the charter forbids on a human-approved gate.
228
- // The state-sync hook already carries exactly this guard; `advance`, the
229
- // other gate write-path, simply never got it.
230
- //
231
- // It skips ONLY the write. The scaffold and the handoff below still run,
232
- // so re-running `advance` after a failed scaffold keeps working — the
233
- // reason this is a guard and not an abort.
165
+ // `setApprovalGate` replaces the gate whole: rewriting an approved gate would turn a human
166
+ // signature into "auto". Skips ONLY the write, so the scaffold and handoff below still run.
234
167
  const priorGate = gate ? feature.approvalGates?.[gate] : null;
235
168
  const alreadyApproved = priorGate?.approved === true;
236
169
 
@@ -250,7 +183,6 @@ export async function advanceCommand(featureName, options = {}) {
250
183
  });
251
184
  }
252
185
 
253
- // 2) Scaffold the next phase folder so the agent can write its first artifact.
254
186
  let nextDir = NEXT_PHASE_DIR[phase];
255
187
  if (phase === 'plan' && options.design) nextDir = '1-design';
256
188
  if (nextDir) {
@@ -260,9 +192,7 @@ export async function advanceCommand(featureName, options = {}) {
260
192
  if (nextDir === '1-design') reportDesignSeed(seedDesignArtifacts(featureName, process.cwd()));
261
193
  if (phase === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
262
194
 
263
- // Fleet handoff: a phase transition is exactly when a session hands the
264
- // feature over (to a new instance, or to itself after /compact) — refresh
265
- // the derived handoff.md so the next reader starts from the new phase.
195
+ // A phase transition is when a session hands the feature over: refresh handoff.md.
266
196
  try {
267
197
  const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
268
198
  writeHandoff(featureName, process.cwd());
@@ -276,8 +206,7 @@ export async function advanceCommand(featureName, options = {}) {
276
206
  const by = options.force ? '--force' : `aprovado por ${approvedBy}`;
277
207
  console.log(chalk.yellow(` ⚠️ ${by}: avançou com o ${checklistBlock}.`));
278
208
  }
279
- // Report what actually happened: claiming "approved (auto)" when nothing
280
- // was written is how the destroyed signature stayed invisible.
209
+ // Never claim "approved (auto)" when nothing was written.
281
210
  if (gate && alreadyApproved) {
282
211
  console.log(chalk.gray(` Gate "${gate}" was already approved (${priorGate.approvedBy || 'unknown'}) — approval preserved, nothing re-written.`));
283
212
  } else if (gate) {
@@ -19,14 +19,8 @@ export { REVIEWED_SHA_TOKEN };
19
19
 
20
20
  const SIGNED_OFF = { required: [], reports: {}, checks: { status: 'ok', total: 0, missing: [], notProven: [] } };
21
21
 
22
- /**
23
- * Do the review reports still describe the current tree? Every report present
24
- * is compared; the worst answer wins.
25
- *
26
- * @param {ReturnType<typeof reviewReports>} reports
27
- * @returns {{ status: 'ok'|'stale'|'unstamped'|'skip', file?: string, reviewed?: string, head?: string, ahead?: number }}
28
- * `skip`: no verifier report or no git repo. Never a false refusal.
29
- */
22
+ // The worst answer across every report present wins. `skip` (no verifier report, no git) never
23
+ // refuses falsely.
30
24
  function reviewStaleness(reports) {
31
25
  if (reports.verifier.status === 'missing') return { status: 'skip' };
32
26
  const cwd = process.cwd();
@@ -64,9 +58,6 @@ function reviewStaleness(reports) {
64
58
  return unstamped ?? { status: 'ok', head };
65
59
  }
66
60
 
67
- /**
68
- * Unapprove action function - Revoke a previously approved gate
69
- */
70
61
  export async function unapproveCommand(featureName, gateName, options = {}) {
71
62
  try {
72
63
  if (!VALID_GATES.includes(gateName)) {
@@ -95,8 +86,7 @@ export async function unapproveCommand(featureName, gateName, options = {}) {
95
86
  return;
96
87
  }
97
88
 
98
- // Same honesty rule as approveCommand: don't pretend a human revoked
99
- // the gate when there's no interactive shell behind the invocation.
89
+ // Don't pretend a human revoked the gate without an interactive shell behind the call.
100
90
  let revokedBy;
101
91
  if (options.revoker) {
102
92
  revokedBy = options.revoker;
@@ -123,44 +113,25 @@ export async function unapproveCommand(featureName, gateName, options = {}) {
123
113
  }
124
114
  }
125
115
 
126
- /**
127
- * Approval Commands - Manage approval gates for MORPH workflow
128
- *
129
- * Commands:
130
- * - approve: Mark a gate as approved
131
- * - unapprove: Revoke a gate approval
132
- * - approval-status: Show approval status for all gates
133
- */
134
-
135
116
  const program = new Command();
136
117
 
137
- /**
138
- * Valid approval gate names
139
- */
140
118
  const VALID_GATES = ['plan', 'uiux', 'review'];
141
119
 
142
- /**
143
- * Approve action function - Mark a gate as approved
144
- */
145
120
  export async function approveCommand(featureName, gateName, options = {}) {
146
121
  try {
147
- // Validate gate name
148
122
  if (!VALID_GATES.includes(gateName)) {
149
123
  console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
150
124
  console.log(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
151
125
  process.exit(1);
152
126
  }
153
127
 
154
- // Get feature state
155
128
  const feature = getFeature(featureName);
156
129
  if (!feature) {
157
130
  console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
158
131
  process.exit(1);
159
132
  }
160
133
 
161
- // Owner-tree guard: a live worktree elsewhere owns this feature's state —
162
- // never stamp a gate onto the root's birth-instant skeleton (branch-drift
163
- // created by this command, not merely left behind by one).
134
+ // A live worktree elsewhere owns the state: never stamp a gate onto the root skeleton.
164
135
  const owner = assertOwnerTree(featureName, process.cwd());
165
136
  if (owner.blocked) {
166
137
  for (const { tone, text } of ownerTreeRefusalLines(featureName, owner.tree, owner.branch, `morph-spec approve ${featureName} ${gateName}`)) {
@@ -169,27 +140,18 @@ export async function approveCommand(featureName, gateName, options = {}) {
169
140
  process.exit(1);
170
141
  }
171
142
 
172
- // Check if already approved
173
143
  const currentStatus = getApprovalGate(featureName, gateName);
174
144
  if (currentStatus?.approved) {
175
145
  console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is already approved`));
176
146
  console.log(chalk.gray(`Approved at: ${currentStatus.timestamp}`));
177
147
  console.log(chalk.gray(`Approved by: ${currentStatus.approvedBy || 'Unknown'}`));
178
- // This early-out is a no-op that still exits 0 — deliberate, and
179
- // `--force` does NOT cover it (that flag only covers gate ORDER). A
180
- // caller who meant to re-approve therefore reads success and is left
181
- // with nowhere to go. The way out already exists as `unapprove`; the
182
- // message just never named it. Naming it is the whole fix (no new flag).
148
+ // `--force` does not cover this no-op (it only covers gate ORDER), so name the way out.
183
149
  console.log(chalk.gray(`To approve it again, revoke it first: morph-spec unapprove ${featureName} ${gateName}\n`));
184
150
  if (gateName === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
185
151
  return;
186
152
  }
187
153
 
188
- // Gate monotonicity: a gate may only be approved once its predecessors are
189
- // approved (plan → review; uiux requires plan). Prevents the shape of a
190
- // review approved with the plan still pending. Policy-stamped
191
- // gates count as approved, so chore/hotfix satisfy prereqs naturally.
192
- // Escape via --force for deliberate recovery.
154
+ // Gate monotonicity: predecessors first; policy stamps count (`--force` to recover).
193
155
  const unmet = unmetPrereqs(gateName, feature.approvalGates);
194
156
  if (unmet.length && !options.force) {
195
157
  console.error(chalk.red(`\n❌ Cannot approve "${gateName}" — prerequisite gate(s) not approved: ${unmet.join(', ')}.`));
@@ -197,22 +159,8 @@ export async function approveCommand(featureName, gateName, options = {}) {
197
159
  process.exit(1);
198
160
  }
199
161
 
200
- // Resolve who approved BEFORE the hotfix backstop — the backstop gates on
201
- // the resolved signature, not the raw --mode string (see below).
202
- //
203
- // approvedBy resolution (most explicit wins):
204
- // 1. --approver flag (CLI caller signalled who approved)
205
- // 2. --mode auto → "auto" (intentionally non-human signature)
206
- // 3. interactive shell (TTY on stdin) → $USER / $USERNAME (a real
207
- // human is at the terminal; their name is the honest answer)
208
- // 4. non-interactive (no TTY) → "auto"
209
- //
210
- // The TTY check exists because morph-spec is increasingly invoked
211
- // from headless contexts (claude -p subprocesses, CI, the
212
- // morph-e2e-tester). Stamping the host's $USERNAME on a gate that
213
- // no human actually approved is misleading — it lies to the audit
214
- // trail. "auto" reads as what it is: a deterministic policy gate,
215
- // not a human signing off.
162
+ // Resolved BEFORE the hotfix backstop, which gates on the signature. Without a TTY the answer
163
+ // is "auto": stamping $USERNAME on a gate no human approved lies to the audit trail.
216
164
  let approvedBy;
217
165
  if (options.approver) {
218
166
  approvedBy = options.approver;
@@ -230,16 +178,8 @@ export async function approveCommand(featureName, gateName, options = {}) {
230
178
  process.exit(1);
231
179
  }
232
180
 
233
- // Gate 3 staleness backstop (D1): a review reviews a TREE, and the tree
234
- // moves. Observed: a review report written at b30ac6e, then T3 landed and
235
- // inverted the default of a business rule; the report stayed in the repo
236
- // claiming 976/976 tests (they were 982) and never mentioning T3. Nothing
237
- // detected that the review artifact had fallen behind the code it reviews.
238
- //
239
- // The report stamps the SHA it reviewed; approval compares it with HEAD.
240
- // A stamp that is BEHIND HEAD refuses. A MISSING stamp only warns — reports
241
- // written before this existed must not brick a legacy feature — but the
242
- // warning names the exact token to add, so it teaches instead of nagging.
181
+ // A review reviews a TREE: a `Reviewed-SHA` behind HEAD refuses. A missing stamp only warns,
182
+ // naming the token to add, so a legacy report does not brick the feature.
243
183
  const reports = gateName === 'review' ? reviewReports(process.cwd(), featureName) : null;
244
184
  if (gateName === 'review') {
245
185
  const stale = reviewStaleness(reports);
@@ -290,9 +230,8 @@ export async function approveCommand(featureName, gateName, options = {}) {
290
230
  selfAssessed = true;
291
231
  }
292
232
 
293
- // Sem gate-check prévio não há `gateDecisions.review` para o backstop acima morder; em
294
- // campo, ships passaram assim com verify vermelho ou ausente. A auto-aprovação recalcula
295
- // a decisão pela MESMA régua do gate-check. --self-assessed já dispensou os relatórios.
233
+ // Sem gate-check prévio não há `gateDecisions.review` para o backstop morder: a auto-aprovação
234
+ // recalcula a decisão pela MESMA régua do gate-check. --self-assessed já dispensou os relatórios.
296
235
  if (approvedBy === 'auto') {
297
236
  const signals = collectSignals(featureName, feature, process.cwd(), 'review');
298
237
  if (selfAssessed) signals.reviewReports = SIGNED_OFF;
@@ -321,12 +260,7 @@ export async function approveCommand(featureName, gateName, options = {}) {
321
260
  ...(selfAssessed ? { selfAssessed: true } : {}),
322
261
  });
323
262
 
324
- // Fleet handoff (mirrors advance.js): approving a gate is exactly when a
325
- // feature hands over between sessions too — `approve` is the real path
326
- // the plan gate took in the field (`approve <f> plan --mode auto`), not just
327
- // `advance`, so a handoff.md left stale after approve was the common
328
- // case, not the edge case. Same helper, same fail-open contract: a
329
- // regeneration failure must never fail the approval itself.
263
+ // Approving is a session handoff too; a regeneration failure must never fail the approval.
330
264
  try {
331
265
  const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
332
266
  writeHandoff(featureName, process.cwd());
@@ -347,28 +281,20 @@ export async function approveCommand(featureName, gateName, options = {}) {
347
281
  }
348
282
  }
349
283
 
350
- /**
351
- * Approve command - Mark a gate as approved
352
- */
353
284
  program
354
285
  .command('approve <feature> <gate>')
355
286
  .description('Approve a phase gate to allow workflow progression')
356
287
  .action(approveCommand);
357
288
 
358
- /**
359
- * Approval status action function - Show all gates and their status
360
- */
361
289
  export async function approvalStatusCommand(featureName, options = {}) {
362
290
  try {
363
- // Get feature state
364
291
  const feature = getFeature(featureName);
365
292
  if (!feature) {
366
293
  console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
367
294
  process.exit(1);
368
295
  }
369
296
 
370
- // v5 removed the stored feature.phase — derive it from the filesystem +
371
- // approved gates (same as `morph-spec status`), never print "unknown".
297
+ // Phase derived from the filesystem + approved gates, never "unknown".
372
298
  const featurePath = join(process.cwd(), '.morph', 'features', featureName);
373
299
  const phase = feature.phase || derivePhase(featurePath);
374
300
 
@@ -377,7 +303,6 @@ export async function approvalStatusCommand(featureName, options = {}) {
377
303
  const uiuxGate = getApprovalGate(featureName, 'uiux');
378
304
  const uiuxTaken = uiuxGate?.approved === true || existsSync(join(featurePath, '1-design'));
379
305
 
380
- // JSON output
381
306
  if (options.json) {
382
307
  const status = {
383
308
  feature: featureName,
@@ -398,11 +323,9 @@ export async function approvalStatusCommand(featureName, options = {}) {
398
323
  console.log(chalk.bold(`\n📋 Approval Status for: ${featureName}`));
399
324
  console.log('━'.repeat(60));
400
325
 
401
- // Show current phase
402
326
  console.log(chalk.cyan(`\nCurrent Phase: ${phase}`));
403
327
  console.log(chalk.gray(`Feature Status: ${feature.status || 'unknown'}\n`));
404
328
 
405
- // Show each gate
406
329
  console.log(chalk.bold('Approval Gates:'));
407
330
 
408
331
  for (const gateName of VALID_GATES) {
@@ -444,7 +367,6 @@ export async function approvalStatusCommand(featureName, options = {}) {
444
367
  }
445
368
  }
446
369
 
447
- // Show approval history if available
448
370
  const history = getApprovalHistory(featureName);
449
371
  if (history && history.length > 0) {
450
372
  console.log(chalk.bold('\nApproval History:'));
@@ -462,16 +384,12 @@ export async function approvalStatusCommand(featureName, options = {}) {
462
384
  }
463
385
  }
464
386
 
465
- /**
466
- * Approval status command - Show all gates and their status
467
- */
468
387
  program
469
388
  .command('approval-status <feature>')
470
389
  .alias('status')
471
390
  .description('Show approval status for all gates')
472
391
  .action(approvalStatusCommand);
473
392
 
474
- // Only parse if run directly (not imported as module)
475
393
  if (import.meta.url === `file://${process.argv[1]}`) {
476
394
  if (process.argv.length > 2) {
477
395
  program.parse(process.argv);
@@ -5,7 +5,6 @@ import { execFileSync } from 'child_process';
5
5
  import { ADR_INDEX_PATHSPEC, updateAdrIndex } from '../../lib/specs/adr-index.js';
6
6
  import { isValidFeatureName } from '../project/worktree.js';
7
7
 
8
- /** Refuse a move whose resolved target escapes `scopeDir`. */
9
8
  function assertContained(scopeDir, name, target) {
10
9
  const scopeAbs = resolve(scopeDir) + sep;
11
10
  const targetAbs = resolve(target);
@@ -14,21 +13,7 @@ function assertContained(scopeDir, name, target) {
14
13
  }
15
14
  }
16
15
 
17
- /**
18
- * `morph-spec archive <feature>` — atomic, git-aware archive of a completed
19
- * feature. Stamps the feature completed, moves `.morph/features/{name}/` →
20
- * `.morph/archive/{name}/`, COMMITS the move (scoped to the feature's pathspec),
21
- * and cleans the thin root index.
22
- *
23
- * Two callers:
24
- * - `archiveCommand` (standalone `morph-spec archive` / `/morph-archive`) —
25
- * archive-without-integrate, for legacy features and cleanup.
26
- * - `finishCommand` — runs `runArchive` INSIDE the `morph/{feature}` branch so
27
- * the committed archived state travels with the merge/PR in one integration.
28
- *
29
- * Taxonomy: archived = `status: "completed"` + `archivedAt` (per the project
30
- * decision — no separate "archived" status value).
31
- */
16
+ // Archived = `status: "completed"` + `archivedAt`; there is no separate "archived" status.
32
17
 
33
18
  function isGitRepo(cwd) {
34
19
  try {
@@ -39,7 +24,6 @@ function isGitRepo(cwd) {
39
24
  }
40
25
  }
41
26
 
42
- /** Tolerant read of the review gate — mirrors the finish path's shape handling. */
43
27
  function readReviewApproved(fjPath) {
44
28
  try {
45
29
  const fj = JSON.parse(readFileSync(fjPath, 'utf8'));
@@ -51,30 +35,14 @@ function readReviewApproved(fjPath) {
51
35
  }
52
36
 
53
37
  /**
54
- * Core archive — NO process.exit. Returns a result the caller reports.
55
- *
56
- * Reads/writes everything relative to `cwd` (never a hard-coded process.cwd()),
57
- * so `finishCommand` can archive inside a worktree's own tree.
58
- *
59
- * Idempotent: if `.morph/archive/{feature}` already exists, returns
60
- * `{ ok: true, alreadyArchived: true }` WITHOUT failing — essential so a re-run
61
- * of `finish` (which archives inside the branch) does not blow up.
62
- *
63
- * @param {string} feature
64
- * @param {{ cwd?: string, force?: boolean, git?: boolean }} [opts]
65
- * @returns {{ ok: boolean, error?: string, archived?: boolean, alreadyArchived?: boolean,
66
- * committed?: boolean, staged?: boolean, archivePath?: string,
67
- * revocationProblems?: Array<{id: string|null, reason: string, message: string}>,
68
- * adrIndex?: {added: string[], revoked: string[], narrowed: string[]}|null }}
38
+ * Never calls process.exit, and resolves everything from `cwd` so `finish` can archive inside a
39
+ * worktree. Idempotent: an existing archive returns `{ ok: true, alreadyArchived: true }`.
69
40
  */
70
41
  export function runArchive(feature, opts = {}) {
71
42
  const cwd = opts.cwd || process.cwd();
72
43
 
73
- // Gate 0: valid kebab name — refuses BEFORE any path is built from it. Same
74
- // gate `finish` already applies upstream (defense-in-depth here); the
75
- // standalone `archiveCommand` has no other gate at all, so without this an
76
- // unvalidated `feature` reaches `renameSync` and can MOVE a directory outside
77
- // .morph/features/ (`../../.claude` resolves to `cwd/.claude`).
44
+ // Validated BEFORE any path is built: `../../.claude` would let renameSync move a directory
45
+ // outside .morph/features/.
78
46
  if (!isValidFeatureName(feature)) {
79
47
  return { ok: false, error: 'invalid-feature-name' };
80
48
  }
@@ -88,9 +56,7 @@ export function runArchive(feature, opts = {}) {
88
56
  const featPathspec = join('.morph', 'features', feature);
89
57
  const archivePathspec = join('.morph', 'archive', feature);
90
58
 
91
- // Idempotent short-circuit: already archived here. Checked FIRST so a re-run
92
- // after a partial finish reports "already done", never "not found"; an ADR
93
- // index update that degraded fail-open is therefore not retried.
59
+ // Checked FIRST so a re-run after a partial finish reports "already done", never "not found".
94
60
  if (existsSync(archivePath)) {
95
61
  return { ok: true, alreadyArchived: true, archived: false, archivePath };
96
62
  }
@@ -99,8 +65,6 @@ export function runArchive(feature, opts = {}) {
99
65
  return { ok: false, error: 'feature-folder-not-found' };
100
66
  }
101
67
 
102
- // Precondition: Gate 3 (review) approved — archiving is for completed work.
103
- // Read from the feature's own folder (pre-move), tolerant of legacy shapes.
104
68
  const fjPathBefore = join(featuresPath, 'feature.json');
105
69
  if (!opts.force && !readReviewApproved(fjPathBefore)) {
106
70
  return { ok: false, error: 'review-not-approved' };
@@ -108,9 +72,7 @@ export function runArchive(feature, opts = {}) {
108
72
 
109
73
  const useGit = opts.git !== false && isGitRepo(cwd);
110
74
 
111
- // 1) Move the folder. Prefer `git mv` (keeps history + stages in one step);
112
- // fall back to a filesystem rename (+ git add) when the folder isn't
113
- // tracked yet or git mv fails.
75
+ // `git mv` keeps history and stages in one step; an untracked folder falls back to rename.
114
76
  mkdirSync(archiveDir, { recursive: true });
115
77
  let staged = false;
116
78
  if (useGit) {
@@ -128,8 +90,6 @@ export function runArchive(feature, opts = {}) {
128
90
  renameSync(featuresPath, archivePath);
129
91
  }
130
92
 
131
- // 2) Stamp the authoritative feature.json (now at the archive path) as
132
- // completed + archived, and re-stage it so the commit carries the stamp.
133
93
  const fjPathAfter = join(archivePath, 'feature.json');
134
94
  if (existsSync(fjPathAfter)) {
135
95
  try {
@@ -144,9 +104,7 @@ export function runArchive(feature, opts = {}) {
144
104
  } catch { /* non-fatal — the move is what matters */ }
145
105
  }
146
106
 
147
- // 3) The closing plan's project-wide ADRs enter the index and the ones it
148
- // revokes leave it, in the same scoped commit below. Fail-open: the index
149
- // must never block archiving.
107
+ // Fail-open: the ADR index must never block archiving.
150
108
  let adrIndex = { changed: false, added: [], revoked: [], narrowed: [], problems: [] };
151
109
  try {
152
110
  adrIndex = updateAdrIndex(cwd, archivePath);
@@ -155,12 +113,8 @@ export function runArchive(feature, opts = {}) {
155
113
  try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
156
114
  }
157
115
 
158
- // 4) Commit the move — SCOPED to this feature's pathspec, NEVER `commit -a`
159
- // (same anti-sweep discipline as `finish`'s worktree teardown). This is what
160
- // lets the archived state travel with the branch on `finish` (archive-before-merge).
161
- // The ADR index joins only when it changed: git refuses a pathspec that
162
- // matches nothing, and that refusal would leave the tree dirty and break
163
- // the checkout/merge that follows.
116
+ // Scoped to this feature's pathspec, never `commit -a`. The ADR index joins only when it
117
+ // changed: git refuses a pathspec that matches nothing, leaving the tree dirty for the merge.
164
118
  let committed = false;
165
119
  if (useGit && staged) {
166
120
  const pathspecs = [
@@ -174,9 +128,7 @@ export function runArchive(feature, opts = {}) {
174
128
  } catch { /* nothing to commit / no identity — not fatal, the move + stage stand */ }
175
129
  }
176
130
 
177
- // 5) Drop the feature from the thin root index. Edit the index file directly
178
- // rather than calling saveState — saveState would re-create
179
- // .morph/features/{name}/feature.json and undo the archive.
131
+ // Not saveState: it would re-create .morph/features/{name}/feature.json and undo the archive.
180
132
  const rootPath = join(cwd, '.morph', 'state.json');
181
133
  if (existsSync(rootPath)) {
182
134
  try {
@@ -197,11 +149,7 @@ export function runArchive(feature, opts = {}) {
197
149
  };
198
150
  }
199
151
 
200
- /**
201
- * Standalone `morph-spec archive <feature>` — thin wrapper over `runArchive`
202
- * that maps its result to the CLI's messages and exit codes. Preserves the
203
- * historical behaviour: an already-archived feature is reported as a refusal.
204
- */
152
+ // Unlike `runArchive`, an already-archived feature is reported as a refusal here.
205
153
  export async function archiveCommand(featureName, options = {}) {
206
154
  try {
207
155
  const result = runArchive(featureName, {