sillyspec 3.25.9 → 3.25.11

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 (71) 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-verify/SKILL.md +1 -1
  8. package/README.md +9 -0
  9. package/package.json +2 -2
  10. package/src/change-list.js +11 -8
  11. package/src/change-risk-profile.js +22 -0
  12. package/src/db.js +160 -159
  13. package/src/dispatch/backends/local-agent.js +152 -0
  14. package/src/dispatch/backends/sillyhub-mcp.js +242 -0
  15. package/src/dispatch/probe.js +208 -0
  16. package/src/dispatch/strategy.js +121 -0
  17. package/src/doctor-diagnostics.js +19 -25
  18. package/src/fs-atomic.js +69 -69
  19. package/src/git-helper.js +66 -0
  20. package/src/hooks/worktree-guard.js +64 -55
  21. package/src/index.js +176 -34
  22. package/src/init.js +7 -14
  23. package/src/machine-interface.js +54 -19
  24. package/src/migrate.js +14 -4
  25. package/src/modules.js +16 -5
  26. package/src/progress/change-registry.js +72 -70
  27. package/src/progress/consistency-doctor.js +71 -14
  28. package/src/progress/shared.js +6 -3
  29. package/src/progress/stage-machine.js +61 -65
  30. package/src/progress/step-store.js +62 -67
  31. package/src/progress.js +155 -233
  32. package/src/quicklog.js +13 -5
  33. package/src/run/command.js +65 -37
  34. package/src/run/complete-handlers.js +1005 -889
  35. package/src/run/complete.js +115 -208
  36. package/src/run/concurrent-detect.js +132 -0
  37. package/src/run/gates.js +289 -17
  38. package/src/run/prompt.js +22 -68
  39. package/src/run/quick-audit.js +10 -9
  40. package/src/run/shared.js +648 -600
  41. package/src/run/stage.js +43 -24
  42. package/src/scan-postcheck.js +9 -31
  43. package/src/sillyhub-mcp/client.js +389 -0
  44. package/src/spec-dir-typo.js +71 -0
  45. package/src/stage-contract-engine.js +22 -1
  46. package/src/stage-contract-spec.js +11 -4
  47. package/src/stage-contract.js +26 -3
  48. package/src/stage-review.js +13 -9
  49. package/src/stages/archive.js +4 -6
  50. package/src/stages/brainstorm-auto.js +26 -20
  51. package/src/stages/brainstorm.js +29 -5
  52. package/src/stages/cmd-existence.js +145 -0
  53. package/src/stages/doctor.js +9 -6
  54. package/src/stages/execute.js +61 -20
  55. package/src/stages/plan-postcheck.js +152 -0
  56. package/src/stages/plan.js +16 -31
  57. package/src/stages/quick.js +1 -1
  58. package/src/stages/scan.js +9 -3
  59. package/src/stages/status.js +0 -1
  60. package/src/stages/verify.js +12 -7
  61. package/src/sync.js +4 -4
  62. package/src/task-review.js +30 -1
  63. package/src/verify-postcheck.js +933 -862
  64. package/src/version.js +19 -0
  65. package/src/worktree-apply.js +85 -68
  66. package/src/worktree-deps.js +89 -5
  67. package/src/worktree.js +213 -111
  68. package/templates/prompts/taskcard-rules.md +27 -0
  69. package/templates/prompts/verify-probes.md +3 -3
  70. package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
  71. package/src/stages/propose.js +0 -181
package/src/progress.js CHANGED
@@ -5,16 +5,16 @@
5
5
  *
6
6
  * 存储结构:
7
7
  * .sillyspec/.runtime/sillyspec.db — SQLite 数据库(权威状态源,含全局状态/项目名/活跃变更)
8
- * .sillyspec/.runtime/gate-status.json — worktree-guard 门禁状态缓存
9
8
  *
10
9
  * 历史迁移:v1/v2 使用 progress.json 文件,v3 已全部迁移至 SQLite。
10
+ * worktree-guard hook 直读 sillyspec.db,不再有 gate-status.json 缓存双源(task-10 废除)。
11
11
  */
12
12
 
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync } from 'fs';
14
14
  import { join, basename, dirname, resolve, sep } from 'path';
15
15
  import { tmpdir } from 'os';
16
16
  import { DB } from './db.js';
17
- import { writeAtomicSync } from './fs-atomic.js';
17
+ import { checkExecuteCodeEvidence } from './stage-contract.js';
18
18
  import { ConsistencyDoctor } from './progress/consistency-doctor.js';
19
19
  import { ChangeRegistry } from './progress/change-registry.js';
20
20
  import { StepStore } from './progress/step-store.js';
@@ -191,11 +191,11 @@ export class ProgressManager {
191
191
  }
192
192
  }
193
193
 
