@hongmaple0820/scale-engine 0.10.0 → 0.11.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 (40) hide show
  1. package/README.en.md +127 -196
  2. package/README.md +168 -1114
  3. package/dist/api/cli.js +2 -2
  4. package/dist/api/cli.js.map +1 -1
  5. package/dist/artifact/types.d.ts +1 -1
  6. package/dist/artifact/types.js.map +1 -1
  7. package/dist/capabilities/BrowserQACapability.d.ts +151 -0
  8. package/dist/capabilities/BrowserQACapability.js +344 -0
  9. package/dist/capabilities/BrowserQACapability.js.map +1 -0
  10. package/dist/cli/evolutionCommands.d.ts +112 -0
  11. package/dist/cli/evolutionCommands.js +246 -0
  12. package/dist/cli/evolutionCommands.js.map +1 -0
  13. package/dist/cli/phaseCommands.d.ts +9 -0
  14. package/dist/cli/phaseCommands.js +169 -48
  15. package/dist/cli/phaseCommands.js.map +1 -1
  16. package/dist/guardrails/OWASPDetector.d.ts +58 -0
  17. package/dist/guardrails/OWASPDetector.js +508 -0
  18. package/dist/guardrails/OWASPDetector.js.map +1 -0
  19. package/dist/workflow/ReviewAnalyzer.d.ts +5 -0
  20. package/dist/workflow/ReviewAnalyzer.js +194 -10
  21. package/dist/workflow/ReviewAnalyzer.js.map +1 -1
  22. package/dist/workflow/VerificationCommands.d.ts +4 -0
  23. package/dist/workflow/VerificationCommands.js +2 -0
  24. package/dist/workflow/VerificationCommands.js.map +1 -1
  25. package/dist/workflow/WorkflowEngine.js +1 -1
  26. package/dist/workflow/WorkflowEngine.js.map +1 -1
  27. package/dist/workflow/evolution/LessonExtractor.d.ts +90 -0
  28. package/dist/workflow/evolution/LessonExtractor.js +317 -0
  29. package/dist/workflow/evolution/LessonExtractor.js.map +1 -0
  30. package/dist/workflow/evolution/SelfImproveEngine.d.ts +156 -0
  31. package/dist/workflow/evolution/SelfImproveEngine.js +361 -0
  32. package/dist/workflow/evolution/SelfImproveEngine.js.map +1 -0
  33. package/dist/workflow/gates/GateSystem.d.ts +28 -2
  34. package/dist/workflow/gates/GateSystem.js +291 -82
  35. package/dist/workflow/gates/GateSystem.js.map +1 -1
  36. package/dist/workflow/qa/E2ETestRunner.d.ts +102 -0
  37. package/dist/workflow/qa/E2ETestRunner.js +227 -0
  38. package/dist/workflow/qa/E2ETestRunner.js.map +1 -0
  39. package/dist/workflow/types.d.ts +7 -0
  40. package/package.json +3 -3
