sillyspec 3.28.7 → 3.28.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.claude/skills/sillyspec-quick/SKILL.md +2 -0
  2. package/.husky/pre-push +3 -1
  3. package/AGENTS.md +3 -2
  4. package/README.md +35 -11
  5. package/SKILL.md +24 -10
  6. package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
  7. package/package.json +1 -1
  8. package/src/agent-session-log.js +446 -35
  9. package/src/change-risk-profile.js +20 -0
  10. package/src/config-cat.js +6 -3
  11. package/src/config-schema.js +32 -0
  12. package/src/constants.js +3 -0
  13. package/src/db.js +23 -6
  14. package/src/decision-distill.js +15 -3
  15. package/src/design-facts.js +33 -3
  16. package/src/docs-check.js +137 -12
  17. package/src/docs-gate.js +108 -6
  18. package/src/doctor-diagnostics.js +243 -9
  19. package/src/friction-ledger.js +171 -0
  20. package/src/friction-tally.js +7 -2
  21. package/src/git-helper.js +6 -4
  22. package/src/hooks/worktree-guard.js +72 -30
  23. package/src/index.js +356 -81
  24. package/src/knowledge-classify.js +363 -0
  25. package/src/knowledge-hits.js +74 -0
  26. package/src/knowledge-stats.js +180 -0
  27. package/src/local-detect.js +30 -15
  28. package/src/local-register.js +13 -4
  29. package/src/mcp-server.js +38 -8
  30. package/src/progress/change-registry.js +224 -43
  31. package/src/progress/consistency-doctor.js +11 -4
  32. package/src/progress/shared.js +3 -2
  33. package/src/progress/stage-machine.js +3 -1
  34. package/src/progress/step-store.js +30 -17
  35. package/src/progress.js +123 -8
  36. package/src/quick-gate-profile.js +251 -0
  37. package/src/quicklog.js +199 -32
  38. package/src/review-dispatch.js +2 -1
  39. package/src/run/command.js +124 -15
  40. package/src/run/complete-handlers.js +514 -24
  41. package/src/run/complete.js +82 -25
  42. package/src/run/concurrent-detect.js +99 -1
  43. package/src/run/gate-snapshot.js +115 -5
  44. package/src/run/gates.js +85 -14
  45. package/src/run/prompt.js +143 -6
  46. package/src/run/quick-audit.js +136 -15
  47. package/src/run/shared.js +174 -20
  48. package/src/run/stage.js +6 -0
  49. package/src/run/verify-quality-scan.js +14 -3
  50. package/src/scan-diff.js +61 -30
  51. package/src/scan-postcheck.js +74 -0
  52. package/src/scan-refresh.js +477 -0
  53. package/src/scan-staleness.js +25 -13
  54. package/src/scope-audit.js +365 -35
  55. package/src/spec-sync.js +19 -14
  56. package/src/stage-contract-spec.js +4 -3
  57. package/src/stage-contract.js +185 -2
  58. package/src/stage-review.js +29 -12
  59. package/src/stages/brainstorm.js +7 -1
  60. package/src/stages/execute.js +91 -4
  61. package/src/stages/knowledge.js +52 -3
  62. package/src/stages/plan-postcheck.js +57 -12
  63. package/src/stages/quick.js +1 -1
  64. package/src/stages/verify.js +1 -0
  65. package/src/sync-noise.js +114 -6
  66. package/src/sync.js +201 -11
  67. package/src/task-review.js +333 -27
  68. package/src/verify-facts-schema.js +10 -3
  69. package/src/verify-postcheck.js +41 -6
  70. package/src/verify-probes.js +348 -7
  71. package/src/worktree-apply.js +594 -62
  72. package/src/worktree-cross.js +12 -18
  73. package/src/worktree-deps.js +100 -2
  74. package/src/worktree.js +255 -30
  75. package/src/wt-commit.js +128 -0
  76. package/templates/agents-instruction.md +2 -2
  77. package/templates/prompts/testcase-design.md +2 -1
  78. package/templates/prompts/verify-probes.md +4 -1
  79. package/sa-rename-probe/src/new.js +0 -4
package/src/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * 只负责两件事:init(安装命令模板)和 setup(安装 MCP 工具)。
7
7
  * 状态管理通过 sillyspec.db(SQLite)完成,使用 `sillyspec progress` 命令。
8
8
  */
9
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
9
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, unlinkSync } from 'fs';
10
10
  import { writeAtomicSync } from './fs-atomic.js';
11
11
  import { basename, dirname, extname, join, resolve, isAbsolute, sep, relative } from 'path';
12
12
  import { safeGit, git } from './git-helper.js';
@@ -60,6 +60,9 @@ SillySpec CLI — 规范驱动开发工具包
60
60
  --force-rescan 覆盖已有 scan 文档保护
61
61
  --diff [--base <commit>] [--full] [--report] scan 文档 vs 源码漂移清单(纯只读)
62
62
  (子命令: sillyspec scan diff 等价 --diff)
63
+ refresh [--project <名>] [--force] [--json] 增量刷新①拍:门控+受影响文档手术工单
64
+ refresh --done [--docs a.md,b.md] [--force] 增量刷新②拍:内容比对+per-doc 基线推进+postcheck
65
+ (检出极限:refresh 只覆盖带 file:line 引用的检出项,不声称文档与源码一致)
63
66
  archive: --confirm 归档确认(必须)
64
67
  auto: --mode <模式> 显式指定流程模式
65
68
  平台: --runtime-root <path> / --workspace-id <id> / --scan-run-id <id>
@@ -70,6 +73,9 @@ SillySpec CLI — 规范驱动开发工具包
70
73
  check | repair [--apply] | validate | reset [--stage X] |
71
74
  batch --total N --completed M [--failed F] [--skipped K] | batch --status
72
75
 
76
+ sillyspec wt-commit [--change <名>] -m <信息> [--pathspec-from-file <f>] -- <path...>
77
+ worktree 内 per-task 串行化提交(文件锁排队 + 强制显式 pathspec,
78
+ 防 add -A 卷入并行兄弟 WIP;worktree 内可省 --change,从 cwd 推断)
73
79
  sillyspec worktree <cmd> git worktree 隔离管理(execute 阶段相关)
74
80
  create <change> [--base <branch>] 创建隔离 worktree
75
81
  apply <change> [--check-only] 校验并应用变更到主工作区
@@ -198,7 +204,7 @@ async function main() {
198
204
 
199
205
  // E22:重路径统一加载(轻路径 --version/help 已早退,未付此税)。
200
206
  const { ProgressManager, resolvePlatformSpecDir, resolvePlatformOpts } = await import('./progress.js');
201
- const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift } = await import('./run/shared.js');
207
+ const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift, detectCwdInsideWorktree } = await import('./run/shared.js');
202
208
 
203
209
  // 解析全局选项
204
210
  let json = false;
