@polymorphism-tech/morph-spec 9.8.0 → 10.1.1

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 (202) hide show
  1. package/CLAUDE.md +12 -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 +28 -36
  6. package/docs/QUICKSTART.md +31 -191
  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-preflight.md +3 -3
  16. package/framework/commands/morph-review.md +15 -0
  17. package/framework/commands/morph-status.md +1 -1
  18. package/framework/docs/CLI.md +84 -95
  19. package/framework/evals/review-checklist.md +45 -53
  20. package/framework/hooks/README.md +4 -4
  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/next-step.js +63 -24
  38. package/framework/hooks/shared/phase-utils.js +6 -9
  39. package/framework/hooks/shared/report-progress.js +86 -0
  40. package/framework/hooks/shared/state-reader.js +3 -2
  41. package/framework/hooks/statusline.js +332 -0
  42. package/framework/mcp/registry.json +4 -4
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +10 -2
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/data-standards.md +1 -1
  47. package/framework/rules/ef-standards.md +20 -0
  48. package/framework/rules/frontend-standards.md +1 -1
  49. package/framework/rules/hangfire-standards.md +1 -1
  50. package/framework/rules/implement-report.md +25 -0
  51. package/framework/rules/infrastructure-standards.md +2 -0
  52. package/framework/rules/neon-auth-standards.md +1 -1
  53. package/framework/rules/neon-standards.md +3 -3
  54. package/framework/rules/nextjs-standards.md +7 -0
  55. package/framework/rules/plan-grammar.md +53 -0
  56. package/framework/rules/ui-design.md +27 -0
  57. package/framework/schemas/config.schema.json +11 -18
  58. package/framework/schemas/feature.schema.json +7 -1
  59. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  60. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  61. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  62. package/framework/skills/core/morph-apply/SKILL.md +39 -166
  63. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  64. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  65. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  66. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  67. package/framework/skills/core/morph-apply/references/review.md +65 -0
  68. package/framework/skills/core/morph-plan/SKILL.md +49 -109
  69. package/framework/skills/core/morph-plan/references/critique.md +43 -0
  70. package/framework/skills/core/morph-plan/references/interview.md +37 -0
  71. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  72. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  73. package/framework/standards/STANDARDS.json +51 -28
  74. package/framework/standards/STANDARDS.meta.json +16 -19
  75. package/framework/standards/ai-agents/agent-session.md +2 -3
  76. package/framework/standards/ai-agents/context-providers.md +2 -2
  77. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  78. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  79. package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -6
  80. package/framework/standards/ai-agents/setup.md +1 -1
  81. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  82. package/framework/standards/architecture/stack-baseline.md +70 -0
  83. package/framework/standards/backend/database/postgresql/database.md +22 -1
  84. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
  85. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
  86. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +12 -13
  87. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  88. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  89. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  90. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  91. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  92. package/framework/standards/infrastructure/docker/local-compose-isolation.md +2 -2
  93. package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
  94. package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
  95. package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
  96. package/framework/standards/integration/mcp/mcp-tools.md +20 -20
  97. package/framework/templates/REGISTRY.json +1 -2
  98. package/framework/templates/context/ARCHITECTURE.md +1 -1
  99. package/framework/templates/docs/plan.md +15 -12
  100. package/framework/templates/docs/ui-mockups.md +17 -105
  101. package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
  102. package/package.json +1 -1
  103. package/src/commands/e2e/e2e.js +88 -23
  104. package/src/commands/graph/diff.js +74 -0
  105. package/src/commands/graph/status.js +1 -1
  106. package/src/commands/observability/cost.js +2 -4
  107. package/src/commands/observability/telemetry.js +1 -1
  108. package/src/commands/project/doctor.js +23 -39
  109. package/src/commands/project/fleet.js +5 -6
  110. package/src/commands/project/init.js +63 -36
  111. package/src/commands/project/stack-scan.js +3 -42
  112. package/src/commands/project/stack-set.js +26 -0
  113. package/src/commands/project/status.js +12 -13
  114. package/src/commands/project/update.js +39 -3
  115. package/src/commands/project/worktree.js +3 -14
  116. package/src/commands/state/advance.js +17 -17
  117. package/src/commands/state/approve.js +0 -6
  118. package/src/commands/state/archive.js +27 -1
  119. package/src/commands/state/auto-approval.js +2 -2
  120. package/src/commands/state/create.js +1 -6
  121. package/src/commands/state/gate-check.js +38 -31
  122. package/src/commands/state/gate-decision.js +3 -2
  123. package/src/commands/state/next.js +1 -0
  124. package/src/commands/state/recap.js +67 -0
  125. package/src/commands/state/report-check.js +36 -178
  126. package/src/commands/validation/plan-check.js +16 -11
  127. package/src/commands/validation/validate-feature.js +3 -3
  128. package/src/commands/validation/verify.js +3 -6
  129. package/src/core/paths/output-schema.js +3 -3
  130. package/src/core/state/state-manager.js +2 -4
  131. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  132. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  133. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  134. package/src/dashboard/app/pages/Activity.jsx +4 -0
  135. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  136. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  137. package/src/dashboard/app/pages/Features.jsx +7 -7
  138. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  139. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  140. package/src/dashboard/scanner/activity-scanner.js +29 -0
  141. package/src/dashboard/scanner/agents-scanner.js +11 -14
  142. package/src/dashboard/scanner/features-scanner.js +45 -24
  143. package/src/dashboard/scanner/project-api.js +11 -2
  144. package/src/dashboard/scanner/skills-scanner.js +11 -1
  145. package/src/lib/detectors/index.js +1 -1
  146. package/src/lib/e2e/applicability.js +3 -1
  147. package/src/lib/git/tree-state.js +1 -16
  148. package/src/lib/graph/diff-tree.js +127 -0
  149. package/src/lib/graph/graphify-runtime.js +12 -5
  150. package/src/lib/installers/memory-installer.js +3 -3
  151. package/src/lib/installers/rules-installer.js +57 -0
  152. package/src/lib/installers/stack-assets.js +17 -0
  153. package/src/lib/installers/stack-sync.js +219 -0
  154. package/src/lib/plan/checks.js +61 -109
  155. package/src/lib/plan/critique.js +28 -0
  156. package/src/lib/plan/media.js +42 -0
  157. package/src/lib/plan/plan-html.js +125 -0
  158. package/src/lib/plan/plan-parser.js +34 -45
  159. package/src/lib/recap/piper.js +36 -0
  160. package/src/lib/recap/recap.js +232 -0
  161. package/src/lib/review-reports.js +28 -4
  162. package/src/lib/risk-detector.js +90 -11
  163. package/src/lib/specs/screen-checklist.js +33 -48
  164. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  165. package/src/lib/telemetry/metrics.js +27 -29
  166. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  167. package/src/lib/validators/verify-runner.js +0 -7
  168. package/src/lib/worktree/fleet-scanner.js +7 -20
  169. package/src/lib/worktree/worktree-audit.js +0 -37
  170. package/src/scripts/setup-infra.js +15 -50
  171. package/src/utils/agents-installer.js +27 -201
  172. package/src/utils/hooks-installer.js +11 -95
  173. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  174. package/framework/hooks/claude-code/statusline.py +0 -699
  175. package/framework/hooks/claude-code/statusline.sh +0 -11
  176. package/framework/hooks/shared/agent-reap.js +0 -74
  177. package/framework/hooks/shared/slice-progress.js +0 -110
  178. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  179. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  180. package/framework/skills/core/morph-review/SKILL.md +0 -129
  181. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  182. package/framework/standards/PROVENANCE.json +0 -119
  183. package/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
  184. package/src/commands/project/worktree-reap.js +0 -34
  185. package/src/commands/state/handoff.js +0 -65
  186. package/src/commands/validation/mutate.js +0 -263
  187. package/src/commands/validation/scout-check.js +0 -28
  188. package/src/lib/generators/handoff-generator.js +0 -172
  189. package/src/lib/mutation/adapters.js +0 -79
  190. package/src/lib/mutation/commands.js +0 -61
  191. package/src/lib/mutation/dotnet-introspection.js +0 -281
  192. package/src/lib/mutation/manifest.js +0 -144
  193. package/src/lib/mutation/plan.js +0 -44
  194. package/src/lib/mutation/report.js +0 -68
  195. package/src/lib/mutation/runner.js +0 -301
  196. package/src/lib/mutation/schemata.js +0 -60
  197. package/src/lib/mutation/snapshot.js +0 -65
  198. package/src/lib/mutation/subset.js +0 -109
  199. package/src/lib/mutation/trx.js +0 -50
  200. package/src/lib/scout/scout-check.js +0 -293
  201. package/src/lib/validators/traceability-check.js +0 -103
  202. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -1,5 +1,4 @@
