@polymorphism-tech/morph-spec 8.40.0 → 8.41.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 (45) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +2 -2
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +1 -1
  7. package/framework/MORPH.md +9 -8
  8. package/framework/agents.json +8 -2
  9. package/framework/docs/CLI.md +5 -3
  10. package/framework/hooks/README.md +1 -1
  11. package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
  12. package/framework/hooks/claude-code/core/worktree-create.js +22 -6
  13. package/framework/hooks/shared/git-paths.js +38 -5
  14. package/framework/mcp/registry.json +26 -65
  15. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  16. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  17. package/framework/skills/core/morph-apply/SKILL.md +55 -25
  18. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  19. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  20. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  21. package/framework/skills/core/morph-implement/SKILL.md +43 -24
  22. package/framework/skills/core/morph-media/SKILL.md +2 -2
  23. package/framework/skills/core/morph-plan/SKILL.md +7 -1
  24. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  25. package/framework/skills/core/morph-review/SKILL.md +1 -1
  26. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  27. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  28. package/framework/skills/core/morph-uiux/SKILL.md +3 -3
  29. package/framework/standards/PROVENANCE.json +4 -4
  30. package/framework/standards/STANDARDS.json +6 -6
  31. package/framework/standards/backend/dotnet/testing.md +4 -2
  32. package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
  33. package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
  34. package/framework/standards/frontend/nextjs/testing.md +4 -2
  35. package/framework/standards/integration/mcp/mcp-tools.md +19 -12
  36. package/package.json +1 -1
  37. package/src/commands/project/doctor.js +9 -1
  38. package/src/commands/project/worktree.js +60 -4
  39. package/src/commands/state/gate-check.js +5 -0
  40. package/src/commands/tasks/dag.js +27 -4
  41. package/src/commands/validation/verify.js +22 -38
  42. package/src/lib/risk-detector.js +13 -0
  43. package/src/lib/tasks/tasks-json-parser.js +21 -0
  44. package/src/lib/worktree/env-allocator.js +9 -3
  45. package/src/utils/agents-installer.js +51 -1
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX — design system, mockups, compone
4
4
  version: core
5
5
  when-to-invoke: ao tomar o ramo uiux (morph-spec advance --design), depois do Gate 1 e antes do morph-plan
6
6
  user-invokable: true
7
- cliVersion: "8.40.0"
7
+ cliVersion: "8.41.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
@@ -32,7 +32,7 @@ Ao tomar o ramo opcional uiux: feature/bug cuja mudança cria ou altera superfí
32
32
  - **Sistemas, não telas isoladas** — mesmo critério da persona `ui-designer`: componentes reutilizáveis antes de composições de uso único.
33
33
  - **Toda tela sai com o checklist de tela preenchido** (template no passo 3) — a superfície, o volume esperado de dados, paginação, ordenação, filtro, os três estados (vazio, carregando, erro), o comportamento no mobile e abas × página longa. "Não se aplica" vale, **desde que escrito**. É o checklist que faz a lista de 2 000 linhas paginar: o que o design não decide não vira `doneCriteria` (`morph-plan` §5d), e o que não é critério ninguém entrega. Tela de dados segue os standards de `admin-app` (lista/tabela, formulário, feedback, estrutura de página), que a persona cita na superfície `admin-app`.
34
34
 
35
- 3. **Despache `morph-ui-designer`** para o trabalho visual — dispatch é o default aqui (domínio diferente do Orchestrator, MORPH.md §6). `morph-ui-designer` tem o escopo das implementadoras (com Bash) + `Skill` + MCPs `stitch`/`gemini-image`/`playwright`/`context7`/`seedance`/`graphify`. Bash serve para `morph-spec standards --find`, `npx`, screenshot e leitura de `git`; build/test da suíte, `verify` e aprovação de gate continuam do orquestrador (cláusula 3, como nas outras personas).
35
+ 3. **Despache `morph-ui-designer`** para o trabalho visual — dispatch é o default aqui (domínio diferente do Orchestrator, MORPH.md §6). `morph-ui-designer` tem o escopo das implementadoras (com Bash) + `Skill` + MCPs `stitch`/`playwright`/`context7`/`graphify` + o MCP de mídia morph-assets (imagem e vídeo). Bash serve para `morph-spec standards --find`, `npx`, screenshot e leitura de `git`; build/test da suíte, `verify` e aprovação de gate continuam do orquestrador (cláusula 3, como nas outras personas).
36
36
 
37
37
  **Persona universal** (`ui-designer.stack = ["*"]` em `agents.json`, mesmo mecanismo de `evaluator`/`scout`): UI/UX importa sempre que há superfície de UI, então o subagent está registrado em **todo** projeto — .NET/Blazor incluído, não só Next.js. **Fallback** (caso raro: instalação anterior a essa mudança, `.claude/agents/morph-ui-designer.md` ainda não regenerado): `general-purpose` colando o campo `persona` (Read em `.morph/framework/agents.json`) no prompt, registrando o fallback no relatório — resolvido rodando `morph-spec setup-infra --reinstall`.
38
38
 
@@ -46,7 +46,7 @@ Ao tomar o ramo opcional uiux: feature/bug cuja mudança cria ou altera superfí
46
46
  Produza, NESTA ORDEM — design-system.md primeiro, os demais só como aplicável ao escopo:
47
47
  1. `design-system.md` — tokens (cor, tipografia, espaço, motion) consistentes com o projeto
48
48
  2. `mockups.md` — descrição estruturada de cada tela nova/alterada (+ imagem via MCP
49
- gemini-image/stitch quando disponível; fallback: descrição textual detalhada).
49
+ stitch ou o MCP de mídia (imagem/vídeo) quando disponível; fallback: descrição textual detalhada).
50
50
  Cada tela é um `## Tela: <nome>` e traz, dentro dela, o checklist abaixo, com cada `…`
51
51
  trocado pela decisão — "não se aplica" vale se escrito; `…`, vazio ou "a definir" não.
52
52
  O que cada item decide: Superfície (admin-app | marketing-site — ela escolhe que
@@ -38,13 +38,13 @@
38
38
  "skills": []
39
39
  },
