@polymorphism-tech/morph-spec 9.8.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CLAUDE.md +10 -10
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +40 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-review.md +15 -0
  16. package/framework/commands/morph-status.md +1 -1
  17. package/framework/docs/CLI.md +83 -94
  18. package/framework/evals/review-checklist.md +45 -53
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/core/README.md +1 -1
  21. package/framework/hooks/claude-code/core/denylist.json +4 -4
  22. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  23. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  24. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  25. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  26. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  27. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  28. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  29. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  30. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  31. package/framework/hooks/shared/e2e-check.js +0 -0
  32. package/framework/hooks/shared/gate-event.js +2 -2
  33. package/framework/hooks/shared/graph-refresh.js +20 -3
  34. package/framework/hooks/shared/heavy-command.js +32 -1
  35. package/framework/hooks/shared/heavy-config.js +1 -1
  36. package/framework/hooks/shared/next-step.js +63 -24
  37. package/framework/hooks/shared/phase-utils.js +6 -9
  38. package/framework/hooks/shared/report-progress.js +86 -0
  39. package/framework/hooks/shared/state-reader.js +3 -2
  40. package/framework/hooks/statusline.js +332 -0
  41. package/framework/phases.json +32 -10
  42. package/framework/rules/ai-agents-standards.md +9 -1
  43. package/framework/rules/csharp-standards.md +4 -0
  44. package/framework/rules/ef-standards.md +20 -0
  45. package/framework/rules/frontend-standards.md +1 -1
  46. package/framework/rules/implement-report.md +25 -0
  47. package/framework/rules/infrastructure-standards.md +2 -0
  48. package/framework/rules/nextjs-standards.md +7 -0
  49. package/framework/rules/plan-grammar.md +53 -0
  50. package/framework/rules/ui-design.md +27 -0
  51. package/framework/schemas/config.schema.json +11 -18
  52. package/framework/schemas/feature.schema.json +7 -1
  53. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  54. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  55. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  56. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  57. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  58. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  59. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  60. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  61. package/framework/skills/core/morph-apply/references/review.md +65 -0
  62. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  63. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  64. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  65. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  66. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  67. package/framework/standards/STANDARDS.json +32 -5
  68. package/framework/standards/STANDARDS.meta.json +5 -1
  69. package/framework/standards/ai-agents/agent-session.md +2 -3
  70. package/framework/standards/ai-agents/context-providers.md +2 -2
  71. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  72. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  73. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  74. package/framework/standards/ai-agents/setup.md +1 -1
  75. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  76. package/framework/standards/architecture/stack-baseline.md +47 -0
  77. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  78. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  79. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  80. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  81. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  82. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  83. package/framework/templates/REGISTRY.json +1 -2
  84. package/framework/templates/context/ARCHITECTURE.md +1 -1
  85. package/framework/templates/docs/plan.md +12 -12
  86. package/framework/templates/docs/ui-mockups.md +17 -105
  87. package/package.json +1 -1
  88. package/src/commands/e2e/e2e.js +88 -23
  89. package/src/commands/graph/diff.js +74 -0
  90. package/src/commands/graph/status.js +1 -1
  91. package/src/commands/observability/cost.js +2 -4
  92. package/src/commands/observability/telemetry.js +1 -1
  93. package/src/commands/project/doctor.js +23 -39
  94. package/src/commands/project/fleet.js +5 -6
  95. package/src/commands/project/init.js +63 -36
  96. package/src/commands/project/stack-scan.js +3 -42
  97. package/src/commands/project/stack-set.js +26 -0
  98. package/src/commands/project/status.js +12 -13
  99. package/src/commands/project/update.js +39 -3
  100. package/src/commands/project/worktree.js +3 -14
  101. package/src/commands/state/advance.js +17 -17
  102. package/src/commands/state/approve.js +0 -6
  103. package/src/commands/state/archive.js +27 -1
  104. package/src/commands/state/auto-approval.js +1 -1
  105. package/src/commands/state/create.js +0 -5
  106. package/src/commands/state/gate-check.js +37 -30
  107. package/src/commands/state/gate-decision.js +3 -2
  108. package/src/commands/state/next.js +1 -0
  109. package/src/commands/state/recap.js +67 -0
  110. package/src/commands/state/report-check.js +36 -178
  111. package/src/commands/validation/plan-check.js +16 -11
  112. package/src/commands/validation/validate-feature.js +3 -3
  113. package/src/commands/validation/verify.js +3 -6
  114. package/src/core/paths/output-schema.js +3 -3
  115. package/src/core/state/state-manager.js +2 -4
  116. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  117. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  118. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  119. package/src/dashboard/app/pages/Activity.jsx +4 -0
  120. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  121. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  122. package/src/dashboard/app/pages/Features.jsx +7 -7
  123. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  124. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  125. package/src/dashboard/scanner/activity-scanner.js +29 -0
  126. package/src/dashboard/scanner/agents-scanner.js +11 -14
  127. package/src/dashboard/scanner/features-scanner.js +45 -24
  128. package/src/dashboard/scanner/project-api.js +11 -2
  129. package/src/dashboard/scanner/skills-scanner.js +11 -1
  130. package/src/lib/e2e/applicability.js +3 -1
  131. package/src/lib/git/tree-state.js +1 -16
  132. package/src/lib/graph/diff-tree.js +127 -0
  133. package/src/lib/graph/graphify-runtime.js +12 -5
  134. package/src/lib/installers/memory-installer.js +2 -2
  135. package/src/lib/installers/rules-installer.js +57 -0
  136. package/src/lib/installers/stack-assets.js +17 -0
  137. package/src/lib/installers/stack-sync.js +219 -0
  138. package/src/lib/plan/checks.js +55 -110
  139. package/src/lib/plan/critique.js +28 -0
  140. package/src/lib/plan/media.js +42 -0
  141. package/src/lib/plan/plan-html.js +125 -0
  142. package/src/lib/plan/plan-parser.js +34 -45
  143. package/src/lib/recap/piper.js +36 -0
  144. package/src/lib/recap/recap.js +232 -0
  145. package/src/lib/review-reports.js +28 -4
  146. package/src/lib/risk-detector.js +90 -11
  147. package/src/lib/specs/screen-checklist.js +33 -48
  148. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  149. package/src/lib/telemetry/metrics.js +27 -29
  150. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  151. package/src/lib/validators/verify-runner.js +0 -7
  152. package/src/lib/worktree/fleet-scanner.js +7 -20
  153. package/src/lib/worktree/worktree-audit.js +0 -37
  154. package/src/scripts/setup-infra.js +15 -50
  155. package/src/utils/agents-installer.js +27 -201
  156. package/src/utils/hooks-installer.js +11 -95
  157. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  158. package/framework/hooks/claude-code/statusline.py +0 -699
  159. package/framework/hooks/claude-code/statusline.sh +0 -11
  160. package/framework/hooks/shared/agent-reap.js +0 -74
  161. package/framework/hooks/shared/slice-progress.js +0 -110
  162. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  163. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  164. package/framework/skills/core/morph-review/SKILL.md +0 -129
  165. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  166. package/framework/standards/PROVENANCE.json +0 -119
  167. package/src/commands/project/worktree-reap.js +0 -34
  168. package/src/commands/state/handoff.js +0 -65
  169. package/src/commands/validation/mutate.js +0 -263
  170. package/src/commands/validation/scout-check.js +0 -28
  171. package/src/lib/generators/handoff-generator.js +0 -172
  172. package/src/lib/mutation/adapters.js +0 -79
  173. package/src/lib/mutation/commands.js +0 -61
  174. package/src/lib/mutation/dotnet-introspection.js +0 -281
  175. package/src/lib/mutation/manifest.js +0 -144
  176. package/src/lib/mutation/plan.js +0 -44
  177. package/src/lib/mutation/report.js +0 -68
  178. package/src/lib/mutation/runner.js +0 -301
  179. package/src/lib/mutation/schemata.js +0 -60
  180. package/src/lib/mutation/snapshot.js +0 -65
  181. package/src/lib/mutation/subset.js +0 -109
  182. package/src/lib/mutation/trx.js +0 -50
  183. package/src/lib/scout/scout-check.js +0 -293
  184. package/src/lib/validators/traceability-check.js +0 -103
  185. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -5,7 +5,7 @@
