sillyspec 3.28.4 → 3.28.6

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 (52) hide show
  1. package/AGENTS.md +1 -0
  2. package/package.json +1 -1
  3. package/results-review-tier.md +18 -0
  4. package/sa-rename-probe/src/new.js +4 -0
  5. package/src/agent-session-log.js +4 -3
  6. package/src/archive-delta.js +2 -2
  7. package/src/change-list.js +11 -1
  8. package/src/commit-suggest.js +52 -1
  9. package/src/config-cat.js +6 -1
  10. package/src/config-schema.js +13 -0
  11. package/src/decision-distill.js +53 -7
  12. package/src/decisions-io.js +96 -0
  13. package/src/design-facts.js +3 -0
  14. package/src/dispatch/probe.js +31 -8
  15. package/src/docs-check.js +77 -8
  16. package/src/friction-tally.js +221 -0
  17. package/src/index.js +430 -18
  18. package/src/mcp-server.js +144 -0
  19. package/src/module-impact.js +8 -1
  20. package/src/modules.js +46 -34
  21. package/src/progress/change-registry.js +20 -0
  22. package/src/progress.js +16 -1
  23. package/src/quicklog.js +60 -6
  24. package/src/review-dispatch.js +377 -0
  25. package/src/run/archive-distill.js +48 -0
  26. package/src/run/command.js +35 -5
  27. package/src/run/complete-handlers.js +136 -3
  28. package/src/run/complete.js +170 -2
  29. package/src/run/gate-snapshot.js +159 -0
  30. package/src/run/gates.js +166 -69
  31. package/src/run/prompt.js +54 -9
  32. package/src/run/quick-audit.js +78 -16
  33. package/src/run/shared.js +138 -7
  34. package/src/run/stage.js +31 -2
  35. package/src/run/verify-quality-scan.js +383 -0
  36. package/src/scope-audit.js +941 -0
  37. package/src/sillyhub-mcp/client.js +58 -3
  38. package/src/stage-review-checklist.js +44 -0
  39. package/src/stage-review.js +122 -4
  40. package/src/stages/archive.js +57 -164
  41. package/src/stages/brainstorm.js +8 -12
  42. package/src/stages/execute.js +13 -10
  43. package/src/stages/plan.js +3 -10
  44. package/src/stages/verify.js +14 -33
  45. package/src/sync.js +51 -10
  46. package/src/task-review.js +108 -16
  47. package/src/taskcard.js +46 -3
  48. package/src/validate-artifacts.js +163 -0
  49. package/src/verify-postcheck.js +90 -10
  50. package/src/verify-probes.js +134 -12
  51. package/src/worktree-apply.js +119 -14
  52. package/templates/agents-instruction.md +1 -0
package/src/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
10
10
  import { writeAtomicSync } from './fs-atomic.js';
11
- import { basename, dirname, extname, join, resolve, isAbsolute, sep } from 'path';
11
+ import { basename, dirname, extname, join, resolve, isAbsolute, sep, relative } from 'path';
12
12
  import { safeGit, git } from './git-helper.js';
13
13
  import { getVersion } from './version.js';
14
14
 
@@ -104,6 +104,7 @@ SillySpec CLI — 规范驱动开发工具包
104
104
  sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
105
105
  sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架
106
106
  sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
107
+ sillyspec scope-audit --change <name> [--json] 变更范围对账:计划×实际三态全表 + 行数(✓ 计划内/⚠️ 计划外/⚠️ 计划未动;quick 会话传 quick-<id> 出归属表;已归档变更可查——快照记录态;advisory 只读不设门禁)
107
108
  sillyspec module-docs-sync --change <name> [--note ...] diff 归属模块 → sidecar 追加变更索引行 + 卡 updated_at 戳(幂等)
108
109
  sillyspec endpoints extract --change <name> [--task task-NN | --all-tasks] [--dir <dir>|--files <a.py,b.js>] 静态扫描路由装饰器生成 endpoints.json
109
110
  sillyspec endpoints baseline --change <name> [--spec-dir <path>] [--json] 拍变更前端点基线(幂等不覆盖;worktree 内跑自动锚主仓;归档 delta 端点增删 before 侧)
@@ -197,7 +198,7 @@ async function main() {
197
198
 
198
199
  // E22:重路径统一加载(轻路径 --version/help 已早退,未付此税)。
199
200
  const { ProgressManager, resolvePlatformSpecDir, resolvePlatformOpts } = await import('./progress.js');
200
- const { didYouMean, assertSafeChangeName, resolveSpecDir, detectWorktreeSpecDrift } = await import('./run/shared.js');
201
+ const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift } = await import('./run/shared.js');
201
202
 
202
203
  // 解析全局选项
203
204
  let json = false;