40
40
  {
41
- "id": "gemini-image",
41
+ "id": "morph-image",
42
42
  "repo": "morph-HUB",
43
- "path": "skills/gemini-image",
43
+ "path": "skills/morph-image",
44
44
  "envVar": "MORPH_HUB",
45
45
  "defaultCheckout": "R:/Polymorphism Tech/repos/morph-HUB",
46
- "sourceTree": "ec0abd827badfd5cd6d4c5f8b1843f81a3d183fe",
47
- "sourceCommit": "b6bbfff",
46
+ "sourceTree": "e55dcf307278583120601c3c5d55118302335f7b",
47
+ "sourceCommit": "c4c9164",
48
48
  "distilledOn": "2026-09-09",
49
49
  "distilledFrom": [
50
50
  "SKILL.md"
@@ -1551,7 +1551,7 @@
1551
1551
  "errorbody": "#rule-2--error-path-tests-assert-the-response-body-not-just-the-status-code",
1552
1552
  "test-budget": "#test-budget"
1553
1553
  },
1554
- "digest": "Inject TimeProvider (never DateTime.UtcNow / DateOnly.FromDateTime(DateTime.UtcNow) directly in a handler or validator) and fake it with FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing) in integration tests — deterministic, no midnight-UTC flake, boundary dates (today vs. yesterday) become facts about the fake clock instead of when the suite happens to run. xUnit + WebApplicationFactory error-path tests (400/404/409) must assert the ProblemDetails response BODY (title/detail/violated field or error code), never just the HTTP status code — a status-only assertion lets the wrong validation rule or the wrong error branch pass silently as long as the status matches. Test budget per task: one test per postcondition plus one regression test per bug, more only with a one-line justification in the task; scenarios of the same behavior (edge cases included) are rows of one [Theory], never copied [Fact]s; no test whose only assertion is a copy, config or prose string (that is an analyzer or a publish-time validation, outside the suite count); shared fixtures before new helpers; TDD for bugs (regression first) and pure logic; a surviving mutant is killed by tightening an assertion or adding a row, not by a new test.",
1554
+ "digest": "Inject TimeProvider (never DateTime.UtcNow / DateOnly.FromDateTime(DateTime.UtcNow) directly in a handler or validator) and fake it with FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing) in integration tests — deterministic, no midnight-UTC flake, boundary dates (today vs. yesterday) become facts about the fake clock instead of when the suite happens to run. xUnit + WebApplicationFactory error-path tests (400/404/409) must assert the ProblemDetails response BODY (title/detail/violated field or error code), never just the HTTP status code — a status-only assertion lets the wrong validation rule or the wrong error branch pass silently as long as the status matches. Test budget per task: one test per postcondition plus one regression test per bug, more only with a one-line justification in the task; scenarios of the same behavior (edge cases included) are rows of one [Theory], never copied [Fact]s; no test whose only assertion is a copy, config or prose string (that is an analyzer or a publish-time validation, outside the suite count); shared fixtures before new helpers; bugs and pure logic get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation (the Gate 3 mutation proof shows the test bites); a surviving mutant is killed by tightening an assertion or adding a row, not by a new test.",
1555
1555
  "verifiedAgainst": ".NET 10 + xUnit 2 + `Microsoft.Extensions.TimeProvider.Testing`.",
1556
1556
  "provedBy": []
1557
1557
  },
@@ -1850,8 +1850,8 @@
1850
1850
  "favicon-og": "#the-two-assets-everyone-forgets",
1851
1851
  "variations": "#variations-and-cost"
1852
1852
  },
1853
- "digest": "Guidelines for generating project images with the Gemini Image MCP: when to use AI versus stock versus SVG, prompt patterns, aspect ratios, iterative editing and assets.md format. Core principle is brand fidelity over invention: always pass the client's real assets as referenceImages via edit_image rather than generating in a vacuum, never AI-recreate a property developer's architectural render or a physical product (animate or reference the real one), and chain derivatives from an approved hero so the whole set shares light, palette and identity. Frame anything destined to become video WITHOUT a face, because video moderation is stricter (E005), and generate scrub frames at the video's exact ratio with the subject opposite the copy. Always produce the two forgotten assets: a favicon derived from the real logo (never a new one) and a 1200x630 OG share image. Offer two to three genuinely different variations per image, varying angle, light or composition rather than the seed, and never advance with a weak image because every derivative inherits it.",
1854
- "verifiedAgainst": "Gemini Image MCP + Next.js 15 `next/image`.",
1853
+ "digest": "Guidelines for generating project images with the morph-assets MCP (OpenRouter; default openai/gpt-image-2.5-flare, drafts on mai-image-2.6-flash, gemini-3.1-flash-image for 2K/4K or 6-14 refs; models: [...] compares): when to use AI versus stock versus SVG, prompt patterns, aspect ratios, iterative editing and assets.md format. Core principle is brand fidelity over invention: always pass the client's real assets as references (or @brand with a project) via edit_image rather than generating in a vacuum, never AI-recreate a property developer's architectural render or a physical product (animate or reference the real one), and chain derivatives from an approved hero so the whole set shares light, palette and identity. Frame anything destined to become video WITHOUT a face, because video moderation is stricter (E005), and generate scrub frames at the video's exact ratio with the subject opposite the copy. Always produce the two forgotten assets: a favicon derived from the real logo (never a new one) and a 1200x630 OG share image. Offer two to three genuinely different variations per image (variants returns a contact sheet), varying angle, light or composition since seed is not supported by the GPT Image/Gemini models, and never advance with a weak image because every derivative inherits it. Copy to render goes between double quotes; CONFIRMATION_REQUIRED needs human approval and a confirmToken.",
1854
+ "verifiedAgainst": "morph-assets MCP (OpenRouter; default model `openai/gpt-image-2.5-flare`) + Next.js 15 `next/image`.",
1855
1855
  "provedBy": []
1856
1856
  },
1857
1857
  {
@@ -1889,8 +1889,8 @@
1889
1889
  "cost-ladder": "#section-4-the-cost-ladder",
1890
1890
  "prompt-skeleton": "#section-5-prompt-skeleton"
1891
1891
  },
