@polymorphism-tech/morph-spec 8.20.0 → 8.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/morph-spec.js +2 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +1 -1
- package/framework/docs/CLI.md +2 -2
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +23 -2
- package/framework/hooks/claude-code/core/state-sync.js +5 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +34 -3
- package/framework/hooks/claude-code/core/validate-completion.js +116 -4
- package/framework/hooks/claude-code/core/worktree-context.js +9 -4
- package/framework/hooks/shared/telemetry-logger.js +64 -1
- package/framework/schemas/tasks.schema.json +1 -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 +6 -15
- package/framework/skills/core/morph-brainstorming/SKILL.md +2 -2
- 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 +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +4 -4
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +4 -3
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +29 -15
- package/framework/skills/core/morph-uiux/SKILL.md +3 -3
- package/package.json +1 -1
- package/src/commands/observability/cost.js +93 -5
- package/src/commands/project/update.js +2 -63
- package/src/commands/state/gate-check.js +10 -3
- package/src/core/templates/template-registry.js +2 -30
- package/src/lib/git/default-branch.js +44 -8
- package/src/lib/installers/templates-reconcile.js +81 -0
- package/src/lib/risk-detector.js +20 -1
- package/src/lib/telemetry/telemetry-reader.js +80 -4
- package/src/scripts/setup-infra.js +23 -5
|
@@ -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.
|
|
7
|
+
cliVersion: "8.21.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-review
|
|
@@ -98,7 +98,8 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
|
|
|
98
98
|
|
|
99
99
|
7. **NÃO finalize o trace manualmente.** O hook `trace-autogen` gera e finaliza `.morph/traces/YYYY-MM-DD-{feature}-implement.json` automaticamente (incl. `phase_summary` e `completed_at`) no último write de `tasks.json` — quando a última task sai de `pending`/`in_progress`. Se o trace parecer desatualizado, a causa é um `morph-spec score` faltante ou um recap sem o bloco da task — corrija a fonte, não o trace.
|
|
100
100
|
|
|
101
|
-
8. Apresente o Gate 3 segundo trust mode
|
|
101
|
+
8. Apresente o Gate 3 segundo trust mode — **única apresentação do gate**: `morph-apply` §5 aponta
|
|
102
|
+
para cá em vez de duplicar; ao devolver o controle a ela, o gate já está decidido.
|
|
102
103
|
|
|
103
104
|
Rode `morph-spec gate-check {feature} review [--json]` — lê `trust` de `.morph/config/config.json`, decide via `shouldPauseGate` e persiste `gateDecisions.review` com timestamp server-side, imprimindo `pause`/`reason`/`signals`. É o default — não recompute a tabela do MORPH.md §5 em linguagem natural.
|
|
104
105
|
|
|
@@ -109,7 +110,7 @@ e o `recap.md` estar atualizado. Normalmente invocada pela skill orquestradora `
|
|
|
109
110
|
|
|
110
111
|
Julgamento do revisor que nenhuma dessas chaves representa: use `gate-decision` direto em vez de `gate-check`.
|
|
111
112
|
|
|
112
|
-
**Nunca edite `feature.json` à mão** (colide com o hook state-sync). Os caminhos acima gravam no mesmo lugar; o hook `gate-guard.js` confere depois se a ação bateu com a decisão persistida.
|
|
113
|
+
**Nunca edite `feature.json` à mão** (colide com o hook state-sync — erro "file modified since read"). Os caminhos acima gravam no mesmo lugar; o hook `gate-guard.js` confere depois se a ação bateu com a decisão persistida.
|
|
113
114
|
|
|
114
115
|
**Se `pause === true`**, use `AskUserQuestion`:
|
|
115
116
|
|
|
@@ -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.
|
|
7
|
+
cliVersion: "8.21.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-stack-scan
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: morph-standards
|
|
3
|
-
description: Localiza
|
|
3
|
+
description: Localiza um standard específico via busca server-side (morph-spec standards --find) e carrega só o necessário
|
|
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.
|
|
7
|
+
cliVersion: "8.21.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-standards
|
|
11
11
|
|
|
12
12
|
## Objetivo
|
|
13
|
-
Encontrar o standard certo para a situação atual e carregar apenas
|
|
14
|
-
Nunca
|
|
13
|
+
Encontrar o standard certo para a situação atual e carregar apenas o necessário no contexto.
|
|
14
|
+
Nunca abra o `STANDARDS.json` inteiro (65 KB) — a busca server-side já devolve o suficiente
|
|
15
|
+
para aplicar o standard na maioria dos casos.
|
|
15
16
|
|
|
16
17
|
## Quando invocar
|
|
17
18
|
Quando durante a implementação surgir uma dúvida de padrão arquitetural, de contrato, de qualidade
|
|
@@ -19,21 +20,33 @@ ou de teste — e o mandate.md não for suficiente para resolver.
|
|
|
19
20
|
|
|
20
21
|
## O que fazer
|
|
21
22
|
|
|
22
|
-
1.
|
|
23
|
+
1. Rode a busca server-side — é O caminho, não uma opção entre várias:
|
|
24
|
+
```
|
|
25
|
+
morph-spec standards --find "<domínio/tecnologia da task>" [--category <cat>] [--tags <a,b>] [--json]
|
|
26
|
+
```
|
|
27
|
+
Devolve o **top 3** como `id · path · digest` — o `digest` é o texto completo do standard;
|
|
28
|
+
na maioria dos casos ele já basta para aplicar o padrão, sem abrir o `.md`.
|
|
23
29
|
|
|
24
|
-
2.
|
|
30
|
+
2. Se dois resultados parecerem relevantes e houver conflito, adote o mais específico ao stack
|
|
31
|
+
do projeto. Registre o conflito no `decisions.md` da feature.
|
|
25
32
|
|
|
26
|
-
3.
|
|
27
|
-
|
|
33
|
+
3. Abra o arquivo `.md` do standard (campo `path`) só quando o `digest` não bastar para resolver
|
|
34
|
+
a dúvida — ex.: precisa de um exemplo de código completo ou de uma seção que o resumo não cobre.
|
|
28
35
|
|
|
29
|
-
4.
|
|
30
|
-
Registre o conflito no `decisions.md` da feature.
|
|
36
|
+
4. Aplique o standard na tarefa atual.
|
|
31
37
|
|
|
32
|
-
5.
|
|
38
|
+
5. Registre no `recap.md`: qual standard foi consultado (via `--find`, ou navegação — item abaixo)
|
|
39
|
+
e como foi aplicado.
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
## Fallback — navegação exploratória
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
`--find` exige uma query. Quando a necessidade é genuinamente exploratória (ex.: "o que existe
|
|
44
|
+
pra categoria X?", sem um termo de busca claro), nesta ordem:
|
|
45
|
+
|
|
46
|
+
1. `morph-spec standards` sem `--find` — lista categorias + contagem, custo mínimo, o suficiente
|
|
47
|
+
pra decidir qual `--category` usar num `--find` em seguida.
|
|
48
|
+
2. Só se isso não bastar, abra `framework/standards/STANDARDS.json` inteiro — último recurso,
|
|
49
|
+
nunca o primeiro passo.
|
|
37
50
|
|
|
38
51
|
## Quando não há standard adequado
|
|
39
52
|
Registre no `decisions.md` que nenhum standard cobre o caso.
|
|
@@ -41,10 +54,11 @@ Descreva o critério adotado e sinalize para revisão futura.
|
|
|
41
54
|
Não invente um standard — padrões não documentados são dívida técnica.
|
|
42
55
|
|
|
43
56
|
## Output esperado
|
|
44
|
-
- Standard identificado
|
|
57
|
+
- Standard identificado (via `--find`/digest, ou `.md` completo quando necessário) no contexto
|
|
45
58
|
- Critério de implementação claro para a tarefa atual
|
|
46
59
|
- Entrada no recap.md registrando o standard consultado
|
|
47
60
|
|
|
48
61
|
## Referências
|
|
49
|
-
- `
|
|
62
|
+
- `morph-spec standards --find "<query>" [--category] [--tags] [--json]` — busca server-side, top 3 `id · path · digest`
|
|
63
|
+
- `framework/standards/STANDARDS.json` — índice completo; abrir só como fallback exploratório (ver acima)
|
|
50
64
|
- `framework/MORPH.md` — seção 4 (Como consultar Standards)
|
|
@@ -4,7 +4,7 @@ description: Conduz a fase opcional de UI/UX — design system, mockups, compone
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: ao tomar o ramo uiux (morph-spec advance --design), depois do Gate 1 e antes do morph-plan
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "8.
|
|
7
|
+
cliVersion: "8.21.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-uiux
|
|
@@ -33,7 +33,7 @@ Ao tomar o ramo opcional uiux: feature/bug cuja mudança cria ou altera superfí
|
|
|
33
33
|
|
|
34
34
|
3. **Despache `morph-ui-designer`** para o trabalho visual — dispatch é o default aqui (domínio diferente do Orchestrator, MORPH.md §6). `morph-ui-designer` tem `Read/Write/Edit/Glob/Grep` + MCPs `stitch`/`gemini-image`/`playwright`/`context7`, mas **não tem Bash** — não peça build, testes nem `morph-spec` a ele; isso é do orquestrador.
|
|
35
35
|
|
|
36
|
-
**
|
|
36
|
+
**Persona universal** (`ui-designer.stack = ["*"]` em `agents.json`, mesmo mecanismo de `evaluator`/`scout`): UI/UX importa sempre que há superfície de UI, então o subagent está registrado em **todo** projeto — .NET/Blazor incluído, não só Next.js. **Fallback** (caso raro: instalação anterior a essa mudança, `.claude/agents/morph-ui-designer.md` ainda não regenerado): `general-purpose` colando o campo `persona` (Read em `.morph/framework/agents.json`) no prompt, registrando o fallback no relatório — resolvido rodando `morph-spec setup-infra --reinstall`.
|
|
37
37
|
|
|
38
38
|
Template do prompt (contrato do relatório em forma curta — copia o padrão de `morph-implement` §4, sem a cláusula de build/isolamento, que não se aplica a um agente sem Bash):
|
|
39
39
|
```
|
|
@@ -93,7 +93,7 @@ Ao tomar o ramo opcional uiux: feature/bug cuja mudança cria ou altera superfí
|
|
|
93
93
|
|
|
94
94
|
Aguardar resposta. Se "preciso ajustar", aplicar e reapresentar.
|
|
95
95
|
|
|
96
|
-
**
|
|
96
|
+
**Hook state-sync reconhece este gate, como os demais.** Igual a Gate 1/2/3, o hook `state-sync.js` (`gateFromQuestion`) reconhece o texto "Gate uiux" — uma resposta "aprovado" ao template acima grava sozinha `approvalGates.uiux = { approved: true, timestamp, approvedBy: "manual" }` no `feature.json` autoritativo, atribuído pelo nome da feature que a pergunta cita (MORPH.md §5). Sem nome na pergunta, ou fora de `AskUserQuestion` (headless/CI): aprove explicitamente:
|
|
97
97
|
```
|
|
98
98
|
morph-spec approve {feature} uiux --approver <quem respondeu>
|
|
99
99
|
# ou, para já aprovar e criar 2-plan/ num passo só:
|
package/package.json
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `morph-spec cost [feature] [--by task|phase] [--json]`
|
|
2
|
+
* `morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`
|
|
3
3
|
*
|
|
4
4
|
* A cost-facing view over the same harness event stream `telemetry` reads
|
|
5
5
|
* (.morph/logs/events.jsonl): how much wall-clock time was spent inside
|
|
6
6
|
* measured tasks (TaskStart→TaskEnd), how many context bytes were
|
|
7
7
|
* re-ingested (grouped by kind: rubric/standard/skill/agents/plan-artifact/
|
|
8
8
|
* code), and how many sub-agent dispatches happened (with total prompt
|
|
9
|
-
* chars).
|
|
9
|
+
* chars). When the stream has `SessionUsage` events (real, hook-observed
|
|
10
|
+
* token counts), those are summed and shown too — see the sessionUsage
|
|
11
|
+
* caveat below.
|
|
10
12
|
*
|
|
11
13
|
* IMPORTANT: bytes re-ingested is a DETERMINISTIC PROXY for cost, not a
|
|
12
14
|
* token count. Hooks only see what tools read off disk — never the model's
|
|
13
15
|
* actual token usage — so this measures re-ingestion pressure, not spend.
|
|
16
|
+
* `sessionUsage` (when present) is the exception: real tokens, observed
|
|
17
|
+
* directly, not a proxy — see aggregateCost's docstring for the contract.
|
|
18
|
+
*
|
|
19
|
+
* `--all-trees` (Furo 3) widens the source from "just the current tree" to
|
|
20
|
+
* every git worktree that has its own events.jsonl — a fleet running N
|
|
21
|
+
* features in N worktrees otherwise only ever sees whichever tree `cost` was
|
|
22
|
+
* run from. Reuses the same worktree enumeration `doctor --worktrees`/`fleet`
|
|
23
|
+
* use (see loadTelemetryAllTrees in telemetry-reader.js).
|
|
14
24
|
*
|
|
15
25
|
* Read-only, always exits 0 on a normal (even empty) run — fail-open, an
|
|
16
26
|
* empty stream is an answer, not an error. Only `--by` with an unknown value
|
|
@@ -18,7 +28,7 @@
|
|
|
18
28
|
*/
|
|
19
29
|
|
|
20
30
|
import chalk from 'chalk';
|
|
21
|
-
import { loadTelemetry, aggregateCost } from '../../lib/telemetry/telemetry-reader.js';
|
|
31
|
+
import { loadTelemetry, loadTelemetryAllTrees, aggregateCost } from '../../lib/telemetry/telemetry-reader.js';
|
|
22
32
|
|
|
23
33
|
const BYTES_CAVEAT =
|
|
24
34
|
'Bytes re-ingeridos são um PROXY determinístico de custo (o que as ferramentas leram do disco) — NÃO são tokens reais. Hooks não enxergam o uso real de tokens do modelo.';
|
|
@@ -77,6 +87,19 @@ function printPhases(phases) {
|
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Real (hook-observed) token counts, when the stream has `SessionUsage`
|
|
92
|
+
* events — the non-proxy sibling of the bytes-re-ingested numbers above.
|
|
93
|
+
* No-op when absent (aggregateCost returns `null`, never a zeroed object).
|
|
94
|
+
*/
|
|
95
|
+
function printSessionUsage(sessionUsage) {
|
|
96
|
+
if (!sessionUsage) return;
|
|
97
|
+
console.log('\n' + chalk.white.bold('Tokens reais (sessões observadas)')
|
|
98
|
+
+ chalk.gray(` · ${sessionUsage.sessions} SessionUsage`));
|
|
99
|
+
console.log(` input=${sessionUsage.inputTokens} output=${sessionUsage.outputTokens} `
|
|
100
|
+
+ `cacheRead=${sessionUsage.cacheReadTokens} cacheCreation=${sessionUsage.cacheCreationTokens}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
80
103
|
function printSummary(summary) {
|
|
81
104
|
console.log(`\n duração dentro de tasks medidas: ${fmtDuration(summary.taskDurationSeconds)}`);
|
|
82
105
|
console.log(` dispatches: ${summary.dispatches.count} (${summary.dispatches.promptChars} chars de prompt)`);
|
|
@@ -88,11 +111,67 @@ function printSummary(summary) {
|
|
|
88
111
|
console.log('\n' + chalk.white.bold('Mix de tools'));
|
|
89
112
|
for (const t of summary.tools) console.log(` ${t.name.padEnd(16)} ${t.count}`);
|
|
90
113
|
}
|
|
114
|
+
|
|
115
|
+
printSessionUsage(summary.sessionUsage);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `--all-trees` rendering: a short line per tree (so a fleet operator can
|
|
120
|
+
* spot which tree is expensive) followed by the COMBINED aggregate — full
|
|
121
|
+
* detail (task/phase breakdown honors `--by` here too, same as the
|
|
122
|
+
* single-tree path; a typeable flag combination is never a silent no-op).
|
|
123
|
+
*
|
|
124
|
+
* @param {string} [feature]
|
|
125
|
+
* @param {{ json?: boolean, by?: 'task'|'phase' }} options
|
|
126
|
+
* @param {string} cwd
|
|
127
|
+
*/
|
|
128
|
+
function printAllTrees(feature, options, cwd) {
|
|
129
|
+
const treesData = loadTelemetryAllTrees(cwd);
|
|
130
|
+
const perTree = treesData.map(({ tree, path, events }) => ({
|
|
131
|
+
tree,
|
|
132
|
+
path,
|
|
133
|
+
...aggregateCost(events, { feature }),
|
|
134
|
+
}));
|
|
135
|
+
const combined = aggregateCost(treesData.flatMap((t) => t.events), { feature });
|
|
136
|
+
|
|
137
|
+
if (options.json) {
|
|
138
|
+
console.log(JSON.stringify({ allTrees: true, trees: perTree, combined }));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (combined.total === 0) {
|
|
143
|
+
const scope = feature ? ` para a feature "${feature}"` : '';
|
|
144
|
+
console.log(chalk.gray(`Telemetria vazia${scope} em todas as árvores — nenhuma tem eventos capturados ainda.`));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(chalk.white.bold(
|
|
149
|
+
`Custo do harness (proxy determinística) — todas as árvores (${perTree.length} com eventos, ${combined.total} eventos no total)`
|
|
150
|
+
));
|
|
151
|
+
console.log(chalk.gray(`⚠ ${BYTES_CAVEAT}`));
|
|
152
|
+
|
|
153
|
+
for (const t of perTree) {
|
|
154
|
+
console.log('\n' + chalk.cyan.bold(`── Árvore: ${t.tree}`) + chalk.gray(` — ${t.path}`));
|
|
155
|
+
console.log(` ${t.total} eventos · duração em tasks: ${fmtDuration(t.taskDurationSeconds)} · `
|
|
156
|
+
+ `bytes: ${fmtBytes(t.bytesTotal)} · dispatches: ${t.dispatches.count}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log('\n' + chalk.white.bold('── Total agregado (todas as árvores)'));
|
|
160
|
+
if (combined.features.length) {
|
|
161
|
+
console.log(chalk.gray(`Features: ${combined.features.join(', ')}`));
|
|
162
|
+
}
|
|
163
|
+
if (options.by === 'task') {
|
|
164
|
+
printTasks(combined.tasks);
|
|
165
|
+
} else if (options.by === 'phase') {
|
|
166
|
+
printPhases(combined.phases);
|
|
167
|
+
} else {
|
|
168
|
+
printSummary(combined);
|
|
169
|
+
}
|
|
91
170
|
}
|
|
92
171
|
|
|
93
172
|
/**
|
|
94
173
|
* @param {string} [feature] - narrow to a single feature
|
|
95
|
-
* @param {{ json?: boolean, by?: 'task'|'phase', cwd?: string }} [options]
|
|
174
|
+
* @param {{ json?: boolean, by?: 'task'|'phase', allTrees?: boolean, cwd?: string }} [options]
|
|
96
175
|
*/
|
|
97
176
|
export function costCommand(feature, options = {}) {
|
|
98
177
|
if (options.by && !BY_MODES.includes(options.by)) {
|
|
@@ -102,7 +181,16 @@ export function costCommand(feature, options = {}) {
|
|
|
102
181
|
return;
|
|
103
182
|
}
|
|
104
183
|
|
|
105
|
-
const
|
|
184
|
+
const cwd = options.cwd || process.cwd();
|
|
185
|
+
|
|
186
|
+
if (options.allTrees) {
|
|
187
|
+
// Same fail-open + Windows-flush discipline as the single-tree path
|
|
188
|
+
// below — no forced process.exit(), --json included.
|
|
189
|
+
printAllTrees(feature, options, cwd);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const events = loadTelemetry(cwd);
|
|
106
194
|
const summary = aggregateCost(events, { feature });
|
|
107
195
|
|
|
108
196
|
if (options.json) {
|
|
@@ -24,8 +24,7 @@ import {
|
|
|
24
24
|
checkProjectOutdated,
|
|
25
25
|
saveProjectMorphVersion,
|
|
26
26
|
getUpdateInstructions,
|
|
27
|
-
detectInstallMethod
|
|
28
|
-
compareVersions
|
|
27
|
+
detectInstallMethod
|
|
29
28
|
} from '../../utils/version-checker.js';
|
|
30
29
|
import { installClaudeHooks, installGlobalStatusline } from '../../utils/claude-settings-manager.js';
|
|
31
30
|
import { installSkills } from '../../utils/skills-installer.js';
|
|
@@ -33,6 +32,7 @@ import { installAgents, installDomainAgents } from '../../utils/agents-installer
|
|
|
33
32
|
import { injectMorphImport } from '../../utils/claude-md-injector.js';
|
|
34
33
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
35
34
|
import { updateClaudeMdAgentsBlock, installRulesForStack } from '../../scripts/setup-infra.js';
|
|
35
|
+
import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Backup user's config.json before cleaning
|
|
@@ -89,67 +89,6 @@ async function cleanFrameworkDirs(morphPath, targetPath) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/**
|
|
93
|
-
* Last CLI version whose `update`/`setup-infra` still wrote
|
|
94
|
-
* `.morph/framework/templates/` unconditionally, before Task 2 of the R3
|
|
95
|
-
* perf plan removed that copy. Used as the decision boundary in
|
|
96
|
-
* `reconcileLegacyTemplatesCopy` below — see its doc comment for why a
|
|
97
|
-
* version check is used instead of diffing file content.
|
|
98
|
-
*/
|
|
99
|
-
const LAST_VERSION_THAT_COPIED_TEMPLATES = '8.17.0';
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Reconcile a pre-existing `.morph/framework/templates/` copy now that
|
|
103
|
-
* `update` no longer writes it (Task 2, R3 perf plan — resolution falls
|
|
104
|
-
* back to the npm package's own framework/templates/, see
|
|
105
|
-
* template-registry.js / architecture-scanner.js's layered resolver).
|
|
106
|
-
*
|
|
107
|
-
* Decided from the project's PRIOR recorded CLI version (before THIS
|
|
108
|
-
* update writes a new one), not by diffing content against the package.
|
|
109
|
-
* A content diff can't tell "stale copy from an older CLI version" apart
|
|
110
|
-
* from "deliberate user override" — and staleness is the dominant case for
|
|
111
|
-
* anyone who actually runs `update`, since template content does change
|
|
112
|
-
* between releases. The version boundary is unambiguous instead:
|
|
113
|
-
*
|
|
114
|
-
* - No local copy → nothing to do.
|
|
115
|
-
* - Prior version is unrecorded, OR <= LAST_VERSION_THAT_COPIED_TEMPLATES
|
|
116
|
-
* → the copy could ONLY have been put there by the OLD installer:
|
|
117
|
-
* `.morph/framework/**` is Edit-denied to the agent (MORPH_DENY in
|
|
118
|
-
* hooks-installer.js — "installer-managed, immutable at runtime"),
|
|
119
|
-
* the directory is gitignored, and every `update` before this fix
|
|
120
|
-
* unconditionally wiped + recopied it. There is no path by which a
|
|
121
|
-
* project on an old version could have a genuine override sitting
|
|
122
|
-
* here. Safe to delete unconditionally — it is dead weight, not data.
|
|
123
|
-
* - Prior version > LAST_VERSION_THAT_COPIED_TEMPLATES → `update` and
|
|
124
|
-
* `setup-infra` have never written this directory for this project.
|
|
125
|
-
* Anything found here was placed deliberately (by the user or other
|
|
126
|
-
* tooling) — this is the forward-looking override use case the
|
|
127
|
-
* layered resolver exists for. Preserve untouched, unconditionally,
|
|
128
|
-
* with no diffing.
|
|
129
|
-
*
|
|
130
|
-
* @param {string} templatesDest - .morph/framework/templates (may not exist)
|
|
131
|
-
* @param {string|null} priorProjectVersion - project's recorded morph-spec
|
|
132
|
-
* version BEFORE this update run (checkProjectOutdated().current)
|
|
133
|
-
* @returns {'absent'|'removed-legacy'|'preserved-override'}
|
|
134
|
-
*/
|
|
135
|
-
export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion) {
|
|
136
|
-
if (!existsSync(templatesDest)) return 'absent';
|
|
137
|
-
|
|
138
|
-
const isLegacy = !priorProjectVersion
|
|
139
|
-
|| compareVersions(priorProjectVersion, LAST_VERSION_THAT_COPIED_TEMPLATES) <= 0;
|
|
140
|
-
|
|
141
|
-
if (!isLegacy) return 'preserved-override';
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
fs.removeSync(templatesDest);
|
|
145
|
-
return 'removed-legacy';
|
|
146
|
-
} catch {
|
|
147
|
-
// Couldn't remove — don't risk a half-broken state; leave it and let
|
|
148
|
-
// the layered resolver's override-first check pick it up as before.
|
|
149
|
-
return 'preserved-override';
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
92
|
/**
|
|
154
93
|
* Restore user's config.json after reinstallation
|
|
155
94
|
* @param {string} morphPath - Path to .morph directory
|
|
@@ -87,9 +87,12 @@ function evaluatorCompositeScore(cwd, featureName) {
|
|
|
87
87
|
* never had (it shipped fully tested with zero callers; see the R4 report).
|
|
88
88
|
*
|
|
89
89
|
* `verificationStatus` feeds a real pause rule (verify vermelho pausa —
|
|
90
|
-
* política adicionada em risk-detector.js no fecho do R4)
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* política adicionada em risk-detector.js no fecho do R4, qualquer gate).
|
|
91
|
+
* `verificationStale` também alimenta uma regra própria, mas só no ship
|
|
92
|
+
* (`gate === 'review'`) — sha do stamp divergindo do HEAD atual. `gate` em si
|
|
93
|
+
* é o que permite risk-detector.js diferenciar as duas. `verification`
|
|
94
|
+
* completo, diffstat e sha seguem como contexto de auditoria persistido, sem
|
|
95
|
+
* regra própria.
|
|
93
96
|
*
|
|
94
97
|
* @param {string} featureName
|
|
95
98
|
* @param {object} feature
|
|
@@ -104,6 +107,10 @@ function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
104
107
|
const verification = feature.verification || null;
|
|
105
108
|
|
|
106
109
|
const signals = {
|
|
110
|
+
// Furo 2: disk-derived, never in OVERRIDABLE_SIGNAL_KEYS — a caller
|
|
111
|
+
// cannot spoof which gate risk-detector.js thinks it is deciding, same
|
|
112
|
+
// anti-spoof rationale as workType (hotfix's forced pause).
|
|
113
|
+
gate: gateName,
|
|
107
114
|
workType: feature.workType || 'feature',
|
|
108
115
|
taskCount: tasks.length,
|
|
109
116
|
standardsViolations: [],
|
|
@@ -171,34 +171,6 @@ export function searchTemplates(keyword, projectPath = null) {
|
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
/**
|
|
175
|
-
* Get template absolute path with fallback logic.
|
|
176
|
-
*
|
|
177
|
-
* Resolution order:
|
|
178
|
-
* 1. Project-local override (.morph/framework/templates/{path})
|
|
179
|
-
* 2. Framework universal template (framework/templates/{path})
|
|
180
|
-
*
|
|
181
|
-
* @param {string} templateId - Template identifier
|
|
182
|
-
* @param {string} projectPath - Project root path
|
|
183
|
-
* @returns {string|null} Absolute template path or null if not found
|
|
184
|
-
*/
|
|
185
|
-
export function resolveTemplatePathById(templateId, projectPath) {
|
|
186
|
-
const template = getTemplateById(templateId, projectPath);
|
|
187
|
-
if (!template) {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// 1. Project-local override
|
|
192
|
-
const projectLocalPath = join(projectPath, '.morph/framework/templates', template.path);
|
|
193
|
-
if (existsSync(projectLocalPath)) {
|
|
194
|
-
return projectLocalPath;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 2. Framework universal template
|
|
198
|
-
const frameworkPath = join(projectPath, 'framework/templates', template.path);
|
|
199
|
-
return existsSync(frameworkPath) ? frameworkPath : null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
174
|
/** Absolute path of this repo's own framework/templates/ directory. */
|
|
203
175
|
const FRAMEWORK_TEMPLATES_DIR = join(__dirname, '../../../framework/templates');
|
|
204
176
|
|
|
@@ -206,8 +178,8 @@ const FRAMEWORK_TEMPLATES_DIR = join(__dirname, '../../../framework/templates');
|
|
|
206
178
|
* Resolve a template id to its absolute path inside THIS framework repo's
|
|
207
179
|
* `framework/templates/` directory — for generators running against the
|
|
208
180
|
* framework itself (scaffold-generator, context-generator), not against an
|
|
209
|
-
* installed `.morph/`. Use
|
|
210
|
-
* its project-local overrides are in play.
|
|
181
|
+
* installed `.morph/`. Use architecture-scanner.js's `resolveTemplatePath()`
|
|
182
|
+
* when a project root and its project-local overrides are in play.
|
|
211
183
|
*
|
|
212
184
|
* @param {string} templateId - Template identifier (REGISTRY.json `id`)
|
|
213
185
|
* @returns {string} Absolute path to the template file
|
|
@@ -112,26 +112,62 @@ function parseShortstat(text) {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Count brand-new UNTRACKED files (never `git add`-ed) — the gap plain
|
|
117
|
+
* `git diff --shortstat` never sees, tracked-edits-only by design. Fail-open:
|
|
118
|
+
* any error (not a repo, git missing) reads as 0 untracked, never throws —
|
|
119
|
+
* this must not take down the whole `getDiffStat` result over a probe that
|
|
120
|
+
* is secondary to the diff itself.
|
|
121
|
+
*
|
|
122
|
+
* `--exclude-standard` matches gitignore the same way `git status` does, so
|
|
123
|
+
* scratch/build output never inflates the count.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} cwd
|
|
126
|
+
* @returns {number}
|
|
127
|
+
*/
|
|
128
|
+
function countUntrackedFiles(cwd) {
|
|
129
|
+
try {
|
|
130
|
+
const out = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], { cwd, stdio: 'pipe' })
|
|
131
|
+
.toString();
|
|
132
|
+
return out.split('\n').filter((l) => l.trim()).length;
|
|
133
|
+
} catch {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
/**
|
|
116
139
|
* `git diff --shortstat <baseRef>` against the CURRENT WORKING TREE (tracked
|
|
117
140
|
* edits — staged, unstaged, and everything committed since baseRef —
|
|
118
141
|
* deliberately not `baseRef...HEAD`, so a gate-check mid-implement still sees
|
|
119
|
-
* uncommitted work)
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
142
|
+
* uncommitted work), PLUS a count of brand-new UNTRACKED files (never
|
|
143
|
+
* `git add`-ed) that plain `git diff` never shows regardless of commit vs
|
|
144
|
+
* working tree. `filesChanged` is the combined total (tracked-changed +
|
|
145
|
+
* untracked) so a caller reading just that one number gets the true scope of
|
|
146
|
+
* what changed; `untrackedFiles` is broken out separately so a caller that
|
|
147
|
+
* cares CAN tell diff-vs-new apart instead of the two being silently fused.
|
|
148
|
+
*
|
|
149
|
+
* Both git calls run with the same `cwd` — every current caller passes a
|
|
150
|
+
* feature/worktree root there, so `git diff`'s repo-wide default and
|
|
151
|
+
* `git ls-files`'s cwd-relative listing agree; a caller passing a
|
|
152
|
+
* sub-directory as `cwd` would see `untrackedFiles` scoped to that
|
|
153
|
+
* sub-directory while `insertions`/`deletions` stay repo-wide — not a
|
|
154
|
+
* concern today, but worth knowing if that assumption ever changes.
|
|
155
|
+
*
|
|
156
|
+
* Fail-open: null when `baseRef` is falsy, cwd isn't a git repo, or the diff
|
|
157
|
+
* itself fails (e.g. baseRef doesn't resolve). The untracked probe fails open
|
|
158
|
+
* on its own (see countUntrackedFiles) and never turns a good diff into null.
|
|
125
159
|
*
|
|
126
160
|
* @param {string|null} baseRef - typically `detectDefaultBranch(cwd)`
|
|
127
161
|
* @param {string} [cwd]
|
|
128
|
-
* @returns {{ filesChanged: number, insertions: number, deletions: number }|null}
|
|
162
|
+
* @returns {{ filesChanged: number, insertions: number, deletions: number, untrackedFiles: number }|null}
|
|
129
163
|
*/
|
|
130
164
|
export function getDiffStat(baseRef, cwd = process.cwd()) {
|
|
131
165
|
if (!baseRef) return null;
|
|
132
166
|
try {
|
|
133
167
|
const out = execFileSync('git', ['diff', '--shortstat', baseRef], { cwd, stdio: 'pipe' }).toString().trim();
|
|
134
|
-
|
|
168
|
+
const stat = parseShortstat(out);
|
|
169
|
+
const untrackedFiles = countUntrackedFiles(cwd);
|
|
170
|
+
return { ...stat, filesChanged: stat.filesChanged + untrackedFiles, untrackedFiles };
|
|
135
171
|
} catch {
|
|
136
172
|
return null;
|
|
137
173
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* templates-reconcile.js — reconcile a pre-existing `.morph/framework/templates/`
|
|
3
|
+
* copy against the project's prior recorded morph-spec version.
|
|
4
|
+
*
|
|
5
|
+
* Shared by BOTH install paths that can encounter this directory:
|
|
6
|
+
* - `morph-spec update` (src/commands/project/update.js) — always knows the
|
|
7
|
+
* project's prior version (`checkProjectOutdated().current`, read from
|
|
8
|
+
* `.morph/.morphversion` BEFORE this run overwrites it).
|
|
9
|
+
* - `morph-spec init`/`setup-infra` (src/scripts/setup-infra.js) — the SAME
|
|
10
|
+
* source (`getProjectMorphVersion()`, the function `checkProjectOutdated`
|
|
11
|
+
* itself calls), read at the same point in the run (before its own write
|
|
12
|
+
* of `.morph/.morphversion`), when it's available — e.g. a repair run
|
|
13
|
+
* over an already-initialized project. Falls back to a plain warning when
|
|
14
|
+
* it isn't (a project with no recorded version has nothing here for this
|
|
15
|
+
* function to reconcile against safely).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync } from 'fs';
|
|
19
|
+
import fs from 'fs-extra';
|
|
20
|
+
import { compareVersions } from '../../utils/version-checker.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Last CLI version whose `update`/`setup-infra` still wrote
|
|
24
|
+
* `.morph/framework/templates/` unconditionally, before Task 2 of the R3
|
|
25
|
+
* perf plan removed that copy. Used as the decision boundary in
|
|
26
|
+
* `reconcileLegacyTemplatesCopy` below — see its doc comment for why a
|
|
27
|
+
* version check is used instead of diffing file content.
|
|
28
|
+
*/
|
|
29
|
+
export const LAST_VERSION_THAT_COPIED_TEMPLATES = '8.17.0';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reconcile a pre-existing `.morph/framework/templates/` copy now that
|
|
33
|
+
* neither install path unconditionally writes it anymore (Task 2, R3 perf
|
|
34
|
+
* plan — resolution falls back to the npm package's own framework/templates/,
|
|
35
|
+
* see template-registry.js / architecture-scanner.js's layered resolver).
|
|
36
|
+
*
|
|
37
|
+
* Decided from the project's PRIOR recorded CLI version (before THIS run
|
|
38
|
+
* writes a new one), not by diffing content against the package. A content
|
|
39
|
+
* diff can't tell "stale copy from an older CLI version" apart from
|
|
40
|
+
* "deliberate user override" — and staleness is the dominant case for
|
|
41
|
+
* anyone who actually runs `update`/`setup-infra` again, since template
|
|
42
|
+
* content does change between releases. The version boundary is unambiguous
|
|
43
|
+
* instead:
|
|
44
|
+
*
|
|
45
|
+
* - No local copy → nothing to do.
|
|
46
|
+
* - Prior version is unrecorded, OR <= LAST_VERSION_THAT_COPIED_TEMPLATES
|
|
47
|
+
* → the copy could ONLY have been put there by the OLD installer:
|
|
48
|
+
* `.morph/framework/**` is Edit-denied to the agent (MORPH_DENY in
|
|
49
|
+
* hooks-installer.js — "installer-managed, immutable at runtime"),
|
|
50
|
+
* the directory is gitignored, and every install/update before this fix
|
|
51
|
+
* unconditionally wiped + recopied it. There is no path by which a
|
|
52
|
+
* project on an old version could have a genuine override sitting
|
|
53
|
+
* here. Safe to delete unconditionally — it is dead weight, not data.
|
|
54
|
+
* - Prior version > LAST_VERSION_THAT_COPIED_TEMPLATES → this install path
|
|
55
|
+
* has never written this directory for this project. Anything found
|
|
56
|
+
* here was placed deliberately (by the user or other tooling) — this is
|
|
57
|
+
* the forward-looking override use case the layered resolver exists
|
|
58
|
+
* for. Preserve untouched, unconditionally, with no diffing.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} templatesDest - .morph/framework/templates (may not exist)
|
|
61
|
+
* @param {string|null} priorProjectVersion - project's recorded morph-spec
|
|
62
|
+
* version BEFORE this run (e.g. `checkProjectOutdated().current`)
|
|
63
|
+
* @returns {'absent'|'removed-legacy'|'preserved-override'}
|
|
64
|
+
*/
|
|
65
|
+
export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion) {
|
|
66
|
+
if (!existsSync(templatesDest)) return 'absent';
|
|
67
|
+
|
|
68
|
+
const isLegacy = !priorProjectVersion
|
|
69
|
+
|| compareVersions(priorProjectVersion, LAST_VERSION_THAT_COPIED_TEMPLATES) <= 0;
|
|
70
|
+
|
|
71
|
+
if (!isLegacy) return 'preserved-override';
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
fs.removeSync(templatesDest);
|
|
75
|
+
return 'removed-legacy';
|
|
76
|
+
} catch {
|
|
77
|
+
// Couldn't remove — don't risk a half-broken state; leave it and let
|
|
78
|
+
// the layered resolver's override-first check pick it up as before.
|
|
79
|
+
return 'preserved-override';
|
|
80
|
+
}
|
|
81
|
+
}
|