sillyspec 3.28.2 → 3.28.4

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 (67) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +16 -55
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-continue/SKILL.md +8 -38
  4. package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +19 -249
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
  7. package/.claude/skills/sillyspec-propose/SKILL.md +6 -77
  8. package/.claude/skills/sillyspec-resume/SKILL.md +8 -57
  9. package/.claude/skills/sillyspec-state/SKILL.md +7 -46
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +14 -156
  11. package/AGENTS.md +24 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/src/archive-delta.js +127 -2
  15. package/src/change-delete.js +2 -1
  16. package/src/change-list.js +7 -2
  17. package/src/change-risk-profile.js +77 -6
  18. package/src/config-cat.js +12 -1
  19. package/src/constants.js +9 -1
  20. package/src/design-facts.js +64 -0
  21. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  22. package/src/docs-check.js +153 -27
  23. package/src/docs-migrate.js +124 -0
  24. package/src/doctor-diagnostics.js +441 -5
  25. package/src/foreign-declared.js +53 -2
  26. package/src/index.js +353 -28
  27. package/src/module-impact.js +77 -12
  28. package/src/plan-adopt-waves.js +13 -5
  29. package/src/progress/change-registry.js +35 -0
  30. package/src/progress.js +64 -1
  31. package/src/quick-recommend.js +6 -1
  32. package/src/quicklog.js +10 -2
  33. package/src/run/command.js +159 -13
  34. package/src/run/complete-handlers.js +162 -9
  35. package/src/run/complete.js +104 -5
  36. package/src/run/gates.js +116 -8
  37. package/src/run/progress-confirm.js +66 -0
  38. package/src/run/prompt.js +235 -36
  39. package/src/run/quick-audit.js +12 -5
  40. package/src/run/scan-profile.js +20 -2
  41. package/src/run/shared.js +84 -11
  42. package/src/run/stage.js +42 -0
  43. package/src/runtime-hygiene.js +84 -0
  44. package/src/scan-postcheck.js +5 -1
  45. package/src/spec-sync.js +63 -8
  46. package/src/stage-contract.js +83 -3
  47. package/src/stage-review.js +3 -0
  48. package/src/stages/archive.js +4 -4
  49. package/src/stages/brainstorm-auto.js +2 -1
  50. package/src/stages/brainstorm.js +11 -25
  51. package/src/stages/doctor.js +41 -515
  52. package/src/stages/execute.js +10 -14
  53. package/src/stages/plan-postcheck.js +191 -7
  54. package/src/stages/plan.js +7 -27
  55. package/src/stages/quick.js +15 -13
  56. package/src/stages/verify.js +23 -21
  57. package/src/sync-noise.js +158 -0
  58. package/src/sync.js +42 -17
  59. package/src/task-review.js +225 -1
  60. package/src/verify-facts-schema.js +141 -0
  61. package/src/verify-postcheck.js +421 -49
  62. package/src/verify-probes.js +142 -13
  63. package/src/workflow.js +6 -0
  64. package/src/worktree-apply.js +51 -13
  65. package/src/worktree.js +5 -1
  66. package/templates/agents-instruction.md +1 -1
  67. package/.claude/skills/verify-per-user/SKILL.md +0 -6
@@ -11,8 +11,8 @@
11
11
  * 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
12
12
  *
13
13
  * verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
14
- * 结论章节留「待填」——extractVerifyConclusion 找不到 PASS/FAIL 关键词即判不过,骨架不能
15
- * 直接过门(与 symbol-impact 骨架同款防偷懒语义)。
14
+ * 结论走「结论枚举:」固定槽行(刀③)——占位符不含枚举词,槽未填 extractVerifyConclusionSlot
15
+ * 返回 '' 即判不过,骨架不能直接过门(与 symbol-impact 骨架同款防偷懒语义)。
16
16
  * P3b 增量:①章节标题行末尾 claims 层标注(可复跑探针/确定性检查/人工判断,纯后缀不新增行);
17
17
  * ②--init 同步落盘 verify-facts.json 机器底稿(探针命令行 + 首跑关键指标 + 时间戳,CLI 全权写,
18
18
  * 供事后独立复跑审计;重复 --init 覆盖为最近一次 init 快照)。
