@polymorphism-tech/morph-spec 8.15.1 → 8.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +19 -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 +10 -4
  7. package/framework/MORPH.md +4 -4
  8. package/framework/evals/rubrics/condensed.md +49 -0
  9. package/framework/hooks/README.md +3 -3
  10. package/framework/hooks/claude-code/core/chain.js +123 -0
  11. package/framework/hooks/claude-code/core/enforce-phase-writes.js +84 -77
  12. package/framework/hooks/claude-code/core/gate-guard.js +21 -25
  13. package/framework/hooks/claude-code/core/loop-detect.js +70 -64
  14. package/framework/hooks/claude-code/core/post-edit-typecheck.js +94 -45
  15. package/framework/hooks/claude-code/core/pre-bash-denylist.js +21 -15
  16. package/framework/hooks/claude-code/core/protect-spec-files.js +119 -103
  17. package/framework/hooks/claude-code/core/state-sync.js +32 -40
  18. package/framework/hooks/claude-code/core/tasks-json-guard.js +44 -44
  19. package/framework/hooks/claude-code/core/telemetry-log.js +151 -74
  20. package/framework/hooks/claude-code/core/trace-autogen.js +116 -6
  21. package/framework/hooks/claude-code/core/validate-completion.js +147 -107
  22. package/framework/hooks/claude-code/core/worktree-context.js +25 -22
  23. package/framework/hooks/claude-code/statusline.py +26 -4
  24. package/framework/hooks/shared/activity-logger.js +120 -69
  25. package/framework/hooks/shared/claim-registry.js +61 -1
  26. package/framework/hooks/shared/git-paths.js +94 -0
  27. package/framework/hooks/shared/hook-response.js +67 -0
  28. package/framework/hooks/shared/state-reader.js +95 -29
  29. package/framework/hooks/shared/state-writer.js +16 -0
  30. package/framework/rules/morph-task-tracking.md +4 -2
  31. package/framework/schemas/tasks.schema.json +10 -0
  32. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  33. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  34. package/framework/skills/core/morph-apply/SKILL.md +40 -28
  35. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  36. package/framework/skills/core/morph-eval/SKILL.md +16 -13
  37. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  38. package/framework/skills/core/morph-implement/SKILL.md +26 -25
  39. package/framework/skills/core/morph-plan/SKILL.md +14 -3
  40. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  41. package/framework/skills/core/morph-review/SKILL.md +1 -1
  42. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  43. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  44. package/package.json +2 -2
  45. package/src/commands/observability/cost.js +137 -0
  46. package/src/commands/project/standards.js +220 -0
  47. package/src/commands/state/approve.js +30 -9
  48. package/src/dashboard/scanner/hooks-scanner.js +11 -2
  49. package/src/lib/installers/memory-installer.js +1 -1
  50. package/src/lib/tasks/tasks-json-parser.js +26 -0
  51. package/src/lib/telemetry/telemetry-reader.js +213 -0
  52. package/src/utils/hooks-installer.js +84 -79
  53. package/framework/hooks/shared/session-snapshot.js +0 -105
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:** 8.15.1
6
+ **Version:** 8.17.0
7
7
  **Requires:** Node.js 22+, Claude Code
8
8
 
9
9
  ---
@@ -368,4 +368,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
368
368
 
369
369
  ---
370
370
 
