sillyspec 3.29.5 → 3.30.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 (110) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  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 +18 -116
  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-brainstorm.md +1 -1
  10. package/assets/command-cards/run-quick.md +8 -25
  11. package/package.json +2 -1
  12. package/packages/dashboard/server/executor.js +2 -1
  13. package/packages/dashboard/server/parser.js +7 -7
  14. package/r7-test-env/README-R7-TEST.md +93 -0
  15. package/r7-test-env/demo-project/lib/calc.js +6 -0
  16. package/r7-test-env/demo-project/package.json +1 -0
  17. package/r7-test-env/demo-project/test.js +10 -0
  18. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  19. package/r7-test-env/sillyspec-r7.cmd +3 -0
  20. package/round4/account.mjs +18 -5
  21. package/round4/b1-analyze-task08.mjs +40 -0
  22. package/round4/b1-b1-target-validation.md +32 -0
  23. package/round4/b3a-prompt-audit-result.md +66 -0
  24. package/round4/b3a-prompt-audit.mjs +89 -0
  25. package/round4/optimization-plan.md +165 -0
  26. package/round4/r4-final-report.html +347 -0
  27. package/round4/r5-scenario-matrix.md +46 -0
  28. package/src/change-list.js +9 -0
  29. package/src/classify-change.js +6 -0
  30. package/src/command-cards.js +2 -1
  31. package/src/commit-guard.js +2 -0
  32. package/src/config-schema.js +64 -12
  33. package/src/db.js +35 -4
  34. package/src/decision-distill.js +9 -3
  35. package/src/docs-check.js +1 -1
  36. package/src/doctor-diagnostics.js +44 -2
  37. package/src/flow-draft.js +748 -0
  38. package/src/flow-parity.js +236 -0
  39. package/src/flow-review.js +184 -0
  40. package/src/flow.js +1492 -0
  41. package/src/foreign-declared.js +17 -1
  42. package/src/fr-index.js +396 -22
  43. package/src/fs-atomic.js +5 -1
  44. package/src/git-helper.js +2 -2
  45. package/src/greenfield-bootstrap.js +98 -0
  46. package/src/handoff.js +120 -7
  47. package/src/hunk-attribution.js +191 -0
  48. package/src/index.js +570 -16
  49. package/src/knowledge-classify.js +47 -1
  50. package/src/knowledge-digest.js +162 -0
  51. package/src/knowledge-quicklog.js +246 -0
  52. package/src/machine-draft.js +128 -0
  53. package/src/machine-interface.js +107 -5
  54. package/src/mcp-server.js +1 -0
  55. package/src/module-resolve.js +3 -1
  56. package/src/preview-progress.js +160 -0
  57. package/src/progress/change-registry.js +11 -3
  58. package/src/progress/shared.js +3 -2
  59. package/src/progress.js +56 -13
  60. package/src/quicklog.js +44 -6
  61. package/src/redomain.js +147 -0
  62. package/src/review-material-pack.js +204 -1
  63. package/src/run/archive-distill.js +4 -2
  64. package/src/run/bg-sync.js +67 -0
  65. package/src/run/command.js +123 -10
  66. package/src/run/complete-handlers.js +327 -72
  67. package/src/run/complete.js +350 -71
  68. package/src/run/gate-snapshot-ledger.js +213 -0
  69. package/src/run/gate-snapshot.js +250 -34
  70. package/src/run/gates.js +370 -233
  71. package/src/run/green-cache.js +123 -0
  72. package/src/run/junction-rm.js +70 -0
  73. package/src/run/next.js +14 -0
  74. package/src/run/prompt.js +185 -55
  75. package/src/run/quick-audit.js +185 -12
  76. package/src/run/shared.js +120 -3
  77. package/src/run/stage.js +182 -216
  78. package/src/run/test-ledger.js +264 -0
  79. package/src/run/verify-quality-scan.js +84 -7
  80. package/src/scope-audit.js +6 -1
  81. package/src/sentinel-assertions.js +114 -0
  82. package/src/setup.js +3 -3
  83. package/src/spec-sync.js +41 -1
  84. package/src/stage-contract.js +26 -8
  85. package/src/stage-review-checklist.js +2 -0
  86. package/src/stage-review.js +7 -2
  87. package/src/stage-templates.js +157 -0
  88. package/src/stages/archive.js +2 -0
  89. package/src/stages/brainstorm.js +24 -106
  90. package/src/stages/execute.js +221 -84
  91. package/src/stages/knowledge.js +74 -1
  92. package/src/stages/plan-postcheck.js +352 -1
  93. package/src/stages/plan.js +23 -1
  94. package/src/stages/quick.js +10 -2
  95. package/src/stages/verify.js +4 -2
  96. package/src/sync.js +81 -23
  97. package/src/task-done.js +181 -0
  98. package/src/task-review.js +8 -55
  99. package/src/test-bindings.js +549 -0
  100. package/src/timeline.js +218 -0
  101. package/src/ui-visual.js +175 -0
  102. package/src/verify-draft.js +214 -0
  103. package/src/verify-postcheck.js +860 -158
  104. package/src/verify-probes.js +59 -3
  105. package/src/watcher.js +1031 -0
  106. package/src/worktree-apply.js +44 -4
  107. package/src/worktree-deps.js +22 -8
  108. package/src/worktree-policy.js +42 -0
  109. package/src/worktree.js +102 -15
  110. package/templates/agents-instruction.md +37 -18
