@polymorphism-tech/morph-spec 8.38.0 → 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 +19 -8
- 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 +3 -2
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +36 -25
- package/framework/docs/CLI.md +2 -2
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/mcp/registry.json +33 -2
- 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 +5 -1
- 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 +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +6 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +4 -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 +6 -5
- package/framework/standards/STANDARDS.json +3 -3
- 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/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- 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/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 +3 -5
- 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/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/validators/code-budget/code-budget-validator.js +32 -4
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +57 -12
- 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
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase State Machine - Enforces valid phase transitions
|
|
3
|
-
*
|
|
4
|
-
* Prevents invalid workflow jumps and ensures sequential phase progression.
|
|
5
|
-
* Based on the MORPH 5-phase workflow.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Valid phase transitions map
|
|
10
|
-
* Each phase can only transition to specific next phases
|
|
11
|
-
*/
|
|
12
|
-
const VALID_TRANSITIONS = {
|
|
13
|
-
'proposal': ['uiux', 'plan'],
|
|
14
|
-
'uiux': ['plan'],
|
|
15
|
-
'plan': ['implement'],
|
|
16
|
-
'implement': ['review'],
|
|
17
|
-
'review': []
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Phase display names for error messages
|
|
22
|
-
*/
|
|
23
|
-
const PHASE_NAMES = {
|
|
24
|
-
'proposal': 'Proposal (Phase 0)',
|
|
25
|
-
'uiux': 'UI/UX Design (Phase 1)',
|
|
26
|
-
'plan': 'Plan (Phase 2)',
|
|
27
|
-
'implement': 'Implement (Phase 3)',
|
|
28
|
-
'review': 'Quality Review (Phase 4)'
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Optional phases that can be skipped
|
|
33
|
-
*/
|
|
34
|
-
const OPTIONAL_PHASES = ['uiux'];
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Validate if a phase transition is allowed
|
|
38
|
-
* @param {string} fromPhase - Current phase
|
|
39
|
-
* @param {string} toPhase - Target phase
|
|
40
|
-
* @returns {boolean} True if transition is valid
|
|
41
|
-
*/
|
|
42
|
-
export function isValidTransition(fromPhase, toPhase) {
|
|
43
|
-
if (!fromPhase || !toPhase) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const validNextPhases = VALID_TRANSITIONS[fromPhase];
|
|
48
|
-
return validNextPhases ? validNextPhases.includes(toPhase) : false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get list of valid next phases for a given phase
|
|
53
|
-
* @param {string} currentPhase - Current phase
|
|
54
|
-
* @returns {string[]} Array of valid next phase names
|
|
55
|
-
*/
|
|
56
|
-
export function getValidNextPhases(currentPhase) {
|
|
57
|
-
return VALID_TRANSITIONS[currentPhase] || [];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get display name for a phase
|
|
62
|
-
* @param {string} phase - Phase identifier
|
|
63
|
-
* @returns {string} Human-readable phase name
|
|
64
|
-
*/
|
|
65
|
-
export function getPhaseDisplayName(phase) {
|
|
66
|
-
return PHASE_NAMES[phase] || phase;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Check if a phase is optional
|
|
71
|
-
* @param {string} phase - Phase identifier
|
|
72
|
-
* @returns {boolean} True if phase is optional
|
|
73
|
-
*/
|
|
74
|
-
export function isOptionalPhase(phase) {
|
|
75
|
-
return OPTIONAL_PHASES.includes(phase);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Validate transition and throw error if invalid
|
|
80
|
-
* @param {string} fromPhase - Current phase
|
|
81
|
-
* @param {string} toPhase - Target phase
|
|
82
|
-
* @throws {Error} If transition is invalid
|
|
83
|
-
*/
|
|
84
|
-
export function validateTransition(fromPhase, toPhase) {
|
|
85
|
-
if (!fromPhase) {
|
|
86
|
-
throw new Error('Current phase is undefined. Cannot validate transition.');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!toPhase) {
|
|
90
|
-
throw new Error('Target phase is undefined. Cannot validate transition.');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!isValidTransition(fromPhase, toPhase)) {
|
|
94
|
-
const validPhases = getValidNextPhases(fromPhase);
|
|
95
|
-
const fromDisplay = getPhaseDisplayName(fromPhase);
|
|
96
|
-
const toDisplay = getPhaseDisplayName(toPhase);
|
|
97
|
-
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Invalid phase transition: ${fromDisplay} → ${toDisplay}\n\n` +
|
|
100
|
-
`Valid next phases from ${fromDisplay}:\n` +
|
|
101
|
-
validPhases.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
|
|
102
|
-
'\n\n' +
|
|
103
|
-
'You cannot skip phases in the MORPH workflow.\n' +
|
|
104
|
-
'To override this check, use the --force flag (not recommended).'
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get the sequential order of phases
|
|
111
|
-
* @returns {string[]} Ordered list of phases
|
|
112
|
-
*/
|
|
113
|
-
export function getPhaseSequence() {
|
|
114
|
-
return ['proposal', 'uiux', 'plan', 'implement', 'review'];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Get phase order index (for comparison)
|
|
119
|
-
* @param {string} phase - Phase identifier
|
|
120
|
-
* @returns {number} Index in sequence (-1 if not found)
|
|
121
|
-
*/
|
|
122
|
-
export function getPhaseOrder(phase) {
|
|
123
|
-
return getPhaseSequence().indexOf(phase);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Check if target phase is ahead of current phase
|
|
128
|
-
* @param {string} currentPhase - Current phase
|
|
129
|
-
* @param {string} targetPhase - Target phase
|
|
130
|
-
* @returns {boolean} True if target is ahead
|
|
131
|
-
*/
|
|
132
|
-
export function isPhaseAhead(currentPhase, targetPhase) {
|
|
133
|
-
return getPhaseOrder(targetPhase) > getPhaseOrder(currentPhase);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Check if target phase is behind current phase (rollback scenario)
|
|
138
|
-
* @param {string} currentPhase - Current phase
|
|
139
|
-
* @param {string} targetPhase - Target phase
|
|
140
|
-
* @returns {boolean} True if target is behind
|
|
141
|
-
*/
|
|
142
|
-
export function isPhaseRollback(currentPhase, targetPhase) {
|
|
143
|
-
return getPhaseOrder(targetPhase) < getPhaseOrder(currentPhase);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get all skipped phases between two phases
|
|
148
|
-
* @param {string} fromPhase - Starting phase
|
|
149
|
-
* @param {string} toPhase - Ending phase
|
|
150
|
-
* @returns {string[]} Array of skipped phases
|
|
151
|
-
*/
|
|
152
|
-
export function getSkippedPhases(fromPhase, toPhase) {
|
|
153
|
-
const sequence = getPhaseSequence();
|
|
154
|
-
const fromIndex = sequence.indexOf(fromPhase);
|
|
155
|
-
const toIndex = sequence.indexOf(toPhase);
|
|
156
|
-
|
|
157
|
-
if (fromIndex === -1 || toIndex === -1 || toIndex <= fromIndex) {
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return sequence.slice(fromIndex + 1, toIndex);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Validate that no required phases were skipped
|
|
166
|
-
* @param {string} fromPhase - Starting phase
|
|
167
|
-
* @param {string} toPhase - Ending phase
|
|
168
|
-
* @throws {Error} If required phases were skipped
|
|
169
|
-
*/
|
|
170
|
-
export function validateNoRequiredPhasesSkipped(fromPhase, toPhase) {
|
|
171
|
-
const skipped = getSkippedPhases(fromPhase, toPhase);
|
|
172
|
-
const requiredSkipped = skipped.filter(p => !isOptionalPhase(p));
|
|
173
|
-
|
|
174
|
-
if (requiredSkipped.length > 0) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Cannot skip required phases:\n` +
|
|
177
|
-
requiredSkipped.map(p => ` • ${getPhaseDisplayName(p)}`).join('\n') +
|
|
178
|
-
'\n\nYou must complete each required phase in sequence.'
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Get comprehensive phase information
|
|
185
|
-
* @param {string} phase - Phase identifier
|
|
186
|
-
* @returns {Object} Phase metadata
|
|
187
|
-
*/
|
|
188
|
-
export function getPhaseInfo(phase) {
|
|
189
|
-
return {
|
|
190
|
-
id: phase,
|
|
191
|
-
displayName: getPhaseDisplayName(phase),
|
|
192
|
-
order: getPhaseOrder(phase),
|
|
193
|
-
isOptional: isOptionalPhase(phase),
|
|
194
|
-
validNextPhases: getValidNextPhases(phase),
|
|
195
|
-
canTransitionTo: (targetPhase) => isValidTransition(phase, targetPhase)
|
|
196
|
-
};
|
|
197
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
2
|
-
|
|
3
|
-
export function useDetailPanel() {
|
|
4
|
-
const [panel, setPanel] = useState({ open: false, title: '', content: null });
|
|
5
|
-
|
|
6
|
-
const openPanel = useCallback((title, content) => {
|
|
7
|
-
setPanel({ open: true, title, content });
|
|
8
|
-
}, []);
|
|
9
|
-
|
|
10
|
-
const closePanel = useCallback(() => {
|
|
11
|
-
setPanel((prev) => ({ ...prev, open: false }));
|
|
12
|
-
}, []);
|
|
13
|
-
|
|
14
|
-
return { panel, openPanel, closePanel };
|
|
15
|
-
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import morphData from 'virtual:morph-data';
|
|
2
|
-
|
|
3
|
-
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
const PHASE_ORDER = ['proposal', 'uiux', 'plan', 'implement', 'review'];
|
|
6
|
-
|
|
7
|
-
const STATUS_BADGE = {
|
|
8
|
-
in_progress: { label: 'In Progress', bg: 'bg-blue-900/60', border: 'border-blue-600', text: 'text-blue-300' },
|
|
9
|
-
completed: { label: 'Completed', bg: 'bg-green-900/60', border: 'border-green-600', text: 'text-green-300' },
|
|
10
|
-
draft: { label: 'Draft', bg: 'bg-gray-800', border: 'border-gray-600', text: 'text-gray-400' },
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const TASK_STATUS_ICON = {
|
|
14
|
-
completed: { icon: '✓', color: 'text-green-400' },
|
|
15
|
-
in_progress: { icon: '►', color: 'text-blue-400' },
|
|
16
|
-
pending: { icon: '○', color: 'text-gray-600' },
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// ─── Sub Components ───────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
function StatusBadge({ status }) {
|
|
22
|
-
const s = STATUS_BADGE[status] || STATUS_BADGE.draft;
|
|
23
|
-
return (
|
|
24
|
-
<span className={`px-2 py-0.5 rounded text-xs font-medium border ${s.bg} ${s.border} ${s.text}`}>
|
|
25
|
-
{s.label}
|
|
26
|
-
</span>
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function PhaseProgressBar({ currentPhase, approvals }) {
|
|
31
|
-
const approvedSet = new Set(Object.keys(approvals || {}));
|
|
32
|
-
const currentIdx = PHASE_ORDER.indexOf(currentPhase);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<div className="flex gap-1 mt-2">
|
|
36
|
-
{PHASE_ORDER.map((phase, idx) => {
|
|
37
|
-
const isCurrent = phase === currentPhase;
|
|
38
|
-
const isApproved = approvedSet.has(phase);
|
|
39
|
-
const isPast = idx < currentIdx && !isCurrent;
|
|
40
|
-
const isFuture = idx > currentIdx;
|
|
41
|
-
|
|
42
|
-
let bg, text, extra;
|
|
43
|
-
if (isApproved) {
|
|
44
|
-
bg = 'bg-green-800 border-green-600';
|
|
45
|
-
text = 'text-green-300';
|
|
46
|
-
extra = '✓';
|
|
47
|
-
} else if (isCurrent) {
|
|
48
|
-
bg = 'bg-blue-800 border-blue-500';
|
|
49
|
-
text = 'text-blue-100 font-bold';
|
|
50
|
-
extra = null;
|
|
51
|
-
} else if (isFuture) {
|
|
52
|
-
bg = 'bg-gray-800 border-gray-700';
|
|
53
|
-
text = 'text-gray-600';
|
|
54
|
-
extra = null;
|
|
55
|
-
} else {
|
|
56
|
-
// Past but not approved
|
|
57
|
-
bg = 'bg-gray-700 border-gray-600';
|
|
58
|
-
text = 'text-gray-400';
|
|
59
|
-
extra = null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<div
|
|
64
|
-
key={phase}
|
|
65
|
-
className={`flex-1 px-1 py-1 rounded border text-center text-[9px] leading-tight ${bg} ${text}`}
|
|
66
|
-
title={phase}
|
|
67
|
-
>
|
|
68
|
-
<div className="truncate">{phase}</div>
|
|
69
|
-
{extra && <div className="text-[8px] mt-0.5">{extra}</div>}
|
|
70
|
-
</div>
|
|
71
|
-
);
|
|
72
|
-
})}
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function TaskList({ taskList }) {
|
|
78
|
-
if (!taskList?.length) return <p className="text-gray-600 text-xs italic">No tasks</p>;
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<div className="space-y-1">
|
|
82
|
-
{taskList.map((task, i) => {
|
|
83
|
-
const status = task.status || 'pending';
|
|
84
|
-
const { icon, color } = TASK_STATUS_ICON[status] || TASK_STATUS_ICON.pending;
|
|
85
|
-
return (
|
|
86
|
-
<div key={i} className="flex items-start gap-2 text-xs">
|
|
87
|
-
<span className={`${color} font-mono flex-shrink-0 mt-0.5 w-3 text-center`}>{icon}</span>
|
|
88
|
-
<span className={`flex-1 leading-snug ${status === 'completed' ? 'text-gray-500' : 'text-gray-300'}`}>
|
|
89
|
-
{task.text || task.description || String(task)}
|
|
90
|
-
</span>
|
|
91
|
-
{task.agent && (
|
|
92
|
-
<span className="text-gray-600 text-[10px] font-mono flex-shrink-0 ml-1">{task.agent}</span>
|
|
93
|
-
)}
|
|
94
|
-
</div>
|
|
95
|
-
);
|
|
96
|
-
})}
|
|
97
|
-
</div>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function FeatureCard({ featureId, feature }) {
|
|
102
|
-
const status = feature.status || 'draft';
|
|
103
|
-
const phase = feature.phase || feature.currentPhase || PHASE_ORDER[0];
|
|
104
|
-
const approvals = feature.approvals || {};
|
|
105
|
-
const taskList = Array.isArray(feature.taskList)
|
|
106
|
-
? feature.taskList
|
|
107
|
-
: typeof feature.taskList === 'object' && feature.taskList !== null
|
|
108
|
-
? Object.values(feature.taskList)
|
|
109
|
-
: [];
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<div className="bg-gray-900 border border-gray-700 rounded-xl p-4 space-y-3">
|
|
113
|
-
{/* Header */}
|
|
114
|
-
<div className="flex items-center justify-between gap-3">
|
|
115
|
-
<h3 className="text-white font-semibold text-sm truncate">{featureId}</h3>
|
|
116
|
-
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
117
|
-
{feature._worktree && (
|
|
118
|
-
<span
|
|
119
|
-
className="px-2 py-0.5 rounded text-xs font-medium border bg-purple-900/60 border-purple-600 text-purple-300"
|
|
120
|
-
title={`In progress in .worktrees/${feature._worktree}`}
|
|
121
|
-
>
|
|
122
|
-
worktree
|
|
123
|
-
</span>
|
|
124
|
-
)}
|
|
125
|
-
<StatusBadge status={status} />
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
{/* Phase progress */}
|
|
130
|
-
<div>
|
|
131
|
-
<p className="text-gray-500 text-[10px] uppercase tracking-wide mb-1">Phase Progress</p>
|
|
132
|
-
<PhaseProgressBar currentPhase={phase} approvals={approvals} />
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
{/* Task list */}
|
|
136
|
-
{taskList.length > 0 && (
|
|
137
|
-
<div>
|
|
138
|
-
<p className="text-gray-500 text-[10px] uppercase tracking-wide mb-1.5">
|
|
139
|
-
Tasks ({taskList.length})
|
|
140
|
-
</p>
|
|
141
|
-
<TaskList taskList={taskList} />
|
|
142
|
-
</div>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ─── Main Page ────────────────────────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
export default function LiveState() {
|
|
151
|
-
const { liveState } = morphData;
|
|
152
|
-
|
|
153
|
-
if (!liveState) {
|
|
154
|
-
return (
|
|
155
|
-
<div className="flex items-center justify-center h-full">
|
|
156
|
-
<div className="text-center space-y-3">
|
|
157
|
-
<div className="text-4xl text-gray-700">○</div>
|
|
158
|
-
<p className="text-gray-400 text-lg font-medium">No active project</p>
|
|
159
|
-
<p className="text-gray-600 text-sm">
|
|
160
|
-
Run <code className="px-1.5 py-0.5 bg-gray-800 border border-gray-700 rounded font-mono text-gray-300">morph-spec init</code> to get started
|
|
161
|
-
</p>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const features = liveState.features || {};
|
|
168
|
-
const featureEntries = Object.entries(features);
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<div className="flex flex-col h-full">
|
|
172
|
-
{/* Header */}
|
|
173
|
-
<div className="px-6 py-4 border-b border-gray-800 flex-shrink-0 flex items-center justify-between">
|
|
174
|
-
<div>
|
|
175
|
-
<h1 className="text-white text-xl font-bold">Live State</h1>
|
|
176
|
-
<p className="text-gray-400 text-sm mt-0.5">
|
|
177
|
-
{liveState.project?.name || 'Project'} · state v{liveState.version}
|
|
178
|
-
</p>
|
|
179
|
-
</div>
|
|
180
|
-
<div className="flex items-center gap-4 text-xs text-gray-400">
|
|
181
|
-
<span>{featureEntries.length} feature{featureEntries.length !== 1 ? 's' : ''}</span>
|
|
182
|
-
{liveState.project?.stack && (
|
|
183
|
-
<span className="px-2 py-0.5 bg-gray-800 border border-gray-700 rounded font-mono">
|
|
184
|
-
{liveState.project.stack}
|
|
185
|
-
</span>
|
|
186
|
-
)}
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
|
|
190
|
-
{/* Feature list */}
|
|
191
|
-
<div className="flex-1 overflow-y-auto px-6 py-5">
|
|
192
|
-
{featureEntries.length === 0 ? (
|
|
193
|
-
<div className="flex items-center justify-center h-48">
|
|
194
|
-
<p className="text-gray-500 text-sm">No features tracked yet</p>
|
|
195
|
-
</div>
|
|
196
|
-
) : (
|
|
197
|
-
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
|
198
|
-
{featureEntries.map(([featureId, feature]) => (
|
|
199
|
-
<FeatureCard key={featureId} featureId={featureId} feature={feature} />
|
|
200
|
-
))}
|
|
201
|
-
</div>
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
);
|
|
206
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
|
|
4
|
-
export function scanSquads(frameworkDir) {
|
|
5
|
-
const dir = join(frameworkDir, 'squad-templates');
|
|
6
|
-
if (!existsSync(dir)) return [];
|
|
7
|
-
return readdirSync(dir).filter(f => f.endsWith('.json')).map(f => {
|
|
8
|
-
const d = JSON.parse(readFileSync(join(dir, f), 'utf8'));
|
|
9
|
-
return { id: d.id, name: d.name, description: d.description || '',
|
|
10
|
-
parallel: d.parallel || false, coordination: d.coordination || null,
|
|
11
|
-
agents: d.agents || [], agentTeam: d.agent_team || null };
|
|
12
|
-
});
|
|
13
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { WORKTREE_DIR_NAMES } from '../../commands/project/worktree.js';
|
|
4
|
-
|
|
5
|
-
export function scanState(projectDir) {
|
|
6
|
-
const statePath = join(projectDir, '.morph', 'state.json');
|
|
7
|
-
let state = null;
|
|
8
|
-
if (existsSync(statePath)) {
|
|
9
|
-
try { state = JSON.parse(readFileSync(statePath, 'utf8')); } catch { state = null; }
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Hydrate full feature objects from the authoritative per-feature files. The
|
|
13
|
-
// root state.json is now a thin, derivable index (status + updatedAt only), so
|
|
14
|
-
// the dashboard must read .morph/features/{name}/feature.json for full data.
|
|
15
|
-
const featuresDir = join(projectDir, '.morph', 'features');
|
|
16
|
-
if (existsSync(featuresDir)) {
|
|
17
|
-
let names = [];
|
|
18
|
-
try {
|
|
19
|
-
names = readdirSync(featuresDir, { withFileTypes: true })
|
|
20
|
-
.filter((e) => e.isDirectory())
|
|
21
|
-
.map((e) => e.name);
|
|
22
|
-
} catch { names = []; }
|
|
23
|
-
for (const name of names) {
|
|
24
|
-
const fjPath = join(featuresDir, name, 'feature.json');
|
|
25
|
-
if (!existsSync(fjPath)) continue;
|
|
26
|
-
try {
|
|
27
|
-
const fj = JSON.parse(readFileSync(fjPath, 'utf8'));
|
|
28
|
-
state = state || { features: {} };
|
|
29
|
-
state.features = state.features || {};
|
|
30
|
-
state.features[name] = fj;
|
|
31
|
-
} catch { /* skip a corrupt feature.json */ }
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Aggregate in-progress features that only exist inside a feature worktree
|
|
36
|
-
// (worktrees/{wt}/.morph/features/*/feature.json). In the v8 multi-instance
|
|
37
|
-
// model a feature's authoritative feature.json is committed on its
|
|
38
|
-
// `morph/{feature}` branch and only lands in the root .morph/features/ after
|
|
39
|
-
// `morph-spec finish`. This is read-only visibility for the dashboard — root
|
|
40
|
-
// features (the merged, authoritative state) always take precedence, and
|
|
41
|
-
// worktree-sourced entries are tagged with `_worktree` so the UI can
|
|
42
|
-
// distinguish them.
|
|
43
|
-
//
|
|
44
|
-
// BOTH directory names are scanned: this scanner resolves worktrees by
|
|
45
|
-
// CONVENTION (unlike fleet/finish, which read `git worktree list`), so it is
|
|
46
|
-
// the one that would otherwise go blind to pre-C1 `.worktrees/` checkouts.
|
|
47
|
-
const worktreeDirs = WORKTREE_DIR_NAMES
|
|
48
|
-
.map((name) => join(projectDir, name))
|
|
49
|
-
.filter((dir) => existsSync(dir));
|
|
50
|
-
|
|
51
|
-
for (const worktreesDir of worktreeDirs) {
|
|
52
|
-
let worktreeNames = [];
|
|
53
|
-
try {
|
|
54
|
-
worktreeNames = readdirSync(worktreesDir, { withFileTypes: true })
|
|
55
|
-
.filter((e) => e.isDirectory())
|
|
56
|
-
.map((e) => e.name);
|
|
57
|
-
} catch { worktreeNames = []; }
|
|
58
|
-
|
|
59
|
-
for (const wt of worktreeNames) {
|
|
60
|
-
const wtFeaturesDir = join(worktreesDir, wt, '.morph', 'features');
|
|
61
|
-
if (!existsSync(wtFeaturesDir)) continue;
|
|
62
|
-
let wtFeatureNames = [];
|
|
63
|
-
try {
|
|
64
|
-
wtFeatureNames = readdirSync(wtFeaturesDir, { withFileTypes: true })
|
|
65
|
-
.filter((e) => e.isDirectory())
|
|
66
|
-
.map((e) => e.name);
|
|
67
|
-
} catch { continue; }
|
|
68
|
-
|
|
69
|
-
for (const name of wtFeatureNames) {
|
|
70
|
-
state = state || { features: {} };
|
|
71
|
-
state.features = state.features || {};
|
|
72
|
-
if (state.features[name]) continue; // root feature already present — it wins
|
|
73
|
-
|
|
74
|
-
const fjPath = join(wtFeaturesDir, name, 'feature.json');
|
|
75
|
-
if (!existsSync(fjPath)) continue;
|
|
76
|
-
try {
|
|
77
|
-
const fj = JSON.parse(readFileSync(fjPath, 'utf8'));
|
|
78
|
-
state.features[name] = { ...fj, _worktree: wt };
|
|
79
|
-
} catch { /* skip a corrupt feature.json */ }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return state;
|
|
85
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Code Generators
|
|
3
|
-
*
|
|
4
|
-
* Code and documentation generation utilities.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export * from './design-system-generator.js';
|
|
8
|
-
export * from './handoff-generator.js';
|
|
9
|
-
export * from './architecture-scanner.js';
|
|
10
|
-
export * from './scaffold-generator.js';
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hook Executor — Agent Teams hook validation
|
|
3
|
-
*
|
|
4
|
-
* Executes validation hooks triggered by Claude Code agent-teams events:
|
|
5
|
-
* TeammateIdle, TaskCompleted, PhaseAdvanced
|
|
6
|
-
*
|
|
7
|
-
* Reads agents.json to find validators with matching hook_triggers,
|
|
8
|
-
* then runs each validator's checks against the current project state.
|
|
9
|
-
*
|
|
10
|
-
* @module hook-executor
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readFileSync, existsSync } from 'fs';
|
|
14
|
-
import { join } from 'path';
|
|
15
|
-
|
|
16
|
-
// ============================================================================
|
|
17
|
-
// Loaders
|
|
18
|
-
// ============================================================================
|
|
19
|
-
|
|
20
|
-
function loadAgents(projectPath) {
|
|
21
|
-
const agentPaths = [
|
|
22
|
-
join(projectPath, '.morph/framework/agents.json'),
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const allAgents = {};
|
|
26
|
-
for (const agentPath of agentPaths) {
|
|
27
|
-
if (existsSync(agentPath)) {
|
|
28
|
-
try {
|
|
29
|
-
const data = JSON.parse(readFileSync(agentPath, 'utf8'));
|
|
30
|
-
Object.assign(allAgents, data.agents || {});
|
|
31
|
-
} catch (e) { /* skip malformed files */ }
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return allAgents;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function loadStateFeature(projectPath, featureName) {
|
|
38
|
-
const statePath = join(projectPath, '.morph/state.json');
|
|
39
|
-
if (!existsSync(statePath)) return null;
|
|
40
|
-
|
|
41
|
-
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
42
|
-
const features = state.features;
|
|
43
|
-
|
|
44
|
-
// Support both object format { featureName: {...} } and array format [{name, ...}]
|
|
45
|
-
if (Array.isArray(features)) {
|
|
46
|
-
return features.find(f => f.name === featureName) || null;
|
|
47
|
-
}
|
|
48
|
-
return features[featureName] || null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ============================================================================
|
|
52
|
-
// Main Executor
|
|
53
|
-
// ============================================================================
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Execute agent-teams hook validation
|
|
57
|
-
* @param {string} projectPath - Absolute path to project root
|
|
58
|
-
* @param {string} featureName - Feature to validate
|
|
59
|
-
* @param {string} hookEvent - Event name (TeammateIdle, TaskCompleted, PhaseAdvanced)
|
|
60
|
-
* @param {Object} [opts]
|
|
61
|
-
* @param {boolean} [opts.dryRun=false] - Identify validators without running them
|
|
62
|
-
* @param {boolean} [opts.verbose=true] - Print results to console
|
|
63
|
-
* @returns {Promise<Object>} { passed, blocked, errors, warnings }
|
|
64
|
-
*/
|
|
65
|
-
export async function executeHook(projectPath, featureName, hookEvent, opts = {}) {
|
|
66
|
-
const { dryRun = false } = opts;
|
|
67
|
-
|
|
68
|
-
// Load feature state
|
|
69
|
-
const feature = loadStateFeature(projectPath, featureName);
|
|
70
|
-
if (!feature) {
|
|
71
|
-
return {
|
|
72
|
-
passed: false,
|
|
73
|
-
blocked: true,
|
|
74
|
-
errors: [`Feature not found: ${featureName}`],
|
|
75
|
-
warnings: []
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Load all available agents
|
|
80
|
-
const agents = loadAgents(projectPath);
|
|
81
|
-
|
|
82
|
-
// Filter: agents that run in hooks AND trigger on this event
|
|
83
|
-
const hookValidators = Object.entries(agents)
|
|
84
|
-
.filter(([, agent]) =>
|
|
85
|
-
agent.relationships?.runs_in === 'hooks' &&
|
|
86
|
-
Array.isArray(agent.relationships?.hook_triggers) &&
|
|
87
|
-
agent.relationships.hook_triggers.includes(hookEvent)
|
|
88
|
-
)
|
|
89
|
-
.map(([agentId, agent]) => ({ agentId, ...agent }));
|
|
90
|
-
|
|
91
|
-
// No validators configured for this event, or dry run — pass immediately
|
|
92
|
-
if (hookValidators.length === 0 || dryRun) {
|
|
93
|
-
return { passed: true, blocked: false, errors: [], warnings: [] };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Run validators
|
|
97
|
-
// In production, each validator would execute its actual checks.
|
|
98
|
-
// This implementation returns pass since real validation requires external tooling.
|
|
99
|
-
const errors = [];
|
|
100
|
-
const warnings = [];
|
|
101
|
-
|
|
102
|
-
const passed = errors.length === 0;
|
|
103
|
-
const blocked = errors.some(e => typeof e === 'object' && e.blocksOnFail !== false);
|
|
104
|
-
|
|
105
|
-
return { passed, blocked, errors, warnings };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ============================================================================
|
|
109
|
-
// Formatter
|
|
110
|
-
// ============================================================================
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Format hook execution results as a human-readable string
|
|
114
|
-
* @param {Object} results - Result from executeHook
|
|
115
|
-
* @param {string} hookEvent - Event name
|
|
116
|
-
* @returns {string} Formatted output
|
|
117
|
-
*/
|
|
118
|
-
export function formatHookResults(results, hookEvent) {
|
|
119
|
-
const { passed, blocked, errors = [], warnings = [] } = results;
|
|
120
|
-
const lines = [];
|
|
121
|
-
|
|
122
|
-
if (!passed && errors.length > 0) {
|
|
123
|
-
lines.push(`❌ Hook: ${hookEvent}`);
|
|
124
|
-
lines.push('FAILED');
|
|
125
|
-
lines.push('ERRORS (blocking):');
|
|
126
|
-
|
|
127
|
-
for (const err of errors) {
|
|
128
|
-
if (err.issues && Array.isArray(err.issues)) {
|
|
129
|
-
for (const issue of err.issues) {
|
|
130
|
-
lines.push(` ${issue.message}`);
|
|
131
|
-
if (issue.file && issue.line !== undefined) {
|
|
132
|
-
lines.push(` ${issue.file}:${issue.line}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
} else if (err.error) {
|
|
136
|
-
lines.push(` Error: ${err.error}`);
|
|
137
|
-
} else if (typeof err === 'string') {
|
|
138
|
-
lines.push(` ${err}`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (blocked) {
|
|
143
|
-
lines.push('BLOCKED');
|
|
144
|
-
}
|
|
145
|
-
} else if (warnings.length === 0) {
|
|
146
|
-
lines.push(`✅ Hook: ${hookEvent}`);
|
|
147
|
-
lines.push('All validators passed');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (warnings.length > 0) {
|
|
151
|
-
lines.push(`⚠️ Hook: ${hookEvent}`);
|
|
152
|
-
lines.push('WARNINGS (non-blocking):');
|
|
153
|
-
|
|
154
|
-
for (const warn of warnings) {
|
|
155
|
-
if (warn.issues && Array.isArray(warn.issues)) {
|
|
156
|
-
for (const issue of warn.issues) {
|
|
157
|
-
lines.push(` ${issue.message}`);
|
|
158
|
-
if (issue.file && issue.line !== undefined) {
|
|
159
|
-
lines.push(` ${issue.file}:${issue.line}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return lines.join('\n');
|
|
167
|
-
}
|