@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,119 +1,31 @@
1
- # UI Mockups & Wireframes: {{titleCase FEATURE_NAME}}
1
+ # Mockups: {{titleCase FEATURE_NAME}}
2
2
 
3
- | Field | Value |
4
- |-------|-------|
5
- | **Feature** | {{FEATURE_NAME}} |
6
- | **Created** | {{DATE}} |
3
+ <!-- Uma `## Tela:` por tela nova ou alterada. As opções navegáveis ficam em 1-design/opcoes/{tela}.html;
4
+ a escolhida vira 1-design/prototipos/{tela}.html, com os prints {tela}-1280.png e {tela}-390.png ao lado,
5
+ e um seletor de estado (data-estado="vazio|carregando|erro", ?estado=) com {tela}-{estado}-390.png de cada
6
+ estado que o checklist diz que se aplica. Faltando um, o Gate uiux pausa. -->
7
7
 
8
- ---
9
-
10
- ## Screen Inventory
11
-
12
- | Screen ID | Name | Route | Priority |
13
- |-----------|------|-------|----------|
14
- | **S001** | {Screen Name} | /path | High |
15
- | **S002** | {Screen Name} | /path | Medium |
16
-
17
- ---
18
-
19
- ## Tela: {Primary Screen}
8
+ ## Tela: {Nome da tela}
20
9
 
21
10
  **Protótipo:** 1-design/prototipos/{tela}.html
22
-
23
- O protótipo renderizado é a fonte visual da tela; os prints `{tela}-1280.png` e `{tela}-390.png` ficam ao lado dele, e sem os três arquivos o Gate uiux pausa. O layout em texto abaixo é opcional.
24
-
25
- ### Desktop Layout
26
-
27
- ```
28
- ┌─────────────────────────────────────┐
29
- │ Header [User ▼] │
30
- ├─────────────────────────────────────┤
31
- │ │
32
- │ [Title] │
33
- │ │
34
- │ ┌──────────────────────────────┐ │
35
- │ │ Main Content Area │ │
36
- │ │ │ │
37
- │ │ [Card 1] [Card 2] │ │
38
- │ │ │ │
39
- │ │ ┌────────────────────────┐ │ │
40
- │ │ │ Data Table │ │ │
41
- │ │ │ ... │ │ │
42
- │ │ └────────────────────────┘ │ │
43
- │ └──────────────────────────────┘ │
44
- │ │
45
- └─────────────────────────────────────┘
46
- ```
47
-
48
- ### Mobile Layout
49
-
50
- ```
51
- ┌──────────────────────┐
52
- │ ☰ Title [User ▼]│
53
- ├──────────────────────┤
54
- │ │
55
- │ [Card 1] │
56
- │ │
57
- │ [Card 2] │
58
- │ │
59
- │ [View All Items] │
60
- │ │
61
- └──────────────────────┘
62
- ```
11
+ **Direção escolhida:** <qual opção de opcoes/{tela}.html e por quê, numa frase>
63
12
 
64
13
  ### Checklist de tela
65
14
 
66
- Troque cada `…` pela decisão — "não se aplica" vale se escrito; `…`, vazio ou "a definir" reprovam o Gate uiux.
15
+ Troque cada `…` pela decisão. "Não se aplica" vale se escrito; `…`, vazio ou "a definir" reprovam o Gate uiux.
67
16
 
68
17
  | Item | Decisão |
69
18
  |---|---|
70
- | Superfície | … |
71
- | Volume esperado | … |
72
- | Paginação | … |
73
- | Ordenação | … |
74
- | Filtro | … |
75
- | Estado vazio | … |
76
- | Estado carregando | … |
77
- | Estado de erro | … |
78
- | Mobile | … |
79
- | Abas | … |
80
-
81
- ---
82
-
83
- ## States
84
-
85
- ### Loading
86
- - Show skeleton placeholders
87
- - Spinner for primary content
88
-
89
- ### Empty
90
- - Icon + message
91
- - Clear call-to-action button
92
-
93
- ### Error
94
- - Warning icon
95
- - User-friendly message
96
- - Retry button
97
-
98
- ### Success (After Action)
99
- - Toast notification
100
- - Auto-dismiss after 3s
101
-
102
- ---
103
-
104
- ## Interactions
105
-
106
- - **Hover**: Subtle elevation increase
107
- - **Click**: Scale down (0.98)
108
- - **Transitions**: Fade-in 200ms
19
+ | Lista no servidor | … |
20
+ | Estados vazio e carregando | … |
21
+ | Erro com próxima ação | … |
22
+ | Uma ação primária | … |
23
+ | Celular | … |
109
24
 