@@ -9,7 +9,8 @@ import { createHash } from 'crypto'
9
9
  import { writeAtomicSync } from './fs-atomic.js'
10
10
 
11
11
  export const COMMAND_CARD_TARGETS = { zcode: '.zcode/commands/sillyspec', claude: '.claude/commands/sillyspec' }
12
- export const COMMAND_CARD_NAMES = ['run-brainstorm', 'run-plan', 'run-execute', 'run-verify', 'run-archive', 'run-quick', 'status']
12
+ // flow=轻量变更默认道入口卡(2026-09-25-quick-channel-retire 配套:quick 退役后快道首次有卡)
13
+ export const COMMAND_CARD_NAMES = ['flow', 'run-brainstorm', 'run-plan', 'run-execute', 'run-verify', 'run-archive', 'run-quick', 'status']
13
14
 
14
15
  const ANCHOR_TAIL_RE = /\n?<!-- sillyspec-card: v(\S+) sha256=([0-9a-f]{64}) -->\s*$/
15
16
  const sha256 = (s) => createHash('sha256').update(s, 'utf8').digest('hex')
@@ -85,6 +85,8 @@ function main() {
85
85
  // stderr 必须管道化:execFileSync 默认把子进程 stderr 透传给父进程——非 git 目录下
86
86
  // git 落 --no-index 模式报 unknown option 的 usage 长文会漏进守卫 stderr(噪声)。
87
87
  stdio: ['ignore', 'pipe', 'pipe'],
88
+ windowsHide: true,
89
+ stdio: ['ignore', 'pipe', 'pipe'],
88
90
  })
89
91
  .split(/\r?\n/)
90
92
  .map((s) => s.trim())
