agentic-flow-kit 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +3 -0
  2. package/modules/gates/dotnet-ca/check-architecture.sh +23 -5
  3. package/modules/hosts/omp/agents/implementer.md +47 -6
  4. package/modules/hosts/omp/agents/independent-reviewer.md +44 -5
  5. package/modules/hosts/omp/agents/ui-verifier.md +43 -6
  6. package/modules/hosts/opencode/agents/implementer.md +47 -1
  7. package/modules/hosts/opencode/agents/independent-reviewer.md +44 -1
  8. package/modules/hosts/opencode/agents/ui-verifier.md +43 -1
  9. package/modules/hosts/opencode/commands/build.md +74 -7
  10. package/modules/hosts/opencode/commands/deploy.md +51 -6
  11. package/modules/hosts/opencode/commands/design.md +36 -6
  12. package/modules/hosts/opencode/commands/gate-checklist.md +77 -0
  13. package/modules/hosts/opencode/commands/maintain.md +70 -6
  14. package/modules/hosts/opencode/commands/new-task.md +59 -6
  15. package/modules/hosts/opencode/commands/plan.md +35 -6
  16. package/modules/hosts/opencode/commands/review.md +41 -6
  17. package/modules/hosts/opencode/commands/source-sync-check.md +89 -0
  18. package/modules/hosts/opencode/commands/sync-hosts.md +66 -0
  19. package/modules/hosts/opencode/commands/test.md +58 -8
  20. package/modules/hosts/trae/agents/implementer.md +46 -2
  21. package/modules/hosts/trae/agents/independent-reviewer.md +44 -1
  22. package/modules/hosts/trae/agents/ui-verifier.md +41 -3
  23. package/modules/hosts/trae/commands/wf-build.md +74 -7
  24. package/modules/hosts/trae/commands/wf-deploy.md +51 -6
  25. package/modules/hosts/trae/commands/wf-design.md +36 -6
  26. package/modules/hosts/trae/commands/wf-gate-checklist.md +78 -0
  27. package/modules/hosts/trae/commands/wf-maintain.md +70 -6
  28. package/modules/hosts/trae/commands/wf-new-task.md +59 -6
  29. package/modules/hosts/trae/commands/wf-plan.md +35 -6
  30. package/modules/hosts/trae/commands/wf-review.md +41 -6
  31. package/modules/hosts/trae/commands/wf-source-sync-check.md +90 -0
  32. package/modules/hosts/trae/commands/wf-sync-hosts.md +67 -0
  33. package/modules/hosts/trae/commands/wf-test.md +58 -7
  34. package/modules/hosts/trae/hooks/post-edit-check.cjs +3 -2
  35. package/modules/hosts/trae/hooks/pre-shell-check.cjs +40 -9
  36. package/modules/hosts/trae/hooks.json +1 -1
  37. package/modules/hosts/zcode/agents/implementer.md +47 -6
  38. package/modules/hosts/zcode/agents/independent-reviewer.md +44 -5
  39. package/modules/hosts/zcode/agents/ui-verifier.md +43 -5
  40. package/package.json +6 -3
  41. package/src/add-gate.mjs +5 -3
  42. package/src/cli.mjs +15 -2
  43. package/src/doctor.mjs +245 -15
  44. package/src/gate-dotnet-ca.test.mjs +124 -0
  45. package/src/init.mjs +79 -17
  46. package/src/init.test.mjs +84 -2
  47. package/src/pack.test.mjs +24 -0
  48. package/src/render.mjs +64 -4
  49. package/src/run-tests.mjs +7 -11
  50. package/src/sync-hosts.mjs +194 -0
  51. package/src/sync-hosts.test.mjs +170 -0
  52. package/src/sync.mjs +65 -20
  53. package/src/sync.test.mjs +52 -0
  54. package/src/trae-hooks.test.mjs +115 -0
  55. package/templates/AGENTS.md +2 -1
  56. package/templates/_agents/board/index.html +32 -3
  57. package/templates/_agents/commands/build.md +22 -1
  58. package/templates/_agents/commands/design.md +2 -0
  59. package/templates/_agents/commands/gate-checklist.md +91 -0
  60. package/templates/_agents/commands/maintain.md +2 -3
  61. package/templates/_agents/commands/plan.md +4 -1
  62. package/templates/_agents/commands/review.md +1 -0
  63. package/templates/_agents/commands/source-sync-check.md +103 -0
  64. package/templates/_agents/commands/sync-hosts.md +79 -0
  65. package/templates/_agents/commands/test.md +4 -2
  66. package/templates/_agents/hooks/check-pairing-incremental.sh +20 -7
  67. package/templates/_agents/notes/runtime-env.md +1 -1
  68. package/templates/_agents/roles/implementer.md +2 -0
  69. package/templates/_agents/roles/independent-reviewer.md +2 -0
  70. package/templates/_agents/roles/ui-verifier.md +2 -0
  71. package/templates/_agents/rule-budgets.txt +1 -1
  72. package/templates/_agents/scripts/agg-delegations.cjs +39 -30
  73. package/templates/_agents/scripts/agg-delegations.test.mjs +32 -0
  74. package/templates/_agents/scripts/check-loop.mjs +763 -0
  75. package/templates/_agents/scripts/check-loop.sh +5 -808
  76. package/templates/_agents/scripts/check-loop.test.mjs +1102 -0
  77. package/templates/_agents/scripts/confirm-doc.mjs +228 -0
  78. package/templates/_agents/scripts/confirm-doc.test.mjs +321 -0
  79. package/templates/_agents/scripts/doctor.test.mjs +371 -0
  80. package/templates/_agents/scripts/ensure-board.mjs +30 -3
  81. package/templates/_agents/scripts/fill-intent.mjs +59 -0
  82. package/templates/_agents/scripts/fill-intent.test.mjs +73 -0
  83. package/templates/_agents/scripts/fill-plan.mjs +60 -0
  84. package/templates/_agents/scripts/fill-plan.test.mjs +67 -0
  85. package/templates/_agents/scripts/fill-spec.mjs +52 -0
  86. package/templates/_agents/scripts/fill-spec.test.mjs +61 -0
  87. package/templates/_agents/scripts/gate-checklist.mjs +194 -0
  88. package/templates/_agents/scripts/gate-checklist.test.mjs +204 -0
  89. package/templates/_agents/scripts/gen-wiki-board.mjs +38 -24
  90. package/templates/_agents/scripts/gen-wiki-board.test.mjs +18 -0
  91. package/templates/_agents/scripts/gen-workflow-index.mjs +4 -2
  92. package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -3
  93. package/templates/_agents/scripts/kb-search.mjs +25 -9
  94. package/templates/_agents/scripts/kb-search.test.mjs +38 -8
  95. package/templates/_agents/scripts/source-sync-check.mjs +145 -0
  96. package/templates/_agents/scripts/source-sync-check.test.mjs +244 -0
  97. package/templates/_agents/scripts/verify-wiki-consistency.mjs +46 -23
  98. package/templates/_agents/scripts/verify.mjs +8 -10
  99. package/templates/_agents/scripts/verify.test.mjs +13 -1
  100. package/templates/_agents/scripts/wf-journal.mjs +158 -0
  101. package/templates/_agents/scripts/wf-journal.test.mjs +150 -0
  102. package/templates/_agents/scripts/workflow-board-server.mjs +73 -17
  103. package/templates/_agents/scripts/workflow-board-server.test.mjs +136 -0
  104. package/templates/_agents/scripts/workflow-enums.mjs +70 -0
  105. package/templates/_agents/scripts/workflow-enums.test.mjs +110 -0
  106. package/templates/_agents/scripts/workflows-check.mjs +260 -0
  107. package/templates/_agents/scripts/workflows-check.test.mjs +267 -0
  108. package/templates/_agents/workflow-enums.txt +19 -0
  109. package/templates/_agents/workflows/_TEMPLATE.md +75 -0
  110. package/templates/_agents/workflows/pipeline-closing.md +29 -0
  111. package/templates/_agents/workflows/source-sync-repair.md +30 -0
  112. package/templates/_agents/workflows/steps//347/244/272/344/276/213-/351/203/250/347/275/262/351/252/214/350/257/201.md +10 -0
  113. package/templates/_agents/workflows//347/244/272/344/276/213-/345/271/266/350/241/214/345/256/236/347/216/260/350/257/204/345/256/241.md +16 -0
  114. package/templates/_githooks/pre-commit +16 -1
  115. package/templates/workflow/README.md +1 -1
  116. package/templates/_agents/cache/kb-index.json +0 -1
  117. package/templates/_agents/scripts/check-loop.test.sh +0 -1042
