@polymorphism-tech/morph-spec 8.38.0 → 8.41.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 -9
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -3
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +43 -26
- package/framework/docs/CLI.md +7 -5
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/hooks/README.md +1 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
- package/framework/hooks/claude-code/core/worktree-create.js +22 -6
- package/framework/hooks/shared/git-paths.js +38 -5
- package/framework/mcp/registry.json +58 -66
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -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 +59 -25
- 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 +48 -24
- package/framework/skills/core/morph-media/SKILL.md +2 -2
- package/framework/skills/core/morph-plan/SKILL.md +10 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- 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 +7 -6
- package/framework/standards/PROVENANCE.json +4 -4
- package/framework/standards/STANDARDS.json +9 -9
- 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/architecture/code-comments.md +52 -16
- package/framework/standards/backend/dotnet/testing.md +4 -2
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
- package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
- package/framework/standards/frontend/nextjs/testing.md +4 -2
- package/framework/standards/integration/mcp/mcp-tools.md +64 -126
- 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/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/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/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +12 -6
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/commands/project/worktree.js +60 -4
- package/src/commands/state/gate-check.js +5 -0
- package/src/commands/tasks/dag.js +27 -4
- package/src/commands/validation/verify.js +22 -38
- 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 +59 -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/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 +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- 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 +13 -0
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/lib/worktree/env-allocator.js +9 -3
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +108 -13
- package/src/utils/banner.js +1 -1
- 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
|
@@ -64,21 +64,49 @@ export async function detectStack(targetPath) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Detect the project's integrations (`neon`, `stripe`...), same sources and
|
|
69
|
+
* order as detectStack. The detectors never fold them into the stack string, so
|
|
70
|
+
* without this an MCP scoped to an integration (neon, asaas) is never offered.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} targetPath
|
|
73
|
+
* @returns {Promise<string[]>}
|
|
74
|
+
*/
|
|
75
|
+
export async function detectIntegrations(targetPath) {
|
|
76
|
+
try {
|
|
77
|
+
const configPath = join(targetPath, '.morph', 'config', 'config.json');
|
|
78
|
+
if (existsSync(configPath)) {
|
|
79
|
+
const integrations = JSON.parse(readFileSync(configPath, 'utf-8')).project?.integrations;
|
|
80
|
+
if (Array.isArray(integrations)) return integrations;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Fall through to filesystem detection
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const results = await detectProject(targetPath, { conversation: false, generateStandards: false });
|
|
88
|
+
return results.structure?.integrations ?? [];
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
67
94
|
export async function mcpSetupCommand(name, options) {
|
|
68
95
|
const targetPath = process.cwd();
|
|
69
96
|
|
|
70
97
|
try {
|
|
71
98
|
// Detect current environment
|
|
72
99
|
const stack = await detectStack(targetPath);
|
|
100
|
+
const integrations = await detectIntegrations(targetPath);
|
|
73
101
|
const claudeConfig = await detectClaudeConfig(targetPath);
|
|
74
102
|
const existingMcps = claudeConfig.mcpServers || [];
|
|
75
103
|
|
|
76
104
|
if (options.list) {
|
|
77
|
-
return showStatusList(targetPath, stack, existingMcps);
|
|
105
|
+
return showStatusList(targetPath, stack, existingMcps, integrations);
|
|
78
106
|
}
|
|
79
107
|
|
|
80
108
|
if (options.auto) {
|
|
81
|
-
return autoInstall(targetPath, stack, existingMcps);
|
|
109
|
+
return autoInstall(targetPath, stack, existingMcps, integrations);
|
|
82
110
|
}
|
|
83
111
|
|
|
84
112
|
if (name) {
|
|
@@ -86,7 +114,7 @@ export async function mcpSetupCommand(name, options) {
|
|
|
86
114
|
}
|
|
87
115
|
|
|
88
116
|
// Interactive wizard
|
|
89
|
-
return interactiveWizard(targetPath, stack, existingMcps);
|
|
117
|
+
return interactiveWizard(targetPath, stack, existingMcps, integrations);
|
|
90
118
|
} catch (err) {
|
|
91
119
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
92
120
|
process.exit(1);
|
|
@@ -96,8 +124,8 @@ export async function mcpSetupCommand(name, options) {
|
|
|
96
124
|
/**
|
|
97
125
|
* Show MCP status list (--list)
|
|
98
126
|
*/
|
|
99
|
-
async function showStatusList(targetPath, stack, existingMcps) {
|
|
100
|
-
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps);
|
|
127
|
+
async function showStatusList(targetPath, stack, existingMcps, integrations) {
|
|
128
|
+
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations });
|
|
101
129
|
const statusRows = formatMcpStatusTable(orchestration, {});
|
|
102
130
|
|
|
103
131
|
console.log(chalk.cyan('\n MCP Server Status\n'));
|
|
@@ -130,8 +158,8 @@ async function showStatusList(targetPath, stack, existingMcps) {
|
|
|
130
158
|
/**
|
|
131
159
|
* Auto-install credential-free MCPs (--auto)
|
|
132
160
|
*/
|
|
133
|
-
async function autoInstall(targetPath, stack, existingMcps) {
|
|
134
|
-
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { autoOnly: true });
|
|
161
|
+
async function autoInstall(targetPath, stack, existingMcps, integrations) {
|
|
162
|
+
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { autoOnly: true, integrations });
|
|
135
163
|
const autoNames = Object.keys(orchestration.autoInstallable);
|
|
136
164
|
|
|
137
165
|
if (autoNames.length === 0) {
|
|
@@ -391,8 +419,8 @@ async function setupSpecificMcp(targetPath, name, stack, existingMcps) {
|
|
|
391
419
|
/**
|
|
392
420
|
* Interactive wizard showing all MCPs
|
|
393
421
|
*/
|
|
394
|
-
async function interactiveWizard(targetPath, stack, existingMcps) {
|
|
395
|
-
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps);
|
|
422
|
+
async function interactiveWizard(targetPath, stack, existingMcps, integrations) {
|
|
423
|
+
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations });
|
|
396
424
|
|
|
397
425
|
console.log(chalk.cyan('\n MCP Setup Wizard'));
|
|
398
426
|
console.log(chalk.gray(` Stack: ${stack}`));
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
getInstalledCLIVersion
|
|
17
17
|
} from '../../utils/version-checker.js';
|
|
18
18
|
import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
|
|
19
|
-
import { checkMissingAgents, checkAgentsDrift, maxTurnsError, surfacesError } from '../../utils/agents-installer.js';
|
|
19
|
+
import { checkMissingAgents, checkAgentsDrift, maxTurnsError, surfacesError, effortError } from '../../utils/agents-installer.js';
|
|
20
20
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
21
21
|
import { auditGraph } from '../../lib/graph/graph-health.js';
|
|
22
22
|
import { auditFeatureLifecycle } from '../../core/state/lifecycle-audit.js';
|
|
@@ -44,10 +44,10 @@ function normCwd(root, cwd) {
|
|
|
44
44
|
/**
|
|
45
45
|
* Check which required Claude Code plugins are installed.
|
|
46
46
|
* @param {Object} pluginsData - The `plugins` object from installed_plugins.json
|
|
47
|
-
* @returns {{
|
|
47
|
+
* @returns {{ context7: boolean }}
|
|
48
48
|
*/
|
|
49
49
|
export function checkInstalledPlugins(pluginsData) {
|
|
50
|
-
const REQUIRED = ['
|
|
50
|
+
const REQUIRED = ['context7'];
|
|
51
51
|
return Object.fromEntries(
|
|
52
52
|
REQUIRED.map(name => [name, `${name}@claude-plugins-official` in (pluginsData || {})])
|
|
53
53
|
);
|
|
@@ -208,6 +208,14 @@ export function validateAgentsJson(agentsData) {
|
|
|
208
208
|
? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
|
|
209
209
|
: { name: 'agents.json surfaces', status: 'ok' });
|
|
210
210
|
|
|
211
|
+
// 6: effort — same installer rule, same severity.
|
|
212
|
+
const effortErrors = agentEntries
|
|
213
|
+
.map(([id, data]) => effortError(id, data?.effort))
|
|
214
|
+
.filter(Boolean);
|
|
215
|
+
checks.push(effortErrors.length > 0
|
|
216
|
+
? { name: 'agents.json effort', status: 'error', msg: effortErrors.join(' | ') }
|
|
217
|
+
: { name: 'agents.json effort', status: 'ok' });
|
|
218
|
+
|
|
211
219
|
if (missingV5Fields.length > 0 && isV5) {
|
|
212
220
|
const sample = missingV5Fields.slice(0, 3).join(', ');
|
|
213
221
|
checks.push({
|
|
@@ -1330,15 +1338,13 @@ export async function doctorCommand(options = {}) {
|
|
|
1330
1338
|
} catch { /* non-fatal */ }
|
|
1331
1339
|
|
|
1332
1340
|
// ── Claude Code Plugins ──────────────────────────────────────────────────
|
|
1333
|
-
console.log(chalk.cyan('\n Claude Code Plugins (required
|
|
1341
|
+
console.log(chalk.cyan('\n Claude Code Plugins (required by morph-spec init)'));
|
|
1334
1342
|
const pluginsFile = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
1335
1343
|
let pluginData = { plugins: {} };
|
|
1336
1344
|
try {
|
|
1337
1345
|
pluginData = JSON.parse(await fs.readFile(pluginsFile, 'utf8'));
|
|
1338
1346
|
} catch { /* file may not exist */ }
|
|
1339
1347
|
const pluginStatus = checkInstalledPlugins(pluginData.plugins || {});
|
|
1340
|
-
check('superpowers plugin', pluginStatus.superpowers, false,
|
|
1341
|
-
'Run: morph-spec install-plugin superpowers');
|
|
1342
1348
|
check('context7 plugin', pluginStatus.context7, false,
|
|
1343
1349
|
'Run: morph-spec install-plugin context7');
|
|
1344
1350
|
} else {
|
|
@@ -34,6 +34,7 @@ import { installAgents, installDomainAgents } from '../../utils/agents-installer
|
|
|
34
34
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
35
35
|
import { updateCommand } from './update.js';
|
|
36
36
|
import { resolvePrimaryRoot } from './worktree.js';
|
|
37
|
+
import { registerProject } from '../../lib/projects/registry.js';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Guard against running a full `init` install from inside a secondary
|
|
@@ -135,8 +136,7 @@ export async function initCommand(options) {
|
|
|
135
136
|
try {
|
|
136
137
|
claudeConfigDetected = await detectClaudeConfig(targetPath);
|
|
137
138
|
const hasIntegrations = claudeConfigDetected.plugins.length > 0
|
|
138
|
-
|| claudeConfigDetected.mcpServers.length > 0
|
|
139
|
-
|| claudeConfigDetected.superpowers.installed;
|
|
139
|
+
|| claudeConfigDetected.mcpServers.length > 0;
|
|
140
140
|
|
|
141
141
|
if (hasIntegrations) {
|
|
142
142
|
spinner.stop();
|
|
@@ -287,7 +287,8 @@ export async function initCommand(options) {
|
|
|
287
287
|
try {
|
|
288
288
|
spinner.stop();
|
|
289
289
|
const existingMcps = claudeConfigDetected?.mcpServers || [];
|
|
290
|
-
const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps
|
|
290
|
+
const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps,
|
|
291
|
+
{ integrations: config.project?.integrations ?? [] });
|
|
291
292
|
|
|
292
293
|
const autoNames = Object.keys(orchestration.autoInstallable);
|
|
293
294
|
const manualNames = Object.keys(orchestration.needsManualSetup);
|
|
@@ -467,6 +468,8 @@ export async function initCommand(options) {
|
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
spinner.succeed('MORPH-SPEC installed successfully!');
|
|
471
|
+
// The dashboard's portfolio learns about the project here (fail-open).
|
|
472
|
+
registerProject(targetPath, 'init');
|
|
470
473
|
|
|
471
474
|
// Show next steps
|
|
472
475
|
logger.blank();
|
|
@@ -5,7 +5,7 @@ import { logger } from '../../utils/logger.js';
|
|
|
5
5
|
export async function installPluginCommand(pluginName, _options) {
|
|
6
6
|
if (!pluginName) {
|
|
7
7
|
logger.error('Plugin name required. Usage: morph-spec install-plugin <name>');
|
|
8
|
-
logger.dim(' Available:
|
|
8
|
+
logger.dim(' Available: context7');
|
|
9
9
|
process.exit(1);
|
|
10
10
|
}
|
|
11
11
|
try {
|
|
@@ -33,6 +33,7 @@ import { getProjectTags } from '../../lib/stack-filter.js';
|
|
|
33
33
|
import { updateClaudeMdAgentsBlock, installRulesForStack } from '../../scripts/setup-infra.js';
|
|
34
34
|
import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
|
|
35
35
|
import { createEffects } from '../../lib/installers/effects.js';
|
|
36
|
+
import { registerProject } from '../../lib/projects/registry.js';
|
|
36
37
|
import {
|
|
37
38
|
writeGraphifyignore, describeOutdatedIgnore, graphifyignoreTracked,
|
|
38
39
|
} from '../../lib/graph/graphifyignore.js';
|
|
@@ -758,6 +759,10 @@ export async function updateCommand(options = {}) {
|
|
|
758
759
|
logger.blank();
|
|
759
760
|
|
|
760
761
|
if (!dryRun) {
|
|
762
|
+
// The dashboard's portfolio learns about the project here (fail-open;
|
|
763
|
+
// outside the project, so not an effect of the update itself).
|
|
764
|
+
registerProject(targetPath, 'update');
|
|
765
|
+
|
|
761
766
|
// Show version change
|
|
762
767
|
if (projectCheck.current && projectCheck.current !== cliCheck.current) {
|
|
763
768
|
logger.success(`Updated: ${projectCheck.current} → ${cliCheck.current}`);
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { execSync, execFileSync } from 'child_process';
|
|
38
|
-
import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, lstatSync, unlinkSync, rmdirSync, cpSync } from 'fs';
|
|
38
|
+
import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, lstatSync, unlinkSync, rmdirSync, cpSync, copyFileSync, readdirSync } from 'fs';
|
|
39
39
|
import { join, resolve, dirname, basename } from 'path';
|
|
40
40
|
import chalk from 'chalk';
|
|
41
41
|
import { detectDefaultBranch } from '../../lib/git/default-branch.js';
|
|
@@ -612,6 +612,43 @@ export function buildTaskBranchName(name) {
|
|
|
612
612
|
return `morph-task/${name}`;
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
const ROOT_ENV_FILE_RE = /^\.env(\..+)?$/;
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Copy the root's `.env` / `.env.*` into a new tree: a sub-agent that builds
|
|
619
|
+
* or runs the app needs the same secrets, and they are never committed. Only
|
|
620
|
+
* a REAL file (never a link — nothing in the tree may reach the root through
|
|
621
|
+
* one), only when the tree lacks it (never overwrite), and only when git
|
|
622
|
+
* IGNORES it there: an untracked-but-visible copy would dirty the tree, and
|
|
623
|
+
* `finish`/`worktree remove --if-clean` refuse a dirty tree. At teardown the
|
|
624
|
+
* copy is byte-identical to the root's, which `triageIgnoredPaths` classifies
|
|
625
|
+
* as REPLICABLE — deleted with nothing lost.
|
|
626
|
+
*
|
|
627
|
+
* @param {string} worktreePath
|
|
628
|
+
* @param {string} root
|
|
629
|
+
* @returns {string[]} names copied
|
|
630
|
+
*/
|
|
631
|
+
export function copyRootEnvFiles(worktreePath, root) {
|
|
632
|
+
let names;
|
|
633
|
+
try {
|
|
634
|
+
names = readdirSync(root).filter((n) => ROOT_ENV_FILE_RE.test(n));
|
|
635
|
+
} catch {
|
|
636
|
+
return [];
|
|
637
|
+
}
|
|
638
|
+
const copied = [];
|
|
639
|
+
for (const name of names) {
|
|
640
|
+
const dst = join(worktreePath, name);
|
|
641
|
+
try {
|
|
642
|
+
if (!lstatSync(join(root, name)).isFile()) continue;
|
|
643
|
+
try { lstatSync(dst); continue; } catch { /* absent — not even a dangling link */ }
|
|
644
|
+
execFileSync('git', ['check-ignore', '-q', '--', name], { cwd: worktreePath, stdio: 'pipe' });
|
|
645
|
+
copyFileSync(join(root, name), dst);
|
|
646
|
+
copied.push(name);
|
|
647
|
+
} catch { /* not ignored, unreadable or unwritable — leave it out */ }
|
|
648
|
+
}
|
|
649
|
+
return copied;
|
|
650
|
+
}
|
|
651
|
+
|
|
615
652
|
/**
|
|
616
653
|
* Non-exiting core of `worktree provision` — the tree `claude -w`,
|
|
617
654
|
* `EnterWorktree` and `Agent isolation:"worktree"` ask for when the name is
|
|
@@ -668,7 +705,11 @@ export async function runWorktreeProvision(name, options = {}) {
|
|
|
668
705
|
execFileSync('git', ['worktree', 'add', worktreePath, branch], { cwd: root, stdio: 'pipe' });
|
|
669
706
|
|
|
670
707
|
setupInfraLinks(worktreePath, root);
|
|
671
|
-
const
|
|
708
|
+
const envFiles = copyRootEnvFiles(worktreePath, root);
|
|
709
|
+
// A tree born from `morph/<f>` works FOR that feature: MORPH_FEATURE says
|
|
710
|
+
// so, while the port block stays the task's own (`task/<name>`).
|
|
711
|
+
const forFeature = String(options.base || '').match(/^morph\/(.+)$/)?.[1] ?? null;
|
|
712
|
+
const envBlock = await allocateWorktreeEnv(worktreePath, root, `task/${name}`, { morphFeature: forFeature });
|
|
672
713
|
const materialized = await ensureLocalState(worktreePath);
|
|
673
714
|
const graph = seedWorktreeGraph(worktreePath, root);
|
|
674
715
|
|
|
@@ -681,6 +722,8 @@ export async function runWorktreeProvision(name, options = {}) {
|
|
|
681
722
|
branchedFrom,
|
|
682
723
|
kind: 'task-isolation',
|
|
683
724
|
name,
|
|
725
|
+
...(forFeature ? { feature: forFeature } : {}),
|
|
726
|
+
...(envFiles.length ? { envFiles } : {}),
|
|
684
727
|
materialized: materialized.materialized,
|
|
685
728
|
...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
|
|
686
729
|
...(graph.seeded ? { graph: 'seeded' } : {}),
|
|
@@ -797,12 +840,25 @@ export function envKeyForWorktree(worktreePath, root) {
|
|
|
797
840
|
* Shared by `setup` (feature key), `provision` (task key) and `link --all`
|
|
798
841
|
* (regeneration), so the manifest has exactly one writer.
|
|
799
842
|
*
|
|
843
|
+
* `morphFeature` (task trees born from `morph/<f>`) only renames the
|
|
844
|
+
* `MORPH_FEATURE` line. Left undefined — the `link --all` regeneration, which
|
|
845
|
+
* derives the key from the branch and cannot see the base — the value the
|
|
846
|
+
* file already carries is kept, so a repair never forgets the feature.
|
|
847
|
+
*
|
|
800
848
|
* @param {string} worktreePath
|
|
801
849
|
* @param {string} root - primary root
|
|
802
850
|
* @param {string} envKey - `<feature>` or `task/<name>`
|
|
851
|
+
* @param {{ morphFeature?: string|null }} [opts]
|
|
803
852
|
* @returns {Promise<{ index: number, base: number, range: [number, number] }|null>}
|
|
804
853
|
*/
|
|
805
|
-
export async function allocateWorktreeEnv(worktreePath, root, envKey) {
|
|
854
|
+
export async function allocateWorktreeEnv(worktreePath, root, envKey, { morphFeature } = {}) {
|
|
855
|
+
const envPath = join(worktreePath, '.morph', 'worktree.env');
|
|
856
|
+
if (morphFeature === undefined && envKey.startsWith('task/')) {
|
|
857
|
+
try {
|
|
858
|
+
const kept = readFileSync(envPath, 'utf8').match(/^MORPH_FEATURE=(.+)$/m)?.[1]?.trim();
|
|
859
|
+
morphFeature = kept && kept !== envKey ? kept : null;
|
|
860
|
+
} catch { morphFeature = null; }
|
|
861
|
+
}
|
|
806
862
|
try {
|
|
807
863
|
const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
|
|
808
864
|
await import('../../lib/worktree/env-allocator.js');
|
|
@@ -816,7 +872,7 @@ export async function allocateWorktreeEnv(worktreePath, root, envKey) {
|
|
|
816
872
|
stack = cfg?.project?.stack || '';
|
|
817
873
|
} catch { /* fail-open — stackless env still carries PORT */ }
|
|
818
874
|
mkdirSync(join(worktreePath, '.morph'), { recursive: true });
|
|
819
|
-
writeFileSync(
|
|
875
|
+
writeFileSync(envPath, buildEnvFileContent(envKey, block, stack, { morphFeature }), 'utf8');
|
|
820
876
|
excludeWorktreePaths(root, [join('.morph', 'worktree.env')]);
|
|
821
877
|
return block;
|
|
822
878
|
} catch {
|
|
@@ -9,6 +9,7 @@ import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-de
|
|
|
9
9
|
import { minimatch } from 'minimatch';
|
|
10
10
|
import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
|
|
11
11
|
import { readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
12
|
+
import { oversizedTasks } from '../../lib/tasks/tasks-json-parser.js';
|
|
12
13
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -196,6 +197,10 @@ function collectSignals(featureName, feature, cwd, gateName) {
|
|
|
196
197
|
designSystemDivergence: false,
|
|
197
198
|
};
|
|
198
199
|
|
|
200
|
+
// Fato de disco, fora de OVERRIDABLE_SIGNAL_KEYS: task que não cabe num
|
|
201
|
+
// dispatch se fatia no plano, não depois de um sub-agent bater no teto.
|
|
202
|
+
if (gateName === 'plan') signals.oversizedTasks = oversizedTasks({ tasks });
|
|
203
|
+
|
|
199
204
|
// Só onde o checklist decide algo: no fecho do design e no ship. Fato de
|
|
200
205
|
// disco, fora de OVERRIDABLE_SIGNAL_KEYS como `mutationProof`.
|
|
201
206
|
if (gateName === 'uiux' || gateName === 'review') {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { parseTasksJsonLenient } from '../../lib/tasks/tasks-json-parser.js';
|
|
4
|
+
import { parseTasksJsonLenient, oversizedTasks, MAX_TASK_OUTPUTS } from '../../lib/tasks/tasks-json-parser.js';
|
|
5
5
|
import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
|
|
6
6
|
import { renderMermaidFlowchart } from '../../lib/tasks/mermaid-renderer.js';
|
|
7
7
|
import { checkUseCases } from '../../lib/specs/usecase-check.js';
|
|
@@ -16,6 +16,15 @@ export function printLifecycleWarnings(lifecycle) {
|
|
|
16
16
|
for (const w of lifecycle.warnings) console.error(chalk.yellow(` ${w}`));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function planGateApproved(featureDir) {
|
|
20
|
+
try {
|
|
21
|
+
const feature = JSON.parse(readFileSync(join(featureDir, 'feature.json'), 'utf8'));
|
|
22
|
+
return feature?.approvalGates?.plan?.approved === true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
/**
|
|
20
29
|
* `morph-spec dag <feature>` — analyze the tasks.json DAG and print the
|
|
21
30
|
* sequential vs parallel dispatch decision, weighted by effort.
|
|
@@ -72,11 +81,17 @@ export async function dagCommand(featureName, options = {}) {
|
|
|
72
81
|
// revocation rows the finish would fail to apply. Warnings only.
|
|
73
82
|
const lifecycle = checkLifecycle({ cwd: process.cwd(), featureDir, tasksData: data });
|
|
74
83
|
|
|
84
|
+
// Task acima do teto não cabe num dispatch. Antes do Gate 2 é erro (o plano
|
|
85
|
+
// ainda fatia); depois é aviso, para não travar feature em voo.
|
|
86
|
+
const oversized = oversizedTasks(data);
|
|
87
|
+
const sizeBlocks = oversized.length > 0 && !planGateApproved(featureDir);
|
|
88
|
+
const taskSize = { max: MAX_TASK_OUTPUTS, oversized, blocking: sizeBlocks };
|
|
89
|
+
|
|
75
90
|
const plan = planDispatch(data);
|
|
76
91
|
|
|
77
92
|
if (options.json) {
|
|
78
|
-
console.log(JSON.stringify({ ...plan, useCases: ucCheck, artifactCriteria, lifecycle }, null, 2));
|
|
79
|
-
if (ucCheck.errors.length) process.exit(1);
|
|
93
|
+
console.log(JSON.stringify({ ...plan, useCases: ucCheck, artifactCriteria, lifecycle, taskSize }, null, 2));
|
|
94
|
+
if (ucCheck.errors.length || sizeBlocks) process.exit(1);
|
|
80
95
|
return;
|
|
81
96
|
}
|
|
82
97
|
|
|
@@ -88,6 +103,14 @@ export async function dagCommand(featureName, options = {}) {
|
|
|
88
103
|
console.error(chalk.gray('\n Corrija a referência no tasks.json ou restaure o use case.\n'));
|
|
89
104
|
process.exit(1);
|
|
90
105
|
}
|
|
106
|
+
if (oversized.length) {
|
|
107
|
+
const tone = sizeBlocks ? chalk.red : chalk.yellow;
|
|
108
|
+
const head = sizeBlocks ? '❌' : '⚠';
|
|
109
|
+
console.error(tone(`\n${head} Tamanho de task — ${plural(oversized.length, 'task', 'tasks')} acima de ${MAX_TASK_OUTPUTS} outputs`));
|
|
110
|
+
for (const t of oversized) console.error(tone(` ${t.id}: ${t.outputs} outputs`));
|
|
111
|
+
console.error(chalk.gray(' Fatie por pós-condição: uma task cabe num dispatch (morph-plan, "Tamanho da task").\n'));
|
|
112
|
+
if (sizeBlocks) process.exit(1);
|
|
113
|
+
}
|
|
91
114
|
if (ucCheck.warnings.length) {
|
|
92
115
|
console.error(chalk.yellow(`\n⚠ Use cases — ${plural(ucCheck.warnings.length, 'aviso', 'avisos')}`));
|
|
93
116
|
for (const w of ucCheck.warnings) console.error(chalk.yellow(` ${w}`));
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
3
|
-
import { resolve, join } from 'path';
|
|
2
|
+
import { resolve } from 'path';
|
|
4
3
|
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
5
4
|
import { runVerify, readTask, VerifyUsageError } from '../../lib/validators/verify-runner.js';
|
|
6
5
|
import { resolvePrimaryRoot } from '../project/worktree.js';
|
|
6
|
+
import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
|
|
7
|
+
import { canonicalPath } from '../../lib/tasks/test-runner.js';
|
|
7
8
|
import { getHeadSha } from '../../lib/git/default-branch.js';
|
|
8
9
|
import { EVAL_RUNNER } from '../../lib/tasks/eval-node.js';
|
|
9
10
|
import { buildVerifyEvent, recordVerifyEvent, findRepeatedVerify } from '../../lib/telemetry/verify-event.js';
|
|
@@ -93,25 +94,25 @@ function formatVerifyResult(result) {
|
|
|
93
94
|
return lines.join('\n');
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
const samePath = (a, b) => canonicalPath(a).replace(/\\/g, '/') === canonicalPath(b).replace(/\\/g, '/');
|
|
98
|
+
|
|
96
99
|
/**
|
|
97
|
-
*
|
|
100
|
+
* Persist only in the tree that OWNS the feature — the same predicate
|
|
101
|
+
* `list`/`handoff`/the mutator guard read (`resolveFeatureTree`), asked from
|
|
102
|
+
* the PRIMARY ROOT so every tree gets the same answer. Any other tree (a
|
|
103
|
+
* sub-agent's `morph-task/*` tree branched from `morph/{f}`, a scratch
|
|
104
|
+
* checkout) must never write feature.json: its copy is not the one the gates
|
|
105
|
+
* read, and the write would merge-conflict with the owner's.
|
|
98
106
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
107
|
+
* The old rule read only the root's `worktree.path` pointer, which a feature
|
|
108
|
+
* created INSIDE its worktree never has: a `worktrees/homolog-v2` on
|
|
109
|
+
* `morph/chat-demo-simulacao` measured `unregistered-worktree` on 11/11 runs,
|
|
110
|
+
* and `repeatOf` (computed only when persisting) never fired. Git's worktree
|
|
111
|
+
* registry names that tree as the owner.
|
|
104
112
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* it never touches the worktree's own branch-local copy. So this must read
|
|
109
|
-
* the ROOT's feature.json to find the registration, never the cwd-local one
|
|
110
|
-
* (`getFeature(featureName)` resolves relative to `process.cwd()`, which
|
|
111
|
-
* inside a worktree is the branch-local copy and will never carry
|
|
112
|
-
* `.worktree` — reading it here would make this branch permanently dead code
|
|
113
|
-
* for the exact case it exists to handle: verify running from inside a
|
|
114
|
-
* registered worktree).
|
|
113
|
+
* The root is not special: with a live worktree owning the feature, the
|
|
114
|
+
* root's copy is the birth-instant skeleton and a stamp there is drift —
|
|
115
|
+
* `owner-tree-elsewhere`, the verify side of `assertOwnerTree`.
|
|
115
116
|
*
|
|
116
117
|
* @param {{ cwd: string, featureName: string, noPersist: boolean }} params
|
|
117
118
|
* @returns {{ persist: boolean, reason: string|null }}
|
|
@@ -120,26 +121,9 @@ function decidePersistence({ cwd, featureName, noPersist }) {
|
|
|
120
121
|
if (noPersist) return { persist: false, reason: 'no-persist-flag' };
|
|
121
122
|
|
|
122
123
|
const root = resolvePrimaryRoot(cwd);
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// cwd is (or looks like) a worktree — consult the ROOT's feature.json for
|
|
128
|
-
// the registration. Fail-open: an unreadable/missing root feature.json
|
|
129
|
-
// means we can't confirm registration, so treat as unregistered.
|
|
130
|
-
let registeredWt = null;
|
|
131
|
-
try {
|
|
132
|
-
const rootFeatureJsonPath = join(root, '.morph', 'features', featureName, 'feature.json');
|
|
133
|
-
const rootFeature = JSON.parse(readFileSync(rootFeatureJsonPath, 'utf8'));
|
|
134
|
-
registeredWt = rootFeature.worktree?.path
|
|
135
|
-
? String(rootFeature.worktree.path).replace(/\\/g, '/')
|
|
136
|
-
: null;
|
|
137
|
-
} catch {
|
|
138
|
-
registeredWt = null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (registeredWt && normalizedCwd === registeredWt) return { persist: true, reason: null };
|
|
142
|
-
return { persist: false, reason: 'unregistered-worktree' };
|
|
124
|
+
const owner = resolveFeatureTree(featureName, root);
|
|
125
|
+
if (owner.tree && samePath(owner.tree, cwd)) return { persist: true, reason: null };
|
|
126
|
+
return { persist: false, reason: samePath(root, cwd) ? 'owner-tree-elsewhere' : 'unregistered-worktree' };
|
|
143
127
|
}
|
|
144
128
|
|
|
145
129
|
/**
|
|
@@ -1,28 +1,92 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Routes, Route, Navigate, useLocation, useParams } from 'react-router-dom';
|
|
3
|
+
import Sidebar, { MobileNav } from './components/Sidebar.jsx';
|
|
4
|
+
import ErrorBoundary from './components/ErrorBoundary.jsx';
|
|
5
|
+
import CommandPalette from './components/CommandPalette.jsx';
|
|
6
|
+
import Portfolio from './pages/Portfolio.jsx';
|
|
7
|
+
import Features, { isWorking } from './pages/Features.jsx';
|
|
8
|
+
import FeatureDetail from './pages/FeatureDetail.jsx';
|
|
9
|
+
import Activity from './pages/Activity.jsx';
|
|
3
10
|
import AgentMap from './pages/AgentMap.jsx';
|
|
4
11
|
import PhasePipeline from './pages/PhasePipeline.jsx';
|
|
5
12
|
import HookInspector from './pages/HookInspector.jsx';
|
|
6
13
|
import SkillsLibrary from './pages/SkillsLibrary.jsx';
|
|
7
14
|
import RulesMap from './pages/RulesMap.jsx';
|
|
8
15
|
import Standards from './pages/Standards.jsx';
|
|
9
|
-
import
|
|
16
|
+
import Guide from './pages/Guide.jsx';
|
|
17
|
+
import Rubrics from './pages/Rubrics.jsx';
|
|
18
|
+
import { useNow } from './hooks/usePref.js';
|
|
19
|
+
import { useProjects, useCurrentProject, projectHref } from './lib/project.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What the sidebar and the tab title say at a glance, from the portfolio:
|
|
23
|
+
* per project, how many features are active, waiting on you and being worked
|
|
24
|
+
* on right now.
|
|
25
|
+
*/
|
|
26
|
+
function usePulse(portfolio) {
|
|
27
|
+
const now = useNow();
|
|
28
|
+
const byProject = {};
|
|
29
|
+
for (const p of portfolio?.projects || []) {
|
|
30
|
+
const s = p.summary;
|
|
31
|
+
byProject[p.id] = s
|
|
32
|
+
? { active: s.active, awaiting: s.awaiting, working: s.activeFeatures.filter((f) => isWorking(f, now)).length }
|
|
33
|
+
: null;
|
|
34
|
+
}
|
|
35
|
+
const awaiting = Object.values(byProject).reduce((n, x) => n + (x?.awaiting || 0), 0);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
document.title = awaiting ? `(${awaiting}) morph-spec` : 'morph-spec';
|
|
38
|
+
}, [awaiting]);
|
|
39
|
+
return { byProject, awaiting };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ProjectIndex() {
|
|
43
|
+
const { project } = useParams();
|
|
44
|
+
return <Navigate to={projectHref(project)} replace />;
|
|
45
|
+
}
|
|
10
46
|
|
|
11
47
|
export default function App() {
|
|
48
|
+
const { pathname } = useLocation();
|
|
49
|
+
const { data: portfolio } = useProjects();
|
|
50
|
+
const current = useCurrentProject(portfolio);
|
|
51
|
+
const pulse = usePulse(portfolio);
|
|
52
|
+
const [palette, setPalette] = useState(false);
|
|
53
|
+
// Ctrl+K / Cmd+K opens the search from anywhere.
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const onKey = (e) => {
|
|
56
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setPalette((o) => !o); }
|
|
57
|
+
};
|
|
58
|
+
window.addEventListener('keydown', onKey);
|
|
59
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
60
|
+
}, []);
|
|
61
|
+
const nav = { portfolio, current: current.id, pulse, onSearch: () => setPalette(true) };
|
|
12
62
|
return (
|
|
13
|
-
<div className="flex h-
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
63
|
+
<div className="flex h-full overflow-hidden">
|
|
64
|
+
<div className="ambient" aria-hidden="true" />
|
|
65
|
+
<Sidebar {...nav} />
|
|
66
|
+
<CommandPalette open={palette} onClose={() => setPalette(false)} portfolio={portfolio} project={current.id} />
|
|
67
|
+
<main className="relative z-10 flex-1 overflow-auto">
|
|
68
|
+
<MobileNav {...nav} />
|
|
69
|
+
{/* Inside <main>, keyed by route: a page that throws leaves the
|
|
70
|
+
sidebar alive, and navigating away remounts a clean boundary. */}
|
|
71
|
+
<ErrorBoundary key={pathname} page={pathname}>
|
|
72
|
+
<Routes>
|
|
73
|
+
<Route path="/" element={<Portfolio />} />
|
|
74
|
+
<Route path="/p/:project" element={<ProjectIndex />} />
|
|
75
|
+
<Route path="/p/:project/features" element={<Features />} />
|
|
76
|
+
<Route path="/p/:project/features/:name" element={<FeatureDetail />} />
|
|
77
|
+
<Route path="/p/:project/activity" element={<Activity />} />
|
|
78
|
+
<Route path="/guide" element={<Guide />} />
|
|
79
|
+
<Route path="/guide/:doc" element={<Guide />} />
|
|
80
|
+
<Route path="/rubrics" element={<Rubrics />} />
|
|
81
|
+
<Route path="/agents" element={<AgentMap />} />
|
|
82
|
+
<Route path="/phases" element={<PhasePipeline />} />
|
|
83
|
+
<Route path="/hooks" element={<HookInspector />} />
|
|
84
|
+
<Route path="/skills" element={<SkillsLibrary />} />
|
|
85
|
+
<Route path="/rules" element={<RulesMap />} />
|
|
86
|
+
<Route path="/standards" element={<Standards />} />
|
|
87
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
88
|
+
</Routes>
|
|
89
|
+
</ErrorBoundary>
|
|
26
90
|
</main>
|
|
27
91
|
</div>
|
|
28
92
|
);
|