@polymorphism-tech/morph-spec 8.33.0 → 8.34.2

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 (176) hide show
  1. package/CLAUDE.md +3 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +25 -1
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -2
  9. package/framework/MORPH.md +4 -5
  10. package/framework/agents.json +1 -0
  11. package/framework/docs/CLI.md +5 -2
  12. package/framework/hooks/README.md +11 -1
  13. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  14. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  15. package/framework/hooks/claude-code/core/chain.js +27 -7
  16. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  17. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  18. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  19. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  20. package/framework/hooks/claude-code/statusline.sh +11 -11
  21. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  22. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  23. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  24. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  25. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  26. package/framework/hooks/shared/command-tokens.js +23 -0
  27. package/framework/hooks/shared/e2e-check.js +0 -0
  28. package/framework/hooks/shared/git-paths.js +25 -0
  29. package/framework/hooks/shared/hook-response.js +17 -0
  30. package/framework/hooks/shared/morph-cli.js +134 -0
  31. package/framework/hooks/shared/stdin-reader.js +26 -26
  32. package/framework/rules/infrastructure-standards.md +3 -1
  33. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  34. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  35. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  36. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  37. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  38. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  39. package/framework/skills/core/morph-implement/SKILL.md +3 -1
  40. package/framework/skills/core/morph-plan/SKILL.md +6 -3
  41. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  42. package/framework/skills/core/morph-review/SKILL.md +1 -1
  43. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  44. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  45. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  46. package/framework/standards/STANDARDS.json +30 -0
  47. package/framework/standards/ai-agents/agent-session.md +160 -160
  48. package/framework/standards/ai-agents/batching.md +193 -193
  49. package/framework/standards/ai-agents/context-providers.md +188 -188
  50. package/framework/standards/ai-agents/mcp-server.md +250 -250
  51. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  52. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  53. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  54. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  55. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  56. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  57. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  58. package/framework/standards/ai-agents/production.md +231 -231
  59. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  60. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  61. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  62. package/framework/standards/ai-agents/structured-output.md +249 -249
  63. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  64. package/framework/standards/infrastructure/docker/local-compose-isolation.md +88 -0
  65. package/framework/templates/IDE-SNIPPETS.md +262 -262
  66. package/framework/templates/REGISTRY.json +1 -1
  67. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  68. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  69. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  70. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  71. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  72. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  73. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  74. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  75. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  76. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  77. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  78. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  79. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  80. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  81. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  82. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  83. package/framework/templates/code/typescript/contracts.ts +167 -167
  84. package/framework/templates/docs/clarifications.md +253 -253
  85. package/framework/templates/docs/schema-analysis.md +119 -119
  86. package/framework/templates/docs/ui-components.md +52 -52
  87. package/framework/templates/docs/ui-design-system.md +76 -76
  88. package/framework/templates/docs/ui-flows.md +167 -167
  89. package/framework/templates/docs/ui-mockups.md +98 -98
  90. package/framework/templates/feature/decisions.md +122 -122
  91. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  92. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  93. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  94. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  95. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  96. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  100. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  101. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  102. package/framework/templates/infrastructure/github/README.md +56 -56
  103. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  104. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  105. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  106. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  107. package/framework/templates/integrations/asaas-client.cs +387 -387
  108. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  109. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  110. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  111. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  112. package/framework/templates/project/validate.js +120 -120
  113. package/framework/templates/state.template.json +17 -17
  114. package/framework/templates/ui/design-system.css +226 -226
  115. package/package.json +1 -1
  116. package/src/commands/dashboard/dashboard.js +53 -53
  117. package/src/commands/e2e/e2e.js +77 -39
  118. package/src/commands/index.js +13 -13
  119. package/src/commands/project/checkpoint.js +209 -209
  120. package/src/commands/project/doctor.js +8 -6
  121. package/src/commands/project/env.js +95 -0
  122. package/src/commands/project/worktree.js +266 -52
  123. package/src/commands/state/delete.js +8 -0
  124. package/src/commands/state/finish.js +29 -34
  125. package/src/core/state/phase-state-machine.js +197 -197
  126. package/src/core/templates/template-renderer.js +477 -477
  127. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  128. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  129. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  130. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  131. package/src/dashboard/app/main.jsx +13 -13
  132. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  133. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  134. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  135. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  136. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  137. package/src/dashboard/app/pages/Standards.jsx +332 -332
  138. package/src/dashboard/app/styles/index.css +47 -47
  139. package/src/dashboard/index.html +12 -12
  140. package/src/dashboard/package.json +24 -24
  141. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  142. package/src/dashboard/scanner/phases-scanner.js +15 -15
  143. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  144. package/src/dashboard/scanner/rules-scanner.js +37 -37
  145. package/src/dashboard/scanner/skills-scanner.js +43 -43
  146. package/src/dashboard/scanner/squads-scanner.js +13 -13
  147. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  148. package/src/dashboard/vite.config.js +24 -24
  149. package/src/lib/detectors/design-system-detector.js +188 -188
  150. package/src/lib/detectors/index.js +217 -217
  151. package/src/lib/e2e/compose-env.js +6 -11
  152. package/src/lib/e2e/stack-teardown.js +115 -0
  153. package/src/lib/generators/architecture-scanner.js +2 -1
  154. package/src/lib/generators/design-system-generator.js +508 -508
  155. package/src/lib/generators/settings-generator.js +209 -209
  156. package/src/lib/graph/graphifyignore.js +2 -1
  157. package/src/lib/hooks/hook-executor.js +167 -167
  158. package/src/lib/installers/mcp-installer.js +719 -719
  159. package/src/lib/tasks/task-classifier.js +30 -30
  160. package/src/lib/tasks/task-parser.js +209 -209
  161. package/src/lib/tasks/test-runner.js +21 -4
  162. package/src/lib/validators/checkpoint-persistence.js +39 -39
  163. package/src/lib/validators/css/css-validator.js +340 -340
  164. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  165. package/src/lib/validators/shared/issue-counter.js +14 -14
  166. package/src/lib/validators/verify-runner.js +7 -0
  167. package/src/lib/worktree/dirs.js +32 -0
  168. package/src/lib/worktree/env-allocator.js +45 -11
  169. package/src/lib/worktree/worktree-audit.js +40 -2
  170. package/src/utils/agent-id-mapper.js +11 -11
  171. package/src/utils/claude-settings-manager.js +300 -300
  172. package/src/utils/file-copier.js +18 -13
  173. package/src/utils/hooks-installer.js +46 -3
  174. package/src/utils/skills-installer.js +187 -187
  175. package/framework/templates/.idea/morph-templates.xml +0 -92
  176. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.33.0",
