sillyspec 3.28.7 → 3.28.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 (79) hide show
  1. package/.claude/skills/sillyspec-quick/SKILL.md +2 -0
  2. package/.husky/pre-push +3 -1
  3. package/AGENTS.md +3 -2
  4. package/README.md +35 -11
  5. package/SKILL.md +24 -10
  6. package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
  7. package/package.json +1 -1
  8. package/src/agent-session-log.js +446 -35
  9. package/src/change-risk-profile.js +20 -0
  10. package/src/config-cat.js +6 -3
  11. package/src/config-schema.js +32 -0
  12. package/src/constants.js +3 -0
  13. package/src/db.js +23 -6
  14. package/src/decision-distill.js +15 -3
  15. package/src/design-facts.js +33 -3
  16. package/src/docs-check.js +137 -12
  17. package/src/docs-gate.js +108 -6
  18. package/src/doctor-diagnostics.js +243 -9
  19. package/src/friction-ledger.js +171 -0
  20. package/src/friction-tally.js +7 -2
  21. package/src/git-helper.js +6 -4
  22. package/src/hooks/worktree-guard.js +72 -30
  23. package/src/index.js +356 -81
  24. package/src/knowledge-classify.js +363 -0
  25. package/src/knowledge-hits.js +74 -0
  26. package/src/knowledge-stats.js +180 -0
  27. package/src/local-detect.js +30 -15
  28. package/src/local-register.js +13 -4
  29. package/src/mcp-server.js +38 -8
  30. package/src/progress/change-registry.js +224 -43
  31. package/src/progress/consistency-doctor.js +11 -4
  32. package/src/progress/shared.js +3 -2
  33. package/src/progress/stage-machine.js +3 -1
  34. package/src/progress/step-store.js +30 -17
  35. package/src/progress.js +123 -8
  36. package/src/quick-gate-profile.js +251 -0
  37. package/src/quicklog.js +199 -32
  38. package/src/review-dispatch.js +2 -1
  39. package/src/run/command.js +124 -15
  40. package/src/run/complete-handlers.js +514 -24
  41. package/src/run/complete.js +82 -25
  42. package/src/run/concurrent-detect.js +99 -1
  43. package/src/run/gate-snapshot.js +115 -5
  44. package/src/run/gates.js +85 -14
  45. package/src/run/prompt.js +143 -6
  46. package/src/run/quick-audit.js +136 -15
  47. package/src/run/shared.js +174 -20
  48. package/src/run/stage.js +6 -0
  49. package/src/run/verify-quality-scan.js +14 -3
  50. package/src/scan-diff.js +61 -30
  51. package/src/scan-postcheck.js +74 -0
  52. package/src/scan-refresh.js +477 -0
  53. package/src/scan-staleness.js +25 -13
  54. package/src/scope-audit.js +365 -35
  55. package/src/spec-sync.js +19 -14
  56. package/src/stage-contract-spec.js +4 -3
  57. package/src/stage-contract.js +185 -2
  58. package/src/stage-review.js +29 -12
  59. package/src/stages/brainstorm.js +7 -1
  60. package/src/stages/execute.js +91 -4
  61. package/src/stages/knowledge.js +52 -3
  62. package/src/stages/plan-postcheck.js +57 -12
  63. package/src/stages/quick.js +1 -1
  64. package/src/stages/verify.js +1 -0
  65. package/src/sync-noise.js +114 -6
  66. package/src/sync.js +201 -11
  67. package/src/task-review.js +333 -27
  68. package/src/verify-facts-schema.js +10 -3
  69. package/src/verify-postcheck.js +41 -6
  70. package/src/verify-probes.js +348 -7
  71. package/src/worktree-apply.js +594 -62
  72. package/src/worktree-cross.js +12 -18
  73. package/src/worktree-deps.js +100 -2
  74. package/src/worktree.js +255 -30
  75. package/src/wt-commit.js +128 -0
  76. package/templates/agents-instruction.md +2 -2
  77. package/templates/prompts/testcase-design.md +2 -1
  78. package/templates/prompts/verify-probes.md +4 -1
  79. package/sa-rename-probe/src/new.js +0 -4
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { existsSync, readFileSync, readdirSync } from 'fs'
13
13
  import path from 'path'
14
- import { execFileSync } from 'child_process'
14
+ import { createRequire } from 'node:module'
15
15
 
16
16
  // ── 常量 ──
17
17
 
@@ -29,8 +29,9 @@ const READONLY_COMMANDS = new Set([
29
29
  'node', 'npm', 'npx', // 只允许 --version 等只读子命令,在 matchReadonlyWhitelist 中处理
30
30
  ])
