sillyspec 3.20.4 → 3.20.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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  author: qinyi
3
3
  created_at: 2026-05-31 11:00:00
4
- updated_at: 2026-06-19 15:00:00
4
+ updated_at: 2026-07-01 15:00:00
5
5
  ---
6
6
 
7
7
  # SillySpec 文件生命周期
@@ -73,6 +73,9 @@ updated_at: 2026-06-19 15:00:00
73
73
 
74
74
  `init.js` 会把 `.sillyspec/.runtime/`、`.sillyspec/local.yaml`、`.sillyspec/codebase/SCAN-RAW.md` 追加到 `.gitignore`。
75
75
 
76
+ > **平台模式残留清理边界**(`init.js` `cleanupRuntimeResidue`,由 `run.js` 启动时复用):
77
+ > 当 `specRoot` 指向外部、源码目录的 `.sillyspec/` 含真实资产(`changes/`/`projects/`/`sillyspec.db`)时,只清理运行时残留,**不整删 `.runtime/`**。清理白名单保留权威状态:`worktrees/`、`sillyspec.db`、`global.json`、`gate-status.json`、`contract-artifacts/`、`execute-runs/`;其余子项(`artifacts/`、`scan-runs/`、`scan-projects.json`、`user-inputs.md`、`postcheck-result.json` 等可重建缓存)逐项删除,`local.yaml`、`codebase/` 整删。未知子项默认保留(安全侧倾斜)。
78
+
76
79
  ## 主要文件流
77
80
 
78
81
  ```text
@@ -122,4 +125,7 @@ quick
122
125
  - `archive` 的目录移动已经由 `run.js` 在第 4 步 `--confirm` 时执行;未带 `--confirm` 会回退该步骤并提示补参。
123
126
  - scan 第 10 步「Extract Project Knowledge」把长期有效的项目知识写入 `.sillyspec/knowledge/`(`conventions.md`/`patterns.md`/`known-issues.md` + 更新 `INDEX.md`);`scan-postcheck.js` 校验产物(INDEX.md 存在、引用文件真实存在)。
124
127
  - execute 启动时由 `knowledge-match.js` 按 plan.md 的 task 关键词匹配知识库,命中报告注入 prompt 并写 `.runtime/knowledge-hit-report.json`。
128
+ - 平台模式残留清理只删缓存、保留权威状态(`worktrees/`、`sillyspec.db`、`global.json`、`gate-status.json`、`contract-artifacts/`、`execute-runs/`),不再整删 `.runtime/`——否则 worktree meta 被清掉会导致 `depsStatus` 恒为 unknown、`branch already exists` 死循环、`worktree doctor` orphan 误判。
129
+ - plan→execute Contract 校验(`parseWavesFromPlan`)只解析 `## Wave N` 段内的 `- [ ] task-XX:` 行;遇到非 Wave 标题行(`## 自检` 等)即退出当前 Wave 段,避免自检 `- [x]` checkbox 被误当 task 定义。
130
+ - `executePlanPostcheck` 的 `resolveChangeDir` 复用 `run.js` 模块内本地函数,不从 `./modules.js` 导入(该模块未导出此函数)。
125
131
  - Revision v1:`stages` 表新增 `revision`/`reopened_from_step`/`reopened_at`/`stale_reason` 列;阶段新增 `revising`/`stale` 状态;`sillyspec run <stage> --reopen --from-step <n>` 重开已完成阶段、级联标记下游 stale;`.runtime/postcheck-result.json` 由 `scan-postcheck.js` 的 `writeStructuredResult` 落盘(本地写 `specDir/.runtime`,平台写 `runtimeRoot/scan-runs/<id>`)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.20.4",
3
+ "version": "3.20.5",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/init.js CHANGED
@@ -8,6 +8,48 @@ import chalk from 'chalk';
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = dirname(__filename);
10
10
 