1892
- "digest": "The decisive rule: a perfect loop has an end frame identical to its start, which is right for a self-playing ambient background and WRONG for scroll scrubbing, because start and end look the same and the page reads as frozen while the user scrolls. Scroll-driven footage needs directional movement via generate_video with an endImage (push-in/dolly is the most reliable, then reveal, pull-back, slow tilt); avoid orbits and any movement that returns to its origin. Client architectural renders are animated with img2vid, never AI-recreated. Human faces repeatedly trip moderation (E005), so crop the face out of frame (neck down, hands, product) and reserve faces for static image generation. Always climb the cost ladder: fast/480p/4s draft to judge framing and movement, 1080p only after approval, then re-extract frames with a bumped cache-bust. Prompts read like director notes with no hype words, and must reserve a calm dark region for the page copy with the subject on the opposite side. Generation is async: generate_video returns a request_id polled via get_video; list_videos reads local history and spends no credits.",
1893
- "verifiedAgainst": "seedance MCP (generate_video, generate_loop_video, get_video, list_videos) over Replicate.",
1892
+ "digest": "The decisive rule: a perfect loop has an end frame identical to its start, which is right for a self-playing ambient background and WRONG for scroll scrubbing, because start and end look the same and the page reads as frozen while the user scrolls. Scroll-driven footage needs directional movement via generate_video with an endImage (push-in/dolly is the most reliable, then reveal, pull-back, slow tilt); avoid orbits and any movement that returns to its origin. Client architectural renders are animated with img2vid, never AI-recreated. Human faces repeatedly trip moderation (E005), so crop the face out of frame (neck down, hands, product) and reserve faces for static image generation. Always climb the cost ladder: seedance-1-5-pro 480p draft to judge framing and movement, then promoteFrom to seedance-2.0 1080p only after approval (same prompt, frames and seed), then re-extract frames with a bumped cache-bust. seedance-2.0-mini does NOT respect endImage and Veo 3.1 Lite/Fast are inconclusive, so scrub only on Seedance 1.5 Pro/2.0 and check the last-frame SSIM QA. Video has no ZDR on OpenRouter: nothing sensitive in the frames. Prompts read like director notes with no hype words, and must reserve a calm dark region for the page copy with the subject on the opposite side. Generation is async via the morph-assets MCP (OpenRouter): a running job is polled with get_video (several ids at once), never by resending generate_video; library reads local history and spends no credits; media_tool extracts scrub frames and posters.",
1893
+ "verifiedAgainst": "morph-assets MCP (generate_video, generate_loop_video, get_video, library, media_tool) over OpenRouter; models `bytedance/seedance-1-5-pro` and `bytedance/seedance-2.0`.",
1894
1894
  "provedBy": []
1895
1895
  },
1896
1896
  {
@@ -2371,7 +2371,7 @@
2371
2371
  "e2e": "#test-file-co-location",
2372
2372
  "test-budget": "#test-budget"
2373
2373
  },
2374
- "digest": "Vitest for unit tests, React Testing Library for component tests. Mock next/navigation with vi.mock. Test Server Actions with direct function calls. Use MSW for API mocking in component tests. Playwright for E2E tests against running app. Never test implementation details — test user behavior and output. Test budget per task: one test per postcondition plus one regression test per bug, more only with a one-line justification in the task; scenarios of the same behavior are rows of one it.each, never copied it() blocks; no test whose only assertion is a copy string (lint or publish-time validation instead); shared wrappers, MSW handlers and factories before new helpers; TDD for bugs and pure logic, component tests may come after the code.",
2374
+ "digest": "Vitest for unit tests, React Testing Library for component tests. Mock next/navigation with vi.mock. Test Server Actions with direct function calls. Use MSW for API mocking in component tests. Playwright for E2E tests against running app. Never test implementation details — test user behavior and output. Test budget per task: one test per postcondition plus one regression test per bug, more only with a one-line justification in the task; scenarios of the same behavior are rows of one it.each, never copied it() blocks; no test whose only assertion is a copy string (lint or publish-time validation instead); shared wrappers, MSW handlers and factories before new helpers; bugs and pure logic get tests in any order — the implementer runs its own filter once, never red-green cycles nor production mutation.",
2375
2375
  "verifiedAgainst": "Vitest 3 + React Testing Library 16 + MSW 2.",
2376
2376
  "provedBy": []
2377
2377
  },
@@ -128,8 +128,10 @@ regression test per bug fixed**. Going above it takes a one-line justification i
128
128
  - **Shared fixture by default.** Reuse the project's `WebApplicationFactory` fixture, fakes and
129
129
  builders. Before writing a new helper, look for the existing one (`morph-spec graph explain
130
130
  <Name>`; fallback without a graph: `grep -rn`).
131
- - **TDD where it pays:** a bug gets its regression test first (red, then the fix), and so does pure
132
- logic. Integration and endpoint tests may come after the code.
131
+ - **Order is free, proof is not the implementer's:** a bug gets a regression test, and pure logic
132
+ gets its tests, in whatever order the code comes out. The implementer does not run red→green
133
+ cycles nor mutate production code to watch a test fail; it runs its own filter once before the
134
+ report, and the Gate 3 mutation proof is what shows the test bites.
133
135
  - **A surviving mutant** of the Gate 3 mutation proof is killed by tightening an existing assertion
134
136
  or adding a table row, not by one new test per mutant.
135
137
 
@@ -2,10 +2,10 @@
2
2
 
3
3
  > **Scope:** nextjs
4
4
  > **Layer:** 1 (load when relevant)
5
- > **Keywords:** image, gemini, asset, ai-generation, placeholder, mcp, next/image
5
+ > **Keywords:** image, gpt-image, gemini, openrouter, morph-assets, asset, ai-generation, placeholder, mcp, next/image
6
6
  > **Load When:** feature has UI components requiring image assets
7
7
 
8
- **Verified against:** Gemini Image MCP + Next.js 15 `next/image`. Last-verified: 2026-05-20.
8
+ **Verified against:** morph-assets MCP (OpenRouter; default model `openai/gpt-image-2.5-flare`) + Next.js 15 `next/image`. Last-verified: 2026-09-27.
9
9
 
10
10
  ---
11
11
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  | Use Case | Approach | Examples |
15
15
  |----------|----------|---------|
16
- | **AI generation** | Gemini Image MCP | Hero images, brand illustrations, feature-specific visuals, empty states, background panels |
16
+ | **AI generation** | morph-assets MCP | Hero images, brand illustrations, feature-specific visuals, empty states, background panels |
17
17
  | **Stock photos** | External sources | Generic content (people in offices, nature scenes, stock portraits) |
18
18
  | **SVG/CSS** | Code-based | Icons, patterns, gradients, geometric shapes, loading animations |
19
19
  | **Skip** | Other tools | Screenshots (Playwright), diagrams (Mermaid), logos (vector tools) |
@@ -22,7 +22,7 @@
22
22
 
23
23
  ## Brand Fidelity: Reference, Don't Invent
