@polymorphism-tech/morph-spec 9.8.0 → 10.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +12 -10
- package/README.md +4 -4
- package/bin/morph-spec.js +40 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +28 -36
- package/docs/QUICKSTART.md +31 -191
- 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-preflight.md +3 -3
- package/framework/commands/morph-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +84 -95
- 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/mcp/registry.json +4 -4
- package/framework/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +10 -2
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/data-standards.md +1 -1
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- package/framework/rules/neon-auth-standards.md +1 -1
- package/framework/rules/neon-standards.md +3 -3
- 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 +39 -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 +49 -109
- package/framework/skills/core/morph-plan/references/critique.md +43 -0
- package/framework/skills/core/morph-plan/references/interview.md +37 -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 +51 -28
- package/framework/standards/STANDARDS.meta.json +16 -19
- 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 +7 -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 +70 -0
- package/framework/standards/backend/database/postgresql/database.md +22 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +12 -13
- 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/standards/infrastructure/docker/local-compose-isolation.md +2 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
- package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
- package/framework/standards/integration/mcp/mcp-tools.md +20 -20
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +15 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
- 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 +2 -2
- package/src/commands/state/create.js +1 -6
- package/src/commands/state/gate-check.js +38 -31
- 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/detectors/index.js +1 -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 +3 -3
- 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 +61 -109
- 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/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { getFeature } from '../../core/state/state-manager.js';
|
|
3
|
-
import { shouldPauseGate, getRiskSignals, classifyE2eSkip, planChecksPause, securityReviewFor } from '../../lib/risk-detector.js';
|
|
3
|
+
import { shouldPauseGate, getRiskSignals, classifyE2eSkip, planChecksPause, securityReviewFor, pausingRungs, migrationsTouched, contractsTouched, humanApprover, rungPause, planCritiquePause } from '../../lib/risk-detector.js';
|
|
4
|
+
import { readPlanCritique } from '../../lib/plan/critique.js';
|
|
4
5
|
import { reviewReports, reviewSignal, requiredReviewRoles, verifierMediaSection } from '../../lib/review-reports.js';
|
|
5
6
|
import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
|
|
6
7
|
import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
|
|
@@ -9,33 +10,22 @@ import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } fro
|
|
|
9
10
|
import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
|
|
10
11
|
import { readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
11
12
|
import { readPlanChecks } from '../../lib/plan/checks.js';
|
|
13
|
+
import { featureGeneratesMedia } from '../../lib/plan/media.js';
|
|
14
|
+
import { readFileSync, existsSync } from 'fs';
|
|
15
|
+
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
12
16
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
13
17
|
|
|
18
|
+
export { featureGeneratesMedia, MEDIA_STANDARD_IDS, standardIdOf } from '../../lib/plan/media.js';
|
|
19
|
+
|
|
14
20
|
// Only signals NOT derivable from disk may be overridden: overriding a disk fact (e.g. `workType`
|
|
15
21
|
// away from hotfix) would silently defeat the policy this command makes deterministic.
|
|
16
22
|
const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence'];
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'ai-agents-media-pipeline',
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
// O plano escreve o standard pelo id ou pelo caminho (com ou sem `framework/`/`standards/`).
|
|
25
|
-
export function standardIdOf(value) {
|
|
26
|
-
return String(value ?? '').trim().replace(/\\/g, '/')
|
|
27
|
-
.replace(/^(\.morph\/)?(framework\/)?(standards\/)?/, '')
|
|
28
|
-
.replace(/\.md$/, '')
|
|
29
|
-
.replace(/\//g, '-');
|
|
24
|
+
function readPlanText(cwd, featureName) {
|
|
25
|
+
const path = getAbsoluteOutputPath(cwd, featureName, 'plan');
|
|
26
|
+
try { return existsSync(path) ? readFileSync(path, 'utf8') : ''; } catch { return ''; }
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
// Decide se um guardrail arma: um predicado que sempre devolve `false` desarma em silêncio a
|
|
33
|
-
// pausa de mídia da review e a rota opus.
|
|
34
|
-
export function featureGeneratesMedia(slices) {
|
|
35
|
-
if (!Array.isArray(slices)) return false;
|
|
36
|
-
return slices.some((s) => Array.isArray(s?.standards)
|
|
37
|
-
&& s.standards.some((id) => MEDIA_STANDARD_IDS.includes(standardIdOf(id))));
|
|
38
|
-
}
|
|
39
29
|
|
|
40
30
|
// `declared: false` (nenhum caminho declarado: regra inerte) e `touched: []` (declarou, nada
|
|
41
31
|
// tocado) não podem colapsar num array vazio. COLETA, NUNCA DECIDE: política no coletor cria
|
|
@@ -69,8 +59,12 @@ export function collectReviewSignal(cwd, featureName, feature, planChecks = read
|
|
|
69
59
|
}
|
|
70
60
|
|
|
71
61
|
/** Why an automatic signature may not close the plan gate: the rule that makes gate-check pause it. */
|
|
72
|
-
export function planAutoApprovalBlock(cwd, featureName) {
|
|
73
|
-
const
|
|
62
|
+
export function planAutoApprovalBlock(cwd, featureName, feature) {
|
|
63
|
+
const planChecks = readPlanChecks(cwd, featureName);
|
|
64
|
+
const workType = feature?.workType || 'feature';
|
|
65
|
+
const reason = planChecksPause(planChecks.status)
|
|
66
|
+
?? planCritiquePause(readPlanCritique(cwd, featureName, { workType, planChecks }))
|
|
67
|
+
?? rungPause({ gate: 'plan', rungs: pausingRungs(planChecks.checks) })?.reason;
|
|
74
68
|
return reason ? reason.replace('{feature}', featureName) : null;
|
|
75
69
|
}
|
|
76
70
|
|
|
@@ -79,7 +73,6 @@ export function planAutoApprovalBlock(cwd, featureName) {
|
|
|
79
73
|
// waiver from missing proof.
|
|
80
74
|
export function collectSignals(featureName, feature, cwd, gateName) {
|
|
81
75
|
const planChecks = readPlanChecks(cwd, featureName);
|
|
82
|
-
const slices = planChecks.checks?.slices ?? null;
|
|
83
76
|
const defaultBranch = detectDefaultBranch(cwd);
|
|
84
77
|
const sha = getHeadSha(cwd);
|
|
85
78
|
const verification = feature.verification || null;
|
|
@@ -88,12 +81,15 @@ export function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
88
81
|
// Disk-derived, never overridable: a caller cannot spoof which gate is being decided.
|
|
89
82
|
gate: gateName,
|
|
90
83
|
workType: feature.workType || 'feature',
|
|
91
|
-
sliceCount: slices ? slices.length : null,
|
|
92
84
|
standardsViolations: [],
|
|
93
85
|
designSystemDivergence: false,
|
|
94
86
|
};
|
|
95
87
|
|
|
96
|
-
if (gateName === 'plan')
|
|
88
|
+
if (gateName === 'plan') {
|
|
89
|
+
signals.planChecks = planChecks.status;
|
|
90
|
+
signals.planCritique = readPlanCritique(cwd, featureName, { workType: signals.workType, planChecks });
|
|
91
|
+
}
|
|
92
|
+
if (gateName === 'plan' || gateName === 'review') signals.rungs = pausingRungs(planChecks.checks);
|
|
97
93
|
|
|
98
94
|
// Só onde o checklist decide algo: no fecho do design e no ship.
|
|
99
95
|
if (gateName === 'uiux' || gateName === 'review') {
|
|
@@ -104,13 +100,20 @@ export function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
104
100
|
signals.reviewReports = collectReviewSignal(cwd, featureName, feature, planChecks);
|
|
105
101
|
signals.mediaClaims = {
|
|
106
102
|
section: verifierMediaSection(cwd, featureName),
|
|
107
|
-
featureGeneratesMedia: featureGeneratesMedia(
|
|
103
|
+
featureGeneratesMedia: featureGeneratesMedia(readPlanText(cwd, featureName)),
|
|
108
104
|
};
|
|
105
|
+
let changed = [];
|
|
106
|
+
try { changed = changedPathsAgainst(defaultBranch, cwd); } catch { changed = []; }
|
|
107
|
+
signals.migrationsTouched = migrationsTouched(changed);
|
|
108
|
+
signals.contractsTouched = contractsTouched(changed);
|
|
109
|
+
signals.planApprovedByHuman = feature.approvalGates?.plan?.approved === true
|
|
110
|
+
&& humanApprover(feature.approvalGates.plan.approvedBy);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
// verificationStale é fail-open: stamp sem sha (checkout sem git) ou ausente lê como NOT stale.
|
|
112
114
|
signals.verification = verification;
|
|
113
115
|
if (verification?.status) signals.verificationStatus = verification.status;
|
|
116
|
+
signals.failStreak = feature.verifications?.feature?.failStreak ?? 0;
|
|
114
117
|
signals.verificationStale = treeMovedSince(cwd, verification?.sha, sha);
|
|
115
118
|
|
|
116
119
|
// Separado do rollup: um `skip` NÃO reprova `verification.status`. `null` é DESCONHECIDO, nunca
|
|
@@ -189,10 +192,12 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
189
192
|
const computed = shouldPauseGate({ trust }, signals);
|
|
190
193
|
const manual = feature.gateDecisions?.[gateName];
|
|
191
194
|
const manualPause = manual?.source === 'manual' && manual.pause === true;
|
|
192
|
-
const { pause, reason, signal } = manualPause
|
|
195
|
+
const { pause, reason, signal, resolver } = manualPause
|
|
196
|
+
? { pause: true, reason: manual.reason, signal: 'manual', resolver: 'human' }
|
|
197
|
+
: computed;
|
|
193
198
|
|
|
194
199
|
const entry = await persistGateDecision(featureName, gateName, {
|
|
195
|
-
pause, reason, signal, signals, trust, cwd, source: manualPause ? 'manual' : 'gate-check',
|
|
200
|
+
pause, reason, signal, resolver, signals, trust, cwd, source: manualPause ? 'manual' : 'gate-check',
|
|
196
201
|
});
|
|
197
202
|
|
|
198
203
|
if (options.json) {
|
|
@@ -201,9 +206,11 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
|
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
const allSignals = getRiskSignals(signals);
|
|
204
|
-
console.log(entry.pause
|
|
205
|
-
? chalk.
|
|
206
|
-
:
|
|
209
|
+
console.log(!entry.pause
|
|
210
|
+
? chalk.green(`\n▶️ Gate "${gateName}" — auto-pass (${featureName})`)
|
|
211
|
+
: entry.resolver === 'session'
|
|
212
|
+
? chalk.yellow(`\n🔧 Gate "${gateName}" — PAUSE, resolver: session (${featureName}): do what the reason says and run gate-check again`)
|
|
213
|
+
: chalk.yellow(`\n⏸️ Gate "${gateName}" — PAUSE, resolver: human (${featureName})`));
|
|
207
214
|
console.log(chalk.gray(` reason: ${entry.reason} | trust: ${entry.trust} | decidedAt: ${entry.decidedAt}`));
|
|
208
215
|
if (manualPause) {
|
|
209
216
|
console.log(chalk.gray(` pausa manual mantida (sinais calculados: ${computed.reason}); só \`morph-spec gate-decision ${featureName} ${gateName} --pause false\` a libera`));
|
|
@@ -32,9 +32,10 @@ export function readTrustFromConfig() {
|
|
|
32
32
|
* The ONE write path of `gateDecisions.{gate}`, shared by `gate-decision` and `gate-check`.
|
|
33
33
|
* Does not validate `gateName`. A uiux decision records the `mockupsHash` it saw.
|
|
34
34
|
*/
|
|
35
|
-
export async function persistGateDecision(featureName, gateName, { pause, reason, signal, signals = {}, trust, cwd = process.cwd(), source } = {}) {
|
|
35
|
+
export async function persistGateDecision(featureName, gateName, { pause, reason, signal, resolver, signals = {}, trust, cwd = process.cwd(), source } = {}) {
|
|
36
36
|
const entry = {
|
|
37
37
|
pause,
|
|
38
|
+
resolver: pause ? resolver ?? 'human' : null,
|
|
38
39
|
reason: reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
|
|
39
40
|
signals,
|
|
40
41
|
trust: trust || readTrustFromConfig(),
|
|
@@ -44,7 +45,7 @@ export async function persistGateDecision(featureName, gateName, { pause, reason
|
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
|
|
47
|
-
recordGateEvent(cwd, { feature: featureName, gate: gateName, step: 'decision', pause, signal, source });
|
|
48
|
+
recordGateEvent(cwd, { feature: featureName, gate: gateName, step: 'decision', pause, signal, source, resolver: entry.resolver });
|
|
48
49
|
return entry;
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -20,5 +20,6 @@ export function nextCommand(featureName, options = {}) {
|
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
console.log(formatNext(next));
|
|
23
|
+
for (const line of next.how ?? []) console.log(` ${line}`);
|
|
23
24
|
if (owner.tree !== process.cwd()) console.log(chalk.gray(` árvore dona: ${owner.tree}`));
|
|
24
25
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { buildRecap, changeSummary, recapScenes, renderRecapHtml } from '../../lib/recap/recap.js';
|
|
6
|
+
import { narrateWithPiper, resolvePiper } from '../../lib/recap/piper.js';
|
|
7
|
+
|
|
8
|
+
export const RECAP_FILES = { html: 'recap.html', data: 'recap.json' };
|
|
9
|
+
|
|
10
|
+
/** Writes `recap.html` + `recap.json` into `targetDir` from the live feature folder. Never throws. */
|
|
11
|
+
export function writeRecap(featureDir, targetDir, feature, { cwd }) {
|
|
12
|
+
try {
|
|
13
|
+
const data = buildRecap(featureDir, feature, { changes: changeSummary(cwd) });
|
|
14
|
+
writeFileSync(join(targetDir, RECAP_FILES.data), JSON.stringify(data, null, 2));
|
|
15
|
+
writeFileSync(join(targetDir, RECAP_FILES.html), renderRecapHtml(data));
|
|
16
|
+
return { ok: true, path: join(targetDir, RECAP_FILES.html) };
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return { ok: false, reason: err.message };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Active feature: rebuilt from its folder and the branch diff. Archived: the data the finish froze,
|
|
23
|
+
// since the archive keeps neither the reports nor the branch.
|
|
24
|
+
export function recapCommand(feature, options = {}) {
|
|
25
|
+
const cwd = options.path || process.cwd();
|
|
26
|
+
const active = join(cwd, '.morph', 'features', feature);
|
|
27
|
+
const archived = join(cwd, '.morph', 'archive', feature);
|
|
28
|
+
let data;
|
|
29
|
+
let dir;
|
|
30
|
+
if (existsSync(active)) {
|
|
31
|
+
dir = active;
|
|
32
|
+
data = buildRecap(active, feature, { changes: changeSummary(cwd) });
|
|
33
|
+
writeFileSync(join(dir, RECAP_FILES.data), JSON.stringify(data, null, 2));
|
|
34
|
+
} else if (existsSync(join(archived, RECAP_FILES.data))) {
|
|
35
|
+
dir = archived;
|
|
36
|
+
data = JSON.parse(readFileSync(join(archived, RECAP_FILES.data), 'utf8'));
|
|
37
|
+
} else {
|
|
38
|
+
const payload = { ok: false, reason: existsSync(archived) ? 'archived-without-recap' : 'feature-not-found' };
|
|
39
|
+
if (options.json) console.log(JSON.stringify(payload));
|
|
40
|
+
else console.error(chalk.yellow(`\n○ ${payload.reason === 'feature-not-found' ? `Feature "${feature}" não existe.` : `"${feature}" foi arquivada antes do recap existir.`}\n`));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let narration = 'sistema';
|
|
45
|
+
let path = join(dir, RECAP_FILES.html);
|
|
46
|
+
let note = null;
|
|
47
|
+
if (options.piper) {
|
|
48
|
+
const piper = resolvePiper();
|
|
49
|
+
const audio = piper.ok ? narrateWithPiper(recapScenes(data), piper) : null;
|
|
50
|
+
if (audio) {
|
|
51
|
+
// Audio is megabytes: it goes to a temp file, never into the committed archive.
|
|
52
|
+
path = join(tmpdir(), `morph-recap-${feature}.html`);
|
|
53
|
+
writeFileSync(path, renderRecapHtml(data, { audio }));
|
|
54
|
+
narration = 'piper';
|
|
55
|
+
} else {
|
|
56
|
+
note = piper.ok ? 'o piper falhou numa cena — ficou a voz do sistema' : piper.reason;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (narration === 'sistema') writeFileSync(path, renderRecapHtml(data));
|
|
60
|
+
|
|
61
|
+
if (options.json) {
|
|
62
|
+
console.log(JSON.stringify({ ok: true, path, narration, note }));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log(chalk.green(`\n✅ Recap de ${feature}: ${path}`));
|
|
66
|
+
console.log(chalk.gray(` narração: ${narration === 'piper' ? 'Piper (áudio embutido)' : 'voz do sistema, pelo navegador'}${note ? ` — ${note}` : ''}\n`));
|
|
67
|
+
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
// Judged from observed evidence, never the agent's word: did the WORK land, independently of
|
|
2
|
-
// the REPORT? The `tree` block is information only and never changes the verdict.
|
|
3
|
-
|
|
4
1
|
import chalk from 'chalk';
|
|
5
|
-
import { existsSync, readFileSync
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
3
|
import { join } from 'path';
|
|
7
|
-
import {
|
|
8
|
-
import { sliceReportPath, inspectReport, REPORT_MARKERS } from '../../../framework/hooks/shared/slice-progress.js';
|
|
9
|
-
import { loadTelemetryAllTrees, dedupeEvents } from '../../lib/telemetry/telemetry-reader.js';
|
|
4
|
+
import { implementProgress, parseReport, reportPath } from '../../../framework/hooks/shared/report-progress.js';
|
|
10
5
|
import { treeState, snapshotTree } from '../../lib/git/tree-state.js';
|
|
11
6
|
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
12
7
|
import { minimatch } from 'minimatch';
|
|
@@ -16,9 +11,9 @@ import { changedPathsAgainst, detectDefaultBranch } from '../../lib/git/default-
|
|
|
16
11
|
import { readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
17
12
|
import { readPlanChecks } from '../../lib/plan/checks.js';
|
|
18
13
|
import { isTestPath } from '../../lib/graph/sanitize.js';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
15
|
+
import { collectReviewRequirement } from './gate-check.js';
|
|
16
|
+
import { featureGeneratesMedia } from '../../lib/plan/media.js';
|
|
22
17
|
|
|
23
18
|
/** Each review report: complete when it carries a readable verdict and is not partial. */
|
|
24
19
|
export function inspectReviewReports(cwd, feature) {
|
|
@@ -31,8 +26,12 @@ export function inspectReviewReports(cwd, feature) {
|
|
|
31
26
|
return out;
|
|
32
27
|
}
|
|
33
28
|
|
|
29
|
+
function readOrEmpty(path) {
|
|
30
|
+
try { return existsSync(path) ? readFileSync(path, 'utf8') : ''; } catch { return ''; }
|
|
31
|
+
}
|
|
32
|
+
|
|
34
33
|
// The verifier routes to opus on prompt/model or media; security is dispatched only when required.
|
|
35
|
-
function reviewDispatch(cwd, feature
|
|
34
|
+
function reviewDispatch(cwd, feature) {
|
|
36
35
|
let changedFiles = [];
|
|
37
36
|
try { changedFiles = changedPathsAgainst(detectDefaultBranch(cwd), cwd); } catch { changedFiles = []; }
|
|
38
37
|
const watch = readMorphConfig(cwd)?.project?.evals?.watch;
|
|
@@ -44,172 +43,45 @@ function reviewDispatch(cwd, feature, slices) {
|
|
|
44
43
|
required,
|
|
45
44
|
verifier: verifierModelFor({
|
|
46
45
|
promptModelTouched: changedFiles.some((f) => globs.some((g) => minimatch(f, g))),
|
|
47
|
-
generatesMedia: featureGeneratesMedia(
|
|
46
|
+
generatesMedia: featureGeneratesMedia(readOrEmpty(getAbsoluteOutputPath(cwd, feature, 'plan'))),
|
|
48
47
|
}),
|
|
49
48
|
reviewer: { model: 'sonnet', reasons: [] },
|
|
50
49
|
security: { required: security.required, model: 'opus', reasons: security.reasons },
|
|
51
50
|
};
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
// Two clocks: the report mtime comes from the filesystem, the event `ts` from the hook. Two
|
|
55
|
-
// seconds absorb their skew and are far less than any real work done after reporting.
|
|
56
|
-
export const STALE_TOLERANCE_MS = 2000;
|
|
57
|
-
|
|
58
53
|
const slash = (p) => String(p || '').replace(/\\/g, '/');
|
|
59
|
-
|
|
60
|
-
function toTreeRelative(target, treePath) {
|
|
61
|
-
const norm = slash(target);
|
|
62
|
-
const root = slash(treePath).replace(/\/+$/, '');
|
|
63
|
-
if (!root) return norm;
|
|
64
|
-
// Case-insensitive prefix: on Windows the same root arrives as `R:/repo` or `r:/repo`.
|
|
65
|
-
return norm.toLowerCase().startsWith(`${root.toLowerCase()}/`) ? norm.slice(root.length + 1) : norm;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const reportSuffix = (feature, sliceId) => `.morph/features/${feature}/3-implement/reports/${sliceId}.md`.toLowerCase();
|
|
69
|
-
|
|
70
|
-
function loadWrites(cwd) {
|
|
71
|
-
let trees = [];
|
|
72
|
-
try { trees = loadTelemetryAllTrees(cwd); } catch { trees = []; }
|
|
73
|
-
const events = [];
|
|
74
|
-
for (const tree of trees) {
|
|
75
|
-
for (const e of tree.events) {
|
|
76
|
-
if (e?.tool !== 'Write' && e?.tool !== 'Edit') continue;
|
|
77
|
-
if (isFailedToolCall(e)) continue;
|
|
78
|
-
const ts = Date.parse(e?.ts ?? '');
|
|
79
|
-
if (!Number.isFinite(ts)) continue;
|
|
80
|
-
events.push({ ...e, rel: toTreeRelative(e.target, tree.path), tsMs: ts });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return { available: trees.some((t) => t.events.length > 0), writes: dedupeEvents(events) };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// The owning dispatch is the latest sub-agent write of the report; a later orchestrator edit
|
|
87
|
-
// does not hide it. Without a report, any write to one of the slice files says the work landed.
|
|
88
|
-
function observedEvidence(telemetry, feature, slice) {
|
|
89
|
-
if (!telemetry.available) return { available: false };
|
|
90
|
-
const suffix = reportSuffix(feature, slice.id);
|
|
91
|
-
const isReport = (w) => slash(w.target).toLowerCase().endsWith(suffix);
|
|
92
|
-
const declared = new Set((slice.files ?? []).map((f) => slash(f).replace(/^\.\//, '').toLowerCase()));
|
|
93
|
-
const isSliceFile = (w) => declared.has(w.rel.toLowerCase());
|
|
94
|
-
|
|
95
|
-
const reportWrites = telemetry.writes.filter((w) => isReport(w) && w.agentId);
|
|
96
|
-
const agentId = reportWrites.length ? reportWrites[reportWrites.length - 1].agentId : null;
|
|
97
|
-
if (!agentId) {
|
|
98
|
-
return { available: true, agentId: null, sliceWrites: telemetry.writes.filter(isSliceFile).length };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const work = telemetry.writes.filter((w) => w.agentId === agentId && !isReport(w) && !w.rel.includes('.morph/'));
|
|
102
|
-
const observed = [...new Set(work.map((w) => w.rel))];
|
|
103
|
-
const observedKeys = new Set(observed.map((f) => f.toLowerCase()));
|
|
104
|
-
return {
|
|
105
|
-
available: true,
|
|
106
|
-
agentId,
|
|
107
|
-
observed,
|
|
108
|
-
lastWriteAt: work.length ? new Date(Math.max(...work.map((w) => w.tsMs))).toISOString() : null,
|
|
109
|
-
undeclared: observed.filter((f) => !declared.has(f.toLowerCase())),
|
|
110
|
-
unobserved: (slice.files ?? []).filter((f) => !observedKeys.has(slash(f).replace(/^\.\//, '').toLowerCase())),
|
|
111
|
-
sliceWrites: work.filter(isSliceFile).length,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isReportStale(reportFsPath, observed) {
|
|
116
|
-
if (!observed?.agentId || !observed.lastWriteAt) return false;
|
|
117
|
-
try {
|
|
118
|
-
return statSync(reportFsPath).mtimeMs + STALE_TOLERANCE_MS < Date.parse(observed.lastWriteAt);
|
|
119
|
-
} catch {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function decide(report, observed, stale) {
|
|
125
|
-
if (report.exists && report.complete) {
|
|
126
|
-
// Structural integrity answers "is the report complete?", never "is it
|
|
127
|
-
// still true?": an agent can write it early, keep working and be cut off.
|
|
128
|
-
return stale
|
|
129
|
-
? { verdict: 'report-stale', action: 'refresh-from-evidence' }
|
|
130
|
-
: { verdict: 'ok', action: 'proceed' };
|
|
131
|
-
}
|
|
132
|
-
// PARCIAL: the agent stopped short (turn ceiling) and resumes from its last
|
|
133
|
-
// `wip` commit — completing it DEGRADADO would stamp unfinished work as done.
|
|
134
|
-
if (report.exists && report.blocked) return { verdict: 'report-blocked', action: 'ask-human' };
|
|
135
|
-
if (report.exists && report.partial) return { verdict: 'report-incomplete', action: 'resume' };
|
|
136
|
-
if (report.exists) return { verdict: 'report-incomplete', action: 'complete-degraded' };
|
|
137
|
-
if (!observed.available) return { verdict: 'report-missing-evidence-unknown', action: 'redispatch' };
|
|
138
|
-
return observed.sliceWrites > 0
|
|
139
|
-
? { verdict: 'work-landed-report-missing', action: 'accept-degraded' }
|
|
140
|
-
: { verdict: 'no-work-no-report', action: 'redispatch' };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
54
|
const isTestRef = (p) => isTestPath(p) || /\.(test|spec)\.[a-z]+$|Tests?\.cs$/i.test(p);
|
|
144
55
|
|
|
145
|
-
/** Cn whose
|
|
146
|
-
export function checksSpread(
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for (const l of lines.slice(start + 1)) {
|
|
152
|
-
if (/^\s*(\*\*|#)/.test(l)) break;
|
|
153
|
-
block.push(l);
|
|
56
|
+
/** Cn whose evidence in the report cites tests in more than one file, as `{ id, files }`. */
|
|
57
|
+
export function checksSpread(reportText) {
|
|
58
|
+
const spread = [];
|
|
59
|
+
for (const [id, { evidence }] of parseReport(reportText)) {
|
|
60
|
+
const files = [...new Set((evidence.match(/[\w@./\\-]+\.[A-Za-z]+/g) ?? []).map(slash).filter(isTestRef))];
|
|
61
|
+
if (files.length > 1) spread.push({ id, files });
|
|
154
62
|
}
|
|
155
|
-
|
|
156
|
-
let current = null;
|
|
157
|
-
for (const piece of block.join('\n').split(/[;\n]/)) {
|
|
158
|
-
const lead = /^\s*(?:[-*]\s*)?\(?(C\d+)\b/.exec(piece);
|
|
159
|
-
if (lead) current = lead[1];
|
|
160
|
-
if (!current) continue;
|
|
161
|
-
const refs = (piece.match(/[\w@.\/\\-]+\.[A-Za-z]+/g) ?? []).map(slash).filter(isTestRef);
|
|
162
|
-
if (!filesByCn.has(current)) filesByCn.set(current, new Set());
|
|
163
|
-
for (const f of refs) filesByCn.get(current).add(f);
|
|
164
|
-
}
|
|
165
|
-
return [...filesByCn].filter(([, files]) => files.size > 1).map(([id, files]) => ({ id, files: [...files] }));
|
|
63
|
+
return spread;
|
|
166
64
|
}
|
|
167
65
|
|
|
168
|
-
function spreadWarnings(fsPath) {
|
|
169
|
-
let body = '';
|
|
170
|
-
try { body = readFileSync(fsPath, 'utf8'); } catch { return []; }
|
|
171
|
-
return checksSpread(body).map(({ id, files }) =>
|
|
172
|
-
`${id} cita testes em ${files.length} arquivos (${files.join(', ')}): um teste por Cn, na camada mais baixa que o prova`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function checkSlice(cwd, feature, slice, telemetry) {
|
|
176
|
-
const fsPath = sliceReportPath(join(cwd, '.morph', 'features', feature), slice.id);
|
|
177
|
-
const report = inspectReport(fsPath, { covers: slice.covers ?? [] });
|
|
178
|
-
const observed = observedEvidence(telemetry, feature, slice);
|
|
179
|
-
const stale = report.exists && isReportStale(fsPath, observed);
|
|
180
|
-
const warnings = report.exists ? spreadWarnings(fsPath) : [];
|
|
181
|
-
return { slice: slice.id, report, observed, warnings, ...decide(report, observed, stale) };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const ACTION_HINT = {
|
|
185
|
-
proceed: 'relatório íntegro — siga o fluxo normal.',
|
|
186
|
-
'refresh-from-evidence': 'o relatório está COMPLETO mas VELHO: o agente da fatia seguiu escrevendo arquivos depois de gravá-lo, então ele descreve um estado já ultrapassado. NÃO re-despache e não confie no texto — confira os arquivos em `observed` (evidência real, não auto-declarada) e atualize o relatório a partir deles.',
|
|
187
|
-
'complete-degraded': 'complete os itens faltantes a partir da mensagem de retorno do sub-agent, carimbe o relatório como DEGRADADO e registre nele a falha de protocolo.',
|
|
188
|
-
resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a fatia NÃO está pronta. Não carimbe DEGRADADO — retome o MESMO agente por `SendMessage` (cole no recado `tree.dirty` e `tree.commits` desta saída e o que falta) ou re-despache só o que falta, a partir do último commit `wip`.',
|
|
189
|
-
'ask-human': 'relatório BLOQUEADO: o sub-agent parou numa decisão que não é dele (requisito que contradiz o plano ou um ADR, escolha do humano) e o relatório traz a pergunta e as opções. NÃO re-despache — a mesma pergunta volta. Se o plano ou um ADR já decide, decida você e registre no relatório; senão, faça a pergunta por `AskUserQuestion`. Depois retome o MESMO agente por `SendMessage` com a resposta.',
|
|
190
|
-
'accept-degraded':'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório da fatia a partir da mensagem de retorno, carimbado como DEGRADADO, com a falha de protocolo registrada nele.',
|
|
191
|
-
redispatch: 're-despache a fatia UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
|
|
192
|
-
};
|
|
193
|
-
|
|
194
66
|
function snapshotPath(cwd, feature) {
|
|
195
67
|
return join(cwd, '.morph', 'features', feature, '4-review', 'tree-before-eval.json');
|
|
196
68
|
}
|
|
197
69
|
|
|
198
70
|
// `beforeEval` snapshots after comparing, so the output still shows what changed since the last one.
|
|
199
|
-
function treeFor(cwd, feature, beforeEval
|
|
71
|
+
function treeFor(cwd, feature, beforeEval) {
|
|
200
72
|
const path = snapshotPath(cwd, feature);
|
|
201
73
|
let snapshot = null;
|
|
202
74
|
try { if (existsSync(path)) snapshot = JSON.parse(readFileSync(path, 'utf8')); } catch { snapshot = null; }
|
|
203
75
|
const tree = treeState(cwd, { snapshot });
|
|
204
76
|
if (!beforeEval) return { tree };
|
|
205
|
-
const reviewModels = reviewDispatch(cwd, feature
|
|
77
|
+
const reviewModels = reviewDispatch(cwd, feature);
|
|
206
78
|
const next = snapshotTree(cwd);
|
|
207
79
|
if (!next) return { tree, beforeEval: null, reviewModels };
|
|
208
80
|
atomicWriteJson(path, next);
|
|
209
81
|
return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, reviewModels };
|
|
210
82
|
}
|
|
211
83
|
|
|
212
|
-
export function reportCheckCommand(feature,
|
|
84
|
+
export function reportCheckCommand(feature, options = {}) {
|
|
213
85
|
const cwd = options.cwd || process.cwd();
|
|
214
86
|
const emit = (obj, code) => {
|
|
215
87
|
if (options.json) console.log(JSON.stringify(obj, null, 2));
|
|
@@ -221,20 +93,15 @@ export function reportCheckCommand(feature, sliceIds, options = {}) {
|
|
|
221
93
|
if (!checks) {
|
|
222
94
|
emit({ error: `feature "${feature}" sem 2-plan/checks.json legível (${status}) — rode morph-spec plan-check ${feature}`, feature }, 1);
|
|
223
95
|
}
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return { ...r, hint: ACTION_HINT[r.action] };
|
|
234
|
-
});
|
|
235
|
-
const clean = results.every((r) => r.verdict === 'ok');
|
|
236
|
-
const reviews = ids.length ? undefined : inspectReviewReports(cwd, feature);
|
|
237
|
-
emit({ feature, results, ...(reviews ? { reviews } : {}), clean, ...treeFor(cwd, feature, options.beforeEval, slices) }, clean ? 0 : 1);
|
|
96
|
+
const featureDir = join(cwd, '.morph', 'features', feature);
|
|
97
|
+
const progress = implementProgress(featureDir);
|
|
98
|
+
const warnings = checksSpread(readOrEmpty(reportPath(featureDir))).map(({ id, files }) =>
|
|
99
|
+
`${id} cita testes em ${files.length} arquivos (${files.join(', ')}): um teste por Cn, na camada mais baixa que o prova`);
|
|
100
|
+
const implement = progress
|
|
101
|
+
? { total: progress.total, done: progress.done, pending: progress.checks.filter((c) => c.status === 'pendente').map((c) => c.id),
|
|
102
|
+
blocked: progress.checks.filter((c) => c.status === 'bloqueado').map((c) => c.id), legacy: progress.legacy, warnings }
|
|
103
|
+
: null;
|
|
104
|
+
emit({ feature, implement, reviews: inspectReviewReports(cwd, feature), ...treeFor(cwd, feature, options.beforeEval) }, 0);
|
|
238
105
|
}
|
|
239
106
|
|
|
240
107
|
function printHuman(obj) {
|
|
@@ -243,18 +110,12 @@ function printHuman(obj) {
|
|
|
243
110
|
return;
|
|
244
111
|
}
|
|
245
112
|
console.log(chalk.bold(`\n📄 report-check ${obj.feature}`));
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (r.report.exists && r.report.missing.length) {
|
|
253
|
-
console.log(chalk.gray(` faltando no relatório: ${r.report.missing.join(', ')}`));
|
|
254
|
-
}
|
|
255
|
-
console.log(chalk.gray(` → ${r.hint}`));
|
|
256
|
-
}
|
|
257
|
-
for (const w of r.warnings ?? []) console.log(chalk.yellow(` ⚠ ${w}`));
|
|
113
|
+
const impl = obj.implement;
|
|
114
|
+
if (impl) {
|
|
115
|
+
const open = [...impl.pending, ...impl.blocked];
|
|
116
|
+
console.log(` ${open.length ? chalk.yellow('⚠') : chalk.green('✓')} report.md ${impl.done}/${impl.total} cenários feitos`
|
|
117
|
+
+ `${impl.pending.length ? ` · pendentes: ${impl.pending.join(', ')}` : ''}${impl.blocked.length ? ` · bloqueados: ${impl.blocked.join(', ')}` : ''}`);
|
|
118
|
+
for (const w of impl.warnings) console.log(chalk.yellow(` ⚠ ${w}`));
|
|
258
119
|
}
|
|
259
120
|
for (const [role, r] of Object.entries(obj.reviews ?? {})) {
|
|
260
121
|
if (!r.exists && role === 'security') continue;
|
|
@@ -280,9 +141,6 @@ function printTree(tree) {
|
|
|
280
141
|
console.log(` árvore ${tree.branch ?? '-'}@${tree.head ?? '-'}: ${tree.dirtyCount} arquivo(s) não commitado(s) fora de .morph/${stat}`);
|
|
281
142
|
for (const d of tree.dirty.slice(0, 10)) console.log(chalk.gray(` ${d.status.padEnd(2)} ${d.path}`));
|
|
282
143
|
if (tree.dirtyCount > 10) console.log(chalk.gray(` … e mais ${tree.dirtyCount - 10}`));
|
|
283
|
-
if (tree.markers.length) {
|
|
284
|
-
console.log(chalk.yellow(` ⚠ marcador de mutação esquecido: ${tree.markers.map((m) => `${m.file}:${m.line}`).join(', ')}`));
|
|
285
|
-
}
|
|
286
144
|
if (tree.commits?.length) console.log(chalk.gray(` commits sobre a default: ${tree.commits.join(' · ')}`));
|
|
287
145
|
const since = tree.sinceBeforeEval;
|
|
288
146
|
if (since) {
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
5
5
|
import { isGateApproved } from '../../../framework/hooks/shared/state-reader.js';
|
|
6
6
|
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
7
7
|
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { renderPlanHtml } from '../../lib/plan/plan-html.js';
|
|
9
|
+
import { buildChecks, planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
|
|
10
10
|
import { revocationWarnings } from '../../lib/specs/adr-index.js';
|
|
11
11
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
12
12
|
import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
|
|
13
13
|
import { reportDotnetIgnores } from '../state/advance.js';
|
|
14
|
+
import { syncStackRules } from '../../lib/installers/stack-sync.js';
|
|
14
15
|
|
|
15
16
|
function readJson(path) {
|
|
16
17
|
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
@@ -21,17 +22,14 @@ export function runPlanCheck(featureName, cwd = process.cwd()) {
|
|
|
21
22
|
const planPath = getAbsoluteOutputPath(cwd, featureName, 'plan');
|
|
22
23
|
const checksPath = getAbsoluteOutputPath(cwd, featureName, 'checks');
|
|
23
24
|
const base = { feature: featureName, plan: planPath, checksPath };
|
|
24
|
-
if (!existsSync(planPath)) return { ...base, ok: false, exit: 2, error: 'plan-not-found', errors: [], warnings: []
|
|
25
|
+
if (!existsSync(planPath)) return { ...base, ok: false, exit: 2, error: 'plan-not-found', errors: [], warnings: [] };
|
|
25
26
|
|
|
26
27
|
const text = readFileSync(planPath, 'utf8');
|
|
27
28
|
const plan = parsePlan(text);
|
|
28
|
-
const { errors, warnings } = validatePlan(plan, {
|
|
29
|
-
personas: knownPersonas(cwd), surfaces: knownSurfaces(cwd), ...planRuleOptions(cwd, featureName),
|
|
30
|
-
});
|
|
29
|
+
const { errors, warnings } = validatePlan(plan, { ...planRuleOptions(cwd, featureName), text });
|
|
31
30
|
warnings.push(...revocationWarnings(cwd, plan));
|
|
32
|
-
const groups = errors.length === 0 ? parallelGroups(plan.slices) : [];
|
|
33
31
|
const hash = planHash(text);
|
|
34
|
-
const result = { ...base, planHash: hash, errors, warnings,
|
|
32
|
+
const result = { ...base, planHash: hash, errors, warnings, written: false };
|
|
35
33
|
if (errors.length > 0) return { ...result, ok: false, exit: 1 };
|
|
36
34
|
|
|
37
35
|
const approved = isGateApproved(featureName, 'plan', cwd);
|
|
@@ -41,7 +39,9 @@ export function runPlanCheck(featureName, cwd = process.cwd()) {
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
atomicWriteJson(checksPath, buildChecks(featureName, text, plan));
|
|
44
|
-
|
|
42
|
+
const viewPath = getAbsoluteOutputPath(cwd, featureName, 'planView');
|
|
43
|
+
writeFileSync(viewPath, renderPlanHtml(featureName, plan, { planHash: hash }));
|
|
44
|
+
return { ...result, ok: true, exit: 0, written: true, viewPath };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function planCheckCommand(featureName, options = {}) {
|
|
@@ -54,8 +54,13 @@ export function planCheckCommand(featureName, options = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const result = runPlanCheck(featureName);
|
|
57
|
+
// After the plan validates: in a greenfield project the plan is where the stack is born.
|
|
58
|
+
const stackSync = result.written
|
|
59
|
+
? syncStackRules(process.cwd(), { log: (line) => console.error(chalk.gray(line)) })
|
|
60
|
+
: null;
|
|
57
61
|
const gitignoreAdded = result.written ? seedDotnetBuildIgnores(process.cwd(), { featureName }) : [];
|
|
58
62
|
if (options.json) {
|
|
63
|
+
if (stackSync?.added.length) result.rulesAdded = stackSync.added;
|
|
59
64
|
if (gitignoreAdded.length) result.gitignoreAdded = gitignoreAdded;
|
|
60
65
|
if (gitignoreAdded.skipped) result.gitignoreSkipped = gitignoreAdded.skipped;
|
|
61
66
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -77,7 +82,7 @@ export function planCheckCommand(featureName, options = {}) {
|
|
|
77
82
|
console.log(chalk.red(`✗ plan-check: ${result.errors.length} erro(s) em ${result.plan} — checks.json não foi gravado`));
|
|
78
83
|
} else {
|
|
79
84
|
console.log(chalk.green(`✓ plan-check: ${join('2-plan', 'checks.json')} gravado (${result.planHash.slice(0, 12)})`));
|
|
80
|
-
|
|
85
|
+
console.log(chalk.gray(` visão do plano: ${result.viewPath}`));
|
|
81
86
|
reportDotnetIgnores(gitignoreAdded);
|
|
82
87
|
}
|
|
83
88
|
process.exit(result.exit);
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
5
|
-
import {
|
|
5
|
+
import { planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
|
|
6
6
|
|
|
7
7
|
// Matched against section markers (headings and line-start bold labels), never whole-file
|
|
8
8
|
// substrings. required → ERROR, recommended → WARNING, optional → never penalized; an array is any-of.
|
|
@@ -117,7 +117,7 @@ function validatePlanOutput(content, planDir, result) {
|
|
|
117
117
|
const plan = parsePlan(content);
|
|
118
118
|
const root = path.resolve(planDir, '..', '..', '..', '..');
|
|
119
119
|
const feature = path.basename(path.resolve(planDir, '..'));
|
|
120
|
-
const { errors, warnings } = validatePlan(plan, {
|
|
120
|
+
const { errors, warnings } = validatePlan(plan, { ...planRuleOptions(root, feature), text: content });
|
|
121
121
|
result.errors.push(...errors);
|
|
122
122
|
result.warnings.push(...warnings);
|
|
123
123
|
result.sections.found.push(...plan.sections.filter((x) => x.key).map((x) => x.name));
|
|
@@ -137,7 +137,7 @@ function validateChecksOutput(content, planDir, result) {
|
|
|
137
137
|
result.errors.push('checks.json is stale — plan.md changed after it was generated; run `morph-spec plan-check`');
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
-
result.sections.found.push(`${doc.checks?.length ?? 0} check(s)
|
|
140
|
+
result.sections.found.push(`${doc.checks?.length ?? 0} check(s)`);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function validateMarkdownSections(content, sections, result) {
|