@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,1046 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * argent-upgrade.mjs — Argent 自举式完整升级器(v0.9 产品语义)
4
+ *
5
+ * 「检查更新 = 完整升级」:无论当前插件版本多旧,升级逻辑本体随 git 分发、
6
+ * 从磁盘读取、不依赖运行中的插件代码——先 pull 再执行最新逻辑,杜绝半升级。
7
+ *
8
+ * 用法:
9
+ * node argent-upgrade.mjs check # 检测远端版本(git fetch + origin/main VERSION)
10
+ * node argent-upgrade.mjs run # 完整升级(pull + preset/skills + patch 组合校正 + 插件同步)
11
+ * node argent-upgrade.mjs run --restart # 同上,完成后自动重启 dsh web(macOS launchd 场景)
12
+ * node argent-upgrade.mjs run --json # JSON 输出(core /api/argent-state/update 端点调用)
13
+ * node argent-upgrade.mjs migrate-deps # 存量迁移:profile 的遗留 `file:` 依赖 ⇒ npm 名(**默认干跑**)
14
+ * node argent-upgrade.mjs migrate-deps --apply # 真的写(写前备份 + sha256 + 审计 + 读回验收;幂等)
15
+ * node argent-upgrade.mjs migrate-deps --apply --allow-package-manager # 显式允许跑包管理器(⛔ 默认不跑)
16
+ *
17
+ * 目标组合(v0.9 产品定义,随版本演进修改此处):
18
+ * 业务型(有 kanban/gateway/notify 任一或 gateway.json)→ core/skin/kanban/notify/drive/gateway
19
+ * 对话型(仅 core/skin) → core/skin
20
+ * v0.8 起摘除的插件(组合外出现即移除行):files / instances / instances-local / tool-instances / packs / ml
21
+ *
22
+ * ## 技能同步**只发外发集**(2026-10-04;D5)
23
+ * 第 3 步只同步 **`classify()` 判定的 external**(= 所有 `verticals/<id>/manifest.json#skills` 的并集),
24
+ * **内部角色技能(`skills/internal.json`)不外发**;宿主技能根里**已有的**内部/旧技能**只报告不删**
25
+ * (客户机现状 = 5 外发 + 6 内部 + 旧 hermes 期技能 ⇒ 报告里列"多出"清单,删不删由人决定)。
26
+ * 判据**不在本文件**:唯一实现是 `scripts/check-skill-classification.mjs#classify()`。
27
+ */
28
+
29
+ import { homedir } from 'node:os'
30
+ import { basename, dirname, join, resolve } from 'node:path'
31
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, cpSync, readdirSync, copyFileSync } from 'node:fs'
32
+ import { execFileSync, spawn } from 'node:child_process'
33
+ import { fileURLToPath, pathToFileURL } from 'node:url'
34
+ import { isDirectRun } from './lib/is-direct-run.mjs'
35
+ import {
36
+ applyDepsMigration, planDepsMigration, realVersionOf, knownPluginId, renderPlan,
37
+ } from './lib/deps-migrate.mjs'
38
+ import { retirePlugins as retirePluginsImpl } from './lib/profile-retire.mjs'
39
+ import { BUNDLE_LAYER_PLUGINS, bundleLayerRows, pluginRowName, retiredRowIds, targetCombos, compositionRows } from './lib/plugin-lifecycle.mjs'
40
+ import { PLUGIN_IDS, canonicalPluginName, migrateBundleNames, migrateDependencyKeys, staleDepsStillReferenced } from './lib/plugin-packages.mjs'
41
+ import { pruneStalePresets } from './lib/preset-sync.mjs'
42
+ import { MECHANISM, detectPresetMechanism, mechanismNote } from './lib/preset-mechanism.mjs'
43
+ import { installPresets, resolveInstalledSelection, applyPresetSelection, materializeBundleDir, checkPresetSelectionConsistency, resolveBundleDir } from './lib/preset-install.mjs'
44
+ import { resolveDshScopeDir } from './lib/dsh-scope.mjs'
45
+ import { composeFix, composeNew, migratePluginRowNames } from './lib/patch-compose.mjs'
46
+ // 技能归类判据:**唯一实现在门禁里**(本文件不写第二份名单、不自己读 manifest —— 照抄即漂移)。
47
+ // 内容摘要(`skillDigest`)同样复用外发安装器的实现:两边对"内容一致"必须是同一把尺子,
48
+ // 否则升级器与安装器会互相判对方"不一致"而反复覆盖。
49
+ import { classify, SKILL_FILE } from './check-skill-classification.mjs'
50
+ import { skillDigest } from './skill-install.mjs'
51
+
52
+ const HOME = homedir()
53
+ // 克隆目录:**接受两个名字**(2026-09-30 实测发现 install-argent.mjs 用 ARGENT_DISH_HOME,
54
+ // 本文件此前只用 ARGENT_HOME ⇒ 设了一个不影响另一个,测试与自定义安装路径都会误判为「~/.argent-dsh 脏」)。
55
+ const ARGENT_HOME = process.env.ARGENT_DISH_HOME || process.env.ARGENT_HOME || join(HOME, '.argent-dsh')
56
+ const DSH_HOME = process.env.DSH_HOME || join(HOME, '.dsh')
57
+ const PROFILE = process.env.DSH_PROFILE || 'web'
58
+ const PROFILE_DIR = join(DSH_HOME, 'profiles', PROFILE)
59
+ const PROFILE_NM = join(PROFILE_DIR, 'node_modules')
60
+ const PRESET_ROOT = join(DSH_HOME, '.agent-presets')
61
+ // `migrate-deps` 取插件真实版本时的**两个候选落点**(与安装器的版本体检同口径:克隆 → 本文件所在的树)
62
+ const REPO_FALLBACK = resolve(dirname(fileURLToPath(import.meta.url)), '..')
63
+ const SKILLS_DIR = join(DSH_HOME, 'skills')
64
+ const PATCH_PATH = join(PROFILE_DIR, 'cordis.patch.yml')
65
+
66
+ const args = process.argv.slice(2)
67
+ const MODE = args[0] || 'help'
68
+ const FLAG_JSON = args.includes('--json')
69
+ const FLAG_RESTART = args.includes('--restart')
70
+ /**
71
+ * ⛔ **显式**才允许升级器替用户修 profile 组合(= `profile-doctor --fix`)。
72
+ * 2026-10-03 事故:`run` 的第 7.5 步**无条件**带 `--fix` ⇒ 每次升级都会在 **web profile**
73
+ * (`~/.dsh/AGENTS.md` 明令避免跑 pnpm 的那棵树)上跑一次 `dsh plugin install`(pnpm install),
74
+ * 并把 5 条 `file:` 依赖从已消失的临时目录**静默**重指到仓库。
75
+ * 现在:默认**只做只读体检**(门禁照旧拦自动重启),修组合/动包管理器必须显式开这个开关。
76
+ */
77
+ const FLAG_PROFILE_FIX = args.includes('--with-profile-fix')
78
+ /**
79
+ * `migrate-deps` 的开关(0.6.0 W8)。
80
+ * ⛔ **默认干跑**:不带 `--apply` 只打印计划;⛔ **默认不跑包管理器**:要跑必须 `--allow-package-manager`
81
+ * (AGENTS 红线:在 web profile 上裸跑 `pnpm install` 可能把 profile 弄坏 —— 2026-10-03 事故)。
82
+ */
83
+ const FLAG_APPLY = args.includes('--apply')
84
+ const FLAG_ALLOW_PM = args.includes('--allow-package-manager')
85
+ const FLAG_DATA_DIR = (() => { const i = args.indexOf('--data-dir'); return i > -1 ? args[i + 1] || '' : '' })()
86
+ /** `--repo <dir>`:`migrate-deps` 读插件**真实版本**的克隆根(夹具/多克隆机器用;缺省 `ARGENT_HOME`→本仓)。 */
87
+ const FLAG_REPO = (() => { const i = args.indexOf('--repo'); return i > -1 ? args[i + 1] || '' : '' })()
88
+ const RESTART_LABEL = 'com.whyshu.dsh-web'
89
+
90
+ // 组合目标来自 scripts/plugin-composition.json(**唯一真源**,客户机预检也读它):
91
+ // patch 行 + bundle 层插件都要装进 profile;bundle 层那部分不进 patch 文件(见 patch-compose)。
92
+ const TARGET_COMBO = Object.fromEntries(
93
+ Object.entries(targetCombos()).map(([type, c]) => [type, [...c.patch, ...c.bundles]]),
94
+ )
95
+ const RETIRED = retiredRowIds()
96
+ const CRITICAL_SCRIPTS = ['invoice_runner.py', 'invoice_stageb.py', 'poll-invoice.sh', 'poll-fulfillment.sh', 'daily-report.mjs', 'init-ml.sh', 'orchestrator.py', 'fulfillment.js']
97
+
98
+ const out = (obj) => { console.log(FLAG_JSON ? JSON.stringify(obj, null, 2) : formatHuman(obj)) }
99
+ function formatHuman(o) {
100
+ if (o.error) return `✗ ${o.error}`
101
+ const ls = []
102
+ if (o.current) ls.push(`当前版本 ${o.current}`)
103
+ if (o.remote) ls.push(`远端版本 ${o.remote}`)
104
+ if (o.updateAvailable === true) ls.push('→ 有可用更新')
105
+ if (o.updateAvailable === false) ls.push('→ 已是最新')
106
+ if (o.comboType) ls.push(`实例类型: ${o.comboType}`)
107
+ if (o.targetCombo) ls.push(`目标组合: ${o.targetCombo.join(', ')}`)
108
+ ;(o.steps || []).forEach((s) => ls.push(
109
+ s.ok === false ? ` ✗ ${s.text}` : s.warn ? ` ⚠ ${s.text}` : ` ✓ ${s.text}`,
110
+ ))
111
+ if (o.profileFix) {
112
+ ls.push(o.profileFix.executed
113
+ ? ` 🔧 profile 组合修复: 已执行(--with-profile-fix 显式开启;包管理器=${
114
+ o.profileFix.plan && o.profileFix.plan.runsPackageManager ? '已跑' : '未跑(无依赖需要重指)'})`
115
+ : ' ⏭ profile 组合修复: 已跳过(默认只做只读体检;要执行须显式 --with-profile-fix)')
116
+ }
117
+ if (o.restartRequired) ls.push(`⚠ 需要重启 dsh web 生效${o.restartHint ? `(${o.restartHint})` : ''}`)
118
+ return ls.join('\n')
119
+ }
120
+
121
+ function runCmd(cmd, argArr, opts = {}) {
122
+ return execFileSync(cmd, argArr, { encoding: 'utf8', timeout: opts.timeout || 120000 })
123
+ }
124
+ function runCmdSafe(cmd, argArr, opts = {}) {
125
+ // `env` 可注入:doctor 等子进程**按 `DSH_HOME` 解析 home**(它们没有 `--dsh-home` 开关)⇒
126
+ // 必须把本脚本解析出来的 home 显式传下去,否则子进程会退回真实 `~/.dsh`(数据外泄,实测同类事故)。
127
+ const options = { encoding: 'utf8', timeout: opts.timeout || 120000 }
128
+ if (opts.env) options.env = opts.env
129
+ try { return { ok: true, stdout: execFileSync(cmd, argArr, options) } }
130
+ catch (e) { return { ok: false, stdout: e.stdout || '', stderr: e.stderr || '', message: String(e.message).split('\n')[0] } }
131
+ }
132
+
133
+ function readVersion(dir) {
134
+ try { return readFileSync(join(dir, 'VERSION'), 'utf8').trim() }
135
+ catch { return null }
136
+ }
137
+
138
+ /** profile-doctor `--json` 报告(拿不到 / 解析失败 ⇒ null,调用方退回文本判据)。 */
139
+ function parseDoctorJson(text) {
140
+ const s = String(text || '')
141
+ const i = s.indexOf('{')
142
+ if (i < 0) return null
143
+ try { return JSON.parse(s.slice(i)) } catch { return null }
144
+ }
145
+
146
+ /**
147
+ * `--with-profile-fix` 的**执行前声明**(⛔ 不得静默):把 doctor 的修复计划翻成人话,
148
+ * 逐条说明它将改哪些 `file:` 依赖、摘/补哪些 patch 行、**是否跑包管理器**。
149
+ * 输出走 stderr(JSON 模式下 stdout 必须保持纯 JSON)。
150
+ */
151
+ function describeProfileFix(plan, profile) {
152
+ const ls = ['[upgrade] ⚠ 即将执行 profile-doctor --fix(--with-profile-fix 已显式开启)—— 改动前置声明:']
153
+ if (!plan) {
154
+ ls.push(' · 预检未给出计划(doctor --json 不可用)—— 仍会执行 --fix,请自行核对 doctor 输出')
155
+ return ls
156
+ }
157
+ const deps = plan.depsToRepoint || []
158
+ ls.push(deps.length
159
+ ? ` · 将重指 ${deps.length} 条 file: 依赖:${deps.map((d) => `${d.name} → ${d.to}`).join(';')}`
160
+ : ' · 不改任何 file: 依赖(依赖路径均已规范)')
161
+ ls.push(` · 将摘除退役/重复 patch 行:${plan.patchRowsToDrop || 0} 行;补齐必需行:${(plan.requiredRowsToAdd || []).join(', ') || '无'}`)
162
+ if (plan.runsPackageManager) {
163
+ ls.push(` · ⛔ **将运行包管理器**:${plan.packageManagerCommand || `dsh plugin --profile ${profile} install`}`)
164
+ ls.push(' (= pnpm install;web profile 上是 `~/.dsh/AGENTS.md` 明令避免的动作,仅因你显式开了开关才做)')
165
+ } else {
166
+ ls.push(' · 不运行包管理器(没有任何依赖需要重指 ⇒ 不会触发 pnpm install)')
167
+ }
168
+ ls.push(' (反悔:Ctrl-C 中断即可;改前 doctor 会把 package.json / cordis.patch.yml 备份到 ~/.dsh/argent/backup/)')
169
+ return ls
170
+ }
171
+
172
+ // ── 实例类型判定 + 目标组合 ───────────────────────────────
173
+ function detectCombo() {
174
+ let patchText = ''
175
+ try { patchText = readFileSync(PATCH_PATH, 'utf8') } catch { /* 无 patch */ }
176
+ const gwJson = join(DSH_HOME, 'argent', 'gateway.json')
177
+ const business =
178
+ /argent-(kanban|notify|gateway)/.test(patchText) ||
179
+ existsSync(gwJson)
180
+ return business ? 'business' : 'chat'
181
+ }
182
+
183
+ /**
184
+ * 写前备份(**唯一实现**;未改动/备份失败都不改客户文件 —— 与安装器 `backupPatchFile` 同纪律)。
185
+ *
186
+ * 由来(2026-10-13):本文件此前只在 `ensureBundles`/`retirePlugins` 里**各自内联**了一份
187
+ * `copyFileSync(...)` 备份 ⇒ 新增写入点时极易漏备份(W9 的行名/依赖键/bundles 迁移全是新写入点)。
188
+ * @returns {{path: string} | null} null = 备份失败 ⇒ 调用方**必须**放弃本次改写
189
+ */
190
+ function backupFile(target, _content, why) {
191
+ try {
192
+ const dir = join(DSH_HOME, 'argent', 'backup')
193
+ mkdirSync(dir, { recursive: true })
194
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
195
+ const base = join(dir, `${basename(target)}.${stamp}`)
196
+ let dst = base
197
+ for (let i = 2; existsSync(dst); i += 1) dst = `${base}.${i}` // 同秒冲突 ⇒ 加序号(⛔ 不覆盖上一份)
198
+ copyFileSync(target, dst)
199
+ const rollback = `cp ${JSON.stringify(dst)} ${JSON.stringify(target)}`
200
+ writeFileSync(`${dst}.why.txt`, `${why}\n回滚:${rollback}\n`, { mode: 0o600 })
201
+ console.log(` ↳ 写前备份:${dst}\n ↳ 回滚:${rollback}`)
202
+ return { path: dst, why, rollback }
203
+ } catch (e) {
204
+ console.error(`[upgrade] ⛔ 写前备份失败(${e.message})⇒ 本次**不改** ${target}`)
205
+ return null
206
+ }
207
+ }
208
+
209
+ // ── 落位目录迁移(**第四处** profile 面向字符串:`node_modules/<名>`)──────
210
+ /**
211
+ * 已知插件的裸名落位(`node_modules/argent-flow`)⇒ 包名落位(`node_modules/@whyshu/argent-flow`)。
212
+ *
213
+ * 为什么必须与**行名迁移**同时做(2026-10-13):组合行的 `name:` 一旦变成包名,dsh 就按那个名字
214
+ * 在 profile 里解析 —— 落位还叫裸名 ⇒ **那一行解析不到**(⛔ 装完不报错,表现为插件静默不加载)。
215
+ * 反过来,只迁落位不迁行名也一样坏。所以两者是**同一个原子动作**。
216
+ * 判据刻意保守:
217
+ * · 只碰**本仓已知插件**(`plugin-packages.mjs` 名单),未知/别人的包一律不动;
218
+ * · 两处都在 ⇒ 删裸名那份(同一个插件 ⛔ 不许有两份);
219
+ * · 只有裸名 ⇒ `cp + rm`(跨设备/软链场景下比 rename 稳)。
220
+ */
221
+ function migrateBareLandings() {
222
+ const moved = []
223
+ const pruned = []
224
+ for (const id of PLUGIN_IDS) {
225
+ const canonical = canonicalPluginName(id)
226
+ if (canonical === id) continue
227
+ const bare = join(PROFILE_NM, id)
228
+ if (!existsSync(bare)) continue
229
+ const dest = join(PROFILE_NM, ...canonical.split('/'))
230
+ if (existsSync(dest)) { rmSync(bare, { recursive: true, force: true }); pruned.push(id); continue }
231
+ mkdirSync(dirname(dest), { recursive: true })
232
+ cpSync(bare, dest, { recursive: true })
233
+ rmSync(bare, { recursive: true, force: true })
234
+ moved.push(id)
235
+ }
236
+ return { moved, pruned }
237
+ }
238
+
239
+ // ── patch 组合校正:只删组合外 argent 行/补缺失行,保留注释与官方行 ──
240
+ /**
241
+ * 该插件在 profile 里**有没有落位**。
242
+ *
243
+ * ⚠️ W9(2026-10-13):落位名是**包名**(`node_modules/@whyshu/argent-*`),⛔ 不是目录名。
244
+ * 只认裸名会把"已 scoped 落位"的插件判成**悬挂** ⇒ 升级器**把活着的行摘掉**(⛔ 且不报错)。
245
+ * ⇒ 两个名字都找(`plugin-packages.mjs#canonicalPluginName` 是搬迁真源)。
246
+ */
247
+ function landedInProfile(ref) {
248
+ for (const n of [String(ref), canonicalPluginName(ref)]) {
249
+ if (!n) continue
250
+ if (existsSync(join(PROFILE_NM, ...n.split('/')))) return true
251
+ }
252
+ return false
253
+ }
254
+
255
+ /** 悬挂行:包没装在 profile 里,规范根里也没有源码 ⇒ 这行永远加载失败,摘掉。 */
256
+ function danglingRow(plugin) {
257
+ if (landedInProfile(plugin)) return false
258
+ for (const root of [join(ARGENT_HOME, 'plugins'), join(DSH_HOME, 'argent', 'packs-cache')]) {
259
+ if (existsSync(join(root, plugin))) return false
260
+ }
261
+ return true
262
+ }
263
+
264
+ /**
265
+ * 把 bundle 层插件登记进 profile `package.json` 的 `dsh.profile.bundles`(幂等、改前备份)。
266
+ * 只登记**已装**的(node_modules 里有),不登记空气。
267
+ *
268
+ * ⚠️ W9(2026-10-13,用户 MBP 现场):条目是**包名**(`@whyshu/argent-skin`)——
269
+ * 裸名(`argent-skin`)在 profile 里**解析不到** ⇒ bundle 加载不到 ⇒ **皮肤/UI 壳静默不加载**。
270
+ * 旧实现把 `BUNDLE_LAYER_PLUGINS`(**id**)直接 push 进这张表,就是那个裸名的**写入点**。
271
+ * 现在:① 存量裸名条目**先迁移**(`migrateBundleNames`,幂等、保序);② 新登记一律写包名;
272
+ * ③ 落位判据两个名字都认(见 {@link landedInProfile})。
273
+ */
274
+ function ensureBundles() {
275
+ const pkgPath = join(PROFILE_DIR, 'package.json')
276
+ let pkg = null
277
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) } catch { return { added: [], renamed: [], stale: [], backup: null } }
278
+ pkg.dsh = pkg.dsh || {}
279
+ pkg.dsh.profile = pkg.dsh.profile || {}
280
+ const mig = migrateBundleNames(pkg.dsh.profile.bundles)
281
+ const bundles = mig.bundles
282
+ const added = []
283
+ for (const row of bundleLayerRows()) {
284
+ if (bundles.includes(row.name) || !landedInProfile(row.id)) continue
285
+ bundles.push(row.name)
286
+ added.push(row.name)
287
+ }
288
+ let backup = null
289
+ const changed = added.length > 0 || mig.renamed.length > 0 || mig.stale.length > 0
290
+ if (changed) {
291
+ const bak = backupFile(pkgPath, '', 'W9 bundles 条目迁移(裸名 → npm 名)')
292
+ if (!bak) return { added: [], renamed: [], stale: [], backup: null, refused: true }
293
+ backup = bak.path
294
+ pkg.dsh.profile.bundles = bundles
295
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
296
+ }
297
+ return { added, renamed: mig.renamed, stale: mig.stale, backup }
298
+ }
299
+
300
+ /**
301
+ * **依赖键**迁移:存量 profile 的裸键(`argent-core`)⇒ npm 名(`@whyshu/argent-core`)。
302
+ *
303
+ * ⚠️ W9 漏迁(2026-10-13):安装器的 `normalizeProfileDeps()` 在**新装路径**上做这件事,
304
+ * 但升级路径在 `install-argent.mjs` 里 `process.exit(0)`(第 2.1 步"完整升级"之后即退出)
305
+ * ⇒ **这一段从不执行**。于是"重跑安装命令"在存量机器上并不迁移依赖键。
306
+ * 判据/摘键时机与安装器**同一份实现**(`plugin-packages.mjs` + `staleDepsStillReferenced`):
307
+ * 先看组合行还有没有裸名,有则**宁可留一个无害别名,也不制造断链**。
308
+ */
309
+ function normalizeDepKeys() {
310
+ const pkgPath = join(PROFILE_DIR, 'package.json')
311
+ let pkg = null
312
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) } catch { return { renamed: [], dropped: [], refused: false } }
313
+ if (!pkg || typeof pkg.dependencies !== 'object' || pkg.dependencies === null) return { renamed: [], dropped: [], refused: false }
314
+ let patchText = ''
315
+ try { patchText = readFileSync(PATCH_PATH, 'utf8') } catch { /* 无 patch */ }
316
+ const mig = migrateDependencyKeys(pkg.dependencies)
317
+ const stillUsed = new Set(staleDepsStillReferenced(patchText, mig.stale))
318
+ const dropped = mig.stale.filter((k) => !stillUsed.has(k))
319
+ if (!mig.renamed.length && !dropped.length) return { renamed: [], dropped: [], refused: false }
320
+ const bak = backupFile(pkgPath, '', 'W9 依赖键迁移(裸名 → npm 名)')
321
+ if (!bak) return { renamed: [], dropped: [], refused: true }
322
+ for (const k of dropped) delete mig.deps[k]
323
+ writeFileSync(pkgPath, `${JSON.stringify({ ...pkg, dependencies: mig.deps }, null, 2)}\n`)
324
+ return { renamed: mig.renamed, dropped, stillUsed: [...stillUsed], refused: false }
325
+ }
326
+
327
+ function fixPatch(type) {
328
+ // ⚠️ W9(2026-10-12):patch 层的 `name:` 是 **npm 包名**(`@whyshu/argent-*`),
329
+ // 而组合 id 仍是裸 id ⇒ 这里给 `composeFix` 的是 `{id, name}` 行
330
+ // (真源 `compositionRows()`;口径见 `scripts/lib/plugin-packages.mjs`)。
331
+ const rows = compositionRows(type)
332
+ const target = rows.map((r) => r.id)
333
+ let text = null
334
+ try { text = readFileSync(PATCH_PATH, 'utf8') } catch { text = null }
335
+ if (text === null) {
336
+ mkdirSync(PROFILE_DIR, { recursive: true })
337
+ writeFileSync(PATCH_PATH, composeNew({ target: rows, type, bundleLayer: BUNDLE_LAYER_PLUGINS }), 'utf8')
338
+ return { changed: true, dropped: [], migrated: [], added: target.filter((p) => !BUNDLE_LAYER_PLUGINS.includes(p)) }
339
+ }
340
+ // 编辑逻辑在 lib/patch-compose.mjs(纯函数 + 离线门禁):这里只做读写与判据注入
341
+ const res = composeFix({
342
+ text,
343
+ target: rows,
344
+ bundleLayer: BUNDLE_LAYER_PLUGINS,
345
+ retired: RETIRED,
346
+ dangling: danglingRow,
347
+ })
348
+ // ── W9 漏迁(2026-10-13):**所有已知插件的行名**都归一为包名 ────────────────────────────
349
+ // `composeFix` 只给**目标组合内**的行改 name(非目标行原样保留)⇒ 一个"不在本实例目标组合、
350
+ // 但确实装着"的插件(如 chat 实例里的 `argent-flow`)会留着**裸名** `name: 'argent-flow'`,
351
+ // 而它的落位是/将是 scoped ⇒ 那一行解析不到(⛔ 装完不报错)。判据与安装器 `patchSelfcheck`
352
+ // 同一份实现(`patch-compose.mjs#migratePluginRowNames`),⛔ 本文件不写第二套。
353
+ const renamed = [...(res.renamed || [])]
354
+ const mig = migratePluginRowNames({
355
+ text: res.changed ? res.text : text,
356
+ nameOf: (id) => (PLUGIN_IDS.includes(id) ? pluginRowName(id) : undefined),
357
+ })
358
+ for (const r of mig.renamed) if (!renamed.some((x) => x.id === r.id)) renamed.push(r)
359
+ const changed = res.changed || mig.changed
360
+ const nextText = mig.changed ? mig.text : (res.changed ? res.text : text)
361
+ if (changed) {
362
+ // ⛔ 没有回滚点就不改客户文件(与安装器 `backupPatchFile` 同纪律)
363
+ const bak = backupFile(PATCH_PATH, text, `组合编辑/行名迁移(${type})`
364
+ + (renamed.length ? `:${renamed.map((r) => `${r.id} ${r.from} → ${r.to}`).join(';')}` : ''))
365
+ if (!bak) return { changed: false, dropped: [], migrated: [], added: [], renamed: [], refused: true }
366
+ writeFileSync(PATCH_PATH, nextText, 'utf8')
367
+ }
368
+ return { changed, dropped: res.dropped, migrated: res.migrated, added: res.added, renamed }
369
+ }
370
+
371
+ // ── 插件同步(目标组合从基座镜像到运行库)────────────────
372
+ /**
373
+ * 把目标组合的插件**镜像**进 profile 的 `node_modules/`。
374
+ *
375
+ * ⚠️ W9 漏迁(2026-10-13,**第四处**:`node_modules/<名>` 落位路径):落位名必须是**包名**
376
+ * (`node_modules/@whyshu/argent-flow`)—— 组合行的 `name:` 已是包名(见 `fixPatch` 的行名迁移),
377
+ * 而"落位 = 目录名"(裸 id)⇒ 那一行**解析不到**(⛔ 装完不报错,表现为插件静默不加载)。
378
+ * 同一个插件的两个名字**不许同时在**盘上 ⇒ 写 scoped 落位时顺手清掉旧裸名目录。
379
+ * 未知插件(不在 `plugin-packages.mjs` 名单里)**逐字原样**(⛔ 不猜、不越权)。
380
+ */
381
+ function syncPlugins(type) {
382
+ const target = TARGET_COMBO[type]
383
+ const synced = []
384
+ const moved = []
385
+ for (const p of target) {
386
+ const src = join(ARGENT_HOME, 'plugins', p)
387
+ if (!existsSync(src)) continue
388
+ const canonical = canonicalPluginName(p)
389
+ const dest = join(PROFILE_NM, ...canonical.split('/'))
390
+ mkdirSync(dirname(dest), { recursive: true })
391
+ rmSync(dest, { recursive: true, force: true })
392
+ cpSync(src, dest, { recursive: true })
393
+ // 旧裸名落位(`node_modules/argent-flow`)在 scoped 落位写好后一律清掉(⛔ 不留两份)
394
+ const bare = join(PROFILE_NM, p)
395
+ if (canonical !== p && existsSync(bare)) {
396
+ rmSync(bare, { recursive: true, force: true })
397
+ moved.push(`${p} → ${canonical}`)
398
+ }
399
+ synced.push(p)
400
+ }
401
+ return { synced, moved }
402
+ }
403
+
404
+ // ── 插件退役收敛(把 profile 真正收敛到目标组合)────────────
405
+ // 实现见 scripts/lib/profile-retire.mjs(纯函数、可离线测试);这里只注入路径。
406
+ function retirePlugins(type) {
407
+ return retirePluginsImpl({
408
+ profileNm: PROFILE_NM,
409
+ profileDir: PROFILE_DIR,
410
+ target: TARGET_COMBO[type],
411
+ // 规范插件根:仓库 plugins/ + 能力包缓存(判据是「源码是否还在」,不是「在不在组合里」)
412
+ canonicalRoots: [join(ARGENT_HOME, 'plugins'), join(DSH_HOME, 'argent', 'packs-cache')],
413
+ backupDir: join(DSH_HOME, 'argent', 'backup'),
414
+ log: (m) => console.log(m),
415
+ })
416
+ }
417
+
418
+ // ── 技能同步(**只发外发集**;内部技能不外发、宿主多出只报告不删)────────────
419
+ /**
420
+ * 归类(**复用门禁的唯一实现**;本文件不写第二份名单)。
421
+ * 兜底:`classify()` 抛错 ⇒ 返回 `ok:false` + 空外发集(**不同步任何东西**,绝不猜名单)。
422
+ * @param {{root?:string}} [opts] root = 要同步的技能所在的树(默认 `ARGENT_HOME`)
423
+ */
424
+ export function classifySkills({ root = ARGENT_HOME } = {}) {
425
+ try {
426
+ return classify({ root })
427
+ } catch (e) {
428
+ return {
429
+ ok: false, root, external: [], internal: [], unclassified: [], sources: [], warnings: [],
430
+ errors: [{ rule: 'G0', message: `classify() 抛错:${e && e.message ? e.message : String(e)}` }],
431
+ counts: { external: 0, internal: 0, skillDirs: 0, unclassified: 0 },
432
+ }
433
+ }
434
+ }
435
+
436
+ /** 宿主技能根下的**目录**名(排序;非目录/点文件不算技能)。 */
437
+ export function hostSkillDirs(dstRoot) {
438
+ try {
439
+ return readdirSync(dstRoot, { withFileTypes: true })
440
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
441
+ .map((e) => e.name).sort()
442
+ } catch { return [] }
443
+ }
444
+
445
+ /**
446
+ * **执行**技能同步(纯路径参数,便于离线夹具回归)。
447
+ *
448
+ * 硬约束(2026-10-04 客户机实测缺口,D5):
449
+ * · 只写 `external` 里的技能 —— 外发集 = `verticals/<id>/manifest.json#skills` 的并集(由 `classify()` 给);
450
+ * · **内部技能(`internal`)一个都不写**;`srcRoot` 里的其它目录(未归类/内部)也不写;
451
+ * · 宿主技能根里**已有的**目录(内部技能 / 旧技能 / 用户自建)**一个都不删** ——
452
+ * `hostInternal`(按设计不同步的)与 `hostExtra`(不在外发集里的旧/未知技能)只**报告**;
453
+ * · **幂等**:目标内容 sha 与源一致 ⇒ 跳过(不 rm/cp ⇒ mtime 不变),重复跑不反复写盘。
454
+ *
455
+ * @returns {{synced:string[], written:string[], skipped:string[], missing:string[],
456
+ * hostInternal:string[], hostExtra:string[]}}
457
+ */
458
+ export function syncSkills({ srcRoot, dstRoot, external = [], internal = [] } = {}) {
459
+ const written = []
460
+ const skipped = []
461
+ const missing = []
462
+ const ext = [...external]
463
+ if (ext.length) mkdirSync(dstRoot, { recursive: true })
464
+ for (const name of ext) {
465
+ const src = join(srcRoot, name)
466
+ // 归类门禁 G4 本该拦住"声明了却不存在";这里再挡一次(**绝不**为一个不存在的技能去删宿主的同名目录)
467
+ if (!existsSync(join(src, SKILL_FILE))) { missing.push(name); continue }
468
+ const dst = join(dstRoot, name)
469
+ if (existsSync(join(dst, SKILL_FILE))) {
470
+ try {
471
+ if (skillDigest(dst).digest === skillDigest(src).digest) { skipped.push(name); continue }
472
+ } catch { /* 读不动/坏目录 ⇒ 按需覆盖(下面走正常写入路径) */ }
473
+ }
474
+ rmSync(dst, { recursive: true, force: true })
475
+ cpSync(src, dst, { recursive: true })
476
+ written.push(name)
477
+ }
478
+ const host = hostSkillDirs(dstRoot)
479
+ const extSet = new Set(ext)
480
+ const intSet = new Set(internal)
481
+ return {
482
+ synced: [...ext],
483
+ written,
484
+ skipped,
485
+ missing,
486
+ // 「宿主多出」两分:内部技能(按设计不外发,但客户机旧装机残留会留着)与**其它**(旧技能/用户自建)
487
+ hostInternal: host.filter((n) => intSet.has(n)),
488
+ hostExtra: host.filter((n) => !extSet.has(n) && !intSet.has(n)),
489
+ }
490
+ }
491
+
492
+ // ── check:远端版本检测 ────────────────────────────────────
493
+ function doCheck() {
494
+ const current = readVersion(ARGENT_HOME)
495
+ const fetch = runCmdSafe('git', ['-C', ARGENT_HOME, 'fetch', 'origin'], { timeout: 45000 })
496
+ let remote = null
497
+ if (fetch.ok) {
498
+ const show = runCmdSafe('git', ['-C', ARGENT_HOME, 'show', 'origin/main:VERSION'], { timeout: 15000 })
499
+ if (show.ok) remote = String(show.stdout).trim() || null
500
+ }
501
+ out({
502
+ ok: true,
503
+ current,
504
+ remote,
505
+ updateAvailable: remote && current && remote !== current ? true : (remote ? false : null),
506
+ fetchError: fetch.ok ? null : fetch.message,
507
+ })
508
+ }
509
+
510
+ // ── migrate-deps:存量迁移(遗留 `file:` 依赖 ⇒ npm 名;**默认干跑**)─────────
511
+ /**
512
+ * 唯一实现 = `scripts/lib/deps-migrate.mjs`(本函数只做取数/排版/开关)。
513
+ *
514
+ * 四条纪律(都有夹具):① 默认干跑(不带 `--apply` 零写入)② 写前备份(时间戳 + sha256 + 回滚命令)
515
+ * ③ 读回验收 + 审计 JSONL ④ 幂等(已迁过 ⇒ 零写入,连 mtime 都不动)。
516
+ * ⛔ **不裸跑包管理器**:`--allow-package-manager` 才跑,且**跑之前**先把计划打印出来。
517
+ */
518
+ async function doMigrateDeps() {
519
+ const pkgPath = join(PROFILE_DIR, 'package.json')
520
+ let pkg = null
521
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) }
522
+ catch (e) {
523
+ out({ ok: false, error: `读不到 profile package.json(${pkgPath}):${String((e && e.message) || e).split('\n')[0]}`, pkgPath })
524
+ process.exitCode = 2
525
+ return
526
+ }
527
+ const plan = planDepsMigration({
528
+ pkg,
529
+ knownId: knownPluginId,
530
+ // 真实版本的**候选落点**(保序):`--repo`(夹具/多克隆机器)→ `ARGENT_HOME`(`~/.argent-dsh`,
531
+ // **存量客户机真正在用的克隆**)→ 本文件所在的树。⛔ 三个都读不到 ⇒ 该条 `blocked`(不编版本号)。
532
+ versionOf: (id) => [FLAG_REPO, ARGENT_HOME, REPO_FALLBACK]
533
+ .filter(Boolean)
534
+ .map((root) => realVersionOf(root, id))
535
+ .find((v) => v) || '',
536
+ })
537
+ const rendered = renderPlan(plan, { profilePkgPath: pkgPath, apply: FLAG_APPLY, runPackageManager: FLAG_ALLOW_PM })
538
+ const result = {
539
+ ok: plan.ok, mode: FLAG_APPLY ? 'apply' : 'dry-run', pkgPath,
540
+ changed: plan.changed, blocked: plan.blocked, packageManager: FLAG_ALLOW_PM,
541
+ plan: plan.entries, lines: rendered.lines, rollback: rendered.rollback,
542
+ }
543
+ if (!FLAG_APPLY) {
544
+ if (!FLAG_JSON) console.log(rendered.lines.join('\n'))
545
+ out({ ...result, applied: false, note: '默认干跑:加 --apply 才写;⛔ 本命令不会自己跑包管理器' })
546
+ process.exitCode = plan.ok ? 0 : 2
547
+ return
548
+ }
549
+ if (!plan.ok) {
550
+ // ⛔ 拒绝半截迁移:有受阻条目 ⇒ 一条都不写(要么全可迁、要么先把证据补齐)
551
+ if (!FLAG_JSON) console.log(rendered.lines.join('\n'))
552
+ out({ ...result, applied: false, error: `有 ${plan.blocked.length} 条受阻 ⇒ ⛔ 拒绝写入(避免半截迁移)` })
553
+ process.exitCode = 1
554
+ return
555
+ }
556
+ // ⚠️ 内核数据目录判据**懒加载**(本文件被 `run` 路径 import 时不该依赖 core/ 存在 ——
557
+ // 夹具/老克隆只拷 `scripts/`;静态 import 会让 `run` 直接 ERR_MODULE_NOT_FOUND 假红)。
558
+ let dataDir = ''
559
+ try {
560
+ const kernel = await import('./lib/argent-datadir.mjs')
561
+ dataDir = kernel.resolveDataDir({ env: { ...process.env, DSH_HOME }, home: HOME }).dir
562
+ } catch (e) {
563
+ if (!FLAG_DATA_DIR) {
564
+ out({ ok: false, error: `取不到内核数据目录判据(core/lib/argent-datadir.mjs 不在本树)⇒ ⛔ 拒绝猜目录;显式给 --data-dir <目录>` })
565
+ process.exitCode = 2
566
+ return
567
+ }
568
+ }
569
+ const backupDir = join(FLAG_DATA_DIR || dataDir, 'backup')
570
+ const auditPath = join(FLAG_DATA_DIR || dataDir, 'deps-migrate-audit.jsonl')
571
+ const applied = applyDepsMigration({
572
+ pkgPath, pkg, plan, auditPath, backupDir, reason: 'W8 file:→npm',
573
+ // 读回验收用**同一套判据**重算(否则"缺版本"这类条目会两套结论 ⇒ 假红)
574
+ replan: (doc) => planDepsMigration({
575
+ pkg: doc, knownId: knownPluginId,
576
+ versionOf: (id) => [FLAG_REPO, ARGENT_HOME, REPO_FALLBACK].filter(Boolean).map((root) => realVersionOf(root, id)).find((v) => v) || '',
577
+ }),
578
+ })
579
+ if (!FLAG_JSON) {
580
+ console.log(rendered.lines.join('\n'))
581
+ if (applied.wrote) {
582
+ console.log(`✅ 已迁移 ${plan.changed} 条依赖`)
583
+ console.log(` 备份:${applied.backup}(sha256 ${applied.backupSha})`)
584
+ console.log(` 回滚:${applied.rollback}`)
585
+ console.log(` 审计:${applied.audit || '(未写)'}`)
586
+ console.log(` 验收:${applied.verify}`)
587
+ }
588
+ }
589
+ out({ ...result, applied: applied.wrote, apply: applied })
590
+ process.exitCode = applied.ok ? 0 : 1
591
+ }
592
+
593
+ // ── run:完整升级 ─────────────────────────────────────────
594
+ async function doRun() {
595
+ const steps = []
596
+ // 缺陷 1(2026-10-12):本次该落的角色集合 + 是否需要收尾重施。
597
+ // 为什么升级也要"收尾重施":第 2 步收窄之后,第 6.5/7.5 步(退役插件收敛、profile-doctor --fix)
598
+ // 都可能重解析 profile 依赖 ⇒ 把副本换成指向货架的链(14 个 patch 全回来)。
599
+ let UPGRADE_PRESET_LANDING = null
600
+ let PENDING_PRESET_REAPPLY = false
601
+ // 内部角色授权(`agents.json#internalRoles`)—— 第 2 步与第 7.6 步的**不变式**都用它:
602
+ // 未授权时落盘集合若含 `visibility: internal` 的角色 ⇒ `applyPresetSelection` 判红并拒绝写出。
603
+ let PRESET_ALLOW_INTERNAL = false
604
+ // **可信已装集**(唯一来源:可信痕迹 → agents.json#roles 并集 → 空集)。第 2 步算一次,
605
+ // 第 7.6 步的回落也用它 —— ⛔ 7.6 不再直接读痕迹(不可信痕迹会把收尾重新放大回去)。
606
+ let PRESET_INSTALLED = { ids: [], source: 'none', rejected: '' }
607
+ // 0) 工作区预检(未提交改动会阻塞 pull——硬错误,请用户先清理,杜绝半升级)
608
+ const st = runCmdSafe('git', ['-C', ARGENT_HOME, 'status', '--porcelain'])
609
+ const dirty = (st.stdout || '').trim().split('\n').filter(Boolean)
610
+ if (dirty.length) {
611
+ out({ ok: false, error: `~/.argent-dsh 有未提交改动(${dirty.slice(0, 5).join('; ')})——请先提交或还原(KB-032),再运行更新` })
612
+ process.exit(2)
613
+ }
614
+ // 1) git pull
615
+ const pull = runCmdSafe('git', ['-C', ARGENT_HOME, 'pull', '--ff-only'], { timeout: 120000 })
616
+ const pulled = pull.ok || /up to date|Already up/i.test(String(pull.stdout || ''))
617
+ steps.push({ ok: pull.ok, text: `git pull: ${pull.ok ? (String(pull.stdout || '').trim().split('\n')[0] || '已是最新') : pull.message}` })
618
+ // 2) preset 全量同步(**按 dsh 机制分派**:0.1.7 起行式,不再扫目录)
619
+ // P1-3b(2026-10-12):scope 定位改用真源 `resolveDshScopeDir()`(**npx 感知** + 探不到 ⇒ null)。
620
+ // 老实现走遗留 `resolveDshScope()`,它在"纯 npx 机器"上**静默回一个不存在的路径** ⇒
621
+ // `detectPresetMechanism()` 得 `unknown` ⇒ 本步静默退到"目录式"(往 `.agent-presets/` 拷,
622
+ // 在 npx/0.2 宿主上根本不生效)。换真源后:npx 机器上机制**判得对**(= 行式)。
623
+ // ⚠️ 遗留缺口(⛔ 本批不改,已登记在 docs/Argent-0.5.0-适配实施计划.md):机制仍为 `unknown`
624
+ // (真的解析不到 scope)时,下面 `else if (existsSync(srcPresets))` 仍会走目录式 ——
625
+ // 那是**既存的降级路径**,要不要改成响亮失败是独立决策。
626
+ const _scopeHit = resolveDshScopeDir()
627
+ // 0.6.0 F1 修复批(2026-10-14):探不到 scope ⇒ 本步是**可选增强**(探不到也能继续,只是机制判成
628
+ // unknown ⇒ 走下面那条目录式)⇒ **降级但响亮**:点名根因 + 给出下一步命令(⛔ 不静默)。
629
+ if (!_scopeHit) {
630
+ steps.push({ ok: true, text: '⚠ 探不到宿主 scope(**可能是 npx 形态**:`~/.npm/_npx/<hash>/node_modules/@deepseek-ai`)' +
631
+ '⇒ preset 机制判成 unknown、本步退到目录式;要判准请显式给 `--scope-dir <…/@deepseek-ai>` / `DSH_SCOPE=<…>` / `DSH_BIN=<该 scope 自带的 dsh>`' })
632
+ }
633
+ const presetMechanism = detectPresetMechanism(_scopeHit ? _scopeHit.dir : '')
634
+ let presets = 0
635
+ const srcPresets = join(ARGENT_HOME, 'presets')
636
+ if (presetMechanism === MECHANISM.ROWS) {
637
+ // A 批(2026-10-10):**升级必须重新施加"角色子集"**,否则一次升级就把 `--agent` 落盘时的
638
+ // 收窄(例:装 HR 只落 2 个角色)**全部还原**成 14 个 —— 用户口径"只装这个智能体相关的东西"
639
+ // 会在第一次升级后静默失效。口径与安装器**同一份实现**(`presetSelectionForAgents`):
640
+ // · `<dshHome>/argent/agents.json` 记着本机装了哪些智能体、是否 `--internal-roles`;
641
+ // · 有智能体记录 ⇒ **agentScoped**(只落它们的角色 + 恒装 + 已装);
642
+ // · 没有记录 ⇒ 默认档(客户可见集 ∪ 已装集;内部角色不落)。
643
+ // ⛔ 两个记录都没有(老机器 / 首次升级)⇒ `onlyPresets = null` ⇒ **与改动前逐字节一致**。
644
+ // ⚠️ 缺陷 1(2026-10-12 真机实测):「已装集」的判据 = **选择痕迹**
645
+ // `presets.selection.json#kept`,⛔ 不是 `package.json#dsh.bundle.patch`(后者在 profile 副本
646
+ // 被 pnpm 换回货架链之后读到的就是货架自带的 14 条 ⇒ 收窄被静默放大回去)。
647
+ // ⚠️ 追修批(2026-10-12 下半场):**唯一来源**收口到 `resolveInstalledSelection()`
648
+ // —— 痕迹不可信(含内部角色而本机未授权内部角色,现场 `kept:14`/`excluded:[]` 就是这个形态)
649
+ // 时按"痕迹缺失"处理,退回 `agents.json#roles` 并集;两级都没有 ⇒ 空集(首次安装语义)。
650
+ // 并:**升级档 = 默认安装档**(客户可见集 ∪ 已装集 ∪ 本智能体角色)。
651
+ // ⛔ 不再用 `agentScoped: agents.length>0` —— 那会把"客户可见集"整个排除掉,
652
+ // 让一台现场已是 14(或 7)的机器只落 `general+hr` 两个角色(实测得 2,见回归①)。
653
+ let onlyPresets = null
654
+ let selectionWhy = ''
655
+ try {
656
+ const agLib = await import(pathToFileURL(join(ARGENT_HOME, 'scripts', 'argent-agents.mjs')).href)
657
+ const shelf = JSON.parse(readFileSync(join(ARGENT_HOME, 'plugins', 'argent-presets', 'presets.json'), 'utf8'))
658
+ // ⚠️ W9:profile 里 bundle 的落点是**包名**(`@whyshu/argent-presets`),存量机器上仍是旧裸名
659
+ // 目录 ⇒ 一律走真源 `resolveBundleDir()`(**两个名字都找**,⛔ 不写死其中一个)。
660
+ const bundleDir = resolveBundleDir(PROFILE_DIR)
661
+ const ledgerPath = join(DSH_HOME, 'argent', 'agents.json')
662
+ const ledger = existsSync(ledgerPath) ? JSON.parse(readFileSync(ledgerPath, 'utf8')) : null
663
+ const agents = (ledger && Array.isArray(ledger.agents)) ? ledger.agents : []
664
+ PRESET_ALLOW_INTERNAL = !!(ledger && ledger.internalRoles)
665
+ const installed = resolveInstalledSelection({ bundleDir, ledger, allowInternal: PRESET_ALLOW_INTERNAL })
666
+ PRESET_INSTALLED = installed
667
+ if (installed.rejected) {
668
+ // **响亮报告**:不可信痕迹一律不采信,也⛔不静默改它(下一句就把它按正确集合重写)
669
+ steps.push({ ok: true, text: `⚠ 选择痕迹不可信 ⇒ 已装集退回 agents.json 角色:${installed.rejected}` })
670
+ }
671
+ onlyPresets = agLib.presetSelectionForAgents({
672
+ presets: shelf, agents, agentScoped: false, internal: PRESET_ALLOW_INTERNAL, previous: installed.ids,
673
+ })
674
+ selectionWhy = `升级重新施加角色子集(智能体:${agents.length ? agents.map((a) => a.id).join('、') : '无记录'}`
675
+ + `;已装集来源:${installed.source === 'trace' ? '选择痕迹' : installed.source === 'agents' ? 'agents.json 角色并集' : '无(首次安装语义)'}`
676
+ + `)${PRESET_ALLOW_INTERNAL ? ' --internal-roles' : ''}`
677
+ } catch (e) {
678
+ steps.push({ ok: true, text: `角色子集策略未施加(${String(e.message || e).split('\n')[0]})⇒ 按全量同步(旧行为)` })
679
+ }
680
+ {
681
+ // ⚠️ W9:bundle 落点 = **包名**(`@whyshu/argent-presets`);存量机器上是旧裸名目录 ⇒
682
+ // 走真源 `resolveBundleDir()`(两个名字都找,⛔ 不写死其中一个)。
683
+ const bundleDir = resolveBundleDir(PROFILE_DIR)
684
+ // 没算出集合时**回落选择痕迹**(自愈:pnpm 抹掉痕迹前的那次收窄结果)
685
+ const want = (Array.isArray(onlyPresets) && onlyPresets.length) ? onlyPresets : PRESET_INSTALLED.ids
686
+ if (want.length) {
687
+ UPGRADE_PRESET_LANDING = { kept: [...want], why: selectionWhy || '升级按选择痕迹' }
688
+ PENDING_PRESET_REAPPLY = true
689
+ }
690
+ }
691
+ // 行式:装 argent-presets bundle + 登记 bundles/注册表默认值(roles 受 onlyPresets 收窄)
692
+ const res = installPresets({
693
+ mechanism: presetMechanism,
694
+ repo: ARGENT_HOME,
695
+ profileDir: PROFILE_DIR,
696
+ presetId: 'argent-general',
697
+ onlyPresets,
698
+ selectionWhy,
699
+ allowInternal: PRESET_ALLOW_INTERNAL,
700
+ log: () => {},
701
+ })
702
+ if (res.ok && res.selection && res.selection.kept.length) {
703
+ UPGRADE_PRESET_LANDING = { kept: [...res.selection.kept], why: selectionWhy || '升级落盘集合' }
704
+ PENDING_PRESET_REAPPLY = true
705
+ }
706
+ presets = res.ok ? Object.keys(JSON.parse(readFileSync(join(resolveBundleDir(PROFILE_DIR), 'presets.json'), 'utf8'))).length : 0
707
+ steps.push({
708
+ ok: res.ok,
709
+ text: res.ok
710
+ ? `preset 同步(行式): ${presets} 个角色 → profile bundles(${mechanismNote(presetMechanism)})`
711
+ + (onlyPresets ? `|策略:${selectionWhy}` : '')
712
+ : `preset 同步失败:${res.reason}`,
713
+ })
714
+ } else if (existsSync(srcPresets)) {
715
+ mkdirSync(PRESET_ROOT, { recursive: true })
716
+ for (const name of readdirSync(srcPresets, { withFileTypes: true })) {
717
+ if (!name.isDirectory()) continue
718
+ if (!existsSync(join(srcPresets, name.name, 'format-version.txt'))) continue
719
+ const src = join(srcPresets, name.name)
720
+ const dst = join(PRESET_ROOT, name.name)
721
+ rmSync(dst, { recursive: true, force: true })
722
+ cpSync(src, dst, { recursive: true })
723
+ presets++
724
+ }
725
+ steps.push({ ok: presets > 0, text: `preset 同步(目录式): ${presets} 个角色 → .agent-presets/` })
726
+ }
727
+ // 仓库当前提供的角色(判据:带安装器标记 format-version.txt)
728
+ const shippedPresets = new Set(
729
+ existsSync(srcPresets) ? readdirSync(srcPresets).filter((d) => existsSync(join(srcPresets, d, 'format-version.txt'))) : [],
730
+ )
731
+ // 2.5) 清理**旧版残留**角色(会直接把 0.1.5 挡在启动门外,见 lib/preset-sync.mjs 注释)
732
+ const prune = pruneStalePresets({
733
+ presetRoot: PRESET_ROOT,
734
+ shipped: shippedPresets,
735
+ backupDir: join(DSH_HOME, 'argent', 'backup'),
736
+ log: (m) => console.log(m),
737
+ })
738
+ if (prune.stale.length) {
739
+ steps.push({ ok: true, text: `旧版残留角色已清理: ${prune.stale.join(', ')}(备份 ${prune.backup || '失败'})` })
740
+ } else if (prune.skipped) {
741
+ steps.push({ ok: false, text: '跳过残留角色清理:仓库未提供任何 preset(源目录缺失?)—— 已刻意不动运行时角色' })
742
+ }
743
+ // 3) skills 同步(**只发外发集**:内部角色技能不外发;宿主多出只报告不删)
744
+ // 判据来自 `classify()`(唯一实现),本文件**不写第二份名单、不自己读 manifest**。
745
+ const srcSkills = join(ARGENT_HOME, 'skills')
746
+ const skillCls = classifySkills({ root: ARGENT_HOME })
747
+ const skillRes = syncSkills({
748
+ srcRoot: srcSkills,
749
+ dstRoot: SKILLS_DIR,
750
+ external: skillCls.external,
751
+ internal: skillCls.internal,
752
+ })
753
+ if (!skillCls.ok) {
754
+ // 归类门禁红 ≠ 外发集不可用:外发集仍严格等于业务线声明 ⇒ 照发(且**未归类目录一个都不发、不删**)
755
+ steps.push({
756
+ ok: false,
757
+ text: `技能归类门禁不通过(${skillCls.errors.length} 条)—— 外发集仍按业务线声明同步、未归类目录不同步也不删;`
758
+ + '请先跑 `node scripts/check-skill-classification.mjs`',
759
+ })
760
+ }
761
+ if (!skillCls.external.length) {
762
+ steps.push({
763
+ ok: false,
764
+ text: `技能同步: 跳过 —— 外发集为空(${join(ARGENT_HOME, 'verticals')} 下读不到 manifest#skills?)`
765
+ + '**不猜名单、不同步**(跑 scripts/sync-scripts.sh 补齐业务线真源后重试)',
766
+ })
767
+ } else {
768
+ steps.push({
769
+ ok: skillRes.missing.length === 0,
770
+ text: `技能同步: ${skillRes.synced.length} 项(外发集;内部 ${skillCls.internal.length} 项按设计不同步)`
771
+ + (skillRes.written.length ? `,写入 ${skillRes.written.length} 项:${skillRes.written.join(', ')}` : ',内容已一致(幂等跳过,未重复写)')
772
+ + (skillRes.missing.length ? `;⚠ 源缺 ${SKILL_FILE}:${skillRes.missing.join(', ')}` : ''),
773
+ })
774
+ }
775
+ const hostExtra = [...skillRes.hostInternal, ...skillRes.hostExtra]
776
+ if (hostExtra.length) {
777
+ const shown = hostExtra.slice(0, 8)
778
+ steps.push({
779
+ ok: true, warn: true,
780
+ text: `宿主技能根多出 ${hostExtra.length} 项(不在外发集;**只报告不删**):${shown.join(', ')}`
781
+ + (hostExtra.length > shown.length ? ` …(另 ${hostExtra.length - shown.length} 项见 --json)` : '')
782
+ + (skillRes.hostInternal.length ? `|其中按设计不外发的内部技能 ${skillRes.hostInternal.length} 项` : ''),
783
+ })
784
+ }
785
+ // 4) scripts 自检
786
+ const missingScripts = CRITICAL_SCRIPTS.filter((f) => !existsSync(join(ARGENT_HOME, 'scripts', f)))
787
+ steps.push({ ok: missingScripts.length === 0, text: missingScripts.length ? `scripts 缺失: ${missingScripts.join(', ')}` : 'scripts 就绪(关键脚本均在)' })
788
+ // 5) 落位目录迁移(W9 漏迁的第四处,**先于**行名迁移:行名 scoped 而落位还是裸名 ⇒ 那一行解析不到)
789
+ const landings = migrateBareLandings()
790
+ if (landings.moved.length || landings.pruned.length) {
791
+ steps.push({
792
+ ok: true,
793
+ text: `落位目录迁移到 npm 名: ${landings.moved.join(', ') || '无'}`
794
+ + `${landings.pruned.length ? `;清掉重复的裸名落位 ${landings.pruned.join(', ')}` : ''}`,
795
+ })
796
+ }
797
+ // 5.5) patch 组合校正
798
+ const type = detectCombo()
799
+ const patchRes = fixPatch(type)
800
+ if (patchRes.migrated && patchRes.migrated.length) {
801
+ steps.push({ ok: true, text: `bundle 层迁移: 摘 patch 行 ${patchRes.migrated.join(', ')}(改由 dsh.profile.bundles 加载)` })
802
+ }
803
+ if (patchRes.renamed && patchRes.renamed.length) {
804
+ steps.push({ ok: true, text: `组合行名迁移到 npm 名: ${patchRes.renamed.map((r) => `${r.id} ${r.from} → ${r.to}`).join(';')}` })
805
+ }
806
+ steps.push({ ok: true, text: patchRes.changed ? `组合校正: ${type}(移除 ${patchRes.dropped.join(', ') || '无'},补齐 ${patchRes.added.join(', ') || '无'})` : `组合已是最新(${type}: ${TARGET_COMBO[type].join('/')})` })
807
+ // 6) 插件同步(**落位名 = 包名**;旧裸名目录顺手清掉)
808
+ const syncedRes = syncPlugins(type)
809
+ const synced = syncedRes.synced
810
+ steps.push({ ok: synced.length > 0, text: `插件同步: ${synced.join(', ') || '无'} → profile「${PROFILE}」` })
811
+ if (syncedRes.moved.length) {
812
+ steps.push({ ok: true, text: `落位路径迁移到 npm 名: ${syncedRes.moved.join(';')}(⛔ 不留两份)` })
813
+ }
814
+ // 6.5) 退役插件收敛(副本 + 依赖行一起清理,否则 doctor 报 dep-missing 卡住重启门禁)
815
+ const bundled = ensureBundles()
816
+ if (bundled.added.length) {
817
+ steps.push({ ok: true, text: `bundle 层登记: ${bundled.added.join(', ')} → dsh.profile.bundles(备份 ${bundled.backup ? '已存' : '失败'})` })
818
+ }
819
+ // W9 漏迁的第三处:存量 `bundles` 里的**裸名**条目(`argent-skin`)在 profile 里解析不到
820
+ // ⇒ bundle 加载不到、**皮肤/UI 壳静默不加载**(装完零报错)。这里当场迁到包名并响亮报告。
821
+ if (bundled.refused) {
822
+ steps.push({ ok: false, text: 'bundles 条目迁移被跳过(写前备份失败 ⇒ 不制造不可逆改动)' })
823
+ }
824
+ if (bundled.renamed.length || bundled.stale.length) {
825
+ steps.push({
826
+ ok: true,
827
+ text: `bundles 条目迁移到 npm 名: ${bundled.renamed.map((r) => `${r.from} → ${r.to}`).join(';') || '无'}`
828
+ + `${bundled.stale.length ? `;摘陈旧别名 ${bundled.stale.join(', ')}` : ''}(备份 ${bundled.backup ? '已存' : '失败'})`,
829
+ })
830
+ }
831
+ // 6.6) 依赖键迁移(W9 漏迁的第二处;安装器那段在升级路径上**不会被执行** —— 见 normalizeDepKeys)
832
+ const depMig = normalizeDepKeys()
833
+ if (depMig.renamed.length || depMig.dropped.length) {
834
+ steps.push({
835
+ ok: true,
836
+ text: `依赖键迁移到 npm 名: ${depMig.renamed.map((r) => `${r.from} → ${r.to}`).join(';') || '无'}`
837
+ + `${depMig.dropped.length ? `;摘无人引用的旧裸键 ${depMig.dropped.join(', ')}` : ''}`,
838
+ })
839
+ } else if (depMig.refused) {
840
+ steps.push({ ok: false, text: '依赖键迁移被跳过(写前备份失败 ⇒ 不制造不可逆改动)' })
841
+ }
842
+ const retired = retirePlugins(type)
843
+ if (retired.removed.length || retired.depsDropped.length) {
844
+ steps.push({
845
+ ok: true,
846
+ text: `退役插件收敛: 删副本 ${retired.removed.join(', ') || '无'};摘依赖 ${retired.depsDropped.join(', ') || '无'}`,
847
+ })
848
+ }
849
+ // 7) 汇总
850
+ const version = readVersion(ARGENT_HOME)
851
+ // 重启判据只看**真的写盘**(幂等跳过不算 —— 否则每次 `run` 都会要求重启)
852
+ const restartRequired = patchRes.changed || skillRes.written.length > 0 || synced.length > 0
853
+ || landings.moved.length > 0 || landings.pruned.length > 0
854
+ || bundled.added.length > 0 || bundled.renamed.length > 0 || bundled.stale.length > 0
855
+ || depMig.renamed.length > 0 || depMig.dropped.length > 0
856
+ const isMac = process.platform === 'darwin'
857
+ const result = {
858
+ ok: pulled,
859
+ current: readVersion(ARGENT_HOME),
860
+ comboType: type,
861
+ targetCombo: TARGET_COMBO[type],
862
+ version,
863
+ steps,
864
+ patchChanged: patchRes.changed,
865
+ synced,
866
+ // 技能同步:外发集写盘清单 + **按设计不同步的内部清单**(D5;宿主多出只报告不删)
867
+ skillsSync: {
868
+ source: 'scripts/check-skill-classification.mjs#classify()(真源 = verticals/*/manifest.json#skills)',
869
+ external: skillCls.external,
870
+ written: skillRes.written,
871
+ skipped: skillRes.skipped,
872
+ missing: skillRes.missing,
873
+ hostInternal: skillRes.hostInternal,
874
+ hostExtra: skillRes.hostExtra,
875
+ classifyOk: skillCls.ok,
876
+ classifyErrors: skillCls.errors.map((e) => `[${e.rule}] ${e.message}`),
877
+ },
878
+ skippedInternal: skillCls.internal,
879
+ restartRequired,
880
+ restartHint: isMac ? '请重启 dsh web(或加 --restart 自动重启)' : '请重启 dsh web(Windows: 关闭进程重开)',
881
+ }
882
+ // 7.5) **重启前门禁**:profile 组合体检(profile-doctor)
883
+ // 由来(2026-09-14):`argent-skin` 同时出现在 bundles 与 cordis.patch.yml
884
+ // ⇒ 加载器报 `duplicate loader entry id` ⇒ dsh web 启动即失败,在 KeepAlive 下 crash loop。
885
+ // 若组合是坏的,**绝不能**自动重启(那会把服务打进崩溃循环),必须先修或中止。
886
+ //
887
+ // ⛔ 2026-10-03:**默认不再 `--fix`**。旧实现无条件带 `--fix`,而 doctor 的 `--fix` 会
888
+ // ① 重指 `file:` 依赖(把已消失的临时目录换成仓库路径)② 为让 node_modules 跟上而跑
889
+ // `dsh plugin install`(= **pnpm install**)。这两件事都在 web profile 上真实发生过
890
+ // (`~/.dsh/AGENTS.md` 明令避免在那棵树上跑 pnpm),且**没有任何提示**。
891
+ // 现在的姿态:默认只做**只读**体检(门禁语义不变:不通过就不自动重启);
892
+ // 要让它动手必须显式 `--with-profile-fix`,且**执行前**先打印它会改什么、要不要跑包管理器。
893
+ const doctorPath = join(dirname(process.argv[1] ?? process.cwd()), 'profile-doctor.mjs')
894
+ let doctorBlocked = false
895
+ if (existsSync(doctorPath)) {
896
+ // 只读预检(--json):拿到问题清单 + "若加 --fix 会做什么"的计划
897
+ // ⚠️ 显式传 DSH_HOME:doctor 按环境变量解析 home(没有 --dsh-home 开关)⇒ 不传就可能去改真实 ~/.dsh
898
+ const probe = runCmdSafe(process.execPath, [doctorPath, '--profile', PROFILE, '--json'], { timeout: 420000, env: { ...process.env, DSH_HOME } })
899
+ const pre = parseDoctorJson(probe.stdout)
900
+ const checkOk = pre ? pre.ok : probe.ok
901
+ const summary = pre ? pre.reportLines.slice(-8) : String(probe.stdout || probe.message || '').split('\n').filter(Boolean).slice(-8)
902
+ result.profileFix = {
903
+ requested: FLAG_PROFILE_FIX,
904
+ executed: false,
905
+ skipped: true,
906
+ reason: '默认跳过 profile --fix(只读体检;要修组合/动包管理器须显式 --with-profile-fix)',
907
+ plan: pre ? pre.fixPlan : null,
908
+ checkOk,
909
+ }
910
+ let fixOk = checkOk
911
+ if (FLAG_PROFILE_FIX) {
912
+ // **执行前**如实声明:改哪些 file: 依赖、是否跑包管理器(⛔ 不得静默)
913
+ const disclosure = describeProfileFix(pre ? pre.fixPlan : null, PROFILE)
914
+ for (const line of disclosure) console.error(line)
915
+ const d = runCmdSafe(process.execPath,
916
+ [doctorPath, '--profile', PROFILE, '--fix', '--allow-package-manager', '--json'],
917
+ { timeout: 420000, env: { ...process.env, DSH_HOME } })
918
+ const dJson = parseDoctorJson(d.stdout)
919
+ fixOk = dJson ? dJson.ok : d.ok
920
+ const fixLines = (dJson ? dJson.reportLines : `${d.stdout || ''}${d.stderr || ''}`.split('\n')).filter(Boolean)
921
+ for (const line of fixLines) console.error(String(line)) // doctor 报告走 stderr(stdout 留给 --json / 人类报告)
922
+ result.profileFix = {
923
+ requested: true,
924
+ executed: true,
925
+ skipped: false,
926
+ allowPackageManager: true,
927
+ plan: pre ? pre.fixPlan : null,
928
+ disclosure,
929
+ ok: fixOk,
930
+ fixes: dJson ? dJson.fixes : null,
931
+ }
932
+ result.doctor = { ok: fixOk, summary: fixLines.filter((l) => l.includes('[doctor]') || l.includes('✅') || l.includes('✗') || l.includes('🔧') || l.includes('⚠')).slice(-8).map((l) => String(l).trim()) }
933
+ steps.push({ ok: fixOk, text: `profile 组合体检+修复(--with-profile-fix): ${fixOk ? '通过' : '仍有问题(见下方)'}` })
934
+ } else {
935
+ result.doctor = { ok: checkOk, summary: summary.map((l) => String(l).trim()) }
936
+ steps.push({
937
+ ok: checkOk,
938
+ text: `profile 组合体检(**只读**,未执行 --fix): ${checkOk ? '通过' : '发现问题'}`
939
+ + (checkOk ? '' : ';⛔ 未自动修复(默认跳过;确需修复请显式 --with-profile-fix)'),
940
+ })
941
+ }
942
+ if (!fixOk) {
943
+ doctorBlocked = true
944
+ result.restartRequired = false
945
+ result.restartHint = '⚠ profile 组合体检未通过 —— **已阻止自动重启**(否则可能崩溃循环);请先按 profile-doctor 输出修复'
946
+ + (FLAG_PROFILE_FIX ? '' : '(本轮**未**自动修复:加 --with-profile-fix 才会执行 doctor --fix)')
947
+ console.error('[upgrade] profile-doctor 未通过 —— 已阻止自动重启:')
948
+ for (const line of (result.doctor.summary || []).slice(-8)) console.error(' ' + String(line).trim())
949
+ }
950
+ }
951
+
952
+ // 7.6) 角色落盘集合:**收尾重施**(缺陷 1,2026-10-12 真机实测)
953
+ // 第 2 步收窄之后,第 6.5/7.5 步都可能重解析 profile 依赖(doctor --fix 甚至会跑包管理器)
954
+ // ⇒ profile 副本被换成指向货架的链(14 个 patch 全回来、选择痕迹消失)。这里按第 2 步算出的
955
+ // **同一目标集合**再施加一次,并核对"三处集合 + 痕迹"四者相等。
956
+ // ⚠️ 追修批:这一步必须**从货架重播**再收窄(`reseedFromShelf`)—— 第 2 步若已把副本收窄到 7,
957
+ // 就地再收窄一次永远只能得 7(`kept ⊆ universe`)⇒ `--internal-roles` 装不回来、
958
+ // 且自检会拿"期望 14 ≠ 落盘 7"判**假红**。货架恒全集,收窄按目标集合。
959
+ // ⚠️ 不变式(落盘不得含内部角色,未授权时)在这一步同样自检,违反 ⇒ 判红。
960
+ if (presetMechanism === MECHANISM.ROWS) {
961
+ // ⚠️ W9:同上 —— 收尾重施的落点也必须是**包名目录**(两个名字都找)。
962
+ const bundleDir = resolveBundleDir(PROFILE_DIR)
963
+ const want = (UPGRADE_PRESET_LANDING && UPGRADE_PRESET_LANDING.kept.length)
964
+ ? UPGRADE_PRESET_LANDING.kept
965
+ : PRESET_INSTALLED.ids
966
+ if (PENDING_PRESET_REAPPLY && want.length && existsSync(bundleDir)) {
967
+ const shelfSrc = join(ARGENT_HOME, 'plugins', 'argent-presets')
968
+ const mat = materializeBundleDir(bundleDir, shelfSrc)
969
+ if (!mat.ok) {
970
+ steps.push({ ok: false, text: `角色落盘集合收尾未通过:${mat.reason}` })
971
+ } else {
972
+ const why = `${(UPGRADE_PRESET_LANDING && UPGRADE_PRESET_LANDING.why) || '升级按选择痕迹'}|收尾重施(doctor 之后)`
973
+ const sel = applyPresetSelection(bundleDir, want, { why, log: () => {}, shelfSrc, allowInternal: PRESET_ALLOW_INTERNAL, reseedFromShelf: true })
974
+ if (sel.refused) {
975
+ steps.push({ ok: false, text: `角色落盘集合收尾**判红并拒绝写出**:${sel.note}` })
976
+ } else {
977
+ const chk = checkPresetSelectionConsistency(bundleDir, want)
978
+ steps.push({
979
+ ok: chk.ok,
980
+ text: `角色落盘集合收尾:${chk.shelf.length} 个${sel.reseeded ? '(已按货架重播后收窄)' : mat.materialized ? '(副本曾被包管理器换成软链 ⇒ 已实体化)' : ''}`
981
+ + (chk.ok ? ';三处集合 + 痕迹一致' : `;⚠ 不一致:${chk.problems.join(';')}`),
982
+ })
983
+ }
984
+ }
985
+ }
986
+ }
987
+
988
+ // 8) 自动重启(--restart,macOS launchd)
989
+ if (FLAG_RESTART && restartRequired && isMac && !doctorBlocked) {
990
+ const lt = runCmdSafe('launchctl', ['list'])
991
+ if (lt.ok && lt.stdout.includes(RESTART_LABEL)) {
992
+ const uid = process.getuid?.() ?? 501
993
+ result.restartScheduled = true
994
+ result.restartHint = `已调度自动重启(${RESTART_LABEL},约 3 秒后执行,服务自动恢复)`
995
+ setTimeout(() => {
996
+ try {
997
+ const kid = spawn('launchctl', ['kickstart', '-k', `gui/${uid}/${RESTART_LABEL}`], { detached: true, stdio: 'ignore' })
998
+ kid.unref()
999
+ } catch { /* 忽略 */ }
1000
+ }, 3000)
1001
+ } else {
1002
+ result.restartHint = '未检测到 launchd 托管(com.whyshu.dsh-web),请手动重启'
1003
+ }
1004
+ }
1005
+ out(result)
1006
+ }
1007
+
1008
+ // ── help ──────────────────────────────────────────────────
1009
+ // ⚠️ CLI 只在**被直接执行**时跑:`scripts/tests/upgrade-skill-whitelist.test.mjs` 要 `import` 本模块的
1010
+ // 纯函数(`syncSkills` / `classifySkills`)做离线回归 —— 若无条件执行,import 一次就打印一遍 help
1011
+ // 并可能按测试进程的 argv 误跑 `run`(那是会写盘的)。
1012
+ // 「是否直接执行」= 共享判据 `scripts/lib/is-direct-run.mjs`(realpath 比对;软链下不退化为"静默不跑")。
1013
+ // 历史:本文件曾内联这份实现,而其它脚本仍用 `pathToFileURL(argv[1]) === import.meta.url`
1014
+ // (`/tmp` → `/private/tmp`、`/var/folders` → `/private/var/folders` 时**假否定** ⇒ CLI 静默不跑)。
1015
+ // 2026-10-04 起**唯一实现**在 helper 里,全仓库 scripts/*.mjs 共用(回归 scripts/tests/is-direct-run.test.mjs)。
1016
+ function main() {
1017
+ if (MODE === 'check') doCheck()
1018
+ else if (MODE === 'run') doRun().catch((e) => { console.error(`✗ 升级执行失败:${String((e && e.message) || e).split('\n')[0]}`); process.exitCode = 1 })
1019
+ else if (MODE === 'migrate-deps') doMigrateDeps().catch((e) => { console.error(`✗ 依赖迁移失败:${String((e && e.message) || e).split('\n')[0]}`); process.exitCode = 1 })
1020
+ else help()
1021
+ }
1022
+ function help() {
1023
+ console.log(`Argent 自举升级器(v0.9)
1024
+ 用法:
1025
+ node argent-upgrade.mjs check 检测远端版本
1026
+ node argent-upgrade.mjs run [--restart] 完整升级(--restart=完成后自动重启 dsh web)
1027
+ node argent-upgrade.mjs run --json JSON 输出
1028
+ node argent-upgrade.mjs migrate-deps [--apply] [--allow-package-manager] [--data-dir <dir>]
1029
+ 存量迁移:profile 的**遗留 \`file:\` 依赖** ⇒ npm 名(\`@whyshu/…\`)。
1030
+ ⛔ 默认**干跑**(先打印"改哪些依赖 / 是否跑包管理器 / 影响哪些 face");
1031
+ 加 \`--apply\` 才写:**写前备份(时间戳 + sha256 + 回滚命令)→ 改 manifest
1032
+ → 读回验收 → 审计 JSONL**;**幂等**(已迁过 ⇒ 零写入,连 mtime 都不动)。
1033
+ ⛔ 默认**不跑包管理器**(红线:裸跑 pnpm 会把 profile 弄坏);
1034
+ 要动它必须显式 \`--allow-package-manager\`(沿用 profile-doctor 的口径)。
1035
+ node argent-upgrade.mjs run --with-profile-fix
1036
+ 显式允许升级器修 profile 组合(= profile-doctor --fix)。
1037
+ ⛔ 默认**不修**(只做只读体检):该修复会重指 file: 依赖,
1038
+ 必要时还会跑包管理器(dsh plugin install = pnpm install),
1039
+ 在 web profile 上属红线动作(见 ~/.dsh/AGENTS.md,2026-10-03 事故)。
1040
+ 开了开关会**先打印**它要改什么、是否跑包管理器,再执行。
1041
+ 技能同步: **只发外发集**(外发 = verticals/*/manifest.json#skills 的并集;判据见 scripts/check-skill-classification.mjs)。
1042
+ 内部角色技能(skills/internal.json)**不外发**;宿主技能根里已有的内部/旧技能**只报告不删**。
1043
+ 环境: ARGENT_DISH_HOME(别名 ARGENT_HOME)/ DSH_HOME / DSH_PROFILE 可覆盖(默认 ~/.argent-dsh / ~/.dsh / web)`)
1044
+ }
1045
+
1046
+ if (isDirectRun(import.meta.url)) main()