3
+ "version": "8.34.2",
4
4
  "displayName": "MORPH-SPEC Framework",
5
5
  "description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
6
6
  "publisher": "polymorphism-tech",
@@ -186,4 +186,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
186
186
 
187
187
  ---
188
188
 
189
- *morph-spec v8.33.0 by Polymorphism Tech*
189
+ *morph-spec v8.34.2 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.33.0 by Polymorphism Tech*
252
+ *morph-spec v8.34.2 by Polymorphism Tech*
@@ -60,7 +60,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
60
60
  | `retype <f> <tipo>` | Muda o workType mid-flight; bloqueado após o Gate 3 |
61
61
  | `finish <f> --pr\|--merge` | Encerramento único: arquiva, promove use cases, desmonta worktree. Integra só no `--merge`; `--pr` abre PR e fica `aguardando-merge` até alguém mergear |
62
62
  | `archive <f>` | Arquiva sem integrar (legadas/faxina) |
63
- | `worktree setup\|link\|remove\|list` | Ciclo do worktree; `remove` é a única remoção segura |
63
+ | `worktree setup\|provision\|link\|remove\|list` | Ciclo do worktree; `provision` = isolamento de task; `remove` é a única remoção segura |
64
64
  | `branch prune [--dry-run]` | Apaga branch já integrada, inclusive squash-merge; nunca a com PR aberto |
65
65
  | `handoff <f>` | Regenera o documento de retomada da feature |
66
66
  | `fleet` | Visão agregada de toda feature ativa (raiz + worktrees) |
