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,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// rewrite-file.test.mjs — 按行字面替换 + 响亮失败 fixture 套件(2026-10-12 silent-noop-loud-failure)
|
|
3
|
+
// 覆盖:CRLF 保全(papercut 2026-10-06 的静默无操作场景)/ 0 命中不写盘 / 命中数不符期望 /
|
|
4
|
+
// --allow-multi / --dry-run / 缺文件 fail-loud / find===replace 自伤防护 / 跨行锚串必失败 /
|
|
5
|
+
// 无尾换行保全 / 写后回读校验成功形态
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const SCRIPT = path.join(SCRIPT_DIR, 'rewrite-file.mjs');
|
|
14
|
+
|
|
15
|
+
let pass = 0;
|
|
16
|
+
let fail = 0;
|
|
17
|
+
const check = (desc, cond, detail = '') => {
|
|
18
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
19
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const run = (file, args) => {
|
|
23
|
+
const r = spawnSync(process.execPath, [SCRIPT, file, ...args], { encoding: 'utf8' });
|
|
24
|
+
return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
|
|
25
|
+
};
|
|
26
|
+
const mk = (name, content) => {
|
|
27
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'rewrite-file-'));
|
|
28
|
+
const p = path.join(d, name);
|
|
29
|
+
fs.writeFileSync(p, content, 'utf8');
|
|
30
|
+
return p;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// T1:基本 LF 替换 → exit 0 + 内容正确 + 成功语含回读校验
|
|
34
|
+
{
|
|
35
|
+
const p = mk('lf.md', 'a\nANCHOR\nc\n');
|
|
36
|
+
const r = run(p, ['--find', 'ANCHOR', '--replace', 'NEW']);
|
|
37
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
38
|
+
check('T1 基本 LF 替换 → exit 0 且内容正确', r.code === 0 && after === 'a\nNEW\nc\n', r.out);
|
|
39
|
+
check('T1b 成功语含「回读校验通过」', /回读校验通过/.test(r.out), r.out);
|
|
40
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
// T2:CRLF 保全——papercut 2026-10-06 的静默无操作场景,本工具必须真改且不坏行尾
|
|
43
|
+
{
|
|
44
|
+
const p = mk('crlf.md', 'line1\r\nANCHOR here\r\nline3\r\n');
|
|
45
|
+
const r = run(p, ['--find', 'ANCHOR', '--replace', 'NEW']);
|
|
46
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
47
|
+
check('T2 CRLF 文件替换成功且行尾保全', r.code === 0 && after === 'line1\r\nNEW here\r\nline3\r\n', r.out);
|
|
48
|
+
check('T2b 未混入裸 LF(无 CRLF→LF 漂移)', !/[^\r]\n/.test(after), JSON.stringify(after));
|
|
49
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
// T3:0 命中 → exit 1 且文件逐字节不变(核心:绝不存在「以为改了」)
|
|
52
|
+
{
|
|
53
|
+
const before = 'x\r\nZZZ not here\r\n';
|
|
54
|
+
const p = mk('nohit.md', before);
|
|
55
|
+
const r = run(p, ['--find', 'QQQ', '--replace', 'Q']);
|
|
56
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
57
|
+
check('T3 0 命中 → exit 1 且文件逐字节不变', r.code === 1 && after === before, r.out);
|
|
58
|
+
check('T3b 报错点名行尾与常见成因(CRLF 检出态)', /0 命中/.test(r.out) && /CRLF/.test(r.out) && /跨行锚/.test(r.out), r.out);
|
|
59
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
// T4:命中数与期望不符 → exit 1 且不写盘(锚串非唯一时必须响亮,不可顺手全改)
|
|
62
|
+
{
|
|
63
|
+
const before = 'k\nk\n';
|
|
64
|
+
const p = mk('multi.md', before);
|
|
65
|
+
const r = run(p, ['--find', 'k', '--replace', 'K']);
|
|
66
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
67
|
+
check('T4 命中 2 ≠ 期望 1 → exit 1 且未写盘', r.code === 1 && after === before && /命中数 2 ≠ 期望 1/.test(r.out), r.out);
|
|
68
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
// T5:显式 --expect 2 → 过(同一行两处命中也按处计)
|
|
71
|
+
{
|
|
72
|
+
const p = mk('expect2.md', 'k k\n');
|
|
73
|
+
const r = run(p, ['--find', 'k', '--replace', 'K', '--expect', '2']);
|
|
74
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
75
|
+
check('T5 --expect 2 且命中 2 → exit 0 且两处都改', r.code === 0 && after === 'K K\n', r.out);
|
|
76
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
// T6:--allow-multi 多命中 → 过(宽松形态,明示批量)
|
|
79
|
+
{
|
|
80
|
+
const p = mk('allowmulti.md', 'k\nk\nk\n');
|
|
81
|
+
const r = run(p, ['--find', 'k', '--replace', 'K', '--allow-multi']);
|
|
82
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
83
|
+
check('T6 --allow-multi 三命中 → exit 0 且全改', r.code === 0 && after === 'K\nK\nK\n', r.out);
|
|
84
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
// T7:--allow-multi 与 --expect 互斥 → exit 1(一宽一严无法裁决)
|
|
87
|
+
{
|
|
88
|
+
const p = mk('conflict.md', 'k\n');
|
|
89
|
+
const r = run(p, ['--find', 'k', '--replace', 'K', '--allow-multi', '--expect', '1']);
|
|
90
|
+
check('T7 --allow-multi 与 --expect 互斥 → exit 1', r.code === 1 && /互斥/.test(r.out), r.out);
|
|
91
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
// T8:--dry-run → 不写盘
|
|
94
|
+
{
|
|
95
|
+
const p = mk('dry.md', 'a\nANCHOR\n');
|
|
96
|
+
const r = run(p, ['--find', 'ANCHOR', '--replace', 'NEW', '--dry-run']);
|
|
97
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
98
|
+
check('T8 --dry-run → exit 0 且未写盘', r.code === 0 && after === 'a\nANCHOR\n' && /dry-run/.test(r.out), r.out);
|
|
99
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
// T9:目标文件不存在 → exit 1 fail-loud(不静默创建新文件)
|
|
102
|
+
{
|
|
103
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'rewrite-file-'));
|
|
104
|
+
const r = run(path.join(d, 'nope.md'), ['--find', 'a', '--replace', 'b']);
|
|
105
|
+
check('T9 文件不存在 → exit 1 且不创建文件', r.code === 1 && /不存在/.test(r.out) && !fs.existsSync(path.join(d, 'nope.md')), r.out);
|
|
106
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
// T10:find === replace → exit 1(无操作却报成功,正是要堵的形态)
|
|
109
|
+
{
|
|
110
|
+
const p = mk('same.md', 'a\nSAME\n');
|
|
111
|
+
const r = run(p, ['--find', 'SAME', '--replace', 'SAME']);
|
|
112
|
+
check('T10 find === replace → exit 1', r.code === 1 && /相同/.test(r.out), r.out);
|
|
113
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
// T11:跨行锚串(含 \n)→ 0 命中 exit 1——把「\n 锚串匹配 CRLF 文件静默无操作」变成响亮失败
|
|
116
|
+
{
|
|
117
|
+
const p = mk('crossline.md', 'aaa\r\nbbb\r\n');
|
|
118
|
+
const r = run(p, ['--find', 'aaa\nbbb', '--replace', 'xxx']);
|
|
119
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
120
|
+
check('T11 跨行锚串 → exit 1 且文件不变(提示拆单行锚)', r.code === 1 && after === 'aaa\r\nbbb\r\n' && /跨行锚/.test(r.out), r.out);
|
|
121
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
// T12:无尾换行保全
|
|
124
|
+
{
|
|
125
|
+
const p = mk('notrailing.md', 'a\nANCHOR');
|
|
126
|
+
const r = run(p, ['--find', 'ANCHOR', '--replace', 'NEW']);
|
|
127
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
128
|
+
check('T12 原文件无尾换行 → 替换后仍无尾换行', r.code === 0 && after === 'a\nNEW', JSON.stringify(after));
|
|
129
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
// T13:空 --find → exit 1(空串会命中每一行,属误用)
|
|
132
|
+
{
|
|
133
|
+
const p = mk('emptyfind.md', 'a\n');
|
|
134
|
+
const r = run(p, ['--find', '', '--replace', 'b']);
|
|
135
|
+
check('T13 空 --find → exit 1', r.code === 1 && /不能为空串/.test(r.out), r.out);
|
|
136
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
// T14:同行的两处命中按处计数,且 --expect 3 精确匹配(处计数语义显式化)
|
|
139
|
+
{
|
|
140
|
+
const p = mk('twiceinline.md', 'k-k-k\n');
|
|
141
|
+
const r = run(p, ['--find', 'k', '--replace', 'K', '--expect', '3']);
|
|
142
|
+
const after = fs.readFileSync(p, 'utf8');
|
|
143
|
+
check('T14 同行多处按处计数(3 处)→ exit 0', r.code === 0 && after === 'K-K-K\n', r.out);
|
|
144
|
+
fs.rmSync(path.dirname(p), { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
148
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 常驻面体积预算检查(单源:本脚本 + .agents/rule-budgets.txt;触发点:check-hygiene 检查 13(--all,WARN)、
|
|
3
|
+
// .githooks/pre-commit(--staged,硬拦))
|
|
4
|
+
// 自 sh 版移植 node(2026-10-08 ci 去 sh 化;2026-10-13 自 Shipyard.Material 参考实现回灌)——CI agent(Windows)
|
|
5
|
+
// 免 git-bash 依赖,check-loop 检查 13 从「无 sh 静默跳过」变恒可跑。语义逐条对齐 sh 版:
|
|
6
|
+
// --all 查「提交后树」全量;--staged 仅查本次暂存触及的条目 + 触及目录的合计
|
|
7
|
+
// 退出码 0 通过 / 1 超限(违例打到 stdout,调用方决定警告还是阻断);缺预算表 exit 0(fixture 沙箱不拦——既有语义)
|
|
8
|
+
// 「提交后树」= 索引(index)内容:staged 版优先于 HEAD;无 git(fixture)回退工作区文件。
|
|
9
|
+
// 字节口径 = 索引 / HEAD 内容(git cat-file,LF 归一)。禁用工作区 wc -c 口径(CRLF 膨胀系统性偏大,
|
|
10
|
+
// 2026-09-21 实测差 ~500B 一度误判超限)——本脚本所有字节数都走 cat-file,仅无 git 时才落工作区。
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const mode = process.argv[2] === '--staged' ? '--staged' : '--all';
|
|
16
|
+
|
|
17
|
+
const git = (args) => {
|
|
18
|
+
const r = spawnSync('git', args, { encoding: 'utf8' });
|
|
19
|
+
return { ok: !r.error && r.status === 0, out: (r.stdout || '').trim() };
|
|
20
|
+
};
|
|
21
|
+
const root = git(['rev-parse', '--show-toplevel']);
|
|
22
|
+
if (root.ok) process.chdir(root.out); // 非 git 环境(fixture 沙箱)留在原地
|
|
23
|
+
|
|
24
|
+
const BUDGETS = '.agents/rule-budgets.txt';
|
|
25
|
+
if (!fs.existsSync(BUDGETS)) process.exit(0);
|
|
26
|
+
|
|
27
|
+
// 「提交后」字节数:索引 → HEAD → 工作区
|
|
28
|
+
const fsize = (f) => {
|
|
29
|
+
if (git(['cat-file', '-e', `:${f}`]).ok) return Number(git(['cat-file', '-s', `:${f}`]).out);
|
|
30
|
+
if (git(['cat-file', '-e', `HEAD:${f}`]).ok) return Number(git(['cat-file', '-s', `HEAD:${f}`]).out);
|
|
31
|
+
try { return fs.readFileSync(f).length; } catch { return 0; }
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const staged = mode === '--staged'
|
|
35
|
+
? git(['-c', 'core.quotepath=off', 'diff', '--cached', '--name-only']).out.split('\n').filter(Boolean)
|
|
36
|
+
: [];
|
|
37
|
+
|
|
38
|
+
// intentional-simple: glob 仅支持预算表在用的两形态——「固定目录 + * 后缀」(.agents/commands/*.md)
|
|
39
|
+
// 与目录本身(/ 结尾,展开一层直接子文件);表出现更复杂 glob 时再升级
|
|
40
|
+
const expand = (pat) => {
|
|
41
|
+
if (pat.endsWith('/')) {
|
|
42
|
+
const dir = pat.replace(/\/$/, '');
|
|
43
|
+
if (!fs.existsSync(dir)) return [];
|
|
44
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile())
|
|
45
|
+
.map((e) => path.join(dir, e.name));
|
|
46
|
+
}
|
|
47
|
+
if (pat.includes('*')) {
|
|
48
|
+
const i = pat.lastIndexOf('/');
|
|
49
|
+
const dir = i < 0 ? '.' : pat.slice(0, i);
|
|
50
|
+
const name = pat.slice(i + 1);
|
|
51
|
+
const star = name.indexOf('*');
|
|
52
|
+
const pre = name.slice(0, star), suf = name.slice(star + 1);
|
|
53
|
+
if (!fs.existsSync(dir)) return [];
|
|
54
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile()
|
|
55
|
+
&& e.name.startsWith(pre) && e.name.endsWith(suf))
|
|
56
|
+
.map((e) => path.join(dir, e.name));
|
|
57
|
+
}
|
|
58
|
+
return fs.existsSync(pat) ? [pat] : [];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let viol = 0;
|
|
62
|
+
const over = (what, bytes, lim) => { console.log(`常驻面超限:${what} ${bytes}B > 上限 ${lim}B`); viol = 1; };
|
|
63
|
+
|
|
64
|
+
for (const line of fs.readFileSync(BUDGETS, 'utf8').split(/\r?\n/)) {
|
|
65
|
+
const sp = line.lastIndexOf(' ');
|
|
66
|
+
if (sp < 0) continue;
|
|
67
|
+
const pat = line.slice(0, sp), lim = line.slice(sp + 1);
|
|
68
|
+
if (pat === '' || pat.startsWith('#')) continue;
|
|
69
|
+
if (!/^\d+$/.test(lim)) continue;
|
|
70
|
+
if (pat.endsWith('/')) {
|
|
71
|
+
// 目录合计:--staged 下仅当暂存触及该目录内文件才查(字面前缀匹配,同 sh 版 2026-09-24 口径)
|
|
72
|
+
if (mode === '--staged' && !staged.some((sf) => sf.startsWith(pat))) continue;
|
|
73
|
+
const total = expand(pat).reduce((s, f) => s + fsize(f), 0);
|
|
74
|
+
if (total > Number(lim)) over(`${pat} 合计`, total, lim);
|
|
75
|
+
} else {
|
|
76
|
+
for (const f of expand(pat)) {
|
|
77
|
+
if (mode === '--staged' && !staged.includes(f.replace(/\\/g, '/'))) continue;
|
|
78
|
+
const b = fsize(f);
|
|
79
|
+
if (b > Number(lim)) over(f, b, lim);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
process.exit(viol ? 1 : 0);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// run-tests — 引擎脚本测试统一入口(2026-10-08 agents-hygiene 批2;2026-10-09 并行化)
|
|
3
|
+
// 用途:一条命令跑 .agents/scripts/ 下全部 *.test.mjs 套件——逐个 node 直跑(与 test.md 逐条口径一致,
|
|
4
|
+
// 不引 node --test:既有套件是自研 check/「合计: PASS n / FAIL n」计数风格,非 node:test 风格)。
|
|
5
|
+
// 语义:套件并发执行(默认 6,--concurrency <N> 可调,1 = 等价旧串行;非正整数 fail-loud)——每套件
|
|
6
|
+
// stdout/stderr 缓冲、完成即回放(「[run-tests] ▶ 套件名」分隔头,避免并行交错不可读);
|
|
7
|
+
// stderr 仅红套件回放尾部 5 行防刷屏;聚合各套件「合计: PASS n / FAIL n」末次计数(无该行按 0 计);
|
|
8
|
+
// 任一套件 exit 非零 / spawn 失败 / 计数 FAIL>0 → 该套件记红;有红套件即 exit 1。
|
|
9
|
+
// 套件发现 = 目录内 *.test.mjs 字典序(非递归);回放按完成序(并行的自然结果,不影响裁决)。
|
|
10
|
+
// 红线:只做调度层并行,不改任何套件内部;并发默认 6(2026-10-09 实测:串行 2m47s → 并发4 2m05s →
|
|
11
|
+
// 并发6 1m48s,套件各异步 tmp fixture 无共享态)——Windows spawn 密集会放大瞬时抖动(同日实证一次
|
|
12
|
+
// git EBUSY 偶发红),故不取更高默认;套件内 git 调用已有 gitRetry 防护。
|
|
13
|
+
// 2026-10-10 复测定案(perf 系列):6→12 仅 -3~5%(114.4s → 111.8/108.4s,本机 12 核,两次 c12 全绿
|
|
14
|
+
// 无偶发红;噪声带 ±3s)——膝点在 4-6 间,并发档位已非杠杆:工作池地板 = check-loop.test 独跑
|
|
15
|
+
// 73.8s(占总时长 65%,234 次套件内顺序 spawn node)。再快须并行化套件内场景(动套件内部结构,
|
|
16
|
+
// 撞本红线),收益/风险另议;默认维持 6。
|
|
17
|
+
// 用法:node .agents/scripts/run-tests.mjs [--dir <目录>] [--only <文件名子串>] [--concurrency <N>]
|
|
18
|
+
// --dir 默认本脚本所在目录(测试注入钩子);--only 过滤文件名(调试单套件用,如 --only check-loop)。
|
|
19
|
+
// 测试:node .agents/scripts/run-tests.test.mjs(临时目录夹具:并行全绿聚合 / 红套件 / 崩溃套件 / --only / 空目录 / 并发数校验 / 并发1=串行)
|
|
20
|
+
// 沿革:2026-10-08 立档(串行);2026-10-09 并行化(sess_52d1d1be 路线图「串行→并行」真缺口)。
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { spawn } from 'node:child_process';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
const val = (flag) => (args.includes(flag) ? args[args.indexOf(flag) + 1] : null);
|
|
29
|
+
const dir = val('--dir') || SCRIPT_DIR;
|
|
30
|
+
const only = val('--only');
|
|
31
|
+
const concRaw = val('--concurrency');
|
|
32
|
+
const CONC = concRaw === null ? 6 : Number(concRaw);
|
|
33
|
+
if (!Number.isInteger(CONC) || CONC < 1) {
|
|
34
|
+
console.error(`run-tests: --concurrency 须为正整数(现 ${concRaw})`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let suites;
|
|
39
|
+
try {
|
|
40
|
+
suites = fs.readdirSync(dir).filter((f) => f.endsWith('.test.mjs') && (!only || f.includes(only))).sort();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error(`run-tests: 无法读取目录 ${dir}(${e.code || e.message})`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
if (!suites.length) {
|
|
46
|
+
console.error(`run-tests: ${dir} 下无 *.test.mjs${only ? `(--only ${only})` : ''}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 工作池调度(完成一个补一个——分块会让最慢套件阻塞同块槽位,2026-10-09 实测 check-loop.test 独占 ~1/3 时长)
|
|
51
|
+
let totalPass = 0, totalFail = 0, red = 0;
|
|
52
|
+
const runSuite = (s) => new Promise((resolve) => {
|
|
53
|
+
const child = spawn(process.execPath, [path.join(dir, s)]);
|
|
54
|
+
let out = '', err = '';
|
|
55
|
+
child.stdout.on('data', (d) => { out += d; });
|
|
56
|
+
child.stderr.on('data', (d) => { err += d; });
|
|
57
|
+
child.on('close', (code) => resolve({ stdout: out, stderr: err, status: code, error: null }));
|
|
58
|
+
child.on('error', (e) => resolve({ stdout: out, stderr: err, status: null, error: e }));
|
|
59
|
+
});
|
|
60
|
+
const replay = (s, r) => {
|
|
61
|
+
const m = [...String(r.stdout || '').matchAll(/合计: PASS (\d+) \/ FAIL (\d+)/g)].pop();
|
|
62
|
+
const p = m ? Number(m[1]) : 0;
|
|
63
|
+
const f = m ? Number(m[2]) : 0;
|
|
64
|
+
totalPass += p;
|
|
65
|
+
totalFail += f;
|
|
66
|
+
const ok = r.status === 0 && f === 0;
|
|
67
|
+
process.stdout.write(`\n[run-tests] ▶ ${s}\n`);
|
|
68
|
+
if (r.stdout) process.stdout.write(r.stdout);
|
|
69
|
+
if (ok) {
|
|
70
|
+
console.log(`[run-tests] OK ${s}(PASS ${p} / FAIL ${f})`);
|
|
71
|
+
} else {
|
|
72
|
+
red++;
|
|
73
|
+
console.log(`[run-tests] RED ${s}${r.status != null ? `(exit ${r.status})` : r.error ? `(spawn ${r.error.code || r.error.message})` : ''}${m ? `(PASS ${p} / FAIL ${f})` : ''}`);
|
|
74
|
+
if (r.stderr) process.stderr.write(String(r.stderr).split('\n').slice(-5).join('\n') + '\n');
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
{
|
|
78
|
+
const queue = [...suites];
|
|
79
|
+
const inflight = new Map(); // suite -> promise(回放按提交序记录、完成即按序回放:用「下一个待回放」指针)
|
|
80
|
+
const pending = []; // {s, promise}
|
|
81
|
+
let cursor = 0;
|
|
82
|
+
const launch = () => {
|
|
83
|
+
while (inflight.size < CONC && queue.length) {
|
|
84
|
+
const s = queue.shift();
|
|
85
|
+
const pr = runSuite(s).then((r) => ({ s, r }));
|
|
86
|
+
inflight.set(s, pr);
|
|
87
|
+
pending.push(pr);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
launch();
|
|
91
|
+
while (cursor < pending.length) {
|
|
92
|
+
const { s, r } = await pending[cursor++];
|
|
93
|
+
inflight.delete(s);
|
|
94
|
+
replay(s, r);
|
|
95
|
+
launch();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
console.log(`\n[run-tests] 套件 ${suites.length}(红 ${red},并发 ${CONC})|断言合计 PASS ${totalPass} / FAIL ${totalFail}${red ? '——红套件见上,修复后原路重跑' : '——全绿'}`);
|
|
99
|
+
process.exit(red ? 1 : 0);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// run-tests.mjs 的夹具驱动测试(2026-10-08 agents-hygiene 批2)
|
|
3
|
+
// 场景:①全绿目录 → exit 0 且聚合计数正确;②含红套件(FAIL>0 + exit 1)→ 记红且 exit 1;
|
|
4
|
+
// ③崩溃套件(throw,无合计行)→ 记红且 exit 1;④--only 过滤;⑤空目录 → exit 1。
|
|
5
|
+
// 用法:node .agents/scripts/run-tests.test.mjs(在仓库任意目录执行均可)
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const MAIN = path.join(SCRIPT_DIR, 'run-tests.mjs');
|
|
14
|
+
const USAGE = '用法:node .agents/scripts/run-tests.test.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
|
+
const mkfix = (files) => {
|
|
24
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'run-tests-fx-'));
|
|
25
|
+
for (const [name, body] of Object.entries(files)) fs.writeFileSync(path.join(d, name), body);
|
|
26
|
+
return d;
|
|
27
|
+
};
|
|
28
|
+
const run = (args) => spawnSync(process.execPath, [MAIN, ...args], { encoding: 'utf8' });
|
|
29
|
+
|
|
30
|
+
// ---- 场景 1:全绿目录 → exit 0 且聚合计数(非 .test.mjs 文件不入围)----
|
|
31
|
+
{
|
|
32
|
+
const d = mkfix({ 'a.test.mjs': 'console.log("合计: PASS 3 / FAIL 0");', 'b.test.mjs': 'console.log("合计: PASS 2 / FAIL 0");', 'readme.md': 'x' });
|
|
33
|
+
const r = run(['--dir', d]);
|
|
34
|
+
check('场景 1:全绿目录 → exit 0', r.status === 0, `exit=${r.status}\n${r.stdout}${r.stderr}`);
|
|
35
|
+
check('场景 1:聚合计数 PASS 5 / FAIL 0 且红 0', /断言合计 PASS 5 \/ FAIL 0/.test(r.stdout) && /红 0/.test(r.stdout), r.stdout);
|
|
36
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---- 场景 2:红套件(计数 FAIL>0 且 exit 1)→ 记红且 exit 1 ----
|
|
40
|
+
{
|
|
41
|
+
const d = mkfix({ 'a.test.mjs': 'console.log("合计: PASS 3 / FAIL 0");', 'bad.test.mjs': 'console.log("合计: PASS 1 / FAIL 2"); process.exit(1);' });
|
|
42
|
+
const r = run(['--dir', d]);
|
|
43
|
+
check('场景 2:含红套件 → exit 1', r.status === 1, `exit=${r.status}`);
|
|
44
|
+
check('场景 2:红套件被点名', /RED bad\.test\.mjs/.test(r.stdout) && /红 1/.test(r.stdout) && /FAIL 2/.test(r.stdout), r.stdout);
|
|
45
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---- 场景 3:崩溃套件(无合计行)→ 记红且 exit 1 ----
|
|
49
|
+
{
|
|
50
|
+
const d = mkfix({ 'boom.test.mjs': 'throw new Error("boom");' });
|
|
51
|
+
const r = run(['--dir', d]);
|
|
52
|
+
check('场景 3:崩溃套件 → exit 1 且标红', r.status === 1 && /RED boom\.test\.mjs(exit 1)/.test(r.stdout), r.stdout);
|
|
53
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- 场景 4:--only 过滤 ----
|
|
57
|
+
{
|
|
58
|
+
const d = mkfix({ 'aa.test.mjs': 'console.log("合计: PASS 1 / FAIL 0");', 'bb.test.mjs': 'console.log("合计: PASS 2 / FAIL 0");' });
|
|
59
|
+
const r = run(['--dir', d, '--only', 'bb']);
|
|
60
|
+
check('场景 4:--only 只跑匹配套件', r.status === 0 && r.stdout.includes('bb.test.mjs') && !r.stdout.includes('aa.test.mjs') && /套件 1/.test(r.stdout), r.stdout);
|
|
61
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---- 场景 5:空目录 → exit 1 报无套件 ----
|
|
65
|
+
{
|
|
66
|
+
const d = mkfix({ 'readme.md': 'x' });
|
|
67
|
+
const r = run(['--dir', d]);
|
|
68
|
+
check('场景 5:空目录 → exit 1 报无套件', r.status === 1 && /无 \*\.test\.mjs/.test(r.stderr || ''), `${r.status}\n${r.stderr}`);
|
|
69
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- 场景 6:--concurrency 校验——非正整数 fail-loud ----
|
|
73
|
+
{
|
|
74
|
+
const d = mkfix({ 'a.test.mjs': 'console.log("合计: PASS 1 / FAIL 0");' });
|
|
75
|
+
for (const bad of ['0', '-2', 'abc', '2.5']) {
|
|
76
|
+
const r = run(['--dir', d, '--concurrency', bad]);
|
|
77
|
+
check(`场景 6:--concurrency ${bad} → exit 1 fail-loud`, r.status === 1 && /正整数/.test(r.stderr), `exit=${r.status}\n${r.stderr}`);
|
|
78
|
+
}
|
|
79
|
+
const ok = run(['--dir', d, '--concurrency', '1']);
|
|
80
|
+
check('场景 6:--concurrency 1(串行兼容)→ exit 0 聚合正确', ok.status === 0 && /断言合计 PASS 1 \/ FAIL 0/.test(ok.stdout), `exit=${ok.status}\n${ok.stdout}`);
|
|
81
|
+
const okN = run(['--dir', d, '--concurrency', '8']);
|
|
82
|
+
check('场景 6:--concurrency 8(超套件数)→ exit 0', okN.status === 0 && /断言合计 PASS 1 \/ FAIL 0/.test(okN.stdout), `exit=${okN.status}\n${okN.stdout}`);
|
|
83
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- 场景 7:并行回放完整(每套件有 ▶ 分隔头且计数聚合不分串)----
|
|
87
|
+
{
|
|
88
|
+
const d = mkfix({
|
|
89
|
+
'a.test.mjs': 'console.log("a-out 合计: PASS 1 / FAIL 0");',
|
|
90
|
+
'b.test.mjs': 'console.log("b-out 合计: PASS 2 / FAIL 0");',
|
|
91
|
+
'c.test.mjs': 'console.log("c-out 合计: PASS 3 / FAIL 0");',
|
|
92
|
+
});
|
|
93
|
+
const r = run(['--dir', d]);
|
|
94
|
+
check('场景 7:并行全绿 → 聚合 PASS 6 且三套件回放齐全',
|
|
95
|
+
r.status === 0 && /断言合计 PASS 6 \/ FAIL 0/.test(r.stdout) && /▶ a\.test\.mjs/.test(r.stdout) && /▶ b\.test\.mjs/.test(r.stdout) && /▶ c\.test\.mjs/.test(r.stdout), `exit=${r.status}\n${r.stdout}`);
|
|
96
|
+
check('场景 7:并行下套件输出不串(a-out 紧邻其回放块)', /▶ a\.test\.mjs\na-out/.test(r.stdout), r.stdout);
|
|
97
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
101
|
+
if (fail) {
|
|
102
|
+
console.log(`\n${USAGE}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
process.exit(0);
|
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
// incident 入口 = 文件存在 + 「时间线」小节内、行首列表条目含「用户确认」(原 maintain.md §3 约定;
|
|
7
7
|
// 2026-09-30 复核 P2-1 收窄为小节内行首条目;第三轮复核 P1-1 起接受日期前缀形态,与模板样例一致);
|
|
8
8
|
// 状态 ∈ {open, fixed, closed}(spec/plan 须先于 fixed 起草,故 open 即为有效态)。
|
|
9
|
-
// 入口「级别」须 ∈
|
|
9
|
+
// 入口「级别」须 ∈ L1-L3(2026-10-13 L0 退役,自源仓收敛系列 1 回灌;L0 → suspect 走 fail-closed);
|
|
10
|
+
// 缺失即拒(fail-closed)——**但库存量形态短路**(`流程: legacy` 或 日期早于
|
|
10
11
|
// 确认门生效日):跳过级别校验与 spec 档(2026-09-30 增量复核 N1——存量最可能缺级别,硬拒会锁死
|
|
11
12
|
// spec 起草与确认)。
|
|
12
13
|
// 注意:**库存量信号本身即等于「入口已确认」**(judgeDoc:`台账行 || 存量口径`,与检查 15 / 检查 1 /
|
|
13
14
|
// 配对门的既有口径一致——早于确认门生效日的自报日期即存量豁免,不要求另有台账行);此处「短路」
|
|
14
15
|
// 只额外豁免**级别前置与 spec 档**,不是对台账要求的二次豁免(P2-1:原注释把二者写反,已订正)。
|
|
15
16
|
// plan 门追加:入口级别(取入口 frontmatter 级别,不取工具 --level)为 L2/L3 时,同名 spec 须已确认(同口径)。
|
|
17
|
+
// L1 即入口例外(2026-10-13 自 Shipyard 回灌):同名 intent/incident 均无时,级别 L1 的 plan 自身即入口,
|
|
18
|
+
// 起草/确认门放行(文件已在则级别以文件 frontmatter 为准,禁用 --level L1 放行存量 L2 plan)。
|
|
16
19
|
// 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):同名 intent 仍为 draft 且风险泳道为
|
|
17
|
-
//
|
|
20
|
+
// L1(级别优先;级别缺失或非法时回落 risk_level——fill-intent 双写同值)→「先起草后确认」是协作道
|
|
18
21
|
// 语义(check-loop 检查 19 同款豁免口径),放行 spec/plan 起草与确认;L2/L3 防御道维持入口确认前置。
|
|
19
22
|
// incident 入口不豁免(过目留痕本就轻量)。收口由 check-loop --hardening 加固门兜底(入 main 前须
|
|
20
23
|
// intent approved/done + spec/plan 在场)。
|
|
@@ -146,32 +149,51 @@ export function specConfirmed(root, base) {
|
|
|
146
149
|
// suspect 消费规则(双严):红线类检查按 low 执行(拦红线);豁免类门(起草豁免/--batch/--auto/加固门
|
|
147
150
|
// lane/泳道豁免/逐阶段深度)按 high 执行(不豁免)。取代原三处并存语义(任一低即低 / 级别优先回落 / 仅级别)。
|
|
148
151
|
export function laneOfEntry(level, risk, riskLevelSince, date) {
|
|
149
|
-
const L = /^L[
|
|
152
|
+
const L = /^L[1-3]$/.test(level || '') ? level : '';
|
|
150
153
|
if (!L) return 'suspect';
|
|
151
|
-
const R = /^L[
|
|
154
|
+
const R = /^L[1-3]$/.test(risk || '') ? risk : '';
|
|
152
155
|
const dualProtocol = !!riskLevelSince && /^\d{4}-\d{2}-\d{2}$/.test(date || '') && date >= riskLevelSince;
|
|
153
|
-
if (dualProtocol) return (R && R === L) ? (
|
|
156
|
+
if (dualProtocol) return (R && R === L) ? (L === 'L1' ? 'low' : 'high') : 'suspect';
|
|
154
157
|
if (R && R !== L) return 'suspect';
|
|
155
|
-
return
|
|
158
|
+
return L === 'L1' ? 'low' : 'high';
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
// laneOfDoc:非 intent 件(spec/plan/incident——级别单字段协议):合法按值判,非法 → suspect。
|
|
159
162
|
export function laneOfDoc(level) {
|
|
160
|
-
if (!/^L[
|
|
161
|
-
return
|
|
163
|
+
if (!/^L[1-3]$/.test(level || '')) return 'suspect';
|
|
164
|
+
return level === 'L1' ? 'low' : 'high';
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
// draftGateFor:起草门总判(kind = 'spec' | 'plan',fill 工具与 confirm-doc 逐阶段门共用)
|
|
165
|
-
|
|
168
|
+
// opts.level(可选,2026-10-13 自 Shipyard 回灌):仅供 plan「L1 即入口」例外取级别——文件未落盘时认
|
|
169
|
+
// fill 工具传入的 --level;confirm-doc 调用(confirmGateFor)不传,落盘后级别一律以文件 frontmatter 为准。
|
|
170
|
+
export function draftGateFor(kind, root, base, opts = {}) {
|
|
166
171
|
const entry = entryConfirmed(root, base);
|
|
167
172
|
if (!entry.ok) {
|
|
168
|
-
// 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):
|
|
173
|
+
// 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):L1 draft intent 放行起草
|
|
169
174
|
// ——「先动手后确认」;泳道取法自 2026-10-02 caliber-convergence 起单源 laneOfEntry(方案 C 分歧
|
|
170
175
|
// 双严:suspect 不豁免——级别/risk_level 缺失、非法或分歧均按防御道走入口确认)。
|
|
171
176
|
const lane = laneOfEntry(entry.level, entry.risk, loadKitPolicy(root).riskLevelSince, entry.date);
|
|
172
177
|
if (entry.kind === 'intent' && entry.status === 'draft' && lane === 'low') {
|
|
173
178
|
return { ok: true, entry, laneBypass: true };
|
|
174
179
|
}
|
|
180
|
+
// L1 plan 即入口(2026-10-13 自 Shipyard 回灌):没有 intent/incident 时,这份 plan 自己就是入口。
|
|
181
|
+
// 文件已在则级别以文件为准(禁止用 --level L1 把已有 L2 plan 放行);尚未落盘才认 opts.level。
|
|
182
|
+
if (kind === 'plan' && entry.kind === null) {
|
|
183
|
+
const planAbs = path.join(root, 'workflow', 'plans', `${base}.md`);
|
|
184
|
+
const declared = fs.existsSync(planAbs) ? readFm(planAbs, '级别') : (opts.level || '');
|
|
185
|
+
if (declared === 'L1') {
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
entry: {
|
|
189
|
+
ok: true, kind: 'plan', rel: `workflow/plans/${base}.md`, level: 'L1',
|
|
190
|
+
status: fs.existsSync(planAbs) ? (readFm(planAbs, '状态') || 'draft') : 'draft',
|
|
191
|
+
selfEntry: true, legacy: false,
|
|
192
|
+
},
|
|
193
|
+
laneBypass: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
175
197
|
return {
|
|
176
198
|
ok: false,
|
|
177
199
|
reason: `同主题入口未确认——${entry.reason}`,
|
|
@@ -183,11 +205,11 @@ export function draftGateFor(kind, root, base) {
|
|
|
183
205
|
// spec 起草与确认。注意:库存量信号**本身即等于入口已确认**(judgeDoc 的 `台账行 || 存量口径`,
|
|
184
206
|
// 与检查 1 / 15 / 配对门同口径)——台账豁免发生在 judgeDoc,不在本处;本处不做二次豁免
|
|
185
207
|
// (第三轮复核 P2-1:原句「台账要求不豁免」把两件事写反,已订正)。
|
|
186
|
-
if (!entry.storageLegacy && !/^L[
|
|
208
|
+
if (!entry.storageLegacy && !/^L[1-3]$/.test(entry.level)) {
|
|
187
209
|
return {
|
|
188
210
|
ok: false,
|
|
189
211
|
reason: `入口缺合法「级别」字段(现「${entry.level || '缺失'}」,无法判定前置深度)`,
|
|
190
|
-
hint: '先给入口补「级别:
|
|
212
|
+
hint: '先给入口补「级别: L1-L3」字段,再继续',
|
|
191
213
|
};
|
|
192
214
|
}
|
|
193
215
|
if (kind === 'plan' && !entry.storageLegacy && (entry.level === 'L2' || entry.level === 'L3')) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// trust-mode: AI 自治信任等级管理(2026-09-30 ai-autonomy-trust;同日 hybrid-governance-risk-lanes 加名称双轨)
|
|
3
|
-
// 管理 .agents/trust-mode.json,授权 AI 对
|
|
3
|
+
// 管理 .agents/trust-mode.json,授权 AI 对 L1 文档进行自治放行(--auto)。
|
|
4
4
|
// 等级语义(名称 ↔ 数值双轨:confirm-doc.mjs 读数值 enabled/level,人机接口名称与数值皆收):
|
|
5
5
|
// Strict (level 0, enabled=false): AI 自治关闭(默认)——confirm-doc --auto 一律拒绝
|
|
6
|
-
// Standard (level 1): AI 可自批
|
|
7
|
-
// Trusted (level 2): AI 可自批
|
|
6
|
+
// Standard (level 1): AI 可自批 L1 draft -> approved
|
|
7
|
+
// Trusted (level 2): AI 可自批 L1 draft -> approved 及 approved -> done(全闭环)
|
|
8
8
|
// 安全默认值:未开启(enabled: false)时,confirm-doc.mjs --auto 一律拒绝。
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
@@ -45,7 +45,7 @@ function main() {
|
|
|
45
45
|
}
|
|
46
46
|
writeConfig({ enabled: true, level, name: NAME_BY_LEVEL[level], enabledAt: new Date().toISOString() });
|
|
47
47
|
console.log(`✅ AI 自治模式已开启 (${NAME_BY_LEVEL[level]} / Level ${level})`);
|
|
48
|
-
console.log(level === 1 ? ' 授权范围:
|
|
48
|
+
console.log(level === 1 ? ' 授权范围:L1 文档 draft -> approved' : ' 授权范围:L1 文档全闭环 (approved + done)');
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -30,7 +30,7 @@ const mkfix = () => {
|
|
|
30
30
|
fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'),
|
|
31
31
|
['doc.status.all=draft approved done superseded cancelled', 'doc.status.confirmed=approved done superseded cancelled',
|
|
32
32
|
'doc.status.active=draft approved', 'doc.status.terminal=done superseded cancelled', 'doc.status.abandoned=superseded cancelled',
|
|
33
|
-
'incident.status.all=open fixed closed', 'incident.status.active=open', 'level.all=
|
|
33
|
+
'incident.status.all=open fixed closed', 'incident.status.active=open', 'level.all=L1 L2 L3'].join('\n') + '\n');
|
|
34
34
|
fs.writeFileSync(path.join(d, '.agents', 'workflow-modules.txt'), '# modules\npipeline\nui\n');
|
|
35
35
|
return d;
|
|
36
36
|
};
|