@whyshu/argent-engine 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/MANIFEST.json +307 -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 +575 -0
  55. package/engine/scripts/billing-mode-daily.sh +12 -0
  56. package/engine/scripts/build-engine-package.mjs +123 -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 +377 -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/config-explain.mjs +278 -0
  81. package/engine/scripts/configure-provider.rb +29 -0
  82. package/engine/scripts/customer-config-migrate.mjs +179 -0
  83. package/engine/scripts/customer-preflight.py +892 -0
  84. package/engine/scripts/customer-readiness.sh +134 -0
  85. package/engine/scripts/daily-report-inbox.py +242 -0
  86. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  87. package/engine/scripts/daily-report.mjs +1310 -0
  88. package/engine/scripts/daily-report.sh +46 -0
  89. package/engine/scripts/data/lip-product-dict.json +306 -0
  90. package/engine/scripts/deliver-customer.mjs +932 -0
  91. package/engine/scripts/delivery-faces.txt +31 -0
  92. package/engine/scripts/desktop-trial.sh +146 -0
  93. package/engine/scripts/dev-window.sh +260 -0
  94. package/engine/scripts/drill-clean-machine.sh +315 -0
  95. package/engine/scripts/dsh-api-client.mjs +215 -0
  96. package/engine/scripts/dsh-backup-scope.sh +83 -0
  97. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  98. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  99. package/engine/scripts/dsh-plane.mjs +153 -0
  100. package/engine/scripts/dsh-portforward.py +223 -0
  101. package/engine/scripts/dsh-preflight.mjs +179 -0
  102. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  103. package/engine/scripts/dsh-release-watch.mjs +458 -0
  104. package/engine/scripts/dsh-remote-check.sh +99 -0
  105. package/engine/scripts/dsh-restart.sh +179 -0
  106. package/engine/scripts/dsh-restore.sh +111 -0
  107. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  108. package/engine/scripts/dsh-snapshot.mjs +116 -0
  109. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  110. package/engine/scripts/dsh-upgrade.mjs +612 -0
  111. package/engine/scripts/dsh-url.sh +86 -0
  112. package/engine/scripts/dsh-web-launch.mjs +155 -0
  113. package/engine/scripts/ensure-store-windows.py +509 -0
  114. package/engine/scripts/entitlements.py +458 -0
  115. package/engine/scripts/env_probe.py +411 -0
  116. package/engine/scripts/feishu-card.mjs +252 -0
  117. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  118. package/engine/scripts/flow-auto-retry.py +454 -0
  119. package/engine/scripts/flow-requirements.json +720 -0
  120. package/engine/scripts/flow-scaffold.py +275 -0
  121. package/engine/scripts/flow-timers-plist.sh +86 -0
  122. package/engine/scripts/flow-timers.sh +182 -0
  123. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  124. package/engine/scripts/flow-trigger.sh +571 -0
  125. package/engine/scripts/flow_billing.py +354 -0
  126. package/engine/scripts/flow_capability_stats.py +314 -0
  127. package/engine/scripts/flow_case.py +194 -0
  128. package/engine/scripts/flow_claim.py +497 -0
  129. package/engine/scripts/flow_config.py +1024 -0
  130. package/engine/scripts/flow_failure.py +588 -0
  131. package/engine/scripts/flow_learn.py +105 -0
  132. package/engine/scripts/flow_ledger.py +235 -0
  133. package/engine/scripts/flow_policy.py +238 -0
  134. package/engine/scripts/flow_preflight.py +339 -0
  135. package/engine/scripts/flow_registry.py +341 -0
  136. package/engine/scripts/flow_retry.py +304 -0
  137. package/engine/scripts/flow_return_to_ops.py +191 -0
  138. package/engine/scripts/flow_source.py +315 -0
  139. package/engine/scripts/flow_sources/__init__.py +53 -0
  140. package/engine/scripts/flow_sources/cases_json.py +106 -0
  141. package/engine/scripts/flow_sources/common.py +428 -0
  142. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  143. package/engine/scripts/flow_stale.py +185 -0
  144. package/engine/scripts/flow_status_reconcile.py +344 -0
  145. package/engine/scripts/flow_triage.py +849 -0
  146. package/engine/scripts/flow_write_verify.py +496 -0
  147. package/engine/scripts/fulfillment.js +586 -0
  148. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  149. package/engine/scripts/gen-preset-rows.mjs +259 -0
  150. package/engine/scripts/init-ml.sh +2275 -0
  151. package/engine/scripts/install-internal-role.sh +190 -0
  152. package/engine/scripts/install-license.mjs +641 -0
  153. package/engine/scripts/install-one-shot.mjs +665 -0
  154. package/engine/scripts/integration_contract.py +220 -0
  155. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  156. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  157. package/engine/scripts/invoice-mapping.example.json +40 -0
  158. package/engine/scripts/invoice-table.sh +194 -0
  159. package/engine/scripts/invoice_carrier_notice.py +279 -0
  160. package/engine/scripts/invoice_fail_closeout.py +297 -0
  161. package/engine/scripts/invoice_lip_runner.py +1508 -0
  162. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  163. package/engine/scripts/invoice_ready_audit.py +191 -0
  164. package/engine/scripts/invoice_runner.py +675 -0
  165. package/engine/scripts/invoice_stageb.py +516 -0
  166. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  167. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  168. package/engine/scripts/lease.py +377 -0
  169. package/engine/scripts/lib/agent-install.mjs +649 -0
  170. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  171. package/engine/scripts/lib/argent_datadir.py +262 -0
  172. package/engine/scripts/lib/argent_filelock.py +507 -0
  173. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  174. package/engine/scripts/lib/cdp_ports.py +197 -0
  175. package/engine/scripts/lib/customer-env.mjs +102 -0
  176. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  177. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  178. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  179. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  180. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  181. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  182. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  183. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  184. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  185. package/engine/scripts/lib/patch-compose.mjs +241 -0
  186. package/engine/scripts/lib/patch-target.mjs +35 -0
  187. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  188. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  189. package/engine/scripts/lib/preset-compat.mjs +127 -0
  190. package/engine/scripts/lib/preset-install.mjs +575 -0
  191. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  192. package/engine/scripts/lib/preset-sync.mjs +59 -0
  193. package/engine/scripts/lib/profile-retire.mjs +97 -0
  194. package/engine/scripts/lib/python-runtime.mjs +190 -0
  195. package/engine/scripts/lib/registry.mjs +72 -0
  196. package/engine/scripts/lib/repo-root.sh +56 -0
  197. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  198. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  199. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  200. package/engine/scripts/lib/settings-panels.mjs +816 -0
  201. package/engine/scripts/lib/stdio_utf8.py +86 -0
  202. package/engine/scripts/lib/wake-auth.mjs +392 -0
  203. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  204. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  205. package/engine/scripts/license-e2e-drill.mjs +426 -0
  206. package/engine/scripts/license-gate.mjs +245 -0
  207. package/engine/scripts/lip_backlog.py +171 -0
  208. package/engine/scripts/lip_table.py +185 -0
  209. package/engine/scripts/local-usage.mjs +214 -0
  210. package/engine/scripts/migrate-providers.mjs +127 -0
  211. package/engine/scripts/migrate-skills.sh +65 -0
  212. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  213. package/engine/scripts/ml-store-ids.json +26 -0
  214. package/engine/scripts/ml_artifact_attribution.py +333 -0
  215. package/engine/scripts/ml_capacity_probe.py +1158 -0
  216. package/engine/scripts/ml_hub_status.py +1556 -0
  217. package/engine/scripts/ml_schema_check.py +115 -0
  218. package/engine/scripts/ml_store_ids.py +139 -0
  219. package/engine/scripts/npm-publish-argent.mjs +499 -0
  220. package/engine/scripts/npm-publish-argent.sh +20 -0
  221. package/engine/scripts/orchestrator.py +13811 -0
  222. package/engine/scripts/pack-plugins.mjs +140 -0
  223. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  224. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  225. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  226. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  227. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  228. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  229. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  230. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  231. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  232. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  233. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  234. package/engine/scripts/plan-role-rename.mjs +125 -0
  235. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  236. package/engine/scripts/plugin-composition.json +15 -0
  237. package/engine/scripts/poc-mcp-host.mjs +455 -0
  238. package/engine/scripts/poll-fulfillment.sh +293 -0
  239. package/engine/scripts/poll-invoice.sh +74 -0
  240. package/engine/scripts/profile-doctor.mjs +686 -0
  241. package/engine/scripts/provider-select.sh +70 -0
  242. package/engine/scripts/provider-switch.sh +146 -0
  243. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  244. package/engine/scripts/python-runtime.sh +78 -0
  245. package/engine/scripts/query_runner.py +310 -0
  246. package/engine/scripts/release-argent.sh +574 -0
  247. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  248. package/engine/scripts/run-tests.sh +256 -0
  249. package/engine/scripts/self-sign-license.mjs +179 -0
  250. package/engine/scripts/session-health-scan.mjs +63 -0
  251. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  252. package/engine/scripts/setup.sh +22 -0
  253. package/engine/scripts/skill-install.mjs +629 -0
  254. package/engine/scripts/sku_translate.py +148 -0
  255. package/engine/scripts/sync-plugins.sh +90 -0
  256. package/engine/scripts/sync-presets.sh +27 -0
  257. package/engine/scripts/sync-scripts.sh +445 -0
  258. package/engine/scripts/telemetry.py +873 -0
  259. package/engine/scripts/ui-copy-count.mjs +220 -0
  260. package/engine/scripts/usage-anchor.sh +136 -0
  261. package/engine/scripts/usage_report.py +531 -0
  262. package/engine/scripts/validate-preset-schema.mjs +208 -0
  263. package/engine/scripts/vertical-init.mjs +323 -0
  264. package/engine/scripts/vertical-lint.mjs +482 -0
  265. package/engine/scripts/wenyao-browser.sh +73 -0
  266. package/engine/scripts/wenyao-fixture-export.py +116 -0
  267. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  268. package/engine/scripts/wenyao-login.sh +34 -0
  269. package/engine/scripts/wenyao-template-map.json +292 -0
  270. package/engine/scripts/wenyao_attach.py +131 -0
  271. package/engine/scripts/wenyao_download_template.py +144 -0
  272. package/engine/scripts/wenyao_fill_template.py +568 -0
  273. package/engine/scripts/wenyao_login.py +145 -0
  274. package/engine/scripts/wenyao_portal.py +633 -0
  275. package/engine/scripts/wenyao_recon.py +230 -0
  276. package/engine/scripts/wenyao_run.py +503 -0
  277. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  278. package/engine/scripts/wenyao_skill_contract.py +156 -0
  279. package/engine/scripts/wenyao_template_fetch.py +175 -0
  280. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  281. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  282. package/engine/scripts/ziniao-keepalive.sh +216 -0
  283. package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
  284. package/engine/scripts/ziniao-windows.sh +153 -0
  285. package/engine/version.mjs +1 -0
  286. package/engine/verticals/README.md +103 -0
  287. package/engine/verticals/_engine-words.baseline.txt +237 -0
  288. package/engine/verticals/_engine-words.json +19 -0
  289. package/engine/verticals/mercadolibre/README.md +43 -0
  290. package/engine/verticals/mercadolibre/config.json +709 -0
  291. package/engine/verticals/mercadolibre/flows.json +706 -0
  292. package/engine/verticals/mercadolibre/manifest.json +538 -0
  293. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  294. package/engine/verticals/mercadolibre/preflight.py +143 -0
  295. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  296. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  297. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  298. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  299. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  300. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  301. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  302. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  303. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  304. package/package.json +21 -4
  305. package/README.md +0 -3
