@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.
- package/CLAUDE.md +3 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +25 -1
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +4 -5
- package/framework/agents.json +1 -0
- package/framework/docs/CLI.md +5 -2
- package/framework/hooks/README.md +11 -1
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
- package/framework/hooks/claude-code/core/chain.js +27 -7
- package/framework/hooks/claude-code/core/worktree-context.js +83 -14
- package/framework/hooks/claude-code/core/worktree-create.js +206 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
- package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/command-tokens.js +23 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/git-paths.js +25 -0
- package/framework/hooks/shared/hook-response.js +17 -0
- package/framework/hooks/shared/morph-cli.js +134 -0
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rules/infrastructure-standards.md +3 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +3 -3
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +3 -1
- package/framework/skills/core/morph-plan/SKILL.md +6 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +30 -0
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +231 -231
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +88 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +1 -1
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/e2e/e2e.js +77 -39
- package/src/commands/index.js +13 -13
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +8 -6
- package/src/commands/project/env.js +95 -0
- package/src/commands/project/worktree.js +266 -52
- package/src/commands/state/delete.js +8 -0
- package/src/commands/state/finish.js +29 -34
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +430 -418
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +5 -1
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/e2e/compose-env.js +6 -11
- package/src/lib/e2e/stack-teardown.js +115 -0
- package/src/lib/generators/architecture-scanner.js +2 -1
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/graph/graphifyignore.js +2 -1
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +21 -4
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/verify-runner.js +7 -0
- package/src/lib/worktree/dirs.js +32 -0
- package/src/lib/worktree/env-allocator.js +45 -11
- package/src/lib/worktree/worktree-audit.js +40 -2
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +18 -13
- package/src/utils/hooks-installer.js +46 -3
- package/src/utils/skills-installer.js +187 -187
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -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*
|
package/framework/MORPH.md
CHANGED
|
@@ -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
|
-
-
|
|
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
|
|
123
|
-
- **Ambiente
|
|
124
|
-
- **
|
|
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.
|
package/framework/agents.json
CHANGED
package/framework/docs/CLI.md
CHANGED
|
@@ -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
|
|
29
|
-
|
|
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
|
-
│ │
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
}
|