24
24
 
25
- The advantage of an image MCP over generating from scratch is staying consistent with the client's **real** assets. Pass them as reference (`edit_image` with `referenceImages`) rather than generating in a vacuum. A generated look-alike is a different product, and the client sees it immediately.
25
+ The advantage of an image MCP over generating from scratch is staying consistent with the client's **real** assets. Pass them as reference (`edit_image` / `references`, or `@brand` with a `project`) rather than generating in a vacuum. A generated look-alike is a different product, and the client sees it immediately.
26
26
 
27
27
  Two cases where this is a hard rule:
28
28
 
@@ -34,46 +34,60 @@ Two cases where this is a hard rule:
34
34
  Generate the **hero first**. Once approved, use it as the reference for every derived image (detail shots, cards, scene variants), so the whole set shares light, palette and identity instead of looking like separate photo shoots.
35
35
 
36
36
  ```
37
- hero -> [approved] -> derivatives with the approved hero as referenceImages -> [approved per image]
37
+ hero -> [approved] -> derivatives with the approved hero as references -> [approved per image]
38
38
  ```
39
39
 
40
40
  Skipping this chain is the most common cause of a page whose images do not feel like one set.
41
41
 
42
- ## Gemini Image MCP API
42
+ ## morph-assets MCP API
43
+
44
+ One MCP for image and video, all models through OpenRouter. `model` accepts any OpenRouter image model by id, alias (`flare`, `mai flash`, `nano banana 2`, `nano banana pro`) or approximate name.
45
+
46
+ | Role | Model |
47
+ |------|-------|
48
+ | Default (editing with refs, text) | `openai/gpt-image-2.5-flare` |
49
+ | Draft | `microsoft/mai-image-2.6-flash` (max 5 refs) |
50
+ | 2K/4K output or 6-14 references | `google/gemini-3.1-flash-image` |
43
51
 
44
52
  ```javascript
45
53
  // Generate a new image
46
- mcp__gemini-image-mcp__generate_image({
47
- prompt: string, // Text description (max 5000 chars, narrative style)
54
+ mcp__morph-assets__generate_image({
55
+ prompt: string, // Narrative description; copy to render goes between double quotes
56
+ model?: string, // Default: openai/gpt-image-2.5-flare
57
+ models?: string[], // 2-4 models: same request in each, cost per model (to choose)
48
58
  aspectRatio?: "1:1" | "2:3" | "3:2" | "3:4" | "4:3" | "9:16" | "16:9",
59
+ resolution?: "1K" | "2K" | "4K", // Only on models that support it
60
+ variants?: number, // 1-4, returns a contact sheet for the review
61
+ references?: string[], // Paths, URLs, library ids, "@brand", "@brand:name"
62
+ project?: string, // Client project: output dir, brand refs, budget
49
63
  outputPath?: string, // File path to save (e.g., "public/images/feature/hero.png")
50
- model?: string, // Default: gemini-2.5-flash-image
64
+ dryRun?: boolean, // Estimate cost only
65
+ confirmToken?: string, // Repeat a CONFIRMATION_REQUIRED call after approval
51
66
  })
52
67
 
53
- // Edit an existing image
54
- mcp__gemini-image-mcp__edit_image({
55
- imageSource: string, // Path to image or image ID
68
+ // Edit an existing image (the base image goes as the 1st reference)
69
+ mcp__morph-assets__edit_image({
70
+ image: string, // Path to image or library id
56
71
  prompt: string, // Short, targeted edit instructions (1-2 sentences)
72
+ references?: string[],
57
73
  outputPath?: string,
58
74
  })
59
75
 
60
- // Continue editing last image
61
- mcp__gemini-image-mcp__continue_editing({
76
+ // Continue editing the last image of the project
77
+ mcp__morph-assets__continue_editing({
62
78
  prompt: string, // One adjustment at a time
79
+ imageId?: string, // Default: the last image
63
80
  })
64
81
 
65
- // List generated images
66
- mcp__gemini-image-mcp__list_images({
67
- type?: "generated" | "edited" | "all",
68
- limit?: number,
69
- })
82
+ // History and metadata (replaces list_images / get_image_info)
83
+ mcp__morph-assets__library({ op: "list" | "search" | "info" | "tag" | "project_get" | "project_set", ... })
70
84
 
71
- // Get image details
72
- mcp__gemini-image-mcp__get_image_info({
73
- imageId?: string, // Defaults to last image
74
- })
85
+ // Pick a model from the curation (verified vs broken)
86
+ mcp__morph-assets__list_models({ kind: "image", needs?: "refs>=5", task?: string })
75
87
  ```
76
88
 
89
+ **Cost gate:** every call estimates cost first. Above US$2, over the project budget or with an unknown price, the tool returns `CONFIRMATION_REQUIRED` + `confirmToken`; get human approval and repeat the same call with the token. Use `dryRun: true` before large batches or 4K.
90
+
77
91
  ## Prompt Engineering Patterns
78
92
 
79
93
  ### Design-System-Aware Prompts
@@ -101,7 +115,7 @@ Describe what the image will BE in the UI, not just the subject:
101
115
 
102
116
  ### Narrative Style
103
117
 
104
- Gemini responds best to coherent paragraphs. Build prompts by layering:
118
+ GPT Image, Gemini and MAI all respond best to coherent paragraphs. Build prompts by layering:
105
119
 
106
120
  1. **Subject** — hyper-specific description
107
121
  2. **Environment** — foreground, mid-ground, background (layered for depth)
@@ -148,9 +162,9 @@ One change at a time:
148
162
  - "More serious expression"
149
163
 
150
164
  ### Text Rendering
151
- The model frequently misspells words. After generating any image with text:
165
+ Put the exact copy between double quotes in the prompt (`the headline "Summer Sale"`) so the model renders it literally; GPT Image (the default) is the strongest at text. Models can still misspell words. After generating any image with text:
152
166
  1. View the result
153
- 2. If text is wrong: `continue_editing` with "Fix the text — it should read '{correct text}' exactly. Keep everything else."
167
+ 2. If text is wrong: `continue_editing` with "Fix the text — it should read "{correct text}" exactly. Keep everything else."
154
168
  3. Expect 2-3 iterations for correct text
155
169
 
156
170
  ## Aspect Ratio Guide
@@ -267,7 +281,7 @@ For a frame that will drive a scroll scrub, also generate at the exact ratio the
267
281
 
268
282
  ## Variations and Cost
