@polymorphism-tech/morph-spec 8.37.1 → 8.40.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 +37 -13
- package/README.md +2 -2
- package/bin/morph-spec.js +75 -49
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +6 -5
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +96 -36
- package/framework/docs/CLI.md +54 -17
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/mcp/registry.json +33 -2
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +10 -4
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -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 +40 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +44 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +73 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +39 -13
- package/framework/standards/STANDARDS.json +189 -14
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +100 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/feature/decisions.md +124 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +85 -15
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +30 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +63 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +4 -1
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +47 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +200 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +31 -22
- package/src/utils/agents-installer.js +338 -24
- package/src/utils/banner.js +1 -1
- package/src/utils/hooks-installer.js +75 -15
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
getInstalledCLIVersion
|
|
17
17
|
} from '../../utils/version-checker.js';
|
|
18
18
|
import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
|
|
19
|
-
import { checkMissingAgents, checkAgentsDrift } from '../../utils/agents-installer.js';
|
|
19
|
+
import { checkMissingAgents, checkAgentsDrift, maxTurnsError, surfacesError } from '../../utils/agents-installer.js';
|
|
20
20
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
21
21
|
import { auditGraph } from '../../lib/graph/graph-health.js';
|
|
22
22
|
import { auditFeatureLifecycle } from '../../core/state/lifecycle-audit.js';
|
|
@@ -44,10 +44,10 @@ function normCwd(root, cwd) {
|
|
|
44
44
|
/**
|
|
45
45
|
* Check which required Claude Code plugins are installed.
|
|
46
46
|
* @param {Object} pluginsData - The `plugins` object from installed_plugins.json
|
|
47
|
-
* @returns {{
|
|
47
|
+
* @returns {{ context7: boolean }}
|
|
48
48
|
*/
|
|
49
49
|
export function checkInstalledPlugins(pluginsData) {
|
|
50
|
-
const REQUIRED = ['
|
|
50
|
+
const REQUIRED = ['context7'];
|
|
51
51
|
return Object.fromEntries(
|
|
52
52
|
REQUIRED.map(name => [name, `${name}@claude-plugins-official` in (pluginsData || {})])
|
|
53
53
|
);
|
|
@@ -191,6 +191,23 @@ export function validateAgentsJson(agentsData) {
|
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// 4: maxTurns (ADR-004) — the installer's own rule, so this cannot pass a
|
|
195
|
+
// value the installer refuses. ERROR, not warn: that persona is not generated.
|
|
196
|
+
const maxTurnsErrors = agentEntries
|
|
197
|
+
.map(([id, data]) => maxTurnsError(id, data?.maxTurns))
|
|
198
|
+
.filter(Boolean);
|
|
199
|
+
checks.push(maxTurnsErrors.length > 0
|
|
200
|
+
? { name: 'agents.json maxTurns', status: 'error', msg: maxTurnsErrors.join(' | ') }
|
|
201
|
+
: { name: 'agents.json maxTurns', status: 'ok' });
|
|
202
|
+
|
|
203
|
+
// 5: surfaces — same installer rule, same severity: the persona is not generated.
|
|
204
|
+
const surfacesErrors = agentEntries
|
|
205
|
+
.map(([id, data]) => surfacesError(id, data?.surfaces, data?.standards))
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
checks.push(surfacesErrors.length > 0
|
|
208
|
+
? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
|
|
209
|
+
: { name: 'agents.json surfaces', status: 'ok' });
|
|
210
|
+
|
|
194
211
|
if (missingV5Fields.length > 0 && isV5) {
|
|
195
212
|
const sample = missingV5Fields.slice(0, 3).join(', ');
|
|
196
213
|
checks.push({
|
|
@@ -348,12 +365,23 @@ async function doctorFullCommand(frameworkRoot) {
|
|
|
348
365
|
projectTags = getProjectTags(JSON.parse(rawCfg));
|
|
349
366
|
} catch { /* no tags */ }
|
|
350
367
|
}
|
|
351
|
-
|
|
368
|
+
// The MCP registry comes from the package, like the installer's: the
|
|
369
|
+
// installed .morph/framework/ has no mcp/, and rendering without it
|
|
370
|
+
// flagged every persona with an `mcp` grant as stale on a clean install.
|
|
371
|
+
const drift = checkAgentsDrift(frameworkRoot, join(frameworkRoot, '.morph/framework'), projectTags,
|
|
372
|
+
{ mcpRegistryDir: join(import.meta.dirname, '..', '..', '..', 'framework') });
|
|
373
|
+
if (drift.invalid.length > 0) {
|
|
374
|
+
check('Personas agents.json valid', false, false,
|
|
375
|
+
`${drift.invalid.map(i => i.reason).join(' | ')} — corrija o agents.json e rode 'morph-spec update'`);
|
|
376
|
+
}
|
|
352
377
|
check(`Personas installed (${drift.expected - drift.missing.length}/${drift.expected})`,
|
|
353
378
|
drift.missing.length === 0, false,
|
|
354
379
|
drift.missing.length > 0 ? `missing: ${drift.missing.join(', ')} — run 'morph-spec setup-infra --reinstall'` : '');
|
|
380
|
+
// Name WHAT differs per file (frontmatter key or body): a hand-edited
|
|
381
|
+
// maxTurns and a CLI upgrade that changed the body read differently.
|
|
382
|
+
const staleList = drift.stale.map(f => `${f} (${(drift.staleFields[f] ?? []).join(', ')})`);
|
|
355
383
|
check('Personas up-to-date (no drift)', drift.stale.length === 0, true,
|
|
356
|
-
drift.stale.length > 0 ? `stale/hand-edited: ${
|
|
384
|
+
drift.stale.length > 0 ? `stale/hand-edited: ${staleList.join(', ')} — regenerate via 'morph-spec setup-infra --reinstall'` : '');
|
|
357
385
|
check('No orphan morph-* agents', drift.orphans.length === 0, true,
|
|
358
386
|
drift.orphans.length > 0 ? `orphans: ${drift.orphans.join(', ')}` : '');
|
|
359
387
|
} catch {
|
|
@@ -653,23 +681,54 @@ async function doctorMcpCommand(targetPath) {
|
|
|
653
681
|
* Deleting a directory that still HOLDS something is reported with the exact
|
|
654
682
|
* command, never performed — same PROVE-or-DECLINE discipline as the finish
|
|
655
683
|
* teardown.
|
|
684
|
+
*
|
|
685
|
+
* `--prune` also applies the retention of the finish backups — the same policy
|
|
686
|
+
* every `finish` applies on its own, and the ONLY path that also applies it to
|
|
687
|
+
* the backups written before the retention existed (no manifest), counting
|
|
688
|
+
* them against the budget. Under `--json` its result rides in the JSON.
|
|
656
689
|
*/
|
|
657
690
|
export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
658
691
|
const { auditWorktrees, pruneWorktrees } = await import('../../lib/worktree/worktree-audit.js');
|
|
659
692
|
|
|
693
|
+
let backupRetention = null;
|
|
694
|
+
let prune = null;
|
|
660
695
|
if (options.prune) {
|
|
661
696
|
const beforeIssues = auditWorktrees(targetPath);
|
|
662
697
|
const beforeMissing = beforeIssues.filter((i) => i.type === 'registered-missing').length;
|
|
663
698
|
const beforeDangling = beforeIssues.filter((i) => i.type === 'dangling-registration').length;
|
|
664
699
|
const beforeEmptyDirs = beforeIssues.filter((i) => i.type === 'unregistered-dir' && i.empty).length;
|
|
665
700
|
const result = await pruneWorktrees(targetPath);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
701
|
+
// Under --json the only thing on stdout is the JSON: a status line in front
|
|
702
|
+
// of it breaks every caller that parses the output.
|
|
703
|
+
prune = {
|
|
704
|
+
pruned: Boolean(result.pruned),
|
|
705
|
+
...(result.pruned ? { orphanRegistrations: beforeMissing } : { error: result.error }),
|
|
706
|
+
removedEmptyDirs: result.removedEmptyDirs,
|
|
707
|
+
clearedRegistrations: result.clearedRegistrations.map((r) => r.feature),
|
|
708
|
+
};
|
|
709
|
+
if (!options.json) {
|
|
710
|
+
if (result.pruned) logger.success(`git worktree prune: ${beforeMissing} registro(s) órfão(s) removido(s).`);
|
|
711
|
+
else logger.warn(`prune falhou: ${result.error}`);
|
|
712
|
+
if (result.removedEmptyDirs.length) {
|
|
713
|
+
logger.success(`${result.removedEmptyDirs.length}/${beforeEmptyDirs} diretório(s) vazio(s) removido(s): ${result.removedEmptyDirs.join(', ')}`);
|
|
714
|
+
}
|
|
715
|
+
if (result.clearedRegistrations.length) {
|
|
716
|
+
logger.success(`registro de worktree limpo em ${result.clearedRegistrations.length}/${beforeDangling} feature(s): ${result.clearedRegistrations.map((r) => r.feature).join(', ')}`);
|
|
717
|
+
}
|
|
670
718
|
}
|
|
671
|
-
|
|
672
|
-
|
|
719
|
+
|
|
720
|
+
const { resolvePrimaryRoot } = await import('./worktree.js');
|
|
721
|
+
const { applyBackupRetention, summarizeRetention } = await import('../../lib/worktree/finish-backup.js');
|
|
722
|
+
// The explicit prune is the one that reaches the backups written before the
|
|
723
|
+
// retention existed (no manifest): the automatic one never touches them.
|
|
724
|
+
backupRetention = summarizeRetention(applyBackupRetention(resolvePrimaryRoot(targetPath), { includeLegacy: true }));
|
|
725
|
+
if (backupRetention?.skipped && !options.json) {
|
|
726
|
+
logger.warn(`retenção dos backups do finish PULADA: ${backupRetention.skipped}`);
|
|
727
|
+
} else if (backupRetention && !options.json) {
|
|
728
|
+
const stripped = backupRetention.secretsStripped ? `, ${backupRetention.secretsStripped} arquivo(s) .env* apagado(s)` : '';
|
|
729
|
+
const adopted = backupRetention.adopted ? `, ${backupRetention.adopted} backup(s) antigo(s) mantido(s) ganharam manifesto` : '';
|
|
730
|
+
logger.success(`retenção dos backups do finish: ${backupRetention.removed ?? 0} pasta(s) removida(s)${stripped}${adopted}`);
|
|
731
|
+
for (const d of backupRetention.declined || []) logger.warn(`backup mantido: ${d.backup} (${d.reason})`);
|
|
673
732
|
}
|
|
674
733
|
}
|
|
675
734
|
|
|
@@ -684,7 +743,7 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
684
743
|
const isProblem = (i) => !['info', 'warn'].includes(i.severity);
|
|
685
744
|
|
|
686
745
|
if (options.json) {
|
|
687
|
-
console.log(JSON.stringify({ issues }, null, 2));
|
|
746
|
+
console.log(JSON.stringify({ issues, ...(prune ? { prune } : {}), ...(backupRetention ? { backupRetention } : {}) }, null, 2));
|
|
688
747
|
return !issues.some(isProblem);
|
|
689
748
|
}
|
|
690
749
|
|
|
@@ -1271,15 +1330,13 @@ export async function doctorCommand(options = {}) {
|
|
|
1271
1330
|
} catch { /* non-fatal */ }
|
|
1272
1331
|
|
|
1273
1332
|
// ── Claude Code Plugins ──────────────────────────────────────────────────
|
|
1274
|
-
console.log(chalk.cyan('\n Claude Code Plugins (required
|
|
1333
|
+
console.log(chalk.cyan('\n Claude Code Plugins (required by morph-spec init)'));
|
|
1275
1334
|
const pluginsFile = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
1276
1335
|
let pluginData = { plugins: {} };
|
|
1277
1336
|
try {
|
|
1278
1337
|
pluginData = JSON.parse(await fs.readFile(pluginsFile, 'utf8'));
|
|
1279
1338
|
} catch { /* file may not exist */ }
|
|
1280
1339
|
const pluginStatus = checkInstalledPlugins(pluginData.plugins || {});
|
|
1281
|
-
check('superpowers plugin', pluginStatus.superpowers, false,
|
|
1282
|
-
'Run: morph-spec install-plugin superpowers');
|
|
1283
1340
|
check('context7 plugin', pluginStatus.context7, false,
|
|
1284
1341
|
'Run: morph-spec install-plugin context7');
|
|
1285
1342
|
} else {
|
|
@@ -1390,6 +1447,19 @@ export async function doctorCommand(options = {}) {
|
|
|
1390
1447
|
checks.push({ name: 'codebase graph', status: 'ok', msg: graphAudit.msg });
|
|
1391
1448
|
}
|
|
1392
1449
|
|
|
1450
|
+
// O que o morph-spec guarda dentro do `.git` (backups do finish, registros de
|
|
1451
|
+
// porta e de claim) cresce sem aparecer em `git status`. Informação — `ok` ou
|
|
1452
|
+
// `warn`, nunca falha —, e nenhuma linha quando não há nada guardado.
|
|
1453
|
+
try {
|
|
1454
|
+
const { resolvePrimaryRoot } = await import('./worktree.js');
|
|
1455
|
+
const { morphStoreCheck } = await import('../../lib/worktree/finish-backup.js');
|
|
1456
|
+
const storeCheck = morphStoreCheck(resolvePrimaryRoot(targetPath));
|
|
1457
|
+
if (storeCheck) {
|
|
1458
|
+
checks.push(storeCheck);
|
|
1459
|
+
if (storeCheck.status === 'warn') hasWarnings = true;
|
|
1460
|
+
}
|
|
1461
|
+
} catch { /* fail-open */ }
|
|
1462
|
+
|
|
1393
1463
|
// Check key templates exist (in framework package, not project copy)
|
|
1394
1464
|
const frameworkTemplatesRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'templates');
|
|
1395
1465
|
const keyTemplates = [
|
|
@@ -34,6 +34,7 @@ import { installAgents, installDomainAgents } from '../../utils/agents-installer
|
|
|
34
34
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
35
35
|
import { updateCommand } from './update.js';
|
|
36
36
|
import { resolvePrimaryRoot } from './worktree.js';
|
|
37
|
+
import { registerProject } from '../../lib/projects/registry.js';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Guard against running a full `init` install from inside a secondary
|
|
@@ -135,8 +136,7 @@ export async function initCommand(options) {
|
|
|
135
136
|
try {
|
|
136
137
|
claudeConfigDetected = await detectClaudeConfig(targetPath);
|
|
137
138
|
const hasIntegrations = claudeConfigDetected.plugins.length > 0
|
|
138
|
-
|| claudeConfigDetected.mcpServers.length > 0
|
|
139
|
-
|| claudeConfigDetected.superpowers.installed;
|
|
139
|
+
|| claudeConfigDetected.mcpServers.length > 0;
|
|
140
140
|
|
|
141
141
|
if (hasIntegrations) {
|
|
142
142
|
spinner.stop();
|
|
@@ -287,7 +287,8 @@ export async function initCommand(options) {
|
|
|
287
287
|
try {
|
|
288
288
|
spinner.stop();
|
|
289
289
|
const existingMcps = claudeConfigDetected?.mcpServers || [];
|
|
290
|
-
const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps
|
|
290
|
+
const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps,
|
|
291
|
+
{ integrations: config.project?.integrations ?? [] });
|
|
291
292
|
|
|
292
293
|
const autoNames = Object.keys(orchestration.autoInstallable);
|
|
293
294
|
const manualNames = Object.keys(orchestration.needsManualSetup);
|
|
@@ -467,6 +468,8 @@ export async function initCommand(options) {
|
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
spinner.succeed('MORPH-SPEC installed successfully!');
|
|
471
|
+
// The dashboard's portfolio learns about the project here (fail-open).
|
|
472
|
+
registerProject(targetPath, 'init');
|
|
470
473
|
|
|
471
474
|
// Show next steps
|
|
472
475
|
logger.blank();
|
|
@@ -5,7 +5,7 @@ import { logger } from '../../utils/logger.js';
|
|
|
5
5
|
export async function installPluginCommand(pluginName, _options) {
|
|
6
6
|
if (!pluginName) {
|
|
7
7
|
logger.error('Plugin name required. Usage: morph-spec install-plugin <name>');
|
|
8
|
-
logger.dim(' Available:
|
|
8
|
+
logger.dim(' Available: context7');
|
|
9
9
|
process.exit(1);
|
|
10
10
|
}
|
|
11
11
|
try {
|
|
@@ -33,6 +33,10 @@ import { getProjectTags } from '../../lib/stack-filter.js';
|
|
|
33
33
|
import { updateClaudeMdAgentsBlock, installRulesForStack } from '../../scripts/setup-infra.js';
|
|
34
34
|
import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
|
|
35
35
|
import { createEffects } from '../../lib/installers/effects.js';
|
|
36
|
+
import { registerProject } from '../../lib/projects/registry.js';
|
|
37
|
+
import {
|
|
38
|
+
writeGraphifyignore, describeOutdatedIgnore, graphifyignoreTracked,
|
|
39
|
+
} from '../../lib/graph/graphifyignore.js';
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
42
|
* Backup user's config.json before cleaning
|
|
@@ -725,6 +729,24 @@ export async function updateCommand(options = {}) {
|
|
|
725
729
|
await updateGitignore(targetPath, fx);
|
|
726
730
|
await ensureGitattributes(targetPath, fx);
|
|
727
731
|
|
|
732
|
+
// Migration: a .graphifyignore the previous version generated and nobody
|
|
733
|
+
// edited is swapped for the current one when its RULES change (test doubles
|
|
734
|
+
// and fixtures back in the graph — a header-only difference is left alone);
|
|
735
|
+
// an edited one is left alone, with the change to make. Only an existing
|
|
736
|
+
// file — the graph is opt-in, and `graph build` writes it. A tracked file
|
|
737
|
+
// reaches worktrees and teammates only once committed: each checkout reads
|
|
738
|
+
// its own.
|
|
739
|
+
let graphIgnore = null;
|
|
740
|
+
if (existsSync(join(targetPath, '.graphifyignore'))) {
|
|
741
|
+
updateSpinner.text = 'Checking .graphifyignore...';
|
|
742
|
+
graphIgnore = writeGraphifyignore(targetPath, projectTags, { fx });
|
|
743
|
+
if (graphIgnore.outdated) notices.push(describeOutdatedIgnore(graphIgnore.outdated));
|
|
744
|
+
if (graphIgnore.migrated && graphifyignoreTracked(targetPath)) {
|
|
745
|
+
notices.push('.graphifyignore é versionado: commite a migração (git add .graphifyignore) — '
|
|
746
|
+
+ 'as worktrees e o resto do time leem o arquivo do próprio checkout e só recebem as regras novas depois do commit');
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
728
750
|
// Migration: untrack a pre-v5 committed state.json so it stops being swept
|
|
729
751
|
// into feature commits. Gitignore alone can't do this for an already-tracked file.
|
|
730
752
|
const untracked = untrackCommittedStateJson(targetPath, fx);
|
|
@@ -737,6 +759,10 @@ export async function updateCommand(options = {}) {
|
|
|
737
759
|
logger.blank();
|
|
738
760
|
|
|
739
761
|
if (!dryRun) {
|
|
762
|
+
// The dashboard's portfolio learns about the project here (fail-open;
|
|
763
|
+
// outside the project, so not an effect of the update itself).
|
|
764
|
+
registerProject(targetPath, 'update');
|
|
765
|
+
|
|
740
766
|
// Show version change
|
|
741
767
|
if (projectCheck.current && projectCheck.current !== cliCheck.current) {
|
|
742
768
|
logger.success(`Updated: ${projectCheck.current} → ${cliCheck.current}`);
|
|
@@ -769,6 +795,10 @@ export async function updateCommand(options = {}) {
|
|
|
769
795
|
logger.dim(` ✓ .claude/agents/ (${agentsResult?.agents?.length ?? '?'} personas — stack: ${tagsLabel})`);
|
|
770
796
|
logger.dim(` ✓ .claude/rules/ (${rulesResult?.installed ?? '?'} stack-filtered rules)`);
|
|
771
797
|
if (commandsCopied) logger.dim(' ✓ .claude/commands/');
|
|
798
|
+
if (graphIgnore?.migrated) {
|
|
799
|
+
logger.dim(' ✓ .graphifyignore (dublês, fixtures e helpers de teste voltam ao grafo da raiz no próximo refresh');
|
|
800
|
+
logger.dim(' em background — início de sessão ou comando git do agente; numa worktree: morph-spec graph refresh)');
|
|
801
|
+
}
|
|
772
802
|
if (untracked) {
|
|
773
803
|
logger.dim(' ✓ .morph/state.json untracked (git rm --cached) — commit to finalize');
|
|
774
804
|
logger.warn('Migration: .morph/state.json was removed from git tracking. The');
|
|
@@ -35,14 +35,24 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { execSync, execFileSync } from 'child_process';
|
|
38
|
-
import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync,
|
|
38
|
+
import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, lstatSync, unlinkSync, rmdirSync, cpSync } from 'fs';
|
|
39
39
|
import { join, resolve, dirname, basename } from 'path';
|
|
40
40
|
import chalk from 'chalk';
|
|
41
41
|
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
42
42
|
import { triageIgnoredPaths } from '../../lib/worktree/ignored-triage.js';
|
|
43
|
+
import {
|
|
44
|
+
finishBackupDir,
|
|
45
|
+
writeFeatureScopeBackup,
|
|
46
|
+
writeWorktreeIgnoredBackup,
|
|
47
|
+
enforceBackupRetention,
|
|
48
|
+
newBackupStamp,
|
|
49
|
+
} from '../../lib/worktree/finish-backup.js';
|
|
43
50
|
// The reparse scan lives in hooks/shared because the PreToolUse guardrail needs
|
|
44
51
|
// the SAME answer this file does, and hooks can never import from `src/`.
|
|
45
52
|
import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
|
|
53
|
+
import { ensureCommonExclude } from '../../../framework/hooks/shared/git-exclude.js';
|
|
54
|
+
import { seedGraphFromRoot } from '../../../framework/hooks/shared/graph-refresh.js';
|
|
55
|
+
import { dropLeftoverIgnore, withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
|
|
46
56
|
|
|
47
57
|
// The container name lives in lib/worktree/dirs.js (single source, C1
|
|
48
58
|
// rationale documented there); re-exported here so existing importers and the
|
|
@@ -288,28 +298,9 @@ const EXCLUDE_MARKER = '# morph-spec: infra links inside worktrees (managed —
|
|
|
288
298
|
* @param {string[]} rels - worktree-relative paths to exclude
|
|
289
299
|
*/
|
|
290
300
|
function excludeWorktreePaths(mainCwd, rels) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// itself be a worktree (whose `.git` is a FILE), and the common dir is the
|
|
295
|
-
// only exclude every linked worktree actually reads.
|
|
296
|
-
const common = execSync('git rev-parse --git-common-dir', { cwd: mainCwd, stdio: 'pipe' }).toString().trim();
|
|
297
|
-
if (!common) return;
|
|
298
|
-
const infoDir = join(resolve(mainCwd, common), 'info');
|
|
299
|
-
const excludePath = join(infoDir, 'exclude');
|
|
300
|
-
const current = existsSync(excludePath) ? readFileSync(excludePath, 'utf8') : '';
|
|
301
|
-
const present = new Set(current.split(/\r?\n/).map((l) => l.trim()));
|
|
302
|
-
// Anchored (`/x`): these live at the top of a worktree, and an unanchored
|
|
303
|
-
// `node_modules` would also swallow nested ones the user may track.
|
|
304
|
-
const missing = rels.map((rel) => `/${rel.replace(/\\/g, '/')}`).filter((p) => !present.has(p));
|
|
305
|
-
if (!missing.length) return;
|
|
306
|
-
mkdirSync(infoDir, { recursive: true });
|
|
307
|
-
appendFileSync(excludePath, [
|
|
308
|
-
current && !current.endsWith('\n') ? '\n' : '',
|
|
309
|
-
present.has(EXCLUDE_MARKER) ? '' : `${EXCLUDE_MARKER}\n`,
|
|
310
|
-
`${missing.join('\n')}\n`,
|
|
311
|
-
].join(''));
|
|
312
|
-
} catch { /* fail-open */ }
|
|
301
|
+
// Anchored (`/x`): these live at the top of a worktree, and an unanchored
|
|
302
|
+
// `node_modules` would also swallow nested ones the user may track.
|
|
303
|
+
ensureCommonExclude(mainCwd, rels.map((rel) => `/${rel.replace(/\\/g, '/')}`), EXCLUDE_MARKER);
|
|
313
304
|
}
|
|
314
305
|
|
|
315
306
|
/**
|
|
@@ -412,6 +403,28 @@ export async function materializeState(worktreePath) {
|
|
|
412
403
|
}
|
|
413
404
|
}
|
|
414
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Codebase graph for a new worktree. The tree is born from a root commit, so
|
|
408
|
+
* the root's graph is either this tree's HEAD graph already or one
|
|
409
|
+
* `morph-spec graph refresh` away — and the `reuse` validator reads the graph
|
|
410
|
+
* of the tree `verify` runs in, so a worktree without one skips reuse checking
|
|
411
|
+
* exactly where the new code is written. Nothing is launched in the
|
|
412
|
+
* background: the automatic refresh only ever touches the root (a detached
|
|
413
|
+
* process holding a worktree directory made `finish` fail on win32).
|
|
414
|
+
* Fail-open: no root graph, no copy.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} worktreePath
|
|
417
|
+
* @param {string} root
|
|
418
|
+
* @returns {{seeded: boolean, reason?: string}}
|
|
419
|
+
*/
|
|
420
|
+
function seedWorktreeGraph(worktreePath, root) {
|
|
421
|
+
try {
|
|
422
|
+
return seedGraphFromRoot(worktreePath, root);
|
|
423
|
+
} catch {
|
|
424
|
+
return { seeded: false, reason: 'seed-failed' };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
415
428
|
// ── Command handler ─────────────────────────────────────────────────────────
|
|
416
429
|
|
|
417
430
|
/**
|
|
@@ -521,6 +534,8 @@ export async function runWorktreeSetup(feature, options = {}) {
|
|
|
521
534
|
// committed feature.json files) so hooks work from the very first session.
|
|
522
535
|
const materialized = await ensureLocalState(worktreePath);
|
|
523
536
|
|
|
537
|
+
const graph = seedWorktreeGraph(worktreePath, cwd);
|
|
538
|
+
|
|
524
539
|
// Fleet handoff: the fresh instance that opens this worktree reads
|
|
525
540
|
// handoff.md before anything else (SessionStart hook points at it). The doc
|
|
526
541
|
// is derived and git-invisible (see handoff-generator). Fail-open — and a
|
|
@@ -560,6 +575,7 @@ export async function runWorktreeSetup(feature, options = {}) {
|
|
|
560
575
|
feature,
|
|
561
576
|
materialized: materialized.materialized,
|
|
562
577
|
...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
|
|
578
|
+
...(graph.seeded ? { graph: 'seeded' } : {}),
|
|
563
579
|
nextStep: `Abra o Claude Code em ${worktreePath} para trabalhar nesta feature`,
|
|
564
580
|
};
|
|
565
581
|
} catch (err) {
|
|
@@ -654,6 +670,7 @@ export async function runWorktreeProvision(name, options = {}) {
|
|
|
654
670
|
setupInfraLinks(worktreePath, root);
|
|
655
671
|
const envBlock = await allocateWorktreeEnv(worktreePath, root, `task/${name}`);
|
|
656
672
|
const materialized = await ensureLocalState(worktreePath);
|
|
673
|
+
const graph = seedWorktreeGraph(worktreePath, root);
|
|
657
674
|
|
|
658
675
|
return {
|
|
659
676
|
ok: true,
|
|
@@ -666,6 +683,7 @@ export async function runWorktreeProvision(name, options = {}) {
|
|
|
666
683
|
name,
|
|
667
684
|
materialized: materialized.materialized,
|
|
668
685
|
...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
|
|
686
|
+
...(graph.seeded ? { graph: 'seeded' } : {}),
|
|
669
687
|
};
|
|
670
688
|
} catch (err) {
|
|
671
689
|
return { ok: true, exitCode: 0, created: false, error: (err.message || 'unknown').slice(0, 200), name };
|
|
@@ -930,7 +948,7 @@ export function unlinkWorktreeInfraLinks(worktreePath, mainCwd) {
|
|
|
930
948
|
|
|
931
949
|
/**
|
|
932
950
|
* Gitignored paths a morph worktree owns and that are safe to remove — but ONLY
|
|
933
|
-
* once backed up (see
|
|
951
|
+
* once backed up (see writeWorktreeIgnoredBackup). `.morph/state.json` is provably
|
|
934
952
|
* derivable (loadState self-heals it from the committed feature.json files) and
|
|
935
953
|
* the rest is morph's own diagnostic output, which `setupInfraLinks` documents
|
|
936
954
|
* as worktree-local. They are small, so backing them up costs nothing and buys
|
|
@@ -1054,60 +1072,6 @@ export function classifyIgnoredLeftovers(worktreePath, rootPath, options = {}) {
|
|
|
1054
1072
|
return { shieldable, blocking: [], discarded: [...disposable, ...replicable], replicable };
|
|
1055
1073
|
}
|
|
1056
1074
|
|
|
1057
|
-
/**
|
|
1058
|
-
* Back up the worktree's morph-owned gitignored leftovers before the removal
|
|
1059
|
-
* deletes them. Same destination and rationale as backupFeatureScope.
|
|
1060
|
-
*
|
|
1061
|
-
* @param {string} worktreePath
|
|
1062
|
-
* @param {string} root
|
|
1063
|
-
* @param {string} feature
|
|
1064
|
-
* @param {string[]} rels - worktree-relative paths
|
|
1065
|
-
* @param {string} stamp
|
|
1066
|
-
* @returns {string|null} backup path, or null if ANY copy failed (caller must
|
|
1067
|
-
* then decline the removal — a partial backup authorizes nothing)
|
|
1068
|
-
*/
|
|
1069
|
-
function backupWorktreeIgnored(worktreePath, root, feature, rels, stamp) {
|
|
1070
|
-
const dest = join(root, '.git', 'morph-spec', 'finish-backup', `${feature}-${stamp}-worktree-ignored`);
|
|
1071
|
-
try {
|
|
1072
|
-
for (const rel of rels) {
|
|
1073
|
-
const src = join(worktreePath, rel);
|
|
1074
|
-
if (!existsSync(src)) continue; // a directory entry already covered by its children
|
|
1075
|
-
const target = join(dest, rel);
|
|
1076
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
1077
|
-
cpSync(src, target, { recursive: true });
|
|
1078
|
-
}
|
|
1079
|
-
return existsSync(dest) ? dest : null;
|
|
1080
|
-
} catch {
|
|
1081
|
-
return null;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
/**
|
|
1086
|
-
* Copy the feature's root-side scope to a backup OUTSIDE the working tree, so
|
|
1087
|
-
* the shed below can never be the reason something is lost.
|
|
1088
|
-
*
|
|
1089
|
-
* Lives under `.git/` on purpose: it is not a working-tree path, so it can never
|
|
1090
|
-
* dirty `git status` (which the very next validation reads) and is never merged,
|
|
1091
|
-
* committed, or cleaned by `git clean`.
|
|
1092
|
-
*
|
|
1093
|
-
* @param {string} root
|
|
1094
|
-
* @param {string} feature
|
|
1095
|
-
* @param {string} stamp - caller-supplied timestamp (keeps this pure/testable)
|
|
1096
|
-
* @returns {string|null} absolute backup path, or null if the copy failed
|
|
1097
|
-
*/
|
|
1098
|
-
function backupFeatureScope(root, feature, stamp) {
|
|
1099
|
-
const src = join(root, '.morph', 'features', feature);
|
|
1100
|
-
if (!existsSync(src)) return null;
|
|
1101
|
-
const dest = join(root, '.git', 'morph-spec', 'finish-backup', `${feature}-${stamp}`);
|
|
1102
|
-
try {
|
|
1103
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
1104
|
-
cpSync(src, dest, { recursive: true });
|
|
1105
|
-
return dest;
|
|
1106
|
-
} catch {
|
|
1107
|
-
return null; // caller MUST treat this as "do not shed"
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
1075
|
// ── finish primitives (used by `morph-spec finish`) ───────────────────────
|
|
1112
1076
|
//
|
|
1113
1077
|
// The old `worktree finish` command dissolved here: `morph-spec finish` (see
|
|
@@ -1119,17 +1083,19 @@ function backupFeatureScope(root, feature, stamp) {
|
|
|
1119
1083
|
/**
|
|
1120
1084
|
* Shed the feature's OWN transient root-side bookkeeping so the merge can own
|
|
1121
1085
|
* this feature's state while the whole-root clean-check keeps guarding UNRELATED
|
|
1122
|
-
* work. Backs up FIRST (see
|
|
1123
|
-
* safe WITHOUT having to be right about what is in scope: if the
|
|
1124
|
-
* the merge implies turns out wrong, the bytes are still on
|
|
1125
|
-
* only; the common flow committed the archive on the
|
|
1126
|
-
* untracked root-side scaffold to shed.
|
|
1086
|
+
* work. Backs up FIRST (see writeFeatureScopeBackup), then discards — the shed
|
|
1087
|
+
* is safe WITHOUT having to be right about what is in scope: if the
|
|
1088
|
+
* classification the merge implies turns out wrong, the bytes are still on
|
|
1089
|
+
* disk. Worktree-flow only; the common flow committed the archive on the
|
|
1090
|
+
* branch, so there is no untracked root-side scaffold to shed.
|
|
1127
1091
|
*
|
|
1128
1092
|
* @param {string} root
|
|
1129
1093
|
* @param {string} feature
|
|
1094
|
+
* @param {{ stamp?: string }} [opts] - the finish's one stamp, shared with the
|
|
1095
|
+
* teardown's backup so the two folders count as one backup
|
|
1130
1096
|
* @returns {{ shedBackup: string|null }} backup path if anything was shed
|
|
1131
1097
|
*/
|
|
1132
|
-
export function shedRootFeatureScope(root, feature) {
|
|
1098
|
+
export function shedRootFeatureScope(root, feature, { stamp } = {}) {
|
|
1133
1099
|
const featPathspec = `.morph/features/${feature}`;
|
|
1134
1100
|
let dirtyInScope = '';
|
|
1135
1101
|
try {
|
|
@@ -1138,7 +1104,7 @@ export function shedRootFeatureScope(root, feature) {
|
|
|
1138
1104
|
|
|
1139
1105
|
if (dirtyInScope.trim() === '') return { shedBackup: null };
|
|
1140
1106
|
|
|
1141
|
-
const shedBackup =
|
|
1107
|
+
const shedBackup = writeFeatureScopeBackup({ root, feature, stamp });
|
|
1142
1108
|
// No backup, no shed. Falling through leaves the root dirty, so the caller's
|
|
1143
1109
|
// clean-check refuses — the exact fail-closed behaviour from before any shed.
|
|
1144
1110
|
if (shedBackup) {
|
|
@@ -1363,14 +1329,17 @@ export function pickRemovalFailure(stderrs) {
|
|
|
1363
1329
|
* `deleteBranch` decouples `git branch -d` from removal: true for `--merge` (the
|
|
1364
1330
|
* branch is integrated), false for `--pr` (the open PR still needs the branch).
|
|
1365
1331
|
*
|
|
1332
|
+
* `stamp` names the backup; `finish` passes the one it gave the shed, so both
|
|
1333
|
+
* folders of that finish count as one backup under the retention.
|
|
1334
|
+
*
|
|
1366
1335
|
* @param {string} worktreePath
|
|
1367
|
-
* @param {{ root: string, feature: string, branch: string, options?: object, deleteBranch?: boolean }} ctx
|
|
1336
|
+
* @param {{ root: string, feature: string, branch: string, options?: object, deleteBranch?: boolean, stamp?: string }} ctx
|
|
1368
1337
|
* @returns {Promise<{ worktreeRemoved: boolean, ignoredBackup: string|null, discarded: string[],
|
|
1369
1338
|
* blockers: string[], ignoredBlocking: string[], shieldable: string[], declineReason: string|null,
|
|
1370
1339
|
* relinked: boolean, refused: boolean, infraLeftUnlinked: string[],
|
|
1371
1340
|
* removalFailure: {kind: string, detail: string}|null }>}
|
|
1372
1341
|
*/
|
|
1373
|
-
export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false }) {
|
|
1342
|
+
export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false, stamp }) {
|
|
1374
1343
|
// Drop the infra junctions FIRST, without following them — this is what makes
|
|
1375
1344
|
// a removal survivable (see unlinkWorktreeInfraLinks). Runs unconditionally.
|
|
1376
1345
|
//
|
|
@@ -1392,7 +1361,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
1392
1361
|
let ignoredBackup = null;
|
|
1393
1362
|
const clearedToRemove = blockers.length === 0 && ignored.blocking.length === 0;
|
|
1394
1363
|
if (clearedToRemove && ignored.shieldable.length) {
|
|
1395
|
-
ignoredBackup =
|
|
1364
|
+
ignoredBackup = writeWorktreeIgnoredBackup({ worktreePath, root, feature, rels: ignored.shieldable, stamp });
|
|
1396
1365
|
}
|
|
1397
1366
|
const canRemove = clearedToRemove && (!ignored.shieldable.length || ignoredBackup !== null);
|
|
1398
1367
|
|
|
@@ -1462,7 +1431,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
|
|
|
1462
1431
|
const removeCmd = (addressByTarget || !isValidFeatureName(feature))
|
|
1463
1432
|
? `morph-spec worktree remove --target "${worktreePath}"`
|
|
1464
1433
|
: `morph-spec worktree remove ${feature}`;
|
|
1465
|
-
const backupDir =
|
|
1434
|
+
const backupDir = finishBackupDir(root);
|
|
1466
1435
|
const sample = (list) => `${list.slice(0, 5).join(', ')}${list.length > 5 ? `, +${list.length - 5}` : ''}`;
|
|
1467
1436
|
const declineReason = worktreeRemoved
|
|
1468
1437
|
? null
|
|
@@ -1659,13 +1628,18 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
|
|
|
1659
1628
|
emit({ removed: false, error: 'status-failed', path: worktreePath, root,
|
|
1660
1629
|
hint: 'não foi possível provar que o worktree está limpo — nada foi removido' }, 1);
|
|
1661
1630
|
}
|
|
1662
|
-
const dirt = status.split(/\r?\n/).filter((l) => l.trim());
|
|
1631
|
+
const dirt = withoutLeftoverIgnore(worktreePath, status.split(/\r?\n/).filter((l) => l.trim()));
|
|
1663
1632
|
if (dirt.length) {
|
|
1664
1633
|
emit({ removed: false, error: 'worktree-dirty', path: worktreePath, root, dirt: dirt.slice(0, 20),
|
|
1665
1634
|
hint: `o worktree tem trabalho não commitado — commit ou descarte antes, ou rode \`morph-spec worktree remove --target "${worktreePath}"\` sem --if-clean para remover com backup` }, 1);
|
|
1666
1635
|
}
|
|
1667
1636
|
}
|
|
1668
1637
|
|
|
1638
|
+
// A leftover temporary .graphifyignore (a `graph refresh` killed mid-run) is a
|
|
1639
|
+
// copy of the root's rules: not a file to back up. Deleted only now, after
|
|
1640
|
+
// every refusal, so a refused removal leaves the tree as it was.
|
|
1641
|
+
dropLeftoverIgnore(worktreePath);
|
|
1642
|
+
|
|
1669
1643
|
// The env-registry key this tree allocated under: `<f>` for morph/<f>,
|
|
1670
1644
|
// `task/<n>` for morph-task/<n>. Read BEFORE the teardown (the entry is gone
|
|
1671
1645
|
// from `git worktree list` afterwards). The old `'worktree'` placeholder
|
|
@@ -1683,9 +1657,13 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
|
|
|
1683
1657
|
e2e = tearDownFeatureStack(envKey || feature || basename(worktreePath), { root, worktreePath, volumes: false });
|
|
1684
1658
|
} catch { e2e = null; /* fail-open */ }
|
|
1685
1659
|
|
|
1660
|
+
// The stamp this removal's backup is written under — protected from the
|
|
1661
|
+
// retention that runs after it.
|
|
1662
|
+
const stamp = newBackupStamp();
|
|
1686
1663
|
const teardown = await teardownWorktree(worktreePath, {
|
|
1687
1664
|
root,
|
|
1688
1665
|
feature: feature || envKey || 'worktree',
|
|
1666
|
+
stamp,
|
|
1689
1667
|
branch,
|
|
1690
1668
|
options: { discardIgnored: options.discardIgnored === true },
|
|
1691
1669
|
// Removing a worktree is not closing a feature: the branch is the work, and
|
|
@@ -1724,6 +1702,11 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
|
|
|
1724
1702
|
if (teardown.infraLeftUnlinked.length) out.infraLeftUnlinked = teardown.infraLeftUnlinked;
|
|
1725
1703
|
out.hint = teardown.declineReason;
|
|
1726
1704
|
}
|
|
1705
|
+
// After the new backup, never before — and never the new backup itself.
|
|
1706
|
+
if (teardown.ignoredBackup) {
|
|
1707
|
+
const retention = enforceBackupRetention(root, { protectStamps: [stamp] });
|
|
1708
|
+
if (retention) out.backupRetention = retention;
|
|
1709
|
+
}
|
|
1727
1710
|
emit(out, teardown.worktreeRemoved ? 0 : 1);
|
|
1728
1711
|
}
|
|
1729
1712
|
|
|
@@ -6,6 +6,7 @@ import { getFeature, setApprovalGate, derivePhase } from '../../core/state/state
|
|
|
6
6
|
import { unmetPrereqs } from '../../core/state/gate-order.js';
|
|
7
7
|
import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
|
|
8
8
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
9
|
+
import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
|
|
9
10
|
|
|
10
11
|
const FRAMEWORK_DIR = fileURLToPath(new URL('../../../framework/', import.meta.url));
|
|
11
12
|
|
|
@@ -169,6 +170,19 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
169
170
|
const gate = PHASE_GATE[phase];
|
|
170
171
|
const approvedBy = resolveApprovedBy(options);
|
|
171
172
|
|
|
173
|
+
// The screen checklist is part of what the uiux phase owes. The predicate
|
|
174
|
+
// is approve.js's too: an "auto" signature never closes a gate the
|
|
175
|
+
// gate-check would pause, and a recorded decision or a human signature
|
|
176
|
+
// (--approver) is the answer to that pause; --force is not, as in approve.
|
|
177
|
+
const checklistBlock = phase === 'uiux' ? uiuxAutoApprovalBlock(process.cwd(), featureName, feature) : null;
|
|
178
|
+
if (checklistBlock && approvedBy === 'auto') {
|
|
179
|
+
console.error(chalk.red(`\n❌ A fase "uiux" não pode ser encerrada com assinatura automática — ${checklistBlock}.`));
|
|
180
|
+
console.error(chalk.gray('\n Complete o checklist (ver a skill morph-uiux) e rode de novo. Quem decidiu aprovar assim'));
|
|
181
|
+
console.error(chalk.gray(` mesmo grava a decisão (morph-spec gate-decision ${featureName} uiux --pause false --reason "...")`));
|
|
182
|
+
console.error(chalk.gray(' ou assina com --approver <nome>. O --force não dispensa o checklist.\n'));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
// Gate monotonicity (mirrors approve.js): advance is a gate write-path too,
|
|
173
187
|
// so it must not stamp gate N while a predecessor is pending. In the
|
|
174
188
|
// sanctioned flow advance is always in-order (it approves G1 before the plan
|
|
@@ -242,6 +256,10 @@ export async function advanceCommand(featureName, options = {}) {
|
|
|
242
256
|
if (missingArtifacts.length) {
|
|
243
257
|
console.log(chalk.yellow(` ⚠️ --force: avançou SEM ${missingArtifacts.join(', ')}.`));
|
|
244
258
|
}
|
|
259
|
+
if (checklistBlock) {
|
|
260
|
+
const by = options.force ? '--force' : `aprovado por ${approvedBy}`;
|
|
261
|
+
console.log(chalk.yellow(` ⚠️ ${by}: avançou com o ${checklistBlock}.`));
|
|
262
|
+
}
|
|
245
263
|
// Report what actually happened: claiming "approved (auto)" when nothing
|
|
246
264
|
// was written is how the destroyed signature stayed invisible.
|
|
247
265
|
if (gate && alreadyApproved) {
|