@polymorphism-tech/morph-spec 7.6.0 → 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/morph-spec.js +18 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +12 -4
- package/framework/MORPH.md +18 -10
- package/framework/agents.json +12 -4
- package/framework/hooks/README.md +8 -10
- package/framework/hooks/claude-code/core/gate-guard.js +36 -16
- package/framework/hooks/claude-code/core/protect-spec-files.js +20 -5
- package/framework/hooks/claude-code/core/tasks-json-guard.js +81 -0
- package/framework/hooks/shared/tasks-validator.js +68 -0
- package/framework/mcp/registry.json +5 -48
- package/framework/rubrics/backend.json +3 -3
- package/framework/rubrics/default.json +3 -3
- package/framework/rubrics/frontend.json +3 -3
- 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 +53 -40
- package/framework/skills/core/morph-brainstorming/SKILL.md +7 -12
- package/framework/skills/core/morph-eval/SKILL.md +10 -8
- package/framework/skills/core/morph-implement/SKILL.md +27 -21
- package/framework/skills/core/morph-plan/SKILL.md +12 -21
- package/framework/skills/core/morph-proposal/SKILL.md +3 -8
- package/framework/skills/core/morph-review/SKILL.md +30 -14
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/integration/mcp/mcp-tools.md +38 -43
- package/package.json +1 -1
- package/src/commands/project/doctor.js +36 -1
- package/src/commands/state/approve.js +29 -4
- package/src/commands/state/score.js +88 -0
- package/src/commands/tasks/dag.js +68 -0
- package/src/dashboard/scanner/rubrics-scanner.js +1 -1
- package/src/lib/generators/recap-generator.js +1 -1
- package/src/lib/tasks/dispatch-planner.js +201 -0
- package/src/lib/validators/score-aggregator.js +3 -3
- package/src/scripts/setup-infra.js +42 -0
- package/src/utils/agents-installer.js +142 -10
- package/src/utils/hooks-installer.js +13 -5
- 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
|
+
**Version:** 7.7.0
|
|
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.
|
|
368
|
+
*morph-spec v7.7.0 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]')
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
package/framework/MORPH.md
CHANGED
|
@@ -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
|
|
22
|
-
| Sub-agent com auto-score < 7 |
|
|
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
|
|
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
|
|
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
|
|
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** (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/framework/agents.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.
|
|
2
|
+
"version": "5.1.0",
|
|
3
3
|
"schema": "domain-personas",
|
|
4
4
|
"total_agents": 7,
|
|
5
|
-
"description": "Domain personas dispatched per task
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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) |
|
|
50
|
-
| **PostToolUse** (Write\|Edit
|
|
51
|
-
| **PostToolUse** (Write\|Edit
|
|
52
|
-
| **PostToolUse** (
|
|
53
|
-
| **
|
|
54
|
-
| **
|
|
55
|
-
| **
|
|
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
|
|
12
|
-
*
|
|
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
|
-
*
|
|
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
|
|
104
|
-
*
|
|
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
|
-
|
|
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"
|
|
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
|
|
138
|
-
// state.json
|
|
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
|
-
|
|
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}'
|
|
175
|
-
`This ${toolName}
|
|
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(', ')}
|
|
178
|
-
`
|
|
179
|
-
`
|
|
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
|
+
}
|