@polymorphism-tech/morph-spec 8.37.1 → 8.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +19 -6
- package/README.md +2 -2
- package/bin/morph-spec.js +16 -7
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -3
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +76 -27
- package/framework/docs/CLI.md +54 -17
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/morph-workflow.md +1 -0
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +36 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +39 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +72 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +34 -9
- package/framework/standards/STANDARDS.json +187 -12
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +64 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/feature/decisions.md +124 -122
- package/package.json +1 -1
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +82 -10
- package/src/commands/project/update.js +25 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/scanner/agents-scanner.js +5 -1
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/evaluator-report.js +25 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +172 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/setup-infra.js +29 -18
- package/src/utils/agents-installer.js +281 -12
- package/src/utils/hooks-installer.js +75 -15
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`
|
|
2
|
+
* `morph-spec cost [feature] [--by task|phase] [--all-trees] [--metrics [--top n]] [--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
|
|
@@ -16,11 +16,14 @@
|
|
|
16
16
|
* `sessionUsage` (when present) is the exception: real tokens, observed
|
|
17
17
|
* directly, not a proxy — see aggregateCost's docstring for the contract.
|
|
18
18
|
*
|
|
19
|
-
* `--all-trees`
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* `--all-trees` widens the source from "just the current tree" to every git
|
|
20
|
+
* worktree that has its own events.jsonl plus the logs `finish` saved before
|
|
21
|
+
* removing a worktree, deduplicated by eventId.
|
|
22
|
+
*
|
|
23
|
+
* `--metrics` measures the loop instead: turns, tool calls by orchestrator and
|
|
24
|
+
* sub-agent, time per command class, identical reruns and why, rereads, how
|
|
25
|
+
* many feature-scope verifies each feature ran, git calls per task and whether
|
|
26
|
+
* the reuse validator ran in root and worktree (see lib/telemetry/metrics.js).
|
|
24
27
|
*
|
|
25
28
|
* Read-only, always exits 0 on a normal (even empty) run — fail-open, an
|
|
26
29
|
* empty stream is an answer, not an error. Only `--by` with an unknown value
|
|
@@ -28,7 +31,8 @@
|
|
|
28
31
|
*/
|
|
29
32
|
|
|
30
33
|
import chalk from 'chalk';
|
|
31
|
-
import { loadTelemetry, loadTelemetryAllTrees, aggregateCost } from '../../lib/telemetry/telemetry-reader.js';
|
|
34
|
+
import { loadTelemetry, loadTelemetryAllTrees, aggregateCost, dedupeEvents } from '../../lib/telemetry/telemetry-reader.js';
|
|
35
|
+
import { computeMetrics } from '../../lib/telemetry/metrics.js';
|
|
32
36
|
|
|
33
37
|
const BYTES_CAVEAT =
|
|
34
38
|
'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.';
|
|
@@ -87,17 +91,30 @@ function printPhases(phases) {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
function tokenLine(u) {
|
|
95
|
+
return ` input=${u.inputTokens} output=${u.outputTokens} `
|
|
96
|
+
+ `cacheRead=${u.cacheReadTokens} cacheCreation=${u.cacheCreationTokens}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
90
99
|
/**
|
|
91
|
-
* Real (hook-observed) token counts
|
|
92
|
-
*
|
|
93
|
-
* No-op when absent (aggregateCost returns `null`, never a zeroed object).
|
|
100
|
+
* Real (hook-observed) token counts — the non-proxy sibling of the bytes
|
|
101
|
+
* above: the main sessions and, separately, the sub-agents.
|
|
94
102
|
*/
|
|
95
|
-
function printSessionUsage(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
+ chalk.
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
function printSessionUsage(summary) {
|
|
104
|
+
const { sessionUsage, agentUsage, sessionUsageLegacySamples } = summary;
|
|
105
|
+
if (sessionUsage) {
|
|
106
|
+
console.log('\n' + chalk.white.bold('Tokens reais (sessões observadas)')
|
|
107
|
+
+ chalk.gray(` · ${sessionUsage.sessions} sessão(ões), ${sessionUsage.samples} amostra(s)`));
|
|
108
|
+
console.log(tokenLine(sessionUsage));
|
|
109
|
+
}
|
|
110
|
+
if (agentUsage) {
|
|
111
|
+
console.log('\n' + chalk.white.bold('Tokens reais (sub-agents)')
|
|
112
|
+
+ chalk.gray(` · ${agentUsage.agents} agente(s), ${agentUsage.samples} amostra(s)`));
|
|
113
|
+
console.log(tokenLine(agentUsage));
|
|
114
|
+
}
|
|
115
|
+
if (sessionUsageLegacySamples > 0) {
|
|
116
|
+
console.log(chalk.gray(` ${sessionUsageLegacySamples} amostra(s) SessionUsage antigas (sem delta) fora da soma: eram o total acumulado, repetido a cada Stop.`));
|
|
117
|
+
}
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
function printSummary(summary) {
|
|
@@ -112,7 +129,115 @@ function printSummary(summary) {
|
|
|
112
129
|
for (const t of summary.tools) console.log(` ${t.name.padEnd(16)} ${t.count}`);
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
printSessionUsage(summary
|
|
132
|
+
printSessionUsage(summary);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pct(r) {
|
|
136
|
+
return r === null || r === undefined ? '-' : `${Math.round(r * 1000) / 10}%`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function split(counts) {
|
|
140
|
+
return `${counts.total} (orq ${counts.orchestrator} · sub ${counts.subagent}${counts.unknown ? ` · sem agente ${counts.unknown}` : ''})`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function printMetricsBody(m, { by }) {
|
|
144
|
+
const legacyShare = m.window.events ? m.coverage.legacy / m.window.events : 0;
|
|
145
|
+
console.log(chalk.gray(`janela: ${m.window.from ?? '-'} → ${m.window.to ?? '-'} · ${m.window.events} eventos`
|
|
146
|
+
+ ` · ${pct(1 - legacyShare)} no esquema v2`));
|
|
147
|
+
if (m.coverage.legacy) {
|
|
148
|
+
console.log(chalk.gray(' eventos v1 não têm agente, exit code, duração nem janela de Read: entram como "sem agente" e rerun "unknown-*".'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('\n' + chalk.white.bold('Turnos e chamadas'));
|
|
152
|
+
console.log(` turnos: ${m.turns ? split(m.turns) : 'desconhecido (nenhum PostToolBatch na janela)'}`
|
|
153
|
+
+ (m.turnsPerTaskMedian !== null ? chalk.gray(` · mediana por task ${m.turnsPerTaskMedian}`) : ''));
|
|
154
|
+
console.log(` chamadas de ferramenta: ${split(m.toolCalls)}`);
|
|
155
|
+
const perTask = m.perTaskByFeature;
|
|
156
|
+
if (perTask.toolCalls !== null) {
|
|
157
|
+
console.log(` por task (total da feature ÷ tasks dela, mediana): ${perTask.toolCalls} chamadas`
|
|
158
|
+
+ (perTask.turns !== null ? ` · ${perTask.turns} turnos` : '') + ` · ${perTask.git} git status+diff`
|
|
159
|
+
+ chalk.gray(` · as janelas TaskStart→TaskEnd têm ${pct(m.taskWindowCoverage)} das chamadas`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log('\n' + chalk.white.bold('Tempo por classe de comando'));
|
|
163
|
+
for (const [klass, t] of Object.entries(m.timeByClass).sort((a, b) => b[1].measuredMs - a[1].measuredMs)) {
|
|
164
|
+
const unmeasured = t.unmeasured ? chalk.gray(` (${t.unmeasured} sem duração)`) : '';
|
|
165
|
+
console.log(` ${klass.padEnd(12)} ${fmtDuration(t.measuredMs / 1000).padStart(8)} ${t.runs} execução(ões)${unmeasured}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('\n' + chalk.white.bold('Rerun do mesmo comando em ≤10 min (mesma feature; o filtro de saída não conta)'));
|
|
169
|
+
console.log(` total: ${pct(m.reruns.rate)} repetidas (${m.reruns.reruns}/${m.reruns.runs}) · ${pct(m.reruns.burstRate)} em rajada — testes, builds e verifies`);
|
|
170
|
+
for (const [kind, r] of Object.entries(m.rerunsByKind).sort((a, b) => b[1].runs - a[1].runs)) {
|
|
171
|
+
console.log(` ${kind.padEnd(22)} ${pct(r.rate).padStart(6)} repetidas ${pct(r.burstRate).padStart(6)} em rajada (${r.runs} execuções)`);
|
|
172
|
+
}
|
|
173
|
+
const classes = Object.entries(m.rerunClasses);
|
|
174
|
+
if (classes.length) console.log(chalk.gray(` por quê: ${classes.map(([k, n]) => `${k} ${n}`).join(' · ')}`));
|
|
175
|
+
|
|
176
|
+
console.log('\n' + chalk.white.bold('Releitura de arquivo (mesma janela, mesma feature)'));
|
|
177
|
+
console.log(` ${pct(m.rereads.rate)} dos bytes lidos (${fmtBytes(m.rereads.redundantBytes)} de ${fmtBytes(m.rereads.bytes)}, ${m.rereads.reads} leituras)`);
|
|
178
|
+
if (m.rereads.windowed < m.rereads.reads) {
|
|
179
|
+
console.log(chalk.gray(` ${m.rereads.reads - m.rereads.windowed} leitura(s) sem janela gravada (v1) contam o arquivo inteiro: a taxa é um teto`));
|
|
180
|
+
}
|
|
181
|
+
for (const f of m.topRereads) {
|
|
182
|
+
console.log(` ${fmtBytes(f.redundantBytes).padStart(9)} ${f.reads}× ${f.file}${chalk.gray(` [${f.feature ?? '-'}]`)}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log('\n' + chalk.white.bold('verify, git e reuso'));
|
|
186
|
+
const perFeature = Object.entries(m.verify.byFeature).sort((a, b) => b[1].feature - a[1].feature);
|
|
187
|
+
console.log(` verify de feature por feature: mediana ${m.verify.featureScopeMedian ?? '-'}`
|
|
188
|
+
+ (perFeature.length ? chalk.gray(` (${perFeature.slice(0, 5).map(([f, v]) => `${f} ${v.feature}`).join(', ')})`) : ''));
|
|
189
|
+
const repeats = Object.entries(m.verify.unchangedRepeats);
|
|
190
|
+
if (repeats.length) {
|
|
191
|
+
console.log(` verify de feature sobre a mesma árvore do anterior: ${repeats.reduce((n, [, v]) => n + v, 0)}`
|
|
192
|
+
+ chalk.gray(` (${repeats.map(([f, n]) => `${f} ${n}`).join(', ')})`));
|
|
193
|
+
}
|
|
194
|
+
if (m.dotnetFullSuite.runs) {
|
|
195
|
+
const phases = Object.entries(m.dotnetFullSuite.byPhase).map(([p, v]) => `${p} ${v.withBuild}/${v.runs}`).join(' · ');
|
|
196
|
+
console.log(` suíte .NET inteira que recompila (sem --no-build): ${m.dotnetFullSuite.withBuild}/${m.dotnetFullSuite.runs}`
|
|
197
|
+
+ chalk.gray(` (${phases})`));
|
|
198
|
+
}
|
|
199
|
+
console.log(` git status / diff por task: mediana ${m.gitPerTaskMedian.status ?? '-'} / ${m.gitPerTaskMedian.diff ?? '-'}`
|
|
200
|
+
+ chalk.gray(` · total ${m.git.status} / ${m.git.diff}`));
|
|
201
|
+
const trees = Object.entries(m.reuse.byTree);
|
|
202
|
+
console.log(` reuse-validator: ${trees.length
|
|
203
|
+
? trees.map(([tree, s]) => `${tree} rodou ${s.ran}, pulou ${s.skipped}`).join(' · ')
|
|
204
|
+
: 'nenhum evento Verify na janela'}`);
|
|
205
|
+
for (const [reason, n] of Object.entries(m.reuse.skipReasons)) console.log(chalk.gray(` pulou ${n}×: ${reason}`));
|
|
206
|
+
|
|
207
|
+
if (by === 'task') {
|
|
208
|
+
for (const t of m.tasks) {
|
|
209
|
+
const status = t.open ? chalk.yellow('[em aberto]') : (t.status || '-');
|
|
210
|
+
console.log('\n' + chalk.white.bold(`Task ${t.task}`) + chalk.gray(` · ${t.feature ?? '(sem feature)'}`) + ` · ${status}`);
|
|
211
|
+
console.log(` turnos ${t.turns ? split(t.turns) : '-'} · chamadas ${split(t.toolCalls)}`);
|
|
212
|
+
const time = Object.entries(t.timeByClass).map(([k, v]) => `${k} ${fmtDuration(v.measuredMs / 1000)}`).join(' · ');
|
|
213
|
+
if (time) console.log(` tempo: ${time}`);
|
|
214
|
+
console.log(` rerun ${pct(t.reruns.rate)} (${t.reruns.reruns}/${t.reruns.runs}) · releitura ${pct(t.rereads.rate)} · git status/diff ${t.git.status}/${t.git.diff}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* `--metrics`: the loop measurements over the current tree (and, with
|
|
221
|
+
* `--all-trees`, every tree plus the finish backups, deduplicated).
|
|
222
|
+
*/
|
|
223
|
+
function printMetrics(feature, options, cwd) {
|
|
224
|
+
const events = options.allTrees
|
|
225
|
+
? dedupeEvents(loadTelemetryAllTrees(cwd).flatMap((t) => t.events))
|
|
226
|
+
: loadTelemetry(cwd);
|
|
227
|
+
const metrics = computeMetrics(events, { feature, top: options.top });
|
|
228
|
+
|
|
229
|
+
if (options.json) {
|
|
230
|
+
console.log(JSON.stringify(metrics));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (metrics.window.events === 0) {
|
|
234
|
+
const scope = feature ? ` para a feature "${feature}"` : '';
|
|
235
|
+
console.log(chalk.gray(`Telemetria vazia${scope} — nada a medir.`));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const scope = feature ? chalk.cyan(` · feature ${feature}`) : '';
|
|
239
|
+
console.log(chalk.white.bold(`Medições do loop${options.allTrees ? ' — todas as árvores + finish-backup' : ''}`) + scope);
|
|
240
|
+
printMetricsBody(metrics, options);
|
|
116
241
|
}
|
|
117
242
|
|
|
118
243
|
/**
|
|
@@ -132,7 +257,7 @@ function printAllTrees(feature, options, cwd) {
|
|
|
132
257
|
path,
|
|
133
258
|
...aggregateCost(events, { feature }),
|
|
134
259
|
}));
|
|
135
|
-
const combined = aggregateCost(treesData.flatMap((t) => t.events), { feature });
|
|
260
|
+
const combined = aggregateCost(dedupeEvents(treesData.flatMap((t) => t.events)), { feature });
|
|
136
261
|
|
|
137
262
|
if (options.json) {
|
|
138
263
|
console.log(JSON.stringify({ allTrees: true, trees: perTree, combined }));
|
|
@@ -183,6 +308,11 @@ export function costCommand(feature, options = {}) {
|
|
|
183
308
|
|
|
184
309
|
const cwd = options.cwd || process.cwd();
|
|
185
310
|
|
|
311
|
+
if (options.metrics) {
|
|
312
|
+
printMetrics(feature, options, cwd);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
186
316
|
if (options.allTrees) {
|
|
187
317
|
// Same fail-open + Windows-flush discipline as the single-tree path
|
|
188
318
|
// below — no forced process.exit(), --json included.
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
getInstalledCLIVersion
|
|
17
17
|
} from '../../utils/version-checker.js';
|
|
18
18
|
import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
|
|
19
|
-
import { checkMissingAgents, checkAgentsDrift } from '../../utils/agents-installer.js';
|
|
19
|
+
import { checkMissingAgents, checkAgentsDrift, maxTurnsError, surfacesError } from '../../utils/agents-installer.js';
|
|
20
20
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
21
21
|
import { auditGraph } from '../../lib/graph/graph-health.js';
|
|
22
22
|
import { auditFeatureLifecycle } from '../../core/state/lifecycle-audit.js';
|
|
@@ -191,6 +191,23 @@ export function validateAgentsJson(agentsData) {
|
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// 4: maxTurns (ADR-004) — the installer's own rule, so this cannot pass a
|
|
195
|
+
// value the installer refuses. ERROR, not warn: that persona is not generated.
|
|
196
|
+
const maxTurnsErrors = agentEntries
|
|
197
|
+
.map(([id, data]) => maxTurnsError(id, data?.maxTurns))
|
|
198
|
+
.filter(Boolean);
|
|
199
|
+
checks.push(maxTurnsErrors.length > 0
|
|
200
|
+
? { name: 'agents.json maxTurns', status: 'error', msg: maxTurnsErrors.join(' | ') }
|
|
201
|
+
: { name: 'agents.json maxTurns', status: 'ok' });
|
|
202
|
+
|
|
203
|
+
// 5: surfaces — same installer rule, same severity: the persona is not generated.
|
|
204
|
+
const surfacesErrors = agentEntries
|
|
205
|
+
.map(([id, data]) => surfacesError(id, data?.surfaces, data?.standards))
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
checks.push(surfacesErrors.length > 0
|
|
208
|
+
? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
|
|
209
|
+
: { name: 'agents.json surfaces', status: 'ok' });
|
|
210
|
+
|
|
194
211
|
if (missingV5Fields.length > 0 && isV5) {
|
|
195
212
|
const sample = missingV5Fields.slice(0, 3).join(', ');
|
|
196
213
|
checks.push({
|
|
@@ -348,12 +365,23 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
348
365
|
projectTags = getProjectTags(JSON.parse(rawCfg));
|
|
349
366
|
} catch { /* no tags */ }
|
|
350
367
|
}
|
|
351
|
-
|
|
368
|
+
// The MCP registry comes from the package, like the installer's: the
|
|
369
|
+
// installed .morph/framework/ has no mcp/, and rendering without it
|
|
370
|
+
// flagged every persona with an `mcp` grant as stale on a clean install.
|
|
371
|
+
const drift = checkAgentsDrift(frameworkRoot, join(frameworkRoot, '.morph/framework'), projectTags,
|
|
372
|
+
{ mcpRegistryDir: join(import.meta.dirname, '..', '..', '..', 'framework') });
|
|
373
|
+
if (drift.invalid.length > 0) {
|
|
374
|
+
check('Personas agents.json valid', false, false,
|
|
375
|
+
`${drift.invalid.map(i => i.reason).join(' | ')} — corrija o agents.json e rode 'morph-spec update'`);
|
|
376
|
+
}
|
|
352
377
|
check(`Personas installed (${drift.expected - drift.missing.length}/${drift.expected})`,
|
|
353
378
|
drift.missing.length === 0, false,
|
|
354
379
|
drift.missing.length > 0 ? `missing: ${drift.missing.join(', ')} — run 'morph-spec setup-infra --reinstall'` : '');
|
|
380
|
+
// Name WHAT differs per file (frontmatter key or body): a hand-edited
|
|
381
|
+
// maxTurns and a CLI upgrade that changed the body read differently.
|
|
382
|
+
const staleList = drift.stale.map(f => `${f} (${(drift.staleFields[f] ?? []).join(', ')})`);
|
|
355
383
|
check('Personas up-to-date (no drift)', drift.stale.length === 0, true,
|
|
356
|
-
drift.stale.length > 0 ? `stale/hand-edited: ${
|
|
384
|
+
drift.stale.length > 0 ? `stale/hand-edited: ${staleList.join(', ')} — regenerate via 'morph-spec setup-infra --reinstall'` : '');
|
|
357
385
|
check('No orphan morph-* agents', drift.orphans.length === 0, true,
|
|
358
386
|
drift.orphans.length > 0 ? `orphans: ${drift.orphans.join(', ')}` : '');
|
|
359
387
|
} catch {
|
|
@@ -653,23 +681,54 @@ async function doctorMcpCommand(targetPath) {
|
|
|
653
681
|
* Deleting a directory that still HOLDS something is reported with the exact
|
|
654
682
|
* command, never performed — same PROVE-or-DECLINE discipline as the finish
|
|
655
683
|
* teardown.
|
|
684
|
+
*
|
|
685
|
+
* `--prune` also applies the retention of the finish backups — the same policy
|
|
686
|
+
* every `finish` applies on its own, and the ONLY path that also applies it to
|
|
687
|
+
* the backups written before the retention existed (no manifest), counting
|
|
688
|
+
* them against the budget. Under `--json` its result rides in the JSON.
|
|
656
689
|
*/
|
|
657
690
|
export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
658
691
|
const { auditWorktrees, pruneWorktrees } = await import('../../lib/worktree/worktree-audit.js');
|
|
659
692
|
|
|
693
|
+
let backupRetention = null;
|
|
694
|
+
let prune = null;
|
|
660
695
|
if (options.prune) {
|
|
661
696
|
const beforeIssues = auditWorktrees(targetPath);
|
|
662
697
|
const beforeMissing = beforeIssues.filter((i) => i.type === 'registered-missing').length;
|
|
663
698
|
const beforeDangling = beforeIssues.filter((i) => i.type === 'dangling-registration').length;
|
|
664
699
|
const beforeEmptyDirs = beforeIssues.filter((i) => i.type === 'unregistered-dir' && i.empty).length;
|
|
665
700
|
const result = await pruneWorktrees(targetPath);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
701
|
+
// Under --json the only thing on stdout is the JSON: a status line in front
|
|
702
|
+
// of it breaks every caller that parses the output.
|
|
703
|
+
prune = {
|
|
704
|
+
pruned: Boolean(result.pruned),
|
|
705
|
+
...(result.pruned ? { orphanRegistrations: beforeMissing } : { error: result.error }),
|
|
706
|
+
removedEmptyDirs: result.removedEmptyDirs,
|
|
707
|
+
clearedRegistrations: result.clearedRegistrations.map((r) => r.feature),
|
|
708
|
+
};
|
|
709
|
+
if (!options.json) {
|
|
710
|
+
if (result.pruned) logger.success(`git worktree prune: ${beforeMissing} registro(s) órfão(s) removido(s).`);
|
|
711
|
+
else logger.warn(`prune falhou: ${result.error}`);
|
|
712
|
+
if (result.removedEmptyDirs.length) {
|
|
713
|
+
logger.success(`${result.removedEmptyDirs.length}/${beforeEmptyDirs} diretório(s) vazio(s) removido(s): ${result.removedEmptyDirs.join(', ')}`);
|
|
714
|
+
}
|
|
715
|
+
if (result.clearedRegistrations.length) {
|
|
716
|
+
logger.success(`registro de worktree limpo em ${result.clearedRegistrations.length}/${beforeDangling} feature(s): ${result.clearedRegistrations.map((r) => r.feature).join(', ')}`);
|
|
717
|
+
}
|
|
670
718
|
}
|
|
671
|
-
|
|
672
|
-
|
|
719
|
+
|
|
720
|
+
const { resolvePrimaryRoot } = await import('./worktree.js');
|
|
721
|
+
const { applyBackupRetention, summarizeRetention } = await import('../../lib/worktree/finish-backup.js');
|
|
722
|
+
// The explicit prune is the one that reaches the backups written before the
|
|
723
|
+
// retention existed (no manifest): the automatic one never touches them.
|
|
724
|
+
backupRetention = summarizeRetention(applyBackupRetention(resolvePrimaryRoot(targetPath), { includeLegacy: true }));
|
|
725
|
+
if (backupRetention?.skipped && !options.json) {
|
|
726
|
+
logger.warn(`retenção dos backups do finish PULADA: ${backupRetention.skipped}`);
|
|
727
|
+
} else if (backupRetention && !options.json) {
|
|
728
|
+
const stripped = backupRetention.secretsStripped ? `, ${backupRetention.secretsStripped} arquivo(s) .env* apagado(s)` : '';
|
|
729
|
+
const adopted = backupRetention.adopted ? `, ${backupRetention.adopted} backup(s) antigo(s) mantido(s) ganharam manifesto` : '';
|
|
730
|
+
logger.success(`retenção dos backups do finish: ${backupRetention.removed ?? 0} pasta(s) removida(s)${stripped}${adopted}`);
|
|
731
|
+
for (const d of backupRetention.declined || []) logger.warn(`backup mantido: ${d.backup} (${d.reason})`);
|
|
673
732
|
}
|
|
674
733
|
}
|
|
675
734
|
|
|
@@ -684,7 +743,7 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
684
743
|
const isProblem = (i) => !['info', 'warn'].includes(i.severity);
|
|
685
744
|
|
|
686
745
|
if (options.json) {
|
|
687
|
-
console.log(JSON.stringify({ issues }, null, 2));
|
|
746
|
+
console.log(JSON.stringify({ issues, ...(prune ? { prune } : {}), ...(backupRetention ? { backupRetention } : {}) }, null, 2));
|
|
688
747
|
return !issues.some(isProblem);
|
|
689
748
|
}
|
|
690
749
|
|
|
@@ -1390,6 +1449,19 @@ export async function doctorCommand(options = {}) {
|
|
|
1390
1449
|
checks.push({ name: 'codebase graph', status: 'ok', msg: graphAudit.msg });
|
|
1391
1450
|
}
|
|
1392
1451
|
|
|
1452
|
+
// O que o morph-spec guarda dentro do `.git` (backups do finish, registros de
|
|
1453
|
+
// porta e de claim) cresce sem aparecer em `git status`. Informação — `ok` ou
|
|
1454
|
+
// `warn`, nunca falha —, e nenhuma linha quando não há nada guardado.
|
|
1455
|
+
try {
|
|
1456
|
+
const { resolvePrimaryRoot } = await import('./worktree.js');
|
|
1457
|
+
const { morphStoreCheck } = await import('../../lib/worktree/finish-backup.js');
|
|
1458
|
+
const storeCheck = morphStoreCheck(resolvePrimaryRoot(targetPath));
|
|
1459
|
+
if (storeCheck) {
|
|
1460
|
+
checks.push(storeCheck);
|
|
1461
|
+
if (storeCheck.status === 'warn') hasWarnings = true;
|
|
1462
|
+
}
|
|
1463
|
+
} catch { /* fail-open */ }
|
|
1464
|
+
|
|
1393
1465
|
// Check key templates exist (in framework package, not project copy)
|
|
1394
1466
|
const frameworkTemplatesRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'templates');
|
|
1395
1467
|
const keyTemplates = [
|
|
@@ -33,6 +33,9 @@ import { getProjectTags } from '../../lib/stack-filter.js';
|
|
|
33
33
|
import { updateClaudeMdAgentsBlock, installRulesForStack } from '../../scripts/setup-infra.js';
|
|
34
34
|
import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
|
|
35
35
|
import { createEffects } from '../../lib/installers/effects.js';
|
|
36
|
+
import {
|
|
37
|
+
writeGraphifyignore, describeOutdatedIgnore, graphifyignoreTracked,
|
|
38
|
+
} from '../../lib/graph/graphifyignore.js';
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* Backup user's config.json before cleaning
|
|
@@ -725,6 +728,24 @@ export async function updateCommand(options = {}) {
|
|
|
725
728
|
await updateGitignore(targetPath, fx);
|
|
726
729
|
await ensureGitattributes(targetPath, fx);
|
|
727
730
|
|
|
731
|
+
// Migration: a .graphifyignore the previous version generated and nobody
|
|
732
|
+
// edited is swapped for the current one when its RULES change (test doubles
|
|
733
|
+
// and fixtures back in the graph — a header-only difference is left alone);
|
|
734
|
+
// an edited one is left alone, with the change to make. Only an existing
|
|
735
|
+
// file — the graph is opt-in, and `graph build` writes it. A tracked file
|
|
736
|
+
// reaches worktrees and teammates only once committed: each checkout reads
|
|
737
|
+
// its own.
|
|
738
|
+
let graphIgnore = null;
|
|
739
|
+
if (existsSync(join(targetPath, '.graphifyignore'))) {
|
|
740
|
+
updateSpinner.text = 'Checking .graphifyignore...';
|
|
741
|
+
graphIgnore = writeGraphifyignore(targetPath, projectTags, { fx });
|
|
742
|
+
if (graphIgnore.outdated) notices.push(describeOutdatedIgnore(graphIgnore.outdated));
|
|
743
|
+
if (graphIgnore.migrated && graphifyignoreTracked(targetPath)) {
|
|
744
|
+
notices.push('.graphifyignore é versionado: commite a migração (git add .graphifyignore) — '
|
|
745
|
+
+ 'as worktrees e o resto do time leem o arquivo do próprio checkout e só recebem as regras novas depois do commit');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
728
749
|
// Migration: untrack a pre-v5 committed state.json so it stops being swept
|
|
729
750
|
// into feature commits. Gitignore alone can't do this for an already-tracked file.
|
|
730
751
|
const untracked = untrackCommittedStateJson(targetPath, fx);
|
|
@@ -769,6 +790,10 @@ export async function updateCommand(options = {}) {
|
|
|
769
790
|
logger.dim(` ✓ .claude/agents/ (${agentsResult?.agents?.length ?? '?'} personas — stack: ${tagsLabel})`);
|
|
770
791
|
logger.dim(` ✓ .claude/rules/ (${rulesResult?.installed ?? '?'} stack-filtered rules)`);
|
|
771
792
|
if (commandsCopied) logger.dim(' ✓ .claude/commands/');
|
|
793
|
+
if (graphIgnore?.migrated) {
|
|
794
|
+
logger.dim(' ✓ .graphifyignore (dublês, fixtures e helpers de teste voltam ao grafo da raiz no próximo refresh');
|
|
795
|
+
logger.dim(' em background — início de sessão ou comando git do agente; numa worktree: morph-spec graph refresh)');
|
|
796
|
+
}
|
|
772
797
|
if (untracked) {
|
|
773
798
|
logger.dim(' ✓ .morph/state.json untracked (git rm --cached) — commit to finalize');
|
|
774
799
|
logger.warn('Migration: .morph/state.json was removed from git tracking. The');
|