sillyspec 3.7.7 → 3.7.9

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 (43) hide show
  1. package/.sillyspec/changes/dashboard/design.md +219 -0
  2. package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
  3. package/.sillyspec/specs/2026-04-05-dashboard-design.md +206 -0
  4. package/bin/sillyspec.js +0 -0
  5. package/package.json +1 -1
  6. package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +1 -0
  7. package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +17 -0
  8. package/packages/dashboard/dist/index.html +16 -0
  9. package/packages/dashboard/index.html +15 -0
  10. package/packages/dashboard/package-lock.json +2164 -0
  11. package/packages/dashboard/package.json +22 -0
  12. package/packages/dashboard/server/executor.js +86 -0
  13. package/packages/dashboard/server/index.js +359 -0
  14. package/packages/dashboard/server/parser.js +154 -0
  15. package/packages/dashboard/server/watcher.js +277 -0
  16. package/packages/dashboard/src/App.vue +154 -0
  17. package/packages/dashboard/src/components/ActionBar.vue +100 -0
  18. package/packages/dashboard/src/components/CommandPalette.vue +117 -0
  19. package/packages/dashboard/src/components/DetailPanel.vue +122 -0
  20. package/packages/dashboard/src/components/LogStream.vue +85 -0
  21. package/packages/dashboard/src/components/PipelineStage.vue +75 -0
  22. package/packages/dashboard/src/components/PipelineView.vue +94 -0
  23. package/packages/dashboard/src/components/ProjectList.vue +152 -0
  24. package/packages/dashboard/src/components/StageBadge.vue +53 -0
  25. package/packages/dashboard/src/components/StepCard.vue +89 -0
  26. package/packages/dashboard/src/composables/useDashboard.js +171 -0
  27. package/packages/dashboard/src/composables/useKeyboard.js +117 -0
  28. package/packages/dashboard/src/composables/useWebSocket.js +129 -0
  29. package/packages/dashboard/src/main.js +5 -0
  30. package/packages/dashboard/src/style.css +132 -0
  31. package/packages/dashboard/vite.config.js +18 -0
  32. package/src/index.js +68 -8
  33. package/src/init.js +23 -1
  34. package/src/progress.js +422 -0
  35. package/src/setup.js +16 -0
  36. package/templates/archive.md +56 -0
  37. package/templates/brainstorm.md +82 -26
  38. package/templates/commit.md +2 -0
  39. package/templates/execute.md +20 -1
  40. package/templates/progress-format.md +90 -0
  41. package/templates/quick.md +36 -3
  42. package/templates/resume-dialog.md +55 -0
  43. package/templates/skills/playwright-e2e/SKILL.md +1 -1
package/src/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  /**
4
4
  * SillySpec CLI — 安装工具
5
- *
5
+ *
6
6
  * 只负责两件事:init(安装命令模板)和 setup(安装 MCP 工具)。
7
7
  * 状态管理由 AI 直接读文件(STATE.md)完成,不需要 CLI。
8
8
  */
9
-
10
9
  import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
11
10
  import { join, resolve } from 'path';
12
11
  import { cmdInit, getVersion } from './init.js';
12
+ import { ProgressManager } from './progress.js';
13
13
 
14
14
  // ── CLI 入口 ──
15
15
 
@@ -25,6 +25,15 @@ SillySpec CLI — 规范驱动开发工具包
25
25
  [--dir <path>] 指定目录
26
26
  sillyspec setup [--list] 安装推荐 MCP 工具
27
27
  [--list] 查看已安装状态
28
+ sillyspec progress <cmd> 进度恢复管理
29
+ init 初始化进度文件
30
+ status 查看当前进度
31
+ validate 校验并修复进度文件
32
+ reset [--stage X] 重置进度(全部或指定阶段)
33
+ complete --stage X 归档已完成阶段
34
+ sillyspec dashboard 启动 Dashboard Web UI
35
+ [--port <number>] 指定端口(默认 3456)
36
+ [--no-open] 不自动打开浏览器
28
37
 
29
38
  选项:
