@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- 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 +86 -21
- 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-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +14 -72
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- 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 +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +3 -3
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +7 -7
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -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/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +18 -105
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +17 -111
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -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 +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +2 -1
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +17 -17
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +31 -126
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- 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/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- 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/dotnet-test-introspection.js +0 -632
- 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/utils/agent-id-mapper.js +0 -11
|
@@ -1,189 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* PreToolUse Hook: Protect Approved Spec Files
|
|
5
|
-
*
|
|
6
|
-
* Event: PreToolUse | Matcher: Write|Edit
|
|
7
|
-
*
|
|
8
|
-
* Blocks Write/Edit to spec artifacts after their approval gate has passed:
|
|
9
|
-
* - 2-plan/spec.md, etc. → blocked if 'plan' gate approved
|
|
10
|
-
* - 1-design/design-system.md, etc. → blocked if 'uiux' gate approved
|
|
11
|
-
*
|
|
12
|
-
* Semantic exception for files in MUTABLE_SPEC_FIELDS (e.g. tasks.json):
|
|
13
|
-
* the plan contract is frozen, but per-task progress fields (status, outputs,
|
|
14
|
-
* notes) may still change during the implement phase. For those files BOTH a
|
|
15
|
-
* full Write and a targeted Edit are allowed — but only when the semantic diff
|
|
16
|
-
* touches solely the mutable fields. The Edit is simulated (old_string →
|
|
17
|
-
* new_string applied to the current file) and diffed exactly like a Write, so a
|
|
18
|
-
* one-field status flip via Edit is permitted while any change to the frozen
|
|
19
|
-
* plan contract (titles, dependencies, doneCriteria, the task set) is blocked.
|
|
20
|
-
* This is the sanctioned progress path — the agent never has to rewrite the
|
|
21
|
-
* whole file nor reach for a Node script to record task status.
|
|
22
|
-
*
|
|
23
|
-
* A gate freezes the APPROVED ARTIFACT, so a protected file that does not yet
|
|
24
|
-
* exist on disk is a CREATION and passes — the criterion is the file's
|
|
25
|
-
* existence, never who signed the gate (see the carve-out in run()).
|
|
26
|
-
*
|
|
27
|
-
* Fail-open: exits 0 on any error.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
3
|
import { existsSync, readFileSync } from 'fs';
|
|
31
4
|
import { stateExists, isGateApproved } from '../../shared/state-reader.js';
|
|
32
5
|
import {
|
|
33
6
|
isFeaturePath,
|
|
34
7
|
extractFeatureName,
|
|
35
8
|
getBasename,
|
|
36
|
-
PROTECTED_SPEC_FILES
|
|
37
|
-
MUTABLE_SPEC_FIELDS
|
|
9
|
+
PROTECTED_SPEC_FILES
|
|
38
10
|
} from '../../shared/phase-utils.js';
|
|
39
11
|
import { blockResult, runStandalone } from '../../shared/hook-response.js';
|
|
40
12
|
import { logHookActivity } from '../../shared/activity-logger.js';
|
|
41
13
|
import { getFilePath, getToolName, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Compare two tasks.json structures and return the list of plan-contract
|
|
46
|
-
* violations — changes that touch anything other than the mutable fields.
|
|
47
|
-
*
|
|
48
|
-
* Frozen-by-default, not an enumerated frozen list: the loop below walks the
|
|
49
|
-
* UNION of old/new task keys and flags any key not in `mutableFields`. A new
|
|
50
|
-
* plan-contract field (e.g. `agent`/`standards`, stamped by morph-plan — R2
|
|
51
|
-
* token-cut) needs NO entry added here to become protected; it is frozen the
|
|
52
|
-
* moment it exists on a task, simply by never being added to
|
|
53
|
-
* MUTABLE_SPEC_FIELDS (output-schema.js → `npm run generate` →
|
|
54
|
-
* framework/hooks/shared/phase-utils.js). Only fields that must stay
|
|
55
|
-
* EDITABLE post-Gate-2 (today: status, outputs, notes) belong in that list.
|
|
56
|
-
*
|
|
57
|
-
* @param {object} oldDoc - parsed previous file content
|
|
58
|
-
* @param {object} newDoc - parsed proposed file content
|
|
59
|
-
* @param {string[]} mutableFields - field names allowed to change
|
|
60
|
-
* @returns {string[]} human-readable violation messages (empty = OK)
|
|
61
|
-
*/
|
|
62
|
-
function diffPlanContract(oldDoc, newDoc, mutableFields) {
|
|
63
|
-
const violations = [];
|
|
64
|
-
const mutable = new Set(mutableFields);
|
|
65
|
-
|
|
66
|
-
// Backfilling a previously-absent `version` (undefined → any value) is a
|
|
67
|
-
// benign schema-tooling update, not a plan-contract change — it's what
|
|
68
|
-
// lets legacy tasks.json adopt the field so `morph-spec dag` can analyze
|
|
69
|
-
// it without a lenient fallback. An actual change between two SET
|
|
70
|
-
// versions is still a real contract change and stays blocked.
|
|
71
|
-
const versionBackfilled = oldDoc.version === undefined && newDoc.version !== undefined;
|
|
72
|
-
if (!versionBackfilled && oldDoc.version !== newDoc.version) {
|
|
73
|
-
violations.push(`'version' changed (${oldDoc.version} → ${newDoc.version})`);
|
|
74
|
-
}
|
|
75
|
-
if (oldDoc.feature !== newDoc.feature) {
|
|
76
|
-
violations.push(`'feature' changed (${oldDoc.feature} → ${newDoc.feature})`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const oldTasks = Array.isArray(oldDoc.tasks) ? oldDoc.tasks : [];
|
|
80
|
-
const newTasks = Array.isArray(newDoc.tasks) ? newDoc.tasks : [];
|
|
81
|
-
|
|
82
|
-
if (oldTasks.length !== newTasks.length) {
|
|
83
|
-
violations.push(`task count changed (${oldTasks.length} → ${newTasks.length}) — tasks cannot be added or removed`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const oldById = new Map(oldTasks.map(t => [t.id, t]));
|
|
87
|
-
const newById = new Map(newTasks.map(t => [t.id, t]));
|
|
88
|
-
|
|
89
|
-
// A task present before must still exist (no removals / id renames).
|
|
90
|
-
for (const id of oldById.keys()) {
|
|
91
|
-
if (!newById.has(id)) violations.push(`task '${id}' was removed`);
|
|
92
|
-
}
|
|
93
|
-
for (const id of newById.keys()) {
|
|
94
|
-
if (!oldById.has(id)) violations.push(`task '${id}' was added`);
|
|
95
|
-
}
|
|
14
|
+
import { ERRATA_HEADING, errataViolation } from '../../shared/errata.js';
|
|
96
15
|
|
|
97
|
-
|
|
98
|
-
for (const [id, newTask] of newById) {
|
|
99
|
-
const oldTask = oldById.get(id);
|
|
100
|
-
if (!oldTask) continue;
|
|
101
|
-
const keys = new Set([...Object.keys(oldTask), ...Object.keys(newTask)]);
|
|
102
|
-
for (const key of keys) {
|
|
103
|
-
if (mutable.has(key)) continue;
|
|
104
|
-
if (JSON.stringify(oldTask[key]) !== JSON.stringify(newTask[key])) {
|
|
105
|
-
violations.push(`task '${id}': immutable field '${key}' changed`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return violations;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* O heading que abre o bloco de errata, no fim de um arquivo congelado.
|
|
115
|
-
*
|
|
116
|
-
* Congelar o CONTRATO não é a mesma coisa que congelar um ERRO DE FATO. Caso
|
|
117
|
-
* real: a spec afirmava, entre parênteses, que certos registros também eram
|
|
118
|
-
* escritos por humanos; o código mostrava que os três escritores eram do
|
|
119
|
-
* próprio motor. A afirmação era falsa sobre o código, não uma mudança de
|
|
120
|
-
* escopo — mas a trava cega não distingue as duas coisas, então a mentira
|
|
121
|
-
* ficava na spec e a correção morava em outro arquivo. Quem abrisse a spec
|
|
122
|
-
* primeiro lia a versão errada.
|
|
123
|
-
*
|
|
124
|
-
* A errata resolve isso SEM abrir o contrato: o corpo continua byte-idêntico e
|
|
125
|
-
* só se acrescenta texto ao fim, num bloco identificado. Escopo negociado ⇒
|
|
126
|
-
* revogar o gate. Fato errado sobre o código ⇒ errata.
|
|
127
|
-
*/
|
|
128
|
-
const ERRATA_HEADING = '## Errata';
|
|
129
|
-
|
|
130
|
-
/** Índice onde começa o bloco de errata (-1 se não há). Só reconhece o heading em início de linha. */
|
|
131
|
-
function errataIndex(text) {
|
|
132
|
-
if (text.startsWith(ERRATA_HEADING)) return 0;
|
|
133
|
-
const at = text.indexOf(`\n${ERRATA_HEADING}`);
|
|
134
|
-
return at === -1 ? -1 : at + 1;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Corpo congelado = tudo ANTES do bloco de errata (ou o texto inteiro, se não
|
|
139
|
-
* há bloco), sem o branco final.
|
|
140
|
-
*
|
|
141
|
-
* O `trimEnd` não é frouxidão: separar o bloco por uma linha em branco é o
|
|
142
|
-
* formato markdown natural, e sem isso a primeira errata de todas seria
|
|
143
|
-
* recusada por "mudou o corpo" — quando o que mudou foi um `\n` de formatação.
|
|
144
|
-
* A garantia que importa (nenhum caractere SIGNIFICATIVO do texto aprovado
|
|
145
|
-
* muda) fica intacta, porque só espaço em branco terminal é ignorado.
|
|
146
|
-
*/
|
|
147
|
-
function frozenBody(text) {
|
|
148
|
-
const at = errataIndex(text);
|
|
149
|
-
return (at === -1 ? text : text.slice(0, at)).trimEnd();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* A mudança proposta é uma errata legítima?
|
|
154
|
-
*
|
|
155
|
-
* Três condições, todas necessárias — frouxo em qualquer uma delas e a trava
|
|
156
|
-
* cega vira uma trava que uma edição criativa atravessa:
|
|
157
|
-
* 1. o corpo congelado permanece BYTE-IDÊNTICO;
|
|
158
|
-
* 2. o resultado de fato tem um bloco `## Errata` (senão é edição do corpo);
|
|
159
|
-
* 3. a errata só CRESCE — o que já estava escrito nela não é reescrito.
|
|
160
|
-
*
|
|
161
|
-
* A (3) existe porque uma errata reescrita perde a serventia: o valor dela é
|
|
162
|
-
* ser trilha acumulada, não um segundo lugar editável.
|
|
163
|
-
*
|
|
164
|
-
* @returns {{ok: true} | {ok: false, why: string}}
|
|
165
|
-
*/
|
|
166
|
-
function errataViolation(original, proposed) {
|
|
167
|
-
if (frozenBody(proposed) !== frozenBody(original)) {
|
|
168
|
-
return { ok: false, why: 'o corpo da spec mudou — errata só ACRESCENTA ao fim, nunca altera o texto aprovado' };
|
|
169
|
-
}
|
|
170
|
-
if (errataIndex(proposed) === -1) {
|
|
171
|
-
return { ok: false, why: `o conteúdo novo precisa estar sob um bloco \`${ERRATA_HEADING}\` no fim do arquivo` };
|
|
172
|
-
}
|
|
173
|
-
const oldAt = errataIndex(original);
|
|
174
|
-
if (oldAt !== -1) {
|
|
175
|
-
const oldErrata = original.slice(oldAt);
|
|
176
|
-
if (!proposed.slice(errataIndex(proposed)).startsWith(oldErrata)) {
|
|
177
|
-
return { ok: false, why: 'a errata existente foi reescrita — ela é append-only, entradas anteriores não se apagam' };
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return { ok: true };
|
|
181
|
-
}
|
|
16
|
+
const CHECKS_FILE = 'checks.json';
|
|
182
17
|
|
|
183
|
-
/**
|
|
184
|
-
* Texto proposto para um arquivo totalmente travado, a partir de Write ou Edit.
|
|
185
|
-
* @returns {string|null} null quando o payload não permite reconstruir (fail-open).
|
|
186
|
-
*/
|
|
187
18
|
function proposedText(payload, toolName, original) {
|
|
188
19
|
if (toolName === 'Edit') {
|
|
189
20
|
const oldStr = payload?.tool_input?.old_string;
|
|
@@ -197,14 +28,8 @@ function proposedText(payload, toolName, original) {
|
|
|
197
28
|
return typeof content === 'string' ? content : null;
|
|
198
29
|
}
|
|
199
30
|
|
|
200
|
-
/**
|
|
201
|
-
* @param {object} payload - parsed PreToolUse stdin payload
|
|
202
|
-
* @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
|
|
203
|
-
* Never prints, never exits — see runStandalone()/chain.js at the bottom.
|
|
204
|
-
*/
|
|
205
31
|
export async function run(payload) {
|
|
206
32
|
try {
|
|
207
|
-
// Amend mode: bypass spec protection for legitimate in-implementation corrections
|
|
208
33
|
if (process.env.MORPH_AMEND_PHASE) {
|
|
209
34
|
console.error(`[morph-spec] AMEND MODE: bypassing spec protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
|
|
210
35
|
return null;
|
|
@@ -215,14 +40,9 @@ export async function run(payload) {
|
|
|
215
40
|
const filePath = getFilePath(payload);
|
|
216
41
|
if (!filePath) return null;
|
|
217
42
|
|
|
218
|
-
// Only check files inside .morph/features/
|
|
219
43
|
if (!isFeaturePath(filePath)) return null;
|
|
220
44
|
|
|
221
|
-
// The gate
|
|
222
|
-
// that OWNS it. Asking the session's tree instead made this guard go DARK in
|
|
223
|
-
// a secondary worktree: the root had no approved gate, so a post-Gate-2
|
|
224
|
-
// spec.md rewrite sailed through. Same defect as enforce-phase-writes,
|
|
225
|
-
// opposite direction — there it over-blocked, here it under-protected.
|
|
45
|
+
// The gate lives in the feature.json of the tree that OWNS the file: asking the session's tree left this guard dark in a worktree.
|
|
226
46
|
const root = resolveMorphRoot(filePath, process.cwd());
|
|
227
47
|
if (!stateExists(root)) return null;
|
|
228
48
|
|
|
@@ -232,133 +52,42 @@ export async function run(payload) {
|
|
|
232
52
|
const filename = getBasename(filePath);
|
|
233
53
|
const requiredGate = PROTECTED_SPEC_FILES[filename];
|
|
234
54
|
|
|
235
|
-
// Not a protected file
|
|
236
55
|
if (!requiredGate) return null;
|
|
237
56
|
|
|
238
|
-
//
|
|
57
|
+
// Generated only by the CLI: a hand-written one would carry checks the parser never judged.
|
|
58
|
+
// Decided before the creation carve-out, since chore/hotfix are born with the plan gate stamped.
|
|
59
|
+
if (filename === CHECKS_FILE) {
|
|
60
|
+
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
61
|
+
return blockResult(
|
|
62
|
+
`MORPH-SPEC: '${CHECKS_FILE}' is generated — never written by hand.\n` +
|
|
63
|
+
`Edit 2-plan/plan.md and run \`morph-spec plan-check ${featureName}\`: it parses the plan and writes ${CHECKS_FILE}.\n` +
|
|
64
|
+
`After the plan gate the plan is frozen: a wrong fact goes in \`${ERRATA_HEADING}\` (append-only, ${CHECKS_FILE} unchanged);\n` +
|
|
65
|
+
`a real scope change needs \`morph-spec unapprove ${featureName} plan\`, then plan-check and the gate again.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
239
69
|
if (!isGateApproved(featureName, requiredGate, root)) {
|
|
240
70
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
241
71
|
return null;
|
|
242
72
|
}
|
|
243
73
|
|
|
244
|
-
// ── Gate approved — file is locked ──────────────────────────────────────
|
|
245
74
|
const absPath = resolveTargetPath(filePath, root);
|
|
246
75
|
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// inicial, não a edição de uma spec fechada. O caso é real e não é raro —
|
|
250
|
-
// `chore`/`hotfix` nascem com `proposal`+`plan` carimbados `policy:{tipo}`
|
|
251
|
-
// (create.js → work-types.js skippedGates), então o primeiro `Write` de
|
|
252
|
-
// `2-plan/spec.md` já chega aqui com o gate "aprovado" sem que humano
|
|
253
|
-
// nenhum tenha aprovado coisa alguma — e era barrado com uma mensagem
|
|
254
|
-
// mandando revogar o gate de um arquivo inexistente.
|
|
255
|
-
//
|
|
256
|
-
// O critério é a EXISTÊNCIA, nunca o `approvedBy`. Liberar por assinatura
|
|
257
|
-
// (`policy:` passa, `lucas` não) transformaria este carve-out em licença
|
|
258
|
-
// para reescrever qualquer spec aprovada: bastaria apagá-la antes. Por ser
|
|
259
|
-
// sobre o disco, a janela fecha sozinha no instante em que o arquivo
|
|
260
|
-
// existe — os contra-canários de test/hooks/protect-spec-files.test.js
|
|
261
|
-
// pinam exatamente isso.
|
|
262
|
-
//
|
|
263
|
-
// A decisão é explícita e vale para as DUAS metades do hook. Antes, o ramo
|
|
264
|
-
// de campos mutáveis (tasks.json) já deixava a primeira criação passar, mas
|
|
265
|
-
// por acidente — o ENOENT caía no catch fail-open da leitura — enquanto o
|
|
266
|
-
// ramo travado bloqueava. Duas metades respondendo à mesma pergunta em
|
|
267
|
-
// direções opostas; agora respondem aqui, uma vez só. O `result` distinto
|
|
268
|
-
// no log é o que torna essa unificação observável.
|
|
76
|
+
// A file not yet on disk is its CREATION (chore/hotfix are born with `plan` stamped), not an edit of a frozen spec.
|
|
77
|
+
// The criterion is existence, never `approvedBy`: by signature, deleting a spec first would license rewriting it.
|
|
269
78
|
if (!existsSync(absPath)) {
|
|
270
79
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok:absent');
|
|
271
80
|
return null;
|
|
272
81
|
}
|
|
273
82
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// Files with mutable progress fields (e.g. tasks.json): allow a full Write
|
|
277
|
-
// that touches only those fields. Anything else stays blocked.
|
|
278
|
-
if (mutableFields) {
|
|
279
|
-
const toolName = getToolName(payload);
|
|
280
|
-
|
|
281
|
-
// Build the proposed document from either a full Write or a targeted Edit,
|
|
282
|
-
// then semantic-diff it against the current file on disk. Any failure to
|
|
283
|
-
// read / parse / apply → fail-open (pass), never a false block. O arquivo
|
|
284
|
-
// EXISTE aqui (a inexistência já foi decidida acima), então este catch
|
|
285
|
-
// volta a significar só o que dizia originalmente: não deu para diffar.
|
|
286
|
-
let oldDoc, newDoc;
|
|
287
|
-
try {
|
|
288
|
-
const original = readFileSync(absPath, 'utf-8');
|
|
289
|
-
oldDoc = JSON.parse(original);
|
|
290
|
-
|
|
291
|
-
if (toolName === 'Edit') {
|
|
292
|
-
// Simulate the edit: apply old_string → new_string to the current file.
|
|
293
|
-
const oldStr = payload?.tool_input?.old_string;
|
|
294
|
-
const newStr = payload?.tool_input?.new_string;
|
|
295
|
-
if (typeof oldStr !== 'string' || typeof newStr !== 'string') throw new Error('no edit strings');
|
|
296
|
-
const edited = payload?.tool_input?.replace_all
|
|
297
|
-
? original.split(oldStr).join(newStr)
|
|
298
|
-
: original.replace(oldStr, newStr);
|
|
299
|
-
newDoc = JSON.parse(edited);
|
|
300
|
-
} else {
|
|
301
|
-
const newContent = payload?.tool_input?.content;
|
|
302
|
-
if (typeof newContent !== 'string') throw new Error('no content');
|
|
303
|
-
newDoc = JSON.parse(newContent);
|
|
304
|
-
}
|
|
305
|
-
} catch {
|
|
306
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const violations = diffPlanContract(oldDoc, newDoc, mutableFields);
|
|
311
|
-
|
|
312
|
-
// Schema check on the proposed doc: even mutable-field writes must keep a
|
|
313
|
-
// valid status enum (a leaf agent writing status:"completed" was the
|
|
314
|
-
// production failure this closes). Only NEWLY introduced schema errors
|
|
315
|
-
// block — a pre-existing violation must not deadlock progress writes.
|
|
316
|
-
const preExisting = new Set(validateTasksDoc(oldDoc).errors);
|
|
317
|
-
violations.push(...validateTasksDoc(newDoc).errors.filter(e => !preExisting.has(e)));
|
|
318
|
-
|
|
319
|
-
// Planned-vs-real outputs diff: changing `outputs` is allowed, but the
|
|
320
|
-
// divergence must be explained in `notes` in the same edit.
|
|
321
|
-
violations.push(...outputsDivergenceViolations(oldDoc, newDoc));
|
|
322
|
-
|
|
323
|
-
if (violations.length === 0) {
|
|
324
|
-
// Only mutable progress fields changed — a legitimate implement-phase
|
|
325
|
-
// update via Write OR Edit.
|
|
326
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
331
|
-
return blockResult(
|
|
332
|
-
`MORPH-SPEC: '${filename}' write rejected — the '${requiredGate}' gate has been approved.\n` +
|
|
333
|
-
`This ${toolName} violates the plan contract or the tasks schema:\n` +
|
|
334
|
-
violations.map(v => ` • ${v}`).join('\n') + '\n\n' +
|
|
335
|
-
`During the implement phase only these fields may change: ${mutableFields.join(', ')}\n` +
|
|
336
|
-
`(status must stay in the schema enum, and outputs that diverge from the plan\n` +
|
|
337
|
-
`require an explanatory 'notes' in the same edit).\n` +
|
|
338
|
-
`To change the plan itself, ask the user to revoke the '${requiredGate}' gate,\n` +
|
|
339
|
-
`then re-present it for approval.`
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// ── Arquivo totalmente travado (spec.md, design) ────────────
|
|
344
|
-
// O corpo continua congelado. A ÚNICA passagem é a errata append-only:
|
|
345
|
-
// corpo byte-idêntico + texto novo sob `## Errata` no fim. Qualquer falha
|
|
346
|
-
// de leitura/reconstrução cai no bloqueio (o conservador aqui é BLOQUEAR —
|
|
347
|
-
// ao contrário do ramo do tasks.json, onde não conseguir diffar significa
|
|
348
|
-
// não ter como provar violação; aqui a regra base é "não se edita").
|
|
349
|
-
//
|
|
350
|
-
// O arquivo EXISTE neste ponto — a inexistência foi decidida lá em cima, e
|
|
351
|
-
// é a única coisa que este catch NÃO significa mais. ENOENT tratado como
|
|
352
|
-
// "ilegível" era o defeito F14; "ilegível de verdade" continua bloqueando.
|
|
83
|
+
// The only way through is the append-only errata; any read or rebuild failure falls into the block.
|
|
353
84
|
const toolName = getToolName(payload);
|
|
354
85
|
let errataCheck = null;
|
|
355
86
|
try {
|
|
356
87
|
const original = readFileSync(absPath, 'utf-8');
|
|
357
88
|
const proposed = proposedText(payload, toolName, original);
|
|
358
89
|
if (proposed !== null) errataCheck = errataViolation(original, proposed);
|
|
359
|
-
} catch {
|
|
360
|
-
// arquivo ilegível → sem errata possível, segue para o bloqueio
|
|
361
|
-
}
|
|
90
|
+
} catch {}
|
|
362
91
|
|
|
363
92
|
if (errataCheck?.ok) {
|
|
364
93
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
@@ -368,16 +97,15 @@ export async function run(payload) {
|
|
|
368
97
|
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
369
98
|
return blockResult(
|
|
370
99
|
`MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
|
|
371
|
-
`Editing approved
|
|
100
|
+
`Editing an approved artifact breaks its contract.\n\n` +
|
|
372
101
|
(errataCheck ? `Esta mudança não é uma errata válida: ${errataCheck.why}.\n\n` : '') +
|
|
373
|
-
`Corrigir um FATO ERRADO sobre o código (
|
|
374
|
-
`desmente) não exige revogar o gate: acrescente
|
|
102
|
+
`Corrigir um FATO ERRADO sobre o código (o texto aprovado afirma algo que o\n` +
|
|
103
|
+
`código desmente) não exige revogar o gate: acrescente sob \`${ERRATA_HEADING}\` no\n` +
|
|
375
104
|
`FIM do arquivo, deixando o corpo intacto — é append-only e fica na trilha.\n\n` +
|
|
376
|
-
`Mudar o ESCOPO acordado é outra coisa:
|
|
377
|
-
`
|
|
105
|
+
`Mudar o ESCOPO acordado é outra coisa: \`morph-spec unapprove ${featureName} ${requiredGate}\`,\n` +
|
|
106
|
+
`edite, rode plan-check quando for o plan.md, e re-apresente o gate para aprovação.`
|
|
378
107
|
);
|
|
379
108
|
} catch {
|
|
380
|
-
// Fail-open
|
|
381
109
|
return null;
|
|
382
110
|
}
|
|
383
111
|
}
|
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* state-sync.js — PostToolUse hook
|
|
4
|
-
*
|
|
5
|
-
* Keeps .morph/state.json in sync with the agent's actions, without requiring
|
|
6
|
-
* the agent to think about state. Fail-open: any error returns {} silently.
|
|
7
|
-
*
|
|
8
|
-
* Triggers:
|
|
9
|
-
* - Write/Edit of .morph/features/{feature}/0-proposal/proposal.md → register feature
|
|
10
|
-
* - Write/Edit of .morph/features/{feature}/{phase}/{output} → mark output created
|
|
11
|
-
* - AskUserQuestion answered with option starting "aprovado" → advance gate
|
|
12
|
-
*/
|
|
13
2
|
|
|
14
3
|
import { existsSync } from 'fs';
|
|
15
4
|
import { join } from 'path';
|
|
@@ -17,6 +6,7 @@ import { loadState, stateExists } from '../../shared/state-reader.js';
|
|
|
17
6
|
import { persistFeature, setGateApproved, createDefaultFeature, readFeatureFile } from '../../shared/state-writer.js';
|
|
18
7
|
import { resolveMorphRoot } from '../../shared/payload-utils.js';
|
|
19
8
|
import { contextResult, runStandalone } from '../../shared/hook-response.js';
|
|
9
|
+
import { gateFromQuestion } from '../../shared/gate-question.js';
|
|
20
10
|
import {
|
|
21
11
|
locateFeatureForQuestion,
|
|
22
12
|
allWorkTrees,
|
|
@@ -25,9 +15,8 @@ import {
|
|
|
25
15
|
} from '../../shared/feature-locator.js';
|
|
26
16
|
|
|
27
17
|
const PHASE_FOLDERS = {
|
|
28
|
-
'0-proposal': 'proposal',
|
|
29
18
|
'1-design': 'design',
|
|
30
|
-
'1-ui': 'uiux', // legacy alias
|
|
19
|
+
'1-ui': 'uiux', // legacy alias
|
|
31
20
|
'2-plan': 'plan',
|
|
32
21
|
'3-implement': 'implement',
|
|
33
22
|
'4-review': 'review',
|
|
@@ -62,8 +51,6 @@ function registerFeatureIfMissing(state, featureName) {
|
|
|
62
51
|
if (!state.features) state.features = {};
|
|
63
52
|
if (state.features[featureName]) return false;
|
|
64
53
|
|
|
65
|
-
// Canonical object-form gates (incl. optional uiux) + stamped identity
|
|
66
|
-
// (name=slug, workType, createdAt) — no anonymous shell, no boolean drift.
|
|
67
54
|
state.features[featureName] = createDefaultFeature(featureName);
|
|
68
55
|
return true;
|
|
69
56
|
}
|
|
@@ -94,24 +81,8 @@ function isApprovalAnswer(answer) {
|
|
|
94
81
|
);
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// Checked BEFORE the generic patterns below: the pinned uiux template is
|
|
100
|
-
// "Gate uiux — Design de {feature} aprovado?" (morph-uiux/SKILL.md) — it
|
|
101
|
-
// must never fall through to 'plan' via a stray "plan"/"plano" substring in
|
|
102
|
-
// the feature name. `/gate\s+uiux/i` alone is unambiguous either way.
|
|
103
|
-
if (/gate\s+uiux/i.test(question)) return 'uiux';
|
|
104
|
-
const lower = question.toLowerCase();
|
|
105
|
-
if (lower.includes('gate 1') || lower.includes('proposta')) return 'proposal';
|
|
106
|
-
if (lower.includes('gate 2') || lower.includes('plano') || lower.includes('plan')) return 'plan';
|
|
107
|
-
if (lower.includes('gate 3') || lower.includes('review')) return 'review';
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Gate monotonicity — mirror of src/core/state/gate-order.js (hooks are
|
|
112
|
-
// installed standalone and cannot import from src/). Keep in sync:
|
|
113
|
-
// proposal → plan → review; uiux requires proposal.
|
|
114
|
-
const GATE_PREREQ = { proposal: [], uiux: ['proposal'], plan: ['proposal'], review: ['plan'] };
|
|
84
|
+
// Mirror of src/core/state/gate-order.js: hooks are installed standalone and cannot import src/.
|
|
85
|
+
const GATE_PREREQ = { plan: [], uiux: ['plan'], review: ['plan'] };
|
|
115
86
|
function unmetGatePrereqs(gate, gates) {
|
|
116
87
|
const isApproved = (g) => gates?.[g]?.approved === true || gates?.[g] === true;
|
|
117
88
|
return (GATE_PREREQ[gate] || []).filter((p) => !isApproved(p));
|
|
@@ -122,9 +93,7 @@ function handleWriteEdit(input, sessionRoot) {
|
|
|
122
93
|
const featureName = extractFeatureName(filePath);
|
|
123
94
|
if (!featureName) return false;
|
|
124
95
|
|
|
125
|
-
// Sync the tree that OWNS the written file
|
|
126
|
-
// instead registered the feature (and its outputs) in the wrong `.morph/`
|
|
127
|
-
// whenever a root-rooted session wrote into a secondary worktree.
|
|
96
|
+
// Sync the tree that OWNS the written file: a root session also writes into worktrees.
|
|
128
97
|
const cwd = resolveMorphRoot(filePath, sessionRoot);
|
|
129
98
|
if (!stateExists(cwd)) return false;
|
|
130
99
|
|
|
@@ -139,8 +108,6 @@ function handleWriteEdit(input, sessionRoot) {
|
|
|
139
108
|
}
|
|
140
109
|
|
|
141
110
|
if (changed) {
|
|
142
|
-
// Persist the touched feature to its authoritative feature.json and refresh
|
|
143
|
-
// the thin root index. The feature's state thus travels in its own folder.
|
|
144
111
|
persistFeature(featureName, state.features[featureName], cwd);
|
|
145
112
|
}
|
|
146
113
|
return true;
|
|
@@ -156,22 +123,11 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
156
123
|
const gate = gateFromQuestion(question);
|
|
157
124
|
if (!gate) continue;
|
|
158
125
|
|
|
159
|
-
//
|
|
160
|
-
// that owns it — never to "the first in_progress feature of whatever tree
|
|
161
|
-
// the session sits in", which is how a Gate 2 for one feature was stamped
|
|
162
|
-
// onto `export-hub`, leaving it with G2 approved and G1 never asked.
|
|
126
|
+
// The feature the QUESTION names, in the tree that owns it — never the session tree's first in-progress feature.
|
|
163
127
|
const located = locateFeatureForQuestion(question, sessionRoot);
|
|
164
128
|
if (!located) {
|
|
165
|
-
// Refuse to guess
|
|
166
|
-
//
|
|
167
|
-
// explicit remediation step.
|
|
168
|
-
//
|
|
169
|
-
// The remediation differs by WHY it refused. When the question names a
|
|
170
|
-
// specific slug that no known tree has registered (a hotfix gate asked
|
|
171
|
-
// before `create` ran — the case that used to fall through to "the
|
|
172
|
-
// tree's one feature"), `morph-spec approve <that-name> <gate>` is a
|
|
173
|
-
// dead end: approve.js does `getFeature()` and hard-refuses "Feature
|
|
174
|
-
// not found". Name the real fix instead of the wrong command.
|
|
129
|
+
// Refuse to guess: a gate on the wrong feature inverts gate order. For an unregistered slug
|
|
130
|
+
// `approve` is a dead end ("Feature not found"), so the remediation names `create`.
|
|
175
131
|
const allNames = allWorkTrees(sessionRoot).flatMap((r) => featureNamesIn(r));
|
|
176
132
|
const candidate = unrecognizedSlugLike(question, allNames);
|
|
177
133
|
const remediation = candidate
|
|
@@ -190,20 +146,14 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
190
146
|
const { name: featureName, root } = located;
|
|
191
147
|
if (!stateExists(root)) continue;
|
|
192
148
|
|
|
193
|
-
//
|
|
194
|
-
// view so a feature carried only by the thin index still reports its real
|
|
195
|
-
// gates (otherwise every predecessor reads as unapproved and the
|
|
196
|
-
// out-of-order warning below fires on a perfectly ordered approval).
|
|
149
|
+
// Fallback to the merged view: a feature only in the thin index would read every predecessor as unapproved.
|
|
197
150
|
const feature = readFeatureFile(featureName, root) || loadState(root)?.features?.[featureName];
|
|
198
151
|
const gates = feature?.approvalGates;
|
|
199
152
|
const existing = gates?.[gate];
|
|
200
153
|
const alreadyApproved = existing === true || existing?.approved === true;
|
|
201
154
|
if (alreadyApproved) continue;
|
|
202
155
|
|
|
203
|
-
//
|
|
204
|
-
// (never block a human in the loop), but if a predecessor gate is still
|
|
205
|
-
// pending we surface a non-blocking warning — the CLI `morph-spec approve`
|
|
206
|
-
// hard-refuses the same shape; here we alert so the operator can reconcile.
|
|
156
|
+
// Out of order still approves (never block a human in the loop); `morph-spec approve` refuses the same shape.
|
|
207
157
|
const unmet = unmetGatePrereqs(gate, gates);
|
|
208
158
|
if (unmet.length) {
|
|
209
159
|
warnings.push(
|
|
@@ -213,18 +163,12 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
213
163
|
);
|
|
214
164
|
}
|
|
215
165
|
|
|
216
|
-
//
|
|
217
|
-
// Route through the shared canonical gate writer so the hook and the CLI
|
|
218
|
-
// (`morph-spec approve`) produce one consistent shape
|
|
219
|
-
// ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
|
|
220
|
-
// and a manual approval stays distinguishable from a policy auto-approval.
|
|
221
|
-
// Same contract as `approve`'s --self-assessed: a human may ship Gate 3 without the
|
|
222
|
-
// independent evaluator, but the degradation is stamped, never silent.
|
|
166
|
+
// Same contract as `approve --self-assessed`: approving review without its reports is stamped, never silent.
|
|
223
167
|
const selfAssessed = gate === 'review' && feature?.workType !== 'hotfix'
|
|
224
|
-
&& !existsSync(join(root, '.morph', 'features', featureName, '4-review',
|
|
168
|
+
&& ['verifier.md', 'reviewer.md'].some((f) => !existsSync(join(root, '.morph', 'features', featureName, '4-review', f)));
|
|
225
169
|
if (selfAssessed) {
|
|
226
170
|
warnings.push(
|
|
227
|
-
`⚠️ [morph-spec] Gate 3 de "${featureName}" aprovado SEM
|
|
171
|
+
`⚠️ [morph-spec] Gate 3 de "${featureName}" aprovado SEM os relatórios de review independentes (verifier.md, reviewer.md) — ` +
|
|
228
172
|
`registrado como selfAssessed (aparece no list/fleet).`
|
|
229
173
|
);
|
|
230
174
|
}
|
|
@@ -233,19 +177,9 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
233
177
|
return warnings.length ? warnings.join('\n') : null;
|
|
234
178
|
}
|
|
235
179
|
|
|
236
|
-
/**
|
|
237
|
-
* @param {object} payload - parsed PostToolUse stdin payload (Write|Edit|AskUserQuestion)
|
|
238
|
-
* @returns {Promise<object|null>} a hookSpecificOutput warning on a gate
|
|
239
|
-
* order/attribution violation, or null (pass/silent).
|
|
240
|
-
*/
|
|
241
180
|
export async function run(payload) {
|
|
242
181
|
try {
|
|
243
|
-
//
|
|
244
|
-
// gets read and written is decided per event — by the target path for
|
|
245
|
-
// Write/Edit, by the feature the question names for AskUserQuestion. The
|
|
246
|
-
// `stateExists` gate therefore moved into each handler, against the tree that
|
|
247
|
-
// handler resolved; gating on the session's tree up here would skip work that
|
|
248
|
-
// belongs to a worktree (and, worse, silently pass it to the wrong one).
|
|
182
|
+
// Only a starting point: each handler resolves the tree it reads and writes, so the stateExists gate lives there.
|
|
249
183
|
const sessionRoot = projectDir();
|
|
250
184
|
|
|
251
185
|
const input = payload || {};
|
|
@@ -258,12 +192,8 @@ export async function run(payload) {
|
|
|
258
192
|
} else if (toolName === 'AskUserQuestion') {
|
|
259
193
|
warning = handleAskUserQuestion(input, sessionRoot);
|
|
260
194
|
}
|
|
261
|
-
} catch {
|
|
262
|
-
// fail-open
|
|
263
|
-
}
|
|
195
|
+
} catch {}
|
|
264
196
|
|
|
265
|
-
// A non-blocking order-violation warning is surfaced as injected context
|
|
266
|
-
// (hookSpecificOutput.additionalContext); everything else stays silent.
|
|
267
197
|
return warning ? contextResult(warning, 'PostToolUse') : null;
|
|
268
198
|
} catch {
|
|
269
199
|
return null;
|