1
- import { join, dirname, resolve } from 'path';
2
- import { fileURLToPath } from 'url';
1
+ import { join, resolve } from 'path';
3
2
  import { homedir } from 'os';
4
3
  import fs from 'fs-extra';
5
4
  import ora from 'ora';
@@ -28,13 +27,43 @@ import {
28
27
  setupStitch,
29
28
  loadMcpRegistry
30
29
  } from '../../lib/installers/mcp-installer.js';
31
- import { setupInfra, installRulesForStack, installCommandsForStack } from '../../scripts/setup-infra.js';
32
- import { installSkills } from '../../utils/skills-installer.js';
33
- import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
30
+ import { setupInfra } from '../../scripts/setup-infra.js';
34
31
  import { getProjectTags } from '../../lib/stack-filter.js';
35
32
  import { updateCommand } from './update.js';
36
33
  import { resolvePrimaryRoot } from './worktree.js';
37
34
  import { registerProject } from '../../lib/projects/registry.js';
35
+ import { setProjectStack } from '../../lib/installers/stack-sync.js';
36
+ import { installStackAssets } from '../../lib/installers/stack-assets.js';
37
+
38
+ const GREENFIELD_CHOICES = [
39
+ { name: 'dotnet+nextjs — stack do Morph: .NET 10 + EF Core/Neon + Next.js/Shadcn (recomendada)', value: { stack: 'dotnet+nextjs', integrations: ['neon'] } },
40
+ { name: 'dotnet — só a API .NET (EF Core/Neon)', value: { stack: 'dotnet', integrations: ['neon'] } },
41
+ { name: 'nextjs — só o app web Next.js', value: { stack: 'nextjs', integrations: [] } },
42
+ { name: 'Decidir depois (o /morph-plan pergunta)', value: null },
43
+ ];
44
+
45
+ /** Empty project: `--stack` wins; a terminal is asked, the Morph stack first; no terminal leaves it to the plan. */
46
+ export async function chooseGreenfieldStack(options = {}, { isTTY = process.stdin.isTTY, prompt = inquirer.prompt } = {}) {
47
+ if (options.stack) {
48
+ const integrations = String(options.with ?? '').split(',').map((t) => t.trim()).filter(Boolean);
49
+ return { stack: options.stack, integrations };
50
+ }
51
+ if (!isTTY) return null;
52
+ const { stackChoice } = await prompt([{
53
+ type: 'list',
54
+ name: 'stackChoice',
55
+ message: 'Projeto novo: qual stack?',
56
+ choices: GREENFIELD_CHOICES,
57
+ }]);
58
+ return stackChoice;
59
+ }
60
+
61
+ async function reinstallForStack(targetPath, stackTags, spinner) {
62
+ if (stackTags.length === 0) return;
63
+ spinner.start('Re-installing stack-filtered assets...');
64
+ await installStackAssets(targetPath, stackTags);
65
+ spinner.succeed(`Stack-filtered assets reinstalled for [${stackTags.join(', ')}]`);
66
+ }
38
67
 
