sillyspec 3.25.9 → 3.26.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 (76) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +8 -13
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
  3. package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
  4. package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
  7. package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
  8. package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
  9. package/.sillyspec-platform-cleaned +1 -0
  10. package/.sillyspec-platform.json +7 -0
  11. package/README.md +9 -0
  12. package/package.json +2 -2
  13. package/src/change-list.js +11 -8
  14. package/src/change-risk-profile.js +22 -0
  15. package/src/db.js +170 -161
  16. package/src/dispatch/backends/local-agent.js +152 -0
  17. package/src/dispatch/backends/sillyhub-mcp.js +242 -0
  18. package/src/dispatch/probe.js +210 -0
  19. package/src/dispatch/strategy.js +121 -0
  20. package/src/doctor-diagnostics.js +19 -25
  21. package/src/fs-atomic.js +69 -69
  22. package/src/git-helper.js +66 -0
  23. package/src/hooks/worktree-guard.js +64 -55
  24. package/src/index.js +326 -36
  25. package/src/init.js +7 -14
  26. package/src/local-detect.js +120 -110
  27. package/src/machine-interface.js +54 -19
  28. package/src/migrate.js +14 -4
  29. package/src/modules.js +16 -5
  30. package/src/progress/change-registry.js +85 -70
  31. package/src/progress/consistency-doctor.js +71 -14
  32. package/src/progress/shared.js +8 -5
  33. package/src/progress/stage-machine.js +67 -65
  34. package/src/progress/step-store.js +72 -67
  35. package/src/progress.js +448 -236
  36. package/src/quicklog.js +59 -12
  37. package/src/run/command.js +78 -39
  38. package/src/run/complete-handlers.js +1028 -889
  39. package/src/run/complete.js +132 -212
  40. package/src/run/concurrent-detect.js +132 -0
  41. package/src/run/gates.js +302 -19
  42. package/src/run/prompt.js +22 -68
  43. package/src/run/quick-audit.js +10 -9
  44. package/src/run/shared.js +722 -600
  45. package/src/run/stage.js +43 -24
  46. package/src/scan-postcheck.js +9 -31
  47. package/src/sillyhub-mcp/client.js +394 -0
  48. package/src/sillyhub-mcp/config.js +62 -0
  49. package/src/spec-dir-typo.js +71 -0
  50. package/src/stage-contract-engine.js +22 -1
  51. package/src/stage-contract-spec.js +11 -4
  52. package/src/stage-contract.js +26 -3
  53. package/src/stage-review.js +118 -10
  54. package/src/stages/archive.js +4 -6
  55. package/src/stages/brainstorm-auto.js +26 -20
  56. package/src/stages/brainstorm.js +29 -5
  57. package/src/stages/cmd-existence.js +145 -0
  58. package/src/stages/doctor.js +10 -7
  59. package/src/stages/execute.js +64 -22
  60. package/src/stages/plan-postcheck.js +152 -0
  61. package/src/stages/plan.js +16 -31
  62. package/src/stages/quick.js +25 -9
  63. package/src/stages/scan.js +53 -8
  64. package/src/stages/status.js +0 -1
  65. package/src/stages/verify.js +14 -9
  66. package/src/sync.js +433 -16
  67. package/src/task-review.js +59 -1
  68. package/src/verify-postcheck.js +938 -862
  69. package/src/version.js +19 -0
  70. package/src/worktree-apply.js +218 -93
  71. package/src/worktree-deps.js +89 -5
  72. package/src/worktree.js +213 -111
  73. package/templates/prompts/taskcard-rules.md +27 -0
  74. package/templates/prompts/verify-probes.md +3 -3
  75. package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
  76. package/src/stages/propose.js +0 -181
@@ -6,6 +6,7 @@
6
6
  import { mkdirSync } from 'fs';
7
7
  import { join, resolve, basename } from 'path';
8
8
  import { writeAtomicSync } from '../fs-atomic.js';
9
+ import { runValidators } from '../stage-contract.js';
9
10
  import { VALID_STAGES, STAGE_LABELS, STAGE_ORDER, MAIN_FLOW_ORDER, SPEC_DIR_NAME, CURRENT_VERSION, emptyStage } from './shared.js';
10
11
 
