sillyspec 3.18.4 → 3.18.5

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.
@@ -68,7 +68,7 @@ $ARGUMENTS
68
68
 
69
69
  ### 关键规则
70
70
  - 不要跳过任何步骤
71
- - 不要手动修改 progress.json
71
+ - 不要手动修改进度数据(SQLite 数据库)
72
72
  - 不要自动 commit,只 git add
73
73
  - 不要使用 npx
74
74
  - 不要编造不存在的 CLI 子命令
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sillyspec:doctor
3
- description: 用于 SillySpec 自检和状态修复。适合用户说"检查下状态、修复 progress、doctor、状态不对"。全量扫描进度一致性,修复 progress.json 与实际产出不匹配的问题。
3
+ description: 用于 SillySpec 自检和状态修复。适合用户说"检查下状态、修复 progress、doctor、状态不对"。全量扫描进度一致性,修复进度数据与实际产出不匹配的问题。
4
4
  ---
5
5
 
6
6
  ## 前置检查
@@ -23,6 +23,8 @@ description: 用于按 plan 执行代码实现。适合用户说"开始写代码
23
23
  - Worktree 路径在 Step 3(确认 worktree 路径)中输出,后续子代理的 cwd 必须设为该路径
24
24
  - **禁止跳过 worktree 或在主仓库直接写代码**
25
25
  - 如果 worktree 创建失败,CLI 会报错并退出,需要排查后再重试
26
+ - **未提交的文件、dirty 状态等不影响 worktree 创建和进入,直接按 CLI 输出的 worktree 路径操作即可**
27
+ - 不要自行检查 git 状态来判断是否可以进入 worktree,CLI 会自动处理
26
28
 
27
29
  ## 用户指令
28
30
  $ARGUMENTS
@@ -22,15 +22,15 @@ description: 恢复工作 — 从中断处继续
22
22
  sillyspec progress show
23
23
  ```
24
24
 
25
- ### 2. 如果有 progress.json
25
+ ### 2. 如果有活跃变更
26
26
 
27
- progress.json 中提取并展示当前状态,使用 `sillyspec progress show` 查看。
27
+ 从 `sillyspec progress show` 输出中提取并展示当前状态。
28
28
 
29
29
  然后问用户:
30
30
  1. 直接继续执行下一步
31
31
  2. 查看更多细节
32
32
 
33
- ### 3. 如果没有 progress.json 或变更目录
33
+ ### 3. 如果没有活跃变更
34
34
 
35
35
  自动探测项目状态:
36
36
 
@@ -64,5 +64,5 @@ cat .sillyspec/ROADMAP.md 2>/dev/null
64
64
 
65
65
  ### 4. 关键原则
66
66
 
67
- - progress.json 是唯一的恢复数据源(存储在 `.sillyspec/changes/<name>/progress.json` 或旧版 `.sillyspec/.runtime/progress.json`)
68
- - progress.json `sillyspec run <stage> --done` 自动更新,不需要手动保存
67
+ - 进度数据存储在 SQLite 数据库中(`.sillyspec/.runtime/sillyspec.db`),通过 `sillyspec progress show` 命令查看
68
+ - 进度随 `sillyspec run <stage> --done` 自动更新,不需要手动保存
@@ -1,19 +1,19 @@
1
1
  ---
2
2
  name: sillyspec:state
3
- description: 查看当前工作状态 — 显示 progress.json 内容
3
+ description: 查看当前工作状态 — 显示 SillySpec 进度
4
4
  ---
5
5
 
6
6
  你现在是 SillySpec 的状态查看器。
7
7
 
8
8
  ## 流程
9
9
 
10
- ### 1. 读取 progress.json
10
+ ### 1. 读取进度
11
11
 
12
12
  ```bash
