@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.
Files changed (157) hide show
  1. package/dist/adapters/AiderAdapter.js +1 -1
  2. package/dist/adapters/AiderAdapter.js.map +1 -1
  3. package/dist/adapters/ClaudeCodeAdapter.d.ts +1 -0
  4. package/dist/adapters/ClaudeCodeAdapter.js +5 -3
  5. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  6. package/dist/adapters/CodexAdapter.js +1 -1
  7. package/dist/adapters/CodexAdapter.js.map +1 -1
  8. package/dist/adapters/CursorAdapter.js +1 -1
  9. package/dist/adapters/CursorAdapter.js.map +1 -1
  10. package/dist/adapters/DeepSeekTuiAdapter.js +5 -3
  11. package/dist/adapters/DeepSeekTuiAdapter.js.map +1 -1
  12. package/dist/adapters/DoubaoAdapter.js +1 -1
  13. package/dist/adapters/DoubaoAdapter.js.map +1 -1
  14. package/dist/adapters/GeminiAdapter.js +1 -1
  15. package/dist/adapters/GeminiAdapter.js.map +1 -1
  16. package/dist/adapters/HermesAdapter.js +1 -1
  17. package/dist/adapters/HermesAdapter.js.map +1 -1
  18. package/dist/adapters/KimiAdapter.js +1 -1
  19. package/dist/adapters/KimiAdapter.js.map +1 -1
  20. package/dist/adapters/KiroAdapter.d.ts +14 -0
  21. package/dist/adapters/KiroAdapter.js +180 -0
  22. package/dist/adapters/KiroAdapter.js.map +1 -0
  23. package/dist/adapters/OpenClawAdapter.js +1 -1
  24. package/dist/adapters/OpenClawAdapter.js.map +1 -1
  25. package/dist/adapters/OpenCodeAdapter.js +1 -1
  26. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  27. package/dist/adapters/QCoderAdapter.js +1 -1
  28. package/dist/adapters/QCoderAdapter.js.map +1 -1
  29. package/dist/adapters/TraeAdapter.js +1 -1
  30. package/dist/adapters/TraeAdapter.js.map +1 -1
  31. package/dist/adapters/VSCAdapter.js +1 -1
  32. package/dist/adapters/VSCAdapter.js.map +1 -1
  33. package/dist/adapters/WindsurfAdapter.js +1 -1
  34. package/dist/adapters/WindsurfAdapter.js.map +1 -1
  35. package/dist/adapters/WorkBuddyAdapter.js +1 -1
  36. package/dist/adapters/WorkBuddyAdapter.js.map +1 -1
  37. package/dist/adapters/index.d.ts +1 -0
  38. package/dist/adapters/index.js +3 -0
  39. package/dist/adapters/index.js.map +1 -1
  40. package/dist/api/cli.js +690 -9
  41. package/dist/api/cli.js.map +1 -1
  42. package/dist/api/doctor.d.ts +13 -0
  43. package/dist/api/doctor.js +261 -5
  44. package/dist/api/doctor.js.map +1 -1
  45. package/dist/api/quickstart.d.ts +20 -1
  46. package/dist/api/quickstart.js +104 -3
  47. package/dist/api/quickstart.js.map +1 -1
  48. package/dist/artifact/types.d.ts +16 -2
  49. package/dist/artifact/types.js.map +1 -1
  50. package/dist/cli/phaseCommands.d.ts +66 -0
  51. package/dist/cli/phaseCommands.js +695 -51
  52. package/dist/cli/phaseCommands.js.map +1 -1
  53. package/dist/guardrails/detectors.d.ts +9 -0
  54. package/dist/guardrails/detectors.js +102 -0
  55. package/dist/guardrails/detectors.js.map +1 -1
  56. package/dist/hooks/HookGeneratorEnhanced.js +29 -0
  57. package/dist/hooks/HookGeneratorEnhanced.js.map +1 -1
  58. package/dist/hooks/WorkflowHooksManager.js +20 -1
  59. package/dist/hooks/WorkflowHooksManager.js.map +1 -1
  60. package/dist/index.d.ts +6 -0
  61. package/dist/index.js +4 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/output/BrandThemeLoader.d.ts +54 -0
  64. package/dist/output/BrandThemeLoader.js +340 -0
  65. package/dist/output/BrandThemeLoader.js.map +1 -0
  66. package/dist/output/HTMLDocumentRenderer.d.ts +83 -0
  67. package/dist/output/HTMLDocumentRenderer.js +717 -0
  68. package/dist/output/HTMLDocumentRenderer.js.map +1 -0
  69. package/dist/output/UIPrototypeRenderer.d.ts +61 -0
  70. package/dist/output/UIPrototypeRenderer.js +500 -0
  71. package/dist/output/UIPrototypeRenderer.js.map +1 -0
  72. package/dist/output/index.d.ts +6 -0
  73. package/dist/output/index.js +6 -0
  74. package/dist/output/index.js.map +1 -0
  75. package/dist/skills/ExternalSkills.js +2 -0
  76. package/dist/skills/ExternalSkills.js.map +1 -1
  77. package/dist/skills/SkillCatalog.d.ts +13 -0
  78. package/dist/skills/SkillCatalog.js +184 -0
  79. package/dist/skills/SkillCatalog.js.map +1 -0
  80. package/dist/skills/SkillDiscovery.js +2 -1
  81. package/dist/skills/SkillDiscovery.js.map +1 -1
  82. package/dist/skills/SkillDoctor.d.ts +37 -0
  83. package/dist/skills/SkillDoctor.js +90 -0
  84. package/dist/skills/SkillDoctor.js.map +1 -0
  85. package/dist/skills/index.d.ts +2 -0
  86. package/dist/skills/index.js +2 -0
  87. package/dist/skills/index.js.map +1 -1
  88. package/dist/skills/routing/SkillGate.d.ts +12 -0
  89. package/dist/skills/routing/SkillGate.js +93 -0
  90. package/dist/skills/routing/SkillGate.js.map +1 -0
  91. package/dist/skills/routing/SkillPlanner.d.ts +8 -0
  92. package/dist/skills/routing/SkillPlanner.js +91 -0
  93. package/dist/skills/routing/SkillPlanner.js.map +1 -0
  94. package/dist/skills/routing/SkillPolicy.d.ts +6 -0
  95. package/dist/skills/routing/SkillPolicy.js +173 -0
  96. package/dist/skills/routing/SkillPolicy.js.map +1 -0
  97. package/dist/skills/routing/SkillRoutingTypes.d.ts +72 -0
  98. package/dist/skills/routing/SkillRoutingTypes.js +2 -0
  99. package/dist/skills/routing/SkillRoutingTypes.js.map +1 -0
  100. package/dist/skills/routing/TaskIntentClassifier.d.ts +6 -0
  101. package/dist/skills/routing/TaskIntentClassifier.js +79 -0
  102. package/dist/skills/routing/TaskIntentClassifier.js.map +1 -0
  103. package/dist/skills/routing/index.d.ts +5 -0
  104. package/dist/skills/routing/index.js +6 -0
  105. package/dist/skills/routing/index.js.map +1 -0
  106. package/dist/workflow/GovernanceLock.d.ts +35 -0
  107. package/dist/workflow/GovernanceLock.js +58 -0
  108. package/dist/workflow/GovernanceLock.js.map +1 -0
  109. package/dist/workflow/GovernanceTemplatePacks.d.ts +24 -0
  110. package/dist/workflow/GovernanceTemplatePacks.js +83 -0
  111. package/dist/workflow/GovernanceTemplatePacks.js.map +1 -0
  112. package/dist/workflow/GovernanceTemplates.d.ts +17 -0
  113. package/dist/workflow/GovernanceTemplates.js +686 -0
  114. package/dist/workflow/GovernanceTemplates.js.map +1 -0
  115. package/dist/workflow/PhaseMarkerTracker.d.ts +63 -0
  116. package/dist/workflow/PhaseMarkerTracker.js +291 -0
  117. package/dist/workflow/PhaseMarkerTracker.js.map +1 -0
  118. package/dist/workflow/SessionStateTracker.d.ts +74 -0
  119. package/dist/workflow/SessionStateTracker.js +270 -0
  120. package/dist/workflow/SessionStateTracker.js.map +1 -0
  121. package/dist/workflow/TaskArtifactScaffolder.d.ts +47 -0
  122. package/dist/workflow/TaskArtifactScaffolder.js +237 -0
  123. package/dist/workflow/TaskArtifactScaffolder.js.map +1 -0
  124. package/dist/workflow/TaskMetricsStore.d.ts +49 -0
  125. package/dist/workflow/TaskMetricsStore.js +149 -0
  126. package/dist/workflow/TaskMetricsStore.js.map +1 -0
  127. package/dist/workflow/VerificationCommands.d.ts +2 -0
  128. package/dist/workflow/VerificationCommands.js +8 -12
  129. package/dist/workflow/VerificationCommands.js.map +1 -1
  130. package/dist/workflow/VerificationProfile.d.ts +56 -0
  131. package/dist/workflow/VerificationProfile.js +170 -0
  132. package/dist/workflow/VerificationProfile.js.map +1 -0
  133. package/dist/workflow/WorkflowArtifactWriter.d.ts +113 -0
  134. package/dist/workflow/WorkflowArtifactWriter.js +241 -0
  135. package/dist/workflow/WorkflowArtifactWriter.js.map +1 -0
  136. package/dist/workflow/WorkflowEngine.d.ts +26 -5
  137. package/dist/workflow/WorkflowEngine.js +38 -9
  138. package/dist/workflow/WorkflowEngine.js.map +1 -1
  139. package/dist/workflow/WorkspaceLifecycle.d.ts +51 -0
  140. package/dist/workflow/WorkspaceLifecycle.js +259 -0
  141. package/dist/workflow/WorkspaceLifecycle.js.map +1 -0
  142. package/dist/workflow/autonomous/AutonomousDevLoop.d.ts +88 -0
  143. package/dist/workflow/autonomous/AutonomousDevLoop.js +381 -0
  144. package/dist/workflow/autonomous/AutonomousDevLoop.js.map +1 -0
  145. package/dist/workflow/autonomous/WorklogManager.d.ts +50 -0
  146. package/dist/workflow/autonomous/WorklogManager.js +264 -0
  147. package/dist/workflow/autonomous/WorklogManager.js.map +1 -0
  148. package/dist/workflow/autonomous/index.d.ts +2 -0
  149. package/dist/workflow/autonomous/index.js +4 -0
  150. package/dist/workflow/autonomous/index.js.map +1 -0
  151. package/dist/workflow/gates/GateSystem.d.ts +12 -3
  152. package/dist/workflow/gates/GateSystem.js +185 -41
  153. package/dist/workflow/gates/GateSystem.js.map +1 -1
  154. package/dist/workflow/index.d.ts +10 -0
  155. package/dist/workflow/index.js +10 -0
  156. package/dist/workflow/index.js.map +1 -1
  157. 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: `Task for ${plan.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: ${taskPayload.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 gateResults = await workflowEngine.verify({
510
- build: args['build-cmd'],
511
- lint: args['lint-cmd'],
512
- test: args['test-cmd'],
513
- coverage: args['coverage-cmd'],
514
- tddEvidence: args['tdd-evidence'],
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 g0Result = gateResults.find(g => g.gate === 'G0');
529
- const g4Result = gateResults.find(g => g.gate === 'G4');
530
- const g5Result = gateResults.find(g => g.gate === 'G5');
531
- const g6Result = gateResults.find(g => g.gate === 'G6');
532
- const g7Result = gateResults.find(g => g.gate === 'G7');
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: g0Result?.passed ? 'success' : 'failed',
535
- buildExitCode: g0Result?.evidenceItems?.find(item => item.kind === 'command')?.exitCode,
536
- lintStatus: g4Result?.passed ? 'success' : 'failed',
537
- testPassed: g5Result?.passed,
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: g7Result?.passed,
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 coverageMatch = g6Result?.evidence.match(/Coverage: (\d+\.?\d*)%/);
546
- if (coverageMatch)
547
- results.testCoverage = parseFloat(coverageMatch[1]);
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 currentPayload = task.payload;
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 allPassed = results.buildStatus === 'success' &&
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 (allPassed) {
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
- const passed = allPassed && (transitionResult?.success ?? false);
592
- const result = { phase: 'VERIFY', taskId: args['task-id'], results, evidenceIds: verificationEvidenceIds, passed };
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
- const statusOutput = mergeUntrackedFilesIntoStatus(status.stdout, untracked.stdout);
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
- // === WorkflowEngine Integration ===
726
- // Step 1: Karpathy Principles Check
727
- const karpathyResult = workflowEngine.checkKarpathy({
728
- hypothesesListed: true, // Would be determined from actual context
729
- hasExtraFeatures: false, // Would be determined from actual context
730
- changesTraceable: true, // Would be determined from actual context
731
- hasVerifiableGoal: true // Would be determined from actual context
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
- recommendation: passed ? 'Ready to ship' : 'Fix CRITICAL issues before shipping'
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');