sillyspec 3.33.1 → 3.33.2

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) {
@@ -2020,11 +2031,119 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2020
2031
  }
2021
2032
  console.warn(`⚠️ --accept-empty-face:显式签收空冻结面与声明面矛盾(${_declared.length} 条声明交付不进冻结面,留痕 flow-state accept_empty_face)`)
2022
2033
  }
2023
- // 纯跨仓规范标注空面:合规书写不拦,advisory 点名 thin 空面分支不做跨仓冻结的已知缺口
2024
- if (ownFiles.length === 0 && _declared.length === 0 && planEntries.some((e) => e && e.repo)) {
2025
- console.warn('⚠️ 声明清单全部为跨仓规范标注且主仓冻结面为空——thin 空面分支不做跨仓冻结与 patch 留档(缺口已知:归档件将无 patch 两件;如需冻结请主仓侧交付或待后续版本支持空面跨仓冻结)')
2034
+ // ── 未标注跨仓交付窗口探测(2026-10-19-undeclared-cross-repo-probe)──
2035
+ // 生产实证(消防工作区 4e9eb05b,2026-10-12..10-19 共 14 例):design 无「文件
2036
+ // 变更清单」段、或清单裸写仓目录前缀时,主仓冻结面为空或只剩附件/治理模板,
2037
+ // 真实代码在注册跨仓窗口内却零告警。既有空面门只拦「清单有未标注条目」形态;
2038
+ // 本探测以真实跨仓 diff 为证据补位:无 repo 标注 + 注册仓非空 → 逐仓看
2039
+ // cross_baselines..工作树窗口,有交付改动才点名(advisory 不阻断)。纯文档变更/
2040
+ // 单仓工作区/窗口零改动 → 零输出。未提交面可能含并行会话改动,故文案用「疑似」。
2041
+ if (!planEntries.some((e) => e && e.repo)) {
2042
+ try {
2043
+ const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
2044
+ const { collectRepoActual } = await import('./cross-repo-reconcile.js')
2045
+ const { filterDeliverableFiles } = await import('./worktree-apply.js')
2046
+ const { isToolGovernancePath } = await import('./flow-parity.js')
2047
+ const registeredKeys = loadRegisteredRepoKeys(specBase)
2048
+ const probeHits = []
2049
+ for (const repoKey of registeredKeys) {
2050
+ try {
2051
+ const actual = collectRepoActual({
2052
+ repoKey, specBase, cwd,
2053
+ runtimeRoot: resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase),
2054
+ changeName: change,
2055
+ crossBaselines: (st && st.cross_baselines && typeof st.cross_baselines === 'object') ? st.cross_baselines : null,
2056
+ })
2057
+ if (!actual || actual.degradedReason) continue
2058
+ const files = filterDeliverableFiles(actual.files || [])
2059
+ .filter((f) => !isToolGovernancePath(f))
2060
+ if (files.length > 0) probeHits.push({ key: repoKey, count: files.length })
2061
+ } catch { /* 单仓探测失败不阻断(fail-soft) */ }
2062
+ }
2063
+ if (probeHits.length > 0) {
2064
+ console.warn(`⚠️ 注册跨仓窗口内有交付改动,但 design 清单未按规范标注(主仓冻结面为空/仅治理件时归档 patch 将缺失这些交付面):${probeHits.map((h) => `${h.key}(${h.count} 文件)`).join('、')}`)
2065
+ console.warn(' 疑似跨仓交付(未提交面可能含并行会话改动,请人工确认归属)。正确写法三选一:① 段头「## <注册key> 仓变更」段内写仓根相对路径;② 子段「### <注册key>」;③ 条目前缀 cross-repo:<注册key>:<仓根相对路径>——key 以 local.yaml repos 注册为准')
2066
+ }
2067
+ } catch { /* 探测整体失败不阻断(fail-soft 不新造阻断面) */ }
2026
2068
  }