@@ -628,6 +629,13 @@ async function main() {
628
629
  } else if (rwResult.ok) {
629
630
  console.log(`✅ review.json 已写入:${rwResult.reviewPath}`);
630
631
  console.log(` executeRunId: ${rwResult.executeRunId}`);
632
+ // P2-f(noai-ir-roadmap §5)task 真源归一:review 落盘即由 CLI 渲染 checkbox——
633
+ // agent 永不手勾(review.json verdict 是唯一真源,tasks.md 勾选是它的显示态)。
634
+ try {
635
+ const { autoCheckPlanFromReviews } = await import('./run/complete.js');
636
+ const ac = await autoCheckPlanFromReviews({ stageName: 'execute', changeName: rwChange, cwd: dir, platformOpts: rwPlatformOpts });
637
+ if (ac.autoChecked && ac.checkedCount > 0) console.log(` ✅ tasks.md checkbox 已按 review verdict 自动勾选(${ac.checkedCount} 个;task 真源 = review.json,CLI 唯一勾选者——勿手动勾选)`);
638
+ } catch { /* 勾选 fail-soft:--done 时 autoCheck 兜底 */ }
631
639
  for (const w of rwResult.warnings) console.log(`⚠️ ${w}`);
632
640
  } else {
633
641
  console.error('❌ review write 失败:');
@@ -817,8 +825,22 @@ async function main() {
817
825
  // 项目状态探测(2026-08-21 审计第二批 G7/G8):吸收 continue/resume 两个 skill 的
818
826
  // 手工探测表(文件存在性状态机),一条命令输出「状态 + 下一步命令 + 依据」。
819
827
  // 纯 fs 零 token;活跃进度的权威状态仍归 progress show,本命令管无活跃进度时反推。
828
+ // --apply(P2,noai-ir-roadmap §5):建议是本 CLI 命令形态时直接代跑(恢复链 noAI——
829
+ // 只建议不执行让 agent 当转述员);非 CLI 命令形态的建议(自由文本)不代跑。
820
830
  const { detectNextStep } = await import('./run/next.js');
821
831
  const nxResult = detectNextStep({ cwd: dir, specDir });
832
+ if (args.includes('--apply')) {
833
+ const m = String(nxResult.next || '').match(/^sillyspec\s+(.+)$/);
834
+ if (!m) {
835
+ console.error(`❌ next --apply:建议不是可代跑的 CLI 命令形态(实得「${nxResult.next}」)——按建议手动执行`);
836
+ process.exit(2);
837
+ }
838
+ const childArgs = m[1].trim().split(/\s+/);
839
+ console.log(`🤖 next --apply 代跑:sillyspec ${childArgs.join(' ')}\n`);
840
+ const { spawnSync } = await import('node:child_process');
841
+ const r = spawnSync(process.execPath, [join(__dirname, '..', 'bin', 'sillyspec.js'), '--dir', dir, ...childArgs], { stdio: 'inherit' });
842
+ process.exit(r.status === null ? 1 : r.status);
843
+ }
822
844
  if (json) {
823
845
  console.log(JSON.stringify({ command: 'next', ...nxResult }, null, 2));
824
846
  break;
@@ -840,7 +862,30 @@ async function main() {
840
862
  // 智能提交建议(2026-08-21 审计第二批 G1-G3):commit skill 此前让 agent 手工收集
841
863
  // 语义(LAST_COMMIT_TIME / cat QUICKLOG / 扫变更目录 / 按路径模式分类)——进度库、
842
864
  // quicklog、git 全在 CLI 手里。一条命令产出统计 + 语义来源 + conventional 建议 +
843
- // 可照抄的 git 命令。**只建议不执行**(确认权在人,对齐 skill 绝对规则「不要自动提交」)。
865
+ // 可照抄的 git 命令。**默认只建议不执行**(确认权在人,对齐 skill 绝对规则「不要自动提交」);
866
+ // --apply(P1-6,noai-ir-roadmap §4)唯一例外且只限归档语境(文件已验收 + pathspec
867
+ // 显式 + pathspec 限定 commit 不扫并行会话暂存)——agent 上下文「需人确认」是伪约束,
868
+ // 通用自动 git 在多会话仓是历史重灾区,故收窄到这一处。
869
+ if (args.includes('--apply')) {
870
+ const apChangeIdx = args.indexOf('--change');
871
+ const apChange = apChangeIdx >= 0 && args[apChangeIdx + 1] ? args[apChangeIdx + 1] : null;
872
+ if (!apChange) {
873
+ console.error('用法: sillyspec commit --apply --change <名>\n 只限归档语境(changes.status=archived 或 current_stage=archive)执行归档产物提交');
874
+ process.exit(2);
875
+ }
876
+ assertSafeChangeName(apChange, '--change 变更名');
877
+ const { applyArchiveCommit } = await import('./commit-suggest.js');
878
+ const r = applyArchiveCommit({ cwd: dir, specDir, changeName: apChange });
879
+ if (!r.ok) {
880
+ console.error(`❌ commit --apply 拒绝: ${r.reason}`);
881
+ process.exit(1);
882
+ }
883
+ console.log(`✅ 归档提交完成(pathspec 限定 ${r.paths.length} 个路径,不含并行会话暂存):`);
884
+ for (const p of r.paths) console.log(` - ${p}`);
885
+ console.log(`\n💬 ${r.subject}`);
886
+ if (r.output) console.log(r.output.split('\n').map(l => ' ' + l).join('\n'));
887
+ break;
888
+ }
844
889
  const { collectCommitContext } = await import('./commit-suggest.js');
845
890
  const cc = collectCommitContext({ cwd: dir, specDir });
846
891
  if (json) {
@@ -887,7 +932,8 @@ async function main() {
887
932
  console.log(cc.suggestion.body.split('\n').map(l => ' ' + l).join('\n'));
888
933
  }
889
934
  const bodyArgs = cc.suggestion.body ? ` -m "${cc.suggestion.body.replace(/"/g, '\\"').replace(/\n/g, ' ')}"` : '';
890
- console.log(`\n 执行:git add -A && git commit -m "${cc.suggestion.subject.replace(/"/g, '\\"')}"${bodyArgs}`);
935
+ // 规则 18(2026-09-10 目录级 git add 事故):建议命令用显式 pathspec,不再教 git add -A
936
+ console.log(`\n 执行:git add -- <本次文件…> && git commit -m "${cc.suggestion.subject.replace(/"/g, '\\"')}"${bodyArgs} -- <本次文件…>`);
891
937
  } else {
892
938
  console.log('\n💡 无语义来源匹配——请结合上方 diff stat 手写 message');
893
939
  }
@@ -908,7 +954,7 @@ async function main() {
908
954
  process.exit(2);
909
955
  }
910
956
  assertSafeChangeName(vpChange, '--change 变更名');
911
- const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts } = await import('./verify-probes.js');
957
+ const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote } = await import('./verify-probes.js');
912
958
  // spec 根统一走漂移锚定(坑 worktree-spec-artifact-misplace):在 worktree 内跑时锚回主仓,
913
959
  // 探针读取与 --init 骨架都落主仓——与 plan/execute/verify/archive 的 command.js 守卫同口径。
914
960
  // resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时
@@ -917,6 +963,19 @@ async function main() {
917
963
  const vpResolved = resolvePlatformSpecDir(dir, specDir);
918
964
  const vpPlatformBase = existsSync(join(dir, '.sillyspec-platform.json')) ? vpResolved : null;
919
965
  const vpSpecBase = resolveVerifyProbesSpecBase(dir, specDir, vpPlatformBase);
966
+ // 平台模式回显注记(2026-09-10 驾驭小结②):物理路径在 hub 镜像根,产物经 daemon
967
+ // spec-sync 落主仓——回显补主仓同步位置,消「回显镜像路径 / 核对主仓路径」的显示混乱
968
+ const vpPlatformNote = formatPlatformPathNote(vpPlatformBase, vpChange, 'verify-result.md');
969
+ // 平台模式双写镜像(坑 platform-init-artifact-daemon-dir-only,2026-09-10 驾驭小结第三批②):
970
+ // agent 本地(pointer 态、非显式 --spec-dir)--init 产物同时镜像主仓 changeDir,不再单点
971
+ // 依赖 spec-sync 回程(用户两次实锤产物落 daemon specs 同步目录)。
972
+ const mirrorInitArtifact = async (platformPath, file, content) => {
973
+ if (!vpPlatformBase || specDir) return null
974
+ try {
975
+ const { mirrorPlatformArtifactToMainRepo } = await import('./run/shared.js')
976
+ return mirrorPlatformArtifactToMainRepo({ cwd: dir, changeName: vpChange, file, content, platformBase: vpPlatformBase })
977
+ } catch { return null }
978
+ }
920
979
  const vpResult = runVerifyProbes({ cwd: dir, changeName: vpChange, specDir: vpSpecBase });
921
980
  if (json) {
922
981
  console.log(JSON.stringify({ command: 'verify-probes', change: vpChange, ...vpResult }, null, 2));
@@ -936,16 +995,25 @@ async function main() {
936
995
  // 指引「重跑 --init」对已存在报告是 no-op。修法:探针段是 CLI 拥有的机械段,检测缺失时
937
996
  // 尾部补注入(正文其余不动),判别子自此有子节可对账。
938
997
  const vpSection = `\n## 探针结果(CLI 机械预填,--init 补注入) [层:可复跑探针——gate 抽查防篡改]\n${renderVerifyProbesReport(vpResult)}\n`;
939
- writeFileSync(vpReportPath, vpExisting.replace(/\s*$/, '\n') + vpSection);
940
- console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)`);
998
+ const vpInjected = vpExisting.replace(/\s*$/, '\n') + vpSection;
999
+ writeFileSync(vpReportPath, vpInjected);
1000
+ await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpInjected);
1001
+ console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)${vpPlatformNote}`);
941
1002
  } else {
942
- console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}`);
1003
+ console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}${vpPlatformNote}`);
943
1004
  }