31
31
 
32
- /** 只读 git 子命令(stash 不在此列:drop/clear/pop 破坏性,仅下方 matchReadonly 细化放行 list/裸查询) */
33
- const READONLY_GIT_SUBS = new Set(['diff', 'status', 'log', 'show', 'branch'])
32
+ /** 只读 git 子命令(stash/branch/worktree 不在此列:branch 的 -d/-D/-m/-M 破坏性、worktree
33
+ * 的 remove --force 可删未提交工作区——各自在下方按参数细化放行) */
34
+ const READONLY_GIT_SUBS = new Set(['diff', 'status', 'log', 'show'])
34
35
 
35
36
  /** 危险 git 子命令 */
36
37
  const DANGER_GIT_SUBS = new Set([
@@ -38,6 +39,14 @@ const DANGER_GIT_SUBS = new Set([
38
39
  'mv', 'rm',
39
40
  ])
40
41
 
42
+ /** git branch 的破坏性 flag(-d/-D 删分支、-m/-M 改名、-f 强建重指;含长形同义)——
43
+ * 删/改分支会让 task review 的 base/head ref 引用悬空(gc 后真丢),2026-09-11 审查 P1 收口 */
44
+ const BRANCH_DESTRUCTIVE_RE = /^(?:-[dDmMf]|--delete|--force|--move|--edit-description)(?:=|$)/
45
+
46
+ function hasDestructiveBranchFlag(parts) {
47
+ return parts.slice(2).some(tok => BRANCH_DESTRUCTIVE_RE.test(tok))
48
+ }
49
+
41
50
  /** 危险 git stash 操作 */
42
51
  const DANGER_STASH_ACTIONS = new Set(['drop', 'clear', 'pop'])
43
52
 
@@ -194,6 +203,14 @@ function shouldBlockScanDocOverwrite(filePath, projectRoot) {
194
203
  const guard = readScanGuard(scanDocInfo, projectRoot)
195
204
  if (!guard || guard.forceRescan) return { blocked: false }
196
205
 
206
+ // 增量刷新会话态前置分支(2026-09-14-scan-incremental-refresh D-007@v1):refresh ①拍写入
207
+ // mode='scan-refresh' + refreshDocs 白名单——白名单内=本次刷新工单面,agent 手术编辑放行;
208
+ // 白名单外的 scan 文档继续走原保护(非本次刷新面不放松)。存量 guard(无 mode 字段)不受影响。
209
+ if (guard.mode === 'scan-refresh' && Array.isArray(guard.refreshDocs)) {
210
+ const refreshRel = toPosixPath(path.relative(scanDocInfo.specRoot, filePath))
211
+ if (guard.refreshDocs.includes(refreshRel)) return { blocked: false }
212
+ }
213
+
197
214
  let frontmatter = {}
198
215
  try {
199
216
  frontmatter = parseFrontmatter(readFileSync(filePath, 'utf8'))
@@ -202,7 +219,11 @@ function shouldBlockScanDocOverwrite(filePath, projectRoot) {
202
219
  }
203
220
 
204
221
  const relPath = toPosixPath(path.relative(projectRoot, filePath))
205
- if (frontmatter.source_commit && guard.sourceCommit && frontmatter.source_commit !== guard.sourceCommit) {
222
+ // 归一化比对(D-007@v1 顺带修复):guard 写入方用 40 位全哈希(run/stage.js rev-parse HEAD)、
223
+ // frontmatter 盖章用 7 位短哈希(scan-postcheck.js --short)——旧精确比对恒不等,guard 存在
224
+ // 即恒拦。两侧截 7 位后恢复「同基线放行 / 异基线拦截」设计本意。
225
+ if (frontmatter.source_commit && guard.sourceCommit
226
+ && String(frontmatter.source_commit).slice(0, 7) !== String(guard.sourceCommit).slice(0, 7)) {
206
227
  return {
207
228
  blocked: true,
208
229
  reason: [
@@ -235,9 +256,9 @@ function isInsideWorktreeStorage(filePath, cwd) {
235
256
  /**
236
257
  * 同步只读查询 sillyspec.db 第一行第一列(不依赖外部 sqlite3 CLI)。
237
258
  *
238
- * 用子进程跑 node:sqlite 的 DatabaseSync(execFileSync 等待):
239
- * - node:sqlite 是 Node 内置模块,子进程 require('node:sqlite') 直接可用,无需先 resolve 出
240
- * 绝对路径再注入(旧方案需手动解析原生绑定路径,node:sqlite 无此步骤)。
259
+ * 进程内 createRequire 同步加载 node:sqlite 的 DatabaseSync(2026-09-11 审查性能包②:旧实现
260
+ * 每次查询 spawn 一个 node 子进程,Windows 100-300ms/次,hook 每次 Write/Edit/Bash 触发 1-2 次;
261
+ * hook 自身就是同一 node 二进制,子进程无隔离收益只有延迟):
241
262
  * - readOnly 打开只读连接(替代旧方案 readonly/fileMustExist 语义):绝不写库;WAL 并发读不阻塞主进程写,
242
263
  * 且只读连接可看到已 commit 但未 checkpoint 的过渡态(修复旧 WASM 内存库裸读 .db
243
264
  * 看不见 WAL 的过渡态可见性窗口)。db 缺失/损坏由 open 异常自然触发 fail-closed(外层 catch)。
@@ -245,7 +266,7 @@ function isInsideWorktreeStorage(filePath, cwd) {
245
266
  * 环境假设(R-09):WAL 模式下只读连接仍需在 .runtime/ 目录建/更新 -shm 索引文件,故 .runtime
246
267
  * 必须可写(与主进程一致)——“只读”指不改 DB 数据,不等于不对 -shm 加索引。
247
268
  *
248
- * fail-closed:db 文件不存在 / 子进程打开、查询异常 / 子进程超时或崩溃——一律 console.warn
269
+ * fail-closed:db 文件不存在 / 打开、查询异常 / node:sqlite 不可用——一律 console.warn
249
270
  * (含 e.stderr 详情)并返回 null,调用方(readCurrentStage/isNoWorktreeMode)对 null 走
250
271
  * fail-closed,禁止 fail-open。
251
272
  */
@@ -257,26 +278,26 @@ function queryDbFirstCell(cwd, sql) {
257
278
  console.warn('⚠️ sillyspec.db 不存在,hook 降级 fail-closed: ' + dbPath)
258
279
  return null
259
280
  }
260
- // 子进程同步内置 require('node:sqlite')(DatabaseSync 是同步 API,无 async/import().then 包装)。
261
- // prepare(sql).get() 取第一行整行后取首列值,无行返回 undefined(写空 stdout → 父侧 trim 后 null)。
262
- const script =
263
- "const {DatabaseSync}=require('node:sqlite')," +
264
- "dbPath=" + JSON.stringify(dbPath) + ",sql=" + JSON.stringify(sql) + ";" +
265
- "let db;" +
266
- "try{db=new DatabaseSync(dbPath,{readOnly:true});" +
267
- "const row=db.prepare(sql).get();" +
268
- "const v=row===undefined?undefined:Object.values(row)[0];" +
269
- "if(v!==undefined)process.stdout.write(String(v??''));db.close()}" +
270
- "catch(e){process.stderr.write('hook db query failed: '+String(e&&e.message||e));process.exit(1)};"
281
+ // 进程内同步 require node:sqlite(DatabaseSync 同步 API;2026-09-11 审查性能包②:旧实现每次
282
+ // hook 触发 spawn 一个 node 子进程查库,Windows 100-300ms/次×每 Write/Edit/Bash 1-2 次——
283
+ // hook 自身就是同一 node 二进制,子进程无隔离收益只有延迟)。异常路径与旧子进程版同语义:
284
+ // 捕获后 warn + 返回 null,调用方(readCurrentStage 等)对 null 走 fail-closed。
271
285
  try {
272
- return execFileSync(process.execPath, ['-e', script], {
273
- encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'],
274
- }).trim() || null
286
+ const req = createRequire(import.meta.url)
287
+ const { DatabaseSync } = req('node:sqlite')
288
+ let db
289
+ try {
290
+ db = new DatabaseSync(dbPath, { readOnly: true })
291
+ const row = db.prepare(sql).get()
292
+ const v = row === undefined ? undefined : Object.values(row)[0]
293
+ return v === undefined ? null : (String(v ?? '') || null)
294
+ } finally {
295
+ if (db) db.close()
296
+ }
275
297
  } catch (e) {
276
- // DB 查询失败(db 损坏 / readOnly 打开失败 / 子进程超时或崩溃):降级返回 null,调用方
277
- //(readCurrentStage 等)对 null 走 fail-closed。补 warn 让 doctor/agent 能看到根因,
278
- // 而非完全静默(子进程把诊断写到了 stderr,execFileSync 抛错时挂在 e.stderr 上)。
279
- const detail = (e && e.stderr ? String(e.stderr).trim() : '') || (e && e.message) || 'timeout/crash'
298
+ // DB 查询失败(db 损坏 / readOnly 打开失败 / node:sqlite 不可用):降级返回 null,调用方
299
+ //(readCurrentStage 等)对 null 走 fail-closed。补 warn 让 doctor/agent 能看到根因。
300
+ const detail = (e && e.message) ? e.message : String(e)
280
301
  console.warn('⚠️ sillyspec.db 查询失败,hook 降级 fail-closed: ' + detail)
281
302
  return null
282
303
  }
@@ -532,10 +553,13 @@ function isSingleCommandReadonly(cmd, extraReadonlyCommands = []) {
532
553
  if (cmdName === 'git') {
533
554
  const sub = parts[1] || ''
534
555
  if (READONLY_GIT_SUBS.has(sub)) return true
556
+ // git branch:裸查询/建分支放行;-d/-D/-m/-M/-f(含长形)破坏性不放行(按参数细化,不再整类放行)
557
+ if (sub === 'branch' && !hasDestructiveBranchFlag(parts)) return true
535
558
  // git stash list
536
559
  if (sub === 'stash' && (parts[2] === 'list' || parts.length === 2)) return true
537
- // git worktree 管理(list/add/remove)放行
538
- if (sub === 'worktree') return true
560
+ // git worktree:仅 list(裸 worktree 同 list)。remove --force 可删含未提交改动的工作区、
561
+ // 且 junction 会穿透删主仓 node_modules——add/remove/prune 一律走 sillyspec worktree 命令(内建护栏)
562
+ if (sub === 'worktree' && (parts.length === 2 || parts[2] === 'list')) return true
539
563
  return false
540
564
  }
541
565
 
@@ -563,6 +587,10 @@ function isSingleCommandDangerous(cmd) {
563
587
  if (cmdName === 'git') {
564
588
  const sub = parts[1] || ''
565
589
  if (DANGER_GIT_SUBS.has(sub)) return true
590
+ // git branch -d/-D/-m/-M/-f(含长形):删/改分支让 task review 的 base/head 引用悬空
591
+ if (sub === 'branch' && hasDestructiveBranchFlag(parts)) return true
592
+ // git worktree remove/prune:删工作区(可能含未提交改动)——清理走 sillyspec worktree cleanup
593
+ if (sub === 'worktree' && ['remove', 'prune'].includes(parts[2] || '')) return true
566
594
  // git stash drop/clear/pop
567
595
  if (sub === 'stash' && DANGER_STASH_ACTIONS.has(parts[2] || '')) return true
568
596
  }
@@ -603,6 +631,20 @@ function matchReadonlyWhitelist(command, extraReadonlyCommands = []) {
603
631
  * @param {string} command
604
632
  * @returns {boolean}
605
633
  */
634
+ /**
635
+ * 危险拦截原因的定向指引(branch/worktree 的破坏形态有官方替代路径,提示同步到拦截消息)
636
+ */
637
+ function dangerBlockHint(command) {
638
+ const parts = String(command).trim().toLowerCase().split(/\s+/)
639
+ if (parts[0] === 'git' && parts[1] === 'branch') {
640
+ return '——分支删除/改名/强建会让 task review 的 base/head 引用悬空;分支管理走 sillyspec worktree 流程'
641
+ }
642
+ if (parts[0] === 'git' && parts[1] === 'worktree') {
643
+ return '——worktree 清理走 sillyspec worktree cleanup(内建未落仓护栏),勿裸删工作区'
644
+ }
645
+ return ''
646
+ }
647
+
606
648
  function matchDangerBlacklist(command) {
607
649
  const parts = splitCommandParts(command)
608
650
  return parts.some(p => isSingleCommandDangerous(p))
@@ -825,7 +867,7 @@ function shouldBlockBash(command, cwd) {
825
867
  if (stage === 'quick') {
826
868
  // 危险黑名单仍然拦截
827
869
  if (matchDangerBlacklist(command)) {
828
- return { blocked: true, reason: `dangerous command blocked: ${command.trim()}` }
870
+ return { blocked: true, reason: `dangerous command blocked: ${command.trim()}${dangerBlockHint(command)}` }
829
871
  }
830
872
  // 检查命令是否会覆盖 baseline files(并集 = 保护所有 session 的 baseline)
831
873
  const { baselineFiles } = readAllQuickGuards(projectRoot)
@@ -843,7 +885,7 @@ function shouldBlockBash(command, cwd) {
843
885
 
844
886
  // 危险黑名单
845
887
  if (matchDangerBlacklist(command)) {
846
- return { blocked: true, reason: `dangerous command blocked: ${command.trim()}` }
888
+ return { blocked: true, reason: `dangerous command blocked: ${command.trim()}${dangerBlockHint(command)}` }
847
889
  }
848
890
 
849
891
  // 只读白名单放行