agentic-flow-kit 1.7.16 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +11 -7
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-maintain.md +1 -1
  4. package/modules/hosts/claude/commands/wf-new-task.md +9 -8
  5. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  6. package/modules/hosts/claude/commands/wf-review.md +2 -2
  7. package/modules/hosts/claude/commands/wf-test.md +1 -1
  8. package/modules/hosts/codex/commands/wf-build.md +1 -1
  9. package/modules/hosts/codex/commands/wf-maintain.md +1 -1
  10. package/modules/hosts/codex/commands/wf-new-task.md +9 -8
  11. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  12. package/modules/hosts/codex/commands/wf-review.md +2 -2
  13. package/modules/hosts/codex/commands/wf-test.md +1 -1
  14. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  15. package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
  16. package/modules/hosts/cursor/commands/wf-new-task.md +9 -8
  17. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  18. package/modules/hosts/cursor/commands/wf-review.md +2 -2
  19. package/modules/hosts/cursor/commands/wf-test.md +1 -1
  20. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  21. package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
  22. package/modules/hosts/opencode/commands/wf-new-task.md +9 -8
  23. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  24. package/modules/hosts/opencode/commands/wf-review.md +2 -2
  25. package/modules/hosts/opencode/commands/wf-test.md +1 -1
  26. package/modules/hosts/trae/commands/wf-build.md +1 -1
  27. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  28. package/modules/hosts/trae/commands/wf-new-task.md +9 -8
  29. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  30. package/modules/hosts/trae/commands/wf-review.md +2 -2
  31. package/modules/hosts/trae/commands/wf-test.md +1 -1
  32. package/package.json +1 -1
  33. package/src/cli.mjs +6 -2
  34. package/src/doctor.mjs +11 -2
  35. package/src/executor.mjs +25 -8
  36. package/src/executor.test.mjs +4 -1
  37. package/src/fresh-init.test.mjs +1 -1
  38. package/src/init.mjs +20 -3
  39. package/src/profiles.mjs +7 -3
  40. package/src/rule-budget.test.mjs +2 -2
  41. package/src/stack-profile.test.mjs +6 -0
  42. package/src/validate.mjs +6 -1
  43. package/src/validate.test.mjs +1 -1
  44. package/templates/.gitattributes +4 -1
  45. package/templates/AGENTS.md +2 -2
  46. package/templates/_agents/commands/build.md +1 -1
  47. package/templates/_agents/commands/maintain.md +1 -1
  48. package/templates/_agents/commands/new-task.md +9 -8
  49. package/templates/_agents/commands/plan.md +1 -1
  50. package/templates/_agents/commands/review.md +2 -2
  51. package/templates/_agents/commands/test.md +1 -1
  52. package/templates/_agents/engine-lock.json +130 -42
  53. package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
  54. package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
  55. package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
  56. package/templates/_agents/lane-surfaces.txt +4 -1
  57. package/templates/_agents/level-hints.txt +1 -1
  58. package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
  59. package/templates/_agents/mechanism-registry.md +27 -0
  60. package/templates/_agents/rule-budgets.txt +2 -2
  61. package/templates/_agents/scripts/agg-delegations.cjs +21 -3
  62. package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
  63. package/templates/_agents/scripts/agg-judgments.mjs +80 -0
  64. package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
  65. package/templates/_agents/scripts/archive-sweep.mjs +233 -0
  66. package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
  67. package/templates/_agents/scripts/check-attribution.mjs +37 -0
  68. package/templates/_agents/scripts/check-convergence.mjs +119 -0
  69. package/templates/_agents/scripts/check-engine-integrity.mjs +4 -1
  70. package/templates/_agents/scripts/check-funnel.mjs +56 -0
  71. package/templates/_agents/scripts/check-hygiene.mjs +5 -5
  72. package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
  73. package/templates/_agents/scripts/check-lane-surface.mjs +2 -2
  74. package/templates/_agents/scripts/check-lane-surface.test.mjs +5 -5
  75. package/templates/_agents/scripts/check-ledger-invariant.mjs +403 -152
  76. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +433 -3
  77. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -5
  78. package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
  79. package/templates/_agents/scripts/check-loop.mjs +215 -34
  80. package/templates/_agents/scripts/check-loop.test.mjs +582 -49
  81. package/templates/_agents/scripts/check-prejudgment.mjs +64 -0
  82. package/templates/_agents/scripts/check-registry.mjs +45 -0
  83. package/templates/_agents/scripts/check-scope.mjs +96 -0
  84. package/templates/_agents/scripts/check-scope.test.mjs +96 -0
  85. package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
  86. package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
  87. package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
  88. package/templates/_agents/scripts/compact-ledgers.mjs +3 -3
  89. package/templates/_agents/scripts/confirm-doc.mjs +114 -38
  90. package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
  91. package/templates/_agents/scripts/edit-face-check.mjs +5 -3
  92. package/templates/_agents/scripts/fill-intent.mjs +5 -3
  93. package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
  94. package/templates/_agents/scripts/fill-plan.mjs +10 -6
  95. package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
  96. package/templates/_agents/scripts/fill-spec.mjs +6 -3
  97. package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
  98. package/templates/_agents/scripts/fixture-scripts.mjs +2 -2
  99. package/templates/_agents/scripts/fixture-scripts.test.mjs +6 -1
  100. package/templates/_agents/scripts/gate-checklist.mjs +6 -0
  101. package/templates/_agents/scripts/gate-seg.mjs +1 -1
  102. package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
  103. package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
  104. package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
  105. package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
  106. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +8 -3
  107. package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
  108. package/templates/_agents/scripts/gen-workflow-metrics.mjs +3 -3
  109. package/templates/_agents/scripts/git-core.mjs +45 -0
  110. package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
  111. package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
  112. package/templates/_agents/scripts/kb-search.mjs +19 -4
  113. package/templates/_agents/scripts/kb-search.test.mjs +33 -0
  114. package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
  115. package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
  116. package/templates/_agents/scripts/ledger-shards.mjs +307 -0
  117. package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
  118. package/templates/_agents/scripts/level-criteria.mjs +9 -10
  119. package/templates/_agents/scripts/level-criteria.test.mjs +7 -5
  120. package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
  121. package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
  122. package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
  123. package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
  124. package/templates/_agents/scripts/pipeline-run.mjs +6 -22
  125. package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
  126. package/templates/_agents/scripts/policy.mjs +28 -0
  127. package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
  128. package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
  129. package/templates/_agents/scripts/rewrite-file.mjs +133 -0
  130. package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
  131. package/templates/_agents/scripts/rule-budget.mjs +83 -0
  132. package/templates/_agents/scripts/run-tests.mjs +99 -0
  133. package/templates/_agents/scripts/run-tests.test.mjs +105 -0
  134. package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
  135. package/templates/_agents/scripts/stage-gates.mjs +34 -12
  136. package/templates/_agents/scripts/trust-mode.mjs +4 -4
  137. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  138. package/templates/_agents/scripts/verify.mjs +135 -58
  139. package/templates/_agents/scripts/verify.test.mjs +156 -47
  140. package/templates/_agents/scripts/wf-journal.mjs +8 -4
  141. package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
  142. package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
  143. package/templates/_agents/scripts-test-exempt.txt +1 -0
  144. package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
  145. package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
  146. package/templates/_agents/skills/review-method/SKILL.md +55 -0
  147. package/templates/_agents/workflow-enums.txt +8 -1
  148. package/templates/_githooks/pre-commit +110 -90
  149. package/templates/_githooks/pre-merge-commit +27 -23
  150. package/templates/_githooks/pre-push +4 -3
  151. package/templates/_github/workflows/kit-ci.yml +4 -3
  152. package/templates/workflow/README.md +14 -7
  153. package/templates/workflow/incidents/_TEMPLATE.md +4 -1
  154. package/templates/workflow/intents/_TEMPLATE.md +4 -2
  155. package/templates/workflow/papercuts.md +2 -0
  156. package/templates/workflow/plans/_TEMPLATE.md +10 -5
  157. package/templates/workflow/regression-checklist.md +1 -0
  158. package/templates/workflow/specs/_TEMPLATE.md +17 -1
  159. package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
  160. package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
  161. package/templates/_agents/scripts/rule-budget.sh +0 -74
