@polymorphism-tech/morph-spec 7.4.0 → 7.5.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 +18 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +9 -4
- package/framework/MORPH.md +4 -4
- package/framework/commands/create-umbraco-lp.md +35 -0
- package/framework/commands/migrate-lp-to-umbraco.md +33 -0
- package/framework/commands/morph-archive.md +17 -27
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +115 -90
- package/framework/hooks/claude-code/core/protect-spec-files.js +32 -30
- package/framework/hooks/claude-code/core/state-sync.js +16 -48
- package/framework/hooks/shared/state-reader.js +74 -8
- package/framework/hooks/shared/state-writer.js +124 -0
- package/framework/rules/morph-task-tracking.md +69 -49
- package/framework/rules/morph-workflow.md +192 -191
- package/framework/skills/core/create-umbraco-lp/SKILL.md +252 -0
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +452 -0
- package/framework/skills/core/morph-apply/SKILL.md +15 -15
- package/framework/skills/core/morph-brainstorming/SKILL.md +5 -5
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +5 -4
- package/framework/skills/core/morph-plan/SKILL.md +6 -6
- package/framework/skills/core/morph-proposal/SKILL.md +8 -7
- package/framework/skills/core/morph-review/SKILL.md +3 -3
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/templates/REGISTRY.json +1 -3
- package/framework/templates/context/CONTEXT-FEATURE.md +10 -17
- package/framework/templates/context/CONTEXT.md +181 -180
- package/framework/templates/docs/onboarding.md +122 -118
- package/framework/templates/docs/proposal.md +10 -0
- package/framework/templates/docs/spec.md +228 -228
- package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -80
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +90 -89
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +2 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +44 -43
- package/framework/templates/meta-prompts/validators/checkpoint-validator.md +107 -107
- package/package.json +1 -1
- package/src/commands/project/update.js +415 -368
- package/src/commands/project/worktree.js +212 -210
- package/src/commands/state/advance.js +82 -0
- package/src/commands/state/archive.js +111 -0
- package/src/commands/validation/validate-feature.js +229 -105
- package/src/core/state/state-manager.js +238 -66
- package/src/dashboard/scanner/state-scanner.js +35 -8
- package/src/lib/generators/context-generator.js +12 -8
- package/src/utils/file-copier.js +7 -0
|
@@ -11,11 +11,9 @@
|
|
|
11
11
|
* - AskUserQuestion answered with option starting "aprovado" → advance gate
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { readFileSync
|
|
15
|
-
import { join } from 'path';
|
|
14
|
+
import { readFileSync } from 'fs';
|
|
16
15
|
import { loadState, stateExists } from '../../shared/state-reader.js';
|
|
17
|
-
|
|
18
|
-
const STATE_FILE = '.morph/state.json';
|
|
16
|
+
import { persistFeature, setGateApproved, createDefaultFeature } from '../../shared/state-writer.js';
|
|
19
17
|
|
|
20
18
|
const PHASE_FOLDERS = {
|
|
21
19
|
'0-proposal': 'proposal',
|
|
@@ -64,31 +62,12 @@ function extractFilename(filePath) {
|
|
|
64
62
|
return normalized.split('/').pop();
|
|
65
63
|
}
|
|
66
64
|
|
|
67
|
-
/**
|
|
68
|
-
* Atomic write — temp file then rename. Mirrors state-manager.js semantics.
|
|
69
|
-
*/
|
|
70
|
-
function writeStateAtomic(cwd, state) {
|
|
71
|
-
const path = join(cwd, STATE_FILE);
|
|
72
|
-
const tmpPath = `${path}.tmp.${process.pid}`;
|
|
73
|
-
writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf8');
|
|
74
|
-
renameSync(tmpPath, path);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
65
|
function registerFeatureIfMissing(state, featureName) {
|
|
78
66
|
if (!state.features) state.features = {};
|
|
79
67
|
if (state.features[featureName]) return false;
|
|
80
68
|
|
|
81
|
-
|
|
82
|
-
state.features[featureName] =
|
|
83
|
-
status: 'in_progress',
|
|
84
|
-
createdAt: now,
|
|
85
|
-
updatedAt: now,
|
|
86
|
-
approvalGates: { proposal: false, plan: false, review: false },
|
|
87
|
-
outputs: {},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
if (!state.metadata) state.metadata = {};
|
|
91
|
-
state.metadata.totalFeatures = Object.keys(state.features).length;
|
|
69
|
+
// Canonical object-form gates (incl. optional uiux) — no boolean drift.
|
|
70
|
+
state.features[featureName] = createDefaultFeature();
|
|
92
71
|
return true;
|
|
93
72
|
}
|
|
94
73
|
|
|
@@ -151,7 +130,9 @@ function handleWriteEdit(input, cwd) {
|
|
|
151
130
|
}
|
|
152
131
|
|
|
153
132
|
if (changed) {
|
|
154
|
-
|
|
133
|
+
// Persist the touched feature to its authoritative feature.json and refresh
|
|
134
|
+
// the thin root index. The feature's state thus travels in its own folder.
|
|
135
|
+
persistFeature(featureName, state.features[featureName], cwd);
|
|
155
136
|
}
|
|
156
137
|
return true;
|
|
157
138
|
}
|
|
@@ -166,34 +147,21 @@ function handleAskUserQuestion(input, cwd) {
|
|
|
166
147
|
const featureName = findActiveFeature(state);
|
|
167
148
|
if (!featureName) return false;
|
|
168
149
|
|
|
169
|
-
let changed = false;
|
|
170
150
|
for (const [question, answer] of Object.entries(answers)) {
|
|
171
151
|
if (!isApprovalAnswer(answer)) continue;
|
|
172
152
|
const gate = gateFromQuestion(question);
|
|
173
153
|
if (!gate) continue;
|
|
174
154
|
|
|
175
|
-
state.features[featureName].approvalGates
|
|
176
|
-
const existing = state.features[featureName].approvalGates[gate];
|
|
155
|
+
const existing = state.features[featureName].approvalGates?.[gate];
|
|
177
156
|
const alreadyApproved = existing === true || existing?.approved === true;
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
approved: true,
|
|
187
|
-
timestamp: now,
|
|
188
|
-
approvedBy: 'manual'
|
|
189
|
-
};
|
|
190
|
-
state.features[featureName].updatedAt = now;
|
|
191
|
-
changed = true;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (changed) {
|
|
196
|
-
writeStateAtomic(cwd, state);
|
|
157
|
+
if (alreadyApproved) continue;
|
|
158
|
+
|
|
159
|
+
// An "aprovado" answer to AskUserQuestion is a human approving in the loop.
|
|
160
|
+
// Route through the shared canonical gate writer so the hook and the CLI
|
|
161
|
+
// (`morph-spec approve`) produce one consistent shape
|
|
162
|
+
// ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
|
|
163
|
+
// and a manual approval stays distinguishable from a policy auto-approval.
|
|
164
|
+
setGateApproved(featureName, gate, { approvedBy: 'manual' }, cwd);
|
|
197
165
|
}
|
|
198
166
|
return true;
|
|
199
167
|
}
|
|
@@ -7,22 +7,62 @@
|
|
|
7
7
|
* - Uses synchronous reads for <100ms performance
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
12
|
|
|
13
13
|
const STATE_FILE = '.morph/state.json';
|
|
14
|
+
const FEATURES_DIR = '.morph/features';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
* Load state
|
|
17
|
-
*
|
|
17
|
+
* Load state from the project root, hydrating each feature from its
|
|
18
|
+
* authoritative per-feature `feature.json`.
|
|
19
|
+
*
|
|
20
|
+
* The root `.morph/state.json` is a thin, gitignored index (status + updatedAt
|
|
21
|
+
* per feature); the heavy feature data lives in `.morph/features/{name}/feature.json`
|
|
22
|
+
* (committed). This reader merges them: it reads the thin index for project /
|
|
23
|
+
* metadata and overlays each feature.json so callers see the full feature object.
|
|
24
|
+
* A single corrupt feature.json is skipped, not fatal.
|
|
25
|
+
*
|
|
26
|
+
* Fail-open: returns null on any unexpected error or when there is no state at
|
|
27
|
+
* all (no index and no feature files).
|
|
18
28
|
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
19
29
|
* @returns {Object|null} State object or null
|
|
20
30
|
*/
|
|
21
31
|
export function loadState(projectPath) {
|
|
22
32
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
const base = projectPath || process.cwd();
|
|
34
|
+
const statePath = join(base, STATE_FILE);
|
|
35
|
+
|
|
36
|
+
let state = null;
|
|
37
|
+
if (existsSync(statePath)) {
|
|
38
|
+
state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const featuresDir = join(base, FEATURES_DIR);
|
|
42
|
+
if (existsSync(featuresDir)) {
|
|
43
|
+
let names = [];
|
|
44
|
+
try {
|
|
45
|
+
names = readdirSync(featuresDir, { withFileTypes: true })
|
|
46
|
+
.filter((e) => e.isDirectory())
|
|
47
|
+
.map((e) => e.name);
|
|
48
|
+
} catch {
|
|
49
|
+
names = [];
|
|
50
|
+
}
|
|
51
|
+
for (const name of names) {
|
|
52
|
+
const fjPath = join(featuresDir, name, 'feature.json');
|
|
53
|
+
if (!existsSync(fjPath)) continue;
|
|
54
|
+
try {
|
|
55
|
+
const fj = JSON.parse(readFileSync(fjPath, 'utf-8'));
|
|
56
|
+
state = state || { features: {} };
|
|
57
|
+
state.features = state.features || {};
|
|
58
|
+
state.features[name] = fj; // feature.json is authoritative
|
|
59
|
+
} catch {
|
|
60
|
+
// skip a single corrupt feature.json — don't blind every hook
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return state;
|
|
26
66
|
} catch {
|
|
27
67
|
return null;
|
|
28
68
|
}
|
|
@@ -119,6 +159,25 @@ export function getFeature(featureName, projectPath) {
|
|
|
119
159
|
* @param {string} [projectPath]
|
|
120
160
|
* @returns {string} Phase name
|
|
121
161
|
*/
|
|
162
|
+
export const PHASE_INDEX = { proposal: 0, uiux: 1, plan: 2, implement: 3, review: 4 };
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The phase implied purely by approved gates, independent of which artifacts
|
|
166
|
+
* exist on disk. An approved gate authorizes the NEXT phase, so:
|
|
167
|
+
* review approved → review (terminal)
|
|
168
|
+
* plan approved → implement
|
|
169
|
+
* uiux approved → plan
|
|
170
|
+
* otherwise → proposal (proposal-approval's fork between uiux/plan is
|
|
171
|
+
* resolved by enforce-phase-writes, not here)
|
|
172
|
+
* @returns {string} phase name
|
|
173
|
+
*/
|
|
174
|
+
export function gateImpliedPhase(featureName, projectPath) {
|
|
175
|
+
if (isGateApproved(featureName, 'review', projectPath)) return 'review';
|
|
176
|
+
if (isGateApproved(featureName, 'plan', projectPath)) return 'implement';
|
|
177
|
+
if (isGateApproved(featureName, 'uiux', projectPath)) return 'plan';
|
|
178
|
+
return 'proposal';
|
|
179
|
+
}
|
|
180
|
+
|
|
122
181
|
export function derivePhaseForFeature(featureName, projectPath) {
|
|
123
182
|
const basePath = join(projectPath || process.cwd(), '.morph/features', featureName);
|
|
124
183
|
const phaseMap = [
|
|
@@ -128,10 +187,17 @@ export function derivePhaseForFeature(featureName, projectPath) {
|
|
|
128
187
|
['1-design', 'uiux', 'design-system.md'],
|
|
129
188
|
['0-proposal', 'proposal', 'proposal.md'],
|
|
130
189
|
];
|
|
190
|
+
let artifactPhase = 'proposal';
|
|
131
191
|
for (const [folder, phase, canonical] of phaseMap) {
|
|
132
|
-
if (existsSync(join(basePath, folder, canonical)))
|
|
192
|
+
if (existsSync(join(basePath, folder, canonical))) { artifactPhase = phase; break; }
|
|
133
193
|
}
|
|
134
|
-
|
|
194
|
+
|
|
195
|
+
// Gate-aware: an approved gate advances the phase even before the next
|
|
196
|
+
// phase's first canonical artifact exists. This breaks the chicken-and-egg
|
|
197
|
+
// where that first artifact could never be written because the phase never
|
|
198
|
+
// advanced to permit it. Take whichever of (artifact, gate-implied) is later.
|
|
199
|
+
const implied = gateImpliedPhase(featureName, projectPath);
|
|
200
|
+
return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
|
|
135
201
|
}
|
|
136
202
|
|
|
137
203
|
/**
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared per-feature state writer for Claude Code hooks.
|
|
3
|
+
*
|
|
4
|
+
* Hooks run inside the user's project and cannot import the CLI's
|
|
5
|
+
* src/core/state-manager.js. This module is the hook-side mirror of the core
|
|
6
|
+
* writers: it persists a feature to its authoritative `feature.json` (committed)
|
|
7
|
+
* and refreshes the thin, gitignored root index (`.morph/state.json`).
|
|
8
|
+
*
|
|
9
|
+
* Fail-open by contract: every function swallows errors and returns a boolean —
|
|
10
|
+
* a hook must never break the agent over a state write.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
|
|
16
|
+
const STATE_FILE = '.morph/state.json';
|
|
17
|
+
const STATE_VERSION = '5.0.0';
|
|
18
|
+
|
|
19
|
+
/** Absolute path to a feature's authoritative feature.json. */
|
|
20
|
+
export function featureJsonPath(featureName, projectPath) {
|
|
21
|
+
return join(projectPath || process.cwd(), '.morph', 'features', featureName, 'feature.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Read a feature's feature.json, or null if missing/corrupt. */
|
|
25
|
+
export function readFeatureFile(featureName, projectPath) {
|
|
26
|
+
const p = featureJsonPath(featureName, projectPath);
|
|
27
|
+
if (!existsSync(p)) return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Atomic JSON write: per-pid temp then rename, with an EPERM retry for Windows. */
|
|
36
|
+
function atomicWriteJson(targetPath, data) {
|
|
37
|
+
const dir = dirname(targetPath);
|
|
38
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
|
+
const tmp = `${targetPath}.tmp.${process.pid}`;
|
|
40
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
41
|
+
for (let attempt = 0; attempt <= 5; attempt++) {
|
|
42
|
+
try {
|
|
43
|
+
renameSync(tmp, targetPath);
|
|
44
|
+
return;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.code === 'EPERM' && attempt < 5) {
|
|
47
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (attempt + 1));
|
|
48
|
+
} else {
|
|
49
|
+
try { unlinkSync(tmp); } catch { /* ignore */ }
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Persist a feature to its authoritative feature.json and refresh the thin root
|
|
58
|
+
* index entry (status + updatedAt) + aggregate metadata. Returns true on
|
|
59
|
+
* success, false fail-open.
|
|
60
|
+
*/
|
|
61
|
+
export function persistFeature(featureName, feature, projectPath) {
|
|
62
|
+
try {
|
|
63
|
+
atomicWriteJson(featureJsonPath(featureName, projectPath), feature);
|
|
64
|
+
|
|
65
|
+
const statePath = join(projectPath || process.cwd(), STATE_FILE);
|
|
66
|
+
let root = {};
|
|
67
|
+
if (existsSync(statePath)) {
|
|
68
|
+
try { root = JSON.parse(readFileSync(statePath, 'utf-8')); } catch { root = {}; }
|
|
69
|
+
}
|
|
70
|
+
root.version = root.version || STATE_VERSION;
|
|
71
|
+
root.features = root.features || {};
|
|
72
|
+
root.features[featureName] = { status: feature.status, updatedAt: feature.updatedAt };
|
|
73
|
+
root.metadata = root.metadata || {};
|
|
74
|
+
root.metadata.totalFeatures = Object.keys(root.features).length;
|
|
75
|
+
atomicWriteJson(statePath, root);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Canonical gate-approval writer (hook-side mirror of CLI setApprovalGate).
|
|
84
|
+
* Stamps approvalGates[gate] = { approved, timestamp, approvedBy } — the single
|
|
85
|
+
* canonical shape — onto the feature and persists it. Returns true on success.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} featureName
|
|
88
|
+
* @param {string} gate - 'proposal'|'uiux'|'plan'|'review'
|
|
89
|
+
* @param {{ approvedBy?: string, timestamp?: string }} [meta]
|
|
90
|
+
* @param {string} [projectPath]
|
|
91
|
+
*/
|
|
92
|
+
export function setGateApproved(featureName, gate, meta = {}, projectPath) {
|
|
93
|
+
try {
|
|
94
|
+
const feature = readFeatureFile(featureName, projectPath) || createDefaultFeature();
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
feature.approvalGates = feature.approvalGates || {};
|
|
97
|
+
feature.approvalGates[gate] = {
|
|
98
|
+
approved: true,
|
|
99
|
+
timestamp: meta.timestamp || now,
|
|
100
|
+
approvedBy: meta.approvedBy || 'manual',
|
|
101
|
+
};
|
|
102
|
+
feature.updatedAt = now;
|
|
103
|
+
return persistFeature(featureName, feature, projectPath);
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Default feature shape (canonical object-form gates, incl. optional uiux). */
|
|
110
|
+
export function createDefaultFeature() {
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
return {
|
|
113
|
+
status: 'in_progress',
|
|
114
|
+
createdAt: now,
|
|
115
|
+
updatedAt: now,
|
|
116
|
+
approvalGates: {
|
|
117
|
+
proposal: { approved: false, timestamp: null, approvedBy: null },
|
|
118
|
+
uiux: { approved: false, timestamp: null, approvedBy: null },
|
|
119
|
+
plan: { approved: false, timestamp: null, approvedBy: null },
|
|
120
|
+
review: { approved: false, timestamp: null, approvedBy: null },
|
|
121
|
+
},
|
|
122
|
+
outputs: {},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -1,49 +1,69 @@
|
|
|
1
|
-
---
|
|
2
|
-
stacks:
|
|
3
|
-
- "*"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# MORPH-SPEC Task Tracking
|
|
7
|
-
|
|
8
|
-
> Session tasks are MANDATORY in every MORPH-SPEC phase. They track real progress and must be
|
|
9
|
-
> created BEFORE any phase work begins.
|
|
10
|
-
|
|
11
|
-
## Rules
|
|
12
|
-
|
|
13
|
-
1. **Every phase skill MUST create session tasks** using `TaskCreate` before reading files or generating outputs
|
|
14
|
-
2. **Tasks are dynamic** — analyze the phase workflow and create tasks that reflect YOUR actual planned actions for this session
|
|
15
|
-
3. **Every task MUST be updated**: `in_progress` before starting, `completed` after finishing
|
|
16
|
-
4. **Pipeline orchestrators** (`/morph-proposal`, `/morph-apply`) MUST create master tasks for each phase
|
|
17
|
-
5. **Never skip task creation** — even if resuming mid-phase, create tasks for remaining steps
|
|
18
|
-
6. **Granularity**: minimum 2, typically 3-7 tasks per phase — adjust to complexity
|
|
19
|
-
7. **For iterative work** (clarification rounds, quality loops): update task `subject` with round/score progress
|
|
20
|
-
|
|
21
|
-
## Task Update Pattern
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
TaskUpdate(taskId, status="in_progress") // before starting the step
|
|
25
|
-
// ... do the work ...
|
|
26
|
-
TaskUpdate(taskId, status="completed") // after step finishes
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
For iterative tasks:
|
|
30
|
-
```
|
|
31
|
-
TaskUpdate(taskId, subject="Round 3 — score 82/100")
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
##
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
---
|
|
2
|
+
stacks:
|
|
3
|
+
- "*"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MORPH-SPEC Task Tracking
|
|
7
|
+
|
|
8
|
+
> Session tasks are MANDATORY in every MORPH-SPEC phase. They track real progress and must be
|
|
9
|
+
> created BEFORE any phase work begins.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
1. **Every phase skill MUST create session tasks** using `TaskCreate` before reading files or generating outputs
|
|
14
|
+
2. **Tasks are dynamic** — analyze the phase workflow and create tasks that reflect YOUR actual planned actions for this session
|
|
15
|
+
3. **Every task MUST be updated**: `in_progress` before starting, `completed` after finishing
|
|
16
|
+
4. **Pipeline orchestrators** (`/morph-proposal`, `/morph-apply`) MUST create master tasks for each phase
|
|
17
|
+
5. **Never skip task creation** — even if resuming mid-phase, create tasks for remaining steps
|
|
18
|
+
6. **Granularity**: minimum 2, typically 3-7 tasks per phase — adjust to complexity
|
|
19
|
+
7. **For iterative work** (clarification rounds, quality loops): update task `subject` with round/score progress
|
|
20
|
+
|
|
21
|
+
## Task Update Pattern
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
TaskUpdate(taskId, status="in_progress") // before starting the step
|
|
25
|
+
// ... do the work ...
|
|
26
|
+
TaskUpdate(taskId, status="completed") // after step finishes
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For iterative tasks:
|
|
30
|
+
```
|
|
31
|
+
TaskUpdate(taskId, subject="Round 3 — score 82/100")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Persisting Progress to `tasks.json`
|
|
35
|
+
|
|
36
|
+
Native session tasks (`TaskCreate`/`TaskUpdate`) track your live work. To persist
|
|
37
|
+
task progress for the feature, write `.morph/features/{feature}/2-plan/tasks.json`,
|
|
38
|
+
touching ONLY the mutable fields:
|
|
39
|
+
|
|
40
|
+
- `status` — `pending` → `in_progress` → `completed`
|
|
41
|
+
- `outputs` — the files the task produced
|
|
42
|
+
- `notes` — free-text record (deviations, context, resolved blockers)
|
|
43
|
+
|
|
44
|
+
The plan contract — `title`, `description`, `dependencies`, `effort`, `doneCriteria`,
|
|
45
|
+
`group` — is frozen after Gate 2. Any attempt to change it is blocked by the
|
|
46
|
+
`protect-spec-files` hook.
|
|
47
|
+
|
|
48
|
+
Use the `Write` tool (whole-file rewrite) OR the `Edit` tool — `Edit` is allowed
|
|
49
|
+
as long as it touches only `status`, `outputs`, and `notes`. State is then synced
|
|
50
|
+
automatically by the `state-sync` PostToolUse hook; you do not call a CLI to mark
|
|
51
|
+
output.
|
|
52
|
+
|
|
53
|
+
## Deviation Tracking
|
|
54
|
+
|
|
55
|
+
When completing a task differently than originally planned, record the reason in the
|
|
56
|
+
task's `notes` field. Use the `notes` array to capture deviations, added context, or
|
|
57
|
+
resolved blockers — for example:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"id": "T3",
|
|
62
|
+
"status": "completed",
|
|
63
|
+
"outputs": ["Features/User/Update/Handler.cs"],
|
|
64
|
+
"notes": [
|
|
65
|
+
"deviation: split validation into a separate validator for testability",
|
|
66
|
+
"blocker-resolved: EF migration conflict fixed by regenerating the snapshot"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
```
|