agentic-flow-kit 1.1.8 → 1.2.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/package.json +1 -1
- package/src/doctor.mjs +50 -1
- package/src/fresh-init.test.mjs +18 -0
- package/src/init.mjs +12 -4
- package/src/profiles.mjs +34 -0
- package/src/sync.mjs +23 -1
- package/src/sync.test.mjs +70 -0
- package/templates/_agents/scripts/check-loop.mjs +33 -3
- package/templates/_agents/scripts/check-loop.test.mjs +41 -0
- package/templates/_agents/scripts/confirm-doc.mjs +21 -1
- package/templates/_agents/scripts/confirm-doc.test.mjs +36 -0
- package/templates/_agents/scripts/gate-checklist.mjs +1 -0
- package/templates/_agents/scripts/pipeline-run.mjs +3 -1
- package/templates/_agents/scripts/policy.mjs +36 -0
- package/templates/_agents/scripts/verify.mjs +25 -3
- package/templates/_agents/scripts/verify.test.mjs +38 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-flow-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "AI-Native 闭环工作流 + wiki 知识层脚手架——flow-kit init 一条命令为新项目装好 .agents 工作流引擎(阶段命令/角色契约/门禁钩子/kb 检索/看板/规则面预算)+ workflow 文档闭环 + wiki 知识库,装完自包含、零运行时依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/doctor.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { execSync, spawnSync } from 'node:child_process';
|
|
|
5
5
|
import net from 'node:net';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { listTree, scriptTrusted } from './render.mjs';
|
|
8
|
-
import { isOwned, HOSTS, pickStackVars } from './profiles.mjs';
|
|
8
|
+
import { isOwned, HOSTS, pickStackVars, srcTemplatePath, templateDriftOf, TEMPLATE_DRIFT_EXCLUDE } from './profiles.mjs';
|
|
9
9
|
|
|
10
10
|
// 宿主目录映射(与 src/profiles.mjs#HOSTS 同源:装副本目录名前缀带点)
|
|
11
11
|
const HOST_DIR = Object.fromEntries(Object.entries(HOSTS).map(([k, v]) => [k, v.dir]));
|
|
@@ -257,6 +257,19 @@ export function doctor(args, pkgRoot) {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// 6.9 模板下发感知(2026-10-06 template-downstream,只读 advisory——与 sync 出账同判据
|
|
261
|
+
// profiles.templateDriftOf 单源;doctor 不写 kit.json,两次 sync 之间持续回显,sync 后自然消隐)。
|
|
262
|
+
// 语义:源仓模板已演进且盘面未跟随 → WARN 提醒(owned 归项目所有,不自动覆盖);首次引入 WARN。
|
|
263
|
+
// 旧装户 owned[] 无 srcSha256 → no-anchor 静默跳过(无判定依据不产出告警),下次 sync 写锚后生效。
|
|
264
|
+
const tplRes = checkTemplateDrift(target, pkgRoot);
|
|
265
|
+
if (tplRes.skipped) {
|
|
266
|
+
if (tplRes.note) add('PASS', `模板感知跳过(${tplRes.note})`);
|
|
267
|
+
} else if (tplRes.stale.length === 0) {
|
|
268
|
+
add('PASS', `模板感知 ${tplRes.total} 份无陈旧分歧(源仓模板与上次同步锚一致或盘面已跟随)`);
|
|
269
|
+
} else {
|
|
270
|
+
add('WARN', `模板感知:源仓 ${tplRes.stale.length} 份模板自上次同步后有演进且盘面未跟随:${tplRes.stale.join('、')}——如需跟随从包源 templates/ 拷贝;有意定制可忽略(跑 flow-kit sync 刷新锚后本项静默)`);
|
|
271
|
+
}
|
|
272
|
+
|
|
260
273
|
// 7. check-loop——2026-09-26 check-loop-node 起 node 实现直跑(sh 版曾需先探 sh 可用性:Windows
|
|
261
274
|
// PowerShell 常无 sh,ENOENT 曾被吞进 hard-block 分支报成空原因假警报,incident 2026-09-24-doctor-sh-enoent;
|
|
262
275
|
// 迁移后无 sh 依赖,探针退役。check-loop.sh 为兼容 shim,pre-push 钩子路径照常)
|
|
@@ -351,6 +364,42 @@ export function checkOwnedDrift(target) {
|
|
|
351
364
|
return { drift, gone, total: owned.length, skipped: false };
|
|
352
365
|
}
|
|
353
366
|
|
|
367
|
+
// 模板下发感知(2026-10-06 template-downstream):只读判据——owned 模板条目三方 sha 比对,
|
|
368
|
+
// 判定单源 profiles.templateDriftOf(与 sync 出账同源,防两处字面量漂移)。恒不写 kit.json。
|
|
369
|
+
// 返回 { skipped, note?, total, stale[] }:skipped=true 时 note 说明原因(无 kit / 无 owned /
|
|
370
|
+
// 无锚——旧装户静默不追溯);total=参与比对的条目数;stale=stale-drift 的 rel 清单。
|
|
371
|
+
export function checkTemplateDrift(target, pkgRoot) {
|
|
372
|
+
const kitPath = path.join(target, '.agents', 'kit.json');
|
|
373
|
+
if (!fs.existsSync(kitPath)) return { skipped: true, note: 'kit.json 不存在', total: 0, stale: [] };
|
|
374
|
+
let kit;
|
|
375
|
+
try {
|
|
376
|
+
kit = JSON.parse(fs.readFileSync(kitPath, 'utf8'));
|
|
377
|
+
} catch (e) {
|
|
378
|
+
return { skipped: true, note: `kit.json 解析失败:${e.message}`, total: 0, stale: [] };
|
|
379
|
+
}
|
|
380
|
+
if (!pkgRoot) return { skipped: true, note: '无包源根', total: 0, stale: [] };
|
|
381
|
+
const owned = Array.isArray(kit.owned) ? kit.owned : [];
|
|
382
|
+
if (owned.length === 0) return { skipped: true, note: 'kit.json 无 owned 条目', total: 0, stale: [] };
|
|
383
|
+
const shaText = (p) => createHash('sha256').update(fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'), 'utf8').digest('hex');
|
|
384
|
+
const stale = [];
|
|
385
|
+
let total = 0;
|
|
386
|
+
let anchored = 0;
|
|
387
|
+
for (const f of owned) {
|
|
388
|
+
if (TEMPLATE_DRIFT_EXCLUDE.has(f.rel)) continue;
|
|
389
|
+
const src = srcTemplatePath(pkgRoot, f.rel);
|
|
390
|
+
if (!src) continue;
|
|
391
|
+
total++;
|
|
392
|
+
if (typeof f.srcSha256 !== 'string' || !f.srcSha256) continue; // no-anchor:旧装户静默跳过
|
|
393
|
+
anchored++;
|
|
394
|
+
const srcCur = shaText(src);
|
|
395
|
+
const diskPath = path.join(target, f.rel);
|
|
396
|
+
const disk = fs.existsSync(diskPath) ? shaText(diskPath) : null;
|
|
397
|
+
if (templateDriftOf({ disk, srcRecord: f.srcSha256, srcCur }) === 'stale-drift') stale.push(f.rel);
|
|
398
|
+
}
|
|
399
|
+
if (anchored === 0) return { skipped: true, note: 'owned 模板条目均无 srcSha256 锚(旧装户,下次 sync 写锚后生效)', total: 0, stale: [] };
|
|
400
|
+
return { skipped: false, total, stale };
|
|
401
|
+
}
|
|
402
|
+
|
|
354
403
|
// 跨宿主薄适配正文段漂移校验(独立 export 供 doctor 主流程 + 单元测试共用;2026-09-25 cross-host-sync)
|
|
355
404
|
// 装户侧视角:.agents/{commands,roles}/*.md 是权威源;4 宿主目录(zcode→.zcode、omp→.omp、
|
|
356
405
|
// opencode→.opencode、trae→.trae)下 {agents,commands}/*.md 是薄适配。按正文段 sha 比对
|
package/src/fresh-init.test.mjs
CHANGED
|
@@ -25,6 +25,24 @@ const out = `${r.stdout || ''}${r.stderr || ''}`;
|
|
|
25
25
|
check('fresh init exit 0', r.status === 0, `exit=${r.status}\n${out.split('\n').slice(-30).join('\n')}`);
|
|
26
26
|
const kit = JSON.parse(fs.readFileSync(path.join(dir, '.agents', 'kit.json'), 'utf8'));
|
|
27
27
|
check('新装 audit 为 false 且 policyVersion 为 2', kit.audit === false && kit.policyVersion === 2, JSON.stringify({ audit: kit.audit, policyVersion: kit.policyVersion }));
|
|
28
|
+
// 模板下发感知初始锚(2026-10-06 template-downstream):有包源模板对应的 owned 条目带 srcSha256=
|
|
29
|
+
// 包源模板原文 LF 归一 sha;生成器目标(INDEX.md 类)排除出感知面无该键
|
|
30
|
+
{
|
|
31
|
+
const { createHash } = await import('node:crypto');
|
|
32
|
+
const tplAnchor = (rel) => kit.owned.find((f) => f.rel === rel);
|
|
33
|
+
const shaOf = (buf) => createHash('sha256').update(buf).digest('hex');
|
|
34
|
+
const agents = tplAnchor('AGENTS.md');
|
|
35
|
+
const srcRaw = fs.readFileSync(path.join(ROOT, 'templates', 'AGENTS.md'), 'utf8').replace(/\r\n/g, '\n');
|
|
36
|
+
check('新装 owned 模板条目带 srcSha256 且等于包源原文 sha(AGENTS.md)',
|
|
37
|
+
agents && typeof agents.srcSha256 === 'string' && agents.srcSha256 === shaOf(Buffer.from(srcRaw, 'utf8')),
|
|
38
|
+
JSON.stringify(agents || null));
|
|
39
|
+
check('新装生成器目标无锚(wiki/INDEX.md 排除出感知面)',
|
|
40
|
+
tplAnchor('wiki/INDEX.md') !== undefined && tplAnchor('wiki/INDEX.md').srcSha256 === undefined);
|
|
41
|
+
const cfg = tplAnchor('.agents/hooks/commit-check.config.json');
|
|
42
|
+
check('新装无包源模板对应条目无锚(生成类配置不参与感知)', cfg !== undefined && cfg.srcSha256 === undefined);
|
|
43
|
+
const rb = tplAnchor('.agents/rule-budgets.txt');
|
|
44
|
+
check('新装 _agents 前缀翻译参与感知(rule-budgets 带锚)', rb !== undefined && typeof rb.srcSha256 === 'string');
|
|
45
|
+
}
|
|
28
46
|
check('claude 命令与角色薄适配落盘',
|
|
29
47
|
fs.existsSync(path.join(dir, '.claude', 'commands', 'wf-plan.md'))
|
|
30
48
|
&& fs.existsSync(path.join(dir, '.claude', 'agents', 'implementer.md')));
|
package/src/init.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import readline from 'node:readline';
|
|
6
6
|
import { execSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import { renderTree, renderContent, sha256, scriptTrusted, listTree } from './render.mjs';
|
|
8
|
-
import { HOSTS, STACKS, STACK_ALIASES, settingsJson, commitCheckConfig, pickStackVars, isOwned } from './profiles.mjs';
|
|
8
|
+
import { HOSTS, STACKS, STACK_ALIASES, settingsJson, commitCheckConfig, pickStackVars, isOwned, srcTemplatePath, TEMPLATE_DRIFT_EXCLUDE } from './profiles.mjs';
|
|
9
9
|
import { doctor } from './doctor.mjs';
|
|
10
10
|
|
|
11
11
|
function fail(msg) {
|
|
@@ -292,7 +292,7 @@ export async function init(args, pkgRoot) {
|
|
|
292
292
|
// owned 的生成器目标按生成后盘面重记(P1-2);记账一律 LF 归一 sha(P1-4——render 写盘已归一,
|
|
293
293
|
// 生成器重写目标与用户合并文件两侧同口径,跨 checkout 字节稳定)
|
|
294
294
|
const shaText = (p) => sha256(Buffer.from(fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'), 'utf8'));
|
|
295
|
-
const GEN_TARGETS =
|
|
295
|
+
const GEN_TARGETS = TEMPLATE_DRIFT_EXCLUDE; // 单源挪 profiles(2026-10-06 template-downstream):模板感知排除面与生成器重记面同集
|
|
296
296
|
const managed = [
|
|
297
297
|
...t.written.filter((f) => !isOwned(f.rel)),
|
|
298
298
|
// 宿主文件渲染时相对宿主根,入台账须还原为项目根相对路径
|
|
@@ -311,6 +311,14 @@ export async function init(args, pkgRoot) {
|
|
|
311
311
|
owned.push({ rel, sha256: shaText(path.join(target, rel)) });
|
|
312
312
|
}
|
|
313
313
|
if (agentsMergedSha) owned.push({ rel: 'AGENTS.md', sha256: agentsMergedSha });
|
|
314
|
+
// 模板下发感知初始锚(2026-10-06 template-downstream):有包源模板对应的 owned 条目记
|
|
315
|
+
// srcSha256=包源模板原始文件 LF 归一 sha——锚记「见过的源」而非「期望盘面」(装户渲染/定制与锚正交);
|
|
316
|
+
// GEN_TARGETS 是生成器重写目标,不期望被跟随,排除出感知面(防「拉取了也被重写」的单周期无谓出账)
|
|
317
|
+
const ownedFinal = owned.map((f) => {
|
|
318
|
+
if (GEN_TARGETS.has(f.rel)) return f;
|
|
319
|
+
const src = srcTemplatePath(pkgRoot, f.rel);
|
|
320
|
+
return src ? { ...f, srcSha256: shaText(src) } : f;
|
|
321
|
+
});
|
|
314
322
|
const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'));
|
|
315
323
|
fs.mkdirSync(path.dirname(kitPath), { recursive: true });
|
|
316
324
|
fs.writeFileSync(kitPath, `${JSON.stringify({
|
|
@@ -322,9 +330,9 @@ export async function init(args, pkgRoot) {
|
|
|
322
330
|
createdAt: new Date().toISOString(),
|
|
323
331
|
options: { hosts, stack: opt.stack, boardPort: String(opt.boardPort) },
|
|
324
332
|
managed,
|
|
325
|
-
owned,
|
|
333
|
+
owned: ownedFinal,
|
|
326
334
|
}, null, 2)}\n`);
|
|
327
|
-
console.log(` kit.json 台账:managed ${managed.length} 份 | owned ${
|
|
335
|
+
console.log(` kit.json 台账:managed ${managed.length} 份 | owned ${ownedFinal.length} 份(版本 ${pkg.version})`);
|
|
328
336
|
|
|
329
337
|
// 8) 自检(kit.json 已按生成后盘面记账——doctor §6.6 应全绿,fresh init 以 exit 0 收场)
|
|
330
338
|
// 输出语义(p2-batch1 P2-4):doctor 打印在异步端口回调里、晚于本横幅——脚本化消费方按退出码判定
|
package/src/profiles.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// 宿主注册表 + 技术栈 profile + init 生成的 owned 基线配置
|
|
2
|
+
// fs/path 仅供 srcTemplatePath(模板下发感知)——显式 import,不依赖 Node 22+ 的全局 fs(engines >=18)
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
2
5
|
// stack 决定门禁配置三处:commit-check 的条件构建(builds)与质量检测(checks,秒级确定性
|
|
3
6
|
// 检查——lint/类型/vet;测试不放提交门,关单在 test.md 阶段门)、settings.json 的自检验
|
|
4
7
|
// 命令权限(allow)、AGENTS.md「项目适配区」命令预填;不改工作流流程本身。
|
|
@@ -112,6 +115,37 @@ export function isOwned(rel) {
|
|
|
112
115
|
|| rel === '.agents/hooks/local-pre-commit';
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
// ---- 模板下发感知(2026-10-06 template-downstream)----
|
|
119
|
+
// 装户 owned 模板无下发通道(init 一次性复制),源仓演进静默陈旧——S18/S20 装户事故根因。
|
|
120
|
+
// 感知层 = 三方 sha 比对(一律 LF 归一,与 ownedSha 自愈同口径):
|
|
121
|
+
// disk=装户盘面 / srcRecord=kit.json owned[].srcSha256(上次 sync/init 见过的包源 sha)/ srcCur=本次包源。
|
|
122
|
+
|
|
123
|
+
// 生成器目标(init「按生成后盘面重记」的清单,原 init 内 GEN_TARGETS 挪此单源):生成器拥有该文件,
|
|
124
|
+
// 装户不期望跟随包源模板——模板感知排除出比对面(防「拉取了也被生成器重写」的无谓出账)。新增生成器目标须同步本清单。
|
|
125
|
+
export const TEMPLATE_DRIFT_EXCLUDE = new Set(['workflow/INDEX.md', 'wiki/INDEX.md', 'wiki/知识沉淀总览.html']);
|
|
126
|
+
|
|
127
|
+
// 映射规则单源:renderTree 的「srcRoot/templates ↔ targetRoot/项目根」关系 + 点目录前缀翻译
|
|
128
|
+
// (render.mjs:`_agents` → `.agents`)——装户 rel 以 .agents/ 开头时探 templates/_agents/<余径>。
|
|
129
|
+
// 复核 P2-1(2026-10-06 template-downstream 独立复核):漏翻译会让 rule-budgets.txt /
|
|
130
|
+
// metric-claims.txt / local-pre-commit 等 5 条有包源起步模板的 owned 件永不参与感知。
|
|
131
|
+
export function srcTemplatePath(pkgRoot, rel) {
|
|
132
|
+
const tplRel = rel.startsWith('.agents/') ? `_agents/${rel.slice('.agents/'.length)}` : rel;
|
|
133
|
+
const p = path.join(pkgRoot, 'templates', tplRel);
|
|
134
|
+
return fs.existsSync(p) ? p : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 三态判定纯函数(sync 出账与 doctor 回显共用——防两处字面量漂移,N3 教训):
|
|
138
|
+
// 'stale-drift' 源已演进且盘面未跟随 → 唯一出账形态(advisory)
|
|
139
|
+
// 'custom-synced' 盘面 ≠ 源但源 == 上次锚 → 装户定制跟源,owned 语义正常态,永不告警
|
|
140
|
+
// 'synced' 盘面 == 当前源 → 静默(手工拉取后天然落此态)
|
|
141
|
+
// 'no-anchor' 台账缺 srcSha256(旧装户)→ 静默跳过,下次 sync 写锚后生效,不追溯
|
|
142
|
+
export function templateDriftOf({ disk, srcRecord, srcCur }) {
|
|
143
|
+
if (typeof srcRecord !== 'string' || !srcRecord) return 'no-anchor';
|
|
144
|
+
if (disk === srcCur) return 'synced';
|
|
145
|
+
if (srcCur !== srcRecord) return 'stale-drift';
|
|
146
|
+
return 'custom-synced';
|
|
147
|
+
}
|
|
148
|
+
|
|
115
149
|
export function settingsJson(stackKey) {
|
|
116
150
|
const allow = [...new Set([
|
|
117
151
|
'node', 'git status', 'git diff', 'git add', 'git commit', 'git log', 'git push',
|
package/src/sync.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import path from 'node:path';
|
|
|
12
12
|
import os from 'node:os';
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
import { renderTree, sha256, scriptTrusted } from './render.mjs';
|
|
15
|
-
import { HOSTS, pickStackVars, isOwned } from './profiles.mjs';
|
|
15
|
+
import { HOSTS, pickStackVars, isOwned, srcTemplatePath, templateDriftOf, TEMPLATE_DRIFT_EXCLUDE } from './profiles.mjs';
|
|
16
16
|
import { doctor } from './doctor.mjs';
|
|
17
17
|
|
|
18
18
|
function fail(msg) {
|
|
@@ -166,6 +166,28 @@ export function sync(args, pkgRoot) {
|
|
|
166
166
|
}
|
|
167
167
|
if (ownedRefreshed) console.log(` owned 台账哈希按盘面刷新 ${ownedRefreshed} 份(owned 归项目所有,仅记账不约束)`);
|
|
168
168
|
|
|
169
|
+
// 模板下发感知(2026-10-06 template-downstream,advisory 恒不 hard-block):owned 模板无下发通道,
|
|
170
|
+
// 源仓演进静默陈旧(S18/S20 装户事故根因)——三方 sha 判定见 profiles.templateDriftOf 单源。
|
|
171
|
+
// 锚「见过即刷新」:本次 sync 见到的包源 sha 记入 srcSha256(含旧账无锚首跑写锚——写锚不出账,
|
|
172
|
+
// 不追溯);唯一出账形态 = 源已演进且盘面未跟随。GEN_TARGETS 生成器目标不期望被跟随,排除。
|
|
173
|
+
const driftList = [];
|
|
174
|
+
if (Array.isArray(kit.owned)) {
|
|
175
|
+
kit.owned = kit.owned.map((f) => {
|
|
176
|
+
if (TEMPLATE_DRIFT_EXCLUDE.has(f.rel)) return f;
|
|
177
|
+
const src = srcTemplatePath(pkgRoot, f.rel);
|
|
178
|
+
if (!src) return f; // 无包源模板对应(项目配置类)不参与
|
|
179
|
+
const srcCur = ownedSha(src);
|
|
180
|
+
const diskPath = path.join(target, f.rel);
|
|
181
|
+
const disk = fs.existsSync(diskPath) ? ownedSha(diskPath) : null;
|
|
182
|
+
if (templateDriftOf({ disk, srcRecord: f.srcSha256, srcCur }) === 'stale-drift') driftList.push(f.rel);
|
|
183
|
+
return srcCur === f.srcSha256 ? f : { ...f, srcSha256: srcCur };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (driftList.length) {
|
|
187
|
+
console.log(` ⚠️ 模板感知(advisory):源仓 ${driftList.length} 份模板自上次同步后有演进且盘面未跟随:${driftList.join('、')}`);
|
|
188
|
+
console.log(' owned 归项目所有,不自动覆盖;如需跟随:从包源 templates/ 拷贝对应文件;有意定制可忽略(下次 sync 起静默)');
|
|
189
|
+
}
|
|
190
|
+
|
|
169
191
|
// 台账重写:版本对齐当前包;removed 出册;skipped 保持包侧基线持续报告
|
|
170
192
|
kit.managed = managedNew.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
171
193
|
kit.version = pkg.version;
|
package/src/sync.test.mjs
CHANGED
|
@@ -296,5 +296,75 @@ else if (cmd === 'add-gate') addGate(rest, root);
|
|
|
296
296
|
check('S14③ 盘上无 → 新增安装(既有行为不回归)', (r3.stdout + r3.stderr).includes('新增安装') && R(path.join(t3, '.agents/commands/new.txt')) === 'new in v2\n');
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// ============ 场景 15:模板下发感知三态(2026-10-06 template-downstream)============
|
|
300
|
+
// fixture 包源 templates/AGENTS.md = 'owned skeleton v2\n';盘面定制 'owned CUSTOM\n'(≠ 包源)
|
|
301
|
+
{
|
|
302
|
+
const sha = (s) => sha256(Buffer.from(s, 'utf8'));
|
|
303
|
+
const SRC_TPL = 'owned skeleton v2\n';
|
|
304
|
+
const kitOf = (t) => JSON.parse(R(path.join(t, '.agents', 'kit.json')));
|
|
305
|
+
const mkStale = (fx, t, owned) => W(path.join(t, '.agents', 'kit.json'), `${JSON.stringify({
|
|
306
|
+
kit: 'agentic-flow-kit', version: '1.0.0', createdAt: '2026-09-23T00:00:00Z',
|
|
307
|
+
options: { hosts: ['zcode'], stack: 'none', boardPort: '777' },
|
|
308
|
+
managed: [{ rel: '.agents/scripts/keep.txt', sha256: sha('keep v2\n') }], owned,
|
|
309
|
+
}, null, 2)}\n`);
|
|
310
|
+
|
|
311
|
+
// ① 源演进未拉取 → advisory 出账(唯一出账形态)+ 锚刷新 + 再跑静默(单周期出账)
|
|
312
|
+
const fx1 = mkFixture(), t1 = mkTarget(fx1);
|
|
313
|
+
W(path.join(t1, 'AGENTS.md'), 'owned CUSTOM\n');
|
|
314
|
+
mkStale(fx1, t1, [{ rel: 'AGENTS.md', sha256: sha('owned CUSTOM\n'), srcSha256: sha('owned skeleton v1\n') }]);
|
|
315
|
+
let r1 = runCmd('sync', fx1, t1);
|
|
316
|
+
const out1 = r1.stdout + r1.stderr;
|
|
317
|
+
// 断言锚定出账特征串「有演进且盘面未跟随」——doctor §6.9 的 PASS 回显也含「模板感知」四字,不可作判据
|
|
318
|
+
check('S15① 源演进未拉取 → 出账「模板感知」含文件名', out1.includes('有演进且盘面未跟随') && out1.includes('AGENTS.md'), out1.slice(0, 300));
|
|
319
|
+
check('S15① 锚刷新为当前包源 sha', kitOf(t1).owned[0].srcSha256 === sha(SRC_TPL));
|
|
320
|
+
r1 = runCmd('sync', fx1, t1);
|
|
321
|
+
check('S15① 锚已刷 → 再跑静默(单周期出账)', !(r1.stdout + r1.stderr).includes('有演进且盘面未跟随'));
|
|
322
|
+
|
|
323
|
+
// ② 定制跟源(srcSha256 == 当前包源)→ custom-synced 静默
|
|
324
|
+
const fx2 = mkFixture(), t2 = mkTarget(fx2);
|
|
325
|
+
W(path.join(t2, 'AGENTS.md'), 'owned CUSTOM\n');
|
|
326
|
+
mkStale(fx2, t2, [{ rel: 'AGENTS.md', sha256: sha('owned CUSTOM\n'), srcSha256: sha(SRC_TPL) }]);
|
|
327
|
+
const r2 = runCmd('sync', fx2, t2);
|
|
328
|
+
check('S15② 定制跟源 → 静默(owned 语义正常态永不告警)', !(r2.stdout + r2.stderr).includes('有演进且盘面未跟随'));
|
|
329
|
+
|
|
330
|
+
// ③ 旧 schema 无锚(mkTarget 原生)→ 静默写锚不追溯
|
|
331
|
+
const fx3b = mkFixture(), t3b = mkTarget(fx3b);
|
|
332
|
+
const r3b = runCmd('sync', fx3b, t3b);
|
|
333
|
+
check('S15③ 旧 schema 无锚 → 静默(写锚不出账,不追溯)', !(r3b.stdout + r3b.stderr).includes('有演进且盘面未跟随'));
|
|
334
|
+
check('S15③ 无锚首跑写入锚(下次 sync 起生效)', kitOf(t3b).owned.find((f) => f.rel === 'AGENTS.md').srcSha256 === sha(SRC_TPL));
|
|
335
|
+
|
|
336
|
+
// ④ 手工拉取(盘面 == 包源)→ synced 静默(可消退出账的路径)
|
|
337
|
+
const fx4 = mkFixture(), t4 = mkTarget(fx4);
|
|
338
|
+
mkStale(fx4, t4, [{ rel: 'AGENTS.md', sha256: sha(SRC_TPL), srcSha256: sha('owned skeleton v1\n') }]);
|
|
339
|
+
const r4 = runCmd('sync', fx4, t4);
|
|
340
|
+
check('S15④ 盘面已跟随(手工拉取)→ 静默', !(r4.stdout + r4.stderr).includes('有演进且盘面未跟随'));
|
|
341
|
+
|
|
342
|
+
// ⑤ doctor 只读回显:同判据出 stale 清单、且运行后 kit.json 字节不变;旧账无锚 skipped
|
|
343
|
+
// (no-anchor 须用未跑过 sync 的独立 fixture——sync 会写锚)
|
|
344
|
+
const { checkTemplateDrift } = await import(pathToFileURL(path.join(SRC_ROOT, 'doctor.mjs')).href);
|
|
345
|
+
const fx5 = mkFixture(), t5 = mkTarget(fx5);
|
|
346
|
+
W(path.join(t5, 'AGENTS.md'), 'owned CUSTOM\n');
|
|
347
|
+
mkStale(fx5, t5, [{ rel: 'AGENTS.md', sha256: sha('owned CUSTOM\n'), srcSha256: sha('owned skeleton v1\n') }]);
|
|
348
|
+
const before = R(path.join(t5, '.agents', 'kit.json'));
|
|
349
|
+
const resStale = checkTemplateDrift(t5, fx5);
|
|
350
|
+
check('S15⑤ doctor 判据同源:stale 出清单', resStale.skipped === false && resStale.stale.includes('AGENTS.md'), JSON.stringify(resStale));
|
|
351
|
+
check('S15⑤ doctor 只读:kit.json 字节不变', R(path.join(t5, '.agents', 'kit.json')) === before);
|
|
352
|
+
const fx6 = mkFixture(), t6 = mkTarget(fx6);
|
|
353
|
+
const resNoAnchor = checkTemplateDrift(t6, fx6);
|
|
354
|
+
check('S15⑤ doctor 旧账无锚 → skipped 静默', resNoAnchor.skipped === true, JSON.stringify(resNoAnchor));
|
|
355
|
+
|
|
356
|
+
// ⑥ 点目录前缀翻译(复核 P2-1):owned rel `.agents/x` ↔ 包源 `templates/_agents/x`——
|
|
357
|
+
// 有包源起步模板的 .agents/ owned 件同样参与感知(rule-budgets.txt 等 5 类)
|
|
358
|
+
const fx7 = mkFixture(), t7 = mkTarget(fx7);
|
|
359
|
+
W(path.join(fx7, 'templates/_agents/rule-budgets.txt'), 'budgets v2\n');
|
|
360
|
+
W(path.join(t7, '.agents/rule-budgets.txt'), 'budgets CUSTOM\n');
|
|
361
|
+
mkStale(fx7, t7, [
|
|
362
|
+
{ rel: 'AGENTS.md', sha256: sha('owned skeleton v2\n'), srcSha256: sha(SRC_TPL) },
|
|
363
|
+
{ rel: '.agents/rule-budgets.txt', sha256: sha('budgets CUSTOM\n'), srcSha256: sha('budgets v1\n') },
|
|
364
|
+
]);
|
|
365
|
+
const r7 = runCmd('sync', fx7, t7);
|
|
366
|
+
check('S15⑥ .agents/ 前缀翻译 → _agents 模板参与感知出账', (r7.stdout + r7.stderr).includes('有演进且盘面未跟随') && (r7.stdout + r7.stderr).includes('.agents/rule-budgets.txt'));
|
|
367
|
+
}
|
|
368
|
+
|
|
299
369
|
console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
|
|
300
370
|
process.exit(failCount ? 1 : 0);
|
|
@@ -111,7 +111,7 @@ import { createRequire } from 'node:module';
|
|
|
111
111
|
import { fileURLToPath } from 'node:url';
|
|
112
112
|
import { loadEnums } from './workflow-enums.mjs';
|
|
113
113
|
import { runCheck16 } from './check-metric-claims.mjs';
|
|
114
|
-
import { auditEnabled, loadKitPolicy } from './policy.mjs';
|
|
114
|
+
import { auditEnabled, loadKitPolicy, hasFreshVerifyLine } from './policy.mjs';
|
|
115
115
|
import { laneOfEntry } from './stage-gates.mjs';
|
|
116
116
|
// MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
|
|
117
117
|
// 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
|
|
@@ -629,9 +629,22 @@ function warnEvidenceExempt(type, base) {
|
|
|
629
629
|
}
|
|
630
630
|
|
|
631
631
|
// 证据真相校验:检查证据字符串是否包含合法的 commit SHA,且该 commit 触及了 plan 声明的文件
|
|
632
|
-
|
|
632
|
+
// 测试绿声明对账(2026-10-07 verify-evidence):ctx = { verifyRelevant, hasFresh } 时,无 SHA 且命中
|
|
633
|
+
// 关键词的证据走凭证对账——有 24h 绿行 → verify 型豁免出账;无 → verify-missing(聚合段出 warning,
|
|
634
|
+
// 灰度第一档非 blocker)。ctx 由调用方按「intent 加入 git ≥ verifySince(v5 锚)」组装;锚不可判定
|
|
635
|
+
// → ctx null → 维持 text 豁免(无判定依据不产出告警)。关键词表最小集起步(check2-datetime 先例:
|
|
636
|
+
// 只豁免实证形态,变体实证再扩)。
|
|
637
|
+
const VERIFY_GREEN_RE = /测试通过|测试全绿|测试绿|全绿/;
|
|
638
|
+
function verifyEvidenceTruth(evidenceStr, planBase, root, ctx) {
|
|
633
639
|
const shaMatch = evidenceStr.match(/\b([a-f0-9]{7,40})\b/i);
|
|
634
640
|
if (!shaMatch) {
|
|
641
|
+
if (ctx && ctx.verifyRelevant && VERIFY_GREEN_RE.test(evidenceStr)) {
|
|
642
|
+
if (ctx.hasFresh) {
|
|
643
|
+
warnEvidenceExempt('verify', planBase);
|
|
644
|
+
return { ok: true, type: 'verify' };
|
|
645
|
+
}
|
|
646
|
+
return { ok: true, type: 'verify-missing' };
|
|
647
|
+
}
|
|
635
648
|
warnEvidenceExempt('text', planBase);
|
|
636
649
|
return { ok: true, type: 'text' };
|
|
637
650
|
}
|
|
@@ -711,6 +724,15 @@ function verifyEvidenceTruth(evidenceStr, planBase, root) {
|
|
|
711
724
|
{
|
|
712
725
|
const accCutoff = '2026-09-12';
|
|
713
726
|
let legacyUnaccounted = 0;
|
|
727
|
+
// 测试绿凭证状态(2026-10-07 verify-evidence):台账读一次循环外缓存;policy 缺 verifySince
|
|
728
|
+
//(v1-v4)→ ctxBase null → 对账整体跳过(向后兼容)
|
|
729
|
+
const verifySinceOf = kitPolicy.verifySince;
|
|
730
|
+
const verifyFresh = (() => {
|
|
731
|
+
try {
|
|
732
|
+
const vp = path.join(ROOT, '.agents', 'verifications.jsonl');
|
|
733
|
+
return fs.existsSync(vp) ? hasFreshVerifyLine(fs.readFileSync(vp, 'utf8').split(/\r?\n/)) : false;
|
|
734
|
+
} catch { return false; }
|
|
735
|
+
})();
|
|
714
736
|
for (const intent of docFiles('intents')) {
|
|
715
737
|
if (fmGet(intent, '状态') !== 'done') continue;
|
|
716
738
|
const base = path.basename(intent);
|
|
@@ -761,9 +783,17 @@ function verifyEvidenceTruth(evidenceStr, planBase, root) {
|
|
|
761
783
|
//(豁免通道不得独立成立,防「只引 docs 提交洗白代码改动」)。
|
|
762
784
|
if (evidenceItems.length) {
|
|
763
785
|
const planBase = base.replace(/.md$/, '');
|
|
764
|
-
|
|
786
|
+
// verify 对账 ctx:intent 首次加入 git ≥ verifySince(v5 锚)才参与;不可判定 → null 走 text 豁免
|
|
787
|
+
const iadd = addedDateOf(intent);
|
|
788
|
+
const vctx = typeof verifySinceOf === 'string' && iadd !== '' && iadd >= verifySinceOf
|
|
789
|
+
? { verifyRelevant: true, hasFresh: verifyFresh } : null;
|
|
790
|
+
const results = evidenceItems.map((ev) => ({ t: verifyEvidenceTruth(ev, planBase, ROOT, vctx) }));
|
|
765
791
|
const hasImplEvidence = results.some((r) => r.t.ok && (r.t.type === 'sha' || r.t.type === 'process'));
|
|
766
792
|
for (const r of results) {
|
|
793
|
+
if (r.t.ok && r.t.type === 'verify-missing') {
|
|
794
|
+
warnings.push(`- [WARN 测试绿缺凭证] ${base} 验收证据声明「测试绿」但 24h 内无 verify 凭证(verifications.jsonl)——跑 node .agents/scripts/verify.mjs 后重跑(advisory,灰度第一档)`);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
767
797
|
if (r.t.ok) continue;
|
|
768
798
|
if (r.t.type === 'irrelevant' && r.t.recordCommit && hasImplEvidence) {
|
|
769
799
|
warnEvidenceExempt('record', planBase);
|
|
@@ -2662,5 +2662,46 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2662
2662
|
rmfix(T2);
|
|
2663
2663
|
}
|
|
2664
2664
|
|
|
2665
|
+
// ---- 场景:检查 8 测试绿声明对账(2026-10-07 verify-evidence)——v5 锚后无 SHA「测试绿」声明须有凭证 ----
|
|
2666
|
+
{
|
|
2667
|
+
// 配对构造(检查 15 须过):approved 态全文算指纹 → done 态落盘 + 台账 done 行(prev=approved,
|
|
2668
|
+
// ts=今天 ≥ bindingTs)——与 bindingSha256 复原口径对称
|
|
2669
|
+
const mkVe = (withLedger) => {
|
|
2670
|
+
const T = mkfix();
|
|
2671
|
+
gitInit(T);
|
|
2672
|
+
w(T, '.agents/kit.json', JSON.stringify({ policyVersion: 5 }));
|
|
2673
|
+
if (withLedger) {
|
|
2674
|
+
w(T, '.agents/verifications.jsonl', `${JSON.stringify({ ts: new Date().toISOString(), exitCode: 0, suite: 'npm test' })}\n`);
|
|
2675
|
+
}
|
|
2676
|
+
const iBody = '# INTENT — ve\n\n## 验收标准(可测试)\n- [x] 用例通过(证据:测试全绿)\n';
|
|
2677
|
+
const pBody = '# PLAN — ve\n';
|
|
2678
|
+
const fpI = computeFingerprint(`---\n状态: approved\n级别: L1\n日期: 2026-10-07\n---\n${iBody}`);
|
|
2679
|
+
const fpP = computeFingerprint(`---\n状态: approved\n级别: L1\n---\n${pBody}`);
|
|
2680
|
+
w(T, 'workflow/intents/2026-10-07-ve.md', `---\n状态: done\n级别: L1\n日期: 2026-10-07\n确认指纹: ${fpI.slice(0, 16)}\n---\n${iBody}`);
|
|
2681
|
+
w(T, 'workflow/plans/2026-10-07-ve.md', `---\n状态: done\n级别: L1\n确认指纹: ${fpP.slice(0, 16)}\n---\n${pBody}`);
|
|
2682
|
+
const now = new Date().toISOString();
|
|
2683
|
+
writeLedger(T, [
|
|
2684
|
+
{ ts: now, doc: 'workflow/intents/2026-10-07-ve.md', stage: 'done', fingerprint: fpI, prev: 'approved' },
|
|
2685
|
+
{ ts: now, doc: 'workflow/plans/2026-10-07-ve.md', stage: 'done', fingerprint: fpP, prev: 'approved' },
|
|
2686
|
+
]);
|
|
2687
|
+
gitCommitAll(T, 'feat: ve fixture');
|
|
2688
|
+
return T;
|
|
2689
|
+
};
|
|
2690
|
+
// ① 无凭证 → warning(advisory,不拦 exit)
|
|
2691
|
+
const T1 = mkVe(false);
|
|
2692
|
+
let r = run(T1);
|
|
2693
|
+
check('检查 8 凭证对账:锚后「测试绿」声明无凭证 → warning 且不 hard',
|
|
2694
|
+
r.status === 0 && outOf(r).includes('测试绿缺凭证') && !outOf(r).includes('证据伪造') && !outOf(r).includes('证据无关'),
|
|
2695
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
2696
|
+
rmfix(T1);
|
|
2697
|
+
// ② 有近期绿行 → verify 型豁免出账、零 warning
|
|
2698
|
+
const T2 = mkVe(true);
|
|
2699
|
+
r = run(T2);
|
|
2700
|
+
check('检查 8 凭证对账:24h 内有绿行 → verify 豁免出账且无 warning',
|
|
2701
|
+
r.status === 0 && outOf(r).includes('证据豁免 verify') && !outOf(r).includes('测试绿缺凭证'),
|
|
2702
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
2703
|
+
rmfix(T2);
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2665
2706
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
2666
2707
|
process.exit(fail ? 1 : 0);
|
|
@@ -40,7 +40,19 @@ import readline from 'node:readline';
|
|
|
40
40
|
import { createHash, randomBytes } from 'node:crypto';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
42
|
import { confirmGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
43
|
-
import { loadKitPolicy } from './policy.mjs';
|
|
43
|
+
import { loadKitPolicy, hasFreshVerifyLine } from './policy.mjs';
|
|
44
|
+
|
|
45
|
+
// hasFreshVerify(root):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
|
|
46
|
+
// policy.hasFreshVerifyLine)。只读,台账缺失/不可读一律 false(=无凭证,advisory 层 fail-open)。
|
|
47
|
+
function hasFreshVerify(root) {
|
|
48
|
+
try {
|
|
49
|
+
const p = path.join(root, '.agents', 'verifications.jsonl');
|
|
50
|
+
if (!fs.existsSync(p)) return false;
|
|
51
|
+
return hasFreshVerifyLine(fs.readFileSync(p, 'utf8').split(/\r?\n/));
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
44
56
|
|
|
45
57
|
// computeFingerprint(text):CRLF 归一 → 剔指纹行 → sha256 hex(64 位)
|
|
46
58
|
export function computeFingerprint(text) {
|
|
@@ -304,6 +316,14 @@ if (isMain) {
|
|
|
304
316
|
continue;
|
|
305
317
|
}
|
|
306
318
|
}
|
|
319
|
+
// 测试绿凭证前置(2026-10-07 verify-evidence,advisory 不拦):→done 且文档含「验收标准」节且
|
|
320
|
+
// policy v5(verifySince)→ 24h 窗口内须有 verifications.jsonl 绿行。只读判定,任何读取异常降级
|
|
321
|
+
// 静默(fail-open——advisory 层不因台账缺失/坏行阻断关单);升 hard 走后续 policyVersion 演进。
|
|
322
|
+
if (target === 'done' && typeof loadKitPolicy(root).verifySince === 'string'
|
|
323
|
+
&& /^##\s*[^#]*验收标准/m.test(text) && !hasFreshVerify(root)) {
|
|
324
|
+
console.error(`⚠️ ${doc} 置 done:24h 内无测试绿凭证(verifications.jsonl)——关单勾验的「测试绿」声明缺机器事实`);
|
|
325
|
+
console.error(' 跑 node .agents/scripts/verify.mjs(全绿自动落账)后重新关单;本次不拦截(advisory,灰度第一档)');
|
|
326
|
+
}
|
|
307
327
|
const fp = computeFingerprint(text);
|
|
308
328
|
// AI 自治放行分支(2026-09-30 ai-autonomy-trust):
|
|
309
329
|
// ① 仅 L0/L1(读 frontmatter「级别」,缺级别按不可自治处理——fail-closed);
|
|
@@ -700,5 +700,41 @@ const writeTrust = (root, cfg) => {
|
|
|
700
700
|
fs.rmSync(root, { recursive: true, force: true });
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
+
// ---- S38 测试绿凭证前置(2026-10-07 verify-evidence):v5 + 验收节 + 无凭证 → ⚠️ advisory 且仍落账 ----
|
|
704
|
+
{
|
|
705
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-verify1-'));
|
|
706
|
+
const docP = path.join(root, 'workflow', 'intents');
|
|
707
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
708
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
709
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: 5 }), 'utf8');
|
|
710
|
+
fs.writeFileSync(path.join(docP, '2026-10-07-v.md'),
|
|
711
|
+
'---\n状态: approved\n级别: L1\n确认指纹: aaaa1111aaaa1111\n---\n# I\n\n## 验收标准(可测试)\n- [x] 用例通过(证据:测试全绿)\n', 'utf8');
|
|
712
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-07-v.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
713
|
+
const after = fs.readFileSync(path.join(docP, '2026-10-07-v.md'), 'utf8');
|
|
714
|
+
check('S38 凭证前置:v5 + 验收节 + 无凭证 → ⚠️ 出账且 done 仍落态(advisory 不拦)',
|
|
715
|
+
r.status === 0 && /无测试绿凭证/.test(r.stderr) && /verify\.mjs/.test(r.stderr) && after.includes('状态: done'),
|
|
716
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
|
|
717
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ---- S39 测试绿凭证前置:近期绿凭证在场 → 静默(无 ⚠️)----
|
|
721
|
+
{
|
|
722
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-verify2-'));
|
|
723
|
+
const docP = path.join(root, 'workflow', 'intents');
|
|
724
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
725
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
726
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: 5 }), 'utf8');
|
|
727
|
+
fs.writeFileSync(path.join(root, '.agents', 'verifications.jsonl'),
|
|
728
|
+
`${JSON.stringify({ ts: new Date().toISOString(), exitCode: 0, suite: 'npm test', passed: 1, failed: 0 })}\n`, 'utf8');
|
|
729
|
+
fs.writeFileSync(path.join(docP, '2026-10-07-w.md'),
|
|
730
|
+
'---\n状态: approved\n级别: L1\n确认指纹: bbbb2222bbbb2222\n---\n# I\n\n## 验收标准(可测试)\n- [x] 用例通过(证据:测试全绿)\n', 'utf8');
|
|
731
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-07-w.md', '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
732
|
+
const after = fs.readFileSync(path.join(docP, '2026-10-07-w.md'), 'utf8');
|
|
733
|
+
check('S39 凭证前置:24h 内有绿行 → 静默(无 ⚠️)且 done 落态',
|
|
734
|
+
r.status === 0 && !/无测试绿凭证/.test(r.stderr) && after.includes('状态: done'),
|
|
735
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 200) }));
|
|
736
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
737
|
+
}
|
|
738
|
+
|
|
703
739
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
704
740
|
process.exit(fail ? 1 : 0);
|
|
@@ -65,6 +65,7 @@ const PAIRS = [
|
|
|
65
65
|
{ doctor: '6.6', cl: null, note: 'owned 漂移——装户体检独有' },
|
|
66
66
|
{ doctor: '6.7', cl: null, note: '跨宿主薄适配——装户体检独有' },
|
|
67
67
|
{ doctor: '6.8', cl: null, note: 'workflows 编排脚本 lint——装户体检独有' },
|
|
68
|
+
{ doctor: '6.9', cl: null, note: '模板下发感知(2026-10-06 template-downstream)——装户体检独有(只读回显 sync 出账同源判据)' },
|
|
68
69
|
{ doctor: '8', cl: null, note: '看板端口——信息级独有' },
|
|
69
70
|
// 经 §7 覆盖:doctor §7 整体运行 check-loop(#2/#11 已直接配对,不重复登记)
|
|
70
71
|
{ doctor: '7', cl: '1', note: '经 §7 整体运行覆盖' },
|
|
@@ -90,7 +90,9 @@ function runNode(ctx, run, script, args) {
|
|
|
90
90
|
const scriptPath = path.join(ctx.bin, script);
|
|
91
91
|
const cmd = `node .agents/scripts/${script} ${args.join(' ')}`;
|
|
92
92
|
const t0 = Date.now();
|
|
93
|
-
|
|
93
|
+
// PIPELINE_RUN_ID 注入(2026-10-07 verify-evidence 复核 P2-1):verify.mjs 绿行据此绑定 runId
|
|
94
|
+
//(对质链与 run 事件流同源,docsCommit 格式已含同一 runId)——spec「pipeline-run 路径自动绑定」落点
|
|
95
|
+
const p = spawnSync(process.execPath, [scriptPath, ...args], { cwd: ctx.root, encoding: 'utf8', windowsHide: true, env: { ...process.env, PIPELINE_RUN_ID: run.runId } });
|
|
94
96
|
const ms = Date.now() - t0;
|
|
95
97
|
const exit = p.status ?? 1;
|
|
96
98
|
emit(run, { type: 'gate', cmd, exit, ms });
|
|
@@ -52,6 +52,23 @@ export const POLICIES = {
|
|
|
52
52
|
bindingTs: '2026-09-28',
|
|
53
53
|
delegationSince: '2026-10-06', // 检查 18 委派台账对账生效日(2026-10-06 loop-audit-remediation);v1-v3 无此键 → 原全量对账行为
|
|
54
54
|
},
|
|
55
|
+
// 版本 5 = 版本 2 的全部日期 + verifySince(测试绿机器凭证生效日,2026-10-07 verify-evidence)。
|
|
56
|
+
// 基准:Anthropic AI-Native SDLC「make test before done」。三处消费:verify.mjs 绿行落账(无开关,
|
|
57
|
+
// 纯事实记录)+ confirm-doc 置 done 门前置 advisory(24h 窗口)+ 检查 8 无 SHA 测试绿声明对账
|
|
58
|
+
// (intent 首次加入 git ≥ verifySince 才判)。缺键(v1-v4)→ 两处判定整体跳过,存量零新增告警。
|
|
59
|
+
// 注意本版本以 v2 为基线(本仓自用版本),不含 v3/v4 的装户后移锚——装户升 v5 视同同时接受
|
|
60
|
+
// v2 基线的早期锚(装户已选 v3/v4 的,升 v5 前 doctor 会提示锚变化,逐键核对后再升)。
|
|
61
|
+
5: {
|
|
62
|
+
moduleSince: '2026-09-22',
|
|
63
|
+
check14Since: '2026-09-26',
|
|
64
|
+
confirmDocsEffective: '2026-09-27',
|
|
65
|
+
confirmIncidentsEffective: '2026-09-28',
|
|
66
|
+
bindingTs: '2026-09-28',
|
|
67
|
+
check17UnclosedAfter: '0.8.0',
|
|
68
|
+
stageGateSince: '2026-09-29',
|
|
69
|
+
riskLevelSince: '2026-10-01',
|
|
70
|
+
verifySince: '2026-10-07', // 测试绿机器凭证生效日(2026-10-07 verify-evidence);v1-v4 无此键 → 前置与对账整体跳过
|
|
71
|
+
},
|
|
55
72
|
};
|
|
56
73
|
|
|
57
74
|
export function loadKitPolicy(root) {
|
|
@@ -72,3 +89,22 @@ export function loadKitPolicy(root) {
|
|
|
72
89
|
export function auditEnabled(policy) {
|
|
73
90
|
return policy.audit !== false;
|
|
74
91
|
}
|
|
92
|
+
|
|
93
|
+
// ---- 测试绿凭证判定(2026-10-07 verify-evidence)----
|
|
94
|
+
// 单源(confirm-doc done 前置与 check-loop 检查 8 对账共用——防两处窗口字面量漂移,N3 教训):
|
|
95
|
+
// hasFreshVerifyLine(lines, now):verifications.jsonl 的行数组里是否存在「ts 距 now ≤ 24h 且
|
|
96
|
+
// exitCode === 0」的绿行。坏行(非 JSON / 缺 ts)跳过不抛;窗口 24h = 覆盖「跑 verify → 跨会话/
|
|
97
|
+
// 隔夜 → 次日关单」节奏(spec 2026-10-07-verify-evidence 论证)。
|
|
98
|
+
export function hasFreshVerifyLine(lines, now = Date.now()) {
|
|
99
|
+
for (const line of lines || []) {
|
|
100
|
+
if (!line || !line.trim()) continue;
|
|
101
|
+
try {
|
|
102
|
+
const e = JSON.parse(line);
|
|
103
|
+
if (e && e.exitCode === 0 && typeof e.ts === 'string') {
|
|
104
|
+
const t = Date.parse(e.ts);
|
|
105
|
+
if (Number.isFinite(t) && now - t <= 24 * 60 * 60 * 1000) return true;
|
|
106
|
+
}
|
|
107
|
+
} catch { /* 坏行容忍跳过(confirmations.jsonl 同口径) */ }
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// verify.mjs — 关单固定编排(2026-09-24 intent closeout-verify-script;2026-09-26 check-loop-node 第 2 步直连 node)
|
|
3
3
|
// 用途:关单前一键过门——按写死顺序执行 npm test → check-loop,逐项输出 pass/fail。
|
|
4
|
-
//
|
|
5
|
-
// 任一步非零即 fail-fast 退出 1
|
|
4
|
+
// 语义:只编排不裁决——两条命令都是既有门禁原样跑,本脚本不读配置、不判窗口、不改退出码;
|
|
5
|
+
// 任一步非零即 fail-fast 退出 1,失败原因原样透传不吞。
|
|
6
6
|
// check-loop 为 node 实现(经 check-loop.mjs 直跑——node 即本脚本运行时必然可用,
|
|
7
7
|
// 旧「缺 sh 环境 fail-closed」分支随 sh 依赖消失退役)。
|
|
8
|
+
// 凭证落账(2026-10-07 verify-evidence):两步全绿后向 <root>/.agents/verifications.jsonl append
|
|
9
|
+
// 一行机器事实 {ts, exitCode:0, suite, passed?, failed?, runId?}——append-only 事实记录,不属裁决
|
|
10
|
+
// (窗口判定/门禁在消费方 confirm-doc 与 check-loop 检查 8);消费语义 = 「按门跑了 verify 且全绿」,
|
|
11
|
+
// 裸跑 npm test 不落账(对齐引导)。npm test 步骤输出从 inherit 改 pipe 捕获后回放(延迟显示,
|
|
12
|
+
// 关单场景可接受)——仅为 best-effort 抓「合计: PASS n / FAIL n」计数行,抓不到则省略计数键。
|
|
8
13
|
// 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具,不触真实 npm test 与 workflow)
|
|
9
14
|
// 用法:node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"](--test-cmd 仅供测试注入)
|
|
15
|
+
import fs from 'node:fs';
|
|
10
16
|
import { spawnSync } from 'node:child_process';
|
|
11
17
|
import path from 'node:path';
|
|
12
18
|
import { fileURLToPath } from 'node:url';
|
|
@@ -15,9 +21,12 @@ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
15
21
|
const args = process.argv.slice(2);
|
|
16
22
|
const testCmd = args.includes('--test-cmd') ? args[args.indexOf('--test-cmd') + 1] : 'npm test';
|
|
17
23
|
const checkLoop = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
24
|
+
const ROOT = process.env.CHECK_LOOP_ROOT || path.resolve(SCRIPT_DIR, '..', '..');
|
|
18
25
|
|
|
19
26
|
console.log(`[verify] ▶ 1/2 ${testCmd}`);
|
|
20
|
-
const t = spawnSync(testCmd, { shell: true,
|
|
27
|
+
const t = spawnSync(testCmd, { shell: true, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
|
|
28
|
+
if (t.stdout) process.stdout.write(t.stdout);
|
|
29
|
+
if (t.stderr) process.stderr.write(t.stderr);
|
|
21
30
|
if (t.status !== 0) {
|
|
22
31
|
console.error(`[verify] ❌ 1/2 ${testCmd} 未通过(exit ${t.status ?? t.error?.code})——修复后重跑,非绿不关单`);
|
|
23
32
|
process.exit(1);
|
|
@@ -31,4 +40,17 @@ if (c.status !== 0) {
|
|
|
31
40
|
process.exit(1);
|
|
32
41
|
}
|
|
33
42
|
console.log('[verify] ✅ 2/2 闭环校验通过');
|
|
43
|
+
|
|
44
|
+
// 凭证落账:两步全绿才落(check-loop 红不产生凭证——凭证语义 = verify 全绿);IO 失败出账不阻断
|
|
45
|
+
try {
|
|
46
|
+
const m = [...String(t.stdout || '').matchAll(/合计: PASS (\d+) \/ FAIL (\d+)/g)].pop();
|
|
47
|
+
const entry = { ts: new Date().toISOString(), exitCode: 0, suite: 'npm test' };
|
|
48
|
+
if (m) { entry.passed = Number(m[1]); entry.failed = Number(m[2]); }
|
|
49
|
+
if (process.env.PIPELINE_RUN_ID) entry.runId = process.env.PIPELINE_RUN_ID;
|
|
50
|
+
fs.mkdirSync(path.join(ROOT, '.agents'), { recursive: true });
|
|
51
|
+
fs.appendFileSync(path.join(ROOT, '.agents', 'verifications.jsonl'), `${JSON.stringify(entry)}\n`);
|
|
52
|
+
console.log(`[verify] 🧾 测试绿凭证已落账 verifications.jsonl${entry.passed !== undefined ? `(PASS ${entry.passed} / FAIL ${entry.failed})` : ''}`);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(`[verify] ⚠️ 凭证落账失败(${e.code || e.message})——不阻断,关单门前置将按无凭证告警`);
|
|
55
|
+
}
|
|
34
56
|
console.log('[verify] ✅ 全绿——可以关单(逐条勾验验收标准补证据后 intent/plan → done)');
|
|
@@ -46,10 +46,10 @@ const mkBroken = () => {
|
|
|
46
46
|
].join('\n'), 'utf8');
|
|
47
47
|
return d;
|
|
48
48
|
};
|
|
49
|
-
// 假命令(绝对路径直调,不依赖 PATH
|
|
50
|
-
const mkCmd = (code) => {
|
|
49
|
+
// 假命令(绝对路径直调,不依赖 PATH,跨平台无引号问题);out 可选输出汇总行
|
|
50
|
+
const mkCmd = (code, out = '') => {
|
|
51
51
|
const f = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'verify-cmd-')), `exit${code}.mjs`);
|
|
52
|
-
fs.writeFileSync(f, `process.exit(${code});`, 'utf8');
|
|
52
|
+
fs.writeFileSync(f, `process.stdout.write(${JSON.stringify(out)});process.exit(${code});`, 'utf8');
|
|
53
53
|
return f;
|
|
54
54
|
};
|
|
55
55
|
const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv], { encoding: 'utf8', ...opts });
|
|
@@ -81,6 +81,41 @@ const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv],
|
|
|
81
81
|
fs.rmSync(root, { recursive: true, force: true });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// ---- 场景 4:绿路径凭证落账(2026-10-07 verify-evidence)——全绿后 fixture 台账出合法绿行 ----
|
|
85
|
+
{
|
|
86
|
+
const root = mkfix();
|
|
87
|
+
const r = run(['--test-cmd', `node ${mkCmd(0, '合计: PASS 3 / FAIL 0\n')}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
88
|
+
const vp = path.join(root, '.agents', 'verifications.jsonl');
|
|
89
|
+
const line = fs.existsSync(vp) ? JSON.parse(fs.readFileSync(vp, 'utf8').trim()) : null;
|
|
90
|
+
check('场景 4:全绿 → verifications.jsonl 落绿行(exitCode 0 + 计数捕获)',
|
|
91
|
+
r.status === 0 && line !== null && line.exitCode === 0 && line.passed === 3 && line.failed === 0
|
|
92
|
+
&& typeof line.ts === 'string' && r.stdout.includes('凭证已落账'),
|
|
93
|
+
`exit=${r.status} line=${JSON.stringify(line)}`);
|
|
94
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- 场景 5:红路径不落账(check-loop 红 → fail-fast 在落账前退出)----
|
|
98
|
+
{
|
|
99
|
+
const root = mkBroken();
|
|
100
|
+
const r = run(['--test-cmd', `node ${mkCmd(0)}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
101
|
+
check('场景 5:npm test 绿但 check-loop 红 → 无凭证行(凭证语义 = verify 全绿)',
|
|
102
|
+
r.status === 1 && !fs.existsSync(path.join(root, '.agents', 'verifications.jsonl')),
|
|
103
|
+
`exit=${r.status}`);
|
|
104
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- 场景 6:输出无汇总行 → 计数键省略(exitCode 才是硬事实)----
|
|
108
|
+
{
|
|
109
|
+
const root = mkfix();
|
|
110
|
+
const r = run(['--test-cmd', `node ${mkCmd(0, 'ok\n')}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
111
|
+
const vp = path.join(root, '.agents', 'verifications.jsonl');
|
|
112
|
+
const line = fs.existsSync(vp) ? JSON.parse(fs.readFileSync(vp, 'utf8').trim()) : null;
|
|
113
|
+
check('场景 6:无汇总输出 → 绿行落盘但计数键省略',
|
|
114
|
+
r.status === 0 && line !== null && line.exitCode === 0 && !('passed' in line) && !('failed' in line),
|
|
115
|
+
`line=${JSON.stringify(line)}`);
|
|
116
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
85
120
|
if (fail) {
|
|
86
121
|
console.log('\n用法:node templates/_agents/scripts/verify.test.mjs');
|