agentic-flow-kit 1.8.1 → 1.8.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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-design.md +1 -1
  4. package/modules/hosts/claude/commands/wf-new-task.md +1 -0
  5. package/modules/hosts/claude/commands/wf-review.md +1 -1
  6. package/modules/hosts/codex/commands/wf-build.md +1 -1
  7. package/modules/hosts/codex/commands/wf-design.md +1 -1
  8. package/modules/hosts/codex/commands/wf-new-task.md +1 -0
  9. package/modules/hosts/codex/commands/wf-review.md +1 -1
  10. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  11. package/modules/hosts/cursor/commands/wf-design.md +1 -1
  12. package/modules/hosts/cursor/commands/wf-new-task.md +1 -0
  13. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  14. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  15. package/modules/hosts/opencode/commands/wf-design.md +1 -1
  16. package/modules/hosts/opencode/commands/wf-new-task.md +1 -0
  17. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  18. package/modules/hosts/trae/commands/wf-build.md +1 -1
  19. package/modules/hosts/trae/commands/wf-design.md +1 -1
  20. package/modules/hosts/trae/commands/wf-new-task.md +1 -0
  21. package/modules/hosts/trae/commands/wf-review.md +1 -1
  22. package/package.json +1 -1
  23. package/src/jscpd-drift.test.mjs +3 -2
  24. package/templates/AGENTS.md +1 -0
  25. package/templates/_agents/commands/build.md +1 -1
  26. package/templates/_agents/commands/design.md +1 -1
  27. package/templates/_agents/commands/new-task.md +1 -0
  28. package/templates/_agents/commands/review.md +1 -1
  29. package/templates/_agents/engine-lock.json +32 -24
  30. package/templates/_agents/hooks/commit-check.cjs +57 -30
  31. package/templates/_agents/mechanism-registry.md +8 -0
  32. package/templates/_agents/notes/engine-internals.md +17 -0
  33. package/templates/_agents/rule-budgets.txt +10 -2
  34. package/templates/_agents/scripts/agg-judgments.mjs +77 -1
  35. package/templates/_agents/scripts/agg-judgments.test.mjs +49 -1
  36. package/templates/_agents/scripts/archive-sweep.mjs +3 -1
  37. package/templates/_agents/scripts/check-convergence.mjs +8 -3
  38. package/templates/_agents/scripts/check-lane-surface.mjs +9 -2
  39. package/templates/_agents/scripts/check-lane-surface.test.mjs +17 -3
  40. package/templates/_agents/scripts/check-ledger-invariant.mjs +109 -24
  41. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +29 -1
  42. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -14
  43. package/templates/_agents/scripts/check-loop.mjs +41 -3
  44. package/templates/_agents/scripts/check-loop.test.mjs +37 -0
  45. package/templates/_agents/scripts/check-scope.test.mjs +4 -1
  46. package/templates/_agents/scripts/check-scripts-syntax.mjs +33 -10
  47. package/templates/_agents/scripts/closeout-l1.mjs +217 -0
  48. package/templates/_agents/scripts/closeout-l1.test.mjs +105 -0
  49. package/templates/_agents/scripts/commit-check-trigger.test.mjs +49 -0
  50. package/templates/_agents/scripts/confirm-doc.mjs +27 -8
  51. package/templates/_agents/scripts/confirm-doc.test.mjs +68 -9
  52. package/templates/_agents/scripts/gen-workflow-metrics.mjs +158 -9
  53. package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +119 -0
  54. package/templates/_agents/scripts/lane-surface-audit.test.mjs +1 -1
  55. package/templates/_agents/scripts/ledger-shards.mjs +42 -5
  56. package/templates/_agents/scripts/ledger-shards.test.mjs +17 -0
  57. package/templates/_agents/scripts/mechanism-retire-watch.mjs +183 -0
  58. package/templates/_agents/scripts/mechanism-retire-watch.test.mjs +114 -0
  59. package/templates/_agents/scripts/pre-push.test.mjs +27 -11
  60. package/templates/_agents/scripts/rule-budget.mjs +14 -1
  61. package/templates/_agents/scripts/rule-budget.test.mjs +118 -0
  62. package/templates/_agents/scripts/solidify-task.mjs +3 -1
  63. package/templates/_agents/scripts/solidify-task.test.mjs +10 -10
  64. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  65. package/templates/_agents/scripts/verify.mjs +237 -78
  66. package/templates/_agents/scripts/verify.test.mjs +44 -0
  67. package/templates/_agents/scripts-test-exempt.txt +2 -1
  68. package/templates/_agents/skills/review-method/SKILL.md +3 -0
  69. package/templates/_githooks/pre-commit +9 -4
  70. package/templates/_githooks/pre-merge-commit +4 -3
  71. package/templates/_githooks/pre-push +129 -75
  72. package/templates/workflow/README.md +2 -1
  73. package/templates/workflow/intents/_TEMPLATE.md +5 -0
  74. package/templates/workflow/specs/_TEMPLATE.md +1 -1
@@ -5,6 +5,9 @@
5
5
  // 判据自多 worktree 合并丢行事故后的实战实现逐字回灌(出处留包源仓 git 历史与 CHANGELOG))
6
6
  // 目的:把「台账本地可写」的伪造面从事后对质级升为机器可检出级(能力评审审计维度扣分项)。
7
7
  // 台账不可变原则:confirmations 历史行禁删改——修正只能走追加补偿行(README 硬规则 6)。
