@polymorphism-tech/morph-spec 9.7.0 → 10.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 (211) hide show
  1. package/CLAUDE.md +11 -11
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +41 -44
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-review.md +15 -0
  16. package/framework/commands/morph-status.md +1 -1
  17. package/framework/docs/CLI.md +100 -102
  18. package/framework/evals/review-checklist.md +46 -54
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  21. package/framework/hooks/claude-code/core/README.md +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +4 -4
  23. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  24. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  25. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  26. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  27. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  29. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  30. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  32. package/framework/hooks/shared/e2e-check.js +0 -0
  33. package/framework/hooks/shared/gate-event.js +2 -2
  34. package/framework/hooks/shared/graph-refresh.js +20 -3
  35. package/framework/hooks/shared/heavy-command.js +32 -1
  36. package/framework/hooks/shared/heavy-config.js +1 -1
  37. package/framework/hooks/shared/heavy-slots.js +41 -4
  38. package/framework/hooks/shared/next-step.js +63 -24
  39. package/framework/hooks/shared/phase-utils.js +6 -9
  40. package/framework/hooks/shared/report-progress.js +86 -0
  41. package/framework/hooks/shared/state-reader.js +3 -2
  42. package/framework/hooks/statusline.js +332 -0
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +9 -1
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/ef-standards.md +20 -0
  47. package/framework/rules/frontend-standards.md +1 -1
  48. package/framework/rules/implement-report.md +25 -0
  49. package/framework/rules/infrastructure-standards.md +2 -0
  50. package/framework/rules/nextjs-standards.md +7 -0
  51. package/framework/rules/plan-grammar.md +53 -0
  52. package/framework/rules/ui-design.md +27 -0
  53. package/framework/schemas/config.schema.json +12 -19
  54. package/framework/schemas/feature.schema.json +7 -1
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  57. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  58. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  59. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  60. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  61. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  62. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  63. package/framework/skills/core/morph-apply/references/review.md +65 -0
  64. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  65. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  66. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  67. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  68. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  69. package/framework/standards/STANDARDS.json +32 -5
  70. package/framework/standards/STANDARDS.meta.json +5 -1
  71. package/framework/standards/ai-agents/agent-session.md +2 -3
  72. package/framework/standards/ai-agents/context-providers.md +2 -2
  73. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  74. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  75. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  76. package/framework/standards/ai-agents/setup.md +1 -1
  77. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  78. package/framework/standards/architecture/stack-baseline.md +47 -0
  79. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  80. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  81. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  82. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  83. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  84. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  85. package/framework/templates/.idea/morph-templates.xml +92 -0
  86. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  87. package/framework/templates/IDE-SNIPPETS.md +262 -262
  88. package/framework/templates/REGISTRY.json +1 -2
  89. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  90. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  92. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  93. package/framework/templates/context/ARCHITECTURE.md +1 -1
  94. package/framework/templates/docs/plan.md +12 -12
  95. package/framework/templates/docs/ui-mockups.md +17 -105
  96. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  100. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  101. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  102. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  103. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  104. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  105. package/framework/templates/state.template.json +17 -17
  106. package/framework/templates/ui/design-system.css +226 -226
  107. package/package.json +1 -1
  108. package/src/commands/e2e/e2e.js +88 -23
  109. package/src/commands/graph/diff.js +74 -0
  110. package/src/commands/graph/status.js +1 -1
  111. package/src/commands/observability/cost.js +2 -4
  112. package/src/commands/observability/telemetry.js +1 -1
  113. package/src/commands/project/doctor.js +23 -39
  114. package/src/commands/project/fleet.js +5 -6
  115. package/src/commands/project/init.js +63 -36
  116. package/src/commands/project/stack-scan.js +3 -42
  117. package/src/commands/project/stack-set.js +26 -0
  118. package/src/commands/project/status.js +12 -13
  119. package/src/commands/project/update.js +39 -3
  120. package/src/commands/project/worktree.js +27 -16
  121. package/src/commands/state/advance.js +17 -17
  122. package/src/commands/state/approve.js +0 -6
  123. package/src/commands/state/archive.js +27 -1
  124. package/src/commands/state/auto-approval.js +1 -1
  125. package/src/commands/state/create.js +0 -5
  126. package/src/commands/state/finish.js +43 -1
  127. package/src/commands/state/gate-check.js +37 -30
  128. package/src/commands/state/gate-decision.js +3 -2
  129. package/src/commands/state/next.js +1 -0
  130. package/src/commands/state/recap.js +67 -0
  131. package/src/commands/state/report-check.js +39 -146
  132. package/src/commands/validation/plan-check.js +16 -11
  133. package/src/commands/validation/validate-feature.js +3 -3
  134. package/src/commands/validation/verify.js +6 -6
  135. package/src/core/paths/output-schema.js +3 -3
  136. package/src/core/state/state-manager.js +2 -4
  137. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  138. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  139. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/Activity.jsx +4 -0
  142. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  143. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  144. package/src/dashboard/app/pages/Features.jsx +7 -7
  145. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  146. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  147. package/src/dashboard/scanner/activity-scanner.js +29 -0
  148. package/src/dashboard/scanner/agents-scanner.js +11 -14
  149. package/src/dashboard/scanner/features-scanner.js +45 -24
  150. package/src/dashboard/scanner/project-api.js +11 -2
  151. package/src/dashboard/scanner/skills-scanner.js +11 -1
  152. package/src/dashboard/vite.config.js +24 -24
  153. package/src/lib/e2e/applicability.js +3 -1
  154. package/src/lib/git/tree-state.js +1 -16
  155. package/src/lib/graph/diff-tree.js +127 -0
  156. package/src/lib/graph/graphify-runtime.js +12 -5
  157. package/src/lib/heavy/gate.js +22 -1
  158. package/src/lib/installers/memory-installer.js +2 -2
  159. package/src/lib/installers/rules-installer.js +57 -0
  160. package/src/lib/installers/stack-assets.js +17 -0
  161. package/src/lib/installers/stack-sync.js +219 -0
  162. package/src/lib/plan/checks.js +55 -110
  163. package/src/lib/plan/critique.js +28 -0
  164. package/src/lib/plan/media.js +42 -0
  165. package/src/lib/plan/plan-html.js +125 -0
  166. package/src/lib/plan/plan-parser.js +34 -45
  167. package/src/lib/recap/piper.js +36 -0
  168. package/src/lib/recap/recap.js +232 -0
  169. package/src/lib/review-reports.js +33 -7
  170. package/src/lib/risk-detector.js +90 -11
  171. package/src/lib/specs/screen-checklist.js +33 -48
  172. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  173. package/src/lib/tasks/test-nodes.js +24 -6
  174. package/src/lib/tasks/test-runner.js +40 -1
  175. package/src/lib/telemetry/metrics.js +27 -29
  176. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  177. package/src/lib/validators/verify-runner.js +50 -19
  178. package/src/lib/worktree/fleet-scanner.js +7 -20
  179. package/src/lib/worktree/worktree-audit.js +0 -37
  180. package/src/scripts/setup-infra.js +15 -50
  181. package/src/utils/agents-installer.js +27 -201
  182. package/src/utils/hooks-installer.js +11 -95
  183. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  184. package/framework/hooks/claude-code/statusline.py +0 -699
  185. package/framework/hooks/claude-code/statusline.sh +0 -11
  186. package/framework/hooks/shared/agent-reap.js +0 -74
  187. package/framework/hooks/shared/slice-progress.js +0 -110
  188. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  189. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  190. package/framework/skills/core/morph-review/SKILL.md +0 -129
  191. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  192. package/framework/standards/PROVENANCE.json +0 -119
  193. package/src/commands/project/worktree-reap.js +0 -34
  194. package/src/commands/state/handoff.js +0 -65
  195. package/src/commands/validation/mutate.js +0 -263
  196. package/src/commands/validation/scout-check.js +0 -28
  197. package/src/lib/generators/handoff-generator.js +0 -172
  198. package/src/lib/mutation/adapters.js +0 -79
  199. package/src/lib/mutation/commands.js +0 -61
  200. package/src/lib/mutation/dotnet-introspection.js +0 -281
  201. package/src/lib/mutation/manifest.js +0 -144
  202. package/src/lib/mutation/plan.js +0 -44
  203. package/src/lib/mutation/report.js +0 -68
  204. package/src/lib/mutation/runner.js +0 -301
  205. package/src/lib/mutation/schemata.js +0 -60
  206. package/src/lib/mutation/snapshot.js +0 -65
  207. package/src/lib/mutation/subset.js +0 -109
  208. package/src/lib/mutation/trx.js +0 -50
  209. package/src/lib/scout/scout-check.js +0 -293
  210. package/src/lib/validators/traceability-check.js +0 -103
  211. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -1,7 +1,7 @@
