sillyspec 3.33.3 → 3.33.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.
Files changed (55) hide show
  1. package/.claude/skills/sillyspec-config/SKILL.md +48 -0
  2. package/.claude/skills/sillyspec-flow/SKILL.md +4 -0
  3. package/AGENTS.md +1 -1
  4. package/package.json +1 -1
  5. package/src/config-known-failures.js +258 -0
  6. package/src/config-schema.js +28 -0
  7. package/src/config-validate.js +180 -0
  8. package/src/decision-distill.js +26 -5
  9. package/src/decision-redomain.js +5 -4
  10. package/src/docs-check.js +8 -4
  11. package/src/doctor-diagnostics.js +89 -6
  12. package/src/flow-parity.js +22 -2
  13. package/src/flow-preflight.js +67 -0
  14. package/src/flow.js +252 -55
  15. package/src/fr-index.js +7 -3
  16. package/src/git-helper.js +82 -0
  17. package/src/handoff.js +4 -0
  18. package/src/hooks/worktree-guard.js +12 -3
  19. package/src/hot-cache.js +60 -0
  20. package/src/index.js +111 -7
  21. package/src/knowledge-graph.js +82 -2
  22. package/src/knowledge-match.js +18 -2
  23. package/src/knowledge-quicklog.js +17 -2
  24. package/src/knowledge-vector.js +5 -3
  25. package/src/machine-interface.js +34 -13
  26. package/src/progress/change-registry.js +15 -0
  27. package/src/progress/shared.js +4 -0
  28. package/src/progress/stage-machine.js +15 -2
  29. package/src/progress.js +5 -0
  30. package/src/redomain-unmapped.js +204 -0
  31. package/src/redomain.js +5 -3
  32. package/src/review-dispatch.js +208 -47
  33. package/src/run/bg-sync.js +4 -0
  34. package/src/run/code-face-key.js +4 -1
  35. package/src/run/command.js +20 -5
  36. package/src/run/complete-handlers.js +38 -4
  37. package/src/run/complete.js +185 -14
  38. package/src/run/gates.js +30 -1
  39. package/src/run/prompt.js +9 -1
  40. package/src/run/quick-audit.js +4 -2
  41. package/src/run/shared.js +18 -1
  42. package/src/run/stage.js +22 -19
  43. package/src/run/verify-quality-scan.js +51 -0
  44. package/src/runtime-paths.js +3 -0
  45. package/src/sillyhub-mcp/client.js +11 -6
  46. package/src/spawn-settle.js +47 -0
  47. package/src/spec-sync.js +89 -3
  48. package/src/stages/knowledge.js +25 -3
  49. package/src/timing.js +55 -0
  50. package/src/verify-postcheck.js +153 -11
  51. package/src/watcher.js +4 -0
  52. package/src/worktree-apply.js +67 -4
  53. package/src/worktree.js +80 -11
  54. package/src/yaml-scalar-unquote.js +43 -0
  55. package/templates/agents-instruction.md +1 -1
@@ -54,6 +54,7 @@ const VALUE_FLAGS = new Set([
54
54
  '--session', // 显式会话标识(吃值;2026-09-14-change-ownership-guards task-02 所有权三级解析最高优先级层,启动 claim 消费)
55
55
  '--inherit-from', // wait 继承盖章锚点 <D-xxx@vN>(吃值;2026-09-18-preflight-slimming task-03 / D-003@v2:仅 --wait 场景合法,--wait 分发点校验消费)
56
56
  '--step', // --done --step <名|序号> 意图断言(吃值;:340 消费。漏登记死路同 --wait-interactive 前例:burst 说明书出示该形态却被未知参数 exit(2) 拦死,2026-09-23 执行会话实证)
57
+ '--answers', // --continue --answers k1=v1,k2=v2 批量回填多个 waiting 步骤(吃值;2026-10-21-protocol-tax task-04/FR-04:仅 --continue 场景合法,complete.js continueStep 消费)
57
58
  ])
58
59
 