2027
- if (ownFiles.length === 0) {
2069
+ // 空面跨仓冻结(2026-10-19-thin-empty-face-cross-repo-freeze):主仓冻结面为空且
2070
+ // 声明清单含规范跨仓标注时,跨仓交付面仍可留档——JSON 路线只写 change-patch.json,
2071
+ // scopeAudit.repos[] 承载各仓 patch 正文/sha256,不写 change.patch(主仓面确为空,
2072
+ // 混入跨仓正文会让按主仓 cwd 解析 hunk 的读方错位)。各仓窗口内无可归属交付时
2073
+ // 维持空面跳过(无清单段形态同此路,留给第 2 步的窗口探测点名)。
2074
+ let emptyFaceCrossRepos = []
2075
+ let emptyFaceCrossRows = undefined
2076
+ if (ownFiles.length === 0 && planEntries.some((e) => e && e.repo)) {
2077
+ try {
2078
+ const { reconcileCrossRepoPlan } = await import('./scope-audit.js')
2079
+ const recon = reconcileCrossRepoPlan({
2080
+ cwd, specBase,
2081
+ runtimeRoot: resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase),
2082
+ changeName: change,
2083
+ planEntries,
2084
+ collectPatch: true,
2085
+ crossBaselines: (st && st.cross_baselines && typeof st.cross_baselines === 'object') ? st.cross_baselines : null,
2086
+ })
2087
+ emptyFaceCrossRows = recon.rows
2088
+ // 「有交付」判定按对账三态行,不按 totals.files——后者含声明未动的 untouched
2089
+ // 占位行(空仓也会得到 files>0,导致空面误落 JSON-only 留档)。判定只决定
2090
+ // 落不落件;落件时 repos[] 原样携带全部仓(含降级仓与 degradedReason——
2091
+ // 与非空主仓分支同口径,读侧可感知采集失败面)。
2092
+ const deliveredRepoKeys = new Set((Array.isArray(recon.rows) ? recon.rows : [])
2093
+ .filter((r) => r && r.crossRepo && r.verdict !== 'untouched')
2094
+ .map((r) => r.crossRepo))
2095
+ const hasDelivered = deliveredRepoKeys.size > 0
2096
+ || (recon.repos || []).some((r) => r && typeof r.patch === 'string' && r.patch.length > 0)
2097
+ emptyFaceCrossRepos = hasDelivered ? (recon.repos || []) : []
2098
+ for (const n of recon.notes) console.log(` 📎 ${n}`)
2099
+ } catch { /* 跨仓对账失败 → 退回空面跳过(fail-soft 不新造阻断面) */ }
2100
+ }
2101
+ if (ownFiles.length === 0 && emptyFaceCrossRepos.length > 0) {
2102
+ const { buildThinSnapshotRows, writeCloseTraceArtifacts } = await import('./flow-parity.js')
2103
+ const { rows: snapRows, totals: snapTotals } = buildThinSnapshotRows({ ownFiles: [], stats: new Map(), planEntries, crossRepoRows: emptyFaceCrossRows })
2104
+ const headCommit = String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null
2105
+ // 先清旧 change.patch 再写 JSON:JSON-only 语义下残留旧正文与新 JSON(无顶级
2106
+ // patchSha256)互相矛盾。清理失败直接抛出(外层 catch → patch 子步不标 done,
2107
+ // 重入重试),不留「双件并存且复查判完整」的自愈盲区。
2108
+ const stalePatchPath = join(changeDir, 'change.patch')
2109
+ if (existsSync(stalePatchPath)) {
2110
+ try { unlinkSync(stalePatchPath) } catch (e) {
2111
+ throw new Error(`旧 change.patch 清理失败(${e.message})——保持半态可重入,修复文件权限后重跑`)
2112
+ }
2113
+ }
2114
+ const closeTrace = writeCloseTraceArtifacts({
2115
+ changeDir,
2116
+ change,
2117
+ baseline: st.baseline_commit,
2118
+ head: headCommit,
2119
+ files: [],
2120
+ metaTotals: { files: 0, additions: 0, deletions: 0 },
2121
+ patchText: null,
2122
+ savedAt: new Date().toISOString(),
2123
+ patchStatus: 'ok',
2124
+ snapObj: {
2125
+ mode: 'full-flow', ok: true, degradedReason: null,
2126
+ baseAnchor: st.baseline_commit || null,
2127
+ totals: snapTotals,
2128
+ rows: snapRows,
2129
+ excluded: { foreignDeclared: [] },
2130
+ note: 'flow done 时点冻结(主仓冻结面为空;跨仓交付面见 repos[])',
2131
+ closedBy: 'flow done',
2132
+ repos: emptyFaceCrossRepos,
2133
+ },
2134
+ meta: {
2135
+ note: 'flow done 时点冻结(主仓冻结面为空;跨仓交付面见 scopeAudit.repos[]——2026-10-19-thin-empty-face-cross-repo-freeze)',
2136
+ modules: [], uncoveredDirs: [], moduleMaps: [],
2137
+ },
2138
+ })
2139
+ jsonOnlyFreeze = true
2140
+ const frozenRepos = emptyFaceCrossRepos.filter((r) => r && typeof r.patch === 'string' && r.patch.length > 0).length
2141
+ console.log(`📦 空面跨仓 patch 留档:change-patch.json(主仓面为空,跨仓 ${emptyFaceCrossRepos.length} 仓、正文 ${frozenRepos}——diff 见 scopeAudit.repos[])`)
2142
+ patchOk = closeTrace.patchStatus === 'ok'
2143
+ } else if (ownFiles.length === 0) {
2144
+ if (_declared.length === 0 && planEntries.some((e) => e && e.repo)) {
2145
+ console.warn('⚠️ 声明清单全部为跨仓规范标注且主仓冻结面为空——跨仓窗口内没有可归属交付,按空面跳过 patch 留档(如实现应在此,先提交后重跑)')
2146
+ }
2028
2147
  console.log('📦 patch 留档跳过:本变更可归属文件面为空(无交付面改动——治理工件不入冻结面;若实现代码应在此,先提交后重跑)')
2029
2148
  patchOk = true
2030
2149
  } else {
@@ -2178,6 +2297,13 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2178
2297
  }
