sillyspec 3.10.5 → 3.10.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.10.5",
3
+ "version": "3.10.6",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/index.js CHANGED
@@ -136,9 +136,13 @@ async function main() {
136
136
  break;
137
137
  case 'progress': {
138
138
  const pm = new ProgressManager();
139
+ pm._migrateIfNeeded(dir);
139
140
  const subCommand = filteredArgs[1];
140
141
  const stageIdx = filteredArgs.indexOf('--stage');
141
142
  const stage = stageIdx >= 0 && filteredArgs[stageIdx + 1] ? filteredArgs[stageIdx + 1] : null;
143
+ // 解析 --change 参数
144
+ const progChangeIdx = args.indexOf('--change');
145
+ const progChangeName = progChangeIdx >= 0 && args[progChangeIdx + 1] ? args[progChangeIdx + 1] : null;
142
146
 
143
147
  switch (subCommand) {
144
148
  case 'init':
@@ -146,49 +150,48 @@ async function main() {
146
150
  break;
147
151
  case 'status':
148
152
  case 'show':
149
- pm.show(dir);
153
+ pm.show(dir, progChangeName);
150
154
  break;
151
155
  case 'validate':
152
- await pm.validate(dir);
156
+ await pm.validate(dir, progChangeName);
153
157
  break;
154
158
  case 'reset':
155
- pm.reset(dir, stage);
159
+ pm.reset(dir, stage, progChangeName);
156
160
  break;
157
161
  case 'set-stage': {
158
162
  const setStageName = filteredArgs[2];
159
- if (!setStageName) { console.log('❌ 用法: sillyspec progress set-stage <stage>'); break; }
160
- pm.setStage(dir, setStageName);
163
+ if (!setStageName) { console.log('❌ 用法: sillyspec progress set-stage <stage> [--change <name>]'); break; }
164
+ pm.setStage(dir, setStageName, progChangeName);
161
165
  break;
162
166
  }
163
167
  case 'add-step': {
164
168
  const addStepStage = filteredArgs[2];
165
169
  const addStepName = filteredArgs[3];
166
- if (!addStepStage || !addStepName) { console.log('❌ 用法: sillyspec progress add-step <stage> <step-name>'); break; }
167
- pm.addStep(dir, addStepStage, addStepName);
170
+ if (!addStepStage || !addStepName) { console.log('❌ 用法: sillyspec progress add-step <stage> <step-name> [--change <name>]'); break; }
171
+ pm.addStep(dir, addStepStage, addStepName, progChangeName);
168
172
  break;
169
173
  }
170
174
  case 'update-step': {
171
175
  const updStepStage = filteredArgs[2];
172
176
  const updStepName = filteredArgs[3];
173
- if (!updStepStage || !updStepName) { console.log('❌ 用法: sillyspec progress update-step <stage> <step-name> --status <status> [--output <text>]'); break; }
174
- // Parse --status and --output from args
177
+ if (!updStepStage || !updStepName) { console.log('❌ 用法: sillyspec progress update-step <stage> <step-name> --status <status> [--output <text>] [--change <name>]'); break; }
175
178
  let updStatus = null, updOutput = undefined;
176
179
  for (let ai = 0; ai < args.length; ai++) {
177
180
  if (args[ai] === '--status' && args[ai + 1]) { updStatus = args[ai + 1]; ai++; }
178
181
  if (args[ai] === '--output' && args[ai + 1]) { updOutput = args[ai + 1]; ai++; }
179
182
  }
180
- pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput });
183
+ pm.updateStep(dir, updStepStage, updStepName, { status: updStatus, output: updOutput }, progChangeName);
181
184
  break;
182
185
  }
183
186
  case 'complete-stage': {
184
187
  const compStageName = filteredArgs[2];
185
188
  if (!compStageName) { console.log('❌ 用法: sillyspec progress complete-stage <stage>'); break; }
186
- pm.completeStage(dir, compStageName);
189
+ pm.completeStage(dir, compStageName, progChangeName);
187
190
  break;
188
191
  }
189
192
  case 'batch': {
190
193
  if (filteredArgs.includes('--status')) {
191
- const bp = pm.readBatchProgress(dir);
194
+ const bp = pm.readBatchProgress(dir, progChangeName);
192
195
  if (!bp) { console.log('📭 无批量进度数据'); break; }
193
196
  const line = pm._renderBatchProgress(bp);
194
197
  console.log(line || '📭 无批量进度数据');
@@ -207,7 +210,7 @@ async function main() {
207
210
  console.log(' sillyspec progress batch --status');
208
211
  break;
209
212
  }
210
- pm.updateBatchProgress(dir, batchData);
213
+ pm.updateBatchProgress(dir, batchData, progChangeName);
211
214
  console.log('✅ 批量进度已更新');
212
215
  }
213
216
  break;
package/src/init.js CHANGED
@@ -56,7 +56,7 @@ const INJECTION_CONTENT = `## SillySpec — 规范驱动开发
56
56
  - 遵循 \`.sillyspec/docs/<project>/scan/CONVENTIONS.md\` 中的代码风格
57
57
 
58
58
  ### 工作流程
59
- - 读取 \`.sillyspec/.runtime/progress.json\` 确认当前阶段(使用 \`sillyspec progress show\`)
59
+ - 读取当前变更的 progress.json 确认当前阶段(使用 \`sillyspec progress show\`)
60
60
  - 各阶段产出文件位于 \`.sillyspec/changes/<变更名>/\` 下
61
61
  `;
62
62
 
@@ -141,18 +141,15 @@ async function doInstall(projectDir, tools, subprojects = []) {
141
141
  writeFileSync(uncatPath, `# 未分类知识\n\n> execute/quick 执行中发现的坑暂存于此,用户审阅后归类到对应文件并更新 INDEX.md。\n`);
142
142
  }
143
143
 
144
- // 创建 .sillyspec/.runtime/ 目录结构
144
+ // 创建 .sillyspec/.runtime/ 目录结构(全局状态)
145
145
  const runtimeDir = join(projectDir, '.sillyspec', '.runtime');
146
146
  for (const sub of ['artifacts', 'history', 'logs', 'templates']) {
147
147
  mkdirSync(join(runtimeDir, sub), { recursive: true });
148
148
  }
149
149
 
150
- // 创建初始 progress.json
151
- const progressPath = join(runtimeDir, 'progress.json');
152
- if (!existsSync(progressPath)) {
153
- const pm = new ProgressManager();
154
- pm.init(projectDir);
155
- }
150
+ // 创建全局状态文件
151
+ const pm = new ProgressManager();
152
+ pm.init(projectDir);
156
153
 
157
154
  // 创建初始 user-inputs.md
158
155
  const inputsPath = join(runtimeDir, 'user-inputs.md');
package/src/progress.js CHANGED
@@ -1,19 +1,25 @@
1
1
  /**
2
2
  * SillySpec ProgressManager — 进度恢复管理
3
3
  *
4
- * 纯 Node.js,无外部依赖。管理 .sillyspec/.runtime/progress.json。
4
+ * 纯 Node.js,无外部依赖。支持多变更并行。
5
5
  *
6
- * Schema v2: { project, currentStage, stages: { [name]: { status, steps, startedAt, completedAt } }, lastActive }
6
+ * 存储结构(v3):
7
+ * .sillyspec/.runtime/global.json — 全局状态(项目名、活跃变更列表)
8
+ * .sillyspec/changes/<name>/progress.json — 每个变更独立的阶段/步骤状态
9
+ *
10
+ * 向后兼容:如果存在旧的 .sillyspec/.runtime/progress.json,自动迁移。
7
11
  */
8
12
 
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, copyFileSync, unlinkSync } from 'fs';
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, copyFileSync, unlinkSync, readdirSync } from 'fs';
10
14
  import { join, basename } from 'path';
11
15
 
12
16
  const RUNTIME_DIR = '.sillyspec/.runtime';
17
+ const CHANGES_DIR = '.sillyspec/changes';
18
+ const GLOBAL_FILE = 'global.json';
13
19
  const PROGRESS_FILE = 'progress.json';
14
- const BACKUP_FILE = 'progress.json.bak';
20
+ const BACKUP_SUFFIX = '.bak';
15
21
 
16
- const CURRENT_VERSION = 2;
22
+ const CURRENT_VERSION = 3;
17
23
  const VALID_STAGES = ['scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
18
24
  const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked'];
19
25
 
@@ -38,18 +44,131 @@ function makeInitialProgress(project) {
38
44
  return { _version: CURRENT_VERSION, project: project || '', currentStage: '', currentChange: null, stages, lastActive: null };
39
45
  }
40
46
 
47
+ function makeInitialGlobal(project) {
48
+ return { _version: CURRENT_VERSION, project: project || '', activeChanges: [] };
49
+ }
50
+
41
51
  // ── ProgressManager ──
42
52
 
43
53
  export class ProgressManager {
44
- // ── 核心读写 ──
54
+ // ── 路径工具 ──
45
55
 
46
- _path(cwd, ...parts) {
56
+ _runtimePath(cwd, ...parts) {
47
57
  return join(cwd, RUNTIME_DIR, ...parts);
48
58
  }
49
59
 
50
- read(cwd) {
51
- const progressPath = this._path(cwd, PROGRESS_FILE);
52
- const backupPath = this._path(cwd, BACKUP_FILE);
60
+ _changePath(cwd, changeName, ...parts) {
61
+ return join(cwd, CHANGES_DIR, changeName, ...parts);
62
+ }
63
+
64
+ _ensureRuntimeDir(cwd) {
65
+ const runtimeDir = this._runtimePath(cwd);
66
+ if (!existsSync(runtimeDir)) {
67
+ mkdirSync(runtimeDir, { recursive: true });
68
+ for (const d of ['artifacts', 'history', 'logs', 'templates']) {
69
+ mkdirSync(join(runtimeDir, d), { recursive: true });
70
+ }
71
+ }
72
+ }
73
+
74
+ _ensureChangeDir(cwd, changeName) {
75
+ const dir = this._changePath(cwd, changeName);
76
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
77
+ return dir;
78
+ }
79
+
80
+ // ── 向后兼容:检测并迁移旧版 progress.json ──
81
+
82
+ _migrateIfNeeded(cwd) {
83
+ const oldPath = this._runtimePath(cwd, PROGRESS_FILE);
84
+ const globalPath = this._runtimePath(cwd, GLOBAL_FILE);
85
+
86
+ // 新版已存在,不迁移
87
+ if (existsSync(globalPath)) return;
88
+
89
+ // 旧版不存在,不迁移
90
+ if (!existsSync(oldPath)) return;
91
+
92
+ const oldData = this._parseWithRecovery(readFileSync(oldPath, 'utf8'));
93
+ if (!oldData) return;
94
+
95
+ console.log('🔄 检测到旧版 progress.json,正在迁移到按变更隔离存储...');
96
+
97
+ // 提取变更名
98
+ const changeName = oldData.currentChange || 'default';
99
+
100
+ // 迁移:将旧 progress.json 复制到变更目录
101
+ this._ensureChangeDir(cwd, changeName);
102
+ const newChangePath = this._changePath(cwd, changeName, PROGRESS_FILE);
103
+ if (!existsSync(newChangePath)) {
104
+ writeFileSync(newChangePath, JSON.stringify(oldData, null, 2) + '\n');
105
+ }
106
+
107
+ // 创建全局文件
108
+ const globalData = makeInitialGlobal(oldData.project);
109
+ globalData.activeChanges = [changeName];
110
+ if (!existsSync(globalPath)) {
111
+ writeFileSync(globalPath, JSON.stringify(globalData, null, 2) + '\n');
112
+ }
113
+
114
+ // 备份旧文件
115
+ const backupPath = oldPath + BACKUP_SUFFIX;
116
+ if (!existsSync(backupPath)) copyFileSync(oldPath, backupPath);
117
+ unlinkSync(oldPath);
118
+
119
+ console.log(` ✅ 已迁移到 .sillyspec/changes/${changeName}/progress.json`);
120
+ console.log(` 📦 旧文件已备份到 .sillyspec/.runtime/progress.json.bak`);
121
+ }
122
+
123
+ // ── 全局状态 ──
124
+
125
+ readGlobal(cwd) {
126
+ this._migrateIfNeeded(cwd);
127
+ const globalPath = this._runtimePath(cwd, GLOBAL_FILE);
128
+ if (!existsSync(globalPath)) return null;
129
+ return this._parseWithRecovery(readFileSync(globalPath, 'utf8'));
130
+ }
131
+
132
+ writeGlobal(cwd, data) {
133
+ this._ensureRuntimeDir(cwd);
134
+ const globalPath = this._runtimePath(cwd, GLOBAL_FILE);
135
+ const tmpPath = globalPath + '.tmp';
136
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
137
+ renameSync(tmpPath, globalPath);
138
+ }
139
+
140
+ // ── 变更级别状态 ──
141
+
142
+ /**
143
+ * 读取指定变更的 progress
144
+ * @param {string} cwd
145
+ * @param {string|null} changeName - 变更名,null 时尝试自动检测
146
+ */
147
+ read(cwd, changeName = null) {
148
+ // 向后兼容:如果没有 changeName,尝试读旧版路径
149
+ if (!changeName) {
150
+ // 先看新版全局文件
151
+ const global = this.readGlobal(cwd);
152
+ if (global && global.activeChanges && global.activeChanges.length === 1) {
153
+ changeName = global.activeChanges[0];
154
+ } else {
155
+ // fallback:扫描 changes 目录
156
+ const changes = this.listChanges(cwd);
157
+ if (changes.length === 1) {
158
+ changeName = changes[0];
159
+ } else {
160
+ // 最后尝试旧版路径
161
+ const oldPath = this._runtimePath(cwd, PROGRESS_FILE);
162
+ if (existsSync(oldPath)) {
163
+ return this._parseWithRecovery(readFileSync(oldPath, 'utf8'));
164
+ }
165
+ return null;
166
+ }
167
+ }
168
+ }
169
+
170
+ const progressPath = this._changePath(cwd, changeName, PROGRESS_FILE);
171
+ const backupPath = progressPath + BACKUP_SUFFIX;
53
172
 
54
173
  for (const p of [progressPath, backupPath]) {
55
174
  if (!existsSync(p)) continue;
@@ -65,62 +184,125 @@ export class ProgressManager {
65
184
  return null;
66
185
  }
67
186
 
68
- _write(cwd, data) {
69
- const progressPath = this._path(cwd, PROGRESS_FILE);
187
+ /**
188
+ * 写入指定变更的 progress
189
+ * @param {string} cwd
190
+ * @param {object} data
191
+ * @param {string|null} changeName - 从 data.currentChange 推导,或显式传入
192
+ */
193
+ _write(cwd, data, changeName = null) {
194
+ const cn = changeName || data.currentChange;
195
+ if (!cn) {
196
+ // 无变更名时 fallback 到旧路径(不应该发生,但保底)
197
+ const progressPath = this._runtimePath(cwd, PROGRESS_FILE);
198
+ this._ensureRuntimeDir(cwd);
199
+ const tmpPath = progressPath + '.tmp';
200
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
201
+ renameSync(tmpPath, progressPath);
202
+ return;
203
+ }
204
+
205
+ this._ensureChangeDir(cwd, cn);
206
+ const progressPath = this._changePath(cwd, cn, PROGRESS_FILE);
70
207
  const tmpPath = progressPath + '.tmp';
71
- this._ensureDir(cwd);
72
208
  writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n');
73
209
  renameSync(tmpPath, progressPath);
74
210
  }
75
211
 
76
- _ensureDir(cwd) {
77
- const runtimeDir = this._path(cwd);
78
- if (!existsSync(runtimeDir)) {
79
- mkdirSync(runtimeDir, { recursive: true });
80
- for (const d of ['artifacts', 'history', 'logs', 'templates']) {
81
- mkdirSync(join(runtimeDir, d), { recursive: true });
82
- }
212
+ _backup(cwd, data) {
213
+ const cn = data?.currentChange;
214
+ if (!cn) return;
215
+ const p = this._changePath(cwd, cn, PROGRESS_FILE);
216
+ if (existsSync(p)) copyFileSync(p, this._changePath(cwd, cn, PROGRESS_FILE + BACKUP_SUFFIX));
217
+ }
218
+
219
+ // ── 变更管理 ──
220
+
221
+ /**
222
+ * 列出所有变更名(不含 archive 子目录)
223
+ */
224
+ listChanges(cwd) {
225
+ const changesDir = join(cwd, CHANGES_DIR);
226
+ if (!existsSync(changesDir)) return [];
227
+ return readdirSync(changesDir, { withFileTypes: true })
228
+ .filter(e => e.isDirectory() && e.name !== 'archive')
229
+ .map(e => e.name);
230
+ }
231
+
232
+ /**
233
+ * 注册变更到全局活跃列表
234
+ */
235
+ registerChange(cwd, changeName) {
236
+ let global = this.readGlobal(cwd);
237
+ if (!global) {
238
+ global = makeInitialGlobal(basename(cwd));
239
+ }
240
+ if (!global.activeChanges.includes(changeName)) {
241
+ global.activeChanges.push(changeName);
242
+ this.writeGlobal(cwd, global);
83
243
  }
84
244
  }
85
245
 
86
- _backup(cwd) {
87
- const p = this._path(cwd, PROGRESS_FILE);
88
- if (existsSync(p)) copyFileSync(p, this._path(cwd, BACKUP_FILE));
246
+ /**
247
+ * 从全局活跃列表移除变更(归档时调用)
248
+ */
249
+ unregisterChange(cwd, changeName) {
250
+ const global = this.readGlobal(cwd);
251
+ if (!global) return;
252
+ global.activeChanges = global.activeChanges.filter(c => c !== changeName);
253
+ this.writeGlobal(cwd, global);
89
254
  }
90
255
 
91
256
  // ── CLI 命令 ──
92
257
 
93
258
  init(cwd) {
94
- this._ensureDir(cwd);
95
- const progressPath = this._path(cwd, PROGRESS_FILE);
96
-
97
- if (existsSync(progressPath)) {
98
- console.log(`ℹ️ progress.json 已存在,跳过`);
99
- return this.read(cwd);
259
+ this._migrateIfNeeded(cwd);
260
+ this._ensureRuntimeDir(cwd);
261
+
262
+ const globalPath = this._runtimePath(cwd, GLOBAL_FILE);
263
+ if (!existsSync(globalPath)) {
264
+ const data = makeInitialGlobal(basename(cwd));
265
+ this.writeGlobal(cwd, data);
266
+ console.log(`✅ 已创建全局状态文件`);
267
+ } else {
268
+ console.log(`ℹ️ 全局状态文件已存在,跳过`);
100
269
  }
101
270
 
102
- const project = basename(cwd);
103
- const data = makeInitialProgress(project);
104
- this._write(cwd, data);
105
- console.log(`✅ 已创建 ${join(RUNTIME_DIR, PROGRESS_FILE)}`);
106
-
107
271
  // 创建 user-inputs.md
108
- const inputsPath = this._path(cwd, 'user-inputs.md');
272
+ const inputsPath = this._runtimePath(cwd, 'user-inputs.md');
109
273
  if (!existsSync(inputsPath)) {
110
274
  writeFileSync(inputsPath, '# 用户输入记录\n\n> 每步完成时由 AI 自动追加,记录用户所有原话。\n\n');
111
275
  }
112
276
 
113
277
  this._ensureGitignore(cwd);
114
- return data;
278
+ return this.readGlobal(cwd);
115
279
  }
116
280
 
117
- setStage(cwd, stage) {
281
+ /**
282
+ * 初始化指定变更的 progress
283
+ */
284
+ initChange(cwd, changeName) {
285
+ this._ensureChangeDir(cwd, changeName);
286
+ this.registerChange(cwd, changeName);
287
+
288
+ const progressPath = this._changePath(cwd, changeName, PROGRESS_FILE);
289
+ if (!existsSync(progressPath)) {
290
+ const data = makeInitialProgress(basename(cwd));
291
+ data.currentChange = changeName;
292
+ this._write(cwd, data, changeName);
293
+ console.log(`✅ 已创建变更 ${changeName} 的 progress.json`);
294
+ }
295
+
296
+ return this.read(cwd, changeName);
297
+ }
298
+
299
+ setStage(cwd, stage, changeName = null) {
118
300
  if (!VALID_STAGES.includes(stage)) {
119
301
  console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
120
302
  return;
121
303
  }
122
304
 
123
- const data = this._readOrInit(cwd);
305
+ const data = this._readOrInit(cwd, changeName);
124
306
  if (!data) return;
125
307
 
126
308
  if (!data.stages[stage]) data.stages[stage] = emptyStage();
@@ -133,14 +315,14 @@ export class ProgressManager {
133
315
  }
134
316
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
135
317
 
136
- this._backup(cwd);
318
+ this._backup(cwd, data);
137
319
  this._write(cwd, data);
138
320
  console.log(`✅ 当前阶段已设为: ${STAGE_LABELS[stage] || stage} (${stageData.status})`);
139
321
  }
140
322
 
141
- addStep(cwd, stage, stepName) {
323
+ addStep(cwd, stage, stepName, changeName = null) {
142
324
  if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
143
- const data = this._requireStage(cwd, stage);
325
+ const data = this._requireStage(cwd, stage, changeName);
144
326
  if (!data) return;
145
327
 
146
328
  const stageData = data.stages[stage];
@@ -152,15 +334,15 @@ export class ProgressManager {
152
334
  stageData.steps.push({ name: stepName, status: 'pending' });
153
335
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
154
336
 
155
- this._backup(cwd);
337
+ this._backup(cwd, data);
156
338
  this._write(cwd, data);
157
339
  console.log(`✅ 已添加步骤: ${stage}/${stepName}`);
158
340
  }
159
341
 
160
- updateStep(cwd, stage, stepName, options = {}) {
342
+ updateStep(cwd, stage, stepName, options = {}, changeName = null) {
161
343
  const { status, output } = options;
162
344
  if (!stepName) { console.log('❌ 请指定步骤名称'); return; }
163
- const data = this._requireStage(cwd, stage);
345
+ const data = this._requireStage(cwd, stage, changeName);
164
346
  if (!data) return;
165
347
 
166
348
  const stageData = data.stages[stage];
@@ -184,18 +366,18 @@ export class ProgressManager {
184
366
  }
185
367
 
186
368
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
187
- this._backup(cwd);
369
+ this._backup(cwd, data);
188
370
  this._write(cwd, data);
189
371
  console.log(`✅ 步骤已更新: ${stage}/${stepName} → ${status || step.status}`);
190
372
  }
191
373
 
192
- completeStage(cwd, stage) {
374
+ completeStage(cwd, stage, changeName = null) {
193
375
  if (!VALID_STAGES.includes(stage)) {
194
376
  console.log(`❌ 未知阶段: ${stage}`);
195
377
  return;
196
378
  }
197
379
 
198
- const data = this._readOrInit(cwd);
380
+ const data = this._readOrInit(cwd, changeName);
199
381
  if (!data) return;
200
382
 
201
383
  if (!data.stages[stage]) data.stages[stage] = emptyStage();
@@ -210,27 +392,74 @@ export class ProgressManager {
210
392
 
211
393
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
212
394
 
213
- // 归档到 history/(ISO 时间戳,去掉所有非法路径字符)
214
- const historyDir = this._path(cwd, 'history');
395
+ // 归档到 history/(ISO 时间戳)
396
+ const historyDir = this._runtimePath(cwd, 'history');
215
397
  mkdirSync(historyDir, { recursive: true });
216
398
  const ts = new Date().toISOString().replace(/[:.TZ-]/g, '');
217
- writeFileSync(join(historyDir, `${stage}-${ts}.json`), JSON.stringify({ stage, data: stageData, completedAt: stageData.completedAt }, null, 2) + '\n');
399
+ const cn = data.currentChange || 'unknown';
400
+ writeFileSync(join(historyDir, `${cn}-${stage}-${ts}.json`), JSON.stringify({ change: cn, stage, data: stageData, completedAt: stageData.completedAt }, null, 2) + '\n');
218
401
 
219
- this._backup(cwd);
402
+ this._backup(cwd, data);
220
403
  this._write(cwd, data);
221
404
 
222
405
  console.log(`✅ 阶段 ${stage} 已标记为完成(不自动推进,下一步由你决定)`);
223
406
  }
224
407
 
225
- show(cwd) {
226
- const data = this.read(cwd);
408
+ show(cwd, changeName = null) {
409
+ // 如果指定了变更名,只显示该变更
410
+ if (changeName) {
411
+ return this._showChange(cwd, changeName);
412
+ }
413
+
414
+ // 否则显示所有变更
415
+ const changes = this.listChanges(cwd);
416
+ if (changes.length === 0) {
417
+ console.log('ℹ️ 没有活跃的变更');
418
+ return;
419
+ }
420
+
421
+ if (changes.length === 1) {
422
+ return this._showChange(cwd, changes[0]);
423
+ }
424
+
425
+ // 多个变更:汇总显示
426
+ const global = this.readGlobal(cwd);
427
+ console.log('');
428
+ console.log(' ═══════════════════════════════════════');
429
+ console.log(` 项目: ${(global?.project) || basename(cwd) || '(未命名)'}`);
430
+ console.log(` 活跃变更: ${changes.length} 个`);
431
+ console.log(' ═══════════════════════════════════════');
432
+ console.log('');
433
+
434
+ for (const cn of changes) {
435
+ const data = this.read(cwd, cn);
436
+ if (!data) {
437
+ console.log(` 📂 ${cn} — (无法读取)`);
438
+ continue;
439
+ }
440
+ const currentStage = data.currentStage || '(无)';
441
+ const stageLabel = STAGE_LABELS[data.currentStage] || currentStage;
442
+ const lastActive = data.lastActive ? this._timeAgo(data.lastActive) : '未知';
443
+
444
+ console.log(` 📂 ${cn}`);
445
+ console.log(` 当前阶段: ${stageLabel} 最近活跃: ${lastActive}`);
446
+ console.log('');
447
+ }
448
+
449
+ console.log(` 💡 查看详情:sillyspec progress show --change <name>`);
450
+ console.log('');
451
+ }
452
+
453
+ _showChange(cwd, changeName) {
454
+ const data = this.read(cwd, changeName);
227
455
  if (!data) {
228
- console.log('❌ 未找到 progress.json,请先运行 sillyspec progress init');
456
+ console.log(`❌ 未找到变更 ${changeName} 的 progress.json`);
229
457
  return;
230
458
  }
231
459
 
232
460
  console.log('');
233
461
  console.log(' ═══════════════════════════════════════');
462
+ console.log(` 变更: ${changeName}`);
234
463
  console.log(` 项目: ${data.project || '(未命名)'}`);
235
464
  console.log(` 当前阶段: ${STAGE_LABELS[data.currentStage] || data.currentStage || '(无)'}`);
236
465
  console.log(` 最近活跃: ${data.lastActive ? this._timeAgo(data.lastActive) : '未知'}`);
@@ -275,12 +504,12 @@ export class ProgressManager {
275
504
  console.log('');
276
505
  }
277
506
 
278
- status(cwd) {
279
- this.show(cwd);
507
+ status(cwd, changeName = null) {
508
+ this.show(cwd, changeName);
280
509
  }
281
510
 
282
- async validate(cwd) {
283
- const data = this.read(cwd);
511
+ async validate(cwd, changeName = null) {
512
+ const data = this.read(cwd, changeName);
284
513
  if (!data) { console.log('❌ 无法读取 progress.json'); return false; }
285
514
 
286
515
  const errors = [];
@@ -307,7 +536,7 @@ export class ProgressManager {
307
536
  if (!fixed.stages[s]) { fixed.stages[s] = emptyStage(); changed = true; }
308
537
  }
309
538
  if (changed) {
310
- this._backup(cwd);
539
+ this._backup(cwd, fixed);
311
540
  this._write(cwd, fixed);
312
541
  console.log('✅ 已修复并备份');
313
542
  }
@@ -315,56 +544,76 @@ export class ProgressManager {
315
544
  return true;
316
545
  }
317
546
 
318
- reset(cwd, stage) {
319
- this._ensureDir(cwd);
320
-
547
+ reset(cwd, stage, changeName = null) {
321
548
  if (stage) {
322
- this._backup(cwd);
323
- const data = this.read(cwd);
549
+ const data = this.read(cwd, changeName);
324
550
  if (!data) { console.log('❌ 无法读取 progress.json'); return; }
551
+ this._backup(cwd, data);
325
552
  if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
326
553
  data.stages[stage] = emptyStage();
327
554
  data.lastActive = new Date().toLocaleString('zh-CN',{hour12:false});
328
555
  this._write(cwd, data);
329
556
  console.log(`✅ 已重置阶段: ${stage}`);
330
557
  } else {
331
- const p = this._path(cwd, PROGRESS_FILE);
332
- const backup = this._path(cwd, BACKUP_FILE);
333
- let didReset = false;
334
- if (existsSync(p)) { unlinkSync(p); didReset = true; }
335
- if (existsSync(backup)) { unlinkSync(backup); didReset = true; }
336
- if (didReset) {
337
- console.log('✅ 已重置所有进度');
558
+ // 重置所有变更或指定变更
559
+ if (changeName) {
560
+ const p = this._changePath(cwd, changeName, PROGRESS_FILE);
561
+ const backup = this._changePath(cwd, changeName, PROGRESS_FILE + BACKUP_SUFFIX);
562
+ let didReset = false;
563
+ if (existsSync(p)) { unlinkSync(p); didReset = true; }
564
+ if (existsSync(backup)) { unlinkSync(backup); didReset = true; }
565
+ if (didReset) console.log(`✅ 已重置变更 ${changeName} 的进度`);
566
+ else console.log('ℹ️ 无进度文件可重置');
338
567
  } else {
339
- console.log('ℹ️ 无进度文件可重置');
568
+ const changes = this.listChanges(cwd);
569
+ for (const cn of changes) {
570
+ const p = this._changePath(cwd, cn, PROGRESS_FILE);
571
+ const backup = this._changePath(cwd, cn, PROGRESS_FILE + BACKUP_SUFFIX);
572
+ if (existsSync(p)) unlinkSync(p);
573
+ if (existsSync(backup)) unlinkSync(backup);
574
+ }
575
+ console.log('✅ 已重置所有变更的进度');
340
576
  }
341
577
  }
342
578
  }
343
579
 
344
580
  // ── 内部辅助 ──
345
581
 
346
- _readOrInit(cwd) {
347
- let data = this.read(cwd);
582
+ _readOrInit(cwd, changeName = null) {
583
+ let data = this.read(cwd, changeName);
348
584
  if (!data) {
349
- this._ensureDir(cwd);
350
- const progressPath = this._path(cwd, PROGRESS_FILE);
351
- if (!existsSync(progressPath)) {
352
- data = makeInitialProgress(basename(cwd));
353
- this._write(cwd, data);
354
- } else {
355
- console.log('❌ progress.json 损坏,请运行 sillyspec progress validate');
585
+ // 尝试自动检测变更名
586
+ if (!changeName) {
587
+ const changes = this.listChanges(cwd);
588
+ if (changes.length === 1) changeName = changes[0];
589
+ }
590
+ if (changeName) {
591
+ this._ensureChangeDir(cwd, changeName);
592
+ const progressPath = this._changePath(cwd, changeName, PROGRESS_FILE);
593
+ if (!existsSync(progressPath)) {
594
+ data = makeInitialProgress(basename(cwd));
595
+ data.currentChange = changeName;
596
+ this._write(cwd, data, changeName);
597
+ this.registerChange(cwd, changeName);
598
+ }
599
+ }
600
+ if (!data) {
601
+ data = this.read(cwd, changeName);
602
+ }
603
+ if (!data) {
604
+ console.log('❌ 无法确定当前变更,请指定 --change <name>');
356
605
  return null;
357
606
  }
358
607
  }
359
608
  return data;
360
609
  }
361
610
 
362
- _requireStage(cwd, stage) {
611
+ _requireStage(cwd, stage, changeName = null) {
363
612
  if (!VALID_STAGES.includes(stage)) {
364
613
  console.log(`❌ 未知阶段: ${stage},可选: ${VALID_STAGES.join(', ')}`);
365
614
  return null;
366
615
  }
367
- const data = this._readOrInit(cwd);
616
+ const data = this._readOrInit(cwd, changeName);
368
617
  if (!data) return null;
369
618
  if (!data.stages[stage]) data.stages[stage] = emptyStage();
370
619
  return data;
@@ -393,7 +642,6 @@ export class ProgressManager {
393
642
  _timeAgo(dateStr) {
394
643
  if (!dateStr) return '未知';
395
644
  let ts = Date.parse(dateStr);
396
- // toLocaleString('zh-CN') 格式(如 2026/5/12 21:09:00)可能解析失败,尝试手动解析
397
645
  if (isNaN(ts)) {
398
646
  const m = dateStr.match(/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})[\s,]+(\d{1,2}):(\d{2})(?::(\d{2}))?/);
399
647
  if (m) ts = new Date(+m[1], +m[2]-1, +m[3], +m[4], +m[5], +(m[6]||0)).getTime();
@@ -410,8 +658,8 @@ export class ProgressManager {
410
658
 
411
659
  // ── 批量进度 ──
412
660
 
413
- updateBatchProgress(cwd, batchData) {
414
- const data = this._readOrInit(cwd);
661
+ updateBatchProgress(cwd, batchData, changeName = null) {
662
+ const data = this._readOrInit(cwd, changeName);
415
663
  if (!data) return;
416
664
 
417
665
  if (!data.batchProgress) {
@@ -423,19 +671,18 @@ export class ProgressManager {
423
671
  if (batchData.skipped !== undefined) data.batchProgress.skipped = batchData.skipped;
424
672
 
425
673
  data.lastActive = new Date().toLocaleString('zh-CN', { hour12: false });
426
- this._backup(cwd);
674
+ this._backup(cwd, data);
427
675
  this._write(cwd, data);
428
676
  }
429
677
 
430
- readBatchProgress(cwd) {
431
- const data = this.read(cwd);
678
+ readBatchProgress(cwd, changeName = null) {
679
+ const data = this.read(cwd, changeName);
432
680
  return data?.batchProgress || null;
433
681
  }
434
682
 
435
683
  _renderBatchProgress(batchProgress) {
436
684
  if (!batchProgress || !batchProgress.total) return null;
437
685
  const { total, completed = 0, failed = 0, skipped = 0 } = batchProgress;
438
- const done = Math.min(completed + failed + skipped, total);
439
686
  const barLen = 20;
440
687
  const filled = Math.round((completed / total) * barLen);
441
688
  const bar = '█'.repeat(filled) + '░'.repeat(barLen - filled);
package/src/run.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * sillyspec run 命令实现
3
3
  *
4
4
  * CLI 成为流程引擎,AI 变成步骤执行器。
5
+ * 支持多变更并行:每个变更独立 progress.json。
5
6
  */
6
7
  import { basename, join } from 'path'
7
8
  import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, statSync } from 'fs'
@@ -11,7 +12,7 @@ import { buildExecuteSteps } from './stages/execute.js'
11
12
  import { buildPlanSteps } from './stages/plan.js'
12
13
 
13
14
  /**
14
- * 统一查找变更目录
15
+ * 统一查找变更目录(与 progress.js 的变更检测逻辑一致)
15
16
  */
16
17
  function resolveChangeDir(cwd, progress) {
17
18
  const changesDir = join(cwd, '.sillyspec', 'changes')
@@ -48,6 +49,19 @@ function autoDetectChange(progress, cwd) {
48
49
  return false
49
50
  }
50
51
 
52
+ /**
53
+ * 从 progress 或变更目录推导变更名
54
+ */
55
+ function resolveChangeName(cwd, progress) {
56
+ if (progress.currentChange) return progress.currentChange
57
+ const changesDir = join(cwd, '.sillyspec', 'changes')
58
+ if (!existsSync(changesDir)) return null
59
+ const entries = readdirSync(changesDir, { withFileTypes: true })
60
+ .filter(e => e.isDirectory() && e.name !== 'archive')
61
+ if (entries.length === 1) return entries[0].name
62
+ return null
63
+ }
64
+
51
65
  /**
52
66
  * 获取阶段的步骤定义(execute 需要动态构建)
53
67
  */
@@ -70,7 +84,7 @@ async function getStageSteps(stageName, cwd, progress) {
70
84
  }
71
85
 
72
86
  /**
73
- * 确保阶段的 steps 已初始化到 progress.json
87
+ * 确保阶段的 steps 已初始化到 progress
74
88
  */
75
89
  async function ensureStageSteps(progress, stageName, cwd) {
76
90
  if (!progress.stages) progress.stages = {}
@@ -90,7 +104,6 @@ async function ensureStageSteps(progress, stageName, cwd) {
90
104
 
91
105
  // 检查步骤数量是否匹配(execute 动态步骤可能变化)
92
106
  if (progress.stages[stageName].steps.length !== steps.length) {
93
- // 保留已完成的状态,重新构建步骤列表
94
107
  const oldSteps = progress.stages[stageName].steps
95
108
  progress.stages[stageName].steps = steps.map(s => {
96
109
  const old = oldSteps.find(step => step.name === s.name)
@@ -106,7 +119,7 @@ async function ensureStageSteps(progress, stageName, cwd) {
106
119
  /**
107
120
  * 输出当前步骤的 prompt
108
121
  */
109
- function outputStep(stageName, stepIndex, steps, cwd) {
122
+ function outputStep(stageName, stepIndex, steps, cwd, changeName) {
110
123
  const step = steps[stepIndex]
111
124
  const total = steps.length
112
125
  const projectName = basename(cwd)
@@ -131,6 +144,7 @@ function outputStep(stageName, stepIndex, steps, cwd) {
131
144
  console.log(`step: ${stepIndex + 1}/${total}`)
132
145
  console.log(`stepName: ${step.name}`)
133
146
  console.log(`project: ${projectName}`)
147
+ if (changeName) console.log(`change: ${changeName}`)
134
148
  console.log(`---\n`)
135
149
  if (personas[stageName]) {
136
150
  console.log(personas[stageName])
@@ -146,8 +160,9 @@ function outputStep(stageName, stepIndex, steps, cwd) {
146
160
  console.log('- 完成后立即执行 --done 命令,不得跳过')
147
161
  console.log('- 文档类型文件(.md/.yaml/.json 等)头部必须包含 author(git 用户名)和 created_at(精确到秒)')
148
162
  console.log('- 执行构建/测试前必须先读 local.yaml,优先使用其中配置的命令、路径和环境变量;未配置时才使用默认值')
163
+ const changeFlag = changeName ? ` --change ${changeName}` : ''
149
164
  console.log(`\n### 完成后执行`)
150
- console.log(`sillyspec run ${stageName} --done --input "用户原始需求/反馈" --output "你的摘要"`)
165
+ console.log(`sillyspec run ${stageName} --done${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
151
166
  }
152
167
 
153
168
  /**
@@ -200,41 +215,45 @@ export async function runCommand(args, cwd) {
200
215
  const isAuxiliary = auxiliaryStages.includes(stageName)
201
216
 
202
217
  const pm = new ProgressManager()
203
- let progress = pm.read(cwd)
218
+ pm._migrateIfNeeded(cwd)
219
+ let progress = pm.read(cwd, changeName)
204
220
 
205
221
  if (!progress) {
206
- // 辅助命令可以在没有 progress.json 时工作(比如 scan)
207
- if (!isAuxiliary) {
222
+ // 如果指定了变更名或有变更目录,自动初始化变更的 progress
223
+ const autoChange = changeName || resolveChangeNameAuto(cwd)
224
+ if (autoChange) {
225
+ progress = pm.initChange(cwd, autoChange)
226
+ } else if (!isAuxiliary) {
208
227
  console.error('❌ 未找到 progress.json,请先运行 sillyspec init')
228
+ console.error(' 提示:使用 --change <name> 指定变更名')
209
229
  process.exit(1)
210
230
  }
211
- progress = pm.init(cwd)
212
231
  }
213
232
 
233
+ // 确保 progress 有 currentChange
234
+ const effectiveChange = changeName || progress.currentChange || resolveChangeName(cwd, progress)
235
+
214
236
  // -- auto 模式:自动推进所有流程阶段
215
237
  if (stageName === 'auto') {
216
- return await runAutoMode(pm, progress, cwd, flags)
238
+ return await runAutoMode(pm, progress, cwd, flags, effectiveChange)
217
239
  }
218
240
 
219
- // --change 设置当前变更名
220
- if (changeName) {
221
- progress.currentChange = changeName
222
- progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
223
- pm._write(cwd, progress)
224
- console.log(`✅ 当前变更设置为:${changeName}`)
225
- return
241
+ // --change 只作为变更名标识,不再拦截流程
242
+ // 注册变更到全局活跃列表(如果尚未注册)
243
+ if (effectiveChange) {
244
+ pm.registerChange(cwd, effectiveChange)
226
245
  }
227
246
 
228
247
  // --reset
229
248
  if (isReset) {
230
- return await resetStage(pm, progress, stageName, cwd)
249
+ return await resetStage(pm, progress, stageName, cwd, effectiveChange)
231
250
  }
232
251
 
233
252
  // 确保步骤已初始化
234
253
  const changed = await ensureStageSteps(progress, stageName, cwd)
235
254
  if (changed) {
236
- pm._write(cwd, progress)
237
- progress = pm.read(cwd)
255
+ pm._write(cwd, progress, effectiveChange)
256
+ progress = pm.read(cwd, effectiveChange)
238
257
  }
239
258
 
240
259
  // --status
@@ -244,23 +263,35 @@ export async function runCommand(args, cwd) {
244
263
 
245
264
  // --skip
246
265
  if (isSkip) {
247
- return await skipStep(pm, progress, stageName, cwd)
266
+ return await skipStep(pm, progress, stageName, cwd, effectiveChange)
248
267
  }
249
268
 
250
269
  // --done
251
270
  if (isDone) {
252
- return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm })
271
+ return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange })
253
272
  }
254
273
 
255
274
  // 默认:输出当前步骤
256
- return await runStage(pm, progress, stageName, cwd)
275
+ return await runStage(pm, progress, stageName, cwd, effectiveChange)
257
276
  }
258
277
 
259
- async function runStage(pm, progress, stageName, cwd) {
278
+ /**
279
+ * 自动推导变更名(不依赖 progress)
280
+ */
281
+ function resolveChangeNameAuto(cwd) {
282
+ const changesDir = join(cwd, '.sillyspec', 'changes')
283
+ if (!existsSync(changesDir)) return null
284
+ const entries = readdirSync(changesDir, { withFileTypes: true })
285
+ .filter(e => e.isDirectory() && e.name !== 'archive')
286
+ if (entries.length === 1) return entries[0].name
287
+ return null
288
+ }
289
+
290
+ async function runStage(pm, progress, stageName, cwd, changeName) {
260
291
  // 自动探测 currentChange
261
292
  if (autoDetectChange(progress, cwd)) {
262
293
  progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
263
- pm._write(cwd, progress)
294
+ pm._write(cwd, progress, changeName)
264
295
  }
265
296
 
266
297
  const stageData = progress.stages[stageName]
@@ -273,7 +304,7 @@ async function runStage(pm, progress, stageName, cwd) {
273
304
  if (progress.currentStage !== stageName) {
274
305
  progress.currentStage = stageName
275
306
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
276
- pm._write(cwd, progress)
307
+ pm._write(cwd, progress, changeName)
277
308
  }
278
309
 
279
310
  const steps = stageData.steps
@@ -288,13 +319,12 @@ async function runStage(pm, progress, stageName, cwd) {
288
319
  stageData.status = 'in-progress'
289
320
  stageData.startedAt = new Date().toLocaleString('zh-CN', { hour12: false })
290
321
  stageData.completedAt = null
291
- pm._write(cwd, progress)
322
+ pm._write(cwd, progress, changeName)
292
323
  currentIdx = 0
293
324
  console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
294
325
  }
295
326
 
296
327
  if (currentIdx > 0) {
297
- // 有进行中的步骤,提示用户
298
328
  const completed = currentIdx
299
329
  const total = steps.length
300
330
  console.log(`⚠️ ${stageName} 已进行到第 ${currentIdx + 1}/${total} 步(前 ${completed} 步已完成)。`)
@@ -303,7 +333,7 @@ async function runStage(pm, progress, stageName, cwd) {
303
333
 
304
334
  const defSteps = await getStageSteps(stageName, cwd, progress)
305
335
  if (defSteps && defSteps[currentIdx]) {
306
- outputStep(stageName, currentIdx, defSteps, cwd)
336
+ outputStep(stageName, currentIdx, defSteps, cwd, changeName)
307
337
  }
308
338
  }
309
339
 
@@ -311,7 +341,6 @@ function validateMetadata(cwd, stageName) {
311
341
  const changesDir = join(cwd, '.sillyspec', 'changes')
312
342
  if (!existsSync(changesDir)) return
313
343
 
314
- // 找最近 10 分钟内修改的 md/yaml 文件
315
344
  const cutoff = Date.now() - 10 * 60 * 1000
316
345
  const missing = []
317
346
 
@@ -340,7 +369,7 @@ function validateMetadata(cwd, stageName) {
340
369
  }
341
370
 
342
371
  async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
343
- const { printNext = true, confirm = false } = options
372
+ const { printNext = true, confirm = false, changeName } = options
344
373
  const stageData = progress.stages[stageName]
345
374
  if (!stageData || !stageData.steps) {
346
375
  console.error(`❌ 阶段 ${stageName} 未初始化`)
@@ -355,19 +384,16 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
355
384
  process.exit(1)
356
385
  }
357
386
 
358
- // 标记完成(先标记,再处理动态步骤插入)
359
-
360
387
  steps[currentIdx].status = 'completed'
361
388
  steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
362
389
  if (outputText) {
363
390
  const MAX_OUTPUT = 200
364
391
  if (outputText.length > MAX_OUTPUT) {
365
392
  steps[currentIdx].output = outputText.slice(0, MAX_OUTPUT) + '…'
366
- // Save full output to artifacts/
367
393
  const artifactsDir = join(cwd, '.sillyspec', '.runtime', 'artifacts')
368
394
  mkdirSync(artifactsDir, { recursive: true })
369
395
  const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
370
- writeFileSync(join(artifactsDir, `${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
396
+ writeFileSync(join(artifactsDir, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
371
397
  } else {
372
398
  steps[currentIdx].output = outputText
373
399
  }
@@ -384,10 +410,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
384
410
  const fullSteps = buildPlanSteps(changeDir, planContent)
385
411
  const prefixLen = fixedPrefix.length
386
412
  const suffixLen = fixedSuffix.length
387
- // 提取协调器步骤(prefix 和 suffix 之间)
388
413
  const coordinatorSteps = fullSteps.slice(prefixLen, suffixLen > 0 ? -suffixLen : undefined)
389
414
  if (coordinatorSteps.length > 0) {
390
- // 在当前步骤之后插入协调器步骤(含 prompt,否则 outputStep 无法打印)
391
415
  for (let i = 0; i < coordinatorSteps.length; i++) {
392
416
  steps.splice(currentIdx + 1 + i, 0, {
393
417
  name: coordinatorSteps[i].name,
@@ -406,36 +430,34 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
406
430
  const nextPendingIdx = steps.findIndex(s => s.status === 'pending')
407
431
 
408
432
  if (nextPendingIdx === -1) {
409
- // 全部完成 — 仅标记当前阶段,不自动推进 currentStage
410
433
  stageData.status = 'completed'
411
434
  stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
412
435
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
413
- pm._write(cwd, progress)
436
+ pm._write(cwd, progress, changeName)
414
437
 
415
438
  // Append to user-inputs.md
416
439
  if (outputText) {
417
440
  const inputsPath = join(cwd, '.sillyspec', '.runtime', 'user-inputs.md')
418
- const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
441
+ const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
419
442
  appendFileSync(inputsPath, entry)
420
443
  }
421
444
 
422
- // 验证:检查生成的文件是否包含 author 和 created_at
423
445
  validateMetadata(cwd, stageName)
424
446
 
425
- // archive 阶段 Step 2(确认归档)完成时自动执行归档移动
447
+ // archive 阶段确认归档
426
448
  if (stageName === 'archive' && steps[currentIdx]?.name === '确认归档') {
427
449
  if (confirm) {
428
450
  const { renameSync } = await import('fs')
429
- const changeName = progress.currentChange
430
- if (!changeName) {
451
+ const archiveChangeName = progress.currentChange
452
+ if (!archiveChangeName) {
431
453
  console.error('❌ 归档失败:未找到当前变更名(currentChange)')
432
454
  process.exit(1)
433
455
  }
434
456
  const changesDir = join(cwd, '.sillyspec', 'changes')
435
457
  const archiveDir = join(changesDir, 'archive')
436
- const srcDir = join(changesDir, changeName)
458
+ const srcDir = join(changesDir, archiveChangeName)
437
459
  const date = new Date().toISOString().slice(0, 10)
438
- const destDir = join(archiveDir, `${date}-${changeName}`)
460
+ const destDir = join(archiveDir, `${date}-${archiveChangeName}`)
439
461
 
440
462
  if (!existsSync(srcDir)) {
441
463
  console.error(`❌ 归档失败:源目录不存在 ${srcDir}`)
@@ -448,21 +470,20 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
448
470
  mkdirSync(archiveDir, { recursive: true })
449
471
  renameSync(srcDir, destDir)
450
472
 
451
- // 校验
452
473
  if (!existsSync(destDir) || existsSync(srcDir)) {
453
474
  console.error('❌ 归档校验失败:移动操作异常')
454
475
  process.exit(1)
455
476
  }
456
477
 
457
- // 清除 currentChange
458
- progress.currentChange = null
459
- console.log(`📦 已归档:${changeName} → archive/${date}-${changeName}/`)
478
+ // 从全局活跃列表移除
479
+ pm.unregisterChange(cwd, archiveChangeName)
480
+ console.log(`📦 已归档:${archiveChangeName} → archive/${date}-${archiveChangeName}/`)
460
481
  } else {
461
482
  console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
462
483
  }
463
484
  }
464
485
 
465
- // 辅助阶段(archive/scan/quick 等)完成后重置步骤,允许重复执行
486
+ // 辅助阶段完成后重置步骤
466
487
  const stageDef = stageRegistry[stageName]
467
488
  if (stageDef?.auxiliary) {
468
489
  const freshSteps = (stageDef.steps || []).map(s => ({
@@ -474,13 +495,12 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
474
495
  stageData.steps = freshSteps
475
496
  stageData.status = 'pending'
476
497
  stageData.completedAt = null
477
- pm._write(cwd, progress)
498
+ pm._write(cwd, progress, changeName)
478
499
  }
479
500
 
480
501
  const total = steps.length
481
502
  console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
482
503
 
483
- // 阶段完成后提示下一步
484
504
  if (stageName === 'execute') {
485
505
  console.log('\n👉 下一步:sillyspec run verify(验证通过后才能归档)')
486
506
  } else if (stageName === 'verify') {
@@ -494,24 +514,24 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
494
514
  }
495
515
 
496
516
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
497
- pm._write(cwd, progress)
517
+ pm._write(cwd, progress, changeName)
498
518
 
499
519
  // Append to user-inputs.md
500
520
  if (outputText) {
501
521
  const inputsPath = join(cwd, '.sillyspec', '.runtime', 'user-inputs.md')
502
- const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
522
+ const entry = `\n## ${new Date().toLocaleString('zh-CN',{hour12:false})} | ${changeName || '?'} | ${stageName}: ${steps[currentIdx].name}\n${inputText ? "- 输入:" + inputText + "\n" : ""}- 输出:${outputText}\n`
503
523
  appendFileSync(inputsPath, entry)
504
524
  }
505
525
 
506
526
  const defSteps = await getStageSteps(stageName, cwd, progress)
507
527
  console.log(`✅ Step ${currentIdx + 1}/${steps.length} 完成:${steps[currentIdx].name}\n`)
508
528
  if (printNext) {
509
- outputStep(stageName, nextPendingIdx, defSteps, cwd)
529
+ outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName)
510
530
  }
511
531
  return { stageCompleted: false, currentIdx, nextPendingIdx }
512
532
  }
513
533
 
514
- async function skipStep(pm, progress, stageName, cwd) {
534
+ async function skipStep(pm, progress, stageName, cwd, changeName) {
515
535
  const stageData = progress.stages[stageName]
516
536
  if (!stageData || !stageData.steps) {
517
537
  console.error(`❌ 阶段 ${stageName} 未初始化`)
@@ -536,15 +556,14 @@ async function skipStep(pm, progress, stageName, cwd) {
536
556
  steps[currentIdx].status = 'skipped'
537
557
  steps[currentIdx].skippedAt = new Date().toLocaleString('zh-CN',{hour12:false})
538
558
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
539
- pm._write(cwd, progress)
559
+ pm._write(cwd, progress, changeName)
540
560
 
541
561
  console.log(`⏭️ Step ${currentIdx + 1}/${steps.length} 已跳过:${steps[currentIdx].name}`)
542
562
 
543
- // 输出下一步
544
563
  const nextPendingIdx = steps.findIndex(s => s.status === 'pending')
545
564
  if (nextPendingIdx !== -1 && defSteps) {
546
565
  console.log('')
547
- outputStep(stageName, nextPendingIdx, defSteps, cwd)
566
+ outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName)
548
567
  }
549
568
  }
550
569
 
@@ -567,7 +586,6 @@ function showStatus(progress, stageName) {
567
586
 
568
587
  const firstPending = steps.findIndex(s => s.status === 'pending')
569
588
 
570
- // 批量进度
571
589
  if (progress.batchProgress) {
572
590
  const bp = progress.batchProgress
573
591
  const bpTotal = bp.total || 0
@@ -591,7 +609,7 @@ function showStatus(progress, stageName) {
591
609
  })
592
610
  }
593
611
 
594
- async function resetStage(pm, progress, stageName, cwd) {
612
+ async function resetStage(pm, progress, stageName, cwd, changeName) {
595
613
  const defSteps = await getStageSteps(stageName, cwd, progress)
596
614
  progress.stages[stageName] = {
597
615
  status: 'in-progress',
@@ -600,14 +618,14 @@ async function resetStage(pm, progress, stageName, cwd) {
600
618
  steps: defSteps ? defSteps.map(s => ({ name: s.name, status: 'pending' })) : []
601
619
  }
602
620
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
603
- pm._write(cwd, progress)
621
+ pm._write(cwd, progress, changeName)
604
622
  console.log(`🔄 ${stageName} 阶段已重置`)
605
623
  }
606
624
 
607
625
  /**
608
626
  * auto 模式:自动推进 brainstorm → plan → execute → verify
609
627
  */
610
- async function runAutoMode(pm, progress, cwd, flags) {
628
+ async function runAutoMode(pm, progress, cwd, flags, changeName) {
611
629
  const flowStages = ['brainstorm', 'plan', 'execute', 'verify']
612
630
  const isDone = flags.includes('--done')
613
631
  const outputIdx = flags.indexOf('--output')
@@ -623,8 +641,8 @@ async function runAutoMode(pm, progress, cwd, flags) {
623
641
  const stageChanged = progress.currentStage !== stage
624
642
  progress.currentStage = stage
625
643
  const changed = await ensureStageSteps(progress, stage, cwd)
626
- if (stageChanged || changed) pm._write(cwd, progress)
627
- progress = pm.read(cwd)
644
+ if (stageChanged || changed) pm._write(cwd, progress, changeName)
645
+ progress = pm.read(cwd, changeName)
628
646
  return progress
629
647
  }
630
648
 
@@ -637,7 +655,6 @@ async function runAutoMode(pm, progress, cwd, flags) {
637
655
  return
638
656
  }
639
657
  if (!flowStages.includes(currentStage)) {
640
- // 当前在辅助阶段(scan/quick/archive 等),自动跳到第一个未完成的流程阶段
641
658
  const openStage = firstOpenStage()
642
659
  if (!openStage) {
643
660
  console.log('All auto flow stages are complete.')
@@ -651,6 +668,7 @@ async function runAutoMode(pm, progress, cwd, flags) {
651
668
  if (!isDone) {
652
669
  console.log('════════════════════════════════════════')
653
670
  console.log(' SillySpec Auto Mode')
671
+ if (changeName) console.log(` Change: ${changeName}`)
654
672
  console.log('════════════════════════════════════════')
655
673
  console.log(` Flow: ${flowStages.join(' -> ')}`)
656
674
  console.log(` Current: ${currentStage}`)
@@ -671,7 +689,7 @@ async function runAutoMode(pm, progress, cwd, flags) {
671
689
  else console.log('All auto flow stages are complete.')
672
690
  return
673
691
  }
674
- outputStep(currentStage, pendingIdx, defSteps, cwd)
692
+ outputStep(currentStage, pendingIdx, defSteps, cwd, changeName)
675
693
  return
676
694
  }
677
695
 
@@ -680,14 +698,14 @@ async function runAutoMode(pm, progress, cwd, flags) {
680
698
  process.exit(1)
681
699
  }
682
700
 
683
- const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false })
701
+ const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName })
684
702
  if (!result) return
685
- progress = pm.read(cwd)
703
+ progress = pm.read(cwd, changeName)
686
704
 
687
705
  const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending') ?? -1
688
706
  if (nextPendingIdx !== -1) {
689
707
  const defSteps = await getStageSteps(currentStage, cwd, progress)
690
- outputStep(currentStage, nextPendingIdx, defSteps, cwd)
708
+ outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName)
691
709
  return
692
710
  }
693
711
 
@@ -707,11 +725,11 @@ async function runAutoMode(pm, progress, cwd, flags) {
707
725
  }
708
726
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
709
727
  await ensureStageSteps(progress, next, cwd)
710
- pm._write(cwd, progress)
711
- progress = pm.read(cwd)
728
+ pm._write(cwd, progress, changeName)
729
+ progress = pm.read(cwd, changeName)
712
730
 
713
731
  console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
714
732
  const nextSteps = await getStageSteps(next, cwd, progress)
715
733
  const firstPending = progress.stages[next]?.steps?.findIndex(step => step.status === 'pending') ?? -1
716
- if (firstPending !== -1) outputStep(next, firstPending, nextSteps, cwd)
734
+ if (firstPending !== -1) outputStep(next, firstPending, nextSteps, cwd, changeName)
717
735
  }
@@ -5,7 +5,7 @@ export const definition = {
5
5
  steps: [
6
6
  {
7
7
  name: '状态检查',
8
- prompt: `检查 .sillyspec/.runtime/progress.json 确认当前状态。
8
+ prompt: `检查当前变更的 progress.json 确认当前状态。
9
9
 
10
10
  ### 操作
11
11
  1. 运行 \`sillyspec progress show\`
@@ -18,9 +18,15 @@ export const definition = {
18
18
  for d in .sillyspec .sillyspec/projects .sillyspec/docs .sillyspec/changes .sillyspec/.runtime; do
19
19
  [ -d "$d" ] && echo "✅ $d" || echo "❌ $d"
20
20
  done
21
- # 检查 progress.json
22
- [ -f .sillyspec/.runtime/progress.json ] && echo "✅ progress.json 存在" || echo "❌ progress.json 不存在"
23
- node -e "JSON.parse(require('fs').readFileSync('.sillyspec/.runtime/progress.json','utf8')); console.log('✅ progress.json 可解析')" 2>/dev/null || echo "⚠️ progress.json 不可解析"
21
+ # 检查 progress.json(支持新版按变更隔离和旧版全局)
22
+ if [ -d .sillyspec/changes ]; then
23
+ PROGRESS_FILE=$(find .sillyspec/changes -maxdepth 2 -name progress.json | head -1)
24
+ fi
25
+ if [ -z "$PROGRESS_FILE" ]; then
26
+ PROGRESS_FILE='.sillyspec/.runtime/progress.json'
27
+ fi
28
+ [ -f "$PROGRESS_FILE" ] && echo "✅ progress.json 存在 ($PROGRESS_FILE)" || echo "❌ progress.json 不存在"
29
+ node -e "JSON.parse(require('fs').readFileSync('$PROGRESS_FILE','utf8')); console.log('✅ progress.json 可解析')" 2>/dev/null || echo "⚠️ progress.json 不可解析"
24
30
  \`\`\`
25
31
 
26
32
  ### 2. 项目配置检查
@@ -42,7 +48,7 @@ done
42
48
  \`\`\`bash
43
49
  # 读取 currentChange 并检查目录存在性
44
50
  node -e "
45
- const p = JSON.parse(require('fs').readFileSync('.sillyspec/.runtime/progress.json','utf8'));
51
+ const p = JSON.parse(require('fs').readFileSync(process.env.PROGRESS_FILE || '.sillyspec/.runtime/progress.json','utf8'));
46
52
  const cc = p.currentChange;
47
53
  if (!cc) { console.log('ℹ️ 无当前变更'); process.exit(0); }
48
54
  const dir = '.sillyspec/changes/' + cc;
@@ -68,7 +74,7 @@ if (!fs.existsSync(dir)) { console.log('ℹ️ changes/ 目录不存在'); proce
68
74
  const subs = fs.readdirSync(dir).filter(f => fs.statSync(dir+'/'+f).isDirectory());
69
75
  if (subs.length === 0) { console.log('ℹ️ 无变更目录'); process.exit(0); }
70
76
  let progress;
71
- try { progress = JSON.parse(fs.readFileSync('.sillyspec/.runtime/progress.json','utf8')); } catch { console.log('⚠️ 无法读取 progress.json'); subs.forEach(s => console.log('❓ ' + s)); process.exit(0); }
77
+ try { progress = JSON.parse(fs.readFileSync(process.env.PROGRESS_FILE || '.sillyspec/.runtime/progress.json','utf8')); } catch { console.log('⚠️ 无法读取 progress.json'); subs.forEach(s => console.log('❓ ' + s)); process.exit(0); }
72
78
  const known = new Set();
73
79
  if (progress.currentChange) known.add(progress.currentChange);
74
80
  for (const sd of Object.values(progress.stages || {})) {
@@ -121,7 +127,7 @@ done
121
127
  # 确定项目路径(使用 progress.json 中的项目或当前目录)
122
128
  PROJECT_DIR=$(node -e "
123
129
  const fs=require('fs');
124
- try{const p=JSON.parse(fs.readFileSync('.sillyspec/.runtime/progress.json','utf8'));if(p.project){console.log(p.project);process.exit(0)}}catch{}
130
+ try{const fs=require('fs'),path=require('path');const changesDir='.sillyspec/changes';let pp=null;if(fs.existsSync(changesDir)){const entries=fs.readdirSync(changesDir,{withFileTypes:true}).filter(e=>e.isDirectory()&&e.name!=='archive');if(entries.length===1)pp=path.join(changesDir,entries[0].name,'progress.json');}if(!pp)pp='.sillyspec/.runtime/progress.json';const p=JSON.parse(fs.readFileSync(pp,'utf8'));if(p.project){console.log(p.project);process.exit(0)}}catch{}
125
131
  const files=fs.readdirSync('.sillyspec/projects').filter(f=>f.endsWith('.yaml'));
126
132
  if(files.length>0){const c=fs.readFileSync('.sillyspec/projects/'+files[0],'utf8');const m=c.match(/^path:\\s*(.+)/m);console.log(m?m[1].trim():'.')}else console.log('.')
127
133
  " 2>/dev/null)
@@ -39,7 +39,7 @@ export const definition = {
39
39
  prompt: `检测已有扫描文档,只生成缺失的。
40
40
 
41
41
  ### 操作
42
- 1. \`PROJECT=$(python3 -c "import sys,json; print(json.load(open('.sillyspec/.runtime/progress.json')).get('project',''))" 2>/dev/null || basename "$(pwd)")\`
42
+ 1. \`PROJECT=$(python3 -c "import sys,json,glob; files=glob.glob('.sillyspec/changes/*/progress.json'); print(json.load(open(files[0])).get('project','')) if files else print('')" 2>/dev/null || basename "$(pwd)")\`
43
43
  2. 检查 7 份文档是否存在:ARCHITECTURE、STRUCTURE、CONVENTIONS、INTEGRATIONS、TESTING、CONCERNS、PROJECT
44
44
  3. 列出已有 ✅ 和缺失 ⬜
45
45
  4. 只生成缺失的文档