@@ -1,47 +1,92 @@
1
1
  #!/usr/bin/env node
2
- // check-ledger-invariant — confirmations.jsonl 提交不变量(2026-10-02 ledger-ci-invariant)
2
+ // check-ledger-invariant — confirmations 台账提交不变量(2026-10-02 ledger-ci-invariant;
3
+ // 2026-10-12 ledger-shard-invariant:校验面自单一主台账扩展到「主台账 + confirmations.d 分片」;
4
+ // 2026-10-13 深水批次:合并感知删片 / 终树补登追认 / 补登语义身份 / DAG 感知 + 历史基线,
5
+ // 判据自多 worktree 合并丢行事故后的实战实现逐字回灌(出处留包源仓 git 历史与 CHANGELOG))
3
6
  // 目的:把「台账本地可写」的伪造面从事后对质级升为机器可检出级(能力评审审计维度扣分项)。
4
- // 台账不可变原则:confirmations.jsonl 历史行禁删改——修正只能走追加补偿行(README 硬规则 6)。
7
+ // 台账不可变原则:confirmations 历史行禁删改——修正只能走追加补偿行(README 硬规则 6)。
5
8
  //
6
- // **特许修剪通道(2026-10-12 ledger-prune-manifest)**:append-only 之上唯一的合法删行出口。场景是
7
- // 仓库所有者明令删除某批 workflow 文档——台账对应行不删则行级④持续报红,退路只剩手改台账
8
- // (即把使用者逼向 `--no-verify`)。判据与凭据格式见 ledger-prune.mjs:暂存集须同带
9
- // `.agents/ledger-prune-manifest.json`(被删行 sha256 + 授权 + 理由),逐行对账「新 = 旧 − manifest
10
- // 所列行」且每哈希恰命中一行、doc ∈ 声明家族,才认领为合法修剪;无 manifest 的删/改/重排**仍全拦**。
11
- // 诚实边界:manifest 是可写文件,刻意伪造即可让任意删行合法化——与「台账本地可写」同级(见 README
12
- // 确认门诚实边界)。本通道不宣称防伪,只把「顺手手改」抬到「提交一份可被 git 历史对质的凭据」。
9
+ // 分片形态(2026-10-10 中期方案;2026-10-12 起为 confirm-doc 默认写入口径):
10
+ // .agents/confirmations.d/<doc 基名>.jsonl —— 按 doc 分片,跨机/跨 worktree 确认不同 doc 时
11
+ // 写不同文件,从根上消除「每合并必冲突 + 手工重建」;同一 doc 被两端并发确认时由
12
+ // .gitattributes 的 merge=jsonl-union 驱动取并集(本地须配 merge.jsonl-union.driver:
13
+ // git config merge.jsonl-union.driver "node .agents/scripts/git-merge-jsonl.mjs %O %A %B")。
14
+ // 本脚本给分片补上主台账同级的 append-only 前缀不变量——否则分片是「无人校验的写入面」,
15
+ // 被改写/删除无人知。
16
+ //
17
+ // 特许修剪(2026-10-09 user-directed-prune):仓库所有者明令删除某批 workflow 文档并清理其台账行时,
18
+ // 允许**一次性、逐行对账**的修剪——暂存/提交须同时携带 .agents/ledger-prune-manifest.json
19
+ // ({ts, authority, reason, removes:[被删行 sha256...], docs:[家族名...]},判据单源 ledger-prune.mjs),
20
+ // 门禁逐行核验「新内容 = 旧内容 − manifest 所列各行」且每个哈希恰对应一行。未带 manifest 的
21
+ // 删/改/重排仍全拦——append-only 红线对其他一切场景不变;修剪凭据随 manifest 入库,事后可对质。
22
+ //
23
+ // 分片删除的唯一合法形态 = compact-ledgers 归并(2026-10-12):分片被删时,该分片**全部非空行必须
24
+ // 已存在于同一提交的主台账**(compact-ledgers 正是把这些行原样追加进主台账后才删片)——认不出
25
+ // 归并凭据的删片一律 hard fail,堵住「删片抹账」这条路。主台账自身永不因归并被删。其上三组深水
26
+ // 判据(2026-10-13,实战回灌):
27
+ // ① 合并感知删片(merge-aware-shard-delete):分片内容从**各父提交取并集**后对账——合并解析收口
28
+ // 删片时,分片可能只在第二父侧(首父早已归并删片,实战实证:单看首父会把合法收口误判成
29
+ // 「增删同提交」)。「分片删除丢行」(行未并入主台账)仍 hard 拦。
30
+ // ② 终树补登追认(merge-shard-restore-aware):合并删片缺行、但缺行已原样回 append 到终树主台账
31
+ // (补偿行:新 ts + note 引原 ts)→ 视为丢失已追认,放行——历史事实不可改写,终态无丢失即
32
+ // 审计实质。
33
+ // ③ 补登语义身份比对:补偿行与被删行的等价判定用**语义三元组 {doc, stage, fingerprint}**——
34
+ // 补偿行本就允许改 ts 加 note,逐字节比对会漏掉全部合法补偿行。
35
+ //
36
+ // 合并并集语义(2026-10-13 merge-union-aware):多父提交不按线性前缀判,改验「并集无丢失」——
37
+ // 合并结果的行集合须 ⊇ 各父行集之并(一行不丢)且 ⊆ 该并集(一行不造)。jsonl-union 策略
38
+ // (多 worktree 并行确认的根基)与线性前缀模型天生矛盾,本判据对齐审计实质:确认行不被丢、
39
+ // 不被造。union 不过再试「合并携带 manifest 的特许修剪集合对账」(对父母并集对账,非第一父——
40
+ // 堵 p2 独有行借 p1 相对 manifest 无凭据消失);支持「分支上修剪、合并时落地」的合法形态。
41
+ // 合并通过后以合并 blob 续扫。
42
+ //
43
+ // DAG 感知(2026-10-13 dag-aware-base):历史全扫按 DAG 而非线性——路径史用 --full-history 取全
44
+ // (TREESAME 合并也入列,防「ours 合并丢一侧行」被历史简化隐藏,审计门立场);每个提交自对
45
+ // 己父取基准 blob(第一父;根提交视为新文件起步)——线性化「前一承」在分支汇合处会拿另一侧
46
+ // 提交当基准产生伪前缀破坏。多父提交无条件走并集对账(前缀通过不得短路——ours-resolution 合并
47
+ // 树与第一父逐字节同 blob,嵌套判法对静默丢行全盲)。
48
+ //
49
+ // 历史基线(2026-10-13 ledger-history-baseline 机制,装户自定):LEDGER_HISTORY_BASELINE=<基线提交>
50
+ // 环境变量声明后,基线之前的提交不进前缀扫描——该时代(特许修剪/合并感知机制上线前)的删改
51
+ // 无对账凭据,原理上不可追溯验证(回填凭据须改写历史且属事后伪造授权);基线由 owner 一次性
52
+ // loud 声明,同「流程: legacy / 存量不回填」哲学。基线 blob 为可信起点,基线后提交照常全扫。
53
+ // kit 缺省为空 = 无基线声明 → 全量扫描(最严语义;装户如确认存在机制上线前的存量断档,自行设
54
+ // 基线豁免,豁免计数 loud 出账。env 覆盖亦供测试)。
13
55
  //