269
283
 
270
- Generate **two or three variations per image**, varying angle, light or composition, **not just the seed**. Seed-only variation produces near-identical options and wastes the review.
284
+ Generate **two or three variations per image** (`variants: 2-3` returns a contact sheet for the review), varying angle, light or composition. Seed is not supported by the GPT Image / Gemini models, so variation has to come from the prompt; near-identical options waste the review. To choose between models or directions, `models: [...]` runs the same request on 2-4 models with the cost of each.
271
285
 
272
286
  If none are good, regenerate at the same step with the prompt adjusted by the feedback. **Do not advance with a weak image**: every derivative, any footage built from it, and the final page all inherit its quality.
273
287
 
@@ -282,7 +296,7 @@ If none are good, regenerate at the same step with the prompt adjusted by the fe
282
296
  - [ ] `priority` prop set for above-the-fold images
283
297
  - [ ] Build compiles after integration (`npm run build`)
284
298
  - [ ] Image visible in dev server at correct route
285
- - [ ] Client assets passed as `referenceImages`; nothing generated in a vacuum
299
+ - [ ] Client assets passed as `references` (or `@brand`); nothing generated in a vacuum
286
300
  - [ ] Real renders and real product photos used as-is, never AI-recreated
287
301
  - [ ] Hero approved first, then used as reference for derivatives
288
302
  - [ ] Images headed for video framed without faces
@@ -2,10 +2,10 @@
2
2
 
3
3
  > **Scope:** frontend-design-system
4
4
  > **Layer:** 3
5
- > **Keywords:** video generation, seedance, img2vid, endImage, directional footage, loop video, E005 moderation, cost ladder
5
+ > **Keywords:** video generation, seedance, morph-assets, openrouter, img2vid, endImage, directional footage, loop video, E005 moderation, cost ladder, promoteFrom
6
6
  > **Load When:** producing footage for a scroll-driven page or animating a client render
7
7
 
8
- **Verified against:** seedance MCP (generate_video, generate_loop_video, get_video, list_videos) over Replicate. Last-verified: 2026-08-21.
8
+ **Verified against:** morph-assets MCP (generate_video, generate_loop_video, get_video, library, media_tool) over OpenRouter; models `bytedance/seedance-1-5-pro` and `bytedance/seedance-2.0`. Last-verified: 2026-09-27.
9
9
 
10
10
  ---
11
11
 
@@ -38,6 +38,13 @@ Give a start image and an end image, and let the model interpolate a camera trav
38
38
  1. Pick or generate the **start frame**: the wide shot, the resting state.
39
39
  2. Pick or generate the **end frame** (`endImage`): the destination of the movement (a detail in close-up, a revealed facade).
40
40
  3. Call `generate_video { prompt, image: start, endImage: end }`. The prompt describes the camera movement between the two.
41
+ 4. Check the **last-frame QA** in the response (SSIM of the real last frame against `endImage`): ✅ respected, ⚠️ does not match, ≈ inconclusive. A ⚠️ means the model ignored `endImage` and the take will not scrub as designed.
42
+
43
+ **Models that respect `endImage` (verified 2026-09-27):** Seedance 1.5 Pro and Seedance 2.0. Use them for scrub.
44
+
45
+ - `seedance-2.0-mini` does **not** respect `endImage` (it overshoots the end frame). Do not use it for scrub.
46
+ - Veo 3.1 Lite/Fast came out **inconclusive** on the last-frame check. Do not rely on them for scrub until re-verified.
47
+ - `aspectRatio: "adaptive"` does not exist on OpenRouter: omit `aspectRatio` to inherit the ratio of the start frame.
41
48
 
42
49
  **Movements that scrub well:**
43
50
 
@@ -52,11 +59,12 @@ Give a start image and an end image, and let the model interpolate a camera trav
52
59
 
53
60
  ## Section 3: Faces Trip Moderation
54
61
 
55
- Img2vid with a **human face** repeatedly fails moderation (error E005). The workaround that works is to **crop the face out of frame** before generating.
62
+ Img2vid with a **human face** repeatedly fails moderation (it surfaced as error E005 on Replicate and still trips moderation through OpenRouter). The workaround that works is to **crop the face out of frame** before generating.
56
63
 
57
64
  - Frame from the neck down (garment, hands, fabric) rather than the whole person. For tailoring, craft and manufacturing this usually improves the shot, because the product becomes the subject.
58
65
  - Hands working (at a cuff, a button, the material) pass reliably and carry the craft story.
59
66
  - If the client insists on a face, generate the **static image** with it (image generation is more permissive) and reserve video for the face-free shots.
67
+ - **Video has no ZDR (zero data retention) on OpenRouter:** the provider may retain the frames. Nothing sensitive in them (documents, screens with client data, identifiable people).
60
68
 
61
69
  ---
62
70
 
@@ -64,8 +72,10 @@ Img2vid with a **human face** repeatedly fails moderation (error E005). The work
64
72
 
65
73
  Every generation costs money and you rarely nail the movement on the first attempt.
66
74
 
67
- 1. **Draft:** fast model, 480p, 4s. Enough to judge framing and movement.
68
- 2. **Final:** only after the draft is approved, regenerate at 1080p. Then re-extract frames and swap them into the build **with a cache-bust** (`?v=N`), or the browser serves the old ones. See `frontend/scroll-driven/frame-scrub.md`.
75
+ 1. **Draft:** the default model (`bytedance/seedance-1-5-pro`), 480p (the default), 4-5s. About US$0.05 per take; enough to judge framing and movement. Ambient loops can stay on this model.
76
+ 2. **Final:** only after the draft is approved, promote it: `generate_video { promoteFrom: "<draft id>", model: "seedance", resolution: "1080p" }` renders `bytedance/seedance-2.0` at 1080p with the **same prompt, frames and seed** (about US$1.50 for 4s). Then re-extract frames and swap them into the build **with a cache-bust** (`?v=N`), or the browser serves the old ones. See `frontend/scroll-driven/frame-scrub.md`.
77
+
78
+ To choose between models before drafting, `models: ["a", "b"]` runs the same request on 2-3 models with the cost of each. Any generation above US$2, over the project budget or with an unknown price returns `CONFIRMATION_REQUIRED` + `confirmToken`: get human approval, then repeat the same call with the token. `dryRun: true` only estimates.
69
79
 
70
80
  Never open at 1080p.
71
81
 