@@ -0,0 +1,246 @@
1
+ // SCALE Engine — Evolution Commands
2
+ // L6 Evolution CLI 命令:Lesson 提取、自改进、规则管理
3
+ // 设计参考:docs/03-CORE-MODULES.md §3.6
4
+ import { defineCommand } from 'citty';
5
+ import { join } from 'node:path';
6
+ import { existsSync, mkdirSync } from 'node:fs';
7
+ import { EventBus } from '../core/eventBus.js';
8
+ import { SQLiteArtifactStore } from '../artifact/sqliteStore.js';
9
+ import { FSM } from '../artifact/fsm.js';
10
+ import { registerAllFSMs } from '../artifact/fsmDefinitions.js';
11
+ import { WorkflowEngine } from '../workflow/WorkflowEngine.js';
12
+ import { CapabilityRegistry } from '../capabilities/CapabilityRegistry.js';
13
+ import { SkillRegistry } from '../skills/SkillRegistry.js';
14
+ import { LessonExtractor } from '../workflow/evolution/LessonExtractor.js';
15
+ import { SelfImproveEngine } from '../workflow/evolution/SelfImproveEngine.js';
16
+ const SCALE_DIR = process.env.SCALE_DIR ?? '.scale';
17
+ function ensureDir(dir) {
18
+ if (!existsSync(dir))
19
+ mkdirSync(dir, { recursive: true });
20
+ }
21
+ // Engine singleton (reuse pattern from phaseCommands.ts)
22
+ function getEngine() {
23
+ ensureDir(SCALE_DIR);
24
+ const eventBus = new EventBus({ eventsDir: join(SCALE_DIR, 'events') });
25
+ const store = new SQLiteArtifactStore(eventBus, {
26
+ dbPath: join(SCALE_DIR, 'scale.db'),
27
+ artifactsDir: join(SCALE_DIR, 'artifacts'),
28
+ });
29
+ const fsm = new FSM(store, eventBus);
30
+ registerAllFSMs(fsm);
31
+ const capabilityRegistry = new CapabilityRegistry(eventBus);
32
+ const skillRegistry = new SkillRegistry(eventBus);
33
+ const workflowEngine = new WorkflowEngine({ eventBus, capabilityRegistry, skillRegistry });
34
+ return { eventBus, store, fsm, workflowEngine, skillRegistry };
35
+ }
36
+ // Evolution extract 命令 - 从会话提取 Lessons
37
+ export const evolutionExtract = defineCommand({
38
+ meta: { name: 'extract', description: 'Extract lessons from session defects' },
39
+ args: {
40
+ 'session-id': { type: 'positional', required: true, description: 'Session ID to analyze' },
41
+ 'output': { type: 'string', alias: 'o', description: 'Output file for lessons (JSON)' },
42
+ 'verbose': { type: 'boolean', alias: 'v', default: false, description: 'Show detailed extraction process' },
43
+ 'min': { type: 'string', default: '2', description: 'Minimum occurrences to become lesson' },
44
+ json: { type: 'boolean', default: false },
45
+ },
46
+ async run({ args }) {
47
+ const { eventBus } = getEngine();
48
+ const sessionId = args['session-id'];
49
+ const minOccurrences = parseInt(args.min, 10) || 2;
50
+ const extractor = new LessonExtractor(eventBus, minOccurrences);
51
+ if (!args.verbose && !args.json) {
52
+ console.log(`Analyzing session ${sessionId} for lessons...`);
53
+ }
54
+ const candidates = await extractor.extractFromSession(sessionId);
55
+ if (candidates.length === 0) {
56
+ if (!args.json) {
57
+ console.log('\nNo lessons extracted from this session.');
58
+ console.log('This may mean:');
59
+ console.log(' - No defects were recorded in this session');
60
+ console.log(' - Patterns did not meet minimum occurrence threshold');
61
+ }
62
+ return;
63
+ }
64
+ if (args.json) {
65
+ const lessons = extractor.toLessonArtifacts(candidates);
66
+ console.log(JSON.stringify(lessons, null, 2));
67
+ }
68
+ else {
69
+ console.log(`\n=== Extracted Lessons (${candidates.length}) ===\n`);
70
+ for (const candidate of candidates) {
71
+ console.log(`[${candidate.priority}] ${candidate.pattern}`);
72
+ console.log(` Solution: ${candidate.solution}`);
73
+ console.log(` Occurrences: ${candidate.frequency}`);
74
+ console.log(` Verified: ${candidate.verified ? 'Yes' : 'No (pending)'}`);
75
+ console.log('');
76
+ }
77
+ }
78
+ if (args.output && !args.json) {
79
+ const lessons = extractor.toLessonArtifacts(candidates);
80
+ console.log(`\nTo save lessons, output to: ${args.output}`);
81
+ }
82
+ },
83
+ });
84
+ // Evolution improve 命令 - 运行自改进闭环
85
+ export const evolutionImprove = defineCommand({
86
+ meta: { name: 'improve', description: 'Run self-improve cycle: Defect → Lesson → Rule → Hook' },
87
+ args: {
88
+ 'session-id': { type: 'positional', required: true, description: 'Session ID to process' },
89
+ 'verbose': { type: 'boolean', alias: 'v', default: false, description: 'Show detailed improvement process' },
90
+ 'verify-threshold': { type: 'string', default: '3', description: 'Lesson verification threshold' },
91
+ 'rule-threshold': { type: 'string', default: '10', description: 'Rule activation threshold' },
92
+ 'hook-threshold': { type: 'string', default: '20', description: 'Hook generation threshold' },
93
+ json: { type: 'boolean', default: false },
94
+ },
95
+ async run({ args }) {
96
+ const { eventBus } = getEngine();
97
+ const sessionId = args['session-id'];
98
+ const thresholds = {
99
+ lessonVerificationThreshold: parseInt(args['verify-threshold'], 10) || 3,
100
+ ruleActivationThreshold: parseInt(args['rule-threshold'], 10) || 10,
101
+ hookGenerationThreshold: parseInt(args['hook-threshold'], 10) || 20,
102
+ maxHooks: 10
103
+ };
104
+ const engine = new SelfImproveEngine(eventBus, thresholds);
105
+ if (args.verbose && !args.json) {
106
+ console.log('Self-improve thresholds:');
107
+ console.log(` Lesson → Rule: ${thresholds.lessonVerificationThreshold} verifications`);
108
+ console.log(` Rule → Active: ${thresholds.ruleActivationThreshold} hits`);
109
+ console.log(` Rule → Hook: ${thresholds.hookGenerationThreshold} hits`);
110
+ console.log('');
111
+ }
112
+ const state = await engine.run(sessionId);
113
+ if (args.json) {
114
+ console.log(JSON.stringify(state, null, 2));
115
+ }
116
+ else {
117
+ console.log('\n=== Self-Improve Result ===\n');
118
+ console.log(`Lessons Extracted: ${state.lessonsExtracted}`);
119
+ console.log(`Lessons Verified: ${state.lessonsVerified}`);
120
+ console.log(`Rules Created: ${state.rulesCreated}`);
121
+ console.log(`Rules Active: ${state.rulesActive}`);
122
+ console.log(`Hooks Generated: ${state.hooksGenerated}`);
123
+ if (state.hooksGenerated > 0) {
124
+ console.log('\n[GENERATED HOOKS]');
125
+ const hooks = engine.getGeneratedHooks();
126
+ for (const hook of hooks) {
127
+ console.log(` ${hook.hookType}: ${hook.matcher}`);
128
+ }
129
+ console.log('\nTo register these hooks, add to .claude/settings.json:');
130
+ const hooksConfig = engine.getGeneratedHooksConfig();
131
+ console.log(JSON.stringify({ hooks: hooksConfig }, null, 2));
132
+ }
133
+ }
134
+ },
135
+ });
136
+ // Evolution report 命令 - 显示自改进报告
137
+ export const evolutionReport = defineCommand({
138
+ meta: { name: 'report', description: 'Show self-improve engine report' },
139
+ args: {
140
+ 'session-id': { type: 'positional', required: false, description: 'Session ID to analyze' },
141
+ json: { type: 'boolean', default: false },
142
+ },
143
+ async run({ args }) {
144
+ const { eventBus } = getEngine();
145
+ const sessionId = args['session-id'];
146
+ if (sessionId) {
147
+ const engine = new SelfImproveEngine(eventBus);
148
+ await engine.run(sessionId);
149
+ if (args.json) {
150
+ console.log(JSON.stringify({ report: engine.generateReport() }, null, 2));
151
+ }
152
+ else {
153
+ console.log(engine.generateReport());
154
+ }
155
+ }
156
+ else {
157
+ console.log('No session provided. Run `scale evolution improve <session-id>` first.');
158
+ }
159
+ },
160
+ });
161
+ // Evolution rules 命令 - 管理规则
162
+ export const evolutionRules = defineCommand({
163
+ meta: { name: 'rules', description: 'List or manage rules' },
164
+ args: {
165
+ 'list': { type: 'boolean', alias: 'l', default: false, description: 'List all rules' },
166
+ 'active': { type: 'boolean', alias: 'a', default: false, description: 'List only active rules' },
167
+ json: { type: 'boolean', default: false },
168
+ },
169
+ async run({ args }) {
170
+ if (!args.json) {
171
+ console.log('\n=== Rules ===\n');
172
+ console.log('Rules are auto-generated from verified lessons.');
173
+ console.log('');
174
+ console.log('To view rules:');
175
+ console.log(' ls knowledge/rules/');
176
+ console.log('');
177
+ console.log('To create a rule:');
178
+ console.log(' scale evolution extract <session> --output knowledge/rules/new-rule.json');
179
+ console.log('');
180
+ console.log('To activate a rule:');
181
+ console.log(' scale evolution improve <session> --rule-threshold 5');
182
+ }
183
+ },
184
+ });
185
+ // Evolution verify 命令 - 手动验证 Lesson
186
+ export const evolutionVerify = defineCommand({
187
+ meta: { name: 'verify', description: 'Manually verify a lesson' },
188
+ args: {
189
+ 'pattern': { type: 'positional', required: true, description: 'Lesson pattern to verify' },
190
+ json: { type: 'boolean', default: false },
191
+ },
192
+ async run({ args }) {
193
+ const pattern = args['pattern'];
194
+ if (!args.json) {
195
+ console.log(`\nVerifying lesson: "${pattern}"`);
196
+ console.log('');
197
+ console.log('Manual verification steps:');
198
+ console.log(' 1. Review the pattern and solution');
199
+ console.log(' 2. Apply the solution in a real scenario');
200
+ console.log(' 3. Confirm the solution resolves the pattern');
201
+ console.log('');
202
+ console.log('After verification, run:');
203
+ console.log(` scale evolution improve <session-id> --verify-threshold 1`);
204
+ }
205
+ },
206
+ });
207
+ // Evolution hooks 命令 - 显示生成的 Hooks
208
+ export const evolutionHooks = defineCommand({
209
+ meta: { name: 'hooks', description: 'Show generated hooks configuration' },
210
+ args: {
211
+ 'session-id': { type: 'positional', required: false, description: 'Session ID to process' },
212
+ 'json': { type: 'boolean', alias: 'j', default: false, description: 'Output as JSON' },
213
+ },
214
+ async run({ args }) {
215
+ const { eventBus } = getEngine();
216
+ const sessionId = args['session-id'];
217
+ if (sessionId) {
218
+ const engine = new SelfImproveEngine(eventBus);
219
+ await engine.run(sessionId);
220
+ if (args.json) {
221
+ const hooksConfig = engine.getGeneratedHooksConfig();
222
+ console.log(JSON.stringify({ hooks: hooksConfig }, null, 2));
223
+ }
224
+ else {
225
+ const hooks = engine.getGeneratedHooks();
226
+ if (hooks.length === 0) {
227
+ console.log('No hooks generated.');
228
+ console.log('Run with a session that has enough rule hits.');
229
+ }
230
+ else {
231
+ console.log('\n=== Generated Hooks ===\n');
232
+ for (const hook of hooks) {
233
+ console.log(`[${hook.hookType}] Matcher: ${hook.matcher}`);
234
+ console.log(` Description: ${hook.description}`);
235
+ console.log('');
236
+ }
237
+ }
238
+ }
239
+ }
240
+ else {
241
+ console.log('Usage: scale evolution hooks <session-id>');
242
+ console.log(' --json, -j Output as JSON for .claude/settings.json');
243
+ }
244
+ },
245
+ });
246
+ //# sourceMappingURL=evolutionCommands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evolutionCommands.js","sourceRoot":"","sources":["../../src/cli/evolutionCommands.ts"],"names":[],"mappings":"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"}
@@ -97,6 +97,15 @@ export declare const phaseVerify: import("citty").CommandDef<{
97
97
  type: "string";
98
98
  description: string;
99
99
  };