14
56
  // 模式:
15
- // 历史全扫(缺省,CI 机器门):遍历台账全部提交历史(时间正序),每次变更必须以旧 blob 为
16
- // **逐字节前缀**(允许纯追加;禁止删行/改行/重排);再对最终树做行级校验(五项,见下)。
17
- // blob 读取走 git 对象库(入库 LF 归一口径),不受工作树 autocrlf 影响。
18
- // --staged(pre-commit 前置):暂存 blob 必须以 HEAD blob 为逐字节前缀——提交时刻即拦重写。
57
+ // 历史全扫(缺省,CI 机器门):对主台账与每个分片各自遍历其提交历史,单父提交必须以第一父 blob
58
+ // 为**逐字节前缀**(允许纯追加;禁止删行/改行/重排)——或前缀破坏但该提交树上携带 manifest
59
+ // 恰好对账被删行(特许修剪认领,loud 记录后以新 blob 为基线继续)。多父提交无条件验并集。
60
+ // 分片另判「删除须已归并(①②③)」。blob 读取走 git 对象库(入库 LF 归一口径),不受工作树
61
+ // autocrlf 影响。
62
+ // --staged(pre-commit 前置):主台账暂存 blob ⊇ HEAD blob(或携带 manifest 的特许修剪);分片
63
+ // 三态——纯追加验前缀 / 暂存删除验「行已全部并入暂存主台账」(compact 归并形态放行、丢行
64
+ // hard 拦)/ 新分片首入库无 HEAD 基线跳过(历史全扫续管)。
19
65
  //
20
- // 行级校验(终树,五项):
21
- // ① ts 为 ISO 字符串,且**同一 doc 内**非降序(跨 doc 物理序乱序不判——并集合并/分片归并的正常形态,
22
- // 2026-10-12 修正;同 doc 内倒挂仍 hard 拦,跨 doc 插行/改行由检查 15 哈希链兜)
66
+ // 行级校验(终树,五项;取数面 = ledger-loader 视图:主台账在前、分片按文件名序在后——既有
67
+ // ledger-line-exempt.json 的行号(校验序号即非空行序)不因引入分片而漂移):
68
+ // ① ts 为 ISO 字符串,且**同一 doc 内**非降序(跨 doc 物理序乱序不判、只计数出账——并集合并/
69
+ // 分片归并的正常形态;同 doc 内倒挂仍 hard 拦,跨 doc 插行/改行由检查 15 doc-chain 哈希链兜。
70
+ // 注:实战源实现用「单文件内非降序」——其主台账已单文件单 doc 化;kit 装户存量台账仍是
71
+ // 主台账多 doc 形态,全表判据会因正常并集协作报红逼出 --no-verify,故保留 kit 的 per-doc 口径)
23
72
  // ② fingerprint 为 64 位 hex
24
73
  // ③ source=chat-delegated 行必有非空 quote(原话对质面)
25
- // ④ doc 匹配 workflow/(intents|specs|plans|incidents)/[^/]+.md 且当前树存在
26
- // ⑤ stage ∈ 合法跳转终态集(approved/done/fixed/closed/superseded/cancelled)
27
- // 取数面 = ledger-loader 视图(主台账 + 分片 confirmations.d/*.jsonl);视图序为主台账在前,
28
- // 故既有 ledger-line-exempt.json 的行号不因引入分片而漂移。
29
- //
30
- // 行级豁免(2026-10-09 ledger-line-exempt,CI 红事故处置):.agents/ledger-line-exempt.json 登记
31
- // 已定性的污染行({line, doc, reason}——line = 校验序号即非空行序)——行级五项对豁免行整体跳过、
32
- // 行**保留在台账**(审计面完整),豁免计数照常出账保持可见。定位:事故行无法追加修复(行级扫最终树
33
- // 全行)且不可就地改(历史前缀不变量)——superseded 重立单之外的唯一出口。**滥用边界**:豁免文件
34
- // 自身入库、git 历史可对质;逐条须 papercuts/incident 定性引用,无定性登记 = 审计事件。
74
+ // ④ doc 匹配 workflow/(intents|specs|plans|incidents)/[^/]+.md 且当前树存在(**归档感知**,
75
+ // 2026-10-13 archive-aware-doc:四目录原路径未命中再探 workflow/archive/*/——关单即归档移动
76
+ // 文件后行仍指向真实文档;真消失(四目录与归档批次均未命中)仍拦)
77
+ // ⑤ stage ∈ 合法跳转终态集(approved/done/fixed/closed/superseded/cancelled + revert 注记行)
78
+ // 行级豁免(ledger-line-exempt.json)与跨 doc 乱序计数照 kit 既有口径出账。
35
79
  //
36
- // 静默条件:台账不存在 / 零提交历史(装户、新仓)/ --staged 且暂存未触台账或无 HEAD 版本。
37
- // 已知边界:git log --follow 未用——台账自创建无重命名(单一路径取史);克隆深度不足时仅扫可得
80
+ // 静默条件:台账路径零提交历史(装户、新仓)/ --staged 且暂存未触任何台账或无 HEAD 版本。
81
+ // 有提交史的路径才进历史扫描**与终树行级校验**(否则新仓首版会被「doc 不存在」误杀)。
82
+ // 已知边界:git log --follow 未用——各台账路径自创建无重命名(单一路径取史);克隆深度不足时仅扫可得
38
83
  // 历史(CI 侧 fetch-depth:0 兜全史,深度不足会告警声明边界,fail-open 于深度、fail-closed 于内容)。
39
84
  // 用法:node check-ledger-invariant.mjs [--root <仓库根>] [--staged]
40
- import { execFileSync } from 'node:child_process';
85
+ import { execFileSync, spawnSync } from 'node:child_process';
41
86
  import fs from 'node:fs';
42
87
  import path from 'node:path';
43
- import { matchPrune, loadManifestFromRef, PRUNE_REL } from './ledger-prune.mjs';
44
- import { readLedgerRows, SHARD_DIR_REL } from './ledger-loader.mjs';
88
+ import { matchPrune, loadManifestFromRef, sha256line, PRUNE_REL } from './ledger-prune.mjs';
89
+ import { readLedgerEntries, SHARD_DIR_REL } from './ledger-loader.mjs';
45
90
 
46
91
  const argv = process.argv.slice(2);
