sillyspec 3.33.1 → 3.33.3

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.
package/src/flow.js CHANGED
@@ -25,7 +25,7 @@
25
25
  * 六子步完成标记/legacy_fallback/route_hint)——fs-atomic 原子写,缺文件=未参与 thin。
26
26
  * 幂等循 task-done 先例:子步各查自身完成标记,中断半态重入断点续,中段失败精确报告。
27
27
  */
28
- import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, renameSync, statSync } from 'node:fs'
28
+ import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, renameSync, statSync, unlinkSync } from 'node:fs'
29
29
  import { runtimeStatePath, runtimeTempPath } from './runtime-paths.js'
30
30
  import { join, relative, dirname } from 'node:path'
31
31
  import { DatabaseSync } from 'node:sqlite'
@@ -674,7 +674,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
674
674
  const r = redraftMissingArtifacts({ changeDir, change, input: null, runtimeRoot })
675
675
  if (r.drafted.length > 0) console.log(`📌 收编补生成缺失机器稿:${r.drafted.join('、')}(brainstorm 未产的工件机器补齐,criteria 从 proposal 成功标准回提;已存在文件未动)`)
676
676
  const b = ensureBindingSlots({ changeDir })
677
- if (b.appended) console.log(`📌 收编追加测试绑定槽 ${b.slots} 枚(brainstorm requirements 无绑定面——干活时作答,flow done 校验)`)
677
+ if (b.appended) console.log(`📌 收编追加测试绑定槽 ${b.slots} 枚(brainstorm requirements 无绑定面——动码前作答,flow done 校验)`)
678
678
  } catch (e) { console.warn(`⚠️ 收编补件失败(best-effort): ${(e && e.message) || e}`) }
679
679
  // route-hindsight 首版快照(收编时点=redraftMissingArtifacts 首次落盘后):adopt 的
680
680
  // design 是头脑风暴产物、机器只补缺件——快照取收编时点内容,此后 agent 改写计入指标。
@@ -723,8 +723,9 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
723
723
  ? `🖼️ 原型在场(${prototypePaths.length} 个 HTML):实现前必看,界面/交互/流程按原型对齐;有出入以 design.md 承诺为准并在回复中说明。`
724
724
  : null,
725
725
  `【你要做的】① spec 阶段先定稿任务面:把 tasks.md 改写为工作分解(每行=可独立完成并当场验证的一步:做什么+怎么验证;全 \`- [ ]\`——代码开动前工作队列先存在);`,
726
+ `🛑 断点①(机器门):spec 面(任务面+测试绑定槽)定稿后把摘要给用户看 → flow approve --change ${change}(或用户显式 --autopilot)——批准后才动码。design 四节槽收编豁免(以 brainstorm design 为准)。`,
726
727
  `② 执行走任务循环:对 tasks.md 每个 pending 行——展示「Working on task N/M: <任务>」→ 做 → 测试绿后当场勾一格(\`- [ ]\`→\`- [x]\`:sillyspec task tick --change ${change} --task task-NN 或直接 Edit)→ 下一行;全勾后 flow done(收口硬门拒单拍多格勾选)。design/decisions 是本变更的承诺锚(flow done 豁免 design 四节槽,以其为准)。`,
727
- `requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前后顺手填。`,
728
+ `requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前作答(动码前,与任务面定稿同批)。`,
728
729
  `✅ 任务面归你(工作分解契约):tasks.md 是你的工作队列(机器种子可增删改,保持 \`- [ ] task-NN:\` 行形态)——CLI 以 checkbox 为进度状态机(task tick/flow status 实时读它),TodoWrite 类工具不替代;`,
729
730
  ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;勾选修改与任务交付同一提交入库(pathspec 带上本变更 tasks.md——task tick 回显给现成命令);`,
730
731
  ` 任务拟法:收口/推送/发布类动作不单列 task(协议自身承担,成功标准照写 requirements);确需单列的,勾选前先有消息预埋 task-NN token 的交付提交(回执类证据写进提交消息)。`,
@@ -1116,17 +1117,20 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1116
1117
  ``,
1117
1118
  `【协议调用 1/2(本次)】change 已建 + 基线锚定(baseline_commit=${baseline ? baseline.slice(0, 10) : '(无 git 历史)'})${withTasks ? ' + 任务卡模式(--with-tasks:中间自愿用 task done,收尾仍 flow done)' : ''}`,
1118
1119
  ``,
1119
- `【你要做的】直接干活:改代码、写测试。治理工件不用你写——flow done 机器做(协议记账单位=change 级)。`,
1120
+ // 顺序正典(2026-10-19-thin-order-canon):行动句必须是顺序句——实证(artifact 时间戳
1121
+ // 还原+跨仓 14 例抽样)agent 跟从【你要做的】首义行动,旧句「直接干活」把动码提前、
1122
+ // 文档与批准沦为收口仪式;正典=先答 spec 面 → 批准 → 才动码(读代码探究不算动码)。
1123
+ `【你要做的】先答 spec 面,批准后才动码:① 答 spec——requirements FR 行为句+测试绑定行、design.md 四节作答(动码前;读代码探究不算动码);② 断点①——摘要给用户看 → flow approve --change ${change}(或用户显式 --autopilot);③ 才干活——写测试、改代码。治理工件骨架 CLI 已起草、收口机器做(协议记账单位=change 级)。`,
1120
1124
  // 收口拒收红线前置(2026-10-11-flow-start-redline-frontload):钉死/空槽/绑定三面都是文档
1121
1125
  // 先行校验,实证第一次最容易踩(四问文本被改写、区间 task-NN 写法零证据被拒)——横幅
1122
1126
  // 头部先行告知,收口才暴露的规则前置到动笔前。
1123
- `🚫 收口拒收红线(文档先行校验,动笔前先读——第一次最容易踩的就是这三条):`,
1127
+ `🚫 收口拒收红线(文档须先于动码——动笔前先读,第一次最容易踩的就是这三条):`,
1124
1128
  ` ① design 四问文本与 FR 标题锚逐字钉死勿改写——问题原文保持原样、答案写在问题下方,改写/删除即收口拒收(文档↔锚逐字对比);`,
1125
- ` ② design.md 四节(做法/接口契约/边界并发四问/风险)每节至少一行——空节拒收(写「不适用:<理由>」也算答);`,
1126
- ` ③ requirements 每条 FR 须有带强度词的行为句(必须/禁止/SHOULD)+ 测试绑定行——缺任一即拒收。`,
1129
+ ` ② design.md 四节(做法/接口契约/边界并发四问/风险)每节至少一行——空节拒收(写「不适用:<理由>」也算答;动码前作答);`,
1130
+ ` ③ requirements 每条 FR 须有带强度词的行为句(必须/禁止/SHOULD)+ 测试绑定行——缺任一即拒收(动码前作答)。`,
1127
1131
  `书写面(纯 markdown 起草):requirements FR 正文/design 四节作答/测试绑定行都直接写正文——`,
1128
1132
  ` 文档内零指纹标记、零槽注释、零书写指导(规则以本横幅与命令卡为唯一源);成功标准漂移只出 advisory 不拒收。`,
1129
- `⚠️ design.md 四节动码前后顺手作答;列改动文件用独立「## 文件变更清单」节+表格(| 操作 | 路径 | 说明 |)`,
1133
+ `⚠️ design.md 四节动码前作答(它是断点① 批准的输入——后补属超期补写、留痕可审计);列改动文件用独立「## 文件变更清单」节+表格(| 操作 | 路径 | 说明 |)`,
1130
1134
  ` ——勿写在「接口契约」节内(收口按节名解析声明面)。`,