@@ -20,6 +20,9 @@
20
20
  import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
21
21
  import { join, dirname, basename } from 'path'
22
22
  import { gitQuiet } from './git-helper.js'
23
+ import {
24
+ FACTS_SCHEMA_VERSION, EVIDENCE_SLOT_HEADING, RECEIPT_SLOT_HEADING, parseEvidenceSlots,
25
+ } from './verify-facts-schema.js'
23
26
  import { parseFileChangeListDetailed } from './change-list.js'
24
27
  import { parseAllowedPaths } from './stages/plan-postcheck.js'
25
28
  import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
@@ -310,8 +313,11 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
310
313
  const p3 = (result && result.probe3) || {}
311
314
  const p5 = (result && result.probe5) || {}
312
315
  const p6 = (result && result.probe6) || {}
316
+ // v2(2026-09-08-ir-verify-facts):probes 机器段原样;conclusion/tests/requiredEvidence/
317
+ // runtimeEvidence/factsConsistency 五段是 slot-backfill/实测回填段(D-001@v2),--init 快照
318
+ // 不落键(writeVerifyFacts 分段合并时保留既有固化段),由 backfillFactsFromMdAndTests 填。
313
319
  return {
314
- schemaVersion: 1,
320
+ schemaVersion: 2,
315
321
  change: changeName,
316
322
  generatedAt: now || new Date().toISOString(),
317
323
  probes: {
@@ -350,37 +356,160 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
350
356
  }
351
357
 
352
358
  /**
353
- * verify-probes --init 落盘 verify-facts.json(CLI 全权写)。重复 --init 无条件覆盖——语义是
354
- * 「最近一次机械预填的审计底稿快照」(骨架已存在跳过时 facts 照样刷新),agent 勿手改:
355
- * 事后复跑审计的对照快照,防篡改一致性检查的对比基准是 verify-result.md 正文(非本文件)。
359
+ * verify-probes --init 落盘 verify-facts.json(CLI 全权写)。v2(2026-09-08-ir-verify-facts)改
360
+ * 分段合并:re-init 刷新机器段(probes/generatedAt),保留既有 slot-backfill/实测固化段
361
+ * (conclusion/tests/requiredEvidence/runtimeEvidence)——旧「无条件覆盖」会抹掉 --done 回填数据
362
+ * (设计审查 P1-5)。agent 勿手改:复跑审计的对照快照,防篡改基准是 verify-result.md 正文。
356
363
  * @param {string} changeDir 变更目录(spec 根下 changes/<name>)
357
364
  * @param {object} result runVerifyProbes 返回值
358
365
  * @param {string} changeName 变更名(统一命令行呈现)
359
366
  * @returns {{ facts: object, path: string }}
360
367
  */
361
368
  export function writeVerifyFacts(changeDir, result, changeName) {
362
- const facts = buildVerifyFacts(result, { changeName })
369
+ const fresh = buildVerifyFacts(result, { changeName })
363
370
  const factsPath = join(changeDir, 'verify-facts.json')
371
+ let facts = fresh
372
+ try {
373
+ const prev = JSON.parse(readFileSync(factsPath, 'utf8'))
374
+ if (prev && prev.schemaVersion === FACTS_SCHEMA_VERSION) {
375
+ facts = {
376
+ ...fresh,
377
+ conclusion: prev.conclusion,
378
+ tests: prev.tests,
379
+ requiredEvidence: prev.requiredEvidence,
380
+ runtimeEvidence: prev.runtimeEvidence,
381
+ factsConsistency: prev.factsConsistency, // 保留至上次对比结论(下次 --done 重算覆盖)
382
+ }
383
+ }
384
+ } catch { /* 首次落盘/v1/损坏 → 全新快照(v1 无固化段,直接升 v2) */ }
364
385
  writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
365
- console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(CLI 全权写,勿手改)`)
386
+ console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(v2 分段合并,固化段保留;CLI 全权写,勿手改)`)
366
387
  return { facts, path: factsPath }
367
388
  }