@@ -0,0 +1,602 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-production-home —— 「**生产归属**」只读门禁(P0 0.7,2026-10-03)
4
+ *
5
+ * ## 为什么有它
6
+ * 「本机(chester Mac)= 开发机,**不跑生产**;生产在客户机 Huangwei」这条纪律此前**只有文档约束**
7
+ * (`~/.dsh/AGENTS.md`「机器角色」)。而 2026-10-02 真出过事故:误信"本机代客户机跑生产",
8
+ * 把本机的 4 个 `com.whyshu.flow-trigger-*` 重新 bootstrap,结果**与客户机同抢同一批生产行**
9
+ * (实测同 3 张日报票两机各自 `planned`)。根因是结构性的、不会自己消失:
10
+ * · 台账是**每机独立文件**、lease **没有跨机互斥** ⇒ 两端同跑**必然**抢行;
11
+ * · 判据("本机 flow-trigger 必须为 0")是**可机器判定**的,却没人每次开窗前跑一遍。
12
+ * 所以把它固化成一条**只报告**的检查:只读 `launchctl list`,**绝不** bootout/bootstrap/kickstart。
13
+ *
14
+ * ## 判据(唯一口径,改判据只改 `CRITERION`;**分角色档**)
15
+ *
16
+ * ### 档位 `dev`(缺省;**语义与退出码与 2026-10-03 版完全一致**,向后兼容)
17
+ * 1. **fail**:`launchctl list` 里 label 命中 `flow-trigger` ⇒ 本机加载了生产触发器(**硬判据 = 0**)。
18
+ * 2. **warn**:名字像**生产型调度器**(`*poll*` / `*invoice*` / `*fulfillment*` / `*flow-timers*` /
19
+ * `argent-bridge`)的非 flow-trigger label —— 同一类"双跑"风险(本机跑 ⇒ 与客户机抢行)。
20
+ * 3. **info**:本机磁盘上**存在** flow-trigger 的 plist 但**没加载** —— 这是"现成弹药",
21
+ * 提示"别 bootstrap";不判红(未加载就是正确姿态)。
22
+ * ⚠️ **磁盘存在 ≠ 未加载**(2026-10-04 修正):已加载的 plist **不会**出现在这条 info 里
23
+ * (它已经被第 1 条按 `launchctl list` 实际状态judged了)—— 旧版对已加载的也印"未加载",是误报。
24
+ * 4. **warn(证据不足)**:`launchctl` 跑不起来 / 输出解析不出任何行 ⇒ **不能证明**姿态正确。
25
+ *
26
+ * ### 档位 `production`(客户机 Huangwei;`--role production` 或 `--expect-flow-triggers <N>`)
27
+ * 生产机与开发机是**相反**的期望:开发机要求 **0 条**,生产机要求**恰好 N 条且全活**。
28
+ * 把开发机的判据套到生产机上 ⇒ **必然判红**(客户机实测 4 条 fail,而那 4 条 label 正是期望值)。
29
+ * | # | 判据 | 级别 |
30
+ * |---|---|---|
31
+ * | P1 | flow-trigger 计数**恰等于**期望值(缺省 **4** = 客户机在册的生产流程条数;`--expect-flow-triggers N` 覆盖) | **fail** |
32
+ * | P2 | 每个已加载 flow-trigger 的 `last exit code` **全 0**(非 0 ⇒ 该触发器在失败循环里) | **fail** |
33
+ * | P3 | 磁盘在册(`~/Library/LaunchAgents` 有同名 plist)但**未加载**的 flow-trigger:生产机应全活 | **warn** |
34
+ * | P4 | 加载了**不在册**的 flow-trigger(磁盘上找不到同名 plist ⇒ 来源不明) | **warn** |
35
+ * | P5 | 数据目录里有 `PAUSE_*` 开关:客户机应保持**全活**(有开关 = 生产被挡住);含义见输出 | **warn** |
36
+ * 生产档下 P1/P2 之外的"生产型调度器"告警与开发档一致(同一类双跑风险)。
37
+ * ⚠️ 判据**不写死业务 label 名**:期望条数是数字(`--expect-flow-triggers`),"在册"由机器侧
38
+ * `~/Library/LaunchAgents` 的 plist 对齐 —— 引擎只认功能,不把厂商/业务名抄进判据。
39
+ *
40
+ * ## 输出与退出码(对齐 `scripts/check-docs-drift.mjs` 的"只报告"口径)
41
+ * node scripts/check-production-home.mjs # 只报告,**恒 exit 0**(除非用法错误)
42
+ * node scripts/check-production-home.mjs --role production # **客户机(生产机)**档:恰好 4 条且全活
43
+ * node scripts/check-production-home.mjs --expect-flow-triggers 4 # 同上(显式期望条数)
44
+ * node scripts/check-production-home.mjs --strict # 有 fail/warn ⇒ exit 1
45
+ * node scripts/check-production-home.mjs --json # 机器可读(stdout 只有一份 JSON)
46
+ * node scripts/check-production-home.mjs --launchctl-output <file> # 用文件当 launchctl 输出(纯夹具测试用)
47
+ * node scripts/check-production-home.mjs --data-dir <dir> # 覆盖数据目录(查遗留 PAUSE_* 开关;夹具用)
48
+ * node scripts/check-production-home.mjs --launchagents-dir <dir> # 覆盖 LaunchAgents 目录(夹具用)
49
+ * 退出码:`0` 检查完成(默认模式**恒 0**)|`1` `--strict` 且有 fail/warn|`2` 用法错误
50
+ * (⛔ 不用 `3`:按 D4 裁定,`3` **只**表示"许可宽限放行"。)
51
+ *
52
+ * ## 排程面体检(P1-A,2026-10-12;**只读,且永不计入 fail**)
53
+ * 把定时从 `flow-trigger*` 换成**宿主原生排程**之后,观测面会整体搬家(投递失败只写宿主日志、不重试;
54
+ * 回执只证"进收件箱"不证"模型跑了")⇒ 这个门禁顺带看一眼排程面(判据 = `scripts/lib/schedule-probe.mjs`
55
+ * 的四道闸,⚠️ **同一份实现**,⛔ 不在这里另写一套):
56
+ * · **默认(离线)**:排程域文件(`<DSH_HOME>/storages/schedule*`)可解析性 + 注册表里的
57
+ * `trigger.schedule` 声明面。**只读文件**,另外会 spawn **一次** `python3 flow_registry.py --json`
58
+ * (Node 侧注册表唯一加载器 `scripts/lib/registry.mjs`)——这是本开关带来的唯一新子进程。
59
+ * · `--schedule-online`:额外跑 ①②③ 三闸(`POST <base>/api/schedule/catalog` + 读**唤醒凭据**签 cookie,
60
+ * 与 `scripts/flow-trigger-wake.mjs` 同一条链、**只读**)—— 此时它**会**读凭据文件并访问 `127.0.0.1`。
61
+ * · `--no-schedule`:整段关掉(行为与 2026-10-04 版逐字一致)。
62
+ * · 严重度上限 = **warn**(从不 fail):红线判据在 `node scripts/argent-schedule.mjs check`(exit 0/1/2);
63
+ * 本门禁只保证"姿态里能看见排程面"。⇒ **开发机默认档不会因为它变红**(无 warn、exit 0)。
64
+ * · 过渡态(本批的已知状态):声明已就位但宿主**没挂载**或**没建任务** ⇒ 只报 **info**(不是故障)。
65
+ *
66
+ * ## ⚠️ 在客户机上怎么跑(2026-10-04)
67
+ * ```bash
68
+ * node scripts/check-production-home.mjs --role production # ✅ 客户机(生产机)用这个
69
+ * node scripts/check-production-home.mjs # 开发机(本机)用默认档,仍要求 0 条
70
+ * ```
71
+ * ⛔ **它是 Node 脚本,不是 bash**:`bash scripts/check-production-home.mjs --role production` 跑不起来
72
+ * (bash 会把它当 shell 语法解析)。
73
+ *
74
+ * ## 纪律
75
+ * · **只读**:不写任何文件、不改 launchd、不读凭据(除非显式 `--schedule-online`)、不联网
76
+ * (`--schedule-online` 只访问 `127.0.0.1` 的宿主)。默认唯一子进程是 `launchctl list`
77
+ * (`--no-schedule` 也不会去掉它;`--schedule-online` 另加"注册表加载器 + 一次本地 POST")。
78
+ * · **不代劳**:发现 flow-trigger 只打印"后果 + 正确做法",**绝不**自动 bootout(那正是事故的动作)。
79
+ * · 本脚本**不接** `run-tests.sh` 的失败判据(本机姿态常年可能有人为偏差,接进去就是恒红陷阱);
80
+ * 回归只覆盖 `scripts/tests/production-home.test.mjs` 的**纯夹具**(注入 launchctl 输出)。
81
+ */
82
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
83
+ import { spawnSync } from 'node:child_process'
84
+ import { homedir } from 'node:os'
85
+ import { join, resolve } from 'node:path'
86
+ import { fileURLToPath } from 'node:url'
87
+ import { resolveDataDir } from '../core/lib/argent-datadir.mjs'
88
+
89
+ /** 本仓库根(注册表加载器的默认 root;`--schedule-*` 才用得到)。 */
90
+ const REPO_DIR = fileURLToPath(new URL('..', import.meta.url))
91
+
92
+ // ── 判据(唯一口径;人话说明也在这一块,便于与 ~/.dsh/AGENTS.md 对齐)────────────
93
+ export const CRITERION = {
94
+ rule: 'launchctl list 里 label 命中 flow-trigger 的数量:dev 档必须为 0;production 档必须恰好等于期望条数且 last exit 全 0、无 PAUSE_*',
95
+ source: '~/.dsh/AGENTS.md「机器角色:生产 = Huangwei 客户机;本机(chester Mac)不跑生产」',
96
+ // 允许清单用**模式**而不是把厂商/业务 label 抄进引擎(`vertical-lint` 的"引擎不掺业务"):
97
+ // 引擎只认**功能**(宿主壳 dsh-web / 隧道 dsh-portforward / 保活 keepalive / 只读巡检),
98
+ // 具体 label 名由机器运行时提供(输出里照原样打印)。具体清单见 ~/.dsh/AGENTS.md「机器角色」。
99
+ allowedLabels: ['com.whyshu.dsh-web', 'com.whyshu.dsh-portforward'],
100
+ allowedLabelRe: /^com\.whyshu\.[a-z0-9-]*keepalive$/,
101
+ allowedNote: '本机只允许保留宿主壳(dsh-web)、端口转发(dsh-portforward)、保活类(*keepalive)与只读巡检类,见 ~/.dsh/AGENTS.md「机器角色」',
102
+ flowTriggerRe: /flow-trigger/i,
103
+ productionishRe: /(flow-timers|fulfillment|invoice-poll|poll-invoice|poll-fulfillment|argent-bridge|daily-report)/i,
104
+ /** "与我们有关"的 label(其余的 com.apple.* 之类不逐条打印,只记数量)。 */
105
+ relevantRe: /(whyshu|argent|hermes|dsh)/i,
106
+ consequence: [
107
+ '台账是**每机独立文件**、lease **没有跨机互斥** ⇒ 本机与客户机同跑**必然抢同一批生产行**。',
108
+ '2026-10-02 实测事故:本机的 4 个 flow-trigger 被重新拉起后,同 3 张日报票两机各自 `planned`。',
109
+ ],
110
+ correctAction: [
111
+ '生产只在客户机 Huangwei 上跑;迁移/回滚时"先起目标端 → 立刻停另一端 → 盯一个完整周期"。',
112
+ '本机要手工试跑:单独执行 runner(默认 dry-run),**不要**靠 cron/launchd。',
113
+ '⛔ 绝不在本机 `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.whyshu.flow-trigger-*.plist`。',
114
+ '要停掉本机某个触发器:由**人工**决定并执行 `launchctl bootout`(本门禁只报告,不代劳)。',
115
+ ],
116
+ productionCorrectAction: [
117
+ '客户机(生产)应当**全活**:flow-trigger 恰好 N 条在册、launchd 加载、last exit 全 0、**没有** PAUSE_* 开关。',
118
+ '条数不对/有失败退出码 ⇒ 先在客户机上 `launchctl list | grep flow-trigger` 看实际状态与 last exit。',
119
+ '有 PAUSE_* ⇒ 该流程的生产跑批被开关挡住(客户机应保持全活;处置由人工决定)。',
120
+ '⛔ 本门禁只报告:不改 launchd、不动开关、不删文件。',
121
+ ],
122
+ }
123
+
124
+ /**
125
+ * 角色档(**唯一口径**;`--role` 选档,缺省 dev = 与旧版逐字兼容)。
126
+ * production 的期望条数**是数字不是名单**(缺省 4 = 客户机实测在册的 4 条生产流程;具体是哪些
127
+ * **见 `~/.dsh/AGENTS.md`「机器角色」,不抄进这里**)—— 引擎只认功能、不掺业务(`vertical-lint` 的
128
+ * engine-words 规则),"在册"由机器侧 `~/Library/LaunchAgents` 的 plist 对齐。
129
+ */
130
+ export const ROLES = {
131
+ dev: {
132
+ id: 'dev', title: '开发机(本机 chester Mac)', expectedFlowTriggers: 0, expectAllAlive: false,
133
+ rule: 'launchctl list 里 label 命中 flow-trigger 的数量必须为 0',
134
+ okText: 'flow-trigger 计数 = 0(正确姿态)',
135
+ },
136
+ production: {
137
+ id: 'production', title: '生产机(客户机 Huangwei)', expectedFlowTriggers: 4, expectAllAlive: true,
138
+ rule: 'launchctl list 里 label 命中 flow-trigger 的数量必须**恰好等于期望条数**,且 last exit code 全 0、无 PAUSE_* 开关',
139
+ okText: 'flow-trigger 恰好 N 条在册、last exit 全 0、无 PAUSE_*(生产机正确姿态)',
140
+ },
141
+ }
142
+ export const ROLE_IDS = Object.keys(ROLES)
143
+
144
+ /** 唯一允许的 launchctl 调用(常量冻结:本脚本永远只会 `launchctl list`)。 */
145
+ export const LAUNCHCTL_ARGV = Object.freeze(['list'])
146
+
147
+ // ── 参数 ────────────────────────────────────────────────────────────────────
148
+ const USAGE = 'node scripts/check-production-home.mjs [--role dev|production] [--expect-flow-triggers <N>] [--strict] [--json] '
149
+ + '[--launchctl-output <file>] [--data-dir <dir>] [--launchagents-dir <dir>] '
150
+ + '[--no-schedule] [--schedule-online] [--dsh-home <dir>] [--base <url>]'
151
+ const argv = process.argv.slice(2)
152
+ const has = (f) => argv.includes(f)
153
+ const val = (f) => {
154
+ const i = argv.indexOf(f)
155
+ return i > -1 ? argv[i + 1] : undefined
156
+ }
157
+ /** 取值型开关:它们的**值**不参与"未知参数"判定(否则 `--expect-flow-triggers 4` 会被误判)。 */
158
+ const VALUE_FLAGS = new Set(['--launchctl-output', '--data-dir', '--launchagents-dir', '--role', '--expect-flow-triggers',
159
+ '--dsh-home', '--base'])
160
+ const KNOWN = new Set(['--strict', '--json', '--help', '-h', '--no-schedule', '--schedule-online', ...VALUE_FLAGS])
161
+ for (let i = 0; i < argv.length; i++) {
162
+ const a = argv[i]
163
+ if (VALUE_FLAGS.has(a)) { i++; continue } // 跳过它的值(值缺失由下面逐项报错,判据不在这里)
164
+ if (a.startsWith('-') && !KNOWN.has(a)) {
165
+ console.error(`未知参数:${a}\n用法:${USAGE}`)
166
+ process.exit(2)
167
+ }
168
+ }
169
+ if (has('--help') || has('-h')) {
170
+ // 打印**整段**文档注释(到闭合行为止):旧的 `slice(1, 34)` 会把后加的用法/退出码截掉
171
+ const src = readFileSync(new URL(import.meta.url), 'utf8').split('\n')
172
+ const end = src.findIndex((l, i) => i > 0 && /^\s*\*\/\s*$/.test(l))
173
+ console.log(src.slice(1, end > 0 ? end : 34).map((l) => l.replace(/^ \* ?/, '')).join('\n'))
174
+ process.exit(0)
175
+ }
176
+
177
+ const AS_JSON = has('--json')
178
+ const STRICT = has('--strict')
179
+
180
+ const outFile = val('--launchctl-output')
181
+ const explicitDataDir = val('--data-dir')
182
+ const explicitAgentsDir = val('--launchagents-dir')
183
+ if (outFile !== undefined && !outFile) { console.error('--launchctl-output 需要一个文件路径'); process.exit(2) }
184
+ if (explicitDataDir !== undefined && !explicitDataDir) { console.error('--data-dir 需要一个目录路径'); process.exit(2) }
185
+ if (explicitAgentsDir !== undefined && !explicitAgentsDir) { console.error('--launchagents-dir 需要一个目录路径'); process.exit(2) }
186
+
187
+ // ── 角色档(`--role`;缺省 dev = 向后兼容)───────────────────────────────────
188
+ const roleFlag = val('--role')
189
+ const expectFlag = val('--expect-flow-triggers')
190
+ if (has('--role')) {
191
+ if (!roleFlag || roleFlag.startsWith('-')) { console.error(`--role 需要一个值(${ROLE_IDS.join('|')})\n用法:${USAGE}`); process.exit(2) }
192
+ if (!ROLE_IDS.includes(roleFlag)) { console.error(`未知角色:${roleFlag}(可用:${ROLE_IDS.join('、')})\n用法:${USAGE}`); process.exit(2) }
193
+ }
194
+ let expectedFlowTriggers = null
195
+ if (has('--expect-flow-triggers')) {
196
+ if (!expectFlag || !/^\d+$/.test(String(expectFlag))) {
197
+ console.error(`--expect-flow-triggers 需要一个非负整数(实得 ${expectFlag === undefined ? '(缺值)' : expectFlag})\n用法:${USAGE}`)
198
+ process.exit(2)
199
+ }
200
+ expectedFlowTriggers = Number(expectFlag)
201
+ }
202
+ if (roleFlag === 'dev' && expectedFlowTriggers !== null && expectedFlowTriggers !== 0) {
203
+ console.error(`--role dev 与 --expect-flow-triggers ${expectedFlowTriggers} 矛盾:开发机档要求 **0 条**`
204
+ + `(要按生产机判请用 --role production)\n用法:${USAGE}`)
205
+ process.exit(2)
206
+ }
207
+ // 给了 `--expect-flow-triggers` 而没给 `--role` ⇒ 按**生产机**档判("我说了期望条数"本身就是生产语义)
208
+ const ROLE = expectedFlowTriggers === null
209
+ ? ROLES[roleFlag || 'dev']
210
+ : { ...ROLES[roleFlag || 'production'], expectedFlowTriggers }
211
+ const IS_PRODUCTION = ROLE.expectAllAlive === true
212
+
213
+ // ── 取证据(**只读**)────────────────────────────────────────────────────────
214
+
215
+ /** 跑 `launchctl list`(唯一的 launchctl 调用;参数是冻结常量,不存在第二条路径)。 */
216
+ export function readLaunchctl() {
217
+ const r = spawnSync('launchctl', [...LAUNCHCTL_ARGV], { encoding: 'utf8' })
218
+ if (r.error) return { ok: false, text: '', why: `launchctl 起不来:${r.error.code || r.error.message}` }
219
+ if (r.status !== 0) return { ok: false, text: String(r.stdout || ''), why: `launchctl list 退出码 ${r.status}(非 macOS 或受限环境)` }
220
+ return { ok: true, text: String(r.stdout || ''), why: '' }
221
+ }
222
+
223
+ /** `launchctl list` 文本 → 行表。表头(`PID Status Label`)与空行跳过;无法解析的行忽略。 */
224
+ export function parseLaunchctlList(text) {
225
+ const rows = []
226
+ for (const line of String(text || '').split('\n')) {
227
+ const t = line.trim()
228
+ if (!t) continue
229
+ const cols = t.split(/\s+/)
230
+ if (cols.length < 3) continue
231
+ if (/^PID$/i.test(cols[0])) continue
232
+ const [pid, status, ...rest] = cols
233
+ rows.push({ pid: pid === '-' ? null : (Number(pid) || null), status: Number(status) || 0, label: rest.join(' ') })
234
+ }
235
+ return rows
236
+ }
237
+
238
+ /**
239
+ * 本机 LaunchAgents 里**存在**的 flow-trigger plist(只读目录名;不解析内容、不加载)。
240
+ * ⚠️ 这是**磁盘在册**清单,与"**已加载**"(`launchctl list` 的实测状态)是两件事 ——
241
+ * 2026-10-04 修正:旧版把在册但**已加载**的也印成"未加载",在客户机上误报。
242
+ */
243
+ export const DEFAULT_AGENTS_DIR = join(homedir(), 'Library', 'LaunchAgents')
244
+ export function flowTriggerPlistsOnDisk(dir = DEFAULT_AGENTS_DIR) {
245
+ try {
246
+ if (!existsSync(dir)) return []
247
+ return readdirSync(dir).filter((n) => CRITERION.flowTriggerRe.test(n) && n.endsWith('.plist')).sort()
248
+ } catch { return [] }
249
+ }
250
+
251
+ /** LaunchAgents 目录的**可读性**(判"在册/不在册"必须有基准:目录读不到时**不**下"不在册"的结论)。 */
252
+ export function agentsDirState(dir = DEFAULT_AGENTS_DIR) {
253
+ try { return { dir, readable: true, exists: existsSync(dir), plists: flowTriggerPlistsOnDisk(dir) } }
254
+ catch { return { dir, readable: false, exists: false, plists: [] } }
255
+ }
256
+
257
+ /** 数据目录里存在的 `PAUSE_*` 遗留开关(只读;用来判断"已加载的生产定时器是不是被开关挡住了")。 */
258
+ export function pauseSwitches(dataDir) {
259
+ try {
260
+ if (!dataDir || !existsSync(dataDir)) return []
261
+ return readdirSync(dataDir).filter((n) => n.startsWith('PAUSE_')).sort()
262
+ } catch { return [] }
263
+ }
264
+
265
+ /**
266
+ * 判定(纯函数,便于夹具回归)。
267
+ * @param {Array} rows `launchctl list` 行表(`{pid, status, label}`;`status` = **last exit code**)
268
+ * @param {object} [opts]
269
+ * @param {string[]} [opts.plistsOnDisk] 磁盘**在册**的 flow-trigger plist 文件名(含 `.plist`)
270
+ * @param {boolean} [opts.agentsDirReadable] 在册清单是否**可信**(目录读不到 ⇒ 不下"不在册"的结论)
271
+ * @param {string[]} [opts.pauseOnDisk] 数据目录里的 `PAUSE_*` 开关
272
+ * @param {object} [opts.role] 角色档(缺省 `ROLES.dev`;`ROLES.production` 是**相反**的期望)
273
+ * @returns {{findings: Array, flowTriggers: Array, productionish: Array, allowed: Array, others: Array, system: Array}}
274
+ */
275
+ export function judge(rows, {
276
+ plistsOnDisk = [], agentsDirReadable = true, pauseOnDisk = [], evidenceOk = true, evidenceWhy = '', role = ROLES.dev,
277
+ } = {}) {
278
+ const findings = []
279
+ const flowTriggers = rows.filter((r) => CRITERION.flowTriggerRe.test(r.label))
280
+ const allowed = rows.filter((r) => CRITERION.allowedLabels.includes(r.label) || CRITERION.allowedLabelRe.test(r.label))
281
+ const productionish = rows.filter((r) => !CRITERION.flowTriggerRe.test(r.label) && CRITERION.productionishRe.test(r.label))
282
+ const others = rows.filter((r) => CRITERION.relevantRe.test(r.label)
283
+ && !flowTriggers.includes(r) && !allowed.includes(r) && !productionish.includes(r))
284
+ const system = rows.filter((r) => !CRITERION.relevantRe.test(r.label))
285
+
286
+ // 在册判据:plist 文件名(去掉 `.plist`)= label 约定
287
+ const onDiskLabels = new Set(plistsOnDisk.map((n) => String(n).replace(/\.plist$/i, '')))
288
+ const onDiskLoaded = plistsOnDisk.filter((n) => flowTriggers.some((r) => r.label === String(n).replace(/\.plist$/i, '')))
289
+ const onDiskNotLoaded = agentsDirReadable ? plistsOnDisk.filter((n) => !onDiskLoaded.includes(n)) : []
290
+ const notOnDisk = agentsDirReadable ? flowTriggers.filter((r) => !onDiskLabels.has(r.label)) : []
291
+
292
+ if (role.expectAllAlive) {
293
+ // ── 生产档(客户机):**恰好 N 条且全活**(数量/退出码是硬判据;在册、开关是告警)──
294
+ const n = flowTriggers.length
295
+ const want = role.expectedFlowTriggers
296
+ if (evidenceOk && n !== want) {
297
+ findings.push({
298
+ level: 'fail', label: `(flow-trigger 计数 ${n} ≠ 期望 ${want})`, pid: null,
299
+ why: `生产机应**恰好加载 ${want} 条** flow-trigger(多一条=多跑,少一条=生产缺岗);`
300
+ + `实际 ${n} 条:${flowTriggers.map((r) => r.label).join('、') || '(无)'}`
301
+ + (onDiskNotLoaded.length ? `;磁盘在册但未加载 ${onDiskNotLoaded.length} 个` : ''),
302
+ })
303
+ }
304
+ for (const r of flowTriggers) {
305
+ if (r.status !== 0) {
306
+ findings.push({
307
+ level: 'fail', label: r.label, pid: r.pid,
308
+ why: `last exit code = ${r.status}(≠0)—— 该触发器在**失败循环**里,生产跑批事实上没在跑`,
309
+ })
310
+ }
311
+ }
312
+ for (const p of onDiskNotLoaded) {
313
+ findings.push({ level: 'warn', label: p, pid: null, why: '磁盘在册但**未加载**:生产机应保持**全活**(该流程当前没有在跑)' })
314
+ }
315
+ for (const r of notOnDisk) {
316
+ findings.push({
317
+ level: 'warn', label: r.label, pid: r.pid,
318
+ why: `已加载但**不在册**(LaunchAgents 目录里找不到同名 plist)⇒ 来源不明,人工确认它是什么`,
319
+ })
320
+ }
321
+ for (const p of pauseOnDisk) {
322
+ findings.push({
323
+ level: 'warn', label: p, pid: null,
324
+ why: '数据目录里有 **PAUSE_\*** 开关:客户机(生产)应保持**全活** —— 有开关 = 该流程的生产跑批被挡住'
325
+ + '(含义:`PAUSE_*` 是本机/旧装机遗留的暂停开关,客户机不该带着它跑生产)',
326
+ })
327
+ }
328
+ } else {
329
+ // ── 开发档(**与 2026-10-03 版逐字一致**):本机一条都不许有 ──
330
+ for (const r of flowTriggers) {
331
+ findings.push({ level: 'fail', label: r.label, pid: r.pid, why: '本机**加载了** flow-trigger:生产触发器必须只在客户机 Huangwei 上跑' })
332
+ }
333
+ // disk 上存在**且没加载** ⇒ 只提示"现成弹药"(已加载的**不在此列**:它已被上面的 fail 覆盖)
334
+ for (const p of onDiskNotLoaded) {
335
+ findings.push({ level: 'info', label: p, pid: null, why: '磁盘上存在该 flow-trigger plist 但**未加载**(正确姿态);它是 bootstrap 的现成弹药,别碰' })
336
+ }
337
+ }
338
+ for (const r of productionish) {
339
+ findings.push({
340
+ level: 'warn', label: r.label, pid: r.pid,
341
+ why: '名字像**生产型调度器**(poll/invoice/fulfillment/flow-timers/daily-report)—— 与 flow-trigger 同一类"双跑"风险',
342
+ })
343
+ }
344
+ if (!evidenceOk) {
345
+ findings.push({ level: 'warn', label: '(证据不足)', pid: null, why: `${evidenceWhy || 'launchctl 不可用'} ⇒ **不能证明**本机姿态正确("查不到" ≠ "干净")` })
346
+ }
347
+ return { findings, flowTriggers, productionish, allowed, others, system, onDiskLoaded, onDiskNotLoaded, notOnDisk }
348
+ }
349
+
350
+ // ── 主流程 ──────────────────────────────────────────────────────────────────
351
+ const source = outFile ? `--launchctl-output ${outFile}` : 'launchctl list'
352
+ let ev = { ok: !outFile, text: '', why: '' }
353
+ if (outFile) {
354
+ try {
355
+ ev = { ok: true, text: readFileSync(outFile, 'utf8'), why: '' }
356
+ } catch (e) {
357
+ console.error(`--launchctl-output 读不了:${outFile}(${e.message})`)
358
+ process.exit(2)
359
+ }
360
+ } else {
361
+ ev = readLaunchctl()
362
+ }
363
+ const rows = parseLaunchctlList(ev.text)
364
+ const dataDir = explicitDataDir || resolveDataDir({ env: process.env }).dir
365
+ const agents = agentsDirState(explicitAgentsDir || DEFAULT_AGENTS_DIR)
366
+ const plists = agents.plists
367
+ const pauses = pauseSwitches(dataDir)
368
+ // "证据不足" = launchctl 跑不起来 **或** 输出解析不出任何行
369
+ const evidenceOk = ev.ok && rows.length > 0
370
+ const verdict = judge(rows, {
371
+ plistsOnDisk: plists, agentsDirReadable: agents.readable, pauseOnDisk: pauses,
372
+ evidenceOk, evidenceWhy: ev.why || '输出里没有任何可解析的 launchctl 行', role: ROLE,
373
+ })
374
+ /** 计数判据的结论(生产档 = 恰好 N 条;开发档 = 0 条)—— 单独算,便于人话与 JSON 共用。 */
375
+ const countOk = evidenceOk && verdict.flowTriggers.length === ROLE.expectedFlowTriggers
376
+
377
+ // ── 排程面体检(P1-A,2026-10-12;只读;**永不计入 fail**)────────────────────
378
+ // 判据 = `scripts/lib/schedule-probe.mjs` 的四道闸(⚠️ 同一份实现,⛔ 这里不另写一套)。
379
+ const NO_SCHEDULE = has('--no-schedule')
380
+ const SCHEDULE_ONLINE = has('--schedule-online')
381
+ const DSH_HOME_FLAG = val('--dsh-home')
382
+ const SCHEDULE_BASE = (val('--base') || 'http://127.0.0.1:3080').replace(/\/+$/, '')
383
+
384
+ /** 注册表里的 `trigger.schedule` 声明(读不到 ⇒ 如实说"读不到",不静默当"没声明")。 */
385
+ async function scheduleDeclarations() {
386
+ try {
387
+ const { loadRegistryDoc } = await import('./lib/registry.mjs')
388
+ const doc = loadRegistryDoc({ root: REPO_DIR })
389
+ return {
390
+ declared: Object.entries(doc.flows || {})
391
+ .map(([id, spec]) => ({ id, schedule: spec && spec.trigger && spec.trigger.schedule, trigger: (spec && spec.trigger) || {} }))
392
+ .filter((x) => x.schedule && typeof x.schedule === 'object'),
393
+ error: '',
394
+ }
395
+ } catch (e) {
396
+ return { declared: [], error: String((e && (e.stderr || e.message)) || e).split('\n')[0] }
397
+ }
398
+ }
399
+
400
+ async function scheduleFace() {
401
+ if (NO_SCHEDULE) return { enabled: false, mode: 'off', gates: [], level: 'info', mounted: null, declared: [], lines: [] }
402
+ const probe = await import('./lib/schedule-probe.mjs')
403
+ const paths = probe.domainPaths(probe.storageRoot({ dshHome: DSH_HOME_FLAG || undefined }))
404
+ const inspection = probe.inspectDomain(paths)
405
+ const gates = [probe.gatePersistence(inspection, { backupRoots: [join(dataDir, 'backup'), join(dataDir, 'backups')] })]
406
+ const { declared, error: regError } = await scheduleDeclarations()
407
+ const enabledDecls = declared.filter((d) => d.schedule.enabled === true)
408
+ let mounted = null
409
+ if (SCHEDULE_ONLINE) {
410
+ const { authSources, acquireWakeAuth } = await import('./lib/wake-auth.mjs')
411
+ const auth = await acquireWakeAuth({
412
+ sources: authSources({ base: SCHEDULE_BASE, env: process.env, dshHome: DSH_HOME_FLAG || undefined }),
413
+ base: SCHEDULE_BASE,
414
+ })
415
+ const res = auth.ok
416
+ ? await probe.postRpc({ base: SCHEDULE_BASE, endpoint: probe.MOUNT_ENDPOINT, args: {}, cookie: auth.cookie })
417
+ : { state: 'unauthorized', httpStatus: 0, message: auth.unreachable || '鉴权链全部失败(详见 argent-schedule probe)' }
418
+ const mountGate = probe.gateMount(res)
419
+ mounted = mountGate.state === 'green'
420
+ gates.push(mountGate)
421
+ const entries = (res && res.state === 'ok' && Array.isArray(res.value)) ? res.value.map(probe.normalizeEntry).filter(Boolean) : null
422
+ if (entries && enabledDecls.length) {
423
+ const nowMs = Date.now()
424
+ for (const d of enabledDecls) {
425
+ const localId = String(d.id).split('.').pop()
426
+ const m = probe.matchTask(entries, { flowId: d.id, alternatives: [localId], taskTitle: String(d.schedule.taskTitle || '') })
427
+ const e = m.entry
428
+ gates.push(probe.gateFreshness({ entry: e, nowMs, periodSec: e ? probe.periodSecondsOf(e) : null, label: d.id }))
429
+ const ev = probe.readL1Evidence({
430
+ logPath: d.trigger.log ? join(dataDir, String(d.trigger.log)) : '',
431
+ ledgerPath: join(dataDir, 'flow-ledger.jsonl'),
432
+ flowIds: [d.id, localId],
433
+ })
434
+ gates.push(probe.gateExecution({ entry: e, evidence: ev, nowMs, label: d.id }))
435
+ }
436
+ }
437
+ }
438
+ // 严重度**上限 = warn**(且未挂载时的红降为 info —— 那是"声明已就位、宿主未挂载/未建任务"的
439
+ // 过渡态,不是故障;P1-B 落地前它会一直是这个形态):
440
+ const reds = gates.filter((g) => g.state === 'red')
441
+ const persistRed = gates.some((g) => g.gate === 'persistence' && g.state === 'red')
442
+ const level = (reds.length && (mounted || persistRed)) ? 'warn' : 'info'
443
+ return {
444
+ enabled: true,
445
+ mode: SCHEDULE_ONLINE ? 'online' : 'offline',
446
+ gates, level, mounted, declared: declared.map((d) => d.id), enabledDecls: enabledDecls.map((d) => d.id),
447
+ registryError: regError,
448
+ domainState: inspection.state, domainPaths: paths, domainFiles: inspection.files.map((f) => f.path),
449
+ base: SCHEDULE_BASE,
450
+ }
451
+ }
452
+
453
+ const schedule = await scheduleFace()
454
+ if (schedule.level === 'warn') {
455
+ const g = schedule.gates.filter((x) => x.state === 'red')
456
+ verdict.findings.push({
457
+ level: 'warn', label: '(排程面)', pid: null,
458
+ why: `${g.map((x) => `[${x.gate}] ${x.headline}`).join(';')}`
459
+ + ' —— 排程面**最高只判 warn**(真正的判红归 `node scripts/argent-schedule.mjs check`,exit 0/1/2)',
460
+ })
461
+ }
462
+
463
+ // `failed`/`warned` **在排程面之后**才算:排程面的 warn 同样受 `--strict` 约束(它只在
464
+ // "宿主已挂载但某道闸红"或"域文件坏了"时出现 —— 那都是真问题,不是过渡态)。
465
+ const failed = verdict.findings.some((f) => f.level === 'fail')
466
+ const warned = verdict.findings.some((f) => f.level === 'warn')
467
+
468
+ const human = []
469
+ human.push(`🔎 生产归属检查(角色 **${ROLE.id}** = ${ROLE.title})`)
470
+ human.push('')
471
+ human.push(` 判据:${ROLE.rule}`)
472
+ human.push(` 出处:${CRITERION.source}`)
473
+ human.push(` 证据:${source}${outFile ? '' : `(${rows.length} 行)`}`)
474
+ human.push('')
475
+ if (ROLE.expectAllAlive) {
476
+ if (!evidenceOk) {
477
+ human.push(' ⚠️ 证据不足:无法确认 flow-trigger 计数(**不能**当成"符合期望")。')
478
+ human.push(` ${ev.why || '输出里没有任何可解析的 launchctl 行'}`)
479
+ } else if (countOk && !failed) {
480
+ human.push(` ✅ flow-trigger 恰好 ${ROLE.expectedFlowTriggers} 条、last exit 全 0`
481
+ + `${pauses.length ? `(⚠ 但数据目录有 ${pauses.length} 个 PAUSE_* 开关,见下)` : '、无 PAUSE_*'}。`)
482
+ for (const r of verdict.flowTriggers) human.push(` · ${r.label.padEnd(34)} ${r.pid ? `PID ${r.pid}` : '未运行'} last exit ${r.status}`)
483
+ } else {
484
+ human.push(` ${failed ? '❌' : '⚠️'} flow-trigger 计数 = ${verdict.flowTriggers.length}(期望**恰好 ${ROLE.expectedFlowTriggers} 条**)`
485
+ + `${failed ? ' —— **姿态不符**:' : ''}`)
486
+ for (const r of verdict.flowTriggers) {
487
+ human.push(` · ${r.label.padEnd(34)} ${r.pid ? `PID ${r.pid}` : '未运行'} last exit ${r.status}${r.status !== 0 ? ' ← 失败退出码' : ''}`)
488
+ }
489
+ if (!verdict.flowTriggers.length) human.push(' (一条都没有:生产机应全活)')
490
+ if (verdict.onDiskNotLoaded.length) {
491
+ human.push(` 磁盘在册但**未加载**(${verdict.onDiskNotLoaded.length} 个):${verdict.onDiskNotLoaded.join(', ')}`)
492
+ }
493
+ }
494
+ } else {
495
+ if (failed) {
496
+ human.push(` ❌ 本机加载了 ${verdict.flowTriggers.length} 个 flow-trigger —— **姿态错误**:`)
497
+ for (const r of verdict.flowTriggers) human.push(` · ${r.label}${r.pid ? `(PID ${r.pid})` : ''}`)
498
+ } else if (!evidenceOk) {
499
+ human.push(' ⚠️ 证据不足:无法确认本机 flow-trigger 计数(**不能**当成"干净")。')
500
+ human.push(` ${ev.why || '输出里没有任何可解析的 launchctl 行'}`)
501
+ } else {
502
+ human.push(` ✅ ${ROLE.okText}。`)
503
+ }
504
+ }
505
+ human.push('')
506
+ human.push(` 相关 LaunchAgent(允许:${CRITERION.allowedLabels.join(' / ')} / *keepalive / 只读巡检):`)
507
+ if (!rows.length) human.push(' (没有读到任何 launchd 行)')
508
+ for (const r of verdict.allowed) human.push(` · ${r.label.padEnd(34)} ${r.pid ? `PID ${r.pid}` : '未运行'}`)
509
+ for (const r of verdict.productionish) human.push(` ⚠️ ${r.label.padEnd(31)} ${r.pid ? `PID ${r.pid}` : '未运行'} ← 生产型调度器`)
510
+ for (const r of verdict.others) human.push(` · ${r.label.padEnd(34)} ${r.pid ? `PID ${r.pid}` : '未运行'}`)
511
+ human.push(` (其余 ${verdict.system.length} 个系统/无关 label 不逐条打印)`)
512
+ if (plists.length) {
513
+ human.push('')
514
+ // ⚠️ 磁盘在册 ≠ 未加载:这里**只**列"在册但没在 launchctl list 里"的那些(已加载的走上面的计数/状态)
515
+ human.push(` ℹ️ 磁盘在册的 flow-trigger plist:${plists.length} 个 —— 其中**已加载** ${verdict.onDiskLoaded.length} 个、`
516
+ + `**未加载** ${verdict.onDiskNotLoaded.length} 个${agents.readable ? '' : '(⚠ LaunchAgents 目录读不到,"在册"判据不可信)'}:`)
517
+ for (const p of plists) {
518
+ const label = String(p).replace(/\.plist$/i, '')
519
+ const loaded = verdict.onDiskLoaded.includes(p)
520
+ human.push(` · ${label.padEnd(34)} ${loaded ? '已加载(见上面的计数/状态)' : '未加载(正确姿态;别 bootstrap)'}`)
521
+ }
522
+ if (verdict.notOnDisk.length) {
523
+ human.push(` ⚠️ 已加载但**不在册**(磁盘上找不到同名 plist):${verdict.notOnDisk.map((r) => r.label).join(', ')}`)
524
+ }
525
+ }
526
+ if (pauses.length) {
527
+ human.push('')
528
+ human.push(` ${ROLE.expectAllAlive ? '⚠️' : 'ℹ️'} 数据目录里的开关(${dataDir}):${pauses.join(', ')}`)
529
+ human.push(ROLE.expectAllAlive
530
+ ? ' —— 客户机(生产)应保持**全活**:有 `PAUSE_*` = 该流程的生产跑批被挡住(该开关本是本机/旧装机遗留,别让它留在生产机上)。'
531
+ : ' —— 已加载的生产定时器**可能**被它挡住;但"挡住"不等于"不该加载",仍应按机器角色清理(由人工决定)。')
532
+ }
533
+ if (schedule.enabled) {
534
+ human.push('')
535
+ human.push(` ── 排程面(P1-A;只读;最高 warn,**从不 fail**)|模式 ${schedule.mode}|域文件 ${schedule.domainState}`)
536
+ if (schedule.registryError) human.push(` ⚠️ 注册表读不到(不静默当"没声明"):${schedule.registryError}`)
537
+ if (!schedule.declared.length && !schedule.registryError) human.push(' ℹ️ 没有任何流程声明 `trigger.schedule` ⇒ 排程面未启用')
538
+ else if (schedule.declared.length) {
539
+ human.push(` ℹ️ 声明了 trigger.schedule 的流程:${schedule.declared.length} 条|其中 enabled=true:${schedule.enabledDecls.join('、') || '(无)'}`)
540
+ }
541
+ if (!SCHEDULE_ONLINE) {
542
+ human.push(' ℹ️ 离线档:只查域文件与声明面;要跑①②③三闸(宿主 HTTP + 唤醒凭据)加 `--schedule-online`')
543
+ }
544
+ const sum = (await import('./lib/schedule-probe.mjs')).summarize(schedule.gates)
545
+ human.push((await import('./lib/schedule-probe.mjs')).formatGates(sum, { prefix: ' ' }))
546
+ human.push(` ↳ 完整判据与红线:\`node scripts/argent-schedule.mjs check\`(exit 0 全绿 / 1 有红 / 2 证据不足)`)
547
+ }
548
+ if (failed || warned) {
549
+ human.push('')
550
+ human.push(' ⛔ 后果:')
551
+ for (const c of CRITERION.consequence) human.push(` · ${c}`)
552
+ human.push(' ✅ 正确做法:')
553
+ for (const c of ROLE.expectAllAlive ? CRITERION.productionCorrectAction : CRITERION.correctAction) human.push(` · ${c}`)
554
+ }
555
+ human.push('')
556
+ human.push(` 结论(${ROLE.id} 档):${failed ? '❌ 与机器角色不符(必须人工处理)' : warned ? '⚠️ 有告警(请人工确认)' : '✅ 姿态正确'}`
557
+ + `(本检查**只报告**,不改 launchd;${STRICT ? '--strict ⇒ 有 fail/warn 时 exit 1' : '默认恒 exit 0,--strict 时才非零'})`)
558
+
559
+ const payload = {
560
+ ok: !failed && !warned,
561
+ tool: 'check-production-home',
562
+ role: ROLE.id,
563
+ roleTitle: ROLE.title,
564
+ expectedFlowTriggers: ROLE.expectedFlowTriggers,
565
+ countOk,
566
+ criterion: ROLE.rule,
567
+ criterionSource: CRITERION.source,
568
+ evidence: { source, rows: rows.length, launchctlOk: ev.ok, why: ev.why || '' },
569
+ counts: {
570
+ flowTriggers: verdict.flowTriggers.length,
571
+ productionish: verdict.productionish.length,
572
+ allowed: verdict.allowed.length,
573
+ others: verdict.others.length,
574
+ plistsOnDisk: plists.length,
575
+ plistsLoaded: verdict.onDiskLoaded.length,
576
+ plistsNotLoaded: verdict.onDiskNotLoaded.length,
577
+ notOnDisk: verdict.notOnDisk.length,
578
+ },
579
+ flowTriggerLabels: verdict.flowTriggers.map((r) => r.label),
580
+ flowTriggerStatus: verdict.flowTriggers.map((r) => ({ label: r.label, pid: r.pid, lastExit: r.status })),
581
+ plistsOnDisk: { all: plists, loaded: verdict.onDiskLoaded, notLoaded: verdict.onDiskNotLoaded, dirReadable: agents.readable },
582
+ findings: verdict.findings,
583
+ pauseSwitches: pauses,
584
+ dataDir,
585
+ // 排程面体检(P1-A):字段与判据同源(lib/schedule-probe.mjs);⛔ 它**不参与** failed/warned 的
586
+ // 计算,除非它自己判了 warn(见上面那段说明)。
587
+ schedule: schedule.enabled
588
+ ? {
589
+ mode: schedule.mode, level: schedule.level, mounted: schedule.mounted, base: schedule.base,
590
+ declared: schedule.declared, enabledFlows: schedule.enabledDecls,
591
+ registryError: schedule.registryError || '',
592
+ domain: { state: schedule.domainState, paths: schedule.domainPaths, files: schedule.domainFiles },
593
+ gates: schedule.gates,
594
+ }
595
+ : { mode: 'off' },
596
+ }
597
+
598
+ if (AS_JSON) console.log(JSON.stringify(payload, null, 2))
599
+ else console.log(human.join('\n'))
600
+
601
+ const bad = failed || warned
602
+ process.exit(STRICT && bad ? 1 : 0)