sillyspec 3.26.9 → 3.26.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 (98) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +85 -85
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
  4. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  5. package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
  6. package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
  7. package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
  8. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  9. package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
  10. package/.claude/skills/sillyspec-quick/SKILL.md +111 -111
  11. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  12. package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
  13. package/.claude/skills/sillyspec-state/SKILL.md +64 -64
  14. package/.claude/skills/sillyspec-status/SKILL.md +54 -54
  15. package/.claude/skills/sillyspec-verify/SKILL.md +83 -81
  16. package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
  17. package/.gitattributes +2 -0
  18. package/.husky/pre-push +20 -20
  19. package/.npmrc-publish +2 -0
  20. package/README.md +201 -201
  21. package/SKILL.md +90 -90
  22. package/package.json +39 -39
  23. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  24. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  25. package/packages/dashboard/dist/index.html +16 -16
  26. package/packages/dashboard/package.json +25 -25
  27. package/packages/dashboard/server/executor.js +86 -86
  28. package/packages/dashboard/server/index.js +601 -601
  29. package/packages/dashboard/server/parser.js +526 -526
  30. package/src/change-risk-profile.js +254 -254
  31. package/src/classify-change.js +109 -109
  32. package/src/config-schema.js +310 -310
  33. package/src/contract-matrix.js +379 -379
  34. package/src/db.js +350 -350
  35. package/src/dispatch/backends/local-agent.js +155 -155
  36. package/src/dispatch/backends/sillyhub-mcp.js +245 -245
  37. package/src/dispatch/probe.js +210 -210
  38. package/src/dispatch/strategy.js +121 -121
  39. package/src/docs-check.js +131 -4
  40. package/src/doctor-diagnostics.js +711 -711
  41. package/src/endpoint-extractor.js +401 -401
  42. package/src/fs-atomic.js +69 -69
  43. package/src/git-helper.js +79 -79
  44. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  45. package/src/hooks/worktree-guard.js +848 -848
  46. package/src/index.js +90 -8
  47. package/src/init.js +623 -623
  48. package/src/knowledge-match.js +130 -130
  49. package/src/local-detect.js +120 -120
  50. package/src/machine-interface.js +543 -543
  51. package/src/migrate.js +127 -127
  52. package/src/progress/change-registry.js +297 -260
  53. package/src/progress/consistency-doctor.js +355 -355
  54. package/src/progress/shared.js +36 -36
  55. package/src/progress/stage-machine.js +29 -1
  56. package/src/progress/step-store.js +261 -261
  57. package/src/progress.js +1131 -1127
  58. package/src/quicklog.js +693 -675
  59. package/src/review-tier.js +77 -77
  60. package/src/run/command.js +1385 -1364
  61. package/src/run/complete-handlers.js +198 -9
  62. package/src/run/complete.js +225 -24
  63. package/src/run/concurrent-detect.js +22 -0
  64. package/src/run/gates.js +755 -755
  65. package/src/run/prompt.js +761 -721
  66. package/src/run/quick-audit.js +155 -146
  67. package/src/run/shared.js +1268 -1197
  68. package/src/run/stage.js +487 -479
  69. package/src/scan-postcheck.js +371 -371
  70. package/src/setup.js +416 -416
  71. package/src/sillyhub-mcp/client.js +529 -529
  72. package/src/spec-sync.js +323 -0
  73. package/src/stage-contract.js +897 -897
  74. package/src/stage-review.js +549 -549
  75. package/src/stages/archive.js +155 -154
  76. package/src/stages/brainstorm-auto.js +236 -236
  77. package/src/stages/brainstorm.js +596 -597
  78. package/src/stages/cmd-existence.js +145 -145
  79. package/src/stages/doctor.js +371 -371
  80. package/src/stages/execute.js +1003 -991
  81. package/src/stages/explore.js +34 -34
  82. package/src/stages/index.js +29 -29
  83. package/src/stages/knowledge.js +498 -498
  84. package/src/stages/plan-postcheck.js +1302 -1281
  85. package/src/stages/plan.js +625 -604
  86. package/src/stages/quick.js +151 -143
  87. package/src/stages/scan.js +606 -605
  88. package/src/stages/status.js +64 -64
  89. package/src/stages/verify.js +264 -261
  90. package/src/sync.js +1223 -1125
  91. package/src/task-review.js +1019 -1019
  92. package/src/verify-postcheck.js +1186 -1186
  93. package/src/workflow.js +600 -600
  94. package/src/worktree-apply.js +1213 -1136
  95. package/src/worktree-deps.js +353 -353
  96. package/src/worktree.js +1481 -1437
  97. package/templates/workflows/archive-impact.yaml +79 -79
  98. package/templates/workflows/scan-docs.yaml +132 -132