194
- /** 懒初始化 DB 连接,缓存在实例上 */
195
- async _ensureDB(cwd) {
194
+ /** 懒初始化 DB 连接,缓存在实例上(better-sqlite3 同步 API,init 无 async) */
195
+ _ensureDB(cwd) {
196
196
  if (!this._db) {
197
197
  this._db = new DB(this._runtimePath(cwd, 'sillyspec.db'));
198
- await this._db.init();
198
+ this._db.init();
199
199
  }
200
200
  return this._db;
201
201
  }
@@ -208,21 +208,19 @@ export class ProgressManager {
208
208
 
209
209
  // ── 全局状态 ──
210
210
 
211
- async readGlobal(cwd) {
211
+ readGlobal(cwd) {
212
212
  // SQL: SELECT FROM project + changes
213
- const db = await this._ensureDB(cwd);
213
+ const db = this._ensureDB(cwd);
214
214
  const sqlDb = db.getDb();
215
215
 
216
216
  // 读取 project 行(id=1)
217
- const rows = sqlDb.exec('SELECT name, schema_version FROM project WHERE id = 1');
218
- if (!rows || rows.length === 0 || rows[0].values.length === 0) return null;
219
- const [name, schemaVersion] = rows[0].values[0];
217
+ const row = sqlDb.prepare('SELECT name, schema_version FROM project WHERE id = 1').get();
218
+ if (row === undefined) return null;
219
+ const { name, schema_version: schemaVersion } = row;
220
220
 
221
221
  // 读取 active 变更列表
222
- const changeRows = sqlDb.exec("SELECT name FROM changes WHERE status = 'active' ORDER BY name");
223
- const activeChanges = changeRows && changeRows.length > 0
224
- ? changeRows[0].values.map(r => r[0])
225
- : [];
222
+ const changeRows = sqlDb.prepare("SELECT name FROM changes WHERE status = 'active' ORDER BY name").all();
223
+ const activeChanges = changeRows.map(r => r.name);
226
224
 
227
225
  return {
228
226
  _version: schemaVersion,
@@ -237,12 +235,12 @@ export class ProgressManager {
237
235
  * 读取指定变更的 progress(SQL 版)
238
236
  * @param {string} cwd
239
237
  * @param {string|null} changeName - 变更名,null 时尝试自动检测
240
- * @returns {Promise<object|null>} 与 SQLite 查询结果一致的 JS 对象
238
+ * @returns {object|null} 与 SQLite 查询结果一致的 JS 对象
241
239
  */
242
- async read(cwd, changeName = null) {
243
- // 自动检测变更名
240
+ read(cwd, changeName = null) {
241
+ // 自动检测变更名(listChanges 已在 task-08 同步化,此处直接拿到活跃变更数组)
244
242
  if (!changeName) {
245
- const changes = await this.listChanges(cwd);
243
+ const changes = this.listChanges(cwd);
246
244
  if (changes.length === 1) {
247
245
  changeName = changes[0];
248
246
  } else {
@@ -251,44 +249,39 @@ export class ProgressManager {
251
249
  }
252
250
  }
253
251
 
254
- const db = await this._ensureDB(cwd);
252
+ const db = this._ensureDB(cwd);
255
253
  const sqlDb = db.getDb();
256
254
 
257
255
  // 1. 从 changes 表获取基本信息
258
- const changeRows = sqlDb.exec('SELECT id, name, current_stage, no_worktree, last_active FROM changes WHERE name = ?', [changeName]);
259
- if (!changeRows || changeRows.length === 0 || changeRows[0].values.length === 0) return null;
260
- const [changeId, cName, currentStage, noWorktree, lastActive] = changeRows[0].values[0];
256
+ const changeRow = sqlDb.prepare('SELECT id, name, current_stage, no_worktree, last_active FROM changes WHERE name = ?').get(changeName);
257
+ if (changeRow === undefined) return null;
258
+ const { id: changeId, name: cName, current_stage: currentStage, no_worktree: noWorktree, last_active: lastActive } = changeRow;
261
259
 
262
260
  // 2. 从 stages 表获取所有阶段(含 revision 列)
263
- const stageRows = sqlDb.exec('SELECT id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason FROM stages WHERE change_id = ? ORDER BY id', [changeId]);
261
+ const stageRows = sqlDb.prepare('SELECT id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason FROM stages WHERE change_id = ? ORDER BY id').all(changeId);
264
262
  const stageMap = {};
265
263
  const stageIds = [];
266
- if (stageRows && stageRows.length > 0) {
267
- for (const [sId, stage, status, startedAt, completedAt, revision, reopenedFromStep, reopenedAt, staleReason] of stageRows[0].values) {
268
- stageMap[stage] = { _dbId: sId, status, startedAt, completedAt,
269
- ...(revision ? { revision } : {}),
270
- ...(reopenedFromStep ? { reopenedFromStep } : {}),
271
- ...(reopenedAt ? { reopenedAt } : {}),
272
- ...(staleReason ? { staleReason } : {}),
273
- };
274
- stageIds.push(sId);
275
- }
264
+ for (const row of stageRows) {
265
+ const { id: sId, stage, status, started_at: startedAt, completed_at: completedAt, revision, reopened_from_step: reopenedFromStep, reopened_at: reopenedAt, stale_reason: staleReason } = row;
266
+ stageMap[stage] = { _dbId: sId, status, startedAt, completedAt,
267
+ ...(revision ? { revision } : {}),
268
+ ...(reopenedFromStep ? { reopenedFromStep } : {}),
269
+ ...(reopenedAt ? { reopenedAt } : {}),
270
+ ...(staleReason ? { staleReason } : {}),
271
+ };
272
+ stageIds.push(sId);
276
273
  }
277
274
 
278
275
  // 3. 从 steps 表获取所有步骤
279
- let stepRows = null;
276
+ const stepsByStage = {};
280
277
  if (stageIds.length > 0) {
281
278
  const placeholders = stageIds.map(() => '?').join(',');
282
- stepRows = sqlDb.exec(
283
- `SELECT stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`,
284
- stageIds
285
- );
286
- }
287
- // 按阶段分组步骤
288
- const stepsByStage = {};
289
- if (stepRows && stepRows.length > 0) {
290
- for (const row of stepRows[0].values) {
291
- const [stageId, name, status, output, completedAt, ordering, waitReason, waitOptions, waitAnswer, waitedAt, waitAnswersJson, waitRound, maxWaitRounds] = row;
279
+ const stepRows = sqlDb.prepare(
280
+ `SELECT stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`
281
+ ).all(...stageIds);
282
+ // 按阶段分组步骤
283
+ for (const row of stepRows) {
284
+ const { stage_id: stageId, name, status, output, completed_at: completedAt, wait_reason: waitReason, wait_options: waitOptions, wait_answer: waitAnswer, waited_at: waitedAt, wait_answers: waitAnswersJson, wait_round: waitRound, max_wait_rounds: maxWaitRounds } = row;
292
285
  if (!stepsByStage[stageId]) stepsByStage[stageId] = [];
293
286
  let waitAnswers = null;
294
287
  if (waitAnswersJson) {
@@ -308,18 +301,16 @@ export class ProgressManager {
308
301
  }
309
302
 
310
303
  // 4. 从 batch_progress 表获取批量进度
311
- const batchRows = sqlDb.exec('SELECT total, completed, failed, skipped FROM batch_progress WHERE change_id = ?', [changeId]);
304
+ const batchRow = sqlDb.prepare('SELECT total, completed, failed, skipped FROM batch_progress WHERE change_id = ?').get(changeId);
312
305
  let batchProgress = undefined;
313
- if (batchRows && batchRows.length > 0 && batchRows[0].values.length > 0) {
314
- const [total, completed, failed, skipped] = batchRows[0].values[0];
306
+ if (batchRow !== undefined) {
307
+ const { total, completed, failed, skipped } = batchRow;
315
308
  batchProgress = { total, completed, failed, skipped };
316
309
  }
317
310
 
318
311
  // 5. 获取项目名
319
- const projectRows = sqlDb.exec('SELECT name FROM project WHERE id = 1');
320
- const projectName = (projectRows && projectRows.length > 0 && projectRows[0].values.length > 0)
321
- ? projectRows[0].values[0][0]
322
- : '';
312
+ const projectRow = sqlDb.prepare('SELECT name FROM project WHERE id = 1').get();
313
+ const projectName = projectRow !== undefined ? projectRow.name : '';
323
314
 
324
315
  // 6. 组装为兼容对象
325
316
  const stages = {};
@@ -378,34 +369,46 @@ export class ProgressManager {
378
369
  * @param {string} cwd
379
370
  * @param {object} data
380
371
  * @param {string|null} changeName - 从 data.currentChange 推导,或显式传入
372
+ *
373
+ * 持锁窗口评估(task-05 / R-08):本方法采用全量 UPSERT——无条件 UPDATE changes 行 +
374
+ * 遍历所有 stages(每 stage UPSERT stages 行 + 每 step 先 DELETE 再 INSERT 全量 + 末尾
375
+ * DELETE data 中不存在的多余 step)。评估过改"只写 diff(仅写变更过的 change/stage/step 行)"
376
+ * 以缩小事务持锁窗口、降低 BUSY 概率,结论:保持现状(全量 UPSERT)。
377
+ * 理由:
378
+ * 1. steps 表无 UNIQUE 约束(见循环内 DELETE-then-INSERT 注释),diff 写需先读现状再逐条
379
+ * 比较,复杂度高易错,易引入孤儿/缺失步骤破坏 FK 级联语义(design §3/§8);
380
+ * 2. busy_timeout=5000 + DB 层 SQLITE_BUSY 应用层有限重试(见 db.js transaction)已兜底
381
+ * 并发冲突,全量 UPSERT 的持锁窗口在此兜底下不是瓶颈;
382
+ * 3. 单进程内调用串行(design §9),事务窗口短(毫秒级),全量写正确性远高于 diff 的边际收益。
383
+ * 结论:全量 UPSERT 的简单正确性 > 持锁窗口优化的边际收益,不强行改 diff。
381
384
  */
382
- async _write(cwd, data, changeName = null) {
385
+ _write(cwd, data, changeName = null) {
383
386
  const cn = changeName || data.currentChange;
384
387
  if (!cn) {
385
388
  console.warn('⚠️ _write: 无变更名,跳过写入');
386
389
  return;
387
390
  }
388
391
 
389
- const db = await this._ensureDB(cwd);
390
- db.transaction((sqlDb) => {
392
+ const db = this._ensureDB(cwd);
393
+ db.transaction(() => {
394
+ const sqlDb = db.getDb();
391
395
  // 1. 更新 changes 表
392
396
  const now = new Date().toISOString();
393
397
  const noWorktree = data.noWorktree ? 1 : 0;
394
- sqlDb.run(
395
- 'UPDATE changes SET current_stage = ?, last_active = ?, no_worktree = ? WHERE name = ?',
396
- [data.currentStage || '', now, noWorktree, cn]
397
- );
398
+ sqlDb.prepare(
399
+ 'UPDATE changes SET current_stage = ?, last_active = ?, no_worktree = ? WHERE name = ?'
400
+ ).run(data.currentStage || '', now, noWorktree, cn);
398
401
 
399
402
  // 2. 获取 change_id
400
- const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [cn]);
401
- if (!changeRow || changeRow.length === 0 || changeRow[0].values.length === 0) return;
402
- const changeId = changeRow[0].values[0][0];
403
+ const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
404
+ if (changeRow === undefined) return;
405
+ const changeId = changeRow.id;
403
406
 
404
407
  // 3. 遍历 stages,UPSERT stages 表和 steps 表
405
408
  if (data.stages && typeof data.stages === 'object') {
406
409
  for (const [stageName, stageData] of Object.entries(data.stages)) {
407
410
  // UPSERT stages 行(含 revision 列)
408
- sqlDb.run(
411
+ sqlDb.prepare(
409
412
  `INSERT INTO stages (change_id, stage, status, started_at, completed_at, revision, reopened_from_step, reopened_at, stale_reason)
410
413
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
411
414
  ON CONFLICT(change_id, stage) DO UPDATE SET
@@ -415,15 +418,14 @@ export class ProgressManager {
415
418
  revision = COALESCE(excluded.revision, stages.revision),
416
419
  reopened_from_step = excluded.reopened_from_step,
417
420
  reopened_at = excluded.reopened_at,
418
- stale_reason = excluded.stale_reason`,
419
- [changeId, stageName, stageData.status || 'pending', stageData.startedAt || null, stageData.completedAt || null,
420
- stageData.revision || 0, stageData.reopenedFromStep || null, stageData.reopenedAt || null, stageData.staleReason || null]
421
- );
421
+ stale_reason = excluded.stale_reason`
422
+ ).run(changeId, stageName, stageData.status || 'pending', stageData.startedAt || null, stageData.completedAt || null,
423
+ stageData.revision || 0, stageData.reopenedFromStep || null, stageData.reopenedAt || null, stageData.staleReason || null);
422
424
 
423
425
  // 获取 stage_id
424
- const stageRow = sqlDb.exec('SELECT id FROM stages WHERE change_id = ? AND stage = ?', [changeId, stageName]);
425
- if (!stageRow || stageRow.length === 0 || stageRow[0].values.length === 0) continue;
426
- const stageId = stageRow[0].values[0][0];
426
+ const stageRow = sqlDb.prepare('SELECT id FROM stages WHERE change_id = ? AND stage = ?').get(changeId, stageName);
427
+ if (stageRow === undefined) continue;
428
+ const stageId = stageRow.id;
427
429
 
428
430
  // 收集 data 中的步骤名
429
431
  const stepNames = new Set();
@@ -432,86 +434,81 @@ export class ProgressManager {
432
434
  const step = stageData.steps[i];
433
435
  stepNames.add(step.name);
434
436
  // UPSERT 步骤(先删再插,steps 表无 UNIQUE 约束)
435
- sqlDb.run('DELETE FROM steps WHERE stage_id = ? AND name = ?', [stageId, step.name]);
436
- sqlDb.run(
437
- 'INSERT INTO steps (stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
438
- [stageId, step.name, step.status || 'pending', step.output || null, step.completedAt || null, i,
439
- step.waitReason ?? null, step.waitOptions ?? null, step.waitAnswer ?? null, step.waitedAt ?? null,
437
+ sqlDb.prepare('DELETE FROM steps WHERE stage_id = ? AND name = ?').run(stageId, step.name);
438
+ sqlDb.prepare(
439
+ 'INSERT INTO steps (stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at, wait_answers, wait_round, max_wait_rounds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
440
+ ).run(stageId, step.name, step.status || 'pending', step.output || null, step.completedAt || null, i,
441
+ step.waitReason ?? null, Array.isArray(step.waitOptions) ? JSON.stringify(step.waitOptions) : (step.waitOptions ?? null), step.waitAnswer ?? null, step.waitedAt ?? null,
440
442
  Array.isArray(step.waitAnswers) ? JSON.stringify(step.waitAnswers) : null,
441
- step.waitRound ?? null, step.maxWaitRounds ?? null]
442
- );
443
+ step.waitRound ?? null, step.maxWaitRounds ?? null);
443
444
  }
444
445
  }
445
446
 
446
447
  // 删除 data 中不存在的多余步骤
447
448
  if (stepNames.size > 0) {
448
449
  const namePlaceholders = [...stepNames].map(() => '?').join(',');
449
- sqlDb.run(
450
- `DELETE FROM steps WHERE stage_id = ? AND name NOT IN (${namePlaceholders})`,
451
- [stageId, ...stepNames]
452
- );
450
+ sqlDb.prepare(
451
+ `DELETE FROM steps WHERE stage_id = ? AND name NOT IN (${namePlaceholders})`
452
+ ).run(stageId, ...stepNames);
453
453
  } else {
454
454
  // data 中没有步骤,清空该阶段所有步骤
455
- sqlDb.run('DELETE FROM steps WHERE stage_id = ?', [stageId]);
455
+ sqlDb.prepare('DELETE FROM steps WHERE stage_id = ?').run(stageId);
456
456
  }
457
457
  }
458
458
  }
459
459
 
460
460
  // 4. UPSERT batch_progress
461
461
  if (data.batchProgress && typeof data.batchProgress === 'object') {
462
- sqlDb.run(
462
+ sqlDb.prepare(
463
463
  `INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
464
464
  VALUES (?, ?, ?, ?, ?)
465
465
  ON CONFLICT(change_id) DO UPDATE SET
466
466
  total = excluded.total,
467
467
  completed = excluded.completed,
468
468
  failed = excluded.failed,
469
- skipped = excluded.skipped`,
470
- [changeId, data.batchProgress.total || 0, data.batchProgress.completed || 0, data.batchProgress.failed || 0, data.batchProgress.skipped || 0]
471
- );
469
+ skipped = excluded.skipped`
470
+ ).run(changeId, data.batchProgress.total || 0, data.batchProgress.completed || 0, data.batchProgress.failed || 0, data.batchProgress.skipped || 0);
472
471
  }
473
472
  });
474
-
475
- await this._updateGateStatus(cwd);
476
473
  }
477
474
 
478
475
  // ── 变更管理 ──
479
476
 
480
- async listChanges(cwd) { return this._changeRegistry.listChanges(cwd); }
477
+ listChanges(cwd) { return this._changeRegistry.listChanges(cwd); }
481
478
 
482
- async registerChange(cwd, changeName) { return this._changeRegistry.registerChange(cwd, changeName); }
479
+ registerChange(cwd, changeName) { return this._changeRegistry.registerChange(cwd, changeName); }
483
480
 
484
- async updateChangeIsolation(cwd, changeName, isolation) { return this._changeRegistry.updateChangeIsolation(cwd, changeName, isolation); }
481
+ updateChangeIsolation(cwd, changeName, isolation) { return this._changeRegistry.updateChangeIsolation(cwd, changeName, isolation); }
485
482
 
486
- async readChangeIsolation(cwd, changeName) { return this._changeRegistry.readChangeIsolation(cwd, changeName); }
483
+ readChangeIsolation(cwd, changeName) { return this._changeRegistry.readChangeIsolation(cwd, changeName); }
487
484
 
488
- async _updatePlatformLastSync(cwd, changeName) { return this._changeRegistry._updatePlatformLastSync(cwd, changeName); }
485
+ _updatePlatformLastSync(cwd, changeName) { return this._changeRegistry._updatePlatformLastSync(cwd, changeName); }
489
486
 
490
- async _updateApprovalStatus(cwd, changeName, status, reason = null) { return this._changeRegistry._updateApprovalStatus(cwd, changeName, status, reason); }
487
+ _updateApprovalStatus(cwd, changeName, status, reason = null) { return this._changeRegistry._updateApprovalStatus(cwd, changeName, status, reason); }
491
488
 
492
- async renameChange(cwd, oldName, newName) { return this._changeRegistry.renameChange(cwd, oldName, newName); }
489
+ renameChange(cwd, oldName, newName) { return this._changeRegistry.renameChange(cwd, oldName, newName); }
493
490
 
494
- async unregisterChange(cwd, changeName) { return this._changeRegistry.unregisterChange(cwd, changeName); }
491
+ unregisterChange(cwd, changeName) { return this._changeRegistry.unregisterChange(cwd, changeName); }
495
492
 
496
493
  // ── CLI 命令 ──
497
494
 
498
- async init(cwd) {
495
+ init(cwd) {
499
496
  this._ensureRuntimeDir(cwd);
500
497
 
501
498
  // 初始化 DB(如不存在则创建文件 + 建表)
502
- const db = await this._ensureDB(cwd);
503
- db.transaction((sqlDb) => {
499
+ const db = this._ensureDB(cwd);
500
+ db.transaction(() => {
501
+ const sqlDb = db.getDb();
504
502
  const now = new Date().toISOString();
505
503
  const projectName = basename(cwd) || 'project';
506
504
 
507
505
  // 检查 project id=1 是否已存在
508
- const existing = sqlDb.exec('SELECT id FROM project WHERE id = 1');
509
- if (!existing || existing.length === 0 || existing[0].values.length === 0) {
510
- sqlDb.run(
506
+ const existing = sqlDb.prepare('SELECT id FROM project WHERE id = 1').get();
507
+ if (existing === undefined) {
508
+ sqlDb.prepare(
511
509
  `INSERT INTO project (id, name, schema_version, created_at, updated_at)
512
- VALUES (1, ?, ?, ?, ?)`,
513
- [projectName, CURRENT_VERSION, now, now]
514
- );
510
+ VALUES (1, ?, ?, ?, ?)`
511
+ ).run(projectName, CURRENT_VERSION, now, now);
515
512
  console.log(`✅ 已创建全局状态文件(SQLite)`);
516
513
  } else {
517
514
  console.log(`ℹ️ 全局状态文件已存在,跳过`);
@@ -525,14 +522,14 @@ export class ProgressManager {
525
522
  }
526
523
 
527
524
  this._ensureGitignore(cwd);
528
- return await this.readGlobal(cwd);
525
+ return this.readGlobal(cwd);
529
526
  }
530
527
 
531
528
  /**
532
529
  * 初始化指定变更的 progress
533
530
  * SQL: INSERT changes + 批量 INSERT stages
534
531
  */
535
- async initChange(cwd, changeName) {
532
+ initChange(cwd, changeName) {
536
533
  if (!changeName) {
537
534
  console.warn('⚠️ initChange: changeName 为空,跳过');
538
535
  return null;
@@ -543,103 +540,101 @@ export class ProgressManager {
543
540
  this._ensureChangeDir(cwd, changeName);
544
541
  }
545
542
 
546
- const db = await this._ensureDB(cwd);
547
- db.transaction((sqlDb) => {
543
+ const db = this._ensureDB(cwd);
544
+ db.transaction(() => {
545
+ const sqlDb = db.getDb();
548
546
  const now = new Date().toISOString();
549
547
 
550
548
  // 检查变更是否已存在
551
- const existing = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
552
- if (!existing || existing.length === 0 || existing[0].values.length === 0) {
549
+ const existing = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
550
+ if (existing === undefined) {
553
551
  // 插入 changes 行
554
- sqlDb.run(
552
+ sqlDb.prepare(
555
553
  `INSERT INTO changes (name, current_stage, status, created_at, last_active)
556
- VALUES (?, 'scan', 'active', ?, ?)`,
557
- [changeName, now, now]
558
- );
554
+ VALUES (?, 'scan', 'active', ?, ?)`
555
+ ).run(changeName, now, now);
559
556
  }
560
557
 
561
558
  // 获取 change_id
562
- const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
563
- const changeId = changeRow[0].values[0][0];
559
+ const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
560
+ const changeId = changeRow.id;
564
561
 
565
- // 批量插入 9 个阶段(INSERT OR IGNORE 跳过已存在的)
566
- const allStages = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
567
- for (const stage of allStages) {
568
- sqlDb.run(
562
+ // 批量插入所有合法阶段(INSERT OR IGNORE 跳过已存在的);沿用顶部 import 的 VALID_STAGES 单一源
563
+ for (const stage of VALID_STAGES) {
564
+ sqlDb.prepare(
569
565
  `INSERT OR IGNORE INTO stages (change_id, stage, status)
570
- VALUES (?, ?, 'pending')`,
571
- [changeId, stage]
572
- );
566
+ VALUES (?, ?, 'pending')`
567
+ ).run(changeId, stage);
573
568
  }
574
569
  });
575
570
 
576
571
  // 不再需要写文件:read() 已改为 SQL
577
- return await this.read(cwd, changeName);
572
+ return this.read(cwd, changeName);
578
573
  }
579
574
 
580
- async setStage(cwd, stage, changeName = null) { return this._stepStore.setStage(cwd, stage, changeName); }
575
+ setStage(cwd, stage, changeName = null) { return this._stepStore.setStage(cwd, stage, changeName); }
581
576
 
582
- async addStep(cwd, stage, stepName, changeName = null) { return this._stepStore.addStep(cwd, stage, stepName, changeName); }
577
+ addStep(cwd, stage, stepName, changeName = null) { return this._stepStore.addStep(cwd, stage, stepName, changeName); }
583
578
 
584
- async updateStep(cwd, stage, stepName, options = {}, changeName = null) { return this._stepStore.updateStep(cwd, stage, stepName, options, changeName); }
579
+ updateStep(cwd, stage, stepName, options = {}, changeName = null) { return this._stepStore.updateStep(cwd, stage, stepName, options, changeName); }
585
580
 
586
581
  _appendAuditLog(cwd, entry) { return this._consistency._appendAuditLog(cwd, entry); }
587
582
 
588
- async _validateStageArtifacts(cwd, stage, changeName) { return this._stageMachine._validateStageArtifacts(cwd, stage, changeName); }
583
+ _validateStageArtifacts(cwd, stage, changeName) { return this._stageMachine._validateStageArtifacts(cwd, stage, changeName); }
589
584
 
590
- async completeStage(cwd, stage, changeName = null, opts = {}) { return this._stageMachine.completeStage(cwd, stage, changeName, opts); }
585
+ completeStage(cwd, stage, changeName = null, opts = {}) { return this._stageMachine.completeStage(cwd, stage, changeName, opts); }
591
586
 
592
- async show(cwd, changeName = null) { return this._stageMachine.show(cwd, changeName); }
587
+ show(cwd, changeName = null) { return this._stageMachine.show(cwd, changeName); }
593
588
 
594
- async _showChange(cwd, changeName) { return this._stageMachine._showChange(cwd, changeName); }
589
+ _showChange(cwd, changeName) { return this._stageMachine._showChange(cwd, changeName); }
595
590
 
596
591
  _getNextSuggestion(data) { return this._stageMachine._getNextSuggestion(data); }
597
592
 
598
- async status(cwd, changeName = null) { return this._stageMachine.status(cwd, changeName); }
593
+ status(cwd, changeName = null) { return this._stageMachine.status(cwd, changeName); }
599
594
 
600
- async checkConsistency(cwd, changeName = null) { return this._consistency.checkConsistency(cwd, changeName); }
595
+ checkConsistency(cwd, changeName = null) { return this._consistency.checkConsistency(cwd, changeName); }
601
596
 
602
- async repairConsistency(cwd, opts = {}) { return this._consistency.repairConsistency(cwd, opts); }
597
+ repairConsistency(cwd, opts = {}) { return this._consistency.repairConsistency(cwd, opts); }
603
598
 
604
- async validate(cwd, changeName = null) { return this._stageMachine.validate(cwd, changeName); }
599
+ validate(cwd, changeName = null) { return this._stageMachine.validate(cwd, changeName); }
605
600
 
606
- async reopenStage(cwd, stage, opts = {}) { return this._stageMachine.reopenStage(cwd, stage, opts); }
601
+ reopenStage(cwd, stage, opts = {}) { return this._stageMachine.reopenStage(cwd, stage, opts); }
607
602
 
608
603
  _getDownstreamStages(stage) { return this._stageMachine._getDownstreamStages(stage); }
609
604
 
610
- async reset(cwd, stage, changeName = null) { return this._stageMachine.reset(cwd, stage, changeName); }
605
+ reset(cwd, stage, changeName = null) { return this._stageMachine.reset(cwd, stage, changeName); }
611
606
 
612
607
  // ── 内部辅助 ──
613
608
 
614
- async _readOrInit(cwd, changeName = null) {
615
- let data = await this.read(cwd, changeName);
609
+ _readOrInit(cwd, changeName = null) {
610
+ let data = this.read(cwd, changeName);
616
611
  if (!data) {
617
612
  // 尝试自动检测变更名
618
613
  if (!changeName) {
619
- const changes = await this.listChanges(cwd);
614
+ const changes = this.listChanges(cwd);
620
615
  if (changes.length === 1) changeName = changes[0];
621
616
  }
622
617
  if (changeName) {
623
618
  // 确保变更在 DB 中已初始化
624
- const db = await this._ensureDB(cwd);
625
- db.transaction((sqlDb) => {
619
+ const db = this._ensureDB(cwd);
620
+ db.transaction(() => {
621
+ const sqlDb = db.getDb();
626
622
  const now = new Date().toISOString();
627
- sqlDb.run(
628
- 'INSERT OR IGNORE INTO changes (name, current_stage, status, created_at, last_active) VALUES (?, "scan", "active", ?, ?)',
629
- [changeName, now, now]
630
- );
631
- const changeRow = sqlDb.exec('SELECT id FROM changes WHERE name = ?', [changeName]);
632
- if (changeRow && changeRow.length > 0 && changeRow[0].values.length > 0) {
633
- const changeId = changeRow[0].values[0][0];
623
+ sqlDb.prepare(
624
+ `INSERT OR IGNORE INTO changes (name, current_stage, status, created_at, last_active) VALUES (?, 'scan', 'active', ?, ?)`
625
+ ).run(changeName, now, now);
626
+ const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
627
+ if (changeRow !== undefined) {
628
+ const changeId = changeRow.id;
634
629
  for (const s of VALID_STAGES) {
635
- sqlDb.run('INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, "pending")', [changeId, s]);
630
+ sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, s);
636
631
  }
637
632
  }
638
633
  });
639
- await this.registerChange(cwd, changeName);
634
+ this.registerChange(cwd, changeName);
640
635
  }
641
636
  if (!data) {
642
- data = await this.read(cwd, changeName);
637
+ data = this.read(cwd, changeName);
643
638
  }
644
639
  if (!data) {
645
640
  console.log('❌ 无法确定当前变更,请指定 --change <name>');
@@ -649,12 +644,12 @@ export class ProgressManager {
649
644
  return data;
650
645
  }
651
646
 
652
- async _requireStage(cwd, stage, changeName = null) {
647
+ _requireStage(cwd, stage, changeName = null) {
653
648
  if (!VALID_STAGES.includes(stage)) {
654
649
  console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
655
650
  return null;
656
651
  }
657
- const data = await this._readOrInit(cwd, changeName);
652
+ const data = this._readOrInit(cwd, changeName);
658
653
  if (!data) return null;
659
654
  if (!data.stages[stage]) data.stages[stage] = emptyStage();
660
655
  return data;
@@ -664,84 +659,12 @@ export class ProgressManager {
664
659
 
665
660
  // ── 批量进度 ──
666
661
 
667
- async updateBatchProgress(cwd, batchData, changeName = null) { return this._stepStore.updateBatchProgress(cwd, batchData, changeName); }
662
+ updateBatchProgress(cwd, batchData, changeName = null) { return this._stepStore.updateBatchProgress(cwd, batchData, changeName); }
668
663
 
669
- async readBatchProgress(cwd, changeName = null) { return this._stepStore.readBatchProgress(cwd, changeName); }
664
+ readBatchProgress(cwd, changeName = null) { return this._stepStore.readBatchProgress(cwd, changeName); }
670
665
 
671
666
  _renderBatchProgress(batchProgress) { return this._stepStore._renderBatchProgress(batchProgress); }
672
667
 
673
- /**
674
- * 更新 gate-status.json,供 worktree-guard hook 读取
675
- * 从 SQLite 查询所有处于 execute/quick 阶段的活跃变更,生成或删除 gate-status.json
676
- */
677
- async _updateGateStatus(cwd) {
678
- const db = await this._ensureDB(cwd);
679
- const sqlDb = db.getDb();
680
-
681
- // SQL 查询:所有处于 execute/quick 阶段的活跃变更
682
- const rows = sqlDb.exec(
683
- `SELECT name, current_stage, no_worktree FROM changes
684
- WHERE status = 'active' AND current_stage IN ('execute', 'quick')`
685
- );
686
-
687
- const gatePath = this._runtimePath(cwd, 'gate-status.json');
688
-
689
- if (!rows || rows.length === 0 || rows[0].values.length === 0) {
690
- // 无 execute/quick 阶段的活跃变更:写墓碑而非 unlink。unlink 失败(Windows 杀毒/索引占用)
691
- // 会被空 catch 吞掉,陈旧 gate-status 残留 → hook 把 brainstorm 误当 execute 放行(fail-open)。
692
- // 墓碑 {stage:null} 让 readCurrentStage 的 `gateStatus.stage` 为假 → 自动 fallback DB,语义等价删除但原子。
693
- if (existsSync(gatePath)) {
694
- try {
695
- this._ensureRuntimeDir(cwd);
696
- writeAtomicSync(gatePath, JSON.stringify({ stage: null, tombstone: true, updatedAt: new Date().toISOString() }) + '\n');
697
- } catch (e) {
698
- console.warn(`⚠️ gate-status 墓碑写入失败(hook 可能读到陈旧缓存): ${e.message}`);
699
- }
700
- }
701
- return;
702
- }
703
-
704
- let gateStage = null;
705
- let hasNoWorktree = false;
706
- const activeChanges = [];
707
-
708
- for (const [name, stage, noWorktree] of rows[0].values) {
709
- if (!stage) continue;
710
- // 优先取 execute,其次 quick
711
- if (gateStage !== 'execute' || stage === 'execute') {
712
- gateStage = stage;
713
- }
714
- activeChanges.push(name);
715
- if (noWorktree === 1) hasNoWorktree = true;
716
- }
717
-
718
- if (!gateStage) {
719
- // current_stage 为 NULL 的边界情况,等同于无 execute/quick:同样写墓碑(见上 rationale)
720
- if (existsSync(gatePath)) {
721
- try {
722
- this._ensureRuntimeDir(cwd);
723
- writeAtomicSync(gatePath, JSON.stringify({ stage: null, tombstone: true, updatedAt: new Date().toISOString() }) + '\n');
724
- } catch (e) {
725
- console.warn(`⚠️ gate-status 墓碑写入失败(hook 可能读到陈旧缓存): ${e.message}`);
726
- }
727
- }
728
- return;
729
- }
730
-
731
- try {
732
- this._ensureRuntimeDir(cwd);
733
- const gateData = {
734
- stage: gateStage,
735
- changes: activeChanges,
736
- updatedAt: new Date().toISOString(),
737
- ...(hasNoWorktree ? { noWorktree: true } : {}),
738
- };
739
- writeAtomicSync(gatePath, JSON.stringify(gateData, null, 2) + '\n');
740
- } catch (err) {
741
- console.warn('⚠️ 写入 gate-status.json 失败:', err.message);
742
- }
743
- }
744
-
745
668
  _ensureGitignore(cwd) {
746
669
  // 外部 specDir 不需要修改项目 .gitignore
747
670
  if (this._customSpecDir) return;
@@ -801,14 +724,14 @@ export class ProgressManager {
801
724
  * @param {string} specBase - platformOpts.specRoot || join(cwd, '.sillyspec'),用于定位 changes 目录
802
725
  * @param {object} [opts]
803
726
  * @param {boolean} [opts.confirm=false] - 默认 dry-run,仅当 confirm=true 才落盘
804
- * @returns {Promise<{ ok: boolean, aligned: number, skipped: number, planTotal: number, planChecked: number, reason?: string, dryRun?: boolean }>}
727
+ * @returns {{ ok: boolean, aligned: number, skipped: number, planTotal: number, planChecked: number, reason?: string, dryRun?: boolean }}
805
728
  */
806
- async alignExecuteToPlan(cwd, changeName, specBase, opts = {}) {
729
+ alignExecuteToPlan(cwd, changeName, specBase, opts = {}) {
807
730
  if (!changeName) throw new Error('changeName 不能为空');
808
731
  const { confirm = false } = opts;
809
732
 
810
733
  // 1. 读 progress;无 progress / 无 execute 阶段 → 拒绝
811
- const progress = await this.read(cwd, changeName);
734
+ const progress = this.read(cwd, changeName);
812
735
  if (!progress || !progress.stages || !progress.stages.execute) {
813
736
  return { ok: false, aligned: 0, skipped: 0, planTotal: 0, planChecked: 0, reason: 'execute 阶段无进度数据' };
814
737
  }
@@ -840,7 +763,6 @@ export class ProgressManager {
840
763
  // 对齐前用 git 客观核验是否存在真实代码变更(能确证零变更时才拒绝,
841
764
  // unknown 不阻断——worktree 已清理且变更已提交的正常场景无法对账)。
842
765
  try {
843
- const { checkExecuteCodeEvidence } = await import('./stage-contract.js');
844
766
  const evidence = checkExecuteCodeEvidence(cwd, changeName);
845
767
  if (evidence.status === 'unchanged') {
846
768
  return {
@@ -882,7 +804,7 @@ export class ProgressManager {
882
804
  executeStage.completedAt = now;
883
805
  progress.currentStage = progress.currentStage || 'execute';
884
806
  progress.lastActive = now;
885
- await this._write(cwd, progress, changeName);
807
+ this._write(cwd, progress, changeName);
886
808
 
887
809
  return { ok: true, aligned, skipped, planTotal, planChecked };
888
810
  }