@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
@@ -0,0 +1,74 @@
1
+ import chalk from 'chalk';
2
+ import { execFileSync } from 'child_process';
3
+ import { loadGraph, getFreshness, staleCause } from '../../lib/graph/graph-loader.js';
4
+ import { parseDiff, buildDiffTree, renderDiffTree, declarationDrift } from '../../lib/graph/diff-tree.js';
5
+ import { detectDefaultBranch } from '../../lib/git/default-branch.js';
6
+
7
+ const git = (cwd, ...args) => execFileSync('git', args, { cwd, stdio: 'pipe' }).toString();
8
+
9
+ function fail(options, payload, lines) {
10
+ if (options.json) console.log(JSON.stringify({ ok: false, ...payload }, null, 2));
11
+ else for (const [tone, text] of lines) console.error(chalk[tone](text));
12
+ process.exit(1);
13
+ }
14
+
15
+ // Only the committed range: the graph is a snapshot of HEAD, so a diff of the working tree would put
16
+ // lines the graph never saw against symbols at the wrong place.
17
+ export async function graphDiffCommand(options = {}) {
18
+ const projectPath = options.path || process.cwd();
19
+ const loaded = loadGraph(projectPath);
20
+ if (!loaded.ok) {
21
+ fail(options, { reason: 'no-graph', detail: loaded.reason }, [
22
+ ['yellow', `\n○ Sem grafo (${loaded.reason}): leia o diff cru (git diff).\n`],
23
+ ]);
24
+ }
25
+
26
+ const freshness = getFreshness(projectPath, loaded.graph);
27
+ if (freshness.state !== 'fresh') {
28
+ fail(options, { reason: 'stale-graph', freshness }, [
29
+ ['yellow', `\n○ O grafo não é deste commit (${staleCause(freshness)}): a árvore sairia com símbolos e linhas errados.`],
30
+ ['gray', ' Rode `morph-spec graph refresh` e repita; ou leia o diff cru (git diff).\n'],
31
+ ]);
32
+ }
33
+
34
+ let base = options.base;
35
+ try {
36
+ base ??= git(projectPath, 'merge-base', detectDefaultBranch(projectPath) || 'main', 'HEAD').trim();
37
+ } catch {
38
+ fail(options, { reason: 'no-base' }, [['yellow', '\n○ Sem base para o diff: passe --base <ref>.\n']]);
39
+ }
40
+ const changes = parseDiff(git(projectPath, 'diff', '-U0', '--no-renames', '--no-color', base, 'HEAD'));
41
+ let uncommitted = [];
42
+ try {
43
+ const dirty = new Set(git(projectPath, 'diff', '--name-only', 'HEAD').split('\n').filter(Boolean));
44
+ uncommitted = [...changes.keys()].filter((f) => dirty.has(f));
45
+ } catch { uncommitted = []; }
46
+
47
+ const tree = buildDiffTree(loaded.graph, changes);
48
+ const drift = declarationDrift(tree, (file) => {
49
+ try { return git(projectPath, 'show', `HEAD:${file}`).split(/\r?\n/); } catch { return []; }
50
+ });
51
+ if (drift.length) {
52
+ fail(options, { reason: 'graph-drift', drift }, [
53
+ ['yellow', `\n○ O grafo diz ser deste commit, mas ${drift.length} símbolo(s) não estão na linha que ele aponta (ex.: ${drift[0].label} em ${drift[0].file}:${drift[0].line}).`],
54
+ ['gray', ' A extração leu arquivos diferentes do commit. Rode `morph-spec graph build` e repita; ou leia o diff cru (git diff).\n'],
55
+ ]);
56
+ }
57
+ const payload = { ok: true, base, head: freshness.headCommit, ...tree, uncommitted };
58
+ if (options.json) {
59
+ console.log(JSON.stringify(payload, null, 2));
60
+ return;
61
+ }
62
+
63
+ const { added, modified, entries, files } = tree.counts;
64
+ console.log(chalk.green(`\n✅ ${added} novo(s) +, ${modified} alterado(s) ~, ${entries} entrada(s), em ${files} arquivo(s) — ${String(base).slice(0, 8)}..${String(freshness.headCommit).slice(0, 8)}`));
65
+ for (const line of renderDiffTree(tree)) console.log(` ${line}`);
66
+ if (tree.unmapped.length) {
67
+ console.log(chalk.gray(`\n Sem símbolo tocado (só import/topo do arquivo, config, doc, ou fora do .graphifyignore) — leia no diff:`));
68
+ for (const f of tree.unmapped) console.log(chalk.gray(` ${f}`));
69
+ }
70
+ if (uncommitted.length) {
71
+ console.log(chalk.yellow(`\n ⚠ Mudança não commitada em ${uncommitted.length} arquivo(s) do diff: ela não está nesta árvore. Commit e repita.`));
72
+ }
73
+ console.log('');
74
+ }
@@ -37,7 +37,7 @@ export async function graphStatusCommand(options = {}) {
37
37
  console.log(chalk.yellow(`\n○ Sem grafo (${loaded.reason})`));
38
38
  console.log(chalk.gray(` ${loaded.path}`));
39
39
  console.log(chalk.gray(' Gere com: morph-spec graph build'));
40
- console.log(chalk.gray(' Sem grafo, scout e skills seguem por grep normalmente.\n'));
40
+ console.log(chalk.gray(' Sem grafo, as skills seguem por grep normalmente.\n'));
41
41
  return;
42
42
  }