1131
1135
  ...crossRepoHintLines,
1132
1136
  `📜 requirements 每条 FR = 标题锚 + 行为句 + 按需场景块(#### 场景:名 + Given/When/Then);`,
@@ -1150,19 +1154,20 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1150
1154
  // 成功标准≠工作分解——收口/推送/发布是协议自身承担的动作;单列则证据(token 提交)必须先于勾选存在。
1151
1155
  ` 任务拟法:收口/推送/发布类动作不单列 task(提交、归档、推送是 flow start→done 协议自身承担的动作——成功标准照写 requirements,勿逐字搬进任务清单);确需单列的收口/操作型任务,勾选前先做消息预埋 task-NN token 的交付提交——回执类证据(push/publish 回执、构建日志路径)写进提交消息,勿依赖勾选后补;`,
1152
1156
  ` flow status --change ${change} 为自愿查看/恢复面(恢复时给下一任务指针与进度)。`,
1157
+ `💡 执行期用户插入决策方向(有实现影响、约束后续变更的取舍/否决/定调)→ 当场 sillyspec decisions add --change ${change} --id D-001 --title "…" 落盘(只留会话=收口档案不可见、跨变更复潮无防);改变承诺面的同步 design/tasks 对应节。`,
1153
1158
  ``,
1154
- `🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
1159
+ `🛑 三断点纪律(顺序纪律:①未过不动码;②③为汇报点——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
1155
1160
  ` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
1156
1161
  ` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
1157
1162
  ` agent 代跑时如实声明通道:--user-confirmed(转述用户已确认)/ --self-approved(agent 自批),`,
1158
1163
  ` 无 flag 按 unattested 留痕(审计面无法区分用户确认与自批)——2026-10-12-approve-channel-attest。`,
1159
- ` 方案错了返工最贵;未批准且未声明 --autopilot 时 flow done 拒收(2026-10-04-thin-docs-v2);`,
1164
+ ` 方案错了返工最贵;批准是动码前置义务(未批准且未声明 --autopilot 时 flow done 亦拒收——2026-10-04-thin-docs-v2);`,
1160
1165
  ` 用户否掉方案 → flow reject --change ${change} --reason <理由>(拒后批准须显式通道再确认)。`,
1161
1166
  ` ② 执行断点:写完代码跑完测试后,把测试结果(过了几个/挂了什么)给用户看,`,
1162
- ` 等用户确认再跑收口。`,
1167
+ ` 等用户确认再跑收口;汇报顺带对账执行期用户决策(decisions 条目列一下,无则说明无)。`,
1163
1168
  ` ③ 归档断点:flow done 跑完后(无论过/拒),把结果(归档成功/被什么拦了)给用户看。`,
1164
- ` 用户明确说「直接跑完/不用问我」→ 重跑 flow start --change ${change} --autopilot 声明豁免(留痕),`,
1165
- ` ②③断点 agent 自主跳过。`,
1169
+ ` 用户明确说「直接跑完/不用问我」→ 重跑 flow start --change ${change} --autopilot 声明豁免(用户显式授权、留痕——非 agent 自选补救通道),`,
1170
+ ` ①②③人审全免(agent 自主跑到底)。`,
1166
1171
  ` 随时可查进度:sillyspec flow status --change ${change}`,
1167
1172
  ...(detectUiTouch(input)
1168
1173
  ? [...buildUiGuidanceLines(specBase), '']
@@ -1211,11 +1216,14 @@ function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot,
1211
1216
  ...(title ? [`- 标题:${title}`] : []),
1212
1217
  `- 做到哪:任务勾选 ${checked}/${total};基线以来提交 ${commits} 个;变更/dirty 文件 ${dirty} 个;P2 质量扫描记录 ${hasLedger ? '在场' : '无'}。`,
1213
1218
  `- 六子步标记:${done.length ? `已完成 ${done.join('/')}` : '(无)'}${left.length ? `;待办 ${left.join('/')}` : ';全部完成'}`,
1219
+ // 顺序正典(2026-10-19-thin-order-canon):恢复面带批准状态——重入时 agent 只看简报,
1220
+ // 未批准时「下一步」必须指向补批而非「继续干活」(防先码后批在恢复路径复发)。
1221
+ `- spec 批准:${st.spec_approved ? `✅ 已批准(channel=${approveChannelOf(st)})` : st.autopilot ? '🔑 autopilot 豁免(用户显式授权)' : '⬜ 未批准——动码前先过断点①(摘要给用户 → flow approve)'}`,
1214
1222
  // 红线重申(2026-10-11-flow-start-redline-frontload):重入不出首进横幅——钉死规则与
1215
1223
  // task-NN token 形态随简报补位,断点续跑生命周期内规则不丢。
1216
1224
  `- 红线重申:design 四问文本/FR 标题锚逐字钉死勿改写(改写即拒收);勾选证据提交消息逐个带 task-NN 或 /、,连写(「task-01..task-05」区间写法不认)。`,
1217
1225
  `- 剩什么:${left.length === 0 && dirty === 0 ? '活已干完' : dirty > 0 ? '活未干完(继续改代码)' : '收尾待裁决'}`,
1218
- `- 下一步:${left.length === 0 ? `sillyspec flow done --change ${change}` : `继续干活;干完跑 flow done(断点续)`}`,
1226
+ `- 下一步:${left.length === 0 ? `sillyspec flow done --change ${change}` : !st.spec_approved && !st.autopilot ? `未批准先补批(摘要给用户 → sillyspec flow approve --change ${change});随后继续干活,干完跑 flow done(断点续)` : `继续干活;干完跑 flow done(断点续)`}`,
1219
1227
  ...(digestLines.length > 0 ? ['', ...digestLines] : []),
1220
1228
  // 尾部原型锚(2026-10-14-guide-anchor-adopt-proto):收编/含原型变更重入时原型指针不隐身
1221
1229
  //(首进 adopt 输出的 🖼️ 行在会话切换后已丢,简报尾部补可发现面;置于 digestLines 后=绝对
@@ -1332,10 +1340,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1332
1340
  // spec 断点机器门(FR-06/FR-07,护栏#2 零 prompt 劝说——advisory 断点在 0/12 勾选类事故
1333
1341
  // 实证下无牙;v2 起批准是收口硬前提,autopilot 是用户显式豁免通道)
1334
1342
  if (!st.autopilot && !st.spec_approved) {
1335
- console.error(`❌ spec 断点未批准:本变更(v2 起草)收口需要方案确认留痕`)
1336
- console.error(` 修复:把 FR 条目+design 四节摘要给用户看,用户确认后由用户运行:`)
1343
+ console.error(`❌ spec 断点未批准:批准是动码前置义务(不是收口补救动作)——本变更缺方案确认留痕`)
1344
+ console.error(` 补批通道(留痕可审计):把 FR 条目+design 四节摘要给用户看,用户确认后由用户运行:`)
1337
1345
  console.error(` sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
1338
- console.error(` 或用户显式豁免(自主跑到底):重跑 flow start --change ${change} --autopilot(声明留痕)`)
1346
+ console.error(` 或用户显式豁免(须用户授权,非 agent 自选):重跑 flow start --change ${change} --autopilot(声明留痕)`)
1339
1347
  reportMidFail('artifacts')
1340
1348
  process.exit(1)
1341
1349
  }
@@ -1712,6 +1720,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1712
1720
  // untracked 自拼 hunk),基=baseline,时点=done 冻结。fail-soft:失败只告警不阻断归档、
1713
1721
  // 不标 done(重入 done 重试)。
1714
1722
  let driftRefreeze = false
1723
+ // JSON-only 冻结形态(无主仓 change.patch,跨仓正文在 repos[])——收口指引文案按它
1724
+ // 切换审计锚表述(2026-10-19-thin-empty-face-cross-repo-freeze)。
1725
+ let jsonOnlyFreeze = false
1715
1726
  if (st.substeps?.patch === 'done') {
1716
1727
  // 处置漂移检测(2026-10-05-disposition-refreeze-drift,主清单 P1):冻结后又有本变更交付
1717
1728
  // 提交时,幂等跳过会把 change.patch/review.json 停在处置前时点(归档审计件缺处置面,
@@ -1725,11 +1736,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1725
1736
  // 当前 HEAD);review 处置沿用漂移面同一隔离规则——缺基面无法判 governance-only,
1726
1737
  // 未锚定一律隔离(fail-safe 宁可多评)。
1727
1738
  const patchJsonPath = join(changeDir, 'change-patch.json')
1728
- const patchTextPath = join(changeDir, 'change.patch')
1729
- const missingFreeze = [
1730
- !existsSync(patchJsonPath) ? 'change-patch.json' : null,
1731
- !existsSync(patchTextPath) ? 'change.patch' : null,
1732
- ].filter(Boolean)
1739
+ // 空面跨仓冻结(2026-10-19-thin-empty-face-cross-repo-freeze):JSON-only 留档
1740
+ // (无 change.patch、repos[] 承载跨仓正文)是完整冻结,存在性复查不得判缺件。
1741
+ // 完整性以探针的 complete 为准(坏 JSON 即使 change.patch 在场也不可信)。
1742
+ const { missingFreezeLabels } = await import('./flow-parity.js')
1743
+ const missingFreeze = missingFreezeLabels(changeDir)
1733
1744
  let drifted = false
1734
1745
  let drift = null
1735
1746
  if (missingFreeze.length > 0) {
@@ -1910,6 +1921,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1910
1921
  console.error(` ① git commit 这些文件(信息尾缀带 (${change}) 或 (${change}),重跑本命令——冻结面自动并入`)
1911
1922
  console.error(` ② 全部归属本变更:flow done --change ${change} --freeze-dirty`)
1912
1923
  console.error(` ③ 接受审计缺口:flow done --change ${change} --accept-dirty-gap`)
1924
+ console.error(` 💡 快速通道:脏文件全属并行会话在途/运行残渣且不在本变更交付面时,直接重跑带 --accept-dirty-gap(显式 pathspec 提交面已隔离归属;属本变更的文件必须走①提交)`)
1913
1925
  reportMidFail('patch')
1914
1926
  process.exit(1)
1915
1927
  }
@@ -2020,11 +2032,119 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2020
2032
  }
2021
2033
  console.warn(`⚠️ --accept-empty-face:显式签收空冻结面与声明面矛盾(${_declared.length} 条声明交付不进冻结面,留痕 flow-state accept_empty_face)`)
2022
2034
  }
2023
- // 纯跨仓规范标注空面:合规书写不拦,advisory 点名 thin 空面分支不做跨仓冻结的已知缺口
2024
- if (ownFiles.length === 0 && _declared.length === 0 && planEntries.some((e) => e && e.repo)) {
2025
- console.warn('⚠️ 声明清单全部为跨仓规范标注且主仓冻结面为空——thin 空面分支不做跨仓冻结与 patch 留档(缺口已知:归档件将无 patch 两件;如需冻结请主仓侧交付或待后续版本支持空面跨仓冻结)')
2035
+ // ── 未标注跨仓交付窗口探测(2026-10-19-undeclared-cross-repo-probe)──
2036
+ // 生产实证(消防工作区 4e9eb05b,2026-10-12..10-19 共 14 例):design 无「文件
2037
+ // 变更清单」段、或清单裸写仓目录前缀时,主仓冻结面为空或只剩附件/治理模板,
2038
+ // 真实代码在注册跨仓窗口内却零告警。既有空面门只拦「清单有未标注条目」形态;
2039
+ // 本探测以真实跨仓 diff 为证据补位:无 repo 标注 + 注册仓非空 → 逐仓看
2040
+ // cross_baselines..工作树窗口,有交付改动才点名(advisory 不阻断)。纯文档变更/
2041
+ // 单仓工作区/窗口零改动 → 零输出。未提交面可能含并行会话改动,故文案用「疑似」。
2042
+ if (!planEntries.some((e) => e && e.repo)) {
2043
+ try {
2044
+ const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
2045
+ const { collectRepoActual } = await import('./cross-repo-reconcile.js')
2046
+ const { filterDeliverableFiles } = await import('./worktree-apply.js')
2047
+ const { isToolGovernancePath } = await import('./flow-parity.js')
2048
+ const registeredKeys = loadRegisteredRepoKeys(specBase)
2049
+ const probeHits = []
2050
+ for (const repoKey of registeredKeys) {
2051
+ try {
2052
+ const actual = collectRepoActual({
2053
+ repoKey, specBase, cwd,
2054
+ runtimeRoot: resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase),
2055
+ changeName: change,
2056
+ crossBaselines: (st && st.cross_baselines && typeof st.cross_baselines === 'object') ? st.cross_baselines : null,
2057
+ })
2058
+ if (!actual || actual.degradedReason) continue
2059
+ const files = filterDeliverableFiles(actual.files || [])
2060
+ .filter((f) => !isToolGovernancePath(f))
2061
+ if (files.length > 0) probeHits.push({ key: repoKey, count: files.length })
2062
+ } catch { /* 单仓探测失败不阻断(fail-soft) */ }
2063
+ }
2064
+ if (probeHits.length > 0) {
2065
+ console.warn(`⚠️ 注册跨仓窗口内有交付改动,但 design 清单未按规范标注(主仓冻结面为空/仅治理件时归档 patch 将缺失这些交付面):${probeHits.map((h) => `${h.key}(${h.count} 文件)`).join('、')}`)
2066
+ console.warn(' 疑似跨仓交付(未提交面可能含并行会话改动,请人工确认归属)。正确写法三选一:① 段头「## <注册key> 仓变更」段内写仓根相对路径;② 子段「### <注册key>」;③ 条目前缀 cross-repo:<注册key>:<仓根相对路径>——key 以 local.yaml repos 注册为准')
2067
+ }
2068
+ } catch { /* 探测整体失败不阻断(fail-soft 不新造阻断面) */ }
2026
2069
  }
2027
- if (ownFiles.length === 0) {
2070
+ // 空面跨仓冻结(2026-10-19-thin-empty-face-cross-repo-freeze):主仓冻结面为空且
2071
+ // 声明清单含规范跨仓标注时,跨仓交付面仍可留档——JSON 路线只写 change-patch.json,
2072
+ // scopeAudit.repos[] 承载各仓 patch 正文/sha256,不写 change.patch(主仓面确为空,
2073
+ // 混入跨仓正文会让按主仓 cwd 解析 hunk 的读方错位)。各仓窗口内无可归属交付时
2074
+ // 维持空面跳过(无清单段形态同此路,留给第 2 步的窗口探测点名)。
2075
+ let emptyFaceCrossRepos = []
2076
+ let emptyFaceCrossRows = undefined
2077
+ if (ownFiles.length === 0 && planEntries.some((e) => e && e.repo)) {
2078
+ try {
2079
+ const { reconcileCrossRepoPlan } = await import('./scope-audit.js')
2080
+ const recon = reconcileCrossRepoPlan({
2081
+ cwd, specBase,
2082
+ runtimeRoot: resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase),
2083
+ changeName: change,
2084
+ planEntries,
2085
+ collectPatch: true,
2086
+ crossBaselines: (st && st.cross_baselines && typeof st.cross_baselines === 'object') ? st.cross_baselines : null,
2087
+ })
2088
+ emptyFaceCrossRows = recon.rows
2089
+ // 「有交付」判定按对账三态行,不按 totals.files——后者含声明未动的 untouched
2090
+ // 占位行(空仓也会得到 files>0,导致空面误落 JSON-only 留档)。判定只决定
2091
+ // 落不落件;落件时 repos[] 原样携带全部仓(含降级仓与 degradedReason——
2092
+ // 与非空主仓分支同口径,读侧可感知采集失败面)。
2093
+ const deliveredRepoKeys = new Set((Array.isArray(recon.rows) ? recon.rows : [])
2094
+ .filter((r) => r && r.crossRepo && r.verdict !== 'untouched')
2095
+ .map((r) => r.crossRepo))
2096
+ const hasDelivered = deliveredRepoKeys.size > 0
2097
+ || (recon.repos || []).some((r) => r && typeof r.patch === 'string' && r.patch.length > 0)
2098
+ emptyFaceCrossRepos = hasDelivered ? (recon.repos || []) : []
2099
+ for (const n of recon.notes) console.log(` 📎 ${n}`)
2100
+ } catch { /* 跨仓对账失败 → 退回空面跳过(fail-soft 不新造阻断面) */ }
2101
+ }
2102
+ if (ownFiles.length === 0 && emptyFaceCrossRepos.length > 0) {
2103
+ const { buildThinSnapshotRows, writeCloseTraceArtifacts } = await import('./flow-parity.js')
2104
+ const { rows: snapRows, totals: snapTotals } = buildThinSnapshotRows({ ownFiles: [], stats: new Map(), planEntries, crossRepoRows: emptyFaceCrossRows })
2105
+ const headCommit = String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null
2106
+ // 先清旧 change.patch 再写 JSON:JSON-only 语义下残留旧正文与新 JSON(无顶级
2107
+ // patchSha256)互相矛盾。清理失败直接抛出(外层 catch → patch 子步不标 done,
2108
+ // 重入重试),不留「双件并存且复查判完整」的自愈盲区。
2109
+ const stalePatchPath = join(changeDir, 'change.patch')
2110
+ if (existsSync(stalePatchPath)) {
2111
+ try { unlinkSync(stalePatchPath) } catch (e) {
2112
+ throw new Error(`旧 change.patch 清理失败(${e.message})——保持半态可重入,修复文件权限后重跑`)
2113
+ }
2114
+ }
2115
+ const closeTrace = writeCloseTraceArtifacts({
2116
+ changeDir,
2117
+ change,
2118
+ baseline: st.baseline_commit,
2119
+ head: headCommit,
2120
+ files: [],
2121
+ metaTotals: { files: 0, additions: 0, deletions: 0 },
2122
+ patchText: null,
2123
+ savedAt: new Date().toISOString(),
2124
+ patchStatus: 'ok',
2125
+ snapObj: {
2126
+ mode: 'full-flow', ok: true, degradedReason: null,
2127
+ baseAnchor: st.baseline_commit || null,
2128
+ totals: snapTotals,
2129
+ rows: snapRows,
2130
+ excluded: { foreignDeclared: [] },
2131
+ note: 'flow done 时点冻结(主仓冻结面为空;跨仓交付面见 repos[])',
2132
+ closedBy: 'flow done',
2133
+ repos: emptyFaceCrossRepos,
2134
+ },
2135
+ meta: {
2136
+ note: 'flow done 时点冻结(主仓冻结面为空;跨仓交付面见 scopeAudit.repos[]——2026-10-19-thin-empty-face-cross-repo-freeze)',
2137
+ modules: [], uncoveredDirs: [], moduleMaps: [],
2138
+ },
2139
+ })
2140
+ jsonOnlyFreeze = true
2141
+ const frozenRepos = emptyFaceCrossRepos.filter((r) => r && typeof r.patch === 'string' && r.patch.length > 0).length
2142
+ console.log(`📦 空面跨仓 patch 留档:change-patch.json(主仓面为空,跨仓 ${emptyFaceCrossRepos.length} 仓、正文 ${frozenRepos}——diff 见 scopeAudit.repos[])`)
2143
+ patchOk = closeTrace.patchStatus === 'ok'
2144
+ } else if (ownFiles.length === 0) {
2145
+ if (_declared.length === 0 && planEntries.some((e) => e && e.repo)) {
2146
+ console.warn('⚠️ 声明清单全部为跨仓规范标注且主仓冻结面为空——跨仓窗口内没有可归属交付,按空面跳过 patch 留档(如实现应在此,先提交后重跑)')
2147
+ }
2028
2148
  console.log('📦 patch 留档跳过:本变更可归属文件面为空(无交付面改动——治理工件不入冻结面;若实现代码应在此,先提交后重跑)')
2029
2149
  patchOk = true
2030
2150
  } else {
@@ -2178,6 +2298,13 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2178
2298
  }
2179
2299
  if (patchOk) mark('patch')
2180
2300
  }
2301
+ // 冻结形态复核(含重入 skip 路径——上轮评审 P2:断点续跑回合不执行 patch 分支,
2302
+ // jsonOnlyFreeze 会停在 false,收口指引误报 change.patch)。直接按在场冻结件判定。
2303
+ try {
2304
+ const { inspectFreezeArtifacts: inspectFinal } = await import('./flow-parity.js')
2305
+ const fsFinal = inspectFinal(changeDir)
2306
+ jsonOnlyFreeze = fsFinal.jsonOk && !fsFinal.hasText && fsFinal.repoPatchCount > 0
2307
+ } catch { /* best-effort */ }
2181
2308
 
2182
2309
  // ②c review:危险证据定档的独立评审(2026-09-25-thin-review-slice)。定档=证据累积制
2183
2310
  // (承诺词一票/盲维实质作答/diff 原语/决策密度/声明一票),文件数出局;缺省要评审、豁免要
@@ -2209,6 +2336,20 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2209
2336
  const { classifyReviewNeed, renderReviewerTaskbook, validateReviewJson } = await import('./flow-review.js')
2210
2337
  let patchText = null
2211
2338
  try { patchText = readFileSync(join(changeDir, 'change.patch'), 'utf8') } catch { /* patch 子步 skip/失败面 */ }
2339
+ // JSON-only 跨仓冻结(2026-10-19-thin-empty-face-cross-repo-freeze):无主仓
2340
+ // change.patch 时交付 diff 证据在 change-patch.json 的 scopeAudit.repos[].patch——
2341
+ // 不接上的话 classifyReviewNeed 恒判「交付 diff 不可得」,评审定档失真。
2342
+ if (patchText === null) {
2343
+ try {
2344
+ const meta = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
2345
+ const repos = meta && meta.scopeAudit && Array.isArray(meta.scopeAudit.repos) ? meta.scopeAudit.repos : []
2346
+ const repoDiff = repos
2347
+ .filter((r) => r && typeof r.patch === 'string' && r.patch.length > 0)
2348
+ .map((r) => r.patch)
2349
+ .join('\n')
2350
+ if (repoDiff) patchText = repoDiff
2351
+ } catch { /* 无/坏 JSON → 保持 null(既有判定) */ }
2352
+ }
2212
2353
  const tier = classifyReviewNeed({ changeDir, input: null, patchText, editRatio: st.edit_ratio ?? null, reviewForce: st.review_force ?? null, change })
2213
2354
  if (!tier.required) {
2214
2355
  console.log(`⚖️ 评审豁免(低风险证据齐全):${tier.exemptEvidence.join(';')}`)
@@ -2220,7 +2361,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2220
2361
  // 不再被漂移重冻结误隔离(实测竞态修复)。rev-parse 失败传 null(任务书退引导形态)。
2221
2362
  let taskbookHead = null
2222
2363
  try { const h = gitQuiet(cwd, ['rev-parse', 'HEAD']); if (h && /^[0-9a-f]{7,40}$/i.test(String(h).trim())) taskbookHead = String(h).trim() } catch { /* best-effort */ }
2223
- console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,起一个干净上下文的子代理执行后重跑本命令:\n`)
2364
+ // 派发纪律(2026-10-19-thin-review-dispatch-verbatim):横幅是主会话派发时最先读到的通道,
2365
+ // 明确「原样复制」——实测手写缩编会丢材料包(厚道 stage-review.js:803 同款指令的薄道对齐)。
2366
+ console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2224
2367
  // 项目评审探针装配(2026-10-12-review-probe-injection):local.yaml 数据驱动的项目特有
2225
2368
  // 必查面注入任务书(未配置零注入;装载告警/帽留痕 best-effort 提示,不阻断渲染)。
2226
2369
  let _probeLines = null
@@ -2230,7 +2373,37 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2230
2373
  for (const _w of _assembled.warnings) console.warn(`⚠️ review_probes:${_w}`)
2231
2374
  if (_assembled.lines.length > 0) _probeLines = _assembled.lines
2232
2375
  } catch { /* 探针面 fail-soft:装配失败零注入 */ }
2233
- console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines }))
2376
+ // 预消化材料包装配(2026-10-19-thin-review-material-pack):机械抽取 FR↔测试绑定表 +
2377
+ // 冻结 patch 文件级摘要注入任务书——砍零上下文评审员的定位类读与大 patch 整吞 prefill;
2378
+ // 摘要只从冻结件本身算(不走活 git,防与评审员手里冻结面漂移);fail-soft 零注入。
2379
+ // 归属切分(2026-10-19-thin-review-dispatch-verbatim):三单源——design 变更清单
2380
+ // (parseFileChangeListDetailed)、他侧归因(splitOwnVsForeignDiffFiles,与 patch 子步
2381
+ // 同源)、冻结面文件集(材料包同源解析)——砍评审员逐文件 git 考古(实测 22 次调用)。
2382
+ // 声明清单缺位/冻结面为空 → attribution=null(该子段零注入,FR 表/摘要不受影响)。
2383
+ let _attribution = null
2384
+ try {
2385
+ const { parseFrozenPatchSummary, buildFreezeAttribution } = await import('./flow-review-pack.js')
2386
+ const _freezeFiles = parseFrozenPatchSummary(patchText).map((f) => f.path)
2387
+ if (_freezeFiles.length > 0) {
2388
+ const { parseFileChangeListDetailed } = await import('./change-list.js')
2389
+ const _declared = parseFileChangeListDetailed(join(changeDir, 'design.md')).map((e) => e.path)
2390
+ if (_declared.length > 0) {
2391
+ const { splitOwnVsForeignDiffFiles } = await import('./foreign-declared.js')
2392
+ const _split = splitOwnVsForeignDiffFiles(cwd, change, _freezeFiles, { specBase })
2393
+ _attribution = buildFreezeAttribution({ declaredFiles: _declared, freezeFiles: _freezeFiles, foreign: _split && _split.foreign })
2394
+ }
2395
+ }
2396
+ } catch { /* 归属面 fail-soft:装配失败零注入该子段 */ }
2397
+ let _packLines = null
2398
+ try {
2399
+ const { assembleReviewPack, findLatestTestReceipt } = await import('./flow-review-pack.js')
2400
+ // ledger 测试回执(2026-10-21-review-rerun-scope):ledger 先于 review(SUBSTEPS 序),
2401
+ // verify-runs 最新本变更 test-result 作回执注入——评审从「自己重跑」变「回执基线抽验」。
2402
+ const _receipt = findLatestTestReceipt(join(specBase, '.runtime', 'verify-runs'), change)
2403
+ const _pack = assembleReviewPack({ changeDir, patchText, attribution: _attribution, testReceipt: _receipt })
2404
+ if (_pack && Array.isArray(_pack.lines) && _pack.lines.length > 0) _packLines = _pack.lines
2405
+ } catch { /* 材料包面 fail-soft:装配失败零注入 */ }
2406
+ console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines, packLines: _packLines }))
2234
2407
  reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