@@ -95,7 +105,11 @@ TECHNICAL: img2vid with image (start) + endImage (end). No audio. No watermark.
95
105
 
96
106
  ## Section 6: Async Contract
97
107
 
98
- `generate_video` and `generate_loop_video` return a `request_id` rather than a file. Poll with `get_video { requestId, outputPath? }`, which downloads the `.mp4` once the job reports COMPLETED. `list_videos { limit? }` reads local job history and spends no credits, which also makes it the safe health check for the MCP.
108
+ `generate_video` and `generate_loop_video` wait up to `waitSeconds` (default 60) and, if the job is still running, return "⏳ generating" with the job id rather than a file. Poll with `get_video { ids? }` (several ids at once; no ids = every pending job), which downloads the `.mp4` once the job completes. **Never resend `generate_video` while a job is running**: an identical request within 15 minutes returns the same job, but a different one is billed again.
109
+
110
+ `library { op: "pending" | "list" | "info" }` reads local history and spends no credits (it replaces the old `list_videos`). `usage { op: "balance" }` checks the OpenRouter key without spending credits, which makes it the safe health check for the MCP.
111
+
112
+ Post-production runs locally with no cost through `media_tool` (ffmpeg): `scrub_frames` (frames + `frames.json`, each re-extraction creates a new `v{N}` folder, which doubles as the cache-bust), `poster` (static image for mobile), `flip` (hflip when the subject collides with the copy), `concat`, `compress` (web background) and `picker` (the side-by-side approval page for the gate).
99
113
 
100
114
  ---
101
115
 
@@ -104,15 +118,16 @@ TECHNICAL: img2vid with image (start) + endImage (end). No audio. No watermark.
104
118
  - [ ] Directional footage (start differs from end) for anything scroll-driven
105
119
  - [ ] Seamless loop used only for a self-playing ambient plane
106
120
  - [ ] Client renders animated, never AI-recreated
107
- - [ ] Faces cropped out of frame before img2vid
121
+ - [ ] Faces cropped out of frame before img2vid; nothing sensitive in the frames (no ZDR)
122
+ - [ ] Scrub footage generated on a model that respects `endImage` (Seedance 1.5 Pro / 2.0, never `seedance-2.0-mini`) and the last-frame QA came back ✅
108
123
  - [ ] Movement chosen from the scrub-friendly list, no orbits or returns
109
124
  - [ ] Prompt reserves a calm region for copy, subject opposite the text
110
- - [ ] Draft at 480p/4s approved before any 1080p render
125
+ - [ ] Draft at 480p approved before any 1080p render, and the final made with `promoteFrom`
111
126
  - [ ] Frames re-extracted with a bumped cache-bust after the final render
112
127
 
113
128
  ## Boundary: Runtime Generation Is Another Standard
114
129
 
115
- This standard is **authoring time**: a human driving the seedance MCP in a session, footage that ends
130
+ This standard is **authoring time**: a human driving the morph-assets MCP in a session, footage that ends
116
131
  up committed as a page asset. When the **product itself** generates video at runtime — a .NET worker
117
132
  creating a paid task, polling an operation, downloading bytes into its own blob store — the standard