11
+ // ── .runtime/ 残留清理(平台模式防源码污染) ──
12
+ // 平台模式(specRoot 指向外部)下,源码目录的 .sillyspec/.runtime/ 是旧残留,
13
+ // 应让 specRoot 成为唯一运行时根。但若源码目录的 .sillyspec/ 本身就是该项目
14
+ // 的 specDir(含真实资产),其 .runtime/ 里的 worktrees/、sillyspec.db、
15
+ // global.json、gate-status.json 是真实工作状态,整删会摧毁 worktree meta、
16
+ // 导致 depsStatus 恒为 unknown、branch already exists 死循环。
17
+ // 详见 docs/sillyspec/runtime-cleanup-destroys-worktree-meta.md
18
+ //
19
+ // 策略:白名单保留权威状态,逐项删除可重建的缓存子项;未知子项默认保留(安全侧倾斜)。
20
+ const RUNTIME_KEEP = new Set([
21
+ 'worktrees', // worktree 目录 + meta.json(worktree.js:17)
22
+ 'sillyspec.db', // SQLite 进度库(权威状态源,progress.js:7)
23
+ 'global.json', // 项目名/活跃变更缓存(progress.js:8)
24
+ 'gate-status.json', // worktree-guard 门禁状态(progress.js:9)
25
+ 'contract-artifacts', // execute endpoint 契约(verify 阶段读取)
26
+ 'execute-runs', // task review 结果(task-review.js)
27
+ ]);
28
+
29
+ /**
30
+ * 清理 .sillyspec/ 下的运行时残留,保留权威状态。
31
+ * 同时清理 local.yaml、codebase/(这些非权威,整删安全)。
32
+ * @param {string} legacyDir - 源码目录的 .sillyspec/ 路径
33
+ */
34
+ export function cleanupRuntimeResidue(legacyDir) {
35
+ // local.yaml、codebase/ 非权威,整删
36
+ for (const residue of ['local.yaml', 'codebase']) {
37
+ const p = join(legacyDir, residue);
38
+ if (existsSync(p)) { try { rmSync(p, { recursive: true, force: true }) } catch {} }
39
+ }
40
+ // .runtime/ 逐项清理,白名单保留
41
+ const runtimeDir = join(legacyDir, '.runtime');
42
+ if (existsSync(runtimeDir)) {
43
+ let entries = [];
44
+ try { entries = readdirSync(runtimeDir, { withFileTypes: true }); } catch {}
45
+ for (const entry of entries) {
46
+ if (RUNTIME_KEEP.has(entry.name)) continue;
47
+ const p = join(runtimeDir, entry.name);
48
+ try { rmSync(p, { recursive: true, force: true }) } catch {}
49
+ }
50
+ }
51
+ }
52
+
11
53
  // ── 递归复制目录 ──