@@ -1,36 +1,36 @@
1
- // W6 Step9 共享常量:progress.js facade 与 src/progress/* 子模块共用,避免 facade↔子模块循环引用。
2
- // 9a: STAGE_ORDER/MAIN_FLOW_ORDER;9c: VALID_STAGES/STAGE_LABELS;9d: SPEC_DIR_NAME/CURRENT_VERSION/emptyStage。
3
-
4
- export const SPEC_DIR_NAME = '.sillyspec';
5
-
6
- // 合法阶段名(含辅助阶段 scan/quick/explore)
7
- export const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
8
-
9
- // 阶段展示文案
10
- export const STAGE_LABELS = {
11
- brainstorm: '🧠 需求探索',
12
- plan: '📐 实现计划',
13
- execute: '⚡ 波次执行',
14
- verify: '🔍 验证确认',
15
- scan: '🔍 代码扫描',
16
- quick: '⚡ 快速任务',
17
- explore: '🧭 自由探索',
18
- archive: '📦 归档变更',
19
- };
20
-
21
- // 完整阶段顺序(含 scan),仅用于展示/迭代顺序
22
- export const STAGE_ORDER = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive'];
23
- // 主流程阶段(不含 scan/quick/explore 等辅助阶段)—— 用于下游 cascade / 一致性上下游判定。
24
- // scan 是 auxiliary(AUXILIARY_STAGES,按需显式跑、可 failed_post_check、永不要求 completed),
25
- // 把它算进主流程上下游会让「scan stale/revising」误报 brainstorm/plan/execute 不该 completed
26
- // (consistency-doctor 坑 + prompt-control-debt plan-c 同根因)。
27
- export const MAIN_FLOW_ORDER = ['brainstorm', 'plan', 'execute', 'verify', 'archive'];
28
-
29
- // progress 数据版本(v3 = SQLite;v4 = changes 加 base_ts / 本地脏度两列,platform-sync §8)
30
- export const CURRENT_VERSION = 5;
31
-
32
- // 空阶段骨架
33
- export function emptyStage() {
34
- return { status: 'pending', steps: [], startedAt: null, completedAt: null };
35
- }
36
-
1
+ // W6 Step9 共享常量:progress.js facade 与 src/progress/* 子模块共用,避免 facade↔子模块循环引用。
2
+ // 9a: STAGE_ORDER/MAIN_FLOW_ORDER;9c: VALID_STAGES/STAGE_LABELS;9d: SPEC_DIR_NAME/CURRENT_VERSION/emptyStage。
3
+
4
+ export const SPEC_DIR_NAME = '.sillyspec';
5
+
6
+ // 合法阶段名(含辅助阶段 scan/quick/explore)
7
+ export const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
8
+
9
+ // 阶段展示文案
10
+ export const STAGE_LABELS = {
11
+ brainstorm: '🧠 需求探索',
12
+ plan: '📐 实现计划',
13
+ execute: '⚡ 波次执行',
14
+ verify: '🔍 验证确认',
15
+ scan: '🔍 代码扫描',
16
+ quick: '⚡ 快速任务',
17
+ explore: '🧭 自由探索',
18
+ archive: '📦 归档变更',
19
+ };
20
+
21
+ // 完整阶段顺序(含 scan),仅用于展示/迭代顺序
22
+ export const STAGE_ORDER = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive'];
23
+ // 主流程阶段(不含 scan/quick/explore 等辅助阶段)—— 用于下游 cascade / 一致性上下游判定。
24
+ // scan 是 auxiliary(AUXILIARY_STAGES,按需显式跑、可 failed_post_check、永不要求 completed),
25
+ // 把它算进主流程上下游会让「scan stale/revising」误报 brainstorm/plan/execute 不该 completed
26
+ // (consistency-doctor 坑 + prompt-control-debt plan-c 同根因)。
27
+ export const MAIN_FLOW_ORDER = ['brainstorm', 'plan', 'execute', 'verify', 'archive'];
28
+
29
+ // progress 数据版本(v3 = SQLite;v4 = changes 加 base_ts / 本地脏度两列,platform-sync §8)
30
+ export const CURRENT_VERSION = 5;
31
+
32
+ // 空阶段骨架
33
+ export function emptyStage() {
34
+ return { status: 'pending', steps: [], startedAt: null, completedAt: null };
35
+ }
36
+
@@ -51,9 +51,37 @@ export class StageMachine {
51
51
  if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
52
52
  }