59
60
  /**
@@ -272,8 +273,10 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
272
273
  // W1-B: run <stage> 暂不支持 --json。之前 --json 进 knownFlags 白名单却从不读取(静默吞),
273
274
  // agent 按 gate/derive 契约期望 envelope 实则拿到混合人类文本。显式 fail-fast 杜绝歧义;
274
275
  // 完整 run envelope 待 outputStep 重构(W6)后支持(prompt + nextAction 结构化)。
275
- if (opts.json) {
276
- console.error('❌ run <stage> 暂不支持 --json(静默吞是 bug,故显式拒绝)。')
276
+ // 例外(2026-10-21-protocol-tax task-04 / FR-04):--wait --json 放行——waitStep 输出唯一
277
+ // JSON 对象(问询+选项结构化,agent 直连 ask_user_question),是明确的局部契约而非静默吞。
278
+ if (opts.json && !args.includes('--wait')) {
279
+ console.error('❌ run <stage> 暂不支持 --json(静默吞是 bug,故显式拒绝;--wait 场景除外)。')
277
280
  console.error(' 查事实用 gate/derive(支持 --json envelope);run 输出人类可读 prompt 供 agent 直接执行。')
278
281
  process.exit(2) // 用法错 → exit 2
279
282
  }
@@ -420,6 +423,15 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
420
423
  process.exit(2) // 用法错 → exit 2
421
424
  }
422
425
  const continueAnswer = getFlagValue('--answer')
426
+ // ── --answers 批量回填(2026-10-21-protocol-tax task-04 / FR-04)──
427
+ // 仅 --continue 场景合法(同 --inherit-from 模式:脱离 --continue 无消费点,静默吞会让
428
+ // agent 误以为已回填);与 --answer 互斥校验在 complete.js 批路径入口(两值都解析完再判)。
429
+ const continueAnswers = getFlagValue('--answers')
430
+ if (continueAnswers !== null && !isContinue) {
431
+ console.error('❌ --answers 仅在 --continue 场景合法(一次回填多个 waiting 步骤:--answers k1=v1,k2=v2)')
432
+ console.error(` 用法: sillyspec run ${stageName} --continue --answers "1=答案A,3=答案B"`)
433
+ process.exit(2) // 用法错 → exit 2
434
+ }
423
435
  const resolvedSpecDir = specDir || getFlagValue('--spec-dir') || getFlagValue('--spec-root')
424
436
  const platformOpts = {
425
437
  specRoot: resolvedSpecDir ? resolve(resolvedSpecDir) : null,
@@ -747,6 +759,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
747
759
  '--skip-apply', // 归档收口跳过 apply 校验(task-02 只注册透传不改行为,消费归 task-03 archive 接线)
748
760
  '--inherit-from', // wait 继承盖章 <D-xxx@vN>(2026-09-18-preflight-slimming task-03:仅 --wait 场景合法,hasDecisionId 校验+盖章在 --wait 分发点/complete 层)
749
761
  '--step', // --done 意图断言 <名|序号>(吃值,VALUE_FLAGS 同步登记;:340 消费——漏登记则说明书出示的 --done --step 形态进命令即被未知参数拦死,2026-09-23 执行会话实证)
762
+ '--answers', // --continue 批量回填 k1=v1,k2=v2(吃值,VALUE_FLAGS 同步登记;2026-10-21-protocol-tax task-04:仅 --continue 场景合法,continueStep 消费)
750
763
  '--upgrade-thick', // 轻量→完整升档用户同意门(2026-09-25-thin-upgrade-consent:布尔 flag,混跑回退写侧 :1362 消费——无 flag 拒跑,带 flag 落 legacy_fallback+同意时点留痕)
751
764
  '--same-session', // R17 实验实证死路清偿(cli-protocol-trust):STAGE_WALL 报错指引「加 --same-session 重跑」(stage.js 硬墙文案),消费在 runStage 逃生口——此前漏登记,照指引重跑即被本白名单 exit 2(指引指向死路的自我矛盾)
752
765
  '--force', // 同族第二实例(静态扫描新发现):quick --cancel 强清消费点与报错指引(可用 --force)在场但未登记——同死路;一致性钉 test/flag-contract.test.mjs 守护整类
@@ -1726,7 +1739,9 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1726
1739
  const changed = await ensureStageSteps(progress, stageName, cwd, specRoot)
1727
1740
  if (changed && effectiveChange) {
1728
1741
  pm._write(cwd, progress, effectiveChange)
1729
- triggerSync(cwd, effectiveChange, platformOpts)
1742
+ // 只读查看(--status)不推平台(2026-10-21-protocol-tax task-06 / FR-06):重播种是本地
1743
+ // 自愈写(保留),平台同步面归写命令与 bg-sync——status 的同步语义对齐 progress show。
1744
+ if (!isStatus) triggerSync(cwd, effectiveChange, platformOpts)
1730
1745
  progress = pm.read(cwd, effectiveChange) || progress
1731
1746
  }
1732
1747
  if (changed && _preReseedPending && (isDone || isSkip || isWait)
@@ -1795,12 +1810,12 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1795
1810
  }
1796
1811
  inheritFrom = waitInheritFrom
1797
1812
  }
1798
- return await waitStep(pm, progress, stageName, cwd, outputText, waitReason, waitOptions, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, inheritFrom })
1813
+ return await waitStep(pm, progress, stageName, cwd, outputText, waitReason, waitOptions, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, inheritFrom, waitJson: Boolean(opts.json) })
1799
1814
  }
1800
1815
 
1801
1816
  // --continue: 从 waiting 恢复
1802
1817
  if (isContinue) {
1803
- return await continueStep(pm, progress, stageName, cwd, continueAnswer, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, fromStep: fromStepValue })
1818
+ return await continueStep(pm, progress, stageName, cwd, continueAnswer, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, fromStep: fromStepValue, answersSpec: continueAnswers })
1804
1819
  }
1805
1820
 
1806
1821
  // --done
@@ -883,6 +883,26 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
883
883
  *
884
884
  * 纯函数(git 读数由调用方喂入),单测直接钉构成口径。
885
885
  * @param {{changeName:string, stagedNameStatus:string, headTreeFiles:string, existsFn?:(p:string)=>boolean}} p
886
+ /**
887
+ * 归档落库判定(2026-10-23-flow-done-shared-repo-multi-session-frictions 缺陷③):HEAD 树中
888
+ * 是否已含本变更的归档目录条目(.sillyspec/changes/archive/<changeName>/ 下一层首段)。
889
+ * 目录名口径:archiveDestDirName 现行=裸变更名;`-<changeName>` 后缀兼容历史日期前缀形态。
890
+ * 纯函数(headTreeFiles 由调用方 git ls-tree 取好传入,测试零 git 依赖)。
891
+ */
892
+ export function archiveLandedInHeadTree({ headTreeFiles, changeName }) {
893
+ const prefix = '.sillyspec/changes/archive/'
894
+ const name = String(changeName || '')
895
+ if (!name) return false
896
+ for (const line of String(headTreeFiles || '').split('\n')) {
897
+ const p = line.trim().replace(/\\/g, '/')
898
+ if (!p.startsWith(prefix)) continue
899
+ const first = p.slice(prefix.length).split('/')[0]
900
+ if (first === name || first.endsWith('-' + name)) return true
901
+ }
902
+ return false
903
+ }
904
+
905
+ /**
886
906
  * @returns {{pathspecs:string[], dropped:string[]}}
887
907
  */
888
908
  export function resolveArchiveCommitPathspecs({ changeName, stagedNameStatus, headTreeFiles, existsFn }) {
@@ -1152,12 +1172,26 @@ try {
1152
1172
  existsFn: (p) => { try { return existsSync(join(cwd, p)) } catch { return false } },
1153
1173
  })
