@hongmaple0820/scale-engine 0.12.3 → 0.13.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/ClaudeCodeAdapter.d.ts +1 -0
- package/dist/adapters/ClaudeCodeAdapter.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/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 +391 -5
- package/dist/api/cli.js.map +1 -1
- package/dist/api/doctor.d.ts +12 -0
- package/dist/api/doctor.js +232 -5
- package/dist/api/doctor.js.map +1 -1
- package/dist/api/quickstart.d.ts +19 -1
- package/dist/api/quickstart.js +103 -2
- 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 +61 -0
- package/dist/cli/phaseCommands.js +559 -39
- 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/SkillDiscovery.js +2 -1
- package/dist/skills/SkillDiscovery.js.map +1 -1
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +1 -0
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/routing/SkillGate.d.ts +11 -0
- package/dist/skills/routing/SkillGate.js +76 -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 +146 -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/GovernanceTemplates.d.ts +12 -0
- package/dist/workflow/GovernanceTemplates.js +515 -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 +7 -4
- package/dist/workflow/VerificationCommands.js.map +1 -1
- package/dist/workflow/VerificationProfile.d.ts +55 -0
- package/dist/workflow/VerificationProfile.js +133 -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 +20 -2
- package/dist/workflow/WorkflowEngine.js +37 -8
- package/dist/workflow/WorkflowEngine.js.map +1 -1
- 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 +7 -0
- package/dist/workflow/index.js +7 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -9,13 +9,22 @@ 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';
|
|
12
15
|
import { WorkflowEngine } from '../workflow/WorkflowEngine.js';
|
|
16
|
+
import { WorkflowArtifactWriter } from '../workflow/WorkflowArtifactWriter.js';
|
|
17
|
+
import { resolveVerificationTargets } from '../workflow/VerificationProfile.js';
|
|
13
18
|
import { EvidenceStore } from '../workflow/EvidenceStore.js';
|
|
14
19
|
import { ReviewStore } from '../workflow/ReviewStore.js';
|
|
20
|
+
import { TaskMetricsStore } from '../workflow/TaskMetricsStore.js';
|
|
21
|
+
import { appendVerificationArtifact, checkTaskArtifactCompleteness, scaffoldTaskArtifacts } from '../workflow/TaskArtifactScaffolder.js';
|
|
15
22
|
import { analyzeReview, parseChangedFiles, shouldReviewFile, summarizeFindings } from '../workflow/ReviewAnalyzer.js';
|
|
16
23
|
import { join } from 'node:path';
|
|
17
24
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { HTMLDocumentRenderer } from '../output/HTMLDocumentRenderer.js';
|
|
18
26
|
const SCALE_DIR = process.env.SCALE_DIR ?? '.scale';
|
|
27
|
+
const PROJECT_DIR = process.env.SCALE_PROJECT_DIR ?? process.cwd();
|
|
19
28
|
function validateVerificationEvidence(ids) {
|
|
20
29
|
const evidenceStore = new EvidenceStore(SCALE_DIR);
|
|
21
30
|
const missing = [];
|
|
@@ -72,11 +81,14 @@ function getEngine() {
|
|
|
72
81
|
const capabilityRegistry = new CapabilityRegistry(eventBus);
|
|
73
82
|
// Initialize skill registry
|
|
74
83
|
const skillRegistry = new SkillRegistry(eventBus);
|
|
84
|
+
registerCoreSkills(skillRegistry);
|
|
85
|
+
registerExternalSkills(skillRegistry, eventBus);
|
|
75
86
|
// Initialize workflow engine with cognitive scaffolding and quality gates.
|
|
76
87
|
const workflowEngine = new WorkflowEngine({
|
|
77
88
|
eventBus,
|
|
78
89
|
capabilityRegistry,
|
|
79
|
-
skillRegistry
|
|
90
|
+
skillRegistry,
|
|
91
|
+
scaleDir: SCALE_DIR,
|
|
80
92
|
});
|
|
81
93
|
return { eventBus, store, fsm, workflowEngine, skillRegistry };
|
|
82
94
|
}
|
|
@@ -93,6 +105,127 @@ function shouldSkipCommit(value) {
|
|
|
93
105
|
function normalizeGitPath(path) {
|
|
94
106
|
return path.replace(/\\/g, '/');
|
|
95
107
|
}
|
|
108
|
+
function normalizeWorkflowLevel(value) {
|
|
109
|
+
const normalized = String(value ?? 'M').trim().toUpperCase();
|
|
110
|
+
if (normalized === 'S' || normalized === 'M' || normalized === 'L' || normalized === 'CRITICAL') {
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Invalid workflow level "${String(value)}"; expected S, M, L, or CRITICAL.`);
|
|
114
|
+
}
|
|
115
|
+
function metricLevelFromPayload(payload) {
|
|
116
|
+
const level = normalizeWorkflowLevel(payload.workflowLevel ?? 'M');
|
|
117
|
+
return level === 'S' ? null : level;
|
|
118
|
+
}
|
|
119
|
+
function normalizeServices(value) {
|
|
120
|
+
if (!value)
|
|
121
|
+
return [];
|
|
122
|
+
return String(value)
|
|
123
|
+
.split(',')
|
|
124
|
+
.map(service => service.trim())
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
}
|
|
127
|
+
function isWorkflowGeneratedArtifact(path) {
|
|
128
|
+
return path.replace(/\\/g, '/').startsWith('docs/worklog/tasks/');
|
|
129
|
+
}
|
|
130
|
+
function checkCurrentTaskArtifacts(level) {
|
|
131
|
+
const state = new WorkflowArtifactWriter(SCALE_DIR).readCurrentState();
|
|
132
|
+
return checkTaskArtifactCompleteness({
|
|
133
|
+
projectDir: PROJECT_DIR,
|
|
134
|
+
artifactsDir: state?.artifactsDir,
|
|
135
|
+
level,
|
|
136
|
+
skillRequiredArtifacts: state?.requiredSkillArtifacts,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function planSkillsForTask(options) {
|
|
140
|
+
return createSkillPlan({
|
|
141
|
+
taskId: options.taskId,
|
|
142
|
+
taskName: options.taskName,
|
|
143
|
+
description: options.description,
|
|
144
|
+
level: options.level,
|
|
145
|
+
services: options.services ?? [],
|
|
146
|
+
files: options.files ?? [],
|
|
147
|
+
policy: loadSkillRoutingPolicy(PROJECT_DIR, SCALE_DIR),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function normalizeArtifactGateMode(value) {
|
|
151
|
+
if (value === undefined || value === null || value === '')
|
|
152
|
+
return undefined;
|
|
153
|
+
const normalized = String(value).trim().toLowerCase();
|
|
154
|
+
if (normalized === 'off' || normalized === 'warn' || normalized === 'block')
|
|
155
|
+
return normalized;
|
|
156
|
+
throw new Error(`Invalid artifact gate mode "${String(value)}"; expected off, warn, or block.`);
|
|
157
|
+
}
|
|
158
|
+
function artifactGateLevels(policy) {
|
|
159
|
+
return policy.artifactGateLevels?.length ? policy.artifactGateLevels : ['M', 'L', 'CRITICAL'];
|
|
160
|
+
}
|
|
161
|
+
function assumeVerificationArtifactWillBeWritten(check) {
|
|
162
|
+
if (!check.artifactsDir)
|
|
163
|
+
return check;
|
|
164
|
+
const missing = check.missing.filter(file => file !== 'verification.md');
|
|
165
|
+
const incomplete = check.incomplete.filter(item => item.file !== 'verification.md');
|
|
166
|
+
return {
|
|
167
|
+
...check,
|
|
168
|
+
missing,
|
|
169
|
+
incomplete,
|
|
170
|
+
complete: missing.length === 0 && incomplete.length === 0,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function evaluateArtifactGate(options) {
|
|
174
|
+
const mode = isTruthyFlag(options.requireArtifacts)
|
|
175
|
+
? 'block'
|
|
176
|
+
: normalizeArtifactGateMode(options.cliMode) ?? options.policy.artifactGate ?? 'warn';
|
|
177
|
+
const levels = artifactGateLevels(options.policy);
|
|
178
|
+
const applies = Boolean(options.level && levels.includes(options.level));
|
|
179
|
+
const checked = applies && mode !== 'off' && Boolean(options.check);
|
|
180
|
+
const complete = checked ? options.check?.complete : undefined;
|
|
181
|
+
return {
|
|
182
|
+
mode,
|
|
183
|
+
levels,
|
|
184
|
+
applies,
|
|
185
|
+
checked,
|
|
186
|
+
complete,
|
|
187
|
+
blocked: mode === 'block' && checked && complete === false,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function countChangedFiles(taskPayload) {
|
|
191
|
+
if (taskPayload.filesInvolved.length > 0)
|
|
192
|
+
return new Set(taskPayload.filesInvolved.map(normalizeGitPath)).size;
|
|
193
|
+
try {
|
|
194
|
+
const status = await runGit(['status', '--short']);
|
|
195
|
+
const untracked = await runGit(['ls-files', '--others', '--exclude-standard']);
|
|
196
|
+
const statusOutput = mergeUntrackedFilesIntoStatus(status.stdout, untracked.stdout);
|
|
197
|
+
return parseChangedFiles(statusOutput)
|
|
198
|
+
.filter(file => shouldReviewFile(file.path))
|
|
199
|
+
.filter(file => !isWorkflowGeneratedArtifact(file.path))
|
|
200
|
+
.length;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function recordVerificationMetric(options) {
|
|
207
|
+
const level = metricLevelFromPayload(options.taskPayload);
|
|
208
|
+
if (!level)
|
|
209
|
+
return null;
|
|
210
|
+
const services = options.taskPayload.servicesTouched?.length
|
|
211
|
+
? options.taskPayload.servicesTouched
|
|
212
|
+
: options.serviceNames ?? [];
|
|
213
|
+
const metricsStore = new TaskMetricsStore(SCALE_DIR);
|
|
214
|
+
const artifactCheck = options.artifactCheck ?? checkCurrentTaskArtifacts(level);
|
|
215
|
+
const record = metricsStore.recordVerification({
|
|
216
|
+
taskId: options.taskId,
|
|
217
|
+
taskName: options.taskName,
|
|
218
|
+
level,
|
|
219
|
+
services,
|
|
220
|
+
filesChanged: await countChangedFiles(options.taskPayload),
|
|
221
|
+
passed: options.passed,
|
|
222
|
+
artifactComplete: artifactCheck.complete,
|
|
223
|
+
residualRisk: options.taskPayload.residualRisk,
|
|
224
|
+
finalGateStatus: options.finalGateStatus,
|
|
225
|
+
});
|
|
226
|
+
metricsStore.writeMarkdownReport(PROJECT_DIR);
|
|
227
|
+
return record;
|
|
228
|
+
}
|
|
96
229
|
// Helper: Generate spec markdown file
|
|
97
230
|
function generateSpecMarkdown(id, title, payload) {
|
|
98
231
|
return `# Spec: ${title}
|
|
@@ -146,6 +279,8 @@ export const phaseDefine = defineCommand({
|
|
|
146
279
|
'context': { type: 'string', description: 'Context answer for Socratic refinement' },
|
|
147
280
|
'risk': { type: 'string', description: 'Risk answer for Socratic refinement' },
|
|
148
281
|
'priority': { type: 'string', description: 'Priority answer for Socratic refinement' },
|
|
282
|
+
format: { type: 'string', alias: 'f', description: 'Output format: html or md (default: html)' },
|
|
283
|
+
brand: { type: 'string', description: 'Brand theme for HTML output (vercel/stripe/notion/linear/github)' },
|
|
149
284
|
json: { type: 'boolean', default: false },
|
|
150
285
|
},
|
|
151
286
|
async run({ args }) {
|
|
@@ -157,7 +292,7 @@ export const phaseDefine = defineCommand({
|
|
|
157
292
|
: ['Feature works as described', 'No regression in existing functionality'];
|
|
158
293
|
// === WorkflowEngine Integration ===
|
|
159
294
|
// Step 1: Explore with AmbiguityScorer + SocraticQuestioner
|
|
160
|
-
const exploreResult = await workflowEngine.explore(desc);
|
|
295
|
+
const exploreResult = await workflowEngine.explore(desc, { persistArtifact: false, runGate: false });
|
|
161
296
|
const ambiguityResult = workflowEngine.getAmbiguityScorer().analyzeRequirement(desc);
|
|
162
297
|
// Step 2: Check if requirement needs refinement.
|
|
163
298
|
if (ambiguityResult.blocked) {
|
|
@@ -251,6 +386,29 @@ export const phaseDefine = defineCommand({
|
|
|
251
386
|
ensureDir(specsDir);
|
|
252
387
|
const specPath = join(specsDir, `${spec.id}.md`);
|
|
253
388
|
writeFileSync(specPath, generateSpecMarkdown(spec.id, args.title, specPayload));
|
|
389
|
+
// Generate spec HTML file (default format: html)
|
|
390
|
+
const outputFormat = args.format ?? 'md';
|
|
391
|
+
let specHtmlPath;
|
|
392
|
+
if (outputFormat === 'html') {
|
|
393
|
+
const renderer = new HTMLDocumentRenderer({
|
|
394
|
+
title: args.title,
|
|
395
|
+
brand: args.brand,
|
|
396
|
+
version: '0.13.0',
|
|
397
|
+
status: 'FROZEN',
|
|
398
|
+
});
|
|
399
|
+
const html = renderer.renderSpec({
|
|
400
|
+
id: spec.id,
|
|
401
|
+
title: args.title,
|
|
402
|
+
what: refinedRequirement,
|
|
403
|
+
successCriteria,
|
|
404
|
+
outOfScope: specPayload.outOfScope,
|
|
405
|
+
edgeCases: specPayload.edgeCases,
|
|
406
|
+
northStar: specPayload.northStar,
|
|
407
|
+
ambiguityScore,
|
|
408
|
+
});
|
|
409
|
+
specHtmlPath = join(specsDir, `${spec.id}.html`);
|
|
410
|
+
renderer.writeToFile(html, specHtmlPath);
|
|
411
|
+
}
|
|
254
412
|
// FSM transitions: DRAFT -> REVIEWING -> FROZEN
|
|
255
413
|
// Phase 1: refine (DRAFT -> REVIEWING) - no guards
|
|
256
414
|
const refineResult = await fsm.canTransition(spec.id, 'refine');
|
|
@@ -277,12 +435,24 @@ export const phaseDefine = defineCommand({
|
|
|
277
435
|
if (!args.json) {
|
|
278
436
|
console.log(' FSM: DRAFT -> REVIEWING -> FROZEN ✓');
|
|
279
437
|
}
|
|
280
|
-
const result = { phase: 'DEFINE', spec, specPath, ambiguityScore, successCriteria };
|
|
438
|
+
const result = { phase: 'DEFINE', spec, specPath, specHtmlPath, ambiguityScore, successCriteria, format: outputFormat };
|
|
439
|
+
// Write explore artifact for Gate G1 verification
|
|
440
|
+
const artifactWriter = new WorkflowArtifactWriter(SCALE_DIR);
|
|
441
|
+
artifactWriter.writeExploreResult({
|
|
442
|
+
timestamp: new Date().toISOString(),
|
|
443
|
+
files: [specPath],
|
|
444
|
+
fileCount: 1,
|
|
445
|
+
mainContradiction: refinedRequirement !== desc ? 'requirement ambiguity resolved via Socratic refinement' : '',
|
|
446
|
+
ambiguityScore,
|
|
447
|
+
socraticCompleted: !ambiguityResult.requiresQuestioning || (ambiguityResult.requiresQuestioning && !exploreResult.socraticSession),
|
|
448
|
+
});
|
|
281
449
|
if (args.json)
|
|
282
450
|
console.log(JSON.stringify(result, null, 2));
|
|
283
451
|
else {
|
|
284
452
|
console.log(`\nDEFINE: ${spec.id}`);
|
|
285
453
|
console.log(` Spec file: ${specPath}`);
|
|
454
|
+
if (specHtmlPath)
|
|
455
|
+
console.log(` HTML file: ${specHtmlPath}`);
|
|
286
456
|
console.log(` Ambiguity score: ${ambiguityScore.toFixed(2)}`);
|
|
287
457
|
console.log(` Success criteria: ${successCriteria.length}`);
|
|
288
458
|
console.log(`\n Next: scale plan ${spec.id}\n`);
|
|
@@ -322,6 +492,8 @@ export const phasePlan = defineCommand({
|
|
|
322
492
|
'spec-id': { type: 'positional', required: true },
|
|
323
493
|
approach: { type: 'string', alias: 'a', description: 'Implementation approach' },
|
|
324
494
|
'rollback': { type: 'string', alias: 'r', description: 'Rollback strategy (required for FSM)' },
|
|
495
|
+
format: { type: 'string', alias: 'f', description: 'Output format: html or md (default: html)' },
|
|
496
|
+
brand: { type: 'string', description: 'Brand theme for HTML output (vercel/stripe/notion/linear/github)' },
|
|
325
497
|
json: { type: 'boolean', default: false },
|
|
326
498
|
},
|
|
327
499
|
async run({ args }) {
|
|
@@ -335,7 +507,7 @@ export const phasePlan = defineCommand({
|
|
|
335
507
|
// === WorkflowEngine Integration ===
|
|
336
508
|
// Step 1: Run ConsensusPlanner (Planner -> Architect -> Critic).
|
|
337
509
|
const specDesc = spec.payload.what;
|
|
338
|
-
const consensusResult = await workflowEngine.plan(specDesc);
|
|
510
|
+
const consensusResult = await workflowEngine.plan(specDesc, { persistArtifact: false, runGate: false });
|
|
339
511
|
// Step 2: Display RALPLAN-DR output
|
|
340
512
|
if (!args.json) {
|
|
341
513
|
console.log('\nConsensus Planning Result:');
|
|
@@ -364,6 +536,41 @@ export const phasePlan = defineCommand({
|
|
|
364
536
|
ensureDir(plansDir);
|
|
365
537
|
const planPath = join(plansDir, `${plan.id}.md`);
|
|
366
538
|
writeFileSync(planPath, generatePlanMarkdown(plan.id, args['spec-id'], planPayload));
|
|
539
|
+
// Generate plan HTML file (default format: html)
|
|
540
|
+
const planOutputFormat = args.format ?? 'md';
|
|
541
|
+
let planHtmlPath;
|
|
542
|
+
if (planOutputFormat === 'html') {
|
|
543
|
+
const planRenderer = new HTMLDocumentRenderer({
|
|
544
|
+
title: `Plan ${plan.id}`,
|
|
545
|
+
brand: args.brand,
|
|
546
|
+
version: '0.13.0',
|
|
547
|
+
status: 'APPROVED',
|
|
548
|
+
});
|
|
549
|
+
const planHtml = planRenderer.renderPlan({
|
|
550
|
+
id: plan.id,
|
|
551
|
+
specId: args['spec-id'],
|
|
552
|
+
approach: planPayload.approach,
|
|
553
|
+
techChoices: planPayload.techChoices,
|
|
554
|
+
modules: planPayload.modules,
|
|
555
|
+
rollbackStrategy: planPayload.rollbackStrategy,
|
|
556
|
+
estimatedComplexity: planPayload.estimatedComplexity,
|
|
557
|
+
});
|
|
558
|
+
planHtmlPath = join(plansDir, `${plan.id}.html`);
|
|
559
|
+
planRenderer.writeToFile(planHtml, planHtmlPath);
|
|
560
|
+
}
|
|
561
|
+
// Write plan artifact for Gate G2 verification
|
|
562
|
+
const artifactWriter = new WorkflowArtifactWriter(SCALE_DIR);
|
|
563
|
+
artifactWriter.writePlanResult({
|
|
564
|
+
timestamp: new Date().toISOString(),
|
|
565
|
+
planId: plan.id,
|
|
566
|
+
specId: args['spec-id'],
|
|
567
|
+
hasBoundaryAnalysis: consensusResult.viableOptions.length > 1,
|
|
568
|
+
hasExceptionHandling: consensusResult.preMortem.rootCauses.length > 0,
|
|
569
|
+
hasRollbackStrategy: !!rollbackStrategy,
|
|
570
|
+
modules: planPayload.modules.map(m => m.path),
|
|
571
|
+
consensusRounds: consensusResult.iterationCount,
|
|
572
|
+
verdict: consensusResult.verdict,
|
|
573
|
+
});
|
|
367
574
|
// FSM transition: DRAFT -> APPROVED (requires rollbackStrategy guard)
|
|
368
575
|
const reviewResult = await fsm.canTransition(plan.id, 'review');
|
|
369
576
|
if (!reviewResult.allowed) {
|
|
@@ -379,12 +586,14 @@ export const phasePlan = defineCommand({
|
|
|
379
586
|
if (!args.json) {
|
|
380
587
|
console.log(' FSM: DRAFT -> APPROVED ✓');
|
|
381
588
|
}
|
|
382
|
-
const result = { phase: 'PLAN', plan, planPath, rollbackStrategy };
|
|
589
|
+
const result = { phase: 'PLAN', plan, planPath, planHtmlPath, rollbackStrategy, format: planOutputFormat };
|
|
383
590
|
if (args.json)
|
|
384
591
|
console.log(JSON.stringify(result, null, 2));
|
|
385
592
|
else {
|
|
386
593
|
console.log(`\nPLAN: ${plan.id}`);
|
|
387
594
|
console.log(` Plan file: ${planPath}`);
|
|
595
|
+
if (planHtmlPath)
|
|
596
|
+
console.log(` HTML file: ${planHtmlPath}`);
|
|
388
597
|
console.log(` Rollback: ${rollbackStrategy}`);
|
|
389
598
|
console.log(`\n Next: scale build ${plan.id}\n`);
|
|
390
599
|
}
|
|
@@ -396,6 +605,9 @@ export const phaseBuild = defineCommand({
|
|
|
396
605
|
args: {
|
|
397
606
|
'plan-id': { type: 'positional', required: true },
|
|
398
607
|
description: { type: 'string', alias: 'd', description: 'Task description' },
|
|
608
|
+
level: { type: 'string', default: 'M', description: 'Workflow task level: S, M, L, or CRITICAL' },
|
|
609
|
+
service: { type: 'string', description: 'Comma-separated service names touched by this task' },
|
|
610
|
+
'residual-risk': { type: 'string', description: 'Known residual risk statement for metrics' },
|
|
399
611
|
json: { type: 'boolean', default: false },
|
|
400
612
|
},
|
|
401
613
|
async run({ args }) {
|
|
@@ -406,9 +618,20 @@ export const phaseBuild = defineCommand({
|
|
|
406
618
|
console.error(`\nPlan not found: ${args['plan-id']}\n`);
|
|
407
619
|
process.exit(1);
|
|
408
620
|
}
|
|
621
|
+
let workflowLevel;
|
|
622
|
+
try {
|
|
623
|
+
workflowLevel = normalizeWorkflowLevel(args.level);
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
console.error(`\n${e.message}\n`);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
409
629
|
// Create TaskPayload
|
|
410
630
|
const taskPayload = {
|
|
411
631
|
description: args.description ?? `Implement ${plan.title}`,
|
|
632
|
+
workflowLevel,
|
|
633
|
+
servicesTouched: normalizeServices(args.service),
|
|
634
|
+
residualRisk: args['residual-risk'],
|
|
412
635
|
filesInvolved: [],
|
|
413
636
|
dependsOn: [],
|
|
414
637
|
requiredRole: 'implementer',
|
|
@@ -428,13 +651,60 @@ export const phaseBuild = defineCommand({
|
|
|
428
651
|
outOfScope: [],
|
|
429
652
|
},
|
|
430
653
|
};
|
|
654
|
+
const taskTitle = `Task for ${plan.title}`;
|
|
431
655
|
const task = await store.create({
|
|
432
|
-
type: 'Task', title:
|
|
656
|
+
type: 'Task', title: taskTitle,
|
|
433
657
|
payload: taskPayload,
|
|
434
658
|
parents: [args['plan-id']],
|
|
435
659
|
initialStatus: 'PENDING',
|
|
436
660
|
createdBy: { kind: 'human', userId: 'cli' },
|
|
437
661
|
});
|
|
662
|
+
const skillPlan = planSkillsForTask({
|
|
663
|
+
taskId: task.id,
|
|
664
|
+
taskName: taskTitle,
|
|
665
|
+
description: taskPayload.description,
|
|
666
|
+
level: workflowLevel,
|
|
667
|
+
services: taskPayload.servicesTouched,
|
|
668
|
+
files: taskPayload.filesInvolved,
|
|
669
|
+
});
|
|
670
|
+
const taskPayloadWithSkills = {
|
|
671
|
+
...taskPayload,
|
|
672
|
+
skillIntents: skillPlan.intents.map(intent => intent.domain),
|
|
673
|
+
skillRoutingMode: skillPlan.mode,
|
|
674
|
+
skillPlanRequired: skillPlan.required,
|
|
675
|
+
requiredSkills: skillPlan.requiredSkills,
|
|
676
|
+
recommendedSkills: skillPlan.recommendedSkills,
|
|
677
|
+
requiredSkillArtifacts: skillPlan.requiredArtifacts,
|
|
678
|
+
requiredSkillVerification: skillPlan.requiredVerification,
|
|
679
|
+
};
|
|
680
|
+
await store.update(task.id, { payload: taskPayloadWithSkills });
|
|
681
|
+
let taskArtifacts;
|
|
682
|
+
if (workflowLevel !== 'S') {
|
|
683
|
+
taskArtifacts = scaffoldTaskArtifacts({
|
|
684
|
+
projectDir: PROJECT_DIR,
|
|
685
|
+
taskId: task.id,
|
|
686
|
+
taskName: task.title,
|
|
687
|
+
description: taskPayloadWithSkills.description,
|
|
688
|
+
level: workflowLevel,
|
|
689
|
+
services: taskPayloadWithSkills.servicesTouched,
|
|
690
|
+
skillPlan,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
new WorkflowArtifactWriter(SCALE_DIR).updateCurrentState({
|
|
694
|
+
taskId: task.id,
|
|
695
|
+
level: workflowLevel,
|
|
696
|
+
phase: 'build',
|
|
697
|
+
lastTaskId: task.id,
|
|
698
|
+
artifactsDir: taskArtifacts?.relativeDir,
|
|
699
|
+
skillIntents: skillPlan.intents.map(intent => intent.domain),
|
|
700
|
+
skillRoutingMode: skillPlan.mode,
|
|
701
|
+
skillPlanRequired: skillPlan.required,
|
|
702
|
+
skillPlanPath: taskArtifacts?.relativeDir ? `${taskArtifacts.relativeDir}/skill-plan.md` : undefined,
|
|
703
|
+
requiredSkills: skillPlan.requiredSkills,
|
|
704
|
+
recommendedSkills: skillPlan.recommendedSkills,
|
|
705
|
+
requiredSkillArtifacts: skillPlan.requiredArtifacts,
|
|
706
|
+
requiredSkillVerification: skillPlan.requiredVerification,
|
|
707
|
+
});
|
|
438
708
|
// FSM transitions: PENDING -> READY -> RUNNING
|
|
439
709
|
// Phase 1: schedule (PENDING -> READY) - no guards
|
|
440
710
|
const scheduleResult = await fsm.canTransition(task.id, 'schedule');
|
|
@@ -456,13 +726,21 @@ export const phaseBuild = defineCommand({
|
|
|
456
726
|
if (implResult.allowed) {
|
|
457
727
|
await fsm.transition(args['plan-id'], 'implement', { actor: { kind: 'system', component: 'phase-build' } });
|
|
458
728
|
}
|
|
459
|
-
const result = { phase: 'BUILD', task, status: 'RUNNING' };
|
|
729
|
+
const result = { phase: 'BUILD', task: { ...task, payload: taskPayloadWithSkills }, status: 'RUNNING', artifactDir: taskArtifacts?.relativeDir, artifactFiles: taskArtifacts?.created ?? [], skillPlan };
|
|
460
730
|
if (args.json)
|
|
461
731
|
console.log(JSON.stringify(result, null, 2));
|
|
462
732
|
else {
|
|
463
733
|
console.log(`\nBUILD: ${task.id}`);
|
|
464
734
|
console.log(` Status: RUNNING (ready to implement)`);
|
|
465
|
-
console.log(` Description: ${
|
|
735
|
+
console.log(` Description: ${taskPayloadWithSkills.description}`);
|
|
736
|
+
if (skillPlan.intents.length)
|
|
737
|
+
console.log(` Skill intents: ${skillPlan.intents.map(intent => intent.domain).join(', ')}`);
|
|
738
|
+
if (skillPlan.requiredSkills.length)
|
|
739
|
+
console.log(` Required skills: ${skillPlan.requiredSkills.join(', ')}`);
|
|
740
|
+
if (skillPlan.recommendedSkills.length)
|
|
741
|
+
console.log(` Recommended skills: ${skillPlan.recommendedSkills.join(', ')}`);
|
|
742
|
+
if (taskArtifacts?.relativeDir)
|
|
743
|
+
console.log(` Artifacts: ${taskArtifacts.relativeDir}`);
|
|
466
744
|
console.log(`\n Implement now, then run: scale verify ${task.id}\n`);
|
|
467
745
|
}
|
|
468
746
|
},
|
|
@@ -487,8 +765,13 @@ export const phaseVerify = defineCommand({
|
|
|
487
765
|
'lint-cmd': { type: 'string', description: 'Override lint command' },
|
|
488
766
|
'test-cmd': { type: 'string', description: 'Override test command' },
|
|
489
767
|
'coverage-cmd': { type: 'string', description: 'Override coverage command' },
|
|
768
|
+
profile: { type: 'string', description: 'Verification profile from .scale/verification.json' },
|
|
769
|
+
service: { type: 'string', description: 'Service name from .scale/verification.json' },
|
|
770
|
+
'artifact-gate': { type: 'string', description: 'Task artifact policy override: off, warn, or block' },
|
|
771
|
+
'require-artifacts': { type: 'boolean', default: false, description: 'Fail verification when required M/L/CRITICAL artifacts are incomplete' },
|
|
490
772
|
'tdd-evidence': { type: 'string', description: 'Path to JSON TDD evidence with red/green/refactor/testFirst=true' },
|
|
491
773
|
'tdd-strict': { type: 'boolean', default: false, description: 'Require TDD evidence before other gates' },
|
|
774
|
+
'residual-risk': { type: 'string', description: 'Residual risk statement to record in task metrics' },
|
|
492
775
|
'skip-build': { type: 'boolean', default: false },
|
|
493
776
|
'skip-lint': { type: 'boolean', default: false },
|
|
494
777
|
'skip-test': { type: 'boolean', default: false },
|
|
@@ -506,14 +789,38 @@ export const phaseVerify = defineCommand({
|
|
|
506
789
|
// Step 1: Run GateSystem G3-G7
|
|
507
790
|
if (!args.json)
|
|
508
791
|
console.log('\nRunning Quality Gates...');
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
tddEvidence: args['tdd-evidence'],
|
|
515
|
-
tddStrict: isTruthyFlag(args['tdd-strict']),
|
|
792
|
+
const resolvedVerification = resolveVerificationTargets({
|
|
793
|
+
projectDir: PROJECT_DIR,
|
|
794
|
+
scaleDir: SCALE_DIR,
|
|
795
|
+
profile: args.profile,
|
|
796
|
+
service: args.service,
|
|
516
797
|
});
|
|
798
|
+
if (!args.json) {
|
|
799
|
+
for (const warning of resolvedVerification.warnings)
|
|
800
|
+
console.log(` [WARN] ${warning}`);
|
|
801
|
+
for (const target of resolvedVerification.targets) {
|
|
802
|
+
if (target.service) {
|
|
803
|
+
console.log(` Service: ${target.service.name} (${target.service.path})`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
console.log(` Profile: ${resolvedVerification.profileName}`);
|
|
807
|
+
}
|
|
808
|
+
const gateResults = [];
|
|
809
|
+
for (const target of resolvedVerification.targets) {
|
|
810
|
+
if (!args.json && resolvedVerification.targets.length > 1) {
|
|
811
|
+
console.log(`\n Target: ${target.service?.name ?? 'root'}`);
|
|
812
|
+
}
|
|
813
|
+
const targetResults = await workflowEngine.verify({
|
|
814
|
+
cwd: target.config.cwd,
|
|
815
|
+
build: args['build-cmd'] ?? target.config.build,
|
|
816
|
+
lint: args['lint-cmd'] ?? target.config.lint,
|
|
817
|
+
test: args['test-cmd'] ?? target.config.test,
|
|
818
|
+
coverage: args['coverage-cmd'] ?? target.config.coverage,
|
|
819
|
+
tddEvidence: args['tdd-evidence'],
|
|
820
|
+
tddStrict: isTruthyFlag(args['tdd-strict']),
|
|
821
|
+
});
|
|
822
|
+
gateResults.push(...targetResults);
|
|
823
|
+
}
|
|
517
824
|
// Step 2: Display gate results
|
|
518
825
|
if (!args.json) {
|
|
519
826
|
console.log('\nGate Results:');
|
|
@@ -525,28 +832,51 @@ export const phaseVerify = defineCommand({
|
|
|
525
832
|
}
|
|
526
833
|
}
|
|
527
834
|
// Extract results from gateResults
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
const
|
|
835
|
+
const g0Results = gateResults.filter(g => g.gate === 'G0');
|
|
836
|
+
const g4Results = gateResults.filter(g => g.gate === 'G4');
|
|
837
|
+
const g5Results = gateResults.filter(g => g.gate === 'G5');
|
|
838
|
+
const g6Results = gateResults.filter(g => g.gate === 'G6');
|
|
839
|
+
const g7Results = gateResults.filter(g => g.gate === 'G7');
|
|
840
|
+
const gatePassed = (results) => results.length > 0 && results.every(result => result.passed);
|
|
841
|
+
const buildExitCodes = g0Results
|
|
842
|
+
.flatMap(result => result.evidenceItems ?? [])
|
|
843
|
+
.filter(item => item.kind === 'command')
|
|
844
|
+
.map(item => item.exitCode)
|
|
845
|
+
.filter((code) => typeof code === 'number');
|
|
533
846
|
const results = {
|
|
534
|
-
buildStatus:
|
|
535
|
-
buildExitCode:
|
|
536
|
-
lintStatus:
|
|
537
|
-
testPassed:
|
|
847
|
+
buildStatus: gatePassed(g0Results) ? 'success' : 'failed',
|
|
848
|
+
buildExitCode: buildExitCodes.find(code => code !== 0) ?? (buildExitCodes.length > 0 ? 0 : undefined),
|
|
849
|
+
lintStatus: gatePassed(g4Results) ? 'success' : 'failed',
|
|
850
|
+
testPassed: gatePassed(g5Results),
|
|
538
851
|
testCoverage: undefined,
|
|
539
|
-
securityPassed:
|
|
852
|
+
securityPassed: gatePassed(g7Results),
|
|
540
853
|
};
|
|
541
854
|
const verificationEvidenceIds = gateResults
|
|
542
855
|
.map(g => g.evidenceRecordId)
|
|
543
856
|
.filter((id) => Boolean(id));
|
|
544
857
|
// Extract coverage from G6 evidence
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
858
|
+
const coverageValues = g6Results
|
|
859
|
+
.map(result => result.evidence.match(/Coverage: (\d+\.?\d*)%/))
|
|
860
|
+
.filter((match) => Boolean(match))
|
|
861
|
+
.map(match => parseFloat(match[1]));
|
|
862
|
+
if (coverageValues.length > 0)
|
|
863
|
+
results.testCoverage = Math.min(...coverageValues);
|
|
548
864
|
// Update Task payload with verification results
|
|
549
865
|
const currentPayload = task.payload;
|
|
866
|
+
const taskLevel = normalizeWorkflowLevel(currentPayload.workflowLevel ?? 'M');
|
|
867
|
+
const verificationSkillPlan = taskLevel === 'S'
|
|
868
|
+
? undefined
|
|
869
|
+
: planSkillsForTask({
|
|
870
|
+
taskId: args['task-id'],
|
|
871
|
+
taskName: task.title,
|
|
872
|
+
description: currentPayload.description,
|
|
873
|
+
level: taskLevel,
|
|
874
|
+
services: currentPayload.servicesTouched,
|
|
875
|
+
files: currentPayload.filesInvolved,
|
|
876
|
+
});
|
|
877
|
+
const verifiedServices = resolvedVerification.targets
|
|
878
|
+
.map(target => target.service?.name)
|
|
879
|
+
.filter((service) => Boolean(service));
|
|
550
880
|
const updatedPayload = {
|
|
551
881
|
...currentPayload,
|
|
552
882
|
buildStatus: results.buildStatus,
|
|
@@ -554,20 +884,64 @@ export const phaseVerify = defineCommand({
|
|
|
554
884
|
lintStatus: results.lintStatus,
|
|
555
885
|
testPassed: results.testPassed,
|
|
556
886
|
testCoverage: results.testCoverage,
|
|
887
|
+
servicesTouched: currentPayload.servicesTouched?.length
|
|
888
|
+
? currentPayload.servicesTouched
|
|
889
|
+
: verifiedServices.length > 0 ? verifiedServices : currentPayload.servicesTouched,
|
|
890
|
+
residualRisk: args['residual-risk'] ?? currentPayload.residualRisk,
|
|
557
891
|
verificationEvidenceIds,
|
|
892
|
+
skillIntents: verificationSkillPlan?.intents.map(intent => intent.domain) ?? currentPayload.skillIntents,
|
|
893
|
+
skillRoutingMode: verificationSkillPlan?.mode ?? currentPayload.skillRoutingMode,
|
|
894
|
+
skillPlanRequired: verificationSkillPlan?.required ?? currentPayload.skillPlanRequired,
|
|
895
|
+
requiredSkills: verificationSkillPlan?.requiredSkills ?? currentPayload.requiredSkills,
|
|
896
|
+
recommendedSkills: verificationSkillPlan?.recommendedSkills ?? currentPayload.recommendedSkills,
|
|
897
|
+
requiredSkillArtifacts: verificationSkillPlan?.requiredArtifacts ?? currentPayload.requiredSkillArtifacts,
|
|
898
|
+
requiredSkillVerification: verificationSkillPlan?.requiredVerification ?? currentPayload.requiredSkillVerification,
|
|
558
899
|
verifiedAt: Date.now(),
|
|
559
900
|
};
|
|
560
901
|
await store.update(args['task-id'], { payload: updatedPayload });
|
|
902
|
+
const workflowState = new WorkflowArtifactWriter(SCALE_DIR).updateCurrentState({
|
|
903
|
+
taskId: args['task-id'],
|
|
904
|
+
phase: 'verify',
|
|
905
|
+
lastTaskId: args['task-id'],
|
|
906
|
+
filesModified: updatedPayload.filesInvolved,
|
|
907
|
+
skillIntents: updatedPayload.skillIntents,
|
|
908
|
+
skillRoutingMode: updatedPayload.skillRoutingMode,
|
|
909
|
+
skillPlanRequired: updatedPayload.skillPlanRequired,
|
|
910
|
+
requiredSkills: updatedPayload.requiredSkills,
|
|
911
|
+
recommendedSkills: updatedPayload.recommendedSkills,
|
|
912
|
+
requiredSkillArtifacts: updatedPayload.requiredSkillArtifacts,
|
|
913
|
+
requiredSkillVerification: updatedPayload.requiredSkillVerification,
|
|
914
|
+
});
|
|
915
|
+
const metricLevel = metricLevelFromPayload(updatedPayload);
|
|
916
|
+
const preArtifactCheck = metricLevel ? checkCurrentTaskArtifacts(metricLevel) : undefined;
|
|
917
|
+
const artifactGate = evaluateArtifactGate({
|
|
918
|
+
policy: resolvedVerification.policy,
|
|
919
|
+
level: metricLevel,
|
|
920
|
+
check: preArtifactCheck ? assumeVerificationArtifactWillBeWritten(preArtifactCheck) : undefined,
|
|
921
|
+
cliMode: args['artifact-gate'],
|
|
922
|
+
requireArtifacts: args['require-artifacts'],
|
|
923
|
+
});
|
|
924
|
+
const skillPolicy = loadSkillRoutingPolicy(PROJECT_DIR, SCALE_DIR);
|
|
925
|
+
const skillGate = metricLevel && verificationSkillPlan
|
|
926
|
+
? evaluateSkillGate({
|
|
927
|
+
projectDir: PROJECT_DIR,
|
|
928
|
+
artifactsDir: workflowState.artifactsDir,
|
|
929
|
+
level: metricLevel,
|
|
930
|
+
plan: verificationSkillPlan,
|
|
931
|
+
enforceLevels: skillPolicy.policy.enforceLevels,
|
|
932
|
+
})
|
|
933
|
+
: undefined;
|
|
561
934
|
// Attempt FSM transition to COMPLETED
|
|
562
|
-
// Guards: build_passed, lint_passed, tests_passed
|
|
563
|
-
const
|
|
935
|
+
// Guards: build_passed, lint_passed, tests_passed, and optional artifact policy.
|
|
936
|
+
const codePassed = results.buildStatus === 'success' &&
|
|
564
937
|
(results.buildExitCode ?? 1) === 0 &&
|
|
565
938
|
results.lintStatus === 'success' &&
|
|
566
939
|
results.testPassed === true &&
|
|
567
940
|
(results.testCoverage ?? 0) >= 80 &&
|
|
568
941
|
results.securityPassed === true;
|
|
942
|
+
const completionEligible = codePassed && !artifactGate.blocked && !(skillGate?.blocked ?? false);
|
|
569
943
|
let transitionResult = null;
|
|
570
|
-
if (
|
|
944
|
+
if (completionEligible) {
|
|
571
945
|
const completeResult = await fsm.canTransition(args['task-id'], 'complete');
|
|
572
946
|
if (!completeResult.allowed) {
|
|
573
947
|
if (!args.json) {
|
|
@@ -582,18 +956,92 @@ export const phaseVerify = defineCommand({
|
|
|
582
956
|
actor: { kind: 'human', userId: 'cli' }
|
|
583
957
|
});
|
|
584
958
|
if (!args.json)
|
|
585
|
-
console.log('\n FSM: RUNNING -> COMPLETED
|
|
959
|
+
console.log('\n FSM: RUNNING -> COMPLETED');
|
|
586
960
|
}
|
|
587
961
|
}
|
|
588
|
-
else if (!args.json) {
|
|
962
|
+
else if (!args.json && !codePassed) {
|
|
589
963
|
console.log('\n Verification requirements not met - cannot complete Task');
|
|
590
964
|
}
|
|
591
|
-
|
|
592
|
-
|
|
965
|
+
else if (!args.json && artifactGate.blocked) {
|
|
966
|
+
console.log('\n Artifact gate blocked completion - required task artifacts are incomplete');
|
|
967
|
+
}
|
|
968
|
+
else if (!args.json && skillGate?.blocked) {
|
|
969
|
+
console.log('\n Skill gate blocked completion - required skill evidence artifacts are incomplete');
|
|
970
|
+
}
|
|
971
|
+
const passed = completionEligible && (transitionResult?.success ?? false);
|
|
972
|
+
const verificationArtifactPath = appendVerificationArtifact({
|
|
973
|
+
projectDir: PROJECT_DIR,
|
|
974
|
+
artifactsDir: workflowState.artifactsDir,
|
|
975
|
+
taskId: args['task-id'],
|
|
976
|
+
profile: resolvedVerification.profileName,
|
|
977
|
+
services: verifiedServices,
|
|
978
|
+
gateResults,
|
|
979
|
+
passed,
|
|
980
|
+
});
|
|
981
|
+
const artifactCheck = metricLevel ? checkCurrentTaskArtifacts(metricLevel) : undefined;
|
|
982
|
+
const finalArtifactGate = artifactCheck
|
|
983
|
+
? evaluateArtifactGate({
|
|
984
|
+
policy: resolvedVerification.policy,
|
|
985
|
+
level: metricLevel,
|
|
986
|
+
check: artifactCheck,
|
|
987
|
+
cliMode: args['artifact-gate'],
|
|
988
|
+
requireArtifacts: args['require-artifacts'],
|
|
989
|
+
})
|
|
990
|
+
: artifactGate;
|
|
991
|
+
const finalSkillGate = metricLevel && verificationSkillPlan
|
|
992
|
+
? evaluateSkillGate({
|
|
993
|
+
projectDir: PROJECT_DIR,
|
|
994
|
+
artifactsDir: workflowState.artifactsDir,
|
|
995
|
+
level: metricLevel,
|
|
996
|
+
plan: verificationSkillPlan,
|
|
997
|
+
enforceLevels: skillPolicy.policy.enforceLevels,
|
|
998
|
+
})
|
|
999
|
+
: skillGate;
|
|
1000
|
+
const finalPayload = {
|
|
1001
|
+
...updatedPayload,
|
|
1002
|
+
artifactGateMode: finalArtifactGate.mode,
|
|
1003
|
+
artifactGatePassed: !finalArtifactGate.blocked,
|
|
1004
|
+
artifactComplete: artifactCheck?.complete,
|
|
1005
|
+
skillGatePassed: finalSkillGate ? !finalSkillGate.blocked : undefined,
|
|
1006
|
+
};
|
|
1007
|
+
await store.update(args['task-id'], { payload: finalPayload });
|
|
1008
|
+
const metricGateStatus = codePassed && (finalArtifactGate.blocked || finalSkillGate?.blocked) ? 'blocked' : undefined;
|
|
1009
|
+
const metricRecord = await recordVerificationMetric({
|
|
1010
|
+
taskId: args['task-id'],
|
|
1011
|
+
taskName: task.title,
|
|
1012
|
+
taskPayload: finalPayload,
|
|
1013
|
+
passed,
|
|
1014
|
+
serviceNames: verifiedServices,
|
|
1015
|
+
artifactCheck,
|
|
1016
|
+
finalGateStatus: metricGateStatus,
|
|
1017
|
+
});
|
|
1018
|
+
const result = {
|
|
1019
|
+
phase: 'VERIFY',
|
|
1020
|
+
taskId: args['task-id'],
|
|
1021
|
+
profile: resolvedVerification.profileName,
|
|
1022
|
+
service: verifiedServices.length === 1 ? verifiedServices[0] : undefined,
|
|
1023
|
+
services: verifiedServices,
|
|
1024
|
+
results,
|
|
1025
|
+
evidenceIds: verificationEvidenceIds,
|
|
1026
|
+
verificationArtifactPath,
|
|
1027
|
+
artifactCheck,
|
|
1028
|
+
artifactGate: finalArtifactGate,
|
|
1029
|
+
skillGate: finalSkillGate,
|
|
1030
|
+
metric: metricRecord,
|
|
1031
|
+
passed
|
|
1032
|
+
};
|
|
593
1033
|
if (args.json)
|
|
594
1034
|
console.log(JSON.stringify(result, null, 2));
|
|
595
1035
|
else {
|
|
596
1036
|
console.log(`\nVERIFY: ${passed ? 'PASSED' : 'FAILED'}`);
|
|
1037
|
+
if (metricRecord)
|
|
1038
|
+
console.log(` Metrics: ${metricRecord.taskId} ${metricRecord.finalGateStatus} (fix iterations: ${metricRecord.fixIterations})`);
|
|
1039
|
+
if (artifactCheck && !artifactCheck.complete) {
|
|
1040
|
+
console.log(` Artifact gaps: ${artifactCheck.missing.length} missing, ${artifactCheck.incomplete.length} incomplete`);
|
|
1041
|
+
}
|
|
1042
|
+
if (finalSkillGate && !finalSkillGate.complete) {
|
|
1043
|
+
console.log(` Skill evidence gaps: ${finalSkillGate.missing.length} missing, ${finalSkillGate.incomplete.length} incomplete`);
|
|
1044
|
+
}
|
|
597
1045
|
if (passed)
|
|
598
1046
|
console.log(`\n Next: scale review\n`);
|
|
599
1047
|
else
|
|
@@ -612,12 +1060,14 @@ async function runGit(args) {
|
|
|
612
1060
|
}
|
|
613
1061
|
function mergeUntrackedFilesIntoStatus(statusOutput, untrackedOutput) {
|
|
614
1062
|
const existing = new Set(parseChangedFiles(statusOutput).map(file => file.path.replace(/\\/g, '/')));
|
|
1063
|
+
// Add '??' status marker for untracked files so parseChangedFiles can recognize them
|
|
615
1064
|
const additions = untrackedOutput
|
|
616
1065
|
.split('\n')
|
|
617
1066
|
.map(line => line.trim())
|
|
618
1067
|
.filter(Boolean)
|
|
619
1068
|
.filter(path => shouldReviewFile(path))
|
|
620
|
-
.filter(path => !existing.has(path.replace(/\\/g, '/')))
|
|
1069
|
+
.filter(path => !existing.has(path.replace(/\\/g, '/')))
|
|
1070
|
+
.map(path => `?? ${path}`); // Add status marker
|
|
621
1071
|
return [statusOutput.trim(), ...additions].filter(Boolean).join('\n');
|
|
622
1072
|
}
|
|
623
1073
|
function readUntrackedFileAsDiff(path) {
|
|
@@ -641,7 +1091,23 @@ function readUntrackedFileAsDiff(path) {
|
|
|
641
1091
|
async function reviewGitChanges(taskPayload) {
|
|
642
1092
|
const status = await runGit(['status', '--short']);
|
|
643
1093
|
const untracked = await runGit(['ls-files', '--others', '--exclude-standard']);
|
|
644
|
-
|
|
1094
|
+
let statusOutput = mergeUntrackedFilesIntoStatus(status.stdout, untracked.stdout);
|
|
1095
|
+
// Scope review to task-relevant files only.
|
|
1096
|
+
// When filesInvolved is set, only analyze those files.
|
|
1097
|
+
// When empty, only analyze untracked (new) files to avoid picking up
|
|
1098
|
+
// unrelated modifications from a dirty working tree.
|
|
1099
|
+
if (taskPayload?.filesInvolved?.length) {
|
|
1100
|
+
const involved = new Set(taskPayload.filesInvolved.map(f => f.replace(/\\/g, '/')));
|
|
1101
|
+
statusOutput = statusOutput.split('\n').filter(line => {
|
|
1102
|
+
const parsed = parseChangedFiles(line);
|
|
1103
|
+
return parsed.length > 0 && involved.has(parsed[0].path.replace(/\\/g, '/'));
|
|
1104
|
+
}).join('\n');
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
// Only include untracked files (status '??') — skip tracked modifications
|
|
1108
|
+
// that may be unrelated to the task under review.
|
|
1109
|
+
statusOutput = statusOutput.split('\n').filter(line => line.startsWith('??')).join('\n');
|
|
1110
|
+
}
|
|
645
1111
|
const verificationEvidence = getVerificationEvidenceSummary(taskPayload?.verificationEvidenceIds);
|
|
646
1112
|
const changedFiles = analyzeReview({ statusOutput, diffs: [], taskPayload, verificationEvidence }).changedFiles;
|
|
647
1113
|
const diffs = [];
|
|
@@ -679,6 +1145,12 @@ async function stageReviewedFiles(reviewRecords) {
|
|
|
679
1145
|
const currentChanges = await getReviewableGitChanges();
|
|
680
1146
|
const stagedFiles = [];
|
|
681
1147
|
const unreviewedFiles = [];
|
|
1148
|
+
// Edge case: if currentChanges is empty but reviewedFiles has files that should be staged,
|
|
1149
|
+
// this indicates files were deleted or moved. Treat reviewed but missing files as unreviewed.
|
|
1150
|
+
if (currentChanges.length === 0 && reviewedFiles.size > 0) {
|
|
1151
|
+
// No changes to stage, but we have review records - this is a pass (nothing to commit)
|
|
1152
|
+
return { stagedFiles: [], unreviewedFiles: [] };
|
|
1153
|
+
}
|
|
682
1154
|
for (const file of currentChanges) {
|
|
683
1155
|
const normalizedPath = normalizeGitPath(file.path);
|
|
684
1156
|
if (reviewedFiles.has(normalizedPath)) {
|
|
@@ -688,6 +1160,7 @@ async function stageReviewedFiles(reviewRecords) {
|
|
|
688
1160
|
unreviewedFiles.push(file.path);
|
|
689
1161
|
}
|
|
690
1162
|
}
|
|
1163
|
+
// Only block if there are actual unreviewed changes
|
|
691
1164
|
if (unreviewedFiles.length > 0) {
|
|
692
1165
|
return { stagedFiles: [], unreviewedFiles };
|
|
693
1166
|
}
|
|
@@ -706,6 +1179,8 @@ export const phaseReview = defineCommand({
|
|
|
706
1179
|
'task-id': { type: 'positional', required: false },
|
|
707
1180
|
'check-security': { type: 'boolean', default: true },
|
|
708
1181
|
'check-style': { type: 'boolean', default: true },
|
|
1182
|
+
format: { type: 'string', alias: 'f', description: 'Output format: html or md (default: html)' },
|
|
1183
|
+
brand: { type: 'string', description: 'Brand theme for HTML output (vercel/stripe/notion/linear/github)' },
|
|
709
1184
|
json: { type: 'boolean', default: false },
|
|
710
1185
|
},
|
|
711
1186
|
async run({ args }) {
|
|
@@ -742,7 +1217,7 @@ export const phaseReview = defineCommand({
|
|
|
742
1217
|
taskId: args['task-id'],
|
|
743
1218
|
passed,
|
|
744
1219
|
findings,
|
|
745
|
-
changedFiles: review.changedFiles.map(file => file.path),
|
|
1220
|
+
changedFiles: review.changedFiles.map(file => normalizeGitPath(file.path)),
|
|
746
1221
|
summary,
|
|
747
1222
|
});
|
|
748
1223
|
if (task && taskPayload) {
|
|
@@ -754,14 +1229,44 @@ export const phaseReview = defineCommand({
|
|
|
754
1229
|
};
|
|
755
1230
|
await store.update(task.id, { payload: updatedPayload });
|
|
756
1231
|
}
|
|
1232
|
+
// Generate review HTML file (default format: html)
|
|
1233
|
+
const reviewOutputFormat = args.format ?? 'md';
|
|
1234
|
+
let reviewHtmlPath;
|
|
1235
|
+
if (reviewOutputFormat === 'html') {
|
|
1236
|
+
const reviewRenderer = new HTMLDocumentRenderer({
|
|
1237
|
+
title: `Review ${record.id}`,
|
|
1238
|
+
brand: args.brand,
|
|
1239
|
+
version: '0.13.0',
|
|
1240
|
+
status: passed ? 'PASS' : 'FAIL',
|
|
1241
|
+
});
|
|
1242
|
+
const reviewHtml = reviewRenderer.renderReview({
|
|
1243
|
+
id: record.id,
|
|
1244
|
+
title: `Code Review — ${record.id}`,
|
|
1245
|
+
timestamp: new Date().toISOString(),
|
|
1246
|
+
findings: findings.map(f => ({
|
|
1247
|
+
severity: f.severity,
|
|
1248
|
+
file: f.file ?? '',
|
|
1249
|
+
message: f.description,
|
|
1250
|
+
})),
|
|
1251
|
+
passed,
|
|
1252
|
+
specCoverage: undefined,
|
|
1253
|
+
specFindings: undefined,
|
|
1254
|
+
});
|
|
1255
|
+
const reviewsDir = join(SCALE_DIR, 'reviews');
|
|
1256
|
+
ensureDir(reviewsDir);
|
|
1257
|
+
reviewHtmlPath = join(reviewsDir, `${record.id}.html`);
|
|
1258
|
+
reviewRenderer.writeToFile(reviewHtml, reviewHtmlPath);
|
|
1259
|
+
}
|
|
757
1260
|
const result = {
|
|
758
1261
|
phase: 'REVIEW',
|
|
759
1262
|
taskId: args['task-id'],
|
|
760
1263
|
reviewId: record.id,
|
|
1264
|
+
reviewHtmlPath,
|
|
761
1265
|
findings,
|
|
762
|
-
changedFiles: review.changedFiles.map(file => file.path),
|
|
1266
|
+
changedFiles: review.changedFiles.map(file => normalizeGitPath(file.path)),
|
|
763
1267
|
summary,
|
|
764
1268
|
passed,
|
|
1269
|
+
format: reviewOutputFormat,
|
|
765
1270
|
recommendation: passed ? 'Ready to ship' : 'Fix CRITICAL issues before shipping'
|
|
766
1271
|
};
|
|
767
1272
|
if (args.json)
|
|
@@ -769,6 +1274,8 @@ export const phaseReview = defineCommand({
|
|
|
769
1274
|
else {
|
|
770
1275
|
console.log('\nREVIEW Phase');
|
|
771
1276
|
console.log(`\nReview evidence: ${record.id}`);
|
|
1277
|
+
if (reviewHtmlPath)
|
|
1278
|
+
console.log(`HTML report: ${reviewHtmlPath}`);
|
|
772
1279
|
console.log('\nReview Findings:');
|
|
773
1280
|
console.log('----------------------------------------');
|
|
774
1281
|
console.log(`CRITICAL: ${summary.critical} issues ${summary.critical > 0 ? 'BLOCKED' : 'OK'}`);
|
|
@@ -817,6 +1324,19 @@ export const phaseShip = defineCommand({
|
|
|
817
1324
|
(payload.testCoverage ?? 0) >= 80 &&
|
|
818
1325
|
evidenceValidation.ok;
|
|
819
1326
|
const reviewPassed = payload.reviewPassed === true && reviewValidation.ok;
|
|
1327
|
+
const artifactGatePassed = payload.artifactGateMode !== 'block' || payload.artifactGatePassed !== false;
|
|
1328
|
+
const skillGatePassed = payload.skillGatePassed !== false;
|
|
1329
|
+
if (!artifactGatePassed) {
|
|
1330
|
+
console.error('\nTask artifact gate did not pass. Complete required task artifacts and re-run: scale verify ' + args['task-id'] + ' --artifact-gate block\n');
|
|
1331
|
+
if (payload.artifactComplete === false) {
|
|
1332
|
+
console.error('Required task artifacts are incomplete.');
|
|
1333
|
+
}
|
|
1334
|
+
process.exit(1);
|
|
1335
|
+
}
|
|
1336
|
+
if (!skillGatePassed) {
|
|
1337
|
+
console.error('\nTask skill gate did not pass. Complete required skill evidence artifacts and re-run: scale verify ' + args['task-id'] + '\n');
|
|
1338
|
+
process.exit(1);
|
|
1339
|
+
}
|
|
820
1340
|
if (task.status !== 'COMPLETED') {
|
|
821
1341
|
if (!verificationPassed) {
|
|
822
1342
|
console.error('\nTask not verified with persisted evidence. Run: scale verify ' + args['task-id'] + '\n');
|