2179
2298
  if (patchOk) mark('patch')
2180
2299
  }
2300
+ // 冻结形态复核(含重入 skip 路径——上轮评审 P2:断点续跑回合不执行 patch 分支,
2301
+ // jsonOnlyFreeze 会停在 false,收口指引误报 change.patch)。直接按在场冻结件判定。
2302
+ try {
2303
+ const { inspectFreezeArtifacts: inspectFinal } = await import('./flow-parity.js')
2304
+ const fsFinal = inspectFinal(changeDir)
2305
+ jsonOnlyFreeze = fsFinal.jsonOk && !fsFinal.hasText && fsFinal.repoPatchCount > 0
2306
+ } catch { /* best-effort */ }
2181
2307
 
2182
2308
  // ②c review:危险证据定档的独立评审(2026-09-25-thin-review-slice)。定档=证据累积制
2183
2309
  // (承诺词一票/盲维实质作答/diff 原语/决策密度/声明一票),文件数出局;缺省要评审、豁免要
@@ -2209,6 +2335,20 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2209
2335
  const { classifyReviewNeed, renderReviewerTaskbook, validateReviewJson } = await import('./flow-review.js')
2210
2336
  let patchText = null
2211
2337
  try { patchText = readFileSync(join(changeDir, 'change.patch'), 'utf8') } catch { /* patch 子步 skip/失败面 */ }