2235
2408
  appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
2236
2409
  reportMidFail('review')
@@ -2362,7 +2535,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2362
2535
  }
2363
2536
  } catch { /* 回读失败 → 空面(回写零命中静默) */ }
2364
2537
  }
2365
- // ④ distill:决策提炼(rejected/needsWait 异态 → 升厚留人工裁决,不静默吞)+ FR 索引提炼
2538
+ // ④ distill:决策提炼(needsWait 异态 → 升厚留人工裁决,不静默吞——判定 isDistillAbnormal
2539
+ // 单一源在 decision-distill.js,2026-10-20-distill-abnormal-upgrade-fix 修复幽灵触发器)+ FR 索引提炼
2366
2540
  // (2026-09-22-thin-fr-distill-sync:轻量变更此前只蒸馏 decisions 不调 indexRequirements——
2367
2541
  // requirements 永不进 knowledge/fr,知识复利在新默认道断流;轻量变更无 design.md,域路由
2368
2542
  // 以基线以来交付 diff 供 deliverableFiles,伪域回退同口径)
@@ -2392,13 +2566,13 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2392
2566
  }
2393
2567
  } catch (e) { console.warn(`⚠️ 变更索引回写失败(fail-soft 不阻断归档):${(e && e.message) || e}`) }
2394
2568
  try {
2395
- const { distillIntoKnowledge } = await import('./decision-distill.js')
2569
+ const { distillIntoKnowledge, isDistillAbnormal } = await import('./decision-distill.js')
2396
2570
  const knowledgeRoot = join(specBase, 'knowledge')
2397
2571
  const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
2398
2572
  const res = distillIntoKnowledge(changeDir, knowledgeRoot, typeof head === 'string' ? head.trim() : '', null)
2399
- const abnormal = res && (res.rejected > 0 || res.needsWait > 0)
2573
+ const abnormal = isDistillAbnormal(res)
2400
2574
  if (abnormal) {
2401
- writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `distill 异态(rejected=${res.rejected} needsWait=${res.needsWait})——升厚留人工裁决` })
2575
+ writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `distill 异态(needsWait=${res.needsWait})——升厚留人工裁决` })
2402
2576
  resetThinStageRowOnUpgrade(cwd, change, specBase)
