@polymorphism-tech/morph-spec 7.6.0 → 7.7.1

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 (43) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +18 -0
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +12 -4
  7. package/framework/MORPH.md +18 -10
  8. package/framework/agents.json +12 -4
  9. package/framework/hooks/README.md +8 -10
  10. package/framework/hooks/claude-code/core/gate-guard.js +36 -16
  11. package/framework/hooks/claude-code/core/protect-spec-files.js +20 -5
  12. package/framework/hooks/claude-code/core/tasks-json-guard.js +81 -0
  13. package/framework/hooks/shared/tasks-validator.js +68 -0
  14. package/framework/mcp/registry.json +5 -48
  15. package/framework/rubrics/backend.json +3 -3
  16. package/framework/rubrics/default.json +3 -3
  17. package/framework/rubrics/frontend.json +3 -3
  18. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  19. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  20. package/framework/skills/core/morph-apply/SKILL.md +53 -40
  21. package/framework/skills/core/morph-brainstorming/SKILL.md +7 -12
  22. package/framework/skills/core/morph-eval/SKILL.md +10 -8
  23. package/framework/skills/core/morph-implement/SKILL.md +27 -21
  24. package/framework/skills/core/morph-plan/SKILL.md +12 -21
  25. package/framework/skills/core/morph-proposal/SKILL.md +3 -8
  26. package/framework/skills/core/morph-review/SKILL.md +30 -14
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/standards/integration/mcp/mcp-tools.md +38 -43
  30. package/package.json +1 -1
  31. package/src/commands/project/doctor.js +36 -1
  32. package/src/commands/project/update.js +26 -2
  33. package/src/commands/state/approve.js +29 -4
  34. package/src/commands/state/score.js +88 -0
  35. package/src/commands/tasks/dag.js +68 -0
  36. package/src/dashboard/scanner/rubrics-scanner.js +1 -1
  37. package/src/lib/generators/recap-generator.js +1 -1
  38. package/src/lib/tasks/dispatch-planner.js +201 -0
  39. package/src/lib/validators/score-aggregator.js +3 -3
  40. package/src/scripts/setup-infra.js +43 -0
  41. package/src/utils/agents-installer.js +142 -10
  42. package/src/utils/hooks-installer.js +13 -5
  43. package/framework/rubrics/eval-rubric.json +0 -66
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
4
4
 
5
5
  **Package:** `@polymorphism-tech/morph-spec`
6
- **Version:** 7.6.0
6
+ **Version:** 7.7.1
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -365,4 +365,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
365
365
 
366
366
  ---
367
367
 
368
- *morph-spec v7.6.0 by [Polymorphism Tech](https://polymorphism.tech)*
368
+ *morph-spec v7.7.1 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -28,6 +28,10 @@ import { worktreeSetupCommand } from '../src/commands/project/worktree.js';
28
28
  import { approveCommand, approvalStatusCommand, unapproveCommand } from '../src/commands/state/approve.js';
29
29
  import { advanceCommand } from '../src/commands/state/advance.js';
30
30
  import { archiveCommand } from '../src/commands/state/archive.js';
31
+ import { scoreCommand } from '../src/commands/state/score.js';
32
+
33
+ // === Tasks ===
34
+ import { dagCommand } from '../src/commands/tasks/dag.js';
31
35
 
32
36
  // === Validation ===
33
37
  import { validateCommand } from './validate.js';
@@ -149,6 +153,20 @@ program
149
153
  .option('--reason <reason>', 'Reason for revocation')
150
154
  .action((feature, gate, options) => unapproveCommand(feature, gate, options));
151
155
 
156
+ program
157
+ .command('score <feature> <task> <score>')
158
+ .description('Record a task eval score in feature.json (taskScores, server-side timestamp)')
159
+ .option('--dims <dims>', 'Four comma-separated dimension scores in order a,c,q,t (architecture,contracts,code-quality,test-coverage)')
160
+ .option('--json', 'Output as JSON')
161
+ .action((feature, task, score, options) => scoreCommand(feature, task, score, options));
162
+
163
+ // ─── Tasks ────────────────────────────────────────────────────────────────
164
+ program
165
+ .command('dag <feature>')
166
+ .description('Analyze the tasks.json DAG and print the sequential vs parallel dispatch decision (effort-weighted)')
167
+ .option('--json', 'Output as JSON')
168
+ .action((feature, options) => dagCommand(feature, options));
169
+
152
170
  // ─── Validation ───────────────────────────────────────────────────────────
153
171
  program
154
172
  .command('validate [validator]')
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "7.6.0",
3
+ "version": "7.7.1",
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",
@@ -196,4 +196,4 @@ These files are never edited directly. Use CLI commands or `morph-spec update` i
196
196
 
197
197
  ---
198
198
 
199
- *morph-spec v7.6.0 by Polymorphism Tech*
199
+ *morph-spec v7.7.1 by Polymorphism Tech*
@@ -197,4 +197,4 @@ morph-spec doctor
197
197
 
198
198
  ---
199
199
 
200
- *morph-spec v7.6.0 by Polymorphism Tech*
200
+ *morph-spec v7.7.1 by Polymorphism Tech*
@@ -24,7 +24,8 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
24
24
  - Atualizar o progresso em `tasks.json` ao concluir cada task gravando apenas os campos mutáveis `status`, `outputs` e `notes` — via `Write` OU `Edit` (o hook `protect-spec-files` faz diff semântico e permite qualquer um dos dois desde que o contrato do plano permaneça congelado).
25
25
  - Apresentar gates com o template fixo definido em MORPH.md / morph-plan.
26
26
  - Registrar decisões não triviais em `decisions.md`.
27
- - Aplicar `morph-eval` após cada tarefa; silêncio em score ≥ 9 é o sinal correto. Score < 9 dispara o loop de correção autônomo (re-implementa, sem pausa), não escala ao humano entre tarefas.
27
+ - Aplicar `morph-eval` após cada tarefa; silêncio em score ≥ 9 é o sinal correto. Score < 9 dispara o loop de correção autônomo (re-implementa, sem pausa), não escala ao humano entre tarefas. No Gate 3, o score composto da feature vem do **avaliador independente** despachado pela `morph-review` (persona `evaluator`, contexto fresco) — nunca de auto-avaliação de quem implementou.
28
+ - Incluir as 3 cláusulas de protocolo em todo dispatch de sub-agent (relatório final obrigatório; não tocar tasks.json/feature.json/board; sem build/test da suíte compartilhada em batch paralelo) — templates em `morph-implement` §4 e `morph-apply` §3b.
28
29
 
29
30
  ## Slash commands
30
31
 
@@ -37,10 +38,12 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
37
38
  | `/morph-preflight` | Validação pré-deploy (specs, contratos, testes, infra) |
38
39
  | `/morph-troubleshoot [erro]` | Diagnóstico de erros .NET/Next.js com root-cause analysis |
39
40
 
40
- **Comandos CLI de gate** (a LLM os usa em vez de editar o estado à mão para aprovar/avançar):
41
+ **Comandos CLI de gate e de estado** (a LLM os usa em vez de editar o estado à mão):
41
42
  - `morph-spec approve <feature> <gate> --mode auto` — aprova um gate (`proposal`/`plan`/`review`) em `trust: auto`.
42
43
  - `morph-spec advance <feature>` — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta).