118
133
  is `ai-agents/media-video.md` (Veo via `Google.GenAI`, the HTTP-only providers, cost per second, the
@@ -116,8 +116,10 @@ happy path + one error path per component above is that budget applied to a comp
116
116
  - **Shared helpers by default.** Reuse the existing wrappers (`test/helpers.tsx`), MSW handlers and
117
117
  factories. Before writing a new one, look for it (`morph-spec graph explain <Name>`; fallback
118
118
  without a graph: `grep -rn`).
119
- - **TDD where it pays:** a bug gets its regression test first, and so does pure logic (formatters,
120
- reducers, schema). Component and integration tests may come after the code.
119
+ - **Order is free, proof is not the implementer's:** a bug gets a regression test, and pure logic
120
+ (formatters, reducers, schema) gets its tests, in whatever order the code comes out. The
121
+ implementer does not run red→green cycles nor mutate production code to watch a test fail; it
122
+ runs its own filter once before the report, and the Gate 3 mutation proof shows the test bites.
121
123
 
122
124
  ## Common Mistakes
123
125
 
@@ -80,37 +80,44 @@ const docs = await mcp__context7__query-docs({
80
80
 
81
81
  Fallback: WebSearch for component documentation, WebFetch for specific component API pages.
82
82
 
83
- ### Gemini Image MCP — AI Image Generation
83
+ ### morph-assets MCP — AI Image and Video Generation
84
84
 
85
85
  > Available in: **UI/UX** (preview generation), **Implement** (production assets)
86
86
 
87
+ One MCP for image, video and local post-production, all models through OpenRouter. Default image model `openai/gpt-image-2.5-flare`; default video model `bytedance/seedance-1-5-pro` (draft/loop), final `bytedance/seedance-2.0` 1080p via `promoteFrom`. `model` accepts any OpenRouter model by id or alias; `models: [...]` compares the same request across models.
88
+
87
89
  | Tool | Use Case | Example |
88
90
  |------|----------|---------|
89
- | `generate_image` | Create new image from text prompt | `mcp__gemini-image-mcp__generate_image({ prompt, aspectRatio, outputPath })` |
90
- | `edit_image` | Modify existing image | `mcp__gemini-image-mcp__edit_image({ imageSource, prompt })` |
91
- | `continue_editing` | Iterate on last generated/edited image | `mcp__gemini-image-mcp__continue_editing({ prompt })` |
92
- | `list_images` | Browse image generation history | `mcp__gemini-image-mcp__list_images({ type: "all" })` |
93
- | `get_image_info` | Get metadata about a specific image | `mcp__gemini-image-mcp__get_image_info({ imageId })` |
91
+ | `generate_image` | Create new image from text prompt | `mcp__morph-assets__generate_image({ prompt, aspectRatio, outputPath })` |
92
+ | `edit_image` | Modify existing image | `mcp__morph-assets__edit_image({ image, prompt, references })` |
93
+ | `continue_editing` | Iterate on last generated/edited image | `mcp__morph-assets__continue_editing({ prompt })` |
94
+ | `generate_video` / `generate_loop_video` | Directional footage (`image` + `endImage`) / ambient loop | `mcp__morph-assets__generate_video({ prompt, image, endImage })` |
95
+ | `get_video` | Poll one or more video jobs (never resend `generate_video`) | `mcp__morph-assets__get_video({ ids })` |
96
+ | `media_tool` | ffmpeg post-production: scrub frames, poster, concat, compress, approval picker | `mcp__morph-assets__media_tool({ op: "scrub_frames", input })` |
97
+ | `library` | History and metadata (replaces `list_images` / `get_image_info` / `list_videos`) | `mcp__morph-assets__library({ op: "list" })` |
98
+ | `list_models` / `usage` | Pick a model from the curation / spend and credit balance | `mcp__morph-assets__usage({ op: "balance" })` |
94
99
 
95
100
  Read `design-system.md` first to get the project colors and aesthetic direction. Keep edits short and targeted, and iterate with `continue_editing` one change at a time.
96
101
 
97
102
  ```javascript
98
- const image = await mcp__gemini-image-mcp__generate_image({
103
+ const image = await mcp__morph-assets__generate_image({
99
104
  prompt: "A floating tablet showing a modern dashboard interface on a dark charcoal background with copper and gold accent lighting. Premium luxury aesthetic, photorealistic, cinematic lighting with warm rim light.",
100
105
  aspectRatio: "16:9",
101
106
  outputPath: "public/images/my-feature/hero-mockup.png"
102
107
  });
103
108
 
104
- await mcp__gemini-image-mcp__edit_image({
105
- imageSource: "public/images/my-feature/hero-mockup.png",
109
+ await mcp__morph-assets__edit_image({
110
+ image: "public/images/my-feature/hero-mockup.png",
106
111
  prompt: "Make the lighting warmer and add a subtle copper glow around the tablet edges."
107
112
  });
108
113
 
109
- await mcp__gemini-image-mcp__continue_editing({
114
+ await mcp__morph-assets__continue_editing({
110
115
  prompt: "More contrast in the shadows."
111
116
  });
112
117
  ```
113
118
 
119
+ Paid per generation: above US$2, over the project budget or with an unknown price the tool returns `CONFIRMATION_REQUIRED` + `confirmToken` (get human approval, then repeat the same call with the token); `dryRun: true` only estimates.
120
+
114
121
  Fallback: manual image creation, stock photos, or placeholder divs.
115
122
 
116
123
  **Use cases by phase:**
@@ -121,7 +128,7 @@ Fallback: manual image creation, stock photos, or placeholder divs.
121
128
  | Implement (7) | Generate production assets from assets.md specs | `generate_image` + `edit_image` |
122
129
  | Implement (7) | Iterate on generated images for quality | `continue_editing` |
123
130
 
124
- > **Standard:** See `frontend/design-system/ai-image-generation.md` for prompt engineering patterns, aspect ratio guide, and `assets.md` format.
131
+ > **Standard:** See `frontend/design-system/ai-image-generation.md` for prompt engineering patterns, aspect ratio guide, and `assets.md` format, and `frontend/design-system/ai-video-generation.md` for directional footage and the video cost ladder.
125
132
 
126
133
  ---
127
134
 
@@ -271,7 +278,7 @@ PRs/issues: sempre `gh` CLI (Bash). Deployments fallback: `vercel` CLI.
271
278
  | Container ops | Docker CLI (`docker ps`, `docker logs`, `docker compose`) | — |
272
279
  | Cloud resources | Azure CLI (`az resource`, `az deployment`) | — |
273
280
  | Vercel deployments | Vercel MCP | Bash `vercel` CLI |
274
- | AI image generation | Gemini Image MCP | Manual creation or stock photos |
281
+ | AI image and video generation | morph-assets MCP | Manual creation or stock photos |
275
282
 
276
283
  **Rule:** MCP tools provide structured data (JSON responses). Native tools require manual parsing. **Always prefer MCP when available** — fall back to native when not. **Exceptions:** Neon, Docker, Azure **and GitHub** always use their respective CLIs directly — never MCP (`gh` já retorna JSON estruturado via `--json`, sem Docker nem PAT em env var).
277
284
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.40.0",
3
+ "version": "8.41.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,7 @@ 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 } from '../../utils/agents-installer.js';
19
+ import { checkMissingAgents, checkAgentsDrift, maxTurnsError, surfacesError, effortError } from '../../utils/agents-installer.js';
20
20
  import { getProjectTags } from '../../lib/stack-filter.js';
21
21
  import { auditGraph } from '../../lib/graph/graph-health.js';
22
22
  import { auditFeatureLifecycle } from '../../core/state/lifecycle-audit.js';
@@ -208,6 +208,14 @@ export function validateAgentsJson(agentsData) {
208
208
  ? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
209
209
  : { name: 'agents.json surfaces', status: 'ok' });
210
210
 
211
+ // 6: effort — same installer rule, same severity.
212
+ const effortErrors = agentEntries
213
+ .map(([id, data]) => effortError(id, data?.effort))
214
+ .filter(Boolean);
215
+ checks.push(effortErrors.length > 0
216
+ ? { name: 'agents.json effort', status: 'error', msg: effortErrors.join(' | ') }
217
+ : { name: 'agents.json effort', status: 'ok' });
218
+
211
219
  if (missingV5Fields.length > 0 && isV5) {
212
220
  const sample = missingV5Fields.slice(0, 3).join(', ');
213
221
  checks.push({
@@ -35,7 +35,7 @@
35
35
  */
36
36
 
37
37
  import { execSync, execFileSync } from 'child_process';
38
- import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, lstatSync, unlinkSync, rmdirSync, cpSync } from 'fs';
38
+ import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, lstatSync, unlinkSync, rmdirSync, cpSync, copyFileSync, readdirSync } from 'fs';
39
39
  import { join, resolve, dirname, basename } from 'path';
40
40
  import chalk from 'chalk';
41
41
  import { detectDefaultBranch } from '../../lib/git/default-branch.js';
@@ -612,6 +612,43 @@ export function buildTaskBranchName(name) {
612
612
  return `morph-task/${name}`;
613
613
  }
614
614
 
615
+ const ROOT_ENV_FILE_RE = /^\.env(\..+)?$/;
616
+
617
+ /**
618
+ * Copy the root's `.env` / `.env.*` into a new tree: a sub-agent that builds
619
+ * or runs the app needs the same secrets, and they are never committed. Only
620
+ * a REAL file (never a link — nothing in the tree may reach the root through
621
+ * one), only when the tree lacks it (never overwrite), and only when git
622
+ * IGNORES it there: an untracked-but-visible copy would dirty the tree, and
623
+ * `finish`/`worktree remove --if-clean` refuse a dirty tree. At teardown the
624
+ * copy is byte-identical to the root's, which `triageIgnoredPaths` classifies
625
+ * as REPLICABLE — deleted with nothing lost.
626
+ *
627
+ * @param {string} worktreePath
628
+ * @param {string} root
629
+ * @returns {string[]} names copied
630
+ */
631
+ export function copyRootEnvFiles(worktreePath, root) {
632
+ let names;
633
+ try {
634
+ names = readdirSync(root).filter((n) => ROOT_ENV_FILE_RE.test(n));
635
+ } catch {
636
+ return [];
637
+ }
638
+ const copied = [];
639
+ for (const name of names) {
640
+ const dst = join(worktreePath, name);
641
+ try {
642
+ if (!lstatSync(join(root, name)).isFile()) continue;
643
+ try { lstatSync(dst); continue; } catch { /* absent — not even a dangling link */ }
644
+ execFileSync('git', ['check-ignore', '-q', '--', name], { cwd: worktreePath, stdio: 'pipe' });
645
+ copyFileSync(join(root, name), dst);
646
+ copied.push(name);
647
+ } catch { /* not ignored, unreadable or unwritable — leave it out */ }
648
+ }
649
+ return copied;
650
+ }
651
+
615
652
  /**
616
653
  * Non-exiting core of `worktree provision` — the tree `claude -w`,
617
654
  * `EnterWorktree` and `Agent isolation:"worktree"` ask for when the name is
@@ -668,7 +705,11 @@ export async function runWorktreeProvision(name, options = {}) {
668
705
  execFileSync('git', ['worktree', 'add', worktreePath, branch], { cwd: root, stdio: 'pipe' });
669
706
 
670
707
  setupInfraLinks(worktreePath, root);
671
- const envBlock = await allocateWorktreeEnv(worktreePath, root, `task/${name}`);
708
+ const envFiles = copyRootEnvFiles(worktreePath, root);
709
+ // A tree born from `morph/<f>` works FOR that feature: MORPH_FEATURE says
710
+ // so, while the port block stays the task's own (`task/<name>`).
711
+ const forFeature = String(options.base || '').match(/^morph\/(.+)$/)?.[1] ?? null;
712
+ const envBlock = await allocateWorktreeEnv(worktreePath, root, `task/${name}`, { morphFeature: forFeature });
672
713
  const materialized = await ensureLocalState(worktreePath);
673
714
  const graph = seedWorktreeGraph(worktreePath, root);
674
715
 
@@ -681,6 +722,8 @@ export async function runWorktreeProvision(name, options = {}) {
681
722
  branchedFrom,
682
723
  kind: 'task-isolation',
683
724
  name,
725
+ ...(forFeature ? { feature: forFeature } : {}),
726
+ ...(envFiles.length ? { envFiles } : {}),
684
727
  materialized: materialized.materialized,
685
728
  ...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
686
729
  ...(graph.seeded ? { graph: 'seeded' } : {}),
@@ -797,12 +840,25 @@ export function envKeyForWorktree(worktreePath, root) {
797
840
  * Shared by `setup` (feature key), `provision` (task key) and `link --all`
798
841
  * (regeneration), so the manifest has exactly one writer.
799
842
  *
843
+ * `morphFeature` (task trees born from `morph/<f>`) only renames the
844
+ * `MORPH_FEATURE` line. Left undefined — the `link --all` regeneration, which
845
+ * derives the key from the branch and cannot see the base — the value the
846
+ * file already carries is kept, so a repair never forgets the feature.
847
+ *
800
848
  * @param {string} worktreePath
801
849
  * @param {string} root - primary root
802
850
  * @param {string} envKey - `<feature>` or `task/<name>`
851
+ * @param {{ morphFeature?: string|null }} [opts]
803
852
  * @returns {Promise<{ index: number, base: number, range: [number, number] }|null>}
804
853
  */
805
- export async function allocateWorktreeEnv(worktreePath, root, envKey) {
854
+ export async function allocateWorktreeEnv(worktreePath, root, envKey, { morphFeature } = {}) {
855
+ const envPath = join(worktreePath, '.morph', 'worktree.env');
856
+ if (morphFeature === undefined && envKey.startsWith('task/')) {
857
+ try {
858
+ const kept = readFileSync(envPath, 'utf8').match(/^MORPH_FEATURE=(.+)$/m)?.[1]?.trim();
859
+ morphFeature = kept && kept !== envKey ? kept : null;
860
+ } catch { morphFeature = null; }
861
+ }
806
862
  try {
807
863
  const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
808
864
  await import('../../lib/worktree/env-allocator.js');
@@ -816,7 +872,7 @@ export async function allocateWorktreeEnv(worktreePath, root, envKey) {
816
872
  stack = cfg?.project?.stack || '';
817
873
  } catch { /* fail-open — stackless env still carries PORT */ }
818
874
  mkdirSync(join(worktreePath, '.morph'), { recursive: true });
819
- writeFileSync(join(worktreePath, '.morph', 'worktree.env'), buildEnvFileContent(envKey, block, stack), 'utf8');
875
+ writeFileSync(envPath, buildEnvFileContent(envKey, block, stack, { morphFeature }), 'utf8');
820
876
  excludeWorktreePaths(root, [join('.morph', 'worktree.env')]);
821
877
  return block;
822
878
  } catch {
@@ -9,6 +9,7 @@ import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-de
9
9
  import { minimatch } from 'minimatch';
10
10
  import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
11
11
  import { readMorphConfig } from '../../lib/tasks/test-runner.js';
12
+ import { oversizedTasks } from '../../lib/tasks/tasks-json-parser.js';
12
13
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
13
14
 
14
15
  /**
@@ -196,6 +197,10 @@ function collectSignals(featureName, feature, cwd, gateName) {
196
197
  designSystemDivergence: false,
197
198
  };
198
199
 
200
+ // Fato de disco, fora de OVERRIDABLE_SIGNAL_KEYS: task que não cabe num
201
+ // dispatch se fatia no plano, não depois de um sub-agent bater no teto.
202
+ if (gateName === 'plan') signals.oversizedTasks = oversizedTasks({ tasks });
203
+
199
204
  // Só onde o checklist decide algo: no fecho do design e no ship. Fato de
200
205
  // disco, fora de OVERRIDABLE_SIGNAL_KEYS como `mutationProof`.
201
206
  if (gateName === 'uiux' || gateName === 'review') {