@polymorphism-tech/morph-spec 8.47.0 → 9.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 +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
|
@@ -1,78 +1,67 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
|
-
import { existsSync
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, derivePhase } from '../../core/state/state-manager.js';
|
|
7
7
|
import { unmetPrereqs } from '../../core/state/gate-order.js';
|
|
8
|
-
import {
|
|
8
|
+
import { reviewReports, reviewFileName, REVIEW_ROLES, VERDICT_TOKEN, REVIEWED_SHA_TOKEN } from '../../lib/review-reports.js';
|
|
9
9
|
import { shouldPauseGate } from '../../lib/risk-detector.js';
|
|
10
|
-
import { collectSignals } from './gate-check.js';
|
|
10
|
+
import { collectSignals, collectReviewRequirement } from './gate-check.js';
|
|
11
|
+
import { autoApprovalRefusal, printRefusal } from './auto-approval.js';
|
|
11
12
|
import { readTrustFromConfig } from './gate-decision.js';
|
|
12
13
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
13
|
-
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
14
14
|
import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
|
|
15
|
+
import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
|
|
16
|
+
import { reportDotnetIgnores } from './advance.js';
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
* The literal token `review-report.md` carries to declare which tree it
|
|
18
|
-
* reviewed. Kept as one exported constant so the CLI parser, the warning text
|
|
19
|
-
* and the `morph-review` skill can never drift apart (the A5 lesson).
|
|
20
|
-
*/
|
|
21
|
-
export const REVIEWED_SHA_TOKEN = '**Reviewed-SHA:**';
|
|
18
|
+
export { REVIEWED_SHA_TOKEN };
|
|
22
19
|
|
|
23
|
-
const
|
|
20
|
+
const SIGNED_OFF = { required: [], reports: {}, checks: { status: 'ok', total: 0, missing: [], notProven: [] } };
|
|
24
21
|
|
|
25
22
|
/**
|
|
26
|
-
*
|
|
23
|
+
* Do the review reports still describe the current tree? Every report present
|
|
24
|
+
* is compared; the worst answer wins.
|
|
27
25
|
*
|
|
28
|
-
* @param {
|
|
29
|
-
* @returns {{ status: 'ok'|'stale'|'unstamped'|'skip', reviewed?: string, head?: string, ahead?: number }}
|
|
30
|
-
* `skip
|
|
31
|
-
* no report, no git repo, an unresolvable SHA. Never a false refusal.
|
|
26
|
+
* @param {ReturnType<typeof reviewReports>} reports
|
|
27
|
+
* @returns {{ status: 'ok'|'stale'|'unstamped'|'skip', file?: string, reviewed?: string, head?: string, ahead?: number }}
|
|
28
|
+
* `skip`: no verifier report or no git repo. Never a false refusal.
|
|
32
29
|
*/
|
|
33
|
-
function reviewStaleness(
|
|
30
|
+
function reviewStaleness(reports) {
|
|
31
|
+
if (reports.verifier.status === 'missing') return { status: 'skip' };
|
|
34
32
|
const cwd = process.cwd();
|
|
35
|
-
const reportPath = join(cwd, '.morph', 'features', featureName, '4-review', 'review-report.md');
|
|
36
|
-
if (!existsSync(reportPath)) return { status: 'skip' };
|
|
37
|
-
|
|
38
33
|
let head;
|
|
39
34
|
try {
|
|
40
35
|
head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
|
|
41
|
-
} catch {
|
|
42
|
-
return { status: 'skip' }; // not a git repo (or no commits) — nothing to compare
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let body = '';
|
|
46
|
-
try {
|
|
47
|
-
body = readFileSync(reportPath, 'utf8');
|
|
48
36
|
} catch {
|
|
49
37
|
return { status: 'skip' };
|
|
50
38
|
}
|
|
51
39
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
40
|
+
let unstamped = null;
|
|
41
|
+
for (const [role, report] of Object.entries(reports)) {
|
|
42
|
+
if (report.status === 'missing') continue;
|
|
43
|
+
const file = reviewFileName(role);
|
|
44
|
+
let reviewed = null;
|
|
45
|
+
try {
|
|
46
|
+
if (report.reviewedSha) {
|
|
47
|
+
reviewed = execFileSync('git', ['rev-parse', '--verify', `${report.reviewedSha}^{commit}`], { cwd, stdio: 'pipe' })
|
|
48
|
+
.toString().trim();
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
reviewed = null;
|
|
52
|
+
}
|
|
53
|
+
if (!reviewed) {
|
|
54
|
+
unstamped ??= { status: 'unstamped', file, head };
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!treeMovedSince(cwd, reviewed, head)) continue;
|
|
58
|
+
let ahead = 0;
|
|
59
|
+
try {
|
|
60
|
+
ahead = Number(execFileSync('git', ['rev-list', '--count', `${reviewed}..${head}`], { cwd, stdio: 'pipe' }).toString().trim()) || 0;
|
|
61
|
+
} catch { ahead = 0; }
|
|
62
|
+
return { status: 'stale', file, reviewed, head, ahead };
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let ahead = 0;
|
|
67
|
-
try {
|
|
68
|
-
ahead = execFileSync('git', ['rev-list', '--count', `${reviewed}..${head}`], { cwd, stdio: 'pipe' })
|
|
69
|
-
.toString().trim();
|
|
70
|
-
ahead = Number(ahead) || 0;
|
|
71
|
-
} catch { ahead = 0; }
|
|
72
|
-
|
|
73
|
-
// HEAD not descending from the reviewed commit (a rebase, a different branch)
|
|
74
|
-
// is still "not the tree that was reviewed".
|
|
75
|
-
return { status: 'stale', reviewed, head, ahead };
|
|
64
|
+
return unstamped ?? { status: 'ok', head };
|
|
76
65
|
}
|
|
77
66
|
|
|
78
67
|
/**
|
|
@@ -148,7 +137,7 @@ const program = new Command();
|
|
|
148
137
|
/**
|
|
149
138
|
* Valid approval gate names
|
|
150
139
|
*/
|
|
151
|
-
const VALID_GATES = ['
|
|
140
|
+
const VALID_GATES = ['plan', 'uiux', 'review'];
|
|
152
141
|
|
|
153
142
|
/**
|
|
154
143
|
* Approve action function - Mark a gate as approved
|
|
@@ -192,12 +181,13 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
192
181
|
// with nowhere to go. The way out already exists as `unapprove`; the
|
|
193
182
|
// message just never named it. Naming it is the whole fix (no new flag).
|
|
194
183
|
console.log(chalk.gray(`To approve it again, revoke it first: morph-spec unapprove ${featureName} ${gateName}\n`));
|
|
184
|
+
if (gateName === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
|
|
195
185
|
return;
|
|
196
186
|
}
|
|
197
187
|
|
|
198
188
|
// Gate monotonicity: a gate may only be approved once its predecessors are
|
|
199
|
-
// approved (
|
|
200
|
-
//
|
|
189
|
+
// approved (plan → review; uiux requires plan). Prevents the shape of a
|
|
190
|
+
// review approved with the plan still pending. Policy-stamped
|
|
201
191
|
// gates count as approved, so chore/hotfix satisfy prereqs naturally.
|
|
202
192
|
// Escape via --force for deliberate recovery.
|
|
203
193
|
const unmet = unmetPrereqs(gateName, feature.approvalGates);
|
|
@@ -234,24 +224,14 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
234
224
|
approvedBy = 'auto';
|
|
235
225
|
}
|
|
236
226
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// FORCED_PAUSE_WORK_TYPES), enforced server-side rather than trusting the
|
|
241
|
-
// calling skill. It gates on the RESOLVED approvedBy, NOT the raw --mode
|
|
242
|
-
// string: every headless context (claude -p, CI, the Bash tool) runs
|
|
243
|
-
// without a TTY, so an omitted --mode — or any --mode ≠ 'auto' — still
|
|
244
|
-
// resolves to "auto" and would otherwise rubber-stamp the gate. A human
|
|
245
|
-
// approval is --approver <name> or an interactive TTY ($USER); anything
|
|
246
|
-
// resolving to "auto" is refused here.
|
|
247
|
-
if (gateName === 'review' && feature.workType === 'hotfix' && approvedBy === 'auto') {
|
|
248
|
-
console.error(chalk.red('\n❌ Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.'));
|
|
249
|
-
console.error(chalk.gray(' Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) ao review-report via AskUserQuestion.\n'));
|
|
227
|
+
const refusal = autoApprovalRefusal({ cwd: process.cwd(), featureName, feature, gate: gateName, approvedBy });
|
|
228
|
+
if (refusal) {
|
|
229
|
+
printRefusal(chalk, refusal);
|
|
250
230
|
process.exit(1);
|
|
251
231
|
}
|
|
252
232
|
|
|
253
233
|
// Gate 3 staleness backstop (D1): a review reviews a TREE, and the tree
|
|
254
|
-
// moves. Observed: review
|
|
234
|
+
// moves. Observed: a review report written at b30ac6e, then T3 landed and
|
|
255
235
|
// inverted the default of a business rule; the report stayed in the repo
|
|
256
236
|
// claiming 976/976 tests (they were 982) and never mentioning T3. Nothing
|
|
257
237
|
// detected that the review artifact had fallen behind the code it reviews.
|
|
@@ -260,93 +240,49 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
260
240
|
// A stamp that is BEHIND HEAD refuses. A MISSING stamp only warns — reports
|
|
261
241
|
// written before this existed must not brick a legacy feature — but the
|
|
262
242
|
// warning names the exact token to add, so it teaches instead of nagging.
|
|
243
|
+
const reports = gateName === 'review' ? reviewReports(process.cwd(), featureName) : null;
|
|
263
244
|
if (gateName === 'review') {
|
|
264
|
-
const stale = reviewStaleness(
|
|
245
|
+
const stale = reviewStaleness(reports);
|
|
265
246
|
if (stale.status === 'stale') {
|
|
266
|
-
console.error(chalk.red(
|
|
247
|
+
console.error(chalk.red(`\n❌ O \`4-review/${stale.file}\` está desatualizado em relação ao código.`));
|
|
267
248
|
console.error(chalk.gray(` Revisado em: ${stale.reviewed.slice(0, 8)}`));
|
|
268
249
|
console.error(chalk.gray(` HEAD atual: ${stale.head.slice(0, 8)}${stale.ahead ? ` (${stale.ahead} commit(s) depois da revisão)` : ''}`));
|
|
269
250
|
console.error(chalk.gray(' Um review revisa uma ÁRVORE, e a árvore andou. Re-rode a `morph-review`'));
|
|
270
|
-
console.error(chalk.gray(
|
|
251
|
+
console.error(chalk.gray(` e atualize o \`${REVIEWED_SHA_TOKEN}\` de cada relatório.\n`));
|
|
271
252
|
process.exit(1);
|
|
272
253
|
}
|
|
273
254
|
if (stale.status === 'unstamped') {
|
|
274
|
-
console.log(chalk.yellow(
|
|
275
|
-
console.log(chalk.gray(` Adicione a linha
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Skipped-pause backstop: a gate whose recorded decision demanded a human
|
|
280
|
-
// pause (gateDecisions[gate].pause === true) cannot then be stamped by a
|
|
281
|
-
// non-human signature. The detection already existed — in gate-guard.js —
|
|
282
|
-
// but that hook is registered for `Write|Edit|AskUserQuestion`, and a Bash
|
|
283
|
-
// tool call fires no hook at all. Since MORPH.md requires gate approval to
|
|
284
|
-
// go through THIS command and forbids hand-editing the state, the only path
|
|
285
|
-
// the hook could ever see was the one the charter bans. The check belongs
|
|
286
|
-
// where the stamp is applied, not beside it.
|
|
287
|
-
//
|
|
288
|
-
// Gated on the RESOLVED `approvedBy`, matching the hotfix backstop above:
|
|
289
|
-
// an omitted --mode in a headless context also resolves to "auto" and must
|
|
290
|
-
// not slip through. `--force` does NOT bypass this either — it is the
|
|
291
|
-
// out-of-order recovery flag, a different concern. The way out is a human
|
|
292
|
-
// signature, or rewriting the decision that asked for the pause.
|
|
293
|
-
const decision = feature.gateDecisions?.[gateName];
|
|
294
|
-
if (decision?.pause === true && approvedBy === 'auto') {
|
|
295
|
-
console.error(chalk.red(`\n❌ O Gate "${gateName}" registrou pause:true — auto-aprovação não se aplica.`));
|
|
296
|
-
console.error(chalk.gray(` Sinal: ${decision.reason || 'risco detectado'}`));
|
|
297
|
-
console.error(chalk.gray(' A decisão gravada exigia pausa humana. Aprove com --approver <nome>, num'));
|
|
298
|
-
console.error(chalk.gray(' terminal interativo, ou pela resposta humana via AskUserQuestion (state-sync).'));
|
|
299
|
-
console.error(chalk.gray(' Se o sinal de risco já não vale, regrave a decisão:'));
|
|
300
|
-
console.error(chalk.gray(` morph-spec gate-decision ${featureName} ${gateName} --pause false --reason "<motivo>"\n`));
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// The same backstop when no gate-check ran: the pause it WOULD record for
|
|
305
|
-
// the screen checklist, by the predicate advance.js reads too. `--force`
|
|
306
|
-
// does not bypass it, for the reason above.
|
|
307
|
-
if (gateName === 'uiux' && approvedBy === 'auto') {
|
|
308
|
-
const block = uiuxAutoApprovalBlock(process.cwd(), featureName, feature);
|
|
309
|
-
if (block) {
|
|
310
|
-
console.error(chalk.red(`\n❌ O Gate "uiux" não pode ser auto-aprovado — ${block}.`));
|
|
311
|
-
console.error(chalk.gray(' Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com'));
|
|
312
|
-
console.error(chalk.gray(' --approver <nome>, ou, se a pausa já foi decidida, grave a decisão:'));
|
|
313
|
-
console.error(chalk.gray(` morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"\n`));
|
|
314
|
-
process.exit(1);
|
|
255
|
+
console.log(chalk.yellow(`\n⚠️ O \`${stale.file}\` não carrega \`${REVIEWED_SHA_TOKEN}\` — impossível saber se ele revisa o código atual.`));
|
|
256
|
+
console.log(chalk.gray(` Adicione a linha \`${REVIEWED_SHA_TOKEN} ${stale.head}\` ao relatório para que o Gate 3 possa verificar.`));
|
|
315
257
|
}
|
|
316
258
|
}
|
|
317
259
|
|
|
318
260
|
let selfAssessed = false;
|
|
319
261
|
if (gateName === 'review') {
|
|
320
262
|
if (feature.workType === 'hotfix') {
|
|
321
|
-
// Hotfix
|
|
322
|
-
// (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
// a degradation for a report that was never supposed to exist). The human
|
|
328
|
-
// signature is already enforced above (line ~215) — this only checks the
|
|
329
|
-
// artifact the skill actually produces.
|
|
330
|
-
const reviewReport = join(process.cwd(), '.morph', 'features', featureName, '4-review', 'review-report.md');
|
|
331
|
-
if (!existsSync(reviewReport)) {
|
|
332
|
-
console.error(chalk.red('\n❌ Gate 3 de hotfix exige `4-review/review-report.md` — não existe.'));
|
|
333
|
-
console.error(chalk.gray(' É o artefato que a `morph-hotfix` produz (diff do fix + evidência real de'));
|
|
334
|
-
console.error(chalk.gray(' teste do `morph-spec verify`), não o `evaluator-report.md` do avaliador'));
|
|
335
|
-
console.error(chalk.gray(' independente — hotfix nunca despacha o avaliador. Escreva o relatório antes'));
|
|
263
|
+
// Hotfix never dispatches: the orchestrator writes `verifier.md` itself
|
|
264
|
+
// (diff + real verify evidence); the human signature above decides.
|
|
265
|
+
if (reports.verifier.status === 'missing') {
|
|
266
|
+
console.error(chalk.red('\n❌ Gate 3 de hotfix exige `4-review/verifier.md` — não existe.'));
|
|
267
|
+
console.error(chalk.gray(' No hotfix o orquestrador o escreve (diff do fix + evidência real do'));
|
|
268
|
+
console.error(chalk.gray(' `morph-spec verify`) sem despachar ninguém. Escreva o relatório antes'));
|
|
336
269
|
console.error(chalk.gray(' de aprovar; a assinatura humana já é exigida pelo backstop acima.\n'));
|
|
337
270
|
process.exit(1);
|
|
338
271
|
}
|
|
339
272
|
} else {
|
|
340
|
-
const
|
|
341
|
-
|
|
273
|
+
const { required } = collectReviewRequirement(process.cwd(), feature);
|
|
274
|
+
const unusable = required.filter((role) => reports[role].status !== 'ok');
|
|
275
|
+
if (unusable.length) {
|
|
342
276
|
if (!options.selfAssessed) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
277
|
+
console.error(chalk.red(`\n❌ Gate 3 exige ${required.map((r) => `\`4-review/${reviewFileName(r)}\``).join(', ')}.`));
|
|
278
|
+
for (const role of unusable) {
|
|
279
|
+
const { status } = reports[role];
|
|
280
|
+
const what = status === 'missing' ? 'não existe'
|
|
281
|
+
: status === 'partial' ? 'está PARCIAL — retome o mesmo agente por SendMessage'
|
|
282
|
+
: `não tem \`${VERDICT_TOKEN} SHIP|FIX\``;
|
|
283
|
+
console.error(chalk.gray(` ${reviewFileName(role)} ${what}.`));
|
|
348
284
|
}
|
|
349
|
-
console.error(chalk.gray(
|
|
285
|
+
console.error(chalk.gray(` Os relatórios vêm de ${unusable.map((r) => `\`${REVIEW_ROLES[r].persona}\``).join(', ')} em contexto fresco, nunca de quem implementou.`));
|
|
350
286
|
console.error(chalk.gray(' Se o dispatch for impossível nesta sessão, aprove com --self-assessed —'));
|
|
351
287
|
console.error(chalk.gray(' a degradação fica registrada no feature.json e aparece no `list`/`fleet`.\n'));
|
|
352
288
|
process.exit(1);
|
|
@@ -356,10 +292,10 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
356
292
|
|
|
357
293
|
// Sem gate-check prévio não há `gateDecisions.review` para o backstop acima morder; em
|
|
358
294
|
// campo, ships passaram assim com verify vermelho ou ausente. A auto-aprovação recalcula
|
|
359
|
-
// a decisão pela MESMA régua do gate-check. --self-assessed já dispensou
|
|
295
|
+
// a decisão pela MESMA régua do gate-check. --self-assessed já dispensou os relatórios.
|
|
360
296
|
if (approvedBy === 'auto') {
|
|
361
297
|
const signals = collectSignals(featureName, feature, process.cwd(), 'review');
|
|
362
|
-
if (selfAssessed) signals.
|
|
298
|
+
if (selfAssessed) signals.reviewReports = SIGNED_OFF;
|
|
363
299
|
const { pause, reason } = shouldPauseGate({ trust: readTrustFromConfig() }, signals);
|
|
364
300
|
if (pause) {
|
|
365
301
|
console.error(chalk.red(`\n❌ Gate 3 não pode ser auto-aprovado — ${reason}.`));
|
|
@@ -368,9 +304,12 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
368
304
|
}
|
|
369
305
|
}
|
|
370
306
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
307
|
+
for (const role of required) {
|
|
308
|
+
const r = reports[role];
|
|
309
|
+
if (r.status === 'ok' && r.superseded.length > 0) {
|
|
310
|
+
console.log(chalk.yellow(`\n⚠️ ${reviewFileName(role)} tem ${r.superseded.length + 1} vereditos (append-only).`));
|
|
311
|
+
console.log(chalk.gray(` Vigente: ${r.verdict}, ${r.blockers.length} bloqueador(es). Superados: ${r.superseded.join(', ')}\n`));
|
|
312
|
+
}
|
|
374
313
|
}
|
|
375
314
|
}
|
|
376
315
|
}
|
|
@@ -384,7 +323,7 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
384
323
|
|
|
385
324
|
// Fleet handoff (mirrors advance.js): approving a gate is exactly when a
|
|
386
325
|
// feature hands over between sessions too — `approve` is the real path
|
|
387
|
-
//
|
|
326
|
+
// the plan gate took in the field (`approve <f> plan --mode auto`), not just
|
|
388
327
|
// `advance`, so a handoff.md left stale after approve was the common
|
|
389
328
|
// case, not the edge case. Same helper, same fail-open contract: a
|
|
390
329
|
// regeneration failure must never fail the approval itself.
|
|
@@ -394,11 +333,12 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
394
333
|
} catch { /* fail-open */ }
|
|
395
334
|
|
|
396
335
|
if (selfAssessed) {
|
|
397
|
-
console.log(chalk.yellow('\n⚠️ Gate 3 aprovado SEM
|
|
336
|
+
console.log(chalk.yellow('\n⚠️ Gate 3 aprovado SEM os relatórios de review independentes (--self-assessed).'));
|
|
398
337
|
console.log(chalk.gray(' Registrado em feature.json → approvalGates.review.selfAssessed.'));
|
|
399
338
|
}
|
|
400
339
|
|
|
401
340
|
console.log(chalk.green(`\n✅ Gate "${gateName}" approved for feature: ${featureName}`));
|
|
341
|
+
if (gateName === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
|
|
402
342
|
console.log(chalk.gray(`Workflow can now proceed past ${gateName} phase\n`));
|
|
403
343
|
|
|
404
344
|
} catch (error) {
|
|
@@ -2,9 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join, resolve, sep } from 'path';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
|
-
import {
|
|
6
|
-
import { applySpecRevocations } from '../../lib/specs/acervo-lifecycle.js';
|
|
7
|
-
import { ADR_INDEX_PATHSPEC } from '../../lib/specs/adr-index.js';
|
|
5
|
+
import { ADR_INDEX_PATHSPEC, updateAdrIndex } from '../../lib/specs/adr-index.js';
|
|
8
6
|
import { isValidFeatureName } from '../project/worktree.js';
|
|
9
7
|
|
|
10
8
|
/** Refuse a move whose resolved target escapes `scopeDir`. */
|
|
@@ -65,10 +63,7 @@ function readReviewApproved(fjPath) {
|
|
|
65
63
|
* @param {string} feature
|
|
66
64
|
* @param {{ cwd?: string, force?: boolean, git?: boolean }} [opts]
|
|
67
65
|
* @returns {{ ok: boolean, error?: string, archived?: boolean, alreadyArchived?: boolean,
|
|
68
|
-
* committed?: boolean, staged?: boolean, archivePath?: string,
|
|
69
|
-
* refusedUseCases?: Array<{id: string, incoming: string, existing: string, existingFeature: string, message: string}>,
|
|
70
|
-
* keptSuperseded?: Array<{id: string, file: string, supersededBy: string|null, scope?: string}>,
|
|
71
|
-
* supersededUseCases?: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
|
|
66
|
+
* committed?: boolean, staged?: boolean, archivePath?: string,
|
|
72
67
|
* revocationProblems?: Array<{id: string|null, reason: string, message: string}>,
|
|
73
68
|
* adrIndex?: {added: string[], revoked: string[], narrowed: string[]}|null }}
|
|
74
69
|
*/
|
|
@@ -94,15 +89,8 @@ export function runArchive(feature, opts = {}) {
|
|
|
94
89
|
const archivePathspec = join('.morph', 'archive', feature);
|
|
95
90
|
|
|
96
91
|
// Idempotent short-circuit: already archived here. Checked FIRST so a re-run
|
|
97
|
-
// after a partial finish reports "already done", never "not found"
|
|
98
|
-
//
|
|
99
|
-
// Consequence, deliberate: this short-circuits BEFORE the use-case promotion
|
|
100
|
-
// below, so a promotion that degraded fail-open is NOT retried by re-running
|
|
101
|
-
// `finish`. That is the price of "closing a feature never fails" — an archived
|
|
102
|
-
// feature stays archived, and the only signal is an absent `promotedUseCases`
|
|
103
|
-
// in the output. Recovering means copying the UCs into `.morph/specs/usecases/`
|
|
104
|
-
// by hand; making this retry would mean re-entering the archive path for an
|
|
105
|
-
// already-closed feature, which is a worse trade.
|
|
92
|
+
// after a partial finish reports "already done", never "not found"; an ADR
|
|
93
|
+
// index update that degraded fail-open is therefore not retried.
|
|
106
94
|
if (existsSync(archivePath)) {
|
|
107
95
|
return { ok: true, alreadyArchived: true, archived: false, archivePath };
|
|
108
96
|
}
|
|
@@ -156,63 +144,29 @@ export function runArchive(feature, opts = {}) {
|
|
|
156
144
|
} catch { /* non-fatal — the move is what matters */ }
|
|
157
145
|
}
|
|
158
146
|
|
|
159
|
-
// 3)
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
// (MORPH.md §2.2). Fail-open: any failure degrades to "promoted nothing",
|
|
164
|
-
// never to a refused close.
|
|
165
|
-
let promotedUseCases = [];
|
|
166
|
-
let refusedUseCases = [];
|
|
167
|
-
let keptSuperseded = [];
|
|
147
|
+
// 3) The closing plan's project-wide ADRs enter the index and the ones it
|
|
148
|
+
// revokes leave it, in the same scoped commit below. Fail-open: the index
|
|
149
|
+
// must never block archiving.
|
|
150
|
+
let adrIndex = { changed: false, added: [], revoked: [], narrowed: [], problems: [] };
|
|
168
151
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (promoted.length && useGit && staged) {
|
|
174
|
-
// This staging exists for the NEW files: promoted use cases are untracked,
|
|
175
|
-
// and the scoped `commit -- <pathspec>` below only knows paths git already
|
|
176
|
-
// tracks. The replaced-by-ID deletions are tracked, so that same commit
|
|
177
|
-
// would carry them even without this line (verified by mutating it away) —
|
|
178
|
-
// `-A` is belt-and-braces, but dropping the `git add` entirely would
|
|
179
|
-
// silently promote nothing on a first delivery.
|
|
180
|
-
try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
181
|
-
}
|
|
182
|
-
} catch { /* promotion must never block archiving */ }
|
|
183
|
-
|
|
184
|
-
// 3b) Apply what the feature's spec.md declared it revokes: acervo use cases
|
|
185
|
-
// turn `superseded`, and the ADR index gains/loses files. After the
|
|
186
|
-
// promotion, so a substitute promoted just now is already in place (and
|
|
187
|
-
// one it refused is known not to be); same commit, same fail-open contract.
|
|
188
|
-
let lifecycle = { superseded: [], adrIndex: null, problems: [] };
|
|
189
|
-
try {
|
|
190
|
-
lifecycle = applySpecRevocations(cwd, archivePath, { refused: refusedUseCases });
|
|
191
|
-
} catch { /* revocation must never block archiving */ }
|
|
192
|
-
const acervoChanged = promotedUseCases.length > 0 || lifecycle.superseded.length > 0;
|
|
193
|
-
const adrIndexChanged = lifecycle.adrIndex?.changed === true;
|
|
194
|
-
if (useGit && staged) {
|
|
195
|
-
if (lifecycle.superseded.length) {
|
|
196
|
-
try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
197
|
-
}
|
|
198
|
-
if (adrIndexChanged) {
|
|
199
|
-
try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
200
|
-
}
|
|
152
|
+
adrIndex = updateAdrIndex(cwd, archivePath);
|
|
153
|
+
} catch { /* fail-open */ }
|
|
154
|
+
if (useGit && staged && adrIndex.changed) {
|
|
155
|
+
try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
201
156
|
}
|
|
202
157
|
|
|
203
158
|
// 4) Commit the move — SCOPED to this feature's pathspec, NEVER `commit -a`
|
|
204
159
|
// (same anti-sweep discipline as `finish`'s worktree teardown). This is what
|
|
205
160
|
// lets the archived state travel with the branch on `finish` (archive-before-merge).
|
|
206
|
-
// The
|
|
207
|
-
//
|
|
208
|
-
//
|
|
161
|
+
// The ADR index joins only when it changed: git refuses a pathspec that
|
|
162
|
+
// matches nothing, and that refusal would leave the tree dirty and break
|
|
163
|
+
// the checkout/merge that follows.
|
|
209
164
|
let committed = false;
|
|
210
165
|
if (useGit && staged) {
|
|
211
166
|
const pathspecs = [
|
|
212
167
|
featPathspec,
|
|
213
168
|
archivePathspec,
|
|
214
|
-
...(
|
|
215
|
-
...(adrIndexChanged ? [ADR_INDEX_PATHSPEC] : []),
|
|
169
|
+
...(adrIndex.changed ? [ADR_INDEX_PATHSPEC] : []),
|
|
216
170
|
];
|
|
217
171
|
try {
|
|
218
172
|
execFileSync('git', ['commit', '-m', `morph-spec archive: ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
|
|
@@ -236,13 +190,10 @@ export function runArchive(feature, opts = {}) {
|
|
|
236
190
|
} catch { /* index is derivable — non-fatal */ }
|
|
237
191
|
}
|
|
238
192
|
|
|
239
|
-
const adr = lifecycle.adrIndex;
|
|
240
193
|
return {
|
|
241
|
-
ok: true, archived: true, committed, staged, archivePath,
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
revocationProblems: lifecycle.problems,
|
|
245
|
-
adrIndex: adrIndexChanged ? { added: adr.added, revoked: adr.revoked, narrowed: adr.narrowed } : null,
|
|
194
|
+
ok: true, archived: true, committed, staged, archivePath,
|
|
195
|
+
revocationProblems: adrIndex.problems,
|
|
196
|
+
adrIndex: adrIndex.changed ? { added: adrIndex.added, revoked: adrIndex.revoked, narrowed: adrIndex.narrowed } : null,
|
|
246
197
|
};
|
|
247
198
|
}
|
|
248
199
|
|
|
@@ -281,27 +232,12 @@ export async function archiveCommand(featureName, options = {}) {
|
|
|
281
232
|
|
|
282
233
|
console.log(chalk.green(`\n📦 Archived "${featureName}" → .morph/archive/${featureName}/`));
|
|
283
234
|
console.log(chalk.gray(` status: completed, archivedAt stamped in feature.json.`));
|
|
284
|
-
if (result.promotedUseCases?.length) {
|
|
285
|
-
console.log(chalk.gray(` ${result.promotedUseCases.length} use case(s) promoted → .morph/specs/usecases/ (status: implemented).`));
|
|
286
|
-
}
|
|
287
|
-
if (result.refusedUseCases?.length) {
|
|
288
|
-
console.log(chalk.yellow(`\n⚠️ ${result.refusedUseCases.length} use case(s) NOT promoted — numbering collision:`));
|
|
289
|
-
for (const r of result.refusedUseCases) {
|
|
290
|
-
console.log(chalk.yellow(` ${r.message}`));
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
for (const k of result.keptSuperseded ?? []) {
|
|
294
|
-
console.log(chalk.yellow(` ${k.id} continua superseded (superseded_by: ${k.supersededBy ?? '?'}) — a cópia promovida não trazia o status, e o acervo o manteve.`));
|
|
295
|
-
}
|
|
296
|
-
for (const s of result.supersededUseCases ?? []) {
|
|
297
|
-
console.log(chalk.gray(` ${s.id} → status: superseded (superseded_by: ${s.supersededBy}${s.scope ? `; ainda vale em: ${s.scope}` : ''}).`));
|
|
298
|
-
}
|
|
299
235
|
if (result.adrIndex) {
|
|
300
236
|
const { added, revoked, narrowed } = result.adrIndex;
|
|
301
237
|
console.log(chalk.gray(` ADR index (.morph/specs/adrs/): +${added.length} −${revoked.length} ~${narrowed.length}.`));
|
|
302
238
|
}
|
|
303
239
|
if (result.revocationProblems?.length) {
|
|
304
|
-
console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do
|
|
240
|
+
console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do plan.md NÃO aplicada(s):`));
|
|
305
241
|
for (const p of result.revocationProblems) {
|
|
306
242
|
console.log(chalk.yellow(` ${p.message}`));
|
|
307
243
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { planAutoApprovalBlock } from './gate-check.js';
|
|
2
|
+
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Why an "auto" signature may not stamp `gate`, or null when it may. The ONE
|
|
6
|
+
* rule `approve` and `advance` both apply: they write the same
|
|
7
|
+
* `approvalGates` entry, so they must refuse the same inputs.
|
|
8
|
+
*
|
|
9
|
+
* @returns {{ title: string, hints: string[] } | null}
|
|
10
|
+
*/
|
|
11
|
+
export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedBy }) {
|
|
12
|
+
if (approvedBy !== 'auto' || !gate) return null;
|
|
13
|
+
|
|
14
|
+
if (gate === 'review' && feature.workType === 'hotfix') {
|
|
15
|
+
return {
|
|
16
|
+
title: 'Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.',
|
|
17
|
+
hints: ['Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) via AskUserQuestion.'],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const decision = feature.gateDecisions?.[gate];
|
|
22
|
+
if (decision?.pause === true) {
|
|
23
|
+
return {
|
|
24
|
+
title: `O Gate "${gate}" registrou pause:true — auto-aprovação não se aplica.`,
|
|
25
|
+
hints: [
|
|
26
|
+
`Sinal: ${decision.reason || 'risco detectado'}`,
|
|
27
|
+
'A decisão gravada exigia pausa humana. Aprove com --approver <nome>, num',
|
|
28
|
+
'terminal interativo, ou pela resposta humana via AskUserQuestion (state-sync).',
|
|
29
|
+
'Se o sinal de risco já não vale, regrave a decisão:',
|
|
30
|
+
`morph-spec gate-decision ${featureName} ${gate} --pause false --reason "<motivo>"`,
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (gate === 'plan') {
|
|
36
|
+
const block = planAutoApprovalBlock(cwd, featureName);
|
|
37
|
+
if (block) {
|
|
38
|
+
return {
|
|
39
|
+
title: `O gate "plan" não pode ser auto-aprovado — ${block}.`,
|
|
40
|
+
hints: ['Com plan-check verde, rode de novo; ou aprove com --approver <nome>. O --force não dispensa o checks.json.'],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (gate === 'uiux') {
|
|
46
|
+
const block = uiuxAutoApprovalBlock(cwd, featureName, feature);
|
|
47
|
+
if (block) {
|
|
48
|
+
return {
|
|
49
|
+
title: `O Gate "uiux" não pode ser auto-aprovado — ${block}.`,
|
|
50
|
+
hints: [
|
|
51
|
+
'Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com',
|
|
52
|
+
'--approver <nome>, ou, se a pausa já foi decidida, grave a decisão:',
|
|
53
|
+
`morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"`,
|
|
54
|
+
'O --force não dispensa o checklist.',
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function printRefusal(chalk, refusal) {
|
|
64
|
+
console.error(chalk.red(`\n❌ ${refusal.title}`));
|
|
65
|
+
refusal.hints.forEach((line, i) => {
|
|
66
|
+
console.error(chalk.gray(` ${line}${i === refusal.hints.length - 1 ? '\n' : ''}`));
|
|
67
|
+
});
|
|
68
|
+
}
|