@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- 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 +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- 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 +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- 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.sh +11 -11
- 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 +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- 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 +1 -42
- 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 +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- 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/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/rules/nextjs-standards.md +1 -1
- 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 +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- 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/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 +2 -2
- 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 +1 -1
- 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/testing.md +10 -4
- 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/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- 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 +17 -104
- 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 +5 -57
- 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.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- 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 +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- 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 +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- 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 +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- 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 +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- 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 +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- 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 +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- 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 +1 -0
- 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 +3 -15
- 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 +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- 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 +2 -24
- 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 +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- 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 +13 -95
- 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 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -5,7 +5,6 @@ import { execFileSync } from 'child_process';
|
|
|
5
5
|
import { ADR_INDEX_PATHSPEC, updateAdrIndex } from '../../lib/specs/adr-index.js';
|
|
6
6
|
import { isValidFeatureName } from '../project/worktree.js';
|
|
7
7
|
|
|
8
|
-
/** Refuse a move whose resolved target escapes `scopeDir`. */
|
|
9
8
|
function assertContained(scopeDir, name, target) {
|
|
10
9
|
const scopeAbs = resolve(scopeDir) + sep;
|
|
11
10
|
const targetAbs = resolve(target);
|
|
@@ -14,21 +13,7 @@ function assertContained(scopeDir, name, target) {
|
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
* `morph-spec archive <feature>` — atomic, git-aware archive of a completed
|
|
19
|
-
* feature. Stamps the feature completed, moves `.morph/features/{name}/` →
|
|
20
|
-
* `.morph/archive/{name}/`, COMMITS the move (scoped to the feature's pathspec),
|
|
21
|
-
* and cleans the thin root index.
|
|
22
|
-
*
|
|
23
|
-
* Two callers:
|
|
24
|
-
* - `archiveCommand` (standalone `morph-spec archive` / `/morph-archive`) —
|
|
25
|
-
* archive-without-integrate, for legacy features and cleanup.
|
|
26
|
-
* - `finishCommand` — runs `runArchive` INSIDE the `morph/{feature}` branch so
|
|
27
|
-
* the committed archived state travels with the merge/PR in one integration.
|
|
28
|
-
*
|
|
29
|
-
* Taxonomy: archived = `status: "completed"` + `archivedAt` (per the project
|
|
30
|
-
* decision — no separate "archived" status value).
|
|
31
|
-
*/
|
|
16
|
+
// Archived = `status: "completed"` + `archivedAt`; there is no separate "archived" status.
|
|
32
17
|
|
|
33
18
|
function isGitRepo(cwd) {
|
|
34
19
|
try {
|
|
@@ -39,7 +24,6 @@ function isGitRepo(cwd) {
|
|
|
39
24
|
}
|
|
40
25
|
}
|
|
41
26
|
|
|
42
|
-
/** Tolerant read of the review gate — mirrors the finish path's shape handling. */
|
|
43
27
|
function readReviewApproved(fjPath) {
|
|
44
28
|
try {
|
|
45
29
|
const fj = JSON.parse(readFileSync(fjPath, 'utf8'));
|
|
@@ -51,30 +35,14 @@ function readReviewApproved(fjPath) {
|
|
|
51
35
|
}
|
|
52
36
|
|
|
53
37
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* Reads/writes everything relative to `cwd` (never a hard-coded process.cwd()),
|
|
57
|
-
* so `finishCommand` can archive inside a worktree's own tree.
|
|
58
|
-
*
|
|
59
|
-
* Idempotent: if `.morph/archive/{feature}` already exists, returns
|
|
60
|
-
* `{ ok: true, alreadyArchived: true }` WITHOUT failing — essential so a re-run
|
|
61
|
-
* of `finish` (which archives inside the branch) does not blow up.
|
|
62
|
-
*
|
|
63
|
-
* @param {string} feature
|
|
64
|
-
* @param {{ cwd?: string, force?: boolean, git?: boolean }} [opts]
|
|
65
|
-
* @returns {{ ok: boolean, error?: string, archived?: boolean, alreadyArchived?: boolean,
|
|
66
|
-
* committed?: boolean, staged?: boolean, archivePath?: string,
|
|
67
|
-
* revocationProblems?: Array<{id: string|null, reason: string, message: string}>,
|
|
68
|
-
* adrIndex?: {added: string[], revoked: string[], narrowed: string[]}|null }}
|
|
38
|
+
* Never calls process.exit, and resolves everything from `cwd` so `finish` can archive inside a
|
|
39
|
+
* worktree. Idempotent: an existing archive returns `{ ok: true, alreadyArchived: true }`.
|
|
69
40
|
*/
|
|
70
41
|
export function runArchive(feature, opts = {}) {
|
|
71
42
|
const cwd = opts.cwd || process.cwd();
|
|
72
43
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// standalone `archiveCommand` has no other gate at all, so without this an
|
|
76
|
-
// unvalidated `feature` reaches `renameSync` and can MOVE a directory outside
|
|
77
|
-
// .morph/features/ (`../../.claude` resolves to `cwd/.claude`).
|
|
44
|
+
// Validated BEFORE any path is built: `../../.claude` would let renameSync move a directory
|
|
45
|
+
// outside .morph/features/.
|
|
78
46
|
if (!isValidFeatureName(feature)) {
|
|
79
47
|
return { ok: false, error: 'invalid-feature-name' };
|
|
80
48
|
}
|
|
@@ -88,9 +56,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
88
56
|
const featPathspec = join('.morph', 'features', feature);
|
|
89
57
|
const archivePathspec = join('.morph', 'archive', feature);
|
|
90
58
|
|
|
91
|
-
//
|
|
92
|
-
// after a partial finish reports "already done", never "not found"; an ADR
|
|
93
|
-
// index update that degraded fail-open is therefore not retried.
|
|
59
|
+
// Checked FIRST so a re-run after a partial finish reports "already done", never "not found".
|
|
94
60
|
if (existsSync(archivePath)) {
|
|
95
61
|
return { ok: true, alreadyArchived: true, archived: false, archivePath };
|
|
96
62
|
}
|
|
@@ -99,8 +65,6 @@ export function runArchive(feature, opts = {}) {
|
|
|
99
65
|
return { ok: false, error: 'feature-folder-not-found' };
|
|
100
66
|
}
|
|
101
67
|
|
|
102
|
-
// Precondition: Gate 3 (review) approved — archiving is for completed work.
|
|
103
|
-
// Read from the feature's own folder (pre-move), tolerant of legacy shapes.
|
|
104
68
|
const fjPathBefore = join(featuresPath, 'feature.json');
|
|
105
69
|
if (!opts.force && !readReviewApproved(fjPathBefore)) {
|
|
106
70
|
return { ok: false, error: 'review-not-approved' };
|
|
@@ -108,9 +72,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
108
72
|
|
|
109
73
|
const useGit = opts.git !== false && isGitRepo(cwd);
|
|
110
74
|
|
|
111
|
-
//
|
|
112
|
-
// fall back to a filesystem rename (+ git add) when the folder isn't
|
|
113
|
-
// tracked yet or git mv fails.
|
|
75
|
+
// `git mv` keeps history and stages in one step; an untracked folder falls back to rename.
|
|
114
76
|
mkdirSync(archiveDir, { recursive: true });
|
|
115
77
|
let staged = false;
|
|
116
78
|
if (useGit) {
|
|
@@ -128,8 +90,6 @@ export function runArchive(feature, opts = {}) {
|
|
|
128
90
|
renameSync(featuresPath, archivePath);
|
|
129
91
|
}
|
|
130
92
|
|
|
131
|
-
// 2) Stamp the authoritative feature.json (now at the archive path) as
|
|
132
|
-
// completed + archived, and re-stage it so the commit carries the stamp.
|
|
133
93
|
const fjPathAfter = join(archivePath, 'feature.json');
|
|
134
94
|
if (existsSync(fjPathAfter)) {
|
|
135
95
|
try {
|
|
@@ -144,9 +104,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
144
104
|
} catch { /* non-fatal — the move is what matters */ }
|
|
145
105
|
}
|
|
146
106
|
|
|
147
|
-
//
|
|
148
|
-
// revokes leave it, in the same scoped commit below. Fail-open: the index
|
|
149
|
-
// must never block archiving.
|
|
107
|
+
// Fail-open: the ADR index must never block archiving.
|
|
150
108
|
let adrIndex = { changed: false, added: [], revoked: [], narrowed: [], problems: [] };
|
|
151
109
|
try {
|
|
152
110
|
adrIndex = updateAdrIndex(cwd, archivePath);
|
|
@@ -155,12 +113,8 @@ export function runArchive(feature, opts = {}) {
|
|
|
155
113
|
try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
156
114
|
}
|
|
157
115
|
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// lets the archived state travel with the branch on `finish` (archive-before-merge).
|
|
161
|
-
// The ADR index joins only when it changed: git refuses a pathspec that
|
|
162
|
-
// matches nothing, and that refusal would leave the tree dirty and break
|
|
163
|
-
// the checkout/merge that follows.
|
|
116
|
+
// Scoped to this feature's pathspec, never `commit -a`. The ADR index joins only when it
|
|
117
|
+
// changed: git refuses a pathspec that matches nothing, leaving the tree dirty for the merge.
|
|
164
118
|
let committed = false;
|
|
165
119
|
if (useGit && staged) {
|
|
166
120
|
const pathspecs = [
|
|
@@ -174,9 +128,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
174
128
|
} catch { /* nothing to commit / no identity — not fatal, the move + stage stand */ }
|
|
175
129
|
}
|
|
176
130
|
|
|
177
|
-
//
|
|
178
|
-
// rather than calling saveState — saveState would re-create
|
|
179
|
-
// .morph/features/{name}/feature.json and undo the archive.
|
|
131
|
+
// Not saveState: it would re-create .morph/features/{name}/feature.json and undo the archive.
|
|
180
132
|
const rootPath = join(cwd, '.morph', 'state.json');
|
|
181
133
|
if (existsSync(rootPath)) {
|
|
182
134
|
try {
|
|
@@ -197,11 +149,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
197
149
|
};
|
|
198
150
|
}
|
|
199
151
|
|
|
200
|
-
|
|
201
|
-
* Standalone `morph-spec archive <feature>` — thin wrapper over `runArchive`
|
|
202
|
-
* that maps its result to the CLI's messages and exit codes. Preserves the
|
|
203
|
-
* historical behaviour: an already-archived feature is reported as a refusal.
|
|
204
|
-
*/
|
|
152
|
+
// Unlike `runArchive`, an already-archived feature is reported as a refusal here.
|
|
205
153
|
export async function archiveCommand(featureName, options = {}) {
|
|
206
154
|
try {
|
|
207
155
|
const result = runArchive(featureName, {
|
|
@@ -2,11 +2,8 @@ import { planAutoApprovalBlock } from './gate-check.js';
|
|
|
2
2
|
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Why an "auto" signature may not stamp `gate`, or null when it may. The
|
|
6
|
-
*
|
|
7
|
-
* `approvalGates` entry, so they must refuse the same inputs.
|
|
8
|
-
*
|
|
9
|
-
* @returns {{ title: string, hints: string[] } | null}
|
|
5
|
+
* Why an "auto" signature may not stamp `gate`, or null when it may. The one rule
|
|
6
|
+
* `approve` and `advance` share: they write the same `approvalGates` entry.
|
|
10
7
|
*/
|
|
11
8
|
export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedBy }) {
|
|
12
9
|
if (approvedBy !== 'auto' || !gate) return null;
|
|
@@ -10,29 +10,9 @@ import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
|
10
10
|
import { writePlanSkeleton } from '../../lib/plan/plan-template.js';
|
|
11
11
|
import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* inherits another feature branch's commits) and checked out in place, so
|
|
17
|
-
* `morph-spec finish` can later merge/PR it back. Symmetric with the worktree
|
|
18
|
-
* flow, which gets its branch from `runWorktreeSetup`.
|
|
19
|
-
*
|
|
20
|
-
* That checkout moves the dev's PRIMARY working tree, and `stdio: 'pipe'`
|
|
21
|
-
* swallows even git's own message — so the move is reported by the caller
|
|
22
|
-
* (`branchMoveWarning`), never silent. `checkout: false` (`--no-checkout`)
|
|
23
|
-
* still CREATES the branch — `finish --pr|--merge` depends on it existing in
|
|
24
|
-
* both flows — and only skips moving the tree.
|
|
25
|
-
*
|
|
26
|
-
* Fail-open: outside a git repo, with no resolvable default, or on any git
|
|
27
|
-
* error, the feature is simply left on the current branch — never a failed birth.
|
|
28
|
-
*
|
|
29
|
-
* @param {string} featureName
|
|
30
|
-
* @param {{ checkout?: boolean }} [opts] - `checkout: false` keeps the tree put
|
|
31
|
-
* @returns {{ branched: boolean, checkedOut: boolean, branch?: string, from?: string,
|
|
32
|
-
* movedFrom?: string, stayedOn?: string, reason?: string }}
|
|
33
|
-
* `movedFrom` is present ONLY when the working tree actually moved (it names
|
|
34
|
-
* the branch left behind); `stayedOn` only when the move was declined.
|
|
35
|
-
*/
|
|
13
|
+
// Branches from the DEFAULT branch, never HEAD, so it never inherits another feature's commits.
|
|
14
|
+
// `checkout: false` still CREATES the branch, which `finish` depends on. Fail-open: any git error
|
|
15
|
+
// leaves the feature on the current branch, never a failed birth.
|
|
36
16
|
function branchCommonFlow(featureName, { checkout = true } = {}) {
|
|
37
17
|
const cwd = process.cwd();
|
|
38
18
|
try {
|
|
@@ -44,7 +24,6 @@ function branchCommonFlow(featureName, { checkout = true } = {}) {
|
|
|
44
24
|
execFileSync('git', ['branch', branch, defaultBranch], { cwd, stdio: 'pipe' });
|
|
45
25
|
}
|
|
46
26
|
const current = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
|
|
47
|
-
// Already there: nothing moved, so nothing to warn about.
|
|
48
27
|
if (current === branch) return { branched: true, checkedOut: true, branch, from: defaultBranch };
|
|
49
28
|
if (!checkout) return { branched: true, checkedOut: false, branch, from: defaultBranch, stayedOn: current };
|
|
50
29
|
execFileSync('git', ['checkout', branch], { cwd, stdio: 'pipe' });
|
|
@@ -54,14 +33,7 @@ function branchCommonFlow(featureName, { checkout = true } = {}) {
|
|
|
54
33
|
}
|
|
55
34
|
}
|
|
56
35
|
|
|
57
|
-
|
|
58
|
-
* The alert for a checkout that MOVED. Built once and rendered on both
|
|
59
|
-
* surfaces — stdout (human) and stderr (under `--json`, so it survives a
|
|
60
|
-
* stdout redirect) — so the two can never drift apart.
|
|
61
|
-
*
|
|
62
|
-
* @param {{ branch: string, movedFrom: string }} branchResult
|
|
63
|
-
* @returns {string[]} lines, unstyled and unindented
|
|
64
|
-
*/
|
|
36
|
+
// One builder for stdout and (under --json) stderr, so the two surfaces never drift.
|
|
65
37
|
function branchMoveWarning({ branch, movedFrom }) {
|
|
66
38
|
return [
|
|
67
39
|
`⚠️ Working tree MOVED: ${movedFrom} → ${branch}`,
|
|
@@ -72,21 +44,12 @@ function branchMoveWarning({ branch, movedFrom }) {
|
|
|
72
44
|
];
|
|
73
45
|
}
|
|
74
46
|
|
|
75
|
-
/** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
|
|
76
47
|
const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
77
48
|
|
|
78
49
|
const VALID_STATUSES = ['draft', 'in_progress', 'completed'];
|
|
79
50
|
const VALID_DETECT_SOURCES = ['deterministic', 'llm', 'user'];
|
|
80
51
|
|
|
81
|
-
|
|
82
|
-
* Resolve the work type for a NEW feature from `--type`/`--request`, or fall
|
|
83
|
-
* back to the default. Never touches the filesystem/state — pure control
|
|
84
|
-
* flow so it can `process.exit` before any side effect happens.
|
|
85
|
-
*
|
|
86
|
-
* @param {{ type?: string, request?: string, detectSource?: string }} options
|
|
87
|
-
* @param {string} now - ISO timestamp, stamped into workTypeDetection.decidedAt
|
|
88
|
-
* @returns {{ workType: string, workTypeDetection?: object }}
|
|
89
|
-
*/
|
|
52
|
+
// Pure control flow, so it can `process.exit` before any side effect happens.
|
|
90
53
|
function resolveWorkType(options, now) {
|
|
91
54
|
if (options.type !== undefined) {
|
|
92
55
|
if (!isValidWorkType(options.type)) {
|
|
@@ -112,10 +75,8 @@ function resolveWorkType(options, now) {
|
|
|
112
75
|
if (options.request !== undefined) {
|
|
113
76
|
const d = detectWorkType({ text: options.request });
|
|
114
77
|
if (d.confidence !== 'high') {
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// feature must NOT be created on this path — always JSON, regardless
|
|
118
|
-
// of --json, since this is the machine-consumable half of the contract.
|
|
78
|
+
// Ambiguity escalates to the caller, which re-runs `create --type`: nothing is created here, and
|
|
79
|
+
// the output is always JSON since this is the machine-consumable half of the contract.
|
|
119
80
|
console.log(JSON.stringify({ needsDecision: true, candidates: d.candidates, matched: d.matched }));
|
|
120
81
|
process.exit(2);
|
|
121
82
|
}
|
|
@@ -133,20 +94,8 @@ function resolveWorkType(options, now) {
|
|
|
133
94
|
return { workType: DEFAULT_WORK_TYPE };
|
|
134
95
|
}
|
|
135
96
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
* work type (`--type`/`--request`), creates `.morph/features/{feature}/`
|
|
139
|
-
* scaffolded per the type's dimensioned pipeline, stamps policy-approved
|
|
140
|
-
* gates (`approvedBy: "policy:{type}"`) for the gates that type skips, and
|
|
141
|
-
* (with `--worktree`) births the feature already inside its own git
|
|
142
|
-
* worktree on branch `morph/{feature}`.
|
|
143
|
-
*
|
|
144
|
-
* Replaces the implicit birth via the state-sync hook on the first Write —
|
|
145
|
-
* the skills call this at plan start so the authoritative state exists
|
|
146
|
-
* with the full canonical shape before any artifact lands. Idempotent: an
|
|
147
|
-
* existing feature is reported and left untouched — no re-stamping, no
|
|
148
|
-
* re-scaffolding, no worktree setup.
|
|
149
|
-
*/
|
|
97
|
+
// Idempotent: an existing feature is reported and left untouched (no re-stamping, no
|
|
98
|
+
// re-scaffolding, no worktree setup).
|
|
150
99
|
export async function createCommand(featureName, options = {}) {
|
|
151
100
|
try {
|
|
152
101
|
if (!FEATURE_NAME_RE.test(featureName)) {
|
|
@@ -161,15 +110,8 @@ export async function createCommand(featureName, options = {}) {
|
|
|
161
110
|
process.exit(1);
|
|
162
111
|
}
|
|
163
112
|
|
|
164
|
-
// `--
|
|
165
|
-
//
|
|
166
|
-
// worktree sequer chama `branchCommonFlow`, que é o único leitor do flag.
|
|
167
|
-
// Aceitar a combinação em silêncio é dizer que o CLI honrou um pedido que
|
|
168
|
-
// ele ignorou — o mesmo anti-padrão que esta série de fixes tirou do
|
|
169
|
-
// `finish --pr --push`. Recusa ALTA, e AQUI: junto das outras validações
|
|
170
|
-
// de forma, ANTES de `createFeature` — a primeira mutação do `create`.
|
|
171
|
-
// Uma recusa depois dela deixaria a feature nascida por um comando que
|
|
172
|
-
// falhou, pior que o no-op que ela substitui.
|
|
113
|
+
// `--worktree` nunca move o checkout principal: aceitar `--no-checkout` junto em silêncio é fingir
|
|
114
|
+
// que o pedido foi honrado. Recusa AQUI, antes de `createFeature`, a primeira mutação do `create`.
|
|
173
115
|
if (options.worktree && options.checkout === false) {
|
|
174
116
|
console.error(chalk.red(`\n❌ no-checkout-with-worktree: --no-checkout não se combina com --worktree`));
|
|
175
117
|
console.error(chalk.gray(` O --worktree já não move o checkout desta árvore: a feature nasce em worktrees/${featureName},`));
|
|
@@ -182,16 +124,10 @@ export async function createCommand(featureName, options = {}) {
|
|
|
182
124
|
const now = new Date().toISOString();
|
|
183
125
|
const { workType, workTypeDetection } = resolveWorkType(options, now);
|
|
184
126
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// `--request` (which the router already consumes) doubles as the
|
|
188
|
-
// description seed. A NEW feature with no description source is refused —
|
|
189
|
-
// the description is the one identity field a human must supply. An
|
|
190
|
-
// idempotent re-run on an EXISTING feature never demands it.
|
|
127
|
+
// A NEW feature with no description source is refused: it is the one identity field a human
|
|
128
|
+
// must supply. An idempotent re-run never demands it.
|
|
191
129
|
const description = options.description ?? options.request ?? null;
|
|
192
|
-
//
|
|
193
|
-
// throws when no state exists yet (a feature can be the first morph action
|
|
194
|
-
// in a fresh project), which would break birth-from-nothing.
|
|
130
|
+
// Filesystem, not loadState: loadState throws when no state exists yet (birth from nothing).
|
|
195
131
|
const alreadyExists = existsSync(join(process.cwd(), '.morph', 'features', featureName, 'feature.json'));
|
|
196
132
|
if (!alreadyExists && (description === null || String(description).trim() === '')) {
|
|
197
133
|
console.error(chalk.red(`\n❌ A feature needs a description at birth: "${featureName}"`));
|
|
@@ -217,14 +153,11 @@ export async function createCommand(featureName, options = {}) {
|
|
|
217
153
|
let gitignoreAdded = [];
|
|
218
154
|
|
|
219
155
|
if (created) {
|
|
220
|
-
// Scaffold the phase folders this work type starts with.
|
|
221
156
|
for (const folder of wt.scaffold) {
|
|
222
157
|
mkdirSync(join(featureDir, folder), { recursive: true });
|
|
223
158
|
}
|
|
224
159
|
|
|
225
|
-
//
|
|
226
|
-
// gate-guard hook only special-cases approvedBy === "auto"; policy:{type}
|
|
227
|
-
// keeps the audit trail honest while staying silent to the guard).
|
|
160
|
+
// NEVER "auto": the gate-guard hook special-cases it; `policy:{type}` keeps the audit honest.
|
|
228
161
|
for (const gate of wt.skippedGates) {
|
|
229
162
|
await setApprovalGate(featureName, gate, true, {
|
|
230
163
|
approvedBy: `${POLICY_APPROVER_PREFIX}${workType}`,
|
|
@@ -236,19 +169,10 @@ export async function createCommand(featureName, options = {}) {
|
|
|
236
169
|
worktreeResult = await runWorktreeSetup(featureName, {});
|
|
237
170
|
if (worktreeResult?.created && worktreeResult.path) {
|
|
238
171
|
try {
|
|
239
|
-
// feature.json
|
|
240
|
-
// wrote it), so the branch `runWorktreeSetup` forked off the
|
|
241
|
-
// DEFAULT branch cannot have it — copy the freshly-scaffolded
|
|
242
|
-
// folder into the worktree and re-materialize its local state.json.
|
|
172
|
+
// The root feature.json is not committed yet, so the worktree branch cannot have it: copy it in.
|
|
243
173
|
cpSync(featureDir, join(worktreeResult.path, '.morph', 'features', featureName), { recursive: true });
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
// Strip it from the worktree's branch-local copy so the branch commits a
|
|
247
|
-
// clean feature.json — otherwise it merges back into the root at
|
|
248
|
-
// `morph-spec finish` and the post-merge cleanup leaves the root dirty.
|
|
249
|
-
// verify.decidePersistence reads the registration from the ROOT's copy,
|
|
250
|
-
// never this branch-local one (see verify.js), so stripping is safe and
|
|
251
|
-
// restores the invariant that comment relies on.
|
|
174
|
+
// `.worktree` is ROOT-OWNED: left in the branch copy it merges back at `finish` and leaves the
|
|
175
|
+
// root dirty. verify reads the registration from the root copy only.
|
|
252
176
|
try {
|
|
253
177
|
const wtFeatureJson = join(worktreeResult.path, '.morph', 'features', featureName, 'feature.json');
|
|
254
178
|
const fj = JSON.parse(readFileSync(wtFeatureJson, 'utf8'));
|
|
@@ -257,25 +181,17 @@ export async function createCommand(featureName, options = {}) {
|
|
|
257
181
|
writeFileSync(wtFeatureJson, JSON.stringify(fj, null, 2), 'utf8');
|
|
258
182
|
}
|
|
259
183
|
} catch { /* fail-open — root copy still carries the registration for verify */ }
|
|
260
|
-
// Re-materialize
|
|
261
|
-
// report THIS result — runWorktreeSetup's earlier materialize ran before
|
|
262
|
-
// the copy (no features yet), so its `materialized:false` was stale.
|
|
184
|
+
// Re-materialize after the copy: the setup's earlier materialize ran with no features yet.
|
|
263
185
|
const remat = await materializeState(worktreeResult.path);
|
|
264
186
|
worktreeResult.materialized = remat.materialized;
|
|
265
|
-
// Same reason the setup path generates one: the new instance's first
|
|
266
|
-
// read. Regenerated here because setup ran before the copy above.
|
|
267
187
|
try {
|
|
268
188
|
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
269
|
-
//
|
|
270
|
-
// stripped from this branch-local copy a few lines up, so the doc
|
|
271
|
-
// must be told explicitly where it actually lives.
|
|
189
|
+
// `worktree` was stripped from this copy above, so pass where it actually lives.
|
|
272
190
|
writeHandoff(featureName, worktreeResult.path, { worktree: { path: worktreeResult.path, branch: worktreeResult.branch } });
|
|
273
191
|
} catch { /* fail-open */ }
|
|
274
192
|
} catch { /* fail-open — the feature already exists in root */ }
|
|
275
193
|
}
|
|
276
194
|
} else {
|
|
277
|
-
// Common flow: still isolate the feature on its own branch so `finish`
|
|
278
|
-
// can merge/PR it back. Fail-open (see branchCommonFlow).
|
|
279
195
|
// Commander's `--no-checkout` sets options.checkout === false.
|
|
280
196
|
branchResult = branchCommonFlow(featureName, { checkout: options.checkout !== false });
|
|
281
197
|
}
|
|
@@ -296,9 +212,7 @@ export async function createCommand(featureName, options = {}) {
|
|
|
296
212
|
gitignoreAdded = seedDotnetBuildIgnores(home, { featureName, configRoot: process.cwd() });
|
|
297
213
|
}
|
|
298
214
|
|
|
299
|
-
// `feature`
|
|
300
|
-
// setApprovalGate/runWorktreeSetup each do their own loadState()/saveState()
|
|
301
|
-
// round-trip on disk, so re-read the authoritative copy for reporting.
|
|
215
|
+
// `feature` predates the gate-stamping and worktree steps, which each round-trip state on disk.
|
|
302
216
|
const finalFeature = created ? (getFeature(featureName) || feature) : feature;
|
|
303
217
|
|
|
304
218
|
if (options.json) {
|
|
@@ -307,8 +221,7 @@ export async function createCommand(featureName, options = {}) {
|
|
|
307
221
|
if (branchResult) out.branch = branchResult;
|
|
308
222
|
if (planSkeleton) out.planSkeleton = planSkeleton;
|
|
309
223
|
if (gitignoreAdded.length) out.gitignoreAdded = gitignoreAdded;
|
|
310
|
-
//
|
|
311
|
-
// to stderr so it survives `... --json > out.json` instead of vanishing.
|
|
224
|
+
// Under --json the move goes to stderr, so it survives `... --json > out.json`.
|
|
312
225
|
if (branchResult?.movedFrom) {
|
|
313
226
|
console.error(chalk.yellow(branchMoveWarning(branchResult).join('\n')));
|
|
314
227
|
}
|
|
@@ -6,33 +6,10 @@ import { isValidFeatureName, cwdIsInsideWorktree, teardownWorktree, resolvePrima
|
|
|
6
6
|
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
7
7
|
import { tearDownFeatureStack } from '../../lib/e2e/stack-teardown.js';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* delete = dead scaffolds and abandoned drafts you clear out.
|
|
14
|
-
*
|
|
15
|
-
* The critical guarantee — mirrored from `archive` — is that the deletion also
|
|
16
|
-
* drops the feature from the thin root index (.morph/state.json). Without that,
|
|
17
|
-
* the next `loadState` resurrects the folder from the stale index entry
|
|
18
|
-
* (turning a delete into a zombie). We edit the index directly rather than
|
|
19
|
-
* calling saveState, which would re-materialize .morph/features/{name}.
|
|
20
|
-
*
|
|
21
|
-
* Refuses when any gate is approved (real work, not a dead shell), unless
|
|
22
|
-
* `--force`.
|
|
23
|
-
*
|
|
24
|
-
* SECURITY: `featureName` is an unsanitized CLI argument that flows straight
|
|
25
|
-
* into `rmSync(..., { recursive: true })` — the same class of bug the worktree
|
|
26
|
-
* teardown guards against, but here the danger is path TRAVERSAL rather than a
|
|
27
|
-
* followed symlink. `join(cwd, '.morph', 'features', '../../.claude')`
|
|
28
|
-
* resolves to `cwd/.claude`, so an unvalidated name can point the hard-delete
|
|
29
|
-
* at the shared `.claude/` install (or anything else relative to cwd). Gated
|
|
30
|
-
* by `isValidFeatureName` (same kebab-slug rule `create` stamps at birth) PLUS
|
|
31
|
-
* a resolved-path containment assert — validated by path, not by the
|
|
32
|
-
* convention that a real feature name never contains `..`.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
/** Refuse a delete/archive whose resolved target escapes `.morph/features/{scope}/{name}`. */
|
|
9
|
+
// `featureName` flows into a recursive rmSync and `../../.claude` would resolve to the shared
|
|
10
|
+
// install: the name is validated AND the resolved path must stay contained. Dropping the root
|
|
11
|
+
// index entry matters too, or the next loadState resurrects the folder.
|
|
12
|
+
|
|
36
13
|
function assertContained(scopeDir, name, target) {
|
|
37
14
|
const scopeAbs = resolve(scopeDir) + sep;
|
|
38
15
|
const targetAbs = resolve(target);
|
|
@@ -57,7 +34,6 @@ function approvedGates(feature) {
|
|
|
57
34
|
.map(([name]) => name);
|
|
58
35
|
}
|
|
59
36
|
|
|
60
|
-
/** Drop the feature from the thin, derivable root index (like archive does). */
|
|
61
37
|
function dropFromIndex(cwd, featureName) {
|
|
62
38
|
const rootPath = join(cwd, '.morph', 'state.json');
|
|
63
39
|
if (!existsSync(rootPath)) return;
|
|
@@ -76,11 +52,7 @@ export async function deleteCommand(featureName, options = {}) {
|
|
|
76
52
|
try {
|
|
77
53
|
const cwd = options.cwd || process.cwd();
|
|
78
54
|
|
|
79
|
-
//
|
|
80
|
-
// traversal attempt (`../../.claude`) never reaches existsSync/rmSync. Same
|
|
81
|
-
// gate `create` stamps at birth and `finish` re-checks; unlike those two,
|
|
82
|
-
// delete/archive take a FRESH argument every invocation, so this cannot rely
|
|
83
|
-
// on "every real feature folder is already a valid slug".
|
|
55
|
+
// Validated BEFORE any path is built: delete takes a fresh argument on every invocation.
|
|
84
56
|
if (!isValidFeatureName(featureName)) {
|
|
85
57
|
console.error(chalk.red(`\n❌ Invalid feature name: "${featureName}"`));
|
|
86
58
|
console.error(chalk.gray('Use kebab-case: lowercase letters, digits and hyphens (e.g. "user-onboarding")\n'));
|
|
@@ -97,14 +69,12 @@ export async function deleteCommand(featureName, options = {}) {
|
|
|
97
69
|
process.exit(1);
|
|
98
70
|
}
|
|
99
71
|
|
|
100
|
-
//
|
|
101
|
-
// mutates state has no place inside a delete).
|
|
72
|
+
// Not loadState: a read that mutates state has no place inside a delete.
|
|
102
73
|
let feature = {};
|
|
103
74
|
if (existsSync(fjPath)) {
|
|
104
75
|
try { feature = JSON.parse(readFileSync(fjPath, 'utf8')); } catch { feature = {}; }
|
|
105
76
|
}
|
|
106
77
|
|
|
107
|
-
// Precondition: no approved gate (real work) unless --force.
|
|
108
78
|
const approved = approvedGates(feature);
|
|
109
79
|
if (approved.length && !options.force) {
|
|
110
80
|
console.error(chalk.red(`\n❌ "${featureName}" has approved gate(s): ${approved.join(', ')}.`));
|
|
@@ -113,14 +83,8 @@ export async function deleteCommand(featureName, options = {}) {
|
|
|
113
83
|
process.exit(1);
|
|
114
84
|
}
|
|
115
85
|
|
|
116
|
-
// A live worktree
|
|
117
|
-
//
|
|
118
|
-
// (no `no-feature` audit hit either: featureCommittedOnBranch stays true).
|
|
119
|
-
// Distinct consent from --force on purpose: --force answers "approved gate
|
|
120
|
-
// present, delete anyway" (a STATE risk); a live worktree is a FILESYSTEM
|
|
121
|
-
// risk (orphan directory, junctions into the primary root) — the same
|
|
122
|
-
// reasoning that keeps `worktree remove`'s PROVE-or-DECLINE separate from
|
|
123
|
-
// gate policy.
|
|
86
|
+
// A live worktree owns the real state; deleting the root registration would orphan it. A distinct
|
|
87
|
+
// consent from --force on purpose: --force answers a state risk, this one is a filesystem risk.
|
|
124
88
|
const owner = resolveFeatureTree(featureName, cwd);
|
|
125
89
|
const liveElsewhere = (owner.source === 'registry' || owner.source === 'pointer') && owner.tree;
|
|
126
90
|
if (liveElsewhere) {
|
|
@@ -141,14 +105,9 @@ export async function deleteCommand(featureName, options = {}) {
|
|
|
141
105
|
process.exit(1);
|
|
142
106
|
}
|
|
143
107
|
|
|
144
|
-
// PROVE-or-DECLINE
|
|
145
|
-
// worktree actually came down. A refusal here must leave EVERYTHING
|
|
146
|
-
// untouched, same contract as `worktree remove`.
|
|
108
|
+
// PROVE-or-DECLINE: a refusal here must leave everything untouched.
|
|
147
109
|
const root = resolvePrimaryRoot(cwd);
|
|
148
|
-
// The
|
|
149
|
-
// comes down BEFORE its worktree: the compose file and the bind mount
|
|
150
|
-
// live there, and a deleted feature has nothing to keep a volume for.
|
|
151
|
-
// Fail-open by contract — a stubborn container never blocks the delete.
|
|
110
|
+
// The stack comes down BEFORE the worktree, where its compose file and bind mount live. Fail-open.
|
|
152
111
|
try {
|
|
153
112
|
tearDownFeatureStack(featureName, { root, worktreePath: owner.tree, volumes: true });
|
|
154
113
|
} catch { /* fail-open */ }
|
|
@@ -166,15 +125,11 @@ export async function deleteCommand(featureName, options = {}) {
|
|
|
166
125
|
console.error(chalk.gray(` ${teardown.declineReason || 'motivo desconhecido'}\n`));
|
|
167
126
|
process.exit(1);
|
|
168
127
|
}
|
|
169
|
-
// Fall through: the feature's own .morph/features/{featureName} copy in
|
|
170
|
-
// `cwd` (the primary root, by construction of `liveElsewhere`) is deleted
|
|
171
|
-
// by the normal flow below, exactly as if there had never been a worktree.
|
|
172
128
|
}
|
|
173
129
|
|
|
174
130
|
const useGit = isGitRepo(cwd) && options.git !== false;
|
|
175
131
|
|
|
176
132
|
if (options.archive) {
|
|
177
|
-
// Soft delete: move to .morph/archive with status "abandoned".
|
|
178
133
|
const archiveDir = join(cwd, '.morph', 'archive');
|
|
179
134
|
const archivePath = join(archiveDir, featureName);
|
|
180
135
|
if (existsSync(archivePath)) {
|
|
@@ -209,7 +164,6 @@ export async function deleteCommand(featureName, options = {}) {
|
|
|
209
164
|
process.exit(0);
|
|
210
165
|
}
|
|
211
166
|
|
|
212
|
-
// Hard delete: remove the folder (git-aware), then drop from index.
|
|
213
167
|
let staged = false;
|
|
214
168
|
if (useGit) {
|
|
215
169
|
try {
|
|
@@ -218,13 +172,7 @@ export async function deleteCommand(featureName, options = {}) {
|
|
|
218
172
|
} catch {
|
|
219
173
|
// Untracked (never committed) — nothing for git rm to act on.
|
|
220
174
|
}
|
|
221
|
-
// `git rm` only
|
|
222
|
-
// with no artifact in it (2-plan/ with nothing written yet) has no
|
|
223
|
-
// tracked blob and survives as an empty directory on disk. This bit
|
|
224
|
-
// real after --with-worktree: teardownWorktree's own registration-clear
|
|
225
|
-
// commits the root's feature.json on its way through, so `git rm`
|
|
226
|
-
// SUCCEEDS on that one file while the sibling empty folders it never
|
|
227
|
-
// tracked are left behind. Unconditional sweep closes both paths at once.
|
|
175
|
+
// `git rm` only removes tracked files: scaffolded empty phase folders survive it.
|
|
228
176
|
if (existsSync(featuresPath)) rmSync(featuresPath, { recursive: true, force: true });
|
|
229
177
|
try { execSync(`git add -A "${featuresPath}"`, { cwd, stdio: 'pipe' }); staged = true; } catch { /* not fatal */ }
|
|
230
178
|
} else {
|