@polymorphism-tech/morph-spec 7.2.0 → 7.4.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 +1 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +4 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +29 -12
- package/framework/agents.json +132 -132
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-proposal.md +1 -1
- package/framework/evals/rubrics/README.md +9 -8
- package/framework/hooks/README.md +3 -1
- package/framework/hooks/claude-code/core/gate-guard.js +160 -0
- package/framework/hooks/claude-code/core/state-sync.js +15 -3
- package/framework/hooks/claude-code/core/validate-completion.js +241 -136
- package/framework/hooks/shared/state-reader.js +236 -221
- package/framework/phases.json +1 -1
- package/framework/skills/README.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +63 -21
- package/framework/skills/core/morph-brainstorming/SKILL.md +5 -1
- package/framework/skills/core/morph-eval/SKILL.md +4 -5
- package/framework/skills/core/morph-implement/SKILL.md +47 -19
- package/framework/skills/core/morph-plan/SKILL.md +8 -2
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +13 -6
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/setup.md +258 -244
- package/framework/templates/README.md +708 -709
- package/framework/templates/REGISTRY.json +1 -20
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/proposal.md +171 -171
- package/framework/templates/feature/recap.md +145 -145
- package/package.json +1 -1
- package/src/commands/project/doctor.js +941 -946
- package/src/commands/project/init.js +517 -515
- package/src/commands/project/status.js +373 -376
- package/src/commands/state/approve.js +251 -213
- package/src/commands/validation/validate-feature.js +356 -287
- package/src/core/state/state-manager.js +910 -934
- package/src/dashboard/app/App.jsx +29 -31
- package/src/dashboard/app/components/Sidebar.jsx +44 -45
- package/src/dashboard/scanner/index.js +20 -21
- package/src/lib/context/context-builder.js +3 -3
- package/src/lib/generators/context-generator.js +2 -3
- package/src/lib/generators/mandate-generator.js +48 -92
- package/src/lib/generators/metadata-extractor.js +385 -386
- package/src/lib/generators/recap-generator.js +285 -285
- package/src/lib/risk-detector.js +2 -2
- package/src/lib/validators/content/content-validator.js +89 -286
- package/src/lib/validators/content/index.js +0 -1
- package/src/utils/hooks-installer.js +527 -516
- package/framework/templates/feature/tasks.md +0 -199
- package/framework/workflows/README.md +0 -1041
- package/framework/workflows/configs/fast-track.json +0 -46
- package/framework/workflows/configs/full.json +0 -64
- package/framework/workflows/configs/quick.json +0 -32
- package/framework/workflows/configs/standard.json +0 -57
- package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +0 -392
- package/framework/workflows/docs/design-impl.md +0 -37
- package/framework/workflows/docs/enforcement-pipeline.md +0 -668
- package/framework/workflows/docs/fast-track.md +0 -29
- package/framework/workflows/docs/full-morph.md +0 -69
- package/framework/workflows/docs/standard.md +0 -49
- package/framework/workflows/docs/ui-refresh.md +0 -39
- package/src/commands/state/index.js +0 -7
- package/src/core/workflows/workflow-detector.js +0 -586
- package/src/core/workflows/workflow-router.js +0 -204
- package/src/dashboard/app/pages/Workflows.jsx +0 -346
- package/src/dashboard/scanner/workflows-scanner.js +0 -18
- package/src/lib/phase-chain/phase-validator.js +0 -359
- package/src/lib/validators/content/cross-phase-validator.js +0 -281
- package/src/lib/validators/spec-validator.js +0 -312
|
@@ -1,221 +1,236 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared read-only state accessor for Claude Code hooks.
|
|
3
|
-
*
|
|
4
|
-
* Wraps state.json reads with fail-open semantics:
|
|
5
|
-
* - Returns null instead of throwing on missing/corrupt state
|
|
6
|
-
* - Never writes to state.json (read-only)
|
|
7
|
-
* - Uses synchronous reads for <100ms performance
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readFileSync, existsSync } from 'fs';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
|
|
13
|
-
const STATE_FILE = '.morph/state.json';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Load state.json from the project root.
|
|
17
|
-
* Fail-open: returns null if file missing or corrupt.
|
|
18
|
-
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
19
|
-
* @returns {Object|null} State object or null
|
|
20
|
-
*/
|
|
21
|
-
export function loadState(projectPath) {
|
|
22
|
-
try {
|
|
23
|
-
const statePath = join(projectPath || process.cwd(), STATE_FILE);
|
|
24
|
-
if (!existsSync(statePath)) return null;
|
|
25
|
-
return JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
26
|
-
} catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Check if state.json exists.
|
|
33
|
-
* @param {string} [projectPath]
|
|
34
|
-
* @returns {boolean}
|
|
35
|
-
*/
|
|
36
|
-
export function stateExists(projectPath) {
|
|
37
|
-
return existsSync(join(projectPath || process.cwd(), STATE_FILE));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Get the active feature (most recently updated in_progress feature).
|
|
42
|
-
* @param {string} [projectPath]
|
|
43
|
-
* @returns {{ name: string, feature: Object }|null}
|
|
44
|
-
*/
|
|
45
|
-
export function getActiveFeature(projectPath) {
|
|
46
|
-
const state = loadState(projectPath);
|
|
47
|
-
if (!state?.features) return null;
|
|
48
|
-
|
|
49
|
-
let active = null;
|
|
50
|
-
let latestUpdate = '';
|
|
51
|
-
|
|
52
|
-
for (const [name, feature] of Object.entries(state.features)) {
|
|
53
|
-
if (feature.status === 'in_progress' || feature.status === 'draft') {
|
|
54
|
-
const updated = feature.updatedAt || feature.createdAt || '';
|
|
55
|
-
if (updated >= latestUpdate) {
|
|
56
|
-
latestUpdate = updated;
|
|
57
|
-
active = { name, feature };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return active;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get the most recently updated feature (any status).
|
|
67
|
-
* Useful as a fallback when getActiveFeature returns null (all features done).
|
|
68
|
-
* @param {string} [projectPath]
|
|
69
|
-
* @returns {{ name: string, feature: Object }|null}
|
|
70
|
-
*/
|
|
71
|
-
export function getMostRecentFeature(projectPath) {
|
|
72
|
-
const state = loadState(projectPath);
|
|
73
|
-
if (!state?.features) return null;
|
|
74
|
-
|
|
75
|
-
let result = null;
|
|
76
|
-
let latestUpdate = '';
|
|
77
|
-
|
|
78
|
-
for (const [name, feature] of Object.entries(state.features)) {
|
|
79
|
-
const updated = feature.updatedAt || feature.createdAt || '';
|
|
80
|
-
if (updated >= latestUpdate) {
|
|
81
|
-
latestUpdate = updated;
|
|
82
|
-
result = { name, feature };
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return result;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get a specific feature by name.
|
|
91
|
-
* @param {string} featureName
|
|
92
|
-
* @param {string} [projectPath]
|
|
93
|
-
* @returns {Object|null}
|
|
94
|
-
*/
|
|
95
|
-
export function getFeature(featureName, projectPath) {
|
|
96
|
-
const state = loadState(projectPath);
|
|
97
|
-
return state?.features?.[featureName] || null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Derive phase from filesystem (phase deleted from state).
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* @
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
*
|
|
152
|
-
* @param {string}
|
|
153
|
-
* @
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
*
|
|
168
|
-
* @
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared read-only state accessor for Claude Code hooks.
|
|
3
|
+
*
|
|
4
|
+
* Wraps state.json reads with fail-open semantics:
|
|
5
|
+
* - Returns null instead of throwing on missing/corrupt state
|
|
6
|
+
* - Never writes to state.json (read-only)
|
|
7
|
+
* - Uses synchronous reads for <100ms performance
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
|
|
13
|
+
const STATE_FILE = '.morph/state.json';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load state.json from the project root.
|
|
17
|
+
* Fail-open: returns null if file missing or corrupt.
|
|
18
|
+
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
19
|
+
* @returns {Object|null} State object or null
|
|
20
|
+
*/
|
|
21
|
+
export function loadState(projectPath) {
|
|
22
|
+
try {
|
|
23
|
+
const statePath = join(projectPath || process.cwd(), STATE_FILE);
|
|
24
|
+
if (!existsSync(statePath)) return null;
|
|
25
|
+
return JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if state.json exists.
|
|
33
|
+
* @param {string} [projectPath]
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
export function stateExists(projectPath) {
|
|
37
|
+
return existsSync(join(projectPath || process.cwd(), STATE_FILE));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the active feature (most recently updated in_progress feature).
|
|
42
|
+
* @param {string} [projectPath]
|
|
43
|
+
* @returns {{ name: string, feature: Object }|null}
|
|
44
|
+
*/
|
|
45
|
+
export function getActiveFeature(projectPath) {
|
|
46
|
+
const state = loadState(projectPath);
|
|
47
|
+
if (!state?.features) return null;
|
|
48
|
+
|
|
49
|
+
let active = null;
|
|
50
|
+
let latestUpdate = '';
|
|
51
|
+
|
|
52
|
+
for (const [name, feature] of Object.entries(state.features)) {
|
|
53
|
+
if (feature.status === 'in_progress' || feature.status === 'draft') {
|
|
54
|
+
const updated = feature.updatedAt || feature.createdAt || '';
|
|
55
|
+
if (updated >= latestUpdate) {
|
|
56
|
+
latestUpdate = updated;
|
|
57
|
+
active = { name, feature };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return active;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the most recently updated feature (any status).
|
|
67
|
+
* Useful as a fallback when getActiveFeature returns null (all features done).
|
|
68
|
+
* @param {string} [projectPath]
|
|
69
|
+
* @returns {{ name: string, feature: Object }|null}
|
|
70
|
+
*/
|
|
71
|
+
export function getMostRecentFeature(projectPath) {
|
|
72
|
+
const state = loadState(projectPath);
|
|
73
|
+
if (!state?.features) return null;
|
|
74
|
+
|
|
75
|
+
let result = null;
|
|
76
|
+
let latestUpdate = '';
|
|
77
|
+
|
|
78
|
+
for (const [name, feature] of Object.entries(state.features)) {
|
|
79
|
+
const updated = feature.updatedAt || feature.createdAt || '';
|
|
80
|
+
if (updated >= latestUpdate) {
|
|
81
|
+
latestUpdate = updated;
|
|
82
|
+
result = { name, feature };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get a specific feature by name.
|
|
91
|
+
* @param {string} featureName
|
|
92
|
+
* @param {string} [projectPath]
|
|
93
|
+
* @returns {Object|null}
|
|
94
|
+
*/
|
|
95
|
+
export function getFeature(featureName, projectPath) {
|
|
96
|
+
const state = loadState(projectPath);
|
|
97
|
+
return state?.features?.[featureName] || null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Derive phase from filesystem (phase deleted from state).
|
|
102
|
+
*
|
|
103
|
+
* Walks the phase map in descending order and returns the first phase whose
|
|
104
|
+
* CANONICAL ARTIFACT exists. An empty folder is not enough — this avoids the
|
|
105
|
+
* chicken-and-egg problem where `mkdir 2-plan/` (or any pre-created sibling)
|
|
106
|
+
* advances the phase before plan content is written, then
|
|
107
|
+
* `enforce-phase-writes` blocks writes back to `0-proposal/`.
|
|
108
|
+
*
|
|
109
|
+
* Canonical artifacts (one per phase) match MORPH.md §3:
|
|
110
|
+
* - 4-review → review-report.md
|
|
111
|
+
* - 3-implement → recap.md
|
|
112
|
+
* - 2-plan → spec.md (the first plan-phase output produced)
|
|
113
|
+
* - 1-design → design-system.md
|
|
114
|
+
* - 0-proposal → proposal.md
|
|
115
|
+
*
|
|
116
|
+
* If no canonical file is found, returns `'proposal'` (the starting phase).
|
|
117
|
+
*
|
|
118
|
+
* @param {string} featureName
|
|
119
|
+
* @param {string} [projectPath]
|
|
120
|
+
* @returns {string} Phase name
|
|
121
|
+
*/
|
|
122
|
+
export function derivePhaseForFeature(featureName, projectPath) {
|
|
123
|
+
const basePath = join(projectPath || process.cwd(), '.morph/features', featureName);
|
|
124
|
+
const phaseMap = [
|
|
125
|
+
['4-review', 'review', 'review-report.md'],
|
|
126
|
+
['3-implement', 'implement', 'recap.md'],
|
|
127
|
+
['2-plan', 'plan', 'spec.md'],
|
|
128
|
+
['1-design', 'uiux', 'design-system.md'],
|
|
129
|
+
['0-proposal', 'proposal', 'proposal.md'],
|
|
130
|
+
];
|
|
131
|
+
for (const [folder, phase, canonical] of phaseMap) {
|
|
132
|
+
if (existsSync(join(basePath, folder, canonical))) return phase;
|
|
133
|
+
}
|
|
134
|
+
return 'proposal';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the current phase of a feature.
|
|
139
|
+
* Falls back to filesystem derivation when phase is not in state.
|
|
140
|
+
* @param {string} featureName
|
|
141
|
+
* @param {string} [projectPath]
|
|
142
|
+
* @returns {string|null}
|
|
143
|
+
*/
|
|
144
|
+
export function getFeaturePhase(featureName, projectPath) {
|
|
145
|
+
const feature = getFeature(featureName, projectPath);
|
|
146
|
+
if (!feature) return null;
|
|
147
|
+
return feature.phase || derivePhaseForFeature(featureName, projectPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get approval status for a gate.
|
|
152
|
+
* @param {string} featureName
|
|
153
|
+
* @param {string} gate - 'proposal'|'uiux'|'plan'|'review'
|
|
154
|
+
* @param {string} [projectPath]
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
export function isGateApproved(featureName, gate, projectPath) {
|
|
158
|
+
const feature = getFeature(featureName, projectPath);
|
|
159
|
+
// Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
|
|
160
|
+
const gates = feature?.approvals || feature?.approvalGates;
|
|
161
|
+
return gates?.[gate]?.approved === true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get all pending approval gates for a feature.
|
|
166
|
+
* @param {string} featureName
|
|
167
|
+
* @param {string} [projectPath]
|
|
168
|
+
* @returns {string[]} List of gate names that are not yet approved
|
|
169
|
+
*/
|
|
170
|
+
export function getPendingGates(featureName, projectPath) {
|
|
171
|
+
const feature = getFeature(featureName, projectPath);
|
|
172
|
+
// Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
|
|
173
|
+
const gates = feature?.approvals || feature?.approvalGates;
|
|
174
|
+
if (!gates) return [];
|
|
175
|
+
|
|
176
|
+
return Object.entries(gates)
|
|
177
|
+
.filter(([, gate]) => !gate.approved)
|
|
178
|
+
.map(([name]) => name);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get feature outputs status.
|
|
183
|
+
* @param {string} featureName
|
|
184
|
+
* @param {string} [projectPath]
|
|
185
|
+
* @returns {Object|null} outputs map or null
|
|
186
|
+
*/
|
|
187
|
+
export function getOutputs(featureName, projectPath) {
|
|
188
|
+
const feature = getFeature(featureName, projectPath);
|
|
189
|
+
return feature?.outputs || null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get uncompleted required outputs for current phase.
|
|
194
|
+
* @param {string} featureName
|
|
195
|
+
* @param {string} [projectPath]
|
|
196
|
+
* @returns {Array<{type: string, path: string}>}
|
|
197
|
+
*/
|
|
198
|
+
/**
|
|
199
|
+
* Get task statistics directly from taskList (preferred over feature.tasks counters).
|
|
200
|
+
* Excludes expanded tasks from counts, matching calculateProgress() behavior.
|
|
201
|
+
*
|
|
202
|
+
* @param {Object} feature - Feature state object
|
|
203
|
+
* @returns {{ total: number, completed: number, inProgress: number, pending: number }}
|
|
204
|
+
*/
|
|
205
|
+
export function getTaskStats(feature) {
|
|
206
|
+
const list = feature?.taskList || [];
|
|
207
|
+
if (list.length > 0) {
|
|
208
|
+
const active = list.filter(t => t.status !== 'expanded');
|
|
209
|
+
return {
|
|
210
|
+
total: active.length,
|
|
211
|
+
completed: active.filter(t => t.status === 'completed').length,
|
|
212
|
+
inProgress: active.filter(t => t.status === 'in_progress').length,
|
|
213
|
+
pending: active.filter(t => t.status === 'pending').length,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// Fallback: read from legacy counter object
|
|
217
|
+
const tasks = feature?.tasks;
|
|
218
|
+
if (tasks && typeof tasks === 'object' && !Array.isArray(tasks)) {
|
|
219
|
+
return {
|
|
220
|
+
total: tasks.total || 0,
|
|
221
|
+
completed: tasks.completed || 0,
|
|
222
|
+
inProgress: tasks.inProgress || 0,
|
|
223
|
+
pending: tasks.pending || 0,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { total: 0, completed: 0, inProgress: 0, pending: 0 };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function getMissingOutputs(featureName, projectPath) {
|
|
230
|
+
const feature = getFeature(featureName, projectPath);
|
|
231
|
+
if (!feature?.outputs) return [];
|
|
232
|
+
|
|
233
|
+
return Object.entries(feature.outputs)
|
|
234
|
+
.filter(([, output]) => !output.created)
|
|
235
|
+
.map(([type, output]) => ({ type, path: output.path }));
|
|
236
|
+
}
|
package/framework/phases.json
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"goals": [
|
|
44
44
|
"Write spec.md (technical spec)",
|
|
45
45
|
"Write mandate.md (implementation contract with stack defaults + forbidden patterns)",
|
|
46
|
-
"Write tasks.
|
|
46
|
+
"Write tasks.json (ordered task breakdown)"
|
|
47
47
|
],
|
|
48
48
|
"requiredOutputs": ["spec", "mandate", "tasks"],
|
|
49
49
|
"optionalOutputs": ["decisions"],
|
|
@@ -14,9 +14,9 @@ These are the only skills the harness needs. MORPH.md references them by path.
|
|
|
14
14
|
|----------------------|----------------------------------------------------------------------|----------------|
|
|
15
15
|
| `morph-brainstorming` | Business understanding + brownfield scan → proposal.md (Gate 1) | yes |
|
|
16
16
|
| `morph-stack-scan` | Detect stack drift in existing projects, generate migration-plan.md | yes |
|
|
17
|
-
| `morph-plan` | Produce spec.md + mandate.md + tasks.
|
|
17
|
+
| `morph-plan` | Produce spec.md + mandate.md + tasks.json (Gate 2) | yes |
|
|
18
18
|
| `morph-implement` | Execute each task with standard + persona, auto-advance between tasks| no |
|
|
19
|
-
| `morph-eval` | Score work 0-10 across 4 dimensions, silence on score >=
|
|
19
|
+
| `morph-eval` | Score work 0-10 across 4 dimensions, silence on score >= 9 | no |
|
|
20
20
|
| `morph-review` | Final review report with scores + recommendation (Gate 3) | yes |
|
|
21
21
|
| `morph-standards` | Locate and load a specific standard on demand | no |
|
|
22
22
|
|
|
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
|
|
|
4
4
|
version: core
|
|
5
5
|
when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
|
|
6
6
|
user-invokable: true
|
|
7
|
-
cliVersion: "7.
|
|
7
|
+
cliVersion: "7.4.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# morph-apply
|
|
@@ -41,18 +41,26 @@ Executar todas as tasks pendentes de uma feature até `done`, aplicando `morph-e
|
|
|
41
41
|
|
|
42
42
|
### 2. Análise de DAG — decisão sequencial vs paralelo
|
|
43
43
|
|
|
44
|
-
Construir o grafo de dependências e detectar
|
|
44
|
+
Construir o grafo de dependências e detectar fluxos de trabalho independentes. Há duas formas de paralelismo a detectar — e a segunda é a que mais aparece na prática:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const groups = groupTasks(data); // { backend: [ids], frontend: [ids], tests: [...], infra: [...], docs: [...] }
|
|
48
|
+
const independentRoots = data.tasks.filter(t => t.dependencies.length === 0);
|
|
49
|
+
```
|
|
45
50
|
|
|
46
51
|
**Regras de decisão:**
|
|
47
52
|
|
|
48
53
|
1. Se `data.tasks.length <= 3`: **sequencial** (overhead de Agent dispatch não compensa).
|
|
49
|
-
2.
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
2. **Fluxos de domínio independentes (o caso comum — backend ‖ frontend).** Use `groupTasks(data)` para separar as tasks por `group`. Dois grupos formam fluxos paralelos quando **nenhuma task de um grupo depende de uma task de outro grupo** (as cadeias só convergem, se convergirem, numa task final de integração). Se existem **≥ 2 fluxos de domínio independentes** — ex: a cadeia `backend` `T1→…→T11` e a cadeia `frontend` `T12→T13` não se cruzam até `T14` —, dispache **um sub-agent por fluxo, em paralelo**, cada um com a persona e o standard do seu domínio (ver passo 3b). Esse é exatamente o caso que a regra antiga de "≥3 raízes independentes" deixava passar (duas cadeias contam como 2 raízes, não 3).
|
|
55
|
+
3. **Raízes independentes (fan-out na largura).** Identificar tasks **100% independentes** (`dependencies: []`). Se `independentRoots.length >= 3`: dispachar essas raízes em **paralelo**, depois cair em sequencial para o resto.
|
|
56
|
+
4. Caso contrário: **sequencial puro** com auto-advance.
|
|
57
|
+
|
|
58
|
+
> O cap de **4 agentes simultâneos** (anti-padrão abaixo) vale para ambas as formas. Se um fluxo de domínio é uma cadeia longa, despache o **fluxo inteiro** a um sub-agent (que executa a cadeia em ordem internamente) — um agente por fluxo, não um agente por task.
|
|
52
59
|
|
|
53
|
-
Logar a decisão:
|
|
60
|
+
Logar a decisão (mostre qual forma disparou):
|
|
54
61
|
```
|
|
55
|
-
DAG analysis:
|
|
62
|
+
DAG analysis: 14 tasks, groups {backend:9, frontend:2, tests:2, infra:1}; backend ‖ frontend independentes até T14 → 2 fluxos paralelos (1 agente backend, 1 agente frontend) + T14 sequencial no fim
|
|
63
|
+
DAG analysis: 12 tasks, 4 raízes independentes → parallel batch (T1, T3, T7, T9) + sequential remainder
|
|
56
64
|
```
|
|
57
65
|
|
|
58
66
|
### 3a. Modo Sequencial (auto-advance)
|
|
@@ -68,39 +76,69 @@ while (true):
|
|
|
68
76
|
updateTaskStatus(data, task.id, 'in_progress')
|
|
69
77
|
writeTasksJson(tasksPath, data)
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
// Loop de correção autônomo — a barra de qualidade é >= 9 (ver MORPH.md §0).
|
|
80
|
+
// Entre tasks NÃO há pausa humana: score < 9 dispara re-implementação, não AskUserQuestion.
|
|
81
|
+
attempt = 0
|
|
82
|
+
best = { score: 0, output: null }
|
|
83
|
+
while attempt < 3:
|
|
84
|
+
attempt += 1
|
|
85
|
+
invoke Skill(morph-implement) with task
|
|
86
|
+
(no attempt > 1: passar o feedback priorizado do morph-eval anterior como input)
|
|
87
|
+
result = invoke Skill(morph-eval) with implementation output
|
|
88
|
+
if result.score > best.score: best = result
|
|
89
|
+
// Persista o score no state.json sob a task — torna o evalScore um signal de disco
|
|
90
|
+
// no MOMENTO da implementação (não no fim). O hook gate-guard.js e o Gate 3 leem dali.
|
|
91
|
+
// Edit em .morph/state.json é permitido. Grave:
|
|
92
|
+
// features[feature].taskScores[task.id] = { lastScore: result.score, bestScore: best.score, attempts: attempt, updatedAt: <ISO> }
|
|
93
|
+
edit .morph/state.json com o taskScores acima
|
|
94
|
+
|
|
95
|
+
if result.score >= 9:
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
if best.score >= 9:
|
|
76
99
|
updateTaskStatus(data, task.id, 'done')
|
|
77
100
|
else:
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
// 3 tentativas sem atingir 9 — AQUI sim escala ao humano (escape definido).
|
|
102
|
+
// Esta é a única pausa do loop de implement, e só após esgotar o loop de correção.
|
|
103
|
+
registrar em recap.md: as 3 tentativas e o melhor score (best.score) com o feedback
|
|
104
|
+
AskUserQuestion: "Task {id} não atingiu score 9 após 3 tentativas (melhor: {best.score}) — aceitar mesmo assim, continuar tentando, ou pular?"
|
|
105
|
+
options: [aceitar (marca done com ressalva no recap), continuar tentando (+3 tentativas), pular task (marca blocked)]
|
|
81
106
|
|
|
82
107
|
writeTasksJson(tasksPath, data)
|
|
83
108
|
```
|
|
84
109
|
|
|
110
|
+
> **Por que loop e não pausa:** MORPH.md §0 manda auto-avançar entre tasks. Score baixo numa task individual é um problema de qualidade que o modelo deve **corrigir sozinho**, não delegar ao humano no meio da implementação. A pausa só existe quando o loop de correção se esgota (3 tentativas) — o "escape" do modelo autônomo. O trust mode não muda esse loop: ele governa os **gates** (proposal/plan/review), não o ciclo de correção intra-task.
|
|
111
|
+
|
|
85
112
|
### 3b. Modo Paralelo (para o subset independente)
|
|
86
113
|
|
|
87
|
-
Para cada task
|
|
114
|
+
**Antes de despachar, escope cada agente ao seu domínio.** Um agente genérico sem persona nem standard não "conhece" as convenções daquele domínio — produz código fora do padrão. Para cada fluxo/task a despachar, resolva:
|
|
115
|
+
|
|
116
|
+
- **Persona:** em `agents.json`, encontre o agente cujo `domains` cobre o `group`/domínio da task (ex: `backend` → `dotnet-senior`, `frontend` → `nextjs-expert`/`ui-designer`, `database` → `ef-modeler`). Pegue o texto do campo `persona`.
|
|
117
|
+
- **Standard(s):** do array `standards` desse mesmo agente em `agents.json` (e/ou do `mandate.md`), selecione o(s) path(s) `.md` relevante(s) à task.
|
|
118
|
+
|
|
88
119
|
```
|
|
89
120
|
dispatch Agent (subagent_type: "general-purpose") with prompt:
|
|
121
|
+
persona: <texto do campo `persona` do agente de domínio em agents.json>
|
|
90
122
|
contexto: spec.md, mandate.md, task description completa, doneCriteria
|
|
123
|
+
standard: <path(s) .md do(s) standard(s) do domínio — leia-o(s) antes de implementar>
|
|
91
124
|
instrução: |
|
|
92
|
-
|
|
125
|
+
Adote a persona acima. Leia o(s) standard(s) indicado(s) antes de escrever código.
|
|
126
|
+
Implemente esta task (ou cadeia de tasks do fluxo) isoladamente.
|
|
127
|
+
Não modifique spec.md, mandate.md.
|
|
93
128
|
Retorne:
|
|
94
129
|
- sumário das mudanças
|
|
95
130
|
- arquivos modificados (paths)
|
|
131
|
+
- standard(s) consultado(s)
|
|
96
132
|
- auto-score 0-10 (rubric: completeness, correctness, mandate compliance)
|
|
97
133
|
```
|
|
98
134
|
|
|
135
|
+
> Este template é deliberadamente paritário com o template sequencial em `morph-implement` §4: ambos passam **persona + standard escopados**. A única diferença é que aqui múltiplos agentes rodam ao mesmo tempo, um por fluxo independente.
|
|
136
|
+
|
|
99
137
|
Aguardar todos os agentes terminarem (Promise.all equivalente). Depois, sequencialmente:
|
|
100
138
|
- Para cada output retornado:
|
|
101
139
|
- Aplicar `Skill(morph-eval)`.
|
|
102
|
-
- Se score >=
|
|
103
|
-
- Se score <
|
|
140
|
+
- Se score >= 9: marcar `done`.
|
|
141
|
+
- Se score < 9: marcar `pending` novamente com nota — volta ao modo sequencial, onde o loop de correção (até 3 tentativas) busca o score 9.
|
|
104
142
|
|
|
105
143
|
Depois do batch paralelo, executar o restante em modo sequencial (passo 3a).
|
|
106
144
|
|
|
@@ -115,12 +153,16 @@ Quando `getPendingTasks(data).length === 0`:
|
|
|
115
153
|
|
|
116
154
|
Sinais de risco para Gate 3:
|
|
117
155
|
|
|
118
|
-
- Score médio <
|
|
156
|
+
- Score médio < 9 no review-report.
|
|
119
157
|
- Presença de erros não resolvidos.
|
|
120
158
|
- Mudanças incidentais em standards/.morph/framework/.
|
|
121
159
|
|
|
122
160
|
Mesma lógica do Gate 1/Gate 2 via `shouldPauseGate(config, signals)`:
|
|
123
161
|
|
|
162
|
+
**Persista a decisão em `state.json` ANTES de apresentar ou auto-aprovar** — grave
|
|
163
|
+
`features[feature].gateDecisions.review = { pause, reason, signals, trust, decidedAt: <ISO> }`
|
|
164
|
+
(o `signals.evalScore` aqui é o score composto do review-report). O hook `gate-guard.js` confere depois se a ação bateu. Sem o registro o guard não verifica.
|
|
165
|
+
|
|
124
166
|
- Pause: `AskUserQuestion` com options `["aprovado, fechar feature", "preciso ajustar", "rollback"]`.
|
|
125
167
|
- Auto: exibir summary do review-report e prosseguir.
|
|
126
168
|
|
|
@@ -133,7 +175,7 @@ Mesma lógica do Gate 1/Gate 2 via `shouldPauseGate(config, signals)`:
|
|
|
133
175
|
## Anti-padrões
|
|
134
176
|
|
|
135
177
|
- Não dispachar paralelo se há deps cruzadas entre as tasks selecionadas (corrompe ordem).
|
|
136
|
-
- Não pular `morph-eval` —
|
|
178
|
+
- Não pular `morph-eval` — uma task só avança com score >= 9; abaixo disso, o loop de correção roda (não pausa).
|
|
137
179
|
- Não reescrever spec.md/mandate.md durante implement (são read-only após Gate 2 — `protect-spec-files.js` hook bloqueia).
|
|
138
180
|
- Não dispachar mais que 4 agentes em paralelo (sobrecarga de contexto + custo).
|
|
139
181
|
|