@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join, relative } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { getFeature, setApprovalGate, derivePhase } from '../../core/state/state-manager.js';
|
|
@@ -8,7 +8,7 @@ import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
|
|
|
8
8
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
9
9
|
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
10
10
|
import { seedDesignArtifacts } from '../../lib/specs/design-seed.js';
|
|
11
|
-
import {
|
|
11
|
+
import { implementProgress, reportPath, reportSkeleton } from '../../../framework/hooks/shared/report-progress.js';
|
|
12
12
|
import { autoApprovalRefusal, printRefusal } from './auto-approval.js';
|
|
13
13
|
import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
|
|
14
14
|
|
|
@@ -38,17 +38,25 @@ function missingPhaseArtifacts(phase, featurePath) {
|
|
|
38
38
|
const rel = `${entry.phaseDir}/${entry.filename}`;
|
|
39
39
|
if (!existsSync(join(featurePath, entry.phaseDir, entry.filename))) missing.push(rel);
|
|
40
40
|
}
|
|
41
|
-
if (phase === 'implement') missing.push(...
|
|
41
|
+
if (phase === 'implement') missing.push(...unfinishedScenarios(featurePath));
|
|
42
42
|
return missing;
|
|
43
43
|
} catch {
|
|
44
44
|
return [];
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function
|
|
49
|
-
const progress =
|
|
48
|
+
function unfinishedScenarios(featurePath) {
|
|
49
|
+
const progress = implementProgress(featurePath);
|
|
50
50
|
if (!progress) return ['2-plan/checks.json'];
|
|
51
|
-
|
|
51
|
+
const open = progress.checks.filter((c) => c.status !== 'feito').map((c) => c.id);
|
|
52
|
+
return open.length ? [`3-implement/report.md (${open.join(', ')} não feito)`] : [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function seedReport(featurePath, featureName) {
|
|
56
|
+
const path = reportPath(featurePath);
|
|
57
|
+
if (existsSync(path)) return;
|
|
58
|
+
const progress = implementProgress(featurePath);
|
|
59
|
+
if (progress) writeFileSync(path, reportSkeleton(featureName, progress.checks.map((c) => c.id)));
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
// Message-only and fail-open: names the worktree that holds the artifacts instead of demanding
|
|
@@ -114,10 +122,6 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
114
122
|
mkdirSync(join(featurePath, '1-design'), { recursive: true });
|
|
115
123
|
reportDesignSeed(seedDesignArtifacts(featureName, process.cwd()));
|
|
116
124
|
reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
|
|
117
|
-
try {
|
|
118
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
119
|
-
writeHandoff(featureName, process.cwd());
|
|
120
|
-
} catch { /* fail-open */ }
|
|
121
125
|
console.log(chalk.green(`\n✅ "${featureName}": gate plan já aprovado — ramo de design aberto (1-design/).\n`));
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
@@ -129,7 +133,7 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
129
133
|
return;
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
// `implement` has no gate, so this is what keeps unfinished
|
|
136
|
+
// `implement` has no gate, so this is what keeps unfinished scenarios out of review.
|
|
133
137
|
// `--force` bypasses it but still names what was skipped.
|
|
134
138
|
const missingArtifacts = missingPhaseArtifacts(phase, featurePath);
|
|
135
139
|
if (missingArtifacts.length && !options.force) {
|
|
@@ -166,7 +170,7 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
// `setApprovalGate` replaces the gate whole: rewriting an approved gate would turn a human
|
|
169
|
-
// signature into "auto". Skips ONLY the write, so the scaffold
|
|
173
|
+
// signature into "auto". Skips ONLY the write, so the scaffold below still runs.
|
|
170
174
|
const priorGate = gate ? feature.approvalGates?.[gate] : null;
|
|
171
175
|
const alreadyApproved = priorGate?.approved === true;
|
|
172
176
|
|
|
@@ -193,13 +197,9 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
193
197
|
if (!existsSync(nextPath)) mkdirSync(nextPath, { recursive: true });
|
|
194
198
|
}
|
|
195
199
|
if (nextDir === '1-design') reportDesignSeed(seedDesignArtifacts(featureName, process.cwd()));
|
|
200
|
+
if (nextDir === '3-implement') seedReport(featurePath, featureName);
|
|
196
201
|
if (phase === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
|
|
197
202
|
|
|
198
|
-
// A phase transition is when a session hands the feature over: refresh handoff.md.
|
|
199
|
-
try {
|
|
200
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
201
|
-
writeHandoff(featureName, process.cwd());
|
|
202
|
-
} catch { /* fail-open */ }
|
|
203
203
|
|
|
204
204
|
console.log(chalk.green(`\n✅ Advanced "${featureName}" past the ${phase} phase.`));
|
|
205
205
|
if (missingArtifacts.length) {
|
|
@@ -260,12 +260,6 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
260
260
|
...(selfAssessed ? { selfAssessed: true } : {}),
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
-
// Approving is a session handoff too; a regeneration failure must never fail the approval.
|
|
264
|
-
try {
|
|
265
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
266
|
-
writeHandoff(featureName, process.cwd());
|
|
267
|
-
} catch { /* fail-open */ }
|
|
268
|
-
|
|
269
263
|
if (selfAssessed) {
|
|
270
264
|
console.log(chalk.yellow('\n⚠️ Gate 3 aprovado SEM os relatórios de review independentes (--self-assessed).'));
|
|
271
265
|
console.log(chalk.gray(' Registrado em feature.json → approvalGates.review.selfAssessed.'));
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { existsSync, renameSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join, resolve, sep } from 'path';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
5
|
import { ADR_INDEX_PATHSPEC, updateAdrIndex } from '../../lib/specs/adr-index.js';
|
|
6
6
|
import { isValidFeatureName } from '../project/worktree.js';
|
|
7
|
+
import { RECAP_FILES, writeRecap } from './recap.js';
|
|
7
8
|
|
|
8
9
|
function assertContained(scopeDir, name, target) {
|
|
9
10
|
const scopeAbs = resolve(scopeDir) + sep;
|
|
@@ -38,6 +39,17 @@ function readReviewApproved(fjPath) {
|
|
|
38
39
|
* Never calls process.exit, and resolves everything from `cwd` so `finish` can archive inside a
|
|
39
40
|
* worktree. Idempotent: an existing archive returns `{ ok: true, alreadyArchived: true }`.
|
|
40
41
|
*/
|
|
42
|
+
// plan.md stays: a squash merge drops every file born and pruned inside the branch.
|
|
43
|
+
function pruneArchive(dir) {
|
|
44
|
+
for (const entry of readdirSync(dir)) {
|
|
45
|
+
if (entry === 'feature.json' || Object.values(RECAP_FILES).includes(entry)) continue;
|
|
46
|
+
if (entry !== '2-plan') { rmSync(join(dir, entry), { recursive: true, force: true }); continue; }
|
|
47
|
+
for (const planEntry of readdirSync(join(dir, entry))) {
|
|
48
|
+
if (planEntry !== 'plan.md') rmSync(join(dir, entry, planEntry), { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
export function runArchive(feature, opts = {}) {
|
|
42
54
|
const cwd = opts.cwd || process.cwd();
|
|
43
55
|
|
|
@@ -71,6 +83,10 @@ export function runArchive(feature, opts = {}) {
|
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
const useGit = opts.git !== false && isGitRepo(cwd);
|
|
86
|
+
let archivedFromSha = null;
|
|
87
|
+
if (useGit) {
|
|
88
|
+
try { archivedFromSha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim(); } catch { /* no commit yet */ }
|
|
89
|
+
}
|
|
74
90
|
|
|
75
91
|
// `git mv` keeps history and stages in one step; an untracked folder falls back to rename.
|
|
76
92
|
mkdirSync(archiveDir, { recursive: true });
|
|
@@ -96,6 +112,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
96
112
|
const fj = JSON.parse(readFileSync(fjPathAfter, 'utf8'));
|
|
97
113
|
fj.status = 'completed';
|
|
98
114
|
fj.archivedAt = new Date().toISOString();
|
|
115
|
+
if (archivedFromSha) fj.archivedFrom = { sha: archivedFromSha };
|
|
99
116
|
fj.updatedAt = fj.archivedAt;
|
|
100
117
|
writeFileSync(fjPathAfter, JSON.stringify(fj, null, 2), 'utf8');
|
|
101
118
|
if (useGit && staged) {
|
|
@@ -113,6 +130,13 @@ export function runArchive(feature, opts = {}) {
|
|
|
113
130
|
try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
114
131
|
}
|
|
115
132
|
|
|
133
|
+
// Before the prune: the recap reads the Gate 3 reports the archive does not keep.
|
|
134
|
+
const recap = opts.recap === false ? null : writeRecap(archivePath, archivePath, feature, { cwd });
|
|
135
|
+
pruneArchive(archivePath);
|
|
136
|
+
if (useGit && staged) {
|
|
137
|
+
try { execFileSync('git', ['add', '-A', '--', archivePathspec], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
116
140
|
// Scoped to this feature's pathspec, never `commit -a`. The ADR index joins only when it
|
|
117
141
|
// changed: git refuses a pathspec that matches nothing, leaving the tree dirty for the merge.
|
|
118
142
|
let committed = false;
|
|
@@ -144,6 +168,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
144
168
|
|
|
145
169
|
return {
|
|
146
170
|
ok: true, archived: true, committed, staged, archivePath,
|
|
171
|
+
recap: recap?.ok ? recap.path : null,
|
|
147
172
|
revocationProblems: adrIndex.problems,
|
|
148
173
|
adrIndex: adrIndex.changed ? { added: adrIndex.added, revoked: adrIndex.revoked, narrowed: adrIndex.narrowed } : null,
|
|
149
174
|
};
|
|
@@ -180,6 +205,7 @@ export async function archiveCommand(featureName, options = {}) {
|
|
|
180
205
|
|
|
181
206
|
console.log(chalk.green(`\n📦 Archived "${featureName}" → .morph/archive/${featureName}/`));
|
|
182
207
|
console.log(chalk.gray(` status: completed, archivedAt stamped in feature.json.`));
|
|
208
|
+
if (result.recap) console.log(chalk.gray(` recap: ${result.recap} (morph-spec recap ${featureName} --piper narra com o Piper)`));
|
|
183
209
|
if (result.adrIndex) {
|
|
184
210
|
const { added, revoked, narrowed } = result.adrIndex;
|
|
185
211
|
console.log(chalk.gray(` ADR index (.morph/specs/adrs/): +${added.length} −${revoked.length} ~${narrowed.length}.`));
|
|
@@ -34,7 +34,7 @@ export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedB
|
|
|
34
34
|
if (block) {
|
|
35
35
|
return {
|
|
36
36
|
title: `O gate "plan" não pode ser auto-aprovado — ${block}.`,
|
|
37
|
-
hints: ['
|
|
37
|
+
hints: ['Sem checks.json fresco: plan-check verde e rode de novo. Com degrau: aprove com --approver <nome> ou pela resposta humana. O --force não dispensa nenhum dos dois.'],
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -185,11 +185,6 @@ export async function createCommand(featureName, options = {}) {
|
|
|
185
185
|
// Re-materialize after the copy: the setup's earlier materialize ran with no features yet.
|
|
186
186
|
const remat = await materializeState(worktreeResult.path);
|
|
187
187
|
worktreeResult.materialized = remat.materialized;
|
|
188
|
-
try {
|
|
189
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
190
|
-
// `worktree` was stripped from this copy above, so pass where it actually lives.
|
|
191
|
-
writeHandoff(featureName, worktreeResult.path, { worktree: { path: worktreeResult.path, branch: worktreeResult.branch } });
|
|
192
|
-
} catch { /* fail-open */ }
|
|
193
188
|
} catch { /* fail-open — the feature already exists in root */ }
|
|
194
189
|
}
|
|
195
190
|
} else {
|
|
@@ -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
|
|
@@ -70,7 +60,11 @@ export function collectReviewSignal(cwd, featureName, feature, planChecks = read
|
|
|
70
60
|
|
|
71
61
|
/** Why an automatic signature may not close the plan gate: the rule that makes gate-check pause it. */
|
|
72
62
|
export function planAutoApprovalBlock(cwd, featureName) {
|
|
73
|
-
const
|
|
63
|
+
const planChecks = readPlanChecks(cwd, featureName);
|
|
64
|
+
const workType = getFeature(featureName)?.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
|
+
}
|