agentic-flow-kit 1.6.0 → 1.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -6
- package/modules/gates/README.md +1 -0
- package/modules/gates/test-protect/README.md +23 -0
- package/modules/gates/test-protect/check-test-protect.mjs +64 -0
- package/modules/gates/test-protect/local-pre-commit.line +1 -0
- package/modules/hosts/README.md +57 -0
- package/modules/hosts/claude/commands/wf-build.md +5 -24
- package/modules/hosts/claude/commands/wf-deploy.md +1 -0
- package/modules/hosts/claude/commands/wf-design.md +2 -2
- package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/claude/commands/wf-plan.md +1 -1
- package/modules/hosts/claude/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/claude/commands/wf-test.md +5 -4
- package/modules/hosts/codex/commands/wf-build.md +5 -24
- package/modules/hosts/codex/commands/wf-deploy.md +1 -0
- package/modules/hosts/codex/commands/wf-design.md +2 -2
- package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/codex/commands/wf-plan.md +1 -1
- package/modules/hosts/codex/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/codex/commands/wf-test.md +5 -4
- package/modules/hosts/cursor/commands/wf-build.md +5 -24
- package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
- package/modules/hosts/cursor/commands/wf-design.md +2 -2
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/cursor/commands/wf-plan.md +1 -1
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/cursor/commands/wf-test.md +5 -4
- package/modules/hosts/opencode/commands/wf-build.md +5 -24
- package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
- package/modules/hosts/opencode/commands/wf-design.md +2 -2
- package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/opencode/commands/wf-plan.md +1 -1
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/opencode/commands/wf-test.md +5 -4
- package/modules/hosts/trae/commands/wf-build.md +5 -24
- package/modules/hosts/trae/commands/wf-deploy.md +1 -0
- package/modules/hosts/trae/commands/wf-design.md +2 -2
- package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/trae/commands/wf-plan.md +1 -1
- package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/trae/commands/wf-test.md +5 -4
- package/package.json +2 -2
- package/src/add-gate.mjs +25 -1
- package/src/add-gate.test.mjs +103 -0
- package/src/cli.mjs +17 -2
- package/src/doctor.mjs +99 -11
- package/src/gates.test.mjs +17 -0
- package/src/init.mjs +7 -1
- package/src/init.test.mjs +19 -0
- package/src/instance-ref.test.mjs +62 -0
- package/src/jscpd-drift.test.mjs +144 -0
- package/src/render.mjs +5 -0
- package/src/rule-budget.test.mjs +103 -0
- package/src/run-tests.mjs +16 -9
- package/src/sync-hosts.mjs +4 -2
- package/src/trae-hooks.test.mjs +15 -3
- package/src/validate.mjs +72 -0
- package/src/validate.test.mjs +95 -0
- package/templates/AGENTS.md +3 -1
- package/templates/_agents/commands/build.md +5 -24
- package/templates/_agents/commands/deploy.md +1 -0
- package/templates/_agents/commands/design.md +2 -2
- package/templates/_agents/commands/gate-checklist.md +1 -3
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/commands/source-sync-check.md +1 -3
- package/templates/_agents/commands/sync-hosts.md +1 -3
- package/templates/_agents/commands/test.md +5 -4
- package/templates/_agents/control-surfaces.txt +53 -0
- package/templates/_agents/scripts/agg-delegations.cjs +51 -6
- package/templates/_agents/scripts/agg-delegations.test.mjs +79 -1
- package/templates/_agents/scripts/check-engine-integrity.mjs +288 -0
- package/templates/_agents/scripts/check-engine-integrity.test.mjs +227 -0
- package/templates/_agents/scripts/check-lane-surface.mjs +7 -2
- package/templates/_agents/scripts/check-ledger-invariant.mjs +18 -0
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +16 -0
- package/templates/_agents/scripts/check-ledger.test.mjs +2 -0
- package/templates/_agents/scripts/check-loop.mjs +247 -5
- package/templates/_agents/scripts/check-loop.test.mjs +367 -2
- package/templates/_agents/scripts/confirm-doc.mjs +50 -3
- package/templates/_agents/scripts/confirm-doc.test.mjs +93 -1
- package/templates/_agents/scripts/doctor.test.mjs +251 -1
- package/templates/_agents/scripts/draft-sediment.mjs +178 -0
- package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
- package/templates/_agents/scripts/gate-checklist.mjs +10 -2
- package/templates/_agents/scripts/gate-checklist.test.mjs +7 -1
- package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +2 -2
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +11 -5
- package/templates/_agents/scripts/kb-search.mjs +87 -12
- package/templates/_agents/scripts/kb-search.test.mjs +83 -0
- package/templates/_agents/scripts/pipeline-run.mjs +1 -1
- package/templates/_agents/scripts/policy.mjs +49 -0
- package/templates/_agents/scripts/solidify-task.mjs +1 -1
- package/templates/_agents/scripts/source-sync-check.test.mjs +9 -4
- package/templates/_agents/scripts/stage-gates.mjs +48 -1
- package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
- package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
- package/templates/_agents/scripts/verify.mjs +12 -4
- package/templates/_agents/scripts/verify.test.mjs +30 -3
- package/templates/_agents/scripts-test-exempt.txt +3 -1
- package/templates/_agents/skills/wiki/SKILL.md +5 -3
- package/templates/wiki/INDEX.md +44 -1
- package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
- package/templates/workflow/delegations.md +5 -4
- package/templates/workflow/incidents/_TEMPLATE.md +2 -0
- package/templates/workflow/intents/_TEMPLATE.md +3 -0
- package/templates/workflow/plans/_TEMPLATE.md +4 -0
- package/templates/workflow/specs/_TEMPLATE.md +23 -0
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
// ④ kit.json 无 owned 字段 → skipped=true total=0
|
|
8
8
|
// 另测 checkAdapterDrift(target)(场景 5-14)与 checkLedgerCoverage(target, pkgRoot)(场景 15-18,
|
|
9
9
|
// 2026-09-26 managed-ledger-adopt:补「盘上有、台账无」一侧)。
|
|
10
|
+
// 另测 checkDelegationSnapshotFreshness(target)(场景 19-23,2026-09-29 p0-gate-noise-batch)与
|
|
11
|
+
// checkGateAnchors(target)(场景 24-30,2026-10-11 P3-1 gate 防改锚:add-gate 写下的 srcSha256 锚让
|
|
12
|
+
// .agents/hooks/ 门禁脚本手改永久可见;local-pre-commit 模板语义锚须排除出比对面)。
|
|
13
|
+
// 另测 checkTemplateDrift(target, pkgRoot)(场景 31-36,2026-10-11 模板感知直测补齐——此前唯一无直测的
|
|
14
|
+
// 导出 check:三态判定 + 排除面 + 无模板跳过)。
|
|
10
15
|
// 用法:node .agents/scripts/doctor.test.mjs(在仓库根执行,需 src/doctor.mjs 存在——dogfooding 模式)
|
|
11
16
|
import fs from 'node:fs';
|
|
12
17
|
import os from 'node:os';
|
|
@@ -27,7 +32,7 @@ if (!fs.existsSync(SRC)) {
|
|
|
27
32
|
console.error(`doctor.test.mjs:SKIP——未找到 ${SRC}(装户环境无包源 src/;本套件仅在包源仓库/dogfooding 跑 npm test 时有意义)`);
|
|
28
33
|
process.exit(0);
|
|
29
34
|
}
|
|
30
|
-
const { checkOwnedDrift, checkAdapterDrift, checkLedgerCoverage, checkDelegationSnapshotFreshness } = await import(pathToFileURL(SRC).href);
|
|
35
|
+
const { checkOwnedDrift, checkAdapterDrift, checkLedgerCoverage, checkDelegationSnapshotFreshness, checkGateAnchors, checkTemplateDrift, gateTrustOf, GATE_TRUST_LEVELS, GATE_TRUST_DEFAULT } = await import(pathToFileURL(SRC).href);
|
|
31
36
|
|
|
32
37
|
let pass = 0;
|
|
33
38
|
let fail = 0;
|
|
@@ -473,5 +478,250 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
|
|
|
473
478
|
fs.rmSync(root, { recursive: true, force: true });
|
|
474
479
|
}
|
|
475
480
|
|
|
481
|
+
// ---- 场景 24-30:checkGateAnchors(2026-10-11 P3-1 gate 防改锚)—— .agents/hooks/ 门禁脚本安装锚 ----
|
|
482
|
+
// 锚语义(见 src/add-gate.mjs 头注):add-gate 逐字拷贝安装时写 srcSha256=安装时刻包源 LF 归一 sha;
|
|
483
|
+
// sync 自愈 sha256 但从不按盘面刷新 gate 条目的 srcSha256(srcTemplatePath 解析不到包源模板提前 return)
|
|
484
|
+
// → 盘面 sha ≠ 锚即「装后手改」,doctor 出 WARN 留痕(不阻断——装户对门有最终所有权,锚只提供可见性)。
|
|
485
|
+
const GATE_A = '.agents/hooks/check-test-protect.mjs';
|
|
486
|
+
const GATE_B = '.agents/hooks/check-node-layer.mjs';
|
|
487
|
+
const GATE_BODY = '// gate v1\n';
|
|
488
|
+
|
|
489
|
+
// ---- 场景 24:锚完好 → PASS 语义(skipped=false mismatch=[] gone=[] total=带锚数)----
|
|
490
|
+
{
|
|
491
|
+
const root = mkfix({
|
|
492
|
+
kitContent: mkKit([{ rel: GATE_A, sha256: sha256Of(GATE_BODY), srcSha256: sha256Of(GATE_BODY) }]),
|
|
493
|
+
files: { [GATE_A]: GATE_BODY },
|
|
494
|
+
});
|
|
495
|
+
const r = checkGateAnchors(root);
|
|
496
|
+
check('场景 24:gate 锚完好 → mismatch=0 gone=0 total=1 skipped=false',
|
|
497
|
+
r.skipped === false && r.mismatch.length === 0 && r.gone.length === 0 && r.total === 1,
|
|
498
|
+
JSON.stringify(r));
|
|
499
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ---- 场景 25:篡改带锚 gate 文件 → mismatch=[rel](手改永久留痕——本单的目标形态)----
|
|
503
|
+
{
|
|
504
|
+
const root = mkfix({
|
|
505
|
+
kitContent: mkKit([{ rel: GATE_A, sha256: sha256Of(GATE_BODY), srcSha256: sha256Of(GATE_BODY) }]),
|
|
506
|
+
files: { [GATE_A]: '// gate v1 本地手改:放宽红线\n' }, // 盘面 ≠ 安装时刻包源
|
|
507
|
+
});
|
|
508
|
+
const r = checkGateAnchors(root);
|
|
509
|
+
check('场景 25:手改带锚 gate → mismatch 含该件 skipped=false',
|
|
510
|
+
r.skipped === false && r.mismatch.length === 1 && r.mismatch[0] === GATE_A,
|
|
511
|
+
JSON.stringify(r));
|
|
512
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ---- 场景 26:混合面——完好 / 失配 / 缺失并存 → mismatch 与 gone 各归各位,total=带锚条目数 ----
|
|
516
|
+
{
|
|
517
|
+
const root = mkfix({
|
|
518
|
+
kitContent: mkKit([
|
|
519
|
+
{ rel: GATE_A, sha256: sha256Of(GATE_BODY), srcSha256: sha256Of(GATE_BODY) }, // 完好
|
|
520
|
+
{ rel: GATE_B, sha256: sha256Of(GATE_BODY), srcSha256: sha256Of(GATE_BODY) }, // 缺失(不落盘)
|
|
521
|
+
]),
|
|
522
|
+
files: { [GATE_A]: GATE_BODY },
|
|
523
|
+
});
|
|
524
|
+
const r = checkGateAnchors(root);
|
|
525
|
+
check('场景 26:完好+缺失并存 → gone=[GATE_B] mismatch=[] total=2',
|
|
526
|
+
r.skipped === false && r.gone.length === 1 && r.gone[0] === GATE_B && r.mismatch.length === 0 && r.total === 2,
|
|
527
|
+
JSON.stringify(r));
|
|
528
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ---- 场景 27:无带锚条目(旧装户 owned 无 srcSha256 / 非 hooks 条目)→ skipped=true 静默 ----
|
|
532
|
+
{
|
|
533
|
+
const root = mkfix({
|
|
534
|
+
kitContent: mkKit([
|
|
535
|
+
{ rel: GATE_A, sha256: sha256Of(GATE_BODY) }, // 旧装户:无锚
|
|
536
|
+
{ rel: 'AGENTS.md', sha256: sha256Of('# a\n') }, // 非 hooks 条目不参与
|
|
537
|
+
]),
|
|
538
|
+
files: { [GATE_A]: GATE_BODY, 'AGENTS.md': '# a\n' },
|
|
539
|
+
});
|
|
540
|
+
const r = checkGateAnchors(root);
|
|
541
|
+
check('场景 27:无带锚 gate 条目 → skipped=true(旧装户重装 add-gate 后生效)',
|
|
542
|
+
r.skipped === true && r.total === 0,
|
|
543
|
+
JSON.stringify(r));
|
|
544
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ---- 场景 28:local-pre-commit 模板语义锚不参与比对(防恒失配误报)----
|
|
548
|
+
// 它的 srcSha256 由 init/sync 按「模板语义」写(锚=templates/_agents/hooks/local-pre-commit),
|
|
549
|
+
// 盘面含各门挂载行 ≠ 模板——纳入比对面必恒失配,须排除(见 checkGateAnchors 注释)。
|
|
550
|
+
{
|
|
551
|
+
const LP_WIRED = '#!/bin/sh\nset -e\nnode .agents/hooks/check-test-protect.mjs\n'; // 接线后盘面
|
|
552
|
+
const root = mkfix({
|
|
553
|
+
kitContent: mkKit([
|
|
554
|
+
{ rel: '.agents/hooks/local-pre-commit', sha256: sha256Of(LP_WIRED), srcSha256: sha256Of('#!/bin/sh\nset -e\n') },
|
|
555
|
+
]),
|
|
556
|
+
files: { '.agents/hooks/local-pre-commit': LP_WIRED },
|
|
557
|
+
});
|
|
558
|
+
const r = checkGateAnchors(root);
|
|
559
|
+
check('场景 28:local-pre-commit(模板锚+接线盘面)不纳入 → skipped=true 不误报',
|
|
560
|
+
r.skipped === true && r.total === 0,
|
|
561
|
+
JSON.stringify(r));
|
|
562
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ---- 场景 29:kit.json 解析失败 → skipped=true 含 error(沿用 §6.6 同款跳过语义)----
|
|
566
|
+
{
|
|
567
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-gateanchor-'));
|
|
568
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
569
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'), '{ broken json');
|
|
570
|
+
const r = checkGateAnchors(root);
|
|
571
|
+
check('场景 29:kit.json 解析失败 → skipped=true 含 error',
|
|
572
|
+
r.skipped === true && typeof r.error === 'string' && r.error.length > 0,
|
|
573
|
+
JSON.stringify(r));
|
|
574
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ---- 场景 30:主流程渐进路径源断言(同场景 8 手法)——§6.6b 失配分支按信任档分级 ----
|
|
578
|
+
// 默认档(gateTrust=project)= WARN(「先 WARN 升级 FAIL」渐进路径):装户可改门禁,锚只提供可见性。
|
|
579
|
+
// org 档(2026-10-12 gate-trust-level)= FAIL —— 组织级强制位。断言的是**分级存在**,不是写死某档。
|
|
580
|
+
{
|
|
581
|
+
const doctorSrc = fs.readFileSync(SRC, 'utf8');
|
|
582
|
+
const lvMatch = /gateRes\.mismatch\.length\)\s*add\((lv)/.test(doctorSrc);
|
|
583
|
+
const lvDef = /const lv = orgMode \? 'FAIL' : 'WARN'/.test(doctorSrc);
|
|
584
|
+
check('场景 30a:§6.6b gate 锚失配分支按信任档分级(lv 常量,不写死单档)',
|
|
585
|
+
lvMatch && lvDef,
|
|
586
|
+
`src/doctor.mjs §6.6b 失配分支应为 add(lv, ...) 且 lv = orgMode ? 'FAIL' : 'WARN'`);
|
|
587
|
+
check('场景 30b:失配出账含「gate 安装锚失配」字样(含重装重锚/恢复原版指引)',
|
|
588
|
+
doctorSrc.includes('gate 安装锚失配') && doctorSrc.includes('add-gate <门名> --force'),
|
|
589
|
+
'src/doctor.mjs §6.6b 文案缺失');
|
|
590
|
+
check('场景 30c:无带锚条目 → PASS 跳过语义(旧装户静默)',
|
|
591
|
+
doctorSrc.includes('无带锚 gate 条目'),
|
|
592
|
+
'src/doctor.mjs §6.6b 应有无锚跳过 PASS 分支');
|
|
593
|
+
// 函数体切取:checkGateAnchors 声明行起、下一个 export function 止(JSDoc 不参与断言)
|
|
594
|
+
const fnStart = doctorSrc.indexOf('export function checkGateAnchors');
|
|
595
|
+
const fnEnd = doctorSrc.indexOf('export function', fnStart + 10);
|
|
596
|
+
check('场景 30d:local-pre-commit 排除在比对面外(防模板锚恒失配误报)',
|
|
597
|
+
fnStart >= 0 && fnEnd > fnStart && doctorSrc.slice(fnStart, fnEnd).includes("'.agents/hooks/local-pre-commit'"),
|
|
598
|
+
'checkGateAnchors 应排除 .agents/hooks/local-pre-commit');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ---- 场景 37-42:gateTrustOf 信任档(2026-10-12 gate-trust-level)----
|
|
602
|
+
// 反例钉子核心:**非法值 / 缺键必须回退 project(零行为变化),绝不因非法配置变严**
|
|
603
|
+
// ——把默认升 org 是另一个决定;让「写错一个字符串」自动升到组织级强制则是配置事故。
|
|
604
|
+
{
|
|
605
|
+
check('场景 37:显式 project / org 均如实返回',
|
|
606
|
+
gateTrustOf({ gateTrust: 'project' }) === 'project' && gateTrustOf({ gateTrust: 'org' }) === 'org');
|
|
607
|
+
check('场景 38:缺键(旧台账)/ null / undefined / 无 kit → 回退 project(零行为变化)',
|
|
608
|
+
gateTrustOf({}) === 'project' && gateTrustOf({ gateTrust: undefined }) === 'project'
|
|
609
|
+
&& gateTrustOf(null) === 'project' && gateTrustOf(undefined) === 'project');
|
|
610
|
+
check('场景 39:非法值全部回退 project(大小写错、自造档、数字、数组皆同)',
|
|
611
|
+
gateTrustOf({ gateTrust: 'ORG' }) === 'project' && gateTrustOf({ gateTrust: 'organization' }) === 'project'
|
|
612
|
+
&& gateTrustOf({ gateTrust: 1 }) === 'project' && gateTrustOf({ gateTrust: ['org'] }) === 'project');
|
|
613
|
+
check('场景 40:默认值常量与档位表同源一致(init 写 kit.json 用的就是它)',
|
|
614
|
+
GATE_TRUST_DEFAULT === 'project' && GATE_TRUST_LEVELS.join(',') === 'project,org');
|
|
615
|
+
|
|
616
|
+
// 端到端:kit.json 带 gateTrust → checkGateAnchors 回读该档(判据确实消费了它)
|
|
617
|
+
const mkTrustKit = (gateTrust) => ({
|
|
618
|
+
...mkKit([{ rel: GATE_A, sha256: sha256Of(GATE_BODY), srcSha256: sha256Of(GATE_BODY) }]),
|
|
619
|
+
...(gateTrust === undefined ? {} : { gateTrust }),
|
|
620
|
+
});
|
|
621
|
+
for (const [label, gt, want] of [
|
|
622
|
+
['显式 org → 回读 org', 'org', 'org'],
|
|
623
|
+
['显式 project → 回读 project', 'project', 'project'],
|
|
624
|
+
['缺键(旧装户)→ 回读 project', undefined, 'project'],
|
|
625
|
+
['非法值 → 回读 project', 'ORG', 'project'],
|
|
626
|
+
]) {
|
|
627
|
+
const root = mkfix({ kitContent: mkTrustKit(gt), files: { [GATE_A]: '// 手改\n' } });
|
|
628
|
+
const r = checkGateAnchors(root);
|
|
629
|
+
check(`场景 41 ${label}(且失配仍照常检出)`, r.trust === want && r.mismatch.length === 1,
|
|
630
|
+
JSON.stringify({ want, got: r.trust, mismatch: r.mismatch }));
|
|
631
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ---- 场景 31-36:checkTemplateDrift(2026-10-11 模板感知直测补齐——此前唯一无直测的导出 check)----
|
|
636
|
+
// 三态判定单源 profiles.templateDriftOf:synced(盘==当前源)/ stale-drift(源演进盘未跟,唯一出账)/
|
|
637
|
+
// custom-synced(盘定制、源==锚,永不告警)/ no-anchor(旧装户静默);TEMPLATE_DRIFT_EXCLUDE 与
|
|
638
|
+
// 无对应模板 rel 均不入比对面。fixture 双根:target(.agents/kit.json + 盘面)+ pkgRoot(templates/)。
|
|
639
|
+
{
|
|
640
|
+
const OLD_BODY = '# tpl v1\n';
|
|
641
|
+
const NEW_BODY = '# tpl v2 — source evolved\n';
|
|
642
|
+
const CUSTOM_BODY = '# hand-customized\n';
|
|
643
|
+
const mkpkg = (tplContent) => {
|
|
644
|
+
const pkg = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-tpl-pkg-'));
|
|
645
|
+
fs.mkdirSync(path.join(pkg, 'templates'), { recursive: true });
|
|
646
|
+
if (tplContent !== null) fs.writeFileSync(path.join(pkg, 'templates', 'AGENTS.md'), tplContent);
|
|
647
|
+
return pkg;
|
|
648
|
+
};
|
|
649
|
+
// 31 盘面 == 当前源 → synced,零 stale
|
|
650
|
+
{
|
|
651
|
+
const pkg = mkpkg(OLD_BODY);
|
|
652
|
+
const root = mkfix({
|
|
653
|
+
kitContent: mkKit([{ rel: 'AGENTS.md', sha256: sha256Of(OLD_BODY), srcSha256: sha256Of(OLD_BODY) }]),
|
|
654
|
+
files: { 'AGENTS.md': OLD_BODY },
|
|
655
|
+
});
|
|
656
|
+
const r = checkTemplateDrift(root, pkg);
|
|
657
|
+
check('场景 31:盘面==当前源 → synced(skipped=false stale=[] total=1)',
|
|
658
|
+
r.skipped === false && r.total === 1 && r.stale.length === 0, JSON.stringify(r));
|
|
659
|
+
fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(pkg, { recursive: true, force: true });
|
|
660
|
+
}
|
|
661
|
+
// 32 源演进 + 盘面未跟 → stale-drift(唯一出账形态)
|
|
662
|
+
{
|
|
663
|
+
const pkg = mkpkg(NEW_BODY); // 包源已演进
|
|
664
|
+
const root = mkfix({
|
|
665
|
+
kitContent: mkKit([{ rel: 'AGENTS.md', sha256: sha256Of(OLD_BODY), srcSha256: sha256Of(OLD_BODY) }]),
|
|
666
|
+
files: { 'AGENTS.md': OLD_BODY }, // 盘面还停在锚版本
|
|
667
|
+
});
|
|
668
|
+
const r = checkTemplateDrift(root, pkg);
|
|
669
|
+
check('场景 32:源演进盘未跟 → stale-drift(stale=[AGENTS.md])',
|
|
670
|
+
r.skipped === false && r.stale.length === 1 && r.stale[0] === 'AGENTS.md', JSON.stringify(r));
|
|
671
|
+
fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(pkg, { recursive: true, force: true });
|
|
672
|
+
}
|
|
673
|
+
// 33 盘面定制(源==锚)→ custom-synced,永不告警
|
|
674
|
+
{
|
|
675
|
+
const pkg = mkpkg(OLD_BODY);
|
|
676
|
+
const root = mkfix({
|
|
677
|
+
kitContent: mkKit([{ rel: 'AGENTS.md', sha256: sha256Of(CUSTOM_BODY), srcSha256: sha256Of(OLD_BODY) }]),
|
|
678
|
+
files: { 'AGENTS.md': CUSTOM_BODY },
|
|
679
|
+
});
|
|
680
|
+
const r = checkTemplateDrift(root, pkg);
|
|
681
|
+
check('场景 33:盘面定制源==锚 → custom-synced(stale=[] 不出账)',
|
|
682
|
+
r.skipped === false && r.total === 1 && r.stale.length === 0, JSON.stringify(r));
|
|
683
|
+
fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(pkg, { recursive: true, force: true });
|
|
684
|
+
}
|
|
685
|
+
// 34 无 srcSha256 锚(旧装户)→ skipped 静默不追溯
|
|
686
|
+
{
|
|
687
|
+
const pkg = mkpkg(OLD_BODY);
|
|
688
|
+
const root = mkfix({
|
|
689
|
+
kitContent: mkKit([{ rel: 'AGENTS.md', sha256: sha256Of(OLD_BODY) }]),
|
|
690
|
+
files: { 'AGENTS.md': OLD_BODY },
|
|
691
|
+
});
|
|
692
|
+
const r = checkTemplateDrift(root, pkg);
|
|
693
|
+
check('场景 34:无锚 → skipped=true(旧装户静默,下次 sync 写锚生效)',
|
|
694
|
+
r.skipped === true && r.total === 0 && r.stale.length === 0, JSON.stringify(r));
|
|
695
|
+
fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(pkg, { recursive: true, force: true });
|
|
696
|
+
}
|
|
697
|
+
// 35 生成器目标(TEMPLATE_DRIFT_EXCLUDE)排除出比对面——混合条目只计 AGENTS.md
|
|
698
|
+
{
|
|
699
|
+
const pkg = mkpkg(OLD_BODY);
|
|
700
|
+
const root = mkfix({
|
|
701
|
+
kitContent: mkKit([
|
|
702
|
+
{ rel: 'workflow/INDEX.md', sha256: sha256Of('# idx\n'), srcSha256: sha256Of('# idx\n') },
|
|
703
|
+
{ rel: 'AGENTS.md', sha256: sha256Of(OLD_BODY), srcSha256: sha256Of(OLD_BODY) },
|
|
704
|
+
]),
|
|
705
|
+
files: { 'AGENTS.md': OLD_BODY },
|
|
706
|
+
});
|
|
707
|
+
const r = checkTemplateDrift(root, pkg);
|
|
708
|
+
check('场景 35:生成器目标排除 → total=1 只计 AGENTS.md',
|
|
709
|
+
r.skipped === false && r.total === 1 && r.stale.length === 0, JSON.stringify(r));
|
|
710
|
+
fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(pkg, { recursive: true, force: true });
|
|
711
|
+
}
|
|
712
|
+
// 36 rel 无对应模板(srcTemplatePath null)→ 不入比对面
|
|
713
|
+
{
|
|
714
|
+
const pkg = mkpkg(OLD_BODY);
|
|
715
|
+
const root = mkfix({
|
|
716
|
+
kitContent: mkKit([{ rel: 'wiki/自定文档.md', sha256: sha256Of('# x\n'), srcSha256: sha256Of('# x\n') }]),
|
|
717
|
+
files: {},
|
|
718
|
+
});
|
|
719
|
+
const r = checkTemplateDrift(root, pkg);
|
|
720
|
+
check('场景 36:无对应模板 rel → 不计数(skipped=true)',
|
|
721
|
+
r.skipped === true && r.total === 0, JSON.stringify(r));
|
|
722
|
+
fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(pkg, { recursive: true, force: true });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
476
726
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
477
727
|
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// flow-kit draft-sediment:关单时把高价值内容自动抽成 wiki 草稿(B2,2026-10-10)
|
|
2
|
+
// 输入:一个 done intent 或 closed incident 文件 → 抽取高价值节(影响面/触达红线 或 复盘三件套)
|
|
3
|
+
// → 渲染成带「来源」frontmatter 的 wiki 草稿 → 落 wiki/drafts-archive/<日期-主题>/<主题>.md
|
|
4
|
+
// → 回写源文件 frontmatter「沉淀:」字段(与 B1 沉淀追踪闭环)→ 重跑 gen-wiki-board(归档计数随动,
|
|
5
|
+
// 否则 verify-wiki-consistency 的 summary.archive 比对会拦提交)
|
|
6
|
+
// 草稿是中间态:drafts-archive 只读不增量、不参与文件级登记(仅计总数),归类到活跃主题目录由人工完成
|
|
7
|
+
// (人工移动后跑 gen-wiki-board + verify-wiki-consistency,并在目标文件保留/改写「来源」字段)
|
|
8
|
+
// 用法:node .agents/scripts/draft-sediment.mjs workflow/intents/<日期>-<主题>.md [--dry-run] [--help]
|
|
9
|
+
// (intent 须 done;incident 须 closed 且复盘三件套齐全——与 check-loop 沉淀追踪子判据同口径)
|
|
10
|
+
// 零依赖;抽节为纯函数(export 供测试);CLI 解析最简字符串匹配
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
const USAGE = `用法:node .agents/scripts/draft-sediment.mjs <workflow/intents|incidents/文件.md> [--dry-run] [--help]
|
|
19
|
+
|
|
20
|
+
关单时把高价值内容抽成 wiki 草稿(B2 沉淀自动化):
|
|
21
|
+
- intent(须 done):抽「影响面」「触达红线」节
|
|
22
|
+
- incident(须 closed 且复盘三件套齐全):抽「影响面」「根因」「为什么之前没拦住」「复盘三件套」节
|
|
23
|
+
落 wiki/drafts-archive/<日期-主题>/<主题>.md,带「来源」frontmatter 指回源文件(B3 双向链接),
|
|
24
|
+
并回写源文件 frontmatter「沉淀:」字段(B1 闭环),随后重跑 gen-wiki-board.mjs 更新归档计数。
|
|
25
|
+
|
|
26
|
+
选项:
|
|
27
|
+
--dry-run 只打印将生成的草稿与回写动作,不写盘
|
|
28
|
+
--help 打印本说明
|
|
29
|
+
|
|
30
|
+
草稿是中间态:drafts-archive 只读不增量。人工归类到 wiki/<主题>/ 后跑
|
|
31
|
+
node .agents/scripts/gen-wiki-board.mjs && node .agents/scripts/verify-wiki-consistency.mjs`;
|
|
32
|
+
|
|
33
|
+
// extractSections:从 markdown 抽指定节(## 标题到下一 ## 之间的非空正文)。纯函数,无 IO。
|
|
34
|
+
// 节标题前缀匹配(兼容「## 影响面与边界」)。占位行(<…> 单行包揽 / HTML 注释 / 引用块)剔除——
|
|
35
|
+
// 与 check-loop sectionHasBody 同口径:模板未填段抽出的是占位,没有沉淀价值。
|
|
36
|
+
// 返回 [{ title, body }],body 为去占位后的非空行拼接(保留原行内容与缩进)。
|
|
37
|
+
export function extractSections(text, headings) {
|
|
38
|
+
const lines = String(text).split(/\r?\n/);
|
|
39
|
+
const out = [];
|
|
40
|
+
let cur = null;
|
|
41
|
+
let buf = [];
|
|
42
|
+
const flush = () => {
|
|
43
|
+
if (cur) {
|
|
44
|
+
const body = buf.filter((l) => {
|
|
45
|
+
const s = l.trim();
|
|
46
|
+
return s && !/^<.*>$/.test(s) && !s.startsWith('<!--') && !s.startsWith('>');
|
|
47
|
+
});
|
|
48
|
+
if (body.length) out.push({ title: cur, body: body.join('\n') });
|
|
49
|
+
}
|
|
50
|
+
buf = [];
|
|
51
|
+
};
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
const h = line.match(/^\s*##\s+(.+?)\s*$/);
|
|
54
|
+
if (h) {
|
|
55
|
+
flush();
|
|
56
|
+
cur = headings.find((x) => h[1].startsWith(x)) ? h[1] : null;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (cur) buf.push(line);
|
|
60
|
+
}
|
|
61
|
+
flush();
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// topicOf:主题取法——标题行(# INTENT — <主题>)优先(文件名主题常是英文短代称,
|
|
66
|
+
// 标题才是可读中文名);无标题行再退文件名尾段(YYYY-MM-DD-<主题>);都无取文件基名。
|
|
67
|
+
export function topicOf(filePath, text) {
|
|
68
|
+
const t = String(text).match(/^#\s+(?:INTENT|INCIDENT|PLAN|SPEC)\s*—\s*(.+?)\s*$/m);
|
|
69
|
+
if (t) return t[1];
|
|
70
|
+
const base = path.basename(filePath, '.md');
|
|
71
|
+
const m = base.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
72
|
+
return m ? m[1] : base;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// renderDraft:草稿渲染(纯函数)。date = 落盘日期(YYYY-MM-DD),source = 来源相对仓根路径。
|
|
76
|
+
export function renderDraft({ topic, date, source, sections, kind }) {
|
|
77
|
+
const fm = `---\n来源: ${source}\n状态: 草稿(待人工归类)\n---\n`;
|
|
78
|
+
const kindLabel = kind === 'incident' ? '事故复盘' : '需求沉淀';
|
|
79
|
+
let body = `# ${topic}(${kindLabel}草稿)\n`;
|
|
80
|
+
body += `> 自动抽取于 ${date},源自 \`${source}\`(关单沉淀,B2)。`;
|
|
81
|
+
body += `本文件是**中间态**——归类到 wiki/<主题>/ 后补主题、用途,并保留「来源」字段。\n\n`;
|
|
82
|
+
for (const s of sections) {
|
|
83
|
+
body += `## ${s.title}\n\n${s.body}\n\n`;
|
|
84
|
+
}
|
|
85
|
+
return fm + body;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// setSedimentField:回写源文件 frontmatter「沉淀:」字段。已有值(非空、非「无」)不覆盖(防误抹人工登记)。
|
|
89
|
+
export function setSedimentField(text, value) {
|
|
90
|
+
const m = String(text).match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
|
|
91
|
+
if (!m) return { text, changed: false };
|
|
92
|
+
const fmBody = m[2];
|
|
93
|
+
const lines = fmBody.split(/\r?\n/);
|
|
94
|
+
const idx = lines.findIndex((l) => /^沉淀:\s*/.test(l));
|
|
95
|
+
if (idx >= 0) {
|
|
96
|
+
const cur = lines[idx].replace(/^沉淀:\s*/, '').trim();
|
|
97
|
+
if (cur && cur !== '无') return { text, changed: false }; // 已有人工登记,不覆盖
|
|
98
|
+
lines[idx] = `沉淀: ${value}`;
|
|
99
|
+
} else {
|
|
100
|
+
lines.push(`沉淀: ${value}`);
|
|
101
|
+
}
|
|
102
|
+
return { text: m[1] + lines.join('\n') + m[3] + String(text).slice(m[0].length), changed: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
|
|
106
|
+
|
|
107
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('draft-sediment.mjs');
|
|
108
|
+
if (isMain) {
|
|
109
|
+
const args = process.argv.slice(2);
|
|
110
|
+
if (args.includes('--help') || args.includes('-h')) { console.log(USAGE); process.exit(0); }
|
|
111
|
+
const DRY = args.includes('--dry-run');
|
|
112
|
+
const target = args.find((a) => !a.startsWith('-'));
|
|
113
|
+
if (!target) fail('必填:源文件路径(workflow/intents/… 或 workflow/incidents/…)' + '\n\n' + USAGE);
|
|
114
|
+
if (!fs.existsSync(target)) fail('源文件不存在:' + target);
|
|
115
|
+
|
|
116
|
+
const text = fs.readFileSync(target, 'utf8');
|
|
117
|
+
const rel = path.relative(process.cwd(), target).split(path.sep).join('/');
|
|
118
|
+
const kind = rel.startsWith('workflow/incidents/') ? 'incident' : rel.startsWith('workflow/intents/') ? 'intent' : null;
|
|
119
|
+
if (!kind) fail('源文件须在 workflow/intents/ 或 workflow/incidents/ 下:' + rel);
|
|
120
|
+
|
|
121
|
+
// 状态校验(与 check-loop 沉淀追踪子判据同口径)
|
|
122
|
+
const fmStatus = (() => {
|
|
123
|
+
const m = text.match(/^---\r?\n[\s\S]*?^状态:\s*(.+?)\s*$/m);
|
|
124
|
+
return m ? m[1].trim() : null;
|
|
125
|
+
})();
|
|
126
|
+
if (kind === 'intent' && fmStatus !== 'done') fail('intent 须为 done 才沉淀(当前:' + (fmStatus || '缺失') + ')');
|
|
127
|
+
if (kind === 'incident' && fmStatus !== 'closed') fail('incident 须为 closed 才沉淀(当前:' + (fmStatus || '缺失') + ')');
|
|
128
|
+
|
|
129
|
+
// 抽节:intent 影响面/触达红线;incident 影响面/根因/为什么之前没拦住/复盘三件套
|
|
130
|
+
const HEADINGS = kind === 'incident'
|
|
131
|
+
? ['影响面', '根因', '为什么之前没拦住', '复盘三件套']
|
|
132
|
+
: ['影响面', '触达红线'];
|
|
133
|
+
const sections = extractSections(text, HEADINGS);
|
|
134
|
+
if (!sections.length) fail('未抽取到高价值节(' + HEADINGS.join('/') + ' 均为空或仅占位)——内容无沉淀价值,停止');
|
|
135
|
+
|
|
136
|
+
const topic = topicOf(target, text);
|
|
137
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
138
|
+
const draftDir = path.join('wiki', 'drafts-archive', `${date}-${topic}`);
|
|
139
|
+
const draftPath = path.join(draftDir, `${topic}.md`);
|
|
140
|
+
const draft = renderDraft({ topic, date, source: rel, sections, kind });
|
|
141
|
+
|
|
142
|
+
// 回写源文件沉淀字段(草稿路径相对仓根)
|
|
143
|
+
const { text: newText, changed } = setSedimentField(text, draftPath.split(path.sep).join('/'));
|
|
144
|
+
|
|
145
|
+
if (DRY) {
|
|
146
|
+
console.log('🔎 --dry-run:未写盘');
|
|
147
|
+
console.log(`• 将生成草稿:${draftPath}(${sections.length} 节 / ${draft.length} 字节)`);
|
|
148
|
+
console.log(`• 将回写 ${rel} frontmatter「沉淀: ${draftPath.split(path.sep).join('/')}」${changed ? '' : '(已有登记,不覆盖)'}`);
|
|
149
|
+
console.log('• 将重跑 gen-wiki-board.mjs(归档计数 +1)');
|
|
150
|
+
console.log('\n--- 草稿预览(前 30 行)---');
|
|
151
|
+
console.log(draft.split(/\r?\n/).slice(0, 30).join('\n'));
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 写草稿
|
|
156
|
+
fs.mkdirSync(draftDir, { recursive: true });
|
|
157
|
+
fs.writeFileSync(draftPath, draft);
|
|
158
|
+
console.log(`✅ 已生成 wiki 草稿:${draftPath}(${sections.length} 节:${sections.map((s) => s.title).join('、')})`);
|
|
159
|
+
|
|
160
|
+
// 回写源文件
|
|
161
|
+
if (changed) {
|
|
162
|
+
fs.writeFileSync(target, newText);
|
|
163
|
+
console.log(`✅ 已回写源文件沉淀字段:${rel} → 沉淀: ${draftPath.split(path.sep).join('/')}`);
|
|
164
|
+
} else {
|
|
165
|
+
console.log(`ℹ️ 源文件已有沉淀登记,未覆盖:${rel}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 重跑 gen-wiki-board(归档计数随动——不跑则 verify-wiki-consistency 的 summary.archive 比对拦提交)
|
|
169
|
+
const gen = path.join(SCRIPT_DIR, 'gen-wiki-board.mjs');
|
|
170
|
+
if (fs.existsSync(gen)) {
|
|
171
|
+
const r = spawnSync(process.execPath, [gen], { encoding: 'utf8', windowsHide: true });
|
|
172
|
+
if (r.status === 0) console.log('✅ 已重跑 gen-wiki-board.mjs(归档计数随动)');
|
|
173
|
+
else console.error('⚠️ gen-wiki-board.mjs 重跑失败(exit ' + r.status + ')——手工跑:node .agents/scripts/gen-wiki-board.mjs');
|
|
174
|
+
} else {
|
|
175
|
+
console.log('ℹ️ gen-wiki-board.mjs 不存在(旧装户),跳过归档计数随动');
|
|
176
|
+
}
|
|
177
|
+
console.log('\n下一步(人工):读草稿 → 归类到 wiki/<主题>/ → 补主题与用途 → 跑 gen-wiki-board + verify-wiki-consistency');
|
|
178
|
+
}
|