@polymorphism-tech/morph-spec 8.46.0 → 9.0.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 +53 -44
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +38 -55
  4. package/bin/validate.js +5 -134
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/create-umbraco-lp.md +2 -0
  12. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  13. package/framework/commands/morph-apply.md +5 -28
  14. package/framework/commands/morph-archive.md +2 -2
  15. package/framework/commands/morph-hotfix.md +8 -8
  16. package/framework/commands/morph-infra.md +17 -203
  17. package/framework/commands/morph-media.md +3 -0
  18. package/framework/commands/morph-preflight.md +7 -31
  19. package/framework/commands/morph-proposal.md +2 -50
  20. package/framework/commands/morph-status.md +3 -3
  21. package/framework/commands/morph-troubleshoot.md +2 -14
  22. package/framework/docs/CLI.md +585 -286
  23. package/framework/evals/review-checklist.md +82 -41
  24. package/framework/hooks/README.md +10 -11
  25. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  26. package/framework/hooks/claude-code/core/denylist.json +33 -3
  27. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  28. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  30. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  31. package/framework/hooks/claude-code/core/state-sync.js +17 -19
  32. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  33. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  34. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  35. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  36. package/framework/hooks/claude-code/statusline.py +55 -41
  37. package/framework/hooks/package.json +3 -0
  38. package/framework/hooks/shared/activity-logger.js +1 -1
  39. package/framework/hooks/shared/build-check.js +10 -6
  40. package/framework/hooks/shared/command-tokens.js +169 -0
  41. package/framework/hooks/shared/comment-rules.js +1 -1
  42. package/framework/hooks/shared/comment-scanner.js +10 -23
  43. package/framework/hooks/shared/comment-stripper.js +1 -1
  44. package/framework/hooks/shared/e2e-check.js +67 -31
  45. package/framework/hooks/shared/errata.js +72 -0
  46. package/framework/hooks/shared/feature-locator.js +1 -1
  47. package/framework/hooks/shared/frontend-locations.js +6 -0
  48. package/framework/hooks/shared/gate-question.js +18 -0
  49. package/framework/hooks/shared/phase-utils.js +15 -32
  50. package/framework/hooks/shared/slice-progress.js +118 -0
  51. package/framework/hooks/shared/spawn-command.js +23 -0
  52. package/framework/hooks/shared/state-reader.js +61 -87
  53. package/framework/hooks/shared/state-writer.js +3 -3
  54. package/framework/mcp/registry.json +3 -4
  55. package/framework/phases.json +23 -39
  56. package/framework/rules/ai-agents-standards.md +18 -16
  57. package/framework/rules/asaas-standards.md +3 -1
  58. package/framework/rules/csharp-standards.md +10 -8
  59. package/framework/rules/data-standards.md +5 -5
  60. package/framework/rules/dotnet-templates.md +1 -1
  61. package/framework/rules/frontend-standards.md +4 -2
  62. package/framework/rules/gsap-standards.md +10 -5
  63. package/framework/rules/hangfire-standards.md +3 -1
  64. package/framework/rules/infrastructure-standards.md +4 -2
  65. package/framework/rules/neon-auth-standards.md +3 -1
  66. package/framework/rules/neon-standards.md +6 -4
  67. package/framework/rules/nextjs-standards.md +13 -11
  68. package/framework/rules/nextjs-templates.md +1 -1
  69. package/framework/rules/resend-standards.md +3 -1
  70. package/framework/schemas/config.schema.json +14 -9
  71. package/framework/schemas/feature.schema.json +15 -52
  72. package/framework/skills/README.md +4 -5
  73. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  74. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  75. package/framework/skills/core/morph-apply/SKILL.md +150 -223
  76. package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
  77. package/framework/skills/core/morph-machine/SKILL.md +4 -4
  78. package/framework/skills/core/morph-media/SKILL.md +3 -3
  79. package/framework/skills/core/morph-plan/SKILL.md +138 -300
  80. package/framework/skills/core/morph-review/SKILL.md +112 -137
  81. package/framework/skills/core/morph-standards/SKILL.md +29 -41
  82. package/framework/skills/core/morph-uiux/SKILL.md +78 -153
  83. package/framework/standards/STANDARDS.json +90 -1219
  84. package/framework/standards/STANDARDS.meta.json +1305 -0
  85. package/framework/standards/ai-agents/evals-with-cache.md +2 -3
  86. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  87. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  88. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  89. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  90. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  91. package/framework/standards/ai-agents/setup.md +2 -2
  92. package/framework/standards/ai-agents/structured-output.md +1 -1
  93. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  94. package/framework/standards/architecture/code-comments.md +8 -8
  95. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  96. package/framework/standards/backend/api/validation.md +135 -47
  97. package/framework/standards/backend/database/postgresql/database.md +11 -7
  98. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  99. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  100. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  101. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  102. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  103. package/framework/standards/backend/dotnet/testing.md +1 -1
  104. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  105. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  106. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  107. package/framework/standards/data/nosql/cache/redis.md +120 -44
  108. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  109. package/framework/standards/frontend/design-system/animations.md +1 -1
  110. package/framework/standards/frontend/design-system/naming.md +1 -1
  111. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  112. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  113. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  114. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  115. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  116. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  117. package/framework/templates/README.md +2 -3
  118. package/framework/templates/REGISTRY.json +6 -170
  119. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  120. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  121. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  122. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  123. package/framework/templates/docs/plan.md +72 -0
  124. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  127. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  128. package/framework/templates/infrastructure/github/README.md +2 -1
  129. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  130. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  131. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  132. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  133. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  134. package/package.json +1 -1
  135. package/src/commands/e2e/e2e.js +2 -2
  136. package/src/commands/observability/cost.js +18 -60
  137. package/src/commands/observability/telemetry.js +3 -5
  138. package/src/commands/project/doctor-procs.js +1 -1
  139. package/src/commands/project/doctor.js +9 -9
  140. package/src/commands/project/fleet.js +4 -4
  141. package/src/commands/project/heavy.js +3 -3
  142. package/src/commands/project/init.js +6 -1
  143. package/src/commands/project/setup-infra-cmd.js +2 -1
  144. package/src/commands/project/stack-scan.js +58 -0
  145. package/src/commands/project/standards.js +79 -25
  146. package/src/commands/project/status.js +29 -69
  147. package/src/commands/project/update.js +33 -19
  148. package/src/commands/project/worktree.js +1 -1
  149. package/src/commands/state/advance.js +62 -46
  150. package/src/commands/state/approve.js +95 -138
  151. package/src/commands/state/archive.js +20 -84
  152. package/src/commands/state/auto-approval.js +68 -0
  153. package/src/commands/state/create.js +28 -1
  154. package/src/commands/state/delete.js +1 -1
  155. package/src/commands/state/finish.js +11 -23
  156. package/src/commands/state/gate-check.js +86 -95
  157. package/src/commands/state/gate-decision.js +6 -4
  158. package/src/commands/state/handoff.js +1 -1
  159. package/src/commands/state/list.js +9 -9
  160. package/src/commands/state/report-check.js +157 -311
  161. package/src/commands/state/retype.js +9 -3
  162. package/src/commands/validation/mutate.js +77 -14
  163. package/src/commands/validation/plan-check.js +81 -0
  164. package/src/commands/validation/validate-feature.js +60 -135
  165. package/src/commands/validation/verify.js +41 -82
  166. package/src/core/paths/output-schema.js +20 -67
  167. package/src/core/state/gate-order.js +5 -6
  168. package/src/core/state/lifecycle-audit.js +2 -2
  169. package/src/core/state/state-manager.js +17 -98
  170. package/src/core/templates/template-registry.js +2 -2
  171. package/src/core/templates/template-seed.js +22 -0
  172. package/src/core/work-types.js +10 -10
  173. package/src/dashboard/app/App.jsx +0 -2
  174. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  175. package/src/dashboard/app/components/CommandPalette.jsx +7 -8
  176. package/src/dashboard/app/components/EntityLink.jsx +1 -2
  177. package/src/dashboard/app/components/Markdown.jsx +1 -1
  178. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  179. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  180. package/src/dashboard/app/lib/phases.js +2 -4
  181. package/src/dashboard/app/pages/Activity.jsx +1 -1
  182. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  183. package/src/dashboard/app/pages/Features.jsx +9 -9
  184. package/src/dashboard/app/pages/Guide.jsx +2 -2
  185. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  186. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  187. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  188. package/src/dashboard/app/pages/Standards.jsx +1 -1
  189. package/src/dashboard/scanner/activity-scanner.js +6 -16
  190. package/src/dashboard/scanner/agents-scanner.js +1 -1
  191. package/src/dashboard/scanner/docs-scanner.js +5 -23
  192. package/src/dashboard/scanner/features-scanner.js +95 -60
  193. package/src/dashboard/scanner/index.js +0 -2
  194. package/src/dashboard/scanner/project-api.js +1 -1
  195. package/src/dashboard/scanner/rules-scanner.js +15 -10
  196. package/src/dashboard/scanner/skills-scanner.js +2 -3
  197. package/src/dashboard/scanner/standards-scanner.js +2 -1
  198. package/src/lib/ci/ci-verify.js +17 -1
  199. package/src/lib/ci/github-ci.js +1 -1
  200. package/src/lib/detectors/index.js +3 -8
  201. package/src/lib/generators/handoff-generator.js +34 -65
  202. package/src/lib/generators/settings-generator.js +11 -89
  203. package/src/lib/git/evidence-staleness.js +1 -1
  204. package/src/lib/git/tree-fingerprint.js +19 -7
  205. package/src/lib/git/tree-state.js +2 -2
  206. package/src/lib/heavy/cleanup.js +1 -5
  207. package/src/lib/installers/memory-installer.js +6 -8
  208. package/src/lib/mutation/commands.js +3 -2
  209. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  210. package/src/lib/mutation/plan.js +14 -6
  211. package/src/lib/mutation/runner.js +1 -1
  212. package/src/lib/mutation/subset.js +46 -9
  213. package/src/lib/plan/artifact-vocabulary.js +60 -0
  214. package/src/lib/plan/checks.js +234 -0
  215. package/src/lib/plan/plan-parser.js +209 -0
  216. package/src/lib/plan/plan-template.js +18 -0
  217. package/src/lib/review-reports.js +222 -0
  218. package/src/lib/risk-detector.js +134 -135
  219. package/src/lib/specs/adr-index.js +50 -69
  220. package/src/lib/specs/design-seed.js +18 -0
  221. package/src/lib/specs/frontmatter.js +44 -0
  222. package/src/lib/specs/gfm-table.js +17 -0
  223. package/src/lib/specs/old-format.js +68 -0
  224. package/src/lib/specs/revocations.js +37 -95
  225. package/src/lib/specs/screen-checklist.js +18 -17
  226. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  227. package/src/lib/stack/stack-profile.js +1 -1
  228. package/src/lib/stack/stack-scan.js +267 -0
  229. package/src/lib/standards/registry.js +43 -0
  230. package/src/lib/standards/sections.js +58 -0
  231. package/src/lib/tasks/eval-node.js +4 -12
  232. package/src/lib/tasks/test-nodes.js +1 -74
  233. package/src/lib/tasks/test-runner.js +75 -117
  234. package/src/lib/telemetry/metrics.js +8 -69
  235. package/src/lib/telemetry/telemetry-reader.js +5 -118
  236. package/src/lib/telemetry/verify-event.js +5 -7
  237. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  238. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  239. package/src/lib/validators/reuse/reuse-validator.js +24 -48
  240. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  241. package/src/lib/validators/traceability-check.js +85 -129
  242. package/src/lib/validators/validation-runner.js +8 -88
  243. package/src/lib/validators/verify-runner.js +134 -301
  244. package/src/lib/worktree/docker-audit.js +1 -2
  245. package/src/lib/worktree/fleet-scanner.js +25 -38
  246. package/src/scripts/setup-infra.js +42 -22
  247. package/src/utils/agents-installer.js +92 -13
  248. package/src/utils/claude-settings-manager.js +0 -35
  249. package/src/utils/file-copier.js +34 -6
  250. package/src/utils/hooks-installer.js +16 -9
  251. package/src/utils/skills-installer.js +1 -2
  252. package/framework/commands/commit.md +0 -166
  253. package/framework/evals/trace-schema.md +0 -67
  254. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  255. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  256. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  257. package/framework/hooks/shared/tasks-validator.js +0 -111
  258. package/framework/rubrics/backend.json +0 -41
  259. package/framework/rubrics/default.json +0 -36
  260. package/framework/rubrics/frontend.json +0 -36
  261. package/framework/rules/morph-task-tracking.md +0 -54
  262. package/framework/rules/morph-workflow.md +0 -244
  263. package/framework/schemas/tasks.schema.json +0 -79
  264. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  265. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  266. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  267. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  268. package/framework/templates/docs/proposal.md +0 -192
  269. package/framework/templates/docs/spec.md +0 -57
  270. package/framework/templates/docs/ui-components.md +0 -52
  271. package/framework/templates/docs/ui-design-system.md +0 -76
  272. package/framework/templates/docs/ui-flows.md +0 -167
  273. package/framework/templates/examples/spec-examples.md +0 -91
  274. package/framework/templates/feature/decisions.md +0 -124
  275. package/framework/templates/feature/recap.md +0 -145
  276. package/framework/templates/project/validate.js +0 -97
  277. package/framework/templates/tasks.template.json +0 -17
  278. package/src/commands/state/usecase.js +0 -143
  279. package/src/commands/tasks/dag.js +0 -181
  280. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  281. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  282. package/src/lib/agents/output-collector.js +0 -76
  283. package/src/lib/evaluator-report.js +0 -91
  284. package/src/lib/specs/acervo-format.js +0 -70
  285. package/src/lib/specs/acervo-lifecycle.js +0 -264
  286. package/src/lib/specs/done-criteria-check.js +0 -200
  287. package/src/lib/specs/usecase-check.js +0 -301
  288. package/src/lib/specs/usecase-promoter.js +0 -301
  289. package/src/lib/specs/usecase-registry.js +0 -237
  290. package/src/lib/tasks/dispatch-planner.js +0 -319
  291. package/src/lib/tasks/mermaid-renderer.js +0 -132
  292. package/src/lib/tasks/task-classifier.js +0 -30
  293. package/src/lib/tasks/task-parser.js +0 -209
  294. package/src/lib/tasks/tasks-json-parser.js +0 -296
  295. package/src/lib/tasks/test-scope.js +0 -597
  296. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  297. package/src/lib/validators/architecture/index.js +0 -7
  298. package/src/lib/validators/css/css-validator.js +0 -340
  299. package/src/lib/validators/css/index.js +0 -7
  300. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  301. package/src/lib/validators/design-system/index.js +0 -7
  302. package/src/lib/validators/score-aggregator.js +0 -244
  303. package/src/lib/validators/ui/index.js +0 -7
  304. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -1,75 +1,67 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { execFileSync } from 'child_process';