package/src/cli.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // flow-kit CLI 入口:init(安装)/ sync(升级)/ add-host(补宿主)/ add-gate(装门禁)/ doctor(体检)/ help
2
+ // flow-kit CLI 入口:init(安装)/ sync(升级)/ add-host(补宿主)/ add-gate(装门禁)/ doctor(体检)/ sync-hosts(跨宿主同步)/ help
3
3
  import { init } from './init.mjs';
4
4
  import { doctor } from './doctor.mjs';
5
5
  import { sync } from './sync.mjs';
6
+ import { syncHosts } from './sync-hosts.mjs';
6
7
  import { addHost } from './add-host.mjs';
7
8
  import { addGate } from './add-gate.mjs';
8
9
  import { readFileSync } from 'node:fs';
@@ -21,7 +22,11 @@ export const HELP = `flow-kit — AI-Native 闭环工作流 + wiki 知识层脚
21
22
  owned 文件永不触碰)
22
23
  flow-kit add-host <宿主> 后补宿主适配层:zcode | opencode | trae | omp(已存在文件跳过且不入台账,--force 接管)
23
24
  flow-kit add-gate <门禁> 装门禁模块并接线 local-pre-commit(当前:dotnet-ca;装后归项目所有)
24
- flow-kit doctor 体检:目录布局 / git 钩子 / managed 清单 / 索引漂移 / check-loop
25
+ flow-kit sync-hosts 跨宿主适配层同步:templates/_agents/{commands,roles}/*.md 权威源 vs
26
+ modules/hosts/<h>/{agents,commands}/*.md 薄适配——默认 --diff 输出报告;
27
+ --apply 单向同步(权威→薄适配,不反向避免污染);
28
+ 改一处权威源要同步 N 份薄适配,人工 grep 易漏
29
+ flow-kit doctor 体检:目录布局 / git 钩子 / managed 清单 / 索引漂移 / 跨宿主漂移 / check-loop
25
30
 
26
31
  init 选项(全部可选,均有默认值):
27
32
  --stack <技术栈> dotnet | node(ts/js) | python | go | none(默认 none;ts/js/typescript/javascript
@@ -35,6 +40,7 @@ init 选项(全部可选,均有默认值):
35
40
  --force 覆盖已存在的同名文件(默认保守跳过)
36
41
 
37
42
  sync / add-host 选项:--dir <目录>、--force(覆盖本地已改 / 已装内容);add-gate 选项:--dir <目录>、--force。
43
+ sync-hosts 选项:--diff(默认,仅报告,不修改)/ --apply(单向同步到薄适配)/ --json(机器可读输出)。
38
44
 
39
45
  示例:
40
46
  npx agentic-flow-kit init # 交互模式:宿主 → 技术栈 → 端口 → 确认安装
@@ -42,6 +48,8 @@ sync / add-host 选项:--dir <目录>、--force(覆盖本地已改 / 已装
42
48
  npx agentic-flow-kit sync # 包出新版后升级(本地改过的 managed 文件会跳过并报告)
43
49
  npx agentic-flow-kit add-host opencode # 后补宿主
44
50
  npx agentic-flow-kit add-gate dotnet-ca # 装 Clean Architecture 门禁
51
+ npx agentic-flow-kit sync-hosts --diff # 看权威源 vs 薄适配漂移
52
+ npx agentic-flow-kit sync-hosts --apply # 单向同步到薄适配
45
53
 
46
54
  装完即自包含:项目不依赖本包运行;AGENTS.md 生成「AI工作流 + Wiki」骨架,项目细节在「项目适配区」自填。`;
