agentic-flow-kit 1.1.9 → 1.2.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-flow-kit",
3
- "version": "1.1.9",
3
+ "version": "1.2.1",
4
4
  "description": "AI-Native 闭环工作流 + wiki 知识层脚手架——flow-kit init 一条命令为新项目装好 .agents 工作流引擎(阶段命令/角色契约/门禁钩子/kb 检索/看板/规则面预算)+ workflow 文档闭环 + wiki 知识库,装完自包含、零运行时依赖",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -75,7 +75,7 @@ node bin/flow-kit.mjs doctor # §6.5 delegations + §6.6 o
75
75
  - 输出测试报告(通过项 / 失败项)
76
76
  - 失败项已修复并复测通过
77
77
  - **关单**(日常闭环点,不依赖是否上 prod):
78
- - 关单前一键过门:`node .agents/scripts/verify.mjs`(npm test + check-loop 固定编排,任一失败非零退出,非绿不关单)
78
+ - 关单前一键过门:`node .agents/scripts/verify.mjs`(npm test + check-loop 固定编排,任一失败非零退出,非绿不关单);**装户仓**(根目录无 `package.json`,工程在子目录)默认 `npm test` 不可跑 → 脚本**显式跳过步骤 1 且不落测试绿凭证**(防假绿)并提示;此时按项目口径手工跑测试,或带 `--test-cmd "<项目测试命令>"` 指定(2026-10-07 papercut)
79
79
  - 新需求:逐条勾验入口 intent「验收标准」,每条补「证据:」(例如:粘贴 CI 运行结果、UI 截图路径、或具体的 commit SHA),intent $\rightarrow$ done;同名 spec/Quick-Plan(若有)仍 approved $\rightarrow$ done。**注意:勾选但缺证据会被 check-loop 拦截(hard-block)**
80
80
  - **approved 留痕提交**(2026-09-30 起):done 确认前确认**本次关单文档**的 approved 态已进 git 历史(越早越好——approved 后即应提交;若尚未,先补提交再 done;门为逐份校验,三件套均关单时逐份各自满足)——confirm-doc 的 done 前置门硬校验(历史未出现「状态: approved」→ 拒绝落账);done 后再提交 done 态与其余改动
81
81
  **确认落态唯一入口**(2026-09-27 起):用户在终端跑 `node .agents/scripts/confirm-doc.mjs <path>` 键入「可以」(draft→approved / approved→done;对话内明确确认后 AI 可 `--delegated "<原话>"` 代录,台账如实记 source/quote——**逐件调用 + 逐件原话**(一次一份,build.md「逐件确认」口径;多份并录被 confirm-doc 拒绝。--delegated 每次传当次实际放行的措辞,不复用同句——复用同句使台账 quote 失去分辨力))。check-loop 15 如实口径:缺记录/指纹不配对 → hard-block 拦截;伪造台账本地不可机器防,留痕供事后对质;done 内容绑定(2026-09-28 起)——**关单编辑(勾验/回填确认结果)先于 done 确认、confirm-doc 是最后一次写入**,此后修订走 superseded 或新 intent。
@@ -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
- function verifyEvidenceTruth(evidenceStr, planBase, root) {
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
- const results = evidenceItems.map((ev) => ({ t: verifyEvidenceTruth(ev, planBase, ROOT) }));
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);
@@ -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
- const p = spawnSync(process.execPath, [scriptPath, ...args], { cwd: ctx.root, encoding: 'utf8', windowsHide: true });
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,28 +1,52 @@
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,失败原因经 stdio inherit 原样透传不吞。
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」计数行,抓不到则省略计数键。
13
+ // 装户仓(根目录无 package.json,工程在子目录)下默认 npm test 恒 ENOENT(exit 4058)→ **显式跳过步骤 1**
14
+ // 且**不落测试绿凭证**(凭证语义 = 测试真跑绿,跳过即无凭证,防假绿),并提示用
15
+ // --test-cmd "<项目测试命令>" 指定;显式 --test-cmd 不受此判定影响(2026-10-07 papercut)。
8
16
  // 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具,不触真实 npm test 与 workflow)
9
- // 用法:node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"](--test-cmd 仅供测试注入)
17
+ // 用法:node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"](--test-cmd 用于测试注入,或在无根 package.json 的装户仓指定项目测试命令)
18
+ import fs from 'node:fs';
10
19
  import { spawnSync } from 'node:child_process';
