sillyspec 3.29.4 → 3.31.0

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 (148) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +1 -1
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
  5. package/.claude/skills/sillyspec-quick/SKILL.md +7 -105
  6. package/AGENTS.md +38 -20
  7. package/SKILL.md +9 -3
  8. package/assets/command-cards/flow.md +32 -0
  9. package/assets/command-cards/run-archive.md +26 -0
  10. package/assets/command-cards/run-brainstorm.md +33 -0
  11. package/assets/command-cards/run-execute.md +29 -0
  12. package/assets/command-cards/run-plan.md +27 -0
  13. package/assets/command-cards/run-quick.md +17 -0
  14. package/assets/command-cards/run-verify.md +29 -0
  15. package/assets/command-cards/status.md +27 -0
  16. package/package.json +2 -1
  17. package/packages/dashboard/server/executor.js +2 -1
  18. package/packages/dashboard/server/parser.js +7 -7
  19. package/r7-test-env/README-R7-TEST.md +93 -0
  20. package/r7-test-env/demo-project/lib/calc.js +6 -0
  21. package/r7-test-env/demo-project/package.json +1 -0
  22. package/r7-test-env/demo-project/test.js +10 -0
  23. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  24. package/r7-test-env/sillyspec-r7.cmd +3 -0
  25. package/round4/account.mjs +98 -0
  26. package/round4/b1-analyze-task08.mjs +40 -0
  27. package/round4/b1-b1-target-validation.md +32 -0
  28. package/round4/b3a-prompt-audit-result.md +66 -0
  29. package/round4/b3a-prompt-audit.mjs +89 -0
  30. package/round4/brief-autocompact.md +34 -0
  31. package/round4/brief-session-replay.md +50 -0
  32. package/round4/deep-read-synthesis.md +38 -0
  33. package/round4/extract-full.mjs +38 -0
  34. package/round4/extract-timeline.mjs +43 -0
  35. package/round4/find-sessions.mjs +13 -0
  36. package/round4/full-r4-l.md +4707 -0
  37. package/round4/full-r4-o-l.md +4300 -0
  38. package/round4/full-r4-os.md +1681 -0
  39. package/round4/full-r4-sf.md +4255 -0
  40. package/round4/full-r4-sq.md +1734 -0
  41. package/round4/gate-audit-relay-receipt.md +30 -0
  42. package/round4/gate-value-audit.md +52 -0
  43. package/round4/optimization-plan.md +165 -0
  44. package/round4/prereg.md +67 -0
  45. package/round4/prompt-R4-L.md +63 -0
  46. package/round4/prompt-R4-O-L.md +59 -0
  47. package/round4/prompt-R4-O-S.md +56 -0
  48. package/round4/prompt-R4-S-F.md +60 -0
  49. package/round4/prompt-R4-S-Q.md +66 -0
  50. package/round4/r4-final-report.html +347 -0
  51. package/round4/r4-os-timeline.txt +297 -0
  52. package/round4/r4-sf-timeline.txt +738 -0
  53. package/round4/r4-sq-timeline.txt +322 -0
  54. package/round4/r4l-timeline.txt +793 -0
  55. package/round4/r4ol-timeline.txt +729 -0
  56. package/round4/r4sf-timeline.txt +852 -0
  57. package/round4/r5-scenario-matrix.md +46 -0
  58. package/round4/result-S.md +171 -0
  59. package/round4/result-final.md +46 -0
  60. package/round4/result-quality.md +63 -0
  61. package/src/change-list.js +9 -0
  62. package/src/classify-change.js +6 -0
  63. package/src/command-cards.js +74 -0
  64. package/src/commit-guard.js +2 -0
  65. package/src/config-schema.js +64 -12
  66. package/src/contract-matrix.js +26 -5
  67. package/src/db.js +35 -4
  68. package/src/decision-distill.js +9 -3
  69. package/src/diagnostic-codes.js +2 -1
  70. package/src/docs-check.js +1 -1
  71. package/src/doctor-diagnostics.js +44 -2
  72. package/src/flow-draft.js +748 -0
  73. package/src/flow-parity.js +236 -0
  74. package/src/flow-review.js +184 -0
  75. package/src/flow.js +1492 -0
  76. package/src/foreign-declared.js +17 -1
  77. package/src/fr-index.js +396 -22
  78. package/src/fs-atomic.js +5 -1
  79. package/src/git-helper.js +2 -2
  80. package/src/greenfield-bootstrap.js +98 -0
  81. package/src/handoff.js +120 -7
  82. package/src/hunk-attribution.js +191 -0
  83. package/src/index.js +602 -21
  84. package/src/init.js +25 -8
  85. package/src/knowledge-classify.js +47 -1
  86. package/src/knowledge-digest.js +162 -0
  87. package/src/knowledge-quicklog.js +246 -0
  88. package/src/machine-draft.js +128 -0
  89. package/src/machine-interface.js +194 -7
  90. package/src/mcp-server.js +1 -0
  91. package/src/module-impact.js +51 -22
  92. package/src/module-resolve.js +3 -1
  93. package/src/preview-progress.js +160 -0
  94. package/src/progress/change-registry.js +11 -3
  95. package/src/progress/shared.js +3 -2
  96. package/src/progress.js +56 -13
  97. package/src/quicklog.js +44 -6
  98. package/src/redomain.js +147 -0
  99. package/src/review-material-pack.js +204 -1
  100. package/src/run/archive-distill.js +4 -2
  101. package/src/run/bg-sync.js +67 -0
  102. package/src/run/command.js +127 -10
  103. package/src/run/complete-handlers.js +363 -73
  104. package/src/run/complete.js +356 -74
  105. package/src/run/gate-snapshot-ledger.js +213 -0
  106. package/src/run/gate-snapshot.js +260 -38
  107. package/src/run/gates.js +482 -268
  108. package/src/run/green-cache.js +123 -0
  109. package/src/run/junction-rm.js +70 -0
  110. package/src/run/next.js +14 -0
  111. package/src/run/prompt.js +185 -55
  112. package/src/run/quick-audit.js +213 -14
  113. package/src/run/shared.js +123 -4
  114. package/src/run/stage.js +182 -216
  115. package/src/run/test-ledger.js +264 -0
  116. package/src/run/verify-quality-scan.js +84 -7
  117. package/src/scope-audit.js +6 -1
  118. package/src/sentinel-assertions.js +114 -0
  119. package/src/setup.js +3 -3
  120. package/src/spec-sync.js +41 -1
  121. package/src/stage-contract.js +26 -8
  122. package/src/stage-review-checklist.js +2 -0
  123. package/src/stage-review.js +7 -2
  124. package/src/stage-templates.js +157 -0
  125. package/src/stages/archive.js +2 -0
  126. package/src/stages/brainstorm.js +24 -106
  127. package/src/stages/execute.js +222 -85
  128. package/src/stages/knowledge.js +74 -1
  129. package/src/stages/plan-postcheck.js +385 -20
  130. package/src/stages/plan.js +23 -1
  131. package/src/stages/quick.js +10 -2
  132. package/src/stages/verify.js +4 -2
  133. package/src/sync.js +81 -23
  134. package/src/task-done.js +181 -0
  135. package/src/task-review.js +8 -55
  136. package/src/taskcard-frontmatter.js +57 -0
  137. package/src/test-bindings.js +549 -0
  138. package/src/timeline.js +218 -0
  139. package/src/ui-visual.js +175 -0
  140. package/src/verify-draft.js +214 -0
  141. package/src/verify-postcheck.js +919 -159
  142. package/src/verify-probes.js +103 -25
  143. package/src/watcher.js +1031 -0
  144. package/src/worktree-apply.js +44 -4
  145. package/src/worktree-deps.js +22 -8
  146. package/src/worktree-policy.js +42 -0
  147. package/src/worktree.js +102 -15
  148. package/templates/agents-instruction.md +37 -18