@@ -221,13 +227,13 @@ async function main() {
221
227
  json = true;
222
228
  } else if (args[i] === '--save') {
223
229
  saveWorkflowRunFlag = true;
224
- } else if (args[i] === '--dir' && args[i + 1]) {
230
+ } else if (args[i] === '--dir' && args[i + 1] && !args[i + 1].startsWith('--')) {
225
231
  targetDir = resolve(args[i + 1]);
226
232
  i++;
227
- } else if (args[i] === '--spec-dir' && args[i + 1]) {
233
+ } else if (args[i] === '--spec-dir' && args[i + 1] && !args[i + 1].startsWith('--')) {
228
234
  specDir = resolve(args[i + 1]);
229
235
  i++;
230
- } else if (args[i] === '--tool' && args[i + 1]) {
236
+ } else if (args[i] === '--tool' && args[i + 1] && !args[i + 1].startsWith('--')) {
231
237
  // 多值支持:逗号分隔(--tool claude,codex)与重复 flag(--tool a --tool b)都收集进数组
232
238
  for (const v of args[i + 1].split(',')) {
233
239
  const t = v.trim();
@@ -237,18 +243,21 @@ async function main() {
237
243
  } else if (args[i] === '--no-skills') {
238
244
  // init 专属:跳过 skills 复制段(吞进变量,不透传 filteredArgs)
239
245
  noSkills = true;
240
- } else if (args[i] === '--workspace-id' && args[i + 1]) {
246
+ } else if (args[i] === '--workspace-id' && args[i + 1] && !args[i + 1].startsWith('--')) {
241
247
  // 平台模式专属 flag(init 落平台指针用;--runtime-root/--scan-run-id 保持在
242
248
  // filteredArgs 由 runCommand 解析,init 侧只需要 workspaceId 作为平台信号 + runtimeRoot)
243
249
  platformWorkspaceId = args[i + 1];
244
250
  filteredArgs.push(args[i], args[i + 1]);
245
251
  i++;
246
- } else if (args[i] === '--runtime-root' && args[i + 1]) {
252
+ } else if (args[i] === '--runtime-root' && args[i + 1] && !args[i + 1].startsWith('--')) {
247
253
  platformRuntimeRoot = resolve(args[i + 1]);
248
254
  filteredArgs.push(args[i], args[i + 1]);
249
255
  i++;
250
256
  } else if (args[i] === '--interactive' || args[i] === '-i') {
251
257
  interactive = true;
258
+ } else if (args[i] === '--allow-worktree-cwd') {
259
+ // worktree cwd 硬拦逃生门:默认阻止在 .sillyspec/.runtime/worktrees/* 内跑 CLI,
260
+ // 此 flag 显式放行(坑 worktree-cwd-silent-split)
252
261
  } else if (args[i] === '--list' || args[i] === '-l') {
253
262
  filteredArgs.push('--list');
254
263
  } else {
@@ -311,6 +320,22 @@ async function main() {
311
320
  process.exit(1);
312
321
  }
313
322
 
323
+ // ── worktree cwd 硬拦(坑 worktree-cwd-silent-split,2026-08-29 用户实证)──
324
+ // Bash cwd 残留让 agent 两次误在 worktree 内跑 sillyspec:status 类完全静默、进度写进
325
+ // worktree 副本 .sillyspec 与主仓进度库分裂。守卫在命令分发前拦截,--dir 显式指向主仓
326
+ // 或 --allow-worktree-cwd 逃生门放行;wt-commit 命令豁免(子代理 workdir=worktree 内合法)。
327
+ if (!args.includes('--allow-worktree-cwd') && command !== 'wt-commit') {
328
+ const guardDir = args.includes('--dir') ? dir : process.cwd();
329
+ const wtInfo = detectCwdInsideWorktree(guardDir);
330
+ if (wtInfo) {
331
+ console.error(`\n❌ 当前在隔离 worktree 内(${wtInfo.changeName}):${guardDir}`);
332
+ console.error(` 主仓根:${wtInfo.mainRepoRoot}`);
333
+ console.error(' SillySpec 进度库只存在于主仓,worktree 内跑会产生分裂进度(坑 worktree-cwd-silent-split)。');
334
+ console.error(' 修复:cd 到主仓根后重跑,或用 --dir <主仓根>,或用 --allow-worktree-cwd 强制放行。');
335
+ process.exit(2);
336
+ }
337
+ }
338
+
314
339
  switch (command) {
315
340
  case 'init':
316
341
  await (await import('./init.js')).cmdInit(dir, { tool, tools: toolValues.length > 0 ? toolValues : null, interactive, specDir, noSkills, platformOpts: (platformWorkspaceId || platformRuntimeRoot) ? { workspaceId: platformWorkspaceId, runtimeRoot: platformRuntimeRoot } : null });
@@ -324,10 +349,10 @@ async function main() {
324
349
  const progDir = specDir ? dir : resolveEffectiveDir(dir);
325
350
  const subCommand = filteredArgs[1];
326
351
  const stageIdx = filteredArgs.indexOf('--stage');
327
- const stage = stageIdx >= 0 && filteredArgs[stageIdx + 1] ? filteredArgs[stageIdx + 1] : null;
352
+ const stage = stageIdx >= 0 && filteredArgs[stageIdx + 1] && !String(filteredArgs[stageIdx + 1]).startsWith("--") ? filteredArgs[stageIdx + 1] : null;
328
353
  // 解析 --change 参数
329
354
  const progChangeIdx = args.indexOf('--change');
330
- const progChangeName = progChangeIdx >= 0 && args[progChangeIdx + 1] ? args[progChangeIdx + 1] : null;
355
+ const progChangeName = progChangeIdx >= 0 && args[progChangeIdx + 1] && !String(args[progChangeIdx + 1]).startsWith("--") ? args[progChangeIdx + 1] : null;
331
356
  // 与 run 入口同源消毒(防路径穿越;progress 下游 completeStage 写 history 文件名拼 change 名)
332
357
  if (progChangeName) assertSafeChangeName(progChangeName, '--change 变更名');
333
358
 
@@ -469,7 +494,7 @@ async function main() {
469
494
  // 只读查询,不依赖 worktree、不写状态(D-002@v1)。
470
495
  const gateStage = filteredArgs[1];
471
496
  const gateChangeIdx = args.indexOf('--change');
472
- const gateChange = gateChangeIdx >= 0 && args[gateChangeIdx + 1] ? args[gateChangeIdx + 1] : null;
497
+ const gateChange = gateChangeIdx >= 0 && args[gateChangeIdx + 1] && !String(args[gateChangeIdx + 1]).startsWith("--") ? args[gateChangeIdx + 1] : null;
473
498
  if (!gateStage || gateStage.startsWith('-') || !gateChange) {
474
499
  console.error('用法: sillyspec gate <stage> --change <name> [--json]\n stage: brainstorm | plan | execute | verify | archive | ...');
475
500
  process.exit(2);
@@ -529,7 +554,7 @@ async function main() {
529
554
  // 单项事实核验(machine-interface v1):查询变更某一 facet 的真实状态,返回结构化 data。
530
555
  const facet = filteredArgs[1];
531
556
  const deriveChangeIdx = args.indexOf('--change');
532
- const deriveChange = deriveChangeIdx >= 0 && args[deriveChangeIdx + 1] ? args[deriveChangeIdx + 1] : null;
557
+ const deriveChange = deriveChangeIdx >= 0 && args[deriveChangeIdx + 1] && !String(args[deriveChangeIdx + 1]).startsWith("--") ? args[deriveChangeIdx + 1] : null;
533
558
  if (!facet || facet.startsWith('-') || !deriveChange) {
534
559
  console.error('用法: sillyspec derive <facet> --change <name> [--json]\n facet: execute-evidence | verify-test | task-reviews | artifacts');
535
560
  process.exit(2);
@@ -587,7 +612,7 @@ async function main() {
587
612
  }
588
613
  const rwVal = (flag) => {
589
614
  const i = filteredArgs.indexOf(flag);
590
- return i >= 0 && filteredArgs[i + 1] ? filteredArgs[i + 1] : null;
615
+ return i >= 0 && filteredArgs[i + 1] && !String(filteredArgs[i + 1]).startsWith("--") ? filteredArgs[i + 1] : null;
591
616
  };
592
617
  const rwChange = rwVal('--change');
593
618
  const rwTask = rwVal('--task');
@@ -645,6 +670,63 @@ async function main() {
645
670
  }
646
671
  break;
647
672
  }
673
+ case 'task': {
674
+ // task 级进行中状态标记(2026-09-12 驾驭第十四批②,用户实证「中断续跑时半成品无主,
675
+ // 主代理接管审查负担重」):开工/完工各一条命令落 .runtime/task-progress/<change>/<task-NN>.json
676
+ // (startedAt + note),list 带陈旧标红(>2h 视为中断遗留——续跑时主代理据此点名接管审查,
677
+ // 不再靠翻 git 工作区猜半成品)。轻量 marker:不进 DB、不设门禁(状态可见性工具,非流程卡点)。
678
+ const taskSub = filteredArgs[1];
679
+ if (!taskSub || ['help', '--help', '-h'].includes(taskSub)) {
680
+ console.log(`用法:
681
+ sillyspec task start --change <名> --task task-NN [--note "一句话在做什么"]
682
+ sillyspec task finish --change <名> --task task-NN
683
+ sillyspec task list --change <名>
684
+ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标红 >2h 中断遗留(接管审查点名用)`);
685
+ break;
686
+ }
687
+ const tChangeIdx = filteredArgs.indexOf('--change');
688
+ const tChange = tChangeIdx >= 0 && filteredArgs[tChangeIdx + 1] && !String(filteredArgs[tChangeIdx + 1]).startsWith("--") ? filteredArgs[tChangeIdx + 1] : null;
689
+ const tTaskIdx = filteredArgs.indexOf('--task');
690
+ const tTask = tTaskIdx >= 0 && filteredArgs[tTaskIdx + 1] && !String(filteredArgs[tTaskIdx + 1]).startsWith("--") ? filteredArgs[tTaskIdx + 1] : null;
691
+ const tNoteIdx = filteredArgs.indexOf('--note');
692
+ const tNote = tNoteIdx >= 0 && filteredArgs[tNoteIdx + 1] && !String(filteredArgs[tNoteIdx + 1]).startsWith("--") ? filteredArgs[tNoteIdx + 1] : null;
693
+ if (!tChange) { console.error('❌ 缺 --change <变更名>'); process.exit(2); }
694
+ if (taskSub !== 'list') {
695
+ if (!tTask || !/^task-\d+$/.test(tTask)) { console.error('❌ --task 格式应为 task-NN'); process.exit(2); }
696
+ }
697
+ const tSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
698
+ const tRuntime = join(tSpecBase, '.runtime');
699
+ const tDir = join(tRuntime, 'task-progress', tChange);
700
+ if (taskSub === 'start') {
701
+ mkdirSync(tDir, { recursive: true });
702
+ writeFileSync(join(tDir, `${tTask}.json`), JSON.stringify({
703
+ task: tTask, change: tChange, note: tNote || '', startedAt: new Date().toISOString(),
704
+ }, null, 2) + '\n');
705
+ console.log(`📌 ${tTask} 已标记进行中${tNote ? '(' + tNote + ')' : ''}——完工时 sillyspec task finish --change ${tChange} --task ${tTask}`);
706
+ } else if (taskSub === 'finish') {
707
+ try { unlinkSync(join(tDir, `${tTask}.json`)); console.log(`✅ ${tTask} 进行中标记已清除(完工)`); }
708
+ catch { console.log(`ℹ️ ${tTask} 无进行中标记(已完工或从未 start)`); }
709
+ } else if (taskSub === 'list') {
710
+ if (!existsSync(tDir)) { console.log(`ℹ️ 无 ${tChange} 的进行中 task 标记`); break; }
711
+ const entries = readdirSync(tDir).filter(f => f.endsWith('.json'));
712
+ if (entries.length === 0) { console.log(`ℹ️ 无进行中标记(全部完工)`); break; }
713
+ console.log(`📋 ${tChange} 进行中 task(${entries.length} 个)——中断遗留接管审查点名用:`);
714
+ let stale = 0;
715
+ for (const f of entries.sort()) {
716
+ try {
717
+ const m = JSON.parse(readFileSync(join(tDir, f), 'utf8'));
718
+ const ageMin = Math.round((Date.now() - new Date(m.startedAt).getTime()) / 60000);
719
+ const isStale = ageMin > 120;
720
+ if (isStale) stale++;
721
+ console.log(` ${isStale ? '🔴' : '🟢'} ${m.task}: 开始于 ${m.startedAt}(${ageMin >= 60 ? Math.floor(ageMin / 60) + 'h' + ageMin % 60 + 'm' : ageMin + 'm'} 前)${m.note ? '——' + m.note : ''}${isStale ? ' ⚠️ 超过 2h,疑似中断遗留(接管者:对照 task 卡 + git 工作区半成品审查)' : ''}`);
722
+ } catch { console.log(` ⚠️ ${f} 标记损坏(忽略)`); }
723
+ }
724
+ if (stale > 0) console.log(` 共 ${stale} 个疑似中断遗留——主代理接管时逐一审查(半成品无主的痛点出口)`);
725
+ } else {
726
+ console.error(`❌ 未知子命令「${taskSub}」(start | finish | list)`); process.exit(2);
727
+ }
728
+ break;
729
+ }
648
730
  case 'backfill-reviews': {
649
731
  // 手动补 task(reopen execute / 直接实现)缺 review.json 的官方入口(坑 verify-archive-flow-pitfalls 坑2)。
650
732
  // 复用 generateTaskReviewDrafts(execute --done 同源兜底,幂等、fail-open):据 git diff base..head + working-tree
@@ -657,7 +739,7 @@ async function main() {
657
739
  // verdict——手算 hash、手抄 diff 清单是纯错误面,gate 校验的恰是这些字段。与草稿互补:
658
740
  // 草稿补「缺失」,adopt 修「已存在但 mechanics 错/缺」,verdict 原样保留。
659
741
  const brChangeIdx = args.indexOf('--change');
660
- const brChange = brChangeIdx >= 0 && args[brChangeIdx + 1] ? args[brChangeIdx + 1] : null;
742
+ const brChange = brChangeIdx >= 0 && args[brChangeIdx + 1] && !String(args[brChangeIdx + 1]).startsWith("--") ? args[brChangeIdx + 1] : null;
661
743
  const brAdopt = args.includes('--adopt');
662
744
  if (!brChange) {
663
745
  console.error('用法: sillyspec backfill-reviews --change <name> [--adopt] [--json] [--spec-dir <path>]\n 缺失 review.json → 生成 cannot_verify 草稿(解 archive 客观完成度阻断)\n --adopt → 已存在 review.json 的 base/head/changedFiles/diffPaths 等 mechanics 字段一键重算代填(verdict 保留;统一 commit 模式 diffPaths=task 卡 allowed_paths)');
@@ -739,7 +821,7 @@ async function main() {
739
821
  const wsArgs = filteredArgs.slice(2);
740
822
  const wsFlag = (name) => {
741
823
  const i = wsArgs.indexOf(name);
742
- return i >= 0 && wsArgs[i + 1] ? wsArgs[i + 1] : null;
824
+ return i >= 0 && wsArgs[i + 1] && !String(wsArgs[i + 1]).startsWith("--") ? wsArgs[i + 1] : null;
743
825
  };
744
826
  const { workspaceAdd, workspaceRemove, workspaceStatus } = await import('./workspace.js');
745
827
  if (wsSub === 'add') {
@@ -838,7 +920,9 @@ async function main() {
838
920
  const childArgs = m[1].trim().split(/\s+/);
839
921
  console.log(`🤖 next --apply 代跑:sillyspec ${childArgs.join(' ')}\n`);
840
922
  const { spawnSync } = await import('node:child_process');
841
- const r = spawnSync(process.execPath, [join(__dirname, '..', 'bin', 'sillyspec.js'), '--dir', dir, ...childArgs], { stdio: 'inherit' });
923
+ const { fileURLToPath } = await import('node:url');
924
+ // ESM 无 __dirname(ql-20260911-029:此处曾直接引用,--apply 常态路径必 ReferenceError)
925
+ const r = spawnSync(process.execPath, [fileURLToPath(new URL('../bin/sillyspec.js', import.meta.url)), '--dir', dir, ...childArgs], { stdio: 'inherit' });
842
926
  process.exit(r.status === null ? 1 : r.status);
843
927
  }
844
928
  if (json) {
@@ -868,7 +952,7 @@ async function main() {
868
952
  // 通用自动 git 在多会话仓是历史重灾区,故收窄到这一处。
869
953
  if (args.includes('--apply')) {
870
954
  const apChangeIdx = args.indexOf('--change');
871
- const apChange = apChangeIdx >= 0 && args[apChangeIdx + 1] ? args[apChangeIdx + 1] : null;
955
+ const apChange = apChangeIdx >= 0 && args[apChangeIdx + 1] && !String(args[apChangeIdx + 1]).startsWith("--") ? args[apChangeIdx + 1] : null;
872
956
  if (!apChange) {
873
957
  console.error('用法: sillyspec commit --apply --change <名>\n 只限归档语境(changes.status=archived 或 current_stage=archive)执行归档产物提交');
874
958
  process.exit(2);
@@ -947,7 +1031,7 @@ async function main() {
947
1031
  // --init 顺带生成 verify-result.md 七章节骨架(探针结果预填;结论留「待填」——gate 找
948
1032
  // 不到 PASS/FAIL 即判不过,骨架不能直接过门)。已存在不覆盖。
949
1033
  const vpChangeIdx = args.indexOf('--change');
950
- const vpChange = vpChangeIdx >= 0 && args[vpChangeIdx + 1] ? args[vpChangeIdx + 1] : null;
1034
+ const vpChange = vpChangeIdx >= 0 && args[vpChangeIdx + 1] && !String(args[vpChangeIdx + 1]).startsWith("--") ? args[vpChangeIdx + 1] : null;
951
1035
  const vpInit = args.includes('--init');
952
1036
  if (!vpChange) {
953
1037
  console.error('用法: sillyspec verify-probes --change <name> [--init] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 对账/删除对账)输出 markdown;--init 生成 verify-result.md 骨架(探针预填,已存在不覆盖)');
@@ -1031,6 +1115,18 @@ async function main() {
1031
1115
  console.log(`📄 已补齐缺失槽段(不触碰既有正文): ${vpAdded.added.join('、')}`)
1032
1116
  }
1033
1117
  }
1118
+ // 2026-09-14-acceptance-test-matrix:探针 7 验收×测试覆盖矩阵骨架幂等补段——
1119
+ // verify-result.md 已存在但缺「#### 探针 7」段且 applicable(有 TaskCard)时补矩阵
1120
+ // 骨架(学 backfillMissingEvidenceSlots 形态,不触既有正文;判定/证据槽 fail-closed
1121
+ // 由 verify 门禁负责)。本变更自举:verify-result.md 先于特性存在的存量走此补段。
1122
+ try {
1123
+ const { ensureAcceptanceMatrixSection } = await import('./verify-probes.js')
1124
+ const vpMatrix = ensureAcceptanceMatrixSection(vpReportPath, vpResult.probe7)
1125
+ if (vpMatrix.added) {
1126
+ try { await mirrorInitArtifact(vpReportPath, 'verify-result.md', readFileSync(vpReportPath, 'utf8')) } catch { /* 镜像失败不阻断 */ }
1127
+ console.log(`🧩 探针 7 验收×测试覆盖矩阵骨架段已补齐(判定=四选一/证据两槽待填,命中≠判定;不触碰既有正文): ${vpReportPath}${vpPlatformNote}`)
1128
+ }
1129
+ } catch { /* 矩阵补段失败不阻断 --init */ }
1034
1130
  // P0-1(noai-ir-roadmap §3)两件机械预填,均幂等 fail-soft:
1035
1131
  // ①决策追踪矩阵机械半边——D→FR→task 链自 decisions.md × tasks/*.md frontmatter
1036
1132
  // 结构化字段构建,Evidence/状态两列留 agent 逐格复核;
@@ -1071,9 +1167,9 @@ async function main() {
1071
1167
  // stage-reviews 路径)、--kill=清在途记录+平台处置指引(处置权在人)。薄壳:全部逻辑在
1072
1168
  // src/review-dispatch.js(可测核心),此处只做 flag 解析与退出码。
1073
1169
  const rdChangeIdx = args.indexOf('--change');
1074
- const rdChange = rdChangeIdx >= 0 && args[rdChangeIdx + 1] ? args[rdChangeIdx + 1] : null;
1170
+ const rdChange = rdChangeIdx >= 0 && args[rdChangeIdx + 1] && !String(args[rdChangeIdx + 1]).startsWith("--") ? args[rdChangeIdx + 1] : null;
1075
1171
  const rdStageIdx = args.indexOf('--stage');
1076
- const rdStage = rdStageIdx >= 0 && args[rdStageIdx + 1] ? args[rdStageIdx + 1] : null;
1172
+ const rdStage = rdStageIdx >= 0 && args[rdStageIdx + 1] && !String(args[rdStageIdx + 1]).startsWith("--") ? args[rdStageIdx + 1] : null;
1077
1173
  const rdStatus = args.includes('--status');
1078
1174
  const rdKill = args.includes('--kill');
1079
1175
  const rdMode = rdKill ? 'kill' : rdStatus ? 'status' : 'create';
@@ -1124,9 +1220,9 @@ async function main() {
1124
1220
  // 模块文档 sidecar 同步命令化(2026-09-09 刀批):diff 归属模块 → sidecar 追加变更索引行 +
1125
1221
  // 卡 updated_at 戳——机械部分 CLI 代算,卡片正文语义更新仍归 agent/归档流程。幂等。
1126
1222
  const mdsChangeIdx = args.indexOf('--change');
1127
- const mdsChange = mdsChangeIdx >= 0 && args[mdsChangeIdx + 1] ? args[mdsChangeIdx + 1] : null;
1223
+ const mdsChange = mdsChangeIdx >= 0 && args[mdsChangeIdx + 1] && !String(args[mdsChangeIdx + 1]).startsWith("--") ? args[mdsChangeIdx + 1] : null;
1128
1224
  const mdsNoteIdx = args.indexOf('--note');
1129
- const mdsNote = mdsNoteIdx >= 0 && args[mdsNoteIdx + 1] ? args[mdsNoteIdx + 1] : null;
1225
+ const mdsNote = mdsNoteIdx >= 0 && args[mdsNoteIdx + 1] && !String(args[mdsNoteIdx + 1]).startsWith("--") ? args[mdsNoteIdx + 1] : null;
1130
1226
  if (!mdsChange) {
1131
1227
  console.error('用法: sillyspec module-docs-sync --change <name> [--note "一句话"] [--json]\n diff 归属模块 → sidecar 追加变更索引行 + 卡 updated_at 戳(幂等;卡片正文语义更新仍归 agent)');
1132
1228
  process.exit(2);
@@ -1151,7 +1247,7 @@ async function main() {
1151
1247
  // 手排 Wave 与拓扑比对不一致需手改;依赖方向硬拦(plan-postcheck)落地后给一条零手写出路。
1152
1248
  // depMap 与 postcheck 同源(collectTaskDepMap)→ topoSortWaves → 重写 Wave 段 + 同步任务总表 W 列。
1153
1249
  const pawChangeIdx = args.indexOf('--change');
1154
- const pawChange = pawChangeIdx >= 0 && args[pawChangeIdx + 1] ? args[pawChangeIdx + 1] : null;
1250
+ const pawChange = pawChangeIdx >= 0 && args[pawChangeIdx + 1] && !String(args[pawChangeIdx + 1]).startsWith("--") ? args[pawChangeIdx + 1] : null;
1155
1251
  const pawDryRun = args.includes('--dry-run');
1156
1252
  if (!pawChange) {
1157
1253
  console.error('用法: sillyspec plan-adopt-waves --change <name> [--dry-run] [--json] [--spec-dir <path>]\n 把 plan.md 的 Wave 段一键重排为 depends_on 拓扑分组(同步任务总表 W 列;--dry-run 只看不写)');
@@ -1196,7 +1292,7 @@ async function main() {
1196
1292
  // archive 模块影响矩阵骨架(2026-08-21 审计第三批 D3):文件×模块归属按 _module-map.yaml
1197
1293
  // paths 前缀匹配预填(机械),影响类型/review 标记留 <!--TODO-->(语义)。已存在不覆盖。
1198
1294
  const miChangeIdx = args.indexOf('--change');
1199
- const miChange = miChangeIdx >= 0 && args[miChangeIdx + 1] ? args[miChangeIdx + 1] : null;
1295
+ const miChange = miChangeIdx >= 0 && args[miChangeIdx + 1] && !String(args[miChangeIdx + 1]).startsWith("--") ? args[miChangeIdx + 1] : null;
1200
1296
  if (!miChange) {
1201
1297
  console.error('用法: sillyspec module-impact --change <name> [--json] [--spec-dir <path>]\n 生成 module-impact.md 骨架(模块影响矩阵按 module-map 预填 + 未匹配文件清单;已存在不覆盖)');
1202
1298
  process.exit(2);
@@ -1231,10 +1327,14 @@ async function main() {
1231
1327
  // 路由形态对齐 verify-probes(:897)/ module-impact(:1041)先例——--change 必填缺参
1232
1328
  // 用法错 exit 2、assertSafeChangeName 消毒、动态 import、--json 结构化输出、运行错
1233
1329
  // fail-soft 出错误摘要 exit 1 不抛栈。
1330
+ // 画像双出口(2026-09-14-quick-exit-tiered-gates task-03,FR-04 / D-008):quick 会话的
1331
+ // gateProfile 由 computeChangeScopeAudit 结果对象携带——--json 出口经 ...saResult 展开
1332
+ // 自动透出(gateProfile-json)、表格出口经 renderScopeAuditTable 按存在性追加 [gate]
1333
+ // 画像段(gate-table-section),本层零分支零新解析;--file 子命令行为不变。
1234
1334
  const saChangeIdx = args.indexOf('--change');
1235
- const saChange = saChangeIdx >= 0 && args[saChangeIdx + 1] ? args[saChangeIdx + 1] : null;
1335
+ const saChange = saChangeIdx >= 0 && args[saChangeIdx + 1] && !String(args[saChangeIdx + 1]).startsWith("--") ? args[saChangeIdx + 1] : null;
1236
1336
  if (!saChange) {
1237
- console.error('用法: sillyspec scope-audit --change <name> [--file <path>] [--json] [--spec-dir <path>]\n 变更范围对账:计划×实际三态全表 + 行数(quick 会话传 quick-<8hex> 出归属表;已归档变更可查——快照冻结记录态);--file <path> 看单文件变化内容(对账同源锚点的 git diff);advisory 只读展示,不构成门禁');
1337
+ console.error('用法: sillyspec scope-audit --change <name> [--file <path>] [--json] [--spec-dir <path>]\n 变更范围对账:计划×实际三态全表 + 行数(quick 会话传 quick-<8hex> 出归属表,附 [gate] 分级门禁画像——表格画像段与 --json gateProfile 字段;已归档变更可查——快照冻结记录态);--file <path> 看单文件变化内容(对账同源锚点的 git diff);advisory 只读展示,不构成门禁');
1238
1338
  process.exit(2);
1239
1339
  }
1240
1340
  assertSafeChangeName(saChange, '--change 变更名');
@@ -1262,10 +1362,12 @@ async function main() {
1262
1362
  }
1263
1363
  // full-flow 变更目录存在性预检(endpoints baseline 先例同款):post-apply 形态下实际侧三源
1264
1364
  // 对「变更不存在」不敏感(status 成功即 ok=true,会把主仓脏区误当实际侧),靠预检给明确
1265
- // 错误摘要 exit 1;quick 会话 id 无 changes/ 目录(guard 祖先链定位),不预检。
1365
+ // 错误摘要 exit 1;quick 会话 id 无 changes/ 目录(guard 祖先链定位),不预检;ql-xxx
1366
+ // 条目号同 quick 会话 id(ql-xxx 反查走 quicklog/patches,亦无 changes/ 目录)不预检。
1266
1367
  // 归档形态兼容(quick-8aa52289):changes/<名> 不在 → 接受 changes/archive/<名>(scope-audit
1267
1368
  // 内部走快照记录态/HEAD 未提交窗口兜底)。
1268
1369
  if (!/^quick-[0-9a-f]{8}$/.test(saChange)
1370
+ && !/^ql-\d{8}-\d{3}-[0-9a-f]{4}$/.test(saChange)
1269
1371
  && !existsSync(join(saSpecBase, 'changes', saChange))
1270
1372
  && !existsSync(join(saSpecBase, 'changes', 'archive', saChange))) {
1271
1373
  console.error(`❌ 变更目录不存在(活跃与归档区均未找到): ${join(saSpecBase, 'changes', saChange)}(确认 --change 名,或 --spec-dir 指向主仓 .sillyspec)`);
@@ -1329,7 +1431,7 @@ async function main() {
1329
1431
  // 端点增删的 before 侧;扫描/落盘在 endpoint-baseline.js 纯函数,此处只做参数解析 +
1330
1432
  // worktree 主仓锚定 + 输出。
1331
1433
  const ebChangeIdx = args.indexOf('--change');
1332
- const ebChange = ebChangeIdx >= 0 && args[ebChangeIdx + 1] ? args[ebChangeIdx + 1] : null;
1434
+ const ebChange = ebChangeIdx >= 0 && args[ebChangeIdx + 1] && !String(args[ebChangeIdx + 1]).startsWith("--") ? args[ebChangeIdx + 1] : null;
1333
1435
  if (!ebChange) {
1334
1436
  console.error('用法: sillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json]\n 拍变更前端点基线(全仓现扫,幂等已拍过不覆盖)→ .runtime/endpoint-baselines/<change>.json;归档 delta 端点增删的 before 侧。\n execute worktree 内跑:扫描根与落点自动锚定主仓(变更前代码在主仓;worktree 内是交付态且其 .runtime 随清理丢失)');
1335
1437
  process.exit(2);
@@ -1397,14 +1499,14 @@ async function main() {
1397
1499
  process.exit(2);
1398
1500
  }
1399
1501
  const epChangeIdx = args.indexOf('--change');
1400
- const epChange = epChangeIdx >= 0 && args[epChangeIdx + 1] ? args[epChangeIdx + 1] : null;
1502
+ const epChange = epChangeIdx >= 0 && args[epChangeIdx + 1] && !String(args[epChangeIdx + 1]).startsWith("--") ? args[epChangeIdx + 1] : null;
1401
1503
  const epTaskIdx = args.indexOf('--task');
1402
- const epTask = epTaskIdx >= 0 && args[epTaskIdx + 1] ? args[epTaskIdx + 1] : null;
1504
+ const epTask = epTaskIdx >= 0 && args[epTaskIdx + 1] && !String(args[epTaskIdx + 1]).startsWith("--") ? args[epTaskIdx + 1] : null;
1403
1505
  const epAllTasks = args.includes('--all-tasks');
1404
1506
  const epDirIdx = args.indexOf('--dir');
1405
- const epDir = epDirIdx >= 0 && args[epDirIdx + 1] ? args[epDirIdx + 1] : null;
1507
+ const epDir = epDirIdx >= 0 && args[epDirIdx + 1] && !String(args[epDirIdx + 1]).startsWith("--") ? args[epDirIdx + 1] : null;
1406
1508
  const epFilesIdx = args.indexOf('--files');
1407
- const epFilesRaw = epFilesIdx >= 0 && args[epFilesIdx + 1] ? args[epFilesIdx + 1] : null;
1509
+ const epFilesRaw = epFilesIdx >= 0 && args[epFilesIdx + 1] && !String(args[epFilesIdx + 1]).startsWith("--") ? args[epFilesIdx + 1] : null;
1408
1510
  if (!epChange) {
1409
1511
  console.error('❌ 缺 --change <变更名>');
1410
1512
  process.exit(2);
@@ -1552,7 +1654,7 @@ async function main() {
1552
1654
  // author/created_at 只 warning,agent 手改还常忘。本命令补齐(git user + 当前时间),
1553
1655
  // 已有两键的文件不动;有 frontmatter 就地插入,无则补一段。
1554
1656
  const sfhProjectIdx = args.indexOf('--project');
1555
- const sfhProject = sfhProjectIdx >= 0 && args[sfhProjectIdx + 1] ? args[sfhProjectIdx + 1] : null;
1657
+ const sfhProject = sfhProjectIdx >= 0 && args[sfhProjectIdx + 1] && !String(args[sfhProjectIdx + 1]).startsWith("--") ? args[sfhProjectIdx + 1] : null;
1556
1658
  const { fixScanDocHeaders } = await import('./scan-postcheck.js');
1557
1659
  const sfhResult = fixScanDocHeaders({ cwd: dir, specDir, project: sfhProject });
1558
1660
  if (json) {
@@ -1572,7 +1674,7 @@ async function main() {
1572
1674
  // plan.md)注册表生成逐 task <!--TODO--> 骨架;gate 拒绝未替换的占位(防骨架直接过门),
1573
1675
  // agent 只需逐行填结论。已存在不覆盖(幂等,agent 产出优先)。
1574
1676
  const siChangeIdx = args.indexOf('--change');
1575
- const siChange = siChangeIdx >= 0 && args[siChangeIdx + 1] ? args[siChangeIdx + 1] : null;
1677
+ const siChange = siChangeIdx >= 0 && args[siChangeIdx + 1] && !String(args[siChangeIdx + 1]).startsWith("--") ? args[siChangeIdx + 1] : null;
1576
1678
  if (!siChange) {
1577
1679
  console.error('用法: sillyspec symbol-impact --change <name> [--spec-dir <path>]\n 生成 symbol-impact.md 逐 task <!--TODO--> 骨架(已存在不覆盖);gate 拒绝未替换的占位行');
1578
1680
  process.exit(2);
@@ -1608,7 +1710,7 @@ async function main() {
1608
1710
  // 手写 proposal/requirements/decisions 的 frontmatter/章节骨架是格式返工根源——本命令
1609
1711
  // 逐文件生成骨架(已存在不覆盖),agent 只填语义。与 design-init 同族(幂等)。
1610
1712
  const fpChangeIdx = args.indexOf('--change');
1611
- const fpChange = fpChangeIdx >= 0 && args[fpChangeIdx + 1] ? args[fpChangeIdx + 1] : null;
1713
+ const fpChange = fpChangeIdx >= 0 && args[fpChangeIdx + 1] && !String(args[fpChangeIdx + 1]).startsWith("--") ? args[fpChangeIdx + 1] : null;
1612
1714
  if (!fpChange) {
1613
1715
  console.error('用法: sillyspec fourpiece-init --change <name> [--json] [--spec-dir <path>]\n 生成 proposal.md/requirements.md/decisions.md 骨架(frontmatter+章节标题+模板占位;已存在不覆盖;design.md 归 design-init)');
1614
1716
  process.exit(2);
@@ -1701,7 +1803,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1701
1803
  // design-facts.js generateDesignSkeleton——本 case 只负责读盘/幂等保护/落盘)。已存在不覆盖
1702
1804
  // (幂等,agent 产出优先),--force 覆盖重生成。
1703
1805
  const diChangeIdx = args.indexOf('--change');
1704
- const diChange = diChangeIdx >= 0 && args[diChangeIdx + 1] ? args[diChangeIdx + 1] : null;
1806
+ const diChange = diChangeIdx >= 0 && args[diChangeIdx + 1] && !String(args[diChangeIdx + 1]).startsWith("--") ? args[diChangeIdx + 1] : null;
1705
1807
  const diForce = args.includes('--force');
1706
1808
  if (!diChange) {
1707
1809
  console.error('用法: sillyspec design-init --change <name> [--force] [--json] [--spec-dir <path>]\n 从变更 decisions.md 生成 design.md 十三章节骨架(决策追踪表按当前版本 D 条目预填;已存在不覆盖,--force 覆盖)');
@@ -1765,7 +1867,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1765
1867
  // (重跑即刷新):与 design-init「agent 产出优先不覆盖」语义相反——delta 是源数据
1766
1868
  // (verify-runs/module-map)时点快照,旧报告天然过期,重新生成即对齐。
1767
1869
  const dlChangeIdx = args.indexOf('--change');
1768
- const dlChange = dlChangeIdx >= 0 && args[dlChangeIdx + 1] ? args[dlChangeIdx + 1] : null;
1870
+ const dlChange = dlChangeIdx >= 0 && args[dlChangeIdx + 1] && !String(args[dlChangeIdx + 1]).startsWith("--") ? args[dlChangeIdx + 1] : null;
1769
1871
  if (!dlChange) {
1770
1872
  console.error('用法: sillyspec delta --change <name> [--spec-dir <path>] [--json]\n 生成变更 delta.md(Before/Delta/After 三段式;四源 fail-soft,缺源逐段降级注记;幂等覆盖重跑即刷新)');
1771
1873
  process.exit(2);
@@ -1827,11 +1929,11 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1827
1929
  // 调度者手动派独立子代理不写 marker 的死锁。docHash 由 CLI computeDocHash 算(部分实现 P6.1b defer)。
1828
1930
  // 纯新增,不改 gate 语义。与 task 级 backfill-reviews 对称(stage 级等价物)。
1829
1931
  const rsrChangeIdx = args.indexOf('--change');
1830
- const rsrChange = rsrChangeIdx >= 0 && args[rsrChangeIdx + 1] ? args[rsrChangeIdx + 1] : null;
1932
+ const rsrChange = rsrChangeIdx >= 0 && args[rsrChangeIdx + 1] && !String(args[rsrChangeIdx + 1]).startsWith("--") ? args[rsrChangeIdx + 1] : null;
1831
1933
  const rsrStageIdx = args.indexOf('--stage');
1832
- const rsrStage = rsrStageIdx >= 0 && args[rsrStageIdx + 1] ? args[rsrStageIdx + 1] : null;
1934
+ const rsrStage = rsrStageIdx >= 0 && args[rsrStageIdx + 1] && !String(args[rsrStageIdx + 1]).startsWith("--") ? args[rsrStageIdx + 1] : null;
1833
1935
  const rsrFromIdx = args.indexOf('--from');
1834
- const rsrFrom = rsrFromIdx >= 0 && args[rsrFromIdx + 1] ? args[rsrFromIdx + 1] : null;
1936
+ const rsrFrom = rsrFromIdx >= 0 && args[rsrFromIdx + 1] && !String(args[rsrFromIdx + 1]).startsWith("--") ? args[rsrFromIdx + 1] : null;
1835
1937
  // docHash 联动(坑 stage-review-dochash-manual-resync,2026-08-21 实证):改一版 design 要
1836
1938
  // 重算 2-3 个 stage review 的 docHash,手工易漏、gate 报错才补。--refresh-hash 就地刷新单个
1837
1939
  // 既有 review 的 hash(保留结论);--all 一次处理三个 stage(有 review 刷 hash,无 review
@@ -1940,13 +2042,14 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1940
2042
  // --dry-run 列独立存在(「报告修复预览 + exit 1」),即 dryRun 置位 = fix 语义自动生效
1941
2043
  // 但零写盘(本口径记录于此,防歧义)。
1942
2044
  const BARE_FLAGS = ['--fix', '--dry-run', '--no-exempt'];
1943
- const PAIRED_FLAGS = ['--paths'];
2045
+ const PAIRED_FLAGS = ['--paths', '--against'];
1944
2046
  const rawDocsArgs = filteredArgs.slice(2);
1945
2047
  const docsCheckFlags = [];
1946
2048
  let cliPaths = null;
1947
2049
  let fix = false;
1948
2050
  let dryRun = false;
1949
2051
  let noExempt = false;
2052
+ let againstRef = null;
1950
2053
  for (let i = 0; i < rawDocsArgs.length; i++) {
1951
2054
  const a = rawDocsArgs[i];
1952
2055
  if (a === '--paths' && rawDocsArgs[i + 1] !== undefined) {
@@ -1955,6 +2058,13 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1955
2058
  } else if (a === '--paths') {
1956
2059
  console.error('❌ docs check: --paths 缺值(逗号分隔 glob,如 --paths "docs/**/*.md")');
1957
2060
  process.exit(2);
2061
+ } else if (a === '--against' && rawDocsArgs[i + 1] !== undefined && !rawDocsArgs[i + 1].startsWith('--')) {
2062
+ // 坑 docs-gate-shared-worktree-parallel-block:按提交树校验,隔离并行在途编辑(移动靶)
2063
+ againstRef = rawDocsArgs[i + 1];
2064
+ i++;
2065
+ } else if (a === '--against') {
2066
+ console.error('❌ docs check: --against 缺值(如 --against HEAD)');
2067
+ process.exit(2);
1958
2068
  } else if (BARE_FLAGS.includes(a)) {
1959
2069
  if (a === '--fix') fix = true;
1960
2070
  else if (a === '--dry-run') dryRun = true;
@@ -1966,6 +2076,11 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1966
2076
  docsCheckFlags.push(a);
1967
2077
  }
1968
2078
  }
2079
+ if ((fix || dryRun) && againstRef) {
2080
+ // --fix 操作工作区磁盘内容,against 语义与其矛盾(修的是未提交文本、校验的是提交树)
2081
+ console.error('❌ docs check: --fix/--dry-run 与 --against 互斥(fix 恒操作工作区,against 恒校验提交树)');
2082
+ process.exit(2);
2083
+ }
1969
2084
  const { runDocsCheck, readDocsCheckConfig, DocsCheckConfigError, applyFixes, runChangeNameAdvisory } = await import('./docs-check.js');
1970
2085
  try {
1971
2086
  // 配置优先级:--paths > local.yaml docs-check.paths > 缺省 docs/**/*.md
@@ -1978,6 +2093,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1978
2093
  keywordAssert: cfg.keywordAssert,
1979
2094
  crossRepoRoots: cfg.crossRepoRoots,
1980
2095
  exempt: !noExempt,
2096
+ against: againstRef,
1981
2097
  });
1982
2098
  // task-03 修复链路(FR-04,platform-map-auto-anchors):fixActive 才构造 fixes——
1983
2099
  // 仅 fix.fixable===true 且 newLine 为整数的条目可重锚;newRef = ref 的行号部分替换为
@@ -2152,11 +2268,14 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2152
2268
  // usage 输出且 exit 0——gate 形同虚设(docs-signals 范围内文件+hook 已就位,仅此分支缺)。
2153
2269
  // B9(2026-08-16):flag 白名单化(对齐 docs check 分支)——未知 --xxx 显式 exit 2
2154
2270
  // (interface-contract §1.3b 宣称),并接线 --paths 透传 runDocsGate.checkOpts(原被忽略)。
2271
+ // 坑 docs-gate-stale-baseline(ql-20260915-004):超基线分支实测 origin/main 兜底——
2272
+ // 未劣于远端放行 + 提示重锚;--json 输出含 originCount 字段(null=未实测/快路径)。
2155
2273
  const GATE_BARE_FLAGS = ['--init-baseline'];
2156
- const GATE_PAIRED_FLAGS = ['--paths'];
2274
+ const GATE_PAIRED_FLAGS = ['--paths', '--against'];
2157
2275
  const rawGateArgs = filteredArgs.slice(2);
2158
2276
  let initBaseline = false;
2159
2277
  let cliGatePaths = null;
2278
+ let gateAgainst = null;
2160
2279
  for (let i = 0; i < rawGateArgs.length; i++) {
2161
2280
  const a = rawGateArgs[i];
2162
2281
  if (a === '--paths' && rawGateArgs[i + 1] !== undefined) {
@@ -2165,13 +2284,21 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2165
2284
  } else if (a === '--paths') {
2166
2285
  console.error('❌ docs gate: --paths 缺值(逗号分隔 glob,如 --paths "docs/**/*.md")');
2167
2286
  process.exit(2);
2287
+ } else if (a === '--against' && rawGateArgs[i + 1] !== undefined && !rawGateArgs[i + 1].startsWith('--')) {
2288
+ // 坑 docs-gate-shared-worktree-parallel-block:按提交树校验(pre-push 语义——推的
2289
+ // 是提交不是工作区),隔离并行会话在途编辑造成的锚点瞬时漂移(移动靶)
2290
+ gateAgainst = rawGateArgs[i + 1];
2291
+ i++;
2292
+ } else if (a === '--against') {
2293
+ console.error('❌ docs gate: --against 缺值(如 --against HEAD)');
2294
+ process.exit(2);
2168
2295
  } else if (GATE_BARE_FLAGS.includes(a)) {
2169
2296
  if (a === '--init-baseline') initBaseline = true;
2170
2297
  } else if (a.startsWith('--')) {
2171
2298
  console.error(`❌ docs gate: 未知 flag「${a}」。已知 flag:${[...GATE_BARE_FLAGS, ...GATE_PAIRED_FLAGS].join(' ')}(--json 为全局 flag)`);
2172
2299
  process.exit(2);
2173
2300
  } else {
2174
- console.error(`❌ docs gate: 多余位置参数「${a}」。用法:sillyspec docs gate [--init-baseline] [--paths <glob,...>] [--json]`);
2301
+ console.error(`❌ docs gate: 多余位置参数「${a}」。用法:sillyspec docs gate [--init-baseline] [--paths <glob,...>] [--against HEAD] [--json]`);
2175
2302
  process.exit(2);
2176
2303
  }
2177
2304
  }
@@ -2181,7 +2308,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2181
2308
  const gateSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
2182
2309
  const g = await runDocsGate(
2183
2310
  { projectRoot: dir, specBase: gateSpecBase, initBaseline },
2184
- cliGatePaths ? { paths: cliGatePaths } : undefined
2311
+ { ...(cliGatePaths ? { paths: cliGatePaths } : {}), ...(gateAgainst ? { against: gateAgainst } : {}) }
2185
2312
  );
2186
2313
  if (json) {
2187
2314
  console.log(JSON.stringify(g, null, 2));
@@ -2237,7 +2364,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2237
2364
  const dumpDbFlag = filteredArgs.includes('--dump-db');
2238
2365
  const doctorConfirm = filteredArgs.includes('--confirm');
2239
2366
  const pathIdx = filteredArgs.indexOf('--path');
2240
- const dbPath = pathIdx >= 0 && filteredArgs[pathIdx + 1] ? filteredArgs[pathIdx + 1] : null;
2367
+ const dbPath = pathIdx >= 0 && filteredArgs[pathIdx + 1] && !String(filteredArgs[pathIdx + 1]).startsWith("--") ? filteredArgs[pathIdx + 1] : null;
2241
2368
 
2242
2369
  // --align-execute-progress:基于 plan.md 声明对齐 execute 派生戳(仿 --cleanup-remnant 范式)
2243
2370
  // 默认 dry-run(只报告将补哪些 step),加 --confirm 才写。命中即 break,绝不 fall-through。
@@ -2245,7 +2372,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2245
2372
  if (alignFlag) {
2246
2373
  // --change 解析:显式优先,缺省用单活跃变更自动兜底(与 run.js resolveChangeNameAuto 同逻辑)
2247
2374
  const alignChangeIdx = filteredArgs.indexOf('--change');
2248
- let alignChange = alignChangeIdx >= 0 && filteredArgs[alignChangeIdx + 1] ? filteredArgs[alignChangeIdx + 1] : null;
2375
+ let alignChange = alignChangeIdx >= 0 && filteredArgs[alignChangeIdx + 1] && !String(filteredArgs[alignChangeIdx + 1]).startsWith("--") ? filteredArgs[alignChangeIdx + 1] : null;
2249
2376
  if (!alignChange) {
2250
2377
  const changesDir = join(resolvePlatformSpecDir(doctorEffectiveDir, specDir) || join(doctorEffectiveDir, '.sillyspec'), 'changes');
2251
2378
  if (existsSync(changesDir)) {
@@ -2417,8 +2544,8 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2417
2544
  const r = buildAndWriteScanFacts({
2418
2545
  cwd: factsEffectiveDir,
2419
2546
  specDir: resolvePlatformSpecDir(dir, specDir) || join(factsEffectiveDir, '.sillyspec'),
2420
- projectName: projIdx >= 0 && filteredArgs[projIdx + 1] ? filteredArgs[projIdx + 1] : basename(factsEffectiveDir),
2421
- projectPath: pathIdx >= 0 && filteredArgs[pathIdx + 1] ? filteredArgs[pathIdx + 1] : null,
2547
+ projectName: projIdx >= 0 && filteredArgs[projIdx + 1] && !String(filteredArgs[projIdx + 1]).startsWith("--") ? filteredArgs[projIdx + 1] : basename(factsEffectiveDir),
2548
+ projectPath: pathIdx >= 0 && filteredArgs[pathIdx + 1] && !String(filteredArgs[pathIdx + 1]).startsWith("--") ? filteredArgs[pathIdx + 1] : null,
2422
2549
  })
2423
2550
  const f = r.facts
2424
2551
  console.log(`scan facts 已写入: ${r.outPath}`)
@@ -2431,7 +2558,7 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2431
2558
  if (filteredArgs[1] === 'diff') {
2432
2559
  const { runScanDiff } = await import('./scan-diff.js')
2433
2560
  const diffBaseIdx = filteredArgs.indexOf('--base')
2434
- const diffBase = diffBaseIdx >= 0 && filteredArgs[diffBaseIdx + 1] ? filteredArgs[diffBaseIdx + 1] : null
2561
+ const diffBase = diffBaseIdx >= 0 && filteredArgs[diffBaseIdx + 1] && !String(filteredArgs[diffBaseIdx + 1]).startsWith("--") ? filteredArgs[diffBaseIdx + 1] : null
2435
2562
  // specBase/projectName 与 scan 主流程同口径(scan-profile.js:specBase=进度库根,projectName=仓库根 basename)。
2436
2563
  // A4 同族:平台指针 / 显式 --spec-dir / 本地 fallback 三合一,平台模式下 diff 不落本地孤儿库。
2437
2564
  const diffEffectiveDir = specDir ? dir : resolveEffectiveDir(dir)
@@ -2445,6 +2572,40 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2445
2572
  })
2446
2573
  break
2447
2574
  }
2575
+ // scan refresh 子命令(2026-09-14-scan-incremental-refresh D-002@v1:增量刷新两拍交互)。
2576
+ // ①拍纯读出工单(含 guard 握手写);②拍 --done 盖章收尾。与 diff 同为旁路:跳过
2577
+ // triggerPullActiveChange、不走 scan 主流程。--project 支持子项目(缺省仓库根 basename,
2578
+ // 与 scan facts 同口径)。
2579
+ if (filteredArgs[1] === 'refresh') {
2580
+ const { runRefresh, finalizeRefresh } = await import('./scan-refresh.js')
2581
+ const refreshEffectiveDir = specDir ? dir : resolveEffectiveDir(dir)
2582
+ const refreshSpecBase = resolvePlatformSpecDir(dir, specDir) || join(refreshEffectiveDir, '.sillyspec')
2583
+ const projIdxR = filteredArgs.indexOf('--project')
2584
+ const refreshProjectName = projIdxR >= 0 && filteredArgs[projIdxR + 1] && !String(filteredArgs[projIdxR + 1]).startsWith("--") ? filteredArgs[projIdxR + 1] : basename(refreshEffectiveDir)
2585
+ if (filteredArgs.includes('--done')) {
2586
+ const docsIdx = filteredArgs.indexOf('--docs')
2587
+ const docsList = docsIdx >= 0 && filteredArgs[docsIdx + 1] && !String(filteredArgs[docsIdx + 1]).startsWith("--")
2588
+ ? filteredArgs[docsIdx + 1].split(',').map(s => s.trim()).filter(Boolean) : null
2589
+ const r = await finalizeRefresh({
2590
+ projectRoot: refreshEffectiveDir,
2591
+ specBase: refreshSpecBase,
2592
+ projectName: refreshProjectName,
2593
+ docs: docsList,
2594
+ force: filteredArgs.includes('--force'),
2595
+ platformOpts: platformWorkspaceId || platformRuntimeRoot ? { workspaceId: platformWorkspaceId, runtimeRoot: platformRuntimeRoot, specRoot: resolvePlatformSpecDir(dir, specDir) } : null,
2596
+ })
2597
+ process.exitCode = r.code
2598
+ } else {
2599
+ process.exitCode = await runRefresh({
2600
+ projectRoot: refreshEffectiveDir,
2601
+ specBase: refreshSpecBase,
2602
+ projectName: refreshProjectName,
2603
+ force: filteredArgs.includes('--force'),
2604
+ json: filteredArgs.includes('--json'),
2605
+ })
2606
+ }
2607
+ break
2608
+ }
2448
2609
  }
2449
2610
  case 'status':
2450
2611
  case 'quick':
@@ -2496,14 +2657,82 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
2496
2657
  console.log('按 Ctrl+C 停止服务器');
2497
2658
  break;
2498
2659
  }
2660
+ case 'wt-commit': {
2661
+ // 坑 wt-commit-ghost-dispatch:runWtCommit 模块自 bd1cb91 存在但 dispatch 从未接线——
2662
+ // execute Wave prompt 指示的 `sillyspec wt-commit` 一直报「未知命令」(2026-09-14
2663
+ // knowledge-loop-close execute W1 实证,ql-20260914-016-8786 根治)。参数面见模块头:
2664
+ // wt-commit [--change <名>] -m <信息> [--pathspec-from-file <f>] -- <path...>;
2665
+ // worktree 内可省 --change(从 cwd 的 worktrees/<名> 段推断)。
2666
+ const wtCommitRest = filteredArgs.slice(1);
2667
+ const wtCommitDd = wtCommitRest.indexOf('--');
2668
+ const wtCommitFlags = wtCommitDd === -1 ? wtCommitRest : wtCommitRest.slice(0, wtCommitDd);
2669
+ const wtCommitPaths = wtCommitDd === -1 ? [] : wtCommitRest.slice(wtCommitDd + 1);
2670
+ let wtCommitChange = null;
2671
+ let wtCommitMessage = null;
2672
+ let wtCommitPathspecFile = null;
2673
+ for (let i = 0; i < wtCommitFlags.length; i++) {
2674
+ if (wtCommitFlags[i] === '--change' && wtCommitFlags[i + 1]) wtCommitChange = wtCommitFlags[++i];
2675
+ else if ((wtCommitFlags[i] === '-m' || wtCommitFlags[i] === '--message') && wtCommitFlags[i + 1]) wtCommitMessage = wtCommitFlags[++i];
2676
+ else if (wtCommitFlags[i] === '--pathspec-from-file' && wtCommitFlags[i + 1]) wtCommitPathspecFile = wtCommitFlags[++i];
2677
+ }
2678
+ if (!wtCommitChange) {
2679
+ const inferred = String(dir).match(/[\\/]worktrees[\\/]([^\\/]+)/);
2680
+ wtCommitChange = inferred ? inferred[1] : null;
2681
+ }
2682
+ try {
2683
+ const { runWtCommit } = await import('./wt-commit.js');
2684
+ const r = await runWtCommit({
2685
+ changeName: wtCommitChange,
2686
+ message: wtCommitMessage,
2687
+ pathspecs: wtCommitPaths,
2688
+ pathspecFile: wtCommitPathspecFile,
2689
+ cwd: dir,
2690
+ });
2691
+ if (json) console.log(JSON.stringify({ command: 'wt-commit', ...r }, null, 2));
2692
+ else if (r.skipped) console.log(`⏭️ wt-commit 无变更跳过(HEAD 不动):${r.shortHead}`);
2693
+ else console.log(`✅ wt-commit 完成:${r.shortHead}(${r.files.length} 文件,worktree=${r.worktreePath})`);
2694
+ } catch (e) {
2695
+ console.error(`❌ wt-commit 失败:${e.message}`);
2696
+ process.exit(1);
2697
+ }
2698
+ break;
2699
+ }
2499
2700
  case 'worktree': {
2500
2701
  const { WorktreeManager } = await import('./worktree.js');
2501
- const { ProgressManager } = await import('./progress.js');
2702
+ const { ProgressManager, resolveSessionIdentity } = await import('./progress.js');
2502
2703
  const wtSubCmd = filteredArgs[1];
2503
2704
  const wtName = filteredArgs.slice(2).find(a => !a.startsWith('-'));
2504
2705
  const wm = new WorktreeManager({ cwd: dir });
2505
2706
  const pm = new ProgressManager({ specDir: resolvePlatformSpecDir(dir, specDir) });
2506
2707
 
2708
+ // ── 所有权护栏公共件(2026-09-14-change-ownership-guards task-02 / FR-01 / D-001@v1)──
2709
+ // 接管类操作(apply / cleanup / assess 自动 apply)改主仓前校验 change 归属:
2710
+ // 他人且活跃(活跃窗内,heartbeat_minutes 缺省 15min)→ 拒绝;无主 / 窗口外 / 本会话
2711
+ // 自有 / --takeover → 放行,接管分支重写 owner=本会话(setChangeOwner 含 last_active
2712
+ // 心跳起算)。调用点必须在 withMainRepoLock 锁内(判定与执行无 TOCTOU);只读路径
2713
+ // (diff/list/meta/doctor/--check-only)不接。会话标识三级解析:--session flag >
2714
+ // env SILLYSPEC_SESSION_ID > anon@host 机器级降级(同机并行不设防是 R-01 明示局限)。
2715
+ const sessionFlagIdx = args.indexOf('--session');
2716
+ const sessionFlagValue = (sessionFlagIdx !== -1 && args[sessionFlagIdx + 1]
2717
+ && !String(args[sessionFlagIdx + 1]).startsWith('--')) ? String(args[sessionFlagIdx + 1]).trim() : null;
2718
+ // soft=true:assess 自动路径拒绝语义(跳过自动落盘 + warning 不抛错,同 overlapSkipped 软跳);
2719
+ // soft=false:显式 apply/cleanup 拒绝(结构化错误,调用方 exit 非零)。
2720
+ function _guardChangeOwnership(changeName, { forced = false, soft = false } = {}) {
2721
+ const { session } = resolveSessionIdentity({ flagSession: sessionFlagValue, cwd: dir });
2722
+ const check = pm.assertChangeOwnership(dir, changeName, { selfSession: session, nowMs: Date.now(), forced });
2723
+ if (check.allowed) {
2724
+ if (check.action !== 'self') pm.setChangeOwner(dir, changeName, session);
2725
+ return { allowed: true, action: check.action };
2726
+ }
2727
+ const lines = [
2728
+ `变更 ${changeName} 正被其他会话持有(owner: ${check.owner},最后活跃: ${check.lastActive || '未知'}),活跃窗口 ${Math.round(check.heartbeatMs / 60000)} 分钟内拒绝接管类操作`,
2729
+ `确认对方已放弃且本会话应接管:加 --takeover 显式强制接管(重写 owner 并留痕);或等对方会话收尾后重试`,
2730
+ ];
2731
+ if (soft) { for (const l of lines) console.warn(`⚠️ ${l}`); }
2732
+ else { for (const l of lines) console.error(`🚫 ${l}`); }
2733
+ return { allowed: false, action: check.action };
2734
+ }
2735
+
2507
2736
  // isolation 写入 DB 的辅助函数
2508
2737
  function _writeIsolationToDB(cwd, changeName, info) {
2509
2738
  if (info.blocked) {
@@ -2528,17 +2757,19 @@ SillySpec worktree — git worktree 隔离管理
2528
2757
 
2529
2758
  用法:
2530
2759
  sillyspec worktree create <change-name> [--base <branch>] [--adopt-branch] 创建隔离 worktree(--adopt-branch:收编既有同名分支为工作分支,分支现状作 baseline)
2531
- sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap] [--stash-dirty] 校验并应用变更到主工作区(--stash-dirty:主仓在途改动自动 stash→apply→恢复,SHA 兜底可审计)
2532
- sillyspec worktree assess <change-name> 风险审计 + 自动 apply
2760
+ sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap] [--stash-dirty] [--force] [--takeover] 校验并应用变更到主工作区(--stash-dirty:主仓在途改动自动 stash→apply→恢复,SHA 兜底可审计;--force:越过活跃 quick 会话 guard 相交预检,overlapForced 留痕;--takeover:越过所有权护栏显式强制接管,owner 重写留痕)
2761
+ sillyspec worktree assess <change-name> 风险审计 + 自动 apply(自动路径受所有权护栏约束:他人活跃 change 跳过自动落盘并 warning)
2533
2762
  sillyspec worktree diff <change-name> [--base <commit>] 查看 worktree 相对 base 的变更
2534
2763
  sillyspec worktree list 列出所有活跃 worktree
2535
2764
  sillyspec worktree meta <change-name> 读取 worktree meta.json
2536
- sillyspec worktree cleanup <change-name> [--force] 强制清理 worktree
2765
+ sillyspec worktree cleanup <change-name> [--force] [--takeover] 强制清理 worktree(受所有权护栏约束:他人活跃 change 拒绝,--takeover 显式接管)
2537
2766
  sillyspec worktree doctor [--fix] [--stale-hours N] [--change <name>] 健康检查 + 修复(--change 仅扫指定 change)
2538
2767
 
2539
2768
  选项:
2540
2769
  --base <branch> create: 指定基础分支(默认当前 HEAD)
2541
2770
  --check-only apply: 只输出检查结果,不实际 apply
2771
+ --takeover apply/cleanup: 所有权护栏拒绝时的显式强制接管口(owner 他人且活跃时解锁,重写 owner 留痕)
2772
+ --session <id> 显式会话标识(所有权三级解析最高优先级;缺省 env SILLYSPEC_SESSION_ID,再缺省 anon@<host> 机器级降级——同机并行不设防)
2542
2773
  `);
2543
2774
  break;
2544
2775
  }
@@ -2553,7 +2784,7 @@ SillySpec worktree — git worktree 隔离管理
2553
2784
  try { assertSafeChangeName(wtName, 'worktree 变更名'); }
2554
2785
  catch (e) { console.error(`❌ ${e.message}`); process.exit(2); }
2555
2786
  const baseIdx = args.indexOf('--base');
2556
- const base = baseIdx >= 0 && args[baseIdx + 1] ? args[baseIdx + 1] : undefined;
2787
+ const base = baseIdx >= 0 && args[baseIdx + 1] && !String(args[baseIdx + 1]).startsWith("--") ? args[baseIdx + 1] : undefined;
2557
2788
  const adoptBranch = args.includes('--adopt-branch'); // 收编既有同名分支为工作分支(坑 worktree-user-branch-conflict)
2558
2789
  try {
2559
2790
  const info = wm.create(wtName, { base, adoptBranch });
@@ -2576,13 +2807,15 @@ SillySpec worktree — git worktree 隔离管理
2576
2807
  }
2577
2808
  case 'apply': {
2578
2809
  if (!wtName) {
2579
- console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap] [--stash-dirty]');
2810
+ console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--base merge-base|baseline] [--merge] [--skip-overlap] [--stash-dirty] [--force] [--takeover]');
2580
2811
  process.exit(1);
2581
2812
  }
2582
2813
  const checkOnly = args.includes('--check-only');
2583
2814
  const merge = args.includes('--merge');
2584
2815
  const skipOverlap = args.includes('--skip-overlap');
2585
2816
  const stashDirty = args.includes('--stash-dirty');
2817
+ const force = args.includes('--force'); // guard 相交预检显式解锁(task-02 / FR-03;人工 flag,自动路径永不带)
2818
+ const takeover = args.includes('--takeover'); // 所有权护栏显式强制接管(2026-09-14-change-ownership-guards task-02 / FR-01;人工 flag,自动路径永不带)
2586
2819
 
2587
2820
  // 解析 --base 参数(默认 merge-base)
2588
2821
  let base = 'merge-base';
@@ -2610,15 +2843,22 @@ SillySpec worktree — git worktree 隔离管理
2610
2843
  }
2611
2844
  // 主仓 apply 互斥锁(坑 main-apply-no-mutex):真 apply(非 checkOnly)在锁内跑——
2612
2845
  // apply 链(rollback/merge/cleanup)直接改主仓工作区,两会话并发互踩会互相清文件。
2846
+ // 所有权护栏(2026-09-14-change-ownership-guards task-02 / FR-01):锁内、实际 apply 前
2847
+ // 判归属——他人且活跃(活跃窗内)拒绝;--check-only 是只读路径不校验(铁律:只读命令不接)。
2848
+ // 拒绝时结构化错误已在护栏内打印,经哨兵返回值带出锁外统一 exit 1(不进结果展示分支)。
2613
2849
  let result;
2614
2850
  try {
2615
2851
  result = checkOnly
2616
- ? applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap, stashDirty })
2617
- : await withMainRepoLock(dir, wtName, 'apply', () => applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap, stashDirty }));
2852
+ ? applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap, stashDirty, force })
2853
+ : await withMainRepoLock(dir, wtName, 'apply', () => {
2854
+ if (!_guardChangeOwnership(wtName, { forced: takeover }).allowed) return { __ownershipDenied: true };
2855
+ return applyWorktree(wtName, { cwd: dir, checkOnly, merge, base, ctx: _applyCtx, skipOverlap, stashDirty, force });
2856
+ });
2618
2857
  } catch (lockErr) {
2619
2858
  console.error(`❌ ${lockErr.message}`);
2620
2859
  process.exit(1);
2621
2860
  }
2861
+ if (result && result.__ownershipDenied) process.exit(1);
2622
2862
 
2623
2863
  if (result.errors.length > 0) {
2624
2864
  console.error(`❌ 校验失败:`);
@@ -2754,15 +2994,29 @@ SillySpec worktree — git worktree 隔离管理
2754
2994
  if (assessment.decision === 'SAFE' || assessment.decision === 'WARNING') {
2755
2995
  console.log('Action: auto-applying...');
2756
2996
  const { applyWorktree, withMainRepoLock } = await import('./worktree-apply.js');
2757
- // 主仓互斥锁(与手动 apply 同款):自动 apply 同样改主仓工作区,必须互斥
2997
+ // 主仓互斥锁(与手动 apply 同款):自动 apply 同样改主仓工作区,必须互斥。
2998
+ // 所有权护栏(2026-09-14-change-ownership-guards task-02 / FR-01):assess 自动 apply
2999
+ // 是护栏①最可能的旁路(SAFE/WARNING 即自动改主仓+链内 cleanup)——锁内、实际动作前
3000
+ // 判归属。自动路径永不带 --takeover;他人活跃 change 拒绝 = 跳过自动落盘 + warning
3001
+ // 不抛错(同 overlapSkipped 软跳语义:无人值守不越权也不阻断审计流)。
2758
3002
  let applyResult;
2759
3003
  try {
2760
- applyResult = await withMainRepoLock(dir, wtName, 'assess-auto-apply', () => applyWorktree(wtName, { cwd: dir, ctx: _assessCtx }));
3004
+ applyResult = await withMainRepoLock(dir, wtName, 'assess-auto-apply', () => {
3005
+ if (!_guardChangeOwnership(wtName, { soft: true }).allowed) return { ownershipSkipped: true };
3006
+ return applyWorktree(wtName, { cwd: dir, ctx: _assessCtx, autoApply: true });
3007
+ });
2761
3008
  } catch (lockErr) {
2762
3009
  console.error(`❌ 自动 apply 未执行:${lockErr.message}`);
2763
3010
  break;
2764
3011
  }
2765
- if (applyResult.errors.length > 0) {
3012
+ if (applyResult.ownershipSkipped) {
3013
+ // 所有权拒绝(结构化 warning 已在护栏内打印):跳过自动落盘,指引人工显式接管
3014
+ console.log(' → 确认对方会话已放弃:人工评估后显式接管 sillyspec worktree apply ' + wtName + ' --takeover(或等对方收尾后重跑 assess)');
3015
+ } else if (applyResult.overlapSkipped) {
3016
+ // guard 相交软跳过(task-02 / FR-03):无人值守不越权也不阻断审计流——不落盘,亮 warning 指引人工评估
3017
+ for (const w of applyResult.warnings || []) console.log(`⚠️ ${w}`);
3018
+ console.log(' → 人工评估后显式 apply: sillyspec worktree apply ' + wtName + '(确认与活跃 quick 会话无冲突;或等对方 --done 后重跑 assess)');
3019
+ } else if (applyResult.errors.length > 0) {
2766
3020
  console.error('❌ apply 失败:', applyResult.errors.join('; '));
2767
3021
  } else {
2768
3022
  console.log(`✅ 已自动应用 ${applyResult.changedFiles.length} 个文件变更:`);
@@ -2855,15 +3109,23 @@ SillySpec worktree — git worktree 隔离管理
2855
3109
  }
2856
3110
  case 'cleanup': {
2857
3111
  if (!wtName) {
2858
- console.error('❌ 用法: sillyspec worktree cleanup <change-name>');
3112
+ console.error('❌ 用法: sillyspec worktree cleanup <change-name> [--force] [--takeover]');
2859
3113
  process.exit(1);
2860
3114
  }
2861
3115
  const forceFlag = args.includes('--force');
3116
+ const takeover = args.includes('--takeover'); // 所有权护栏显式强制接管(task-02 / FR-01)
2862
3117
  try {
2863
3118
  // 主仓互斥锁(坑 main-repo-no-mutex 二批):cleanup 删 worktree 注册表/分支/目录,
2864
- // 与并行会话的 apply/cleanup 互踩会互相清——与其他写主仓操作共用一把 main-repo.lock
3119
+ // 与并行会话的 apply/cleanup 互踩会互相清——与其他写主仓操作共用一把 main-repo.lock。
3120
+ // 所有权护栏(2026-09-14-change-ownership-guards task-02 / FR-01):锁内、实际清理前
3121
+ // 判归属——他人且活跃拒绝(结构化错误在护栏内打印,哨兵带出统一 exit 1,跨仓清理
3122
+ // 同步跳过);--takeover 显式强制接管放行。
2865
3123
  const { withMainRepoLock } = await import('./worktree-apply.js');
2866
- const result = await withMainRepoLock(dir, wtName, 'worktree-cleanup', () => wm.cleanup(wtName, { force: forceFlag }));
3124
+ const result = await withMainRepoLock(dir, wtName, 'worktree-cleanup', () => {
3125
+ if (!_guardChangeOwnership(wtName, { forced: takeover }).allowed) return { __ownershipDenied: true };
3126
+ return wm.cleanup(wtName, { force: forceFlag });
3127
+ });
3128
+ if (result && result.__ownershipDenied) process.exit(1);
2867
3129
  // 跨仓 worktree(坑 cross-repo-no-worktree-isolation):与主仓同锁清理——跨仓 worktree
2868
3130
  // 注册在各跨仓仓 .git 内,但目录/meta 在主仓 .sillyspec 运行时区,同样怕并发互踩
2869
3131
  const { cleanupCrossWorktrees } = await import('./worktree-cross.js');
@@ -2885,7 +3147,16 @@ SillySpec worktree — git worktree 隔离管理
2885
3147
  }
2886
3148
  } else if (result.result === 'skipped') {
2887
3149
  console.log(`⏭️ worktree 跳过清理: ${wtName} (mode: ${result.mode})`);
2888
- console.log(` 原因: in-place 模式没有隔离目录需要清理`);
3150
+ // ql-20260915-003 修复②(坑 cleanup-meta-fallback,2026-09-15 实证「mode: null
3151
+ // 跳过清理」但 worktree 实际创建过):早退跳过(meta/目录/git 注册三路探针全未
3152
+ // 命中)时,误导性的「in-place 模式」文案升级为诊断式——列三路探针路径 + meta
3153
+ // 丢失可能原因 + doctor 指引。probePaths 由 cleanup 兜底探测带回。
3154
+ if (Array.isArray(result.probePaths) && result.probePaths.length > 0) {
3155
+ for (const p of result.probePaths) console.log(` 探针: ${p}`);
3156
+ console.log(` meta 丢失可能原因:apply/cleanup 后随目录删除、并行会话已清理、cwd/specDir 漂移。可用 sillyspec worktree doctor 检查 git 注册与分支残留。`);
3157
+ } else {
3158
+ console.log(` 原因: in-place 模式没有隔离目录需要清理`);
3159
+ }
2889
3160
  } else if (result.result === 'blocked') {
2890
3161
  console.error(`🚫 拒绝清理:有未落主仓交付变更,请先 sillyspec worktree apply ${wtName} 或 --force`);
2891
3162
  } else if (result.result === 'partial') {
@@ -2907,7 +3178,7 @@ SillySpec worktree — git worktree 隔离管理
2907
3178
  const staleIdx = args.indexOf('--stale-hours');
2908
3179
  const staleHours = staleIdx !== -1 && args[staleIdx + 1] ? parseInt(args[staleIdx + 1], 10) : 24;
2909
3180
  const changeIdx = args.indexOf('--change');
2910
- const changeName = changeIdx !== -1 && args[changeIdx + 1] ? args[changeIdx + 1] : null;
3181
+ const changeName = changeIdx !== -1 && args[changeIdx + 1] && !String(args[changeIdx + 1]).startsWith("--") ? args[changeIdx + 1] : null;
2911
3182
  const diag = await wm.doctor({ fix: fixFlag, staleHours, changeName });
2912
3183
  if (diag.issues.length === 0) {
2913
3184
  console.log('✅ worktree 健康检查通过,无异常');
@@ -2990,7 +3261,7 @@ SillySpec dispatch — SillyHub 派发能力探测与策略生成(agent 调用
2990
3261
  if (dispatchSubCmd === 'hint') {
2991
3262
  // 解析 --contract <json>(仿 worktree 用 args.indexOf 取 flag 后一个值)
2992
3263
  const contractIdx = args.indexOf('--contract');
2993
- const contractRaw = contractIdx >= 0 && args[contractIdx + 1] ? args[contractIdx + 1] : null;
3264
+ const contractRaw = contractIdx >= 0 && args[contractIdx + 1] && !String(args[contractIdx + 1]).startsWith("--") ? args[contractIdx + 1] : null;
2994
3265
  if (!contractRaw) {
2995
3266
  console.error('用法: sillyspec dispatch hint --contract <json> [--json]\n 生成派发指令(DispatchContract:brief/worktreePath/branch/allowedPaths/readOnly/...)');
2996
3267
  process.exit(2);
@@ -3062,7 +3333,7 @@ SillySpec platform — SillyHub 平台同步
3062
3333
  // join(runtimeDir, `sync-conflict-${changeName}.json`) 可被 ../ 穿越出 .runtime
3063
3334
  {
3064
3335
  const pChangeIdx = args.indexOf('--change');
3065
- const pChangeVal = pChangeIdx >= 0 && args[pChangeIdx + 1] ? args[pChangeIdx + 1] : null;
3336
+ const pChangeVal = pChangeIdx >= 0 && args[pChangeIdx + 1] && !String(args[pChangeIdx + 1]).startsWith("--") ? args[pChangeIdx + 1] : null;
3066
3337
  if (pChangeVal) assertSafeChangeName(pChangeVal, 'platform --change 变更名');
3067
3338
  }
3068
3339
 
@@ -3130,7 +3401,7 @@ SillySpec platform — SillyHub 平台同步
3130
3401
  process.exit(1);
3131
3402
  }
3132
3403
  const tokenIdx = args.indexOf('--token');
3133
- const token = tokenIdx >= 0 && args[tokenIdx + 1] ? args[tokenIdx + 1] : undefined;
3404
+ const token = tokenIdx >= 0 && args[tokenIdx + 1] && !String(args[tokenIdx + 1]).startsWith("--") ? args[tokenIdx + 1] : undefined;
3134
3405
  if (!token) {
3135
3406
  // 缺 token 直接终止(体检 HUB-02):交互式输入尚未实现(task-11),此前
3136
3407
  // 继续执行会把 undefined 序列化成字符串 "undefined" 写入 local.yaml,
@@ -3142,17 +3413,20 @@ SillySpec platform — SillyHub 平台同步
3142
3413
  break;
3143
3414
  }
3144
3415
  case 'disconnect':
3145
- await syncModule.disconnect(dir);
3416
+ // --keep-pointer(坑 platform-sync-progress-rollback-and-db-corruption 坑1③):只清
3417
+ // local.yaml platform 段,保留指针/声明给 daemon 内嵌 CLI——回滚环恢复序列正规出口
3418
+ await syncModule.disconnect(dir, { keepPointer: filteredArgs.includes('--keep-pointer') });
3146
3419
  break;
3147
3420
  case 'sync': {
3148
3421
  const syncChangeIdx = args.indexOf('--change');
3149
- const syncChangeName = syncChangeIdx >= 0 && args[syncChangeIdx + 1] ? args[syncChangeIdx + 1] : null;
3150
- await syncModule.sync(syncChangeName, dir);
3422
+ const syncChangeName = syncChangeIdx >= 0 && args[syncChangeIdx + 1] && !String(args[syncChangeIdx + 1]).startsWith("--") ? args[syncChangeIdx + 1] : null;
3423
+ // manual:用户显式同步,change_deleted 回执噪音闸旁路(显式动作的回执必须可见)
3424
+ await syncModule.sync(syncChangeName, dir, { manual: true });
3151
3425
  break;
3152
3426
  }
3153
3427
  case 'sync-docs': {
3154
3428
  const syncDocsChangeIdx = args.indexOf('--change');
3155
- const syncDocsChangeName = syncDocsChangeIdx >= 0 && args[syncDocsChangeIdx + 1] ? args[syncDocsChangeIdx + 1] : null;
3429
+ const syncDocsChangeName = syncDocsChangeIdx >= 0 && args[syncDocsChangeIdx + 1] && !String(args[syncDocsChangeIdx + 1]).startsWith("--") ? args[syncDocsChangeIdx + 1] : null;
3156
3430
  await syncModule.syncDocuments(syncDocsChangeName, dir);
3157
3431
  break;
3158
3432
  }
@@ -3187,7 +3461,7 @@ SillySpec platform — SillyHub 平台同步
3187
3461
  // task-11 / D-006 / D-009 / FR-03:手动下行拉取平台进度到本地。
3188
3462
  // 与自动 triggerPull 共用 syncModule.pull(SyncManager.pull 实例方法),行为一致。
3189
3463
  const pullChangeIdx = args.indexOf('--change');
3190
- const pullChangeName = pullChangeIdx >= 0 && args[pullChangeIdx + 1] ? args[pullChangeIdx + 1] : null;
3464
+ const pullChangeName = pullChangeIdx >= 0 && args[pullChangeIdx + 1] && !String(args[pullChangeIdx + 1]).startsWith("--") ? args[pullChangeIdx + 1] : null;
3191
3465
  // 未连接明确提示不崩(acceptance:未连接平台输出明确提示)
3192
3466
  const smForCheck = new syncModule.SyncManager(dir);
3193
3467
  if (!smForCheck._getPlatform()) {
@@ -3231,7 +3505,7 @@ SillySpec platform — SillyHub 平台同步
3231
3505
  // 第一个非 flag 位置参数 → 唯一未决冲突自动选中。修复:旧实现盲取 platformArgs[0],
3232
3506
  // flag 放前面时(resolve --keep-local --change x)把 '--keep-local' 当变更名去读冲突文件。
3233
3507
  const changeIdx = args.indexOf('--change');
3234
- let resolveName = changeIdx >= 0 && args[changeIdx + 1] ? args[changeIdx + 1] : null;
3508
+ let resolveName = changeIdx >= 0 && args[changeIdx + 1] && !String(args[changeIdx + 1]).startsWith("--") ? args[changeIdx + 1] : null;
3235
3509
  if (!resolveName) {
3236
3510
  resolveName = platformArgs.find((a) => a && !a.startsWith('--')) || null;
3237
3511
  }
@@ -3297,7 +3571,7 @@ SillySpec platform — SillyHub 平台同步
3297
3571
  process.exit(1);
3298
3572
  }
3299
3573
  const reasonIdx = args.indexOf('--reason');
3300
- const reason = reasonIdx >= 0 && args[reasonIdx + 1] ? args[reasonIdx + 1] : undefined;
3574
+ const reason = reasonIdx >= 0 && args[reasonIdx + 1] && !String(args[reasonIdx + 1]).startsWith("--") ? args[reasonIdx + 1] : undefined;
3301
3575
  await syncModule.reject(rejectName, reason, dir);
3302
3576
  break;
3303
3577
  }
@@ -3321,7 +3595,7 @@ SillySpec platform — SillyHub 平台同步
3321
3595
  // task review / stage review / module-map)+ 轻形状检查(required-evidence / endpoints)
3322
3596
  // 为一把伞——「新解析器写之前先过 schema」的统一落点。skip = 未生成不算失败。
3323
3597
  const vaChangeIdx = args.indexOf('--change');
3324
- const vaChange = vaChangeIdx >= 0 && args[vaChangeIdx + 1] ? args[vaChangeIdx + 1] : null;
3598
+ const vaChange = vaChangeIdx >= 0 && args[vaChangeIdx + 1] && !String(args[vaChangeIdx + 1]).startsWith("--") ? args[vaChangeIdx + 1] : null;
3325
3599
  if (!vaChange) {
3326
3600
  console.error('用法: sillyspec validate --change <名> [--json]');
3327
3601
  console.error(' 校验该变更的全部结构化产物(verify-facts/required-evidence/task reviews/stage reviews/endpoints + module-map)');
@@ -3350,7 +3624,7 @@ SillySpec platform — SillyHub 平台同步
3350
3624
  // decisions list --change <c>——parseDecisions 只读视图(id/状态/类型/模块域/标题)。
3351
3625
  const decSub = filteredArgs[1];
3352
3626
  const decChangeIdx = args.indexOf('--change');
3353
- const decChange = decChangeIdx >= 0 && args[decChangeIdx + 1] ? args[decChangeIdx + 1] : null;
3627
+ const decChange = decChangeIdx >= 0 && args[decChangeIdx + 1] && !String(args[decChangeIdx + 1]).startsWith("--") ? args[decChangeIdx + 1] : null;
3354
3628
  if (!decChange || !['add', 'list'].includes(decSub)) {
3355
3629
  console.error('用法: sillyspec decisions add --change <名> --id D-001 --title "…" [--version N] [--status confirmed] [--type architecture] [--domain x(可重复)] [--question/--answer/--anchor/--reject-reason/--revisit-when/--supersedes/--impacts/--note]\n sillyspec decisions list --change <名>');
3356
3630
  process.exit(2);
@@ -3649,12 +3923,12 @@ SillySpec workflow — 工作流管理
3649
3923
  }
3650
3924
  // 解析 --project
3651
3925
  const projectIdx = filteredArgs.indexOf('--project');
3652
- const project = projectIdx !== -1 && filteredArgs[projectIdx + 1] ? filteredArgs[projectIdx + 1] : null;
3926
+ const project = projectIdx !== -1 && filteredArgs[projectIdx + 1] && !String(filteredArgs[projectIdx + 1]).startsWith("--") ? filteredArgs[projectIdx + 1] : null;
3653
3927
  // 解析 --json(已在顶层解析)
3654
3928
  const isJson = json;
3655
3929
  // 解析 --change
3656
3930
  const changeIdx = filteredArgs.indexOf('--change');
3657
- const changeName = changeIdx !== -1 && filteredArgs[changeIdx + 1] ? filteredArgs[changeIdx + 1] : null;
3931
+ const changeName = changeIdx !== -1 && filteredArgs[changeIdx + 1] && !String(filteredArgs[changeIdx + 1]).startsWith("--") ? filteredArgs[changeIdx + 1] : null;
3658
3932
 
3659
3933
  if (!project && wfName !== 'archive-impact') {
3660
3934
  console.error('❌ 请指定 --project,例如:--project sillyspec');
@@ -3757,7 +4031,7 @@ SillySpec modules — 模块文档管理
3757
4031
  // per-task 模块卡级联解析(token 成本优化 P0a):execute「加载上下文」步的
3758
4032
  // {MODULE_RESOLVE_TABLE} 注入同源;独立命令供 agent/人随时查(细卡优先,防整读大卡)
3759
4033
  const mrChangeIdx = args.indexOf('--change');
3760
- const mrChange = mrChangeIdx >= 0 && args[mrChangeIdx + 1] ? args[mrChangeIdx + 1] : null;
4034
+ const mrChange = mrChangeIdx >= 0 && args[mrChangeIdx + 1] && !String(args[mrChangeIdx + 1]).startsWith("--") ? args[mrChangeIdx + 1] : null;
3761
4035
  if (!mrChange) {
3762
4036
  console.error('用法: sillyspec modules resolve --change <名> [--spec-dir <path>] [--json]\n 输出 per-task 最优模块卡表(跨全部 _module-map.yaml 最长前缀匹配,细卡优先)');
3763
4037
  process.exit(2);
@@ -3765,7 +4039,8 @@ SillySpec modules — 模块文档管理
3765
4039
  assertSafeChangeName(mrChange, '--change 变更名');
3766
4040
  const mrSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
3767
4041
  const { resolveChangeModuleCards, renderModuleResolveTable } = await import('./module-resolve.js');
3768
- if (filteredArgs.includes('--json')) {
4042
+ if (json) {
4043
+ // 全局解析器把 --json 收进顶层 json 变量、不进 filteredArgs——查 filteredArgs 恒 false(ql-20260911-029)
3769
4044
  const r = resolveChangeModuleCards({ cwd: dir, specBase: mrSpecBase, changeName: mrChange });
3770
4045
  console.log(JSON.stringify(r, null, 2));
3771
4046
  } else {
@@ -3818,7 +4093,7 @@ SillySpec modules — 模块文档管理
3818
4093
  const { registerRepoInLocalYaml } = await import('./local-register.js');
3819
4094
  let rrResult;
3820
4095
  try {
3821
- rrResult = registerRepoInLocalYaml(target.path, rrKey, absRepo);
4096
+ rrResult = await registerRepoInLocalYaml(target.path, rrKey, absRepo);
3822
4097
  } catch (e) {
3823
4098
  console.error(`❌ 注册失败: ${e.message}`);
3824
4099
  process.exit(1);