@polymorphism-tech/morph-spec 8.42.0 → 8.44.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 (101) hide show
  1. package/CLAUDE.md +15 -5
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +48 -2
  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 +5 -4
  8. package/framework/MORPH.md +10 -9
  9. package/framework/agents.json +1 -1
  10. package/framework/docs/CLI.md +22 -10
  11. package/framework/evals/proof-rubrics/mutation-proof.md +43 -13
  12. package/framework/hooks/claude-code/core/heavy-gate.js +72 -0
  13. package/framework/hooks/claude-code/core/heavy-release.js +34 -0
  14. package/framework/hooks/claude-code/core/worktree-context.js +21 -20
  15. package/framework/hooks/claude-code/core/worktree-create.js +4 -4
  16. package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
  17. package/framework/hooks/shared/agent-reap.js +89 -0
  18. package/framework/hooks/shared/e2e-check.js +3 -1
  19. package/framework/hooks/shared/graph-refresh.js +1 -9
  20. package/framework/hooks/shared/heavy-command.js +128 -0
  21. package/framework/hooks/shared/heavy-config.js +65 -0
  22. package/framework/hooks/shared/heavy-holders.js +85 -0
  23. package/framework/hooks/shared/heavy-slots.js +207 -0
  24. package/framework/hooks/shared/process-alive.js +8 -0
  25. package/framework/hooks/shared/worktree-root.js +93 -0
  26. package/framework/rules/ai-agents-standards.md +1 -0
  27. package/framework/schemas/config.schema.json +80 -0
  28. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  29. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  30. package/framework/skills/core/morph-apply/SKILL.md +8 -3
  31. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  32. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  33. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  34. package/framework/skills/core/morph-implement/SKILL.md +9 -4
  35. package/framework/skills/core/morph-machine/SKILL.md +46 -0
  36. package/framework/skills/core/morph-media/SKILL.md +1 -1
  37. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  38. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  39. package/framework/skills/core/morph-review/SKILL.md +45 -16
  40. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  41. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  42. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  43. package/framework/standards/PROVENANCE.json +10 -10
  44. package/framework/standards/STANDARDS.json +45 -1
  45. package/framework/standards/ai-agents/tool-adapters.md +226 -0
  46. package/framework/standards/frontend/design-system/ai-video-generation.md +2 -0
  47. package/framework/standards/frontend/scroll-driven/frame-scrub.md +11 -1
  48. package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
  49. package/package.json +1 -1
  50. package/src/commands/e2e/e2e.js +27 -8
  51. package/src/commands/mcp/mcp-setup.js +10 -9
  52. package/src/commands/project/doctor-procs.js +162 -0
  53. package/src/commands/project/doctor.js +42 -4
  54. package/src/commands/project/heavy.js +61 -0
  55. package/src/commands/project/init.js +1 -1
  56. package/src/commands/project/update.js +4 -0
  57. package/src/commands/project/worktree-reap.js +34 -0
  58. package/src/commands/project/worktree.js +112 -43
  59. package/src/commands/state/approve.js +2 -1
  60. package/src/commands/state/finish.js +4 -0
  61. package/src/commands/state/gate-check.js +2 -1
  62. package/src/commands/validation/mutate.js +200 -0
  63. package/src/commands/validation/verify.js +39 -22
  64. package/src/dashboard/scanner/features-scanner.js +4 -4
  65. package/src/dashboard/scanner/project-api.js +4 -4
  66. package/src/lib/detectors/claude-config-detector.js +30 -1
  67. package/src/lib/e2e/compose-env.js +5 -2
  68. package/src/lib/git/evidence-staleness.js +39 -0
  69. package/src/lib/heavy/cleanup.js +70 -0
  70. package/src/lib/heavy/gate.js +96 -0
  71. package/src/lib/installers/mcp-installer.js +15 -2
  72. package/src/lib/mutation/adapters.js +90 -0
  73. package/src/lib/mutation/commands.js +60 -0
  74. package/src/lib/mutation/manifest.js +138 -0
  75. package/src/lib/mutation/plan.js +36 -0
  76. package/src/lib/mutation/report.js +66 -0
  77. package/src/lib/mutation/runner.js +351 -0
  78. package/src/lib/mutation/schemata.js +60 -0
  79. package/src/lib/mutation/snapshot.js +69 -0
  80. package/src/lib/mutation/subset.js +75 -0
  81. package/src/lib/mutation/trx.js +51 -0
  82. package/src/lib/procs/classify.js +91 -0
  83. package/src/lib/procs/list.js +124 -0
  84. package/src/lib/procs/mcp-config.js +103 -0
  85. package/src/lib/procs/prune.js +25 -0
  86. package/src/lib/procs/wslconfig.js +70 -0
  87. package/src/lib/projects/registry.js +6 -2
  88. package/src/lib/risk-detector.js +3 -3
  89. package/src/lib/tasks/eval-node.js +4 -34
  90. package/src/lib/tasks/linux-runner.js +30 -3
  91. package/src/lib/tasks/test-runner.js +1 -1
  92. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
  93. package/src/lib/validators/verify-runner.js +26 -8
  94. package/src/lib/worktree/agent-reaper.js +247 -0
  95. package/src/lib/worktree/dirs.js +3 -32
  96. package/src/lib/worktree/env-allocator.js +35 -5
  97. package/src/lib/worktree/vscode-settings.js +113 -0
  98. package/src/lib/worktree/worktree-audit.js +49 -5
  99. package/src/lib/worktree/worktree-include.js +46 -0
  100. package/src/scripts/setup-infra.js +4 -0
  101. package/src/utils/hooks-installer.js +11 -4
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-review
@@ -63,7 +63,7 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
63
63
  rodada.
64
64
 
65
65
  **Despache o avaliador independente** — o score do Gate 3 nunca é auto-atribuído por quem implementou:
66
- - Rode `morph-spec verify {feature} --json` (escopo feature) e capture o JSON — é a saída real de build+testes+validadores+e2e que você anexa ao prompt do avaliador independente (`morph-evaluator`). **Sem `--filter`, sempre:** o `--filter` é do loop por task, onde o relógio de uma task não paga a suíte inteira; a prova que o avaliador lê tem de cobrir **todos** os nós de `nodes.tests.runs[]` (uma suíte filtrada aparece como `skip` nomeando o filtro — Gate 3 aberto sobre isso é verde sobre código não testado). Se a suíte inteira não couber numa chamada, rode-a em background; não a encolha com `--filter`. `verify {feature}` (sem task) **nunca recorta** — o recorte automático é só do escopo de task —, e um `verify {feature} --filter …` nem persiste o stamp (`persisted: false`, `reason: 'filtered'`) — o mesmo vale para `--skip-tests`/`--skip-build` no escopo de feature. Rode-o **depois** da última task: o `gate-check {feature} review` pausa quando o último stamp `verification` é de escopo `task:{id}`, porque aquela rodada cobriu só a task. Uma rodada por árvore: se o JSON vier com `repeatOf`, nada mudou fora de `.morph/` desde o verify de feature anterior — use aquele resultado em vez de rodar de novo; depois de uma correção a árvore mudou, e a nova rodada é a prova.
66
+ - Rode `morph-spec verify {feature} --json` (escopo feature; exit 3 / `status: "busy"` é fila de slot pesado cheia, não vermelho: repita a chamada) e capture o JSON — é a saída real de build+testes+validadores+e2e que você anexa ao prompt do avaliador independente (`morph-evaluator`). **Sem `--filter`, sempre:** o `--filter` é do loop por task, onde o relógio de uma task não paga a suíte inteira; a prova que o avaliador lê tem de cobrir **todos** os nós de `nodes.tests.runs[]` (uma suíte filtrada aparece como `skip` nomeando o filtro — Gate 3 aberto sobre isso é verde sobre código não testado). Se a suíte inteira não couber numa chamada, rode-a em background; não a encolha com `--filter`. `verify {feature}` (sem task) **nunca recorta** — o recorte automático é só do escopo de task —, e um `verify {feature} --filter …` nem persiste o stamp (`persisted: false`, `reason: 'filtered'`) — o mesmo vale para `--skip-tests`/`--skip-build` no escopo de feature. Rode-o **depois** da última task: o `gate-check {feature} review` pausa quando o último stamp `verification` é de escopo `task:{id}`, porque aquela rodada cobriu só a task. Uma rodada por árvore: se o JSON vier com `repeatOf`, nada mudou fora de `.morph/` desde o verify de feature anterior — use aquele resultado em vez de rodar de novo; depois de uma correção a árvore mudou, e a nova rodada é a prova.
67
67
  - **Confira o nó `e2e` do verify antes de despachar.** Ele carrega o estado do stack:
68
68
  - `pass` → a aplicação subiu e está saudável; o avaliador pode navegá-la. **Passe a URL no prompt**
69
69
  — cada feature roda no seu bloco de portas, não em `localhost:3000`; pegue de
@@ -174,21 +174,48 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
174
174
  MUTAÇÃO — o método, e a ordem dele importa:
175
175
  - COMMITE O FIX ANTES DE MUTAR. Se a sessão morrer no meio, o
176
176
  que sobrevive é a mutação, não a correção.
177
- - Ancore no CALL SITE que carrega a decisão, não no `if`
178
- refatorado. Uma rodada POR CLÁUSULA de um predicado composto.
179
- - Rode a suíte INTEIRA (sem filtro): só ela autoriza a frase
180
- "derruba N de M". Filtro estreito é prova vácua.
177
+ - VOCÊ ESCOLHE os mutantes: ancore no CALL SITE que carrega a
178
+ decisão, não no `if` refatorado. Uma rodada POR CLÁUSULA de um
179
+ predicado composto — cada cláusula é um mutante próprio.
180
+ - ESCREVA `.morph/features/{feature}/4-review/mutants.json`:
181
+ {"mutants":[{"id":"M1","file":"src/x.cs","line":42,
182
+ "find":"a && b","replace":"a"}]}. `find` começa na `line` e é
183
+ único nela; arquivo:linha, diff e md5 saem da ferramenta.
184
+ Com a ferramenta Write, se você a tem; só com Bash, heredoc
185
+ de delimitador entre aspas simples. O transporte do Bash
186
+ reduz `\\` a `\` e decodifica escapes unicode, então escreva
187
+ cada barra invertida do código como `{{BS}}` (a ferramenta a
188
+ expande): atravessa qualquer transporte. `find` que deixa de
189
+ bater com o arquivo é recusado com o erro, nunca aplicado.
190
+ - RODE `morph-spec mutate {feature}` UMA vez, com o Bash. Ela
191
+ faz o lote inteiro: um build só (chaves por variável de
192
+ ambiente; o que a chave não compila cai para rebuild real),
193
+ subconjunto de testes primeiro — escolhido por ela, não por
194
+ você — e, para CADA sobrevivente, o mutante compilado de
195
+ verdade contra a suíte INTEIRA. Só esse caminho produz
196
+ "sobreviveu"; "derruba K de M" sempre diz o escopo do K
197
+ (≥K no subconjunto, K na suíte inteira); na suíte inteira a
198
+ falha tem de se repetir, senão é instabilidade e o mutante
199
+ sobrevive. Sem a ferramenta,
200
+ mute e rode a suíte INTEIRA à mão, um mutante por vez.
181
201
  - LEIA O MOTIVO da falha, não só o md5: md5 prova que ALGO
182
- mudou, jamais O QUÊ.
183
- - REVERTA por snapshot/restore de BYTES (cópia feita ANTES) +
184
- `os.utime` para devolver o mtime, e confirme por md5. NUNCA
202
+ mudou, jamais O QUÊ. Falha de build não é morte, e
203
+ `inconclusive` não conta como nada.
204
+ - A REVERSÃO é da ferramenta: bytes do snapshot com md5
205
+ conferido, por escrita NOVA, rebuild forçado e a DLL final
206
+ igual (md5) à construída antes do 1º mutante. NUNCA `os.utime`
207
+ para devolver o mtime: o MSBuild acha o binário do último
208
+ mutante em dia e a rodada-base roda sobre ele. NUNCA
185
209
  `git checkout --`: ele descarta trabalho não commitado de
186
210
  outra pessoa — aconteceu de verdade num Gate 3 desta epic.
187
- - TERMINE com `git status --porcelain src/` VAZIO. Mutação
188
- esquecida em produção é o pior desfecho deste método.
189
- Grave no relatório a seção `## Mutações`, uma linha por mutação,
190
- com os CINCO campos: arquivo:linha | diff | md5 antes/depois |
191
- quantos testes derrubou (sobre o total da suíte) | o MOTIVO
211
+ Rodada que morreu no meio: `morph-spec mutate --restore`.
212
+ - TERMINE com `git status --porcelain src/` VAZIO e exit 0. Exit
213
+ 4 é inseguro (arquivo não restaurado, árvore ou binário
214
+ diferentes): resolva antes de pontuar.
215
+ Cole no relatório a seção `## Mutações` que a ferramenta imprime
216
+ (ela também a grava em `4-review/mutation-section.md`), uma linha
217
+ por mutação, com os CINCO campos: arquivo:linha | diff | md5
218
+ antes/depois | quantos testes derrubou (com o escopo) | o MOTIVO
192
219
  literal da falha. Feature sem código de produção declara a seção