11
12
  export class StageMachine {
@@ -17,14 +18,13 @@ export class StageMachine {
17
18
  * 阶段产物校验门:progress complete-stage / update-step 自动完成时复用
18
19
  * stage-contract 的 validator,防止零产物阶段被直接标 completed。
19
20
  */
20
- async _validateStageArtifacts(cwd, stage, changeName) {
21
- const { runValidators } = await import('../stage-contract.js');
21
+ _validateStageArtifacts(cwd, stage, changeName) {
22
22
  const specDir = this.pm._getSpecDir(cwd);
23
23
  const defaultSpec = join(resolve(cwd), SPEC_DIR_NAME);
24
24
  const specRoot = resolve(specDir) === defaultSpec ? null : specDir;
25
25
  let projectName = null;
26
26
  try {
27
- const g = await this.pm.readGlobal(cwd);
27
+ const g = this.pm.readGlobal(cwd);
28
28
  projectName = g?.project || null;
29
29
  } catch {}
30
30
  return runValidators(stage, cwd, changeName, {
@@ -33,27 +33,27 @@ export class StageMachine {
33
33
  });
34
34
  }
35
35
 
36
- async completeStage(cwd, stage, changeName = null, opts = {}) {
36
+ completeStage(cwd, stage, changeName = null, opts = {}) {
37
37
  const { force = false } = opts;
38
38
  if (!VALID_STAGES.includes(stage)) {
39
39
  console.log(`❌ 未知阶段: ${stage}`);
40
40
  return;
41
41
  }
42
42
 
43
- const db = await this.pm._ensureDB(cwd);
43
+ const db = this.pm._ensureDB(cwd);
44
44
  const now = new Date().toISOString();
45
45
 
46
46
  // 获取变更名
47
47
  let cn = changeName;
48
48
  if (!cn) {
49
- const changes = await this.pm.listChanges(cwd);
49
+ const changes = this.pm.listChanges(cwd);
50
50
  if (changes.length === 1) cn = changes[0];
51
51
  if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
52
52
  }
53
53
 
54
54
  // ── 产物校验门:complete-stage 不再是零校验后门 ──
55
55
  // 与 run.js completeStep 的 validator 同源;--force 为显式逃生口(留审计)。
56
- const validation = await this._validateStageArtifacts(cwd, stage, cn);
56
+ const validation = this._validateStageArtifacts(cwd, stage, cn);
57
57
  if (!validation.ok) {
58
58
  if (!force) {
59
59
  console.error(`❌ complete-stage 被拒绝:阶段 ${stage} 产物校验未通过`);
@@ -75,39 +75,35 @@ export class StageMachine {
75
75
  this.pm._appendAuditLog(cwd, { action: 'complete-stage --force', stage, change: cn, validationErrors: [] });
76
76
  }
77
77
 
78
- db.transaction((sqlDb) => {
79
- const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
80
- if (!changeRow || changeRow.length === 0 || changeRow[0].values.length === 0) return;
81
- const changeId = changeRow[0].values[0][0];
78
+ db.transaction(() => {
79
+ const sqlDb = db.getDb();
80
+ const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
81
+ if (changeRow === undefined) return;
82
+ const changeId = changeRow.id;
82
83
 
83
84
  // 确保 stages 行存在(阶段不存在时自动创建)
84
- sqlDb.run(
85
- 'INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")',
86
- [changeId, stage]
87
- );
85
+ sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, stage);
88
86
 
89
87
  // UPDATE stages.status=completed + completed_at
90
- sqlDb.run(
91
- 'UPDATE stages SET status = "completed", completed_at = ? WHERE change_id = ? AND stage = ?',
92
- [now, changeId, stage]
93
- );
88
+ sqlDb.prepare(`UPDATE stages SET status = 'completed', completed_at = ? WHERE change_id = ? AND stage = ?`).run(now, changeId, stage);
94
89
 
95
90
  // 将该阶段所有 pending 步骤标记为 completed
96
- const stageRow = sqlDb.exec('SELECT id FROM stages WHERE change_id = ? AND stage = ?', [changeId, stage]);
97
- if (stageRow && stageRow.length > 0 && stageRow[0].values.length > 0) {
98
- const stageId = stageRow[0].values[0][0];
99
- sqlDb.run(
100
- 'UPDATE steps SET status = "completed", completed_at = ? WHERE stage_id = ? AND status IN ("pending", "stale")', // stale 一并回填(reopen --done 场景,坑 brainstorm-reopen-step-state-desync)
101
- [now, stageId]
102
- );
91
+ const stageRow = sqlDb.prepare('SELECT id FROM stages WHERE change_id = ? AND stage = ?').get(changeId, stage);
92
+ if (stageRow !== undefined) {
93
+ const stageId = stageRow.id;
94
+ sqlDb.prepare(
95
+ `UPDATE steps SET status = 'completed', completed_at = ? WHERE stage_id = ? AND status IN ('pending', 'stale')` // stale 一并回填(reopen --done 场景,坑 brainstorm-reopen-step-state-desync)
96
+ ).run(now, stageId);
103
97
  }
104
98
 
105
99
  // UPDATE changes.last_active
106
- sqlDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [now, cn]);
100
+ sqlDb.prepare('UPDATE changes SET last_active = ? WHERE name = ?').run(now, cn);
101
+ // 本地脏度(D-013 / task-04):阶段完成是本地推进
102
+ this.pm._touchLocalModified(cwd, cn, now);
107
103
  });
108
104
 
109
105
  // 写 history 文件(保持文件系统,不变)
110
- const data = await this.pm.read(cwd, cn);
106
+ const data = this.pm.read(cwd, cn);
111
107
  if (data && data.stages && data.stages[stage]) {
112
108
  const historyDir = this.pm._runtimePath(cwd, 'history');
113
109
  mkdirSync(historyDir, { recursive: true });
@@ -122,25 +118,25 @@ export class StageMachine {
122
118
  console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
123
119
  }
124
120
 
125
- async show(cwd, changeName = null) {
121
+ show(cwd, changeName = null) {
126
122
  // 如果指定了变更名,只显示该变更
127
123
  if (changeName) {
128
- return await this._showChange(cwd, changeName);
124
+ return this._showChange(cwd, changeName);
129
125
  }
130
126
 
131
127
  // 否则显示所有变更
132
- const changes = await this.pm.listChanges(cwd);
128
+ const changes = this.pm.listChanges(cwd);
133
129
  if (changes.length === 0) {
134
130
  console.log('ℹ️ 没有活跃的变更');
135
131
  return;
136
132
  }
137
133
 
138
134
  if (changes.length === 1) {
139
- return await this._showChange(cwd, changes[0]);
135
+ return this._showChange(cwd, changes[0]);
140
136
  }
141
137
 
142
138
  // 多个变更:汇总显示
143
- const global = await this.pm.readGlobal(cwd);
139
+ const global = this.pm.readGlobal(cwd);
144
140
  console.log('');
145
141
  console.log(' ═══════════════════════════════════════');
146
142
  console.log(` 项目: ${(global?.project) || basename(cwd) || '(未命名)'}`);
@@ -149,7 +145,7 @@ export class StageMachine {
149
145
  console.log('');
150
146
 
151
147
  for (const cn of changes) {
152
- const data = await this.pm.read(cwd, cn);
148
+ const data = this.pm.read(cwd, cn);
153
149
  if (!data) {
154
150
  console.log(` 📂 ${cn} — (无法读取)`);
155
151
  continue;
@@ -167,8 +163,8 @@ export class StageMachine {
167
163
  console.log('');
168
164
  }
169
165
 
170
- async _showChange(cwd, changeName) {
171
- const data = await this.pm.read(cwd, changeName);
166
+ _showChange(cwd, changeName) {
167
+ const data = this.pm.read(cwd, changeName);
172
168
  if (!data) {
173
169
  console.log(`❌ 未找到变更 ${changeName}`);
174
170
  return;
@@ -313,12 +309,12 @@ export class StageMachine {
313
309
  return null;
314
310
  }
315
311
 
316
- async status(cwd, changeName = null) {
317
- await this.show(cwd, changeName);
312
+ status(cwd, changeName = null) {
313
+ this.show(cwd, changeName);
318
314
  }
319
315
 
320
- async validate(cwd, changeName = null) {
321
- const data = await this.pm.read(cwd, changeName);
316
+ validate(cwd, changeName = null) {
317
+ const data = this.pm.read(cwd, changeName);
322
318
  if (!data) { console.log('❌ 无法读取进度数据'); return false; }
323
319
 
324
320
  const errors = [];
@@ -345,7 +341,7 @@ export class StageMachine {
345
341
  if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
346
342
  }
347
343
  if (changed) {
348
- await this.pm._write(cwd, fixed);
344
+ this.pm._write(cwd, fixed);
349
345
  console.log('✅ 已修复');
350
346
  }
351
347
 
@@ -365,10 +361,10 @@ export class StageMachine {
365
361
  * @param {string} [opts.changeName]
366
362
  * @returns {{ ok: boolean, error?: string }}
367
363
  */
368
- async reopenStage(cwd, stage, opts = {}) {
364
+ reopenStage(cwd, stage, opts = {}) {
369
365
  const { fromStep, changeName = null } = opts;
370
366
 
371
- const data = await this.pm.read(cwd, changeName);
367
+ const data = this.pm.read(cwd, changeName);
372
368
  if (!data) return { ok: false, error: '无法读取进度数据' };
373
369
 
374
370
  const stageData = data.stages[stage];
@@ -435,12 +431,12 @@ export class StageMachine {
435
431
  data.lastActive = now;
436
432
  data.currentStage = stage;
437
433
 
438
- await this.pm._write(cwd, data, changeName);
434
+ this.pm._write(cwd, data, changeName);
439
435
 
440
436
  // 级联标记下游阶段为 stale
441
437
  const downstreamStages = this._getDownstreamStages(stage);
442
438
  if (downstreamStages.length > 0) {
443
- const data2 = await this.pm.read(cwd, changeName); // 重新读取以获取最新状态
439
+ const data2 = this.pm.read(cwd, changeName); // 重新读取以获取最新状态
444
440
  if (data2) {
445
441
  for (const ds of downstreamStages) {
446
442
  if (data2.stages[ds] && data2.stages[ds].status === 'completed') {
@@ -449,7 +445,7 @@ export class StageMachine {
449
445
  data2.stages[ds].completedAt = null;
450
446
  }
451
447
  }
452
- await this.pm._write(cwd, data2, changeName);
448
+ this.pm._write(cwd, data2, changeName);
453
449
  }
454
450
  }
455
451
 
@@ -467,9 +463,9 @@ export class StageMachine {
467
463
  return MAIN_FLOW_ORDER.slice(idx + 1);
468
464
  }
469
465
 
470
- async reset(cwd, stage, changeName = null) {
466
+ reset(cwd, stage, changeName = null) {
471
467
  if (stage) {
472
- const data = await this.pm.read(cwd, changeName);
468
+ const data = this.pm.read(cwd, changeName);
473
469
  if (!data) { console.log('❌ 无法读取进度数据'); return; }
474
470
  if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
475
471
  // 破坏性预览:reset 不可逆地清空该阶段所有步骤(含已完成),先告诉 agent 丢了什么。
@@ -479,38 +475,44 @@ export class StageMachine {
479
475
  console.warn(`⚠️ 即将重置阶段「${stage}」:丢弃 ${steps.length} 个步骤(其中 ${doneCount} 个已完成,revision ${sd.revision || 0})。此操作不可逆。`);
480
476
  data.stages[stage] = emptyStage();
481
477
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
482
- await this.pm._write(cwd, data);
478
+ this.pm._write(cwd, data);
483
479
  console.log(`✅ 已重置阶段: ${stage}`);
484
480
  } else {
485
481
  // 重置所有变更或指定变更
486
482
  if (changeName) {
487
483
  console.warn(`⚠️ 即将重置变更「${changeName}」的全部进度(所有阶段的 steps + stage 状态,产物文件不动)。此操作不可逆。`);
488
484
  // SQL: 删除该变更的所有 stages 和 steps 数据
489
- const db = await this.pm._ensureDB(cwd);
490
- db.transaction((sqlDb) => {
491
- const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
492
- if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
493
- const changeId = changeRow[0].values[0][0];
494
- sqlDb.run('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)', [changeId]);
495
- sqlDb.run('DELETE FROM stages WHERE change_id = ?', [changeId]);
485
+ const db = this.pm._ensureDB(cwd);
486
+ db.transaction(() => {
487
+ const sqlDb = db.getDb();
488
+ const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
489
+ if (changeRow !== undefined) {
490
+ const changeId = changeRow.id;
491
+ sqlDb.prepare('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)').run(changeId);
492
+ sqlDb.prepare('DELETE FROM stages WHERE change_id = ?').run(changeId);
496
493
  // 重新插入所有阶段(注意:上方已 DELETE stages,无需再 UPDATE)
497
494
  for (const s of VALID_STAGES) {
498
- sqlDb.run('INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")', [changeId, s]);
495
+ sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, s);
499
496
  }
497
+ // 本地脏度(D-013 / task-04):重置该变更全部进度是本地推进
498
+ this.pm._touchLocalModified(cwd, changeName);
500
499
  }
501
500
  });
502
501
  console.log(`✅ 已重置变更 ${changeName} 的进度`);
503
502
  } else {
504
- const changes = await this.pm.listChanges(cwd);
503
+ const changes = this.pm.listChanges(cwd);
505
504
  console.warn(`⚠️ 即将重置【所有 ${changes.length} 个变更】的进度(仅 stage 状态,产物文件不动)。此操作不可逆且范围最大。`);
506
- const db = await this.pm._ensureDB(cwd);
507
- db.transaction((sqlDb) => {
505
+ const db = this.pm._ensureDB(cwd);
506
+ db.transaction(() => {
507
+ const sqlDb = db.getDb();
508
508
  for (const cn of changes) {
509
- const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
510
- if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
511
- const changeId = changeRow[0].values[0][0];
512
- sqlDb.run('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)', [changeId]);
513
- sqlDb.run('UPDATE stages SET status = "pending", started_at = NULL, completed_at = NULL WHERE change_id = ?', [changeId]);
509
+ const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
510
+ if (changeRow !== undefined) {
511
+ const changeId = changeRow.id;
512
+ sqlDb.prepare('DELETE FROM steps WHERE stage_id IN (SELECT id FROM stages WHERE change_id = ?)').run(changeId);
513
+ sqlDb.prepare(`UPDATE stages SET status = 'pending', started_at = NULL, completed_at = NULL WHERE change_id = ?`).run(changeId);
514
+ // 本地脏度(D-013 / task-04):重置进度是本地推进
515
+ this.pm._touchLocalModified(cwd, cn);
514
516
  }
515
517
  }
516
518
  });
@@ -12,46 +12,44 @@ export class StepStore {
12
12
  this.pm = pm;
13
13
  }
14
14
 
15
- async setStage(cwd, stage, changeName = null) {
15
+ setStage(cwd, stage, changeName = null) {
16
16
  if (!VALID_STAGES.includes(stage)) {
17
17
  console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
18
18
  return;
19
19
  }
20
20
 
21
- const db = await this.pm._ensureDB(cwd);
21
+ const db = this.pm._ensureDB(cwd);
22
22
  const now = new Date().toISOString();
23
23
 
24
24
  // 获取变更名
25
25
  let cn = changeName;
26
26
  if (!cn) {
27
- const changes = await this.pm.listChanges(cwd);
27
+ const changes = this.pm.listChanges(cwd);
28
28
  if (changes.length === 1) cn = changes[0];
29
29
  if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
30
30
  }
31
31
 
32
32
  let changeFound = false;
33
- db.transaction((sqlDb) => {
33
+ db.transaction(() => {
34
+ const sqlDb = db.getDb();
34
35
  // 确保 change 存在
35
- const changeRow = sqlDb.exec('SELECT id, current_stage FROM changes WHERE name = ?', [cn]);
36
- if (!changeRow || changeRow.length === 0 || changeRow[0].values.length === 0) return;
36
+ const changeRow = sqlDb.prepare('SELECT id, current_stage FROM changes WHERE name = ?').get(cn);
37
+ if (changeRow === undefined) return;
37
38
 
38
39
  changeFound = true;
39
- const changeId = changeRow[0].values[0][0];
40
+ const changeId = changeRow.id;
40
41
 
41
42
  // UPDATE changes.current_stage + last_active
42
- sqlDb.run('UPDATE changes SET current_stage = ?, last_active = ? WHERE name = ?', [stage, now, cn]);
43
+ sqlDb.prepare('UPDATE changes SET current_stage = ?, last_active = ? WHERE name = ?').run(stage, now, cn);
43
44
 
44
45
  // 确保 stages 行存在(INSERT OR IGNORE)
45
- sqlDb.run(
46
- 'INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")',
47
- [changeId, stage]
48
- );
46
+ sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, stage);
49
47
 
50
48
  // UPDATE stages.status 为 in-progress(仅当仍为 pending 时)
51
- sqlDb.run(
52
- "UPDATE stages SET status = 'in-progress', started_at = ? WHERE change_id = ? AND stage = ? AND status = 'pending'",
53
- [now, changeId, stage]
54
- );
49
+ sqlDb.prepare("UPDATE stages SET status = 'in-progress', started_at = ? WHERE change_id = ? AND stage = ? AND status = 'pending'").run(now, changeId, stage);
50
+
51
+ // 本地脏度(D-013 / task-04):切换阶段是本地推进
52
+ this.pm._touchLocalModified(cwd, cn, now);
55
53
  });
56
54
 
57
55
  if (!changeFound) {
@@ -65,62 +63,63 @@ export class StepStore {
65
63
  console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage}(变更 ${cn})`);
66
64
  }
67
65
 
68
- async addStep(cwd, stage, stepName, changeName = null) {
66
+ addStep(cwd, stage, stepName, changeName = null) {
69
67
  if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
70
68
 
71
- const db = await this.pm._ensureDB(cwd);
69
+ const db = this.pm._ensureDB(cwd);
72
70
 
73
71
  // 获取变更名
74
72
  let cn = changeName;
75
73
  if (!cn) {
76
- const changes = await this.pm.listChanges(cwd);
74
+ const changes = this.pm.listChanges(cwd);
77
75
  if (changes.length === 1) cn = changes[0];
78
76
  if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
79
77
  }
80
78
 
81
79
  // 查找 stage_id
82
80
  const sqlDb = db.getDb();
83
- const stageRow = sqlDb.exec(
84
- 'SELECT s.id FROM stages s JOIN changes c ON s.change_id = c.id WHERE c.name = ? AND s.stage = ?',
85
- [cn, stage]
86
- );
87
- if (!stageRow || stageRow.length === 0 || stageRow[0].values.length === 0) {
81
+ const stageRow = sqlDb.prepare(
82
+ 'SELECT s.id FROM stages s JOIN changes c ON s.change_id = c.id WHERE c.name = ? AND s.stage = ?'
83
+ ).get(cn, stage);
84
+ if (stageRow === undefined) {
88
85
  // stages 行不存在,静默跳过
89
86
  console.log(`ℹ️ 阶段 ${stage} 不存在`);
90
87
  return;
91
88
  }
92
- const stageId = stageRow[0].values[0][0];
89
+ const stageId = stageRow.id;
93
90
 
94
91
  // 重复步骤名检查
95
- const dupRow = sqlDb.exec('SELECT id FROM steps WHERE stage_id = ? AND name = ?', [stageId, stepName]);
96
- if (dupRow && dupRow.length > 0 && dupRow[0].values.length > 0) {
92
+ const dupRow = sqlDb.prepare('SELECT id FROM steps WHERE stage_id = ? AND name = ?').get(stageId, stepName);
93
+ if (dupRow !== undefined) {
97
94
  console.log(`ℹ️ 步骤 "${stepName}" 已存在于 ${stage}`);
98
95
  return;
99
96
  }
100
97
 
101
98
  // INSERT INTO steps(ordering 递增)
102
- db.transaction((tDb) => {
103
- tDb.run(
99
+ db.transaction(() => {
100
+ const tDb = db.getDb();
101
+ tDb.prepare(
104
102
  `INSERT INTO steps (stage_id, name, ordering, status)
105
- VALUES (?, ?, (SELECT COALESCE(MAX(ordering), 0) + 1 FROM steps WHERE stage_id = ?), 'pending')`,
106
- [stageId, stepName, stageId]
107
- );
108
- tDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [new Date().toISOString(), cn]);
103
+ VALUES (?, ?, (SELECT COALESCE(MAX(ordering), 0) + 1 FROM steps WHERE stage_id = ?), 'pending')`
104
+ ).run(stageId, stepName, stageId);
105
+ tDb.prepare('UPDATE changes SET last_active = ? WHERE name = ?').run(new Date().toISOString(), cn);
106
+ // 本地脏度(D-013 / task-04):添加步骤是本地推进
107
+ this.pm._touchLocalModified(cwd, cn);
109
108
  });
110
109
 
111
110
  console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
112
111
  }
113
112
 
114
- async updateStep(cwd, stage, stepName, options = {}, changeName = null) {
113
+ updateStep(cwd, stage, stepName, options = {}, changeName = null) {
115
114
  const { status, output } = options;
116
115
  if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
117
116
 
118
- const db = await this.pm._ensureDB(cwd);
117
+ const db = this.pm._ensureDB(cwd);
119
118
 
120
119
  // 获取变更名
121
120
  let cn = changeName;
122
121
  if (!cn) {
123
- const changes = await this.pm.listChanges(cwd);
122
+ const changes = this.pm.listChanges(cwd);
124
123
  if (changes.length === 1) cn = changes[0];
125
124
  if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
126
125
  }
@@ -133,50 +132,52 @@ export class StepStore {
133
132
 
134
133
  // 查找 step_id:通过 changes → stages → steps JOIN 查询
135
134
  const sqlDb = db.getDb();
136
- const stepRow = sqlDb.exec(
135
+ const stepRow = sqlDb.prepare(
137
136
  `SELECT st.id, st.status FROM steps st
138
137
  JOIN stages sg ON st.stage_id = sg.id
139
138
  JOIN changes c ON sg.change_id = c.id
140
- WHERE c.name = ? AND sg.stage = ? AND st.name = ?`,
141
- [cn, stage, stepName]
142
- );
143
- if (!stepRow || stepRow.length === 0 || stepRow[0].values.length === 0) {
139
+ WHERE c.name = ? AND sg.stage = ? AND st.name = ?`
140
+ ).get(cn, stage, stepName);
141
+ if (stepRow === undefined) {
144
142
  console.log(`❌ 步骤不存在: ${stage}/${stepName}`);
145
143
  return;
146
144
  }
147
- const stepId = stepRow[0].values[0][0];
145
+ const stepId = stepRow.id;
148
146
 
149
147
  // UPDATE steps
150
148
  let stageCompletionCandidateId = null;
151
- db.transaction((tDb) => {
149
+ db.transaction(() => {
150
+ const tDb = db.getDb();
152
151
  const now = new Date().toISOString();
153
152
  if (status) {
154
- tDb.run('UPDATE steps SET status = ?, completed_at = ? WHERE id = ? AND name = ?', [status, now, stepId, stepName]);
153
+ tDb.prepare('UPDATE steps SET status = ?, completed_at = ? WHERE id = ? AND name = ?').run(status, now, stepId, stepName);
155
154
  }
156
155
  if (output !== undefined) {
157
- tDb.run('UPDATE steps SET output = ? WHERE id = ? AND name = ?', [output, stepId, stepName]);
156
+ tDb.prepare('UPDATE steps SET output = ? WHERE id = ? AND name = ?').run(output, stepId, stepName);
158
157
  }
159
158
 
160
159
  // 自动完成检测:同 stage_id 下所有 steps 都 completed 时,候选标记 stage completed
161
160
  // (实际标记延后到事务外,先过产物校验门,防止 update-step 成为绕过 validator 的后门)
162
161
  if (status === 'completed') {
163
162
  // 获取 stage_id
164
- const stRow = tDb.exec('SELECT stage_id FROM steps WHERE id = ?', [stepId]);
165
- if (stRow && stRow.length > 0 && stRow[0].values.length > 0) {
166
- const stId = stRow[0].values[0][0];
167
- const pendingRows = tDb.exec('SELECT COUNT(*) FROM steps WHERE stage_id = ? AND status != "completed"', [stId]);
168
- if (pendingRows && pendingRows.length > 0 && pendingRows[0].values[0][0] === 0) {
163
+ const stRow = tDb.prepare('SELECT stage_id FROM steps WHERE id = ?').get(stepId);
164
+ if (stRow !== undefined) {
165
+ const stId = stRow.stage_id;
166
+ const pendingRow = tDb.prepare("SELECT COUNT(*) AS cnt FROM steps WHERE stage_id = ? AND status NOT IN ('completed','skipped')").get(stId);
167
+ if (pendingRow.cnt === 0) {
169
168
  stageCompletionCandidateId = stId;
170
169
  }
171
170
  }
172
171
  }
173
172
 
174
- tDb.run('UPDATE changes SET last_active = ? WHERE name = ?', [now, cn]);
173
+ tDb.prepare('UPDATE changes SET last_active = ? WHERE name = ?').run(now, cn);
174
+ // 本地脏度(D-013 / task-04):更新步骤是本地推进
175
+ this.pm._touchLocalModified(cwd, cn, now);
175
176
  });
176
177
 
177
178
  if (stageCompletionCandidateId !== null) {
178
179
  const { force = false } = options;
179
- const validation = await this.pm._validateStageArtifacts(cwd, stage, cn);
180
+ const validation = this.pm._validateStageArtifacts(cwd, stage, cn);
180
181
  if (!validation.ok && !force) {
181
182
  console.error(`⚠️ 阶段 ${stage} 所有步骤已完成,但产物校验未通过,阶段不标记为 completed:`);
182
183
  for (const err of validation.errors) console.error(` - ${err}`);
@@ -194,8 +195,9 @@ export class StepStore {
194
195
  validationErrors: validation.errors,
195
196
  });
196
197
  }
197
- db.transaction((tDb) => {
198
- tDb.run('UPDATE stages SET status = "completed", completed_at = ? WHERE id = ?', [new Date().toISOString(), stageCompletionCandidateId]);
198
+ db.transaction(() => {
199
+ const tDb = db.getDb();
200
+ tDb.prepare(`UPDATE stages SET status = 'completed', completed_at = ? WHERE id = ?`).run(new Date().toISOString(), stageCompletionCandidateId);
199
201
  });
200
202
  console.log(`✅ 阶段 ${stage} 所有步骤已完成,阶段已标记为 completed`);
201
203
  }
@@ -204,39 +206,42 @@ export class StepStore {
204
206
  console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || '(仅更新 output)'}`);
205
207
  }
206
208
 
207
- async updateBatchProgress(cwd, batchData, changeName = null) {
209
+ updateBatchProgress(cwd, batchData, changeName = null) {
208
210
  const cn = changeName || null;
209
211
 
210
- const db = await this.pm._ensureDB(cwd);
211
- db.transaction((sqlDb) => {
212
+ const db = this.pm._ensureDB(cwd);
213
+ db.transaction(() => {
214
+ const sqlDb = db.getDb();
212
215
  // 获取 change_id
213
216
  let changeId = null;
214
217
  if (cn) {
215
- const row = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
216
- if (row && row.length > 0 && row[0].values.length > 0) changeId = row[0].values[0][0];
218
+ const row = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
219
+ if (row !== undefined) changeId = row.id;
217
220
  }
218
221
  if (!changeId) {
219
222
  // 尝试从唯一活跃变更获取
220
- const rows = sqlDb.exec("SELECT id FROM changes WHERE status = 'active'");
221
- if (rows && rows.length > 0 && rows[0].values.length === 1) changeId = rows[0].values[0][0];
223
+ const rows = sqlDb.prepare("SELECT id FROM changes WHERE status = 'active'").all();
224
+ if (rows.length === 1) changeId = rows[0].id;
222
225
  }
223
226
  if (!changeId) return;
224
227
 
225
- sqlDb.run(
228
+ sqlDb.prepare(
226
229
  `INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
227
230
  VALUES (?, ?, ?, ?, ?)
228
231
  ON CONFLICT(change_id) DO UPDATE SET
229
232
  total = excluded.total,
230
233
  completed = excluded.completed,
231
234
  failed = excluded.failed,
232
- skipped = excluded.skipped`,
233
- [changeId, batchData.total || 0, batchData.completed || 0, batchData.failed || 0, batchData.skipped || 0]
234
- );
235
+ skipped = excluded.skipped`
236
+ ).run(changeId, batchData.total || 0, batchData.completed || 0, batchData.failed || 0, batchData.skipped || 0);
237
+ // 本地脏度(D-013 / task-04):批量进度更新是本地推进
238
+ const nameRow = sqlDb.prepare('SELECT name FROM changes WHERE id = ?').get(changeId);
239
+ if (nameRow) this.pm._touchLocalModified(cwd, nameRow.name);
235
240
  });
236
241
  }
237
242
 
238
- async readBatchProgress(cwd, changeName = null) {
239
- const data = await this.pm.read(cwd, changeName);
243
+ readBatchProgress(cwd, changeName = null) {
244
+ const data = this.pm.read(cwd, changeName);
240
245
  return data?.batchProgress || null;
241
246
  }
242
247