368
389
 
390
+ /**
391
+ * --done 回填(D-001@v2 slot-backfill):结论枚举槽 + 证据/回执槽解析固化进 facts;
392
+ * testCheckResult 提供时回填 tests 段(实测在 runValidators 之后 → gates 二次回填,FR-03 次序)。
393
+ * 机器段(probes/factsConsistency)不经本函数(writeVerifyFacts / checkProbeConsistency 所有)。
394
+ * fail-soft:facts 缺失时原地升 v2(probes 保留或空);落盘失败 warn 不抛。
395
+ * @param {string} factsPath verify-facts.json 路径
396
+ * @param {{ verifyMd: string, testCheckResult?: object|null }} opts
397
+ * @returns {{ facts: object, conclusion: string|null, evidenceCount: number, receiptCount: number, testsBackfilled: boolean }}
398
+ */
399
+ export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResult = null, conclusion = null }) {
400
+ let facts = null
401
+ try { facts = JSON.parse(readFileSync(factsPath, 'utf8')) } catch { /* 缺失见下 */ }
402
+ // 只固化既有底稿,不无中生有(2026-09-08-ir-verify-facts 接线实证):无 facts 的存量变更
403
+ // (未跑过 --init)若被回填凭空建出 facts.json,checkProbeConsistency 的「有 facts 无探针
404
+ // 子节 → error」判别会把存量兼容 skip 误升 error(run-complete-step-verify e2e 抓出)。
405
+ // 底稿创建唯一入口是 verify-probes --init。
406
+ if (!facts) {
407
+ return { facts: null, skipped: true, conclusion: null, evidenceCount: 0, receiptCount: 0, testsBackfilled: false, reason: '无 verify-facts.json(未跑 --init),回填跳过——底稿创建唯一入口是 --init' }
408
+ }
409
+ if (facts.schemaVersion !== FACTS_SCHEMA_VERSION) {
410
+ facts = { ...facts, schemaVersion: FACTS_SCHEMA_VERSION, probes: (facts && facts.probes) || {} }
411
+ }
412
+ const slots = parseEvidenceSlots(verifyMd || '')
413
+ // conclusion 由调用方传 stage-contract.extractVerifyConclusionSlot(verifyMd) 的结果
414
+ // (避免 verify-probes → stage-contract 静态 import 的潜在环,分层单向)
415
+ const conclusionSlot = conclusion || null
416
+ if (conclusionSlot) facts.conclusion = conclusionSlot
417
+ if (slots.hasEvidenceSlot && slots.requiredEvidence.length > 0) {
418
+ facts.requiredEvidence = slots.requiredEvidence.map(e => ({
419
+ task: e.task, status: e.status, verifiedFiles: e.verifiedFiles,
420
+ ...(e.exempt ? { exempt: true, exemptionReason: e.exemptionReason } : {}),
421
+ }))
422
+ }
423
+ if (slots.hasReceiptSlot && slots.runtimeEvidence.length > 0) {
424
+ facts.runtimeEvidence = slots.runtimeEvidence
425
+ }
426
+ let testsBackfilled = false
427
+ if (testCheckResult && testCheckResult.status && testCheckResult.status !== 'skipped') {
428
+ facts.tests = {
429
+ command: testCheckResult.command || null,
430
+ exitCode: typeof testCheckResult.exitCode === 'number' ? testCheckResult.exitCode : null,
431
+ strategy: testCheckResult.mode || testCheckResult.strategy || null,
432
+ failedRemaining: testCheckResult.failureNames || testCheckResult.failedTests || [],
433
+ passedAt: new Date().toISOString(),
434
+ }
435
+ testsBackfilled = true
436
+ }
437
+ try {
438
+ writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
439
+ } catch (e) {
440
+ console.warn(`⚠️ facts 回填落盘失败(fail-soft,不阻断): ${e && e.message ? e.message : e}`)
441
+ }
442
+ return { facts, conclusion: conclusionSlot, evidenceCount: slots.requiredEvidence.length, receiptCount: slots.runtimeEvidence.length, testsBackfilled }
443
+ }
444
+
445
+ /**
446
+ * md 槽段缺失补齐(--init 段落级补齐,2026-09-08-ir-verify-facts R-02):已存在的
447
+ * verify-result.md 缺「证据账/集成验证回执」槽段时仅追加骨架(不触碰既有正文),幂等二跑零改动。
448
+ * @param {string} mdPath verify-result.md 路径
449
+ * @param {Array<{task:string,evidence:string[]}>} [requiredEvidenceItems] verify-required-evidence.json items(预填 task 行)
450
+ * @returns {{ added: string[] }} 追加的槽段标题列表
451
+ */
452
+ export function backfillMissingEvidenceSlots(mdPath, requiredEvidenceItems = []) {
453
+ let text = ''
454
+ try { text = readFileSync(mdPath, 'utf8') } catch { return { added: [] } }
455
+ const normalized = text.replace(/\r\n/g, '\n')
456
+ const added = []
457
+ const blocks = []
458
+ if (!normalized.includes(EVIDENCE_SLOT_HEADING)) {
459
+ const lines = [EVIDENCE_SLOT_HEADING, '[层:人工判断——CLI 核验]', '', '<!-- 无 cannot_verify 任务时本节写「无」 -->']
460
+ if (requiredEvidenceItems.length > 0) {
461
+ for (const it of requiredEvidenceItems) {
462
+ lines.push(`- ${it.task}: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)`)
463
+ }
464
+ } else {
465
+ lines.push('无(本次变更无 cannot_verify 任务)')
466
+ }
467
+ lines.push('')
468
+ blocks.push(lines.join('\n'))
469
+ added.push(EVIDENCE_SLOT_HEADING)
470
+ }
471
+ if (!normalized.includes(RECEIPT_SLOT_HEADING)) {
472
+ blocks.push([
473
+ RECEIPT_SLOT_HEADING, '[层:自述声明——CLI 一致性校验]', '',
474
+ '<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
475
+ '- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
476
+ '',
477
+ ].join('\n'))
478
+ added.push(RECEIPT_SLOT_HEADING)
479
+ }
480
+ if (added.length > 0) {
481
+ writeFileSync(mdPath, normalized.replace(/\n?$/, '\n') + blocks.join('\n') + '\n')
482
+ }
483
+ return { added }
484
+ }
485
+
369
486
  /**
370
487
  * 生成 verify-result.md 骨架(七章节;探针结果机械预填,语义章节 <!--TODO--> 占位)。
371
- * 结论章节留「待填」——extractVerifyConclusion 无 PASS/FAIL 关键词即判不过,骨架不能直接过门。
488
+ * 结论走「结论枚举:」固定槽行(刀③,2026-09-08)——占位符不含枚举词(<待填:三选一>),
489
+ * 槽未填 → extractVerifyConclusionSlot 返回 '' → gate 判不过(fail-closed;旧占位符
490
+ * `<待填:PASS 或 FAIL>` 含 PASS 字样会被窗口正则误读成已填 PASS,已修)。
372
491
  * 章节标题行末尾带 claims 层标注(P3b):探针结果=可复跑探针 / 测试结果=确定性检查 / 其余=
373
- * 人工判断——纯渲染层后缀,不新增行,verify gate 结论提取按关键词窗口制不受影响。
492
+ * 人工判断——纯渲染层后缀,不新增行;结论解析槽优先于标题关键词窗口(存量兼容)。
374
493
  * @returns {string|null} 骨架全文;无 design.md/tasks.md(非完整流程变更)→ null
375
494
  */