11
20
  import path from 'node:path';
12
21
  import { fileURLToPath } from 'node:url';
13
22
 
14
23
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
15
24
  const args = process.argv.slice(2);
16
- const testCmd = args.includes('--test-cmd') ? args[args.indexOf('--test-cmd') + 1] : 'npm test';
25
+ const explicitTestCmd = args.includes('--test-cmd') ? args[args.indexOf('--test-cmd') + 1] : null;
26
+ const testCmd = explicitTestCmd ?? 'npm test';
17
27
  const checkLoop = path.join(SCRIPT_DIR, 'check-loop.mjs');
28
+ const ROOT = process.env.CHECK_LOOP_ROOT || path.resolve(SCRIPT_DIR, '..', '..');
18
29
 
19
- console.log(`[verify] ▶ 1/2 ${testCmd}`);
20
- const t = spawnSync(testCmd, { shell: true, stdio: 'inherit' });
21
- if (t.status !== 0) {
22
- console.error(`[verify] ❌ 1/2 ${testCmd} 未通过(exit ${t.status ?? t.error?.code})——修复后重跑,非绿不关单`);
23
- process.exit(1);
30
+ // 装户仓判定:默认命令只在**当前目录**有 package.json 时才跑(npm test 亦在该目录执行)。
31
+ // 跳过时后续不落测试绿凭证(见文末落账段)。
32
+ const hasRootPackageJson = fs.existsSync(path.join(process.cwd(), 'package.json'));
33
+ const skipTest = explicitTestCmd === null && !hasRootPackageJson;
34
+ let testStdout = '';
35
+
36
+ if (skipTest) {
37
+ console.log('[verify] ⏭ 1/2 跳过 npm test:当前目录无 package.json(装户仓)——请按项目口径手工跑测试,或用 --test-cmd "<项目测试命令>" 指定后重跑');
38
+ } else {
39
+ console.log(`[verify] ▶ 1/2 ${testCmd}`);
40
+ const t = spawnSync(testCmd, { shell: true, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
41
+ if (t.stdout) process.stdout.write(t.stdout);
42
+ if (t.stderr) process.stderr.write(t.stderr);
43
+ if (t.status !== 0) {
44
+ console.error(`[verify] ❌ 1/2 ${testCmd} 未通过(exit ${t.status ?? t.error?.code})——修复后重跑,非绿不关单`);
45
+ process.exit(1);
46
+ }
47
+ testStdout = String(t.stdout || '');
48
+ console.log('[verify] ✅ 1/2 测试通过');
24
49
  }
25
- console.log('[verify] ✅ 1/2 测试通过');
26
50
 
27
51
  console.log('[verify] ▶ 2/2 check-loop');
28
52
  const c = spawnSync(process.execPath, [checkLoop], { stdio: 'inherit' });
@@ -31,4 +55,22 @@ if (c.status !== 0) {
31
55
  process.exit(1);
32
56
  }
33
57
  console.log('[verify] ✅ 2/2 闭环校验通过');
58
+
59
+ // 凭证落账:两步全绿才落(check-loop 红不产生凭证——凭证语义 = verify 全绿);**步骤 1 被跳过同样不落**
60
+ // (测试未跑即无「测试绿」事实,落账即假绿);IO 失败出账不阻断
61
+ try {
62
+ if (skipTest) {
63
+ console.log('[verify] ⚠️ 步骤 1 已跳过——本次不落测试绿凭证(凭证语义 = 测试真跑绿)');
64
+ } else {
65
+ const m = [...testStdout.matchAll(/合计: PASS (\d+) \/ FAIL (\d+)/g)].pop();
66
+ const entry = { ts: new Date().toISOString(), exitCode: 0, suite: 'npm test' };
67
+ if (m) { entry.passed = Number(m[1]); entry.failed = Number(m[2]); }
68
+ if (process.env.PIPELINE_RUN_ID) entry.runId = process.env.PIPELINE_RUN_ID;
69
+ fs.mkdirSync(path.join(ROOT, '.agents'), { recursive: true });
70
+ fs.appendFileSync(path.join(ROOT, '.agents', 'verifications.jsonl'), `${JSON.stringify(entry)}\n`);
71
+ console.log(`[verify] 🧾 测试绿凭证已落账 verifications.jsonl${entry.passed !== undefined ? `(PASS ${entry.passed} / FAIL ${entry.failed})` : ''}`);
72
+ }
73
+ } catch (e) {
74
+ console.error(`[verify] ⚠️ 凭证落账失败(${e.code || e.message})——不阻断,关单门前置将按无凭证告警`);
75
+ }
34
76
  console.log('[verify] ✅ 全绿——可以关单(逐条勾验验收标准补证据后 intent/plan → done)');
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // verify.mjs 的 fixture 测试(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具根,不触真实 npm test 与 workflow)
3
3
  // 断言:①绿路径(假过 + 空夹具闭环)exit 0;②步骤 1 假败 exit 1 且不出全绿;
4
- // ③步骤 2 配对断裂夹具 exit 1(verify 自身标记断言——孙进程 stdio inherit 输出不进本测试管道)。
4
+ // ③步骤 2 配对断裂夹具 exit 1(verify 自身标记断言——孙进程 stdio inherit 输出不进本测试管道);
5
+ // ④⑤⑥凭证落账三态(绿落 / check-loop 红不落 / 无汇总行省略计数键);
6
+ // ⑦装户仓(cwd 无 package.json)未给 --test-cmd → 跳过步骤 1、不落凭证、仍跑步骤 2、exit 0。
5
7
  // 用法:node templates/_agents/scripts/verify.test.mjs
6
8
  import fs from 'node:fs';
7
9
  import os from 'node:os';
@@ -46,10 +48,10 @@ const mkBroken = () => {
46
48
  ].join('\n'), 'utf8');
47
49
  return d;
48
50
  };
49
- // 假命令(绝对路径直调,不依赖 PATH,跨平台无引号问题)
50
- const mkCmd = (code) => {
51
+ // 假命令(绝对路径直调,不依赖 PATH,跨平台无引号问题);out 可选输出汇总行
52
+ const mkCmd = (code, out = '') => {
51
53
  const f = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'verify-cmd-')), `exit${code}.mjs`);