43
44
  - `morph-spec archive <feature>` — arquiva a feature (atômico, git-aware); é o que `/morph-archive` chama.
45
+ - `morph-spec dag <feature>` — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio).
46
+ - `morph-spec score <feature> <task> <score> [--dims a,c,q,t]` — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**.
44
47
 
45
48
  ## Skills core (em `.claude/skills/`)
46
49
 
@@ -56,8 +59,13 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
56
59
 
57
60
  ## Personas (em `.claude/agents/`)
58
61
 
59
- 7 personas de domínio dispatchávels via Agent tool:
60
- `dotnet-senior`, `ef-modeler`, `maf-expert`, `nextjs-expert`, `ui-designer`, `infra-engineer`, `evaluator`.
62
+ Despache tasks de domínio com `subagent_type: "morph-{id}"` — a persona é o **system prompt** do subagent registrado (gerado de `agents.json`, a fonte única). Se o tipo não estiver na sua lista de subagents (stack não instalado neste projeto), use o fallback documentado em `morph-apply` §3b: `general-purpose` + campo `persona` de `agents.json` colado no prompt, **registrando o fallback nas `notes` da task**.
63
+
64
+ <!-- morph-agents:start -->
65
+ _Lista derivada na instalação (`morph-spec setup-infra`). Se este bloco está vazio, rode `morph-spec setup-infra --reinstall`._
66
+ <!-- morph-agents:end -->
67
+
68
+ Drift entre `agents.json` e os `.md` gerados é detectado por `morph-spec doctor` — nunca edite `.claude/agents/morph-*.md` à mão.
61
69
 
62
70
  ## Regra de precedência de skills
63
71
 
@@ -18,8 +18,8 @@ Entre gates, você opera com autonomia total. O dev valida. Você executa.
18
18
  | Tarefa completada na fase implement | Iniciar a próxima task do tasks.json sem aguardar instrução |
19
19
  | Ambiguidade de escopo coberta pelo mandate | Resolver conservadoramente e registrar em decisions.md |
20
20
  | Ambiguidade bloqueante (requisito ausente ou contraditório) | UMA pergunta via AskUserQuestion |
21
- | Sub-agent com auto-score ≥ 7 | Integrar ao recap.md e continuar |
22
- | Sub-agent com auto-score < 7 | Aplicar morph-eval antes de aceitar |
21
+ | Sub-agent retornou relatório (qualquer auto-score) | Auto-score é triagem, não aprovação: a task só avança com morph-eval ≥ 9 |
22
+ | Sub-agent com auto-score < 7 | Re-despachar com feedback específico (ou corrigir inline) antes do morph-eval |
23
23
  | Build/typecheck quebra | Corrigir antes de marcar tarefa como done |
24
24
  | Score morph-eval ≥ 9 | Silêncio (aprovação implícita), próxima tarefa |
25
25
  | Score morph-eval < 9 numa task | Loop de correção autônomo: re-implementar com o feedback priorizado, até 3 tentativas, sem pausa humana |
@@ -68,7 +68,8 @@ Responsabilidades: executar tarefas do tasks.json em ordem, consultar standards
68
68
  Anti-padrão: o Specialist não cria novas tarefas. Descobertas de escopo durante a implementação vão para o decisions.md com flag "escopo expandido" — o Orchestrator avalia se o tasks.json precisa ser atualizado.
69
69
 
70
70
  **Evaluator** — ativo após uma tarefa ou fase ser concluída.
71
- Responsabilidades: aplicar as rubricas em `framework/evals/rubrics/`, produzir score 0-10, emitir feedback priorizado quando score < 9 (que alimenta o loop de correção autônomo do morph-apply), escalar para revisão humana apenas quando o loop de correção se esgotar (3 tentativas sem atingir 9).
71
+ Responsabilidades: aplicar as rubricas em `.morph/framework/evals/rubrics/`, produzir score 0-10, emitir feedback priorizado quando score < 9 (que alimenta o loop de correção autônomo do morph-apply), escalar para revisão humana apenas quando o loop de correção se esgotar (3 tentativas sem atingir 9).
72
+ No **Gate 3** o Evaluator é obrigatoriamente um sub-agent independente: a `morph-review` despacha o subagent registrado `morph-evaluator` (gerado de `agents.json`, tools read-only) com contexto fresco — mandate + diff da implementação + saída real dos testes — e o score composto da feature vem desse retorno, nunca de auto-avaliação de quem implementou.
72
73
  Anti-padrão: o Evaluator não reescreve o código que avaliou. Se o score está abaixo de 6, o relatório vai para o humano — não para uma segunda rodada de tentativa automática.
73
74
 
74
75
  Os papéis não são exclusivos — numa sessão longa você transita entre Orchestrator e Specialist conforme as tarefas avançam. A transição deve ser explícita: antes de mudar de papel, conclua o que estava fazendo ou registre o estado corrente no recap.md.
@@ -123,7 +124,7 @@ Não invente caminhos alternativos.
123
124
 
