@polymorphism-tech/morph-spec 8.47.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 (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  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 +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -1,78 +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
9
  import { shouldPauseGate } from '../../lib/risk-detector.js';
10
- import { collectSignals } from './gate-check.js';
10
+ import { collectSignals, collectReviewRequirement } from './gate-check.js';
11
+ import { autoApprovalRefusal, printRefusal } from './auto-approval.js';
11
12
  import { readTrustFromConfig } from './gate-decision.js';
12
13
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
13
- import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
14
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';
15
17
 
16
- /**
17
- * The literal token `review-report.md` carries to declare which tree it
18
- * reviewed. Kept as one exported constant so the CLI parser, the warning text
19
- * and the `morph-review` skill can never drift apart (the A5 lesson).
20
- */
21
- export const REVIEWED_SHA_TOKEN = '**Reviewed-SHA:**';
18
+ export { REVIEWED_SHA_TOKEN };
22
19
 
23
- 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: [] } };
24
21
 
25
22
  /**
26
- * 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.
27
25
  *
28
- * @param {string} featureName
29
- * @returns {{ status: 'ok'|'stale'|'unstamped'|'skip', reviewed?: string, head?: string, ahead?: number }}
30
- * `skip` covers every case where the question cannot be answered honestly:
31
- * 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.
32
29
  */
33
- function reviewStaleness(featureName) {
30
+ function reviewStaleness(reports) {
31
+ if (reports.verifier.status === 'missing') return { status: 'skip' };
34
32
  const cwd = process.cwd();
35
- const reportPath = join(cwd, '.morph', 'features', featureName, '4-review', 'review-report.md');
36
- if (!existsSync(reportPath)) return { status: 'skip' };
37
-
38
33
  let head;
39
34
  try {
40
35
  head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
41
- } catch {
42
- return { status: 'skip' }; // not a git repo (or no commits) — nothing to compare
43
- }
44
-
45
- let body = '';
46
- try {
47
- body = readFileSync(reportPath, 'utf8');
48
36
  } catch {
49
37
  return { status: 'skip' };
50
38
  }
51
39
 
52
- const stamped = body.match(REVIEWED_SHA_RE)?.[1];
53
- if (!stamped) return { status: 'unstamped', head };
54
-
55
- let reviewed;
56
- try {
57
- reviewed = execFileSync('git', ['rev-parse', '--verify', `${stamped}^{commit}`], { cwd, stdio: 'pipe' })
58
- .toString().trim();
59
- } catch {
60
- // A stamp git cannot resolve (rewritten history, a typo) is not evidence
61
- // of staleness — say "unstamped" rather than block on a guess.
62
- 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 };
63
63
  }
64
- if (!treeMovedSince(cwd, reviewed, head)) return { status: 'ok', reviewed, head };
65
-
66
- let ahead = 0;
67
- try {
68
- ahead = execFileSync('git', ['rev-list', '--count', `${reviewed}..${head}`], { cwd, stdio: 'pipe' })
69
- .toString().trim();
70
- ahead = Number(ahead) || 0;
71
- } catch { ahead = 0; }
72
-
73
- // HEAD not descending from the reviewed commit (a rebase, a different branch)
74
- // is still "not the tree that was reviewed".
75
- return { status: 'stale', reviewed, head, ahead };
64
+ return unstamped ?? { status: 'ok', head };
76
65
  }
77
66
 
78
67
  /**
@@ -148,7 +137,7 @@ const program = new Command();
148
137
  /**
149
138
  * Valid approval gate names
150
139
  */
151
- const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
140
+ const VALID_GATES = ['plan', 'uiux', 'review'];
152
141
 
