@polymorphism-tech/morph-spec 8.40.0 → 8.42.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/CLAUDE.md +7 -5
- package/README.md +3 -3
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +9 -8
- package/framework/agents.json +14 -8
- package/framework/docs/CLI.md +5 -3
- package/framework/hooks/README.md +2 -1
- package/framework/hooks/claude-code/core/chain.js +2 -0
- package/framework/hooks/claude-code/core/comment-guard.js +107 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
- package/framework/hooks/claude-code/core/worktree-create.js +22 -6
- package/framework/hooks/shared/comment-rules.js +171 -0
- package/{src/lib/validators/code-budget → framework/hooks/shared}/comment-scanner.js +2 -2
- package/{src/lib/ai → framework/hooks/shared}/comment-stripper.js +2 -1
- package/framework/hooks/shared/git-paths.js +38 -5
- package/framework/hooks/shared/source-files.js +34 -0
- package/framework/mcp/registry.json +26 -65
- 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 +58 -26
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +46 -25
- package/framework/skills/core/morph-media/SKILL.md +2 -2
- package/framework/skills/core/morph-plan/SKILL.md +7 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +3 -3
- package/framework/standards/PROVENANCE.json +4 -4
- package/framework/standards/STANDARDS.json +7 -7
- package/framework/standards/architecture/code-comments.md +9 -2
- package/framework/standards/backend/dotnet/testing.md +4 -2
- package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
- package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
- package/framework/standards/frontend/nextjs/testing.md +4 -2
- package/framework/standards/integration/mcp/mcp-tools.md +19 -12
- package/package.json +1 -1
- package/src/commands/project/doctor.js +9 -1
- package/src/commands/project/worktree.js +60 -4
- package/src/commands/state/gate-check.js +5 -0
- package/src/commands/tasks/dag.js +27 -4
- package/src/commands/validation/verify.js +22 -38
- package/src/lib/ai/code-audit.js +1 -1
- package/src/lib/ai/registry-audit.js +1 -1
- package/src/lib/risk-detector.js +13 -0
- package/src/lib/tasks/dotnet-test-introspection.js +1 -1
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -6
- package/src/lib/validators/code-budget/index.js +1 -1
- package/src/lib/validators/reuse/symbol-scanner.js +1 -28
- package/src/lib/worktree/env-allocator.js +9 -3
- package/src/utils/agents-installer.js +51 -1
- package/src/utils/hooks-installer.js +3 -2
package/CLAUDE.md
CHANGED
|
@@ -106,6 +106,7 @@ State lives in `.morph/state.json`. Phase derived from filesystem (not stored).
|
|
|
106
106
|
- **Output type normalization** — `normalizeOutputType()` auto-converts kebab-case to camelCase
|
|
107
107
|
- **Template helpers** — Handlebars v2.0: pascalCase, camelCase, snakeCase, kebabCase, pluralize, plus comparison/logic helpers
|
|
108
108
|
- **CLI structure** — Commands are thin wrappers; business logic in `src/core/` or `src/lib/`
|
|
109
|
+
- **Comentários** — o padrão é nenhum: nome, tipo e teste explicam o código. Só o porquê que o código não mostra (invariante, ordem que importa, quirk externo com link), em até 3 linhas. Sem história de processo (issue, PR, feature, "medido na 8.x") no código: isso vai no commit e na memória. JSDoc só em export que outro módulo consome e cujo contrato o nome não diz. **Não imite a densidade dos arquivos existentes**: boa parte do código legado deste repo é mais comentada do que esta regra. O hook `comment-guard` roda aqui também (`.claude/settings.json`)
|
|
109
110
|
|
|
110
111
|
## Framework Development Guardrails
|
|
111
112
|
|
|
@@ -115,7 +116,7 @@ When modifying framework components, verify the full scope of related files.
|
|
|
115
116
|
|
|
116
117
|
| When you modify... | Also verify... |
|
|
117
118
|
|----------------------------------|-------------------------------------------------------------------------------------------|
|
|
118
|
-
| `framework/agents.json` | agents-installer.js (test/utils/agents-installer.test.js), doctor.js (REQUIRED_AGENTS). **`maxTurns` (ADR-004):** UMA régua de validade, `maxTurnsError` em `agents-installer.js`, com QUATRO leitores — `installAgents` (recusa antes de escrever), `checkAgentsDrift` (`invalid`), `validateAgentsJson` do `doctor` (error) e o Check 6 do `validate-framework.js` (`test/framework/validate-framework-agents.test.js`); uma segunda noção de "valor válido" faz o mesmo arquivo passar num e ser recusado no outro. Sem o campo, o frontmatter sai byte a byte igual ao de antes (canário COMPAT). O corpo gerado anuncia o teto ao sub-agent, e a cláusula 5 dos templates de dispatch depende disso. O drift lê o registry MCP do PACOTE (`mcpRegistryDir`): a cópia instalada não tem `mcp/`, e ler dela dava toda persona com `mcp` como stale numa instalação limpa. O marcador `PARCIAL` (1ª linha) que a cláusula 5 e o corpo da persona mandam gravar é lido por `report-check.js` (`PARTIAL_MARKER` → `report-incomplete`, ação `resume`): mudar a grafia num lado só devolve o parcial lido como `ok`. **`surfaces` (ADR-005):** mesma régua única em quatro leitores (`surfacesError`), e `personaStandardIds` é a leitura única de "todo standard da persona" para o Check 1, o drift e os scanners do dashboard (`agents-scanner.js`, `standards-scanner.js`) — iterar só `standards` deixa id de superfície sem validar e a aresta do grafo sumida. Sem o campo, o `.md` sai byte a byte o de antes: `test/fixtures/agents/ui-designer-5.3.0.{json,md}` é a persona do 5.3.0 e o render do installer de main, comparados com `strictEqual` — nunca regere a fixture para fazer o teste passar. A chave da superfície é a que o checklist de tela do `morph-uiux` escreve em `Superfície` e que o `morph-plan` §5a usa para somar a lista em `tasks[].standards` |
|
|
119
|
+
| `framework/agents.json` | agents-installer.js (test/utils/agents-installer.test.js), doctor.js (REQUIRED_AGENTS). **`maxTurns` (ADR-004):** UMA régua de validade, `maxTurnsError` em `agents-installer.js`, com QUATRO leitores — `installAgents` (recusa antes de escrever), `checkAgentsDrift` (`invalid`), `validateAgentsJson` do `doctor` (error) e o Check 6 do `validate-framework.js` (`test/framework/validate-framework-agents.test.js`); uma segunda noção de "valor válido" faz o mesmo arquivo passar num e ser recusado no outro. Sem o campo, o frontmatter sai byte a byte igual ao de antes (canário COMPAT). O corpo gerado anuncia o teto ao sub-agent, e a cláusula 5 dos templates de dispatch depende disso. O drift lê o registry MCP do PACOTE (`mcpRegistryDir`): a cópia instalada não tem `mcp/`, e ler dela dava toda persona com `mcp` como stale numa instalação limpa. O marcador `PARCIAL` (1ª linha) que a cláusula 5 e o corpo da persona mandam gravar é lido por `report-check.js` (`PARTIAL_MARKER` → `report-incomplete`, ação `resume`): mudar a grafia num lado só devolve o parcial lido como `ok`. **`effort`:** mesma régua única em quatro leitores (`effortError`); ausente, o frontmatter não ganha linha e o sub-agent herda o effort da sessão (COMPAT intacto). Implementadores `medium`, `scout` `low`, `evaluator` sem o campo — o Gate 3 não é barateado. **`surfaces` (ADR-005):** mesma régua única em quatro leitores (`surfacesError`), e `personaStandardIds` é a leitura única de "todo standard da persona" para o Check 1, o drift e os scanners do dashboard (`agents-scanner.js`, `standards-scanner.js`) — iterar só `standards` deixa id de superfície sem validar e a aresta do grafo sumida. Sem o campo, o `.md` sai byte a byte o de antes: `test/fixtures/agents/ui-designer-5.3.0.{json,md}` é a persona do 5.3.0 e o render do installer de main, comparados com `strictEqual` — nunca regere a fixture para fazer o teste passar. A chave da superfície é a que o checklist de tela do `morph-uiux` escreve em `Superfície` e que o `morph-plan` §5a usa para somar a lista em `tasks[].standards` |
|
|
119
120
|
| `framework/standards/**` | STANDARDS.json entry exists (o **digest** e o que `standards --find` devolve — corrigir a prosa e esquecer o digest e o caso mais comum), agents reference standard, .md file exists. **Bloco de codigo `csharp` num standard e afirmacao verificavel:** extraia com `node scripts/extract-doc-csharp.mjs <std.md> "## Secao" out.cs` e compile numa sonda com o pin + `TreatWarningsAsErrors` (sem ele `MEAI001` vira aviso e o build fica verde sobre codigo que o consumidor nao compila). Medido: um exemplo carimbado sem compilar tinha 3 erros independentes |
|
|
120
121
|
| `isImplementer` (Check 6 de `scripts/validate-framework.js`) | toda persona com Write/Edit no escopo efetivo de tools tem de alcançar, pelos `standards`, as âncoras `comment-policy` e `test-budget` do STANDARDS.json (`IMPLEMENTER_ANCHORS`). Renomear a chave da âncora no registry derruba o `validate`, e persona implementadora nova sem os dois standards também. Installer e `doctor` não cobram isso de propósito: um `agents.json` instalado antigo continua valendo |
|
|
121
122
|
| `scripts/generate-standards-registry.js` | `framework/standards/STANDARDS.json` (REGENERE, nunca patch textual — tres features escrevem no mesmo arquivo), `src/lib/standards/verified-against.js` (`parseVerifiedAgainst`/`deriveProvedBy`: `verifiedAgainst` e `provedBy` sao DERIVADOS a cada run e NAO entram em `PRESERVE_FIELDS`), `test/framework/standards-registry.test.js`. **Renomear um standard e uma PARADA, nao um exit 0**: path sumido + path novo faz o gerador recusar nomeando os dois conjuntos e prescrevendo a edicao do campo `path` da entrada antiga — ele nao adivinha o par. `MORPH_STANDARDS_BASE` e o unico seam de teste do script e nada em producao o define |
|
|
@@ -143,11 +144,11 @@ When modifying framework components, verify the full scope of related files.
|
|
|
143
144
|
| `src/lib/git/gh.js` | `pr-index.js`, `pr-files.js` e `finish.js` — as DUAS formas de invocar o `gh` viraram uma; uma segunda cópia de `resolveGh` volta a tornar o caminho de sucesso do índice intestável |
|
|
144
145
|
| `src/lib/git/tree-fingerprint.js` (a impressão da árvore) | `verify.js` (grava `fingerprint` no evento `Verify`; `findRepeatedVerify`, de `verify-event.js`, compara com o último verify de feature COMPLETO — rodada com `persisted: false` nunca é a prova que uma completa substitui) e `metrics.js` (`verify.unchangedRepeats`, pela mesma regra). `.morph/` fica fora, exceto `config/config.json`: o próprio verify escreve feature.json e logs a cada rodada, e contá-los faria duas rodadas sobre o mesmo código parecerem diferentes. `repeatOf` é informação e nenhum gate o lê: ligá-lo ao Gate 3 é aceitar a prova de uma rodada anterior no lugar da atual |
|
|
145
146
|
| `src/lib/git/tree-state.js` (o bloco `tree` do `report-check`) | `report-check.js` (toda saída, inclusive o batch `report-check <f> T1 T2`, e a foto `--before-eval` em `4-review/tree-before-eval.json`) e as skills que leem os campos: `morph-implement`/`morph-apply` (pós-dispatch e retomada do PARCIAL) e `morph-review` (higiene contra `tree.sinceBeforeEval`). Campo renomeado aqui sem as skills é instrução que lê `undefined`: `test/framework/report-check-wiring.test.js` confere que todo `tree.*` citado existe na saída real. A foto compara CONTEÚDO (blob por arquivo), não o `git status`: a mutação que o avaliador commitou também aparece. Fora de git o bloco é `null`, nunca árvore vazia. `MUTATION_MARKER` tem a mesma grafia que as skills mandam gravar |
|
|
146
|
-
| `src/lib/worktree/feature-truth.js` | The
|
|
147
|
+
| `src/lib/worktree/feature-truth.js` | The six readers of the same "which tree owns this feature" predicate: `fleet-scanner.js`, `list.js`, `handoff.js`, the mutator guard (`approve.js`/`advance.js`/`score.js`/`gate-decision.js`/`gate-check.js`), `verify.js` (`decidePersistence`, asked from the PRIMARY ROOT — asked from cwd, step 4 would make any scratch tree carrying a `feature.json` persist) and the dashboard's `features-scanner.js` (only for ACTIVE features — it spawns git — and a worktree owner without the feature files is shown as `rootStale`, never silently replaced by the root skeleton) |
|
|
147
148
|
| `src/lib/tasks/test-runner.js` | `verify-runner.js` (o nó `tests` repassa o `--timeout` e lê `timedOut`), `src/commands/project/worktree.js` (as constantes `WORKTREES_DIR`/`LEGACY_WORKTREES_DIR` que a descoberta ignora — duplicar as strings aqui é como o bug volta), `test/lib/test-runner.test.js` (o canário do ignore de worktrees e o do timeout escalado). Um worktree é o checkout de OUTRA feature: varrê-lo dobra o relógio e reporta sobre código fora da árvore avaliada. `stripAnsi` roda antes de TODO extrator de contagem (ponto único em `parseTestCount`) e das strings de `isZeroTestRun` — uma cor SGR no meio do sumário derrotava os regexes ancorados (#110). O extrator `dotnet` lê os DOIS sumários (#115): a linha VSTest e o bloco MTP (cabeçalho `Test run summary:`/`Resumo da execução de teste:` na coluna 0, `total:` indentado), em pt-BR e en-US — UM bloco por invocação de `dotnet test` (uma solução de dois projetos dá um bloco agregado, medido), então somar blocos é somar links `&&`; um terceiro locale é `null`. `mtpSummaryTotals` (um total por bloco) é o que `judgeNarrowedRun` lê para achar, numa cadeia MTP recortada, o link que rodou 0 (exit 8 ignorado) escondido pela soma. `ZERO_RUN_SIGNATURES` mistura o medido e o documentado — medidos (capturas em `test/fixtures/test-output/`): o exit 8 do MTP e a string pt-BR do VSTest; só documentados, sem captura: a string en-US do VSTest e as de vitest/jest. O exit 9 fica FORA de propósito: é piso de projeto, não zero testes. Leitor: `judgeNarrowedRun` (`verify-runner.js`). `extractCsprojTargets`, `isInside` e `canonicalPath` também são lidos por `test-scope.js` (`canonicalPath` dá a todo path de `outputs` a grafia do DISCO — no win32 um `Foo.test.ts` mal grafado passa no `isFile`, e sob `--linux`, case-sensitive, o runner não o acha enquanto um irmão mantém a contagem acima de zero); `dotnet-test-introspection.js` lê só `isInside`. Um segundo canonicalizador é como os dois lados passam a discordar sobre o mesmo path |
|
|
148
149
|
| `src/lib/specs/screen-checklist.js` (checklist de tela, #129) | o sinal mora aqui (`collectScreenChecklist`) e tem QUATRO leitores: `gate-check.js` na `uiux` (`problems` → pausa) e no `review` (`uncovered`: item decidido sem linha na seção `## Checklist de tela` que `evaluator-report.js` lê — última ocorrência; tela toda "não se aplica" não deve nada), e `approve.js`/`advance.js` com assinatura `auto` (`uiuxAutoApprovalBlock`). Política só em `risk-detector.js`: `uiuxChecklistPause` é o predicado ÚNICO do gate-check pausar e do auto recusar — uma segunda regra no advance fez approve e advance discordarem da mesma feature — e `gateDecisions.uiux.pause === false` destrava os dois só para o mockup que a decisão viu: `persistGateDecision` (`gate-decision.js`, o escritor único de `gateDecisions`) grava `mockupsHash` e `uiuxAutoApprovalBlock` o compara com o arquivo atual — os dois leem `mockupsHash` daqui, e um segundo cálculo do hash (outra normalização de BOM/CRLF) faz toda decisão parecer de outro mockup. `--force` não destrava em nenhum dos dois. Mockup legado (`## S\d+:` e nenhuma `## Tela:`, o template anterior) é isento nas duas fases com `exempt` = `LEGACY_MOCKUP_EXEMPTION` à vista no `getRiskSignals`; qualquer outro heading é "nenhuma tela declarada", senão trocar o heading pularia o checklist (fixture: `test/fixtures/mockups/legacy-ui-mockups.md`, o template de main). Os rótulos dos itens casam com DOIS textos que o parser não vê, os dois com canário em `test/lib/specs/screen-checklist.test.js`: o template do prompt da `morph-uiux` §3 e `framework/templates/docs/ui-mockups.md`; mais o formato de linha que o prompt do avaliador (`morph-review` §4) e a rubrica `contracts.md` ("Contrato de tela") prescrevem. A `Superfície` é validada contra as chaves de `surfaces` da `ui-designer` (`knownSurfaces`) — é a chave que o `morph-plan` §5a usa para somar standards |
|
|
149
150
|
| `src/lib/tasks/test-scope.js` (o recorte por task, #87) | Este DECIDE, `verify-runner.js` executa: `applyTaskScope` roda UMA vez em `runVerify`, antes do build, e `preDecideRun` (pula `out-of-scope`) e `judgeRun` → `judgeNarrowedRun` (nunca `compareTestCount`; zero ou desconhecido é vermelho) são a cópia única para host e `--linux` — o container recebe o recorte pelo `node.command`. Importa `effectiveGroups` (`test-nodes.js`), `extractCsprojTargets`/`isInside`/`canonicalPath` (`test-runner.js`), `normalizePath` (`traceability-check.js` — um normalizador só para `outputs`), `samePathCase` (`docker-audit.js` — só a CAIXA vem do disco: um path que atravessa symlink/junction fica como escrito, senão o arquivo muda de nó) e a introspecção .NET abaixo. `runs[].selection` sai só no `--json`: `nodeStatuses()` não o persiste. Piso `--minimum-expected-tests` de projeto MTP (#114): quando `floorOverride` o lê de UM lugar, o link recortado ganha `"-p:TestingPlatformCommandLineArguments=<valor com o piso em 1>"` (o `-p:` troca o valor inteiro — medido, sem recompilar); qualquer outra forma roda inteiro, e também o comando que já declara a propriedade (o `-p:` do recorte vem por último e ganharia) e o link que roda mais de um projeto (o `-p:` é global à invocação e trocaria os argumentos do outro). Forma de comando nova se lê e se MEDE (`test/fixtures/test-output/README.md`) antes de estreitar; o resto roda INTEIRO com motivo. Canários: `test/lib/tasks/test-scope.test.js`, o bloco `runVerify — task-scope recorte` de `test/lib/verify-runner.test.js`, `test/commands/verify-filter.test.js` e a prosa em `test/framework/verify-stack-docs-prose.test.js` |
|
|
150
|
-
| `src/lib/tasks/dotnet-test-introspection.js` | Só LÊ (dividido de `test-scope.js`): o varredor C# que apaga literais (`blankCSharpLiterals`), o tripwire contra o texto CRU + o balanço de chaves (`csharpClasses`), `classNamesOf`, `dotnetTestMode` (lê o `global.json` por `jsoncToJson` de `
|
|
151
|
+
| `src/lib/tasks/dotnet-test-introspection.js` | Só LÊ (dividido de `test-scope.js`): o varredor C# que apaga literais (`blankCSharpLiterals`), o tripwire contra o texto CRU + o balanço de chaves (`csharpClasses`), `classNamesOf`, `dotnetTestMode` (lê o `global.json` por `jsoncToJson` de `framework/hooks/shared/comment-stripper.js`), o piso `--minimum-expected-tests` / `<VSTestTestCaseFilter>` do projeto, `floorOverride` (#114: o valor do `-p:` que rebaixa o piso a 1 e repassa o resto — só com UM `<TestingPlatformCommandLineArguments>` literal no csproj, sem atributo, num `<PropertyGroup>` sem atributo direto sob `<Project>` (`enclosingElements` anda nas tags: `Condition` no GRUPO, `<Target>` e `<Choose>` injetavam um filtro que o MSBuild não usaria), sem `TreatAsLocalProperty`, sem `<Import>`, sem a propriedade em nenhum outro arquivo que liga o projeto, sem piso no testconfig e sem `` $ @ % ; , & " ' ` \ ! `` no valor; `null` roda inteiro; `bindingMsbuildFiles` é a lista única de arquivos MSBuild que `msbuildDeclares` também lê — o `.csproj.user` e `Directory.Build.props/.targets/.rsp` + `Directory.Packages.props` até a raiz do DISCO, porque o MSBuild não para no repositório), `ignoresExitCode` (o MESMO scan de csproj/Directory.Build.*/testconfig: `--ignore-exit-code` ou `TESTINGPLATFORM_EXITCODE_IGNORE` desarma a prova do recorte MTP que resta quando o sumário é ilegível, o exit code — desde a #115 a contagem do sumário, pt-BR/en-US, prova primeiro, e o scan fica porque decide ANTES da rodada), o projeto de teste dono de um `.cs` (`dotnetTestProjectOf` — só `.cs`/`*.csproj`, via `nearestCsprojOf`), `nearestProjectOf` + `isDotnetTestProject` (qualquer arquivo; cobrem `.csproj`/`.fsproj`/`.vbproj` — `test-scope.js` roda o nó INTEIRO por QUALQUER arquivo não-`.cs` que o nó pode rodar cujo projeto mais próximo é de teste: `.feature`, `.verified.txt`, `.razor`, dado de teste), `isNonCSharpSource`/`isDotnetSource`, `readsLikeTestSource` (o marcador FROUXO no texto cru atrás de `unrecognisedTestFile` e do segundo braço do F2 — falso positivo só custa relógio) e `CS_TEST_ATTRIBUTE` (nome inteiro numa seção de atributo que FECHA — `, TestContext` num construtor e uma coleção `[\n Env.Test,\n]` compravam um verde; o resíduo sondado mora no JSDoc de `csharpClasses`). **Falha para o lado SEGURO:** varredura desbalanceada, literal sem fim, buraco de interpolação cortado ou palavra-chave de tipo que não virou declaração → o nó roda INTEIRO; um erro que ESTREITA roda menos testes e sai verde. `test-scope.js` reexporta `dotnetTestMode`/`classNamesOf`, e `test/lib/tasks/dotnet-test-introspection.test.js` pina que são as MESMAS funções. A garantia e o que ela NÃO cobre moram no JSDoc de `csharpClasses`: mexeu no varredor, reescreva-o junto e prove por mutação que o tripwire ainda morde |
|
|
151
152
|
| `src/lib/tasks/linux-runner.js` | `verify-runner.js` (`runNodesInLinux` faz as MESMAS pré-decisões do laço local e julga o resultado pelo MESMO `judgeRun` — uma segunda cópia dessa regra é como container e host passam a discordar da mesma suíte), `framework/hooks/shared/e2e-check.js` (`defaultExec`/`isDockerAvailable` são reusados, nunca recriados), `src/lib/worktree/dirs.js` (nomes do contêiner de worktree nas exclusões do `tar`), `framework/schemas/config.schema.json` (`linuxImage`) e `test/lib/tasks/linux-runner.test.js` (o canário do bind mount: montar a árvore em `/work` é v9fs case-insensitive e não reproduz F35) |
|
|
152
153
|
| `--record-baseline` (`recordTestBaselines` em `src/lib/tasks/test-nodes.js`) | `verify-runner.js` (`baselineTargetProblem` recusa ANTES do build; `recordBaselineFrom` só grava com todos os `runs[]` verdes), `src/commands/validation/verify.js` (não persiste stamp: é manutenção de config, não prova de gate), `framework/schemas/config.schema.json` (`expectedCount`) e `atomicWriteJson` de `state-manager.js` — o número é MEDIDO por comando, nunca digitado (D8). Com `{task}` é erro de uso ANTES do build (`baselineTargetProblem`, #87), e `recordTestBaselines` recusa de novo qualquer medição com `selection` — a segunda barreira mora no gravador |
|
|
153
154
|
| `src/lib/tasks/eval-node.js` | os leitores do MESMO nó `runner: "evals"`: `test-nodes.js` (o enum de `TEST_RUNNERS` e a validação fail-closed do bloco `evals`), `verify-runner.js` (`preDecideRun` faz o pré-voo — os três estados de cache reprovam ANTES de `dotnet test`; `judgeEvalRun` lê o sumário depois), `verify.js` (`evalsStatusOf` deriva `nodes.evals` do stamp) e `framework/schemas/config.schema.json`. O pin tem UM leitor (`readAiPin`), chamado com duas raízes (`framework/` e `.morph/framework/`) — um segundo parser de `ai-pin.json` é como as duas verdades começam. **`null` é DESCONHECIDO, nunca 0**: sumário ilegível não pode fabricar queda contra `expectedCount`, e miss de cache é `fail` nomeado, JAMAIS `skip`. A unidade do nó é o CENÁRIO: só `judgeEvalRun` compara com `expectedCount` — `judgeRun` passa baseline `null` para `runner: "evals"` (#110 parte 2: os `[Fact]` do extrator contra a baseline de cenários reprovavam um 24/24), e sem sumário legível a contagem é `null`, nunca a do extrator — nem compara nem vira baseline. O sumário (`morph-eval-{id}-{pid}.json` no temp) é APAGADO antes e depois da rodada: um arquivo herdado sob o mesmo pid era lido como desta. `--linux` com nó de eval é erro de uso: o canal `MORPH_EVAL_*` carrega caminhos absolutos do host e o container roda uma cópia em `/work` |
|
|
@@ -157,7 +158,8 @@ When modifying framework components, verify the full scope of related files.
|
|
|
157
158
|
| `framework/hooks/shared/graph-refresh.js` (o refresh em background) | os dois disparos (`graph-refresh-kick`, depois de Bash/PowerShell com `git`/`gh`/`morph-spec`, e o SessionStart de `worktree-context.js`), `graph build\|refresh` (o MESMO lock por repositório) e `worktree.js` (`seedGraphFromRoot`: a worktree nasce com o grafo da raiz e o registro dela — em dia no HEAD só se a raiz estava). **Só a raiz**: o worker destacado nasce com `cwd` no git common dir e nunca recebe o caminho de uma worktree — no win32 um diretório em uso não se apaga, e o `finish` de uma worktree com refresh correndo falhava no meio. A worktree anda por `morph-spec graph refresh`, em primeiro plano. `runGraphify` roda o graphify com `cwd` no git common dir, nunca na árvore (ele lê o alvo, o ignore e o git pelo argumento — medido no 0.9.50). O disparo lê o HEAD pelo disco (`readPrimaryHead`, `git-paths.js`) e não solta processo quando o grafo não pede refresh: ele roda a cada comando git do agente. O step 18b de `setup-infra.js` não instala o hook git do graphify: ele não roda em worktree, não tem `post-merge` e carimba como em dia um grafo sem os arquivos do merge |
|
|
158
159
|
| `framework/hooks/shared/git-exclude.js` | ÚNICO escritor dos blocos do morph-spec no `info/exclude` do COMMON dir — o `.git/worktrees/<id>/info/exclude` o git não lê para worktree vinculada. Leitores que passam o seu marcador: `worktree.js`, `handoff-generator.js` e `graph-refresh.js`. Resolve o common dir pelo disco, sem subprocesso: um `git` gerado por um processo destacado abria janela de console no win32 |
|
|
159
160
|
| `src/lib/validators/reuse/**` | `validation-runner.js` (case + push universal), `framework/rubrics/*.json` (o id `reuse` mapeado numa categoria, senão o achado não pesa no score), `test/fixtures/graph/prospectpro-reuse.graph.json` (recorte do grafo real — regerar com `build-reuse-fixture.mjs`, nunca editar à mão) |
|
|
160
|
-
| `src/lib/validators/code-budget/**` | `validation-runner.js` (case + push universal fora do `light`), `framework/rubrics/*.json` (o id `code-budget` em `code_quality` nas TRÊS, senão o aviso não pesa), `framework/docs/CLI.md` (o que conta como teste e os limites 1,5×/40 linhas) e `test/integration/code-budget-verify.test.js`. Lê `addedLinesAgainst` (`default-branch.js`), `isTestFile
|
|
161
|
+
| `src/lib/validators/code-budget/**` | `validation-runner.js` (case + push universal fora do `light`), `framework/rubrics/*.json` (o id `code-budget` em `code_quality` nas TRÊS, senão o aviso não pesa), `framework/docs/CLI.md` (o que conta como teste e os limites 1,5×/40 linhas) e `test/integration/code-budget-verify.test.js`. Lê `addedLinesAgainst` (`default-branch.js`), `isTestFile` (`symbol-scanner.js`, a mesma régua de teste do `reuse`), `isProjectSource`/`isGenerated` (`hooks/shared/source-files.js`) e `commentLines`/`citesProcess` (`hooks/shared/comment-scanner.js`, sobre `stripComments` com `js`). Sempre `warning`. Não conseguir medir é skip `unavailable` com razão, e o `validation-runner` não põe validador pulado em `results`: pulado não conta como verde |
|
|
162
|
+
| `framework/hooks/shared/comment-rules.js` / `comment-scanner.js` / `source-files.js` (o hook `comment-guard`) | Os dois leitores do MESMO comentário: o hook `comment-guard` (PostToolUse `Write\|Edit`, bloqueia na hora da edição) e o `code-budget` (avisa no `verify`). Moram em `hooks/shared` porque hook não importa `src/`; o `src/` importa daqui, nunca uma cópia. O hook julga só o comentário ADICIONADO (`tool_response.originalFile`, medido: `null` no Write que cria) e só FORMA — processo, banner/`#region`, bloco > 3 linhas, doc-comment fora de pacote publicado —, nunca "repete o código". É o ÚLTIMO elo da chain `Write\|Edit` (um block encerra a chain; canário em `test/hooks/hooks-installer.test.js`). Marcador que template precisa manter (`READS STANDARD(S):`, `VERIFY:`) entra em `ALLOWED`; o canário de template em `test/hooks/comment-rules.test.js` reprova template que dispararia o hook no scaffold |
|
|
161
163
|
| o ciclo de reuso (`reuse-map.md`) | `output-schema.js` (+ `npm run generate`), `framework/schemas/tasks.schema.json` + `tasks-json-parser.js` (campo `reuse`), `morph-plan` §1c/§5b, template de dispatch de `morph-implement`/`morph-apply`, personas de `agents.json`, `evals/rubrics/code-quality.md` + `condensed.md`, `morph-review` (insumo do avaliador), `test/framework/graph-fallback.test.js` |
|
|
162
164
|
| `framework/mcp/registry.json` | mcp-installer.js, claude-config-detector.js, agents-installer.js (personas' `mcp` field), `scripts/validate-framework.js` Check 8, `test/framework/mcp-registry.test.js` |
|
|
163
165
|
| Any `framework/` file that calls `morph-spec graph` | The fallback to grep must be declared in the SAME file — `test/framework/graph-fallback.test.js` enforces it (ADR-002: a skill that requires the graph with no fallback is a bug) |
|
|
@@ -168,7 +170,7 @@ When modifying framework components, verify the full scope of related files.
|
|
|
168
170
|
| `src/lib/risk-detector.js` | os DOIS lados da mesma decisão de gate: `gate-check.js` (`collectSignals` COLETA do disco; nenhum sinal que descreva um FATO pode entrar em `OVERRIDABLE_SIGNAL_KEYS` — `gate`/`workType`/`e2e*` são o precedente), `src/commands/validation/verify.js` (o stamp `verification` é a ÚNICA coisa que o gate lê: campo novo aqui só vira sinal se o stamp o gravar), `framework/schemas/feature.schema.json` (`verification` é `additionalProperties: false`), `framework/MORPH.md` §5 (tabela de sinais — ela se declara fonte única) e `framework/docs/CLI.md`. Política escrita no coletor cria duas fontes para o mesmo fato — foi assim que `gate-check` e `approve` divergiram uma vez sobre o score do Gate 3. **`verificationScope` é sinal, não auditoria**: a dispensa de e2e (`project`/`scope`) só conta vinda de um stamp de escopo `feature` — um `verify {f} {task}` de task `group: docs` carimba a razão de dispensa e compraria o Gate 3 da feature inteira. **E o escopo de task pausa o ship por si só** (#87): no `review`, `verificationScope` string ≠ `'feature'` pausa ANTES do bloco de e2e, mesmo com todos os nós verdes — o recorte automático faz TODO `verify {f} {task}` rodar menos que a suíte; `null` (stamp legado) não pausa por esta regra. O par mora em `verify.js`: `--filter` de escopo de feature não persiste o stamp (`reason: 'filtered'`), nem `--skip-tests`/`--skip-build` (`'skip-tests'`/`'skip-build'`, #113 — nó pulado não reprova o rollup e nenhuma regra do gate lê `tests`/`build` pulados; a lista é `PARTIAL_RUN_FLAGS`) |
|
|
169
171
|
| `src/lib/installers/effects.js` | os NOVE pontos de emissão que atravessam o caminho de `update`: `update.js` (clean, payloads, spawn de `generate-refs.js`, `gitIndex`), `skills-installer.js`, `agents-installer.js`, `setup-infra.js` (`installRulesForStack`, `updateClaudeMdAgentsBlock`), `hooks-installer.js` (`installClaudeHooks`, `installGlobalStatusline`), `file-copier.js` (`updateGitignore`, `ensureGitattributes`), `claude-md-injector.js`, `version-checker.js` (`saveProjectMorphVersion`) e `templates-reconcile.js`. **O caminho real também emite** — o resumo de uma execução real sai de `list()`, e é por isso que um ponto de emissão esquecido some das DUAS saídas ao mesmo tempo. Canários: `test/lib/installers/effects.test.js` (igualdade dry × real do MESMO roteiro) e `test/commands/update-dry-run.test.js` — que tem DOIS canários e precisa dos dois: **igualdade** prova que os dois lados percorrem o mesmo caminho, **diferencial** (mudar `--templates` muda a previsão) prova que o caminho lê a flag. Só igualdade aprova uma previsão e uma execução que ignoram a MESMA flag — foi medido nesta feature, por mutação. Um laço de limpeza de órfão novo tem de nascer calado em `fx.dryRun` (D6): o clean o subsome. E a lista de `frameworkDirsToClean` é pinada **à mão** no teste (`DIRETORIOS_APAGADOS_POR_INTEIRO`, dez caminhos, `deepEqual` ordenado): derivar a expectativa chamando a própria função deixou a suíte inteira cega — medido, dava para remover 2 das 10 entradas sem matar nenhum dos 207 arquivos de teste |
|
|
170
172
|
| `src/lib/ai/**` (`doctor --ai`) | `src/commands/project/doctor.js` (`doctorAiCommand` + a guarda de uso indevido), `bin/morph-spec.js` (`--ai`), `framework/docs/CLI.md`. **`--ai` NUNCA entra no `doctor --full` nem no default** — o renderer do `--full` manda tudo que não é `ok`/`warn` para `✗` e `test/commands/doctor.test.js` exige zero `✗` neste repositório. **Nada alcançável por `--ai` pode escrever**: `test/commands/doctor-ai.test.js` varre o grafo de imports E o corpo de `doctorAiCommand` (com os imports dinâmicos fixados nos dois permitidos), reprovando `writeFileSync`/`spawn`/`fs-extra` em CÓDIGO — comentários apagados antes, porque os módulos EXPLICAM por escrito que não fazem isso e medir a menção seria medir uma cópia do objeto. `null` é DESCONHECIDO e vira `skip` **com razão obrigatória**; `[]` é "perguntei e não achei" e vira `ok` |
|
|
171
|
-
| `src/lib/ai/registry-audit.js` | o leitor de PRODUÇÃO do mesmo documento: `framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs` (`CommentHandling = Skip`, `AllowTrailingCommas`, `AllowedAliasProperties` com `pricing`/`pricingWaiver`) e o exemplar `model-registry.v2.json` que o framework manda COPIAR. A auditoria tem de enxergar a **mesma forma** que o leitor de produção enxerga: com `JSON.parse` estrito, duas das cinco checagens saíam `skip` sobre o exemplar canônico — e `skip` não muda exit code, então o silêncio era total. A tolerância a JSONC vive em `
|
|
173
|
+
| `src/lib/ai/registry-audit.js` | o leitor de PRODUÇÃO do mesmo documento: `framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs` (`CommentHandling = Skip`, `AllowTrailingCommas`, `AllowedAliasProperties` com `pricing`/`pricingWaiver`) e o exemplar `model-registry.v2.json` que o framework manda COPIAR. A auditoria tem de enxergar a **mesma forma** que o leitor de produção enxerga: com `JSON.parse` estrito, duas das cinco checagens saíam `skip` sobre o exemplar canônico — e `skip` não muda exit code, então o silêncio era total. A tolerância a JSONC vive em `framework/hooks/shared/comment-stripper.js`, ÚNICA (o `code-audit.js` usa o mesmo varredor, e o `code-budget` também, o único com a opção `js` — template e regex literal de TS/JS; sem ela o varredor é o de antes) — e a paridade é nos DOIS sentidos: `jsoncToJson` **lança** no comentário de bloco não fechado, porque o `JsonCommentHandling.Skip` do .NET lança, e ser mais frouxo que o kit é aprovar documento que a app recusa carregar. Ao mascarar strings, **as aspas delimitadoras ficam**: apagá-las junto faz um elemento-string virar espaço puro e o detector de vírgula sobrando comer um SEPARADOR real (`{"tags":["a","b"]}` saía inválido). Canário obrigatório: array terminado em **string**, com e sem vírgula sobrando — uma suíte só com arrays de número passa dos dois lados dessa inversão. A **REGRA DURA** do exemplar — *todo alias declara `pricing` OU `pricingWaiver`, nunca os dois, nunca nenhum* — é do alias, não de um mapa no topo: ler só o mapa dava 6 falsos positivos sobre um documento correto. O canário **copia o exemplar do disco**, nunca uma paráfrase dele |
|
|
172
174
|
| `src/lib/worktree/dirs.js` | ÚNICA fonte do nome do container (`worktrees/`, sem ponto — Jest esconde diretório com ponto e reporta 0 testes sem erro). `worktree.js` re-exporta; `graphifyignore.js`, `file-copier.js`, `architecture-scanner.js`, `test-runner.js`, `worktree-audit.js`, os scanners do dashboard (`features-scanner.js`, `project-api.js`), `tasks/linux-runner.js` (exclusões do `tar` do `--linux`) importam. `test/lib/worktree/dirs.test.js` cobra cada leitor |
|
|
173
175
|
| `src/dashboard/**` (páginas e scanners) | `test/dashboard/pages-smoke.test.js` renderiza as 7 rotas e os painéis de detalhe pela config Vite do dashboard com os dados REAIS do scanner — build verde não prova nada (8.38.0 saiu com `ReferenceError` na rota inicial). Campo que a página lê tem de sair do scanner: `activePhases` é DERIVADO das linhas de dispatch das skills de fase (`personaPhases`, com `isImplementer`), nunca uma lista à mão. Edge do React Flow só existe no cliente (chave duplicada não aparece no SSR — o dedupe de `standardRefs` mora no scanner). `src/dashboard/.gitignore` é o que mantém `node_modules` fora do tarball (`test/dashboard/packaging.test.js`). `scanner/feature-status.js` (`isFinished`) é a regra ÚNICA de "feature encerrada" do servidor (portfólio, `awaiting`, `taskIndex`) e do cliente (lista e quadro) — arquivada conta como encerrada seja qual for o `status` legado. O plugin engole o `full-reload` do Vite para `.html` sob `.morph/` (`isMorphDataReload`): `handleHotUpdate` devolvendo `[]` NÃO segura `.html` no Vite 6, e um agente gravando um mockup recarregava o dashboard. `resolveFeatureTree` (git, ~0,3 s por chamada) é lembrado por feature no `project-api.js` e só é esquecido por evento estrutural (`add`/`unlink`/`addDir`/`unlinkDir`) ou TTL de 60 s — um scan síncrono trava o event loop do dev server inteiro |
|
|
174
176
|
| `src/lib/projects/registry.js` (projetos da máquina, `~/.morph-spec/projects.json`) | os TRÊS escritores automáticos — `init`, `update` (nunca sob `--dry-run`) e `dashboard` — mais o add/remove do portfólio (`src/dashboard/scanner/portfolio.js`). Registrar é fail-open: registro ilegível ou não gravável nunca derruba o comando. Nunca registra pasta sob o temp do sistema (as suítes rodam `init`/`update` ali às centenas, inclusive por CLI em spawn que herda o home real) nem checkout de worktree (`WORKTREE_DIR_NAMES`). Teste que registra usa `MORPH_PROJECTS_REGISTRY` apontando para um temp, ou o seam `tempRoot` — nenhuma suíte escreve o `~/.morph-spec` de quem roda. Leitura poda projeto cuja pasta sumiu; descoberta é UM nível abaixo das pastas-mãe e só sugere, nunca adiciona |
|
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.42.0
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -245,7 +245,7 @@ Guardrail hooks are installed into `.claude/settings.local.json` across 7 event
|
|
|
245
245
|
| `SessionStart` | `worktree-context.js` | Injects feature identity (worktree) or active-feature summary (root) |
|
|
246
246
|
| `PreToolUse` (Write\|Edit) | `protect-spec-files.js`, `enforce-phase-writes.js` | Blocks edits to the frozen plan contract post-Gate-2; enforces phase folders |
|
|
247
247
|
| `PreToolUse` (Bash) | `pre-bash-denylist.js` | Detects destructive shell patterns |
|
|
248
|
-
| `PostToolUse` | `post-edit-typecheck.js`, `tasks-json-guard.js`, `trace-autogen.js`
|
|
248
|
+
| `PostToolUse` | `post-edit-typecheck.js`, `tasks-json-guard.js`, `trace-autogen.js`, `comment-guard.js` | Stack-aware typecheck; guards `tasks.json` plan-contract edits; auto-generates execution traces; blocks added comments the comment standard forbids |
|
|
249
249
|
| `PostToolUse` | `state-sync.js`, `gate-guard.js`, `loop-detect.js` | Syncs feature state; enforces gate ordering; detects repeated-action loops |
|
|
250
250
|
| `Stop` | `validate-completion.js` | Warns about incomplete tasks, missing outputs, or pending approval gates |
|
|
251
251
|
|
|
@@ -379,4 +379,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
379
379
|
|
|
380
380
|
---
|
|
381
381
|
|
|
382
|
-
*morph-spec v8.
|
|
382
|
+
*morph-spec v8.42.0 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -20,7 +20,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
20
20
|
- Editar `.morph/specs/usecases/` à mão — só o `finish` escreve lá. Mudar um UC existente (`bug`): copie para `2-plan/usecases/` da feature e corrija lá.
|
|
21
21
|
- Listar perguntas em texto simples — sempre `AskUserQuestion` (1-4 perguntas por chamada).
|
|
22
22
|
- 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.
|
|
23
|
-
- Comentar o que nome, tipo ou teste já dizem: o padrão é nenhum comentário (`architecture-code-comments`).
|
|
23
|
+
- Comentar o que nome, tipo ou teste já dizem: o padrão é nenhum comentário, mesmo ao lado de código comentado; o elo com a pós-condição vai no nome do teste (`architecture-code-comments`, hook `comment-guard`).
|
|
24
24
|
- Editar `taskScores`, `gateDecisions` ou gates de `feature.json` à mão — sempre via `morph-spec score`/`gate-check`/`approve` (timestamps server-side; Edit manual colide com o hook `state-sync`).
|
|
25
25
|
|
|
26
26
|
**SEMPRE:**
|
|
@@ -32,7 +32,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
32
32
|
- 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, sem escalar ao humano entre tarefas. No Gate 3, o score composto vem do **avaliador independente** despachado pela `morph-review` — nunca de auto-avaliação de quem implementou.
|
|
33
33
|
- Em `hotfix`, o Gate 3 pausa **sempre** para um humano, mesmo em `trust: auto` — `approve ... review --mode auto` é recusado; aprove com `--approver`.
|
|
34
34
|
- Confirmar antes de `finish`/`archive`/`delete` se o SessionStart avisar que outra sessão tem claim vivo na mesma feature.
|
|
35
|
-
- **Contrato do relatório final** em todo dispatch de sub-agent: (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); é o retorno autoritativo, nunca chamar `SendMessage`; (2) não tocar `tasks.json`/`feature.json`/`state.json` nem o board de sessão; (3)
|
|
35
|
+
- **Contrato do relatório final** em todo dispatch de sub-agent: (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); é o retorno autoritativo, nunca chamar `SendMessage`; (2) não tocar `tasks.json`/`feature.json`/`state.json` nem o board de sessão; (3) testes pelo orçamento, só o próprio filtro uma vez, sem red→green (a prova é o `verify` do orquestrador); (8) nunca mutar produção. Racional completo: `MORPH.md` §6. Templates: `morph-implement` §4, `morph-apply` §3b. **O orquestrador lê o arquivo ao retomar controle** — nunca confia só na mensagem de retorno da tool.
|
|
36
36
|
|
|
37
37
|
## Slash commands
|
|
38
38
|
|
package/framework/MORPH.md
CHANGED
|
@@ -217,11 +217,11 @@ Gates são pontos de **pausa potencial**. Em `trust: "auto"` (default), auto-pas
|
|
|
217
217
|
|
|
218
218
|
| Sinal | Threshold | Quando aplica |
|
|
219
219
|
|---|---|---|
|
|
220
|
-
| `designSystemDivergence` | true |
|
|
221
|
-
| `standardsViolations` | array.length > 0 | Mandate ou
|
|
222
|
-
| `evalScore` | < 9 | score composto da feature
|
|
220
|
+
| `designSystemDivergence` | true | Fontes de design conflitantes |
|
|
221
|
+
| `standardsViolations` | array.length > 0 | Mandate ou código viola padrões proibidos |
|
|
222
|
+
| `evalScore` | < 9 | score composto da feature abaixo do mínimo |
|
|
223
223
|
| `verificationStatus` | === 'fail' | `verify` vermelho — build, testes, validadores **ou e2e**. Vale em **qualquer** gate |
|
|
224
|
-
| `verificationStale` | true | Stamp do `verify` com sha != HEAD: o verde
|
|
224
|
+
| `verificationStale` | true | Stamp do `verify` com sha != HEAD: o verde é de outro código. Só no **ship** |
|
|
225
225
|
| `e2eSkipClass` (derivada) | environment/requested/unknown | e2e pulou sem dispensa legítima (só `project`/`scope`). Só no **ship** |
|
|
226
226
|
| `verificationScope` | !== `feature` (presente) | Stamp do `verify` é de escopo parcial (`task:{id}`) — não cobre a feature inteira. Só no **ship** |
|
|
227
227
|
| `mutationProof` | seção ausente | Relatório do avaliador sem `## Mutações` numa feature com código de produção. Só no **ship** |
|
|
@@ -229,17 +229,18 @@ Gates são pontos de **pausa potencial**. Em `trust: "auto"` (default), auto-pas
|
|
|
229
229
|
| `promptModel` | prompt/modelo tocado | Sem nó `evals` declarado, ou eval pulado neste verify. Só no **ship** |
|
|
230
230
|
| `mediaClaims` | seção ausente | Feature que gera mídia, relatório sem `## Mídia gerada`. Só no **ship** |
|
|
231
231
|
| `screenChecklist` | item em aberto | **uiux**: tela sem checklist completo, ou nenhuma `## Tela:`. **ship**: item decidido sem linha em `## Checklist de tela` |
|
|
232
|
-
| `taskCount` | > 20 | Escopo grande —
|
|
232
|
+
| `taskCount` | > 20 | Escopo grande — revise antes de implementar |
|
|
233
|
+
| `oversizedTasks` | > 8 `outputs` | Task não cabe num dispatch. Só no **plano** |
|
|
233
234
|
| `workType` | === 'hotfix' | Gate 3 pausa SEMPRE (ver "Gates por tipo de trabalho") |
|
|
234
235
|
|
|
235
236
|
| Gate | Sinais | Skill responsável |
|
|
236
237
|
|---|---|---|
|
|
237
238
|
| 1 — Proposta | `designSystemDivergence`, `standardsViolations` | `morph-brainstorming` |
|
|
238
|
-
| 2 — Plano | Gate 1 + `taskCount`, `standardsViolations` em mandate | `morph-plan` |
|
|
239
|
+
| 2 — Plano | Gate 1 + `taskCount`, `oversizedTasks`, `standardsViolations` em mandate | `morph-plan` |
|
|
239
240
|
| uiux — Design | `screenChecklist` | `morph-uiux` |
|
|
240
241
|
| 3 — Review | `evalScore` composite, `standardsViolations` em implementation, `verificationStale`, `verificationScope`, `e2eSkipClass`, `mutationProof`, `evalScoreSource`, `promptModel`, `mediaClaims`, `screenChecklist` | `morph-review` |
|
|
241
242
|
|
|
242
|
-
**Gates por tipo de trabalho:** `chore`/`hotfix` pulam Gates 1 e 2 — carimbados por política no nascimento (`approvedBy: "policy:{tipo}"`, nunca `"auto"`); a barra do morph-eval e o Gate 3 valem para todos os tipos. `hotfix`: Gate 3 **sempre humano** (mesmo em `trust: auto
|
|
243
|
+
**Gates por tipo de trabalho:** `chore`/`hotfix` pulam Gates 1 e 2 — carimbados por política no nascimento (`approvedBy: "policy:{tipo}"`, nunca `"auto"`); a barra do morph-eval e o Gate 3 valem para todos os tipos. `hotfix`: Gate 3 **sempre humano** (mesmo em `trust: auto`; `approve ... review --mode auto` recusa) e exige `4-review/review-report.md` em vez de `evaluator-report.md`. Demais tipos exigem `evaluator-report.md`, **enforced server-side** (`approve {feature} review` recusa sem ele; `--force` não dispensa) — ou `--self-assessed` registrado (`approvalGates.review.selfAssessed`, marcado no `morph-spec list`).
|
|
243
244
|
|
|
244
245
|
Modo: `.morph/config/config.json → trust` (`"auto"`, o default, ou `"manual"`).
|
|
245
246
|
|
|
@@ -316,7 +317,7 @@ aceito em silêncio. Segunda ausência sem trabalho entregue é falha real: esca
|
|
|
316
317
|
- Problemas encontrados e como foram resolvidos
|
|
317
318
|
- Auto-score 0-10 com justificativa em uma linha
|
|
318
319
|
|
|
319
|
-
**Contrato do relatório final:** as
|
|
320
|
+
**Contrato do relatório final:** as oito cláusulas que todo prompt de dispatch inclui vivem nos DOIS templates — `morph-implement` §4 e `morph-apply` §3b — e não são repetidas aqui: duas cópias da mesma regra divergem, e quem manda é a que o sub-agent recebe. **4-8** (escopo, sobrevivência, processos, retorno, mutação) são idênticas **palavra por palavra**, e um canário as compara; **1-3** (relatório, estado, teste) dizem o mesmo na redação de cada template.
|
|
320
321
|
|
|
321
322
|
**Teto de turnos:** `maxTurns` por persona no `agents.json` (sem o campo, 20; implementadores 120), anunciado ao sub-agent no corpo da persona. O corte é cego: a cláusula 5 manda `wip` por marco e, a ~70% do teto, relatório `PARCIAL` sem score (`report-incomplete`): retome o agente, não carimbe DEGRADADO.
|
|
322
323
|
|
package/framework/agents.json
CHANGED
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
],
|
|
48
48
|
"model": "sonnet",
|
|
49
49
|
"maxTurns": 120,
|
|
50
|
-
"
|
|
50
|
+
"effort": "medium",
|
|
51
|
+
"persona": "Você é um engenheiro .NET sênior. Pensa em VSA com handlers diretos, result pattern, sealed classes, Guid.CreateVersion7(). Acesso a dados: ApplicationDbContext injetado direto no slice (DbSet<T> já é repository, DbContext já é unit of work); para concorrência ou SQL específico, store dedicado hand-written com justificativa em decisions.md; nunca uma camada de Service/Manager; nunca repository genérico (IRepository<T>/IUnitOfWork). Não usa exceptions como fluxo. Consulta o standard antes de escrever cada slice. Nomeia tudo em PascalCase para tipos, camelCase para parâmetros. REUSO ANTES DE CRIAR: antes de declarar record/class/interface novo, rode `morph-spec graph explain <Nome>` e `graph affected <Conceito> --uses-only` (sem grafo: `grep -rn`); se já existe algo que cobre o caso, reuse ou estenda, e se criar mesmo assim diga por que em uma linha no relatório da task. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
51
52
|
},
|
|
52
53
|
"ef-modeler": {
|
|
53
54
|
"title": "EF Core / Database Modeler",
|
|
@@ -86,7 +87,8 @@
|
|
|
86
87
|
],
|
|
87
88
|
"model": "sonnet",
|
|
88
89
|
"maxTurns": 120,
|
|
89
|
-
"
|
|
90
|
+
"effort": "medium",
|
|
91
|
+
"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. REUSO ANTES DE CRIAR: antes de declarar entidade, configuração ou value object novo, rode `morph-spec graph explain <Nome>` (sem grafo: `grep -rn`) — entidade duplicada vira migration divergente; se criar mesmo assim, justifique em uma linha no relatório da task. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto. NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável (prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — nunca chame tool destrutiva sem confirmação humana explícita."
|
|
90
92
|
},
|
|
91
93
|
"maf-expert": {
|
|
92
94
|
"title": "Microsoft Agent Framework Expert",
|
|
@@ -146,7 +148,8 @@
|
|
|
146
148
|
],
|
|
147
149
|
"model": "sonnet",
|
|
148
150
|
"maxTurns": 120,
|
|
149
|
-
"
|
|
151
|
+
"effort": "medium",
|
|
152
|
+
"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 UseOpenTelemetry(sourceName, configure) no builder, nos DOIS níveis (agente e chat client), com EnableSensitiveData desligado fora de dev — AgentOpenTelemetryConsts e agent.WithOpenTelemetry() NÃO existem no pin (medido por reflexão); os nomes de ActivitySource, span e métrica estão no standard ai-agents-observability-patterns, não os deduza. Para OpenAI o caminho default é a Responses API (GetResponsesClient() + AsAIAgent); no Model Registry, alias que OMITE `api` significa `responses` — Chat Completions é opt-in declarado, nunca suposição. HISTÓRICO DE CONVERSA: conversa própria como default (tabela/jsonb do projeto exposta por um ChatHistoryProvider seu, com store: false), nunca o AgentSession — o AgentSession com provider embutido só é a fonte certa quando o histórico é SÓ a lista de mensagens, sem campo de negócio e sem leitura fora do agente; retry, remontagem e auditoria exigem histórico visível e manipulável. INSTRUÇÕES E TOOLS POR FASE: compositor determinístico em C# testável (um record Instructions + Tools escolhido por switch antes de montar ChatOptions), nem context provider nem tool — AIContext.Tools ACRESCENTA tools e nunca remove, então só o código consegue tirar uma tool da mesa. DESCOBERTA DE STANDARD: a lista acima é o que se cola em todo dispatch, não o catálogo — antes de concluir que um assunto não tem standard, rode `morph-spec standards --find \"<tema>\" --category ai-agents`. REUSO ANTES DE CRIAR: antes de escrever tool, contrato ou serviço de agente novo, rode `morph-spec graph explain <Nome>` (sem grafo: `grep -rn`); se já existe algo que cobre o caso, reuse ou estenda, e se criar mesmo assim justifique em uma linha no relatório da task. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
150
153
|
},
|
|
151
154
|
"nextjs-expert": {
|
|
152
155
|
"title": "Next.js / React Expert",
|
|
@@ -197,7 +200,8 @@
|
|
|
197
200
|
],
|
|
198
201
|
"model": "sonnet",
|
|
199
202
|
"maxTurns": 120,
|
|
200
|
-
"
|
|
203
|
+
"effort": "medium",
|
|
204
|
+
"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. REUSO ANTES DE CRIAR: antes de declarar tipo, hook ou componente novo, rode `morph-spec graph explain <Nome>` (sem grafo: `grep -rn`) — o tipo local redeclarado em cada arquivo é o defeito mais comum aqui; reuse ou estenda o que existe, e se criar mesmo assim justifique em uma linha no relatório da task. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
201
205
|
},
|
|
202
206
|
"ui-designer": {
|
|
203
207
|
"title": "UI/UX Designer",
|
|
@@ -272,14 +276,14 @@
|
|
|
272
276
|
"mcp": [
|
|
273
277
|
"context7",
|
|
274
278
|
"stitch",
|
|
275
|
-
"
|
|
279
|
+
"morph-assets",
|
|
276
280
|
"playwright",
|
|
277
|
-
"seedance",
|
|
278
281
|
"graphify"
|
|
279
282
|
],
|
|
280
283
|
"model": "sonnet",
|
|
281
284
|
"maxTurns": 120,
|
|
282
|
-
"
|
|
285
|
+
"effort": "medium",
|
|
286
|
+
"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. TELA DE DADOS: lista pagina, ordena e filtra no servidor e tem os três estados (vazio, carregando, erro); toda tela sai com o checklist de tela da morph-uiux preenchido, superfície incluída. REUSO ANTES DE CRIAR: componente reutilizável se BUSCA antes de se escrever — rode `morph-spec graph explain <Nome>` e confira `.morph/context/design-system.md` (sem grafo: `grep -rn`); criar um novo exige justificativa em uma linha no relatório da task, a mesma regra que já vale para token de design. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
283
287
|
},
|
|
284
288
|
"infra-engineer": {
|
|
285
289
|
"title": "Infrastructure / Jobs Engineer",
|
|
@@ -322,7 +326,8 @@
|
|
|
322
326
|
],
|
|
323
327
|
"model": "sonnet",
|
|
324
328
|
"maxTurns": 120,
|
|
325
|
-
"
|
|
329
|
+
"effort": "medium",
|
|
330
|
+
"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. COMENTÁRIO E TESTE: nenhum comentário, mesmo com vizinho comentado: nome, tipo e teste explicam (pós-condição no nome do teste); só o porquê invisível, sem `///`/JSDoc, UC/task ou banner; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto. NEON: o MCP `neon` inspeciona schema e ensaia migração numa branch descartável (prepare_database_migration → complete_database_migration); ele também APAGA branch e projeto — nunca chame tool destrutiva sem confirmação humana explícita."
|
|
326
331
|
},
|
|
327
332
|
"evaluator": {
|
|
328
333
|
"title": "Code & Architecture Evaluator",
|
|
@@ -384,6 +389,7 @@
|
|
|
384
389
|
],
|
|
385
390
|
"model": "haiku",
|
|
386
391
|
"maxTurns": 60,
|
|
392
|
+
"effort": "low",
|
|
387
393
|
"persona": "Você é um scout read-only: mapeia o território sem escrever código. Dado um sintoma ou uma feature, localiza os arquivos relevantes, o root cause provável e o blast radius (o que mais é afetado).\n\nCOMECE PELO GRAFO, não pelo grep. `morph-spec graph affected <símbolo> --json` dá o blast radius; `morph-spec graph explain <símbolo> --json` dá a vizinhança com arquivo:linha. Contra o grep isso é 58% menos ruído e, principalmente, classifica a relação: 20 tipos podem IMPLEMENTAR uma interface enquanto só 2 a USAM, e é essa diferença que decide se dá para mexer. Use `--uses-only` quando a pergunta for \"quem realmente usa\" e `--prod` para ignorar consumidores em projeto de teste.\n\nFALLBACK OBRIGATÓRIO: se o comando devolver `no-graph`, `not-found` ou `ambiguous`, caia para Read/Glob/Grep e siga normalmente — o grafo é um subconjunto do grep, então nunca perde arquivo que o grep acharia. Diga no seu mapa qual caminho você usou. Nunca trate ausência de grafo como impedimento.\n\nDUAS RESSALVAS que mudam a leitura do resultado: (1) o grafo pode estar STALE: na raiz o refresh roda em background depois dos comandos git e no início da sessão, e enquanto ele corre o grafo ainda é o do commit anterior (`graph status --json` traz `refresh.running`); numa worktree (`refresh.auto: false`) o grafo é o semeado da raiz e não anda sozinho, então o que a feature criou ali pode não estar nele; confirme no código antes de afirmar que algo NÃO tem consumidor, que é justamente a conclusão perigosa antes de um hotfix. Dublês, fixtures e helpers de teste estão no grafo com `test: true` — é por eles que se acha o fake a reusar num teste novo. (2) Em frontend o grafo é estrutura de módulos, não call graph: ali o blast radius responde QUEM IMPORTA, não quem chama. Em backend responde quem chama.\n\nUsa Read/Glob/Grep e Bash SOMENTE-LEITURA (morph-spec graph, git log/diff/show, ls, cat) — nunca Write/Edit, nunca comandos que mudam estado. Devolve um mapa curto e acionável (arquivos com paths, hipótese de causa, áreas de impacto) como relatório final e encerra. Não implementa, não corrige — ilumina o caminho para quem vai. O MCP `graphify` devolve o grafo CRU: serve para exploração, mas a conclusão sai do `morph-spec graph`, que aplica o saneamento da ADR-003 (em C# o grafo cru dá zero consumidores onde há dois)."
|
|
388
394
|
}
|
|
389
395
|
}
|
package/framework/docs/CLI.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`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). **No `uiux`, a assinatura `auto` é recusada (exit 1) onde o `gate-check uiux` pausaria pelo checklist de tela** — o mesmo predicado do `advance` (`uiuxChecklistPause`, em `risk-detector.js`), lido mesmo sem gate-check antes. Destravam: uma decisão gravada com `pause: false` (`gate-decision {f} uiux --pause false --reason …`, ou um gate-check que não pausou) e a assinatura humana (`--approver`); `--force` não. A decisão vale para o mockup que viu: ela grava `mockupsHash` (o `mockups.md` por conteúdo), e um mockup escrito ou mudado depois dela volta a ser conferido.
|
|
10
10
|
- **`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). Ao encerrar a **uiux**, a assinatura `auto` é recusada (exit 1) pelo mesmo predicado do `approve` e do `gate-check uiux`: checklist de tela incompleto no `1-design/mockups.md` — toda `## Tela:` com o `### Checklist de tela` respondido item a item (`src/lib/specs/screen-checklist.js`), com a `Superfície` sendo uma das `surfaces` da `ui-designer` no `agents.json` instalado. Destravam a decisão gravada com `pause: false` (`gate-decision`, e só para o mockup que ela viu) e a assinatura humana (`--approver <nome>`); `--force` não dispensa o checklist, como no `approve`. Quem avança sobre o checklist incompleto com `--approver` vê o que ficou faltando. Sem `mockups.md`, ou com o mockup no formato antigo (telas `## S001:`, sem `## Tela:`), não há o que cobrar.
|
|
11
11
|
- **`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**. **Duas recusas duras (exit 1), nunca um registro silencioso:** `<score>` em branco ou ausente é **recusado, jamais gravado como `0`** (o `Number('')` que valia `0` gravava um zero fabricado — e era esse zero que o Gate 3 lia depois, como `evalScore: 0 / evalScoreSource: taskScores-proxy`); e `<task>` fora de `^T\d+$` é **recusado** em vez de aninhado (`T3.2` virava `taskScores.T3["2"]` — inválido contra `feature.schema.json` e invisível ao proxy). O `[score]` aparece opcional só para que o valor ausente chegue ao comando e receba uma mensagem que nomeia o argumento e a ordem posicional — ele continua obrigatório. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
|
|
12
|
-
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp). **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `scope` ("task group … does not touch runtime") são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. **E o escopo de task pausa o ship por si só**, antes de qualquer leitura do e2e: se o último stamp `verification` for de escopo `task:{id}`, o `review` pausa com `último morph-spec verify foi de escopo "task:{id}" (recorte parcial de task)`, mesmo com todos os nós verdes — build, testes e validadores daquele verify rodaram só para a task, e desde o recorte automático (ver `verify` abaixo) todo `verify {f} {task}` roda menos que a suíte. O caminho de volta é rodar `morph-spec verify {feature}` antes do ship. Stamp sem `scope` (feature legada) não pausa por esta regra, e fora do `review` ela não vale. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **Prova de mutação:** no ship, o `gate-check` lê do MESMO `evaluator-report.md` se existe a seção `## Mutações` (`evaluatorProofState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.mutationProof = {section, min, productionCode}`. Relatório **sem** a seção numa feature com código de produção (ao menos uma task com `group` diferente de `docs`) **pausa o Gate 3**, qualquer que seja o score: um teste verde prova que o código roda, não que o teste morde. Feature 100% `docs` é isenta, e `tasks.json` ausente também (sem plano não há como afirmar que existe código de produção). `min` é o menor `**Proof-Scores:**` do relatório e impõe teto em `test-coverage` (rubricas em `framework/evals/proof-rubrics/`); `null` é **sem teto**, jamais teto 0. `mutationProof` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos sinais de e2e. **O token vigente é o ÚLTIMO:** o `evaluator-report.md` é append-only, então numa reavaliação o `**Composite-Score:**` que decide o gate é o do FIM do arquivo — ler o primeiro auto-passaria o Gate 3 com a nota superada quando uma correção BAIXA a nota. **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, score, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (rubrica `framework/evals/proof-rubrics/media-claims.md`). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `score` é a chave `media=` dos `**Proof-Scores:**` e impõe teto em `architecture`, **não** em `test-coverage`: ela fica FORA do `min` do `mutationProof`, senão um `media` baixo derrubaria o teto de teste por um motivo que não é de teste. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco. **Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `evaluator-report.md`, e a linha que o avaliador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (rubrica `evals/rubrics/contracts.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
|
|
12
|
+
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, `oversizedTasks` — só no `plan`, pausa com task acima de 8 `outputs` —, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp). **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `scope` ("task group … does not touch runtime") são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. **E o escopo de task pausa o ship por si só**, antes de qualquer leitura do e2e: se o último stamp `verification` for de escopo `task:{id}`, o `review` pausa com `último morph-spec verify foi de escopo "task:{id}" (recorte parcial de task)`, mesmo com todos os nós verdes — build, testes e validadores daquele verify rodaram só para a task, e desde o recorte automático (ver `verify` abaixo) todo `verify {f} {task}` roda menos que a suíte. O caminho de volta é rodar `morph-spec verify {feature}` antes do ship. Stamp sem `scope` (feature legada) não pausa por esta regra, e fora do `review` ela não vale. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **Prova de mutação:** no ship, o `gate-check` lê do MESMO `evaluator-report.md` se existe a seção `## Mutações` (`evaluatorProofState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.mutationProof = {section, min, productionCode}`. Relatório **sem** a seção numa feature com código de produção (ao menos uma task com `group` diferente de `docs`) **pausa o Gate 3**, qualquer que seja o score: um teste verde prova que o código roda, não que o teste morde. Feature 100% `docs` é isenta, e `tasks.json` ausente também (sem plano não há como afirmar que existe código de produção). `min` é o menor `**Proof-Scores:**` do relatório e impõe teto em `test-coverage` (rubricas em `framework/evals/proof-rubrics/`); `null` é **sem teto**, jamais teto 0. `mutationProof` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos sinais de e2e. **O token vigente é o ÚLTIMO:** o `evaluator-report.md` é append-only, então numa reavaliação o `**Composite-Score:**` que decide o gate é o do FIM do arquivo — ler o primeiro auto-passaria o Gate 3 com a nota superada quando uma correção BAIXA a nota. **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, score, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (rubrica `framework/evals/proof-rubrics/media-claims.md`). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `score` é a chave `media=` dos `**Proof-Scores:**` e impõe teto em `architecture`, **não** em `test-coverage`: ela fica FORA do `min` do `mutationProof`, senão um `media` baixo derrubaria o teto de teste por um motivo que não é de teste. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco. **Checklist de tela:** na `uiux` e no `review`, o `gate-check` grava `signals.screenChecklist` lido do `1-design/mockups.md` (`mockups`, `legacy`, `screens`, `problems`). Na **uiux**, checklist incompleto **pausa** nomeando a tela e o item, e um mockup sem nenhuma `## Tela:` é o problema "nenhuma tela declarada". No **review**, o que se cobra é o item **devido** — decidido, nem "não se aplica" nem a superfície (`uncovered`): cada um exige uma linha na seção `## Checklist de tela` do `evaluator-report.md`, e a linha que o avaliador marca como não verificável conta. Com item devido, relatório sem a seção ou com linha faltando (`section`, `uncovered`) **pausa** o Gate 3 (rubrica `evals/rubrics/contracts.md`, "Contrato de tela"); tela toda "não se aplica" não deve nada e não pausa. **Mockup legado** — telas no formato antigo (`## S001: …`) e nenhuma `## Tela:`, o que o template anterior gerava — é **isento nas duas fases**, com a isenção à vista: o sinal grava `exempt: "checklist dispensado: mockup no formato antigo"` e a mesma razão aparece em "risk signals present". Mockup ausente não pausa. `screenChecklist` **não** é sobrescrevível por `--signals`.
|
|
13
13
|
- **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
|
|
14
14
|
- **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`score`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/score/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
|
|
15
15
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
- **`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.
|
|
29
29
|
- **`morph-spec worktree setup|provision|link|remove|list`** — gerencia o ciclo do worktree (`setup <f>` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `provision <n> [--base <ref>]` cria `worktrees/{n}/` na branch `morph-task/{n}` — um worktree de **isolamento de task**, sem feature, com as mesmas junctions e um bloco próprio sob a chave `task/{n}`; `link --all` religa a infra compartilhada **e regenera o `worktree.env`** com o layout atual; `list` mostra fase/gate de cada worktree ativo). `remove` recusa, antes de tocar em qualquer junction, quando o cwd do processo está no alvo (`cwd-inside-worktree`) **ou quando a sessão do Claude Code que o rodou está ancorada nele** (`session-anchored-in-worktree` — mesmo sinal do `finish`, acima); a recusa é o adiamento: feche a sessão e rode de uma aberta na raiz. Falha de remoção reporta a CAUSA real: um diretório travado vence o eco `is not a working tree` das tentativas seguintes, e `alreadyDeregistered` avisa quando o git já desregistrou a árvore e só o diretório vazio sobrou. **Fechar a feature** continua sendo `morph-spec finish` (acima).
|
|
30
30
|
**Worktrees nativos do Claude Code.** `EnterWorktree`, `claude -w <n>` e `Agent` com `isolation: "worktree"` passam pelo hook `WorktreeCreate` instalado pelo morph: nome conhecido como feature → `worktree setup`; senão → `worktree provision`. O resultado cai sempre em `worktrees/`, nunca em `.claude/worktrees/`. O hook `WorktreeRemove` chama `worktree remove --target <path> --if-clean` (só remove árvore limpa; a branch sobrevive) — mas ele **não disparou** em nenhum run headless medido, então a limpeza primária continua sendo o passo 4 de `morph-apply` §3b-w e o `doctor --worktrees`. `git worktree add` cru é bloqueado pelo hook `block-raw-worktree-add` (sem junctions, sem portas, sem identidade → invisível ao `fleet`).
|
|
31
|
+
**Base da árvore de task.** Com `worktree.baseRef` ausente nas settings e o cwd de quem despacha numa branch `morph/{f}`, o hook passa `--base morph/{f}`: o sub-agent do apply nasce com o trabalho da feature (nada de `git merge --ff-only morph/{f}` como passo 0). `baseRef: "head"` usa o que o cwd tiver checado; `baseRef: "fresh"` explícito força a branch default. Com base `morph/{f}`, o `worktree.env` grava `MORPH_FEATURE={f}` — portas e `COMPOSE_PROJECT_NAME` seguem os da chave `task/{n}` — e o `link --all` preserva o valor. O `provision` também **copia** (arquivo, nunca junction) o `.env`/`.env.*` da raiz que o git ignora e que a árvore não tem; no desmonte a cópia idêntica à raiz é `replicatedInRoot`, apagada sem backup.
|
|
31
32
|
**Layout do bloco (10 portas):** `PORT` (+0), `API_PORT` (+1, `ASPNETCORE_URLS` em dotnet), `POSTGRES_PORT` (+2), `REDIS_PORT` (+3), +4..+9 livres; mais `COMPOSE_PROJECT_NAME=morph-{f}`, `MORPH_PORT_BASE/RANGE`. O mesmo layout alimenta o `worktree.env`, o `docker compose` do `verify`/`e2e` e o hook `worktree-env-inject` (que prefixa `npm run dev`/`dotnet run`/`docker compose`/`playwright test` com `export …` quando o cwd tem manifesto e o comando não escolheu porta própria).
|
|
32
33
|
- **`morph-spec env [feature] [--json|--powershell]`** — imprime o bloco do worktree em forma shell-loadable (`eval "$(morph-spec env)"`; PowerShell: `morph-spec env --powershell | Invoke-Expression`). Feature vem do argumento ou da branch `morph/{f}`/`morph-task/{n}` do cwd. Deriva de `buildComposeEnv`, a mesma função do nó e2e — um shell e o stack nunca discordam. É o caminho para quem roda **fora** do Claude Code (terminal humano, CI, sub-agent sem hooks). As credenciais do arquivo apontado por `e2e.auth.envFile` entram no ambiente, mas **chave de layout de portas vinda de lá é ignorada** — o bloco alocado a esta worktree prevalece (um `.env.e2e` copiado de outra árvore carrega o bloco DELA). A chave ignorada é nomeada em **stderr**, nunca em stdout, para o `eval` do shell seguir limpo.
|
|
33
34
|
- **`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho. Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build ou cache regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `.next/`, `.turbo/`, `.pytest_cache/`, `TestResults/`, `graphify-out/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`) — e nunca entra no backup, nem quando está DENTRO de um diretório exclusivo que o git reporta como uma entrada só; o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`), com a retenção descrita abaixo aplicada logo depois (`backupRetention` na saída, quando podou algo). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido.
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
|
|
40
41
|
## Validação
|
|
41
42
|
|
|
42
|
-
- **`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). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. UC com nome fora do padrão `UC-{nn}-{slug}.md` (slug com maiúscula ou `_`) continua encontrado, com aviso: o `finish` não o promove ao acervo. `doneCriteria` com vocabulário de artefato (`grep`, "contém a string", "nenhum arquivo", glob `**/`…) sem `Por que artefato:` também é aviso, sob `⚠ Critérios de artefato` — o exit não muda, e o `--json` traz a chave irmã `artifactCriteria`. Os achados de ciclo de vida do `usecase check` (task sobre UC `superseded`, revogação que o `finish` não aplicaria) saem sob `⚠ Ciclo de vida do acervo`, também só aviso, na chave `lifecycle`. `--mermaid` nunca é bloqueado.
|
|
43
|
-
- **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--full] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza. **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). **No escopo de task o recorte é automático:** `verify {f} {task}` deixa o `group` da task escolher os nós e os arquivos de teste de `outputs` estreitarem o comando dentro de cada nó — o relógio de uma task não paga a suíte inteira (regras, veredito e limites em *O recorte por task*, na seção **Suítes de teste**). **`--filter <ids>` força a escolha de nós** (lista separada por vírgula, casando `id` exato ou substring; desliga a escolha pelo `group`, e o estreitamento dentro dos nós continua) e **`--full` desliga o recorte** (o `group` não escolhe nós e nenhum comando é estreitado; os nós com `e2e: true` e os de `runner: "evals"` continuam só no escopo de feature; no escopo de feature `--full` é no-op, porque ali nada é recortado). O Gate 3 continua sendo `verify {f} --json` **sem filtro**, porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` de escopo de **feature** não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um de escopo de feature com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. No escopo de task as três gravam normalmente — o stamp já diz `task:{id}`, e o Gate 3 pausa nele. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar. **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar. Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. **Com `{task}` também é erro de uso (exit 2)**, recusado antes do build: no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline — o gravador ainda recusa, uma segunda vez, qualquer medição que carregue `selection`. Também **não** persiste o stamp `verification`: é manutenção de config, não prova de gate. **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado. **`--no-persist` não grava nada no `feature.json`** — nem o histórico `verifications.*` nem o stamp `verification` que o gate lê: a execução vira sonda, nunca prova para gate algum, qualquer que seja a regra de carimbo do escopo. E o `--filter` só estreita o nó de testes (build, validadores e e2e rodam igual). Por isso a execução de evidência de eval do `morph-implement` §5 é `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist`: só o eval, sem stack, sem escrever no `feature.json`. **Rodada repetida:** no escopo de feature, se nada mudou fora de `.morph/` desde o verify de feature anterior (HEAD, diff, arquivos não rastreados e o `config.json`), o `--json` traz `repeatOf: { ts, status, sha }` e a saída humana avisa — é aviso, a rodada executa e carimba igual, porque só o código sabe que a árvore não mudou, não que o ambiente (Docker, banco) também não. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo. **Validador que não pôde checar:** o `reuse` sem grafo não acha nada, e um `validators: pass` dos outros validadores o esconderia — então o `feedback[]` diz `[validators/reuse] não rodou: …`, e o nó leva `skipped: [{ validator, reason, unavailable }]` (quando o nó inteiro pula, a própria linha dele já dá o motivo).
|
|
43
|
+
- **`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). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. UC com nome fora do padrão `UC-{nn}-{slug}.md` (slug com maiúscula ou `_`) continua encontrado, com aviso: o `finish` não o promove ao acervo. `doneCriteria` com vocabulário de artefato (`grep`, "contém a string", "nenhum arquivo", glob `**/`…) sem `Por que artefato:` também é aviso, sob `⚠ Critérios de artefato` — o exit não muda, e o `--json` traz a chave irmã `artifactCriteria`. Os achados de ciclo de vida do `usecase check` (task sobre UC `superseded`, revogação que o `finish` não aplicaria) saem sob `⚠ Ciclo de vida do acervo`, também só aviso, na chave `lifecycle`. **Tamanho de task:** task com mais de 8 `outputs` (`MAX_TASK_OUTPUTS`) é **erro** (exit 1) enquanto o Gate 2 não foi aprovado — fatie por pós-condição — e só aviso depois dele (chore/hotfix nascem com o plano aprovado por política); o `--json` traz `taskSize: {max, oversized, blocking}`. `--mermaid` nunca é bloqueado.
|
|
44
|
+
- **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--full] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-tests] [--skip-build] [--skip-e2e] [--fresh-e2e] [--no-persist]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza. **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). **No escopo de task o recorte é automático:** `verify {f} {task}` deixa o `group` da task escolher os nós e os arquivos de teste de `outputs` estreitarem o comando dentro de cada nó — o relógio de uma task não paga a suíte inteira (regras, veredito e limites em *O recorte por task*, na seção **Suítes de teste**). **`--filter <ids>` força a escolha de nós** (lista separada por vírgula, casando `id` exato ou substring; desliga a escolha pelo `group`, e o estreitamento dentro dos nós continua) e **`--full` desliga o recorte** (o `group` não escolhe nós e nenhum comando é estreitado; os nós com `e2e: true` e os de `runner: "evals"` continuam só no escopo de feature; no escopo de feature `--full` é no-op, porque ali nada é recortado). O Gate 3 continua sendo `verify {f} --json` **sem filtro**, porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem) — e um `verify {f} --filter …` de escopo de **feature** não persiste o stamp (`persisted: false`, `reason: 'filtered'`), nem um de escopo de feature com `--skip-tests` (`reason: 'skip-tests'`) ou `--skip-build` (`reason: 'skip-build'`): um nó pulado não reprova o rollup, e rodada parcial nunca fica no lugar da prova do Gate 3. No escopo de task as três gravam normalmente — o stamp já diz `task:{id}`, e o Gate 3 pausa nele. O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar. **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar. Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. **Com `{task}` também é erro de uso (exit 2)**, recusado antes do build: no escopo de task o verify recorta nós e testes, e baseline de recorte não é baseline — o gravador ainda recusa, uma segunda vez, qualquer medição que carregue `selection`. Também **não** persiste o stamp `verification`: é manutenção de config, não prova de gate. **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado. **`--no-persist` não grava nada no `feature.json`** — nem o histórico `verifications.*` nem o stamp `verification` que o gate lê: a execução vira sonda, nunca prova para gate algum, qualquer que seja a regra de carimbo do escopo. E o `--filter` só estreita o nó de testes (build, validadores e e2e rodam igual). Por isso a execução de evidência de eval do `morph-implement` §5 é `verify {f} --filter {id-do-nó-evals} --skip-e2e --no-persist`: só o eval, sem stack, sem escrever no `feature.json`. **Onde grava:** só na árvore DONA da feature (`resolveFeatureTree`, a regra do guard dos mutadores, perguntada da raiz) — o worktree com `morph/{f}` checada, mesmo sem ponteiro `worktree.path` na raiz, ou a raiz quando nenhum worktree vivo a possui. Fora dela, `persisted: false` com `reason: 'unregistered-worktree'` (outro worktree, como o `morph-task/{n}` de um sub-agent) ou `'owner-tree-elsewhere'` (a raiz, com o dono vivo em outro lugar). **Rodada repetida:** no escopo de feature, se nada mudou fora de `.morph/` desde o verify de feature anterior (HEAD, diff, arquivos não rastreados e o `config.json`), o `--json` traz `repeatOf: { ts, status, sha }` e a saída humana avisa — é aviso, a rodada executa e carimba igual, porque só o código sabe que a árvore não mudou, não que o ambiente (Docker, banco) também não. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo. **Validador que não pôde checar:** o `reuse` sem grafo não acha nada, e um `validators: pass` dos outros validadores o esconderia — então o `feedback[]` diz `[validators/reuse] não rodou: …`, e o nó leva `skipped: [{ validator, reason, unavailable }]` (quando o nó inteiro pula, a própria linha dele já dá o motivo).
|
|
44
45
|
- **Validador `code-budget` (nó `validators` do `verify`)** — mede o que a árvore **adicionou** contra a branch default: `git diff -U0` a partir do merge-base com `origin/{default}` (ou `{default}` local), arquivo novo não rastreado conta inteiro e arquivo movido sem edição não conta. Lê `.cs`/`.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`, fora de `.d.ts`, de `node_modules`/`bin`/`obj`/`dist`/`.morph` e de código gerado (`*.Designer.cs`, `*.g.cs`, `*ModelSnapshot.cs`). Três medidas, cada uma **um aviso, nunca erro**, mapeado em `code_quality` da rubrica: (a) **linhas de comentário adicionadas que citam processo** — `UC-nn`, `ADR-nn`, `FR-nn`, `T{n}`, `Gate N`, `spec.md`/`mandate.md`, mockup — avisa a partir de uma, com a contagem sobre o total de linhas de comentário adicionadas e até 4 `arquivo:linha` (standard `architecture-code-comments`); comentário é o que o varredor de `src/lib/ai/comment-stripper.js` separa de string, com template e regex literal de TS/JS; (b) **linhas não vazias de teste ÷ de produção** — avisa acima de **1,5×**, e só quando o teste adicionado passa de **40 linhas** (a regressão de um bug de duas linhas cabe abaixo disso); sem linha de produção, o aviso dá as duas contagens sem razão. (c) **densidade de comentário** — linhas de comentário adicionadas ÷ linhas de código adicionadas (as não vazias menos as de comentário), doc-comment (`///`, JSDoc) incluído — avisa acima de **1 a cada 10**, e só a partir de **40 linhas** de código, com até 4 arquivos mais comentados e suas contagens (o padrão do standard `architecture-code-comments` é nenhum comentário). Teste é o que o validador `reuse` já trata como teste: arquivo `.test.` ou `.spec.` com extensão `js`/`jsx`/`ts`/`tsx`, e tudo sob `__tests__/`, `test/`, `tests/` ou pasta terminada em `Test`/`Tests` (`Foo.Tests/`, `FooTests/`). O resto conta como produção, inclusive `a.test.mjs`, `a.test.cjs` e `*.e2e.ts` fora dessas pastas. Sem base git, ou sem linha de código adicionada, pula com razão; qualquer falha de medição também vira skip, nunca erro. Fica fora do modo `light`. Excesso justificado não silencia o aviso: a justificativa mora na task (orçamento do `morph-plan` passo 5), e é lá que o avaliador a lê.
|
|
45
46
|
- **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. O motivo impresso diz **o que** decidiu o reuso, e as duas procedências nunca se confundem: `source unchanged since build` é o carimbo, `source is bind-mounted` é o disco. E a segunda só sai quando **todo** serviço com seção `build:` monta a raiz do projeto — um sidecar Playwright montando `./e2e` ao lado de uma app compilada em imagem não fala pela stack inteira; nesse caso quem decide é o carimbo. `--fresh` derruba com volumes (`down -v`), apaga o carimbo e re-roda o seed. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
|
|
46
47
|
- **`morph-spec doctor [--full] [--mcp] [--reset]`** — checagem de saúde padrão (sem `--worktrees`): instalação (versões, `.claude/`, hooks, agents, grafo de codebase, templates, `state.json`) mais duas auditorias. Uma linha `.git/morph-spec (<tamanho> · N backup(s) do finish)` mostra o que o morph-spec guarda dentro do `.git` (backups do finish, registros de porta e de claim), que não aparece em `git status`: `✓` normalmente, `⚠` quando há backup legado esperando `--prune`, backup fora da retenção ou `.env*` além do prazo curto (com o comando de poda) — nunca `✗`, e nenhuma linha quando não há nada guardado.
|
|
@@ -261,6 +262,7 @@ passa com aviso de alta, e o comando grava.
|
|
|
261
262
|
|
|
262
263
|
| Momento | Comando | Por quê |
|
|
263
264
|
| --- | --- | --- |
|
|
265
|
+
| Sub-agent implementador | só o filtro dos próprios testes, **uma vez**, antes do relatório (nunca `morph-spec verify`, nunca red→green) | A prova é do orquestrador; cada rodada extra dentro do sub-agent custa turnos que o teto corta |
|
|
264
266
|
| Loop por task | `verify {f} {task}` (recorte automático: o `group` escolhe os nós, os `outputs` estreitam os testes — ver *O recorte por task*; `--filter {stack-da-task}` só para forçar os nós, `--full` para desligar) | O relógio de uma task não paga a suíte inteira; a task tocou **um** stack |
|
|
265
267
|
| Gate 3 | `verify {f} --json` (sem filtro) | A prova que o avaliador lê tem de cobrir **todos** os nós |
|
|
266
268
|
| CI | a suíte inteira, todos os nós | idem |
|
|
@@ -52,11 +52,12 @@ framework/hooks/
|
|
|
52
52
|
| **PreToolUse** (Bash) | pre-bash-denylist.js | Block | Blocks `rm -rf .morph/` and direct state edits |
|
|
53
53
|
| **PreToolUse** (Bash) | block-destructive-worktree-removal.js | Block | Blocks `git worktree remove` / `rm -rf` / `Remove-Item -Recurse` on a directory holding reparse points (the infra junctions a raw removal would FOLLOW out, emptying the root's `.claude/`); names `morph-spec worktree remove` |
|
|
54
54
|
| **PreToolUse** (Bash) | block-raw-worktree-add.js | Block | Blocks raw `git worktree add` (a tree with no junctions → no hooks, no port block, no identity, outside `worktrees/`); names `morph-spec worktree setup <feature>` / `worktree provision <name> --base <ref>`. `list`/`prune`/`remove`/`lock`/`move`/`repair` pass |
|
|
55
|
-
| **WorktreeCreate** | worktree-create.js | Replaces native | Fires for `claude -w`, EnterWorktree and `Agent isolation:"worktree"`; replaces Claude Code's bare `git worktree add`. Runs `morph-spec worktree setup <n>` (known feature) or `worktree provision <n>` (task tree, branch `morph-task/<n>`),
|
|
55
|
+
| **WorktreeCreate** | worktree-create.js | Replaces native | Fires for `claude -w`, EnterWorktree and `Agent isolation:"worktree"`; replaces Claude Code's bare `git worktree add`. Runs `morph-spec worktree setup <n>` (known feature) or `worktree provision <n>` (task tree, branch `morph-task/<n>`), bases a task tree on the dispatcher's `morph/<f>` branch when `worktree.baseRef` is unset (`"head"` = current HEAD, `"fresh"` = default branch), prints the path as its last stdout line. CLI unreachable → plain git, still under `<root>/worktrees/` |
|
|
56
56
|
| **WorktreeRemove** | worktree-remove.js | Fail-open | Routes Claude Code's own cleanup through `morph-spec worktree remove --target --if-clean` (junctions dropped first; a dirty tree is left alone). Always exit 0. Secondary net — the event was observed NOT to fire headless; the Bash guard above stays the primary defense |
|
|
57
57
|
| **PostToolUse** (Write\|Edit) | post-edit-typecheck.js | Advisory | Debounced `tsc --noEmit` after TS/TSX edits; silent on success. Also touches `.morph/memory/build-dirty.json` on every source-file edit — the write side of validate-completion.js's Stop-hook build-dirty gate |
|
|
58
58
|
| **PostToolUse** (Write\|Edit) | tasks-json-guard.js | Advisory | Validates the written tasks.json schema (status/effort enums); warns so the LLM fixes it immediately |
|
|
59
59
|
| **PostToolUse** (Write\|Edit) | trace-autogen.js | State sync | Auto-generates `.morph/traces/YYYY-MM-DD-{feature}-implement.json` from tasks.json + recap.md + taskScores on every tasks.json write — the skills never write traces by hand |
|
|
60
|
+
| **PostToolUse** (Write\|Edit) | comment-guard.js | Blocking | Blocks when the edit ADDED a comment the `architecture-code-comments` standard never allows: process history (UC/ADR/task/gate), banner/`#region`, a block over 3 lines, or a doc-comment outside a published package. Old comments are never reported; last link of the chain |
|
|
60
61
|
| **PostToolUse** (Write\|Edit\|AskUserQuestion) | state-sync.js | State sync | Autonomous state updates (feature register, output tracking, gate approval) |
|
|
61
62
|
| **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) |
|
|
62
63
|
| **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` |
|
|
@@ -101,6 +101,8 @@ async function main() {
|
|
|
101
101
|
|
|
102
102
|
// PreToolUse: o primeiro bloqueio vence e encerra a cadeia.
|
|
103
103
|
if (result.decision === 'block') {
|
|
104
|
+
// A later link blocking must not swallow what earlier links reported (a tsc error on the same edit).
|
|
105
|
+
if (contexts.length) result = { ...result, reason: [...contexts, result.reason].join('\n\n') };
|
|
104
106
|
console.log(JSON.stringify(result));
|
|
105
107
|
process.exit(0);
|
|
106
108
|
}
|