@polymorphism-tech/morph-spec 7.8.1 → 7.9.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/README.md +2 -2
- package/bin/morph-spec.js +30 -1
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -1
- package/framework/MORPH.md +4 -3
- package/framework/commands/commit.md +166 -166
- package/framework/commands/morph-infra.md +215 -215
- package/framework/commands/morph-preflight.md +205 -205
- package/framework/commands/morph-troubleshoot.md +124 -124
- package/framework/evals/trace-schema.md +22 -12
- package/framework/hooks/README.md +2 -0
- package/framework/hooks/claude-code/core/subagent-report-guard.js +95 -0
- package/framework/hooks/claude-code/core/trace-autogen.js +183 -0
- package/framework/hooks/claude-code/statusline.py +679 -679
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/activity-logger.js +160 -160
- package/framework/hooks/shared/hook-response.js +45 -45
- package/framework/hooks/shared/payload-utils.js +78 -78
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/hooks/shared/tasks-validator.js +47 -4
- package/framework/rubrics/backend.json +41 -41
- package/framework/rules/ai-agents-standards.md +45 -45
- package/framework/rules/dotnet-templates.md +86 -86
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +27 -27
- package/framework/rules/neon-standards.md +25 -25
- package/framework/rules/nextjs-standards.md +20 -20
- package/framework/rules/nextjs-templates.md +40 -40
- package/framework/schemas/feature.schema.json +128 -0
- 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 +23 -10
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +3 -6
- package/framework/skills/core/morph-implement/SKILL.md +28 -15
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +2 -2
- package/framework/skills/core/morph-review/SKILL.md +2 -6
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +275 -275
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -113
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/index.js +13 -13
- package/src/commands/mcp/mcp-setup.js +519 -519
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +23 -18
- package/src/commands/project/update.js +16 -9
- package/src/commands/state/create.js +58 -0
- package/src/commands/state/gate-decision.js +99 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +55 -0
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/LiveState.jsx +239 -239
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +41 -41
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/scanner/standards-scanner.js +26 -26
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/metrics-aggregator.js +56 -56
- package/src/lib/agents/metrics-calculator.js +125 -125
- package/src/lib/agents/output-collector.js +73 -73
- package/src/lib/detectors/claude-config-detector.js +152 -152
- package/src/lib/detectors/design-system-detector.js +231 -231
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +639 -639
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/index.js +13 -13
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +81 -81
- package/src/lib/validators/architecture/architecture-validator.js +259 -259
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/packages/package-validator.js +347 -347
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +349 -349
- package/src/scripts/setup-infra.js +385 -383
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +25 -10
- package/src/utils/banner.js +37 -4
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/hooks-installer.js +32 -4
- package/src/utils/skills-installer.js +174 -172
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
5
|
+
|
|
6
|
+
const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
|
|
7
|
+
|
|
8
|
+
function parsePauseValue(raw) {
|
|
9
|
+
const v = String(raw).toLowerCase();
|
|
10
|
+
if (v === 'true') return true;
|
|
11
|
+
if (v === 'false') return false;
|
|
12
|
+
console.error(chalk.red(`\n❌ Invalid --pause: "${raw}" — expected true or false\n`));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** trust from the committed config (BOM-tolerant), defaulting to "auto". */
|
|
17
|
+
function readTrustFromConfig() {
|
|
18
|
+
try {
|
|
19
|
+
const path = join(process.cwd(), '.morph', 'config', 'config.json');
|
|
20
|
+
if (!existsSync(path)) return 'auto';
|
|
21
|
+
const raw = readFileSync(path, 'utf8').replace(/^/, '');
|
|
22
|
+
return JSON.parse(raw).trust || 'auto';
|
|
23
|
+
} catch {
|
|
24
|
+
return 'auto';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>]
|
|
30
|
+
* [--signals <json>] [--trust <mode>]` — record the trust decision for a gate
|
|
31
|
+
* under `gateDecisions` in the authoritative feature.json.
|
|
32
|
+
*
|
|
33
|
+
* Same philosophy as `morph-spec score`: the timestamp (`decidedAt`) is
|
|
34
|
+
* stamped server-side and the write is atomic (state-manager). This replaces
|
|
35
|
+
* the manual Edit of feature.json from the skills — which collided with the
|
|
36
|
+
* state-sync hook rewriting the file between the agent's Read and Edit
|
|
37
|
+
* ("file modified since read"). The canonical shape is documented in
|
|
38
|
+
* framework/schemas/feature.schema.json and consumed by gate-guard.js.
|
|
39
|
+
*/
|
|
40
|
+
export async function gateDecisionCommand(featureName, gateName, options = {}) {
|
|
41
|
+
try {
|
|
42
|
+
if (!VALID_GATES.includes(gateName)) {
|
|
43
|
+
console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
|
|
44
|
+
console.error(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (options.pause === undefined) {
|
|
49
|
+
console.error(chalk.red('\n❌ --pause is required (true or false)\n'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const pause = parsePauseValue(options.pause);
|
|
53
|
+
|
|
54
|
+
const feature = getFeature(featureName);
|
|
55
|
+
if (!feature) {
|
|
56
|
+
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let signals = {};
|
|
61
|
+
if (options.signals !== undefined) {
|
|
62
|
+
try {
|
|
63
|
+
signals = JSON.parse(options.signals);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(chalk.red(`\n❌ Invalid --signals: not valid JSON (${err.message})\n`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
if (!signals || typeof signals !== 'object' || Array.isArray(signals)) {
|
|
69
|
+
console.error(chalk.red('\n❌ Invalid --signals: expected a JSON object, e.g. \'{"taskCount":12}\'\n'));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const entry = {
|
|
75
|
+
pause,
|
|
76
|
+
reason: options.reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
|
|
77
|
+
signals,
|
|
78
|
+
trust: options.trust || readTrustFromConfig(),
|
|
79
|
+
decidedAt: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
|
|
83
|
+
|
|
84
|
+
if (options.json) {
|
|
85
|
+
console.log(JSON.stringify({ feature: featureName, gate: gateName, ...entry }, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(chalk.green(`\n✅ Gate decision recorded: ${featureName}/${gateName} → pause: ${entry.pause}`));
|
|
90
|
+
console.log(chalk.gray(` reason: ${entry.reason} | trust: ${entry.trust} | decidedAt: ${entry.decidedAt}`));
|
|
91
|
+
if (Object.keys(entry.signals).length > 0) {
|
|
92
|
+
console.log(chalk.gray(` signals: ${JSON.stringify(entry.signals)}`));
|
|
93
|
+
}
|
|
94
|
+
console.log('');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(chalk.red(`\n❌ Error recording gate decision: ${error.message}\n`));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,197 +1,197 @@
|
|
|
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
|
+
/**
|
|
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
|
+
}
|
|
@@ -484,6 +484,61 @@ async function ensureFeature(featureName) {
|
|
|
484
484
|
return state.features[featureName];
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Explicitly create a feature with the canonical shape (documented in
|
|
489
|
+
* framework/schemas/feature.schema.json). Idempotent: an existing feature is
|
|
490
|
+
* returned untouched. This is the server-side birth path behind
|
|
491
|
+
* `morph-spec create` — replacing the implicit registration on the first
|
|
492
|
+
* Write into .morph/features/ (the state-sync hook remains as fallback).
|
|
493
|
+
*
|
|
494
|
+
* Works even before any state exists (scaffolds the root index), so a
|
|
495
|
+
* feature can be born as the very first morph action in a project.
|
|
496
|
+
*
|
|
497
|
+
* @param {string} featureName - Feature name (kebab-case)
|
|
498
|
+
* @param {{ status?: string }} [options]
|
|
499
|
+
* @returns {Promise<{ feature: Object, created: boolean }>}
|
|
500
|
+
*/
|
|
501
|
+
export async function createFeature(featureName, options = {}) {
|
|
502
|
+
const { status = 'in_progress' } = options;
|
|
503
|
+
const state = loadState(false) || scaffoldState();
|
|
504
|
+
|
|
505
|
+
const existing = state.features[featureName];
|
|
506
|
+
if (existing) {
|
|
507
|
+
return { feature: existing, created: false };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const now = new Date().toISOString();
|
|
511
|
+
state.features[featureName] = {
|
|
512
|
+
status,
|
|
513
|
+
createdAt: now,
|
|
514
|
+
updatedAt: now,
|
|
515
|
+
activeAgents: [],
|
|
516
|
+
approvalGates: {
|
|
517
|
+
proposal: { approved: false, timestamp: null, approvedBy: null },
|
|
518
|
+
uiux: { approved: false, timestamp: null, approvedBy: null },
|
|
519
|
+
plan: { approved: false, timestamp: null, approvedBy: null },
|
|
520
|
+
review: { approved: false, timestamp: null, approvedBy: null }
|
|
521
|
+
},
|
|
522
|
+
gateDecisions: {},
|
|
523
|
+
taskScores: {},
|
|
524
|
+
tasks: {
|
|
525
|
+
total: 0,
|
|
526
|
+
completed: 0,
|
|
527
|
+
inProgress: 0,
|
|
528
|
+
pending: 0
|
|
529
|
+
},
|
|
530
|
+
checkpoints: [],
|
|
531
|
+
dispatchLog: [],
|
|
532
|
+
trustConfig: {},
|
|
533
|
+
contextBundles: [],
|
|
534
|
+
fileChanges: []
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
state.metadata.totalFeatures = Object.keys(state.features).length;
|
|
538
|
+
saveState(state);
|
|
539
|
+
return { feature: state.features[featureName], created: true };
|
|
540
|
+
}
|
|
541
|
+
|
|
487
542
|
/**
|
|
488
543
|
* Update feature property (supports nested keys like "tasks.completed")
|
|
489
544
|
* @param {string} featureName - Feature name
|