110
- ---
25
+ ### Componentes
111
26
 
112
- ## Accessibility
113
- - [ ] 44x44px minimum touch targets
114
- - [ ] Focus indicators visible
115
- - [ ] Proper heading hierarchy (H1 → H2 → H3)
27
+ - <componente do design system reusado, ou novo e por quê>
116
28
 
117
- ---
29
+ ### Fluxo
118
30
 
119
- *Generated with MORPH Framework*
31
+ 1. <passo do usuário até o resultado>
@@ -56,7 +56,7 @@ services:
56
56
 
57
57
  # PostgreSQL Database (Local development)
58
58
  postgres:
59
- image: postgres:15-alpine
59
+ image: pgvector/pgvector:pg17
60
60
  restart: unless-stopped
61
61
  ports:
62
62
  - "${POSTGRES_PORT:-5432}:5432"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "9.8.0",
3
+ "version": "10.1.1",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -16,7 +16,9 @@ import {
16
16
  deleteStamp,
17
17
  getStackStatus,
18
18
  isDockerAvailable,
19
+ isHostOnlyStack,
19
20
  lintComposeIsolation,
21
+ resetE2eData,
20
22
  readStamp,
21
23
  resolveComposeFile,
22
24
  resolveSpecCommand,
@@ -34,8 +36,9 @@ import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js
34
36
  const EXIT_USAGE = 2;
35
37
  const EXIT_BUSY = 3;
36
38
 
37
- // No compose file is a usage error here, unlike in the verify node where it is a graded skip.
38
- function resolveContext(featureName, { json = false } = {}) {
39
+ // No compose file and no e2e.hostServices is a usage error here, unlike in the verify node where
40
+ // it is a graded skip. `composeFile` is null for a host-only stack.
41
+ function resolveContext(featureName, { json = false, onMissing } = {}) {
39
42
  const root = resolvePrimaryRoot(process.cwd());
40
43
  const found = findWorktreeForFeature(featureName, root);
41
44
  const cwd = found?.path ?? root;
@@ -43,11 +46,13 @@ function resolveContext(featureName, { json = false } = {}) {
43
46
  const config = readConfig(cwd) ?? readConfig(root);
44
47
  const composeFile = resolveComposeFile(cwd, config);
45
48
 
46
- if (!composeFile) {
49
+ if (!composeFile && !isHostOnlyStack(config, composeFile)) {
47
50
  const message =
48
- `No compose file found in ${cwd} (compose.yaml, docker-compose.yml, …). ` +
49
- 'Set project.e2e.composeFile in .morph/config/config.json if it lives elsewhere.';
50
- if (json) console.log(JSON.stringify({ error: message, tree: cwd }, null, 2));
51
+ `No compose file found in ${cwd} (compose.yaml, docker-compose.yml, …) and no e2e.hostServices. ` +
52
+ 'Set e2e.composeFile in .morph/config/config.json if it lives elsewhere, ' +
53
+ 'or declare the app in e2e.hostServices when it has no dependency to containerize.';
54
+ if (onMissing) onMissing(message, cwd);
55
+ else if (json) console.log(JSON.stringify({ error: message, tree: cwd }, null, 2));
51
56
  else console.error(chalk.red(`\n❌ ${message}\n`));
52
57
  return null;
53
58
  }
@@ -86,7 +91,7 @@ export async function e2eUpCommand(featureName, options = {}) {
86
91
 
87
92
  // Isolation BEFORE the docker check: a compose that hardcodes host ports or container names
88
93
  // collides with a sibling worktree whatever we inject.
89
- const lint = lintComposeIsolation(readComposeText(ctx.cwd, ctx.composeFile));
94
+ const lint = ctx.composeFile ? lintComposeIsolation(readComposeText(ctx.cwd, ctx.composeFile)) : { findings: [] };
90
95
  const isolation = decideIsolation({ findings: lint.findings, insideWorktree: ctx.insideWorktree });
91
96
  if (isolation.status === 'fail') {
92
97
  if (options.json) {
@@ -106,7 +111,7 @@ export async function e2eUpCommand(featureName, options = {}) {
106
111
  }
107
112
 
108
113
  const exec = options._exec ?? defaultExec;
109
- if (!isDockerAvailable(ctx.cwd, exec)) {
114
+ if (ctx.composeFile && !isDockerAvailable(ctx.cwd, exec)) {
110
115
  console.error(chalk.red('\n❌ Docker is not available in this environment.\n'));
111
116
  process.exit(EXIT_USAGE);
112
117
  }
@@ -149,7 +154,7 @@ export async function e2eUpCommand(featureName, options = {}) {
149
154
  tree: ctx.tree,
150
155
  composeFile: ctx.composeFile,
151
156
  status: failed ? 'fail' : 'pass',
152
- reused: result.stack.reused,
157
+ reused: ctx.composeFile ? result.stack.reused : hostServices.reused,
153
158
  reason: result.reason,
154
159
  auth: { status: auth.status, reason: auth.reason },
155
160
  ...(hostServices.status === 'skip' ? {} : { hostServices: hostServices.services }),
@@ -164,7 +169,9 @@ export async function e2eUpCommand(featureName, options = {}) {
164
169
  if (result.output) console.error(result.output);
165
170
  for (const w of result.warnings) console.error(chalk.yellow(`⚠️ ${w}`));
166
171
  } else {
167
- const how = result.stack.reused ? chalk.gray(`(${result.stack.reason})`) : chalk.gray('(built)');
172
+ const how = !ctx.composeFile
173
+ ? chalk.gray(`(${result.stack.reason})`)
174
+ : result.stack.reused ? chalk.gray(`(${result.stack.reason})`) : chalk.gray('(built)');
168
175
  console.log(chalk.green(`\n✓ Stack up — ${ctx.projectName} ${how}`));
169
176
  console.log(chalk.gray(` tree: ${ctx.tree}`));
170
177
  for (const s of hostServices.services) {
@@ -190,18 +197,24 @@ export async function e2eStatusCommand(featureName, options = {}) {
190
197
  if (!ctx) process.exit(EXIT_USAGE);
191
198
 
192
199
  const exec = options._exec ?? defaultExec;
193
- const status = getStackStatus(ctx.cwd, {
194
- composeFile: ctx.composeFile,
195
- projectName: ctx.projectName,
196
- env: ctx.env,
197
- }, exec);
200
+ const hostServices = ctx.gitCommonDir ? hostServicesStatus(ctx.gitCommonDir, ctx.projectName) : [];
201
+ const status = ctx.composeFile
202
+ ? getStackStatus(ctx.cwd, {
203
+ composeFile: ctx.composeFile,
204
+ projectName: ctx.projectName,
205
+ env: ctx.env,
206
+ }, exec)
207
+ : {
208
+ up: hostServices.length > 0 && hostServices.every((h) => h.alive),
209
+ healthy: hostServices.length > 0 && hostServices.every((h) => h.alive && h.health !== false),
210
+ services: [],
211
+ };
198
212
 
199
213
  const stamp = computeSourceStamp(ctx.cwd, exec);
200
214
  const previous = ctx.gitCommonDir ? readStamp(ctx.gitCommonDir, ctx.projectName) : null;
201
215
  const fresh = Boolean(stamp && previous?.stamp === stamp);
202
216
 
203
217
  const spec = resolveSpecCommand(ctx.cwd, ctx.config, ctx.config?.project?.frontendPath);
204
- const hostServices = ctx.gitCommonDir ? hostServicesStatus(ctx.gitCommonDir, ctx.projectName) : [];
205
218
 
206
219
  const payload = {
207
220
  feature: featureName,
@@ -228,7 +241,7 @@ export async function e2eStatusCommand(featureName, options = {}) {
228
241
  console.log('');
229
242
  console.log(chalk.bold(`E2E stack — ${ctx.projectName}`));
230
243
  console.log(chalk.gray(`tree: ${ctx.tree}`));
231
- console.log(chalk.gray(`compose: ${ctx.composeFile}`));
244
+ console.log(chalk.gray(`compose: ${ctx.composeFile ?? 'none (host services only)'}`));
232
245
  console.log(chalk.gray(`url: ${payload.baseUrl}`));
233
246
  console.log('');
234
247
  console.log(` up ${status.up ? chalk.green('yes') : chalk.gray('no')}`);
@@ -257,12 +270,14 @@ export async function e2eDownCommand(featureName, options = {}) {
257
270
  if (!ctx) process.exit(EXIT_USAGE);
258
271
 
259
272
  const hostServices = stopHostServices(ctx.gitCommonDir, ctx.projectName);
260
- const result = tearDownStack(ctx.cwd, {
261
- composeFile: ctx.composeFile,
262
- projectName: ctx.projectName,
263
- env: ctx.env,
264
- volumes: Boolean(options.volumes),
265
- }, options._exec ?? defaultExec);
273
+ const result = ctx.composeFile
274
+ ? tearDownStack(ctx.cwd, {
275
+ composeFile: ctx.composeFile,
276
+ projectName: ctx.projectName,
277
+ env: ctx.env,
278
+ volumes: Boolean(options.volumes),
279
+ }, options._exec ?? defaultExec)
280
+ : { status: 'pass', output: '' };
266
281
  const stampDeleted = deleteStamp(ctx.gitCommonDir, ctx.projectName);
267
282
  const cleanup = afterHeavyRun({ projectPath: ctx.cwd, config: readHeavyConfig(ctx.cwd) });
268
283
 
@@ -288,6 +303,56 @@ export async function e2eDownCommand(featureName, options = {}) {
288
303
  process.exit(0);
289
304
  }
290
305
 
306
+ // Between the verifier's flows: the next flow starts from the seeded state, not from the rows the
307
+ // previous one left. Exit 1 only on fail; skip (nothing configured to reset) is exit 0.
308
+ export async function e2eResetCommand(featureName, options = {}) {
309
+ const emit = (payload) => {
310
+ if (options.json) {
311
+ console.log(JSON.stringify(payload, null, 2));
312
+ } else if (payload.status === 'fail') {
313
+ console.error(chalk.red(`\n✗ ${payload.reason}\n`));
314
+ if (payload.output) console.error(payload.output);
315
+ } else if (payload.status === 'skip') {
316
+ console.log(chalk.gray(`\n○ e2e reset: ${payload.reason}\n`));
317
+ } else {
318
+ const what = payload.reset.mode === 'command'
319
+ ? `e2e.resetCommand (${payload.reset.command})`
320
+ : payload.reset.mode === 'volumes'
321
+ ? `recreated ${payload.reset.services.join(', ')} (volumes ${payload.reset.volumes.join(', ')})`
322
+ : 'no data reset';
323
+ console.log(chalk.green(`\n✓ e2e reset — ${what}`));
324
+ if (payload.auth.reason) console.log(chalk.gray(` auth: ${payload.auth.reason}`));
325
+ console.log('');
326
+ }
327
+ process.exit(payload.status === 'fail' ? 1 : 0);
328
+ };
329
+
330
+ const ctx = resolveContext(featureName, {
331
+ ...options,
332
+ onMissing: (message, tree) => emit({ feature: featureName, tree, status: 'fail', reason: message }),
333
+ });
334
+ if (!ctx) return;
335
+
336
+ const result = resetE2eData(ctx.cwd, {
337
+ config: ctx.config,
338
+ composeFile: ctx.composeFile,
339
+ projectName: ctx.projectName,
340
+ gitCommonDir: ctx.gitCommonDir,
341
+ env: ctx.env,
342
+ }, options._exec ?? defaultExec);
343
+
344
+ emit({
345
+ feature: featureName,
346
+ project: ctx.projectName,
347
+ tree: ctx.tree,
348
+ status: result.status,
349
+ reason: result.reason,
350
+ reset: result.reset,
351
+ auth: result.auth,
352
+ ...(result.status === 'fail' && result.output ? { output: result.output } : {}),
353
+ });
354
+ }
355
+
291
356
  function stoppedAnything({ killed, skipped, warnings }) {
292
357
  return killed.length + skipped.length + warnings.length > 0;
293
358
  }
@@ -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);