@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
|
@@ -2,12 +2,9 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join, resolve, sep } from 'path';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
|
-
import {
|
|
6
|
-
import { applySpecRevocations } from '../../lib/specs/acervo-lifecycle.js';
|
|
7
|
-
import { ADR_INDEX_PATHSPEC } from '../../lib/specs/adr-index.js';
|
|
5
|
+
import { ADR_INDEX_PATHSPEC, updateAdrIndex } from '../../lib/specs/adr-index.js';
|
|
8
6
|
import { isValidFeatureName } from '../project/worktree.js';
|
|
9
7
|
|
|
10
|
-
/** Refuse a move whose resolved target escapes `scopeDir`. */
|
|
11
8
|
function assertContained(scopeDir, name, target) {
|
|
12
9
|
const scopeAbs = resolve(scopeDir) + sep;
|
|
13
10
|
const targetAbs = resolve(target);
|
|
@@ -16,21 +13,7 @@ function assertContained(scopeDir, name, target) {
|
|
|
16
13
|
}
|
|
17
14
|
}
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
* `morph-spec archive <feature>` — atomic, git-aware archive of a completed
|
|
21
|
-
* feature. Stamps the feature completed, moves `.morph/features/{name}/` →
|
|
22
|
-
* `.morph/archive/{name}/`, COMMITS the move (scoped to the feature's pathspec),
|
|
23
|
-
* and cleans the thin root index.
|
|
24
|
-
*
|
|
25
|
-
* Two callers:
|
|
26
|
-
* - `archiveCommand` (standalone `morph-spec archive` / `/morph-archive`) —
|
|
27
|
-
* archive-without-integrate, for legacy features and cleanup.
|
|
28
|
-
* - `finishCommand` — runs `runArchive` INSIDE the `morph/{feature}` branch so
|
|
29
|
-
* the committed archived state travels with the merge/PR in one integration.
|
|
30
|
-
*
|
|
31
|
-
* Taxonomy: archived = `status: "completed"` + `archivedAt` (per the project
|
|
32
|
-
* decision — no separate "archived" status value).
|
|
33
|
-
*/
|
|
16
|
+
// Archived = `status: "completed"` + `archivedAt`; there is no separate "archived" status.
|
|
34
17
|
|
|
35
18
|
function isGitRepo(cwd) {
|
|
36
19
|
try {
|
|
@@ -41,7 +24,6 @@ function isGitRepo(cwd) {
|
|
|
41
24
|
}
|
|
42
25
|
}
|
|
43
26
|
|
|
44
|
-
/** Tolerant read of the review gate — mirrors the finish path's shape handling. */
|
|
45
27
|
function readReviewApproved(fjPath) {
|
|
46
28
|
try {
|
|
47
29
|
const fj = JSON.parse(readFileSync(fjPath, 'utf8'));
|
|
@@ -53,33 +35,14 @@ function readReviewApproved(fjPath) {
|
|
|
53
35
|
}
|
|
54
36
|
|
|
55
37
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Reads/writes everything relative to `cwd` (never a hard-coded process.cwd()),
|
|
59
|
-
* so `finishCommand` can archive inside a worktree's own tree.
|
|
60
|
-
*
|
|
61
|
-
* Idempotent: if `.morph/archive/{feature}` already exists, returns
|
|
62
|
-
* `{ ok: true, alreadyArchived: true }` WITHOUT failing — essential so a re-run
|
|
63
|
-
* of `finish` (which archives inside the branch) does not blow up.
|
|
64
|
-
*
|
|
65
|
-
* @param {string} feature
|
|
66
|
-
* @param {{ cwd?: string, force?: boolean, git?: boolean }} [opts]
|
|
67
|
-
* @returns {{ ok: boolean, error?: string, archived?: boolean, alreadyArchived?: boolean,
|
|
68
|
-
* committed?: boolean, staged?: boolean, archivePath?: string, promotedUseCases?: string[],
|
|
69
|
-
* refusedUseCases?: Array<{id: string, incoming: string, existing: string, existingFeature: string, message: string}>,
|
|
70
|
-
* keptSuperseded?: Array<{id: string, file: string, supersededBy: string|null, scope?: string}>,
|
|
71
|
-
* supersededUseCases?: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
|
|
72
|
-
* revocationProblems?: Array<{id: string|null, reason: string, message: string}>,
|
|
73
|
-
* 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 }`.
|
|
74
40
|
*/
|
|
75
41
|
export function runArchive(feature, opts = {}) {
|
|
76
42
|
const cwd = opts.cwd || process.cwd();
|
|
77
43
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// standalone `archiveCommand` has no other gate at all, so without this an
|
|
81
|
-
// unvalidated `feature` reaches `renameSync` and can MOVE a directory outside
|
|
82
|
-
// .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/.
|
|
83
46
|
if (!isValidFeatureName(feature)) {
|
|
84
47
|
return { ok: false, error: 'invalid-feature-name' };
|
|
85
48
|
}
|
|
@@ -93,16 +56,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
93
56
|
const featPathspec = join('.morph', 'features', feature);
|
|
94
57
|
const archivePathspec = join('.morph', 'archive', feature);
|
|
95
58
|
|
|
96
|
-
//
|
|
97
|
-
// after a partial finish reports "already done", never "not found".
|
|
98
|
-
//
|
|
99
|
-
// Consequence, deliberate: this short-circuits BEFORE the use-case promotion
|
|
100
|
-
// below, so a promotion that degraded fail-open is NOT retried by re-running
|
|
101
|
-
// `finish`. That is the price of "closing a feature never fails" — an archived
|
|
102
|
-
// feature stays archived, and the only signal is an absent `promotedUseCases`
|
|
103
|
-
// in the output. Recovering means copying the UCs into `.morph/specs/usecases/`
|
|
104
|
-
// by hand; making this retry would mean re-entering the archive path for an
|
|
105
|
-
// already-closed feature, which is a worse trade.
|
|
59
|
+
// Checked FIRST so a re-run after a partial finish reports "already done", never "not found".
|
|
106
60
|
if (existsSync(archivePath)) {
|
|
107
61
|
return { ok: true, alreadyArchived: true, archived: false, archivePath };
|
|
108
62
|
}
|
|
@@ -111,8 +65,6 @@ export function runArchive(feature, opts = {}) {
|
|
|
111
65
|
return { ok: false, error: 'feature-folder-not-found' };
|
|
112
66
|
}
|
|
113
67
|
|
|
114
|
-
// Precondition: Gate 3 (review) approved — archiving is for completed work.
|
|
115
|
-
// Read from the feature's own folder (pre-move), tolerant of legacy shapes.
|
|
116
68
|
const fjPathBefore = join(featuresPath, 'feature.json');
|
|
117
69
|
if (!opts.force && !readReviewApproved(fjPathBefore)) {
|
|
118
70
|
return { ok: false, error: 'review-not-approved' };
|
|
@@ -120,9 +72,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
120
72
|
|
|
121
73
|
const useGit = opts.git !== false && isGitRepo(cwd);
|
|
122
74
|
|
|
123
|
-
//
|
|
124
|
-
// fall back to a filesystem rename (+ git add) when the folder isn't
|
|
125
|
-
// tracked yet or git mv fails.
|
|
75
|
+
// `git mv` keeps history and stages in one step; an untracked folder falls back to rename.
|
|
126
76
|
mkdirSync(archiveDir, { recursive: true });
|
|
127
77
|
let staged = false;
|
|
128
78
|
if (useGit) {
|
|
@@ -140,8 +90,6 @@ export function runArchive(feature, opts = {}) {
|
|
|
140
90
|
renameSync(featuresPath, archivePath);
|
|
141
91
|
}
|
|
142
92
|
|
|
143
|
-
// 2) Stamp the authoritative feature.json (now at the archive path) as
|
|
144
|
-
// completed + archived, and re-stage it so the commit carries the stamp.
|
|
145
93
|
const fjPathAfter = join(archivePath, 'feature.json');
|
|
146
94
|
if (existsSync(fjPathAfter)) {
|
|
147
95
|
try {
|
|
@@ -156,63 +104,23 @@ export function runArchive(feature, opts = {}) {
|
|
|
156
104
|
} catch { /* non-fatal — the move is what matters */ }
|
|
157
105
|
}
|
|
158
106
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
// a single source path, and land inside the SAME scoped commit below — the
|
|
162
|
-
// archive travels with the integration, exactly like the archived state
|
|
163
|
-
// (MORPH.md §2.2). Fail-open: any failure degrades to "promoted nothing",
|
|
164
|
-
// never to a refused close.
|
|
165
|
-
let promotedUseCases = [];
|
|
166
|
-
let refusedUseCases = [];
|
|
167
|
-
let keptSuperseded = [];
|
|
168
|
-
try {
|
|
169
|
-
const { promoted, refused, keptSuperseded: kept } = promoteUseCases(cwd, archivePath);
|
|
170
|
-
promotedUseCases = promoted;
|
|
171
|
-
refusedUseCases = refused;
|
|
172
|
-
keptSuperseded = kept;
|
|
173
|
-
if (promoted.length && useGit && staged) {
|
|
174
|
-
// This staging exists for the NEW files: promoted use cases are untracked,
|
|
175
|
-
// and the scoped `commit -- <pathspec>` below only knows paths git already
|
|
176
|
-
// tracks. The replaced-by-ID deletions are tracked, so that same commit
|
|
177
|
-
// would carry them even without this line (verified by mutating it away) —
|
|
178
|
-
// `-A` is belt-and-braces, but dropping the `git add` entirely would
|
|
179
|
-
// silently promote nothing on a first delivery.
|
|
180
|
-
try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
181
|
-
}
|
|
182
|
-
} catch { /* promotion must never block archiving */ }
|
|
183
|
-
|
|
184
|
-
// 3b) Apply what the feature's spec.md declared it revokes: acervo use cases
|
|
185
|
-
// turn `superseded`, and the ADR index gains/loses files. After the
|
|
186
|
-
// promotion, so a substitute promoted just now is already in place (and
|
|
187
|
-
// one it refused is known not to be); same commit, same fail-open contract.
|
|
188
|
-
let lifecycle = { superseded: [], adrIndex: null, problems: [] };
|
|
107
|
+
// Fail-open: the ADR index must never block archiving.
|
|
108
|
+
let adrIndex = { changed: false, added: [], revoked: [], narrowed: [], problems: [] };
|
|
189
109
|
try {
|
|
190
|
-
|
|
191
|
-
} catch { /*
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (useGit && staged) {
|
|
195
|
-
if (lifecycle.superseded.length) {
|
|
196
|
-
try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
197
|
-
}
|
|
198
|
-
if (adrIndexChanged) {
|
|
199
|
-
try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
200
|
-
}
|
|
110
|
+
adrIndex = updateAdrIndex(cwd, archivePath);
|
|
111
|
+
} catch { /* fail-open */ }
|
|
112
|
+
if (useGit && staged && adrIndex.changed) {
|
|
113
|
+
try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
|
|
201
114
|
}
|
|
202
115
|
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
// lets the archived state travel with the branch on `finish` (archive-before-merge).
|
|
206
|
-
// The acervo pathspecs join only when something under them changed: git
|
|
207
|
-
// refuses a pathspec that matches nothing, and that refusal would leave the
|
|
208
|
-
// tree dirty and break 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.
|
|
209
118
|
let committed = false;
|
|
210
119
|
if (useGit && staged) {
|
|
211
120
|
const pathspecs = [
|
|
212
121
|
featPathspec,
|
|
213
122
|
archivePathspec,
|
|
214
|
-
...(
|
|
215
|
-
...(adrIndexChanged ? [ADR_INDEX_PATHSPEC] : []),
|
|
123
|
+
...(adrIndex.changed ? [ADR_INDEX_PATHSPEC] : []),
|
|
216
124
|
];
|
|
217
125
|
try {
|
|
218
126
|
execFileSync('git', ['commit', '-m', `morph-spec archive: ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
|
|
@@ -220,9 +128,7 @@ export function runArchive(feature, opts = {}) {
|
|
|
220
128
|
} catch { /* nothing to commit / no identity — not fatal, the move + stage stand */ }
|
|
221
129
|
}
|
|
222
130
|
|
|
223
|
-
//
|
|
224
|
-
// rather than calling saveState — saveState would re-create
|
|
225
|
-
// .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.
|
|
226
132
|
const rootPath = join(cwd, '.morph', 'state.json');
|
|
227
133
|
if (existsSync(rootPath)) {
|
|
228
134
|
try {
|
|
@@ -236,21 +142,14 @@ export function runArchive(feature, opts = {}) {
|
|
|
236
142
|
} catch { /* index is derivable — non-fatal */ }
|
|
237
143
|
}
|
|
238
144
|
|
|
239
|
-
const adr = lifecycle.adrIndex;
|
|
240
145
|
return {
|
|
241
|
-
ok: true, archived: true, committed, staged, archivePath,
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
revocationProblems: lifecycle.problems,
|
|
245
|
-
adrIndex: adrIndexChanged ? { added: adr.added, revoked: adr.revoked, narrowed: adr.narrowed } : null,
|
|
146
|
+
ok: true, archived: true, committed, staged, archivePath,
|
|
147
|
+
revocationProblems: adrIndex.problems,
|
|
148
|
+
adrIndex: adrIndex.changed ? { added: adrIndex.added, revoked: adrIndex.revoked, narrowed: adrIndex.narrowed } : null,
|
|
246
149
|
};
|
|
247
150
|
}
|
|
248
151
|
|
|
249
|
-
|
|
250
|
-
* Standalone `morph-spec archive <feature>` — thin wrapper over `runArchive`
|
|
251
|
-
* that maps its result to the CLI's messages and exit codes. Preserves the
|
|
252
|
-
* historical behaviour: an already-archived feature is reported as a refusal.
|
|
253
|
-
*/
|
|
152
|
+
// Unlike `runArchive`, an already-archived feature is reported as a refusal here.
|
|
254
153
|
export async function archiveCommand(featureName, options = {}) {
|
|
255
154
|
try {
|
|
256
155
|
const result = runArchive(featureName, {
|
|
@@ -281,27 +180,12 @@ export async function archiveCommand(featureName, options = {}) {
|
|
|
281
180
|
|
|
282
181
|
console.log(chalk.green(`\n📦 Archived "${featureName}" → .morph/archive/${featureName}/`));
|
|
283
182
|
console.log(chalk.gray(` status: completed, archivedAt stamped in feature.json.`));
|
|
284
|
-
if (result.promotedUseCases?.length) {
|
|
285
|
-
console.log(chalk.gray(` ${result.promotedUseCases.length} use case(s) promoted → .morph/specs/usecases/ (status: implemented).`));
|
|
286
|
-
}
|
|
287
|
-
if (result.refusedUseCases?.length) {
|
|
288
|
-
console.log(chalk.yellow(`\n⚠️ ${result.refusedUseCases.length} use case(s) NOT promoted — numbering collision:`));
|
|
289
|
-
for (const r of result.refusedUseCases) {
|
|
290
|
-
console.log(chalk.yellow(` ${r.message}`));
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
for (const k of result.keptSuperseded ?? []) {
|
|
294
|
-
console.log(chalk.yellow(` ${k.id} continua superseded (superseded_by: ${k.supersededBy ?? '?'}) — a cópia promovida não trazia o status, e o acervo o manteve.`));
|
|
295
|
-
}
|
|
296
|
-
for (const s of result.supersededUseCases ?? []) {
|
|
297
|
-
console.log(chalk.gray(` ${s.id} → status: superseded (superseded_by: ${s.supersededBy}${s.scope ? `; ainda vale em: ${s.scope}` : ''}).`));
|
|
298
|
-
}
|
|
299
183
|
if (result.adrIndex) {
|
|
300
184
|
const { added, revoked, narrowed } = result.adrIndex;
|
|
301
185
|
console.log(chalk.gray(` ADR index (.morph/specs/adrs/): +${added.length} −${revoked.length} ~${narrowed.length}.`));
|
|
302
186
|
}
|
|
303
187
|
if (result.revocationProblems?.length) {
|
|
304
|
-
console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do
|
|
188
|
+
console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do plan.md NÃO aplicada(s):`));
|
|
305
189
|
for (const p of result.revocationProblems) {
|
|
306
190
|
console.log(chalk.yellow(` ${p.message}`));
|
|
307
191
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { planAutoApprovalBlock } from './gate-check.js';
|
|
2
|
+
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedBy }) {
|
|
9
|
+
if (approvedBy !== 'auto' || !gate) return null;
|
|
10
|
+
|
|
11
|
+
if (gate === 'review' && feature.workType === 'hotfix') {
|
|
12
|
+
return {
|
|
13
|
+
title: 'Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.',
|
|
14
|
+
hints: ['Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) via AskUserQuestion.'],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const decision = feature.gateDecisions?.[gate];
|
|
19
|
+
if (decision?.pause === true) {
|
|
20
|
+
return {
|
|
21
|
+
title: `O Gate "${gate}" registrou pause:true — auto-aprovação não se aplica.`,
|
|
22
|
+
hints: [
|
|
23
|
+
`Sinal: ${decision.reason || 'risco detectado'}`,
|
|
24
|
+
'A decisão gravada exigia pausa humana. Aprove com --approver <nome>, num',
|
|
25
|
+
'terminal interativo, ou pela resposta humana via AskUserQuestion (state-sync).',
|
|
26
|
+
'Se o sinal de risco já não vale, regrave a decisão:',
|
|
27
|
+
`morph-spec gate-decision ${featureName} ${gate} --pause false --reason "<motivo>"`,
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (gate === 'plan') {
|
|
33
|
+
const block = planAutoApprovalBlock(cwd, featureName);
|
|
34
|
+
if (block) {
|
|
35
|
+
return {
|
|
36
|
+
title: `O gate "plan" não pode ser auto-aprovado — ${block}.`,
|
|
37
|
+
hints: ['Com plan-check verde, rode de novo; ou aprove com --approver <nome>. O --force não dispensa o checks.json.'],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (gate === 'uiux') {
|
|
43
|
+
const block = uiuxAutoApprovalBlock(cwd, featureName, feature);
|
|
44
|
+
if (block) {
|
|
45
|
+
return {
|
|
46
|
+
title: `O Gate "uiux" não pode ser auto-aprovado — ${block}.`,
|
|
47
|
+
hints: [
|
|
48
|
+
'Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com',
|
|
49
|
+
'--approver <nome>, ou, se a pausa já foi decidida, grave a decisão:',
|
|
50
|
+
`morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"`,
|
|
51
|
+
'O --force não dispensa o checklist.',
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function printRefusal(chalk, refusal) {
|
|
61
|
+
console.error(chalk.red(`\n❌ ${refusal.title}`));
|
|
62
|
+
refusal.hints.forEach((line, i) => {
|
|
63
|
+
console.error(chalk.gray(` ${line}${i === refusal.hints.length - 1 ? '\n' : ''}`));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -7,30 +7,12 @@ import { WORK_TYPES, DEFAULT_WORK_TYPE, POLICY_APPROVER_PREFIX, isValidWorkType,
|
|
|
7
7
|
import { detectWorkType } from '../../lib/work-type-detector.js';
|
|
8
8
|
import { runWorktreeSetup, materializeState, isGitRepo, branchExists, buildBranchName } from '../project/worktree.js';
|
|
9
9
|
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
10
|
+
import { writePlanSkeleton } from '../../lib/plan/plan-template.js';
|
|
11
|
+
import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* inherits another feature branch's commits) and checked out in place, so
|
|
15
|
-
* `morph-spec finish` can later merge/PR it back. Symmetric with the worktree
|
|
16
|
-
* flow, which gets its branch from `runWorktreeSetup`.
|
|
17
|
-
*
|
|
18
|
-
* That checkout moves the dev's PRIMARY working tree, and `stdio: 'pipe'`
|
|
19
|
-
* swallows even git's own message — so the move is reported by the caller
|
|
20
|
-
* (`branchMoveWarning`), never silent. `checkout: false` (`--no-checkout`)
|
|
21
|
-
* still CREATES the branch — `finish --pr|--merge` depends on it existing in
|
|
22
|
-
* both flows — and only skips moving the tree.
|
|
23
|
-
*
|
|
24
|
-
* Fail-open: outside a git repo, with no resolvable default, or on any git
|
|
25
|
-
* error, the feature is simply left on the current branch — never a failed birth.
|
|
26
|
-
*
|
|
27
|
-
* @param {string} featureName
|
|
28
|
-
* @param {{ checkout?: boolean }} [opts] - `checkout: false` keeps the tree put
|
|
29
|
-
* @returns {{ branched: boolean, checkedOut: boolean, branch?: string, from?: string,
|
|
30
|
-
* movedFrom?: string, stayedOn?: string, reason?: string }}
|
|
31
|
-
* `movedFrom` is present ONLY when the working tree actually moved (it names
|
|
32
|
-
* the branch left behind); `stayedOn` only when the move was declined.
|
|
33
|
-
*/
|
|
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.
|
|
34
16
|
function branchCommonFlow(featureName, { checkout = true } = {}) {
|
|
35
17
|
const cwd = process.cwd();
|
|
36
18
|
try {
|
|
@@ -42,7 +24,6 @@ function branchCommonFlow(featureName, { checkout = true } = {}) {
|
|
|
42
24
|
execFileSync('git', ['branch', branch, defaultBranch], { cwd, stdio: 'pipe' });
|
|
43
25
|
}
|
|
44
26
|
const current = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
|
|
45
|
-
// Already there: nothing moved, so nothing to warn about.
|
|
46
27
|
if (current === branch) return { branched: true, checkedOut: true, branch, from: defaultBranch };
|
|
47
28
|
if (!checkout) return { branched: true, checkedOut: false, branch, from: defaultBranch, stayedOn: current };
|
|
48
29
|
execFileSync('git', ['checkout', branch], { cwd, stdio: 'pipe' });
|
|
@@ -52,14 +33,7 @@ function branchCommonFlow(featureName, { checkout = true } = {}) {
|
|
|
52
33
|
}
|
|
53
34
|
}
|
|
54
35
|
|
|
55
|
-
|
|
56
|
-
* The alert for a checkout that MOVED. Built once and rendered on both
|
|
57
|
-
* surfaces — stdout (human) and stderr (under `--json`, so it survives a
|
|
58
|
-
* stdout redirect) — so the two can never drift apart.
|
|
59
|
-
*
|
|
60
|
-
* @param {{ branch: string, movedFrom: string }} branchResult
|
|
61
|
-
* @returns {string[]} lines, unstyled and unindented
|
|
62
|
-
*/
|
|
36
|
+
// One builder for stdout and (under --json) stderr, so the two surfaces never drift.
|
|
63
37
|
function branchMoveWarning({ branch, movedFrom }) {
|
|
64
38
|
return [
|
|
65
39
|
`⚠️ Working tree MOVED: ${movedFrom} → ${branch}`,
|
|
@@ -70,21 +44,12 @@ function branchMoveWarning({ branch, movedFrom }) {
|
|
|
70
44
|
];
|
|
71
45
|
}
|
|
72
46
|
|
|
73
|
-
/** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
|
|
74
47
|
const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
75
48
|
|
|
76
49
|
const VALID_STATUSES = ['draft', 'in_progress', 'completed'];
|
|
77
50
|
const VALID_DETECT_SOURCES = ['deterministic', 'llm', 'user'];
|
|
78
51
|
|
|
79
|
-
|
|
80
|
-
* Resolve the work type for a NEW feature from `--type`/`--request`, or fall
|
|
81
|
-
* back to the default. Never touches the filesystem/state — pure control
|
|
82
|
-
* flow so it can `process.exit` before any side effect happens.
|
|
83
|
-
*
|
|
84
|
-
* @param {{ type?: string, request?: string, detectSource?: string }} options
|
|
85
|
-
* @param {string} now - ISO timestamp, stamped into workTypeDetection.decidedAt
|
|
86
|
-
* @returns {{ workType: string, workTypeDetection?: object }}
|
|
87
|
-
*/
|
|
52
|
+
// Pure control flow, so it can `process.exit` before any side effect happens.
|
|
88
53
|
function resolveWorkType(options, now) {
|
|
89
54
|
if (options.type !== undefined) {
|
|
90
55
|
if (!isValidWorkType(options.type)) {
|
|
@@ -110,10 +75,8 @@ function resolveWorkType(options, now) {
|
|
|
110
75
|
if (options.request !== undefined) {
|
|
111
76
|
const d = detectWorkType({ text: options.request });
|
|
112
77
|
if (d.confidence !== 'high') {
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
// feature must NOT be created on this path — always JSON, regardless
|
|
116
|
-
// 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.
|
|
117
80
|
console.log(JSON.stringify({ needsDecision: true, candidates: d.candidates, matched: d.matched }));
|
|
118
81
|
process.exit(2);
|
|
119
82
|
}
|
|
@@ -131,20 +94,8 @@ function resolveWorkType(options, now) {
|
|
|
131
94
|
return { workType: DEFAULT_WORK_TYPE };
|
|
132
95
|
}
|
|
133
96
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
* work type (`--type`/`--request`), creates `.morph/features/{feature}/`
|
|
137
|
-
* scaffolded per the type's dimensioned pipeline, stamps policy-approved
|
|
138
|
-
* gates (`approvedBy: "policy:{type}"`) for the gates that type skips, and
|
|
139
|
-
* (with `--worktree`) births the feature already inside its own git
|
|
140
|
-
* worktree on branch `morph/{feature}`.
|
|
141
|
-
*
|
|
142
|
-
* Replaces the implicit birth via the state-sync hook on the first Write —
|
|
143
|
-
* the skills call this at proposal start so the authoritative state exists
|
|
144
|
-
* with the full canonical shape before any artifact lands. Idempotent: an
|
|
145
|
-
* existing feature is reported and left untouched — no re-stamping, no
|
|
146
|
-
* re-scaffolding, no worktree setup.
|
|
147
|
-
*/
|
|
97
|
+
// Idempotent: an existing feature is reported and left untouched (no re-stamping, no
|
|
98
|
+
// re-scaffolding, no worktree setup).
|
|
148
99
|
export async function createCommand(featureName, options = {}) {
|
|
149
100
|
try {
|
|
150
101
|
if (!FEATURE_NAME_RE.test(featureName)) {
|
|
@@ -159,15 +110,8 @@ export async function createCommand(featureName, options = {}) {
|
|
|
159
110
|
process.exit(1);
|
|
160
111
|
}
|
|
161
112
|
|
|
162
|
-
// `--
|
|
163
|
-
//
|
|
164
|
-
// worktree sequer chama `branchCommonFlow`, que é o único leitor do flag.
|
|
165
|
-
// Aceitar a combinação em silêncio é dizer que o CLI honrou um pedido que
|
|
166
|
-
// ele ignorou — o mesmo anti-padrão que esta série de fixes tirou do
|
|
167
|
-
// `finish --pr --push`. Recusa ALTA, e AQUI: junto das outras validações
|
|
168
|
-
// de forma, ANTES de `createFeature` — a primeira mutação do `create`.
|
|
169
|
-
// Uma recusa depois dela deixaria a feature nascida por um comando que
|
|
170
|
-
// 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`.
|
|
171
115
|
if (options.worktree && options.checkout === false) {
|
|
172
116
|
console.error(chalk.red(`\n❌ no-checkout-with-worktree: --no-checkout não se combina com --worktree`));
|
|
173
117
|
console.error(chalk.gray(` O --worktree já não move o checkout desta árvore: a feature nasce em worktrees/${featureName},`));
|
|
@@ -180,16 +124,10 @@ export async function createCommand(featureName, options = {}) {
|
|
|
180
124
|
const now = new Date().toISOString();
|
|
181
125
|
const { workType, workTypeDetection } = resolveWorkType(options, now);
|
|
182
126
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// `--request` (which the router already consumes) doubles as the
|
|
186
|
-
// description seed. A NEW feature with no description source is refused —
|
|
187
|
-
// the description is the one identity field a human must supply. An
|
|
188
|
-
// 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.
|
|
189
129
|
const description = options.description ?? options.request ?? null;
|
|
190
|
-
//
|
|
191
|
-
// throws when no state exists yet (a feature can be the first morph action
|
|
192
|
-
// in a fresh project), which would break birth-from-nothing.
|
|
130
|
+
// Filesystem, not loadState: loadState throws when no state exists yet (birth from nothing).
|
|
193
131
|
const alreadyExists = existsSync(join(process.cwd(), '.morph', 'features', featureName, 'feature.json'));
|
|
194
132
|
if (!alreadyExists && (description === null || String(description).trim() === '')) {
|
|
195
133
|
console.error(chalk.red(`\n❌ A feature needs a description at birth: "${featureName}"`));
|
|
@@ -211,16 +149,15 @@ export async function createCommand(featureName, options = {}) {
|
|
|
211
149
|
|
|
212
150
|
let worktreeResult = null;
|
|
213
151
|
let branchResult = null;
|
|
152
|
+
let planSkeleton = null;
|
|
153
|
+
let gitignoreAdded = [];
|
|
214
154
|
|
|
215
155
|
if (created) {
|
|
216
|
-
// Scaffold the phase folders this work type starts with.
|
|
217
156
|
for (const folder of wt.scaffold) {
|
|
218
157
|
mkdirSync(join(featureDir, folder), { recursive: true });
|
|
219
158
|
}
|
|
220
159
|
|
|
221
|
-
//
|
|
222
|
-
// gate-guard hook only special-cases approvedBy === "auto"; policy:{type}
|
|
223
|
-
// 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.
|
|
224
161
|
for (const gate of wt.skippedGates) {
|
|
225
162
|
await setApprovalGate(featureName, gate, true, {
|
|
226
163
|
approvedBy: `${POLICY_APPROVER_PREFIX}${workType}`,
|
|
@@ -232,19 +169,10 @@ export async function createCommand(featureName, options = {}) {
|
|
|
232
169
|
worktreeResult = await runWorktreeSetup(featureName, {});
|
|
233
170
|
if (worktreeResult?.created && worktreeResult.path) {
|
|
234
171
|
try {
|
|
235
|
-
// feature.json
|
|
236
|
-
// wrote it), so the branch `runWorktreeSetup` forked off the
|
|
237
|
-
// DEFAULT branch cannot have it — copy the freshly-scaffolded
|
|
238
|
-
// 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.
|
|
239
173
|
cpSync(featureDir, join(worktreeResult.path, '.morph', 'features', featureName), { recursive: true });
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// Strip it from the worktree's branch-local copy so the branch commits a
|
|
243
|
-
// clean feature.json — otherwise it merges back into the root at
|
|
244
|
-
// `morph-spec finish` and the post-merge cleanup leaves the root dirty.
|
|
245
|
-
// verify.decidePersistence reads the registration from the ROOT's copy,
|
|
246
|
-
// never this branch-local one (see verify.js), so stripping is safe and
|
|
247
|
-
// 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.
|
|
248
176
|
try {
|
|
249
177
|
const wtFeatureJson = join(worktreeResult.path, '.morph', 'features', featureName, 'feature.json');
|
|
250
178
|
const fj = JSON.parse(readFileSync(wtFeatureJson, 'utf8'));
|
|
@@ -253,41 +181,47 @@ export async function createCommand(featureName, options = {}) {
|
|
|
253
181
|
writeFileSync(wtFeatureJson, JSON.stringify(fj, null, 2), 'utf8');
|
|
254
182
|
}
|
|
255
183
|
} catch { /* fail-open — root copy still carries the registration for verify */ }
|
|
256
|
-
// Re-materialize
|
|
257
|
-
// report THIS result — runWorktreeSetup's earlier materialize ran before
|
|
258
|
-
// 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.
|
|
259
185
|
const remat = await materializeState(worktreeResult.path);
|
|
260
186
|
worktreeResult.materialized = remat.materialized;
|
|
261
|
-
// Same reason the setup path generates one: the new instance's first
|
|
262
|
-
// read. Regenerated here because setup ran before the copy above.
|
|
263
187
|
try {
|
|
264
188
|
const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
|
|
265
|
-
//
|
|
266
|
-
// stripped from this branch-local copy a few lines up, so the doc
|
|
267
|
-
// must be told explicitly where it actually lives.
|
|
189
|
+
// `worktree` was stripped from this copy above, so pass where it actually lives.
|
|
268
190
|
writeHandoff(featureName, worktreeResult.path, { worktree: { path: worktreeResult.path, branch: worktreeResult.branch } });
|
|
269
191
|
} catch { /* fail-open */ }
|
|
270
192
|
} catch { /* fail-open — the feature already exists in root */ }
|
|
271
193
|
}
|
|
272
194
|
} else {
|
|
273
|
-
// Common flow: still isolate the feature on its own branch so `finish`
|
|
274
|
-
// can merge/PR it back. Fail-open (see branchCommonFlow).
|
|
275
195
|
// Commander's `--no-checkout` sets options.checkout === false.
|
|
276
196
|
branchResult = branchCommonFlow(featureName, { checkout: options.checkout !== false });
|
|
277
197
|
}
|
|
198
|
+
|
|
199
|
+
// protect-spec-files freezes an existing file once its approval is stamped,
|
|
200
|
+
// so a type born pre-approved must create it itself. With a worktree it goes
|
|
201
|
+
// only there: an extra file in the root copy makes `finish` refuse a dirty root.
|
|
202
|
+
if (wt.scaffold.includes('2-plan') && !wt.skippedGates.includes('plan')) {
|
|
203
|
+
const home = worktreeResult?.created && worktreeResult.path ? worktreeResult.path : process.cwd();
|
|
204
|
+
try {
|
|
205
|
+
planSkeleton = writePlanSkeleton(featureName, home);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
planSkeleton = { written: false, error: err.message };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const home = worktreeResult?.created && worktreeResult.path ? worktreeResult.path : process.cwd();
|
|
212
|
+
gitignoreAdded = seedDotnetBuildIgnores(home, { featureName, configRoot: process.cwd() });
|
|
278
213
|
}
|
|
279
214
|
|
|
280
|
-
// `feature`
|
|
281
|
-
// setApprovalGate/runWorktreeSetup each do their own loadState()/saveState()
|
|
282
|
-
// 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.
|
|
283
216
|
const finalFeature = created ? (getFeature(featureName) || feature) : feature;
|
|
284
217
|
|
|
285
218
|
if (options.json) {
|
|
286
219
|
const out = { feature: featureName, created, ...finalFeature };
|
|
287
220
|
if (worktreeResult) out.worktree = worktreeResult;
|
|
288
221
|
if (branchResult) out.branch = branchResult;
|
|
289
|
-
|
|
290
|
-
|
|
222
|
+
if (planSkeleton) out.planSkeleton = planSkeleton;
|
|
223
|
+
if (gitignoreAdded.length) out.gitignoreAdded = gitignoreAdded;
|
|
224
|
+
// Under --json the move goes to stderr, so it survives `... --json > out.json`.
|
|
291
225
|
if (branchResult?.movedFrom) {
|
|
292
226
|
console.error(chalk.yellow(branchMoveWarning(branchResult).join('\n')));
|
|
293
227
|
}
|
|
@@ -300,6 +234,12 @@ export async function createCommand(featureName, options = {}) {
|
|
|
300
234
|
console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape)`));
|
|
301
235
|
console.log(chalk.gray(` status: ${finalFeature.status} | createdAt: ${finalFeature.createdAt}`));
|
|
302
236
|
console.log(chalk.gray(` scaffolded: ${wt.scaffold.join(', ')}`));
|
|
237
|
+
if (gitignoreAdded.length) {
|
|
238
|
+
console.log(chalk.gray(` .gitignore: ${gitignoreAdded.join(' ')} adicionados (projeto .NET)`));
|
|
239
|
+
}
|
|
240
|
+
if (planSkeleton?.written) {
|
|
241
|
+
console.log(chalk.gray(` 2-plan/plan.md: esqueleto escrito (gramática: morph-spec plan-check --help)`));
|
|
242
|
+
}
|
|
303
243
|
if (wt.skippedGates.length) {
|
|
304
244
|
console.log(chalk.yellow(` gates skipped by policy:${finalFeature.workType}: ${wt.skippedGates.join(', ')}`));
|
|
305
245
|
}
|