@@ -68,8 +68,10 @@ export const LOCAL_YAML_SCHEMA = {
68
68
  note: 'detect 核验 package.json(或对应构建文件) scripts 存在性后才写对应键;缺失则不写。agent 可改。',
69
69
  keys: [
70
70
  { path: 'commands.build', type: 'string', optional: true, status: 'live', readers: ['detectLocalYaml (src/local-detect.js)', 'validateCommands (src/scan-postcheck.js)'], desc: '构建命令。', example: 'npm run build' },
71
- { path: 'commands.test', type: 'string', optional: true, status: 'live', readers: ['extractTestCommand (src/verify-postcheck.js)', 'runQuickTestLintGate (src/run/quick-audit.js)', 'validateCommands (src/scan-postcheck.js)'], desc: '测试命令——verify 阶段 CLI 亲自执行此命令与 verify-result.md 对账,实测失败即阻断;quick --done 触及 src/test 时同款实测(P0-2 门禁)。', example: 'npm test' },
71
+ { path: 'commands.test', type: 'string', optional: true, status: 'deprecated', readers: ['extractTestCommand (src/verify-postcheck.js)', 'validateCommands (src/scan-postcheck.js)'], desc: '【2026-09-26-dynamic-test-inference 退役为全量逃生阀】仅显式 test_strategy: full 时生效——测试面缺省按变更动态推断(本变更测试 ∪ FR 关联回归 ∪ import 依赖,runner 自项目结构推断),不再依赖此配置;未配置且显式 full 时按 package.json scripts.test / pyproject 结构推断全量命令。', example: 'npm test' },
72
72
  { path: 'commands.lint', type: 'string', optional: true, status: 'live', readers: ['detectLocalYaml (src/local-detect.js)', 'runQuickTestLintGate (src/run/quick-audit.js)', 'validateCommands (src/scan-postcheck.js)'], desc: 'lint 命令——quick --done 触及 src/test 时 CLI 亲自执行(P0-2 门禁,失败阻断完成)。', example: 'npm run lint' },
73
+ { path: 'commands.test_timeout_sec', type: 'number', optional: true, status: 'live', readers: ['resolveTestTimeoutMs (src/verify-postcheck.js)'], desc: 'commands.test 超时秒数(R9 实证 2026-09-23:全量套件实测可达 27min,固定 600s 帽必杀)——优先级 local.yaml 本键 > env SILLYSPEC_TEST_TIMEOUT_MS > 缺省 600。慢仓在此提帽,防真慢套件被当超时失败。', example: '2400' },
74
+ { path: 'commands.lint_timeout_sec', type: 'number', optional: true, status: 'live', readers: ['runVerifyLintCheck (src/verify-postcheck.js)'], desc: 'commands.lint 超时秒数——优先级 显式调用参 > 本键 > env SILLYSPEC_LINT_TIMEOUT_MS > 缺省 180(快照内 junction I/O 慢,快照路径调用侧显式传 300)。', example: '600' },
73
75
  { path: 'commands.smoke', type: 'string', optional: true, status: 'live', readers: ['executeVerifyQualityScan (src/run/verify-quality-scan.js)'], desc: '接口冒烟命令——verify 阶段 CLI 亲跑(脚本自理服务生命周期:后台起服+轮询就绪+finally 杀),300s 超时帽,失败/超时只记失败态不阻断本步、触发 PASS 封顶(D-002);指纹含此键,未变不重跑。', example: 'node scripts/smoke.mjs' },
74
76
  { path: 'commands.install', type: 'string', optional: true, status: 'live', readers: ['extractInstallCommand (src/worktree-deps.js)'], desc: '依赖安装命令(worktree 依赖 provisioning 用)。', example: 'npm install' },
75
77
  ],
@@ -110,7 +112,7 @@ export const LOCAL_YAML_SCHEMA = {
110
112
  note: 'test_strategy: module 时,按 git diff 命中的模块子集收窄测试。只支持 inline flow 形态(extractModules 不解析嵌套展开式)。',
111
113
  keys: [
112
114
  { path: 'modules.<name>.path', type: 'string', optional: true, status: 'live', readers: ['extractModules (src/verify-postcheck.js)', 'parseLocalYamlModules (src/plan-postcheck.js)', 'extractModulePaths (src/worktree-deps.js)'], desc: '子模块目录路径(相对仓库根)。', example: 'frontend/' },
113
- { path: 'modules.<name>.test', type: 'string', optional: true, status: 'live', readers: ['extractModules (src/verify-postcheck.js)'], desc: '该子模块的测试命令。', example: 'cd frontend && pnpm test' },
115
+ { path: 'modules.<name>.test', type: 'string', optional: true, status: 'deprecated', readers: ['extractModules (src/verify-postcheck.js,退役提示用)'], desc: '【2026-09-26-dynamic-test-inference 退役,不再消费】测试面按变更动态推断、runner 自项目结构(pyproject/uv.lock/package.json 就近祖先)推断。模块级 flaky 处置迁 known_failures。块在场时 CLI 打印退役指引。', example: 'cd frontend && pnpm test' },
114
116
  ],
115
117
  },
116
118
  {
@@ -118,7 +120,7 @@ export const LOCAL_YAML_SCHEMA = {
118
120
  title: '测试策略',
119
121
  note: 'verify 阶段 CLI 对账的收窄策略(D-005@v2:skip 接线兑现 + evidence-auto 新增,full/module 语义不变)。',
120
122
  keys: [
121
- { path: 'test_strategy', type: 'enum', values: ['full', 'module', 'skip', 'evidence-auto'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)', 'resolveTestStrategy (src/verify-postcheck.js)'], desc: 'full=全量 commands.test;module=按 git diff 命中 modules 子集收窄(需配 modules);skip=真跳过测试(不回退全量,verify 输出显式标注留审计痕迹,R-07);evidence-auto=按变更目录 module-impact.md 影响类型推荐检查组合(行为→module 聚焦测试、文档/prompt→docs-check、门禁契约→gate;缺失/不可解析降级 module 并注记)。缺省:配了 modules: 块 → module(v3.29.3 起缺省收窄,ql-20260920-010);未配 modules → 全量。', example: 'full' },
123
+ { path: 'test_strategy', type: 'enum', values: ['full', 'module', 'skip', 'evidence-auto'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)', 'resolveTestStrategy (src/verify-postcheck.js)'], desc: '【2026-09-26-dynamic-test-inference 起】full=全量(commands.test 在场生效,否则结构推断);module=已退役折算动态子集(超集覆盖:变更文件面收窄 + FR 关联回归 + import 依赖);skip=真跳过(R-07 不变);evidence-auto=按 module-impact.md 推荐(行为→动态子集、文档/门禁→skip)。缺省=动态子集:三源并集非空实测、空则不硬跑全量(防超时/预存失败面)。', example: 'full' },
122
124
  ],
123
125
  },
124
126
  {
@@ -127,6 +129,8 @@ export const LOCAL_YAML_SCHEMA = {
127
129
  note: '变更前就失败的测试行,豁免后不计入 verify 阻断。支持块列表或 inline 数组。务必定期复核,避免误豁免本变更引入的真实失败。',
128
130
  keys: [
129
131
  { path: 'known_failures', type: 'array', optional: true, status: 'live', readers: ['extractKnownFailures (src/verify-postcheck.js)'], desc: '失败行模式列表(匹配测试输出失败行)。', example: 'tests/some-old-test' },
132
+ { path: 'ui_visual_gate', type: 'string', optional: true, status: 'live', readers: ['runUiVisualProbe (src/ui-visual.js)'], desc: '【2026-09-27-ui-visual-guidance】UI 视觉证据分级门档位:warn(默认,缺 visual-evidence.md 仅警告)/ error(缺证据即阻断)/ off(探针整体关闭,含降级硬规则)。视觉降级无用户裁决留痕时无论档位恒阻断。', example: 'warn' },
133
+ { path: 'hunk_gate', type: 'string', optional: true, status: 'live', readers: ['runHunkAttributionGate (src/hunk-attribution.js)'], desc: '【2026-09-27-hunk-attribution-gate】提交面行级归属门档位:warn(默认,未归因文件/跨变更竞争/在途残留仅警告)/ error(未归因与跨变更竞争任一在场阻断收口;在途残留恒仅警告——并发 WIP 弱信号不硬拦)/ off(关闭)。', example: 'warn' },
130
134
  ],
131
135
  },
132
136
  {
@@ -151,6 +155,9 @@ export const LOCAL_YAML_SCHEMA = {
151
155
  note: 'gitignore 生成物(如构建期产出的 src/build-id.ts)不在 git 树也不进 untracked overlay(ls-files --others --exclude-standard 尊重 .gitignore),worktree 缺失致构建炸 Failed to load url——create 期按本清单从主仓复制供给(2026-09-15-worktree-dual-truth-gates 坑③,D-003@v1)。块列表或 inline flow 数组均可解析。',
152
156
  keys: [
153
157
  { path: 'worktree.supplyFiles', type: 'array', optional: true, status: 'live', readers: ['readSupplyFilesConfig + _supplyGeneratedFiles (src/worktree.js — create step 5.9 供给步)'], desc: '随 worktree create 从主仓复制的生成物清单:精确路径或 glob(* 单层 / ** 多层),相对仓根。展开上限 200 文件超出截断警告;实供清单记 meta.supplyFiles;缺省 [] = 供给步空转零行为变化。', example: 'src/build-id.ts' },
158
+ { path: 'worktree.policy.adopt_branch', type: 'boolean', optional: true, status: 'live', readers: ['readWorktreePolicy (src/worktree-policy.js) → WorktreeManager.create 分支冲突守卫 (src/worktree.js)'], desc: '分支已存在冲突的选择记忆:true = 自动收编既有分支(等效常备 --adopt-branch,meta 记 adoptViaPolicy 留痕)。缺省 false = 既有三选一 fail-closed 菜单。显式 flag 优先于 policy。', example: 'true' },
159
+ { path: 'worktree.policy.apply_overlap', type: 'enum', values: ['manual', 'force', 'skip'], optional: true, status: 'live', readers: ['readWorktreePolicy (src/worktree-policy.js) → applyWorktree guard 相交拦截 (src/worktree-apply.js)'], desc: 'apply 与活跃 quick 会话在途声明相交时的处置记忆:manual(缺省)= 既有 fail-closed 报错;force = 放行并留痕(overlapForced.via 记 policy 来源);skip = 无人值守软跳过(同 autoApply 路径)。--force 显式 flag 优先。', example: 'manual' },
160
+ { path: 'worktree.policy.stash_dirty', type: 'boolean', optional: true, status: 'live', readers: ['readWorktreePolicy (src/worktree-policy.js) → applyWorktree 4.4 stash 段 (src/worktree-apply.js)'], desc: '主仓在途改动自动 stash 的选择记忆:true = 等效常备 --stash-dirty(stash SHA 显著打印 + 失败绝不 drop)。缺省 false = 显式 flag opt-in。', example: 'false' },
154
161
  ],
155
162
  },
156
163
  {
@@ -162,6 +169,25 @@ export const LOCAL_YAML_SCHEMA = {
162
169
  { path: 'gate_snapshot.commands', type: 'array', optional: true, status: 'live', readers: ['parseGateSnapshotCommands + runGateSnapshotCommands (src/run/gate-snapshot.js — createGateSnapshot 环境预检后 copy 面前)'], desc: '门禁隔离快照的命令面(元素 string):快照构建期在环境目录链接后、copy 面前于快照根逐条执行(cwd=快照根、300s/条超时帽、非零退出/超时只 warn 继续——fail-open 不作废快照),产出「本仓应然态」生成物,收口主仓生成物缺失/过期(fresh clone 未跑 postinstall / build-id 类产物)时快照内全量 lint/test 必挂的坑(D-002@v2,2026-09-17-feedback-hardening FR-01)。⚠️ 环境目录(node_modules/venv)是活链接,命令写它们会穿透主仓——只放生成物命令(npm run gen:build-id 类),不放 install/build 全家桶。未配置 = 全段空转零行为。', example: 'npm run gen:build-id' },
163
170
  ],
164
171
  },
172
+ {
173
+ id: 'flow',
174
+ title: '2-调用轻量协议(R7 切片二)',
175
+ note: 'flow start/done 协议形状开关与路由参数。design 措辞 flow: thin|legacy 落地为单键 flow.mode(YAML 单键形态,语义一致)。',
176
+ keys: [
177
+ { path: 'flow.mode', type: 'enum', values: ['thin', 'legacy'], optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '2-调用轻量协议开关:thin(缺省——2026-09-25-thin-default-flip 翻转,入口归一:常规变更默认轻量跑道,需求不清晰 CLI 拦下指路头脑风暴预段,实测失败自动升厚)= flow start/done 轻量跑道;legacy = 显式回旧道(run <stage> 全族行为逐字不动,flow start 拒跑并指路)。切回一行 yaml。', example: 'thin' },
178
+ { path: 'flow.edit_ratio_threshold', type: 'number', optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '机器稿改写比例路由阈值(amend 通道计算的行级 editRatio 超阈 → 厚档提示 route_hint)。缺省 0.5。', example: '0.5' },
179
+ { path: 'flow.edit_ratio_enforcement', type: 'enum', values: ['advisory', 'block'], optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: 'editRatio 超阈的执行档:advisory(缺省——测绿可轻量档过,只提示+遥测)| block(超阈阻断 flow done)。用户裁定 advisory 定案,dogfood 后可按遥测翻 block。', example: 'advisory' },
180
+ ],
181
+ },
182
+ {
183
+ id: 'stage',
184
+ title: '阶段说明书下发与会话墙',
185
+ note: 'run <stage> 渲染与阶段边界行为开关。burst 管「一次下发多少说明书」;wall 管「阶段边界要不要强制换会话」(瘦会话模式强制力)。',
186
+ keys: [
187
+ { path: 'stage.burst', type: 'boolean', optional: true, status: 'live', readers: ['readStageBurst (src/run/shared.js)'], desc: 'burst 模式:一次渲染阶段内全部剩余步骤说明书、一次 --done 收口(CLI 内部循环逐步推进)。R13 对撞实测:CLI 往返锐减(×19→×4)但 token 反创系列新高(单次载荷变肥 + 逐轮全量回放)——只减往返不减 token,非默认推荐(体验项)。env SILLYSPEC_STAGE_BURST=0/1 强制。', example: 'false' },
188
+ { path: 'stage.wall', type: 'enum', values: ['advisory', 'hard'], optional: true, status: 'live', readers: ['readStageWall (src/run/shared.js) + runStage 入口墙 (src/run/stage.js)'], desc: '阶段边界会话墙:advisory(缺省)= 阶段完成时提示可 handoff 换会话,不阻断;hard = 同一会话(SILLYSPEC_SESSION_ID 相同)刚收口前驱阶段后硬续墙后阶段(execute/verify)→ run <stage> 拒启动(exit 1),指引 sillyspec handoff 生成交接块换新会话续跑;--same-session 显式逃生口(应急/平台编排)。墙后阶段 = execute/verify(上下文最贵两段:R15 实测 execute 均轮 263K,断崖拆会话后回落 120-170K)。stage-session ledger 缺失/会话标识不可判定 → 放行(fail-open,与账本 best-effort 一致)。', example: 'hard' },
189
+ ],
190
+ },
165
191
  {
166
192
  id: 'auto_mode',
167
193
  title: '变更规模自动分类',
@@ -334,6 +360,8 @@ commands:
334
360
  lint: npm run lint
335
361
  # smoke: node scripts/smoke.mjs # verify 冒烟命令(可选;脚本自理服务生命周期,失败只封顶 PASS 不阻断)
336
362
  # install: npm install # worktree 依赖安装命令
363
+ # test_timeout_sec: 2400 # commands.test 超时秒数(慢仓提帽;缺省 600,env SILLYSPEC_TEST_TIMEOUT_MS 同效)
364
+ # lint_timeout_sec: 600 # commands.lint 超时秒数(缺省 180)
337
365
 
338
366
  # ── SillyHub MCP 客户端凭据(派发到 worker 用)──
339
367
  # agent 手填,或 sillyspec platform connect 在 mcp 段缺失时同源自动填。
@@ -355,17 +383,41 @@ dispatch:
355
383
  # poll_interval_ms: 15000 # ⚠ 路径A 预留·未落地,配了暂不生效
356
384
  # worker_timeout_ms: 60000 # ⚠ 路径A 预留·未落地,配了暂不生效
357
385
 
358
- # ── monorepo 子模块映射(test_strategy: module 时按 git diff 命中模块收窄测试)──
359
- # 只支持 inline flow 形态(嵌套展开式解析不出):
360
- modules:
361
- frontend: { path: "frontend/", test: "cd frontend && pnpm test" }
362
- backend: { path: "backend/", test: "cd backend && npm test" }
386
+ # ── 2-调用轻量协议(R7 切片二;缺省 thin 即轻量跑道,无需配置)──
387
+ # flow:
388
+ # mode: thin # thin=flow start/done 轻量跑道(协议必需交互=2)| legacy=既有 run <stage> 全族(回滚一行)
389
+ # edit_ratio_threshold: 0.5 # 机器稿改写比例路由阈值(超阈提示厚档;缺省 0.5)
390
+ # edit_ratio_enforcement: advisory # advisory=测绿可轻量过只提示(缺省)| block=超阈阻断
391
+
392
+ # ── 阶段说明书下发与会话墙(R16 减负批次)──
393
+ # stage:
394
+ # # burst: true # 一次下发全阶段说明书(R13 实测:往返↓ token↑,体验项非默认)
395
+ # # wall: hard # 阶段边界会话墙(hard=同会话硬续 execute/verify 拒启动,指引 handoff;缺省 advisory 提示不阻断)
396
+
397
+ # ── worktree 守卫选择记忆(R16 减负批次;显式 flag > policy > fail-closed 缺省)──
398
+ # worktree:
399
+ # policy:
400
+ # # adopt_branch: true # 分支已存在冲突自动收编(等效常备 --adopt-branch)
401
+ # # apply_overlap: manual # manual=fail-closed(缺省)| force=放行留痕 | skip=软跳过
402
+ # # stash_dirty: false # true=主仓在途改动自动 stash(等效常备 --stash-dirty)
363
403
 
364
- # ── 测试策略(verify 实测收窄;full/module 语义不变,skip/evidence-auto 为 2026-08-23 新增)──
365
- # full=全量 commands.test | module=按命中模块收窄(需配 modules)
404
+ # ── 测试策略(2026-09-26-dynamic-test-inference 起)──
405
+ # 缺省=动态子集:本变更测试 ∪ FR 关联回归(active FR 覆盖面∩触碰文件→绑定 tests)∪ import 依赖,
406
+ # runner 自项目结构推断(pyproject/uv.lock/package.json 就近祖先)——modules.*.test 与 commands.test
407
+ # 均不再需要(后者仅显式 full 时作全量逃生阀)。三源空(doc-only)不硬跑全量。
408
+ # full=全量(commands.test 在场生效,否则按 package.json scripts.test / pyproject 推断)
366
409
  # skip=真跳过测试(不回退全量,verify 输出显式标注留审计痕迹)
367
- # evidence-auto=按变更 module-impact.md 影响面推荐检查组合(行为→module 聚焦测试、文档→docs-check、门禁→gate;缺失降级 module)
368
- test_strategy: full
410
+ # evidence-auto=按变更 module-impact.md 影响面推荐(行为→动态子集、文档→docs-check、门禁→gate)
411
+ # test_strategy: full # 缺省注释态=动态子集(评审 P3-5 清偿:复制即全量道的自相矛盾陷阱——缺省注释才与上方「缺省=动态子集」一致);确要全量再解注释
412
+
413
+ # ── 证据门档位(2026-09-27-ui-visual-guidance / 2026-09-27-hunk-attribution-gate;注释态=缺省档位生效)──
414
+ # ui_visual_gate: warn # UI 视觉证据分级门:warn(缺省,缺 visual-evidence.md 仅警告)| error(缺证据即阻断)| off(探针整体关闭;视觉降级无用户裁决留痕时无论档位恒阻断)
415
+ # hunk_gate: warn # 提交面行级归属门:warn(缺省)| error(未归因文件/跨变更竞争任一在场阻断收口;在途残留恒仅警告——并发 WIP 弱信号不硬拦)| off(关闭)
416
+
417
+ # ── monorepo 子模块路径映射(.path 仍被 plan/worktree-deps 消费;.test 已退役勿再配)──
418
+ modules:
419
+ frontend: { path: "frontend/" }
420
+ backend: { path: "backend/" }
369
421
 
370
422
  # ── 决策库 behind 复核阈值(docs-check 决策规则:源码在「最近确认」后前进超阈值 → 待复核提示;缺省 10)──
371
423
  decisions:
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
  }
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) {