30
39
  --json 输出 JSON(给 AI 程序化读取)
@@ -36,12 +45,14 @@ SillySpec CLI — 规范驱动开发工具包
36
45
  sillyspec init --workspace
37
46
  sillyspec setup
38
47
  sillyspec setup --list
48
+ sillyspec dashboard
49
+ sillyspec dashboard --port 8080 --no-open
39
50
  `);
40
51
  }
41
52
 
42
53
  async function main() {
43
54
  const args = process.argv.slice(2);
44
-
55
+
45
56
  if (args[0] === '--version' || args[0] === '-v') {
46
57
  console.log(getVersion());
47
58
  process.exit(0);
@@ -51,7 +62,7 @@ async function main() {
51
62
  printUsage();
52
63
  process.exit(0);
53
64
  }
54
-
65
+
55
66
  // 解析全局选项
56
67
  let json = false;
57
68
  let targetDir = process.cwd();
@@ -59,7 +70,7 @@ async function main() {
59
70
  let workspace = false;
60
71
  let interactive = false;
61
72
  const filteredArgs = [];
62
-
73
+
63
74
  for (let i = 0; i < args.length; i++) {
64
75
  if (args[i] === '--json') {
65
76
  json = true;
@@ -79,15 +90,15 @@ async function main() {
79
90
  filteredArgs.push(args[i]);
80
91
  }
81
92
  }
82
-
93
+
83
94
  const command = filteredArgs[0];
84
95
  const dir = targetDir;
85
-
96
+
86
97
  if (!existsSync(dir)) {
87
98
  console.error(`❌ 目录不存在: ${dir}`);
88
99
  process.exit(1);
89
100
  }
90
-
101
+
91
102
  switch (command) {
92
103
  case 'init':
93
104
  await cmdInit(dir, { tool, workspace, interactive });
@@ -96,6 +107,55 @@ async function main() {
96
107
  const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
97
108
  await (await import('./setup.js')).cmdSetup(dir, { json, list: setupList });
98
109
  break;
110
+ case 'progress': {
111
+ const pm = new ProgressManager();
112
+ const subCommand = filteredArgs[1];
113
+ const stageIdx = args.indexOf('--stage');
114
+ const stage = stageIdx >= 0 && args[stageIdx + 1] ? args[stageIdx + 1] : null;
115
+
116
+ switch (subCommand) {
117
+ case 'init':
118
+ pm.init(dir);
119
+ break;
120
+ case 'status':
121
+ pm.status(dir);
122
+ break;
123
+ case 'validate':
124
+ pm.validate(dir);
125
+ break;
126
+ case 'reset':
127
+ pm.reset(dir, stage);
128
+ break;
129
+ case 'complete':
130
+ pm.complete(dir, stage);
131
+ break;
132
+ default:
133
+ console.log('用法: sillyspec progress <init|status|validate|reset|complete> [--stage <stage>]');
134
+ }
135
+ break;
136
+ }
137
+ case 'dashboard': {
138
+ // Parse dashboard options
139
+ let port = 3456;
140
+ let openBrowser = true;
141
+
142
+ for (let i = 1; i < args.length; i++) {
143
+ if (args[i] === '--port' && args[i + 1]) {
144
+ port = parseInt(args[i + 1], 10);
145
+ i++;
146
+ } else if (args[i] === '--no-open') {
147
+ openBrowser = false;
148
+ }
149
+ }
150
+
151
+ // Import and start dashboard server
152
+ const { startServer } = await import('../packages/dashboard/server/index.js');
153
+ startServer({ port, open: openBrowser });
154
+
155
+ // Keep process alive
156
+ console.log('按 Ctrl+C 停止服务器');
157
+ break;
158
+ }
99
159
  default:
100
160
  console.error(`❌ 未知命令: ${command}`);
101
161
  printUsage();
package/src/init.js CHANGED
@@ -209,6 +209,7 @@ async function doInstall(projectDir, tools, isWorkspace, subprojects = []) {
209
209
  // .sillyspec/changes/archive/ → archive
210
210
  // .sillyspec/quicklog/ → quick
211
211
  // .sillyspec/knowledge/ → archive (spec 沉淀)
212
+ // .sillyspec/.runtime/ → progress (gitignored)
212
213
  // (plan 内容已合并到 tasks.md)
213
214
  if (isWorkspace) {
214
215
  mkdirSync(join(projectDir, '.sillyspec', 'shared'), { recursive: true });
@@ -227,8 +228,29 @@ async function doInstall(projectDir, tools, isWorkspace, subprojects = []) {
227
228
  writeFileSync(uncatPath, `# 未分类知识\n\n> execute/quick 执行中发现的坑暂存于此,用户审阅后归类到对应文件并更新 INDEX.md。\n`);
