@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,73 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* A fleet runs one Claude Code instance per feature worktree. What travels
|
|
5
|
-
* between sessions must be a HANDOFF, not an export: a compact document that
|
|
6
|
-
* points at the feature's own files instead of copying their contents, so a
|
|
7
|
-
* fresh instance starts focused instead of inheriting a wall of stale context.
|
|
8
|
-
*
|
|
9
|
-
* Deterministic by construction — everything here is derived from feature.json,
|
|
10
|
-
* tasks.json, decisions.md and the worktree env manifest. The document is
|
|
11
|
-
* therefore REGENERABLE (never hand-edited, never merged): `morph-spec handoff`
|
|
12
|
-
* rebuilds it, and setup/create/advance/approve regenerate it at the moments a
|
|
13
|
-
* new session is most likely to pick the feature up.
|
|
14
|
-
*
|
|
15
|
-
* Every section is fail-open: a missing tasks.json or a corrupt decisions.md
|
|
16
|
-
* degrades that section, never the document.
|
|
17
|
-
*/
|
|
1
|
+
// Derived only from committed sources, so it is regenerated, never hand-edited or merged.
|
|
2
|
+
// Every section is fail-open.
|
|
18
3
|
|
|
19
4
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
20
5
|
import { join } from 'path';
|
|
21
6
|
import { ensureCommonExclude } from '../../../framework/hooks/shared/git-exclude.js';
|
|
22
7
|
import { derivePhaseForFeature, getPendingGates } from '../../../framework/hooks/shared/state-reader.js';
|
|
8
|
+
import { sliceProgress, readySlices } from '../../../framework/hooks/shared/slice-progress.js';
|
|
23
9
|
|
|
24
|
-
const PHASE_DIRS = {
|
|
25
|
-
proposal: '0-proposal',
|
|
26
|
-
uiux: '1-design',
|
|
27
|
-
plan: '2-plan',
|
|
28
|
-
implement: '3-implement',
|
|
29
|
-
review: '4-review',
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** Ordered pointer candidates — only existing paths make it into the doc. */
|
|
33
10
|
const POINTER_CANDIDATES = [
|
|
34
|
-
['
|
|
35
|
-
|
|
36
|
-
['
|
|
37
|
-
['
|
|
38
|
-
['
|
|
39
|
-
['
|
|
40
|
-
['
|
|
41
|
-
['4-review/review-report.md', 'relatório do Gate 3'],
|
|
11
|
+
['2-plan/plan.md', 'plano: problema, cenários com prova, decisões e fatias'],
|
|
12
|
+
['2-plan/checks.json', 'cenários e fatias extraídos pelo plan-check'],
|
|
13
|
+
['1-design/design-system.md', 'design system da feature'],
|
|
14
|
+
['3-implement/reports', 'relatórios das fatias (**Arquivos:**/**Standards:**): o registro da implementação'],
|
|
15
|
+
['4-review/verifier.md', 'verificador do Gate 3: cada cenário provado'],
|
|
16
|
+
['4-review/reviewer.md', 'revisor do Gate 3: escopo, arquitetura, regressão'],
|
|
17
|
+
['4-review/security.md', 'segurança do Gate 3 (só com caminho sensível)'],
|
|
42
18
|
];
|
|
43
19
|
|
|
44
20
|
function readJson(path) {
|
|
45
21
|
return JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''));
|
|
46
22
|
}
|
|
47
23
|
|
|
48
|
-
function taskSummary(featureDir) {
|
|
49
|
-
try {
|
|
50
|
-
const tasksPath = join(featureDir, '2-plan', 'tasks.json');
|
|
51
|
-
if (!existsSync(tasksPath)) return null;
|
|
52
|
-
const data = readJson(tasksPath);
|
|
53
|
-
if (!Array.isArray(data.tasks)) return null;
|
|
54
|
-
const done = data.tasks.filter((t) => t.status === 'done').length;
|
|
55
|
-
const doneIds = new Set(data.tasks.filter((t) => t.status === 'done').map((t) => t.id));
|
|
56
|
-
const ready = data.tasks.filter(
|
|
57
|
-
(t) => t.status === 'pending' && Array.isArray(t.dependencies) && t.dependencies.every((d) => doneIds.has(d)),
|
|
58
|
-
);
|
|
59
|
-
return { done, total: data.tasks.length, ready };
|
|
60
|
-
} catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
24
|
function decisionTitles(featureDir, limit = 5) {
|
|
66
25
|
try {
|
|
67
|
-
const path = join(featureDir, '
|
|
26
|
+
const path = join(featureDir, '2-plan', 'checks.json');
|
|
68
27
|
if (!existsSync(path)) return [];
|
|
69
|
-
const
|
|
70
|
-
|
|
28
|
+
const adrs = readJson(path).adrs;
|
|
29
|
+
if (!Array.isArray(adrs)) return [];
|
|
30
|
+
return adrs.map((a) => `${a.id} — ${a.title}`).slice(-limit);
|
|
71
31
|
} catch {
|
|
72
32
|
return [];
|
|
73
33
|
}
|
|
@@ -86,53 +46,34 @@ function readWorktreeEnv(cwd) {
|
|
|
86
46
|
}
|
|
87
47
|
}
|
|
88
48
|
|
|
89
|
-
function nextAction(feature, phase,
|
|
49
|
+
function nextAction(feature, phase, slices, featureName) {
|
|
90
50
|
const gates = feature.approvals || feature.approvalGates || {};
|
|
91
51
|
const approved = (g) => gates[g]?.approved === true;
|
|
92
52
|
|
|
93
53
|
switch (phase) {
|
|
94
|
-
case '
|
|
95
|
-
|
|
96
|
-
// the derived phase only flips when 2-plan/spec.md lands (derivePhase is
|
|
97
|
-
// artifact-first). Pointing back at the proposal here would be wrong.
|
|
98
|
-
return approved('proposal')
|
|
99
|
-
? `Gate 1 aprovado — produza \`2-plan/spec.md\` + \`tasks.json\` (a fase avança com o primeiro artefato; Gate 2 em seguida).`
|
|
100
|
-
: `Complete \`0-proposal/proposal.md\` e aprove o Gate 1 (\`morph-spec advance ${featureName}\`). Skill: /morph-proposal ${featureName}.`;
|
|
54
|
+
case 'plan':
|
|
55
|
+
return `Escreva \`2-plan/plan.md\`, rode \`morph-spec plan-check ${featureName}\` até zero erro e aprove o gate do plano (\`morph-spec advance ${featureName}\`). Skill: /morph-plan ${featureName}.`;
|
|
101
56
|
case 'uiux':
|
|
102
57
|
return approved('uiux')
|
|
103
|
-
? `Gate de uiux aprovado —
|
|
58
|
+
? `Gate de uiux aprovado — implemente as fatias do plano via /morph-apply ${featureName}.`
|
|
104
59
|
: `Complete \`1-design/design-system.md\` e aprove o gate de uiux (\`morph-spec advance ${featureName}\`).`;
|
|
105
|
-
case 'plan':
|
|
106
|
-
return `Produza \`2-plan/spec.md\` + \`tasks.json\` e aprove o Gate 2 (\`morph-spec advance ${featureName}\`).`;
|
|
107
60
|
case 'implement': {
|
|
108
|
-
const ready =
|
|
61
|
+
const ready = readySlices(slices).map((s) => `${s.id} "${s.title}"`).join(', ');
|
|
109
62
|
return ready
|
|
110
|
-
? `
|
|
111
|
-
: `Todas as
|
|
63
|
+
? `Implemente as fatias prontas — ${ready} — via /morph-apply ${featureName} (cada fatia grava 3-implement/reports/{id}.md).`
|
|
64
|
+
: `Todas as fatias têm relatório final — rode \`morph-spec verify ${featureName}\` e siga para a review. Skill: /morph-apply ${featureName}.`;
|
|
112
65
|
}
|
|
113
66
|
case 'review':
|
|
114
67
|
return approved('review')
|
|
115
68
|
? `Gate 3 aprovado — feche: \`morph-spec finish ${featureName} --pr\` (abre PR) ou \`--merge\` (merge local na default). Arquiva + integra num passo.`
|
|
116
|
-
: `
|
|
69
|
+
: `Rode /morph-review ${featureName} (verificador ∥ revisor, segurança se exigida) e aprove o Gate 3; depois \`morph-spec finish ${featureName} --pr|--merge\`.`;
|
|
117
70
|
default:
|
|
118
71
|
return `Rode \`morph-spec status ${featureName}\` para orientar o próximo passo.`;
|
|
119
72
|
}
|
|
120
73
|
}
|
|
121
74
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* root or worktree — wherever the feature's `.morph/features/{f}` lives).
|
|
125
|
-
*
|
|
126
|
-
* @param {string} featureName
|
|
127
|
-
* @param {string} [cwd]
|
|
128
|
-
* @param {{ worktree?: {path:string, branch:string}|null }} [opts] - override
|
|
129
|
-
* the `worktree` bookkeeping instead of reading `feature.worktree`. Needed
|
|
130
|
-
* because that field is ROOT-OWNED and stripped from every branch-local
|
|
131
|
-
* copy at birth (create.js) — a handoff built FROM the worktree's own copy
|
|
132
|
-
* would otherwise always say "no worktree registered", even while sitting
|
|
133
|
-
* inside one. Pass `undefined` (the default) to fall back to `feature.worktree`.
|
|
134
|
-
* @returns {{ markdown: string, meta: { generatedAt: string, phase: string, tasksDone: number, tasksTotal: number } } | null}
|
|
135
|
-
*/
|
|
75
|
+
// `opts.worktree` overrides `feature.worktree`: that field is root-owned and stripped from branch-local
|
|
76
|
+
// copies, so a handoff built inside a worktree would otherwise say "no worktree registered".
|
|
136
77
|
export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
137
78
|
const featureDir = join(cwd, '.morph', 'features', featureName);
|
|
138
79
|
if (!existsSync(join(featureDir, 'feature.json'))) return null;
|
|
@@ -144,15 +85,15 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
144
85
|
return null;
|
|
145
86
|
}
|
|
146
87
|
|
|
147
|
-
let phase = '
|
|
88
|
+
let phase = 'plan';
|
|
148
89
|
let pendingGates = [];
|
|
149
90
|
try {
|
|
150
|
-
phase = derivePhaseForFeature(featureName, cwd) || '
|
|
91
|
+
phase = derivePhaseForFeature(featureName, cwd) || 'plan';
|
|
151
92
|
pendingGates = getPendingGates(featureName, cwd) || [];
|
|
152
|
-
} catch {
|
|
93
|
+
} catch {}
|
|
153
94
|
|
|
154
95
|
const generatedAt = new Date().toISOString();
|
|
155
|
-
const
|
|
96
|
+
const slices = sliceProgress(featureDir);
|
|
156
97
|
const env = readWorktreeEnv(cwd);
|
|
157
98
|
const decisions = decisionTitles(featureDir);
|
|
158
99
|
const worktree = opts.worktree !== undefined ? opts.worktree : (feature.worktree || null);
|
|
@@ -187,9 +128,9 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
187
128
|
'## Estado',
|
|
188
129
|
'',
|
|
189
130
|
`- Gates: ${gateLine}`,
|
|
190
|
-
|
|
191
|
-
? `-
|
|
192
|
-
: '-
|
|
131
|
+
slices
|
|
132
|
+
? `- Fatias: ${slices.done}/${slices.total} feitas${slices.partial ? ` · ${slices.partial} parcial(is)` : ''} · prontas agora: ${readySlices(slices).map((s) => `**${s.id}** "${s.title}"`).join(', ') || 'nenhuma'}`
|
|
133
|
+
: '- Fatias: sem checks.json ainda (rode plan-check)',
|
|
193
134
|
pendingGates.length ? `- Gates pendentes: ${pendingGates.join(', ')}` : null,
|
|
194
135
|
'',
|
|
195
136
|
'## Leia nesta ordem (ponteiros — nada copiado)',
|
|
@@ -200,7 +141,7 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
200
141
|
'',
|
|
201
142
|
'## Próxima ação',
|
|
202
143
|
'',
|
|
203
|
-
nextAction(feature, phase,
|
|
144
|
+
nextAction(feature, phase, slices, featureName),
|
|
204
145
|
'',
|
|
205
146
|
'## Retomar',
|
|
206
147
|
'',
|
|
@@ -214,8 +155,8 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
214
155
|
meta: {
|
|
215
156
|
generatedAt,
|
|
216
157
|
phase,
|
|
217
|
-
|
|
218
|
-
|
|
158
|
+
slicesDone: slices?.done ?? 0,
|
|
159
|
+
slicesTotal: slices?.total ?? 0,
|
|
219
160
|
},
|
|
220
161
|
};
|
|
221
162
|
}
|
|
@@ -224,29 +165,13 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
224
165
|
const HANDOFF_EXCLUDE_MARKER = '# morph-spec: derived handoff docs (managed — see handoff-generator)';
|
|
225
166
|
const HANDOFF_EXCLUDE_PATTERN = '/.morph/**/handoff.md';
|
|
226
167
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
* itself — regenerate > synchronize, the same principle as the materialized
|
|
230
|
-
* state.json. Left visible, every `worktree setup`/`advance` would drop an
|
|
231
|
-
* untracked file into the very `git status` the finish gates read. The
|
|
232
|
-
* common-dir `.git/info/exclude` hides it in every worktree of the repo
|
|
233
|
-
* (including the primary root); `**` also covers a feature folder after
|
|
234
|
-
* `archive` moves it. Idempotent and fail-open — a handoff that shows up as
|
|
235
|
-
* untracked is noise, never data loss.
|
|
236
|
-
*/
|
|
168
|
+
// Never committed: left visible it lands in the `git status` the finish gates read. The common-dir
|
|
169
|
+
// exclude hides it in every worktree; `**` also covers a feature folder after `archive` moves it.
|
|
237
170
|
function ensureHandoffExcluded(cwd) {
|
|
238
171
|
ensureCommonExclude(cwd, [HANDOFF_EXCLUDE_PATTERN], HANDOFF_EXCLUDE_MARKER);
|
|
239
172
|
}
|
|
240
173
|
|
|
241
|
-
/**
|
|
242
|
-
* Write `.morph/features/{f}/handoff.md`. Returns the written path, or null
|
|
243
|
-
* when the feature does not exist (nothing is created).
|
|
244
|
-
*
|
|
245
|
-
* @param {string} featureName
|
|
246
|
-
* @param {string} [cwd]
|
|
247
|
-
* @param {{ worktree?: {path:string, branch:string}|null }} [opts] - see buildHandoff
|
|
248
|
-
* @returns {string|null}
|
|
249
|
-
*/
|
|
174
|
+
/** The written path, or null when the feature does not exist (nothing is created). */
|
|
250
175
|
export function writeHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
251
176
|
const result = buildHandoff(featureName, cwd, opts);
|
|
252
177
|
if (!result) return null;
|
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scaffold Generator - Generates VSA feature slice (.NET) and feature page (Next.js) files.
|
|
3
|
-
*
|
|
4
|
-
* Uses existing Handlebars templates from framework/templates/ to generate
|
|
5
|
-
* complete feature structures with one command.
|
|
6
|
-
*
|
|
7
|
-
* @module scaffold-generator
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
11
2
|
import { join, dirname, basename } from 'path';
|
|
12
3
|
import { renderTemplate } from '../../core/templates/template-renderer.js';
|
|
13
4
|
import { resolveFrameworkTemplatePath } from '../../core/templates/template-registry.js';
|
|
14
5
|
|
|
15
|
-
/**
|
|
16
|
-
* Convert kebab-case to PascalCase.
|
|
17
|
-
* @param {string} str
|
|
18
|
-
* @returns {string}
|
|
19
|
-
*/
|
|
20
6
|
function toPascalCase(str) {
|
|
21
7
|
return str
|
|
22
8
|
.split('-')
|
|
@@ -24,11 +10,6 @@ function toPascalCase(str) {
|
|
|
24
10
|
.join('');
|
|
25
11
|
}
|
|
26
12
|
|
|
27
|
-
/**
|
|
28
|
-
* Convert kebab-case to camelCase.
|
|
29
|
-
* @param {string} str
|
|
30
|
-
* @returns {string}
|
|
31
|
-
*/
|
|
32
13
|
function toCamelCase(str) {
|
|
33
14
|
return str
|
|
34
15
|
.split('-')
|
|
@@ -36,16 +17,8 @@ function toCamelCase(str) {
|
|
|
36
17
|
.join('');
|
|
37
18
|
}
|
|
38
19
|
|
|
39
|
-
/**
|
|
40
|
-
* Normalize a namespace to kebab-case so Handlebars {{pascalCase}} reconstructs it correctly.
|
|
41
|
-
* Handles: PascalCase ("MyBookStore" → "my-book-store"), dot-separated ("My.App" → "my-app"),
|
|
42
|
-
* and already kebab-case pass-through.
|
|
43
|
-
* @param {string} ns
|
|
44
|
-
* @returns {string}
|
|
45
|
-
*/
|
|
20
|
+
/** Kebab-case so Handlebars {{pascalCase}} reconstructs it: "MyBookStore" → "my-book-store", "My.App" → "my-app". */
|
|
46
21
|
export function normalizeNamespaceToKebab(ns) {
|
|
47
|
-
// Split on dots first (My.BookStore → ["My", "BookStore"])
|
|
48
|
-
// Then split PascalCase on uppercase boundaries (BookStore → Book-Store)
|
|
49
22
|
return ns
|
|
50
23
|
.split('.')
|
|
51
24
|
.map(segment =>
|
|
@@ -57,15 +30,8 @@ export function normalizeNamespaceToKebab(ns) {
|
|
|
57
30
|
.join('-');
|
|
58
31
|
}
|
|
59
32
|
|
|
60
|
-
//
|
|
61
|
-
// .NET VSA Scaffold
|
|
62
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
// Each entry pairs a REGISTRY.json template `id` with the generator-specific
|
|
65
|
-
// output shaping (class-name suffix, target directory). REGISTRY owns the
|
|
66
|
-
// template path; the generator owns where the rendered file lands.
|
|
33
|
+
// REGISTRY.json owns the template path; the generator owns where the rendered file lands.
|
|
67
34
|
|
|
68
|
-
/** .NET slice-level files (endpoint/handler/request/response/validator). */
|
|
69
35
|
const DOTNET_SLICE_TEMPLATES = [
|
|
70
36
|
{ templateId: 'dotnet-feature-endpoint', suffix: 'Endpoint' },
|
|
71
37
|
{ templateId: 'dotnet-feature-handler', suffix: 'Handler' },
|
|
@@ -74,23 +40,11 @@ const DOTNET_SLICE_TEMPLATES = [
|
|
|
74
40
|
{ templateId: 'dotnet-feature-validator', suffix: 'Validator' },
|
|
75
41
|
];
|
|
76
42
|
|
|
77
|
-
/** .NET feature-level files (entity, errors). */
|
|
78
43
|
const DOTNET_FEATURE_TEMPLATES = [
|
|
79
44
|
{ templateId: 'dotnet-feature-entity', suffix: '', dir: 'entities' },
|
|
80
45
|
{ templateId: 'dotnet-feature-errors', suffix: 'Errors', dir: 'feature' },
|
|
81
46
|
];
|
|
82
47
|
|
|
83
|
-
/**
|
|
84
|
-
* Scaffold a .NET VSA feature slice.
|
|
85
|
-
*
|
|
86
|
-
* @param {Object} options
|
|
87
|
-
* @param {string} options.feature - Entity/feature name in kebab-case (e.g., "book")
|
|
88
|
-
* @param {string} options.sliceName - Slice name in kebab-case (e.g., "create-book")
|
|
89
|
-
* @param {string} options.namespace - Root namespace (e.g., "MyApp")
|
|
90
|
-
* @param {string} [options.outputDir] - Base output directory (default: cwd)
|
|
91
|
-
* @param {boolean} [options.dryRun] - Preview without writing
|
|
92
|
-
* @returns {{ files: Array<{path: string, content: string, skipped: boolean}> }}
|
|
93
|
-
*/
|
|
94
48
|
export function scaffoldDotnetSlice(options) {
|
|
95
49
|
const { feature, sliceName, namespace, outputDir = process.cwd(), dryRun = false } = options;
|
|
96
50
|
|
|
@@ -107,7 +61,6 @@ export function scaffoldDotnetSlice(options) {
|
|
|
107
61
|
|
|
108
62
|
const files = [];
|
|
109
63
|
|
|
110
|
-
// 1. Render slice-level templates
|
|
111
64
|
for (const tmpl of DOTNET_SLICE_TEMPLATES) {
|
|
112
65
|
const templatePath = resolveFrameworkTemplatePath(tmpl.templateId);
|
|
113
66
|
const outputPath = join(
|
|
@@ -128,7 +81,6 @@ export function scaffoldDotnetSlice(options) {
|
|
|
128
81
|
files.push({ path: outputPath, content, skipped: false });
|
|
129
82
|
}
|
|
130
83
|
|
|
131
|
-
// 2. Render feature-level templates (skip if already exist)
|
|
132
84
|
for (const tmpl of DOTNET_FEATURE_TEMPLATES) {
|
|
133
85
|
const templatePath = resolveFrameworkTemplatePath(tmpl.templateId);
|
|
134
86
|
let outputPath;
|
|
@@ -157,12 +109,6 @@ export function scaffoldDotnetSlice(options) {
|
|
|
157
109
|
return { files };
|
|
158
110
|
}
|
|
159
111
|
|
|
160
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
-
// Next.js Scaffold
|
|
162
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
// Each entry pairs a REGISTRY.json template `id` with where the rendered file
|
|
165
|
-
// lands. REGISTRY owns the template path; the generator owns the output path.
|
|
166
112
|
const NEXTJS_TEMPLATES = [
|
|
167
113
|
{
|
|
168
114
|
templateId: 'nextjs-page',
|
|
@@ -182,15 +128,6 @@ const NEXTJS_TEMPLATES = [
|
|
|
182
128
|
},
|
|
183
129
|
];
|
|
184
130
|
|
|
185
|
-
/**
|
|
186
|
-
* Scaffold a Next.js feature page with components and hooks.
|
|
187
|
-
*
|
|
188
|
-
* @param {Object} options
|
|
189
|
-
* @param {string} options.featureName - Feature name in kebab-case (e.g., "book")
|
|
190
|
-
* @param {string} [options.outputDir] - Base output directory (default: cwd)
|
|
191
|
-
* @param {boolean} [options.dryRun] - Preview without writing
|
|
192
|
-
* @returns {{ files: Array<{path: string, content: string, skipped: boolean}> }}
|
|
193
|
-
*/
|
|
194
131
|
export function scaffoldNextjsFeature(options) {
|
|
195
132
|
const { featureName, outputDir = process.cwd(), dryRun = false } = options;
|
|
196
133
|
|
|
@@ -226,33 +163,15 @@ export function scaffoldNextjsFeature(options) {
|
|
|
226
163
|
return { files };
|
|
227
164
|
}
|
|
228
165
|
|
|
229
|
-
|
|
230
|
-
// Namespace Detection
|
|
231
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Detect the .NET root namespace from .csproj or config.json.
|
|
235
|
-
*
|
|
236
|
-
* Search order:
|
|
237
|
-
* 1. *.csproj in dir (extract <RootNamespace>)
|
|
238
|
-
* 2. *.csproj in dir (use filename without extension)
|
|
239
|
-
* 3. *.csproj one level deep
|
|
240
|
-
* 4. .morph/config.json → project.name
|
|
241
|
-
*
|
|
242
|
-
* @param {string} dir - Directory to search in
|
|
243
|
-
* @returns {string|null} Detected namespace or null
|
|
244
|
-
*/
|
|
166
|
+
/** <RootNamespace> or the name of a .csproj here or one level deep, else config.json `project.name`. */
|
|
245
167
|
export function detectNamespace(dir) {
|
|
246
|
-
// 1. Search for .csproj in current dir
|
|
247
168
|
const csprojInDir = findCsproj(dir);
|
|
248
169
|
if (csprojInDir) {
|
|
249
170
|
const ns = extractNamespaceFromCsproj(csprojInDir);
|
|
250
171
|
if (ns) return ns;
|
|
251
|
-
// Fallback: use filename
|
|
252
172
|
return basename(csprojInDir, '.csproj');
|
|
253
173
|
}
|
|
254
174
|
|
|
255
|
-
// 2. Search one level deep
|
|
256
175
|
try {
|
|
257
176
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
258
177
|
for (const entry of entries) {
|
|
@@ -265,29 +184,19 @@ export function detectNamespace(dir) {
|
|
|
265
184
|
}
|
|
266
185
|
}
|
|
267
186
|
}
|
|
268
|
-
} catch {
|
|
269
|
-
// ignore read errors
|
|
270
|
-
}
|
|
187
|
+
} catch {}
|
|
271
188
|
|
|
272
|
-
// 3. Fall back to config.json
|
|
273
189
|
const configPath = join(dir, '.morph', 'config.json');
|
|
274
190
|
if (existsSync(configPath)) {
|
|
275
191
|
try {
|
|
276
192
|
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
277
193
|
if (config.project?.name) return config.project.name;
|
|
278
|
-
} catch {
|
|
279
|
-
// ignore parse errors
|
|
280
|
-
}
|
|
194
|
+
} catch {}
|
|
281
195
|
}
|
|
282
196
|
|
|
283
197
|
return null;
|
|
284
198
|
}
|
|
285
199
|
|
|
286
|
-
/**
|
|
287
|
-
* Find the first .csproj file in a directory.
|
|
288
|
-
* @param {string} dir
|
|
289
|
-
* @returns {string|null}
|
|
290
|
-
*/
|
|
291
200
|
function findCsproj(dir) {
|
|
292
201
|
try {
|
|
293
202
|
const entries = readdirSync(dir);
|
|
@@ -298,11 +207,6 @@ function findCsproj(dir) {
|
|
|
298
207
|
}
|
|
299
208
|
}
|
|
300
209
|
|
|
301
|
-
/**
|
|
302
|
-
* Extract <RootNamespace> from a .csproj file.
|
|
303
|
-
* @param {string} csprojPath
|
|
304
|
-
* @returns {string|null}
|
|
305
|
-
*/
|
|
306
210
|
function extractNamespaceFromCsproj(csprojPath) {
|
|
307
211
|
try {
|
|
308
212
|
const content = readFileSync(csprojPath, 'utf-8');
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Settings Generator
|
|
3
|
-
*
|
|
4
|
-
* Generates .claude/settings.local.json for projects based on detected
|
|
5
|
-
* stack, MCPs, and morph-spec configuration. Uses _morph_managed marker
|
|
6
|
-
* to track managed entries for safe reset.
|
|
7
|
-
*/
|
|
1
|
+
// `_morph_managed` marks the managed entries, so a reset never touches the user's.
|
|
8
2
|
|
|
9
3
|
import { join } from 'path';
|
|
10
4
|
import { existsSync } from 'fs';
|
|
@@ -12,44 +6,22 @@ import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
|
12
6
|
|
|
13
7
|
const MORPH_MANAGED_KEY = '_morph_managed';
|
|
14
8
|
|
|
15
|
-
/**
|
|
16
|
-
* Generate and merge settings for a project
|
|
17
|
-
* @param {string} targetPath - Project root directory
|
|
18
|
-
* @param {Object} detectedConfig - Output from detectClaudeConfig()
|
|
19
|
-
* @param {Object} morphConfig - morph-spec config (from .morph/config/config.json)
|
|
20
|
-
* @returns {Promise<Object>} Generated settings summary
|
|
21
|
-
*/
|
|
22
9
|
export async function generateSettings(targetPath, detectedConfig, morphConfig = {}) {
|
|
23
10
|
const claudeDir = join(targetPath, '.claude');
|
|
24
11
|
const settingsPath = join(claudeDir, 'settings.local.json');
|
|
25
12
|
|
|
26
|
-
|
|
27
|
-
const existing = await readSettingsSafe(settingsPath);
|
|
28
|
-
|
|
29
|
-
// Build stack-based permissions
|
|
30
|
-
const stack = morphConfig?.project?.architecture || 'unknown';
|
|
31
|
-
const permissions = buildPermissions(stack, detectedConfig);
|
|
32
|
-
|
|
33
|
-
// Build generated settings
|
|
13
|
+
const existing = withoutLegacyPermissions(await readSettingsSafe(settingsPath));
|
|
34
14
|
const generated = {};
|
|
35
15
|
|
|
36
|
-
if (permissions.length > 0) {
|
|
37
|
-
generated.permissions = permissions;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Track what we manage
|
|
41
16
|
const managed = {
|
|
42
|
-
permissions_added: permissions,
|
|
43
17
|
mcps_detected: detectedConfig.mcpServers.map(s => s.name),
|
|
44
|
-
plugins_detected: detectedConfig.plugins.map(p => p
|
|
18
|
+
plugins_detected: detectedConfig.plugins.map(p => (typeof p === 'string' ? p : p?.name)),
|
|
45
19
|
generated_at: new Date().toISOString()
|
|
46
20
|
};
|
|
47
21
|
|
|
48
|
-
// Merge without overwriting user config
|
|
49
22
|
const merged = mergeSettings(existing, generated);
|
|
50
23
|
merged[MORPH_MANAGED_KEY] = managed;
|
|
51
24
|
|
|
52
|
-
// Ensure directory exists
|
|
53
25
|
if (!existsSync(claudeDir)) {
|
|
54
26
|
await mkdir(claudeDir, { recursive: true });
|
|
55
27
|
}
|
|
@@ -58,50 +30,11 @@ export async function generateSettings(targetPath, detectedConfig, morphConfig =
|
|
|
58
30
|
|
|
59
31
|
return {
|
|
60
32
|
settingsPath,
|
|
61
|
-
permissionsAdded: permissions.length,
|
|
62
33
|
managed
|
|
63
34
|
};
|
|
64
35
|
}
|
|
65
36
|
|
|
66
|
-
/**
|
|
67
|
-
* Build permission rules based on detected stack
|
|
68
|
-
* @param {string} stack - Detected project stack
|
|
69
|
-
* @param {Object} detectedConfig - Detected Claude config
|
|
70
|
-
* @returns {Array<string>} Permission patterns
|
|
71
|
-
*/
|
|
72
|
-
export function buildPermissions(stack, detectedConfig = {}) {
|
|
73
|
-
const permissions = [];
|
|
74
|
-
|
|
75
|
-
// Stack-based permissions
|
|
76
|
-
const stackPermissions = {
|
|
77
|
-
nodejs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
78
|
-
typescript: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)', 'Bash(tsc:*)'],
|
|
79
|
-
nextjs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
80
|
-
react: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
81
|
-
vue: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
|
|
82
|
-
dotnet: ['Bash(dotnet:*)', 'Bash(nuget:*)'],
|
|
83
|
-
python: ['Bash(python:*)', 'Bash(pip:*)', 'Bash(pytest:*)'],
|
|
84
|
-
go: ['Bash(go:*)']
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
if (stackPermissions[stack]) {
|
|
88
|
-
permissions.push(...stackPermissions[stack]);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// morph-spec CLI always needs node/npx
|
|
92
|
-
if (!permissions.includes('Bash(npx:*)')) {
|
|
93
|
-
permissions.push('Bash(npx:*)');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return permissions;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Deep merge settings without overwriting user entries
|
|
101
|
-
* @param {Object} existing - Current settings.local.json
|
|
102
|
-
* @param {Object} generated - Generated settings to merge
|
|
103
|
-
* @returns {Object} Merged settings
|
|
104
|
-
*/
|
|
37
|
+
/** Never overwrites user entries: arrays are unioned, objects merged, only new keys added. */
|
|
105
38
|
export function mergeSettings(existing, generated) {
|
|
106
39
|
const result = { ...existing };
|
|
107
40
|
|
|
@@ -109,17 +42,14 @@ export function mergeSettings(existing, generated) {
|
|
|
109
42
|
if (key === MORPH_MANAGED_KEY) continue;
|
|
110
43
|
|
|
111
44
|
if (Array.isArray(value) && Array.isArray(result[key])) {
|
|
112
|
-
// Merge arrays without duplicates
|
|
113
45
|
const set = new Set(result[key]);
|
|
114
46
|
for (const item of value) {
|
|
115
47
|
set.add(item);
|
|
116
48
|
}
|
|
117
49
|
result[key] = [...set];
|
|
118
50
|
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
119
|
-
// Recursively merge objects
|
|
120
51
|
result[key] = mergeSettings(result[key] || {}, value);
|
|
121
52
|
} else if (!(key in result)) {
|
|
122
|
-
// Only add new keys, never overwrite existing
|
|
123
53
|
result[key] = value;
|
|
124
54
|
}
|
|
125
55
|
}
|
|
@@ -127,51 +57,6 @@ export function mergeSettings(existing, generated) {
|
|
|
127
57
|
return result;
|
|
128
58
|
}
|
|
129
59
|
|
|
130
|
-
/**
|
|
131
|
-
* Remove only morph-spec managed entries from settings
|
|
132
|
-
* @param {string} settingsPath - Path to settings.local.json
|
|
133
|
-
* @returns {Promise<Object>} Summary of what was removed
|
|
134
|
-
*/
|
|
135
|
-
export async function resetMorphSettings(settingsPath) {
|
|
136
|
-
if (!existsSync(settingsPath)) {
|
|
137
|
-
return { removed: false, reason: 'file not found' };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const settings = await readSettingsSafe(settingsPath);
|
|
141
|
-
const managed = settings[MORPH_MANAGED_KEY];
|
|
142
|
-
|
|
143
|
-
if (!managed) {
|
|
144
|
-
return { removed: false, reason: 'no _morph_managed section found' };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Remove managed permissions
|
|
148
|
-
if (managed.permissions_added && Array.isArray(settings.permissions)) {
|
|
149
|
-
settings.permissions = settings.permissions.filter(
|
|
150
|
-
p => !managed.permissions_added.includes(p)
|
|
151
|
-
);
|
|
152
|
-
if (settings.permissions.length === 0) {
|
|
153
|
-
delete settings.permissions;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Remove the managed marker itself
|
|
158
|
-
delete settings[MORPH_MANAGED_KEY];
|
|
159
|
-
|
|
160
|
-
// Write back
|
|
161
|
-
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
removed: true,
|
|
165
|
-
permissionsRemoved: managed.permissions_added || [],
|
|
166
|
-
previouslyGenerated: managed.generated_at
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Save integrations.json with detected config
|
|
172
|
-
* @param {string} targetPath - Project root directory
|
|
173
|
-
* @param {Object} detectedConfig - Output from detectClaudeConfig()
|
|
174
|
-
*/
|
|
175
60
|
export async function saveIntegrations(targetPath, detectedConfig) {
|
|
176
61
|
const integrationsPath = join(targetPath, '.morph', 'config', 'integrations.json');
|
|
177
62
|
const integrationsDir = join(targetPath, '.morph', 'config');
|
|
@@ -194,9 +79,13 @@ export async function saveIntegrations(targetPath, detectedConfig) {
|
|
|
194
79
|
return integrationsPath;
|
|
195
80
|
}
|
|
196
81
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
82
|
+
// Claude Code reads only `permissions: { allow, deny }`. Old generators wrote wildcard rules as a bare
|
|
83
|
+
// array that never applied; converting it to { allow } would silently enable them.
|
|
84
|
+
function withoutLegacyPermissions(settings) {
|
|
85
|
+
if (!Array.isArray(settings.permissions)) return settings;
|
|
86
|
+
const { permissions, ...rest } = settings;
|
|
87
|
+
return rest;
|
|
88
|
+
}
|
|
200
89
|
|
|
201
90
|
async function readSettingsSafe(settingsPath) {
|
|
202
91
|
try {
|