@polymorphism-tech/morph-spec 9.8.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 (185) hide show
  1. package/CLAUDE.md +10 -10
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +40 -43
  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 +83 -94
  18. package/framework/evals/review-checklist.md +45 -53
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/core/README.md +1 -1
  21. package/framework/hooks/claude-code/core/denylist.json +4 -4
  22. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  23. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  24. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  25. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  26. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  27. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  28. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  29. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  30. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  31. package/framework/hooks/shared/e2e-check.js +0 -0
  32. package/framework/hooks/shared/gate-event.js +2 -2
  33. package/framework/hooks/shared/graph-refresh.js +20 -3
  34. package/framework/hooks/shared/heavy-command.js +32 -1
  35. package/framework/hooks/shared/heavy-config.js +1 -1
  36. package/framework/hooks/shared/next-step.js +63 -24
  37. package/framework/hooks/shared/phase-utils.js +6 -9
  38. package/framework/hooks/shared/report-progress.js +86 -0
  39. package/framework/hooks/shared/state-reader.js +3 -2
  40. package/framework/hooks/statusline.js +332 -0
  41. package/framework/phases.json +32 -10
  42. package/framework/rules/ai-agents-standards.md +9 -1
  43. package/framework/rules/csharp-standards.md +4 -0
  44. package/framework/rules/ef-standards.md +20 -0
  45. package/framework/rules/frontend-standards.md +1 -1
  46. package/framework/rules/implement-report.md +25 -0
  47. package/framework/rules/infrastructure-standards.md +2 -0
  48. package/framework/rules/nextjs-standards.md +7 -0
  49. package/framework/rules/plan-grammar.md +53 -0
  50. package/framework/rules/ui-design.md +27 -0
  51. package/framework/schemas/config.schema.json +11 -18
  52. package/framework/schemas/feature.schema.json +7 -1
  53. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  54. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  55. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  56. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  57. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  58. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  59. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  60. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  61. package/framework/skills/core/morph-apply/references/review.md +65 -0
  62. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  63. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  64. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  65. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  66. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  67. package/framework/standards/STANDARDS.json +32 -5
  68. package/framework/standards/STANDARDS.meta.json +5 -1
  69. package/framework/standards/ai-agents/agent-session.md +2 -3
  70. package/framework/standards/ai-agents/context-providers.md +2 -2
  71. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  72. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  73. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  74. package/framework/standards/ai-agents/setup.md +1 -1
  75. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  76. package/framework/standards/architecture/stack-baseline.md +47 -0
  77. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  78. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  79. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  80. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  81. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  82. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  83. package/framework/templates/REGISTRY.json +1 -2
  84. package/framework/templates/context/ARCHITECTURE.md +1 -1
  85. package/framework/templates/docs/plan.md +12 -12
  86. package/framework/templates/docs/ui-mockups.md +17 -105
  87. package/package.json +1 -1
  88. package/src/commands/e2e/e2e.js +88 -23
  89. package/src/commands/graph/diff.js +74 -0
  90. package/src/commands/graph/status.js +1 -1
  91. package/src/commands/observability/cost.js +2 -4
  92. package/src/commands/observability/telemetry.js +1 -1
  93. package/src/commands/project/doctor.js +23 -39
  94. package/src/commands/project/fleet.js +5 -6
  95. package/src/commands/project/init.js +63 -36
  96. package/src/commands/project/stack-scan.js +3 -42
  97. package/src/commands/project/stack-set.js +26 -0
  98. package/src/commands/project/status.js +12 -13
  99. package/src/commands/project/update.js +39 -3
  100. package/src/commands/project/worktree.js +3 -14
  101. package/src/commands/state/advance.js +17 -17
  102. package/src/commands/state/approve.js +0 -6
  103. package/src/commands/state/archive.js +27 -1
  104. package/src/commands/state/auto-approval.js +1 -1
  105. package/src/commands/state/create.js +0 -5
  106. package/src/commands/state/gate-check.js +37 -30
  107. package/src/commands/state/gate-decision.js +3 -2
  108. package/src/commands/state/next.js +1 -0
  109. package/src/commands/state/recap.js +67 -0
  110. package/src/commands/state/report-check.js +36 -178
  111. package/src/commands/validation/plan-check.js +16 -11
  112. package/src/commands/validation/validate-feature.js +3 -3
  113. package/src/commands/validation/verify.js +3 -6
  114. package/src/core/paths/output-schema.js +3 -3
  115. package/src/core/state/state-manager.js +2 -4
  116. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  117. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  118. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  119. package/src/dashboard/app/pages/Activity.jsx +4 -0
  120. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  121. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  122. package/src/dashboard/app/pages/Features.jsx +7 -7
  123. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  124. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  125. package/src/dashboard/scanner/activity-scanner.js +29 -0
  126. package/src/dashboard/scanner/agents-scanner.js +11 -14
  127. package/src/dashboard/scanner/features-scanner.js +45 -24
  128. package/src/dashboard/scanner/project-api.js +11 -2
  129. package/src/dashboard/scanner/skills-scanner.js +11 -1
  130. package/src/lib/e2e/applicability.js +3 -1
  131. package/src/lib/git/tree-state.js +1 -16
  132. package/src/lib/graph/diff-tree.js +127 -0
  133. package/src/lib/graph/graphify-runtime.js +12 -5
  134. package/src/lib/installers/memory-installer.js +2 -2
  135. package/src/lib/installers/rules-installer.js +57 -0
  136. package/src/lib/installers/stack-assets.js +17 -0
  137. package/src/lib/installers/stack-sync.js +219 -0
  138. package/src/lib/plan/checks.js +55 -110
  139. package/src/lib/plan/critique.js +28 -0
  140. package/src/lib/plan/media.js +42 -0
  141. package/src/lib/plan/plan-html.js +125 -0
  142. package/src/lib/plan/plan-parser.js +34 -45
  143. package/src/lib/recap/piper.js +36 -0
  144. package/src/lib/recap/recap.js +232 -0
  145. package/src/lib/review-reports.js +28 -4
  146. package/src/lib/risk-detector.js +90 -11
  147. package/src/lib/specs/screen-checklist.js +33 -48
  148. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  149. package/src/lib/telemetry/metrics.js +27 -29
  150. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  151. package/src/lib/validators/verify-runner.js +0 -7
  152. package/src/lib/worktree/fleet-scanner.js +7 -20
  153. package/src/lib/worktree/worktree-audit.js +0 -37
  154. package/src/scripts/setup-infra.js +15 -50
  155. package/src/utils/agents-installer.js +27 -201
  156. package/src/utils/hooks-installer.js +11 -95
  157. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  158. package/framework/hooks/claude-code/statusline.py +0 -699
  159. package/framework/hooks/claude-code/statusline.sh +0 -11
  160. package/framework/hooks/shared/agent-reap.js +0 -74
  161. package/framework/hooks/shared/slice-progress.js +0 -110
  162. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  163. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  164. package/framework/skills/core/morph-review/SKILL.md +0 -129
  165. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  166. package/framework/standards/PROVENANCE.json +0 -119
  167. package/src/commands/project/worktree-reap.js +0 -34
  168. package/src/commands/state/handoff.js +0 -65
  169. package/src/commands/validation/mutate.js +0 -263
  170. package/src/commands/validation/scout-check.js +0 -28
  171. package/src/lib/generators/handoff-generator.js +0 -172
  172. package/src/lib/mutation/adapters.js +0 -79
  173. package/src/lib/mutation/commands.js +0 -61
  174. package/src/lib/mutation/dotnet-introspection.js +0 -281
  175. package/src/lib/mutation/manifest.js +0 -144
  176. package/src/lib/mutation/plan.js +0 -44
  177. package/src/lib/mutation/report.js +0 -68
  178. package/src/lib/mutation/runner.js +0 -301
  179. package/src/lib/mutation/schemata.js +0 -60
  180. package/src/lib/mutation/snapshot.js +0 -65
  181. package/src/lib/mutation/subset.js +0 -109
  182. package/src/lib/mutation/trx.js +0 -50
  183. package/src/lib/scout/scout-check.js +0 -293
  184. package/src/lib/validators/traceability-check.js +0 -103
  185. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -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 {
@@ -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
  }
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { buildRecap, changeSummary, recapScenes, renderRecapHtml } from '../../lib/recap/recap.js';
6
+ import { narrateWithPiper, resolvePiper } from '../../lib/recap/piper.js';
7
+
8
+ export const RECAP_FILES = { html: 'recap.html', data: 'recap.json' };
9
+
10
+ /** Writes `recap.html` + `recap.json` into `targetDir` from the live feature folder. Never throws. */
11
+ export function writeRecap(featureDir, targetDir, feature, { cwd }) {
12
+ try {
13
+ const data = buildRecap(featureDir, feature, { changes: changeSummary(cwd) });
14
+ writeFileSync(join(targetDir, RECAP_FILES.data), JSON.stringify(data, null, 2));
15
+ writeFileSync(join(targetDir, RECAP_FILES.html), renderRecapHtml(data));
16
+ return { ok: true, path: join(targetDir, RECAP_FILES.html) };
17
+ } catch (err) {
18
+ return { ok: false, reason: err.message };
19
+ }
20
+ }
21
+
22
+ // Active feature: rebuilt from its folder and the branch diff. Archived: the data the finish froze,
23
+ // since the archive keeps neither the reports nor the branch.
24
+ export function recapCommand(feature, options = {}) {
25
+ const cwd = options.path || process.cwd();
26
+ const active = join(cwd, '.morph', 'features', feature);
27
+ const archived = join(cwd, '.morph', 'archive', feature);
28
+ let data;
29
+ let dir;
30
+ if (existsSync(active)) {
31
+ dir = active;
32
+ data = buildRecap(active, feature, { changes: changeSummary(cwd) });
33
+ writeFileSync(join(dir, RECAP_FILES.data), JSON.stringify(data, null, 2));
34
+ } else if (existsSync(join(archived, RECAP_FILES.data))) {
35
+ dir = archived;
36
+ data = JSON.parse(readFileSync(join(archived, RECAP_FILES.data), 'utf8'));
37
+ } else {
38
+ const payload = { ok: false, reason: existsSync(archived) ? 'archived-without-recap' : 'feature-not-found' };
39
+ if (options.json) console.log(JSON.stringify(payload));
40
+ else console.error(chalk.yellow(`\n○ ${payload.reason === 'feature-not-found' ? `Feature "${feature}" não existe.` : `"${feature}" foi arquivada antes do recap existir.`}\n`));
41
+ process.exit(1);
42
+ }
43
+
44
+ let narration = 'sistema';
45
+ let path = join(dir, RECAP_FILES.html);
46
+ let note = null;
47
+ if (options.piper) {
48
+ const piper = resolvePiper();
49
+ const audio = piper.ok ? narrateWithPiper(recapScenes(data), piper) : null;
50
+ if (audio) {
51
+ // Audio is megabytes: it goes to a temp file, never into the committed archive.
52
+ path = join(tmpdir(), `morph-recap-${feature}.html`);
53
+ writeFileSync(path, renderRecapHtml(data, { audio }));
54
+ narration = 'piper';
55
+ } else {
56
+ note = piper.ok ? 'o piper falhou numa cena — ficou a voz do sistema' : piper.reason;
57
+ }
58
+ }
59
+ if (narration === 'sistema') writeFileSync(path, renderRecapHtml(data));
60
+
61
+ if (options.json) {
62
+ console.log(JSON.stringify({ ok: true, path, narration, note }));
63
+ return;
64
+ }
65
+ console.log(chalk.green(`\n✅ Recap de ${feature}: ${path}`));
66
+ console.log(chalk.gray(` narração: ${narration === 'piper' ? 'Piper (áudio embutido)' : 'voz do sistema, pelo navegador'}${note ? ` — ${note}` : ''}\n`));
67
+ }