sillyspec 3.32.5 → 3.32.7
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/AGENTS.md +2 -0
- package/package.json +1 -1
- package/src/config-schema.js +20 -0
- package/src/doctor-diagnostics.js +61 -1
- package/src/flow-parity.js +215 -4
- package/src/flow-review.js +75 -10
- package/src/flow.js +297 -55
- package/src/index.js +94 -1
- package/src/local-register.js +20 -1
- package/src/machine-interface.js +10 -5
- package/src/patch-reanchor.js +131 -0
- package/src/review-dispatch.js +18 -3
- package/src/review-probes.js +161 -0
- package/src/run/complete-handlers.js +48 -15
- package/src/run/gates.js +17 -2
- package/src/run/shared.js +14 -0
- package/src/stage-contract.js +2 -2
- package/src/stage-review.js +67 -1
- package/src/task-tick.js +43 -2
- package/src/verify-probes.js +1 -1
- package/src/worktree-apply.js +22 -0
- package/src/worktree-cross.js +158 -9
package/src/flow.js
CHANGED
|
@@ -62,6 +62,17 @@ export function writeFlowState(changeDir, patch) {
|
|
|
62
62
|
}, { lineWidth: 120 }) + '\n')
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** 批准通道取值(2026-10-12-approve-channel-attest):flow done 回显与遥测同源——历史变更
|
|
66
|
+
* 无 channel 字段时归一 unattested-legacy(2026-10-12 之前真批/自批不可区分的留痕形态),
|
|
67
|
+
* autopilot 是用户显式豁免通道非批准。三态封闭集(user-confirmed/agent-self/unattested)是
|
|
68
|
+
* 治理域(谁在主张批准),不承载内容语义——开放内容走 spec_rejected_reason 自由文本。 */
|
|
69
|
+
export function approveChannelOf(st) {
|
|
70
|
+
const s = st || {}
|
|
71
|
+
if (s.autopilot) return 'autopilot'
|
|
72
|
+
if (s.spec_approved_channel) return String(s.spec_approved_channel)
|
|
73
|
+
return s.spec_approved ? 'unattested-legacy' : 'none'
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
/** 读 local.yaml 的 flow 配置(缺省 thin——2026-09-25-thin-default-flip 翻转:入口归一,
|
|
66
77
|
* D-010@v2 实验通道定位由轻量变更加固件(设计记录/测试绑定/patch 留档/预段收编)的数据推翻;
|
|
67
78
|
* 显式 mode: legacy 的仓维持旧道。文本级读同既有习惯)。 */
|
|
@@ -73,13 +84,17 @@ export function readFlowConfig(specBase) {
|
|
|
73
84
|
if (/^flow\s*:\s*(thin|legacy)\s*$/m.test(raw)) mode = raw.match(/^flow\s*:\s*(thin|legacy)\s*$/m)[1]
|
|
74
85
|
const th = raw.match(/^\s*edit_ratio_threshold\s*:\s*([0-9.]+)\s*$/m)
|
|
75
86
|
const en = raw.match(/^\s*edit_ratio_enforcement\s*:\s*(advisory|block)\s*$/m)
|
|
87
|
+
// module_gate_ignore(2026-10-12-thin-module-gate-and-feed):未登记目录软门的固定忽略清单,
|
|
88
|
+
// 单行逗号串(目录前缀,posix)——列表块形态不支持,readFlowConfig 同款逐键正则防线
|
|
89
|
+
const ig = raw.match(/^\s*module_gate_ignore\s*:\s*(.+)$/m)
|
|
76
90
|
return {
|
|
77
91
|
mode,
|
|
78
92
|
editRatioThreshold: th ? Number(th[1]) : 0.5,
|
|
79
93
|
editRatioEnforcement: en ? en[1] : 'advisory',
|
|
94
|
+
moduleGateIgnore: ig ? ig[1].split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
80
95
|
}
|
|
81
96
|
} catch {
|
|
82
|
-
return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory' }
|
|
97
|
+
return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory', moduleGateIgnore: [] }
|
|
83
98
|
}
|
|
84
99
|
}
|
|
85
100
|
|
|
@@ -310,6 +325,16 @@ export async function flowKnowledgeDigest({ specBase, change, changeDir, input,
|
|
|
310
325
|
lines.length = 0
|
|
311
326
|
lines.push(`🧠 知识注入(轻量道读取面):语料未命中知识库(无触达域依据、无否决命中、INDEX 无对齐条目)——可自行查 knowledge/INDEX.md。`)
|
|
312
327
|
}
|
|
328
|
+
// 模块卡欠账提示前移(2026-10-12-thin-module-gate-and-feed 三档):触碰命中模块 behind>0
|
|
329
|
+
// 才出事实行——动手前看到「你要动的卡已落后」比收口时警告有效;computeDocsDebt(厚道
|
|
330
|
+
// execute {DOCS_DEBT})同源,空债/无图零输出,异常走外层 catch fail-open 不阻断 start。
|
|
331
|
+
if (touched.length > 0) {
|
|
332
|
+
try {
|
|
333
|
+
const { renderThinDocsDebt } = await import('./flow-parity.js')
|
|
334
|
+
const debtLines = renderThinDocsDebt({ specBase, projectRoot: dirname(specBase), changedFiles: touched })
|
|
335
|
+
if (debtLines.length > 0) lines.push(...debtLines)
|
|
336
|
+
} catch { /* 欠账计算 fail-open */ }
|
|
337
|
+
}
|
|
313
338
|
} catch (e) {
|
|
314
339
|
lines.length = 0
|
|
315
340
|
lines.push(`🧠 知识注入(轻量道读取面):注入失败(${(e && e.message) || e})——可自行读 knowledge/INDEX.md 与 knowledge/fr/`)
|
|
@@ -759,14 +784,19 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
|
|
|
759
784
|
`【协议调用 1/2(本次)】change 已建 + 基线锚定(baseline_commit=${baseline ? baseline.slice(0, 10) : '(无 git 历史)'})${withTasks ? ' + 任务卡模式(--with-tasks:中间自愿用 task done,收尾仍 flow done)' : ''}`,
|
|
760
785
|
``,
|
|
761
786
|
`【你要做的】直接干活:改代码、写测试。治理工件不用你写——flow done 机器做(协议记账单位=change 级)。`,
|
|
787
|
+
// 收口拒收红线前置(2026-10-11-flow-start-redline-frontload):钉死/空槽/绑定三面都是文档
|
|
788
|
+
// 先行校验,实证第一次最容易踩(四问文本被改写、区间 task-NN 写法零证据被拒)——横幅
|
|
789
|
+
// 头部先行告知,收口才暴露的规则前置到动笔前。
|
|
790
|
+
`🚫 收口拒收红线(文档先行校验,动笔前先读——第一次最容易踩的就是这三条):`,
|
|
791
|
+
` ① design 四问文本与 FR 标题锚逐字钉死勿改写——问题原文保持原样、答案写在问题下方,改写/删除即收口拒收(文档↔锚逐字对比);`,
|
|
792
|
+
` ② design.md 四节(做法/接口契约/边界并发四问/风险)每节至少一行——空节拒收(写「不适用:<理由>」也算答);`,
|
|
793
|
+
` ③ requirements 每条 FR 须有带强度词的行为句(必须/禁止/SHOULD)+ 测试绑定行——缺任一即拒收。`,
|
|
762
794
|
`书写面(纯 markdown 起草):requirements FR 正文/design 四节作答/测试绑定行都直接写正文——`,
|
|
763
|
-
`
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
`
|
|
768
|
-
`📜 requirements 每条 FR = 标题锚 + 一句带强度词的行为规定(必须/禁止/SHOULD/可以)+ 按需场景块`,
|
|
769
|
-
` (#### 场景:名 + Given/When/Then);测试绑定节每条 FR 一行 \`FR-NN: test/路径「用例」\`。`,
|
|
795
|
+
` 文档内零指纹标记、零槽注释、零书写指导(规则以本横幅与命令卡为唯一源);成功标准漂移只出 advisory 不拒收。`,
|
|
796
|
+
`⚠️ design.md 四节动码前后顺手作答;列改动文件用独立「## 文件变更清单」节+表格(| 操作 | 路径 | 说明 |)`,
|
|
797
|
+
` ——勿写在「接口契约」节内(收口按节名解析声明面)。`,
|
|
798
|
+
`📜 requirements 每条 FR = 标题锚 + 行为句 + 按需场景块(#### 场景:名 + Given/When/Then);`,
|
|
799
|
+
` 测试绑定节每条 FR 一行 \`FR-NN: test/路径「用例」\`(无测试面写「不适用:理由」)。`,
|
|
770
800
|
`📦 冻结面在 flow done 时点采集(baseline..HEAD 提交面):未提交交付文件——会话专属 worktree 自动并入;`,
|
|
771
801
|
` 共享主仓可带 --freeze-dirty 显式声明并入;git 中间提交归档后可 reset --soft 压扁为单提交`,
|
|
772
802
|
` (审计真相在 change.patch 冻结件 sha256 锚定,不依赖 git 历史形态)。`,
|
|
@@ -781,13 +811,16 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
|
|
|
781
811
|
` 执行循环:对每个未勾行 Working on task N/M → 做一件 → 测试绿后当场勾这一格 → 下一行——CLI 以 checkbox 为进度状态机`,
|
|
782
812
|
` (sillyspec task tick --change ${change} --task task-NN 即时回显进度 N/M 与下一任务指针,flow status 同源;或直接 Edit 翻格 \`- [ ]\`→\`- [x]\`),watcher 实时上平台。`,
|
|
783
813
|
` ⚠️ harness 的 TodoWrite 类工具是会话内便利面,不替代 tasks.md——平台进度/收口证据只读它(0/12 事故实证:todo 全 completed 而 tasks.md 0 勾);`,
|
|
784
|
-
` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN
|
|
814
|
+
` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
|
|
785
815
|
` flow status --change ${change} 为自愿查看/恢复面(恢复时给下一任务指针与进度)。`,
|
|
786
816
|
``,
|
|
787
817
|
`🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
|
|
788
818
|
` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
|
|
789
819
|
` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
|
|
790
|
-
`
|
|
820
|
+
` agent 代跑时如实声明通道:--user-confirmed(转述用户已确认)/ --self-approved(agent 自批),`,
|
|
821
|
+
` 无 flag 按 unattested 留痕(审计面无法区分用户确认与自批)——2026-10-12-approve-channel-attest。`,
|
|
822
|
+
` 方案错了返工最贵;未批准且未声明 --autopilot 时 flow done 拒收(2026-10-04-thin-docs-v2);`,
|
|
823
|
+
` 用户否掉方案 → flow reject --change ${change} --reason <理由>(拒后批准须显式通道再确认)。`,
|
|
791
824
|
` ② 执行断点:写完代码跑完测试后,把测试结果(过了几个/挂了什么)给用户看,`,
|
|
792
825
|
` 等用户确认再跑收口。`,
|
|
793
826
|
` ③ 归档断点:flow done 跑完后(无论过/拒),把结果(归档成功/被什么拦了)给用户看。`,
|
|
@@ -840,6 +873,9 @@ function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot,
|
|
|
840
873
|
...(title ? [`- 标题:${title}`] : []),
|
|
841
874
|
`- 做到哪:任务勾选 ${checked}/${total};基线以来提交 ${commits} 个;变更/dirty 文件 ${dirty} 个;P2 质量扫描记录 ${hasLedger ? '在场' : '无'}。`,
|
|
842
875
|
`- 六子步标记:${done.length ? `已完成 ${done.join('/')}` : '(无)'}${left.length ? `;待办 ${left.join('/')}` : ';全部完成'}`,
|
|
876
|
+
// 红线重申(2026-10-11-flow-start-redline-frontload):重入不出首进横幅——钉死规则与
|
|
877
|
+
// task-NN token 形态随简报补位,断点续跑生命周期内规则不丢。
|
|
878
|
+
`- 红线重申:design 四问文本/FR 标题锚逐字钉死勿改写(改写即拒收);勾选证据提交消息逐个带 task-NN 或 /、,连写(「task-01..task-05」区间写法不认)。`,
|
|
843
879
|
`- 剩什么:${left.length === 0 && dirty === 0 ? '活已干完' : dirty > 0 ? '活未干完(继续改代码)' : '收尾待裁决'}`,
|
|
844
880
|
`- 下一步:${left.length === 0 ? `sillyspec flow done --change ${change}` : `继续干活;干完跑 flow done(断点续)`}`,
|
|
845
881
|
...(digestLines.length > 0 ? ['', ...digestLines] : []),
|
|
@@ -864,7 +900,7 @@ export function remainingSubstepsAtFail({ snapshotSubsteps = {}, doneList = [],
|
|
|
864
900
|
return SUBSTEPS.filter((k) => k !== failed && snapshotSubsteps?.[k] !== 'done' && !doneNow.has(k))
|
|
865
901
|
}
|
|
866
902
|
|
|
867
|
-
export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, allowBatchTick = false }) {
|
|
903
|
+
export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null, confirmArchive = true, freezeDirty = false, acceptDirtyGap = false, acceptOrphanFace = false, allowBatchTick = false, acceptUnmapped = false }) {
|
|
868
904
|
const changeDir = join(specBase, 'changes', change)
|
|
869
905
|
const st = readFlowState(changeDir)
|
|
870
906
|
if (!st) {
|
|
@@ -898,6 +934,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
898
934
|
const skip = (k) => { doneList.push(`${k}(skip)`) }
|
|
899
935
|
let reviewOutcome = null
|
|
900
936
|
let gateSummaryText = null
|
|
937
|
+
// 模块对账面(2026-10-12-thin-module-gate-and-feed):patch 子步算出后供 distill 回写消费;
|
|
938
|
+
// 断点续跑 patch 已 done 时从 change-patch.json files[] 回读重算(单一时点口径,不随图回写漂移)
|
|
939
|
+
let moduleScope = null
|
|
940
|
+
let changelogAppended = []
|
|
901
941
|
// 遥测单点(2026-09-25-thin-parity-assets 修评审 P2①:失败路径 exit 前也要落账——校准信号
|
|
902
942
|
// 不许在失败面丢失;成功收口走函数末尾同款记录,失败面只多不少)
|
|
903
943
|
const appendTelemetry = (review) => {
|
|
@@ -906,6 +946,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
906
946
|
ts: new Date().toISOString(), change, protocolCalls: 2,
|
|
907
947
|
draftAmendments: st.edit_ratio != null ? 1 : 0, editRatio: st.edit_ratio ?? null,
|
|
908
948
|
routeHint: st.route_hint ?? null, tier: st.tier, upgraded: st.upgrade_reason ?? null,
|
|
949
|
+
// 批准通道遥测(2026-10-12-approve-channel-attest):与收口回显同源取值,校准
|
|
950
|
+
// unattested/agent-self 占比用(dogfood 实证留痕不可区分 → 通道可聚合)。
|
|
951
|
+
approveChannel: approveChannelOf(st),
|
|
909
952
|
review,
|
|
910
953
|
}) + '\n', 'utf8')
|
|
911
954
|
} catch { /* 遥测 best-effort */ }
|
|
@@ -938,11 +981,27 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
938
981
|
if (!st.autopilot && !st.spec_approved) {
|
|
939
982
|
console.error(`❌ spec 断点未批准:本变更(v2 起草)收口需要方案确认留痕`)
|
|
940
983
|
console.error(` 修复:把 FR 条目+design 四节摘要给用户看,用户确认后由用户运行:`)
|
|
941
|
-
console.error(` sillyspec flow approve --change ${change}`)
|
|
984
|
+
console.error(` sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
|
|
942
985
|
console.error(` 或用户显式豁免(自主跑到底):重跑 flow start --change ${change} --autopilot(声明留痕)`)
|
|
943
986
|
reportMidFail('artifacts')
|
|
944
987
|
process.exit(1)
|
|
945
988
|
}
|
|
989
|
+
// 批准通道回显(2026-10-12-approve-channel-attest):channel 是审计面第一公民——agent-self
|
|
990
|
+
// 与 unattested 醒目(dogfood 实证:用户真批转述与 agent 自批在留痕层面曾不可区分)。
|
|
991
|
+
if (st.autopilot) {
|
|
992
|
+
console.log('🔑 spec 断点:autopilot 显式豁免(用户授权自主跑到底)')
|
|
993
|
+
} else {
|
|
994
|
+
const _ch = approveChannelOf(st)
|
|
995
|
+
const _warn = _ch === 'agent-self' || _ch.startsWith('unattested')
|
|
996
|
+
// 评审 P3② 清偿:翻越语只在 superseded 戳在场时陈述——并发窄窗混合态(approved 真 +
|
|
997
|
+
// 拒绝未翻越)不得假称「已显式再确认」,按状态异常如实告警。
|
|
998
|
+
const _rej = st.spec_rejected_reason
|
|
999
|
+
? (st.spec_rejected_superseded_at
|
|
1000
|
+
? `(曾拒绝:${String(st.spec_rejected_reason).slice(0, 60)}→显式再确认翻越)`
|
|
1001
|
+
: `(⚠️ 存在未翻越拒绝记录(spec_rejected 未 superseded)——状态异常,请核查 flow-state)`)
|
|
1002
|
+
: ''
|
|
1003
|
+
console.log(`${_warn ? '⚠️' : '🔑'} spec 断点批准留痕:channel=${_ch}|by=${st.spec_approved_by || '?'}${_rej}`)
|
|
1004
|
+
}
|
|
946
1005
|
} else {
|
|
947
1006
|
if (r.applicable && r.violations.length > 0) {
|
|
948
1007
|
console.error(`❌ 工件校验拒收(机器稿指纹三态):`)
|
|
@@ -1034,8 +1093,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1034
1093
|
// 机器稿基线做镜像豁免(route-hindsight 基线快照仍供改写比指标,与哨兵解耦)。
|
|
1035
1094
|
const sent = detectFakeCheckCompletion({ changeDir, tasksMd: readFileSync(tasksPath, 'utf8'), commits: commitMessages })
|
|
1036
1095
|
if (sent.status === 'fake') {
|
|
1037
|
-
console.error(`🚫 哨兵断言拒收:tasks.md 全勾(${sent.checked}/${sent.claimTotal})但 ${sent.missing.length}
|
|
1038
|
-
|
|
1096
|
+
console.error(`🚫 哨兵断言拒收:tasks.md 全勾(${sent.checked}/${sent.claimTotal})但 ${sent.missing.length} 个任务零完成证据(baseline..HEAD 提交标题与正文均无 token、无 review.json):${sent.missing.join('、')}`)
|
|
1097
|
+
// token 合法形态写明(2026-10-11-flow-start-redline-frontload):实证「task-01..task-05」
|
|
1098
|
+
// 区间写法被判零证据时才知道格式约束——拒收文案自释,减少一轮试错。
|
|
1099
|
+
console.error(' 补证据(提交标题或正文带 task-NN——逐个列出或 /、,连写(如 task-01/02/03),「task-01..task-05」区间写法不认;或产 review.json)或取消勾选后重跑——假完成主张不许过门')
|
|
1100
|
+
// 扫描面透明化(2026-10-12-cli-friction-signal,dogfood 实证:跨仓交付的 agent 靠猜
|
|
1101
|
+
// 证据该落在哪)——声明哨兵只扫工作区仓与变更目录,跨仓仓提交天然不在面内。
|
|
1102
|
+
console.error(` 证据扫描面:工作区仓 git log ${st.baseline_commit ? String(st.baseline_commit).slice(0, 10) : 'baseline'}..HEAD 的提交消息(标题+正文)+ 变更目录内 review.json;`)
|
|
1103
|
+
console.error(' 跨仓仓(register-repo 登记的外部仓)的提交不在扫描面——交付提交在其他仓时,须在工作区仓的提交消息带 task-NN(如治理工件提交)或产出 review.json 补证。')
|
|
1039
1104
|
appendTelemetry({ sentinel: 'fake', missing: sent.missing.length })
|
|
1040
1105
|
reportMidFail('ledger')
|
|
1041
1106
|
process.exit(1)
|
|
@@ -1276,10 +1341,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1276
1341
|
// 治理面等价保留(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop
|
|
1277
1342
|
// 用户实证):评审结论的对象是交付面——窗口内本变更提交只碰 .sillyspec/** 治理文件且
|
|
1278
1343
|
// 未触本变更 requirements/design(承诺面也是评审对象)时,结论对最新交付面逐字节等价,
|
|
1279
|
-
// 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze
|
|
1344
|
+
// 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结(FR-01 后治理
|
|
1345
|
+
// 工件不入冻结面——治理修复的留档真相=归档目录本体文件,重冻结保交付面时点对齐)
|
|
1280
1346
|
// (审计件覆盖最新提交面不变量不破)。此前任何后缀提交一律隔离重评——评审后修一个
|
|
1281
1347
|
// P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
|
|
1282
|
-
console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch
|
|
1348
|
+
console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结(治理工件不入冻结面,治理修复随归档目录本体留档)`)
|
|
1283
1349
|
} else {
|
|
1284
1350
|
const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
|
|
1285
1351
|
try {
|
|
@@ -1308,11 +1374,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1308
1374
|
}
|
|
1309
1375
|
if (st.substeps?.patch !== 'done' || driftRefreeze) {
|
|
1310
1376
|
let patchOk = false
|
|
1377
|
+
let orphanGapCount = 0
|
|
1311
1378
|
try {
|
|
1312
|
-
// patch 面 =
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1315
|
-
//
|
|
1379
|
+
// patch 面 = 本变更可归属的交付面(2026-10-12-close-trace-freeze-integrity FR-01 收敛):
|
|
1380
|
+
// ①baseline..HEAD 提交面过滤 .sillyspec/(只留 docs/ 交付文档——治理工件目录不入冻,
|
|
1381
|
+
// 对齐 execute --done 沉淀资产面投影;工件真相随归档目录本体入库,patch 再冻是冗余且
|
|
1382
|
+
// 掩盖「可归属面为空」哨兵——ws-ref-ws-name +137/-0 全工件 0 代码实证)②独占树未提交
|
|
1383
|
+
// 交付面(worktree/--freeze-dirty)。他侧 quicklog/knowledge WIP 不入;提交面再过他侧
|
|
1384
|
+
// 声明切分(他侧同窗口提交的防线)。
|
|
1316
1385
|
if (!st.baseline_commit) {
|
|
1317
1386
|
console.log('📦 patch 留档跳过:无 git 基线(无历史仓)')
|
|
1318
1387
|
patchOk = true
|
|
@@ -1373,18 +1442,6 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1373
1442
|
if (dirtyForeign.length > 0) {
|
|
1374
1443
|
console.warn(`⚠️ patch 留档:${dirtyForeign.length} 个 dirty 文件被其他活跃变更声明排除(${dirtyForeign.slice(0, 3).map((x) => `${x.file}←${x.owners[0]}`).join(', ')}${dirtyForeign.length > 3 ? ' 等' : ''})——如有误(陈旧声明),清理该旧变更或用 --refreeze 重冻结`)
|
|
1375
1444
|
}
|
|
1376
|
-
const changeDirFiles = []
|
|
1377
|
-
const walk = (dir) => {
|
|
1378
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
1379
|
-
// flow-state.yaml 是运行态(机器写、冻结后仍随子步推进变化)——不入审计 patch
|
|
1380
|
-
//(2026-09-27-tool-debt-cleanup:旧口径把它当 new file 冻进 patch,评审 P2)
|
|
1381
|
-
if (e.name === 'flow-state.yaml') continue
|
|
1382
|
-
const p = join(dir, e.name)
|
|
1383
|
-
if (e.isDirectory()) walk(p)
|
|
1384
|
-
else changeDirFiles.push(relative(cwd, p).replace(/\\/g, '/'))
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
try { walk(changeDir) } catch { /* 目录异常=空面 */ }
|
|
1388
1445
|
// 冻结面收集(2026-09-25-thin-r16-patches 修复①):committed 为主;会话专属 worktree
|
|
1389
1446
|
// (gate-snapshot 同款路径判定)下未提交 dirty 交付面一并入冻结——独占树内全归属本变更;
|
|
1390
1447
|
// 共享主仓 dirty 无法归属只警告(他侧声明免警告),审计缺口显式化不再静默。
|
|
@@ -1433,26 +1490,49 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1433
1490
|
// change-patch.json 两件,但旧轮冻结件里可能仍有 scope-audit.json/.patch——
|
|
1434
1491
|
// legacy 防护保留):上一轮留痕件作为 untracked 新文件被全文自嵌入即自引用循环
|
|
1435
1492
|
// (旧 patch 内容混进新 patch,三评 P1 根因)。
|
|
1436
|
-
const ownFiles = [...new Set([...freeze.files
|
|
1493
|
+
const ownFiles = [...new Set([...freeze.files])]
|
|
1437
1494
|
.filter((f) => f && !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
|
|
1438
1495
|
&& !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
|
|
1496
|
+
// 独占 worktree 冻结完备性守卫(2026-10-12-close-trace-freeze-integrity FR-02):
|
|
1497
|
+
// 事故形态(multi-agent-platform ws-ref-ws-name 取证):实现提交在会话 worktree 内完成
|
|
1498
|
+
// 但不在 flow done 时点 HEAD 的祖先链(reset/detach/平行分支)——baseline..HEAD 提交面
|
|
1499
|
+
// 与 git status 脏面都看不到,冻结面静默缺实现。本 worktree 的 HEAD reflog 会话独占
|
|
1500
|
+
// (linked worktree 独立 reflog),扫全部轨迹:不在 HEAD 祖先链且触及交付面的提交,
|
|
1501
|
+
// patch-id 与 baseline..HEAD 提交等价(amend/rebase 残留)视为已覆盖;否则阻断收口
|
|
1502
|
+
// (与 dirty gate 同款断点续语义)。共享主仓路径明确不启用——reflog 混他侧会话操作
|
|
1503
|
+
// 无法归属,--freeze-dirty 手动声明只覆盖脏面不进 reflog 扫描。
|
|
1504
|
+
if (exclusiveFrom === 'worktree') {
|
|
1505
|
+
try {
|
|
1506
|
+
const { scanOrphanFace } = await import('./flow-parity.js')
|
|
1507
|
+
const orphan = scanOrphanFace(cwd, { baseline: st.baseline_commit })
|
|
1508
|
+
if (orphan.files.length > 0 && !acceptOrphanFace) {
|
|
1509
|
+
console.error(`⛔ 会话专属 worktree 存在冻结面外交付改动:${orphan.commits.length} 个提交不在 HEAD 祖先链(patch-id 不等价)触及 ${orphan.files.length} 个交付文件——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
|
|
1510
|
+
console.error(` 提交:${orphan.commits.slice(0, 5).map((c) => c.slice(0, 10)).join('、')}${orphan.commits.length > 5 ? ' 等' : ''}`)
|
|
1511
|
+
console.error(` 文件:${orphan.files.slice(0, 5).join('、')}${orphan.files.length > 5 ? ' 等' : ''}`)
|
|
1512
|
+
console.error(` ① git rebase/cherry-pick 这些提交到 HEAD(补齐冻结面)后重跑`)
|
|
1513
|
+
console.error(` ② 确认放弃这些提交:清理对应分支/reflog 源后重跑`)
|
|
1514
|
+
console.error(` ③ 接受缺口留痕:flow done --change ${change} --accept-orphan-face`)
|
|
1515
|
+
reportMidFail('patch')
|
|
1516
|
+
process.exit(1)
|
|
1517
|
+
}
|
|
1518
|
+
if (orphan.files.length > 0 && acceptOrphanFace) {
|
|
1519
|
+
orphanGapCount = orphan.files.length
|
|
1520
|
+
console.warn(`⚠️ --accept-orphan-face:显式接受 ${orphan.commits.length} 个冻结面外提交的 ${orphan.files.length} 个交付文件不进冻结面(缺口数随 change-patch.json acceptedOrphanFace 留痕)`)
|
|
1521
|
+
}
|
|
1522
|
+
} catch { /* 守卫异常不阻断(fail-safe 不新造阻断面) */ }
|
|
1523
|
+
}
|
|
1439
1524
|
if (ownFiles.length === 0) {
|
|
1440
|
-
console.log('📦 patch
|
|
1525
|
+
console.log('📦 patch 留档跳过:本变更可归属文件面为空(无交付面改动——治理工件不入冻结面;若实现代码应在此,先提交后重跑)')
|
|
1441
1526
|
patchOk = true
|
|
1442
1527
|
} else {
|
|
1443
1528
|
const { buildFrozenPatch, collectNumstatByPath } = await import('./scope-audit.js')
|
|
1444
1529
|
// 双口径冻结(2026-09-28-split-guard-and-gate-report 评审 P2 清偿):交付面(committed)
|
|
1445
1530
|
// 用 baseRef..HEAD 提交区间 diff——共享主仓里并行会话对同文件的未提交 hunk 不再漏进
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1531
|
+
// 冻结件;独占树未提交交付(dirtyAdded)保留工作树口径(FR-01 后治理工件目录不再入冻,
|
|
1532
|
+
// 工作树口径集只剩 dirtyAdded)。重叠文件归提交区间,避免双 hunk。
|
|
1448
1533
|
const headCommit = String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null
|
|
1449
1534
|
const toPosix = (p) => String(p).replace(/\\/g, '/')
|
|
1450
|
-
|
|
1451
|
-
// diff 能捕捉提交后编辑)∪ 独占树未提交交付(dirtyAdded);其余交付面走提交区间。
|
|
1452
|
-
// 排除项与 ownFiles 同款(四名全排,含 scope-audit.* 旧轮冻结件 legacy 防护)。
|
|
1453
|
-
const dirFilesForPatch = changeDirFiles.filter((f) => !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
|
|
1454
|
-
&& !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
|
|
1455
|
-
const worktreeSet = new Set([...dirFilesForPatch, ...((freeze.dirtyAdded) || [])].map(toPosix))
|
|
1535
|
+
const worktreeSet = new Set([...((freeze.dirtyAdded) || [])].map(toPosix))
|
|
1456
1536
|
const committedFace = freeze.files.map(toPosix).filter((f) => !worktreeSet.has(f))
|
|
1457
1537
|
const frozenCommitted = buildFrozenPatch(cwd, committedFace, { baseRef: st.baseline_commit, headRef: headCommit })
|
|
1458
1538
|
const frozenDir = buildFrozenPatch(cwd, [...worktreeSet], { baseRef: st.baseline_commit })
|
|
@@ -1473,13 +1553,36 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1473
1553
|
// 范围,单一计算喂 console 与落盘两处。ownFiles 传冻结面(与 files[] 同口径——
|
|
1474
1554
|
// worktree/--freeze-dirty 场景 dirty 交付也计入模块命中;.sillyspec 治理面对账内部
|
|
1475
1555
|
// 滤除);三键恒在场(对账异常兜底空数组,评审 P3 清偿);best-effort 不阻断。
|
|
1476
|
-
|
|
1556
|
+
moduleScope = { modules: [], uncoveredDirs: [], moduleMaps: [] }
|
|
1477
1557
|
try {
|
|
1478
1558
|
const { reconcileModuleDocs } = await import('./flow-parity.js')
|
|
1479
1559
|
const rec = reconcileModuleDocs({ cwd, specBase, ownFiles, committedRaw })
|
|
1480
1560
|
if (rec.lines.length > 0) for (const l of rec.lines) console.log(l)
|
|
1481
1561
|
moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
|
|
1482
1562
|
} catch { /* 对账 best-effort(三键空数组兜底) */ }
|
|
1563
|
+
// 未登记目录软门(2026-10-12-thin-module-gate-and-feed 一档):拦在 distill 之前——
|
|
1564
|
+
// indexRequirements 的域路由发生在 distill,拦在前面 FR 才不落伪域。拦但可签收:登记/
|
|
1565
|
+
// --accept-unmapped(缺口数留痕)/ module_gate_ignore 固定忽略三条出路;绿地无图不拦。
|
|
1566
|
+
// 门消费的是本子步刚算的对账面(与收口留痕同时点口径),失败在 mark('patch') 前——
|
|
1567
|
+
// 重入 done 从 patch 断点续,登记后重算自然通过。
|
|
1568
|
+
let moduleGate = { block: false, dirs: [], maps: [] }
|
|
1569
|
+
try {
|
|
1570
|
+
const { evaluateModuleGate } = await import('./flow-parity.js')
|
|
1571
|
+
moduleGate = evaluateModuleGate({ moduleScope, ignore: readFlowConfig(specBase).moduleGateIgnore })
|
|
1572
|
+
} catch { /* 门判定异常不拦(对账面空 → 无可拦事实,fail-open 同对账口径) */ }
|
|
1573
|
+
if (moduleGate.block && !acceptUnmapped) {
|
|
1574
|
+
const dirList = moduleGate.dirs.slice(0, 5).map((d) => `${d.dir}(${d.files} 文件)`).join('、') + (moduleGate.dirs.length > 5 ? ' 等' : '')
|
|
1575
|
+
console.error(`❌ 模块图未登记交付目录(门):${dirList}——共 ${moduleGate.dirs.length} 个目录不在任何 _module-map.yaml 的 paths 下`)
|
|
1576
|
+
console.error(' 未登记后果是静默的:FR 蒸馏将落伪域 auto-*、知识图谱模块侧边(module-files/describes/changelog-of)断供、模块上下文注入绕开这些目录。')
|
|
1577
|
+
console.error(` ① 登记后重跑:在 docs/<project>/modules/_module-map.yaml 增补 modules.<id>(paths 指向该目录,可带 doc: modules/<id>.md)后重跑 flow done`)
|
|
1578
|
+
console.error(` ② 确认无需登记:flow done --change ${change} --accept-unmapped(缺口数随 change-patch.json acceptedUnmappedDirs 留痕)`)
|
|
1579
|
+
console.error(' ③ 固定忽略(合法未登记面:根级脚本/配置等):.sillyspec/local.yaml 加 module_gate_ignore: <目录>(单行逗号分隔,前缀匹配)')
|
|
1580
|
+
reportMidFail('patch')
|
|
1581
|
+
process.exit(1)
|
|
1582
|
+
}
|
|
1583
|
+
if (moduleGate.block && acceptUnmapped) {
|
|
1584
|
+
console.warn(`⚠️ --accept-unmapped:显式接受 ${moduleGate.dirs.length} 个未登记目录不进模块图(缺口数随 change-patch.json acceptedUnmappedDirs 留痕)`)
|
|
1585
|
+
}
|
|
1483
1586
|
// 收口留痕单套写(2026-10-09-close-trace-single-set):共用 writeCloseTraceArtifacts
|
|
1484
1587
|
// 只写 change.patch + change-patch.json 两件——对账面数据并入 JSON 的 scopeAudit
|
|
1485
1588
|
// 子对象(thin 通道同样有对账面),旧名 scope-audit.* 停写、读侧兼容兜存量归档。
|
|
@@ -1534,13 +1637,18 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1534
1637
|
...(crossRepos.length > 0 ? { repos: crossRepos } : {}),
|
|
1535
1638
|
},
|
|
1536
1639
|
meta: {
|
|
1537
|
-
note: 'flow done
|
|
1640
|
+
note: 'flow done 时点冻结(本变更可归属交付面:baseline..HEAD 提交面过滤 .sillyspec/ 只留 .sillyspec/docs/ 交付文档 + 独占树未提交交付;治理工件目录不入冻结——2026-10-12-close-trace-freeze-integrity FR-01)',
|
|
1538
1641
|
// 模块对账结构化面(2026-09-27-thin-module-scope-persist):done 时点口径
|
|
1539
1642
|
// (与 files[] 同时点语义,不随模块图后续变更回写);modules[] 空≠无影响
|
|
1540
1643
|
...moduleScope,
|
|
1541
1644
|
// dirty 缺口显式接受留痕(2026-10-08-thin-done-dirty-gate-and-paren-attribution):
|
|
1542
1645
|
// --accept-dirty-gap 放行时缺口数入档(审计面可见,非只 console)
|
|
1543
1646
|
...(acceptDirtyGap && freeze.dirtyWarned.length > 0 ? { acceptedDirtyGap: freeze.dirtyWarned.length } : {}),
|
|
1647
|
+
// 未登记目录签收留痕(2026-10-12-thin-module-gate-and-feed 一档):同款显式接受语义
|
|
1648
|
+
...(acceptUnmapped && moduleGate.dirs.length > 0 ? { acceptedUnmappedDirs: moduleGate.dirs.length } : {}),
|
|
1649
|
+
// 孤儿面缺口显式接受留痕(2026-10-12-close-trace-freeze-integrity FR-02):
|
|
1650
|
+
// --accept-orphan-face 放行时缺口数入档
|
|
1651
|
+
...(acceptOrphanFace && orphanGapCount > 0 ? { acceptedOrphanFace: orphanGapCount } : {}),
|
|
1544
1652
|
},
|
|
1545
1653
|
})
|
|
1546
1654
|
console.log(`📦 变更 patch 留档(单套):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions},对账面并入 scopeAudit 子对象)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
|
|
@@ -1608,7 +1716,16 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1608
1716
|
let taskbookHead = null
|
|
1609
1717
|
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 */ }
|
|
1610
1718
|
console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,起一个干净上下文的子代理执行后重跑本命令:\n`)
|
|
1611
|
-
|
|
1719
|
+
// 项目评审探针装配(2026-10-12-review-probe-injection):local.yaml 数据驱动的项目特有
|
|
1720
|
+
// 必查面注入任务书(未配置零注入;装载告警/帽留痕 best-effort 提示,不阻断渲染)。
|
|
1721
|
+
let _probeLines = null
|
|
1722
|
+
try {
|
|
1723
|
+
const { assembleProbeLinesForScope } = await import('./review-probes.js')
|
|
1724
|
+
const _assembled = assembleProbeLinesForScope(specBase, 'thin')
|
|
1725
|
+
for (const _w of _assembled.warnings) console.warn(`⚠️ review_probes:${_w}`)
|
|
1726
|
+
if (_assembled.lines.length > 0) _probeLines = _assembled.lines
|
|
1727
|
+
} catch { /* 探针面 fail-soft:装配失败零注入 */ }
|
|
1728
|
+
console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines }))
|
|
1612
1729
|
reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
|
|
1613
1730
|
appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
|
|
1614
1731
|
reportMidFail('review')
|
|
@@ -1719,6 +1836,20 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1719
1836
|
mark('probes')
|
|
1720
1837
|
}
|
|
1721
1838
|
|
|
1839
|
+
// 模块对账面断点续跑回读(2026-10-12-thin-module-gate-and-feed 二档):patch 子步已 done 的
|
|
1840
|
+
// 重入轮次上 moduleScope 为空(进程内变量不跨轮)——从 change-patch.json files[] 回读重算
|
|
1841
|
+
// (distill 回写与 archive 前的 extraDocs 推导共用;files[] 与冻结面同时点口径)。
|
|
1842
|
+
if (!moduleScope) {
|
|
1843
|
+
try {
|
|
1844
|
+
const pm = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
|
|
1845
|
+
const filesArr = Array.isArray(pm && pm.files) ? pm.files : []
|
|
1846
|
+
if (filesArr.length > 0) {
|
|
1847
|
+
const { reconcileModuleDocs } = await import('./flow-parity.js')
|
|
1848
|
+
const rec = reconcileModuleDocs({ cwd, specBase, ownFiles: filesArr, committedRaw: [] })
|
|
1849
|
+
moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
|
|
1850
|
+
}
|
|
1851
|
+
} catch { /* 回读失败 → 空面(回写零命中静默) */ }
|
|
1852
|
+
}
|
|
1722
1853
|
// ④ distill:决策提炼(rejected/needsWait 异态 → 升厚留人工裁决,不静默吞)+ FR 索引提炼
|
|
1723
1854
|
// (2026-09-22-thin-fr-distill-sync:轻量变更此前只蒸馏 decisions 不调 indexRequirements——
|
|
1724
1855
|
// requirements 永不进 knowledge/fr,知识复利在新默认道断流;轻量变更无 design.md,域路由
|
|
@@ -1731,6 +1862,21 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1731
1862
|
const h = harvestSlot4Decision({ changeDir, change })
|
|
1732
1863
|
if (h.harvested) console.log(`🌱 槽4(风险与死路)收割 → decisions.md(底稿——作答里的放弃方案请拆成 rejected 条目再收口;随蒸馏链进 knowledge)`)
|
|
1733
1864
|
} catch { /* 收割 best-effort */ }
|
|
1865
|
+
// 模块变更索引机械回写(2026-10-12-thin-module-gate-and-feed 二档):命中模块的卡/sidecar
|
|
1866
|
+
// 追加一行本变更条目——零语义判断(日期/变更名/动机首行全已知),split-changelog 契约 sidecar
|
|
1867
|
+
// 优先、卡内「## 变更索引」节次之、变更名在场即跳过(断点续跑幂等)。moduleScope 由上方
|
|
1868
|
+
// 回读块保证在场(patch 本轮算出或 change-patch.json 回读)。
|
|
1869
|
+
try {
|
|
1870
|
+
if (moduleScope && Array.isArray(moduleScope.modules) && moduleScope.modules.length > 0) {
|
|
1871
|
+
const { appendChangeIndexEntries } = await import('./flow-parity.js')
|
|
1872
|
+
const motive = String(st.input || '').split('\n').map((s) => s.trim()).find(Boolean) || ''
|
|
1873
|
+
const wr = appendChangeIndexEntries({ specBase, modules: moduleScope.modules, change, summary: motive, date: new Date().toISOString().slice(0, 10) })
|
|
1874
|
+
changelogAppended = wr.appended
|
|
1875
|
+
if (wr.appended.length > 0) console.log(`📖 模块变更索引回写:${wr.appended.length} 个目标(${wr.appended.join('、')})——sidecar 优先/卡内节次之,随归档窄化入暂存`)
|
|
1876
|
+
const skippedReal = wr.skipped.filter((s) => !/已记录/.test(s.reason))
|
|
1877
|
+
if (skippedReal.length > 0) console.log(` ↩️ 跳过 ${skippedReal.length} 个(${skippedReal.slice(0, 3).map((s) => `${s.id}:${s.reason}`).join(';')}${skippedReal.length > 3 ? ' 等' : ''})`)
|
|
1878
|
+
}
|
|
1879
|
+
} catch (e) { console.warn(`⚠️ 变更索引回写失败(fail-soft 不阻断归档):${(e && e.message) || e}`) }
|
|
1734
1880
|
try {
|
|
1735
1881
|
const { distillIntoKnowledge } = await import('./decision-distill.js')
|
|
1736
1882
|
const knowledgeRoot = join(specBase, 'knowledge')
|
|
@@ -1803,6 +1949,16 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1803
1949
|
} catch (e) { console.warn(`⚠️ 回执合成失败(不阻断归档):${(e && e.message) || e}`) }
|
|
1804
1950
|
const { ProgressManager } = await import('./progress.js')
|
|
1805
1951
|
const { runArchiveChain } = await import('./run/complete-handlers.js')
|
|
1952
|
+
// extraDocs 断点续跑推导(评审 P3 清偿 2026-10-12-thin-module-gate-and-feed):distill 已
|
|
1953
|
+
// done 的重入轮次上 changelogAppended 为空(进程内变量不跨轮)——从命中模块的卡/sidecar
|
|
1954
|
+
// 反查含本变更名的目标文件(幂等事实,判据与回写去重同款 text.includes(change)),
|
|
1955
|
+
// extraDocs 精确入暂存不靠目录级回退兜底。
|
|
1956
|
+
if (changelogAppended.length === 0 && moduleScope && Array.isArray(moduleScope.modules) && moduleScope.modules.length > 0) {
|
|
1957
|
+
try {
|
|
1958
|
+
const { deriveRecordedChangeIndexTargets } = await import('./flow-parity.js')
|
|
1959
|
+
changelogAppended = deriveRecordedChangeIndexTargets({ specBase, modules: moduleScope.modules, change })
|
|
1960
|
+
} catch { /* 推导失败 → extraDocs 空(目录级回退兜底) */ }
|
|
1961
|
+
}
|
|
1806
1962
|
// 锚定同 start(平台参数面)
|
|
1807
1963
|
const { archiveDestDirName } = await import('./stage-contract.js')
|
|
1808
1964
|
const pm = new ProgressManager()
|
|
@@ -1814,6 +1970,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1814
1970
|
srcDir: changeDir,
|
|
1815
1971
|
destDir,
|
|
1816
1972
|
skipPlanCheck: (st.born_face ?? st.tier) === 'thin',
|
|
1973
|
+
// 二档回写文件随归档窄化精确入暂存(文件级 pathspec;空数组传 null 走既有零行为)
|
|
1974
|
+
extraDocs: changelogAppended.length > 0 ? changelogAppended.map((p) => join(specBase, p)) : null,
|
|
1817
1975
|
})
|
|
1818
1976
|
// 归档后 changeDir 已搬走——后续子步标记随归档目录走(flow-state 随变更包留存审计)
|
|
1819
1977
|
markDir = destDir
|
|
@@ -1865,17 +2023,34 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1865
2023
|
/** flow 命令族入口(index.js case 'flow' 接线):flow start|done|amend-draft。 */
|
|
1866
2024
|
/**
|
|
1867
2025
|
* flow approve(2026-10-04-thin-docs-v2 FR-06):spec 断点用户批准留痕——用户确认 FR+design
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
1870
|
-
*
|
|
1871
|
-
*
|
|
2026
|
+
* 方案后运行;写 flow-state {spec_approved,spec_approved_at,spec_approved_by}。flow done 的
|
|
2027
|
+
* v2 机器门消费该证据(未批准且未声明 autopilot → 拒收)。
|
|
2028
|
+
*
|
|
2029
|
+
* 通道 attest(2026-10-12-approve-channel-attest,dogfood 实证驱动:8 次 approve 中 2 次为
|
|
2030
|
+
* agent 自批、其中一次在用户明确拒绝方案后把重复催促反读为授权,且留痕层面用户真批转述与
|
|
2031
|
+
* agent 自批不可区分——spec_approved_by 恒为 agent session id):
|
|
2032
|
+
* --user-confirmed agent 转述「用户已确认方案」(channel=user-confirmed)
|
|
2033
|
+
* --self-approved agent 自批如实声明(channel=agent-self)
|
|
2034
|
+
* 无 flag 默认路径行为不变(零破坏),channel=unattested 留痕+警示
|
|
2035
|
+
* 拒后派生授权硬拦:spec_rejected 在场且未被显式翻越(无 spec_rejected_superseded_at)时,
|
|
2036
|
+
* approve 必须带显式通道——「用户拒绝过方案」之后的无通道批准正是实证事故形态。
|
|
2037
|
+
* 诚实边界:同机 agent 仍可谎报 --user-confirmed(与 --by 同界)——通道的价值是必留痕+
|
|
2038
|
+
* 可区分+评审抽查兜底,非防伪;强凭据属平台层课题(CLI 直连用户问询),不在此层伪造。
|
|
1872
2039
|
*/
|
|
1873
|
-
async function cmdFlowApprove({ change, cwd, specBase, by = null }) {
|
|
2040
|
+
async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag = null }) {
|
|
1874
2041
|
const changeDir = join(specBase, 'changes', change)
|
|
1875
2042
|
if (!existsSync(changeDir)) {
|
|
1876
2043
|
console.error(`❌ 变更不存在:${change}(flow approve 在 flow start 之后运行)`)
|
|
1877
2044
|
process.exit(2)
|
|
1878
2045
|
}
|
|
2046
|
+
const st = readFlowState(changeDir) || {}
|
|
2047
|
+
if (st.spec_rejected && !st.spec_rejected_superseded_at && !channelFlag) {
|
|
2048
|
+
console.error(`❌ 本变更存在未翻越的拒绝记录(spec_rejected,reason=${String(st.spec_rejected_reason || '?').slice(0, 80)})——拒绝后的批准必须显式声明通道:`)
|
|
2049
|
+
console.error(` 用户重新确认后:sillyspec flow approve --change ${change} --user-confirmed(agent 转述用户确认)`)
|
|
2050
|
+
console.error(` agent 自批如实声明:sillyspec flow approve --change ${change} --self-approved`)
|
|
2051
|
+
console.error(` (拒绝后派生授权须显式再确认——2026-10-12-approve-channel-attest)`)
|
|
2052
|
+
process.exit(2)
|
|
2053
|
+
}
|
|
1879
2054
|
let approvedBy = by
|
|
1880
2055
|
if (!approvedBy) {
|
|
1881
2056
|
try {
|
|
@@ -1883,11 +2058,64 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null }) {
|
|
|
1883
2058
|
approvedBy = (resolveSessionIdentity({ cwd }) || {}).session || 'user'
|
|
1884
2059
|
} catch { approvedBy = 'user' }
|
|
1885
2060
|
}
|
|
2061
|
+
const channel = channelFlag === 'user-confirmed' ? 'user-confirmed'
|
|
2062
|
+
: channelFlag === 'self-approved' ? 'agent-self' : 'unattested'
|
|
1886
2063
|
const at = new Date().toISOString()
|
|
1887
|
-
|
|
2064
|
+
const patch = {
|
|
2065
|
+
spec_approved: true, spec_approved_at: at, spec_approved_by: approvedBy,
|
|
2066
|
+
spec_approved_channel: channel,
|
|
2067
|
+
}
|
|
2068
|
+
if (channelFlag) patch.spec_attested_by = approvedBy
|
|
2069
|
+
else patch.spec_attested_by = null // 评审 P3① 清偿:无通道再批准清除旧声明者,防 unattested 态残留陈旧 attest
|
|
2070
|
+
if (st.spec_rejected && !st.spec_rejected_superseded_at) patch.spec_rejected_superseded_at = at
|
|
2071
|
+
writeFlowState(changeDir, patch)
|
|
1888
2072
|
console.log(`✅ spec 断点已批准(change=${change})`)
|
|
1889
|
-
|
|
1890
|
-
|
|
2073
|
+
const chLabel = channelFlag === 'user-confirmed' ? 'user-confirmed(agent 转述用户确认)'
|
|
2074
|
+
: channelFlag === 'self-approved' ? 'agent-self(agent 自批如实声明)' : 'unattested(未声明通道)'
|
|
2075
|
+
console.log(` 批准人:${approvedBy}|时间:${at}|通道:${chLabel}`)
|
|
2076
|
+
if (!channelFlag) {
|
|
2077
|
+
console.warn(`⚠️ 本次批准未带 attest 通道(--user-confirmed / --self-approved)——按 unattested 留痕,评审/审计无法区分用户确认与 agent 自批。`)
|
|
2078
|
+
}
|
|
2079
|
+
if (channelFlag === 'self-approved') {
|
|
2080
|
+
console.warn(`⚠️ agent 自批留痕:如用户实际未确认,请撤销——sillyspec flow reject --change ${change} --reason <理由>`)
|
|
2081
|
+
}
|
|
2082
|
+
if (patch.spec_rejected_superseded_at) console.log(` 已翻越既有拒绝记录(superseded_at=${patch.spec_rejected_superseded_at},拒绝理由保留于 spec_rejected_reason)`)
|
|
2083
|
+
console.log(` flow done 的断点机器门已满足;agent 可继续执行/收口。撤销:sillyspec flow reject --change ${change} --reason <理由>`)
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
/**
|
|
2087
|
+
* flow reject(2026-10-12-approve-channel-attest):spec 断点拒绝留痕——撤销既有批准并记录
|
|
2088
|
+
* 拒绝理由(审计可复原「批准→用户否掉→显式再确认」序);此后 approve 必须带显式 attest 通道
|
|
2089
|
+
* (防「拒绝后派生授权」——dogfood 实证:用户拒绝方案后 agent 把重复催促反读为授权自批)。
|
|
2090
|
+
* spec_approved 撤销为显式 false(非删键)——重入读侧恒见;spec_rejected* 历史字段保留不删。
|
|
2091
|
+
*/
|
|
2092
|
+
async function cmdFlowReject({ change, cwd, specBase, reason }) {
|
|
2093
|
+
const changeDir = join(specBase, 'changes', change)
|
|
2094
|
+
if (!existsSync(changeDir)) {
|
|
2095
|
+
console.error(`❌ 变更不存在:${change}(flow reject 在 flow start 之后运行)`)
|
|
2096
|
+
process.exit(2)
|
|
2097
|
+
}
|
|
2098
|
+
const reasonText = String(reason || '').trim()
|
|
2099
|
+
if (!reasonText) {
|
|
2100
|
+
console.error('❌ flow reject 需要 --reason <文本>(拒绝理由,留痕供审计)')
|
|
2101
|
+
process.exit(2)
|
|
2102
|
+
}
|
|
2103
|
+
let rejectedBy = 'user'
|
|
2104
|
+
try {
|
|
2105
|
+
const { resolveSessionIdentity } = await import('./progress.js')
|
|
2106
|
+
rejectedBy = (resolveSessionIdentity({ cwd }) || {}).session || 'user'
|
|
2107
|
+
} catch { /* 缺省 user */ }
|
|
2108
|
+
const at = new Date().toISOString()
|
|
2109
|
+
writeFlowState(changeDir, {
|
|
2110
|
+
spec_approved: false,
|
|
2111
|
+
spec_rejected: true, spec_rejected_at: at,
|
|
2112
|
+
spec_rejected_reason: reasonText, spec_rejected_by: rejectedBy,
|
|
2113
|
+
spec_rejected_superseded_at: null, // 再拒绝重置翻越戳(上一轮「拒→显式确认」序已被新拒绝终结)
|
|
2114
|
+
})
|
|
2115
|
+
console.log(`🚫 spec 断点已拒绝留痕(change=${change})`)
|
|
2116
|
+
console.log(` 理由:${reasonText}|操作者:${rejectedBy}|时间:${at}`)
|
|
2117
|
+
console.log(` 既有批准已撤销(spec_approved=false)——flow done 断点机器门将重新拦收。`)
|
|
2118
|
+
console.log(` 用户重新确认后批准须显式通道:flow approve --change ${change} --user-confirmed(agent 自批则 --self-approved 如实声明)`)
|
|
1891
2119
|
}
|
|
1892
2120
|
|
|
1893
2121
|
export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
@@ -1991,8 +2219,22 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
|
1991
2219
|
const change = getFlag('--change')
|
|
1992
2220
|
if (!change) { console.error('❌ flow approve 需 --change <名>'); process.exit(2) }
|
|
1993
2221
|
validateChangeName(change)
|
|
2222
|
+
// attest 通道(2026-10-12-approve-channel-attest):互斥二选一,无 flag=unattested(留痕+警示)
|
|
2223
|
+
const userConfirmed = hasFlag('--user-confirmed')
|
|
2224
|
+
const selfApproved = hasFlag('--self-approved')
|
|
2225
|
+
if (userConfirmed && selfApproved) {
|
|
2226
|
+
console.error('❌ --user-confirmed 与 --self-approved 互斥(一次只能声明一个 attest 通道)')
|
|
2227
|
+
process.exit(2)
|
|
2228
|
+
}
|
|
2229
|
+
await reportAgentLog(change, sub)
|
|
2230
|
+
return cmdFlowApprove({ change, cwd, specBase, by: getFlag('--by') || null, channelFlag: userConfirmed ? 'user-confirmed' : (selfApproved ? 'self-approved' : null) })
|
|
2231
|
+
}
|
|
2232
|
+
if (sub === 'reject') {
|
|
2233
|
+
const change = getFlag('--change')
|
|
2234
|
+
if (!change) { console.error('❌ flow reject 需 --change <名>'); process.exit(2) }
|
|
2235
|
+
validateChangeName(change)
|
|
1994
2236
|
await reportAgentLog(change, sub)
|
|
1995
|
-
return
|
|
2237
|
+
return cmdFlowReject({ change, cwd, specBase, reason: getFlag('--reason') })
|
|
1996
2238
|
}
|
|
1997
2239
|
if (sub === 'status') {
|
|
1998
2240
|
// 三断点配套(2026-09-25-flow-checkpoints):随时可查当前变更阶段/槽位/子步进度
|
|
@@ -2138,7 +2380,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
|
2138
2380
|
console.log('🔄 --refreeze:patch 子步标记已重置,本次 done 将重新冻结(change.patch 按 baseline..HEAD 最新面重建)')
|
|
2139
2381
|
}
|
|
2140
2382
|
}
|
|
2141
|
-
return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty'), acceptDirtyGap: hasFlag('--accept-dirty-gap'), allowBatchTick: hasFlag('--allow-batch-tick') })
|
|
2383
|
+
return cmdFlowDone({ change, cwd, specBase, runtimeRootOpt, freezeDirty: hasFlag('--freeze-dirty'), acceptDirtyGap: hasFlag('--accept-dirty-gap'), acceptOrphanFace: hasFlag('--accept-orphan-face'), allowBatchTick: hasFlag('--allow-batch-tick'), acceptUnmapped: hasFlag('--accept-unmapped') })
|
|
2142
2384
|
}
|
|
2143
2385
|
if (sub === 'amend-draft') {
|
|
2144
2386
|
// 机器稿唯一留痕修改通道(R7 切片三 / FR-08):重锚哈希 + ledger amendment 审计;
|