100
+ 'tdd-evidence': {
101
+ type: "string";
102
+ description: string;
103
+ };
104
+ 'tdd-strict': {
105
+ type: "boolean";
106
+ default: false;
107
+ description: string;
108
+ };
100
109
  'skip-build': {
101
110
  type: "boolean";
102
111
  default: false;
@@ -46,6 +46,19 @@ function validateReviewEvidence(ids) {
46
46
  }
47
47
  return { ok: (ids?.length ?? 0) > 0 && missing.length === 0 && failed.length === 0, missing, failed };
48
48
  }
49
+ function getValidatedReviewRecords(ids) {
50
+ const reviewStore = new ReviewStore(SCALE_DIR);
51
+ return (ids ?? [])
52
+ .map(id => reviewStore.getReview(id))
53
+ .filter((record) => Boolean(record?.passed));
54
+ }
55
+ function getVerificationEvidenceSummary(ids) {
56
+ const evidenceStore = new EvidenceStore(SCALE_DIR);
57
+ return (ids ?? [])
58
+ .map(id => evidenceStore.getGateResult(id))
59
+ .filter((record) => Boolean(record))
60
+ .map(record => ({ gate: record.gate, passed: record.passed }));
61
+ }
49
62
  function getEngine() {
50
63
  ensureDir(SCALE_DIR);
51
64
  const eventBus = new EventBus({ eventsDir: join(SCALE_DIR, 'events') });
@@ -77,6 +90,9 @@ function isTruthyFlag(value) {
77
90
  function shouldSkipCommit(value) {
78
91
  return isTruthyFlag(value) || process.argv.includes('--no-commit') || process.argv.includes('--skip-commit');
79
92
  }
93
+ function normalizeGitPath(path) {
94
+ return path.replace(/\\/g, '/');
95
+ }
80
96
  // Helper: Generate spec markdown file
81
97
  function generateSpecMarkdown(id, title, payload) {
82
98
  return `# Spec: ${title}
@@ -236,15 +252,30 @@ export const phaseDefine = defineCommand({
236
252
  const specPath = join(specsDir, `${spec.id}.md`);
237
253
  writeFileSync(specPath, generateSpecMarkdown(spec.id, args.title, specPayload));
238
254
  // FSM transitions: DRAFT -> REVIEWING -> FROZEN
239
- try {
240
- await fsm.transition(spec.id, 'refine', { actor: { kind: 'system', component: 'phase-define' } });
241
- await fsm.transition(spec.id, 'approve', { actor: { kind: 'system', component: 'phase-define' } });
255
+ // Phase 1: refine (DRAFT -> REVIEWING) - no guards
256
+ const refineResult = await fsm.canTransition(spec.id, 'refine');
257
+ if (!refineResult.allowed) {
258
+ if (!args.json) {
259
+ console.error('\nFSM transition blocked: DRAFT -> REVIEWING');
260
+ refineResult.blockedBy?.forEach(b => console.error(` [GUARD] ${b.guard}: ${b.message}`));
261
+ }
262
+ process.exit(1);
242
263
  }
243
- catch (e) {
244
- // Guard may fail - report reason
245
- const error = e;
246
- if (!args.json)
247
- console.log(` FSM transition: ${error.message}`);
264
+ await fsm.transition(spec.id, 'refine', { actor: { kind: 'system', component: 'phase-define' } });
265
+ // Phase 2: approve (REVIEWING -> FROZEN) - guards: ambiguityScore <= 0.2, has successCriteria
266
+ const approveResult = await fsm.canTransition(spec.id, 'approve');
267
+ if (!approveResult.allowed) {
268
+ if (!args.json) {
269
+ console.error('\nFSM transition blocked: REVIEWING -> FROZEN');
270
+ console.error(' Spec cannot be frozen due to:');
271
+ approveResult.blockedBy?.forEach(b => console.error(` [GUARD] ${b.guard}: ${b.message}`));
272
+ console.error('\n Resolve issues before proceeding.');
273
+ }
274
+ process.exit(1);
275
+ }
276
+ await fsm.transition(spec.id, 'approve', { actor: { kind: 'system', component: 'phase-define' } });
277
+ if (!args.json) {
278
+ console.log(' FSM: DRAFT -> REVIEWING -> FROZEN ✓');
248
279
  }
249
280
  const result = { phase: 'DEFINE', spec, specPath, ambiguityScore, successCriteria };
250
281
  if (args.json)
@@ -333,14 +364,20 @@ export const phasePlan = defineCommand({
333
364
  ensureDir(plansDir);
334
365
  const planPath = join(plansDir, `${plan.id}.md`);
335
366
  writeFileSync(planPath, generatePlanMarkdown(plan.id, args['spec-id'], planPayload));
336
- // FSM transition: DRAFT -> APPROVED (requires rollbackStrategy)
337
- try {
338
- await fsm.transition(plan.id, 'review', { actor: { kind: 'system', component: 'phase-plan' } });
367
+ // FSM transition: DRAFT -> APPROVED (requires rollbackStrategy guard)
368
+ const reviewResult = await fsm.canTransition(plan.id, 'review');
369
+ if (!reviewResult.allowed) {
370
+ if (!args.json) {
371
+ console.error('\nFSM transition blocked: DRAFT -> APPROVED');
372
+ console.error(' Plan cannot be approved due to:');
373
+ reviewResult.blockedBy?.forEach(b => console.error(` [GUARD] ${b.guard}: ${b.message}`));
374
+ console.error('\n Provide rollback strategy: --rollback "Revert strategy description"');
375
+ }
376
+ process.exit(1);
339
377
  }
340
- catch (e) {
341
- const error = e;
342
- if (!args.json)
343
- console.log(` FSM transition: ${error.message}`);
378
+ await fsm.transition(plan.id, 'review', { actor: { kind: 'system', component: 'phase-plan' } });
379
+ if (!args.json) {
380
+ console.log(' FSM: DRAFT -> APPROVED ✓');
344
381
  }
345
382
  const result = { phase: 'PLAN', plan, planPath, rollbackStrategy };
346
383
  if (args.json)
@@ -390,20 +427,26 @@ export const phaseBuild = defineCommand({
390
427
  createdBy: { kind: 'human', userId: 'cli' },
391
428
  });
392
429
  // FSM transitions: PENDING -> READY -> RUNNING
393
- try {
394
- await fsm.transition(task.id, 'schedule', { actor: { kind: 'system', component: 'phase-build' } });
395
- await fsm.transition(task.id, 'start', { actor: { kind: 'human', userId: 'cli' } });
430
+ // Phase 1: schedule (PENDING -> READY) - no guards
431
+ const scheduleResult = await fsm.canTransition(task.id, 'schedule');
432
+ if (!scheduleResult.allowed) {
433
+ if (!args.json) {
434
+ console.error('\nFSM transition blocked: PENDING -> READY');
435
+ scheduleResult.blockedBy?.forEach(b => console.error(` [GUARD] ${b.guard}: ${b.message}`));
436
+ }
437
+ process.exit(1);
396
438
  }
397
- catch (e) {
398
- const error = e;
399
- if (!args.json)
400
- console.log(` FSM transition: ${error.message}`);
439
+ await fsm.transition(task.id, 'schedule', { actor: { kind: 'system', component: 'phase-build' } });
440
+ // Phase 2: start (READY -> RUNNING) - no guards
441
+ await fsm.transition(task.id, 'start', { actor: { kind: 'human', userId: 'cli' } });
442
+ if (!args.json) {
443
+ console.log(' FSM: PENDING -> READY -> RUNNING ✓');
401
444
  }
402
445
  // Update Plan status to IMPLEMENTING
403
- try {
446
+ const implResult = await fsm.canTransition(args['plan-id'], 'implement');
447
+ if (implResult.allowed) {
404
448
  await fsm.transition(args['plan-id'], 'implement', { actor: { kind: 'system', component: 'phase-build' } });
405
449
  }
406
- catch { }
407
450
  const result = { phase: 'BUILD', task, status: 'RUNNING' };
408
451
  if (args.json)
409
452
  console.log(JSON.stringify(result, null, 2));
@@ -435,6 +478,8 @@ export const phaseVerify = defineCommand({
435
478
  'lint-cmd': { type: 'string', description: 'Override lint command' },
436
479
  'test-cmd': { type: 'string', description: 'Override test command' },
437
480
  'coverage-cmd': { type: 'string', description: 'Override coverage command' },
481
+ 'tdd-evidence': { type: 'string', description: 'Path to JSON TDD evidence with red/green/refactor/testFirst=true' },
482
+ 'tdd-strict': { type: 'boolean', default: false, description: 'Require TDD evidence before other gates' },
438
483
  'skip-build': { type: 'boolean', default: false },
439
484
  'skip-lint': { type: 'boolean', default: false },
440
485
  'skip-test': { type: 'boolean', default: false },
@@ -457,6 +502,8 @@ export const phaseVerify = defineCommand({
457
502
  lint: args['lint-cmd'],
458
503
  test: args['test-cmd'],
459
504
  coverage: args['coverage-cmd'],
505
+ tddEvidence: args['tdd-evidence'],
506
+ tddStrict: isTruthyFlag(args['tdd-strict']),
460
507
  });
461
508
  // Step 2: Display gate results
462
509
  if (!args.json) {
@@ -503,6 +550,7 @@ export const phaseVerify = defineCommand({
503
550
  };
504
551
  await store.update(args['task-id'], { payload: updatedPayload });
505
552
  // Attempt FSM transition to COMPLETED
553
+ // Guards: build_passed, lint_passed, tests_passed
506
554
  const allPassed = results.buildStatus === 'success' &&
507
555
  (results.buildExitCode ?? 1) === 0 &&
508
556
  results.lintStatus === 'success' &&
@@ -511,19 +559,26 @@ export const phaseVerify = defineCommand({
511
559
  results.securityPassed === true;
512
560
  let transitionResult = null;
513
561
  if (allPassed) {
514
- try {
562
+ const completeResult = await fsm.canTransition(args['task-id'], 'complete');
563
+ if (!completeResult.allowed) {
564
+ if (!args.json) {
565
+ console.error('\nFSM transition blocked: RUNNING -> COMPLETED');
566
+ console.error(' Task cannot be completed due to:');
567
+ completeResult.blockedBy?.forEach(b => console.error(` [GUARD] ${b.guard}: ${b.message}`));
568
+ }
569
+ // Don't exit - allow user to see what passed and fix issues
570
+ }
571
+ else {
515
572
  transitionResult = await fsm.transition(args['task-id'], 'complete', {
516
573
  actor: { kind: 'human', userId: 'cli' }
517
574
  });
518
575
  if (!args.json)
519
- console.log('\nTask marked COMPLETED');
520
- }
521
- catch (e) {
522
- const error = e;
523
- if (!args.json)
524
- console.log('\nFSM transition failed:', error.message);
576
+ console.log('\n FSM: RUNNING -> COMPLETED');
525
577
  }
526
578
  }
579
+ else if (!args.json) {
580
+ console.log('\n Verification requirements not met - cannot complete Task');
581
+ }
527
582
  const passed = allPassed && (transitionResult?.success ?? false);
528
583
  const result = { phase: 'VERIFY', taskId: args['task-id'], results, evidenceIds: verificationEvidenceIds, passed };
529
584
  if (args.json)
@@ -578,7 +633,8 @@ async function reviewGitChanges(taskPayload) {
578
633
  const status = await runGit(['status', '--short']);
579
634
  const untracked = await runGit(['ls-files', '--others', '--exclude-standard']);
580
635
  const statusOutput = mergeUntrackedFilesIntoStatus(status.stdout, untracked.stdout);
581
- const changedFiles = analyzeReview({ statusOutput, diffs: [], taskPayload }).changedFiles;
636
+ const verificationEvidence = getVerificationEvidenceSummary(taskPayload?.verificationEvidenceIds);
637
+ const changedFiles = analyzeReview({ statusOutput, diffs: [], taskPayload, verificationEvidence }).changedFiles;
582
638
  const diffs = [];
583
639
  for (const file of changedFiles.slice(0, 50)) {
584
640
  if (file.status === '??') {
@@ -589,7 +645,50 @@ async function reviewGitChanges(taskPayload) {
589
645
  diffs.push({ file: file.path, text: diff.stdout });
590
646
  }
591
647
  }
592
- return analyzeReview({ statusOutput, diffs, taskPayload });
648
+ return analyzeReview({ statusOutput, diffs, taskPayload, verificationEvidence });
649
+ }
650
+ function collectReviewedFiles(records) {
651
+ const reviewed = new Set();
652
+ for (const record of records) {
653
+ if (!record.passed)
654
+ continue;
655
+ for (const file of record.changedFiles) {
656
+ if (shouldReviewFile(file))
657
+ reviewed.add(normalizeGitPath(file));
658
+ }
659
+ }
660
+ return reviewed;
661
+ }
662
+ async function getReviewableGitChanges() {
663
+ const status = await runGit(['status', '--short']);
664
+ const untracked = await runGit(['ls-files', '--others', '--exclude-standard']);
665
+ const statusOutput = mergeUntrackedFilesIntoStatus(status.stdout, untracked.stdout);
666
+ return parseChangedFiles(statusOutput).filter(file => shouldReviewFile(file.path));
667
+ }
668
+ async function stageReviewedFiles(reviewRecords) {
669
+ const reviewedFiles = collectReviewedFiles(reviewRecords);
670
+ const currentChanges = await getReviewableGitChanges();
671
+ const stagedFiles = [];
672
+ const unreviewedFiles = [];
673
+ for (const file of currentChanges) {
674
+ const normalizedPath = normalizeGitPath(file.path);
675
+ if (reviewedFiles.has(normalizedPath)) {
676
+ stagedFiles.push(file.path);
677
+ }
678
+ else {
679
+ unreviewedFiles.push(file.path);
680
+ }
681
+ }
682
+ if (unreviewedFiles.length > 0) {
683
+ return { stagedFiles: [], unreviewedFiles };
684
+ }
685
+ if (stagedFiles.length > 0) {
686
+ const gitAdd = await runGit(['add', '--', ...stagedFiles]);
687
+ if (gitAdd.exitCode !== 0) {
688
+ throw new Error(gitAdd.stderr || 'git add failed');
689
+ }
690
+ }
691
+ return { stagedFiles, unreviewedFiles: [] };
593
692
  }
594
693
  // REVIEW Phase - KarpathyEvaluator + deterministic review evidence
595
694
  export const phaseReview = defineCommand({
@@ -720,18 +819,17 @@ export const phaseShip = defineCommand({
720
819
  }
721
820
  process.exit(1);
722
821
  }
723
- // Attempt FSM transition
724
- try {
725
- await fsm.transition(args['task-id'], 'complete', {
726
- actor: { kind: 'human', userId: 'cli' }
727
- });
728
- }
729
- catch (e) {
730
- const error = e;
731
- console.error('\nFSM transition failed:', error.message);
822
+ // FSM transition with guard check
823
+ const completeResult = await fsm.canTransition(args['task-id'], 'complete');
824
+ if (!completeResult.allowed) {
825
+ console.error('\nFSM transition blocked: RUNNING -> COMPLETED');
826
+ completeResult.blockedBy?.forEach(b => console.error(` [GUARD] ${b.guard}: ${b.message}`));
732
827
  console.log('\n Run verification first: scale verify ' + args['task-id'] + '\n');
733
828
  process.exit(1);
734
829
  }
830
+ await fsm.transition(args['task-id'], 'complete', {
831
+ actor: { kind: 'human', userId: 'cli' }
832
+ });
735
833
  }
736
834
  if (!reviewPassed) {
737
835
  console.error('\nTask not reviewed with persisted passing evidence. Run: scale review ' + args['task-id'] + '\n');
@@ -745,18 +843,38 @@ export const phaseShip = defineCommand({
745
843
  }
746
844
  // Git operations
747
845
  let commitHash = null;
846
+ let stagedFiles = [];
748
847
  if (!shouldSkipCommit(args['skip-commit'])) {
749
- const { execa } = await import('execa');
750
848
  const commitMessage = args.message ?? `feat: ${task.title ?? args['task-id']}`;
751
849
  try {
752
- await execa('git', ['add', '.']);
753
- const result = await execa('git', ['commit', '-m', commitMessage]);
754
- commitHash = result.stdout.split('\n')[0]; // First line contains hash
850
+ const reviewRecords = getValidatedReviewRecords(payload.reviewEvidenceIds);
851
+ const stageResult = await stageReviewedFiles(reviewRecords);
852
+ if (stageResult.unreviewedFiles.length > 0) {
853
+ console.error('\nUnreviewed working tree changes detected. Re-run scale review before shipping.');
854
+ stageResult.unreviewedFiles.forEach(file => console.error(' - ' + file));
855
+ console.error('\nUse scale ship ' + args['task-id'] + ' --no-commit to generate the delivery report without committing.\n');
856
+ process.exit(1);
857
+ }
858
+ stagedFiles = stageResult.stagedFiles;
859
+ const result = await runGit(['commit', '-m', commitMessage]);
860
+ if (result.exitCode !== 0) {
861
+ const message = result.stderr || result.stdout || 'git commit failed';
862
+ if (/nothing to commit|no changes added/i.test(message)) {
863
+ if (!args.json)
864
+ console.log(' Git commit skipped: nothing to commit');
865
+ }
866
+ else {
867
+ throw new Error(message);
868
+ }
869
+ }
870
+ else {
871
+ commitHash = result.stdout.split('\n')[0]; // First line contains hash
872
+ }
755
873
  }
756
874
  catch (e) {
757
875
  const error = e;
758
- if (!args.json)
759
- console.log(' Git commit skipped:', error.message);
876
+ console.error('\nGit commit failed:', error.message);
877
+ process.exit(1);
760
878
  }
761
879
  }
762
880
  // Update Plan to DONE if Task completed
@@ -777,6 +895,8 @@ export const phaseShip = defineCommand({
777
895
  console.log(` - Status: COMPLETED`);
778
896
  if (commitHash)
779
897
  console.log(` - Commit: ${commitHash}`);
898
+ if (stagedFiles.length)
899
+ console.log(` - Files committed: ${stagedFiles.length}`);
780
900
  console.log('');
781
901
  console.log(`[VERIFIED]`);
782
902
  console.log(' [PASS] Build: passed');
@@ -811,6 +931,7 @@ export const phaseShip = defineCommand({
811
931
  reviewEvidenceIds: payload.reviewEvidenceIds ?? [],
812
932
  reviewValidation,
813
933
  commitHash,
934
+ stagedFiles,
814
935
  };
815
936
  if (args.json)
816
937
  console.log(JSON.stringify(result, null, 2));