124
125
  Cada sessão de trabalho produz um trace JSON em `.morph/traces/YYYY-MM-DD-{feature}-{phase}.json`.
125
126
  O trace registra tarefas, scores de eval, standards consultados e sub-agents despachados.
126
- O formato canônico está em `framework/evals/trace-schema.md` (criado na issue #17).
127
+ O formato canônico está em `.morph/framework/evals/trace-schema.md` (criado na issue #17).
127
128
  Traces são ignorados pelo git (`.gitignore`) e servem para análise do harness — não fazem parte da entrega da feature.
128
129
 
129
130
  ---
@@ -145,7 +146,7 @@ O índice de standards fica em `framework/standards/STANDARDS.json`. Cada entrad
145
146
 
146
147
  **Como identificar o standard certo:** filtre por `category` primeiro (ex: "architecture", "contracts", "testing"), depois por `tags` (ex: "dotnet", "nextjs", "blazor"). Se dois standards parecem relevantes e há conflito, adote o mais específico ao stack do projeto — e registre o conflito no decisions.md.
147
148
 
148
- O agente responsável por um domínio está listado em `framework/agents.json`. Consulte o campo `persona` do agente para entender o ponto de vista que você deve adotar ao aplicar os standards daquele domínio. A persona define o nível de rigor esperado: um agente de arquitetura tem critérios mais rígidos do que um agente de suporte mensal.
149
+ O agente responsável por um domínio está listado em `.morph/framework/agents.json` (fonte única — os subagents `morph-{id}` em `.claude/agents/` são gerados dele). Consulte o campo `persona` do agente para entender o ponto de vista que você deve adotar ao aplicar os standards daquele domínio quando trabalhar inline. A persona define o nível de rigor esperado: um agente de arquitetura tem critérios mais rígidos do que um agente de suporte mensal.
149
150
 
150
151
  **Quando não há standard adequado:** registre no decisions.md que nenhum standard cobre o caso, descreva o critério que você adotou e sinalize para revisão futura. Não invente um standard — padrões não documentados são dívida técnica disfarçada.
151
152
 
@@ -161,7 +162,7 @@ Gates são pontos de **pausa potencial**. Em `trust: "auto"` (default), gates **
161
162
  - "cancelar feature" (Gate 3 usa "rollback" em vez de "cancelar")
162
163
  ```
163
164
 
164
- **Sinais de risco que forçam pausa em auto** (detectados via `src/lib/risk-detector.js`):
165
+ **Sinais de risco que forçam pausa em auto** (as skills aplicam estas regras em linguagem natural — a tabela abaixo é a fonte única):
165
166
 
166
167
  | Sinal | Threshold | Quando aplica |
167
168
  |-------|-----------|---------------|
@@ -186,7 +187,7 @@ Para alterar o modo: editar `.morph/config/config.json → trust` (`"auto"` ou `
186
187
 
187
188
  **Comportamento padrão fora de gate:** trabalhe com autonomia total. Gates só existem nestes três pontos. Não pause para confirmação no meio de implementação a menos que encontre uma ambiguidade de escopo que o mandate não cobre — nesse caso, registre a ambiguidade no decisions.md, resolva da forma mais conservadora possível, e sinalize na entrega da task.
188
189
 
189
- **A aprovação de gate é unificada nos comandos da CLI — a LLM nunca edita o estado à mão para aprovar um gate.** Em `trust: "auto"` (sem `AskUserQuestion`), a skill aprova o gate via `morph-spec approve <feature> <gate> --mode auto`; para aprovar o gate da fase corrente **e** já criar a pasta da próxima fase num passo só, use `morph-spec advance <feature>` (adicione `--design` para tomar o ramo opcional de uiux a partir da proposta) — isso substitui o Edit manual de state.json + `mkdir` da pasta de fase. **O hook state-sync.js** cobre o caminho manual: quando uma `AskUserQuestion` é respondida com label começando em "aprovado", ele registra o gate correspondente no `feature.json` autoritativo da feature com `approvedBy: "manual"` (um humano aprovou no loop), na forma canônica. Nos dois caminhos o gate acaba gravado em `.morph/features/{feature}/feature.json` — nunca via Edit à mão de `state.json`.
190
+ **A aprovação de gate é unificada nos comandos da CLI — a LLM nunca edita o estado à mão para aprovar um gate.** Em `trust: "auto"` (sem `AskUserQuestion`), a skill aprova o gate via `morph-spec approve <feature> <gate> --mode auto`; para aprovar o gate da fase corrente **e** já criar a pasta da próxima fase num passo só, use `morph-spec advance <feature>` (adicione `--design` para tomar o ramo opcional de uiux a partir da proposta) — isso substitui o Edit manual de state.json + `mkdir` da pasta de fase. Os scores por task também têm comando dedicado: `morph-spec score <feature> <task> <score> [--dims a,c,q,t]` grava `taskScores` no `feature.json` com timestamp server-side — nunca edite `taskScores` à mão. **O hook state-sync.js** cobre o caminho manual: quando uma `AskUserQuestion` é respondida com label começando em "aprovado", ele registra o gate correspondente no `feature.json` autoritativo da feature com `approvedBy: "manual"` (um humano aprovou no loop), na forma canônica. Nos dois caminhos o gate acaba gravado em `.morph/features/{feature}/feature.json` — nunca via Edit à mão de `state.json`.
190
191
 
191
192
  **Registre quem aprovou cada gate (`approvedBy`):** o gate é um objeto `{ approved, timestamp, approvedBy }`.
192
193
  - `approvedBy: "auto"` — gate avançado por política `trust: auto` sem decisão humana.
@@ -194,7 +195,7 @@ Para alterar o modo: editar `.morph/config/config.json → trust` (`"auto"` ou `
194
195
 
195
196
  Isso mantém o audit trail honesto justamente nos contextos headless (CI, `claude -p`) onde ele mais importa.
196
197
 
197
- **O hook gate-guard.js** confere, sem bloquear, se a ação tomada num gate (pausar via `AskUserQuestion` vs auto-aprovar) bateu com a decisão de trust que a skill computou via `shouldPauseGate`. Para isso, a skill **persiste** a decisão no `feature.json` autoritativo da feature antes de agir: `gateDecisions[gate] = { pause, reason, signals, trust, decidedAt }` (escrito com a tool `Edit` — a allowlist permite `Edit(.morph/features/**)`). Se a ação observada divergir do registro (pausou um gate auto-aprovável, ou auto-aprovou um gate que exigia pausa), o guard injeta um aviso no contexto — a LLM corrige. É um guardrail silencioso, nunca um orquestrador: coerente, sem registro, ou erro → silêncio.
198
+ **O hook gate-guard.js** confere, sem bloquear, se a ação tomada num gate (pausar via `AskUserQuestion` vs auto-aprovar) bateu com a decisão de trust que a skill computou via `shouldPauseGate`. Para isso, a skill **persiste** a decisão no `feature.json` autoritativo da feature antes de agir: `gateDecisions[gate] = { pause, reason, signals, trust, decidedAt }` (escrito com a tool `Edit` — a allowlist permite `Edit(.morph/features/**)`). Se a ação observada divergir do registro (pausou um gate auto-aprovável, ou auto-aprovou um gate que exigia pausa), o guard injeta um aviso no contexto — a LLM corrige. É um guardrail que nunca bloqueia: a cada conferência ele registra uma linha no activity log (`.morph/logs/activity.json`) com o resultado — `coherent` ou `divergent` — e só injeta aviso quando diverge; sem registro ou erro → silêncio.
198
199
 
199
200
  ---
200
201
 
@@ -213,6 +214,8 @@ A thread principal é o **Orchestrator**. Trabalho de implementação pesado ou
213
214
 
214
215
  Inline (sem dispatch) é aceitável para tasks curtas (effort S/M) **dentro do domínio que você já tem carregado em contexto**. Se optar por inline numa task que dispara um gatilho acima, **registre a justificativa em uma linha no decisions.md** ("inline porque o contexto X já está carregado e o overhead de dispatch não compensa") — uma escolha de inline silenciosa numa task pesada/multi-domínio é o anti-padrão.
215
216
 
217
+ **Mecanismo de dispatch:** os agents de domínio são subagents **registrados** (`.claude/agents/morph-{id}.md`, gerados de `agents.json` — a fonte única — na instalação). Despache com `subagent_type: "morph-{id}"` (ex.: `morph-dotnet-senior`): a persona é o **system prompt** do subagent, com tools, model e MCPs definidos por domínio no próprio `agents.json` (ex.: `context7` para docs vivas nos implementadores; Stitch/Gemini/Playwright no `morph-ui-designer`; o `morph-evaluator` é read-only). **Fallback** quando o tipo não está registrado (stack não instalado neste projeto): `general-purpose` com o campo `persona` de `agents.json` colado no prompt, **registrado nas `notes` da task**. O `**Persona:**` do recap registra sempre o mecanismo real usado — nunca uma auto-declaração.
218
+
216
219
  **O que passar ao sub-agent:**
217
220
  - Especificação da tarefa (do tasks.json)
218
221
  - Path do standard relevante (apenas um)
@@ -230,6 +233,11 @@ Inline (sem dispatch) é aceitável para tasks curtas (effort S/M) **dentro do d
230
233
  - Problemas encontrados e como foram resolvidos
231
234
  - Auto-score 0-10 com justificativa em uma linha
232
235
 
236
+ **Protocolo obrigatório do sub-agent** (cláusulas que todo prompt de dispatch inclui — ver templates em `morph-implement` §4 e `morph-apply` §3b):
237
+ 1. Ao concluir, entregar o relatório final (última mensagem, ou SendMessage para main se a sessão permanecer viva) — terminar idle sem reportar é **falha de protocolo**; o orquestrador cobra via SendMessage e registra a falha no recap.
238
+ 2. Não tocar em `tasks.json`, `feature.json`, `state.json` nem no board de sessão (TaskCreate/TaskUpdate) — todo estado é gravado exclusivamente pelo orquestrador.
239
+ 3. Em batch paralelo, não rodar build/test da suíte compartilhada — a verificação é do orquestrador, uma vez, pós-batch (alternativa: dispatch com `isolation: "worktree"`, e aí o build roda dentro do worktree).
240
+
233
241
  O sub-agent deve ser autossuficiente com o que você passou. Se precisar de mais contexto para começar, a tarefa não estava bem especificada — refine antes de despachar.
234
242
 
235
243
  **Quando NÃO despachar sub-agent:**
@@ -237,11 +245,11 @@ O sub-agent deve ser autossuficiente com o que você passou. Se precisar de mais
237
245
  - Quando o feedback do sub-agent precisaria ser discutido em tempo real — dispatch é assíncrono.
238
246
  - Quando a tarefa envolve mudanças de arquitetura que o Orchestrator precisa validar antes de executar.
239
247
 
240
- **Após receber o retorno do sub-agent:** leia o auto-score. Se for < 7, aplique a skill morph-eval no resultado antes de aceitar. Se for >= 7 e sem itens bloqueantes, integre o resultado ao recap.md e siga para a próxima tarefa.
248
+ **Após receber o retorno do sub-agent:** leia o auto-score — ele é **triagem, não aprovação**. Se for < 7, re-despache com o feedback específico (ou corrija inline) antes de avaliar. Em qualquer caso, a task só avança quando `morph-eval` der score ≥ 9 (a barra única do §0); integre o resultado ao recap.md e siga para a próxima tarefa.
241
249
 
242
250
  > **Anti-padrão a evitar:** implementar múltiplas tasks de domínios diferentes inline, em sequência, na thread principal — "eu já sei o padrão de cada uma". Isso é exatamente o sinal de que o sistema de delegação foi ignorado. Quando uma feature cruza ≥2 domínios, o caminho default é o Orchestrator despachando especialistas, não o Orchestrator virando faz-tudo.
243
251
 
244
- **Dispatch em paralelo (multi-agent):** quando há fluxos de trabalho **genuinamente independentes** (ex: a cadeia de backend e a cadeia de frontend de uma mesma feature, que só convergem no fim), despache múltiplos sub-agents **ao mesmo tempo** — um por fluxo — em vez de processá-los em série. O orquestrador analisa o DAG de `tasks.json`, identifica os fluxos independentes (por `dependencies` e por `group`), e dispara o batch paralelo. As regras concretas de decisão sequencial vs. paralelo e o cap de agentes simultâneos vivem na skill `morph-apply` (seção "Análise de DAG"). A thread principal coordena; os especialistas executam — inclusive em paralelo.
252
+ **Dispatch em paralelo (multi-agent):** quando há fluxos de trabalho **genuinamente independentes** (ex: a cadeia de backend e a cadeia de frontend de uma mesma feature, que só convergem no fim), despache múltiplos sub-agents **ao mesmo tempo** — um por fluxo — em vez de processá-los em série. O orquestrador roda `morph-spec dag {feature}` — o comando analisa o DAG de `tasks.json` (por `dependencies` e por `group`), **pondera o esforço** (raízes e fluxos só de tasks S não ganham dispatch próprio: vão inline ou agrupados num único agente) e imprime a decisão pronta. As regras concretas e o cap de agentes simultâneos vivem na skill `morph-apply` (seção "Análise de DAG"). A thread principal coordena; os especialistas executam — inclusive em paralelo.
245
253
 
246
254
  O padrão de dispatch e o template de prompt para sub-agents estão detalhados em `framework/skills/core/morph-implement/SKILL.md`.
247
255
 
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "5.0.0",
2
+ "version": "5.1.0",
3
3
  "schema": "domain-personas",
4
4
  "total_agents": 7,
5
- "description": "Domain personas dispatched per task by the morph-implement skill. The 3 roles (Orchestrator/Specialist/Evaluator) are assumed by Claude itself per MORPH.md — these are domain personas the Specialist consults when working in a specific area.",
5
+ "description": "Domain personas dispatched per task via subagent_type morph-{id} (.claude/agents/ generated from this file — single source of truth, ADR-001). Optional per-agent fields: `tools` (comma-separated tool scope; default Read, Write, Edit, Bash, Glob, Grep), `model` (default inherit), `mcp` (registry ids from framework/mcp/registry.json, granted as mcp__{id}). The 3 roles (Orchestrator/Specialist/Evaluator) are assumed by Claude itself per MORPH.md.",
6
6
  "agents": {
7
7
  "dotnet-senior": {
8
8
  "title": ".NET Senior Engineer",
@@ -21,6 +21,7 @@
21
21
  "backend-dotnet-vsa-handler-patterns",
22
22
  "backend-dotnet-fluent-validation-vsa"
23
23
  ],
24
+ "mcp": ["context7"],
24
25
  "persona": "Você é um engenheiro .NET sênior. Pensa em VSA com handlers diretos, result pattern, sealed classes, Guid.CreateVersion7(). Acesso a dados híbrido: IRepository<T> + IUnitOfWork para CRUD, DbContext direto no slice para queries específicas; nunca uma camada de Service/Manager. Não usa exceptions como fluxo. Consulta o standard antes de escrever cada slice. Nomeia tudo em PascalCase para tipos, camelCase para parâmetros."
25
26
  },
26
27
  "ef-modeler": {
@@ -38,6 +39,7 @@
38
39
  "infrastructure-neon-setup",
39
40
  "infrastructure-neon-pgvector"
40
41
  ],
42
+ "mcp": ["context7"],
41
43
  "persona": "Você é um especialista em EF Core sobre Neon (Postgres). Pensa em migrations explícitas, configurações fluentes, navigation properties bem nomeadas, índices intencionais. Sempre Guid.CreateVersion7() para PKs. Sealed entities. Nunca leak de DbContext fora do handler."
42
44
  },
43
45
  "maf-expert": {
@@ -61,6 +63,7 @@
61
63
  "ai-agents-middleware-patterns",
62
64
  "ai-agents-multi-agent-patterns"
63
65
  ],
66
+ "mcp": ["context7"],
64
67
  "persona": "Você é especialista no Microsoft Agent Framework (MAF 1.0 GA). Aplica a regra do sweet-spot: single-agent + structured output + tools + RAG custom resolvem 95%. Refere alias do Model Registry, nunca model hardcoded. Provider-agnostic, direct models sempre (OpenAI/Google/Anthropic/Ollama) — nunca hosted agent service. Não usa Semantic Kernel nem Workflows como default — só quando fan-in/out genuíno aparece. Telemetria via AgentOpenTelemetryConsts.DefaultSourceName."
65
68
  },
66
69
  "nextjs-expert": {
@@ -80,6 +83,7 @@
80
83
  "frontend-nextjs-naming-conventions",
81
84
  "frontend-nextjs-state-management"
82
85
  ],
86
+ "mcp": ["context7"],
83
87
  "persona": "Você escreve Next.js App Router idiomático. Server Components por padrão, Client Components apenas quando há interatividade real. Tailwind + Shadcn + Framer Motion. Tipos via Zod. Nada de getServerSideProps. RSC streaming quando faz sentido."
84
88
  },
85
89
  "ui-designer": {
@@ -97,6 +101,8 @@
97
101
  "frontend-design-system-animations",
98
102
  "frontend-nextjs-motion-patterns"
99
103
  ],
104
+ "tools": "Read, Write, Edit, Glob, Grep",
105
+ "mcp": ["context7", "stitch", "gemini-image", "playwright"],
100
106
  "persona": "Você desenha sistemas, não telas isoladas. Pensa em tokens (cor, espaço, tipografia), componentes reutilizáveis, estados (default/hover/focus/disabled/loading/error), acessibilidade WCAG AA. Mockups em Tailwind direto, sem Figma intermediário."
101
107
  },
102
108
  "infra-engineer": {
@@ -114,7 +120,8 @@
114
120
  "infrastructure-docker-coolify-deploy",
115
121
  "backend-dotnet-scrutor"
116
122
  ],
117
- "persona": "Você cuida de infra: jobs assíncronos com Hangfire (apenas quando há razão real), deployment, observabilidade. Pensa em retry, idempotência, dead-letter, métricas. Não inventa infra — usa o que o stack já oferece."
123
+ "mcp": ["context7"],
124
+ "persona": "Você cuida de infra: jobs assíncronos com Hangfire (apenas quando há razão real), deployment, observabilidade. Pensa em retry, idempotência, dead-letter, métricas. Não inventa infra — usa o que o stack já oferece. Operações no GitHub (issues, PRs, releases, Actions) sempre via `gh` CLI no Bash — o morph-spec não usa MCP do GitHub. Coolify sempre via SSH + curl na API local — não há MCP nem CLI."
118
125
  },
119
126
  "evaluator": {
120
127
  "title": "Code & Architecture Evaluator",
@@ -126,7 +133,8 @@
126
133
  ],
127
134
  "stack": ["*"],
128
135
  "standards": [],
129
- "persona": "Você aplica as rubricas em framework/evals/rubrics/. Não reescreve código — produz scores 0-10 por dimensão (architecture, contracts, code-quality, test-coverage), feedback priorizado acionável, e ação clara (aprovado em >=9 / feedback para o loop de correção em <9). Silêncio em scores ≥ 9 é o sinal correto."
136
+ "tools": "Read, Glob, Grep",
137
+ "persona": "Você aplica as rubricas em .morph/framework/evals/rubrics/. Você não participou da implementação — avalia apenas o mandate, o diff e a saída real dos testes que o orquestrador fornece, com contexto fresco. Não reescreve código — produz scores 0-10 por dimensão (architecture, contracts, code-quality, test-coverage), feedback priorizado acionável, e ação clara (aprovado em >=9 / feedback para o loop de correção em <9). Silêncio em scores ≥ 9 é o sinal correto."
130
138
  }
131
139
  }
132
140
  }
@@ -41,18 +41,16 @@ framework/hooks/
41
41
 
42
42
  | Event | Hook | Type | Purpose |
43
43
  |-------|------|------|---------|
44
- | **SessionStart** | inject-morph-context.js | Inject context | Shows active feature, phase, pending approvals |
45
- | **UserPromptSubmit** | enrich-prompt.js | Inject context | Warns about wrong-phase work, injects commands |
46
44
  | **PreToolUse** (Write\|Edit) | _(native permissions.deny)_ | Block | Blocks edits to state.json and .morph/framework/ |
47
- | **PreToolUse** (Write\|Edit) | protect-spec-files.js | Block | Blocks edits to spec files after approval |
45
+ | **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 |
48
46
  | **PreToolUse** (Write\|Edit) | enforce-phase-writes.js | Block | Ensures writes go to current phase directory |
49
- | **PreToolUse** (Bash) | _(prompt-type inline guard)_ | Block | Blocks `rm -rf .morph/` and direct state edits via Claude's reasoning |
50
- | **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state.json updates (feature register, output tracking, gate approval) |
51
- | **PostToolUse** (Write\|Edit\|AskUserQuestion) | gate-guard.js | Advisory | Warns (never blocks) when a gate action disagrees with the persisted gateDecisions (trust-mode enforcement, issue #63) |
52
- | **PostToolUse** (Bash) | dispatch.js | Dispatch | Triggers checkpoints on task completion |
53
- | **PostToolUseFailure** | handle-tool-failure.js | Logging | Appends structured JSON to .morph/logs/tool-failures.log |
54
- | **Stop** | validate-completion.js | Advisory | Warns about incomplete tasks/missing outputs/pending gates |
55
- | **PreCompact** | save-morph-context.js | Snapshot | Saves state to .morph/memory/ before compaction |
47
+ | **PreToolUse** (Bash) | pre-bash-denylist.js | Block | Blocks `rm -rf .morph/` and direct state edits |
48
+ | **PostToolUse** (Write\|Edit) | post-edit-typecheck.js | Advisory | Debounced `tsc --noEmit` after TS/TSX edits; silent on success |
49
+ | **PostToolUse** (Write\|Edit) | tasks-json-guard.js | Advisory | Validates the written tasks.json schema (status/effort enums); warns so the LLM fixes it immediately |
50
+ | **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state updates (feature register, output tracking, gate approval) |
51
+ | **PostToolUse** (Write\|Edit\|AskUserQuestion) | gate-guard.js | Advisory | Warns (never blocks) when a gate action disagrees with the persisted gateDecisions; logs one `coherent`/`divergent` line per check in `.morph/logs/activity.json` (issue #63) |
52
+ | **PostToolUse** (all tools) | loop-detect.js | Advisory | Warns when the last 8 actions collapse to ≤2 unique signatures |
53
+ | **Stop** | validate-completion.js | Advisory | Warns about incomplete tasks/missing outputs/recap drift; build check; session snapshot |
56
54
 
57
55
  ## Design Principles
58
56
 
@@ -8,8 +8,8 @@
8
8
  * to state.json (`features[feature].gateDecisions[gate]`) and injects a
9
9
  * non-blocking warning when they disagree.
10
10
  *
11
- * It is a silent guardrail, NOT an orchestrator (per MORPH.md): it never
12
- * blocks, never writes state, and fails open on any error or missing data.
11
+ * It is a guardrail, NOT an orchestrator (per MORPH.md): it never blocks,
12
+ * never writes state, and fails open on any error or missing data.
13
13
  *
14
14
  * Two mismatch directions:
15
15
  * - LLM PAUSED (AskUserQuestion on a gate) but the persisted decision said
@@ -17,12 +17,17 @@
17
17
  * - LLM AUTO-APPROVED (gate became approvedBy:"auto") but the persisted
18
18
  * decision said pause:true → skipped a required human pause. Warn.
19
19
  *
20
- * Coherent action, no persisted decision, or any error → silence.
20
+ * Observability: every time a gate action is actually checked against a
21
+ * persisted gateDecisions entry, one line lands in the activity log
22
+ * (.morph/logs/activity.json) with result 'coherent' or 'divergent' — so a
23
+ * human can confirm the guard verified coherence instead of absolute silence.
24
+ * No persisted decision, non-gate action, or any error → silence.
21
25
  */
22
26
 
23
27
  import { readFileSync } from 'fs';
24
28
  import { loadState, stateExists } from '../../shared/state-reader.js';
25
29
  import { injectContext, pass } from '../../shared/hook-response.js';
30
+ import { logHookActivity } from '../../shared/activity-logger.js';
26
31
 
27
32
  function readStdin() {
28
33
  try {
@@ -64,7 +69,7 @@ function getDecision(state, feature, gate) {
64
69
  * The LLM just asked a gate question. If the persisted decision said this
65
70
  * gate should auto-pass (pause:false), the human was interrupted needlessly.
66
71
  */
67
- function checkAskUserQuestion(input, state, feature) {
72
+ function checkAskUserQuestion(input, state, feature, cwd) {
68
73
  // Questions live in tool_input.questions[]; answers in tool_response.
69
74
  const questions = input.tool_input?.questions;
70
75
  const askedTexts = [];
@@ -86,6 +91,7 @@ function checkAskUserQuestion(input, state, feature) {
86
91
  const decision = getDecision(state, feature, gate);
87
92
  if (!decision) continue; // no persisted decision → nothing to verify
88
93
  if (decision.pause === false) {
94
+ logHookActivity('gate-guard', 'PostToolUse', 'divergent', cwd);
89
95
  return injectContext(
90
96
  `⚠️ [morph-spec] GATE OVER-PAUSE — você apresentou o Gate "${gate}" via AskUserQuestion, ` +
91
97
  `mas a decisão registrada em state.json (gateDecisions.${gate}) era pause:false ` +
@@ -95,18 +101,29 @@ function checkAskUserQuestion(input, state, feature) {
95
101
  `caso contrário, evite pausar gates auto-aprováveis.`
96
102
  );
97
103
  }
104
+ // The decision said pause:true and the LLM did pause — coherent.
105
+ logHookActivity('gate-guard', 'PostToolUse', 'coherent', cwd);
106
+ return pass();
98
107
  }
99
108
  return pass();
100
109
  }
101
110
 
111
+ /** True for edits to .morph/state.json (legacy) or a per-feature feature.json (v5). */
112
+ function isGateStateFile(filePath) {
113
+ const fp = String(filePath ?? '').replace(/\\/g, '/');
114
+ if (fp.endsWith('.morph/state.json')) return true;
115
+ return fp.includes('.morph/features/') && fp.endsWith('/feature.json');
116
+ }
117
+
102
118
  /**
103
- * The LLM just edited state.json. If a gate flipped to approvedBy:"auto" but
104
- * the persisted decision said pause:true, a required human pause was skipped.
119
+ * The LLM just edited gate state (state.json legacy, or the authoritative
120
+ * feature.json in v5). If a gate flipped to approvedBy:"auto" but the
121
+ * persisted decision said pause:true, a required human pause was skipped.
105
122
  */
106
- function checkStateEdit(input, state, feature) {
107
- const filePath = String(input.tool_input?.file_path ?? '').replace(/\\/g, '/');
108
- if (!filePath.endsWith('.morph/state.json')) return pass();
123
+ function checkStateEdit(input, state, feature, cwd) {
124
+ if (!isGateStateFile(input.tool_input?.file_path)) return pass();
109
125
 
126
+ let checked = false;
110
127
  const gates = state.features?.[feature]?.approvalGates ?? {};
111
128
  for (const gate of ['proposal', 'plan', 'review']) {
112
129
  const g = gates[gate];
@@ -115,15 +132,18 @@ function checkStateEdit(input, state, feature) {
115
132
  const decision = getDecision(state, feature, gate);
116
133
  if (!decision) continue;
117
134
  if (decision.pause === true) {
135
+ logHookActivity('gate-guard', 'PostToolUse', 'divergent', cwd);
118
136
  return injectContext(
119
- `⚠️ [morph-spec] GATE SKIPPED PAUSE — o Gate "${gate}" foi marcado approvedBy:"auto" em state.json, ` +
137
+ `⚠️ [morph-spec] GATE SKIPPED PAUSE — o Gate "${gate}" foi marcado approvedBy:"auto", ` +
120
138
  `mas a decisão registrada (gateDecisions.${gate}) era pause:true ` +
121
139
  `(sinal: ${decision.reason || 'risco detectado'}). ` +
122
140
  `Um sinal de risco exigia pausa humana via AskUserQuestion — uma auto-aprovação aqui ignora o trust mode. ` +
123
141
  `Reabra o gate com AskUserQuestion (label "aprovado, prosseguir") ou corrija o sinal de risco antes de avançar.`
124
142
  );
125
143
  }
144
+ checked = true;
126
145
  }
146
+ if (checked) logHookActivity('gate-guard', 'PostToolUse', 'coherent', cwd);
127
147
  return pass();
128
148
  }
129
149
 
@@ -134,11 +154,11 @@ function main() {
134
154
  const input = readStdin();
135
155
  const toolName = input.tool_name;
136
156
 
137
- // Short-circuit before loading state: a Write/Edit only matters if it touched
138
- // state.json. Skip the disk read for the dozens of unrelated edits per feature.
157
+ // Short-circuit before loading state: a Write/Edit only matters if it
158
+ // touched gate state (state.json legacy, or the v5 per-feature
159
+ // feature.json). Skip the disk read for unrelated edits.
139
160
  if (toolName === 'Edit' || toolName === 'Write') {
140
- const fp = String(input.tool_input?.file_path ?? '').replace(/\\/g, '/');
141
- if (!fp.endsWith('.morph/state.json')) return pass();
161
+ if (!isGateStateFile(input.tool_input?.file_path)) return pass();
142
162
  } else if (toolName !== 'AskUserQuestion') {
143
163
  return pass();
144
164
  }
@@ -149,8 +169,8 @@ function main() {
149
169
  const feature = findActiveFeature(state);
150
170
  if (!feature) return pass();
151
171
 
152
- if (toolName === 'AskUserQuestion') return checkAskUserQuestion(input, state, feature);
153
- if (toolName === 'Edit' || toolName === 'Write') return checkStateEdit(input, state, feature);
172
+ if (toolName === 'AskUserQuestion') return checkAskUserQuestion(input, state, feature, cwd);
173
+ if (toolName === 'Edit' || toolName === 'Write') return checkStateEdit(input, state, feature, cwd);
154
174
  } catch {
155
175
  // fail-open
156
176
  }
@@ -37,6 +37,7 @@ import {
37
37
  import { block, pass } from '../../shared/hook-response.js';
38
38
  import { logHookActivity } from '../../shared/activity-logger.js';
39
39
  import { getFilePath, getToolName } from '../../shared/payload-utils.js';
40
+ import { validateTasksDoc, outputsDivergenceViolations } from '../../shared/tasks-validator.js';
40
41
 
41
42
  /**
42
43
  * Compare two tasks.json structures and return the list of plan-contract
@@ -162,6 +163,18 @@ try {
162
163
  }
163
164
 
164
165
  const violations = diffPlanContract(oldDoc, newDoc, mutableFields);
166
+
167
+ // Schema check on the proposed doc: even mutable-field writes must keep a
168
+ // valid status enum (a leaf agent writing status:"completed" was the
169
+ // production failure this closes). Only NEWLY introduced schema errors
170
+ // block — a pre-existing violation must not deadlock progress writes.
171
+ const preExisting = new Set(validateTasksDoc(oldDoc).errors);
172
+ violations.push(...validateTasksDoc(newDoc).errors.filter(e => !preExisting.has(e)));
173
+
174
+ // Planned-vs-real outputs diff: changing `outputs` is allowed, but the
175
+ // divergence must be explained in `notes` in the same edit.
176
+ violations.push(...outputsDivergenceViolations(oldDoc, newDoc));
177
+
165
178
  if (violations.length === 0) {
166
179
  // Only mutable progress fields changed — a legitimate implement-phase
167
180
  // update via Write OR Edit.
@@ -171,12 +184,14 @@ try {
171
184
 
172
185
  logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
173
186
  block(
174
- `MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
175
- `This ${toolName} changes the frozen plan contract:\n` +
187
+ `MORPH-SPEC: '${filename}' write rejected — the '${requiredGate}' gate has been approved.\n` +
188
+ `This ${toolName} violates the plan contract or the tasks schema:\n` +
176
189
  violations.map(v => ` • ${v}`).join('\n') + '\n\n' +
177
- `During the implement phase only these fields may change: ${mutableFields.join(', ')}.\n` +
178
- `To change the plan itself, ask the user to revoke the '${requiredGate}' gate\n` +
179
- `in .morph/state.json, then re-present it for approval.`
190
+ `During the implement phase only these fields may change: ${mutableFields.join(', ')}\n` +
191
+ `(status must stay in the schema enum, and outputs that diverge from the plan\n` +
192
+ `require an explanatory 'notes' in the same edit).\n` +
193
+ `To change the plan itself, ask the user to revoke the '${requiredGate}' gate,\n` +
194
+ `then re-present it for approval.`
180
195
  );
181
196
  }
182
197
 
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tasks-json-guard.js — PostToolUse hook (validate-and-warn, never block)
4
+ *
5
+ * Event: PostToolUse | Matcher: Write|Edit
6
+ *
7
+ * After any Write/Edit that touches a tasks.json under .morph/features/,
8
+ * re-reads the file from disk and validates the schema (status enum, effort
9
+ * enum). Invalid → injects a warning so the LLM fixes the file immediately;
10
+ * valid → logs one activity line and stays silent.
11
+ *
12
+ * The production failure this guards: a leaf sub-agent wrote
13
+ * status:"completed" (not in the enum — the schema accepts "done") into
14
+ * tasks.json and nothing flagged it. The blocking pre-write validation for
15
+ * post-Gate-2 edits lives in protect-spec-files.js (PreToolUse); this
16
+ * PostToolUse pass also covers pre-gate authoring (morph-plan) and anything
17
+ * that landed on disk regardless.
18
+ *
19
+ * Fail-open: exits 0 on any error.
20
+ */
21
+
22
+ import { readFileSync } from 'fs';
23
+ import { isAbsolute, join } from 'path';
24
+ import { stateExists } from '../../shared/state-reader.js';
25
+ import { injectContext, pass } from '../../shared/hook-response.js';
26
+ import { logHookActivity } from '../../shared/activity-logger.js';
27
+ import { getFilePath } from '../../shared/payload-utils.js';
28
+ import { isFeaturePath, getBasename } from '../../shared/phase-utils.js';
29
+ import { VALID_TASK_STATUS, validateTasksDoc } from '../../shared/tasks-validator.js';
30
+
31
+ function readStdin() {
32
+ try {
33
+ return JSON.parse(readFileSync(0, 'utf8') || '{}');
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ function projectDir() {
40
+ return process.env.CLAUDE_PROJECT_DIR || process.cwd();
41
+ }
42
+
43
+ try {
44
+ const cwd = projectDir();
45
+ if (!stateExists(cwd)) pass();
46
+
47
+ const payload = readStdin();
48
+ const filePath = getFilePath(payload);
49
+ if (!filePath) pass();
50
+ if (!isFeaturePath(filePath)) pass();
51
+ if (getBasename(filePath) !== 'tasks.json') pass();
52
+
53
+ const absPath = isAbsolute(filePath) ? filePath : join(cwd, filePath);
54
+
55
+ let doc;
56
+ try {
57
+ doc = JSON.parse(readFileSync(absPath, 'utf-8'));
58
+ } catch {
59
+ logHookActivity('tasks-json-guard', 'PostToolUse', 'invalid', cwd);
60
+ injectContext(
61
+ '⚠️ [morph-spec] tasks.json INVÁLIDO — o arquivo gravado não parseia como JSON. ' +
62
+ 'Corrija o arquivo antes de continuar.'
63
+ );
64
+ }
65
+
66
+ const { valid, errors } = validateTasksDoc(doc);
67
+ if (!valid) {
68
+ logHookActivity('tasks-json-guard', 'PostToolUse', 'invalid', cwd);
69
+ injectContext(
70
+ '⚠️ [morph-spec] tasks.json com schema inválido após o write:\n' +
71
+ errors.map(e => ` • ${e}`).join('\n') +
72
+ `\nStatus válidos: ${VALID_TASK_STATUS.join(', ')}. Corrija antes de marcar a task como done.`
73
+ );
74
+ }
75
+
76
+ logHookActivity('tasks-json-guard', 'PostToolUse', 'ok', cwd);
77
+ pass();
78
+ } catch {
79
+ // Fail-open
80
+ process.exit(0);
81
+ }