153
142
  /**
154
143
  * Approve action function - Mark a gate as approved
@@ -192,12 +181,13 @@ export async function approveCommand(featureName, gateName, options = {}) {
192
181
  // with nowhere to go. The way out already exists as `unapprove`; the
193
182
  // message just never named it. Naming it is the whole fix (no new flag).
194
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 }));
195
185
  return;
196
186
  }
197
187
 
198
188
  // Gate monotonicity: a gate may only be approved once its predecessors are
199
- // approved (proposal → plan → review; uiux requires proposal). Prevents the
200
- // 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
201
191
  // gates count as approved, so chore/hotfix satisfy prereqs naturally.
202
192
  // Escape via --force for deliberate recovery.
203
193
  const unmet = unmetPrereqs(gateName, feature.approvalGates);
@@ -234,24 +224,14 @@ export async function approveCommand(featureName, gateName, options = {}) {
234
224
  approvedBy = 'auto';
235
225
  }
236
226
 
237
- // Hotfix backstop: Gate 3 (review) requires a genuine HUMAN signature even
238
- // in trust:auto — a work-type policy (forcedPauseGates in
239
- // src/core/work-types.js, mirrored by risk-detector's
240
- // FORCED_PAUSE_WORK_TYPES), enforced server-side rather than trusting the
241
- // calling skill. It gates on the RESOLVED approvedBy, NOT the raw --mode
242
- // string: every headless context (claude -p, CI, the Bash tool) runs
243
- // without a TTY, so an omitted --mode — or any --mode ≠ 'auto' — still
244
- // resolves to "auto" and would otherwise rubber-stamp the gate. A human
245
- // approval is --approver <name> or an interactive TTY ($USER); anything
246
- // resolving to "auto" is refused here.
247
- if (gateName === 'review' && feature.workType === 'hotfix' && approvedBy === 'auto') {
248
- console.error(chalk.red('\n❌ Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.'));
249
- 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);
250
230
  process.exit(1);
251
231
  }
252
232
 
253
233
  // Gate 3 staleness backstop (D1): a review reviews a TREE, and the tree
254
- // 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
255
235
  // inverted the default of a business rule; the report stayed in the repo
256
236
  // claiming 976/976 tests (they were 982) and never mentioning T3. Nothing
257
237
  // detected that the review artifact had fallen behind the code it reviews.
@@ -260,93 +240,49 @@ export async function approveCommand(featureName, gateName, options = {}) {
260
240
  // A stamp that is BEHIND HEAD refuses. A MISSING stamp only warns — reports
261
241
  // written before this existed must not brick a legacy feature — but the
262
242
  // warning names the exact token to add, so it teaches instead of nagging.
243
+ const reports = gateName === 'review' ? reviewReports(process.cwd(), featureName) : null;
263
244
  if (gateName === 'review') {
264
- const stale = reviewStaleness(featureName);
245
+ const stale = reviewStaleness(reports);
265
246
  if (stale.status === 'stale') {
266
- 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.`));
267
248
  console.error(chalk.gray(` Revisado em: ${stale.reviewed.slice(0, 8)}`));
268
249
  console.error(chalk.gray(` HEAD atual: ${stale.head.slice(0, 8)}${stale.ahead ? ` (${stale.ahead} commit(s) depois da revisão)` : ''}`));
269
250
  console.error(chalk.gray(' Um review revisa uma ÁRVORE, e a árvore andou. Re-rode a `morph-review`'));
270
- 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`));
271
252
  process.exit(1);
272
253
  }
273
254
  if (stale.status === 'unstamped') {
274
- console.log(chalk.yellow('\n⚠️ O `review-report.md` não carrega `**Reviewed-SHA:**` — impossível saber se ele revisa o código atual.'));
275
- console.log(chalk.gray(` Adicione a linha \`**Reviewed-SHA:** ${stale.head}\` ao relatório para que o Gate 3 possa verificar.`));
276
- }
277
- }
278
-
279
- // Skipped-pause backstop: a gate whose recorded decision demanded a human
280
- // pause (gateDecisions[gate].pause === true) cannot then be stamped by a
281
- // non-human signature. The detection already existed — in gate-guard.js —
282
- // but that hook is registered for `Write|Edit|AskUserQuestion`, and a Bash
283
- // tool call fires no hook at all. Since MORPH.md requires gate approval to
284
- // go through THIS command and forbids hand-editing the state, the only path
285
- // the hook could ever see was the one the charter bans. The check belongs
286
- // where the stamp is applied, not beside it.
287
- //
288
- // Gated on the RESOLVED `approvedBy`, matching the hotfix backstop above:
289
- // an omitted --mode in a headless context also resolves to "auto" and must
290
- // not slip through. `--force` does NOT bypass this either — it is the
291
- // out-of-order recovery flag, a different concern. The way out is a human
292
- // signature, or rewriting the decision that asked for the pause.
293
- const decision = feature.gateDecisions?.[gateName];
294
- if (decision?.pause === true && approvedBy === 'auto') {
295
- console.error(chalk.red(`\n❌ O Gate "${gateName}" registrou pause:true — auto-aprovação não se aplica.`));
296
- console.error(chalk.gray(` Sinal: ${decision.reason || 'risco detectado'}`));
297
- console.error(chalk.gray(' A decisão gravada exigia pausa humana. Aprove com --approver <nome>, num'));
298
- console.error(chalk.gray(' terminal interativo, ou pela resposta humana via AskUserQuestion (state-sync).'));
299
- console.error(chalk.gray(' Se o sinal de risco já não vale, regrave a decisão:'));
300
- console.error(chalk.gray(` morph-spec gate-decision ${featureName} ${gateName} --pause false --reason "<motivo>"\n`));
301
- process.exit(1);
302
- }
303
-
304
- // The same backstop when no gate-check ran: the pause it WOULD record for
305
- // the screen checklist, by the predicate advance.js reads too. `--force`
306
- // does not bypass it, for the reason above.
307
- if (gateName === 'uiux' && approvedBy === 'auto') {
308
- const block = uiuxAutoApprovalBlock(process.cwd(), featureName, feature);
309
- if (block) {
310
- console.error(chalk.red(`\n❌ O Gate "uiux" não pode ser auto-aprovado — ${block}.`));
311
- console.error(chalk.gray(' Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com'));
312
- console.error(chalk.gray(' --approver <nome>, ou, se a pausa já foi decidida, grave a decisão:'));
313
- console.error(chalk.gray(` morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"\n`));
314
- 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.`));
315
257
  }
316
258
  }
317
259
 
318
260
  let selfAssessed = false;
319
261
  if (gateName === 'review') {
320
262
  if (feature.workType === 'hotfix') {
321
- // Hotfix Gate 3 does NOT require the independent evaluator: `morph-hotfix`
322
- // (SKILL.md §7) never dispatches `morph-evaluator` — speed over ceremony
323
- // is the point of a hotfix — and instead writes `4-review/review-report.md`
324
- // (diff + real test evidence) before asking the human to approve. Requiring
325
- // evaluator-report.md here made the skill's own documented flow unable to
326
- // clear its own Gate 3 (the only escape was --self-assessed, which stamps
327
- // a degradation for a report that was never supposed to exist). The human
328
- // signature is already enforced above (line ~215) — this only checks the
329
- // artifact the skill actually produces.
330
- const reviewReport = join(process.cwd(), '.morph', 'features', featureName, '4-review', 'review-report.md');
331
- if (!existsSync(reviewReport)) {
332
- console.error(chalk.red('\n❌ Gate 3 de hotfix exige `4-review/review-report.md` — não existe.'));
333
- console.error(chalk.gray(' É o artefato que a `morph-hotfix` produz (diff do fix + evidência real de'));
334
- console.error(chalk.gray(' teste do `morph-spec verify`), não o `evaluator-report.md` do avaliador'));
335
- 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'));
336
269
  console.error(chalk.gray(' de aprovar; a assinatura humana já é exigida pelo backstop acima.\n'));
337
270
  process.exit(1);
338
271
  }
339
272
  } else {
340
- const report = evaluatorReportState(process.cwd(), featureName);
341
- 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) {
342
276
  if (!options.selfAssessed) {
343
- if (report.status === 'missing') {
344
- console.error(chalk.red('\n❌ Gate 3 exige o avaliador independente — `4-review/evaluator-report.md` não existe.'));
345
- } else {
346
- console.error(chalk.red(`\n❌ Gate 3: o \`4-review/evaluator-report.md\` não tem \`${VERDICT_TOKEN} SHIP|FIX\`.`));
347
- 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}.`));
348
284
  }
349
- 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.`));
350
286
  console.error(chalk.gray(' Se o dispatch for impossível nesta sessão, aprove com --self-assessed —'));
351
287
  console.error(chalk.gray(' a degradação fica registrada no feature.json e aparece no `list`/`fleet`.\n'));
352
288
  process.exit(1);
@@ -356,10 +292,10 @@ export async function approveCommand(featureName, gateName, options = {}) {
356
292
 
357
293
  // Sem gate-check prévio não há `gateDecisions.review` para o backstop acima morder; em
358
294
  // campo, ships passaram assim com verify vermelho ou ausente. A auto-aprovação recalcula
359
- // a decisão pela MESMA régua do gate-check. --self-assessed já dispensou o veredito.
295
+ // a decisão pela MESMA régua do gate-check. --self-assessed já dispensou os relatórios.
360
296
  if (approvedBy === 'auto') {
361
297
  const signals = collectSignals(featureName, feature, process.cwd(), 'review');
362
- if (selfAssessed) signals.evaluatorVerdict = { status: 'ok', verdict: 'SHIP', blockers: 0, untyped: 0 };
298
+ if (selfAssessed) signals.reviewReports = SIGNED_OFF;
363
299
  const { pause, reason } = shouldPauseGate({ trust: readTrustFromConfig() }, signals);
364
300
  if (pause) {
365
301
  console.error(chalk.red(`\n❌ Gate 3 não pode ser auto-aprovado — ${reason}.`));
@@ -368,9 +304,12 @@ export async function approveCommand(featureName, gateName, options = {}) {
368
304
  }
369
305
  }
370
306
 
371
- if (report.status === 'ok' && report.superseded.length > 0) {
372
- console.log(chalk.yellow(`\n⚠️ O relatório do avaliador tem ${report.superseded.length + 1} vereditos (append-only).`));
373
- console.log(chalk.gray(` Vigente: ${report.verdict}, ${report.blockers.length} bloqueador(es). Superados: ${report.superseded.join(', ')}\n`));
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
+ }
374
313
  }
375
314
  }
376
315
  }
@@ -384,7 +323,7 @@ export async function approveCommand(featureName, gateName, options = {}) {
384
323
 
385
324
  // Fleet handoff (mirrors advance.js): approving a gate is exactly when a
386
325
  // feature hands over between sessions too — `approve` is the real path
387
- // 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
388
327
  // `advance`, so a handoff.md left stale after approve was the common
389
328
  // case, not the edge case. Same helper, same fail-open contract: a
390
329
  // regeneration failure must never fail the approval itself.
@@ -394,11 +333,12 @@ export async function approveCommand(featureName, gateName, options = {}) {
394
333
  } catch { /* fail-open */ }
395
334
 
396
335
  if (selfAssessed) {
397
- 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).'));
398
337
  console.log(chalk.gray(' Registrado em feature.json → approvalGates.review.selfAssessed.'));
399
338
  }
400
339
 
401
340
  console.log(chalk.green(`\n✅ Gate "${gateName}" approved for feature: ${featureName}`));
341
+ if (gateName === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
402
342
  console.log(chalk.gray(`Workflow can now proceed past ${gateName} phase\n`));
403
343
 
404
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
+ }