193
220
  com a linha `não aplicável — feature sem código de produção`. A
194
221
  seção é OBRIGATÓRIA: sem ela o `morph-spec gate-check` PAUSA o
@@ -294,6 +321,7 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
294
321
  > diff antes). É a mesma proibição que o prompt do passo 4 já dá ao avaliador: aqui ela vale
295
322
  > para VOCÊ.
296
323
  - **Leia primeiro** `.morph/features/{feature}/4-review/evaluator-report.md` — é o valor de retorno autoritativo, não a mensagem de texto do dispatch (que é cortesia, útil só quando chega inteira). Os scores por dimensão e o composto do review-report vêm **desse arquivo**, não da sua auto-avaliação. Se o arquivo vier ausente, oco, ou sem os scores por dimensão, registre a falha de protocolo na seção de método do review-report e re-despache.
324
+ - Com o relatório lido, rode `morph-spec worktree reap --json`: a árvore do avaliador que parou limpa sai sozinha; a que ficou com trabalho vem listada com a razão.
297
325
  - **`PARCIAL` na 1ª linha = o avaliador parou antes do fim** (limite de turnos; o `report-check` devolve `evaluator.partial: true`). Não leia score dele — o parcial não tem `**Composite-Score:**`, e o `gate-check` pausaria de qualquer jeito — e não re-despache frio: um avaliador novo refaz a rodada inteira (probes, mutações, navegação). Retome o MESMO avaliador por `SendMessage`, como no passo 6, colando `tree.dirty` e `tree.commits` do `report-check` no recado; ele completa o relatório, e o final substitui o parcial. Só se o id não retomar, despache um novo pelo caminho normal — o parcial no disco é resíduo, não insumo, como o prompt já diz. Antes de qualquer das duas, a higiene da árvore acima continua valendo: um avaliador cortado no meio pode ter deixado mutação no disco.
298
326
 
299
327
  5. Produza `review-report.md` em `.morph/features/{feature}/4-review/` com:
@@ -318,9 +346,10 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
318
346
 
319
347
  | # | Arquivo:linha | Mutação (diff) | md5 antes | md5 depois | Testes derrubados | Motivo da falha |
320
348
  |---|---|---|---|---|---|---|
321
- | M1 | src/lib/x.js:42 | `- if (a && b)` / `+ if (a)` | abc… | def… | 3 de 2684 | `expected 3 got 0` em x.test.js:88 |
349
+ | M1 | src/lib/x.js:42 | `- if (a && b)` / `+ if (a)` | abc… | def… | ≥3 de 212 (subconjunto) | `expected 3 got 0` em x.test.js:88 |
350
+ | M2 | src/lib/y.js:7 | `- return ok` / `+ return true` | 123… | 456… | 0 de 2684 (SOBREVIVEU, suíte inteira) | nenhum teste falhou na suíte inteira |
322
351
 
323
- **Reversão:** snapshot/restore de bytes + `os.utime`; `git status --porcelain src/` vazio.
352
+ **Reversão:** snapshot/restore de bytes por escrita nova; DLL final igual (md5); `git status --porcelain src/` vazio.
324
353
  **Proof-Scores:** mutation=9 verde=8 fiacao=9 guard=7