376
495
  export function generateVerifyResultSkeleton(result) {
377
496
  const L = [
378
497
  '# 验证报告(骨架由 `sillyspec verify-probes --change <变更名> --init` 生成)',
379
498
  '',
380
- '> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论必须写明 PASS / FAIL**——',
381
- '> 留「待填」会被 gate 判不过(fail-closed)。',
499
+ '> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论只认「结论枚举:」槽行**——',
500
+ '> 槽行留「<待填:三选一>」会被 gate 判不过(fail-closed),正文其他位置的 PASS/FAIL 字样不参与判定。',
501
+ '',
502
+ '## 结论 [层:人工判断]',
503
+ '',
504
+ '结论枚举:`<待填:三选一>`(把尖括号占位整体替换为 PASS / PASS WITH NOTES / FAIL 之一;一句话理由写在枚举后同行或下一行)',
505
+ '',
506
+ '## 证据账(cannot_verify 任务) [层:人工判断——CLI 核验]',
507
+ '<!-- 无 cannot_verify 任务时本节写「无」;有则逐 task 一行 -->',
508
+ '- task-NN: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)',
382
509
  '',
383
- '## 结论:<待填:PASS 或 FAIL(+一句话理由)> [层:人工判断]',
510
+ '## 集成验证回执 [层:自述声明——CLI 一致性校验]',
511
+ '<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
512
+ '- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
384
513
  '',
385
514
  '## 任务完成度 [层:人工判断]',
386
515
  '<!--TODO: 逐 task 对照 tasks.md 勾选与验收标准,完成/未完成/存疑三态-->',
package/src/workflow.js CHANGED
@@ -12,6 +12,7 @@
12
12
  import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs'
13
13
  import { join, resolve, basename } from 'path'
14
14
  import jsYaml from 'js-yaml'
15
+ import { pruneTimestampedEntries } from './runtime-hygiene.js'
15
16
  import { WORKFLOW_STATUS } from './constants.js'
16
17
  import { contentNonEmpty, lineCount, missingSections, placeholderLineMatches } from './check-primitives.js'
17
18
  import { collectInvalidDocRefs } from './docs-check.js'
@@ -626,6 +627,11 @@ export function saveWorkflowRun(result, options = {}) {
626
627
 
627
628
  try {
628
629
  writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8')
630
+ // 写入侧滚动裁剪(runtime-hygiene.js,2026-09-08 系统性回收):workflow-runs/ 只写不收,
631
+ // 实证本仓 21 份累积。文件名以零填充时间戳开头,name 字典序 == 时间序。fail-open 静默。
632
+ try {
633
+ pruneTimestampedEntries({ dir: runDir, keep: 30 })
634
+ } catch { /* 卫生动作 fail-open */ }
629
635
  return filepath
630
636
  } catch (e) {
631
637
  console.warn('⚠️ 保存 workflow run 失败:', e.message)
@@ -217,23 +217,31 @@ function getBlobHashMap(cwd, treeish, files) {
217
217
  * 跨仓文件不在主仓 worktree 故永不进 diff);跨仓 task 走 no-op(D-009),其 allowed_paths 仅
218
218
  * 作 Map 切片返回,供上游(assess / 跨仓 review 校验)按 repo 取用。
219
219
  *
220
+ * **specBase 解析(2026-09-08 用户反馈:平台模式 apply allowlist 读本地空目录整批 BLOCKED)**:
221
+ * 优先级 opts.specBase(调用方传 platformOpts.specRoot)> 平台指针(.sillyspec-platform.json
222
+ * 静默读,不触发 resolvePlatformSpecDir 的守卫 warn)> 本地 projectRoot/.sillyspec。平台模式下
223
+ * changes/ 实体在 specRoot,本地目录常为空——旧硬编码 CHANGES_REL 恒读空 → allow 集恒空 →
224
+ * Gate1 整批误拦(sync.js BUG-01 同族)。
225
+ *
220
226
  * @param {string} projectRoot - 主仓库根
221
227
  * @param {string} changeName - 变更名
228
+ * @param {{ specBase?: string }} [opts] specBase 显式传入(平台模式必传 specRoot)
222
229
  * @returns {Map<string, Set<string>>} repoKey → 并集清单 Set(无 design 清单且无 task 卡片时为空 Map)
223
230
  */
224
- export function resolveApplyAllowSet(projectRoot, changeName) {
231
+ export function resolveApplyAllowSet(projectRoot, changeName, opts = {}) {
225
232
  const repoMap = new Map();
226
233
  const getOrCreate = (key) => {
227
234
  if (!repoMap.has(key)) repoMap.set(key, new Set());
228
235
  return repoMap.get(key);
229
236
  };
230
- // design §6 清单归属 main(清单路径相对主仓根;跨仓 task 的 allowed_paths 由 task 卡片 repo 切片)
237
+ const specBase = opts.specBase || _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
238
+ // design §6 清单归属 main(清单路径相对主仓根;跨仓 task 的 allowed_paths 由 task 卡 repo 切片)
231
239
  const mainSet = getOrCreate('main');
232
240
  parseFileChangeList(
233
- join(projectRoot, CHANGES_REL, changeName, 'design.md'),
241
+ join(specBase, 'changes', changeName, 'design.md'),
234
242
  { keepSillyspecDocs: true }
235
243
  ).forEach(p => mainSet.add(p));
236
- const tasksDir = join(projectRoot, CHANGES_REL, changeName, 'tasks');
244
+ const tasksDir = join(specBase, 'changes', changeName, 'tasks');
237
245
  if (existsSync(tasksDir)) {
238
246
  for (const tf of readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))) {
239
247
  const content = readFileSync(join(tasksDir, tf), 'utf8');
@@ -246,6 +254,18 @@ export function resolveApplyAllowSet(projectRoot, changeName) {
246
254
  return repoMap;
247
255
  }
248
256
 
257
+ // 平台指针静默读(worktree-apply 内部用):只读 <projectRoot>/.sillyspec-platform.json 的
258
+ // specRoot 字段,不做存在性/自指/temp 守卫、不打 warn——allowlist 只需一个「读得到就用」的
259
+ // 基准目录,坏指针/缺字段落回本地(与 sync-noise.js bindSyncNoiseFromCwd 同款零副作用风格)。
260
+ function _silentPointerSpecRoot(projectRoot) {
261
+ try {
262
+ const p = join(projectRoot, '.sillyspec-platform.json');
263
+ if (!existsSync(p)) return null;
264
+ const s = JSON.parse(readFileSync(p, 'utf8').replace(/^\uFEFF/, ''));
265
+ return s && typeof s.specRoot === 'string' && s.specRoot ? s.specRoot : null;
266
+ } catch { return null; }
267
+ }
268
+
249
269
  /**
250
270
  * 收集最新 execute run 各 task review.json 的 changedFiles 声明 → Map<repoKey, string[]>。
251
271
  *
@@ -257,14 +277,23 @@ export function resolveApplyAllowSet(projectRoot, changeName) {
257
277
  * collectExecuteChangedFiles 一致:change 戳归属 run、.sillyspec//meta.json 过滤、
258
278
  * review.repo 切片(跨仓声明不进 main 集)。读不到 run/review → 空 Map(fail-closed 回退旧行为)。
259
279
  *
280
+ * runtimeRoot 解析(2026-09-08 平台模式修复,与 resolveApplyAllowSet 的 specBase 同族):
281
+ * 优先级 opts.runtimeRoot(调用方传 resolveRuntimeRoot(platformOpts, specBase) 产物)>
282
+ * 指针 specRoot/.runtime(静默读)> 本地 projectRoot/.sillyspec/.runtime。平台模式
283
+ * execute-runs 在平台 runtime root,旧硬编码本地恒空 → 第三源恒缺失。
284
+ *
260
285
  * @param {string} projectRoot - 主仓库根
261
286
  * @param {string} changeName - 变更名
287
+ * @param {{ runtimeRoot?: string }} [opts]
262
288
  * @returns {Map<string, string[]>}
263
289
  */
264
- export function collectReviewDeclaredFiles(projectRoot, changeName) {
290
+ export function collectReviewDeclaredFiles(projectRoot, changeName, opts = {}) {
265
291
  const byRepo = new Map();
266
292
  try {
267
- const runtimeRoot = join(projectRoot, '.sillyspec', '.runtime');
293
+ const ptrSpec = _silentPointerSpecRoot(projectRoot);
294
+ const runtimeRoot = opts.runtimeRoot
295
+ || (ptrSpec ? join(ptrSpec, '.runtime') : null)
296
+ || join(projectRoot, '.sillyspec', '.runtime');
268
297
  const runId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName });
269
298
  if (!runId) return byRepo;
270
299
  const runTasksDir = join(runtimeRoot, 'execute-runs', runId, 'tasks');
@@ -504,10 +533,11 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
504
533
  continue;
505
534
  }
506
535
 
507
- // 清单校验(per-repo 切片 + review 声明切片并入,与主仓 Gate1/3b 同语义)
508
- const allowMap = resolveApplyAllowSet(projectRoot, changeName);
536
+ // 清单校验(per-repo 切片 + review 声明切片并入,与主仓 Gate1/3b 同语义)。
537
+ // specBase 显式传(平台模式 changes/ 在 specRoot;本地模式与旧硬编码同值零回归)。
538
+ const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase });
509
539
  let allowSet = allowMap.get(repoKey) || new Set();
510
- const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName).get(repoKey);
540
+ const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(specBase, '.runtime') }).get(repoKey);
511
541
  if (reviewDeclared) {
512
542
  const merged = new Set([...allowSet]);
513
543
  for (const f of reviewDeclared) {
@@ -754,15 +784,21 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
754
784
  }
755
785
 
756
786
  // --- 3. 解析 apply 文件清单(design §6 清单 ∪ plan TaskCard allowed_paths,execute 复盘 c) ---
787
+ // allowlist 基准目录(2026-09-08 平台模式修复):ctx.platformOpts.specRoot 优先(execute
788
+ // ctx 透传),否则 resolveApplyAllowSet 内部静默读平台指针兜底,最后本地——旧硬编码在
789
+ // 平台模式读本地空 changes/ → allow 集恒空 → Gate1 整批误拦。
790
+ const allowSpecBase = (ctx && ctx.platformOpts && ctx.platformOpts.specRoot) || undefined;
791
+ const allowRuntimeRoot = (ctx && ctx.platformOpts && ctx.platformOpts.runtimeRoot)
792
+ || (allowSpecBase ? join(allowSpecBase, '.runtime') : undefined);
757
793
  // resolveApplyAllowSet 返回 Map<repo, Set>(task-05 跨仓切片);主仓 apply 只消费 main 仓 Set
758
794
  // (主仓 worktree diff 只含主仓文件,跨仓文件在跨仓仓不进主仓 worktree diff)。跨仓 allowed_paths
759
795
  // 仅作 Map 切片返回供上游用,主流程不消费(跨仓 apply=no-op,D-009)。
760
- const allowMap = resolveApplyAllowSet(projectRoot, changeName);
796
+ const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase: allowSpecBase });
761
797
  // --- 3b. review.json 声明偏差文件并入(坑 apply-undeclared-deviation-block)---