@@ -109,6 +109,6 @@ Drift entre `agents.json` e os `.md` gerados é detectado por `morph-spec doctor
109
109
 
110
110
  ## Regra de precedência de skills
111
111
 
112
- Prefira `morph-*` a `superpowers:*` equivalentes (`brainstorming`→`morph-brainstorming`, `writing-plans`→`morph-plan`, `executing-plans`→`morph-apply`, `test-driven-development`→seguir `mandate.md`, TDD embutido): `morph-*` aplica Gates e paths corretos de `.morph/`; `superpowers:*` é genérico, sem esse contexto.
112
+ Prefira `morph-*` a `superpowers:*` equivalentes (`brainstorming`→`morph-brainstorming`, `writing-plans`→`morph-plan`, `executing-plans`→`morph-apply`, `test-driven-development`→seguir `mandate.md`, TDD embutido, `using-git-worktrees`→`morph-spec worktree setup|provision`): `morph-*` aplica Gates e paths corretos de `.morph/`; `superpowers:*` é genérico, sem esse contexto.
113
113
 
114
114
  *MORPH-SPEC v5 by Polymorphism Tech*
@@ -118,11 +118,10 @@ Features independentes podem ser desenvolvidas **em paralelo, cada uma no seu gi
118
118
 
119
119
  - **Uma feature = um worktree = uma instância.** Nunca abra duas instâncias na mesma feature (setup recusa, exit 3).
120
120
  - `feature.json` viaja **commitado na branch** `morph/{f}`; a cópia da raiz é esqueleto do nascimento, nunca mais atualizado — leitores (`list`/`handoff`/`fleet`) seguem a branch, mutadores recusam gravar fora da árvore dona (detalhes: CLI.md). `state.json` do worktree é índice derivável — self-heal, nunca sincronizado à mão.
121
- - Infra compartilhada (`node_modules`, `.claude`, `.morph/framework`) linkada por junction; `memory/`/`logs/` são locais de cada worktree. Faltou infra (worktree aberto com `git worktree add` puro) → `morph-spec worktree link --all`.
122
- - **Só o morph remove worktree.** Remoção genérica — `git worktree remove` (com OU sem `--force`), `rm -rf`, `Remove-Item -Recurse`, explorador/IDE — **atravessa a junction** e apaga o conteúdo real compartilhado da raiz (`.claude`/`node_modules`/`.morph/framework`), não só o link, saindo com sucesso. Foi assim que uma raiz teve `.claude/` e `.morph/framework/` esvaziados. Só `morph-spec` desmonta a junction ANTES de remover (PROVE-or-DECLINE): **fechar a feature** = `morph-spec finish <f> --pr|--merge`; **só desmontar o worktree** (feature segue aberta) = `morph-spec worktree remove <f>` — ou `--target <path>` para worktree sem feature. O hook `block-destructive-worktree-removal` barra a versão crua, mas a regra vale antes dele.
123
- - **Ambiente separado (portas):** setup aloca **bloco de portas determinístico** por feature em `.morph/worktree.env` (`PORT`, `MORPH_PORT_RANGE`; `ASPNETCORE_URLS` se dotnet). **Suba dev servers SEMPRE nas portas do bloco** — as padrão (3000/5000) pertencem à raiz. Local, git-invisível; SessionStart injeta as portas. Override: `config.json → fleet.basePort/blockSize`.
124
- - **Banco de dados:** compartilhado por padrão; isolar via Neon MCP: branch de DB `morph/{f}` + connection string em `.morph/worktree.env` (sem Neon, prefixe schema/DB pelo nome da feature). Migration destrutiva → integre antes de abrir worktrees irmãos.
125
- - **Limitação conhecida:** `node_modules` compartilhado (junction) — dependência mudada afeta os irmãos; integre (`finish`) antes de seguir em paralelo.
121
+ - **Só o morph cria worktree:** `worktree setup <f>` (feature) ou `worktree provision <n>` (isolamento de task, branch `morph-task/<n>`). `git worktree add` cru é bloqueado; `EnterWorktree`/`-w`/`isolation:"worktree"` passam pelo hook `WorktreeCreate` e caem em `worktrees/`. Fora de `worktrees/` = criado sem o morph (`doctor --worktrees` acusa). Infra (`node_modules`, `.claude`, `.morph/framework`) por junction; faltou → `worktree link --all`.
122
+ - **Só o morph remove worktree.** Remoção genérica — `git worktree remove` (com OU sem `--force`), `rm -rf`, `Remove-Item -Recurse`, explorador/IDE — **atravessa a junction** e apaga o conteúdo real da raiz (`.claude`/`node_modules`/`.morph/framework`), saindo com sucesso (já esvaziou uma raiz). Só `morph-spec` desmonta a junction ANTES de remover (PROVE-or-DECLINE): **fechar a feature** = `morph-spec finish <f> --pr|--merge`; **só desmontar o worktree** = `morph-spec worktree remove <f>` — ou `--target <path>` para worktree sem feature. O hook barra a versão crua, mas a regra vale antes dele.
123
+ - **Ambiente dedicado:** setup escreve `.morph/worktree.env` (`PORT`, `API_PORT`/`ASPNETCORE_URLS`, `POSTGRES_PORT`, `COMPOSE_PROJECT_NAME=morph-{f}`); cada worktree sobe o **seu** compose. O hook `PreToolUse:Bash` injeta as variáveis em dev server/compose; fora do Claude, `eval "$(morph-spec env)"`. Porta fixa ou `container_name` no compose **reprova o nó e2e** em worktree. `finish` derruba o stack **com volumes** (`--keep-volumes` preserva). Neon branch opcional, credencial em `e2e.auth.envFile`, nunca em `worktree.env`. Migration destrutiva → integre antes de abrir irmãos.
124
+ - **Limitação:** `node_modules` é junction compartilhada — dependência mudada afeta os irmãos; integre antes de seguir em paralelo.
126
125
  - **Handoff entre sessões:** `.morph/features/{f}/handoff.md` — retomada comprimida, regenerável (`morph-spec handoff <f>`, resolve a árvore dona sozinho), **aponta** para os artefatos em vez de copiá-los. Regenerado em `setup`/`create --worktree`/`advance`/`approve`. **Leia-o antes de re-derivar contexto à mão** — nunca edite manualmente.
127
126
  - **Visão da frota:** `fleet` agrega raiz + worktrees (fase, gates, tasks, portas, handoff, commit, sujeira); `worktree list` é a visão enxuta por worktree.
128
127
  - **Ownership de arquivos:** sem lock runtime — colisão resolve **em plan-time** (`dag` detecta overlap de `outputs`, recomenda `isolation: "worktree"`) e **por construção** (uma feature = um worktree = um escritor). Arquivos compartilhados convergem no merge do `finish`; conflito ali é escopo sobreposto, não bug.
@@ -138,6 +138,7 @@
138
138
  "standards": [
139
139
  "backend-integrations-hangfire-hangfire-jobs",
140
140
  "infrastructure-docker-coolify-deploy",
141
+ "infrastructure-docker-local-compose-isolation",
141
142
  "backend-dotnet-scrutor",
142
143
  "ai-agents-observability-patterns"
143
144
  ],
@@ -25,8 +25,11 @@
25
25
  - **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas.
26
26
  - **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
27
27
  - **`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
