@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
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* REPLICABLE — the primary root has the same relative path with identical
|
|
19
19
|
* content. Deleting the worktree's copy loses nothing; the
|
|
20
20
|
* original stays where it always was.
|
|
21
|
-
* DISPOSABLE — a well-known build artifact (`bin/`, `obj/`,
|
|
22
|
-
* `
|
|
23
|
-
* Reproducible from source, which is a
|
|
24
|
-
* backed up.
|
|
21
|
+
* DISPOSABLE — a well-known build artifact or cache (`bin/`, `obj/`,
|
|
22
|
+
* `node_modules/`, `.next/`, `TestResults/`, `graphify-out/`…,
|
|
23
|
+
* see DISPOSABLE_DIR_NAMES). Reproducible from source, which is a
|
|
24
|
+
* STRONGER guarantee than backed up.
|
|
25
25
|
* EXCLUSIVE — everything else: no counterpart in the root, or a counterpart
|
|
26
26
|
* whose content differs. These are the only bytes at stake, and
|
|
27
27
|
* the caller copies them to `finish-backup` before proceeding.
|
|
@@ -37,10 +37,16 @@ import { existsSync, lstatSync, readdirSync, readFileSync } from 'fs';
|
|
|
37
37
|
import { join } from 'path';
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Well-known, regenerable build artifacts. Deliberately narrow — do
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* backed up rather than assumed disposable.
|
|
40
|
+
* Well-known, regenerable build artifacts and caches. Deliberately narrow — do
|
|
41
|
+
* NOT add names a project might use for hand-written output (`dist/`,
|
|
42
|
+
* `build/`, `coverage/`); anything unlisted still reaches the EXCLUSIVE bucket,
|
|
43
|
+
* where it is backed up rather than assumed disposable. Each name here is a
|
|
44
|
+
* directory its own tool rewrites from scratch: `.next/` (next build/dev),
|
|
45
|
+
* `.turbo/`, `.pytest_cache/`, `TestResults/` (VSTest), `graphify-out/`
|
|
46
|
+
* (`morph-spec graph build`). `.next/` is also the one that makes a backup
|
|
47
|
+
* weigh hundreds of megabytes; `graphify-out/` is seeded into every worktree
|
|
48
|
+
* and is past the replica size cap (18 MB in a real project), so without its
|
|
49
|
+
* entry every `finish` would back it up.
|
|
44
50
|
*
|
|
45
51
|
* `.auth/` (Playwright storage state) belongs here for a reason worth stating:
|
|
46
52
|
* it holds a captured session, so it LOOKS like precious local state — but it
|
|
@@ -49,6 +55,7 @@ import { join } from 'path';
|
|
|
49
55
|
*/
|
|
50
56
|
const DISPOSABLE_DIR_NAMES = new Set([
|
|
51
57
|
'bin', 'obj', 'node_modules', '__pycache__', 'test-results', '.auth',
|
|
58
|
+
'.next', '.turbo', '.pytest_cache', 'TestResults', 'graphify-out',
|
|
52
59
|
]);
|
|
53
60
|
const DISPOSABLE_FILE_RE = /\.tsbuildinfo$/;
|
|
54
61
|
|
|
@@ -37,6 +37,7 @@ 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
39
|
import { auditDockerStacks } from './docker-audit.js';
|
|
40
|
+
import { auditFinishBackups } from './finish-backup.js';
|
|
40
41
|
|
|
41
42
|
const norm = (p) => resolve(p).replace(/\\/g, '/');
|
|
42
43
|
|
|
@@ -423,6 +424,10 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
|
|
|
423
424
|
}
|
|
424
425
|
issues.push(...auditDockerStacks(root, { exec, features: treeFeatures }));
|
|
425
426
|
|
|
427
|
+
// What a worktree removal leaves behind in `.git/morph-spec/finish-backup/`
|
|
428
|
+
// past its retention — `warn`, pruned by `--prune` and by the next finish.
|
|
429
|
+
issues.push(...auditFinishBackups(root));
|
|
430
|
+
|
|
426
431
|
return issues;
|
|
427
432
|
}
|
|
428
433
|
|
|
@@ -100,7 +100,7 @@ async function downloadPluginFiles(pluginName, sha, destDir) {
|
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
102
|
* Install a Claude Code plugin from GitHub.
|
|
103
|
-
* @param {string} pluginName - e.g. "
|
|
103
|
+
* @param {string} pluginName - e.g. "context7"
|
|
104
104
|
* @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
|
|
105
105
|
*/
|
|
106
106
|
export async function installPlugin(pluginName) {
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* setup-infra.js
|
|
3
3
|
*
|
|
4
4
|
* Headless infrastructure setup for MORPH-SPEC.
|
|
5
|
-
* Extracted from init.js (steps 1-10) so it can
|
|
6
|
-
*
|
|
7
|
-
* interactive prompts or stack detection.
|
|
5
|
+
* Extracted from init.js (steps 1-10) so it can run as
|
|
6
|
+
* `morph-spec setup-infra` without interactive prompts or stack detection.
|
|
8
7
|
*
|
|
9
8
|
* Usage:
|
|
10
9
|
* import { setupInfra } from './setup-infra.js';
|
|
@@ -38,7 +37,6 @@ import { realEffects } from '../lib/installers/effects.js';
|
|
|
38
37
|
const FRAMEWORK_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
|
|
39
38
|
|
|
40
39
|
const REQUIRED_PLUGINS = [
|
|
41
|
-
'superpowers@claude-plugins-official',
|
|
42
40
|
'context7@claude-plugins-official',
|
|
43
41
|
];
|
|
44
42
|
|
|
@@ -174,16 +172,23 @@ export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = [], optio
|
|
|
174
172
|
* @param {string} targetPath - Absolute path to the target project directory
|
|
175
173
|
* @param {object} options
|
|
176
174
|
* @param {Function} options._exec - exec function (for testing)
|
|
175
|
+
* @param {Function} [options._detectGraphifyVersion] - graphify probe (for testing: the
|
|
176
|
+
* step-18b branches differ with graphify installed or not, and CI has no Python)
|
|
177
|
+
* @param {Function} [options._log] - log sink (for testing: replaces stdout, which the test
|
|
178
|
+
* runner itself reports through — stubbing it swallows test results)
|
|
177
179
|
* @param {boolean} options.reinstall - If true, only re-install stack-aware assets (rules, skills, agents)
|
|
178
180
|
* @param {boolean} options.force - Bypass the secondary-worktree guard (see below). Callers
|
|
179
181
|
* (e.g. `initCommand`) that already performed their own equivalent check pass this so the
|
|
180
182
|
* check isn't redundantly re-applied here.
|
|
181
183
|
*/
|
|
182
|
-
export async function setupInfra(targetPath, {
|
|
184
|
+
export async function setupInfra(targetPath, {
|
|
185
|
+
_exec = execSync, _detectGraphifyVersion = null, _log = null, reinstall = false, force = false,
|
|
186
|
+
} = {}) {
|
|
183
187
|
const quiet = process.env.MORPH_QUIET === '1';
|
|
184
188
|
|
|
185
189
|
function log(msg) {
|
|
186
|
-
if (
|
|
190
|
+
if (_log) _log(msg);
|
|
191
|
+
else if (!quiet) process.stdout.write(msg + '\n');
|
|
187
192
|
}
|
|
188
193
|
|
|
189
194
|
// Guard: refuse a FULL install from inside a secondary (linked) worktree.
|
|
@@ -441,24 +446,26 @@ export async function setupInfra(targetPath, { _exec = execSync, reinstall = fal
|
|
|
441
446
|
// (setup-infra-cmd.js), então nada aqui pode escapar do try.
|
|
442
447
|
log('Step 18b: Codebase graph (graphify)...');
|
|
443
448
|
try {
|
|
444
|
-
const { writeGraphifyignore } = await import('../lib/graph/graphifyignore.js');
|
|
449
|
+
const { writeGraphifyignore, describeOutdatedIgnore } = await import('../lib/graph/graphifyignore.js');
|
|
445
450
|
const {
|
|
446
451
|
detectInstaller, installArgs, detectGraphifyVersion,
|
|
447
|
-
GRAPHIFY_VERSION,
|
|
452
|
+
GRAPHIFY_VERSION, GRAPHIFY_PACKAGE,
|
|
448
453
|
} = await import('../lib/graph/graphify-runtime.js');
|
|
449
454
|
const { ensureClaudeignore } = await import('../utils/file-copier.js');
|
|
450
455
|
const { scanDatabaseSchema } = await import('../lib/generators/architecture-scanner.js');
|
|
451
456
|
|
|
452
457
|
const ignore = writeGraphifyignore(targetPath, projectTags ?? []);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
458
|
+
if (ignore.migrated) log(' ✓ .graphifyignore atualizado (dublês, fixtures e helpers de teste no grafo)');
|
|
459
|
+
else if (ignore.written) log(' ✓ .graphifyignore (stack-aware)');
|
|
460
|
+
else log(' ✓ .graphifyignore já existia — preservado');
|
|
461
|
+
if (ignore.outdated) log(` ⚠ ${describeOutdatedIgnore(ignore.outdated)}`);
|
|
456
462
|
|
|
457
463
|
// Um graph.json de 12 MB reescrito a cada commit invalida o prompt cache.
|
|
458
464
|
await ensureClaudeignore(targetPath, ['graphify-out/']);
|
|
459
465
|
log(' ✓ graphify-out/ no .claudeignore');
|
|
460
466
|
|
|
461
|
-
const
|
|
467
|
+
const probe = _detectGraphifyVersion ?? detectGraphifyVersion;
|
|
468
|
+
const installed = probe();
|
|
462
469
|
if (installed === GRAPHIFY_VERSION) {
|
|
463
470
|
log(` ✓ graphify ${installed} já instalado`);
|
|
464
471
|
} else {
|
|
@@ -479,16 +486,18 @@ export async function setupInfra(targetPath, { _exec = execSync, reinstall = fal
|
|
|
479
486
|
}
|
|
480
487
|
}
|
|
481
488
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
489
|
+
// Nenhum hook do graphify: o refresh mora nos hooks do Claude (step 16 —
|
|
490
|
+
// graph-refresh-kick + SessionStart), que o `update` reinstala. O
|
|
491
|
+
// `graphify hook install` ficou de fora por três defeitos medidos no 0.9.50:
|
|
492
|
+
// sai sem fazer nada em worktree vinculada, não tem post-merge (merge e pull
|
|
493
|
+
// nunca rebuildam) e, num commit depois de um fast-forward, carimba o HEAD
|
|
494
|
+
// num grafo sem os arquivos do merge. E nunca `graphify claude install`:
|
|
495
|
+
// escreveria no CLAUDE.md e um PreToolUse próprio.
|
|
496
|
+
//
|
|
497
|
+
// A linha só sai com o graphify presente, e diz o que dispara de fato: um
|
|
498
|
+
// commit feito no terminal não dispara nada até o próximo desses.
|
|
499
|
+
if (probe()) {
|
|
500
|
+
log(' ✓ refresh automático do grafo da raiz: depois dos comandos git/gh/morph-spec do agente e no início de cada sessão');
|
|
492
501
|
}
|
|
493
502
|
} catch (err) {
|
|
494
503
|
log(` ⚠ Step do grafo pulado: ${err.message}`);
|
|
@@ -30,19 +30,229 @@ import { realEffects } from '../lib/installers/effects.js';
|
|
|
30
30
|
export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* A persona that can change files: its effective tool scope grants Write or Edit.
|
|
34
|
+
* Derived from the tools, so a persona made read-only stops being one.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} agent - agents.json entry
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
export function isImplementer(agent) {
|
|
40
|
+
const tools = String(agent?.tools ?? DEFAULT_AGENT_TOOLS).split(',').map((t) => t.trim());
|
|
41
|
+
return tools.includes('Write') || tools.includes('Edit');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Turn ceiling for a persona that omits agents.json `maxTurns` (ADR-004).
|
|
46
|
+
*
|
|
47
|
+
* 20 is the value the generator hardcoded before the field existed — the v4
|
|
48
|
+
* Tier-2 default (commit 93aad05f) that survived the v5 flattening with no
|
|
49
|
+
* recorded rationale. Keeping it as the default is what lets an old
|
|
50
|
+
* agents.json render byte-for-byte the same .md as before.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_MAX_TURNS = 20;
|
|
53
|
+
|
|
54
|
+
/** Share of the ceiling (percent) at which a sub-agent checkpoints (partial report). */
|
|
55
|
+
const CHECKPOINT_PERCENT = 70;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The ONE rule for a valid `maxTurns`. Four readers — installAgents,
|
|
59
|
+
* checkAgentsDrift, doctor's validateAgentsJson and validate-framework.js
|
|
60
|
+
* Check 6 — so the same agents.json can never pass in one and fail in another.
|
|
61
|
+
*
|
|
62
|
+
* Absent (`undefined`) is valid: it means "use the default". Anything present
|
|
63
|
+
* must be an integer >= 1. `null` is NOT absent — whoever wants the default
|
|
64
|
+
* removes the field.
|
|
65
|
+
*
|
|
66
|
+
* @param {unknown} value - the raw agents.json value
|
|
67
|
+
* @returns {string|null} what was expected, or null when valid
|
|
68
|
+
*/
|
|
69
|
+
export function maxTurnsProblem(value) {
|
|
70
|
+
if (value === undefined) return null;
|
|
71
|
+
return Number.isInteger(value) && value >= 1 ? null : 'esperado inteiro >= 1';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The full message for an invalid `maxTurns` — persona, value received, what
|
|
76
|
+
* is expected and how to get the default back. Every reader prints this one.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} id - persona id (agents.json key)
|
|
79
|
+
* @param {unknown} value - the raw agents.json value
|
|
80
|
+
* @returns {string|null} null when valid
|
|
81
|
+
*/
|
|
82
|
+
export function maxTurnsError(id, value) {
|
|
83
|
+
const problem = maxTurnsProblem(value);
|
|
84
|
+
if (!problem) return null;
|
|
85
|
+
const shown = typeof value === 'string' || (value !== null && typeof value === 'object')
|
|
86
|
+
? JSON.stringify(value)
|
|
87
|
+
: String(value);
|
|
88
|
+
return `agents.json: persona "${id}" tem maxTurns inválido (${shown}) — ${problem}. ` +
|
|
89
|
+
`Remova o campo para usar o default ${DEFAULT_MAX_TURNS}.`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the ceiling a persona is generated with: its `maxTurns`, or the
|
|
94
|
+
* default when absent. Throws `maxTurnsError()` on an invalid value.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} agent - agents.json entry with `id` merged in
|
|
97
|
+
* @returns {number}
|
|
98
|
+
*/
|
|
99
|
+
export function resolveMaxTurns(agent) {
|
|
100
|
+
const error = maxTurnsError(agent.id, agent.maxTurns);
|
|
101
|
+
if (error) throw new Error(error);
|
|
102
|
+
return agent.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A surface id is the key the mockup checklist names (`Superfície: admin-app`). */
|
|
106
|
+
const SURFACE_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
107
|
+
|
|
108
|
+
const SURFACE_KEYS = new Set(['when', 'standards']);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The ONE rule for a valid `surfaces` — the optional map that splits a
|
|
112
|
+
* persona's standards by the kind of screen it works on
|
|
113
|
+
* (`{ "admin-app": { when, standards: [ids] } }`). Same four readers as
|
|
114
|
+
* `maxTurnsProblem`, for the same reason.
|
|
115
|
+
*
|
|
116
|
+
* Absent (`undefined`) is valid: the persona reads only `standards`. A standard
|
|
117
|
+
* listed both in `standards` (read on every surface) and in a surface is
|
|
118
|
+
* refused: that is exactly how a "separated" persona keeps loading marketing
|
|
119
|
+
* standards on a CRUD screen without anyone noticing.
|
|
120
|
+
*
|
|
121
|
+
* @param {unknown} surfaces - the raw agents.json value
|
|
122
|
+
* @param {unknown} [standards] - the persona's universal `standards`
|
|
123
|
+
* @returns {string|null} what is wrong, or null when valid
|
|
124
|
+
*/
|
|
125
|
+
export function surfacesProblem(surfaces, standards = []) {
|
|
126
|
+
if (surfaces === undefined) return null;
|
|
127
|
+
if (surfaces === null || typeof surfaces !== 'object' || Array.isArray(surfaces)) {
|
|
128
|
+
return 'esperado objeto { "<superfície>": { when, standards } }';
|
|
129
|
+
}
|
|
130
|
+
const entries = Object.entries(surfaces);
|
|
131
|
+
if (entries.length === 0) return 'esperado ao menos uma superfície';
|
|
132
|
+
|
|
133
|
+
const universal = new Set(Array.isArray(standards) ? standards : []);
|
|
134
|
+
for (const [name, def] of entries) {
|
|
135
|
+
if (!SURFACE_ID_RE.test(name)) return `superfície "${name}" — o id é kebab-case (ex.: "admin-app")`;
|
|
136
|
+
if (def === null || typeof def !== 'object' || Array.isArray(def)) {
|
|
137
|
+
return `superfície "${name}" — esperado { when, standards }`;
|
|
138
|
+
}
|
|
139
|
+
const unknown = Object.keys(def).filter((k) => !SURFACE_KEYS.has(k));
|
|
140
|
+
if (unknown.length > 0) return `superfície "${name}" — campo desconhecido: ${unknown.join(', ')}`;
|
|
141
|
+
if (typeof def.when !== 'string' || def.when.trim() === '') {
|
|
142
|
+
return `superfície "${name}" — "when" precisa dizer que telas são dessa superfície`;
|
|
143
|
+
}
|
|
144
|
+
const ids = def.standards;
|
|
145
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.some((s) => typeof s !== 'string' || s.trim() === '')) {
|
|
146
|
+
return `superfície "${name}" — "standards" precisa ser uma lista não vazia de ids`;
|
|
147
|
+
}
|
|
148
|
+
const repeated = ids.filter((s, i) => ids.indexOf(s) !== i);
|
|
149
|
+
if (repeated.length > 0) return `superfície "${name}" repete ${[...new Set(repeated)].join(', ')}`;
|
|
150
|
+
const overlap = ids.filter((s) => universal.has(s));
|
|
151
|
+
if (overlap.length > 0) {
|
|
152
|
+
return `superfície "${name}" repete standard da lista universal (${overlap.join(', ')}) — ` +
|
|
153
|
+
'um standard é universal OU de uma superfície';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The full message for an invalid `surfaces`. Every reader prints this one.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} id - persona id (agents.json key)
|
|
163
|
+
* @param {unknown} surfaces
|
|
164
|
+
* @param {unknown} [standards]
|
|
165
|
+
* @returns {string|null} null when valid
|
|
166
|
+
*/
|
|
167
|
+
export function surfacesError(id, surfaces, standards) {
|
|
168
|
+
const problem = surfacesProblem(surfaces, standards);
|
|
169
|
+
if (!problem) return null;
|
|
170
|
+
return `agents.json: persona "${id}" tem surfaces inválido — ${problem}. ` +
|
|
171
|
+
'Remova o campo para a persona ler só `standards`.';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Every standard id a persona can cite: the universal list plus each
|
|
176
|
+
* surface's. Reference checks read this, so a surface id that does not exist
|
|
177
|
+
* fails like any other.
|
|
178
|
+
*
|
|
179
|
+
* @param {object} persona - agents.json entry
|
|
180
|
+
* @returns {string[]} unique ids, universal first
|
|
181
|
+
*/
|
|
182
|
+
export function personaStandardIds(persona) {
|
|
183
|
+
const ids = [...(Array.isArray(persona?.standards) ? persona.standards : [])];
|
|
184
|
+
const surfaces = persona?.surfaces;
|
|
185
|
+
if (surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces)) {
|
|
186
|
+
for (const def of Object.values(surfaces)) {
|
|
187
|
+
if (Array.isArray(def?.standards)) ids.push(...def.standards);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return [...new Set(ids)];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The first validity error of a persona entry, or null — what drift reports as `invalid`. */
|
|
194
|
+
function personaError(agent) {
|
|
195
|
+
return maxTurnsError(agent.id, agent.maxTurns) ?? surfacesError(agent.id, agent.surfaces, agent.standards);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Tools every persona gets on top of its scope. A `tools:` line is a strict
|
|
200
|
+
* allowlist: without `Skill` the sub-agent cannot invoke a single skill, and
|
|
201
|
+
* without `ToolSearch` it cannot load an MCP tool the session defers.
|
|
202
|
+
*/
|
|
203
|
+
export const PLATFORM_TOOLS = ['Skill', 'ToolSearch'];
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The server names an MCP registry entry is granted under. Claude Code names a
|
|
207
|
+
* server's tools after WHERE it is configured — `mcp__context7__*` from
|
|
208
|
+
* `.mcp.json`, `mcp__plugin_context7_context7__*` from the plugin — and a grant
|
|
209
|
+
* only matches its own spelling (measured 2026-09-24: `mcp__context7` gave a
|
|
210
|
+
* plugin-installed context7 nothing). So every spelling becomes a grant; one
|
|
211
|
+
* the project does not configure is inert.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} id - registry key
|
|
214
|
+
* @param {object} entry - registry entry
|
|
215
|
+
* @returns {string[]}
|
|
216
|
+
*/
|
|
217
|
+
export function mcpServerNames(id, entry) {
|
|
218
|
+
// A malformed list falls back to the id instead of breaking the render:
|
|
219
|
+
// installAgents and checkAgentsDrift both go through here (fail-open).
|
|
220
|
+
return serverNamesProblem(id, entry?.serverNames) === null ? entry?.serverNames ?? [id] : [id];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The ONE rule for a registry entry's `serverNames` (validate-framework Check 8
|
|
225
|
+
* reads it). Absent is valid (`[id]`). Present: non-empty, only the characters
|
|
226
|
+
* Claude Code keeps in a tool name, and it must include the id itself — that is
|
|
227
|
+
* the name the MCP installer writes into `.mcp.json`.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} id - registry key
|
|
230
|
+
* @param {unknown} value - the raw `serverNames`
|
|
231
|
+
* @returns {string|null} what was expected, or null when valid
|
|
232
|
+
*/
|
|
233
|
+
export function serverNamesProblem(id, value) {
|
|
234
|
+
if (value === undefined) return null;
|
|
235
|
+
if (!Array.isArray(value) || value.length === 0) return 'esperado array não vazio';
|
|
236
|
+
const bad = value.find(n => typeof n !== 'string' || !/^[A-Za-z0-9_-]+$/.test(n));
|
|
237
|
+
if (bad !== undefined) return `nome inválido ${JSON.stringify(bad)} (só [A-Za-z0-9_-])`;
|
|
238
|
+
return value.includes(id) ? null : `precisa incluir o próprio id "${id}"`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Load the MCP servers known to the framework registry
|
|
243
|
+
* (framework/mcp/registry.json) with the server names each is granted under.
|
|
244
|
+
* Only registry-known ids in agents.json `mcp` become tool grants (fail-open
|
|
245
|
+
* on any error).
|
|
36
246
|
*
|
|
37
247
|
* @param {string} frameworkDir
|
|
38
|
-
* @returns {
|
|
248
|
+
* @returns {Map<string, string[]>|null} id → server names; null = no registry found (accept nothing)
|
|
39
249
|
*/
|
|
40
250
|
export function loadKnownMcpIds(frameworkDir) {
|
|
41
251
|
try {
|
|
42
252
|
const registryPath = join(frameworkDir, 'mcp', 'registry.json');
|
|
43
253
|
if (!existsSync(registryPath)) return null;
|
|
44
254
|
const registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
|
|
45
|
-
return new
|
|
255
|
+
return new Map(Object.entries(registry.mcps ?? {}).map(([id, entry]) => [id, mcpServerNames(id, entry)]));
|
|
46
256
|
} catch {
|
|
47
257
|
return null;
|
|
48
258
|
}
|
|
@@ -95,6 +305,13 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
95
305
|
.filter(([key]) => !key.startsWith('_comment'))
|
|
96
306
|
.map(([id, data]) => ({ id, ...data }));
|
|
97
307
|
|
|
308
|
+
// Validate EVERY persona before the first write: refusing halfway through
|
|
309
|
+
// would leave some personas regenerated and others stale.
|
|
310
|
+
for (const agent of agents) {
|
|
311
|
+
const error = personaError(agent);
|
|
312
|
+
if (error) throw new Error(error);
|
|
313
|
+
}
|
|
314
|
+
|
|
98
315
|
const targetDir = join(projectDir, '.claude', 'agents');
|
|
99
316
|
fx.ensureDir(targetDir);
|
|
100
317
|
|
|
@@ -148,29 +365,35 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
|
|
|
148
365
|
* stale file differs byte-for-byte from this output).
|
|
149
366
|
*
|
|
150
367
|
* @param {object} agent - agents.json entry with `id` merged in
|
|
151
|
-
* @param {
|
|
152
|
-
* `mcp` entries outside
|
|
368
|
+
* @param {Map<string, string[]>|null} [knownMcpIds] - registry-known MCP ids and
|
|
369
|
+
* their server names; agents.json `mcp` entries outside it are ignored (null = ignore all)
|
|
153
370
|
* @returns {string} complete file content
|
|
154
371
|
*/
|
|
155
372
|
export function renderAgentMd(agent, knownMcpIds = null) {
|
|
156
373
|
const description = buildDescription(agent);
|
|
157
|
-
|
|
158
|
-
|
|
374
|
+
// Resolved ONCE: the frontmatter ceiling and the budget the body announces
|
|
375
|
+
// must be the same number — the sub-agent never sees its own frontmatter.
|
|
376
|
+
const maxTurns = resolveMaxTurns(agent);
|
|
377
|
+
const surfaces = surfacesError(agent.id, agent.surfaces, agent.standards);
|
|
378
|
+
if (surfaces) throw new Error(surfaces);
|
|
379
|
+
const frontmatter = buildFrontmatter(agent, description, knownMcpIds, maxTurns);
|
|
380
|
+
const body = buildBody(agent, maxTurns);
|
|
159
381
|
return `---\n${frontmatter}---\n\n${MANAGED_HEADER}\n\n${body}\n`;
|
|
160
382
|
}
|
|
161
383
|
|
|
162
384
|
function buildToolsLine(agent, knownMcpIds) {
|
|
163
|
-
const base = agent.tools ?? DEFAULT_AGENT_TOOLS;
|
|
385
|
+
const base = String(agent.tools ?? DEFAULT_AGENT_TOOLS).split(',').map(t => t.trim()).filter(Boolean);
|
|
386
|
+
const platform = PLATFORM_TOOLS.filter(t => !base.includes(t));
|
|
164
387
|
// Domain-relevant MCP servers (agents.json `mcp: [ids]`) become server-level
|
|
165
|
-
// grants (`mcp__{
|
|
166
|
-
// without that server configured the grant is inert.
|
|
388
|
+
// grants, one per server name (`mcp__{name}`). Only registry-known ids are
|
|
389
|
+
// emitted; in projects without that server configured the grant is inert.
|
|
167
390
|
const mcpGrants = (agent.mcp ?? [])
|
|
168
391
|
.filter(id => knownMcpIds?.has(id))
|
|
169
|
-
.
|
|
170
|
-
return [base, ...mcpGrants].join(', ');
|
|
392
|
+
.flatMap(id => knownMcpIds.get(id).map(name => `mcp__${name}`));
|
|
393
|
+
return [...base, ...platform, ...mcpGrants].join(', ');
|
|
171
394
|
}
|
|
172
395
|
|
|
173
|
-
function buildFrontmatter(agent, description, knownMcpIds) {
|
|
396
|
+
function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
|
|
174
397
|
const lines = [
|
|
175
398
|
// Canonical 1:1 naming: frontmatter name === filename === subagent_type.
|
|
176
399
|
`name: morph-${agent.id}`,
|
|
@@ -181,7 +404,10 @@ function buildFrontmatter(agent, description, knownMcpIds) {
|
|
|
181
404
|
// personas that omit the field.
|
|
182
405
|
`model: ${agent.model ?? 'inherit'}`,
|
|
183
406
|
`tools: ${buildToolsLine(agent, knownMcpIds)}`,
|
|
184
|
-
|
|
407
|
+
// Per persona in agents.json (ADR-004); absent → DEFAULT_MAX_TURNS. The
|
|
408
|
+
// line keeps its position so an agents.json without the field renders
|
|
409
|
+
// exactly what the hardcoded `maxTurns: 20` rendered.
|
|
410
|
+
`maxTurns: ${maxTurns}`,
|
|
185
411
|
];
|
|
186
412
|
if (agent.color) lines.push(`color: ${agent.color}`);
|
|
187
413
|
lines.push(`memory: project`, '');
|
|
@@ -195,7 +421,7 @@ function buildDescription(agent) {
|
|
|
195
421
|
return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
|
|
196
422
|
}
|
|
197
423
|
|
|
198
|
-
function buildBody(agent) {
|
|
424
|
+
function buildBody(agent, maxTurns) {
|
|
199
425
|
const lines = [
|
|
200
426
|
`# ${agent.title ?? agent.id}`,
|
|
201
427
|
'',
|
|
@@ -213,15 +439,51 @@ function buildBody(agent) {
|
|
|
213
439
|
lines.push('');
|
|
214
440
|
}
|
|
215
441
|
|
|
442
|
+
if (agent.surfaces) {
|
|
443
|
+
const read = agent.standards?.length ? 'Read the list above plus only' : 'Read only';
|
|
444
|
+
lines.push(
|
|
445
|
+
'## Standards by surface',
|
|
446
|
+
'',
|
|
447
|
+
'A screen is ONE surface — the mockup checklist names it (`Superfície`). ' +
|
|
448
|
+
`${read} the list of that screen's surface; never another surface's standards for that screen.`,
|
|
449
|
+
'',
|
|
450
|
+
);
|
|
451
|
+
for (const [name, def] of Object.entries(agent.surfaces)) {
|
|
452
|
+
lines.push(`### ${name}`, def.when, ...def.standards.map((s) => `- ${s}`), '');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
216
456
|
lines.push(
|
|
217
457
|
'## How you operate',
|
|
218
458
|
'',
|
|
219
459
|
'You are a domain persona dispatched per task. Read the spec/mandate/tasks/standards passed to you, do the work, and return a concise summary plus an auto-score 0-10. Refer to MORPH.md section 6 for the dispatch contract.',
|
|
460
|
+
'',
|
|
461
|
+
// Generic on purpose: this body reaches every dispatch (implement, apply,
|
|
462
|
+
// uiux, hotfix, review). The scout has no canonical file and no Write; the
|
|
463
|
+
// evaluator's file is evaluator-report.md. `PARCIAL` on line 1 is what
|
|
464
|
+
// report-check reads (verdict report-incomplete, action resume); "no
|
|
465
|
+
// score" keeps an evaluator partial from carrying a Composite-Score the
|
|
466
|
+
// gate would read as final.
|
|
467
|
+
'## Turn budget',
|
|
468
|
+
'',
|
|
469
|
+
// Pasted into EVERY dispatch of every persona — kept to the minimum that
|
|
470
|
+
// still carries the contract (test/framework/maf-expert-persona.test.js
|
|
471
|
+
// pins the rendered size).
|
|
472
|
+
`You have ${maxTurns} turns (\`maxTurns\`); at the ceiling you are cut off mid-work, without warning. ` +
|
|
473
|
+
'One response is one turn, however many parallel tool calls it makes. ' +
|
|
474
|
+
`By turn ${checkpointTurn(maxTurns)}, if your dispatch names a report file, save a partial there — ` +
|
|
475
|
+
'`PARCIAL` on line 1, done, remaining, files, no score (final report only) — then continue or stop, your call; the final report replaces it.',
|
|
220
476
|
);
|
|
221
477
|
|
|
222
478
|
return lines.join('\n');
|
|
223
479
|
}
|
|
224
480
|
|
|
481
|
+
/** The turn at which a sub-agent with `maxTurns` saves its partial report. */
|
|
482
|
+
function checkpointTurn(maxTurns) {
|
|
483
|
+
// Integer arithmetic: 0.7 × N in floating point is not always exact.
|
|
484
|
+
return Math.max(1, Math.floor((maxTurns * CHECKPOINT_PERCENT) / 100));
|
|
485
|
+
}
|
|
486
|
+
|
|
225
487
|
/**
|
|
226
488
|
* Compatibility shim for older callers that expected installDomainAgents.
|
|
227
489
|
* In v5 there is no separate "domain" install step — installAgents handles
|
|
@@ -279,19 +541,30 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
|
|
|
279
541
|
*
|
|
280
542
|
* - missing: eligible persona with no .md installed → run setup-infra --reinstall
|
|
281
543
|
* - stale: .md exists but differs from renderAgentMd() output (hand-edited
|
|
282
|
-
* or generated by an older CLI) → regenerate
|
|
544
|
+
* or generated by an older CLI) → regenerate. `staleFields` names
|
|
545
|
+
* what differs per file: the frontmatter keys, and `body`.
|
|
546
|
+
* - invalid: persona whose agents.json entry cannot be rendered (an invalid
|
|
547
|
+
* `maxTurns` or `surfaces`) — reported with the reason instead of throwing, so
|
|
548
|
+
* the doctor prints the cause and not "failed to compute"
|
|
283
549
|
* - orphans: morph-*.md files with no matching agents.json entry
|
|
284
550
|
*
|
|
285
551
|
* @param {string} projectDir
|
|
286
552
|
* @param {string} frameworkDir - directory containing agents.json
|
|
287
553
|
* @param {string[]} projectTags
|
|
288
|
-
* @
|
|
554
|
+
* @param {object} [options]
|
|
555
|
+
* @param {string} [options.mcpRegistryDir=frameworkDir] - where `mcp/registry.json`
|
|
556
|
+
* lives. It must be the directory the INSTALLER read: an installed project's
|
|
557
|
+
* `.morph/framework/` carries agents.json but no `mcp/`, so rendering against
|
|
558
|
+
* it drops every `mcp__*` grant and flags a pristine install as stale.
|
|
559
|
+
* @returns {{ missing: string[], stale: string[], staleFields: Object<string, string[]>,
|
|
560
|
+
* invalid: Array<{ file: string, reason: string }>, orphans: string[], expected: number }}
|
|
289
561
|
*/
|
|
290
|
-
export function checkAgentsDrift(projectDir, frameworkDir, projectTags = []) {
|
|
562
|
+
export function checkAgentsDrift(projectDir, frameworkDir, projectTags = [], options = {}) {
|
|
563
|
+
const { mcpRegistryDir = frameworkDir } = options;
|
|
291
564
|
const targetDir = join(projectDir, '.claude', 'agents');
|
|
292
565
|
const agentsJsonPath = join(frameworkDir, 'agents.json');
|
|
293
566
|
if (!existsSync(agentsJsonPath)) {
|
|
294
|
-
return { missing: [], stale: [], orphans: [], expected: 0 };
|
|
567
|
+
return { missing: [], stale: [], staleFields: {}, invalid: [], orphans: [], expected: 0 };
|
|
295
568
|
}
|
|
296
569
|
|
|
297
570
|
const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
|
|
@@ -299,24 +572,33 @@ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = []) {
|
|
|
299
572
|
.filter(([key]) => !key.startsWith('_comment'))
|
|
300
573
|
.map(([id, data]) => ({ id, ...data }));
|
|
301
574
|
|
|
302
|
-
const knownMcpIds = loadKnownMcpIds(
|
|
575
|
+
const knownMcpIds = loadKnownMcpIds(mcpRegistryDir);
|
|
303
576
|
const knownFilenames = new Set(agents.map(a => `morph-${a.id}.md`));
|
|
304
577
|
const missing = [];
|
|
305
578
|
const stale = [];
|
|
579
|
+
const staleFields = {};
|
|
580
|
+
const invalid = [];
|
|
306
581
|
let expected = 0;
|
|
307
582
|
|
|
308
583
|
for (const agent of agents) {
|
|
309
584
|
if (!agentEligible(agent, projectTags)) continue;
|
|
310
585
|
expected++;
|
|
311
586
|
const filename = `morph-${agent.id}.md`;
|
|
587
|
+
const error = personaError(agent);
|
|
588
|
+
if (error) {
|
|
589
|
+
invalid.push({ file: filename, reason: error });
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
312
592
|
const filePath = join(targetDir, filename);
|
|
313
593
|
if (!existsSync(filePath)) {
|
|
314
594
|
missing.push(filename);
|
|
315
595
|
continue;
|
|
316
596
|
}
|
|
317
597
|
const onDisk = readFileSync(filePath, 'utf-8').replace(/\r\n/g, '\n');
|
|
318
|
-
|
|
598
|
+
const rendered = renderAgentMd(agent, knownMcpIds);
|
|
599
|
+
if (onDisk !== rendered) {
|
|
319
600
|
stale.push(filename);
|
|
601
|
+
staleFields[filename] = diffAgentMd(onDisk, rendered);
|
|
320
602
|
}
|
|
321
603
|
}
|
|
322
604
|
|
|
@@ -329,7 +611,39 @@ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = []) {
|
|
|
329
611
|
}
|
|
330
612
|
}
|
|
331
613
|
|
|
332
|
-
return { missing, stale, orphans, expected };
|
|
614
|
+
return { missing, stale, staleFields, invalid, orphans, expected };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Split a generated .md into its frontmatter map and the rest.
|
|
619
|
+
* @returns {{ fields: Map<string, string>, body: string }}
|
|
620
|
+
*/
|
|
621
|
+
function splitAgentMd(content) {
|
|
622
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
623
|
+
if (!match) return { fields: new Map(), body: content };
|
|
624
|
+
const fields = new Map();
|
|
625
|
+
for (const line of match[1].split('\n')) {
|
|
626
|
+
const kv = line.match(/^([A-Za-z][\w-]*):\s?(.*)$/);
|
|
627
|
+
if (kv) fields.set(kv[1], kv[2]);
|
|
628
|
+
}
|
|
629
|
+
return { fields, body: match[2] };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Name what differs between a .md on disk and its render: the frontmatter
|
|
634
|
+
* keys whose value (or presence) differs, in render order, then `body`.
|
|
635
|
+
* A file with no parseable frontmatter differs in everything → `frontmatter`.
|
|
636
|
+
*/
|
|
637
|
+
function diffAgentMd(onDisk, rendered) {
|
|
638
|
+
const disk = splitAgentMd(onDisk);
|
|
639
|
+
const want = splitAgentMd(rendered);
|
|
640
|
+
if (disk.fields.size === 0) return disk.body === want.body ? ['frontmatter'] : ['frontmatter', 'body'];
|
|
641
|
+
const keys = [...new Set([...want.fields.keys(), ...disk.fields.keys()])];
|
|
642
|
+
const fields = keys.filter(k => disk.fields.get(k) !== want.fields.get(k));
|
|
643
|
+
if (disk.body !== want.body) fields.push('body');
|
|
644
|
+
// Bytes differ but no key/value does (e.g. a stray blank line between the
|
|
645
|
+
// fences): still drift, and it lives in the frontmatter.
|
|
646
|
+
return fields.length > 0 ? fields : ['frontmatter'];
|
|
333
647
|
}
|
|
334
648
|
|
|
335
649
|
/**
|