@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,577 @@
1
+ /**
2
+ * `argent doctor --customer` —— **客户机一页式自检**(M1 判据③;P4,2026-10-03)。
3
+ *
4
+ * ## 它回答什么(M1 判据③ 的原文)
5
+ * 实施方案 §4.2 M1 判据③ =「`argent doctor --customer` **无 dsh 不报 FAIL**」。
6
+ * 陪跑式的第一个客户机上**很可能没有 dsh**(客户用的是自己的 agent 宿主:claude-code / codex /
7
+ * cursor / 桌面端…)⇒ 自检必须能在那台机器上跑完并给出**可照做的下一步**,而不是因为"缺宿主"
8
+ * 就整体判红(那是把**宿主档事实**当成了**内核故障** —— 契约 §1 的易混点 + §2⑥)。
9
+ *
10
+ * ## 检查项(7 条;每条都指向唯一判据,本文件**不复制**任何判据)
11
+ * | id | 检查 | 唯一判据(本文件只调用) | 什么算 fail |
12
+ * |---|---|---|---|
13
+ * | `data_dir` | 数据目录:解析链命中哪一级 / 实际目录 / 是否隔离 / 可写性 | `core/lib/argent-datadir.mjs#resolveDataDir`(判断⑧) | **存在但不可写**(或最近祖先不可写) |
14
+ * | `interpreter` | 解释器:多候选探测结论 + 缺失时的修复指引 | `core/lib/python.mjs#pickPython`(D3:多候选 + 响亮失败,与 `flow-trigger.sh#pick_py` 同源) | 全部候选不可用(**没有它跑不了任何流程**) |
15
+ * | `skills` | 外发技能:5 个是否已装 / 装在哪个根 / 内容与仓库是否一致 | 外发集 = `scripts/check-skill-classification.mjs#classify`(唯一真源);目录约定真源 = `scripts/skill-install.mjs#hostTable`(本文件只做**只读候选表**,由回归测试逐项对拍) | 归类门禁不通过,或**已装但内容与仓库不一致** |
16
+ * | `license` | 许可:三态(valid/grace/denied)+ 设备绑定态 + 到期/宽限 | `core/lib/license-verdict.mjs#licenseVerdict`(§3.5① 判定点唯一在 CLI 进程内) | `denied` **且不是因为"没装许可"**(验签失败/过期/设备不匹配…) |
17
+ * | `channels` | 通知通道:可用 / 预留位 | `core/lib/notify/index.mjs#listChannels`(P2 端口) | 一条可用通道都没有(异常上报没有出口) |
18
+ * | `source` | 数据源可达性:**只读**探测(能否列出流程 / 声明的 kind 是否已实现) | `core/lib/flow-registry.mjs#loadRegistry` + `core/lib/flow-list.mjs#summarizeFlows` + `scripts/flow_source.py --list-sources` | 注册表读不到,或流程声明的 `source.kind` **没有实现** |
19
+ * | `host` | **宿主档事实**:dsh 在不在 / 补丁清单 / 定时器与面板的权威判据在哪 | 只探测、如实上报(`core/lib/versions.mjs#patchesProfile`);**从不**调 `launchctl` | **永不 fail**:`applicable:false` = 本档不适用(契约 §1:会话唤醒/宿主调度/系统集成归 L2) |
20
+ *
21
+ * ## A1 退出码(唯一口径,§P0-A)
22
+ * · `0` —— 全部 `ok`/`warn`。**warn 只在 JSON 里标记,不占用 2**(A1 明写"`2` 不得再被用作
23
+ * '有告警 / 部分成功 / 需要人工看一眼'");许可宽限也只记 `warn`(`3` 是**业务放行**的码,
24
+ * 自检不放行任何业务 ⇒ 不回传 3,许可态在 JSON 的 `license.state` 里如实给出)。
25
+ * · `1` —— 存在 `fail`(`reason:"check_failed"`,见 `reasonCodes`)。
26
+ * · `2` —— **用法错误**(未知参数 / `--customer` 收到 `--flow` 这类互斥形态)。
27
+ *
28
+ * ## A2 JSON(`--json` 时 **stdout 只有一份 JSON**)
29
+ * `ok/empty/code/reason` 恒在;外加 `checks[]`(每条 `level` ∈ ok|warn|fail + `detail` 事实 +
30
+ * `advice` 处置建议 + `evidence` 取证)与 `summary`。`code` 恒等于进程退出码。
31
+ *
32
+ * ## A3 / 零副作用(**硬约束,写进代码也写进输出**)
33
+ * · 本 verb **只读**:不建目录(缺目录 ⇒ `warn` + 建议 `argent init --apply`)、不写任何文件、
34
+ * 不写 `~/.dsh/settings.yaml`、不碰 credentials、**不真发通知**(只看通道清单);
35
+ * · 可写性探测用 `accessSync(dir, W_OK)`(**不落探针文件**);数据源探测**只列流程/只列 kind**,
36
+ * 不读远端表("能不能读源"的权限核验是 `python3 scripts/flow_preflight.py --flow <id> --online`);
37
+ * · 输出里的 `sideEffects` 是**机器可读的自证**:调用方可以据此断言"这次自检没动过任何东西"。
38
+ *
39
+ * ## ⛔ 本文件不做什么
40
+ * ① **不要求 dsh 存在**(M1 判据③);② 不复述任何判断(数据目录链/解释器候选/许可三态/
41
+ * 外发集/通道表/注册表合并**全部**从各自唯一判据取);③ 不 import L2 宿主档模块
42
+ * (契约 §1 明写"不得让 L0/L1 反过来依赖 L2")—— 故技能根的**目录约定**在这里是一张
43
+ * **只读候选表**,真源仍是 `scripts/skill-install.mjs#hostTable`,两者由
44
+ * `scripts/tests/doctor-customer.test.mjs` **逐项对拍**(漂移即红,形态同 `argent-datadir-parity`)。
45
+ *
46
+ * 跑法:`node core/cli/argent.mjs doctor --customer [--json]`;回归 `node scripts/tests/doctor-customer.test.mjs`
47
+ */
48
+ import { execFileSync } from 'node:child_process'
49
+ import { accessSync, constants as FS, existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
50
+ import { createHash } from 'node:crypto'
51
+ import { delimiter, join, sep } from 'node:path'
52
+ import { dshHomeOf, osHome, resolveDataDir } from './argent-datadir.mjs'
53
+ import { REASON_TEXT, pickPython } from './python.mjs'
54
+ import { licenseVerdict } from './license-verdict.mjs'
55
+ import { listChannels } from './notify/index.mjs'
56
+ import { loadRegistry } from './flow-registry.mjs'
57
+ import { summarizeFlows } from './flow-list.mjs'
58
+ import { patchesProfile, probeDsh } from './versions.mjs'
59
+ import { SKILL_FILE, classify } from '../../scripts/check-skill-classification.mjs'
60
+
61
+ /** 检查项 id(顺序即人读顺序;`--json` 的 `checks[]` 同序)。 */
62
+ export const CHECK_IDS = ['data_dir', 'interpreter', 'skills', 'license', 'channels', 'source', 'host']
63
+ /** 三档(每条检查恒取其一;"本档不适用"用 `applicable:false` 表达,**不是**第四档,更不是 fail)。 */
64
+ export const LEVELS = ['ok', 'warn', 'fail']
65
+
66
+ const sha12 = (buf) => createHash('sha256').update(buf).digest('hex').slice(0, 12)
67
+
68
+ /** `--customer` 的用法串(`--help` 与用法错误共用一份)。 */
69
+ export const DOCTOR_CUSTOMER_USAGE = [
70
+ '用法:argent doctor --customer [--json]',
71
+ '',
72
+ '客户机一页式自检(**只读**):数据目录 / 解释器 / 外发技能 / 许可 / 通知通道 / 数据源可达性 / 宿主档事实。',
73
+ '退出码(A1):全部 ok·warn ⇒ 0(warn 只在 JSON 标记);存在 fail ⇒ 1;用法错误 ⇒ 2。',
74
+ '⛔ 不要求本机装 dsh(无宿主 ⇒ 宿主档事实记「本档不适用」,**不算 FAIL**);',
75
+ '⛔ 不写任何文件 / 凭据 / settings,不建目录,不真发通知。',
76
+ '单流程前置检查仍走:`argent doctor --flow <id> [--online]`(同一脚本 flow_preflight.py,判据不重复)。',
77
+ ].join('\n')
78
+
79
+ // ── 只读小工具 ─────────────────────────────────────────────────────────────
80
+
81
+ /** 在 PATH 上找一个可执行文件(**不 spawn**、不 shell out 到 `which`;极简 PATH 下更可靠)。 */
82
+ export function findOnPath(name, { env = process.env, exists = existsSync } = {}) {
83
+ for (const dir of String(env.PATH || '').split(delimiter)) {
84
+ if (!dir) continue
85
+ const p = join(dir, name)
86
+ if (exists(p)) return p
87
+ }
88
+ return ''
89
+ }
90
+
91
+ /** 目录可写性:`accessSync(W_OK)`(**只读调用,不落探针文件**)。 */
92
+ export function writableOf(dir, { access = accessSync, exists = existsSync } = {}) {
93
+ let cur = String(dir || '')
94
+ const missing = []
95
+ while (cur && cur !== '/' && !exists(cur)) { missing.push(cur); cur = join(cur, '..') }
96
+ if (!exists(cur)) return { ok: false, checked: null, missing: missing.length, why: '整条路径都不存在(连根都没有)' }
97
+ try {
98
+ access(cur, FS.W_OK)
99
+ return { ok: true, checked: cur, missing: missing.length, why: '' }
100
+ } catch (e) {
101
+ return { ok: false, checked: cur, missing: missing.length, why: `${(e && e.code) || e}` } // 只给错误码(调用方拼「不可写(…)」)
102
+ }}
103
+
104
+ /** 技能目录 → `{相对路径: sha12}`(与 `scripts/skill-install.mjs#skillFiles` 同形;由回归测试逐项对拍)。 */
105
+ export function skillFileDigests(dir, { exists = existsSync, readdir = readdirSync, stat = statSync, readFile = readFileSync } = {}) {
106
+ const out = {}
107
+ const walk = (d, base) => {
108
+ let entries = []
109
+ try { entries = readdir(d, { withFileTypes: true }) } catch { return }
110
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
111
+ if (e.name === '.DS_Store') continue
112
+ const p = join(d, e.name)
113
+ let st = null
114
+ try { st = stat(p) } catch { continue }
115
+ if (st.isDirectory()) walk(p, base)
116
+ else if (st.isFile()) out[p.slice(base.length + 1).split(sep).join('/')] = sha12(readFile(p))
117
+ }
118
+ }
119
+ if (!exists(dir)) return out
120
+ walk(dir, dir)
121
+ return out
122
+ }
123
+
124
+ /** 逐文件比较两份摘要(同键集且同 sha ⇒ 一致;给出差异明细,便于"可照做"的建议)。 */
125
+ export function diffDigests(a, b) {
126
+ const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort()
127
+ const different = keys.filter((k) => a[k] && b[k] && a[k] !== b[k])
128
+ const missing = keys.filter((k) => a[k] && !b[k])
129
+ const extra = keys.filter((k) => !a[k] && b[k])
130
+ return { same: different.length === 0 && missing.length === 0 && extra.length === 0, different, missing, extra, files: keys.length }
131
+ }
132
+
133
+ /** SKILL.md frontmatter 的 `version`(与 `scripts/skill-install.mjs#frontmatterVersion` 同形;测试对拍)。 */
134
+ export function frontmatterVersion(text) {
135
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(String(text || ''))
136
+ if (!m) return null
137
+ const v = /^\s*version\s*:\s*(.+?)\s*$/m.exec(m[1])
138
+ return v ? v[1].replace(/^["']|["']$/g, '') : null
139
+ }
140
+
141
+ /**
142
+ * 技能根**候选表**(只读探测用)。
143
+ * ⚠️ **真源是 `scripts/skill-install.mjs#hostTable`(L2 安装器)** —— 本表只是 L1 侧的**只读副本**
144
+ * (契约 §1:不得让 L1 反过来依赖 L2)⇒ 由回归测试 `scripts/tests/doctor-customer.test.mjs`
145
+ * 与 `hostTable()` **逐项对拍**(目录、宿主 id 必须完全一致;漂移即红)。
146
+ */
147
+ export function skillRootCandidates({ repo = '', dataDir = '', dshHome = '', home = '' } = {}) {
148
+ const dsh = dshHome || join(home, '.dsh')
149
+ return [
150
+ { id: 'data', who: 'Argent 数据目录', dir: join(dataDir, 'skills'), installHost: null, source: false },
151
+ { id: 'dsh', who: 'dsh 宿主', dir: join(dsh, 'skills'), installHost: 'dsh', source: false },
152
+ { id: 'claude-code', who: 'Claude Code', dir: join(home, '.claude', 'skills'), installHost: 'claude-code', source: false },
153
+ { id: 'codex', who: 'Codex', dir: join(home, '.codex', 'skills'), installHost: 'codex', source: false },
154
+ { id: 'cursor', who: 'Cursor', dir: join(home, '.cursor', 'skills'), installHost: 'cursor', source: false },
155
+ { id: 'repo', who: '仓库技能源(不是安装目标)', dir: join(repo, 'skills'), installHost: null, source: true },
156
+ ]
157
+ }
158
+
159
+ // ── 七条检查(每条返回 { id, title, level, applicable, detail, advice, evidence })──────
160
+
161
+ /** ① 数据目录:解析链 / 实际目录 / 是否隔离 / 可写性。 */
162
+ export function checkDataDir({ env = process.env, ...deps } = {}) {
163
+ const r = deps.resolved || resolveDataDir({ env })
164
+ const exists = (deps.exists || existsSync)(r.dir)
165
+ const w = writableOf(r.dir, deps)
166
+ const isolatedNote = r.isolated
167
+ ? '隔离态:写入不会进默认目录;失败回路(flow-watch 泵)在隔离目录下**默认暂停**(除非 ARGENT_ALLOW_ISOLATED_PUMP=1)'
168
+ : '默认目录:失败回路按默认运行'
169
+ let level = 'ok'; let advice = ''
170
+ if (!exists && w.ok) {
171
+ level = 'warn'
172
+ advice = `目录还不存在(首次运行):跑 \`node core/cli/argent.mjs init --apply\` 建它;要钉死位置用 ARGENT_DATA_DIR=<绝对路径>`
173
+ } else if (!exists && !w.ok) {
174
+ level = 'fail'
175
+ advice = `最近的已存在祖先也不可写:${w.checked || '(无)'} —— 换一个可写目录(ARGENT_DATA_DIR=<可写绝对路径>),或修正目录权限`
176
+ } else if (exists && !w.ok) {
177
+ level = 'fail'
178
+ advice = `数据目录不可写(${w.why}):跑 \`chmod u+w ${r.dir}\`,或把 ARGENT_DATA_DIR 指到可写目录;在修好之前一切写类 verb 都会失败`
179
+ } else if (r.isolated) {
180
+ level = 'warn'
181
+ advice = '这是**隔离**目录(非默认):确认它就是你想要的那棵树;隔离目录不是错误,但失败回路会默认暂停'
182
+ }
183
+ return {
184
+ id: 'data_dir', title: '数据目录', level, applicable: true,
185
+ detail: `${r.dir}(来源=${r.source}${r.isolated ? ',隔离' : ''};存在=${exists};可写=${w.ok})`,
186
+ advice,
187
+ evidence: {
188
+ dir: r.dir, raw: r.raw, source: r.source, isolated: r.isolated, defaultDir: r.defaultDir, dshHome: r.dshHome,
189
+ exists, writable: w.ok, writableChecked: w.checked, missingLevels: w.missing,
190
+ chain: [
191
+ { level: 'ARGENT_DATA_DIR', hit: r.source === 'ARGENT_DATA_DIR', value: env.ARGENT_DATA_DIR ?? null },
192
+ { level: 'DSH_HOME', hit: r.source === 'DSH_HOME', value: env.DSH_HOME ? join(r.dshHome, 'argent') : null },
193
+ { level: 'default', hit: r.source === 'default', value: r.defaultDir },
194
+ ],
195
+ note: isolatedNote,
196
+ authoritative: 'core/lib/argent-datadir.mjs#resolveDataDir(判断⑧,全仓库唯一一份链)',
197
+ },
198
+ }
199
+ }
200
+
201
+ /** ② 解释器:多候选探测结论 + 修复指引(**与 D3 同口径**)。 */
202
+ export function checkInterpreter({ env = process.env, dataDir = '', repo = '', picker = null } = {}) {
203
+ const p = picker || pickPython({ env, dataDir, repo })
204
+ const tried = (p.tried || []).map((t) => ({ path: t.path, source: t.source, ok: t.ok, reason: t.reason, detail: t.detail, version: t.version || null }))
205
+ if (p.ok) {
206
+ return {
207
+ id: 'interpreter', title: '解释器', level: 'ok', applicable: true,
208
+ detail: `${p.path}(${(p.version || []).join('.') || '版本未知'};来源=${p.source};探测 ${tried.length} 个候选)`,
209
+ advice: '',
210
+ evidence: { ok: true, path: p.path, version: p.version || null, source: p.source || null, tried, authoritative: 'core/lib/python.mjs#pickPython(与 scripts/flow-trigger.sh#pick_py 同源)' },
211
+ }
212
+ }
213
+ return {
214
+ id: 'interpreter', title: '解释器', level: 'fail', applicable: true,
215
+ detail: `找不到可用解释器(探测 ${tried.length} 个候选全部失败):`
216
+ + (tried.map((t) => `${t.source}=${t.path}(${REASON_TEXT[t.reason] || t.reason})`).join(';') || '(无候选)'),
217
+ advice: `修复:① \`python3 -m venv ${dataDir || '<数据目录>'}/venv && ${dataDir || '<数据目录>'}/venv/bin/pip install openpyxl pyyaml\`;`
218
+ + '② 或显式指定 `ARGENT_PY=/path/to/python3`(须 ≥ 3.10 且能 import openpyxl、yaml)。'
219
+ + '在修好之前:任何流程都跑不起来(这是硬失败,不是"没结果")。',
220
+ evidence: { ok: false, tried, diagnostics: p.diagnostics || null, authoritative: 'core/lib/python.mjs#pickPython(D3:多候选 + 响亮失败)' },
221
+ }
222
+ }
223
+
224
+ /**
225
+ * ③ 技能:外发集是否已装 / 装在哪个根 / 内容与仓库是否一致。
226
+ * @param {{repo?:string, roots?:Array, classifySkills?:Function, digests?:Function}} o
227
+ */
228
+ export function checkSkills({ repo = '', roots = [], classifySkills = null, digests = skillFileDigests } = {}) {
229
+ const cls = (classifySkills || (({ root }) => classify({ root })))({ root: repo })
230
+ const external = cls.external || []
231
+ const repoRoot = roots.find((r) => r.source) || null
232
+ const targets = roots.filter((r) => !r.source)
233
+ const repoDigests = {}
234
+ for (const name of external) {
235
+ const d = join(repo, 'skills', name)
236
+ repoDigests[name] = (repoRoot && existsSync(d)) ? digests(d) : null
237
+ }
238
+ const rows = targets.map((r) => {
239
+ const installed = []
240
+ const missing = []
241
+ const mismatched = []
242
+ const skills = {}
243
+ for (const name of external) {
244
+ const dir = join(r.dir, name)
245
+ const has = existsSync(join(dir, SKILL_FILE))
246
+ if (!has) { missing.push(name); continue }
247
+ installed.push(name)
248
+ const cur = digests(dir)
249
+ const want = repoDigests[name]
250
+ const cmp = want ? diffDigests(want, cur) : { same: null, different: [], missing: [], extra: [], files: Object.keys(cur).length }
251
+ if (cmp.same === false) mismatched.push({ name, ...cmp })
252
+ let version = null
253
+ try { version = frontmatterVersion(readFileSync(join(dir, SKILL_FILE), 'utf8')) } catch { /* 读不到就不报版本 */ }
254
+ skills[name] = { installed: true, match: cmp.same, version, files: Object.keys(cur).length, diff: cmp.same === false ? { different: cmp.different, missing: cmp.missing, extra: cmp.extra } : null }
255
+ }
256
+ return {
257
+ id: r.id, who: r.who, dir: r.dir, exists: existsSync(r.dir),
258
+ installed, missing, mismatched: mismatched.map((m) => m.name), skills,
259
+ manifest: existsSync(join(r.dir, '.argent-skills.json')),
260
+ }
261
+ })
262
+ const clean = rows.filter((r) => r.installed.length === external.length && r.mismatched.length === 0 && external.length > 0)
263
+ const dirty = rows.filter((r) => r.mismatched.length > 0)
264
+ const anyInstalled = rows.filter((r) => r.installed.length > 0)
265
+ const repoMissing = external.length > 0 && repoDigests[external[0]] === null
266
+
267
+ let level = 'warn'; let advice = ''
268
+ let detail = ''
269
+ if (!cls.ok) {
270
+ level = 'fail'
271
+ detail = `归类门禁不通过(${(cls.errors || []).length} 条)⇒ 外发集不可信,先修门禁再谈安装`
272
+ advice = '跑 `node scripts/check-skill-classification.mjs`(未归类即红)——修好前**不要**安装外发技能'
273
+ } else if (!external.length) {
274
+ level = 'warn'
275
+ detail = '没有任何业务线声明外发技能(外发集为空)'
276
+ advice = '若这台机器本该跑流程,检查业务线声明 `verticals/*/manifest.json#skills` 是否到位'
277
+ } else if (repoMissing) {
278
+ level = 'warn'
279
+ detail = `本机没有仓库技能源目录(${join(repo, 'skills')})⇒ 无法比对内容(这不代表技能没装)`
280
+ advice = '把完整交付包放到本机(`skills/` 与 `core/`、`scripts/` 同批),再重跑本自检'
281
+ } else if (clean.length) {
282
+ level = 'ok'
283
+ detail = `外发 ${external.length} 个全部已装且内容与仓库一致:${clean.map((r) => `${r.id}(${r.dir})`).join('、')}`
284
+ } else if (dirty.length) {
285
+ level = 'fail'
286
+ const bad = dirty.flatMap((r) => r.mismatched.map((n) => `${r.id}:${n}`))
287
+ detail = `已装但内容与仓库**不一致**:${bad.join('、')} ⇒ 客户机跑的是改过的/过期的技能(判断会跟着漂)`
288
+ advice = `重装(会先备份再覆盖):\`node scripts/skill-install.mjs --host <宿主> --list\` 看清单,\`node scripts/skill-install.mjs --host <宿主> --apply\` 真装`
289
+ } else if (anyInstalled.length) {
290
+ level = 'warn'
291
+ detail = `只装了一部分:${anyInstalled.map((r) => `${r.id} ${r.installed.length}/${external.length}(缺 ${r.missing.join('、')})`).join(';')}`
292
+ advice = `补齐:\`node scripts/skill-install.mjs --host <宿主> --apply\`(缺省 dry-run 会先打印计划)`
293
+ } else {
294
+ level = 'warn'
295
+ detail = `外发 ${external.length} 个技能在常见宿主根里都还没装(已探测 ${targets.length} 个根)`
296
+ advice = '先看清单:`node scripts/skill-install.mjs --host <客户宿主> --list`;确认后 `--apply` 真装(缺省 dry-run)'
297
+ }
298
+ return {
299
+ id: 'skills', title: '外发技能', level, applicable: true, detail, advice,
300
+ evidence: {
301
+ external, internal: cls.internal || [], classificationOk: Boolean(cls.ok),
302
+ classificationErrors: (cls.errors || []).map((e) => `${e.rule}:${e.message}`),
303
+ repoSkillsDir: join(repo, 'skills'), repoSkillsPresent: !repoMissing,
304
+ roots: rows,
305
+ note: '外发集真源 = 各 verticals/*/manifest.json#skills 的并集(scripts/check-skill-classification.mjs#classify);'
306
+ + '目录约定真源 = scripts/skill-install.mjs#hostTable(本表由回归测试逐项对拍)',
307
+ },
308
+ }
309
+ }
310
+
311
+ /** ④ 许可:三态 + 设备绑定态 + 到期/宽限(**缺失不算 FAIL**)。 */
312
+ export function checkLicense({ env = process.env, now = Date.now(), verdict = null, verdictFn = null } = {}) {
313
+ const v = verdict || (verdictFn || licenseVerdict)({ env, now })
314
+ const dev = v.device || {}
315
+ const facts = {
316
+ state: v.state, exit: v.exit, reason: v.reason || null,
317
+ file: v.path || null, exists: Boolean(v.path && existsSync(v.path)),
318
+ expiresAt: v.expiresAt || null, graceDays: v.graceDays ?? null, graceUntil: v.graceUntil || null,
319
+ graceSource: v.graceSource || null, allowed: v.allowed ?? null,
320
+ device: { present: Boolean(dev.present), signed: Boolean(dev.signed), state: dev.state || null, reason: dev.reason || null, rebindsLeft: dev.rebindsLeft ?? null, graceUntil: dev.graceUntil || null, machineReadable: Boolean(dev.machineReadable) },
321
+ authoritative: 'core/lib/license-verdict.mjs#licenseVerdict(§3.5① 判定点唯一在 CLI 进程内)',
322
+ }
323
+ const deviceText = facts.device.present
324
+ ? `设备绑定=${facts.device.state}(重绑剩余 ${facts.device.rebindsLeft ?? '—'})`
325
+ : '设备绑定=未声明(存量 unbound 许可:不比对、放行 —— D1 兼容)'
326
+ if (v.state === 'valid') {
327
+ return { id: 'license', title: '许可', level: 'ok', applicable: true,
328
+ detail: `valid${facts.reason ? `(${facts.reason})` : ''};到期 ${facts.expiresAt || '—'};${deviceText}`, advice: '', evidence: facts }
329
+ }
330
+ if (v.state === 'grace') {
331
+ return { id: 'license', title: '许可', level: 'warn', applicable: true,
332
+ detail: `grace(宽限期,业务放行但要上报):来源=${facts.graceSource || '—'},宽限至 ${facts.graceUntil || '—'};到期 ${facts.expiresAt || '—'};${deviceText}`,
333
+ advice: '继续跑业务,但**必须**把"许可在宽限期内"上报给人;续期 = 拿一份新许可文件后 `node core/cli/argent.mjs license install --file <path> --apply`',
334
+ evidence: facts }
335
+ }
336
+ // denied:**只有"没装许可"不算 FAIL**(M1 判据③ 的同精神:缺件是事实,不是故障);其余 denied 是真问题
337
+ const missingOnly = facts.reason === 'missing' && !facts.exists
338
+ return {
339
+ id: 'license', title: '许可',
340
+ level: missingOnly ? 'warn' : 'fail',
341
+ applicable: true,
342
+ detail: missingOnly
343
+ ? `denied(reason=missing):本机还没有许可文件(${facts.file || '<数据目录>/license.json'})`
344
+ : `denied(reason=${facts.reason || '—'}):许可存在但不可用(验签失败/过期/设备不匹配…)`,
345
+ advice: missingOnly
346
+ ? '**缺许可不是故障**(写类 verb 会被拒,读类与自检照常):拿签发文件后 `node core/cli/argent.mjs license install --file <path>`(缺省 dry-run,`--apply` 才落盘);'
347
+ + '先看设备码:`node core/cli/argent.mjs license request`'
348
+ : `跑 \`node core/cli/argent.mjs license explain --json\` 看"缺什么/下一步";`
349
+ + '设备不匹配且自助重绑没用完 ⇒ `license install --file <path> --rebind --apply`',
350
+ evidence: facts,
351
+ }
352
+ }
353
+
354
+ /** ⑤ 通知通道:可用 / 预留位(**只列表,绝不实发**)。 */
355
+ export function checkChannels({ channels = null, listFn = null } = {}) {
356
+ const all = channels || (listFn || listChannels)()
357
+ const available = all.filter((c) => c.implemented).map((c) => c.channel)
358
+ const reserved = all.filter((c) => !c.implemented).map((c) => c.channel)
359
+ const level = available.length ? 'ok' : 'fail'
360
+ return {
361
+ id: 'channels', title: '通知通道', level, applicable: true,
362
+ detail: available.length
363
+ ? `可用:${available.join('、')}|预留位(端口已定、实现未落地):${reserved.join('、') || '(无)'}`
364
+ : `一条可用通道都没有(全部是预留位:${reserved.join('、')})⇒ 异常/上报没有出口`,
365
+ advice: available.length
366
+ ? '预留位不是故障(通知**投递**归 L2 宿主):`argent notify --channel <id>` 选到未实现通道会**响亮报错**,不会静默成功;'
367
+ + '要发一次自测(**会真发包**):`node core/cli/argent.mjs notify --channel webhook --url <你的接收端> --text hi --apply`'
368
+ : '至少落地一条通道(webhook 端口已实现,配置 URL 即可用)',
369
+ evidence: { available, reserved, all, probe: false, note: '本自检**不真发通知**(只看通道清单:send 只在 `argent notify --apply` 时发生)' },
370
+ }
371
+ }
372
+
373
+ /**
374
+ * ⑥ 数据源可达性(**只读探测**):能否列出流程 / 声明的 kind 是否已实现。
375
+ * ⛔ 不读远端表、不联网、不写任何东西 —— 权限核验是 `flow_preflight.py --flow <id> --online` 的事。
376
+ */
377
+ export function checkSource({ repo = '', py = '', pyFailure = null, doc = null, kinds = null, listFn = null, loadFn = null, summarizeFn = null } = {}) {
378
+ const base = { authoritative: 'core/lib/flow-registry.mjs#loadRegistry(合并真源 scripts/flow_registry.py)+ scripts/flow_source.py --list-sources(数据面端口表)', readProbe: { performed: false, why: '本自检不读写远端表:只列流程与数据面;能不能真读源(权限/身份)用 `python3 scripts/flow_preflight.py --flow <id> --online` 核' } }
379
+ if (pyFailure || !py) {
380
+ return {
381
+ id: 'source', title: '数据源可达性', level: 'warn', applicable: true,
382
+ detail: `未探测(解释器不可用:${(pyFailure || '未指定解释器').split('\n')[0]})`,
383
+ advice: '先把解释器修好(见「解释器」一条),再重跑本自检;现在**不知道**流程与数据面是否就位',
384
+ evidence: { ...base, probed: false, reason: 'python_unavailable', error: pyFailure || null },
385
+ }
386
+ }
387
+ const reg = doc ? { ok: true, doc } : (loadFn || loadRegistry)({ repo, py })
388
+ if (!reg.ok) {
389
+ return {
390
+ id: 'source', title: '数据源可达性', level: 'fail', applicable: true,
391
+ detail: `列不出流程:注册表读不到(${reg.reason || 'unknown'})${reg.error ? ` —— ${String(reg.error).split('\n')[0]}` : ''}`,
392
+ advice: '确认交付包完整(`scripts/flow-requirements.json` + `verticals/*/flows.json` + `scripts/flow_registry.py`),'
393
+ + '再跑 `node core/cli/argent.mjs flow list --json` 复核',
394
+ evidence: { ...base, probed: true, registry: { ok: false, reason: reg.reason || null, error: reg.error || null } },
395
+ }
396
+ }
397
+ const sum = (summarizeFn || summarizeFlows)(reg.doc, {})
398
+ const declaredKinds = [...new Set(sum.rows.map((r) => r.sourceKind).filter(Boolean))].sort()
399
+ const impl = kinds || probeSourceKinds({ repo, py })
400
+ const implemented = impl.ok ? Object.keys(impl.kinds) : []
401
+ const unknown = declaredKinds.filter((k) => !implemented.includes(k))
402
+ const level = unknown.length ? 'fail' : 'ok'
403
+ return {
404
+ id: 'source', title: '数据源可达性', level, applicable: true,
405
+ detail: `能列出流程 ${sum.rows.length} 条(业务线 ${sum.verticalIds.join('、') || '(无)'});`
406
+ + `声明的数据面:${declaredKinds.join('、') || '(无)'};引擎已实现:${implemented.join('、') || `(读不到:${impl.error || '未知'})`}`
407
+ + (unknown.length ? `;❌ 未实现:${unknown.join('、')}` : ''),
408
+ advice: unknown.length
409
+ ? `流程声明的 source.kind「${unknown.join('、')}」没有实现 ⇒ 这些流程**跑不起来**(加数据面 = 加一个 \`scripts/flow_sources/<kind>.py\` 并在 MODULES 登记,见 P4 端口契约)`
410
+ : (impl.ok ? '' : '数据面端口表读不到(不影响"能否列出流程"的结论,但少了"声明的 kind 有没有实现"这一向);'
411
+ + '跑 `python3 scripts/flow_source.py --list-sources` 复核'),
412
+ evidence: {
413
+ ...base, probed: true,
414
+ registry: { ok: true, flows: sum.rows.length, verticals: sum.verticalIds, rows: sum.rows.map((r) => ({ id: r.id, sourceKind: r.sourceKind, capability: r.capability, skill: r.skill })) },
415
+ kinds: { declared: declaredKinds, implemented, ok: Boolean(impl.ok), error: impl.error || null },
416
+ },
417
+ }
418
+ }
419
+
420
+ /** `python3 scripts/flow_source.py --list-sources`(只读;失败如实回报,不抛栈)。 */
421
+ export function probeSourceKinds({ repo = '', py = '', run = null } = {}) {
422
+ const exec = run || ((bin, argv) => execFileSync(bin, argv, { encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] }))
423
+ try {
424
+ const out = exec(py, [join(repo, 'scripts', 'flow_source.py'), '--list-sources'])
425
+ const json = JSON.parse(String(out).slice(String(out).indexOf('{')))
426
+ return { ok: true, kinds: json, error: null }
427
+ } catch (e) {
428
+ return { ok: false, kinds: {}, error: String((e && e.message) || e).slice(0, 200) }
429
+ }
430
+ }
431
+
432
+ /** ⑦ 宿主档事实:dsh / 补丁 / 定时器与面板(**永不 FAIL**;无宿主 ⇒ applicable:false)。 */
433
+ export function checkHost({ env = process.env, repo = '', which = null, dshVersion = undefined, patches = null } = {}) {
434
+ const dshHome = dshHomeOf(env)
435
+ const dshHomeExists = existsSync(dshHome)
436
+ const dshBin = which ? which('dsh') : findOnPath('dsh', { env })
437
+ const applicable = Boolean(dshBin || dshHomeExists)
438
+ const version = dshVersion !== undefined ? dshVersion : (dshBin ? probeDsh() : '')
439
+ const p = patches || patchesProfile(repo)
440
+ const settingsPath = join(dshHome, 'settings.yaml')
441
+ const settingsExists = existsSync(settingsPath)
442
+ const facts = {
443
+ dshBin: dshBin || null, dshVersion: version || null, dshHome, dshHomeExists,
444
+ settingsYamlExists: settingsExists,
445
+ patches: { present: Boolean(p.present), dir: p.dir, count: p.count || 0, patches: p.patches || [] },
446
+ authoritative: {
447
+ triggers: 'bash scripts/flow-timers.sh status(launchctl + LaunchAgent 是宿主档;本页**不调** launchctl)',
448
+ patches: 'node scripts/dsh-upgrade.mjs --patches-only(补丁在不在位)',
449
+ panels: '宿主 profile 组合(plugins/argent-* 的 settings.section)—— 面板渲染归 L2',
450
+ },
451
+ productionHomeRule: '生产在客户机(Huangwei):**本机(开发/测试 Mac)`launchctl list | grep flow-trigger` 必须为 0**;客户机才应有 4 条。',
452
+ note: '宿主档事实(触发器/补丁/面板/会话唤醒/系统集成)归 L2(契约 §1)——本页只如实列事实,**不重算判据、不判 FAIL**。',
453
+ }
454
+ if (!applicable) {
455
+ return {
456
+ id: 'host', title: '宿主档事实', level: 'ok', applicable: false,
457
+ detail: `本档不适用:本机没有 dsh(dsh 不在 PATH,且 <dsh home>=${dshHome} 不存在)—— `
458
+ + '触发器/补丁/面板都是**宿主档事实**(契约 §1 归 L2),无宿主时它们**没有意义**,因此**不算 FAIL**(M1 判据③)',
459
+ advice: '无需处置。这台机器按"无宿主档"(陪跑式:客户用自己的 agent 宿主)使用即可;'
460
+ + '若之后装了 dsh,重跑本自检会多出宿主档一行',
461
+ evidence: facts,
462
+ }
463
+ }
464
+ return {
465
+ id: 'host', title: '宿主档事实', level: 'ok', applicable: true,
466
+ detail: `dsh=${version || '(在 PATH 上但 --version 读不到)'}|dsh home=${dshHome}(存在)|`
467
+ + `settings.yaml=${settingsExists ? '在' : '不在'}|补丁清单 ${facts.patches.count} 个${facts.patches.present ? '' : '(缺目录)'}`,
468
+ advice: '',
469
+ evidence: facts,
470
+ }
471
+ }
472
+
473
+ // ── verb 入口 ──────────────────────────────────────────────────────────────
474
+
475
+ /**
476
+ * verb 入口:`argent doctor --customer [--json]`。
477
+ * @param {string[]} argv `doctor` 之后的参数(含 `--customer`)
478
+ * @param {{repo?:string, env?:object, now?:number, deps?:object}} [deps]
479
+ * `deps` 里每一项都可注入(夹具用):`resolved`/`picker`/`verdict`/`channels`/`doc`/`kinds`/`which`/`dshVersion`/`patches`/`roots`/`classifySkills`/`digests`
480
+ * @returns {{json:object, human:string, stderr:string, exit:number}} CLI 信封(`core/cli/argent.mjs` 消费)
481
+ */
482
+ export function runDoctorCustomerVerb(argv = [], deps = {}) {
483
+ let customer = false
484
+ for (const t of argv) {
485
+ if (t === '--customer') { customer = true; continue }
486
+ if (t === '--json') continue
487
+ if (t === '--help' || t === '-h') {
488
+ return { json: { ok: true, empty: false, code: 0, reason: null, verb: 'doctor', subcommand: 'customer', usage: DOCTOR_CUSTOMER_USAGE }, human: DOCTOR_CUSTOMER_USAGE, stderr: '', exit: 0 }
489
+ }
490
+ if (t === '--flow' || t === '--online') {
491
+ return doctorFail(2, 'usage_error', `${t} 与 --customer 互斥:整机自检不接受单流程参数(单流程用:argent doctor ${t} <值>)`)
492
+ }
493
+ return doctorFail(2, 'usage_error', `未知参数:${t}(用法:argent doctor --customer [--json])`)
494
+ }
495
+ if (!customer) return doctorFail(2, 'usage_error', '缺 --customer(用法:argent doctor --customer [--json];单流程检查用 argent doctor --flow <id>)')
496
+
497
+ const env = deps.env || process.env
498
+ const repo = deps.repo || ''
499
+ const now = deps.now || Date.now()
500
+ const d = deps.deps || {}
501
+ const home = osHome(env)
502
+
503
+ const dd = checkDataDir({ env, ...(d.dataDir || {}) })
504
+ const dataDir = dd.evidence.dir
505
+ const dshHome = dshHomeOf(env)
506
+
507
+ const interpreter = checkInterpreter({ env, dataDir, repo, picker: d.picker || null })
508
+ const skills = checkSkills({
509
+ repo,
510
+ roots: d.roots || skillRootCandidates({ repo, dataDir, dshHome, home }),
511
+ classifySkills: d.classifySkills || null,
512
+ digests: d.digests || skillFileDigests,
513
+ })
514
+ const license = checkLicense({ env, now, verdict: d.verdict || null, verdictFn: d.verdictFn || null })
515
+ const channels = checkChannels({ channels: d.channels || null })
516
+ const source = checkSource({
517
+ repo, py: d.py !== undefined ? d.py : (interpreter.evidence.ok ? interpreter.evidence.path : ''),
518
+ pyFailure: d.pyFailure !== undefined ? d.pyFailure : (interpreter.evidence.ok ? null : (interpreter.evidence.diagnostics || '解释器不可用')),
519
+ doc: d.doc || null, kinds: d.kinds || null, listFn: d.listFn || null, loadFn: d.loadFn || null, summarizeFn: d.summarizeFn || null,
520
+ })
521
+ const host = checkHost({ env, repo, which: d.which || null, dshVersion: d.dshVersion, patches: d.patches || null })
522
+
523
+ const checks = [dd, interpreter, skills, license, channels, source, host]
524
+ const summary = {
525
+ ok: checks.filter((c) => c.level === 'ok').length,
526
+ warn: checks.filter((c) => c.level === 'warn').length,
527
+ fail: checks.filter((c) => c.level === 'fail').length,
528
+ total: checks.length,
529
+ }
530
+ const failed = checks.filter((c) => c.level === 'fail')
531
+ const ok = failed.length === 0
532
+ const argent = (() => { try { return readFileSync(join(repo, 'VERSION'), 'utf8').trim() } catch { return null } })()
533
+
534
+ const json = {
535
+ // A2:`ok/empty/code/reason` 恒在;`code` 恒等于进程退出码
536
+ ok, empty: false, code: ok ? 0 : 1, reason: ok ? null : 'check_failed',
537
+ verb: 'doctor', subcommand: 'customer', form: 'customer', argent,
538
+ generatedAt: new Date(now).toISOString(),
539
+ summary,
540
+ checks,
541
+ // 回显**实际生效**的关键条件(A2:过滤/环境条件必须回显,未过滤写 null)
542
+ dataDir: { dir: dataDir, source: dd.evidence.source, isolated: dd.evidence.isolated, writable: dd.evidence.writable },
543
+ licenseState: license.evidence.state,
544
+ interpreterPath: interpreter.evidence.ok ? interpreter.evidence.path : null,
545
+ hostApplicable: host.applicable,
546
+ skillRootsScanned: skills.evidence.roots.map((r) => ({ id: r.id, dir: r.dir, installed: r.installed.length })),
547
+ sideEffects: {
548
+ writes: 'none', files: [], directories: [], settings: false, credentials: false, notifications: false, subprocesses: ['python(注册表/数据面端口表:只读)'],
549
+ note: '本 verb **只读**:不建目录、不写文件、不碰 settings.yaml/credentials、不真发通知;可写性用 access(W_OK) 探(不落探针文件)。',
550
+ },
551
+ authoritative: {
552
+ dataDir: dd.evidence.authoritative, interpreter: interpreter.evidence.authoritative, skills: skills.evidence.note,
553
+ license: license.evidence.authoritative, channels: 'core/lib/notify/index.mjs#listChannels(端口表)',
554
+ source: source.evidence.authoritative, host: host.evidence.note,
555
+ },
556
+ error: failed.length ? `存在 ${failed.length} 条 fail:${failed.map((c) => `${c.id}(${c.title})`).join('、')}` : null,
557
+ note: 'M1 判据③:无 dsh **不报 FAIL**(宿主档事实记「本档不适用」);缺许可、缺技能、缺目录都只记 warn + 处置建议;'
558
+ + '只有 fail 才 exit 1(A1),warn **不占用 2**。',
559
+ }
560
+
561
+ const icon = { ok: '✅', warn: '⚠️ ', fail: '❌' }
562
+ const human = [
563
+ `🩺 Argent 客户机自检(${ok ? '通过' : `未通过:${failed.length} 条 fail`} | ok ${summary.ok} / warn ${summary.warn} / fail ${summary.fail})`,
564
+ ...checks.map((c) => ` ${icon[c.level] || '·'} ${c.title}${c.applicable === false ? '(本档不适用)' : ''}:${c.detail}`
565
+ + (c.advice ? `\n ↳ ${c.advice}` : '')),
566
+ ' ⛔ 本次自检没有写任何文件 / 凭据 / settings,也没有发任何通知',
567
+ summary.fail ? ` ❌ 退出码 1(存在 fail);修好后重跑:argent doctor --customer` : ' ✅ 退出码 0(warn 只在 JSON 标记,不占用 2)',
568
+ ].join('\n')
569
+ return { json, human, stderr: '', exit: Number(json.code) }
570
+ }
571
+
572
+ function doctorFail(code, reason, error) {
573
+ return {
574
+ json: { ok: false, empty: false, code, reason, error, verb: 'doctor', subcommand: 'customer', checks: [], summary: { ok: 0, warn: 0, fail: 0, total: 0 }, usage: DOCTOR_CUSTOMER_USAGE },
575
+ human: `❌ ${reason}:${error}`, stderr: '', exit: code,
576
+ }
577
+ }