@polymorphism-tech/morph-spec 9.7.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 +11 -11
- package/README.md +4 -4
- package/bin/morph-spec.js +41 -44
- 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 +100 -102
- package/framework/evals/review-checklist.md +46 -54
- package/framework/hooks/README.md +4 -4
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- 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/heavy-slots.js +41 -4
- 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 +12 -19
- 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/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- 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/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- 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 +27 -16
- 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/finish.js +43 -1
- 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 +39 -146
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +6 -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/main.jsx +13 -13
- 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/dashboard/vite.config.js +24 -24
- 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/heavy/gate.js +22 -1
- 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 +33 -7
- 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/tasks/test-nodes.js +24 -6
- package/src/lib/tasks/test-runner.js +40 -1
- 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 +50 -19
- 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,7 +1,7 @@
|
|
|
1
1
|
import { join, dirname, relative, isAbsolute } from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
|
-
import { existsSync, readdirSync } from 'fs';
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -27,9 +27,10 @@ import {
|
|
|
27
27
|
} from '../../utils/version-checker.js';
|
|
28
28
|
import { installClaudeHooks, installGlobalStatusline } from '../../utils/claude-settings-manager.js';
|
|
29
29
|
import { installSkills } from '../../utils/skills-installer.js';
|
|
30
|
-
import { installAgents
|
|
30
|
+
import { installAgents } from '../../utils/agents-installer.js';
|
|
31
31
|
import { injectMorphImport } from '../../utils/claude-md-injector.js';
|
|
32
32
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
33
|
+
import { detectStackTags, mergeDetectedTags } from '../../lib/installers/stack-sync.js';
|
|
33
34
|
import { updateClaudeMdAgentsBlock, installRulesForStack, installCommandsForStack } from '../../scripts/setup-infra.js';
|
|
34
35
|
import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
|
|
35
36
|
import { createEffects } from '../../lib/installers/effects.js';
|
|
@@ -145,6 +146,26 @@ export function stateJsonIsTracked(targetPath) {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
const SLICES_HEADING = /^## Fatias\s*$/m;
|
|
150
|
+
|
|
151
|
+
/** Active features still planned in 9.x slices: a plan with `## Fatias` and no `3-implement/report.md` yet. */
|
|
152
|
+
export function legacySliceFeatures(targetPath) {
|
|
153
|
+
const featuresDir = join(targetPath, '.morph', 'features');
|
|
154
|
+
let names;
|
|
155
|
+
try { names = readdirSync(featuresDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return []; }
|
|
156
|
+
return names.filter((name) => {
|
|
157
|
+
const dir = join(featuresDir, name);
|
|
158
|
+
if (existsSync(join(dir, '3-implement', 'report.md'))) return false;
|
|
159
|
+
try { return SLICES_HEADING.test(readFileSync(join(dir, '2-plan', 'plan.md'), 'utf8')); } catch { return false; }
|
|
160
|
+
}).sort();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function legacySliceNotice(names) {
|
|
164
|
+
if (names.length === 0) return null;
|
|
165
|
+
return `Feature(s) planejada(s) em fatias (9.x): ${names.join(', ')}. Plano não aprovado: tire \`## Fatias\` e rode \`morph-spec plan-check <f>\`. `
|
|
166
|
+
+ 'Já em implement: segue pelos `reports/F*.md`; para o formato novo, crie `3-implement/report.md` com uma linha por `Cn` (CHANGELOG 10.0.0).';
|
|
167
|
+
}
|
|
168
|
+
|
|
148
169
|
export function untrackCommittedStateJson(targetPath, fx = createEffects({ dryRun: false })) {
|
|
149
170
|
if (!stateJsonIsTracked(targetPath)) return false;
|
|
150
171
|
return fx.gitIndex('rm --cached --quiet', '.morph/state.json', { cwd: targetPath });
|
|
@@ -487,6 +508,19 @@ export async function updateCommand(options = {}) {
|
|
|
487
508
|
try {
|
|
488
509
|
if (configBackup) projectTags = getProjectTags(configBackup);
|
|
489
510
|
} catch {}
|
|
511
|
+
// The same detection as `syncStackRules`, folded in before the installs: `restoreUserConfig`
|
|
512
|
+
// writes `configBackup`, and a dry run sees `.claude/rules/` before the clean, not after.
|
|
513
|
+
if (configBackup) {
|
|
514
|
+
try {
|
|
515
|
+
const detected = detectStackTags(targetPath, { config: configBackup });
|
|
516
|
+
const merged = mergeDetectedTags(configBackup, detected.persist);
|
|
517
|
+
if (merged.changed) {
|
|
518
|
+
configBackup.project = merged.config.project;
|
|
519
|
+
logger.dim(` ✓ config.json: + ${merged.added.join(', ')} (detectado nos arquivos)`);
|
|
520
|
+
}
|
|
521
|
+
projectTags = [...new Set([...getProjectTags(configBackup), ...detected.tags])];
|
|
522
|
+
} catch {}
|
|
523
|
+
}
|
|
490
524
|
|
|
491
525
|
let commandsCopied = false;
|
|
492
526
|
const commandsSrc = join(frameworkDir, 'commands');
|
|
@@ -504,7 +538,6 @@ export async function updateCommand(options = {}) {
|
|
|
504
538
|
|
|
505
539
|
updateSpinner.text = 'Syncing agents to .claude/agents/...';
|
|
506
540
|
const agentsResult = await installAgents(targetPath, frameworkDir, { projectTags, fx });
|
|
507
|
-
await installDomainAgents(targetPath, frameworkDir, { projectTags, fx });
|
|
508
541
|
|
|
509
542
|
updateSpinner.text = 'Syncing rules to .claude/rules/...';
|
|
510
543
|
const rulesSrc = join(frameworkDir, 'rules');
|
|
@@ -566,6 +599,9 @@ export async function updateCommand(options = {}) {
|
|
|
566
599
|
}
|
|
567
600
|
}
|
|
568
601
|
|
|
602
|
+
const sliceNotice = legacySliceNotice(legacySliceFeatures(targetPath));
|
|
603
|
+
if (sliceNotice) notices.push(sliceNotice);
|
|
604
|
+
|
|
569
605
|
const untracked = untrackCommittedStateJson(targetPath, fx);
|
|
570
606
|
|
|
571
607
|
updateSpinner.text = 'Saving version info...';
|
|
@@ -85,8 +85,27 @@ function getDateSuffix() {
|
|
|
85
85
|
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
function trackedPackageNodeModules(mainCwd) {
|
|
89
|
+
let out;
|
|
90
|
+
try {
|
|
91
|
+
out = execFileSync('git', ['ls-files', '-z', '--', '*package.json'], { cwd: mainCwd, stdio: 'pipe' }).toString();
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const dirs = [];
|
|
96
|
+
for (const file of out.split('\0')) {
|
|
97
|
+
const parts = file.split('/');
|
|
98
|
+
if (parts.length < 2 || parts.at(-1) !== 'package.json') continue;
|
|
99
|
+
const dirParts = parts.slice(0, -1);
|
|
100
|
+
if (dirParts.includes('node_modules') || WORKTREE_DIR_NAMES.includes(dirParts[0])) continue;
|
|
101
|
+
const rel = join(...dirParts, 'node_modules');
|
|
102
|
+
if (existsSync(join(mainCwd, rel))) dirs.push(rel);
|
|
103
|
+
}
|
|
104
|
+
return dirs;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The ONLY links `finish` may auto-remove (never widened to any reparse point found), plus the
|
|
108
|
+
// `node_modules` of each tracked nested package: a monorepo package has its own install.
|
|
90
109
|
function resolveInfraLinkDirs(mainCwd) {
|
|
91
110
|
const dirs = ['node_modules', '.claude'];
|
|
92
111
|
if (existsSync(join(mainCwd, '.morph', 'framework'))) {
|
|
@@ -100,6 +119,9 @@ function resolveInfraLinkDirs(mainCwd) {
|
|
|
100
119
|
if (frontendPath) dirs.push(join(frontendPath, 'node_modules'));
|
|
101
120
|
}
|
|
102
121
|
} catch {}
|
|
122
|
+
for (const rel of trackedPackageNodeModules(mainCwd)) {
|
|
123
|
+
if (!dirs.includes(rel)) dirs.push(rel);
|
|
124
|
+
}
|
|
103
125
|
return dirs;
|
|
104
126
|
}
|
|
105
127
|
|
|
@@ -298,15 +320,6 @@ export async function runWorktreeSetup(feature, options = {}) {
|
|
|
298
320
|
|
|
299
321
|
const graph = seedWorktreeGraph(worktreePath, cwd);
|
|
300
322
|
|
|
301
|
-
// Fail-open; a no-op under `create --worktree`, which regenerates it after its copy.
|
|
302
|
-
try {
|
|
303
|
-
if (existsSync(join(worktreePath, '.morph', 'features', feature, 'feature.json'))) {
|
|
304
|
-
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
305
|
-
// `worktree` is root-owned and absent from this branch copy: pass it so the doc says "Local: worktree".
|
|
306
|
-
writeHandoff(feature, worktreePath, { worktree: { path: worktreePath, branch } });
|
|
307
|
-
}
|
|
308
|
-
} catch {}
|
|
309
|
-
|
|
310
323
|
try {
|
|
311
324
|
const { loadState, saveState } = await import('../../core/state/state-manager.js');
|
|
312
325
|
const state = loadState(false);
|
|
@@ -812,9 +825,9 @@ export function pickRemovalFailure(stderrs) {
|
|
|
812
825
|
|
|
813
826
|
/**
|
|
814
827
|
* PROVE-or-DECLINE. `refused` (proved unsafe) restores the junctions; a git FAILURE leaves them down
|
|
815
|
-
* (`infraLeftUnlinked`)
|
|
828
|
+
* (`infraLeftUnlinked`).
|
|
816
829
|
*/
|
|
817
|
-
export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false,
|
|
830
|
+
export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false, stamp }) {
|
|
818
831
|
// ORDER IS LOAD-BEARING: unlink the junctions BEFORE git touches the dir. `git worktree remove`
|
|
819
832
|
// follows them and deletes the root's installation; a declined path is repaired by the rollback below.
|
|
820
833
|
const unlinked = unlinkWorktreeInfraLinks(worktreePath, root);
|
|
@@ -859,8 +872,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
859
872
|
// would make the next retry (or a raw `--force`) follow the junctions out.
|
|
860
873
|
const refused = !canRemove;
|
|
861
874
|
let relinked = false;
|
|
862
|
-
|
|
863
|
-
if ((refused || relinkAfterFailure) && unlinked.length && existsSync(worktreePath)) {
|
|
875
|
+
if (refused && unlinked.length && existsSync(worktreePath)) {
|
|
864
876
|
try {
|
|
865
877
|
setupInfraLinks(worktreePath, root);
|
|
866
878
|
relinked = true;
|
|
@@ -1033,7 +1045,6 @@ export async function runWorktreeRemove(featureArg, options = {}) {
|
|
|
1033
1045
|
options: { discardIgnored: options.discardIgnored === true },
|
|
1034
1046
|
// The branch is the work: deleting it would discard unintegrated commits. Opt-in only.
|
|
1035
1047
|
deleteBranch: options.deleteBranch === true,
|
|
1036
|
-
relinkOnFailure: options.relinkOnFailure === true,
|
|
1037
1048
|
// Re-run advice must address the worktree the way it was addressable here:
|
|
1038
1049
|
// a `--target` worktree has no feature name that would resolve.
|
|
1039
1050
|
addressByTarget: !featureAddressable,
|
|
@@ -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 {
|
|
@@ -292,7 +292,8 @@ export async function finishCommand(feature, options = {}) {
|
|
|
292
292
|
}
|
|
293
293
|
const dirtLines = withoutLeftoverIgnore(cleanCwd, dirty.split(/\r?\n/).filter((l) => l.trim()));
|
|
294
294
|
const testArtifacts = untrackedTestArtifacts(dirtLines);
|
|
295
|
-
|
|
295
|
+
const commitState = dirtLines.length > testArtifacts.length && onlyFeatureStateDirty(cleanCwd, feature);
|
|
296
|
+
if (dirtLines.length > testArtifacts.length && !commitState) {
|
|
296
297
|
emit({ finished: false, error: isWorktreeFlow ? 'worktree-dirty' : 'working-dir-dirty', hint: 'commit seu trabalho na branch antes do finish', feature });
|
|
297
298
|
process.exit(1);
|
|
298
299
|
}
|
|
@@ -341,6 +342,10 @@ export async function finishCommand(feature, options = {}) {
|
|
|
341
342
|
|
|
342
343
|
// Every refusal is behind us: the leftover of a killed `graph refresh` goes now, never before.
|
|
343
344
|
dropLeftoverIgnore(cleanCwd);
|
|
345
|
+
if (commitState && !commitFeatureState(cleanCwd, feature)) {
|
|
346
|
+
emit({ finished: false, error: 'state-commit-failed', hint: `não foi possível commitar .morph/features/${feature} (identidade git ausente?) — commite o estado na branch e rode o finish de novo`, feature });
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
344
349
|
if (isWorktreeFlow) {
|
|
345
350
|
for (const p of testArtifacts) rmSync(join(cleanCwd, p), { recursive: true, force: true });
|
|
346
351
|
}
|
|
@@ -409,6 +414,15 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
|
|
|
409
414
|
|
|
410
415
|
if (defaultBranch) out.defaultBranch = defaultBranch;
|
|
411
416
|
|
|
417
|
+
try {
|
|
418
|
+
const tree = worktreePath || root;
|
|
419
|
+
const dirt = gitStatusPorcelain(tree);
|
|
420
|
+
const work = dirt === null ? [] : withoutLeftoverIgnore(tree, dirt.split(/\r?\n/).filter((l) => l.trim()));
|
|
421
|
+
if (work.length > untrackedTestArtifacts(work).length && onlyFeatureStateDirty(tree, feature)) {
|
|
422
|
+
out.wouldDo.unshift(`commitar o estado sujo de .morph/features/${feature} (${stateCommitMessage(feature)}) na branch ${branch}`);
|
|
423
|
+
}
|
|
424
|
+
} catch { /* o pré-voo nunca lança */ }
|
|
425
|
+
|
|
412
426
|
// A retenção automática também é mutação do finish real; a contagem é a de AGORA, antes do
|
|
413
427
|
// backup que este finish gravar.
|
|
414
428
|
try {
|
|
@@ -808,6 +822,34 @@ function untrackedTestArtifacts(lines) {
|
|
|
808
822
|
.filter((p) => isTestArtifact(p));
|
|
809
823
|
}
|
|
810
824
|
|
|
825
|
+
const stateCommitMessage = (feature) => `chore(morph): estado da feature ${feature}`;
|
|
826
|
+
|
|
827
|
+
// `--untracked-files=all`: o porcelain padrão colapsa `?? .morph/` e esconderia sujeira fora do escopo.
|
|
828
|
+
function onlyFeatureStateDirty(cwd, feature) {
|
|
829
|
+
let out;
|
|
830
|
+
try {
|
|
831
|
+
out = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd, stdio: 'pipe' }).toString();
|
|
832
|
+
} catch {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
const scope = `.morph/features/${feature}/`;
|
|
836
|
+
const paths = out.split(/\r?\n/).filter((l) => l.trim())
|
|
837
|
+
.flatMap((l) => l.slice(3).split(' -> '))
|
|
838
|
+
.map((p) => p.replace(/^"|"$/g, ''));
|
|
839
|
+
return paths.length > 0 && paths.every((p) => p.startsWith(scope));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function commitFeatureState(cwd, feature) {
|
|
843
|
+
const scope = `.morph/features/${feature}`;
|
|
844
|
+
try {
|
|
845
|
+
execFileSync('git', ['add', '-A', '--', scope], { cwd, stdio: 'pipe' });
|
|
846
|
+
execFileSync('git', ['commit', '-q', '-m', stateCommitMessage(feature), '--', scope], { cwd, stdio: 'pipe' });
|
|
847
|
+
return true;
|
|
848
|
+
} catch {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
811
853
|
function gitStatusPorcelain(cwd) {
|
|
812
854
|
try {
|
|
813
855
|
return execFileSync('git', ['status', '--porcelain'], { cwd, stdio: 'pipe' }).toString();
|
|
@@ -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
|
}
|