@polymorphism-tech/morph-spec 8.37.1 → 8.38.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 +19 -6
- package/README.md +2 -2
- package/bin/morph-spec.js +16 -7
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -3
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +76 -27
- package/framework/docs/CLI.md +54 -17
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/morph-workflow.md +1 -0
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -1
- 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 +36 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +39 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +72 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- 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 +34 -9
- package/framework/standards/STANDARDS.json +187 -12
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +64 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/feature/decisions.md +124 -122
- package/package.json +1 -1
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +82 -10
- package/src/commands/project/update.js +25 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/scanner/agents-scanner.js +5 -1
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/evaluator-report.js +25 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +172 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/setup-infra.js +29 -18
- package/src/utils/agents-installer.js +281 -12
- package/src/utils/hooks-installer.js +75 -15
package/framework/agents.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.
|
|
2
|
+
"version": "5.4.0",
|
|
3
3
|
"schema": "domain-personas",
|
|
4
4
|
"total_agents": 8,
|
|
5
|
-
"description": "Domain personas dispatched per task via subagent_type morph-{id} (.claude/agents/ generated from this file — single source of truth, ADR-001). Two independent axes: domain (dotnet-senior, ef-modeler, maf-expert, nextjs-expert, ui-designer, infra-engineer, evaluator) and stage (scout — read-only diagnosis before implementation). Optional per-agent fields: `tools` (comma-separated tool scope; default Read, Write, Edit, Bash, Glob, Grep), `model` (dimensioned per stage/criticality — evaluator: opus, domain implementers: sonnet, scout: haiku; no longer uniformly inherit), `mcp` (registry ids from framework/mcp/registry.json, granted as mcp__{id}). The 3 roles (Orchestrator/Specialist/Evaluator) are assumed by Claude itself per MORPH.md.",
|
|
5
|
+
"description": "Domain personas dispatched per task via subagent_type morph-{id} (.claude/agents/ generated from this file — single source of truth, ADR-001). Two independent axes: domain (dotnet-senior, ef-modeler, maf-expert, nextjs-expert, ui-designer, infra-engineer, evaluator) and stage (scout — read-only diagnosis before implementation). Optional per-agent fields: `tools` (comma-separated tool scope; default Read, Write, Edit, Bash, Glob, Grep), `model` (dimensioned per stage/criticality — evaluator: opus, domain implementers: sonnet, scout: haiku; no longer uniformly inherit), `mcp` (registry ids from framework/mcp/registry.json, granted as mcp__{id}), `maxTurns` (turn ceiling of the generated subagent, integer >= 1; default 20 when absent — ADR-004: implementers 40, evaluator 30, scout 20; the generated body announces it to the sub-agent with the ~70% checkpoint turn), `surfaces` (standards split by kind of screen — `{ \"<surface>\": { when, standards: [ids] } }`; the persona reads `standards` on every surface plus only the list of the screen's surface, which the mockup checklist names; a standard is universal OR of a surface, never both). The 3 roles (Orchestrator/Specialist/Evaluator) are assumed by Claude itself per MORPH.md.",
|
|
6
6
|
"agents": {
|
|
7
7
|
"dotnet-senior": {
|
|
8
8
|
"title": ".NET Senior Engineer",
|
|
@@ -38,13 +38,15 @@
|
|
|
38
38
|
"backend-dotnet-vsa-handler-patterns",
|
|
39
39
|
"backend-dotnet-fluent-validation-vsa",
|
|
40
40
|
"backend-dotnet-testing",
|
|
41
|
-
"backend-integrations-neon-auth-neon-auth"
|
|
41
|
+
"backend-integrations-neon-auth-neon-auth",
|
|
42
|
+
"architecture-code-comments"
|
|
42
43
|
],
|
|
43
44
|
"mcp": [
|
|
44
45
|
"context7"
|
|
45
46
|
],
|
|
46
47
|
"model": "sonnet",
|
|
47
|
-
"
|
|
48
|
+
"maxTurns": 40,
|
|
49
|
+
"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: comentário só com o porquê que o código não mostra, nunca história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
48
50
|
},
|
|
49
51
|
"ef-modeler": {
|
|
50
52
|
"title": "EF Core / Database Modeler",
|
|
@@ -72,13 +74,16 @@
|
|
|
72
74
|
"standards": [
|
|
73
75
|
"backend-database-postgresql-database",
|
|
74
76
|
"infrastructure-neon-setup",
|
|
75
|
-
"infrastructure-neon-pgvector"
|
|
77
|
+
"infrastructure-neon-pgvector",
|
|
78
|
+
"architecture-code-comments",
|
|
79
|
+
"backend-dotnet-testing"
|
|
76
80
|
],
|
|
77
81
|
"mcp": [
|
|
78
82
|
"context7"
|
|
79
83
|
],
|
|
80
84
|
"model": "sonnet",
|
|
81
|
-
"
|
|
85
|
+
"maxTurns": 40,
|
|
86
|
+
"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: comentário só com o porquê que o código não mostra, nunca história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
82
87
|
},
|
|
83
88
|
"maf-expert": {
|
|
84
89
|
"title": "Microsoft Agent Framework Expert",
|
|
@@ -128,13 +133,16 @@
|
|
|
128
133
|
"ai-agents-testing-ai",
|
|
129
134
|
"ai-agents-context-providers",
|
|
130
135
|
"ai-agents-agent-session",
|
|
131
|
-
"ai-agents-durable-workflows-hitl"
|
|
136
|
+
"ai-agents-durable-workflows-hitl",
|
|
137
|
+
"architecture-code-comments",
|
|
138
|
+
"backend-dotnet-testing"
|
|
132
139
|
],
|
|
133
140
|
"mcp": [
|
|
134
141
|
"context7"
|
|
135
142
|
],
|
|
136
143
|
"model": "sonnet",
|
|
137
|
-
"
|
|
144
|
+
"maxTurns": 40,
|
|
145
|
+
"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: comentário só com o porquê que o código não mostra, nunca história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
138
146
|
},
|
|
139
147
|
"nextjs-expert": {
|
|
140
148
|
"title": "Next.js / React Expert",
|
|
@@ -155,7 +163,11 @@
|
|
|
155
163
|
"client component",
|
|
156
164
|
"tailwind",
|
|
157
165
|
"framer motion",
|
|
158
|
-
"shadcn"
|
|
166
|
+
"shadcn",
|
|
167
|
+
"data table",
|
|
168
|
+
"pagination",
|
|
169
|
+
"react-hook-form",
|
|
170
|
+
"tanstack table"
|
|
159
171
|
],
|
|
160
172
|
"stack": [
|
|
161
173
|
"nextjs"
|
|
@@ -166,13 +178,21 @@
|
|
|
166
178
|
"frontend-nextjs-data-fetching",
|
|
167
179
|
"frontend-nextjs-naming-conventions",
|
|
168
180
|
"frontend-nextjs-state-management",
|
|
169
|
-
"
|
|
181
|
+
"frontend-nextjs-forms",
|
|
182
|
+
"frontend-admin-app-data-table",
|
|
183
|
+
"frontend-admin-app-forms",
|
|
184
|
+
"frontend-admin-app-feedback",
|
|
185
|
+
"frontend-admin-app-page-structure",
|
|
186
|
+
"backend-integrations-neon-auth-neon-auth",
|
|
187
|
+
"architecture-code-comments",
|
|
188
|
+
"frontend-nextjs-testing"
|
|
170
189
|
],
|
|
171
190
|
"mcp": [
|
|
172
191
|
"context7"
|
|
173
192
|
],
|
|
174
193
|
"model": "sonnet",
|
|
175
|
-
"
|
|
194
|
+
"maxTurns": 40,
|
|
195
|
+
"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: comentário só com o porquê que o código não mostra, nunca história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
176
196
|
},
|
|
177
197
|
"ui-designer": {
|
|
178
198
|
"title": "UI/UX Designer",
|
|
@@ -201,7 +221,12 @@
|
|
|
201
221
|
"scrub",
|
|
202
222
|
"gsap",
|
|
203
223
|
"lenis",
|
|
204
|
-
"seo"
|
|
224
|
+
"seo",
|
|
225
|
+
"admin",
|
|
226
|
+
"dashboard",
|
|
227
|
+
"data table",
|
|
228
|
+
"pagination",
|
|
229
|
+
"form"
|
|
205
230
|
],
|
|
206
231
|
"stack": [
|
|
207
232
|
"*"
|
|
@@ -210,17 +235,35 @@
|
|
|
210
235
|
"frontend-design-system-naming",
|
|
211
236
|
"frontend-design-system-animations",
|
|
212
237
|
"frontend-nextjs-motion-patterns",
|
|
213
|
-
"
|
|
214
|
-
"frontend-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
"architecture-code-comments",
|
|
239
|
+
"frontend-nextjs-testing"
|
|
240
|
+
],
|
|
241
|
+
"surfaces": {
|
|
242
|
+
"marketing-site": {
|
|
243
|
+
"when": "site de marketing: landing page, institucional, campanha — narrativa, conversão, movimento e SEO",
|
|
244
|
+
"standards": [
|
|
245
|
+
"frontend-design-system-premium-finish",
|
|
246
|
+
"frontend-design-system-ai-image-generation",
|
|
247
|
+
"frontend-design-system-ai-video-generation",
|
|
248
|
+
"frontend-scroll-driven-frame-scrub",
|
|
249
|
+
"frontend-scroll-driven-smooth-scroll",
|
|
250
|
+
"frontend-scroll-driven-scroll-components",
|
|
251
|
+
"frontend-seo-meta-schema-audit",
|
|
252
|
+
"frontend-gsap-core",
|
|
253
|
+
"frontend-gsap-scrolltrigger",
|
|
254
|
+
"frontend-gsap-plugins"
|
|
255
|
+
]
|
|
256
|
+
},
|
|
257
|
+
"admin-app": {
|
|
258
|
+
"when": "app de dados: painel administrativo, CRUD, back office, dashboard — lista, tabela, formulário e densidade",
|
|
259
|
+
"standards": [
|
|
260
|
+
"frontend-admin-app-data-table",
|
|
261
|
+
"frontend-admin-app-forms",
|
|
262
|
+
"frontend-admin-app-feedback",
|
|
263
|
+
"frontend-admin-app-page-structure"
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
},
|
|
224
267
|
"tools": "Read, Write, Edit, Glob, Grep",
|
|
225
268
|
"mcp": [
|
|
226
269
|
"context7",
|
|
@@ -230,7 +273,8 @@
|
|
|
230
273
|
"seedance"
|
|
231
274
|
],
|
|
232
275
|
"model": "sonnet",
|
|
233
|
-
"
|
|
276
|
+
"maxTurns": 40,
|
|
277
|
+
"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: comentário só com o porquê que o código não mostra, nunca história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
234
278
|
},
|
|
235
279
|
"infra-engineer": {
|
|
236
280
|
"title": "Infrastructure / Jobs Engineer",
|
|
@@ -262,13 +306,16 @@
|
|
|
262
306
|
"infrastructure-docker-local-compose-isolation",
|
|
263
307
|
"backend-dotnet-scrutor",
|
|
264
308
|
"ai-agents-observability-patterns",
|
|
265
|
-
"ai-agents-cost-and-budget"
|
|
309
|
+
"ai-agents-cost-and-budget",
|
|
310
|
+
"architecture-code-comments",
|
|
311
|
+
"backend-dotnet-testing"
|
|
266
312
|
],
|
|
267
313
|
"mcp": [
|
|
268
314
|
"context7"
|
|
269
315
|
],
|
|
270
316
|
"model": "sonnet",
|
|
271
|
-
"
|
|
317
|
+
"maxTurns": 40,
|
|
318
|
+
"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: comentário só com o porquê que o código não mostra, nunca história de task/UC/ADR; um teste por pós-condição, cenários do mesmo comportamento em tabela, nenhum teste que só fixa texto."
|
|
272
319
|
},
|
|
273
320
|
"evaluator": {
|
|
274
321
|
"title": "Code & Architecture Evaluator",
|
|
@@ -296,6 +343,7 @@
|
|
|
296
343
|
"playwright"
|
|
297
344
|
],
|
|
298
345
|
"model": "opus",
|
|
346
|
+
"maxTurns": 30,
|
|
299
347
|
"persona": "Você aplica as rubricas em .morph/framework/evals/rubrics/. Você não participou da implementação — avalia o mandate, o diff e a saída real dos testes, com contexto fresco. O orquestrador anexa o resultado do `morph-spec verify`, mas você tem Bash para conferir por conta própria: rode `morph-spec verify {feature} [task]`, testes/build do stack, `git log/diff/show` — nunca confie cegamente no que foi reportado. Bash é só para verificação: NUNCA para modificar código-fonte, tasks.json, feature.json ou o board de sessão — quem faz isso é o loop de correção, não você. Você também tem Playwright: quando o nó `e2e` do verify subiu o stack, NAVEGUE a aplicação como um usuário faria e percorra os fluxos da feature — ler o diff prova que o código existe, usar a aplicação prova que ela funciona. Confira console e network atrás de erro que nenhum teste cobre. Quem sobe e derruba o stack é o `morph-spec verify`/`morph-spec e2e`, não você: se ele não subiu, isso é um achado de infra a reportar, não um defeito de implementação a pontuar. Se um fluxo que você exercitou merece virar regressão, recomende a spec Playwright no relatório — recomendar é seu papel, escrever não. Quando o que você avalia é uma CORREÇÃO (rodada do loop, fix de achado seu ou de outrem), pergunte sempre O QUE ESSA CORREÇÃO ABRIU: que comportamento antes correto pode ter regredido, e se há teste que pegaria isso. Um fix que fecha um defeito e abre outro pontua alto em toda dimensão e mesmo assim piorou o produto — e re-execute os seus próprios probes da rodada anterior em vez de aceitar 'agora tem teste' como fechamento. Não reescreve código — produz scores 0-10 por dimensão (architecture, contracts, code-quality, test-coverage), feedback priorizado acionável, e ação clara (aprovado em >=9 / feedback para o loop de correção em <9). Silêncio em scores ≥ 9 é o sinal correto."
|
|
300
348
|
},
|
|
301
349
|
"scout": {
|
|
@@ -324,7 +372,8 @@
|
|
|
324
372
|
"standards": [],
|
|
325
373
|
"tools": "Read, Glob, Grep, Bash",
|
|
326
374
|
"model": "haiku",
|
|
327
|
-
"
|
|
375
|
+
"maxTurns": 20,
|
|
376
|
+
"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."
|
|
328
377
|
}
|
|
329
378
|
}
|
|
330
379
|
}
|
package/framework/docs/CLI.md
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
## Gates e estado
|
|
8
8
|
|
|
9
|
-
- **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`.
|
|
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).
|
|
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
|
+
- **`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.
|
|
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`.
|
|
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
|
|
|
@@ -22,14 +22,16 @@
|
|
|
22
22
|
|
|
23
23
|
## Worktree / Fleet
|
|
24
24
|
|
|
25
|
-
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. **Sessão ancorada no worktree → o desmonte é ADIADO** (`--merge` e `--pr`): quando a sessão do Claude Code que rodou o `finish` tem o próprio worktree como projeto (`CLAUDE_PROJECT_DIR` — exportado ao CLI como `MORPH_SESSION_PROJECT_DIR` pelo hook `worktree-env-inject`, porque o Bash tool não o exporta e `cd <raiz> && morph-spec finish` apaga o cwd), a integração acontece normalmente, mas o worktree fica: saída com `worktreeRemoved: false`, `teardownDeferred: "session-anchored"`, `worktree: {path, branch}` e um `hint` com `morph-spec worktree remove <f>` para rodar depois, de uma sessão aberta na raiz. Desmontar ali desligava a junction `.morph/framework` pela qual TODO hook da sessão resolve (`node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`) e o diretório seguia travado pela própria sessão — o `git worktree remove` apagava o conteúdo e desregistrava a árvore, falhava só no `rmdir`, e cada tool call seguinte morria com `MODULE_NOT_FOUND`. O `--dry-run` já avisa (`teardownDeferred` no pré-voo). **O `--merge` diz o que falta publicar:** depois do merge local a saída traz `publish: {remote, branch, ahead, pushed}` e um `nextStep` com a CONTAGEM de commits que a default local tem à frente de `origin/{default}` mais o comando concreto (`git push origin {default}`) — em **todas** as saídas do `--merge`, inclusive sob `--keep`. Sem contraparte remota para comparar (`refs/remotes/origin/{default}` ausente) `ahead` fica `null` e a frase estática de integração local permanece — nenhuma contagem é inventada. Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed: true` e `publish.published: N`; sem remoto `origin` o merge NÃO é desfeito — a saída reporta `pushError: "no-origin-remote"` e mantém o aviso local) — **`--push` só existe com `--merge`: combinado com `--pr` é RECUSA, exit 1, `push-with-pr`, antes de qualquer mutação** (o `--pr` já publica `morph/{f}` e não tem merge local para publicar depois; a recusa nomeia o comando de cada intenção); `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas. **`--dry-run` é o PRÉ-VOO:** não muta nada (nenhum commit, archive, remoção de worktree, chamada a `gh` que crie algo, nem `git fetch`) e emite `{preflight: true, finished: false}` com o que ESTE finish faria — `ignored.shieldable` (gitignored exclusivo, copiado para o backup) × `ignored.replicable` × `ignored.discardable`, os `reparsePoints` com o aviso de que apagá-los fora do morph-spec apaga o conteúdo REAL na raiz, `cwdInsideWorktree` e `baseAdvanced: {ref, commits}` — a pergunta INVERSA da do `--merge`: quantos commits `origin/{default}` tem que a base da feature não tem. `commits: null` é DESCONHECIDO, nunca 0. Rodado de DENTRO do worktree ele **avisa e completa o relatório** (sai 0); o caminho real continua recusando antes da primeira mutação.
|
|
25
|
+
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. **Revogações:** no mesmo commit, o `finish` aplica a seção `UCs e ADRs que esta feature altera ou revoga` do `spec.md` (tabela `Id | Mudança | Substituto | Escopo`; forma em `morph-plan` §3) — o UC revogado no acervo ganha `status: superseded` + `superseded_by` (+ `scope`, quando parcial), e o índice `.morph/specs/adrs/` (um arquivo por ADR, `{feature}--ADR-{nnn}.md` — features paralelas nunca tocam o mesmo arquivo) recebe os ADRs da feature marcados `**Alcance:** projeto` e perde (total, o arquivo sai) ou estreita (parcial, `scope:` no arquivo) os revogados. Uma linha só é aplicada se o substituto estiver no acervo depois da promoção (e não tiver sido recusado por colisão), se o id aparecer uma vez no spec e um arquivo só no acervo. Uma cópia promovida que substitui um UC `superseded` sem declarar a supersessão a herda do acervo. O payload traz `supersededUseCases: [{id, file, supersededBy, scope?}]`, `keptSuperseded: [{id, file, supersededBy, scope?}]`, `adrIndex: {added, revoked, narrowed}` e `revocationProblems: [{id, reason, message}]` (linha malformada ou ambígua, UC fora do acervo, substituto inexistente, ADR fora do índice) — cada chave só aparece com conteúdo, e nenhum problema recusa o `finish`. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. **Sessão ancorada no worktree → o desmonte é ADIADO** (`--merge` e `--pr`): quando a sessão do Claude Code que rodou o `finish` tem o próprio worktree como projeto (`CLAUDE_PROJECT_DIR` — exportado ao CLI como `MORPH_SESSION_PROJECT_DIR` pelo hook `worktree-env-inject`, porque o Bash tool não o exporta e `cd <raiz> && morph-spec finish` apaga o cwd), a integração acontece normalmente, mas o worktree fica: saída com `worktreeRemoved: false`, `teardownDeferred: "session-anchored"`, `worktree: {path, branch}` e um `hint` com `morph-spec worktree remove <f>` para rodar depois, de uma sessão aberta na raiz. Desmontar ali desligava a junction `.morph/framework` pela qual TODO hook da sessão resolve (`node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`) e o diretório seguia travado pela própria sessão — o `git worktree remove` apagava o conteúdo e desregistrava a árvore, falhava só no `rmdir`, e cada tool call seguinte morria com `MODULE_NOT_FOUND`. O `--dry-run` já avisa (`teardownDeferred` no pré-voo). **O `--merge` diz o que falta publicar:** depois do merge local a saída traz `publish: {remote, branch, ahead, pushed}` e um `nextStep` com a CONTAGEM de commits que a default local tem à frente de `origin/{default}` mais o comando concreto (`git push origin {default}`) — em **todas** as saídas do `--merge`, inclusive sob `--keep`. Sem contraparte remota para comparar (`refs/remotes/origin/{default}` ausente) `ahead` fica `null` e a frase estática de integração local permanece — nenhuma contagem é inventada. Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed: true` e `publish.published: N`; sem remoto `origin` o merge NÃO é desfeito — a saída reporta `pushError: "no-origin-remote"` e mantém o aviso local) — **`--push` só existe com `--merge`: combinado com `--pr` é RECUSA, exit 1, `push-with-pr`, antes de qualquer mutação** (o `--pr` já publica `morph/{f}` e não tem merge local para publicar depois; a recusa nomeia o comando de cada intenção); `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas. **`--dry-run` é o PRÉ-VOO:** não muta nada (nenhum commit, archive, remoção de worktree, chamada a `gh` que crie algo, nem `git fetch`) e emite `{preflight: true, finished: false}` com o que ESTE finish faria — `ignored.shieldable` (gitignored exclusivo, copiado para o backup) × `ignored.replicable` × `ignored.discardable`, os `reparsePoints` com o aviso de que apagá-los fora do morph-spec apaga o conteúdo REAL na raiz, `cwdInsideWorktree` e `baseAdvanced: {ref, commits}` — a pergunta INVERSA da do `--merge`: quantos commits `origin/{default}` tem que a base da feature não tem. `commits: null` é DESCONHECIDO, nunca 0. Rodado de DENTRO do worktree ele **avisa e completa o relatório** (sai 0); o caminho real continua recusando antes da primeira mutação.
|
|
26
26
|
- **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
|
|
27
|
+
- **`morph-spec usecase check <feature> [--json]`** — o ciclo de vida do acervo contra a feature, rodável antes de existir `tasks.json` (o `spec.md` congela no Gate 2, então é aqui que uma linha de revogação errada ainda tem conserto): task cujo `usecase` está `superseded` (no acervo, na cópia da feature ou revogado pelo próprio `spec.md`), frontmatter de supersessão malformado (`superseded` sem `superseded_by` UC-{nn}, `superseded_by`/`scope` sem `status: superseded`) e linha de revogação que o `finish` não aplicaria (id malformado, UC fora do acervo, substituto inexistente, ADR fora do índice). Avisos, exit 0. Com `tasks.json`, roda também o cruzamento task × use case do `dag` — e sai 1 onde o `dag` sairia (UC inexistente, schema inválido). `--json`: `{feature, useCases, lifecycle: {warnings}}`.
|
|
27
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.
|
|
28
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).
|
|
29
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`).
|
|
30
31
|
**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).
|
|
31
32
|
- **`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.
|
|
32
|
-
- **`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 regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`)
|
|
33
|
+
- **`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.
|
|
34
|
+
- **Backups do finish (`.git/morph-spec/finish-backup/`) e a retenção** — o que o `finish` (o shed da raiz, `{f}-{carimbo}/`) e o desmonte do worktree (`{f}-{carimbo}-worktree-ignored/`) copiam antes de apagar um arquivo que não existe em outro lugar; `worktree remove` de árvore de task grava em `task/{n}-{carimbo}-worktree-ignored/`. Cada pasta ganha um manifesto ao lado (`{pasta}.manifest.json`): o que foi copiado, os caches deixados de fora (`excludedCaches`) e os **nomes** dos `.env*` que entraram (`secrets`). **Um finish é UM backup**: as duas pastas dele levam o mesmo carimbo e contam juntas; pastas gravadas antes disso têm carimbos diferentes e contam uma a uma. **Retenção ligada por padrão**, também em projeto cujo `config.json` não tem a chave: os **20 backups mais novos de cada namespace** (a raiz de `finish-backup/` e `task/` têm orçamentos separados — um fan-out de sub-agents não despeja o backup da feature fechada um minuto antes) **sempre ficam, de qualquer idade** — não há corte por idade no padrão, então um período parado nunca zera os backups das últimas features fechadas —; dentro de um backup que fica, os arquivos `.env*` são apagados depois de **7 dias** (o resto fica, e o manifesto guarda os nomes e `secretsStrippedAt`). Configurável em `config.json → finishBackup.retention: { maxCount, maxAgeDays, secretMaxAgeDays }` (inteiros ≥ 1; `maxAgeDays` é um corte por idade OPCIONAL — ausente ou `null`, o padrão, não corta) ou desligada com `finishBackup.retention: false` (schema em `framework/schemas/config.schema.json`, autoridade em `resolveBackupRetention()`). **O que não dá para ler PULA a poda naquela execução, nunca cai no padrão** — é uma operação que apaga, e um `{ "enabled": false }`, um `"false"`, um `maxCount: 0`, uma chave desconhecida, um `finishBackup` que não é objeto ou um `config.json` que não parseia podem muito bem querer dizer "guarde tudo": a saída diz `backupRetention: {skipped: "<razão>"}`, o `doctor --worktrees --prune` avisa com a razão, e o `doctor`/`doctor --worktrees` acusam até alguém corrigir. Só a ausência da chave liga o padrão. **Backup sem manifesto é legado** (gravado antes desta retenção) — esse é o critério, e nenhum outro. A poda automática nunca o apaga, nunca tira os `.env*` dele e não lhe dá vaga no orçamento; ela só conta quantos esperam (`backupRetention.legacyPending: N` + `legacyHint`). **A primeira poda deles é só com `doctor --worktrees --prune` explícito**, que os ranqueia junto com os demais (contando no teto), apaga os que ficam fora e grava um manifesto (`legacy: true`, `adoptedAt`) em cada um que mantém — dali em diante são backups comuns. A poda automática roda **depois** do backup novo — no fim de todo `finish` com `finished: true` e de todo `worktree remove` que gravou backup (a saída traz `backupRetention: {removed, removedSample, secretsStripped, declined, legacyPending, legacyHint}` só com o que se aplica). **O backup que a execução acabou de gravar nunca é podado por ela** — o carimbo dela é protegido —, e backup com carimbo no FUTURO (relógio adiantado e depois corrigido) fica e não ocupa vaga na contagem. O `finish --dry-run` anuncia só o que a poda AUTOMÁTICA faria (`backupRetention: {outsideRetention, overdueSecrets, legacyPending}` + uma linha em `wouldDo`), sem apagar nada. Fail-open: uma poda que falha nunca derruba o comando. PROVE-or-DECLINE como o resto do desmonte: backup com reparse point dentro é mantido (`declined`) — mas perde os `.env*`, porque a política o queria fora —, link nunca é atravessado (nem um `task/` que seja junction), e **só `task/` é namespace**: qualquer outra pasta de primeiro nível que não tenha nome de backup nunca é visitada nem removida, nem vazia. O `.env*` idêntico ao da raiz nunca chega a ser copiado (é replicável, ver acima); o que chega é o que difere — o `.env.e2e` de cada worktree carrega o bloco de portas dela, então nenhum é igual ao da raiz, e é por isso que o prazo curto existe.
|
|
33
35
|
- **`morph-spec branch prune [--remote] [--dry-run] [--json]`** — apaga as branches locais cujo trabalho **já está** na default, **incluindo as squash-merged que o git não enxerga**. O problema que resolve: num repo que faz squash-merge, o commit de merge entra em `origin/master` mas os SHAs da branch não — `git branch --merged` não a lista, `git branch -d` recusa, e sobra o `-D`, indistinguível de descartar trabalho de verdade. Cada branch é classificada com evidência impressa: `merged-ancestral` (o git prova), `merged-squash` (PR `MERGED` pelo `gh`, **ou** árvore idêntica à da default), `open` (PR OPEN — nunca apagada), `in-worktree` (em uso — nunca apagada) e `unmerged` (**não foi possível provar** integração: sem `gh`, sem default resolvível ou trabalho de verdade — todas tratadas igual, de propósito). Só as duas primeiras são apagadas. Branch remota só com `--remote` explícito.
|
|
34
36
|
- **`morph-spec handoff <feature> [--here]`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. **Resolve a árvore dona** antes de gerar (mesma lógica do `list`/`fleet`) e escreve LÁ, não necessariamente no cwd — rodar da raiz com o worktree vivo é seguro: o handoff sai correto e no lugar certo, em vez do esqueleto de nascimento da raiz. Sem árvore nenhuma materializada (só existe commitado em `morph/{f}`) recusa com `feature-not-in-tree`, nomeando a branch, em vez de gerar um documento apontando para arquivos inexistentes. `--here` força construir/escrever no cwd mesmo assim. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
|
|
35
37
|
- **`morph-spec fleet`** — visão da frota: toda feature ativa (raiz + worktrees) com fase, gates pendentes, tasks, **remoto** (`local`/`pushed`/`PR #N`), **claim** (que sessão está na feature), portas, handoff e atividade. Use entre sessões para decidir onde atuar.
|
|
@@ -37,10 +39,12 @@
|
|
|
37
39
|
|
|
38
40
|
## Validação
|
|
39
41
|
|
|
40
|
-
- **`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. `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`. `--mermaid` nunca é bloqueado.
|
|
41
|
-
- **`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`. 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.
|
|
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).
|
|
44
|
+
- **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`). Duas 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. 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ê.
|
|
42
45
|
- **`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.
|
|
43
|
-
- **`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.
|
|
46
|
+
- **`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.
|
|
47
|
+
**Personas (`.claude/agents/morph-*.md`).** São artefatos derivados de `.morph/framework/agents.json` — nunca editados à mão. O `doctor` padrão valida o `agents.json` (inclusive o `maxTurns`: inteiro ≥ 1 ou ausente — `0`, negativo, fracionário, string ou `null` é **erro** nomeando persona e valor, porque o installer recusa gerar essa persona — e o `surfaces`: `{ "<superfície>": { when, standards } }` com id kebab-case, `when` e lista não vazios, e nenhum standard repetido da lista universal `standards`; inválido é **erro** pelo mesmo motivo). Com `surfaces`, o corpo da persona ganha `## Standards by surface`: cada superfície com o seu `when`, e a instrução de ler a lista universal mais só a da superfície da tela — a que o checklist de tela do mockup nomeia em `Superfície`. O `--full` compara cada `.md` com o render atual e, no drift, diz **qual campo** divergiu por arquivo — `morph-x.md (maxTurns)` é edição à mão do teto; `(body)` depois de um upgrade da CLI é só regeneração pendente. Os grants `mcp__*` são conferidos contra o registry do **pacote**, o mesmo que o installer leu (a cópia instalada não tem `mcp/`). O campo `maxTurns` é o teto de turnos do subagent gerado: por persona no `agents.json`, `20` quando ausente, e o corpo da persona o anuncia ao sub-agent junto com o turno de checkpoint (~70%) — o sub-agent não vê o próprio frontmatter. O valor vem do `agents.json` do **pacote**: `update` recopia o arquivo e regenera os `.md`, então editar a cópia em `.morph/framework/` não muda a instalação — só faz o `--full` acusar drift.
|
|
44
48
|
**Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê. Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
|
|
45
49
|
**Pull requests:** `pr-awaiting-merge` (PR aberto há mais de 7 dias sem cópia local nesta árvore) e `pr-merged-not-landed` (PR já `MERGED` mas `.morph/archive/{f}` ainda não chegou aqui — sinal de `git pull` faltando; branch `morph/*` cujo diff nunca tocou `.morph/features/{f}/` nem `.morph/archive/{f}/` — um PR só de docs — não é acusada, e diff ilegível ou truncado no teto do `gh` mantém o aviso). Offline-safe: sem `gh`, sem remoto no GitHub, ou qualquer falha, a checagem sai **não verificado** (nunca `ok`) — `[]` (perguntei, nada encontrado) e `null` (não pude perguntar) nunca se confundem, e o "não verificado" nunca deixa o `doctor` mais barulhento nem muda o exit code.
|
|
46
50
|
Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado), e `--ai` para a auditoria de projeto .NET com agentes.
|
|
@@ -51,8 +55,8 @@
|
|
|
51
55
|
**Só `pin-drift` produz `error`, e só na faixa flutuante** — é a única classe com prejuízo medido: uma faixa `2.*` sobe sozinha para uma versão com assinatura mudada e o `MissingMethodException` chega **depois** de a chamada ao modelo ser paga. Versão fixa ≠ pin é `warn` nomeando as duas versões; pacote de IA presente no projeto e ausente do pin é `warn` descrito como **lacuna do pin**, nunca como defeito do projeto. As outras quatro checagens informam e nunca travam. **Exit code:** 1 com ao menos um `error`; 0 caso contrário — `warn` e `skip` nunca mudam o código de saída.
|
|
52
56
|
|
|
53
57
|
**Quatro condições de silêncio**, todas com razão obrigatória (um "não verificado" sem motivo é ruído): (1) nenhum `.csproj` alcançável — e aí **o pin nem é lido**; (2) nenhum `ai-pin.json` resolvível — falta de régua é ausência de dado, nunca defeito do projeto; (3) `.csproj` presentes e nenhum pacote de IA entre eles — um projeto .NET sem MAF não é um projeto MAF quebrado; (4) `.csproj` ilegível devolve **não verificado** para aquele arquivo, nunca "sem drift". Quando NENHUMA checagem teve insumo, o relatório sai em **uma linha** em vez de cinco `○` vazios. O gate é o **arquivo em disco**, nunca `project.stack` do `config.json`: o campo declara intenção, o arquivo é fato. Nada alcançável por `--ai` escreve — `readAiPin()` sim (leitura pura); os geradores que escrevem por padrão, jamais.
|
|
54
|
-
- **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado. **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto. **A quarta fonte não é um arquivo: as stacks docker.** Um container sobrevive à remoção do worktree, da branch e da feature — nenhuma das três reconciliações acima consegue vê-lo, e ele segue segurando o bloco de portas. Dois achados novos, ambos `severity: warn` (reportados, nunca falha do comando): `docker-stack-orphan` — projeto compose `morph-*` **deste repositório** (o arquivo compose fica sob a raiz; o prefixo `morph-` é global no host, e reportar a stack de outro repo seria sugerir `down -v`, com volumes, na stack viva do vizinho) de pé para uma feature que esta árvore não conhece, com o teardown exato (`docker compose -p morph-{slug} down -v`); e `docker-port-held` — porta do bloco de uma feature (derivada de `portLayout`, nunca recalculada) publicada por um container de **outro** projeto compose, nomeando o container que a segura. A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhuma feature alocou bloco de porta (sem registro em `<git-common-dir>/morph-spec/`), docker indisponível, ou qualquer chamada ao docker saindo != 0 / respondendo algo que não é JSON. Por isso ela é estruturalmente silenciosa num repo que não containeriza — e por isso ela **não** entra no `doctor --full`, onde toda checagem tem de ser satisfazível.
|
|
55
|
-
- **`morph-spec report-check <feature> [task] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache). A evidência de `observed` vem do `events.jsonl` (todo `Write`/`Edit` observado na janela da task, cruzada com `task-times.json`), não de `outputs`, que é auto-declarado pelo próprio agente — writes do orquestrador sob `.morph/` e o do próprio relatório são excluídos. Sem telemetria no projeto, o comando volta ao comportamento anterior (fail-open). Protocolo completo em MORPH.md §6.
|
|
58
|
+
- **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run). O `--prune` também aplica a **retenção dos backups do finish** (ver acima) — a mesma política que todo `finish` já aplica sozinho, e a **única via que a aplica aos backups legados** (sem manifesto), contando-os no teto e dando manifesto aos que ficam; sob `--json` a saída é só o JSON, e o que o `--prune` fez vai nele: `prune` (`pruned`, `orphanRegistrations`, `removedEmptyDirs`, `clearedRegistrations`) e `backupRetention` (com `adopted`). Sem `--prune`, backup legado esperando poda, backup fora da retenção ou `.env*` além do prazo curto vira o achado `finish-backup-retention`, que descreve o que o `--prune` faria (`severity: warn` — faxina, nunca falha do comando). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado. **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto. **A quarta fonte não é um arquivo: as stacks docker.** Um container sobrevive à remoção do worktree, da branch e da feature — nenhuma das três reconciliações acima consegue vê-lo, e ele segue segurando o bloco de portas. Dois achados novos, ambos `severity: warn` (reportados, nunca falha do comando): `docker-stack-orphan` — projeto compose `morph-*` **deste repositório** (o arquivo compose fica sob a raiz; o prefixo `morph-` é global no host, e reportar a stack de outro repo seria sugerir `down -v`, com volumes, na stack viva do vizinho) de pé para uma feature que esta árvore não conhece, com o teardown exato (`docker compose -p morph-{slug} down -v`); e `docker-port-held` — porta do bloco de uma feature (derivada de `portLayout`, nunca recalculada) publicada por um container de **outro** projeto compose, nomeando o container que a segura. A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhuma feature alocou bloco de porta (sem registro em `<git-common-dir>/morph-spec/`), docker indisponível, ou qualquer chamada ao docker saindo != 0 / respondendo algo que não é JSON. Por isso ela é estruturalmente silenciosa num repo que não containeriza — e por isso ela **não** entra no `doctor --full`, onde toda checagem tem de ser satisfazível.
|
|
59
|
+
- **`morph-spec report-check <feature> [task...] [--before-eval] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache; relatório com `PARCIAL` na 1ª linha — o checkpoint a ~70% do `maxTurns` — sai `report-incomplete` com `partial: true` e ação `resume`: retome o agente, NÃO carimbe DEGRADADO. O marcador é lido de propósito, nunca inferido da falta de score: o marcador de score é frouxo e um parcial que diga "auto-score: pendente (0-10 no final)" casaria com ele). A evidência de `observed` vem do `events.jsonl` (todo `Write`/`Edit` observado na janela da task, cruzada com `task-times.json`), não de `outputs`, que é auto-declarado pelo próprio agente — writes do orquestrador sob `.morph/` e o do próprio relatório são excluídos. Sem telemetria no projeto, o comando volta ao comportamento anterior (fail-open). Vários ids (`report-check {f} T1 T2 --json`) auditam um batch paralelo numa chamada: sem task, o modo feature só audita tasks `done`, e as de um batch recém-voltado ainda não estão. **Bloco `tree`**, em toda saída e sem mudar veredito nem exit code: `dirty`/`dirtyCount` (o que não está commitado fora de `.morph/`), `diffstat` (contra `HEAD`), `commits` (sobre a default, até 10; `null` sem default), `markers` (linhas com `MUTACAO` em arquivo rastreado ou não, fora dos ignorados), `fingerprint` (a mesma impressão da árvore que o `verify` grava), `head` e `branch`; `null` quando o git não responde (fora de repositório ou erro) — desconhecido, nunca árvore limpa. **`--before-eval`** grava em `4-review/tree-before-eval.json` a foto da árvore (HEAD e o blob de cada arquivo que difere dele, fora de `.morph/`) antes do dispatch do avaliador, e daí em diante toda saída traz `tree.sinceBeforeEval: { at, headChanged, changed, changedCount }`: os arquivos cujo conteúdo não é o da foto, inclusive o que foi commitado desde então. É assim que uma sessão que retoma depois de o avaliador morrer no meio vê a mutação que ficou; sem foto, `sinceBeforeEval` é `null`. É o que o orquestrador lê depois de um dispatch, em vez de `git status`/`git diff`/`git log`/`grep`, e o que cola no recado de retomada. Protocolo completo em MORPH.md §6.
|
|
56
60
|
|
|
57
61
|
## Suítes de teste (`project.tests[]`) e a divisão loop × suíte
|
|
58
62
|
|
|
@@ -477,11 +481,17 @@ O ganho medido **não é recall**: contra o grep, o grafo não achou um único a
|
|
|
477
481
|
|
|
478
482
|
**Por que não consumir o graphify cru.** O resolvedor C# dele não implementa escopo hierárquico de namespace, então um tipo usado por um namespace filho sem `using` explícito vira um *stub* por arquivo — e o blast radius do símbolo real devolve **zero** consumidores onde existem dois. Falso negativo silencioso é o pior modo de falha possível antes de um hotfix. A camada de saneamento coalesce esses stubs (+91,9% de arestas de uso recuperadas no MORPH_GHLBrain), trata como externo só o que **não tem nenhum** nó com arquivo (BCL/NuGet), e devolve as opções quando o símbolo é genuinamente ambíguo em vez de escolher por conta própria.
|
|
479
483
|
|
|
480
|
-
- **`morph-spec graph status [-p <path>] [--json]`** — existência, tamanho, **frescor** e saúde do saneamento. Grafo ausente **sai 0**, não 1: não ter grafo é um estado válido do projeto. O frescor compara o `built_at_commit` gravado no `graph.json` com o `HEAD
|
|
481
|
-
- **`morph-spec graph build [-p <path>] [--json]`** — extração completa (`graphify extract --code-only`). Gera o `.graphifyignore` stack-aware antes de extrair, porque um grafo que nasce com o ruído dentro não se conserta depois. Projeto **sem nenhum arquivo de código** (o caso de um `init` recém-feito) sai **0** com aviso, não erro. Sem graphify no PATH: exit 1 com `reason: graphify-unavailable` e o comando de instalação — nunca stack trace.
|
|
482
|
-
- **`morph-spec graph refresh [-p <path>] [--json]`** —
|
|
483
|
-
- **`morph-spec graph affected <símbolo> [--prod] [--uses-only] [-p <path>] [--json]`** — blast radius saneado. `--uses-only` exclui `inherits`/`implements`/`extends`, que é o que separa *"20 implementam"* de *"2 usam"*; `--prod` descarta consumidores
|
|
484
|
-
- **`morph-spec graph explain <símbolo> [-p <path>] [--json]`** — vizinhança com citação `arquivo:linha`, separando quem depende dele de do que ele depende. **Prefira `explain` a `query`** nas personas: no eval, `explain` foi consistentemente mais preciso, enquanto `query` trunca por budget e traz ruído. Cada aresta traz a procedência em `via` (`direct`, `coalesced`, `language-disambiguated`), então dá para ver exatamente o que o saneamento recuperou.
|
|
484
|
+
- **`morph-spec graph status [-p <path>] [--json]`** — existência, tamanho, **frescor** e saúde do saneamento. Grafo ausente **sai 0**, não 1: não ter grafo é um estado válido do projeto. O frescor compara o `built_at_commit` gravado no `graph.json` com o `HEAD`, e também o registro da última rodada do graphify (abaixo) — e importa porque o refresh roda **em background**: enquanto o `graphify update` que um commit disparou corre, o grafo ainda é o do commit anterior. Stale, o status diz o porquê (`freshness.reason`: carimbo antigo, `ignore-changed` ou `rules-unknown`) e o que vai acontecer: refresh em andamento (e desde quando), o último falhou naquele HEAD, o graphify saiu do PATH — ou, numa worktree, que o grafo foi semeado da raiz e só anda com `morph-spec graph refresh`. `--json` devolve `{ available, path, nodes, testNodes, links, freshness: {state, reason?}, refresh: {auto, running, since, pid, last}, version: {installed, pinned, matches}, sanitize: {coalescible, external, ambiguous, recoveredDispatch} }`; `refresh.auto` é `true` só na árvore principal.
|
|
485
|
+
- **`morph-spec graph build [-p <path>] [--json]`** — extração completa (`graphify extract --code-only`). Gera o `.graphifyignore` stack-aware antes de extrair, porque um grafo que nasce com o ruído dentro não se conserta depois — e migra o que uma versão anterior gerou (abaixo). Projeto **sem nenhum arquivo de código** (o caso de um `init` recém-feito) sai **0** com aviso, não erro. Sem graphify no PATH: exit 1 com `reason: graphify-unavailable` e o comando de instalação — nunca stack trace. Com um refresh em background segurando o lock do repositório, sai 0 com `running: true` e não dispara um segundo graphify sobre o mesmo `graph.json`.
|
|
486
|
+
- **`morph-spec graph refresh [-p <path>] [--json]`** — `graphify update`, o mesmo comando que o refresh em background roda, com o mesmo registro e, na raiz, o mesmo lock (numa worktree não há lock: ninguém mais escreve o grafo dela). É o único que atualiza o grafo de uma **worktree**. O eval mediu 2 a 3× sobre o build; no MORPH_GHLBrain (1.573 arquivos indexados) ele custou ~80 s mesmo sem mudança nenhuma, o mesmo que o build — o ganho está em não precisar rodar à mão.
|
|
487
|
+
- **`morph-spec graph affected <símbolo> [--prod] [--uses-only] [-p <path>] [--json]`** — blast radius saneado. `--uses-only` exclui `inherits`/`implements`/`extends`, que é o que separa *"20 implementam"* de *"2 usam"*; `--prod` descarta consumidores de teste (projeto de teste, `__tests__/`, `*.test.*`, `*.spec.*`); sem ele, cada consumidor traz `test: true|false`. Erros são estados, não exceções: `no-graph`, `not-found` e `ambiguous` (este devolvendo as opções com path e id) saem 1 com a dica de fallback para grep.
|
|
488
|
+
- **`morph-spec graph explain <símbolo> [-p <path>] [--json]`** — vizinhança com citação `arquivo:linha`, separando quem depende dele de do que ele depende. **Prefira `explain` a `query`** nas personas: no eval, `explain` foi consistentemente mais preciso, enquanto `query` trunca por budget e traz ruído. Cada aresta traz a procedência em `via` (`direct`, `coalesced`, `language-disambiguated`, `recovered-dispatch`), então dá para ver exatamente o que o saneamento recuperou, e `test` diz se a outra ponta é código de teste. É também como se acha o dublê a reusar num teste novo: `graph explain FakeX`.
|
|
489
|
+
|
|
490
|
+
**Refresh automático — só da raiz.** Depois de um comando de shell do agente (Bash ou PowerShell) que chama `git`, `gh` ou `morph-spec` (elo `graph-refresh-kick` do PostToolUse) e no início de cada sessão (SessionStart), o morph-spec solta um processo **destacado** (`.morph/framework/hooks/shared/graph-refresh.js`) que roda `graphify update` na **árvore principal** quando o grafo dela está velho. Quem dispara nunca espera. O processo 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 um refresh de worktree correndo fazia o `finish`/`worktree remove` falhar com "Permission denied". "Velho" é a regra única de `shared/graph-freshness.js`: o carimbo `built_at_commit` contra o HEAD e o registro da última rodada em `<git-common-dir>/morph-spec/graph-refresh-state.json`. O registro existe porque o `graphify update` do 0.9.50 **não recarimba** quando a topologia não muda ("outputs left untouched" — commit só de `.morph/`, corpo de método editado): uma rodada com exit 0 no HEAD H vale como grafo em dia em H, uma que falhou em H só se repete quando o HEAD andar, e graphify ausente fica registrado (o disparo não solta processo até ele voltar ao PATH). As regras do `.graphifyignore` entram por **conteúdo** — o registro guarda o hash das regras com que o grafo foi extraído; comentário, cabeçalho, BOM e CRLF não contam. Um lock por repositório (`graph-refresh.lock`, renovado a cada rodada) impede dois graphify no mesmo `graph.json`, a checagem é refeita depois de soltá-lo, e cada tentativa vai para `graph-refresh.log`. `MORPH_GRAPH_AUTO_REFRESH=0` desliga. **Limite:** um commit feito fora do agente (terminal, IDE) só é alcançado no próximo início de sessão ou no próximo comando git do agente. O hook git do próprio graphify (`graphify hook install`) **não** é usado: medido no 0.9.50, ele sai sem fazer nada em worktree vinculada, não tem `post-merge` (merge, fast-forward e `pull` nunca rebuildam) e, num commit depois de um fast-forward, carimba o HEAD num grafo sem os arquivos do merge. O `doctor` avisa quando ele está instalado.
|
|
491
|
+
|
|
492
|
+
**Grafo em worktree.** `worktree setup`/`provision` copiam o grafo da raiz para a worktree nova (só `graphify-out/`, sem `.graphify_root`, que guarda o caminho absoluto da raiz, e sem `cache/`), no exclude do common dir, com o registro da raiz: o commit em que ele está em dia e as regras com que foi extraído. O `.graphifyignore` **não** vem: a worktree lê o do próprio checkout, e um `/.graphifyignore` no exclude do common dir escondia também o da raiz ainda não versionado. Uma worktree anterior a isso ganha a cópia no próximo SessionStart. Como a worktree nasce de um commit da raiz, o grafo copiado já é o do HEAD dela ou fica a um `morph-spec graph refresh` de distância — e o validador `reuse` do `verify`, que lê o grafo da árvore em que roda, deixa de pular justamente onde a feature é feita. **Dali em diante a worktree só anda com `morph-spec graph refresh`**, em primeiro plano: o refresh automático não a toca (acima). Ali o `graph build`/`refresh` não espera o lock da raiz e nunca reescreve o `.graphifyignore` do checkout; sem um (a raiz ainda não o versionou), usa as regras da raiz só durante a rodada e as tira depois — um `?? .graphifyignore` deixado na worktree faria o `finish` e o `worktree remove --if-clean` recusarem com `worktree-dirty`. A cópia leva um comentário-marcador na 1ª linha: se o processo morrer antes de tirá-la, a próxima rodada a reusa, e o `finish` e o `worktree remove` não a contam como sujeira e só a apagam depois de todas as recusas. O graphify roda com `cwd` no git common dir, nunca na árvore: ele lê o alvo pelo argumento, e um graphify órfão com `cwd` numa worktree a prendia no win32 até terminar. As regras que valem para o frescor da worktree são as mesmas: sem arquivo próprio, as da raiz — uma mudança nelas (o `update` migrando o ignore) deixa a worktree `stale` com `ignore-changed`. O `finish` trata `graphify-out/` como artefato descartável.
|
|
493
|
+
|
|
494
|
+
**Testes no grafo.** Em .NET o conteúdo dos projetos de teste fica fora, mas dublês, fixtures e helpers (`TestDoubles/`, `Fakes/`, `Stubs/`, `Mocks/`, `Fixtures/`, `Helpers/`, `Builders/`, `*Fixture.cs`, `*Fixtures.cs`, `Fake*.cs`, `Stub*.cs`, `*Stub.cs`) entram, marcados como teste. A forma do `.graphifyignore` segue a regra do gitignore que o graphify aplica — um `!` não reinclui nada sob diretório excluído —, então o arquivo exclui o conteúdo (`**/*.Tests/**`), reinclui os diretórios e só então os helpers, com `bin/`/`obj/` por último. Nó de teste fica fora dos god nodes e não conta grau; os `dispatches_to` que o graphify perde quando um fake vira o segundo implementador de uma interface são recuperados pelo saneamento (no MORPH_GHLBrain: de 122 para 5 no graphify, 122 de volta). Um `.graphifyignore` idêntico ao que a versão anterior gerava é trocado pelo atual no `update`, no `setup-infra` e no `graph build`/`refresh` — só quando as **regras** mudam (sem .NET a diferença era só o cabeçalho, e o arquivo fica como está); versionado, o `update` avisa para commitá-lo, porque cada checkout lê o seu. Um editado não é tocado, e o aviso diz as linhas a trocar.
|
|
485
495
|
|
|
486
496
|
**O que o resultado significa muda com a stack.** Em backend, blast radius é *quem chama*. Em frontend, o grafo é estrutura de módulos e não call graph (`imports: 78` contra `calls: 3` no LP_Polymorphism), então ali ele responde *quem importa*. Ler um como o outro leva a conclusão errada, e é por isso que as personas dizem isso no próprio texto.
|
|
487
497
|
|
|
@@ -489,5 +499,32 @@ O servidor MCP (`graphify-mcp`, 10 tools, todas aceitando `project_path`) está
|
|
|
489
499
|
|
|
490
500
|
## Observabilidade
|
|
491
501
|
|
|
492
|
-
- **`morph-spec telemetry [--json] [--feature <f>] [--limit <n>]`** — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, personas despachadas + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log
|
|
493
|
-
- **`morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), bytes de contexto
|
|
502
|
+
- **`morph-spec telemetry [--json] [--feature <f>] [--limit <n>]`** — reconstrói o caminho de execução da sessão a partir de `.morph/logs/events.jsonl` (skills invocadas, personas despachadas + prompts, standards/base de conhecimento lidos, mix de tools, prompts do usuário). Capturado silenciosamente pelo hook `telemetry-log`. Lê também os arquivos rotacionados em `.morph/logs/sessions/`, sem contar duas vezes a mesma linha.
|
|
503
|
+
- **`morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`** — custo da execução a partir do mesmo `events.jsonl`: duração por task (TaskStart→TaskEnd), bytes de contexto lidos por tipo (`rubric`/`standard`/`skill`/`agents`/`plan-artifact`/`code` — a janela `offset`/`limit` que o Read trouxe, quando o evento a tem), dispatches + tamanho de prompt e mix de tools. Bytes são **proxy determinística** de custo de token; os tokens reais vêm à parte: `SessionUsage` (sessão principal) e `AgentUsage` (cada sub-agent, lido do transcript dele), somados **só pelo `delta`** de cada amostra — o total de topo é acumulado e se repete a cada Stop, e amostra antiga sem `delta` fica fora da soma e é contada como legada. `--all-trees` agrega **toda árvore git** com log, mais os logs que o `finish` guardou em `.git/morph-spec/finish-backup/` antes de desmontar a worktree, deduplicados por `eventId`.
|
|
504
|
+
- **`morph-spec cost [feature] --metrics [--by task] [--all-trees] [--top <n>] [--json]`** — as medições do loop, sempre pelo mesmo método, para comparar uma janela com outra:
|
|
505
|
+
|
|
506
|
+
| Medida | Definição |
|
|
507
|
+
|---|---|
|
|
508
|
+
| turnos | eventos `ToolBatch` (um por turno do modelo que usou ferramenta), orquestrador × sub-agent; `null` se a janela não tem nenhum |
|
|
509
|
+
| chamadas de ferramenta | `PostToolUse` + `PostToolUseFailure`, orquestrador (`agentId` ausente num evento v2) × sub-agent × sem agente (v1) |
|
|
510
|
+
| tempo por classe | soma de `durationMs` do Bash por classe (`test`, `build`, `verify`, `e2e`, `git`, `morph-spec`, `read`, `other`), com a contagem de chamadas sem duração |
|
|
511
|
+
| rerun | o mesmo comando **produtor** (antes do primeiro `\|` fora de aspas, sem `2>&1`) na mesma feature até 10 min depois do anterior; `rate` conta as repetições, `burstRate` toda execução que cai numa rajada, a primeira incluída. `rerunClasses`: `fix-loop` (falhou, houve edição), `flake` (falhou, sem edição, passou), `retry` (falhou de novo sem edição), `re-verify` (passou, houve edição), `redundant` (passou, sem edição), `timeout`, `refilter` (mesmo produtor, outro filtro de saída, sem edição: rodou de novo para ver outras linhas), `unknown-after-edit`/`unknown-no-edit` (exit mascarado por pipe ou evento v1) |
|
|
512
|
+
| releitura | bytes da mesma janela (arquivo + `offset`/`limit`) lida de novo na mesma feature, com os arquivos de maior releitura; `windowed` conta as leituras com `bytesRead`. As outras (v1) contam o arquivo inteiro, e a taxa vira um teto |
|
|
513
|
+
| verify de feature | `morph-spec verify <f>` executado (fora de aspas e de heredoc) por feature, com a mediana; `verify.unchangedRepeats` conta, por feature, o evento `Verify` de escopo de feature cujo `fingerprint` é igual ao do anterior |
|
|
514
|
+
| suíte .NET que recompila | `dotnetFullSuite`: `dotnet test` sem `--filter` nem `--list-tests`, quantos sem `--no-build`, por fase |
|
|
515
|
+
| git por task | ocorrências de `git status` e `git diff` por task, com a mediana |
|
|
516
|
+
| por task, pela feature | `perTaskByFeature`: total da feature (chamadas, turnos, `git status`+`diff`) ÷ tasks dela, mediana entre as features. Não depende das janelas `TaskStart`→`TaskEnd`, que um batch paralelo ou um `in_progress` tardio deixam curtas; `taskWindowCoverage` diz que fração das chamadas cai dentro de alguma janela |
|
|
517
|
+
| reuse-validator | por árvore (raiz × worktree), dos eventos `Verify`: rodou, pulou (e por quê) ou ausente |
|
|
518
|
+
|
|
519
|
+
Os eventos v1 (sem `schemaVersion`) nunca contam como zero: sem agente, exit code ou duração, eles caem em "sem agente"/`unknown-*`/"sem duração", e o cabeçalho diz que fração da janela está no esquema v2.
|
|
520
|
+
|
|
521
|
+
**Esquema do evento (v2).** Todo evento carrega `ts`, `schemaVersion: 2`, `eventId` (chave de dedupe entre log, arquivos rotacionados e backups), `feature`, `phase` e, quando o payload traz, `sessionId`, `promptId`, `agentId`/`agentType` (só dentro de sub-agent), `cwd`. Por tipo:
|
|
522
|
+
|
|
523
|
+
| Evento | Campos próprios |
|
|
524
|
+
|---|---|
|
|
525
|
+
| `PostToolUse` / `PostToolUseFailure` | `tool`, `toolUseId`, `durationMs`; Bash/PowerShell: `command` (redigido, ≤2000 chars) + `commandChars`, `exitCode`, `outcome` (`ok`/`error`/`timeout`/`interrupted`/`background`), `exitMasked` quando o exit gravado não é o do trabalho (pipe sem `pipefail`, `|| …`, `&` em background, ou `;` seguido de `echo`/`true`/`:` no fim); Read: `target`, `kind`, `bytes` (tamanho do arquivo), `offset`, `limit`, `bytesRead` — um Read que falhou não grava bytes, porque não trouxe nada; Agent: `persona`, `desc`, `prompt` + `promptChars`; SendMessage: `target`, `messageChars` |
|
|
526
|
+
| `ToolBatch` | `size`, `tools` (contagem por ferramenta) — um por turno do modelo |
|
|
527
|
+
| `SessionUsage` / `AgentUsage` | totais acumulados do transcript + `delta` desde a amostra anterior |
|
|
528
|
+
| `Verify` | `scope`, `task`, `status`, `sha`, `fingerprint` (a árvore fora de `.morph/`), `tree` (`root`/`worktree`), `persisted`, `reason`, `nodes.{build,tests,validators,e2e}.{status,durationMs}`, `nodes.validators.{ran,skipped}` |
|
|
529
|
+
|
|
530
|
+
Nada da saída de um comando é gravado — nem o corpo de erro do `PostToolUseFailure`, onde um valor de `.env` pode aparecer sem nome ao lado. `command`, `prompt`, `desc` e `target` passam por redação de segredos óbvios antes de gravar: valor ao lado de um nome de segredo (`API_KEY=…`, `*_KEY`/`*_PASS=…`, `--password …`, `user-secrets set <k> <v>`, `AccountKey=…`), `Authorization: …`, `user:pass@` (senha com `/` inclusive) e `?sig=`/`?access_token=` em URL, e formatos de token (GitHub, `sk-`, `sk_live_`, `npm_`, Slack, AWS, Google, JWT). É por padrão, não por garantia: um valor sem nome ao lado passa.
|