53
53
 
54
+ // ── stale 拒绝门(W1 改动点 2):complete-stage 不再静默回填 stale 步骤 ──
55
+ // 放在产物校验门之前——stale 是更具体的拒绝原因(产物缺失时旧序会被产物门先拦走,
56
+ // stale 门永远执行不到);无 --force 拒绝并列名,有 --force 放行并把 stale 步骤名并入审计。
57
+ let staleBackfillNote = null;
58
+ {
59
+ const sqlDbForStaleCheck = db.getDb();
60
+ const changeRowForStale = sqlDbForStaleCheck.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
61
+ if (changeRowForStale !== undefined) {
62
+ const stageRowForStale = sqlDbForStaleCheck.prepare('SELECT id FROM stages WHERE change_id = ? AND stage = ?').get(changeRowForStale.id, stage);
63
+ if (stageRowForStale !== undefined) {
64
+ const staleSteps = sqlDbForStaleCheck.prepare('SELECT name FROM steps WHERE stage_id = ? AND status = ?').all(stageRowForStale.id, 'stale');
65
+ if (staleSteps.length > 0) {
66
+ if (!force) {
67
+ console.error(`❌ complete-stage 被拒绝:阶段 ${stage} 存在 ${staleSteps.length} 个 stale 步骤`);
68
+ for (const st of staleSteps) console.error(` - ${st.name}`);
69
+ console.error(` stale 步骤需真实执行推进(sillyspec run ${stage} 逐个完成),或确认方案未变后用 --force 强制回填(将记录审计日志)。`);
70
+ return;
71
+ }
72
+ staleBackfillNote = `stale 步骤回填: ${staleSteps.map(s => s.name).join(', ')}`;
73
+ }
74
+ }
75
+ }
76
+ }
77
+
54
78
  // ── 产物校验门:complete-stage 不再是零校验后门 ──
55
79
  // 与 run.js completeStep 的 validator 同源;--force 为显式逃生口(留审计)。
56
80
  const validation = this._validateStageArtifacts(cwd, stage, cn);