5
5
  > **Keywords:** pinned gallery, horizontal scroll, modal, lightbox, parallax, count-up, reveal, scroll progress, accordion
6
6
  > **Load When:** ui-designer active, or building a scroll-driven marketing page
7
7
 
8
- **Verified against:** GSAP 3.13 (ScrollTrigger, matchMedia, context) + React 18 + Lenis 1.x, shipped to production. Last-verified: 2026-08-21.
8
+ **Verified against:** GSAP 3.13 (ScrollTrigger, matchMedia, context) + React 18 + Lenis 1.x, shipped to production. Last-verified: 2026-10-27.
9
9
 
10
10
  ---
11
11
 
@@ -114,6 +114,19 @@ useEffect(() => {
114
114
 
115
115
  **Render overlays outside any pinned section.** A pin applies a `transform` to its container, which becomes the containing block for `position: fixed` and breaks the overlay. Put the modal in a fragment as a sibling of the section.
116
116
 
117
+ The markup: the backdrop closes on click, the panel stops propagation so a click inside does not close it, and the dialog carries its role.
118
+
119
+ ```jsx
120
+ {active !== null && (
121
+ <div className={`lmodal${shown ? ' in' : ''}`} onClick={close} role="dialog" aria-modal="true">
122
+ <div className="lmodal__panel" data-lenis-prevent onClick={(e) => e.stopPropagation()}>
123
+ <button className="lmodal__close" onClick={close} aria-label="Close">×</button>
124
+ {/* content */}
125
+ </div>
126
+ </div>
127
+ )}
128
+ ```
129
+
117
130
  **Mark the scrollable panel `data-lenis-prevent`.** Stopping the smooth scroller is not enough: while stopped it calls `preventDefault()` on any `touchmove` that reaches it, so a panel with `overflow-y: auto` scrolls with the wheel and is frozen under a finger. Whatever sits at the bottom of the panel, usually the CTA, becomes unreachable on a phone. See `frontend/scroll-driven/smooth-scroll.md`, Section 3.
118
131
 
119
132
  ```jsx
@@ -199,6 +212,8 @@ Format for the locale at write time (the decimal comma above is pt-BR). Never an
199
212
 
200
213
  ## Section 7: Persistent Elements
201
214
 
215
+ These live at the app root, outside every section, and repeat on almost every marketing page.
216
+
202
217
  **Slim translucent nav** that gains a background once scrolled:
203
218
 
204
219
  ```jsx
@@ -208,6 +223,19 @@ useEffect(() => {
208
223
  window.addEventListener('scroll', onScroll, { passive: true })
209
224
  return () => window.removeEventListener('scroll', onScroll)
210
225
  }, [])
226
+ return (
227
+ <nav className={`nav${solid ? ' nav--solid' : ''}`}>
228
+ <a className="nav__logo" href="#top"><span>{BRAND}</span></a>
229
+ <div className="nav__links">{/* anchors */}</div>
230
+ <a className="btn btn-accent" href="#contact">{CTA}</a>
231
+ </nav>
232
+ )
233
+ ```
234
+
235
+ ```css
236
+ .nav { position: fixed; top: 0; inset-inline: 0; z-index: 100; display: flex; align-items: center;
237
+ justify-content: space-between; padding: 16px clamp(16px,5vw,48px); transition: background .3s, backdrop-filter .3s; }
238
+ .nav--solid { background: rgba(14,11,12,.7); backdrop-filter: blur(10px); border-bottom: 1px solid var(--hair); }
211
239
  ```
212
240
 
213
241
  **Scroll progress bar** that prefers the smooth scroller when present:
@@ -225,20 +253,64 @@ useEffect(() => {
225
253
  }, [])
226
254
  ```
227
255
 
256
+ ```jsx
257
+ return <div className="scroll-progress" ref={ref} />
258
+ ```
259
+
260
+ ```css
261
+ .scroll-progress { position: fixed; top: 0; left: 0; z-index: 101; height: 3px; width: 100%;
262
+ transform: scaleX(0); transform-origin: 0 50%; background: linear-gradient(90deg, var(--accent), var(--accent-soft)); }
263
+ ```
264
+
228
265
  Transform-based progress (`scaleX` with `transform-origin: 0 50%`) stays on the compositor. Animating `width` does not.
229
266
 
267
+ **Floating chat button.** A fixed exit channel to the business's messaging app, always within reach. It is also the fallback when the lead form fails. For WhatsApp, a `wa.me` deep link with a prefilled message:
268
+
269
+ ```jsx
270
+ <a className="chat-float" href={`https://wa.me/${PHONE}?text=${encodeURIComponent(MESSAGE)}`}
271
+ target="_blank" rel="noopener noreferrer" aria-label="Chat on WhatsApp">
272
+ <svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true">{/* icon path */}</svg>
273
+ </a>
274
+ ```
275
+
276
+ ```css
277
+ .chat-float { position: fixed; right: 20px; bottom: 20px; z-index: 90; width: 56px; height: 56px;
278
+ display: grid; place-items: center; border-radius: 50%; background: #25d366; color: #fff;
279
+ box-shadow: 0 8px 24px rgba(0,0,0,.35); transition: transform .25s; }
280
+ .chat-float:hover { transform: translateY(-3px) scale(1.05); }
281
+ ```
282
+
283
+ Keep its z-index below the nav and overlays, and check that it does not cover a CTA on mobile.
284
+
230
285
  ---
231
286
 
232
287
  ## Section 8: Accordion
233
288
 
234
- Use native `<details>`. It provides toggle, keyboard support and accessibility for free.
289
+ Use native `<details>`. It provides toggle, keyboard support and accessibility for free. On a landing page the FAQ sits right before the form, to clear objections.
290
+
291
+ ```jsx
292
+ const FAQ = [{ q: '...', a: '...' }]
293
+ return (
294
+ <section className="section faq" id="faq">
295
+ <h2>{TITLE}</h2>
296
+ {FAQ.map((f) => (
297
+ <details className="faq__item" key={f.q}>
298
+ <summary>{f.q}<span className="faq__icon" aria-hidden="true">+</span></summary>
299
+ <p>{f.a}</p>
300
+ </details>
301
+ ))}
302
+ </section>
303
+ )
304
+ ```
235
305
 
236
306
  ```css
307
+ .faq__item { border-bottom: 1px solid var(--hair); padding: 18px 0; }
237
308
  .faq__item summary { cursor: pointer; list-style: none; display: flex;
238
- justify-content: space-between; align-items: center; }
309
+ justify-content: space-between; align-items: center; font-family: var(--serif); font-size: 1.15rem; }
239
310
  .faq__item summary::-webkit-details-marker { display: none; }
240
- .faq__item .faq__icon { transition: transform .3s; }
311
+ .faq__item .faq__icon { transition: transform .3s; color: var(--accent); }
241
312
  .faq__item[open] .faq__icon { transform: rotate(45deg); }
313
+ .faq__item p { margin: 14px 0 0; color: var(--fg-dim); max-width: 70ch; }
242
314
  ```
243
315
 
244
316
  Native `<details>` cannot animate its own height. If height animation is required, drive the inner content with GSAP rather than replacing the element with a custom widget.
@@ -332,10 +332,9 @@
332
332
  "path": "docs/ui-mockups.md",
333
333
  "category": "documentation",
334
334
  "phase": "uiux",
335
- "description": "Wireframes and layout specifications with ASCII mockups and responsive breakpoints",
335
+ "description": "Por tela: protótipo HTML, direção escolhida, checklist de tela de 5 itens, componentes e fluxo",
336
336
  "outputName": "mockups.md",
337
337
  "placeholders": [
338
- "DATE",
339
338
  "FEATURE_NAME"
340
339
  ],
341
340
  "deprecated": false,
@@ -88,7 +88,7 @@
88
88
  ## Centralidade (grafo de codebase)
89
89
 
90
90
  Símbolos com mais conexões depois do saneamento — o que quebra mais se mudar.
91
- Detalhe de um símbolo: `morph-spec graph explain <nome>`.
91
+ Detalhe de um símbolo: `morph-spec graph explain <nome>` (sem grafo, símbolo ausente ou ambíguo: `grep -rn <nome>`).
92
92
 
93
93
  | Símbolo | Arquivo | Grau |
94
94
  |---------|---------|------|
@@ -15,10 +15,21 @@
15
15
 
16
16
  - <o que foi assumido sem prova; "nenhuma" vale>
17
17
 
18
+ ## Padrões assumidos
19
+
20
+ <!-- Valor de baixo risco que você decidiu sem perguntar, numerado: o usuário veta por número ("veta 2").
21
+ Auth, credencial, permissão, pagamento e apagar dado nunca entram aqui: viram pergunta. "nenhum" vale. -->
22
+
23
+ 1. <padrão e o valor escolhido>
24
+
25
+ <!-- Opcional, só se algo muda de estado: uma seção "Estados" (h2) com uma transição por linha,
26
+ "- origem → destino: gatilho". O plan-check desenha a máquina em 2-plan/plan.html. -->
27
+
18
28
  ## Cenários
19
29
 
20
30
  <!-- Um cenário por comportamento observável. `prova:` é teste | e2e | eval | manual | leitura.
21
31
  `prova: teste` exige `risco:` regra | dinheiro | contrato | concorrência | dado | regressão; sem risco, `leitura`.
32
+ `degrau:` migrar | irreversível quando um revert não desfaz (toda migration é migrar); sem a linha, refazer.
22
33
  Cenário que fala de arquivo, grep ou texto exige a linha "> Por que artefato:" logo abaixo. -->
23
34
 
24
35
  - **C1** — Dado <estado>, quando <ação>, então <resultado observável>.
@@ -56,19 +67,8 @@ nenhum
56
67
  |---|---|---|---|
57
68
  | <conceito> | REUSAR | <arquivo ou símbolo existente> | |
58
69
 
59
- ## Fatias
60
-
61
- <!-- Uma fatia = uma unidade de entrega (com ou sem dispatch), relatório em 3-implement/reports/Fn.md.
62
- arquivos ≤ 8; superfície e depende são opcionais; toda Cn é coberta por alguma fatia. -->
63
-
64
- ### F1 — <título>
65
- - persona: dotnet-senior
66
- - standards: <id>
67
- - arquivos: <caminho/a.cs>, <caminho/b.cs>
68
- - cobre: C1
69
-
70
70
  ## Contrato de implementação
71
71
 
72
- <Assinaturas, records, enums e rotas que as fatias precisam honrar.>
72
+ <Assinaturas, records, enums e rotas que a implementação honra; runtime do e2e; baseline de teste como comando.>
73
73
 
74
74
  ## Errata
@@ -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>
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.0.0",
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')) {