12
54
  function copyDirSync(src, dst) {
13
55
  mkdirSync(dst, { recursive: true });
@@ -130,11 +172,8 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
130
172
  // 真实资产存在:拒绝整体删除,仅清理运行时残留
131
173
  console.error('❌ [sillyspec] 拒绝删除源码目录的 .sillyspec/:检测到真实资产(changes/、projects/ 或 sillyspec.db)。');
132
174
  console.error(' 该项目似乎本身就用 SillySpec 管理。如需改用外部 spec 目录,请先手动迁移/备份。');
133
- console.error(' 本次仅清理运行时残留(.runtime/、local.yaml、codebase/)。');
134
- for (const residue of ['.runtime', 'local.yaml', 'codebase']) {
135
- const p = join(legacyDir, residue);
136
- if (existsSync(p)) { try { rmSync(p, { recursive: true, force: true }) } catch {} }
137
- }
175
+ console.error(' 本次仅清理运行时残留(.runtime/ 缓存、local.yaml、codebase/),保留 worktrees 与进度状态。');
176
+ cleanupRuntimeResidue(legacyDir);
138
177
  } else {
139
178
  // 无真实资产:确属旧版本残留,安全删除
140
179
  try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
package/src/run.js CHANGED
@@ -1131,8 +1131,10 @@ async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
1131
1131
  * Plan postcheck 的执行代理:委托给 plan-postcheck.js 模块
1132
1132
  */
1133
1133
  async function executePlanPostcheck(cwd, platformOpts, progress) {
1134
+ // resolveChangeDir 是本模块 line 505 定义的本地函数;历史上曾误从 ./modules.js
1135
+ // 导入(该模块未导出此函数,得到 undefined),导致 plan-postcheck.js:388 抛
1136
+ // "resolveChangeDir is not a function"。详见 docs/sillyspec/plan-postcheck-resolvechangedir-not-a-function.md
1134
1137
  const { executePlanPostcheck: runPostcheck } = await import('./stages/plan-postcheck.js')
1135
- const { resolveChangeDir } = await import('./modules.js')
1136
1138
  await runPostcheck({
1137
1139
  cwd,
1138
1140
  specRoot: platformOpts?.specRoot,
@@ -1292,11 +1294,9 @@ export async function runCommand(args, cwd, specDir = null) {
1292
1294
  const hasDb = existsSync(join(legacyDir, 'sillyspec.db'));
1293
1295
 
1294
1296
  if (hasChanges || hasProjects || hasDb) {
1295
- console.error('❌ [sillyspec] 拒绝删除源码目录的 .sillyspec/:检测到真实资产。仅清理运行时残留。');
1296
- for (const residue of ['.runtime', 'local.yaml', 'codebase']) {
1297
- const p = join(legacyDir, residue);
1298
- if (existsSync(p)) { try { rmSync(p, { recursive: true, force: true }) } catch {} }
1299
- }
1297
+ console.error('❌ [sillyspec] 拒绝删除源码目录的 .sillyspec/:检测到真实资产。仅清理运行时残留,保留 worktrees 与进度状态。');
1298
+ const { cleanupRuntimeResidue } = await import('./init.js')
1299
+ cleanupRuntimeResidue(legacyDir);
1300
1300
  } else {
1301
1301
  try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
1302
1302
  }
@@ -370,6 +370,15 @@ function parseWavesFromPlan(planContent) {
370
370
  continue
371
371
  }
372
372
 
373
+ // 遇到任何非 Wave 的标题行(## 自检、## 备注 等)时退出当前 Wave 段,
374
+ // 避免「## 自检」段里的 - [x] checkbox 被误当 task 定义解析(导致
375
+ // Contract 校验报 task id 重复/不连续)。详见 docs/sillyspec/plan-postcheck-self-check-checkbox-false-dup.md
376
+ if (/^#+\s/.test(line)) {
377
+ currentWave = null
378
+ currentTask = null
379
+ continue
380
+ }
381
+
373
382
  if (!currentWave) continue
374
383
 
375
384
  const taskMatch = line.match(/^[-*]\s*\[[ x]\]\s*(.+)/)
@@ -470,6 +470,13 @@ export async function executePlanPostcheck(context) {
470
470
  currentWaveTasks = []
471
471
  continue
472
472
  }
473
+ // 非 Wave 标题行(## 自检 等)退出当前 Wave 段,避免自检 checkbox
474
+ // 被混入 task 列表导致 Wave 结构比对误判。
475
+ if (/^#+\s/.test(line)) {
476
+ if (currentWaveTasks) existingWaves.push(currentWaveTasks)
477
+ currentWaveTasks = null
478
+ continue
479
+ }
473
480
  const tm = line.match(/^[-*]\s*\[[ x]\]\s*task-(\d+)/i)
474
481
  if (tm && currentWaveTasks) {
475
482
  currentWaveTasks.push(`task-${tm[1]}`)
@@ -318,9 +318,36 @@ console.log('\n--- Plan Postcheck: task id 不连续失败 ---')
318
318
  assert(!result.ok, 'id 不连续应不通过 postcheck')
319
319
  }
320
320
 
321
+ // ─────────────────────────────────────────
322
+ // Bug C 回归: 「## 自检」段的 - [x] checkbox 不应被解析为 task
323
+ // 详见 docs/sillyspec/plan-postcheck-self-check-checkbox-false-dup.md
324
+ // ─────────────────────────────────────────
325
+ console.log('\n--- Bug C 回归: 自检段 checkbox 不误解析 ---')
326
+ {
327
+ const plan = `# Plan
328
+
329
+ ## Wave 1
330
+ - [ ] task-01: 建立 schema
331
+ - [ ] task-02: 接口实现
332
+
333
+ ## Wave 2
334
+ - [ ] task-03: 前端对接
335
+ - [ ] task-04: 集成测试
336
+
337
+ ## 自检
338
+ - [x] 每个 task 有编号(task-01~04),总数 4(≤15)
339
+ - [x] 无泛泛风险(转为具体验收条目与 task-04/task-06 等)
340
+ `
341
+ const result = validatePlanForExecute(plan)
342
+ assert(result.ok, `自检段含 - [x] 不应误报,errors: ${result.errors.join('; ')}`)
343
+ assert(result.tasks.length === 4, `应只解析 4 个 task,实际 ${result.tasks.length}(自检 checkbox 被误纳入)`)
344
+ const ids = result.tasks.map(t => t.index).sort((a, b) => a - b)
345
+ assert(JSON.stringify(ids) === JSON.stringify([1, 2, 3, 4]), `task id 应为 1-4,实际 ${ids}`)
346
+ }
347
+
321
348
  // ── 结果 ──
322
349
  console.log(`\n${'='.repeat(50)}`)
323
- console.log(`✅ 通过: ${12 - failed} ❌ 失败: ${failed}`)
350
+ console.log(`✅ 通过: ${13 - failed} ❌ 失败: ${failed}`)
324
351
  if (failures.length > 0) {
325
352
  console.log(`失败项:`)
326
353
  failures.forEach(f => console.log(` - ${f}`))
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Bug A 回归:清理逻辑不得摧毁 worktree meta / 进度状态
3
+ *
4
+ * 平台模式(specRoot 指向外部)下,源码目录 .sillyspec/ 含真实资产时,
5
+ * cleanupRuntimeResidue 应只清缓存,保留 worktrees/、sillyspec.db、
6
+ * global.json、gate-status.json、contract-artifacts/、execute-runs/。
7
+ * 详见 docs/sillyspec/runtime-cleanup-destroys-worktree-meta.md
8
+ */
9
+ import { cleanupRuntimeResidue } from '../src/init.js'
10
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'
11
+ import { join } from 'path'
12
+ import os from 'os'
13
+
14
+ let failed = 0
15
+ const failures = []
16
+ function assert(condition, msg) {
17
+ if (!condition) { failed++; failures.push(msg); console.log(` ❌ FAIL: ${msg}`) }
18
+ else console.log(` ✅ PASS: ${msg}`)
19
+ }
20
+
21
+ function makeTempLegacy() {
22
+ const root = join(os.tmpdir(), `sillyspec-bugA-${Date.now()}-${Math.random().toString(36).slice(2)}`)
23
+ const legacyDir = join(root, '.sillyspec')
24
+ mkdirSync(join(legacyDir, '.runtime'), { recursive: true })
25
+ return { root, legacyDir }
26
+ }
27
+
28
+ console.log('=== Bug A 回归: cleanupRuntimeResidue 保留权威状态 ===\n')
29
+
30
+ // ── Case 1: 权威状态全部保留 ──
31
+ console.log('--- Case 1: worktrees / db / global.json / gate-status 保留 ---')
32
+ {
33
+ const { root, legacyDir } = makeTempLegacy()
34
+ const runtime = join(legacyDir, '.runtime')
35
+ // 权威状态
36
+ mkdirSync(join(runtime, 'worktrees', 'my-change'), { recursive: true })
37
+ writeFileSync(join(runtime, 'worktrees', 'my-change', 'meta.json'), '{"depsStatus":"installed"}')
38
+ writeFileSync(join(runtime, 'sillyspec.db'), 'sqlite-bytes')
39
+ writeFileSync(join(runtime, 'global.json'), '{"projectName":"x"}')
40
+ writeFileSync(join(runtime, 'gate-status.json'), '{}')
41
+ mkdirSync(join(runtime, 'contract-artifacts'), { recursive: true })
42
+ writeFileSync(join(runtime, 'contract-artifacts', 'foo.json'), '{}')
43
+ mkdirSync(join(runtime, 'execute-runs'), { recursive: true })
44
+ // 可删缓存
45
+ mkdirSync(join(runtime, 'artifacts'), { recursive: true })
46
+ mkdirSync(join(runtime, 'scan-runs'), { recursive: true })
47
+ writeFileSync(join(runtime, 'scan-projects.json'), '{}')
48
+ writeFileSync(join(runtime, 'user-inputs.md'), 'x')
49
+ writeFileSync(join(runtime, 'postcheck-result.json'), '{}')
50
+ // local.yaml / codebase(非权威,应删)
51
+ writeFileSync(join(legacyDir, 'local.yaml'), 'x')
52
+ mkdirSync(join(legacyDir, 'codebase'), { recursive: true })
53
+
54
+ cleanupRuntimeResidue(legacyDir)
55
+
56
+ assert(existsSync(join(runtime, 'worktrees', 'my-change', 'meta.json')), 'worktrees/meta.json 应保留')
57
+ assert(existsSync(join(runtime, 'sillyspec.db')), 'sillyspec.db 应保留')
58
+ assert(existsSync(join(runtime, 'global.json')), 'global.json 应保留')
59
+ assert(existsSync(join(runtime, 'gate-status.json')), 'gate-status.json 应保留')
60
+ assert(existsSync(join(runtime, 'contract-artifacts', 'foo.json')), 'contract-artifacts/ 应保留')
61
+ assert(existsSync(join(runtime, 'execute-runs')), 'execute-runs/ 应保留')
62
+ assert(!existsSync(join(runtime, 'artifacts')), 'artifacts/ 缓存应删除')
63
+ assert(!existsSync(join(runtime, 'scan-runs')), 'scan-runs/ 缓存应删除')
64
+ assert(!existsSync(join(runtime, 'scan-projects.json')), 'scan-projects.json 缓存应删除')
65
+ assert(!existsSync(join(runtime, 'user-inputs.md')), 'user-inputs.md 缓存应删除')
66
+ assert(!existsSync(join(legacyDir, 'local.yaml')), 'local.yaml 应删除')
67
+ assert(!existsSync(join(legacyDir, 'codebase')), 'codebase/ 应删除')
68
+
69
+ rmSync(root, { recursive: true, force: true })
70
+ }
71
+
72
+ // ── Case 2: worktree meta 内容不被破坏(depsStatus 不丢) ──
73
+ console.log('\n--- Case 2: 清理后 worktree meta 内容完整 ---')
74
+ {
75
+ const { root, legacyDir } = makeTempLegacy()
76
+ const runtime = join(legacyDir, '.runtime')
77
+ mkdirSync(join(runtime, 'worktrees', 'c1'), { recursive: true })
78
+ const metaPath = join(runtime, 'worktrees', 'c1', 'meta.json')
79
+ const original = '{"depsStatus":"installed","worktreePath":"/x","branch":"sillyspec/c1"}'
80
+ writeFileSync(metaPath, original)
81
+
82
+ cleanupRuntimeResidue(legacyDir)
83
+
84
+ assert(existsSync(metaPath), 'meta.json 应仍存在')
85
+ const read = readFileSync(metaPath, 'utf8')
86
+ assert(read === original, 'meta.json 内容应未被改写')
87
+
88
+ rmSync(root, { recursive: true, force: true })
89
+ }
90
+
91
+ // ── Case 3: .runtime/ 不存在时不报错 ──
92
+ console.log('\n--- Case 3: 无 .runtime/ 时安全无副作用 ---')
93
+ {
94
+ const { root, legacyDir } = makeTempLegacy()
95
+ rmSync(join(legacyDir, '.runtime'), { recursive: true, force: true })
96
+ let threw = false
97
+ try { cleanupRuntimeResidue(legacyDir) } catch { threw = true }
98
+ assert(!threw, '无 .runtime/ 时不应抛错')
99
+ rmSync(root, { recursive: true, force: true })
100
+ }
101
+
102
+ // ── 结果 ──
103
+ console.log(`\n${'='.repeat(50)}`)
104
+ console.log(`✅ 通过: ${28 - failed} ❌ 失败: ${failed}`)
105
+ if (failures.length > 0) { console.log('失败项:'); failures.forEach(f => console.log(` - ${f}`)) }
106
+ console.log('='.repeat(50))
107
+ if (failed > 0) process.exit(1)