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
@@ -15,13 +15,20 @@
15
15
  * 行数三档(D-002):tracked → numstat;untracked 新文件 → wc-l 记全 + 行;binary(numstat
16
16
  * 两列 '-')→ additions/deletions=null。三态(full-flow):planned / unplanned / untouched。
17
17
  *
18
+ * quick 画像出口(2026-09-14-quick-exit-tiered-gates task-03,FR-04 / D-008):quick 模式结果
19
+ * 增量携带 gateProfile(实时态透传 auditQuickCompletion 挂的 review.gateProfile,不重复计算;
20
+ * 冻结重放态透传 quicklog/patches 记录冻结值,旧记录按 rows 清单重算),表格出口按存在性
21
+ * 追加 [gate] 画像段、--json 出口随结果对象序列化——画像的独立可重放出口与阈值校准数据源。
22
+ * 既有三态对账表/归属表/--file 出口与返回字段语义零变化(gateProfile 为增量字段)。
23
+ *
18
24
  * 全 advisory(D-006):不抛错阻断、不写门禁状态;任何异常 catch 后并入 degradedReason。
19
25
  * 纯读:不落盘、不改 guard/进度库。Windows 路径 \\→/ 归一,ESM-only,零新依赖。
20
26
  */
21
27
  import { existsSync, readFileSync, readdirSync } from 'node:fs'
22
28
  import { createHash } from 'node:crypto'
23
29
  import { join, dirname } from 'node:path'
24
- import { locateQuickSessionGuard, auditQuickCompletion, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, ancestorSpecDirs } from './run/shared.js'
30
+ import { locateQuickSessionGuard, auditQuickCompletion, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, ancestorSpecDirs, isQuickMetadata } from './run/shared.js'
31
+ import { computeGateProfile, resolveGateThresholds } from './quick-gate-profile.js'
25
32
  import { safeGit } from './git-helper.js'
26
33
  import { parseFileChangeListDetailed, pathMatches } from './change-list.js'
27
34
 
@@ -266,18 +273,20 @@ function filterPatchForFiles(patchText, filesSet) {
266
273
  }
267
274
 
