agentic-flow-kit 1.8.1 → 1.8.2
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/modules/hosts/claude/commands/wf-new-task.md +1 -0
- package/modules/hosts/codex/commands/wf-new-task.md +1 -0
- package/modules/hosts/cursor/commands/wf-new-task.md +1 -0
- package/modules/hosts/opencode/commands/wf-new-task.md +1 -0
- package/modules/hosts/trae/commands/wf-new-task.md +1 -0
- package/package.json +1 -1
- package/templates/_agents/commands/new-task.md +1 -0
- package/templates/_agents/engine-lock.json +27 -19
- package/templates/_agents/hooks/commit-check.cjs +57 -30
- package/templates/_agents/notes/engine-internals.md +17 -0
- package/templates/_agents/rule-budgets.txt +7 -1
- package/templates/_agents/scripts/agg-judgments.mjs +77 -1
- package/templates/_agents/scripts/agg-judgments.test.mjs +49 -1
- package/templates/_agents/scripts/archive-sweep.mjs +3 -1
- package/templates/_agents/scripts/check-convergence.mjs +8 -3
- package/templates/_agents/scripts/check-lane-surface.mjs +9 -2
- package/templates/_agents/scripts/check-lane-surface.test.mjs +16 -2
- package/templates/_agents/scripts/check-ledger-invariant.mjs +30 -4
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +28 -0
- package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -14
- package/templates/_agents/scripts/check-loop.mjs +9 -0
- package/templates/_agents/scripts/closeout-l1.mjs +217 -0
- package/templates/_agents/scripts/closeout-l1.test.mjs +105 -0
- package/templates/_agents/scripts/commit-check-trigger.test.mjs +49 -0
- package/templates/_agents/scripts/confirm-doc.mjs +14 -2
- package/templates/_agents/scripts/confirm-doc.test.mjs +43 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +17 -0
- package/templates/_agents/scripts/ledger-shards.mjs +42 -5
- package/templates/_agents/scripts/ledger-shards.test.mjs +17 -0
- package/templates/_agents/scripts/mechanism-retire-watch.mjs +183 -0
- package/templates/_agents/scripts/mechanism-retire-watch.test.mjs +114 -0
- package/templates/_agents/scripts/pre-push.test.mjs +26 -10
- package/templates/_agents/scripts/rule-budget.mjs +14 -1
- package/templates/_agents/scripts/rule-budget.test.mjs +118 -0
- package/templates/_agents/scripts/verify.mjs +32 -8
- package/templates/_agents/scripts/verify.test.mjs +29 -0
- package/templates/_agents/scripts-test-exempt.txt +2 -1
- package/templates/_githooks/pre-commit +9 -4
- package/templates/_githooks/pre-merge-commit +4 -3
- package/templates/_githooks/pre-push +129 -75
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// rule-budget.test.mjs — 常驻面预算黑盒套件(2026-10-15 减重手段② 自源仓回灌随附:#count 条目数指令)
|
|
3
|
+
// 夹具:os.tmpdir 下临时目录(非 git 环境——脚本 git rev-parse 失败留在 fixture 目录,
|
|
4
|
+
// 字节口径回退工作区文件;与真实仓「索引/HEAD」口径的差异不影响指令语义判定)。
|
|
5
|
+
// 覆盖:字节超限/未超、#count 超限/压线、旧格式行不受 #count 影响、缺预算表 exit 0、
|
|
6
|
+
// --staged 无 git(staged 空)时 #count 门跳过。
|
|
7
|
+
// 运行:node templates/_agents/scripts/rule-budget.test.mjs
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'rule-budget.mjs');
|
|
15
|
+
|
|
16
|
+
let pass = 0;
|
|
17
|
+
let fail = 0;
|
|
18
|
+
const check = (desc, cond, detail = '') => {
|
|
19
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
20
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function makeFixture({ budgets, dirs = {}, files = {} }) {
|
|
24
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rule-budget-'));
|
|
25
|
+
if (budgets !== null) {
|
|
26
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
27
|
+
fs.writeFileSync(path.join(root, '.agents', 'rule-budgets.txt'), budgets, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
for (const [dir, names] of Object.entries(dirs)) {
|
|
30
|
+
fs.mkdirSync(path.join(root, dir), { recursive: true });
|
|
31
|
+
for (const n of names) fs.writeFileSync(path.join(root, dir, n), 'x\n', 'utf8');
|
|
32
|
+
}
|
|
33
|
+
for (const [f, content] of Object.entries(files)) {
|
|
34
|
+
fs.mkdirSync(path.dirname(path.join(root, f)), { recursive: true });
|
|
35
|
+
fs.writeFileSync(path.join(root, f), content, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const run = (cwd, mode = '--all') => {
|
|
41
|
+
const r = spawnSync(process.execPath, [SCRIPT, mode], { cwd, encoding: 'utf8' });
|
|
42
|
+
return { code: r.status, out: String(r.stdout || '') };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let root;
|
|
46
|
+
|
|
47
|
+
// ---- T1 字节超限(旧格式语义不回归)----
|
|
48
|
+
root = makeFixture({
|
|
49
|
+
budgets: 'AGENTS.md 10\n',
|
|
50
|
+
files: { 'AGENTS.md': 'x'.repeat(50) },
|
|
51
|
+
});
|
|
52
|
+
{
|
|
53
|
+
const r = run(root);
|
|
54
|
+
check('T1 字节超限 exit 1 + 违例行', r.code === 1 && r.out.includes('常驻面超限:AGENTS.md') && r.out.includes('> 上限 10B'), JSON.stringify(r));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---- T2 字节未超 → 0 ----
|
|
58
|
+
root = makeFixture({
|
|
59
|
+
budgets: 'AGENTS.md 100\n',
|
|
60
|
+
files: { 'AGENTS.md': 'x'.repeat(50) },
|
|
61
|
+
});
|
|
62
|
+
check('T2 字节未超 exit 0', run(root).code === 0);
|
|
63
|
+
|
|
64
|
+
// ---- T3 #count 超限 ----
|
|
65
|
+
root = makeFixture({
|
|
66
|
+
budgets: '.agents/rules/ 2#count\n',
|
|
67
|
+
dirs: { '.agents/rules': ['r01-a.md', 'r02-b.md', 'r03-c.md'] },
|
|
68
|
+
});
|
|
69
|
+
{
|
|
70
|
+
const r = run(root);
|
|
71
|
+
check('T3 #count 超限 exit 1 + 条目数违例行 + 一进一出提示', r.code === 1 && r.out.includes('条目数') && r.out.includes('3 项 > 上限 2 项') && r.out.includes('一进一出'), JSON.stringify(r));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- T4 #count 压线(= 上限)→ 0 ----
|
|
75
|
+
root = makeFixture({
|
|
76
|
+
budgets: '.agents/rules/ 3#count\n',
|
|
77
|
+
dirs: { '.agents/rules': ['r01-a.md', 'r02-b.md', 'r03-c.md'] },
|
|
78
|
+
});
|
|
79
|
+
check('T4 #count 压线 exit 0', run(root).code === 0);
|
|
80
|
+
|
|
81
|
+
// ---- T5 混合表:#count 行存在但未触其它字节条目 → 0(互不干扰)----
|
|
82
|
+
root = makeFixture({
|
|
83
|
+
budgets: 'AGENTS.md 100\n.agents/rules/ 3#count\n',
|
|
84
|
+
files: { 'AGENTS.md': 'x'.repeat(50) },
|
|
85
|
+
dirs: { '.agents/rules': ['r01-a.md', 'r02-b.md', 'r03-c.md'] },
|
|
86
|
+
});
|
|
87
|
+
check('T5 混合表双通过 exit 0', run(root).code === 0);
|
|
88
|
+
|
|
89
|
+
// ---- T6 缺预算表 → exit 0(fixture 沙箱不拦,既有语义)----
|
|
90
|
+
root = makeFixture({ budgets: null });
|
|
91
|
+
check('T6 缺预算表 exit 0', run(root).code === 0);
|
|
92
|
+
|
|
93
|
+
// ---- T7 --staged 无 git(staged 空):#count 门跳过不拦 ----
|
|
94
|
+
root = makeFixture({
|
|
95
|
+
budgets: '.agents/rules/ 2#count\n',
|
|
96
|
+
dirs: { '.agents/rules': ['r01-a.md', 'r02-b.md', 'r03-c.md'] },
|
|
97
|
+
});
|
|
98
|
+
{
|
|
99
|
+
const r = run(root, '--staged');
|
|
100
|
+
check('T7 --staged 无 git 环境跳过 #count 门 exit 0', r.code === 0, JSON.stringify(r));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- T8 glob 形态 #count(* 后缀)也按条数执法 ----
|
|
104
|
+
root = makeFixture({
|
|
105
|
+
budgets: '.agents/commands/*.md 2#count\n',
|
|
106
|
+
dirs: { '.agents/commands': ['a.md', 'b.md', 'c.md', 'd.txt'] },
|
|
107
|
+
});
|
|
108
|
+
{
|
|
109
|
+
const r = run(root);
|
|
110
|
+
check('T8 glob #count 只数 *.md(3 项 > 2)exit 1', r.code === 1 && r.out.includes('3 项'), JSON.stringify(r));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const d of fs.readdirSync(os.tmpdir()).filter((f) => f.startsWith('rule-budget-'))) {
|
|
114
|
+
try { fs.rmSync(path.join(os.tmpdir(), d), { recursive: true, force: true }); } catch { /* Windows 文件锁容忍 */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
118
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -24,9 +24,12 @@
|
|
|
24
24
|
// 提交——新装户由 init 生成器首跑种子该文件,缺文件对 --check 是漂移(红),不静默放行。
|
|
25
25
|
// 绿凭证摘要(gen-verify-evidence.mjs):全绿尾部自动重生成 workflow/verify-evidence.md——全绿事实与
|
|
26
26
|
// 仓内摘要同刻生成,消除「本地台账已绿、committed 摘要滞后」的双真相分叉;生成失败响亮告警但不推翻绿裁决。
|
|
27
|
-
// 用法:node .agents/scripts/verify.mjs [--list] [--test-cmd "<shell 命令>"] [--doc <本单 workflow 路径>]
|
|
28
|
-
// --list:按执行序打印门 id 后退出,不执行(与 --test-cmd 互斥)。
|
|
27
|
+
// 用法:node .agents/scripts/verify.mjs [--list] [--test-cmd "<shell 命令>"] [--stub-gate "<命令>"] [--doc <本单 workflow 路径>]
|
|
28
|
+
// --list:按执行序打印门 id 后退出,不执行(与 --test-cmd / --stub-gate 互斥)。
|
|
29
29
|
// --test-cmd 用于测试注入,或在无根 package.json 的装户仓指定项目测试命令;
|
|
30
|
+
// --stub-gate <命令>(2026-10-16 自源仓回灌):单门夹具但走**生产尾段**(落凭证 / 生成摘要)——
|
|
31
|
+
// 与 --test-cmd 的差异是不打 testCmd 标记、尾段按生产路径执行;**必须配 CHECK_LOOP_ROOT**(夹具根),
|
|
32
|
+
// 禁止在真实仓库写出可被当成关单凭证的绿行。
|
|
30
33
|
// --doc 把凭证绑到本单,如 --doc workflow/intents/<日期>-<主题>.md
|
|
31
34
|
// 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具)
|
|
32
35
|
import fs from 'node:fs';
|
|
@@ -59,6 +62,24 @@ const fixtureGates = (cmd) => [
|
|
|
59
62
|
const args = process.argv.slice(2);
|
|
60
63
|
const explicitTestCmd = args.includes('--test-cmd') ? args[args.indexOf('--test-cmd') + 1] : null;
|
|
61
64
|
|
|
65
|
+
// --stub-gate(2026-10-16 自源仓 13f67ef7 回灌):单门夹具但走**生产尾段**(落凭证 / 生成摘要)。
|
|
66
|
+
// 与 --test-cmd 的关键差异:不打 testCmd 标记(fixture 模式绿行带 testCmd:true,生产凭证不与之混淆)。
|
|
67
|
+
// **必须配 CHECK_LOOP_ROOT**——禁止在真实仓库写出可被当成关单凭证的绿行(信任边界:夹具单门不是关单凭证)。
|
|
68
|
+
const stubIdx = args.indexOf('--stub-gate');
|
|
69
|
+
const stubCmd = stubIdx >= 0 ? args[stubIdx + 1] : null;
|
|
70
|
+
if (stubIdx >= 0 && !stubCmd) {
|
|
71
|
+
console.error('[verify] --stub-gate 需要一条命令');
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
if (stubCmd && explicitTestCmd) {
|
|
75
|
+
console.error('[verify] --stub-gate 与 --test-cmd 互斥');
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
if (stubCmd && !process.env.CHECK_LOOP_ROOT) {
|
|
79
|
+
console.error('[verify] --stub-gate 只允许配合 CHECK_LOOP_ROOT 夹具根(禁止在真实仓库落无关单凭证)');
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
// --doc 解析(沿 --test-cmd 同款取下一 argv 的写法;缺值报用法错误 exit 1 与 --test-cmd 对称)。
|
|
63
84
|
// 归一基准 = **ROOT**(不是 cwd)——消费方两处都以仓库根为基准(check-loop 检查 8 用
|
|
64
85
|
// path.relative(ROOT, intent);confirm-doc 的 docRelOf 用 path.resolve(root, doc))。若这里按 cwd
|
|
@@ -78,12 +99,14 @@ const docRel = docArg
|
|
|
78
99
|
// 装户仓判定:engine-tests 门只在**当前目录**有 package.json 时才跑(npm test 亦在该目录执行)。
|
|
79
100
|
// 跳过时后续不落测试绿凭证(见全绿落账段)。显式 --test-cmd 不受此判定影响。
|
|
80
101
|
const hasRootPackageJson = fs.existsSync(path.join(process.cwd(), 'package.json'));
|
|
81
|
-
const skipTest = explicitTestCmd === null && !hasRootPackageJson;
|
|
82
|
-
const gates = explicitTestCmd ? fixtureGates(explicitTestCmd)
|
|
102
|
+
const skipTest = explicitTestCmd === null && stubCmd === null && !hasRootPackageJson;
|
|
103
|
+
const gates = explicitTestCmd ? fixtureGates(explicitTestCmd)
|
|
104
|
+
: stubCmd ? [{ id: 'stub', name: '夹具单门', cmd: stubCmd, folder: '.', type: 'shell' }]
|
|
105
|
+
: GATES;
|
|
83
106
|
|
|
84
107
|
if (args.includes('--list')) {
|
|
85
|
-
if (explicitTestCmd) {
|
|
86
|
-
console.error('[verify] --list 与 --test-cmd 互斥');
|
|
108
|
+
if (explicitTestCmd || stubCmd) {
|
|
109
|
+
console.error('[verify] --list 与 --test-cmd / --stub-gate 互斥');
|
|
87
110
|
process.exit(1);
|
|
88
111
|
}
|
|
89
112
|
for (const g of gates) console.log(g.id);
|
|
@@ -104,7 +127,7 @@ const recordFailure = (step, exitInfo, note) => {
|
|
|
104
127
|
}
|
|
105
128
|
};
|
|
106
129
|
|
|
107
|
-
console.log(`[verify] 🚀 开始静态门验证 (共 ${gates.length} 项)${explicitTestCmd ? '【fixture 模式】' : ''}\n`);
|
|
130
|
+
console.log(`[verify] 🚀 开始静态门验证 (共 ${gates.length} 项)${explicitTestCmd ? '【fixture 模式】' : ''}${stubCmd ? '【stub 尾段】' : ''}\n`);
|
|
108
131
|
|
|
109
132
|
let passedCount = 0;
|
|
110
133
|
const outputs = {};
|
|
@@ -173,7 +196,8 @@ try {
|
|
|
173
196
|
|
|
174
197
|
// 摘要事件原子化:全绿事实与仓内摘要(workflow/verify-evidence.md)同刻生成,消除「本地台账已绿、
|
|
175
198
|
// committed 摘要滞后」的双真相分叉。仅生产模式(fixture 假门凭证不产 committed 摘要;测试门跳过时
|
|
176
|
-
//
|
|
199
|
+
// 本就无凭证);stub 尾段按生产路径执行(摘要落在 CHECK_LOOP_ROOT 夹具根,不触真实仓)。
|
|
200
|
+
// 生成失败响亮告警但不推翻绿裁决(同落账口径)。
|
|
177
201
|
if (!explicitTestCmd && !skipTest) {
|
|
178
202
|
const gen = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'gen-verify-evidence.mjs'), '--root', ROOT], { encoding: 'utf8' });
|
|
179
203
|
if (gen.status === 0) {
|
|
@@ -257,6 +257,35 @@ const readLedger = (root) => {
|
|
|
257
257
|
fs.rmSync(root10, { recursive: true, force: true });
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// ---- 场景 12:--stub-gate 走生产尾段(落凭证;2026-10-16 自源仓 13f67ef7 回灌)----
|
|
261
|
+
// 单门夹具但按生产路径落绿行(不打 testCmd 标记);必须配 CHECK_LOOP_ROOT。
|
|
262
|
+
{
|
|
263
|
+
const root = mkfix();
|
|
264
|
+
fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
|
|
265
|
+
const git = (args) => spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
266
|
+
git(['init', '-q']);
|
|
267
|
+
git(['config', 'user.email', 't@t']);
|
|
268
|
+
git(['config', 'user.name', 't']);
|
|
269
|
+
fs.writeFileSync(path.join(root, 'README.md'), 'stub\n');
|
|
270
|
+
git(['add', '-A']);
|
|
271
|
+
git(['commit', '-qm', 'base']);
|
|
272
|
+
const r = run(['--stub-gate', `node ${mkCmd(0)}`], { env: { ...process.env, CHECK_LOOP_ROOT: root, RUN_E2E: '' } });
|
|
273
|
+
const l = readLedger(root)[0] || null;
|
|
274
|
+
check('场景 12:stub 单门 exit 0', r.status === 0, `exit=${r.status}\n${r.stdout}\n${r.stderr}`);
|
|
275
|
+
check('场景 12:落绿行(suite=npm test)且无 testCmd 标记',
|
|
276
|
+
l !== null && l.suite === 'npm test' && l.exitCode === 0 && !('testCmd' in l), JSON.stringify(l));
|
|
277
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
// 场景 12b:无 CHECK_LOOP_ROOT → exit 2,不跑门(信任边界:夹具单门不得当真实凭证)
|
|
280
|
+
{
|
|
281
|
+
const env = { ...process.env, RUN_E2E: '' };
|
|
282
|
+
delete env.CHECK_LOOP_ROOT;
|
|
283
|
+
const r = run(['--stub-gate', 'node -e "process.exit(0)"'], { env });
|
|
284
|
+
check('场景 12b:--stub-gate 无 CHECK_LOOP_ROOT → exit 2 且不跑门',
|
|
285
|
+
r.status === 2 && String(r.stderr).includes('CHECK_LOOP_ROOT') && !String(r.stdout).includes('全绿'),
|
|
286
|
+
`exit=${r.status}\n${r.stdout}\n${r.stderr}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
260
289
|
// ---- 场景 11:--list 锁定 engine cohort 七门序,不执行门 ----
|
|
261
290
|
{
|
|
262
291
|
const expect = ['scripts-syntax', 'engine-tests', 'workflows-check', 'rule-budget', 'effective-policy', 'check-loop', 'ledger-invariant'];
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
# 新增引擎脚本默认必须带同名 .test.mjs(README 规范条目);确属库件/工具才登记本文件。
|
|
5
5
|
# 2026-10-09 A2 评估:verify-wiki-consistency 已移出(补测 3 场景);wiki-search 已移出(转发壳,判定单源 kb-search.test 覆盖)。
|
|
6
6
|
check-metric-claims.mjs | 经 check-loop.mjs 消费 | 由 check-loop.test.mjs 间接覆盖其调用路径
|
|
7
|
-
|
|
7
|
+
# 2026-10-16 减重手段② 随附:rule-budget.mjs 登记撤除(补同名直测黑盒套件 T1-T8——字节语义不回归 +
|
|
8
|
+
# #count 条目数指令全形态;src/rule-budget.test.mjs 仍是包源预算自检门,职责不同并行保留)。
|
|
8
9
|
ensure-board.mjs | 看板启动器 | 永久工具豁免(需真实端口/进程环境,自动化覆盖无性价比——2026-10-09 engine-quality-round2 A2 评估定性)
|
|
9
10
|
policy.mjs | 库件 | 被 check-loop.test.mjs 直接 import 覆盖
|
|
10
11
|
# 2026-10-12 needs-clarification:stage-gates.mjs 已建同名直测(stage-gates.test.mjs 15 断言),
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 2026-10-13 门禁去 sh 化(B2,自 Shipyard.Material 参考实现回灌):钩子体不再含 sh 逻辑——git 仍以自带
|
|
4
4
|
// sh 启动本文件(钩子机制要求),但 shebang 即 node,PATH 无 sh 不再影响任何逻辑;语言单化后 CI 代理/
|
|
5
5
|
// 本地行为一致。写法为 CommonJS——无扩展名文件 node 缺省按 CJS 解析,不依赖 ≥22.7 的 ESM 语法探测。
|
|
6
|
-
// 通用门禁序:闭环配对(增量) → 泳道完整性(增量) → wiki 台账(增量) → 索引漂移(增量) → 常驻面体积预算(增量) → 确认台账不变量(暂存触台账) → 项目门禁(local-pre-commit) → 敏感信息扫描+条件构建(commit-check.cjs)
|
|
6
|
+
// 通用门禁序:闭环配对(增量) → 泳道完整性(增量) → wiki 台账(增量) → 索引漂移(增量) → 常驻面体积预算(增量) → 确认台账不变量(暂存触台账) → 项目门禁(local-pre-commit) → 敏感信息扫描+条件构建(commit-check.cjs;2026-10-16 起常规提交 --no-build——条件构建后移 pre-push 按推送范围执法,合并提交仍 --full 全量构建)
|
|
7
7
|
// 项目专属门禁(架构红线/业务守卫等)放 .agents/hooks/local-pre-commit——sh 挂载点(add-gate 生态按行接线),
|
|
8
8
|
// 存在即在本钩子内以 sh(缺则 bash)执行,返回非零即阻断提交
|
|
9
9
|
// 合并提交场景由 .githooks/pre-merge-commit 原样转发到本脚本(合并时 git 调的是 pre-merge-commit,
|
|
@@ -58,7 +58,10 @@ if (branchName.startsWith('experiment/')) {
|
|
|
58
58
|
// 检查 B「探索标记缺失」——experiment/* 分支暂存 intent 缺「阶段: exploring」→ 拦(标记零成本,
|
|
59
59
|
// advisory 免纪律不免标记——标记是加固门覆盖率的前提)。
|
|
60
60
|
// 脚本缺失 fail-closed 阻断(见 gate())。
|
|
61
|
-
|
|
61
|
+
// `git merge` 自动提交由 pre-merge-commit 传入 `--merge`(此时 MERGE_HEAD 已不可靠,不能靠探测);
|
|
62
|
+
// 冲突解决后的手动 `git commit` 仍由脚本自己看 MERGE_HEAD(2026-10-16 自源仓回灌)。
|
|
63
|
+
const mergeCommit = process.argv.includes('--merge');
|
|
64
|
+
gate('泳道完整性(增量)', '.agents/scripts/check-lane-surface.mjs', mergeCommit ? ['--merge'] : []);
|
|
62
65
|
|
|
63
66
|
// wiki 台账门禁(增量,check-wiki-ledger.mjs:暂存区触及 wiki/ 时跑 verify-wiki-consistency 三方一致性;
|
|
64
67
|
// 无 wiki/ 变更即跳过。脚本缺失 fail-closed)
|
|
@@ -105,12 +108,14 @@ if (fs.existsSync('.agents/hooks/local-pre-commit')) {
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
// 合并提交 → 构建验证覆盖【完整合并树】,不做按扩展名的条件裁剪。
|
|
111
|
+
// 常规提交 --no-build(2026-10-16 构建门后移 pre-push,自源仓回灌):提交内环只跑敏感扫描,条件构建在
|
|
112
|
+
// 推送时按 old..new 范围执法(.githooks/pre-push);合并结果是没人验证过的新状态,仍全量构建。
|
|
108
113
|
// 两条路径:`git merge` 自动提交由 .githooks/pre-merge-commit 显式传 `--full` 转发到本脚本;
|
|
109
114
|
// 冲突解决后手动 `git commit` 由 git 直接调本脚本,此处按 MERGE_HEAD 探测补上(此时合并仍在进行)。
|
|
110
|
-
let fullArgs = process.argv.slice(2);
|
|
115
|
+
let fullArgs = process.argv.slice(2).filter((a) => a !== '--merge');
|
|
111
116
|
if (fullArgs.length === 0) {
|
|
112
117
|
const mergeHead = spawnSync('git', ['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { encoding: 'utf8' });
|
|
113
|
-
|
|
118
|
+
fullArgs = mergeHead.status === 0 ? ['--full'] : ['--no-build'];
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
const cc = spawnSync(process.execPath, ['.agents/hooks/commit-check.cjs', ...fullArgs], { stdio: 'inherit' });
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
// 靠人工补跑静态门 + 全量测试才覆盖到)。
|
|
9
9
|
//
|
|
10
10
|
// 做法:转发到 .githooks/pre-commit(门禁清单只此一份,避免两份拷贝日后分叉),
|
|
11
|
-
// 并显式传 `--full`——本钩子「只对 merge 触发」这一身份就是最强信号,无需再去探测
|
|
11
|
+
// 并显式传 `--full` 与 `--merge`——本钩子「只对 merge 触发」这一身份就是最强信号,无需再去探测
|
|
12
12
|
// MERGE_HEAD(2026-09-19 实测:本时点 MERGE_HEAD 已不可靠)。`--full` 令编译验证
|
|
13
13
|
// 覆盖【完整合并树】、不按扩展名的条件裁剪:合并结果是没人验证过的新状态,且其 diff
|
|
14
|
-
//
|
|
14
|
+
// 相对第一父,裁剪会漏掉需要重跑的一侧。`--merge` 令泳道检查 A 跳过(2026-10-16 自源仓回灌:
|
|
15
|
+
// 同窗口 MERGE_HEAD 已不在,已关单产品路径会被误判「触达面判低」)。
|
|
15
16
|
//
|
|
16
17
|
// 冲突解决后手动 `git commit` 走的是 pre-commit 自身,那条路径由 pre-commit 内部的
|
|
17
18
|
// MERGE_HEAD 探测覆盖(此时合并仍在进行,MERGE_HEAD 确定存在)。
|
|
@@ -24,5 +25,5 @@ if (top.status !== 0 || !top.stdout.trim()) {
|
|
|
24
25
|
process.exit(1);
|
|
25
26
|
}
|
|
26
27
|
const hook = path.join(top.stdout.trim(), '.githooks', 'pre-commit');
|
|
27
|
-
const r = spawnSync(process.execPath, [hook, '--full'], { stdio: 'inherit' });
|
|
28
|
+
const r = spawnSync(process.execPath, [hook, '--full', '--merge'], { stdio: 'inherit' });
|
|
28
29
|
process.exit(r.status === null ? 1 : r.status);
|
|
@@ -1,82 +1,136 @@
|
|
|
1
|
-
#!/bin/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Git 原生 pre-push hook(经 core.hooksPath 挂载,git push 时触发)— agentic-flow-kit 通用编排
|
|
3
|
+
// 2026-10-16 node 化(自源仓 2ff869a9/13f67ef7 回灌 pre-push 面):钩子体不再含 sh 逻辑——git 仍以自带
|
|
4
|
+
// sh 启动本文件(钩子机制要求),但 shebang 即 node,PATH 无 sh 不影响任何逻辑。写法为 CommonJS——
|
|
5
|
+
// 无扩展名文件 node 缺省按 CJS 解析,不依赖 ≥22.7 的 ESM 语法探测。
|
|
6
|
+
// 跑 .agents/scripts/check-loop.mjs:扫 workflow/ 闭环骨架断档(默认只扫硬门)
|
|
7
|
+
// - 硬阻断(exit 1)= 配对断裂 / 回路断档 / 证据与确认链 / 发版未收口 → push 立即阻断
|
|
8
|
+
// - 卫生段(模板占位 / 引用漂移 / 宿主副本 / 索引)不在本钩子。要看时跑 --hygiene 或 --all
|
|
9
|
+
// stdin 有行时,每个非删除的本地 sha 用 --rev 扫那棵树。没有行时(手跑钩子)仍无参数扫工作区。
|
|
10
|
+
//
|
|
11
|
+
// 构建门(2026-10-16 自 pre-commit 后移):另跑 commit-check.cjs --range <old> <new> 按推送范围条件构建
|
|
12
|
+
// (常规提交时刻只做敏感扫描——提交内环提速;构建执法集中到推送与关单)。范围匹配逻辑单源 =
|
|
13
|
+
// commit-check.cjs。experiment/* 目标降级 advisory。手动运行(无 stdin 推送行)无范围事实,构建门跳过。
|
|
14
|
+
//
|
|
15
|
+
// experiment/* 泳道:**目标分支**为 experiment/* 时,闭环扫描与构建门降级 advisory(报告不阻断)——
|
|
16
|
+
// 快速 PoC 推送不因闭环断档/构建失败被拦;转正(目标为 main)由加固门兜底。
|
|
17
|
+
// 加固门(2026-10-01 v09-review-defects):推送**目标**为 refs/heads/main 时以 --hardening 运行
|
|
18
|
+
// (无论来源分支——experiment/* 直推 main 同样覆盖),exploring 任务须先收口(intent approved/done +
|
|
19
|
+
// 同名 plan 在场 + L2/L3 另须同名 spec;判据见 check-loop.mjs 加固门段),hard 失败即阻断 push。
|
|
20
|
+
//
|
|
21
|
+
// 回路日检(2026-10-16 自源仓 13f67ef7 回灌):退役催办与 papercut 报债此前无调度,机制空转。每个
|
|
22
|
+
// 本地日在推送成功路径跑一次,advisory,失败不改退出码。摘要落 .agents/cache/loop-watch.jsonl(gitignore)。
|
|
23
|
+
//
|
|
24
|
+
// 机器门:本地钩子跑 check-loop.mjs。装户仓的远端门是随包下发的 .github/workflows/kit-ci.yml(verify.mjs)。
|
|
25
|
+
// kit 源仓:.github/workflows/ci.yml 于 push main / 全部 PR 跑 npm test(lint + 全量套件)。
|
|
26
|
+
// 不要用 `git push --no-verify` 绕开——发现 hard-block 应先修 workflow 文档再推。
|
|
27
|
+
const { spawnSync } = require('node:child_process');
|
|
28
|
+
const fs = require('node:fs');
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });
|
|
31
|
+
if (top.status !== 0 || !top.stdout.trim()) {
|
|
32
|
+
console.error('pre-push: 无法定位仓库根——推送已阻止');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
process.chdir(top.stdout.trim());
|
|
36
|
+
|
|
37
|
+
const LOOP = '.agents/scripts/check-loop.mjs';
|
|
38
|
+
if (!fs.existsSync(LOOP)) {
|
|
39
|
+
console.error(`pre-push: 未找到 ${LOOP}——闭环扫描无法执行,推送已阻止(脚本随包下发;flow-kit sync 恢复后重试)`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// 构建门脚本(2026-10-16 自 pre-commit 后移):缺失 fail-closed
|
|
43
|
+
const BUILD_CHECK = '.agents/hooks/commit-check.cjs';
|
|
44
|
+
if (!fs.existsSync(BUILD_CHECK)) {
|
|
45
|
+
console.error(`pre-push: 未找到 ${BUILD_CHECK}——构建门无法执行,推送已阻止(脚本随包下发;git checkout -- ${BUILD_CHECK} 恢复后重试)`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
22
48
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
exit 0
|
|
27
|
-
fi
|
|
49
|
+
const runLoop = (args = []) => spawnSync(process.execPath, [LOOP, ...args], { stdio: 'inherit' });
|
|
50
|
+
// 范围参数:远端全零 sha(新分支首推)→ 全量构建语义(commit-check 认全零 old)
|
|
51
|
+
const rangeArgsOf = (remoteSha, localSha) => ['--range', remoteSha && !/^0+$/.test(remoteSha) ? remoteSha : '0'.repeat(40), localSha];
|
|
28
52
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
let stdinText = '';
|
|
54
|
+
try { stdinText = fs.readFileSync(0, 'utf8'); } catch { stdinText = ''; }
|
|
55
|
+
|
|
56
|
+
let saw = 0;
|
|
57
|
+
let rc = 0;
|
|
58
|
+
for (const line of stdinText.split(/\r?\n/)) {
|
|
59
|
+
if (!line.trim()) continue;
|
|
60
|
+
const parts = line.trim().split(/\s+/);
|
|
61
|
+
if (parts.length < 4) continue;
|
|
62
|
+
const [localRef, localSha, remoteRef, remoteSha] = parts;
|
|
63
|
+
saw = 1;
|
|
64
|
+
if (/^0+$/.test(localSha)) continue; // 删除推送不扫
|
|
65
|
+
// 按目标分支分派(判定键 = remote_ref):
|
|
66
|
+
// main → 加固门(必带 --hardening);experiment/* → advisory(报告不阻断);其他 → 全量扫描(硬门)
|
|
67
|
+
if (/^refs\/heads\/main$|^main$/.test(remoteRef)) {
|
|
68
|
+
const r = runLoop(['--rev', localSha, '--hardening']);
|
|
69
|
+
if (r.status !== 0 && rc === 0) rc = r.status === null ? 1 : r.status;
|
|
70
|
+
const rb = spawnSync(process.execPath, [BUILD_CHECK, ...rangeArgsOf(remoteSha, localSha)], { stdio: 'inherit' });
|
|
71
|
+
if (rb.status !== 0 && rc === 0) rc = rb.status === null ? 1 : rb.status;
|
|
72
|
+
} else if (/^refs\/heads\/experiment\/|^experiment\//.test(remoteRef)) {
|
|
73
|
+
console.error(`pre-push: experiment/* 目标分支(${remoteRef})——闭环扫描与构建门降级 advisory,不阻断`);
|
|
74
|
+
const adv = spawnSync(process.execPath, [LOOP, '--rev', localSha], { stdio: 'inherit' });
|
|
75
|
+
if (adv.status !== 0) console.error('pre-push: (advisory)闭环断档已在上方列出——experiment 泳道可推送,转正(推 main)须过加固门');
|
|
76
|
+
const advB = spawnSync(process.execPath, [BUILD_CHECK, ...rangeArgsOf(remoteSha, localSha)], { stdio: 'inherit' });
|
|
77
|
+
if (advB.status !== 0) console.error('pre-push: (advisory)构建门未过已在上方列出——experiment 泳道不阻断推送');
|
|
78
|
+
} else {
|
|
79
|
+
const r = runLoop(['--rev', localSha]);
|
|
80
|
+
if (r.status !== 0 && rc === 0) rc = r.status === null ? 1 : r.status;
|
|
81
|
+
const rb = spawnSync(process.execPath, [BUILD_CHECK, ...rangeArgsOf(remoteSha, localSha)], { stdio: 'inherit' });
|
|
82
|
+
if (rb.status !== 0 && rc === 0) rc = rb.status === null ? 1 : rb.status;
|
|
83
|
+
}
|
|
35
84
|
}
|
|
36
85
|
|
|
37
|
-
saw
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*[!0]*) ;;
|
|
43
|
-
*) continue ;;
|
|
44
|
-
esac
|
|
45
|
-
# 按目标分支三态分派(2026-10-01 v09-review-defects:判定键从 local_ref 改为 remote_ref——
|
|
46
|
-
# 修复 experiment/*→main 直推被 advisory continue 吞掉加固门的漏口):
|
|
47
|
-
# main → 加固门(必带 --hardening)
|
|
48
|
-
# experiment/* → advisory(报告不阻断)
|
|
49
|
-
# 其他 → 全量扫描
|
|
50
|
-
case "$remote_ref" in
|
|
51
|
-
refs/heads/main|main)
|
|
52
|
-
run_loop --rev "$local_sha" --hardening
|
|
53
|
-
;;
|
|
54
|
-
refs/heads/experiment/*|experiment/*)
|
|
55
|
-
echo "pre-push: experiment/* 目标分支($remote_ref)——闭环扫描降级 advisory,不阻断" >&2
|
|
56
|
-
if command -v sh >/dev/null 2>&1; then
|
|
57
|
-
sh "$SCRIPT" --rev "$local_sha" >&2 || echo "pre-push: (advisory)闭环断档已在上方列出——experiment 泳道可推送,转正(推 main)须过加固门" >&2
|
|
58
|
-
else
|
|
59
|
-
bash "$SCRIPT" --rev "$local_sha" >&2 || echo "pre-push: (advisory)闭环断档已在上方列出——experiment 泳道可推送,转正(推 main)须过加固门" >&2
|
|
60
|
-
fi
|
|
61
|
-
;;
|
|
62
|
-
*)
|
|
63
|
-
run_loop --rev "$local_sha"
|
|
64
|
-
;;
|
|
65
|
-
esac
|
|
66
|
-
one=$?
|
|
67
|
-
if [ "$one" -ne 0 ] && [ "$rc" -eq 0 ]; then
|
|
68
|
-
rc=$one
|
|
69
|
-
fi
|
|
70
|
-
done
|
|
86
|
+
if (saw === 0) {
|
|
87
|
+
const r = runLoop();
|
|
88
|
+
if (r.status !== 0) rc = r.status === null ? 1 : r.status;
|
|
89
|
+
console.error('pre-push: 手动运行无推送范围——构建门跳过(真实推送时按 old..new 范围执法)');
|
|
90
|
+
}
|
|
71
91
|
|
|
72
|
-
if
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
92
|
+
if (rc !== 0) {
|
|
93
|
+
console.error('pre-push: 闭环骨架断档 / 构建未过(hard-block)未通过,push 已阻断。修 workflow 文档 / 构建错误后重推——不要用 --no-verify 绕过(本钩子是唯一机器门,无服务端兜底,见头部说明)');
|
|
94
|
+
console.error('pre-push: 若为「加固未过」= experiment 泳道件转正前须收口:intent approved/done + 同名 plan(L2/L3 另须 spec)且状态非 draft');
|
|
95
|
+
process.exit(rc);
|
|
96
|
+
}
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
// 回路日检(2026-10-16 自源仓 13f67ef7 回灌):每个本地日在推送成功路径跑一次,advisory,失败不改退出码。
|
|
99
|
+
try {
|
|
100
|
+
const cacheDir = '.agents/cache';
|
|
101
|
+
const stampPath = `${cacheDir}/loop-watch-stamp`;
|
|
102
|
+
const ledgerPath = `${cacheDir}/loop-watch.jsonl`;
|
|
103
|
+
const now = new Date();
|
|
104
|
+
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
105
|
+
let prev = '';
|
|
106
|
+
try { prev = fs.readFileSync(stampPath, 'utf8').trim(); } catch { /* 首跑 */ }
|
|
107
|
+
if (prev !== today) {
|
|
108
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
109
|
+
// 退役催办:mechanism-retire-watch(--json 产 actions);papercut 报债:kit 复用 sensor --json 取 debt
|
|
110
|
+
const jobs = [
|
|
111
|
+
['retire-watch', '.agents/scripts/mechanism-retire-watch.mjs'],
|
|
112
|
+
['papercut-report', '.agents/scripts/papercut-debt-sensor.mjs'],
|
|
113
|
+
];
|
|
114
|
+
const runs = [];
|
|
115
|
+
for (const [id, script] of jobs) {
|
|
116
|
+
if (!fs.existsSync(script)) { runs.push({ id, missing: true }); continue; }
|
|
117
|
+
const r = spawnSync(process.execPath, [script, '--json'], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
118
|
+
let summary = {};
|
|
119
|
+
try { summary = JSON.parse(r.stdout || '{}'); } catch { summary = { parseError: true }; }
|
|
120
|
+
runs.push({
|
|
121
|
+
id,
|
|
122
|
+
exit: r.status,
|
|
123
|
+
actions: Array.isArray(summary.actions) ? summary.actions.length : null,
|
|
124
|
+
debt: typeof summary.debt === 'number' ? summary.debt : null,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
fs.appendFileSync(ledgerPath, `${JSON.stringify({ ts: now.toISOString(), day: today, runs })}\n`);
|
|
128
|
+
fs.writeFileSync(stampPath, `${today}\n`);
|
|
129
|
+
const retire = runs.find((x) => x.id === 'retire-watch');
|
|
130
|
+
const paper = runs.find((x) => x.id === 'papercut-report');
|
|
131
|
+
console.error(`pre-push: (advisory)回路日检已记 ${ledgerPath}(退役动作 ${retire && retire.actions != null ? retire.actions : '?'},papercut debt ${paper && paper.debt != null ? paper.debt : '?'})`);
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.error(`pre-push: (advisory)回路日检跳过——${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
process.exit(0);
|