@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
@@ -7,6 +7,8 @@ import { WORK_TYPES, DEFAULT_WORK_TYPE, POLICY_APPROVER_PREFIX, isValidWorkType,
7
7
  import { detectWorkType } from '../../lib/work-type-detector.js';
8
8
  import { runWorktreeSetup, materializeState, isGitRepo, branchExists, buildBranchName } from '../project/worktree.js';
9
9
  import { detectDefaultBranch } from '../../lib/git/default-branch.js';
10
+ import { writePlanSkeleton } from '../../lib/plan/plan-template.js';
11
+ import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
10
12
 
11
13
  /**
12
14
  * Common flow (no `--worktree`): still isolate the feature on its own branch
@@ -140,7 +142,7 @@ function resolveWorkType(options, now) {
140
142
  * worktree on branch `morph/{feature}`.
141
143
  *
142
144
  * Replaces the implicit birth via the state-sync hook on the first Write —
143
- * the skills call this at proposal start so the authoritative state exists
145
+ * the skills call this at plan start so the authoritative state exists
144
146
  * with the full canonical shape before any artifact lands. Idempotent: an
145
147
  * existing feature is reported and left untouched — no re-stamping, no
146
148
  * re-scaffolding, no worktree setup.
@@ -211,6 +213,8 @@ export async function createCommand(featureName, options = {}) {
211
213
 
212
214
  let worktreeResult = null;
213
215
  let branchResult = null;
216
+ let planSkeleton = null;
217
+ let gitignoreAdded = [];
214
218
 
215
219
  if (created) {
216
220
  // Scaffold the phase folders this work type starts with.
@@ -275,6 +279,21 @@ export async function createCommand(featureName, options = {}) {
275
279
  // Commander's `--no-checkout` sets options.checkout === false.
276
280
  branchResult = branchCommonFlow(featureName, { checkout: options.checkout !== false });
277
281
  }
282
+
283
+ // protect-spec-files freezes an existing file once its approval is stamped,
284
+ // so a type born pre-approved must create it itself. With a worktree it goes
285
+ // only there: an extra file in the root copy makes `finish` refuse a dirty root.
286
+ if (wt.scaffold.includes('2-plan') && !wt.skippedGates.includes('plan')) {
287
+ const home = worktreeResult?.created && worktreeResult.path ? worktreeResult.path : process.cwd();
288
+ try {
289
+ planSkeleton = writePlanSkeleton(featureName, home);
290
+ } catch (err) {
291
+ planSkeleton = { written: false, error: err.message };
292
+ }
293
+ }
294
+
295
+ const home = worktreeResult?.created && worktreeResult.path ? worktreeResult.path : process.cwd();
296
+ gitignoreAdded = seedDotnetBuildIgnores(home, { featureName, configRoot: process.cwd() });
278
297
  }
279
298
 
280
299
  // `feature` is a snapshot from BEFORE the gate-stamping/worktree steps —
@@ -286,6 +305,8 @@ export async function createCommand(featureName, options = {}) {
286
305
  const out = { feature: featureName, created, ...finalFeature };
287
306
  if (worktreeResult) out.worktree = worktreeResult;
288
307
  if (branchResult) out.branch = branchResult;
308
+ if (planSkeleton) out.planSkeleton = planSkeleton;
309
+ if (gitignoreAdded.length) out.gitignoreAdded = gitignoreAdded;
289
310
  // The move of the PRIMARY checkout is never silent — under --json it goes
290
311
  // to stderr so it survives `... --json > out.json` instead of vanishing.
291
312
  if (branchResult?.movedFrom) {
@@ -300,6 +321,12 @@ export async function createCommand(featureName, options = {}) {
300
321
  console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape)`));
301
322
  console.log(chalk.gray(` status: ${finalFeature.status} | createdAt: ${finalFeature.createdAt}`));
302
323
  console.log(chalk.gray(` scaffolded: ${wt.scaffold.join(', ')}`));
324
+ if (gitignoreAdded.length) {
325
+ console.log(chalk.gray(` .gitignore: ${gitignoreAdded.join(' ')} adicionados (projeto .NET)`));
326
+ }
327
+ if (planSkeleton?.written) {
328
+ console.log(chalk.gray(` 2-plan/plan.md: esqueleto escrito (gramática: morph-spec plan-check --help)`));
329
+ }
303
330
  if (wt.skippedGates.length) {
304
331
  console.log(chalk.yellow(` gates skipped by policy:${finalFeature.workType}: ${wt.skippedGates.join(', ')}`));
305
332
  }
@@ -219,7 +219,7 @@ export async function deleteCommand(featureName, options = {}) {
219
219
  // Untracked (never committed) — nothing for git rm to act on.
220
220
  }
221
221
  // `git rm` only ever removes TRACKED files — a scaffolded phase folder
222
- // with no artifact in it (0-proposal/ with nothing written yet) has no
222
+ // with no artifact in it (2-plan/ with nothing written yet) has no
223
223
  // tracked blob and survives as an empty directory on disk. This bit
224
224
  // real after --with-worktree: teardownWorktree's own registration-clear
225
225
  // commits the root's feature.json on its way through, so `git rm`
@@ -65,7 +65,7 @@ export function buildPrTitle(feature, fj) {
65
65
  return prefix + (raw.length <= room ? raw : `${raw.slice(0, room - 1)}…`);
66
66
  }
67
67
 
68
- const MEMORY_NOTE = 'o fechamento desta feature já está registrado no repositório (commit de archive, `decisions.md`, use cases promovidos, `git log`) — memória de sessão só para o que NENHUM deles guarda: decisão do usuário, pendência a observar em produção, custo medido.';
68
+ const MEMORY_NOTE = 'o fechamento desta feature já está registrado no repositório (commit de archive, `plan.md` com as decisões, índice de ADRs, `git log`) — memória de sessão só para o que NENHUM deles guarda: decisão do usuário, pendência a observar em produção, custo medido.';
69
69
 
70
70
  /**
71
71
  * `morph-spec finish <feature> --pr|--merge` — the single, symmetric close of a
@@ -449,8 +449,8 @@ export async function finishCommand(feature, options = {}) {
449
449
  // ── MERGE precondition, BEFORE the archive ────────────────────────────────
450
450
  // `--merge` lands its merge commit in the PRIMARY ROOT, so the root has to be
451
451
  // clean. That check used to live inside integrateMerge — i.e. AFTER
452
- // runArchive had already moved the feature to .morph/archive/, promoted its
453
- // use cases and COMMITTED. A root dirtied by unrelated work therefore got a
452
+ // runArchive had already moved the feature to .morph/archive/, updated the
453
+ // ADR index and COMMITTED. A root dirtied by unrelated work therefore got a
454
454
  // refusal on a half-finished feature, and the retry then failed for a
455
455
  // different reason entirely (the feature was no longer in features/).
456
456
  //
@@ -709,7 +709,7 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
709
709
 
710
710
  async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp }) {
711
711
  const mergeMsg = `morph-spec finish: merge ${expectedBranch}`;
712
- const ucNote = promotedNote(arch);
712
+ const adrNote = adrIndexNote(arch);
713
713
 
714
714
  if (isWorktreeFlow) {
715
715
  // The root stays on the default; shed this feature's own root-side scaffold,
@@ -748,7 +748,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
748
748
  dropFromRootIndex(root, feature);
749
749
  emit({
750
750
  ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, shedBackup, kept: true }),
751
- ...ucNote,
751
+ ...adrNote,
752
752
  });
753
753
  process.exit(0);
754
754
  }
@@ -766,7 +766,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
766
766
  ? null
767
767
  : await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true, stamp: backupStamp });
768
768
  dropFromRootIndex(root, feature);
769
- emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...ucNote, e2e });
769
+ emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...adrNote, e2e });
770
770
  process.exit(0);
771
771
  }
772
772
 
@@ -797,7 +797,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
797
797
  const publish = publicationState({ root, defaultBranch, push: options.push });
798
798
  const e2e = reclaimStack(feature, { root, worktreePath: null, options });
799
799
  // The root index (gitignored) was already updated by the archive (same cwd).
800
- emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish }), ...ucNote, e2e });
800
+ emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish }), ...adrNote, e2e });
801
801
  process.exit(0);
802
802
  }
803
803
 
@@ -887,7 +887,7 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
887
887
  base: defaultBranch,
888
888
  pr: { number: prNumber, url: prUrl },
889
889
  nextStep: `mergeie ${prLabel} — a feature só estará encerrada quando ${defaultBranch} receber o archive pelo merge`,
890
- ...promotedNote(arch),
890
+ ...adrIndexNote(arch),
891
891
  ...(shedBackup ? { shedBackup } : {}),
892
892
  ...(e2e ? { e2e } : {}),
893
893
  ...(teardown
@@ -917,23 +917,11 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
917
917
  // ── output helpers ────────────────────────────────────────────────────────
918
918
 
919
919
  /**
920
- * Report what the archive did to the acervo: use cases promoted into
921
- * `.morph/specs/usecases/`, any REFUSED for a numbering collision
922
- * (`ucCollisions` — see `usecase-promoter.js`'s reservation-backed guard), the
923
- * promoted copies that stayed superseded because the acervo said so
924
- * (`keptSuperseded`), the ones the feature's spec.md revoked
925
- * (`supersededUseCases`), the ADR index files it moved (`adrIndex`) and the
926
- * revocations it could not apply
927
- * (`revocationProblems`). Every key is absent when there is nothing to report,
928
- * so a feature that touches no acervo keeps the exact output shape it had
929
- * before any of them existed.
920
+ * What the archive did to the ADR index (`adrIndex`) and the revocations it
921
+ * could not apply (`revocationProblems`); both absent when there is nothing to report.
930
922
  */
931
- function promotedNote(arch) {
923
+ function adrIndexNote(arch) {
932
924
  return {
933
- ...(arch?.promotedUseCases?.length ? { promotedUseCases: arch.promotedUseCases } : {}),
934
- ...(arch?.refusedUseCases?.length ? { ucCollisions: arch.refusedUseCases } : {}),
935
- ...(arch?.keptSuperseded?.length ? { keptSuperseded: arch.keptSuperseded } : {}),
936
- ...(arch?.supersededUseCases?.length ? { supersededUseCases: arch.supersededUseCases } : {}),
937
925
  ...(arch?.adrIndex ? { adrIndex: arch.adrIndex } : {}),
938
926
  ...(arch?.revocationProblems?.length ? { revocationProblems: arch.revocationProblems } : {}),
939
927
  };
@@ -1,21 +1,19 @@
1
1
  import chalk from 'chalk';
2
- import { existsSync, readFileSync } from 'fs';
3
2
  import { getFeature } from '../../core/state/state-manager.js';
4
- import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
5
- import { shouldPauseGate, getRiskSignals, classifyE2eSkip } from '../../lib/risk-detector.js';
6
- import { evaluatorReportState, evaluatorMediaSection, verdictSignal } from '../../lib/evaluator-report.js';
3
+ import { shouldPauseGate, getRiskSignals, classifyE2eSkip, planChecksPause, securityReviewFor } from '../../lib/risk-detector.js';
4
+ import { reviewReports, reviewSignal, requiredReviewRoles, verifierMediaSection } from '../../lib/review-reports.js';
7
5
  import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
8
6
  import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
9
7
  import { minimatch } from 'minimatch';
10
8
  import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
11
9
  import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
12
10
  import { readMorphConfig } from '../../lib/tasks/test-runner.js';
13
- import { oversizedTasks } from '../../lib/tasks/tasks-json-parser.js';
11
+ import { readPlanChecks } from '../../lib/plan/checks.js';
14
12
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
15
13
 
16
14
  /**
17
15
  * `--signals` on `gate-check` is NOT a general override like `gate-decision`'s
18
- * — everything else here is read straight off disk (workType, taskCount, sha,
16
+ * — everything else here is read straight off disk (workType, sliceCount, sha,
19
17
  * diffstat, verification), and letting a caller override those would let a
20
18
  * careless/hostile `--signals` silently defeat the policy this command exists
21
19
  * to make deterministic (e.g. overriding `workType` away from "hotfix" would
@@ -28,67 +26,39 @@ import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/featu
28
26
  const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence'];
29
27
 
30
28
  /**
31
- * Os paths de standard que fazem uma feature ser "de midia".
32
- *
33
- * Escritos aqui, uma vez, e lidos do campo `standards[]` das tasks — que o Gate
34
- * 2 congelou. Nao ha julgamento envolvido: ou a task pinou o standard, ou nao
35
- * pinou. Um sinal derivado de opiniao seria sobrescrivivel na pratica ainda que
36
- * nao estivesse em OVERRIDABLE_SIGNAL_KEYS.
37
- *
38
- * A forma dos valores e a de `tasks.json` (`standards/...`), nao a do disco
39
- * (`framework/standards/...`): e o vocabulario que o plano usa.
29
+ * Os standards que fazem uma feature ser "de mídia", como os ids de
30
+ * STANDARDS.json. Fato do plano congelado: ou uma fatia pinou o standard em
31
+ * `standards:`, ou não pinou.
40
32
  */
41
- export const MEDIA_STANDARD_PATHS = [
42
- 'standards/ai-agents/modalities-image-gen.md',
43
- 'standards/ai-agents/media-video.md',
44
- 'standards/ai-agents/media-pipeline.md',
33
+ export const MEDIA_STANDARD_IDS = [
34
+ 'ai-agents-modalities-image-gen',
35
+ 'ai-agents-media-video',
36
+ 'ai-agents-media-pipeline',
45
37
  ];
46
38
 
47
39
  /**
48
- * Uma feature "gera midia" quando ALGUMA task pinou um dos tres standards de
49
- * midia no seu array `standards[]`.
50
- *
51
- * Isto e uma FUNCAO EXPORTADA, e nao uma expressao dentro de `collectSignals`,
52
- * porque um predicado que decide se um guardrail arma tem de ser testavel
53
- * sozinho — o irmao `productionCode` tem quatro casos, e este nao tinha nenhum.
54
- * Um predicado que sempre devolve `false` desarma a clausula em silencio, e
55
- * silencio e o defeito que esta epic catalogou sete vezes.
56
- *
57
- * **O prefixo `framework/` e normalizado, e essa e a razao de existir da
58
- * funcao.** O vocabulario do plano e `standards/...` (o exemplo do
59
- * `tasks.schema.json`, e a unica forma medida hoje nos `standards[]` desta
60
- * arvore), mas o disco e `framework/standards/...` — e a mesma prosa das skills
61
- * cita as duas formas. Um plano que escrevesse a forma do disco desarmaria o
62
- * guardrail sem erro nenhum: `standards --find`, `validate` e o proprio Gate 3
63
- * continuariam verdes. Aceitar as duas grafias custa uma linha; recusar uma
64
- * delas em silencio custa o gate.
65
- *
66
- * Fail-open por AUSENCIA (nao por opiniao): lista vazia, `standards` ausente ou
67
- * de tipo errado ⇒ `false` ⇒ nao pausa. Sem plano legivel nao ha como AFIRMAR
68
- * que a feature gera midia, e uma regra nova nao trava feature legada.
69
- *
70
- * @param {Array<object>} tasks - `tasks.json` → `tasks[]` (qualquer coisa nao-array vira []).
71
- * @returns {boolean} true se ao menos uma task pinou um standard de midia.
40
+ * O plano escreve o standard pelo id ou pelo caminho (`ai-agents/media-video.md`,
41
+ * com ou sem `framework/` ou `standards/` na frente): as duas grafias viram o id.
72
42
  */
73
- export function featureGeneratesMedia(tasks) {
74
- if (!Array.isArray(tasks)) return false;
75
-
76
- return tasks.some((t) =>
77
- Array.isArray(t?.standards) &&
78
- t.standards.some((s) =>
79
- typeof s === 'string' &&
80
- MEDIA_STANDARD_PATHS.includes(s.replace(/^framework\//, ''))));
43
+ export function standardIdOf(value) {
44
+ return String(value ?? '').trim().replace(/\\/g, '/')
45
+ .replace(/^(\.morph\/)?(framework\/)?(standards\/)?/, '')
46
+ .replace(/\.md$/, '')
47
+ .replace(/\//g, '-');
81
48
  }
82
49
 
83
- /** Read tasks.json's task list for a feature; [] if missing/corrupt. Mirrors verify.js's readTasks(). */
84
- function readTasks(featureName) {
85
- try {
86
- const tasksPath = getAbsoluteOutputPath(process.cwd(), featureName, 'tasks');
87
- if (!existsSync(tasksPath)) return [];
88
- return JSON.parse(readFileSync(tasksPath, 'utf8')).tasks || [];
89
- } catch {
90
- return [];
91
- }
50
+ /**
51
+ * Uma feature "gera mídia" quando alguma fatia extraída pinou um dos
52
+ * standards de mídia. Predicado exportado porque decide se um guardrail arma:
53
+ * um que sempre devolve `false` desarma a pausa de mídia da review e a rota opus em
54
+ * silêncio.
55
+ *
56
+ * @param {{ standards?: string[] }[]|null|undefined} slices
57
+ */
58
+ export function featureGeneratesMedia(slices) {
59
+ if (!Array.isArray(slices)) return false;
60
+ return slices.some((s) => Array.isArray(s?.standards)
61
+ && s.standards.some((id) => MEDIA_STANDARD_IDS.includes(standardIdOf(id))));
92
62
  }
93
63
 
94
64
  /**
@@ -126,6 +96,37 @@ function collectPromptModel(cwd, defaultBranch) {
126
96
  };
127
97
  }
128
98
 
99
+ /**
100
+ * Quais relatórios o ship exige: a regra única que gate-check, approve e
101
+ * `report-check --before-eval` leem. Coleta do git, nunca decide.
102
+ *
103
+ * @returns {{ required: string[], security: { required: boolean, reasons: string[] } }}
104
+ */
105
+ export function collectReviewRequirement(cwd, feature) {
106
+ let changed = [];
107
+ try { changed = changedPathsAgainst(detectDefaultBranch(cwd), cwd); } catch { changed = []; }
108
+ const security = securityReviewFor(changed);
109
+ return { required: requiredReviewRoles({ workType: feature?.workType, securityRequired: security.required }), security };
110
+ }
111
+
112
+ /** O sinal `reviewReports` do ship, lido do disco. */
113
+ export function collectReviewSignal(cwd, featureName, feature, planChecks = readPlanChecks(cwd, featureName)) {
114
+ const { required } = collectReviewRequirement(cwd, feature);
115
+ const checkIds = Array.isArray(planChecks.checks?.checks) ? planChecks.checks.checks.map((c) => c.id) : null;
116
+ return reviewSignal(reviewReports(cwd, featureName), { required, checkIds });
117
+ }
118
+
119
+ /**
120
+ * Why an automatic signature may not close the plan gate — the same rule that
121
+ * makes gate-check pause it, read here by approve and advance.
122
+ *
123
+ * @returns {string|null}
124
+ */
125
+ export function planAutoApprovalBlock(cwd, featureName) {
126
+ const reason = planChecksPause(readPlanChecks(cwd, featureName).status);
127
+ return reason ? reason.replace('{feature}', featureName) : null;
128
+ }
129
+
129
130
  /**
130
131
  * Collect every signal `shouldPauseGate()` (risk-detector.js) is documented
131
132
  * to consume, straight off disk — the deterministic call site risk-detector
@@ -139,9 +140,8 @@ function collectPromptModel(cwd, defaultBranch) {
139
140
  * alimentam a regra da dispensa de e2e (também só no ship): um nó e2e que
140
141
  * PULOU não reprova o rollup, logo `verificationStatus` sai 'pass' e a razão
141
142
  * literal é o único fato que distingue dispensa legítima de ausência de prova.
142
- * `verificationScope` é o quarto sinal da mesma regra: um `verify {f} {task}`
143
- * carimba `scope: 'task:{id}'` e o e2e dele pula com a razão do GRUPO daquela
144
- * task, que risk-detector.js recusa como dispensa da feature.
143
+ * `planChecks` (só no gate do plano) é o estado dos checks extraídos contra o
144
+ * plano; `sliceCount` e as `standards` das fatias saem da mesma extração.
145
145
  * `verification` completo, `e2eSkipClass`, diffstat e sha seguem como contexto
146
146
  * de auditoria persistido, sem regra própria.
147
147
  *
@@ -152,7 +152,8 @@ function collectPromptModel(cwd, defaultBranch) {
152
152
  * @returns {object}
153
153
  */
154
154
  export function collectSignals(featureName, feature, cwd, gateName) {
155
- const tasks = readTasks(featureName);
155
+ const planChecks = readPlanChecks(cwd, featureName);
156
+ const slices = planChecks.checks?.slices ?? null;
156
157
  const defaultBranch = detectDefaultBranch(cwd);
157
158
  const sha = getHeadSha(cwd);
158
159
  const verification = feature.verification || null;
@@ -163,14 +164,12 @@ export function collectSignals(featureName, feature, cwd, gateName) {
163
164
  // anti-spoof rationale as workType (hotfix's forced pause).
164
165
  gate: gateName,
165
166
  workType: feature.workType || 'feature',
166
- taskCount: tasks.length,
167
+ sliceCount: slices ? slices.length : null,
167
168
  standardsViolations: [],
168
169
  designSystemDivergence: false,
169
170
  };
170
171
 
171
- // Fato de disco, fora de OVERRIDABLE_SIGNAL_KEYS: task que não cabe num
172
- // dispatch se fatia no plano, não depois de um sub-agent bater no teto.
173
- if (gateName === 'plan') signals.oversizedTasks = oversizedTasks({ tasks });
172
+ if (gateName === 'plan') signals.planChecks = planChecks.status;
174
173
 
175
174
  // Só onde o checklist decide algo: no fecho do design e no ship. Fato de
176
175
  // disco, fora de OVERRIDABLE_SIGNAL_KEYS.
@@ -179,10 +178,10 @@ export function collectSignals(featureName, feature, cwd, gateName) {
179
178
  }
180
179
 
181
180
  if (gateName === 'review') {
182
- signals.evaluatorVerdict = verdictSignal(evaluatorReportState(cwd, featureName));
181
+ signals.reviewReports = collectReviewSignal(cwd, featureName, feature, planChecks);
183
182
  signals.mediaClaims = {
184
- section: evaluatorMediaSection(cwd, featureName),
185
- featureGeneratesMedia: featureGeneratesMedia(tasks),
183
+ section: verifierMediaSection(cwd, featureName),
184
+ featureGeneratesMedia: featureGeneratesMedia(slices),
186
185
  };
187
186
  }
188
187
 
@@ -207,15 +206,9 @@ export function collectSignals(featureName, feature, cwd, gateName) {
207
206
  signals.e2eStatus = verification?.nodes?.e2e ?? null;
208
207
  signals.e2eReason = verification?.e2eReason ?? null;
209
208
  signals.e2eSkipClass = signals.e2eStatus === 'skip' ? classifyE2eSkip(signals.e2eReason) : null;
210
- // De QUEM é o carimbo. `verify {f} {task}` grava 'task:{id}'; só 'feature'
211
- // responde pela app inteira, e é por isso que este sinal existe: a razão
212
- // 'task group "docs" does not touch runtime' é dispensa legítima do que
213
- // AQUELA task verificou, e sem o escopo ela dispensaria o Gate 3 da feature.
214
- // Fato de disco como os outros três — jamais sobrescrevível por --signals.
215
- signals.verificationScope = verification?.scope ?? null;
216
209
 
217
210
  // O nó de eval, separado do rollup pela mesma razão que o e2e: um eval que
218
- // PULOU (escopo de task) não avermelha `verification.status`, então o gate
211
+ // PULOU não avermelha `verification.status`, então o gate
219
212
  // leria 'pass' tanto para "o eval rodou e passou" quanto para "o eval nunca
220
213
  // rodou". `null` aqui é "este projeto não declara nó de eval" — DISTINTO de
221
214
  // 'skip', que é "declara, e esta corrida não o executou".
@@ -242,8 +235,8 @@ export function collectSignals(featureName, feature, cwd, gateName) {
242
235
  * Golden rule: this command does NOT change gate policy. It is a faithful
243
236
  * caller of risk-detector.js's existing, already-tested rules — nothing here
244
237
  * adds a new pause condition. Where the skills' natural-language risk tables
245
- * (morph-brainstorming/morph-plan/morph-review/morph-apply) diverge from
246
- * risk-detector.js, risk-detector.js wins (see the R4 report for the full list).
238
+ * (morph-plan/morph-review/morph-apply) diverge from risk-detector.js,
239
+ * risk-detector.js wins.
247
240
  */
248
241
  export async function gateCheckCommand(featureName, gateName, options = {}) {
249
242
  try {
@@ -259,7 +252,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
259
252
  process.exit(1);
260
253
  }
261
254
 
262
- // Owner-tree guard: gate-check reads workType/tasks/verdict/git off cwd
255
+ // Owner-tree guard: gate-check reads workType/checks/verdict/git off cwd
263
256
  // AND writes the resulting decision through persistGateDecision — both
264
257
  // halves are wrong from a tree that isn't this feature's own, so refuse
265
258
  // rather than compute a decision from stale reads and then fail to save it.
@@ -289,7 +282,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
289
282
  if (rejected.length > 0) {
290
283
  console.error(chalk.yellow(
291
284
  `\n⚠️ --signals ignored for keys not derived from disk-facts you may safely override: ${rejected.join(', ')}\n` +
292
- ` Only ${OVERRIDABLE_SIGNAL_KEYS.join(', ')} may be overridden here — everything else is read from feature.json/tasks.json/git.\n` +
285
+ ` Only ${OVERRIDABLE_SIGNAL_KEYS.join(', ')} may be overridden here — everything else is read from feature.json/checks.json/git.\n` +
293
286
  ` Use \`morph-spec gate-decision\` directly for a fully manual decision.\n`
294
287
  ));
295
288
  }
@@ -303,9 +296,14 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
303
296
  const signals = { ...collectSignals(featureName, feature, cwd, gateName), ...overrides };
304
297
 
305
298
  const trust = options.trust || readTrustFromConfig();
306
- const { pause, reason } = shouldPauseGate({ trust }, signals);
299
+ const computed = shouldPauseGate({ trust }, signals);
300
+ const manual = feature.gateDecisions?.[gateName];
301
+ const manualPause = manual?.source === 'manual' && manual.pause === true;
302
+ const { pause, reason } = manualPause ? { pause: true, reason: manual.reason } : computed;
307
303
 
308
- const entry = await persistGateDecision(featureName, gateName, { pause, reason, signals, trust, cwd });
304
+ const entry = await persistGateDecision(featureName, gateName, {
305
+ pause, reason, signals, trust, cwd, source: manualPause ? 'manual' : 'gate-check',
306
+ });
309
307
 
310
308
  if (options.json) {
311
309
  console.log(JSON.stringify({ feature: featureName, gate: gateName, ...entry }, null, 2));
@@ -317,6 +315,9 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
317
315
  ? chalk.yellow(`\n⏸️ Gate "${gateName}" — PAUSE (${featureName})`)
318
316
  : chalk.green(`\n▶️ Gate "${gateName}" — auto-pass (${featureName})`));
319
317
  console.log(chalk.gray(` reason: ${entry.reason} | trust: ${entry.trust} | decidedAt: ${entry.decidedAt}`));
318
+ if (manualPause) {
319
+ console.log(chalk.gray(` pausa manual mantida (sinais calculados: ${computed.reason}); só \`morph-spec gate-decision ${featureName} ${gateName} --pause false\` a libera`));
320
+ }
320
321
  if (allSignals.length > 0) {
321
322
  console.log(chalk.gray(` risk signals present: ${allSignals.join('; ')}`));
322
323
  }
@@ -5,7 +5,7 @@ import { getFeature, updateFeature } from '../../core/state/state-manager.js';
5
5
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
6
6
  import { mockupsHash } from '../../lib/specs/screen-checklist.js';
7
7
 
8
- export const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
8
+ export const VALID_GATES = ['plan', 'uiux', 'review'];
9
9
 
10
10
  function parsePauseValue(raw) {
11
11
  const v = String(raw).toLowerCase();
@@ -43,13 +43,13 @@ export function readTrustFromConfig() {
43
43
  *
44
44
  * @param {string} featureName
45
45
  * @param {string} gateName
46
- * @param {{ pause: boolean, reason?: string, signals?: object, trust?: string, cwd?: string }} decision
46
+ * @param {{ pause: boolean, reason?: string, signals?: object, trust?: string, cwd?: string, source: 'manual'|'gate-check' }} decision
47
47
  * @returns {Promise<{ pause: boolean, reason: string, signals: object, trust: string, decidedAt: string, mockupsHash?: string|null }>}
48
48
  * the exact entry written (decidedAt is server-side, generated here). A uiux
49
49
  * decision also carries the hash of the mockup it saw: it answers that
50
50
  * mockup's pause, not the next one's.
51
51
  */
52
- export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust, cwd = process.cwd() } = {}) {
52
+ export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust, cwd = process.cwd(), source } = {}) {
53
53
  const entry = {
54
54
  pause,
55
55
  reason: reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
@@ -57,6 +57,7 @@ export async function persistGateDecision(featureName, gateName, { pause, reason
57
57
  trust: trust || readTrustFromConfig(),
58
58
  decidedAt: new Date().toISOString(),
59
59
  ...(gateName === 'uiux' ? { mockupsHash: mockupsHash(cwd, featureName) } : {}),
60
+ source,
60
61
  };
61
62
 
62
63
  await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
@@ -118,7 +119,7 @@ export async function gateDecisionCommand(featureName, gateName, options = {}) {
118
119
  process.exit(1);
119
120
  }
120
121
  if (!signals || typeof signals !== 'object' || Array.isArray(signals)) {
121
- console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"taskCount":12}\'\n'));
122
+ console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"standardsViolations":[]}\'\n'));
122
123
  process.exit(1);
123
124
  }
124
125
  }
@@ -128,6 +129,7 @@ export async function gateDecisionCommand(featureName, gateName, options = {}) {
128
129
  reason: options.reason,
129
130
  signals,
130
131
  trust: options.trust,
132
+ source: 'manual',
131
133
  });
132
134
 
133
135
  if (options.json) {
@@ -87,6 +87,6 @@ export function handoffCommand(featureName, options = {}) {
87
87
  if (tree !== cwd) {
88
88
  console.log(chalk.yellow(` resolvido para ${tree} — a cópia da raiz é esqueleto de nascimento, não a verdade.`));
89
89
  }
90
- console.log(chalk.gray(` fase ${result.meta.phase} · tasks ${result.meta.tasksDone}/${result.meta.tasksTotal} · ${result.meta.generatedAt}`));
90
+ console.log(chalk.gray(` fase ${result.meta.phase} · fatias ${result.meta.slicesDone}/${result.meta.slicesTotal} · ${result.meta.generatedAt}`));
91
91
  console.log(chalk.gray(' A próxima sessão lê este documento antes de continuar (ponteiros, não cópias).\n'));
92
92
  }
@@ -4,7 +4,7 @@ import { basename, join } from 'path';
4
4
  import { derivePhase } from '../../core/state/state-manager.js';
5
5
  import { gateOrderViolations } from '../../core/state/gate-order.js';
6
6
  import { missingIdentity } from '../../core/state/feature-identity.js';
7
- import { derivePhaseFrom } from '../../../framework/hooks/shared/state-reader.js';
7
+ import { derivePhaseFrom, fileSetArtifactProbe } from '../../../framework/hooks/shared/state-reader.js';
8
8
  import { isGitRepo } from '../project/worktree.js';
9
9
  import { morphBranches, currentBranch, branchView } from '../../lib/worktree/feature-truth.js';
10
10
  import { buildPrIndex } from '../../lib/git/pr-index.js';
@@ -13,7 +13,7 @@ import { pendingPrFeatures, prForFeature } from '../../lib/git/pending-prs.js';
13
13
  /**
14
14
  * `morph-spec list [--all] [--json] [--no-branch]` — the feature audit board.
15
15
  *
16
- * One row per feature: slug × workType × status × phase × gates (G1/G2/G3) ×
16
+ * One row per feature: slug × workType × status × phase × gates (G1/Gd/G3) ×
17
17
  * description, plus a flags column for anomalies (missing description, gate
18
18
  * order, dangling worktree pointer). Complements `delete`: see what's dead
19
19
  * before removing it. Replaces the ad-hoc "loop over every feature.json in
@@ -37,7 +37,7 @@ import { pendingPrFeatures, prForFeature } from '../../lib/git/pending-prs.js';
37
37
  * `morph-spec fleet`'s job, not this one's.
38
38
  */
39
39
 
40
- const G = ['proposal', 'uiux', 'plan', 'review'];
40
+ const G = ['plan', 'uiux', 'review'];
41
41
 
42
42
  function readFeaturesIn(baseDir, subdir, location) {
43
43
  const root = join(baseDir, '.morph', subdir);
@@ -76,7 +76,7 @@ function shapeRow(name, fj, location, phase, { corruptDisk = false, worktree = n
76
76
  const gates = {};
77
77
  for (const g of G) gates[g] = fj.approvalGates?.[g]?.approved === true;
78
78
 
79
- // Gate 3 approved WITHOUT the independent evaluator (`approve --self-assessed`).
79
+ // Gate 3 approved WITHOUT the independent review reports (`approve --self-assessed`).
80
80
  // A recorded degradation is only worth recording if it is visible — this is
81
81
  // the surface that makes it so (D2).
82
82
  const selfAssessed = fj.approvalGates?.review?.selfAssessed === true;
@@ -150,7 +150,7 @@ function resolveBranchTruth(cwd, rows) {
150
150
  if (head === featureBranch) continue; // cwd IS the checkout — disk is freshest, never go backwards
151
151
  const view = branchView(cwd, row.feature, featureBranch);
152
152
  if (!view?.fj) continue;
153
- const phase = derivePhaseFrom(view.fj, (rel) => view.files.has(rel));
153
+ const phase = derivePhaseFrom(view.fj, fileSetArtifactProbe(view.files));
154
154
  rows[i] = shapeRow(row.feature, view.fj, row.location, phase, {
155
155
  corruptDisk: row.flags.includes('corrupt'),
156
156
  worktree: row.worktree, // root-owned — never trust the branch copy for this
@@ -193,7 +193,7 @@ function pendingRow(p) {
193
193
  workType: null,
194
194
  status: 'in_progress',
195
195
  phase: 'aguardando-merge',
196
- gates: { proposal: false, uiux: false, plan: false, review: false },
196
+ gates: { plan: false, uiux: false, review: false },
197
197
  selfAssessed: false,
198
198
  flags: ['pr-pending'],
199
199
  location: 'pr-pending',
@@ -281,7 +281,7 @@ export function listCommand(options = {}) {
281
281
  const warnFlags = (r) => r.flags.filter((f) => f !== 'pr-pending');
282
282
  console.log(chalk.white.bold(
283
283
  col('FEATURE', 22) + col('TYPE', 9) + col('STATUS', 13) + col('PHASE', 17) + col('WHERE', 22) + (showPr ? col('PR', 12) : '')
284
- + col('G1', 4) + col('G2', 4) + col('G3', 4) + col('', 3) + 'DESCRIPTION',
284
+ + col('G1', 4) + col('Gd', 4) + col('G3', 4) + col('', 3) + 'DESCRIPTION',
285
285
  ));
286
286
  for (const r of rows) {
287
287
  const flag = warnFlags(r).length ? chalk.yellow('⚠') : ' ';
@@ -292,7 +292,7 @@ export function listCommand(options = {}) {
292
292
  // padEnd counts the invisible color codes; pad the plain marks manually.
293
293
  console.log(
294
294
  col(r.feature, 22) + col(r.workType, 9) + col(r.status, 13) + col(r.phase, 17) + col(whereLabel(r), 22) + (showPr ? col(prCell(r.pr), 12) : '')
295
- + mark(r.gates.proposal) + ' ' + mark(r.gates.plan) + ' ' + mark(r.gates.review) + ' '
295
+ + mark(r.gates.plan) + ' ' + mark(r.gates.uiux) + ' ' + mark(r.gates.review) + ' '
296
296
  + flag + ' ' + desc,
297
297
  );
298
298
  }
@@ -300,6 +300,6 @@ export function listCommand(options = {}) {
300
300
  if (flagged.length) {
301
301
  console.log(chalk.gray(`\n⚠ ${flagged.length} com inconsistência — rode 'morph-spec doctor' para detalhes.`));
302
302
  }
303
- console.log(chalk.gray(`\nG1=proposal · G2=plan · G3=review${options.all || options.archived ? ' · inclui arquivadas' : ''}`));
303
+ console.log(chalk.gray(`\nG1=plan · Gd=uiux (design) · G3=review${options.all || options.archived ? ' · inclui arquivadas' : ''}`));
304
304
  process.exit(0);
305
305
  }