2403
2577
  console.warn(`⚠️ distill 异态 → 已升厚档(tier=thick)。剩余流程按厚档走 run <stage> 完整仪式;归档子步仍将执行(厚档语义不跳过 plan.md 校验)。`)
2404
2578
  }
@@ -2526,7 +2700,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2526
2700
  const finalHead = gitQuiet(cwd, ['rev-parse', 'HEAD'])
2527
2701
  if (st.baseline_commit && typeof finalHead === 'string' && finalHead.trim() && finalHead.trim() !== st.baseline_commit) {
2528
2702
  console.log(`🧹 Git 历史可自由整理:多轮中间提交可压扁为单提交——git reset --soft ${st.baseline_commit.slice(0, 10)} && git commit -m "<最终提交信息>"`)
2529
- console.log(` 审计真相已冻结在归档件 change.patch(sha256 锚定)+ verify-result + review.json,不依赖 git 历史;`)
2703
+ console.log(` 审计真相已冻结在归档件 ${jsonOnlyFreeze ? 'change-patch.json(跨仓 repos[] 正文+sha256 锚定)' : 'change.patch(sha256 锚定)'} + verify-result + review.json,不依赖 git 历史;`)
2530
2704
  console.log(` 注:knowledge「最近确认」hash 为溯源快照,压扁后成孤儿引用属预期(非活引用)。`)
2531
2705
  }