13
- sillyspec run state --status 2>/dev/null
13
+ sillyspec progress show
14
14
  ```
15
15
 
16
- ### 2. 如果有 progress.json
16
+ ### 2. 如果有活跃变更
17
17
 
18
18
  格式化展示当前状态:
19
19
 
@@ -33,9 +33,9 @@ sillyspec run state --status 2>/dev/null
33
33
  > **阻塞项**:
34
34
  > - xxx(如无则省略)
35
35
 
36
- ### 3. 如果没有 progress.json
36
+ ### 3. 如果没有活跃变更
37
37
 
38
- 提示用户项目还没有开始,或 progress.json 尚未生成:
38
+ 提示用户项目还没有开始:
39
39
 
40
40
  > 📊 还没有工作记录。
41
41
  >
@@ -44,11 +44,11 @@ sillyspec run state --status 2>/dev/null
44
44
  > - 已有项目:`/sillyspec:scan`
45
45
  > - 恢复中断的工作:`/sillyspec:resume`
46
46
  >
47
- > progress.json 会在 `sillyspec init` 时自动创建。
47
+ > 进度数据会在 `sillyspec init` 时自动创建到 SQLite 数据库中。
48
48
 
49
49
  ### 注意
50
50
 
51
51
  - 这是只读命令,**不修改任何文件**
52
52
  - `/sillyspec:status` 查看项目整体进度(change 文件级别)
53
- - `/sillyspec:state` 查看当前工作状态(progress.json 级别)
53
+ - `/sillyspec:state` 查看当前工作状态(阶段/步骤级别)
54
54
  - 两者互补:status 看"有什么",state 看"在做什么"
@@ -43,7 +43,7 @@ created_at: 2026-06-04 16:25:42
43
43
  | `batch_progress` | 批量任务统计 |
44
44
  | `approvals` | 平台审批状态 |
45
45
 
46
- `progress.js` 通过 SQL 读写这些表,并组装成兼容旧 progress JSON JS 对象。当前没有看到 `progress.json` 被作为权威状态写入。
46
+ `progress.js` 通过 SQL 读写这些表,并组装成兼容旧 progress 格式的 JS 对象。进度数据仅存储在 SQLite 数据库中,不再使用 progress.json 文件。
47
47
 
48
48
  注意:`db.js` 的 `project.schema_version` DDL 默认值是 `4`,但 `progress.js` 的 `CURRENT_VERSION` 是 `3`,并在初始化/写入时使用 `3`。文档不要把这里写成稳定的 v4 schema 事实。
49
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.18.4",
3
+ "version": "3.18.5",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -48,12 +48,13 @@ function startProgressWatch(projectPath) {
48
48
  progressWatchers.get(projectPath).refCount++
49
49
  return
50
50
  }
51
- const progressFile = join(projectPath, '.sillyspec', '.runtime', 'progress.json')
52
- if (!existsSync(progressFile)) return
51
+ // Watch the SQLite database file for changes (replaces old progress.json watch)
52
+ const dbFile = join(projectPath, '.sillyspec', '.runtime', 'sillyspec.db')
53
+ if (!existsSync(dbFile)) return
53
54
 
54
55
  let timer = null
55
56
  try {
56
- const watcher = watch(progressFile, (eventType) => {
57
+ const watcher = watch(dbFile, (eventType) => {
57
58
  if (timer) clearTimeout(timer)
58
59
  timer = setTimeout(() => {
59
60
  timer = null
@@ -77,18 +77,13 @@ export function parseProjectOverview(projectPath) {
77
77
 
78
78
  // --- Last active ---
79
79
  const sillyspecDir = join(projectPath, '.sillyspec')
80
- const progressPath = join(sillyspecDir, '.runtime', 'progress.json')
81
- if (existsSync(progressPath)) {
80
+ // Progress is stored in SQLite (.sillyspec/.runtime/sillyspec.db), not progress.json
81
+ // Use mtime of the DB file as a fallback for lastActive
82
+ const dbPath = join(sillyspecDir, '.runtime', 'sillyspec.db')
83
+ if (existsSync(dbPath)) {
82
84
  try {
83
- const progress = JSON.parse(readFileSync(progressPath, 'utf-8'))
84
- if (progress.stages) {
85
- for (const stageData of Object.values(progress.stages)) {
86
- if (stageData.lastActive && (!result.lastActive || new Date(stageData.lastActive) > new Date(result.lastActive))) {
87
- result.lastActive = stageData.lastActive
88
- }
89
- }
90
- }
91
- if (progress.lastActive) result.lastActive = progress.lastActive
85
+ const s = statSync(dbPath)
86
+ result.lastActive = s.mtime.toISOString()
92
87
  } catch {}
93
88
  }
94
89
  if (!result.lastActive) {
@@ -432,81 +427,46 @@ export function parseSillyspecDocsTree(projectPath) {
432
427
  }
433
428
 
434
429
  /**
435
- * Parse project state from .sillyspec directory
430
+ * Parse project state from .sillyspec SQLite database via CLI.
431
+ * Progress data is stored in SQLite (.sillyspec/.runtime/sillyspec.db),
432
+ * accessed through `sillyspec progress show`.
436
433
  * @param {string} projectPath - Path to the project directory
437
- * @returns {object} Project state with currentStage, nextStep, progress, stages, specs, lastActive
434
+ * @returns {object|null} Project state with currentStage, stages, lastActive
438
435
  */
439
436
  export function parseProjectState(projectPath) {
440
437
  const sillyspecDir = join(projectPath, '.sillyspec')
438
+ const dbPath = join(sillyspecDir, '.runtime', 'sillyspec.db')
441
439
 
442
- if (!existsSync(sillyspecDir)) {
440
+ if (!existsSync(sillyspecDir) || !existsSync(dbPath)) {
443
441
  return null
444
442
  }
445
443
 
446
444
  let currentStage = ''
447
- let nextStep = null
448
- let progress = { stages: {} }
449
- let stages = []
450
- let specs = []
451
445
  let lastActive = null
452
446
 
453
- // Read progress.json for current stage
454
- const progressPath = join(sillyspecDir, '.runtime', 'progress.json')
455
- if (existsSync(progressPath)) {
456
- try {
457
- const progressData = JSON.parse(readFileSync(progressPath, 'utf-8'))
458
- progress = progressData
459
- currentStage = progressData.currentStage || ''
460
- stages = Object.keys(progressData.stages || {})
461
-
462
- // Find last active
463
- if (progressData.lastActive) lastActive = progressData.lastActive
464
- if (progressData.stages) {
465
- for (const [stageName, stageData] of Object.entries(progressData.stages)) {
466
- if (stageData.lastActive || stageData.startedAt) {
467
- const t = stageData.lastActive || stageData.startedAt
468
- if (!lastActive || new Date(t) > new Date(lastActive)) lastActive = t
469
- }
470
- }
471
- }
472
- } catch (err) {
473
- // Progress file exists but couldn't be parsed
474
- }
475
- }
447
+ // Use DB file mtime as lastActive indicator
448
+ try {
449
+ const s = statSync(dbPath)
450
+ lastActive = s.mtime.toISOString()
451
+ } catch {}
476
452
 
477
- // List all spec files
478
- const specsDir = join(sillyspecDir, 'specs')
479
- if (existsSync(specsDir)) {
480
- try {
481
- const specFiles = readdirSync(specsDir)
482
- .filter(f => f.endsWith('.md'))
483
- .sort()
484
-
485
- specs = specFiles.map(f => {
486
- const specPath = join(specsDir, f)
487
- try {
488
- const content = readFileSync(specPath, 'utf-8')
489
- const titleMatch = content.match(/^#\s+(.+)$/m)
490
- return {
491
- name: f,
492
- title: titleMatch ? titleMatch[1] : f,
493
- path: specPath
494
- }
495
- } catch {
496
- return { name: f, title: f, path: specPath }
497
- }
498
- })
499
- } catch (err) {
500
- // Specs directory couldn't be read
501
- }
453
+ // Use CLI to read current stage from SQLite
454
+ try {
455
+ const output = execSync('sillyspec progress show 2>/dev/null', {
456
+ cwd: projectPath, encoding: 'utf-8', timeout: 5000
457
+ })
458
+ const stageMatch = output.match(/当前阶段:\s*(\S+)/)
459
+ if (stageMatch) currentStage = stageMatch[1]
460
+ } catch {
461
+ // CLI unavailable or no active change
502
462
  }
503
463
 
504
464
  return {
505
465
  currentStage,
506
- nextStep,
507
- progress,
508
- stages,
509
- specs,
466
+ nextStep: null,
467
+ progress: { stages: {} },
468
+ stages: [],
469
+ specs: [],
510
470
  lastActive
511
471
  }
512
472
  }
package/src/index.js CHANGED
@@ -145,6 +145,21 @@ async function main() {
145
145
  targetDir = resolve(filteredArgs[1]);
146
146
  filteredArgs.splice(1, 1);
147
147
  }
148
+ // ── 自动纠正 cwd ──
149
+ // 当 agent 在 worktree 内跑 pnpm 等工具后 shell cwd 可能被改变,
150
+ // 导致 sillyspec 命令找不到 .sillyspec。此函数尝试从 git root 解析。
151
+ function resolveEffectiveDir(baseDir) {
152
+ if (existsSync(join(baseDir, '.sillyspec'))) return baseDir
153
+ try {
154
+ const { execSync } = require('child_process')
155
+ const gitRoot = execSync('git rev-parse --show-toplevel', {
156
+ cwd: baseDir, encoding: 'utf8', timeout: 5000
157
+ }).trim()
158
+ if (gitRoot && existsSync(join(gitRoot, '.sillyspec'))) return gitRoot
159
+ } catch {}
160
+ return baseDir
161
+ }
162
+
148
163
  const dir = targetDir;
149
164
 
150
165
  if (command === 'init' && !existsSync(dir)) {
@@ -167,6 +182,7 @@ async function main() {
167
182
  break;
168
183
  case 'progress': {
169
184
  const pm = new ProgressManager();
185
+ const progDir = resolveEffectiveDir(dir);
170
186
  const subCommand = filteredArgs[1];
171
187
  const stageIdx = filteredArgs.indexOf('--stage');
172
188
  const stage = stageIdx >= 0 && filteredArgs[stageIdx + 1] ? filteredArgs[stageIdx + 1] : null;
@@ -176,18 +192,18 @@ async function main() {
176
192
 
177
193
  switch (subCommand) {
178
194
  case 'init':
179
- pm.init(dir);
195
+ pm.init(progDir);
180
196
  break;
181
197
  case 'status':
182
198
  case 'show':
183
- pm.show(dir, progChangeName);
199
+ pm.show(progDir, progChangeName);
184
200
  break;
185
201
  case 'check':
186
- await pm.checkConsistency(dir, progChangeName);
202
+ await pm.checkConsistency(progDir, progChangeName);
187
203
  break;
188
204
  case 'repair': {
189
205
  const repairApply = filteredArgs.includes('--apply');
190
- await pm.repairConsistency(dir, { apply: repairApply, changeName: progChangeName });
206
+ await pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
191
207
  break;
192
208
  }
193
209
  case 'validate':
@@ -270,7 +286,7 @@ async function main() {
270
286
  }
271
287
  case 'run': {
272
288
  const { runCommand } = await import('./run.js')
273
- await runCommand(filteredArgs.slice(1), dir, specDir)
289
+ await runCommand(filteredArgs.slice(1), resolveEffectiveDir(dir), specDir)
274
290
  break
275
291
  }
276
292
  case 'dashboard': {
@@ -165,6 +165,7 @@ const fixedPrefix = [
165
165
  - **worktree 已由 CLI 在 execute 阶段启动时自动创建,不要自行创建或跳过**
166
166
  - **后续所有子代理的 cwd 必须设为该 worktree 路径**
167
167
  - 如果 meta.json 不存在(说明创建失败),停止并报错
168
+ - **不要自行检查 git dirty/uncommitted 状态来判断是否可以进入 worktree,CLI 已自动处理**
168
169
 
169
170
  ### 输出
170
171
  worktree 路径 + 分支名 + 模式
@@ -214,7 +214,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
214
214
  const patchFiles = hasAllowList
215
215
  ? [...allowSet].filter(f => changedFiles.includes(f))
216
216
  : changedFiles;
217
- const fileArgs = patchFiles.map(f => `-- ${f}`).join(' ');
217
+ const fileArgs = patchFiles.length > 0 ? `-- ${patchFiles.join(' ')}` : '';
218
218
 
219
219
  // 创建临时文件
220
220
  const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-patch-'));
@@ -236,7 +236,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
236
236
 
237
237
  // tracked 文件:git diff baseHash
238
238
  if (trackedFiles.length > 0) {
239
- const trackedArgs = trackedFiles.map(f => `-- ${f}`).join(' ');
239
+ const trackedArgs = trackedFiles.length > 0 ? `-- ${trackedFiles.join(' ')}` : '';
240
240
  patchContent += execSync(
241
241
  `git diff --binary ${diffBase} ${trackedArgs}`,
242
242
  { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
@@ -245,11 +245,12 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
245
245
 
246
246
  // untracked 新文件:git add 到 index,git diff --cached,然后 reset
247
247
  if (untrackedPatchFiles.length > 0) {
248
- const addArgs = untrackedPatchFiles.map(f => `-- ${f}`).join(' ');
248
+ const addArgs = untrackedPatchFiles.length > 0 ? `-- ${untrackedPatchFiles.join(' ')}` : '';
249
249
  git(worktreePath, `add ${addArgs}`);
250
250
  try {
251
+ const diffCachedArgs = untrackedPatchFiles.length > 0 ? `-- ${untrackedPatchFiles.join(' ')}` : '';
251
252
  patchContent += execSync(
252
- `git diff --binary --cached ${untrackedPatchFiles.map(f => `-- ${f}`).join(' ')}`,
253
+ `git diff --binary --cached ${diffCachedArgs}`,
253
254
  { cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
254
255
  );
255
256
  } finally {