2338
+ // JSON-only 跨仓冻结(2026-10-19-thin-empty-face-cross-repo-freeze):无主仓
2339
+ // change.patch 时交付 diff 证据在 change-patch.json 的 scopeAudit.repos[].patch——
2340
+ // 不接上的话 classifyReviewNeed 恒判「交付 diff 不可得」,评审定档失真。
2341
+ if (patchText === null) {
2342
+ try {
2343
+ const meta = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
2344
+ const repos = meta && meta.scopeAudit && Array.isArray(meta.scopeAudit.repos) ? meta.scopeAudit.repos : []
2345
+ const repoDiff = repos
2346
+ .filter((r) => r && typeof r.patch === 'string' && r.patch.length > 0)
2347
+ .map((r) => r.patch)
2348
+ .join('\n')
2349
+ if (repoDiff) patchText = repoDiff
2350
+ } catch { /* 无/坏 JSON → 保持 null(既有判定) */ }
2351
+ }
2212
2352
  const tier = classifyReviewNeed({ changeDir, input: null, patchText, editRatio: st.edit_ratio ?? null, reviewForce: st.review_force ?? null, change })
2213
2353
  if (!tier.required) {
2214
2354
  console.log(`⚖️ 评审豁免(低风险证据齐全):${tier.exemptEvidence.join(';')}`)
@@ -2220,7 +2360,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2220
2360
  // 不再被漂移重冻结误隔离(实测竞态修复)。rev-parse 失败传 null(任务书退引导形态)。
2221
2361
  let taskbookHead = null
2222
2362
  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`)
2363
+ // 派发纪律(2026-10-19-thin-review-dispatch-verbatim):横幅是主会话派发时最先读到的通道,
2364
+ // 明确「原样复制」——实测手写缩编会丢材料包(厚道 stage-review.js:803 同款指令的薄道对齐)。
2365
+ console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2224
2366
  // 项目评审探针装配(2026-10-12-review-probe-injection):local.yaml 数据驱动的项目特有
2225
2367
  // 必查面注入任务书(未配置零注入;装载告警/帽留痕 best-effort 提示,不阻断渲染)。
2226
2368
  let _probeLines = null
@@ -2230,7 +2372,34 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2230
2372
  for (const _w of _assembled.warnings) console.warn(`⚠️ review_probes:${_w}`)
2231
2373
  if (_assembled.lines.length > 0) _probeLines = _assembled.lines
2232
2374
  } catch { /* 探针面 fail-soft:装配失败零注入 */ }
2233
- console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines }))
2375
+ // 预消化材料包装配(2026-10-19-thin-review-material-pack):机械抽取 FR↔测试绑定表 +
2376
+ // 冻结 patch 文件级摘要注入任务书——砍零上下文评审员的定位类读与大 patch 整吞 prefill;
2377
+ // 摘要只从冻结件本身算(不走活 git,防与评审员手里冻结面漂移);fail-soft 零注入。
2378
+ // 归属切分(2026-10-19-thin-review-dispatch-verbatim):三单源——design 变更清单
2379
+ // (parseFileChangeListDetailed)、他侧归因(splitOwnVsForeignDiffFiles,与 patch 子步
2380
+ // 同源)、冻结面文件集(材料包同源解析)——砍评审员逐文件 git 考古(实测 22 次调用)。
2381
+ // 声明清单缺位/冻结面为空 → attribution=null(该子段零注入,FR 表/摘要不受影响)。
2382
+ let _attribution = null
2383
+ try {
2384
+ const { parseFrozenPatchSummary, buildFreezeAttribution } = await import('./flow-review-pack.js')
2385
+ const _freezeFiles = parseFrozenPatchSummary(patchText).map((f) => f.path)
2386
+ if (_freezeFiles.length > 0) {
2387
+ const { parseFileChangeListDetailed } = await import('./change-list.js')
2388
+ const _declared = parseFileChangeListDetailed(join(changeDir, 'design.md')).map((e) => e.path)
2389
+ if (_declared.length > 0) {
2390
+ const { splitOwnVsForeignDiffFiles } = await import('./foreign-declared.js')
2391
+ const _split = splitOwnVsForeignDiffFiles(cwd, change, _freezeFiles, { specBase })
2392
+ _attribution = buildFreezeAttribution({ declaredFiles: _declared, freezeFiles: _freezeFiles, foreign: _split && _split.foreign })
2393
+ }
2394
+ }
2395
+ } catch { /* 归属面 fail-soft:装配失败零注入该子段 */ }
2396
+ let _packLines = null
2397
+ try {
2398
+ const { assembleReviewPack } = await import('./flow-review-pack.js')
2399
+ const _pack = assembleReviewPack({ changeDir, patchText, attribution: _attribution })
2400
+ if (_pack && Array.isArray(_pack.lines) && _pack.lines.length > 0) _packLines = _pack.lines
2401
+ } catch { /* 材料包面 fail-soft:装配失败零注入 */ }
2402
+ console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines, packLines: _packLines }))
2234
2403
  reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
2235
2404
  appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
2236
2405
  reportMidFail('review')
@@ -2362,7 +2531,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2362
2531
  }
2363
2532
  } catch { /* 回读失败 → 空面(回写零命中静默) */ }
2364
2533
  }
2365
- // ④ distill:决策提炼(rejected/needsWait 异态 → 升厚留人工裁决,不静默吞)+ FR 索引提炼
2534
+ // ④ distill:决策提炼(needsWait 异态 → 升厚留人工裁决,不静默吞——判定 isDistillAbnormal
2535
+ // 单一源在 decision-distill.js,2026-10-20-distill-abnormal-upgrade-fix 修复幽灵触发器)+ FR 索引提炼
2366
2536
  // (2026-09-22-thin-fr-distill-sync:轻量变更此前只蒸馏 decisions 不调 indexRequirements——
2367
2537
  // requirements 永不进 knowledge/fr,知识复利在新默认道断流;轻量变更无 design.md,域路由
2368
2538
  // 以基线以来交付 diff 供 deliverableFiles,伪域回退同口径)
@@ -2392,13 +2562,13 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2392
2562
  }
2393
2563
  } catch (e) { console.warn(`⚠️ 变更索引回写失败(fail-soft 不阻断归档):${(e && e.message) || e}`) }
2394
2564
  try {
2395
- const { distillIntoKnowledge } = await import('./decision-distill.js')
2565
+ const { distillIntoKnowledge, isDistillAbnormal } = await import('./decision-distill.js')
2396
2566
  const knowledgeRoot = join(specBase, 'knowledge')
2397
2567
  const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
2398
2568
  const res = distillIntoKnowledge(changeDir, knowledgeRoot, typeof head === 'string' ? head.trim() : '', null)
2399
- const abnormal = res && (res.rejected > 0 || res.needsWait > 0)
2569
+ const abnormal = isDistillAbnormal(res)
2400
2570
  if (abnormal) {
2401
- writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `distill 异态(rejected=${res.rejected} needsWait=${res.needsWait})——升厚留人工裁决` })
2571
+ writeFlowState(changeDir, { tier: 'thick', upgrade_reason: `distill 异态(needsWait=${res.needsWait})——升厚留人工裁决` })
2402
2572
  resetThinStageRowOnUpgrade(cwd, change, specBase)
2403
2573
  console.warn(`⚠️ distill 异态 → 已升厚档(tier=thick)。剩余流程按厚档走 run <stage> 完整仪式;归档子步仍将执行(厚档语义不跳过 plan.md 校验)。`)
2404
2574
  }
@@ -2526,7 +2696,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2526
2696
  const finalHead = gitQuiet(cwd, ['rev-parse', 'HEAD'])
2527
2697
  if (st.baseline_commit && typeof finalHead === 'string' && finalHead.trim() && finalHead.trim() !== st.baseline_commit) {
2528
2698
  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 历史;`)