8
+ // 性能(2026-10-16 自源仓 ledger-git-batch 回灌):历史全扫原为逐提交 `git show <sha>:<path>` +
9
+ // `git log -1 %P`,即 O(提交数) 次进程创建(Windows 单次 ~68ms,随历史线性膨胀);改为一次
10
+ // `git log` 取 sha+父、一次 `cat-file --batch` 取该路径全部 blob(O(路径数)),判据与输出不变。
8
11
  //
9
12
  // 分片形态(2026-10-10 中期方案;2026-10-12 起为 confirm-doc 默认写入口径):
10
13
  // .agents/confirmations.d/<doc 基名>.jsonl —— 按 doc 分片,跨机/跨 worktree 确认不同 doc 时
@@ -103,14 +106,50 @@ const VALID_STAGES = new Set(['approved', 'done', 'fixed', 'closed', 'superseded
103
106
  // 历史基线(机制 2026-10-13 ledger-history-baseline,详见头注):kit 缺省空 = 全量扫描
104
107
  const HISTORY_BASELINE = process.env.LEDGER_HISTORY_BASELINE || '';
105
108
 
106
- const git = (args, opts = {}) => execFileSync('git', args, { cwd: ROOT, encoding: 'buffer', maxBuffer: 64 * 1024 * 1024, ...opts });
107
109
  const gitText = (args) => execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' });
108
- const blobOf = (ref) => git(['show', ref]); // ref 必须为完整对象引用(<sha>:<path> / HEAD:<path> / :<path>)
109
- // tryBlob:安全读 blob,失败返回 null 且**不向 stderr 外泄 git fatal 噪声**(删片场景读当前提交必失败,
110
- // 那是预期路径不是错误——历史扫描的高频探针路径必须静默)。
110
+
111
+ // ---- git 对象批量读取(2026-10-16 自源仓 ledger-git-batch 回灌)----
112
+ // 动机:逐提交 `git show <sha>:<path>` 在 Windows 上 ~68ms/次(进程创建主导),主台账提交数 × 2 次
113
+ // (blob + 父列表)随历史线性膨胀;改为「一次 git log 取 sha+父」+「一次 cat-file --batch 取该路径
114
+ // 全部 blob」——spawn 数自 O(提交数) 降到 O(路径数),判据与输出不变。
115
+ const blobCache = new Map(); // ref -> Buffer | null(null = 对象不可读,与旧 tryBlob 同语义)
116
+ const prefetchBlobs = (refs) => {
117
+ const want = [...new Set(refs)].filter((r) => !blobCache.has(r));
118
+ if (!want.length) return;
119
+ const r = spawnSync('git', ['cat-file', '--batch'], {
120
+ cwd: ROOT, input: `${want.join('\n')}\n`, maxBuffer: 512 * 1024 * 1024,
121
+ });
122
+ // 非零 = 真错误(仓库不可读 / spawn 失败 / 输出超 maxBuffer),不是「对象不存在」——后者由
123
+ // `<ref> missing` 头表达且 status 仍为 0。此处必须响亮抛出:静默置 null 会让每个 blobOf 都判
124
+ // 「对象不可读」,把一次环境抖动伪装成满屏「台账删除」的假硬拦(判据失真比崩溃更坏)。
125
+ if (r.status !== 0) {
126
+ throw new Error(`git cat-file --batch 失败(exit ${r.status ?? r.error?.code}):${String(r.stderr || r.error?.message || '').trim().slice(0, 200)}`);
127
+ }
128
+ const buf = r.stdout;
129
+ let off = 0;
130
+ for (const ref of want) {
131
+ const nl = buf.indexOf(0x0a, off);
132
+ if (nl < 0) { blobCache.set(ref, null); continue; }
133
+ const header = buf.toString('utf8', off, nl);
134
+ off = nl + 1;
135
+ const m = /^[0-9a-f]+ \w+ (\d+)$/.exec(header); // 不匹配 = `<ref> missing` / ambiguous
136
+ if (!m) { blobCache.set(ref, null); continue; }
137
+ const size = Number(m[1]);
138
+ blobCache.set(ref, buf.subarray(off, off + size));
139
+ off += size + 1; // 内容后随一个 LF
140
+ }
141
+ };
142
+ // blobOf:ref 必须为完整对象引用(<sha>:<path> / HEAD:<path> / :<path>);不可读即抛(调用方据此判删除)
143
+ const blobOf = (ref) => {
144
+ if (!blobCache.has(ref)) prefetchBlobs([ref]);
145
+ const b = blobCache.get(ref);
146
+ if (b === null) throw new Error(`git cat-file: ${ref} 不可读`);
147
+ return b;
148
+ };
149
+ // tryBlob:安全读 blob,失败返回 null(缓存 miss 时单点回源;删片场景读当前提交必失败,是预期路径)。
111
150
  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;
151
+ if (!blobCache.has(ref)) prefetchBlobs([ref]);
152
+ return blobCache.get(ref);
114
153
  };
115
154
  const linesOf = (buf) => buf.toString('utf8').split('\n').filter((l, i, a) => !(l === '' && i === a.length - 1) && l !== '');
116
155
  const isShard = (rel) => rel.startsWith(`${SHARD_DIR}/`) && rel.endsWith('.jsonl');
@@ -187,13 +226,26 @@ const shardDeleteReason = (sha, rel) => {
187
226
  // allowShardDelete:分片允许「已归并后删片(或缺行已终树补登)」这一种合法消失形态;主台账永不因归并被删。
188
227
  // 返回该路径的提交史(空 = 从未入库:装户/新仓/尚在暂存区)——零史路径整体跳过,与原实现
189
228
  // 「零提交历史即静默」的契约一致(行级校验也只覆盖有史的路径,否则新仓首版会被 doc 不存在误杀)。
190
- function historyShas(rel) {
229
+ // 2026-10-16 ledger-git-batch:一次 git log 同取 %H 与 %P(父列表原先逐提交另起一次 spawn),
230
+ // 结果按 rel 缓存并回填 parentMap 供 parentShas 免 spawn 查询。
231
+ const historyCache = new Map(); // rel -> [{sha, parents}]
232
+ const parentMap = new Map(); // sha -> parents
233
+ function historyEntries(rel) {
234
+ if (historyCache.has(rel)) return historyCache.get(rel);
235
+ let entries = [];
191
236
  try {
192
237
  // --full-history:不做历史简化——TREESAME 合并也入列,防「ours 合并丢一侧行」被简化隐藏(审计门立场)
193
- return gitText(['log', '--full-history', '--format=%H', '--reverse', '--', rel]).split(/\s+/).filter(Boolean);
238
+ const out = gitText(['log', '--full-history', '--format=%H %P', '--reverse', '--', rel]);
239
+ entries = out.split(/\r?\n/).filter((l) => l.trim()).map((l) => {
240
+ const [sha, ...parents] = l.trim().split(/\s+/);
241
+ return { sha, parents };
242
+ });
194
243
  } catch {
195
- return [];
244
+ entries = [];
196
245
  }
246
+ for (const e of entries) parentMap.set(e.sha, e.parents);
247
+ historyCache.set(rel, entries);
248
+ return entries;
197
249
  }
198
250
 
199
251
  // 基线后提交集(含基线可达性探测;无基线声明或基线不在本仓 → null = 基线不生效,全史扫描)
@@ -210,8 +262,9 @@ const postBaselineSet = () => {
210
262
  return _postBaseline;
211
263
  };
212
264
 
213
- // 多父提交的父列表(%P 全父)
265
+ // 多父提交的父列表(%P 全父)——优先查 historyEntries 回填的 parentMap(免 spawn),未命中才回退 git
214
266
  const parentShas = (sha) => {
267
+ if (parentMap.has(sha)) return parentMap.get(sha);
215
268
  try {
216
269
  return gitText(['log', '-1', '--format=%P', sha]).split(/\s+/).filter(Boolean);
217
270
  } catch {
@@ -221,10 +274,12 @@ const parentShas = (sha) => {
221
274
 
222
275
  // 合并并集对账:cur 行集须恰为各父行集之并(无丢失、无凭空新增)。返回 null = 认可;否则返回原因。
223
276
  // 父提交无此路径(单侧新增)跳过该父;行比对按整行字符串(与 linesOf/前缀判据同粒度)。
224
- const mergeUnionProblem = (sha, rel, curBuf) => {
277
+ // 2026-10-16 自源仓回灌:签名自 sha 改收 parents 数组——--staged 合并态无提交 sha,由调用方传
278
+ // [HEAD, MERGE_HEAD…];历史全扫(传 parentShas(sha))与 --staged 共用本判据(不新开口子)。
279
+ const mergeUnionProblem = (parents, rel, curBuf) => {
225
280
  const curLines = new Set(linesOf(curBuf));
226
281
  const parentLines = new Set();
227
- for (const p of parentShas(sha)) {
282
+ for (const p of parents) {
228
283
  const b = tryBlob(`${p}:${rel}`);
229
284
  if (b === null) continue;
230
285
  for (const l of linesOf(b)) parentLines.add(l);
@@ -272,19 +327,26 @@ const mergePruneProblem = (parents, rel, curBuf, manifest) => {
272
327
  };
273
328
 
274
329
  function scanHistory(rel, { allowShardDelete }) {
275
- const allShas = historyShas(rel);
276
- if (!allShas.length) return allShas;
277
- let shas = allShas;
330
+ const allEntries = historyEntries(rel);
331
+ if (!allEntries.length) return [];
332
+ let entries = allEntries;
278
333
  // 历史基线:只走基线后提交(详见头注)——DAG 感知比对下各提交自对己父,无需线性化起点
279
334
  const post = postBaselineSet();
280
335
  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 授权,见脚本头注)`);
336
+ const kept = entries.filter((e) => post.has(e.sha));
337
+ if (kept.length !== entries.length) {
338
+ console.error(`[info] ${rel}:${entries.length - kept.length} 个前基线提交按 HISTORY_BASELINE 声明豁免(owner 授权,见脚本头注)`);
284
339
  }
285
- shas = kept;
340
+ entries = kept;
286
341
  }
287
- for (const sha of shas) {
342
+ // 批量预取本路径全部 blob(各提交版本 + 各父版本)——一次 cat-file --batch 取代逐提交 git show
343
+ const refs = [];
344
+ for (const e of entries) {
345
+ refs.push(`${e.sha}:${rel}`);
346
+ for (const p of e.parents) refs.push(`${p}:${rel}`);
347
+ }
348
+ prefetchBlobs(refs);
349
+ for (const { sha, parents } of entries) {
288
350
  let cur;
289
351
  try { cur = blobOf(`${sha}:${rel}`); } catch {
290
352
  // 删除提交不是静默边界(复核 P2 收口):主台账在 append-only 世界里永不应删除——任一历史提交删除
@@ -303,13 +365,12 @@ function scanHistory(rel, { allowShardDelete }) {
303
365
  }
304
366
  // DAG 感知基准:本提交第一父的 blob(dag-aware-base)——线性化「前一承」在分支汇合处会拿
305
367
  // 另一侧提交当基准产生伪前缀破坏;根提交(无父)视为新文件起步。
306
- const parents = parentShas(sha);
307
368
  const base = parents.length ? tryBlob(`${parents[0]}:${rel}`) : null;
308
369
  if (base === null) continue; // 首现(父无此路径):无基线可比,行级校验兜底
309
370
  if (parents.length >= 2) {
310
371
  // 多父提交无条件走并集对账(前缀通过不得短路——ours-resolution 合并树与第一父逐字节同 blob,
311
372
  // 嵌套判法对静默丢行全盲)
312
- const unionFail = mergeUnionProblem(sha, rel, cur);
373
+ const unionFail = mergeUnionProblem(parents, rel, cur);
313
374
  if (!unionFail) {
314
375
  console.error(`[info] ${sha.slice(0, 10)}:合并并集无丢失——union 合并语义认可,续扫`);
315
376
  continue;
@@ -336,7 +397,7 @@ function scanHistory(rel, { allowShardDelete }) {
336
397
  }
337
398
  }
338
399
  }
339
- return allShas; // 交回该路径的全量提交史,供调用方判定「有史才进行级校验」
400
+ return allEntries.map((e) => e.sha); // 交回该路径的全量提交史,供调用方判定「有史才进行级校验」
340
401
  }
341
402
 
342
403
  // ---- 台账路径全集:主台账 + 历史上出现过的全部分片 ----
@@ -363,6 +424,18 @@ function ledgerPaths() {
363
424
  }
364
425
 
365
426
  // ---- --staged:暂存台账面 ⊇ HEAD 前缀,或携带 manifest 的特许修剪;分片删除须已并入暂存主台账 ----
427
+ // 合并态口径(2026-10-16 自源仓 13f67ef7 回灌 staged-merge-union):MERGE_HEAD 在场时暂存内容是**多父
428
+ // 合并结果**,单看 HEAD 一父不够——合法合并形态下暂存 = 父母并集(− manifest removes),严格前缀等式
429
+ // 必然不等而误拦。此时在严格等格外**再试父母并集集合对账**——与历史全扫同一函数 mergeUnionProblem /
430
+ // mergePruneProblem、同一口径,不新开口子:并集须无丢失、无凭空行(既禁丢行也禁凭空行)。非合并态不启用。
431
+ const mergeParents = (() => {
432
+ const r = spawnSync('git', ['rev-parse', 'MERGE_HEAD'], { cwd: ROOT, encoding: 'utf8' }); // octopus 可多行
433
+ if (r.status !== 0) return null; // 非合并态;spawnSync 吞 stderr,不外泄 fatal 噪声
434
+ const shas = String(r.stdout || '').split(/\s+/).filter(Boolean);
435
+ if (!shas.length) return null;
436
+ const h = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT, encoding: 'utf8' });
437
+ return [String(h.stdout || '').trim(), ...shas];
438
+ })();
366
439
  if (staged) {
367
440
  const stagedHit = gitText(['-c', 'core.quotepath=off', 'diff', '--cached', '--name-only']).split(/\r?\n/).filter(Boolean);
368
441
  const touched = ledgerPaths().filter((rel) => stagedHit.includes(rel));
@@ -388,9 +461,21 @@ if (staged) {
388
461
  console.log(`台账不变量(--staged)— ${rel} 特许修剪通过:移除 ${m.removed} 行(${m.docs.length} 家族)——凭据 ${PRUNE_REL} 随笔入库,被删行可在 git 历史对质`);
389
462
  continue;
390
463
  }
464
+ // 合并态(2026-10-16 回灌):严格前缀不适用 → 先纯并集对账,再落 manifest 修剪集合对账
465
+ // (与历史全扫的判定顺序一致)。非合并态两者皆拒。
466
+ const unionFail = mergeParents ? mergeUnionProblem(mergeParents, rel, cur) : '非合并态';
467
+ if (!unionFail) {
468
+ console.log(`台账不变量(--staged)— ${rel} 合并并集无丢失认可(${mergeParents.length} 父)——暂存行集恰为父母行集之并(无丢失、无凭空行),union 语义与历史全扫同口径`);
469
+ continue;
470
+ }
471
+ const setErr = mergeParents ? mergePruneProblem(mergeParents, rel, cur, loadManifestFromRef(`:${PRUNE_REL}`, ROOT)) : '非合并态';
472
+ if (!setErr) {
473
+ console.log(`台账不变量(--staged)— ${rel} 合并携带特许修剪通过:父母并集集合对账认可(${mergeParents.length} 父)——凭据 ${PRUNE_REL} 随笔入库,被删行可在 git 历史对质`);
474
+ continue;
475
+ }
391
476
  let off = 0;
392
477
  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}` : ''}`);
478
+ problems.push(`- [台账重写] ${rel} 暂存内容不是 HEAD 的前缀扩展(首异偏移 ${off},HEAD ${head.length}B → 暂存 ${cur.length}B)${m && m.err ? `;特许修剪对账失败:${m.err}` : ''}${mergeParents ? `;合并并集集合对账亦失败:${setErr}` : ''}${mergeParents ? `;合并纯并集对账失败:${unionFail}` : ''}`);
394
479
  }
395
480
  continue;
396
481
  }
@@ -591,6 +591,34 @@ const mkMergeRepo = () => {
591
591
  fs.rmSync(d, { recursive: true, force: true });
592
592
  }
593
593
 
594
+ // M12(2026-10-16 自源仓 13f67ef7 回灌补夹具):合并态 --staged 删掉两父母共有行(不在 manifest
595
+ // removes 里)→ 仍拦。共享行被删是 union 语义下的真丢失——manifest 只申报了分支侧行,共有行无凭据消失。
596
+ const mkMergePruneRepo = () => {
597
+ const d = mkrepo();
598
+ const BR = G(d, ['symbolic-ref', '--short', 'HEAD']).stdout.trim();
599
+ const r2 = row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/intents/gone.md' });
600
+ const comp = row({ ts: '2026-10-13T00:00:00.000Z', stage: 'done', note: '补偿行:doc 仍在' });
601
+ append(d, r2); commitAll(d, 'main r2');
602
+ G(d, ['checkout', '-qb', 'br']);
603
+ fs.writeFileSync(ledPath(d), [row(), comp].join('\n') + '\n');
604
+ writeManifest(d, { ts: '2026-10-13T00:00:00.000Z', authority: 't', reason: 'branch prune', removes: [sha256Of(r2)], docs: ['gone'] });
605
+ commitAll(d, 'br prune + compensation');
606
+ G(d, ['checkout', '-q', BR]);
607
+ fs.writeFileSync(path.join(d, 'other.txt'), 'x');
608
+ commitAll(d, 'main unrelated change');
609
+ G(d, ['merge', '--no-commit', 'br']);
610
+ return { d, r2, comp };
611
+ };
612
+ {
613
+ const { d, comp } = mkMergePruneRepo();
614
+ fs.writeFileSync(ledPath(d), comp + '\n'); // 双父共有行 row() 被删,comp 也不在 removes 内
615
+ G(d, ['add', '-A']);
616
+ const r = run(d, ['--staged']);
617
+ check('M12 合并态 --staged 删双父共有行 → exit 1',
618
+ r.code === 1 && /合并仍丢失 manifest 外/.test(r.out), r.out);
619
+ fs.rmSync(d, { recursive: true, force: true });
620
+ }
621
+
594
622
  // ---- 历史基线(2026-10-13 ledger-history-baseline):owner 声明豁免前基线时代 ----
595
623
  // B1:前基线改写无凭据——无基线全史扫描拦;LEDGER_HISTORY_BASELINE 指到 HEAD 后豁免过
596
624
  // (kit 缺省无基线 = 全量扫描,与源实现「缺省基线指向自身仓提交、外来仓退化为全扫」同语义)
@@ -607,5 +635,5 @@ const mkMergeRepo = () => {
607
635
  fs.rmSync(d, { recursive: true, force: true });
608
636
  }
609
637
 
610
- console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
638
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
611
639
  process.exit(fail ? 1 : 0);
@@ -35,14 +35,6 @@ const check = (name, cond, detail = '') => {
35
35
  else { fail++; console.log(`FAIL ${name}${detail ? `\n ${String(detail).slice(0, 800)}` : ''}`); }
36
36
  };
37
37
 
38
- function shBin() {
39
- for (const bin of ['sh', 'bash']) {
40
- const r = spawnSync(bin, ['-c', 'echo ok'], { encoding: 'utf8' });
41
- if (!r.error && r.status === 0) return bin;
42
- }
43
- return null;
44
- }
45
-
46
38
  function gitRun(cwd, args) {
47
39
  const r = spawnSync(git, args, { cwd, encoding: 'utf8' });
48
40
  if (r.status !== 0) throw new Error(`${args.join(' ')}\n${r.stderr || r.stdout}`);
@@ -145,17 +137,19 @@ try {
145
137
  console.log('SKIP doctor 契约核对(装户位置无包源 src/)');
146
138
  }
147
139
 
148
- const sh = shBin();
149
- check('找到 sh 以跑 pre-push', !!sh);
150
- if (sh && !HOOK) console.log('SKIP pre-push 相关场景(未找到 _githooks / .githooks 下的 pre-push)');
151
- if (sh && HOOK) {
140
+ // pre-push 已 node 化(2026-10-16 自源仓回灌):直调 node 跑钩子,桩改 check-loop.mjs / commit-check.cjs
141
+ if (!HOOK) console.log('SKIP pre-push 相关场景(未找到 _githooks / .githooks 下的 pre-push)');
142
+ if (HOOK) {
152
143
  fs.mkdirSync(path.join(root, '.agents', 'scripts'), { recursive: true });
144
+ fs.mkdirSync(path.join(root, '.agents', 'hooks'), { recursive: true });
153
145
  const node = process.execPath.replace(/\\/g, '/');
154
146
  const loopSh = LOOP.replace(/\\/g, '/');
155
- fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.sh'), `#!/bin/sh\nexec "${node}" "${loopSh}" "$@"\n`);
147
+ fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.mjs'), `import { spawnSync } from 'node:child_process';\nconst r = spawnSync(${JSON.stringify(node)}, [${JSON.stringify(loopSh)}, ...process.argv.slice(2)], { stdio: 'inherit' });\nprocess.exit(r.status ?? 1);\n`);
148
+ // 构建门桩:零退出占位——本套件测 --rev 扫描面,不测构建触发
149
+ fs.writeFileSync(path.join(root, '.agents', 'hooks', 'commit-check.cjs'), 'process.exit(0);\n');
156
150
  const hookPath = path.join(root, 'pre-push');
157
151
  fs.copyFileSync(HOOK, hookPath);
158
- const push = (input) => spawnSync(sh, [hookPath], { cwd: root, input, encoding: 'utf8', timeout: 120000 });
152
+ const push = (input) => spawnSync(process.execPath, [hookPath], { cwd: root, input, encoding: 'utf8', timeout: 120000 });
159
153
  const onlyDelete = push(`refs/heads/old ${Z} refs/heads/old ${'b'.repeat(40)}\n`);
160
154
  check('stdin 只有删除行:不阻断', onlyDelete.status === 0, outOf(onlyDelete));
161
155
  const mixed = push(
@@ -92,6 +92,8 @@
92
92
  // quote 字段职责冲突(对质凭据 vs 并录指纹,现归还单一职责)。无 batch 的历史行静默跳过
93
93
  // (无判定依据的行不产出不可消除噪声——沿 audit-gate-hardening P3 教训)
94
94
  // 17. 发版提交树上仍未收口的 intent/spec/plan [hard-block]
95
+ // (2026-10-16 快径:当前树 package.json 无 version 字段 → 整段跳过——无 version 则版本变化提交
96
+ // 永不存在恒不触发,省全历史 git 扫描;源仓「registry 登记恒不触发」同款口径)
95
97
  // 18. 委派台账对账 [warning](policy v4 加 delegationSince 生效日豁免:日期早于锚的 L2/L3 存量豁免——
96
98
  // delegations.md 记法自始不含文件名、无豁免时 113 条不可消退;缺键 v1-v3 维持原全量对账)
97
99
  // 19. 逐阶段审计(起草先于入口确认 / 台账审批顺序倒置) [warning]
@@ -926,6 +928,12 @@ export function runCheckLoop(opts = {}) {
926
928
  {
927
929
  const accCutoff = '2026-09-12';
928
930
  let legacyUnaccounted = 0;
931
+ // 测试绿缺凭证聚合面(2026-10-18 告警聚合口径批):**判据不变**(同一批文档/证据项照常检出),
932
+ // 只改输出形态——单文档多证据项只计一份;跨运行当日去重(同文档当日只逐份点名一次,已点名的
933
+ // 进一条聚合计数行,防静默被误读为已修)。背景:源仓 9 天遥测该 WARN 占全部告警 61%
934
+ //(1062/1738,文档密集期 14.8 条/运行)——复读刷屏稀释 WARN 信号价值。状态面放 os.tmpdir()
935
+ // 按 ROOT 派生键(本地机判、不进装户仓、无 gitignore 依赖、fail-open:读失败=全点名,落失败不影响裁决)。
936
+ const verifyMissingDocs = new Map(); // rel -> base(去重键 = 仓内相对路径)
929
937
  // 测试绿凭证状态(2026-10-07 verify-evidence):台账读一次循环外缓存;policy 缺 verifySince
930
938
  //(v1-v4)→ ctxBase null → 对账整体跳过(向后兼容)
931
939
  const verifySinceOf = kitPolicy.verifySince;
@@ -1027,9 +1035,7 @@ export function runCheckLoop(opts = {}) {
1027
1035
  const hasImplEvidence = results.some((r) => r.t.ok && (r.t.type === 'sha' || r.t.type === 'process'));
1028
1036
  for (const r of results) {
1029
1037
  if (r.t.ok && r.t.type === 'verify-missing') {
1030
- // ⚠️ 文案锚点硬约束(既有 check-loop.test.mjs:2743 锚定「测试绿缺凭证」四字连续,零改动前提):
1031
- // doc 与修复命令只能追加在整句之后,不得插进这四个字中间。
1032
- warnings.push(`- [WARN 测试绿缺凭证] ${base} 验收证据声明「测试绿」但 24h 内无 verify 凭证(verifications.jsonl${verifyDocBound ? `;本单须带 --doc 落账` : ''})——跑 node .agents/scripts/verify.mjs${verifyDocBound ? ` --doc ${rel}` : ''} 后重跑(advisory,灰度第一档)`);
1038
+ verifyMissingDocs.set(rel, base); // 出账统一在循环外(聚合/当日去重,见声明处注释)
1033
1039
  continue;
1034
1040
  }
1035
1041
  if (r.t.ok) continue;
@@ -1118,6 +1124,31 @@ export function runCheckLoop(opts = {}) {
1118
1124
  legacyUnaccounted++;
1119
1125
  }
1120
1126
  }
1127
+ // 测试绿缺凭证出账(判据见 collector;⚠️ 文案锚点硬约束:既有 check-loop.test.mjs 锚定
1128
+ // 「测试绿缺凭证」四字连续,doc 与修复命令只能追加在整句之后,不得插进四字中间):
1129
+ // 当日首次命中的文档逐份点名(可操作明细);当日已点名的聚合成一条计数行。
1130
+ if (verifyMissingDocs.size) {
1131
+ const today = new Date().toISOString().slice(0, 10);
1132
+ const statePath = path.join(os.tmpdir(), `agentic-flow-kit-check8-warn-${ROOT.replace(/[^A-Za-z0-9]/g, '-').slice(-60)}.json`);
1133
+ let warnedToday = {};
1134
+ try { warnedToday = JSON.parse(fs.readFileSync(statePath, 'utf8')); } catch { /* 无状态/坏状态 = 空集 → 全部点名 */ }
1135
+ const silent = [];
1136
+ for (const [rel, base] of verifyMissingDocs) {
1137
+ if (warnedToday[rel] === today) silent.push({ rel, base });
1138
+ else {
1139
+ warnings.push(`- [WARN 测试绿缺凭证] ${base} 验收证据声明「测试绿」但 24h 内无 verify 凭证(verifications.jsonl${verifyDocBound ? `;本单须带 --doc 落账` : ''})——跑 node .agents/scripts/verify.mjs${verifyDocBound ? ` --doc ${rel}` : ''} 后重跑(advisory,灰度第一档)`);
1140
+ }
1141
+ }
1142
+ if (silent.length) {
1143
+ warnings.push(`- [WARN 测试绿缺凭证] 另有 ${silent.length} 份在飞文档同日已点名、去重静默(${silent.slice(0, 5).map((d) => d.base).join('、')}${silent.length > 5 ? '…' : ''})——修法同上:跑 node .agents/scripts/verify.mjs${verifyDocBound ? ' --doc <本单>' : ''} 后重跑(advisory,灰度第一档)`);
1144
+ }
1145
+ try {
1146
+ // 只留当日命中(隔日自动失效,状态面自洁;本机各 ROOT 独立键,互不串扰)
1147
+ const next = {};
1148
+ for (const [rel] of verifyMissingDocs) next[rel] = today;
1149
+ fs.writeFileSync(statePath, JSON.stringify(next));
1150
+ } catch { /* 落状态失败不影响裁决与输出(fail-open,advisory 层不因状态面故障改行为面) */ }
1151
+ }
1121
1152
  if (legacyUnaccounted > 0) {
1122
1153
  warnings.push(`- [WARN 验收对账存量] ${legacyUnaccounted} 个存量 done intent 验收标准未对账(生效日锚之前加入仓库,豁免 hard,不回填)`);
1123
1154
  }
@@ -1422,7 +1453,13 @@ export function runCheckLoop(opts = {}) {
1422
1453
  // policy.check17UnclosedAfter 时阻断。版本 1 没有该键,因此不启用后两格。
1423
1454
  // 版本按主、次、修订三段整数比较。沿 package.json 的全部历史查找,不设次数上限。
1424
1455
  // 该提交之后新建的文件不在范围内。open 的 incident 不在此列。
1456
+ // 快径(2026-10-16 减重 自源仓回灌):当前树 package.json 无 version 字段(装户非 node 仓 / 快照树)
1457
+ // → 整段跳过——无 version 则「version 变化的提交」永不存在,本段恒不触发,省去全历史 git log+show
1458
+ // 扫描。适配点:源仓 s17 为独立模块函数用 return 快返;kit 单体函数内改 if 包裹,保后续段照常执行。
1425
1459
  {
1460
+ let curVer = null;
1461
+ try { curVer = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version; } catch { curVer = null; }
1462
+ if (curVer) {
1426
1463
  const log = gitOut(['log', '--format=%H', '--', 'package.json']);
1427
1464
  if (log) {
1428
1465
  let release = null;
@@ -1452,6 +1489,7 @@ export function runCheckLoop(opts = {}) {
1452
1489
  }
1453
1490
  }
1454
1491
  }
1492
+ }
1455
1493
  }
1456
1494
  } // laneOn(17) 守卫闭合
1457
1495
 
@@ -2792,6 +2792,43 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
2792
2792
  rmfix(T2);
2793
2793
  }
2794
2794
 
2795
+ // ---- 检查 8 测试绿缺凭证聚合口径(2026-10-18 告警聚合批):判据不变、只改输出形态 ----
2796
+ // ① 单文档多「测试绿」证据项 → 首跑只出一份明细(per-doc 去重,不再逐项刷屏);
2797
+ // ② 同一夹具当日第二次运行 → 明细静默 + 聚合计数行点名(跨运行当日去重——不是不报,是不复读;
2798
+ // 静默必须被计数行点名,防「没输出」被误读为「已修」)。
2799
+ {
2800
+ const mkVeAgg = () => {
2801
+ const T = mkfix();
2802
+ gitInit(T);
2803
+ w(T, '.agents/kit.json', JSON.stringify({ policyVersion: 5 }));
2804
+ const iBody = '# INTENT — vea\n\n## 验收标准(可测试)\n- [x] 用例A通过(证据:测试全绿)\n- [x] 用例B通过(证据:测试全绿)\n';
2805
+ const pBody = '# PLAN — vea\n';
2806
+ const fpI = computeFingerprint(`---\n状态: approved\n级别: L1\n日期: 2026-10-18\n---\n${iBody}`);
2807
+ const fpP = computeFingerprint(`---\n状态: approved\n级别: L1\n---\n${pBody}`);
2808
+ w(T, 'workflow/intents/2026-10-18-vea.md', `---\n状态: done\n级别: L1\n日期: 2026-10-18\n确认指纹: ${fpI.slice(0, 16)}\n---\n${iBody}`);
2809
+ w(T, 'workflow/plans/2026-10-18-vea.md', `---\n状态: done\n级别: L1\n确认指纹: ${fpP.slice(0, 16)}\n---\n${pBody}`);
2810
+ const now = new Date().toISOString();
2811
+ writeLedger(T, [
2812
+ { ts: now, doc: 'workflow/intents/2026-10-18-vea.md', stage: 'done', fingerprint: fpI, prev: 'approved' },
2813
+ { ts: now, doc: 'workflow/plans/2026-10-18-vea.md', stage: 'done', fingerprint: fpP, prev: 'approved' },
2814
+ ]);
2815
+ gitCommitAll(T, 'feat: vea fixture');
2816
+ return T;
2817
+ };
2818
+ const T = mkVeAgg();
2819
+ let r = run(T);
2820
+ const first = outOf(r).split('\n').filter((l) => l.includes('测试绿缺凭证'));
2821
+ check('检查 8 聚合口径:单文档多「测试绿」项 → 首跑只出一份明细',
2822
+ r.status === 0 && first.length === 1 && first[0].includes('2026-10-18-vea.md'),
2823
+ `exit=${r.status}\n${outOf(r)}`);
2824
+ r = run(T);
2825
+ const second = outOf(r).split('\n').filter((l) => l.includes('测试绿缺凭证'));
2826
+ check('检查 8 聚合口径:同日二跑 → 明细静默 + 聚合计数行(去重不吞债)',
2827
+ r.status === 0 && second.length === 1 && second[0].includes('另有 1 份在飞文档同日已点名') && second[0].includes('2026-10-18-vea.md'),
2828
+ `exit=${r.status}\n${outOf(r)}`);
2829
+ rmfix(T);
2830
+ }
2831
+
2795
2832
  // ---- 检查 8 凭证按 doc 绑定(2026-10-08 verify-doc-binding):v6 收紧 / v5 不变 ----
2796
2833
  // 漏洞本体:凭证行不带 doc 字段 + 判定只看「全仓任意一份 24h 内绿行」→ 跑一次 verify 就能给全仓
2797
2834
  // 任意一单的「测试绿」声明背书。本组三条:① v6 + 只有他单绿行 → 仍出 WARN(漏洞已堵);
@@ -4,8 +4,11 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
7
8
 
8
- const CLI = path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')), 'check-scope.mjs');
9
+ // fileURLToPath 而非手搓 pathname 解码(源仓 fdded128):URL pathname 对非 ASCII 路径百分号编码,
10
+ // Windows 中文 worktree 路径下子进程「Cannot find module」静默红。
11
+ const CLI = path.join(path.dirname(fileURLToPath(import.meta.url)), 'check-scope.mjs');
9
12
  let pass = 0, fail = 0;
10
13
  const check = (name, cond, detail = '') => {
11
14
  if (cond) { pass++; console.log(`PASS ${name}`); }
@@ -4,13 +4,16 @@
4
4
  // 任何文件不可解析即判红(先例教训:verify.mjs:31 SyntaxError 带病进 CI,全链门静默失效)。
5
5
  // 语义与红线:只读裁决,不改任何状态;检查目录缺失或可检文件数为 0 一律判红(fail-closed,
6
6
  // 防「改名/删除脚本即去门」);不 import 任何引擎件,零依赖可独立调度。
7
+ // 性能(2026-10-16 自源仓 scripts-syntax-parallel 回灌):node --check 无原地解析 ESM 的 API,
8
+ // 只能逐文件起子进程(单次 ~70ms × 全量脚本);改工作池并发(min(8, cpus-2)),判红集合不变。
7
9
  // 测试:node .agents/scripts/check-scripts-syntax.test.mjs
8
10
  // 用法:node .agents/scripts/check-scripts-syntax.mjs [目录...](相对仓库根;缺省 .agents/scripts .agents/hooks;
9
11
  // CHECK_LOOP_ROOT 可覆写仓库根,与 check-loop/verify 同口径)
10
12
  // 沿革:2026-10-09 guardian-selfcheck(审计 P0-1 立项,verify.mjs GATES 首门消费)。
11
13
 
12
14
  import fs from 'node:fs';
13
- import { spawnSync } from 'node:child_process';
15
+ import os from 'node:os';
16
+ import { spawn } from 'node:child_process';
14
17
  import path from 'node:path';
15
18
  import { fileURLToPath } from 'node:url';
16
19
 
@@ -21,8 +24,8 @@ const DEFAULT_DIRS = ['.agents/scripts', '.agents/hooks'];
21
24
  const dirs = process.argv.slice(2);
22
25
  const targets = dirs.length ? dirs : DEFAULT_DIRS;
23
26
 
24
- let total = 0;
25
- const reds = [];
27
+ // 守卫文件清单(目录缺失 fail-closed;可检文件数 0 fail-closed——防「改名/删除脚本即去门」)
28
+ const files = [];
26
29
  const missing = [];
27
30
  for (const d of targets) {
28
31
  const abs = path.resolve(ROOT, d);
@@ -34,14 +37,34 @@ for (const d of targets) {
34
37
  missing.push(d);
35
38
  continue;
36
39
  }
37
- for (const f of names.filter((n) => /\.(mjs|cjs)$/.test(n)).sort()) {
38
- total++;
39
- const r = spawnSync(process.execPath, ['--check', path.join(abs, f)], { encoding: 'utf8' });
40
- if (r.status !== 0) {
41
- const first = (r.stderr || '').split(/\r?\n/).find((l) => l.trim()) || `exit ${r.status}`;
42
- reds.push(`[syntax] ❌ ${d}/${f}: ${first}`);
40
+ for (const f of names.filter((n) => /\.(mjs|cjs)$/.test(n)).sort()) files.push({ rel: `${d}/${f}`, abs: path.join(abs, f) });
41
+ }
42
+ const total = files.length;
43
+
44
+ // node --check 逐文件起子进程(无 API 可原地解析 ESM 而不执行),工作池并发后墙钟 ≈ 单次 spawn ×
45
+ // ceil(文件数/并发)。裁决口径不变:任一文件不可解析即判红,判红文件按路径排序输出(并发下不回放乱序)。
46
+ const reds = [];
47
+ {
48
+ const conc = Math.max(2, Math.min(8, os.cpus().length - 2));
49
+ let cursor = 0;
50
+ const worker = async () => {
51
+ while (cursor < files.length) {
52
+ const t = files[cursor++];
53
+ const r = await new Promise((resolve) => {
54
+ const child = spawn(process.execPath, ['--check', t.abs]);
55
+ let err = '';
56
+ child.stderr.on('data', (d) => { err += d; });
57
+ child.on('error', (e) => resolve({ status: null, stderr: String(e.message) }));
58
+ child.on('close', (status) => resolve({ status, stderr: err }));
59
+ });
60
+ if (r.status !== 0) {
61
+ const first = (r.stderr || '').split(/\r?\n/).find((l) => l.trim()) || `exit ${r.status}`;
62
+ reds.push(`[syntax] ❌ ${t.rel}: ${first}`);
63
+ }
43
64
  }
44
- }
65
+ };
66
+ await Promise.all(Array.from({ length: Math.min(conc, files.length) }, worker));
67
+ reds.sort();
45
68
  }
46
69
 
47
70
  if (missing.length) {