762
798
  // 第三源:reviewer 声明的 changedFiles(已过 Task Review Gate git 证据交叉校验)——执行期
763
799
  // 有据越界(facade 转发/名单测试)不再逼回改 design.md。各 repo 切片各自并入;仅靠 review
764
800
  // 放行的文件记审计 warning(result.reviewAdmittedFiles),完全越界文件仍拦(Gate1 在扩展后判)。
765
- const reviewDeclaredByRepo = collectReviewDeclaredFiles(projectRoot, changeName);
801
+ const reviewDeclaredByRepo = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: allowRuntimeRoot });
766
802
  const reviewAdmittedFiles = [];
767
803
  for (const [repoKey, files] of reviewDeclaredByRepo) {
768
804
  const repoSet = allowMap.get(repoKey);
@@ -1693,13 +1729,15 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
1693
1729
 
1694
1730
  // design §6 标记为「顺带修复」的文件(坑 worktree-execute-apply-friction 坑1):合规修预存债,
1695
1731
  // 不属任何 task 边界,assess 豁免 allowed_paths 严格校验(降级 warning),避免被迫 cherry-pick 绕过。
1696
- const designPath = join(projectRoot, CHANGES_REL, changeName, 'design.md');
1732
+ // specBase 走指针/本地解析(2026-09-08 平台模式修复,与 resolveApplyAllowSet 同族)。
1733
+ const assessSpecBase = _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
1734
+ const designPath = join(assessSpecBase, 'changes', changeName, 'design.md');
1697
1735
  const incidentalSet = new Set(
1698
1736
  parseFileChangeListDetailed(designPath).filter(e => e.incidental).map(e => e.path)
1699
1737
  );
1700
1738
  // review.json 声明偏差文件(坑 apply-undeclared-deviation-block):与顺带修复同等待遇——
1701
1739
  // reviewer 声明(已过 git 证据校验)的执行期偏差豁免 allowed_paths 严格校验,降 warning 注明来源。
1702
- const reviewDeclaredSet = new Set(collectReviewDeclaredFiles(projectRoot, changeName).get('main') || []);
1740
+ const reviewDeclaredSet = new Set(collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(assessSpecBase, '.runtime') }).get('main') || []);
1703
1741
 
