@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,56 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MORPH-SPEC State Manager Library
|
|
3
|
-
*
|
|
4
|
-
* Manages state.json for tracking features, progress, agents, and checkpoints.
|
|
5
|
-
* Used both by CLI commands and internal automation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, rmdirSync, statSync, readdirSync } from 'fs';
|
|
9
2
|
import { join, dirname } from 'path';
|
|
10
3
|
import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
|
|
4
|
+
import { derivePhaseFrom, diskArtifactProbe } from '../../../framework/hooks/shared/state-reader.js';
|
|
11
5
|
|
|
12
6
|
const STATE_FILE_NAME = '.morph/state.json';
|
|
13
7
|
const FEATURES_DIR_NAME = '.morph/features';
|
|
14
8
|
const FEATURE_FILE_NAME = 'feature.json';
|
|
15
|
-
/**
|
|
16
|
-
* The one spelling of the committed project config's location, relative to a
|
|
17
|
-
* project root. Exported because three modules used to write it out by hand
|
|
18
|
-
* (`readProjectFromConfig` here, `readMorphConfig` and `morphConfigPath` in
|
|
19
|
-
* src/lib/tasks/) — and `morphConfigPath`'s own JSDoc promised the reader that
|
|
20
|
-
* the writer and the reader "can never disagree about which file is meant",
|
|
21
|
-
* a guarantee three copies of a string cannot give. Now they can't.
|
|
22
|
-
*/
|
|
23
9
|
export const CONFIG_FILE_NAME = '.morph/config/config.json';
|
|
24
10
|
|
|
25
|
-
//
|
|
26
|
-
// is
|
|
27
|
-
// `.morph/features/{name}/feature.json` (authoritative, committed) with the root
|
|
28
|
-
// `.morph/state.json` acting as a thin, derivable, gitignored index. Migration to
|
|
29
|
-
// the split layout is detected by `feature.json` presence, not by this number.
|
|
11
|
+
// Versions the feature-object schema, not the storage layout: the per-feature
|
|
12
|
+
// split is detected by the presence of `feature.json`.
|
|
30
13
|
const STATE_VERSION = '5.0.0';
|
|
31
14
|
|
|
32
|
-
// ============================================================================
|
|
33
|
-
// Core Functions
|
|
34
|
-
// ============================================================================
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get the state file path (looks in current working directory)
|
|
38
|
-
*/
|
|
39
15
|
export function getStatePath() {
|
|
40
16
|
return join(process.cwd(), STATE_FILE_NAME);
|
|
41
17
|
}
|
|
42
18
|
|
|
43
|
-
/** Absolute path to the per-feature folder root (.morph/features). */
|
|
44
19
|
function getFeaturesDir() {
|
|
45
20
|
return join(process.cwd(), FEATURES_DIR_NAME);
|
|
46
21
|
}
|
|
47
22
|
|
|
48
|
-
/** Absolute path to a feature's authoritative feature.json. */
|
|
49
23
|
function getFeatureJsonPath(featureName) {
|
|
50
24
|
return join(getFeaturesDir(), featureName, FEATURE_FILE_NAME);
|
|
51
25
|
}
|
|
52
26
|
|
|
53
|
-
/** List feature names that have a folder on disk under .morph/features. */
|
|
54
27
|
function listFeatureNamesOnDisk() {
|
|
55
28
|
const dir = getFeaturesDir();
|
|
56
29
|
if (!existsSync(dir)) return [];
|
|
@@ -63,7 +36,6 @@ function listFeatureNamesOnDisk() {
|
|
|
63
36
|
}
|
|
64
37
|
}
|
|
65
38
|
|
|
66
|
-
/** Read a feature's feature.json, or null if missing/corrupt. */
|
|
67
39
|
function readFeatureJson(featureName) {
|
|
68
40
|
const path = getFeatureJsonPath(featureName);
|
|
69
41
|
if (!existsSync(path)) return null;
|
|
@@ -74,7 +46,6 @@ function readFeatureJson(featureName) {
|
|
|
74
46
|
}
|
|
75
47
|
}
|
|
76
48
|
|
|
77
|
-
/** Read project identity from the committed .morph/config/config.json, if present. */
|
|
78
49
|
function readProjectFromConfig() {
|
|
79
50
|
const path = join(process.cwd(), CONFIG_FILE_NAME);
|
|
80
51
|
if (!existsSync(path)) return null;
|
|
@@ -91,10 +62,7 @@ function readProjectFromConfig() {
|
|
|
91
62
|
}
|
|
92
63
|
}
|
|
93
64
|
|
|
94
|
-
|
|
95
|
-
* Atomic JSON write: per-process temp file then rename, with an EPERM retry loop
|
|
96
|
-
* for the brief Windows lock race. Creates the parent directory if needed.
|
|
97
|
-
*/
|
|
65
|
+
// Per-process temp file then rename; the EPERM retry covers the brief Windows lock race.
|
|
98
66
|
export function atomicWriteJson(targetPath, data) {
|
|
99
67
|
const dir = dirname(targetPath);
|
|
100
68
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
@@ -111,41 +79,25 @@ export function atomicWriteJson(targetPath, data) {
|
|
|
111
79
|
if (err.code === 'EPERM' && attempt < MAX_RETRIES) {
|
|
112
80
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (attempt + 1));
|
|
113
81
|
} else {
|
|
114
|
-
try { unlinkSync(tmpPath); } catch {
|
|
82
|
+
try { unlinkSync(tmpPath); } catch { }
|
|
115
83
|
throw err;
|
|
116
84
|
}
|
|
117
85
|
}
|
|
118
86
|
}
|
|
119
87
|
}
|
|
120
88
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
*
|
|
124
|
-
* Skips the write entirely when the on-disk content is already byte-identical to
|
|
125
|
-
* what we'd write. `saveState` hydrates and re-persists EVERY feature, so a CLI
|
|
126
|
-
* mutation on feature A would otherwise rewrite sibling B's file from A's loaded
|
|
127
|
-
* copy. Skipping unchanged siblings means A never touches B's authoritative file
|
|
128
|
-
* in the common case — shrinking the cross-feature clobber window to nothing
|
|
129
|
-
* unless B genuinely changed under A.
|
|
130
|
-
*
|
|
131
|
-
* A formatting mismatch is NOT free, though: the guard compares bytes, so a
|
|
132
|
-
* sibling committed with a trailing newline fails it and gets rewritten. The
|
|
133
|
-
* content is equivalent, but git sees ` M` on a file this caller never meant to
|
|
134
|
-
* touch — enough to fail a whole-tree clean check (see the finish teardown's clearRootWorktreeRegistration).
|
|
135
|
-
* And the guard only skips IDENTICAL content: a concurrent instance's CHANGED
|
|
136
|
-
* write to a sibling would still be clobbered by the sweep.
|
|
137
|
-
*/
|
|
89
|
+
// saveState re-persists every feature; skipping byte-identical content keeps a
|
|
90
|
+
// mutation on one feature from rewriting (and clobbering) its siblings' files.
|
|
138
91
|
function writeFeatureJson(featureName, feature) {
|
|
139
92
|
const path = getFeatureJsonPath(featureName);
|
|
140
93
|
try {
|
|
141
94
|
if (existsSync(path) && readFileSync(path, 'utf8') === JSON.stringify(feature, null, 2)) {
|
|
142
|
-
return;
|
|
95
|
+
return;
|
|
143
96
|
}
|
|
144
|
-
} catch {
|
|
97
|
+
} catch { }
|
|
145
98
|
atomicWriteJson(path, feature);
|
|
146
99
|
}
|
|
147
100
|
|
|
148
|
-
/** A fresh, empty state scaffold (used when no root index exists yet). */
|
|
149
101
|
function scaffoldState() {
|
|
150
102
|
return {
|
|
151
103
|
version: STATE_VERSION,
|
|
@@ -155,67 +107,16 @@ function scaffoldState() {
|
|
|
155
107
|
metadata: { totalFeatures: 0, completedFeatures: 0, totalTimeSpent: 0 },
|
|
156
108
|
};
|
|
157
109
|
}
|
|
158
|
-
|
|
159
|
-
* Derive current phase from filesystem.
|
|
160
|
-
*
|
|
161
|
-
* Walks the phase map in descending order and returns the first phase whose
|
|
162
|
-
* CANONICAL ARTIFACT exists. An empty folder is not enough — `mkdir 2-plan/`
|
|
163
|
-
* does not count as "in plan phase" until `spec.md` (the first canonical
|
|
164
|
-
* plan-phase output) lands. If no canonical artifact is found anywhere,
|
|
165
|
-
* returns `'proposal'` (the starting phase).
|
|
166
|
-
*
|
|
167
|
-
* This avoids the chicken-and-egg problem where the agent pre-creates all
|
|
168
|
-
* phase folders, the derived phase jumps to the highest one, and
|
|
169
|
-
* `enforce-phase-writes` then blocks the agent from writing back to the
|
|
170
|
-
* prior phase folder.
|
|
171
|
-
*
|
|
172
|
-
* @param {string} featurePath - Absolute path to .morph/features/{feature}/
|
|
173
|
-
* @returns {string} Phase name: 'review' | 'implement' | 'plan' | 'uiux' | 'proposal'
|
|
174
|
-
*/
|
|
175
|
-
const PHASE_INDEX = { proposal: 0, uiux: 1, plan: 2, implement: 3, review: 4 };
|
|
176
|
-
|
|
177
|
-
/** Phase implied purely by approved gates (mirrors hook-side gateImpliedPhase). */
|
|
178
|
-
function gateImpliedPhaseFromGates(gates) {
|
|
179
|
-
const ok = (g) => gates?.[g]?.approved === true;
|
|
180
|
-
if (ok('review')) return 'review';
|
|
181
|
-
if (ok('plan')) return 'implement';
|
|
182
|
-
if (ok('uiux')) return 'plan';
|
|
183
|
-
return 'proposal';
|
|
184
|
-
}
|
|
185
|
-
|
|
110
|
+
// Delegates to the hook-side rule so the CLI and the hooks never disagree on a phase.
|
|
186
111
|
export function derivePhase(featurePath) {
|
|
187
|
-
|
|
188
|
-
['4-review', 'review', 'review-report.md'],
|
|
189
|
-
['3-implement', 'implement', 'recap.md'],
|
|
190
|
-
['2-plan', 'plan', 'spec.md'],
|
|
191
|
-
['1-design', 'uiux', 'design-system.md'],
|
|
192
|
-
['0-proposal', 'proposal', 'proposal.md'],
|
|
193
|
-
];
|
|
194
|
-
let artifactPhase = 'proposal';
|
|
195
|
-
for (const [folder, phase, canonical] of phaseMap) {
|
|
196
|
-
if (existsSync(join(featurePath, folder, canonical))) { artifactPhase = phase; break; }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Gate-aware (same rule as the hook-side derivePhaseForFeature): an approved
|
|
200
|
-
// gate advances the phase before the next phase's canonical artifact exists,
|
|
201
|
-
// so the agent can actually write that first artifact.
|
|
202
|
-
let gates = null;
|
|
112
|
+
let feature = null;
|
|
203
113
|
try {
|
|
204
114
|
const fjPath = join(featurePath, 'feature.json');
|
|
205
|
-
if (existsSync(fjPath))
|
|
206
|
-
} catch {
|
|
207
|
-
|
|
208
|
-
return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
|
|
115
|
+
if (existsSync(fjPath)) feature = JSON.parse(readFileSync(fjPath, 'utf8'));
|
|
116
|
+
} catch { feature = null; }
|
|
117
|
+
return derivePhaseFrom(feature, diskArtifactProbe(featurePath));
|
|
209
118
|
}
|
|
210
119
|
|
|
211
|
-
/**
|
|
212
|
-
* Derive output existence from filesystem — checks if each output file exists at its expected path.
|
|
213
|
-
* Returns an object matching the old outputs shape for backwards-compatible display.
|
|
214
|
-
*
|
|
215
|
-
* @param {string} featureName - Feature name
|
|
216
|
-
* @param {string} [baseDir] - Project base dir (defaults to cwd)
|
|
217
|
-
* @returns {Object} Map of outputType -> { created: boolean, path: string }
|
|
218
|
-
*/
|
|
219
120
|
export function deriveOutputs(featureName, baseDir = process.cwd()) {
|
|
220
121
|
const outputPaths = getAllOutputPaths(featureName);
|
|
221
122
|
const result = {};
|
|
@@ -226,25 +127,16 @@ export function deriveOutputs(featureName, baseDir = process.cwd()) {
|
|
|
226
127
|
return result;
|
|
227
128
|
}
|
|
228
129
|
|
|
229
|
-
/**
|
|
230
|
-
* Check if state file exists
|
|
231
|
-
*/
|
|
232
130
|
export function stateExists() {
|
|
233
131
|
return existsSync(getStatePath());
|
|
234
132
|
}
|
|
235
133
|
|
|
236
|
-
/**
|
|
237
|
-
* Load state from disk
|
|
238
|
-
* @param {boolean} throwOnError - If false, returns null instead of throwing
|
|
239
|
-
* @returns {Object|null} State object or null
|
|
240
|
-
*/
|
|
241
134
|
export function loadState(throwOnError = true) {
|
|
242
135
|
const statePath = getStatePath();
|
|
243
136
|
const rootExists = existsSync(statePath);
|
|
244
137
|
const featureNames = listFeatureNamesOnDisk();
|
|
245
138
|
const featuresExist = featureNames.some((name) => existsSync(getFeatureJsonPath(name)));
|
|
246
139
|
|
|
247
|
-
// "No state" only when neither the root index nor any per-feature file exists.
|
|
248
140
|
if (!rootExists && !featuresExist) {
|
|
249
141
|
if (throwOnError) {
|
|
250
142
|
throw new Error(`State file not found: ${statePath}\nRun 'morph-spec init' to initialize MORPH in this project.`);
|
|
@@ -269,19 +161,12 @@ export function loadState(throwOnError = true) {
|
|
|
269
161
|
if (fromConfig) state.project = { ...(state.project || {}), ...fromConfig };
|
|
270
162
|
}
|
|
271
163
|
|
|
272
|
-
// Snapshot the
|
|
273
|
-
//
|
|
274
|
-
// predates the split — materialize) from a current-version index entry whose
|
|
275
|
-
// folder is gone (a deleted feature — prune). A missing root index is treated
|
|
276
|
-
// as "no migration" (null): the loop is empty in that case (features hydrate
|
|
277
|
-
// from disk), so the value only matters when the root index exists.
|
|
164
|
+
// Snapshot before the migrations below bump it: the reconcile loop tells a
|
|
165
|
+
// legacy monolith (materialize) from a deleted feature (prune) by this value.
|
|
278
166
|
const fileVersion = rootExists ? state.version : null;
|
|
279
167
|
|
|
280
|
-
// A missing root index means we must (re)generate it from the per-feature
|
|
281
|
-
// files we're about to hydrate — this is the self-heal path after a wipe.
|
|
282
168
|
let mutated = !rootExists;
|
|
283
169
|
|
|
284
|
-
// Migrate v3.0.0 → v4.0.0 (new directory layout) — operates on root features.
|
|
285
170
|
if (state.version === '3.0.0') {
|
|
286
171
|
state.version = '4.0.0';
|
|
287
172
|
for (const [name, feature] of Object.entries(state.features)) {
|
|
@@ -294,8 +179,6 @@ export function loadState(throwOnError = true) {
|
|
|
294
179
|
mutated = true;
|
|
295
180
|
}
|
|
296
181
|
|
|
297
|
-
// Migrate v4.x → v5.0.0: drop derived fields (now filesystem-derived) and
|
|
298
|
-
// workflow selection (v5 has fixed phases).
|
|
299
182
|
if (state.version && state.version.startsWith('4.')) {
|
|
300
183
|
state.version = '5.0.0';
|
|
301
184
|
for (const feature of Object.values(state.features)) {
|
|
@@ -307,57 +190,35 @@ export function loadState(throwOnError = true) {
|
|
|
307
190
|
mutated = true;
|
|
308
191
|
}
|
|
309
192
|
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
// it
|
|
313
|
-
// signal: deleting EVERY folder at once (the reported incident) would leave no
|
|
314
|
-
// survivor and resurrect them all. Version is unconditional:
|
|
315
|
-
//
|
|
316
|
-
// • A pre-v5 root (v3/v4) or an entry rich beyond the thin {status,updatedAt}
|
|
317
|
-
// shape = a legacy monolithic state.json that predates the split. It is the
|
|
318
|
-
// sole copy of its feature — materialize each to its authoritative
|
|
319
|
-
// feature.json (split-on-load migration; idempotent thereafter).
|
|
320
|
-
//
|
|
321
|
-
// • A current-version (v5) thin entry with NO folder = the feature was
|
|
322
|
-
// DELETED and this gitignored index entry is stale. Prune it; NEVER recreate
|
|
323
|
-
// the folder — that turned an intentional delete into a zombie. This is safe
|
|
324
|
-
// and exhaustive because saveState ALWAYS co-writes feature.json + a thin
|
|
325
|
-
// index entry: a v5 thin entry with no feature.json can ONLY mean the folder
|
|
326
|
-
// was removed. The prune sticks — saveState below derives the index from this
|
|
327
|
-
// same in-memory map, and hydration only re-adds features whose folder exists.
|
|
193
|
+
// Index entry without feature.json: a pre-v5 or rich entry is the sole copy of a
|
|
194
|
+
// legacy monolith (materialize it); a v5 thin entry means the folder was deleted
|
|
195
|
+
// (prune it, never resurrect). Safe because saveState always co-writes both.
|
|
328
196
|
const isRichEntry = (f) =>
|
|
329
197
|
f && typeof f === 'object' && Object.keys(f).some((k) => k !== 'status' && k !== 'updatedAt');
|
|
330
198
|
const wasVersionMigration = fileVersion !== null && fileVersion !== STATE_VERSION;
|
|
331
199
|
for (const [name, feature] of Object.entries(state.features)) {
|
|
332
200
|
if (existsSync(getFeatureJsonPath(name))) continue;
|
|
333
201
|
if (wasVersionMigration || isRichEntry(feature)) {
|
|
334
|
-
writeFeatureJson(name, feature);
|
|
202
|
+
writeFeatureJson(name, feature);
|
|
335
203
|
} else {
|
|
336
|
-
delete state.features[name];
|
|
204
|
+
delete state.features[name];
|
|
337
205
|
console.error(`ℹ morph-spec: dropped stale index entry "${name}" (feature folder was deleted).`);
|
|
338
206
|
}
|
|
339
207
|
mutated = true;
|
|
340
208
|
}
|
|
341
209
|
|
|
342
|
-
// Hydrate from per-feature files (authoritative). Also picks up features that
|
|
343
|
-
// exist on disk but are absent from a stale/deleted root index — so a wiped
|
|
344
|
-
// state.json self-heals from the committed feature.json files.
|
|
345
210
|
for (const name of featureNames) {
|
|
346
211
|
const fj = readFeatureJson(name);
|
|
347
212
|
if (fj) state.features[name] = fj;
|
|
348
213
|
}
|
|
349
214
|
|
|
350
215
|
if (mutated) {
|
|
351
|
-
saveState(state);
|
|
216
|
+
saveState(state);
|
|
352
217
|
}
|
|
353
218
|
|
|
354
219
|
return state;
|
|
355
220
|
}
|
|
356
221
|
|
|
357
|
-
/**
|
|
358
|
-
* Save state to disk
|
|
359
|
-
* @param {Object} state - State object to save
|
|
360
|
-
*/
|
|
361
222
|
export function saveState(state) {
|
|
362
223
|
state.metadata = state.metadata || {};
|
|
363
224
|
state.metadata.lastUpdated = new Date().toISOString();
|
|
@@ -365,32 +226,18 @@ export function saveState(state) {
|
|
|
365
226
|
|
|
366
227
|
const features = state.features || {};
|
|
367
228
|
|
|
368
|
-
// Aggregate metadata is derived from the authoritative feature set so the
|
|
369
|
-
// thin root index never drifts from the per-feature files.
|
|
370
229
|
state.metadata.totalFeatures = Object.keys(features).length;
|
|
371
230
|
state.metadata.completedFeatures = Object.values(features)
|
|
372
231
|
.filter((f) => f && f.status === 'completed').length;
|
|
373
232
|
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// NOTE: this IS a cross-feature sweep. Mutating one feature re-persists
|
|
378
|
-
// every sibling; writeFeatureJson's byte-identical guard makes that a no-op
|
|
379
|
-
// only while a sibling's committed file matches our formatting exactly. A
|
|
380
|
-
// sibling saved with a trailing newline (any editor, prettier) is rewritten
|
|
381
|
-
// and shows up as ` M` — dirtying the tree on behalf of a feature the
|
|
382
|
-
// caller never touched. Callers that need to change ONE feature's field
|
|
383
|
-
// should write that feature.json directly instead of round-tripping state
|
|
384
|
-
// through here (see the finish teardown's clearRootWorktreeRegistration).
|
|
233
|
+
// A cross-feature sweep: a sibling saved in another format (trailing newline) is
|
|
234
|
+
// rewritten and dirties the tree. To change one feature, write its feature.json directly.
|
|
385
235
|
const thinFeatures = {};
|
|
386
236
|
for (const [name, feature] of Object.entries(features)) {
|
|
387
237
|
writeFeatureJson(name, feature);
|
|
388
238
|
thinFeatures[name] = { status: feature.status, updatedAt: feature.updatedAt };
|
|
389
239
|
}
|
|
390
240
|
|
|
391
|
-
// 2) Persist the thin, derivable, gitignored root index (status + updatedAt
|
|
392
|
-
// per feature is all that callers like getActiveFeature need from the root;
|
|
393
|
-
// full feature data is hydrated from feature.json on load).
|
|
394
241
|
atomicWriteJson(getStatePath(), {
|
|
395
242
|
version: state.version,
|
|
396
243
|
project: state.project,
|
|
@@ -400,20 +247,11 @@ export function saveState(state) {
|
|
|
400
247
|
});
|
|
401
248
|
}
|
|
402
249
|
|
|
403
|
-
// ============================================================================
|
|
404
|
-
// File Locking (BUG-002)
|
|
405
|
-
// ============================================================================
|
|
406
|
-
|
|
407
250
|
const LOCK_DIR_NAME = '.morph/state.json.lock';
|
|
408
251
|
const LOCK_MAX_RETRIES = 5;
|
|
409
252
|
const LOCK_BASE_MS = 100;
|
|
410
253
|
const LOCK_STALE_MS = 30_000;
|
|
411
254
|
|
|
412
|
-
/**
|
|
413
|
-
* Wrap a read-modify-write cycle on state.json with a mkdir-based mutex.
|
|
414
|
-
* @param {Function} fn - Async function to execute while holding the lock
|
|
415
|
-
* @returns {Promise<*>} Return value of fn
|
|
416
|
-
*/
|
|
417
255
|
export async function withStateLock(fn) {
|
|
418
256
|
const lockPath = join(process.cwd(), LOCK_DIR_NAME);
|
|
419
257
|
|
|
@@ -444,14 +282,6 @@ export async function withStateLock(fn) {
|
|
|
444
282
|
}
|
|
445
283
|
}
|
|
446
284
|
|
|
447
|
-
/**
|
|
448
|
-
* Initialize new state file
|
|
449
|
-
* @param {Object} options - Options
|
|
450
|
-
* @param {boolean} options.force - Overwrite existing file
|
|
451
|
-
* @param {string} options.projectName - Project name
|
|
452
|
-
* @param {string} options.projectType - Project type (e.g., 'dotnet-nextjs')
|
|
453
|
-
* @returns {Object} Initial state
|
|
454
|
-
*/
|
|
455
285
|
export function initState(options = {}) {
|
|
456
286
|
const { force = false, projectName = '{PROJECT_NAME}', projectType = 'dotnet-nextjs' } = options;
|
|
457
287
|
|
|
@@ -481,27 +311,11 @@ export function initState(options = {}) {
|
|
|
481
311
|
return initialState;
|
|
482
312
|
}
|
|
483
313
|
|
|
484
|
-
// ============================================================================
|
|
485
|
-
// Feature Operations
|
|
486
|
-
// ============================================================================
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Get feature from state
|
|
490
|
-
* @param {string} featureName - Feature name
|
|
491
|
-
* @returns {Object|null} Feature object or null
|
|
492
|
-
*/
|
|
493
314
|
export function getFeature(featureName) {
|
|
494
315
|
const state = loadState();
|
|
495
316
|
return state.features[featureName] || null;
|
|
496
317
|
}
|
|
497
318
|
|
|
498
|
-
/**
|
|
499
|
-
* Create or get feature with default structure.
|
|
500
|
-
* v5: every feature follows the fixed phase sequence
|
|
501
|
-
* (proposal → uiux → plan → implement → review). No workflow selection.
|
|
502
|
-
* @param {string} featureName - Feature name
|
|
503
|
-
* @returns {Promise<Object>} Feature object
|
|
504
|
-
*/
|
|
505
319
|
async function ensureFeature(featureName) {
|
|
506
320
|
const state = loadState();
|
|
507
321
|
|
|
@@ -511,17 +325,10 @@ async function ensureFeature(featureName) {
|
|
|
511
325
|
createdAt: new Date().toISOString(),
|
|
512
326
|
updatedAt: new Date().toISOString(),
|
|
513
327
|
approvalGates: {
|
|
514
|
-
proposal: { approved: false, timestamp: null, approvedBy: null },
|
|
515
|
-
uiux: { approved: false, timestamp: null, approvedBy: null },
|
|
516
328
|
plan: { approved: false, timestamp: null, approvedBy: null },
|
|
329
|
+
uiux: { approved: false, timestamp: null, approvedBy: null },
|
|
517
330
|
review: { approved: false, timestamp: null, approvedBy: null }
|
|
518
331
|
},
|
|
519
|
-
tasks: {
|
|
520
|
-
total: 0,
|
|
521
|
-
completed: 0,
|
|
522
|
-
inProgress: 0,
|
|
523
|
-
pending: 0
|
|
524
|
-
},
|
|
525
332
|
checkpoints: [],
|
|
526
333
|
trustConfig: {},
|
|
527
334
|
contextBundles: [],
|
|
@@ -535,28 +342,6 @@ async function ensureFeature(featureName) {
|
|
|
535
342
|
return state.features[featureName];
|
|
536
343
|
}
|
|
537
344
|
|
|
538
|
-
/**
|
|
539
|
-
* Explicitly create a feature with the canonical shape (documented in
|
|
540
|
-
* framework/schemas/feature.schema.json). Idempotent: an existing feature is
|
|
541
|
-
* returned untouched (including its `workType` — a later `--type` on a
|
|
542
|
-
* repeat call never overwrites it; see `morph-spec retype` for that). This
|
|
543
|
-
* is the server-side birth path behind `morph-spec create` — replacing the
|
|
544
|
-
* implicit registration on the first Write into .morph/features/ (the
|
|
545
|
-
* state-sync hook remains as fallback).
|
|
546
|
-
*
|
|
547
|
-
* Works even before any state exists (scaffolds the root index), so a
|
|
548
|
-
* feature can be born as the very first morph action in a project.
|
|
549
|
-
*
|
|
550
|
-
* @param {string} featureName - Feature name (kebab-case), also the folder id
|
|
551
|
-
* @param {{ status?: string, workType?: string, workTypeDetection?: object, name?: string, description?: string }} [options]
|
|
552
|
-
* `name` defaults to `featureName` (the slug) — always stamped so a feature
|
|
553
|
-
* is never nameless. `description` is written only when provided (the CLI
|
|
554
|
-
* `create` enforces it; internal callers may omit it). `workType` defaults to
|
|
555
|
-
* 'feature' (v8 routing — see src/core/work-types.js). `workTypeDetection` is
|
|
556
|
-
* only written when provided (source/confidence/matched audit trail from the
|
|
557
|
-
* `--type`/`--request` resolution in `create.js`).
|
|
558
|
-
* @returns {Promise<{ feature: Object, created: boolean }>}
|
|
559
|
-
*/
|
|
560
345
|
export async function createFeature(featureName, options = {}) {
|
|
561
346
|
const { status = 'in_progress', workType, workTypeDetection, name, description } = options;
|
|
562
347
|
const state = loadState(false) || scaffoldState();
|
|
@@ -575,18 +360,11 @@ export async function createFeature(featureName, options = {}) {
|
|
|
575
360
|
createdAt: now,
|
|
576
361
|
updatedAt: now,
|
|
577
362
|
approvalGates: {
|
|
578
|
-
proposal: { approved: false, timestamp: null, approvedBy: null },
|
|
579
|
-
uiux: { approved: false, timestamp: null, approvedBy: null },
|
|
580
363
|
plan: { approved: false, timestamp: null, approvedBy: null },
|
|
364
|
+
uiux: { approved: false, timestamp: null, approvedBy: null },
|
|
581
365
|
review: { approved: false, timestamp: null, approvedBy: null }
|
|
582
366
|
},
|
|
583
367
|
gateDecisions: {},
|
|
584
|
-
tasks: {
|
|
585
|
-
total: 0,
|
|
586
|
-
completed: 0,
|
|
587
|
-
inProgress: 0,
|
|
588
|
-
pending: 0
|
|
589
|
-
},
|
|
590
368
|
checkpoints: [],
|
|
591
369
|
trustConfig: {},
|
|
592
370
|
contextBundles: [],
|
|
@@ -602,12 +380,6 @@ export async function createFeature(featureName, options = {}) {
|
|
|
602
380
|
return { feature: state.features[featureName], created: true };
|
|
603
381
|
}
|
|
604
382
|
|
|
605
|
-
/**
|
|
606
|
-
* Update feature property (supports nested keys like "tasks.completed")
|
|
607
|
-
* @param {string} featureName - Feature name
|
|
608
|
-
* @param {string} key - Property key (supports dot notation)
|
|
609
|
-
* @param {any} value - Value to set
|
|
610
|
-
*/
|
|
611
383
|
export async function updateFeature(featureName, key, value) {
|
|
612
384
|
await ensureFeature(featureName);
|
|
613
385
|
const state = loadState(); // Load AFTER ensuring feature exists
|
|
@@ -629,11 +401,6 @@ export async function updateFeature(featureName, key, value) {
|
|
|
629
401
|
saveState(state);
|
|
630
402
|
}
|
|
631
403
|
|
|
632
|
-
/**
|
|
633
|
-
* Update multiple feature properties at once
|
|
634
|
-
* @param {string} featureName - Feature name
|
|
635
|
-
* @param {Object} updates - Object with key-value pairs to update
|
|
636
|
-
*/
|
|
637
404
|
export async function updateFeatureMultiple(featureName, updates) {
|
|
638
405
|
await ensureFeature(featureName);
|
|
639
406
|
const state = loadState(); // Load AFTER ensuring feature exists
|
|
@@ -657,63 +424,16 @@ export async function updateFeatureMultiple(featureName, updates) {
|
|
|
657
424
|
saveState(state);
|
|
658
425
|
}
|
|
659
426
|
|
|
660
|
-
/**
|
|
661
|
-
* Add checkpoint to feature
|
|
662
|
-
* @param {string} featureName - Feature name
|
|
663
|
-
* @param {string} note - Checkpoint note
|
|
664
|
-
* @returns {Object} Checkpoint object
|
|
665
|
-
*/
|
|
666
|
-
export async function addCheckpoint(featureName, note) {
|
|
667
|
-
await ensureFeature(featureName);
|
|
668
|
-
const state = loadState();
|
|
669
|
-
const feature = state.features[featureName];
|
|
670
|
-
|
|
671
|
-
const checkpoint = {
|
|
672
|
-
passed: true,
|
|
673
|
-
checkpointNum: feature.checkpoints.length + 1,
|
|
674
|
-
timestamp: new Date().toISOString(),
|
|
675
|
-
phase: feature.phase || 'proposal',
|
|
676
|
-
results: [],
|
|
677
|
-
summary: {
|
|
678
|
-
note: note,
|
|
679
|
-
completedTasks: feature.tasks.completed
|
|
680
|
-
}
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
feature.checkpoints.push(checkpoint);
|
|
684
|
-
feature.updatedAt = new Date().toISOString();
|
|
685
|
-
|
|
686
|
-
saveState(state);
|
|
687
|
-
return checkpoint;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Normalize output type from kebab-case to camelCase (BUG-002 fix - Enhanced)
|
|
692
|
-
* Supports both kebab-case and camelCase input formats for all output types
|
|
693
|
-
* @param {string} type - Output type (e.g., 'ui-design-system', 'stack-scan', or 'uiDesignSystem')
|
|
694
|
-
* @returns {string} Normalized type in camelCase
|
|
695
|
-
*/
|
|
696
427
|
function normalizeOutputType(type) {
|
|
697
|
-
// If string doesn't contain hyphens, assume it's already camelCase
|
|
698
428
|
if (!type.includes('-')) {
|
|
699
429
|
return type;
|
|
700
430
|
}
|
|
701
431
|
|
|
702
|
-
// Convert kebab-case to camelCase automatically
|
|
703
|
-
// Example: 'ui-design-system' → 'uiDesignSystem'
|
|
704
|
-
// Example: 'stack-scan' → 'stackScan'
|
|
705
432
|
const camelCase = type.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
706
433
|
|
|
707
434
|
return camelCase;
|
|
708
435
|
}
|
|
709
436
|
|
|
710
|
-
/**
|
|
711
|
-
* Calculate Levenshtein distance between two strings
|
|
712
|
-
* Used for fuzzy matching and typo detection
|
|
713
|
-
* @param {string} a - First string
|
|
714
|
-
* @param {string} b - Second string
|
|
715
|
-
* @returns {number} Edit distance
|
|
716
|
-
*/
|
|
717
437
|
function levenshteinDistance(a, b) {
|
|
718
438
|
const matrix = [];
|
|
719
439
|
|
|
@@ -731,9 +451,9 @@ function levenshteinDistance(a, b) {
|
|
|
731
451
|
matrix[i][j] = matrix[i - 1][j - 1];
|
|
732
452
|
} else {
|
|
733
453
|
matrix[i][j] = Math.min(
|
|
734
|
-
matrix[i - 1][j - 1] + 1,
|
|
735
|
-
matrix[i][j - 1] + 1,
|
|
736
|
-
matrix[i - 1][j] + 1
|
|
454
|
+
matrix[i - 1][j - 1] + 1,
|
|
455
|
+
matrix[i][j - 1] + 1,
|
|
456
|
+
matrix[i - 1][j] + 1
|
|
737
457
|
);
|
|
738
458
|
}
|
|
739
459
|
}
|
|
@@ -742,12 +462,6 @@ function levenshteinDistance(a, b) {
|
|
|
742
462
|
return matrix[b.length][a.length];
|
|
743
463
|
}
|
|
744
464
|
|
|
745
|
-
/**
|
|
746
|
-
* Find closest matching output type for error suggestions
|
|
747
|
-
* @param {string} inputType - The invalid input type
|
|
748
|
-
* @param {Array<string>} validTypes - List of valid output types
|
|
749
|
-
* @returns {string|null} Closest match or null
|
|
750
|
-
*/
|
|
751
465
|
function findClosestMatch(inputType, validTypes) {
|
|
752
466
|
let closestMatch = null;
|
|
753
467
|
let minDistance = Infinity;
|
|
@@ -757,14 +471,11 @@ function findClosestMatch(inputType, validTypes) {
|
|
|
757
471
|
for (const validType of validTypes) {
|
|
758
472
|
const distance = levenshteinDistance(normalizedInput, validType.toLowerCase());
|
|
759
473
|
|
|
760
|
-
// Consider it a match if distance is less than 3 (likely typo)
|
|
761
|
-
// or if one string contains the other (partial match)
|
|
762
474
|
if (distance < minDistance && distance <= 3) {
|
|
763
475
|
minDistance = distance;
|
|
764
476
|
closestMatch = validType;
|
|
765
477
|
}
|
|
766
478
|
|
|
767
|
-
// Also check for partial matches
|
|
768
479
|
if (validType.toLowerCase().includes(normalizedInput) ||
|
|
769
480
|
normalizedInput.includes(validType.toLowerCase())) {
|
|
770
481
|
if (distance < minDistance) {
|
|
@@ -774,28 +485,20 @@ function findClosestMatch(inputType, validTypes) {
|
|
|
774
485
|
}
|
|
775
486
|
}
|
|
776
487
|
|
|
777
|
-
// Only suggest if similarity is reasonable
|
|
778
488
|
return minDistance <= 3 ? closestMatch : null;
|
|
779
489
|
}
|
|
780
490
|
|
|
781
|
-
/**
|
|
782
|
-
* Mark output as created
|
|
783
|
-
* @param {string} featureName - Feature name
|
|
784
|
-
* @param {string} outputType - Output type (proposal, spec, tasks, etc.)
|
|
785
|
-
*/
|
|
786
491
|
export async function markOutput(featureName, outputType) {
|
|
787
492
|
await ensureFeature(featureName);
|
|
788
493
|
const state = loadState();
|
|
789
494
|
|
|
790
495
|
const normalized = normalizeOutputType(outputType);
|
|
791
496
|
|
|
792
|
-
// Initialize outputs on demand (not pre-stored)
|
|
793
497
|
if (!state.features[featureName].outputs) {
|
|
794
498
|
state.features[featureName].outputs = getAllOutputPaths(featureName);
|
|
795
499
|
}
|
|
796
500
|
|
|
797
501
|
if (!state.features[featureName].outputs[normalized]) {
|
|
798
|
-
// Try to find closest match for better error message
|
|
799
502
|
const validCamelCaseTypes = Object.keys(state.features[featureName].outputs);
|
|
800
503
|
const suggestion = findClosestMatch(outputType, validCamelCaseTypes);
|
|
801
504
|
|
|
@@ -803,7 +506,6 @@ export async function markOutput(featureName, outputType) {
|
|
|
803
506
|
|
|
804
507
|
if (suggestion) {
|
|
805
508
|
errorMsg += `\n\nDid you mean '${suggestion}'?`;
|
|
806
|
-
// If the suggestion has a kebab-case alternative, show it too
|
|
807
509
|
const kebabAlternative = {
|
|
808
510
|
'uiDesignSystem': 'ui-design-system',
|
|
809
511
|
'uiMockups': 'ui-mockups',
|
|
@@ -825,7 +527,6 @@ export async function markOutput(featureName, outputType) {
|
|
|
825
527
|
state.features[featureName].outputs[normalized].created = true;
|
|
826
528
|
state.features[featureName].updatedAt = new Date().toISOString();
|
|
827
529
|
|
|
828
|
-
// Track the file change automatically
|
|
829
530
|
const outputPath = state.features[featureName].outputs[normalized].path;
|
|
830
531
|
if (outputPath) {
|
|
831
532
|
if (!Array.isArray(state.features[featureName].fileChanges)) {
|
|
@@ -842,13 +543,6 @@ export async function markOutput(featureName, outputType) {
|
|
|
842
543
|
saveState(state);
|
|
843
544
|
}
|
|
844
545
|
|
|
845
|
-
/**
|
|
846
|
-
* Track a file change for a feature
|
|
847
|
-
* @param {string} featureName - Feature name
|
|
848
|
-
* @param {string} filePath - Path of the changed file
|
|
849
|
-
* @param {string} action - 'created' | 'modified' | 'deleted'
|
|
850
|
-
* @param {string} phase - Phase during which the change occurred
|
|
851
|
-
*/
|
|
852
546
|
export async function trackFileChange(featureName, filePath, action, phase) {
|
|
853
547
|
await ensureFeature(featureName);
|
|
854
548
|
const state = loadState();
|
|
@@ -860,7 +554,7 @@ export async function trackFileChange(featureName, filePath, action, phase) {
|
|
|
860
554
|
state.features[featureName].fileChanges.push({
|
|
861
555
|
path: filePath,
|
|
862
556
|
action,
|
|
863
|
-
phase: phase || state.features[featureName].phase || '
|
|
557
|
+
phase: phase || state.features[featureName].phase || 'plan',
|
|
864
558
|
timestamp: new Date().toISOString()
|
|
865
559
|
});
|
|
866
560
|
|
|
@@ -868,13 +562,6 @@ export async function trackFileChange(featureName, filePath, action, phase) {
|
|
|
868
562
|
saveState(state);
|
|
869
563
|
}
|
|
870
564
|
|
|
871
|
-
/**
|
|
872
|
-
* Get file changes for a feature, optionally grouped by phase
|
|
873
|
-
* @param {string} featureName - Feature name
|
|
874
|
-
* @param {Object} options - Options
|
|
875
|
-
* @param {boolean} options.groupByPhase - Group changes by phase
|
|
876
|
-
* @returns {Array|Object} File changes
|
|
877
|
-
*/
|
|
878
565
|
export function getFileChanges(featureName, options = {}) {
|
|
879
566
|
const state = loadState();
|
|
880
567
|
const feature = state.features[featureName];
|
|
@@ -898,12 +585,6 @@ export function getFileChanges(featureName, options = {}) {
|
|
|
898
585
|
return changes;
|
|
899
586
|
}
|
|
900
587
|
|
|
901
|
-
/**
|
|
902
|
-
* Skip a phase and record it
|
|
903
|
-
* @param {string} featureName - Feature name
|
|
904
|
-
* @param {string} phase - Phase to skip
|
|
905
|
-
* @param {string} reason - Reason for skipping
|
|
906
|
-
*/
|
|
907
588
|
export async function skipPhase(featureName, phase, reason = '') {
|
|
908
589
|
await ensureFeature(featureName);
|
|
909
590
|
const state = loadState();
|
|
@@ -912,7 +593,6 @@ export async function skipPhase(featureName, phase, reason = '') {
|
|
|
912
593
|
state.features[featureName].skippedPhases = [];
|
|
913
594
|
}
|
|
914
595
|
|
|
915
|
-
// Don't add duplicates
|
|
916
596
|
const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
|
|
917
597
|
if (!existing) {
|
|
918
598
|
state.features[featureName].skippedPhases.push({
|
|
@@ -926,11 +606,6 @@ export async function skipPhase(featureName, phase, reason = '') {
|
|
|
926
606
|
saveState(state);
|
|
927
607
|
}
|
|
928
608
|
|
|
929
|
-
/**
|
|
930
|
-
* Get skipped phases for a feature
|
|
931
|
-
* @param {string} featureName - Feature name
|
|
932
|
-
* @returns {Array} Array of skipped phases
|
|
933
|
-
*/
|
|
934
609
|
export function getSkippedPhases(featureName) {
|
|
935
610
|
const state = loadState();
|
|
936
611
|
if (!state.features[featureName]) {
|
|
@@ -939,19 +614,11 @@ export function getSkippedPhases(featureName) {
|
|
|
939
614
|
return state.features[featureName].skippedPhases || [];
|
|
940
615
|
}
|
|
941
616
|
|
|
942
|
-
/**
|
|
943
|
-
* List all features
|
|
944
|
-
* @returns {Array} Array of [featureName, featureObject] tuples
|
|
945
|
-
*/
|
|
946
617
|
export function listFeatures() {
|
|
947
618
|
const state = loadState();
|
|
948
619
|
return Object.entries(state.features);
|
|
949
620
|
}
|
|
950
621
|
|
|
951
|
-
/**
|
|
952
|
-
* Get project summary
|
|
953
|
-
* @returns {Object} Summary with metadata
|
|
954
|
-
*/
|
|
955
622
|
export function getSummary() {
|
|
956
623
|
const state = loadState();
|
|
957
624
|
return {
|
|
@@ -961,26 +628,14 @@ export function getSummary() {
|
|
|
961
628
|
};
|
|
962
629
|
}
|
|
963
630
|
|
|
964
|
-
// ============================================================================
|
|
965
|
-
// Approval Gates Operations
|
|
966
|
-
// ============================================================================
|
|
967
|
-
|
|
968
|
-
/**
|
|
969
|
-
* Set approval gate status
|
|
970
|
-
* @param {string} featureName - Feature name
|
|
971
|
-
* @param {string} gate - Gate name (proposal, uiux, plan, review)
|
|
972
|
-
* @param {boolean} approved - Approval status
|
|
973
|
-
* @param {Object} metadata - Additional metadata (approvedBy, reason, etc.)
|
|
974
|
-
*/
|
|
975
631
|
export async function setApprovalGate(featureName, gate, approved, metadata = {}) {
|
|
976
632
|
const state = loadState();
|
|
977
633
|
const feature = await ensureFeature(featureName);
|
|
978
634
|
|
|
979
635
|
if (!feature.approvalGates) {
|
|
980
636
|
feature.approvalGates = {
|
|
981
|
-
proposal: { approved: false, timestamp: null, approvedBy: null },
|
|
982
|
-
uiux: { approved: false, timestamp: null, approvedBy: null },
|
|
983
637
|
plan: { approved: false, timestamp: null, approvedBy: null },
|
|
638
|
+
uiux: { approved: false, timestamp: null, approvedBy: null },
|
|
984
639
|
review: { approved: false, timestamp: null, approvedBy: null }
|
|
985
640
|
};
|
|
986
641
|
}
|
|
@@ -996,12 +651,6 @@ export async function setApprovalGate(featureName, gate, approved, metadata = {}
|
|
|
996
651
|
saveState(state);
|
|
997
652
|
}
|
|
998
653
|
|
|
999
|
-
/**
|
|
1000
|
-
* Get approval gate status
|
|
1001
|
-
* @param {string} featureName - Feature name
|
|
1002
|
-
* @param {string} gate - Gate name
|
|
1003
|
-
* @returns {Object|null} Gate object or null
|
|
1004
|
-
*/
|
|
1005
654
|
export function getApprovalGate(featureName, gate) {
|
|
1006
655
|
const state = loadState();
|
|
1007
656
|
const feature = state.features[featureName];
|
|
@@ -1013,11 +662,6 @@ export function getApprovalGate(featureName, gate) {
|
|
|
1013
662
|
return feature.approvalGates[gate] || null;
|
|
1014
663
|
}
|
|
1015
664
|
|
|
1016
|
-
/**
|
|
1017
|
-
* Check if feature is pending approval
|
|
1018
|
-
* @param {string} featureName - Feature name
|
|
1019
|
-
* @returns {boolean} True if any gate is pending approval
|
|
1020
|
-
*/
|
|
1021
665
|
export function isPendingApproval(featureName) {
|
|
1022
666
|
const state = loadState();
|
|
1023
667
|
const feature = state.features[featureName];
|
|
@@ -1026,9 +670,8 @@ export function isPendingApproval(featureName) {
|
|
|
1026
670
|
return false;
|
|
1027
671
|
}
|
|
1028
672
|
|
|
1029
|
-
const currentPhase = feature.phase || '
|
|
673
|
+
const currentPhase = feature.phase || 'plan';
|
|
1030
674
|
|
|
1031
|
-
// Check if current phase has an approval gate
|
|
1032
675
|
const phaseGateMap = {
|
|
1033
676
|
'plan': 'plan',
|
|
1034
677
|
'review': 'review',
|
|
@@ -1044,11 +687,6 @@ export function isPendingApproval(featureName) {
|
|
|
1044
687
|
return gate && !gate.approved;
|
|
1045
688
|
}
|
|
1046
689
|
|
|
1047
|
-
/**
|
|
1048
|
-
* Get approval history for a feature
|
|
1049
|
-
* @param {string} featureName - Feature name
|
|
1050
|
-
* @returns {Array} Array of approval events
|
|
1051
|
-
*/
|
|
1052
690
|
export function getApprovalHistory(featureName) {
|
|
1053
691
|
const state = loadState();
|
|
1054
692
|
const feature = state.features[featureName];
|
|
@@ -1071,6 +709,5 @@ export function getApprovalHistory(featureName) {
|
|
|
1071
709
|
}
|
|
1072
710
|
});
|
|
1073
711
|
|
|
1074
|
-
// Sort by timestamp
|
|
1075
712
|
return history.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
1076
713
|
}
|