228
229
  }
229
230
 
231
+ // 创建 .sillyspec/.runtime/ 目录结构
232
+ const runtimeDir = join(projectDir, '.sillyspec', '.runtime');
233
+ for (const sub of ['artifacts', 'history', 'logs', 'templates']) {
234
+ mkdirSync(join(runtimeDir, sub), { recursive: true });
235
+ }
236
+
237
+ // 复制 resume-dialog.md 到 .runtime/templates/
238
+ const resumeDialogSrc = join(TEMPLATE_DIR, 'resume-dialog.md');
239
+ if (existsSync(resumeDialogSrc)) {
240
+ const dest = join(runtimeDir, 'templates', 'resume-dialog.md');
241
+ if (!existsSync(dest)) {
242
+ copyFileSync(resumeDialogSrc, dest);
243
+ }
244
+ }
245
+
246
+ // 创建初始 user-inputs.md
247
+ const inputsPath = join(runtimeDir, 'user-inputs.md');
248
+ if (!existsSync(inputsPath)) {
249
+ writeFileSync(inputsPath, '# 用户输入记录\n\n> 每步完成时由 AI 自动追加,记录用户所有原话。\n\n');
250
+ }
251
+
230
252
  const gitignorePath = join(projectDir, '.gitignore');
231
- const ignoreRules = ['.sillyspec/STATE.md', '.sillyspec/codebase/SCAN-RAW.md', '.sillyspec/local.yaml'];
253
+ const ignoreRules = ['.sillyspec/STATE.md', '.sillyspec/codebase/SCAN-RAW.md', '.sillyspec/local.yaml', '.sillyspec/.runtime/'];
232
254
  if (existsSync(gitignorePath)) {
233
255
  const content = readFileSync(gitignorePath, 'utf8');
234
256
  let updated = content.trimEnd();
@@ -0,0 +1,422 @@
1
+ /**
2
+ * SillySpec ProgressManager — 进度恢复管理
3
+ *
4
+ * 纯 Node.js,无外部依赖。管理 .sillyspec/.runtime/progress.json。
5
+ */
6
+
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync, copyFileSync } from 'fs';
8
+ import { join, resolve, dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const RUNTIME_DIR = '.sillyspec/.runtime';
14
+ const PROGRESS_FILE = 'progress.json';
15
+ const BACKUP_FILE = 'progress.json.bak';
16
+
17
+ // ── 默认步骤定义 ──
18
+
19
+ const STEP_DEFINITIONS = {
20
+ brainstorm: [
21
+ { id: 1, name: '加载项目上下文' },
22
+ { id: 2, name: '协作与复用检查' },
23
+ { id: 3, name: '原型/设计图分析' },
24
+ { id: 4, name: '评估需求范围' },
25
+ { id: 5, name: '对话式探索' },
26
+ { id: 6, name: '提出方案并推荐' },
27
+ { id: 7, name: '分段展示设计' },
28
+ { id: 8, name: '写设计文档' },
29
+ { id: 9, name: 'AI 自审' },
30
+ { id: 10, name: '用户确认设计方案' },
31
+ { id: 11, name: '输出技术方案' },
32
+ { id: 12, name: '更新 STATE.md' },
33
+ { id: 13, name: '保存最终进度' },
34
+ ],
35
+ propose: [
36
+ { id: 1, name: '变更范围' },
37
+ { id: 2, name: '方案设计' },
38
+ { id: 3, name: '任务拆分' },
39
+ { id: 4, name: '优先级排序' },
40
+ { id: 5, name: '规范输出' },
41
+ ],
42
+ plan: [
43
+ { id: 1, name: '任务分析' },
44
+ { id: 2, name: '依赖梳理' },
45
+ { id: 3, name: '实现路径' },
46
+ { id: 4, name: '风险评估' },
47
+ { id: 5, name: '计划输出' },
48
+ ],
49
+ execute: [
50
+ { id: 1, name: '环境准备' },
51
+ { id: 2, name: '编码实现' },
52
+ { id: 3, name: '单元测试' },
53
+ { id: 4, name: '代码审查' },
54
+ { id: 5, name: '集成验证' },
55
+ ],
56
+ verify: [
57
+ { id: 1, name: '规范对照' },
58
+ { id: 2, name: '功能测试' },
59
+ { id: 3, name: '边界测试' },
60
+ { id: 4, name: '回归测试' },
61
+ { id: 5, name: '验收报告' },
62
+ ],
63
+ };
64
+
65
+ const EMPTY_PROGRESS = {
66
+ _version: 1,
67
+ schemaVersion: '1.0.0',
68
+ currentStage: 'brainstorm',
69
+ lastActiveAt: new Date().toISOString(),
70
+ resumeCount: 0,
71
+ checkpoint: '',
72
+ stages: {},
73
+ artifacts: [],
74
+ };
75
+
76
+ function emptyStage() {
77
+ return {
78
+ status: 'not_started',
79
+ completedSteps: [],
80
+ inProgressStep: null,
81
+ summaries: {},
82
+ artifacts: [],
83
+ stageSummary: null,
84
+ };
85
+ }
86
+
87
+ // ── ProgressManager ──
88
+
89
+ export class ProgressManager {
90
+ // ── 公开方法 ──
91
+
92
+ init(cwd) {
93
+ const runtimeDir = join(cwd, RUNTIME_DIR);
94
+ const subdirs = ['artifacts', 'history', 'logs', 'templates'];
95
+ mkdirSync(runtimeDir, { recursive: true });
96
+ for (const d of subdirs) {
97
+ mkdirSync(join(runtimeDir, d), { recursive: true });
98
+ }
99
+
100
+ const progressPath = join(runtimeDir, PROGRESS_FILE);
101
+ if (!existsSync(progressPath)) {
102
+ // 初始化所有阶段
103
+ const data = { ...EMPTY_PROGRESS, stages: {} };
104
+ for (const stage of Object.keys(STEP_DEFINITIONS)) {
105
+ data.stages[stage] = emptyStage();
106
+ }
107
+ writeFileSync(progressPath, JSON.stringify(data, null, 2) + '\n');
108
+ console.log(`✅ 已创建 ${join(RUNTIME_DIR, PROGRESS_FILE)}`);
109
+ } else {
110
+ console.log(`ℹ️ ${join(RUNTIME_DIR, PROGRESS_FILE)} 已存在,跳过`);
111
+ }
112
+
113
+ // 复制 resume-dialog.md 模板
114
+ const templateDir = resolve(__dirname, '..', 'templates');
115
+ const resumeSrc = join(templateDir, 'resume-dialog.md');
116
+ const resumeDest = join(runtimeDir, 'templates', 'resume-dialog.md');
117
+ if (existsSync(resumeSrc) && !existsSync(resumeDest)) {
118
+ copyFileSync(resumeSrc, resumeDest);
119
+ }
120
+
121
+ // 创建 user-inputs.md
122
+ const inputsPath = join(runtimeDir, 'user-inputs.md');
123
+ if (!existsSync(inputsPath)) {
124
+ writeFileSync(inputsPath, '# 用户输入记录\n\n> 每步完成时由 AI 自动追加,记录用户所有原话。\n\n');
125
+ }
126
+
127
+ // .gitignore
128
+ this._ensureGitignore(cwd);
129
+
130
+ return this.read(cwd);
131
+ }
132
+
133
+ read(cwd) {
134
+ const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
135
+ const backupPath = join(cwd, RUNTIME_DIR, BACKUP_FILE);
136
+
137
+ // 三层容错:正常解析 → 修复 → 读 .bak
138
+ if (existsSync(progressPath)) {
139
+ const raw = readFileSync(progressPath, 'utf8');
140
+ const parsed = this._parseWithRecovery(raw);
141
+ if (parsed) return parsed;
142
+ }
143
+
144
+ if (existsSync(backupPath)) {
145
+ const raw = readFileSync(backupPath, 'utf8');
146
+ const parsed = this._parseWithRecovery(raw);
147
+ if (parsed) {
148
+ console.log('⚠️ progress.json 损坏,已从备份恢复');
149
+ writeFileSync(progressPath, JSON.stringify(parsed, null, 2) + '\n');
150
+ return parsed;
151
+ }
152
+ }
153
+
154
+ return null;
155
+ }
156
+
157
+ validate(cwd) {
158
+ const data = this.read(cwd);
159
+ if (!data) {
160
+ console.log('❌ 无法读取 progress.json');
161
+ return false;
162
+ }
163
+
164
+ const errors = this._validate(data);
165
+ if (errors.length === 0) {
166
+ console.log('✅ progress.json 格式正确');
167
+ return true;
168
+ }
169
+
170
+ console.log(`⚠️ 发现 ${errors.length} 个问题,尝试修复...`);
171
+
172
+ // 自动修复:补全缺失的阶段
173
+ let fixed = { ...data };
174
+ let changed = false;
175
+ for (const stage of Object.keys(STEP_DEFINITIONS)) {
176
+ if (!fixed.stages[stage]) {
177
+ fixed.stages[stage] = emptyStage();
178
+ changed = true;
179
+ }
180
+ }
181
+ if (!fixed._version) { fixed._version = 1; changed = true; }
182
+ if (!fixed.schemaVersion) { fixed.schemaVersion = '1.0.0'; changed = true; }
183
+ if (!fixed.artifacts) { fixed.artifacts = []; changed = true; }
184
+ if (!fixed.lastActiveAt) { fixed.lastActiveAt = new Date().toISOString(); changed = true; }
185
+
186
+ if (changed) {
187
+ this._backup(cwd);
188
+ const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
189
+ writeFileSync(progressPath, JSON.stringify(fixed, null, 2) + '\n');
190
+ console.log('✅ 已修复并备份');
191
+ } else {
192
+ console.log('❌ 无法自动修复:');
193
+ errors.forEach(e => console.log(` - ${e}`));
194
+ }
195
+
196
+ return errors.length === 0;
197
+ }
198
+
199
+ status(cwd) {
200
+ const data = this.read(cwd);
201
+ if (!data) {
202
+ console.log('❌ 未找到 progress.json,请先运行 sillyspec progress init');
203
+ return;
204
+ }
205
+
206
+ const stageLabels = {
207
+ brainstorm: '🧠 需求探索',
208
+ propose: '📋 方案设计',
209
+ plan: '📐 实现计划',
210
+ execute: '⚡ 波次执行',
211
+ verify: '🔍 验证确认',
212
+ };
213
+
214
+ console.log('');
215
+ console.log(' ═══════════════════════════════════════');
216
+ console.log(` 当前阶段: ${(stageLabels[data.currentStage] || data.currentStage)}`);
217
+ console.log(` 最近活跃: ${data.lastActiveAt ? this._timeAgo(data.lastActiveAt) : '未知'}`);
218
+ console.log(` 恢复次数: ${data.resumeCount ?? 0}`);
219
+ if (data.checkpoint) {
220
+ console.log(` 检查点: ${data.checkpoint}`);
221
+ }
222
+ console.log(' ═══════════════════════════════════════');
223
+ console.log('');
224
+
225
+ for (const [stage, def] of Object.entries(STEP_DEFINITIONS)) {
226
+ const stageData = data.stages[stage] || emptyStage();
227
+ const label = stageLabels[stage] || stage;
228
+ const statusIcons = { not_started: '⬜', in_progress: '🔵', completed: '✅' };
229
+ const icon = statusIcons[stageData.status] || '⬜';
230
+
231
+ let steps = '';
232
+ for (const step of def) {
233
+ if (stageData.completedSteps.includes(step.id)) {
234
+ steps += '●';
235
+ } else if (stageData.inProgressStep && stageData.inProgressStep.id === step.id) {
236
+ steps += '◐';
237
+ } else {
238
+ steps += '○';
239
+ }
240
+ }
241
+
242
+ const completed = (stageData.completedSteps || []).length;
243
+ const total = def.length;
244
+ console.log(` ${icon} ${label} [${steps}] ${completed}/${total}`);
245
+ }
246
+
247
+ console.log('');
248
+
249
+ // 产出文件
250
+ if (data.artifacts && data.artifacts.length > 0) {
251
+ console.log(' 📦 产出文件:');
252
+ for (const a of data.artifacts) {
253
+ console.log(` - ${a.name} (${a.stage}) → ${a.path}`);
254
+ }
255
+ console.log('');
256
+ }
257
+ }
258
+
259
+ reset(cwd, stage) {
260
+ this._ensureDir(cwd);
261
+ this._backup(cwd);
262
+
263
+ const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
264
+
265
+ if (stage) {
266
+ // 只重置指定阶段
267
+ const data = this.read(cwd);
268
+ if (!data) { console.log('❌ 无法读取 progress.json'); return; }
269
+ if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
270
+
271
+ data.stages[stage] = emptyStage();
272
+ data.lastActiveAt = new Date().toISOString();
273
+ writeFileSync(progressPath, JSON.stringify(data, null, 2) + '\n');
274
+ console.log(`✅ 已重置阶段: ${stage}`);
275
+ } else {
276
+ // 全部重置:备份后删除
277
+ if (existsSync(progressPath)) {
278
+ unlinkSync(progressPath);
279
+ console.log('✅ 已重置所有进度(备份已保留)');
280
+ } else {
281
+ console.log('ℹ️ 无进度文件可重置');
282
+ }
283
+ }
284
+ }
285
+
286
+ complete(cwd, stage) {
287
+ if (!stage) {
288
+ console.log('❌ 请指定阶段: --stage <stage>');
289
+ return;
290
+ }
291
+
292
+ const data = this.read(cwd);
293
+ if (!data) { console.log('❌ 无法读取 progress.json'); return; }
294
+
295
+ const stageData = data.stages[stage];
296
+ if (!stageData) { console.log(`❌ 未知阶段: ${stage}`); return; }
297
+
298
+ // 归档到 history/
299
+ const historyDir = join(cwd, RUNTIME_DIR, 'history');
300
+ mkdirSync(historyDir, { recursive: true });
301
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
302
+ const archiveName = `${stage}-${ts}.json`;
303
+ writeFileSync(join(historyDir, archiveName), JSON.stringify({ stage, data: stageData, completedAt: new Date().toISOString() }, null, 2) + '\n');
304
+
305
+ console.log(`✅ 已归档 ${stage} → ${join(RUNTIME_DIR, 'history', archiveName)}`);
306
+ }
307
+
308
+ // ── 内部方法 ──
309
+
310
+ _ensureDir(cwd) {
311
+ const runtimeDir = join(cwd, RUNTIME_DIR);
312
+ if (!existsSync(runtimeDir)) {
313
+ mkdirSync(runtimeDir, { recursive: true });
314
+ for (const d of ['artifacts', 'history', 'logs', 'templates']) {
315
+ mkdirSync(join(runtimeDir, d), { recursive: true });
316
+ }
317
+ }
318
+ }
319
+
320
+ _backup(cwd) {
321
+ const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
322
+ const backupPath = join(cwd, RUNTIME_DIR, BACKUP_FILE);
323
+ if (existsSync(progressPath)) {
324
+ renameSync(progressPath, backupPath);
325
+ }
326
+ }
327
+
328
+ _parseWithRecovery(jsonString) {
329
+ // 第一层:直接解析
330
+ try {
331
+ return JSON.parse(jsonString);
332
+ } catch {}
333
+
334
+ // 第二层:修复常见问题
335
+ let fixed = jsonString;
336
+
337
+ // 去尾随逗号(对象和数组中的 ,} 和 ,])
338
+ fixed = fixed.replace(/,\s*([}\]])/g, '$1');
339
+
340
+ // 单引号转双引号(简易处理)
341
+ // 只处理 key 和简单字符串值
342
+ fixed = fixed.replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3');
343
+ fixed = fixed.replace(/:\s*'([^']*)'([,}\]])/g, ':"$1"$2');
344
+
345
+ try {
346
+ return JSON.parse(fixed);
347
+ } catch {}
348
+
349
+ // 第三层:尝试截断到最后一个完整对象
350
+ const lastBrace = fixed.lastIndexOf('}');
351
+ if (lastBrace > 0) {
352
+ const truncated = fixed.substring(0, lastBrace + 1);
353
+ // 补全外层括号
354
+ let open = 0;
355
+ for (const ch of truncated) {
356
+ if (ch === '{') open++;
357
+ if (ch === '}') open--;
358
+ }
359
+ const repaired = truncated + '}'.repeat(Math.max(0, open));
360
+ try {
361
+ return JSON.parse(repaired);
362
+ } catch {}
363
+ }
364
+
365
+ return null;
366
+ }
367
+
368
+ _validate(data) {
369
+ const errors = [];
370
+ if (!data || typeof data !== 'object') { errors.push('数据不是有效对象'); return errors; }
371
+ if (!data.stages || typeof data.stages !== 'object') { errors.push('缺少 stages 字段'); }
372
+ if (!data.currentStage || typeof data.currentStage !== 'string') { errors.push('缺少 currentStage 字段'); }
373
+ if (!data.schemaVersion) { errors.push('缺少 schemaVersion 字段'); }
374
+ if (typeof data._version !== 'number' || data._version < 1) { errors.push('_version 应为正整数'); }
375
+
376
+ // 校验阶段数据
377
+ if (data.stages) {
378
+ for (const [name, stage] of Object.entries(data.stages)) {
379
+ if (!stage.status) errors.push(`阶段 ${name} 缺少 status`);
380
+ if (stage.completedSteps && !Array.isArray(stage.completedSteps)) {
381
+ errors.push(`阶段 ${name} 的 completedSteps 不是数组`);
382
+ }
383
+ }
384
+ }
385
+
386
+ return errors;
387
+ }
388
+
389
+ _timeAgo(dateStr) {
390
+ if (!dateStr) return '未知';
391
+ const now = Date.now();
392
+ const then = new Date(dateStr).getTime();
393
+ const diff = now - then;
394
+
395
+ const seconds = Math.floor(diff / 1000);
396
+ const minutes = Math.floor(seconds / 60);
397
+ const hours = Math.floor(minutes / 60);
398
+ const days = Math.floor(hours / 24);
399
+
400
+ if (days > 0) return `${days} 天前`;
401
+ if (hours > 0) return `${hours} 小时前`;
402
+ if (minutes > 0) return `${minutes} 分钟前`;
403
+ return '刚刚';
404
+ }
405
+
406
+ _getStepDefinitions(stage) {
407
+ return STEP_DEFINITIONS[stage] || [];
408
+ }
409
+
410
+ _ensureGitignore(cwd) {
411
+ const gitignorePath = join(cwd, '.gitignore');
412
+ const rule = '.sillyspec/.runtime/';
413
+
414
+ if (existsSync(gitignorePath)) {
415
+ const content = readFileSync(gitignorePath, 'utf8');
416
+ if (content.includes(rule)) return;
417
+ writeFileSync(gitignorePath, content.trimEnd() + '\n' + rule + '\n');
418
+ } else {
419
+ writeFileSync(gitignorePath, rule + '\n');
420
+ }
421
+ }
422
+ }
package/src/setup.js CHANGED
@@ -40,6 +40,22 @@ const MCP_TOOLS = [
40
40
  args: ['chrome-devtools-mcp@latest'],
41
41
  url: 'https://github.com/ChromeDevTools/chrome-devtools-mcp',
42
42
  },