1
1
  import { join, dirname, relative, isAbsolute } from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { spawnSync } from 'child_process';
4
- import { existsSync, readdirSync } from 'fs';
4
+ import { existsSync, readdirSync, readFileSync } from 'fs';
5
5
  import { homedir } from 'os';
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -27,9 +27,10 @@ import {
27
27
  } from '../../utils/version-checker.js';
28
28
  import { installClaudeHooks, installGlobalStatusline } from '../../utils/claude-settings-manager.js';
29
29
  import { installSkills } from '../../utils/skills-installer.js';
30
- import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
30
+ import { installAgents } from '../../utils/agents-installer.js';
31
31
  import { injectMorphImport } from '../../utils/claude-md-injector.js';
32
32
  import { getProjectTags } from '../../lib/stack-filter.js';
33
+ import { detectStackTags, mergeDetectedTags } from '../../lib/installers/stack-sync.js';
33
34
  import { updateClaudeMdAgentsBlock, installRulesForStack, installCommandsForStack } from '../../scripts/setup-infra.js';
34
35
  import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
35
36
  import { createEffects } from '../../lib/installers/effects.js';
@@ -145,6 +146,26 @@ export function stateJsonIsTracked(targetPath) {
145
146
  }
146
147
  }
147
148
 
149
+ const SLICES_HEADING = /^## Fatias\s*$/m;
150
+
151
+ /** Active features still planned in 9.x slices: a plan with `## Fatias` and no `3-implement/report.md` yet. */
152
+ export function legacySliceFeatures(targetPath) {
153
+ const featuresDir = join(targetPath, '.morph', 'features');
154
+ let names;
155
+ try { names = readdirSync(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return []; }
156
+ return names.filter((name) => {
157
+ const dir = join(featuresDir, name);
158
+ if (existsSync(join(dir, '3-implement', 'report.md'))) return false;
159
+ try { return SLICES_HEADING.test(readFileSync(join(dir, '2-plan', 'plan.md'), 'utf8')); } catch { return false; }
160
+ }).sort();
161
+ }
162
+
163
+ export function legacySliceNotice(names) {
164
+ if (names.length === 0) return null;
165
+ return `Feature(s) planejada(s) em fatias (9.x): ${names.join(', ')}. Plano não aprovado: tire \`## Fatias\` e rode \`morph-spec plan-check <f>\`. `
166
+ + 'Já em implement: segue pelos `reports/F*.md`; para o formato novo, crie `3-implement/report.md` com uma linha por `Cn` (CHANGELOG 10.0.0).';
167
+ }
168
+
148
169
  export function untrackCommittedStateJson(targetPath, fx = createEffects({ dryRun: false })) {
149
170
  if (!stateJsonIsTracked(targetPath)) return false;
150
171
  return fx.gitIndex('rm --cached --quiet', '.morph/state.json', { cwd: targetPath });
@@ -487,6 +508,19 @@ export async function updateCommand(options = {}) {
487
508
  try {
488
509
  if (configBackup) projectTags = getProjectTags(configBackup);
489
510
  } catch {}
511
+ // The same detection as `syncStackRules`, folded in before the installs: `restoreUserConfig`
512
+ // writes `configBackup`, and a dry run sees `.claude/rules/` before the clean, not after.
513
+ if (configBackup) {
514
+ try {
515
+ const detected = detectStackTags(targetPath, { config: configBackup });
516
+ const merged = mergeDetectedTags(configBackup, detected.persist);
517
+ if (merged.changed) {
518
+ configBackup.project = merged.config.project;
519
+ logger.dim(` ✓ config.json: + ${merged.added.join(', ')} (detectado nos arquivos)`);
520
+ }
521
+ projectTags = [...new Set([...getProjectTags(configBackup), ...detected.tags])];
522
+ } catch {}
523
+ }
490
524
 
491
525
  let commandsCopied = false;
492
526
  const commandsSrc = join(frameworkDir, 'commands');
@@ -504,7 +538,6 @@ export async function updateCommand(options = {}) {
504
538
 
505
539
  updateSpinner.text = 'Syncing agents to .claude/agents/...';
506
540
  const agentsResult = await installAgents(targetPath, frameworkDir, { projectTags, fx });
507
- await installDomainAgents(targetPath, frameworkDir, { projectTags, fx });
508
541
 
509
542
  updateSpinner.text = 'Syncing rules to .claude/rules/...';
510
543
  const rulesSrc = join(frameworkDir, 'rules');
@@ -566,6 +599,9 @@ export async function updateCommand(options = {}) {
566
599
  }
567
600
  }
568
601
 
602
+ const sliceNotice = legacySliceNotice(legacySliceFeatures(targetPath));
603
+ if (sliceNotice) notices.push(sliceNotice);
604
+
569
605
  const untracked = untrackCommittedStateJson(targetPath, fx);
570
606
 
571
607
  updateSpinner.text = 'Saving version info...';
@@ -85,8 +85,27 @@ function getDateSuffix() {
85
85
  return new Date().toISOString().slice(0, 10).replace(/-/g, '');
86
86
  }
87
87
 
88
- // The ONLY links `finish` may auto-remove (never widened to any reparse point found), plus
89
- // `{project.frontendPath}/node_modules`: a monorepo frontend has its own install.
88
+ function trackedPackageNodeModules(mainCwd) {
89
+ let out;
90
+ try {
91
+ out = execFileSync('git', ['ls-files', '-z', '--', '*package.json'], { cwd: mainCwd, stdio: 'pipe' }).toString();
92
+ } catch {
93
+ return [];
94
+ }
95
+ const dirs = [];
96
+ for (const file of out.split('\0')) {
97
+ const parts = file.split('/');
98
+ if (parts.length < 2 || parts.at(-1) !== 'package.json') continue;
99
+ const dirParts = parts.slice(0, -1);
100
+ if (dirParts.includes('node_modules') || WORKTREE_DIR_NAMES.includes(dirParts[0])) continue;
101
+ const rel = join(...dirParts, 'node_modules');
102
+ if (existsSync(join(mainCwd, rel))) dirs.push(rel);
103
+ }
104
+ return dirs;
105
+ }
106
+
107
+ // The ONLY links `finish` may auto-remove (never widened to any reparse point found), plus the
108
+ // `node_modules` of each tracked nested package: a monorepo package has its own install.
90
109
  function resolveInfraLinkDirs(mainCwd) {
91
110
  const dirs = ['node_modules', '.claude'];
92
111
  if (existsSync(join(mainCwd, '.morph', 'framework'))) {
@@ -100,6 +119,9 @@ function resolveInfraLinkDirs(mainCwd) {
100
119
  if (frontendPath) dirs.push(join(frontendPath, 'node_modules'));
101
120
  }
102
121
  } catch {}
122
+ for (const rel of trackedPackageNodeModules(mainCwd)) {
123
+ if (!dirs.includes(rel)) dirs.push(rel);
124
+ }
103
125
  return dirs;
104
126
  }
105
127
 
@@ -298,15 +320,6 @@ export async function runWorktreeSetup(feature, options = {}) {
298
320
 
299
321
  const graph = seedWorktreeGraph(worktreePath, cwd);
300
322
 
301
- // Fail-open; a no-op under `create --worktree`, which regenerates it after its copy.
302
- try {
303
- if (existsSync(join(worktreePath, '.morph', 'features', feature, 'feature.json'))) {
304
- const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
305
- // `worktree` is root-owned and absent from this branch copy: pass it so the doc says "Local: worktree".
306
- writeHandoff(feature, worktreePath, { worktree: { path: worktreePath, branch } });
307
- }
308
- } catch {}
309
-
310
323
  try {
311
324
  const { loadState, saveState } = await import('../../core/state/state-manager.js');
312
325
  const state = loadState(false);
@@ -812,9 +825,9 @@ export function pickRemovalFailure(stderrs) {
812
825
 
813
826
  /**
814
827
  * PROVE-or-DECLINE. `refused` (proved unsafe) restores the junctions; a git FAILURE leaves them down
815
- * (`infraLeftUnlinked`) unless `relinkOnFailure` (the reaper, whose agent may still be live).
828
+ * (`infraLeftUnlinked`).
816
829
  */
817
- export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false, relinkOnFailure = false, stamp }) {
830
+ export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false, stamp }) {
818
831
  // ORDER IS LOAD-BEARING: unlink the junctions BEFORE git touches the dir. `git worktree remove`
819
832
  // follows them and deletes the root's installation; a declined path is repaired by the rollback below.
820
833
  const unlinked = unlinkWorktreeInfraLinks(worktreePath, root);
@@ -859,8 +872,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
859
872
  // would make the next retry (or a raw `--force`) follow the junctions out.
860
873
  const refused = !canRemove;
861
874
  let relinked = false;
862
- const relinkAfterFailure = relinkOnFailure && !refused && !worktreeRemoved && !removalFailure?.alreadyDeregistered;
863
- if ((refused || relinkAfterFailure) && unlinked.length && existsSync(worktreePath)) {
875
+ if (refused && unlinked.length && existsSync(worktreePath)) {
864
876
  try {
865
877
  setupInfraLinks(worktreePath, root);
866
878
  relinked = true;
@@ -1033,7 +1045,6 @@ export async function runWorktreeRemove(featureArg, options = {}) {
1033
1045
  options: { discardIgnored: options.discardIgnored === true },
1034
1046
  // The branch is the work: deleting it would discard unintegrated commits. Opt-in only.
1035
1047
  deleteBranch: options.deleteBranch === true,
1036
- relinkOnFailure: options.relinkOnFailure === true,
1037
1048
  // Re-run advice must address the worktree the way it was addressable here:
1038
1049
  // a `--target` worktree has no feature name that would resolve.
1039
1050
  addressByTarget: !featureAddressable,
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { existsSync, mkdirSync, readFileSync } from 'fs';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { join, relative } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { getFeature, setApprovalGate, derivePhase } from '../../core/state/state-manager.js';
@@ -8,7 +8,7 @@ import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
8
8
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
9
9
  import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
10
10
  import { seedDesignArtifacts } from '../../lib/specs/design-seed.js';
11
- import { sliceProgress } from '../../../framework/hooks/shared/slice-progress.js';
11
+ import { implementProgress, reportPath, reportSkeleton } from '../../../framework/hooks/shared/report-progress.js';
12
12
  import { autoApprovalRefusal, printRefusal } from './auto-approval.js';
13
13
  import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
14
14
 
@@ -38,17 +38,25 @@ function missingPhaseArtifacts(phase, featurePath) {
38
38
  const rel = `${entry.phaseDir}/${entry.filename}`;
39
39
  if (!existsSync(join(featurePath, entry.phaseDir, entry.filename))) missing.push(rel);
40
40
  }
41
- if (phase === 'implement') missing.push(...unfinishedSliceReports(featurePath));
41
+ if (phase === 'implement') missing.push(...unfinishedScenarios(featurePath));
42
42
  return missing;
43
43
  } catch {
44
44
  return [];
45
45
  }
46
46
  }
47
47
 
48
- function unfinishedSliceReports(featurePath) {
49
- const progress = sliceProgress(featurePath);
48
+ function unfinishedScenarios(featurePath) {
49
+ const progress = implementProgress(featurePath);
50
50
  if (!progress) return ['2-plan/checks.json'];
51
- return progress.slices.filter((s) => s.status !== 'feito').map((s) => `3-implement/reports/${s.id}.md`);
51
+ const open = progress.checks.filter((c) => c.status !== 'feito').map((c) => c.id);
52
+ return open.length ? [`3-implement/report.md (${open.join(', ')} não feito)`] : [];
53
+ }
54
+
55
+ function seedReport(featurePath, featureName) {
56
+ const path = reportPath(featurePath);
57
+ if (existsSync(path)) return;
58
+ const progress = implementProgress(featurePath);
59
+ if (progress) writeFileSync(path, reportSkeleton(featureName, progress.checks.map((c) => c.id)));
52
60
  }
53
61
 
54
62
  // Message-only and fail-open: names the worktree that holds the artifacts instead of demanding
@@ -114,10 +122,6 @@ export async function advanceCommand(featureName, options = {}) {
114
122
  mkdirSync(join(featurePath, '1-design'), { recursive: true });
115
123
  reportDesignSeed(seedDesignArtifacts(featureName, process.cwd()));
116
124
  reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
117
- try {
118
- const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
119
- writeHandoff(featureName, process.cwd());
120
- } catch { /* fail-open */ }
121
125
  console.log(chalk.green(`\n✅ "${featureName}": gate plan já aprovado — ramo de design aberto (1-design/).\n`));
122
126
  return;
123
127
  }
@@ -129,7 +133,7 @@ export async function advanceCommand(featureName, options = {}) {
129
133
  return;
130
134
  }
131
135
 
132
- // `implement` has no gate, so this is what keeps unfinished slice reports out of review.
136
+ // `implement` has no gate, so this is what keeps unfinished scenarios out of review.
133
137
  // `--force` bypasses it but still names what was skipped.
134
138
  const missingArtifacts = missingPhaseArtifacts(phase, featurePath);
135
139
  if (missingArtifacts.length && !options.force) {
@@ -166,7 +170,7 @@ export async function advanceCommand(featureName, options = {}) {
166
170
  }
167
171
 
168
172
  // `setApprovalGate` replaces the gate whole: rewriting an approved gate would turn a human
169
- // signature into "auto". Skips ONLY the write, so the scaffold and handoff below still run.
173
+ // signature into "auto". Skips ONLY the write, so the scaffold below still runs.
170
174
  const priorGate = gate ? feature.approvalGates?.[gate] : null;
171
175
  const alreadyApproved = priorGate?.approved === true;
172
176
 
@@ -193,13 +197,9 @@ export async function advanceCommand(featureName, options = {}) {
193
197
  if (!existsSync(nextPath)) mkdirSync(nextPath, { recursive: true });
194
198
  }
195
199
  if (nextDir === '1-design') reportDesignSeed(seedDesignArtifacts(featureName, process.cwd()));
200
+ if (nextDir === '3-implement') seedReport(featurePath, featureName);
196
201
  if (phase === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
197
202
 
198
- // A phase transition is when a session hands the feature over: refresh handoff.md.
199
- try {
200
- const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
201
- writeHandoff(featureName, process.cwd());
202
- } catch { /* fail-open */ }
203
203
 
204
204
  console.log(chalk.green(`\n✅ Advanced "${featureName}" past the ${phase} phase.`));
205
205
  if (missingArtifacts.length) {
@@ -260,12 +260,6 @@ export async function approveCommand(featureName, gateName, options = {}) {
260
260
  ...(selfAssessed ? { selfAssessed: true } : {}),
261
261
  });
262
262
 
263
- // Approving is a session handoff too; a regeneration failure must never fail the approval.
264
- try {
265
- const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
266
- writeHandoff(featureName, process.cwd());
267
- } catch { /* fail-open */ }
268
-
269
263
  if (selfAssessed) {
270
264
  console.log(chalk.yellow('\n⚠️ Gate 3 aprovado SEM os relatórios de review independentes (--self-assessed).'));
271
265
  console.log(chalk.gray(' Registrado em feature.json → approvalGates.review.selfAssessed.'));
@@ -1,9 +1,10 @@
1
1
  import chalk from 'chalk';
2
- import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { existsSync, renameSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
3
3
  import { join, resolve, sep } from 'path';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { ADR_INDEX_PATHSPEC, updateAdrIndex } from '../../lib/specs/adr-index.js';
6
6
  import { isValidFeatureName } from '../project/worktree.js';
7
+ import { RECAP_FILES, writeRecap } from './recap.js';
7
8
 
8
9
  function assertContained(scopeDir, name, target) {
9
10
  const scopeAbs = resolve(scopeDir) + sep;
@@ -38,6 +39,17 @@ function readReviewApproved(fjPath) {
38
39
  * Never calls process.exit, and resolves everything from `cwd` so `finish` can archive inside a
39
40
  * worktree. Idempotent: an existing archive returns `{ ok: true, alreadyArchived: true }`.
40
41
  */
42
+ // plan.md stays: a squash merge drops every file born and pruned inside the branch.
43
+ function pruneArchive(dir) {
44
+ for (const entry of readdirSync(dir)) {
45
+ if (entry === 'feature.json' || Object.values(RECAP_FILES).includes(entry)) continue;
46
+ if (entry !== '2-plan') { rmSync(join(dir, entry), { recursive: true, force: true }); continue; }
47
+ for (const planEntry of readdirSync(join(dir, entry))) {
48
+ if (planEntry !== 'plan.md') rmSync(join(dir, entry, planEntry), { recursive: true, force: true });
49
+ }
50
+ }
51
+ }
52
+
41
53
  export function runArchive(feature, opts = {}) {
42
54
  const cwd = opts.cwd || process.cwd();
43
55
 
@@ -71,6 +83,10 @@ export function runArchive(feature, opts = {}) {
71
83
  }
72
84
 
73
85
  const useGit = opts.git !== false && isGitRepo(cwd);
86
+ let archivedFromSha = null;
87
+ if (useGit) {
88
+ try { archivedFromSha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim(); } catch { /* no commit yet */ }
89
+ }
74
90
 
75
91
  // `git mv` keeps history and stages in one step; an untracked folder falls back to rename.
76
92
  mkdirSync(archiveDir, { recursive: true });
@@ -96,6 +112,7 @@ export function runArchive(feature, opts = {}) {
96
112
  const fj = JSON.parse(readFileSync(fjPathAfter, 'utf8'));
97
113
  fj.status = 'completed';
98
114
  fj.archivedAt = new Date().toISOString();
115
+ if (archivedFromSha) fj.archivedFrom = { sha: archivedFromSha };
99
116
  fj.updatedAt = fj.archivedAt;
100
117
  writeFileSync(fjPathAfter, JSON.stringify(fj, null, 2), 'utf8');
101
118
  if (useGit && staged) {
@@ -113,6 +130,13 @@ export function runArchive(feature, opts = {}) {
113
130
  try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
114
131
  }
115
132
 
133
+ // Before the prune: the recap reads the Gate 3 reports the archive does not keep.
134
+ const recap = opts.recap === false ? null : writeRecap(archivePath, archivePath, feature, { cwd });
135
+ pruneArchive(archivePath);
136
+ if (useGit && staged) {
137
+ try { execFileSync('git', ['add', '-A', '--', archivePathspec], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
138
+ }
139
+
116
140
  // Scoped to this feature's pathspec, never `commit -a`. The ADR index joins only when it
117
141
  // changed: git refuses a pathspec that matches nothing, leaving the tree dirty for the merge.
118
142
  let committed = false;
@@ -144,6 +168,7 @@ export function runArchive(feature, opts = {}) {
144
168
 
145
169
  return {
146
170
  ok: true, archived: true, committed, staged, archivePath,
171
+ recap: recap?.ok ? recap.path : null,
147
172
  revocationProblems: adrIndex.problems,
148
173
  adrIndex: adrIndex.changed ? { added: adrIndex.added, revoked: adrIndex.revoked, narrowed: adrIndex.narrowed } : null,
149
174
  };
@@ -180,6 +205,7 @@ export async function archiveCommand(featureName, options = {}) {
180
205
 
181
206
  console.log(chalk.green(`\n📦 Archived "${featureName}" → .morph/archive/${featureName}/`));
182
207
  console.log(chalk.gray(` status: completed, archivedAt stamped in feature.json.`));
208
+ if (result.recap) console.log(chalk.gray(` recap: ${result.recap} (morph-spec recap ${featureName} --piper narra com o Piper)`));
183
209
  if (result.adrIndex) {
184
210
  const { added, revoked, narrowed } = result.adrIndex;
185
211
  console.log(chalk.gray(` ADR index (.morph/specs/adrs/): +${added.length} −${revoked.length} ~${narrowed.length}.`));
@@ -34,7 +34,7 @@ export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedB
34
34
  if (block) {
35
35
  return {
36
36
  title: `O gate "plan" não pode ser auto-aprovado — ${block}.`,
37
- hints: ['Com plan-check verde, rode de novo; ou aprove com --approver <nome>. O --force não dispensa o checks.json.'],
37
+ hints: ['Sem checks.json fresco: plan-check verde e rode de novo. Com degrau: aprove com --approver <nome> ou pela resposta humana. O --force não dispensa nenhum dos dois.'],
38
38
  };
39
39
  }
40
40
  }
@@ -185,11 +185,6 @@ export async function createCommand(featureName, options = {}) {
185
185
  // Re-materialize after the copy: the setup's earlier materialize ran with no features yet.
186
186
  const remat = await materializeState(worktreeResult.path);
187
187
  worktreeResult.materialized = remat.materialized;
188
- try {
189
- const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
190
- // `worktree` was stripped from this copy above, so pass where it actually lives.
191
- writeHandoff(featureName, worktreeResult.path, { worktree: { path: worktreeResult.path, branch: worktreeResult.branch } });
192
- } catch { /* fail-open */ }
193
188
  } catch { /* fail-open — the feature already exists in root */ }
194
189
  }
195
190
  } else {
@@ -292,7 +292,8 @@ export async function finishCommand(feature, options = {}) {
292
292
  }
293
293
  const dirtLines = withoutLeftoverIgnore(cleanCwd, dirty.split(/\r?\n/).filter((l) => l.trim()));
294
294
  const testArtifacts = untrackedTestArtifacts(dirtLines);
295
- if (dirtLines.length > testArtifacts.length) {
295
+ const commitState = dirtLines.length > testArtifacts.length && onlyFeatureStateDirty(cleanCwd, feature);
296
+ if (dirtLines.length > testArtifacts.length && !commitState) {
296
297
  emit({ finished: false, error: isWorktreeFlow ? 'worktree-dirty' : 'working-dir-dirty', hint: 'commit seu trabalho na branch antes do finish', feature });
297
298
  process.exit(1);
298
299
  }
@@ -341,6 +342,10 @@ export async function finishCommand(feature, options = {}) {
341
342
 
342
343
  // Every refusal is behind us: the leftover of a killed `graph refresh` goes now, never before.
343
344
  dropLeftoverIgnore(cleanCwd);
345
+ if (commitState && !commitFeatureState(cleanCwd, feature)) {
346
+ emit({ finished: false, error: 'state-commit-failed', hint: `não foi possível commitar .morph/features/${feature} (identidade git ausente?) — commite o estado na branch e rode o finish de novo`, feature });
347
+ process.exit(1);
348
+ }
344
349
  if (isWorktreeFlow) {
345
350
  for (const p of testArtifacts) rmSync(join(cleanCwd, p), { recursive: true, force: true });
346
351
  }
@@ -409,6 +414,15 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
409
414
 
410
415
  if (defaultBranch) out.defaultBranch = defaultBranch;
411
416
 
417
+ try {
418
+ const tree = worktreePath || root;
419
+ const dirt = gitStatusPorcelain(tree);
420
+ const work = dirt === null ? [] : withoutLeftoverIgnore(tree, dirt.split(/\r?\n/).filter((l) => l.trim()));
421
+ if (work.length > untrackedTestArtifacts(work).length && onlyFeatureStateDirty(tree, feature)) {
422
+ out.wouldDo.unshift(`commitar o estado sujo de .morph/features/${feature} (${stateCommitMessage(feature)}) na branch ${branch}`);
423
+ }
424
+ } catch { /* o pré-voo nunca lança */ }
425
+
412
426
  // A retenção automática também é mutação do finish real; a contagem é a de AGORA, antes do
413
427
  // backup que este finish gravar.
414
428
  try {
@@ -808,6 +822,34 @@ function untrackedTestArtifacts(lines) {
808
822
  .filter((p) => isTestArtifact(p));
809
823
  }
810
824
 
825
+ const stateCommitMessage = (feature) => `chore(morph): estado da feature ${feature}`;
826
+
827
+ // `--untracked-files=all`: o porcelain padrão colapsa `?? .morph/` e esconderia sujeira fora do escopo.
828
+ function onlyFeatureStateDirty(cwd, feature) {
829
+ let out;
830
+ try {
831
+ out = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd, stdio: 'pipe' }).toString();
832
+ } catch {
833
+ return false;
834
+ }
835
+ const scope = `.morph/features/${feature}/`;
836
+ const paths = out.split(/\r?\n/).filter((l) => l.trim())
837
+ .flatMap((l) => l.slice(3).split(' -> '))
838
+ .map((p) => p.replace(/^"|"$/g, ''));
839
+ return paths.length > 0 && paths.every((p) => p.startsWith(scope));
840
+ }
841
+
842
+ function commitFeatureState(cwd, feature) {
843
+ const scope = `.morph/features/${feature}`;
844
+ try {
845
+ execFileSync('git', ['add', '-A', '--', scope], { cwd, stdio: 'pipe' });
846
+ execFileSync('git', ['commit', '-q', '-m', stateCommitMessage(feature), '--', scope], { cwd, stdio: 'pipe' });
847
+ return true;
848
+ } catch {
849
+ return false;
850
+ }
851
+ }
852
+
811
853
  function gitStatusPorcelain(cwd) {
812
854
  try {
813
855
  return execFileSync('git', ['status', '--porcelain'], { cwd, stdio: 'pipe' }).toString();
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { getFeature } from '../../core/state/state-manager.js';
3
- import { shouldPauseGate, getRiskSignals, classifyE2eSkip, planChecksPause, securityReviewFor } from '../../lib/risk-detector.js';
3
+ import { shouldPauseGate, getRiskSignals, classifyE2eSkip, planChecksPause, securityReviewFor, pausingRungs, migrationsTouched, contractsTouched, humanApprover, rungPause, planCritiquePause } from '../../lib/risk-detector.js';
4
+ import { readPlanCritique } from '../../lib/plan/critique.js';
4
5
  import { reviewReports, reviewSignal, requiredReviewRoles, verifierMediaSection } from '../../lib/review-reports.js';
5
6
  import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
6
7
  import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
@@ -9,33 +10,22 @@ import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } fro
9
10
  import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
10
11
  import { readMorphConfig } from '../../lib/tasks/test-runner.js';
11
12
  import { readPlanChecks } from '../../lib/plan/checks.js';
13
+ import { featureGeneratesMedia } from '../../lib/plan/media.js';
14
+ import { readFileSync, existsSync } from 'fs';
15
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
12
16
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
13
17
 
18
+ export { featureGeneratesMedia, MEDIA_STANDARD_IDS, standardIdOf } from '../../lib/plan/media.js';
19
+
14
20
  // Only signals NOT derivable from disk may be overridden: overriding a disk fact (e.g. `workType`
15
21
  // away from hotfix) would silently defeat the policy this command makes deterministic.
16
22
  const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence'];
17
23
 
18
- export const MEDIA_STANDARD_IDS = [
19
- 'ai-agents-modalities-image-gen',
20
- 'ai-agents-media-video',
21
- 'ai-agents-media-pipeline',
22
- ];
23
-
24
- // O plano escreve o standard pelo id ou pelo caminho (com ou sem `framework/`/`standards/`).
25
- export function standardIdOf(value) {
26
- return String(value ?? '').trim().replace(/\\/g, '/')
27
- .replace(/^(\.morph\/)?(framework\/)?(standards\/)?/, '')
28
- .replace(/\.md$/, '')
29
- .replace(/\//g, '-');
24
+ function readPlanText(cwd, featureName) {
25
+ const path = getAbsoluteOutputPath(cwd, featureName, 'plan');
26
+ try { return existsSync(path) ? readFileSync(path, 'utf8') : ''; } catch { return ''; }
30
27
  }
31
28
 
32
- // Decide se um guardrail arma: um predicado que sempre devolve `false` desarma em silêncio a
33
- // pausa de mídia da review e a rota opus.
34
- export function featureGeneratesMedia(slices) {
35
- if (!Array.isArray(slices)) return false;
36
- return slices.some((s) => Array.isArray(s?.standards)
37
- && s.standards.some((id) => MEDIA_STANDARD_IDS.includes(standardIdOf(id))));
38
- }
39
29
 
40
30
  // `declared: false` (nenhum caminho declarado: regra inerte) e `touched: []` (declarou, nada
41
31
  // tocado) não podem colapsar num array vazio. COLETA, NUNCA DECIDE: política no coletor cria
@@ -70,7 +60,11 @@ export function collectReviewSignal(cwd, featureName, feature, planChecks = read
70
60
 
71
61
  /** Why an automatic signature may not close the plan gate: the rule that makes gate-check pause it. */
72
62
  export function planAutoApprovalBlock(cwd, featureName) {
73
- const reason = planChecksPause(readPlanChecks(cwd, featureName).status);
63
+ const planChecks = readPlanChecks(cwd, featureName);
64
+ const workType = getFeature(featureName)?.workType || 'feature';
65
+ const reason = planChecksPause(planChecks.status)
66
+ ?? planCritiquePause(readPlanCritique(cwd, featureName, { workType, planChecks }))
67
+ ?? rungPause({ gate: 'plan', rungs: pausingRungs(planChecks.checks) })?.reason;
74
68
  return reason ? reason.replace('{feature}', featureName) : null;
75
69
  }
76
70
 
@@ -79,7 +73,6 @@ export function planAutoApprovalBlock(cwd, featureName) {
79
73
  // waiver from missing proof.
80
74
  export function collectSignals(featureName, feature, cwd, gateName) {
81
75
  const planChecks = readPlanChecks(cwd, featureName);
82
- const slices = planChecks.checks?.slices ?? null;
83
76
  const defaultBranch = detectDefaultBranch(cwd);
84
77
  const sha = getHeadSha(cwd);
85
78
  const verification = feature.verification || null;
@@ -88,12 +81,15 @@ export function collectSignals(featureName, feature, cwd, gateName) {
88
81
  // Disk-derived, never overridable: a caller cannot spoof which gate is being decided.
89
82
  gate: gateName,
90
83
  workType: feature.workType || 'feature',
91
- sliceCount: slices ? slices.length : null,
92
84
  standardsViolations: [],
93
85
  designSystemDivergence: false,
94
86
  };
95
87
 
96
- if (gateName === 'plan') signals.planChecks = planChecks.status;
88
+ if (gateName === 'plan') {
89
+ signals.planChecks = planChecks.status;
90
+ signals.planCritique = readPlanCritique(cwd, featureName, { workType: signals.workType, planChecks });
91
+ }
92
+ if (gateName === 'plan' || gateName === 'review') signals.rungs = pausingRungs(planChecks.checks);
97
93
 
98
94
  // Só onde o checklist decide algo: no fecho do design e no ship.
99
95
  if (gateName === 'uiux' || gateName === 'review') {
@@ -104,13 +100,20 @@ export function collectSignals(featureName, feature, cwd, gateName) {
104
100
  signals.reviewReports = collectReviewSignal(cwd, featureName, feature, planChecks);
105
101
  signals.mediaClaims = {
106
102
  section: verifierMediaSection(cwd, featureName),
107
- featureGeneratesMedia: featureGeneratesMedia(slices),
103
+ featureGeneratesMedia: featureGeneratesMedia(readPlanText(cwd, featureName)),
108
104
  };
105
+ let changed = [];
106
+ try { changed = changedPathsAgainst(defaultBranch, cwd); } catch { changed = []; }
107
+ signals.migrationsTouched = migrationsTouched(changed);
108
+ signals.contractsTouched = contractsTouched(changed);
109
+ signals.planApprovedByHuman = feature.approvalGates?.plan?.approved === true
110
+ && humanApprover(feature.approvalGates.plan.approvedBy);
109
111
  }
110
112
 
111
113
  // verificationStale é fail-open: stamp sem sha (checkout sem git) ou ausente lê como NOT stale.
112
114
  signals.verification = verification;
113
115
  if (verification?.status) signals.verificationStatus = verification.status;
116
+ signals.failStreak = feature.verifications?.feature?.failStreak ?? 0;
114
117
  signals.verificationStale = treeMovedSince(cwd, verification?.sha, sha);
115
118
 
116
119
  // Separado do rollup: um `skip` NÃO reprova `verification.status`. `null` é DESCONHECIDO, nunca
@@ -189,10 +192,12 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
189
192
  const computed = shouldPauseGate({ trust }, signals);
190
193
  const manual = feature.gateDecisions?.[gateName];
191
194
  const manualPause = manual?.source === 'manual' && manual.pause === true;
192
- const { pause, reason, signal } = manualPause ? { pause: true, reason: manual.reason, signal: 'manual' } : computed;
195
+ const { pause, reason, signal, resolver } = manualPause
196
+ ? { pause: true, reason: manual.reason, signal: 'manual', resolver: 'human' }
197
+ : computed;
193
198
 
194
199
  const entry = await persistGateDecision(featureName, gateName, {
195
- pause, reason, signal, signals, trust, cwd, source: manualPause ? 'manual' : 'gate-check',
200
+ pause, reason, signal, resolver, signals, trust, cwd, source: manualPause ? 'manual' : 'gate-check',
196
201
  });
197
202
 
198
203
  if (options.json) {
@@ -201,9 +206,11 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
201
206
  }
202
207
 
203
208
  const allSignals = getRiskSignals(signals);
204
- console.log(entry.pause
205
- ? chalk.yellow(`\n⏸️ Gate "${gateName}" — PAUSE (${featureName})`)
206
- : chalk.green(`\n▶️ Gate "${gateName}" — auto-pass (${featureName})`));
209
+ console.log(!entry.pause
210
+ ? chalk.green(`\n▶️ Gate "${gateName}" — auto-pass (${featureName})`)
211
+ : entry.resolver === 'session'
212
+ ? chalk.yellow(`\n🔧 Gate "${gateName}" — PAUSE, resolver: session (${featureName}): do what the reason says and run gate-check again`)
213
+ : chalk.yellow(`\n⏸️ Gate "${gateName}" — PAUSE, resolver: human (${featureName})`));
207
214
  console.log(chalk.gray(` reason: ${entry.reason} | trust: ${entry.trust} | decidedAt: ${entry.decidedAt}`));
208
215
  if (manualPause) {
209
216
  console.log(chalk.gray(` pausa manual mantida (sinais calculados: ${computed.reason}); só \`morph-spec gate-decision ${featureName} ${gateName} --pause false\` a libera`));
@@ -32,9 +32,10 @@ export function readTrustFromConfig() {
32
32
  * The ONE write path of `gateDecisions.{gate}`, shared by `gate-decision` and `gate-check`.
33
33
  * Does not validate `gateName`. A uiux decision records the `mockupsHash` it saw.
34
34
  */
35
- export async function persistGateDecision(featureName, gateName, { pause, reason, signal, signals = {}, trust, cwd = process.cwd(), source } = {}) {
35
+ export async function persistGateDecision(featureName, gateName, { pause, reason, signal, resolver, signals = {}, trust, cwd = process.cwd(), source } = {}) {
36
36
  const entry = {
37
37
  pause,
38
+ resolver: pause ? resolver ?? 'human' : null,
38
39
  reason: reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
39
40
  signals,
40
41
  trust: trust || readTrustFromConfig(),
@@ -44,7 +45,7 @@ export async function persistGateDecision(featureName, gateName, { pause, reason
44
45
  };
45
46
 
46
47
  await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
47
- recordGateEvent(cwd, { feature: featureName, gate: gateName, step: 'decision', pause, signal, source });
48
+ recordGateEvent(cwd, { feature: featureName, gate: gateName, step: 'decision', pause, signal, source, resolver: entry.resolver });
48
49
  return entry;
49
50
  }
50
51
 
@@ -20,5 +20,6 @@ export function nextCommand(featureName, options = {}) {
20
20
  return;
21
21
  }
22
22
  console.log(formatNext(next));
23
+ for (const line of next.how ?? []) console.log(` ${line}`);
23
24
  if (owner.tree !== process.cwd()) console.log(chalk.gray(` árvore dona: ${owner.tree}`));
24
25
  }