sillyspec 3.25.8 → 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.
- package/.claude/skills/sillyspec-auto/SKILL.md +8 -13
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
- package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
- package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
- package/README.md +9 -0
- package/package.json +2 -2
- package/src/change-list.js +11 -8
- package/src/change-risk-profile.js +22 -0
- package/src/db.js +160 -159
- package/src/dispatch/backends/local-agent.js +152 -0
- package/src/dispatch/backends/sillyhub-mcp.js +242 -0
- package/src/dispatch/probe.js +208 -0
- package/src/dispatch/strategy.js +121 -0
- package/src/doctor-diagnostics.js +19 -25
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +66 -0
- package/src/hooks/worktree-guard.js +64 -55
- package/src/index.js +215 -34
- package/src/init.js +7 -14
- package/src/machine-interface.js +54 -19
- package/src/migrate.js +14 -4
- package/src/modules.js +16 -5
- package/src/progress/change-registry.js +72 -70
- package/src/progress/consistency-doctor.js +71 -14
- package/src/progress/shared.js +6 -3
- package/src/progress/stage-machine.js +61 -65
- package/src/progress/step-store.js +62 -67
- package/src/progress.js +155 -233
- package/src/quicklog.js +13 -5
- package/src/run/command.js +65 -37
- package/src/run/complete-handlers.js +1005 -889
- package/src/run/complete.js +115 -208
- package/src/run/concurrent-detect.js +132 -0
- package/src/run/gates.js +289 -17
- package/src/run/prompt.js +22 -68
- package/src/run/quick-audit.js +10 -9
- package/src/run/shared.js +648 -600
- package/src/run/stage.js +43 -24
- package/src/scan-postcheck.js +9 -31
- package/src/sillyhub-mcp/client.js +389 -0
- package/src/spec-dir-typo.js +71 -0
- package/src/stage-contract-engine.js +22 -1
- package/src/stage-contract-spec.js +11 -4
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +13 -9
- package/src/stages/archive.js +4 -6
- package/src/stages/brainstorm-auto.js +26 -20
- package/src/stages/brainstorm.js +29 -5
- package/src/stages/cmd-existence.js +145 -0
- package/src/stages/doctor.js +9 -6
- package/src/stages/execute.js +61 -20
- package/src/stages/plan-postcheck.js +152 -0
- package/src/stages/plan.js +16 -31
- package/src/stages/quick.js +1 -1
- package/src/stages/scan.js +9 -3
- package/src/stages/status.js +0 -1
- package/src/stages/verify.js +12 -7
- package/src/sync.js +4 -4
- package/src/task-review.js +30 -1
- package/src/verify-postcheck.js +933 -862
- package/src/version.js +19 -0
- package/src/worktree-apply.js +85 -68
- package/src/worktree-deps.js +89 -5
- package/src/worktree.js +213 -111
- package/templates/prompts/taskcard-rules.md +27 -0
- package/templates/prompts/verify-probes.md +3 -3
- package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
+
readGlobal(cwd) {
|
|
212
212
|
// SQL: SELECT FROM project + changes
|
|
213
|
-
const db =
|
|
213
|
+
const db = this._ensureDB(cwd);
|
|
214
214
|
const sqlDb = db.getDb();
|
|
215
215
|
|
|
216
216
|
// 读取 project 行(id=1)
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
const
|
|
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.
|
|
223
|
-
const activeChanges = changeRows
|
|
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 {
|
|
238
|
+
* @returns {object|null} 与 SQLite 查询结果一致的 JS 对象
|
|
241
239
|
*/
|
|
242
|
-
|
|
243
|
-
//
|
|
240
|
+
read(cwd, changeName = null) {
|
|
241
|
+
// 自动检测变更名(listChanges 已在 task-08 同步化,此处直接拿到活跃变更数组)
|
|
244
242
|
if (!changeName) {
|
|
245
|
-
const changes =
|
|
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 =
|
|
252
|
+
const db = this._ensureDB(cwd);
|
|
255
253
|
const sqlDb = db.getDb();
|
|
256
254
|
|
|
257
255
|
// 1. 从 changes 表获取基本信息
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
const
|
|
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.
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
276
|
+
const stepsByStage = {};
|
|
280
277
|
if (stageIds.length > 0) {
|
|
281
278
|
const placeholders = stageIds.map(() => '?').join(',');
|
|
282
|
-
stepRows = sqlDb.
|
|
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
|
-
|
|
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
|
|
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 (
|
|
314
|
-
const
|
|
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
|
|
320
|
-
const projectName =
|
|
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
|
-
|
|
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 =
|
|
390
|
-
db.transaction((
|
|
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.
|
|
395
|
-
'UPDATE changes SET current_stage = ?, last_active = ?, no_worktree = ? WHERE name = ?'
|
|
396
|
-
|
|
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.
|
|
401
|
-
if (
|
|
402
|
-
const changeId = changeRow
|
|
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.
|
|
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
|
-
|
|
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.
|
|
425
|
-
if (
|
|
426
|
-
const stageId = stageRow
|
|
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.
|
|
436
|
-
sqlDb.
|
|
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
|
-
|
|
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.
|
|
450
|
-
`DELETE FROM steps WHERE stage_id = ? AND name NOT IN (${namePlaceholders})
|
|
451
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
477
|
+
listChanges(cwd) { return this._changeRegistry.listChanges(cwd); }
|
|
481
478
|
|
|
482
|
-
|
|
479
|
+
registerChange(cwd, changeName) { return this._changeRegistry.registerChange(cwd, changeName); }
|
|
483
480
|
|
|
484
|
-
|
|
481
|
+
updateChangeIsolation(cwd, changeName, isolation) { return this._changeRegistry.updateChangeIsolation(cwd, changeName, isolation); }
|
|
485
482
|
|
|
486
|
-
|
|
483
|
+
readChangeIsolation(cwd, changeName) { return this._changeRegistry.readChangeIsolation(cwd, changeName); }
|
|
487
484
|
|
|
488
|
-
|
|
485
|
+
_updatePlatformLastSync(cwd, changeName) { return this._changeRegistry._updatePlatformLastSync(cwd, changeName); }
|
|
489
486
|
|
|
490
|
-
|
|
487
|
+
_updateApprovalStatus(cwd, changeName, status, reason = null) { return this._changeRegistry._updateApprovalStatus(cwd, changeName, status, reason); }
|
|
491
488
|
|
|
492
|
-
|
|
489
|
+
renameChange(cwd, oldName, newName) { return this._changeRegistry.renameChange(cwd, oldName, newName); }
|
|
493
490
|
|
|
494
|
-
|
|
491
|
+
unregisterChange(cwd, changeName) { return this._changeRegistry.unregisterChange(cwd, changeName); }
|
|
495
492
|
|
|
496
493
|
// ── CLI 命令 ──
|
|
497
494
|
|
|
498
|
-
|
|
495
|
+
init(cwd) {
|
|
499
496
|
this._ensureRuntimeDir(cwd);
|
|
500
497
|
|
|
501
498
|
// 初始化 DB(如不存在则创建文件 + 建表)
|
|
502
|
-
const db =
|
|
503
|
-
db.transaction((
|
|
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.
|
|
509
|
-
if (
|
|
510
|
-
sqlDb.
|
|
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
|
-
|
|
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
|
|
525
|
+
return this.readGlobal(cwd);
|
|
529
526
|
}
|
|
530
527
|
|
|
531
528
|
/**
|
|
532
529
|
* 初始化指定变更的 progress
|
|
533
530
|
* SQL: INSERT changes + 批量 INSERT stages
|
|
534
531
|
*/
|
|
535
|
-
|
|
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 =
|
|
547
|
-
db.transaction((
|
|
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.
|
|
552
|
-
if (
|
|
549
|
+
const existing = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
|
|
550
|
+
if (existing === undefined) {
|
|
553
551
|
// 插入 changes 行
|
|
554
|
-
sqlDb.
|
|
552
|
+
sqlDb.prepare(
|
|
555
553
|
`INSERT INTO changes (name, current_stage, status, created_at, last_active)
|
|
556
|
-
VALUES (?, 'scan', 'active', ?, ?)
|
|
557
|
-
|
|
558
|
-
);
|
|
554
|
+
VALUES (?, 'scan', 'active', ?, ?)`
|
|
555
|
+
).run(changeName, now, now);
|
|
559
556
|
}
|
|
560
557
|
|
|
561
558
|
// 获取 change_id
|
|
562
|
-
const changeRow = sqlDb.
|
|
563
|
-
const changeId = changeRow
|
|
559
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
|
|
560
|
+
const changeId = changeRow.id;
|
|
564
561
|
|
|
565
|
-
//
|
|
566
|
-
const
|
|
567
|
-
|
|
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
|
-
|
|
572
|
-
);
|
|
566
|
+
VALUES (?, ?, 'pending')`
|
|
567
|
+
).run(changeId, stage);
|
|
573
568
|
}
|
|
574
569
|
});
|
|
575
570
|
|
|
576
571
|
// 不再需要写文件:read() 已改为 SQL
|
|
577
|
-
return
|
|
572
|
+
return this.read(cwd, changeName);
|
|
578
573
|
}
|
|
579
574
|
|
|
580
|
-
|
|
575
|
+
setStage(cwd, stage, changeName = null) { return this._stepStore.setStage(cwd, stage, changeName); }
|
|
581
576
|
|
|
582
|
-
|
|
577
|
+
addStep(cwd, stage, stepName, changeName = null) { return this._stepStore.addStep(cwd, stage, stepName, changeName); }
|
|
583
578
|
|
|
584
|
-
|
|
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
|
-
|
|
583
|
+
_validateStageArtifacts(cwd, stage, changeName) { return this._stageMachine._validateStageArtifacts(cwd, stage, changeName); }
|
|
589
584
|
|
|
590
|
-
|
|
585
|
+
completeStage(cwd, stage, changeName = null, opts = {}) { return this._stageMachine.completeStage(cwd, stage, changeName, opts); }
|
|
591
586
|
|
|
592
|
-
|
|
587
|
+
show(cwd, changeName = null) { return this._stageMachine.show(cwd, changeName); }
|
|
593
588
|
|
|
594
|
-
|
|
589
|
+
_showChange(cwd, changeName) { return this._stageMachine._showChange(cwd, changeName); }
|
|
595
590
|
|
|
596
591
|
_getNextSuggestion(data) { return this._stageMachine._getNextSuggestion(data); }
|
|
597
592
|
|
|
598
|
-
|
|
593
|
+
status(cwd, changeName = null) { return this._stageMachine.status(cwd, changeName); }
|
|
599
594
|
|
|
600
|
-
|
|
595
|
+
checkConsistency(cwd, changeName = null) { return this._consistency.checkConsistency(cwd, changeName); }
|
|
601
596
|
|
|
602
|
-
|
|
597
|
+
repairConsistency(cwd, opts = {}) { return this._consistency.repairConsistency(cwd, opts); }
|
|
603
598
|
|
|
604
|
-
|
|
599
|
+
validate(cwd, changeName = null) { return this._stageMachine.validate(cwd, changeName); }
|
|
605
600
|
|
|
606
|
-
|
|
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
|
-
|
|
605
|
+
reset(cwd, stage, changeName = null) { return this._stageMachine.reset(cwd, stage, changeName); }
|
|
611
606
|
|
|
612
607
|
// ── 内部辅助 ──
|
|
613
608
|
|
|
614
|
-
|
|
615
|
-
let data =
|
|
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 =
|
|
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 =
|
|
625
|
-
db.transaction((
|
|
619
|
+
const db = this._ensureDB(cwd);
|
|
620
|
+
db.transaction(() => {
|
|
621
|
+
const sqlDb = db.getDb();
|
|
626
622
|
const now = new Date().toISOString();
|
|
627
|
-
sqlDb.
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
);
|
|
631
|
-
|
|
632
|
-
|
|
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.
|
|
630
|
+
sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, s);
|
|
636
631
|
}
|
|
637
632
|
}
|
|
638
633
|
});
|
|
639
|
-
|
|
634
|
+
this.registerChange(cwd, changeName);
|
|
640
635
|
}
|
|
641
636
|
if (!data) {
|
|
642
|
-
data =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
662
|
+
updateBatchProgress(cwd, batchData, changeName = null) { return this._stepStore.updateBatchProgress(cwd, batchData, changeName); }
|
|
668
663
|
|
|
669
|
-
|
|
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 {
|
|
727
|
+
* @returns {{ ok: boolean, aligned: number, skipped: number, planTotal: number, planChecked: number, reason?: string, dryRun?: boolean }}
|
|
805
728
|
*/
|
|
806
|
-
|
|
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 =
|
|
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
|
-
|
|
807
|
+
this._write(cwd, progress, changeName);
|
|
886
808
|
|
|
887
809
|
return { ok: true, aligned, skipped, planTotal, planChecked };
|
|
888
810
|
}
|