2699
+ console.log(` 审计真相已冻结在归档件 ${jsonOnlyFreeze ? 'change-patch.json(跨仓 repos[] 正文+sha256 锚定)' : 'change.patch(sha256 锚定)'} + verify-result + review.json,不依赖 git 历史;`)
2530
2700
  console.log(` 注:knowledge「最近确认」hash 为溯源快照,压扁后成孤儿引用属预期(非活引用)。`)
2531
2701
  }
2532
2702
  } catch { /* 指引 best-effort */ }
@@ -2568,6 +2738,39 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2568
2738
  console.error(` (拒绝后派生授权须显式再确认——2026-10-12-approve-channel-attest)`)
2569
2739
  process.exit(2)
2570
2740
  }
2741
+ // 批准前文档完整性校验(2026-10-19-approve-gate-tick-nudge FR-01/02):批准=有内容的机器
2742
+ // 事实(批时文档必齐)。复用 done 门同源校验(verifyFlowDrafts 双轨);repair:false——批准
2743
+ // 面只读,不做 design 问题行自动回填(写盘修复属 done 面)。诚实边界:本门证明「答在批前」,
2744
+ // 不证明「答在码前」——时序证据路线已实证不可行(批量收口下 git 时间戳说谎,thin-order-canon
2745
+ // 四路调查),不做假装能拦的检查。
2746
+ {
2747
+ const runtimeRoot = resolveRuntimeRoot({}, specBase)
2748
+ const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
2749
+ const adoptedDesign = st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))
2750
+ const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign, repair: false })
2751
+ for (const a of r.advisories || []) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2752
+ if (r.applicable && r.violations.length > 0) {
2753
+ console.error(`❌ spec 断点未批准——文档未齐(批准是动码前置,先答 spec 面再批):`)
2754
+ for (const v of r.violations) console.error(` - ${v}`)
2755
+ console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2756
+ process.exit(1)
2757
+ }
2758
+ if (!r.applicable) {
2759
+ // draft-ledger 机本位缺失(跨机批准/存量变更):fail-open 降级——仅 v1(AGENT 槽形态)跑
2760
+ // 空槽兜底;v2 纯 markdown 无 ledger 无法对锚校验,如实降级放行(不假装能查)。
2761
+ let v1Text = null
2762
+ try { v1Text = readFileSync(join(changeDir, 'design.md'), 'utf8') } catch { /* 无 design */ }
2763
+ if (v1Text && /<!--AGENT:槽\d+/.test(v1Text)) {
2764
+ const dr = verifyDesignRecordFilled({ changeDir })
2765
+ if (dr.applicable && dr.emptySlots.length > 0) {
2766
+ console.error(`❌ spec 断点未批准——design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2767
+ console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2768
+ process.exit(1)
2769
+ }
2770
+ }
2771
+ console.log(`ℹ️ 批准校验降级放行:draft-ledger 不在此机(.runtime 机本位/存量变更)——空槽兜底校验通过`)
2772
+ }
2773
+ }
2571
2774
  let approvedBy = by
2572
2775
  if (!approvedBy) {
2573
2776
  try {
@@ -2597,7 +2800,7 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2597
2800
  console.warn(`⚠️ agent 自批留痕:如用户实际未确认,请撤销——sillyspec flow reject --change ${change} --reason <理由>`)
2598
2801
  }
2599
2802
  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 <理由>`)
2803
+ console.log(` 批准是动码前置义务:若尚未动码,现在可以开始写码;若已动码,本次属超期补批(留痕可审计)。flow done 断点门已满足。撤销:sillyspec flow reject --change ${change} --reason <理由>`)
2601
2804
  }
2602
2805
 
2603
2806
  /**
@@ -2808,17 +3011,10 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2808
3011
  let designFilled = false, frFilled = false, bindingsFilled = 0, bindingsTotal = 0
2809
3012
  try {
2810
3013
  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
- }
3014
+ // 双格式判定单一源(2026-10-19-approve-gate-tick-nudge FR-03 上移 flow-review.js;
3015
+ // 判据与原内联逻辑逐字一致——status/tick 同源,行为零变化)
3016
+ const { designSectionsFilled } = await import('./flow-review.js')
3017
+ designFilled = designSectionsFilled(dText)
2822
3018
  } catch { /* 无 design */ }
2823
3019
  try {
2824
3020
  const rText = readFileSync(join(changeDir, 'requirements.md'), 'utf8')
@@ -2833,9 +3029,18 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2833
3029
  bindingsFilled = _rows.filter((m) => m[2].trim() && !m[2].trim().startsWith('(待填')).length
2834
3030
  }
2835
3031
  } catch { /* 无 requirements */ }