1154
1174
  if (pathspecs.length > 0) {
1155
- console.log(`🧾 归档提交一笔到位(src 锚 HEAD 树/归档侧目录 pathspec/共享面按在场——共享暂存区竞态加固):`)
1156
- console.log(` git commit -m "chore(archive): ${changeName} 归档留档" -- ${pathspecs.join(' ')}`)
1175
+ // 落库断言(2026-10-23-flow-done-shared-repo-multi-session-frictions 缺陷③):打印命令≠落库
1176
+ // ——共享暂存区竞态下命令没人执行/被并行会话抢跑,都呈现「归档目录全 staged(A/R)+ 源侧
1177
+ // 删除停 D + flow done 报成功」的半态(实测 pull/清理丢归档)。终态不得只以打印当成功:
1178
+ // HEAD 树含本变更归档路径=已落库(ℹ️ 语义不变);未落库=🚨 升级 stderr + 可复制命令 + 后果。
1179
+ const landed = archiveLandedInHeadTree({ headTreeFiles: String(headTreeRaw || ''), changeName })
1180
+ const out = landed ? console.log : console.error
1181
+ if (!landed) {
1182
+ console.error(`🚨 归档提交未落库:归档目录已进暂存区(A/R)但 HEAD 树中无 .sillyspec/changes/archive/ 下本变更路径——只打印不执行 ≠ 归档成功(共享仓竞态:命令未被本会话执行或被并行会话抢跑)`)
1183
+ console.error(` 立即执行(幂等;pathspec 限本变更面):`)
1184
+ } else {
1185
+ console.log(`🧾 归档提交一笔到位(归档路径已在 HEAD 树——余下 staged 面一笔带走;src 锚 HEAD 树/归档侧目录 pathspec/共享面按在场——共享暂存区竞态加固):`)
1186
+ }
1187
+ out(` git commit -m "chore(archive): ${changeName} 归档留档" -- ${pathspecs.join(' ')}`)
1157
1188
  if (dropped.length > 0) {
1158
- console.log(` ⚠️ 丢弃 ${dropped.length} 个瞬时暂存条目(HEAD 不在册且工作区不在场——并行会话动了共享暂存区,未入命令):${dropped.join('、')}`)
1189
+ out(` ⚠️ 丢弃 ${dropped.length} 个瞬时暂存条目(HEAD 不在册且工作区不在场——并行会话动了共享暂存区,未入命令):${dropped.join('、')}`)
1190
+ }
1191
+ out(` (若执行报 pathspec 不匹配:共享仓暂存区在打印→执行间又被并行会话改动——git diff --cached --name-only 全量重核,以实际面替换 pathspec 后再提交)`)
1192
+ if (!landed) {
1193
+ console.error(` 不执行的后果:归档件只存在于暂存区——下一次 pull/清理/工作区重置会丢归档。执行后核对:git log --oneline -1 应见「chore(archive): ${changeName} 归档留档」。`)
1159
1194
  }
1160
- console.log(` (若执行报 pathspec 不匹配:共享仓暂存区在打印→执行间又被并行会话改动——git diff --cached --name-only 全量重核,以实际面替换 pathspec 后再提交)`)
1161
1195
  }
1162
1196
  } catch { /* 提示失败不阻断归档(advisory) */ }