268
275
  /**
269
- /** patch 内容 sha256(A-F01 防篡改锚——写入方存 json.patchSha256,读取方校验) */
270
- function sha256Text(text) {
271
- return createHash('sha256').update(text, 'utf8').digest('hex')
276
+ * patch sha256——换行归一化口径(quick-ec5fc714):git autocrlf 会在提交/检出时把 patch
277
+ * 转 CRLF(平台仓实测 mtime 晚落盘一天的假篡改告警)——双侧统一 LF 归一后取 hash。
278
+ */
279
+ function sha256PatchNormalized(text) {
280
+ return createHash('sha256').update(String(text).replace(/\r\n/g, '\n'), 'utf8').digest('hex')
272
281
  }
273
282
 
274
283
  /**
275
- * 冻结 patch 完整性校验(A-F01):json 记录的 patchSha256 与 patch 实文比对。
284
+ * 冻结 patch 完整性校验(A-F01):json 记录的 patchSha256 与 patch 实文比对(LF 归一口径)。
276
285
  * @returns {boolean|null} true=一致;false=不匹配(可能被篡改);null=未记录 hash(旧记录)
277
286
  */
278
287
  function verifyPatchIntegrity(recordedSha, patchText) {
279
288
  if (!recordedSha || typeof recordedSha !== 'string') return null
280
- return sha256Text(patchText) === recordedSha
289
+ return sha256PatchNormalized(patchText) === recordedSha
281
290
  }
282
291
 
283
292
  /**
@@ -344,7 +353,8 @@ function slicePatchForFile(patchText, filePath) {
344
353
 
345
354
  /**
346
355
  * quick 记录态反查(guard 清理后):扫祖先链各 specBase 的 quicklog/patches/*.json,
347
- * 按 json 内冗余的 sessionId 匹配。返回记录 + patch 路径(--file 切片源)。
356
+ * 按 json 内冗余的 sessionId 匹配。返回记录 + patch 路径(--file 切片源)+ specBase
357
+ * (命中目录——画像重放的 module-map/local.yaml 加载锚,task-03)。
348
358
  */
349
359
  function findQuickPatchRecord(cwd, sessionId) {
350
360
  for (const sb of ancestorSpecDirs(cwd)) {
@@ -355,7 +365,7 @@ function findQuickPatchRecord(cwd, sessionId) {
355
365
  try {
356
366
  const j = JSON.parse(readFileSync(join(dir, f), 'utf8'))
357
367
  if (j && j.sessionId === sessionId && Array.isArray(j.rows)) {
358
- return { record: j, patchPath: join(dir, f.replace(/\.json$/, '.patch')), qlId: j.qlId || f.replace(/\.json$/, '') }
368
+ return { record: j, patchPath: join(dir, f.replace(/\.json$/, '.patch')), qlId: j.qlId || f.replace(/\.json$/, ''), specBase: sb }
359
369
  }
360
370
  } catch { /* 单文件损坏跳过 */ }
361
371
  }
@@ -363,6 +373,167 @@ function findQuickPatchRecord(cwd, sessionId) {
363
373
  return null
364
374
  }
365
375
 
376
+ /** QUICKLOG 条目号形态(ql-20260914-001-abcd:日期-序号-短 hash,短 hash 4 hex) */
377
+ const QL_ID_RE = /^ql-\d{8}-\d{3}-[0-9a-f]{4}$/
378
+
379
+ /**
380
+ * ql-xxx → quick-xxx 反查(用户「为什么不直接用 ql-xxx 查」根治,ql-20260910-014 设计:
381
+ * 映射只在本机 guard,会话结束即清;patches 记录按 qlId 命名且 json 冗余 sessionId——
382
+ * 平台可抓的持久映射)。按文件名前缀匹配(qlId=文件名),避免逐文件解析全目录。
383
+ */
384
+ function findQuickSessionByQlId(cwd, qlId) {
385
+ const norm = String(qlId || '').trim()
386
+ if (!QL_ID_RE.test(norm)) return null
387
+ for (const sb of ancestorSpecDirs(cwd)) {
388
+ try {
389
+ const jsonPath = join(sb, 'quicklog', 'patches', `${norm}.json`)
390
+ if (!existsSync(jsonPath)) continue
391
+ const j = JSON.parse(readFileSync(jsonPath, 'utf8'))
392
+ if (j && typeof j.sessionId === 'string' && QUICK_SID_RE.test(j.sessionId)) {
393
+ return { sessionId: j.sessionId, record: j, patchPath: join(sb, 'quicklog', 'patches', `${norm}.patch`), qlId: norm, specBase: sb }
394
+ }
395
+ } catch { /* 单文件损坏跳过 */ }
396
+ }
397
+ return null
398
+ }
399
+
400
+ /**
401
+ * quick 记录态回放结果构建(共用:guard 清理后 sessionId 反查 + ql-xxx 反查两入口)。
402
+ * 画像重放(FR-04 / D-008 / task-03)+ 「声明即归属」过滤(quick-ec5fc714)。
403
+ */
404
+ async function buildQuickRecordResult(rec, requestedName) {
405
+ let gateProfile = null
406
+ try {
407
+ if (rec.record.gateProfile && typeof rec.record.gateProfile === 'object') {
408
+ gateProfile = rec.record.gateProfile
409
+ } else {
410
+ const rowsPaths = rec.record.rows
411
+ .map(r => (r && typeof r.path === 'string' ? r.path : ''))
412
+ .filter(Boolean)
413
+ if (rowsPaths.length > 0) {
414
+ const thresholds = await resolveReplayGateThresholds(rec.specBase)
415
+ gateProfile = computeGateProfile(
416
+ rowsPaths,
417
+ await loadGateModuleIndex(rec.specBase, rowsPaths),
418
+ thresholds ? { thresholds } : {},
419
+ )
420
+ }
421
+ }
422
+ } catch { gateProfile = null }
423
+ const filteredRows = rec.record.rows.filter(r => r && !isQuickMetadata(r.path || '', []))
424
+ const filterDropped = rec.record.rows.length - filteredRows.length
425
+ const viaQl = requestedName !== rec.record.sessionId && QL_ID_RE.test(requestedName)
426
+ return {
427
+ mode: 'quick', ok: true, degradedReason: null,
428
+ baseAnchor: rec.record.baseAnchor || `quick-window:${rec.record.sessionId || requestedName}`,
429
+ totals: rec.record.totals && typeof rec.record.totals === 'object' && filterDropped === 0
430
+ ? rec.record.totals
431
+ : { files: filteredRows.length, ...sumTotals(filteredRows) },
432
+ rows: filteredRows,
433
+ excluded: rec.record.excluded && Array.isArray(rec.record.excluded.foreignDeclared)
434
+ ? rec.record.excluded
435
+ : { foreignDeclared: [] },
436
+ frozenPatchPath: rec.patchPath,
437
+ patchSha256: rec.record.patchSha256 || null,
438
+ patchStatus: rec.record.patchStatus || null,
439
+ gateProfile,
440
+ note: `quick 会话已收尾——记录态(quicklog/patches/${rec.qlId},${rec.record.savedAt ? String(rec.record.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘' : '--done 时点冻结'})${viaQl ? `;经 ql-Id 反查(会话 ${rec.record.sessionId})` : ''}${filterDropped > 0 ? `;回放已剔除 ${filterDropped} 个 quick 元数据/并行变更目录文件(范围视图收紧)` : ''}`,
441
+ }
442
+ }
443
+
444
+ /**
445
+ * quick 审计链 module-map 的项目推导(2026-09-14-quick-exit-tiered-gates task-03):
446
+ * loadQuickModuleIndex(run/shared.js,未 export)的加载路径是
447
+ * <specBase>/docs/<projectName>/modules/_module-map.yaml——scope-audit 侧无 progress 上下文
448
+ * 拿 projectName,按候选 map 推导:
449
+ * - 单候选 → 直取(绝大多数仓单项目);
450
+ * - 多候选 + 样本文件(实时态=guard.allowedFiles 声明面 / 重放态=记录 rows 冻结集)→ 按
451
+ * 「样本归属得分唯一最高」消歧(computeGateProfile.unmappedFiles 同款归属口径,纯函数
452
+ * 零 IO 重用——本仓 dashboard+sillyspec 双 map 实证需要);
453
+ * - 零候选 / 无样本 / 得分平分 → null(画像走 degraded 档,fail-open 不猜,兼容策略第 1 条)。
454
+ * @param {string} specBase
455
+ * @returns {string[]} 带 _module-map.yaml 的项目名清单
456
+ */
457
+ function listModuleMapProjects(specBase) {
458
+ if (!specBase) return []
459
+ try {
460
+ const docsDir = join(specBase, 'docs')
461
+ return readdirSync(docsDir).filter(n => existsSync(join(docsDir, n, 'modules', '_module-map.yaml')))
462
+ } catch {
463
+ return []
464
+ }
465
+ }
466
+
467
+ /** 单项目 _module-map.yaml 读取(parseModuleMapSimple canonical 解析,与 loadQuickModuleIndex 同款);不可得/解析空 → null。 */
468
+ async function readModuleMap(specBase, project) {
469
+ try {
470
+ const { parseModuleMapSimple } = await import('./modules.js')
471
+ const idx = parseModuleMapSimple(readFileSync(join(specBase, 'docs', project, 'modules', '_module-map.yaml'), 'utf8'))
472
+ return idx && Object.keys(idx).length > 0 ? idx : null
473
+ } catch {
474
+ return null
475
+ }
476
+ }
477
+
478
+ /**
479
+ * 项目名推导(口径见 listModuleMapProjects 头注释):唯一得分(样本非文档文件中归属最少
480
+ * unmapped 的 map)才采信,平分/无样本/多候选全失败 → null。
481
+ * @param {string} specBase
482
+ * @param {string[]} [sampleFiles] 归属得分样本(声明面 / 记录 rows)
483
+ * @returns {Promise<string|null>}
484
+ */
485
+ async function pickModuleMapProject(specBase, sampleFiles = []) {
486
+ const names = listModuleMapProjects(specBase)
487
+ if (names.length === 1) return names[0]
488
+ if (names.length === 0 || !Array.isArray(sampleFiles)) return null
489
+ const files = sampleFiles.map(f => String(f || '')).filter(Boolean)
490
+ if (files.length === 0) return null
491
+ let best = null
492
+ let bestUnmapped = Infinity
493
+ let tie = false
494
+ for (const n of names) {
495
+ const idx = await readModuleMap(specBase, n)
496
+ if (!idx) continue
497
+ const unmapped = computeGateProfile(files, idx).unmappedFiles.length
498
+ if (unmapped < bestUnmapped) { best = n; bestUnmapped = unmapped; tie = false }
499
+ else if (unmapped === bestUnmapped) tie = true
500
+ }
501
+ return tie ? null : best
502
+ }
503
+
504
+ /**
505
+ * 画像重放的 moduleIndex 加载(task-03,FR-04):与 run/shared.js loadQuickModuleIndex 同链路
506
+ * ——同路径 + parseModuleMapSimple(canonical)解析,项目按记录 rows 样本消歧;不可得 → null
507
+ * (computeGateProfile 走 degraded 档)。动态 import modules.js 隔离依赖(shared.js 同款)。
508
+ * @param {string} specBase quicklog/patches 记录命中的 spec 根
509
+ * @param {string[]} [sampleFiles] 项目消歧样本(记录 rows)
510
+ * @returns {Promise<object|null>}
511
+ */
512
+ async function loadGateModuleIndex(specBase, sampleFiles = []) {
513
+ const project = await pickModuleMapProject(specBase, sampleFiles)
514
+ return project ? readModuleMap(specBase, project) : null
515
+ }
516
+
517
+ /**
518
+ * 画像重放的阈值合并(task-03,D-009):与 auditQuickCompletion 画像挂载段同链路——
519
+ * <specBase>/local.yaml(= 实时态 readLocalYamlRaw(sessionRoot) 读的同一文件)+ js-yaml +
520
+ * resolveGateThresholds;缺失/坏 YAML → null(computeGateProfile 内回 THRESHOLDS 默认)。
521
+ * @param {string} specBase
522
+ * @returns {Promise<object|null>}
523
+ */
524
+ async function resolveReplayGateThresholds(specBase) {
525
+ try {
526
+ const p = join(specBase, 'local.yaml')
527
+ if (!existsSync(p)) return null
528
+ const mod = await import('js-yaml')
529
+ const yamlLoad = mod.load || mod.default?.load
530
+ const cfg = yamlLoad(readFileSync(p, 'utf8'))
531
+ return cfg && typeof cfg === 'object' ? resolveGateThresholds(cfg) : null
532
+ } catch {
533
+ return null
534
+ }
535
+ }
536
+
366
537
  /** 汇总非 null 行数(binary 与降级 null 不计入,不出伪数据) */
367
538
  function sumTotals(rows) {
368
539
  let additions = 0
@@ -391,6 +562,16 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
391
562
  try {
392
563
  const mergedGuard = {
393
564
  ...guard,
565
+ // 画像输入对齐(2026-09-14-quick-exit-tiered-gates task-03,FR-04):specBase/projectName
566
+ // 透传——complete-handlers :1050 同款 merge(「--done 收尾同源实时采集」的既有约定)补齐。
567
+ // guard.json 落盘不含这两键,缺省会让 auditQuickCompletion 挂的 review.gateProfile 恒走
568
+ // degraded 档(moduleSpan=null),与 --done 时点 review.gateProfile 字段不一致。projectName
569
+ // 无 progress 上下文,按 module-map 候选推导(声明会话以 allowedFiles 为归属得分样本;
570
+ // 零候选/无样本/平分 → null → degraded,fail-open)。specBase/projectName 在 audit 内只喂
571
+ // quicklog 目录解析(与 cwd 推导同一路径)、docSyncHint 模块归属与 gate 画像(computeQuickAudit
572
+ // 不消费 docSyncHint),不触 status/reasons/归属切分语义。
573
+ specBase: guardSpecBase,
574
+ projectName: await pickModuleMapProject(guardSpecBase, guard.allowedFiles),
394
575
  otherSessionsDeclared: collectOtherQuickSessionDeclarations(platformOpts, guardSpecBase, sessionId),
395
576
  }
396
577
  audit = await auditQuickCompletion(sessionRoot, mergedGuard, {})
@@ -419,14 +600,39 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
419
600
  return { ...base, ok: false, degradedReason: `quick 窗口审计失败: ${hardFailure}` }
420
601
  }
421
602
 
422
- // 归属切分(消费 auditQuickCompletion 产物,不重算):attributedFiles(含 sameFileHits)→
423
- // declared;undeclaredFiles − softTestFiles → undeclared;softTestFiles → soft。
603
+ // 归属切分(quick-ec5fc714 平台实测修正):scope-audit 是**范围视图**不是门禁——按
604
+ // 「声明即归属」硬切:declared=声明∩窗口;soft=软归属;其余(含 quick 元数据/其他变更
605
+ // 目录/linkedChanges 文件——audit 侧刻意保留它们供审计行落盘不静默挤走,范围视图则不该
606
+ // 把别人的活算进来)归为 undeclared 仅 note 交代。窗口文件全在 undeclared(如声明文件
607
+ // 已提交无 diff)时,undeclared 升格进 rows(范围视图只展示改了的东西)——本 quick
608
+ // 实测:声明两 pi-rpc 文件已提交 + 他者变更目录 19 文件未声明,行数贡献恰来自后者。
424
609
  const attributed = Array.isArray(audit.attributedFiles) ? audit.attributedFiles.map(toPosix) : []
425
610
  const undeclared = Array.isArray(audit.undeclaredFiles) ? audit.undeclaredFiles.map(toPosix) : []
426
611
  const softSet = new Set((Array.isArray(audit.softTestFiles) ? audit.softTestFiles : []).map(toPosix))
427
612
  const attrSet = new Set(attributed)
428
-
429
- const rowFiles = [...new Set([...attributed, ...undeclared])]
613
+ const linkedChangesArr = Array.isArray(guard.linkedChanges) ? guard.linkedChanges.map(c => String(c).replace(/\\/g, '/')) : []
614
+ const isLinkedLeftover = (f) => linkedChangesArr.some(lc => f.startsWith(`.sillyspec/changes/${lc}/`) || f.startsWith(`.sillyspec/changes/archive/${lc}/`))
615
+
616
+ let rowFiles
617
+ let dropNote = null
618
+ if (attributed.length > 0 || softSet.size > 0) {
619
+ // 有归属文件时:undeclared 全部剔出范围视图——undeclared 即「不属于本 quick」的文件
620
+ // (他者变更目录/quicklog 产物/无关未提交),audit 侧留它们供审计行追溯,范围视图不该
621
+ // 展示(平台仓实测:窗口 13 个文件里 11 个是他者变更目录,唯二归属文件已提交)。
622
+ rowFiles = [...new Set([...attributed, ...softSet])]
623
+ if (undeclared.length > 0) {
624
+ dropNote = `${undeclared.length} 个未声明文件不计入范围视图(他者变更目录/quicklog 产物/漏声明——audit 行有追溯)`
625
+ }
626
+ } else {
627
+ // 窗口全 undeclared(如声明文件已提交无 diff):范围视图仍剔 quick 元数据/变更目录
628
+ // ——audit 侧留它们供审计行追溯,范围视图不展示他者活(platform quick-ffb92f60 实测:
629
+ // 19 个 undeclared 中 17 个是他者变更目录/quicklog 产物,唯二声明文件已提交)。真未知的
630
+ // 源码/业务文件保留进 rows(他者窗口改动或漏声明,note 交代)。
631
+ rowFiles = [...new Set(undeclared.filter(f => !isQuickMetadata(f, linkedChangesArr) && !isLinkedLeftover(f)))]
632
+ if (rowFiles.length < undeclared.length) {
633
+ dropNote = `${undeclared.length - rowFiles.length} 个 quick 元数据/他者变更目录文件不计入范围视图`
634
+ }
635
+ }
430
636
  const stats = collectNumstatByPath(sessionRoot, rowFiles, { baseRef: 'HEAD' })
431
637
 
432
638
  const rows = rowFiles.map(f => {
@@ -446,7 +652,7 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
446
652
 
447
653
  // 「quick 已提交」降级(design 总体方案):窗口空(changedFiles 空)且 QUICKLOG 已有该会话
448
654
  // 条目 → 记录态提示读 QUICKLOG,不以空表冒充实时。
449
- let note = null
655
+ let note = dropNote
450
656
  if (rowFiles.length === 0) {
451
657
  const qlId = guard.quicklogId || null
452
658
  if (quicklogHasEntry(guardSpecBase, qlId)) {
@@ -475,6 +681,11 @@ async function computeQuickAudit({ cwd, platformOpts, sessionId, located, collec
475
681
  },
476
682
  frozenPatch,
477
683
  note,
684
+ // 画像透传(FR-04 / task-03 gateProfile-json):auditQuickCompletion 挂的 review.gateProfile
685
+ // 原样随 review 携带进结果对象——不重复计算(与 --done 时点同源);fail-open 时为 null
686
+ // (消费方按存在性读取)。complete-handlers --done 落 quicklog/patches/<qlId>.json 时随
687
+ // 快照对象冻结,供冻结重放路径透传。
688
+ gateProfile: audit.gateProfile && typeof audit.gateProfile === 'object' ? audit.gateProfile : null,
478
689
  }
479
690
  }
480
691
 
@@ -562,16 +773,71 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
562
773
  : `design.md 不存在(${toPosix(designMdPath)})——降级实际侧 only 视图(不出三态列)`
563
774
  }
564
775
 
565
- // —— 实际侧(verify-postcheck.js task-02 契约:files + baseAnchor;动态 import 隔离
566
- // 并行时序——task-02 未落地时降级说明而非炸)——
776
+ // —— 实际侧采集 ——
777
+ // 形态 A(worktree 存活,quick-ec5fc714):**自采纯 worktree 口径**——锚(meta baselineCommit
778
+ // 优先)..worktree 工作区 = 本变更真实边界(子代理未提交改动 + 分支已提交)。不沿用
779
+ // resolveVerifyChangedFiles(其语义是「verify 证据核验面」:主仓∪worktree 并集——对证据核验
780
+ // 合理,对本变更范围全是噪音:主仓侧 20 并行文件混入,且它们对 worktree 锚 diff miss 后落
781
+ // wc-l 档记成整文件行数,两放大器叠加出 +11840/22 文件的巨型表)。
782
+ // 形态 B(post-apply):继续走 resolveReconcileActualFiles 三源(本变更活已回主仓,语义正确)。
567
783
  let actual = null
568
784
  let actualFailure = null
569
785
  try {
570
- const mod = await import('./verify-postcheck.js')
571
- if (typeof mod.resolveReconcileActualFiles !== 'function') {
572
- throw new Error('resolveReconcileActualFiles 未导出(依赖契约未就绪)')
786
+ const metaPath = join(sb, '.runtime', 'worktrees', changeName, 'meta.json')
787
+ if (existsSync(metaPath)) {
788
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
789
+ const wtRoot = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
790
+ ? meta.worktreePath
791
+ : cwd
792
+ const anchor = meta.baselineCommit || meta.actualBaseHash || meta.baseHash || null
793
+ if (anchor) {
794
+ const diffOut = safeGit(wtRoot, ['diff', '--name-only', anchor], { timeout: 30 * 1000 })
795
+ if (!diffOut.error && typeof diffOut.value === 'string') {
796
+ // 过滤口径与 resolveVerifyChangedFiles 的 wtFiles 同款(保留 docs 交付物,排运行时/进度面)
797
+ const wtFilter = (p) => p && p !== '.sillyspec'
798
+ && !p.startsWith('.sillyspec/changes/')
799
+ && !p.startsWith('.sillyspec/.runtime/')
800
+ && !p.startsWith('.sillyspec/quicklog/')
801
+ const files = diffOut.value.split('\n')
802
+ .map(p => toPosix(p.replace(/^"|"$/g, '').trim()))
803
+ .filter(wtFilter)
804
+ // untracked 新文件不在 diff(git diff 只看 tracked)——status --porcelain 并入
805
+ // (与旧链路 includeWorkingTree 的 wtFiles 半边同覆盖,行数侧走 wc-l 档)
806
+ const st = safeGit(wtRoot, ['status', '--porcelain', '--untracked-files=all'], { timeout: 30 * 1000, trim: false })
807
+ if (!st.error && typeof st.value === 'string') {
808
+ for (const line of st.value.split('\n')) {
809
+ if (!line.startsWith('??')) continue
810
+ const p = toPosix(line.slice(3).trim().replace(/^"|"$/g, '').split(' -> ').pop() || '')
811
+ if (wtFilter(p) && !files.includes(p)) files.push(p)
812
+ }
813
+ }
814
+ actual = { ok: true, form: 'worktree', files, sources: ['worktree:diff-anchor..worktree', 'worktree:status-untracked'], foreignExcluded: 0, degradedReason: null, baseAnchor: anchor }
815
+ } else {
816
+ actual = { ok: false, form: 'worktree', files: [], sources: [], foreignExcluded: 0, degradedReason: 'worktree 锚定 diff 失败', baseAnchor: null }
817
+ }
818
+ } else {
819
+ // 无锚 meta(baseHash 等字段缺失):退 HEAD 未提交窗口口径——文件集=status 全量
820
+ // (tracked 改动 + untracked),行数侧由主链路 HEAD 兜底(quick-8aa52289 契约不变)
821
+ const st = safeGit(wtRoot, ['status', '--porcelain', '--untracked-files=all'], { timeout: 30 * 1000, trim: false })
822
+ const files = []
823
+ if (!st.error && typeof st.value === 'string') {
824
+ for (const line of st.value.split('\n')) {
825
+ const p = toPosix(line.slice(3).trim().replace(/^"|"$/g, '').split(' -> ').pop() || '')
826
+ if (p && p !== '.sillyspec'
827
+ && !p.startsWith('.sillyspec/changes/')
828
+ && !p.startsWith('.sillyspec/.runtime/')
829
+ && !p.startsWith('.sillyspec/quicklog/')) files.push(p)
830
+ }
831
+ }
832
+ actual = { ok: true, form: 'worktree', files, sources: ['worktree:status-porcelain(HEAD-window)'], foreignExcluded: 0, degradedReason: null, baseAnchor: null }
833
+ }
834
+ } else {
835
+ const mod = await import('./verify-postcheck.js')
836
+ if (typeof mod.resolveReconcileActualFiles !== 'function') {
837
+ throw new Error('resolveReconcileActualFiles 未导出(依赖契约未就绪)')
838
+ }
839
+ actual = mod.resolveReconcileActualFiles({ cwd, specBase: sb, runtimeRoot, changeName })
573
840
  }
574
- actual = mod.resolveReconcileActualFiles({ cwd, specBase: sb, runtimeRoot, changeName })
575
841
  } catch (e) {
576
842
  actualFailure = e && e.message ? String(e.message).split('\n')[0] : String(e)
577
843
  }
@@ -596,6 +862,29 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
596
862
  return { ...base, degradedReason: [reason, planDegraded].filter(Boolean).join(';') }
597
863
  }
598
864
 
865
+ // —— execute --done 采集·apply-pathspec 归属收窄(quick-ec5fc714):apply 时点在 .runtime
866
+ // 落过本变更精确文件清单(worktree-apply.js 产 apply-pathspec-<change>.txt)——execute 采集
867
+ // 形态(freshActual+collectPatch,仅 printExecuteScopeAudit 组合)下按它收窄 actual.files,
868
+ // 快照/patch 冻**归属面**而非开放区间(平台仓实测:186KB 快照中 23/44 段为并行会话 docs
869
+ // 同步等噪音)。查询面不收窄(快照说什么是什么);pathspec 缺失退开放区间(in-place 等)。
870
+ let narrowNote = null
871
+ if (freshActual && collectPatch) {
872
+ try {
873
+ const pathspecFile = join(runtimeRoot, `apply-pathspec-${changeName}.txt`)
874
+ const owned = readFileSync(pathspecFile, 'utf8').split('\n').map(l => toPosix(l.trim())).filter(Boolean)
875
+ if (owned.length > 0) {
876
+ const ownedSet = new Set(owned)
877
+ const before = actual.files.length
878
+ actual = { ...actual, files: actual.files.filter(f => ownedSet.has(toPosix(f))) }
879
+ // 收窄掉的并行噪音可见性保留(审计不静默挤走)
880
+ const dropped = before - actual.files.length
881
+ if (dropped > 0) {
882
+ narrowNote = `快照已按 apply-pathspec 收窄至本变更归属文件(剔除开放区间并行文件 ${dropped} 个——它们归属各自会话/变更)`
883
+ }
884
+ }
885
+ } catch { /* pathspec 缺失 → 开放区间(旧行为) */ }
886
+ }
887
+
599
888
  // —— 已收尾变更·冻结快照优先(真实 > 冻结记录 > 实时开放区间):归档变更的范围应封闭在
600
889
  // apply 时点,不随后续主仓改动漂移(quick-f5acdeeb);quick-bd84b852 放宽——**post-apply
601
890
  // (分支已删 = execute 已 apply 收尾)的活跃变更同样收尾**,此后实时区间只会被并行演进泡大
@@ -669,6 +958,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
669
958
  const degraded = []
670
959
  if (planDegraded) degraded.push(planDegraded)
671
960
  const notes = []
961
+ if (narrowNote) notes.push(narrowNote)
672
962
  // 已收尾但快照缺失(上方分支置位):实时开放区间兜底,明示漂移语义——快照机制上线
673
963
  // (2026-09-10)前收尾的旧变更冻结记录无法重建,如实告知替代渠道。
674
964
  if (noSnapshotDrift) {
@@ -766,7 +1056,12 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
766
1056
  * kind: 'binary'|'new'|'modified'|'deleted',
767
1057
  * verdict?: 'planned'|'unplanned'|'untouched', attribution?: 'declared'|'soft'|'undeclared' }>,
768
1058
  * excluded: { foreignDeclared: Array<{ file: string, sessions: string[] }> },
769
- * note?: string|null }>}
1059
+ * note?: string|null,
1060
+ * gateProfile?: object|null }>}
1061
+ * gateProfile(2026-09-14-quick-exit-tiered-gates task-03 增量字段,消费方按存在性读取):
1062
+ * 仅 quick 模式携带——实时态透传 auditQuickCompletion 挂的 review.gateProfile(不重复计算);
1063
+ * 冻结重放态透传 quicklog/patches 记录冻结值(旧记录无 → rows 文件清单现算重放)。full-flow
1064
+ * 全流程变更恒无该字段(零回归契约)。
770
1065
  */
771
1066
  export async function computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch, freshActual } = {}) {
772
1067
  const empty = {
@@ -778,6 +1073,21 @@ export async function computeChangeScopeAudit({ cwd, specBase, changeName, platf
778
1073
  if (!cwd || !changeName) {
779
1074
  return { ...empty, degradedReason: '参数缺失:cwd 与 changeName 必填' }
780
1075
  }
1076
+ // ql-xxx 反查(「为什么不直接用 ql-xxx 查」根治):patches 记录按 qlId 命名且 json 冗余
1077
+ // sessionId——ql-xxx → quick-xxx 映射在 patches 里持久可查(guard 已清也能出记录态)。
1078
+ // 提升到 quick 判定外层:ql-xxx 不匹配 QUICK_SID_RE,在内部分支永远进不去。
1079
+ if (QL_ID_RE.test(changeName)) {
1080
+ const byQl = findQuickSessionByQlId(cwd, changeName)
1081
+ if (byQl) {
1082
+ const rec = { record: byQl.record, patchPath: byQl.patchPath, qlId: byQl.qlId, specBase: byQl.specBase }
1083
+ return buildQuickRecordResult(rec, changeName)
1084
+ }
1085
+ return {
1086
+ ...empty,
1087
+ mode: 'quick',
1088
+ degradedReason: `quick 条目 ${changeName} 在 quicklog/patches/ 无记录——会话未产生改动、早于记录机制,或 patches 目录已被清理`,
1089
+ }
1090
+ }
781
1091
  // quick 模式判定(pattern + guard 双条件,与 run/command.js :140/:652 口径一致)
782
1092
  if (QUICK_SID_RE.test(changeName)) {
783
1093
  const located = locateQuickSessionGuard(cwd, changeName)
@@ -788,21 +1098,7 @@ export async function computeChangeScopeAudit({ cwd, specBase, changeName, platf
788
1098
  // 命中出冻结记录(行数为 --done 时点真值),patch 可供 --file 切片
789
1099
  const rec = findQuickPatchRecord(cwd, changeName)
790
1100
  if (rec) {
791
- return {
792
- mode: 'quick', ok: true, degradedReason: null,
793
- baseAnchor: rec.record.baseAnchor || `quick-window:${changeName}`,
794
- totals: rec.record.totals && typeof rec.record.totals === 'object'
795
- ? rec.record.totals
796
- : { files: rec.record.rows.length, ...sumTotals(rec.record.rows) },
797
- rows: rec.record.rows,
798
- excluded: rec.record.excluded && Array.isArray(rec.record.excluded.foreignDeclared)
799
- ? rec.record.excluded
800
- : { foreignDeclared: [] },
801
- frozenPatchPath: rec.patchPath,
802
- patchSha256: rec.record.patchSha256 || null,
803
- patchStatus: rec.record.patchStatus || null,
804
- note: `quick 会话已收尾——记录态(quicklog/patches/${rec.qlId},${rec.record.savedAt ? String(rec.record.savedAt).replace('T', ' ').slice(0, 19) + ' 落盘' : '--done 时点冻结'})`,
805
- }
1101
+ return buildQuickRecordResult(rec, changeName)
806
1102
  }
807
1103
  return {
808
1104
  ...empty,
@@ -936,6 +1232,8 @@ function fmtCount(n) {
936
1232
  *
937
1233
  * 防御式:result 任意字段缺失/畸形不抛(advisory 展示层)。opts.maxRows 截断 + 指引行
938
1234
  * (R-03:大表注入场景由调用方传 60,命令直跑缺省全表)。
1235
+ * [gate] 画像段(task-03,gate-table-section):result.gateProfile 存在时(quick 模式)追加
1236
+ * 级别/跨度/模块/风险命中/缺失检查项段;字段缺失/为 null(full-flow / 重放不可得)→ 零输出零回归。
939
1237
  *
940
1238
  * @param {object} result computeChangeScopeAudit 返回值
941
1239
  * @param {{ maxRows?: number }} [opts] 可选截断(缺省不截)
@@ -1001,6 +1299,38 @@ export function renderScopeAuditTable(result, opts = {}) {
1001
1299
  const undeclared = rows.filter(x => x && x.attribution === 'undeclared').length
1002
1300
  if (undeclared > 0) lines.push(` ⚠️ 未声明 ${undeclared} 文件——超出 allowedFiles 声明面,补 --files 声明或注明原因`)
1003
1301
 
1302
+ // ── [gate] 画像段(2026-09-14-quick-exit-tiered-gates task-03,FR-04 / D-008 gate-table-section):
1303
+ // quick 会话分级门禁画像的表格出口。按 gateProfile 存在性渲染——full-flow 变更 / 画像为 null
1304
+ // → 零输出零回归;检查项随级别分层(L1:每文件注记+测试增量;L2:模块文档认领+风险命中),
1305
+ // 与 run/quick-audit.js [gate] 打印块同一 review.gateProfile 数据源、同 tier 语义(advisory 展示,
1306
+ // 不构成门禁)。防御式:字段缺失/畸形不抛(degraded 无 span、modules 空集等均有占位)。
1307
+ const gate = r.gateProfile && typeof r.gateProfile === 'object' ? r.gateProfile : null
1308
+ if (gate) {
1309
+ const gateModules = Array.isArray(gate.modules) ? gate.modules.filter(m => m && typeof m === 'object') : []
1310
+ const spanTxt = gate.degraded
1311
+ ? '无 module-map(降级档:span 不参与判级)'
1312
+ : `跨 ${gate.moduleSpan ?? '—'} 模块${gateModules.length > 0 ? `(${gateModules.map(m => String(m.id)).join(' · ')})` : ''}`
1313
+ lines.push(` 🚦 [gate] ${gate.level ?? '—'} 规模门(${spanTxt} · ${gate.fileCount ?? '—'} 文件:${gate.codeFileCount ?? '—'} 代码 / ${gate.testFileCount ?? '—'} 测试)——advisory 画像不阻断`)
1314
+ if (Array.isArray(gate.unmappedFiles) && gate.unmappedFiles.length > 0) {
1315
+ lines.push(` ⚠️ ${gate.unmappedFiles.length} 个文件未命中 module-map(${gate.unmappedFiles.slice(0, 5).join(', ')}${gate.unmappedFiles.length > 5 ? ' 等' : ''})`)
1316
+ }
1317
+ const checks = gate.checks && typeof gate.checks === 'object' ? gate.checks : {}
1318
+ if (gate.level === 'L1') {
1319
+ if (checks.perFileNotes === false) lines.push(' - 每文件注记缺失——补 --file-notes "path1::注 || path2::注"(覆盖变更文件全集)')
1320
+ if (checks.testDelta === 'missing') lines.push(` - 测试增量缺失——${gate.codeFileCount} 个代码文件改动无测试文件`)
1321
+ } else if (gate.level === 'L2') {
1322
+ if (checks.docClaim === 'missing') lines.push(' - 模块文档认领缺失——触及模块的卡片文件不在改动集(补模块卡/changelog 或 --no-docs 豁免)')
1323
+ if (checks.docClaim === 'exempt-no-docs') lines.push(' - 模块文档认领:--no-docs 显式豁免')
1324
+ }
1325
+ if (Array.isArray(gate.riskHits) && gate.riskHits.length > 0) {
1326
+ lines.push(` - 风险命中 ${gate.riskHits.length} 处(运行时证据要求——风险路径改动需附运行验证说明):`)
1327
+ for (const h of gate.riskHits.slice(0, 5)) {
1328
+ if (h && typeof h === 'object') lines.push(` · [${h.pattern}] ${h.file}`)
1329
+ }
1330
+ if (gate.riskHits.length > 5) lines.push(` … 共 ${gate.riskHits.length} 处`)
1331
+ }
1332
+ }
1333
+
1004
1334
  // 他者会话声明排除面(R-04:不进 rows,单列可见)
1005
1335
  if (foreignDeclared.length > 0) {
1006
1336
  lines.push(` 他者会话声明(不进上表,归属对应会话审计):`)
package/src/spec-sync.js CHANGED
@@ -124,6 +124,9 @@ export function hashFiles(entries) {
124
124
  hash: _hashBuffer(buf),
125
125
  mtime: Math.floor(e.mtimeMs / 1000),
126
126
  absPath: e.absPath,
127
+ // 批 E-①:buf 随行携带(computeSpecOps 的 update/add 直接取用)——消除变更文件二次
128
+ // readFileSync(大文件双读)。docs 树为文本内容、量级有限,整批驻留可接受。
129
+ buf,
127
130
  };
128
131
  });
129
132
  }
@@ -177,20 +180,22 @@ export function computeSpecOps(serverManifest, localFiles) {
177
180
  return [];
178
181
  }
179
182
 
180
- // rename 检测:旧路径(服务器有、本地无)↔ 新路径(本地有、服务器无)hash 相同
183
+ // rename 检测:旧路径(服务器有、本地无)↔ 新路径(本地有、服务器无)hash 相同。
184
+ // 批 E-①:候选新路径按 hash 建索引 O(N+M)——旧双重循环 O(N×M),万级文件树明显变慢。
181
185
  const renames = [];
182
- const consumedNew = new Set();
186
+ const newByHash = new Map(); // hash → 未被服务器收录且未被消费的新路径队列
187
+ for (const [newPath, entry] of localMap) {
188
+ if (serverSet.has(newPath)) continue
189
+ const list = newByHash.get(entry.hash)
190
+ if (list) list.push(newPath); else newByHash.set(entry.hash, [newPath])
191
+ }
183
192
  for (const oldPath of serverPaths) {
184
- if (localSet.has(oldPath)) continue;
185
- const cachedEntry = server[oldPath];
186
- for (const newPath of localMap.keys()) {
187
- if (serverSet.has(newPath)) continue;
188
- if (consumedNew.has(newPath)) continue;
189
- if (localMap.get(newPath).hash === cachedEntry.hash) {
190
- renames.push({ oldPath, newPath });
191
- consumedNew.add(newPath);
192
- break;
193
- }
193
+ if (localSet.has(oldPath)) continue
194
+ const candidates = newByHash.get(server[oldPath].hash)
195
+ if (candidates && candidates.length > 0) {
196
+ const newPath = candidates.shift()
197
+ renames.push({ oldPath, newPath })
198
+ if (candidates.length === 0) newByHash.delete(server[oldPath].hash)
194
199
  }
195
200
  }
196
201
  const renamedOld = new Set(renames.map((r) => r.oldPath));
@@ -212,7 +217,7 @@ export function computeSpecOps(serverManifest, localFiles) {
212
217
  if (localSet.has(p)) {
213
218
  const localEntry = localMap.get(p);
214
219
  if (localEntry.hash !== server[p].hash) {
215
- const content = readFileSync(localEntry.absPath ?? join('.', p)).toString('base64');
220
+ const content = (localEntry.buf ?? readFileSync(localEntry.absPath ?? join('.', p))).toString('base64');
216
221
  ops.push({
217
222
  op: 'update',
218
223
  path: p,
@@ -235,7 +240,7 @@ export function computeSpecOps(serverManifest, localFiles) {
235
240
  if (renamedNew.has(p)) continue;
236
241
  if (!serverSet.has(p)) {
237
242
  const localEntry = localMap.get(p);
238
- const content = readFileSync(localEntry.absPath ?? join('.', p)).toString('base64');
243
+ const content = (localEntry.buf ?? readFileSync(localEntry.absPath ?? join('.', p))).toString('base64');
239
244
  ops.push({
240
245
  op: 'add',
241
246
  path: p,
@@ -317,10 +317,11 @@ const PLAN_RULES = [
317
317
  target: { root: 'change', path: 'design.md', scope: 'full' },
318
318
  data: {
319
319
  messageMissingList: 'design.md 缺少「文件变更清单」章节(或清单解析为空),无法做文件覆盖对账。该章节在 brainstorm 模板中为必填;请在 design.md 补上完整的文件变更清单(列出本次新增/修改/删除的源码文件)后重试。',
320
- messageUncovered: 'design.md 文件变更清单中 ${count} 个文件未被任何 task 的 allowed_paths 覆盖:\n${files}\n 这些文件在 execute 阶段将无 task 有权修改 → 必然漏改。\n 修复:为每个遗漏文件新建/补充 task 并在其 allowed_paths 声明,或在 design.md「不修改文件」章节说明不改原因。',
320
+ messageUncovered: 'design.md 文件变更清单中 ${count} 个文件未被任何 task 的 allowed_paths 覆盖:\n${files}\n 这些文件在 execute 阶段将无 task 有权修改 → 必然漏改。\n 修复:为每个遗漏文件新建/补充 task 并在其 allowed_paths 声明,或在 design.md「不修改文件」章节说明不改原因。\n 若跨仓文件被标为 [main]:多为仓变更段头未被识别(段头须为「## <repo-key> 仓变更」),先核对段头格式再补 task。',
321
+ messageMalformedHeader: 'design.md「文件变更清单」内疑似仓变更段头但格式无法解析:「${line}」\n 该段下文件会被记到上一段仓(或 main)→ 文件覆盖对账整段错位(误报未覆盖或漏检)。\n 修复:段头写成「## <repo-key> 仓变更」(repo-key 限字母数字._-,可加编号前缀与(…)备注后缀);若这是下一章节标题而非段头,请改名避免形如「<词> 仓变更…」。',
321
322
  },
322
- spec: 'design.md「文件变更清单」中的每个源码文件,必须被至少一个 task 的 allowed_paths 覆盖(前缀/glob 容差匹配),否则 execute 子代理无权改它→必然漏改。有 task 卡片但 design 缺「文件变更清单」章节也阻断(清单是覆盖对账的基准)。',
323
- failMessage: 'design.md 文件变更清单中 ${count} 个文件未被任何 task 的 allowed_paths 覆盖:\n${files}\n 这些文件在 execute 阶段将无 task 有权修改 → 必然漏改。\n 修复:为每个遗漏文件新建/补充 task 并在其 allowed_paths 声明,或在 design.md「不修改文件」章节说明不改原因。',
323
+ spec: 'design.md「文件变更清单」中的每个源码文件,必须被至少一个 task 的 allowed_paths 覆盖(前缀/glob 容差匹配),否则 execute 子代理无权改它→必然漏改。有 task 卡片但 design 缺「文件变更清单」章节也阻断(清单是覆盖对账的基准)。疑似仓变更段头但格式解析不了(repo-key 非法/仓变更后接自由文本)同样阻断并点名该行——段头错记仓会让对账整段错位。',
324
+ failMessage: 'design.md 文件变更清单中 ${count} 个文件未被任何 task 的 allowed_paths 覆盖:\n${files}\n 这些文件在 execute 阶段将无 task 有权修改 → 必然漏改。\n 修复:为每个遗漏文件新建/补充 task 并在其 allowed_paths 声明,或在 design.md「不修改文件」章节说明不改原因。\n 若跨仓文件被标为 [main]:多为仓变更段头未被识别(段头须为「## <repo-key> 仓变更」),先核对段头格式再补 task。',
324
325
  },
325
326
  // task-plan-reconciliation(custom):plan.md 声明任务 ↔ tasks/ 卡片双向对账(债单 D-2b)。
326
327
  // 修复场景:plan 列 17 个任务只生成 12 张卡(尾部文档同步类任务漏卡),execute 审计只看