sillyspec 3.24.4 → 3.24.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.
@@ -11,6 +11,9 @@
11
11
 
12
12
  import { existsSync, readFileSync, readdirSync } from 'fs'
13
13
  import path from 'path'
14
+ import { execFileSync } from 'child_process'
15
+ import { createRequire } from 'module'
16
+ import { pathToFileURL } from 'url'
14
17
 
15
18
  // ── 常量 ──
16
19
 
@@ -236,6 +239,35 @@ function readGateStatus(cwd) {
236
239
  }
237
240
  }
238
241
 
242
+ /**
243
+ * 同步只读查询 sillyspec.db 第一行第一列(不依赖外部 sqlite3 CLI)。
244
+ * sql.js 的 init 是 async,故用子进程跑(execFileSync 等待);sql.js 模块经 hook 进程的
245
+ * require.resolve 解析出绝对路径传入,避免子进程 cwd(项目目录)找不到 sql.js。
246
+ * 全平台一致(node 是 SillySpec 自身运行时)。返回字符串值或 null。
247
+ */
248
+ function queryDbFirstCell(cwd, sql) {
249
+ const dbPath = path.join(cwd, '.sillyspec', '.runtime', 'sillyspec.db')
250
+ if (!existsSync(dbPath)) return null
251
+ let sqlJsPath
252
+ try { sqlJsPath = createRequire(import.meta.url).resolve('sql.js') } catch { return null }
253
+ // Windows 上 dynamic import 不接受裸 "C:\..." 路径,必须转 file:// URL(全平台合法)
254
+ const libUrl = pathToFileURL(sqlJsPath).href
255
+ const script =
256
+ "const fs=require('fs');" +
257
+ "const dbPath=" + JSON.stringify(dbPath) + ",sql=" + JSON.stringify(sql) + ",lib=" + JSON.stringify(libUrl) + ";" +
258
+ "if(!fs.existsSync(dbPath))process.exit(0);" +
259
+ "import(lib).then(async m=>{const SQL=await m.default();" +
260
+ "const db=new SQL.Database(fs.readFileSync(dbPath));const r=db.exec(sql);" +
261
+ "if(r.length&&r[0].values.length)process.stdout.write(String(r[0].values[0][0]??''));db.close()}).catch(()=>{});"
262
+ try {
263
+ return execFileSync(process.execPath, ['-e', script], {
264
+ encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'],
265
+ }).trim() || null
266
+ } catch { return null }
267
+ }
268
+
269
+ export { queryDbFirstCell as _queryDbFirstCellForTest };
270
+
239
271
  /**
240
272
  * 从 sillyspec.db 读取 currentStage
241
273
  * 优先级:gate-status.json > sillyspec.db
@@ -247,19 +279,9 @@ function readCurrentStage(cwd) {
247
279
  const gateStatus = readGateStatus(cwd)
248
280
  if (gateStatus && gateStatus.stage) return gateStatus.stage
249
281
 
250
- // 2. 从 sillyspec.db 读取(通过 sqlite3 CLI 同步调用)
251
- const dbPath = path.join(cwd, '.sillyspec', '.runtime', 'sillyspec.db')
252
- if (!existsSync(dbPath)) return null
253
- try {
254
- const { execSync } = require('child_process')
255
- const result = execSync(
256
- `sqlite3 "${dbPath}" "SELECT current_stage FROM changes WHERE status='active' AND current_stage IN ('execute','quick') ORDER BY last_active DESC LIMIT 1"`,
257
- { encoding: 'utf8', timeout: 2000 }
258
- ).trim()
259
- return result || null
260
- } catch { /* sqlite3 CLI 不可用或查询失败 */ }
261
-
262
- return null
282
+ // 2. 从 sillyspec.db 读取(node + sql.js 子进程,不依赖外部 sqlite3 CLI——Windows 默认没有)
283
+ const v = queryDbFirstCell(cwd, "SELECT current_stage FROM changes WHERE status='active' AND current_stage IN ('execute','quick') ORDER BY last_active DESC LIMIT 1")
284
+ return v || null
263
285
  }
264
286
 
265
287
  /**
@@ -272,19 +294,8 @@ function isNoWorktreeMode(cwd) {
272
294
  const gateStatus = readGateStatus(cwd)
273
295
  if (gateStatus && gateStatus.noWorktree) return true
274
296
 
275
- // 2. 从 sillyspec.db 读取
276
- const dbPath = path.join(cwd, '.sillyspec', '.runtime', 'sillyspec.db')
277
- if (!existsSync(dbPath)) return false
278
- try {
279
- const { execSync } = require('child_process')
280
- const result = execSync(
281
- `sqlite3 "${dbPath}" "SELECT no_worktree FROM changes WHERE status='active' AND current_stage IN ('execute','quick') LIMIT 1"`,
282
- { encoding: 'utf8', timeout: 2000 }
283
- ).trim()
284
- return result === '1'
285
- } catch { /* sqlite3 CLI 不可用或查询失败 */ }
286
-
287
- return false
297
+ // 2. 从 sillyspec.db 读取(node + sql.js,同 readCurrentStage)
298
+ return queryDbFirstCell(cwd, "SELECT no_worktree FROM changes WHERE status='active' AND current_stage IN ('execute','quick') LIMIT 1") === '1'
288
299
  }