39
68
  /**
40
69
  * A linked worktree junctions `.claude/` and `.morph/framework/` but not `.morph/config`/state:
@@ -196,15 +225,17 @@ export async function initCommand(options) {
196
225
  if (structure?.uiLibrary) logger.dim(`UI Library: ${structure.uiLibrary}`);
197
226
  if (structure?.integrations?.length > 0) logger.dim(`Integrations: ${structure.integrations.join(', ')}`);
198
227
 
199
- const { standardsChoice } = await inquirer.prompt([{
200
- type: 'list',
201
- name: 'standardsChoice',
202
- message: 'How should morph-spec handle your project standards?',
203
- choices: [
204
- { name: 'Keep morph-spec defaults (recommended)', value: 'morph-spec' },
205
- { name: 'Detect project-specific standards', value: 'detect' }
206
- ]
207
- }]);
228
+ const { standardsChoice } = process.stdin.isTTY
229
+ ? await inquirer.prompt([{
230
+ type: 'list',
231
+ name: 'standardsChoice',
232
+ message: 'How should morph-spec handle your project standards?',
233
+ choices: [
234
+ { name: 'Keep morph-spec defaults (recommended)', value: 'morph-spec' },
235
+ { name: 'Detect project-specific standards', value: 'detect' }
236
+ ]
237
+ }])
238
+ : { standardsChoice: 'morph-spec' };
208
239
 
209
240
  if (standardsChoice === 'detect') {
210
241
  spinner.start('Generating project-specific standards...');
@@ -215,30 +246,27 @@ export async function initCommand(options) {
215
246
  logger.dim('Review and edit .morph/context/standards.md as needed.');
216
247
  }
217
248
 
218
- const stackTags = getProjectTags(config);
219
- if (stackTags.length > 0) {
220
- spinner.start('Re-installing stack-filtered assets...');
221
- const frameworkDir = join(targetPath, '.morph', 'framework');
222
- const fwSrcDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'framework');
223
-
224
- const rulesSrc = join(fwSrcDir, 'rules');
225
- const rulesDest = join(targetPath, '.claude', 'rules');
226
- installRulesForStack(rulesSrc, rulesDest, stackTags);
227
- installCommandsForStack(join(fwSrcDir, 'commands'), join(targetPath, '.claude', 'commands'), stackTags);
228
-
229
- await installSkills(targetPath, { projectTags: stackTags });
230
-
231
- await installAgents(targetPath, fwSrcDir, { projectTags: stackTags });
232
- await installDomainAgents(targetPath, fwSrcDir, { projectTags: stackTags });
233
-
234
- spinner.succeed(`Stack-filtered assets reinstalled for [${stackTags.join(', ')}]`);
235
- }
236
-
249
+ await reinstallForStack(targetPath, getProjectTags(config), spinner);
237
250
  spinner.start('Continuing...');
238
251
  }
239
252
  } catch (error) {
240
253
  logger.dim(` Stack detection skipped: ${error.message}`);
241
254
  }
255
+ } else {
256
+ const choice = await chooseGreenfieldStack(options);
257
+ if (choice) {
258
+ spinner.stop();
259
+ const result = setProjectStack(targetPath, choice.stack, { integrations: choice.integrations, log: (line) => logger.dim(line) });
260
+ if (result.error) {
261
+ logger.warn(`Stack não declarada: ${result.error}`);
262
+ } else {
263
+ config = await readJson(configPath);
264
+ await reinstallForStack(targetPath, getProjectTags(config), spinner);
265
+ }
266
+ spinner.start('Continuing...');
267
+ } else {
268
+ logger.dim(' Stack: não declarada — o /morph-plan pergunta antes de planejar (recomendada: a do Morph)');
269
+ }
242
270
  }
243
271
 
244
272
  let mcpSetupResult = null;
@@ -460,13 +488,12 @@ export async function initCommand(options) {
460
488
 
461
489
  const agents = infraResult?.agents;
462
490
  if (agents) {
463
- const total = (agents.tier1 || 0) + (agents.tier2 || 0) + (agents.specialists || 0) + (agents.personas || 0);
464
- logger.dim(` ✓ .claude/agents/ (${total} domain personas installed)`);
491
+ logger.dim(` ✓ .claude/agents/ (${agents.installed ?? 0} Gate 3 agents installed)`);
465
492
  } else {
466
493
  logger.dim(' ✓ .claude/agents/ (native subagents installed)');
467
494
  }
468
495
 
469
- logger.dim(' ✓ ~/.claude/statusline.sh (global statusline installed)');
496
+ logger.dim(' ✓ ~/.claude/statusline.js (global statusline installed)');
470
497
 
471
498
  logger.blank();
472
499
  const globalSettingsPath = join(homedir(), '.claude', 'settings.local.json');
@@ -1,56 +1,17 @@
1
- import chalk from 'chalk';
2
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
3
- import { join } from 'path';
4
- import { getFeatureDir } from '../../core/paths/output-schema.js';
5
1
  import { readMorphConfig } from '../../lib/tasks/test-runner.js';
6
- import { notAuditedReason, scanStack, renderStackScan } from '../../lib/stack/stack-scan.js';
7
- import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
2
+ import { scanStack, renderStackScan } from '../../lib/stack/stack-scan.js';
8
3
 
9
- export const STACK_SCAN_FILE = 'stack-scan.md';
10
-
11
- /** Scan the tree at `cwd`; with a feature, write `2-plan/stack-scan.md`. */
4
+ /** Scan the tree at `cwd`; prints, never writes — the plan reads stdout. */
12
5
  export function runStackScan(featureName, cwd = process.cwd()) {
13
6
  const result = scanStack(cwd, readMorphConfig(cwd));
14
- const markdown = renderStackScan(result, featureName ?? 'projeto');
15
- if (!featureName) return { ...result, markdown, written: null, exit: 0 };
16
-
17
- const featureDir = join(cwd, getFeatureDir(featureName));
18
- if (!existsSync(featureDir)) return { ...result, markdown, written: null, exit: 2, error: 'feature-not-found' };
19
- const planDir = join(featureDir, '2-plan');
20
- mkdirSync(planDir, { recursive: true });
21
- const written = join(planDir, STACK_SCAN_FILE);
22
- writeFileSync(written, markdown, 'utf8');
23
- return { ...result, markdown, written, exit: 0 };
7
+ return { ...result, markdown: renderStackScan(result, featureName ?? 'projeto'), exit: 0 };
24
8
  }