2836
- // 阶段推断
3032
+ // 阶段推断(2026-10-19-thin-order-canon:相位看批准——docs 齐但未批准仍停 ① spec 待批准,
3033
+ // 防 status 中途查询面诱导「先执行后卡点」;autopilot 是用户显式豁免、进执行段带标注)
3034
+ const specApproved = st.spec_approved === true
3035
+ const autopilotOn = st.autopilot === true
2837
3036
  let phase = '① spec(填 FR + design 槽)'
2838
- if (designFilled && frFilled && bindingsFilled >= bindingsTotal && bindingsTotal > 0) phase = '② 执行(写代码跑测试)→ ①卡点:spec 摘要给用户确认'
3037
+ if (designFilled && frFilled && bindingsFilled >= bindingsTotal && bindingsTotal > 0) {
3038
+ phase = specApproved
3039
+ ? '② 执行(已批准·写代码跑测试)'
3040
+ : autopilotOn
3041
+ ? '② 执行(autopilot 豁免·写代码跑测试)'
3042
+ : '① spec(→ 待用户批准:摘要给用户后 flow approve)'
3043
+ }
2839
3044
  if (subDone.includes('ledger')) phase = '③ 归档(flow done 收口)'
2840
3045
  // 任务心跳(2026-09-29-flow-task-heartbeat + d007-incontext 纠偏):②执行阶段把「下一个任务」
