@polymorphism-tech/morph-spec 8.34.2 → 8.35.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 +20 -3
- package/README.md +2 -2
- package/bin/morph-spec.js +18 -4
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +16 -17
- package/framework/agents.json +197 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +234 -9
- package/framework/evals/proof-rubrics/README.md +76 -0
- package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
- package/framework/evals/proof-rubrics/fiacao.md +54 -0
- package/framework/evals/proof-rubrics/guard-cego.md +52 -0
- package/framework/evals/proof-rubrics/media-claims.md +109 -0
- package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
- package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
- package/framework/evals/rubrics/architecture.md +30 -0
- package/framework/evals/rubrics/code-quality.md +26 -0
- package/framework/evals/rubrics/test-coverage.md +20 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/e2e-check.js +135 -22
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rules/ai-agents-standards.md +14 -2
- package/framework/schemas/ai-pin.schema.json +97 -0
- package/framework/schemas/config.schema.json +107 -0
- package/framework/schemas/feature.schema.json +23 -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 +26 -5
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +18 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +33 -2
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +166 -9
- 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 +1 -1
- package/framework/standards/PROVENANCE.json +81 -3
- package/framework/standards/STANDARDS.json +1483 -839
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +164 -68
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +61 -37
- package/framework/standards/ai-agents/context-providers.md +97 -24
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
- package/framework/standards/ai-agents/cost-and-budget.md +256 -0
- package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
- package/framework/standards/ai-agents/evals-with-cache.md +289 -0
- package/framework/standards/ai-agents/guardrails.md +222 -0
- package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
- package/framework/standards/ai-agents/mcp-server.md +119 -18
- package/framework/standards/ai-agents/mcp-tools.md +85 -32
- package/framework/standards/ai-agents/media-pipeline.md +352 -0
- package/framework/standards/ai-agents/media-video.md +267 -0
- package/framework/standards/ai-agents/middleware-patterns.md +39 -5
- package/framework/standards/ai-agents/modalities-image-gen.md +354 -105
- package/framework/standards/ai-agents/modalities-speech-to-text.md +8 -2
- package/framework/standards/ai-agents/modalities-text-to-speech.md +10 -2
- package/framework/standards/ai-agents/modalities-vision.md +6 -2
- package/framework/standards/ai-agents/multi-agent-patterns.md +61 -5
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +38 -9
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +174 -39
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +27 -19
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +34 -3
- package/framework/standards/ai-agents/sweet-spot.md +23 -3
- package/framework/standards/ai-agents/testing-ai.md +236 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
- package/framework/standards/ai-agents/workflows.md +27 -7
- package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +3 -1
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +13 -0
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +1 -1
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +1 -1
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
- package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
- package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
- package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
- package/framework/templates/dotnet/ai-kit/README.md +128 -0
- package/framework/templates/dotnet/ai-kit/global.json +9 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +3 -3
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/e2e/e2e.js +15 -2
- package/src/commands/index.js +13 -13
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +140 -5
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +8 -1
- package/src/commands/state/advance.js +24 -2
- package/src/commands/state/approve.js +33 -1
- package/src/commands/state/create.js +72 -11
- package/src/commands/state/finish.js +333 -21
- package/src/commands/state/gate-check.js +190 -6
- package/src/commands/state/score.js +36 -1
- package/src/commands/validation/verify.js +134 -25
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +9 -1
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/ai/ai-checks.js +182 -0
- package/src/lib/ai/ai-project-scan.js +347 -0
- package/src/lib/ai/code-audit.js +181 -0
- package/src/lib/ai/comment-stripper.js +184 -0
- package/src/lib/ai/pin-drift.js +138 -0
- package/src/lib/ai/registry-audit.js +285 -0
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/e2e/compose-env.js +50 -4
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/effects.js +193 -0
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/installers/templates-reconcile.js +3 -3
- package/src/lib/risk-detector.js +284 -0
- package/src/lib/standards/verified-against.js +276 -0
- package/src/lib/tasks/eval-node.js +485 -0
- package/src/lib/tasks/linux-runner.js +297 -0
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +410 -33
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/verify-runner.js +583 -28
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +24 -0
- package/src/lib/worktree/worktree-audit.js +30 -1
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +11 -4
- package/src/utils/hooks-installer.js +29 -26
- package/src/utils/skills-installer.js +36 -29
- package/src/utils/version-checker.js +68 -3
- package/framework/standards/ai-agents/prompts-in-db.md +0 -214
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* docker-audit.js — the part of "which worktrees exist" that the filesystem
|
|
3
|
+
* cannot answer: the docker stacks still running for them.
|
|
4
|
+
*
|
|
5
|
+
* `worktree-audit.js` reconciles git's registry, the disk and morph's features.
|
|
6
|
+
* None of the three can see a CONTAINER. A stack survives everything they look
|
|
7
|
+
* at: `finish` removes the worktree and the branch, the directory disappears,
|
|
8
|
+
* git forgets the registration — and `morph-{feature}` keeps running, holding
|
|
9
|
+
* the feature's whole port block (and its named volumes) against the next
|
|
10
|
+
* worktree that lands on the same block. The failure surfaces later, somewhere
|
|
11
|
+
* else, as `port is already allocated` during an `e2e up` that has nothing to
|
|
12
|
+
* do with the stack that is actually holding the port.
|
|
13
|
+
*
|
|
14
|
+
* Two findings, both `severity: 'warn'` — reported, never a failure of
|
|
15
|
+
* `doctor --worktrees` (`doctor.js` treats `info`/`warn` as non-problems):
|
|
16
|
+
*
|
|
17
|
+
* • docker-stack-orphan — a `morph-*` compose project of THIS repository
|
|
18
|
+
* running for a feature this tree does not know
|
|
19
|
+
* • docker-port-held — a port from a feature's allocated block published
|
|
20
|
+
* by a container belonging to a DIFFERENT compose project
|
|
21
|
+
*
|
|
22
|
+
* This module does not go into `doctor --full`, on purpose (D10): every check
|
|
23
|
+
* there must be satisfiable in the framework repo itself, and a docker check in
|
|
24
|
+
* a repo with no compose file could only be permanently yellow. A check that
|
|
25
|
+
* can never pass where it runs teaches nothing.
|
|
26
|
+
*
|
|
27
|
+
* Nothing here mutates anything — not the registry it reads (see
|
|
28
|
+
* `allocatedFeatures`), and certainly not docker. The `fix` strings name the
|
|
29
|
+
* destructive command; a human runs it.
|
|
30
|
+
*
|
|
31
|
+
* @module lib/worktree/docker-audit
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { execFileSync } from 'child_process';
|
|
35
|
+
import { resolve } from 'path';
|
|
36
|
+
|
|
37
|
+
// src/ → framework/hooks/shared/ is the allowed direction (hooks are installed
|
|
38
|
+
// standalone into target projects, where src/ does not exist).
|
|
39
|
+
import {
|
|
40
|
+
composeProjectName,
|
|
41
|
+
defaultExec,
|
|
42
|
+
isDockerAvailable,
|
|
43
|
+
parseComposePs,
|
|
44
|
+
} from '../../../framework/hooks/shared/e2e-check.js';
|
|
45
|
+
import { allocateEnvBlock, allocatedFeatures, portLayout, resolveFleetConfig } from './env-allocator.js';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The prefix `composeProjectName()` stamps on every stack morph brings up,
|
|
49
|
+
* derived from the function itself rather than retyped — the string `morph-`
|
|
50
|
+
* living in two places is how the two stop agreeing.
|
|
51
|
+
*/
|
|
52
|
+
const PROJECT_PROBE = 'probe';
|
|
53
|
+
const MORPH_PROJECT_PREFIX = composeProjectName(PROJECT_PROBE).slice(0, -PROJECT_PROBE.length);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Same fail-open resolution the other callers use: the COMMON git dir is the
|
|
57
|
+
* one location every linked worktree shares, and it is where the port-block
|
|
58
|
+
* registry lives.
|
|
59
|
+
*/
|
|
60
|
+
function resolveGitCommonDir(cwd) {
|
|
61
|
+
try {
|
|
62
|
+
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
|
|
63
|
+
cwd, stdio: 'pipe', encoding: 'utf-8', windowsHide: true,
|
|
64
|
+
}).trim();
|
|
65
|
+
return common ? resolve(cwd, common) : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Path comparison that matches the host's own rules. Windows paths differ in
|
|
73
|
+
* drive-letter case between what git reports and what docker echoes back;
|
|
74
|
+
* treating those as different paths would make every stack look foreign.
|
|
75
|
+
*/
|
|
76
|
+
const samePathCase = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
|
|
77
|
+
const norm = (p) => samePathCase(resolve(String(p)).replace(/\\/g, '/'));
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Is this compose project OURS to talk about?
|
|
81
|
+
*
|
|
82
|
+
* The `morph-` prefix is global to the host, not to a repository: two different
|
|
83
|
+
* repos that use morph-spec and both have a feature named `triagem` produce the
|
|
84
|
+
* SAME compose project name. Without this test, `doctor` in repo A would report
|
|
85
|
+
* repo B's live stack as an orphan and hand the reader a `down -v` — volumes
|
|
86
|
+
* included — for somebody else's running database. The compose file's path is
|
|
87
|
+
* what ties a stack to a repository, and it keeps pointing inside the root even
|
|
88
|
+
* after the worktree directory it named is gone.
|
|
89
|
+
*
|
|
90
|
+
* Unattributable (no config file in the listing) counts as NOT ours: an
|
|
91
|
+
* unknown owner is not evidence of ownership.
|
|
92
|
+
*/
|
|
93
|
+
function belongsToRoot(entry, rootNorm) {
|
|
94
|
+
const configFiles = String(entry?.ConfigFiles ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
95
|
+
if (configFiles.length === 0) return false;
|
|
96
|
+
return configFiles.some((f) => {
|
|
97
|
+
try {
|
|
98
|
+
return norm(f).startsWith(`${rootNorm}/`);
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Host ports a `docker ps`/`docker compose ps` entry publishes.
|
|
107
|
+
* Both output shapes are accepted rather than pinning a docker version:
|
|
108
|
+
* `Ports` as the human string (`0.0.0.0:4002->5432/tcp`) and `Publishers` as
|
|
109
|
+
* the structured array compose emits.
|
|
110
|
+
*
|
|
111
|
+
* @returns {Set<number>}
|
|
112
|
+
*/
|
|
113
|
+
function publishedPorts(entry) {
|
|
114
|
+
const ports = new Set();
|
|
115
|
+
for (const pub of Array.isArray(entry?.Publishers) ? entry.Publishers : []) {
|
|
116
|
+
const p = Number(pub?.PublishedPort);
|
|
117
|
+
if (Number.isInteger(p) && p > 0) ports.add(p);
|
|
118
|
+
}
|
|
119
|
+
const raw = String(entry?.Ports ?? '');
|
|
120
|
+
for (const m of raw.matchAll(/:(\d{1,5})->/g)) {
|
|
121
|
+
const p = Number(m[1]);
|
|
122
|
+
if (Number.isInteger(p) && p > 0) ports.add(p);
|
|
123
|
+
}
|
|
124
|
+
return ports;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The compose project a container belongs to, from its labels. `null` = unknown. */
|
|
128
|
+
function composeProjectOf(entry) {
|
|
129
|
+
const labels = String(entry?.Labels ?? '');
|
|
130
|
+
for (const part of labels.split(',')) {
|
|
131
|
+
const idx = part.indexOf('=');
|
|
132
|
+
if (idx < 0) continue;
|
|
133
|
+
if (part.slice(0, idx).trim() === 'com.docker.compose.project') return part.slice(idx + 1).trim() || null;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Readable container name for a `docker ps` entry (Names may be a list). */
|
|
139
|
+
function containerNameOf(entry) {
|
|
140
|
+
const names = String(entry?.Names ?? entry?.Name ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
141
|
+
return names[0] || String(entry?.ID ?? 'container desconhecido');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse a docker JSON listing that may be an array or NDJSON.
|
|
146
|
+
* `null` means the output could not be read — DISTINCT from `[]`, which means
|
|
147
|
+
* docker answered and there was nothing running. A caller that conflated the
|
|
148
|
+
* two would turn "could not ask" into "nothing to report", or worse, into a
|
|
149
|
+
* finding.
|
|
150
|
+
*
|
|
151
|
+
* @returns {Array<object>|null}
|
|
152
|
+
*/
|
|
153
|
+
function parseDockerJson(raw) {
|
|
154
|
+
const text = String(raw ?? '').trim();
|
|
155
|
+
if (!text) return [];
|
|
156
|
+
const parsed = parseComposePs(text);
|
|
157
|
+
if (parsed.length > 0) return parsed;
|
|
158
|
+
// parseComposePs (the ONE docker-JSON parser in this codebase) collapses two
|
|
159
|
+
// very different answers into `[]`: "docker listed nothing" and "this is not
|
|
160
|
+
// JSON". Only a literal empty array is the first one; everything else that
|
|
161
|
+
// parsed to nothing is garbage, and garbage must not read as "all clear".
|
|
162
|
+
return /^\[\s*\]$/.test(text) ? [] : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Audit the docker stacks around this repository.
|
|
167
|
+
*
|
|
168
|
+
* Returns `[]` — asking docker nothing at all — in three situations, and the
|
|
169
|
+
* silence is the point: this module is loaded by `doctor --worktrees` in every
|
|
170
|
+
* repo, including ones that never containerize anything.
|
|
171
|
+
*
|
|
172
|
+
* 1. no feature ever allocated a port block (no registry): there is no
|
|
173
|
+
* isolation contract to compare a running stack against;
|
|
174
|
+
* 2. the docker CLI is not usable here;
|
|
175
|
+
* 3. any docker call exits non-zero or answers with something that is not
|
|
176
|
+
* JSON — fail-open, the standing rule of this repo's diagnostics.
|
|
177
|
+
*
|
|
178
|
+
* `[]` here reads as "nothing to report", not as "unknown": with no docker or
|
|
179
|
+
* no blocks, nothing CAN be colliding. That is why this is not the `null`-is-
|
|
180
|
+
* UNKNOWN shape `pr-index.js` uses — there the question stays open, here the
|
|
181
|
+
* absence is itself the answer.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} root - primary root of the repository
|
|
184
|
+
* @param {object} [opts]
|
|
185
|
+
* @param {(file: string, args: string[], o?: object) => {code: number, output: string}} [opts.exec]
|
|
186
|
+
* injectable executor, exactly like `e2e-check.js` — the CI runners have no
|
|
187
|
+
* guaranteed daemon, so every path here must be testable without docker
|
|
188
|
+
* @param {Iterable<string>} [opts.features] - feature names this tree knows.
|
|
189
|
+
* Passed IN rather than re-derived: `auditWorktrees` already read
|
|
190
|
+
* `.morph/features/` and `git worktree list`, and two scans of the same fact
|
|
191
|
+
* are two chances to disagree about whether a stack is orphaned
|
|
192
|
+
* @param {string|null} [opts.gitCommonDir]
|
|
193
|
+
* @returns {Array<{type: string, severity: string, detail: string, fix: string, feature?: string}>}
|
|
194
|
+
*/
|
|
195
|
+
export function auditDockerStacks(root, { exec = defaultExec, features = [], gitCommonDir } = {}) {
|
|
196
|
+
const commonDir = gitCommonDir === undefined ? resolveGitCommonDir(root) : gitCommonDir;
|
|
197
|
+
if (!commonDir) return [];
|
|
198
|
+
|
|
199
|
+
// (1) No port block was ever allocated → no fleet isolation to protect.
|
|
200
|
+
const allocated = allocatedFeatures(commonDir);
|
|
201
|
+
if (allocated.length === 0) return [];
|
|
202
|
+
|
|
203
|
+
// (2) No docker → nothing can be holding anything.
|
|
204
|
+
if (!isDockerAvailable(root, exec)) return [];
|
|
205
|
+
|
|
206
|
+
const known = new Set(features);
|
|
207
|
+
const rootNorm = norm(root);
|
|
208
|
+
const issues = [];
|
|
209
|
+
|
|
210
|
+
// ---- docker-stack-orphan ----------------------------------------------
|
|
211
|
+
const lsResult = exec('docker', ['compose', 'ls', '--format', 'json'], { cwd: root });
|
|
212
|
+
if (lsResult.code !== 0) return []; // (3) fail-open
|
|
213
|
+
const projects = parseDockerJson(lsResult.output);
|
|
214
|
+
if (projects === null) return []; // (3) unreadable answer is not a finding
|
|
215
|
+
|
|
216
|
+
const knownProjects = new Set([...known].map((f) => composeProjectName(f)));
|
|
217
|
+
for (const entry of projects) {
|
|
218
|
+
const name = String(entry?.Name ?? '').trim();
|
|
219
|
+
if (!name.startsWith(MORPH_PROJECT_PREFIX)) continue;
|
|
220
|
+
if (knownProjects.has(name)) continue;
|
|
221
|
+
if (!belongsToRoot(entry, rootNorm)) continue;
|
|
222
|
+
|
|
223
|
+
const slug = name.slice(MORPH_PROJECT_PREFIX.length);
|
|
224
|
+
issues.push({
|
|
225
|
+
type: 'docker-stack-orphan',
|
|
226
|
+
severity: 'warn',
|
|
227
|
+
feature: slug || null,
|
|
228
|
+
detail: `stack docker "${name}" está de pé (${String(entry?.Status ?? 'estado desconhecido')}) e não corresponde a nenhuma feature desta árvore — sobra de outra sessão, segurando o bloco de portas e os volumes de "${slug}"`,
|
|
229
|
+
// NEVER `morph-spec e2e down {slug}`: that command resolves the feature's
|
|
230
|
+
// compose file, and this stack's feature no longer exists here. The
|
|
231
|
+
// project name is the only handle that still works.
|
|
232
|
+
fix: `docker compose -p ${name} down -v`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---- docker-port-held --------------------------------------------------
|
|
237
|
+
const psResult = exec('docker', ['ps', '--format', 'json'], { cwd: root });
|
|
238
|
+
if (psResult.code !== 0) return []; // (3) fail-open
|
|
239
|
+
const containers = parseDockerJson(psResult.output);
|
|
240
|
+
if (containers === null) return [];
|
|
241
|
+
|
|
242
|
+
const { basePort, blockSize } = resolveFleetConfig(root);
|
|
243
|
+
for (const feature of allocated) {
|
|
244
|
+
// A block allocated by a feature this tree does not know belongs to another
|
|
245
|
+
// worktree of the repo; its collisions are that worktree's to report.
|
|
246
|
+
if (!known.has(feature)) continue;
|
|
247
|
+
|
|
248
|
+
let layout;
|
|
249
|
+
try {
|
|
250
|
+
// `allocateEnvBlock` for a feature that is ALREADY in the registry takes
|
|
251
|
+
// the early-return path and writes nothing. The expected ports are
|
|
252
|
+
// DERIVED from portLayout — the same function that writes worktree.env
|
|
253
|
+
// and the compose env — never recomputed here.
|
|
254
|
+
const block = allocateEnvBlock(feature, { gitCommonDir: commonDir, basePort, blockSize });
|
|
255
|
+
layout = portLayout(block, { feature });
|
|
256
|
+
} catch {
|
|
257
|
+
continue; // block out of range / corrupt entry — nothing to compare
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const ownProject = layout.COMPOSE_PROJECT_NAME;
|
|
261
|
+
const expected = new Map();
|
|
262
|
+
for (const key of ['PORT', 'API_PORT', 'POSTGRES_PORT', 'REDIS_PORT']) {
|
|
263
|
+
const port = Number(layout[key]);
|
|
264
|
+
if (Number.isInteger(port)) expected.set(port, key);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const container of containers) {
|
|
268
|
+
const project = composeProjectOf(container);
|
|
269
|
+
if (project === ownProject) continue; // the feature's own stack, as intended
|
|
270
|
+
for (const port of publishedPorts(container)) {
|
|
271
|
+
const key = expected.get(port);
|
|
272
|
+
if (!key) continue;
|
|
273
|
+
issues.push({
|
|
274
|
+
type: 'docker-port-held',
|
|
275
|
+
severity: 'warn',
|
|
276
|
+
feature,
|
|
277
|
+
detail: `a porta ${port} (${key} do bloco de "${feature}") está publicada pelo container ${containerNameOf(container)}${project ? ` do projeto compose "${project}"` : ' (sem projeto compose)'}, não por "${ownProject}" — a stack de "${feature}" não sobe enquanto ela estiver presa`,
|
|
278
|
+
fix: project
|
|
279
|
+
? `docker compose -p ${project} down (ou pare o container ${containerNameOf(container)}) antes de subir a stack de "${feature}"`
|
|
280
|
+
: `docker stop ${containerNameOf(container)} antes de subir a stack de "${feature}"`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return issues;
|
|
287
|
+
}
|
|
@@ -110,6 +110,30 @@ export function allocateEnvBlock(feature, { gitCommonDir, basePort = DEFAULT_BAS
|
|
|
110
110
|
return { index, base, range: [base, base + blockSize - 1], created: true };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* The features that ALREADY own a block — read-only, allocating nothing.
|
|
115
|
+
*
|
|
116
|
+
* `allocateEnvBlock` is the only sanctioned way to get a block, and it WRITES
|
|
117
|
+
* when the feature has none. A diagnostic that has to ask "who owns a block?"
|
|
118
|
+
* must not answer by handing one out: `doctor` is read-only, and an audit that
|
|
119
|
+
* mutates the registry it audits invents the very state it then reports. So
|
|
120
|
+
* this reader exists to bound the question, and the block itself still comes
|
|
121
|
+
* from `allocateEnvBlock` (which, for a feature listed here, takes the
|
|
122
|
+
* early-return path and writes nothing).
|
|
123
|
+
*
|
|
124
|
+
* An absent or corrupt registry yields `[]` — "nobody allocated anything",
|
|
125
|
+
* which for every caller is the same as "nothing to compare against".
|
|
126
|
+
*
|
|
127
|
+
* @param {string} gitCommonDir
|
|
128
|
+
* @returns {string[]} feature names with an allocated block
|
|
129
|
+
*/
|
|
130
|
+
export function allocatedFeatures(gitCommonDir) {
|
|
131
|
+
const registry = readRegistry(gitCommonDir);
|
|
132
|
+
return Object.entries(registry.allocations)
|
|
133
|
+
.filter(([, a]) => Number.isInteger(a?.index))
|
|
134
|
+
.map(([feature]) => feature);
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
/**
|
|
114
138
|
* Release a feature's block. Returns false when nothing was allocated.
|
|
115
139
|
*
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { detectDefaultBranch } from '../git/default-branch.js';
|
|
37
37
|
import { buildPrIndex } from '../git/pr-index.js';
|
|
38
38
|
import { classifyBranch, commitsAhead, resolveBaseRef } from '../git/branch-classifier.js';
|
|
39
|
+
import { auditDockerStacks } from './docker-audit.js';
|
|
39
40
|
|
|
40
41
|
const norm = (p) => resolve(p).replace(/\\/g, '/');
|
|
41
42
|
|
|
@@ -212,10 +213,24 @@ function dirsOnDisk(root) {
|
|
|
212
213
|
* directory that exists but git's own
|
|
213
214
|
* registry does not know as a worktree
|
|
214
215
|
*
|
|
216
|
+
* • docker-stack-orphan (warn) — a `morph-*` docker stack of this repository
|
|
217
|
+
* still running for a feature this tree does
|
|
218
|
+
* not know, and
|
|
219
|
+
* • docker-port-held (warn) — a port from a feature's block published by
|
|
220
|
+
* another compose project. Both come from
|
|
221
|
+
* `docker-audit.js`: the three sources above
|
|
222
|
+
* cannot see a CONTAINER, and a stack outlives
|
|
223
|
+
* every one of them
|
|
224
|
+
*
|
|
215
225
|
* @param {string} [cwd]
|
|
226
|
+
* @param {object} [opts]
|
|
227
|
+
* @param {Function} [opts.exec] - injectable executor handed to the docker
|
|
228
|
+
* audit, so the docker paths are testable with no daemon (the CI runners
|
|
229
|
+
* have none). Absent, the real `docker` CLI is used — and answers `[]` when
|
|
230
|
+
* it is not installed.
|
|
216
231
|
* @returns {Array<{ type: string, detail: string, path?: string, feature?: string, fix: string }>}
|
|
217
232
|
*/
|
|
218
|
-
export function auditWorktrees(cwd = process.cwd()) {
|
|
233
|
+
export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
219
234
|
if (!isGitRepo(cwd)) return [];
|
|
220
235
|
const root = resolvePrimaryRoot(cwd);
|
|
221
236
|
const rootNorm = norm(root);
|
|
@@ -394,6 +409,20 @@ export function auditWorktrees(cwd = process.cwd()) {
|
|
|
394
409
|
}
|
|
395
410
|
}
|
|
396
411
|
|
|
412
|
+
// The fourth source, and the only one that is not a file: the docker stacks.
|
|
413
|
+
// A container survives the removal of the worktree, of the branch and of the
|
|
414
|
+
// feature — none of the three reconciliations above can see it, and it keeps
|
|
415
|
+
// holding the feature's port block. The feature names come from HERE (the
|
|
416
|
+
// ones morph knows plus the ones the registered `morph/*` worktrees carry)
|
|
417
|
+
// rather than from a second scan inside docker-audit: two readings of the
|
|
418
|
+
// same fact are two chances to disagree about which stack is orphaned.
|
|
419
|
+
const treeFeatures = new Set(features);
|
|
420
|
+
for (const entry of registered) {
|
|
421
|
+
const f = entry.branch?.match(/^refs\/heads\/morph\/(.+)$/)?.[1];
|
|
422
|
+
if (f) treeFeatures.add(f);
|
|
423
|
+
}
|
|
424
|
+
issues.push(...auditDockerStacks(root, { exec, features: treeFeatures }));
|
|
425
|
+
|
|
397
426
|
return issues;
|
|
398
427
|
}
|
|
399
428
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { join, dirname, resolve } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { execSync } from 'child_process';
|
|
17
|
-
import { readFileSync, readdirSync
|
|
17
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
18
18
|
import {
|
|
19
19
|
copyDirectory,
|
|
20
20
|
copyFile,
|
|
@@ -33,6 +33,7 @@ import { installSkills } from '../utils/skills-installer.js';
|
|
|
33
33
|
import { installAgents, installDomainAgents } from '../utils/agents-installer.js';
|
|
34
34
|
import { resolvePrimaryRoot } from '../commands/project/worktree.js';
|
|
35
35
|
import { reconcileLegacyTemplatesCopy } from '../lib/installers/templates-reconcile.js';
|
|
36
|
+
import { realEffects } from '../lib/installers/effects.js';
|
|
36
37
|
|
|
37
38
|
const FRAMEWORK_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
|
|
38
39
|
|
|
@@ -61,7 +62,7 @@ async function installRequiredPlugins(log, exec) {
|
|
|
61
62
|
* @param {string} rulesDest - Destination rules directory (.claude/rules/)
|
|
62
63
|
* @param {string[]} projectTags - Stack tags from config (e.g., ['dotnet', 'blazor'])
|
|
63
64
|
*/
|
|
64
|
-
export function installRulesForStack(rulesSrc, rulesDest, projectTags) {
|
|
65
|
+
export function installRulesForStack(rulesSrc, rulesDest, projectTags, fx = realEffects()) {
|
|
65
66
|
const sourceFiles = readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
|
|
66
67
|
const installed = new Set();
|
|
67
68
|
|
|
@@ -69,15 +70,19 @@ export function installRulesForStack(rulesSrc, rulesDest, projectTags) {
|
|
|
69
70
|
const content = readFileSync(join(rulesSrc, file), 'utf-8');
|
|
70
71
|
const stacks = parseStacks(content);
|
|
71
72
|
if (shouldInstall(stacks, projectTags)) {
|
|
72
|
-
|
|
73
|
+
fx.copyFile(join(rulesSrc, file), join(rulesDest, file));
|
|
73
74
|
installed.add(file);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
// Cleanup orphans
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// Cleanup orphans — calado em `dryRun` pelo mesmo motivo dos laços gêmeos em
|
|
79
|
+
// skills-installer.js e agents-installer.js (D6): `cleanFrameworkDirs()` já
|
|
80
|
+
// apagou `.claude/rules/` inteiro antes de este código rodar de verdade.
|
|
81
|
+
if (!fx.dryRun) {
|
|
82
|
+
for (const file of readdirSync(rulesDest).filter(f => f.endsWith('.md'))) {
|
|
83
|
+
if (!installed.has(file)) {
|
|
84
|
+
fx.remove(join(rulesDest, file));
|
|
85
|
+
}
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -134,12 +139,20 @@ async function installStackAwareAssets(targetPath, log) {
|
|
|
134
139
|
*
|
|
135
140
|
* @param {string} claudeMdPath - Absolute path to the installed .claude/CLAUDE.md
|
|
136
141
|
* @param {Array<{ name: string, title: string, domains: string[] }>} agents
|
|
142
|
+
* @param {object} [options]
|
|
143
|
+
* @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [options.fx]
|
|
144
|
+
* @param {string} [options.readFrom] - De onde LER o conteúdo-base.
|
|
145
|
+
* Num `--dry-run` o arquivo de destino ainda não recebeu a cópia que o passo
|
|
146
|
+
* anterior previu, então ler o destino diria "sem marcadores" e o efeito de
|
|
147
|
+
* escrita sumiria da previsão — divergindo da execução real. Lendo a MESMA
|
|
148
|
+
* origem que seria copiada, previsão e execução decidem igual.
|
|
137
149
|
*/
|
|
138
|
-
export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = []) {
|
|
139
|
-
|
|
150
|
+
export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = [], options = {}) {
|
|
151
|
+
const { fx = realEffects(), readFrom = claudeMdPath } = options;
|
|
152
|
+
if (!(await pathExists(readFrom))) return;
|
|
140
153
|
const START = '<!-- morph-agents:start -->';
|
|
141
154
|
const END = '<!-- morph-agents:end -->';
|
|
142
|
-
const content = readFileSync(
|
|
155
|
+
const content = readFileSync(readFrom, 'utf-8');
|
|
143
156
|
if (!content.includes(START) || !content.includes(END)) return;
|
|
144
157
|
|
|
145
158
|
const list = agents.length > 0
|
|
@@ -151,7 +164,7 @@ export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = []) {
|
|
|
151
164
|
new RegExp(`${START}[\\s\\S]*?${END}`),
|
|
152
165
|
block
|
|
153
166
|
);
|
|
154
|
-
|
|
167
|
+
fx.writeFile(claudeMdPath, updated);
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
/**
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map an agents.json ID to the Claude Code subagent_type.
|
|
3
|
-
*
|
|
4
|
-
* @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
|
|
5
|
-
* @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
|
|
6
|
-
* @returns {string} subagent_type matching .claude/agents/ filename stem
|
|
7
|
-
*/
|
|
8
|
-
export function agentIdToSubagentType(id, tier) {
|
|
9
|
-
const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
|
|
10
|
-
return `${prefix}${id}`;
|
|
11
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Map an agents.json ID to the Claude Code subagent_type.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
|
|
5
|
+
* @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
|
|
6
|
+
* @returns {string} subagent_type matching .claude/agents/ filename stem
|
|
7
|
+
*/
|
|
8
|
+
export function agentIdToSubagentType(id, tier) {
|
|
9
|
+
const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
|
|
10
|
+
return `${prefix}${id}`;
|
|
11
|
+
}
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
* Evaluator) are assumed by Claude itself per MORPH.md, not as separate agents.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { readFileSync,
|
|
24
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
26
|
import { shouldInstall } from '../lib/stack-filter.js';
|
|
27
|
+
import { realEffects } from '../lib/installers/effects.js';
|
|
27
28
|
|
|
28
29
|
/** Default tool scope for domain personas (override per agent via agents.json `tools`). */
|
|
29
30
|
export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
|
|
@@ -79,10 +80,12 @@ const MANAGED_HEADER =
|
|
|
79
80
|
* @param {string} frameworkDir - Path to morph-spec framework/ directory
|
|
80
81
|
* @param {object} options
|
|
81
82
|
* @param {string[]} [options.projectTags=[]] - Stack tags from config (e.g. ['dotnet', 'nextjs'])
|
|
83
|
+
* @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [options.fx]
|
|
84
|
+
* Coletor de efeitos; o default executa e registra numa lista descartada.
|
|
82
85
|
* @returns {Promise<{ installed: number }>}
|
|
83
86
|
*/
|
|
84
87
|
export async function installAgents(projectDir, frameworkDir = 'framework', options = {}) {
|
|
85
|
-
const { projectTags = [] } = options;
|
|
88
|
+
const { projectTags = [], fx = realEffects() } = options;
|
|
86
89
|
|
|
87
90
|
const agentsJsonPath = join(frameworkDir, 'agents.json');
|
|
88
91
|
const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
|
|
@@ -93,7 +96,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
93
96
|
.map(([id, data]) => ({ id, ...data }));
|
|
94
97
|
|
|
95
98
|
const targetDir = join(projectDir, '.claude', 'agents');
|
|
96
|
-
|
|
99
|
+
fx.ensureDir(targetDir);
|
|
97
100
|
|
|
98
101
|
const knownMcpIds = loadKnownMcpIds(frameworkDir);
|
|
99
102
|
|
|
@@ -111,7 +114,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
111
114
|
const filename = `morph-${agent.id}.md`;
|
|
112
115
|
const targetPath = join(targetDir, filename);
|
|
113
116
|
|
|
114
|
-
|
|
117
|
+
fx.writeFile(targetPath, renderAgentMd(agent, knownMcpIds));
|
|
115
118
|
installedFilenames.add(filename);
|
|
116
119
|
installedAgents.push({
|
|
117
120
|
id: agent.id,
|
|
@@ -122,10 +125,17 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
122
125
|
installed++;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
// Cleanup orphan morph-* files filtered out by stack
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
// Cleanup orphan morph-* files filtered out by stack.
|
|
129
|
+
//
|
|
130
|
+
// Calado em `dryRun` pelo mesmo motivo do laço gêmeo em skills-installer.js
|
|
131
|
+
// (D6): numa execução real do `update` este diretório acabou de ser apagado
|
|
132
|
+
// inteiro por `cleanFrameworkDirs()`, então o laço nunca remove nada. Num
|
|
133
|
+
// dry ingênuo ele veria o diretório antigo e preveria remoções inexistentes.
|
|
134
|
+
if (!fx.dryRun) {
|
|
135
|
+
for (const file of readdirSync(targetDir)) {
|
|
136
|
+
if (allPossible.has(file) && !installedFilenames.has(file)) {
|
|
137
|
+
fx.remove(join(targetDir, file));
|
|
138
|
+
}
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
141
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* The root CLAUDE.md belongs to the USER — morph-spec only adds its import.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { pathExists, readFile
|
|
11
|
+
import { pathExists, readFile } from './file-copier.js';
|
|
12
|
+
import { realEffects } from '../lib/installers/effects.js';
|
|
12
13
|
|
|
13
14
|
const MORPH_IMPORT = '@.claude/CLAUDE.md';
|
|
14
15
|
const MORPH_MARKER = 'MORPH-SPEC';
|
|
@@ -24,11 +25,15 @@ const MORPH_MARKER = 'MORPH-SPEC';
|
|
|
24
25
|
* Idempotent — does nothing if the import already exists.
|
|
25
26
|
*
|
|
26
27
|
* @param {string} claudeMdPath - Absolute path to root CLAUDE.md
|
|
28
|
+
* @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [fx]
|
|
29
|
+
* Coletor de efeitos; o default executa e registra numa lista descartada.
|
|
30
|
+
* A DECISÃO (criar / migrar / anexar / nada) sai de uma LEITURA, que
|
|
31
|
+
* acontece igual nos dois modos — por isso previsão e execução concordam.
|
|
27
32
|
* @returns {Promise<'created'|'migrated'|'injected'|'already-present'>}
|
|
28
33
|
*/
|
|
29
|
-
export async function injectMorphImport(claudeMdPath) {
|
|
34
|
+
export async function injectMorphImport(claudeMdPath, fx = realEffects()) {
|
|
30
35
|
if (!await pathExists(claudeMdPath)) {
|
|
31
|
-
|
|
36
|
+
fx.writeFile(claudeMdPath, `# Project\n\n${MORPH_IMPORT}\n`);
|
|
32
37
|
return 'created';
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -39,13 +44,13 @@ export async function injectMorphImport(claudeMdPath) {
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
if (isMorphOnly(content)) {
|
|
42
|
-
|
|
47
|
+
fx.writeFile(claudeMdPath, `# Project\n\n${MORPH_IMPORT}\n`);
|
|
43
48
|
return 'migrated';
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
// User has their own content — append the import section
|
|
47
52
|
const separator = content.endsWith('\n') ? '\n' : '\n\n';
|
|
48
|
-
|
|
53
|
+
fx.writeFile(claudeMdPath, `${content}${separator}## MORPH-SPEC\n\n${MORPH_IMPORT}\n`);
|
|
49
54
|
return 'injected';
|
|
50
55
|
}
|
|
51
56
|
|