@polymorphism-tech/morph-spec 7.3.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.
Files changed (33) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +4 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +1 -1
  8. package/framework/MORPH.md +29 -12
  9. package/framework/agents.json +132 -132
  10. package/framework/evals/rubrics/README.md +9 -8
  11. package/framework/hooks/README.md +2 -0
  12. package/framework/hooks/claude-code/core/gate-guard.js +160 -0
  13. package/framework/hooks/claude-code/core/state-sync.js +15 -3
  14. package/framework/hooks/claude-code/core/validate-completion.js +105 -0
  15. package/framework/hooks/shared/state-reader.js +236 -221
  16. package/framework/skills/README.md +1 -1
  17. package/framework/skills/core/morph-apply/SKILL.md +63 -21
  18. package/framework/skills/core/morph-brainstorming/SKILL.md +5 -1
  19. package/framework/skills/core/morph-eval/SKILL.md +4 -5
  20. package/framework/skills/core/morph-implement/SKILL.md +47 -19
  21. package/framework/skills/core/morph-plan/SKILL.md +8 -2
  22. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  23. package/framework/skills/core/morph-review/SKILL.md +13 -6
  24. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  25. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  26. package/framework/standards/ai-agents/setup.md +258 -244
  27. package/package.json +1 -1
  28. package/src/commands/project/init.js +517 -515
  29. package/src/commands/state/approve.js +251 -213
  30. package/src/commands/validation/validate-feature.js +64 -23
  31. package/src/core/state/state-manager.js +19 -9
  32. package/src/lib/risk-detector.js +2 -2
  33. package/src/utils/hooks-installer.js +527 -516
@@ -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
- * Checks for phase folders in descending order, returns highest present.
103
- * @param {string} featureName
104
- * @param {string} [projectPath]
105
- * @returns {string} Phase name
106
- */
107
- export function derivePhaseForFeature(featureName, projectPath) {
108
- const basePath = join(projectPath || process.cwd(), '.morph/features', featureName);
109
- const phaseMap = [
110
- ['4-review', 'review'],
111
- ['3-implement', 'implement'],
112
- ['2-plan', 'plan'],
113
- ['1-design', 'uiux'],
114
- ['0-proposal', 'proposal'],
115
- ];
116
- for (const [folder, phase] of phaseMap) {
117
- if (existsSync(join(basePath, folder))) return phase;
118
- }
119
- return 'proposal';
120
- }
121
-
122
- /**
123
- * Get the current phase of a feature.
124
- * Falls back to filesystem derivation when phase is not in state.
125
- * @param {string} featureName
126
- * @param {string} [projectPath]
127
- * @returns {string|null}
128
- */
129
- export function getFeaturePhase(featureName, projectPath) {
130
- const feature = getFeature(featureName, projectPath);
131
- if (!feature) return null;
132
- return feature.phase || derivePhaseForFeature(featureName, projectPath);
133
- }
134
-
135
- /**
136
- * Get approval status for a gate.
137
- * @param {string} featureName
138
- * @param {string} gate - 'proposal'|'uiux'|'plan'|'review'
139
- * @param {string} [projectPath]
140
- * @returns {boolean}
141
- */
142
- export function isGateApproved(featureName, gate, projectPath) {
143
- const feature = getFeature(featureName, projectPath);
144
- // Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
145
- const gates = feature?.approvals || feature?.approvalGates;
146
- return gates?.[gate]?.approved === true;
147
- }
148
-
149
- /**
150
- * Get all pending approval gates for a feature.
151
- * @param {string} featureName
152
- * @param {string} [projectPath]
153
- * @returns {string[]} List of gate names that are not yet approved
154
- */
155
- export function getPendingGates(featureName, projectPath) {
156
- const feature = getFeature(featureName, projectPath);
157
- // Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
158
- const gates = feature?.approvals || feature?.approvalGates;
159
- if (!gates) return [];
160
-
161
- return Object.entries(gates)
162
- .filter(([, gate]) => !gate.approved)
163
- .map(([name]) => name);
164
- }
165
-
166
- /**
167
- * Get feature outputs status.
168
- * @param {string} featureName
169
- * @param {string} [projectPath]
170
- * @returns {Object|null} outputs map or null
171
- */
172
- export function getOutputs(featureName, projectPath) {
173
- const feature = getFeature(featureName, projectPath);
174
- return feature?.outputs || null;
175
- }
176
-
177
- /**
178
- * Get uncompleted required outputs for current phase.
179
- * @param {string} featureName
180
- * @param {string} [projectPath]
181
- * @returns {Array<{type: string, path: string}>}
182
- */
183
- /**
184
- * Get task statistics directly from taskList (preferred over feature.tasks counters).
185
- * Excludes expanded tasks from counts, matching calculateProgress() behavior.
186
- *
187
- * @param {Object} feature - Feature state object
188
- * @returns {{ total: number, completed: number, inProgress: number, pending: number }}
189
- */
190
- export function getTaskStats(feature) {
191
- const list = feature?.taskList || [];
192
- if (list.length > 0) {
193
- const active = list.filter(t => t.status !== 'expanded');
194
- return {
195
- total: active.length,
196
- completed: active.filter(t => t.status === 'completed').length,
197
- inProgress: active.filter(t => t.status === 'in_progress').length,
198
- pending: active.filter(t => t.status === 'pending').length,
199
- };
200
- }
201
- // Fallback: read from legacy counter object
202
- const tasks = feature?.tasks;
203
- if (tasks && typeof tasks === 'object' && !Array.isArray(tasks)) {
204
- return {
205
- total: tasks.total || 0,
206
- completed: tasks.completed || 0,
207
- inProgress: tasks.inProgress || 0,
208
- pending: tasks.pending || 0,
209
- };
210
- }
211
- return { total: 0, completed: 0, inProgress: 0, pending: 0 };
212
- }
213
-
214
- export function getMissingOutputs(featureName, projectPath) {
215
- const feature = getFeature(featureName, projectPath);
216
- if (!feature?.outputs) return [];
217
-
218
- return Object.entries(feature.outputs)
219
- .filter(([, output]) => !output.created)
220
- .map(([type, output]) => ({ type, path: output.path }));
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
+ }
@@ -16,7 +16,7 @@ These are the only skills the harness needs. MORPH.md references them by path.
16
16
  | `morph-stack-scan` | Detect stack drift in existing projects, generate migration-plan.md | yes |