944
1005
  } else {
945
- writeFileSync(vpReportPath, generateVerifyResultSkeleton(vpResult));
946
- console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)`);
1006
+ const vpSkeleton = generateVerifyResultSkeleton(vpResult);
1007
+ writeFileSync(vpReportPath, vpSkeleton);
1008
+ await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpSkeleton);
1009
+ console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)${vpPlatformNote}`);
1010
+ }
1011
+ const vpFacts = writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange, {
1012
+ platformNote: formatPlatformPathNote(vpPlatformBase, vpChange, 'verify-facts.json'),
1013
+ }); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新;v2 起分段合并保留固化段)
1014
+ if (vpFacts && vpFacts.path) {
1015
+ try { await mirrorInitArtifact(vpFacts.path, 'verify-facts.json', readFileSync(vpFacts.path, 'utf8')) } catch { /* 读回失败跳过镜像 */ }
947
1016
  }
948
- writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新;v2 起分段合并保留固化段)
949
1017
  // 2026-09-08-ir-verify-facts R-02:已存在 md 缺证据账/回执槽段时仅追加骨架(幂等二跑
950
1018
  // 零改动);task 行按 verify-required-evidence.json 预填(存在时)。
951
1019
  if (existsSync(vpReportPath)) {
@@ -963,6 +1031,92 @@ async function main() {
963
1031
  console.log(`📄 已补齐缺失槽段(不触碰既有正文): ${vpAdded.added.join('、')}`)
964
1032
  }
965
1033
  }
