@polymorphism-tech/morph-spec 4.8.19 → 4.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/CLAUDE.md +21 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +15 -56
- package/bin/task-manager.js +115 -14
- package/bin/validate.js +67 -33
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +201 -203
- package/docs/QUICKSTART.md +2 -2
- package/framework/CLAUDE.md +21 -0
- package/framework/agents.json +698 -176
- package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +2 -2
- package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +155 -0
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +1 -1
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +71 -2
- package/framework/hooks/claude-code/statusline.py +76 -30
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +14 -6
- package/framework/hooks/shared/activity-logger.js +0 -24
- package/framework/hooks/shared/phase-utils.js +3 -0
- package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
- package/framework/hooks/shared/stale-task-reset.js +57 -0
- package/framework/hooks/shared/state-reader.js +2 -2
- package/framework/hooks/shared/worktree-helpers.js +53 -0
- package/framework/phases.json +40 -8
- package/framework/skills/level-0-meta/brainstorming/SKILL.md +1 -1
- package/framework/skills/level-0-meta/code-review/SKILL.md +1 -1
- package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +163 -163
- package/framework/skills/level-0-meta/frontend-review/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
- package/framework/skills/level-0-meta/morph-init/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +4 -4
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
- package/framework/skills/level-0-meta/post-implementation/SKILL.md +59 -12
- package/framework/skills/level-0-meta/simulation-checklist/SKILL.md +1 -1
- package/framework/skills/level-0-meta/terminal-title/SKILL.md +1 -1
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +1 -1
- package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +6 -5
- package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +215 -189
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +251 -251
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +382 -365
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +492 -450
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +194 -190
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +270 -270
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +285 -285
- package/framework/standards/STANDARDS.json +640 -88
- package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
- package/framework/templates/REGISTRY.json +1825 -1909
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/onboarding.md +1 -5
- package/package.json +2 -6
- package/src/commands/agents/dispatch-agents.js +55 -4
- package/src/commands/project/doctor.js +16 -47
- package/src/commands/project/init.js +1 -1
- package/src/commands/project/status.js +2 -2
- package/src/commands/project/update.js +381 -365
- package/src/commands/project/worktree.js +154 -0
- package/src/commands/state/advance-phase.js +120 -30
- package/src/commands/state/approve.js +2 -2
- package/src/commands/state/index.js +7 -8
- package/src/commands/state/phase-runner.js +1 -1
- package/src/commands/state/state.js +61 -6
- package/src/commands/tasks/task.js +78 -99
- package/src/commands/templates/template-render.js +93 -173
- package/src/commands/trust/trust.js +26 -21
- package/src/core/paths/output-schema.js +15 -0
- package/src/core/state/state-manager.js +28 -54
- package/src/core/workflows/workflow-detector.js +9 -87
- package/src/lib/phase-chain/phase-validator.js +330 -0
- package/src/lib/stack/stack-profile.js +88 -0
- package/src/lib/tasks/task-classifier.js +16 -0
- package/src/lib/tasks/test-runner.js +77 -0
- package/src/lib/trust/trust-manager.js +32 -144
- package/src/lib/validators/spec-validator.js +58 -4
- package/src/lib/validators/validation-runner.js +23 -11
- package/src/scripts/setup-infra.js +240 -224
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/banner.js +1 -1
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +1 -0
- package/src/utils/hooks-installer.js +258 -8
- package/framework/hooks/dev/check-sync-health.js +0 -117
- package/framework/hooks/dev/guard-version-numbers.js +0 -57
- package/framework/hooks/dev/sync-standards-registry.js +0 -60
- package/framework/hooks/dev/sync-template-registry.js +0 -60
- package/framework/hooks/dev/validate-skill-format.js +0 -70
- package/framework/hooks/dev/validate-standard-format.js +0 -73
- package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
- package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
- package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
- package/framework/workflows/configs/design-impl.json +0 -49
- package/framework/workflows/configs/express.json +0 -45
- package/framework/workflows/configs/fast-track.json +0 -42
- package/framework/workflows/configs/full-morph.json +0 -79
- package/framework/workflows/configs/fusion.json +0 -39
- package/framework/workflows/configs/long-running.json +0 -33
- package/framework/workflows/configs/spec-only.json +0 -43
- package/framework/workflows/configs/ui-refresh.json +0 -49
- package/framework/workflows/configs/zero-touch.json +0 -82
- package/src/commands/project/monitor.js +0 -295
- package/src/commands/project/tutorial.js +0 -115
- package/src/commands/state/validate-phase.js +0 -238
- package/src/commands/templates/generate-contracts.js +0 -445
- package/src/core/orchestrator.js +0 -171
- package/src/core/registry/command-registry.js +0 -28
- package/src/core/registry/index.js +0 -8
- package/src/core/registry/validator-registry.js +0 -204
- package/src/core/templates/template-validator.js +0 -296
- package/src/generator/config-generator.js +0 -206
- package/src/generator/templates/config.json.template +0 -40
- package/src/generator/templates/project.md.template +0 -67
- package/src/lib/agents/micro-agent-factory.js +0 -161
- package/src/lib/analysis/complexity-analyzer.js +0 -441
- package/src/lib/analysis/index.js +0 -7
- package/src/lib/analytics/analytics-engine.js +0 -345
- package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
- package/src/lib/checkpoints/index.js +0 -7
- package/src/lib/context/context-bundler.js +0 -241
- package/src/lib/context/context-optimizer.js +0 -212
- package/src/lib/context/context-tracker.js +0 -273
- package/src/lib/context/core-four-tracker.js +0 -201
- package/src/lib/context/mcp-optimizer.js +0 -200
- package/src/lib/execution/fusion-executor.js +0 -304
- package/src/lib/execution/parallel-executor.js +0 -270
- package/src/lib/hooks/stop-hook-executor.js +0 -286
- package/src/lib/hops/hop-composer.js +0 -221
- package/src/lib/phase-chain/eligibility-checker.js +0 -243
- package/src/lib/threads/thread-coordinator.js +0 -238
- package/src/lib/threads/thread-manager.js +0 -317
- package/src/lib/tracking/artifact-trail.js +0 -202
- package/src/scanner/project-scanner.js +0 -242
- package/src/ui/diff-display.js +0 -91
- package/src/ui/interactive-wizard.js +0 -96
- package/src/ui/user-review.js +0 -211
- package/src/ui/wizard-questions.js +0 -188
- package/src/utils/color-utils.js +0 -70
- package/src/utils/process-handler.js +0 -97
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Worktree Setup Command
|
|
3
|
+
*
|
|
4
|
+
* Creates a git worktree at .worktrees/{feature}/ on branch morph/{feature}.
|
|
5
|
+
* Used by SessionStart hook to isolate feature work from the main branch.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* morph-spec worktree setup <feature>
|
|
9
|
+
* morph-spec worktree setup <feature> --fresh
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — success (created) or fail-open (not a git repo, git error)
|
|
13
|
+
* 2 — worktree already exists (signals resume scenario to hook caller)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
|
|
20
|
+
// ── Pure helpers (exported for tests) ──────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a directory is inside a git repository.
|
|
24
|
+
* @param {string} cwd
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
export function isGitRepo(cwd = process.cwd()) {
|
|
28
|
+
try {
|
|
29
|
+
execSync('git rev-parse --git-dir', { cwd, stdio: 'pipe' });
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a branch exists locally.
|
|
38
|
+
* @param {string} branch
|
|
39
|
+
* @param {string} cwd
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function branchExists(branch, cwd = process.cwd()) {
|
|
43
|
+
try {
|
|
44
|
+
execSync(`git show-ref --verify --quiet refs/heads/${branch}`, { cwd, stdio: 'pipe' });
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a worktree path is already registered with git.
|
|
53
|
+
* @param {string} worktreePath Relative or absolute path
|
|
54
|
+
* @param {string} cwd
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function worktreeExists(worktreePath, cwd = process.cwd()) {
|
|
58
|
+
try {
|
|
59
|
+
const output = execSync('git worktree list --porcelain', { cwd, stdio: 'pipe' }).toString();
|
|
60
|
+
const normalizedPath = worktreePath.replace(/\\/g, '/');
|
|
61
|
+
const absPath = normalizedPath.startsWith('/') ? normalizedPath : `${cwd.replace(/\\/g, '/')}/${normalizedPath}`;
|
|
62
|
+
return output.replace(/\\/g, '/').includes(absPath);
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the worktree directory path (always forward slashes).
|
|
70
|
+
* @param {string} feature
|
|
71
|
+
* @param {string} cwd
|
|
72
|
+
* @param {{ fresh?: boolean }} options
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
export function buildWorktreePath(feature, cwd = process.cwd(), options = {}) {
|
|
76
|
+
const suffix = options.fresh ? `-${getDateSuffix()}` : '';
|
|
77
|
+
const normalizedCwd = cwd.replace(/\\/g, '/');
|
|
78
|
+
return `${normalizedCwd}/.worktrees/${feature}${suffix}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build the branch name for a feature worktree.
|
|
83
|
+
* @param {string} feature
|
|
84
|
+
* @param {{ fresh?: boolean }} options
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
export function buildBranchName(feature, options = {}) {
|
|
88
|
+
const suffix = options.fresh ? `-${getDateSuffix()}` : '';
|
|
89
|
+
return `morph/${feature}${suffix}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getDateSuffix() {
|
|
93
|
+
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Command handler ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Main command handler for `morph-spec worktree setup <feature>`
|
|
100
|
+
* @param {string} feature
|
|
101
|
+
* @param {{ fresh?: boolean, cwd?: string }} options
|
|
102
|
+
*/
|
|
103
|
+
export async function worktreeSetupCommand(feature, options = {}) {
|
|
104
|
+
const cwd = options.cwd || process.cwd();
|
|
105
|
+
|
|
106
|
+
// Gate 1: Must be a git repo
|
|
107
|
+
if (!isGitRepo(cwd)) {
|
|
108
|
+
console.log(JSON.stringify({ created: false, error: 'not-a-git-repo', feature }));
|
|
109
|
+
process.exit(0); // Fail-open
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const branch = buildBranchName(feature, options);
|
|
113
|
+
const worktreePath = buildWorktreePath(feature, cwd, options);
|
|
114
|
+
|
|
115
|
+
// Gate 2: If worktree already registered and not --fresh, signal resume
|
|
116
|
+
if (!options.fresh && worktreeExists(worktreePath, cwd)) {
|
|
117
|
+
console.log(JSON.stringify({ created: false, alreadyExists: true, path: worktreePath, branch, feature }));
|
|
118
|
+
process.exit(2); // Convention: 2 = already exists
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Create branch if needed (based off current HEAD)
|
|
123
|
+
if (!branchExists(branch, cwd)) {
|
|
124
|
+
execSync(`git branch ${branch}`, { cwd, stdio: 'pipe' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Create worktree directory parent if needed
|
|
128
|
+
const parentDir = join(cwd, '.worktrees');
|
|
129
|
+
if (!existsSync(parentDir)) {
|
|
130
|
+
mkdirSync(parentDir, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add worktree
|
|
134
|
+
execSync(`git worktree add "${worktreePath}" ${branch}`, { cwd, stdio: 'pipe' });
|
|
135
|
+
|
|
136
|
+
// Persist in state (non-blocking — feature may not be in state yet)
|
|
137
|
+
try {
|
|
138
|
+
const { loadState, saveState } = await import('../../lib/state/state-manager.js');
|
|
139
|
+
const state = loadState(cwd);
|
|
140
|
+
if (state?.features?.[feature]) {
|
|
141
|
+
state.features[feature].worktree = { path: worktreePath, branch, createdAt: new Date().toISOString() };
|
|
142
|
+
saveState(state, cwd);
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Non-blocking
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log(JSON.stringify({ created: true, path: worktreePath, branch, feature }));
|
|
149
|
+
process.exit(0);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.log(JSON.stringify({ created: false, error: (err.message || 'unknown').slice(0, 200), feature }));
|
|
152
|
+
process.exit(0); // Fail-open
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import { loadState, saveState, getFeature, getApprovalGate, derivePhase } from '../../core/state/state-manager.js';
|
|
14
|
-
import { PHASES, validatePhase } from '
|
|
14
|
+
import { PHASES, validatePhase } from '../../lib/phase-chain/phase-validator.js';
|
|
15
15
|
import { detectDesignSystem, hasUIAgentsActive } from '../../lib/detectors/design-system-detector.js';
|
|
16
|
-
import { getOutputPath } from '../../core/paths/output-schema.js';
|
|
17
|
-
import { shouldAutoApprove
|
|
16
|
+
import { getOutputPath, getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
17
|
+
import { shouldAutoApprove } from '../../lib/trust/trust-manager.js';
|
|
18
18
|
import { validateSpec } from '../../lib/validators/spec-validator.js';
|
|
19
19
|
import { validateTransition, getPhaseDisplayName } from '../../core/state/phase-state-machine.js';
|
|
20
20
|
import { validateSpecContent, validateTasksContent, validateFeatureOutputs } from '../../lib/validators/content/content-validator.js';
|
|
21
21
|
import { getWorkflowConfig } from '../../core/workflows/workflow-detector.js';
|
|
22
|
+
import { getStackProfile } from '../../lib/stack/stack-profile.js';
|
|
22
23
|
import { readFileSync, existsSync } from 'fs';
|
|
23
24
|
import { join, dirname } from 'path';
|
|
24
25
|
import { fileURLToPath } from 'url';
|
|
@@ -28,6 +29,17 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
28
29
|
// Phase order for advancing (skips optional phases unless active)
|
|
29
30
|
const PHASE_ORDER = ['proposal', 'setup', 'uiux', 'design', 'clarify', 'tasks', 'implement', 'sync'];
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Phases that require an explicit approval gate before advancing.
|
|
34
|
+
* When --skip-approval is passed, these gates are bypassed and a warning is logged.
|
|
35
|
+
* Exported for use in tests and in phase-runner warnings.
|
|
36
|
+
*/
|
|
37
|
+
export const APPROVAL_GATE_MAP = {
|
|
38
|
+
'design': 'design',
|
|
39
|
+
'tasks': 'tasks',
|
|
40
|
+
'uiux': 'uiux'
|
|
41
|
+
};
|
|
42
|
+
|
|
31
43
|
/**
|
|
32
44
|
* Get the next phase after the current one.
|
|
33
45
|
* Returns { nextPhase, skippedPhases } so callers can persist skipped phases in state.
|
|
@@ -119,14 +131,11 @@ function getNextPhase(currentPhase, feature, skipOptional, featureName) {
|
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
//
|
|
123
|
-
if (candidate === 'sync') {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
skippedPhases.push(candidate);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
134
|
+
// sync is optional — only skip if --skip-optional is set
|
|
135
|
+
if (candidate === 'sync' && skipOptional) {
|
|
136
|
+
console.log(chalk.gray(` ⏩ Skipping ${candidate}: optional phase`));
|
|
137
|
+
skippedPhases.push(candidate);
|
|
138
|
+
continue;
|
|
130
139
|
}
|
|
131
140
|
}
|
|
132
141
|
|
|
@@ -189,13 +198,15 @@ export async function advancePhaseCommand(feature, options = {}) {
|
|
|
189
198
|
|
|
190
199
|
// === GATE 2: Approval Gate Check ===
|
|
191
200
|
// Check if current phase requires approval before advancing
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
// Track gates that get approved implicitly so we can persist them after state load.
|
|
202
|
+
let gateApprovedImplicitly = null; // { gate, approvedBy, reason }
|
|
203
|
+
|
|
204
|
+
const requiredGate = APPROVAL_GATE_MAP[currentPhase];
|
|
205
|
+
if (requiredGate && options.skipApproval) {
|
|
206
|
+
console.log(chalk.yellow(`\n⚠ --skip-approval: Approval gate "${requiredGate}" bypassed for phase "${currentPhase}"`));
|
|
207
|
+
console.log(chalk.gray(' Intended for CI/automation only. Run without --skip-approval for normal workflow.\n'));
|
|
208
|
+
gateApprovedImplicitly = { gate: requiredGate, approvedBy: 'skip-approval' };
|
|
209
|
+
}
|
|
199
210
|
if (requiredGate && !options.skipApproval) {
|
|
200
211
|
const gateStatus = getApprovalGate(feature, requiredGate);
|
|
201
212
|
|
|
@@ -206,6 +217,7 @@ export async function advancePhaseCommand(feature, options = {}) {
|
|
|
206
217
|
if (trustResult.autoApprove) {
|
|
207
218
|
console.log(chalk.green(`\n✓ Auto-approved gate "${requiredGate}" (${trustResult.reason})`));
|
|
208
219
|
// Proceed — trust bypasses manual gate requirement
|
|
220
|
+
gateApprovedImplicitly = { gate: requiredGate, approvedBy: `trust:${trustResult.level}` };
|
|
209
221
|
} else {
|
|
210
222
|
console.log(chalk.red(`\n✗ Phase "${currentPhase}" requires approval before advancing`));
|
|
211
223
|
console.log(chalk.gray(` Trust level: ${trustResult.level} (${trustResult.reason})`));
|
|
@@ -245,9 +257,10 @@ export async function advancePhaseCommand(feature, options = {}) {
|
|
|
245
257
|
// === GATE 4: Content Validation ===
|
|
246
258
|
// Validate spec.md and contracts.cs when advancing from design phase
|
|
247
259
|
if (currentPhase === 'design' && (nextPhase === 'clarify' || nextPhase === 'tasks')) {
|
|
248
|
-
// Check spec.md structure and content
|
|
249
|
-
|
|
250
|
-
|
|
260
|
+
// Check spec.md structure and content (filesystem-based)
|
|
261
|
+
const specAbsPath = getAbsoluteOutputPath(process.cwd(), feature, 'spec');
|
|
262
|
+
if (existsSync(specAbsPath)) {
|
|
263
|
+
const specContentValidation = validateSpecContent(specAbsPath);
|
|
251
264
|
|
|
252
265
|
if (!specContentValidation.valid) {
|
|
253
266
|
console.log(chalk.red('\n✗ Spec content validation failed:'));
|
|
@@ -294,8 +307,9 @@ export async function advancePhaseCommand(feature, options = {}) {
|
|
|
294
307
|
// === GATE 5: Tasks Content Validation ===
|
|
295
308
|
// Validate tasks.md structure when advancing to implement
|
|
296
309
|
if (currentPhase === 'tasks' && nextPhase === 'implement') {
|
|
297
|
-
|
|
298
|
-
|
|
310
|
+
const tasksAbsPath = getAbsoluteOutputPath(process.cwd(), feature, 'tasks');
|
|
311
|
+
if (existsSync(tasksAbsPath)) {
|
|
312
|
+
const tasksContentValidation = validateTasksContent(tasksAbsPath);
|
|
299
313
|
|
|
300
314
|
if (!tasksContentValidation.valid) {
|
|
301
315
|
console.log(chalk.red('\n✗ Tasks content validation failed:'));
|
|
@@ -345,6 +359,40 @@ export async function advancePhaseCommand(feature, options = {}) {
|
|
|
345
359
|
const existingSkipped = state.features[feature].skippedPhases || [];
|
|
346
360
|
state.features[feature].skippedPhases = [...new Set([...existingSkipped, ...skippedPhases])];
|
|
347
361
|
}
|
|
362
|
+
|
|
363
|
+
// Ensure approvalGates object exists
|
|
364
|
+
if (!state.features[feature].approvalGates) {
|
|
365
|
+
state.features[feature].approvalGates = {};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
const gates = state.features[feature].approvalGates;
|
|
370
|
+
|
|
371
|
+
// Auto-approve proposal gate when advancing past the proposal phase.
|
|
372
|
+
// The act of advancing is itself the approval — no explicit gate check needed.
|
|
373
|
+
if (currentPhase === 'proposal' && !gates.proposal?.approved) {
|
|
374
|
+
gates.proposal = { approved: true, timestamp: now, approvedBy: 'workflow-advance' };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Persist gates approved implicitly (trust auto-approve or --skip-approval bypass).
|
|
378
|
+
if (gateApprovedImplicitly && !gates[gateApprovedImplicitly.gate]?.approved) {
|
|
379
|
+
gates[gateApprovedImplicitly.gate] = {
|
|
380
|
+
approved: true,
|
|
381
|
+
timestamp: now,
|
|
382
|
+
approvedBy: gateApprovedImplicitly.approvedBy
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Auto-approve gates for phases that were legitimately skipped.
|
|
387
|
+
// Skipped phases never run their explicit approve step, so their gate would
|
|
388
|
+
// otherwise stay false in state — making approval-status misleading.
|
|
389
|
+
for (const skippedPhase of skippedPhases) {
|
|
390
|
+
const skippedGate = APPROVAL_GATE_MAP[skippedPhase];
|
|
391
|
+
if (skippedGate && !gates[skippedGate]?.approved) {
|
|
392
|
+
gates[skippedGate] = { approved: true, timestamp: now, approvedBy: 'phase-skipped' };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
348
396
|
saveState(state);
|
|
349
397
|
|
|
350
398
|
// Run PhaseAdvanced agent-teams hook (non-blocking)
|
|
@@ -369,6 +417,9 @@ export async function advancePhaseCommand(feature, options = {}) {
|
|
|
369
417
|
await emitValidatorDispatch(feature, validationPhase, process.cwd());
|
|
370
418
|
}
|
|
371
419
|
|
|
420
|
+
// Emit SKILL DISPATCH for any phase that has requiredSkills
|
|
421
|
+
await emitSkillDispatch(nextPhase, process.cwd());
|
|
422
|
+
|
|
372
423
|
console.log(chalk.green(`\n✓ Advanced to ${nextPhaseDef.name}`));
|
|
373
424
|
|
|
374
425
|
// Show what's needed in the new phase
|
|
@@ -407,11 +458,14 @@ function getPhaseGuidance(phase, feature) {
|
|
|
407
458
|
'Provide layout references and preferences',
|
|
408
459
|
'Review wireframes before proceeding'
|
|
409
460
|
],
|
|
410
|
-
'design':
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
461
|
+
'design': (() => {
|
|
462
|
+
const { isDotnet } = getStackProfile();
|
|
463
|
+
return [
|
|
464
|
+
`Resume spec pipeline: /morph-proposal ${feature}`,
|
|
465
|
+
'Review DECISION POINTS carefully',
|
|
466
|
+
isDotnet ? 'Approve spec.md and contracts.cs' : 'Approve spec.md and contracts.ts (TypeScript + Zod schemas)'
|
|
467
|
+
];
|
|
468
|
+
})(),
|
|
415
469
|
'clarify': [
|
|
416
470
|
`Resume spec pipeline: /morph-proposal ${feature}`,
|
|
417
471
|
'Resolve edge cases and unknowns'
|
|
@@ -428,7 +482,7 @@ function getPhaseGuidance(phase, feature) {
|
|
|
428
482
|
],
|
|
429
483
|
'sync': [
|
|
430
484
|
'Review decisions.md for standards to promote',
|
|
431
|
-
`
|
|
485
|
+
`Decisions file: ${getOutputPath(feature, 'decisions')}`
|
|
432
486
|
]
|
|
433
487
|
};
|
|
434
488
|
|
|
@@ -466,7 +520,7 @@ function designSystemGate(feature, projectPath = '.') {
|
|
|
466
520
|
'Create a design system with one of these options:',
|
|
467
521
|
' 1. Project-level: .morph/context/design-system.md (shared across features)',
|
|
468
522
|
` 2. Feature-level: ${getOutputPath(feature, 'uiDesignSystem')} (feature-specific)`,
|
|
469
|
-
' 3.
|
|
523
|
+
' 3. Run /morph:phase-uiux skill to auto-generate from existing codebase',
|
|
470
524
|
'',
|
|
471
525
|
'Or remove UI agents if they are not needed:',
|
|
472
526
|
` morph-spec state remove-agent ${feature} blazor-builder`
|
|
@@ -522,3 +576,39 @@ async function emitValidatorDispatch(featureName, phase, cwd) {
|
|
|
522
576
|
// Non-blocking — fail silently
|
|
523
577
|
}
|
|
524
578
|
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Emit a SKILL DISPATCH block when advancing to a phase that has requiredSkills.
|
|
582
|
+
* Outputs structured JSON for the LLM to read and follow.
|
|
583
|
+
* Non-blocking — fails silently.
|
|
584
|
+
*
|
|
585
|
+
* @param {string} nextPhase - The phase being advanced into
|
|
586
|
+
* @param {string} cwd - Project root path (for locating phases.json)
|
|
587
|
+
*/
|
|
588
|
+
async function emitSkillDispatch(nextPhase, cwd) {
|
|
589
|
+
try {
|
|
590
|
+
// Try project-local phases.json first, fall back to package-level
|
|
591
|
+
const localPath = join(cwd, 'framework', 'phases.json');
|
|
592
|
+
const packagePath = join(__dirname, '../../../framework/phases.json');
|
|
593
|
+
const resolvedPath = existsSync(localPath) ? localPath : packagePath;
|
|
594
|
+
|
|
595
|
+
if (!existsSync(resolvedPath)) return;
|
|
596
|
+
|
|
597
|
+
const phasesData = JSON.parse(readFileSync(resolvedPath, 'utf8'));
|
|
598
|
+
const skills = phasesData?.phases?.[nextPhase]?.requiredSkills;
|
|
599
|
+
if (!skills || skills.length === 0) return;
|
|
600
|
+
|
|
601
|
+
const dispatch = {
|
|
602
|
+
skillDispatch: true,
|
|
603
|
+
phase: nextPhase,
|
|
604
|
+
skills,
|
|
605
|
+
instruction: `These skills are MANDATORY for phase '${nextPhase}'. Invoke them at the specified trigger points using Skill(). Do NOT skip them.`,
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
console.log(chalk.cyan('\n--- SKILL DISPATCH ---'));
|
|
609
|
+
console.log(JSON.stringify(dispatch, null, 2));
|
|
610
|
+
console.log(chalk.cyan('--- END SKILL DISPATCH ---\n'));
|
|
611
|
+
} catch {
|
|
612
|
+
// Non-blocking — fail silently
|
|
613
|
+
}
|
|
614
|
+
}
|
|
@@ -25,7 +25,7 @@ export async function unapproveCommand(featureName, gateName, options = {}) {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
setApprovalGate(featureName, gateName, false, {
|
|
28
|
+
await setApprovalGate(featureName, gateName, false, {
|
|
29
29
|
revokedBy: options.revoker || process.env.USER || process.env.USERNAME || 'user',
|
|
30
30
|
revokedAt: new Date().toISOString(),
|
|
31
31
|
reason: options.reason
|
|
@@ -85,7 +85,7 @@ export async function approveCommand(featureName, gateName, options = {}) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Set approval
|
|
88
|
-
setApprovalGate(featureName, gateName, true, {
|
|
88
|
+
await setApprovalGate(featureName, gateName, true, {
|
|
89
89
|
approvedBy: options.approver || process.env.USER || process.env.USERNAME || 'user',
|
|
90
90
|
approvedAt: new Date().toISOString(),
|
|
91
91
|
notes: options.notes
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State Management Commands
|
|
3
|
-
*/
|
|
4
|
-
export { stateCommand } from './state.js';
|
|
5
|
-
export { advancePhaseCommand } from './advance-phase.js';
|
|
6
|
-
export { approveCommand, unapproveCommand, approvalStatusCommand } from './approve.js';
|
|
7
|
-
export {
|
|
8
|
-
export { phaseRunCommand } from './phase-runner.js';
|
|
1
|
+
/**
|
|
2
|
+
* State Management Commands
|
|
3
|
+
*/
|
|
4
|
+
export { stateCommand } from './state.js';
|
|
5
|
+
export { advancePhaseCommand } from './advance-phase.js';
|
|
6
|
+
export { approveCommand, unapproveCommand, approvalStatusCommand } from './approve.js';
|
|
7
|
+
export { phaseRunCommand } from './phase-runner.js';
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import chalk from 'chalk';
|
|
25
25
|
import { join } from 'path';
|
|
26
|
-
import { checkPhaseEligibility, computePassRate } from '../../lib/phase-chain/
|
|
26
|
+
import { checkPhaseEligibility, computePassRate } from '../../lib/phase-chain/phase-validator.js';
|
|
27
27
|
import { advancePhaseCommand } from './advance-phase.js';
|
|
28
28
|
import { loadState } from '../../core/state/state-manager.js';
|
|
29
29
|
import { derivePhase } from '../../core/state/state-manager.js';
|
|
@@ -11,6 +11,36 @@ import { logger } from '../../utils/logger.js';
|
|
|
11
11
|
import * as StateManager from '../../core/state/state-manager.js';
|
|
12
12
|
import { derivePhase } from '../../core/state/state-manager.js';
|
|
13
13
|
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Phase Protection
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Phases that have approval gates and cannot be set directly via `state set`.
|
|
20
|
+
* These phases must be reached through `morph-spec phase advance` which enforces
|
|
21
|
+
* all validation gates (approval, output requirements, spec content, etc.).
|
|
22
|
+
*
|
|
23
|
+
* Lightweight phases (proposal, setup, clarify, sync) are not in this list
|
|
24
|
+
* because they have no approval gates and are safe to set directly.
|
|
25
|
+
*/
|
|
26
|
+
export const PROTECTED_PHASES = ['design', 'tasks', 'uiux', 'implement'];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate a direct phase-set request.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} phase - The target phase value
|
|
32
|
+
* @returns {{ blocked: boolean, message?: string }}
|
|
33
|
+
*/
|
|
34
|
+
export function validatePhaseSet(phase) {
|
|
35
|
+
if (PROTECTED_PHASES.includes(phase)) {
|
|
36
|
+
return {
|
|
37
|
+
blocked: true,
|
|
38
|
+
message: `Phase "${phase}" has approval gates — use "morph-spec phase advance" instead of setting it directly.`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { blocked: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
14
44
|
// ============================================================================
|
|
15
45
|
// Command Functions
|
|
16
46
|
// ============================================================================
|
|
@@ -174,10 +204,30 @@ async function setCommand(featureName, key, value, options) {
|
|
|
174
204
|
}
|
|
175
205
|
|
|
176
206
|
try {
|
|
177
|
-
//
|
|
207
|
+
// Guard: block protected phases unless --force is explicitly passed
|
|
178
208
|
if (key === 'phase') {
|
|
179
|
-
|
|
180
|
-
|
|
209
|
+
const phaseValidation = validatePhaseSet(value);
|
|
210
|
+
if (phaseValidation.blocked && !options.force) {
|
|
211
|
+
logger.error(phaseValidation.message);
|
|
212
|
+
logger.blank();
|
|
213
|
+
logger.dim(` Protected phases (have approval gates): ${PROTECTED_PHASES.join(', ')}`);
|
|
214
|
+
logger.dim(` Use the validated advance flow instead:`);
|
|
215
|
+
logger.dim(` morph-spec phase advance ${featureName}`);
|
|
216
|
+
logger.blank();
|
|
217
|
+
logger.dim(` To bypass all gates (expert use only):`);
|
|
218
|
+
logger.dim(` morph-spec state set ${featureName} phase ${value} --force`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (phaseValidation.blocked) {
|
|
223
|
+
logger.warn(`FORCED phase override to "${value}" — all approval gates bypassed.`);
|
|
224
|
+
} else {
|
|
225
|
+
logger.warn(`Direct phase override — validation gates may be bypassed.`);
|
|
226
|
+
logger.dim(` The actual phase is derived from filesystem subdirectories`);
|
|
227
|
+
logger.dim(` (0-proposal/, 1-design/, 3-tasks/, 4-implement/). This sets a state.json`);
|
|
228
|
+
logger.dim(` override used only for lightweight phases (setup, clarify, sync).`);
|
|
229
|
+
logger.dim(` Recommended: morph-spec phase advance ${featureName}`);
|
|
230
|
+
}
|
|
181
231
|
logger.blank();
|
|
182
232
|
}
|
|
183
233
|
|
|
@@ -328,13 +378,18 @@ async function markOutputCommand(featureName, outputType, options) {
|
|
|
328
378
|
logger.dim(' Usage: morph-spec state mark-output <feature> <output-type>');
|
|
329
379
|
logger.blank();
|
|
330
380
|
logger.dim(' Valid types:');
|
|
331
|
-
logger.dim('
|
|
332
|
-
logger.dim('
|
|
381
|
+
logger.dim(' Proposal: proposal');
|
|
382
|
+
logger.dim(' Design: schemaAnalysis, spec, contracts, contractsVsa, decisions');
|
|
383
|
+
logger.dim(' Clarify: clarifications');
|
|
384
|
+
logger.dim(' Tasks: tasks');
|
|
385
|
+
logger.dim(' UI/UX: uiDesignSystem, uiMockups, uiComponents, uiFlows');
|
|
386
|
+
logger.dim(' Implement: recap');
|
|
333
387
|
logger.blank();
|
|
334
|
-
logger.dim(' Note:
|
|
388
|
+
logger.dim(' Note: kebab-case aliases accepted (e.g., ui-design-system, schema-analysis)');
|
|
335
389
|
logger.blank();
|
|
336
390
|
logger.dim(' Examples:');
|
|
337
391
|
logger.dim(' morph-spec state mark-output my-feature spec');
|
|
392
|
+
logger.dim(' morph-spec state mark-output my-feature schemaAnalysis');
|
|
338
393
|
logger.dim(' morph-spec state mark-output my-feature uiDesignSystem');
|
|
339
394
|
logger.dim(' morph-spec state mark-output my-feature ui-design-system');
|
|
340
395
|
process.exit(1);
|