@@ -542,11 +542,16 @@ export function verifyApiParity(specBase, scanRoot, runtimeRoot, changeName = nu
542
542
  // worktree,scanRoot(主仓 cwd)里是旧版本——读旧版会漏新调用/多已删调用。
543
543
  let frontendCalls = []
544
544
  let frontendScope = 'full-repo'
545
+ // 变更文件面一次解析两用(quick-C,2026-09-20 报告问题 C):前端调用收窄 + unused 分层的
546
+ // change-relevant 判定共消费;解析失败 → null(前端回退全仓、分层回退 artifact 口径)
547
+ const parityChanged = changeName
548
+ ? (() => { try { return _resolveDiffFilesForParity(specBase, scanRoot, changeName, runtimeRoot) } catch { return null } })()
549
+ : null
545
550
  const frontendRoot = (wt && wt.gitDir !== scanRoot) ? wt.gitDir : scanRoot
546
551
  const frontendRootLabel = frontendRoot === scanRoot ? 'scan-root' : 'worktree'
547
552
  if (changeName) {
548
553
  try {
549
- const changed = _resolveDiffFilesForParity(specBase, scanRoot, changeName, runtimeRoot)
554
+ const changed = parityChanged
550
555
  if (changed && changed.length > 0) {
551
556
  const changedSet = new Set(changed.map(f => f.replace(/\\/g, '/')))
552
557
  frontendCalls = scanFrontendApiCalls(frontendRoot).filter(c => {
@@ -647,12 +652,28 @@ export function verifyApiParity(specBase, scanRoot, runtimeRoot, changeName = nu
647
652
  return liveKeys.has(key) || liveEndpoints.length === 0
648
653
  })
649
654
  // unused 分层(坑 probe5-unused-stock-noise,2026-09-15 EHS 生产实证:490 个 unused 全是
650
- // urgent 等他模块存量端点——全仓 live 扫描 × 本变更对账的口径噪音刷屏)。本变更相关 =
651
- // contract artifact 端点集内(本变更换的端点漏配才是真信号);存量其余折叠计数。无
652
- // artifact(非契约流)回退全列零回归。
655
+ // urgent 等他模块存量端点——全仓 live 扫描 × 本变更对账的口径噪音刷屏)。本变更相关判定
656
+ // 两级(quick-C,2026-09-20 报告问题 C:artifact 集口径被整仓 baseline 击穿——execute 起点
657
+ // 拍的全仓端点基线让 artifactKeys ⊇ 全仓、418 存量端点全误标「本变更端点前端未调用」):
658
+ // ①有变更文件面(_resolveDiffFilesForParity 同款链,前端收窄复用同一次解析)→ 判「端点定义
659
+ // 源文件 ∈ 本变更 diff 面」(源串反斜杠归一 + artifact 任务目录前缀形态,双向 endsWith——
660
+ // 与前端调用收窄同款匹配);
661
+ // ②面不可得(CLI contractScan 无 changeName / diff 兜底链全空)→ 回退 artifact 端点集口径;
662
+ // ③无 artifact(非契约流)→ 回退全列零回归。
653
663
  const artifactKeys = new Set(allProviderEndpoints.map(e => `${e.method} ${e.path}`))
654
664
  const hasArtifacts = allProviderEndpoints.length > 0
655
- const unusedChangeRelevant = hasArtifacts ? narrowedUnused.filter(u => artifactKeys.has(`${u.method} ${u.path}`)) : narrowedUnused
665
+ const unusedChangeRelevant = parityChanged && parityChanged.length > 0
666
+ ? narrowedUnused.filter(u => {
667
+ // diffApiParity 的 unused 项定义源字段是 providerFile(endpoint-extractor.js:492),
668
+ // 兼容直调方可能给的 source 形态
669
+ const src = String(u.providerFile || u.source || '').replace(/\\/g, '/')
670
+ if (!src) return false
671
+ return parityChanged.some(cf => {
672
+ const c = String(cf).replace(/\\/g, '/')
673
+ return src === c || src.endsWith('/' + c) || c.endsWith('/' + src) || src.endsWith(c) || c.endsWith(src)
674
+ })
675
+ })
676
+ : (hasArtifacts ? narrowedUnused.filter(u => artifactKeys.has(`${u.method} ${u.path}`)) : narrowedUnused)
656
677
  const unusedStockCount = narrowedUnused.length - unusedChangeRelevant.length
657
678
 
658
679
  const ok = missingBackend.length === 0
package/src/db.js CHANGED
@@ -7,7 +7,7 @@ import { dirname } from 'path';
7
7
  // 每次 new ProgressManager 都过 init,靠版本戳跳过建表省开销)。
8
8
  // node:sqlite(DatabaseSync)是原生 SQLite 引擎,打开即持久化(不像 sql.js 纯内存需整库 export 落盘),
9
9
  // _createSchema 内 DDL 直接落盘,无需额外 _save。
10
- const DB_SCHEMA_VERSION = 6;
10
+ const DB_SCHEMA_VERSION = 8;
11
11
 
12
12
  // SQLITE_BUSY 应用层有限重试(R-08 / NFR-03):WAL 单写者模型,并发写第二者在
13
13
  // busy_timeout=5000(init PRAGMA)后抛 SQLITE_BUSY。busy_timeout 已在引擎层处理大部分等待;
@@ -252,7 +252,7 @@ export class DB {
252
252
  CREATE TABLE IF NOT EXISTS project (
253
253
  id INTEGER PRIMARY KEY DEFAULT 1,
254
254
  name TEXT NOT NULL,
255
- schema_version INTEGER DEFAULT 6,
255
+ schema_version INTEGER DEFAULT 8,
256
256
  created_at TEXT NOT NULL,
257
257
  updated_at TEXT NOT NULL
258
258
  )
@@ -263,7 +263,7 @@ export class DB {
263
263
  CREATE TABLE IF NOT EXISTS changes (
264
264
  id INTEGER PRIMARY KEY AUTOINCREMENT,
265
265
  name TEXT UNIQUE NOT NULL,
266
- current_stage TEXT DEFAULT 'scan',
266
+ current_stage TEXT DEFAULT 'brainstorm',
267
267
  status TEXT DEFAULT 'active',
268
268
  no_worktree INTEGER DEFAULT 0,
269
269
  created_at TEXT NOT NULL,
@@ -285,6 +285,8 @@ export class DB {
285
285
  status TEXT DEFAULT 'pending',
286
286
  started_at TEXT,
287
287
  completed_at TEXT,
288
+ authority TEXT NOT NULL DEFAULT 'cli',
289
+ preview_evidence TEXT,
288
290
  UNIQUE(change_id, stage)
289
291
  )
290
292
  `);
@@ -298,7 +300,8 @@ export class DB {
298
300
  status TEXT DEFAULT 'pending',
299
301
  output TEXT,
300
302
  completed_at TEXT,
301
- ordering INTEGER NOT NULL DEFAULT 0
303
+ ordering INTEGER NOT NULL DEFAULT 0,
304
+ authority TEXT NOT NULL DEFAULT 'cli'
302
305
  )
303
306
  `);
304
307
 
@@ -335,6 +338,13 @@ export class DB {
335
338
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_stages_change ON stages(change_id)');
336
339
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_steps_stage ON steps(stage_id)');
337
340
 
341
+ // Migration: authority 双轨列(watcher 预览账本,幂等——ADD COLUMN NOT NULL DEFAULT 存量即盖章)
342
+ this._migrateAddColumn('stages', 'authority', "TEXT NOT NULL DEFAULT 'cli'");
343
+ this._migrateAddColumn('stages', 'preview_evidence', 'TEXT');
344
+ this._migrateAddColumn('steps', 'authority', "TEXT NOT NULL DEFAULT 'cli'");
345
+ // 预览账本(2026-09-23-watcher-preview-progress):authority 探测与 GC 走此复合索引
346
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_stages_authority ON stages(change_id, stage, authority)');
347
+
338
348
  // Migration: add isolation columns to changes table (idempotent)
339
349
  this._migrateAddColumn('changes', 'isolation_status', 'TEXT');
340
350
  this._migrateAddColumn('changes', 'isolation_mode', 'TEXT');
@@ -378,6 +388,27 @@ export class DB {
378
388
  // change-registry 首建/claimChangeOwner(task-02 接线会话标识);消费方=assertChangeOwnership
379
389
  // (task-02)+ serializeForSync 平台同步投影。幂等 ALTER(列存在跳过,v5 先例同款)。
380
390
  this._migrateAddColumn('changes', 'owner_session', 'TEXT');
391
+
392
+ // v8(2026-09-27-change-birth-stage-brainstorm):出生阶段 'scan'→'brainstorm' 的存量数据迁移。
393
+ // 旧版 changes 行出生 current_stage='scan'(db.js DDL 默认值 + progress.js 两处 INSERT),
394
+ // thin/quick 等不进主流程的变更全生命周期显示「🔍 代码扫描」误导(scan 是 auxiliary,
395
+ // shared.js MAIN_FLOW_ORDER 从不含它)。只改写「出生默认未被真跑过」的行:active 且
396
+ // (stages.scan='pending' 或根本没有 stages.scan 行——registerChange 出生的行不带 stages 行,
397
+ // 评审 P3-2 缺口)——setStage('scan') 置 in-progress,真在跑/已跑完 scan 的行不受影响。
398
+ // 同步刷 last_local_modified_ts(与 pm._touchLocalModified 同语义):否则平台 pull 见本地
399
+ // 无脏度会静默 import,把平台的旧 'scan' 导回来(platform-sync-pull 冲突保留本地现状)。
400
+ // 幂等:改写后条件不再命中,重跑零效果。strftime 对齐 toISOString 形态(T 分隔 + ms + Z)。
401
+ this.db.exec(`
402
+ UPDATE changes
403
+ SET current_stage = 'brainstorm',
404
+ last_local_modified_ts = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
405
+ WHERE current_stage = 'scan'
406
+ AND status = 'active'
407
+ AND (
408
+ id IN (SELECT change_id FROM stages WHERE stage = 'scan' AND status = 'pending')
409
+ OR NOT EXISTS (SELECT 1 FROM stages WHERE change_id = changes.id AND stage = 'scan')
410
+ )
411
+ `);
381
412
  }
382
413
 
383
414
  /**
@@ -186,8 +186,11 @@ function normalizeModules(moduleIndex) {
186
186
  return moduleIndex.modules ? moduleIndex.modules : moduleIndex
187
187
  }
188
188
 
189
- /** _module-map.yaml 子集解析(仅模块 id → paths/core_files;不复用 modules.js 以保零依赖纯函数) */
190
- function parseModulePathsSubset(content) {
189
+ /** _module-map.yaml 子集解析(仅模块 id → paths/core_files/doc;不复用 modules.js 以保零依赖纯函数)。
190
+ * 2026-09-23 资产三小件③:增 doc 标量捕获(` doc: modules/<id>.md`——模块卡路径,归档文档
191
+ * 认领 advisory 消费)+ 导出(complete-handlers 复用同解析源,跨项目扫描需项目归属故不走
192
+ * discoverModuleIndex 的合并口径)。既有消费方只读 paths/core_files,增字段零行为差。 */
193
+ export function parseModulePathsSubset(content) {
191
194
  const modules = {}
192
195
  let cur = null
193
196
  let key = null
@@ -202,6 +205,8 @@ function parseModulePathsSubset(content) {
202
205
  }
203
206
  const block = line.match(/^ (paths|core_files):$/)
204
207
  if (block) { key = block[1]; modules[cur][key] = modules[cur][key] || []; continue }
208
+ const doc = line.match(/^ doc: (.+)$/)
209
+ if (doc) { modules[cur].doc = doc[1].trim().replace(/^['"]|['"]$/g, ''); key = null; continue }
205
210
  const item = line.match(/^ - (.+)$/)
206
211
  if (item && key) modules[cur][key].push(item[1].trim().replace(/^['"]|['"]$/g, ''))
207
212
  }
@@ -224,10 +229,11 @@ export function discoverModuleIndex(knowledgeRoot) {
224
229
  if (!existsSync(mapPath)) continue
225
230
  const sub = parseModulePathsSubset(readFileSync(mapPath, 'utf8'))
226
231
  for (const [id, m] of Object.entries(sub)) {
227
- if (!merged[id]) merged[id] = { paths: m.paths || [], core_files: m.core_files || [] }
232
+ if (!merged[id]) merged[id] = { paths: m.paths || [], core_files: m.core_files || [], ...(m.doc ? { doc: m.doc } : {}) }
228
233
  else {
229
234
  merged[id].paths = [...new Set([...(merged[id].paths || []), ...(m.paths || [])])]
230
235
  merged[id].core_files = [...new Set([...(merged[id].core_files || []), ...(m.core_files || [])])]
236
+ if (!merged[id].doc && m.doc) merged[id].doc = m.doc
231
237
  }
232
238
  }
233
239
  }
@@ -76,7 +76,7 @@ export const DIAGNOSTIC_CODES = Object.freeze({
76
76
  },
77
77
  });
78
78
 
79
- /** gate check id → 失败码(六 check 全覆盖;表外 id 返回 undefined,调用方不挂键)。 */
79
+ /** gate check id → 失败码(七 check 全覆盖;表外 id 返回 undefined,调用方不挂键)。 */
80
80
  const CHECK_ID_TO_CODE = Object.freeze({
81
81
  artifacts: 'artifacts_invalid',
82
82
  'design-file-list': 'design_file_ref_invalid',
@@ -84,6 +84,7 @@ const CHECK_ID_TO_CODE = Object.freeze({
84
84
  'execute-evidence': 'execute_evidence_unchanged',
85
85
  'task-reviews': 'task_reviews_invalid',
86
86
  'verify-test': 'verify_test_failed',
87
+ 'verify-lint': 'verify_lint_failed',
87
88
  });
88
89
 
89
90
  /**
package/src/docs-check.js CHANGED
@@ -60,7 +60,7 @@ function _rdExists(absPath) {
60
60
  export function createHeadReader(projectRoot, ref = 'HEAD') {
61
61
  const root = pathResolve(projectRoot)
62
62
  const git = (args) => execFileSync('git', ['-C', root, ...args], {
63
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
63
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
64
64
  })
65
65
  let toplevel
66
66
  try { toplevel = git(['rev-parse', '--show-toplevel']).trim() } catch { return null }
@@ -31,6 +31,7 @@ import { gitQuiet } from './git-helper.js'
31
31
  import jsYaml from 'js-yaml';
32
32
  import { pruneTimestampedEntries } from './runtime-hygiene.js';
33
33
  import { CHECK_SEVERITY } from './constants.js';
34
+ import { readGateSnapshotLedger, selectStaleSnapshots } from './run/gate-snapshot-ledger.js'
34
35
  import { checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME, QUICK_SID_RE, resolveRuntimeRoot } from './run/shared.js';
35
36
  import { readFrictionLedger } from './friction-ledger.js';
36
37
  // FR 索引消费(2026-09-18-fr-index-l1 L1,D-007):D14 第四检查——epoch 后归档的索引在场+取代完整。
@@ -1430,6 +1431,34 @@ export function renderDoctorSummary(diagnostics) {
1430
1431
  import * as _wtm from './worktree.js'
1431
1432
  function require_wtm() { return _wtm }
1432
1433
 
1434
+ /**
1435
+ * 门禁快照泄漏探测(task-04,2026-09-24-gate-snapshot-lifecycle / FR-03 / D-007@v1):
1436
+ * 账本内「守卫通过 ∧ 超 TTL(env/24h 单源)∧ pid 明确死」的条目即失活残留——门禁快照
1437
+ * (%TEMP%/sillyspec-gate-*)在进程被杀/Windows 删除失败时必然泄漏且此前零可见面。
1438
+ * 只读:只报不修(零修复),warning 级不阻断(doctor --json 的 overall_status 与退出码
1439
+ * 沿用现行全局口径)。runtimeRoot 缺失/账本损坏→ok(fail-open 零误报)。
1440
+ */
1441
+ export function detectGateSnapshotLeak({ runtimeRoot = null, now = Date.now(), staleHours, isProcessAlive } = {}) {
1442
+ if (!runtimeRoot) {
1443
+ return { name: 'gate_snapshot_leak', label: '门禁快照泄漏', pass: true, severity: null, findings: [], safe_actions: [] }
1444
+ }
1445
+ let leaked = []
1446
+ try {
1447
+ const { stale } = selectStaleSnapshots(readGateSnapshotLedger(runtimeRoot), { now, staleHours, isProcessAlive })
1448
+ leaked = stale.map((e) => ({ root: e.snapshotRoot, ageHours: Math.max(0, Math.round((now - e.createdAt) / 3600000)) }))
1449
+ } catch { leaked = [] }
1450
+ return {
1451
+ name: 'gate_snapshot_leak',
1452
+ label: '门禁快照泄漏',
1453
+ pass: leaked.length === 0,
1454
+ severity: leaked.length > 0 ? CHECK_SEVERITY.WARNING : null,
1455
+ findings: leaked.length > 0
1456
+ ? leaked.map((x) => `残留快照 ${x.root}(账龄约 ${x.ageHours}h,超 TTL 且登记进程已失活——下个门禁建快照时自动回收,也可 rm -rf 后 git worktree prune)`)
1457
+ : [],
1458
+ safe_actions: [],
1459
+ }
1460
+ }
1461
+
1433
1462
  export async function runDoctorDiagnostics({ cwd }) {
1434
1463
  const pointer = resolvePointer(cwd);
1435
1464
  const multiDb = detectMultiDb(cwd, pointer);
@@ -1464,7 +1493,20 @@ export async function runDoctorDiagnostics({ cwd }) {
1464
1493
  // task-06(2026-09-18-ceremony-risk-pricing / FR-04 / R-04):影子对照——轻/重 catch 差异报告 +
1465
1494
  // 转正判据计数(纯增量挂载,只读不阻断;无影子数据跳过不误报)
1466
1495
  const ceremonyShadow = detectCeremonyShadowComparison(cwd, authoritySpecDir)
1467
- const dimensions = [multiDb, pointerHealth, changesSplit, changeDb, executeMismatch, docBloat, repoNativeChain, lifecycleDoc, worktreeHealth, buildEnv, mcpEndpoints, applyManifestDrift, selfMaintenanceTax, archiveIntegrity, ceremonyShadow];
1496
+ // task-04(2026-09-24-gate-snapshot-lifecycle / FR-03 / D-007@v1):门禁快照泄漏
1497
+ // (%TEMP%/sillyspec-gate-* 崩溃残留——warning 级只读可见面;阈值与 create 前扫共用
1498
+ // env SILLYSPEC_GATE_SNAPSHOT_STALE_HOURS/24h 单源,顶层 doctor 无 --stale-hours 参数面)。
1499
+ // runtimeRoot 与 quick/verify 落账本处同源:平台指针 runtimeRoot 优先(否则 specBase/.runtime),
1500
+ // 否则平台模式下账本写别处、doctor 恒读空(独立审查 P1-3)
1501
+ let gateLeakRuntimeRoot = null
1502
+ try {
1503
+ gateLeakRuntimeRoot = resolveRuntimeRoot(
1504
+ { runtimeRoot: pointer.present && pointer.runtimeRoot && existsSync(pointer.runtimeRoot) ? pointer.runtimeRoot : null },
1505
+ authoritySpecDir || join(cwd, '.sillyspec'),
1506
+ )
1507
+ } catch { gateLeakRuntimeRoot = null }
1508
+ const gateSnapshotLeak = detectGateSnapshotLeak({ runtimeRoot: gateLeakRuntimeRoot })
1509
+ const dimensions = [multiDb, pointerHealth, changesSplit, changeDb, executeMismatch, docBloat, repoNativeChain, lifecycleDoc, worktreeHealth, buildEnv, mcpEndpoints, applyManifestDrift, selfMaintenanceTax, archiveIntegrity, ceremonyShadow, gateSnapshotLeak];
1468
1510
 
1469
1511
  return {
1470
1512
  dimensions,
@@ -1697,7 +1739,7 @@ export async function dumpDb({ dbPath, cwd }) {
1697
1739
  const schemaVersion = sv.length ? sv[0].schema_version : null;
1698
1740
  const changes = rows('SELECT name, current_stage, status, created_at, last_active FROM changes ORDER BY last_active DESC')
1699
1741
  .map((r) => ({ name: r.name, current_stage: r.current_stage, status: r.status, created_at: r.created_at, last_active: r.last_active }));
1700
- const stages = rows('SELECT c.name, s.stage, s.status, s.started_at, s.completed_at FROM stages s JOIN changes c ON s.change_id = c.id ORDER BY c.name, s.stage')
1742
+ const stages = rows(`SELECT c.name, s.stage, s.status, s.started_at, s.completed_at FROM stages s JOIN changes c ON s.change_id = c.id WHERE (s.authority = 'cli' OR s.authority IS NULL) ORDER BY c.name, s.stage`)
1701
1743
  .map((r) => ({ change: r.name, stage: r.stage, status: r.status, started_at: r.started_at, completed_at: r.completed_at }));
1702
1744
  return writeDump({ ok: true, meta, schema_version: schemaVersion, changes, stages }, authoritySpecDir);
1703
1745
  } catch (e) {