325
354
  ```
326
355
 
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
4
4
  version: core
5
5
  when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-stack-scan
@@ -4,7 +4,7 @@ description: Localiza um standard específico via busca server-side (morph-spec
4
4
  version: core
5
5
  when-to-invoke: quando precisar consultar um standard específico durante a implementação
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-standards
@@ -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.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-uiux
@@ -8,15 +8,15 @@
8
8
  "path": "skills/morph-lp-cinematic",
9
9
  "envVar": "MORPH_HUB",
10
10
  "defaultCheckout": "R:/Polymorphism Tech/repos/morph-HUB",
11
- "sourceTree": "e43114d4b28ff55680a5355cce27d31ffa74f22f",
12
- "sourceCommit": "465c9b0",
11
+ "sourceTree": "2987b44241cae479b1aedc63f689cdcd6ef6fb94",
12
+ "sourceCommit": "c4c9164",
13
13
  "distilledOn": "2026-08-21",
14
14
  "distilledFrom": [
15
15
  "references/build-stack.md",
16
16
  "references/component-recipes.md",
17
17
  "references/premium-finish.md",
18
18
  "references/seo-and-qa.md",
19
- "references/gemini-image-prompt.md",
19
+ "references/image-prompt.md",
20
20
  "references/seedance-scrub-prompt.md",
21
21
  "references/seedance-loop-prompt.md"
22
22
  ],
@@ -25,7 +25,7 @@
25
25
  "references/copy-and-messaging.md",
26
26
  "references/section-blueprint.md"
27
27
  ],
28
- "notDistilledReason": "Deliberate split: a standard carries knowledge, a skill carries process. Deploy and copy strategy stay with the skill. `references/gates.md` LEFT this list on 2026-09-09: the reason that kept it out was \"a skill carries process\", and the destination is now a skill — see the \"cinematic-lp\" source below, which distils it into morph-media.",
28
+ "notDistilledReason": "Deliberate split: a standard carries knowledge, a skill carries process. Deploy and copy strategy stay with the skill. `references/gates.md` LEFT this list on 2026-09-09: the reason that kept it out was \"a skill carries process\", and the destination is now a skill — see the \"cinematic-lp\" source below, which distils it into morph-media. Review of 2026-09-29 (source c4c9164): only tool-independent facts were ported (the scrub_frames manifest contract for frame-scrub, chained clips for ai-video-generation); the rest was already in the standards or stays with the skill (ffmpeg-to-media_tool swaps, model prices, SKILL.md, gates.md, assets/).",
29
29
  "standards": [
30
30
  "frontend-scroll-driven-frame-scrub",
31
31
  "frontend-scroll-driven-smooth-scroll",
@@ -65,8 +65,8 @@
65
65
  "path": "skills/cinematic-lp/references",
66
66
  "envVar": "MORPH_HUB",
67
67
  "defaultCheckout": "R:/Polymorphism Tech/repos/morph-HUB",
68
- "sourceTree": "57a1734bc17651a6fed381a6e817c8b0c0a89edd",
69
- "sourceCommit": "b6bbfff",
68
+ "sourceTree": "3c557425d649ed477bf6645b9297b6d1d72ae4a3",
69
+ "sourceCommit": "c4c9164",
70
70
  "distilledOn": "2026-09-09",
71
71
  "distilledFrom": [
72
72
  "gates.md"
@@ -85,7 +85,7 @@
85
85
  "video-loop-prompt.md",
86
86
  "video-scrub-prompt.md"
87
87
  ],
88
- "notDistilledReason": "Only the HITL gate mechanics were distilled: the N-option picker, \"rejection goes back to the same step\", and \"show MOVEMENT, never a still\". The rest is either already a standard (the frame-scrub / smooth-scroll / premium-finish family, from the morph-lp-cinematic source above) or build/deploy strategy that belongs to the calling workflow.",
88
+ "notDistilledReason": "Only the HITL gate mechanics were distilled: the N-option picker, \"rejection goes back to the same step\", and \"show MOVEMENT, never a still\". The rest is either already a standard (the frame-scrub / smooth-scroll / premium-finish family, from the morph-lp-cinematic source above) or build/deploy strategy that belongs to the calling workflow. Review of 2026-09-29 (source c4c9164): gates.md did not change; the changes were in image-prompt.md and mcp-setup.md, both not distilled. Re-pinned only.",
89
89
  "standards": [],
90
90
  "skills": [
91
91
  "morph-media"
@@ -97,8 +97,8 @@
97
97
  "path": "skills/brand-design/references",
98
98
  "envVar": "MORPH_HUB",
99
99
  "defaultCheckout": "R:/Polymorphism Tech/repos/morph-HUB",
100
- "sourceTree": "d8801b8c277f63b06e1a22f57563ec468e7d4d9a",
101
- "sourceCommit": "b6bbfff",
100
+ "sourceTree": "7b637a08e931d8b7ba5be7fc75b851d89cd6e6f4",
101
+ "sourceCommit": "c4c9164",
102
102
  "distilledOn": "2026-09-09",
103
103
  "distilledFrom": [
104
104
  "image-generation.md"
@@ -109,7 +109,7 @@
109
109
  "example-identidade-visual.md",
110
110
  "phases.md"
111
111
  ],
112
- "notDistilledReason": "Only the image-per-phase map and the \"concept, never the final vector\" rule were distilled. The narrative prompt recipes were NOT copied verbatim: they are bound to one brand pipeline, and morph-media carries the prompt STRUCTURE instead, which survives the model changing. Brand book, checklist and phases are the process of another skill.",
112
+ "notDistilledReason": "Only the image-per-phase map and the \"concept, never the final vector\" rule were distilled. The narrative prompt recipes were NOT copied verbatim: they are bound to one brand pipeline, and morph-media carries the prompt STRUCTURE instead, which survives the model changing. Brand book, checklist and phases are the process of another skill. Review of 2026-09-29 (source c4c9164): the image-per-phase table and the \"concept, never the final vector\" rule are unchanged (the text now says \"image model\" instead of Gemini); what was added is morph-assets call mechanics (project_set, @brand, outputPath, models, variants, dryRun), tool-bound and kept out on purpose. Re-pinned only.",
113
113
  "standards": [],
114
114
  "skills": [
115
115
  "morph-media"
@@ -2566,15 +2566,18 @@
2566
2566
  "POSTGRES_PORT",
2567
2567
  "COMPOSE_PROJECT_NAME",
2568
2568
  "port block",
2569
+ "PG_PORT",
2570
+ "portEnvNames",
2569
2571
  "local docker compose"
2570
2572
  ],
2571
2573
  "anchors": {
2572
2574
  "layout": "#port-layout-one-contract-everywhere",
2575
+ "portNames": "#project-named-port-variables",
2573
2576
  "rules": "#core-rules",
2574
2577
  "enforcement": "#what-the-harness-enforces",
2575
2578
  "skeleton": "#minimal-compose-skeleton"
2576
2579
  },
2577
- "digest": "Every worktree runs its OWN compose stack (project morph-{feature}, own postgres, own volumes). Ports come from the worktree block: PORT +0, API_PORT +1 (ASPNETCORE_URLS on .NET), POSTGRES_PORT +2, REDIS_PORT +3 — publish via ${VAR:-default}:container, never a fixed host port, never container_name (not prefixed by the project → collides). Stable POSTGRES_DB, healthcheck on every service, DATABASE_URL with an in-network default, playwright.config reads PLAYWRIGHT_BASE_URL. A non-isolable compose fails the verify e2e node inside a worktree. finish tears down with volumes (--keep-volumes opts out). Cloud DB credentials go in e2e.auth.envFile, never in .morph/worktree.env.",
2580
+ "digest": "Every worktree runs its OWN compose stack (project morph-{feature}, own postgres, own volumes). Ports come from the worktree block: PORT +0, API_PORT +1 (ASPNETCORE_URLS on .NET), POSTGRES_PORT +2, REDIS_PORT +3 — publish via ${VAR:-default}:container, never a fixed host port, never container_name (not prefixed by the project → collides). Stable POSTGRES_DB, healthcheck on every service, DATABASE_URL with an in-network default, playwright.config reads PLAYWRIGHT_BASE_URL. A non-isolable compose fails the verify e2e node inside a worktree. finish tears down with volumes (--keep-volumes opts out). Cloud DB credentials go in e2e.auth.envFile, never in .morph/worktree.env. A compose that reads a port by ANOTHER name (PG_PORT) is not fed by the block: declare it in config.json e2e.portEnvNames ({\"postgres\": \"PG_PORT\"}) and the harness exports it with the same value in worktree.env, the compose env and the hook.",
2578
2581
  "verifiedAgainst": "Docker Compose v2 + morph-spec 8.34 (dedicated stack per worktree).",
2579
2582
  "provedBy": []
2580
2583
  },
@@ -2851,6 +2854,47 @@
2851
2854
  "digest": "Default is NO comment: code explains itself through names, types and tests. Before commenting, rename, extract a function or named constant, encode the state in the type, or write a test; comment only what none of these can express: an invariant the type does not express, an order that matters, an external quirk (third-party API, library bug with its link), or the contract of a package published for consumers outside the repo. Never process history (task, UC, ADR, FR, Gate, evaluator, spec.md, mockup: commit message and decisions.md; in a test the link to the postcondition goes in the test NAME), never restate the code, no XML doc/JSDoc on application members that repeats the signature, no file headers, section banners, #region, commented-out code or loose TODOs, no claims about behavior (make them test assertions). One line is normal, ~3 is the ceiling; more than one comment line per ten code lines means the names are not doing the work. A heavily commented neighbour file is not a model. The comment-guard hook blocks an edit that ADDS process history, a banner/#region, a block over 3 lines or a doc-comment outside a published package; the verify validators node (code-budget) warns on added comments that cite process and on added comment density.",
2852
2855
  "verifiedAgainst": "C#, TypeScript e JavaScript (convenção de linguagem, sem pacote externo).",
2853
2856
  "provedBy": []
2857
+ },
2858
+ {
2859
+ "id": "ai-agents-tool-adapters",
2860
+ "name": "tool adapters",
2861
+ "path": "ai-agents/tool-adapters.md",
2862
+ "category": "ai-agents",
2863
+ "subcategory": null,
2864
+ "tags": [
2865
+ "ai-agents",
2866
+ "maf",
2867
+ "tools",
2868
+ "testing"
2869
+ ],
2870
+ "stacks": [
2871
+ "dotnet"
2872
+ ],
2873
+ "aliases": [
2874
+ "tool fina",
2875
+ "adaptador de tool",
2876
+ "Ops",
2877
+ "Desfecho",
2878
+ "Render",
2879
+ "regra de negocio em tool",
2880
+ "descricao da tool",
2881
+ "golden de tool",
2882
+ "refactor neutro",
2883
+ "migracao de teste"
2884
+ ],
2885
+ "anchors": {
2886
+ "fronteira": "#fronteira-com-os-outros-standards",
2887
+ "problema": "#o-problema",
2888
+ "padrao": "#o-padrão",
2889
+ "exemplo": "#exemplo-c-completo",
2890
+ "testes": "#os-testes-das-duas-camadas",
2891
+ "migracao": "#migrar-uma-tool-que-já-tem-a-regra-no-adaptador",
2892
+ "reais": "#exemplos-reais",
2893
+ "regras": "#regras"
2894
+ },
2895
+ "digest": "A tool that carries business rule keeps apart three things that change for different reasons: the RULE (sealed class XOps(...), primary constructor, explicit AddScoped; never *Service, which the mandate bans as a layer between handler and data), the sentence the MODEL reads, and the EFFECT a human sees. Each Ops method returns a typed outcome (abstract record XDesfecho, one sealed record per case); the model-facing text (description, refusal, self-correction, success, the ToolUnavailable decision) lives in the adapter, in public static object Render(XDesfecho), while human text delivered by code (task body, note, ack) stays in Ops because it is an effect. Checkable by grep: Ops names no tool and instructs no model. The adapter (AIFunctionFactory.Create) only builds the typed request, calls Ops and returns Render: zero rule, not a line count, and a long dynamic description stays byte for byte. Two test layers: Ops tests assert outcome, calls and order on the fake, never text; contract tests assert Render's exact literal and .Description, with a golden of name, description and schema per real tenant. A neutral refactor is proven before any test moves: a proof commit with the old suite untouched and green (offline evals included), then the migration, with mutation run only against the new tests. \"Already covered\" needs proof assertion by assertion (11 of 22 such claims had lost one); the tests left at the origin must be identical to the base; mutation on the new tests found rules the old suite never protected. The C# blocks were compiled against the pin and the three xunit tests run.",
2896
+ "verifiedAgainst": "Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08); os dois blocos C# abaixo foram extraídos com `scripts/extract-doc-csharp.mjs` e compilados contra o pin com `TreatWarningsAsErrors`, e os três testes do segundo bloco foram executados e passam. **A separação em si e a migração de teste são verificação documental:** vêm da feature `tools-finas` do MORPH_GHLBrain e o ai-kit não as traz.",
2897
+ "provedBy": []
2854
2898
  }
2855
2899
  ],
2856
2900
  "sharedAliases": {
@@ -0,0 +1,226 @@
1
+ # Tool Adapters — a regra em `*Ops`, o texto do modelo no adaptador
2
+
3
+ > **Scope:** stacks=["dotnet"]
4
+ > **Layer:** 2 (on-keyword)
5
+ > **Keywords:** tool, AIFunctionFactory, adaptador de tool, tool fina, Ops, Desfecho, Render, regra de negócio em tool, descrição da tool, golden de tool, refactor neutro, migração de teste
6
+ > **Read by Claude in:** implement (ao escrever ou migrar uma tool de agente que carrega regra de negócio)
7
+
8
+ **Verified against:** Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08); os dois blocos C# abaixo foram extraídos com `scripts/extract-doc-csharp.mjs` e compilados contra o pin com `TreatWarningsAsErrors`, e os três testes do segundo bloco foram executados e passam. **A separação em si e a migração de teste são verificação documental:** vêm da feature `tools-finas` do MORPH_GHLBrain e o ai-kit não as traz. Last-verified: 2026-09-08.
9
+
10
+ ---
11
+
12
+ ## Fronteira com os outros standards
13
+
14
+ `ai-agents-mcp-tools` cobre **consumir** tools de um MCP server. `ai-agents-testing-ai` cobre **testar** um agente sem rede. `backend-dotnet-testing` é dono de xUnit, relógio falso e orçamento de teste. **Nada disso se repete aqui.**
15
+
16
+ Este standard cobre uma coisa só: a tool que o agente chama **e que carrega regra**. Onde mora a regra, onde mora a frase que o modelo lê, e como se testa cada uma sem que uma mascare a outra.
17
+
18
+ ## O problema
19
+
20
+ Uma tool nasce como uma lambda dentro do `AIFunctionFactory.Create`. Enquanto é só "chame a API e devolva", serve. Quando ganha regra, a lambda passa a misturar três coisas que mudam por motivos diferentes:
21
+
22
+ | O que é | Muda quando | Quem lê |
23
+ |---|---|---|
24
+ | a **regra** (validar, decidir, gravar) | o negócio muda | o código e o teste |
25
+ | a **frase para o modelo** (descrição, recusa, instrução de correção) | o prompt é ajustado | o modelo |
26
+ | o **efeito** (task, nota, mensagem entregue por código) | o processo muda | um humano |
27
+
28
+ Misturadas, o único teste possível é por string: qualquer ajuste de redação quebra o teste da regra, e qualquer refactor da regra arrisca mudar, sem ninguém ver, o que o modelo lê.
29
+
30
+ ## O padrão
31
+
32
+ ```
33
+ o modelo chama o modelo lê
34
+ ┌──────────────┐ pedido tipado ┌───────────┐ desfecho ┌────────────────┐
35
+ │ Adaptador │ ──────────────────► │ *Ops │ ─────────► │ Render(desfecho)│
36
+ │ (tool fina) │ │ (a regra) │ tipado │ (frase p/ modelo)│
37
+ └──────────────┘ ◄───────────────────────────────────────────└────────────────┘
38
+ ```
39
+
40
+ 1. **`Ops`, não `Service`.** Regra determinística com dependências injetadas vira `sealed class XOps(...)`, com construtor primário e um `AddScoped` explícito. `*Service` colide com a proibição de camada de Service/Manager entre handler e dados; `*Ops` é outra coisa: a regra de uma tool, não um intermediário de persistência.
41
+ 2. **Desfecho tipado, e a fronteira é quem lê o texto.** Cada método de `Ops` devolve um `abstract record XDesfecho` com um `sealed record` por caso. A frase para o **modelo** (descrição, recusa, instrução de autocorreção, sucesso, a decisão de "tool indisponível") mora no adaptador, em `public static object Render(XDesfecho)`. O texto para **humano** entregue por código (corpo de task, nota, ack) mora na `Ops`, porque é efeito. É verificável por `grep`: a `Ops` não cita o nome da tool nem dá instrução ao modelo.
42
+ 3. **Adaptador fino.** `AIFunctionFactory.Create` com a lambda montando o pedido tipado, chamando a `Ops` e devolvendo `Render(desfecho)`. A régua é **zero regra no adaptador**, não um número de linhas: uma descrição dinâmica pode ser longa e tem de continuar byte a byte.
43
+ 4. **Testes em duas camadas.** O teste da `Ops` asserta desfecho, chamadas e ordem no fake, **nunca texto**. O teste de contrato asserta o `Render` com o literal exato e a `.Description`. Um golden de nome, descrição e schema, por tenant real, protege a superfície que o modelo lê.
44
+ 5. **Refactor neutro se prova antes de migrar teste.** Primeiro um commit de prova, com a suíte antiga intocada e verde (inclusive os evals offline); só depois a migração dos testes, com mutação rodando **só** contra os testes novos.
45
+
46
+ ## Exemplo C# completo
47
+
48
+ A regra, o desfecho, o adaptador e o registro. O pedido é tipado, então a lambda não decide nada: monta, chama, renderiza.
49
+
50
+ ```csharp
51
+ using System.ComponentModel;
52
+ using Microsoft.Extensions.AI;
53
+ using Microsoft.Extensions.DependencyInjection;
54
+
55
+ namespace Exemplo.Tools;
56
+
57
+ public interface IAgendaExterna
58
+ {
59
+ Task<int> CapacidadeAsync(string salaId, CancellationToken ct);
60
+ Task<string> GravarAsync(ReservaPedido pedido, CancellationToken ct);
61
+ }
62
+
63
+ public sealed record ReservaPedido(string SalaId, DateOnly Dia, int Pessoas);
64
+
65
+ public abstract record ReservaDesfecho
66
+ {
67
+ public sealed record Reservada(string ConfirmacaoId) : ReservaDesfecho;
68
+ public sealed record SalaCheia(int Capacidade) : ReservaDesfecho;
69
+ public sealed record DiaSemExpediente(DateOnly Dia) : ReservaDesfecho;
70
+ }
71
+
72
+ public sealed class ReservaOps(IAgendaExterna agenda)
73
+ {
74
+ public async Task<ReservaDesfecho> ReservarAsync(ReservaPedido pedido, CancellationToken ct)
75
+ {
76
+ if (pedido.Dia.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
77
+ return new ReservaDesfecho.DiaSemExpediente(pedido.Dia);
78
+
79
+ var capacidade = await agenda.CapacidadeAsync(pedido.SalaId, ct);
80
+ if (pedido.Pessoas > capacidade)
81
+ return new ReservaDesfecho.SalaCheia(capacidade);
82
+
83
+ return new ReservaDesfecho.Reservada(await agenda.GravarAsync(pedido, ct));
84
+ }
85
+ }
86
+
87
+ public static class ReservarSalaTool
88
+ {
89
+ public const string Name = "reservar_sala";
90
+
91
+ public static AIFunction Create(ReservaOps ops) =>
92
+ AIFunctionFactory.Create(
93
+ async (
94
+ [Description("Identificador da sala, como listado por consultar_salas")] string salaId,
95
+ [Description("Dia da reserva, AAAA-MM-DD")] DateOnly dia,
96
+ [Description("Quantidade de pessoas")] int pessoas,
97
+ CancellationToken ct) =>
98
+ Render(await ops.ReservarAsync(new ReservaPedido(salaId, dia, pessoas), ct)),
99
+ Name,
100
+ "Reserva uma sala para um dia. Só chame depois de confirmar sala, dia e número de pessoas com o cliente.");
101
+
102
+ public static object Render(ReservaDesfecho desfecho) => desfecho switch
103
+ {
104
+ ReservaDesfecho.Reservada r => new { ok = true, confirmacao = r.ConfirmacaoId },
105
+ ReservaDesfecho.SalaCheia c => new { ok = false, erro = $"A sala comporta no máximo {c.Capacidade} pessoas. Ofereça outra sala ou reduza o grupo." },
106
+ ReservaDesfecho.DiaSemExpediente d => new { ok = false, erro = $"{d.Dia:yyyy-MM-dd} não tem expediente. Peça outro dia." },
107
+ _ => throw new ArgumentOutOfRangeException(nameof(desfecho)),
108
+ };
109
+ }
110
+
111
+ public static class ReservaToolsRegistration
112
+ {
113
+ public static IServiceCollection AddReservaTools(this IServiceCollection services) =>
114
+ services.AddScoped<ReservaOps>();
115
+ }
116
+ ```
117
+
118
+ A checagem de fronteira é um `grep`, e tem de sair vazio: a `Ops` não conhece a tool nem fala com o modelo.
119
+
120
+ ```bash
121
+ grep -rnE "reservar_sala|Ofereça|Peça|AIFunction" Features/Reservas/Ops/
122
+ ```
123
+
124
+ ## Os testes das duas camadas
125
+
126
+ O teste da `Ops` compara **tipos e chamadas**; o de contrato compara **o texto que o modelo lê**. Trocar a redação de uma recusa quebra só o segundo.
127
+
128
+ ```csharp
129
+ using System.Text.Encodings.Web;
130
+ using System.Text.Json;
131
+ using Exemplo.Tools;
132
+ using Xunit;
133
+
134
+ namespace Exemplo.Tools.Tests;
135
+
136
+ internal sealed class AgendaFake(int capacidade) : IAgendaExterna
137
+ {
138
+ public List<string> Chamadas { get; } = [];
139
+
140
+ public Task<int> CapacidadeAsync(string salaId, CancellationToken ct)
141
+ {
142
+ Chamadas.Add($"capacidade:{salaId}");
143
+ return Task.FromResult(capacidade);
144
+ }
145
+
146
+ public Task<string> GravarAsync(ReservaPedido pedido, CancellationToken ct)
147
+ {
148
+ Chamadas.Add($"gravar:{pedido.SalaId}");
149
+ return Task.FromResult("R-1");
150
+ }
151
+ }
152
+
153
+ public sealed class ReservaOpsTests
154
+ {
155
+ [Fact]
156
+ public async Task Sala_cheia_nao_grava_e_devolve_a_capacidade()
157
+ {
158
+ var agenda = new AgendaFake(capacidade: 4);
159
+ var ops = new ReservaOps(agenda);
160
+
161
+ var desfecho = await ops.ReservarAsync(new ReservaPedido("s1", new DateOnly(2026, 10, 5), 6), CancellationToken.None);
162
+
163
+ Assert.Equal(new ReservaDesfecho.SalaCheia(4), desfecho);
164
+ Assert.Equal(["capacidade:s1"], agenda.Chamadas);
165
+ }
166
+ }
167
+
168
+ public sealed class ReservarSalaToolTests
169
+ {
170
+ private static readonly JsonSerializerOptions Literal = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
171
+
172
+ [Fact]
173
+ public void Render_da_sala_cheia_diz_ao_modelo_o_que_fazer()
174
+ {
175
+ var json = JsonSerializer.Serialize(ReservarSalaTool.Render(new ReservaDesfecho.SalaCheia(4)), Literal);
176
+
177
+ Assert.Equal("{\"ok\":false,\"erro\":\"A sala comporta no máximo 4 pessoas. Ofereça outra sala ou reduza o grupo.\"}", json);
178
+ }
179
+
180
+ [Fact]
181
+ public void A_superficie_que_o_modelo_le_nao_mudou()
182
+ {
183
+ var tool = ReservarSalaTool.Create(new ReservaOps(new AgendaFake(capacidade: 10)));
184
+
185
+ Assert.Equal("reservar_sala", tool.Name);
186
+ Assert.StartsWith("Reserva uma sala para um dia.", tool.Description);
187
+ Assert.Contains("\"salaId\"", tool.JsonSchema.GetRawText());
188
+ }
189
+ }
190
+ ```
191
+
192
+ ## Migrar uma tool que já tem a regra no adaptador
193
+
194
+ A migração é um refactor que **não pode mudar o que o modelo lê**. Por isso ela tem ordem:
195
+
196
+ 1. **Commit de prova.** Extraia a `Ops` e o `Render` sem tocar nenhum teste. A suíte antiga tem de continuar verde, inclusive os evals offline. Esse commit é a âncora: prova que o refactor é neutro.
197
+ 2. **Migre os testes**, e só depois de o commit de prova estar verde.
198
+ 3. **Matriz de migração.** Liste cada asserção antiga e a asserção nova que a cobre. "Já está coberto pelo teste X" precisa de prova **asserção por asserção**: na migração que originou este standard, 11 de 22 alegações desse tipo tinham perdido alguma asserção do teste antigo.
199
+ 4. **Os testes que ficam na origem têm de ser idênticos à base.** A migração só apaga. Uma checagem byte a byte dos métodos restantes contra o commit de prova pegou (e provou corrigidos) `Edit`s que casaram no método errado.
200
+ 5. **Mutação só contra os testes novos.** Ela acha lacunas que a suíte antiga nunca protegeu: em duas das três `Ops` migradas apareceram regras sem teste algum (valor digitado pelo comercial sobrescrito pelo catálogo, campos reafirmados sumindo da falha do `PUT`, corpo de update reenviando valor igual).
201
+
202
+ ## Exemplos reais
203
+
204
+ MORPH_GHLBrain, `src/Morph.GHLBrain/Features/Conversation/Operator/`:
205
+
206
+ | Tool | Adaptador | `Ops` | Desfecho |
207
+ |---|---|---|---|
208
+ | `finalizar_handoff` | `Tools/FinalizarHandoffTool.cs` | `Ops/HandoffOps.cs` | `HandoffDesfecho` |
209
+ | `atualizar_dossie` | `Tools/AtualizarDossieTool.cs` | `Ops/DossieOps.cs` | `DossieDesfecho` |
210
+ | `set_lead_fields` | `Tools/SetLeadFieldsTool.cs` | `Ops/DossieOps.cs` | `CamposDesfecho` |
211
+ | `route_lead` | `Tools/RouteLeadTool.cs` | `Ops/TriagemOps.cs` | `TriagemDesfecho` |
212
+
213
+ As decisões estão em `.morph/features/tools-finas/2-plan/decisions.md` (ADR-1 e ADR-2) daquele repositório.
214
+
215
+ ## Regras
216
+
217
+ - **ALWAYS** a regra de uma tool vai numa `sealed class XOps(...)` com construtor primário e `AddScoped` explícito
218
+ - **NEVER** chame essa classe de `*Service` ou `*Manager`: o mandate proíbe essa camada entre handler e dados, e `*Ops` é a forma da casa
219
+ - **ALWAYS** cada método de `Ops` devolve um `abstract record XDesfecho`, com um `sealed record` por caso que a regra distingue
220
+ - **NEVER** ponha na `Ops` uma frase para o modelo, o nome da tool ou uma instrução de autocorreção. Verifique por `grep`
221
+ - **ALWAYS** o texto para humano entregue por código (task, nota, ack) fica na `Ops`: é efeito
222
+ - **ALWAYS** o adaptador só monta o pedido tipado, chama a `Ops` e devolve `Render(desfecho)`. Zero regra ali; descrição dinâmica longa é permitida e segue byte a byte
223
+ - **ALWAYS** o teste da `Ops` asserta desfecho, chamadas e ordem no fake, nunca texto
224
+ - **ALWAYS** o teste de contrato asserta o `Render` com o literal exato e a `.Description`, e há um golden de nome, descrição e schema por tenant real
225
+ - **NEVER** declare "já coberto" sem a asserção nova que cobre cada asserção antiga
226
+ - **NEVER** migre o teste antes do commit de prova verde, nem deixe na origem um teste diferente da base
@@ -55,6 +55,8 @@ Give a start image and an end image, and let the model interpolate a camera trav
55
55
 
56
56
  **Avoid for scrub:** full orbits and any movement that returns to its origin. They become loops and kill the sense of progress.
57
57
 
58
+ **Long continuous sequence (hero → reveal → product).** Generate each next clip with `chainFrom: "<id of the previous clip>"` and its own `endImage`: the previous clip's real last frame becomes the start, so the seam matches (SSIM about 0.94 in the upstream skill's test). Join the clips with `media_tool { op: "concat" }` before `scrub_frames`. Source: the upstream `morph-lp-cinematic` skill; not re-measured here.
59
+
58
60
  ---
59
61
 
60
62
  ## Section 3: Faces Trip Moderation
@@ -41,6 +41,16 @@ ffmpeg -i source.mp4 -vf "fps=30,scale=1280:-1" -q:v 5 frames/frame_%03d.jpg
41
41
  | Numbering | `%03d` (frame_001) matches the 1-based `framePath` contract in Section 4. |
42
42
  | Format | JPG is the safe default (fast decode, universal). If the sequence gets heavy (tens of MB), also try WebP via `-c:v libwebp -q:v 70`, which usually cuts total weight at similar quality. |
43
43
 
44
+ **Frames from `media_tool scrub_frames` (morph-assets) do not follow the table above.** The tool writes `v{N}/frame_0001.webp …` (4-digit, 1-based, WebP by default) and a `frames.json` manifest (`version`, `count`, `format`, `width`, `height`, `fps`, `pattern`, `totalBytes`). Build `frameCount` and `framePath` from the manifest; a `%03d`/`.jpg` path from this section will not resolve against them. A new `v{N}` folder per extraction changes the URL by construction, so it is the cache-bust of Section 3:
45
+
46
+ ```js
47
+ import manifest from './hero-frames/frames.json'
48
+ const frameCount = manifest.count
49
+ const framePath = (i) => `/hero-frames/v${manifest.version}/frame_${String(i).padStart(4, '0')}.${manifest.format}`
50
+ ```
51
+
52
+ Source: the upstream `morph-lp-cinematic` skill (build-stack reference); not re-measured here.
53
+
44
54
  **Treat total sequence weight as a performance budget.** It shows up directly in Lighthouse. If it is too heavy, cut frame count (120 instead of 145) or resolution before cutting quality.
45
55
 
46
56
  **Mirror the footage when the subject collides with the copy.** If the subject sits on the side where text goes, flip it during extraction:
@@ -57,7 +67,7 @@ ffmpeg -i source.mp4 -vf "hflip,fps=30,scale=1664:-1" -q:v 5 frames/frame_%03d.j
57
67
 
58
68
  Re-extracting frames (for example going from 480p to 1080p) while keeping the same filenames means the browser serves the stale ones and nothing appears to change.
59
69
 
60
- Always version the frame URL and bump `N` on every re-extraction:
70
+ Always version the frame URL and bump `N` on every re-extraction (a `v{N}` folder per extraction, as `scrub_frames` writes, does it by construction):
61
71
 
62
72
  ```js
63
73
  const framePath = (i) => `/hero-frames/frame_${String(i).padStart(3, '0')}.jpg?v=2`
@@ -27,6 +27,16 @@ The harness gives each worktree a **block of 10 ports** (`.morph/worktree.env`,
27
27
 
28
28
  Also exported: `COMPOSE_PROJECT_NAME=morph-{feature}` (isolates container, network and volume names), `MORPH_PORT_BASE`, `MORPH_PORT_RANGE`, `PLAYWRIGHT_BASE_URL=http://localhost:<+0>`.
29
29
 
30
+ ## Project-named port variables
31
+
32
+ The harness exports only the four names above. A compose file that reads a port by another name (`"${PG_PORT:-5433}:5432"`) is **not** fed by the block: it publishes its own default, or whatever a `.env.e2e` copied from a sibling worktree carries, while `worktree.env` announces a different port. Hand-run specs then hit a closed port, or worse, seed one postgres and read another. Declare the name the project reads:
33
+
34
+ ```json
35
+ { "e2e": { "portEnvNames": { "postgres": "PG_PORT" } } }
36
+ ```
37
+
38
+ Keys are `web`, `api`, `postgres` and `redis`. Each declared name carries the **same value** as the standard variable, everywhere the layout goes: `worktree.env`, the compose env of `verify`/`e2e up`/`morph-spec env`, and the `PreToolUse:Bash` hook. Being layout, it is ignored (and named on stderr) if the credentials file carries it. An entry the harness cannot honour — unknown port, invalid or reserved name, a name used twice — is dropped and named on stderr.
39
+
30
40
  ## Core Rules
31
41
 
32
42
  - NEVER set `container_name` — Compose already names containers `<project>-<service>-1`, and an explicit name is shared by every worktree
@@ -36,7 +46,7 @@ Also exported: `COMPOSE_PROJECT_NAME=morph-{feature}` (isolates container, netwo
36
46
  - ALWAYS give `DATABASE_URL` an in-network default so the stack comes up with no `.env`: `postgresql://postgres:postgres@postgres:5432/${POSTGRES_DB:-app}`
37
47
  - ALWAYS read the base URL from the environment in `playwright.config.ts`: `process.env.PLAYWRIGHT_BASE_URL ?? \`http://localhost:${process.env.PORT ?? 3000}\``
38
48
  - NEVER put a cloud connection string (Neon branch etc.) in `.morph/worktree.env` — it is regenerated; credentials go in the gitignored file named by `config.json → e2e.auth.envFile`
39
- - NEVER put a variable from the port layout table above (nor `PLAYWRIGHT_BASE_URL`) in that credentials file — the harness **ignores** it and says which key it ignored on stderr. Ports come from the allocated block; the file carries credentials. A `.env.e2e` copied from a sibling worktree carries THAT worktree's block, and before the filter existed it silently repointed this stack at the neighbour's postgres
49
+ - NEVER put a variable from the port layout table above (nor `PLAYWRIGHT_BASE_URL`, nor a name declared in `e2e.portEnvNames`) in that credentials file — the harness **ignores** it and says which key it ignored on stderr. Ports come from the allocated block; the file carries credentials. A `.env.e2e` copied from a sibling worktree carries THAT worktree's block, and before the filter existed it silently repointed this stack at the neighbour's postgres
40
50
 
41
51
  ## What the harness enforces
42
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.42.0",
3
+ "version": "8.44.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",