sillyspec 3.32.6 → 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 +64 -8
- package/src/flow.js +277 -45
- package/src/index.js +94 -1
- package/src/local-register.js +20 -1
- 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/`)
|
|
@@ -792,7 +817,10 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
|
|
|
792
817
|
`🛑 三断点纪律(可控性要求——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
|
|
793
818
|
` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
|
|
794
819
|
` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
|
|
795
|
-
`
|
|
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 <理由>(拒后批准须显式通道再确认)。`,
|
|
796
824
|
` ② 执行断点:写完代码跑完测试后,把测试结果(过了几个/挂了什么)给用户看,`,
|
|
797
825
|
` 等用户确认再跑收口。`,
|
|
798
826
|
` ③ 归档断点:flow done 跑完后(无论过/拒),把结果(归档成功/被什么拦了)给用户看。`,
|
|
@@ -872,7 +900,7 @@ export function remainingSubstepsAtFail({ snapshotSubsteps = {}, doneList = [],
|
|
|
872
900
|
return SUBSTEPS.filter((k) => k !== failed && snapshotSubsteps?.[k] !== 'done' && !doneNow.has(k))
|
|
873
901
|
}
|
|
874
902
|
|
|
875
|
-
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 }) {
|
|
876
904
|
const changeDir = join(specBase, 'changes', change)
|
|
877
905
|
const st = readFlowState(changeDir)
|
|
878
906
|
if (!st) {
|
|
@@ -906,6 +934,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
906
934
|
const skip = (k) => { doneList.push(`${k}(skip)`) }
|
|
907
935
|
let reviewOutcome = null
|
|
908
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 = []
|
|
909
941
|
// 遥测单点(2026-09-25-thin-parity-assets 修评审 P2①:失败路径 exit 前也要落账——校准信号
|
|
910
942
|
// 不许在失败面丢失;成功收口走函数末尾同款记录,失败面只多不少)
|
|
911
943
|
const appendTelemetry = (review) => {
|
|
@@ -914,6 +946,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
914
946
|
ts: new Date().toISOString(), change, protocolCalls: 2,
|
|
915
947
|
draftAmendments: st.edit_ratio != null ? 1 : 0, editRatio: st.edit_ratio ?? null,
|
|
916
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),
|
|
917
952
|
review,
|
|
918
953
|
}) + '\n', 'utf8')
|
|
919
954
|
} catch { /* 遥测 best-effort */ }
|
|
@@ -946,11 +981,27 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
946
981
|
if (!st.autopilot && !st.spec_approved) {
|
|
947
982
|
console.error(`❌ spec 断点未批准:本变更(v2 起草)收口需要方案确认留痕`)
|
|
948
983
|
console.error(` 修复:把 FR 条目+design 四节摘要给用户看,用户确认后由用户运行:`)
|
|
949
|
-
console.error(` sillyspec flow approve --change ${change}`)
|
|
984
|
+
console.error(` sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
|
|
950
985
|
console.error(` 或用户显式豁免(自主跑到底):重跑 flow start --change ${change} --autopilot(声明留痕)`)
|
|
951
986
|
reportMidFail('artifacts')
|
|
952
987
|
process.exit(1)
|
|
953
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
|
+
}
|
|
954
1005
|
} else {
|
|
955
1006
|
if (r.applicable && r.violations.length > 0) {
|
|
956
1007
|
console.error(`❌ 工件校验拒收(机器稿指纹三态):`)
|
|
@@ -1046,6 +1097,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1046
1097
|
// token 合法形态写明(2026-10-11-flow-start-redline-frontload):实证「task-01..task-05」
|
|
1047
1098
|
// 区间写法被判零证据时才知道格式约束——拒收文案自释,减少一轮试错。
|
|
1048
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 补证。')
|
|
1049
1104
|
appendTelemetry({ sentinel: 'fake', missing: sent.missing.length })
|
|
1050
1105
|
reportMidFail('ledger')
|
|
1051
1106
|
process.exit(1)
|
|
@@ -1286,10 +1341,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1286
1341
|
// 治理面等价保留(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop
|
|
1287
1342
|
// 用户实证):评审结论的对象是交付面——窗口内本变更提交只碰 .sillyspec/** 治理文件且
|
|
1288
1343
|
// 未触本变更 requirements/design(承诺面也是评审对象)时,结论对最新交付面逐字节等价,
|
|
1289
|
-
// 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze
|
|
1344
|
+
// 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结(FR-01 后治理
|
|
1345
|
+
// 工件不入冻结面——治理修复的留档真相=归档目录本体文件,重冻结保交付面时点对齐)
|
|
1290
1346
|
// (审计件覆盖最新提交面不变量不破)。此前任何后缀提交一律隔离重评——评审后修一个
|
|
1291
1347
|
// P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
|
|
1292
|
-
console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch
|
|
1348
|
+
console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结(治理工件不入冻结面,治理修复随归档目录本体留档)`)
|
|
1293
1349
|
} else {
|
|
1294
1350
|
const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
|
|
1295
1351
|
try {
|
|
@@ -1318,11 +1374,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1318
1374
|
}
|
|
1319
1375
|
if (st.substeps?.patch !== 'done' || driftRefreeze) {
|
|
1320
1376
|
let patchOk = false
|
|
1377
|
+
let orphanGapCount = 0
|
|
1321
1378
|
try {
|
|
1322
|
-
// patch 面 =
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
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
|
+
// 声明切分(他侧同窗口提交的防线)。
|
|
1326
1385
|
if (!st.baseline_commit) {
|
|
1327
1386
|
console.log('📦 patch 留档跳过:无 git 基线(无历史仓)')
|
|
1328
1387
|
patchOk = true
|
|
@@ -1383,18 +1442,6 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1383
1442
|
if (dirtyForeign.length > 0) {
|
|
1384
1443
|
console.warn(`⚠️ patch 留档:${dirtyForeign.length} 个 dirty 文件被其他活跃变更声明排除(${dirtyForeign.slice(0, 3).map((x) => `${x.file}←${x.owners[0]}`).join(', ')}${dirtyForeign.length > 3 ? ' 等' : ''})——如有误(陈旧声明),清理该旧变更或用 --refreeze 重冻结`)
|
|
1385
1444
|
}
|
|
1386
|
-
const changeDirFiles = []
|
|
1387
|
-
const walk = (dir) => {
|
|
1388
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
1389
|
-
// flow-state.yaml 是运行态(机器写、冻结后仍随子步推进变化)——不入审计 patch
|
|
1390
|
-
//(2026-09-27-tool-debt-cleanup:旧口径把它当 new file 冻进 patch,评审 P2)
|
|
1391
|
-
if (e.name === 'flow-state.yaml') continue
|
|
1392
|
-
const p = join(dir, e.name)
|
|
1393
|
-
if (e.isDirectory()) walk(p)
|
|
1394
|
-
else changeDirFiles.push(relative(cwd, p).replace(/\\/g, '/'))
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
try { walk(changeDir) } catch { /* 目录异常=空面 */ }
|
|
1398
1445
|
// 冻结面收集(2026-09-25-thin-r16-patches 修复①):committed 为主;会话专属 worktree
|
|
1399
1446
|
// (gate-snapshot 同款路径判定)下未提交 dirty 交付面一并入冻结——独占树内全归属本变更;
|
|
1400
1447
|
// 共享主仓 dirty 无法归属只警告(他侧声明免警告),审计缺口显式化不再静默。
|
|
@@ -1443,26 +1490,49 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1443
1490
|
// change-patch.json 两件,但旧轮冻结件里可能仍有 scope-audit.json/.patch——
|
|
1444
1491
|
// legacy 防护保留):上一轮留痕件作为 untracked 新文件被全文自嵌入即自引用循环
|
|
1445
1492
|
// (旧 patch 内容混进新 patch,三评 P1 根因)。
|
|
1446
|
-
const ownFiles = [...new Set([...freeze.files
|
|
1493
|
+
const ownFiles = [...new Set([...freeze.files])]
|
|
1447
1494
|
.filter((f) => f && !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
|
|
1448
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
|
+
}
|
|
1449
1524
|
if (ownFiles.length === 0) {
|
|
1450
|
-
console.log('📦 patch
|
|
1525
|
+
console.log('📦 patch 留档跳过:本变更可归属文件面为空(无交付面改动——治理工件不入冻结面;若实现代码应在此,先提交后重跑)')
|
|
1451
1526
|
patchOk = true
|
|
1452
1527
|
} else {
|
|
1453
1528
|
const { buildFrozenPatch, collectNumstatByPath } = await import('./scope-audit.js')
|
|
1454
1529
|
// 双口径冻结(2026-09-28-split-guard-and-gate-report 评审 P2 清偿):交付面(committed)
|
|
1455
1530
|
// 用 baseRef..HEAD 提交区间 diff——共享主仓里并行会话对同文件的未提交 hunk 不再漏进
|
|
1456
|
-
//
|
|
1457
|
-
//
|
|
1531
|
+
// 冻结件;独占树未提交交付(dirtyAdded)保留工作树口径(FR-01 后治理工件目录不再入冻,
|
|
1532
|
+
// 工作树口径集只剩 dirtyAdded)。重叠文件归提交区间,避免双 hunk。
|
|
1458
1533
|
const headCommit = String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null
|
|
1459
1534
|
const toPosix = (p) => String(p).replace(/\\/g, '/')
|
|
1460
|
-
|
|
1461
|
-
// diff 能捕捉提交后编辑)∪ 独占树未提交交付(dirtyAdded);其余交付面走提交区间。
|
|
1462
|
-
// 排除项与 ownFiles 同款(四名全排,含 scope-audit.* 旧轮冻结件 legacy 防护)。
|
|
1463
|
-
const dirFilesForPatch = changeDirFiles.filter((f) => !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
|
|
1464
|
-
&& !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
|
|
1465
|
-
const worktreeSet = new Set([...dirFilesForPatch, ...((freeze.dirtyAdded) || [])].map(toPosix))
|
|
1535
|
+
const worktreeSet = new Set([...((freeze.dirtyAdded) || [])].map(toPosix))
|
|
1466
1536
|
const committedFace = freeze.files.map(toPosix).filter((f) => !worktreeSet.has(f))
|
|
1467
1537
|
const frozenCommitted = buildFrozenPatch(cwd, committedFace, { baseRef: st.baseline_commit, headRef: headCommit })
|
|
1468
1538
|
const frozenDir = buildFrozenPatch(cwd, [...worktreeSet], { baseRef: st.baseline_commit })
|
|
@@ -1483,13 +1553,36 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1483
1553
|
// 范围,单一计算喂 console 与落盘两处。ownFiles 传冻结面(与 files[] 同口径——
|
|
1484
1554
|
// worktree/--freeze-dirty 场景 dirty 交付也计入模块命中;.sillyspec 治理面对账内部
|
|
1485
1555
|
// 滤除);三键恒在场(对账异常兜底空数组,评审 P3 清偿);best-effort 不阻断。
|
|
1486
|
-
|
|
1556
|
+
moduleScope = { modules: [], uncoveredDirs: [], moduleMaps: [] }
|
|
1487
1557
|
try {
|
|
1488
1558
|
const { reconcileModuleDocs } = await import('./flow-parity.js')
|
|
1489
1559
|
const rec = reconcileModuleDocs({ cwd, specBase, ownFiles, committedRaw })
|
|
1490
1560
|
if (rec.lines.length > 0) for (const l of rec.lines) console.log(l)
|
|
1491
1561
|
moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
|
|
1492
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
|
+
}
|
|
1493
1586
|
// 收口留痕单套写(2026-10-09-close-trace-single-set):共用 writeCloseTraceArtifacts
|
|
1494
1587
|
// 只写 change.patch + change-patch.json 两件——对账面数据并入 JSON 的 scopeAudit
|
|
1495
1588
|
// 子对象(thin 通道同样有对账面),旧名 scope-audit.* 停写、读侧兼容兜存量归档。
|
|
@@ -1544,13 +1637,18 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1544
1637
|
...(crossRepos.length > 0 ? { repos: crossRepos } : {}),
|
|
1545
1638
|
},
|
|
1546
1639
|
meta: {
|
|
1547
|
-
note: 'flow done
|
|
1640
|
+
note: 'flow done 时点冻结(本变更可归属交付面:baseline..HEAD 提交面过滤 .sillyspec/ 只留 .sillyspec/docs/ 交付文档 + 独占树未提交交付;治理工件目录不入冻结——2026-10-12-close-trace-freeze-integrity FR-01)',
|
|
1548
1641
|
// 模块对账结构化面(2026-09-27-thin-module-scope-persist):done 时点口径
|
|
1549
1642
|
// (与 files[] 同时点语义,不随模块图后续变更回写);modules[] 空≠无影响
|
|
1550
1643
|
...moduleScope,
|
|
1551
1644
|
// dirty 缺口显式接受留痕(2026-10-08-thin-done-dirty-gate-and-paren-attribution):
|
|
1552
1645
|
// --accept-dirty-gap 放行时缺口数入档(审计面可见,非只 console)
|
|
1553
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 } : {}),
|
|
1554
1652
|
},
|
|
1555
1653
|
})
|
|
1556
1654
|
console.log(`📦 变更 patch 留档(单套):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions},对账面并入 scopeAudit 子对象)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
|
|
@@ -1618,7 +1716,16 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1618
1716
|
let taskbookHead = null
|
|
1619
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 */ }
|
|
1620
1718
|
console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,起一个干净上下文的子代理执行后重跑本命令:\n`)
|
|
1621
|
-
|
|
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 }))
|
|
1622
1729
|
reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
|
|
1623
1730
|
appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
|
|
1624
1731
|
reportMidFail('review')
|
|
@@ -1729,6 +1836,20 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1729
1836
|
mark('probes')
|
|
1730
1837
|
}
|
|
1731
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
|
+
}
|
|
1732
1853
|
// ④ distill:决策提炼(rejected/needsWait 异态 → 升厚留人工裁决,不静默吞)+ FR 索引提炼
|
|
1733
1854
|
// (2026-09-22-thin-fr-distill-sync:轻量变更此前只蒸馏 decisions 不调 indexRequirements——
|
|
1734
1855
|
// requirements 永不进 knowledge/fr,知识复利在新默认道断流;轻量变更无 design.md,域路由
|
|
@@ -1741,6 +1862,21 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1741
1862
|
const h = harvestSlot4Decision({ changeDir, change })
|
|
1742
1863
|
if (h.harvested) console.log(`🌱 槽4(风险与死路)收割 → decisions.md(底稿——作答里的放弃方案请拆成 rejected 条目再收口;随蒸馏链进 knowledge)`)
|
|
1743
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}`) }
|
|
1744
1880
|
try {
|
|
1745
1881
|
const { distillIntoKnowledge } = await import('./decision-distill.js')
|
|
1746
1882
|
const knowledgeRoot = join(specBase, 'knowledge')
|
|
@@ -1813,6 +1949,16 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1813
1949
|
} catch (e) { console.warn(`⚠️ 回执合成失败(不阻断归档):${(e && e.message) || e}`) }
|
|
1814
1950
|
const { ProgressManager } = await import('./progress.js')
|
|
1815
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
|
+
}
|
|
1816
1962
|
// 锚定同 start(平台参数面)
|
|
1817
1963
|
const { archiveDestDirName } = await import('./stage-contract.js')
|
|
1818
1964
|
const pm = new ProgressManager()
|
|
@@ -1824,6 +1970,8 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1824
1970
|
srcDir: changeDir,
|
|
1825
1971
|
destDir,
|
|
1826
1972
|
skipPlanCheck: (st.born_face ?? st.tier) === 'thin',
|
|
1973
|
+
// 二档回写文件随归档窄化精确入暂存(文件级 pathspec;空数组传 null 走既有零行为)
|
|
1974
|
+
extraDocs: changelogAppended.length > 0 ? changelogAppended.map((p) => join(specBase, p)) : null,
|
|
1827
1975
|
})
|
|
1828
1976
|
// 归档后 changeDir 已搬走——后续子步标记随归档目录走(flow-state 随变更包留存审计)
|
|
1829
1977
|
markDir = destDir
|
|
@@ -1875,17 +2023,34 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1875
2023
|
/** flow 命令族入口(index.js case 'flow' 接线):flow start|done|amend-draft。 */
|
|
1876
2024
|
/**
|
|
1877
2025
|
* flow approve(2026-10-04-thin-docs-v2 FR-06):spec 断点用户批准留痕——用户确认 FR+design
|
|
1878
|
-
*
|
|
1879
|
-
*
|
|
1880
|
-
*
|
|
1881
|
-
*
|
|
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 直连用户问询),不在此层伪造。
|
|
1882
2039
|
*/
|
|
1883
|
-
async function cmdFlowApprove({ change, cwd, specBase, by = null }) {
|
|
2040
|
+
async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag = null }) {
|
|
1884
2041
|
const changeDir = join(specBase, 'changes', change)
|
|
1885
2042
|
if (!existsSync(changeDir)) {
|
|
1886
2043
|
console.error(`❌ 变更不存在:${change}(flow approve 在 flow start 之后运行)`)
|
|
1887
2044
|
process.exit(2)
|
|
1888
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
|
+
}
|
|
1889
2054
|
let approvedBy = by
|
|
1890
2055
|
if (!approvedBy) {
|
|
1891
2056
|
try {
|
|
@@ -1893,11 +2058,64 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null }) {
|
|
|
1893
2058
|
approvedBy = (resolveSessionIdentity({ cwd }) || {}).session || 'user'
|
|
1894
2059
|
} catch { approvedBy = 'user' }
|
|
1895
2060
|
}
|
|
2061
|
+
const channel = channelFlag === 'user-confirmed' ? 'user-confirmed'
|
|
2062
|
+
: channelFlag === 'self-approved' ? 'agent-self' : 'unattested'
|
|
1896
2063
|
const at = new Date().toISOString()
|
|
1897
|
-
|
|
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)
|
|
1898
2072
|
console.log(`✅ spec 断点已批准(change=${change})`)
|
|
1899
|
-
|
|
1900
|
-
|
|
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 如实声明)`)
|
|
1901
2119
|
}
|
|
1902
2120
|
|
|
1903
2121
|
export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
@@ -2001,8 +2219,22 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
|
2001
2219
|
const change = getFlag('--change')
|
|
2002
2220
|
if (!change) { console.error('❌ flow approve 需 --change <名>'); process.exit(2) }
|
|
2003
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)
|
|
2004
2236
|
await reportAgentLog(change, sub)
|
|
2005
|
-
return
|
|
2237
|
+
return cmdFlowReject({ change, cwd, specBase, reason: getFlag('--reason') })
|
|
2006
2238
|
}
|
|
2007
2239
|
if (sub === 'status') {
|
|
2008
2240
|
// 三断点配套(2026-09-25-flow-checkpoints):随时可查当前变更阶段/槽位/子步进度
|
|
@@ -2148,7 +2380,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
|
|
|
2148
2380
|
console.log('🔄 --refreeze:patch 子步标记已重置,本次 done 将重新冻结(change.patch 按 baseline..HEAD 最新面重建)')
|
|
2149
2381
|
}
|
|
2150
2382
|
}
|
|
2151
|
-
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') })
|
|
2152
2384
|
}
|
|
2153
2385
|
if (sub === 'amend-draft') {
|
|
2154
2386
|
// 机器稿唯一留痕修改通道(R7 切片三 / FR-08):重锚哈希 + ledger amendment 审计;
|