371
- *morph-spec v8.15.1 by [Polymorphism Tech](https://polymorphism.tech)*
371
+ *morph-spec v8.17.0 by [Polymorphism Tech](https://polymorphism.tech)*
package/bin/morph-spec.js CHANGED
@@ -26,6 +26,7 @@ import { statusCommand } from '../src/commands/project/status.js';
26
26
  import { worktreeSetupCommand, worktreeLinkCommand, worktreeListCommand } from '../src/commands/project/worktree.js';
27
27
  import { fleetCommand } from '../src/commands/project/fleet.js';
28
28
  import { claimCommand } from '../src/commands/project/claim.js';
29
+ import { standardsCommand } from '../src/commands/project/standards.js';
29
30
  import { reportCheckCommand } from '../src/commands/state/report-check.js';
30
31
 
31
32
  // === Approval gates (human-only) ===
@@ -63,6 +64,7 @@ import { dashboardCommand } from '../src/commands/dashboard/dashboard.js';
63
64
 
64
65
  // === Observability ===
65
66
  import { telemetryCommand } from '../src/commands/observability/telemetry.js';
67
+ import { costCommand } from '../src/commands/observability/cost.js';
66
68
 
67
69
  const __dirname = dirname(fileURLToPath(import.meta.url));
68
70
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -380,6 +382,16 @@ program
380
382
  .option('--json', 'Output as JSON')
381
383
  .action((feature, options) => claimCommand(feature, options));
382
384
 
385
+ // ─── Standards search (server-side, R2 token cut) ─────────────────────────
386
+ program
387
+ .command('standards')
388
+ .description('Search STANDARDS.json server-side (top 3, full digest) instead of reading the whole registry. No --find: list categories + counts')
389
+ .option('--find <query>', 'Search query — matches name/id, tags/aliases, category, digest')
390
+ .option('--category <category>', 'Restrict --find to a single category')
391
+ .option('--tags <tags>', 'Restrict --find to standards carrying any of these comma-separated tags')
392
+ .option('--json', 'Output as JSON')
393
+ .action((options) => standardsCommand(options));
394
+
383
395
  // ─── Sub-agent report protocol ────────────────────────────────────────────
384
396
  program
385
397
  .command('report-check <feature> [task]')
@@ -404,4 +416,11 @@ program
404
416
  .option('--limit <n>', 'How many recent prompts to show (default 10)', (v) => parseInt(v, 10))
405
417
  .action((options) => telemetryCommand(options));
406
418
 
419
+ program
420
+ .command('cost [feature]')
421
+ .description('Cost-facing view over .morph/logs/events.jsonl: task duration, bytes re-ingested by kind (proxy, not tokens), dispatch counts')
422
+ .option('--by <mode>', 'Group by: task|phase (default: feature-wide summary)')
423
+ .option('--json', 'Output the aggregate as JSON')
424
+ .action((feature, options) => costCommand(feature, options));
425
+
407
426
  program.parse();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "morph-spec",
3
- "version": "8.15.1",
3
+ "version": "8.17.0",
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",
@@ -185,4 +185,4 @@ Never edited directly — go through CLI commands or `morph-spec update` instead
185
185
 
186
186
  ---
187
187
 
188
- *morph-spec v8.15.1 by Polymorphism Tech*
188
+ *morph-spec v8.17.0 by Polymorphism Tech*
@@ -249,4 +249,4 @@ morph-spec doctor
249
249
 
250
250
  ---
251
251
 
252
- *morph-spec v8.15.1 by Polymorphism Tech*
252
+ *morph-spec v8.17.0 by Polymorphism Tech*
@@ -15,10 +15,10 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
15
15
 
16
16
  **NUNCA:**
17
17
  - Pular para código sem proposal aprovado (Gate 1) e plano aprovado (Gate 2).
18
- - Modificar o contrato do plano depois do Gate 2: `spec.md`, `mandate.md`, e os campos de plano de `tasks.json` (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`) estão congelados. **Os use cases (`2-plan/usecases/UC-*.md`) NÃO estão** — são editáveis por design, é assim que se muda o comportamento e se re-roda `implement`; a auditoria fica no git.
18
+ - Modificar o contrato do plano depois do Gate 2: `spec.md`, `mandate.md`, e os campos de plano de `tasks.json` (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`, `agent`, `standards`) estão congelados. **Os use cases (`2-plan/usecases/UC-*.md`) NÃO estão** — são editáveis por design, é assim que se muda o comportamento e se re-roda `implement`; a auditoria fica no git.
19
19
  - Escrever critérios de aceite no `proposal.md`. O comportamento verificável mora nos use cases, cuja pós-condição precisa ser **observável em teste** e chega **literal** ao `doneCriteria` da task.
20
20
  - Editar `.morph/specs/usecases/` à mão. O acervo só se escreve pela promoção do `finish`, que lê de `2-plan/usecases/` — é ela que carimba `status: implemented` e sobrescreve por ID. Para mudar um UC existente (o caso `bug`), copie-o para o `2-plan/usecases/` da feature e corrija lá.
21
- - Tocar os campos de contrato do plano em `tasks.json` após o Gate 2 (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`) — o hook `protect-spec-files` faz diff semântico e bloqueia mudanças neles, seja via `Write` ou `Edit`. (Os campos mutáveis `status`/`outputs`/`notes` continuam graváveis por `Write` ou `Edit` — ver SEMPRE.)
21
+ - Tocar os campos de contrato do plano em `tasks.json` após o Gate 2 (`title`, `description`, `dependencies`, `effort`, `doneCriteria`, `group`, `agent`, `standards`) — o hook `protect-spec-files` faz diff semântico e bloqueia mudanças neles, seja via `Write` ou `Edit`. (Os campos mutáveis `status`/`outputs`/`notes` continuam graváveis por `Write` ou `Edit` — ver SEMPRE.)
22
22
  - Listar perguntas em texto simples — sempre use a tool `AskUserQuestion` (1-4 perguntas por chamada).
23
23
  - Usar `Guid.NewGuid()`, classes não-sealed sem justificativa, exceptions como fluxo de controle, camada de Service/Manager entre handler e dados, repository genérico (`IRepository<T>`/`IUnitOfWork`), ou SQL cru sem justificativa.
24
24
 
@@ -29,7 +29,12 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
29
29
  - Apresentar gates com o template fixo definido em MORPH.md / morph-plan. **A pergunta PRECISA nomear a feature** ("Gate 2 — Plano de {feature} aprovado?"): é por esse nome que o hook `state-sync` atribui a aprovação ao `feature.json` certo, na árvore certa. Sem nome, e havendo mais de uma feature na árvore, ele **recusa a gravar** e avisa — adivinhar já carimbou um Gate 2 na feature errada. Nesse caso aprove com `morph-spec approve <feature> <gate>`.
30
30
  - Registrar decisões não triviais em `decisions.md`.
31
31
  - 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.
32
- - Incluir as 3 cláusulas do contrato do relatório final em todo dispatch de sub-agent (**o sub-agent GRAVA o relatório em arquivo canônico ANTES de encerrar — `.morph/features/{f}/3-implement/reports/{taskId}.md`, ou `4-review/evaluator-report.md` para o avaliador — com os 4 itens obrigatórios (o quê / arquivos / problemas / auto-score 0-10); esse arquivo é o valor de retorno autoritativo, a última mensagem de texto é cortesia best-effort e pode não chegar inteira; ainda assim não chamar `SendMessage`, que gera um evento a mais e custa um turno inútil**; 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. **O orquestrador lê o arquivo ao retomar controle** — nunca confia só na mensagem de retorno da tool.
32
+ - **Contrato do relatório final** em todo dispatch de sub-agent:
33
+ 1. Grava o relatório em arquivo canônico ANTES de encerrar — `.morph/features/{f}/3-implement/reports/{taskId}.md` (avaliador: `4-review/evaluator-report.md`) — com os 4 itens obrigatórios (o quê / arquivos / problemas / auto-score 0-10). Esse arquivo é o retorno autoritativo, não a mensagem final; nunca chamar `SendMessage`.
34
+ 2. Não tocar `tasks.json`/`feature.json`/`state.json` nem o board de sessão — estado é do orquestrador.
35
+ 3. Sem build/test da suíte compartilhada em batch paralelo sem isolamento (verificação é do orquestrador, pós-batch).
36
+
37
+ Racional completo: `MORPH.md` §6. Templates curtos: `morph-implement` §4, `morph-apply` §3b. **O orquestrador lê o arquivo ao retomar controle** — nunca confia só na mensagem de retorno da tool.
33
38
 
34
39
  ## Slash commands
35
40
 
@@ -44,7 +49,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
44
49
  | `/morph-troubleshoot [erro]` | Diagnóstico de erros .NET/Next.js com root-cause analysis |
45
50
 
46
51
  **Comandos CLI de gate e de estado** (a LLM os usa em vez de editar o estado à mão):
47
- - `morph-spec approve <feature> <gate> --mode auto` — aprova um gate (`proposal`/`plan`/`review`) em `trust: auto`. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate).
52
+ - `morph-spec approve <feature> <gate> --mode auto` — aprova um gate (`proposal`/`plan`/`review`) em `trust: auto`. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
48
53
  - `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).
49
54
  - `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.
50
55
  - `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` 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, `--force` dispensa o Gate 3, `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados.
@@ -63,6 +68,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
63
68
  - `morph-spec report-check <feature> [task] [--json]` — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez). Protocolo completo em MORPH.md §6.
64
69
  - `morph-spec claim [feature] [--release] [--list] [--json]` — posse **advisory** de feature. Cada sessão reclama sua feature ativa automaticamente (hook SessionStart) e renova o heartbeat a cada tool call; o registro vive em `<git-common-dir>/morph-spec/claims.json`, compartilhado por todos os worktrees. **`morph-spec claim <feature>` (sem flags) ADQUIRE** — se esta árvore já tinha claim vivo em outra feature, ele é liberado no mesmo passo (o CLI reporta "movido de X para Y"); é o jeito de trocar de feature no meio da tarefa sem o dança de dois passos `--release` + reclamar. `--list` força a leitura sem adquirir mesmo passando uma feature. **Claims avisam, nunca bloqueiam** — mas se o SessionStart avisar que outra sessão tem claim vivo, **confirme antes de `finish`/`archive`/`delete`**: duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent (o worktree sumiu debaixo dele). Claim órfão de sessão morta: `morph-spec claim <f> --release`.
65
70
  - `morph-spec telemetry [--json] [--feature <f>] [--limit <n>]` — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, personas despachadas + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log` em todo tool call e todo UserPromptSubmit.
71
+ - `morph-spec cost [feature] [--by task|phase] [--json]` — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), bytes de contexto re-ingerido por tipo (`rubric`/`standard`/`skill`/`agents`/`plan-artifact`/`code`), dispatches + tamanho de prompt e mix de tools. Bytes re-ingeridos são **proxy determinística** de custo de token (tokens reais não são observáveis por hook). Use antes/depois de mudar skill ou fluxo para medir o efeito.
66
72
 
67
73
  ## Skills core (em `.claude/skills/`)
68
74
 
@@ -75,7 +75,7 @@ Anti-padrão: o Specialist não cria novas tarefas. Descobertas de escopo durant
75
75
 
76
76
  **Evaluator** — ativo após uma tarefa ou fase ser concluída.
77
77
  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).
78
- 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. **A regra é enforced server-side:** `morph-spec approve {feature} review` recusa (exit 1) enquanto `4-review/evaluator-report.md` não existir. Se o dispatch for genuinamente impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` não dispensa a regra (é só para ordem de gate).
78
+ 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. **A regra é enforced server-side:** `morph-spec approve {feature} review` recusa (exit 1) enquanto `4-review/evaluator-report.md` não existir. Se o dispatch for genuinamente impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` não dispensa a regra (é só para ordem de gate). **Exceção `hotfix`:** o tipo nunca despacha o avaliador (velocidade é o ponto do hotfix); o Gate 3 exige `4-review/review-report.md` — o artefato que a `morph-hotfix` de fato produz — e a assinatura humana continua obrigatória (§2.1/§5), sem afrouxar nada para os demais tipos.
79
79
  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.
80
80
 
81
81
  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.
@@ -199,7 +199,7 @@ O índice de standards fica em `framework/standards/STANDARDS.json`. Cada entrad
199
199
 
200
200
  **Fluxo de consulta:**
201
201
 
202
- 1. Antes de iniciar uma tarefa, identifique de 1 a 3 standards relevantes pelo `id` ou `tags`.
202
+ 1. Antes de iniciar uma tarefa, identifique de 1 a 3 standards relevantes — rode `morph-spec standards --find "<domínio/tecnologia da task>"` (o comando devolve o top 3 como `id · path · digest`, sem você abrir o índice inteiro) em vez de filtrar `id`/`tags` à mão no JSON.
203
203
  2. Leia apenas o arquivo `.md` daquele standard específico — o path está no campo `path` do JSON.
204
204
  3. Aplique o standard durante a implementação. Se precisar de contexto adicional, leia mais um standard.
205
205
  4. Registre no recap.md qual standard foi consultado e como foi aplicado.
@@ -246,7 +246,7 @@ Gates são pontos de **pausa potencial**. Em `trust: "auto"` (default), gates **
246
246
  - Sinais: `evalScore` composite, `standardsViolations` detectadas em implementation.
247
247
  - Skill responsável: `morph-review`.
248
248
 
249
- **Gates por tipo de trabalho:** `chore` e `hotfix` não passam pelos Gates 1 e 2 — eles são carimbados por política no nascimento (`approvedBy: "policy:{tipo}"`, nunca `"auto"`, para não colidir com o gate-guard). A barra de qualidade do morph-eval e o Gate 3 continuam valendo para todos os tipos. Para `hotfix`, o Gate 3 é **sempre humano**: mesmo em `trust: auto` a `morph-hotfix` apresenta o review via `AskUserQuestion` e o `risk-detector` força a pausa; o backstop server-side (`morph-spec approve ... review --mode auto` recusa) garante que nenhum caminho auto passe por cima.
249
+ **Gates por tipo de trabalho:** `chore` e `hotfix` não passam pelos Gates 1 e 2 — eles são carimbados por política no nascimento (`approvedBy: "policy:{tipo}"`, nunca `"auto"`, para não colidir com o gate-guard). A barra de qualidade do morph-eval e o Gate 3 continuam valendo para todos os tipos. Para `hotfix`, o Gate 3 é **sempre humano**: mesmo em `trust: auto` a `morph-hotfix` apresenta o review via `AskUserQuestion` e o `risk-detector` força a pausa; o backstop server-side (`morph-spec approve ... review --mode auto` recusa) garante que nenhum caminho auto passe por cima. O artefato exigido também difere: `hotfix` nunca despacha o avaliador independente, então o Gate 3 exige `4-review/review-report.md` em vez de `evaluator-report.md`; todos os outros tipos continuam exigindo o `evaluator-report.md` (ou `--self-assessed` registrado).
250
250
 
251
251
  Para alterar o modo: editar `.morph/config/config.json → trust` (`"auto"` ou `"manual"`). Configs sem o campo `trust` defaultam para `"auto"`.
252
252
 
@@ -402,4 +402,4 @@ Nenhuma skill deve ser invocada proativamente por um hook ou script automático.
402
402
  > Alterações de comportamento do harness exigem atualização aqui, não em hooks ou scripts.
403
403
  > Versão inicial criada em 2026-04-24. Próxima revisão recomendada após o ciclo completo com NLH.
404
404
  >
405
- > Para histórico de decisões sobre este documento, consulte os commits do arquivo e o `harness-token-audit.md` na raiz do repositório.
405
+ > Para histórico de decisões sobre este documento, consulte os commits do arquivo.
@@ -0,0 +1,49 @@
1
+ # Rubrica Condensada — Eval Inline por Task
2
+
3
+ Versão compacta das 4 rubricas completas de `framework/evals/rubrics/`, para o eval
4
+ **inline por task** (`morph-eval` §2, escopo tarefa única). Mesmas dimensões, mesmos
5
+ pesos, mesma régua — só menos texto por banda.
6
+
7
+ > **O Gate 3 NÃO usa esta condensada.** O avaliador independente da `morph-review`
8
+ > (persona `evaluator`, contexto fresco) sempre lê as 4 rubricas completas — a régua
9
+ > do juiz de fase não muda. Esta condensada só corta releitura no loop intra-task.
10
+
11
+ ## Verificações mecânicas obrigatórias — auto-reprovação (teto 2)
12
+
13
+ Se o diff toca endpoint minimal API ou fetch cross-camada, independente do resto:
14
+
15
+ - Serviço injetado na lambda do endpoint sem `AddScoped`/`AddSingleton`/`AddTransient` em `Program.cs`.
16
+ - Chamada server-direct e client-side proxy pro mesmo recurso reusando a mesma constante de prefixo.
17
+
18
+ ## Dimensões e pesos
19
+
20
+ | Dimensão | Peso |
21
+ |---|---|
22
+ | Arquitetura | 30% |
23
+ | Contratos | 25% |
24
+ | Qualidade de código | 25% |
25
+ | Cobertura de testes | 20% |
26
+
27
+ ## Bandas por dimensão
28
+
29
+ | Dimensão | 1-3 | 4-6 | 7-8 | 9-10 |
30
+ |---|---|---|---|---|
31
+ | **Arquitetura** | Ignora a arquitetura do spec.md; sem feature folder; deps circulares; ou violação mecânica acima | Pastas corretas, responsabilidades se misturam; deps cross-feature sem justificativa; domain acoplado a infra | Desvios menores justificados; deps cross-feature via abstração; impurezas de infra pontuais | Feature folder rigoroso; zero deps cross-feature diretas; domain isolado; DI onde exige |
32
+ | **Contratos** | Sem tipos ou `any` generalizado; zero validação; sem tratamento de erro; naming arbitrário | `any`/genérico visível; validação parcial; mix exceptions/Result; naming inconsistente | Tipos bem definidos, `any` raro e justificado; validação na maioria; erro consistente | Zero `any`; validação em todos endpoints; Result pattern/discriminated union; naming consistente |
33
+ | **Qualidade** | Nomes arbitrários; god objects; duplicação massiva; fora do padrão do stack | Nomes genéricos (`data`,`temp`); funções >30 linhas; duplicação perceptível | Nomes claros, raras exceções; bem dimensionado; duplicação mínima | Nomes comunicam intenção; funções ≤20-25 linhas, 1 responsabilidade; zero duplicação/dead code |
34
+ | **Testes** | Sem testes ou sem asserts reais; zero edge case; nomes genéricos | Happy path OK, edge cases parciais; erros só óbvios; testes interdependentes | Happy path completo; maioria dos edge cases; erros nos fluxos principais | Toda pós-condição do `usecase` tem teste; A1/A2 cobertos; edge+erro cobertos; isolados |
35
+
36
+ > Task com `usecase: UC-nn`: pós-condição sem teste **impede a faixa 9-10** em Testes, mesmo com o resto da suíte completo (herdado de `test-coverage.md`).
37
+
38
+ ## Score composto
39
+
40
+ ```
41
+ score = (arquitetura × 0.30) + (contratos × 0.25) + (qualidade × 0.25) + (testes × 0.20)
42
+ ```
43
+
44
+ Arredonde para uma casa decimal. ≥9 aprova em silêncio; <9 alimenta o loop de correção do `morph-apply`.
45
+
46
+ ## Quando abrir a completa em vez desta
47
+
48
+ Dúvida de banda, violação mecânica suspeita, ou escopo de fase (Gate 3) — abra
49
+ `framework/evals/rubrics/{dimension}.md`. Esta condensada é o atalho do caso comum.
@@ -46,15 +46,15 @@ framework/hooks/
46
46
  | **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
47
  | **PreToolUse** (Write\|Edit) | enforce-phase-writes.js | Block | Ensures writes go to current phase directory |
48
48
  | **PreToolUse** (Bash) | pre-bash-denylist.js | Block | Blocks `rm -rf .morph/` and direct state edits |
49
- | **PostToolUse** (Write\|Edit) | post-edit-typecheck.js | Advisory | Debounced `tsc --noEmit` after TS/TSX edits; silent on success |
49
+ | **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
50
  | **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
51
  | **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 |
52
52
  | **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state updates (feature register, output tracking, gate approval) |
53
- | **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) |
53
+ | **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.jsonl` (issue #63) |
54
54
  | **PostToolUse** (all tools) | telemetry-log.js | Telemetry | Appends every tool call (skill, sub-agent dispatch + prompt, knowledge-base read, bash/edit) to `.morph/logs/events.jsonl`; silent, fail-open — the NLH execution-path capture read back by `morph-spec telemetry` |
55
55
  | **PostToolUse** (all tools) | loop-detect.js | Advisory | Warns when the last 8 actions collapse to ≤2 unique signatures |
56
56
  | **UserPromptSubmit** | telemetry-log.js | Telemetry | Appends the user's prompt to the same `events.jsonl` stream; silent, fail-open |
57
- | **Stop** | validate-completion.js | Advisory | Warns about incomplete tasks/missing outputs/recap drift; build check; session snapshot |
57
+ | **Stop** | validate-completion.js | Advisory | Warns about incomplete tasks/missing outputs/recap drift (hash-deduped, only re-injects when the warning set changes); dirty-flag-gated build check |
58
58
 
59
59
  ## Design Principles
60
60
 
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * chain.js — roda vários hooks core num único processo node.
4
+ *
5
+ * Motivação (onda de performance R2): cada hook registrado no Claude Code é
6
+ * hoje um processo node próprio (~60ms de cold start). Um único Write/Edit
7
+ * disparava até 9 desses processos — 2 PreToolUse + 3+2+2 PostToolUse. Este
8
+ * script agrupa os elos de um mesmo event+matcher (a mesma entrada do
9
+ * registry em src/utils/hooks-installer.js) num só spawn: lê stdin UMA vez,
10
+ * importa e chama `run(payload)` de cada hook em sequência, e combina as
11
+ * respostas — reduzindo esses 9 spawns para 4 por Write/Edit (1 PreToolUse +
12
+ * 3 PostToolUse: Write|Edit / Write|Edit|AskUserQuestion / matcher-`*`).
13
+ *
14
+ * Uso: `node chain.js <hook1> <hook2> ...` — nomes de arquivo SEM `.js`,
15
+ * resolvidos no mesmo diretório deste script (core/), na ORDEM em que devem
16
+ * rodar. A ordem é a mesma que o registry antigo (duas entries separadas)
17
+ * já implicava — ver o comentário de cada grupo em hooks-installer.js.
18
+ *
19
+ * Cada hook core exporta `run(payload)` (ver framework/hooks/shared/hook-response.js
20
+ * `runStandalone`) — o arquivo continua executável standalone (spawnSync
21
+ * direto, como os testes já fazem) porque a própria cauda do arquivo só lê
22
+ * stdin/imprime/sai quando NÃO está sendo importado por esta cadeia.
23
+ *
24
+ * Regras de combinação:
25
+ * - PreToolUse (decision:'block'): o PRIMEIRO elo que retornar um bloqueio
26
+ * VENCE e encerra a cadeia — os elos seguintes nem chegam a rodar. Isto é
27
+ * uma otimização NOVA desta consolidação (antes, Claude Code podia
28
+ * disparar os dois processos de qualquer forma); o resultado observável
29
+ * pro Claude Code é idêntico (o tool call é bloqueado do mesmo jeito).
30
+ * - PostToolUse / outros eventos de contexto: os textos de TODOS os elos que
31
+ * retornarem `hookSpecificOutput.additionalContext` (ou `decision:'approve'`
32
+ * com additionalContext) são concatenados, na ORDEM dos argv, separados por
33
+ * uma linha em branco — nenhum elo é pulado por um vizinho ter respondido.
34
+ * - Nome do evento (hookSpecificOutput.hookEventName) no resultado final:
35
+ * o valor que o PRIMEIRO elo a declarar um venceu (mesma regra de
36
+ * "primeiro vence" do bloqueio acima — na prática todos os elos de um
37
+ * mesmo grupo observam o MESMO evento real do Claude Code, então isto só
38
+ * importa como desempate determinístico); na ausência de qualquer elo
39
+ * declarando, cai para `payload.hook_event_name`; por último, `'PostToolUse'`.
40
+ * - Fail-open POR ELO: uma exceção não tratada por um hook individual (import
41
+ * falhou, run() não existe, etc.) não derruba a cadeia — os elos seguintes
42
+ * ainda rodam. Cada hook já fail-opens sozinho dentro do seu próprio
43
+ * try/catch; isto é a segunda rede de segurança, para o import em si.
44
+ */
45
+
46
+ import { dirname, join } from 'path';
47
+ import { fileURLToPath, pathToFileURL } from 'url';
48
+ import { readStdin } from '../../shared/stdin-reader.js';
49
+
50
+ const __dirname = dirname(fileURLToPath(import.meta.url));
51
+
52
+ // Marca "já estou dentro de uma cadeia" para os hooks importados abaixo: a
53
+ // cauda de execução standalone de cada hook (runStandalone() em
54
+ // hook-response.js) só dispara quando esta flag NÃO está setada. Setada
55
+ // ANTES do primeiro import dinâmico — cada hook importado herda o mesmo
56
+ // process.env deste processo.
57
+ process.env.MORPH_HOOK_CHAINED = '1';
58
+
59
+ // Override só para testes da mecânica genérica desta cadeia (fixtures fora
60
+ // de core/, sem precisar ser hooks reais nem satisfazer as convenções que
61
+ // hook-wiring.test.js exige de todo arquivo em core/). Produção nunca seta
62
+ // isto — resolve sempre no próprio diretório deste script.
63
+ const coreDir = process.env.MORPH_HOOK_CORE_DIR || __dirname;
64
+
65
+ /** Extrai o texto de contexto de um resultado de hook, se houver. */
66
+ function contextOf(result) {
67
+ if (!result) return null;
68
+ if (result.decision === 'approve' && typeof result.additionalContext === 'string') {
69
+ return result.additionalContext;
70
+ }
71
+ const ctx = result?.hookSpecificOutput?.additionalContext;
72
+ return typeof ctx === 'string' && ctx ? ctx : null;
73
+ }
74
+
75
+ async function main() {
76
+ const names = process.argv.slice(2);
77
+ const payload = await readStdin();
78
+
79
+ const contexts = [];
80
+ let eventName = null;
81
+
82
+ for (const name of names) {
83
+ let result = null;
84
+ try {
85
+ const mod = await import(pathToFileURL(join(coreDir, `${name}.js`)).href);
86
+ result = await mod.run(payload);
87
+ } catch {
88
+ // Fail-open POR ELO — ver doc do módulo acima.
89
+ result = null;
90
+ }
91
+
92
+ if (!result) continue;
93
+
94
+ // PreToolUse: o primeiro bloqueio vence e encerra a cadeia.
95
+ if (result.decision === 'block') {
96
+ console.log(JSON.stringify(result));
97
+ process.exit(0);
98
+ }
99
+
100
+ const ctx = contextOf(result);
101
+ if (ctx) {
102
+ contexts.push(ctx);
103
+ // First link to declare a name wins (mirrors block-first-wins above) —
104
+ // in practice every link in a group observes the SAME real Claude Code
105
+ // event, so this only matters as a deterministic tie-break.
106
+ eventName = eventName || result?.hookSpecificOutput?.hookEventName;
107
+ }
108
+ }
109
+
110
+ if (contexts.length === 0) {
111
+ process.exit(0);
112
+ }
113
+
114
+ console.log(JSON.stringify({
115
+ hookSpecificOutput: {
116
+ hookEventName: eventName || payload?.hook_event_name || 'PostToolUse',
117
+ additionalContext: contexts.join('\n\n'),
118
+ },
119
+ }));
120
+ process.exit(0);
121
+ }
122
+
123
+ main().catch(() => process.exit(0));
@@ -18,7 +18,6 @@
18
18
  */
19
19
 
20
20
  import { existsSync } from 'fs';
21
- import { readStdin } from '../../shared/stdin-reader.js';
22
21
  import { stateExists, getFeaturePhase, isGateApproved } from '../../shared/state-reader.js';
23
22
  import {
24
23
  isFeaturePath,
@@ -40,88 +39,96 @@ const PHASE_DIR_ENTRY_GATE = {
40
39
  '3-implement': 'plan',
41
40
  '4-review': 'plan',
42
41
  };
43
- import { block, pass } from '../../shared/hook-response.js';
42
+ import { blockResult, runStandalone } from '../../shared/hook-response.js';
44
43
  import { logHookActivity } from '../../shared/activity-logger.js';
45
44
  import { getFilePath, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
46
45
 
47
- try {
48
- // Amend mode: bypass phase write enforcement for legitimate corrections
49
- if (process.env.MORPH_AMEND_PHASE) {
50
- console.error(`[morph-spec] AMEND MODE: bypassing phase protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
51
- pass();
52
- }
53
-
54
- const payload = await readStdin();
55
- if (!payload) pass();
56
-
57
- const filePath = getFilePath(payload);
58
- if (!filePath) pass();
59
-
60
- // Only check files inside .morph/features/
61
- if (!isFeaturePath(filePath)) pass();
62
-
63
- // Resolve the tree that OWNS this file, NOT the tree the session sits in.
64
- // A session rooted in the primary repo may legitimately write to a feature
65
- // living in a secondary worktree; reading the session's `.morph/` there
66
- // reported the ROOT's phase for the WORKTREE's file, and blocked a legal
67
- // `2-plan/` write with "not allowed during PROPOSAL phase". `stateExists`
68
- // must be asked of that same tree — a worktree carries its own state.
69
- const root = resolveMorphRoot(filePath, process.cwd());
70
- if (!stateExists(root)) pass();
71
-
72
- const featureName = extractFeatureName(filePath);
73
- if (!featureName) pass();
74
-
75
- const phase = getFeaturePhase(featureName, root);
76
- if (!phase) pass();
77
-
78
- // During implement phase, all writes are allowed (source code is unrestricted)
79
- if (phase === 'implement' || phase === 'sync') pass();
80
-
81
- const targetDir = extractPhaseDir(filePath);
82
- if (!targetDir) pass(); // File directly in feature root — allow
83
-
84
- const allowedDir = PHASE_DIRS[phase];
85
- if (!allowedDir) pass(); // Unknown phase — allow
86
-
87
- if (targetDir !== allowedDir) {
88
- // Allow editing already-existing files from previous phases
89
- const absolutePath = resolveTargetPath(filePath, root);
90
- if (existsSync(absolutePath)) {
91
- logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
92
- pass(); // editing existing file — allow
46
+ /**
47
+ * @param {object} payload - parsed PreToolUse stdin payload
48
+ * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
49
+ * Never prints, never exits — see runStandalone()/chain.js at the bottom.
50
+ */
51
+ export async function run(payload) {
52
+ try {
53
+ // Amend mode: bypass phase write enforcement for legitimate corrections
54
+ if (process.env.MORPH_AMEND_PHASE) {
55
+ console.error(`[morph-spec] AMEND MODE: bypassing phase protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
56
+ return null;
93
57
  }
94
58
 
95
- // Gate-aware forward transition: allow creating the NEXT phase's first
96
- // artifact as soon as the gate authorizing that phase is approved. Without
97
- // this, the first file of a new phase can never be written (the phase only
98
- // advances once that file exists) — the deadlock this hook used to create.
99
- const entryGate = PHASE_DIR_ENTRY_GATE[targetDir];
100
- if (entryGate && isGateApproved(featureName, entryGate, root)) {
101
- logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
102
- pass();
59
+ if (!payload) return null;
60
+
61
+ const filePath = getFilePath(payload);
62
+ if (!filePath) return null;
63
+
64
+ // Only check files inside .morph/features/
65
+ if (!isFeaturePath(filePath)) return null;
66
+
67
+ // Resolve the tree that OWNS this file, NOT the tree the session sits in.
68
+ // A session rooted in the primary repo may legitimately write to a feature
69
+ // living in a secondary worktree; reading the session's `.morph/` there
70
+ // reported the ROOT's phase for the WORKTREE's file, and blocked a legal
71
+ // `2-plan/` write with "not allowed during PROPOSAL phase". `stateExists`
72
+ // must be asked of that same tree — a worktree carries its own state.
73
+ const root = resolveMorphRoot(filePath, process.cwd());
74
+ if (!stateExists(root)) return null;
75
+
76
+ const featureName = extractFeatureName(filePath);
77
+ if (!featureName) return null;
78
+
79
+ const phase = getFeaturePhase(featureName, root);
80
+ if (!phase) return null;
81
+
82
+ // During implement phase, all writes are allowed (source code is unrestricted)
83
+ if (phase === 'implement' || phase === 'sync') return null;
84
+
85
+ const targetDir = extractPhaseDir(filePath);
86
+ if (!targetDir) return null; // File directly in feature root — allow
87
+
88
+ const allowedDir = PHASE_DIRS[phase];
89
+ if (!allowedDir) return null; // Unknown phase — allow
90
+
91
+ if (targetDir !== allowedDir) {
92
+ // Allow editing already-existing files from previous phases
93
+ const absolutePath = resolveTargetPath(filePath, root);
94
+ if (existsSync(absolutePath)) {
95
+ logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
96
+ return null; // editing existing file — allow
97
+ }
98
+
99
+ // Gate-aware forward transition: allow creating the NEXT phase's first
100
+ // artifact as soon as the gate authorizing that phase is approved. Without
101
+ // this, the first file of a new phase can never be written (the phase only
102
+ // advances once that file exists) — the deadlock this hook used to create.
103
+ const entryGate = PHASE_DIR_ENTRY_GATE[targetDir];
104
+ if (entryGate && isGateApproved(featureName, entryGate, root)) {
105
+ logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
106
+ return null;
107
+ }
108
+
109
+ const phaseLabel = phase.toUpperCase();
110
+ // Name the tree the verdict came from whenever it is NOT the session's own.
111
+ // The phase read is per-tree, so "which .morph/ said proposal?" is the first
112
+ // question anyone debugging this asks.
113
+ const treeNote = root !== process.cwd().replace(/\\/g, '/')
114
+ ? `\n(phase read from the tree that owns the file: ${root})`
115
+ : '';
116
+ logHookActivity('enforce-phase-writes', 'PreToolUse', 'blocked');
117
+ return blockResult(
118
+ `MORPH-SPEC: Writing to '${targetDir}/' is not allowed during ${phaseLabel} phase.\n` +
119
+ `Current phase '${phase}' only permits writes to '${allowedDir}/'.${treeNote}\n\n` +
120
+ `To move forward, approve the current gate and scaffold the next phase in\n` +
121
+ `one step: morph-spec advance ${featureName}\n` +
122
+ `(run it in that tree — the gate is recorded in its own feature.json).`
123
+ );
103
124
  }
104
125
 
105
- const phaseLabel = phase.toUpperCase();
106
- // Name the tree the verdict came from whenever it is NOT the session's own.
107
- // The phase read is per-tree, so "which .morph/ said proposal?" is the first
108
- // question anyone debugging this asks.
109
- const treeNote = root !== process.cwd().replace(/\\/g, '/')
110
- ? `\n(phase read from the tree that owns the file: ${root})`
111
- : '';
112
- logHookActivity('enforce-phase-writes', 'PreToolUse', 'blocked');
113
- block(
114
- `MORPH-SPEC: Writing to '${targetDir}/' is not allowed during ${phaseLabel} phase.\n` +
115
- `Current phase '${phase}' only permits writes to '${allowedDir}/'.${treeNote}\n\n` +
116
- `To move forward, approve the current gate and scaffold the next phase in\n` +
117
- `one step: morph-spec advance ${featureName}\n` +
118
- `(run it in that tree — the gate is recorded in its own feature.json).`
119
- );
126
+ logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
127
+ return null;
128
+ } catch {
129
+ // Fail-open
130
+ return null;
120
131
  }
121
-
122
- logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
123
- pass();
124
- } catch {
125
- // Fail-open
126
- process.exit(0);
127
132
  }
133
+
134
+ await runStandalone(run);