1034
+ // P0-1(noai-ir-roadmap §3)两件机械预填,均幂等 fail-soft:
1035
+ // ①决策追踪矩阵机械半边——D→FR→task 链自 decisions.md × tasks/*.md frontmatter
1036
+ // 结构化字段构建,Evidence/状态两列留 agent 逐格复核;
1037
+ // ②结论草稿——机械事实全绿(noAI 实测 + 探针 1/3/5/6 + 风险门)才预填 PASS,
1038
+ // 「草稿待确认」形态,gate 判定链不动(前置三)。
1039
+ try {
1040
+ const { injectDecisionChainDraft } = await import('./verify-probes.js')
1041
+ const vpChain = injectDecisionChainDraft(vpReportPath, join(vpSpecBase, 'changes', vpChange))
1042
+ if (vpChain) {
1043
+ try { await mirrorInitArtifact(vpReportPath, 'verify-result.md', readFileSync(vpReportPath, 'utf8')) } catch { /* 镜像失败不阻断 */ }
1044
+ console.log(`🔗 决策追踪矩阵机械半边已预填:${vpChain.decisions} 条决策 × ${vpChain.tasks} 张 task 卡(Evidence/状态两列待逐格复核;未闭环行须在报告标注风险)`)
1045
+ }
1046
+ } catch { /* 链路预填失败不阻断 --init */ }
1047
+ try {
1048
+ const { extractVerifyConclusionSlot } = await import('./stage-contract.js')
1049
+ const { evaluateConclusionDraft } = await import('./run/verify-quality-scan.js')
1050
+ const { applyConclusionDraftToText } = await import('./verify-probes.js')
1051
+ const vpMd = readFileSync(vpReportPath, 'utf8')
1052
+ if (extractVerifyConclusionSlot(vpMd) === '') {
1053
+ const vpDraftEval = evaluateConclusionDraft({ cwd: dir, specBase: vpSpecBase, changeName: vpChange, changeDir: join(vpSpecBase, 'changes', vpChange), probesResult: vpResult })
1054
+ if (vpDraftEval.draft) {
1055
+ const vpNext = applyConclusionDraftToText(vpMd, vpDraftEval.draft)
1056
+ writeFileSync(vpReportPath, vpNext)
1057
+ try { await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpNext) } catch { /* 镜像失败不阻断 */ }
1058
+ console.log(`🤖 结论草稿已预填 ${vpDraftEval.draft}(机械事实全绿:noAI 实测通过 + 探针 1/3/5/6 干净 + 风险门非 integration/deployment)——草稿待确认,复核后不同意即改写槽行;gate 独立复核不受预填影响。`)
1059
+ } else {
1060
+ console.log(`ℹ️ 结论草稿未预填(${vpDraftEval.reasons.join(';')})——槽行由你填写。`)
1061
+ }
1062
+ }
1063
+ } catch { /* 草稿预填失败不阻断 --init */ }
1064
+ }
1065
+ break;
1066
+ }
1067
+ case 'review-dispatch': {
1068
+ // tier=independent 独立审查的平台派发命令(2026-09-10-review-dispatch FR-01~04 / D-001~003@1):
1069
+ // 三形态——缺省=create(probe 三层前置 + createMission(external) + dispatchWorker(read_only),
1070
+ // 异步立即返回)、--status=轮询+停滞检测+终态回收(artifacts→schema/docHash 校验→落盘既有
1071
+ // stage-reviews 路径)、--kill=清在途记录+平台处置指引(处置权在人)。薄壳:全部逻辑在
1072
+ // src/review-dispatch.js(可测核心),此处只做 flag 解析与退出码。
1073
+ const rdChangeIdx = args.indexOf('--change');
1074
+ const rdChange = rdChangeIdx >= 0 && args[rdChangeIdx + 1] ? args[rdChangeIdx + 1] : null;
1075
+ const rdStageIdx = args.indexOf('--stage');
1076
+ const rdStage = rdStageIdx >= 0 && args[rdStageIdx + 1] ? args[rdStageIdx + 1] : null;
1077
+ const rdStatus = args.includes('--status');
1078
+ const rdKill = args.includes('--kill');
1079
+ const rdMode = rdKill ? 'kill' : rdStatus ? 'status' : 'create';
1080
+ const rdUsage = '用法: sillyspec review-dispatch --change <名> --stage <brainstorm|plan|execute> [--status] [--kill]\n' +
1081
+ ' 缺省=派发(probe 前置→create_mission(external)→dispatch_worker(read_only),异步返回 missionId)\n' +
1082
+ ' --status=轮询状态+停滞提示;worker completed 自动回收落盘 review.json(reviewer.channel=platform)\n' +
1083
+ ' --kill=清本地在途记录+平台处置指引(不自动 kill,处置权在人)';
1084
+ if (!rdChange) { console.error(rdUsage); process.exit(2); }
1085
+ assertSafeChangeName(rdChange, '--change 变更名');
1086
+ if (rdMode === 'create' && !['brainstorm', 'plan', 'execute'].includes(rdStage)) {
1087
+ console.error(`❌ create 形态必须 --stage ∈ brainstorm|plan|execute(实际:${rdStage || '缺'})\n${rdUsage}`);
1088
+ process.exit(2);
1089
+ }
1090
+ const { runReviewDispatch, readReviewDispatchConfig } = await import('./review-dispatch.js');
1091
+ const rdCfg = readReviewDispatchConfig(dir);
1092
+ // MCP client:kill 不需要网络;create/status 需要(未配置时核心层 probe no-config 退出并给降级指引)
1093
+ let rdClient = null;
1094
+ if (rdMode !== 'kill') {
1095
+ const { SillyHubMcpClient } = await import('./sillyhub-mcp/client.js');
1096
+ rdClient = new SillyHubMcpClient({ cwd: dir });
1097
+ }
1098
+ // specBase:与其他顶级命令同口径(平台接管优先;无 pointer 回退本地 .sillyspec)
1099
+ const rdSpecBase = resolvePlatformSpecDir(dir, specDir);
1100
+ const r = await runReviewDispatch({
1101
+ mode: rdMode, cwd: dir, specBase: rdSpecBase, changeName: rdChange, stage: rdStage,
1102
+ client: rdClient, budgetUsd: rdCfg.budget_usd, stallMs: rdCfg.stall_ms,
1103
+ });
1104
+ if (r.ok) {
1105
+ if (r.statusLine) console.log(`🛰️ ${r.statusLine}`);
1106
+ if (r.state === 'completed') {
1107
+ console.log(`✅ 平台审查回收完成:review.json 已落 ${r.reviewPath}(verdict ${r.verdict})——重跑阶段 --done 走 Stage Review Gate`);
1108
+ } else if (rdMode === 'create') {
1109
+ console.log(`✅ 已派发:mission ${r.missionId} / worker ${r.workerId}(reviewRunId ${r.reviewRunId})`);
1110
+ }
1111
+ if (r.stalled) console.warn(`⚠️ ${r.stallHint}`);
1112
+ if (r.nextStep && rdMode !== 'kill') console.log(`👉 ${r.nextStep}`);
1113
+ if (rdMode === 'kill') console.log(`👉 ${r.platformHint}`);
1114
+ } else {
1115
+ console.error(`❌ review-dispatch(${rdMode}): ${r.reason}`);
1116
+ if (r.statusLine) console.error(` ${r.statusLine}`);
1117
+ if (r.errors) for (const e of r.errors) console.error(` - ${e}`);
1118
+ if (r.guidance) console.error(` 下一步:\n${r.guidance}`);
1119
+ process.exitCode = 1;
966
1120
  }
967
1121
  break;
968
1122
  }
@@ -1069,6 +1223,96 @@ async function main() {
1069
1223
  console.log(` 归类 ${miResult.matchedCount} 个文件,未匹配 ${miResult.unmatchedCount} 个;影响类型列逐行替换 <!--TODO-->。`);
1070
1224
  break;
1071
1225
  }
