@hongmaple0820/scale-engine 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +127 -179
- package/README.md +168 -1094
- package/dist/agents/AgentSourceLoader.js +1 -1
- package/dist/agents/AgentSourceLoader.js.map +1 -1
- package/dist/agents/types.d.ts +1 -1
- package/dist/api/cli.js +222 -6
- package/dist/api/cli.js.map +1 -1
- package/dist/api/quickstart.d.ts +23 -0
- package/dist/api/quickstart.js +57 -0
- package/dist/api/quickstart.js.map +1 -0
- package/dist/artifact/types.d.ts +5 -1
- package/dist/artifact/types.js.map +1 -1
- package/dist/capabilities/BrowserCapability.d.ts +30 -0
- package/dist/capabilities/BrowserCapability.js +73 -0
- package/dist/capabilities/BrowserCapability.js.map +1 -0
- package/dist/capabilities/CapabilityRegistry.d.ts +17 -0
- package/dist/capabilities/CapabilityRegistry.js +65 -0
- package/dist/capabilities/CapabilityRegistry.js.map +1 -0
- package/dist/capabilities/ComputerCapability.d.ts +28 -0
- package/dist/capabilities/ComputerCapability.js +40 -0
- package/dist/capabilities/ComputerCapability.js.map +1 -0
- package/dist/capabilities/InstalledSkillsIntegration.d.ts +66 -0
- package/dist/capabilities/InstalledSkillsIntegration.js +188 -0
- package/dist/capabilities/InstalledSkillsIntegration.js.map +1 -0
- package/dist/capabilities/SearchCapability.d.ts +46 -0
- package/dist/capabilities/SearchCapability.js +88 -0
- package/dist/capabilities/SearchCapability.js.map +1 -0
- package/dist/capabilities/index.d.ts +6 -0
- package/dist/capabilities/index.js +9 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/types.d.ts +92 -0
- package/dist/capabilities/types.js +7 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/cli/liteCommands.js +1 -1
- package/dist/cli/liteCommands.js.map +1 -1
- package/dist/cli/phaseCommands.d.ts +42 -3
- package/dist/cli/phaseCommands.js +490 -149
- package/dist/cli/phaseCommands.js.map +1 -1
- package/dist/core/logger.js +9 -2
- package/dist/core/logger.js.map +1 -1
- package/dist/hooks/HookGeneratorEnhanced.js +84 -5
- package/dist/hooks/HookGeneratorEnhanced.js.map +1 -1
- package/dist/hooks/WorkflowHooksManager.d.ts +30 -0
- package/dist/hooks/WorkflowHooksManager.js +117 -0
- package/dist/hooks/WorkflowHooksManager.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/skills/SkillExecutor.d.ts +11 -1
- package/dist/skills/SkillExecutor.js +160 -5
- package/dist/skills/SkillExecutor.js.map +1 -1
- package/dist/workflow/EvidenceStore.d.ts +20 -0
- package/dist/workflow/EvidenceStore.js +48 -0
- package/dist/workflow/EvidenceStore.js.map +1 -0
- package/dist/workflow/ReviewAnalyzer.d.ts +33 -0
- package/dist/workflow/ReviewAnalyzer.js +264 -0
- package/dist/workflow/ReviewAnalyzer.js.map +1 -0
- package/dist/workflow/ReviewStore.d.ts +32 -0
- package/dist/workflow/ReviewStore.js +42 -0
- package/dist/workflow/ReviewStore.js.map +1 -0
- package/dist/workflow/VerificationCommands.d.ts +23 -0
- package/dist/workflow/VerificationCommands.js +125 -0
- package/dist/workflow/VerificationCommands.js.map +1 -0
- package/dist/workflow/WorkflowEngine.d.ts +62 -0
- package/dist/workflow/WorkflowEngine.js +151 -0
- package/dist/workflow/WorkflowEngine.js.map +1 -0
- package/dist/workflow/cognitive/AmbiguityScorer.d.ts +17 -0
- package/dist/workflow/cognitive/AmbiguityScorer.js +107 -0
- package/dist/workflow/cognitive/AmbiguityScorer.js.map +1 -0
- package/dist/workflow/cognitive/ConsensusPlanner.d.ts +26 -0
- package/dist/workflow/cognitive/ConsensusPlanner.js +141 -0
- package/dist/workflow/cognitive/ConsensusPlanner.js.map +1 -0
- package/dist/workflow/cognitive/SocraticQuestioner.d.ts +33 -0
- package/dist/workflow/cognitive/SocraticQuestioner.js +276 -0
- package/dist/workflow/cognitive/SocraticQuestioner.js.map +1 -0
- package/dist/workflow/execution/RalphEngine.d.ts +36 -0
- package/dist/workflow/execution/RalphEngine.js +123 -0
- package/dist/workflow/execution/RalphEngine.js.map +1 -0
- package/dist/workflow/execution/UltraworkEngine.d.ts +43 -0
- package/dist/workflow/execution/UltraworkEngine.js +135 -0
- package/dist/workflow/execution/UltraworkEngine.js.map +1 -0
- package/dist/workflow/gates/GateSystem.d.ts +130 -0
- package/dist/workflow/gates/GateSystem.js +788 -0
- package/dist/workflow/gates/GateSystem.js.map +1 -0
- package/dist/workflow/index.d.ts +12 -0
- package/dist/workflow/index.js +14 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/quality/HonestDelivery.d.ts +19 -0
- package/dist/workflow/quality/HonestDelivery.js +77 -0
- package/dist/workflow/quality/HonestDelivery.js.map +1 -0
- package/dist/workflow/quality/KarpathyEvaluator.d.ts +18 -0
- package/dist/workflow/quality/KarpathyEvaluator.js +76 -0
- package/dist/workflow/quality/KarpathyEvaluator.js.map +1 -0
- package/dist/workflow/types.d.ts +146 -0
- package/dist/workflow/types.js +4 -0
- package/dist/workflow/types.js.map +1 -0
- package/dist/workflows/DAGBuilder.js +1 -1
- package/dist/workflows/DAGBuilder.js.map +1 -1
- package/dist/workflows/WorkflowOrchestrator.js +1 -1
- package/dist/workflows/WorkflowOrchestrator.js.map +1 -1
- package/dist/workflows/index.js +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,14 +1,64 @@
|
|
|
1
|
-
// SCALE Engine
|
|
2
|
-
// 6
|
|
1
|
+
// SCALE Engine - Phase-Aligned Commands (v0.10.0)
|
|
2
|
+
// 6 phase commands: DEFINE -> PLAN -> BUILD -> VERIFY -> REVIEW -> SHIP
|
|
3
|
+
// Integrates WorkflowEngine cognitive scaffolding and quality gates.
|
|
3
4
|
import { defineCommand } from 'citty';
|
|
4
5
|
// Engine singleton (reuse from cli.ts)
|
|
5
6
|
import { EventBus } from '../core/eventBus.js';
|
|
6
7
|
import { SQLiteArtifactStore } from '../artifact/sqliteStore.js';
|
|
7
8
|
import { FSM } from '../artifact/fsm.js';
|
|
8
9
|
import { registerAllFSMs } from '../artifact/fsmDefinitions.js';
|
|
10
|
+
import { CapabilityRegistry } from '../capabilities/CapabilityRegistry.js';
|
|
11
|
+
import { SkillRegistry } from '../skills/SkillRegistry.js';
|
|
12
|
+
import { WorkflowEngine } from '../workflow/WorkflowEngine.js';
|
|
13
|
+
import { EvidenceStore } from '../workflow/EvidenceStore.js';
|
|
14
|
+
import { ReviewStore } from '../workflow/ReviewStore.js';
|
|
15
|
+
import { analyzeReview, parseChangedFiles, shouldReviewFile, summarizeFindings } from '../workflow/ReviewAnalyzer.js';
|
|
9
16
|
import { join } from 'node:path';
|
|
10
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
11
18
|
const SCALE_DIR = process.env.SCALE_DIR ?? '.scale';
|
|
19
|
+
function validateVerificationEvidence(ids) {
|
|
20
|
+
const evidenceStore = new EvidenceStore(SCALE_DIR);
|
|
21
|
+
const missing = [];
|
|
22
|
+
const failed = [];
|
|
23
|
+
for (const id of ids ?? []) {
|
|
24
|
+
const record = evidenceStore.getGateResult(id);
|
|
25
|
+
if (!record) {
|
|
26
|
+
missing.push(id);
|
|
27
|
+
}
|
|
28
|
+
else if (!record.passed) {
|
|
29
|
+
failed.push(id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { ok: (ids?.length ?? 0) > 0 && missing.length === 0 && failed.length === 0, missing, failed };
|
|
33
|
+
}
|
|
34
|
+
function validateReviewEvidence(ids) {
|
|
35
|
+
const reviewStore = new ReviewStore(SCALE_DIR);
|
|
36
|
+
const missing = [];
|
|
37
|
+
const failed = [];
|
|
38
|
+
for (const id of ids ?? []) {
|
|
39
|
+
const record = reviewStore.getReview(id);
|
|
40
|
+
if (!record) {
|
|
41
|
+
missing.push(id);
|
|
42
|
+
}
|
|
43
|
+
else if (!record.passed) {
|
|
44
|
+
failed.push(id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { ok: (ids?.length ?? 0) > 0 && missing.length === 0 && failed.length === 0, missing, failed };
|
|
48
|
+
}
|
|
49
|
+
function getValidatedReviewRecords(ids) {
|
|
50
|
+
const reviewStore = new ReviewStore(SCALE_DIR);
|
|
51
|
+
return (ids ?? [])
|
|
52
|
+
.map(id => reviewStore.getReview(id))
|
|
53
|
+
.filter((record) => Boolean(record?.passed));
|
|
54
|
+
}
|
|
55
|
+
function getVerificationEvidenceSummary(ids) {
|
|
56
|
+
const evidenceStore = new EvidenceStore(SCALE_DIR);
|
|
57
|
+
return (ids ?? [])
|
|
58
|
+
.map(id => evidenceStore.getGateResult(id))
|
|
59
|
+
.filter((record) => Boolean(record))
|
|
60
|
+
.map(record => ({ gate: record.gate, passed: record.passed }));
|
|
61
|
+
}
|
|
12
62
|
function getEngine() {
|
|
13
63
|
ensureDir(SCALE_DIR);
|
|
14
64
|
const eventBus = new EventBus({ eventsDir: join(SCALE_DIR, 'events') });
|
|
@@ -18,12 +68,31 @@ function getEngine() {
|
|
|
18
68
|
});
|
|
19
69
|
const fsm = new FSM(store, eventBus);
|
|
20
70
|
registerAllFSMs(fsm);
|
|
21
|
-
|
|
71
|
+
// Initialize capability registry
|
|
72
|
+
const capabilityRegistry = new CapabilityRegistry(eventBus);
|
|
73
|
+
// Initialize skill registry
|
|
74
|
+
const skillRegistry = new SkillRegistry(eventBus);
|
|
75
|
+
// Initialize workflow engine with cognitive scaffolding and quality gates.
|
|
76
|
+
const workflowEngine = new WorkflowEngine({
|
|
77
|
+
eventBus,
|
|
78
|
+
capabilityRegistry,
|
|
79
|
+
skillRegistry
|
|
80
|
+
});
|
|
81
|
+
return { eventBus, store, fsm, workflowEngine, skillRegistry };
|
|
22
82
|
}
|
|
23
83
|
function ensureDir(dir) {
|
|
24
84
|
if (!existsSync(dir))
|
|
25
85
|
mkdirSync(dir, { recursive: true });
|
|
26
86
|
}
|
|
87
|
+
function isTruthyFlag(value) {
|
|
88
|
+
return value === true || value === '' || value === 'true' || value === '1';
|
|
89
|
+
}
|
|
90
|
+
function shouldSkipCommit(value) {
|
|
91
|
+
return isTruthyFlag(value) || process.argv.includes('--no-commit') || process.argv.includes('--skip-commit');
|
|
92
|
+
}
|
|
93
|
+
function normalizeGitPath(path) {
|
|
94
|
+
return path.replace(/\\/g, '/');
|
|
95
|
+
}
|
|
27
96
|
// Helper: Generate spec markdown file
|
|
28
97
|
function generateSpecMarkdown(id, title, payload) {
|
|
29
98
|
return `# Spec: ${title}
|
|
@@ -63,34 +132,107 @@ function calculateAmbiguityScore(description, successCriteria) {
|
|
|
63
132
|
score -= 0.02;
|
|
64
133
|
return Math.max(0.05, score);
|
|
65
134
|
}
|
|
66
|
-
// DEFINE Phase
|
|
135
|
+
// DEFINE Phase - AmbiguityScorer + SocraticQuestioner + G1 gate
|
|
67
136
|
export const phaseDefine = defineCommand({
|
|
68
|
-
meta: { name: 'define', description: 'DEFINE: Create Spec (/spec)' },
|
|
137
|
+
meta: { name: 'define', description: 'DEFINE: Create Spec with AmbiguityScorer + SocraticQuestioner (/spec)' },
|
|
69
138
|
args: {
|
|
70
139
|
title: { type: 'positional', required: true },
|
|
71
140
|
description: { type: 'string', alias: 'd' },
|
|
72
141
|
'success-criteria': { type: 'string', alias: 'c', description: 'Comma-separated criteria' },
|
|
142
|
+
// Socratic refinement answers (optional)
|
|
143
|
+
'goal': { type: 'string', description: 'Goal answer for Socratic refinement' },
|
|
144
|
+
'constraint': { type: 'string', description: 'Constraint answer for Socratic refinement' },
|
|
145
|
+
'acceptance': { type: 'string', description: 'Acceptance criteria answer for Socratic refinement' },
|
|
146
|
+
'context': { type: 'string', description: 'Context answer for Socratic refinement' },
|
|
147
|
+
'risk': { type: 'string', description: 'Risk answer for Socratic refinement' },
|
|
148
|
+
'priority': { type: 'string', description: 'Priority answer for Socratic refinement' },
|
|
73
149
|
json: { type: 'boolean', default: false },
|
|
74
150
|
},
|
|
75
151
|
async run({ args }) {
|
|
76
|
-
const { store, fsm } = getEngine();
|
|
152
|
+
const { store, fsm, workflowEngine } = getEngine();
|
|
77
153
|
const desc = args.description ?? args.title;
|
|
78
154
|
// Parse success criteria
|
|
79
155
|
const successCriteria = args['success-criteria']
|
|
80
156
|
? args['success-criteria'].split(',').map(s => s.trim()).filter(s => s)
|
|
81
157
|
: ['Feature works as described', 'No regression in existing functionality'];
|
|
82
|
-
//
|
|
83
|
-
|
|
158
|
+
// === WorkflowEngine Integration ===
|
|
159
|
+
// Step 1: Explore with AmbiguityScorer + SocraticQuestioner
|
|
160
|
+
const exploreResult = await workflowEngine.explore(desc);
|
|
161
|
+
const ambiguityResult = workflowEngine.getAmbiguityScorer().analyzeRequirement(desc);
|
|
162
|
+
// Step 2: Check if requirement needs refinement.
|
|
163
|
+
if (ambiguityResult.blocked) {
|
|
164
|
+
console.error('\nRequirement ambiguity is too high (>40%); refine the requirement first.');
|
|
165
|
+
console.log('\n Refine the requirement by answering:');
|
|
166
|
+
console.log(' - What is the goal?');
|
|
167
|
+
console.log(' - What are the input/output boundaries?');
|
|
168
|
+
console.log(' - What are the acceptance criteria?\n');
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
// Step 3: Handle Socratic refinement if ambiguity > 20%
|
|
172
|
+
let refinedRequirement = desc;
|
|
173
|
+
let finalAmbiguityScore = ambiguityResult.totalScore;
|
|
174
|
+
if (ambiguityResult.requiresQuestioning && exploreResult.socraticSession) {
|
|
175
|
+
const session = exploreResult.socraticSession;
|
|
176
|
+
if (!args.json) {
|
|
177
|
+
console.log('\nRequirement ambiguity is >20%; starting Socratic refinement.');
|
|
178
|
+
console.log('\nSix-question refinement framework:');
|
|
179
|
+
console.log(workflowEngine.getSocraticQuestioner().formatSessionReport(session));
|
|
180
|
+
}
|
|
181
|
+
// Check if user provided answers via CLI args
|
|
182
|
+
const answers = [];
|
|
183
|
+
if (args.goal)
|
|
184
|
+
answers.push({ questionId: 'q-goal', answer: args.goal });
|
|
185
|
+
if (args.constraint)
|
|
186
|
+
answers.push({ questionId: 'q-constraint', answer: args.constraint });
|
|
187
|
+
if (args.acceptance)
|
|
188
|
+
answers.push({ questionId: 'q-acceptance', answer: args.acceptance });
|
|
189
|
+
if (args.context)
|
|
190
|
+
answers.push({ questionId: 'q-context', answer: args.context });
|
|
191
|
+
if (args.risk)
|
|
192
|
+
answers.push({ questionId: 'q-risk', answer: args.risk });
|
|
193
|
+
if (args.priority)
|
|
194
|
+
answers.push({ questionId: 'q-priority', answer: args.priority });
|
|
195
|
+
// If answers provided, process them
|
|
196
|
+
if (answers.length > 0) {
|
|
197
|
+
for (const { questionId, answer } of answers) {
|
|
198
|
+
workflowEngine.getSocraticQuestioner().recordAnswer(session.sessionId, questionId, answer);
|
|
199
|
+
}
|
|
200
|
+
const progress = workflowEngine.getSocraticQuestioner().evaluateProgress(session);
|
|
201
|
+
if (progress.refined) {
|
|
202
|
+
refinedRequirement = workflowEngine.getSocraticQuestioner().generateRefinedRequirement(session);
|
|
203
|
+
finalAmbiguityScore = progress.newAmbiguity;
|
|
204
|
+
if (!args.json) {
|
|
205
|
+
console.log('\nRequirement refined; ambiguity reduced to: ' + finalAmbiguityScore.toFixed(2));
|
|
206
|
+
console.log('\nRefined requirement:');
|
|
207
|
+
console.log(refinedRequirement);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (!args.json) {
|
|
211
|
+
console.log('\nMore answers are needed to refine the requirement.');
|
|
212
|
+
console.log(' Current ambiguity: ' + progress.newAmbiguity.toFixed(2));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (!args.json) {
|
|
216
|
+
console.log('\nYou can refine the requirement with:');
|
|
217
|
+
console.log(' --goal "goal description"');
|
|
218
|
+
console.log(' --constraint "constraints and boundaries"');
|
|
219
|
+
console.log(' --acceptance "acceptance criteria"');
|
|
220
|
+
console.log(' --context "context and dependencies"');
|
|
221
|
+
console.log(' --risk "risk scenarios"');
|
|
222
|
+
console.log(' --priority "priority order"\n');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const ambiguityScore = finalAmbiguityScore;
|
|
84
226
|
// Create Need artifact
|
|
85
227
|
const need = await store.create({
|
|
86
228
|
type: 'Need', title: args.title,
|
|
87
|
-
payload: { rawText:
|
|
229
|
+
payload: { rawText: refinedRequirement },
|
|
88
230
|
initialStatus: 'DRAFT',
|
|
89
231
|
createdBy: { kind: 'human', userId: 'cli' },
|
|
90
232
|
});
|
|
91
|
-
// Create Spec artifact with proper payload
|
|
233
|
+
// Create Spec artifact with proper payload (use refined requirement if available)
|
|
92
234
|
const specPayload = {
|
|
93
|
-
what:
|
|
235
|
+
what: refinedRequirement,
|
|
94
236
|
successCriteria,
|
|
95
237
|
outOfScope: [],
|
|
96
238
|
edgeCases: [],
|
|
@@ -118,13 +260,13 @@ export const phaseDefine = defineCommand({
|
|
|
118
260
|
// Guard may fail - report reason
|
|
119
261
|
const error = e;
|
|
120
262
|
if (!args.json)
|
|
121
|
-
console.log(`
|
|
263
|
+
console.log(` FSM transition: ${error.message}`);
|
|
122
264
|
}
|
|
123
265
|
const result = { phase: 'DEFINE', spec, specPath, ambiguityScore, successCriteria };
|
|
124
266
|
if (args.json)
|
|
125
267
|
console.log(JSON.stringify(result, null, 2));
|
|
126
268
|
else {
|
|
127
|
-
console.log(`\
|
|
269
|
+
console.log(`\nDEFINE: ${spec.id}`);
|
|
128
270
|
console.log(` Spec file: ${specPath}`);
|
|
129
271
|
console.log(` Ambiguity score: ${ambiguityScore.toFixed(2)}`);
|
|
130
272
|
console.log(` Success criteria: ${successCriteria.length}`);
|
|
@@ -158,9 +300,9 @@ ${payload.estimatedComplexity ?? 5}/10
|
|
|
158
300
|
*Generated by SCALE Engine PLAN phase*
|
|
159
301
|
`;
|
|
160
302
|
}
|
|
161
|
-
// PLAN Phase
|
|
303
|
+
// PLAN Phase - ConsensusPlanner + G2 gate
|
|
162
304
|
export const phasePlan = defineCommand({
|
|
163
|
-
meta: { name: 'plan', description: 'PLAN: Create Plan (/plan)' },
|
|
305
|
+
meta: { name: 'plan', description: 'PLAN: Create Plan with ConsensusPlanner (/plan)' },
|
|
164
306
|
args: {
|
|
165
307
|
'spec-id': { type: 'positional', required: true },
|
|
166
308
|
approach: { type: 'string', alias: 'a', description: 'Implementation approach' },
|
|
@@ -168,16 +310,25 @@ export const phasePlan = defineCommand({
|
|
|
168
310
|
json: { type: 'boolean', default: false },
|
|
169
311
|
},
|
|
170
312
|
async run({ args }) {
|
|
171
|
-
const { store, fsm } = getEngine();
|
|
313
|
+
const { store, fsm, workflowEngine } = getEngine();
|
|
172
314
|
// Validate spec exists
|
|
173
315
|
const spec = await store.get(args['spec-id']);
|
|
174
316
|
if (!spec || spec.type !== 'Spec') {
|
|
175
|
-
console.error(`\
|
|
317
|
+
console.error(`\nSpec not found: ${args['spec-id']}\n`);
|
|
176
318
|
process.exit(1);
|
|
177
319
|
}
|
|
320
|
+
// === WorkflowEngine Integration ===
|
|
321
|
+
// Step 1: Run ConsensusPlanner (Planner -> Architect -> Critic).
|
|
322
|
+
const specDesc = spec.payload.what;
|
|
323
|
+
const consensusResult = await workflowEngine.plan(specDesc);
|
|
324
|
+
// Step 2: Display RALPLAN-DR output
|
|
325
|
+
if (!args.json) {
|
|
326
|
+
console.log('\nConsensus Planning Result:');
|
|
327
|
+
console.log(workflowEngine.getConsensusPlanner().formatReport(consensusResult));
|
|
328
|
+
}
|
|
178
329
|
// Default rollback strategy (FSM guard requires this)
|
|
179
|
-
const rollbackStrategy = args.rollback ?? 'Revert git commits
|
|
180
|
-
const approach = args.approach ??
|
|
330
|
+
const rollbackStrategy = args.rollback ?? consensusResult.preMortem.mitigations.join('\n') ?? 'Revert git commits';
|
|
331
|
+
const approach = args.approach ?? consensusResult.viableOptions.find((o) => o.selected)?.description ?? 'Standard implementation';
|
|
181
332
|
// Create PlanPayload with rollback strategy
|
|
182
333
|
const planPayload = {
|
|
183
334
|
approach,
|
|
@@ -205,13 +356,13 @@ export const phasePlan = defineCommand({
|
|
|
205
356
|
catch (e) {
|
|
206
357
|
const error = e;
|
|
207
358
|
if (!args.json)
|
|
208
|
-
console.log(`
|
|
359
|
+
console.log(` FSM transition: ${error.message}`);
|
|
209
360
|
}
|
|
210
361
|
const result = { phase: 'PLAN', plan, planPath, rollbackStrategy };
|
|
211
362
|
if (args.json)
|
|
212
363
|
console.log(JSON.stringify(result, null, 2));
|
|
213
364
|
else {
|
|
214
|
-
console.log(`\
|
|
365
|
+
console.log(`\nPLAN: ${plan.id}`);
|
|
215
366
|
console.log(` Plan file: ${planPath}`);
|
|
216
367
|
console.log(` Rollback: ${rollbackStrategy}`);
|
|
217
368
|
console.log(`\n Next: scale build ${plan.id}\n`);
|
|
@@ -231,7 +382,7 @@ export const phaseBuild = defineCommand({
|
|
|
231
382
|
// Validate plan exists
|
|
232
383
|
const plan = await store.get(args['plan-id']);
|
|
233
384
|
if (!plan || plan.type !== 'Plan') {
|
|
234
|
-
console.error(`\
|
|
385
|
+
console.error(`\nPlan not found: ${args['plan-id']}\n`);
|
|
235
386
|
process.exit(1);
|
|
236
387
|
}
|
|
237
388
|
// Create TaskPayload
|
|
@@ -262,7 +413,7 @@ export const phaseBuild = defineCommand({
|
|
|
262
413
|
catch (e) {
|
|
263
414
|
const error = e;
|
|
264
415
|
if (!args.json)
|
|
265
|
-
console.log(`
|
|
416
|
+
console.log(` FSM transition: ${error.message}`);
|
|
266
417
|
}
|
|
267
418
|
// Update Plan status to IMPLEMENTING
|
|
268
419
|
try {
|
|
@@ -273,7 +424,7 @@ export const phaseBuild = defineCommand({
|
|
|
273
424
|
if (args.json)
|
|
274
425
|
console.log(JSON.stringify(result, null, 2));
|
|
275
426
|
else {
|
|
276
|
-
console.log(`\
|
|
427
|
+
console.log(`\nBUILD: ${task.id}`);
|
|
277
428
|
console.log(` Status: RUNNING (ready to implement)`);
|
|
278
429
|
console.log(` Description: ${taskPayload.description}`);
|
|
279
430
|
console.log(`\n Implement now, then run: scale verify ${task.id}\n`);
|
|
@@ -291,81 +442,73 @@ async function runVerificationCmd(cmd) {
|
|
|
291
442
|
child.on('close', (code) => resolve({ exitCode: code ?? 1, output }));
|
|
292
443
|
});
|
|
293
444
|
}
|
|
294
|
-
// VERIFY Phase
|
|
445
|
+
// VERIFY Phase - GateSystem quality gates
|
|
295
446
|
export const phaseVerify = defineCommand({
|
|
296
|
-
meta: { name: 'verify', description: 'VERIFY: Run
|
|
447
|
+
meta: { name: 'verify', description: 'VERIFY: Run Gates G3-G7 (/test)' },
|
|
297
448
|
args: {
|
|
298
449
|
'task-id': { type: 'positional', required: true },
|
|
299
|
-
'build-cmd': { type: 'string',
|
|
300
|
-
'lint-cmd': { type: 'string',
|
|
301
|
-
'test-cmd': { type: 'string',
|
|
450
|
+
'build-cmd': { type: 'string', description: 'Override build command' },
|
|
451
|
+
'lint-cmd': { type: 'string', description: 'Override lint command' },
|
|
452
|
+
'test-cmd': { type: 'string', description: 'Override test command' },
|
|
453
|
+
'coverage-cmd': { type: 'string', description: 'Override coverage command' },
|
|
454
|
+
'tdd-evidence': { type: 'string', description: 'Path to JSON TDD evidence with red/green/refactor/testFirst=true' },
|
|
455
|
+
'tdd-strict': { type: 'boolean', default: false, description: 'Require TDD evidence before other gates' },
|
|
302
456
|
'skip-build': { type: 'boolean', default: false },
|
|
303
457
|
'skip-lint': { type: 'boolean', default: false },
|
|
304
458
|
'skip-test': { type: 'boolean', default: false },
|
|
305
459
|
json: { type: 'boolean', default: false },
|
|
306
460
|
},
|
|
307
461
|
async run({ args }) {
|
|
308
|
-
const { store, fsm } = getEngine();
|
|
462
|
+
const { store, fsm, workflowEngine } = getEngine();
|
|
309
463
|
// Validate task exists
|
|
310
464
|
const task = await store.get(args['task-id']);
|
|
311
465
|
if (!task || task.type !== 'Task') {
|
|
312
|
-
console.error(`\
|
|
466
|
+
console.error(`\nTask not found: ${args['task-id']}\n`);
|
|
313
467
|
process.exit(1);
|
|
314
468
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// Run lint
|
|
337
|
-
if (!args['skip-lint']) {
|
|
338
|
-
if (!args.json)
|
|
339
|
-
console.log('\n🔍 Running lint...');
|
|
340
|
-
const lint = await runVerificationCmd(args['lint-cmd']);
|
|
341
|
-
results.lintStatus = lint.exitCode === 0 ? 'success' : 'failed';
|
|
342
|
-
if (!args.json) {
|
|
343
|
-
if (lint.exitCode === 0)
|
|
344
|
-
console.log(' ✅ Lint passed');
|
|
345
|
-
else
|
|
346
|
-
console.log(' ❌ Lint failed (exit code:', lint.exitCode, ')');
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
// Run tests
|
|
350
|
-
if (!args['skip-test']) {
|
|
351
|
-
if (!args.json)
|
|
352
|
-
console.log('\n🧪 Running tests...');
|
|
353
|
-
const test = await runVerificationCmd(args['test-cmd']);
|
|
354
|
-
results.testPassed = test.exitCode === 0;
|
|
355
|
-
// Extract coverage (Jest format)
|
|
356
|
-
const coverageMatch = test.output.match(/All files[^|]*\|[^|]*\|[^|]*\|[^|]*\|[^|]*\|[^|]*\|\s*(\d+\.?\d*)/);
|
|
357
|
-
if (coverageMatch)
|
|
358
|
-
results.testCoverage = parseFloat(coverageMatch[1]);
|
|
359
|
-
if (!args.json) {
|
|
360
|
-
if (test.exitCode === 0) {
|
|
361
|
-
console.log(' ✅ Tests passed');
|
|
362
|
-
if (results.testCoverage)
|
|
363
|
-
console.log(' Coverage:', results.testCoverage, '%');
|
|
469
|
+
// === WorkflowEngine Integration ===
|
|
470
|
+
// Step 1: Run GateSystem G3-G7
|
|
471
|
+
if (!args.json)
|
|
472
|
+
console.log('\nRunning Quality Gates...');
|
|
473
|
+
const gateResults = await workflowEngine.verify({
|
|
474
|
+
build: args['build-cmd'],
|
|
475
|
+
lint: args['lint-cmd'],
|
|
476
|
+
test: args['test-cmd'],
|
|
477
|
+
coverage: args['coverage-cmd'],
|
|
478
|
+
tddEvidence: args['tdd-evidence'],
|
|
479
|
+
tddStrict: isTruthyFlag(args['tdd-strict']),
|
|
480
|
+
});
|
|
481
|
+
// Step 2: Display gate results
|
|
482
|
+
if (!args.json) {
|
|
483
|
+
console.log('\nGate Results:');
|
|
484
|
+
for (const result of gateResults) {
|
|
485
|
+
console.log(` ${result.passed ? '[PASS]' : '[FAIL]'} ${result.gate}: ${result.evidence.slice(0, 50)}`);
|
|
486
|
+
if (result.blockers.length > 0) {
|
|
487
|
+
result.blockers.forEach((b) => console.log(` [BLOCKER] ${b.slice(0, 80)}`));
|
|
364
488
|
}
|
|
365
|
-
else
|
|
366
|
-
console.log(' ❌ Tests failed (exit code:', test.exitCode, ')');
|
|
367
489
|
}
|
|
368
490
|
}
|
|
491
|
+
// Extract results from gateResults
|
|
492
|
+
const g0Result = gateResults.find(g => g.gate === 'G0');
|
|
493
|
+
const g4Result = gateResults.find(g => g.gate === 'G4');
|
|
494
|
+
const g5Result = gateResults.find(g => g.gate === 'G5');
|
|
495
|
+
const g6Result = gateResults.find(g => g.gate === 'G6');
|
|
496
|
+
const g7Result = gateResults.find(g => g.gate === 'G7');
|
|
497
|
+
const results = {
|
|
498
|
+
buildStatus: g0Result?.passed ? 'success' : 'failed',
|
|
499
|
+
buildExitCode: g0Result?.evidenceItems?.find(item => item.kind === 'command')?.exitCode,
|
|
500
|
+
lintStatus: g4Result?.passed ? 'success' : 'failed',
|
|
501
|
+
testPassed: g5Result?.passed,
|
|
502
|
+
testCoverage: undefined,
|
|
503
|
+
securityPassed: g7Result?.passed,
|
|
504
|
+
};
|
|
505
|
+
const verificationEvidenceIds = gateResults
|
|
506
|
+
.map(g => g.evidenceRecordId)
|
|
507
|
+
.filter((id) => Boolean(id));
|
|
508
|
+
// Extract coverage from G6 evidence
|
|
509
|
+
const coverageMatch = g6Result?.evidence.match(/Coverage: (\d+\.?\d*)%/);
|
|
510
|
+
if (coverageMatch)
|
|
511
|
+
results.testCoverage = parseFloat(coverageMatch[1]);
|
|
369
512
|
// Update Task payload with verification results
|
|
370
513
|
const currentPayload = task.payload;
|
|
371
514
|
const updatedPayload = {
|
|
@@ -375,12 +518,17 @@ export const phaseVerify = defineCommand({
|
|
|
375
518
|
lintStatus: results.lintStatus,
|
|
376
519
|
testPassed: results.testPassed,
|
|
377
520
|
testCoverage: results.testCoverage,
|
|
521
|
+
verificationEvidenceIds,
|
|
522
|
+
verifiedAt: Date.now(),
|
|
378
523
|
};
|
|
379
524
|
await store.update(args['task-id'], { payload: updatedPayload });
|
|
380
525
|
// Attempt FSM transition to COMPLETED
|
|
381
526
|
const allPassed = results.buildStatus === 'success' &&
|
|
527
|
+
(results.buildExitCode ?? 1) === 0 &&
|
|
382
528
|
results.lintStatus === 'success' &&
|
|
383
|
-
results.testPassed === true
|
|
529
|
+
results.testPassed === true &&
|
|
530
|
+
(results.testCoverage ?? 0) >= 80 &&
|
|
531
|
+
results.securityPassed === true;
|
|
384
532
|
let transitionResult = null;
|
|
385
533
|
if (allPassed) {
|
|
386
534
|
try {
|
|
@@ -388,20 +536,20 @@ export const phaseVerify = defineCommand({
|
|
|
388
536
|
actor: { kind: 'human', userId: 'cli' }
|
|
389
537
|
});
|
|
390
538
|
if (!args.json)
|
|
391
|
-
console.log('\
|
|
539
|
+
console.log('\nTask marked COMPLETED');
|
|
392
540
|
}
|
|
393
541
|
catch (e) {
|
|
394
542
|
const error = e;
|
|
395
543
|
if (!args.json)
|
|
396
|
-
console.log('\
|
|
544
|
+
console.log('\nFSM transition failed:', error.message);
|
|
397
545
|
}
|
|
398
546
|
}
|
|
399
547
|
const passed = allPassed && (transitionResult?.success ?? false);
|
|
400
|
-
const result = { phase: 'VERIFY', taskId: args['task-id'], results, passed };
|
|
548
|
+
const result = { phase: 'VERIFY', taskId: args['task-id'], results, evidenceIds: verificationEvidenceIds, passed };
|
|
401
549
|
if (args.json)
|
|
402
550
|
console.log(JSON.stringify(result, null, 2));
|
|
403
551
|
else {
|
|
404
|
-
console.log(`\
|
|
552
|
+
console.log(`\nVERIFY: ${passed ? 'PASSED' : 'FAILED'}`);
|
|
405
553
|
if (passed)
|
|
406
554
|
console.log(`\n Next: scale review\n`);
|
|
407
555
|
else
|
|
@@ -409,9 +557,107 @@ export const phaseVerify = defineCommand({
|
|
|
409
557
|
}
|
|
410
558
|
},
|
|
411
559
|
});
|
|
412
|
-
|
|
560
|
+
async function runGit(args) {
|
|
561
|
+
const { execa } = await import('execa');
|
|
562
|
+
const result = await execa('git', args, { reject: false });
|
|
563
|
+
return {
|
|
564
|
+
exitCode: result.exitCode ?? 1,
|
|
565
|
+
stdout: result.stdout ?? '',
|
|
566
|
+
stderr: result.stderr ?? '',
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function mergeUntrackedFilesIntoStatus(statusOutput, untrackedOutput) {
|
|
570
|
+
const existing = new Set(parseChangedFiles(statusOutput).map(file => file.path.replace(/\\/g, '/')));
|
|
571
|
+
const additions = untrackedOutput
|
|
572
|
+
.split('\n')
|
|
573
|
+
.map(line => line.trim())
|
|
574
|
+
.filter(Boolean)
|
|
575
|
+
.filter(path => shouldReviewFile(path))
|
|
576
|
+
.filter(path => !existing.has(path.replace(/\\/g, '/')));
|
|
577
|
+
return [statusOutput.trim(), ...additions].filter(Boolean).join('\n');
|
|
578
|
+
}
|
|
579
|
+
function readUntrackedFileAsDiff(path) {
|
|
580
|
+
try {
|
|
581
|
+
const stat = statSync(path);
|
|
582
|
+
if (!stat.isFile() || stat.size > 250_000)
|
|
583
|
+
return '';
|
|
584
|
+
const content = readFileSync(path, 'utf-8');
|
|
585
|
+
if (content.includes('\u0000'))
|
|
586
|
+
return '';
|
|
587
|
+
return content
|
|
588
|
+
.split('\n')
|
|
589
|
+
.slice(0, 2000)
|
|
590
|
+
.map(line => `+${line}`)
|
|
591
|
+
.join('\n');
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
return '';
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async function reviewGitChanges(taskPayload) {
|
|
598
|
+
const status = await runGit(['status', '--short']);
|
|
599
|
+
const untracked = await runGit(['ls-files', '--others', '--exclude-standard']);
|
|
600
|
+
const statusOutput = mergeUntrackedFilesIntoStatus(status.stdout, untracked.stdout);
|
|
601
|
+
const verificationEvidence = getVerificationEvidenceSummary(taskPayload?.verificationEvidenceIds);
|
|
602
|
+
const changedFiles = analyzeReview({ statusOutput, diffs: [], taskPayload, verificationEvidence }).changedFiles;
|
|
603
|
+
const diffs = [];
|
|
604
|
+
for (const file of changedFiles.slice(0, 50)) {
|
|
605
|
+
if (file.status === '??') {
|
|
606
|
+
diffs.push({ file: file.path, text: readUntrackedFileAsDiff(file.path) });
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const diff = await runGit(['diff', '--', file.path]);
|
|
610
|
+
diffs.push({ file: file.path, text: diff.stdout });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return analyzeReview({ statusOutput, diffs, taskPayload, verificationEvidence });
|
|
614
|
+
}
|
|
615
|
+
function collectReviewedFiles(records) {
|
|
616
|
+
const reviewed = new Set();
|
|
617
|
+
for (const record of records) {
|
|
618
|
+
if (!record.passed)
|
|
619
|
+
continue;
|
|
620
|
+
for (const file of record.changedFiles) {
|
|
621
|
+
if (shouldReviewFile(file))
|
|
622
|
+
reviewed.add(normalizeGitPath(file));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return reviewed;
|
|
626
|
+
}
|
|
627
|
+
async function getReviewableGitChanges() {
|
|
628
|
+
const status = await runGit(['status', '--short']);
|
|
629
|
+
const untracked = await runGit(['ls-files', '--others', '--exclude-standard']);
|
|
630
|
+
const statusOutput = mergeUntrackedFilesIntoStatus(status.stdout, untracked.stdout);
|
|
631
|
+
return parseChangedFiles(statusOutput).filter(file => shouldReviewFile(file.path));
|
|
632
|
+
}
|
|
633
|
+
async function stageReviewedFiles(reviewRecords) {
|
|
634
|
+
const reviewedFiles = collectReviewedFiles(reviewRecords);
|
|
635
|
+
const currentChanges = await getReviewableGitChanges();
|
|
636
|
+
const stagedFiles = [];
|
|
637
|
+
const unreviewedFiles = [];
|
|
638
|
+
for (const file of currentChanges) {
|
|
639
|
+
const normalizedPath = normalizeGitPath(file.path);
|
|
640
|
+
if (reviewedFiles.has(normalizedPath)) {
|
|
641
|
+
stagedFiles.push(file.path);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
unreviewedFiles.push(file.path);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (unreviewedFiles.length > 0) {
|
|
648
|
+
return { stagedFiles: [], unreviewedFiles };
|
|
649
|
+
}
|
|
650
|
+
if (stagedFiles.length > 0) {
|
|
651
|
+
const gitAdd = await runGit(['add', '--', ...stagedFiles]);
|
|
652
|
+
if (gitAdd.exitCode !== 0) {
|
|
653
|
+
throw new Error(gitAdd.stderr || 'git add failed');
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return { stagedFiles, unreviewedFiles: [] };
|
|
657
|
+
}
|
|
658
|
+
// REVIEW Phase - KarpathyEvaluator + deterministic review evidence
|
|
413
659
|
export const phaseReview = defineCommand({
|
|
414
|
-
meta: { name: 'review', description: 'REVIEW: Code review (/review)' },
|
|
660
|
+
meta: { name: 'review', description: 'REVIEW: Code review with Karpathy Principles (/review)' },
|
|
415
661
|
args: {
|
|
416
662
|
'task-id': { type: 'positional', required: false },
|
|
417
663
|
'check-security': { type: 'boolean', default: true },
|
|
@@ -419,100 +665,123 @@ export const phaseReview = defineCommand({
|
|
|
419
665
|
json: { type: 'boolean', default: false },
|
|
420
666
|
},
|
|
421
667
|
async run({ args }) {
|
|
422
|
-
const { store } = getEngine();
|
|
668
|
+
const { store, workflowEngine } = getEngine();
|
|
669
|
+
const reviewStore = new ReviewStore(SCALE_DIR);
|
|
423
670
|
// If task-id provided, validate task exists
|
|
424
671
|
let task = null;
|
|
672
|
+
let taskPayload;
|
|
425
673
|
if (args['task-id']) {
|
|
426
674
|
task = await store.get(args['task-id']);
|
|
427
675
|
if (!task || task.type !== 'Task') {
|
|
428
|
-
console.error(`\
|
|
676
|
+
console.error(`\nTask not found: ${args['task-id']}\n`);
|
|
429
677
|
process.exit(1);
|
|
430
678
|
}
|
|
679
|
+
taskPayload = task.payload;
|
|
680
|
+
}
|
|
681
|
+
// === WorkflowEngine Integration ===
|
|
682
|
+
// Step 1: Karpathy Principles Check
|
|
683
|
+
const karpathyResult = workflowEngine.checkKarpathy({
|
|
684
|
+
hypothesesListed: true, // Would be determined from actual context
|
|
685
|
+
hasExtraFeatures: false, // Would be determined from actual context
|
|
686
|
+
changesTraceable: true, // Would be determined from actual context
|
|
687
|
+
hasVerifiableGoal: true // Would be determined from actual context
|
|
688
|
+
});
|
|
689
|
+
if (!args.json) {
|
|
690
|
+
console.log('\nKarpathy Principles Check:');
|
|
691
|
+
console.log(workflowEngine.getKarpathyEvaluator().formatReport());
|
|
692
|
+
}
|
|
693
|
+
const review = await reviewGitChanges(taskPayload);
|
|
694
|
+
const findings = review.findings;
|
|
695
|
+
const summary = summarizeFindings(findings);
|
|
696
|
+
const passed = summary.critical === 0 && summary.high === 0;
|
|
697
|
+
const record = reviewStore.saveReview({
|
|
698
|
+
taskId: args['task-id'],
|
|
699
|
+
passed,
|
|
700
|
+
findings,
|
|
701
|
+
changedFiles: review.changedFiles.map(file => file.path),
|
|
702
|
+
summary,
|
|
703
|
+
});
|
|
704
|
+
if (task && taskPayload) {
|
|
705
|
+
const updatedPayload = {
|
|
706
|
+
...taskPayload,
|
|
707
|
+
reviewPassed: passed,
|
|
708
|
+
reviewEvidenceIds: [...(taskPayload.reviewEvidenceIds ?? []), record.id],
|
|
709
|
+
reviewedAt: Date.now(),
|
|
710
|
+
};
|
|
711
|
+
await store.update(task.id, { payload: updatedPayload });
|
|
431
712
|
}
|
|
432
|
-
const findings = [];
|
|
433
|
-
// Style checklist
|
|
434
|
-
if (args['check-style']) {
|
|
435
|
-
findings.push({ category: 'style', severity: 'MEDIUM', description: 'Check function length (<50 lines)' });
|
|
436
|
-
findings.push({ category: 'style', severity: 'MEDIUM', description: 'Check file size (<800 lines)' });
|
|
437
|
-
findings.push({ category: 'style', severity: 'LOW', description: 'Check naming conventions' });
|
|
438
|
-
findings.push({ category: 'style', severity: 'LOW', description: 'Check no deep nesting (>4 levels)' });
|
|
439
|
-
}
|
|
440
|
-
// Security checklist
|
|
441
|
-
if (args['check-security']) {
|
|
442
|
-
findings.push({ category: 'security', severity: 'CRITICAL', description: 'No hardcoded secrets/credentials' });
|
|
443
|
-
findings.push({ category: 'security', severity: 'HIGH', description: 'Input validation at boundaries' });
|
|
444
|
-
findings.push({ category: 'security', severity: 'HIGH', description: 'No SQL injection (parameterized queries)' });
|
|
445
|
-
findings.push({ category: 'security', severity: 'HIGH', description: 'No XSS (sanitized output)' });
|
|
446
|
-
findings.push({ category: 'security', severity: 'MEDIUM', description: 'Error messages not leak sensitive data' });
|
|
447
|
-
}
|
|
448
|
-
// Logic checklist
|
|
449
|
-
findings.push({ category: 'logic', severity: 'HIGH', description: 'Edge cases handled' });
|
|
450
|
-
findings.push({ category: 'logic', severity: 'HIGH', description: 'Error handling explicit' });
|
|
451
|
-
findings.push({ category: 'logic', severity: 'MEDIUM', description: 'No N+1 queries' });
|
|
452
|
-
// Performance checklist
|
|
453
|
-
findings.push({ category: 'performance', severity: 'MEDIUM', description: 'No unbounded loops/queries' });
|
|
454
|
-
findings.push({ category: 'performance', severity: 'LOW', description: 'Caching where appropriate' });
|
|
455
|
-
// Count by severity
|
|
456
|
-
const critical = findings.filter(f => f.severity === 'CRITICAL').length;
|
|
457
|
-
const high = findings.filter(f => f.severity === 'HIGH').length;
|
|
458
|
-
const medium = findings.filter(f => f.severity === 'MEDIUM').length;
|
|
459
|
-
const low = findings.filter(f => f.severity === 'LOW').length;
|
|
460
|
-
const passed = critical === 0; // Only block on CRITICAL issues
|
|
461
713
|
const result = {
|
|
462
714
|
phase: 'REVIEW',
|
|
463
715
|
taskId: args['task-id'],
|
|
716
|
+
reviewId: record.id,
|
|
464
717
|
findings,
|
|
465
|
-
|
|
718
|
+
changedFiles: review.changedFiles.map(file => file.path),
|
|
719
|
+
summary,
|
|
466
720
|
passed,
|
|
467
721
|
recommendation: passed ? 'Ready to ship' : 'Fix CRITICAL issues before shipping'
|
|
468
722
|
};
|
|
469
723
|
if (args.json)
|
|
470
724
|
console.log(JSON.stringify(result, null, 2));
|
|
471
725
|
else {
|
|
472
|
-
console.log('\
|
|
473
|
-
console.log(
|
|
474
|
-
console.log('
|
|
475
|
-
console.log(
|
|
476
|
-
console.log(
|
|
477
|
-
console.log(
|
|
478
|
-
console.log(
|
|
479
|
-
console.log(
|
|
726
|
+
console.log('\nREVIEW Phase');
|
|
727
|
+
console.log(`\nReview evidence: ${record.id}`);
|
|
728
|
+
console.log('\nReview Findings:');
|
|
729
|
+
console.log('----------------------------------------');
|
|
730
|
+
console.log(`CRITICAL: ${summary.critical} issues ${summary.critical > 0 ? 'BLOCKED' : 'OK'}`);
|
|
731
|
+
console.log(`HIGH: ${summary.high} issues ${summary.high > 0 ? 'BLOCKED' : 'OK'}`);
|
|
732
|
+
console.log(`MEDIUM: ${summary.medium} issues`);
|
|
733
|
+
console.log(`LOW: ${summary.low} issues`);
|
|
734
|
+
console.log('----------------------------------------');
|
|
735
|
+
findings.slice(0, 10).forEach(f => console.log(` [${f.severity}] ${f.file ? `${f.file}: ` : ''}${f.description}`));
|
|
480
736
|
if (passed) {
|
|
481
|
-
console.log('\
|
|
737
|
+
console.log('\nReview passed (no CRITICAL issues)');
|
|
482
738
|
console.log('\n Next: scale ship ' + (args['task-id'] ?? '<task-id>') + '\n');
|
|
483
739
|
}
|
|
484
740
|
else {
|
|
485
|
-
console.log('\
|
|
741
|
+
console.log('\nReview blocked by CRITICAL issues');
|
|
486
742
|
console.log('\n Fix critical issues, then: scale review\n');
|
|
487
743
|
}
|
|
488
744
|
}
|
|
489
745
|
},
|
|
490
746
|
});
|
|
491
|
-
// SHIP Phase
|
|
747
|
+
// SHIP Phase - HonestDelivery
|
|
492
748
|
export const phaseShip = defineCommand({
|
|
493
|
-
meta: { name: 'ship', description: 'SHIP: Commit
|
|
749
|
+
meta: { name: 'ship', description: 'SHIP: Commit with HonestDelivery Report (/ship)' },
|
|
494
750
|
args: {
|
|
495
751
|
'task-id': { type: 'positional', required: true },
|
|
496
752
|
message: { type: 'string', alias: 'm', description: 'Commit message' },
|
|
497
753
|
'no-commit': { type: 'boolean', default: false, description: 'Skip git commit' },
|
|
754
|
+
'skip-commit': { type: 'boolean', default: false, description: 'Skip git commit' },
|
|
498
755
|
json: { type: 'boolean', default: false },
|
|
499
756
|
},
|
|
500
757
|
async run({ args }) {
|
|
501
|
-
const { store, fsm } = getEngine();
|
|
758
|
+
const { store, fsm, workflowEngine } = getEngine();
|
|
502
759
|
// Validate task exists
|
|
503
760
|
const task = await store.get(args['task-id']);
|
|
504
761
|
if (!task || task.type !== 'Task') {
|
|
505
|
-
console.error(`\
|
|
762
|
+
console.error(`\nTask not found: ${args['task-id']}\n`);
|
|
506
763
|
process.exit(1);
|
|
507
764
|
}
|
|
508
765
|
// Check if task is completed (or attempt transition)
|
|
509
766
|
const payload = task.payload;
|
|
767
|
+
const evidenceValidation = validateVerificationEvidence(payload.verificationEvidenceIds);
|
|
768
|
+
const reviewValidation = validateReviewEvidence(payload.reviewEvidenceIds);
|
|
510
769
|
const verificationPassed = payload.buildStatus === 'success' &&
|
|
770
|
+
(payload.buildExitCode ?? 1) === 0 &&
|
|
511
771
|
payload.lintStatus === 'success' &&
|
|
512
|
-
payload.testPassed === true
|
|
772
|
+
payload.testPassed === true &&
|
|
773
|
+
(payload.testCoverage ?? 0) >= 80 &&
|
|
774
|
+
evidenceValidation.ok;
|
|
775
|
+
const reviewPassed = payload.reviewPassed === true && reviewValidation.ok;
|
|
513
776
|
if (task.status !== 'COMPLETED') {
|
|
514
777
|
if (!verificationPassed) {
|
|
515
|
-
console.error('\
|
|
778
|
+
console.error('\nTask not verified with persisted evidence. Run: scale verify ' + args['task-id'] + '\n');
|
|
779
|
+
if (evidenceValidation.missing.length > 0) {
|
|
780
|
+
console.error('Missing evidence records: ' + evidenceValidation.missing.join(', '));
|
|
781
|
+
}
|
|
782
|
+
if (evidenceValidation.failed.length > 0) {
|
|
783
|
+
console.error('Failed evidence records: ' + evidenceValidation.failed.join(', '));
|
|
784
|
+
}
|
|
516
785
|
process.exit(1);
|
|
517
786
|
}
|
|
518
787
|
// Attempt FSM transition
|
|
@@ -523,25 +792,55 @@ export const phaseShip = defineCommand({
|
|
|
523
792
|
}
|
|
524
793
|
catch (e) {
|
|
525
794
|
const error = e;
|
|
526
|
-
console.error('\
|
|
795
|
+
console.error('\nFSM transition failed:', error.message);
|
|
527
796
|
console.log('\n Run verification first: scale verify ' + args['task-id'] + '\n');
|
|
528
797
|
process.exit(1);
|
|
529
798
|
}
|
|
530
799
|
}
|
|
800
|
+
if (!reviewPassed) {
|
|
801
|
+
console.error('\nTask not reviewed with persisted passing evidence. Run: scale review ' + args['task-id'] + '\n');
|
|
802
|
+
if (reviewValidation.missing.length > 0) {
|
|
803
|
+
console.error('Missing review records: ' + reviewValidation.missing.join(', '));
|
|
804
|
+
}
|
|
805
|
+
if (reviewValidation.failed.length > 0) {
|
|
806
|
+
console.error('Failed review records: ' + reviewValidation.failed.join(', '));
|
|
807
|
+
}
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
531
810
|
// Git operations
|
|
532
811
|
let commitHash = null;
|
|
533
|
-
|
|
534
|
-
|
|
812
|
+
let stagedFiles = [];
|
|
813
|
+
if (!shouldSkipCommit(args['skip-commit'])) {
|
|
535
814
|
const commitMessage = args.message ?? `feat: ${task.title ?? args['task-id']}`;
|
|
536
815
|
try {
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
|
|
816
|
+
const reviewRecords = getValidatedReviewRecords(payload.reviewEvidenceIds);
|
|
817
|
+
const stageResult = await stageReviewedFiles(reviewRecords);
|
|
818
|
+
if (stageResult.unreviewedFiles.length > 0) {
|
|
819
|
+
console.error('\nUnreviewed working tree changes detected. Re-run scale review before shipping.');
|
|
820
|
+
stageResult.unreviewedFiles.forEach(file => console.error(' - ' + file));
|
|
821
|
+
console.error('\nUse scale ship ' + args['task-id'] + ' --no-commit to generate the delivery report without committing.\n');
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
stagedFiles = stageResult.stagedFiles;
|
|
825
|
+
const result = await runGit(['commit', '-m', commitMessage]);
|
|
826
|
+
if (result.exitCode !== 0) {
|
|
827
|
+
const message = result.stderr || result.stdout || 'git commit failed';
|
|
828
|
+
if (/nothing to commit|no changes added/i.test(message)) {
|
|
829
|
+
if (!args.json)
|
|
830
|
+
console.log(' Git commit skipped: nothing to commit');
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
throw new Error(message);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
commitHash = result.stdout.split('\n')[0]; // First line contains hash
|
|
838
|
+
}
|
|
540
839
|
}
|
|
541
840
|
catch (e) {
|
|
542
841
|
const error = e;
|
|
543
|
-
|
|
544
|
-
|
|
842
|
+
console.error('\nGit commit failed:', error.message);
|
|
843
|
+
process.exit(1);
|
|
545
844
|
}
|
|
546
845
|
}
|
|
547
846
|
// Update Plan to DONE if Task completed
|
|
@@ -552,20 +851,62 @@ export const phaseShip = defineCommand({
|
|
|
552
851
|
}
|
|
553
852
|
catch { }
|
|
554
853
|
}
|
|
854
|
+
// === WorkflowEngine Integration ===
|
|
855
|
+
// Generate HonestDelivery report
|
|
856
|
+
if (!args.json) {
|
|
857
|
+
console.log('\nHonest Delivery Report:');
|
|
858
|
+
console.log('-'.repeat(40));
|
|
859
|
+
console.log(`[COMPLETED]`);
|
|
860
|
+
console.log(` - Task: ${args['task-id']}`);
|
|
861
|
+
console.log(` - Status: COMPLETED`);
|
|
862
|
+
if (commitHash)
|
|
863
|
+
console.log(` - Commit: ${commitHash}`);
|
|
864
|
+
if (stagedFiles.length)
|
|
865
|
+
console.log(` - Files committed: ${stagedFiles.length}`);
|
|
866
|
+
console.log('');
|
|
867
|
+
console.log(`[VERIFIED]`);
|
|
868
|
+
console.log(' [PASS] Build: passed');
|
|
869
|
+
console.log(' [PASS] Lint: passed');
|
|
870
|
+
console.log(' [PASS] Tests: passed');
|
|
871
|
+
if (payload.testCoverage)
|
|
872
|
+
console.log(` [PASS] Coverage: ${payload.testCoverage}%`);
|
|
873
|
+
if (payload.verificationEvidenceIds?.length) {
|
|
874
|
+
console.log(` [PASS] Evidence records validated: ${payload.verificationEvidenceIds.join(', ')}`);
|
|
875
|
+
}
|
|
876
|
+
if (payload.reviewEvidenceIds?.length) {
|
|
877
|
+
console.log(` [PASS] Review records validated: ${payload.reviewEvidenceIds.join(', ')}`);
|
|
878
|
+
}
|
|
879
|
+
console.log('');
|
|
880
|
+
// Check for unverified items
|
|
881
|
+
const unverifiedItems = [];
|
|
882
|
+
if (!payload.testCoverage || payload.testCoverage < 80) {
|
|
883
|
+
unverifiedItems.push('Coverage below 80%');
|
|
884
|
+
}
|
|
885
|
+
if (unverifiedItems.length > 0) {
|
|
886
|
+
console.log(`[UNVERIFIED]`);
|
|
887
|
+
unverifiedItems.forEach(item => console.log(` [UNVERIFIED] ${item}`));
|
|
888
|
+
console.log('');
|
|
889
|
+
}
|
|
890
|
+
}
|
|
555
891
|
const result = {
|
|
556
892
|
phase: 'SHIP',
|
|
557
893
|
taskId: args['task-id'],
|
|
558
894
|
status: 'COMPLETED',
|
|
895
|
+
verificationEvidenceIds: payload.verificationEvidenceIds ?? [],
|
|
896
|
+
evidenceValidation,
|
|
897
|
+
reviewEvidenceIds: payload.reviewEvidenceIds ?? [],
|
|
898
|
+
reviewValidation,
|
|
559
899
|
commitHash,
|
|
900
|
+
stagedFiles,
|
|
560
901
|
};
|
|
561
902
|
if (args.json)
|
|
562
903
|
console.log(JSON.stringify(result, null, 2));
|
|
563
904
|
else {
|
|
564
|
-
console.log('\
|
|
565
|
-
console.log('\
|
|
905
|
+
console.log('\nSHIP Phase');
|
|
906
|
+
console.log('\nTask COMPLETED: ' + args['task-id']);
|
|
566
907
|
if (commitHash)
|
|
567
908
|
console.log(' Commit: ' + commitHash);
|
|
568
|
-
console.log('\
|
|
909
|
+
console.log('\nDone. Feature shipped.\n');
|
|
569
910
|
}
|
|
570
911
|
},
|
|
571
912
|
});
|