2841
3046
  // 交给机器指——当场重读 tasks.md(唯一进度源)取第一个未勾行 + 进度。进度推进本身零 CLI
@@ -2876,6 +3081,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2876
3081
  designFilled, frFilled, bindingsFilled, bindingsTotal,
2877
3082
  tasksChecked, tasksTotal,
2878
3083
  substeps: subDone, substepsTotal: SUBSTEPS.length,
3084
+ specApproved, specApprovedChannel: specApproved ? approveChannelOf(st) : null,
2879
3085
  ...(st.legacy_fallback ? { legacyFallback: true } : {}),
2880
3086
  }))
2881
3087
  return
@@ -2884,6 +3090,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2884
3090
  `📋 ${change}`,
2885
3091
  ...(changeTitle ? [` 标题:${changeTitle}`] : []),
2886
3092
  ` 阶段:${phase}`,
3093
+ ` spec 批准:${specApproved ? '✅ 已批准' : autopilotOn ? '🔑 autopilot 豁免(用户显式授权)' : '⬜ 未批准(动码前先过断点①)'}`,
2887
3094
  ` design 槽:${designFilled ? '✅ 已填' : '⬜ 未填'}|FR 区:${frFilled ? '✅ 已填' : '⬜ 未填'}|绑定槽:${bindingsFilled}/${bindingsTotal}`,
2888
3095
  hasTasks ? ` 任务勾选:${tasksChecked}/${tasksTotal}` : null,
2889
3096
  ...(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
 
@@ -1454,7 +1454,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1454
1454
  if (!flags.includes('--upgrade-thick')) {
1455
1455
  console.error(`⛔ 本变更在轻量跑道上(flow-state 在场)——升厚走完整流程需用户同意,agent 不得自行转道:`)
1456
1456
  console.error(` · 征得用户同意后带 --upgrade-thick 重跑本命令(落痕留档,首次转厚即转)`)
1457
- console.error(` · 用户未确认或选择轻量跑 → 继续轻量变更:直接干活,收口 sillyspec flow done --change ${effectiveChange}`)
1457
+ console.error(` · 用户未确认或选择轻量跑 → 继续轻量变更:先答 spec 面并过批准再干活(未批准先 flow approve),收口 sillyspec flow done --change ${effectiveChange}`)
1458
1458
  process.exit(2)
1459
1459
  }
1460
1460
  writeFlowState(changeDir, { legacy_fallback: true, upgraded_by_consent: new Date().toISOString() })