1226
+ case 'scope-audit': {
1227
+ // 变更范围对账命令化(2026-09-10-change-scope-audit task-03,FR-02):Wave 1 纯函数
1228
+ // computeChangeScopeAudit 的 CLI 形态——用户与 agent 不依赖阶段 --done 时点,随时查
1229
+ // 「计划改动 × 实际改动」三态全表(✓ 计划内 / ⚠️ 计划外 / ⚠️ 计划未动;quick 会话出
1230
+ // 归属表)。advisory only(D-006):只读不落盘、不写门禁状态、既有命令行为零变化;
1231
+ // 路由形态对齐 verify-probes(:897)/ module-impact(:1041)先例——--change 必填缺参
1232
+ // 用法错 exit 2、assertSafeChangeName 消毒、动态 import、--json 结构化输出、运行错
1233
+ // fail-soft 出错误摘要 exit 1 不抛栈。
1234
+ const saChangeIdx = args.indexOf('--change');
1235
+ const saChange = saChangeIdx >= 0 && args[saChangeIdx + 1] ? args[saChangeIdx + 1] : null;
1236
+ 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 只读展示,不构成门禁');
1238
+ process.exit(2);
1239
+ }
1240
+ assertSafeChangeName(saChange, '--change 变更名');
1241
+ // --file(quick-63776328):单文件 diff 内容查看——锚点与表格行数同源(getFileDiff 内部
1242
+ // 复用 computeChangeScopeAudit 锚 + resolveDiffRoot),用户无需手记基点/worktree 路径。
1243
+ const saFileIdx = args.indexOf('--file');
1244
+ const saFile = saFileIdx >= 0 && args[saFileIdx + 1] && !String(args[saFileIdx + 1]).startsWith('--') ? args[saFileIdx + 1] : null;
1245
+ const { computeChangeScopeAudit, renderScopeAuditTable, getFileDiff } = await import('./scope-audit.js');
1246
+ // specBase 解析对齐 verify-probes 的 worktree 锚定主仓口径(坑 worktree-spec-artifact-misplace):
1247
+ // resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时才把
1248
+ // 返回值当平台根——无 pointer 时它回退 resolveSpecDir(cwd) 会返回 worktree 副本根,误当
1249
+ // platformBase 会让锚定被跳过。无显式 --spec-dir 时 detectWorktreeSpecDrift 锚回主仓
1250
+ // (worktree 副本里查不到主仓 change 目录——dogfood 自测即此形态)。
1251
+ const saResolved = resolvePlatformSpecDir(dir, specDir);
1252
+ const saPlatformBase = existsSync(join(dir, '.sillyspec-platform.json')) ? saResolved : null;
1253
+ let saSpecBase = saPlatformBase || resolveSpecDir(dir, { specDir: specDir || undefined });
1254
+ let saDriftAnchor = null;
1255
+ if (!saPlatformBase && !specDir) {
1256
+ const saWt = detectWorktreeSpecDrift(saSpecBase);
1257
+ if (saWt) {
1258
+ console.warn(`⚠️ 已自动锚定主仓 spec:${saWt.mainSpecBase}(原 cwd 命中 worktree 副本 ${saWt.changeName},scope-audit 对账读主仓,流程继续)`);
1259
+ saSpecBase = saWt.mainSpecBase;
1260
+ saDriftAnchor = saWt.mainSpecBase;
1261
+ }
1262
+ }
1263
+ // full-flow 变更目录存在性预检(endpoints baseline 先例同款):post-apply 形态下实际侧三源
1264
+ // 对「变更不存在」不敏感(status 成功即 ok=true,会把主仓脏区误当实际侧),靠预检给明确
1265
+ // 错误摘要 exit 1;quick 会话 id 无 changes/ 目录(guard 祖先链定位),不预检。
1266
+ // 归档形态兼容(quick-8aa52289):changes/<名> 不在 → 接受 changes/archive/<名>(scope-audit
1267
+ // 内部走快照记录态/HEAD 未提交窗口兜底)。
1268
+ if (!/^quick-[0-9a-f]{8}$/.test(saChange)
1269
+ && !existsSync(join(saSpecBase, 'changes', saChange))
1270
+ && !existsSync(join(saSpecBase, 'changes', 'archive', saChange))) {
1271
+ console.error(`❌ 变更目录不存在(活跃与归档区均未找到): ${join(saSpecBase, 'changes', saChange)}(确认 --change 名,或 --spec-dir 指向主仓 .sillyspec)`);
1272
+ process.exit(1);
1273
+ }
1274
+ // --file 分支:单文件 diff(预检同表模式——锚点依赖变更上下文;json 面出结构化)
1275
+ if (saFile) {
1276
+ let fd;
1277
+ try {
1278
+ fd = await getFileDiff({ cwd: dir, specBase: saSpecBase, changeName: saChange, platformOpts: saDriftAnchor ? { specDriftAnchor: saDriftAnchor } : null, filePath: saFile });
1279
+ } catch (e) {
1280
+ console.error(`❌ scope-audit --file 执行失败: ${e && e.message ? String(e.message).split('\n')[0] : e}`);
1281
+ process.exit(1);
1282
+ }
1283
+ if (json) {
1284
+ console.log(JSON.stringify({ command: 'scope-audit', change: saChange, file: saFile, ...fd }, null, 2));
1285
+ } else if (fd.ok && fd.diff) {
1286
+ console.log(`📄 ${saFile}(${fd.mode}|锚点 ${fd.anchorLabel}|git diff 原生格式)`);
1287
+ console.log(fd.diff);
1288
+ } else {
1289
+ console.log(`ℹ️ ${saFile}(${fd.mode}|锚点 ${fd.anchorLabel || '—'}):${fd.note || '无 diff'}`);
1290
+ }
1291
+ if (!fd.ok) process.exitCode = 1;
1292
+ break;
1293
+ }
1294
+ // 计算包在 withJsonOutput 内(gate/derive 先例同款):quick 模式重跑 auditQuickCompletion
1295
+ // 的既有 console 输出(并发 warn 等,R-07)不污染 --json 的 stdout。
1296
+ let saResult;
1297
+ try {
1298
+ saResult = await withJsonOutput(json, () => computeChangeScopeAudit({ cwd: dir, specBase: saSpecBase, changeName: saChange, platformOpts: saDriftAnchor ? { specDriftAnchor: saDriftAnchor } : null }));
1299
+ } catch (e) {
1300
+ console.error(`❌ scope-audit 计算失败: ${e && e.message ? String(e.message).split('\n')[0] : e}`);
1301
+ process.exit(1);
1302
+ }
1303
+ if (json) {
1304
+ console.log(JSON.stringify({ command: 'scope-audit', change: saChange, ...saResult }, null, 2));
1305
+ } else {
1306
+ console.log(renderScopeAuditTable(saResult));
1307
+ }
1308
+ // ok=false(quick 会话不存在 / 实际侧整体失败)→ 明确错误摘要 + exit 1(json 面摘要走
1309
+ // stderr,stdout 保持合法 JSON)
1310
+ if (saResult && saResult.ok === false) {
1311
+ console.error(`❌ scope-audit 对账不可用: ${saResult.degradedReason || '未知原因'}`);
1312
+ process.exitCode = 1;
1313
+ }
1314
+ break;
1315
+ }
1072
1316
  case 'endpoints': {
1073
1317
  // endpoints artifact 生成(2026-08-21 审计第三批 H6):后端 router task 的 endpoints.json
1074
1318
  // 此前靠 agent 手扫装饰器手写(易漏 endpoint)。CLI 复用 endpoint-extractor 全套提取器
@@ -1386,6 +1630,7 @@ async function main() {
1386
1630
  mk('proposal.md', `---
1387
1631
  author: qinyi
1388
1632
  created_at: ${fpStamp}
1633
+ generated_by: sillyspec-fourpiece-init
1389
1634
  ---
1390
1635
  # 提案书(Proposal)
1391
1636
 
@@ -1407,6 +1652,7 @@ created_at: ${fpStamp}
1407
1652
  mk('requirements.md', `---
1408
1653
  author: qinyi
1409
1654
  created_at: ${fpStamp}
1655
+ generated_by: sillyspec-fourpiece-init
1410
1656
  ---
1411
1657
  # 需求规格(Requirements)
1412
1658
 
@@ -1432,6 +1678,7 @@ Then <!--TODO-->
1432
1678
  mk('decisions.md', `---
1433
1679
  author: qinyi
1434
1680
  created_at: ${fpStamp}
1681
+ generated_by: sillyspec-fourpiece-init
1435
1682
  change: ${fpChange}
1436
1683
  ---
1437
1684
 
@@ -1492,6 +1739,17 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1492
1739
  break;
1493
1740
  }
1494
1741
  writeFileSync(diDesignPath, diSkeleton);
1742
+ // 平台模式双写镜像(坑 platform-init-artifact-daemon-dir-only,2026-09-10 驾驭小结第三批②):
1743
+ // agent 本地(pointer 态、非显式 --spec-dir)跑 design-init 时 design.md 同时镜像主仓
1744
+ // changeDir——不再单点依赖 spec-sync 回程(两次实锤产物落 daemon specs 同步目录)。
1745
+ try {
1746
+ const { mirrorPlatformArtifactToMainRepo } = await import('./run/shared.js');
1747
+ mirrorPlatformArtifactToMainRepo({
1748
+ cwd: dir, changeName: diChange, file: 'design.md', content: diSkeleton,
1749
+ platformBase: existsSync(join(dir, '.sillyspec-platform.json')) ? diSpecBase : null,
1750
+ specDirExplicit: Boolean(specDir),
1751
+ });
1752
+ } catch { /* 镜像失败不阻断主写入 */ }
1495
1753
  if (json) {
1496
1754
  console.log(JSON.stringify({ command: 'design-init', change: diChange, ok: true, path: diDesignPath, written: true, overwritten: diExisted }, null, 2));
1497
1755
  break;
@@ -1792,23 +2050,36 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1792
2050
  // 只读 stdout 可捕获);stderr 仅留 ⚠️ 诊断(console.warn)与配置错误(exit 2 路径)。
1793
2051
  // 文本逐字不变,只改通道。
1794
2052
  console.log(`\n❌ docs check: ${result.invalid.length}/${result.total} 处引用失效:`);
2053
+ // 显示去重(2026-09-10 驾驭小结第二批③):同 doc+docLine+ref 的重复 invalid(同一
2054
+ // 引用在行内多次出现时逐条收集)只显示一次、带 ×N 次数——invalid 集与 exit code
2055
+ // 不动,纯渲染降噪。
2056
+ const shownInvalid = [];
2057
+ const dupCountByIdentity = new Map();
2058
+ for (const inv of result.invalid) {
2059
+ const key = `${inv.doc}#${inv.docLine}#${inv.ref}`;
2060
+ dupCountByIdentity.set(key, (dupCountByIdentity.get(key) || 0) + 1);
2061
+ if (dupCountByIdentity.get(key) === 1) shownInvalid.push(inv);
2062
+ }
2063
+ const dupTotal = result.invalid.length - shownInvalid.length;
1795
2064
  let appliedLeft = fixResult ? fixResult.applied : 0;
1796
2065
  const skippedQueue = fixResult ? [...fixResult.skipped] : [];
1797
- for (const inv of result.invalid) {
2066
+ for (const inv of shownInvalid) {
2067
+ const dupN = dupCountByIdentity.get(`${inv.doc}#${inv.docLine}#${inv.ref}`);
2068
+ const dupTag = dupN > 1 ? `(同引用重复 ×${dupN})` : '';
1798
2069
  const newRef = newRefByInv.get(inv);
1799
2070
  if (newRef !== undefined && appliedLeft > 0) {
1800
2071
  // 已应用(dry-run 为将应用):归因按构造顺序消耗 applied 计数——写回 skipped 仅
1801
2072
  // 防御路径(文档消失/行内失配)触发,CLI 正常流 fixable 必然落位
1802
- console.log(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}`);
2073
+ console.log(` ✅ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${newRef}${dryRun ? '(dry-run 未写盘)' : ''}${dupTag}`);
1803
2074
  appliedLeft--;
1804
2075
  } else if (newRef !== undefined) {
1805
2076
  const s = skippedQueue.shift();
1806
- console.log(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}`);
2077
+ console.log(` ⚠️ [${inv.doc}:L${inv.docLine}] ${inv.ref} → 写回跳过:${s ? s.reason : '未知原因'}${dupTag}`);
1807
2078
  } else if (fixResult) {
1808
2079
  // needs-manual:多命中歧义/零命中/无 token——fix.reason 含候选行号,交人工
1809
- console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)`);
2080
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${(inv.fix && inv.fix.reason) || inv.reason}(待人工)${dupTag}`);
1810
2081
  } else {
1811
- console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}`);
2082
+ console.log(` ❌ [${inv.doc}:L${inv.docLine}] ${inv.ref} → ${inv.reason}${dupTag}`);
1812
2083
  }
1813
2084
  if (inv.suggest && inv.suggest.length > 0) {
1814
2085
  // 2026-09-07-ir-hardening task-08:原 --suggest 旗标删除(no-op 首选路径退役),
@@ -1816,6 +2087,9 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1816
2087
  console.log(` 💡 候选行号: ${inv.suggest.join(', ')}(token 命中行,人工确认后更新文档锚)`);
1817
2088
  }
1818
2089
  }
2090
+ if (dupTotal > 0) {
2091
+ console.log(` ℹ️ 已折叠 ${dupTotal} 条同文档行同引用的重复条目(计数 ×N 已并入上文)。`);
2092
+ }
1819
2093
  if (fixResult) {
1820
2094
  console.log(`\n🔧 重锚报告:${fixResult.applied} 处已${dryRun ? '预览' : '改写'}${dryRun ? '(dry-run 未写盘)' : ''}、${result.invalid.length - fixes.length} 处待人工${fixResult.skipped.length > 0 ? `、${fixResult.skipped.length} 处写回跳过` : ''}。`);
1821
2095
  if (fixReceipt) {
@@ -1828,9 +2102,21 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
1828
2102
  // 变更名提名 advisory 段(主结果之后、exit 之前;不参与 ok/invalid 与 exit code)。
1829
2103
  // 零输出原则(docs-check-fix 契约:无新 flag 时 stdout 逐字节一致;决策规则族同款
1830
2104
  // 「无信号零输出」):仅 findings 非空才输出——悬空即信号,无悬空不打扰。
2105
+ // 输出降噪(2026-09-10 驾驭小结第二批③):①advisory 固定尾部分区(硬失效区块与
2106
+ // 修复指引之后),标题明示「不阻断、不进 exit code」,与硬失效 ❌ 视觉分离;②同一
2107
+ // 悬空名多处提名按名聚合一行(名 + 提及处数 + 首处定位),不再逐处刷屏。
1831
2108
  if (changeNameReport && changeNameReport.findings.length > 0) {
1832
- console.warn(`⚠️ 变更名提名悬空 ${changeNameReport.findings.length}/${changeNameReport.mentions} 处(advisory 不阻断、不进 docs gate):`);
1833
- for (const f of changeNameReport.findings) console.warn(` - ${f.message}`);
2109
+ console.log(`\n⚠️ ── 变更名提名悬空(advisory:不阻断、不进 exit code / docs gate)──`);
2110
+ const byName = new Map(); // name -> { count, first }
2111
+ for (const f of changeNameReport.findings) {
2112
+ const e = byName.get(f.name) || { count: 0, first: f };
2113
+ e.count++;
2114
+ byName.set(f.name, e);
2115
+ }
2116
+ for (const [name, e] of byName) {
2117
+ console.log(` - 「${name}」${e.count} 处提及悬空(活跃∪归档变更名单中不存在;首处 ${e.first.doc}:L${e.first.docLine})——拼错 / 改名未同步 / 他仓变更?修正后复跑,或 local.yaml known_failures 加 "change-name.${name}" 豁免`);
2118
+ }
2119
+ console.log(` (共 ${changeNameReport.findings.length} 处 / ${changeNameReport.uniqueNames} 个名字,全文提及 ${changeNameReport.mentions} 处)`);
1834
2120
  if (changeNameReport.exempted.length > 0) {
1835
2121
  console.log(`ℹ️ 另有 ${changeNameReport.exempted.length} 处经 known_failures change-name.* 键豁免`);
1836
2122
  }
@@ -3022,6 +3308,108 @@ SillySpec platform — SillyHub 平台同步
3022
3308
  }
3023
3309
  break;
3024
3310
  }
3311
+ case 'mcp': {
3312
+ // MCP Phase 2(P2-g,noai-ir-roadmap §5):最小 stdio MCP server——只读 tools
3313
+ //(next/gate/derive/progress)经子进程 --json 隔离执行,schema 化参数锁死 flag 幻觉面。
3314
+ // hosts 配 command=sillyspec args=[mcp]。Phase 3(--done 状态推进走 MCP)另计。
3315
+ const { startMcpServer } = await import('./mcp-server.js');
3316
+ await startMcpServer();
3317
+ break;
3318
+ }
3319
+ case 'validate': {
3320
+ // 产物 schema 校验总命令(P2-d,noai-ir-roadmap §5):聚合既有校验器(verify-facts /
3321
+ // task review / stage review / module-map)+ 轻形状检查(required-evidence / endpoints)
3322
+ // 为一把伞——「新解析器写之前先过 schema」的统一落点。skip = 未生成不算失败。
3323
+ const vaChangeIdx = args.indexOf('--change');
3324
+ const vaChange = vaChangeIdx >= 0 && args[vaChangeIdx + 1] ? args[vaChangeIdx + 1] : null;
3325
+ if (!vaChange) {
3326
+ console.error('用法: sillyspec validate --change <名> [--json]');
3327
+ console.error(' 校验该变更的全部结构化产物(verify-facts/required-evidence/task reviews/stage reviews/endpoints + module-map)');
3328
+ process.exit(2);
3329
+ }
3330
+ assertSafeChangeName(vaChange, '--change 变更名');
3331
+ const { validateChangeArtifacts, renderValidateReport } = await import('./validate-artifacts.js');
3332
+ const vaSpecBase = resolveSpecDir(dir, specDir && typeof specDir === 'string' ? { specDir } : undefined);
3333
+ const vaResult = await validateChangeArtifacts({ cwd: dir, specBase: vaSpecBase, changeName: vaChange });
3334
+ if (json) {
3335
+ console.log(JSON.stringify({ command: 'validate', change: vaChange, ok: vaResult.ok, checks: vaResult.checks }, null, 2));
3336
+ } else {
3337
+ console.log(renderValidateReport(vaResult));
3338
+ }
3339
+ if (!vaResult.ok) process.exit(1);
3340
+ break;
3341
+ }
3342
+ case 'decisions': {
3343
+ // P1-5(noai-ir-roadmap §4):decisions.md 单一写入者——canonical 格式只此一家(标题式
3344
+ // 条目 + applyField 白名单中文标签),agent 不再手拼自由 markdown(扁平列表静默 0 条 /
3345
+ // 字段拼写漂移事故族退役;读取侧 parseDecisions 零改动双向兼容)。
3346
+ // decisions add --change <c> --id D-001 [--version N] --title "…" [--status confirmed]
3347
+ // [--type architecture] [--domain a(可重复)] [--question/--answer/--anchor]
3348
+ // [--reject-reason/--revisit-when/--supersedes/--impacts] [--note "散文行(可重复)"]
3349
+ // 同 id@v 整块替换(幂等);新 id/version 追加。
3350
+ // decisions list --change <c>——parseDecisions 只读视图(id/状态/类型/模块域/标题)。
3351
+ const decSub = filteredArgs[1];
3352
+ const decChangeIdx = args.indexOf('--change');
3353
+ const decChange = decChangeIdx >= 0 && args[decChangeIdx + 1] ? args[decChangeIdx + 1] : null;
3354
+ if (!decChange || !['add', 'list'].includes(decSub)) {
3355
+ 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
+ process.exit(2);
3357
+ }
3358
+ assertSafeChangeName(decChange, '--change 变更名');
3359
+ const decChangeDir = join(resolveSpecDir(dir), 'changes', decChange);
3360
+ if (decSub === 'list') {
3361
+ const { parseDecisions } = await import('./decision-distill.js');
3362
+ const parsed = parseDecisions(decChangeDir);
3363
+ if (parsed.missing) { console.error(`❌ decisions.md 不存在: ${join(decChangeDir, 'decisions.md')}`); process.exit(1); }
3364
+ for (const e of parsed.entries) {
3365
+ console.log(`${e.id} [${e.status || '?'}${e.type ? '/' + e.type : ''}]${(e.domains || []).length ? `[${e.domains.join(',')}]` : ''} ${e.title || '(无标题)'}`);
3366
+ }
3367
+ console.log(`共 ${parsed.entries.length} 条`);
3368
+ break;
3369
+ }
3370
+ // add:flag 收集(多值 --domain/--note 重复收集)
3371
+ const decFlag = (name) => { const i = args.indexOf(name); return i >= 0 && args[i + 1] && !String(args[i + 1]).startsWith('--') ? args[i + 1] : null; };
3372
+ const decMulti = (name) => { const out = []; for (let i = 0; i < args.length; i++) if (args[i] === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) out.push(args[i + 1]); return out; };
3373
+ const decId = decFlag('--id');
3374
+ const decTitle = decFlag('--title');
3375
+ if (!decId || !decTitle) { console.error('❌ decisions add 需要 --id D-xxx 与 --title'); process.exit(2); }
3376
+ if (!/^D-\d+$/.test(decId)) { console.error(`❌ --id 形态非法(期望 D-xxx,实得 ${decId})`); process.exit(2); }
3377
+ const decVersionRaw = decFlag('--version');
3378
+ const decVersion = decVersionRaw === null ? null : parseInt(decVersionRaw, 10);
3379
+ const { parseDecisions } = await import('./decision-distill.js');
3380
+ const decParsed = parseDecisions(decChangeDir);
3381
+ let version = decVersion;
3382
+ if (!Number.isInteger(version) || version < 1) {
3383
+ const maxV = decParsed.entries.filter(e => e.number === decId).reduce((m, e) => Math.max(m, e.version), 0);
3384
+ version = maxV + 1;
3385
+ }
3386
+ const decision = {
3387
+ number: decId,
3388
+ version,
3389
+ title: decTitle,
3390
+ status: decFlag('--status') || undefined,
3391
+ type: decFlag('--type') || undefined,
3392
+ domains: decMulti('--domain'),
3393
+ question: decFlag('--question') || undefined,
3394
+ answer: decFlag('--answer') || undefined,
3395
+ anchor: decFlag('--anchor') || undefined,
3396
+ rejectReason: decFlag('--reject-reason') || undefined,
3397
+ revisitWhen: decFlag('--revisit-when') || undefined,
3398
+ supersedes: decFlag('--supersedes') || undefined,
3399
+ impacts: decFlag('--impacts') || undefined,
3400
+ body: decMulti('--note'),
3401
+ };
3402
+ const { upsertDecision } = await import('./decisions-io.js');
3403
+ try {
3404
+ const r = upsertDecision(decChangeDir, decision);
3405
+ console.log(`📝 决策 ${decId}@v${version} 已${r.action === 'replaced' ? '整块替换(幂等)' : '追加'}: ${r.path}`);
3406
+ if (!decVersionRaw) console.log(` 版本缺省 → 既有最高 v${version - 1} + 1(显式版本用 --version)`);
3407
+ } catch (e) {
3408
+ console.error(`❌ 决策写入被拒: ${e.message}`);
3409
+ process.exit(1);
3410
+ }
3411
+ break;
3412
+ }
3025
3413
  case 'taskcard': {
3026
3414
  const tcName = filteredArgs.slice(1).find(a => !a.startsWith('-'));
3027
3415
  if (!tcName || tcName === 'help' || tcName === '--help' || tcName === '-h') {
@@ -3096,6 +3484,27 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
3096
3484
  // 对齐 endpoints 等命令:先 resolvePlatformSpecDir(指针 fail-closed 语义同源)。
3097
3485
  const tcPlatformSpecDir = resolvePlatformSpecDir(dir, specDir) || specDir;
3098
3486
  const result = cmdTaskcard(tcName, { cwd: dir, specDir: tcPlatformSpecDir, taskIds, title: titleVal, titleZh: titleZhVal, force, sets });
3487
+ // 平台模式产物双写镜像(坑 platform-init-artifact-daemon-dir-only 同族,2026-09-10 驾驭
3488
+ // 小结第六批①,用户实证 taskcard --all 落 daemon 镜像「中间一度两份副本」):agent 本地
3489
+ // (pointer 态、非显式 --spec-dir)生成的卡片即时镜像主仓 changeDir——不再等 spec-sync
3490
+ // 回程。镜像失败不阻断(镜像根照写,spec-sync 仍是兜底)。
3491
+ if (result.created.length > 0 && existsSync(join(dir, '.sillyspec-platform.json')) && !specDir) {
3492
+ try {
3493
+ const { mirrorPlatformArtifactToMainRepo } = await import('./run/shared.js');
3494
+ const changesAbs = join(tcPlatformSpecDir, 'changes', tcName);
3495
+ for (const created of result.created) {
3496
+ const rel = relative(changesAbs, created).split('\\').join('/');
3497
+ if (!rel || rel.startsWith('..')) continue;
3498
+ try {
3499
+ mirrorPlatformArtifactToMainRepo({
3500
+ cwd: dir, changeName: tcName, file: rel,
3501
+ content: readFileSync(created, 'utf8'),
3502
+ platformBase: tcPlatformSpecDir, ensureParentDir: true,
3503
+ });
3504
+ } catch { /* 单卡镜像失败不连坐 */ }
3505
+ }
3506
+ } catch { /* 镜像链路异常不阻断主命令 */ }
3507
+ }
3099
3508
  for (const f of result.created) console.log(`✅ 已生成: ${f}`);
3100
3509
  for (const f of result.skipped) console.log(`⏭️ 已存在,跳过(--force 覆盖): ${f}`);
3101
3510
  if (result.created.length > 0) {
@@ -3115,9 +3524,12 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
3115
3524
  process.exit(1);
3116
3525
  }
3117
3526
  // F6 路径穿越消毒:renameChange 会 mv changes/<old> → changes/<new>,名含 ../ 会逃出 changes/。
3527
+ // 日期前缀门禁(brainstorm step6 规则 CLI 化):改名产生的新名必须 YYYY-MM-DD-<描述>
3528
+ // (2026-09-11 实证:friction-signal-hint 重命名丢前缀被静默物化)。
3118
3529
  try {
3119
3530
  assertSafeChangeName(oldName, '旧变更名');
3120
3531
  assertSafeChangeName(newName, '新变更名');
3532
+ assertDatedChangeName(newName, '新变更名');
3121
3533
  } catch (e) {
3122
3534
  console.error(`❌ ${e.message}`);
3123
3535
  process.exit(2);