@polymorphism-tech/morph-spec 8.46.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 +53 -44
- package/README.md +3 -4
- package/bin/morph-spec.js +38 -55
- package/bin/validate.js +5 -134
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -2
- 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/create-umbraco-lp.md +2 -0
- package/framework/commands/migrate-lp-to-umbraco.md +2 -0
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +2 -2
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +17 -203
- package/framework/commands/morph-media.md +3 -0
- package/framework/commands/morph-preflight.md +7 -31
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/commands/morph-troubleshoot.md +2 -14
- 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 +17 -19
- 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 +67 -31
- 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 +3 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/ai-agents-standards.md +18 -16
- package/framework/rules/asaas-standards.md +3 -1
- package/framework/rules/csharp-standards.md +10 -8
- package/framework/rules/data-standards.md +5 -5
- package/framework/rules/dotnet-templates.md +1 -1
- package/framework/rules/frontend-standards.md +4 -2
- package/framework/rules/gsap-standards.md +10 -5
- package/framework/rules/hangfire-standards.md +3 -1
- package/framework/rules/infrastructure-standards.md +4 -2
- package/framework/rules/neon-auth-standards.md +3 -1
- package/framework/rules/neon-standards.md +6 -4
- package/framework/rules/nextjs-standards.md +13 -11
- package/framework/rules/nextjs-templates.md +1 -1
- package/framework/rules/resend-standards.md +3 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -52
- package/framework/skills/README.md +4 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
- package/framework/skills/core/morph-apply/SKILL.md +150 -223
- package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
- package/framework/skills/core/morph-machine/SKILL.md +4 -4
- package/framework/skills/core/morph-media/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/SKILL.md +138 -300
- package/framework/skills/core/morph-review/SKILL.md +112 -137
- package/framework/skills/core/morph-standards/SKILL.md +29 -41
- package/framework/skills/core/morph-uiux/SKILL.md +78 -153
- package/framework/standards/STANDARDS.json +90 -1219
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +2 -3
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
- package/framework/standards/ai-agents/prompt-sources.md +6 -4
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/architecture/code-comments.md +8 -8
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
- package/framework/standards/backend/api/validation.md +135 -47
- package/framework/standards/backend/database/postgresql/database.md +11 -7
- package/framework/standards/backend/database/vector-search-rag.md +207 -324
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
- package/framework/standards/backend/dotnet/result-pattern.md +40 -6
- package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
- package/framework/standards/backend/dotnet/scrutor.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
- package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
- package/framework/standards/data/nosql/cache/redis.md +120 -44
- package/framework/standards/data/nosql/cosmos-db.md +114 -71
- package/framework/standards/frontend/design-system/animations.md +1 -1
- package/framework/standards/frontend/design-system/naming.md +1 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
- package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
- package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
- 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/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
- 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/MediaDisclosureTests.cs +1 -1
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
- package/framework/templates/infrastructure/github/README.md +2 -1
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
- 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/observability/telemetry.js +3 -5
- 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 +6 -1
- 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 +33 -19
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +95 -138
- 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 +86 -95
- 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 +41 -82
- package/src/core/paths/output-schema.js +20 -67
- 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/App.jsx +0 -2
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +7 -8
- package/src/dashboard/app/components/EntityLink.jsx +1 -2
- package/src/dashboard/app/components/Markdown.jsx +1 -1
- package/src/dashboard/app/components/Sidebar.jsx +1 -2
- 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/RulesMap.jsx +3 -3
- package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +6 -16
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +5 -23
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/index.js +0 -2
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/rules-scanner.js +15 -10
- package/src/dashboard/scanner/skills-scanner.js +2 -3
- 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 +134 -135
- 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-profile.js +1 -1
- 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 +24 -48
- package/src/lib/validators/reuse/symbol-scanner.js +2 -4
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/validation-runner.js +8 -88
- package/src/lib/validators/verify-runner.js +134 -301
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +42 -22
- package/src/utils/agents-installer.js +92 -13
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +34 -6
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/commands/commit.md +0 -166
- 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/rubrics/backend.json +0 -41
- package/framework/rubrics/default.json +0 -36
- package/framework/rubrics/frontend.json +0 -36
- 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/project/validate.js +0 -97
- 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/dashboard/app/pages/Rubrics.jsx +0 -97
- package/src/dashboard/scanner/rubrics-scanner.js +0 -12
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -91
- 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
- package/src/lib/validators/architecture/architecture-validator.js +0 -270
- package/src/lib/validators/architecture/index.js +0 -7
- package/src/lib/validators/css/css-validator.js +0 -340
- package/src/lib/validators/css/index.js +0 -7
- package/src/lib/validators/design-system/design-system-validator.js +0 -237
- package/src/lib/validators/design-system/index.js +0 -7
- package/src/lib/validators/score-aggregator.js +0 -244
- package/src/lib/validators/ui/index.js +0 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
|
@@ -1,75 +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
|
+
import { shouldPauseGate } from '../../lib/risk-detector.js';
|
|
10
|
+
import { collectSignals, collectReviewRequirement } from './gate-check.js';
|
|
11
|
+
import { autoApprovalRefusal, printRefusal } from './auto-approval.js';
|
|
12
|
+
import { readTrustFromConfig } from './gate-decision.js';
|
|
9
13
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
10
|
-
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
11
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';
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
* The literal token `review-report.md` carries to declare which tree it
|
|
15
|
-
* reviewed. Kept as one exported constant so the CLI parser, the warning text
|
|
16
|
-
* and the `morph-review` skill can never drift apart (the A5 lesson).
|
|
17
|
-
*/
|
|
18
|
-
export const REVIEWED_SHA_TOKEN = '**Reviewed-SHA:**';
|
|
18
|
+
export { REVIEWED_SHA_TOKEN };
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const SIGNED_OFF = { required: [], reports: {}, checks: { status: 'ok', total: 0, missing: [], notProven: [] } };
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Do the review reports still describe the current tree? Every report present
|
|
24
|
+
* is compared; the worst answer wins.
|
|
24
25
|
*
|
|
25
|
-
* @param {
|
|
26
|
-
* @returns {{ status: 'ok'|'stale'|'unstamped'|'skip', reviewed?: string, head?: string, ahead?: number }}
|
|
27
|
-
* `skip
|
|
28
|
-
* 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.
|
|
29
29
|
*/
|
|
30
|
-
function reviewStaleness(
|
|
30
|
+
function reviewStaleness(reports) {
|
|
31
|
+
if (reports.verifier.status === 'missing') return { status: 'skip' };
|
|
31
32
|
const cwd = process.cwd();
|
|
32
|
-
const reportPath = join(cwd, '.morph', 'features', featureName, '4-review', 'review-report.md');
|
|
33
|
-
if (!existsSync(reportPath)) return { status: 'skip' };
|
|
34
|
-
|
|
35
33
|
let head;
|
|
36
34
|
try {
|
|
37
35
|
head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
|
|
38
|
-
} catch {
|
|
39
|
-
return { status: 'skip' }; // not a git repo (or no commits) — nothing to compare
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let body = '';
|
|
43
|
-
try {
|
|
44
|
-
body = readFileSync(reportPath, 'utf8');
|
|
45
36
|
} catch {
|
|
46
37
|
return { status: 'skip' };
|
|
47
38
|
}
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 };
|
|
60
63
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
let ahead = 0;
|
|
64
|
-
try {
|
|
65
|
-
ahead = execFileSync('git', ['rev-list', '--count', `${reviewed}..${head}`], { cwd, stdio: 'pipe' })
|
|
66
|
-
.toString().trim();
|
|
67
|
-
ahead = Number(ahead) || 0;
|
|
68
|
-
} catch { ahead = 0; }
|
|
69
|
-
|
|
70
|
-
// HEAD not descending from the reviewed commit (a rebase, a different branch)
|
|
71
|
-
// is still "not the tree that was reviewed".
|
|
72
|
-
return { status: 'stale', reviewed, head, ahead };
|
|
64
|
+
return unstamped ?? { status: 'ok', head };
|
|
73
65
|
}
|
|
74
66
|
|
|
75
67
|
/**
|
|
@@ -145,7 +137,7 @@ const program = new Command();
|
|
|
145
137
|
/**
|
|
146
138
|
* Valid approval gate names
|
|
147
139
|
*/
|
|
148
|
-
const VALID_GATES = ['
|
|
140
|
+
const VALID_GATES = ['plan', 'uiux', 'review'];
|
|
149
141
|
|
|
150
142
|
/**
|
|
151
143
|
* Approve action function - Mark a gate as approved
|
|
@@ -189,12 +181,13 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
189
181
|
// with nowhere to go. The way out already exists as `unapprove`; the
|
|
190
182
|
// message just never named it. Naming it is the whole fix (no new flag).
|
|
191
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 }));
|
|
192
185
|
return;
|
|
193
186
|
}
|
|
194
187
|
|
|
195
188
|
// Gate monotonicity: a gate may only be approved once its predecessors are
|
|
196
|
-
// approved (
|
|
197
|
-
//
|
|
189
|
+
// approved (plan → review; uiux requires plan). Prevents the shape of a
|
|
190
|
+
// review approved with the plan still pending. Policy-stamped
|
|
198
191
|
// gates count as approved, so chore/hotfix satisfy prereqs naturally.
|
|
199
192
|
// Escape via --force for deliberate recovery.
|
|
200
193
|
const unmet = unmetPrereqs(gateName, feature.approvalGates);
|
|
@@ -231,24 +224,14 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
231
224
|
approvedBy = 'auto';
|
|
232
225
|
}
|
|
233
226
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// FORCED_PAUSE_WORK_TYPES), enforced server-side rather than trusting the
|
|
238
|
-
// calling skill. It gates on the RESOLVED approvedBy, NOT the raw --mode
|
|
239
|
-
// string: every headless context (claude -p, CI, the Bash tool) runs
|
|
240
|
-
// without a TTY, so an omitted --mode — or any --mode ≠ 'auto' — still
|
|
241
|
-
// resolves to "auto" and would otherwise rubber-stamp the gate. A human
|
|
242
|
-
// approval is --approver <name> or an interactive TTY ($USER); anything
|
|
243
|
-
// resolving to "auto" is refused here.
|
|
244
|
-
if (gateName === 'review' && feature.workType === 'hotfix' && approvedBy === 'auto') {
|
|
245
|
-
console.error(chalk.red('\n❌ Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.'));
|
|
246
|
-
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);
|
|
247
230
|
process.exit(1);
|
|
248
231
|
}
|
|
249
232
|
|
|
250
233
|
// Gate 3 staleness backstop (D1): a review reviews a TREE, and the tree
|
|
251
|
-
// moves. Observed: review
|
|
234
|
+
// moves. Observed: a review report written at b30ac6e, then T3 landed and
|
|
252
235
|
// inverted the default of a business rule; the report stayed in the repo
|
|
253
236
|
// claiming 976/976 tests (they were 982) and never mentioning T3. Nothing
|
|
254
237
|
// detected that the review artifact had fallen behind the code it reviews.
|
|
@@ -257,93 +240,49 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
257
240
|
// A stamp that is BEHIND HEAD refuses. A MISSING stamp only warns — reports
|
|
258
241
|
// written before this existed must not brick a legacy feature — but the
|
|
259
242
|
// warning names the exact token to add, so it teaches instead of nagging.
|
|
243
|
+
const reports = gateName === 'review' ? reviewReports(process.cwd(), featureName) : null;
|
|
260
244
|
if (gateName === 'review') {
|
|
261
|
-
const stale = reviewStaleness(
|
|
245
|
+
const stale = reviewStaleness(reports);
|
|
262
246
|
if (stale.status === 'stale') {
|
|
263
|
-
console.error(chalk.red(
|
|
247
|
+
console.error(chalk.red(`\n❌ O \`4-review/${stale.file}\` está desatualizado em relação ao código.`));
|
|
264
248
|
console.error(chalk.gray(` Revisado em: ${stale.reviewed.slice(0, 8)}`));
|
|
265
249
|
console.error(chalk.gray(` HEAD atual: ${stale.head.slice(0, 8)}${stale.ahead ? ` (${stale.ahead} commit(s) depois da revisão)` : ''}`));
|
|
266
250
|
console.error(chalk.gray(' Um review revisa uma ÁRVORE, e a árvore andou. Re-rode a `morph-review`'));
|
|
267
|
-
console.error(chalk.gray(
|
|
251
|
+
console.error(chalk.gray(` e atualize o \`${REVIEWED_SHA_TOKEN}\` de cada relatório.\n`));
|
|
268
252
|
process.exit(1);
|
|
269
253
|
}
|
|
270
254
|
if (stale.status === 'unstamped') {
|
|
271
|
-
console.log(chalk.yellow(
|
|
272
|
-
console.log(chalk.gray(` Adicione a linha
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Skipped-pause backstop: a gate whose recorded decision demanded a human
|
|
277
|
-
// pause (gateDecisions[gate].pause === true) cannot then be stamped by a
|
|
278
|
-
// non-human signature. The detection already existed — in gate-guard.js —
|
|
279
|
-
// but that hook is registered for `Write|Edit|AskUserQuestion`, and a Bash
|
|
280
|
-
// tool call fires no hook at all. Since MORPH.md requires gate approval to
|
|
281
|
-
// go through THIS command and forbids hand-editing the state, the only path
|
|
282
|
-
// the hook could ever see was the one the charter bans. The check belongs
|
|
283
|
-
// where the stamp is applied, not beside it.
|
|
284
|
-
//
|
|
285
|
-
// Gated on the RESOLVED `approvedBy`, matching the hotfix backstop above:
|
|
286
|
-
// an omitted --mode in a headless context also resolves to "auto" and must
|
|
287
|
-
// not slip through. `--force` does NOT bypass this either — it is the
|
|
288
|
-
// out-of-order recovery flag, a different concern. The way out is a human
|
|
289
|
-
// signature, or rewriting the decision that asked for the pause.
|
|
290
|
-
const decision = feature.gateDecisions?.[gateName];
|
|
291
|
-
if (decision?.pause === true && approvedBy === 'auto') {
|
|
292
|
-
console.error(chalk.red(`\n❌ O Gate "${gateName}" registrou pause:true — auto-aprovação não se aplica.`));
|
|
293
|
-
console.error(chalk.gray(` Sinal: ${decision.reason || 'risco detectado'}`));
|
|
294
|
-
console.error(chalk.gray(' A decisão gravada exigia pausa humana. Aprove com --approver <nome>, num'));
|
|
295
|
-
console.error(chalk.gray(' terminal interativo, ou pela resposta humana via AskUserQuestion (state-sync).'));
|
|
296
|
-
console.error(chalk.gray(' Se o sinal de risco já não vale, regrave a decisão:'));
|
|
297
|
-
console.error(chalk.gray(` morph-spec gate-decision ${featureName} ${gateName} --pause false --reason "<motivo>"\n`));
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// The same backstop when no gate-check ran: the pause it WOULD record for
|
|
302
|
-
// the screen checklist, by the predicate advance.js reads too. `--force`
|
|
303
|
-
// does not bypass it, for the reason above.
|
|
304
|
-
if (gateName === 'uiux' && approvedBy === 'auto') {
|
|
305
|
-
const block = uiuxAutoApprovalBlock(process.cwd(), featureName, feature);
|
|
306
|
-
if (block) {
|
|
307
|
-
console.error(chalk.red(`\n❌ O Gate "uiux" não pode ser auto-aprovado — ${block}.`));
|
|
308
|
-
console.error(chalk.gray(' Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com'));
|
|
309
|
-
console.error(chalk.gray(' --approver <nome>, ou, se a pausa já foi decidida, grave a decisão:'));
|
|
310
|
-
console.error(chalk.gray(` morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"\n`));
|
|
311
|
-
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.`));
|
|
312
257
|
}
|
|
313
258
|
}
|
|
314
259
|
|
|
315
260
|
let selfAssessed = false;
|
|
316
261
|
if (gateName === 'review') {
|
|
317
262
|
if (feature.workType === 'hotfix') {
|
|
318
|
-
// Hotfix
|
|
319
|
-
// (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
// a degradation for a report that was never supposed to exist). The human
|
|
325
|
-
// signature is already enforced above (line ~215) — this only checks the
|
|
326
|
-
// artifact the skill actually produces.
|
|
327
|
-
const reviewReport = join(process.cwd(), '.morph', 'features', featureName, '4-review', 'review-report.md');
|
|
328
|
-
if (!existsSync(reviewReport)) {
|
|
329
|
-
console.error(chalk.red('\n❌ Gate 3 de hotfix exige `4-review/review-report.md` — não existe.'));
|
|
330
|
-
console.error(chalk.gray(' É o artefato que a `morph-hotfix` produz (diff do fix + evidência real de'));
|
|
331
|
-
console.error(chalk.gray(' teste do `morph-spec verify`), não o `evaluator-report.md` do avaliador'));
|
|
332
|
-
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'));
|
|
333
269
|
console.error(chalk.gray(' de aprovar; a assinatura humana já é exigida pelo backstop acima.\n'));
|
|
334
270
|
process.exit(1);
|
|
335
271
|
}
|
|
336
272
|
} else {
|
|
337
|
-
const
|
|
338
|
-
|
|
273
|
+
const { required } = collectReviewRequirement(process.cwd(), feature);
|
|
274
|
+
const unusable = required.filter((role) => reports[role].status !== 'ok');
|
|
275
|
+
if (unusable.length) {
|
|
339
276
|
if (!options.selfAssessed) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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}.`));
|
|
345
284
|
}
|
|
346
|
-
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.`));
|
|
347
286
|
console.error(chalk.gray(' Se o dispatch for impossível nesta sessão, aprove com --self-assessed —'));
|
|
348
287
|
console.error(chalk.gray(' a degradação fica registrada no feature.json e aparece no `list`/`fleet`.\n'));
|
|
349
288
|
process.exit(1);
|
|
@@ -351,9 +290,26 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
351
290
|
selfAssessed = true;
|
|
352
291
|
}
|
|
353
292
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
293
|
+
// Sem gate-check prévio não há `gateDecisions.review` para o backstop acima morder; em
|
|
294
|
+
// campo, ships passaram assim com verify vermelho ou ausente. A auto-aprovação recalcula
|
|
295
|
+
// a decisão pela MESMA régua do gate-check. --self-assessed já dispensou os relatórios.
|
|
296
|
+
if (approvedBy === 'auto') {
|
|
297
|
+
const signals = collectSignals(featureName, feature, process.cwd(), 'review');
|
|
298
|
+
if (selfAssessed) signals.reviewReports = SIGNED_OFF;
|
|
299
|
+
const { pause, reason } = shouldPauseGate({ trust: readTrustFromConfig() }, signals);
|
|
300
|
+
if (pause) {
|
|
301
|
+
console.error(chalk.red(`\n❌ Gate 3 não pode ser auto-aprovado — ${reason}.`));
|
|
302
|
+
console.error(chalk.gray(' Corrija e rode de novo, ou leve a decisão ao humano (--approver <nome>).\n'));
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
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
|
+
}
|
|
357
313
|
}
|
|
358
314
|
}
|
|
359
315
|
}
|
|
@@ -367,7 +323,7 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
367
323
|
|
|
368
324
|
// Fleet handoff (mirrors advance.js): approving a gate is exactly when a
|
|
369
325
|
// feature hands over between sessions too — `approve` is the real path
|
|
370
|
-
//
|
|
326
|
+
// the plan gate took in the field (`approve <f> plan --mode auto`), not just
|
|
371
327
|
// `advance`, so a handoff.md left stale after approve was the common
|
|
372
328
|
// case, not the edge case. Same helper, same fail-open contract: a
|
|
373
329
|
// regeneration failure must never fail the approval itself.
|
|
@@ -377,11 +333,12 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
377
333
|
} catch { /* fail-open */ }
|
|
378
334
|
|
|
379
335
|
if (selfAssessed) {
|
|
380
|
-
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).'));
|
|
381
337
|
console.log(chalk.gray(' Registrado em feature.json → approvalGates.review.selfAssessed.'));
|
|
382
338
|
}
|
|
383
339
|
|
|
384
340
|
console.log(chalk.green(`\n✅ Gate "${gateName}" approved for feature: ${featureName}`));
|
|
341
|
+
if (gateName === 'plan') reportDotnetIgnores(seedDotnetBuildIgnores(process.cwd(), { featureName }));
|
|
385
342
|
console.log(chalk.gray(`Workflow can now proceed past ${gateName} phase\n`));
|
|
386
343
|
|
|
387
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
|
+
}
|