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.
- 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-quick/SKILL.md +3 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
- package/.sillyspec-platform-cleaned +1 -0
- package/.sillyspec-platform.json +7 -0
- 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 +170 -161
- package/src/dispatch/backends/local-agent.js +152 -0
- package/src/dispatch/backends/sillyhub-mcp.js +242 -0
- package/src/dispatch/probe.js +210 -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 +326 -36
- package/src/init.js +7 -14
- package/src/local-detect.js +120 -110
- 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 +85 -70
- package/src/progress/consistency-doctor.js +71 -14
- package/src/progress/shared.js +8 -5
- package/src/progress/stage-machine.js +67 -65
- package/src/progress/step-store.js +72 -67
- package/src/progress.js +448 -236
- package/src/quicklog.js +59 -12
- package/src/run/command.js +78 -39
- package/src/run/complete-handlers.js +1028 -889
- package/src/run/complete.js +132 -212
- package/src/run/concurrent-detect.js +132 -0
- package/src/run/gates.js +302 -19
- package/src/run/prompt.js +22 -68
- package/src/run/quick-audit.js +10 -9
- package/src/run/shared.js +722 -600
- package/src/run/stage.js +43 -24
- package/src/scan-postcheck.js +9 -31
- package/src/sillyhub-mcp/client.js +394 -0
- package/src/sillyhub-mcp/config.js +62 -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 +118 -10
- 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 +10 -7
- package/src/stages/execute.js +64 -22
- package/src/stages/plan-postcheck.js +152 -0
- package/src/stages/plan.js +16 -31
- package/src/stages/quick.js +25 -9
- package/src/stages/scan.js +53 -8
- package/src/stages/status.js +0 -1
- package/src/stages/verify.js +14 -9
- package/src/sync.js +433 -16
- package/src/task-review.js +59 -1
- package/src/verify-postcheck.js +938 -862
- package/src/version.js +19 -0
- package/src/worktree-apply.js +218 -93
- 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
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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((
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
97
|
-
if (stageRow
|
|
98
|
-
const stageId = stageRow
|
|
99
|
-
sqlDb.
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
121
|
+
show(cwd, changeName = null) {
|
|
126
122
|
// 如果指定了变更名,只显示该变更
|
|
127
123
|
if (changeName) {
|
|
128
|
-
return
|
|
124
|
+
return this._showChange(cwd, changeName);
|
|
129
125
|
}
|
|
130
126
|
|
|
131
127
|
// 否则显示所有变更
|
|
132
|
-
const changes =
|
|
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
|
|
135
|
+
return this._showChange(cwd, changes[0]);
|
|
140
136
|
}
|
|
141
137
|
|
|
142
138
|
// 多个变更:汇总显示
|
|
143
|
-
const global =
|
|
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 =
|
|
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
|
-
|
|
171
|
-
const data =
|
|
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
|
-
|
|
317
|
-
|
|
312
|
+
status(cwd, changeName = null) {
|
|
313
|
+
this.show(cwd, changeName);
|
|
318
314
|
}
|
|
319
315
|
|
|
320
|
-
|
|
321
|
-
const data =
|
|
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
|
-
|
|
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
|
-
|
|
364
|
+
reopenStage(cwd, stage, opts = {}) {
|
|
369
365
|
const { fromStep, changeName = null } = opts;
|
|
370
366
|
|
|
371
|
-
const data =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
466
|
+
reset(cwd, stage, changeName = null) {
|
|
471
467
|
if (stage) {
|
|
472
|
-
const data =
|
|
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
|
-
|
|
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 =
|
|
490
|
-
db.transaction((
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
sqlDb.
|
|
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.
|
|
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 =
|
|
503
|
+
const changes = this.pm.listChanges(cwd);
|
|
505
504
|
console.warn(`⚠️ 即将重置【所有 ${changes.length} 个变更】的进度(仅 stage 状态,产物文件不动)。此操作不可逆且范围最大。`);
|
|
506
|
-
const db =
|
|
507
|
-
db.transaction((
|
|
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.
|
|
510
|
-
if (changeRow
|
|
511
|
-
const changeId = changeRow
|
|
512
|
-
sqlDb.
|
|
513
|
-
sqlDb.
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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((
|
|
33
|
+
db.transaction(() => {
|
|
34
|
+
const sqlDb = db.getDb();
|
|
34
35
|
// 确保 change 存在
|
|
35
|
-
const changeRow = sqlDb.
|
|
36
|
-
if (
|
|
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
|
|
40
|
+
const changeId = changeRow.id;
|
|
40
41
|
|
|
41
42
|
// UPDATE changes.current_stage + last_active
|
|
42
|
-
sqlDb.
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
66
|
+
addStep(cwd, stage, stepName, changeName = null) {
|
|
69
67
|
if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
|
|
70
68
|
|
|
71
|
-
const db =
|
|
69
|
+
const db = this.pm._ensureDB(cwd);
|
|
72
70
|
|
|
73
71
|
// 获取变更名
|
|
74
72
|
let cn = changeName;
|
|
75
73
|
if (!cn) {
|
|
76
|
-
const changes =
|
|
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.
|
|
84
|
-
'SELECT s.id FROM stages s JOIN changes c ON s.change_id = c.id WHERE c.name = ? AND s.stage = ?'
|
|
85
|
-
|
|
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
|
|
89
|
+
const stageId = stageRow.id;
|
|
93
90
|
|
|
94
91
|
// 重复步骤名检查
|
|
95
|
-
const dupRow = sqlDb.
|
|
96
|
-
if (dupRow
|
|
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((
|
|
103
|
-
tDb.
|
|
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
|
-
|
|
107
|
-
);
|
|
108
|
-
|
|
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
|
-
|
|
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 =
|
|
117
|
+
const db = this.pm._ensureDB(cwd);
|
|
119
118
|
|
|
120
119
|
// 获取变更名
|
|
121
120
|
let cn = changeName;
|
|
122
121
|
if (!cn) {
|
|
123
|
-
const changes =
|
|
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.
|
|
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
|
-
|
|
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
|
|
145
|
+
const stepId = stepRow.id;
|
|
148
146
|
|
|
149
147
|
// UPDATE steps
|
|
150
148
|
let stageCompletionCandidateId = null;
|
|
151
|
-
db.transaction((
|
|
149
|
+
db.transaction(() => {
|
|
150
|
+
const tDb = db.getDb();
|
|
152
151
|
const now = new Date().toISOString();
|
|
153
152
|
if (status) {
|
|
154
|
-
tDb.
|
|
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.
|
|
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.
|
|
165
|
-
if (stRow
|
|
166
|
-
const stId = stRow
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
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.
|
|
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 =
|
|
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((
|
|
198
|
-
tDb
|
|
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
|
-
|
|
209
|
+
updateBatchProgress(cwd, batchData, changeName = null) {
|
|
208
210
|
const cn = changeName || null;
|
|
209
211
|
|
|
210
|
-
const db =
|
|
211
|
-
db.transaction((
|
|
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.
|
|
216
|
-
if (row
|
|
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.
|
|
221
|
-
if (rows
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
const data =
|
|
243
|
+
readBatchProgress(cwd, changeName = null) {
|
|
244
|
+
const data = this.pm.read(cwd, changeName);
|
|
240
245
|
return data?.batchProgress || null;
|
|
241
246
|
}
|
|
242
247
|
|