2532
2706
  } catch { /* 指引 best-effort */ }
@@ -2568,6 +2742,39 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2568
2742
  console.error(` (拒绝后派生授权须显式再确认——2026-10-12-approve-channel-attest)`)
2569
2743
  process.exit(2)
2570
2744
  }
2745
+ // 批准前文档完整性校验(2026-10-19-approve-gate-tick-nudge FR-01/02):批准=有内容的机器
2746
+ // 事实(批时文档必齐)。复用 done 门同源校验(verifyFlowDrafts 双轨);repair:false——批准
2747
+ // 面只读,不做 design 问题行自动回填(写盘修复属 done 面)。诚实边界:本门证明「答在批前」,
2748
+ // 不证明「答在码前」——时序证据路线已实证不可行(批量收口下 git 时间戳说谎,thin-order-canon
2749
+ // 四路调查),不做假装能拦的检查。
2750
+ {
2751
+ const runtimeRoot = resolveRuntimeRoot({}, specBase)
2752
+ const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
2753
+ const adoptedDesign = st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))
2754
+ const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign, repair: false })
2755
+ for (const a of r.advisories || []) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2756
+ if (r.applicable && r.violations.length > 0) {
2757
+ console.error(`❌ spec 断点未批准——文档未齐(批准是动码前置,先答 spec 面再批):`)
2758
+ for (const v of r.violations) console.error(` - ${v}`)
2759
+ console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2760
+ process.exit(1)
2761
+ }
2762
+ if (!r.applicable) {
2763
+ // draft-ledger 机本位缺失(跨机批准/存量变更):fail-open 降级——仅 v1(AGENT 槽形态)跑
2764
+ // 空槽兜底;v2 纯 markdown 无 ledger 无法对锚校验,如实降级放行(不假装能查)。
2765
+ let v1Text = null
2766
+ try { v1Text = readFileSync(join(changeDir, 'design.md'), 'utf8') } catch { /* 无 design */ }
2767
+ if (v1Text && /<!--AGENT:槽\d+/.test(v1Text)) {
2768
+ const dr = verifyDesignRecordFilled({ changeDir })
2769
+ if (dr.applicable && dr.emptySlots.length > 0) {
2770
+ console.error(`❌ spec 断点未批准——design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2771
+ console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2772
+ process.exit(1)
2773
+ }
2774
+ }
2775
+ console.log(`ℹ️ 批准校验降级放行:draft-ledger 不在此机(.runtime 机本位/存量变更)——空槽兜底校验通过`)
2776
+ }
2777
+ }
2571
2778
  let approvedBy = by
2572
2779
  if (!approvedBy) {
2573
2780
  try {
@@ -2597,7 +2804,7 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2597
2804
  console.warn(`⚠️ agent 自批留痕:如用户实际未确认,请撤销——sillyspec flow reject --change ${change} --reason <理由>`)
2598
2805
  }
2599
2806
  if (patch.spec_rejected_superseded_at) console.log(` 已翻越既有拒绝记录(superseded_at=${patch.spec_rejected_superseded_at},拒绝理由保留于 spec_rejected_reason)`)
2600
- console.log(` flow done 的断点机器门已满足;agent 可继续执行/收口。撤销:sillyspec flow reject --change ${change} --reason <理由>`)
2807
+ console.log(` 批准是动码前置义务:若尚未动码,现在可以开始写码;若已动码,本次属超期补批(留痕可审计)。flow done 断点门已满足。撤销:sillyspec flow reject --change ${change} --reason <理由>`)
2601
2808
  }
2602
2809
 
2603
2810
  /**
@@ -2808,17 +3015,10 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2808
3015
  let designFilled = false, frFilled = false, bindingsFilled = 0, bindingsTotal = 0
2809
3016
  try {
2810
3017
  const dText = readFileSync(join(changeDir, 'design.md'), 'utf8')
2811
- // 双格式:v1 AGENT 槽计数;v2 纯 markdown 按节「有实质作答」判(复用 flow-review 的
2812
- // v2 节读取——问题原文行剥离;仅标题在场不算已填,防 spec 相位误跳「②执行」)
2813
- if (/<!--AGENT:槽\d+/.test(dText)) {
2814
- designFilled = Array.from(dText.matchAll(/<!--AGENT:槽\d+[^\n]*-->\n(\S)/g)).length >= 3
2815
- } else {
2816
- try {
2817
- const { readV2SectionAnswer } = await import('./flow-review.js')
2818
- const { DESIGN_QUESTIONS } = await import('./flow-draft.js')
2819
- designFilled = DESIGN_QUESTIONS.sections.filter((s) => readV2SectionAnswer(dText, s.heading, DESIGN_QUESTIONS)).length >= 3
2820
- } catch { designFilled = false }
2821
- }
3018
+ // 双格式判定单一源(2026-10-19-approve-gate-tick-nudge FR-03 上移 flow-review.js;
3019
+ // 判据与原内联逻辑逐字一致——status/tick 同源,行为零变化)
3020
+ const { designSectionsFilled } = await import('./flow-review.js')
3021
+ designFilled = designSectionsFilled(dText)
2822
3022
  } catch { /* 无 design */ }
2823
3023
  try {
2824
3024
  const rText = readFileSync(join(changeDir, 'requirements.md'), 'utf8')
@@ -2833,9 +3033,18 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2833
3033
  bindingsFilled = _rows.filter((m) => m[2].trim() && !m[2].trim().startsWith('(待填')).length
2834
3034
  }
2835
3035
  } catch { /* 无 requirements */ }
2836
- // 阶段推断
3036
+ // 阶段推断(2026-10-19-thin-order-canon:相位看批准——docs 齐但未批准仍停 ① spec 待批准,
3037
+ // 防 status 中途查询面诱导「先执行后卡点」;autopilot 是用户显式豁免、进执行段带标注)
3038
+ const specApproved = st.spec_approved === true
3039
+ const autopilotOn = st.autopilot === true
2837
3040
  let phase = '① spec(填 FR + design 槽)'
2838
- if (designFilled && frFilled && bindingsFilled >= bindingsTotal && bindingsTotal > 0) phase = '② 执行(写代码跑测试)→ ①卡点:spec 摘要给用户确认'
3041
+ if (designFilled && frFilled && bindingsFilled >= bindingsTotal && bindingsTotal > 0) {
3042
+ phase = specApproved
3043
+ ? '② 执行(已批准·写代码跑测试)'
3044
+ : autopilotOn
3045
+ ? '② 执行(autopilot 豁免·写代码跑测试)'
3046
+ : '① spec(→ 待用户批准:摘要给用户后 flow approve)'
3047
+ }
2839
3048
  if (subDone.includes('ledger')) phase = '③ 归档(flow done 收口)'
2840
3049
  // 任务心跳(2026-09-29-flow-task-heartbeat + d007-incontext 纠偏):②执行阶段把「下一个任务」
2841
3050
  // 交给机器指——当场重读 tasks.md(唯一进度源)取第一个未勾行 + 进度。进度推进本身零 CLI
@@ -2876,6 +3085,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2876
3085
  designFilled, frFilled, bindingsFilled, bindingsTotal,
2877
3086
  tasksChecked, tasksTotal,
2878
3087
  substeps: subDone, substepsTotal: SUBSTEPS.length,
3088
+ specApproved, specApprovedChannel: specApproved ? approveChannelOf(st) : null,
2879
3089
  ...(st.legacy_fallback ? { legacyFallback: true } : {}),
2880
3090
  }))
2881
3091
  return
@@ -2884,6 +3094,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2884
3094
  `📋 ${change}`,
2885
3095
  ...(changeTitle ? [` 标题:${changeTitle}`] : []),
2886
3096
  ` 阶段:${phase}`,
3097
+ ` spec 批准:${specApproved ? '✅ 已批准' : autopilotOn ? '🔑 autopilot 豁免(用户显式授权)' : '⬜ 未批准(动码前先过断点①)'}`,
2887
3098
  ` design 槽:${designFilled ? '✅ 已填' : '⬜ 未填'}|FR 区:${frFilled ? '✅ 已填' : '⬜ 未填'}|绑定槽:${bindingsFilled}/${bindingsTotal}`,
2888
3099
  hasTasks ? ` 任务勾选:${tasksChecked}/${tasksTotal}` : null,
2889
3100
  ...(heartbeat || []),
package/src/index.js CHANGED
@@ -394,25 +394,32 @@ async function main() {
394
394
  process.exit(1);
395
395
  }
396
396
 
397
- // ── worktree cwd 硬拦(坑 worktree-cwd-silent-split,2026-08-29 用户实证)──
398
- // Bash cwd 残留让 agent 两次误在 worktree 内跑 sillyspec:status 类完全静默、进度写进
399
- // worktree 副本 .sillyspec 与主仓进度库分裂。守卫在命令分发前拦截,--dir 显式指向主仓
400
- // 或 --allow-worktree-cwd 逃生门放行;wt-commit 命令豁免(子代理 workdir=worktree 内合法)。
397
+ // ── worktree cwd 锚定(坑 worktree-cwd-silent-split 硬拦 → 坑 cli-cwd-coupling 拦→导,
398
+ // 2026-10-20-cli-worktree-cwd-anchor)──
399
+ // Bash cwd 残留让 agent 误在 worktree 内跑 sillyspec(生产实证会话 47e2ff1a 三次被拦、每次
400
+ // 1-2 个交互来回)。守卫建议的事(cd 回主仓根重跑)自动做掉:⚠️警告 + 重锚定主仓根执行——
401
+ // 写入恒落主仓(单源不变量延续,worktree 副本零 CLI 写入);刻意以本 worktree 作独立实例跑
402
+ // 的显式路径(--allow-worktree-cwd --spec-dir)在警告中原样保留;wt-commit 豁免不变。
401
403
  if (!args.includes('--allow-worktree-cwd') && command !== 'wt-commit') {
402
404
  const guardDir = args.includes('--dir') ? dir : process.cwd();
403
405
  const wtInfo = detectCwdInsideWorktree(guardDir);
404
406
  if (wtInfo) {
405
- console.error(`\n❌ 当前在隔离 worktree 内(${wtInfo.changeName}):${guardDir}`);
406
- console.error(` 主仓根:${wtInfo.mainRepoRoot}`);
407
- console.error(' SillySpec 进度库只存在于主仓,worktree 内跑会产生分裂进度(坑 worktree-cwd-silent-split)。');
408
- console.error(' 修复:cd 到主仓根后重跑,或用 --dir <主仓根>,或用 --allow-worktree-cwd 强制放行。');
409
- // 一键放行整行(R4-S-Q 实证:agent 试 3 次才拼对组合——worktree 守卫要 --allow-worktree-cwd,
410
- // 而 worktree 嵌套在主仓 .sillyspec/.runtime/ 下又触发 monorepo 多实例拦,需再补 --spec-dir
411
- // 指向 worktree 自己的 .sillyspec,两 flag 缺一不可。给整行可贴命令,不再让调用方试错拼装。)
412
407
  const wtSpecDir = (ancestorSpecDirs(guardDir)[0]) || join(guardDir, '.sillyspec');
413
- console.error(` 一键放行(本 worktree 作独立实例跑,整行照贴):`);
414
- console.error(` sillyspec ${args.join(' ')} --allow-worktree-cwd --spec-dir "${wtSpecDir}"`);
415
- process.exit(2);
408
+ console.warn(`\n⚠️ cwd 在隔离 worktree 内(${wtInfo.changeName}):${guardDir}`);
409
+ console.warn(` 已自动锚定主仓根执行:${wtInfo.mainRepoRoot}(进度库/变更状态以主仓单源为准——坑 worktree-cwd-silent-split / cli-cwd-coupling 拦→导)。`);
410
+ console.warn(` 如确需以本 worktree 作独立实例跑(整行照贴):`);
411
+ console.warn(` sillyspec ${args.join(' ')} --allow-worktree-cwd --spec-dir "${wtSpecDir}"`);
412
+ dir = wtInfo.mainRepoRoot;
413
+ // 评审 P2 清偿:显式 --spec-dir 指进同一 worktree(无逃生 flag)时一并锚定——否则警告
414
+ // 「已锚定主仓根」半真,写入仍可落 worktree 副本 .sillyspec(旧硬拦对该组合是全拦的,
415
+ // 锚定语义必须覆盖同款面);刻意独立实例跑走 --allow-worktree-cwd(跳过整个守卫)。
416
+ if (specDir) {
417
+ const sdInfo = detectCwdInsideWorktree(specDir);
418
+ if (sdInfo && sdInfo.worktreeRoot === wtInfo.worktreeRoot) {
419
+ console.warn(` ⚠️ --spec-dir 指向同一 worktree(${specDir})——已一并锚定主仓 spec 根。`);
420
+ specDir = join(wtInfo.mainRepoRoot, '.sillyspec');
421
+ }
422
+ }
416
423
  }
417
424
  }
418
425
 
package/src/init.js CHANGED
@@ -85,18 +85,8 @@ const TOOL_LABELS = {
85
85
  cursor: 'Cursor',
86
86
  openclaw: 'OpenClaw',
87
87
  codex: 'OpenAI Codex (通过 AGENTS.md)',
88
- gemini: 'Gemini CLI (通过 GEMINI.md)',
89
- opencode: 'OpenCode (通过 INSTRUCTIONS.md)',
90
- };
91
-
92
- // 指令文件工具(gemini/opencode)。codex/claude/zcode 共用 AGENTS.md(跨工具通用标准内容源);
93
- // gemini/opencode 写各自指令文件,内容与 AGENTS.md 同源同全文(改 @AGENTS.md 指针需先验证
94
- // 两家对 @ 导入语法的支持,留待后续变更)。
95
- const INSTRUCTION_TOOLS = ['gemini', 'opencode'];
96
-
97
- const INSTRUCTION_FILE_MAP = {
98
- gemini: 'GEMINI.md',
99
- opencode: 'INSTRUCTIONS.md',
88
+ gemini: 'Gemini CLI',
89
+ opencode: 'OpenCode',
100
90
  };
101
91
 
102
92
  // ── 注入指令文件(全量方案,2026-10-10-init-full-injection:完整模板是唯一注入源)──
@@ -145,7 +135,7 @@ function stripLegacySmallBlock(content) {
145
135
 
146
136
  /**
147
137
  * 向指引文件注入完整模板(版本感知幂等,三态四分支 + 旧标记迁移)。
148
- * AGENTS.md / GEMINI.md / INSTRUCTIONS.md 共用;完整模板(templates/agents-instruction.md)
138
+ * AGENTS.md 专用(GEMINI.md/INSTRUCTIONS.md 专属注入面 2026-10-19 下线);完整模板(templates/agents-instruction.md)
149
139
  * 是唯一注入源。取代 design: 2026-08-02-init-claude-md 的 CLAUDE.md 单文件方案与
150
140
  * v≤3.32.3 的追加态小段方案(其 D-004 预留的「marker 方案迁移」即彼方案)。
151
141
  * - 不存在:写完整模板 + 顶部版本注释
@@ -210,12 +200,6 @@ export function injectAgentsInstructions(projectDir) {
210
200
  injectFullInstructions(join(projectDir, 'AGENTS.md'));
211
201
  }
212
202
 
213
- export function injectInstructions(tool, projectDir) {
214
- const fileName = INSTRUCTION_FILE_MAP[tool];
215
- if (!fileName) return;
216
- injectFullInstructions(join(projectDir, fileName));
217
- }
218
-
219
203
  /**
220
204
  * 为 Claude Code 写 CLAUDE.md 指针(@AGENTS.md 导入,薄文件)。
221
205
  * - 不存在:写指针文件(版本头注释 + @AGENTS.md)
@@ -438,17 +422,10 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null, op
438
422
  }
439
423
  }
440
424
 
441
- // 注入指令文件(gemini/opencode:与 AGENTS.md 同源同全文,共用全量注入器)
442
- for (let i = 0; i < tools.length; i++) {
443
- const toolName = tools[i];
444
- if (INSTRUCTION_TOOLS.includes(toolName)) {
445
- injectInstructions(toolName, projectDir);
446
- }
447
- }
448
-
449
- // 注入 AGENTS.md 完整指引(claude/codex/zcode 共用:版本感知幂等三态四分支 + 旧 ## SillySpec 小段迁移;
450
- // zcode 2026-09-21-flow-command-cards FR-05 入面——AGENTS.md 是跨工具通用标准内容源)
451
- if (tools.includes('claude') || tools.includes('codex') || tools.includes('zcode')) {
425
+ // 注入 AGENTS.md 完整指引(版本感知幂等三态四分支 + 旧 ## SillySpec 小段迁移)。
426
+ // 2026-10-19 起全工具统一落 AGENTS.md:gemini/opencode 不再产 GEMINI.md/INSTRUCTIONS.md
427
+ // (OpenCode 原生读 AGENTS.md;Gemini CLI 现默认 GEMINI.md,需自行引用 AGENTS.md,边界见 file-lifecycle)
428
+ if (tools.some((t) => ['claude', 'codex', 'zcode', 'gemini', 'opencode'].includes(t))) {
452
429
  injectAgentsInstructions(projectDir);
453
430
  }
454
431