1704
1742
  // 检查 2: 变更在 allowed_paths 内(仅在 TaskCard 存在时);顺带修复/review 声明文件豁免。
1705
1743
  // 匹配换 pathMatches(与 Gate1/plan-postcheck 同语义容差),消除原字面前缀弱匹配漂移。
package/src/worktree.js CHANGED
@@ -203,7 +203,11 @@ function printSyncReport(diag, baseHash, changeName) {
203
203
  }
204
204
 
205
205
  function parseJSON(raw) {
206
- try { return JSON.parse(raw); } catch { return null; }
206
+ // BOM 容错(2026-09-08 用户反馈:meta.json 带 UTF-8 BOM 被当损坏 → apply 报
207
+ // 「meta.json 不存在或已损坏」、幽灵判定误清):Windows 编辑器(记事本/部分 PowerShell
208
+ // 重定向)写 JSON 常带 \uFEFF 前缀,JSON.parse 对首字符 BOM 直接抛。剥掉再 parse。
209
+ const s = typeof raw === 'string' ? raw.replace(/^\uFEFF/, '') : raw
210
+ try { return JSON.parse(s); } catch { return null; }
207
211
  }
208
212
 
209
213
  /**
@@ -11,7 +11,7 @@
11
11
  5. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档。
12
12
  6. **判规模选档**:≤3 文件、范围明确走 `quick`;多阶段 / 架构级走完整流程。
13
13
  7. **代码先行不补流程(倒推 B 模式)**:代码若已先写好,**不回头补 brainstorm/plan 装样子**——用 `quick --done` 收尾 + 补 quicklog 条目,把已落盘改动如实登记进进度库。
14
- 8. **实证核验再 `--done`**:`quick --done` 前,触及 `src`/`test` 的改动先跑 `.sillyspec/local.yaml` 配置的测试与 lint 命令(`commands.test` / `commands.lint`;未配置时用项目构建工具的等价命令,如 Node 项目 `npm test` + `npm run lint`、Python 项目 `pytest` + `ruff check`);纯 doc/配置改动可跳过(lint 不扫 docs/,空转无信息),以落盘文件与测试结果为准,不信口头"已完成"。
14
+ 8. **实证核验再 `--done`**:触及 `src`/`test` 的改动,CLI 会在 `quick --done` 时**亲自实测** `.sillyspec/local.yaml` 的 `commands.test` / `commands.lint`(门禁:实测失败阻断 --done 回 pending;纯 doc/配置与未配置命令自动跳过;倒推 B 模式按 --files 声明边界兜底判定)。agent 侧预跑**可选**——想省「--done 被拦→修复→重跑」一轮时才预跑;纯 doc/配置改动无需跑。以落盘文件与测试结果为准,不信口头"已完成"。
15
15
  9. **中途停下不靠额外命令存进度**——进度已由上一次 `--done` 自动落盘;恢复时用 `sillyspec progress show` 查看进度,再用 `sillyspec run <stage>` 续跑,不直接 commit 半成品。
16
16
  10. **实现完成后对照文档验收**(design.md / 模块文档),并检查是否影响已有测试。
17
17
  11. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
@@ -1,6 +0,0 @@
1
- ---
2
- name: verify-per-user
3
- description: verify
4
- ---
5
-
6
- # test