28
- - **`morph-spec worktree setup|link|remove|list`** — gerencia o ciclo do worktree por feature (`setup` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `link` religa infra compartilhada; `list` mostra fase/gate de cada worktree ativo). **Fechar a feature** continua sendo `morph-spec finish` (acima).
29
- - **`morph-spec worktree remove [feature] [--target <path>] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`); o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido.
28
+ - **`morph-spec worktree setup|provision|link|remove|list`** — gerencia o ciclo do worktree (`setup <f>` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `provision <n> [--base <ref>]` cria `worktrees/{n}/` na branch `morph-task/{n}` — um worktree de **isolamento de task**, sem feature, com as mesmas junctions e um bloco próprio sob a chave `task/{n}`; `link --all` religa a infra compartilhada **e regenera o `worktree.env`** com o layout atual; `list` mostra fase/gate de cada worktree ativo). **Fechar a feature** continua sendo `morph-spec finish` (acima).
29
+ **Worktrees nativos do Claude Code.** `EnterWorktree`, `claude -w <n>` e `Agent` com `isolation: "worktree"` passam pelo hook `WorktreeCreate` instalado pelo morph: nome conhecido como feature → `worktree setup`; senão → `worktree provision`. O resultado cai sempre em `worktrees/`, nunca em `.claude/worktrees/`. O hook `WorktreeRemove` chama `worktree remove --target <path> --if-clean` (só remove árvore limpa; a branch sobrevive) — mas ele **não disparou** em nenhum run headless medido, então a limpeza primária continua sendo o passo 4 de `morph-apply` §3b-w e o `doctor --worktrees`. `git worktree add` cru é bloqueado pelo hook `block-raw-worktree-add` (sem junctions, sem portas, sem identidade → invisível ao `fleet`).
30
+ **Layout do bloco (10 portas):** `PORT` (+0), `API_PORT` (+1, `ASPNETCORE_URLS` em dotnet), `POSTGRES_PORT` (+2), `REDIS_PORT` (+3), +4..+9 livres; mais `COMPOSE_PROJECT_NAME=morph-{f}`, `MORPH_PORT_BASE/RANGE`. O mesmo layout alimenta o `worktree.env`, o `docker compose` do `verify`/`e2e` e o hook `worktree-env-inject` (que prefixa `npm run dev`/`dotnet run`/`docker compose`/`playwright test` com `export …` quando o cwd tem manifesto e o comando não escolheu porta própria).
31
+ - **`morph-spec env [feature] [--json|--powershell]`** — imprime o bloco do worktree em forma shell-loadable (`eval "$(morph-spec env)"`; PowerShell: `morph-spec env --powershell | Invoke-Expression`). Feature vem do argumento ou da branch `morph/{f}`/`morph-task/{n}` do cwd. Deriva de `buildComposeEnv`, a mesma função do nó e2e — um shell e o stack nunca discordam. É o caminho para quem roda **fora** do Claude Code (terminal humano, CI, sub-agent sem hooks).
32
+ - **`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho. Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`); o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido.
30
33
  - **`morph-spec branch prune [--remote] [--dry-run] [--json]`** — apaga as branches locais cujo trabalho **já está** na default, **incluindo as squash-merged que o git não enxerga**. O problema que resolve: num repo que faz squash-merge, o commit de merge entra em `origin/master` mas os SHAs da branch não — `git branch --merged` não a lista, `git branch -d` recusa, e sobra o `-D`, indistinguível de descartar trabalho de verdade. Cada branch é classificada com evidência impressa: `merged-ancestral` (o git prova), `merged-squash` (PR `MERGED` pelo `gh`, **ou** árvore idêntica à da default), `open` (PR OPEN — nunca apagada), `in-worktree` (em uso — nunca apagada) e `unmerged` (**não foi possível provar** integração: sem `gh`, sem default resolvível ou trabalho de verdade — todas tratadas igual, de propósito). Só as duas primeiras são apagadas. Branch remota só com `--remote` explícito.
31
34
  - **`morph-spec handoff <feature> [--here]`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. **Resolve a árvore dona** antes de gerar (mesma lógica do `list`/`fleet`) e escreve LÁ, não necessariamente no cwd — rodar da raiz com o worktree vivo é seguro: o handoff sai correto e no lugar certo, em vez do esqueleto de nascimento da raiz. Sem árvore nenhuma materializada (só existe commitado em `morph/{f}`) recusa com `feature-not-in-tree`, nomeando a branch, em vez de gerar um documento apontando para arquivos inexistentes. `--here` força construir/escrever no cwd mesmo assim. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
32
35
  - **`morph-spec fleet`** — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
@@ -8,7 +8,11 @@ Comprehensive hooks system for enforcing spec-driven development at the Claude C
8
8
  framework/hooks/
9
9
  ├── claude-code/ # Claude Code native hooks
10
10
  │ ├── core/
11
- │ │ └── worktree-context.js # SessionStart: worktree feature identity / active-feature summary
11
+ │ │ ├── worktree-context.js # SessionStart: worktree feature identity / active-feature summary
12
+ │ │ ├── worktree-create.js # WorktreeCreate: governed tree via `morph-spec worktree setup|provision`
13
+ │ │ ├── worktree-remove.js # WorktreeRemove: safe teardown via `worktree remove --if-clean`
14
+ │ │ ├── block-destructive-worktree-removal.js # PreToolUse(Bash): raw removal follows junctions out
15
+ │ │ └── block-raw-worktree-add.js # PreToolUse(Bash): raw `git worktree add` is ungoverned
12
16
  │ ├── user-prompt/
