@hongmaple0820/scale-engine 0.12.3 → 0.14.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/dist/adapters/AiderAdapter.js +1 -1
- package/dist/adapters/AiderAdapter.js.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +1 -0
- package/dist/adapters/ClaudeCodeAdapter.js +5 -3
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.js +1 -1
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/CursorAdapter.js +1 -1
- package/dist/adapters/CursorAdapter.js.map +1 -1
- package/dist/adapters/DeepSeekTuiAdapter.js +5 -3
- package/dist/adapters/DeepSeekTuiAdapter.js.map +1 -1
- package/dist/adapters/DoubaoAdapter.js +1 -1
- package/dist/adapters/DoubaoAdapter.js.map +1 -1
- package/dist/adapters/GeminiAdapter.js +1 -1
- package/dist/adapters/GeminiAdapter.js.map +1 -1
- package/dist/adapters/HermesAdapter.js +1 -1
- package/dist/adapters/HermesAdapter.js.map +1 -1
- package/dist/adapters/KimiAdapter.js +1 -1
- package/dist/adapters/KimiAdapter.js.map +1 -1
- package/dist/adapters/KiroAdapter.d.ts +14 -0
- package/dist/adapters/KiroAdapter.js +180 -0
- package/dist/adapters/KiroAdapter.js.map +1 -0
- package/dist/adapters/OpenClawAdapter.js +1 -1
- package/dist/adapters/OpenClawAdapter.js.map +1 -1
- package/dist/adapters/OpenCodeAdapter.js +1 -1
- package/dist/adapters/OpenCodeAdapter.js.map +1 -1
- package/dist/adapters/QCoderAdapter.js +1 -1
- package/dist/adapters/QCoderAdapter.js.map +1 -1
- package/dist/adapters/TraeAdapter.js +1 -1
- package/dist/adapters/TraeAdapter.js.map +1 -1
- package/dist/adapters/VSCAdapter.js +1 -1
- package/dist/adapters/VSCAdapter.js.map +1 -1
- package/dist/adapters/WindsurfAdapter.js +1 -1
- package/dist/adapters/WindsurfAdapter.js.map +1 -1
- package/dist/adapters/WorkBuddyAdapter.js +1 -1
- package/dist/adapters/WorkBuddyAdapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/api/cli.js +690 -9
- package/dist/api/cli.js.map +1 -1
- package/dist/api/doctor.d.ts +13 -0
- package/dist/api/doctor.js +261 -5
- package/dist/api/doctor.js.map +1 -1
- package/dist/api/quickstart.d.ts +20 -1
- package/dist/api/quickstart.js +104 -3
- package/dist/api/quickstart.js.map +1 -1
- package/dist/artifact/types.d.ts +16 -2
- package/dist/artifact/types.js.map +1 -1
- package/dist/cli/phaseCommands.d.ts +66 -0
- package/dist/cli/phaseCommands.js +695 -51
- package/dist/cli/phaseCommands.js.map +1 -1
- package/dist/guardrails/detectors.d.ts +9 -0
- package/dist/guardrails/detectors.js +102 -0
- package/dist/guardrails/detectors.js.map +1 -1
- package/dist/hooks/HookGeneratorEnhanced.js +29 -0
- package/dist/hooks/HookGeneratorEnhanced.js.map +1 -1
- package/dist/hooks/WorkflowHooksManager.js +20 -1
- package/dist/hooks/WorkflowHooksManager.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/output/BrandThemeLoader.d.ts +54 -0
- package/dist/output/BrandThemeLoader.js +340 -0
- package/dist/output/BrandThemeLoader.js.map +1 -0
- package/dist/output/HTMLDocumentRenderer.d.ts +83 -0
- package/dist/output/HTMLDocumentRenderer.js +717 -0
- package/dist/output/HTMLDocumentRenderer.js.map +1 -0
- package/dist/output/UIPrototypeRenderer.d.ts +61 -0
- package/dist/output/UIPrototypeRenderer.js +500 -0
- package/dist/output/UIPrototypeRenderer.js.map +1 -0
- package/dist/output/index.d.ts +6 -0
- package/dist/output/index.js +6 -0
- package/dist/output/index.js.map +1 -0
- package/dist/skills/ExternalSkills.js +2 -0
- package/dist/skills/ExternalSkills.js.map +1 -1
- package/dist/skills/SkillCatalog.d.ts +13 -0
- package/dist/skills/SkillCatalog.js +184 -0
- package/dist/skills/SkillCatalog.js.map +1 -0
- package/dist/skills/SkillDiscovery.js +2 -1
- package/dist/skills/SkillDiscovery.js.map +1 -1
- package/dist/skills/SkillDoctor.d.ts +37 -0
- package/dist/skills/SkillDoctor.js +90 -0
- package/dist/skills/SkillDoctor.js.map +1 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/routing/SkillGate.d.ts +12 -0
- package/dist/skills/routing/SkillGate.js +93 -0
- package/dist/skills/routing/SkillGate.js.map +1 -0
- package/dist/skills/routing/SkillPlanner.d.ts +8 -0
- package/dist/skills/routing/SkillPlanner.js +91 -0
- package/dist/skills/routing/SkillPlanner.js.map +1 -0
- package/dist/skills/routing/SkillPolicy.d.ts +6 -0
- package/dist/skills/routing/SkillPolicy.js +173 -0
- package/dist/skills/routing/SkillPolicy.js.map +1 -0
- package/dist/skills/routing/SkillRoutingTypes.d.ts +72 -0
- package/dist/skills/routing/SkillRoutingTypes.js +2 -0
- package/dist/skills/routing/SkillRoutingTypes.js.map +1 -0
- package/dist/skills/routing/TaskIntentClassifier.d.ts +6 -0
- package/dist/skills/routing/TaskIntentClassifier.js +79 -0
- package/dist/skills/routing/TaskIntentClassifier.js.map +1 -0
- package/dist/skills/routing/index.d.ts +5 -0
- package/dist/skills/routing/index.js +6 -0
- package/dist/skills/routing/index.js.map +1 -0
- package/dist/workflow/GovernanceLock.d.ts +35 -0
- package/dist/workflow/GovernanceLock.js +58 -0
- package/dist/workflow/GovernanceLock.js.map +1 -0
- package/dist/workflow/GovernanceTemplatePacks.d.ts +24 -0
- package/dist/workflow/GovernanceTemplatePacks.js +83 -0
- package/dist/workflow/GovernanceTemplatePacks.js.map +1 -0
- package/dist/workflow/GovernanceTemplates.d.ts +17 -0
- package/dist/workflow/GovernanceTemplates.js +686 -0
- package/dist/workflow/GovernanceTemplates.js.map +1 -0
- package/dist/workflow/PhaseMarkerTracker.d.ts +63 -0
- package/dist/workflow/PhaseMarkerTracker.js +291 -0
- package/dist/workflow/PhaseMarkerTracker.js.map +1 -0
- package/dist/workflow/SessionStateTracker.d.ts +74 -0
- package/dist/workflow/SessionStateTracker.js +270 -0
- package/dist/workflow/SessionStateTracker.js.map +1 -0
- package/dist/workflow/TaskArtifactScaffolder.d.ts +47 -0
- package/dist/workflow/TaskArtifactScaffolder.js +237 -0
- package/dist/workflow/TaskArtifactScaffolder.js.map +1 -0
- package/dist/workflow/TaskMetricsStore.d.ts +49 -0
- package/dist/workflow/TaskMetricsStore.js +149 -0
- package/dist/workflow/TaskMetricsStore.js.map +1 -0
- package/dist/workflow/VerificationCommands.d.ts +2 -0
- package/dist/workflow/VerificationCommands.js +8 -12
- package/dist/workflow/VerificationCommands.js.map +1 -1
- package/dist/workflow/VerificationProfile.d.ts +56 -0
- package/dist/workflow/VerificationProfile.js +170 -0
- package/dist/workflow/VerificationProfile.js.map +1 -0
- package/dist/workflow/WorkflowArtifactWriter.d.ts +113 -0
- package/dist/workflow/WorkflowArtifactWriter.js +241 -0
- package/dist/workflow/WorkflowArtifactWriter.js.map +1 -0
- package/dist/workflow/WorkflowEngine.d.ts +26 -5
- package/dist/workflow/WorkflowEngine.js +38 -9
- package/dist/workflow/WorkflowEngine.js.map +1 -1
- package/dist/workflow/WorkspaceLifecycle.d.ts +51 -0
- package/dist/workflow/WorkspaceLifecycle.js +259 -0
- package/dist/workflow/WorkspaceLifecycle.js.map +1 -0
- package/dist/workflow/autonomous/AutonomousDevLoop.d.ts +88 -0
- package/dist/workflow/autonomous/AutonomousDevLoop.js +381 -0
- package/dist/workflow/autonomous/AutonomousDevLoop.js.map +1 -0
- package/dist/workflow/autonomous/WorklogManager.d.ts +50 -0
- package/dist/workflow/autonomous/WorklogManager.js +264 -0
- package/dist/workflow/autonomous/WorklogManager.js.map +1 -0
- package/dist/workflow/autonomous/index.d.ts +2 -0
- package/dist/workflow/autonomous/index.js +4 -0
- package/dist/workflow/autonomous/index.js.map +1 -0
- package/dist/workflow/gates/GateSystem.d.ts +12 -3
- package/dist/workflow/gates/GateSystem.js +185 -41
- package/dist/workflow/gates/GateSystem.js.map +1 -1
- package/dist/workflow/index.d.ts +10 -0
- package/dist/workflow/index.js +10 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -9,13 +9,23 @@ import { FSM } from '../artifact/fsm.js';
|
|
|
9
9
|
import { registerAllFSMs } from '../artifact/fsmDefinitions.js';
|
|
10
10
|
import { CapabilityRegistry } from '../capabilities/CapabilityRegistry.js';
|
|
11
11
|
import { SkillRegistry } from '../skills/SkillRegistry.js';
|
|
12
|
+
import { registerCoreSkills } from '../skills/coreSkills.js';
|
|
13
|
+
import { registerExternalSkills } from '../skills/ExternalSkills.js';
|
|
14
|
+
import { createSkillPlan, evaluateSkillGate, loadSkillRoutingPolicy } from '../skills/routing/index.js';
|
|
15
|
+
import { inspectRequiredWorkflowSkills } from '../skills/SkillDoctor.js';
|
|
12
16
|
import { WorkflowEngine } from '../workflow/WorkflowEngine.js';
|
|
17
|
+
import { WorkflowArtifactWriter } from '../workflow/WorkflowArtifactWriter.js';
|
|
18
|
+
import { resolveVerificationTargets } from '../workflow/VerificationProfile.js';
|
|
13
19
|
import { EvidenceStore } from '../workflow/EvidenceStore.js';
|
|
14
20
|
import { ReviewStore } from '../workflow/ReviewStore.js';
|
|
21
|
+
import { TaskMetricsStore } from '../workflow/TaskMetricsStore.js';
|
|
22
|
+
import { appendVerificationArtifact, checkTaskArtifactCompleteness, scaffoldTaskArtifacts } from '../workflow/TaskArtifactScaffolder.js';
|
|
15
23
|
import { analyzeReview, parseChangedFiles, shouldReviewFile, summarizeFindings } from '../workflow/ReviewAnalyzer.js';
|
|
16
24
|
import { join } from 'node:path';
|
|
17
25
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { HTMLDocumentRenderer } from '../output/HTMLDocumentRenderer.js';
|
|
18
27
|
const SCALE_DIR = process.env.SCALE_DIR ?? '.scale';
|
|
28
|
+
const PROJECT_DIR = process.env.SCALE_PROJECT_DIR ?? process.cwd();
|
|
19
29
|
function validateVerificationEvidence(ids) {
|
|
20
30
|
const evidenceStore = new EvidenceStore(SCALE_DIR);
|
|
21
31
|
const missing = [];
|
|
@@ -72,11 +82,14 @@ function getEngine() {
|
|
|
72
82
|
const capabilityRegistry = new CapabilityRegistry(eventBus);
|
|
73
83
|
// Initialize skill registry
|
|
74
84
|
const skillRegistry = new SkillRegistry(eventBus);
|
|
85
|
+
registerCoreSkills(skillRegistry);
|
|
86
|
+
registerExternalSkills(skillRegistry, eventBus);
|
|
75
87
|
// Initialize workflow engine with cognitive scaffolding and quality gates.
|
|
76
88
|
const workflowEngine = new WorkflowEngine({
|
|
77
89
|
eventBus,
|
|
78
90
|
capabilityRegistry,
|
|
79
|
-
skillRegistry
|
|
91
|
+
skillRegistry,
|
|
92
|
+
scaleDir: SCALE_DIR,
|
|
80
93
|
});
|
|
81
94
|
return { eventBus, store, fsm, workflowEngine, skillRegistry };
|
|
82
95
|
}
|
|
@@ -93,6 +106,127 @@ function shouldSkipCommit(value) {
|
|
|
93
106
|
function normalizeGitPath(path) {
|
|
94
107
|
return path.replace(/\\/g, '/');
|
|
95
108
|
}
|
|
109
|
+
function normalizeWorkflowLevel(value) {
|
|
110
|
+
const normalized = String(value ?? 'M').trim().toUpperCase();
|
|
111
|
+
if (normalized === 'S' || normalized === 'M' || normalized === 'L' || normalized === 'CRITICAL') {
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Invalid workflow level "${String(value)}"; expected S, M, L, or CRITICAL.`);
|
|
115
|
+
}
|
|
116
|
+
function metricLevelFromPayload(payload) {
|
|
117
|
+
const level = normalizeWorkflowLevel(payload.workflowLevel ?? 'M');
|
|
118
|
+
return level === 'S' ? null : level;
|
|
119
|
+
}
|
|
120
|
+
function normalizeServices(value) {
|
|
121
|
+
if (!value)
|
|
122
|
+
return [];
|
|
123
|
+
return String(value)
|
|
124
|
+
.split(',')
|
|
125
|
+
.map(service => service.trim())
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
}
|
|
128
|
+
function isWorkflowGeneratedArtifact(path) {
|
|
129
|
+
return path.replace(/\\/g, '/').startsWith('docs/worklog/tasks/');
|
|
130
|
+
}
|
|
131
|
+
function checkCurrentTaskArtifacts(level) {
|
|
132
|
+
const state = new WorkflowArtifactWriter(SCALE_DIR).readCurrentState();
|
|
133
|
+
return checkTaskArtifactCompleteness({
|
|
134
|
+
projectDir: PROJECT_DIR,
|
|
135
|
+
artifactsDir: state?.artifactsDir,
|
|
136
|
+
level,
|
|
137
|
+
skillRequiredArtifacts: state?.requiredSkillArtifacts,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function planSkillsForTask(options) {
|
|
141
|
+
return createSkillPlan({
|
|
142
|
+
taskId: options.taskId,
|
|
143
|
+
taskName: options.taskName,
|
|
144
|
+
description: options.description,
|
|
145
|
+
level: options.level,
|
|
146
|
+
services: options.services ?? [],
|
|
147
|
+
files: options.files ?? [],
|
|
148
|
+
policy: loadSkillRoutingPolicy(PROJECT_DIR, SCALE_DIR),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function normalizeArtifactGateMode(value) {
|
|
152
|
+
if (value === undefined || value === null || value === '')
|
|
153
|
+
return undefined;
|
|
154
|
+
const normalized = String(value).trim().toLowerCase();
|
|
155
|
+
if (normalized === 'off' || normalized === 'warn' || normalized === 'block')
|
|
156
|
+
return normalized;
|
|
157
|
+
throw new Error(`Invalid artifact gate mode "${String(value)}"; expected off, warn, or block.`);
|
|
158
|
+
}
|
|
159
|
+
function artifactGateLevels(policy) {
|
|
160
|
+
return policy.artifactGateLevels?.length ? policy.artifactGateLevels : ['M', 'L', 'CRITICAL'];
|
|
161
|
+
}
|
|
162
|
+
function assumeVerificationArtifactWillBeWritten(check) {
|
|
163
|
+
if (!check.artifactsDir)
|
|
164
|
+
return check;
|
|
165
|
+
const missing = check.missing.filter(file => file !== 'verification.md');
|
|
166
|
+
const incomplete = check.incomplete.filter(item => item.file !== 'verification.md');
|
|
167
|
+
return {
|
|
168
|
+
...check,
|
|
169
|
+
missing,
|
|
170
|
+
incomplete,
|
|
171
|
+
complete: missing.length === 0 && incomplete.length === 0,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function evaluateArtifactGate(options) {
|
|
175
|
+
const mode = isTruthyFlag(options.requireArtifacts)
|
|
176
|
+
? 'block'
|
|
177
|
+
: normalizeArtifactGateMode(options.cliMode) ?? options.policy.artifactGate ?? 'warn';
|
|
178
|
+
const levels = artifactGateLevels(options.policy);
|
|
179
|
+
const applies = Boolean(options.level && levels.includes(options.level));
|
|
180
|
+
const checked = applies && mode !== 'off' && Boolean(options.check);
|
|
181
|
+
const complete = checked ? options.check?.complete : undefined;
|
|
182
|
+
return {
|
|
183
|
+
mode,
|
|
184
|
+
levels,
|
|
185
|
+
applies,
|
|
186
|
+
checked,
|
|
187
|
+
complete,
|
|
188
|
+
blocked: mode === 'block' && checked && complete === false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function countChangedFiles(taskPayload) {
|
|
192
|
+
const filesInvolved = taskPayload.filesInvolved ?? [];
|
|
193
|
+
if (filesInvolved.length > 0)
|
|
194
|
+
return new Set(filesInvolved.map(normalizeGitPath)).size;
|
|
195
|
+
return (await detectTaskChangedFiles()).length;
|
|
196
|
+
}
|
|
197
|
+
async function detectTaskChangedFiles() {
|
|
198
|
+
try {
|
|
199
|
+
return (await getReviewableGitChanges())
|
|
200
|
+
.filter(file => !isWorkflowGeneratedArtifact(file.path))
|
|
201
|
+
.map(file => normalizeGitPath(file.path));
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function recordVerificationMetric(options) {
|
|
208
|
+
const level = metricLevelFromPayload(options.taskPayload);
|
|
209
|
+
if (!level)
|
|
210
|
+
return null;
|
|
211
|
+
const services = options.taskPayload.servicesTouched?.length
|
|
212
|
+
? options.taskPayload.servicesTouched
|
|
213
|
+
: options.serviceNames ?? [];
|
|
214
|
+
const metricsStore = new TaskMetricsStore(SCALE_DIR);
|
|
215
|
+
const artifactCheck = options.artifactCheck ?? checkCurrentTaskArtifacts(level);
|
|
216
|
+
const record = metricsStore.recordVerification({
|
|
217
|
+
taskId: options.taskId,
|
|
218
|
+
taskName: options.taskName,
|
|
219
|
+
level,
|
|
220
|
+
services,
|
|
221
|
+
filesChanged: await countChangedFiles(options.taskPayload),
|
|
222
|
+
passed: options.passed,
|
|
223
|
+
artifactComplete: artifactCheck.complete,
|
|
224
|
+
residualRisk: options.taskPayload.residualRisk,
|
|
225
|
+
finalGateStatus: options.finalGateStatus,
|
|
226
|
+
});
|
|
227
|
+
metricsStore.writeMarkdownReport(PROJECT_DIR);
|
|
228
|
+
return record;
|
|
229
|
+
}
|
|
96
230
|
// Helper: Generate spec markdown file
|
|
97
231
|
function generateSpecMarkdown(id, title, payload) {
|
|
98
232
|
return `# Spec: ${title}
|
|
@@ -146,6 +280,8 @@ export const phaseDefine = defineCommand({
|
|
|
146
280
|
'context': { type: 'string', description: 'Context answer for Socratic refinement' },
|
|
147
281
|
'risk': { type: 'string', description: 'Risk answer for Socratic refinement' },
|
|
148
282
|
'priority': { type: 'string', description: 'Priority answer for Socratic refinement' },
|
|
283
|
+
format: { type: 'string', alias: 'f', description: 'Output format: html or md (default: html)' },
|
|
284
|
+
brand: { type: 'string', description: 'Brand theme for HTML output (vercel/stripe/notion/linear/github)' },
|
|
149
285
|
json: { type: 'boolean', default: false },
|
|
150
286
|
},
|
|
151
287
|
async run({ args }) {
|
|
@@ -157,7 +293,7 @@ export const phaseDefine = defineCommand({
|
|
|
157
293
|
: ['Feature works as described', 'No regression in existing functionality'];
|
|
158
294
|
// === WorkflowEngine Integration ===
|
|
159
295
|
// Step 1: Explore with AmbiguityScorer + SocraticQuestioner
|
|
160
|
-
const exploreResult = await workflowEngine.explore(desc);
|
|
296
|
+
const exploreResult = await workflowEngine.explore(desc, { persistArtifact: false, runGate: false });
|
|
161
297
|
const ambiguityResult = workflowEngine.getAmbiguityScorer().analyzeRequirement(desc);
|
|
162
298
|
// Step 2: Check if requirement needs refinement.
|
|
163
299
|
if (ambiguityResult.blocked) {
|
|
@@ -251,6 +387,29 @@ export const phaseDefine = defineCommand({
|
|
|
251
387
|
ensureDir(specsDir);
|
|
252
388
|
const specPath = join(specsDir, `${spec.id}.md`);
|
|
253
389
|
writeFileSync(specPath, generateSpecMarkdown(spec.id, args.title, specPayload));
|
|
390
|
+
// Generate spec HTML file (default format: html)
|
|
391
|
+
const outputFormat = args.format ?? 'md';
|
|
392
|
+
let specHtmlPath;
|
|
393
|
+
if (outputFormat === 'html') {
|
|
394
|
+
const renderer = new HTMLDocumentRenderer({
|
|
395
|
+
title: args.title,
|
|
396
|
+
brand: args.brand,
|
|
397
|
+
version: '0.13.0',
|
|
398
|
+
status: 'FROZEN',
|
|
399
|
+
});
|
|
400
|
+
const html = renderer.renderSpec({
|
|
401
|
+
id: spec.id,
|
|
402
|
+
title: args.title,
|
|
403
|
+
what: refinedRequirement,
|
|
404
|
+
successCriteria,
|
|
405
|
+
outOfScope: specPayload.outOfScope,
|
|
406
|
+
edgeCases: specPayload.edgeCases,
|
|
407
|
+
northStar: specPayload.northStar,
|
|
408
|
+
ambiguityScore,
|
|
409
|
+
});
|
|
410
|
+
specHtmlPath = join(specsDir, `${spec.id}.html`);
|
|
411
|
+
renderer.writeToFile(html, specHtmlPath);
|
|
412
|
+
}
|
|
254
413
|
// FSM transitions: DRAFT -> REVIEWING -> FROZEN
|
|
255
414
|
// Phase 1: refine (DRAFT -> REVIEWING) - no guards
|
|
256
415
|
const refineResult = await fsm.canTransition(spec.id, 'refine');
|
|
@@ -277,12 +436,24 @@ export const phaseDefine = defineCommand({
|
|
|
277
436
|
if (!args.json) {
|
|
278
437
|
console.log(' FSM: DRAFT -> REVIEWING -> FROZEN ✓');
|
|
279
438
|
}
|
|
280
|
-
const result = { phase: 'DEFINE', spec, specPath, ambiguityScore, successCriteria };
|
|
439
|
+
const result = { phase: 'DEFINE', spec, specPath, specHtmlPath, ambiguityScore, successCriteria, format: outputFormat };
|
|
440
|
+
// Write explore artifact for Gate G1 verification
|
|
441
|
+
const artifactWriter = new WorkflowArtifactWriter(SCALE_DIR);
|
|
442
|
+
artifactWriter.writeExploreResult({
|
|
443
|
+
timestamp: new Date().toISOString(),
|
|
444
|
+
files: [specPath],
|
|
445
|
+
fileCount: 1,
|
|
446
|
+
mainContradiction: refinedRequirement !== desc ? 'requirement ambiguity resolved via Socratic refinement' : '',
|
|
447
|
+
ambiguityScore,
|
|
448
|
+
socraticCompleted: !ambiguityResult.requiresQuestioning || (ambiguityResult.requiresQuestioning && !exploreResult.socraticSession),
|
|
449
|
+
});
|
|
281
450
|
if (args.json)
|
|
282
451
|
console.log(JSON.stringify(result, null, 2));
|
|
283
452
|
else {
|
|
284
453
|
console.log(`\nDEFINE: ${spec.id}`);
|
|
285
454
|
console.log(` Spec file: ${specPath}`);
|
|
455
|
+
if (specHtmlPath)
|
|
456
|
+
console.log(` HTML file: ${specHtmlPath}`);
|
|
286
457
|
console.log(` Ambiguity score: ${ambiguityScore.toFixed(2)}`);
|
|
287
458
|
console.log(` Success criteria: ${successCriteria.length}`);
|
|
288
459
|
console.log(`\n Next: scale plan ${spec.id}\n`);
|
|
@@ -322,6 +493,8 @@ export const phasePlan = defineCommand({
|
|
|
322
493
|
'spec-id': { type: 'positional', required: true },
|
|
323
494
|
approach: { type: 'string', alias: 'a', description: 'Implementation approach' },
|
|
324
495
|
'rollback': { type: 'string', alias: 'r', description: 'Rollback strategy (required for FSM)' },
|
|
496
|
+
format: { type: 'string', alias: 'f', description: 'Output format: html or md (default: html)' },
|
|
497
|
+
brand: { type: 'string', description: 'Brand theme for HTML output (vercel/stripe/notion/linear/github)' },
|
|
325
498
|
json: { type: 'boolean', default: false },
|
|
326
499
|
},
|
|
327
500
|
async run({ args }) {
|
|
@@ -335,7 +508,7 @@ export const phasePlan = defineCommand({
|
|
|
335
508
|
// === WorkflowEngine Integration ===
|
|
336
509
|
// Step 1: Run ConsensusPlanner (Planner -> Architect -> Critic).
|
|
337
510
|
const specDesc = spec.payload.what;
|
|
338
|
-
const consensusResult = await workflowEngine.plan(specDesc);
|
|
511
|
+
const consensusResult = await workflowEngine.plan(specDesc, { persistArtifact: false, runGate: false });
|
|
339
512
|
// Step 2: Display RALPLAN-DR output
|
|
340
513
|
if (!args.json) {
|
|
341
514
|
console.log('\nConsensus Planning Result:');
|
|
@@ -364,6 +537,41 @@ export const phasePlan = defineCommand({
|
|
|
364
537
|
ensureDir(plansDir);
|
|
365
538
|
const planPath = join(plansDir, `${plan.id}.md`);
|
|
366
539
|
writeFileSync(planPath, generatePlanMarkdown(plan.id, args['spec-id'], planPayload));
|
|
540
|
+
// Generate plan HTML file (default format: html)
|
|
541
|
+
const planOutputFormat = args.format ?? 'md';
|
|
542
|
+
let planHtmlPath;
|
|
543
|
+
if (planOutputFormat === 'html') {
|
|
544
|
+
const planRenderer = new HTMLDocumentRenderer({
|
|
545
|
+
title: `Plan ${plan.id}`,
|
|
546
|
+
brand: args.brand,
|
|
547
|
+
version: '0.13.0',
|
|
548
|
+
status: 'APPROVED',
|
|
549
|
+
});
|
|
550
|
+
const planHtml = planRenderer.renderPlan({
|
|
551
|
+
id: plan.id,
|
|
552
|
+
specId: args['spec-id'],
|
|
553
|
+
approach: planPayload.approach,
|
|
554
|
+
techChoices: planPayload.techChoices,
|
|
555
|
+
modules: planPayload.modules,
|
|
556
|
+
rollbackStrategy: planPayload.rollbackStrategy,
|
|
557
|
+
estimatedComplexity: planPayload.estimatedComplexity,
|
|
558
|
+
});
|
|
559
|
+
planHtmlPath = join(plansDir, `${plan.id}.html`);
|
|
560
|
+
planRenderer.writeToFile(planHtml, planHtmlPath);
|
|
561
|
+
}
|
|
562
|
+
// Write plan artifact for Gate G2 verification
|
|
563
|
+
const artifactWriter = new WorkflowArtifactWriter(SCALE_DIR);
|
|
564
|
+
artifactWriter.writePlanResult({
|
|
565
|
+
timestamp: new Date().toISOString(),
|
|
566
|
+
planId: plan.id,
|
|
567
|
+
specId: args['spec-id'],
|
|
568
|
+
hasBoundaryAnalysis: consensusResult.viableOptions.length > 1,
|
|
569
|
+
hasExceptionHandling: consensusResult.preMortem.rootCauses.length > 0,
|
|
570
|
+
hasRollbackStrategy: !!rollbackStrategy,
|
|
571
|
+
modules: planPayload.modules.map(m => m.path),
|
|
572
|
+
consensusRounds: consensusResult.iterationCount,
|
|
573
|
+
verdict: consensusResult.verdict,
|
|
574
|
+
});
|
|
367
575
|
// FSM transition: DRAFT -> APPROVED (requires rollbackStrategy guard)
|
|
368
576
|
const reviewResult = await fsm.canTransition(plan.id, 'review');
|
|
369
577
|
if (!reviewResult.allowed) {
|
|
@@ -379,12 +587,14 @@ export const phasePlan = defineCommand({
|
|
|
379
587
|
if (!args.json) {
|
|
380
588
|
console.log(' FSM: DRAFT -> APPROVED ✓');
|
|
381
589
|
}
|
|
382
|
-
const result = { phase: 'PLAN', plan, planPath, rollbackStrategy };
|
|
590
|
+
const result = { phase: 'PLAN', plan, planPath, planHtmlPath, rollbackStrategy, format: planOutputFormat };
|
|
383
591
|
if (args.json)
|
|
384
592
|
console.log(JSON.stringify(result, null, 2));
|
|
385
593
|
else {
|
|
386
594
|
console.log(`\nPLAN: ${plan.id}`);
|
|
387
595
|
console.log(` Plan file: ${planPath}`);
|
|
596
|
+
if (planHtmlPath)
|
|
597
|
+
console.log(` HTML file: ${planHtmlPath}`);
|
|
388
598
|
console.log(` Rollback: ${rollbackStrategy}`);
|
|
389
599
|
console.log(`\n Next: scale build ${plan.id}\n`);
|
|
390
600
|
}
|
|
@@ -396,6 +606,9 @@ export const phaseBuild = defineCommand({
|
|
|
396
606
|
args: {
|
|
397
607
|
'plan-id': { type: 'positional', required: true },
|
|
398
608
|
description: { type: 'string', alias: 'd', description: 'Task description' },
|
|
609
|
+
level: { type: 'string', default: 'M', description: 'Workflow task level: S, M, L, or CRITICAL' },
|
|
610
|
+
service: { type: 'string', description: 'Comma-separated service names touched by this task' },
|
|
611
|
+
'residual-risk': { type: 'string', description: 'Known residual risk statement for metrics' },
|
|
399
612
|
json: { type: 'boolean', default: false },
|
|
400
613
|
},
|
|
401
614
|
async run({ args }) {
|
|
@@ -406,9 +619,20 @@ export const phaseBuild = defineCommand({
|
|
|
406
619
|
console.error(`\nPlan not found: ${args['plan-id']}\n`);
|
|
407
620
|
process.exit(1);
|
|
408
621
|
}
|
|
622
|
+
let workflowLevel;
|
|
623
|
+
try {
|
|
624
|
+
workflowLevel = normalizeWorkflowLevel(args.level);
|
|
625
|
+
}
|
|
626
|
+
catch (e) {
|
|
627
|
+
console.error(`\n${e.message}\n`);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
409
630
|
// Create TaskPayload
|
|
410
631
|
const taskPayload = {
|
|
411
632
|
description: args.description ?? `Implement ${plan.title}`,
|
|
633
|
+
workflowLevel,
|
|
634
|
+
servicesTouched: normalizeServices(args.service),
|
|
635
|
+
residualRisk: args['residual-risk'],
|
|
412
636
|
filesInvolved: [],
|
|
413
637
|
dependsOn: [],
|
|
414
638
|
requiredRole: 'implementer',
|
|
@@ -428,13 +652,60 @@ export const phaseBuild = defineCommand({
|
|
|
428
652
|
outOfScope: [],
|
|
429
653
|
},
|
|
430
654
|
};
|
|
655
|
+
const taskTitle = `Task for ${plan.title}`;
|
|
431
656
|
const task = await store.create({
|
|
432
|
-
type: 'Task', title:
|
|
657
|
+
type: 'Task', title: taskTitle,
|
|
433
658
|
payload: taskPayload,
|
|
434
659
|
parents: [args['plan-id']],
|
|
435
660
|
initialStatus: 'PENDING',
|
|
436
661
|
createdBy: { kind: 'human', userId: 'cli' },
|
|
437
662
|
});
|
|
663
|
+
const skillPlan = planSkillsForTask({
|
|
664
|
+
taskId: task.id,
|
|
665
|
+
taskName: taskTitle,
|
|
666
|
+
description: taskPayload.description,
|
|
667
|
+
level: workflowLevel,
|
|
668
|
+
services: taskPayload.servicesTouched,
|
|
669
|
+
files: taskPayload.filesInvolved,
|
|
670
|
+
});
|
|
671
|
+
const taskPayloadWithSkills = {
|
|
672
|
+
...taskPayload,
|
|
673
|
+
skillIntents: skillPlan.intents.map(intent => intent.domain),
|
|
674
|
+
skillRoutingMode: skillPlan.mode,
|
|
675
|
+
skillPlanRequired: skillPlan.required,
|
|
676
|
+
requiredSkills: skillPlan.requiredSkills,
|
|
677
|
+
recommendedSkills: skillPlan.recommendedSkills,
|
|
678
|
+
requiredSkillArtifacts: skillPlan.requiredArtifacts,
|
|
679
|
+
requiredSkillVerification: skillPlan.requiredVerification,
|
|
680
|
+
};
|
|
681
|
+
await store.update(task.id, { payload: taskPayloadWithSkills });
|
|
682
|
+
let taskArtifacts;
|
|
683
|
+
if (workflowLevel !== 'S') {
|
|
684
|
+
taskArtifacts = scaffoldTaskArtifacts({
|
|
685
|
+
projectDir: PROJECT_DIR,
|
|
686
|
+
taskId: task.id,
|
|
687
|
+
taskName: task.title,
|
|
688
|
+
description: taskPayloadWithSkills.description,
|
|
689
|
+
level: workflowLevel,
|
|
690
|
+
services: taskPayloadWithSkills.servicesTouched,
|
|
691
|
+
skillPlan,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
new WorkflowArtifactWriter(SCALE_DIR).updateCurrentState({
|
|
695
|
+
taskId: task.id,
|
|
696
|
+
level: workflowLevel,
|
|
697
|
+
phase: 'build',
|
|
698
|
+
lastTaskId: task.id,
|
|
699
|
+
artifactsDir: taskArtifacts?.relativeDir,
|
|
700
|
+
skillIntents: skillPlan.intents.map(intent => intent.domain),
|
|
701
|
+
skillRoutingMode: skillPlan.mode,
|
|
702
|
+
skillPlanRequired: skillPlan.required,
|
|
703
|
+
skillPlanPath: taskArtifacts?.relativeDir ? `${taskArtifacts.relativeDir}/skill-plan.md` : undefined,
|
|
704
|
+
requiredSkills: skillPlan.requiredSkills,
|
|
705
|
+
recommendedSkills: skillPlan.recommendedSkills,
|
|
706
|
+
requiredSkillArtifacts: skillPlan.requiredArtifacts,
|
|
707
|
+
requiredSkillVerification: skillPlan.requiredVerification,
|
|
708
|
+
});
|
|
438
709
|
// FSM transitions: PENDING -> READY -> RUNNING
|
|
439
710
|
// Phase 1: schedule (PENDING -> READY) - no guards
|
|
440
711
|
const scheduleResult = await fsm.canTransition(task.id, 'schedule');
|
|
@@ -456,13 +727,21 @@ export const phaseBuild = defineCommand({
|
|
|
456
727
|
if (implResult.allowed) {
|
|
457
728
|
await fsm.transition(args['plan-id'], 'implement', { actor: { kind: 'system', component: 'phase-build' } });
|
|
458
729
|
}
|
|
459
|
-
const result = { phase: 'BUILD', task, status: 'RUNNING' };
|
|
730
|
+
const result = { phase: 'BUILD', task: { ...task, payload: taskPayloadWithSkills }, status: 'RUNNING', artifactDir: taskArtifacts?.relativeDir, artifactFiles: taskArtifacts?.created ?? [], skillPlan };
|
|
460
731
|
if (args.json)
|
|
461
732
|
console.log(JSON.stringify(result, null, 2));
|
|
462
733
|
else {
|
|
463
734
|
console.log(`\nBUILD: ${task.id}`);
|
|
464
735
|
console.log(` Status: RUNNING (ready to implement)`);
|
|
465
|
-
console.log(` Description: ${
|
|
736
|
+
console.log(` Description: ${taskPayloadWithSkills.description}`);
|
|
737
|
+
if (skillPlan.intents.length)
|
|
738
|
+
console.log(` Skill intents: ${skillPlan.intents.map(intent => intent.domain).join(', ')}`);
|
|
739
|
+
if (skillPlan.requiredSkills.length)
|
|
740
|
+
console.log(` Required skills: ${skillPlan.requiredSkills.join(', ')}`);
|
|
741
|
+
if (skillPlan.recommendedSkills.length)
|
|
742
|
+
console.log(` Recommended skills: ${skillPlan.recommendedSkills.join(', ')}`);
|
|
743
|
+
if (taskArtifacts?.relativeDir)
|
|
744
|
+
console.log(` Artifacts: ${taskArtifacts.relativeDir}`);
|
|
466
745
|
console.log(`\n Implement now, then run: scale verify ${task.id}\n`);
|
|
467
746
|
}
|
|
468
747
|
},
|
|
@@ -487,8 +766,14 @@ export const phaseVerify = defineCommand({
|
|
|
487
766
|
'lint-cmd': { type: 'string', description: 'Override lint command' },
|
|
488
767
|
'test-cmd': { type: 'string', description: 'Override test command' },
|
|
489
768
|
'coverage-cmd': { type: 'string', description: 'Override coverage command' },
|
|
769
|
+
profile: { type: 'string', description: 'Verification profile from .scale/verification.json' },
|
|
770
|
+
service: { type: 'string', description: 'Service name from .scale/verification.json' },
|
|
771
|
+
'artifact-gate': { type: 'string', description: 'Task artifact policy override: off, warn, or block' },
|
|
772
|
+
'require-artifacts': { type: 'boolean', default: false, description: 'Fail verification when required M/L/CRITICAL artifacts are incomplete' },
|
|
773
|
+
'require-installed-skills': { type: 'boolean', default: false, description: 'Fail verification when required workflow skills are not installed locally' },
|
|
490
774
|
'tdd-evidence': { type: 'string', description: 'Path to JSON TDD evidence with red/green/refactor/testFirst=true' },
|
|
491
775
|
'tdd-strict': { type: 'boolean', default: false, description: 'Require TDD evidence before other gates' },
|
|
776
|
+
'residual-risk': { type: 'string', description: 'Residual risk statement to record in task metrics' },
|
|
492
777
|
'skip-build': { type: 'boolean', default: false },
|
|
493
778
|
'skip-lint': { type: 'boolean', default: false },
|
|
494
779
|
'skip-test': { type: 'boolean', default: false },
|
|
@@ -502,18 +787,48 @@ export const phaseVerify = defineCommand({
|
|
|
502
787
|
console.error(`\nTask not found: ${args['task-id']}\n`);
|
|
503
788
|
process.exit(1);
|
|
504
789
|
}
|
|
790
|
+
const currentPayload = task.payload;
|
|
791
|
+
const taskServices = currentPayload.servicesTouched ?? [];
|
|
792
|
+
const taskFiles = currentPayload.filesInvolved?.length
|
|
793
|
+
? currentPayload.filesInvolved.map(normalizeGitPath)
|
|
794
|
+
: await detectTaskChangedFiles();
|
|
505
795
|
// === WorkflowEngine Integration ===
|
|
506
796
|
// Step 1: Run GateSystem G3-G7
|
|
507
797
|
if (!args.json)
|
|
508
798
|
console.log('\nRunning Quality Gates...');
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
tddStrict: isTruthyFlag(args['tdd-strict']),
|
|
799
|
+
const resolvedVerification = resolveVerificationTargets({
|
|
800
|
+
projectDir: PROJECT_DIR,
|
|
801
|
+
scaleDir: SCALE_DIR,
|
|
802
|
+
profile: args.profile,
|
|
803
|
+
service: args.service,
|
|
804
|
+
services: args.service ? undefined : taskServices,
|
|
516
805
|
});
|
|
806
|
+
if (!args.json) {
|
|
807
|
+
for (const warning of resolvedVerification.warnings)
|
|
808
|
+
console.log(` [WARN] ${warning}`);
|
|
809
|
+
for (const target of resolvedVerification.targets) {
|
|
810
|
+
if (target.service) {
|
|
811
|
+
console.log(` Service: ${target.service.name} (${target.service.path})`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
console.log(` Profile: ${resolvedVerification.profileName}`);
|
|
815
|
+
}
|
|
816
|
+
const gateResults = [];
|
|
817
|
+
for (const target of resolvedVerification.targets) {
|
|
818
|
+
if (!args.json && resolvedVerification.targets.length > 1) {
|
|
819
|
+
console.log(`\n Target: ${target.service?.name ?? 'root'}`);
|
|
820
|
+
}
|
|
821
|
+
const targetResults = await workflowEngine.verify({
|
|
822
|
+
cwd: target.config.cwd,
|
|
823
|
+
build: args['build-cmd'] ?? target.config.build,
|
|
824
|
+
lint: args['lint-cmd'] ?? target.config.lint,
|
|
825
|
+
test: args['test-cmd'] ?? target.config.test,
|
|
826
|
+
coverage: args['coverage-cmd'] ?? target.config.coverage,
|
|
827
|
+
tddEvidence: args['tdd-evidence'],
|
|
828
|
+
tddStrict: isTruthyFlag(args['tdd-strict']),
|
|
829
|
+
});
|
|
830
|
+
gateResults.push(...targetResults);
|
|
831
|
+
}
|
|
517
832
|
// Step 2: Display gate results
|
|
518
833
|
if (!args.json) {
|
|
519
834
|
console.log('\nGate Results:');
|
|
@@ -525,28 +840,50 @@ export const phaseVerify = defineCommand({
|
|
|
525
840
|
}
|
|
526
841
|
}
|
|
527
842
|
// Extract results from gateResults
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
const
|
|
843
|
+
const g0Results = gateResults.filter(g => g.gate === 'G0');
|
|
844
|
+
const g4Results = gateResults.filter(g => g.gate === 'G4');
|
|
845
|
+
const g5Results = gateResults.filter(g => g.gate === 'G5');
|
|
846
|
+
const g6Results = gateResults.filter(g => g.gate === 'G6');
|
|
847
|
+
const g7Results = gateResults.filter(g => g.gate === 'G7');
|
|
848
|
+
const gatePassed = (results) => results.length > 0 && results.every(result => result.passed);
|
|
849
|
+
const buildExitCodes = g0Results
|
|
850
|
+
.flatMap(result => result.evidenceItems ?? [])
|
|
851
|
+
.filter(item => item.kind === 'command')
|
|
852
|
+
.map(item => item.exitCode)
|
|
853
|
+
.filter((code) => typeof code === 'number');
|
|
533
854
|
const results = {
|
|
534
|
-
buildStatus:
|
|
535
|
-
buildExitCode:
|
|
536
|
-
lintStatus:
|
|
537
|
-
testPassed:
|
|
855
|
+
buildStatus: gatePassed(g0Results) ? 'success' : 'failed',
|
|
856
|
+
buildExitCode: buildExitCodes.find(code => code !== 0) ?? (buildExitCodes.length > 0 ? 0 : undefined),
|
|
857
|
+
lintStatus: gatePassed(g4Results) ? 'success' : 'failed',
|
|
858
|
+
testPassed: gatePassed(g5Results),
|
|
538
859
|
testCoverage: undefined,
|
|
539
|
-
securityPassed:
|
|
860
|
+
securityPassed: gatePassed(g7Results),
|
|
540
861
|
};
|
|
541
862
|
const verificationEvidenceIds = gateResults
|
|
542
863
|
.map(g => g.evidenceRecordId)
|
|
543
864
|
.filter((id) => Boolean(id));
|
|
544
865
|
// Extract coverage from G6 evidence
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
866
|
+
const coverageValues = g6Results
|
|
867
|
+
.map(result => result.evidence.match(/Coverage: (\d+\.?\d*)%/))
|
|
868
|
+
.filter((match) => Boolean(match))
|
|
869
|
+
.map(match => parseFloat(match[1]));
|
|
870
|
+
if (coverageValues.length > 0)
|
|
871
|
+
results.testCoverage = Math.min(...coverageValues);
|
|
548
872
|
// Update Task payload with verification results
|
|
549
|
-
const
|
|
873
|
+
const taskLevel = normalizeWorkflowLevel(currentPayload.workflowLevel ?? 'M');
|
|
874
|
+
const verificationSkillPlan = taskLevel === 'S'
|
|
875
|
+
? undefined
|
|
876
|
+
: planSkillsForTask({
|
|
877
|
+
taskId: args['task-id'],
|
|
878
|
+
taskName: task.title,
|
|
879
|
+
description: currentPayload.description,
|
|
880
|
+
level: taskLevel,
|
|
881
|
+
services: currentPayload.servicesTouched,
|
|
882
|
+
files: taskFiles,
|
|
883
|
+
});
|
|
884
|
+
const verifiedServices = resolvedVerification.targets
|
|
885
|
+
.map(target => target.service?.name)
|
|
886
|
+
.filter((service) => Boolean(service));
|
|
550
887
|
const updatedPayload = {
|
|
551
888
|
...currentPayload,
|
|
552
889
|
buildStatus: results.buildStatus,
|
|
@@ -554,20 +891,68 @@ export const phaseVerify = defineCommand({
|
|
|
554
891
|
lintStatus: results.lintStatus,
|
|
555
892
|
testPassed: results.testPassed,
|
|
556
893
|
testCoverage: results.testCoverage,
|
|
894
|
+
servicesTouched: currentPayload.servicesTouched?.length
|
|
895
|
+
? currentPayload.servicesTouched
|
|
896
|
+
: verifiedServices.length > 0 ? verifiedServices : currentPayload.servicesTouched,
|
|
897
|
+
filesInvolved: currentPayload.filesInvolved?.length ? currentPayload.filesInvolved : taskFiles,
|
|
898
|
+
residualRisk: args['residual-risk'] ?? currentPayload.residualRisk,
|
|
557
899
|
verificationEvidenceIds,
|
|
900
|
+
skillIntents: verificationSkillPlan?.intents.map(intent => intent.domain) ?? currentPayload.skillIntents,
|
|
901
|
+
skillRoutingMode: verificationSkillPlan?.mode ?? currentPayload.skillRoutingMode,
|
|
902
|
+
skillPlanRequired: verificationSkillPlan?.required ?? currentPayload.skillPlanRequired,
|
|
903
|
+
requiredSkills: verificationSkillPlan?.requiredSkills ?? currentPayload.requiredSkills,
|
|
904
|
+
recommendedSkills: verificationSkillPlan?.recommendedSkills ?? currentPayload.recommendedSkills,
|
|
905
|
+
requiredSkillArtifacts: verificationSkillPlan?.requiredArtifacts ?? currentPayload.requiredSkillArtifacts,
|
|
906
|
+
requiredSkillVerification: verificationSkillPlan?.requiredVerification ?? currentPayload.requiredSkillVerification,
|
|
558
907
|
verifiedAt: Date.now(),
|
|
559
908
|
};
|
|
560
909
|
await store.update(args['task-id'], { payload: updatedPayload });
|
|
910
|
+
const workflowState = new WorkflowArtifactWriter(SCALE_DIR).updateCurrentState({
|
|
911
|
+
taskId: args['task-id'],
|
|
912
|
+
phase: 'verify',
|
|
913
|
+
lastTaskId: args['task-id'],
|
|
914
|
+
filesModified: updatedPayload.filesInvolved,
|
|
915
|
+
skillIntents: updatedPayload.skillIntents,
|
|
916
|
+
skillRoutingMode: updatedPayload.skillRoutingMode,
|
|
917
|
+
skillPlanRequired: updatedPayload.skillPlanRequired,
|
|
918
|
+
requiredSkills: updatedPayload.requiredSkills,
|
|
919
|
+
recommendedSkills: updatedPayload.recommendedSkills,
|
|
920
|
+
requiredSkillArtifacts: updatedPayload.requiredSkillArtifacts,
|
|
921
|
+
requiredSkillVerification: updatedPayload.requiredSkillVerification,
|
|
922
|
+
});
|
|
923
|
+
const metricLevel = metricLevelFromPayload(updatedPayload);
|
|
924
|
+
const preArtifactCheck = metricLevel ? checkCurrentTaskArtifacts(metricLevel) : undefined;
|
|
925
|
+
const artifactGate = evaluateArtifactGate({
|
|
926
|
+
policy: resolvedVerification.policy,
|
|
927
|
+
level: metricLevel,
|
|
928
|
+
check: preArtifactCheck ? assumeVerificationArtifactWillBeWritten(preArtifactCheck) : undefined,
|
|
929
|
+
cliMode: args['artifact-gate'],
|
|
930
|
+
requireArtifacts: args['require-artifacts'],
|
|
931
|
+
});
|
|
932
|
+
const skillPolicy = loadSkillRoutingPolicy(PROJECT_DIR, SCALE_DIR);
|
|
933
|
+
const skillGate = metricLevel && verificationSkillPlan
|
|
934
|
+
? evaluateSkillGate({
|
|
935
|
+
projectDir: PROJECT_DIR,
|
|
936
|
+
artifactsDir: workflowState.artifactsDir,
|
|
937
|
+
level: metricLevel,
|
|
938
|
+
plan: verificationSkillPlan,
|
|
939
|
+
enforceLevels: skillPolicy.policy.enforceLevels,
|
|
940
|
+
})
|
|
941
|
+
: undefined;
|
|
942
|
+
const requireInstalledSkills = isTruthyFlag(args['require-installed-skills']);
|
|
943
|
+
const skillInstallation = inspectRequiredWorkflowSkills(updatedPayload.requiredSkills ?? [], { projectDir: PROJECT_DIR });
|
|
944
|
+
const skillInstallationBlocked = requireInstalledSkills && !skillInstallation.ok;
|
|
561
945
|
// Attempt FSM transition to COMPLETED
|
|
562
|
-
// Guards: build_passed, lint_passed, tests_passed
|
|
563
|
-
const
|
|
946
|
+
// Guards: build_passed, lint_passed, tests_passed, and optional artifact policy.
|
|
947
|
+
const codePassed = results.buildStatus === 'success' &&
|
|
564
948
|
(results.buildExitCode ?? 1) === 0 &&
|
|
565
949
|
results.lintStatus === 'success' &&
|
|
566
950
|
results.testPassed === true &&
|
|
567
951
|
(results.testCoverage ?? 0) >= 80 &&
|
|
568
952
|
results.securityPassed === true;
|
|
953
|
+
const completionEligible = codePassed && !artifactGate.blocked && !(skillGate?.blocked ?? false) && !skillInstallationBlocked;
|
|
569
954
|
let transitionResult = null;
|
|
570
|
-
if (
|
|
955
|
+
if (completionEligible) {
|
|
571
956
|
const completeResult = await fsm.canTransition(args['task-id'], 'complete');
|
|
572
957
|
if (!completeResult.allowed) {
|
|
573
958
|
if (!args.json) {
|
|
@@ -582,18 +967,103 @@ export const phaseVerify = defineCommand({
|
|
|
582
967
|
actor: { kind: 'human', userId: 'cli' }
|
|
583
968
|
});
|
|
584
969
|
if (!args.json)
|
|
585
|
-
console.log('\n FSM: RUNNING -> COMPLETED
|
|
970
|
+
console.log('\n FSM: RUNNING -> COMPLETED');
|
|
586
971
|
}
|
|
587
972
|
}
|
|
588
|
-
else if (!args.json) {
|
|
973
|
+
else if (!args.json && !codePassed) {
|
|
589
974
|
console.log('\n Verification requirements not met - cannot complete Task');
|
|
590
975
|
}
|
|
591
|
-
|
|
592
|
-
|
|
976
|
+
else if (!args.json && artifactGate.blocked) {
|
|
977
|
+
console.log('\n Artifact gate blocked completion - required task artifacts are incomplete');
|
|
978
|
+
}
|
|
979
|
+
else if (!args.json && skillGate?.blocked) {
|
|
980
|
+
console.log('\n Skill gate blocked completion - required skill evidence artifacts are incomplete');
|
|
981
|
+
}
|
|
982
|
+
else if (!args.json && skillInstallationBlocked) {
|
|
983
|
+
console.log('\n Skill installation gate blocked completion - required workflow skills are missing');
|
|
984
|
+
}
|
|
985
|
+
const passed = completionEligible && (transitionResult?.success ?? false);
|
|
986
|
+
const verificationArtifactPath = appendVerificationArtifact({
|
|
987
|
+
projectDir: PROJECT_DIR,
|
|
988
|
+
artifactsDir: workflowState.artifactsDir,
|
|
989
|
+
taskId: args['task-id'],
|
|
990
|
+
profile: resolvedVerification.profileName,
|
|
991
|
+
services: verifiedServices,
|
|
992
|
+
gateResults,
|
|
993
|
+
passed,
|
|
994
|
+
});
|
|
995
|
+
const artifactCheck = metricLevel ? checkCurrentTaskArtifacts(metricLevel) : undefined;
|
|
996
|
+
const finalArtifactGate = artifactCheck
|
|
997
|
+
? evaluateArtifactGate({
|
|
998
|
+
policy: resolvedVerification.policy,
|
|
999
|
+
level: metricLevel,
|
|
1000
|
+
check: artifactCheck,
|
|
1001
|
+
cliMode: args['artifact-gate'],
|
|
1002
|
+
requireArtifacts: args['require-artifacts'],
|
|
1003
|
+
})
|
|
1004
|
+
: artifactGate;
|
|
1005
|
+
const finalSkillGate = metricLevel && verificationSkillPlan
|
|
1006
|
+
? evaluateSkillGate({
|
|
1007
|
+
projectDir: PROJECT_DIR,
|
|
1008
|
+
artifactsDir: workflowState.artifactsDir,
|
|
1009
|
+
level: metricLevel,
|
|
1010
|
+
plan: verificationSkillPlan,
|
|
1011
|
+
enforceLevels: skillPolicy.policy.enforceLevels,
|
|
1012
|
+
})
|
|
1013
|
+
: skillGate;
|
|
1014
|
+
const finalPayload = {
|
|
1015
|
+
...updatedPayload,
|
|
1016
|
+
artifactGateMode: finalArtifactGate.mode,
|
|
1017
|
+
artifactGatePassed: !finalArtifactGate.blocked,
|
|
1018
|
+
artifactComplete: artifactCheck?.complete,
|
|
1019
|
+
skillGatePassed: finalSkillGate ? !finalSkillGate.blocked && !skillInstallationBlocked : !skillInstallationBlocked,
|
|
1020
|
+
};
|
|
1021
|
+
await store.update(args['task-id'], { payload: finalPayload });
|
|
1022
|
+
const metricGateStatus = codePassed && (finalArtifactGate.blocked || finalSkillGate?.blocked || skillInstallationBlocked) ? 'blocked' : undefined;
|
|
1023
|
+
const metricRecord = await recordVerificationMetric({
|
|
1024
|
+
taskId: args['task-id'],
|
|
1025
|
+
taskName: task.title,
|
|
1026
|
+
taskPayload: finalPayload,
|
|
1027
|
+
passed,
|
|
1028
|
+
serviceNames: verifiedServices,
|
|
1029
|
+
artifactCheck,
|
|
1030
|
+
finalGateStatus: metricGateStatus,
|
|
1031
|
+
});
|
|
1032
|
+
const result = {
|
|
1033
|
+
phase: 'VERIFY',
|
|
1034
|
+
taskId: args['task-id'],
|
|
1035
|
+
profile: resolvedVerification.profileName,
|
|
1036
|
+
service: verifiedServices.length === 1 ? verifiedServices[0] : undefined,
|
|
1037
|
+
services: verifiedServices,
|
|
1038
|
+
results,
|
|
1039
|
+
evidenceIds: verificationEvidenceIds,
|
|
1040
|
+
verificationArtifactPath,
|
|
1041
|
+
artifactCheck,
|
|
1042
|
+
artifactGate: finalArtifactGate,
|
|
1043
|
+
skillGate: finalSkillGate,
|
|
1044
|
+
skillInstallation: {
|
|
1045
|
+
...skillInstallation,
|
|
1046
|
+
checked: requireInstalledSkills,
|
|
1047
|
+
blocked: skillInstallationBlocked,
|
|
1048
|
+
},
|
|
1049
|
+
metric: metricRecord,
|
|
1050
|
+
passed
|
|
1051
|
+
};
|
|
593
1052
|
if (args.json)
|
|
594
1053
|
console.log(JSON.stringify(result, null, 2));
|
|
595
1054
|
else {
|
|
596
1055
|
console.log(`\nVERIFY: ${passed ? 'PASSED' : 'FAILED'}`);
|
|
1056
|
+
if (metricRecord)
|
|
1057
|
+
console.log(` Metrics: ${metricRecord.taskId} ${metricRecord.finalGateStatus} (fix iterations: ${metricRecord.fixIterations})`);
|
|
1058
|
+
if (artifactCheck && !artifactCheck.complete) {
|
|
1059
|
+
console.log(` Artifact gaps: ${artifactCheck.missing.length} missing, ${artifactCheck.incomplete.length} incomplete`);
|
|
1060
|
+
}
|
|
1061
|
+
if (finalSkillGate && !finalSkillGate.complete) {
|
|
1062
|
+
console.log(` Skill evidence gaps: ${finalSkillGate.missing.length} missing, ${finalSkillGate.incomplete.length} incomplete`);
|
|
1063
|
+
}
|
|
1064
|
+
if (skillInstallationBlocked) {
|
|
1065
|
+
console.log(` Missing required workflow skills: ${skillInstallation.missing.join(', ')}`);
|
|
1066
|
+
}
|
|
597
1067
|
if (passed)
|
|
598
1068
|
console.log(`\n Next: scale review\n`);
|
|
599
1069
|
else
|
|
@@ -603,7 +1073,7 @@ export const phaseVerify = defineCommand({
|
|
|
603
1073
|
});
|
|
604
1074
|
async function runGit(args) {
|
|
605
1075
|
const { execa } = await import('execa');
|
|
606
|
-
const result = await execa('git', args, { reject: false });
|
|
1076
|
+
const result = await execa('git', args, { cwd: PROJECT_DIR, reject: false });
|
|
607
1077
|
return {
|
|
608
1078
|
exitCode: result.exitCode ?? 1,
|
|
609
1079
|
stdout: result.stdout ?? '',
|
|
@@ -612,12 +1082,14 @@ async function runGit(args) {
|
|
|
612
1082
|
}
|
|
613
1083
|
function mergeUntrackedFilesIntoStatus(statusOutput, untrackedOutput) {
|
|
614
1084
|
const existing = new Set(parseChangedFiles(statusOutput).map(file => file.path.replace(/\\/g, '/')));
|
|
1085
|
+
// Add '??' status marker for untracked files so parseChangedFiles can recognize them
|
|
615
1086
|
const additions = untrackedOutput
|
|
616
1087
|
.split('\n')
|
|
617
1088
|
.map(line => line.trim())
|
|
618
1089
|
.filter(Boolean)
|
|
619
1090
|
.filter(path => shouldReviewFile(path))
|
|
620
|
-
.filter(path => !existing.has(path.replace(/\\/g, '/')))
|
|
1091
|
+
.filter(path => !existing.has(path.replace(/\\/g, '/')))
|
|
1092
|
+
.map(path => `?? ${path}`); // Add status marker
|
|
621
1093
|
return [statusOutput.trim(), ...additions].filter(Boolean).join('\n');
|
|
622
1094
|
}
|
|
623
1095
|
function readUntrackedFileAsDiff(path) {
|
|
@@ -641,7 +1113,23 @@ function readUntrackedFileAsDiff(path) {
|
|
|
641
1113
|
async function reviewGitChanges(taskPayload) {
|
|
642
1114
|
const status = await runGit(['status', '--short']);
|
|
643
1115
|
const untracked = await runGit(['ls-files', '--others', '--exclude-standard']);
|
|
644
|
-
|
|
1116
|
+
let statusOutput = mergeUntrackedFilesIntoStatus(status.stdout, untracked.stdout);
|
|
1117
|
+
// Scope review to task-relevant files only.
|
|
1118
|
+
// When filesInvolved is set, only analyze those files.
|
|
1119
|
+
// When empty, only analyze untracked (new) files to avoid picking up
|
|
1120
|
+
// unrelated modifications from a dirty working tree.
|
|
1121
|
+
if (taskPayload?.filesInvolved?.length) {
|
|
1122
|
+
const involved = new Set(taskPayload.filesInvolved.map(f => f.replace(/\\/g, '/')));
|
|
1123
|
+
statusOutput = statusOutput.split('\n').filter(line => {
|
|
1124
|
+
const parsed = parseChangedFiles(line);
|
|
1125
|
+
return parsed.length > 0 && involved.has(parsed[0].path.replace(/\\/g, '/'));
|
|
1126
|
+
}).join('\n');
|
|
1127
|
+
}
|
|
1128
|
+
else {
|
|
1129
|
+
// Only include untracked files (status '??') — skip tracked modifications
|
|
1130
|
+
// that may be unrelated to the task under review.
|
|
1131
|
+
statusOutput = statusOutput.split('\n').filter(line => line.startsWith('??')).join('\n');
|
|
1132
|
+
}
|
|
645
1133
|
const verificationEvidence = getVerificationEvidenceSummary(taskPayload?.verificationEvidenceIds);
|
|
646
1134
|
const changedFiles = analyzeReview({ statusOutput, diffs: [], taskPayload, verificationEvidence }).changedFiles;
|
|
647
1135
|
const diffs = [];
|
|
@@ -679,6 +1167,12 @@ async function stageReviewedFiles(reviewRecords) {
|
|
|
679
1167
|
const currentChanges = await getReviewableGitChanges();
|
|
680
1168
|
const stagedFiles = [];
|
|
681
1169
|
const unreviewedFiles = [];
|
|
1170
|
+
// Edge case: if currentChanges is empty but reviewedFiles has files that should be staged,
|
|
1171
|
+
// this indicates files were deleted or moved. Treat reviewed but missing files as unreviewed.
|
|
1172
|
+
if (currentChanges.length === 0 && reviewedFiles.size > 0) {
|
|
1173
|
+
// No changes to stage, but we have review records - this is a pass (nothing to commit)
|
|
1174
|
+
return { stagedFiles: [], unreviewedFiles: [] };
|
|
1175
|
+
}
|
|
682
1176
|
for (const file of currentChanges) {
|
|
683
1177
|
const normalizedPath = normalizeGitPath(file.path);
|
|
684
1178
|
if (reviewedFiles.has(normalizedPath)) {
|
|
@@ -688,6 +1182,7 @@ async function stageReviewedFiles(reviewRecords) {
|
|
|
688
1182
|
unreviewedFiles.push(file.path);
|
|
689
1183
|
}
|
|
690
1184
|
}
|
|
1185
|
+
// Only block if there are actual unreviewed changes
|
|
691
1186
|
if (unreviewedFiles.length > 0) {
|
|
692
1187
|
return { stagedFiles: [], unreviewedFiles };
|
|
693
1188
|
}
|
|
@@ -699,6 +1194,104 @@ async function stageReviewedFiles(reviewRecords) {
|
|
|
699
1194
|
}
|
|
700
1195
|
return { stagedFiles, unreviewedFiles: [] };
|
|
701
1196
|
}
|
|
1197
|
+
function collectTaskReviewText(taskPayload) {
|
|
1198
|
+
if (!taskPayload)
|
|
1199
|
+
return '';
|
|
1200
|
+
const brief = taskPayload.agentBrief;
|
|
1201
|
+
const parts = [
|
|
1202
|
+
taskPayload.description,
|
|
1203
|
+
taskPayload.workflowLevel,
|
|
1204
|
+
taskPayload.residualRisk,
|
|
1205
|
+
...(taskPayload.servicesTouched ?? []),
|
|
1206
|
+
...(taskPayload.filesInvolved ?? []),
|
|
1207
|
+
...(taskPayload.requiredCapabilities ?? []),
|
|
1208
|
+
...(taskPayload.skillIntents ?? []),
|
|
1209
|
+
...(taskPayload.requiredSkills ?? []),
|
|
1210
|
+
...(taskPayload.recommendedSkills ?? []),
|
|
1211
|
+
brief?.category,
|
|
1212
|
+
brief?.summary,
|
|
1213
|
+
brief?.currentBehavior,
|
|
1214
|
+
brief?.desiredBehavior,
|
|
1215
|
+
...(brief?.keyInterfaces ?? []),
|
|
1216
|
+
...(brief?.acceptanceCriteria ?? []),
|
|
1217
|
+
...(brief?.outOfScope ?? []),
|
|
1218
|
+
];
|
|
1219
|
+
return parts
|
|
1220
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
1221
|
+
.join('\n')
|
|
1222
|
+
.toLowerCase();
|
|
1223
|
+
}
|
|
1224
|
+
function isDeclaredReviewFile(path, declaredFiles) {
|
|
1225
|
+
const normalized = normalizeGitPath(path);
|
|
1226
|
+
if (declaredFiles.has(normalized))
|
|
1227
|
+
return true;
|
|
1228
|
+
for (const declared of declaredFiles) {
|
|
1229
|
+
const clean = declared.replace(/\/+$/, '');
|
|
1230
|
+
if (clean && normalized.startsWith(`${clean}/`))
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
function pathTraceableToTaskText(path, taskText) {
|
|
1236
|
+
if (!taskText)
|
|
1237
|
+
return false;
|
|
1238
|
+
const normalized = normalizeGitPath(path).toLowerCase();
|
|
1239
|
+
if (taskText.includes(normalized))
|
|
1240
|
+
return true;
|
|
1241
|
+
const ignored = new Set(['src', 'lib', 'test', 'tests', 'spec', 'index', 'main', 'types', 'utils', 'docs']);
|
|
1242
|
+
const tokens = normalized
|
|
1243
|
+
.split(/[\/_.-]+/)
|
|
1244
|
+
.map(token => token.trim())
|
|
1245
|
+
.filter(token => token.length >= 4 && !ignored.has(token));
|
|
1246
|
+
return tokens.some(token => taskText.includes(token));
|
|
1247
|
+
}
|
|
1248
|
+
function hasTaskHypotheses(taskPayload) {
|
|
1249
|
+
if (!taskPayload)
|
|
1250
|
+
return false;
|
|
1251
|
+
const brief = taskPayload.agentBrief;
|
|
1252
|
+
return Boolean((brief?.currentBehavior && brief.desiredBehavior && (brief.acceptanceCriteria?.length ?? 0) > 0) ||
|
|
1253
|
+
(taskPayload.skillIntents?.length ?? 0) > 0 ||
|
|
1254
|
+
(taskPayload.requiredSkills?.length ?? 0) > 0 ||
|
|
1255
|
+
(taskPayload.requiredCapabilities?.length ?? 0) > 0);
|
|
1256
|
+
}
|
|
1257
|
+
function hasVerifiableTaskGoal(taskPayload) {
|
|
1258
|
+
if (!taskPayload)
|
|
1259
|
+
return false;
|
|
1260
|
+
const brief = taskPayload.agentBrief;
|
|
1261
|
+
const text = collectTaskReviewText(taskPayload);
|
|
1262
|
+
return Boolean((taskPayload.verificationEvidenceIds?.length ?? 0) > 0 ||
|
|
1263
|
+
taskPayload.testPassed === true ||
|
|
1264
|
+
taskPayload.buildStatus === 'success' ||
|
|
1265
|
+
taskPayload.lintStatus === 'success' ||
|
|
1266
|
+
(brief?.acceptanceCriteria?.length ?? 0) > 0 ||
|
|
1267
|
+
/\b(verify|verified|test|coverage|acceptance|criteria|evidence|gate)\b/.test(text));
|
|
1268
|
+
}
|
|
1269
|
+
function hasOutOfScopeReviewChange(reviewedFiles, taskPayload) {
|
|
1270
|
+
const outOfScope = taskPayload?.agentBrief?.outOfScope ?? [];
|
|
1271
|
+
if (outOfScope.length === 0 || reviewedFiles.length === 0)
|
|
1272
|
+
return false;
|
|
1273
|
+
const changedText = reviewedFiles.join('\n').toLowerCase();
|
|
1274
|
+
return outOfScope
|
|
1275
|
+
.map(item => item.toLowerCase().trim())
|
|
1276
|
+
.filter(item => item.length >= 4)
|
|
1277
|
+
.some(item => changedText.includes(item));
|
|
1278
|
+
}
|
|
1279
|
+
function deriveReviewKarpathyContext(review, taskPayload) {
|
|
1280
|
+
const reviewedFiles = review.changedFiles
|
|
1281
|
+
.map(file => normalizeGitPath(file.path))
|
|
1282
|
+
.filter(path => shouldReviewFile(path));
|
|
1283
|
+
const declaredFiles = new Set((taskPayload?.filesInvolved ?? []).map(normalizeGitPath));
|
|
1284
|
+
const taskText = collectTaskReviewText(taskPayload);
|
|
1285
|
+
const changesTraceable = reviewedFiles.length === 0 || reviewedFiles.every(file => isDeclaredReviewFile(file, declaredFiles) || pathTraceableToTaskText(file, taskText));
|
|
1286
|
+
const hasExtraFeatures = hasOutOfScopeReviewChange(reviewedFiles, taskPayload) ||
|
|
1287
|
+
review.findings.some(finding => /extra|out[-\s]?of[-\s]?scope|scope creep/i.test(`${finding.category} ${finding.description}`));
|
|
1288
|
+
return {
|
|
1289
|
+
hypothesesListed: hasTaskHypotheses(taskPayload) || reviewedFiles.length === 0,
|
|
1290
|
+
hasExtraFeatures,
|
|
1291
|
+
changesTraceable,
|
|
1292
|
+
hasVerifiableGoal: hasVerifiableTaskGoal(taskPayload) || reviewedFiles.length === 0,
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
702
1295
|
// REVIEW Phase - KarpathyEvaluator + deterministic review evidence
|
|
703
1296
|
export const phaseReview = defineCommand({
|
|
704
1297
|
meta: { name: 'review', description: 'REVIEW: Code review with Karpathy Principles (/review)' },
|
|
@@ -706,6 +1299,8 @@ export const phaseReview = defineCommand({
|
|
|
706
1299
|
'task-id': { type: 'positional', required: false },
|
|
707
1300
|
'check-security': { type: 'boolean', default: true },
|
|
708
1301
|
'check-style': { type: 'boolean', default: true },
|
|
1302
|
+
format: { type: 'string', alias: 'f', description: 'Output format: html or md (default: html)' },
|
|
1303
|
+
brand: { type: 'string', description: 'Brand theme for HTML output (vercel/stripe/notion/linear/github)' },
|
|
709
1304
|
json: { type: 'boolean', default: false },
|
|
710
1305
|
},
|
|
711
1306
|
async run({ args }) {
|
|
@@ -722,19 +1317,20 @@ export const phaseReview = defineCommand({
|
|
|
722
1317
|
}
|
|
723
1318
|
taskPayload = task.payload;
|
|
724
1319
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const karpathyResult = workflowEngine.checkKarpathy(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1320
|
+
const review = await reviewGitChanges(taskPayload);
|
|
1321
|
+
const karpathyContext = deriveReviewKarpathyContext(review, taskPayload);
|
|
1322
|
+
const karpathyResult = workflowEngine.checkKarpathy(karpathyContext);
|
|
1323
|
+
const karpathyReport = {
|
|
1324
|
+
context: karpathyContext,
|
|
1325
|
+
checks: karpathyResult,
|
|
1326
|
+
passed: karpathyResult.every(check => check.passed),
|
|
1327
|
+
violations: workflowEngine.getKarpathyEvaluator().getViolations(),
|
|
1328
|
+
};
|
|
733
1329
|
if (!args.json) {
|
|
734
1330
|
console.log('\nKarpathy Principles Check:');
|
|
1331
|
+
console.log(` Derived context: hypotheses=${karpathyContext.hypothesesListed}, extraFeatures=${karpathyContext.hasExtraFeatures}, traceable=${karpathyContext.changesTraceable}, verifiableGoal=${karpathyContext.hasVerifiableGoal}`);
|
|
735
1332
|
console.log(workflowEngine.getKarpathyEvaluator().formatReport());
|
|
736
1333
|
}
|
|
737
|
-
const review = await reviewGitChanges(taskPayload);
|
|
738
1334
|
const findings = review.findings;
|
|
739
1335
|
const summary = summarizeFindings(findings);
|
|
740
1336
|
const passed = summary.critical === 0 && summary.high === 0;
|
|
@@ -742,7 +1338,7 @@ export const phaseReview = defineCommand({
|
|
|
742
1338
|
taskId: args['task-id'],
|
|
743
1339
|
passed,
|
|
744
1340
|
findings,
|
|
745
|
-
changedFiles: review.changedFiles.map(file => file.path),
|
|
1341
|
+
changedFiles: review.changedFiles.map(file => normalizeGitPath(file.path)),
|
|
746
1342
|
summary,
|
|
747
1343
|
});
|
|
748
1344
|
if (task && taskPayload) {
|
|
@@ -754,21 +1350,56 @@ export const phaseReview = defineCommand({
|
|
|
754
1350
|
};
|
|
755
1351
|
await store.update(task.id, { payload: updatedPayload });
|
|
756
1352
|
}
|
|
1353
|
+
// Generate review HTML file (default format: html)
|
|
1354
|
+
const reviewOutputFormat = args.format ?? 'md';
|
|
1355
|
+
let reviewHtmlPath;
|
|
1356
|
+
if (reviewOutputFormat === 'html') {
|
|
1357
|
+
const reviewRenderer = new HTMLDocumentRenderer({
|
|
1358
|
+
title: `Review ${record.id}`,
|
|
1359
|
+
brand: args.brand,
|
|
1360
|
+
version: '0.13.0',
|
|
1361
|
+
status: passed ? 'PASS' : 'FAIL',
|
|
1362
|
+
});
|
|
1363
|
+
const reviewHtml = reviewRenderer.renderReview({
|
|
1364
|
+
id: record.id,
|
|
1365
|
+
title: `Code Review — ${record.id}`,
|
|
1366
|
+
timestamp: new Date().toISOString(),
|
|
1367
|
+
findings: findings.map(f => ({
|
|
1368
|
+
severity: f.severity,
|
|
1369
|
+
file: f.file ?? '',
|
|
1370
|
+
message: f.description,
|
|
1371
|
+
})),
|
|
1372
|
+
passed,
|
|
1373
|
+
specCoverage: undefined,
|
|
1374
|
+
specFindings: undefined,
|
|
1375
|
+
});
|
|
1376
|
+
const reviewsDir = join(SCALE_DIR, 'reviews');
|
|
1377
|
+
ensureDir(reviewsDir);
|
|
1378
|
+
reviewHtmlPath = join(reviewsDir, `${record.id}.html`);
|
|
1379
|
+
reviewRenderer.writeToFile(reviewHtml, reviewHtmlPath);
|
|
1380
|
+
}
|
|
757
1381
|
const result = {
|
|
758
1382
|
phase: 'REVIEW',
|
|
759
1383
|
taskId: args['task-id'],
|
|
760
1384
|
reviewId: record.id,
|
|
1385
|
+
reviewHtmlPath,
|
|
761
1386
|
findings,
|
|
762
|
-
changedFiles: review.changedFiles.map(file => file.path),
|
|
1387
|
+
changedFiles: review.changedFiles.map(file => normalizeGitPath(file.path)),
|
|
763
1388
|
summary,
|
|
1389
|
+
karpathy: karpathyReport,
|
|
764
1390
|
passed,
|
|
765
|
-
|
|
1391
|
+
format: reviewOutputFormat,
|
|
1392
|
+
recommendation: passed
|
|
1393
|
+
? karpathyReport.passed ? 'Ready to ship' : 'Review passed; address Karpathy advisory warnings before release hardening'
|
|
1394
|
+
: 'Fix CRITICAL issues before shipping'
|
|
766
1395
|
};
|
|
767
1396
|
if (args.json)
|
|
768
1397
|
console.log(JSON.stringify(result, null, 2));
|
|
769
1398
|
else {
|
|
770
1399
|
console.log('\nREVIEW Phase');
|
|
771
1400
|
console.log(`\nReview evidence: ${record.id}`);
|
|
1401
|
+
if (reviewHtmlPath)
|
|
1402
|
+
console.log(`HTML report: ${reviewHtmlPath}`);
|
|
772
1403
|
console.log('\nReview Findings:');
|
|
773
1404
|
console.log('----------------------------------------');
|
|
774
1405
|
console.log(`CRITICAL: ${summary.critical} issues ${summary.critical > 0 ? 'BLOCKED' : 'OK'}`);
|
|
@@ -817,6 +1448,19 @@ export const phaseShip = defineCommand({
|
|
|
817
1448
|
(payload.testCoverage ?? 0) >= 80 &&
|
|
818
1449
|
evidenceValidation.ok;
|
|
819
1450
|
const reviewPassed = payload.reviewPassed === true && reviewValidation.ok;
|
|
1451
|
+
const artifactGatePassed = payload.artifactGateMode !== 'block' || payload.artifactGatePassed !== false;
|
|
1452
|
+
const skillGatePassed = payload.skillGatePassed !== false;
|
|
1453
|
+
if (!artifactGatePassed) {
|
|
1454
|
+
console.error('\nTask artifact gate did not pass. Complete required task artifacts and re-run: scale verify ' + args['task-id'] + ' --artifact-gate block\n');
|
|
1455
|
+
if (payload.artifactComplete === false) {
|
|
1456
|
+
console.error('Required task artifacts are incomplete.');
|
|
1457
|
+
}
|
|
1458
|
+
process.exit(1);
|
|
1459
|
+
}
|
|
1460
|
+
if (!skillGatePassed) {
|
|
1461
|
+
console.error('\nTask skill gate did not pass. Complete required skill evidence artifacts and re-run: scale verify ' + args['task-id'] + '\n');
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
}
|
|
820
1464
|
if (task.status !== 'COMPLETED') {
|
|
821
1465
|
if (!verificationPassed) {
|
|
822
1466
|
console.error('\nTask not verified with persisted evidence. Run: scale verify ' + args['task-id'] + '\n');
|