agentic-flow-kit 1.7.16 → 1.8.0
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/README.md +11 -7
- package/modules/hosts/claude/commands/wf-build.md +1 -1
- package/modules/hosts/claude/commands/wf-maintain.md +1 -1
- package/modules/hosts/claude/commands/wf-new-task.md +9 -8
- package/modules/hosts/claude/commands/wf-plan.md +1 -1
- package/modules/hosts/claude/commands/wf-review.md +1 -1
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-build.md +1 -1
- package/modules/hosts/codex/commands/wf-maintain.md +1 -1
- package/modules/hosts/codex/commands/wf-new-task.md +9 -8
- package/modules/hosts/codex/commands/wf-plan.md +1 -1
- package/modules/hosts/codex/commands/wf-review.md +1 -1
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-build.md +1 -1
- package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
- package/modules/hosts/cursor/commands/wf-new-task.md +9 -8
- package/modules/hosts/cursor/commands/wf-plan.md +1 -1
- package/modules/hosts/cursor/commands/wf-review.md +1 -1
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-build.md +1 -1
- package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
- package/modules/hosts/opencode/commands/wf-new-task.md +9 -8
- package/modules/hosts/opencode/commands/wf-plan.md +1 -1
- package/modules/hosts/opencode/commands/wf-review.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +1 -1
- package/modules/hosts/trae/commands/wf-maintain.md +1 -1
- package/modules/hosts/trae/commands/wf-new-task.md +9 -8
- package/modules/hosts/trae/commands/wf-plan.md +1 -1
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +6 -2
- package/src/doctor.mjs +11 -2
- package/src/executor.mjs +25 -8
- package/src/executor.test.mjs +4 -1
- package/src/fresh-init.test.mjs +1 -1
- package/src/init.mjs +20 -3
- package/src/profiles.mjs +7 -3
- package/src/rule-budget.test.mjs +2 -2
- package/src/stack-profile.test.mjs +6 -0
- package/src/validate.mjs +6 -1
- package/src/validate.test.mjs +1 -1
- package/templates/.gitattributes +4 -1
- package/templates/AGENTS.md +2 -2
- package/templates/_agents/commands/build.md +1 -1
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/new-task.md +9 -8
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/commands/test.md +1 -1
- package/templates/_agents/engine-lock.json +130 -42
- package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
- package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
- package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
- package/templates/_agents/lane-surfaces.txt +4 -1
- package/templates/_agents/level-hints.txt +1 -1
- package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
- package/templates/_agents/mechanism-registry.md +27 -0
- package/templates/_agents/rule-budgets.txt +2 -2
- package/templates/_agents/scripts/agg-delegations.cjs +21 -3
- package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
- package/templates/_agents/scripts/agg-judgments.mjs +80 -0
- package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
- package/templates/_agents/scripts/archive-sweep.mjs +233 -0
- package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
- package/templates/_agents/scripts/check-attribution.mjs +37 -0
- package/templates/_agents/scripts/check-convergence.mjs +119 -0
- package/templates/_agents/scripts/check-engine-integrity.mjs +4 -1
- package/templates/_agents/scripts/check-funnel.mjs +56 -0
- package/templates/_agents/scripts/check-hygiene.mjs +5 -5
- package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
- package/templates/_agents/scripts/check-lane-surface.mjs +2 -2
- package/templates/_agents/scripts/check-lane-surface.test.mjs +5 -5
- package/templates/_agents/scripts/check-ledger-invariant.mjs +403 -152
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +433 -3
- package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -5
- package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
- package/templates/_agents/scripts/check-loop.mjs +215 -34
- package/templates/_agents/scripts/check-loop.test.mjs +570 -49
- package/templates/_agents/scripts/check-prejudgment.mjs +59 -0
- package/templates/_agents/scripts/check-registry.mjs +45 -0
- package/templates/_agents/scripts/check-scope.mjs +96 -0
- package/templates/_agents/scripts/check-scope.test.mjs +96 -0
- package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
- package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
- package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
- package/templates/_agents/scripts/compact-ledgers.mjs +3 -3
- package/templates/_agents/scripts/confirm-doc.mjs +114 -38
- package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
- package/templates/_agents/scripts/edit-face-check.mjs +5 -3
- package/templates/_agents/scripts/fill-intent.mjs +5 -3
- package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
- package/templates/_agents/scripts/fill-plan.mjs +10 -6
- package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
- package/templates/_agents/scripts/fill-spec.mjs +6 -3
- package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
- package/templates/_agents/scripts/fixture-scripts.mjs +2 -2
- package/templates/_agents/scripts/fixture-scripts.test.mjs +6 -1
- package/templates/_agents/scripts/gate-checklist.mjs +6 -0
- package/templates/_agents/scripts/gate-seg.mjs +1 -1
- package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
- package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
- package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
- package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +8 -3
- package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +3 -3
- package/templates/_agents/scripts/git-core.mjs +45 -0
- package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
- package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
- package/templates/_agents/scripts/kb-search.mjs +19 -4
- package/templates/_agents/scripts/kb-search.test.mjs +33 -0
- package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
- package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
- package/templates/_agents/scripts/ledger-shards.mjs +307 -0
- package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
- package/templates/_agents/scripts/level-criteria.mjs +9 -10
- package/templates/_agents/scripts/level-criteria.test.mjs +7 -5
- package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
- package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
- package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
- package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
- package/templates/_agents/scripts/pipeline-run.mjs +6 -22
- package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
- package/templates/_agents/scripts/policy.mjs +28 -0
- package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
- package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
- package/templates/_agents/scripts/rewrite-file.mjs +133 -0
- package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
- package/templates/_agents/scripts/rule-budget.mjs +83 -0
- package/templates/_agents/scripts/run-tests.mjs +99 -0
- package/templates/_agents/scripts/run-tests.test.mjs +105 -0
- package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
- package/templates/_agents/scripts/stage-gates.mjs +34 -12
- package/templates/_agents/scripts/trust-mode.mjs +4 -4
- package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
- package/templates/_agents/scripts/verify.mjs +135 -58
- package/templates/_agents/scripts/verify.test.mjs +156 -47
- package/templates/_agents/scripts/wf-journal.mjs +8 -4
- package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
- package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
- package/templates/_agents/scripts-test-exempt.txt +1 -0
- package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
- package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
- package/templates/_agents/workflow-enums.txt +8 -1
- package/templates/_githooks/pre-commit +110 -90
- package/templates/_githooks/pre-merge-commit +27 -23
- package/templates/_githooks/pre-push +4 -3
- package/templates/_github/workflows/kit-ci.yml +4 -3
- package/templates/workflow/README.md +14 -7
- package/templates/workflow/incidents/_TEMPLATE.md +4 -1
- package/templates/workflow/intents/_TEMPLATE.md +4 -2
- package/templates/workflow/papercuts.md +2 -0
- package/templates/workflow/plans/_TEMPLATE.md +10 -5
- package/templates/workflow/regression-checklist.md +1 -0
- package/templates/workflow/specs/_TEMPLATE.md +17 -1
- package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
- package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
- package/templates/_agents/scripts/rule-budget.sh +0 -74
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lane-surface-audit.test.mjs — 闭集审计 fixture 套件(2026-10-13 自 Shipyard.Material 参考实现回灌)
|
|
3
|
+
// 方法:临时 git 仓构造「L2 文档 + 同笔代码提交」历史,断言候选目录判定(阈值/ERE 覆盖排除/无候选静默)。
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const SCRIPT = path.join(SCRIPT_DIR, 'lane-surface-audit.mjs');
|
|
12
|
+
|
|
13
|
+
let pass = 0;
|
|
14
|
+
let fail = 0;
|
|
15
|
+
const check = (desc, cond, detail = '') => {
|
|
16
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
17
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const mkrepo = () => {
|
|
21
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'lane-audit-test-'));
|
|
22
|
+
const g = (a) => spawnSync('git', ['-c', 'core.quotepath=off', ...a], { cwd: d, encoding: 'utf8' });
|
|
23
|
+
g(['init', '-q', '-b', 'main']); g(['config', 'user.email', 't@t']); g(['config', 'user.name', 't']);
|
|
24
|
+
for (const s of ['intents', 'incidents']) fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
|
|
25
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
26
|
+
w(d, '.agents/lane-surfaces.txt', '## product\n^backend/shared/\n');
|
|
27
|
+
const commit = (msg) => { g(['add', '-A']); g(['commit', '-q', '-m', msg]); };
|
|
28
|
+
return { d, g, commit };
|
|
29
|
+
};
|
|
30
|
+
const w = (root, rel, content) => { fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), content); };
|
|
31
|
+
const intent = (name, level, date) => `---\n状态: done\n级别: ${level}\n日期: ${date}\n模块: pipeline\n---\n# INTENT — ${name}\n`;
|
|
32
|
+
const run = (root, ...args) => {
|
|
33
|
+
const r = spawnSync(process.execPath, [SCRIPT, '--root', root, ...args], { encoding: 'utf8' });
|
|
34
|
+
return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// 场景 1:3 个不同 L2 任务反复触及未覆盖目录 → 候选;已覆盖目录(backend/shared)不进候选
|
|
38
|
+
{
|
|
39
|
+
const { d, commit } = mkrepo();
|
|
40
|
+
for (let i = 1; i <= 3; i++) {
|
|
41
|
+
w(d, `workflow/intents/2026-10-0${i}-t${i}.md`, intent(`t${i}`, 'L2', `2026-10-0${i}`));
|
|
42
|
+
w(d, `backend/material/Shipyard.Material.Application/Services/S${i}.cs`, `// ${i}\n`);
|
|
43
|
+
w(d, 'backend/shared/Covered.cs', `// c${i}\n`); // 同任务也触已覆盖面
|
|
44
|
+
commit(`task ${i}`);
|
|
45
|
+
}
|
|
46
|
+
const r = run(d, '--since', '2026-10-01');
|
|
47
|
+
check('场景 1:未覆盖目录 3 任务 → 候选列出且提示人工评估;已覆盖目录不出现',
|
|
48
|
+
r.code === 0 && r.out.includes('候选漏登面 1 个') && r.out.includes('backend/material/Shipyard.Material.Application') && r.out.includes('lane-surfaces.txt') && !r.out.includes('backend/shared/'), r.out);
|
|
49
|
+
}
|
|
50
|
+
// 场景 2:阈值未达(2 个任务)→ 无候选静默
|
|
51
|
+
{
|
|
52
|
+
const { d, commit } = mkrepo();
|
|
53
|
+
for (let i = 1; i <= 2; i++) {
|
|
54
|
+
w(d, `workflow/intents/2026-10-0${i}-t${i}.md`, intent(`t${i}`, 'L2', `2026-10-0${i}`));
|
|
55
|
+
w(d, `backend/material/Shipyard.Material.Application/Services/S${i}.cs`, `// ${i}\n`);
|
|
56
|
+
commit(`task ${i}`);
|
|
57
|
+
}
|
|
58
|
+
const r = run(d, '--since', '2026-10-01');
|
|
59
|
+
check('场景 2:2 任务未达阈值 3 → 无漂移信号', r.code === 0 && r.out.includes('无漂移信号'), r.out);
|
|
60
|
+
}
|
|
61
|
+
// 场景 3:L1 文档不计数(级别过滤)+ --min 覆盖(2 任务 + min 2 → 候选)
|
|
62
|
+
{
|
|
63
|
+
const { d, commit } = mkrepo();
|
|
64
|
+
for (let i = 1; i <= 2; i++) {
|
|
65
|
+
w(d, `workflow/intents/2026-10-0${i}-l${i}.md`, intent(`l${i}`, 'L1', `2026-10-0${i}`));
|
|
66
|
+
w(d, 'frontend/src/views/Some/V.vue', `// ${i}\n`);
|
|
67
|
+
commit(`l ${i}`);
|
|
68
|
+
}
|
|
69
|
+
const r1 = run(d, '--since', '2026-10-01');
|
|
70
|
+
check('场景 3a:纯 L1 不入审计面(无 L2/L3 文档)', r1.code === 0 && r1.out.includes('无 L2/L3 文档'), r1.out);
|
|
71
|
+
const { d: d2, commit: c2 } = mkrepo();
|
|
72
|
+
for (let i = 1; i <= 2; i++) {
|
|
73
|
+
w(d2, `workflow/intents/2026-10-0${i}-t${i}.md`, intent(`t${i}`, 'L2', `2026-10-0${i}`));
|
|
74
|
+
w(d2, 'frontend/src/views/Some/V.vue', `// ${i}\n`);
|
|
75
|
+
c2(`t ${i}`);
|
|
76
|
+
}
|
|
77
|
+
const r2 = run(d2, '--since', '2026-10-01', '--min', '2');
|
|
78
|
+
check('场景 3b:--min 2 时 2 任务 → 候选', r2.code === 0 && r2.out.includes('frontend/src/views'), r2.out);
|
|
79
|
+
}
|
|
80
|
+
// 场景 4:incident 等价(发现日期键)入审计面
|
|
81
|
+
{
|
|
82
|
+
const { d, commit } = mkrepo();
|
|
83
|
+
for (let i = 1; i <= 3; i++) {
|
|
84
|
+
w(d, `workflow/incidents/2026-10-0${i}-inc${i}.md`, `---\n状态: closed\n级别: L3\n发现: 2026-10-0${i}\n模块: pipeline\n---\n# INCIDENT\n`);
|
|
85
|
+
w(d, `db/procs/P${i}.sql`, `-- ${i}\n`);
|
|
86
|
+
commit(`inc ${i}`);
|
|
87
|
+
}
|
|
88
|
+
const r = run(d, '--since', '2026-10-01');
|
|
89
|
+
check('场景 4:incident(发现键)×3 → db/procs 候选', r.code === 0 && r.out.includes('db/procs'), r.out);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
|
|
93
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ledger-shards.mjs — 三个 append-only 台账的分片读写单源(2026-10-13 ledger-shard-ledgers,自源仓回灌)
|
|
3
|
+
//
|
|
4
|
+
// 背景:workflow/{regression-checklist,delegations,papercuts}.md 是 append-only 活文档,每次追加
|
|
5
|
+
// 全量重写整文件——源仓实测 blob 膨胀(regression-checklist 单文件 14.0 MiB、delegations 11.5 MiB,
|
|
6
|
+
// 膨胀/冲突实测数据出处留痕在包源仓 git 历史与 CHANGELOG),且多 worktree 并行时每次合并都可能冲突。
|
|
7
|
+
//
|
|
8
|
+
// 分片形态(沿 confirmations.d 先例,2026-10-12 ledger-shard-default):
|
|
9
|
+
// workflow/<ledger>.d/<YYYY-MM>.md —— 按月切分,每月一个文件
|
|
10
|
+
// 读侧 = 主台账 ∪ 全部分片(按文件名排序,月序即时间序);写侧 = 追加进当月分片。
|
|
11
|
+
// 主台账保留「头部说明 + 维护规则 + 表头」等人类阅读面,数据行迁入分片。
|
|
12
|
+
//
|
|
13
|
+
// 为什么按月而非按条目:三个台账的数据行天然带日期列(首列 YYYY-MM-DD),月边界即天然分片键;
|
|
14
|
+
// 同月并发追加落同一文件(冲突面从「全历史」缩到「当月」),跨月并发零冲突。
|
|
15
|
+
//
|
|
16
|
+
// 合并驱动:.gitattributes 的 merge=jsonl-union 只覆盖 .agents/*.jsonl;本模块的 .md 分片走
|
|
17
|
+
// 追加式表格行,冲突面已由月切分压到最小,不另配驱动(如需可后续加 merge=union)。
|
|
18
|
+
//
|
|
19
|
+
// kit 适配(源仓→kit,2026-10-13 B8 批):表头签名对齐 kit 模板形态——delegations 两表末列为
|
|
20
|
+
// 「合入结论」(源仓为「返工原因」)、regression-checklist 表头为「incident | 条目 | 落点」
|
|
21
|
+
// (源仓为「来源 incident | 回归条目 | 落点」)。数据行判据不变(首列日期即数据行),表头签名
|
|
22
|
+
// 只进分片渲染。分片仍带节标题:kit 侧 wf-journal.delegationSectionRows 按 `## 委派结果` 节门
|
|
23
|
+
// 读行(kit check-loop 检查 18 走表头签名、不依赖节标题——与源仓相反;节标题 + 表头都带则两边读侧全兼容)。
|
|
24
|
+
//
|
|
25
|
+
// 用法(库):
|
|
26
|
+
// import { readLedger, shardPathFor, appendRows } from './ledger-shards.mjs';
|
|
27
|
+
// const { main, shards, all } = readLedger(root, 'delegations');
|
|
28
|
+
// 用法(CLI,迁移/巡检/追加):
|
|
29
|
+
// node .agents/scripts/ledger-shards.mjs --check # 巡检:主台账残留数据行 / 分片可读性
|
|
30
|
+
// node .agents/scripts/ledger-shards.mjs --split <ledger> # 把主台账数据行按月迁入分片(幂等)
|
|
31
|
+
// node .agents/scripts/ledger-shards.mjs --append <ledger> --row "<行>" [--month YYYY-MM]
|
|
32
|
+
// 测试:node .agents/scripts/ledger-shards.test.mjs
|
|
33
|
+
import fs from 'node:fs';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
|
|
36
|
+
// 受管台账清单(单源):键 = 台账基名,值 = 数据行判定
|
|
37
|
+
// - 表格式台账(delegations/papercuts):整文件数据行按月迁出
|
|
38
|
+
// - 分节台账(regression-checklist):只有「防复发验证」节是 append-only 追加区(incident 追加区),
|
|
39
|
+
// 其余模块节是稳定参考内容——故用 section 限定,只迁该节的数据行
|
|
40
|
+
export const LEDGERS = {
|
|
41
|
+
'regression-checklist': {
|
|
42
|
+
section: '防复发验证', // 只迁此节内的数据行(节标题前缀匹配)
|
|
43
|
+
row: /^\s*\|\s*\d{4}-\d{2}-\d{2}/, // 表格式:首列日期
|
|
44
|
+
},
|
|
45
|
+
delegations: {
|
|
46
|
+
row: /^\s*\|\s*\d{4}-\d{2}-\d{2}/,
|
|
47
|
+
},
|
|
48
|
+
papercuts: {
|
|
49
|
+
row: /^\s*\|\s*\d{4}-\d{2}-\d{2}/,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 表格式台账的表头签名(分片须自带表头——读侧按表头识别数据边界,见 agg-delegations.splitTables)。
|
|
54
|
+
// delegations 有两张表(委派表 / 自做表),分片须两张都带且行归入正确表下。
|
|
55
|
+
// **归属判据 = 原节标题**(`## 委派结果` / `## 自做任务结果`),不用列数——末列「合入结论」可缺省、
|
|
56
|
+
// 存量行不回填,两表都可能出现 5 列行,列数无法区分两表。
|
|
57
|
+
// **分片须同时带节标题**:kit 读侧 wf-journal.delegationSectionRows 要求行落在 `## 委派结果`
|
|
58
|
+
// 节内(节门);只带表头会让 wf-journal 读到 0 行。kit check-loop 检查 18 与
|
|
59
|
+
// agg-delegations.splitTables 走表头签名(节标题只服务人类阅读)——节标题 + 表头都带则两边读侧全兼容。
|
|
60
|
+
const TABLE_HEADERS = {
|
|
61
|
+
delegations: [
|
|
62
|
+
{ section: '委派结果', header: ['## 委派结果', '', '| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |', '|------|----------------|------------|------|------|----------|'] },
|
|
63
|
+
{ section: '自做任务结果', header: ['## 自做任务结果', '', '| 日期 | 任务一句话 | 结果 | 备注 | 合入结论 |', '|------|------------|------|------|----------|'] },
|
|
64
|
+
],
|
|
65
|
+
papercuts: [
|
|
66
|
+
{ section: null, header: ['| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|'] },
|
|
67
|
+
],
|
|
68
|
+
'regression-checklist': [
|
|
69
|
+
{ section: null, header: ['| incident | 条目 | 落点 |', '|----------|------|------|'] },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// tableIndexOf(name, line, section):该行属于哪张表。
|
|
74
|
+
// 多表台账(delegations)按**原节标题**归属(列数不可靠,见 TABLE_HEADERS 注释);
|
|
75
|
+
// 单表台账恒 0。section 为行所在节的标题(调用方按 `## ` 滚动维护)。
|
|
76
|
+
function tableIndexOf(name, line, section = '') {
|
|
77
|
+
const specs = TABLE_HEADERS[name];
|
|
78
|
+
if (!specs || specs.length === 1) return 0;
|
|
79
|
+
for (let i = 0; i < specs.length; i++) {
|
|
80
|
+
if (specs[i].section && String(section).startsWith(specs[i].section)) return i;
|
|
81
|
+
}
|
|
82
|
+
return 0; // 无节信息(旧分片/裸行)→ 归首表(委派表),沿原主台账行序语义
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function ledgerPath(root, name) {
|
|
86
|
+
return path.join(root, 'workflow', `${name}.md`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function shardDir(root, name) {
|
|
90
|
+
return path.join(root, 'workflow', `${name}.d`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function shardPathFor(root, name, month) {
|
|
94
|
+
return path.join(shardDir(root, name), `${month}.md`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// monthOf(line):从数据行提取分片月(YYYY-MM)。表格式取首列日期;清单式无日期 → 返回 ''(调用方兜底)。
|
|
98
|
+
export function monthOf(name, line) {
|
|
99
|
+
const m = /(\d{4})-(\d{2})-\d{2}/.exec(line);
|
|
100
|
+
return m ? `${m[1]}-${m[2]}` : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// isDataRow(name, line):该行是否属于「应迁入分片」的数据行(不含节上下文)。
|
|
104
|
+
export function isDataRow(name, line) {
|
|
105
|
+
const spec = LEDGERS[name];
|
|
106
|
+
if (!spec) return false;
|
|
107
|
+
if (!spec.row.test(line)) return false;
|
|
108
|
+
// 表头/分隔行排除
|
|
109
|
+
if (/^\s*\|/.test(line)) {
|
|
110
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
111
|
+
if (!cells.length || cells.every((c) => /^[-: ]*$/.test(c))) return false;
|
|
112
|
+
if (cells[0] === '日期' || cells[0] === '来源 incident' || cells[0] === 'incident') return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// inScope(name, section):当前节是否属于该台账的迁移范围(无 section 限定 → 恒 true)。
|
|
118
|
+
export function inScope(name, section) {
|
|
119
|
+
const spec = LEDGERS[name];
|
|
120
|
+
if (!spec || !spec.section) return true;
|
|
121
|
+
return String(section).startsWith(spec.section);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// readLedger(root, name):主台账 ∪ 全部分片。返回 { main, shards: [{file, text}], all }。
|
|
125
|
+
// 分片按文件名排序(月序 = 时间序);读失败 fail-loud(指标缺数会喂错门禁,沿 agg-delegations 口径)。
|
|
126
|
+
export function readLedger(root, name) {
|
|
127
|
+
const mainP = ledgerPath(root, name);
|
|
128
|
+
let main = '';
|
|
129
|
+
if (fs.existsSync(mainP)) {
|
|
130
|
+
try { main = fs.readFileSync(mainP, 'utf8'); }
|
|
131
|
+
catch (err) { throw new Error(`台账读取失败: ${mainP} (${err.message})`); }
|
|
132
|
+
}
|
|
133
|
+
const dir = shardDir(root, name);
|
|
134
|
+
const shards = [];
|
|
135
|
+
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
|
|
136
|
+
let files;
|
|
137
|
+
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); }
|
|
138
|
+
catch (err) { throw new Error(`分片目录遍历失败: ${dir} (${err.message})`); }
|
|
139
|
+
for (const f of files) {
|
|
140
|
+
const p = path.join(dir, f);
|
|
141
|
+
try { shards.push({ file: f, text: fs.readFileSync(p, 'utf8') }); }
|
|
142
|
+
catch (err) { throw new Error(`分片读取失败: ${f} (${err.message})`); }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const all = [main, ...shards.map((s) => s.text)].join('\n');
|
|
146
|
+
return { main, shards, all };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// readLedgerText(root, name):便捷形态——只要合并文本(读侧最小改动接入点)。
|
|
150
|
+
export function readLedgerText(root, name) {
|
|
151
|
+
return readLedger(root, name).all;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// readDelegationsText(root, file):delegations 合并文本(主台账 ∪ delegations.d/*.md)。
|
|
155
|
+
// 供读侧消费者共用(单源,禁各自拼路径):
|
|
156
|
+
// - agg-delegations.splitTables(表头签名识别)
|
|
157
|
+
// - check-loop.delegationResultRows(表头签名识别,检查 18)
|
|
158
|
+
// - wf-journal.delegationSectionRows(`## 委派结果` 节门)
|
|
159
|
+
// file 语义:**仅当它不是本仓默认主台账路径时**才作显式覆盖(测试注入临时文件用);
|
|
160
|
+
// 传默认路径(消费者按 `path.join(ROOT,'workflow','delegations.md')` 拼出来的)→ 走分片合并,
|
|
161
|
+
// 否则分片被静默跳过(2026-10-13 源仓实测踩中:检查 18 读到 0 行)。
|
|
162
|
+
export function readDelegationsText(root, file = null) {
|
|
163
|
+
const def = ledgerPath(root, 'delegations');
|
|
164
|
+
if (file && path.resolve(file) !== path.resolve(def)) {
|
|
165
|
+
try { return fs.readFileSync(file, 'utf8'); } catch { return ''; }
|
|
166
|
+
}
|
|
167
|
+
try { return readLedgerText(root, 'delegations'); } catch { return ''; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// splitMain(root, name, { dryRun }):把主台账里的数据行按月迁入分片(幂等:已迁走的行不再出现)。
|
|
171
|
+
// 返回 { moved: {month: count}, kept: 主台账保留行数 }。主台账保留非数据行(说明/规则/表头/生成区)。
|
|
172
|
+
export function splitMain(root, name, { dryRun = false } = {}) {
|
|
173
|
+
const mainP = ledgerPath(root, name);
|
|
174
|
+
if (!fs.existsSync(mainP)) throw new Error(`台账不存在: ${mainP}`);
|
|
175
|
+
const raw = fs.readFileSync(mainP, 'utf8');
|
|
176
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
|
|
177
|
+
const lines = raw.split(/\r?\n/);
|
|
178
|
+
const kept = [];
|
|
179
|
+
const byMonth = new Map();
|
|
180
|
+
let section = '';
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
const h = /^##\s+(.+?)\s*$/.exec(line);
|
|
183
|
+
if (h) section = h[1];
|
|
184
|
+
if (!inScope(name, section) || !isDataRow(name, line)) { kept.push(line); continue; }
|
|
185
|
+
const month = monthOf(name, line);
|
|
186
|
+
if (!month) { kept.push(line); continue; } // 无日期行不迁
|
|
187
|
+
if (!byMonth.has(month)) byMonth.set(month, []);
|
|
188
|
+
byMonth.get(month).push({ line, section }); // 带节信息(多表台账按节归属表)
|
|
189
|
+
}
|
|
190
|
+
const moved = {};
|
|
191
|
+
for (const [month, rows] of byMonth) moved[month] = rows.length;
|
|
192
|
+
if (dryRun) return { moved, kept: kept.length, byMonth };
|
|
193
|
+
// 写分片:已存在的分片保留原内容,新行追加——**按行去重**(幂等:重跑不重复追加)。
|
|
194
|
+
// 2026-10-13 源仓实测踩中:无去重时二次 split 把 58 行重复写入(120 行 vs 62 行)。
|
|
195
|
+
for (const [month, rows] of byMonth) {
|
|
196
|
+
const p = shardPathFor(root, name, month);
|
|
197
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
198
|
+
const existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
|
|
199
|
+
const existingRows = new Set(existing.split(/\r?\n/).filter((l) => l.trim()));
|
|
200
|
+
const fresh = rows.filter((r) => !existingRows.has(r.line));
|
|
201
|
+
if (!fresh.length) continue; // 全部已在分片 → 不写(幂等)
|
|
202
|
+
const next = existing
|
|
203
|
+
? `${existing.replace(/\s*$/, '')}${eol}${fresh.map((r) => r.line).join(eol)}${eol}`
|
|
204
|
+
: renderShard(name, fresh, eol);
|
|
205
|
+
fs.writeFileSync(p, next, 'utf8');
|
|
206
|
+
}
|
|
207
|
+
// 主台账:保留行 + 尾部空行规整
|
|
208
|
+
const out = kept.join(eol).replace(/(\r?\n)+$/, '') + eol;
|
|
209
|
+
fs.writeFileSync(mainP, out, 'utf8');
|
|
210
|
+
return { moved, kept: kept.length };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// renderShard(name, rows, eol):完整分片内容 = 表头 + 行(行归入各自表头之下)。
|
|
214
|
+
// 多表台账(delegations)必须逐表「表头 + 该表行」交错输出——否则读侧 splitTables 会把
|
|
215
|
+
// 后一张表的行按前一张表解析(2026-10-13 源仓实测:240 任务塌成 100)。
|
|
216
|
+
// rows 项 = { line, section }(section 为原节标题,用于表归属);rows 为空 → 出全部表头(空分片骨架)。
|
|
217
|
+
export function renderShard(name, rows, eol = '\n') {
|
|
218
|
+
const specs = TABLE_HEADERS[name];
|
|
219
|
+
if (!specs) return rows.length ? `${rows.map((r) => r.line ?? r).join(eol)}${eol}` : '';
|
|
220
|
+
const out = [];
|
|
221
|
+
for (let i = 0; i < specs.length; i++) {
|
|
222
|
+
const mine = rows.filter((r) => tableIndexOf(name, r.line ?? r, r.section) === i);
|
|
223
|
+
if (rows.length && !mine.length) continue; // 有行但本表无行 → 不出空表头
|
|
224
|
+
out.push(...specs[i].header, ...mine.map((r) => r.line ?? r));
|
|
225
|
+
}
|
|
226
|
+
return out.length ? `${out.join(eol)}${eol}` : '';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// appendRows(root, name, rows, { month, section }):把数据行追加进指定月分片(缺省取行内日期月)。
|
|
230
|
+
// rows 可为字符串数组或 { line, section } 数组(多表台账须带 section 才能归对表)。
|
|
231
|
+
// 返回实际写入的分片路径列表。写失败 fail-loud(不静默吞)。
|
|
232
|
+
export function appendRows(root, name, rows, { month, section = '' } = {}) {
|
|
233
|
+
const written = [];
|
|
234
|
+
const byMonth = new Map();
|
|
235
|
+
for (const row of rows) {
|
|
236
|
+
const line = typeof row === 'string' ? row : row.line;
|
|
237
|
+
const sec = typeof row === 'string' ? section : (row.section ?? section);
|
|
238
|
+
const m = month || monthOf(name, line);
|
|
239
|
+
if (!m) throw new Error(`appendRows: 无法确定分片月(行无日期且未传 month): ${String(line).slice(0, 80)}`);
|
|
240
|
+
if (!byMonth.has(m)) byMonth.set(m, []);
|
|
241
|
+
byMonth.get(m).push({ line, section: sec });
|
|
242
|
+
}
|
|
243
|
+
for (const [m, rs] of byMonth) {
|
|
244
|
+
const p = shardPathFor(root, name, m);
|
|
245
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
246
|
+
const existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
|
|
247
|
+
const existingRows = new Set(existing.split(/\r?\n/).filter((l) => l.trim()));
|
|
248
|
+
const fresh = rs.filter((r) => !existingRows.has(r.line));
|
|
249
|
+
if (!fresh.length) { written.push(p); continue; } // 已在分片 → 幂等跳过
|
|
250
|
+
const next = existing
|
|
251
|
+
? `${existing.replace(/\s*$/, '')}\n${fresh.map((r) => r.line).join('\n')}\n`
|
|
252
|
+
: renderShard(name, fresh, '\n');
|
|
253
|
+
fs.writeFileSync(p, next, 'utf8');
|
|
254
|
+
written.push(p);
|
|
255
|
+
}
|
|
256
|
+
return written;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---- CLI:巡检 / 迁移 / 追加 ----
|
|
260
|
+
function main() {
|
|
261
|
+
const argv = process.argv.slice(2);
|
|
262
|
+
const root = process.cwd();
|
|
263
|
+
if (argv.includes('--check')) {
|
|
264
|
+
let bad = 0;
|
|
265
|
+
for (const name of Object.keys(LEDGERS)) {
|
|
266
|
+
const mainP = ledgerPath(root, name);
|
|
267
|
+
if (!fs.existsSync(mainP)) { console.log(`[skip] ${name}: 主台账不存在`); continue; }
|
|
268
|
+
const lines = fs.readFileSync(mainP, 'utf8').split(/\r?\n/);
|
|
269
|
+
const residual = lines.filter((l) => isDataRow(name, l)).length;
|
|
270
|
+
let shardRows = 0;
|
|
271
|
+
try {
|
|
272
|
+
const { shards } = readLedger(root, name);
|
|
273
|
+
for (const s of shards) shardRows += s.text.split(/\r?\n/).filter((l) => isDataRow(name, l)).length;
|
|
274
|
+
} catch (err) { console.error(`[FAIL] ${name}: ${err.message}`); bad++; continue; }
|
|
275
|
+
console.log(`[${residual ? 'WARN' : 'OK'}] ${name}: 主台账残留数据行 ${residual}|分片数据行 ${shardRows}`);
|
|
276
|
+
if (residual) bad++;
|
|
277
|
+
}
|
|
278
|
+
process.exit(bad ? 1 : 0);
|
|
279
|
+
}
|
|
280
|
+
const splitIdx = argv.indexOf('--split');
|
|
281
|
+
if (splitIdx >= 0) {
|
|
282
|
+
const name = argv[splitIdx + 1];
|
|
283
|
+
if (!LEDGERS[name]) { console.error(`--split 须指定台账名(${Object.keys(LEDGERS).join('|')})`); process.exit(2); }
|
|
284
|
+
const dry = argv.includes('--dry-run');
|
|
285
|
+
const r = splitMain(root, name, { dryRun: dry });
|
|
286
|
+
console.log(`${dry ? '[dry] ' : ''}${name}: 迁出 ${Object.values(r.moved).reduce((a, b) => a + b, 0)} 行 → ${JSON.stringify(r.moved)}|主台账保留 ${r.kept} 行`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// --append <ledger> --row "<行>" [--month YYYY-MM]:追加一行进当月分片(写侧唯一入口)
|
|
290
|
+
const appIdx = argv.indexOf('--append');
|
|
291
|
+
if (appIdx >= 0) {
|
|
292
|
+
const name = argv[appIdx + 1];
|
|
293
|
+
if (!LEDGERS[name]) { console.error(`--append 须指定台账名(${Object.keys(LEDGERS).join('|')})`); process.exit(2); }
|
|
294
|
+
const rowIdx = argv.indexOf('--row');
|
|
295
|
+
const row = rowIdx >= 0 ? argv[rowIdx + 1] : '';
|
|
296
|
+
if (!row) { console.error('--append 须带 --row "<数据行>"'); process.exit(2); }
|
|
297
|
+
const mIdx = argv.indexOf('--month');
|
|
298
|
+
const month = mIdx >= 0 ? argv[mIdx + 1] : undefined;
|
|
299
|
+
const written = appendRows(root, name, [row], { month });
|
|
300
|
+
console.log(`✓ 追加 1 行 → ${written.map((p) => path.relative(root, p).split(path.sep).join('/')).join(', ')}`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
console.error('用法:node .agents/scripts/ledger-shards.mjs --check | --split <ledger> [--dry-run] | --append <ledger> --row "<行>" [--month YYYY-MM]');
|
|
304
|
+
process.exit(2);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('ledger-shards.mjs')) main();
|