agentic-flow-kit 1.5.0 → 1.6.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/modules/hosts/claude/commands/wf-build.md +7 -0
- package/modules/hosts/claude/commands/wf-design.md +6 -0
- package/modules/hosts/claude/commands/wf-new-task.md +2 -0
- package/modules/hosts/codex/commands/wf-build.md +7 -0
- package/modules/hosts/codex/commands/wf-design.md +6 -0
- package/modules/hosts/codex/commands/wf-new-task.md +2 -0
- package/modules/hosts/cursor/commands/wf-build.md +7 -0
- package/modules/hosts/cursor/commands/wf-design.md +6 -0
- package/modules/hosts/cursor/commands/wf-new-task.md +2 -0
- package/modules/hosts/opencode/commands/wf-build.md +7 -0
- package/modules/hosts/opencode/commands/wf-design.md +6 -0
- package/modules/hosts/opencode/commands/wf-new-task.md +2 -0
- package/modules/hosts/trae/commands/wf-build.md +7 -0
- package/modules/hosts/trae/commands/wf-design.md +6 -0
- package/modules/hosts/trae/commands/wf-new-task.md +2 -0
- package/package.json +1 -1
- package/src/sync-hosts.mjs +100 -62
- package/src/sync-hosts.test.mjs +39 -0
- package/templates/_agents/commands/build.md +7 -0
- package/templates/_agents/commands/design.md +6 -0
- package/templates/_agents/commands/new-task.md +2 -0
- package/templates/_agents/scripts/check-loop.mjs +82 -28
- package/templates/_agents/scripts/check-loop.test.mjs +66 -0
- package/templates/_agents/scripts/check-stage-index.mjs +40 -0
- package/templates/_agents/scripts/check-stage-index.test.mjs +77 -0
- package/templates/_agents/scripts/confirm-doc.mjs +17 -2
- package/templates/_agents/scripts/confirm-doc.test.mjs +17 -0
- package/templates/_agents/scripts/edit-face-check.mjs +107 -0
- package/templates/_agents/scripts/edit-face-check.test.mjs +98 -0
- package/templates/_agents/scripts/fill-intent.mjs +32 -3
- package/templates/_agents/scripts/fill-intent.test.mjs +13 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +52 -1
- package/templates/_agents/scripts/policy.mjs +13 -0
- package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +81 -0
- package/templates/_agents/scripts/wiki-search.test.mjs +58 -0
- package/templates/_agents/scripts-test-exempt.txt +2 -3
- package/templates/_github/workflows/kit-ci.yml +21 -1
|
@@ -111,7 +111,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
111
111
|
import { createHash } from 'node:crypto';
|
|
112
112
|
import { loadEnums } from './workflow-enums.mjs';
|
|
113
113
|
import { runCheck16 } from './check-metric-claims.mjs';
|
|
114
|
-
import { auditEnabled, loadKitPolicy, hasFreshVerifyLine } from './policy.mjs';
|
|
114
|
+
import { auditEnabled, loadKitPolicy, hasFreshVerifyLine, ledgerChainHash } from './policy.mjs';
|
|
115
115
|
import { laneOfEntry } from './stage-gates.mjs';
|
|
116
116
|
// MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
|
|
117
117
|
// 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
|
|
@@ -120,6 +120,8 @@ import { MARK_RE, approvedTraceHit } from './stage-gates.mjs';
|
|
|
120
120
|
// 本文件只组装 ctx 并消费返回的文案数组(先例 = check-metric-claims.mjs 承载检查 16)。
|
|
121
121
|
// 调用点位置即输出顺序位:须留在检查 10 与 12 之间以保持 warnings 行序(稳定输出契约),见该模块内注释。
|
|
122
122
|
import { runCheckHygiene } from './check-hygiene.mjs';
|
|
123
|
+
// 检查 7 拆模块(2026-10-09 engine-quality-round2 A1):阶段索引同步迁 check-stage-index.mjs
|
|
124
|
+
import { runCheckStageIndex } from './check-stage-index.mjs';
|
|
123
125
|
import { gateSeg, finishSegs, makeCollector } from './gate-seg.mjs';
|
|
124
126
|
|
|
125
127
|
// CHECK_LOOP_GIT:测试注入钩子(指向不存在/不可执行路径可稳定触发 spawn 异常);未设置时与原行为逐字节一致(2026-10-05-gitout-fail-open)
|
|
@@ -592,19 +594,10 @@ export function runCheckLoop(opts = {}) {
|
|
|
592
594
|
if (modelHits.length) warnings.push(`- [WARN 宿主耦合] 薄 Adapter 不应固定模型:\n${modelHits.join('\n')}`);
|
|
593
595
|
}
|
|
594
596
|
|
|
595
|
-
// --- 6. 阶段索引同步 [warning]
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
// existsSync 守卫(2026-10-06-check19-entry-enoent 同类):两文件可合法缺失(下方 text 判空本就
|
|
600
|
-
// 预期缺失态),fail-loud fs 读下按空跳过、不再响亮出账
|
|
601
|
-
const docPath = path.join(ROOT, doc);
|
|
602
|
-
const text = fs.existsSync(docPath) ? linesOf(docPath) : null;
|
|
603
|
-
if (text && !text.some((l) => l.includes(`.agents/commands/${cmd}.md`))) {
|
|
604
|
-
warnings.push(`- [WARN 阶段索引漂移] ${doc} 缺 ${cmd} 指令索引(两处阶段表须同步维护)`);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
597
|
+
// --- 6. 阶段索引同步 [warning](2026-10-09 engine-quality-round2 A1:实现迁 check-stage-index.mjs——
|
|
598
|
+
// 先例 check-hygiene / check-metric-claims;段 id 沿 sh 版 '6',清单编号 7 与文案逐字不动;
|
|
599
|
+
// 调用点位置即输出顺序契约)---
|
|
600
|
+
warnings.push(...runCheckStageIndex({ root: ROOT, linesOf, gateStats }));
|
|
608
601
|
|
|
609
602
|
// --- 共享:文件「首次加入 git」的日期索引(2026-09-28 check8-git-anchor)---
|
|
610
603
|
// 用途:检查 8 的生效日锚从**可手填的文件名前缀**(项目命名规范本身即 `YYYY-MM-DD-<主题>`,作者
|
|
@@ -618,18 +611,13 @@ export function runCheckLoop(opts = {}) {
|
|
|
618
611
|
// 非 git / 命令失败 → null,调用方按既有「非 git 跳过」语义退化(不 fail-loud)。
|
|
619
612
|
// 注:检查 10 需要的是「文件→加入 commit」+「commit→文件清单」两份映射(判断同提交是否触及迁移 SQL),
|
|
620
613
|
// 与本索引「文件→日期」不同构,故**不合并**——避免把两处判据耦死。
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (line.startsWith('@')) { cur = line.slice(1); continue; }
|
|
629
|
-
if (line.trim() && cur && !m.has(line.trim())) m.set(line.trim(), cur); // 新→旧序首遇 = 最早
|
|
630
|
-
}
|
|
631
|
-
return m;
|
|
632
|
-
})();
|
|
614
|
+
// addedDates(2026-10-09 engine-quality-round3 W1):全史解析走持久缓存(HEAD 键)——
|
|
615
|
+
// 同 HEAD 多入口复跑(verify/edit-face-check/pre-commit/CI)不再重复全算;git-dir 探测保留在前。
|
|
616
|
+
const addedDates = gitOut(['rev-parse', '--git-dir']) === null ? null : addedDatesWithCache({
|
|
617
|
+
head: gitOut(['rev-parse', 'HEAD']),
|
|
618
|
+
gitLog: () => gitOut(['log', '--diff-filter=A', '--format=@%aI', '--name-only']),
|
|
619
|
+
cachePath: path.join(ROOT, '.agents', 'cache', 'added-dates.json'),
|
|
620
|
+
});
|
|
633
621
|
// 某文档「首次加入日期」(YYYY-MM-DD);不可判定(非 git / 查不到)返回 ''
|
|
634
622
|
const addedDateOf = (absDoc) => {
|
|
635
623
|
if (!addedDates) return '';
|
|
@@ -983,10 +971,39 @@ export function runCheckLoop(opts = {}) {
|
|
|
983
971
|
const EFFECTIVE = kitPolicy.confirmDocsEffective;
|
|
984
972
|
const ledgerPath = path.join(ROOT, '.agents', 'confirmations.jsonl');
|
|
985
973
|
const ledger = [];
|
|
974
|
+
const ledgerBadLines = []; // 坏行(JSON.parse 失败)的物理行号(复核 P2-1:报文行号须对齐物理行)
|
|
986
975
|
if (fs.existsSync(ledgerPath)) {
|
|
987
|
-
for (const line of linesOf(ledgerPath) || []) {
|
|
976
|
+
for (const [lineNo, line] of (linesOf(ledgerPath) || []).entries()) {
|
|
988
977
|
if (!line.trim()) continue;
|
|
989
|
-
try { ledger.push(JSON.parse(line)); } catch {
|
|
978
|
+
try { ledger.push(JSON.parse(line)); } catch { ledgerBadLines.push(lineNo + 1); }
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
// 台账哈希链校验(2026-10-09 engine-quality-round2 D;写入侧=confirm-doc appendLedger,哈希单源
|
|
982
|
+
// policy.ledgerChainHash——判定零复刻)。判据:连续带 hash 行段内,① prevHash 须等于前一带 hash
|
|
983
|
+
// 行的 hash(无 hash 行后链段重启 '',与写入侧读末行口径一致);② hash 须可重算一致。
|
|
984
|
+
// **坏行口径(复核 P2-1 勘误,此前注释误称「与写入侧一致」)**:坏行被本解析整体剔除、不重置游标
|
|
985
|
+
// ——坏行后写入的链行(写入侧读末行遇坏行 prevHash='')验链必失配出账。这是**预期 fail-loud**
|
|
986
|
+
// (坏行本身即台账完整性事故,应人工查验),非口径一致场景。
|
|
987
|
+
// 断裂 = hard「台账链断裂」(只报首断**物理行号** + 影响行计数,不做逐行刷屏)——就地改/删中间行
|
|
988
|
+
// 必留痕迹;边界:整文件重写可重建链,不可机器防,兜底 = 台账自身 git 历史(check-ledger-invariant)。
|
|
989
|
+
// 历史行(v1.5.0 前,无 hash 字段)零回填零告警——向后兼容。
|
|
990
|
+
{
|
|
991
|
+
let prevHash = '';
|
|
992
|
+
let brokenAt = 0;
|
|
993
|
+
let brokenCount = 0;
|
|
994
|
+
ledger.forEach((row, idx) => {
|
|
995
|
+
if (!row || typeof row.hash !== 'string') { prevHash = ''; return; } // 存量行/无链字段 → 链段重启
|
|
996
|
+
const { hash, prevHash: p, ...rest } = row;
|
|
997
|
+
const ok = (p ?? '') === prevHash && ledgerChainHash(rest, p ?? '') === hash;
|
|
998
|
+
if (!ok) {
|
|
999
|
+
if (!brokenAt) brokenAt = idx + 1;
|
|
1000
|
+
brokenCount++;
|
|
1001
|
+
}
|
|
1002
|
+
prevHash = hash;
|
|
1003
|
+
});
|
|
1004
|
+
if (brokenAt) {
|
|
1005
|
+
const physical = brokenAt + ledgerBadLines.filter((n) => n <= brokenAt).length; // 物理行号(复核 P2-1:剔除坏行会令数组下标漂移)
|
|
1006
|
+
blockers.push(`- [台账链断裂] confirmations.jsonl 自第 ${physical} 行(物理)起哈希链校验失败(共 ${brokenCount} 行不一致——append-only 台账出现就地改/删痕迹;prevHash 接续或内容重算不符。整文件重写不可机器防,对质走台账自身 git 历史)`);
|
|
990
1007
|
}
|
|
991
1008
|
}
|
|
992
1009
|
// incidents 侧覆盖(2026-09-27 gate-coverage):fixed/closed 为已确认态(open = 起草态不加门——
|
|
@@ -1437,6 +1454,43 @@ export function fmStatus(text) {
|
|
|
1437
1454
|
// lines 已 CRLF 归一(split(/\r?\n/)),frontmatter 区内首个「状态:」行保分隔符换值为 prev(`状态: done`
|
|
1438
1455
|
// 双空格 → `状态: approved`——与前向按原行字面计算对称,非规范分隔符不误伤;行尾空白等更奇异格式仍会
|
|
1439
1456
|
// 失配,触发前提本身已违反「确认落态唯一入口」约定,接受),全文剔「确认指纹:」行后 join('\n') 再 sha256。
|
|
1457
|
+
// parseAddedDatesLog:git log(--diff-filter=A --format=@%aI --name-only)文本 → Map(rel → 加入 ISO 时间)。
|
|
1458
|
+
// 新→旧序首遇 = 最新一次加入(复核 P2-1 勘误:旧注释「最早」系误述——行为原样零复刻,检查 8 isNew 取最新加入日期更保守)。
|
|
1459
|
+
export function parseAddedDatesLog(logText) {
|
|
1460
|
+
const m = new Map();
|
|
1461
|
+
let cur = null;
|
|
1462
|
+
for (const line of logText.split('\n')) {
|
|
1463
|
+
if (line.startsWith('@')) { cur = line.slice(1); continue; }
|
|
1464
|
+
if (line.trim() && cur && !m.has(line.trim())) m.set(line.trim(), cur);
|
|
1465
|
+
}
|
|
1466
|
+
return m;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// addedDatesWithCache:addedDates 的持久缓存层(2026-10-09 engine-quality-round3 W1)。
|
|
1470
|
+
// 键 = HEAD sha(同 commit 祖先不可变 → log 幂等;rebase 必改 HEAD 必重算;复核 P2-4:shallow→unshallow 同 HEAD 可见性增长有陈旧窗口——后果退化为旧行为 addedDateOf 返回 "",HEAD 变化自愈);
|
|
1471
|
+
// 缓存位于 .agents/cache/(gitignored 运行态);读/写任一失败 fail-open 回退全算(门禁不失效);
|
|
1472
|
+
// head 缺省(detached --rev worktree 等场景)→ 不读写缓存直接全算(语义不变)。
|
|
1473
|
+
export function addedDatesWithCache({ head, gitLog, cachePath }) {
|
|
1474
|
+
if (head) {
|
|
1475
|
+
try {
|
|
1476
|
+
const c = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
1477
|
+
if (c && c.head === head && c.map && Object.keys(c.map).length) return new Map(Object.entries(c.map));
|
|
1478
|
+
} catch { /* 缓存缺失/损坏 → 全算 */ }
|
|
1479
|
+
}
|
|
1480
|
+
const log = gitLog();
|
|
1481
|
+
if (log === null) return null;
|
|
1482
|
+
const m = parseAddedDatesLog(log);
|
|
1483
|
+
if (head) {
|
|
1484
|
+
try {
|
|
1485
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
1486
|
+
const tmp = cachePath + '.tmp';
|
|
1487
|
+
fs.writeFileSync(tmp, JSON.stringify({ head, map: Object.fromEntries(m), savedAt: new Date().toISOString() }) + '\n');
|
|
1488
|
+
fs.renameSync(tmp, cachePath);
|
|
1489
|
+
} catch { /* 写失败 fail-open:本轮已用全算结果,缓存留待下次 */ }
|
|
1490
|
+
}
|
|
1491
|
+
return m;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1440
1494
|
export function bindingSha256(lines, prevStatus) {
|
|
1441
1495
|
const isDelim = (l) => /^---\s*$/.test(l);
|
|
1442
1496
|
const out = [];
|
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
13
|
import { computeFingerprint } from './confirm-doc.mjs';
|
|
14
14
|
import { POLICIES, loadKitPolicy } from './policy.mjs';
|
|
15
15
|
import { entryConfirmed, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
16
|
+
import { parseAddedDatesLog, addedDatesWithCache } from './check-loop.mjs';
|
|
16
17
|
|
|
17
18
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
@@ -2902,5 +2903,70 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2902
2903
|
rmfix(T);
|
|
2903
2904
|
}
|
|
2904
2905
|
|
|
2906
|
+
// ---- 场景 21:台账哈希链(2026-10-09 engine-quality-round2 D)——篡改必抓 / 存量零告警 ----
|
|
2907
|
+
{
|
|
2908
|
+
// 21a:连续带 hash 行中间被篡改(改 quote 不动链字段)→ hard「台账链断裂」
|
|
2909
|
+
const T = mkfix();
|
|
2910
|
+
const { ledgerChainHash } = await import('./policy.mjs');
|
|
2911
|
+
const chainRow = (prevHash, fields) => {
|
|
2912
|
+
const row = { ...fields, prevHash };
|
|
2913
|
+
return { ...row, hash: ledgerChainHash(fields, prevHash) };
|
|
2914
|
+
};
|
|
2915
|
+
const row1 = chainRow('', { ts: '2026-10-09T01:00:00Z', doc: 'workflow/intents/2026-10-09-a.md', stage: 'approved', fingerprint: 'a'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '真原话一' });
|
|
2916
|
+
const row2 = chainRow(row1.hash, { ts: '2026-10-09T02:00:00Z', doc: 'workflow/intents/2026-10-09-a.md', stage: 'done', fingerprint: 'b'.repeat(64), prev: 'approved', source: 'chat-delegated', quote: '真原话二' });
|
|
2917
|
+
// 篡改第一行 quote(链字段不动 → hash 失配)
|
|
2918
|
+
const tampered = { ...row1, quote: '篡改后的假原话' };
|
|
2919
|
+
writeLedger(T, [tampered, row2]);
|
|
2920
|
+
let r = run(T);
|
|
2921
|
+
let out = outOf(r);
|
|
2922
|
+
check('场景 21a 链断裂:中间行篡改 → hard 台账链断裂(指首断行号)',
|
|
2923
|
+
r.status !== 0 && out.includes('台账链断裂') && /第 1 行/.test(out), `exit=${r.status}\n${out.slice(-400)}`);
|
|
2924
|
+
// 反证还原:篡改行恢复原 quote → 链复通(断言非恒绿)
|
|
2925
|
+
writeLedger(T, [row1, row2]);
|
|
2926
|
+
r = run(T);
|
|
2927
|
+
check('场景 21b 反证还原:篡改修复 → 链校验通过', !outOf(r).includes('台账链断裂'), outOf(r).slice(-300));
|
|
2928
|
+
rmfix(T);
|
|
2929
|
+
}
|
|
2930
|
+
{
|
|
2931
|
+
// 21c:全无 hash 的存量台账(v1.5.0 前形态)→ 零链告警(向后兼容)
|
|
2932
|
+
const T = mkfix();
|
|
2933
|
+
writeLedger(T, [
|
|
2934
|
+
{ ts: '2026-09-20T01:00:00Z', doc: 'workflow/intents/2026-09-20-legacy.md', stage: 'approved', fingerprint: 'c'.repeat(64), prev: 'draft' },
|
|
2935
|
+
{ ts: '2026-09-20T02:00:00Z', doc: 'workflow/intents/2026-09-20-legacy.md', stage: 'done', fingerprint: 'd'.repeat(64), prev: 'approved' },
|
|
2936
|
+
]);
|
|
2937
|
+
const r = run(T);
|
|
2938
|
+
check('场景 21c 存量无 hash 行:零链告警(不追溯)', !outOf(r).includes('台账链断裂'), outOf(r).slice(-300));
|
|
2939
|
+
rmfix(T);
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
|
|
2943
|
+
// ---- 场景 22:addedDates 持久缓存(2026-10-09 engine-quality-round3 W1)----
|
|
2944
|
+
{
|
|
2945
|
+
const log = '@2026-10-02T10:00:00+08:00\nworkflow/intents/a.md\nworkflow/plans/a.md\n@2026-10-01T09:00:00+08:00\nworkflow/intents/a.md\n';
|
|
2946
|
+
const m = parseAddedDatesLog(log);
|
|
2947
|
+
check('场景 22a 提取:新→旧序首遇 = 最新一次加入(原 IIFE 语义零复刻;注释「最早」为历史误述)', m.get('workflow/intents/a.md') === '2026-10-02T10:00:00+08:00' && m.get('workflow/plans/a.md') === '2026-10-02T10:00:00+08:00', JSON.stringify([...m]));
|
|
2948
|
+
}
|
|
2949
|
+
{
|
|
2950
|
+
const cacheP = path.join(os.tmpdir(), 'adc-' + Date.now() + '.json');
|
|
2951
|
+
let gitLogCalls = 0;
|
|
2952
|
+
const gitLog = () => { gitLogCalls++; return '@2026-10-01T09:00:00+08:00\nworkflow/intents/a.md\n'; };
|
|
2953
|
+
const head = 'head-sha-1';
|
|
2954
|
+
const m1 = addedDatesWithCache({ head, gitLog, cachePath: cacheP });
|
|
2955
|
+
const m2 = addedDatesWithCache({ head, gitLog, cachePath: cacheP });
|
|
2956
|
+
check('场景 22b 缓存命中:同 HEAD 二次调用 gitLog 仅 1 次且结果一致',
|
|
2957
|
+
gitLogCalls === 1 && m1.get('workflow/intents/a.md') === m2.get('workflow/intents/a.md'), 'calls=' + gitLogCalls);
|
|
2958
|
+
const m3 = addedDatesWithCache({ head: 'head-sha-2', gitLog, cachePath: cacheP });
|
|
2959
|
+
check('场景 22c HEAD 变更:重算(gitLog 计 2 次)', gitLogCalls === 2 && m3.get('workflow/intents/a.md') === '2026-10-01T09:00:00+08:00', 'calls=' + gitLogCalls);
|
|
2960
|
+
fs.writeFileSync(cacheP, '{broken json');
|
|
2961
|
+
const before = gitLogCalls;
|
|
2962
|
+
const m4 = addedDatesWithCache({ head: 'head-sha-2', gitLog, cachePath: cacheP });
|
|
2963
|
+
check('场景 22d 缓存损坏:fail-open 全算(gitLog 再调 1 次)且结果正确',
|
|
2964
|
+
gitLogCalls === before + 1 && m4.get('workflow/intents/a.md') === '2026-10-01T09:00:00+08:00', 'calls=' + gitLogCalls);
|
|
2965
|
+
const before2 = gitLogCalls;
|
|
2966
|
+
addedDatesWithCache({ head: null, gitLog, cachePath: cacheP });
|
|
2967
|
+
check('场景 22e 无 head:不读缓存直接全算(gitLog 再调 1 次)', gitLogCalls === before2 + 1, 'calls=' + gitLogCalls);
|
|
2968
|
+
fs.rmSync(cacheP, { force: true });
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2905
2971
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
2906
2972
|
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// check-stage-index.mjs — check-loop 检查 7 模块(阶段索引同步,2026-10-09 engine-quality-round2 A1)
|
|
2
|
+
// 拆分先例 = check-hygiene.mjs(检查 2/9/11/12/13)/ check-metric-claims.mjs(检查 16):
|
|
3
|
+
// ctx 传入既有 helper(linesOf——读异常响亮出账 + 按空文档降级的语义承载者,零复刻),
|
|
4
|
+
// 模块只返回 warnings 数组,不打印、不 exit、不改 ctx。
|
|
5
|
+
// 输出顺序契约:调用点位置即 warnings 插入序(check-loop 原位调用,段 id 沿 sh 版 '6'——清单编号 7
|
|
6
|
+
// 不动,gate-checklist 按 id 消费面不受影响)。
|
|
7
|
+
// 判据(沿 sh 版逐条):AGENTS.md 与 .agents/commands/new-task.md 须各含全部阶段指令路径串
|
|
8
|
+
// `.agents/commands/<cmd>.md`(双向索引);缺任一 = WARN。两文件可合法缺失(text 判空预期缺失态)。
|
|
9
|
+
// 测试:node templates/_agents/scripts/check-stage-index.test.mjs
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { gateSeg, finishSegs, makeCollector, appendSegs } from './gate-seg.mjs';
|
|
13
|
+
|
|
14
|
+
const STAGE_CMDS = ['plan', 'design', 'build', 'test', 'deploy', 'maintain', 'review'];
|
|
15
|
+
const FACE_DOCS = ['AGENTS.md', '.agents/commands/new-task.md'];
|
|
16
|
+
|
|
17
|
+
export function runCheckStageIndex(ctx) {
|
|
18
|
+
const { root: ROOT, linesOf } = ctx;
|
|
19
|
+
const warnings = [];
|
|
20
|
+
// 插桩(selfmeasure 先例):局部收集器 + appendSegs 并入全局——模块产出局部数组,直接共用全局
|
|
21
|
+
// 游标会错账(check-hygiene.mjs:36-39 实测教训)
|
|
22
|
+
const localStats = makeCollector(warnings, []);
|
|
23
|
+
const markGate = (id, label) => { if (ctx.gateStats) gateSeg(localStats, id, label); };
|
|
24
|
+
|
|
25
|
+
markGate('6', '阶段索引同步');
|
|
26
|
+
for (const cmd of STAGE_CMDS) {
|
|
27
|
+
for (const doc of FACE_DOCS) {
|
|
28
|
+
// existsSync 守卫(2026-10-06-check19-entry-enoent 同类):两文件可合法缺失(下方 text 判空本就
|
|
29
|
+
// 预期缺失态),fail-loud fs 读下按空跳过、不再响亮出账
|
|
30
|
+
const docPath = path.join(ROOT, doc);
|
|
31
|
+
const text = fs.existsSync(docPath) ? linesOf(docPath) : null;
|
|
32
|
+
if (text && !text.some((l) => l.includes(`.agents/commands/${cmd}.md`))) {
|
|
33
|
+
warnings.push(`- [WARN 阶段索引漂移] ${doc} 缺 ${cmd} 指令索引(两处阶段表须同步维护)`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (ctx.gateStats) appendSegs(ctx.gateStats, finishSegs(localStats));
|
|
39
|
+
return warnings;
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// check-stage-index.test.mjs — 检查 7 模块直测(2026-10-09 engine-quality-round2 A1)
|
|
2
|
+
// 四面:全绿 / AGENTS 侧缺索引 / new-task 文件缺失合法 / gate-seg 段归属(局部收集器并入)
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { runCheckStageIndex } from './check-stage-index.mjs';
|
|
7
|
+
import { makeCollector, finishSegs } from './gate-seg.mjs';
|
|
8
|
+
|
|
9
|
+
let pass = 0, fail = 0;
|
|
10
|
+
const check = (name, cond, detail = '') => {
|
|
11
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
12
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// ctx 构造:linesOf 提供读异常→null 语义(与 check-loop 内部实现同语义;「判定零复刻」指模块不重写
|
|
16
|
+
// 判据,测试 fixture 提供测试版 helper 是 check-hygiene.test 先例)
|
|
17
|
+
function makeCtx(root, overrides = {}) {
|
|
18
|
+
const fileLinesCache = new Map();
|
|
19
|
+
const linesOf = (file) => {
|
|
20
|
+
if (!fileLinesCache.has(file)) {
|
|
21
|
+
let lines = null;
|
|
22
|
+
try { lines = fs.readFileSync(file, 'utf8').split(/\r?\n/); } catch { lines = null; }
|
|
23
|
+
fileLinesCache.set(file, lines);
|
|
24
|
+
}
|
|
25
|
+
return fileLinesCache.get(file);
|
|
26
|
+
};
|
|
27
|
+
return { root, linesOf, ...overrides };
|
|
28
|
+
}
|
|
29
|
+
const ROUTES = '.agents/commands/plan.md → .agents/commands/design.md → .agents/commands/build.md → .agents/commands/test.md → .agents/commands/deploy.md → .agents/commands/maintain.md';
|
|
30
|
+
const mkRoot = (agentsBody, newTaskBody, { noAgents, noNewTask } = {}) => {
|
|
31
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'csi-'));
|
|
32
|
+
fs.mkdirSync(path.join(root, '.agents', 'commands'), { recursive: true });
|
|
33
|
+
if (!noAgents) fs.writeFileSync(path.join(root, 'AGENTS.md'), agentsBody);
|
|
34
|
+
if (!noNewTask) fs.writeFileSync(path.join(root, '.agents', 'commands', 'new-task.md'), newTaskBody);
|
|
35
|
+
return root;
|
|
36
|
+
};
|
|
37
|
+
const FULL = `# AGENTS\n\n${ROUTES}\n\n**横切**:.agents/commands/review.md\n`;
|
|
38
|
+
const FULL_NT = `# new-task\n\n| Plan | ${ROUTES} | Review(.agents/commands/review.md) |\n`;
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
// ① 双面全索引 → 零告警
|
|
42
|
+
const root = mkRoot(FULL, FULL_NT);
|
|
43
|
+
const r = runCheckStageIndex(makeCtx(root));
|
|
44
|
+
check('① 双面全索引 → 零告警', r.length === 0, JSON.stringify(r));
|
|
45
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
{
|
|
48
|
+
// ② AGENTS 侧缺 plan → 仅 AGENTS.md 侧告警(new-task 索引齐全不误报)
|
|
49
|
+
const root = mkRoot(FULL.replace('.agents/commands/plan.md → ', ''), FULL_NT);
|
|
50
|
+
const r = runCheckStageIndex(makeCtx(root));
|
|
51
|
+
check('② AGENTS 侧缺 plan → WARN 点名 AGENTS.md',
|
|
52
|
+
r.some((w) => w.includes('AGENTS.md 缺 plan 指令索引')), JSON.stringify(r));
|
|
53
|
+
check('② new-task 侧不告警(其索引齐全)', !r.some((w) => w.includes('new-task.md 缺')), JSON.stringify(r));
|
|
54
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
{
|
|
57
|
+
// ③ new-task 文件不存在 = 合法缺失态(existsSync 守卫)→ 零告警
|
|
58
|
+
const root = mkRoot(FULL, FULL_NT, { noNewTask: true });
|
|
59
|
+
const r = runCheckStageIndex(makeCtx(root));
|
|
60
|
+
check('③ new-task 文件缺失 → 合法缺失零告警', r.length === 0, JSON.stringify(r));
|
|
61
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
{
|
|
64
|
+
// ④ gate-seg 段归属:ctx.gateStats 在场时段 '6' 入账、warns 与实际 WARN 数一致(selfmeasure 接线
|
|
65
|
+
// 教训:模块须用局部收集器 + appendSegs,直接共用全局游标会错账)
|
|
66
|
+
const root = mkRoot(FULL.replace('.agents/commands/plan.md → ', ''), FULL_NT);
|
|
67
|
+
const stats = makeCollector([], []);
|
|
68
|
+
const r = runCheckStageIndex(makeCtx(root, { gateStats: stats }));
|
|
69
|
+
const seg6 = finishSegs(stats).filter((s) => s.id === '6');
|
|
70
|
+
check('④ gate-seg:段 6 入账一次且 warns 与实际 WARN 数一致',
|
|
71
|
+
seg6.length === 1 && seg6[0].warns === r.length && r.length === 1,
|
|
72
|
+
JSON.stringify({ seg6, ret: r }));
|
|
73
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
77
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -39,7 +39,7 @@ import path from 'node:path';
|
|
|
39
39
|
import readline from 'node:readline';
|
|
40
40
|
import { createHash, randomBytes } from 'node:crypto';
|
|
41
41
|
import { confirmGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
42
|
-
import { loadKitPolicy, hasFreshVerifyLine } from './policy.mjs';
|
|
42
|
+
import { loadKitPolicy, hasFreshVerifyLine, ledgerChainHash } from './policy.mjs';
|
|
43
43
|
|
|
44
44
|
// hasFreshVerify(root, doc):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
|
|
45
45
|
// policy.hasFreshVerifyLine)。只读,台账缺失/不可读一律 false(=无凭证,advisory 层 fail-open)。
|
|
@@ -120,10 +120,25 @@ export function applyTransition(text, targetStage, fingerprint16) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// appendLedger(root, entry):追加一行 JSON(目录不存在自动建;只 append 不重写)
|
|
123
|
+
// 哈希链(2026-10-09 engine-quality-round2 D):每行追加 prevHash(末行 hash,末行无 hash/空文件 → '')
|
|
124
|
+
// 与 hash(ledgerChainHash 单源计算,check-loop 15 同函数验链)——「就地改/删中间行」必留断链痕迹;
|
|
125
|
+
// 历史行零回填(无 hash 行后链重启),整文件重写不可机器防(边界声明见 policy.ledgerChainHash)。
|
|
123
126
|
export function appendLedger(root, entry) {
|
|
124
127
|
const p = path.join(root, '.agents', 'confirmations.jsonl');
|
|
125
128
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
126
|
-
|
|
129
|
+
let prevHash = '';
|
|
130
|
+
try {
|
|
131
|
+
if (fs.existsSync(p)) {
|
|
132
|
+
const lines = fs.readFileSync(p, 'utf8').split(/\r?\n/).filter((l) => l.trim());
|
|
133
|
+
if (lines.length) {
|
|
134
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
135
|
+
prevHash = last && typeof last.hash === 'string' ? last.hash : '';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch { /* 末行不可解析 → prevHash ''(链从本行重启;断裂由 check-loop 15 出账) */ }
|
|
139
|
+
const { hash: _h, prevHash: _p, ...rest } = entry;
|
|
140
|
+
const row = { ...rest, prevHash, hash: ledgerChainHash(rest, prevHash) };
|
|
141
|
+
fs.appendFileSync(p, JSON.stringify(row) + '\n');
|
|
127
142
|
return p;
|
|
128
143
|
}
|
|
129
144
|
|
|
@@ -104,6 +104,23 @@ const readLedgerOf = (root) => {
|
|
|
104
104
|
fs.rmSync(root, { recursive: true, force: true });
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// ---- S8b 哈希链(2026-10-09 engine-quality-round2 D):链字段写入 + 接续 + 重算自洽 ----
|
|
108
|
+
{
|
|
109
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-chain-'));
|
|
110
|
+
// 先写一行无 hash 的「存量」行(模拟 v1.5.0 前台账)——新行 prevHash 应为 ''(链重启)
|
|
111
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
112
|
+
fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: 'T0', doc: 'workflow/plans/old.md', stage: 'done', fingerprint: 'e'.repeat(64), prev: 'approved' }) + '\n');
|
|
113
|
+
appendLedger(root, { ts: 'T1', doc: 'workflow/plans/a.md', stage: 'approved', fingerprint: 'f'.repeat(64), prev: 'draft' });
|
|
114
|
+
appendLedger(root, { ts: 'T2', doc: 'workflow/plans/a.md', stage: 'done', fingerprint: 'g'.repeat(64), prev: 'approved' });
|
|
115
|
+
const rows = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
116
|
+
check('S8b 存量行后首链行 prevHash 为空(链重启,历史零回填)', rows[0].hash === undefined && rows[1].prevHash === '');
|
|
117
|
+
check('S8b 第二链行 prevHash 接续第一链行 hash', rows[2].prevHash === rows[1].hash);
|
|
118
|
+
const { ledgerChainHash } = await import('./policy.mjs');
|
|
119
|
+
const recompute = (row) => { const { hash, prevHash, ...rest } = row; return ledgerChainHash(rest, prevHash) === hash; };
|
|
120
|
+
check('S8b 两链行 hash 重算自洽(CRLF/键序规范化单源)', recompute(rows[1]) && recompute(rows[2]));
|
|
121
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
// ---- S9【核心】非 TTY spawn(AI 调用路径)必须被拒 ----
|
|
108
125
|
{
|
|
109
126
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tty-'));
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// edit-face-check — 常驻面编辑时快检(2026-10-09 engine-quality-round2 B2)
|
|
3
|
+
// 动机:常驻面(AGENTS.md 等)的机器约束此前只在 git 钩子层生效——check-loop 仓库模式只扫 HEAD,
|
|
4
|
+
// 工作树编辑期零反馈(实测:常驻面重构砍掉阶段索引,到提交才被检查 7 报警)。本脚本把判定左移到编辑时。
|
|
5
|
+
// 判定零复刻铁律:闭环判定**单源**复用 check-loop——以 CHECK_LOOP_ROOT=<root>(全扫模式,扫工作树
|
|
6
|
+
// 非 HEAD)子进程消费其裁决,本脚本不实现任何门判定;唯一自算是常驻面字节预算 advisory(工作树
|
|
7
|
+
// LF 字节 vs rule-budgets.txt 上限——提交门口径走 git index/HEAD(rule-budget.sh),两者判据不同面,
|
|
8
|
+
// 此处为编辑时 advisory,冲突时以提交门为准)。
|
|
9
|
+
// exit 语义:check-loop hard(exit≠0)→ 2;预算 advisory 超限 → 1;其余 → 0。
|
|
10
|
+
// WARN **不进 exit**(打印明细供 AI/人对照)——绝对 WARN 计数会让带存量 advisory 的仓库恒非零
|
|
11
|
+
// (本仓即有用户拍板保留的存量 WARN),工具就不可用了;编辑破坏的表现(如阶段索引漂移 WARN)
|
|
12
|
+
// 在输出里一眼可见,由编辑者处置。
|
|
13
|
+
// 用法:node .agents/scripts/edit-face-check.mjs [--root <仓库根>](缺省 cwd;测试注入 EDIT_FACE_CHECK_SPAWN)
|
|
14
|
+
// 测试:node templates/_agents/scripts/edit-face-check.test.mjs
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
// LF 归一字节数(编辑时口径——工作区文件可能 CRLF,预算表口径注释声明「比较用 LF 归一」)
|
|
21
|
+
export function lfBytes(text) {
|
|
22
|
+
return Buffer.byteLength(String(text).replace(/\r\n/g, '\n'), 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// globToRe:rule-budgets.txt 行首路径 → 正则(支持 `*` 段内通配与结尾 `/` 目录前缀;其余字面)
|
|
26
|
+
export function globToRe(pattern) {
|
|
27
|
+
const dir = pattern.endsWith('/');
|
|
28
|
+
let src = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
|
|
29
|
+
if (dir) src += '.*';
|
|
30
|
+
return new RegExp(`^${src}${dir ? '' : '$'}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// budgetAdvisory:常驻面预算 advisory(编辑时口径)。files: Map<rel, 工作树文本>;budgetsText: 表原文。
|
|
34
|
+
// 返回超限行 [{ rel, limit, actual }];表缺失/行非法 → 空数组(advisory 层 fail-open)。
|
|
35
|
+
export function budgetAdvisory(files, budgetsText) {
|
|
36
|
+
const out = [];
|
|
37
|
+
if (!budgetsText) return out;
|
|
38
|
+
for (const line of budgetsText.split(/\r?\n/)) {
|
|
39
|
+
const t = line.replace(/\r$/, '').trim();
|
|
40
|
+
if (!t || t.startsWith('#')) continue;
|
|
41
|
+
const m = t.match(/^(\S+)\s+(\d+)$/);
|
|
42
|
+
if (!m) continue;
|
|
43
|
+
const [, pattern, limitStr] = m;
|
|
44
|
+
const limit = Number(limitStr);
|
|
45
|
+
const re = globToRe(pattern);
|
|
46
|
+
for (const [rel, text] of files) {
|
|
47
|
+
if (!re.test(rel)) continue;
|
|
48
|
+
const actual = lfBytes(text);
|
|
49
|
+
if (actual > limit) out.push({ rel, limit, actual });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// runCheck:工作树面 check-loop(CHECK_LOOP_ROOT 全扫)。返回 { code, output, ms }。
|
|
56
|
+
function runCheck(root, scriptDir) {
|
|
57
|
+
const t0 = Date.now();
|
|
58
|
+
const r = spawnSync(process.execPath, [path.join(scriptDir, 'check-loop.mjs')], {
|
|
59
|
+
cwd: root, encoding: 'utf8', windowsHide: true,
|
|
60
|
+
env: { ...process.env, CHECK_LOOP_ROOT: root },
|
|
61
|
+
});
|
|
62
|
+
return { code: r.status ?? 1, output: `${r.stdout || ''}${r.stderr || ''}`, ms: Date.now() - t0 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function main(root = process.cwd(), scriptDir = path.dirname(fileURLToPath(import.meta.url))) {
|
|
66
|
+
// ① 闭环判定(单源 check-loop,工作树全扫)
|
|
67
|
+
const ck = runCheck(root, scriptDir);
|
|
68
|
+
const warnCount = (ck.output.match(/\[WARN /g) || []).length;
|
|
69
|
+
// ② 常驻面预算 advisory(AGENTS.md + 预算表内文件的工作树字节)
|
|
70
|
+
let over = [];
|
|
71
|
+
try {
|
|
72
|
+
const budgetsPath = path.join(root, '.agents', 'rule-budgets.txt');
|
|
73
|
+
const budgetsText = fs.existsSync(budgetsPath) ? fs.readFileSync(budgetsPath, 'utf8') : null;
|
|
74
|
+
const files = new Map();
|
|
75
|
+
const agentsPath = path.join(root, 'AGENTS.md');
|
|
76
|
+
if (fs.existsSync(agentsPath)) files.set('AGENTS.md', fs.readFileSync(agentsPath, 'utf8'));
|
|
77
|
+
over = budgetAdvisory(files, budgetsText);
|
|
78
|
+
} catch { /* advisory 层 fail-open:预算读不到不阻断编辑时反馈 */ }
|
|
79
|
+
|
|
80
|
+
console.log(`▶ edit-face-check → ${root}(工作树面)`);
|
|
81
|
+
console.log(` check-loop:${ck.code === 0 ? '通过' : `exit ${ck.code}`},WARN ${warnCount} 条(${ck.ms}ms)——与本仓存量 advisory 对照看新增`);
|
|
82
|
+
// WARN 明细透传(编辑破坏的判读面——如删阶段索引会在此点名「阶段索引漂移」)
|
|
83
|
+
for (const line of ck.output.split(/\r?\n/)) {
|
|
84
|
+
if (/\[WARN /.test(line)) console.log(' ' + line.trim().slice(0, 200));
|
|
85
|
+
}
|
|
86
|
+
for (const { rel, limit, actual } of over) {
|
|
87
|
+
console.log(` ⚠️ 预算 advisory:${rel} 工作树 ${actual}B > 上限 ${limit}B(编辑时口径;提交门以 rule-budget.sh 为准)`);
|
|
88
|
+
}
|
|
89
|
+
if (ck.code !== 0) {
|
|
90
|
+
console.error('❌ 闭环判定 hard 未过——先修复再提交(判定单源 check-loop,详见上方输出)');
|
|
91
|
+
return 2;
|
|
92
|
+
}
|
|
93
|
+
if (over.length > 0) {
|
|
94
|
+
console.error('⚠️ 常驻面预算 advisory 超限——建议编辑期就瘦,别留到提交门');
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
console.log('✅ 编辑时快检完成(hard/预算两道均过;WARN 明细请对照处置)');
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('edit-face-check.mjs');
|
|
102
|
+
if (isMain) {
|
|
103
|
+
let root = process.cwd();
|
|
104
|
+
const i = process.argv.indexOf('--root');
|
|
105
|
+
if (i > 0 && process.argv[i + 1]) root = path.resolve(process.argv[i + 1]);
|
|
106
|
+
process.exit(main(root));
|
|
107
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// edit-face-check.test.mjs — 编辑时快检(2026-10-09 engine-quality-round2 B2)
|
|
2
|
+
// 三面:纯函数(lfBytes/globToRe/budgetAdvisory)+ 端到端全绿面 + 破坏面(删阶段索引→WARN→exit 1、
|
|
3
|
+
// 预算超限→exit 1)。判定零复刻:端到端经 CHECK_LOOP_ROOT 消费真 check-loop,fixture 复制脚本集。
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const SRC = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
let pass = 0, fail = 0;
|
|
12
|
+
const check = (name, cond, detail = '') => {
|
|
13
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
14
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const mod = await import(pathToFileURL(path.join(SRC, 'edit-face-check.mjs')).href);
|
|
18
|
+
const { lfBytes, globToRe, budgetAdvisory } = mod;
|
|
19
|
+
|
|
20
|
+
// ---- ① 纯函数 ----
|
|
21
|
+
check('lfBytes:CRLF 归一计 UTF-8 字节', lfBytes('a\r\nb中文') === Buffer.byteLength('a\nb中文', 'utf8'));
|
|
22
|
+
check('globToRe:精确路径', globToRe('AGENTS.md').test('AGENTS.md') && !globToRe('AGENTS.md').test('x/AGENTS.md'));
|
|
23
|
+
check('globToRe:段内通配', globToRe('.agents/commands/*.md').test('.agents/commands/plan.md') && !globToRe('.agents/commands/*.md').test('.agents/commands/x/plan.md'));
|
|
24
|
+
check('globToRe:目录前缀', globToRe('.agents/cache/').test('.agents/cache/a/b.json'));
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
const files = new Map([
|
|
28
|
+
['AGENTS.md', 'x'.repeat(120)],
|
|
29
|
+
['.agents/commands/plan.md', 'y'.repeat(50)],
|
|
30
|
+
]);
|
|
31
|
+
const budgets = '# 注释行\nAGENTS.md 100\n.agents/commands/*.md 200\n非法行\n';
|
|
32
|
+
const r = budgetAdvisory(files, budgets);
|
|
33
|
+
check('budgetAdvisory:超限单文件报、未超限不报、非法行跳过',
|
|
34
|
+
r.length === 1 && r[0].rel === 'AGENTS.md' && r[0].limit === 100 && r[0].actual === 120, JSON.stringify(r));
|
|
35
|
+
check('budgetAdvisory:表缺失 fail-open', budgetAdvisory(files, null).length === 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- ② 端到端 fixture ----
|
|
39
|
+
const SCRIPTS = ['check-loop.mjs', 'policy.mjs', 'stage-gates.mjs', 'check-metric-claims.mjs', 'check-hygiene.mjs', 'check-stage-index.mjs', 'gate-seg.mjs', 'workflow-enums.mjs', 'edit-face-check.mjs'];
|
|
40
|
+
// 引用完整性哑文件(检查 4/6/7 的存在性判据——避免 fixture 结构性 WARN 淹没断言面)
|
|
41
|
+
const REF_COMMANDS = ['new-task', 'plan', 'design', 'build', 'test', 'deploy', 'maintain', 'review'];
|
|
42
|
+
const REF_ROLES = ['implementer', 'independent-reviewer', 'ui-verifier'];
|
|
43
|
+
function mkFixture({ breakStageIndex, overBudget }) {
|
|
44
|
+
const fx = fs.mkdtempSync(path.join(os.tmpdir(), 'efc-'));
|
|
45
|
+
fs.mkdirSync(path.join(fx, '.agents', 'scripts'), { recursive: true });
|
|
46
|
+
for (const f of SCRIPTS) fs.copyFileSync(path.join(SRC, f), path.join(fx, '.agents', 'scripts', f));
|
|
47
|
+
for (const f of ['kit.json', 'workflow-modules.txt', 'workflow-enums.txt'])
|
|
48
|
+
if (fs.existsSync(path.join(SRC, '..', f))) fs.copyFileSync(path.join(SRC, '..', f), path.join(fx, '.agents', f));
|
|
49
|
+
fs.mkdirSync(path.join(fx, 'workflow', 'intents'), { recursive: true });
|
|
50
|
+
for (const d of ['specs', 'plans', 'incidents']) fs.mkdirSync(path.join(fx, 'workflow', d), { recursive: true });
|
|
51
|
+
for (const c of REF_COMMANDS) { fs.mkdirSync(path.join(fx, '.agents', 'commands'), { recursive: true }); fs.writeFileSync(path.join(fx, '.agents', 'commands', `${c}.md`), `# ${c}\n`); }
|
|
52
|
+
for (const rname of REF_ROLES) { fs.mkdirSync(path.join(fx, '.agents', 'roles'), { recursive: true }); fs.writeFileSync(path.join(fx, '.agents', 'roles', `${rname}.md`), `# ${rname}\n`); }
|
|
53
|
+
// 常驻面 AGENTS.md:阶段索引齐全(含 review)/ 破坏面删索引(破坏只在 AGENTS.md 侧——new-task
|
|
54
|
+
// 哑文件写全索引,钉检查 7 的双向语义差异)
|
|
55
|
+
const routes = '.agents/commands/plan.md → .agents/commands/design.md → .agents/commands/build.md → .agents/commands/test.md → .agents/commands/deploy.md → .agents/commands/maintain.md';
|
|
56
|
+
const reviewLine = breakStageIndex ? '' : '\n**横切**:.agents/commands/review.md\n';
|
|
57
|
+
const body = `# AGENTS\n\n## 阶段路由\n\n**入口**:.agents/commands/new-task.md\n**指令**:${breakStageIndex ? '(已删)' : routes}${reviewLine}\n`;
|
|
58
|
+
fs.writeFileSync(path.join(fx, 'AGENTS.md'), overBudget ? body + 'x'.repeat(200) : body);
|
|
59
|
+
fs.writeFileSync(path.join(fx, '.agents', 'commands', 'new-task.md'),
|
|
60
|
+
`# new-task\n\n| Plan | ${routes} | Review |\n`);
|
|
61
|
+
fs.writeFileSync(path.join(fx, '.agents', 'rule-budgets.txt'), `AGENTS.md ${overBudget ? 100 : 100000}\n`);
|
|
62
|
+
return fx;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
// 全绿面:空 workflow + 引用齐全 + 预算内 → hard/预算两道过(exit 0)。
|
|
67
|
+
// 注:不要求零 WARN——check-loop 全扫对极简 fixture 有结构性 advisory(引用/角色形态),明细
|
|
68
|
+
// 承载给编辑者对照;exit 语义只钉 hard 与预算两道(绝对 WARN 计数会让存量仓库恒非零)。
|
|
69
|
+
const fx = mkFixture({});
|
|
70
|
+
const driver = path.join(fx, 'driver.mjs');
|
|
71
|
+
fs.writeFileSync(driver, `import { main } from ${JSON.stringify('file:///' + path.join(fx, '.agents', 'scripts', 'edit-face-check.mjs').replace(/\\/g, '/'))};\nprocess.exit(main(${JSON.stringify(fx)}, ${JSON.stringify(path.join(fx, '.agents', 'scripts'))}));\n`);
|
|
72
|
+
const r = spawnSync(process.execPath, [driver], { encoding: 'utf8' });
|
|
73
|
+
check('E2E 全绿面:exit 0(hard/预算两道过)且 AGENTS.md 侧无索引漂移', r.status === 0 && !/AGENTS\.md 缺 .* 指令索引/.test(r.stdout || ''), `status=${r.status}\n${(r.stdout || '') + (r.stderr || '')}`.slice(0, 600));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
// 破坏面:删阶段索引 → 检查 7 WARN 点名(明细承载编辑破坏——破坏面 vs 全绿面的差异就在输出;
|
|
78
|
+
// hard 不误触:阶段索引是 warning 级,exit≠2)
|
|
79
|
+
const fx = mkFixture({ breakStageIndex: true });
|
|
80
|
+
const driver = path.join(fx, 'driver.mjs');
|
|
81
|
+
fs.writeFileSync(driver, `import { main } from ${JSON.stringify('file:///' + path.join(fx, '.agents', 'scripts', 'edit-face-check.mjs').replace(/\\/g, '/'))};\nprocess.exit(main(${JSON.stringify(fx)}, ${JSON.stringify(path.join(fx, '.agents', 'scripts'))}));\n`);
|
|
82
|
+
const r = spawnSync(process.execPath, [driver], { encoding: 'utf8' });
|
|
83
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
84
|
+
check('E2E 破坏面(删阶段索引):WARN 点名 AGENTS.md 侧索引漂移且 hard 不误触', r.status !== 2 && /AGENTS\.md 缺 plan 指令索引/.test(out), `status=${r.status}\n${out.slice(0, 600)}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
// 预算超限面:AGENTS.md 超上限 → exit 1(advisory 面单独可达——workflow 全绿)
|
|
89
|
+
const fx = mkFixture({ overBudget: true });
|
|
90
|
+
const driver = path.join(fx, 'driver.mjs');
|
|
91
|
+
fs.writeFileSync(driver, `import { main } from ${JSON.stringify('file:///' + path.join(fx, '.agents', 'scripts', 'edit-face-check.mjs').replace(/\\/g, '/'))};\nprocess.exit(main(${JSON.stringify(fx)}, ${JSON.stringify(path.join(fx, '.agents', 'scripts'))}));\n`);
|
|
92
|
+
const r = spawnSync(process.execPath, [driver], { encoding: 'utf8' });
|
|
93
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
94
|
+
check('E2E 预算超限面:exit 1 且 advisory 点名', r.status === 1 && /预算 advisory/.test(out), `status=${r.status}\n${out.slice(0, 500)}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
98
|
+
process.exit(fail ? 1 : 0);
|