47
55
 
@@ -67,6 +75,11 @@ export function run(argv) {
67
75
  sync(argv.slice(1), PKG_ROOT);
68
76
  return;
69
77
  }
78
+ if (cmd === 'sync-hosts') {
79
+ const code = syncHosts(argv.slice(1), PKG_ROOT);
80
+ if (typeof code === 'number') process.exit(code);
81
+ return;
82
+ }
70
83
  if (cmd === 'add-host') {
71
84
  addHost(argv.slice(1), PKG_ROOT);
72
85
  return;
package/src/doctor.mjs CHANGED
@@ -4,6 +4,11 @@ import path from 'node:path';
4
4
  import { execSync, spawnSync } from 'node:child_process';
5
5
  import net from 'node:net';
6
6
  import { createHash } from 'node:crypto';
7
+ import { listTree, scriptTrusted } from './render.mjs';
8
+ import { isOwned, HOSTS, pickStackVars } from './profiles.mjs';
9
+
10
+ // 宿主目录映射(与 src/profiles.mjs#HOSTS 同源:装副本目录名前缀带点)
11
+ const HOST_DIR = Object.fromEntries(Object.entries(HOSTS).map(([k, v]) => [k, v.dir]));
7
12
 
8
13
  function sh(cmd, cwd) {
9
14
  try {
@@ -50,8 +55,12 @@ export function doctor(args, pkgRoot) {
50
55
  '.agents/board/index.html',
51
56
  '.agents/hooks/commit-check.cjs',
52
57
  '.agents/workflow-modules.txt',
58
+ '.agents/workflow-enums.txt',
53
59
  '.agents/rule-budgets.txt',
54
60
  '.agents/settings.json',
61
+ '.agents/workflows/_TEMPLATE.md',
62
+ '.agents/scripts/check-loop.mjs',
63
+ '.agents/scripts/confirm-doc.mjs',
55
64
  '.githooks/pre-commit',
56
65
  '.githooks/pre-push',
57
66
  '.githooks/commit-msg',
@@ -72,7 +81,20 @@ export function doctor(args, pkgRoot) {
72
81
  const isRepo = sh('git rev-parse --git-dir', target).ok;
73
82
  if (isRepo) {
74
83
  const hp = sh('git config core.hooksPath', target);
75
- if (hp.ok && hp.out.trim() === '.githooks') add('PASS', 'core.hooksPath=.githooks(钩子已挂载)');
84
+ if (hp.ok && hp.out.trim() === '.githooks') {
85
+ add('PASS', 'core.hooksPath=.githooks(钩子已挂载)');
86
+ // 执行位检查(init-p1-batch P1-1):POSIX 上 git 对不可执行钩子静默跳过、全部门禁失效且原不可发现;
87
+ // win32 无执行位语义(mode 恒 0666 系),跳过本检查
88
+ if (process.platform !== 'win32') {
89
+ try {
90
+ const hookDir = path.join(target, '.githooks');
91
+ const noExec = fs.readdirSync(hookDir)
92
+ .filter((f) => fs.statSync(path.join(hookDir, f)).isFile())
93
+ .filter((f) => !(fs.statSync(path.join(hookDir, f)).mode & 0o111));
94
+ if (noExec.length) add('FAIL', `钩子缺执行位(POSIX 上 git 静默跳过不可执行钩子,门禁全部失效):${noExec.join('、')}——执行 chmod +x .githooks/*`);
95
+ } catch { /* .githooks 目录缺失由 §2 布局检查覆盖 */ }
96
+ }
97
+ }
76
98
  else add('FAIL', `core.hooksPath 当前为「${(hp.ok ? hp.out : '').trim() || '未设置'}」——执行 git config core.hooksPath .githooks`);
77
99
  } else {
78
100
  add('WARN', '不在 git 仓库内——钩子未挂载(git init 后执行 git config core.hooksPath .githooks)');
@@ -91,7 +113,7 @@ export function doctor(args, pkgRoot) {
91
113
  for (const f of kit.managed || []) {
92
114
  const p = path.join(target, f.rel);
93
115
  if (!fs.existsSync(p)) { gone.push(f.rel); continue; }
94
- const h = createHash('sha256').update(fs.readFileSync(p)).digest('hex');
116
+ const h = createHash('sha256').update(fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'), 'utf8').digest('hex'); // LF 归一(closing-coverage:跨 checkout 稳定,同 sync shaText 口径)
95
117
  if (h === f.sha256) ok++; else modified++;
96
118
  }
97
119
  add('PASS', `kit.json v${kit.version}:managed ${ok + modified} 份校验通过`);
@@ -102,6 +124,35 @@ export function doctor(args, pkgRoot) {
102
124
  }
103
125
  }
104
126
 
127
+ // 4.5 台账覆盖率(2026-09-26 managed-ledger-adopt):§4 只遍历台账条目做双向校验,
128
+ // 从不反向问「盘上还有谁没登记」——15 份 managed 类文件曾因此长期游离台账外,sync 只报不修。
129
+ // 本项由 pkgRoot 包源枚举应有 managed 清单(排除 isOwned)与台账取差集,补上「盘上有、台账无」一侧。
130
+ // 首次引入用 WARN:doctor 的 pkgRoot 是包安装目录,装户可能落后于包版本(包内新增 managed 件
131
+ // 在装户 sync 前必然未登记),直接 FAIL 会让存量装户升级包后一跑 doctor 即挂。
132
+ // 沿用本仓库三次先例(doctor-owned-drift / workflows-check / adapter 薄适配漂移均先 WARN)。
133
+ {
134
+ const covRes = checkLedgerCoverage(target, pkgRoot);
135
+ if (covRes.skipped) {
136
+ add('PASS', `台账覆盖率检查跳过(${covRes.note || '无包源模板'})`);
137
+ } else if (covRes.missing.length === 0) {
138
+ add('PASS', `台账覆盖率完整(${covRes.total} 份 managed 全在册)`);
139
+ } else {
140
+ const shown = covRes.missing.slice(0, 5).join('、');
141
+ add('WARN', `台账覆盖率缺口 ${covRes.missing.length} 份——盘上 managed 类文件未登记进 kit.json,包源改动不会同步到装副本;跑 node bin/flow-kit.mjs sync 收养(内容等于新版才登记)${covRes.missing.length > 5 ? `:${shown} …` : `:${shown}`}`);
142
+ }
143
+ }
144
+
145
+ // scriptGuard(init-p1-batch P1-3 供应链防线):spawn 目标侧脚本前校验其与包源渲染值(LF 归一)一致——
146
+ // 失配说明预置/被改动,不可信:调用方记 WARN 跳过(fail-visible,不执行也不下通过结论)。
147
+ // 无 pkgRoot(理论路径)不设防;vars 从 kit.json options 重建(与 sync 同源构造)。
148
+ const scriptGuard = (rel) => {
149
+ if (!pkgRoot) return { ok: true, note: '' };
150
+ let opts = null;
151
+ try { opts = JSON.parse(fs.readFileSync(kitPath, 'utf8')).options; } catch { /* 无/坏台账→用默认 vars */ }
152
+ const vars = { BOARD_PORT: String(opts?.boardPort || '8933'), ...pickStackVars(opts?.stack) };
153
+ return scriptTrusted({ pkgRoot, target, rel, vars });
154
+ };
155
+
105
156
  // 5. 占位符残留
106
157
  const phFiles = ['AGENTS.md', 'workflow/README.md', '.agents/notes/runtime-env.md']
107
158
  .concat(fs.existsSync(path.join(target, '.agents/commands')) ? fs.readdirSync(path.join(target, '.agents/commands')).filter((f) => f.endsWith('.md')).map((f) => `.agents/commands/${f}`) : []);
@@ -116,25 +167,91 @@ export function doctor(args, pkgRoot) {
116
167
  else add('WARN', `占位符残留 ${phHits.length} 处(init 变量缺失或需手改):${phHits.slice(0, 5).join(';')}${phHits.length > 5 ? ' …' : ''}`);
117
168
 
118
169
  // 6. workflow 索引漂移
119
- const idx = spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs', '--check'], { cwd: target, encoding: 'utf8' });
120
- if (idx.status === 0) add('PASS', 'workflow/INDEX.md 无漂移');
121
- else add('WARN', `workflow/INDEX.md 漂移——跑 node .agents/scripts/gen-workflow-index.mjs 重生成`);
170
+ const idxGuard = scriptGuard('.agents/scripts/gen-workflow-index.mjs');
171
+ if (!idxGuard.ok) {
172
+ add('WARN', `workflow/INDEX.md 漂移检查跳过——${idxGuard.note}(供应链防线)`);
173
+ } else {
174
+ const idx = spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs', '--check'], { cwd: target, encoding: 'utf8' });
175
+ if (idx.status === 0) add('PASS', 'workflow/INDEX.md 无漂移');
176
+ else add('WARN', `workflow/INDEX.md 漂移——跑 node .agents/scripts/gen-workflow-index.mjs 重生成`);
177
+ }
122
178
 
123
179
  // 6.5 delegations 台账结构(量化层非门禁:结构漂移曾静默吞掉全部记录,2026-09-24)
180
+ // 存在性先行(复核 P2-2):旧装户无此脚本 = 既有「静默跳过」语义,不产生防线 WARN 噪音
124
181
  if (fs.existsSync(path.join(target, '.agents/scripts/agg-delegations.cjs'))) {
125
- const agg = spawnSync(process.execPath, ['.agents/scripts/agg-delegations.cjs'], { cwd: target, encoding: 'utf8' });
126
- if (agg.status === 0) add('PASS', 'delegations 台账结构有效(agg 可解析)');
127
- else add('WARN', `delegations 台账结构漂移——${String(agg.stderr || agg.stdout || '').split('\n')[0]}`);
182
+ const aggGuard = scriptGuard('.agents/scripts/agg-delegations.cjs');
183
+ if (!aggGuard.ok) {
184
+ add('WARN', `delegations 台账结构检查跳过——${aggGuard.note}(供应链防线)`);
185
+ } else {
186
+ const agg = spawnSync(process.execPath, ['.agents/scripts/agg-delegations.cjs'], { cwd: target, encoding: 'utf8' });
187
+ if (agg.status === 0) add('PASS', 'delegations 台账结构有效(agg 可解析)');
188
+ else add('WARN', `delegations 台账结构漂移——${String(agg.stderr || agg.stdout || '').split('\n')[0]}`);
189
+ }
190
+ }
191
+
192
+ // 6.6 owned 漂移校验(kit.owned 列表盘面 sha 不一致;engine 双源纪律对 owned 走「项目自持 + 哈希记账不约束」,
193
+ // 漂移信号靠本校验给装户可见性。2026-09-25 wf-runtime 复盘:包源改了装副本未同步 = 漂移但 sync 不报。)
194
+ const ownedRes = checkOwnedDrift(target);
195
+ if (ownedRes.error) {
196
+ add('WARN', `owned 校验跳过(kit.json 解析失败:${ownedRes.error})`);
197
+ } else if (ownedRes.skipped) {
198
+ add('PASS', 'owned 校验跳过(kit.json 无 owned 条目)');
199
+ } else if (ownedRes.total === 0) {
200
+ add('PASS', 'owned 校验跳过(kit.json 无 owned 条目)');
201
+ } else if (ownedRes.drift === 0 && ownedRes.gone.length === 0) {
202
+ add('PASS', `owned ${ownedRes.total} 份无漂移(装副本与台账 sha 对齐)`);
203
+ } else {
204
+ // owned 漂移已严化为 FAIL(2026-09-25 doctor-owned-drift-strict 复盘;首次引入用 WARN,installed 装户吃过警告后升级)
205
+ if (ownedRes.drift) add('FAIL', `owned 漂移 ${ownedRes.drift} 份——装副本手改未跑 sync 刷台账 / 包源改了装副本未同步;须手动同步装副本后跑 node bin/flow-kit.mjs sync 刷台账(见 incidents/2026-09-25-wf-runtime 复盘)`);
206
+ if (ownedRes.gone.length) add('FAIL', `owned 文件缺失:${ownedRes.gone.join('、')}——sync 恢复或手动恢复`);
207
+ }
208
+
209
+ // 6.7 跨宿主薄适配正文段漂移校验(2026-09-25 cross-host-sync,参见 workflows/intents/2026-09-25-cross-host-sync.md)
210
+ // 装户侧视角:.agents/{commands,roles}/*.md 是权威源;4 宿主目录(.zcode/.omp/.opencode/.trae)下的
211
+ // {agents,commands}/*.md 是薄适配。按正文段 sha 比对(B-b 语义,frontmatter 不计入漂移),
212
+ // trae commands 加 wf- 前缀映射。首次引入为 WARN(沿用 wf-runtime 复盘「先 WARN 升级 FAIL」路径),
213
+ // 待装户升级视反馈升级 FAIL。包源侧对应工具是 flow-kit sync-hosts(src/sync-hosts.mjs)。
214
+ const adapterRes = checkAdapterDrift(target);
215
+ if (adapterRes.skipped) {
216
+ add('PASS', `跨宿主薄适配校验跳过(${adapterRes.note || '权威源目录不存在'})`);
217
+ } else if (adapterRes.drift === 0) {
218
+ add('PASS', `跨宿主薄适配 ${adapterRes.total} 对无正文漂移(装副本引擎 vs 4 宿主薄适配,B-b 语义)`);
219
+ } else {
220
+ add('WARN', `跨宿主薄适配正文漂移 ${adapterRes.drift} 对——权威源与薄适配正文段 sha 不一致;按 .agents/commands/sync-hosts.md 跑 flow-kit sync-hosts --apply 单向同步薄适配正文(frontmatter 不动);装户可在 bin/flow-kit.mjs sync 时一并修复`);
221
+ }
222
+
223
+ // 6.8 workflows 编排脚本 lint(stages 表解析校验:role/step/after 引用、无环、三形态互斥、数值枚举;
224
+ // 2026-09-25 workflows-linter 把 _TEMPLATE.md「解析校验先行」从 prose 变机器门。首次引入 WARN
225
+ // (沿用 wf-runtime 复盘「先 WARN 升级 FAIL」渐进路径),装户吃过警告后可升 FAIL。
226
+ // 旧版装户 sync 前 .agents/scripts/workflows-check.mjs 不存在 → 自然跳过不误报)
227
+ // 6.8 workflows 编排脚本 lint(stages 表解析校验:role/step/after 引用、无环、三形态互斥、数值枚举;
228
+ // 2026-09-25 workflows-linter 把 _TEMPLATE.md「解析校验先行」从 prose 变机器门。首次引入 WARN
229
+ // (沿用 wf-runtime 复盘「先 WARN 升级 FAIL」渐进路径),装户吃过警告后可升 FAIL。
230
+ // 旧版装户 sync 前 .agents/scripts/workflows-check.mjs 不存在 → 既有「静默跳过」语义保留(存在性先行,
231
+ // 复核 P2-2),防线只拦「存在但与包源不符」)
232
+ const wfExists = fs.existsSync(path.join(target, '.agents/scripts/workflows-check.mjs')) && fs.existsSync(path.join(target, '.agents/workflows'));
233
+ const wfGuard = wfExists ? scriptGuard('.agents/scripts/workflows-check.mjs') : { ok: true, note: '' };
234
+ if (wfExists && !wfGuard.ok) {
235
+ add('WARN', `workflows 编排脚本 lint 跳过——${wfGuard.note}(供应链防线)`);
236
+ } else if (wfExists) {
237
+ const wf = spawnSync(process.execPath, ['.agents/scripts/workflows-check.mjs'], { cwd: target, encoding: 'utf8' });
238
+ if (wf.status === 0) {
239
+ const warnN = (String(wf.stdout || '').match(/- ⚠️ W\d/g) || []).length;
240
+ add('PASS', `workflows 编排脚本 lint 干净${warnN ? `(${warnN} 条 advisory 告警)` : ''}`);
241
+ } else {
242
+ const firstHit = String(wf.stdout || wf.stderr || '').split('\n').find((l) => / E\d/.test(l)) || `exit ${wf.status}`;
243
+ add('WARN', `workflows 编排脚本 lint 未过——${firstHit.trim()};修 .agents/workflows/ 编排脚本(role/step/after 引用、三形态、retries)后重跑`);
244
+ }
128
245
  }
129
246
 
130
- // 7. check-loop——先探 sh 可用性(对齐 run-tests.mjs 先例):Windows PowerShell 常无 sh,
131
- // ENOENT 曾被吞进 hard-block 分支报成空原因假警报(incident 2026-09-24-doctor-sh-enoent)。
132
- // git 钩子门禁不受此影响——git 以自带 sh 执行钩子,与用户 PATH 无关。
133
- const shProbe = spawnSync('sh', ['-c', 'true'], { encoding: 'utf8' });
134
- if (shProbe.status !== 0) {
135
- add('WARN', '环境无 sh——check-loop 未跑,勿当作通过;装 Git Bash / WSL 后重跑 doctor(git 钩子门禁不受影响,git 以自带 sh 执行)');
247
+ // 7. check-loop——2026-09-26 check-loop-node 起 node 实现直跑(sh 版曾需先探 sh 可用性:Windows
248
+ // PowerShell 常无 sh,ENOENT 曾被吞进 hard-block 分支报成空原因假警报,incident 2026-09-24-doctor-sh-enoent;
249
+ // 迁移后无 sh 依赖,探针退役。check-loop.sh 为兼容 shim,pre-push 钩子路径照常)
250
+ const clGuard = scriptGuard('.agents/scripts/check-loop.mjs');
251
+ if (!clGuard.ok) {
252
+ add('WARN', `check-loop 跳过——${clGuard.note}(供应链防线:目标侧脚本与包源渲染值不符,不执行不下结论)`);
136
253
  } else {
137
- const cl = spawnSync('sh', ['.agents/scripts/check-loop.sh'], { cwd: target, encoding: 'utf8' });
254
+ const cl = spawnSync(process.execPath, ['.agents/scripts/check-loop.mjs'], { cwd: target, encoding: 'utf8' });
138
255
  if (cl.status === 0) {
139
256
  const warnTxt = String(cl.stderr || '').trim();
140
257
  add('PASS', `check-loop 干净${warnTxt ? `(${warnTxt.split('\n').filter((l) => l.includes('WARN')).length} 条 advisory 警告)` : ''}`);
@@ -162,3 +279,116 @@ function print(results) {
162
279
  const warns = results.filter((r) => r.level === 'WARN').length;
163
280
  console.log(`\ndoctor:${results.length - fails - warns} PASS | ${warns} WARN | ${fails} FAIL`);
164
281
  }
282
+
283
+ export function checkLedgerCoverage(target, pkgRoot) {
284
+ const kitPath = path.join(target, '.agents', 'kit.json');
285
+ if (!fs.existsSync(kitPath)) return { missing: [], total: 0, skipped: true, note: 'kit.json 不存在' };
286
+ let kit;
287
+ try {
288
+ kit = JSON.parse(fs.readFileSync(kitPath, 'utf8'));
289
+ } catch (e) {
290
+ return { missing: [], total: 0, skipped: true, note: `kit.json 解析失败:${e.message}` };
291
+ }
292
+ if (!Array.isArray(kit.managed)) return { missing: [], total: 0, skipped: true, note: 'kit.json 无 managed 条目' };
293
+ // 无包源 = 装户环境:盘上没有 templates/,无法枚举应有清单,跳过
294
+ const tmplRoot = path.join(pkgRoot || '', 'templates');
295
+ if (!pkgRoot || !fs.existsSync(tmplRoot)) return { missing: [], total: 0, skipped: true, note: '无包源模板(templates/ 不存在)' };
296
+
297
+ // 应有清单 = 模板树 + 台账宿主层,排除 owned(与 src/sync.mjs 的 fresh 计算口径一致)
298
+ const expected = new Set(listTree(tmplRoot));
299
+ const hosts = Array.isArray(kit.options?.hosts) ? kit.options.hosts : [];
300
+ for (const h of hosts) {
301
+ if (!HOST_DIR[h]) continue;
302
+ const hostRoot = path.join(pkgRoot, 'modules', 'hosts', h);
303
+ if (!fs.existsSync(hostRoot)) continue;
304
+ for (const rel of listTree(hostRoot)) expected.add(`${HOST_DIR[h]}/${rel}`);
305
+ }
306
+ const ledger = new Set(kit.managed.map((f) => f.rel));
307
+ const missing = [...expected].filter((rel) => !ledger.has(rel) && !isOwned(rel)).sort();
308
+ return { missing, total: ledger.size, skipped: false };
309
+ }
310
+
311
+ // owned 漂移校验(独立 export 供 doctor 主流程 + 单元测试共用;2026-09-25 wf-runtime 复盘)
312
+ // 返回:{ drift, gone, total, skipped, error? }
313
+ // - skipped=true:kit.json 不存在或解析失败/无 owned 字段
314
+ // - drift:盘面 sha != kit.owned sha 的文件数
315
+ // - gone:盘面缺失的文件 rel 列表
316
+ // - total:kit.owned 列表总文件数
317
+ export function checkOwnedDrift(target) {
318
+ const kitPath = path.join(target, '.agents', 'kit.json');
319
+ if (!fs.existsSync(kitPath)) return { drift: 0, gone: [], total: 0, skipped: true };
320
+ let kit;
321
+ try {
322
+ kit = JSON.parse(fs.readFileSync(kitPath, 'utf8'));
323
+ } catch (e) {
324
+ return { drift: 0, gone: [], total: 0, skipped: true, error: e.message };
325
+ }
326
+ const owned = Array.isArray(kit.owned) ? kit.owned : [];
327
+ if (owned.length === 0) return { drift: 0, gone: [], total: 0, skipped: true };
328
+ let drift = 0;
329
+ const gone = [];
330
+ for (const f of owned) {
331
+ const p = path.join(target, f.rel);
332
+ if (!fs.existsSync(p)) { gone.push(f.rel); continue; }
333
+ // sha 按 LF 归一口径(同 rule-budgets「索引/HEAD 字节」约定):盘面 CRLF(autocrlf 检出)与克隆检出 LF
334
+ // 须同值,否则跨环境必漂移(CI 克隆 §6.6 必红——2026-09-27 gate-coverage 复核 P1-1 根因;owned 均为文本件)
335
+ const h = createHash('sha256').update(fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'), 'utf8').digest('hex');
336
+ if (h !== f.sha256) drift++;
337
+ }
338
+ return { drift, gone, total: owned.length, skipped: false };
339
+ }
340
+
341
+ // 跨宿主薄适配正文段漂移校验(独立 export 供 doctor 主流程 + 单元测试共用;2026-09-25 cross-host-sync)
342
+ // 装户侧视角:.agents/{commands,roles}/*.md 是权威源;4 宿主目录(zcode→.zcode、omp→.omp、
343
+ // opencode→.opencode、trae→.trae)下 {agents,commands}/*.md 是薄适配。按正文段 sha 比对
344
+ // (B-b 语义;frontmatter 不计入漂移);trae commands 加 wf- 前缀映射。
345
+ // 返回:{ drift, total, skipped, note? }
346
+ // - skipped=true:权威源目录不存在 / 包源环境 / 无 .md 文件
347
+ // - drift:权威源正文段与薄适配正文段 sha 不一致的对数
348
+ // - total:参与比对的对数(权威源文件 × 适配数)
349
+ export function checkAdapterDrift(target) {
350
+ const authorityRoot = path.join(target, '.agents');
351
+ if (!fs.existsSync(authorityRoot)) return { drift: 0, total: 0, skipped: true, note: '.agents 目录不存在' };
352
+ // 包源环境检测:包源仓库同时含 templates/ 与 modules/,装户不应有;包源下跑 doctor §7.x 永远是
353
+ // drift(装副本是历史 init 渲染产物,不会随包源改动而重渲),无信息价值——直接 skip
354
+ if (fs.existsSync(path.join(target, 'templates', '_agents')) && fs.existsSync(path.join(target, 'modules', 'hosts'))) {
355
+ return { drift: 0, total: 0, skipped: true, note: '包源环境(templates/ + modules/ 同时存在)——§7.x 仅在装户环境有意义' };
356
+ }
357
+ // 4 宿主目录映射(与顶层 HOST_DIR 同源)
358
+ const splitFm = (text) => {
359
+ const m = String(text || '').match(/^---\r?\n([\s\S]*?\r?\n)---\r?\n?([\s\S]*)$/);
360
+ if (!m) return { fm: '', body: String(text || '') };
361
+ return { fm: `---\n${m[1]}---\n`, body: m[2] };
362
+ };
363
+ const bodySha = (p) => {
364
+ // 正文段 LF 归一后哈希(closing-coverage:与 sync-hosts bodySha 同口径,CRLF/LF 盘面不误报漂移)
365
+ try { return createHash('sha256').update(splitFm(fs.readFileSync(p, 'utf8')).body.replace(/\r\n/g, '\n'), 'utf8').digest('hex'); } catch { return null; }
366
+ };
367
+ let total = 0;
368
+ let drift = 0;
369
+ const listMd = (dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory() ? fs.readdirSync(dir).filter((f) => f.endsWith('.md')) : [];
370
+ for (const sub of ['commands', 'roles']) {
371
+ const subAbs = path.join(authorityRoot, sub);
372
+ for (const f of listMd(subAbs)) {
373
+ const authRel = `${sub}/${f}`;
374
+ const authSha = bodySha(path.join(authorityRoot, authRel));
375
+ const name = f.slice(0, -'.md'.length);
376
+ // 构建映射:commands/* → opencode/commands/<name>.md + trae/commands/wf-<name>.md;roles/* → 4 宿主 agents/<name>.md
377
+ const targets = [];
378
+ if (sub === 'commands') {
379
+ targets.push([HOST_DIR.opencode, 'commands', `${name}.md`], [HOST_DIR.trae, 'commands', `wf-${name}.md`]);
380
+ } else {
381
+ for (const h of Object.keys(HOST_DIR)) targets.push([HOST_DIR[h], 'agents', `${name}.md`]);
382
+ }
383
+ for (const [hostDir, subDir, fileName] of targets) {
384
+ const adapterAbs = path.join(target, hostDir, subDir, fileName);
385
+ const adapterSha = bodySha(adapterAbs);
386
+ if (adapterSha === null) continue; // 缺失不在 drift 统计(apply 不自动创建)
387
+ total++;
388
+ if (authSha !== adapterSha) drift++;
389
+ }
390
+ }
391
+ }
392
+ if (total === 0) return { drift: 0, total: 0, skipped: true, note: '权威源无 commands/roles .md 文件' };
393
+ return { drift, total, skipped: false };
394
+ }
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ // gate-dotnet-ca.test.mjs — dotnet-ca 门禁模块判定回归(2026-09-27 host-gates-p1,incident 三件套「防复发验证」落点)
3
+ // 方法:fixture 临时 git 仓构造 CONFIG 指向的最小目录树,复制包源 check-architecture.sh 并按场景改 CONFIG,
4
+ // spawn sh 跑门禁断言 exit 码与输出。三场景对位三个 P1:
5
+ // ①豁免清单多值生效(P1-B1:旧 tr ':' '|' 造 "A|B" BRE 字面量永不命中)
6
+ // ②CONTROLLERS_DIR 缺失 fail-closed(P1-B3:旧 2>/dev/null 静默放行)
7
+ // ③csproj 缺失 fail-closed(随 B3:旧 glob 展开失败静默放行)
8
+ // 用法:node src/gate-dotnet-ca.test.mjs(npm test 随跑)
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { spawnSync } from 'node:child_process';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ let pass = 0;
16
+ let fail = 0;
17
+ const check = (desc, cond, detail = '') => {
18
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
19
+ else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
20
+ };
21
+
22
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+ const GATE_SRC = path.join(PKG_ROOT, 'modules', 'gates', 'dotnet-ca', 'check-architecture.sh');
24
+
25
+ // mkfixture:最小 dotnet 目录树(Application/API/Domain/Infrastructure/Controllers)+ 可覆盖 CONFIG
26
+ function mkfixture(configOverride = {}) {
27
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gate-ca-test-'));
28
+ const dirs = {
29
+ APP_DIR: 'backend/src/App.Application',
30
+ API_DIR: 'backend/src/App.API',
31
+ DBCTX_FILE: 'backend/src/App.Infrastructure/Data/AppDbContext.cs',
32
+ DOMAIN_ENTITIES_DIR: 'backend/src/App.Domain/Entities',
33
+ CONTROLLERS_DIR: 'backend/src/App.API/Controllers',
34
+ };
35
+ for (const rel of Object.values(dirs)) fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); // 文件型键取父目录;目录型键随后显式建
36
+ fs.mkdirSync(path.join(root, dirs.APP_DIR), { recursive: true });
37
+ fs.mkdirSync(path.join(root, dirs.API_DIR), { recursive: true });
38
+ fs.mkdirSync(path.join(root, dirs.DOMAIN_ENTITIES_DIR), { recursive: true });
39
+ fs.mkdirSync(path.join(root, dirs.CONTROLLERS_DIR), { recursive: true });
40
+ fs.writeFileSync(path.join(root, dirs.DBCTX_FILE), 'class AppDbContext { }\n');
41
+ const cfg = {
42
+ ...Object.fromEntries(Object.entries(dirs).map(([k, v]) => [k, v])),
43
+ DBCTX_CLASS: 'AppDbContext',
44
+ INFRA_USING: 'using App.Infrastructure',
45
+ EXEMPT_CTRL: 'ExemptedA:ExemptedB',
46
+ SHARED_TYPES: '',
47
+ ...configOverride,
48
+ };
49
+ // 包源脚本 CONFIG 段替换:按「^KEY=" 任意到行尾」整行重写(值含 / 且行尾带注释——不用值内正则);
50
+ // EXEMPT_CTRL/SHARED_TYPES 行尾有注释,重写时保留注释语义(测试值自带说明则省略)
51
+ let script = fs.readFileSync(GATE_SRC, 'utf8');
52
+ for (const [k, v] of Object.entries(cfg)) {
53
+ script = script.replace(new RegExp(`^${k}=.*$`, 'm'), `${k}="${v}"`);
54
+ }
55
+ const scriptPath = path.join(root, 'check-architecture.sh');
56
+ fs.writeFileSync(scriptPath, script);
57
+ // fixture 需为 git 仓(脚本首行 git rev-parse --show-toplevel)
58
+ spawnSync('git', ['init', '-q', '.'], { cwd: root });
59
+ return { root, scriptPath, dirs };
60
+ }
61
+ const runGate = (root, scriptPath) => spawnSync('sh', [scriptPath], { cwd: root, encoding: 'utf8' });
62
+ // ctrlWrite:往 fixture 的 Controllers 目录写文件(目录可能尚未创建)
63
+ const ctrlWrite = (root, dirs, name, content) => {
64
+ fs.mkdirSync(path.join(root, dirs.CONTROLLERS_DIR), { recursive: true });
65
+ fs.writeFileSync(path.join(root, dirs.CONTROLLERS_DIR, name), content);
66
+ };
67
+ // addCsproj:补干净 csproj(fail-closed 后红线 1 先查 csproj——不写则走不到目标断言)
68
+ const addCsproj = (root, dirs) => fs.writeFileSync(path.join(root, dirs.APP_DIR, 'App.Application.csproj'), '<Project Sdk="Microsoft.NET.Sdk" />\n');
69
+
70
+ // ---- ① 豁免清单多值(P1-B1)----
71
+ {
72
+ const { root, scriptPath, dirs } = mkfixture();
73
+ addCsproj(root, dirs);
74
+ // API 层两处 DbContext 引用:一个在豁免 Controller 文件、一个在普通 Controller——豁免只应放过前者
75
+ ctrlWrite(root, dirs, 'ExemptedAController.cs', '// AppDbContext 直接引用(豁免)\n');
76
+ ctrlWrite(root, dirs, 'OrdersController.cs', '// AppDbContext 直接引用(应拦)\n');
77
+ const r = runGate(root, scriptPath);
78
+ check('①多值豁免生效:ExemptedA 文件行不出现、Orders 行出现(违例 exit 1)',
79
+ r.status === 1 && !r.stderr.includes('ExemptedAController') && r.stderr.includes('OrdersController'),
80
+ `exit=${r.status}\n${r.stdout}\n${r.stderr}`);
81
+ fs.rmSync(root, { recursive: true, force: true });
82
+ }
83
+ // ①附:干净树(豁免命中全部 DbContext 引用)→ 红线 3 不拦(整体 exit 0 需其余红线干净)
84
+ {
85
+ const { root, scriptPath, dirs } = mkfixture();
86
+ addCsproj(root, dirs);
87
+ ctrlWrite(root, dirs, 'ExemptedAController.cs', '// AppDbContext\n');
88
+ ctrlWrite(root, dirs, 'ExemptedBController.cs', '// AppDbContext\n');
89
+ const r = runGate(root, scriptPath);
90
+ check('①两个豁免 Controller 全命中 → 红线 3 零输出 exit 0', r.status === 0 && !r.stderr, `exit=${r.status}\n${r.stderr}`);
91
+ fs.rmSync(root, { recursive: true, force: true });
92
+ }
93
+
94
+ // ---- ② CONTROLLERS_DIR 缺失 fail-closed(P1-B3)----
95
+ {
96
+ const { root, scriptPath, dirs } = mkfixture({ CONTROLLERS_DIR: 'backend/src/App.API/NotThere' });
97
+ addCsproj(root, dirs);
98
+ const r = runGate(root, scriptPath);
99
+ check('②Controllers 目录缺失 → fail-closed exit 1 点名目标(旧代码静默放行)',
100
+ r.status === 1 && r.stderr.includes('NotThere') && r.stderr.includes('fail-closed'),
101
+ `exit=${r.status}\n${r.stderr}`);
102
+ fs.rmSync(root, { recursive: true, force: true });
103
+ }
104
+
105
+ // ---- ③ csproj 缺失 fail-closed(随 B3)----
106
+ {
107
+ const { root, scriptPath } = mkfixture();
108
+ // Application 目录存在但无 .csproj(glob 展开失败形态)
109
+ const r = runGate(root, scriptPath);
110
+ check('③Application 无 csproj → fail-closed exit 1(旧代码静默放行)',
111
+ r.status === 1 && r.stderr.includes('csproj') && r.stderr.includes('fail-closed'),
112
+ `exit=${r.status}\n${r.stderr}`);
113
+ fs.rmSync(root, { recursive: true, force: true });
114
+ }
115
+ // ③附:有 csproj 且干净 → 过红线 1
116
+ {
117
+ const { root, scriptPath, dirs } = mkfixture();
118
+ fs.writeFileSync(path.join(root, dirs.APP_DIR, 'App.Application.csproj'), '<Project Sdk="Microsoft.NET.Sdk" />\n');
119
+ const r = runGate(root, scriptPath);
120
+ check('③干净 csproj 存在 → 整体 exit 0', r.status === 0, `exit=${r.status}\n${r.stderr}`); fs.rmSync(root, { recursive: true, force: true });
121
+ }
122
+
123
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
124
+ process.exit(fail ? 1 : 0);