17
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 >= 8 | no |
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.3.0"
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 subsets independentes.
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. Identificar tasks **100% independentes** (`dependencies: []`):
50
- - Se `>= 3` tasks independentes existem: dispachar essas em **paralelo**, depois cair em sequencial para o resto.
51
- 3. Caso contrário: **sequencial puro** com auto-advance.
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: 12 tasks, 4 independent → parallel batch (T1, T3, T7, T9) + sequential remainder
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
- invoke Skill(morph-implement) with task
72
-
73
- result = invoke Skill(morph-eval) with implementation output
74
-
75
- if result.score >= 8:
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
- // forçar pausa — qualidade é guardrail, trust não ignora aqui
79
- AskUserQuestion: "Task {id} com score {score} — aceitar, reimplementar ou pular?"
80
- options: [aceitar mesmo assim, reimplementar, pular task]
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 independente:
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
- Implemente esta task isoladamente. Não modifique spec.md, mandate.md.
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 >= 8: marcar `done`.
103
- - Se score < 8: marcar `pending` novamente com nota — será re-executada no modo sequencial.
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 < 8 no review-report.
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` — silêncio em score >= 8 é o sinal correto.
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
 
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
4
4
  version: core
5
5
  when-to-invoke: ao receber um novo request de feature, antes de morph-plan
6
6
  user-invokable: true
7
- cliVersion: "7.3.0"
7
+ cliVersion: "7.4.0"
8
8
  ---
9
9
 
10
10
  # morph-brainstorming
@@ -87,6 +87,10 @@ O harness conhece o stack por padrão. Não pergunte sobre tecnologia a menos qu
87
87
  const riskList = getRiskSignals(signals);
88
88
  ```
89
89
 
90
+ **Persista a decisão em `state.json` ANTES de apresentar ou auto-aprovar** — grave
91
+ `features[feature].gateDecisions.proposal = { pause, reason, signals, trust, decidedAt: <ISO> }`
92
+ (Edit em `.morph/state.json` é permitido). O hook `gate-guard.js` confere depois se a ação (pausar vs auto-aprovar) bateu com esta decisão e avisa em divergência. Sem o registro o guard não verifica — não pule.
93
+
90
94
  **Se `pause === true`** (manual mode OU risco detectado em auto), use `AskUserQuestion`:
91
95
 
92
96
  ```