@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.
- package/README.md +2 -2
- package/bin/morph-spec.js +19 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +10 -4
- package/framework/MORPH.md +4 -4
- package/framework/evals/rubrics/condensed.md +49 -0
- package/framework/hooks/README.md +3 -3
- package/framework/hooks/claude-code/core/chain.js +123 -0
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +84 -77
- package/framework/hooks/claude-code/core/gate-guard.js +21 -25
- package/framework/hooks/claude-code/core/loop-detect.js +70 -64
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +94 -45
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +21 -15
- package/framework/hooks/claude-code/core/protect-spec-files.js +119 -103
- package/framework/hooks/claude-code/core/state-sync.js +32 -40
- package/framework/hooks/claude-code/core/tasks-json-guard.js +44 -44
- package/framework/hooks/claude-code/core/telemetry-log.js +151 -74
- package/framework/hooks/claude-code/core/trace-autogen.js +116 -6
- package/framework/hooks/claude-code/core/validate-completion.js +147 -107
- package/framework/hooks/claude-code/core/worktree-context.js +25 -22
- package/framework/hooks/claude-code/statusline.py +26 -4
- package/framework/hooks/shared/activity-logger.js +120 -69
- package/framework/hooks/shared/claim-registry.js +61 -1
- package/framework/hooks/shared/git-paths.js +94 -0
- package/framework/hooks/shared/hook-response.js +67 -0
- package/framework/hooks/shared/state-reader.js +95 -29
- package/framework/hooks/shared/state-writer.js +16 -0
- package/framework/rules/morph-task-tracking.md +4 -2
- package/framework/schemas/tasks.schema.json +10 -0
- 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 +40 -28
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +16 -13
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +26 -25
- package/framework/skills/core/morph-plan/SKILL.md +14 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/package.json +2 -2
- package/src/commands/observability/cost.js +137 -0
- package/src/commands/project/standards.js +220 -0
- package/src/commands/state/approve.js +30 -9
- package/src/dashboard/scanner/hooks-scanner.js +11 -2
- package/src/lib/installers/memory-installer.js +1 -1
- package/src/lib/tasks/tasks-json-parser.js +26 -0
- package/src/lib/telemetry/telemetry-reader.js +213 -0
- package/src/utils/hooks-installer.js +84 -79
- 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.
|
|
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.
|
|
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();
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -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
|
-
-
|
|
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
|
|
package/framework/MORPH.md
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
|
|
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);
|