47
92
  const argOf = (k) => {
@@ -51,144 +96,352 @@ const argOf = (k) => {
51
96
  const staged = argv.includes('--staged');
52
97
  const ROOT = argOf('--root') || (staged ? process.cwd() : execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim());
53
98
  const REL = '.agents/confirmations.jsonl';
99
+ const SHARD_DIR = SHARD_DIR_REL; // '.agents/confirmations.d'(读口径单源 ledger-loader)
54
100
  const FILE = path.join(ROOT, REL);
55
101
  const VALID_STAGES = new Set(['approved', 'done', 'fixed', 'closed', 'superseded', 'cancelled',
56
102
  'revert-draft', 'revert-open']); // 回退注记行(check-loop :822 认可;fingerprint 恒 n/a)
103
+ // 历史基线(机制 2026-10-13 ledger-history-baseline,详见头注):kit 缺省空 = 全量扫描
104
+ const HISTORY_BASELINE = process.env.LEDGER_HISTORY_BASELINE || '';
57
105
 
58
106
  const git = (args, opts = {}) => execFileSync('git', args, { cwd: ROOT, encoding: 'buffer', maxBuffer: 64 * 1024 * 1024, ...opts });
59
107
  const gitText = (args) => execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' });
60
108
  const blobOf = (ref) => git(['show', ref]); // ref 必须为完整对象引用(<sha>:<path> / HEAD:<path> / :<path>)
109
+ // tryBlob:安全读 blob,失败返回 null 且**不向 stderr 外泄 git fatal 噪声**(删片场景读当前提交必失败,
110
+ // 那是预期路径不是错误——历史扫描的高频探针路径必须静默)。
111
+ const tryBlob = (ref) => {
112
+ const r = spawnSync('git', ['show', ref], { cwd: ROOT, encoding: 'buffer', maxBuffer: 64 * 1024 * 1024 });
113
+ return r.status === 0 ? r.stdout : null;
114
+ };
115
+ const linesOf = (buf) => buf.toString('utf8').split('\n').filter((l, i, a) => !(l === '' && i === a.length - 1) && l !== '');
116
+ const isShard = (rel) => rel.startsWith(`${SHARD_DIR}/`) && rel.endsWith('.jsonl');
117
+
118
+ // ④ 归档感知存在性(2026-10-13 archive-aware-doc):四目录原路径未命中再探 workflow/archive/*/——
119
+ // 归档是 sanctioned 的文档生命周期事件,行的审计价值(指向真实文档)不受移动影响;真消失(既不在
120
+ // 四目录也无归档批次)仍拦。
121
+ const docExists = (doc) => {
122
+ if (fs.existsSync(path.join(ROOT, doc))) return true;
123
+ const m = /^workflow\/(intents|specs|plans|incidents)\/([^/]+)\.md$/.exec(doc);
124
+ if (!m) return false;
125
+ try {
126
+ const arcRoot = path.join(ROOT, 'workflow', 'archive');
127
+ for (const batch of fs.readdirSync(arcRoot)) {
128
+ if (fs.existsSync(path.join(arcRoot, batch, m[1], `${m[2]}.md`))) return true;
129
+ }
130
+ } catch { /* archive 目录不存在 */ }
131
+ return false;
132
+ };
61
133
 
62
134
  const problems = [];
135
+ const fail = (msg) => { console.error(msg); process.exit(1); };
63
136
 
64
- // ---- --staged:暂存 blob ⊇ HEAD blob ----
65
- if (staged) {
66
- const stagedHit = gitText(['-c', 'core.quotepath=off', 'diff', '--cached', '--name-only']).split(/\r?\n/).includes(REL);
67
- if (!stagedHit) process.exit(0);
68
- const headExists = (() => { try { gitText(['rev-parse', '--verify', '--quiet', `HEAD:${REL}`]); return true; } catch { return false; } })();
69
- if (!headExists) process.exit(0); // 首次入库:无 HEAD 基线,历史全扫后续覆盖
70
- let head, cur;
137
+ // 终树主台账行集(CRLF 归一;补登追认的比对基准——与行级校验「终树」同语义)
138
+ // 补登追认按**语义身份**比对:{doc, stage, fingerprint} 三元组——补登合法形态是补偿行
139
+ // (原 ts 早于尾行时须记现时刻 + note,内容字段沿原行),逐字节比对会漏掉全部合法补偿。
140
+ const finalLedgerRows = () => {
71
141
  try {
72
- head = blobOf(`HEAD:${REL}`);
73
- cur = blobOf(`:${REL}`); // 暂存区
74
- } catch {
75
- process.exit(0); // 对象读取异常(如极端并发)交由 CI 全扫兜底
76
- }
77
- if (!cur.subarray(0, head.length).equals(head)) {
78
- // 特许修剪(2026-10-12 ledger-prune-manifest):仓库所有者明令删某批 workflow 文档时,台账对应行
79
- // 须随之删除,否则行级④「doc 在当前树存在」持续报红、退路只剩手改台账(踩 --no-verify 红线)。
80
- // 出口 = **逐行对账 + 凭据入库**:暂存集须同时携带 .agents/ledger-prune-manifest.json,判据见
81
- // ledger-prune.mjs matchPrune(四条缺一不可)。无 manifest 的删/改/重排仍全拦——append-only 红线
82
- // 对其他一切场景不变。被删行哈希随 manifest 入库,事后可对质。
83
- const m = matchPrune(head.toString('utf8'), cur.toString('utf8'),
84
- loadManifestFromRef(`:${PRUNE_REL}`, ROOT));
85
- if (m && !m.err) {
86
- console.log(`台账不变量(--staged)— 特许修剪通过:移除 ${m.removed} 行(${m.docs.length} 家族)——凭据 ${PRUNE_REL} 随笔入库,被删行可在 git 历史对质`);
87
- process.exit(0);
142
+ return fs.readFileSync(FILE, 'utf8').replace(/\r\n/g, '\n').split('\n')
143
+ .filter((l) => l.trim())
144
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } });
145
+ } catch { return null; }
146
+ };
147
+ const restoredInFinal = (missingLines, finRows) => {
148
+ if (!finRows) return false;
149
+ return missingLines.every((l) => {
150
+ let e;
151
+ try { e = JSON.parse(l); } catch { return false; }
152
+ return finRows.some((r) => r && r.doc === e.doc && r.stage === e.stage && r.fingerprint === e.fingerprint);
153
+ });
154
+ };
155
+
156
+ // ---- 分片删除合法性:该分片全部非空行须存在于同一提交的主台账(compact-ledgers 归并凭据)----
157
+ // 返回 null = 合法归并(或缺行已终树补登追认);否则返回原因串。
158
+ // 读法要点(① 合并感知):分片在**该提交已被删除**,其内容从各父取并集——合并收口删片时分片可能
159
+ // 只在第二父(首父已归并删片),单读首父会误判「增删同提交」;主台账取该提交当时的版本
160
+ // (归并行把分片原样追加进主台账后同提交删片,故主台账须含全部行)。
161
+ const shardDeleteReason = (sha, rel) => {
162
+ const mainBuf = tryBlob(`${sha}:${REL}`);
163
+ if (mainBuf === null) return `该提交无主台账 ${REL}——无法认定归并`;
164
+ const shardLines = new Set();
165
+ let foundInParent = false;
166
+ for (const p of parentShas(sha)) {
167
+ const b = tryBlob(`${p}:${rel}`);
168
+ if (b === null) continue;
169
+ foundInParent = true;
170
+ for (const l of linesOf(b)) if (l.trim()) shardLines.add(l);
171
+ }
172
+ if (!foundInParent) return `父提交 ${sha.slice(0, 10)}^ 中无此分片(增删同提交?)——无法认定归并`;
173
+ const mainLines = new Set(linesOf(mainBuf).filter((l) => l.trim()));
174
+ const missing = [...shardLines].filter((l) => !mainLines.has(l));
175
+ if (missing.length) {
176
+ // ② 终树补登追认:缺行已按语义身份(③)回append进终树主台账 → 丢失已追认
177
+ if (restoredInFinal(missing, finalLedgerRows())) {
178
+ console.error(`[info] ${sha.slice(0, 10)}:${rel} 合并删片缺 ${missing.length} 行已在终树主台账补登(例:${missing[0].slice(0, 60)}…)——丢失追认,续扫`);
179
+ return null;
88
180
  }
89
- let off = 0;
90
- while (off < Math.min(head.length, cur.length) && head[off] === cur[off]) off++;
91
- problems.push(`- [台账重写] 暂存内容不是 HEAD 的前缀扩展(首异偏移 ${off},HEAD ${head.length}B → 暂存 ${cur.length}B)${m && m.err ? `;特许修剪对账失败:${m.err}` : ''}`);
92
- console.error('台账不可变(--staged)— BLOCK:\n' + problems.join('\n'));
93
- console.error(` 修法:台账历史行禁删改——误写走追加补偿行(node .agents/scripts/confirm-doc.mjs ...);确需撤销历史提交请 git revert 后重新追加。\n 所有者明令删除某批文档须同笔暂存 ${PRUNE_REL}(逐行 sha256 + 授权 + 理由),门禁逐行对账后才放行。`);
94
- process.exit(1);
181
+ return `删除时分片有 ${missing.length} 行不在同提交主台账内(例:${missing[0].slice(0, 60)}…)——只有 compact-ledgers 归并后才允许删片`;
95
182
  }
96
- process.exit(0);
97
- }
183
+ return null;
184
+ };
98
185
 
