@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,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;
|