43
43
 
@@ -111,11 +111,9 @@ function printMetricsBody(m) {
111
111
  const tov = m.testOutsideVerify;
112
112
  const timed = (t) => `${String(t.runs).padStart(4)}× ${fmtDuration(t.measuredMs / 1000).padStart(8)}`
113
113
  + (t.unmeasured ? chalk.gray(` (${t.unmeasured} sem duração)`) : '');
114
- console.log('\n' + chalk.white.bold('Teste fora do verify (sub-agentes)')
114
+ console.log('\n' + chalk.white.bold('Teste fora do verify')
115
115
  + chalk.gray(` · de ${tov.total.runs} testes+verifies, ${fmtDuration(tov.total.measuredMs / 1000)} cronometrados`
116
116
  + (tov.total.unmeasured ? `, ${tov.total.unmeasured} sem duração` : '')));
117
- console.log(` sub-agente rodando verify cheio ${timed(tov.subagentVerify)}`);
118
- console.log(` implementador, 3ª rodada em diante ${timed(tov.implementerRound3Plus)}`);
119
117
  console.log(` agente de review rodando teste ${timed(tov.reviewTests)}`);
120
118
  console.log(chalk.gray(` ${tov.reviewTests.byPersona.runs}× pela persona morph-* (o que o hook test-budget reconhece);`
121
119
  + ' o resto veio de um nome de dispatch (ver-, rev-, gate3-…)'));
@@ -163,7 +161,7 @@ function printMetricsBody(m) {
163
161
  return;
164
162
  }
165
163
  for (const [gate, g] of gates) {
166
- console.log(` gate ${gate}: ${g.decisions} decisões, ${g.paused} pausas → ${g.approvedAfterPause} aprovadas como estavam, `
164
+ console.log(` gate ${gate}: ${g.decisions} decisões, ${g.paused} pausas (${g.pausedForHuman} para o humano) → ${g.approvedAfterPause} aprovadas como estavam, `
167
165
  + `${g.reworkedAfterPause} retrabalhadas${g.unapproved ? `, ${g.unapproved} desaprovações` : ''}`);
168
166
  for (const [signal, s] of Object.entries(g.bySignal)) {
169
167
  console.log(chalk.gray(` ${signal}: ${s.paused} pausas, ${s.approvedAfterPause} aprovadas como estavam, ${s.reworkedAfterPause} retrabalhadas`));
@@ -35,7 +35,7 @@ export function telemetryCommand(options = {}) {
35
35
  }
36
36
 
37
37
  section('Skills', summary.skills, s => `${chalk.green(s.name.padEnd(24))} ${s.count}`);
38
- section('Personas despachadas', summary.personas, p => `${chalk.magenta(p.name.padEnd(24))} ${p.count}`);
38
+ section('Sub-agents despachados', summary.personas, p => `${chalk.magenta(p.name.padEnd(24))} ${p.count}`);
39
39
  section('Base de conhecimento (standards lidos explicitamente)', summary.standards,
40
40
  s => `${chalk.yellow(String(s.count).padStart(3))}× ${s.path}`);
41
41
  if (summary.standards.length || summary.tools.some(t => t.name === 'Read')) {
@@ -16,8 +16,9 @@ import {
16
16
  getInstalledCLIVersion
17
17
  } from '../../utils/version-checker.js';
18
18
  import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
19
- import { checkMissingAgents, checkAgentsDrift, maxTurnsError, surfacesError, effortError } from '../../utils/agents-installer.js';
19
+ import { checkMissingAgents, checkAgentsDrift, maxTurnsError } from '../../utils/agents-installer.js';
20
20
  import { getProjectTags } from '../../lib/stack-filter.js';
21
+ import { stackRuleGaps } from '../../lib/installers/stack-sync.js';
21
22
  import { auditGraph } from '../../lib/graph/graph-health.js';
22
23
  import { auditFeatureLifecycle } from '../../core/state/lifecycle-audit.js';
23
24
  import { validateTestNodes, morphConfigPath, discoverFrontendPackages } from '../../lib/tasks/test-nodes.js';
@@ -114,27 +115,27 @@ export function validateAgentsJson(agentsData) {
114
115
  }
115
116
 
116
117
  const version = agentsData.version || 'unknown';
117
- const isV5 = version.startsWith('5.');
118
+ const isCurrent = version.startsWith('6.');
118
119
  const v4Fields = [];
119
- const missingV5Fields = [];
120
+ const missingFields = [];
120
121
 
121
122
  for (const [id, data] of agentEntries) {
122
123
  if (data.tier !== undefined) v4Fields.push(`${id}.tier`);
123
124
  if (data.active_phases !== undefined) v4Fields.push(`${id}.active_phases`);
124
125
  if (data.reports_to !== undefined) v4Fields.push(`${id}.reports_to`);
125
126
  if (data.teammate !== undefined) v4Fields.push(`${id}.teammate`);
126
- if (!data.title) missingV5Fields.push(`${id}.title`);
127
- if (!Array.isArray(data.domains)) missingV5Fields.push(`${id}.domains`);
128
- if (typeof data.persona !== 'string' || !data.persona) missingV5Fields.push(`${id}.persona`);
127
+ if (!data.title) missingFields.push(`${id}.title`);
128
+ if (!Array.isArray(data.domains)) missingFields.push(`${id}.domains`);
129
+ if (typeof data.persona !== 'string' || !data.persona) missingFields.push(`${id}.persona`);
129
130
  }
130
131
 
131
- if (isV5) {
132
+ if (isCurrent) {
132
133
  checks.push({ name: `agents.json schema (v${version})`, status: 'ok' });
133
134
  } else {
134
135
  checks.push({
135
136
  name: `agents.json schema (v${version})`,
136
137
  status: 'warn',
137
- msg: 'expected v5.x — run agents migration'
138
+ msg: "expected v6.x — run 'morph-spec update'"
138
139
  });
139
140
  }
140
141
 
@@ -155,26 +156,12 @@ export function validateAgentsJson(agentsData) {
155
156
  ? { name: 'agents.json maxTurns', status: 'error', msg: maxTurnsErrors.join(' | ') }
156
157
  : { name: 'agents.json maxTurns', status: 'ok' });
157
158
 
158
- const surfacesErrors = agentEntries
159
- .map(([id, data]) => surfacesError(id, data?.surfaces, data?.standards))
160
- .filter(Boolean);
161
- checks.push(surfacesErrors.length > 0
162
- ? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
163
- : { name: 'agents.json surfaces', status: 'ok' });
164
-
165
- const effortErrors = agentEntries
166
- .map(([id, data]) => effortError(id, data?.effort))
167
- .filter(Boolean);
168
- checks.push(effortErrors.length > 0
169
- ? { name: 'agents.json effort', status: 'error', msg: effortErrors.join(' | ') }
170
- : { name: 'agents.json effort', status: 'ok' });
171
-
172
- if (missingV5Fields.length > 0 && isV5) {
173
- const sample = missingV5Fields.slice(0, 3).join(', ');
159
+ if (missingFields.length > 0 && isCurrent) {
160
+ const sample = missingFields.slice(0, 3).join(', ');
174
161
  checks.push({
175
- name: 'agents.json v5 required fields',
162
+ name: 'agents.json required fields',
176
163
  status: 'warn',
177
- msg: `${missingV5Fields.length} missing: ${sample}${missingV5Fields.length > 3 ? '...' : ''}`
164
+ msg: `${missingFields.length} missing: ${sample}${missingFields.length > 3 ? '...' : ''}`
178
165
  });
179
166
  }
180
167
 
@@ -194,7 +181,6 @@ const REQUIRED_AGENTS = [
194
181
  'verifier',
195
182
  'reviewer',
196
183
  'security',
197
- 'scout'
198
184
  ];
199
185
 
200
186
  async function doctorFullCommand(frameworkRoot) {
@@ -589,15 +575,6 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
589
575
  }
590
576
  }
591
577
 
592
- const { reapAgentWorktrees } = await import('../../lib/worktree/agent-reaper.js');
593
- const reap = await reapAgentWorktrees(targetPath);
594
- if (reap.ok) {
595
- const gone = reap.reaped.map((r) => r.path ?? r.branch);
596
- prune.agentReap = { reaped: gone, branchesDeleted: reap.branchesDeleted, preserved: reap.preserved, ...(reap.busy ? { busy: true } : {}) };
597
- if (!options.json && gone.length) logger.success(`sobras de sub-agent removidas: ${gone.join(', ')}`);
598
- if (!options.json && reap.busy) logger.warn('reaper de sub-agent já em execução em outro processo; nada foi feito nesta rodada');
599
- }
600
-
601
578
  const { resolvePrimaryRoot } = await import('./worktree.js');
602
579
  const { applyEditorSettings } = await import('../../lib/worktree/vscode-settings.js');
603
580
  const editor = applyEditorSettings(resolvePrimaryRoot(targetPath));
@@ -660,9 +637,6 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
660
637
  if (problems.some(prunable) && !options.prune) {
661
638
  logger.dim('\n Rode `morph-spec doctor --worktrees --prune` para limpar os registros órfãos e os diretórios vazios.');
662
639
  }
663
- if (issues.some((i) => i.type === 'agent-worktree-leftover' && i.reapable) && !options.prune) {
664
- logger.dim('\n Rode `morph-spec worktree reap` (ou `doctor --worktrees --prune`) para remover as sobras de sub-agent que já pararam.');
665
- }
666
640
  if (!problems.length) {
667
641
  logger.success('Nenhuma inconsistência — os itens acima são informativos.');
668
642
  return true;
@@ -1058,6 +1032,16 @@ export async function doctorCommand(options = {}) {
1058
1032
  hasWarnings = true;
1059
1033
  }
1060
1034
 
1035
+ const gaps = stackRuleGaps(targetPath);
1036
+ if (gaps?.stackMissing) {
1037
+ checks.push({ name: 'project.stack', status: 'warn', msg: 'ausente/unknown no config.json — nenhuma regra de stack instalada; morph-spec stack-set <stack> (a do Morph: standard architecture-stack-baseline), antes da sessão que implementa' });
1038
+ hasWarnings = true;
1039
+ }
1040
+ if (gaps?.missing.length) {
1041
+ checks.push({ name: 'stack rules', status: 'warn', msg: `faltando para [${gaps.tags.join(', ')}]: ${gaps.missing.join(', ')} — rode morph-spec verify ou update` });
1042
+ hasWarnings = true;
1043
+ }
1044
+
1061
1045
  const agentsPathClaude = join(claudePath, 'agents');
1062
1046
  if (await pathExists(agentsPathClaude)) {
1063
1047
  const agentFiles = await fs.readdir(agentsPathClaude);
@@ -32,17 +32,16 @@ export function fleetCommand(options = {}) {
32
32
 
33
33
  console.log(chalk.white.bold(
34
34
  // PHASE at 18 fits `aguardando-merge`; narrower misaligns every column after it.
35
- col('FEATURE', 20) + col('TYPE', 9) + col('PHASE', 18) + col('FATIAS', 8)
35
+ col('FEATURE', 20) + col('TYPE', 9) + col('PHASE', 18) + col('CENÁRIOS', 10)
36
36
  + col('GATES PEND.', 20) + col('WHERE', 24) + col('REMOTO', 16) + col('CLAIM', 16)
37
- + col('PORTS', 12) + col('HANDOFF', 9) + 'ATIVIDADE',
37
+ + col('PORTS', 12) + 'ATIVIDADE',
38
38
  ));
39
39
  for (const e of fleet.entries) {
40
- const slices = e.slices ? `${e.slices.done}/${e.slices.total}` : '-';
40
+ const scenarios = e.scenarios ? `${e.scenarios.done}/${e.scenarios.total}` : '-';
41
41
  const gates = e.pendingGates.length ? e.pendingGates.join(',') : '-';
42
42
  // pr-pending exists only on its branch, never in a worktree or the root.
43
43
  const where = (e.location === 'worktree' || e.location === 'pr-pending') ? e.branch : 'raiz';
44
44
  const ports = e.env ? `${e.env.range[0]}-${e.env.range[1]}` : '-';
45
- const handoff = e.handoff?.exists ? 'sim' : '-';
46
45
  const activity = [
47
46
  e.lastCommit ? `commit ${e.lastCommit}` : null,
48
47
  Number.isInteger(e.dirty) && e.dirty > 0 ? chalk.yellow(`${e.dirty} dirty`) : null,
@@ -51,10 +50,10 @@ export function fleetCommand(options = {}) {
51
50
  // length and append the padding by hand.
52
51
  const claimPlain = e.claim ? `${e.claim.live ? 'sessão' : 'morta'} (${claimAge(e.claim)})` : '-';
53
52
  console.log(
54
- col(e.feature, 20) + col(e.workType || '-', 9) + col(e.phase, 18) + col(slices, 8)
53
+ col(e.feature, 20) + col(e.workType || '-', 9) + col(e.phase, 18) + col(scenarios, 10)
55
54
  + col(gates, 20) + col(where, 24) + col(remoteCell(e.remote), 16)
56
55
  + claimCell(e.claim) + ' '.repeat(Math.max(1, 16 - claimPlain.length))
57
- + col(ports, 12) + col(handoff, 9) + activity,
56
+ + col(ports, 12) + activity,
58
57
  );
59
58
  }
60
59
  const claimed = fleet.entries.filter((e) => e.claim?.live);
@@ -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