@hongmaple0820/scale-engine 0.12.2 → 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.
Files changed (116) hide show
  1. package/README.md +30 -2
  2. package/dist/adapters/ClaudeCodeAdapter.d.ts +1 -0
  3. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  4. package/dist/adapters/KiroAdapter.d.ts +14 -0
  5. package/dist/adapters/KiroAdapter.js +180 -0
  6. package/dist/adapters/KiroAdapter.js.map +1 -0
  7. package/dist/adapters/index.d.ts +1 -0
  8. package/dist/adapters/index.js +3 -0
  9. package/dist/adapters/index.js.map +1 -1
  10. package/dist/api/cli.js +396 -5
  11. package/dist/api/cli.js.map +1 -1
  12. package/dist/api/doctor.d.ts +12 -0
  13. package/dist/api/doctor.js +232 -5
  14. package/dist/api/doctor.js.map +1 -1
  15. package/dist/api/quickstart.d.ts +19 -1
  16. package/dist/api/quickstart.js +103 -2
  17. package/dist/api/quickstart.js.map +1 -1
  18. package/dist/artifact/types.d.ts +16 -2
  19. package/dist/artifact/types.js.map +1 -1
  20. package/dist/cli/phaseCommands.d.ts +61 -0
  21. package/dist/cli/phaseCommands.js +559 -39
  22. package/dist/cli/phaseCommands.js.map +1 -1
  23. package/dist/cli/vibeCommands.d.ts +44 -0
  24. package/dist/cli/vibeCommands.js +244 -0
  25. package/dist/cli/vibeCommands.js.map +1 -0
  26. package/dist/guardrails/detectors.d.ts +9 -0
  27. package/dist/guardrails/detectors.js +102 -0
  28. package/dist/guardrails/detectors.js.map +1 -1
  29. package/dist/hooks/HookGeneratorEnhanced.js +29 -0
  30. package/dist/hooks/HookGeneratorEnhanced.js.map +1 -1
  31. package/dist/hooks/WorkflowHooksManager.js +20 -1
  32. package/dist/hooks/WorkflowHooksManager.js.map +1 -1
  33. package/dist/index.d.ts +6 -0
  34. package/dist/index.js +4 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/output/BrandThemeLoader.d.ts +54 -0
  37. package/dist/output/BrandThemeLoader.js +340 -0
  38. package/dist/output/BrandThemeLoader.js.map +1 -0
  39. package/dist/output/HTMLDocumentRenderer.d.ts +83 -0
  40. package/dist/output/HTMLDocumentRenderer.js +717 -0
  41. package/dist/output/HTMLDocumentRenderer.js.map +1 -0
  42. package/dist/output/UIPrototypeRenderer.d.ts +61 -0
  43. package/dist/output/UIPrototypeRenderer.js +500 -0
  44. package/dist/output/UIPrototypeRenderer.js.map +1 -0
  45. package/dist/output/index.d.ts +6 -0
  46. package/dist/output/index.js +6 -0
  47. package/dist/output/index.js.map +1 -0
  48. package/dist/prompts/PhasePromptRegistry.d.ts +53 -0
  49. package/dist/prompts/PhasePromptRegistry.js +517 -0
  50. package/dist/prompts/PhasePromptRegistry.js.map +1 -0
  51. package/dist/skills/SkillDiscovery.js +2 -1
  52. package/dist/skills/SkillDiscovery.js.map +1 -1
  53. package/dist/skills/index.d.ts +1 -0
  54. package/dist/skills/index.js +1 -0
  55. package/dist/skills/index.js.map +1 -1
  56. package/dist/skills/routing/SkillGate.d.ts +11 -0
  57. package/dist/skills/routing/SkillGate.js +76 -0
  58. package/dist/skills/routing/SkillGate.js.map +1 -0
  59. package/dist/skills/routing/SkillPlanner.d.ts +8 -0
  60. package/dist/skills/routing/SkillPlanner.js +91 -0
  61. package/dist/skills/routing/SkillPlanner.js.map +1 -0
  62. package/dist/skills/routing/SkillPolicy.d.ts +6 -0
  63. package/dist/skills/routing/SkillPolicy.js +146 -0
  64. package/dist/skills/routing/SkillPolicy.js.map +1 -0
  65. package/dist/skills/routing/SkillRoutingTypes.d.ts +72 -0
  66. package/dist/skills/routing/SkillRoutingTypes.js +2 -0
  67. package/dist/skills/routing/SkillRoutingTypes.js.map +1 -0
  68. package/dist/skills/routing/TaskIntentClassifier.d.ts +6 -0
  69. package/dist/skills/routing/TaskIntentClassifier.js +79 -0
  70. package/dist/skills/routing/TaskIntentClassifier.js.map +1 -0
  71. package/dist/skills/routing/index.d.ts +5 -0
  72. package/dist/skills/routing/index.js +6 -0
  73. package/dist/skills/routing/index.js.map +1 -0
  74. package/dist/workflow/GovernanceTemplates.d.ts +12 -0
  75. package/dist/workflow/GovernanceTemplates.js +515 -0
  76. package/dist/workflow/GovernanceTemplates.js.map +1 -0
  77. package/dist/workflow/PhaseMarkerTracker.d.ts +63 -0
  78. package/dist/workflow/PhaseMarkerTracker.js +291 -0
  79. package/dist/workflow/PhaseMarkerTracker.js.map +1 -0
  80. package/dist/workflow/SessionStateTracker.d.ts +74 -0
  81. package/dist/workflow/SessionStateTracker.js +270 -0
  82. package/dist/workflow/SessionStateTracker.js.map +1 -0
  83. package/dist/workflow/TaskArtifactScaffolder.d.ts +47 -0
  84. package/dist/workflow/TaskArtifactScaffolder.js +237 -0
  85. package/dist/workflow/TaskArtifactScaffolder.js.map +1 -0
  86. package/dist/workflow/TaskMetricsStore.d.ts +49 -0
  87. package/dist/workflow/TaskMetricsStore.js +149 -0
  88. package/dist/workflow/TaskMetricsStore.js.map +1 -0
  89. package/dist/workflow/VerificationCommands.d.ts +2 -0
  90. package/dist/workflow/VerificationCommands.js +7 -4
  91. package/dist/workflow/VerificationCommands.js.map +1 -1
  92. package/dist/workflow/VerificationProfile.d.ts +55 -0
  93. package/dist/workflow/VerificationProfile.js +133 -0
  94. package/dist/workflow/VerificationProfile.js.map +1 -0
  95. package/dist/workflow/WorkflowArtifactWriter.d.ts +113 -0
  96. package/dist/workflow/WorkflowArtifactWriter.js +241 -0
  97. package/dist/workflow/WorkflowArtifactWriter.js.map +1 -0
  98. package/dist/workflow/WorkflowEngine.d.ts +20 -2
  99. package/dist/workflow/WorkflowEngine.js +37 -8
  100. package/dist/workflow/WorkflowEngine.js.map +1 -1
  101. package/dist/workflow/autonomous/AutonomousDevLoop.d.ts +88 -0
  102. package/dist/workflow/autonomous/AutonomousDevLoop.js +381 -0
  103. package/dist/workflow/autonomous/AutonomousDevLoop.js.map +1 -0
  104. package/dist/workflow/autonomous/WorklogManager.d.ts +50 -0
  105. package/dist/workflow/autonomous/WorklogManager.js +264 -0
  106. package/dist/workflow/autonomous/WorklogManager.js.map +1 -0
  107. package/dist/workflow/autonomous/index.d.ts +2 -0
  108. package/dist/workflow/autonomous/index.js +4 -0
  109. package/dist/workflow/autonomous/index.js.map +1 -0
  110. package/dist/workflow/gates/GateSystem.d.ts +12 -3
  111. package/dist/workflow/gates/GateSystem.js +185 -41
  112. package/dist/workflow/gates/GateSystem.js.map +1 -1
  113. package/dist/workflow/index.d.ts +7 -0
  114. package/dist/workflow/index.js +7 -0
  115. package/dist/workflow/index.js.map +1 -1
  116. 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: `Task for ${plan.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: ${taskPayload.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 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']),
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 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');
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: g0Result?.passed ? 'success' : 'failed',
535
- buildExitCode: g0Result?.evidenceItems?.find(item => item.kind === 'command')?.exitCode,
536
- lintStatus: g4Result?.passed ? 'success' : 'failed',
537
- testPassed: g5Result?.passed,
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: g7Result?.passed,
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 coverageMatch = g6Result?.evidence.match(/Coverage: (\d+\.?\d*)%/);
546
- if (coverageMatch)
547
- results.testCoverage = parseFloat(coverageMatch[1]);
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 allPassed = results.buildStatus === 'success' &&
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 (allPassed) {
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
- const passed = allPassed && (transitionResult?.success ?? false);
592
- const result = { phase: 'VERIFY', taskId: args['task-id'], results, evidenceIds: verificationEvidenceIds, passed };
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
- const statusOutput = mergeUntrackedFilesIntoStatus(status.stdout, untracked.stdout);
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');