@polymorphism-tech/morph-spec 9.8.0 → 10.0.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 +10 -10
- package/README.md +4 -4
- package/bin/morph-spec.js +40 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +24 -30
- package/framework/MORPH.md +47 -123
- package/framework/agents.json +6 -374
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-archive.md +4 -3
- package/framework/commands/morph-hotfix.md +8 -30
- package/framework/commands/morph-infra.md +2 -2
- package/framework/commands/morph-media.md +10 -9
- package/framework/commands/morph-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +83 -94
- package/framework/evals/review-checklist.md +45 -53
- package/framework/hooks/README.md +4 -4
- package/framework/hooks/claude-code/core/README.md +1 -1
- package/framework/hooks/claude-code/core/denylist.json +4 -4
- package/framework/hooks/claude-code/core/gate-guard.js +9 -0
- package/framework/hooks/claude-code/core/masked-exit.js +49 -0
- package/framework/hooks/claude-code/core/state-sync.js +4 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
- package/framework/hooks/claude-code/core/test-budget.js +10 -70
- package/framework/hooks/claude-code/core/validate-completion.js +11 -24
- package/framework/hooks/claude-code/core/worktree-context.js +5 -22
- package/framework/hooks/claude-code/core/worktree-create.js +6 -45
- package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/gate-event.js +2 -2
- package/framework/hooks/shared/graph-refresh.js +20 -3
- package/framework/hooks/shared/heavy-command.js +32 -1
- package/framework/hooks/shared/heavy-config.js +1 -1
- package/framework/hooks/shared/next-step.js +63 -24
- package/framework/hooks/shared/phase-utils.js +6 -9
- package/framework/hooks/shared/report-progress.js +86 -0
- package/framework/hooks/shared/state-reader.js +3 -2
- package/framework/hooks/statusline.js +332 -0
- package/framework/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +9 -1
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- package/framework/rules/nextjs-standards.md +7 -0
- package/framework/rules/plan-grammar.md +53 -0
- package/framework/rules/ui-design.md +27 -0
- package/framework/schemas/config.schema.json +11 -18
- package/framework/schemas/feature.schema.json +7 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +38 -166
- package/framework/skills/core/morph-apply/references/debugging.md +8 -0
- package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
- package/framework/skills/core/morph-apply/references/machine.md +29 -0
- package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
- package/framework/skills/core/morph-apply/references/review.md +65 -0
- package/framework/skills/core/morph-plan/SKILL.md +42 -108
- package/framework/skills/core/morph-plan/references/critique.md +40 -0
- package/framework/skills/core/morph-plan/references/interview.md +25 -0
- package/framework/skills/core/morph-uiux/SKILL.md +47 -64
- package/framework/skills/core/morph-uiux/surfaces.json +32 -0
- package/framework/standards/STANDARDS.json +32 -5
- package/framework/standards/STANDARDS.meta.json +5 -1
- package/framework/standards/ai-agents/agent-session.md +2 -3
- package/framework/standards/ai-agents/context-providers.md +2 -2
- package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +2 -2
- package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +2 -2
- package/framework/standards/architecture/stack-baseline.md +47 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
- package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
- package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
- package/framework/standards/frontend/design-system/art-direction.md +2 -2
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
- package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +12 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +88 -23
- package/src/commands/graph/diff.js +74 -0
- package/src/commands/graph/status.js +1 -1
- package/src/commands/observability/cost.js +2 -4
- package/src/commands/observability/telemetry.js +1 -1
- package/src/commands/project/doctor.js +23 -39
- package/src/commands/project/fleet.js +5 -6
- package/src/commands/project/init.js +63 -36
- package/src/commands/project/stack-scan.js +3 -42
- package/src/commands/project/stack-set.js +26 -0
- package/src/commands/project/status.js +12 -13
- package/src/commands/project/update.js +39 -3
- package/src/commands/project/worktree.js +3 -14
- package/src/commands/state/advance.js +17 -17
- package/src/commands/state/approve.js +0 -6
- package/src/commands/state/archive.js +27 -1
- package/src/commands/state/auto-approval.js +1 -1
- package/src/commands/state/create.js +0 -5
- package/src/commands/state/gate-check.js +37 -30
- package/src/commands/state/gate-decision.js +3 -2
- package/src/commands/state/next.js +1 -0
- package/src/commands/state/recap.js +67 -0
- package/src/commands/state/report-check.js +36 -178
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +3 -6
- package/src/core/paths/output-schema.js +3 -3
- package/src/core/state/state-manager.js +2 -4
- package/src/dashboard/app/components/CommandPalette.jsx +2 -5
- package/src/dashboard/app/components/GatePauses.jsx +116 -0
- package/src/dashboard/app/components/SessionMap.jsx +77 -0
- package/src/dashboard/app/pages/Activity.jsx +4 -0
- package/src/dashboard/app/pages/AgentMap.jsx +1 -2
- package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Portfolio.jsx +40 -5
- package/src/dashboard/scanner/activity-scanner.js +29 -0
- package/src/dashboard/scanner/agents-scanner.js +11 -14
- package/src/dashboard/scanner/features-scanner.js +45 -24
- package/src/dashboard/scanner/project-api.js +11 -2
- package/src/dashboard/scanner/skills-scanner.js +11 -1
- package/src/lib/e2e/applicability.js +3 -1
- package/src/lib/git/tree-state.js +1 -16
- package/src/lib/graph/diff-tree.js +127 -0
- package/src/lib/graph/graphify-runtime.js +12 -5
- package/src/lib/installers/memory-installer.js +2 -2
- package/src/lib/installers/rules-installer.js +57 -0
- package/src/lib/installers/stack-assets.js +17 -0
- package/src/lib/installers/stack-sync.js +219 -0
- package/src/lib/plan/checks.js +55 -110
- package/src/lib/plan/critique.js +28 -0
- package/src/lib/plan/media.js +42 -0
- package/src/lib/plan/plan-html.js +125 -0
- package/src/lib/plan/plan-parser.js +34 -45
- package/src/lib/recap/piper.js +36 -0
- package/src/lib/recap/recap.js +232 -0
- package/src/lib/review-reports.js +28 -4
- package/src/lib/risk-detector.js +90 -11
- package/src/lib/specs/screen-checklist.js +33 -48
- package/src/lib/stack/dotnet-build-ignores.js +8 -9
- package/src/lib/telemetry/metrics.js +27 -29
- package/src/lib/validators/reuse/reuse-validator.js +1 -9
- package/src/lib/validators/verify-runner.js +0 -7
- package/src/lib/worktree/fleet-scanner.js +7 -20
- package/src/lib/worktree/worktree-audit.js +0 -37
- package/src/scripts/setup-infra.js +15 -50
- package/src/utils/agents-installer.js +27 -201
- package/src/utils/hooks-installer.js +11 -95
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
- package/framework/hooks/claude-code/statusline.py +0 -699
- package/framework/hooks/claude-code/statusline.sh +0 -11
- package/framework/hooks/shared/agent-reap.js +0 -74
- package/framework/hooks/shared/slice-progress.js +0 -110
- package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
- package/framework/skills/core/morph-machine/SKILL.md +0 -46
- package/framework/skills/core/morph-review/SKILL.md +0 -129
- package/framework/skills/core/morph-standards/SKILL.md +0 -52
- package/framework/standards/PROVENANCE.json +0 -119
- package/src/commands/project/worktree-reap.js +0 -34
- package/src/commands/state/handoff.js +0 -65
- package/src/commands/validation/mutate.js +0 -263
- package/src/commands/validation/scout-check.js +0 -28
- package/src/lib/generators/handoff-generator.js +0 -172
- package/src/lib/mutation/adapters.js +0 -79
- package/src/lib/mutation/commands.js +0 -61
- package/src/lib/mutation/dotnet-introspection.js +0 -281
- package/src/lib/mutation/manifest.js +0 -144
- package/src/lib/mutation/plan.js +0 -44
- package/src/lib/mutation/report.js +0 -68
- package/src/lib/mutation/runner.js +0 -301
- package/src/lib/mutation/schemata.js +0 -60
- package/src/lib/mutation/snapshot.js +0 -65
- package/src/lib/mutation/subset.js +0 -109
- package/src/lib/mutation/trx.js +0 -50
- package/src/lib/scout/scout-check.js +0 -293
- package/src/lib/validators/traceability-check.js +0 -103
- package/src/lib/worktree/agent-reaper.js +0 -247
package/src/lib/risk-detector.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MAX_SLICES } from './plan/checks.js';
|
|
2
1
|
import { BLOCKER_TYPES, reviewFileName } from './review-reports.js';
|
|
2
|
+
import { FIX_ATTEMPTS } from '../../framework/hooks/shared/next-step.js';
|
|
3
3
|
import { isTestPath } from './graph/sanitize.js';
|
|
4
4
|
import { isTestArtifact } from './worktree/ignored-triage.js';
|
|
5
5
|
|
|
@@ -55,7 +55,7 @@ function firstOf(items) {
|
|
|
55
55
|
|
|
56
56
|
// Exportada porque approve/advance `auto` recusam pela mesma regra. Sem design de tela não conta,
|
|
57
57
|
// e o legado chega sem problemas: a auditoria o isenta. Checklist
|
|
58
|
-
// completo ainda deve o protótipo
|
|
58
|
+
// completo ainda deve o protótipo, os prints 1280/390 e os estados (`prototypes`).
|
|
59
59
|
export function uiuxChecklistPause(screenChecklist) {
|
|
60
60
|
if (screenChecklist?.mockups !== true) return null;
|
|
61
61
|
const problems = screenChecklist.problems ?? [];
|
|
@@ -95,6 +95,11 @@ export function reviewPause(signal) {
|
|
|
95
95
|
return `${untyped} bloqueador(es) sem tipo de evidência (${BLOCKER_TYPES.join(' | ')})`
|
|
96
96
|
+ ' — sem evidência tipada não é bloqueador: reclassifique como nota ou complete a evidência';
|
|
97
97
|
}
|
|
98
|
+
for (const [role, r] of present) {
|
|
99
|
+
if ((r.humanBlockerIds?.length ?? 0) > 0) {
|
|
100
|
+
return `${role} achou mudança irreversível que o plano não declara: ${firstOf(r.humanBlockerIds)} — o usuário decide`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
98
103
|
for (const [role, r] of present) {
|
|
99
104
|
if ((r.blockers ?? 0) > 0) return `${role} abriu ${r.blockers} bloqueador(es): ${firstOf(r.blockerIds ?? [])}`;
|
|
100
105
|
}
|
|
@@ -162,7 +167,78 @@ export function planChecksPause(status) {
|
|
|
162
167
|
return `${what} — rode \`morph-spec plan-check {feature}\` até zero erro antes do gate do plano`;
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
export const PAUSING_RUNGS = ['migrar', 'irreversível'];
|
|
171
|
+
const MIGRATION_PATH_RE = /(^|\/)migrations?\//i;
|
|
172
|
+
|
|
173
|
+
export function pausingRungs(checks) {
|
|
174
|
+
const list = Array.isArray(checks?.checks) ? checks.checks : [];
|
|
175
|
+
return list.filter((c) => PAUSING_RUNGS.includes(c.rung)).map((c) => ({ id: c.id, rung: c.rung }));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Só o arquivo que É o contrato. Webhook e endpoint ficam com o revisor (`irreversivel`): o caminho não diz se quebra quem consome.
|
|
179
|
+
const CONTRACT_PATH_RE = /(^|\/)(openapi|swagger)\.(ya?ml|json)$|\.proto$/i;
|
|
180
|
+
|
|
181
|
+
const touchedBy = (re) => (changedFiles = []) =>
|
|
182
|
+
changedFiles.map((f) => f.replaceAll('\\', '/')).filter((f) => re.test(f) && !isTestOnly(f));
|
|
183
|
+
export const migrationsTouched = touchedBy(MIGRATION_PATH_RE);
|
|
184
|
+
export const contractsTouched = touchedBy(CONTRACT_PATH_RE);
|
|
185
|
+
|
|
186
|
+
export const humanApprover = (approvedBy) => !!approvedBy && approvedBy !== 'auto' && !String(approvedBy).startsWith('policy:');
|
|
187
|
+
|
|
188
|
+
// Regra única do gate-check (pausa) e do approve/advance automáticos do plano (recusa).
|
|
189
|
+
export function rungPause(signals) {
|
|
190
|
+
const rungs = signals.rungs ?? [];
|
|
191
|
+
const named = rungs.map((r) => `${r.id} (${r.rung})`).join(', ');
|
|
192
|
+
if (signals.gate === 'plan') {
|
|
193
|
+
return rungs.length ? { signal: 'rung', reason: `${named} não se desfaz com revert — o usuário confirma antes de implementar` } : null;
|
|
194
|
+
}
|
|
195
|
+
if (signals.gate !== 'review') return null;
|
|
196
|
+
const migrations = signals.migrationsTouched ?? [];
|
|
197
|
+
if (migrations.length > 0 && rungs.length === 0) {
|
|
198
|
+
return { signal: 'rung-undeclared', reason: `a feature toca migration (${firstOf(migrations)}) e nenhum cenário declara \`degrau: migrar\` ou \`irreversível\`` };
|
|
199
|
+
}
|
|
200
|
+
const contracts = signals.contractsTouched ?? [];
|
|
201
|
+
if (contracts.length > 0 && !rungs.some((r) => r.rung === 'irreversível')) {
|
|
202
|
+
return { signal: 'rung-undeclared', reason: `a feature toca contrato externo (${firstOf(contracts)}) e nenhum cenário declara \`degrau: irreversível\`` };
|
|
203
|
+
}
|
|
204
|
+
if (rungs.length > 0 && !signals.planApprovedByHuman) return { signal: 'rung', reason: `${named} não passou por aprovação humana no plano` };
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const PLAN_CRITIQUE_REASONS = {
|
|
209
|
+
missing: 'sem `2-plan/critica.md`',
|
|
210
|
+
'no-hash': '`2-plan/critica.md` sem `**Plan-Hash:**`',
|
|
211
|
+
stale: '`2-plan/critica.md` é de outra versão do plano e nenhum `**Aplicado-em:**` cobre a atual',
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Regra única do gate-check (pausa) e do approve/advance automáticos do plano (recusa).
|
|
215
|
+
export function planCritiquePause(status) {
|
|
216
|
+
const what = PLAN_CRITIQUE_REASONS[status];
|
|
217
|
+
return what ? `${what} — despache a crítica em contexto fresco (morph-plan, references/critique.md) antes do gate do plano` : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Pauses the session clears by doing the work the reason names; the rest wait for the human.
|
|
221
|
+
const REPORT_SIGNALS = ['review-reports', 'screen-checklist', 'media-claims'];
|
|
222
|
+
export const SESSION_SIGNALS = ['verify-red', 'verify-missing', 'verify-stale', 'e2e-skip', 'plan-checks', 'plan-critique', 'uiux-checklist', ...REPORT_SIGNALS];
|
|
223
|
+
|
|
224
|
+
const reviewRounds = (review) => Math.max(0, ...Object.values(review?.reports ?? {}).map((r) => r.rounds ?? 0));
|
|
225
|
+
const humanBlockers = (review) => Object.values(review?.reports ?? {}).some((r) => (r.humanBlockerIds?.length ?? 0) > 0);
|
|
226
|
+
|
|
227
|
+
export function pauseResolver(signal, signals = {}) {
|
|
228
|
+
if (!SESSION_SIGNALS.includes(signal)) return 'human';
|
|
229
|
+
if (signal === 'verify-red' && (signals.failStreak ?? 0) >= FIX_ATTEMPTS) return 'human';
|
|
230
|
+
if (signal === 'e2e-skip' && classifyE2eSkip(signals.e2eReason) !== 'requested') return 'human';
|
|
231
|
+
if (REPORT_SIGNALS.includes(signal) && reviewRounds(signals.reviewReports) >= FIX_ATTEMPTS) return 'human';
|
|
232
|
+
if (signal === 'review-reports' && humanBlockers(signals.reviewReports)) return 'human';
|
|
233
|
+
return 'session';
|
|
234
|
+
}
|
|
235
|
+
|
|
165
236
|
export function shouldPauseGate(config, signals) {
|
|
237
|
+
const decision = pauseDecision(config, signals);
|
|
238
|
+
return decision.pause ? { ...decision, resolver: pauseResolver(decision.signal, signals) } : decision;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function pauseDecision(config, signals) {
|
|
166
242
|
const trust = config?.trust ?? 'auto';
|
|
167
243
|
|
|
168
244
|
if (trust === 'manual') {
|
|
@@ -301,15 +377,13 @@ export function shouldPauseGate(config, signals) {
|
|
|
301
377
|
if (signals.gate === 'plan') {
|
|
302
378
|
const reason = planChecksPause(signals.planChecks);
|
|
303
379
|
if (reason) return { pause: true, signal: 'plan-checks', reason };
|
|
380
|
+
const critique = planCritiquePause(signals.planCritique);
|
|
381
|
+
if (critique) return { pause: true, signal: 'plan-critique', reason: critique };
|
|
304
382
|
}
|
|
305
383
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
signal: 'slice-count',
|
|
310
|
-
reason: `${signals.sliceCount} fatias, acima de ${MAX_SLICES} — o plano é grande demais para uma feature`,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
384
|
+
// Por último: o usuário só é chamado pelo degrau quando a sessão não tem mais nada a corrigir.
|
|
385
|
+
const rung = rungPause(signals);
|
|
386
|
+
if (rung) return { pause: true, ...rung };
|
|
313
387
|
|
|
314
388
|
return { pause: false, signal: null, reason: '' };
|
|
315
389
|
}
|
|
@@ -346,8 +420,13 @@ export function getRiskSignals(signals) {
|
|
|
346
420
|
if (signals.gate === 'plan' && signals.planChecks && signals.planChecks !== 'fresh') {
|
|
347
421
|
out.push(`checks.json ${signals.planChecks}`);
|
|
348
422
|
}
|
|
349
|
-
if (
|
|
350
|
-
|
|
423
|
+
if (signals.gate === 'plan' && signals.planCritique) out.push(`crítica do plano: ${signals.planCritique}`);
|
|
424
|
+
if ((signals.rungs?.length ?? 0) > 0) out.push(`degrau: ${signals.rungs.map((r) => `${r.id} ${r.rung}`).join(', ')}`);
|
|
425
|
+
if (signals.gate === 'review' && (signals.migrationsTouched?.length ?? 0) > 0) {
|
|
426
|
+
out.push(`migration tocada: ${firstOf(signals.migrationsTouched)}`);
|
|
427
|
+
}
|
|
428
|
+
if (signals.gate === 'review' && (signals.contractsTouched?.length ?? 0) > 0) {
|
|
429
|
+
out.push(`contrato externo tocado: ${firstOf(signals.contractsTouched)}`);
|
|
351
430
|
}
|
|
352
431
|
if (signals.gate === 'review' && signals.mediaClaims?.featureGeneratesMedia === true) {
|
|
353
432
|
out.push(signals.mediaClaims.section === true
|
|
@@ -5,30 +5,19 @@
|
|
|
5
5
|
import { createHash } from 'crypto';
|
|
6
6
|
import { existsSync, readFileSync } from 'fs';
|
|
7
7
|
import { dirname, join } from 'path';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
8
|
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
10
9
|
import { verifierScreenChecklist } from '../review-reports.js';
|
|
11
10
|
import { LEGACY_MOCKUP_EXEMPTION, uiuxChecklistPause } from '../risk-detector.js';
|
|
12
11
|
|
|
13
|
-
const PACKAGE_AGENTS = fileURLToPath(new URL('../../../framework/agents.json', import.meta.url));
|
|
14
|
-
|
|
15
12
|
/** `match` roda sobre o rótulo normalizado; um rótulo que casa com vários itens responde a todos. */
|
|
16
13
|
export const SCREEN_CHECKLIST_ITEMS = [
|
|
17
|
-
{ id: '
|
|
18
|
-
{ id: '
|
|
19
|
-
{ id: '
|
|
20
|
-
{ id: '
|
|
21
|
-
{ id: '
|
|
22
|
-
{ id: 'vazio', label: 'Estado vazio', match: /\bvazio\b/ },
|
|
23
|
-
{ id: 'carregando', label: 'Estado carregando', match: /\bcarregando\b|\bcarregamento\b/ },
|
|
24
|
-
{ id: 'erro', label: 'Estado de erro', match: /\berro\b/ },
|
|
25
|
-
{ id: 'mobile', label: 'Mobile', match: /\bmobile\b/ },
|
|
26
|
-
{ id: 'abas', label: 'Abas', match: /\babas\b/ },
|
|
14
|
+
{ id: 'servidor', label: 'Lista no servidor', match: /\bpaginacao\b|\bordenacao\b|\bservidor\b/ },
|
|
15
|
+
{ id: 'estados', label: 'Estados vazio e carregando', match: /\bvazio\b|\bcarregando\b|\bcarregamento\b/ },
|
|
16
|
+
{ id: 'erro', label: 'Erro com próxima ação', match: /\berro\b/ },
|
|
17
|
+
{ id: 'acao', label: 'Uma ação primária', match: /\bprimaria\b/ },
|
|
18
|
+
{ id: 'mobile', label: 'Celular', match: /\bmobile\b|\bcelular\b/ },
|
|
27
19
|
];
|
|
28
20
|
|
|
29
|
-
// A superfície decide quais standards a tela lê, não um comportamento que a tela entregue exiba.
|
|
30
|
-
const NOT_VERIFIED_AT_REVIEW = new Set(['superficie']);
|
|
31
|
-
|
|
32
21
|
// As exatas, e as que começam adiando a decisão ("a definir depois", "TBD.").
|
|
33
22
|
const PLACEHOLDERS = new Set(['', '-', '--', '—', '–', '...', 'x', 'todo', 'definir', 'pendente']);
|
|
34
23
|
const PLACEHOLDER_PREFIX_RE = /^(a definir|tbd|\?|…)/;
|
|
@@ -141,30 +130,9 @@ const isNotApplicable = (answer) => NOT_APPLICABLE_RE.test(normalizeLabel(answer
|
|
|
141
130
|
|
|
142
131
|
const labelOf = (id) => SCREEN_CHECKLIST_ITEMS.find((item) => item.id === id)?.label ?? id;
|
|
143
132
|
|
|
144
|
-
// O vocabulário válido de `Superfície`: as `surfaces` da `ui-designer`, da cópia instalada ou do pacote.
|
|
145
|
-
// null quando a persona não declara o campo, e então o valor não é validado.
|
|
146
|
-
export function knownSurfaces(cwd) {
|
|
147
|
-
const surfaces = readAgentsJson(cwd)?.agents?.['ui-designer']?.surfaces;
|
|
148
|
-
return surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces) ? Object.keys(surfaces) : null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** The project's installed `agents.json`, else the package's; null when unreadable. */
|
|
152
|
-
export function readAgentsJson(cwd) {
|
|
153
|
-
for (const path of [join(cwd, '.morph', 'framework', 'agents.json'), PACKAGE_AGENTS]) {
|
|
154
|
-
if (!existsSync(path)) continue;
|
|
155
|
-
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
156
|
-
}
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function namesSurface(answer, surfaces) {
|
|
161
|
-
const norm = normalizeLabel(answer);
|
|
162
|
-
return surfaces.some((key) => new RegExp(`(^|[^a-z0-9-])${key}([^a-z0-9-]|$)`).test(norm));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
133
|
// Sem nenhuma `## Tela:`, só o formato antigo é legado; qualquer outro heading é "nenhuma tela
|
|
166
134
|
// declarada", senão trocar o heading pularia o checklist.
|
|
167
|
-
export function auditScreenChecklist(markdown
|
|
135
|
+
export function auditScreenChecklist(markdown) {
|
|
168
136
|
const parsed = parseScreens(markdown);
|
|
169
137
|
const problems = [];
|
|
170
138
|
if (parsed.length === 0) {
|
|
@@ -185,18 +153,25 @@ export function auditScreenChecklist(markdown, { surfaces = null } = {}) {
|
|
|
185
153
|
} else {
|
|
186
154
|
if (missing.length) problems.push(`tela "${screen.name}": falta ${missing.map(labelOf).join(', ')}`);
|
|
187
155
|
if (unanswered.length) problems.push(`tela "${screen.name}": sem resposta em ${unanswered.map(labelOf).join(', ')}`);
|
|
188
|
-
const surface = screen.answers.get('superficie');
|
|
189
|
-
if (surfaces?.length && surface !== undefined && !isPlaceholder(surface) && !isNotApplicable(surface)
|
|
190
|
-
&& !namesSurface(surface, surfaces)) {
|
|
191
|
-
problems.push(`tela "${screen.name}": Superfície "${surface}" não é uma das superfícies da ui-designer (${surfaces.join(', ')})`);
|
|
192
|
-
}
|
|
193
156
|
}
|
|
194
|
-
return { name: screen.name, hasChecklist: screen.hasChecklist, missing, unanswered, prototype: screen.prototype };
|
|
157
|
+
return { name: screen.name, hasChecklist: screen.hasChecklist, missing, unanswered, prototype: screen.prototype, states: statesOf(screen) };
|
|
195
158
|
});
|
|
196
159
|
return { legacy: false, screens, problems };
|
|
197
160
|
}
|
|
198
161
|
|
|
199
|
-
|
|
162
|
+
/** The states the prototype must switch to, by the checklist items that apply to the screen. */
|
|
163
|
+
export const PROTOTYPE_STATES = { estados: ['vazio', 'carregando'], erro: ['erro'] };
|
|
164
|
+
export const STATE_PRINT_WIDTH = 390;
|
|
165
|
+
|
|
166
|
+
function statesOf(screen) {
|
|
167
|
+
return Object.entries(PROTOTYPE_STATES).flatMap(([id, states]) => {
|
|
168
|
+
const answer = screen.answers.get(id);
|
|
169
|
+
return answer && !isPlaceholder(answer) && !isNotApplicable(answer) ? states : [];
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Linha **Protótipo:** da tela + o HTML + os prints do laço de autocrítica ao lado dele; cada estado
|
|
174
|
+
// que o checklist diz que se aplica tem `data-estado` no HTML e o print de celular.
|
|
200
175
|
export function auditPrototypes(screens, designDir) {
|
|
201
176
|
const problems = [];
|
|
202
177
|
for (const screen of screens) {
|
|
@@ -205,9 +180,19 @@ export function auditPrototypes(screens, designDir) {
|
|
|
205
180
|
continue;
|
|
206
181
|
}
|
|
207
182
|
const base = screen.prototype.replace(/\.html$/i, '');
|
|
208
|
-
const
|
|
183
|
+
const states = screen.states ?? [];
|
|
184
|
+
const expected = [
|
|
185
|
+
screen.prototype,
|
|
186
|
+
...PROTOTYPE_WIDTHS.map((width) => `${base}-${width}.png`),
|
|
187
|
+
...states.map((state) => `${base}-${state}-${STATE_PRINT_WIDTH}.png`),
|
|
188
|
+
];
|
|
209
189
|
const missing = expected.filter((rel) => !existsSync(join(designDir, rel)));
|
|
210
190
|
if (missing.length) problems.push(`tela "${screen.name}": falta ${missing.map((rel) => `1-design/${rel}`).join(', ')}`);
|
|
191
|
+
const html = existsSync(join(designDir, screen.prototype)) ? readFileSync(join(designDir, screen.prototype), 'utf8') : '';
|
|
192
|
+
const unswitchable = html ? states.filter((state) => !new RegExp(`data-estado\\s*=\\s*["']${state}["']`).test(html)) : [];
|
|
193
|
+
if (unswitchable.length) {
|
|
194
|
+
problems.push(`tela "${screen.name}": o protótipo não alterna para ${unswitchable.join(', ')} (falta \`data-estado="…"\` no seletor de estado)`);
|
|
195
|
+
}
|
|
211
196
|
}
|
|
212
197
|
return problems;
|
|
213
198
|
}
|
|
@@ -216,7 +201,7 @@ export function auditPrototypes(screens, designDir) {
|
|
|
216
201
|
export function collectScreenChecklist(cwd, featureName, gateName) {
|
|
217
202
|
const mockups = readMockups(cwd, featureName);
|
|
218
203
|
if (mockups === null) return { mockups: false, legacy: false, screens: [], problems: [] };
|
|
219
|
-
const audit = auditScreenChecklist(mockups
|
|
204
|
+
const audit = auditScreenChecklist(mockups);
|
|
220
205
|
const signal = { mockups: true, legacy: audit.legacy, screens: audit.screens.map((s) => s.name), problems: audit.problems };
|
|
221
206
|
if (audit.legacy) signal.exempt = LEGACY_MOCKUP_EXEMPTION;
|
|
222
207
|
if (gateName === 'uiux' && !audit.legacy) {
|
|
@@ -255,7 +240,7 @@ export function checklistCoverage(designMarkdown, reportSection) {
|
|
|
255
240
|
for (const screen of parseScreens(designMarkdown)) {
|
|
256
241
|
const done = covered.get(screenKey(screen.name)) ?? new Set();
|
|
257
242
|
const owed = [...screen.answers]
|
|
258
|
-
.filter(([
|
|
243
|
+
.filter(([, answer]) => !isPlaceholder(answer) && !isNotApplicable(answer))
|
|
259
244
|
.map(([id]) => id)
|
|
260
245
|
.filter((id) => !done.has(id));
|
|
261
246
|
if (owed.length) gaps.push(`${screen.name}: ${owed.map(labelOf).join(', ')}`);
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { readdirSync } from 'fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { ensureDotnetBuildIgnores } from '../../utils/file-copier.js';
|
|
4
4
|
import { getProjectTags } from '../stack-filter.js';
|
|
5
5
|
import { readMorphConfig } from '../tasks/test-runner.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
7
7
|
import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
|
|
8
8
|
|
|
9
|
-
export const DOTNET_IMPLEMENTERS = ['dotnet-senior', 'ef-modeler', 'maf-expert'];
|
|
10
|
-
|
|
11
9
|
const SKIPPED_DIRS = new Set([...WORKTREE_DIR_NAMES, 'node_modules', 'bin', 'obj']);
|
|
12
10
|
const DOTNET_PROJECT_FILE = /\.(?:csproj|slnx?)$/i;
|
|
11
|
+
const DOTNET_IN_PLAN = /\.(?:cs|csproj|slnx?)\b|\bdotnet\s+(?:test|build|new)\b/i;
|
|
13
12
|
const MAX_DEPTH = 6;
|
|
14
13
|
|
|
15
14
|
export function hasDotnetProjectFile(root, depth = 0) {
|
|
@@ -21,16 +20,16 @@ export function hasDotnetProjectFile(root, depth = 0) {
|
|
|
21
20
|
&& hasDotnetProjectFile(join(root, e.name), depth + 1));
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
function
|
|
23
|
+
function planMentionsDotnet(tree, featureName) {
|
|
25
24
|
if (!featureName) return false;
|
|
26
|
-
const
|
|
27
|
-
|
|
25
|
+
const path = getAbsoluteOutputPath(tree, featureName, 'plan');
|
|
26
|
+
try { return existsSync(path) && DOTNET_IN_PLAN.test(readFileSync(path, 'utf8')); } catch { return false; }
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
// The plan
|
|
29
|
+
// The plan is read because a greenfield tree has no stack or .csproj yet.
|
|
31
30
|
export function isDotnetProject(tree, { featureName, configRoot = tree } = {}) {
|
|
32
31
|
if (getProjectTags(readMorphConfig(configRoot)).includes('dotnet')) return true;
|
|
33
|
-
return
|
|
32
|
+
return planMentionsDotnet(tree, featureName) || hasDotnetProjectFile(tree);
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
/** Returns the `.gitignore` entries added; `[]` when not .NET or on any error. */
|
|
@@ -95,36 +95,16 @@ function addRun(slot, durationMs) {
|
|
|
95
95
|
const REVIEW_NAME = /^(?:ver|rev|sec|g3|gate3|eval|review|reviewer|security)\d*-/;
|
|
96
96
|
const isReviewAgent = (type) => REVIEW_AGENT_TYPES.has(type) || type === 'morph-evaluator' || REVIEW_NAME.test(String(type ?? ''));
|
|
97
97
|
|
|
98
|
-
// Same split as the test-budget hook: review first, then an unfiltered verify, then the round count.
|
|
99
98
|
// A run refused by the full heavy queue ran no test.
|
|
100
99
|
function testOutsideVerify(bashEvents) {
|
|
101
|
-
const out = {
|
|
102
|
-
total: newTimeSlot(),
|
|
103
|
-
subagentVerify: newTimeSlot(),
|
|
104
|
-
implementerRound3Plus: newTimeSlot(),
|
|
105
|
-
reviewTests: { ...newTimeSlot(), byPersona: newTimeSlot() },
|
|
106
|
-
};
|
|
107
|
-
const byAgent = new Map();
|
|
100
|
+
const out = { total: newTimeSlot(), reviewTests: { ...newTimeSlot(), byPersona: newTimeSlot() } };
|
|
108
101
|
for (const e of bashEvents) {
|
|
109
102
|
if (e.event === 'PostToolUseFailure' && e.exitCode === QUEUE_FULL_EXIT) continue;
|
|
110
|
-
|
|
111
|
-
if (!verify && !testRunKind(e.command)) continue;
|
|
103
|
+
if (!morphVerifyCall(e.command) && !testRunKind(e.command)) continue;
|
|
112
104
|
addRun(out.total, e.durationMs);
|
|
113
|
-
if (!e.agentId) continue;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (REVIEW_AGENT_TYPES.has(e.agentType)) addRun(out.reviewTests.byPersona, e.durationMs);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (!byAgent.has(e.agentId)) byAgent.set(e.agentId, []);
|
|
120
|
-
byAgent.get(e.agentId).push({ e, unfilteredVerify: Boolean(verify && !verify.filtered) });
|
|
121
|
-
}
|
|
122
|
-
for (const runs of byAgent.values()) {
|
|
123
|
-
runs.sort((a, b) => String(a.e.ts ?? '').localeCompare(String(b.e.ts ?? '')));
|
|
124
|
-
runs.forEach(({ e, unfilteredVerify }, round) => {
|
|
125
|
-
if (unfilteredVerify) addRun(out.subagentVerify, e.durationMs);
|
|
126
|
-
else if (round >= 2) addRun(out.implementerRound3Plus, e.durationMs);
|
|
127
|
-
});
|
|
105
|
+
if (!e.agentId || !isReviewAgent(e.agentType)) continue;
|
|
106
|
+
addRun(out.reviewTests, e.durationMs);
|
|
107
|
+
if (REVIEW_AGENT_TYPES.has(e.agentType)) addRun(out.reviewTests.byPersona, e.durationMs);
|
|
128
108
|
}
|
|
129
109
|
return out;
|
|
130
110
|
}
|
|
@@ -164,7 +144,7 @@ function reuseStateOf(e) {
|
|
|
164
144
|
return { state: 'absent', reason: null };
|
|
165
145
|
}
|
|
166
146
|
|
|
167
|
-
const newGateSlot = () => ({ decisions: 0, paused: 0, autoPassed: 0, approvedAfterPause: 0, reworkedAfterPause: 0, unapproved: 0, bySignal: {} });
|
|
147
|
+
const newGateSlot = () => ({ decisions: 0, paused: 0, pausedForHuman: 0, pausedUnattributed: 0, autoPassed: 0, approvedAfterPause: 0, reworkedAfterPause: 0, unapproved: 0, bySignal: {} });
|
|
168
148
|
|
|
169
149
|
// A pause stays open until the next approve or a decision that no longer pauses on the same signal:
|
|
170
150
|
// re-running gate-check over the same unchanged pause is not rework.
|
|
@@ -173,7 +153,7 @@ function trackGate(gates, open, e) {
|
|
|
173
153
|
const slot = gates[e.gate] ||= newGateSlot();
|
|
174
154
|
const key = `${e.feature || ''}\u0000${e.gate}`;
|
|
175
155
|
const pending = open.get(key);
|
|
176
|
-
const bySignal = (signal) => slot.bySignal[signal || 'desconhecido'] ||= { paused: 0, approvedAfterPause: 0, reworkedAfterPause: 0 };
|
|
156
|
+
const bySignal = (signal) => slot.bySignal[signal || 'desconhecido'] ||= { paused: 0, pausedForHuman: 0, pausedUnattributed: 0, approvedAfterPause: 0, reworkedAfterPause: 0 };
|
|
177
157
|
const credit = (field) => {
|
|
178
158
|
slot[field] += 1;
|
|
179
159
|
bySignal(pending.signal)[field] += 1;
|
|
@@ -189,12 +169,30 @@ function trackGate(gates, open, e) {
|
|
|
189
169
|
return;
|
|
190
170
|
}
|
|
191
171
|
slot.decisions += 1;
|
|
192
|
-
|
|
172
|
+
const resolver = e.resolver ?? 'human';
|
|
173
|
+
if (pending && e.pause && e.signal === pending.signal) {
|
|
174
|
+
// Same signal, new owner: the session ran out of attempts and the pause escalated to the human.
|
|
175
|
+
if (pending.resolver === 'session' && resolver === 'human') {
|
|
176
|
+
slot.pausedForHuman += 1;
|
|
177
|
+
bySignal(pending.signal).pausedForHuman += 1;
|
|
178
|
+
}
|
|
179
|
+
pending.resolver = resolver;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
193
182
|
if (pending) credit(e.source === 'manual' && !e.pause ? 'approvedAfterPause' : 'reworkedAfterPause');
|
|
194
183
|
if (e.pause) {
|
|
195
184
|
slot.paused += 1;
|
|
196
185
|
bySignal(e.signal).paused += 1;
|
|
197
|
-
|
|
186
|
+
if (resolver === 'human') {
|
|
187
|
+
slot.pausedForHuman += 1;
|
|
188
|
+
bySignal(e.signal).pausedForHuman += 1;
|
|
189
|
+
}
|
|
190
|
+
// Written before the resolver existed: counted as human above, but nobody recorded who cleared it.
|
|
191
|
+
if (e.resolver === undefined) {
|
|
192
|
+
slot.pausedUnattributed += 1;
|
|
193
|
+
bySignal(e.signal).pausedUnattributed += 1;
|
|
194
|
+
}
|
|
195
|
+
open.set(key, { signal: e.signal, resolver });
|
|
198
196
|
} else {
|
|
199
197
|
slot.autoPassed += 1;
|
|
200
198
|
open.delete(key);
|
|
@@ -123,16 +123,8 @@ export function readReuseVerdicts(projectPath, featureName) {
|
|
|
123
123
|
return verdicts;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
// ao commit; `git status` pega o que o plano não previu.
|
|
128
|
-
export function collectChangedFiles(projectPath, featureName) {
|
|
126
|
+
export function collectChangedFiles(projectPath) {
|
|
129
127
|
const files = new Set();
|
|
130
|
-
for (const slice of readPlanChecks(projectPath, featureName).checks?.slices ?? []) {
|
|
131
|
-
for (const file of slice.files ?? []) {
|
|
132
|
-
if (typeof file === 'string') files.add(file.replace(/\\/g, '/'));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
128
|
try {
|
|
137
129
|
const out = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], {
|
|
138
130
|
cwd: projectPath,
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
evalEmptyRunProblem, evalColdCacheWarning,
|
|
22
22
|
} from '../tasks/eval-node.js';
|
|
23
23
|
import { runValidation } from './validation-runner.js';
|
|
24
|
-
import { checkOutputsTraceability } from './traceability-check.js';
|
|
25
24
|
import { createHeavyGate } from '../heavy/gate.js';
|
|
26
25
|
|
|
27
26
|
const DEFAULT_BUILD_TIMEOUT = 1_800_000;
|
|
@@ -70,7 +69,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
70
69
|
_runValidation = runValidation,
|
|
71
70
|
_runE2eCheck = runE2eCheck,
|
|
72
71
|
_e2eApplicability = e2eApplicability,
|
|
73
|
-
_checkOutputsTraceability = checkOutputsTraceability,
|
|
74
72
|
} = options;
|
|
75
73
|
|
|
76
74
|
const stack = readStack(projectPath);
|
|
@@ -170,10 +168,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
170
168
|
appendTestsFeedback(feedback, nodes.tests);
|
|
171
169
|
}
|
|
172
170
|
|
|
173
|
-
// Outside `nodes` on purpose: status comes only from the nodes, so a traceability
|
|
174
|
-
// warning never turns red.
|
|
175
|
-
const traceability = _checkOutputsTraceability(projectPath, featureName);
|
|
176
|
-
|
|
177
171
|
const baseline = recordBaseline ? recordBaselineFrom(projectPath, nodes.tests) : null;
|
|
178
172
|
if (baseline && !baseline.recorded) {
|
|
179
173
|
feedback.push(`[baseline] não gravada: ${baseline.reason}`);
|
|
@@ -193,7 +187,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
|
|
|
193
187
|
status,
|
|
194
188
|
nodes,
|
|
195
189
|
feedback,
|
|
196
|
-
traceability,
|
|
197
190
|
...(baseline ? { baseline } : {}),
|
|
198
191
|
};
|
|
199
192
|
}
|
|
@@ -18,12 +18,12 @@ import {
|
|
|
18
18
|
import { listClaims } from '../../../framework/hooks/shared/claim-registry.js';
|
|
19
19
|
import { buildPrIndex } from '../git/pr-index.js';
|
|
20
20
|
import { pendingPrFeatures } from '../git/pending-prs.js';
|
|
21
|
-
import {
|
|
21
|
+
import { implementProgress, implementProgressFrom } from '../../../framework/hooks/shared/report-progress.js';
|
|
22
22
|
|
|
23
23
|
const counts = (progress) => (progress ? { done: progress.done, total: progress.total } : null);
|
|
24
24
|
|
|
25
|
-
// A root-resident feature's
|
|
26
|
-
function
|
|
25
|
+
// A root-resident feature's report lives on `morph/{f}`; the root tree only has the pre-work copy.
|
|
26
|
+
function scenarioCounts(featureDir, { root, feature, featureBranch } = {}) {
|
|
27
27
|
if (featureBranch && root && feature) {
|
|
28
28
|
const show = (rel) => {
|
|
29
29
|
try {
|
|
@@ -32,21 +32,10 @@ function sliceCounts(featureDir, { root, feature, featureBranch } = {}) {
|
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
-
const onBranch =
|
|
35
|
+
const onBranch = implementProgressFrom(show('2-plan/checks.json'), show('3-implement/report.md'), (id) => show(`3-implement/reports/${id}.md`));
|
|
36
36
|
if (onBranch) return counts(onBranch);
|
|
37
37
|
}
|
|
38
|
-
return counts(
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function handoffInfo(featureDir) {
|
|
42
|
-
try {
|
|
43
|
-
const path = join(featureDir, 'handoff.md');
|
|
44
|
-
if (!existsSync(path)) return { exists: false, generatedAt: null };
|
|
45
|
-
const generatedAt = readFileSync(path, 'utf8').match(/gerado em (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/)?.[1] || null;
|
|
46
|
-
return { exists: true, generatedAt };
|
|
47
|
-
} catch {
|
|
48
|
-
return { exists: false, generatedAt: null };
|
|
49
|
-
}
|
|
38
|
+
return counts(implementProgress(featureDir));
|
|
50
39
|
}
|
|
51
40
|
|
|
52
41
|
function lastCommitRelative(root, branch) {
|
|
@@ -132,11 +121,10 @@ function buildEntry(feature, { location, path, branch, root, allocations, blockS
|
|
|
132
121
|
location,
|
|
133
122
|
path,
|
|
134
123
|
branch,
|
|
135
|
-
|
|
124
|
+
scenarios: scenarioCounts(featureDir, { root, feature, featureBranch }),
|
|
136
125
|
env: alloc && Number.isInteger(alloc.base)
|
|
137
126
|
? { base: alloc.base, range: [alloc.base, alloc.base + blockSize - 1] }
|
|
138
127
|
: null,
|
|
139
|
-
handoff: handoffInfo(featureDir),
|
|
140
128
|
lastCommit: location === 'worktree' && branch ? lastCommitRelative(root, branch) : null,
|
|
141
129
|
dirty: location === 'worktree' ? dirtyCount(path) : null,
|
|
142
130
|
remote: buildRemote(feature, branch, heads, prs),
|
|
@@ -221,9 +209,8 @@ export function scanFleet(cwd = process.cwd()) {
|
|
|
221
209
|
location: 'pr-pending',
|
|
222
210
|
path: null,
|
|
223
211
|
branch: p.branch,
|
|
224
|
-
|
|
212
|
+
scenarios: null,
|
|
225
213
|
env: null,
|
|
226
|
-
handoff: { exists: false, generatedAt: null },
|
|
227
214
|
lastCommit: null,
|
|
228
215
|
dirty: null,
|
|
229
216
|
remote: { branch: p.branch, pushed: true, sha: null, pr: { number: p.number, state: p.state } },
|
|
@@ -13,40 +13,8 @@ import { buildPrIndex } from '../git/pr-index.js';
|
|
|
13
13
|
import { classifyBranch, commitsAhead, resolveBaseRef } from '../git/branch-classifier.js';
|
|
14
14
|
import { auditDockerStacks } from './docker-audit.js';
|
|
15
15
|
import { auditFinishBackups } from './finish-backup.js';
|
|
16
|
-
import { planAgentReap } from './agent-reaper.js';
|
|
17
16
|
|
|
18
17
|
const norm = (p) => resolve(p).replace(/\\/g, '/');
|
|
19
|
-
const AGENT_TASK = /^agent-[0-9a-f]+$/;
|
|
20
|
-
|
|
21
|
-
const AGENT_FIX = {
|
|
22
|
-
'stopped-clean': 'morph-spec worktree reap (ou `morph-spec doctor --worktrees --prune`)',
|
|
23
|
-
'orphan-branch': 'morph-spec worktree reap (ou `morph-spec doctor --worktrees --prune`)',
|
|
24
|
-
'directory-missing': 'morph-spec doctor --worktrees --prune',
|
|
25
|
-
locked: 'a árvore está com `git worktree lock`; `git worktree unlock <path>` libera, se o lock já não vale',
|
|
26
|
-
unknown: 'não foi possível provar que é seguro remover — deixe como está',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function agentLeftoverIssue(item) {
|
|
30
|
-
const where = item.path ?? item.branch;
|
|
31
|
-
const reapable = item.state === 'reapable';
|
|
32
|
-
const fix = AGENT_FIX[item.reason]
|
|
33
|
-
?? (item.reason === 'exclusive-commits'
|
|
34
|
-
? `integre a branch (\`git merge --no-ff ${item.branch}\` no orquestrador) antes de remover — ela é o único exemplar`
|
|
35
|
-
: item.reason === 'dirty'
|
|
36
|
-
? `confira o trabalho na árvore (commite ou descarte) e rode \`morph-spec worktree remove --target "${item.path}"\``
|
|
37
|
-
: `o harness não registrou o fim do agente; se ele já terminou, \`morph-spec worktree remove --target "${item.path}"\``);
|
|
38
|
-
return {
|
|
39
|
-
type: 'agent-worktree-leftover',
|
|
40
|
-
severity: 'info',
|
|
41
|
-
...(item.path ? { path: item.path } : {}),
|
|
42
|
-
branch: item.branch,
|
|
43
|
-
reapable,
|
|
44
|
-
detail: reapable
|
|
45
|
-
? `${where} é sobra de sub-agent que já parou, sem trabalho pendente`
|
|
46
|
-
: `${where} é sobra de sub-agent com trabalho a preservar (${item.reason}${item.detail ? `: ${item.detail}` : ''})`,
|
|
47
|
-
fix,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
18
|
|
|
51
19
|
function knownFeatures(root) {
|
|
52
20
|
const names = new Set();
|
|
@@ -197,7 +165,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
197
165
|
}
|
|
198
166
|
|
|
199
167
|
if (task) {
|
|
200
|
-
if (AGENT_TASK.test(task)) continue; // reported once, by the agent plan below
|
|
201
168
|
const work = unintegratedWork(root, entry.branch, prIndex);
|
|
202
169
|
if (work.unintegrated) {
|
|
203
170
|
issues.push({
|
|
@@ -262,10 +229,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
262
229
|
}
|
|
263
230
|
}
|
|
264
231
|
|
|
265
|
-
for (const item of planAgentReap(root, { cwd }).items) {
|
|
266
|
-
if (item.state === 'reapable' || item.state === 'preserved') issues.push(agentLeftoverIssue(item));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
232
|
for (const d of dirsOnDisk(root)) {
|
|
270
233
|
if (registeredPaths.has(d.path)) continue;
|
|
271
234
|
issues.push({
|