4
- import { existsSync, readFileSync } from 'fs';
4
+ import { existsSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, derivePhase } from '../../core/state/state-manager.js';
7
7
  import { unmetPrereqs } from '../../core/state/gate-order.js';
8
- import { evaluatorReportState, VERDICT_TOKEN } from '../../lib/evaluator-report.js';
8
+ import { reviewReports, reviewFileName, REVIEW_ROLES, VERDICT_TOKEN, REVIEWED_SHA_TOKEN } from '../../lib/review-reports.js';
9
+ import { shouldPauseGate } from '../../lib/risk-detector.js';
10
+ import { collectSignals, collectReviewRequirement } from './gate-check.js';
11
+ import { autoApprovalRefusal, printRefusal } from './auto-approval.js';
12
+ import { readTrustFromConfig } from './gate-decision.js';
9
13
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
10
- import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
11
14
  import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
15
+ import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
16
+ import { reportDotnetIgnores } from './advance.js';
12
17
 
13
- /**
14
- * The literal token `review-report.md` carries to declare which tree it
15
- * reviewed. Kept as one exported constant so the CLI parser, the warning text
16
- * and the `morph-review` skill can never drift apart (the A5 lesson).
17
- */
18
- export const REVIEWED_SHA_TOKEN = '**Reviewed-SHA:**';
18
+ export { REVIEWED_SHA_TOKEN };
19
19
 
