@whyshu/argent-engine 0.0.0-stage → 0.6.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.
Files changed (340) hide show
  1. package/MANIFEST.json +341 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +576 -0
  55. package/engine/scripts/billing-mode-daily.sh +13 -0
  56. package/engine/scripts/build-engine-package.mjs +126 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +378 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/collect-evidence.mjs +42 -0
  81. package/engine/scripts/config-explain.mjs +278 -0
  82. package/engine/scripts/configure-provider.rb +29 -0
  83. package/engine/scripts/customer-config-migrate.mjs +179 -0
  84. package/engine/scripts/customer-preflight.py +892 -0
  85. package/engine/scripts/customer-readiness.sh +135 -0
  86. package/engine/scripts/daily-report-inbox.py +242 -0
  87. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  88. package/engine/scripts/daily-report.mjs +1310 -0
  89. package/engine/scripts/daily-report.sh +47 -0
  90. package/engine/scripts/data/lip-product-dict.json +306 -0
  91. package/engine/scripts/deliver-customer.mjs +932 -0
  92. package/engine/scripts/delivery-faces.txt +31 -0
  93. package/engine/scripts/desktop-trial.sh +146 -0
  94. package/engine/scripts/dev-window.sh +261 -0
  95. package/engine/scripts/drill-clean-machine.sh +315 -0
  96. package/engine/scripts/dsh-api-client.mjs +215 -0
  97. package/engine/scripts/dsh-backup-scope.sh +83 -0
  98. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  99. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  100. package/engine/scripts/dsh-plane.mjs +153 -0
  101. package/engine/scripts/dsh-portforward.py +223 -0
  102. package/engine/scripts/dsh-preflight.mjs +179 -0
  103. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  104. package/engine/scripts/dsh-release-watch.mjs +458 -0
  105. package/engine/scripts/dsh-remote-check.sh +99 -0
  106. package/engine/scripts/dsh-restart.sh +179 -0
  107. package/engine/scripts/dsh-restore.sh +111 -0
  108. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  109. package/engine/scripts/dsh-snapshot.mjs +116 -0
  110. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  111. package/engine/scripts/dsh-upgrade.mjs +612 -0
  112. package/engine/scripts/dsh-url.sh +86 -0
  113. package/engine/scripts/dsh-web-launch.mjs +155 -0
  114. package/engine/scripts/ensure-store-windows.py +509 -0
  115. package/engine/scripts/entitlements.py +458 -0
  116. package/engine/scripts/env_probe.py +411 -0
  117. package/engine/scripts/feishu-card.mjs +252 -0
  118. package/engine/scripts/final-check.mjs +43 -0
  119. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  120. package/engine/scripts/flow-auto-retry.py +454 -0
  121. package/engine/scripts/flow-requirements.json +735 -0
  122. package/engine/scripts/flow-scaffold.py +275 -0
  123. package/engine/scripts/flow-timers-plist.sh +87 -0
  124. package/engine/scripts/flow-timers.sh +184 -0
  125. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  126. package/engine/scripts/flow-trigger.sh +571 -0
  127. package/engine/scripts/flow_billing.py +354 -0
  128. package/engine/scripts/flow_capability_stats.py +314 -0
  129. package/engine/scripts/flow_case.py +194 -0
  130. package/engine/scripts/flow_claim.py +497 -0
  131. package/engine/scripts/flow_config.py +1025 -0
  132. package/engine/scripts/flow_failure.py +588 -0
  133. package/engine/scripts/flow_learn.py +105 -0
  134. package/engine/scripts/flow_ledger.py +235 -0
  135. package/engine/scripts/flow_policy.py +238 -0
  136. package/engine/scripts/flow_preflight.py +339 -0
  137. package/engine/scripts/flow_registry.py +341 -0
  138. package/engine/scripts/flow_retry.py +304 -0
  139. package/engine/scripts/flow_return_to_ops.py +191 -0
  140. package/engine/scripts/flow_source.py +315 -0
  141. package/engine/scripts/flow_sources/__init__.py +53 -0
  142. package/engine/scripts/flow_sources/cases_json.py +106 -0
  143. package/engine/scripts/flow_sources/common.py +428 -0
  144. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  145. package/engine/scripts/flow_stale.py +185 -0
  146. package/engine/scripts/flow_status_reconcile.py +344 -0
  147. package/engine/scripts/flow_triage.py +849 -0
  148. package/engine/scripts/flow_write_verify.py +496 -0
  149. package/engine/scripts/fulfillment.js +586 -0
  150. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  151. package/engine/scripts/gen-preset-rows.mjs +259 -0
  152. package/engine/scripts/init-ml.sh +2278 -0
  153. package/engine/scripts/install-internal-role.sh +190 -0
  154. package/engine/scripts/install-license.mjs +641 -0
  155. package/engine/scripts/install-one-shot.mjs +665 -0
  156. package/engine/scripts/integration_contract.py +220 -0
  157. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  158. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  159. package/engine/scripts/invoice-mapping.example.json +40 -0
  160. package/engine/scripts/invoice-table.sh +195 -0
  161. package/engine/scripts/invoice_carrier_notice.py +279 -0
  162. package/engine/scripts/invoice_fail_closeout.py +297 -0
  163. package/engine/scripts/invoice_lip_runner.py +1508 -0
  164. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  165. package/engine/scripts/invoice_ready_audit.py +191 -0
  166. package/engine/scripts/invoice_runner.py +675 -0
  167. package/engine/scripts/invoice_stageb.py +516 -0
  168. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  169. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  170. package/engine/scripts/lease.py +377 -0
  171. package/engine/scripts/lib/agent-install.mjs +649 -0
  172. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  173. package/engine/scripts/lib/argent-datadir.sh +18 -0
  174. package/engine/scripts/lib/argent_datadir.py +285 -0
  175. package/engine/scripts/lib/argent_filelock.py +507 -0
  176. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  177. package/engine/scripts/lib/cdp_ports.py +197 -0
  178. package/engine/scripts/lib/customer-env.mjs +102 -0
  179. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  180. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  181. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  182. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  183. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  184. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  185. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  186. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  187. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  188. package/engine/scripts/lib/patch-compose.mjs +241 -0
  189. package/engine/scripts/lib/patch-target.mjs +35 -0
  190. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  191. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  192. package/engine/scripts/lib/preset-compat.mjs +127 -0
  193. package/engine/scripts/lib/preset-install.mjs +575 -0
  194. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  195. package/engine/scripts/lib/preset-sync.mjs +59 -0
  196. package/engine/scripts/lib/profile-retire.mjs +97 -0
  197. package/engine/scripts/lib/python-runtime.mjs +190 -0
  198. package/engine/scripts/lib/registry.mjs +72 -0
  199. package/engine/scripts/lib/repo-root.sh +56 -0
  200. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  201. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  202. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  203. package/engine/scripts/lib/settings-panels.mjs +816 -0
  204. package/engine/scripts/lib/stdio_utf8.py +86 -0
  205. package/engine/scripts/lib/wake-auth.mjs +392 -0
  206. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  207. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  208. package/engine/scripts/license-e2e-drill.mjs +426 -0
  209. package/engine/scripts/license-gate.mjs +245 -0
  210. package/engine/scripts/lip_backlog.py +171 -0
  211. package/engine/scripts/lip_table.py +185 -0
  212. package/engine/scripts/local-usage.mjs +214 -0
  213. package/engine/scripts/migrate-providers.mjs +127 -0
  214. package/engine/scripts/migrate-skills.sh +65 -0
  215. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  216. package/engine/scripts/ml-store-ids.json +26 -0
  217. package/engine/scripts/ml_artifact_attribution.py +333 -0
  218. package/engine/scripts/ml_capacity_probe.py +1158 -0
  219. package/engine/scripts/ml_hub_status.py +1556 -0
  220. package/engine/scripts/ml_schema_check.py +115 -0
  221. package/engine/scripts/ml_store_ids.py +139 -0
  222. package/engine/scripts/npm-publish-argent.mjs +499 -0
  223. package/engine/scripts/npm-publish-argent.sh +20 -0
  224. package/engine/scripts/orchestrator.py +13811 -0
  225. package/engine/scripts/pack-plugins.mjs +140 -0
  226. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  227. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  228. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  229. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  230. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  231. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  232. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  233. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  234. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  235. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  236. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  237. package/engine/scripts/plan-role-rename.mjs +125 -0
  238. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  239. package/engine/scripts/plugin-composition.json +15 -0
  240. package/engine/scripts/poc-mcp-host.mjs +455 -0
  241. package/engine/scripts/poll-fulfillment.sh +295 -0
  242. package/engine/scripts/poll-invoice.sh +76 -0
  243. package/engine/scripts/profile-doctor.mjs +686 -0
  244. package/engine/scripts/provider-select.sh +71 -0
  245. package/engine/scripts/provider-switch.sh +146 -0
  246. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  247. package/engine/scripts/python-runtime.sh +78 -0
  248. package/engine/scripts/query_runner.py +310 -0
  249. package/engine/scripts/release-argent.sh +574 -0
  250. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  251. package/engine/scripts/retire-inventory.mjs +40 -0
  252. package/engine/scripts/run-tests.sh +257 -0
  253. package/engine/scripts/s1-audit.mjs +85 -0
  254. package/engine/scripts/self-sign-license.mjs +179 -0
  255. package/engine/scripts/session-health-scan.mjs +63 -0
  256. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  257. package/engine/scripts/setup.sh +22 -0
  258. package/engine/scripts/skill-install.mjs +629 -0
  259. package/engine/scripts/sku_translate.py +148 -0
  260. package/engine/scripts/sync-plugins.sh +90 -0
  261. package/engine/scripts/sync-presets.sh +27 -0
  262. package/engine/scripts/sync-scripts.sh +445 -0
  263. package/engine/scripts/telemetry.py +873 -0
  264. package/engine/scripts/ui-copy-count.mjs +220 -0
  265. package/engine/scripts/usage-anchor.sh +137 -0
  266. package/engine/scripts/usage_report.py +531 -0
  267. package/engine/scripts/validate-preset-schema.mjs +208 -0
  268. package/engine/scripts/vertical-init.mjs +323 -0
  269. package/engine/scripts/vertical-lint.mjs +482 -0
  270. package/engine/scripts/wenyao-browser.sh +74 -0
  271. package/engine/scripts/wenyao-fixture-export.py +116 -0
  272. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  273. package/engine/scripts/wenyao-login.sh +35 -0
  274. package/engine/scripts/wenyao-template-map.json +292 -0
  275. package/engine/scripts/wenyao_attach.py +131 -0
  276. package/engine/scripts/wenyao_download_template.py +144 -0
  277. package/engine/scripts/wenyao_fill_template.py +568 -0
  278. package/engine/scripts/wenyao_login.py +145 -0
  279. package/engine/scripts/wenyao_portal.py +633 -0
  280. package/engine/scripts/wenyao_recon.py +230 -0
  281. package/engine/scripts/wenyao_run.py +503 -0
  282. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  283. package/engine/scripts/wenyao_skill_contract.py +156 -0
  284. package/engine/scripts/wenyao_template_fetch.py +175 -0
  285. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  286. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  287. package/engine/scripts/ziniao-keepalive.sh +217 -0
  288. package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
  289. package/engine/scripts/ziniao-windows.sh +154 -0
  290. package/engine/skills/README.md +86 -0
  291. package/engine/skills/argent-dev/SKILL.md +41 -0
  292. package/engine/skills/internal.json +37 -0
  293. package/engine/skills/lktop-pm/SKILL.md +35 -0
  294. package/engine/skills/lktopapp-dev/SKILL.md +34 -0
  295. package/engine/skills/ml-config-guide/SKILL.md +50 -0
  296. package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
  297. package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
  298. package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
  299. package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
  300. package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
  301. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
  302. package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
  303. package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
  304. package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
  305. package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
  306. package/engine/skills/ml-invoice/SKILL.md +506 -0
  307. package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
  308. package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
  309. package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
  310. package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
  311. package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
  312. package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
  313. package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
  314. package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
  315. package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
  316. package/engine/skills/pet-dev/SKILL.md +42 -0
  317. package/engine/skills/whyshu-pm/SKILL.md +35 -0
  318. package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
  319. package/engine/version.mjs +1 -0
  320. package/engine/verticals/README.md +103 -0
  321. package/engine/verticals/_engine-words.baseline.txt +237 -0
  322. package/engine/verticals/_engine-words.json +19 -0
  323. package/engine/verticals/mercadolibre/README.md +43 -0
  324. package/engine/verticals/mercadolibre/config.json +709 -0
  325. package/engine/verticals/mercadolibre/flows.json +738 -0
  326. package/engine/verticals/mercadolibre/manifest.json +538 -0
  327. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  328. package/engine/verticals/mercadolibre/preflight.py +143 -0
  329. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  330. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  331. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  332. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  333. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  334. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  335. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  336. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  337. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  338. package/package.json +22 -4
  339. package/requirements.txt +5 -0
  340. package/README.md +0 -3
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 角色映射一致性门禁 + roles.json 生成器(2026-09-15)
4
+ *
5
+ * 背景:`PRESET_BY_ROLE` 曾在 gateway 与 drive **各存一份**,漏改一处 ⇒ 角色拿不到工具
6
+ * (实测 KB-134:`unknown tool "bash"`)。本脚本把"漏配"从事后发现变成门禁拦截。
7
+ *
8
+ * 用法:
9
+ * node scripts/check-role-mapping.mjs # 门禁:gateway.json 角色 ↔ 两份 PRESET_BY_ROLE(**双向**)
10
+ * node scripts/check-role-mapping.mjs --emit # 额外把合并结果写到 stdout(供生成 roles.json)
11
+ * node scripts/check-role-mapping.mjs --write # 生成/刷新 roles.json(角色→preset 的唯一来源)
12
+ * node scripts/check-role-mapping.mjs --fix-skills [--yes] # 按真源修 gateway.json 里**无效**的技能声明(Q6)
13
+ * 退出码:0 = 一致;1 = 有缺口
14
+ *
15
+ * ## 第 ⑤ 项:**反向校验**(声明侧 → gateway 侧;2026-10-04)
16
+ * 老门禁只做单向:`gateway.json` 里的每个角色必须有映射。反过来「源码/注册表**声明了**某角色、
17
+ * 但运行期 `gateway.json` 里**根本没有这一行**」不检查 ⇒ 实测 `general` 与 `ml-invoice-xzh`
18
+ * 有声明无网关角色(**飞书 @ 唤不醒**),而门禁 exit 0(盘点文档 §3-④)。
19
+ * 现在这一向判红,并区分两类:
20
+ * · **有意不接网关** —— 登记在仓库侧 `NO_GATEWAY_BY_DESIGN` 或站点侧
21
+ * `$ARGENT_DATA_DIR/role-gateway-exemptions.json`(`{"exemptions":{"<role>":"<理由>"}}`),
22
+ * **必须带理由**(缺理由/登记了不存在的角色都判红 —— 否则登记表会变成洗白工具);
23
+ * · **遗漏** —— 未登记 ⇒ ❌ 判红(缺口如实暴露,**不要**为了变绿而登记或擅自塞进 gateway.json)。
24
+ *
25
+ * ## 第 ⑥ 项:**技能声明必须真实存在**(Q6,2026-10-04)
26
+ * `roles[].skills` / `roles[].chats[*].skills` 曾被填成**角色名**(`ml-daily` / `ml-fulfillment`),
27
+ * 而 `~/.dsh/skills/` 下的真实目录是 `ml-store-daily-report` / `ml-fulfillment-sop` ⇒ 网关注入的
28
+ * 「【本群启用技能】」提示词**指向空目录**:不抛错、日志无异常、技能永不生效(**静默失效**)。
29
+ * 判据的**唯一实现**在 `plugins/argent-gateway/src/skill-declaration.js`(网关启动/热重载跑同一份,
30
+ * ⛔ 不另写"门禁版判据");本项把两个声明面都过一遍:
31
+ * ① 运行期 `$ARGENT_DATA_DIR/gateway.json`;
32
+ * ② 仓库侧客户模板 `deploy/customer/roles.spec.json`(由 `customer-config-migrate.mjs` 并入客户机
33
+ * gateway.json ⇒ 模板里的错名会**批量复制**到每台客户机,必须先在这里拦住)。
34
+ * 空数组 = **没声明**(合法,不报);缺字段同理。修法见下面的 `--fix-skills`。
35
+ *
36
+ * ## `--fix-skills`:按真源修(**只修错的,不猜**)
37
+ * 真源 = `verticals/<id>/flows.json` 的 `role`+`skill` 配对 + `skills/internal.json` 的键
38
+ * (见 `skill-declaration.js` 头注释 §二)—— 本脚本不维护第二份名单。
39
+ * · 默认 **dry-run**:打印每条 `before → after`,不落盘;
40
+ * · `--yes`:写前备份 `<数据目录>/backup/gateway.json.<ts>.skills-fix` ⇒ 同目录 `*.tmp` + `rename`
41
+ * **原子写**(网关 watch 的是**目录**,原子写**能**触发热重载;旧的文件级 watch 会被 rename 打死)
42
+ * ⇒ 写后**读回并重跑判据**(不靠"我以为写对了");
43
+ * · `skills: []` / 缺字段 **不动**(那是"没声明");真源推导不出候选的 ⇒ 列进 `unresolved` 并 exit 1,
44
+ * **绝不编一个名字**。
45
+ */
46
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
47
+ import { join } from 'node:path'
48
+ import { homedir } from 'node:os'
49
+ import { resolveDataDir } from './lib/argent-datadir.mjs'
50
+
51
+ const REPO = new URL('..', import.meta.url).pathname
52
+ // 数据目录:内核三级链(唯一判据)。注意 launchd/定时器里 `HOME` 可能未导出 ⇒ 内核用
53
+ // `env.HOME || homedir()` 兜底,与旧注释(2026-09-28)同一口径。
54
+ const DATA_DIR = resolveDataDir({ env: process.env }).dir
55
+ const GATEWAY_JSON = join(DATA_DIR, 'gateway.json')
56
+
57
+ /**
58
+ * 技能声明判据(唯一实现 = 网关那份)。加载失败 ⇒ 由第 ⑥ 项判红(不静默跳过)。
59
+ * 用动态 import:本脚本是 L2(判据面是宿主),允许引用 `plugins/`。
60
+ */
61
+ const SD = await (async () => {
62
+ try { return await import('../plugins/argent-gateway/src/skill-declaration.js') } catch { return null }
63
+ })()
64
+
65
+ /** 声明面(gateway.json + 仓库客户模板),供第 ⑥ 项与 `--fix-skills` 共用。 */
66
+ const SPEC_JSON = join(REPO, 'deploy/customer/roles.spec.json')
67
+ const readJsonDoc = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return null } }
68
+ /** dsh 家目录(`DSH_HOME` → `$HOME/.dsh`)——技能目录 `<home>/skills/` 在它下面。 */
69
+ const DSH_HOME = process.env.DSH_HOME || join(process.env.HOME || homedir(), '.dsh')
70
+ /** 技能查找根(宿主优先,其次仓库)——与网关启动校验同一份实现。 */
71
+ const SKILL_ROOTS = SD ? SD.resolveSkillRoots({ dshHome: DSH_HOME, repoRoots: [REPO] }) : []
72
+
73
+ // ══════════════════════════════════════════════════════════════════════════
74
+ // `--fix-skills`:按真源修**无效**的技能声明(Q6)—— 默认 dry-run,`--yes` 才落盘
75
+ // 子命令形态(命中即**短路**,不再跑 ①~⑥ 那套门禁):它是"修复入口",不是"体检报告"。
76
+ // ══════════════════════════════════════════════════════════════════════════
77
+ if (process.argv.includes('--fix-skills')) {
78
+ const APPLY = process.argv.includes('--yes')
79
+ if (!SD) { console.error('❌ 技能声明判据模块加载失败:plugins/argent-gateway/src/skill-declaration.js(同步插件后再跑)'); process.exit(2) }
80
+ const doc = readJsonDoc(GATEWAY_JSON)
81
+ if (!doc || !Array.isArray(doc.roles)) { console.error(`❌ 读不到 gateway.json 的 roles 数组:${GATEWAY_JSON}`); process.exit(2) }
82
+
83
+ const known = SD.listSkillNames(SKILL_ROOTS)
84
+ const { byRole, sources } = SD.deriveRoleSkills({ repoRoots: [REPO] })
85
+ const { changes, unresolved } = SD.planSkillFixes({
86
+ declarations: SD.declarationsFromRoles(doc.roles), knownSkills: known, derived: byRole,
87
+ })
88
+ console.log(` 文件:${GATEWAY_JSON}`)
89
+ console.log(` 技能目录:${SKILL_ROOTS.join(' | ') || '(未找到)'}(${known.length} 个技能)`)
90
+ console.log(` 真源:${sources.join('、') || '(未找到声明文件)'}`)
91
+ for (const c of changes) {
92
+ console.log(` ▸ ${c.where}(role=${c.role})`)
93
+ console.log(` ${JSON.stringify(c.from)} → ${JSON.stringify(c.to)}`)
94
+ console.log(` 无效条目:${c.bad.map((b) => JSON.stringify(b)).join(', ')}`)
95
+ }
96
+ for (const u of unresolved) {
97
+ console.log(` ❌ 修不了(真源推导不出候选,**不猜**):${u.where}(role=${u.role})无效条目 ${u.bad.map((b) => JSON.stringify(b)).join(', ')}`)
98
+ console.log(' ⇒ 二选一:① 该角色没有对应流程 ⇒ 从 gateway.json 删掉这些声明;② 补一条 `role`+`skill` 声明(flows.json)或把它登记进 skills/internal.json。')
99
+ }
100
+ if (!changes.length && !unresolved.length) { console.log(' ✅ 无需修复:所有已声明的技能都真实存在'); process.exit(0) }
101
+ if (!APPLY) {
102
+ console.log(` (dry-run:未落盘。加 --yes 应用这 ${changes.length} 条修改:写前备份 + 同目录 tmp+rename 原子写 + 写后读回验收)`)
103
+ process.exit(unresolved.length ? 1 : 0)
104
+ }
105
+
106
+ // ── 落盘:① 备份 ② 原子写 ③ 读回验收(三步都不省)──────────────────────────
107
+ const before = readFileSync(GATEWAY_JSON, 'utf8')
108
+ const pad = (n) => String(n).padStart(2, '0')
109
+ const d = new Date()
110
+ const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
111
+ const backupDir = join(DATA_DIR, 'backup')
112
+ mkdirSync(backupDir, { recursive: true })
113
+ let backup = join(backupDir, `gateway.json.${ts}.skills-fix`)
114
+ for (let i = 2; existsSync(backup); i++) backup = join(backupDir, `gateway.json.${ts}-${i}.skills-fix`)
115
+ writeFileSync(backup, before)
116
+ console.log(` ✅ 备份:${backup}(${Buffer.byteLength(before)} 字节)`)
117
+
118
+ const { applied, skipped } = SD.applySkillFixes(doc, changes)
119
+ const tmp = `${GATEWAY_JSON}.tmp` // 同目录 ⇒ rename 是原子的(跨盘才不原子)
120
+ const text = JSON.stringify(doc, null, 2) + (before.endsWith('\n') ? '\n' : '')
121
+ writeFileSync(tmp, text, { mode: statSync(GATEWAY_JSON).mode & 0o777 })
122
+ renameSync(tmp, GATEWAY_JSON)
123
+ console.log(` ✅ 已原子写 ${GATEWAY_JSON}(${applied} 条;跳过 ${skipped.length} 条${skipped.length ? ':' + skipped.join('、') : ''})`)
124
+
125
+ const after = readJsonDoc(GATEWAY_JSON)
126
+ const left = after ? SD.validateSkillDeclarations({
127
+ declarations: SD.declarationsFromRoles(after.roles), knownSkills: known, derived: byRole,
128
+ }) : [{ where: '(读回)', owner: '-', value: 'JSON 解析失败', reason: 'not-array', index: -1, derived: [] }]
129
+ if (left.length) {
130
+ console.log(` ❌ 写后读回**仍有** ${left.length} 条无效声明 —— 备份在 ${backup}(可原样回滚)`)
131
+ for (const line of SD.formatSkillWarnings(left, { knownSkills: known })) console.log(` ${line}`)
132
+ process.exit(1)
133
+ }
134
+ console.log(` ✅ 写后读回验收:${changes.length} 条声明全部指向真实技能目录`)
135
+ process.exit(unresolved.length ? 1 : 0)
136
+ }
137
+
138
+
139
+
140
+ /** 抽出成对映射(角色 → preset id),用于生成 roles.json。 */
141
+ function extractPairs(file, constName) {
142
+ const src = existsSync(file) ? readFileSync(file, 'utf8') : ''
143
+ const re = new RegExp(`const\\s+${constName}\\s*=\\s*Object\\.assign\\(Object\\.create\\(null\\),\\s*\\{`)
144
+ const m = re.exec(src)
145
+ if (!m) return {}
146
+ let i = src.indexOf('{', m.index + m[0].length - 1)
147
+ let depth = 0, end = -1
148
+ for (let k = i; k < src.length; k++) {
149
+ if (src[k] === '{') depth++
150
+ else if (src[k] === '}') { depth--; if (depth === 0) { end = k; break } }
151
+ }
152
+ const out = {}
153
+ for (const mm of src.slice(i + 1, end).matchAll(/'([a-zA-Z0-9_-]+)'\s*:\s*'([^']+)'/g)) out[mm[1]] = mm[2]
154
+ return out
155
+ }
156
+
157
+ /** 从插件源码里抽出 `const NAME = Object.assign(Object.create(null), { ... })` 的键。 */
158
+ function extractKeys(file, constName) {
159
+ if (!existsSync(file)) return null
160
+ const src = readFileSync(file, 'utf8')
161
+ const re = new RegExp(`const\\s+${constName}\\s*=\\s*Object\\.assign\\(Object\\.create\\(null\\),\\s*\\{`)
162
+ const m = re.exec(src)
163
+ if (!m) return null
164
+ let i = src.indexOf('{', m.index + m[0].length - 1)
165
+ let depth = 0, end = -1
166
+ for (let k = i; k < src.length; k++) {
167
+ if (src[k] === '{') depth++
168
+ else if (src[k] === '}') { depth--; if (depth === 0) { end = k; break } }
169
+ }
170
+ const body = src.slice(i + 1, end)
171
+ const keys = []
172
+ for (const line of body.split('\n')) {
173
+ const km = /^\s*'([a-zA-Z0-9_-]+)'\s*:/.exec(line) || /^\s*([a-zA-Z0-9_-]+)\s*:/.exec(line)
174
+ if (km) keys.push(km[1])
175
+ }
176
+ return [...new Set(keys)]
177
+ }
178
+
179
+ const gw = join(REPO, 'plugins/argent-gateway/src/index.js')
180
+ const dr = join(REPO, 'plugins/argent-drive/src/index.js')
181
+
182
+ const gwMap = extractKeys(gw, 'PRESET_BY_ROLE')
183
+ // 2026-09-15 P4-4c:argent-drive 已退场 ⇒ 其副本不再要求存在;存在则继续比对(过渡期保护)
184
+ const drMap = extractKeys(dr, 'PRESET_BY_ROLE')
185
+ if (!gwMap) { console.error('❌ 未能从 gateway 源码解析 PRESET_BY_ROLE(源码结构可能变了)'); process.exit(1) }
186
+ if (!drMap) console.log(' · 提示:未找到 argent-drive 的 PRESET_BY_ROLE(已退场,跳过其比对)')
187
+
188
+ let roles = []
189
+ /** gateway.json 的**整份**解析结果(第 ⑥ 项要按 roles[].skills 逐项核,光有角色名不够)。 */
190
+ let gwDoc = null
191
+ if (existsSync(GATEWAY_JSON)) {
192
+ try {
193
+ gwDoc = JSON.parse(readFileSync(GATEWAY_JSON, 'utf8'))
194
+ const raw = gwDoc.roles
195
+ // gateway.json 的 roles 是**数组**(每项含 role 字段);兼容对象写法
196
+ roles = Array.isArray(raw) ? raw.map((r) => r && r.role).filter(Boolean) : Object.keys(raw || {})
197
+ } catch (e) { console.error('❌ gateway.json 解析失败:', e.message); process.exit(1) }
198
+ } else {
199
+ console.log('⚠️ 未找到 gateway.json,跳过角色对比,仅报告两份映射的差异')
200
+ }
201
+
202
+ // 代码注入的兜底角色:不需要 PRESET_BY_ROLE(用默认 preset)
203
+ // main = 主 Bot 兜底(gateway 源码 KB-046:config.feishuAppId 的通用助手,运行时注入内存数组)
204
+ const INJECTED_ROLES = new Set(['main'])
205
+ const need = (r) => !INJECTED_ROLES.has(r)
206
+
207
+ // ── 第 ⑤ 项用:「有意不接网关」的**显式登记**(仓库侧)────────────────────────────
208
+ // 语义:登记 = 声明「这个角色**不需要** gateway.json 的独立条目」,不是「我知道它缺着」。
209
+ // ⛔ 不要为了变绿往这里塞角色(那就是把门禁变成橡皮图章);每条必须带 reason。
210
+ // 站点侧登记(客户机姿态不同、不该改仓库)用 $ARGENT_DATA_DIR/role-gateway-exemptions.json。
211
+ const NO_GATEWAY_BY_DESIGN = Object.assign(Object.create(null), {
212
+ // `general`:@ 能力由**主 Bot 兜底**承载 —— gateway 源码 KB-046 的 main 行(agentId=gw-main,
213
+ // prefix=ROLE_PREFIX.general)+ 无 bot 命中路由(`plugins/argent-gateway/src/index.js:624`
214
+ // 的 `{ role: 'general', agentId: 'gw-general', prefix: ROLE_PREFIX.general }`)⇒ 无需独立条目。
215
+ // (客户机模板 `deploy/customer/roles.spec.json` 同样不含 general,与该设计一致。)
216
+ general: { reason: '主 Bot 兜底行(main)+ 无 bot 命中路由承载,源码 index.js:624 / KB-046 ⇒ 不需要独立条目', since: '2026-10-04' },
217
+ // `hr-office` / `ml-dev`:**2026-10-04 退役**(docs/Argent-角色合并与收敛-操作方案-2026-10-04.md §3.3)。
218
+ // 语义要点:这两条**不是**"知道它缺着",而是"退役后**本就不该**有独立条目"——
219
+ // · role 层已撤:`~/.dsh/argent/gateway.json` 里两行已删 ⇒ 飞书 @ 不再派单(这是退役的目的);
220
+ // · 但**墓碑仍在**:`presets/argent-{hr-office,ml-dev}/` 与两份 `PRESET_BY_ROLE` 映射**故意保留**,
221
+ // 因为 `preset id` 是 web 手选会话头 `agentPreset` 的**外键** —— 删了老会话就打不开
222
+ // (`Unknown agent preset`)。⇒ 声明侧仍有这两个角色,必须在这里登记,否则本门禁会把
223
+ // "按设计的墓碑"误报成"遗漏"。
224
+ // · 收口:观察窗口 ≥2 周后删墓碑(preset 目录 + 行式产物 + 这两条登记),届时本登记一并删除。
225
+ 'hr-office': { reason: '已退役(2026-10-04,方案 §3.3.1):role 层已撤 ⇒ 不需要独立条目;preset 保留为墓碑(web 老会话的 agentPreset 外键)', since: '2026-10-04' },
226
+ 'ml-dev': { reason: '已退役(2026-10-04,方案 §3.3.2):role 层已撤 ⇒ 不需要独立条目;preset 保留为墓碑(web 老会话的 agentPreset 外键)', since: '2026-10-04' },
227
+ })
228
+
229
+ /** 站点侧豁免登记表路径(测试可覆盖;缺省落在数据目录,随站点走、不进仓库)。 */
230
+ const EXEMPTIONS_FILE = process.env.ARGENT_ROLE_GATEWAY_EXEMPTIONS || join(DATA_DIR, 'role-gateway-exemptions.json')
231
+
232
+ /**
233
+ * 读站点侧豁免登记表。格式:`{ "exemptions": { "<role>": "<理由>" } }`(也接受扁平 `{role: 理由}`)。
234
+ * 解析失败**返回 error**(由调用方判红)—— 坏登记表绝不能静默当成"没有豁免"。
235
+ */
236
+ function loadSiteExemptions() {
237
+ if (!existsSync(EXEMPTIONS_FILE)) return { map: {}, file: EXEMPTIONS_FILE, present: false, error: null }
238
+ try {
239
+ const doc = JSON.parse(readFileSync(EXEMPTIONS_FILE, 'utf8'))
240
+ const src = (doc && typeof doc === 'object' && doc.exemptions && typeof doc.exemptions === 'object') ? doc.exemptions : (doc || {})
241
+ const map = Object.assign(Object.create(null), {})
242
+ for (const [k, v] of Object.entries(src)) {
243
+ if (['version', 'note', '_doc', '_note', 'exemptions'].includes(k)) continue
244
+ const reason = typeof v === 'string' ? v : ((v && (v.reason || v.why)) || '')
245
+ map[k] = { reason: String(reason), since: (v && typeof v === 'object' && v.since) || '', source: 'site' }
246
+ }
247
+ return { map, file: EXEMPTIONS_FILE, present: true, error: null }
248
+ } catch (e) {
249
+ return { map: {}, file: EXEMPTIONS_FILE, present: true, error: (e && e.message) || String(e) }
250
+ }
251
+ }
252
+
253
+ const onlyGw = drMap ? gwMap.filter((r) => !drMap.includes(r)) : []
254
+ const onlyDr = drMap ? drMap.filter((r) => !gwMap.includes(r)) : []
255
+ const missingGw = roles.filter((r) => need(r) && !gwMap.includes(r))
256
+ const missingDr = drMap ? roles.filter((r) => need(r) && !drMap.includes(r)) : []
257
+
258
+ console.log(` gateway PRESET_BY_ROLE: ${gwMap.length} 个|drive: ${drMap ? drMap.length : '—'} 个|gateway.json 角色: ${roles.length} 个`)
259
+ // 注意:后续检查(preset 目录、roles.json)也会置红,故必须是 let。
260
+ // 2026-09-28 实测:写成 const 时,客户机的 roles.json 缺失分支一赋值就 TypeError ——
261
+ // 门禁**自己崩掉**而不是报红,比漏检更糟(人看到的是栈,不是"缺 roles.json")。
262
+ let bad = onlyGw.length + onlyDr.length + missingGw.length + missingDr.length
263
+ if (onlyGw.length) console.log(` ❌ 仅 gateway 有(drive 缺): ${onlyGw.join(', ')}`)
264
+ if (onlyDr.length) console.log(` ❌ 仅 drive 有(gateway 缺): ${onlyDr.join(', ')}`)
265
+ if (missingGw.length) console.log(` ❌ gateway.json 有但 gateway 映射缺: ${missingGw.join(', ')}`)
266
+ if (missingDr.length) console.log(` ❌ gateway.json 有但 drive 映射缺: ${missingDr.join(', ')}`)
267
+ if (!bad) console.log(' ✅ 三方一致:gateway.json 每个角色在两份 PRESET_BY_ROLE 里都有映射')
268
+
269
+ // 2026-09-17 新增:**角色引用的 preset 目录必须存在**(否则角色起来会缺工具,静默失败)。
270
+ // 来源两处:① 真实产物 ~/.dsh/argent/roles.json 的 presets 段;② gateway 源码里的映射对。
271
+ // 注意:`roles` 这个变量是**角色名数组**(来自 gateway.json),不是 roles.json 的解析结果 —— 别读错。
272
+ {
273
+ const referenced = new Set()
274
+ try {
275
+ // 用 homedir() 而不是 process.env.HOME:launchd/定时器环境里 HOME 可能未导出(2026-09-28)
276
+ const p = join(DATA_DIR, 'roles.json')
277
+ if (existsSync(p)) {
278
+ const doc = JSON.parse(readFileSync(p, 'utf8'))
279
+ for (const v of Object.values((doc && doc.presets) || {})) if (typeof v === 'string') referenced.add(v)
280
+ }
281
+ } catch (e) {
282
+ // 这里只负责"收集引用",**不判红**;坏文件由下面第 ④ 项检查统一判红并给修法
283
+ console.error(` ⚠️ roles.json 解析失败(第 ④ 项检查会判红):${e && e.message}`)
284
+ }
285
+ for (const v of Object.values(extractPairs(gw, 'PRESET_BY_ROLE'))) if (typeof v === 'string') referenced.add(v)
286
+ if (!referenced.size) {
287
+ console.error(' ❌ 检查到 0 个 preset 引用 —— 说明来源路径/解析有问题(不允许假绿灯)')
288
+ process.exit(1)
289
+ }
290
+ const presetsDir = join(REPO, 'presets')
291
+ const missingPresets = [...referenced].filter((pid) =>
292
+ !existsSync(join(presetsDir, pid)) || !existsSync(join(presetsDir, pid, 'agent.cordis.yml')))
293
+ if (missingPresets.length) {
294
+ console.error(` ❌ 引用了不存在的 preset:${missingPresets.join(', ')}`)
295
+ console.error(' ⇒ 角色起来会缺工具(静默)。修:改名/新建 preset 后跑 node scripts/check-role-mapping.mjs --write')
296
+ process.exit(1)
297
+ }
298
+ console.log(` ✅ 引用的 ${referenced.size} 个 preset 目录都存在`)
299
+
300
+ // 2026-09-17 新增(客户机核对时发现):**仓库里有 ≠ 运行时装着**。
301
+ // 角色真正唤起时 dsh 从 `$DSH_HOME/.agent-presets/<id>/` 挂载;只在仓库 presets/ 里存在
302
+ // ⇒ 角色会话建得起来但缺工具(实测:新建的 argent-ml-invoice-xzh 一直没装进运行时目录,
303
+ // 而旧门禁只查仓库目录 ⇒ 假绿灯)。这里补一条运行时检查:
304
+ // · 运行时目录整个不存在 ⇒ 只提示(可能是隔离 home / 客户机尚未部署)
305
+ // · 目录存在但缺某个被引用的 preset ⇒ 直接失败(一定会静默缺工具)
306
+ const runtimeDir = join(DSH_HOME, '.agent-presets')
307
+ if (!existsSync(runtimeDir)) {
308
+ console.log(` ⚠️ 运行时 preset 目录不存在(跳过运行时检查):${runtimeDir}`)
309
+ } else {
310
+ const notInstalled = [...referenced].filter((pid) => !existsSync(join(runtimeDir, pid, 'agent.cordis.yml')))
311
+ if (notInstalled.length) {
312
+ console.error(` ❌ 运行时未安装的 preset:${notInstalled.join(', ')}(目录 ${runtimeDir})`)
313
+ console.error(' ⇒ 角色会话能建起来但缺工具(静默失败)。修:bash scripts/sync-presets.sh <id>')
314
+ process.exit(1)
315
+ }
316
+ console.log(` ✅ 运行时已安装全部 ${referenced.size} 个 preset(${runtimeDir})`)
317
+ }
318
+ }
319
+
320
+ // ④ roles.json 是否与角色集一致(2026-09-28 P10 踩坑):
321
+ // 这份文件是**运行时**角色→preset 的唯一来源(flow_preflight 读它判"角色映射不一致")。
322
+ // 老版本门禁只比 gateway.json ↔ 源码常量 ⇒ 客户机上 roles.json **根本不存在**时这里仍然全绿,
323
+ // 而流程触发器却因角色映射缺失被拦 ⇒ 两边说法不一致。故补这一项。
324
+ {
325
+ const rolesJson = join(DATA_DIR, 'roles.json')
326
+ let doc = null
327
+ try { doc = JSON.parse(readFileSync(rolesJson, 'utf8')) } catch { doc = null }
328
+ const expect = Object.assign({}, extractPairs(dr, 'PRESET_BY_ROLE'), extractPairs(gw, 'PRESET_BY_ROLE'))
329
+ const need = [...roles].filter((r) => expect[r]) // 只要求 gateway 现有角色可用
330
+ if (!doc) {
331
+ bad = true
332
+ console.log(` ❌ 缺 ${rolesJson}(运行时角色→preset 的唯一来源)⇒ flow_preflight 会判「角色映射不一致」并拦住触发器`)
333
+ console.log(' ⇒ 修:node scripts/check-role-mapping.mjs --write')
334
+ } else {
335
+ const got = doc.presets || {}
336
+ const missing = need.filter((r) => !got[r])
337
+ const wrong = need.filter((r) => got[r] && got[r] !== expect[r])
338
+ if (missing.length || wrong.length) {
339
+ bad = true
340
+ if (missing.length) console.log(` ❌ roles.json 缺角色:${missing.join(', ')}(流程会挂错 preset 或挂不上)`)
341
+ if (wrong.length) console.log(` ❌ roles.json 映射不符:${wrong.map((r) => `${r}: ${got[r]} ≠ ${expect[r]}`).join(';')}`)
342
+ console.log(' ⇒ 修:node scripts/check-role-mapping.mjs --write')
343
+ } else {
344
+ console.log(` ✅ roles.json 与角色集一致(${need.length} 个角色)`)
345
+ }
346
+ }
347
+ }
348
+
349
+ // ⑤ **反向校验:声明侧有、gateway.json 没有**(2026-10-04,盘点 §3-④ 的缺口)
350
+ // 为什么要它:老门禁只有 `missingGw`(gateway.json 的角色必须有映射)这**一个方向**;
351
+ // 「源码/注册表声明了某角色、而运行期 gateway.json 里根本没有这一行」= 该角色**飞书 @ 唤不醒**,
352
+ // 却一路绿灯(实测:`general` / `ml-invoice-xzh`)。这一项把该缺口从"没人知道"变成"门禁报红"。
353
+ {
354
+ if (!existsSync(GATEWAY_JSON)) {
355
+ // 没有任何 gateway 配置 ⇒ 没有任何角色可被唤醒,属**未配置**状态(上面已提示"跳过角色对比");
356
+ // 这里**不**把 13 个声明全列成缺口(那是噪音,不是漂移)。⚠️ 该状态下反向校验不生效 —— 如实标注。
357
+ console.log(' ⚠️ 无 gateway.json ⇒ 第 ⑤ 项反向校验跳过(该状态下没有任何角色能被 @ 唤醒,属未配置而非映射漂移)')
358
+ } else {
359
+ const gwSet = new Set(roles)
360
+ // 声明侧 = 两份 PRESET_BY_ROLE(源码)+ roles.json#presets(运行期注册表;坏文件由第 ④ 项判红)
361
+ const declared = new Set([...gwMap, ...(drMap || [])])
362
+ try {
363
+ const doc = JSON.parse(readFileSync(join(DATA_DIR, 'roles.json'), 'utf8'))
364
+ for (const r of Object.keys((doc && doc.presets) || {})) declared.add(r)
365
+ } catch { /* 缺/坏 roles.json 由第 ④ 项判红;这里只是少一路声明来源 */ }
366
+ const notInGateway = [...declared].filter((r) => need(r) && !gwSet.has(r)).sort()
367
+ // 登记表 = 仓库侧(随代码评审)+ 站点侧(机器姿态);同角色以仓库侧为准
368
+ const site = loadSiteExemptions()
369
+ const registered = new Map()
370
+ for (const [r, v] of Object.entries(NO_GATEWAY_BY_DESIGN)) registered.set(r, { ...v, source: 'repo' })
371
+ for (const [r, v] of Object.entries(site.map)) if (!registered.has(r)) registered.set(r, v)
372
+ // 登记表自身卫生:① 每条必须带理由;② 不许登记"不存在的角色"(错别字/腐烂 ⇒ 会掩盖真缺口)
373
+ const known = new Set([...declared, ...gwSet, ...INJECTED_ROLES])
374
+ const noReason = [...registered].filter(([, v]) => !String(v.reason || '').trim()).map(([r]) => r).sort()
375
+ const unknownReg = [...registered.keys()].filter((r) => !known.has(r)).sort()
376
+ const intentional = notInGateway.filter((r) => registered.has(r))
377
+ const gaps = notInGateway.filter((r) => !registered.has(r))
378
+ if (site.error) {
379
+ bad = true
380
+ console.log(` ❌ 站点侧豁免登记表解析失败:${site.file}(${site.error})⇒ 不能当成"没有豁免"(否则缺口被静默吞掉)`)
381
+ }
382
+ if (noReason.length) {
383
+ bad = true
384
+ console.log(` ❌ 豁免登记缺理由:${noReason.join(', ')}(带理由才叫"有意",无理由与"遗漏"无法区分)`)
385
+ }
386
+ if (unknownReg.length) {
387
+ bad = true
388
+ console.log(` ❌ 豁免登记了不存在的角色:${unknownReg.join(', ')}(错别字/腐烂登记 ⇒ 会掩盖真正缺的角色)`)
389
+ }
390
+ bad += gaps.length
391
+ console.log(` 声明侧 ${declared.size} 个角色 → gateway.json ${gwSet.size} 个:不在网关的 ${notInGateway.length} 个(有意不接 ${intentional.length} / 遗漏 ${gaps.length})`)
392
+ for (const r of intentional) {
393
+ const e = registered.get(r)
394
+ console.log(` ✅ 有意不接网关(已登记理由,非"遗漏"):${r} —— ${e.reason}(来源:${e.source === 'repo' ? 'NO_GATEWAY_BY_DESIGN' : site.file})`)
395
+ }
396
+ if (gaps.length) {
397
+ console.log(` ❌ 声明了但 gateway.json 没有(**飞书 @ 唤不醒**):${gaps.join(', ')}`)
398
+ console.log(' ⇒ 二选一:① 在运行期 gateway.json 补该角色条目(机器配置 ⇒ 需用户点头 + 重启窗口);')
399
+ console.log(` ② 确属**有意**不接网关 ⇒ 带理由登记:站点侧 ${site.file}`)
400
+ console.log(' ({"exemptions":{"<role>":"<为什么不需要独立条目>"}})或仓库侧本脚本的 NO_GATEWAY_BY_DESIGN。')
401
+ console.log(' ⛔ 不要为了变绿而登记,也不要为了变绿擅自把角色塞进 gateway.json。')
402
+ } else {
403
+ console.log(' ✅ 声明侧每个角色在 gateway.json 里都有条目(或已登记"有意不接")')
404
+ }
405
+ }
406
+ }
407
+
408
+ // ⑥ 技能声明必须真实存在(Q6,2026-10-04)
409
+ // 判据的唯一实现在 plugins/argent-gateway/src/skill-declaration.js(**网关启动/热重载跑同一份**)。
410
+ // 两个声明面都过:① 运行期 gateway.json ② 仓库侧客户模板 deploy/customer/roles.spec.json
411
+ // (模板会被 customer-config-migrate 并入每台客户机 ⇒ 这里的错名是**批量**故障)。
412
+ {
413
+ if (!SD) {
414
+ bad = true
415
+ console.log(' ❌ 技能声明判据模块加载失败:plugins/argent-gateway/src/skill-declaration.js')
416
+ console.log(' ⇒ 该模块是网关启动校验与本节判据的**唯一实现**;缺它 ⇒ 技能声明无人校验(静默失效复发面)')
417
+ } else {
418
+ const known = SD.listSkillNames(SKILL_ROOTS)
419
+ const { byRole, sources } = SD.deriveRoleSkills({ repoRoots: [REPO] })
420
+ const surfaces = []
421
+ if (gwDoc && Array.isArray(gwDoc.roles)) surfaces.push({ file: GATEWAY_JSON, roles: gwDoc.roles })
422
+ else console.log(' ⚠️ 无 gateway.json(或 roles 非数组)⇒ 本节只查仓库侧客户模板')
423
+ const specDoc = readJsonDoc(SPEC_JSON)
424
+ if (specDoc && Array.isArray(specDoc.roles)) surfaces.push({ file: SPEC_JSON, roles: specDoc.roles })
425
+ else console.log(` ⚠️ 读不到客户模板 ${SPEC_JSON} ⇒ 本节不查它(模板缺失本身不判红)`)
426
+
427
+ console.log(` 技能目录:${SKILL_ROOTS.join(' | ') || '(未找到)'}(${known.length} 个技能)|真源:${sources.length} 份声明文件`)
428
+ let bad6 = 0
429
+ for (const s of surfaces) {
430
+ const problems = SD.validateSkillDeclarations({
431
+ declarations: SD.declarationsFromRoles(s.roles), knownSkills: known, derived: byRole,
432
+ })
433
+ if (!problems.length) { console.log(` ✅ ${s.file}:已声明的技能都真实存在(空数组 = 没声明,不算错)`); continue }
434
+ bad6 += problems.length
435
+ console.log(` ❌ ${s.file}:${problems.length} 条技能声明指向**不存在**的技能目录(网关注入的「【本群启用技能】」会指向空目录,静默失效)`)
436
+ for (const line of SD.formatSkillWarnings(problems, { knownSkills: known })) console.log(` ${line}`)
437
+ }
438
+ if (bad6) {
439
+ console.log(' ⇒ 修:node scripts/check-role-mapping.mjs --fix-skills --yes(按真源修错名;先备份 + 原子写 + 写后读回)')
440
+ console.log(' ⛔ 别手改第二份名单:角色→技能 的真源是 verticals/<id>/flows.json 的 role+skill 与 skills/internal.json。')
441
+ bad += bad6
442
+ }
443
+ }
444
+ }
445
+
446
+ if (process.argv.includes('--write')) {
447
+ const pairs = Object.assign({}, extractPairs(dr, 'PRESET_BY_ROLE'), extractPairs(gw, 'PRESET_BY_ROLE'))
448
+ const doc = { version: 1, generatedAt: new Date().toISOString(),
449
+ note: '角色 → preset 的**唯一来源**(Hub 化)。gateway/drive 读取本文件;源码内常量仅作缺失兜底。由 scripts/check-role-mapping.mjs --write 生成。',
450
+ presets: pairs }
451
+ const out = join(DATA_DIR, 'roles.json')
452
+ writeFileSync(out, JSON.stringify(doc, null, 2) + '\n', 'utf8')
453
+ console.log(` ✅ 已写 ${out}(${Object.keys(pairs).length} 个角色)`)
454
+ }
455
+
456
+ if (process.argv.includes('--emit')) {
457
+ const merged = {}
458
+ for (const r of [...new Set([...gwMap, ...drMap, ...roles])].sort()) merged[r] = { preset: gwMap.includes(r) ? true : (drMap.includes(r) ? true : false) }
459
+ console.log(JSON.stringify({ generatedAt: new Date().toISOString(), roles: merged }, null, 2))
460
+ }
461
+ process.exit(bad ? 1 : 0)