25
9
 
26
10
  export function stackScanCommand(featureName, options = {}) {
27
- if (featureName) {
28
- const owner = assertOwnerTree(featureName, process.cwd());
29
- if (owner.blocked) {
30
- for (const { tone, text } of ownerTreeRefusalLines(featureName, owner.tree, owner.branch, `morph-spec stack-scan ${featureName}`)) {
31
- console.error(chalk[tone](text));
32
- }
33
- process.exit(1);
34
- }
35
- }
36
-
37
11
  const result = runStackScan(featureName);
38
12
  if (options.json) {
39
13
  const { markdown, ...rest } = result;
40
14
  console.log(JSON.stringify(rest, null, 2));
41
- process.exit(result.exit);
42
- }
43
- if (result.error === 'feature-not-found') {
44
- console.error(chalk.red(`stack-scan: a feature "${featureName}" não existe — rode morph-spec create ${featureName} antes`));
45
- process.exit(2);
46
- }
47
- if (result.written) {
48
- const high = result.deviations.filter((d) => d.severity === 'Bloqueante' || d.severity === 'Alto').length;
49
- console.log(chalk.green(`✓ stack-scan: ${join(getFeatureDir(featureName), '2-plan', STACK_SCAN_FILE)} gravado`));
50
- const summary = result.greenfield ? 'greenfield'
51
- : !result.audited ? notAuditedReason(result)
52
- : `${result.deviations.length} desvio(s), ${high} Bloqueante/Alto`;
53
- console.log(chalk.gray(` ${summary}`));
54
15
  } else {
55
16
  process.stdout.write(result.markdown);
56
17
  }
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk';
2
+ import { setProjectStack } from '../../lib/installers/stack-sync.js';
3
+ import { installStackAssets } from '../../lib/installers/stack-assets.js';
4
+ import { getProjectTags } from '../../lib/stack-filter.js';
5
+
6
+ export async function stackSetCommand(stack, options = {}) {
7
+ const integrations = String(options.with ?? '').split(',').map((t) => t.trim()).filter(Boolean);
8
+ const result = setProjectStack(process.cwd(), stack, {
9
+ integrations,
10
+ log: options.json ? () => {} : (line) => console.log(chalk.gray(line)),
11
+ });
12
+ if (result.error) {
13
+ if (options.json) console.log(JSON.stringify({ error: result.error }, null, 2));
14
+ else console.error(chalk.red(`✗ ${result.error}`));
15
+ process.exit(1);
16
+ }
17
+ const { stack: declared, integrations: declaredIntegrations, sync } = result;
18
+ await installStackAssets(process.cwd(), getProjectTags({ project: { stack: declared, integrations: declaredIntegrations } }));
19
+ if (options.json) {
20
+ console.log(JSON.stringify({ stack: declared, integrations: declaredIntegrations, rulesAdded: sync.added, rulesUpdated: sync.updated }, null, 2));
21
+ } else {
22
+ console.log(chalk.green(`✓ stack ${declared}`) + chalk.gray(` — rules, commands e skills da stack instalados (${sync.added.length + sync.updated.length} rule(s) de domínio nova(s))`));
23
+ console.log(chalk.yellow(' As rules carregam no início de uma sessão do Claude Code: implemente numa sessão nova.'));
24
+ }
25
+ process.exit(0);
26
+ }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { loadState, getFeature, derivePhase, deriveOutputs } from '../../core/state/state-manager.js';
3
- import { sliceProgress } from '../../../framework/hooks/shared/slice-progress.js';
3
+ import { implementProgress } from '../../../framework/hooks/shared/report-progress.js';
4
4
  import { formatNext } from '../../../framework/hooks/shared/next-step.js';
5
5
  import { nextStepFor } from '../../lib/next-step.js';
6
6
 
@@ -20,8 +20,8 @@ const PHASE_ORDER = [
20
20
 
21
21
  const PHASE_OUTPUTS = {
22
22
  plan: ['plan', 'checks'],
23
- uiux: ['uiDesignSystem', 'uiMockups', 'uiComponents', 'uiFlows'],
24
- implement: [],
23
+ uiux: ['uiMockups'],
24
+ implement: ['implementReport'],
25
25
  review: ['verifierReport', 'reviewerReport']
26
26
  };
27
27
 
@@ -118,7 +118,7 @@ export async function statusCommand(featureName, options = {}) {
118
118
  if (!feature.outputs) {
119
119
  feature.outputs = deriveOutputs(featureName, process.cwd());
120
120
  }
121
- const slices = sliceProgress(featurePath);
121
+ const scenarios = implementProgress(featurePath);
122
122
 
123
123
  if (options.json) {
124
124
  const status = {
@@ -137,7 +137,7 @@ export async function statusCommand(featureName, options = {}) {
137
137
  created: output.created,
138
138
  path: output.path
139
139
  })),
140
- slices,
140
+ scenarios,
141
141
  approvals: feature.approvalGates || {},
142
142
  next: nextStepFor(featureName, process.cwd())
143
143
  };
@@ -177,14 +177,13 @@ export async function statusCommand(featureName, options = {}) {
177
177
  }
178
178
  console.log('');
179
179
 
180
- if (slices && slices.total > 0) {
181
- console.log(chalk.white.bold('Fatias:'));
182
- const percentage = Math.round((slices.done / slices.total) * 100);
183
- console.log(` Total: ${slices.total}`);
184
- console.log(chalk.green(` ✓ Feitas: ${slices.done} (${percentage}%)`));
185
- if (slices.partial > 0) console.log(chalk.cyan(` ▶ Parciais: ${slices.partial}`));
186
- if (slices.blocked > 0) console.log(chalk.yellow(` ⏸ Bloqueadas (esperam decisão): ${slices.blocked}`));
187
- if (slices.pending > 0) console.log(chalk.gray(` ⬜ Pendentes: ${slices.pending}`));
180
+ if (scenarios && scenarios.total > 0) {
181
+ console.log(chalk.white.bold('Cenários (3-implement/report.md):'));
182
+ const percentage = Math.round((scenarios.done / scenarios.total) * 100);
183
+ console.log(` Total: ${scenarios.total}`);
184
+ console.log(chalk.green(` ✓ Feitos: ${scenarios.done} (${percentage}%)`));
185
+ if (scenarios.blocked > 0) console.log(chalk.yellow(` ⏸ Bloqueados (esperam decisão): ${scenarios.blocked}`));
186
+ if (scenarios.pending > 0) console.log(chalk.gray(` ⬜ Pendentes: ${scenarios.pending}`));
188
187
  console.log('');
189
188
  }
190
189
 
@@ -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...';
@@ -320,15 +320,6 @@ export async function runWorktreeSetup(feature, options = {}) {
320
320
 
321
321
  const graph = seedWorktreeGraph(worktreePath, cwd);
322
322
 
323
- // Fail-open; a no-op under `create --worktree`, which regenerates it after its copy.
324
- try {
325
- if (existsSync(join(worktreePath, '.morph', 'features', feature, 'feature.json'))) {
326
- const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
327
- // `worktree` is root-owned and absent from this branch copy: pass it so the doc says "Local: worktree".
328
- writeHandoff(feature, worktreePath, { worktree: { path: worktreePath, branch } });
329
- }
330
- } catch {}
331
-
332
323
  try {
333
324
  const { loadState, saveState } = await import('../../core/state/state-manager.js');
334
325
  const state = loadState(false);
@@ -834,9 +825,9 @@ export function pickRemovalFailure(stderrs) {
834
825
 
835
826
  /**
836
827
  * PROVE-or-DECLINE. `refused` (proved unsafe) restores the junctions; a git FAILURE leaves them down
837
- * (`infraLeftUnlinked`) unless `relinkOnFailure` (the reaper, whose agent may still be live).
828
+ * (`infraLeftUnlinked`).
838
829
  */
839
- 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 }) {
840
831
  // ORDER IS LOAD-BEARING: unlink the junctions BEFORE git touches the dir. `git worktree remove`
841
832
  // follows them and deletes the root's installation; a declined path is repaired by the rollback below.
842
833
  const unlinked = unlinkWorktreeInfraLinks(worktreePath, root);
@@ -881,8 +872,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
881
872
  // would make the next retry (or a raw `--force`) follow the junctions out.
882
873
  const refused = !canRemove;
883
874
  let relinked = false;
884
- const relinkAfterFailure = relinkOnFailure && !refused && !worktreeRemoved && !removalFailure?.alreadyDeregistered;
885
- if ((refused || relinkAfterFailure) && unlinked.length && existsSync(worktreePath)) {
875
+ if (refused && unlinked.length && existsSync(worktreePath)) {
886
876
  try {
887
877
  setupInfraLinks(worktreePath, root);
888
878
  relinked = true;
@@ -1055,7 +1045,6 @@ export async function runWorktreeRemove(featureArg, options = {}) {
1055
1045
  options: { discardIgnored: options.discardIgnored === true },
1056
1046
  // The branch is the work: deleting it would discard unintegrated commits. Opt-in only.
1057
1047
  deleteBranch: options.deleteBranch === true,
1058
- relinkOnFailure: options.relinkOnFailure === true,
1059
1048
  // Re-run advice must address the worktree the way it was addressable here:
1060
1049
  // a `--target` worktree has no feature name that would resolve.
1061
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}.`));
@@ -30,11 +30,11 @@ export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedB
30
30
  }
31
31
 
32
32
  if (gate === 'plan') {
33
- const block = planAutoApprovalBlock(cwd, featureName);
33
+ const block = planAutoApprovalBlock(cwd, featureName, feature);
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
  }
@@ -132,7 +132,7 @@ export async function createCommand(featureName, options = {}) {
132
132
  const alreadyExists = existsSync(join(process.cwd(), '.morph', 'features', featureName, 'feature.json'));
133
133
  if (!alreadyExists && (description === null || String(description).trim() === '')) {
134
134
  console.error(chalk.red(`\n❌ A feature needs a description at birth: "${featureName}"`));
135
- console.error(chalk.gray(' Pass --description "<what/why>" (or --request "<pedido>", which seeds it).'));
135
+ console.error(chalk.gray(' Pass --description "<what/why>" (or --request '<pedido>', which seeds it).'));
136
136
  console.error(chalk.gray(' This keeps features identifiable — no more anonymous scaffolds.\n'));
137
137
  process.exit(1);
138
138
  }
@@ -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 {