81
+ if (staleBackfillNote) {
82
+ validation.errors = [...(validation.errors || []), staleBackfillNote];
83
+ validation.ok = false;
84
+ }
57
85
  if (!validation.ok) {
58
86
  if (!force) {
59
87
  console.error(`❌ complete-stage 被拒绝:阶段 ${stage} 产物校验未通过`);
@@ -92,7 +120,7 @@ export class StageMachine {
92
120
  if (stageRow !== undefined) {
93
121
  const stageId = stageRow.id;
94
122
  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)
123
+ `UPDATE steps SET status = 'completed', completed_at = ? WHERE stage_id = ? AND status IN ('pending', 'stale')` // stale 一并回填(reopen --done 场景,坑 brainstorm-reopen-step-state-desync;--force 时放行)
96
124
  ).run(now, stageId);
97
125
  }
98
126
 
@@ -1,261 +1,261 @@
1
- // W6 Step9c: 阶段/步骤/批量进度管理(stages + steps + batch_progress 表)。
2
- // 从 src/progress.js 单体 ProgressManager 抽出。持有 pm 引用(构造注入),调 pm._ensureDB /
3
- // pm.listChanges / pm._validateStageArtifacts / pm._appendAuditLog / pm.read(persistence-core
4
- // + 其他组 delegate 留 facade)。纯 SQL + 常量,无 fs/path 依赖。
5
- import { VALID_STAGES, STAGE_LABELS } from './shared.js';
6
-
7
- // step 状态白名单(仅 updateStep 用,本地化)
8
- const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked', 'waiting', 'stale'];
9
-
10
- export class StepStore {
11
- constructor(pm) {
12
- this.pm = pm;
13
- }
14
-
15
- setStage(cwd, stage, changeName = null) {
16
- if (!VALID_STAGES.includes(stage)) {
17
- console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
18
- return;
19
- }
20
-
21
- const db = this.pm._ensureDB(cwd);
22
- const now = new Date().toISOString();
23
-
24
- // 获取变更名
25
- let cn = changeName;
26
- if (!cn) {
27
- const changes = this.pm.listChanges(cwd);
28
- if (changes.length === 1) cn = changes[0];
29
- if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
30
- }
31
-
32
- let changeFound = false;
33
- db.transaction(() => {
34
- const sqlDb = db.getDb();
35
- // 确保 change 存在
36
- const changeRow = sqlDb.prepare('SELECT id, current_stage FROM changes WHERE name = ?').get(cn);
37
- if (changeRow === undefined) return;
38
-
39
- changeFound = true;
40
- const changeId = changeRow.id;
41
-
42
- // UPDATE changes.current_stage + last_active
43
- sqlDb.prepare('UPDATE changes SET current_stage = ?, last_active = ? WHERE name = ?').run(stage, now, cn);
44
-
45
- // 确保 stages 行存在(INSERT OR IGNORE)
46
- sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, stage);
47
-
48
- // UPDATE stages.status 为 in-progress(仅当仍为 pending 时)
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);
53
- });
54
-
55
- if (!changeFound) {
56
- // 历史教训:变更名打错/cwd 漂移命中错误 spec 时,事务内静默 return,
57
- // 但旧代码此处仍无条件打「✅ 已设为」→ 假成功(agent 以为生效,实际什么都没做)。
58
- console.log(`❌ 变更「${cn}」不存在,未设置阶段。检查变更名拼写,或 sillyspec status 查看活跃变更;cwd 漂移时回项目根或加 --spec-dir。`);
59
- return;
60
- }
61
-
62
- // read() 已改为 SQL,直接通过 SQL 查询即可,无需 _write
63
- console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage}(变更 ${cn})`);
64
- }
65
-
66
- addStep(cwd, stage, stepName, changeName = null) {
67
- if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
68
-
69
- const db = this.pm._ensureDB(cwd);
70
-
71
- // 获取变更名
72
- let cn = changeName;
73
- if (!cn) {
74
- const changes = this.pm.listChanges(cwd);
75
- if (changes.length === 1) cn = changes[0];
76
- if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
77
- }
78
-
79
- // 查找 stage_id
80
- const sqlDb = db.getDb();
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) {
85
- // stages 行不存在,静默跳过
86
- console.log(`ℹ️ 阶段 ${stage} 不存在`);
87
- return;
88
- }
89
- const stageId = stageRow.id;
90
-
91
- // 重复步骤名检查
92
- const dupRow = sqlDb.prepare('SELECT id FROM steps WHERE stage_id = ? AND name = ?').get(stageId, stepName);
93
- if (dupRow !== undefined) {
94
- console.log(`ℹ️ 步骤 "${stepName}" 已存在于 ${stage}`);
95
- return;
96
- }
97
-
98
- // INSERT INTO steps(ordering 递增)
99
- db.transaction(() => {
100
- const tDb = db.getDb();
101
- tDb.prepare(
102
- `INSERT INTO steps (stage_id, name, ordering, status)
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);
108
- });
109
-
110
- console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
111
- }
112
-
113
- updateStep(cwd, stage, stepName, options = {}, changeName = null) {
114
- const { status, output } = options;
115
- if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
116
-
117
- const db = this.pm._ensureDB(cwd);
118
-
119
- // 获取变更名
120
- let cn = changeName;
121
- if (!cn) {
122
- const changes = this.pm.listChanges(cwd);
123
- if (changes.length === 1) cn = changes[0];
124
- if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
125
- }
126
-
127
- // 状态校验
128
- if (status && !VALID_STATUSES.includes(status)) {
129
- console.log(`❌ 无效状态: ${status},可选: ${VALID_STATUSES.join(', ')}`);
130
- return;
131
- }
132
-
133
- // 查找 step_id:通过 changes → stages → steps JOIN 查询
134
- const sqlDb = db.getDb();
135
- const stepRow = sqlDb.prepare(
136
- `SELECT st.id, st.status FROM steps st
137
- JOIN stages sg ON st.stage_id = sg.id
138
- JOIN changes c ON sg.change_id = c.id
139
- WHERE c.name = ? AND sg.stage = ? AND st.name = ?`
140
- ).get(cn, stage, stepName);
141
- if (stepRow === undefined) {
142
- console.log(`❌ 步骤不存在: ${stage}/${stepName}`);
143
- return;
144
- }
145
- const stepId = stepRow.id;
146
-
147
- // UPDATE steps
148
- let stageCompletionCandidateId = null;
149
- db.transaction(() => {
150
- const tDb = db.getDb();
151
- const now = new Date().toISOString();
152
- if (status) {
153
- tDb.prepare('UPDATE steps SET status = ?, completed_at = ? WHERE id = ? AND name = ?').run(status, now, stepId, stepName);
154
- }
155
- if (output !== undefined) {
156
- tDb.prepare('UPDATE steps SET output = ? WHERE id = ? AND name = ?').run(output, stepId, stepName);
157
- }
158
-
159
- // 自动完成检测:同 stage_id 下所有 steps 都 completed 时,候选标记 stage completed
160
- // (实际标记延后到事务外,先过产物校验门,防止 update-step 成为绕过 validator 的后门)
161
- if (status === 'completed') {
162
- // 获取 stage_id
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) {
168
- stageCompletionCandidateId = stId;
169
- }
170
- }
171
- }
172
-
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);
176
- });
177
-
178
- if (stageCompletionCandidateId !== null) {
179
- const { force = false } = options;
180
- const validation = this.pm._validateStageArtifacts(cwd, stage, cn);
181
- if (!validation.ok && !force) {
182
- console.error(`⚠️ 阶段 ${stage} 所有步骤已完成,但产物校验未通过,阶段不标记为 completed:`);
183
- for (const err of validation.errors) console.error(` - ${err}`);
184
- // 出路提示常放在 warnings(如「写明不改理由即可豁免」),阻断时必须一并打出,
185
- // 否则 agent 只看到症状看不到怎么过(驾驭问题根因:hint 在 warning、出口只打 error)。
186
- for (const warn of (validation.warnings || [])) console.error(` · ${warn}`);
187
- console.error(` 请修复产物后走正常流程 sillyspec run ${stage} --done,或使用 --force(将记录审计日志)。`);
188
- } else {
189
- if (!validation.ok && force) {
190
- console.warn(`⚠️ --force 强制标记阶段 ${stage} completed(校验未通过,已记录审计日志)`);
191
- this.pm._appendAuditLog(cwd, {
192
- action: 'update-step --force (stage auto-complete)',
193
- stage,
194
- change: cn,
195
- validationErrors: validation.errors,
196
- });
197
- }
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);
201
- });
202
- console.log(`✅ 阶段 ${stage} 所有步骤已完成,阶段已标记为 completed`);
203
- }
204
- }
205
-
206
- console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || '(仅更新 output)'}`);
207
- }
208
-
209
- updateBatchProgress(cwd, batchData, changeName = null) {
210
- const cn = changeName || null;
211
-
212
- const db = this.pm._ensureDB(cwd);
213
- db.transaction(() => {
214
- const sqlDb = db.getDb();
215
- // 获取 change_id
216
- let changeId = null;
217
- if (cn) {
218
- const row = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
219
- if (row !== undefined) changeId = row.id;
220
- }
221
- if (!changeId) {
222
- // 尝试从唯一活跃变更获取
223
- const rows = sqlDb.prepare("SELECT id FROM changes WHERE status = 'active'").all();
224
- if (rows.length === 1) changeId = rows[0].id;
225
- }
226
- if (!changeId) return;
227
-
228
- sqlDb.prepare(
229
- `INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
230
- VALUES (?, ?, ?, ?, ?)
231
- ON CONFLICT(change_id) DO UPDATE SET
232
- total = excluded.total,
233
- completed = excluded.completed,
234
- failed = excluded.failed,
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);
240
- });
241
- }
242
-
243
- readBatchProgress(cwd, changeName = null) {
244
- const data = this.pm.read(cwd, changeName);
245
- return data?.batchProgress || null;
246
- }
247
-
248
- _renderBatchProgress(batchProgress) {
249
- if (!batchProgress || !batchProgress.total) return null;
250
- const { total, completed = 0, failed = 0, skipped = 0 } = batchProgress;
251
- const barLen = 20;
252
- const filled = Math.round((completed / total) * barLen);
253
- const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
254
- const parts = [];
255
- if (failed > 0) parts.push(`${failed} 失败`);
256
- if (skipped > 0) parts.push(`${skipped} 跳过`);
257
- const suffix = parts.length ? ` (${parts.join(', ')})` : '';
258
- return `📊 批量进度: ${bar} ${completed}/${total}${suffix}`;
259
- }
260
- }
261
-
1
+ // W6 Step9c: 阶段/步骤/批量进度管理(stages + steps + batch_progress 表)。
2
+ // 从 src/progress.js 单体 ProgressManager 抽出。持有 pm 引用(构造注入),调 pm._ensureDB /
3
+ // pm.listChanges / pm._validateStageArtifacts / pm._appendAuditLog / pm.read(persistence-core
4
+ // + 其他组 delegate 留 facade)。纯 SQL + 常量,无 fs/path 依赖。
5
+ import { VALID_STAGES, STAGE_LABELS } from './shared.js';
6
+
7
+ // step 状态白名单(仅 updateStep 用,本地化)
8
+ const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked', 'waiting', 'stale'];
9
+
10
+ export class StepStore {
11
+ constructor(pm) {
12
+ this.pm = pm;
13
+ }
14
+
15
+ setStage(cwd, stage, changeName = null) {
16
+ if (!VALID_STAGES.includes(stage)) {
17
+ console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
18
+ return;
19
+ }
20
+
21
+ const db = this.pm._ensureDB(cwd);
22
+ const now = new Date().toISOString();
23
+
24
+ // 获取变更名
25
+ let cn = changeName;
26
+ if (!cn) {
27
+ const changes = this.pm.listChanges(cwd);
28
+ if (changes.length === 1) cn = changes[0];
29
+ if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
30
+ }
31
+
32
+ let changeFound = false;
33
+ db.transaction(() => {
34
+ const sqlDb = db.getDb();
35
+ // 确保 change 存在
36
+ const changeRow = sqlDb.prepare('SELECT id, current_stage FROM changes WHERE name = ?').get(cn);
37
+ if (changeRow === undefined) return;
38
+
39
+ changeFound = true;
40
+ const changeId = changeRow.id;
41
+
42
+ // UPDATE changes.current_stage + last_active
43
+ sqlDb.prepare('UPDATE changes SET current_stage = ?, last_active = ? WHERE name = ?').run(stage, now, cn);
44
+
45
+ // 确保 stages 行存在(INSERT OR IGNORE)
46
+ sqlDb.prepare(`INSERT OR IGNORE INTO stages (change_id, stage, status) VALUES (?, ?, 'pending')`).run(changeId, stage);
47
+
48
+ // UPDATE stages.status 为 in-progress(仅当仍为 pending 时)
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);
53
+ });
54
+
55
+ if (!changeFound) {
56
+ // 历史教训:变更名打错/cwd 漂移命中错误 spec 时,事务内静默 return,
57
+ // 但旧代码此处仍无条件打「✅ 已设为」→ 假成功(agent 以为生效,实际什么都没做)。
58
+ console.log(`❌ 变更「${cn}」不存在,未设置阶段。检查变更名拼写,或 sillyspec status 查看活跃变更;cwd 漂移时回项目根或加 --spec-dir。`);
59
+ return;
60
+ }
61
+
62
+ // read() 已改为 SQL,直接通过 SQL 查询即可,无需 _write
63
+ console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage}(变更 ${cn})`);
64
+ }
65
+
66
+ addStep(cwd, stage, stepName, changeName = null) {
67
+ if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
68
+
69
+ const db = this.pm._ensureDB(cwd);
70
+
71
+ // 获取变更名
72
+ let cn = changeName;
73
+ if (!cn) {
74
+ const changes = this.pm.listChanges(cwd);
75
+ if (changes.length === 1) cn = changes[0];
76
+ if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
77
+ }
78
+
79
+ // 查找 stage_id
80
+ const sqlDb = db.getDb();
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) {
85
+ // stages 行不存在,静默跳过
86
+ console.log(`ℹ️ 阶段 ${stage} 不存在`);
87
+ return;
88
+ }
89
+ const stageId = stageRow.id;
90
+
91
+ // 重复步骤名检查
92
+ const dupRow = sqlDb.prepare('SELECT id FROM steps WHERE stage_id = ? AND name = ?').get(stageId, stepName);
93
+ if (dupRow !== undefined) {
94
+ console.log(`ℹ️ 步骤 "${stepName}" 已存在于 ${stage}`);
95
+ return;
96
+ }
97
+
98
+ // INSERT INTO steps(ordering 递增)
99
+ db.transaction(() => {
100
+ const tDb = db.getDb();
101
+ tDb.prepare(
102
+ `INSERT INTO steps (stage_id, name, ordering, status)
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);
108
+ });
109
+
110
+ console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
111
+ }
112
+
113
+ updateStep(cwd, stage, stepName, options = {}, changeName = null) {
114
+ const { status, output } = options;
115
+ if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
116
+
117
+ const db = this.pm._ensureDB(cwd);
118
+
119
+ // 获取变更名
120
+ let cn = changeName;
121
+ if (!cn) {
122
+ const changes = this.pm.listChanges(cwd);
123
+ if (changes.length === 1) cn = changes[0];
124
+ if (!cn) { console.log('❌ 无法确定当前变更,请指定 --change <name>'); return; }
125
+ }
126
+
127
+ // 状态校验
128
+ if (status && !VALID_STATUSES.includes(status)) {
129
+ console.log(`❌ 无效状态: ${status},可选: ${VALID_STATUSES.join(', ')}`);
130
+ return;
131
+ }
132
+
133
+ // 查找 step_id:通过 changes → stages → steps JOIN 查询
134
+ const sqlDb = db.getDb();
135
+ const stepRow = sqlDb.prepare(
136
+ `SELECT st.id, st.status FROM steps st
137
+ JOIN stages sg ON st.stage_id = sg.id
138
+ JOIN changes c ON sg.change_id = c.id
139
+ WHERE c.name = ? AND sg.stage = ? AND st.name = ?`
140
+ ).get(cn, stage, stepName);
141
+ if (stepRow === undefined) {
142
+ console.log(`❌ 步骤不存在: ${stage}/${stepName}`);
143
+ return;
144
+ }
145
+ const stepId = stepRow.id;
146
+
147
+ // UPDATE steps
148
+ let stageCompletionCandidateId = null;
149
+ db.transaction(() => {
150
+ const tDb = db.getDb();
151
+ const now = new Date().toISOString();
152
+ if (status) {
153
+ tDb.prepare('UPDATE steps SET status = ?, completed_at = ? WHERE id = ? AND name = ?').run(status, now, stepId, stepName);
154
+ }
155
+ if (output !== undefined) {
156
+ tDb.prepare('UPDATE steps SET output = ? WHERE id = ? AND name = ?').run(output, stepId, stepName);
157
+ }
158
+
159
+ // 自动完成检测:同 stage_id 下所有 steps 都 completed 时,候选标记 stage completed
160
+ // (实际标记延后到事务外,先过产物校验门,防止 update-step 成为绕过 validator 的后门)
161
+ if (status === 'completed') {
162
+ // 获取 stage_id
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) {
168
+ stageCompletionCandidateId = stId;
169
+ }
170
+ }
171
+ }
172
+
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);
176
+ });
177
+
178
+ if (stageCompletionCandidateId !== null) {
179
+ const { force = false } = options;
180
+ const validation = this.pm._validateStageArtifacts(cwd, stage, cn);
181
+ if (!validation.ok && !force) {
182
+ console.error(`⚠️ 阶段 ${stage} 所有步骤已完成,但产物校验未通过,阶段不标记为 completed:`);
183
+ for (const err of validation.errors) console.error(` - ${err}`);
184
+ // 出路提示常放在 warnings(如「写明不改理由即可豁免」),阻断时必须一并打出,
185
+ // 否则 agent 只看到症状看不到怎么过(驾驭问题根因:hint 在 warning、出口只打 error)。
186
+ for (const warn of (validation.warnings || [])) console.error(` · ${warn}`);
187
+ console.error(` 请修复产物后走正常流程 sillyspec run ${stage} --done,或使用 --force(将记录审计日志)。`);
188
+ } else {
189
+ if (!validation.ok && force) {
190
+ console.warn(`⚠️ --force 强制标记阶段 ${stage} completed(校验未通过,已记录审计日志)`);
191
+ this.pm._appendAuditLog(cwd, {
192
+ action: 'update-step --force (stage auto-complete)',
193
+ stage,
194
+ change: cn,
195
+ validationErrors: validation.errors,
196
+ });
197
+ }
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);
201
+ });
202
+ console.log(`✅ 阶段 ${stage} 所有步骤已完成,阶段已标记为 completed`);
203
+ }
204
+ }
205
+
206
+ console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || '(仅更新 output)'}`);
207
+ }
208
+
209
+ updateBatchProgress(cwd, batchData, changeName = null) {
210
+ const cn = changeName || null;
211
+
212
+ const db = this.pm._ensureDB(cwd);
213
+ db.transaction(() => {
214
+ const sqlDb = db.getDb();
215
+ // 获取 change_id
216
+ let changeId = null;
217
+ if (cn) {
218
+ const row = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(cn);
219
+ if (row !== undefined) changeId = row.id;
220
+ }
221
+ if (!changeId) {
222
+ // 尝试从唯一活跃变更获取
223
+ const rows = sqlDb.prepare("SELECT id FROM changes WHERE status = 'active'").all();
224
+ if (rows.length === 1) changeId = rows[0].id;
225
+ }
226
+ if (!changeId) return;
227
+
228
+ sqlDb.prepare(
229
+ `INSERT INTO batch_progress (change_id, total, completed, failed, skipped)
230
+ VALUES (?, ?, ?, ?, ?)
231
+ ON CONFLICT(change_id) DO UPDATE SET
232
+ total = excluded.total,
233
+ completed = excluded.completed,
234
+ failed = excluded.failed,
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);
240
+ });
241
+ }
242
+
243
+ readBatchProgress(cwd, changeName = null) {
244
+ const data = this.pm.read(cwd, changeName);
245
+ return data?.batchProgress || null;
246
+ }
247
+
248
+ _renderBatchProgress(batchProgress) {
249
+ if (!batchProgress || !batchProgress.total) return null;
250
+ const { total, completed = 0, failed = 0, skipped = 0 } = batchProgress;
251
+ const barLen = 20;
252
+ const filled = Math.round((completed / total) * barLen);
253
+ const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
254
+ const parts = [];
255
+ if (failed > 0) parts.push(`${failed} 失败`);
256
+ if (skipped > 0) parts.push(`${skipped} 跳过`);
257
+ const suffix = parts.length ? ` (${parts.join(', ')})` : '';
258
+ return `📊 批量进度: ${bar} ${completed}/${total}${suffix}`;
259
+ }
260
+ }
261
+