20
- const REVIEWED_SHA_RE = /\*\*Reviewed-SHA:\*\*\s*([0-9a-f]{7,40})/i;
20
+ const SIGNED_OFF = { required: [], reports: {}, checks: { status: 'ok', total: 0, missing: [], notProven: [] } };
21
21
 
22
22
  /**
23
- * Is the feature's `review-report.md` still describing the current tree?
23
+ * Do the review reports still describe the current tree? Every report present
24
+ * is compared; the worst answer wins.
24
25
  *
25
- * @param {string} featureName
26
- * @returns {{ status: 'ok'|'stale'|'unstamped'|'skip', reviewed?: string, head?: string, ahead?: number }}
27
- * `skip` covers every case where the question cannot be answered honestly:
28
- * no report, no git repo, an unresolvable SHA. Never a false refusal.
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
29
  */
30
- function reviewStaleness(featureName) {
30
+ function reviewStaleness(reports) {
31
+ if (reports.verifier.status === 'missing') return { status: 'skip' };
31
32
  const cwd = process.cwd();
32
- const reportPath = join(cwd, '.morph', 'features', featureName, '4-review', 'review-report.md');
33
- if (!existsSync(reportPath)) return { status: 'skip' };
34
-
35
33
  let head;
36
34
  try {
37
35
  head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
38
- } catch {
39
- return { status: 'skip' }; // not a git repo (or no commits) — nothing to compare
40
- }
41
-
42
- let body = '';
43
- try {
44
- body = readFileSync(reportPath, 'utf8');
45
36
  } catch {
46
37
  return { status: 'skip' };
47
38
  }
48
39
 
49
- const stamped = body.match(REVIEWED_SHA_RE)?.[1];
50
- if (!stamped) return { status: 'unstamped', head };
51
-
52
- let reviewed;
53
- try {
54
- reviewed = execFileSync('git', ['rev-parse', '--verify', `${stamped}^{commit}`], { cwd, stdio: 'pipe' })
55
- .toString().trim();
56
- } catch {
57
- // A stamp git cannot resolve (rewritten history, a typo) is not evidence
58
- // of staleness — say "unstamped" rather than block on a guess.
59
- return { status: 'unstamped', head };
40
+ let unstamped = null;
41
+ for (const [role, report] of Object.entries(reports)) {
42
+ if (report.status === 'missing') continue;
43
+ const file = reviewFileName(role);
44
+ let reviewed = null;
45
+ try {
46
+ if (report.reviewedSha) {
47
+ reviewed = execFileSync('git', ['rev-parse', '--verify', `${report.reviewedSha}^{commit}`], { cwd, stdio: 'pipe' })
48
+ .toString().trim();
49
+ }
50
+ } catch {
51
+ reviewed = null;
52
+ }
53
+ if (!reviewed) {
54
+ unstamped ??= { status: 'unstamped', file, head };
55
+ continue;
56
+ }
57
+ if (!treeMovedSince(cwd, reviewed, head)) continue;
58
+ let ahead = 0;
59
+ try {
60
+ ahead = Number(execFileSync('git', ['rev-list', '--count', `${reviewed}..${head}`], { cwd, stdio: 'pipe' }).toString().trim()) || 0;
61
+ } catch { ahead = 0; }
62
+ return { status: 'stale', file, reviewed, head, ahead };
60
63
  }
61
- if (!treeMovedSince(cwd, reviewed, head)) return { status: 'ok', reviewed, head };
62
-
63
- let ahead = 0;
64
- try {
65
- ahead = execFileSync('git', ['rev-list', '--count', `${reviewed}..${head}`], { cwd, stdio: 'pipe' })
66
- .toString().trim();
67
- ahead = Number(ahead) || 0;
68
- } catch { ahead = 0; }
69
-
70
- // HEAD not descending from the reviewed commit (a rebase, a different branch)
71
- // is still "not the tree that was reviewed".
72
- return { status: 'stale', reviewed, head, ahead };
64
+ return unstamped ?? { status: 'ok', head };
73
65
  }
74
66
 
75
67
  /**
@@ -145,7 +137,7 @@ const program = new Command();
145
137
  /**
146
138
  * Valid approval gate names
147
139
  */
148
- const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
140
+ const VALID_GATES = ['plan', 'uiux', 'review'];
149
141
 
150
142
  /**
151
143
  * Approve action function - Mark a gate as approved
@@ -189,12 +181,13 @@ export async function approveCommand(featureName, gateName, options = {}) {
189
181
  // with nowhere to go. The way out already exists as `unapprove`; the
190
182
  // message just never named it. Naming it is the whole fix (no new flag).
191
183
  console.log(chalk.gray(`To approve it again, revoke it first: morph-spec unapprove ${featureName} ${gateName}\n`));
184
+ if (gateName === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
192
185
  return;
193
186
  }
194
187
 
195
188
  // Gate monotonicity: a gate may only be approved once its predecessors are
196
- // approved (proposal → plan → review; uiux requires proposal). Prevents the
197
- // lead-termometro shape — plan.approved with proposal pending. Policy-stamped
189
+ // approved (plan → review; uiux requires plan). Prevents the shape of a
190
+ // review approved with the plan still pending. Policy-stamped
198
191
  // gates count as approved, so chore/hotfix satisfy prereqs naturally.
199
192
  // Escape via --force for deliberate recovery.
200
193
  const unmet = unmetPrereqs(gateName, feature.approvalGates);
@@ -231,24 +224,14 @@ export async function approveCommand(featureName, gateName, options = {}) {
231
224
  approvedBy = 'auto';
232
225
  }
233
226
 
234
- // Hotfix backstop: Gate 3 (review) requires a genuine HUMAN signature even
235
- // in trust:auto — a work-type policy (forcedPauseGates in
236
- // src/core/work-types.js, mirrored by risk-detector's
237
- // FORCED_PAUSE_WORK_TYPES), enforced server-side rather than trusting the
238
- // calling skill. It gates on the RESOLVED approvedBy, NOT the raw --mode
239
- // string: every headless context (claude -p, CI, the Bash tool) runs
240
- // without a TTY, so an omitted --mode — or any --mode ≠ 'auto' — still
241
- // resolves to "auto" and would otherwise rubber-stamp the gate. A human
242
- // approval is --approver <name> or an interactive TTY ($USER); anything
243
- // resolving to "auto" is refused here.
244
- if (gateName === 'review' && feature.workType === 'hotfix' && approvedBy === 'auto') {
245
- console.error(chalk.red('\n❌ Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.'));
246
- console.error(chalk.gray(' Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) ao review-report via AskUserQuestion.\n'));
227
+ const refusal = autoApprovalRefusal({ cwd: process.cwd(), featureName, feature, gate: gateName, approvedBy });
228
+ if (refusal) {
229
+ printRefusal(chalk, refusal);
247
230
  process.exit(1);
248
231
  }
249
232
 
250
233
  // Gate 3 staleness backstop (D1): a review reviews a TREE, and the tree
251
- // moves. Observed: review-report.md written at b30ac6e, then T3 landed and
234
+ // moves. Observed: a review report written at b30ac6e, then T3 landed and
252
235
  // inverted the default of a business rule; the report stayed in the repo
253
236
  // claiming 976/976 tests (they were 982) and never mentioning T3. Nothing
254
237
  // detected that the review artifact had fallen behind the code it reviews.
@@ -257,93 +240,49 @@ export async function approveCommand(featureName, gateName, options = {}) {
257
240
  // A stamp that is BEHIND HEAD refuses. A MISSING stamp only warns — reports
258
241
  // written before this existed must not brick a legacy feature — but the
259
242
  // warning names the exact token to add, so it teaches instead of nagging.
243
+ const reports = gateName === 'review' ? reviewReports(process.cwd(), featureName) : null;
260
244
  if (gateName === 'review') {
261
- const stale = reviewStaleness(featureName);
245
+ const stale = reviewStaleness(reports);
262
246
  if (stale.status === 'stale') {
263
- console.error(chalk.red('\n❌ O `4-review/review-report.md` está desatualizado em relação ao código.'));
247
+ console.error(chalk.red(`\n❌ O \`4-review/${stale.file}\` está desatualizado em relação ao código.`));
264
248
  console.error(chalk.gray(` Revisado em: ${stale.reviewed.slice(0, 8)}`));
265
249
  console.error(chalk.gray(` HEAD atual: ${stale.head.slice(0, 8)}${stale.ahead ? ` (${stale.ahead} commit(s) depois da revisão)` : ''}`));
266
250
  console.error(chalk.gray(' Um review revisa uma ÁRVORE, e a árvore andou. Re-rode a `morph-review`'));
267
- console.error(chalk.gray(' (incluindo o avaliador independente) e atualize o `**Reviewed-SHA:**`.\n'));
251
+ console.error(chalk.gray(` e atualize o \`${REVIEWED_SHA_TOKEN}\` de cada relatório.\n`));
268
252
  process.exit(1);
269
253
  }
270
254
  if (stale.status === 'unstamped') {
271
- console.log(chalk.yellow('\n⚠️ O `review-report.md` não carrega `**Reviewed-SHA:**` — impossível saber se ele revisa o código atual.'));
272
- console.log(chalk.gray(` Adicione a linha \`**Reviewed-SHA:** ${stale.head}\` ao relatório para que o Gate 3 possa verificar.`));
273
- }
274
- }
275
-
276
- // Skipped-pause backstop: a gate whose recorded decision demanded a human
277
- // pause (gateDecisions[gate].pause === true) cannot then be stamped by a
278
- // non-human signature. The detection already existed — in gate-guard.js —
279
- // but that hook is registered for `Write|Edit|AskUserQuestion`, and a Bash
280
- // tool call fires no hook at all. Since MORPH.md requires gate approval to
281
- // go through THIS command and forbids hand-editing the state, the only path
282
- // the hook could ever see was the one the charter bans. The check belongs
283
- // where the stamp is applied, not beside it.
284
- //
285
- // Gated on the RESOLVED `approvedBy`, matching the hotfix backstop above:
286
- // an omitted --mode in a headless context also resolves to "auto" and must
287
- // not slip through. `--force` does NOT bypass this either — it is the
288
- // out-of-order recovery flag, a different concern. The way out is a human
289
- // signature, or rewriting the decision that asked for the pause.
290
- const decision = feature.gateDecisions?.[gateName];
291
- if (decision?.pause === true && approvedBy === 'auto') {
292
- console.error(chalk.red(`\n❌ O Gate "${gateName}" registrou pause:true — auto-aprovação não se aplica.`));
293
- console.error(chalk.gray(` Sinal: ${decision.reason || 'risco detectado'}`));
294
- console.error(chalk.gray(' A decisão gravada exigia pausa humana. Aprove com --approver <nome>, num'));
295
- console.error(chalk.gray(' terminal interativo, ou pela resposta humana via AskUserQuestion (state-sync).'));
296
- console.error(chalk.gray(' Se o sinal de risco já não vale, regrave a decisão:'));
297
- console.error(chalk.gray(` morph-spec gate-decision ${featureName} ${gateName} --pause false --reason "<motivo>"\n`));
298
- process.exit(1);
299
- }
300
-
301
- // The same backstop when no gate-check ran: the pause it WOULD record for
302
- // the screen checklist, by the predicate advance.js reads too. `--force`
303
- // does not bypass it, for the reason above.
304
- if (gateName === 'uiux' && approvedBy === 'auto') {
305
- const block = uiuxAutoApprovalBlock(process.cwd(), featureName, feature);
306
- if (block) {
307
- console.error(chalk.red(`\n❌ O Gate "uiux" não pode ser auto-aprovado — ${block}.`));
308
- console.error(chalk.gray(' Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com'));
309
- console.error(chalk.gray(' --approver <nome>, ou, se a pausa já foi decidida, grave a decisão:'));
310
- console.error(chalk.gray(` morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"\n`));
311
- process.exit(1);
255
+ console.log(chalk.yellow(`\n⚠️ O \`${stale.file}\` não carrega \`${REVIEWED_SHA_TOKEN}\` — impossível saber se ele revisa o código atual.`));
256
+ console.log(chalk.gray(` Adicione a linha \`${REVIEWED_SHA_TOKEN} ${stale.head}\` ao relatório para que o Gate 3 possa verificar.`));
312
257
  }
313
258
  }
314
259
 
315
260
  let selfAssessed = false;
316
261
  if (gateName === 'review') {
317
262
  if (feature.workType === 'hotfix') {
318
- // Hotfix Gate 3 does NOT require the independent evaluator: `morph-hotfix`
319
- // (SKILL.md §7) never dispatches `morph-evaluator` — speed over ceremony
320
- // is the point of a hotfix — and instead writes `4-review/review-report.md`
321
- // (diff + real test evidence) before asking the human to approve. Requiring
322
- // evaluator-report.md here made the skill's own documented flow unable to
323
- // clear its own Gate 3 (the only escape was --self-assessed, which stamps
324
- // a degradation for a report that was never supposed to exist). The human
325
- // signature is already enforced above (line ~215) — this only checks the
326
- // artifact the skill actually produces.
327
- const reviewReport = join(process.cwd(), '.morph', 'features', featureName, '4-review', 'review-report.md');
328
- if (!existsSync(reviewReport)) {
329
- console.error(chalk.red('\n❌ Gate 3 de hotfix exige `4-review/review-report.md` — não existe.'));
330
- console.error(chalk.gray(' É o artefato que a `morph-hotfix` produz (diff do fix + evidência real de'));
331
- console.error(chalk.gray(' teste do `morph-spec verify`), não o `evaluator-report.md` do avaliador'));
332
- console.error(chalk.gray(' independente — hotfix nunca despacha o avaliador. Escreva o relatório antes'));
263
+ // Hotfix never dispatches: the orchestrator writes `verifier.md` itself
264
+ // (diff + real verify evidence); the human signature above decides.
265
+ if (reports.verifier.status === 'missing') {
266
+ console.error(chalk.red('\n❌ Gate 3 de hotfix exige `4-review/verifier.md` — não existe.'));
267
+ console.error(chalk.gray(' No hotfix o orquestrador o escreve (diff do fix + evidência real do'));
268
+ console.error(chalk.gray(' `morph-spec verify`) sem despachar ninguém. Escreva o relatório antes'));
333
269
  console.error(chalk.gray(' de aprovar; a assinatura humana já é exigida pelo backstop acima.\n'));
334
270
  process.exit(1);
335
271
  }
336
272
  } else {
337
- const report = evaluatorReportState(process.cwd(), featureName);
338
- if (report.status !== 'ok') {
273
+ const { required } = collectReviewRequirement(process.cwd(), feature);
274
+ const unusable = required.filter((role) => reports[role].status !== 'ok');
275
+ if (unusable.length) {
339
276
  if (!options.selfAssessed) {
340
- if (report.status === 'missing') {
341
- console.error(chalk.red('\n❌ Gate 3 exige o avaliador independente — `4-review/evaluator-report.md` não existe.'));
342
- } else {
343
- console.error(chalk.red(`\n❌ Gate 3: o \`4-review/evaluator-report.md\` não tem \`${VERDICT_TOKEN} SHIP|FIX\`.`));
344
- console.error(chalk.gray(' Peça ao avaliador o relatório completo (morph-review §4) ou retome-o por SendMessage.'));
277
+ console.error(chalk.red(`\n❌ Gate 3 exige ${required.map((r) => `\`4-review/${reviewFileName(r)}\``).join(', ')}.`));
278
+ for (const role of unusable) {
279
+ const { status } = reports[role];
280
+ const what = status === 'missing' ? 'não existe'
281
+ : status === 'partial' ? 'está PARCIAL — retome o mesmo agente por SendMessage'
282
+ : `não tem \`${VERDICT_TOKEN} SHIP|FIX\``;
283
+ console.error(chalk.gray(` ${reviewFileName(role)} ${what}.`));
345
284
  }
346
- console.error(chalk.gray(' O veredito vem do avaliador (persona `evaluator`, contexto fresco), nunca de quem implementou.'));
285
+ console.error(chalk.gray(` Os relatórios vêm de ${unusable.map((r) => `\`${REVIEW_ROLES[r].persona}\``).join(', ')} em contexto fresco, nunca de quem implementou.`));
347
286
  console.error(chalk.gray(' Se o dispatch for impossível nesta sessão, aprove com --self-assessed —'));
348
287
  console.error(chalk.gray(' a degradação fica registrada no feature.json e aparece no `list`/`fleet`.\n'));
349
288
  process.exit(1);
@@ -351,9 +290,26 @@ export async function approveCommand(featureName, gateName, options = {}) {
351
290
  selfAssessed = true;
352
291
  }
353
292
 
354
- if (report.status === 'ok' && report.superseded.length > 0) {
355
- console.log(chalk.yellow(`\n⚠️ O relatório do avaliador tem ${report.superseded.length + 1} vereditos (append-only).`));
356
- console.log(chalk.gray(` Vigente: ${report.verdict}, ${report.blockers.length} bloqueador(es). Superados: ${report.superseded.join(', ')}\n`));
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.
296
+ if (approvedBy === 'auto') {
297
+ const signals = collectSignals(featureName, feature, process.cwd(), 'review');
298
+ if (selfAssessed) signals.reviewReports = SIGNED_OFF;
299
+ const { pause, reason } = shouldPauseGate({ trust: readTrustFromConfig() }, signals);
300
+ if (pause) {
301
+ console.error(chalk.red(`\n❌ Gate 3 não pode ser auto-aprovado — ${reason}.`));
302
+ console.error(chalk.gray(' Corrija e rode de novo, ou leve a decisão ao humano (--approver <nome>).\n'));
303
+ process.exit(1);
304
+ }
305
+ }
306
+
307
+ for (const role of required) {
308
+ const r = reports[role];
309
+ if (r.status === 'ok' && r.superseded.length > 0) {
310
+ console.log(chalk.yellow(`\n⚠️ ${reviewFileName(role)} tem ${r.superseded.length + 1} vereditos (append-only).`));
311
+ console.log(chalk.gray(` Vigente: ${r.verdict}, ${r.blockers.length} bloqueador(es). Superados: ${r.superseded.join(', ')}\n`));
312
+ }
357
313
  }
358
314
  }
359
315
  }
@@ -367,7 +323,7 @@ export async function approveCommand(featureName, gateName, options = {}) {
367
323
 
368
324
  // Fleet handoff (mirrors advance.js): approving a gate is exactly when a
369
325
  // feature hands over between sessions too — `approve` is the real path
370
- // Gate 2 took in the field (`approve <f> plan --mode auto`), not just
326
+ // the plan gate took in the field (`approve <f> plan --mode auto`), not just
371
327
  // `advance`, so a handoff.md left stale after approve was the common
372
328
  // case, not the edge case. Same helper, same fail-open contract: a
373
329
  // regeneration failure must never fail the approval itself.
@@ -377,11 +333,12 @@ export async function approveCommand(featureName, gateName, options = {}) {
377
333
  } catch { /* fail-open */ }
378
334
 
379
335
  if (selfAssessed) {
380
- console.log(chalk.yellow('\n⚠️ Gate 3 aprovado SEM avaliador independente (--self-assessed).'));
336
+ console.log(chalk.yellow('\n⚠️ Gate 3 aprovado SEM os relatórios de review independentes (--self-assessed).'));
381
337
  console.log(chalk.gray(' Registrado em feature.json → approvalGates.review.selfAssessed.'));
382
338
  }
383
339
 
384
340
  console.log(chalk.green(`\n✅ Gate "${gateName}" approved for feature: ${featureName}`));
341
+ if (gateName === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
385
342
  console.log(chalk.gray(`Workflow can now proceed past ${gateName} phase\n`));
386
343
 
387
344
  } catch (error) {
@@ -2,9 +2,7 @@ import chalk from 'chalk';
2
2
  import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { join, resolve, sep } from 'path';
4
4
  import { execFileSync } from 'child_process';
5
- import { promoteUseCases, SPECS_USECASES_PATHSPEC } from '../../lib/specs/usecase-promoter.js';
6
- import { applySpecRevocations } from '../../lib/specs/acervo-lifecycle.js';
7
- import { ADR_INDEX_PATHSPEC } from '../../lib/specs/adr-index.js';
5
+ import { ADR_INDEX_PATHSPEC, updateAdrIndex } from '../../lib/specs/adr-index.js';
8
6
  import { isValidFeatureName } from '../project/worktree.js';
9
7
 
10
8
  /** Refuse a move whose resolved target escapes `scopeDir`. */
@@ -65,10 +63,7 @@ function readReviewApproved(fjPath) {
65
63
  * @param {string} feature
66
64
  * @param {{ cwd?: string, force?: boolean, git?: boolean }} [opts]
67
65
  * @returns {{ ok: boolean, error?: string, archived?: boolean, alreadyArchived?: boolean,
68
- * committed?: boolean, staged?: boolean, archivePath?: string, promotedUseCases?: string[],
69
- * refusedUseCases?: Array<{id: string, incoming: string, existing: string, existingFeature: string, message: string}>,
70
- * keptSuperseded?: Array<{id: string, file: string, supersededBy: string|null, scope?: string}>,
71
- * supersededUseCases?: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
66
+ * committed?: boolean, staged?: boolean, archivePath?: string,
72
67
  * revocationProblems?: Array<{id: string|null, reason: string, message: string}>,
73
68
  * adrIndex?: {added: string[], revoked: string[], narrowed: string[]}|null }}
74
69
  */
@@ -94,15 +89,8 @@ export function runArchive(feature, opts = {}) {
94
89
  const archivePathspec = join('.morph', 'archive', feature);
95
90
 
96
91
  // Idempotent short-circuit: already archived here. Checked FIRST so a re-run
97
- // after a partial finish reports "already done", never "not found".
98
- //
99
- // Consequence, deliberate: this short-circuits BEFORE the use-case promotion
100
- // below, so a promotion that degraded fail-open is NOT retried by re-running
101
- // `finish`. That is the price of "closing a feature never fails" — an archived
102
- // feature stays archived, and the only signal is an absent `promotedUseCases`
103
- // in the output. Recovering means copying the UCs into `.morph/specs/usecases/`
104
- // by hand; making this retry would mean re-entering the archive path for an
105
- // already-closed feature, which is a worse trade.
92
+ // after a partial finish reports "already done", never "not found"; an ADR
93
+ // index update that degraded fail-open is therefore not retried.
106
94
  if (existsSync(archivePath)) {
107
95
  return { ok: true, alreadyArchived: true, archived: false, archivePath };
108
96
  }
@@ -156,63 +144,29 @@ export function runArchive(feature, opts = {}) {
156
144
  } catch { /* non-fatal — the move is what matters */ }
157
145
  }
158
146
 
159
- // 3) Promote the feature's use cases into the project's permanent archive
160
- // (.morph/specs/usecases/). Read from the ALREADY-MOVED folder so there is
161
- // a single source path, and land inside the SAME scoped commit below — the
162
- // archive travels with the integration, exactly like the archived state
163
- // (MORPH.md §2.2). Fail-open: any failure degrades to "promoted nothing",
164
- // never to a refused close.
165
- let promotedUseCases = [];
166
- let refusedUseCases = [];
167
- let keptSuperseded = [];
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.
150
+ let adrIndex = { changed: false, added: [], revoked: [], narrowed: [], problems: [] };
168
151
  try {
169
- const { promoted, refused, keptSuperseded: kept } = promoteUseCases(cwd, archivePath);
170
- promotedUseCases = promoted;
171
- refusedUseCases = refused;
172
- keptSuperseded = kept;
173
- if (promoted.length && useGit && staged) {
174
- // This staging exists for the NEW files: promoted use cases are untracked,
175
- // and the scoped `commit -- <pathspec>` below only knows paths git already
176
- // tracks. The replaced-by-ID deletions are tracked, so that same commit
177
- // would carry them even without this line (verified by mutating it away) —
178
- // `-A` is belt-and-braces, but dropping the `git add` entirely would
179
- // silently promote nothing on a first delivery.
180
- try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
181
- }
182
- } catch { /* promotion must never block archiving */ }
183
-
184
- // 3b) Apply what the feature's spec.md declared it revokes: acervo use cases
185
- // turn `superseded`, and the ADR index gains/loses files. After the
186
- // promotion, so a substitute promoted just now is already in place (and
187
- // one it refused is known not to be); same commit, same fail-open contract.
188
- let lifecycle = { superseded: [], adrIndex: null, problems: [] };
189
- try {
190
- lifecycle = applySpecRevocations(cwd, archivePath, { refused: refusedUseCases });
191
- } catch { /* revocation must never block archiving */ }
192
- const acervoChanged = promotedUseCases.length > 0 || lifecycle.superseded.length > 0;
193
- const adrIndexChanged = lifecycle.adrIndex?.changed === true;
194
- if (useGit && staged) {
195
- if (lifecycle.superseded.length) {
196
- try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
197
- }
198
- if (adrIndexChanged) {
199
- try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
200
- }
152
+ adrIndex = updateAdrIndex(cwd, archivePath);
153
+ } catch { /* fail-open */ }
154
+ if (useGit && staged && adrIndex.changed) {
155
+ try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
201
156
  }
202
157
 
203
158
  // 4) Commit the move — SCOPED to this feature's pathspec, NEVER `commit -a`
204
159
  // (same anti-sweep discipline as `finish`'s worktree teardown). This is what
205
160
  // lets the archived state travel with the branch on `finish` (archive-before-merge).
206
- // The acervo pathspecs join only when something under them changed: git
207
- // refuses a pathspec that matches nothing, and that refusal would leave the
208
- // tree dirty and break the checkout/merge that follows.
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.
209
164
  let committed = false;
210
165
  if (useGit && staged) {
211
166
  const pathspecs = [
212
167
  featPathspec,
213
168
  archivePathspec,
214
- ...(acervoChanged ? [SPECS_USECASES_PATHSPEC] : []),
215
- ...(adrIndexChanged ? [ADR_INDEX_PATHSPEC] : []),
169
+ ...(adrIndex.changed ? [ADR_INDEX_PATHSPEC] : []),
216
170
  ];
217
171
  try {
218
172
  execFileSync('git', ['commit', '-m', `morph-spec archive: ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
@@ -236,13 +190,10 @@ export function runArchive(feature, opts = {}) {
236
190
  } catch { /* index is derivable — non-fatal */ }
237
191
  }
238
192
 
239
- const adr = lifecycle.adrIndex;
240
193
  return {
241
- ok: true, archived: true, committed, staged, archivePath, promotedUseCases, refusedUseCases,
242
- keptSuperseded,
243
- supersededUseCases: lifecycle.superseded,
244
- revocationProblems: lifecycle.problems,
245
- adrIndex: adrIndexChanged ? { added: adr.added, revoked: adr.revoked, narrowed: adr.narrowed } : null,
194
+ ok: true, archived: true, committed, staged, archivePath,
195
+ revocationProblems: adrIndex.problems,
196
+ adrIndex: adrIndex.changed ? { added: adrIndex.added, revoked: adrIndex.revoked, narrowed: adrIndex.narrowed } : null,
246
197
  };
247
198
  }
248
199
 
@@ -281,27 +232,12 @@ export async function archiveCommand(featureName, options = {}) {
281
232
 
282
233
  console.log(chalk.green(`\n📦 Archived "${featureName}" → .morph/archive/${featureName}/`));
283
234
  console.log(chalk.gray(` status: completed, archivedAt stamped in feature.json.`));
284
- if (result.promotedUseCases?.length) {
285
- console.log(chalk.gray(` ${result.promotedUseCases.length} use case(s) promoted → .morph/specs/usecases/ (status: implemented).`));
286
- }
287
- if (result.refusedUseCases?.length) {
288
- console.log(chalk.yellow(`\n⚠️ ${result.refusedUseCases.length} use case(s) NOT promoted — numbering collision:`));
289
- for (const r of result.refusedUseCases) {
290
- console.log(chalk.yellow(` ${r.message}`));
291
- }
292
- }
293
- for (const k of result.keptSuperseded ?? []) {
294
- console.log(chalk.yellow(` ${k.id} continua superseded (superseded_by: ${k.supersededBy ?? '?'}) — a cópia promovida não trazia o status, e o acervo o manteve.`));
295
- }
296
- for (const s of result.supersededUseCases ?? []) {
297
- console.log(chalk.gray(` ${s.id} → status: superseded (superseded_by: ${s.supersededBy}${s.scope ? `; ainda vale em: ${s.scope}` : ''}).`));
298
- }
299
235
  if (result.adrIndex) {
300
236
  const { added, revoked, narrowed } = result.adrIndex;
301
237
  console.log(chalk.gray(` ADR index (.morph/specs/adrs/): +${added.length} −${revoked.length} ~${narrowed.length}.`));
302
238
  }
303
239
  if (result.revocationProblems?.length) {
304
- console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do spec.md NÃO aplicada(s):`));
240
+ console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do plan.md NÃO aplicada(s):`));
305
241
  for (const p of result.revocationProblems) {
306
242
  console.log(chalk.yellow(` ${p.message}`));
307
243
  }
@@ -0,0 +1,68 @@
1
+ import { planAutoApprovalBlock } from './gate-check.js';
2
+ import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
3
+
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}
10
+ */
11
+ export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedBy }) {
12
+ if (approvedBy !== 'auto' || !gate) return null;
13
+
14
+ if (gate === 'review' && feature.workType === 'hotfix') {
15
+ return {
16
+ title: 'Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.',
17
+ hints: ['Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) via AskUserQuestion.'],
18
+ };
19
+ }
20
+
21
+ const decision = feature.gateDecisions?.[gate];
22
+ if (decision?.pause === true) {
23
+ return {
24
+ title: `O Gate "${gate}" registrou pause:true — auto-aprovação não se aplica.`,
25
+ hints: [
26
+ `Sinal: ${decision.reason || 'risco detectado'}`,
27
+ 'A decisão gravada exigia pausa humana. Aprove com --approver <nome>, num',
28
+ 'terminal interativo, ou pela resposta humana via AskUserQuestion (state-sync).',
29
+ 'Se o sinal de risco já não vale, regrave a decisão:',
30
+ `morph-spec gate-decision ${featureName} ${gate} --pause false --reason "<motivo>"`,
31
+ ],
32
+ };
33
+ }
34
+
35
+ if (gate === 'plan') {
36
+ const block = planAutoApprovalBlock(cwd, featureName);
37
+ if (block) {
38
+ return {
39
+ title: `O gate "plan" não pode ser auto-aprovado — ${block}.`,
40
+ hints: ['Com plan-check verde, rode de novo; ou aprove com --approver <nome>. O --force não dispensa o checks.json.'],
41
+ };
42
+ }
43
+ }
44
+
45
+ if (gate === 'uiux') {
46
+ const block = uiuxAutoApprovalBlock(cwd, featureName, feature);
47
+ if (block) {
48
+ return {
49
+ title: `O Gate "uiux" não pode ser auto-aprovado — ${block}.`,
50
+ hints: [
51
+ 'Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com',
52
+ '--approver <nome>, ou, se a pausa já foi decidida, grave a decisão:',
53
+ `morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"`,
54
+ 'O --force não dispensa o checklist.',
55
+ ],
56
+ };
57
+ }
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ export function printRefusal(chalk, refusal) {
64
+ console.error(chalk.red(`\n❌ ${refusal.title}`));
65
+ refusal.hints.forEach((line, i) => {
66
+ console.error(chalk.gray(` ${line}${i === refusal.hints.length - 1 ? '\n' : ''}`));
67
+ });
68
+ }