@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,659 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * schedule-mechanism —— 「定时到底怎么来」的**唯一判据**(0.6.0 W8,2026-10-14)
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * 为什么必须有它(本模块的由来)
7
+ * ══════════════════════════════════════════════════════════════════════════
8
+ * 0.6.0 起「定时」要在**两条宿主线**上都成立:
9
+ * · **0.2 线(新)**:宿主自带原生排程(`@deepseek-ai/dsh-schedule`)⇒ 定时**原生**,
10
+ * ⛔ 客户机不再需要装 launchd/cron;
11
+ * · **0.1.7 线(存量)**:保留**激活垫片**(`com.whyshu.flow-trigger-*` + `flow-timers.sh`)。
12
+ * ⇒ 同一个事实("这台机器的定时归谁")此前有**三份**说法:`flow-timers.sh` 自己的命令模板、
13
+ * 插件 `setup-boot.mjs#detectNativeSchedule`、`argent-setup.mjs#checkTimer` 的 `mechanism` 字符串。
14
+ * 三份就会漂移(本仓库为此付过多次代价)。本模块把它收敛成**一处声明 + 一处判据**:
15
+ *
16
+ * · **声明面(唯一)** = 注册表 `schedule.mechanism`(引擎级 base:
17
+ * `scripts/flow-requirements.json`),取值 `auto | native | activation-shim | none`
18
+ * + `schedule.jobs`(激活垫片的安装入口清单:`labelPrefix` / `installCommand` / `source`)。
19
+ * 运行期**只有两个**读取点,且都是"读它、不重写它":
20
+ * - Node 侧:本模块 `declaredScheduleMechanism()`(经 `scripts/lib/registry.mjs` 读合并注册表);
21
+ * - Python 侧:`scripts/flow_config.py --schedule-declaration`(同一条合并链)。
22
+ * · **判据面(唯一)** = 本模块 `resolveScheduleMechanism()`:按声明 + 宿主证据算出**实际机制**。
23
+ *
24
+ * ══════════════════════════════════════════════════════════════════════════
25
+ * 纪律(改本文件前先读这四条)
26
+ * ══════════════════════════════════════════════════════════════════════════
27
+ * 1. **纯 L1**:只用 node 内建(`fs`/`path`/`os`)+ 同层模块。⛔ 不 import 宿主模块
28
+ * (`node scripts/tests/kernel-purity.test.mjs` 会查),⛔ 不 import L2 插件。
29
+ * 2. **只读**:本模块**不写任何文件**、不注册任何任务、⛔ 不 bootstrap 任何计划任务
30
+ * (AGENTS 红线:本机⛔ 不许 `launchctl bootstrap` 那 4 条 flow-trigger)。
31
+ * 3. **⛔ 缺证据不许当可用**:`native` 只在**真的探到**宿主排程包时才成立;
32
+ * 探不到**且**连"该往哪儿探"都拿不到(scope 目录都不存在 / npx 形态 ⇒ F1 未修的坑)
33
+ * ⇒ 如实报 `unknown` + 原因,⛔ 不假装、⛔ 不静默装 cron。
34
+ * 4. **单一调度者**:一条流程**同时**被原生任务与激活垫片调度 ⇒ `conflict`(响亮报冲突,
35
+ * ⛔ 不静默择一)。判据是 `schedulerConflict()`,与"两条线各自跑得好不好"分开。
36
+ */
37
+
38
+ import { execFileSync } from 'node:child_process'
39
+ import { existsSync, readFileSync, statSync } from 'node:fs'
40
+ import { homedir } from 'node:os'
41
+ import { dirname, join } from 'node:path'
42
+ import { fileURLToPath } from 'node:url'
43
+
44
+ // ══════════════════════════════════════════════════════════════════════════
45
+ // 一、机制枚举(**唯一出处**:别处只许引用,⛔ 不再写字面量)
46
+ // ══════════════════════════════════════════════════════════════════════════
47
+
48
+ /**
49
+ * 三种**生效**机制(外加两个"非机制"状态:`auto` 是声明、`unknown` 是证据不足)。
50
+ *
51
+ * | 值 | 谁在调度 | 判据 | 谁负责装 |
52
+ * |---|---|---|---|
53
+ * | `native` | 宿主原生排程(`@deepseek-ai/dsh-schedule`) | 作用域里**能探到**该包 | 宿主(⛔ 我们不注册) |
54
+ * | `activation-shim` | 我们自己的激活垫片(launchd/systemd + `flow-timers.sh`) | 声明允许 ∧ 探到垫片入口或已装 job | 我们(`flow-timers.sh install`) |
55
+ * | `none` | 没人调度(合法姿态:无定时流程的机器 / 不用宿主排程的客户机) | 声明 `none`,或两条线都**确证**没有 | 无人 |
56
+ */
57
+ export const MECHANISMS = Object.freeze(['native', 'activation-shim', 'none'])
58
+
59
+ /** 声明面的全部取值(多一个 `auto` = "按宿主探到的来")。 */
60
+ export const DECLARED_MECHANISMS = Object.freeze(['auto', ...MECHANISMS])
61
+
62
+ /** 证据不足(⛔ 与 `none` 是两件事:一个是"不知道",一个是"确证没有")。 */
63
+ export const UNKNOWN = 'unknown'
64
+
65
+ /** 状态取值全集(`resolveScheduleMechanism()` 的 `state`)。 */
66
+ export const STATES = Object.freeze([...MECHANISMS, UNKNOWN])
67
+
68
+ /** 人读标签(CLI 与插件共用同一句,⛔ 别处不再写第二份)。 */
69
+ export const MECHANISM_ZH = Object.freeze({
70
+ native: '原生(宿主排程)',
71
+ 'activation-shim': '激活垫片(本机计划任务 + flow-timers.sh)',
72
+ none: '无(这台机器不该有定时器)',
73
+ unknown: '证据不足(⛔ 不猜)',
74
+ })
75
+
76
+ /** 声明读不到时的**中性默认**:`auto` = 让判据按宿主证据决定(⛔ 不写死任何一条线)。 */
77
+ export const DEFAULT_DECLARED = 'auto'
78
+
79
+ // ══════════════════════════════════════════════════════════════════════════
80
+ // 二、声明面:读注册表 `schedule` 段(**只读**,⛔ 不在这里合并注册表)
81
+ // ══════════════════════════════════════════════════════════════════════════
82
+
83
+ /** 垫片 job 的**默认**声明(只有当注册表没给 `schedule.jobs` 时才用;⛔ 不覆盖声明值)。 */
84
+ export const DEFAULT_SHIM_JOBS = Object.freeze({
85
+ source: 'scripts/flow-timers.sh',
86
+ labelPrefix: 'com.whyshu.flow-trigger',
87
+ installCommand: 'bash {repo}/scripts/flow-timers.sh install --flow {flow}',
88
+ platform: 'darwin',
89
+ })
90
+
91
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
92
+
93
+ /**
94
+ * 归一化"声明面"(注册表 `schedule` 段 → 稳定形状)。
95
+ * ⛔ 未知 `mechanism` **原样带出**(由 `errors` 判红),**不静默退回 auto**
96
+ * —— 静默退回正是"两条线各自写一份"的温床。
97
+ *
98
+ * @returns {{declared:string, declaredRaw:any, jobs:object, hostAllow:string[], errors:string[], source:string}}
99
+ */
100
+ export function normalizeScheduleDeclaration(raw, { source = 'registry' } = {}) {
101
+ const errors = []
102
+ const d = isObj(raw) ? raw : {}
103
+ if (raw !== undefined && !isObj(raw)) errors.push('注册表 `schedule` 段必须是对象')
104
+ const declared = d.mechanism === undefined ? DEFAULT_DECLARED : d.mechanism
105
+ if (typeof declared !== 'string' || !DECLARED_MECHANISMS.includes(declared)) {
106
+ errors.push(`schedule.mechanism=${JSON.stringify(declared)} 不在 ${DECLARED_MECHANISMS.join('|')} 内`)
107
+ }
108
+ const jobsRaw = isObj(d.jobs) ? d.jobs : {}
109
+ const jobs = {
110
+ source: typeof jobsRaw.source === 'string' && jobsRaw.source ? jobsRaw.source : DEFAULT_SHIM_JOBS.source,
111
+ labelPrefix: typeof jobsRaw.labelPrefix === 'string' && jobsRaw.labelPrefix
112
+ ? jobsRaw.labelPrefix : DEFAULT_SHIM_JOBS.labelPrefix,
113
+ installCommand: typeof jobsRaw.installCommand === 'string' && jobsRaw.installCommand
114
+ ? jobsRaw.installCommand : DEFAULT_SHIM_JOBS.installCommand,
115
+ platform: typeof jobsRaw.platform === 'string' && jobsRaw.platform
116
+ ? jobsRaw.platform : DEFAULT_SHIM_JOBS.platform,
117
+ }
118
+ const hostAllow = Array.isArray(d.hostAllow) && d.hostAllow.length
119
+ ? d.hostAllow.filter((x) => typeof x === 'string')
120
+ : []
121
+ return { declared, declaredRaw: d.mechanism, jobs, hostAllow, errors, source }
122
+ }
123
+
124
+ /** 从**合并后**的注册表文档里取声明(`doc.schedule`;⛔ 不读 base 文件、不自己合并)。 */
125
+ export function declaredScheduleMechanism(doc, { source = 'registry' } = {}) {
126
+ return normalizeScheduleDeclaration(doc ? doc.schedule : undefined, { source })
127
+ }
128
+
129
+ /** 把 `installCommand` 模板渲染成人可照抄的一行(`{repo}` / `{flow}` / `{label}` 占位)。 */
130
+ export function renderInstallCommand(jobs, { repo = '', flow = '', label = '' } = {}) {
131
+ const tpl = (jobs && jobs.installCommand) || DEFAULT_SHIM_JOBS.installCommand
132
+ return String(tpl)
133
+ .replace(/\{repo\}/g, repo)
134
+ .replace(/\{flow\}/g, flow)
135
+ .replace(/\{label\}/g, label)
136
+ }
137
+
138
+ /** 激活垫片的 LaunchAgent/计划任务名(`com.whyshu.flow-trigger-<flow>`)。 */
139
+ export function shimLabelOf(jobs, flowId) {
140
+ const pre = (jobs && jobs.labelPrefix) || DEFAULT_SHIM_JOBS.labelPrefix
141
+ return `${pre}-${flowId}`
142
+ }
143
+
144
+ // ══════════════════════════════════════════════════════════════════════════
145
+ // 三、证据面:探针(全都是**只读**的文件系统事实)
146
+ // ══════════════════════════════════════════════════════════════════════════
147
+
148
+ const statOrNull = (p) => { try { return statSync(p) } catch { return null } }
149
+
150
+ /**
151
+ * 宿主原生排程(`@deepseek-ai/dsh-schedule`)到底在不在。
152
+ *
153
+ * ⚠️ **三条硬纪律**(F1 修复后仍在 —— 修的是"探得到",⛔ 不是"放宽缺证据"):
154
+ * ① 目录**存在**且其中**有** `dsh-schedule/` ⇒ `available`(能力探测,⛔ 不认版本号);
155
+ * ② 目录**存在**但没有该包 ⇒ `absent`(**确证没有**);
156
+ * ③ 候选目录**一个都不存在** ⇒ `unknown`(**证据不足**)—— npx 形态若仍探不到就还是这一态,
157
+ * ⛔ 不许当成"没有原生排程"(那会把人骗去装一份用不上的 cron)。
158
+ *
159
+ * @param {{scopeDirs?: string[], exists?: Function, packages?: string[], source?: string}} [o]
160
+ * `source` —— 这批候选**怎么来的**("本机 profile 候选 N 个 + 运行时解析链 M 个"),
161
+ * 只进 `reason`/`source`(可诊断),⛔ 不参与判据。
162
+ * @returns {{state:'available'|'absent'|'unknown', available:boolean, found:string, source:string,
163
+ * dirs:string[], existing:string[], reason:string}}
164
+ */
165
+ export function probeNativeSchedule({ scopeDirs = [], exists = existsSync, packages = ['dsh-schedule'], source = '' } = {}) {
166
+ const dirs = (scopeDirs || []).filter((d) => typeof d === 'string' && d)
167
+ const existing = []
168
+ const found = []
169
+ for (const dir of dirs) {
170
+ if (!exists(dir)) continue
171
+ existing.push(dir)
172
+ for (const pkg of packages) {
173
+ const p = join(dir, pkg)
174
+ if (exists(p)) found.push(p)
175
+ }
176
+ }
177
+ if (found.length) {
178
+ return {
179
+ state: 'available', available: true, found: found[0], dirs, existing, source,
180
+ reason: `作用域里探到宿主排程包(${found[0]})`,
181
+ }
182
+ }
183
+ if (existing.length) {
184
+ return {
185
+ state: 'absent', available: false, found: '', dirs, existing, source,
186
+ reason: `作用域存在(${existing.join('、')})但没有 ${packages.join('/')} ⇒ 确证:这条线上没有原生排程`,
187
+ }
188
+ }
189
+ return {
190
+ state: 'unknown', available: false, found: '', dirs, existing, source,
191
+ reason: dirs.length
192
+ ? `候选作用域**一个都不存在**(${dirs.join('、')})⇒ 探不到 ≠ 没有(候选来源:${source || '未标注'})`
193
+ : '没有给出任何候选作用域 ⇒ 无从探测',
194
+ }
195
+ }
196
+
197
+ /**
198
+ * 从已装计划任务路径反推**是哪几条流程**(`<prefix>-<flow>.plist` / `.service`)。
199
+ * ⛔ 认不出就**不列**(宁可少报一条,也不猜一个流程名)。
200
+ */
201
+ export function flowsFromPlists(plists = [], jobs = DEFAULT_SHIM_JOBS) {
202
+ const pre = `${(jobs && jobs.labelPrefix) || DEFAULT_SHIM_JOBS.labelPrefix}-`
203
+ const out = []
204
+ for (const p of plists || []) {
205
+ const base = String(p).split('/').pop() || ''
206
+ const name = base.replace(/\.(plist|service)$/, '')
207
+ if (!name.startsWith(pre)) continue
208
+ const f = name.slice(pre.length)
209
+ if (f && !out.includes(f)) out.push(f)
210
+ }
211
+ return out
212
+ }
213
+
214
+ /**
215
+ * 激活垫片(`com.whyshu.flow-trigger-*`)在不在。
216
+ * 判据(**只看文件系统事实,⛔ 不跑 `launchctl`**):
217
+ * · 已装的计划任务文件(`LaunchAgents/<label>.plist`)—— **最强**证据;
218
+ * · 垫片入口脚本本身(`<repo>/scripts/flow-timers.sh`)—— 弱证据(装了 ≠ 已启用)。
219
+ *
220
+ * @returns {{present:boolean, strength:'installed'|'entry-only'|'none', plists:string[],
221
+ * entry:string, reason:string}}
222
+ */
223
+ export function probeActivationShim({
224
+ flows = [], jobs = DEFAULT_SHIM_JOBS, repo = '', launchAgentsDir = '',
225
+ exists = existsSync, home = homedir(), platform = process.platform,
226
+ } = {}) {
227
+ const laDir = launchAgentsDir || (platform === 'darwin'
228
+ ? join(home, 'Library', 'LaunchAgents')
229
+ : join(home, '.config', 'systemd', 'user'))
230
+ const plists = []
231
+ for (const f of flows.filter(Boolean)) {
232
+ for (const ext of ['.plist', '.service']) {
233
+ const p = join(laDir, `${shimLabelOf(jobs, f)}${ext}`)
234
+ if (exists(p)) plists.push(p)
235
+ }
236
+ }
237
+ const entry = repo ? join(repo, 'scripts', 'flow-timers.sh') : ''
238
+ const entryExists = Boolean(entry && exists(entry))
239
+ if (plists.length) {
240
+ return {
241
+ present: true, strength: 'installed', plists, entry,
242
+ reason: `已装计划任务 ${plists.length} 个(${plists.slice(0, 3).join('、')}${plists.length > 3 ? ' …' : ''})`,
243
+ }
244
+ }
245
+ if (entryExists) {
246
+ return {
247
+ present: true, strength: 'entry-only', plists, entry,
248
+ reason: `垫片入口在(${entry})但没有已装的计划任务 ⇒ 装了 ≠ 已启用`,
249
+ }
250
+ }
251
+ return { present: false, strength: 'none', plists, entry, reason: '既没有已装计划任务、也没有垫片入口' }
252
+ }
253
+
254
+ // ══════════════════════════════════════════════════════════════════════════
255
+ // 四、判据:声明 + 证据 ⇒ 实际机制(**唯一**的一处推理)
256
+ // ══════════════════════════════════════════════════════════════════════════
257
+
258
+ /**
259
+ * 算"这台机器实际的定时机制"。**纯函数**(无 IO —— 证据由调用方探好传进来)。
260
+ *
261
+ * 判据表(⛔ 缺证据一律 `unknown`,⛔ 绝不"缺证据就当可用"):
262
+ *
263
+ * | 声明 | native 证据 | shim 证据 | ⇒ 机制 | 说明 |
264
+ * |---|---|---|---|---|
265
+ * | `native` | available | — | `native` | 声明与证据一致 |
266
+ * | `native` | absent/unknown | — | `unknown` | **响亮降级**:声明要原生,但探不到 ⇒ 不假装可用 |
267
+ * | `activation-shim` | — | present | `activation-shim` | 存量线 |
268
+ * | `activation-shim` | — | none | `unknown` | 声明要垫片但没装 ⇒ 不假装已装 |
269
+ * | `none` | — | — | `none` | 明确不要定时器 |
270
+ * | `auto` | available | none | `native` | 有原生 ⇒ 走原生(⛔ 不再装垫片) |
271
+ * | `auto` | available | present | `conflict` | 两条线同时在 ⇒ 响亮报冲突(细节见 `schedulerConflict`) |
272
+ * | `auto` | absent | installed | `activation-shim` | 存量线(0.1.7) |
273
+ * | `auto` | absent | none/entry-only | `none` | 确证两条线都没有 |
274
+ * | `auto` | unknown | installed | `activation-shim` | 垫片已确证在用 ⇒ 不因"探不到原生"而停滞 |
275
+ * | `auto` | unknown | none/entry-only | `unknown` | **证据不足**(npx 形态的典型) |
276
+ *
277
+ * @returns {{state:string, declared:string, reason:string, next:string[], native:object|null,
278
+ * shim:object|null, conflict:object|null}}
279
+ */
280
+ export function resolveScheduleMechanism({ declaration, native = null, shim = null } = {}) {
281
+ const decl = isObj(declaration) && declaration.declared
282
+ ? declaration
283
+ : normalizeScheduleDeclaration(declaration)
284
+ if (decl.errors && decl.errors.length) {
285
+ return {
286
+ state: UNKNOWN, declared: String(decl.declaredRaw),
287
+ reason: `声明面非法(${decl.errors.join(';')})⇒ ⛔ 不猜机制`,
288
+ next: ['修注册表 `schedule.mechanism`(唯一声明点)后重跑;判据见 scripts/lib/schedule-mechanism.mjs'],
289
+ native, shim, conflict: null,
290
+ }
291
+ }
292
+ const declared = decl.declared
293
+ const natState = native ? native.state : 'unknown'
294
+ const shimPresent = Boolean(shim && shim.present)
295
+ const shimStrong = Boolean(shim && shim.strength === 'installed')
296
+ // ⚠️ **冲突只看已装计划任务**(`strength === 'installed'`)——"垫片入口在"(entry-only)
297
+ // 在每台有仓库的机器上都成立(那只是"装了仓"),拿它判双跑会**误报**(响亮 ≠ 狼来了)。
298
+ const conflict = (natState === 'available' && shimStrong)
299
+ ? schedulerConflict({ native, shim })
300
+ : null
301
+
302
+ if (declared === 'none') {
303
+ return {
304
+ state: 'none', declared, reason: '声明 schedule.mechanism=none ⇒ 这台机器不该有定时器(合法姿态)',
305
+ next: [], native, shim, conflict: null,
306
+ }
307
+ }
308
+ if (declared === 'native') {
309
+ if (natState === 'available') {
310
+ return {
311
+ state: 'native', declared,
312
+ reason: `声明 native ∧ 探到宿主排程(${native.found})⇒ 定时归宿主(⛔ 不装垫片)`,
313
+ next: [], native, shim, conflict,
314
+ }
315
+ }
316
+ return {
317
+ state: UNKNOWN, declared,
318
+ reason: `声明 native,但 ${native ? native.reason : '没有原生排程证据'} ⇒ **响亮降级**(⛔ 不假装可用、⛔ 不静默装 cron)`,
319
+ next: [
320
+ '确认宿主的排程服务真的在组合里(`node scripts/argent-schedule.mjs probe`:404 ⇒ 没挂载)',
321
+ '0.2 线:把排程 bundle 加进 profile 的 dsh.profile.bundles;0.1.x:按 id 覆盖三行 disabled:false',
322
+ '若这台机器本来就该用激活垫片 ⇒ 把声明改成 activation-shim(唯一声明点:注册表 `schedule.mechanism`)',
323
+ ],
324
+ native, shim, conflict,
325
+ }
326
+ }
327
+ if (declared === 'activation-shim') {
328
+ if (shimPresent) {
329
+ return {
330
+ state: 'activation-shim', declared,
331
+ reason: `声明 activation-shim ∧ ${shim.reason}`,
332
+ next: [], native, shim, conflict,
333
+ }
334
+ }
335
+ return {
336
+ state: UNKNOWN, declared,
337
+ reason: `声明 activation-shim,但 ${shim ? shim.reason : '没有垫片证据'} ⇒ 不假装已装`,
338
+ next: [`按声明面的命令装:${renderInstallCommand(decl.jobs, { flow: '<flow>' })}`],
339
+ native, shim, conflict,
340
+ }
341
+ }
342
+ // ── auto:按证据决定 ──────────────────────────────────────────────────────
343
+ if (natState === 'available') {
344
+ if (shimStrong) {
345
+ return {
346
+ state: 'conflict', declared,
347
+ reason: `原生排程可得(${native.found})**且**激活垫片**已装计划任务**(${shim.reason})⇒ **单一调度者**被破坏`,
348
+ next: conflict ? conflict.next : [],
349
+ native, shim, conflict,
350
+ }
351
+ }
352
+ return {
353
+ state: 'native', declared,
354
+ reason: `auto ∧ ${native.reason} ∧ ${shimPresent ? `垫片只有弱证据(${shim.reason},不足以判双跑)` : '无垫片'} ⇒ 走原生(⛔ 不再装 cron/launchd)`,
355
+ next: [], native, shim, conflict: null,
356
+ }
357
+ }
358
+ if (shimStrong) {
359
+ return {
360
+ state: 'activation-shim', declared,
361
+ reason: `auto ∧ 原生${natState === 'unknown' ? '探不到(证据不足,不足以推翻垫片)' : '确证没有'} ∧ ${shim.reason} ⇒ 走垫片`,
362
+ next: [], native, shim, conflict: null,
363
+ }
364
+ }
365
+ if (natState === 'absent' && !shimPresent) {
366
+ return {
367
+ state: 'none', declared,
368
+ reason: 'auto ∧ 原生确证没有 ∧ 垫片也没装 ⇒ 这台机器当前无定时器(合法姿态,但不是 native)',
369
+ next: ['要开定时:走原生(装宿主排程)或走垫片(`flow-timers.sh install`)——二选一,⛔ 不许两条都开'],
370
+ native, shim, conflict: null,
371
+ }
372
+ }
373
+ return {
374
+ state: UNKNOWN, declared,
375
+ reason: `auto ∧ ${native ? native.reason : '没有原生证据'}${shimPresent ? `;垫片只有弱证据(${shim.reason})` : ';垫片也没有'}`,
376
+ next: [
377
+ '补证据再判:宿主排程用 `node scripts/argent-schedule.mjs probe`(200=挂载 / 404=没有 / 连不上=证据不足)',
378
+ '垫片用 `bash scripts/flow-timers.sh status`(本机 ⛔ 不许 bootstrap)',
379
+ '⛔ 证据不足时**不要**顺手装一份 cron —— 那会造出第二个调度者',
380
+ ],
381
+ native, shim, conflict: null,
382
+ }
383
+ }
384
+
385
+ /**
386
+ * **native 注册的计划清单**("要交给宿主什么")—— 纯函数、幂等(同样的输入 ⇒ 同样的输出)。
387
+ *
388
+ * ⛔ 本函数**不注册任何东西**、**不发任何请求**:它只把声明面的 `trigger.schedule`
389
+ * 摊成宿主 `schedule_create` 要的那几项(title / 时机 / 静态指令命令),供
390
+ * ① 运维照抄(`argent-schedule.mjs status` 的 `toolArgs` 已在做同一件事)、
391
+ * ② 幂等判据(把宿主**已有**的条目喂进 `existing` ⇒ `action==='already-native'`,零变化)、
392
+ * ③ 冲突排查(`shouldRegisterShim` 让"该不该再装垫片"变成**一个布尔**,不靠人记)。
393
+ *
394
+ * @param {{doc:object, native:object|null, shim:object|null, existing?:Array,
395
+ * repo?:string, home?:string, component?:string}} opts
396
+ */
397
+ export function scheduleCatalogRows({ doc, native = null, shim = null, existing = [], repo = '', home = '', component = '<数据目录>' } = {}) {
398
+ const rows = []
399
+ const aliases = (doc && doc.aliases) || {}
400
+ const back = {}
401
+ for (const [short, canon] of Object.entries(aliases)) back[canon] = short
402
+ const nativeOk = Boolean(native && native.state === 'available')
403
+ const shimInstalled = Boolean(shim && shim.strength === 'installed')
404
+ const existingTitles = new Set((existing || [])
405
+ .map((e) => String((e && (e.jobTitle || e.taskTitle || e.title)) || ''))
406
+ .filter(Boolean))
407
+ for (const [flowId, spec] of Object.entries((doc && doc.flows) || {})) {
408
+ const t = (spec && spec.trigger) || {}
409
+ const sc = t.schedule
410
+ if (!sc || typeof sc !== 'object' || sc.enabled !== true) continue
411
+ const localId = back[flowId] || String(flowId).split('.').pop()
412
+ const title = String(sc.taskTitle || '')
413
+ const logRel = String(t.log || '')
414
+ const row = {
415
+ flowId,
416
+ localId,
417
+ jobTitle: title,
418
+ kind: String(sc.kind || ''),
419
+ // 周期只有一个真源 ⇒ kind=every 时从 trigger.intervalSec 取(声明面的 everySecondsFrom 指回它)
420
+ everySeconds: sc.everySecondsFrom === 'intervalSec' && Number.isFinite(t.intervalSec) ? t.intervalSec : null,
421
+ time: sc.time === undefined ? null : sc.time,
422
+ timeZone: sc.timeZone === undefined ? null : sc.timeZone,
423
+ expression: sc.expression === undefined ? null : sc.expression,
424
+ weekdays: sc.weekdays === undefined ? null : sc.weekdays,
425
+ promptFile: String(sc.promptPath || sc.promptFile || ''),
426
+ onPause: String(sc.onPause || ''),
427
+ // 静态轮次指令:宿主在**创建时固定** prompt ⇒ 它必须是"调一条 L1 命令",⛔ 不带本轮票号。
428
+ // ⭐ `--no-wake`(0.6.0 P1-B):宿主的计划任务是**无会话**执行的(只跑这条固定命令)
429
+ // ⇒ 命令必须是"只认领、不唤起会话"(认领与 `planned` 台账照常,见 flow-trigger.sh 的该开关)。
430
+ // ⛔ 这条只属于**原生排程**;0.1.7 线的激活垫片跑的是 `flow-timers.sh` 生成的 plist
431
+ // (ProgramArguments 直接指向 flow-trigger.sh 的**默认**模式,它要唤醒角色会话)
432
+ // —— 两条线各自一条命令,⛔ 不互相抄。
433
+ promptCommand: `bash ${repo || '<repo>'}/scripts/flow-trigger.sh --flow ${localId} --no-wake`
434
+ + (logRel ? ` >> ${join(home || component, logRel)} 2>&1` : ''),
435
+ scheduler: nativeOk ? 'native' : (shimInstalled ? 'activation-shim' : 'none'),
436
+ shouldRegisterShim: !nativeOk,
437
+ action: existingTitles.has(title) ? 'already-native' : (nativeOk ? 'register-native' : 'print-shim-command'),
438
+ }
439
+ if (!nativeOk && shimInstalled) row.action = 'conflict'
440
+ rows.push(row)
441
+ }
442
+ return rows
443
+ }
444
+
445
+ /** 取"用于比对幂等"的那几个字段(⛔ 不把 reason/时间戳这类易变字段算进判据)。 */
446
+ export function normalizedNativeRow(row) {
447
+ return {
448
+ flowId: row.flowId, localId: row.localId, jobTitle: row.jobTitle, kind: row.kind,
449
+ everySeconds: row.everySeconds, time: row.time, timeZone: row.timeZone,
450
+ expression: row.expression, weekdays: row.weekdays, promptFile: row.promptFile, onPause: row.onPause,
451
+ promptCommand: row.promptCommand, scheduler: row.scheduler,
452
+ }
453
+ }
454
+
455
+ /**
456
+ * 单一调度者纪律:同一条流程**同时**被两条线调度 ⇒ 冲突(⛔ 不静默择一)。
457
+ * @param {{flows?:string[], native?:object, shim?:object, nativeFlows?:string[], shimFlows?:string[]}} [opts]
458
+ * @returns {{state:'conflict', flows:string[], detail:string, next:string[], closeOne:string}}
459
+ * `closeOne` = **逐字可照抄**的一句"怎么关掉一条"(两条都给,⛔ 不替用户选)。
460
+ */
461
+ export function schedulerConflict({ flows = [], native = null, shim = null, nativeFlows = [], shimFlows = [] } = {}) {
462
+ const both = (nativeFlows.length && shimFlows.length)
463
+ ? nativeFlows.filter((f) => shimFlows.includes(f))
464
+ : (flows || [])
465
+ const closeOne = '二选一(⛔ 别两条都留着):\n'
466
+ + ' · 关垫片:`bash scripts/flow-timers.sh uninstall --flow <flow>`(会 bootout 该 label)\n'
467
+ + ' · 关原生:会话内 `schedule_delete`(宿主没有"置 inactive"的远端端点;CLI 侧见 `node scripts/argent-schedule.mjs pause --apply`)'
468
+ if (!nativeFlows.length && !shimFlows.length && !both.length) {
469
+ return {
470
+ state: 'conflict',
471
+ flows: [],
472
+ detail: '原生排程与激活垫片**同时在场**;但没有逐条流程的对照证据 ⇒ 只能判"有双跑风险",⛔ 不能点名是哪几条',
473
+ next: ['先按上面的 `bootstrap`/`install` 命令把两条线的**逐条**证据取出来,再定点关掉一条'],
474
+ closeOne,
475
+ }
476
+ }
477
+ return {
478
+ state: 'conflict',
479
+ flows: both,
480
+ detail: `这些流程被**两条线同时**调度:${both.join('、') || '(无法逐条对照)'}`
481
+ + `|原生:${native ? native.reason || '(在场)' : '(无证据)'}|垫片:${shim ? shim.reason || '(在场)' : '(无证据)'}`,
482
+ next: [
483
+ '**先停一条再验证**:⛔ 不要靠"两边各跑一半" —— 台账是每机独立文件、lease 无跨机/跨调度者互斥 ⇒ 必然抢行',
484
+ '关掉一条后盯一个完整周期:另一条线的日志不再增长、无新增认领',
485
+ ],
486
+ closeOne,
487
+ }
488
+ }
489
+
490
+ // ══════════════════════════════════════════════════════════════════════════
491
+ // 五、一步到位:从**声明 + 探针**算出结论(CLI/插件/门禁共用这一条链)
492
+ // ══════════════════════════════════════════════════════════════════════════
493
+
494
+ /** 本模块所在目录(定位 `flow_registry.py` 用;⛔ 不依赖 cwd)。 */
495
+ const HERE = dirname(fileURLToPath(import.meta.url))
496
+
497
+ /**
498
+ * **同步**读合并注册表(唯一真源仍是 `scripts/flow_registry.py`,⛔ 本模块不实现合并)。
499
+ * 命令与 `scripts/lib/registry.mjs#loadRegistryDoc()` **同一条**:`<py> scripts/flow_registry.py --json`
500
+ * + `ARGENT_REGISTRY_ROOT=<root>`;解释器优选 `ARGENT_PY`(与内核口径一致),否则裸 `python3`。
501
+ * 读不到 ⇒ **抛错**(调用方按"证据不足 + 原因"上报,⛔ 不静默用一份空注册表顶上)。
502
+ */
503
+ export function defaultSyncRegistryLoader({ repo = '', runSync = null } = {}) {
504
+ const root = repo || dirname(HERE)
505
+ const py = (process.env && process.env.ARGENT_PY) || 'python3'
506
+ return ({ root: r } = {}) => {
507
+ const use = r || root
508
+ const args = [join(use, 'scripts', 'flow_registry.py'), '--json']
509
+ const run = runSync || ((cmd, argv, opts) => execFileSync(cmd, argv, opts))
510
+ const out = run(py, args, {
511
+ encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
512
+ env: { ...process.env, ARGENT_REGISTRY_ROOT: use },
513
+ })
514
+ return JSON.parse(out)
515
+ }
516
+ }
517
+
518
+ /**
519
+ * 组装声明 + native 探针 + shim 探针 ⇒ `resolveScheduleMechanism()`。
520
+ * `deps` 全可注入(夹具零 IO):
521
+ * · `doc` —— 已合并注册表(给 `doc` 就不调 `loadRegistryDoc()`)
522
+ * · `loadDoc` —— 注册表加载器(缺省动态 import `./registry.mjs`)
523
+ * · `scopeDirs` —— 宿主作用域候选(缺省由 `scopeDirsFrom()` 从 dshHome/profile 推)
524
+ * · `scopeDirsProbe` —— **调用方注入的 scope 候选解析器**(返回字符串数组;缺省 null = 用 `scopeDirsFrom()`)。
525
+ * ⚠️ **0.6.0 F1 修复批(2026-10-14)**:npx 形态(`npx @deepseek-ai/dsh web`)的宿主树在
526
+ * `~/.npm/_npx/<hash>/node_modules/@deepseek-ai` ⇒ `scopeDirsFrom()` **一条都推不出来** ⇒
527
+ * native 恒判 `unknown`(MBP 实测)。npx 感知的候选链**唯一实现在 L2 的
528
+ * `scripts/lib/dsh-scope.mjs#autoScopeCandidates()`**(本文件是 L1,⛔ 不 import 宿主档);
529
+ * 故由调用方(`scripts/argent-schedule.mjs` / `scripts/argent-setup-check.mjs`,均 L2)注入。
530
+ * 优先级仍由 L1 定:**profile 候选优先**(现役机器语义不变)> 注入链的其余候选。
531
+ * · `flows` —— 已启用的流程 id(垫片 job 名用;缺省从 doc 的 trigger.schedule 推)
532
+ */
533
+ export async function detectScheduleMechanism(opts = {}) {
534
+ return detectScheduleMechanismSync(opts)
535
+ }
536
+
537
+ /**
538
+ * **同步**版判据(0.6.0 W8):宿主启动期那条路径要求**同步有界**
539
+ * (见 `plugins/argent-core/src/index.js` 的"本批刻意不 await")⇒ 异步版会让状态文件晚一拍落盘。
540
+ * 判据与异步版**逐字同源**(`resolveScheduleMechanism()` 是纯函数);差别只在"怎么把注册表读进来":
541
+ * 这里走 `execFileSync`(与 `scripts/lib/registry.mjs` 同一条命令),⛔ 不自己实现合并。
542
+ */
543
+ export function detectScheduleMechanismSync({
544
+ doc = null, loadDoc = null, dshHome = '', profile = '', repo = '', flows = null,
545
+ scopeDirs = null, scopeDirsProbe = null, launchAgentsDir = '', platform = process.platform, home = homedir(),
546
+ exists = existsSync, env = process.env, runSync = null,
547
+ } = {}) {
548
+ let document = doc
549
+ let how = 'doc(调用方传入)'
550
+ if (!document) {
551
+ try {
552
+ const loader = loadDoc || defaultSyncRegistryLoader({ repo, runSync })
553
+ document = loader({ root: repo || undefined })
554
+ how = 'flow_registry.py(合并,唯一真源)'
555
+ } catch (e) {
556
+ return {
557
+ state: UNKNOWN,
558
+ declaration: normalizeScheduleDeclaration(undefined, { source: '(读不到)' }),
559
+ reason: `注册表读不到(${String((e && (e.stderr || e.message)) || e).split('\n')[0]})⇒ ⛔ 不猜机制`,
560
+ next: ['确认 python 与 scripts/flow_registry.py 可用:python3 scripts/flow_registry.py --json | head'],
561
+ native: null, shim: null, conflict: null, how,
562
+ }
563
+ }
564
+ }
565
+ const declaration = declaredScheduleMechanism(document, { source: how })
566
+ // scope 候选:显式 > 调用方注入的解析器(**npx 感知**,见 `scopeDirsProbe` 文档)> `scopeDirsFrom()`。
567
+ // ⚠️ 注入链的候选**排在 profile 候选之后** ⇒ 现役机器的选择不变(F1 修复不制造回归)。
568
+ const scopePick = Array.isArray(scopeDirs) && scopeDirs.length
569
+ ? { dirs: scopeDirs, from: '调用方显式传入' }
570
+ : mergeScopeDirs(scopeDirsFrom({ dshHome, profile, env }), scopeDirsProbe)
571
+ const native = probeNativeSchedule({ scopeDirs: scopePick.dirs, exists, source: scopePick.from })
572
+ const flowIds = Array.isArray(flows) && flows.length ? flows : scheduledFlowIds(document)
573
+ const shim = probeActivationShim({
574
+ flows: flowIds, jobs: declaration.jobs, repo, launchAgentsDir, exists, home, platform,
575
+ })
576
+ const r = resolveScheduleMechanism({ declaration, native, shim })
577
+ // ⛔ `conflict` 的细节必须**点名是哪几条**流程(判"有双跑风险"而不点名,等于让人自己去猜)——
578
+ // 逐条证据来自垫片探针(已装计划任务名 ⇒ 流程本地名),原生侧 = 声明里 enabled 的那几条。
579
+ if (r.state === 'conflict') {
580
+ const shimFlows = flowsFromPlists(shim.plists, declaration.jobs)
581
+ r.conflict = schedulerConflict({ native, shim, nativeFlows: flowIds, shimFlows })
582
+ }
583
+ return { ...r, declaration, how, flows: flowIds }
584
+ }
585
+
586
+ /** 从合并注册表里取"声明了 `trigger.schedule` 且 enabled"的流程 id(本地名优先)。 */
587
+ export function scheduledFlowIds(doc) {
588
+ const out = []
589
+ const aliases = (doc && doc.aliases) || {}
590
+ const back = {}
591
+ for (const [short, canon] of Object.entries(aliases)) back[canon] = short
592
+ for (const [flowId, spec] of Object.entries((doc && doc.flows) || {})) {
593
+ const s = spec && spec.trigger && spec.trigger.schedule
594
+ if (!s || typeof s !== 'object') continue
595
+ if (s.enabled !== true) continue
596
+ out.push(back[flowId] || String(flowId).split('.').pop())
597
+ }
598
+ return out
599
+ }
600
+
601
+ /**
602
+ * 宿主作用域候选(`…/node_modules/@deepseek-ai`)。
603
+ * ⚠️ 与内核既有口径同源(`scripts/lib/dsh-scope.mjs` / 插件 `setup-boot.mjs#scopeCandidates`),
604
+ * 但**本函数只产生候选**:探不到就由 `probeNativeSchedule()` 报 `unknown`(F1 未修 ⇒ 必须响亮)。
605
+ */
606
+ export function scopeDirsFrom({ dshHome = '', profile = '', env = process.env, home = homedir() } = {}) {
607
+ const out = []
608
+ const push = (p) => { if (p && !out.includes(p)) out.push(p) }
609
+ push(env && env.DSH_SCOPE)
610
+ const dh = dshHome || (env && env.DSH_HOME) || (home ? join(home, '.dsh') : '')
611
+ if (dh && profile) push(join(dh, 'profiles', profile, 'node_modules', '@deepseek-ai'))
612
+ return out
613
+ }
614
+
615
+ /**
616
+ * 合并 L1 自有候选 + 调用方注入的候选(去重;**L1 侧在前** ⇒ 现役 profile 语义不变)。
617
+ *
618
+ * ⚠️ 注入器抛错/返回非数组 ⇒ **静默退到 L1 自有候选**(⛔ 不让一次探针故障把判据打崩 ——
619
+ * 探不到仍由 `probeNativeSchedule()` 如实报 `unknown`)。返回**来源标签**便于把"试过哪些"打给人看。
620
+ * @returns {{dirs: string[], from: string}}
621
+ */
622
+ export function mergeScopeDirs(own = [], probe = null) {
623
+ const out = []
624
+ const push = (p) => { if (typeof p === 'string' && p && !out.includes(p)) out.push(p) }
625
+ for (const p of own || []) push(p)
626
+ const ownCount = out.length
627
+ let extra = []
628
+ if (typeof probe === 'function') {
629
+ try { extra = probe() || [] } catch { extra = [] }
630
+ } else if (Array.isArray(probe)) extra = probe
631
+ for (const p of extra) push(p)
632
+ const n = out.length - ownCount
633
+ return {
634
+ dirs: out,
635
+ from: n > 0
636
+ ? `本机 profile 候选 ${ownCount} 个 + 运行时解析链 ${n} 个(npx 感知)`
637
+ : `本机 profile 候选 ${ownCount} 个`,
638
+ }
639
+ }
640
+
641
+ /** 一行结论(CLI / 插件 / 门禁共用;⛔ 别处不再拼第二句)。 */
642
+ export function mechanismVerdictLine(r) {
643
+ if (!r) return '❓ 定时机制:没有结论'
644
+ const tag = r.state === 'native' || r.state === 'activation-shim' ? '✅'
645
+ : r.state === 'none' ? '➖' : r.state === 'conflict' ? '❌' : '❓'
646
+ return `${tag} 定时机制:${MECHANISM_ZH[r.state] || r.state}(声明 ${r.declared})—— ${r.reason}`
647
+ }
648
+
649
+ /** 读一份 JSON(读不到/解析失败 ⇒ `{ok:false, error}`;⛔ 不抛)。 */
650
+ export function readJsonFile(path) {
651
+ try { return { ok: true, doc: JSON.parse(readFileSync(path, 'utf8')) } }
652
+ catch (e) { return { ok: false, error: String((e && (e.code || e.message)) || e) } }
653
+ }
654
+
655
+ /** 文件存在且是非空目录(探针用;`stat` 失败 ⇒ false,⛔ 不抛)。 */
656
+ export function isDir(path) {
657
+ const st = statOrNull(path)
658
+ return Boolean(st && st.isDirectory())
659
+ }