@polymorphism-tech/morph-spec 9.2.0 → 9.4.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 +14 -78
- package/README.md +2 -2
- package/bin/morph-spec.js +7 -0
- 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 +5 -8
- package/framework/agents.json +14 -9
- package/framework/commands/morph-status.md +2 -2
- package/framework/docs/CLI.md +8 -2
- package/framework/evals/review-checklist.md +4 -1
- package/framework/hooks/README.md +1 -0
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -15
- package/framework/hooks/claude-code/core/conventional-commit.js +57 -0
- package/framework/hooks/claude-code/core/worktree-context.js +23 -0
- package/framework/hooks/claude-code/statusline.py +3 -2
- package/framework/hooks/shared/command-tokens.js +17 -0
- package/framework/hooks/shared/gate-event.js +18 -0
- package/framework/hooks/shared/next-step.js +107 -0
- package/framework/hooks/shared/slice-progress.js +18 -5
- package/framework/hooks/shared/state-writer.js +6 -1
- package/framework/mcp/registry.json +31 -1
- package/framework/rules/csharp-standards.md +5 -3
- package/framework/rules/dotnet-templates.md +2 -2
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/feature.schema.json +1 -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 +4 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +3 -3
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +2 -1
- package/framework/standards/STANDARDS.json +55 -8
- package/framework/standards/STANDARDS.meta.json +29 -1
- package/framework/standards/ai-agents/setup.md +2 -0
- package/framework/standards/architecture/engineering-lens.md +100 -0
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +7 -9
- package/framework/standards/backend/dotnet/analyzers.md +59 -0
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -12
- package/framework/standards/integration/mcp/mcp-tools.md +3 -1
- package/framework/templates/README.md +0 -1
- package/framework/templates/REGISTRY.json +0 -207
- package/package.json +1 -1
- package/src/commands/observability/cost.js +12 -0
- package/src/commands/project/status.js +6 -68
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +1 -1
- package/src/commands/state/archive.js +1 -1
- package/src/commands/state/finish.js +1 -1
- package/src/commands/state/gate-check.js +2 -2
- package/src/commands/state/gate-decision.js +4 -1
- package/src/commands/state/next.js +24 -0
- package/src/commands/state/report-check.js +3 -1
- package/src/commands/validation/verify.js +6 -1
- package/src/core/state/state-manager.js +4 -0
- package/src/core/templates/template-registry.js +1 -43
- package/src/dashboard/app/pages/FeatureDetail.jsx +6 -2
- package/src/dashboard/scanner/features-scanner.js +5 -2
- package/src/lib/generators/handoff-generator.js +11 -25
- package/src/lib/next-step.js +10 -0
- package/src/lib/risk-detector.js +18 -7
- package/src/lib/stack/stack-scan.js +17 -7
- package/src/lib/telemetry/metrics.js +46 -0
- package/src/lib/validators/verify-runner.js +1 -1
- package/src/utils/agents-installer.js +24 -1
- package/src/utils/hooks-installer.js +1 -1
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +0 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +0 -100
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +0 -90
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +0 -84
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +0 -44
- package/framework/templates/meta-prompts/validators/checkpoint-validator.md +0 -107
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +0 -94
|
@@ -6,6 +6,8 @@ import { join } from 'path';
|
|
|
6
6
|
import { ensureCommonExclude } from '../../../framework/hooks/shared/git-exclude.js';
|
|
7
7
|
import { derivePhaseForFeature, getPendingGates } from '../../../framework/hooks/shared/state-reader.js';
|
|
8
8
|
import { sliceProgress, readySlices } from '../../../framework/hooks/shared/slice-progress.js';
|
|
9
|
+
import { formatNext } from '../../../framework/hooks/shared/next-step.js';
|
|
10
|
+
import { nextStepFor } from '../next-step.js';
|
|
9
11
|
|
|
10
12
|
const POINTER_CANDIDATES = [
|
|
11
13
|
['2-plan/plan.md', 'plano: problema, cenários com prova, decisões e fatias'],
|
|
@@ -46,29 +48,13 @@ function readWorktreeEnv(cwd) {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
case 'uiux':
|
|
57
|
-
return approved('uiux')
|
|
58
|
-
? `Gate de uiux aprovado — implemente as fatias do plano via /morph-apply ${featureName}.`
|
|
59
|
-
: `Complete \`1-design/design-system.md\` e aprove o gate de uiux (\`morph-spec advance ${featureName}\`).`;
|
|
60
|
-
case 'implement': {
|
|
61
|
-
const ready = readySlices(slices).map((s) => `${s.id} "${s.title}"`).join(', ');
|
|
62
|
-
return ready
|
|
63
|
-
? `Implemente as fatias prontas — ${ready} — via /morph-apply ${featureName} (cada fatia grava 3-implement/reports/{id}.md).`
|
|
64
|
-
: `Todas as fatias têm relatório final — rode \`morph-spec verify ${featureName}\` e siga para a review. Skill: /morph-apply ${featureName}.`;
|
|
65
|
-
}
|
|
66
|
-
case 'review':
|
|
67
|
-
return approved('review')
|
|
68
|
-
? `Gate 3 aprovado — feche: \`morph-spec finish ${featureName} --pr\` (abre PR) ou \`--merge\` (merge local na default). Arquiva + integra num passo.`
|
|
69
|
-
: `Rode /morph-review ${featureName} (verificador ∥ revisor, segurança se exigida) e aprove o Gate 3; depois \`morph-spec finish ${featureName} --pr|--merge\`.`;
|
|
70
|
-
default:
|
|
71
|
-
return `Rode \`morph-spec status ${featureName}\` para orientar o próximo passo.`;
|
|
51
|
+
function nextLine(featureName, cwd) {
|
|
52
|
+
const fallback = `Rode \`morph-spec next ${featureName}\`.`;
|
|
53
|
+
try {
|
|
54
|
+
const next = nextStepFor(featureName, cwd);
|
|
55
|
+
return next ? formatNext(next) : fallback;
|
|
56
|
+
} catch {
|
|
57
|
+
return fallback;
|
|
72
58
|
}
|
|
73
59
|
}
|
|
74
60
|
|
|
@@ -129,7 +115,7 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
129
115
|
'',
|
|
130
116
|
`- Gates: ${gateLine}`,
|
|
131
117
|
slices
|
|
132
|
-
? `- Fatias: ${slices.done}/${slices.total} feitas${slices.partial ? ` · ${slices.partial} parcial(is)` : ''} · prontas agora: ${readySlices(slices).map((s) => `**${s.id}** "${s.title}"`).join(', ') || 'nenhuma'}`
|
|
118
|
+
? `- Fatias: ${slices.done}/${slices.total} feitas${slices.partial ? ` · ${slices.partial} parcial(is)` : ''}${slices.blocked ? ` · ${slices.blocked} bloqueada(s)` : ''} · prontas agora: ${readySlices(slices).map((s) => `**${s.id}** "${s.title}"`).join(', ') || 'nenhuma'}`
|
|
133
119
|
: '- Fatias: sem checks.json ainda (rode plan-check)',
|
|
134
120
|
pendingGates.length ? `- Gates pendentes: ${pendingGates.join(', ')}` : null,
|
|
135
121
|
'',
|
|
@@ -141,7 +127,7 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
141
127
|
'',
|
|
142
128
|
'## Próxima ação',
|
|
143
129
|
'',
|
|
144
|
-
|
|
130
|
+
nextLine(featureName, cwd),
|
|
145
131
|
'',
|
|
146
132
|
'## Retomar',
|
|
147
133
|
'',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { collectNextSignals, decideNext } from '../../framework/hooks/shared/next-step.js';
|
|
2
|
+
import { treeMovedSince } from './git/evidence-staleness.js';
|
|
3
|
+
|
|
4
|
+
/** The CLI collector: the hook-readable facts plus verify staleness (git). Null without feature.json. */
|
|
5
|
+
export function nextStepFor(featureName, tree) {
|
|
6
|
+
const signals = collectNextSignals(featureName, tree);
|
|
7
|
+
if (!signals) return null;
|
|
8
|
+
signals.verificationStale = treeMovedSince(tree, signals.verification?.sha);
|
|
9
|
+
return { ...decideNext(signals), phase: signals.phase, failStreak: signals.failStreak };
|
|
10
|
+
}
|
package/src/lib/risk-detector.js
CHANGED
|
@@ -158,23 +158,25 @@ export function shouldPauseGate(config, signals) {
|
|
|
158
158
|
const trust = config?.trust ?? 'auto';
|
|
159
159
|
|
|
160
160
|
if (trust === 'manual') {
|
|
161
|
-
return { pause: true, reason: 'trust mode is manual' };
|
|
161
|
+
return { pause: true, signal: 'trust-manual', reason: 'trust mode is manual' };
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
if (FORCED_PAUSE_WORK_TYPES.includes(signals.workType)) {
|
|
165
165
|
return {
|
|
166
166
|
pause: true,
|
|
167
|
+
signal: 'hotfix',
|
|
167
168
|
reason: 'hotfix: aprovação humana obrigatória no ship (política de tipo — ignora trust:auto)',
|
|
168
169
|
};
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
if (signals.designSystemDivergence) {
|
|
172
|
-
return { pause: true, reason: 'design-system divergence detected' };
|
|
173
|
+
return { pause: true, signal: 'design-system', reason: 'design-system divergence detected' };
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
if (Array.isArray(signals.standardsViolations) && signals.standardsViolations.length > 0) {
|
|
176
177
|
return {
|
|
177
178
|
pause: true,
|
|
179
|
+
signal: 'standards',
|
|
178
180
|
reason: `standards violations detected: ${signals.standardsViolations.join(', ')}`,
|
|
179
181
|
};
|
|
180
182
|
}
|
|
@@ -183,6 +185,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
183
185
|
if (signals.verificationStatus === 'fail') {
|
|
184
186
|
return {
|
|
185
187
|
pause: true,
|
|
188
|
+
signal: 'verify-red',
|
|
186
189
|
reason: 'último morph-spec verify está VERMELHO — rode verify e corrija antes de aprovar o gate',
|
|
187
190
|
};
|
|
188
191
|
}
|
|
@@ -191,6 +194,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
191
194
|
if (signals.gate === 'review' && !signals.verificationStatus) {
|
|
192
195
|
return {
|
|
193
196
|
pause: true,
|
|
197
|
+
signal: 'verify-missing',
|
|
194
198
|
reason: 'nenhum `morph-spec verify` carimbado nesta feature — rode `morph-spec verify {feature}` antes do ship',
|
|
195
199
|
};
|
|
196
200
|
}
|
|
@@ -200,7 +204,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
200
204
|
if (signals.gate === 'review' && signals.e2eStatus === 'skip') {
|
|
201
205
|
const klass = classifyE2eSkip(signals.e2eReason);
|
|
202
206
|
if (E2E_SKIP_CLASSES_THAT_PAUSE.includes(klass)) {
|
|
203
|
-
return { pause: true, reason: e2ePauseReason(klass, signals.e2eReason) };
|
|
207
|
+
return { pause: true, signal: 'e2e-skip', reason: e2ePauseReason(klass, signals.e2eReason) };
|
|
204
208
|
}
|
|
205
209
|
}
|
|
206
210
|
|
|
@@ -208,13 +212,14 @@ export function shouldPauseGate(config, signals) {
|
|
|
208
212
|
if (signals.gate === 'review' && signals.verificationStale === true) {
|
|
209
213
|
return {
|
|
210
214
|
pause: true,
|
|
215
|
+
signal: 'verify-stale',
|
|
211
216
|
reason: 'verification stamp desatualizado (o código mudou depois do commit do stamp) — rode `morph-spec verify` de novo antes do ship',
|
|
212
217
|
};
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
if (signals.gate === 'review') {
|
|
216
221
|
const reason = reviewPause(signals.reviewReports);
|
|
217
|
-
if (reason) return { pause: true, reason };
|
|
222
|
+
if (reason) return { pause: true, signal: 'review-reports', reason };
|
|
218
223
|
}
|
|
219
224
|
|
|
220
225
|
// Prompt e modelo não são lidos por build nem por teste. Depois dos relatórios de review de propósito:
|
|
@@ -229,6 +234,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
229
234
|
if (signals.evalsStatus === null || signals.evalsStatus === undefined) {
|
|
230
235
|
return {
|
|
231
236
|
pause: true,
|
|
237
|
+
signal: 'prompt-model-no-eval',
|
|
232
238
|
reason: `mudou prompt/modelo (${where}) e o projeto não declara nó de eval`
|
|
233
239
|
+ ' — nenhum build ou teste unitário fica vermelho por um prompt trocado;'
|
|
234
240
|
+ ' declare um `runner: "evals"` em project.tests[] ou registre a dispensa com `morph-spec gate-decision`',
|
|
@@ -237,6 +243,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
237
243
|
if (signals.evalsStatus === 'skip') {
|
|
238
244
|
return {
|
|
239
245
|
pause: true,
|
|
246
|
+
signal: 'prompt-model-eval-skip',
|
|
240
247
|
reason: `mudou prompt/modelo (${where}) e o eval não foi executado neste verify`
|
|
241
248
|
+ ' — rode `morph-spec verify {feature}` (escopo de feature) antes do ship',
|
|
242
249
|
};
|
|
@@ -250,6 +257,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
250
257
|
&& signals.mediaClaims.section !== true) {
|
|
251
258
|
return {
|
|
252
259
|
pause: true,
|
|
260
|
+
signal: 'media-claims',
|
|
253
261
|
reason: '`4-review/verifier.md` sem a seção `## Mídia gerada` — as quatro afirmações de mídia'
|
|
254
262
|
+ ' (custo, revisão humana, marcação, hot path) não foram provadas'
|
|
255
263
|
+ ' (evals/review-checklist.md, "Verificador")',
|
|
@@ -259,7 +267,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
259
267
|
// Sem o checklist de tela completo, paginação, ordenação, filtro e os três estados não viram cenário.
|
|
260
268
|
if (signals.gate === 'uiux') {
|
|
261
269
|
const reason = uiuxChecklistPause(signals.screenChecklist);
|
|
262
|
-
if (reason) return { pause: true, reason };
|
|
270
|
+
if (reason) return { pause: true, signal: 'uiux-checklist', reason };
|
|
263
271
|
}
|
|
264
272
|
|
|
265
273
|
// Cobra-se só o item devido (`uncovered`): tela toda "não se aplica", design legado ou ausente não devem nada.
|
|
@@ -268,6 +276,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
268
276
|
if (section !== true) {
|
|
269
277
|
return {
|
|
270
278
|
pause: true,
|
|
279
|
+
signal: 'screen-checklist',
|
|
271
280
|
reason: `\`4-review/verifier.md\` sem a seção \`## Checklist de tela\` — o design decidiu ${firstOf(uncovered)}`
|
|
272
281
|
+ ' e o verificador não registrou a conferência contra a tela entregue'
|
|
273
282
|
+ ' (evals/review-checklist.md, "Contrato de tela")',
|
|
@@ -275,6 +284,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
275
284
|
}
|
|
276
285
|
return {
|
|
277
286
|
pause: true,
|
|
287
|
+
signal: 'screen-checklist',
|
|
278
288
|
reason: `a seção \`## Checklist de tela\` não confere ${firstOf(uncovered)}`
|
|
279
289
|
+ ' — uma linha por item decidido de cada tela, mesmo quando não verificável',
|
|
280
290
|
};
|
|
@@ -282,17 +292,18 @@ export function shouldPauseGate(config, signals) {
|
|
|
282
292
|
|
|
283
293
|
if (signals.gate === 'plan') {
|
|
284
294
|
const reason = planChecksPause(signals.planChecks);
|
|
285
|
-
if (reason) return { pause: true, reason };
|
|
295
|
+
if (reason) return { pause: true, signal: 'plan-checks', reason };
|
|
286
296
|
}
|
|
287
297
|
|
|
288
298
|
if (typeof signals.sliceCount === 'number' && signals.sliceCount > MAX_SLICES) {
|
|
289
299
|
return {
|
|
290
300
|
pause: true,
|
|
301
|
+
signal: 'slice-count',
|
|
291
302
|
reason: `${signals.sliceCount} fatias, acima de ${MAX_SLICES} — o plano é grande demais para uma feature`,
|
|
292
303
|
};
|
|
293
304
|
}
|
|
294
305
|
|
|
295
|
-
return { pause: false, reason: '' };
|
|
306
|
+
return { pause: false, signal: null, reason: '' };
|
|
296
307
|
}
|
|
297
308
|
|
|
298
309
|
export function getRiskSignals(signals) {
|
|
@@ -99,7 +99,19 @@ function auditDotnet(files, read) {
|
|
|
99
99
|
const otherDb = has(/^(Microsoft\.EntityFrameworkCore\.SqlServer|Pomelo\.EntityFrameworkCore\.MySql|MySql\.|MySqlConnector|Microsoft\.Data\.SqlClient|System\.Data\.SqlClient)/);
|
|
100
100
|
rows.push({ layer: 'ORM', detected: ef.length > 0 ? 'EF Core' : (packages.has('Dapper') ? 'Dapper' : 'ausente'), expected: 'EF Core', ok: ef.length > 0 });
|
|
101
101
|
rows.push({ layer: 'Banco', detected: npgsql.length > 0 ? 'Postgres (Npgsql)' : (otherDb.join(', ') || 'ausente'), expected: 'Neon (Postgres)', ok: npgsql.length > 0 && otherDb.length === 0 });
|
|
102
|
-
|
|
102
|
+
const semanticKernel = has(/^Microsoft\.SemanticKernel/);
|
|
103
|
+
const kernelFunction = grepFiles(source, read, /\[KernelFunction\b/);
|
|
104
|
+
const hostedAgents = has(/^Azure\.AI\.(Projects|Agents\.Persistent)\b/);
|
|
105
|
+
const floatingAi = has(/^(Microsoft\.Agents\.AI|Microsoft\.Extensions\.AI|OpenAI$|Google\.GenAI$|ModelContextProtocol)/)
|
|
106
|
+
.filter((p) => /\*|^[[(]/.test(packages.get(p) ?? ''));
|
|
107
|
+
rows.push({ layer: 'IA', detected: has(/^Microsoft\.(Agents\.AI|Extensions\.AI|SemanticKernel)/).join(', ') || 'ausente', expected: 'MAF quando houver agentes', ok: semanticKernel.length === 0 && kernelFunction.length === 0 && hostedAgents.length === 0 });
|
|
108
|
+
const antiPatterns = '`morph-spec standards --show ai-agents-setup#7-what-not-to-do-anti-patterns`';
|
|
109
|
+
if (semanticKernel.length > 0 || kernelFunction.length > 0) {
|
|
110
|
+
add('Alto', `Semantic Kernel em vez de MAF (${semanticKernel.join(', ') || '`[KernelFunction]`'})`, [...(semanticKernel.length > 0 ? projects : []), ...kernelFunction],
|
|
111
|
+
`\`Microsoft.Agents.AI\` (\`agent.RunAsync()\`, tool por \`AIFunctionFactory.Create()\`); ${antiPatterns}`);
|
|
112
|
+
}
|
|
113
|
+
if (hostedAgents.length > 0) add('Alto', `Serviço de agente hospedado (${hostedAgents.join(', ')})`, projects, `modelo direto por \`IChatClient\`; ${antiPatterns}`);
|
|
114
|
+
for (const p of floatingAi) add('Médio', `Versão flutuante de SDK de IA (${p} ${packages.get(p)})`, [], 'versão exata, a do pin (`morph-spec doctor --ai`)');
|
|
103
115
|
rows.push({ layer: 'Jobs', detected: has(/^Hangfire/).length > 0 ? 'Hangfire' : 'ausente', expected: 'Hangfire só com razão real', ok: true });
|
|
104
116
|
if (packages.has('Dapper') && ef.length === 0) add('Bloqueante', 'ORM diferente (Dapper puro sem EF Core)', projects, 'migrar o acesso a dados para EF Core');
|
|
105
117
|
if (otherDb.length > 0) add('Bloqueante', `Banco diferente (${otherDb.join(', ')})`, projects, 'migrar para Neon (Postgres, Npgsql)');
|
|
@@ -123,12 +135,10 @@ function auditDotnet(files, read) {
|
|
|
123
135
|
if (serviceLayer.length > 0) add('Alto', 'Camada de Service/Manager entre handler e dados', serviceLayer, 'o handler fala com o `ApplicationDbContext` direto');
|
|
124
136
|
const rawSql = grepFiles(source, read, /\b(FromSqlRaw|ExecuteSqlRaw|SqlQueryRaw|ExecuteQuery)\s*\(/);
|
|
125
137
|
if (rawSql.length > 0) add('Alto', 'SQL cru', rawSql, 'LINQ, ou store dedicado com justificativa em ADR');
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
131
|
-
if (notSealed.length > 0) add('Médio', 'Classes não-sealed', notSealed, '`sealed` por padrão');
|
|
138
|
+
if (projects.length > 0 && !packages.has('Microsoft.CodeAnalysis.BannedApiAnalyzers')) {
|
|
139
|
+
add('Médio', 'Analyzers do stack ausentes (o build não barra `Guid.NewGuid()` nem token não repassado)', [],
|
|
140
|
+
'`Directory.Build.props` + `BannedSymbols.txt` + `morph.globalconfig` na raiz: `morph-spec standards --show backend-dotnet-analyzers#files`');
|
|
141
|
+
}
|
|
132
142
|
const controllers = grepFiles(source, read, /:\s*Controller(Base)?\b/);
|
|
133
143
|
rows.push({ layer: 'Estrutura', detected: controllers.length > 0 ? `${controllers.length} controller(s) MVC` : 'sem controllers', expected: 'VSA (endpoint por slice)', ok: controllers.length === 0 });
|
|
134
144
|
return { rows, deviations };
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { ingestedBytes } from './telemetry-reader.js';
|
|
5
5
|
import { producerOf } from '../../../framework/hooks/shared/command-tokens.js';
|
|
6
6
|
import { isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
|
|
7
|
+
import { GATE_EVENT, GATE_STEPS } from '../../../framework/hooks/shared/gate-event.js';
|
|
7
8
|
|
|
8
9
|
export const RERUN_WINDOW_MS = 10 * 60 * 1000;
|
|
9
10
|
|
|
@@ -122,6 +123,43 @@ function reuseStateOf(e) {
|
|
|
122
123
|
return { state: 'absent', reason: null };
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
const newGateSlot = () => ({ decisions: 0, paused: 0, autoPassed: 0, approvedAfterPause: 0, reworkedAfterPause: 0, unapproved: 0, bySignal: {} });
|
|
127
|
+
|
|
128
|
+
// A pause stays open until the next approve or a decision that no longer pauses on the same signal:
|
|
129
|
+
// re-running gate-check over the same unchanged pause is not rework.
|
|
130
|
+
function trackGate(gates, open, e) {
|
|
131
|
+
if (!e.gate || !GATE_STEPS.includes(e.step)) return;
|
|
132
|
+
const slot = gates[e.gate] ||= newGateSlot();
|
|
133
|
+
const key = `${e.feature || ''}\u0000${e.gate}`;
|
|
134
|
+
const pending = open.get(key);
|
|
135
|
+
const bySignal = (signal) => slot.bySignal[signal || 'desconhecido'] ||= { paused: 0, approvedAfterPause: 0, reworkedAfterPause: 0 };
|
|
136
|
+
const credit = (field) => {
|
|
137
|
+
slot[field] += 1;
|
|
138
|
+
bySignal(pending.signal)[field] += 1;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (e.step === 'unapprove') {
|
|
142
|
+
slot.unapproved += 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (e.step === 'approve') {
|
|
146
|
+
if (pending) credit('approvedAfterPause');
|
|
147
|
+
open.delete(key);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
slot.decisions += 1;
|
|
151
|
+
if (pending && e.pause && e.signal === pending.signal) return;
|
|
152
|
+
if (pending) credit(e.source === 'manual' && !e.pause ? 'approvedAfterPause' : 'reworkedAfterPause');
|
|
153
|
+
if (e.pause) {
|
|
154
|
+
slot.paused += 1;
|
|
155
|
+
bySignal(e.signal).paused += 1;
|
|
156
|
+
open.set(key, { signal: e.signal });
|
|
157
|
+
} else {
|
|
158
|
+
slot.autoPassed += 1;
|
|
159
|
+
open.delete(key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
125
163
|
/** The returned shape is documented in framework/docs/CLI.md, "cost --metrics". */
|
|
126
164
|
export function computeMetrics(events, opts = {}) {
|
|
127
165
|
const top = Number.isInteger(opts.top) && opts.top > 0 ? opts.top : 10;
|
|
@@ -140,6 +178,8 @@ export function computeMetrics(events, opts = {}) {
|
|
|
140
178
|
const lastFingerprint = new Map();
|
|
141
179
|
const unchangedRepeats = {};
|
|
142
180
|
const reuse = { runs: 0, byTree: {}, skipReasons: {} };
|
|
181
|
+
const gates = {};
|
|
182
|
+
const openPauses = new Map();
|
|
143
183
|
let v2 = 0;
|
|
144
184
|
let legacy = 0;
|
|
145
185
|
let turnsObserved = false;
|
|
@@ -166,6 +206,11 @@ export function computeMetrics(events, opts = {}) {
|
|
|
166
206
|
continue;
|
|
167
207
|
}
|
|
168
208
|
|
|
209
|
+
if (e.event === GATE_EVENT) {
|
|
210
|
+
trackGate(gates, openPauses, e);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
169
214
|
if (e.event === 'Verify') {
|
|
170
215
|
if (e.fingerprint && e.persisted !== false) {
|
|
171
216
|
if (lastFingerprint.get(fkey) === e.fingerprint) {
|
|
@@ -291,6 +336,7 @@ export function computeMetrics(events, opts = {}) {
|
|
|
291
336
|
},
|
|
292
337
|
dotnetFullSuite: fullSuite,
|
|
293
338
|
reuse,
|
|
339
|
+
gates,
|
|
294
340
|
features: featureRows,
|
|
295
341
|
};
|
|
296
342
|
}
|
|
@@ -713,7 +713,7 @@ async function runE2eNode({
|
|
|
713
713
|
command: result.command ?? null,
|
|
714
714
|
durationMs,
|
|
715
715
|
reason: result.reason ?? null,
|
|
716
|
-
output: result.output || '',
|
|
716
|
+
output: tailLines(result.output || '', OUTPUT_TAIL_LINES),
|
|
717
717
|
count: Number.isInteger(result.count) ? result.count : null,
|
|
718
718
|
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
719
719
|
},
|
|
@@ -2,6 +2,7 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { shouldInstall } from '../lib/stack-filter.js';
|
|
4
4
|
import { realEffects } from '../lib/installers/effects.js';
|
|
5
|
+
import { REVIEW_ROLES, VERDICT_TOKEN, REVIEWED_SHA_TOKEN, VERDICTS } from '../lib/review-reports.js';
|
|
5
6
|
|
|
6
7
|
export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
|
|
7
8
|
|
|
@@ -288,7 +289,7 @@ export const DISPATCH_CONTRACT = [
|
|
|
288
289
|
'Grave a saída em arquivo (`… > .morph/logs/test-last.log 2>&1`, sem nada depois) e leia o arquivo — um pipe troca o exit do teste pelo do último comando. ' +
|
|
289
290
|
'Em batch paralelo SEM isolamento, só compile. Com `isolation: "worktree"`, o mesmo vale no seu worktree, e o último ato é o commit na branch dele.',
|
|
290
291
|
'4. ESCOPO: uma fatia, ~4 arquivos (nunca mais de 8). Escopo maior falha e volta — fatie antes, não depois.',
|
|
291
|
-
'5. SOBREVIVÊNCIA: o limite de turnos corta sem aviso: commite `
|
|
292
|
+
'5. SOBREVIVÊNCIA: o limite de turnos corta sem aviso: commite `chore(F{n}): wip …` a cada marco (um arquivo ou uma fatia escrita), não só ao fim — ' +
|
|
292
293
|
'e só os seus arquivos (`git add <paths>`, nunca `-A` nem diretório). Sem commit, a retomada recomeça do zero.',
|
|
293
294
|
'6. PROCESSOS: NUNCA `taskkill /IM node.exe` (derruba os MCPs de todas as sessões). Encerre dev server pelo PID que você mesmo subiu. ' +
|
|
294
295
|
'Build e suíte pesados rodam por `morph-spec heavy run -- <comando>`, em primeiro plano. Exit 3 é fila cheia depois da espera da própria fila: ' +
|
|
@@ -300,6 +301,8 @@ export const DISPATCH_CONTRACT = [
|
|
|
300
301
|
'`git checkout --`). A prova de que o teste morde é do verificador (Gate 3).',
|
|
301
302
|
'9. ESCRITA: código e teste do projeto só por Write/Edit, nunca pelo Bash — heredoc, redirecionamento, `sed -i` ou script python/node que grava (`cat > X.cs <<EOF`, `sed -i … X.cs`): ' +
|
|
302
303
|
'os guards de escrita só enxergam Write/Edit.',
|
|
304
|
+
'10. BLOQUEIO: decisão que não é sua (requisito contra o plano ou um ADR, escolha deixada ao humano) → `BLOQUEADO` na 1ª linha, ' +
|
|
305
|
+
'com a pergunta e as opções. Dificuldade técnica não é bloqueio.',
|
|
303
306
|
'',
|
|
304
307
|
'Formato do relatório (o `report-check` lê estas marcas):',
|
|
305
308
|
'',
|
|
@@ -317,6 +320,25 @@ export const DISPATCH_CONTRACT = [
|
|
|
317
320
|
'Problemas e como foram resolvidos vão em prosa depois.',
|
|
318
321
|
];
|
|
319
322
|
|
|
323
|
+
export const REVIEW_REPORT_CONTRACT = [
|
|
324
|
+
'## Relatório do Gate 3',
|
|
325
|
+
'',
|
|
326
|
+
'Grave o relatório que o dispatch nomeia ANTES de encerrar — o arquivo é o retorno, não a mensagem final. ' +
|
|
327
|
+
'Se ele já existir quando você começar, é resíduo de outra rodada: não o leia antes de escrever o seu (ancoragem no veredito alheio), ' +
|
|
328
|
+
'e não leia os relatórios dos outros papéis.',
|
|
329
|
+
'',
|
|
330
|
+
`Primeira linha \`${VERDICT_TOKEN} ${VERDICTS.join('|')}\`, depois \`${REVIEWED_SHA_TOKEN} {sha}\`, ` +
|
|
331
|
+
'tabela `## Bloqueadores` (`| id | tipo | onde | evidência |`; vazia = `Nenhum.`) e `## Notas`. ' +
|
|
332
|
+
'Bloqueador só com evidência e tipo; o resto é nota, e nota nunca pede rodada nova.',
|
|
333
|
+
'',
|
|
334
|
+
'Numa confirmação (o orquestrador te retoma depois da correção, com o `sha` novo) acrescente ao fim uma seção nova com ' +
|
|
335
|
+
`\`${VERDICT_TOKEN}\` e \`${REVIEWED_SHA_TOKEN} {sha novo}\`: se você abriu bloqueadores, julgue só eles; ` +
|
|
336
|
+
'se não abriu, julgue só o delta `{sha anterior}..{sha novo}` — o que a correção abriu. ' +
|
|
337
|
+
'O relatório é append-only: vale o último veredito e, na `## Checks`, a última linha de cada Cn.',
|
|
338
|
+
'',
|
|
339
|
+
'Um comando por chamada de Bash, sem encadear (`;`, `&&`, pipe): o composto é negado e custa a rodada.',
|
|
340
|
+
];
|
|
341
|
+
|
|
320
342
|
function buildBody(agent, maxTurns) {
|
|
321
343
|
const lines = [
|
|
322
344
|
`# ${agent.title ?? agent.id}`,
|
|
@@ -374,6 +396,7 @@ function buildBody(agent, maxTurns) {
|
|
|
374
396
|
'O entregável é o arquivo de relatório que o dispatch nomeia; sem arquivo nomeado, é a sua mensagem final (no scout, o mapa).',
|
|
375
397
|
'',
|
|
376
398
|
);
|
|
399
|
+
if (Object.hasOwn(REVIEW_ROLES, agent.id)) lines.push(...REVIEW_REPORT_CONTRACT, '');
|
|
377
400
|
}
|
|
378
401
|
|
|
379
402
|
lines.push(
|
|
@@ -64,7 +64,7 @@ const MORPH_HOOKS = [
|
|
|
64
64
|
hooks: [{
|
|
65
65
|
type: 'command',
|
|
66
66
|
command: 'node framework/hooks/claude-code/core/chain.js',
|
|
67
|
-
args: ['pre-bash-denylist', 'block-destructive-worktree-removal', 'block-raw-worktree-add', 'warn-bash-source-write', 'heavy-gate', 'worktree-env-inject']
|
|
67
|
+
args: ['pre-bash-denylist', 'conventional-commit', 'block-destructive-worktree-removal', 'block-raw-worktree-add', 'warn-bash-source-write', 'heavy-gate', 'worktree-env-inject']
|
|
68
68
|
}]
|
|
69
69
|
},
|
|
70
70
|
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
# Fusion Agent — {{AGENT_ID}} (F-Thread {{THREAD_INDEX}}/{{THREAD_COUNT}})
|
|
2
|
-
|
|
3
|
-
You are **Fusion Agent {{THREAD_INDEX}}** in a best-of-N execution session.
|
|
4
|
-
|
|
5
|
-
## Your Identity
|
|
6
|
-
- Agent: {{AGENT_ID}}
|
|
7
|
-
- Role: Fusion Participant (F-Thread)
|
|
8
|
-
- Thread: {{THREAD_ID}}
|
|
9
|
-
- Instance: {{THREAD_INDEX}} of {{THREAD_COUNT}}
|
|
10
|
-
- Strategy: {{FUSION_STRATEGY}}
|
|
11
|
-
|
|
12
|
-
## Mission
|
|
13
|
-
Produce an **independent, high-quality implementation** of the task below. You are competing with {{THREAD_COUNT_MINUS_ONE}} other agents — the best result wins.
|
|
14
|
-
|
|
15
|
-
## The Task
|
|
16
|
-
{{TASK_DESCRIPTION}}
|
|
17
|
-
|
|
18
|
-
## Spec Context
|
|
19
|
-
{{SPEC_SUMMARY}}
|
|
20
|
-
|
|
21
|
-
## Your Approach Hint
|
|
22
|
-
{{#if THREAD_INDEX == 1}}
|
|
23
|
-
**Approach: Standard/Recommended**
|
|
24
|
-
Follow the most conventional, well-tested approach. Prioritize clarity and maintainability over cleverness.
|
|
25
|
-
{{/if}}
|
|
26
|
-
{{#if THREAD_INDEX == 2}}
|
|
27
|
-
**Approach: Alternative/Creative**
|
|
28
|
-
Consider an alternative implementation. What would a different architectural style look like? Explore a different pattern while still meeting all requirements.
|
|
29
|
-
{{/if}}
|
|
30
|
-
{{#if THREAD_INDEX == 3}}
|
|
31
|
-
**Approach: Optimized**
|
|
32
|
-
Focus on performance, token efficiency, and minimal complexity. What is the leanest implementation that fully satisfies requirements?
|
|
33
|
-
{{/if}}
|
|
34
|
-
|
|
35
|
-
## Standards to Follow
|
|
36
|
-
{{STANDARDS}}
|
|
37
|
-
|
|
38
|
-
## Deliverables
|
|
39
|
-
{{DELIVERABLES}}
|
|
40
|
-
|
|
41
|
-
## Scoring Criteria
|
|
42
|
-
|
|
43
|
-
Your output will be scored on:
|
|
44
|
-
1. **Completeness (40%)** — Did you implement everything in the requirements?
|
|
45
|
-
2. **Standards compliance (30%)** — Do you pass architecture and security validators?
|
|
46
|
-
3. **Code quality (30%)** — Is the code clean, readable, and maintainable?
|
|
47
|
-
|
|
48
|
-
## Output Format
|
|
49
|
-
|
|
50
|
-
Structure your output as:
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
FUSION AGENT REPORT
|
|
54
|
-
Agent: {{AGENT_ID}}
|
|
55
|
-
Thread: {{THREAD_ID}}
|
|
56
|
-
Approach: [brief description of your approach]
|
|
57
|
-
|
|
58
|
-
IMPLEMENTATION:
|
|
59
|
-
[your actual implementation]
|
|
60
|
-
|
|
61
|
-
SELF-ASSESSMENT:
|
|
62
|
-
Completeness: [X/10] — [brief justification]
|
|
63
|
-
Standards: [X/10] — [validators passed/failed]
|
|
64
|
-
Quality: [X/10] — [brief justification]
|
|
65
|
-
|
|
66
|
-
FILES CREATED/MODIFIED:
|
|
67
|
-
- [list each file]
|
|
68
|
-
|
|
69
|
-
APPROACH RATIONALE:
|
|
70
|
-
[explain your key design decisions in 2-3 sentences]
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Important
|
|
74
|
-
- Work independently — do NOT coordinate with other fusion threads
|
|
75
|
-
- Be decisive — don't hedge or produce "option A / option B" — commit to one implementation
|
|
76
|
-
- Self-assess honestly — the aggregator uses your self-assessment in scoring
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# Fusion Aggregator — {{SESSION_ID}}
|
|
2
|
-
|
|
3
|
-
You are the **Fusion Aggregator** — you select the best result from {{THREAD_COUNT}} competing implementations.
|
|
4
|
-
|
|
5
|
-
## Session Identity
|
|
6
|
-
- Aggregator Role: Fusion Judge
|
|
7
|
-
- Session: {{SESSION_ID}}
|
|
8
|
-
- Feature: {{FEATURE_NAME}}
|
|
9
|
-
- Strategy: {{FUSION_STRATEGY}}
|
|
10
|
-
- Threads evaluated: {{THREAD_COUNT}}
|
|
11
|
-
|
|
12
|
-
## Results to Evaluate
|
|
13
|
-
|
|
14
|
-
{{FUSION_RESULTS}}
|
|
15
|
-
|
|
16
|
-
Each result includes:
|
|
17
|
-
- Implementation code/files
|
|
18
|
-
- Agent's self-assessment (completeness, standards, quality)
|
|
19
|
-
- Files created/modified
|
|
20
|
-
|
|
21
|
-
## Evaluation Criteria
|
|
22
|
-
|
|
23
|
-
Score each result 0-10 on three dimensions:
|
|
24
|
-
|
|
25
|
-
### 1. Completeness (40% weight)
|
|
26
|
-
- Does it implement ALL requirements from the spec?
|
|
27
|
-
- Are edge cases handled?
|
|
28
|
-
- Are deliverables complete (no TODOs, no stubs)?
|
|
29
|
-
|
|
30
|
-
### 2. Standards Compliance (30% weight)
|
|
31
|
-
- Architecture patterns correct (DI, async/await, layer separation)?
|
|
32
|
-
- Security requirements met (no secrets, input validation)?
|
|
33
|
-
- Code follows project conventions?
|
|
34
|
-
|
|
35
|
-
### 3. Code Quality (30% weight)
|
|
36
|
-
- Is the code readable and maintainable?
|
|
37
|
-
- Is it appropriately simple (not over-engineered)?
|
|
38
|
-
- Naming conventions followed?
|
|
39
|
-
- No obvious bugs or anti-patterns?
|
|
40
|
-
|
|
41
|
-
## Aggregation Strategy
|
|
42
|
-
|
|
43
|
-
{{#if STRATEGY == "best-of-n"}}
|
|
44
|
-
### Best-of-N Strategy
|
|
45
|
-
Select the single highest-scoring result. Use it as-is (do not merge).
|
|
46
|
-
|
|
47
|
-
Tiebreaker: prefer the result with higher standards compliance score.
|
|
48
|
-
{{/if}}
|
|
49
|
-
|
|
50
|
-
{{#if STRATEGY == "consensus"}}
|
|
51
|
-
### Consensus Strategy
|
|
52
|
-
Identify elements that appear in 2+ results (majority vote). Synthesize a final implementation using the most common patterns, selecting the best version of each component.
|
|
53
|
-
|
|
54
|
-
Structure your output as a synthesized implementation, not a selection.
|
|
55
|
-
{{/if}}
|
|
56
|
-
|
|
57
|
-
{{#if STRATEGY == "manual-select"}}
|
|
58
|
-
### Manual Select Strategy
|
|
59
|
-
Present all results with your scoring to the user. Do NOT make a final selection — let the user choose.
|
|
60
|
-
{{/if}}
|
|
61
|
-
|
|
62
|
-
## Your Output
|
|
63
|
-
|
|
64
|
-
### Scoring Table
|
|
65
|
-
```
|
|
66
|
-
| Thread | Agent | Completeness | Standards | Quality | TOTAL |
|
|
67
|
-
|--------|-------|-------------|-----------|---------|-------|
|
|
68
|
-
| T-001 | {{AGENT_1}} | X/10 | X/10 | X/10 | XX/30 |
|
|
69
|
-
| T-002 | {{AGENT_2}} | X/10 | X/10 | X/10 | XX/30 |
|
|
70
|
-
| T-003 | {{AGENT_3}} | X/10 | X/10 | X/10 | XX/30 |
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Winner Selection
|
|
74
|
-
```
|
|
75
|
-
FUSION RESULT
|
|
76
|
-
Session: {{SESSION_ID}}
|
|
77
|
-
Winner: Thread {{WINNING_THREAD}} (Agent: {{WINNING_AGENT}})
|
|
78
|
-
Score: {{WINNING_SCORE}}/30
|
|
79
|
-
Strategy: {{FUSION_STRATEGY}}
|
|
80
|
-
|
|
81
|
-
Key strengths of winning implementation:
|
|
82
|
-
- [reason 1]
|
|
83
|
-
- [reason 2]
|
|
84
|
-
|
|
85
|
-
Areas where other implementations excelled:
|
|
86
|
-
- [any cross-thread insights worth preserving]
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Final Implementation
|
|
90
|
-
[Include the winning implementation in full, or merged implementation for consensus strategy]
|
|
91
|
-
|
|
92
|
-
### Files to Apply
|
|
93
|
-
```
|
|
94
|
-
- [each file from winning/merged result]
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Important Notes
|
|
98
|
-
- Be objective — don't favor any particular agent
|
|
99
|
-
- If all results are poor (< 15/30), flag for human review rather than selecting
|
|
100
|
-
- Note any elements from non-winning results worth cherry-picking
|