43
+ {
44
+ id: 'agent-browser',
45
+ name: 'Agent Browser (Vercel)',
46
+ description: 'Rust 原生浏览器 CLI,token 消耗极低,50+ 命令覆盖导航/表单/截图/网络',
47
+ command: 'npx',
48
+ args: ['@anthropic-ai/agent-browser@latest'],
49
+ url: 'https://github.com/vercel-labs/agent-browser',
50
+ },
51
+ {
52
+ id: 'pinchtab',
53
+ name: 'PinchTab',
54
+ description: '12MB Go 二进制,零依赖,accessibility tree 极省 token,有 MCP 支持',
55
+ command: 'npx',
56
+ args: ['pinchtab-mcp@latest'],
57
+ url: 'https://github.com/pinchtab/pinchtab',
58
+ },
43
59
  ];
44
60
 
45
61
  // ── 数据库 MCP 定义(需要连接信息)──
@@ -32,6 +32,7 @@ $ARGUMENTS
32
32
  - 包含的文件列表
33
33
  - 任务完成统计(✅ 已完成 / ⬜ 未完成)
34
34
  - 一句话总结本次变更
35
+ - quicklog 修改记录(如有 `.sillyspec/changes/<change-name>/quicklog/` 目录)
35
36
 
36
37
  ### 3. Spec 沉淀