52
- fs.writeFileSync(f, `process.exit(${code});`, 'utf8');
54
+ fs.writeFileSync(f, `process.stdout.write(${JSON.stringify(out)});process.exit(${code});`, 'utf8');
53
55
  return f;
54
56
  };
55
57
  const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv], { encoding: 'utf8', ...opts });
@@ -81,6 +83,55 @@ const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv],
81
83
  fs.rmSync(root, { recursive: true, force: true });
82
84
  }
83
85
 
86
+ // ---- 场景 4:绿路径凭证落账(2026-10-07 verify-evidence)——全绿后 fixture 台账出合法绿行 ----
87
+ {
88
+ const root = mkfix();
89
+ const r = run(['--test-cmd', `node ${mkCmd(0, '合计: PASS 3 / FAIL 0\n')}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
90
+ const vp = path.join(root, '.agents', 'verifications.jsonl');
91
+ const line = fs.existsSync(vp) ? JSON.parse(fs.readFileSync(vp, 'utf8').trim()) : null;
92
+ check('场景 4:全绿 → verifications.jsonl 落绿行(exitCode 0 + 计数捕获)',
93
+ r.status === 0 && line !== null && line.exitCode === 0 && line.passed === 3 && line.failed === 0
94
+ && typeof line.ts === 'string' && r.stdout.includes('凭证已落账'),
95
+ `exit=${r.status} line=${JSON.stringify(line)}`);
96
+ fs.rmSync(root, { recursive: true, force: true });
97
+ }
98
+
99
+ // ---- 场景 5:红路径不落账(check-loop 红 → fail-fast 在落账前退出)----
100
+ {
101
+ const root = mkBroken();
102
+ const r = run(['--test-cmd', `node ${mkCmd(0)}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
103
+ check('场景 5:npm test 绿但 check-loop 红 → 无凭证行(凭证语义 = verify 全绿)',
104
+ r.status === 1 && !fs.existsSync(path.join(root, '.agents', 'verifications.jsonl')),
105
+ `exit=${r.status}`);
106
+ fs.rmSync(root, { recursive: true, force: true });
107
+ }
108
+
109
+ // ---- 场景 6:输出无汇总行 → 计数键省略(exitCode 才是硬事实)----
110
+ {
111
+ const root = mkfix();
112
+ const r = run(['--test-cmd', `node ${mkCmd(0, 'ok\n')}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
113
+ const vp = path.join(root, '.agents', 'verifications.jsonl');
114
+ const line = fs.existsSync(vp) ? JSON.parse(fs.readFileSync(vp, 'utf8').trim()) : null;
115
+ check('场景 6:无汇总输出 → 绿行落盘但计数键省略',
116
+ r.status === 0 && line !== null && line.exitCode === 0 && !('passed' in line) && !('failed' in line),
117
+ `line=${JSON.stringify(line)}`);
118
+ fs.rmSync(root, { recursive: true, force: true });
119
+ }
120
+
121
+ // ---- 场景 7:装户仓(cwd 无 package.json)且未给 --test-cmd → 跳过步骤 1、不落凭证、仍跑步骤 2 → exit 0 ----
122
+ {
123
+ const root = mkfix();
124
+ const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-bare-'));
125
+ const r = run([], { cwd: bare, env: { ...process.env, CHECK_LOOP_ROOT: root } });
126
+ check('场景 7:exit 0', r.status === 0, `exit=${r.status}\n${r.stdout}\n${r.stderr}`);
127
+ check('场景 7:步骤 1 显式跳过(⏭ 1/2)、步骤 2 通过且全绿', r.stdout.includes('⏭ 1/2') && r.stdout.includes('✅ 2/2') && r.stdout.includes('全绿'), r.stdout);
128
+ check('场景 7:跳过时不落测试绿凭证(防假绿)',
129
+ !fs.existsSync(path.join(root, '.agents', 'verifications.jsonl')) && r.stdout.includes('不落测试绿凭证'),
130
+ r.stdout);
131
+ fs.rmSync(root, { recursive: true, force: true });
132
+ fs.rmSync(bare, { recursive: true, force: true });
133
+ }
134
+
84
135
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
85
136
  if (fail) {
86
137
  console.log('\n用法:node templates/_agents/scripts/verify.test.mjs');
@@ -3,22 +3,59 @@ name: verify-ui
3
3
  description: 启动本地服务并用浏览器实测 UI 改动(只读验证、不留脏数据);项目无测试基建时靠实测兜底。当用户要求"实测 / UI 验证 / 浏览器验证"时使用。
4
4
  ---
5
5
 
6
- # verify-ui — UI 实测引导(骨架,项目按实际填「启动命令 / 实测入口」)
6
+ # verify-ui — UI 实测引导
7
+
8
+ 无测试基建时,UI 改动必须浏览器实测。步骤:
7
9
 
8
10
  ## 1. 起服务
9
11
 
10
- - API:<待填:启动命令(见 .agents/notes/runtime-env.md §2)>
11
- - 前端:<待填:启动命令>
12
+ 启动命令**不写死在本文件**(本文件是 managed 件,随包升级覆盖——项目特定值写这里会丢):以 `.agents/notes/runtime-env.md`「本地端口与进程」节的记录为准;该节无记录时,先起服务并把命令补记进该文件再继续。
12
13
 
13
14
  进程清单记进 `.agents/notes/runtime-env.md`,实测结束必须关闭。
14
15
 
15
- ## 2. 实测步骤
16
+ ## 2. 浏览器验证
17
+
18
+ - 优先 `browser` 工具;不可用时用 `.agents/scripts/e2e/harness.mjs`(如项目已装;封装 `launch`/`seedSession`/`goto`/`hardGoto`/`clickText`/`pressKey`/`ensureClean`/`screenshot` + 网络/控制台/toast 采集器);自写脚本优先复用其常量与函数。
19
+ - 浏览器首选**较新的 Chromium 内核 headless**(如 Edge `--headless=new`)——旧版 Chrome 会把 antd 图标字体渲染成方框、表格 `waitFor` 超时(见下「浏览器选择」)。
20
+ - 登录态:先调 API 登录拿 token,注入前端约定的 localStorage 键,**并整页重载**让前端状态库重读 token(仅改 hash 的导航不重建文档,会被路由守卫弹回登录页)。
21
+ - 路由为 hash 模式时:`goto` 用 hash;深链接(query 变化)用 `hardGoto`(about:blank 中转强制新文档)。
22
+
23
+ ## 3. 实测步骤
16
24
 
17
25
  1. 打开目标页面,按改动点逐项操作:正常路径一条 + 失败路径一条(校验/空态/权限)。
18
26
  2. 证据落盘:截图或关键控制台/网络报文,路径写在任务文档「验收标准」勾验项后。
19
- 3. 不留脏数据:验证产生的临时数据当场删除或还原;不确定可回滚的操作先停下问用户。
27
+ 3. **不留脏数据**:验证产生的临时数据当场删除或还原;清理**只按本次创建的 ID 精确删除**(ID 取自网络捕获的 POST 响应),**禁止按文案/字段模糊匹配**——模糊匹配会误删用户手建数据;清理脚本一律先备份再删;不确定可回滚的操作先停下问用户。
20
28
 
21
- ## 3. 收尾
29
+ ## 4. 收尾
22
30
 
23
31
  - 关闭本次起动的全部进程(按 §1 清单逐个停)。
24
32
  - 结论按「页面 / 步骤 / 期望 vs 实际 / 证据」回报;发现缺陷只记录定性,不修代码(修复走 build/maintain 流程)。
33
+
34
+ ## 关键坑
35
+
36
+ - API 用 **HTTP** 避开自签证书问题(HTTPS 自签证书会让浏览器/脚本报错或需额外信任配置)。
37
+ - MCP 报「browser is already running」是残留实例占 MCP 专用 profile 锁(`~/.cache/chrome-devtools-mcp/`),清掉对应 chrome 进程重试即可,勿误判为 MCP 不可用。
38
+ - 布局类修复用 `getBoundingClientRect` 程序化检测,比截图肉眼更可靠。
39
+ - 后台任务生命周期不可依赖(实测可长可短):长命服务一律独立进程起(如 PowerShell `Start-Process`)。
40
+ - 用 shell 工具起长命服务时**勿在命令末尾接管道**(`| tail` 等)——子进程继承 stdout 句柄会让命令永不返回,直到被工具超时杀掉(并可能连带杀掉服务)。
41
+
42
+ ### 浏览器选择
43
+
44
+ - 旧版 Chrome(实测 83)内核太旧 → antd 图标字体全渲染成方框、表格 `waitFor` 超时。**优先较新的 Chromium 内核**(如 Edge `--headless=new`);harness 的 `CHROME` 常量按本机实际安装路径改。
45
+
46
+ ### antd 交互(JS `.click()` 不够)
47
+
48
+ - 按钮文本内含空格(`确 定` / `保 存` / `取 消`)→ 匹配须去空白,勿用 `innerText === '确定'`。
49
+ - Select / Picker / Dropdown 的 `.click()` 打不开弹层 → 须 `Input.dispatchMouseEvent`(mousedown+mouseup 序列);普通按钮用 `clickText` 够用,弹层控件须补 `realClick`。
50
+ - `new KeyboardEvent('Escape')` 关不掉弹层 → 须 `Input.dispatchKeyEvent`(rawKeyDown+keyUp)。
51
+ - **关弹层慎用 Esc**:antd Drawer 同样响应 Esc,会连带关闭抽屉并 reset 表单(表现为「保存后列表无新行」,易误判为保存链路故障)——改点遮罩/关闭按钮。
52
+ - 表格行常无 `tr[data-row-key]` 属性 → 用 `.ant-table-tbody > tr.ant-table-row` + textContent 过滤业务键(如单号)定位行。
53
+ - 宽表 `:scroll="{x:N}"` 截图前把 `.ant-table-content` scrollLeft 置底,露出右侧列。
54
+ - 虚拟滚动下拉(a-select)DOM 只渲染约 10 个节点 → **勿按 DOM 节点数断言选项条数**,按接口响应条数断言。
55
+
56
+ ### CDP 采集(SPA hash 导航不重建文档)
57
+
58
+ - Log/Console 采集不随导航重置 → 每次导航前重置采集器,防上一页错误泄漏成后续页面的「控制台错误」。
59
+ - toast 观察器须**导航前**安装;导航后装会漏首屏 toast。
60
+ - 首屏无 token 被路由守卫落登录页:写 localStorage 后仅 hash 变化的 `Page.navigate` 不整页重载、前端状态库仍持空 token → 须 `about:blank` 中转强制新文档,或写后 `Page.reload`。
61
+ - 同路由 hash query 变化不重挂载(`#/x?k=A` → `#/x?k=B`)→ 深链接用例走 `about:blank` 中转。