@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,63 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* This lives in `framework/hooks/shared/` rather than `src/` because BOTH sides
|
|
6
|
-
* need it and the dependency direction in this repo only runs one way: `src/`
|
|
7
|
-
* imports from `framework/hooks/shared/` (see `finish.js`'s `claim-registry`
|
|
8
|
-
* and `e2e-check` imports), never the reverse — a hook shipped into a target
|
|
9
|
-
* project has the package, not this repo's `src/` layout.
|
|
10
|
-
*
|
|
11
|
-
* Two callers, one question:
|
|
12
|
-
* - `teardownWorktree` (src/commands/project/worktree.js) — may this worktree
|
|
13
|
-
* be handed to `git worktree remove`?
|
|
14
|
-
* - the `block-destructive-worktree-removal` PreToolUse hook — is the command
|
|
15
|
-
* about to run going to traverse a link out of the worktree?
|
|
16
|
-
*
|
|
17
|
-
* WHY IT MUST BE EXHAUSTIVE — both `git worktree remove` and its `--force`
|
|
18
|
-
* fallback FOLLOW reparse points out of the worktree and delete the PRIMARY
|
|
19
|
-
* ROOT's real files. The plain removal is not the safe one: it refuses only
|
|
20
|
-
* when it sees modified/untracked *tracked-scope* content, so a GITIGNORED
|
|
21
|
-
* junction (`node_modules`, `.claude`) leaves `git status` empty, the removal
|
|
22
|
-
* proceeds, exits 0 — and the target is gone.
|
|
23
|
-
*
|
|
24
|
-
* So the check cannot live in the `catch` of the plain removal (unreachable on
|
|
25
|
-
* exactly the destructive path), and it cannot be a fixed list of the links
|
|
26
|
-
* `setup` creates: a nested link inside a real `node_modules` (npm link, npm
|
|
27
|
-
* workspaces) is invisible to any top-level inventory. We scan the whole tree
|
|
28
|
-
* and NEVER recurse INTO a reparse point — descending one would walk the target
|
|
29
|
-
* we are trying to protect (and could not terminate on a cycle).
|
|
30
|
-
*
|
|
31
|
-
* FAIL-CLOSED, deliberately: an empty result is NOT the same as a safe one. A
|
|
32
|
-
* directory we could not read or an entry we could not lstat might BE (or
|
|
33
|
-
* contain) a reparse point. Those answer `unprovable`, and every caller must
|
|
34
|
-
* treat that exactly like a found link — decline. "I saw nothing" and "I proved
|
|
35
|
-
* there is nothing" are different claims, and only the second may authorize a
|
|
36
|
-
* removal. This is the one module in the shared/ directory that must NOT be
|
|
37
|
-
* fail-open; the whole point is that uncertainty blocks.
|
|
38
|
-
*
|
|
39
|
-
* `lstat` reports a POSIX symlink and a Windows junction alike as a symbolic
|
|
40
|
-
* link, so the scan behaves identically on both platforms — as does the
|
|
41
|
-
* destruction it prevents.
|
|
42
|
-
*/
|
|
1
|
+
// `git worktree remove` (plain or --force) follows reparse points and deletes the
|
|
2
|
+
// primary root's files, nested links included. FAIL-CLOSED: an entry that cannot
|
|
3
|
+
// be read is `unprovable` and blocks exactly like a found link.
|
|
43
4
|
|
|
44
5
|
import { readdirSync, lstatSync } from 'fs';
|
|
45
6
|
import { join, relative } from 'path';
|
|
46
7
|
|
|
47
|
-
/**
|
|
48
|
-
* Every reparse point anywhere under `dir`, as relative paths.
|
|
49
|
-
*
|
|
50
|
-
* @param {string} dir
|
|
51
|
-
* @param {string} [base] - root of the scan, for relative reporting
|
|
52
|
-
* @param {{points: string[], unprovable: string[]}} [acc]
|
|
53
|
-
* @returns {{points: string[], unprovable: string[]}} both empty = provably clean
|
|
54
|
-
*/
|
|
8
|
+
/** @returns {{points: string[], unprovable: string[]}} both empty = provably clean */
|
|
55
9
|
export function findReparsePoints(dir, base = dir, acc = { points: [], unprovable: [] }) {
|
|
56
10
|
let entries;
|
|
57
11
|
try {
|
|
58
12
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
59
13
|
} catch {
|
|
60
|
-
// Unreadable (ACL, lock, EMFILE): we cannot prove what is in here.
|
|
61
14
|
acc.unprovable.push((relative(base, dir) || '.').replace(/\\/g, '/'));
|
|
62
15
|
return acc;
|
|
63
16
|
}
|
|
@@ -71,28 +24,15 @@ export function findReparsePoints(dir, base = dir, acc = { points: [], unprovabl
|
|
|
71
24
|
continue;
|
|
72
25
|
}
|
|
73
26
|
if (st.isSymbolicLink()) {
|
|
74
|
-
// Forward slashes: every other path this command reports is normalized,
|
|
75
|
-
// and these end up in JSON the caller may compare or display.
|
|
76
27
|
acc.points.push(relative(base, p).replace(/\\/g, '/'));
|
|
77
|
-
continue; //
|
|
28
|
+
continue; // descending would walk the target, not the worktree
|
|
78
29
|
}
|
|
79
30
|
if (st.isDirectory() && entry.name !== '.git') findReparsePoints(p, base, acc);
|
|
80
31
|
}
|
|
81
32
|
return acc;
|
|
82
33
|
}
|
|
83
34
|
|
|
84
|
-
|
|
85
|
-
* The single question both callers actually ask: may this tree be handed to a
|
|
86
|
-
* command that follows links?
|
|
87
|
-
*
|
|
88
|
-
* Collapses `points` and `unprovable` into one list precisely because callers
|
|
89
|
-
* must not weigh them differently — `teardownWorktree` already concatenated
|
|
90
|
-
* them, and a hook that treated `unprovable` as "probably fine" would reopen
|
|
91
|
-
* the hole this module exists to close.
|
|
92
|
-
*
|
|
93
|
-
* @param {string} dir
|
|
94
|
-
* @returns {{ safe: boolean, blockers: string[] }}
|
|
95
|
-
*/
|
|
35
|
+
// One list on purpose: callers must not weigh `unprovable` lighter than a link.
|
|
96
36
|
export function scanForBlockers(dir) {
|
|
97
37
|
const { points, unprovable } = findReparsePoints(dir);
|
|
98
38
|
const blockers = [...points, ...unprovable];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/** `PARCIAL` on the report's first non-blank line, tolerating heading/bold/quote typography. */
|
|
5
|
+
export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
|
|
6
|
+
|
|
7
|
+
export const SLICE_STATUSES = ['pendente', 'parcial', 'feito'];
|
|
8
|
+
|
|
9
|
+
export function checksPath(featureDir) {
|
|
10
|
+
return join(featureDir, '2-plan', 'checks.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sliceReportPath(featureDir, sliceId) {
|
|
14
|
+
return join(featureDir, '3-implement', 'reports', `${sliceId}.md`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sliceStatusFromReport(text) {
|
|
18
|
+
if (text == null) return 'pendente';
|
|
19
|
+
const first = String(text).split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
|
|
20
|
+
return PARTIAL_MARKER.test(first) ? 'parcial' : 'feito';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @returns {{ total, done, partial, pending, slices } | null} null when the checks text is unreadable */
|
|
24
|
+
export function sliceProgressFrom(checksText, readReport) {
|
|
25
|
+
let slices;
|
|
26
|
+
try {
|
|
27
|
+
slices = JSON.parse(checksText ?? '').slices;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (!Array.isArray(slices)) return null;
|
|
32
|
+
const out = slices
|
|
33
|
+
.filter((s) => s && typeof s.id === 'string')
|
|
34
|
+
.map((s) => ({
|
|
35
|
+
id: s.id,
|
|
36
|
+
title: s.title ?? '',
|
|
37
|
+
depends: Array.isArray(s.depends) ? s.depends : [],
|
|
38
|
+
covers: Array.isArray(s.covers) ? s.covers : [],
|
|
39
|
+
status: sliceStatusFromReport(readReport(s.id)),
|
|
40
|
+
}));
|
|
41
|
+
const count = (status) => out.filter((s) => s.status === status).length;
|
|
42
|
+
return { total: out.length, done: count('feito'), partial: count('parcial'), pending: count('pendente'), slices: out };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readOrNull(path) {
|
|
46
|
+
try {
|
|
47
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function sliceProgress(featureDir) {
|
|
54
|
+
return sliceProgressFrom(readOrNull(checksPath(featureDir)), (id) => readOrNull(sliceReportPath(featureDir, id)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readySlices(progress) {
|
|
58
|
+
if (!progress) return [];
|
|
59
|
+
const done = new Set(progress.slices.filter((s) => s.status === 'feito').map((s) => s.id));
|
|
60
|
+
return progress.slices.filter((s) => s.status !== 'feito' && s.depends.every((d) => done.has(d)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Loose on purpose: a false "complete" costs a read, a false "incomplete"
|
|
64
|
+
// re-dispatches finished work.
|
|
65
|
+
export const REPORT_MARKERS = [
|
|
66
|
+
{ id: 'files', re: /^\s*(?:[-*]\s*)?(?:\*\*|#+\s*)?arquivos\s*:?\**\s*:?/mi },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const MIN_PROSE_CHARS = 80;
|
|
70
|
+
|
|
71
|
+
const scenarioRe = (id) => new RegExp(`(?<![\\w-])${id}(?![\\w-])`);
|
|
72
|
+
|
|
73
|
+
/** Every scenario in `covers` must be named in the report. */
|
|
74
|
+
export function inspectReport(path, { covers = [] } = {}) {
|
|
75
|
+
const missingAll = ['what', 'files', ...covers];
|
|
76
|
+
if (!existsSync(path)) return { exists: false, complete: false, missing: missingAll };
|
|
77
|
+
let body = '';
|
|
78
|
+
try {
|
|
79
|
+
body = readFileSync(path, 'utf8');
|
|
80
|
+
} catch {
|
|
81
|
+
return { exists: true, complete: false, missing: missingAll, unreadable: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const missing = REPORT_MARKERS.filter((m) => !m.re.test(body)).map((m) => m.id);
|
|
85
|
+
missing.push(...covers.filter((id) => !scenarioRe(id).test(body)));
|
|
86
|
+
const prose = body
|
|
87
|
+
.split('\n')
|
|
88
|
+
.filter((l) => !/^\s*(#|[-*]\s|\*\*)/.test(l))
|
|
89
|
+
.join(' ')
|
|
90
|
+
.replace(/\s+/g, ' ')
|
|
91
|
+
.trim();
|
|
92
|
+
if (prose.length < MIN_PROSE_CHARS) missing.unshift('what');
|
|
93
|
+
|
|
94
|
+
const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
|
|
95
|
+
const partial = PARTIAL_MARKER.test(firstLine);
|
|
96
|
+
if (partial) missing.push('partial');
|
|
97
|
+
|
|
98
|
+
return { exists: true, complete: missing.length === 0, missing, partial };
|
|
99
|
+
}
|
|
@@ -1,34 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* `reuse` and `code-budget` validators and the `comment-guard` hook, so the
|
|
4
|
-
* three never disagree about the same file.
|
|
5
|
-
*
|
|
6
|
-
* @module source-files
|
|
7
|
-
*/
|
|
1
|
+
// One reading of "hand-written project source" for the `reuse` and
|
|
2
|
+
// `code-budget` validators and the `comment-guard` hook.
|
|
8
3
|
|
|
9
4
|
const SCANNABLE = /\.(cs|ts|tsx|js|jsx|mjs|cjs)$/i;
|
|
10
5
|
|
|
11
|
-
// `.morph/`
|
|
12
|
-
// graph as a second `RoulettePriority`.
|
|
6
|
+
// `.morph/` holds archived contracts that would duplicate real types.
|
|
13
7
|
const NOT_SOURCE = /(^|\/)(?:\.morph|node_modules|dist|build|out|\.next|bin|obj|coverage)\//;
|
|
14
8
|
|
|
15
9
|
const DECLARATION_FILE = /\.d\.ts$/i;
|
|
16
10
|
|
|
17
11
|
const GENERATED = /(?:\.Designer|\.g|\.g\.i)\.cs$|ModelSnapshot\.cs$/i;
|
|
18
12
|
|
|
19
|
-
/**
|
|
20
|
-
* @param {string} sourceFile
|
|
21
|
-
* @returns {boolean} project code in a scanned extension, tests included
|
|
22
|
-
*/
|
|
13
|
+
/** Tests included. */
|
|
23
14
|
export function isProjectSource(sourceFile) {
|
|
24
15
|
const f = String(sourceFile || '').replace(/\\/g, '/');
|
|
25
16
|
return SCANNABLE.test(f) && !DECLARATION_FILE.test(f) && !NOT_SOURCE.test(f);
|
|
26
17
|
}
|
|
27
18
|
|
|
28
|
-
/**
|
|
29
|
-
* @param {string} sourceFile
|
|
30
|
-
* @returns {boolean} generated C# nobody writes by hand
|
|
31
|
-
*/
|
|
32
19
|
export function isGenerated(sourceFile) {
|
|
33
20
|
return GENERATED.test(String(sourceFile || ''));
|
|
34
21
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const NEEDS_CMD_SHIM = /^(npm|npx|pnpm|yarn)$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* On win32 `npx`/`npm`/… are `.cmd` shims: ENOENT without a shell, EINVAL as `.cmd`
|
|
8
|
+
* with `shell: false`. Playwright runs from its own JS entry, so it needs no shell.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveSpawn(file, args = [], {
|
|
11
|
+
cwd = process.cwd(),
|
|
12
|
+
platform = process.platform,
|
|
13
|
+
exists = existsSync,
|
|
14
|
+
execPath = process.execPath,
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (platform !== 'win32' || !NEEDS_CMD_SHIM.test(file)) return { file, args, shell: false };
|
|
17
|
+
|
|
18
|
+
if (file === 'npx' && args[0] === 'playwright') {
|
|
19
|
+
const cli = join(cwd, 'node_modules', '@playwright', 'test', 'cli.js');
|
|
20
|
+
if (exists(cli)) return { file: execPath, args: [cli, ...args.slice(1)], shell: false };
|
|
21
|
+
}
|
|
22
|
+
return { file: `${file}.cmd`, args, shell: true };
|
|
23
|
+
}
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared read-only state accessor for Claude Code hooks.
|
|
3
|
-
*
|
|
4
|
-
* Wraps state.json reads with fail-open semantics:
|
|
5
|
-
* - Returns null instead of throwing on missing/corrupt state
|
|
6
|
-
* - Never writes to state.json (read-only)
|
|
7
|
-
* - Uses synchronous reads for <100ms performance
|
|
8
|
-
*/
|
|
1
|
+
// Fail-open: missing or corrupt state reads as null, never throws.
|
|
9
2
|
|
|
10
3
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
11
4
|
import { join } from 'path';
|
|
@@ -13,80 +6,15 @@ import { join } from 'path';
|
|
|
13
6
|
const STATE_FILE = '.morph/state.json';
|
|
14
7
|
const FEATURES_DIR = '.morph/features';
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
* `loadState()` does a `readdirSync` of `.morph/features` plus a
|
|
20
|
-
* `readFileSync` + `JSON.parse` of EVERY `feature.json` under it, on every
|
|
21
|
-
* call. A single hook invocation calls it 1-3x (directly, or via
|
|
22
|
-
* `getActiveFeature`/`derivePhaseForFeature`/`isGateApproved`/etc.) — re-doing
|
|
23
|
-
* that full hydration each time is pure waste when nothing on disk changed
|
|
24
|
-
* between those calls.
|
|
25
|
-
*
|
|
26
|
-
* SAFE because every hook process is short-lived and single-invocation: one
|
|
27
|
-
* `node state-reader.js`-importing process handles exactly one tool call (or
|
|
28
|
-
* one CLI command) and exits. There is no risk of the cache going stale
|
|
29
|
-
* ACROSS invocations, because each invocation starts with an empty Map.
|
|
30
|
-
* Within a SINGLE invocation, staleness would only matter if that same
|
|
31
|
-
* process both reads and then writes a feature's `feature.json` and
|
|
32
|
-
* expects a LATER read to see the write — verified (grep across every
|
|
33
|
-
* consumer, 2026-08-11) that the one hook that writes feature.json
|
|
34
|
-
* (`state-sync.js`) never re-reads through `loadState()` after writing in the
|
|
35
|
-
* same invocation; its `AskUserQuestion` handler reads via
|
|
36
|
-
* `readFeatureFile()` (a different, uncached module) as the PRIMARY path and
|
|
37
|
-
* only falls back to `loadState()` for a feature that has NEVER had a
|
|
38
|
-
* feature.json written — which, by construction, can't have just been
|
|
39
|
-
* written to in this same invocation either.
|
|
40
|
-
*
|
|
41
|
-
* A directory-mtime-based invalidation was considered instead (so a
|
|
42
|
-
* LONG-LIVED consumer could reuse this module safely too) but rejected on two
|
|
43
|
-
* grounds: (1) verified — grep for every importer of this file — that none is
|
|
44
|
-
* long-lived (they're all hooks, one process per tool call, or CLI commands,
|
|
45
|
-
* one process per invocation; the dashboard is a separate Vite/React app and
|
|
46
|
-
* the statusline is Python, neither imports this module); and (2) even if one
|
|
47
|
-
* existed, `.morph/features`'s own mtime only changes when feature
|
|
48
|
-
* DIRECTORIES are added/removed, not when an EXISTING feature.json's content
|
|
49
|
-
* is overwritten in place — so mtime-of-the-dir can't actually detect the
|
|
50
|
-
* mutation that matters here, and getting that right would mean stat'ing
|
|
51
|
-
* every feature.json individually, which re-adds the very cost this cache
|
|
52
|
-
* exists to remove. If a long-lived consumer is ever added, it should
|
|
53
|
-
* re-derive state per request rather than lean on this cache.
|
|
54
|
-
*
|
|
55
|
-
* CAVEAT for any future consumer: the object returned by `loadState()` is the
|
|
56
|
-
* SAME object on every cache hit, not a fresh copy — a caller that mutates it
|
|
57
|
-
* (as `state-sync.js`'s `handleWriteEdit` does, in-place, before persisting)
|
|
58
|
-
* is mutating the cached entry too. That's harmless today because nothing
|
|
59
|
-
* currently reads the cache again after such a mutation within one process,
|
|
60
|
-
* but treat the return value as read-only unless you've re-verified that.
|
|
61
|
-
*
|
|
62
|
-
* `clearStateCache()` is exported for tests that write/rewrite feature.json
|
|
63
|
-
* and then call a `loadState()`-backed function again WITHIN THE SAME
|
|
64
|
-
* process and need to see the fresh content — call it at the point of
|
|
65
|
-
* mutation.
|
|
66
|
-
*/
|
|
9
|
+
// Memoized per process: each hook/CLI process handles one call. The returned
|
|
10
|
+
// object is shared, so treat it as read-only; writers call `clearStateCache()`.
|
|
67
11
|
const stateCache = new Map();
|
|
68
12
|
|
|
69
|
-
/** Drop all cached state — see `stateCache` above for when this is needed. */
|
|
70
13
|
export function clearStateCache() {
|
|
71
14
|
stateCache.clear();
|
|
72
15
|
}
|
|
73
16
|
|
|
74
|
-
/**
|
|
75
|
-
* Load state from the project root, hydrating each feature from its
|
|
76
|
-
* authoritative per-feature `feature.json`.
|
|
77
|
-
*
|
|
78
|
-
* The root `.morph/state.json` is a thin, gitignored index (status + updatedAt
|
|
79
|
-
* per feature); the heavy feature data lives in `.morph/features/{name}/feature.json`
|
|
80
|
-
* (committed). This reader merges them: it reads the thin index for project /
|
|
81
|
-
* metadata and overlays each feature.json so callers see the full feature object.
|
|
82
|
-
* A single corrupt feature.json is skipped, not fatal.
|
|
83
|
-
*
|
|
84
|
-
* Fail-open: returns null on any unexpected error or when there is no state at
|
|
85
|
-
* all (no index and no feature files). Memoized per `projectPath` for the
|
|
86
|
-
* lifetime of this process — see `stateCache` above.
|
|
87
|
-
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
88
|
-
* @returns {Object|null} State object or null
|
|
89
|
-
*/
|
|
17
|
+
/** The thin `state.json` index overlaid with each authoritative `feature.json`; null when neither exists. */
|
|
90
18
|
export function loadState(projectPath) {
|
|
91
19
|
const base = projectPath || process.cwd();
|
|
92
20
|
if (stateCache.has(base)) return stateCache.get(base);
|
|
@@ -119,7 +47,7 @@ export function loadState(projectPath) {
|
|
|
119
47
|
state.features = state.features || {};
|
|
120
48
|
state.features[name] = fj; // feature.json is authoritative
|
|
121
49
|
} catch {
|
|
122
|
-
//
|
|
50
|
+
// one corrupt feature.json must not blind every hook
|
|
123
51
|
}
|
|
124
52
|
}
|
|
125
53
|
}
|
|
@@ -134,17 +62,8 @@ export function loadState(projectPath) {
|
|
|
134
62
|
return state;
|
|
135
63
|
}
|
|
136
64
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
* exists, or `.morph/features/` (committed, gitignore-exempt) already has at
|
|
140
|
-
* least one feature with a `feature.json`. The latter case covers a freshly
|
|
141
|
-
* checked-out worktree where state.json (gitignored) hasn't been materialized
|
|
142
|
-
* yet, but `loadState()` can already self-heal from feature.json — so hooks
|
|
143
|
-
* gating on `stateExists()` should be "on" there too, not dark.
|
|
144
|
-
* Fail-open: any unexpected error (e.g. permission denied) returns false.
|
|
145
|
-
* @param {string} [projectPath]
|
|
146
|
-
* @returns {boolean}
|
|
147
|
-
*/
|
|
65
|
+
// A fresh worktree has `feature.json` but no gitignored `state.json` yet; hooks
|
|
66
|
+
// must stay on there.
|
|
148
67
|
export function stateExists(projectPath) {
|
|
149
68
|
try {
|
|
150
69
|
const base = projectPath || process.cwd();
|
|
@@ -163,11 +82,6 @@ export function stateExists(projectPath) {
|
|
|
163
82
|
}
|
|
164
83
|
}
|
|
165
84
|
|
|
166
|
-
/**
|
|
167
|
-
* Get the active feature (most recently updated in_progress feature).
|
|
168
|
-
* @param {string} [projectPath]
|
|
169
|
-
* @returns {{ name: string, feature: Object }|null}
|
|
170
|
-
*/
|
|
171
85
|
export function getActiveFeature(projectPath) {
|
|
172
86
|
const state = loadState(projectPath);
|
|
173
87
|
if (!state?.features) return null;
|
|
@@ -188,12 +102,6 @@ export function getActiveFeature(projectPath) {
|
|
|
188
102
|
return active;
|
|
189
103
|
}
|
|
190
104
|
|
|
191
|
-
/**
|
|
192
|
-
* Get the most recently updated feature (any status).
|
|
193
|
-
* Useful as a fallback when getActiveFeature returns null (all features done).
|
|
194
|
-
* @param {string} [projectPath]
|
|
195
|
-
* @returns {{ name: string, feature: Object }|null}
|
|
196
|
-
*/
|
|
197
105
|
export function getMostRecentFeature(projectPath) {
|
|
198
106
|
const state = loadState(projectPath);
|
|
199
107
|
if (!state?.features) return null;
|
|
@@ -212,155 +120,94 @@ export function getMostRecentFeature(projectPath) {
|
|
|
212
120
|
return result;
|
|
213
121
|
}
|
|
214
122
|
|
|
215
|
-
/**
|
|
216
|
-
* Get a specific feature by name.
|
|
217
|
-
* @param {string} featureName
|
|
218
|
-
* @param {string} [projectPath]
|
|
219
|
-
* @returns {Object|null}
|
|
220
|
-
*/
|
|
221
123
|
export function getFeature(featureName, projectPath) {
|
|
222
124
|
const state = loadState(projectPath);
|
|
223
125
|
return state?.features?.[featureName] || null;
|
|
224
126
|
}
|
|
225
127
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
* CANONICAL ARTIFACT exists. An empty folder is not enough — this avoids the
|
|
231
|
-
* chicken-and-egg problem where `mkdir 2-plan/` (or any pre-created sibling)
|
|
232
|
-
* advances the phase before plan content is written, then
|
|
233
|
-
* `enforce-phase-writes` blocks writes back to `0-proposal/`.
|
|
234
|
-
*
|
|
235
|
-
* Canonical artifacts (one per phase) match MORPH.md §3:
|
|
236
|
-
* - 4-review → review-report.md
|
|
237
|
-
* - 3-implement → recap.md
|
|
238
|
-
* - 2-plan → spec.md (the first plan-phase output produced)
|
|
239
|
-
* - 1-design → design-system.md
|
|
240
|
-
* - 0-proposal → proposal.md
|
|
241
|
-
*
|
|
242
|
-
* If no canonical file is found, returns `'proposal'` (the starting phase).
|
|
243
|
-
*
|
|
244
|
-
* @param {string} featureName
|
|
245
|
-
* @param {string} [projectPath]
|
|
246
|
-
* @returns {string} Phase name
|
|
247
|
-
*/
|
|
248
|
-
export const PHASE_INDEX = { proposal: 0, uiux: 1, plan: 2, implement: 3, review: 4 };
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* The canonical artifact of each phase, descending. Exported so a caller that
|
|
252
|
-
* holds the file list from somewhere OTHER than the local filesystem (a git
|
|
253
|
-
* tree, say) can answer the same question without re-encoding the map.
|
|
254
|
-
*/
|
|
128
|
+
export const PHASE_INDEX = { plan: 0, uiux: 1, implement: 2, review: 3 };
|
|
129
|
+
|
|
130
|
+
// Descending. An empty folder is not an artifact: a pre-created folder must
|
|
131
|
+
// not advance the phase.
|
|
255
132
|
export const PHASE_ARTIFACTS = [
|
|
256
|
-
['4-review', 'review', '
|
|
257
|
-
['3-implement', 'implement', '
|
|
258
|
-
['2-plan', 'plan', 'spec.md'],
|
|
133
|
+
['4-review', 'review', 'verifier.md'],
|
|
134
|
+
['3-implement', 'implement', 'reports/'],
|
|
259
135
|
['1-design', 'uiux', 'design-system.md'],
|
|
260
|
-
['
|
|
136
|
+
['2-plan', 'plan', 'plan.md'],
|
|
261
137
|
];
|
|
262
138
|
|
|
139
|
+
const DESIGN_DIR = '1-design';
|
|
140
|
+
|
|
263
141
|
const gatesOf = (feature) => feature?.approvals || feature?.approvalGates;
|
|
264
142
|
|
|
265
|
-
|
|
266
|
-
* Gate-implied phase from an ALREADY-LOADED feature object — no disk access.
|
|
267
|
-
*
|
|
268
|
-
* Split out of `gateImpliedPhase` so callers holding a feature.json that did
|
|
269
|
-
* not come from the working tree (e.g. `git show morph/{f}:…`) can reuse the
|
|
270
|
-
* rule instead of copying it. The path-based wrapper below is unchanged.
|
|
271
|
-
*
|
|
272
|
-
* @param {Object|null} feature
|
|
273
|
-
* @returns {string} phase name
|
|
274
|
-
*/
|
|
275
|
-
export function gateImpliedPhaseFrom(feature) {
|
|
143
|
+
export function gateImpliedPhaseFrom(feature, designStarted = false) {
|
|
276
144
|
const gates = gatesOf(feature);
|
|
277
145
|
const ok = (g) => gates?.[g]?.approved === true;
|
|
278
146
|
if (ok('review')) return 'review';
|
|
279
|
-
if (ok('plan')) return 'implement';
|
|
280
|
-
|
|
281
|
-
|
|
147
|
+
if (ok('plan')) return designStarted && !ok('uiux') ? 'uiux' : 'implement';
|
|
148
|
+
return 'plan';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A path ending in `/` asks for any `.md` file directly under it. */
|
|
152
|
+
export function diskArtifactProbe(featureDir) {
|
|
153
|
+
return (rel) => {
|
|
154
|
+
const abs = join(featureDir, ...rel.split('/').filter(Boolean));
|
|
155
|
+
if (!rel.endsWith('/')) return existsSync(abs);
|
|
156
|
+
try {
|
|
157
|
+
return readdirSync(abs, { withFileTypes: true }).some((e) => e.isFile() && e.name.endsWith('.md'));
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
282
162
|
}
|
|
283
163
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
164
|
+
export function fileSetArtifactProbe(files) {
|
|
165
|
+
return (rel) => {
|
|
166
|
+
if (!rel.endsWith('/')) return files.has(rel);
|
|
167
|
+
for (const f of files) {
|
|
168
|
+
if (f.startsWith(rel) && f.endsWith('.md') && !f.slice(rel.length).includes('/')) return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @param {(relPath: string) => boolean} hasArtifact - {@link diskArtifactProbe} or {@link fileSetArtifactProbe} */
|
|
293
175
|
export function derivePhaseFrom(feature, hasArtifact) {
|
|
294
|
-
let artifactPhase = '
|
|
176
|
+
let artifactPhase = 'plan';
|
|
295
177
|
for (const [folder, phase, canonical] of PHASE_ARTIFACTS) {
|
|
296
178
|
if (hasArtifact(`${folder}/${canonical}`)) { artifactPhase = phase; break; }
|
|
297
179
|
}
|
|
298
180
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const implied = gateImpliedPhaseFrom(feature);
|
|
181
|
+
// An approved gate advances the phase before the next artifact exists, or
|
|
182
|
+
// that artifact could never be written.
|
|
183
|
+
const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/design-system.md`);
|
|
184
|
+
const implied = gateImpliedPhaseFrom(feature, designStarted);
|
|
304
185
|
return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
|
|
305
186
|
}
|
|
306
187
|
|
|
307
|
-
/**
|
|
308
|
-
* The phase implied purely by approved gates, independent of which artifacts
|
|
309
|
-
* exist on disk. An approved gate authorizes the NEXT phase, so:
|
|
310
|
-
* review approved → review (terminal)
|
|
311
|
-
* plan approved → implement
|
|
312
|
-
* uiux approved → plan
|
|
313
|
-
* otherwise → proposal (proposal-approval's fork between uiux/plan is
|
|
314
|
-
* resolved by enforce-phase-writes, not here)
|
|
315
|
-
* @returns {string} phase name
|
|
316
|
-
*/
|
|
317
188
|
export function gateImpliedPhase(featureName, projectPath) {
|
|
318
|
-
|
|
189
|
+
const designDir = join(projectPath || process.cwd(), '.morph/features', featureName, DESIGN_DIR);
|
|
190
|
+
return gateImpliedPhaseFrom(getFeature(featureName, projectPath), existsSync(designDir));
|
|
319
191
|
}
|
|
320
192
|
|
|
321
193
|
export function derivePhaseForFeature(featureName, projectPath) {
|
|
322
194
|
const basePath = join(projectPath || process.cwd(), '.morph/features', featureName);
|
|
323
|
-
return derivePhaseFrom(
|
|
324
|
-
getFeature(featureName, projectPath),
|
|
325
|
-
(rel) => existsSync(join(basePath, rel)),
|
|
326
|
-
);
|
|
195
|
+
return derivePhaseFrom(getFeature(featureName, projectPath), diskArtifactProbe(basePath));
|
|
327
196
|
}
|
|
328
197
|
|
|
329
|
-
/**
|
|
330
|
-
* Get the current phase of a feature.
|
|
331
|
-
* Falls back to filesystem derivation when phase is not in state.
|
|
332
|
-
* @param {string} featureName
|
|
333
|
-
* @param {string} [projectPath]
|
|
334
|
-
* @returns {string|null}
|
|
335
|
-
*/
|
|
336
198
|
export function getFeaturePhase(featureName, projectPath) {
|
|
337
199
|
const feature = getFeature(featureName, projectPath);
|
|
338
200
|
if (!feature) return null;
|
|
339
201
|
return feature.phase || derivePhaseForFeature(featureName, projectPath);
|
|
340
202
|
}
|
|
341
203
|
|
|
342
|
-
/**
|
|
343
|
-
* Get approval status for a gate.
|
|
344
|
-
* @param {string} featureName
|
|
345
|
-
* @param {string} gate - 'proposal'|'uiux'|'plan'|'review'
|
|
346
|
-
* @param {string} [projectPath]
|
|
347
|
-
* @returns {boolean}
|
|
348
|
-
*/
|
|
349
204
|
export function isGateApproved(featureName, gate, projectPath) {
|
|
350
205
|
const feature = getFeature(featureName, projectPath);
|
|
351
|
-
// Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
|
|
352
206
|
const gates = feature?.approvals || feature?.approvalGates;
|
|
353
207
|
return gates?.[gate]?.approved === true;
|
|
354
208
|
}
|
|
355
209
|
|
|
356
|
-
/**
|
|
357
|
-
* Get all pending approval gates for a feature.
|
|
358
|
-
* @param {string} featureName
|
|
359
|
-
* @param {string} [projectPath]
|
|
360
|
-
* @returns {string[]} List of gate names that are not yet approved
|
|
361
|
-
*/
|
|
362
210
|
export function pendingGatesFrom(feature) {
|
|
363
|
-
// Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
|
|
364
211
|
const gates = gatesOf(feature);
|
|
365
212
|
if (!gates) return [];
|
|
366
213
|
|
|
@@ -373,54 +220,11 @@ export function getPendingGates(featureName, projectPath) {
|
|
|
373
220
|
return pendingGatesFrom(getFeature(featureName, projectPath));
|
|
374
221
|
}
|
|
375
222
|
|
|
376
|
-
/**
|
|
377
|
-
* Get feature outputs status.
|
|
378
|
-
* @param {string} featureName
|
|
379
|
-
* @param {string} [projectPath]
|
|
380
|
-
* @returns {Object|null} outputs map or null
|
|
381
|
-
*/
|
|
382
223
|
export function getOutputs(featureName, projectPath) {
|
|
383
224
|
const feature = getFeature(featureName, projectPath);
|
|
384
225
|
return feature?.outputs || null;
|
|
385
226
|
}
|
|
386
227
|
|
|
387
|
-
/**
|
|
388
|
-
* Get uncompleted required outputs for current phase.
|
|
389
|
-
* @param {string} featureName
|
|
390
|
-
* @param {string} [projectPath]
|
|
391
|
-
* @returns {Array<{type: string, path: string}>}
|
|
392
|
-
*/
|
|
393
|
-
/**
|
|
394
|
-
* Get task statistics directly from taskList (preferred over feature.tasks counters).
|
|
395
|
-
* Excludes expanded tasks from counts, matching calculateProgress() behavior.
|
|
396
|
-
*
|
|
397
|
-
* @param {Object} feature - Feature state object
|
|
398
|
-
* @returns {{ total: number, completed: number, inProgress: number, pending: number }}
|
|
399
|
-
*/
|
|
400
|
-
export function getTaskStats(feature) {
|
|
401
|
-
const list = feature?.taskList || [];
|
|
402
|
-
if (list.length > 0) {
|
|
403
|
-
const active = list.filter(t => t.status !== 'expanded');
|
|
404
|
-
return {
|
|
405
|
-
total: active.length,
|
|
406
|
-
completed: active.filter(t => t.status === 'completed').length,
|
|
407
|
-
inProgress: active.filter(t => t.status === 'in_progress').length,
|
|
408
|
-
pending: active.filter(t => t.status === 'pending').length,
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
// Fallback: read from legacy counter object
|
|
412
|
-
const tasks = feature?.tasks;
|
|
413
|
-
if (tasks && typeof tasks === 'object' && !Array.isArray(tasks)) {
|
|
414
|
-
return {
|
|
415
|
-
total: tasks.total || 0,
|
|
416
|
-
completed: tasks.completed || 0,
|
|
417
|
-
inProgress: tasks.inProgress || 0,
|
|
418
|
-
pending: tasks.pending || 0,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
return { total: 0, completed: 0, inProgress: 0, pending: 0 };
|
|
422
|
-
}
|
|
423
|
-
|
|
424
228
|
export function getMissingOutputs(featureName, projectPath) {
|
|
425
229
|
const feature = getFeature(featureName, projectPath);
|
|
426
230
|
if (!feature?.outputs) return [];
|