37
38
 
@@ -43,6 +44,61 @@ $ARGUMENTS
43
44
  - ① 确认归档
44
45
  - ② 取消
45
46
 
47
+ ### 4.5 生成归档摘要
48
+
49
+ 在变更目录下自动生成 `SUMMARY.md`:
50
+
51
+ ```markdown
52
+ # <变更名> 归档
53
+
54
+ - 创建:YYYY-MM-DD
55
+ - 完成:YYYY-MM-DD
56
+ - 涉及阶段:brainstorm → plan → execute → verify
57
+
58
+ ## 关键决策
59
+ - (从 design.md 提取 3-5 条核心决策)
60
+
61
+ ## 产出文件
62
+ - design.md — 设计文档
63
+ - tasks.md — 任务清单
64
+ - quicklog/ — 关联 quick 修改(N次)
65
+ - quick1: 描述
66
+ - quick2: 描述
67
+
68
+ ## 代码变更统计
69
+ - 新增 X 文件,修改 Y 文件,删除 Z 文件
70
+ - 详见 CHANGELOG.md
71
+ ```
72
+
73
+ 在变更目录下自动生成 `CHANGELOG.md`:
74
+
75
+ ```bash
76
+ # 收集该变更相关的 git commit(按变更名过滤或按时间范围)
77
+ git log --oneline --no-merges -- .sillyspec/changes/<change-name>/ 2>/dev/null
78
+ # 以及变更目录创建后的所有 commit
79
+ git log --oneline --no-merges --since="<创建时间>" -- "*.ts" "*.js" "*.vue" "*.java" 2>/dev/null
80
+ ```
81
+
82
+ 写入 CHANGELOG.md,格式:
83
+ ```markdown
84
+ # <变更名> 变更日志
85
+
86
+ ## brainstorm 阶段
87
+ - (相关 commit)
88
+
89
+ ## plan 阶段
90
+ - (相关 commit)
91
+
92
+ ## execute 阶段
93
+ - (相关 commit)
94
+
95
+ ## quick 修改
96
+ - (相关 commit)
97
+
98
+ ## verify 阶段
99
+ - (相关 commit)
100
+ ```
101
+
46
102
  ### 5. 执行归档
47
103
 
48
104
  - 目标路径:`.sillyspec/changes/archive/YYYY-MM-DD-<change-name>/`