@polymorphism-tech/morph-spec 7.9.4 → 8.0.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 +2 -2
- package/README.md +31 -2
- package/bin/morph-spec.js +50 -2
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +38 -1
- package/docs/QUICKSTART.md +60 -1
- package/framework/CLAUDE.md +13 -2
- package/framework/MORPH.md +38 -2
- package/framework/agents.json +22 -3
- package/framework/commands/commit.md +166 -166
- package/framework/commands/morph-hotfix.md +39 -0
- 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/hooks/README.md +25 -10
- package/framework/hooks/claude-code/core/gate-guard.js +4 -2
- package/framework/hooks/claude-code/core/loop-detect.js +2 -1
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +14 -5
- package/framework/hooks/claude-code/core/tasks-json-guard.js +4 -2
- package/framework/hooks/claude-code/core/validate-completion.js +1 -1
- package/framework/hooks/claude-code/core/worktree-context.js +160 -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/build-check.js +2 -2
- package/framework/hooks/shared/hook-response.js +17 -5
- package/framework/hooks/shared/payload-utils.js +42 -0
- package/framework/hooks/shared/state-reader.js +22 -2
- package/framework/hooks/shared/stdin-reader.js +26 -26
- 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 +95 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +39 -8
- package/framework/skills/core/morph-brainstorming/SKILL.md +9 -1
- package/framework/skills/core/morph-eval/SKILL.md +6 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +136 -0
- package/framework/skills/core/morph-implement/SKILL.md +8 -4
- package/framework/skills/core/morph-plan/SKILL.md +5 -1
- package/framework/skills/core/morph-proposal/SKILL.md +24 -1
- package/framework/skills/core/morph-review/SKILL.md +3 -2
- 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/REGISTRY.json +0 -62
- 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/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 +4 -2
- package/src/commands/project/init.js +46 -2
- package/src/commands/project/update.js +5 -3
- package/src/commands/project/worktree.js +1005 -46
- package/src/commands/state/advance.js +17 -2
- package/src/commands/state/approve.js +18 -1
- package/src/commands/state/create.js +158 -14
- package/src/commands/state/retype.js +126 -0
- package/src/commands/state/score.js +4 -0
- package/src/commands/tasks/dag.js +32 -0
- package/src/commands/validation/index.js +1 -0
- package/src/commands/validation/validate-feature.js +33 -5
- package/src/commands/validation/verify.js +190 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +35 -10
- package/src/core/templates/template-renderer.js +477 -477
- package/src/core/work-types.js +79 -0
- 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 +11 -1
- 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/scanner/state-scanner.js +42 -0
- 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/installers/memory-installer.js +1 -1
- package/src/lib/risk-detector.js +16 -0
- package/src/lib/tasks/dispatch-planner.js +122 -4
- package/src/lib/tasks/mermaid-renderer.js +132 -0
- 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 +27 -6
- 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/lib/validators/verify-runner.js +322 -0
- package/src/lib/work-type-detector.js +143 -0
- package/src/scripts/setup-infra.js +32 -5
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +10 -6
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +43 -0
- package/src/utils/hooks-installer.js +41 -11
- package/src/utils/skills-installer.js +174 -174
- package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
- package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
- package/src/lib/tasks/wave-resolver.js +0 -353
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { resolve, join } from 'path';
|
|
4
|
+
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
5
|
+
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
6
|
+
import { runVerify } from '../../lib/validators/verify-runner.js';
|
|
7
|
+
import { resolvePrimaryRoot } from '../project/worktree.js';
|
|
8
|
+
|
|
9
|
+
/** ✓ pass · ✗ fail · – skip */
|
|
10
|
+
function statusIcon(status) {
|
|
11
|
+
if (status === 'pass') return chalk.green('✓');
|
|
12
|
+
if (status === 'fail') return chalk.red('✗');
|
|
13
|
+
return chalk.gray('–');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Human-readable rendering of a runVerify() result — mirrors the
|
|
18
|
+
* ✓/✗-per-node + feedback-list style of formatScoreBreakdown() in
|
|
19
|
+
* score-aggregator.js, adapted to verify's build/tests/validators nodes.
|
|
20
|
+
*/
|
|
21
|
+
function formatVerifyResult(result) {
|
|
22
|
+
const lines = [];
|
|
23
|
+
const scopeLabel = result.scope === 'task' ? `${result.feature}/${result.task}` : result.feature;
|
|
24
|
+
|
|
25
|
+
lines.push('');
|
|
26
|
+
lines.push(chalk.bold(`Verify — ${scopeLabel}`));
|
|
27
|
+
lines.push(chalk.gray(`rubric: ${result.rubric}${result.stack ? ` | stack: ${result.stack}` : ''}`));
|
|
28
|
+
lines.push('');
|
|
29
|
+
lines.push(result.status === 'pass' ? chalk.green.bold('✓ PASS') : chalk.red.bold('✗ FAIL'));
|
|
30
|
+
lines.push('');
|
|
31
|
+
lines.push('Nodes:');
|
|
32
|
+
for (const [name, node] of Object.entries(result.nodes)) {
|
|
33
|
+
const icon = statusIcon(node.status);
|
|
34
|
+
let detail;
|
|
35
|
+
if (node.status === 'skip') {
|
|
36
|
+
detail = node.reason || '';
|
|
37
|
+
} else if (name === 'validators' && node.score != null) {
|
|
38
|
+
detail = `score ${node.score}/10`;
|
|
39
|
+
} else {
|
|
40
|
+
detail = `${node.durationMs}ms`;
|
|
41
|
+
}
|
|
42
|
+
lines.push(` ${icon} ${name.padEnd(11)} ${node.status.padEnd(5)} ${chalk.gray(detail)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (result.feedback.length > 0) {
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push('Feedback:');
|
|
48
|
+
for (const line of result.feedback) lines.push(` ${line}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lines.push('');
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decide whether it's safe to persist a verify result to feature.json.
|
|
57
|
+
*
|
|
58
|
+
* Safe contexts: the primary git root, or the feature's own REGISTERED
|
|
59
|
+
* worktree (`worktree.path` — where feature.json is authoritative and
|
|
60
|
+
* travels with the branch). Any other worktree (e.g. a harness subagent's
|
|
61
|
+
* temporary scratch worktree) must never write feature.json — that would
|
|
62
|
+
* risk a merge conflict / violate the single-writer clause.
|
|
63
|
+
*
|
|
64
|
+
* Worktree registration is ROOT-OWNED data: `runWorktreeSetup` (see
|
|
65
|
+
* src/commands/project/worktree.js) writes `.worktree` ONLY into the PRIMARY
|
|
66
|
+
* ROOT's copy of feature.json (via saveState after chdir'ing back to root) —
|
|
67
|
+
* it never touches the worktree's own branch-local copy. So this must read
|
|
68
|
+
* the ROOT's feature.json to find the registration, never the cwd-local one
|
|
69
|
+
* (`getFeature(featureName)` resolves relative to `process.cwd()`, which
|
|
70
|
+
* inside a worktree is the branch-local copy and will never carry
|
|
71
|
+
* `.worktree` — reading it here would make this branch permanently dead code
|
|
72
|
+
* for the exact case it exists to handle: verify running from inside a
|
|
73
|
+
* registered worktree).
|
|
74
|
+
*
|
|
75
|
+
* @param {{ cwd: string, featureName: string, noPersist: boolean }} params
|
|
76
|
+
* @returns {{ persist: boolean, reason: string|null }}
|
|
77
|
+
*/
|
|
78
|
+
function decidePersistence({ cwd, featureName, noPersist }) {
|
|
79
|
+
if (noPersist) return { persist: false, reason: 'no-persist-flag' };
|
|
80
|
+
|
|
81
|
+
const root = resolvePrimaryRoot(cwd);
|
|
82
|
+
const normalizedCwd = resolve(cwd).replace(/\\/g, '/');
|
|
83
|
+
|
|
84
|
+
if (normalizedCwd === root) return { persist: true, reason: null };
|
|
85
|
+
|
|
86
|
+
// cwd is (or looks like) a worktree — consult the ROOT's feature.json for
|
|
87
|
+
// the registration. Fail-open: an unreadable/missing root feature.json
|
|
88
|
+
// means we can't confirm registration, so treat as unregistered.
|
|
89
|
+
let registeredWt = null;
|
|
90
|
+
try {
|
|
91
|
+
const rootFeatureJsonPath = join(root, '.morph', 'features', featureName, 'feature.json');
|
|
92
|
+
const rootFeature = JSON.parse(readFileSync(rootFeatureJsonPath, 'utf8'));
|
|
93
|
+
registeredWt = rootFeature.worktree?.path
|
|
94
|
+
? String(rootFeature.worktree.path).replace(/\\/g, '/')
|
|
95
|
+
: null;
|
|
96
|
+
} catch {
|
|
97
|
+
registeredWt = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (registeredWt && normalizedCwd === registeredWt) return { persist: true, reason: null };
|
|
101
|
+
return { persist: false, reason: 'unregistered-worktree' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Read tasks.json's task list for a feature; [] if missing/corrupt. */
|
|
105
|
+
function readTasks(featureName) {
|
|
106
|
+
try {
|
|
107
|
+
const tasksPath = getAbsoluteOutputPath(process.cwd(), featureName, 'tasks');
|
|
108
|
+
if (!existsSync(tasksPath)) return [];
|
|
109
|
+
return JSON.parse(readFileSync(tasksPath, 'utf8')).tasks || [];
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `morph-spec verify <feature> [task]` — the deterministic code-before-judge
|
|
117
|
+
* gate: build + tests + validators, BEFORE the LLM eval. See verify-runner.js
|
|
118
|
+
* for the node cadence; this command wraps it with CLI ergonomics
|
|
119
|
+
* (human/JSON output, exit codes) and context-aware persistence.
|
|
120
|
+
*
|
|
121
|
+
* Exit codes: 0 = pass (including all-skip) · 1 = fail (>=1 red node) ·
|
|
122
|
+
* 2 = usage error (unknown feature or task — the task's `group` decides the
|
|
123
|
+
* rubric, so an unknown task id is a hard usage error here, unlike the
|
|
124
|
+
* advisory warning `morph-spec score` gives for the same situation).
|
|
125
|
+
*/
|
|
126
|
+
export async function verifyCommand(featureName, taskId, options = {}) {
|
|
127
|
+
let feature;
|
|
128
|
+
try {
|
|
129
|
+
feature = getFeature(featureName);
|
|
130
|
+
} catch {
|
|
131
|
+
feature = null;
|
|
132
|
+
}
|
|
133
|
+
if (!feature) {
|
|
134
|
+
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (taskId) {
|
|
139
|
+
const tasks = readTasks(featureName);
|
|
140
|
+
if (!tasks.some((t) => t.id === taskId)) {
|
|
141
|
+
console.error(chalk.red(`\n❌ Task "${taskId}" not found in tasks.json for feature "${featureName}"\n`));
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = await runVerify(process.cwd(), featureName, taskId || null, {
|
|
147
|
+
rubric: options.rubric,
|
|
148
|
+
timeout: options.timeout,
|
|
149
|
+
skipTests: options.skipTests,
|
|
150
|
+
skipBuild: options.skipBuild,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const scopeKey = taskId ? `taskVerifications.${taskId}` : 'verifications.feature';
|
|
154
|
+
const { persist, reason } = decidePersistence({
|
|
155
|
+
cwd: process.cwd(),
|
|
156
|
+
featureName,
|
|
157
|
+
noPersist: options.noPersist,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
let compact = null;
|
|
161
|
+
if (persist) {
|
|
162
|
+
const prevEntry = taskId ? feature.taskVerifications?.[taskId] : feature.verifications?.feature;
|
|
163
|
+
compact = {
|
|
164
|
+
status: result.status,
|
|
165
|
+
nodes: {
|
|
166
|
+
build: result.nodes.build.status,
|
|
167
|
+
tests: result.nodes.tests.status,
|
|
168
|
+
validators: result.nodes.validators.status,
|
|
169
|
+
},
|
|
170
|
+
score: result.nodes.validators.score ?? null,
|
|
171
|
+
attempts: (prevEntry?.attempts ?? 0) + 1,
|
|
172
|
+
updatedAt: new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
await updateFeature(featureName, scopeKey, compact);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (options.json) {
|
|
178
|
+
const output = { ...result, persisted: persist, ...(persist ? {} : { reason }) };
|
|
179
|
+
console.log(JSON.stringify(output, null, 2));
|
|
180
|
+
} else {
|
|
181
|
+
console.log(formatVerifyResult(result));
|
|
182
|
+
if (persist) {
|
|
183
|
+
console.log(chalk.gray(`Persisted: ${scopeKey} (attempts: ${compact.attempts})\n`));
|
|
184
|
+
} else {
|
|
185
|
+
console.log(chalk.yellow(`⚠️ Not persisted (${reason})\n`));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
process.exit(result.status === 'pass' ? 0 : 1);
|
|
190
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -87,7 +87,7 @@ function readProjectFromConfig() {
|
|
|
87
87
|
* Atomic JSON write: per-process temp file then rename, with an EPERM retry loop
|
|
88
88
|
* for the brief Windows lock race. Creates the parent directory if needed.
|
|
89
89
|
*/
|
|
90
|
-
function atomicWriteJson(targetPath, data) {
|
|
90
|
+
export function atomicWriteJson(targetPath, data) {
|
|
91
91
|
const dir = dirname(targetPath);
|
|
92
92
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
93
93
|
|
|
@@ -118,8 +118,14 @@ function atomicWriteJson(targetPath, data) {
|
|
|
118
118
|
* mutation on feature A would otherwise rewrite sibling B's file from A's loaded
|
|
119
119
|
* copy. Skipping unchanged siblings means A never touches B's authoritative file
|
|
120
120
|
* in the common case — shrinking the cross-feature clobber window to nothing
|
|
121
|
-
* unless B genuinely changed under A.
|
|
122
|
-
*
|
|
121
|
+
* unless B genuinely changed under A.
|
|
122
|
+
*
|
|
123
|
+
* A formatting mismatch is NOT free, though: the guard compares bytes, so a
|
|
124
|
+
* sibling committed with a trailing newline fails it and gets rewritten. The
|
|
125
|
+
* content is equivalent, but git sees ` M` on a file this caller never meant to
|
|
126
|
+
* touch — enough to fail a whole-tree clean check (see worktreeFinishCommand).
|
|
127
|
+
* And the guard only skips IDENTICAL content: a concurrent instance's CHANGED
|
|
128
|
+
* write to a sibling would still be clobbered by the sweep.
|
|
123
129
|
*/
|
|
124
130
|
function writeFeatureJson(featureName, feature) {
|
|
125
131
|
const path = getFeatureJsonPath(featureName);
|
|
@@ -327,8 +333,17 @@ export function saveState(state) {
|
|
|
327
333
|
state.metadata.completedFeatures = Object.values(features)
|
|
328
334
|
.filter((f) => f && f.status === 'completed').length;
|
|
329
335
|
|
|
330
|
-
// 1) Persist each feature to its authoritative, committed feature.json
|
|
331
|
-
//
|
|
336
|
+
// 1) Persist each feature to its authoritative, committed feature.json — what
|
|
337
|
+
// travels with the feature's own commit.
|
|
338
|
+
//
|
|
339
|
+
// NOTE: this IS a cross-feature sweep. Mutating one feature re-persists
|
|
340
|
+
// every sibling; writeFeatureJson's byte-identical guard makes that a no-op
|
|
341
|
+
// only while a sibling's committed file matches our formatting exactly. A
|
|
342
|
+
// sibling saved with a trailing newline (any editor, prettier) is rewritten
|
|
343
|
+
// and shows up as ` M` — dirtying the tree on behalf of a feature the
|
|
344
|
+
// caller never touched. Callers that need to change ONE feature's field
|
|
345
|
+
// should write that feature.json directly instead of round-tripping state
|
|
346
|
+
// through here (see worktreeFinishCommand's registration cleanup).
|
|
332
347
|
const thinFeatures = {};
|
|
333
348
|
for (const [name, feature] of Object.entries(features)) {
|
|
334
349
|
writeFeatureJson(name, feature);
|
|
@@ -487,19 +502,24 @@ async function ensureFeature(featureName) {
|
|
|
487
502
|
/**
|
|
488
503
|
* Explicitly create a feature with the canonical shape (documented in
|
|
489
504
|
* framework/schemas/feature.schema.json). Idempotent: an existing feature is
|
|
490
|
-
* returned untouched
|
|
491
|
-
* `morph-spec
|
|
492
|
-
*
|
|
505
|
+
* returned untouched (including its `workType` — a later `--type` on a
|
|
506
|
+
* repeat call never overwrites it; see `morph-spec retype` for that). This
|
|
507
|
+
* is the server-side birth path behind `morph-spec create` — replacing the
|
|
508
|
+
* implicit registration on the first Write into .morph/features/ (the
|
|
509
|
+
* state-sync hook remains as fallback).
|
|
493
510
|
*
|
|
494
511
|
* Works even before any state exists (scaffolds the root index), so a
|
|
495
512
|
* feature can be born as the very first morph action in a project.
|
|
496
513
|
*
|
|
497
514
|
* @param {string} featureName - Feature name (kebab-case)
|
|
498
|
-
* @param {{ status?: string }} [options]
|
|
515
|
+
* @param {{ status?: string, workType?: string, workTypeDetection?: object }} [options]
|
|
516
|
+
* `workType` defaults to 'feature' (v8 routing — see src/core/work-types.js).
|
|
517
|
+
* `workTypeDetection` is only written when provided (source/confidence/matched
|
|
518
|
+
* audit trail from the `--type`/`--request` resolution in `create.js`).
|
|
499
519
|
* @returns {Promise<{ feature: Object, created: boolean }>}
|
|
500
520
|
*/
|
|
501
521
|
export async function createFeature(featureName, options = {}) {
|
|
502
|
-
const { status = 'in_progress' } = options;
|
|
522
|
+
const { status = 'in_progress', workType, workTypeDetection } = options;
|
|
503
523
|
const state = loadState(false) || scaffoldState();
|
|
504
524
|
|
|
505
525
|
const existing = state.features[featureName];
|
|
@@ -510,6 +530,7 @@ export async function createFeature(featureName, options = {}) {
|
|
|
510
530
|
const now = new Date().toISOString();
|
|
511
531
|
state.features[featureName] = {
|
|
512
532
|
status,
|
|
533
|
+
workType: workType || 'feature',
|
|
513
534
|
createdAt: now,
|
|
514
535
|
updatedAt: now,
|
|
515
536
|
activeAgents: [],
|
|
@@ -534,6 +555,10 @@ export async function createFeature(featureName, options = {}) {
|
|
|
534
555
|
fileChanges: []
|
|
535
556
|
};
|
|
536
557
|
|
|
558
|
+
if (workTypeDetection) {
|
|
559
|
+
state.features[featureName].workTypeDetection = workTypeDetection;
|
|
560
|
+
}
|
|
561
|
+
|
|
537
562
|
state.metadata.totalFeatures = Object.keys(state.features).length;
|
|
538
563
|
saveState(state);
|
|
539
564
|
return { feature: state.features[featureName], created: true };
|