13
17
  │ │ └── enrich-prompt.js # Context-aware prompt enrichment
14
18
  │ ├── pre-tool-use/
@@ -46,6 +50,10 @@ framework/hooks/
46
50
  | **PreToolUse** (Write\|Edit) | protect-spec-files.js | Block | Blocks edits to spec files after approval; for tasks.json validates the mutable-field diff, the status enum, and requires a `notes` when `outputs` diverge from the plan |
47
51
  | **PreToolUse** (Write\|Edit) | enforce-phase-writes.js | Block | Ensures writes go to current phase directory |
48
52
  | **PreToolUse** (Bash) | pre-bash-denylist.js | Block | Blocks `rm -rf .morph/` and direct state edits |
53
+ | **PreToolUse** (Bash) | block-destructive-worktree-removal.js | Block | Blocks `git worktree remove` / `rm -rf` / `Remove-Item -Recurse` on a directory holding reparse points (the infra junctions a raw removal would FOLLOW out, emptying the root's `.claude/`); names `morph-spec worktree remove` |
54
+ | **PreToolUse** (Bash) | block-raw-worktree-add.js | Block | Blocks raw `git worktree add` (a tree with no junctions → no hooks, no port block, no identity, outside `worktrees/`); names `morph-spec worktree setup <feature>` / `worktree provision <name> --base <ref>`. `list`/`prune`/`remove`/`lock`/`move`/`repair` pass |
55
+ | **WorktreeCreate** | worktree-create.js | Replaces native | Fires for `claude -w`, EnterWorktree and `Agent isolation:"worktree"`; replaces Claude Code's bare `git worktree add`. Runs `morph-spec worktree setup <n>` (known feature) or `worktree provision <n>` (task tree, branch `morph-task/<n>`), honours `worktree.baseRef:"head"`, prints the path as its last stdout line. CLI unreachable → plain git, still under `<root>/worktrees/` |
56
+ | **WorktreeRemove** | worktree-remove.js | Fail-open | Routes Claude Code's own cleanup through `morph-spec worktree remove --target --if-clean` (junctions dropped first; a dirty tree is left alone). Always exit 0. Secondary net — the event was observed NOT to fire headless; the Bash guard above stays the primary defense |
49
57
  | **PostToolUse** (Write\|Edit) | post-edit-typecheck.js | Advisory | Debounced `tsc --noEmit` after TS/TSX edits; silent on success. Also touches `.morph/memory/build-dirty.json` on every source-file edit — the write side of validate-completion.js's Stop-hook build-dirty gate |
50
58
  | **PostToolUse** (Write\|Edit) | tasks-json-guard.js | Advisory | Validates the written tasks.json schema (status/effort enums); warns so the LLM fixes it immediately |
51
59
  | **PostToolUse** (Write\|Edit) | trace-autogen.js | State sync | Auto-generates `.morph/traces/YYYY-MM-DD-{feature}-implement.json` from tasks.json + recap.md + taskScores on every tasks.json write — the skills never write traces by hand |
@@ -115,6 +123,8 @@ All Claude Code hooks import from `framework/hooks/shared/`:
115
123
  | `hook-response.js` | `block(reason)`, `approve(context)`, `injectContext(text)`, `pass()` |
116
124
  | `stdin-reader.js` | `readStdin()` — Promise-based stdin JSON reader |
117
125
  | `telemetry-logger.js` | `appendTelemetryEvent()`, `readTelemetryEvents()` — append-only JSONL event stream (`.morph/logs/events.jsonl`) |
126
+ | `morph-cli.js` | `runMorphSpec(args, { cwd, timeoutMs, env })` — spawns the morph-spec CLI from a hook (`MORPH_SPEC_BIN` → `morph-spec` on PATH → `npx --no-install`); `unavailable: true` when none resolves, never a throw. Hooks may never import from `src/` |
127
+ | `command-tokens.js` | `tokenize(command)` — quote-aware shell tokenizer shared by the Bash guards (a hook must never import a sibling hook: its standalone tail would fire) |
118
128
 
119
129
  `injectContext(text)` is how `worktree-context.js` (and any other SessionStart hook) delivers context: it emits `{ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": text } }` on stdout. Claude Code only honors additional context nested under `hookSpecificOutput` — a bare top-level field is ignored.
120
130
 
@@ -4,6 +4,7 @@ import { resolve, isAbsolute } from 'path';
4
4
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
5
5
  import { stateExists } from '../../shared/state-reader.js';
6
6
  import { scanForBlockers } from '../../shared/reparse-scan.js';
7
+ import { tokenize } from '../../shared/command-tokens.js';
7
8
 
8
9
  /**
9
10
  * block-destructive-worktree-removal — the only defense that does not depend on
@@ -32,14 +33,10 @@ import { scanForBlockers } from '../../shared/reparse-scan.js';
32
33
  * nothing to traverse is not blocked for looking dangerous.
33
34
  */
34
35
 
35
- /** Split a shell-ish command into tokens, honoring single and double quotes. */
36
- function tokenize(command) {
37
- const tokens = [];
38
- const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
39
- let m;
40
- while ((m = re.exec(command)) !== null) tokens.push(m[1] ?? m[2] ?? m[3]);
41
- return tokens;
42
- }
36
+ // The tokenizer lives in shared/command-tokens.js — block-raw-worktree-add.js
37
+ // reads `git worktree add` off the same tokens, and a hook must never import a
38
+ // sibling hook (its standalone tail would fire in the importer's process).
39
+ export { tokenize };
43
40
 
44
41
  /**
45
42
  * The path a recursive-removal command would destroy, or null when this is not
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import { blockResult, runStandalone } from '../../shared/hook-response.js';
3
+ import { stateExists } from '../../shared/state-reader.js';
4
+ // From shared/, never from the sibling hook: importing a hook module fires its
5
+ // standalone tail (stdin read + exit) inside THIS process.
6
+ import { tokenize } from '../../shared/command-tokens.js';
7
+
8
+ /**
9
+ * block-raw-worktree-add — PreToolUse (Bash) guard against `git worktree add`.
10
+ *
11
+ * A worktree created by raw git is a worktree morph cannot see: no infra
12
+ * junctions (`.claude`, `node_modules`, `.morph/framework` — so NO hooks run
13
+ * in there), no port block (its dev server collides with the root's), no
14
+ * feature/task identity (invisible to `fleet`, `doctor --worktrees`,
15
+ * `worktree list`), and a location git chose, not `<root>/worktrees/`. The
16
+ * sibling guard (block-destructive-worktree-removal) protects the way OUT;
17
+ * this one protects the way IN, by naming the two commands that build a
18
+ * governed tree:
19
+ *
20
+ * morph-spec worktree setup <feature> — a feature's tree
21
+ * morph-spec worktree provision <name> [--base <ref>] — a task-isolation tree
22
+ *
23
+ * DELIBERATELY NARROW: only the `add` subcommand. `list`, `prune`, `remove`
24
+ * (the sibling's business), `lock`, `unlock`, `move`, `repair` all pass. The
25
+ * `git` token may be followed by global options (`-C <dir>`, `--git-dir=<d>`,
26
+ * `-c k=v`) before `worktree`, and may sit anywhere in a chained command
27
+ * (`cd a && git worktree add b`) — every `git` occurrence is checked.
28
+ */
29
+
30
+ /** Global `git` options that take a separate value: `git -C <dir> worktree add`. */
31
+ const GIT_GLOBAL_WITH_VALUE = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env']);
32
+
33
+ /**
34
+ * Does this command line invoke `git worktree add` anywhere?
35
+ * @param {string} command
36
+ * @returns {boolean}
37
+ */
38
+ export function isRawWorktreeAdd(command) {
39
+ const tokens = tokenize(command);
40
+ const lower = tokens.map((t) => t.toLowerCase());
41
+ for (let i = 0; i < lower.length; i++) {
42
+ if (lower[i] !== 'git' && !/(^|[\\/])git(\.exe)?$/.test(lower[i])) continue;
43
+ let j = i + 1;
44
+ // Skip global options between `git` and the subcommand.
45
+ while (j < lower.length && lower[j].startsWith('-')) {
46
+ if (GIT_GLOBAL_WITH_VALUE.has(tokens[j])) j += 2; // `-C dir` consumes its value
47
+ else j += 1; // `--git-dir=x`, `--no-pager`, …
48
+ }
49
+ if (lower[j] === 'worktree' && lower[j + 1] === 'add') return true;
50
+ }
51
+ return false;
52
+ }
53
+
54
+ /**
55
+ * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
56
+ * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
57
+ */
58
+ export async function run(payload) {
59
+ try {
60
+ if (!stateExists()) return null;
61
+
62
+ const command = payload?.tool_input?.command || '';
63
+ if (!command || !isRawWorktreeAdd(command)) return null;
64
+
65
+ return blockResult(
66
+ 'Bloqueado: `git worktree add` cru cria um worktree que o morph-spec NÃO enxerga — sem as junctions de infra ' +
67
+ '(`.claude`, `node_modules`, `.morph/framework` → nenhum hook roda lá dentro), sem bloco de portas (o dev server ' +
68
+ 'colide com o da raiz), sem identidade (invisível para `fleet`, `doctor --worktrees` e `worktree list`) e fora de `<raiz>/worktrees/`.\n\n' +
69
+ 'Use o comando que constrói a árvore governada:\n' +
70
+ ' - feature: `morph-spec worktree setup <feature>`\n' +
71
+ ' - tarefa isolada (sem feature): `morph-spec worktree provision <nome> --base <ref>`\n\n' +
72
+ 'Os dois deixam a árvore em `<raiz>/worktrees/<nome>` com a infra completa; `list`/`prune`/`remove` do git não são afetados por este guard.'
73
+ );
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ await runStandalone(run);
@@ -37,6 +37,13 @@
37
37
  * mesmo grupo observam o MESMO evento real do Claude Code, então isto só
38
38
  * importa como desempate determinístico); na ausência de qualquer elo
39
39
  * declarando, cai para `payload.hook_event_name`; por último, `'PostToolUse'`.
40
+ * - updatedInput (PreToolUse): um elo pode devolver
41
+ * `hookSpecificOutput.updatedInput` (ver `updatedInputResult` em
42
+ * hook-response.js) para reescrever o tool_input. O ÚLTIMO elo a devolver um
43
+ * vence (só um elo do grupo reescreve, por construção). `permissionDecision:
44
+ * 'allow'` + `updatedInput` só entram no resultado final quando algum elo
45
+ * reescreveu — um `allow` incondicional pularia o prompt de permissão de
46
+ * TODO Bash, que é exatamente o contrário de um guardrail.
40
47
  * - Fail-open POR ELO: uma exceção não tratada por um hook individual (import
41
48
  * falhou, run() não existe, etc.) não derruba a cadeia — os elos seguintes
42
49
  * ainda rodam. Cada hook já fail-opens sozinho dentro do seu próprio
@@ -78,6 +85,7 @@ async function main() {
78
85
 
79
86
  const contexts = [];
80
87
  let eventName = null;
88
+ let updatedInput = null;
81
89
 
82
90
  for (const name of names) {
83
91
  let result = null;
@@ -97,6 +105,12 @@ async function main() {
97
105
  process.exit(0);
98
106
  }
99
107
 
108
+ const rewrite = result?.hookSpecificOutput?.updatedInput;
109
+ if (rewrite && typeof rewrite === 'object') {
110
+ updatedInput = rewrite; // last writer wins
111
+ eventName = eventName || result?.hookSpecificOutput?.hookEventName;
112
+ }
113
+
100
114
  const ctx = contextOf(result);
101
115
  if (ctx) {
102
116
  contexts.push(ctx);
@@ -107,16 +121,22 @@ async function main() {
107
121
  }
108
122
  }
109
123
 
110
- if (contexts.length === 0) {
124
+ if (contexts.length === 0 && !updatedInput) {
111
125
  process.exit(0);
112
126
  }
113
127
 
114
- console.log(JSON.stringify({
115
- hookSpecificOutput: {
116
- hookEventName: eventName || payload?.hook_event_name || 'PostToolUse',
117
- additionalContext: contexts.join('\n\n'),
118
- },
119
- }));
128
+ const hookSpecificOutput = {
129
+ hookEventName: eventName || payload?.hook_event_name || 'PostToolUse',
130
+ };
131
+ // `permissionDecision:'allow'` ONLY travels with a rewrite — an unconditional
132
+ // allow would skip the permission prompt for every command the chain sees.
133
+ if (updatedInput) {
134
+ hookSpecificOutput.permissionDecision = 'allow';
135
+ hookSpecificOutput.updatedInput = updatedInput;
136
+ }
137
+ if (contexts.length) hookSpecificOutput.additionalContext = contexts.join('\n\n');
138
+
139
+ console.log(JSON.stringify({ hookSpecificOutput }));
120
140
  process.exit(0);
121
141
  }
122
142
 
@@ -89,6 +89,59 @@ function featureFromBranch(dir) {
89
89
  }
90
90
  }
91
91
 
92
+ /**
93
+ * Task-isolation worktree (`morph-task/{name}`): the tree a `WorktreeCreate`
94
+ * hook provisions for a parallel sub-agent batch (morph-apply §3b-w) or for a
95
+ * `claude -w` session with no feature behind it. Not a feature — no
96
+ * feature.json, no gates — but it owns a port block and the same junctions.
97
+ */
98
+ function taskFromBranch(dir) {
99
+ try {
100
+ const branch = gitRevParse('--abbrev-ref HEAD', dir);
101
+ const match = branch.match(/^morph-task\/(.+)$/);
102
+ return match ? match[1] : null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Is this secondary worktree inside the primary root's `worktrees/` (or the
110
+ * legacy `.worktrees/`) container? A tree kept anywhere else — `.claude/
111
+ * worktrees/` from a native Claude Code run before the hook existed, a
112
+ * sibling `../repo-x`, superpowers' `~/.config/...` — was created without
113
+ * morph: no junctions, no ports, invisible to `fleet`. Fail-open: any error
114
+ * reads as "in container" so a healthy tree is never nagged by mistake.
115
+ */
116
+ function isOutsideContainer(dir) {
117
+ try {
118
+ const common = resolve(dir, gitRevParse('--git-common-dir', dir)).replace(/\\/g, '/');
119
+ const root = common.replace(/\/\.git$/, '');
120
+ const norm = resolve(dir).replace(/\\/g, '/');
121
+ return !['worktrees', '.worktrees'].some((d) => norm.startsWith(`${root}/${d}/`));
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ function buildOutOfContainerWarning(dir) {
128
+ return '⚠ Este worktree está FORA do container do morph (worktrees/): foi criado sem o morph-spec — ' +
129
+ 'sem junctions de infra, sem bloco de portas, invisível ao `fleet`. ' +
130
+ 'Integre a branch e remova com `morph-spec worktree remove --target "<path>"`; ' +
131
+ 'crie worktrees por `morph-spec worktree setup <f>` ou `worktree provision <n>` (o hook WorktreeCreate faz isso sozinho). ' +
132
+ 'Diagnóstico: `morph-spec doctor --worktrees`.';
133
+ }
134
+
135
+ function buildTaskContext(dir, task) {
136
+ let text = `🧩 Worktree de isolamento de task: ${task} (branch morph-task/${task}). ` +
137
+ 'Não é uma feature: trabalhe só na task despachada, commite na branch como último ato e NUNCA faça merge — ' +
138
+ 'o orquestrador integra e remove este worktree.';
139
+ const envLine = buildEnvLine(dir);
140
+ if (envLine) text += `\n${envLine}`;
141
+ text += buildMissingInfraWarning(dir);
142
+ return text;
143
+ }
144
+
92
145
  /** Fallback: if exactly one feature directory exists, assume that's the one. */
93
146
  function featureFromSingleDir(dir) {
94
147
  try {
@@ -114,8 +167,11 @@ function buildEnvLine(dir) {
114
167
  const port = content.match(/^PORT=(\d+)$/m)?.[1];
115
168
  if (!port) return null;
116
169
  const range = content.match(/^MORPH_PORT_RANGE=([\d-]+)$/m)?.[1];
117
- return `🔌 Ambiente desta worktree: PORT=${port}${range ? ` (bloco ${range})` : ''}. ` +
118
- `Suba dev servers SEMPRE nessas portas (fonte: .morph/worktree.env) — as portas padrão pertencem à raiz.`;
170
+ const pg = content.match(/^POSTGRES_PORT=(\d+)$/m)?.[1];
171
+ const project = content.match(/^COMPOSE_PROJECT_NAME=(\S+)$/m)?.[1];
172
+ const extras = [pg ? `POSTGRES_PORT=${pg}` : null, project ? `COMPOSE_PROJECT_NAME=${project}` : null].filter(Boolean);
173
+ return `🔌 Ambiente desta worktree: PORT=${port}${range ? ` (bloco ${range})` : ''}${extras.length ? `, ${extras.join(', ')}` : ''}. ` +
174
+ 'Suba dev servers SEMPRE nessas portas (fonte: .morph/worktree.env; o hook PreToolUse injeta em npm run dev/dotnet run/docker compose) — as portas padrão pertencem à raiz.';
119
175
  } catch {
120
176
  return null;
121
177
  }
@@ -148,20 +204,25 @@ function buildWorktreeContext(dir, feature) {
148
204
  const handoffLine = buildHandoffLine(dir, feature);
149
205
  if (handoffLine) text += `\n${handoffLine}`;
150
206
 
151
- // `.morph/framework` is checked alongside the other two because the hook
152
- // commands resolve to `$CLAUDE_PROJECT_DIR/.morph/framework/hooks/...`: if
153
- // that junction is missing, `node` finds no file and EVERY guardrail in this
154
- // worktree is silently dark — the worst failure mode of the lot, and the one
155
- // the old two-item check could not see.
207
+ text += buildMissingInfraWarning(dir);
208
+ return text;
209
+ }
210
+
211
+ /**
212
+ * `.morph/framework` is checked alongside the other two because the hook
213
+ * commands resolve to `$CLAUDE_PROJECT_DIR/.morph/framework/hooks/...`: if
214
+ * that junction is missing, `node` finds no file and EVERY guardrail in this
215
+ * worktree is silently dark — the worst failure mode of the lot, and the one
216
+ * the old two-item check could not see.
217
+ */
218
+ function buildMissingInfraWarning(dir) {
156
219
  const missing = ['node_modules', '.claude', join('.morph', 'framework')]
157
220
  .filter((rel) => !existsSync(join(dir, rel)));
158
- if (missing.length) {
159
- text += `\n⚠ Infra compartilhada ausente (${missing.join(', ').replace(/\\/g, '/')}). Rode: morph-spec worktree link --all`;
160
- if (missing.includes(join('.morph', 'framework'))) {
161
- text += `\n Sem .morph/framework os hooks do morph-spec NÃO executam neste worktree (nenhum guardrail ativo).`;
162
- }
221
+ if (!missing.length) return '';
222
+ let text = `\n⚠ Infra compartilhada ausente (${missing.join(', ').replace(/\\/g, '/')}). Rode: morph-spec worktree link --all`;
223
+ if (missing.includes(join('.morph', 'framework'))) {
224
+ text += `\n Sem .morph/framework os hooks do morph-spec NÃO executam neste worktree (nenhum guardrail ativo).`;
163
225
  }
164
-
165
226
  return text;
166
227
  }
167
228
 
@@ -234,9 +295,17 @@ export async function run(payload) {
234
295
  try { rotateEventsLogIfLarge(dir); } catch { /* non-blocking */ }
235
296
 
236
297
  if (isSecondaryWorktree(dir)) {
298
+ const task = taskFromBranch(dir);
299
+ if (task) {
300
+ return contextResult(buildTaskContext(dir, task), 'SessionStart');
301
+ }
237
302
  const feature = featureFromBranch(dir) || featureFromSingleDir(dir);
238
303
  if (!feature) {
239
- return null;
304
+ // A tree with no morph identity at all: worth a warning ONLY when it
305
+ // also lives outside the container — that is the signature of a
306
+ // worktree created behind morph's back (native tool, raw git, another
307
+ // skill). Inside the container with no identity → stay silent.
308
+ return isOutsideContainer(dir) ? contextResult(buildOutOfContainerWarning(dir), 'SessionStart') : null;
240
309
  }
241
310
  return contextResult(buildWorktreeContext(dir, feature) + claimBlock(dir, feature, sessionId), 'SessionStart');
242
311
  }