289
300
 
290
301
  /**
package/src/run.js CHANGED
@@ -632,11 +632,26 @@ async function getStageSteps(stageName, cwd, progress, specDir = null) {
632
632
  if (stageName === 'execute') {
633
633
  const changeDir = resolveChangeDir(cwd, progress, specDir)
634
634
  let planFile = null
635
+ let worktreePath = null
635
636
  if (changeDir) {
636
637
  const p = join(changeDir, 'plan.md')
637
638
  if (existsSync(p)) planFile = p
639
+ // 自动检测 worktree 路径,注入 Wave prompt 的 workdir 指令
640
+ // 修复:之前未传 worktreePath 给 buildExecuteSteps,导致 Wave prompt 缺失工作目录指令,
641
+ // 子代理可能把文件写到主工作区而非 worktree 内,破坏隔离。
642
+ try {
643
+ const changeName = basename(changeDir)
644
+ const { WorktreeManager } = await import('./worktree.js')
645
+ const wm = new WorktreeManager({ cwd })
646
+ const meta = wm.getMeta(changeName)
647
+ if (meta?.worktreePath && existsSync(meta.worktreePath)) {
648
+ worktreePath = meta.worktreePath
649
+ }
650
+ } catch {
651
+ // 无 worktree meta 不阻断——可能是首次启动或 in-place 模式
652
+ }
638
653
  }
639
- return buildExecuteSteps(planFile)
654
+ return buildExecuteSteps(planFile, { worktreePath })
640
655
  }
641
656
  if (stageName === 'plan') {
642
657
  const changeDir = resolveChangeDir(cwd, progress, specDir)
@@ -879,7 +894,12 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
879
894
  if (platformOpts.workspaceId) {
880
895
  platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
881
896
  }
882
- promptText = platformDirectives.join('\n') + '\n\n' + promptText
897
+ // 平台 directives 仅 step0 前置(对齐 persona 仅 step0 策略):路径列表/Write 规则/
898
+ // 占位符映射一次建立即可;后续 step 靠 agent 跨步记忆 + 每步 changeDir header +
899
+ // footer 平台规则(下方 L1077+)维持写入约束,避免每步重复注入 ~500 tokens。
900
+ if (stepIndex === 0) {
901
+ promptText = platformDirectives.join('\n') + '\n\n' + promptText
902
+ }
883
903
  } else {
884
904
  // 常规模式(无平台 specRoot):占位符替换为 cwd/.sillyspec 下对应路径
885
905
  // 让用 {SPEC_ROOT}/{DOCS_ROOT} 等占位符的 prompt(如 quick/scan)在常规模式也写到正确位置
@@ -1048,7 +1068,7 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
1048
1068
 
1049
1069
  console.log(promptText)
1050
1070
  console.log(`\n### ⚠️ 铁律`)
1051
- console.log('- **文档是核心资产,代码是文档的产物。** 没有文档就没有代码——文档是 AI 的记忆,是团队协作的基础,是后续维护的唯一依据。任何代码产出必须先有对应的设计/规范文档支撑。')
1071
+ console.log('- 文档优先:代码产出必须先有对应的设计/规范文档支撑。')
1052
1072
  console.log('- 只做本步骤描述的操作,不得自行扩展或跳过')
1053
1073
  console.log('- 不要回头修改已完成的步骤')
1054
1074
  console.log('- 不要编造不存在的 CLI 子命令')
@@ -1332,7 +1352,10 @@ export async function runCommand(args, cwd, specDir = null) {
1332
1352
  // ── cwd 纠正:向上查找真实项目根 ──
1333
1353
  // 防止多 project 工作区中 cwd 停在子目录(如 backend/)时
1334
1354
  // 状态写入子目录下误建的 .sillyspec,导致状态分裂
1335
- if (!specDir) {
1355
+ if (!specDir && !existsSync(join(cwd, '.sillyspec-platform.json'))) {
1356
+ // 平台模式(cwd 有 .sillyspec-platform.json 指针)不做 cwd 纠正:指针已明确 specDir,
1357
+ // 向上找 .sillyspec 反而会撞到无关项目(如用户 home 的 .sillyspec),导致 --done 恢复
1358
+ // 读写 ~/.sillyspec-platform.json 出错的 specRoot。
1336
1359
  const resolvedRoot = resolveSpecDir(cwd)
1337
1360
  if (resolvedRoot && resolvedRoot !== join(cwd, '.sillyspec')) {
1338
1361
  const realRoot = dirname(resolvedRoot)
@@ -2106,18 +2129,22 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
2106
2129
  }
2107
2130
 
2108
2131
  if (currentIdx === -1) {
2109
- // 已完成 → 自动重置,重新开始
2110
- const freshSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
2111
- stageData.steps = freshSteps
2112
- ? freshSteps.map(s => ({ name: s.name, status: 'pending' }))
2113
- : []
2114
- stageData.status = 'in-progress'
2115
- stageData.startedAt = new Date().toLocaleString('zh-CN', { hour12: false })
2116
- stageData.completedAt = null
2117
- await pm._write(cwd, progress, changeName)
2118
- triggerSync(cwd, changeName, platformOpts)
2119
- currentIdx = 0
2120
- console.log(`🔄 ${stageName} 阶段已自动重置,重新开始。\n`)
2132
+ // 所有步骤已 completed/skipped,但阶段未盖到 completed —— 这是上次最后一步完成后、
2133
+ // stage 升级事务未提交的崩溃中间态(正常完成会被上方 stageStatus==='completed' 守卫拦下)。
2134
+ // 旧逻辑无条件清空 steps 为 pending 会丢掉已完成的进度且不可恢复。改为走正式
2135
+ // completeStage:产物齐则补盖完成戳,不齐则给出 actionable 提示,永不静默清空步骤。
2136
+ console.log(`\nℹ️ ${stageName} 所有步骤已完成,但阶段未标记完成(上次可能中断)。尝试补盖完成戳…`)
2137
+ await pm.completeStage(cwd, stageName, changeName)
2138
+ const after = await pm.read(cwd, changeName)
2139
+ if (after?.stages?.[stageName]?.status === 'completed') {
2140
+ console.log(` ✅ 已补盖完成戳。下一步: sillyspec run <下一阶段>,或 sillyspec run ${stageName} --status 查看。`)
2141
+ process.exit(0)
2142
+ }
2143
+ // completeStage 已打印产物校验失败的明细;这里只补充恢复指引
2144
+ console.error(`\n ⚠️ 已保留步骤进度(未清空)。修复产物后重跑,或:`)
2145
+ console.error(` - 重新开始: sillyspec run ${stageName} --reset`)
2146
+ console.error(` - 强制补盖: sillyspec progress complete-stage ${stageName}${changeName ? ` --change ${changeName}` : ''} --force`)
2147
+ process.exit(1)
2121
2148
  }
2122
2149
 
2123
2150
  // quick 阶段:记录 baselineFiles + 分配 ql-ID(CLI 接管 QUICKLOG 写入)
@@ -12,6 +12,8 @@ export const definition = {
12
12
  name: 'SillySpec 内部检查',
13
13
  prompt: `运行 SillySpec 内部检查。逐项执行以下命令并汇总结果:
14
14
 
15
+ > ⚠️ Windows 兼容:下列 sqlite3 查询依赖外部 sqlite3 CLI(Windows 默认没有)。若某条 sqlite3 失败,改用 sillyspec 自身命令替代——sillyspec progress show 查看当前变更与各阶段状态(等价于查 changes/stages 表),sillyspec doctor --json 查看 db 健康与活跃变更列表。
16
+
15
17
  ### 1. 目录结构完整性
16
18
  \`\`\`bash
17
19
  # 检查 .sillyspec/ 及子目录
@@ -21,7 +23,8 @@ done
21
23
  # 检查 sillyspec.db(SQLite 权威状态源)
22
24
  DB_FILE='.sillyspec/.runtime/sillyspec.db'
23
25
  [ -f "$DB_FILE" ] && echo "✅ sillyspec.db 存在" || echo "❌ sillyspec.db 不存在"
24
- sqlite3 "$DB_FILE" "SELECT count(*) FROM project" 2>/dev/null && echo "✅ sillyspec.db 可查询" || echo "⚠️ sillyspec.db 不可查询"
26
+ # 用 sillyspec 自身命令验证 db 可读(fallback 不依赖外部 sqlite3 CLI,兼容 Windows)
27
+ sqlite3 "$DB_FILE" "SELECT count(*) FROM project" 2>/dev/null && echo "✅ sillyspec.db 可查询" || (sillyspec progress show >/dev/null 2>&1 && echo "✅ sillyspec.db 可查询(经 sillyspec 命令)" || echo "⚠️ sillyspec.db 不可查询")
25
28
  \`\`\`
26
29
 
27
30
  ### 2. 项目配置检查