@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.
- 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 +1 -1
- package/framework/MORPH.md +29 -12
- package/framework/agents.json +132 -132
- package/framework/evals/rubrics/README.md +9 -8
- package/framework/hooks/README.md +2 -0
- 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 +105 -0
- package/framework/hooks/shared/state-reader.js +236 -221
- package/framework/skills/README.md +1 -1
- 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/package.json +1 -1
- package/src/commands/project/init.js +517 -515
- package/src/commands/state/approve.js +251 -213
- package/src/commands/validation/validate-feature.js +64 -23
- package/src/core/state/state-manager.js +19 -9
- package/src/lib/risk-detector.js +2 -2
- package/src/utils/hooks-installer.js +527 -516
|
@@ -1,213 +1,251 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory } from '../../core/state/state-manager.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Unapprove action function - Revoke a previously approved gate
|
|
7
|
-
*/
|
|
8
|
-
export async function unapproveCommand(featureName, gateName, options = {}) {
|
|
9
|
-
try {
|
|
10
|
-
if (!VALID_GATES.includes(gateName)) {
|
|
11
|
-
console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
|
|
12
|
-
console.log(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const feature = getFeature(featureName);
|
|
17
|
-
if (!feature) {
|
|
18
|
-
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const currentStatus = getApprovalGate(featureName, gateName);
|
|
23
|
-
if (!currentStatus?.approved) {
|
|
24
|
-
console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is not currently approved — nothing to revoke\n`));
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory } from '../../core/state/state-manager.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Unapprove action function - Revoke a previously approved gate
|
|
7
|
+
*/
|
|
8
|
+
export async function unapproveCommand(featureName, gateName, options = {}) {
|
|
9
|
+
try {
|
|
10
|
+
if (!VALID_GATES.includes(gateName)) {
|
|
11
|
+
console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
|
|
12
|
+
console.log(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const feature = getFeature(featureName);
|
|
17
|
+
if (!feature) {
|
|
18
|
+
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const currentStatus = getApprovalGate(featureName, gateName);
|
|
23
|
+
if (!currentStatus?.approved) {
|
|
24
|
+
console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is not currently approved — nothing to revoke\n`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Same honesty rule as approveCommand: don't pretend a human revoked
|
|
29
|
+
// the gate when there's no interactive shell behind the invocation.
|
|
30
|
+
let revokedBy;
|
|
31
|
+
if (options.revoker) {
|
|
32
|
+
revokedBy = options.revoker;
|
|
33
|
+
} else if (options.mode === 'auto') {
|
|
34
|
+
revokedBy = 'auto';
|
|
35
|
+
} else if (process.stdin.isTTY) {
|
|
36
|
+
revokedBy = process.env.USER || process.env.USERNAME || 'user';
|
|
37
|
+
} else {
|
|
38
|
+
revokedBy = 'auto';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await setApprovalGate(featureName, gateName, false, {
|
|
42
|
+
revokedBy,
|
|
43
|
+
revokedAt: new Date().toISOString(),
|
|
44
|
+
reason: options.reason
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
console.log(chalk.yellow(`\n↩️ Gate "${gateName}" approval revoked for feature: ${featureName}`));
|
|
48
|
+
console.log(chalk.gray(`Workflow is now blocked at the ${gateName} gate\n`));
|
|
49
|
+
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(chalk.red(`\n❌ Error revoking gate: ${error.message}\n`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Approval Commands - Manage approval gates for MORPH workflow
|
|
58
|
+
*
|
|
59
|
+
* Commands:
|
|
60
|
+
* - approve: Mark a gate as approved
|
|
61
|
+
* - unapprove: Revoke a gate approval
|
|
62
|
+
* - approval-status: Show approval status for all gates
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
const program = new Command();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Valid approval gate names
|
|
69
|
+
*/
|
|
70
|
+
const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Approve action function - Mark a gate as approved
|
|
74
|
+
*/
|
|
75
|
+
export async function approveCommand(featureName, gateName, options = {}) {
|
|
76
|
+
try {
|
|
77
|
+
// Validate gate name
|
|
78
|
+
if (!VALID_GATES.includes(gateName)) {
|
|
79
|
+
console.error(chalk.red(`\n❌ Invalid gate: ${gateName}`));
|
|
80
|
+
console.log(chalk.gray(`Valid gates: ${VALID_GATES.join(', ')}\n`));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Get feature state
|
|
85
|
+
const feature = getFeature(featureName);
|
|
86
|
+
if (!feature) {
|
|
87
|
+
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if already approved
|
|
92
|
+
const currentStatus = getApprovalGate(featureName, gateName);
|
|
93
|
+
if (currentStatus?.approved) {
|
|
94
|
+
console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is already approved`));
|
|
95
|
+
console.log(chalk.gray(`Approved at: ${currentStatus.timestamp}`));
|
|
96
|
+
console.log(chalk.gray(`Approved by: ${currentStatus.approvedBy || 'Unknown'}\n`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Set approval.
|
|
101
|
+
//
|
|
102
|
+
// approvedBy resolution (most explicit wins):
|
|
103
|
+
// 1. --approver flag (CLI caller signalled who approved)
|
|
104
|
+
// 2. --mode auto → "auto" (intentionally non-human signature)
|
|
105
|
+
// 3. interactive shell (TTY on stdin) → $USER / $USERNAME (a real
|
|
106
|
+
// human is at the terminal; their name is the honest answer)
|
|
107
|
+
// 4. non-interactive (no TTY) → "auto"
|
|
108
|
+
//
|
|
109
|
+
// The TTY check exists because morph-spec is increasingly invoked
|
|
110
|
+
// from headless contexts (claude -p subprocesses, CI, the
|
|
111
|
+
// morph-e2e-tester). Stamping the host's $USERNAME on a gate that
|
|
112
|
+
// no human actually approved is misleading — it lies to the audit
|
|
113
|
+
// trail. "auto" reads as what it is: a deterministic policy gate,
|
|
114
|
+
// not a human signing off.
|
|
115
|
+
let approvedBy;
|
|
116
|
+
if (options.approver) {
|
|
117
|
+
approvedBy = options.approver;
|
|
118
|
+
} else if (options.mode === 'auto') {
|
|
119
|
+
approvedBy = 'auto';
|
|
120
|
+
} else if (process.stdin.isTTY) {
|
|
121
|
+
approvedBy = process.env.USER || process.env.USERNAME || 'user';
|
|
122
|
+
} else {
|
|
123
|
+
approvedBy = 'auto';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await setApprovalGate(featureName, gateName, true, {
|
|
127
|
+
approvedBy,
|
|
128
|
+
approvedAt: new Date().toISOString(),
|
|
129
|
+
notes: options.notes
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log(chalk.green(`\n✅ Gate "${gateName}" approved for feature: ${featureName}`));
|
|
133
|
+
console.log(chalk.gray(`Workflow can now proceed past ${gateName} phase\n`));
|
|
134
|
+
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(chalk.red(`\n❌ Error approving gate: ${error.message}\n`));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Approve command - Mark a gate as approved
|
|
143
|
+
*/
|
|
144
|
+
program
|
|
145
|
+
.command('approve <feature> <gate>')
|
|
146
|
+
.description('Approve a phase gate to allow workflow progression')
|
|
147
|
+
.action(approveCommand);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Approval status action function - Show all gates and their status
|
|
151
|
+
*/
|
|
152
|
+
export async function approvalStatusCommand(featureName, options = {}) {
|
|
153
|
+
try {
|
|
154
|
+
// Get feature state
|
|
155
|
+
const feature = getFeature(featureName);
|
|
156
|
+
if (!feature) {
|
|
157
|
+
console.error(chalk.red(`\n❌ Feature not found: ${featureName}\n`));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// JSON output
|
|
162
|
+
if (options.json) {
|
|
163
|
+
const status = {
|
|
164
|
+
feature: featureName,
|
|
165
|
+
phase: feature.phase,
|
|
166
|
+
status: feature.status,
|
|
167
|
+
gates: {}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (const gateName of VALID_GATES) {
|
|
171
|
+
status.gates[gateName] = getApprovalGate(featureName, gateName) || { approved: false };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log(JSON.stringify(status, null, 2));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(chalk.bold(`\n📋 Approval Status for: ${featureName}`));
|
|
179
|
+
console.log('━'.repeat(60));
|
|
180
|
+
|
|
181
|
+
// Show current phase
|
|
182
|
+
console.log(chalk.cyan(`\nCurrent Phase: ${feature.phase || 'unknown'}`));
|
|
183
|
+
console.log(chalk.gray(`Feature Status: ${feature.status || 'unknown'}\n`));
|
|
184
|
+
|
|
185
|
+
// Show each gate
|
|
186
|
+
console.log(chalk.bold('Approval Gates:'));
|
|
187
|
+
|
|
188
|
+
for (const gateName of VALID_GATES) {
|
|
189
|
+
const gateStatus = getApprovalGate(featureName, gateName);
|
|
190
|
+
|
|
191
|
+
if (!gateStatus) {
|
|
192
|
+
console.log(` ${chalk.gray('○')} ${gateName}: ${chalk.gray('Not yet evaluated')}`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (gateStatus.approved) {
|
|
197
|
+
console.log(` ${chalk.green('✓')} ${gateName}: ${chalk.green('Approved')}`);
|
|
198
|
+
if (gateStatus.timestamp) {
|
|
199
|
+
console.log(chalk.gray(` Approved at: ${gateStatus.timestamp}`));
|
|
200
|
+
}
|
|
201
|
+
if (gateStatus.approvedBy) {
|
|
202
|
+
console.log(chalk.gray(` Approved by: ${gateStatus.approvedBy}`));
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
console.log(` ${chalk.red('✗')} ${gateName}: ${chalk.red('Rejected / Not Approved')}`);
|
|
206
|
+
if (gateStatus.reason) {
|
|
207
|
+
console.log(chalk.gray(` Reason: ${gateStatus.reason}`));
|
|
208
|
+
}
|
|
209
|
+
if (gateStatus.timestamp) {
|
|
210
|
+
console.log(chalk.gray(` Rejected at: ${gateStatus.timestamp}`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Show approval history if available
|
|
216
|
+
const history = getApprovalHistory(featureName);
|
|
217
|
+
if (history && history.length > 0) {
|
|
218
|
+
console.log(chalk.bold('\nApproval History:'));
|
|
219
|
+
history.slice(-5).forEach(entry => {
|
|
220
|
+
const status = entry.approved ? chalk.green('✓ Approved') : chalk.red('✗ Rejected');
|
|
221
|
+
console.log(` ${status} ${entry.gate} - ${entry.timestamp}`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log('\n' + '━'.repeat(60) + '\n');
|
|
226
|
+
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error(chalk.red(`\n❌ Error showing approval status: ${error.message}\n`));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Approval status command - Show all gates and their status
|
|
235
|
+
*/
|
|
236
|
+
program
|
|
237
|
+
.command('approval-status <feature>')
|
|
238
|
+
.alias('status')
|
|
239
|
+
.description('Show approval status for all gates')
|
|
240
|
+
.action(approvalStatusCommand);
|
|
241
|
+
|
|
242
|
+
// Only parse if run directly (not imported as module)
|
|
243
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
244
|
+
if (process.argv.length > 2) {
|
|
245
|
+
program.parse(process.argv);
|
|
246
|
+
} else {
|
|
247
|
+
program.help();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default program;
|
|
@@ -19,34 +19,61 @@ import { getFeature } from '../../core/state/state-manager.js';
|
|
|
19
19
|
import { validateTasksJson } from '../../lib/tasks/tasks-json-parser.js';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Expected sections per output file
|
|
22
|
+
* Expected sections per output file.
|
|
23
|
+
*
|
|
24
|
+
* Each section entry can be a string (single substring to match) or an array
|
|
25
|
+
* of acceptable substrings (any-of match). This lets pt-BR and English
|
|
26
|
+
* coexist — e.g. "Problem" matches both "Problem" and "Problema", but
|
|
27
|
+
* "Scope" needs to be paired with "Escopo" explicitly.
|
|
28
|
+
*
|
|
29
|
+
* The matcher (`validateMarkdownSections`) does case-insensitive substring
|
|
30
|
+
* matching against the whole file content.
|
|
23
31
|
*/
|
|
24
32
|
const OUTPUT_SECTIONS = {
|
|
25
33
|
'proposal.md': {
|
|
26
|
-
required: [
|
|
27
|
-
|
|
34
|
+
required: [
|
|
35
|
+
'Overview',
|
|
36
|
+
'Problem', // also matches Problema
|
|
37
|
+
['Solution', 'Solução'],
|
|
38
|
+
['Scope', 'Escopo']
|
|
39
|
+
],
|
|
40
|
+
optional: ['Agents', 'Stack', ['Risks', 'Riscos']]
|
|
28
41
|
},
|
|
29
42
|
'spec.md': {
|
|
30
43
|
required: [
|
|
31
|
-
'Overview',
|
|
32
|
-
'
|
|
44
|
+
'Overview',
|
|
45
|
+
['Functional Requirements', 'Requisitos Funcionais'],
|
|
46
|
+
['Non-Functional Requirements', 'Requisitos Não-Funcionais', 'Requisitos Nao-Funcionais'],
|
|
47
|
+
['Technical Architecture', 'Arquitetura'],
|
|
48
|
+
['Data Model', 'Modelo de Dados', 'Contratos']
|
|
33
49
|
],
|
|
34
50
|
optional: [
|
|
35
|
-
'Infrastructure', '
|
|
36
|
-
'
|
|
51
|
+
['Infrastructure', 'Infraestrutura'],
|
|
52
|
+
['Testing Strategy', 'Estratégia de Testes', 'Testes'],
|
|
53
|
+
['Security', 'Segurança'],
|
|
54
|
+
'Performance',
|
|
55
|
+
['Edge Cases', 'Casos Limite'],
|
|
56
|
+
['Clarifications', 'Esclarecimentos']
|
|
37
57
|
]
|
|
38
58
|
},
|
|
39
59
|
'mandate.md': {
|
|
40
|
-
required: ['Stack', 'Standards', 'Done'],
|
|
41
|
-
optional: [
|
|
60
|
+
required: ['Stack', 'Standards', ['Done', 'Pronto']],
|
|
61
|
+
optional: [
|
|
62
|
+
['Patterns proibidos', 'Padrões proibidos', 'Forbidden Patterns'],
|
|
63
|
+
['Riscos', 'Risks']
|
|
64
|
+
]
|
|
42
65
|
},
|
|
43
66
|
'decisions.md': {
|
|
44
|
-
required: ['ADR', 'Status', 'Context', 'Decision'],
|
|
45
|
-
optional: [
|
|
67
|
+
required: ['ADR', 'Status', ['Context', 'Contexto'], ['Decision', 'Decisão']],
|
|
68
|
+
optional: [
|
|
69
|
+
['Consequences', 'Consequências', 'Implicações'],
|
|
70
|
+
['Alternatives', 'Alternativas'],
|
|
71
|
+
['Costs', 'Custos']
|
|
72
|
+
]
|
|
46
73
|
},
|
|
47
74
|
'review-report.md': {
|
|
48
|
-
required: ['Scores', 'Recommendation'],
|
|
49
|
-
optional: ['Blockers', 'Improvements']
|
|
75
|
+
required: [['Scores', 'Pontuação', 'Scoring'], ['Recommendation', 'Recomendação']],
|
|
76
|
+
optional: [['Blockers', 'Bloqueantes'], ['Improvements', 'Melhorias']]
|
|
50
77
|
},
|
|
51
78
|
};
|
|
52
79
|
|
|
@@ -143,24 +170,38 @@ function validateTasksJsonOutput(content, result) {
|
|
|
143
170
|
}
|
|
144
171
|
|
|
145
172
|
/**
|
|
146
|
-
* Validate markdown sections
|
|
173
|
+
* Validate markdown sections.
|
|
174
|
+
*
|
|
175
|
+
* Each `section` entry can be:
|
|
176
|
+
* - a string → must appear as a substring (case-insensitive)
|
|
177
|
+
* - an array → any-of: at least one of the strings must appear
|
|
178
|
+
*
|
|
179
|
+
* The any-of form supports bilingual content (e.g. ['Scope', 'Escopo']).
|
|
147
180
|
*/
|
|
148
181
|
function validateMarkdownSections(content, sections, result) {
|
|
149
182
|
const contentLower = content.toLowerCase();
|
|
183
|
+
const matches = (entry) => {
|
|
184
|
+
const candidates = Array.isArray(entry) ? entry : [entry];
|
|
185
|
+
const hit = candidates.find(c => contentLower.includes(c.toLowerCase()));
|
|
186
|
+
return { hit, label: candidates[0] };
|
|
187
|
+
};
|
|
150
188
|
|
|
151
|
-
for (const
|
|
152
|
-
|
|
153
|
-
|
|
189
|
+
for (const entry of sections.required) {
|
|
190
|
+
const { hit, label } = matches(entry);
|
|
191
|
+
if (hit) {
|
|
192
|
+
result.sections.found.push(hit);
|
|
154
193
|
} else {
|
|
155
|
-
result.sections.missing.push(
|
|
156
|
-
|
|
194
|
+
result.sections.missing.push(label);
|
|
195
|
+
const acceptable = Array.isArray(entry)
|
|
196
|
+
? entry.map(s => `"${s}"`).join(' or ')
|
|
197
|
+
: `"${entry}"`;
|
|
198
|
+
result.errors.push(`Missing required section: ${acceptable}`);
|
|
157
199
|
}
|
|
158
200
|
}
|
|
159
201
|
|
|
160
|
-
for (const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
202
|
+
for (const entry of (sections.optional || [])) {
|
|
203
|
+
const { hit } = matches(entry);
|
|
204
|
+
if (hit) result.sections.optional.push(hit);
|
|
164
205
|
}
|
|
165
206
|
}
|
|
166
207
|
|
|
@@ -22,22 +22,32 @@ export function getStatePath() {
|
|
|
22
22
|
return join(process.cwd(), STATE_FILE_NAME);
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* Derive current phase from filesystem
|
|
26
|
-
*
|
|
25
|
+
* Derive current phase from filesystem.
|
|
26
|
+
*
|
|
27
|
+
* Walks the phase map in descending order and returns the first phase whose
|
|
28
|
+
* CANONICAL ARTIFACT exists. An empty folder is not enough — `mkdir 2-plan/`
|
|
29
|
+
* does not count as "in plan phase" until `spec.md` (the first canonical
|
|
30
|
+
* plan-phase output) lands. If no canonical artifact is found anywhere,
|
|
31
|
+
* returns `'proposal'` (the starting phase).
|
|
32
|
+
*
|
|
33
|
+
* This avoids the chicken-and-egg problem where the agent pre-creates all
|
|
34
|
+
* phase folders, the derived phase jumps to the highest one, and
|
|
35
|
+
* `enforce-phase-writes` then blocks the agent from writing back to the
|
|
36
|
+
* prior phase folder.
|
|
27
37
|
*
|
|
28
38
|
* @param {string} featurePath - Absolute path to .morph/features/{feature}/
|
|
29
39
|
* @returns {string} Phase name: 'review' | 'implement' | 'plan' | 'uiux' | 'proposal'
|
|
30
40
|
*/
|
|
31
41
|
export function derivePhase(featurePath) {
|
|
32
42
|
const phaseMap = [
|
|
33
|
-
['4-review',
|
|
34
|
-
['3-implement', 'implement'],
|
|
35
|
-
['2-plan',
|
|
36
|
-
['1-design',
|
|
37
|
-
['0-proposal',
|
|
43
|
+
['4-review', 'review', 'review-report.md'],
|
|
44
|
+
['3-implement', 'implement', 'recap.md'],
|
|
45
|
+
['2-plan', 'plan', 'spec.md'],
|
|
46
|
+
['1-design', 'uiux', 'design-system.md'],
|
|
47
|
+
['0-proposal', 'proposal', 'proposal.md'],
|
|
38
48
|
];
|
|
39
|
-
for (const [folder, phase] of phaseMap) {
|
|
40
|
-
if (existsSync(join(featurePath, folder))) return phase;
|
|
49
|
+
for (const [folder, phase, canonical] of phaseMap) {
|
|
50
|
+
if (existsSync(join(featurePath, folder, canonical))) return phase;
|
|
41
51
|
}
|
|
42
52
|
return 'proposal';
|
|
43
53
|
}
|
package/src/lib/risk-detector.js
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* Signals checked (in order):
|
|
9
9
|
* - designSystemDivergence: boolean — conflicting design sources detected
|
|
10
10
|
* - standardsViolations: string[] — violations of mandate.md proibições
|
|
11
|
-
* - evalScore: number — morph-eval score (pause if <
|
|
11
|
+
* - evalScore: number — morph-eval score (pause if < 9)
|
|
12
12
|
* - taskCount: number — total tasks (pause if > 20)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const SCOPE_TASK_THRESHOLD = 20;
|
|
16
|
-
const MIN_EVAL_SCORE =
|
|
16
|
+
const MIN_EVAL_SCORE = 9;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @param {object} config - .morph/config/config.json contents (uses `trust`)
|