1163
1197
  if (othersResidual.length > 0) {
@@ -161,7 +161,34 @@ export function synthesizeStepOutput({ stageName, stepName, cwd, platformOpts =
161
161
  return `【CLI 合成】步骤「${stepName}」完成${windowNote};门禁以本次 CLI 校验输出为准(方案取舍等语义说明需 --output 手写)`
162
162
  }
163
163
 
164
+ /**
165
+ * --done 心跳可观测 wrapper(FR-03 / dsh z2 实证:verify 纯文档步骤 --done 静默挂起 >5 分钟,
166
+ * agent 无从分辨在跑还是死锁):收口链(单步 --done 与阶段完成门全链)超过 60s 时每 60s 打一行
167
+ * stderr 进度行——stdout 保持纯净(--json envelope 消费方 JSON.parse(stdout) 不被污染);结束
168
+ * (任意 return/throw)finally 清理,interval unref(命令中断不残留定时器)。常规 <60s 路径
169
+ * 零输出零行为变化;intervalMs 可注入供测试。
170
+ */
171
+ export async function withDoneHeartbeat(label, fn, { intervalMs = 60_000 } = {}) {
172
+ let beats = 0
173
+ const hb = setInterval(() => {
174
+ beats += 1
175
+ console.error(`⏳ ${label} 收口链仍在执行(已约 ${Math.round((beats * intervalMs) / 1000)}s)——实测门/探针/同步可能较慢;持续超 5 分钟可用 sillyspec flow status / progress show 旁路核对状态(非死锁判定)`)
176
+ }, intervalMs)
177
+ hb.unref?.()
178
+ try {
179
+ return await fn()
180
+ } finally {
181
+ clearInterval(hb)
182
+ }
183
+ }
184
+
164
185
  export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
186
+ return withDoneHeartbeat(`${stageName} --done`,
187
+ () => _completeStepInner(pm, progress, stageName, cwd, outputText, inputText, options),
188
+ { intervalMs: options.heartbeat?.intervalMs })
189
+ }
190
+
191
+ async function _completeStepInner(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
165
192
  const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, isNoDocs = false, isSkipApply = false, sessionFlag = null, quickFiles = [], linkedChanges = [], linkedChangesAuto = [] } = options
166
193
  // specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)——
167
194
  // 与 prompt.js resolvePromptSpecBase 同序:--done 收尾的 user-inputs.md/超长 artifact 落盘
@@ -697,8 +724,16 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
697
724
  // 批量保持单步推进(亲测步照 noAI 硬门走)——「批量省的是上下文往返,不省任何门」承诺恢复成立。
698
725
  // 判定经 hasPendingVerifyScanStep:DB 步骤行只有 {name,status}(_cliAction 不持久化——评审
699
726
  // P1 修复:初版直接读 s._cliAction 恒 false 死代码),按 stageRegistry 定义索引对齐判。
727
+ // 资格先行(2026-10-21-verify-reuse-large-repo FR-02):isVerifyBatchEligible 纯判定(只查
728
+ // 不改)先于亲测——批量不会触发时(报告未就绪/锚定步未过)中间步 --done 不再预跑整轮实测
729
+ // (dsh 0ff6f1c8 取证:step 3/4/5 各烧一轮 10~13.5 分钟全量实测只为随后单步推进)。资格成立
730
+ // 时行为不变:先亲测后批量;detectVerifyBatchFinish 内部全量复检不消费本判定,无 TOCTOU。
700
731
  let _skipBatch = false
701
- if (hasPendingVerifyScanStep(stageName, steps)) {
732
+ const _batchEligible = isVerifyBatchEligible({ changeName, specBase, steps })
733
+ if (!_batchEligible.eligible && hasPendingVerifyScanStep(stageName, steps)) {
734
+ console.log(` ℹ️ 批量对齐未就绪(${_batchEligible.reason})——亲测归质量扫描步与收口门,本步不预跑`)
735
+ }
736
+ if (_batchEligible.eligible && hasPendingVerifyScanStep(stageName, steps)) {
702
737
  try {
703
738
  const { executeVerifyQualityScan } = await import('./verify-quality-scan.js')
704
739
  console.log(' 🧪 批量对齐面含 noAI 亲测步——先亲测(幂等,有记录秒回)…')
@@ -1372,6 +1407,12 @@ function prefetchDiffFileSet(ctx) {
1372
1407
  * @returns {Promise<object|undefined>} 末轮 completeStep 返回值
1373
1408
  */
1374
1409
  export async function completeStepBurst(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
1410
+ return withDoneHeartbeat(`${stageName} --done(burst)`,
1411
+ () => _completeStepBurstInner(pm, progress, stageName, cwd, outputText, inputText, options),
1412
+ { intervalMs: options.heartbeat?.intervalMs })
1413
+ }
1414
+
1415
+ async function _completeStepBurstInner(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
1375
1416
  if (outputText) console.log(`📦 burst 收口摘要:${outputText}`)
1376
1417
  let roundOptions = { ...options, printNext: false }
1377
1418
  // burst 单调时钟基准(坑 stage-burst ①):null=首轮(用真实时刻),此后每轮保严格递增
@@ -1735,24 +1776,42 @@ export function hasPendingVerifyScanStep(stageName, steps) {
1735
1776
  }
1736
1777
  }
1737
1778
 
1738
- export function detectVerifyBatchFinish({ changeName, specBase, steps }) {
1739
- if (!changeName) return { batched: false, aligned: 0 }
1779
+ /**
1780
+ * 批量资格纯判定(2026-10-21-verify-reuse-large-repo FR-02,从 detectVerifyBatchFinish 抽出):
1781
+ * 只查不改(不 mutating 步骤状态)——verify --done 批量对齐块用它做「预跑亲测」的前置闸:
1782
+ * 资格不成立时中间步 --done 不预跑 executeVerifyQualityScan(实测归亲测步自身与收口门;
1783
+ * dsh 0ff6f1c8 取证:报告未就绪的每个中间步 --done 都整轮实测 10~13.5 分钟纯浪费)。
1784
+ * 判据与 detectVerifyBatchFinish 逐字同源(那边消费本函数,理由串不变)。
1785
+ */
1786
+ export function isVerifyBatchEligible({ changeName, specBase, steps }) {
1787
+ if (!changeName) return { eligible: false, reason: '无 changeName' }
1740
1788
  try {
1741
1789
  const changeDir = join(specBase, 'changes', changeName)
1742
1790
  const reportPath = join(changeDir, 'verify-result.md')
1743
- if (!existsSync(reportPath)) return { batched: false, aligned: 0, reason: 'verify-result.md 缺席(报告未写不批量)' }
1791
+ if (!existsSync(reportPath)) return { eligible: false, reason: 'verify-result.md 缺席(报告未写不批量)' }
1744
1792
  const report = readFileSync(reportPath, 'utf8')
1745
1793
  if (!/结论枚举[::]\s*`?\s*(PASS WITH NOTES|PASS|FAIL)/.test(report)) {
1746
- return { batched: false, aligned: 0, reason: '结论枚举未填(待填占位不批量)' }
1794
+ return { eligible: false, reason: '结论枚举未填(待填占位不批量)' }
1747
1795
  }
1748
1796
  if (!existsSync(join(changeDir, 'verify-facts.json'))) {
1749
- return { batched: false, aligned: 0, reason: 'verify-facts.json 缺席(探针未跑不批量)' }
1797
+ return { eligible: false, reason: 'verify-facts.json 缺席(探针未跑不批量)' }
1750
1798
  }
1751
- const firstPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
1752
- if (firstPendingIdx === -1) return { batched: false, aligned: 0, reason: '无剩余 step' }
1799
+ const firstPendingIdx = (steps || []).findIndex(s => s.status === 'pending' || s.status === 'in-progress')
1800
+ if (firstPendingIdx === -1) return { eligible: false, reason: '无剩余 step' }
1753
1801
  if (firstPendingIdx < 2) {
1754
- return { batched: false, aligned: 0, reason: '锚定步未过(step 1-2 须逐步完成,语义锚定不批量)' }
1802
+ return { eligible: false, reason: '锚定步未过(step 1-2 须逐步完成,语义锚定不批量)' }
1755
1803
  }
1804
+ return { eligible: true }
1805
+ } catch (e) {
1806
+ return { eligible: false, reason: `批量资格判定异常(fail-open 不批量):${e && e.message ? e.message : e}` }
1807
+ }
1808
+ }
1809
+
1810
+ export function detectVerifyBatchFinish({ changeName, specBase, steps }) {
1811
+ if (!changeName) return { batched: false, aligned: 0 }
1812
+ const pre = isVerifyBatchEligible({ changeName, specBase, steps })
1813
+ if (!pre.eligible) return { batched: false, aligned: 0, reason: pre.reason }
1814
+ try {
1756
1815
  const now = new Date().toLocaleString('zh-CN', { hour12: false })
1757
1816
  let aligned = 0
1758
1817
  for (const step of steps) {
@@ -2041,6 +2100,31 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
2041
2100
  return
2042
2101
  }
2043
2102
 
2103
+ // ── --wait --json(2026-10-21-protocol-tax task-04 / FR-04)──
2104
+ // stdout 输出唯一一个可 JSON.parse 的对象(问询+选项结构化,agent 直连 ask_user_question,
2105
+ // 免解析长文本);退出码与文本模式一致(成功 0)。仅非盖章路径——--inherit-from 保持文本契约
2106
+ //(盖章路径无问询语义,JSON 化无对象可给)。
2107
+ if (options.waitJson && !inheritFrom) {
2108
+ let waitOptionList = []
2109
+ try {
2110
+ const raw = stageData.steps[currentIdx].waitOptions
2111
+ if (raw) waitOptionList = JSON.parse(raw)
2112
+ } catch { /* 解析失败按无选项处理——JSON 面宁缺勿错 */ }
2113
+ console.log(JSON.stringify({
2114
+ ok: true,
2115
+ command: 'wait',
2116
+ change: changeName || null,
2117
+ stage: stageName,
2118
+ step: currentIdx + 1,
2119
+ stepName: stageData.steps[currentIdx].name,
2120
+ question: outputText || waitReason || null,
2121
+ reason: waitReason || null,
2122
+ options: (Array.isArray(waitOptionList) ? waitOptionList : []).map((o) => ({ label: String(o), description: '' })),
2123
+ resumeCommand: `sillyspec run ${stageName} --continue --answer "你的选择"${changeName ? ` --change ${changeName}` : ''}`,
2124
+ }))
2125
+ return
2126
+ }
2127
+
2044
2128
  console.log(`⏸️ Step ${currentIdx + 1}/${stageData.steps.length} 已暂停等待:${stageData.steps[currentIdx].name}`)
2045
2129
  if (waitReason) console.log(` 原因:${waitReason}`)
2046
2130
  if (waitOptions) console.log(` 选项:${formatWaitOptions(waitOptions)}`)
@@ -2057,8 +2141,36 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
2057
2141
  }
2058
2142
  }
2059
2143
 
2144
+ /**
2145
+ * --answers 批量回填键值解析(2026-10-21-protocol-tax task-04 / FR-04)。
2146
+ * 形态:k1=v1,k2=v2;值含逗号用引号 k="v,含逗号"(引号包裹段内逗号不切分,引号字符本身不保留)。
2147
+ * 纯函数:解析失败返回 null(调用方 exit 2);空键/空值/无等号 token 均视为格式错。
2148
+ */
2149
+ function parseAnswersSpec(spec) {
2150
+ const parts = []
2151
+ let cur = ''
2152
+ let inQuote = false
2153
+ for (const ch of String(spec)) {
2154
+ if (ch === '"') { inQuote = !inQuote; continue }
2155
+ if (ch === ',' && !inQuote) { parts.push(cur); cur = ''; continue }
2156
+ cur += ch
2157
+ }
2158
+ parts.push(cur)
2159
+ const pairs = []
2160
+ for (const p of parts) {
2161
+ const t = p.trim()
2162
+ const eq = t.indexOf('=')
2163
+ if (!t || eq <= 0) return null
2164
+ const key = t.slice(0, eq).trim()
2165
+ const value = t.slice(eq + 1).trim()
2166
+ if (!key || !value) return null
2167
+ pairs.push({ key, value })
2168
+ }
2169
+ return pairs.length > 0 ? pairs : null
2170
+ }
2171
+
2060
2172
  export async function continueStep(pm, progress, stageName, cwd, answer, options = {}) {
2061
- const { changeName, platformOpts = {}, fromStep } = options
2173
+ const { changeName, platformOpts = {}, fromStep, answersSpec = null } = options
2062
2174
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
2063
2175
  const stageData = progress.stages[stageName]
2064
2176
 
@@ -2067,16 +2179,75 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
2067
2179
  process.exit(1)
2068
2180
  }
2069
2181
 
2070
- if (!answer) {
2071
- console.error('❌ --continue 需要 --answer 参数')
2182
+ // --answers 格式错 fail-fast(先于幂等分支:格式错是用法错误,与等待态无关——
2183
+ // 幂等放行的是「合法调用但无等待对象」,不是「任何调用都吞」)
2184
+ if (answersSpec != null && parseAnswersSpec(answersSpec) == null) {
2185
+ console.error('❌ --answers 格式无法解析(期望 k1=v1,k2=v2 形态,值含逗号用引号 k="v,含逗号")')
2072
2186
  process.exit(2) // 用法错 → exit 2
2073
2187
  }
2074
2188
 
2075
2189
  // 查找 waiting 的步骤
2076
2190
  const waitingSteps = stageData.steps.map((s, i) => ({ ...s, idx: i })).filter(s => s.status === 'waiting')
2191
+ // ── 幂等 no-op(2026-10-21-protocol-tax task-05 / FR-05)──
2192
+ // 无 waiting 步骤时 --continue 从 exit=1 报错改为 exit 0 + 指引行:会话考古(z1)实证
2193
+ // 4 次「无等待步骤」失败全部来自 agent 在两次 --wait 之间干活后的链式防御写法误撞——
2194
+ // 等待态不存在本身不是协议错误,失败化只迫使 agent 多一轮试错。答案不落盘(无对象可回填)。
2077
2195
  if (waitingSteps.length === 0) {
2078
- console.error(`没有处于等待状态的步骤(阶段 ${stageName} 当前无 waiting 步骤,--continue 无目标)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
2079
- process.exit(1)
2196
+ console.log(`ℹ️ 当前无等待中的步骤,--continue 幂等跳过(${answersSpec != null ? '--answers 未回填任何答案' : '--answer 未落盘'})。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
2197
+ const firstActive = stageData.steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked')
2198
+ const firstPending = stageData.steps.findIndex(s => s.status === 'pending')
2199
+ return { stageCompleted: false, currentIdx: firstActive, nextPendingIdx: firstPending }
2200
+ }
2201
+
2202
+ // 有 waiting 步骤时 answer 仍是必填(用法错 exit 2)——放在幂等分支之后:裸 --continue
2203
+ // 无等待对象时幂等放行(FR-05),有等待对象时缺答案照旧硬拦(回填语义不容空答案)。
2204
+ if (!answer && answersSpec == null) {
2205
+ console.error('❌ --continue 需要 --answer 参数(批量回填用 --answers k1=v1,k2=v2)')
2206
+ process.exit(2) // 用法错 → exit 2
2207
+ }
2208
+
2209
+ // ── --answers 批量回填(2026-10-21-protocol-tax task-04 / FR-04)──
2210
+ // 一次调用回填多个 waiting 步骤:k=步骤名或 1-based 序号,值含逗号用引号 k="v,含逗号"。
2211
+ // 契约:全键命中才动状态——任一键未命中/重复命中同一 步骤,整单拒绝 exit 1 并列出可用键
2212
+ //(禁止部分回填:半程状态比一次可重试的失败更难收拾)。
2213
+ // 实现:校验通过后按序号升序逐个递归单答路径(--from-step <序号> + --answer),记录/回当前步
2214
+ // 语义/收尾 gate/回放全部复用单答实现——批路径不复制状态机逻辑。
2215
+ if (answersSpec != null) {
2216
+ const pairs = parseAnswersSpec(answersSpec)
2217
+ if (!pairs) {
2218
+ console.error('❌ --answers 格式无法解析(期望 k1=v1,k2=v2 形态,值含逗号用引号 k="v,含逗号")')
2219
+ process.exit(2) // 用法错 → exit 2
2220
+ }
2221
+ if (answer) {
2222
+ console.error('❌ --answers 与 --answer 互斥(批量回填与单步回答是两条路径,不要同时给)')
2223
+ process.exit(2)
2224
+ }
2225
+ const targets = []
2226
+ const unmatched = []
2227
+ for (const { key, value } of pairs) {
2228
+ const idx = /^\d+$/.test(key)
2229
+ ? parseInt(key, 10) - 1
2230
+ : stageData.steps.findIndex(s => s.name === key)
2231
+ const step = idx >= 0 ? stageData.steps[idx] : null
2232
+ if (!step || step.status !== 'waiting' || targets.some(t => t.idx === idx)) unmatched.push(key)
2233
+ else targets.push({ idx, value })
2234
+ }
2235
+ if (unmatched.length > 0) {
2236
+ console.error(`❌ --answers 键未命中等待中的步骤:${unmatched.join('、')}——整单拒绝,未回填任何步骤。`)
2237
+ console.error(` 可用键(步骤名或 1-based 序号):${waitingSteps.map(ws => `${ws.idx + 1}="${ws.name}"`).join('、')}`)
2238
+ process.exit(1)
2239
+ }
2240
+ console.log(`📦 --answers 批量回填开始:${targets.length} 个等待步骤(升序逐个回填)`)
2241
+ let lastResult = null
2242
+ for (const t of [...targets].sort((a, b) => a.idx - b.idx)) {
2243
+ lastResult = await continueStep(pm, progress, stageName, cwd, t.value, {
2244
+ ...options,
2245
+ answersSpec: null,
2246
+ fromStep: String(t.idx + 1),
2247
+ })
2248
+ }
2249
+ console.log(`📦 --answers 批量回填完成:${targets.length} 步`)
2250
+ return lastResult
2080
2251
  }
2081
2252
 
2082
2253
  // --from-step 支持在多个 waiting 时指定恢复哪一个(避免被迫 --reset 破坏性重置)。
package/src/run/gates.js CHANGED
@@ -28,6 +28,7 @@
28
28
  * 不搬:ensureDepsFreshness(execute 入口 deps 自检,调用方 runStage 非 completeStep,归属未来 execute-handler)
29
29
  */
30
30
  import { basename, join, relative, dirname } from 'node:path'
31
+ import { timedAsync } from '../timing.js'
31
32
  import { runtimeStatePath } from '../runtime-paths.js'
32
33
  import { createHash } from 'node:crypto'
33
34
  import { execFileSync } from 'node:child_process'
@@ -1227,6 +1228,29 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1227
1228
  printVerifyTestCheck(testCheck)
1228
1229
  _obsJournal('green-cache', true, null)
1229
1230
  } else {
1231
+ // ── 失败签名复用(2026-10-21-verify-reuse-large-repo FR-03):真跑前先咨询上次失败──
1232
+ // 同签名(代码指纹×豁免面×快照口径全等)+ 30min 窗口内 → 不重跑,合成 failed 照常阻断
1233
+ // (dsh 0ff6f1c8 取证:文档门修复循环里每轮 --done 都整轮重测同签名失败 10~13.5 分钟)。
1234
+ // fail-closed:任一判据不可得/失配/TTL 过期 → 照旧真跑。RERUN 逃生阀与 noAI 闸同阀对称。
1235
+ let _failedReuse = null
1236
+ try {
1237
+ const { consultFailedScanReuse, synthesizeReusedFailedCheck } = await import('./verify-quality-scan.js')
1238
+ const consult = consultFailedScanReuse({
1239
+ specBase, cwd, changeName,
1240
+ plannedSnapshot: Boolean(verifyGateSnap),
1241
+ forceRerun: Boolean(process.env.SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN),
1242
+ })
1243
+ if (consult.reuse) _failedReuse = synthesizeReusedFailedCheck(consult.record)
1244
+ else if (consult.reason && consult.reason !== 'no-failed-record') {
1245
+ _obsJournal('quality-scan-failed', false, consult.reason)
1246
+ }
1247
+ } catch { /* 咨询异常 → 真跑(复用是优化不是正确性依赖) */ }
1248
+ if (_failedReuse) {
1249
+ testCheck = _failedReuse
1250
+ console.log(`\n♻️ Verify 测试对账:失败签名复用——上次实测 failed 且签名未变,免重跑(阻断照常,出路见下)`)
1251
+ printVerifyTestCheck(testCheck)
1252
+ _obsJournal('quality-scan-failed', true, null)
1253
+ } else {
1230
1254
  // 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
1231
1255
  console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
1232
1256
  // restrictFiles 接线(2026-09-26-verify-gate-restrictfiles,评审 P2 留痕缺口的清偿):
@@ -1287,6 +1311,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
1287
1311
  }
1288
1312
  }
1289
1313
  } catch { /* auto-bind fail-soft */ }
1314
+ } // 失败签名复用分支到此为止——下方记账层对 failed 复用结果是 no-op(passed 守卫)
1290
1315
  // P2 记账(fail-closed 层②:只有通过结果落账本——失败永不缓存,修复后重跑才能记)
1291
1316
  if (!traceHasActiveRows) {
1292
1317
  try {
@@ -1916,7 +1941,11 @@ function readDesignOwnFiles(specBase, changeName) {
1916
1941
  * null = 全部通过,调用方继续自管收尾(如下一步提示);
1917
1942
  * 非 null = gate/handler 失败已 rollback,调用方直接 return。
1918
1943
  */
1919
- export async function completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText, ctx = null }) {
1944
+ export async function completeStageGates(args) {
1945
+ return timedAsync('gates', () => completeStageGatesImpl(args))
1946
+ }
1947
+
1948
+ async function completeStageGatesImpl({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText, ctx = null }) {
1920
1949
  // 整体 try/catch(task-04 / review-2026-08-09 #2):收尾段(execute 预检 + scan handler + validate* +
1921
1950
  // auxiliary 重置 + runStageCompletionGates)任一段抛非结构化异常 → rollbackCompletionAndReturn
1922
1951
  // (回滚 in-progress + 落盘 + 返回未完成对象),不冒顶 exit 1。正常的 _scanResult/_gateEarlyReturn
package/src/run/prompt.js CHANGED
@@ -19,6 +19,7 @@
19
19
  * - loadModuleContextIndex/buildModuleContextInjection 内 require('fs'/'path') 改顶部静态 import
20
20
  */
21
21
  import { basename, dirname, join } from 'node:path'
22
+ import { timedAsync } from '../timing.js'
22
23
  import { runtimeStatePath } from '../runtime-paths.js'
23
24
  import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, rmSync } from 'node:fs'
24
25
  import { createHash } from 'node:crypto'
@@ -921,6 +922,10 @@ async function recordPromptInjectLedger({ runtimeRoot, changeName, stageName, fi
921
922
  }
922
923
 
923
924
  export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null, autoMeta = null) {
925
+ return timedAsync('render', () => outputStepImpl(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts, prevStepAnswer, waitHistory, autoMeta))
926
+ }
927
+
928
+ async function outputStepImpl(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null, autoMeta = null) {
924
929
  const step = steps[stepIndex]
925
930
  const total = steps.length
926
931
  // ── 越界防御 ──
@@ -941,7 +946,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
941
946
  // 网络阻塞;未连接平台静默 / quick 会话降级跳过 / 8s 熔断 / 失败只 warn,契约全在
942
947
  // triggerStepStartSync 内(shared.js docstring 即本接线点的说明),Node 事件循环保证在飞
943
948
  // POST 落地后进程才退出。
944
- triggerStepStartSync(cwd, changeName, platformOpts)
949
+ // 例外(2026-10-21-protocol-tax task-06 / FR-06):status 是只读查看阶段,不推——消费仓
950
+ // 实测该推送(进程退出等在飞 POST)占 status 命令耗时的大头(10-20s 量级,平台慢时更甚);
951
+ // 查看态无步骤起点语义,平台停滞信号的新鲜度交给写命令。
952
+ if (stageName !== 'status') triggerStepStartSync(cwd, changeName, platformOpts)
945
953
 
946
954
  // ── 阶段产物模板落盘(R16 减负批次 2026-09-24):模板出上下文——prompt 只留指针,模板
947
955
  // 幂等落盘 <specBase>/.runtime/templates/ 供 agent 写产物前 Read 一次。best-effort:
@@ -358,8 +358,10 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
358
358
  // 无 /g 标志无状态,提级零行为差):
359
359
  // ① 路径段匹配:任意层出现 src / test / tests / __tests__ 段(src-guide 等长名不误蹭——按段全等)
360
360
  // ② 代码扩展名兜底:src/test 目录约定外的代码(backend/app/**.py、scripts/*.go 等)
361
- const CODE_PATH_SEGMENT_RE = /(^|\/)(src|tests?|__tests__)(\/|$)/
362
- const CODE_EXTENSION_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|py|pyw|go|rs|java|kt|kts|rb|php|cs|c|h|cpp|cc|hpp|swift|scala|groovy|vue|svelte)$/
361
+ // export(2026-10-23-turbo-docs-auto):flow.js docs-auto 档纯文档面判定同源消费——
362
+ // 分类器单一源(宁可多跑不可漏跑口径),本体零变化
363
+ export const CODE_PATH_SEGMENT_RE = /(^|\/)(src|tests?|__tests__)(\/|$)/
364
+ export const CODE_EXTENSION_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|py|pyw|go|rs|java|kt|kts|rb|php|cs|c|h|cpp|cc|hpp|swift|scala|groovy|vue|svelte)$/
363
365
 
364
366
  // 测试文件判定:目录段(test/tests/__tests__——比 quick-gate-profile 的 TEST_DIR_RE 多认裸
365
367
  // test/ 段,本仓 test/ 下即有无 .test. 后缀的 .mjs)+ basename test_ 前缀 + .test./_test./.spec.
package/src/run/shared.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * 本模块仅 re-export,run/ 层现有调用方路径与行为不变。
12
12
  */
13
13
  import { basename, join, resolve, dirname, sep } from 'node:path'
14
+ import { timedAsync } from '../timing.js'
14
15
  import { runtimeTempPath } from '../runtime-paths.js'
15
16
  import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, appendFileSync, realpathSync } from 'node:fs'
16
17
  import { fileURLToPath } from 'node:url'
@@ -981,6 +982,10 @@ function warnPlatformNotConnectedDaily(cwd, platformOpts = {}) {
981
982
  }
982
983
 
983
984
  export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {}) {
985
+ return timedAsync('sync', () => triggerSyncImpl(cwd, changeName, platformOpts, opts))
986
+ }
987
+
988
+ async function triggerSyncImpl(cwd, changeName, platformOpts = {}, opts = {}) {
984
989
  // ── 后台异步化(2026-09-20 用户反馈:--done 被网络尾巴拖住分钟级)──
985
990
  // 默认把同步整体挪进 detached 后台子进程:17 个调用点全部 fire-and-forget,但进程
986
991
  // 退出要等事件循环排空,在飞 fetch 拖住命令返回。回落 inline 的三条件:
@@ -1066,9 +1071,13 @@ export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {})
1066
1071
  * @returns {{confirmed: boolean, delegated?: boolean, skipped?: string, result?: object}}
1067
1072
  */
1068
1073
  export async function triggerSyncTerminalStrict(cwd, changeName, platformOpts = {}, opts = {}) {
1074
+ return timedAsync('sync', () => triggerSyncTerminalStrictImpl(cwd, changeName, platformOpts, opts))
1075
+ }
1076
+
1077
+ async function triggerSyncTerminalStrictImpl(cwd, changeName, platformOpts = {}, opts = {}) {
1069
1078
  // quick 会话无实体变更目录(无归档/删除终态)——沿用既有 best-effort 语义
1070
1079
  if (changeName && QUICK_SID_RE.test(changeName)) {
1071
- await triggerSync(cwd, changeName, platformOpts, opts)
1080
+ await triggerSyncImpl(cwd, changeName, platformOpts, opts)
1072
1081
  return { confirmed: false, delegated: true }
1073
1082
  }
1074
1083
  const syncMod = await import('../sync.js')
@@ -1139,6 +1148,10 @@ export async function triggerSyncTerminalStrict(cwd, changeName, platformOpts =
1139
1148
  * 每任务上报指引)覆盖。
1140
1149
  */
1141
1150
  export async function triggerStepStartSync(cwd, changeName, platformOpts = {}, opts = {}) {
1151
+ return timedAsync('sync', () => triggerStepStartSyncImpl(cwd, changeName, platformOpts, opts))
1152
+ }
1153
+
1154
+ async function triggerStepStartSyncImpl(cwd, changeName, platformOpts = {}, opts = {}) {
1142
1155
  try {
1143
1156
  // 无 changeName(渲染层个别路径/测试 fixture)直接静默——sync(null) 只会打 warn 噪音
1144
1157
  if (!changeName) return
@@ -1226,6 +1239,10 @@ export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {})
1226
1239
  * 供 index.js 在 stage 命令(run/--done/archive)与 approve 决策点注入一行调用。
1227
1240
  */
1228
1241
  export async function triggerPullActiveChange(cwd, platformOpts = {}) {
1242
+ return timedAsync('sync', () => triggerPullActiveChangeImpl(cwd, platformOpts))
1243
+ }
1244
+
1245
+ async function triggerPullActiveChangeImpl(cwd, platformOpts = {}) {
1229
1246
  // 平台模式走自己的链路,跳过;自指回环(repo-native junction)按本地模式处理
1230
1247
  if (isPlatformMode(platformOpts, cwd)) return
1231
1248
  // 先检查是否连接平台:未连接直接 return,避免 _ensureDB 在无 local.yaml 的 cwd 创建空 DB 污染
package/src/run/stage.js CHANGED
@@ -100,29 +100,32 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
100
100
  const effectiveChange = changeName
101
101
  const { WorktreeManager } = await import('../worktree.js')
102
102
  const wm = new WorktreeManager({ cwd })
103
- const existingMeta = wm.getMeta(effectiveChange)
104
- if (existingMeta) {
105
- console.log(`🔗 worktree 已存在: ${existingMeta.worktreePath} (${existingMeta.mode})`)
103
+ // ensure(2026-10-21-brainstorm-proto-worktree):meta 已登记即复用——头脑风暴原型期
104
+ // 早建的 worktree / native 复用形态与 execute 首建走同一幂等入口,原 getMeta 短路语义收敛于此。
105
+ let wtResult
106
+ try {
107
+ wtResult = wm.ensure(effectiveChange, { adoptBranch: !!(quickOpts && quickOpts.adoptBranch) })
108
+ } catch (e) {
109
+ console.error(`❌ worktree 创建失败: ${e.message}`)
110
+ // 修复建议不再无条件推荐 git branch -D(坑 worktree-user-branch-conflict,2026-08-24:
111
+ // 同名分支可能是用户有意创建的——分支冲突时 create 报错已带三选一决策菜单,删不删
112
+ // 交由菜单/人工判断;doctor 的分支删除也有 review 锚点 + 无库保守双保护)。
113
+ console.error(` 修复建议:`)
114
+ console.error(` 1. 同名分支冲突/收编:按上方报错内三选一菜单处置(遗留分支确认作废后才删;用户指定分支用 --adopt-branch 收编)`)
115
+ console.error(` 2. worktree 状态异常:sillyspec worktree doctor --fix(分支删除有 review 锚点保护,不会误删在用分支)`)
116
+ console.error(` 3. 目录残留:确认无未提交改动后清理 .sillyspec/.runtime/worktrees/${effectiveChange}/ 再重试`)
117
+ process.exit(1)
118
+ }
119
+ if (wtResult.existing) {
120
+ console.log(`🔗 worktree 已存在: ${wtResult.worktreePath} (${wtResult.mode})`)
106
121
  } else {
107
- try {
108
- const result = wm.create(effectiveChange, { adoptBranch: !!(quickOpts && quickOpts.adoptBranch) })
109
- console.log(`🔗 worktree 已创建: ${result.worktreePath} (分支: ${result.branch}, 模式: ${result.mode}${result.adoptedBranch ? ', 收编既有分支' : ''})`)
110
- } catch (e) {
111
- console.error(`❌ worktree 创建失败: ${e.message}`)
112
- // 修复建议不再无条件推荐 git branch -D(坑 worktree-user-branch-conflict,2026-08-24:
113
- // 同名分支可能是用户有意创建的——分支冲突时 create 报错已带三选一决策菜单,删不删
114
- // 交由菜单/人工判断;doctor 的分支删除也有 review 锚点 + 无库保守双保护)。
115
- console.error(` 修复建议:`)
116
- console.error(` 1. 同名分支冲突/收编:按上方报错内三选一菜单处置(遗留分支确认作废后才删;用户指定分支用 --adopt-branch 收编)`)
117
- console.error(` 2. worktree 状态异常:sillyspec worktree doctor --fix(分支删除有 review 锚点保护,不会误删在用分支)`)
118
- console.error(` 3. 目录残留:确认无未提交改动后清理 .sillyspec/.runtime/worktrees/${effectiveChange}/ 再重试`)
119
- process.exit(1)
120
- }
122
+ console.log(`🔗 worktree 已创建: ${wtResult.worktreePath} (分支: ${wtResult.branch}, 模式: ${wtResult.mode}${wtResult.adoptedBranch ? ', 收编既有分支' : ''})`)
121
123
  }
122
124
  // 入口 deps 自检(D-002):已存在 worktree(create short-circuit 不供给)时重供给
123
- if (existingMeta) {
125
+ //(existing 标志置 undefined:JSON.stringify 丢弃,不随 deps 写回污染 meta.json)
126
+ if (wtResult.existing) {
124
127
  const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
125
- await ensureDepsFreshness(cwd, effectiveChange, execSpecBase, existingMeta)
128
+ await ensureDepsFreshness(cwd, effectiveChange, execSpecBase, { ...wtResult, existing: undefined })
126
129
  }
127
130
 
128
131
  // ── 跨仓仓 worktree 隔离(坑 cross-repo-no-worktree-isolation,2026-08-27 用户实证)──