@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,558 @@
1
+ #!/usr/bin/env bash
2
+ # ════════════════════════════════════════════════════════════════════════════════
3
+ # scripts/ci-run.sh —— Argent **自建 CI runner**:一次跑完**固定判据集合** + 互斥 + 可追溯记录
4
+ #
5
+ # 为什么有它(对着 1.0 判据表的 G6「全门禁进 CI」):
6
+ # · 全量套件实测 169/0、约 312–389 秒;本机常有**多代理并发** ⇒ 并发跑套件会出现**瞬时假红**
7
+ # (`argent-datadir-parity` / `capability-pack` 各实测一次,**单跑即绿**)。
8
+ # 故 CI 的第一价值**不是"跑得更多"**,而是三件事:
9
+ # ① **串行化**:同一时刻只允许一份 CI(互斥锁)⇒ 从根上消掉并发假红;
10
+ # ② **固定判据集合**:15 条写死在本文件里,不靠"这次记得跑哪几条";
11
+ # ③ **可追溯记录**:每次运行落一份 JSON(每条判据的退出码/耗时/摘要行 + git HEAD/dirty +
12
+ # 机器与版本),失败时**点名**是哪条 ⇒ "这次为什么红"一眼可查。
13
+ #
14
+ # 用法:
15
+ # bash scripts/ci-run.sh # 全量判据(顺序 = 由快到慢;全量套件倒数第二,环境哨兵最后)
16
+ # bash scripts/ci-run.sh --list # 只列判据清单(名字/命令/期望输出/超时),**不加锁**
17
+ # bash scripts/ci-run.sh --only <a,b,...> # 只跑指定判据(开发期省时间;记录里标 partial=true)
18
+ # bash scripts/ci-run.sh --quiet # 只打印失败项与汇总
19
+ # 环境变量:ARGENT_CI_HOME=<目录>(缺省 ~/.dsh/argent/ci)|CI_TIMEOUT_<判据名大写> 覆盖该条超时
20
+ #
21
+ # 退出码:
22
+ # 0 全部判据通过
23
+ # 1 任一判据失败(末尾**点名**失败的判据 + 摘要行 + 日志段)
24
+ # 2 用法错误(未知参数 / 未知判据名 / 仓库不对)
25
+ # 75 已有 CI 在跑(TEMPFAIL:**不排队、不等待**,按 sysexits 惯例 75 = 临时故障)
26
+ #
27
+ # 锁语义(`~/.dsh/argent/ci/lock`,用 `mkdir` 的**原子性**做互斥;macOS 无 flock(1)):
28
+ # · 抢不到锁(锁在、pid 还活着)⇒ 立刻 exit 75 并打印「已有 CI 在跑 (pid …)」;
29
+ # · **陈旧锁**(锁在、pid 已不在)⇒ 打印接管提示后接管 —— 否则一次 kill -9 会让 CI **永久锁死**;
30
+ # ⛔ 这是唯一的"自动接管"路径,且只在 pid 确实不存在时触发;
31
+ # · 拿锁者只删**自己**的锁(EXIT/INT/TERM 清理),绝不删别人的。
32
+ #
33
+ # ⛔ 只读纪律(本脚本及其判据集合都不许违反):
34
+ # · 不 push、不部署、不连客户机、不改凭据/签名密钥、**不跑包管理器**(pnpm/npm install);
35
+ # · **不** git checkout / clean / stash —— git 状态**只只读采集**(HEAD / 分支 / dirty 清单),
36
+ # 因为本仓库常有**他人未提交的在制品**(一次 checkout 就毁了);
37
+ # · 唯一的本地写入只有两处:① 本脚本的记录与日志(`~/.dsh/argent/ci/**`);
38
+ # ② `sync-scripts --no-pull` 按**它自己的契约**把仓库 scripts/core/configs/skills/verticals/deploy 重新投递到
39
+ # 本机安装克隆(`~/.argent-dsh`)—— `--no-pull` 去掉唯一的网络动作;两处都不是生产、都不出网。
40
+ #
41
+ # 纪律(判据措辞即判据):每条判据除退出码外还要求输出**命中 `expect` 正则**(见 `criterion_expect`),
42
+ # 防"脚本跑了但什么都没查"的假绿(例:套件必须打印 `测试结果:N 通过 / 0 失败` 且 N≥1)。
43
+ # 改判据输出措辞 ⇒ 这里会转红并点名 —— 那是**有意的**:请同步改本文件的 expect。
44
+ # ════════════════════════════════════════════════════════════════════════════════
45
+ set -uo pipefail
46
+
47
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
48
+ REPO="${ARGENT_DSH_REPO:-$(cd "${SELF_DIR}/.." && pwd -P)}"
49
+
50
+ # ── 解释器判据(与 run-tests.sh 同源:venv 有就用 venv)──────────────────────────
51
+ PY="${ARGENT_PY:-$HOME/.dsh/argent/venv/bin/python3}"
52
+ [ -x "$PY" ] || PY="$(command -v python3 || echo python3)"
53
+ PYVER="$("$PY" -V 2>&1 | head -1 || true)"
54
+
55
+ # CI 自己的目录 = **数据目录** 下的 `ci/`。⛔ 数据目录**只认唯一判据**(CLI 包装
56
+ # `scripts/argent-datadir.py`),**不在 shell 里重写三级链**(不写 `$HOME/.dsh/argent`、
57
+ # 不读那两个实例级开关)—— 这条由 `scripts/tests/argent-datadir-parity.test.mjs`【4】守着,
58
+ # **2026-10-05 本 CI 首跑就被它抓到过**(第一版正是自写硬编码 + 读 ARGENT_DATA_DIR ⇒ 套件判红)。
59
+ DATA_DIR="$("$PY" "${REPO}/scripts/argent-datadir.py" 2>/dev/null || true)"
60
+ if [ -z "${DATA_DIR}" ]; then
61
+ echo "✗ 数据目录判据不可用(${REPO}/scripts/argent-datadir.py 取不到)⇒ 停住,不猜目录" >&2
62
+ echo " (锁/记录/日志都落在数据目录下,猜错目录等于把记录写到别处)" >&2
63
+ exit 2
64
+ fi
65
+ CI_HOME="${ARGENT_CI_HOME:-${DATA_DIR}/ci}"
66
+ LOCK="${CI_HOME}/lock"
67
+ LOGS="${CI_HOME}/logs"
68
+
69
+ usage() {
70
+ cat <<'EOF'
71
+ 用法:
72
+ bash scripts/ci-run.sh # 全量判据(由快到慢)
73
+ bash scripts/ci-run.sh --list # 列判据清单(不加锁)
74
+ bash scripts/ci-run.sh --only <a,b,...> # 只跑指定判据(记录标 partial)
75
+ bash scripts/ci-run.sh --quiet # 只打印失败项与汇总
76
+ 退出码:0 全绿 | 1 有判据红 | 2 用法错误 | 75 已有 CI 在跑(TEMPFAIL)
77
+ EOF
78
+ }
79
+
80
+ ARGV_STR="$*" # 原始参数(记录用;下面的解析会 shift 掉)
81
+
82
+ # ── 参数 ───────────────────────────────────────────────────────────────────────
83
+ ONLY=""; QUIET=0; DO_LIST=0
84
+ while [ $# -gt 0 ]; do
85
+ case "$1" in
86
+ --list) DO_LIST=1; shift ;;
87
+ --only)
88
+ [ $# -ge 2 ] || { echo "✗ --only 缺值(用法:--only <名字,名字>;--list 看名字)" >&2; exit 2; }
89
+ ONLY="${ONLY:+${ONLY},}$2"; shift 2 ;;
90
+ --only=*)
91
+ ONLY="${ONLY:+${ONLY},}${1#--only=}"; shift ;;
92
+ --quiet|-q) QUIET=1; shift ;;
93
+ -h|--help) usage; exit 0 ;;
94
+ *) echo "✗ 未知参数:$1" >&2; usage >&2; exit 2 ;;
95
+ esac
96
+ done
97
+
98
+ # ── 毫秒时钟(记录耗时用;bash 3.2 没有 EPOCHREALTIME,故按可用工具降级)───────────
99
+ MS_TOOL=""
100
+ if command -v perl >/dev/null 2>&1; then MS_TOOL=perl
101
+ elif command -v "${PY}" >/dev/null 2>&1; then MS_TOOL=python
102
+ else MS_TOOL=date; fi
103
+ now_ms() {
104
+ case "${MS_TOOL}" in
105
+ perl) perl -MTime::HiRes=time -e 'printf "%.0f\n", time*1000' ;;
106
+ python) "${PY}" -c 'import time;print(int(time.time()*1000))' ;;
107
+ *) echo "$(( $(date +%s) * 1000 ))" ;;
108
+ esac
109
+ }
110
+
111
+ # ── 进程树终止(超时用;macOS 没有 timeout(1))──────────────────────────────────
112
+ kill_tree() {
113
+ local p="$1" c
114
+ for c in $(pgrep -P "${p}" 2>/dev/null || true); do kill_tree "${c}"; done
115
+ kill -TERM "${p}" 2>/dev/null || true
116
+ return 0
117
+ }
118
+
119
+ # ── 判据集合(**唯一的清单**;顺序 = 执行顺序 = 由快到慢)─────────────────────────
120
+ # 设计口径:① 几秒的判据先跑(红了立刻看得见)② 全量套件倒数第二(最慢)
121
+ # ③ `test-env-safety` **最后**跑 —— 它比对的是本次 CI 开始时拍的快照 ⇒ 覆盖整个窗口。
122
+ CRITERIA="kernel-purity vertical-lint docs-drift host-matrix sample-redaction 1.0-criteria judgment-drift capability-pack pack-closure config-catalog role-mapping skill-classification production-home plugin-bootcheck sync-scripts suite test-env-safety"
123
+
124
+ criterion_title() {
125
+ case "$1" in
126
+ kernel-purity) printf '%s' '内核纯净度(L0/L1 不引宿主;scripts/ 未归类即红)' ;;
127
+ vertical-lint) printf '%s' '业务线 lint(命名空间/跨线引用/引擎业务词/manifest 自洽)' ;;
128
+ docs-drift) printf '%s' '文档漂移(--strict:可疑 0 条;含 R4 矩阵结构)' ;;
129
+ host-matrix) printf '%s' '宿主矩阵结构(--strict --matrix-only:列数/空标签/状态符号)' ;;
130
+ sample-redaction) printf '%s' '官网样例脱敏(--strict:禁用串 0 命中 + 键名与指标清单对齐)' ;;
131
+ # 1.0 判据表:§2.0(八面总览)与 §2.1–§2.8(判据行)的**计数自洽**(唯一真源 = 判据行)
132
+ 1.0-criteria) printf '%s' '1.0 判据计数自洽(§2.0 复算:40 条 + 逐面计数 + 卡点非空)' ;;
133
+ judgment-drift) printf '%s' '判断漂移(要求 R1=0:契约漂移 0 条)' ;;
134
+ capability-pack) printf '%s' '能力包自洽(--validate:版本/文件/流程/preset/提示词)' ;;
135
+ pack-closure) printf '%s' '能力包闭包(--strict:缺件 0 个)' ;;
136
+ # 2026-10-09:配置地基的四道门禁(双向对账 / 敏感不出门 / 必须项可解析 / 新增即生效)
137
+ config-catalog) printf '%s' '配置目录四道门禁(--strict:双向对账 + 敏感不出门 + 必须项可解析 + 新增即生效)' ;;
138
+ role-mapping) printf '%s' '角色-技能声明映射(声明的技能都真实存在)' ;;
139
+ skill-classification) printf '%s' '技能归类(外发/内部两边齐全,未归类 0)' ;;
140
+ production-home) printf '%s' '本机姿态(开发机档:launchctl 里 flow-trigger = 0)' ;;
141
+ plugin-bootcheck) printf '%s' '插件启动冒烟(隔离 home;preset 全挂载)' ;;
142
+ sync-scripts) printf '%s' '安装克隆对账(--no-pull:scripts/core/configs/skills/verticals/deploy 与仓库一致)' ;;
143
+ suite) printf '%s' '全量测试套件(scripts/run-tests.sh 的枚举面)' ;;
144
+ test-env-safety) printf '%s' '测试环境卫生哨兵(--verify:真实数据目录关键文件未变)' ;;
145
+ *) printf '%s' '(未知判据)' ;;
146
+ esac
147
+ }
148
+
149
+ criterion_cmd() {
150
+ case "$1" in
151
+ kernel-purity) printf '%s' 'node scripts/tests/kernel-purity.test.mjs' ;;
152
+ vertical-lint) printf '%s' 'node scripts/vertical-lint.mjs' ;;
153
+ docs-drift) printf '%s' 'node scripts/check-docs-drift.mjs --strict' ;;
154
+ host-matrix) printf '%s' 'node scripts/check-docs-drift.mjs --strict --matrix-only' ;;
155
+ sample-redaction) printf '%s' 'node scripts/check-sample-redaction.mjs --strict' ;;
156
+ 1.0-criteria) printf '%s' 'node scripts/check-1.0-criteria.mjs --strict' ;;
157
+ judgment-drift) printf '%s' 'node scripts/check-judgment-drift.mjs' ;;
158
+ capability-pack) printf '%s' "${PY} scripts/capability_pack.py --validate" ;;
159
+ pack-closure) printf '%s' 'node scripts/check-pack-closure.mjs --strict' ;;
160
+ config-catalog) printf '%s' 'node scripts/check-config-catalog.mjs --strict' ;;
161
+ role-mapping) printf '%s' 'node scripts/check-role-mapping.mjs' ;;
162
+ skill-classification) printf '%s' 'node scripts/check-skill-classification.mjs' ;;
163
+ production-home) printf '%s' 'node scripts/check-production-home.mjs --json' ;;
164
+ plugin-bootcheck) printf '%s' 'node scripts/plugin-bootcheck.mjs' ;;
165
+ sync-scripts) printf '%s' 'bash scripts/sync-scripts.sh --no-pull' ;;
166
+ suite) printf '%s' 'bash scripts/run-tests.sh' ;;
167
+ test-env-safety)
168
+ if [ -n "${ENV_SNAP:-}" ]; then printf '%s' "node scripts/check-test-env-safety.mjs --verify ${ENV_SNAP}"
169
+ else printf '%s' 'node scripts/check-test-env-safety.mjs --verify <运行期快照文件>'; fi ;;
170
+ *) printf '%s' '' ;;
171
+ esac
172
+ }
173
+
174
+ # 每条判据的**期望输出**(多行 = 全部必须命中;⚠️ 只用 POSIX ERE:BSD grep 不认 \s 这类简写)
175
+ criterion_expect() {
176
+ case "$1" in
177
+ kernel-purity) printf '%s\n' 'kernel-purity: [1-9][0-9]* 通过 / 0 失败' ;;
178
+ vertical-lint) printf '%s\n' '结果:✅ 通过' ;;
179
+ docs-drift) printf '%s\n' '可疑 0 条' ;;
180
+ host-matrix) printf '%s\n' '矩阵结构:0 处' '可疑 0 条' ;;
181
+ sample-redaction) printf '%s\n' '禁用串命中 0 处' '结构与键名一致' ;;
182
+ 1.0-criteria) printf '%s\n' '判据条数:40 条' '结论:✅ 一致' ;;
183
+ judgment-drift) printf '%s\n' '契约漂移 0 条' ;;
184
+ capability-pack) printf '%s\n' '每个能力包都自洽' ;;
185
+ pack-closure) printf '%s\n' '缺件 0 个' ;;
186
+ config-catalog) printf '%s\n' '四道门禁全绿' ;;
187
+ role-mapping) printf '%s\n' '已声明的技能都真实存在' ;;
188
+ skill-classification) printf '%s\n' '未归类 0' ;;
189
+ production-home) printf '%s\n' '"countOk":[[:space:]]*true' '"flowTriggers":[[:space:]]*0' ;;
190
+ plugin-bootcheck) printf '%s\n' 'DONE fail=0' '启动冒烟通过' ;;
191
+ sync-scripts) printf '%s\n' '已与仓库一致' ;;
192
+ suite) printf '%s\n' '测试结果:[1-9][0-9]* 通过 / 0 失败' ;;
193
+ test-env-safety) printf '%s\n' '关键文件未变' ;;
194
+ *) printf '%s\n' '' ;;
195
+ esac
196
+ }
197
+
198
+ # 摘要行抽取:优先取**最后一条**命中 summaryRe 的行;抽不到 ⇒ 退化为末条非空行(绝不空着)
199
+ criterion_summary_re() {
200
+ case "$1" in
201
+ kernel-purity) printf '%s' 'kernel-purity: ' ;;
202
+ vertical-lint) printf '%s' '结果:' ;;
203
+ docs-drift) printf '%s' '可疑 [0-9]+ 条' ;;
204
+ host-matrix) printf '%s' '矩阵结构:' ;;
205
+ sample-redaction) printf '%s' '结论:' ;;
206
+ 1.0-criteria) printf '%s' '结论:' ;;
207
+ judgment-drift) printf '%s' '结论:' ;;
208
+ capability-pack) printf '%s' '每个能力包都自洽' ;;
209
+ pack-closure) printf '%s' '闭包体检:' ;;
210
+ config-catalog) printf '%s' '结果:' ;;
211
+ role-mapping) printf '%s' '技能目录:' ;;
212
+ skill-classification) printf '%s' '结论:' ;;
213
+ production-home) printf '%s' '"flowTriggers":' ;;
214
+ plugin-bootcheck) printf '%s' '启动冒烟通过' ;;
215
+ sync-scripts) printf '%s' '已与仓库一致' ;;
216
+ suite) printf '%s' '测试结果:' ;;
217
+ test-env-safety) printf '%s' '结论:' ;;
218
+ *) printf '%s' '' ;;
219
+ esac
220
+ }
221
+
222
+ # 超时(秒):全量套件 312–389s 实测 ⇒ 给 30 分钟余量;可用 CI_TIMEOUT_<判据名大写> 覆盖
223
+ criterion_timeout() {
224
+ local n ov
225
+ n="$(printf '%s' "$1" | tr 'a-z-' 'A-Z_')"
226
+ ov="$(printenv "CI_TIMEOUT_${n}" 2>/dev/null || true)"
227
+ if [ -n "$ov" ]; then printf '%s' "$ov"; return 0; fi
228
+ case "$1" in
229
+ suite) printf '%s' 1800 ;;
230
+ sync-scripts|plugin-bootcheck) printf '%s' 600 ;;
231
+ production-home) printf '%s' 120 ;;
232
+ *) printf '%s' 300 ;;
233
+ esac
234
+ }
235
+
236
+ in_set() { case " ${CRITERIA} " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
237
+
238
+ # ── 由快到慢的执行顺序里筛出本次要跑的(保持 CRITERIA 的先后)─────────────────────
239
+ ONLY_NORM=""
240
+ if [ -n "${ONLY}" ]; then
241
+ ONLY_NORM="$(printf '%s' "${ONLY}" | tr ',' ' ')"
242
+ for n in ${ONLY_NORM}; do
243
+ in_set "${n}" || { echo "✗ 未知判据:${n}(用 --list 看清单)" >&2; exit 2; }
244
+ done
245
+ fi
246
+ SELECTED=""
247
+ for c in ${CRITERIA}; do
248
+ if [ -z "${ONLY_NORM}" ]; then SELECTED="${SELECTED}${SELECTED:+ }${c}"; continue; fi
249
+ case " ${ONLY_NORM} " in *" ${c} "*) SELECTED="${SELECTED}${SELECTED:+ }${c}" ;; esac
250
+ done
251
+ PARTIAL=0
252
+ [ "${SELECTED}" = "${CRITERIA}" ] || PARTIAL=1
253
+
254
+ TOTAL_ALL=0; for c in ${CRITERIA}; do TOTAL_ALL=$((TOTAL_ALL + 1)); done
255
+ TOTAL_N=0; for c in ${SELECTED}; do TOTAL_N=$((TOTAL_N + 1)); done
256
+
257
+ # ── --list:只列清单(不加锁、不建目录、不跑任何判据)────────────────────────────
258
+ if [ "${DO_LIST}" = 1 ]; then
259
+ echo "Argent 自建 CI —— 判据清单(${TOTAL_ALL} 条;执行顺序 = 由快到慢,全量套件倒数第二、环境哨兵最后)"
260
+ echo " 仓库:${REPO}"
261
+ echo " 记录:${CI_HOME}/<UTC 时间戳>.json(并更新 latest.json / latest-full.json)| 日志:${LOGS}/<ts>.log"
262
+ echo
263
+ i=0
264
+ for c in ${CRITERIA}; do
265
+ i=$((i + 1))
266
+ printf ' %2d. %-20s %s\n' "$i" "$c" "$(criterion_title "$c")"
267
+ printf ' 命令:%s\n' "$(criterion_cmd "$c")"
268
+ printf ' 期望:%s\n' "$(criterion_expect "$c" | tr '\n' ' ')"
269
+ printf ' 超时:%ss\n' "$(criterion_timeout "$c")"
270
+ done
271
+ echo
272
+ echo " 跑全部:bash scripts/ci-run.sh | 跑一条:bash scripts/ci-run.sh --only <名字>"
273
+ exit 0
274
+ fi
275
+
276
+ # ── 仓库自检(判据都要在仓库里跑)───────────────────────────────────────────────
277
+ if [ ! -f "${REPO}/scripts/run-tests.sh" ]; then
278
+ echo "✗ 仓库不对:${REPO} 里没有 scripts/run-tests.sh(用 ARGENT_DSH_REPO=<仓库路径> 指定)" >&2
279
+ exit 2
280
+ fi
281
+
282
+ # ── 遥测总闸(⛔ CI 绝不发遥测;与 run-tests.sh 同源)───────────────────────────
283
+ export ARGENT_TELEMETRY_OFF=1
284
+
285
+ # ── 互斥锁(mkdir 原子创建)────────────────────────────────────────────────────
286
+ LOCK_STALE=0
287
+ # ⚠️ 必须先建父目录:`mkdir <锁>` 在**父目录不存在**时失败原因是 **ENOENT**(不是 EEXIST)——
288
+ # 2026-10-05 首跑实测踩到:`~/.dsh/argent/ci` 尚不存在 ⇒ 第一版把 ENOENT 误判成"已有 CI 在跑"
289
+ # ⇒ **首次运行假 75**。故先 `mkdir -p`,且 mkdir 失败后必须分清"锁真的在"与"根本建不出来"。
290
+ mkdir -p "${CI_HOME}" "${LOGS}"
291
+ if ! mkdir "${LOCK}" 2>/dev/null; then
292
+ if [ ! -d "${LOCK}" ]; then
293
+ echo "✗ 建锁失败(不是锁竞争):${LOCK} —— 检查父目录是否存在且可写" >&2
294
+ exit 2
295
+ fi
296
+ OWNER_PID="$(cat "${LOCK}/pid" 2>/dev/null | tr -dc '0-9' || true)"
297
+ OWNER_AT="$(cat "${LOCK}/started" 2>/dev/null || true)"
298
+ if [ -n "${OWNER_PID}" ] && kill -0 "${OWNER_PID}" 2>/dev/null; then
299
+ echo "⛔ 已有 CI 在跑 (pid ${OWNER_PID},起于 ${OWNER_AT:-未知}) ⇒ 本次不排队,直接退出(exit 75)"
300
+ echo " 锁:${LOCK}(要立刻跑,请等它结束;⛔ 不要手删活锁)"
301
+ exit 75
302
+ fi
303
+ echo "⚠️ 发现陈旧锁(pid ${OWNER_PID:-未知} 已不在,起于 ${OWNER_AT:-未知})⇒ 接管(否则一次 kill -9 会永久锁死 CI)"
304
+ rm -rf "${LOCK}"
305
+ if ! mkdir "${LOCK}" 2>/dev/null; then
306
+ echo "⛔ 接管锁失败(可能刚好有并发进程抢到)⇒ exit 75" >&2
307
+ exit 75
308
+ fi
309
+ LOCK_STALE=1
310
+ fi
311
+ printf '%s\n' "$$" > "${LOCK}/pid"
312
+ date -u +%Y-%m-%dT%H:%M:%SZ > "${LOCK}/started"
313
+ printf '%s\n' "${REPO}" > "${LOCK}/repo"
314
+
315
+ LOCK_OWNED=1
316
+ cleanup() {
317
+ if [ "${LOCK_OWNED}" = 1 ] && [ -n "${LOCK}" ] && [ -f "${LOCK}/pid" ] && [ "$(cat "${LOCK}/pid" 2>/dev/null)" = "$$" ]; then
318
+ rm -rf "${LOCK}"
319
+ fi
320
+ [ -n "${TMP_CI:-}" ] && [ -d "${TMP_CI}" ] && rm -rf "${TMP_CI}"
321
+ return 0
322
+ }
323
+ trap cleanup EXIT INT TERM
324
+
325
+ TMP_CI="$(mktemp -d -t argent-ci)"
326
+ TS="$(date -u +%Y%m%dT%H%M%SZ)"
327
+ [ -e "${CI_HOME}/${TS}.json" ] && TS="${TS}-$$"
328
+ LOG="${LOGS}/${TS}.log"
329
+ REC="${CI_HOME}/${TS}.json"
330
+ LATEST="${CI_HOME}/latest.json"
331
+ LATEST_FULL="${CI_HOME}/latest-full.json"
332
+ TSV="${TMP_CI}/results.tsv"
333
+ : > "${TSV}"
334
+
335
+ START_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
336
+ START_MS="$(now_ms)"
337
+
338
+ # 日志:本脚本自己的话(同时进控制台与日志)
339
+ say() { printf '%s\n' "$*" | tee -a "${LOG}"; }
340
+
341
+ say "════════ Argent 自建 CI ════════"
342
+ say " 运行 ${TS}(UTC)|仓库 ${REPO}"
343
+ say " 判据 $( [ "${PARTIAL}" = 1 ] && printf '%s' "${TOTAL_N}/${TOTAL_ALL}(--only)" || printf '%s' "${TOTAL_ALL} 条(全量)" )"
344
+ say " 锁 ${LOCK}(pid $$)$([ "${LOCK_STALE}" = 1 ] && printf '%s' ' |⚠️ 本次是接管陈旧锁' || true)"
345
+ say " 记录 ${REC} | 日志 ${LOG}"
346
+
347
+ # ── 环境哨兵的快照:**在第一条判据之前**拍(这样最后那条 --verify 覆盖整个 CI 窗口)──
348
+ ENV_SNAP=""
349
+ case " ${SELECTED} " in
350
+ *" test-env-safety "*)
351
+ ENV_SNAP="${TMP_CI}/env-snapshot.json"
352
+ if node scripts/check-test-env-safety.mjs --snapshot "${ENV_SNAP}" >"${TMP_CI}/snapshot.out" 2>&1; then
353
+ say " 环境哨兵:已在跑任何判据前拍下真实数据目录快照(${ENV_SNAP})"
354
+ else
355
+ say " ⚠️ 环境哨兵快照拍失败(${ENV_SNAP}):$(tail -2 "${TMP_CI}/snapshot.out" | tr '\n' ' ')"
356
+ fi
357
+ ;;
358
+ esac
359
+
360
+ # 单行字段净化(TSV 安全)+ 摘要抽取
361
+ san() { printf '%s' "${1:-}" | tr '\t\n' ' ' | cut -c1-600; }
362
+ extract_summary() {
363
+ local f="$1" re="$2" line=""
364
+ if [ -n "${re}" ]; then
365
+ line="$(grep -E -- "${re}" "${f}" 2>/dev/null | tail -1 | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' || true)"
366
+ fi
367
+ if [ -z "${line}" ]; then
368
+ line="$(grep -v '^[[:space:]]*$' "${f}" 2>/dev/null | tail -1 | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' || true)"
369
+ fi
370
+ printf '%s' "${line}" | tr '\t' ' ' | cut -c1-400
371
+ }
372
+
373
+ PASS_N=0; FAIL_N=0
374
+ for id in ${SELECTED}; do
375
+ cmd="$(criterion_cmd "${id}")"
376
+ title="$(criterion_title "${id}")"
377
+ tmo="$(criterion_timeout "${id}")"
378
+ expects="$(criterion_expect "${id}")"
379
+ expects_flat="$(printf '%s' "${expects}" | tr '\n' ';' | sed -e 's/;;*$//' -e 's/;;*/; /g')"
380
+ out_file="${TMP_CI}/${id}.out"
381
+ tflag="${TMP_CI}/${id}.timedout"
382
+ : > "${out_file}"; rm -f "${tflag}"
383
+
384
+ started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
385
+ s_ms="$(now_ms)"
386
+ ( cd "${REPO}" && bash -c "${cmd}" ) >"${out_file}" 2>&1 &
387
+ cpid=$!
388
+ ( sleep "${tmo}"; : > "${tflag}"; kill_tree "${cpid}" ) >/dev/null 2>&1 &
389
+ wpid=$!
390
+ wait "${cpid}"; rc=$?
391
+ kill_tree "${wpid}"
392
+ wait "${wpid}" 2>/dev/null || true
393
+ e_ms="$(now_ms)"
394
+ finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
395
+ dur=$((e_ms - s_ms))
396
+ [ "${dur}" -lt 0 ] && dur=0
397
+ dur_s="$(awk "BEGIN{printf \"%.1f\", ${dur}/1000}")"
398
+
399
+ timed_out=0; [ -f "${tflag}" ] && timed_out=1
400
+ expect_ok=1; miss_pat=""
401
+ if [ "${timed_out}" = 1 ]; then
402
+ status="fail"; why="(超时 ${tmo}s 未结束,已尽力终止进程树)"
403
+ else
404
+ while IFS= read -r pat; do
405
+ [ -n "${pat}" ] || continue
406
+ if ! grep -Eq -- "${pat}" "${out_file}"; then expect_ok=0; miss_pat="${pat}"; break; fi
407
+ done < <(printf '%s\n' "${expects}")
408
+ if [ "${rc}" -ne 0 ]; then
409
+ status="fail"; why="(exit ${rc})"
410
+ elif [ "${expect_ok}" != 1 ]; then
411
+ # 退出码 0 但输出没命中期望 ⇒ 判红:防"脚本跑了但什么都没查"的假绿
412
+ status="fail"; why="(输出未命中期望判据:${miss_pat})"
413
+ else
414
+ status="pass"; why=""
415
+ fi
416
+ fi
417
+ summary="$(extract_summary "${out_file}" "$(criterion_summary_re "${id}")")"
418
+
419
+ # 全文进日志(按判据分段,可 grep)
420
+ {
421
+ printf '\n──────── [%s] %s\n' "${id}" "${title}"
422
+ printf ' 命令:%s\n 期望:%s\n 开始:%s | 超时:%ss\n' "${cmd}" "${expects_flat:-(无)}" "${started_at}" "${tmo}"
423
+ cat "${out_file}"
424
+ printf '──────── [%s] 结束 rc=%s 状态=%s 耗时=%sms\n' "${id}" "${rc}" "${status}" "${dur}"
425
+ } >> "${LOG}"
426
+
427
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
428
+ "${id}" "$(san "${title}")" "${status}" "${rc}" "${timed_out}" "${expect_ok}" "${dur}" \
429
+ "${started_at}" "${finished_at}" "$(san "${cmd}")" "$(san "${expects_flat}")" "$(san "${summary}")" >> "${TSV}"
430
+
431
+ if [ "${status}" = "pass" ]; then
432
+ PASS_N=$((PASS_N + 1))
433
+ [ "${QUIET}" = 1 ] || printf ' ✅ %-20s %6ss %s\n' "${id}" "${dur_s}" "${summary}"
434
+ else
435
+ FAIL_N=$((FAIL_N + 1))
436
+ printf ' ❌ %-20s %6ss %s %s\n' "${id}" "${dur_s}" "${summary}" "${why}"
437
+ fi
438
+ done
439
+
440
+ END_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
441
+ END_MS="$(now_ms)"
442
+ ELAPSED_MS=$((END_MS - START_MS))
443
+ [ "${ELAPSED_MS}" -lt 0 ] && ELAPSED_MS=0
444
+ ELAPSED_S="$(awk "BEGIN{printf \"%.1f\", ${ELAPSED_MS}/1000}")"
445
+ [ "${FAIL_N}" -eq 0 ] && RESULT="pass" || RESULT="fail"
446
+ [ "${FAIL_N}" -eq 0 ] && EXIT_CODE=0 || EXIT_CODE=1
447
+
448
+ # ── 记录(git 状态**只读**采集)────────────────────────────────────────────────
449
+ CI_HEAD="$(git -C "${REPO}" rev-parse --short=12 HEAD 2>/dev/null || true)"
450
+ CI_BRANCH="$(git -C "${REPO}" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
451
+ DIRTY_LIST="$(git -C "${REPO}" status --porcelain 2>/dev/null | head -20 || true)"
452
+ DIRTY_COUNT="$(git -C "${REPO}" status --porcelain 2>/dev/null | wc -l | tr -d ' ' || true)"
453
+ [ -n "${DIRTY_COUNT}" ] || DIRTY_COUNT=0
454
+ [ "${DIRTY_COUNT}" -gt 0 ] 2>/dev/null && CI_DIRTY=1 || CI_DIRTY=0
455
+
456
+ export CI_TS="${TS}" CI_PARTIAL="${PARTIAL}" CI_ONLY="${ONLY_NORM// /,}" CI_RESULT="${RESULT}" CI_EXIT="${EXIT_CODE}"
457
+ export CI_STARTED="${START_ISO}" CI_FINISHED="${END_ISO}" CI_DURATION_MS="${ELAPSED_MS}"
458
+ export CI_REPO="${REPO}" CI_HEAD="${CI_HEAD}" CI_BRANCH="${CI_BRANCH}" CI_DIRTY="${CI_DIRTY}"
459
+ export CI_DIRTY_COUNT="${DIRTY_COUNT}" CI_DIRTY_FILES="${DIRTY_LIST}"
460
+ export CI_HOST="$(hostname -s 2>/dev/null || hostname)" CI_PLATFORM="$(uname -s)" CI_RELEASE="$(uname -r)" CI_ARCH="$(uname -m)"
461
+ export CI_MACOS="$(sw_vers -productVersion 2>/dev/null || true)"
462
+ export CI_NODE="$(node -v 2>/dev/null || true)" CI_DSH="$(dsh --version 2>/dev/null | head -1 || true)"
463
+ export CI_PY="${PY}" CI_PYVER="${PYVER}" CI_PATH="${PATH}"
464
+ export CI_LOCK="${LOCK}" CI_PID="$$" CI_STALE="${LOCK_STALE}"
465
+ export CI_ALL_COUNT="${TOTAL_ALL}" CI_LOG="${LOG}" CI_HOME="${CI_HOME}"
466
+ export CI_DATA_DIR="${DATA_DIR}"
467
+ export CI_ARGV="${ARGV_STR}"
468
+
469
+ node - "${TSV}" "${REC}" "${LATEST}" "${LATEST_FULL}" <<'NODE' || echo "⚠️ 记录写入失败(JSON 组装出错)" >&2
470
+ const fs = require('node:fs')
471
+ const [tsvPath, recPath, latestPath, latestFullPath] = process.argv.slice(2)
472
+ const E = process.env
473
+ const rows = fs.readFileSync(tsvPath, 'utf8').split('\n').filter((l) => l.trim() !== '')
474
+ const criteria = rows.map((line) => {
475
+ const c = line.split('\t')
476
+ return {
477
+ name: c[0] || '',
478
+ title: c[1] || '',
479
+ status: c[2] || '',
480
+ exitCode: Number(c[3] || 0),
481
+ timedOut: c[4] === '1',
482
+ expectOk: c[5] === '1',
483
+ durationMs: Number(c[6] || 0),
484
+ startedAt: c[7] || '',
485
+ finishedAt: c[8] || '',
486
+ cmd: c[9] || '',
487
+ expect: (c[10] || '') === '' ? [] : c[10].split('; '),
488
+ summary: c[11] || '',
489
+ }
490
+ })
491
+ const passCount = criteria.filter((c) => c.status === 'pass').length
492
+ const rec = {
493
+ schema: 'argent-ci/1',
494
+ runId: E.CI_TS || '',
495
+ partial: E.CI_PARTIAL === '1',
496
+ only: (E.CI_ONLY || '') === '' ? [] : E.CI_ONLY.split(','),
497
+ result: E.CI_RESULT || '',
498
+ exitCode: Number(E.CI_EXIT || 0),
499
+ startedAt: E.CI_STARTED || '',
500
+ finishedAt: E.CI_FINISHED || '',
501
+ durationMs: Number(E.CI_DURATION_MS || 0),
502
+ repo: {
503
+ path: E.CI_REPO || '',
504
+ head: E.CI_HEAD || '',
505
+ branch: E.CI_BRANCH || '',
506
+ dirty: E.CI_DIRTY === '1',
507
+ dirtyCount: Number(E.CI_DIRTY_COUNT || 0),
508
+ dirtyFiles: (E.CI_DIRTY_FILES || '') === '' ? [] : E.CI_DIRTY_FILES.split('\n'),
509
+ },
510
+ machine: {
511
+ hostname: E.CI_HOST || '', platform: E.CI_PLATFORM || '', release: E.CI_RELEASE || '',
512
+ arch: E.CI_ARCH || '', macos: E.CI_MACOS || '',
513
+ },
514
+ runtime: { node: E.CI_NODE || '', dsh: E.CI_DSH || '', python: E.CI_PY || '', pythonVersion: E.CI_PYVER || '', path: E.CI_PATH || '' },
515
+ lock: { path: E.CI_LOCK || '', pid: Number(E.CI_PID || 0), staleTakenOver: E.CI_STALE === '1' },
516
+ counts: { all: Number(E.CI_ALL_COUNT || 0), selected: criteria.length, pass: passCount, fail: criteria.length - passCount },
517
+ criteria, // ⚠️ 2026-10-05 修:第一版漏了这个键 ⇒ 记录里只有 counts、**没有逐条判据**(正是要交付的东西)。
518
+ // 自证口径:写完必须 `node -e 'console.log(r.criteria.length)'` 回读一次,不是看一眼 counts 就算数。
519
+ env: { dataDir: E.CI_DATA_DIR || '', dataDirSource: 'scripts/argent-datadir.py(唯一判据)' },
520
+ log: E.CI_LOG || '',
521
+ argv: (E.CI_ARGV || '') === '' ? [] : E.CI_ARGV.split('\n'),
522
+ notes: [
523
+ '只读校验:不 push / 不部署 / 不连客户机 / 不改凭据 / 不跑包管理器 / 不做 git checkout|clean(git 状态只读采集)。',
524
+ '本地写入仅两处:本记录与日志(' + (E.CI_HOME || '') + '/**);sync-scripts --no-pull 按脚本自身契约把仓库重新投递到本机安装克隆(无网络动作)。',
525
+ '判据措辞即判据:每条 criterion.expect 是必须命中的输出正则;措辞变更会让该条转红(防"脚本跑了但什么都没查"的假绿)。',
526
+ ],
527
+ }
528
+ const text = JSON.stringify(rec, null, 2) + '\n'
529
+ fs.writeFileSync(recPath, text)
530
+ fs.writeFileSync(latestPath, text)
531
+ if (rec.partial !== true) fs.writeFileSync(latestFullPath, text)
532
+ NODE
533
+
534
+ # ── 汇总 ───────────────────────────────────────────────────────────────────────
535
+ say ""
536
+ say "════════════════════════════════════════════════════════════════"
537
+ if [ "${FAIL_N}" -eq 0 ]; then
538
+ say " CI 结果:${PASS_N}/${TOTAL_N} 通过 ✓(耗时 ${ELAPSED_S}s)"
539
+ else
540
+ say " CI 结果:${PASS_N}/${TOTAL_N} 通过(耗时 ${ELAPSED_S}s)⇒ 退出码 1"
541
+ say " ❌ 失败的判据(${FAIL_N}):"
542
+ while IFS="$(printf '\t')" read -r c_name c_title c_status c_rc c_to c_eo c_dur c_s c_f c_cmd c_exp c_sum; do
543
+ [ "${c_status}" = "pass" ] && continue
544
+ [ -n "${c_name}" ] || continue
545
+ say " · ${c_name}(${c_title})exit=${c_rc}$([ "${c_to}" = 1 ] && printf '%s' ' 超时' || true)"
546
+ say " 摘要:${c_sum}"
547
+ say " 全文:${LOG} 的 [${c_name}] 段"
548
+ done < "${TSV}"
549
+ fi
550
+ if [ "${PARTIAL}" = 1 ]; then
551
+ say " ⚠️ 本次只跑了 ${TOTAL_N}/${TOTAL_ALL} 条(--only)⇒ 这是**部分结果**:"
552
+ say " latest.json 已更新为部分结果(partial=true);**全量门禁状态看 latest-full.json**"
553
+ fi
554
+ say " 记录:${REC}"
555
+ say " ${LATEST}$([ "${PARTIAL}" = 1 ] && printf '%s' '(本次已更新,partial=true)' || true)"
556
+ say " 日志:${LOG}"
557
+ say "════════════════════════════════════════════════════════════════"
558
+ exit "${EXIT_CODE}"