@polymorphism-tech/morph-spec 8.2.0 → 8.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/morph-spec.js +1 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +20 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -1
- package/framework/hooks/shared/build-check.js +8 -1
- package/framework/hooks/shared/payload-utils.js +16 -2
- package/framework/hooks/shared/phase-utils.js +2 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +12 -7
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +22 -10
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +19 -12
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/project/worktree.js +150 -36
- package/src/commands/state/score.js +13 -2
- package/src/commands/tasks/dag.js +11 -3
- package/src/core/paths/output-schema.js +28 -0
- package/src/lib/stack/stack-profile.js +40 -9
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/tasks/test-runner.js +26 -4
- package/src/lib/validators/validation-runner.js +4 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Spec-driven development framework for multi-stack projects. Turns feature requests into implementation-ready code through structured, AI-orchestrated phases.
|
|
4
4
|
|
|
5
5
|
**Package:** `@polymorphism-tech/morph-spec`
|
|
6
|
-
**Version:** 8.2.
|
|
6
|
+
**Version:** 8.2.1
|
|
7
7
|
**Requires:** Node.js 22+, Claude Code
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -394,4 +394,4 @@ Code generated by morph-spec (contracts, templates, implementation output) belon
|
|
|
394
394
|
|
|
395
395
|
---
|
|
396
396
|
|
|
397
|
-
*morph-spec v8.2.
|
|
397
|
+
*morph-spec v8.2.1 by [Polymorphism Tech](https://polymorphism.tech)*
|
package/bin/morph-spec.js
CHANGED
|
@@ -186,7 +186,7 @@ program
|
|
|
186
186
|
program
|
|
187
187
|
.command('score <feature> <task> <score>')
|
|
188
188
|
.description('Record a task eval score in feature.json (taskScores, server-side timestamp)')
|
|
189
|
-
.option('--dims <dims
|
|
189
|
+
.option('--dims <dims...>', 'Four dimension scores in order a,c,q,t (architecture,contracts,code-quality,test-coverage) — comma- or space-separated; quote under PowerShell: --dims "9.5,9,9,9"')
|
|
190
190
|
.option('--json', 'Output as JSON')
|
|
191
191
|
.action((feature, task, score, options) => scoreCommand(feature, task, score, options));
|
|
192
192
|
|
package/claude-plugin.json
CHANGED
package/docs/CHEATSHEET.md
CHANGED
package/docs/QUICKSTART.md
CHANGED
package/framework/CLAUDE.md
CHANGED
|
@@ -26,7 +26,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
26
26
|
- Apresentar gates com o template fixo definido em MORPH.md / morph-plan.
|
|
27
27
|
- Registrar decisões não triviais em `decisions.md`.
|
|
28
28
|
- Aplicar `morph-eval` após cada tarefa; silêncio em score ≥ 9 é o sinal correto. Score < 9 dispara o loop de correção autônomo (re-implementa, sem pausa), não escala ao humano entre tarefas. No Gate 3, o score composto da feature vem do **avaliador independente** despachado pela `morph-review` (persona `evaluator`, contexto fresco) — nunca de auto-avaliação de quem implementou.
|
|
29
|
-
- Incluir as 3 cláusulas do contrato do relatório final em todo dispatch de sub-agent (**
|
|
29
|
+
- Incluir as 3 cláusulas do contrato do relatório final em todo dispatch de sub-agent (**o sub-agent GRAVA o relatório em arquivo canônico ANTES de encerrar — `.morph/features/{f}/3-implement/reports/{taskId}.md`, ou `4-review/evaluator-report.md` para o avaliador — com os 4 itens obrigatórios (o quê / arquivos / problemas / auto-score 0-10); esse arquivo é o valor de retorno autoritativo, a última mensagem de texto é cortesia best-effort e pode não chegar inteira; ainda assim não chamar `SendMessage`, que gera um evento a mais e custa um turno inútil**; não tocar tasks.json/feature.json/board; sem build/test da suíte compartilhada em batch paralelo) — templates em `morph-implement` §4 e `morph-apply` §3b. **O orquestrador lê o arquivo ao retomar controle** — nunca confia só na mensagem de retorno da tool.
|
|
30
30
|
|
|
31
31
|
## Slash commands
|
|
32
32
|
|
|
@@ -45,7 +45,7 @@ Se não houver feature ativa, pergunte ao usuário qual feature trabalhar.
|
|
|
45
45
|
- `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).
|
|
46
46
|
- `morph-spec archive <feature>` — arquiva a feature (atômico, git-aware); é o que `/morph-archive` chama.
|
|
47
47
|
- `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).
|
|
48
|
-
- `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**.
|
|
48
|
+
- `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**. 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.
|
|
49
49
|
- `morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]` — 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.
|
|
50
50
|
- `morph-spec create <feature> --type <feature|bug|chore|hotfix> [--request "<txt>"] [--worktree]` — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type`); `--worktree` já nasce a feature no seu worktree próprio.
|
|
51
51
|
- `morph-spec retype <feature> <newType>` — muda o tipo de trabalho mid-flight; desfaz só os gates carimbados por política, preserva aprovações humanas; bloqueado após o Gate 3.
|
package/framework/MORPH.md
CHANGED
|
@@ -269,14 +269,32 @@ Inline (sem dispatch) é aceitável para tasks curtas (effort S/M) **dentro do d
|
|
|
269
269
|
- Contexto de outras tarefas não relacionadas
|
|
270
270
|
- Outputs de ferramentas não pertinentes à tarefa
|
|
271
271
|
|
|
272
|
-
**O
|
|
272
|
+
**O ARQUIVO é o retorno, a última mensagem é cortesia.** Em uso real (feature `config-tenant-split`,
|
|
273
|
+
MORPH_GHLBrain, 2026-07-22 — overnight com dispatch de sub-agents), o evento recebido de volta do
|
|
274
|
+
sub-agent foi só um `idle_notification`, sem o texto — o orquestrador teve que reconstruir o
|
|
275
|
+
relatório do disco e pedir via `SendMessage` que o avaliador o persistisse. A última mensagem de
|
|
276
|
+
texto **pode não chegar** inteira ao orquestrador; por isso ela deixou de ser o canal
|
|
277
|
+
autoritativo. O sub-agent **sempre** grava o relatório também em um arquivo canônico:
|
|
278
|
+
|
|
279
|
+
- Task de implementação: `.morph/features/{feature}/3-implement/reports/{taskId}.md` (um arquivo
|
|
280
|
+
por task concluída, mesmo quando uma única sessão de sub-agent cobre várias tasks de um fluxo).
|
|
281
|
+
- Avaliador do Gate 3 (`morph-evaluator`): `.morph/features/{feature}/4-review/evaluator-report.md`
|
|
282
|
+
— o relatório BRUTO do avaliador; a `morph-review` o lê e sintetiza o `review-report.md`
|
|
283
|
+
apresentado no Gate 3 (os dois nunca colidem: um é saída crua do sub-agent, o outro é a curadoria
|
|
284
|
+
do orquestrador).
|
|
285
|
+
|
|
286
|
+
O orquestrador **lê o arquivo** ao retomar controle após o dispatch — nunca confia apenas na
|
|
287
|
+
mensagem de retorno da tool. Se a mensagem de texto final chegou inteira, ótimo (economiza uma
|
|
288
|
+
leitura); se não chegou, o arquivo já está lá.
|
|
289
|
+
|
|
290
|
+
**O que receber de volta (no arquivo, e na mensagem final quando ela chega):**
|
|
273
291
|
- O que foi feito (2-3 frases)
|
|
274
292
|
- Arquivos modificados (lista)
|
|
275
293
|
- Problemas encontrados e como foram resolvidos
|
|
276
294
|
- Auto-score 0-10 com justificativa em uma linha
|
|
277
295
|
|
|
278
296
|
**Contrato do relatório final** (cláusulas que todo prompt de dispatch inclui — ver templates em `morph-implement` §4 e `morph-apply` §3b):
|
|
279
|
-
1. **
|
|
297
|
+
1. **GRAVE o relatório no arquivo canônico do seu dispatch ANTES de encerrar** — é o valor de retorno autoritativo desta sessão, não a sua última mensagem de texto (que é cortesia best-effort e pode não chegar inteira ao orquestrador). Conteúdo obrigatório, idêntico no arquivo: (1) o que foi feito em 2-3 frases, (2) arquivos modificados, (3) problemas encontrados e como foram resolvidos, (4) auto-score 0-10 com justificativa em uma linha. Relatório ausente, oco, ou sem os 4 itens é **falha de protocolo** — registrada no recap. Ainda assim **não chame `SendMessage`**: ele gera um evento a mais (o teammate-message) além da notificação de término, custando um turno inútil ao orquestrador — a mensagem de texto final (cortesia) já é entregue automaticamente.
|
|
280
298
|
2. Não tocar em `tasks.json`, `feature.json`, `state.json` nem no board de sessão (TaskCreate/TaskUpdate) — todo estado é gravado exclusivamente pelo orquestrador.
|
|
281
299
|
3. **Verificação em batch paralelo — condicional ao isolamento:** em batch **sem isolamento** (agentes na mesma working tree), não rode build/test da suíte compartilhada — a verificação (`morph-spec verify`) é do orquestrador, uma vez, pós-batch. Em batch **com `isolation: "worktree"`** (quando o `morph-spec dag` recomenda, por colisão de `outputs` entre fluxos), a regra se inverte: rode build/test **dentro do seu worktree**, commite o trabalho na branch do worktree como último ato antes do relatório, reporte a branch + status de build, e **nunca** faça merge nem toque na árvore principal — a integração é do orquestrador.
|
|
282
300
|
|
|
@@ -52,7 +52,13 @@ function diffPlanContract(oldDoc, newDoc, mutableFields) {
|
|
|
52
52
|
const violations = [];
|
|
53
53
|
const mutable = new Set(mutableFields);
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// Backfilling a previously-absent `version` (undefined → any value) is a
|
|
56
|
+
// benign schema-tooling update, not a plan-contract change — it's what
|
|
57
|
+
// lets legacy tasks.json adopt the field so `morph-spec dag` can analyze
|
|
58
|
+
// it without a lenient fallback. An actual change between two SET
|
|
59
|
+
// versions is still a real contract change and stays blocked.
|
|
60
|
+
const versionBackfilled = oldDoc.version === undefined && newDoc.version !== undefined;
|
|
61
|
+
if (!versionBackfilled && oldDoc.version !== newDoc.version) {
|
|
56
62
|
violations.push(`'version' changed (${oldDoc.version} → ${newDoc.version})`);
|
|
57
63
|
}
|
|
58
64
|
if (oldDoc.feature !== newDoc.feature) {
|
|
@@ -84,7 +84,14 @@ export function getBuildSteps(stack, { cwd, frontendPath, backendPath } = {}) {
|
|
|
84
84
|
|
|
85
85
|
if (isFrontendStack(s)) {
|
|
86
86
|
const frontendDir = frontendPath ? join(cwd, frontendPath) : cwd;
|
|
87
|
-
|
|
87
|
+
// node_modules guard (same fail-open as post-edit-typecheck.js): a fresh worktree before
|
|
88
|
+
// `morph-spec worktree link` has tsconfig.json (versioned) but no toolchain — running
|
|
89
|
+
// `npx tsc` there fetches an unrelated "tsc" package and fails noisily. No toolchain →
|
|
90
|
+
// no tsc step; the real typecheck runs wherever node_modules exists (root/CI).
|
|
91
|
+
if (
|
|
92
|
+
existsSync(join(frontendDir, 'tsconfig.json')) &&
|
|
93
|
+
existsSync(join(frontendDir, 'node_modules'))
|
|
94
|
+
) {
|
|
88
95
|
steps.push({ command: TSC_BUILD_CMD, cwd: frontendDir });
|
|
89
96
|
}
|
|
90
97
|
}
|
|
@@ -93,7 +93,16 @@ export function getToolName(payload) {
|
|
|
93
93
|
* Build a normalized signature for loop detection.
|
|
94
94
|
* - Edit/Write: "Edit:normalized/file/path"
|
|
95
95
|
* - Bash: "Bash:first two words" (e.g., "Bash:npm test")
|
|
96
|
-
* -
|
|
96
|
+
* - Every other tool WITH input: "Tool:<truncated JSON of tool_input>"
|
|
97
|
+
* - No tool_input at all: just the tool name
|
|
98
|
+
*
|
|
99
|
+
* The general (non-Edit/Write/Bash) case matters: a burst of `Read`/`Skill`/
|
|
100
|
+
* `Grep`/`Agent` calls against DIFFERENT targets (phase-context loading,
|
|
101
|
+
* post-task evaluation reads, parallel subagent dispatch) is normal work, not
|
|
102
|
+
* a loop. Collapsing every one of them to the bare tool name is what made
|
|
103
|
+
* loop-detect fire on exactly that legitimate traffic — a real, reported
|
|
104
|
+
* false positive. Folding in the actual input (rather than hand-listing a
|
|
105
|
+
* target field per tool) fixes it uniformly for every tool at once.
|
|
97
106
|
*
|
|
98
107
|
* @param {Object} payload - Hook payload from stdin
|
|
99
108
|
* @returns {string} Signature string
|
|
@@ -116,5 +125,10 @@ export function getCommandSignature(payload) {
|
|
|
116
125
|
return words ? `Bash:${words}` : 'Bash';
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
|
|
128
|
+
try {
|
|
129
|
+
const json = JSON.stringify(input).replace(/\\/g, '/');
|
|
130
|
+
return json ? `${tool}:${json.slice(0, 200)}` : tool;
|
|
131
|
+
} catch {
|
|
132
|
+
return tool;
|
|
133
|
+
}
|
|
120
134
|
}
|
|
@@ -35,6 +35,7 @@ export const OUTPUT_PHASE_MAP = {
|
|
|
35
35
|
decisions: 'plan',
|
|
36
36
|
recap: 'implement',
|
|
37
37
|
reviewReport: 'review',
|
|
38
|
+
evaluatorReport: 'review',
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
/** Map filename → output type (camelCase) */
|
|
@@ -52,6 +53,7 @@ export const FILENAME_TO_OUTPUT = {
|
|
|
52
53
|
'decisions.md': 'decisions',
|
|
53
54
|
'recap.md': 'recap',
|
|
54
55
|
'review-report.md': 'reviewReport',
|
|
56
|
+
'evaluator-report.md': 'evaluatorReport',
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
/** Protected spec files and the approval gate that locks them */
|
|
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# create-umbraco-lp
|
|
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# migrate-lp-to-umbraco
|
|
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -135,11 +135,16 @@ dispatch Agent (subagent_type: "morph-{id}") with prompt:
|
|
|
135
135
|
Não modifique spec.md, mandate.md.
|
|
136
136
|
|
|
137
137
|
Contrato do relatório final:
|
|
138
|
-
1.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
1. GRAVE um relatório por task concluída em
|
|
139
|
+
`.morph/features/{feature}/3-implement/reports/{taskId}.md` ANTES de
|
|
140
|
+
encerrar — mesmo quando esta sessão cobre várias tasks do fluxo, um
|
|
141
|
+
arquivo por task. Esse arquivo é o valor de retorno autoritativo; a sua
|
|
142
|
+
última mensagem de texto é cortesia best-effort e pode não chegar
|
|
143
|
+
inteira ao orquestrador (o evento recebido às vezes é só um
|
|
144
|
+
idle_notification, sem texto). Ainda assim NÃO chame SendMessage: ele
|
|
145
|
+
gera um evento a mais além da notificação de término, custando um turno
|
|
146
|
+
inútil ao orquestrador. Relatório ausente, oco, ou sem os itens abaixo,
|
|
147
|
+
é falha de protocolo.
|
|
143
148
|
2. NÃO toque em tasks.json, feature.json, state.json nem no board de sessão
|
|
144
149
|
(TaskCreate/TaskUpdate) — todo estado (status de task, scores, gates) é
|
|
145
150
|
gravado exclusivamente pelo orquestrador.
|
|
@@ -163,7 +168,7 @@ dispatch Agent (subagent_type: "morph-{id}") with prompt:
|
|
|
163
168
|
|
|
164
169
|
> **Alternativa quando os fluxos tocam os mesmos projetos/arquivos** (ex.: o mesmo csproj de testes compartilhado): siga a §3b-w abaixo em vez desta seção — despache com `isolation: "worktree"`, cada agente trabalha numa working tree própria e o orquestrador integra ao final.
|
|
165
170
|
|
|
166
|
-
Aguardar todos os agentes terminarem
|
|
171
|
+
Aguardar todos os agentes terminarem, depois **ler primeiro** `.morph/features/{feature}/3-implement/reports/{taskId}.md` de cada task — é o valor de retorno autoritativo; a mensagem de texto do dispatch, se chegou inteira, é só um atalho que economiza essa leitura. Se um arquivo vier ausente, oco, ou sem os itens da cláusula 1, registre a falha de protocolo no recap. Depois, sequencialmente:
|
|
167
172
|
- **Rodar `morph-spec verify {feature} --json` uma única vez, pós-batch** (escopo feature) — os agentes não a rodaram (cláusula 3). Vermelho: use `verify.feedback` para corrigir/redespachar ANTES de qualquer morph-eval. (Com isolamento por worktree, cada agente já rodou o verify dentro do seu worktree; o orquestrador roda o verify compartilhado UMA vez após o merge — ver §3b-w.)
|
|
168
173
|
- Para cada relatório retornado:
|
|
169
174
|
- Aplicar `Skill(morph-eval)`.
|
|
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber um novo request de feature, antes de morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-brainstorming
|
|
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-eval
|
|
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-hotfix
|
|
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
|
|
6
6
|
user-invokable: false
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-implement
|
|
@@ -59,11 +59,14 @@ Ao iniciar cada task de `tasks.json` — antes de escrever qualquer código. Nor
|
|
|
59
59
|
Done quando: [doneCriteria do tasks.json]
|
|
60
60
|
|
|
61
61
|
Contrato do relatório final:
|
|
62
|
-
1.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
1. GRAVE seu relatório em `.morph/features/{feature}/3-implement/reports/[ID].md`
|
|
63
|
+
ANTES de encerrar — esse arquivo é o valor de retorno autoritativo desta
|
|
64
|
+
sessão. A sua última mensagem de texto é cortesia best-effort: pode não
|
|
65
|
+
chegar inteira ao orquestrador (o evento recebido às vezes é só um
|
|
66
|
+
idle_notification, sem texto). Ainda assim NÃO chame SendMessage: ele
|
|
67
|
+
gera um evento a mais além da notificação de término, custando um turno
|
|
68
|
+
inútil ao orquestrador. Relatório ausente, oco, ou sem os 4 itens abaixo,
|
|
69
|
+
é falha de protocolo.
|
|
67
70
|
2. NÃO toque em tasks.json, feature.json, state.json nem no board de sessão
|
|
68
71
|
(TaskCreate/TaskUpdate) — todo estado é gravado exclusivamente pelo
|
|
69
72
|
orquestrador.
|
|
@@ -74,11 +77,18 @@ Ao iniciar cada task de `tasks.json` — antes de escrever qualquer código. Nor
|
|
|
74
77
|
commite na branch do worktree como último ato antes do relatório
|
|
75
78
|
e reporte a branch — nunca faça merge.
|
|
76
79
|
|
|
77
|
-
Seu relatório
|
|
78
|
-
(
|
|
79
|
-
(4) auto-score 0-10.
|
|
80
|
+
Seu relatório (no arquivo, e na mensagem final quando ela chega) deve conter
|
|
81
|
+
APENAS: (1) o que foi feito em 2-3 frases, (2) arquivos modificados,
|
|
82
|
+
(3) problemas encontrados e como foram resolvidos, (4) auto-score 0-10.
|
|
80
83
|
```
|
|
81
|
-
Após
|
|
84
|
+
Após o retorno do dispatch: **leia primeiro** `.morph/features/{feature}/3-implement/reports/[ID].md`
|
|
85
|
+
— é o valor de retorno autoritativo. Trate a mensagem de texto do sub-agent, se chegou inteira,
|
|
86
|
+
como um atalho que economiza essa leitura — nunca como a única fonte. Arquivo ausente é falha de
|
|
87
|
+
protocolo (registre no recap e re-despache/complete a partir do que o sub-agent já fez, se
|
|
88
|
+
recuperável). O auto-score é **triagem, não aprovação** — se < 7, re-despache com o feedback
|
|
89
|
+
específico (ou corrija inline) antes de prosseguir. Em qualquer caso, a task só avança quando
|
|
90
|
+
`morph-eval` der score ≥ 9 (barra única do MORPH.md §0). Integre o relatório ao recap.md e vá
|
|
91
|
+
para o passo 7.
|
|
82
92
|
|
|
83
93
|
**Se não — siga para o passo 5.**
|
|
84
94
|
|
|
@@ -136,6 +146,8 @@ Ao iniciar cada task de `tasks.json` — antes de escrever qualquer código. Nor
|
|
|
136
146
|
|
|
137
147
|
## Output esperado
|
|
138
148
|
- Código implementado conforme o standard consultado (direto ou via sub-agent)
|
|
149
|
+
- Quando despachada: `.morph/features/{feature}/3-implement/reports/{taskId}.md` — o relatório
|
|
150
|
+
autoritativo do sub-agent, canal durável (passo 4)
|
|
139
151
|
- `.morph/features/{feature}/3-implement/recap.md` atualizado com entrada da task concluída
|
|
140
152
|
- `.morph/features/{feature}/2-plan/tasks.json` com status da task atualizado para `done`
|
|
141
153
|
- `.morph/features/{feature}/2-plan/decisions.md` atualizado se houver decisão não trivial
|
|
@@ -4,7 +4,7 @@ description: Guia a produção dos três artefatos de planejamento de uma featur
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-plan
|
|
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-proposal
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase de revisão e produz o review-report.md para o Gate 3
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar a fase review, após todas as tasks de tasks.json estarem com status `done`
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -56,18 +56,22 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
|
|
|
56
56
|
seguindo as bandas 0-10 de cada uma.
|
|
57
57
|
NÃO reescreva código. NÃO toque em tasks.json, feature.json nem no board
|
|
58
58
|
de sessão.
|
|
59
|
-
Contrato do relatório final
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
Contrato do relatório final: GRAVE seu relatório em
|
|
60
|
+
`.morph/features/{feature}/4-review/evaluator-report.md` ANTES de
|
|
61
|
+
encerrar — esse arquivo é o valor de retorno autoritativo desta
|
|
62
|
+
sessão. A sua última mensagem de texto é cortesia best-effort: pode
|
|
63
|
+
não chegar inteira ao orquestrador (o evento recebido às vezes é só
|
|
64
|
+
um idle_notification, sem texto). Ainda assim NÃO chame SendMessage:
|
|
65
|
+
ele gera um evento a mais além da notificação de término, custando
|
|
66
|
+
um turno inútil ao orquestrador.
|
|
67
|
+
Seu relatório (no arquivo, e na mensagem final quando ela chega)
|
|
68
|
+
deve conter: score 0-10 por dimensão, score composto (arch×0.30 +
|
|
69
|
+
contracts×0.25 + quality×0.25 + tests×0.20), e feedback priorizado
|
|
70
|
+
(bloqueantes primeiro) para qualquer dimensão < 9. Relatório
|
|
71
|
+
ausente, oco, ou sem esses itens, é falha de protocolo.
|
|
68
72
|
```
|
|
69
73
|
- **Fallback (tipo não registrado):** se `morph-evaluator` não estiver na sua lista de subagent types, despache `general-purpose` colando o campo `persona` do agente `evaluator` (Read em `.morph/framework/agents.json`) no topo do prompt — e **registre o fallback no review-report** (seção de método).
|
|
70
|
-
- Os scores por dimensão e o composto do review-report vêm **desse
|
|
74
|
+
- **Leia primeiro** `.morph/features/{feature}/4-review/evaluator-report.md` — é o valor de retorno autoritativo, não a mensagem de texto do dispatch (que é cortesia, útil só quando chega inteira). Os scores por dimensão e o composto do review-report vêm **desse arquivo**, não da sua auto-avaliação. Se o arquivo vier ausente, oco, ou sem os scores por dimensão, registre a falha de protocolo na seção de método do review-report e re-despache.
|
|
71
75
|
|
|
72
76
|
5. Produza `review-report.md` em `.morph/features/{feature}/4-review/` com:
|
|
73
77
|
- Scores por dimensão (arquitetura, contratos, qualidade, cobertura)
|
|
@@ -117,7 +121,10 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
|
|
|
117
121
|
No caminho manual, o hook state-sync detecta a aprovação em answers de AskUserQuestion e grava `approvalGates.review = { approved: true, timestamp, approvedBy: "manual" }` no `feature.json` autoritativo da feature. Em auto sem AskUserQuestion, aprove via `morph-spec approve {feature} review --mode auto` — nunca via Edit à mão de state.json.
|
|
118
122
|
|
|
119
123
|
## Output esperado
|
|
120
|
-
- `.morph/features/{feature}/4-review/
|
|
124
|
+
- `.morph/features/{feature}/4-review/evaluator-report.md` — relatório bruto e autoritativo do
|
|
125
|
+
`morph-evaluator` despachado no passo 4 (canal durável, ver MORPH.md §6)
|
|
126
|
+
- `.morph/features/{feature}/4-review/review-report.md` — síntese do orquestrador a partir do
|
|
127
|
+
evaluator-report, com scores e recomendação
|
|
121
128
|
- Trace da fase com `phase_summary` preenchido
|
|
122
129
|
- Gate 3 registrado aprovado em `.morph/features/{feature}/feature.json` (autoritativo), na forma canônica `{ approved, timestamp, approvedBy }` — via `morph-spec approve` em auto, ou pelo hook state-sync no caminho manual
|
|
123
130
|
|
|
@@ -4,7 +4,7 @@ description: Varre o codebase existente, detecta stack atual, compara com o stac
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao iniciar uma feature em projeto brownfield (codebase já existente)
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-stack-scan
|
|
@@ -4,7 +4,7 @@ description: Localiza e carrega um standard específico do STANDARDS.json sob de
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: quando precisar consultar um standard específico durante a implementação
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.2.
|
|
7
|
+
cliVersion: "8.2.1"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
package/package.json
CHANGED
|
@@ -145,6 +145,38 @@ function getDateSuffix() {
|
|
|
145
145
|
|
|
146
146
|
// ── Infrastructure linking ───────────────────────────────────────────────────
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* The infra directories morph itself links into a worktree — the ONLY ones
|
|
150
|
+
* `setupInfraLinks`/`worktreeLinkCommand --all`/`unlinkWorktreeInfraLinks` treat
|
|
151
|
+
* as KNOWN, and therefore the only ones `finish` may auto-remove via the
|
|
152
|
+
* already-proven-safe unlink mechanism (never by widening the reparse-point
|
|
153
|
+
* scan — see `findReparsePoints`).
|
|
154
|
+
*
|
|
155
|
+
* Always: `node_modules`, `.claude`. Conditionally: `.morph/framework` (client
|
|
156
|
+
* projects only) and `{project.frontendPath}/node_modules` — a monorepo
|
|
157
|
+
* frontend (e.g. a Next.js app under `apps/catalog-admin`) commonly has its OWN
|
|
158
|
+
* node_modules, a separate `npm install` from the root's, invisible to a static
|
|
159
|
+
* list. Fail-open: a missing/unreadable config.json just means no frontendPath.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} mainCwd - Absolute path to the main project root
|
|
162
|
+
* @returns {string[]} relative paths, root-first
|
|
163
|
+
*/
|
|
164
|
+
function resolveInfraLinkDirs(mainCwd) {
|
|
165
|
+
const dirs = ['node_modules', '.claude'];
|
|
166
|
+
if (existsSync(join(mainCwd, '.morph', 'framework'))) {
|
|
167
|
+
dirs.push(join('.morph', 'framework'));
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const configPath = join(mainCwd, '.morph', 'config', 'config.json');
|
|
171
|
+
if (existsSync(configPath)) {
|
|
172
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
173
|
+
const frontendPath = config?.project?.frontendPath;
|
|
174
|
+
if (frontendPath) dirs.push(join(frontendPath, 'node_modules'));
|
|
175
|
+
}
|
|
176
|
+
} catch { /* fail-open — no frontendPath to add */ }
|
|
177
|
+
return dirs;
|
|
178
|
+
}
|
|
179
|
+
|
|
148
180
|
/**
|
|
149
181
|
* Create junctions in the worktree for shared infrastructure directories
|
|
150
182
|
* (node_modules/, .claude/, .morph/framework/).
|
|
@@ -169,16 +201,9 @@ export function setupInfraLinks(worktreePath, mainCwd, opts = {}) {
|
|
|
169
201
|
const mainAbs = resolve(mainCwd);
|
|
170
202
|
const wtAbs = resolve(worktreePath);
|
|
171
203
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
} else {
|
|
176
|
-
dirLinks = ['node_modules', '.claude'];
|
|
177
|
-
// .morph/framework/ — client projects (in framework repo, framework/ is tracked)
|
|
178
|
-
if (existsSync(join(mainAbs, '.morph', 'framework'))) {
|
|
179
|
-
dirLinks.push(join('.morph', 'framework'));
|
|
180
|
-
}
|
|
181
|
-
}
|
|
204
|
+
const dirLinks = (Array.isArray(opts.dirs) && opts.dirs.length)
|
|
205
|
+
? opts.dirs
|
|
206
|
+
: resolveInfraLinkDirs(mainAbs);
|
|
182
207
|
|
|
183
208
|
for (const rel of dirLinks) {
|
|
184
209
|
const target = join(mainAbs, rel);
|
|
@@ -530,7 +555,7 @@ export function worktreeLinkCommand(options = {}) {
|
|
|
530
555
|
}
|
|
531
556
|
|
|
532
557
|
const dirs = options.all
|
|
533
|
-
?
|
|
558
|
+
? resolveInfraLinkDirs(root)
|
|
534
559
|
: ['node_modules'];
|
|
535
560
|
|
|
536
561
|
const alreadyPresent = {};
|
|
@@ -624,12 +649,10 @@ function findWorktreeForFeature(feature, root) {
|
|
|
624
649
|
return { path: match.path, branch: actual, expectedBranch };
|
|
625
650
|
}
|
|
626
651
|
|
|
627
|
-
/** The infra paths `setupInfraLinks` junctions into a worktree. */
|
|
628
|
-
const INFRA_LINK_RELS = ['node_modules', '.claude', join('.morph', 'framework')];
|
|
629
|
-
|
|
630
652
|
/**
|
|
631
653
|
* Drop the infra junctions a worktree carries (`node_modules`, `.claude`,
|
|
632
|
-
* `.morph/framework
|
|
654
|
+
* `.morph/framework`, and the `project.frontendPath` node_modules if
|
|
655
|
+
* configured) WITHOUT following them.
|
|
633
656
|
*
|
|
634
657
|
* CRITICAL — this is what keeps `worktree finish` from destroying the project.
|
|
635
658
|
* `git worktree remove` refuses a worktree still holding these ("contains
|
|
@@ -646,12 +669,19 @@ const INFRA_LINK_RELS = ['node_modules', '.claude', join('.morph', 'framework')]
|
|
|
646
669
|
* target's contents, and `rmdirSync` fails on a real non-empty directory — the
|
|
647
670
|
* safety that stops this from ever deleting genuine files.
|
|
648
671
|
*
|
|
672
|
+
* This is deliberately narrow to the KNOWN inventory from `resolveInfraLinkDirs`
|
|
673
|
+
* — never widened to auto-remove any reparse point `findReparsePoints` might
|
|
674
|
+
* discover elsewhere. An unknown/nested link (npm link, npm workspaces) must
|
|
675
|
+
* still decline the removal; see the CANARY tests pinning that in worktree.test.js.
|
|
676
|
+
*
|
|
649
677
|
* @param {string} worktreePath
|
|
678
|
+
* @param {string} mainCwd - Absolute path to the main project root, to resolve
|
|
679
|
+
* `project.frontendPath` from config.json
|
|
650
680
|
* @returns {string[]} relative paths actually unlinked
|
|
651
681
|
*/
|
|
652
|
-
export function unlinkWorktreeInfraLinks(worktreePath) {
|
|
682
|
+
export function unlinkWorktreeInfraLinks(worktreePath, mainCwd) {
|
|
653
683
|
const removed = [];
|
|
654
|
-
for (const rel of
|
|
684
|
+
for (const rel of resolveInfraLinkDirs(mainCwd)) {
|
|
655
685
|
const p = join(worktreePath, rel);
|
|
656
686
|
let st;
|
|
657
687
|
try {
|
|
@@ -746,9 +776,60 @@ function findReparsePoints(dir, base = dir, acc = { points: [], unprovable: [] }
|
|
|
746
776
|
*/
|
|
747
777
|
const IGNORED_SHIELDABLE_PREFIXES = ['.morph/'];
|
|
748
778
|
|
|
779
|
+
/**
|
|
780
|
+
* Well-known, regenerable build artifacts — `bin/`, `obj/` (dotnet), a real
|
|
781
|
+
* `node_modules` left behind outside the setup-created junction, and
|
|
782
|
+
* `*.tsbuildinfo` (tsc incremental cache). These are the everyday leftovers of
|
|
783
|
+
* running a build/test cycle inside a worktree, not user data: they are fully
|
|
784
|
+
* reproducible from source, and requiring `--discard-ignored` for every finish
|
|
785
|
+
* that ever ran a build turns the flag from "explicit consent for a real risk"
|
|
786
|
+
* into a rubber stamp nobody reads anymore. Auto-discarding them (like any
|
|
787
|
+
* `discarded` entry, WITHOUT a backup — see the note below) is safe precisely
|
|
788
|
+
* because "regenerable" is a stronger guarantee than "backed up".
|
|
789
|
+
*
|
|
790
|
+
* Deliberately narrow — do NOT add speculative patterns (`dist/`, `.next/`)
|
|
791
|
+
* beyond what's proven to be pure noise; anything else stays in `blocking` by
|
|
792
|
+
* default, requiring the explicit `--discard-ignored` consent.
|
|
793
|
+
*
|
|
794
|
+
* @param {string} p - forward-slash relative path from `git status --ignored`
|
|
795
|
+
*/
|
|
796
|
+
/**
|
|
797
|
+
* Whether a dirty root-side path is EXACTLY a sibling feature's live worktree
|
|
798
|
+
* registration — see the call site (Validation 3 in worktreeFinishCommand)
|
|
799
|
+
* for why this is safe to exempt from the whole-root clean gate rather than a
|
|
800
|
+
* widened hole.
|
|
801
|
+
*
|
|
802
|
+
* PROVES it rather than assuming: the path must be exactly that sibling's
|
|
803
|
+
* `feature.json`, must parse, and its `worktree.path` must point at a
|
|
804
|
+
* currently-existing directory. A parse failure, a missing registration, or a
|
|
805
|
+
* STALE pointer (worktree already removed) is NOT exempted.
|
|
806
|
+
*
|
|
807
|
+
* @param {string} relPath - forward-slash path relative to `root`
|
|
808
|
+
* @param {string} root - absolute path to the primary root
|
|
809
|
+
* @param {string} feature - the feature THIS finish is acting on (never exempted — its own scope is the shed's job)
|
|
810
|
+
*/
|
|
811
|
+
function isLiveSiblingWorktreeRegistration(relPath, root, feature) {
|
|
812
|
+
const m = relPath.match(/^\.morph\/features\/([^/]+)\/feature\.json$/);
|
|
813
|
+
if (!m || m[1] === feature) return false;
|
|
814
|
+
try {
|
|
815
|
+
const fj = JSON.parse(readFileSync(join(root, relPath), 'utf8'));
|
|
816
|
+
const wtPath = fj?.worktree?.path;
|
|
817
|
+
return typeof wtPath === 'string' && wtPath.length > 0 && existsSync(wtPath);
|
|
818
|
+
} catch {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function isDisposableBuildArtifact(p) {
|
|
824
|
+
if (/\.tsbuildinfo$/.test(p)) return true;
|
|
825
|
+
const segments = p.split('/').filter(Boolean);
|
|
826
|
+
return segments.some((s) => s === 'bin' || s === 'obj' || s === 'node_modules');
|
|
827
|
+
}
|
|
828
|
+
|
|
749
829
|
/**
|
|
750
830
|
* Split the worktree's GITIGNORED leftovers into what this command may remove
|
|
751
|
-
* (morph's own, after a backup
|
|
831
|
+
* (morph's own, after a backup, or a known disposable build artifact, without
|
|
832
|
+
* one) and what it must refuse to destroy.
|
|
752
833
|
*
|
|
753
834
|
* Why this exists: `git worktree remove` deletes gitignored files outright, and
|
|
754
835
|
* the clean-check that gates it uses `git status --porcelain`, which omits them
|
|
@@ -762,34 +843,46 @@ const IGNORED_SHIELDABLE_PREFIXES = ['.morph/'];
|
|
|
762
843
|
*
|
|
763
844
|
* @param {string} worktreePath
|
|
764
845
|
* @param {{ discardIgnored?: boolean }} options
|
|
765
|
-
* @returns {{ shieldable: string[], blocking: string[] }}
|
|
846
|
+
* @returns {{ shieldable: string[], blocking: string[], discarded: string[] }}
|
|
766
847
|
*/
|
|
767
848
|
function classifyIgnoredLeftovers(worktreePath, options = {}) {
|
|
768
849
|
let out;
|
|
769
850
|
try {
|
|
770
851
|
// `-z` + execFile: NUL-delimited, no shell, no C-style quoting to undo —
|
|
771
852
|
// paths with spaces or non-ASCII (a pt-BR codebase has both) stay intact.
|
|
772
|
-
|
|
853
|
+
// `=matching` (not the traditional default): an ignored directory whose own
|
|
854
|
+
// name matches no pattern — e.g. `MyProj/`, ignored only because EVERYTHING
|
|
855
|
+
// inside it (`bin/`, `obj/`) does — otherwise collapses into one opaque
|
|
856
|
+
// `MyProj/` entry, hiding the actual disposable-artifact names from
|
|
857
|
+
// isDisposableBuildArtifact. A directory that IS itself the matched pattern
|
|
858
|
+
// (`node_modules/`) still reports as one entry either way — verified
|
|
859
|
+
// empirically, not assumed.
|
|
860
|
+
out = execFileSync('git', ['status', '--porcelain', '--ignored=matching', '-z'], { cwd: worktreePath, stdio: 'pipe' }).toString();
|
|
773
861
|
} catch {
|
|
774
|
-
return { shieldable: [], blocking: ['<git status --ignored falhou — não foi possível enumerar o que seria destruído>'] };
|
|
862
|
+
return { shieldable: [], blocking: ['<git status --ignored falhou — não foi possível enumerar o que seria destruído>'], discarded: [] };
|
|
775
863
|
}
|
|
776
864
|
|
|
777
865
|
const shieldable = [];
|
|
778
|
-
const
|
|
866
|
+
const blockingRest = [];
|
|
867
|
+
const disposable = [];
|
|
779
868
|
for (const record of out.split('\0')) {
|
|
780
869
|
if (!record || record.length < 4) continue;
|
|
781
870
|
if (record.slice(0, 2) !== '!!') continue; // only ignored entries; the rest is the clean-check's job
|
|
782
871
|
const p = record.slice(3).replace(/\\/g, '/');
|
|
783
872
|
if (IGNORED_SHIELDABLE_PREFIXES.some((prefix) => p.startsWith(prefix))) shieldable.push(p);
|
|
784
|
-
else
|
|
873
|
+
else if (isDisposableBuildArtifact(p)) disposable.push(p);
|
|
874
|
+
else blockingRest.push(p);
|
|
785
875
|
}
|
|
786
|
-
// Explicit consent clears the block — but the list is NOT
|
|
787
|
-
// are the files that exist nowhere else, and they are the
|
|
788
|
-
// reporting them is the least this can do. (Backing them
|
|
789
|
-
// option: this bucket is where
|
|
790
|
-
// is unbounded. The user declared them
|
|
791
|
-
|
|
792
|
-
return { shieldable, blocking, discarded: [] };
|
|
876
|
+
// Explicit consent clears the rest of the block too — but the list is NOT
|
|
877
|
+
// thrown away. These are the files that exist nowhere else, and they are the
|
|
878
|
+
// ones about to die; reporting them is the least this can do. (Backing them
|
|
879
|
+
// up instead is not an option: this bucket is where arbitrarily large
|
|
880
|
+
// leftovers land, so a copy is unbounded. The user declared them
|
|
881
|
+
// disposable — they still get told.)
|
|
882
|
+
if (options.discardIgnored) return { shieldable, blocking: [], discarded: [...disposable, ...blockingRest] };
|
|
883
|
+
// Known build artifacts are discarded by default, no flag required — they
|
|
884
|
+
// are reproducible from source, unlike anything else in `blockingRest`.
|
|
885
|
+
return { shieldable, blocking: blockingRest, discarded: disposable };
|
|
793
886
|
}
|
|
794
887
|
|
|
795
888
|
/**
|
|
@@ -1015,15 +1108,34 @@ export async function worktreeFinishCommand(feature, options = {}) {
|
|
|
1015
1108
|
}
|
|
1016
1109
|
: {};
|
|
1017
1110
|
|
|
1018
|
-
// Validation 3: primary root must be clean
|
|
1019
|
-
|
|
1111
|
+
// Validation 3: primary root must be clean — except a SIBLING feature's own
|
|
1112
|
+
// LIVE worktree registration (see isLiveSiblingWorktreeRegistration). That
|
|
1113
|
+
// artifact is how `runWorktreeSetup`/`create --worktree` are DESIGNED to
|
|
1114
|
+
// leave the root for that sibling's entire life (decidePersistence in
|
|
1115
|
+
// verify.js and the anti-dupe guard above both read the root's copy for
|
|
1116
|
+
// `.worktree.path`), so refusing THIS feature's finish over it would block
|
|
1117
|
+
// every OTHER feature for as long as any sibling stays open in its own
|
|
1118
|
+
// worktree — the exact "orphaned scaffold collides with finish" friction
|
|
1119
|
+
// from real multi-feature use. Anything else is still unexplained dirt and
|
|
1120
|
+
// still blocks, fail-closed, exactly as before.
|
|
1121
|
+
let rootStatusRaw;
|
|
1020
1122
|
try {
|
|
1021
|
-
|
|
1123
|
+
// `--untracked-files=all`, not the default `normal`: the default collapses
|
|
1124
|
+
// an entirely-untracked directory into one entry (e.g. `.morph/features/
|
|
1125
|
+
// sibling-feat/`, verified empirically) — which would never match the
|
|
1126
|
+
// exact `.../feature.json` path isLiveSiblingWorktreeRegistration expects,
|
|
1127
|
+
// silently defeating the exemption for the very shape it exists to allow.
|
|
1128
|
+
rootStatusRaw = execFileSync('git', ['status', '--porcelain', '--untracked-files=all', '-z'], { cwd: root, stdio: 'pipe' }).toString();
|
|
1022
1129
|
} catch (err) {
|
|
1023
1130
|
console.log(JSON.stringify({ finished: false, error: 'primary-root-status-failed', detail: (err.message || '').slice(0, 200), feature, ...backupNote }));
|
|
1024
1131
|
process.exit(1);
|
|
1025
1132
|
}
|
|
1026
|
-
|
|
1133
|
+
const unexplainedDirt = rootStatusRaw.split('\0').filter((record) => {
|
|
1134
|
+
if (!record || record.length < 4) return false;
|
|
1135
|
+
const p = record.slice(3).replace(/\\/g, '/');
|
|
1136
|
+
return !isLiveSiblingWorktreeRegistration(p, root, feature);
|
|
1137
|
+
});
|
|
1138
|
+
if (unexplainedDirt.length) {
|
|
1027
1139
|
console.log(JSON.stringify({
|
|
1028
1140
|
finished: false,
|
|
1029
1141
|
error: 'primary-root-dirty',
|
|
@@ -1075,7 +1187,7 @@ export async function worktreeFinishCommand(feature, options = {}) {
|
|
|
1075
1187
|
// Both gates below therefore PROVE-or-DECLINE. Declining costs a stale
|
|
1076
1188
|
// directory the user can delete in one command; being wrong costs data that
|
|
1077
1189
|
// exists nowhere else.
|
|
1078
|
-
unlinkWorktreeInfraLinks(worktreePath);
|
|
1190
|
+
unlinkWorktreeInfraLinks(worktreePath, root);
|
|
1079
1191
|
|
|
1080
1192
|
const scan = findReparsePoints(worktreePath);
|
|
1081
1193
|
const ignored = classifyIgnoredLeftovers(worktreePath, options);
|
|
@@ -1202,7 +1314,9 @@ export async function worktreeFinishCommand(feature, options = {}) {
|
|
|
1202
1314
|
...(worktreeRemoved && ignored.discarded.length
|
|
1203
1315
|
? {
|
|
1204
1316
|
discardedIgnored: ignored.discarded,
|
|
1205
|
-
discardedNote:
|
|
1317
|
+
discardedNote: options.discardIgnored
|
|
1318
|
+
? `--discard-ignored: os caminhos acima eram gitignored, existiam só no worktree e foram DESTRUÍDOS sem backup (não são copiados por poderem ser arbitrariamente grandes)`
|
|
1319
|
+
: `artefatos de build reconhecidos (bin/, obj/, node_modules/, *.tsbuildinfo) — descartados por padrão, sem backup (regeneráveis a partir do source)`,
|
|
1206
1320
|
}
|
|
1207
1321
|
: {}),
|
|
1208
1322
|
...(worktreeRemoved
|
|
@@ -39,9 +39,20 @@ export async function scoreCommand(featureName, taskId, scoreArg, options = {})
|
|
|
39
39
|
|
|
40
40
|
let dimensions;
|
|
41
41
|
if (options.dims) {
|
|
42
|
-
|
|
42
|
+
// Commander captures --dims as variadic (`<dims...>`), so an UNQUOTED
|
|
43
|
+
// comma list typed in PowerShell — `--dims 9.5,9,9,9` — that arrives as
|
|
44
|
+
// 4 separate argv tokens (confirmed: Commander binds only the first
|
|
45
|
+
// token to a non-variadic option and silently drops the rest) is not
|
|
46
|
+
// lost. Accept either shape: one comma-joined string (the documented,
|
|
47
|
+
// quoted form) or N separate tokens — split on both separators so a
|
|
48
|
+
// mix of the two never matters either.
|
|
49
|
+
const raw = Array.isArray(options.dims) ? options.dims.join(' ') : String(options.dims);
|
|
50
|
+
const parts = raw.split(/[,\s]+/).filter(Boolean);
|
|
43
51
|
if (parts.length !== DIM_KEYS.length) {
|
|
44
|
-
console.error(chalk.red(
|
|
52
|
+
console.error(chalk.red(
|
|
53
|
+
`\n❌ --dims expects ${DIM_KEYS.length} values in order a,c,q,t (architecture,contracts,code-quality,test-coverage) — got ${parts.length} (${JSON.stringify(parts)}).\n` +
|
|
54
|
+
` Quote the value under PowerShell to keep it as one argument: --dims "9.5,9,9,9"\n`
|
|
55
|
+
));
|
|
45
56
|
process.exit(1);
|
|
46
57
|
}
|
|
47
58
|
dimensions = {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { parseTasksJsonLenient } from '../../lib/tasks/tasks-json-parser.js';
|
|
5
5
|
import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
|
|
6
6
|
import { renderMermaidFlowchart } from '../../lib/tasks/mermaid-renderer.js';
|
|
7
7
|
|
|
@@ -24,8 +24,16 @@ export async function dagCommand(featureName, options = {}) {
|
|
|
24
24
|
process.exit(1);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
27
|
+
// parseTasksJsonLenient validates the schema and throws with the error
|
|
28
|
+
// list, but defaults a missing `version` (legacy plans predating the
|
|
29
|
+
// field) instead of failing — the DAG analysis doesn't depend on it.
|
|
30
|
+
const { data, versionDefaulted } = await parseTasksJsonLenient(tasksPath);
|
|
31
|
+
if (versionDefaulted) {
|
|
32
|
+
console.error(chalk.yellow(
|
|
33
|
+
`⚠ tasks.json has no 'version' field — defaulting to "1.0" for this analysis.\n` +
|
|
34
|
+
` Consider adding it back explicitly (safe: protect-spec-files allows backfilling it).`
|
|
35
|
+
));
|
|
36
|
+
}
|
|
29
37
|
|
|
30
38
|
// --mermaid short-circuits before the dispatch analysis: visualizing the
|
|
31
39
|
// plan's DAG doesn't need a sequential-vs-parallel decision.
|
|
@@ -49,6 +49,11 @@ export const OUTPUT_SCHEMA = {
|
|
|
49
49
|
|
|
50
50
|
// 4-review/
|
|
51
51
|
reviewReport: { filename: 'review-report.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
52
|
+
// Durable report the dispatched morph-evaluator sub-agent writes itself
|
|
53
|
+
// (MORPH.md §6) — the orchestrator reads it and synthesizes reviewReport
|
|
54
|
+
// from it; the two never collide because one is raw sub-agent output and
|
|
55
|
+
// the other is the orchestrator's curated Gate 3 presentation.
|
|
56
|
+
evaluatorReport:{ filename: 'evaluator-report.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
52
57
|
};
|
|
53
58
|
|
|
54
59
|
// ============================================================================
|
|
@@ -89,6 +94,29 @@ export function getFeatureDir(featureName) {
|
|
|
89
94
|
return join('.morph', 'features', featureName).replace(/\\/g, '/');
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Per-task sub-agent report — the durable "return value" channel for a
|
|
99
|
+
* dispatched implementation task (MORPH.md §6). A sub-agent's final text
|
|
100
|
+
* message may never reach the orchestrator (idle_notification with no text
|
|
101
|
+
* is an observed failure mode); the file it writes here always does. Doesn't
|
|
102
|
+
* fit OUTPUT_SCHEMA's one-fixed-filename-per-type model since the filename
|
|
103
|
+
* is parameterized by taskId, so it's a sibling helper instead of a schema
|
|
104
|
+
* entry.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} featureName
|
|
107
|
+
* @param {string} taskId - e.g. "T7"
|
|
108
|
+
* @returns {string} morph-relative, forward-slash path
|
|
109
|
+
*/
|
|
110
|
+
export function getTaskReportPath(featureName, taskId) {
|
|
111
|
+
return join('.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`)
|
|
112
|
+
.replace(/\\/g, '/');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Absolute filesystem variant of {@link getTaskReportPath}. */
|
|
116
|
+
export function getAbsoluteTaskReportPath(projectRoot, featureName, taskId) {
|
|
117
|
+
return join(projectRoot, '.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`);
|
|
118
|
+
}
|
|
119
|
+
|
|
92
120
|
/**
|
|
93
121
|
* Get all output paths for a feature, ready for state.json `outputs` block.
|
|
94
122
|
*/
|
|
@@ -51,24 +51,55 @@ function readStack(cwd) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Merge two or more profiles' validator lists (deduped) and boolean flags. */
|
|
55
|
+
function mergeProfiles(profiles) {
|
|
56
|
+
return {
|
|
57
|
+
isDotnet: profiles.some((p) => p.isDotnet),
|
|
58
|
+
isNextjs: profiles.some((p) => p.isNextjs),
|
|
59
|
+
defaultValidators: [...new Set(profiles.flatMap((p) => p.defaultValidators))],
|
|
60
|
+
checkpointValidators: [...new Set(profiles.flatMap((p) => p.checkpointValidators))],
|
|
61
|
+
buildCommand: profiles.map((p) => p.buildCommand).find(Boolean) ?? null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
/**
|
|
55
66
|
* Get the StackProfile for the current project.
|
|
67
|
+
*
|
|
68
|
+
* `stack` may be a compound value (e.g. "dotnet-nextjs", "dotnet+nextjs") —
|
|
69
|
+
* the framework's own convention for monorepos (see stack-filter.js's
|
|
70
|
+
* getProjectTags, which splits project.stack the same way). Each segment is
|
|
71
|
+
* resolved independently and matching profiles are merged, instead of the
|
|
72
|
+
* whole compound string failing every exact-match check and silently
|
|
73
|
+
* collapsing to the empty profile (0 default validators).
|
|
56
74
|
*/
|
|
57
75
|
export function getStackProfile(cwd, stackOverride) {
|
|
58
76
|
const root = cwd ?? process.cwd();
|
|
59
77
|
const raw = stackOverride ?? readStack(root);
|
|
60
78
|
const stack = raw?.toLowerCase() ?? 'unknown';
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
const segments = stack.split(/[-+]/).filter(Boolean);
|
|
81
|
+
|
|
82
|
+
// Single-segment stack (the overwhelming common case): resolve exactly as
|
|
83
|
+
// before, byte-for-byte, including the 'unknown' → dotnet fallback.
|
|
84
|
+
if (segments.length <= 1) {
|
|
85
|
+
let profile;
|
|
86
|
+
if (NEXTJS_STACKS.has(stack)) {
|
|
87
|
+
profile = PROFILES.nextjs;
|
|
88
|
+
} else if (NO_VALIDATOR_STACKS.has(stack)) {
|
|
89
|
+
profile = PROFILES.empty;
|
|
90
|
+
} else if (DOTNET_STACKS.has(stack) || stack === 'unknown') {
|
|
91
|
+
profile = PROFILES.dotnet;
|
|
92
|
+
} else {
|
|
93
|
+
profile = PROFILES.empty;
|
|
94
|
+
}
|
|
95
|
+
return { stack, ...profile };
|
|
71
96
|
}
|
|
72
97
|
|
|
98
|
+
// Compound stack: resolve each recognized segment and merge.
|
|
99
|
+
const matched = [];
|
|
100
|
+
if (segments.some((s) => DOTNET_STACKS.has(s))) matched.push(PROFILES.dotnet);
|
|
101
|
+
if (segments.some((s) => NEXTJS_STACKS.has(s))) matched.push(PROFILES.nextjs);
|
|
102
|
+
|
|
103
|
+
const profile = matched.length > 0 ? mergeProfiles(matched) : PROFILES.empty;
|
|
73
104
|
return { stack, ...profile };
|
|
74
105
|
}
|
|
@@ -98,6 +98,27 @@ export async function parseTasksJson(filePath) {
|
|
|
98
98
|
return data;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Lenient variant for read-only analysis commands (e.g. `morph-spec dag`):
|
|
103
|
+
* a missing `version` field is defaulted to "1.0" instead of failing
|
|
104
|
+
* validation — legacy tasks.json predating the field is still analyzable.
|
|
105
|
+
* All other schema errors still throw exactly like `parseTasksJson`.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} filePath - Absolute path to tasks.json
|
|
108
|
+
* @returns {Promise<{ data: object, versionDefaulted: boolean }>}
|
|
109
|
+
*/
|
|
110
|
+
export async function parseTasksJsonLenient(filePath) {
|
|
111
|
+
const data = await fs.readJson(filePath);
|
|
112
|
+
const versionDefaulted = data.version === undefined;
|
|
113
|
+
if (versionDefaulted) data.version = '1.0';
|
|
114
|
+
|
|
115
|
+
const { valid, errors } = validateTasksJson(data);
|
|
116
|
+
if (!valid) {
|
|
117
|
+
throw new Error(`Invalid tasks.json:\n ${errors.join('\n ')}`);
|
|
118
|
+
}
|
|
119
|
+
return { data, versionDefaulted };
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
/**
|
|
102
123
|
* Synchronous variant — used by hooks.
|
|
103
124
|
*/
|
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
* Resolution order for test command:
|
|
5
5
|
* 1. .morph/config/config.json → project.testCommand
|
|
6
6
|
* 2. .sln or .csproj present at project root → "dotnet test"
|
|
7
|
-
* 3.
|
|
8
|
-
*
|
|
7
|
+
* 3. Nested *Tests.csproj anywhere in the tree (monorepo without a root
|
|
8
|
+
* .sln) → "dotnet test <proj> --nologo -v q" for each match
|
|
9
|
+
* 4. package.json present AND has scripts.test → "npm test"
|
|
10
|
+
* 5. null (no test suite detected)
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { execSync } from 'child_process';
|
|
12
14
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
13
15
|
import { join } from 'path';
|
|
16
|
+
import { globSync } from 'glob';
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Resolve the test command for a project.
|
|
@@ -46,7 +49,26 @@ export function resolveTestCommand(projectPath = '.') {
|
|
|
46
49
|
// Unreadable project directory — fall through to next detection
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
// 3.
|
|
52
|
+
// 3. No root-level project file — look for a nested test project
|
|
53
|
+
// (e.g. tests/MyApp.Tests/MyApp.Tests.csproj), common in monorepos that
|
|
54
|
+
// have no root .sln. Run each matched project explicitly rather than a
|
|
55
|
+
// bare `dotnet test` (which finds nothing to build at the root).
|
|
56
|
+
try {
|
|
57
|
+
const testProjects = globSync('**/*Tests.csproj', {
|
|
58
|
+
cwd: projectPath,
|
|
59
|
+
ignore: ['**/bin/**', '**/obj/**', '**/node_modules/**'],
|
|
60
|
+
});
|
|
61
|
+
if (testProjects.length > 0) {
|
|
62
|
+
return testProjects
|
|
63
|
+
.sort()
|
|
64
|
+
.map((p) => `dotnet test "${p}" --nologo -v q`)
|
|
65
|
+
.join(' && ');
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Glob failure — fall through to next detection
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. package.json present AND has scripts.test → npm test
|
|
50
72
|
const packageJsonPath = join(projectPath, 'package.json');
|
|
51
73
|
if (existsSync(packageJsonPath)) {
|
|
52
74
|
try {
|
|
@@ -59,7 +81,7 @@ export function resolveTestCommand(projectPath = '.') {
|
|
|
59
81
|
}
|
|
60
82
|
}
|
|
61
83
|
|
|
62
|
-
//
|
|
84
|
+
// 5. Nothing found
|
|
63
85
|
return null;
|
|
64
86
|
}
|
|
65
87
|
|
|
@@ -124,7 +124,10 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
124
124
|
const _profileForContracts = getStackProfile(projectPath);
|
|
125
125
|
const contractsType = resolveContractsOutputType(_profileForContracts.stack);
|
|
126
126
|
const contractsPath = getAbsoluteOutputPath(projectPath, featureName, contractsType);
|
|
127
|
-
|
|
127
|
+
// Positive check on isDotnet (contract-compliance is a VSA/.NET concern) —
|
|
128
|
+
// NOT `!isNextjs`, which used to wrongly suppress it for a compound
|
|
129
|
+
// "dotnet-nextjs" stack that has a .NET backend but is ALSO isNextjs.
|
|
130
|
+
if (existsSync(contractsPath) && _profileForContracts.isDotnet) {
|
|
128
131
|
validatorIds.push('contract-compliance');
|
|
129
132
|
}
|
|
130
133
|
}
|