@whyshu/argent-engine 0.0.0-stage → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/MANIFEST.json +341 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +576 -0
  55. package/engine/scripts/billing-mode-daily.sh +13 -0
  56. package/engine/scripts/build-engine-package.mjs +126 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +378 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/collect-evidence.mjs +42 -0
  81. package/engine/scripts/config-explain.mjs +278 -0
  82. package/engine/scripts/configure-provider.rb +29 -0
  83. package/engine/scripts/customer-config-migrate.mjs +179 -0
  84. package/engine/scripts/customer-preflight.py +892 -0
  85. package/engine/scripts/customer-readiness.sh +135 -0
  86. package/engine/scripts/daily-report-inbox.py +242 -0
  87. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  88. package/engine/scripts/daily-report.mjs +1310 -0
  89. package/engine/scripts/daily-report.sh +47 -0
  90. package/engine/scripts/data/lip-product-dict.json +306 -0
  91. package/engine/scripts/deliver-customer.mjs +932 -0
  92. package/engine/scripts/delivery-faces.txt +31 -0
  93. package/engine/scripts/desktop-trial.sh +146 -0
  94. package/engine/scripts/dev-window.sh +261 -0
  95. package/engine/scripts/drill-clean-machine.sh +315 -0
  96. package/engine/scripts/dsh-api-client.mjs +215 -0
  97. package/engine/scripts/dsh-backup-scope.sh +83 -0
  98. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  99. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  100. package/engine/scripts/dsh-plane.mjs +153 -0
  101. package/engine/scripts/dsh-portforward.py +223 -0
  102. package/engine/scripts/dsh-preflight.mjs +179 -0
  103. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  104. package/engine/scripts/dsh-release-watch.mjs +458 -0
  105. package/engine/scripts/dsh-remote-check.sh +99 -0
  106. package/engine/scripts/dsh-restart.sh +179 -0
  107. package/engine/scripts/dsh-restore.sh +111 -0
  108. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  109. package/engine/scripts/dsh-snapshot.mjs +116 -0
  110. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  111. package/engine/scripts/dsh-upgrade.mjs +612 -0
  112. package/engine/scripts/dsh-url.sh +86 -0
  113. package/engine/scripts/dsh-web-launch.mjs +155 -0
  114. package/engine/scripts/ensure-store-windows.py +509 -0
  115. package/engine/scripts/entitlements.py +458 -0
  116. package/engine/scripts/env_probe.py +411 -0
  117. package/engine/scripts/feishu-card.mjs +252 -0
  118. package/engine/scripts/final-check.mjs +43 -0
  119. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  120. package/engine/scripts/flow-auto-retry.py +454 -0
  121. package/engine/scripts/flow-requirements.json +735 -0
  122. package/engine/scripts/flow-scaffold.py +275 -0
  123. package/engine/scripts/flow-timers-plist.sh +87 -0
  124. package/engine/scripts/flow-timers.sh +184 -0
  125. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  126. package/engine/scripts/flow-trigger.sh +571 -0
  127. package/engine/scripts/flow_billing.py +354 -0
  128. package/engine/scripts/flow_capability_stats.py +314 -0
  129. package/engine/scripts/flow_case.py +194 -0
  130. package/engine/scripts/flow_claim.py +497 -0
  131. package/engine/scripts/flow_config.py +1025 -0
  132. package/engine/scripts/flow_failure.py +588 -0
  133. package/engine/scripts/flow_learn.py +105 -0
  134. package/engine/scripts/flow_ledger.py +235 -0
  135. package/engine/scripts/flow_policy.py +238 -0
  136. package/engine/scripts/flow_preflight.py +339 -0
  137. package/engine/scripts/flow_registry.py +341 -0
  138. package/engine/scripts/flow_retry.py +304 -0
  139. package/engine/scripts/flow_return_to_ops.py +191 -0
  140. package/engine/scripts/flow_source.py +315 -0
  141. package/engine/scripts/flow_sources/__init__.py +53 -0
  142. package/engine/scripts/flow_sources/cases_json.py +106 -0
  143. package/engine/scripts/flow_sources/common.py +428 -0
  144. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  145. package/engine/scripts/flow_stale.py +185 -0
  146. package/engine/scripts/flow_status_reconcile.py +344 -0
  147. package/engine/scripts/flow_triage.py +849 -0
  148. package/engine/scripts/flow_write_verify.py +496 -0
  149. package/engine/scripts/fulfillment.js +586 -0
  150. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  151. package/engine/scripts/gen-preset-rows.mjs +259 -0
  152. package/engine/scripts/init-ml.sh +2278 -0
  153. package/engine/scripts/install-internal-role.sh +190 -0
  154. package/engine/scripts/install-license.mjs +641 -0
  155. package/engine/scripts/install-one-shot.mjs +665 -0
  156. package/engine/scripts/integration_contract.py +220 -0
  157. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  158. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  159. package/engine/scripts/invoice-mapping.example.json +40 -0
  160. package/engine/scripts/invoice-table.sh +195 -0
  161. package/engine/scripts/invoice_carrier_notice.py +279 -0
  162. package/engine/scripts/invoice_fail_closeout.py +297 -0
  163. package/engine/scripts/invoice_lip_runner.py +1508 -0
  164. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  165. package/engine/scripts/invoice_ready_audit.py +191 -0
  166. package/engine/scripts/invoice_runner.py +675 -0
  167. package/engine/scripts/invoice_stageb.py +516 -0
  168. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  169. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  170. package/engine/scripts/lease.py +377 -0
  171. package/engine/scripts/lib/agent-install.mjs +649 -0
  172. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  173. package/engine/scripts/lib/argent-datadir.sh +18 -0
  174. package/engine/scripts/lib/argent_datadir.py +285 -0
  175. package/engine/scripts/lib/argent_filelock.py +507 -0
  176. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  177. package/engine/scripts/lib/cdp_ports.py +197 -0
  178. package/engine/scripts/lib/customer-env.mjs +102 -0
  179. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  180. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  181. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  182. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  183. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  184. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  185. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  186. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  187. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  188. package/engine/scripts/lib/patch-compose.mjs +241 -0
  189. package/engine/scripts/lib/patch-target.mjs +35 -0
  190. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  191. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  192. package/engine/scripts/lib/preset-compat.mjs +127 -0
  193. package/engine/scripts/lib/preset-install.mjs +575 -0
  194. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  195. package/engine/scripts/lib/preset-sync.mjs +59 -0
  196. package/engine/scripts/lib/profile-retire.mjs +97 -0
  197. package/engine/scripts/lib/python-runtime.mjs +190 -0
  198. package/engine/scripts/lib/registry.mjs +72 -0
  199. package/engine/scripts/lib/repo-root.sh +56 -0
  200. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  201. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  202. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  203. package/engine/scripts/lib/settings-panels.mjs +816 -0
  204. package/engine/scripts/lib/stdio_utf8.py +86 -0
  205. package/engine/scripts/lib/wake-auth.mjs +392 -0
  206. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  207. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  208. package/engine/scripts/license-e2e-drill.mjs +426 -0
  209. package/engine/scripts/license-gate.mjs +245 -0
  210. package/engine/scripts/lip_backlog.py +171 -0
  211. package/engine/scripts/lip_table.py +185 -0
  212. package/engine/scripts/local-usage.mjs +214 -0
  213. package/engine/scripts/migrate-providers.mjs +127 -0
  214. package/engine/scripts/migrate-skills.sh +65 -0
  215. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  216. package/engine/scripts/ml-store-ids.json +26 -0
  217. package/engine/scripts/ml_artifact_attribution.py +333 -0
  218. package/engine/scripts/ml_capacity_probe.py +1158 -0
  219. package/engine/scripts/ml_hub_status.py +1556 -0
  220. package/engine/scripts/ml_schema_check.py +115 -0
  221. package/engine/scripts/ml_store_ids.py +139 -0
  222. package/engine/scripts/npm-publish-argent.mjs +499 -0
  223. package/engine/scripts/npm-publish-argent.sh +20 -0
  224. package/engine/scripts/orchestrator.py +13811 -0
  225. package/engine/scripts/pack-plugins.mjs +140 -0
  226. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  227. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  228. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  229. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  230. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  231. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  232. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  233. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  234. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  235. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  236. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  237. package/engine/scripts/plan-role-rename.mjs +125 -0
  238. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  239. package/engine/scripts/plugin-composition.json +15 -0
  240. package/engine/scripts/poc-mcp-host.mjs +455 -0
  241. package/engine/scripts/poll-fulfillment.sh +295 -0
  242. package/engine/scripts/poll-invoice.sh +76 -0
  243. package/engine/scripts/profile-doctor.mjs +686 -0
  244. package/engine/scripts/provider-select.sh +71 -0
  245. package/engine/scripts/provider-switch.sh +146 -0
  246. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  247. package/engine/scripts/python-runtime.sh +78 -0
  248. package/engine/scripts/query_runner.py +310 -0
  249. package/engine/scripts/release-argent.sh +574 -0
  250. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  251. package/engine/scripts/retire-inventory.mjs +40 -0
  252. package/engine/scripts/run-tests.sh +257 -0
  253. package/engine/scripts/s1-audit.mjs +85 -0
  254. package/engine/scripts/self-sign-license.mjs +179 -0
  255. package/engine/scripts/session-health-scan.mjs +63 -0
  256. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  257. package/engine/scripts/setup.sh +22 -0
  258. package/engine/scripts/skill-install.mjs +629 -0
  259. package/engine/scripts/sku_translate.py +148 -0
  260. package/engine/scripts/sync-plugins.sh +90 -0
  261. package/engine/scripts/sync-presets.sh +27 -0
  262. package/engine/scripts/sync-scripts.sh +445 -0
  263. package/engine/scripts/telemetry.py +873 -0
  264. package/engine/scripts/ui-copy-count.mjs +220 -0
  265. package/engine/scripts/usage-anchor.sh +137 -0
  266. package/engine/scripts/usage_report.py +531 -0
  267. package/engine/scripts/validate-preset-schema.mjs +208 -0
  268. package/engine/scripts/vertical-init.mjs +323 -0
  269. package/engine/scripts/vertical-lint.mjs +482 -0
  270. package/engine/scripts/wenyao-browser.sh +74 -0
  271. package/engine/scripts/wenyao-fixture-export.py +116 -0
  272. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  273. package/engine/scripts/wenyao-login.sh +35 -0
  274. package/engine/scripts/wenyao-template-map.json +292 -0
  275. package/engine/scripts/wenyao_attach.py +131 -0
  276. package/engine/scripts/wenyao_download_template.py +144 -0
  277. package/engine/scripts/wenyao_fill_template.py +568 -0
  278. package/engine/scripts/wenyao_login.py +145 -0
  279. package/engine/scripts/wenyao_portal.py +633 -0
  280. package/engine/scripts/wenyao_recon.py +230 -0
  281. package/engine/scripts/wenyao_run.py +503 -0
  282. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  283. package/engine/scripts/wenyao_skill_contract.py +156 -0
  284. package/engine/scripts/wenyao_template_fetch.py +175 -0
  285. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  286. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  287. package/engine/scripts/ziniao-keepalive.sh +217 -0
  288. package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
  289. package/engine/scripts/ziniao-windows.sh +154 -0
  290. package/engine/skills/README.md +86 -0
  291. package/engine/skills/argent-dev/SKILL.md +41 -0
  292. package/engine/skills/internal.json +37 -0
  293. package/engine/skills/lktop-pm/SKILL.md +35 -0
  294. package/engine/skills/lktopapp-dev/SKILL.md +34 -0
  295. package/engine/skills/ml-config-guide/SKILL.md +50 -0
  296. package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
  297. package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
  298. package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
  299. package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
  300. package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
  301. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
  302. package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
  303. package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
  304. package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
  305. package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
  306. package/engine/skills/ml-invoice/SKILL.md +506 -0
  307. package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
  308. package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
  309. package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
  310. package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
  311. package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
  312. package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
  313. package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
  314. package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
  315. package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
  316. package/engine/skills/pet-dev/SKILL.md +42 -0
  317. package/engine/skills/whyshu-pm/SKILL.md +35 -0
  318. package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
  319. package/engine/version.mjs +1 -0
  320. package/engine/verticals/README.md +103 -0
  321. package/engine/verticals/_engine-words.baseline.txt +237 -0
  322. package/engine/verticals/_engine-words.json +19 -0
  323. package/engine/verticals/mercadolibre/README.md +43 -0
  324. package/engine/verticals/mercadolibre/config.json +709 -0
  325. package/engine/verticals/mercadolibre/flows.json +738 -0
  326. package/engine/verticals/mercadolibre/manifest.json +538 -0
  327. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  328. package/engine/verticals/mercadolibre/preflight.py +143 -0
  329. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  330. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  331. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  332. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  333. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  334. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  335. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  336. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  337. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  338. package/package.json +22 -4
  339. package/requirements.txt +5 -0
  340. package/README.md +0 -3
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """裁定沉淀闭环(P6,2026-09-17)—— 用户确认一次 → **两边同时落库 + 互相引用**。
3
+
4
+ 分层(docs/Argent-宿主适配契约.md §1「判断归属」):
5
+ · **机器判据** → 政策库(`flow-policy.jsonl`,L2 政策层):下次同类 case 命中即执行,不再问用户;
6
+ · **判读知识** → 技能里的 pitfalls/SKILL(给角色/人看):遇到**没见过**的新情况时能推理;
7
+ · 两边**互相指向**:政策条目带 `whyRef: <skill>#<anchor>`;markdown 条目带「判据权威:政策 `pol-…`」。
8
+
9
+ 为什么必须一次写完两边:只写政策 ⇒ 模型不会判读新情况;只写文档 ⇒ 脚本/引擎读不到、判据漂移。
10
+ `--yes` 才落库(默认 dry-run);每次落库都写台账事件 `policy_new`(审计:谁授权、依据哪条案例)。
11
+
12
+ 用法:
13
+ python3 scripts/flow_learn.py --flow full --case full__recvvgPQgoZW9B \\
14
+ --decision retry --kind retryable --title "瞬时类失败自动重跑" \\
15
+ --summary "浏览器页面被关/网络超时属瞬时类:清货件号重建(≤2 次),不叫人" \\
16
+ --skill skills/ml-fulfillment-sop/references/pitfalls.md --anchor C7 --yes
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import datetime
22
+ import pathlib
23
+ import subprocess
24
+ import sys
25
+
26
+ HERE = pathlib.Path(__file__).resolve().parent
27
+ sys.path.insert(0, str(HERE))
28
+ import flow_policy # noqa: E402 政策库(同一个 append/id 生成,避免两套写法)
29
+
30
+ REPO = HERE.parent
31
+
32
+
33
+ def render_entry(policy_id: str, anchor: str, title: str, summary: str, case: str,
34
+ authorizer: str, at: str, flow: str) -> str:
35
+ return (f"\n### {anchor} {title}({at[:10]},来源 `{case}`)\n\n"
36
+ f"- 现象/触发:{summary}\n"
37
+ f"- 裁定:见政策条目({flow})\n"
38
+ f"- 判据权威:政策 `{policy_id}`(`~/.dsh/argent/flow-policy.jsonl`)\n"
39
+ f"- 授权人:{authorizer}|流程:{flow}\n")
40
+
41
+
42
+ def main(argv=None) -> int:
43
+ ap = argparse.ArgumentParser(description="裁定沉淀:政策条目 + 技能条目(互相引用)")
44
+ ap.add_argument("--flow", required=True)
45
+ ap.add_argument("--case", required=True, help="来源 case(可追溯)")
46
+ ap.add_argument("--decision", required=True, choices=flow_policy.DECISIONS)
47
+ ap.add_argument("--kind", help="失败分类(failureKind),作为匹配条件")
48
+ ap.add_argument("--status", help="状态匹配条件(可选)")
49
+ ap.add_argument("--title", required=True, help="pitfalls 条目标题")
50
+ ap.add_argument("--summary", required=True, help="现象/触发(写进 pitfalls)")
51
+ ap.add_argument("--skill", required=True, help="pitfalls/SKILL 文件(相对仓库或绝对路径)")
52
+ ap.add_argument("--anchor", required=True, help="锚点(如 C7),政策 whyRef 指向它")
53
+ ap.add_argument("--authorizer", default="用户")
54
+ ap.add_argument("--max-attempts", type=int)
55
+ ap.add_argument("--yes", action="store_true", help="真写(默认 dry-run)")
56
+ ap.add_argument("--json", action="store_true")
57
+ a = ap.parse_args(argv)
58
+
59
+ skill_path = pathlib.Path(a.skill)
60
+ if not skill_path.is_absolute():
61
+ skill_path = REPO / skill_path
62
+ if not skill_path.exists():
63
+ print(f"❌ skill 文件不存在:{skill_path}(先建文件再加条目,避免引用悬空)", file=sys.stderr)
64
+ return 2
65
+ if a.anchor not in skill_path.read_text(encoding="utf-8") and not a.yes:
66
+ pass # 允许首次新增该锚点(dry-run 预览会写出来)
67
+
68
+ diff = (f"{a.skill}#{a.anchor}")
69
+ pid = flow_policy.new_id()
70
+ at = datetime.datetime.now().isoformat(timespec="seconds")
71
+ match = {"flow": a.flow}
72
+ if a.kind:
73
+ match["failureKind"] = a.kind
74
+ if a.status:
75
+ match["status"] = a.status
76
+ policy = {"id": pid, "match": match, "decision": a.decision,
77
+ "limits": ({"maxAttempts": a.max_attempts} if a.max_attempts else {}),
78
+ "authorizer": a.authorizer, "sourceCase": a.case, "whyRef": diff, "at": at, "enabled": True}
79
+ entry = render_entry(pid, a.anchor, a.title, a.summary, a.case, a.authorizer, at, a.flow)
80
+
81
+ print("── 将写入两边(互相引用)──")
82
+ print(f" ① 政策:{json_dumps(policy)}")
83
+ print(f" ② 技能:{a.skill} 追加锚点 {a.anchor}({len(entry)} 字)")
84
+ print(f" ③ 台账:policy_new(case={a.case},授权人={a.authorizer})")
85
+ if not a.yes:
86
+ print("\n[dry-run] 未写入。加 --yes 才落库。")
87
+ return 0
88
+
89
+ flow_policy.append(policy)
90
+ with skill_path.open("a", encoding="utf-8") as fh:
91
+ fh.write(entry)
92
+ subprocess.run([sys.executable, str(HERE / "flow_ledger.py"), "--case-id", a.case, "--event", "policy_new",
93
+ "--by", a.authorizer or "user", "--detail",
94
+ f"新增政策 {pid}({a.decision})+ 技能条目 {diff}"], check=False)
95
+ print(f"\n ✅ 政策 {pid} 已入库;技能条目已追加({diff})")
96
+ return 0
97
+
98
+
99
+ def json_dumps(obj) -> str:
100
+ import json
101
+ return json.dumps(obj, ensure_ascii=False)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ sys.exit(main())
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env python3
2
+ """flow_ledger —— 流程台账读写(掌舵者与 subagent 共用的确定性入口)。
3
+
4
+ 台账:~/.dsh/argent/flow-ledger.jsonl(单行事件,追加写 + 文件锁,避免并发交错)
5
+ {"ts":<epoch秒>,"caseId":"<flow>__<ticket>","event":"dispatched|done|failed|blocked","attempt":N,"error":"..","by":"<谁>"}
6
+ · `planned`(认领)可带 `release`/`releaseRetry`(人工放行重跑)、`externalTiming`/`externalRetry`
7
+ (外部时序独立通道)、`upstreamFieldHash`(上游逐字段 8 位哈希 = "上次派发快照")、`upstreamReset`;
8
+ · `policy_apply` 可带 `policy`(见 `POLICIES`;`release_budget_reset` = 上游数据变化 ⇒ 放行额度重置)。
9
+
10
+ 用法:
11
+ python3 scripts/flow_ledger.py --case-id wenyao-invoice__76135791 --event dispatched --by gw-ml-invoice-wy
12
+ python3 scripts/flow_ledger.py --case-id … --event failed --attempt 1 --error "门户登录失败"
13
+ python3 scripts/flow_ledger.py --status [--json] # 每 case 最新状态
14
+ python3 scripts/flow_ledger.py --status --flow wenyao-invoice # 只看某流程(caseId 前缀)
15
+ python3 scripts/flow_ledger.py --inflight [--flow X] # 只输出在飞数(限流用)
16
+ 退出码:0 成功;2 参数/写入错误
17
+ """
18
+ from __future__ import annotations
19
+ from typing import Any
20
+ import argparse, json, os, pathlib, sys, time
21
+
22
+ # 默认台账:数据目录走**唯一判据** `scripts/lib/argent_datadir.py`(三级链,与 JS 内核同语义;
23
+ # 勿在此复制链 —— 旧的两级写法不看 DSH_HOME,隔离实例的 CLI/cron 会写进生产台账)
24
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent / 'lib'))
25
+ from argent_datadir import resolve_data_dir # noqa: E402
26
+ # 跨平台文件锁**唯一真源**(POSIX `fcntl.flock` / Windows `msvcrt.locking`):
27
+ # 此前本文件顶层 `import fcntl` ⇒ Windows 上 `ModuleNotFoundError` ⇒ 台账整个不可用。
28
+ from argent_filelock import best_effort # noqa: E402
29
+ DEFAULT_LEDGER = pathlib.Path(resolve_data_dir()['dir']) / 'flow-ledger.jsonl'
30
+ EVENTS = ('dispatched', 'done', 'failed', 'blocked', 'planned', 'phase',
31
+ 'wake', 'retry', 'failed_ack',
32
+ 'requeued', # 停滞回收(flow_stale 写):triage 认它 ⇒ 票回到 to_dispatch
33
+ "policy_apply", "policy_new") # wake=因失败唤醒角色;retry=判定可重试;failed_ack=已人工确认
34
+ DONE_EVENTS = ('done', 'failed', 'blocked')
35
+ # `policy_apply` 可带 `policy` 标记(机器可判的**政策名**):
36
+ # · `release_budget_reset` —— 上游数据变化 ⇒ 该票放行额度重置(`flow_triage` 写;
37
+ # ⛔ 它只把**计数基线归零**,不改变票据状态 —— `flow_triage#INFO_EVENTS` 已含 policy_apply)。
38
+ # 只加键:老台账没有 `policy` 字段照旧可读(未知 policy 一律当普通信息性政策记账)。
39
+ POLICIES = ('release_budget_reset',)
40
+
41
+
42
+ def _clean_text(v: Any) -> Any:
43
+ """把**孤立代理字符**替换成 `?`(缺陷⑦,2026-09-23 角色实测)。
44
+
45
+ 为什么会有:`--detail` 走 argv 传入超长中文时,某些来源(shell/终端)会给到含孤立 surrogate
46
+ 的字符串;`json.dumps` 能产出它,但**写 UTF-8 文件时会抛**
47
+ `'utf-8' codec can't encode … surrogates not allowed` ⇒ 整条台账记录丢失(比截断更糟)。
48
+ 只替换代理区(U+D800–U+DFFF),其余字符原样保留。
49
+ """
50
+ if isinstance(v, str):
51
+ if not any(0xD800 <= ord(ch) <= 0xDFFF for ch in v):
52
+ return v # 常见路径零开销
53
+ return ''.join('?' if 0xD800 <= ord(ch) <= 0xDFFF else ch for ch in v)
54
+ if isinstance(v, dict):
55
+ return {k: _clean_text(x) for k, x in v.items()}
56
+ if isinstance(v, list):
57
+ return [_clean_text(x) for x in v]
58
+ return v
59
+
60
+
61
+ def append(path: pathlib.Path, rec: dict) -> None:
62
+ path.parent.mkdir(parents=True, exist_ok=True)
63
+ rec = _clean_text(rec) # ⑦:先清代理字符,避免整条记录写盘失败
64
+ line = json.dumps(rec, ensure_ascii=False) + '\n'
65
+ with open(path, 'a', encoding='utf-8') as f:
66
+ # 追加写加锁(避免多 subagent 交错):策略 (b) **响亮降级** —— 台账自身的约定是
67
+ # 「绝不阻断主流程」(见模块 docstring),所以锁不可用时无锁继续,但**必须出声**
68
+ # (旧实现在 Windows 上 `import fcntl` 静默失败 ⇒ 一声不吭地无锁写)。
69
+ with best_effort(f, site='flow_ledger.append', note='台账追加降级为无锁写'):
70
+ f.write(line)
71
+ f.flush()
72
+ os.fsync(f.fileno())
73
+
74
+
75
+ def read(path: pathlib.Path) -> list[dict]:
76
+ if not path.exists():
77
+ return []
78
+ out = []
79
+ for ln in path.read_text(encoding='utf-8').splitlines():
80
+ ln = ln.strip()
81
+ if not ln:
82
+ continue
83
+ try:
84
+ out.append(json.loads(ln))
85
+ except Exception:
86
+ continue
87
+ return out
88
+
89
+
90
+ INFLIGHT_WINDOW_MIN = 30
91
+
92
+
93
+ def status(path: pathlib.Path, flow: str | None = None, now: float | None = None,
94
+ inflight_min: int = INFLIGHT_WINDOW_MIN) -> dict:
95
+ """每 case 最新事件 + 在飞清单。
96
+
97
+ `flow` 给定时只统计该流程 —— 多流程共用一份台账(`caseId = <flow>__<业务键>`,
98
+ 由 flow_case.py 保证前缀),所以按前缀过滤即可,**无需数据迁移**。
99
+
100
+ ⚠️ 在飞口径(2026-09-18 修,用户实测两处矛盾后统一):
101
+ · 最新事件 `planned` 且 `leaseUntil > now` ⇒ 已认领、租约未过期 = **真在飞**
102
+ (新引擎认领写 `planned`,**从不写 `dispatched`** ⇒ 旧规则永远数不到在跑的票:
103
+ 实测 FULL 真跑时面板显示"在飞=0");
104
+ · 最新事件 `dispatched` 且 `now - ts < inflight_min` ⇒ 旧架构遗留事件按窗口算在飞
105
+ (旧事件没有收尾事件 ⇒ 不加窗口会让历史票**永久**显示在飞:实测
106
+ wenyao-invoice__76636923/76637122 两天前的 dispatched ⇒ 面板恒显示"在飞=2")。
107
+ 与 `flow_claim.live_leases()` / `flow_triage` 的在飞判据一致。
108
+ """
109
+ now = time.time() if now is None else now
110
+ prefix = f'{flow}__' if flow else None
111
+ last, inflight = {}, []
112
+ for e in read(path):
113
+ cid = e.get('caseId')
114
+ if not cid:
115
+ continue
116
+ if prefix and not str(cid).startswith(prefix):
117
+ continue
118
+ last[cid] = e
119
+ for cid, e in last.items():
120
+ ev = e.get('event')
121
+ if ev == 'planned':
122
+ if float(e.get('leaseUntil') or 0) > now:
123
+ inflight.append(cid)
124
+ elif ev == 'dispatched':
125
+ if now - float(e.get('ts') or 0) < inflight_min * 60:
126
+ inflight.append(cid)
127
+ return {'last': last, 'inflight': sorted(inflight), 'now': now, 'inflightMin': inflight_min}
128
+
129
+
130
+ def main(argv=None) -> int:
131
+ ap = argparse.ArgumentParser(description='流程台账读写')
132
+ ap.add_argument('--ledger', default=str(DEFAULT_LEDGER))
133
+ ap.add_argument('--case-id')
134
+ ap.add_argument('--event', choices=EVENTS)
135
+ ap.add_argument('--attempt', type=int)
136
+ ap.add_argument('--error')
137
+ ap.add_argument('--by', help='记录者(角色名或 subagent 标识)')
138
+ ap.add_argument('--phase', help='阶段名(--event phase 时用,如 P1路由/P3填报)')
139
+ ap.add_argument('--policy', choices=POLICIES,
140
+ help='政策名(--event policy_apply 时用;见 POLICIES)')
141
+ ap.add_argument('--detail', help='阶段细节(产物路径/退出码等)')
142
+ ap.add_argument('--status', action='store_true')
143
+ ap.add_argument('--inflight', action='store_true')
144
+ ap.add_argument('--flow', help='只看该流程的 case(按 caseId 前缀 <flow>__ 过滤 --status/--inflight)')
145
+ ap.add_argument('--inflight-min', type=int, default=INFLIGHT_WINDOW_MIN,
146
+ help='dispatched 事件算在飞的窗口(分钟,缺省 30;与 flow_triage 同口径)')
147
+ ap.add_argument('--now', type=float, help='覆盖"当前时间"(测试用,保证判定确定)')
148
+ ap.add_argument('--json', action='store_true')
149
+ a = ap.parse_args(argv)
150
+ path = pathlib.Path(a.ledger)
151
+
152
+ if a.status or a.inflight:
153
+ st = status(path, a.flow, now=a.now, inflight_min=a.inflight_min)
154
+ if a.inflight:
155
+ print(len(st['inflight']))
156
+ elif a.json:
157
+ print(json.dumps({'flow': a.flow, **st}, ensure_ascii=False, indent=2))
158
+ else:
159
+ scope = f'|流程 {a.flow}' if a.flow else ''
160
+ print(f' 台账 {path}{scope}|case 数 {len(st["last"])}|在飞 {len(st["inflight"])}')
161
+ for cid, e in sorted(st['last'].items()):
162
+ print(f" {cid} {e.get('event')} attempt={e.get('attempt', '-')} {str(e.get('error') or '')[:50]}")
163
+ return 0
164
+
165
+ if not (a.case_id and a.event):
166
+ print('需要 --case-id 与 --event(或用 --status/--inflight)', file=sys.stderr)
167
+ return 2
168
+ rec = {'ts': int(time.time()), 'caseId': a.case_id, 'event': a.event}
169
+ if a.attempt is not None:
170
+ rec['attempt'] = a.attempt
171
+ if a.error:
172
+ rec['error'] = str(a.error)[:500]
173
+ if a.phase:
174
+ rec['phase'] = str(a.phase)[:60]
175
+ if a.policy:
176
+ rec['policy'] = str(a.policy)
177
+ if a.detail:
178
+ rec['detail'] = str(a.detail)[:300]
179
+ if a.by:
180
+ rec['by'] = a.by
181
+ try:
182
+ append(path, rec)
183
+ except Exception as e:
184
+ print(f'台账写入失败: {e}', file=sys.stderr)
185
+ return 2
186
+ if not a.json:
187
+ print(f" ✅ 已记 {rec['caseId']} → {rec['event']}" + (f"(attempt {rec['attempt']})" if 'attempt' in rec else ''))
188
+ else:
189
+ print(json.dumps(rec, ensure_ascii=False))
190
+ _telemetry_hook(rec)
191
+ return 0
192
+
193
+
194
+ def _telemetry_hook(rec: dict) -> None:
195
+ """失败/阻塞 → 采集一条**元数据**遥测事件(2026-09-21)。
196
+
197
+ 为什么挂在台账这一层:**四条流程的失败都写这里**(FULL/文鳐/鑫中航/日报),
198
+ 一个钩子就全覆盖,不必在每个 runner 里各写一遍。
199
+
200
+ 三条纪律(照 `scripts/telemetry.py` 的口径):
201
+ · 只落本地队列(不联网)—— 联网交给 flow-trigger 每轮的 `--flush`;
202
+ · **不传原始错误文本**:`detail` 只用 `flow_failure.classify` 给出的**固定措辞**,
203
+ 原始 error 里可能有店铺名/货件号(实测过:数字串能掩,中文店名掩不掉);
204
+ · 任何异常都吞掉 —— 遥测绝不允许影响台账写入与主流程。
205
+ """
206
+ if rec.get('event') not in ('failed', 'blocked'):
207
+ return
208
+ try:
209
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
210
+ import telemetry as T # noqa: PLC0415 同目录模块
211
+ # 引擎级失败的合成票号(claim__/preflight__/…)由**触发链**发 kind=engine,
212
+ # 这里跳过 —— 否则同一次失败会写两条事件。
213
+ if T.is_engine_case(rec.get('caseId')):
214
+ return
215
+ from flow_failure import classify # noqa: PLC0415 与失败回路同一套判据
216
+ raw = str(rec.get('error') or rec.get('detail') or '')
217
+ cls = classify(raw, rec.get('attempt'))
218
+ flow = T.flow_of_case(rec.get('caseId'))
219
+ T.emit(
220
+ 'failure',
221
+ flow=flow,
222
+ capability=T.capability_of(flow),
223
+ role=str(rec.get('by') or ''),
224
+ case=str(rec.get('caseId') or ''),
225
+ phase=str(rec.get('phase') or ''),
226
+ error_class=str(cls.get('kind') or ''),
227
+ attempt=int(rec.get('attempt') or 0),
228
+ detail=str(cls.get('reason') or ''), # 固定措辞,不含业务值
229
+ )
230
+ except Exception: # noqa: BLE001
231
+ pass
232
+
233
+
234
+ if __name__ == '__main__':
235
+ raise SystemExit(main())
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env python3
2
+ """流程**政策库**(P1,2026-09-17)—— "你确认一次,以后同类不再问你"的落地件。
3
+
4
+ 分层(见 docs/Argent-宿主适配契约.md §1「判断归属」):
5
+ · 本文件 = **L2 政策层**的一部分:判据的**唯一权威副本**,机器可读、可匹配、可追溯;
6
+ · 角色(L4)遇到 case 先 `--match`:命中 ⇒ 依政策执行并记台账 `policy_apply`;
7
+ 未命中 ⇒ 问你,得到答复后由角色 **提议** 一条政策(`--add`)→ 你确认 → 入库。
8
+ · 权限边界(什么能自动、什么必须问)由 `--authority` 给出(注册表 `flows.<id>.authority`,缺省见下)。
9
+
10
+ 政策条目(每行一条 JSON,`~/.dsh/argent/flow-policy.jsonl`):
11
+ {"id":"pol-...", "match":{"flow":"full","failureKind":"retryable"}, "decision":"retry",
12
+ "limits":{"maxAttempts":2}, "authorizer":"用户", "sourceCase":"full__recvvgPQgoZW9B",
13
+ "at":"2026-09-17T17:37:00", "whyRef":"skills/.../pitfalls.md#C7", "enabled":true}
14
+
15
+ 匹配规则:`match` 的每个键都要对上(`*` 表示任意;数组表示枚举);**越具体越优先**
16
+ (命中键数多者胜,其次时间新者胜)。口径类变更(caliber)不允许自动,只能由用户改。
17
+
18
+ 用法:
19
+ python3 scripts/flow_policy.py --list [--flow full] [--json]
20
+ python3 scripts/flow_policy.py --match --flow full --failure-kind retryable --status 失败
21
+ python3 scripts/flow_policy.py --add --decision retry --match-kv flow=full --match-kv failureKind=retryable \\
22
+ --authorizer 用户 --source-case full__recvvgPQgoZW9B --why-ref skills/ml-fulfillment-sop/references/pitfalls.md#C7
23
+ python3 scripts/flow_policy.py --retire pol-xxxx # 软停用(保留审计)
24
+ python3 scripts/flow_policy.py --authority --flow full # 权限边界
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+
30
+ try:
31
+ from flow_registry import consumer_doc as _registry_doc
32
+ except Exception: # noqa: BLE001 —— 半坏机器上退回单文件
33
+ def _registry_doc(path=None):
34
+ _p = pathlib.Path(path) if path else pathlib.Path(__file__).resolve().with_name("flow-requirements.json")
35
+ return json.loads(_p.read_text(encoding="utf-8"))
36
+
37
+ import datetime
38
+ import json
39
+ import os
40
+ import pathlib
41
+ import sys
42
+
43
+ # 数据目录:**唯一判据**在 `scripts/lib/argent_datadir.py`(三级链,与 JS 内核同语义)——
44
+ # 勿在此复制链(旧的两级写法不看 DSH_HOME,隔离实例的 CLI 会写进生产政策库)
45
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent / 'lib'))
46
+ from argent_datadir import resolve_data_dir # noqa: E402
47
+ DATA_DIR = pathlib.Path(resolve_data_dir()['dir'])
48
+ POLICY = DATA_DIR / "flow-policy.jsonl"
49
+ DECISIONS = ("retry", "hold", "escalate", "discard")
50
+
51
+ # 权限边界缺省(注册表 flows.<id>.authority 可覆盖):auto=角色可自行决定;ask=必须问用户
52
+ DEFAULT_AUTHORITY = {
53
+ "retry": {"auto": True, "maxAttempts": 2, "_why": "瞬时类失败自动重跑,上限内不打扰用户(用户口径 A)"},
54
+ "hold": {"auto": True, "_why": "等人补数据/落卡:不算处置,属于把球踢回运营"},
55
+ "escalate": {"auto": True, "_why": "升级给用户(需要口径或授权)"},
56
+ "discard": {"auto": False, "_why": "作废一票=不可逆,必须用户确认"},
57
+ "write": {"scope": "guarded", "askOutside": True, "_why": "仅守卫白名单内的字段/产出物可写,越界要授权"},
58
+ "caliber": {"auto": False, "_why": "口径(阈值/字段语义/候选判据)只能用户改"},
59
+ }
60
+
61
+
62
+ def load(path: pathlib.Path | None = None) -> list[dict]:
63
+ p = path or POLICY
64
+ out = []
65
+ try:
66
+ for line in p.read_text(encoding="utf-8").splitlines():
67
+ if not line.strip():
68
+ continue
69
+ try:
70
+ d = json.loads(line)
71
+ except Exception:
72
+ continue
73
+ if isinstance(d, dict) and d.get("id"):
74
+ out.append(d)
75
+ except FileNotFoundError:
76
+ pass
77
+ return out
78
+
79
+
80
+ def matches(policy: dict, ctx: dict) -> bool:
81
+ """`match` 的每个键都要对上(`*`=任意;数组=枚举;其余按字符串比较)。"""
82
+ m = policy.get("match") or {}
83
+ for key, want in m.items():
84
+ got = ctx.get(key)
85
+ if want == "*":
86
+ continue
87
+ if isinstance(want, list):
88
+ if str(got) not in [str(x) for x in want]:
89
+ return False
90
+ elif str(got) != str(want):
91
+ return False
92
+ return True
93
+
94
+
95
+ def pick(policies: list[dict], ctx: dict) -> dict | None:
96
+ """选中最具体的启用政策:命中键数多者胜 → 时间新者胜。"""
97
+ hits = [p for p in policies if p.get("enabled", True) and matches(p, ctx)]
98
+ if not hits:
99
+ return None
100
+ hits.sort(key=lambda p: (len(p.get("match") or {}), str(p.get("at") or "")), reverse=True)
101
+ return hits[0]
102
+
103
+
104
+ def authority(flow: str | None) -> dict:
105
+ """权限边界:注册表 flows.<flow>.authority 覆盖缺省。"""
106
+ out = {k: dict(v) for k, v in DEFAULT_AUTHORITY.items()}
107
+ try:
108
+ reg = _registry_doc()
109
+ spec = ((reg.get("flows") or {}).get(flow or "") or {})
110
+ for k, v in (spec.get("authority") or {}).items():
111
+ out[k] = {**(out.get(k) or {}), **(v if isinstance(v, dict) else {"auto": bool(v)})}
112
+ except Exception:
113
+ pass
114
+ return out
115
+
116
+
117
+ def append(entry: dict, path: pathlib.Path | None = None) -> dict:
118
+ p = path or POLICY
119
+ p.parent.mkdir(parents=True, exist_ok=True)
120
+ with p.open("a", encoding="utf-8") as fh:
121
+ fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
122
+ return entry
123
+
124
+
125
+ def new_id() -> str:
126
+ return "pol-" + datetime.datetime.now().strftime("%Y%m%d%H%M%S%f")[:17]
127
+
128
+
129
+ def main(argv=None) -> int:
130
+ ap = argparse.ArgumentParser(description="流程政策库(L2 政策层)")
131
+ ap.add_argument("--path", help="政策文件(默认 ~/.dsh/argent/flow-policy.jsonl)")
132
+ ap.add_argument("--list", action="store_true")
133
+ ap.add_argument("--match", action="store_true", help="按上下文匹配政策")
134
+ ap.add_argument("--add", action="store_true", help="新增一条政策")
135
+ ap.add_argument("--retire", help="软停用某条政策(保留审计)")
136
+ ap.add_argument("--authority", action="store_true", help="打印权限边界")
137
+ ap.add_argument("--flow")
138
+ ap.add_argument("--failure-kind", dest="failureKind")
139
+ ap.add_argument("--status")
140
+ ap.add_argument("--store")
141
+ ap.add_argument("--phase")
142
+ ap.add_argument("--decision", choices=DECISIONS)
143
+ ap.add_argument("--match-kv", dest="match_pairs", action="append", default=[],
144
+ help="匹配条件 k=v(可多次;v 可用逗号分隔表示枚举)")
145
+ ap.add_argument("--max-attempts", type=int)
146
+ ap.add_argument("--authorizer", default="")
147
+ ap.add_argument("--source-case", default="")
148
+ ap.add_argument("--why-ref", default="")
149
+ ap.add_argument("--json", action="store_true")
150
+ a = ap.parse_args(argv)
151
+
152
+ path = pathlib.Path(a.path).expanduser() if a.path else None
153
+ policies = load(path)
154
+
155
+ if a.authority:
156
+ out = authority(a.flow)
157
+ print(json.dumps(out, ensure_ascii=False, indent=2) if a.json else
158
+ "\n".join(f" {k:9} auto={v.get('auto', '?')} {v.get('_why', '')}" for k, v in out.items()))
159
+ return 0
160
+
161
+ if a.list:
162
+ rows = [p for p in policies if not a.flow or (p.get("match") or {}).get("flow") in (a.flow, None, "*")]
163
+ if a.json:
164
+ print(json.dumps(rows, ensure_ascii=False, indent=2))
165
+ else:
166
+ print(f" 政策 {len(rows)} 条({path or POLICY})")
167
+ for p in rows:
168
+ flag = "" if p.get("enabled", True) else "(已停用)"
169
+ print(f" {p['id']} {p.get('decision'):8} match={json.dumps(p.get('match'), ensure_ascii=False)}{flag}")
170
+ if p.get("sourceCase"):
171
+ print(f" 来源 case={p['sourceCase']}|授权人={p.get('authorizer') or '-'}|{p.get('at') or ''}")
172
+ return 0
173
+
174
+ if a.match:
175
+ ctx = {k: v for k, v in {"flow": a.flow, "failureKind": a.failureKind, "status": a.status,
176
+ "store": a.store, "phase": a.phase}.items() if v}
177
+ hit = pick(policies, ctx)
178
+ if a.json:
179
+ print(json.dumps({"context": ctx, "policy": hit}, ensure_ascii=False, indent=2))
180
+ elif hit:
181
+ print(f" ✅ 命中政策 {hit['id']}:{hit.get('decision')}"
182
+ f"(match={json.dumps(hit.get('match'), ensure_ascii=False)}"
183
+ f"|授权人={hit.get('authorizer') or '-'}|来源={hit.get('sourceCase') or '-'})")
184
+ else:
185
+ print(f" ⚠️ 无匹配政策(context={json.dumps(ctx, ensure_ascii=False)})⇒ 按权限边界决定:能自动就自动,否则问用户;"
186
+ "得到答复后用 --add 提议一条政策")
187
+ return 0 if hit else 1
188
+
189
+ if a.add:
190
+ if not a.decision:
191
+ print("❌ --add 需要 --decision", file=sys.stderr)
192
+ return 2
193
+ match = {}
194
+ for kv in a.match_pairs:
195
+ if "=" not in kv:
196
+ print(f"❌ --match-kv 需要 k=v:{kv}", file=sys.stderr)
197
+ return 2
198
+ k, v = kv.split("=", 1)
199
+ match[k.strip()] = [x.strip() for x in v.split(",")] if "," in v else v.strip()
200
+ if a.flow and "flow" not in match:
201
+ match["flow"] = a.flow
202
+ if a.failureKind and "failureKind" not in match:
203
+ match["failureKind"] = a.failureKind
204
+ if not match:
205
+ print("❌ 至少要有一个匹配条件(--flow 或 --match k=v)—— 无条件政策会吞掉所有判断", file=sys.stderr)
206
+ return 2
207
+ if a.decision == "discard" and (a.authorizer or "") not in ("用户", "user"):
208
+ print("❌ 作废类(discard)必须由用户授权(--authorizer 用户)", file=sys.stderr)
209
+ return 3
210
+ entry = {"id": new_id(), "match": match, "decision": a.decision,
211
+ "limits": ({"maxAttempts": a.max_attempts} if a.max_attempts else {}),
212
+ "authorizer": a.authorizer or "", "sourceCase": a.source_case or "",
213
+ "whyRef": a.why_ref or "",
214
+ "at": datetime.datetime.now().isoformat(timespec="seconds"), "enabled": True}
215
+ append(entry, path)
216
+ print(json.dumps(entry, ensure_ascii=False, indent=2) if a.json else
217
+ f" ✅ 已入库 {entry['id']}({a.decision}|match={json.dumps(match, ensure_ascii=False)})")
218
+ return 0
219
+
220
+ if a.retire:
221
+ target = next((p for p in policies if p["id"] == a.retire), None)
222
+ if not target:
223
+ print(f"❌ 找不到政策 {a.retire}", file=sys.stderr)
224
+ return 2
225
+ target["enabled"] = False
226
+ target["retiredAt"] = datetime.datetime.now().isoformat(timespec="seconds")
227
+ p = path or POLICY
228
+ keep = [x for x in policies if x["id"] != a.retire] + [target]
229
+ p.write_text("\n".join(json.dumps(x, ensure_ascii=False) for x in keep) + "\n", encoding="utf-8")
230
+ print(f" ✅ 已停用 {a.retire}(保留审计记录)")
231
+ return 0
232
+
233
+ ap.print_help()
234
+ return 0
235
+
236
+
237
+ if __name__ == "__main__":
238
+ sys.exit(main())