99
- // ---- 历史全扫(缺省)----
100
- if (!fs.existsSync(FILE)) process.exit(0); // 装户/新仓无台账面
101
- let shas;
102
- try {
103
- shas = gitText(['log', '--format=%H', '--reverse', '--', REL]).split(/\s+/).filter(Boolean);
104
- } catch {
105
- process.exit(0); // 非仓库或文件未入库(测试夹具)
186
+ // ---- 历史前缀扫描(单台账路径;主台账与分片共用)----
187
+ // allowShardDelete:分片允许「已归并后删片(或缺行已终树补登)」这一种合法消失形态;主台账永不因归并被删。
188
+ // 返回该路径的提交史(空 = 从未入库:装户/新仓/尚在暂存区)——零史路径整体跳过,与原实现
189
+ // 「零提交历史即静默」的契约一致(行级校验也只覆盖有史的路径,否则新仓首版会被 doc 不存在误杀)。
190
+ function historyShas(rel) {
191
+ try {
192
+ // --full-history:不做历史简化——TREESAME 合并也入列,防「ours 合并丢一侧行」被简化隐藏(审计门立场)
193
+ return gitText(['log', '--full-history', '--format=%H', '--reverse', '--', rel]).split(/\s+/).filter(Boolean);
194
+ } catch {
195
+ return [];
196
+ }
106
197
  }
107
- if (!shas.length) process.exit(0);
108
198
 
109
- // 分片路径清单(2026-10-12 ledger-shard-write):**扫 git 全历史里出现过的分片**,不只看工作树现有的
110
- // ——compact 会把分片删掉,只看工作树就永远扫不到「已删分片的历史」。用 pathspec 枚举全史分片路径。
111
- let shardRelPaths = [];
112
- try {
113
- shardRelPaths = gitText(['log', '--all', '--format=', '--name-only', '--', `${SHARD_DIR_REL}/`])
114
- .split(/\r?\n/).map((l) => l.trim()).filter((l) => l.startsWith(`${SHARD_DIR_REL}/`) && l.endsWith('.jsonl'));
115
- shardRelPaths = [...new Set(shardRelPaths)].sort();
116
- } catch { /* 非仓库/无分片历史 → 空清单,分片段整段跳过 */ }
117
-
118
- let prev = null;
119
- let prevSha = null;
120
- for (const sha of shas) {
121
- let cur;
122
- // 删除提交不是静默边界(复核 P2 收口):台账在 append-only 世界里永不应删除——任一历史提交删除
123
- // 台账即 hard fail(否则「删除→次提交重加」会把重加 blob 当新基线绕过前缀判据)。
124
- try { cur = blobOf(`${sha}:${REL}`); } catch {
125
- problems.push(`- [台账删除] ${sha.slice(0, 10)}:该提交删除了 confirmations.jsonl(append-only 世界禁删文件)`);
126
- prev = null; // 后续重加无基线可比,仅受行级校验约束
127
- continue;
128
- }
129
- if (prev !== null && !cur.subarray(0, prev.length).equals(prev)) {
130
- // 特许修剪:前缀破坏时,若该提交树上携带 manifest 且逐行对账通过 = 认领为合法修剪,
131
- // loud 记录后**以新 blob 为基线续扫**(对质凭据 = 该提交的 manifest + 被删行内容在 git 历史里)。
132
- const m = matchPrune(prev.toString('utf8'), cur.toString('utf8'), loadManifestFromRef(`${sha}:${PRUNE_REL}`, ROOT));
133
- if (m && !m.err) {
134
- console.error(`[info] ${sha.slice(0, 10)}:特许修剪 ${m.removed} 行(${m.docs.length} 家族)——manifest 对账通过,凭据 ${PRUNE_REL} 入库可对质`);
135
- } else {
136
- let off = 0;
137
- while (off < Math.min(prev.length, cur.length) && prev[off] === cur[off]) off++;
138
- const removed = prev.length > cur.length;
139
- problems.push(`- [前缀破坏] ${sha.slice(0, 10)}(前承 ${prevSha.slice(0, 10)}):历史内容被${removed ? `删减 ${prev.length - cur.length}B` : `改写(首异偏移 ${off},旧 ${prev.length}B → 新 ${cur.length}B)`}${m && m.err ? `;特许修剪对账失败:${m.err}` : ''}`);
199
+ // 基线后提交集(含基线可达性探测;无基线声明或基线不在本仓 → null = 基线不生效,全史扫描)
200
+ let _postBaseline;
201
+ const postBaselineSet = () => {
202
+ if (_postBaseline !== undefined) return _postBaseline;
203
+ if (!HISTORY_BASELINE) return (_postBaseline = null); // kit 缺省:无基线声明 → 全量扫描
204
+ try {
205
+ gitText(['rev-parse', '--verify', '--quiet', `${HISTORY_BASELINE}^{commit}`]);
206
+ _postBaseline = new Set(gitText(['rev-list', `${HISTORY_BASELINE}..HEAD`]).split(/\s+/).filter(Boolean));
207
+ } catch {
208
+ _postBaseline = null;
209
+ }
210
+ return _postBaseline;
211
+ };
212
+
213
+ // 多父提交的父列表(%P 全父)
214
+ const parentShas = (sha) => {
215
+ try {
216
+ return gitText(['log', '-1', '--format=%P', sha]).split(/\s+/).filter(Boolean);
217
+ } catch {
218
+ return [];
219
+ }
220
+ };
221
+
222
+ // 合并并集对账:cur 行集须恰为各父行集之并(无丢失、无凭空新增)。返回 null = 认可;否则返回原因。
223
+ // 父提交无此路径(单侧新增)跳过该父;行比对按整行字符串(与 linesOf/前缀判据同粒度)。
224
+ const mergeUnionProblem = (sha, rel, curBuf) => {
225
+ const curLines = new Set(linesOf(curBuf));
226
+ const parentLines = new Set();
227
+ for (const p of parentShas(sha)) {
228
+ const b = tryBlob(`${p}:${rel}`);
229
+ if (b === null) continue;
230
+ for (const l of linesOf(b)) parentLines.add(l);
231
+ }
232
+ const lost = [...parentLines].filter((l) => !curLines.has(l));
233
+ if (lost.length) return `合并丢失 ${lost.length} 行(例:${lost[0].slice(0, 60)}…)——union 合并不得丢行`;
234
+ const made = [...curLines].filter((l) => !parentLines.has(l));
235
+ if (made.length) return `合并凭空多出 ${made.length} 行(例:${made[0].slice(0, 60)}…)——合并结果须为各父行集之并`;
236
+ return null;
237
+ };
238
+
239
+ // 合并携带 manifest 的特许修剪对账(merge-prune-set):集合口径——每个 removes 哈希须恰命中
240
+ // 父母并集中一行;cur 行集须恰为「父母并集 − 被删行集」。支持「分支上修剪、合并时落地」的合法
241
+ // 形态(修剪凭据随合并入库);对父母并集对账(非第一父)堵住 p2 独有行借 p1 相对 manifest 无凭据
242
+ // 消失。返回 null = 认可;否则原因串。
243
+ const mergePruneProblem = (parents, rel, curBuf, manifest) => {
244
+ if (!manifest || !Array.isArray(manifest.removes) || !manifest.removes.length) return 'manifest 缺失或 removes 为空';
245
+ const union = new Set();
246
+ for (const p of parents) {
247
+ const b = tryBlob(`${p}:${rel}`);
248
+ if (b === null) continue;
249
+ for (const l of linesOf(b)) union.add(l);
250
+ }
251
+ const want = new Set(manifest.removes);
252
+ const removed = [...union].filter((l) => want.has(sha256line(l)));
253
+ if (removed.length !== manifest.removes.length) return `manifest 哈希在父母并集中命中 ${removed.length} 行 ≠ 声明 ${manifest.removes.length} 行`;
254
+ const removedSet = new Set(removed);
255
+ const expected = [...union].filter((l) => !removedSet.has(l));
256
+ const expectedSet = new Set(expected);
257
+ const curLines = new Set(linesOf(curBuf));
258
+ const extra = [...curLines].filter((l) => !expectedSet.has(l));
259
+ if (extra.length) return `合并多出对账外 ${extra.length} 行(例:${extra[0].slice(0, 60)}…)`;
260
+ const stillLost = expected.filter((l) => !curLines.has(l));
261
+ if (stillLost.length) return `合并仍丢失 manifest 外 ${stillLost.length} 行(例:${stillLost[0].slice(0, 60)}…)`;
262
+ const docs = new Set(manifest.docs || []);
263
+ if (docs.size) {
264
+ for (const l of removed) {
265
+ try {
266
+ const doc = JSON.parse(l).doc || '';
267
+ if (!docs.has(path.basename(doc).replace(/\.md$/, ''))) return `被删行 doc ${doc} 不在 manifest.docs 清单内`;
268
+ } catch { return '被删行非 JSON'; }
269
+ }
270
+ }
271
+ return null;
272
+ };
273
+
274
+ function scanHistory(rel, { allowShardDelete }) {
275
+ const allShas = historyShas(rel);
276
+ if (!allShas.length) return allShas;
277
+ let shas = allShas;
278
+ // 历史基线:只走基线后提交(详见头注)——DAG 感知比对下各提交自对己父,无需线性化起点
279
+ const post = postBaselineSet();
280
+ if (post) {
281
+ const kept = shas.filter((sha) => post.has(sha));
282
+ if (kept.length !== shas.length) {
283
+ console.error(`[info] ${rel}:${shas.length - kept.length} 个前基线提交按 HISTORY_BASELINE 声明豁免(owner 授权,见脚本头注)`);
284
+ }
285
+ shas = kept;
286
+ }
287
+ for (const sha of shas) {
288
+ let cur;
289
+ try { cur = blobOf(`${sha}:${rel}`); } catch {
290
+ // 删除提交不是静默边界(复核 P2 收口):主台账在 append-only 世界里永不应删除——任一历史提交删除
291
+ // 主台账即 hard fail(否则「删除→次提交重加」会把重加 blob 当新基线绕过前缀判据)。
292
+ if (!allowShardDelete) {
293
+ problems.push(`- [台账删除] ${sha.slice(0, 10)}:该提交删除了 ${rel}(append-only 世界禁删文件)`);
294
+ continue;
295
+ }
296
+ const reason = shardDeleteReason(sha, rel);
297
+ if (reason) {
298
+ problems.push(`- [分片删除丢行] ${sha.slice(0, 10)}:删除 ${rel} —— ${reason}`);
299
+ continue;
300
+ }
301
+ console.error(`[info] ${sha.slice(0, 10)}:${rel} 归并后删除(compact 归并形态——全部行已在同提交主台账对账,或缺行已终树补登追认)`);
302
+ continue;
303
+ }
304
+ // DAG 感知基准:本提交第一父的 blob(dag-aware-base)——线性化「前一承」在分支汇合处会拿
305
+ // 另一侧提交当基准产生伪前缀破坏;根提交(无父)视为新文件起步。
306
+ const parents = parentShas(sha);
307
+ const base = parents.length ? tryBlob(`${parents[0]}:${rel}`) : null;
308
+ if (base === null) continue; // 首现(父无此路径):无基线可比,行级校验兜底
309
+ if (parents.length >= 2) {
310
+ // 多父提交无条件走并集对账(前缀通过不得短路——ours-resolution 合并树与第一父逐字节同 blob,
311
+ // 嵌套判法对静默丢行全盲)
312
+ const unionFail = mergeUnionProblem(sha, rel, cur);
313
+ if (!unionFail) {
314
+ console.error(`[info] ${sha.slice(0, 10)}:合并并集无丢失——union 合并语义认可,续扫`);
315
+ continue;
316
+ }
317
+ // union 不过 → manifest 集合对账(分支修剪合并落地;对父母并集对账,非第一父)
318
+ const setFail = mergePruneProblem(parents, rel, cur, loadManifestFromRef(`${sha}:${PRUNE_REL}`, ROOT));
319
+ if (!setFail) {
320
+ console.error(`[info] ${sha.slice(0, 10)}:合并携带特许修剪——父母并集集合对账通过`);
321
+ continue;
322
+ }
323
+ problems.push(`- [前缀破坏] ${sha.slice(0, 10)}(合并):${rel} 并集对账失败:${unionFail};修剪集合对账:${setFail}`);
324
+ continue;
325
+ }
326
+ if (!cur.subarray(0, base.length).equals(base)) {
327
+ // 前缀破坏 → 该提交树上携带 manifest 且逐行对账 = 特许修剪(loud 记录)
328
+ const m = matchPrune(base.toString('utf8'), cur.toString('utf8'), loadManifestFromRef(`${sha}:${PRUNE_REL}`, ROOT));
329
+ if (m && !m.err) {
330
+ console.error(`[info] ${sha.slice(0, 10)}:特许修剪 ${m.removed} 行(${m.docs.length} 家族)——manifest 对账通过,凭据 ${PRUNE_REL} 入库可对质`);
331
+ } else {
332
+ let off = 0;
333
+ while (off < Math.min(base.length, cur.length) && base[off] === cur[off]) off++;
334
+ const removed = base.length > cur.length;
335
+ problems.push(`- [前缀破坏] ${sha.slice(0, 10)}(父 ${parents[0].slice(0, 10)}):${rel} 历史内容被${removed ? `删减 ${base.length - cur.length}B` : `改写(首异偏移 ${off},旧 ${base.length}B → 新 ${cur.length}B)`}${m && m.err ? `;特许修剪对账失败:${m.err}` : ''}`);
336
+ }
140
337
  }
141
338
  }
142
- prev = cur;
143
- prevSha = sha;
339
+ return allShas; // 交回该路径的全量提交史,供调用方判定「有史才进行级校验」
144
340
  }
145
341
 
146
- // ---- 分片历史扫描(2026-10-12 ledger-shard-write)----
147
- // 分片 `.agents/confirmations.d/*.jsonl` 此前**不受本门约束**——`--shard` 写入一旦可用,分片就成了
148
- // 前缀不变量的缺口(可改可删而不被历史扫到)。故逐分片路径做同样的前缀不变量扫描。
149
- // **删除豁免(与主台账相反)**:compact-ledgers.mjs 归并后**删除分片是合法形态**,不能照抄主台账那条
150
- // 「删文件即 hard fail」。豁免判据:该分片被删时的内容,其**每一行都已出现在同提交的主台账里**
151
- // (并集归并的必然结果)→ 认领为 compact 归并;否则(行未并入 = 丢行)仍 hard fail。
152
- for (const shard of shardRelPaths) {
153
- let sShas;
342
+ // ---- 台账路径全集:主台账 + 历史上出现过的全部分片 ----
343
+ function ledgerPaths() {
344
+ const rels = [REL];
345
+ try {
346
+ const out = gitText(['log', '--name-only', '--pretty=format:', '--', SHARD_DIR]);
347
+ for (const line of out.split(/\r?\n/)) {
348
+ const p = line.trim();
349
+ if (p && isShard(p) && !rels.includes(p)) rels.push(p);
350
+ }
351
+ } catch { /* 非 git 或无历史:仅主台账 */ }
352
+ // 工作区里新出现(尚未入库)的分片也要进行 --staged 前缀判定
154
353
  try {
155
- sShas = gitText(['log', '--format=%H', '--reverse', '--', shard]).split(/\s+/).filter(Boolean);
156
- } catch { continue; }
157
- let sPrev = null;
158
- for (const sha of sShas) {
354
+ const dir = path.join(ROOT, SHARD_DIR);
355
+ if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
356
+ for (const f of fs.readdirSync(dir)) {
357
+ const p = `${SHARD_DIR}/${f}`;
358
+ if (isShard(p) && !rels.includes(p)) rels.push(p);
359
+ }
360
+ }
361
+ } catch { /* 忽略 */ }
362
+ return rels;
363
+ }
364
+
365
+ // ---- --staged:暂存台账面 ⊇ HEAD 前缀,或携带 manifest 的特许修剪;分片删除须已并入暂存主台账 ----
366
+ if (staged) {
367
+ const stagedHit = gitText(['-c', 'core.quotepath=off', 'diff', '--cached', '--name-only']).split(/\r?\n/).filter(Boolean);
368
+ const touched = ledgerPaths().filter((rel) => stagedHit.includes(rel));
369
+ if (!touched.length) process.exit(0);
370
+ // 暂存主台账行集(分片删除的 compact 归并判据比对基准——与历史扫「同提交主台账」同语义,取暂存态)
371
+ let stagedMainLines = new Set();
372
+ try { stagedMainLines = new Set(linesOf(blobOf(`:${REL}`))); } catch { /* 暂存主台账缺失/未触 → 空集 */ }
373
+ for (const rel of touched) {
374
+ if (rel === REL) {
375
+ const headExists = (() => { try { gitText(['rev-parse', '--verify', '--quiet', `HEAD:${rel}`]); return true; } catch { return false; } })();
376
+ if (!headExists) continue; // 首次入库:无 HEAD 基线,历史全扫与终树行级校验覆盖
377
+ let head, cur;
378
+ try {
379
+ head = blobOf(`HEAD:${rel}`);
380
+ cur = blobOf(`:${rel}`); // 暂存区
381
+ } catch {
382
+ continue; // 对象读取异常(如极端并发)交由 CI 全扫兜底
383
+ }
384
+ if (!cur.subarray(0, head.length).equals(head)) {
385
+ // 前缀破坏 → 特许修剪对账(manifest 必须同在暂存集)
386
+ const m = matchPrune(head.toString('utf8'), cur.toString('utf8'), loadManifestFromRef(`:${PRUNE_REL}`, ROOT));
387
+ if (m && !m.err) {
388
+ console.log(`台账不变量(--staged)— ${rel} 特许修剪通过:移除 ${m.removed} 行(${m.docs.length} 家族)——凭据 ${PRUNE_REL} 随笔入库,被删行可在 git 历史对质`);
389
+ continue;
390
+ }
391
+ let off = 0;
392
+ while (off < Math.min(head.length, cur.length) && head[off] === cur[off]) off++;
393
+ problems.push(`- [台账重写] ${rel} 暂存内容不是 HEAD 的前缀扩展(首异偏移 ${off},HEAD ${head.length}B → 暂存 ${cur.length}B)${m && m.err ? `;特许修剪对账失败:${m.err}` : ''}`);
394
+ }
395
+ continue;
396
+ }
397
+ // 分片三态(2026-10-13 ledger-shard-default:分片为默认写入口径):纯追加验前缀 / 暂存删除验
398
+ // compact 归并(丢行 hard 拦)/ 新分片首入库无 HEAD 基线跳过(历史全扫续管)。
399
+ let sHead;
400
+ try { sHead = blobOf(`HEAD:${rel}`); } catch { continue; } // 新分片首入库
159
401
  let sCur = null;
160
- try { sCur = blobOf(`${sha}:${shard}`); } catch { sCur = null; }
402
+ try { sCur = blobOf(`:${rel}`); } catch { sCur = null; }
161
403
  if (sCur === null) {
162
- // 分片在本次提交消失 → 只在「行已全部并入主台账」时认领 compact 归并
163
- if (sPrev !== null) {
164
- const shardLines = sPrev.toString('utf8').replace(/\r\n/g, '\n').split('\n').filter((l) => l.trim());
165
- let mainNow = '';
166
- try { mainNow = blobOf(`${sha}:${REL}`).toString('utf8'); } catch { /* 主台账亦不在 = 整体删除,另行报 */ }
167
- const mainLines = new Set(mainNow.replace(/\r\n/g, '\n').split('\n').filter((l) => l.trim()));
168
- const missing = shardLines.filter((l) => !mainLines.has(l));
169
- if (missing.length === 0) {
170
- console.error(`[info] ${sha.slice(0, 10)}:分片 ${shard} 删除(${shardLines.length} 行已全部并入主台账——compact 归并形态,放行)`);
171
- } else {
172
- problems.push(`- [分片删除丢行] ${sha.slice(0, 10)}:${shard} 被删除,但其中 ${missing.length} 行未出现在主台账(compact 归并须先并入再删)`);
173
- }
404
+ // 暂存里分片被删 → compact 归并判据(行全并入暂存主台账才放行)
405
+ const shardLines = linesOf(sHead);
406
+ const missing = shardLines.filter((l) => !stagedMainLines.has(l));
407
+ if (missing.length === 0) {
408
+ console.log(`台账不变量(--staged)— 分片 ${rel} 删除:${shardLines.length} 行已全部并入暂存主台账(compact 归并形态,放行)`);
409
+ } else {
410
+ problems.push(`- [分片删除丢行] ${rel} 被删除,但其中 ${missing.length} 行未出现在暂存主台账(compact 归并须先并入再删)`);
174
411
  }
175
- sPrev = null;
176
412
  continue;
177
413
  }
178
- if (sPrev !== null && !sCur.subarray(0, sPrev.length).equals(sPrev)) {
414
+ if (!sCur.subarray(0, sHead.length).equals(sHead)) {
179
415
  let off = 0;
180
- while (off < Math.min(sPrev.length, sCur.length) && sPrev[off] === sCur[off]) off++;
181
- problems.push(`- [分片前缀破坏] ${sha.slice(0, 10)}:${shard} 历史内容被改写(首异偏移 ${off},旧 ${sPrev.length}B → 新 ${sCur.length}B)——分片同受 append-only 前缀不变量约束`);
416
+ while (off < Math.min(sHead.length, sCur.length) && sHead[off] === sCur[off]) off++;
417
+ problems.push(`- [分片重写] ${rel} 暂存内容不是 HEAD 的前缀扩展(首异偏移 ${off},HEAD ${sHead.length}B → 暂存 ${sCur.length}B)——分片同受 append-only 前缀不变量约束`);
418
+ }
419
+ }
420
+ if (problems.length) {
421
+ console.error('台账不可变(--staged)— BLOCK:\n' + problems.join('\n'));
422
+ console.error(` 修法:台账历史行禁删改——误写走追加补偿行(node .agents/scripts/confirm-doc.mjs ...);确需撤销历史提交请 git revert 后重新追加。\n 所有者明令删除某批文档须同笔暂存 ${PRUNE_REL}(逐行 sha256 + 授权 + 理由),门禁逐行对账后才放行。`);
423
+ if (problems.some((p) => p.startsWith('- [分片重写] '))) {
424
+ console.error(' 分片重写另因:分片按 doc 命名、单端追加;跨端并发确认同一 doc 须有 merge=jsonl-union 驱动(git config merge.jsonl-union.driver "node .agents/scripts/git-merge-jsonl.mjs %O %A %B" + .gitattributes 标 merge=jsonl-union)——驱动生效则合并自动取并集、不破坏前缀');
182
425
  }
183
- sPrev = sCur;
426
+ process.exit(1);
184
427
  }
428
+ process.exit(0);
429
+ }
430
+
431
+ // ---- 历史全扫(缺省)----
432
+ // 有提交史的路径才进历史扫描**与终树行级校验**——零史路径(装户/新仓/尚在暂存区)整体跳过,
433
+ // 与原实现「零提交历史即静默」的契约一致(否则新仓首版会被 doc 不存在误杀)。
434
+ const withHistory = new Set();
435
+ for (const rel of ledgerPaths()) {
436
+ const shas = scanHistory(rel, { allowShardDelete: isShard(rel) });
437
+ if (shas && shas.length) withHistory.add(rel);
185
438
  }
186
439
 
187
- // ---- 行级校验(终树)----
188
- // 取数走 ledger-loader 视图(2026-10-12 ledger-loader-p1 / ledger-doc-chain):主台账 + 分片
189
- // `.agents/confirmations.d/*.jsonl`。**视图序 = 主台账行在前、分片行在后**——故既有
190
- // `.agents/ledger-line-exempt.json` 的行号(基于主台账全量)不因引入分片而漂移。
191
- const rows = readLedgerRows(ROOT);
440
+ // ---- 行级校验(终树:主台账 + 各现存分片;仅覆盖有提交史的路径)----
441
+ // 取数走 ledger-loader 带来源视图(主台账在前、分片按文件名序在后)——既有
442
+ // .agents/ledger-line-exempt.json 的行号(校验序号即非空行序)不因引入分片而漂移;
443
+ // 报文带来源标签(坏行点名所在台账文件)。JSON 坏行以 null 占位计号并显式报「非 JSON 对象行」。
444
+ const entries = readLedgerEntries(ROOT).filter((en) => withHistory.has(en.rel));
192
445
  const EXEMPT_P = path.join(ROOT, '.agents', 'ledger-line-exempt.json');
193
446
  const exemptLines = new Set();
194
447
  try {
@@ -199,17 +452,17 @@ try {
199
452
  }
200
453
  } catch { /* 豁免表损坏 → 视同无豁免(fail-closed 于校验面) */ }
201
454
  let exemptCount = 0;
202
- // ① ts 单调口径(2026-10-12 ledger-doc-chain 配套修正):**按 doc 分组内单调**,跨 doc 物理序乱序不判。
203
- // 为什么必须改:git 并集合并(见 git-merge-jsonl.mjs)与分片归并都会把「别的 doc 的新行」追加到本文件
204
- // 尾部,而那些行的 ts 可能早于本文件末行的 ts → **必然**触发原「全表非降序」判据。这是正常协作的形态,
205
- // 不是篡改;让门禁因正常协作报红,只会逼出 `--no-verify`。
206
- // 仍保留的强判据:**同一 doc 内的 ts 倒挂照报**(该 doc 的确认时序倒挂是真问题),另加哈希链
207
- // (检查 15)覆盖跨 doc 的插行/改行。跨 doc 乱序仅计数出账、不阻断(可观测性保留)。
455
+ // ① ts 单调口径(2026-10-12 ledger-doc-chain 配套修正):**按 doc 分组内单调**,跨 doc 物理序乱序
456
+ // 不判、只计数出账。为什么与实战源实现的「单文件内非降序」不同:并集合并与分片归都会把「别的 doc
457
+ // 的新行」追加到本文件尾部,而那些行的 ts 可能早于本文件末行的 ts → 全表判据必然因正常协作报红,
458
+ // 逼出 --no-verify;kit 装户存量台账仍是主台账单文件多 doc 形态,per-doc 才是可满足的判据。
459
+ // 仍保留的强判据:**同一 doc 内的 ts 倒挂照报**;跨 doc 的插行/改行由检查 15 doc-chain 哈希链兜。
208
460
  const prevTsByDoc = new Map();
209
461
  let crossDocOutOfOrder = 0;
210
462
  let prevTsAny = '';
211
- rows.forEach((e, idx) => {
212
- const at = `第 ${idx + 1} 行`;
463
+ entries.forEach((en, idx) => {
464
+ const at = `${en.rel === REL ? '主台账' : en.rel} 第 ${idx + 1} 行`;
465
+ const e = en.row;
213
466
  if (exemptLines.has(idx + 1)) { exemptCount++; return; } // 事故行豁免(登记文件入库可对质)
214
467
  if (!e || typeof e !== 'object') { problems.push(`- [行级] ${at}:非 JSON 对象行`); return; }
215
468
  if (typeof e.ts !== 'string' || Number.isNaN(Date.parse(e.ts))) problems.push(`- [行级] ${at}:ts 非 ISO 字符串`);
@@ -232,8 +485,8 @@ rows.forEach((e, idx) => {
232
485
  if (e.source === 'chat-delegated' && !(typeof e.quote === 'string' && e.quote.trim())) problems.push(`- [行级] ${at}:chat-delegated 行缺非空 quote(原话对质面)`);
233
486
  if (typeof e.doc !== 'string' || !/^workflow\/(intents|specs|plans|incidents)\/[^/]+\.md$/.test(e.doc)) {
234
487
  problems.push(`- [行级] ${at}:doc 路径不在 workflow 四目录协议内`);
235
- } else if (!fs.existsSync(path.join(ROOT, e.doc))) {
236
- problems.push(`- [行级] ${at}:doc ${e.doc} 在当前树不存在`);
488
+ } else if (!docExists(e.doc)) {
489
+ problems.push(`- [行级] ${at}:doc ${e.doc} 在当前树不存在(四目录与归档批次均未命中)`);
237
490
  }
238
491
  if (!VALID_STAGES.has(e.stage)) problems.push(`- [行级] ${at}:stage「${e.stage || '缺失'}」不在合法跳转终态集`);
239
492
  });
@@ -243,8 +496,6 @@ if (crossDocOutOfOrder) {
243
496
  console.log(`ℹ️ 台账跨 doc 物理序 ts 乱序 ${crossDocOutOfOrder} 处(并集合并 / 分片归并的正常形态——不阻断;同 doc 内时序倒挂仍 hard 拦,插行/改行由检查 15 哈希链兜)`);
244
497
  }
245
498
  if (problems.length) {
246
- console.error(`台账不可变(历史全扫)— HARD-BLOCK:\n\n${problems.join('\n')}\n`);
247
- console.error(' 修法:台账历史行禁删改(append-only 前缀不变量)——误写只能追加补偿行;确需撤销历史提交请 git revert 后重新追加。篡改属审计事件,保留现场联系仓库所有者。');
248
- process.exit(1);
499
+ fail(`台账不可变(历史全扫)— HARD-BLOCK:\n\n${problems.join('\n')}\n\n 修法:台账历史行禁删改(append-only 前缀不变量)——误写只能追加补偿行;确需撤销历史提交请 git revert 后重新追加。篡改属审计事件,保留现场联系仓库所有者。`);
249
500
  }
250
- process.exit(0);
501
+ process.exit(0);