@whyshu/argent-engine 0.0.0-stage → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/MANIFEST.json +341 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +576 -0
  55. package/engine/scripts/billing-mode-daily.sh +13 -0
  56. package/engine/scripts/build-engine-package.mjs +126 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +378 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/collect-evidence.mjs +42 -0
  81. package/engine/scripts/config-explain.mjs +278 -0
  82. package/engine/scripts/configure-provider.rb +29 -0
  83. package/engine/scripts/customer-config-migrate.mjs +179 -0
  84. package/engine/scripts/customer-preflight.py +892 -0
  85. package/engine/scripts/customer-readiness.sh +135 -0
  86. package/engine/scripts/daily-report-inbox.py +242 -0
  87. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  88. package/engine/scripts/daily-report.mjs +1310 -0
  89. package/engine/scripts/daily-report.sh +47 -0
  90. package/engine/scripts/data/lip-product-dict.json +306 -0
  91. package/engine/scripts/deliver-customer.mjs +932 -0
  92. package/engine/scripts/delivery-faces.txt +31 -0
  93. package/engine/scripts/desktop-trial.sh +146 -0
  94. package/engine/scripts/dev-window.sh +261 -0
  95. package/engine/scripts/drill-clean-machine.sh +315 -0
  96. package/engine/scripts/dsh-api-client.mjs +215 -0
  97. package/engine/scripts/dsh-backup-scope.sh +83 -0
  98. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  99. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  100. package/engine/scripts/dsh-plane.mjs +153 -0
  101. package/engine/scripts/dsh-portforward.py +223 -0
  102. package/engine/scripts/dsh-preflight.mjs +179 -0
  103. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  104. package/engine/scripts/dsh-release-watch.mjs +458 -0
  105. package/engine/scripts/dsh-remote-check.sh +99 -0
  106. package/engine/scripts/dsh-restart.sh +179 -0
  107. package/engine/scripts/dsh-restore.sh +111 -0
  108. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  109. package/engine/scripts/dsh-snapshot.mjs +116 -0
  110. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  111. package/engine/scripts/dsh-upgrade.mjs +612 -0
  112. package/engine/scripts/dsh-url.sh +86 -0
  113. package/engine/scripts/dsh-web-launch.mjs +155 -0
  114. package/engine/scripts/ensure-store-windows.py +509 -0
  115. package/engine/scripts/entitlements.py +458 -0
  116. package/engine/scripts/env_probe.py +411 -0
  117. package/engine/scripts/feishu-card.mjs +252 -0
  118. package/engine/scripts/final-check.mjs +43 -0
  119. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  120. package/engine/scripts/flow-auto-retry.py +454 -0
  121. package/engine/scripts/flow-requirements.json +735 -0
  122. package/engine/scripts/flow-scaffold.py +275 -0
  123. package/engine/scripts/flow-timers-plist.sh +87 -0
  124. package/engine/scripts/flow-timers.sh +184 -0
  125. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  126. package/engine/scripts/flow-trigger.sh +571 -0
  127. package/engine/scripts/flow_billing.py +354 -0
  128. package/engine/scripts/flow_capability_stats.py +314 -0
  129. package/engine/scripts/flow_case.py +194 -0
  130. package/engine/scripts/flow_claim.py +497 -0
  131. package/engine/scripts/flow_config.py +1025 -0
  132. package/engine/scripts/flow_failure.py +588 -0
  133. package/engine/scripts/flow_learn.py +105 -0
  134. package/engine/scripts/flow_ledger.py +235 -0
  135. package/engine/scripts/flow_policy.py +238 -0
  136. package/engine/scripts/flow_preflight.py +339 -0
  137. package/engine/scripts/flow_registry.py +341 -0
  138. package/engine/scripts/flow_retry.py +304 -0
  139. package/engine/scripts/flow_return_to_ops.py +191 -0
  140. package/engine/scripts/flow_source.py +315 -0
  141. package/engine/scripts/flow_sources/__init__.py +53 -0
  142. package/engine/scripts/flow_sources/cases_json.py +106 -0
  143. package/engine/scripts/flow_sources/common.py +428 -0
  144. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  145. package/engine/scripts/flow_stale.py +185 -0
  146. package/engine/scripts/flow_status_reconcile.py +344 -0
  147. package/engine/scripts/flow_triage.py +849 -0
  148. package/engine/scripts/flow_write_verify.py +496 -0
  149. package/engine/scripts/fulfillment.js +586 -0
  150. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  151. package/engine/scripts/gen-preset-rows.mjs +259 -0
  152. package/engine/scripts/init-ml.sh +2278 -0
  153. package/engine/scripts/install-internal-role.sh +190 -0
  154. package/engine/scripts/install-license.mjs +641 -0
  155. package/engine/scripts/install-one-shot.mjs +665 -0
  156. package/engine/scripts/integration_contract.py +220 -0
  157. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  158. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  159. package/engine/scripts/invoice-mapping.example.json +40 -0
  160. package/engine/scripts/invoice-table.sh +195 -0
  161. package/engine/scripts/invoice_carrier_notice.py +279 -0
  162. package/engine/scripts/invoice_fail_closeout.py +297 -0
  163. package/engine/scripts/invoice_lip_runner.py +1508 -0
  164. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  165. package/engine/scripts/invoice_ready_audit.py +191 -0
  166. package/engine/scripts/invoice_runner.py +675 -0
  167. package/engine/scripts/invoice_stageb.py +516 -0
  168. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  169. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  170. package/engine/scripts/lease.py +377 -0
  171. package/engine/scripts/lib/agent-install.mjs +649 -0
  172. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  173. package/engine/scripts/lib/argent-datadir.sh +18 -0
  174. package/engine/scripts/lib/argent_datadir.py +285 -0
  175. package/engine/scripts/lib/argent_filelock.py +507 -0
  176. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  177. package/engine/scripts/lib/cdp_ports.py +197 -0
  178. package/engine/scripts/lib/customer-env.mjs +102 -0
  179. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  180. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  181. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  182. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  183. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  184. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  185. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  186. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  187. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  188. package/engine/scripts/lib/patch-compose.mjs +241 -0
  189. package/engine/scripts/lib/patch-target.mjs +35 -0
  190. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  191. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  192. package/engine/scripts/lib/preset-compat.mjs +127 -0
  193. package/engine/scripts/lib/preset-install.mjs +575 -0
  194. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  195. package/engine/scripts/lib/preset-sync.mjs +59 -0
  196. package/engine/scripts/lib/profile-retire.mjs +97 -0
  197. package/engine/scripts/lib/python-runtime.mjs +190 -0
  198. package/engine/scripts/lib/registry.mjs +72 -0
  199. package/engine/scripts/lib/repo-root.sh +56 -0
  200. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  201. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  202. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  203. package/engine/scripts/lib/settings-panels.mjs +816 -0
  204. package/engine/scripts/lib/stdio_utf8.py +86 -0
  205. package/engine/scripts/lib/wake-auth.mjs +392 -0
  206. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  207. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  208. package/engine/scripts/license-e2e-drill.mjs +426 -0
  209. package/engine/scripts/license-gate.mjs +245 -0
  210. package/engine/scripts/lip_backlog.py +171 -0
  211. package/engine/scripts/lip_table.py +185 -0
  212. package/engine/scripts/local-usage.mjs +214 -0
  213. package/engine/scripts/migrate-providers.mjs +127 -0
  214. package/engine/scripts/migrate-skills.sh +65 -0
  215. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  216. package/engine/scripts/ml-store-ids.json +26 -0
  217. package/engine/scripts/ml_artifact_attribution.py +333 -0
  218. package/engine/scripts/ml_capacity_probe.py +1158 -0
  219. package/engine/scripts/ml_hub_status.py +1556 -0
  220. package/engine/scripts/ml_schema_check.py +115 -0
  221. package/engine/scripts/ml_store_ids.py +139 -0
  222. package/engine/scripts/npm-publish-argent.mjs +499 -0
  223. package/engine/scripts/npm-publish-argent.sh +20 -0
  224. package/engine/scripts/orchestrator.py +13811 -0
  225. package/engine/scripts/pack-plugins.mjs +140 -0
  226. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  227. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  228. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  229. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  230. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  231. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  232. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  233. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  234. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  235. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  236. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  237. package/engine/scripts/plan-role-rename.mjs +125 -0
  238. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  239. package/engine/scripts/plugin-composition.json +15 -0
  240. package/engine/scripts/poc-mcp-host.mjs +455 -0
  241. package/engine/scripts/poll-fulfillment.sh +295 -0
  242. package/engine/scripts/poll-invoice.sh +76 -0
  243. package/engine/scripts/profile-doctor.mjs +686 -0
  244. package/engine/scripts/provider-select.sh +71 -0
  245. package/engine/scripts/provider-switch.sh +146 -0
  246. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  247. package/engine/scripts/python-runtime.sh +78 -0
  248. package/engine/scripts/query_runner.py +310 -0
  249. package/engine/scripts/release-argent.sh +574 -0
  250. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  251. package/engine/scripts/retire-inventory.mjs +40 -0
  252. package/engine/scripts/run-tests.sh +257 -0
  253. package/engine/scripts/s1-audit.mjs +85 -0
  254. package/engine/scripts/self-sign-license.mjs +179 -0
  255. package/engine/scripts/session-health-scan.mjs +63 -0
  256. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  257. package/engine/scripts/setup.sh +22 -0
  258. package/engine/scripts/skill-install.mjs +629 -0
  259. package/engine/scripts/sku_translate.py +148 -0
  260. package/engine/scripts/sync-plugins.sh +90 -0
  261. package/engine/scripts/sync-presets.sh +27 -0
  262. package/engine/scripts/sync-scripts.sh +445 -0
  263. package/engine/scripts/telemetry.py +873 -0
  264. package/engine/scripts/ui-copy-count.mjs +220 -0
  265. package/engine/scripts/usage-anchor.sh +137 -0
  266. package/engine/scripts/usage_report.py +531 -0
  267. package/engine/scripts/validate-preset-schema.mjs +208 -0
  268. package/engine/scripts/vertical-init.mjs +323 -0
  269. package/engine/scripts/vertical-lint.mjs +482 -0
  270. package/engine/scripts/wenyao-browser.sh +74 -0
  271. package/engine/scripts/wenyao-fixture-export.py +116 -0
  272. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  273. package/engine/scripts/wenyao-login.sh +35 -0
  274. package/engine/scripts/wenyao-template-map.json +292 -0
  275. package/engine/scripts/wenyao_attach.py +131 -0
  276. package/engine/scripts/wenyao_download_template.py +144 -0
  277. package/engine/scripts/wenyao_fill_template.py +568 -0
  278. package/engine/scripts/wenyao_login.py +145 -0
  279. package/engine/scripts/wenyao_portal.py +633 -0
  280. package/engine/scripts/wenyao_recon.py +230 -0
  281. package/engine/scripts/wenyao_run.py +503 -0
  282. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  283. package/engine/scripts/wenyao_skill_contract.py +156 -0
  284. package/engine/scripts/wenyao_template_fetch.py +175 -0
  285. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  286. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  287. package/engine/scripts/ziniao-keepalive.sh +217 -0
  288. package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
  289. package/engine/scripts/ziniao-windows.sh +154 -0
  290. package/engine/skills/README.md +86 -0
  291. package/engine/skills/argent-dev/SKILL.md +41 -0
  292. package/engine/skills/internal.json +37 -0
  293. package/engine/skills/lktop-pm/SKILL.md +35 -0
  294. package/engine/skills/lktopapp-dev/SKILL.md +34 -0
  295. package/engine/skills/ml-config-guide/SKILL.md +50 -0
  296. package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
  297. package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
  298. package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
  299. package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
  300. package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
  301. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
  302. package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
  303. package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
  304. package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
  305. package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
  306. package/engine/skills/ml-invoice/SKILL.md +506 -0
  307. package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
  308. package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
  309. package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
  310. package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
  311. package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
  312. package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
  313. package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
  314. package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
  315. package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
  316. package/engine/skills/pet-dev/SKILL.md +42 -0
  317. package/engine/skills/whyshu-pm/SKILL.md +35 -0
  318. package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
  319. package/engine/version.mjs +1 -0
  320. package/engine/verticals/README.md +103 -0
  321. package/engine/verticals/_engine-words.baseline.txt +237 -0
  322. package/engine/verticals/_engine-words.json +19 -0
  323. package/engine/verticals/mercadolibre/README.md +43 -0
  324. package/engine/verticals/mercadolibre/config.json +709 -0
  325. package/engine/verticals/mercadolibre/flows.json +738 -0
  326. package/engine/verticals/mercadolibre/manifest.json +538 -0
  327. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  328. package/engine/verticals/mercadolibre/preflight.py +143 -0
  329. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  330. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  331. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  332. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  333. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  334. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  335. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  336. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  337. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  338. package/package.json +22 -4
  339. package/requirements.txt +5 -0
  340. package/README.md +0 -3
@@ -0,0 +1,575 @@
1
+ /**
2
+ * 角色 preset 安装/同步(**按 dsh 机制分派**,唯一真源)。
3
+ *
4
+ * 两套机制的安装形态完全不同(见 lib/preset-mechanism.mjs):
5
+ *
6
+ * 目录式(0.1.5 及更早)
7
+ * · 拷 `presets/<id>/` → `$DSH_HOME/.agent-presets/<id>/`
8
+ * · `settings.yaml` 的 `agent-presets.default` 设默认角色
9
+ *
10
+ * 行式(0.1.7+)
11
+ * · 拷 `plugins/argent-presets/` → `<profile>/node_modules/argent-presets`
12
+ * · 登记进 `<profile>/package.json` 的 `dependencies` + `dsh.profile.bundles`
13
+ * (bundle 的 `dsh.bundle.patch` 才会被加载 ⇒ preset 行才被 insert)
14
+ * · 用 profile patch 覆盖注册表默认值(`- id: agent-preset-registry` + `config.default`)
15
+ * · `~/.dsh/.agent-presets/` **不再使用**(留着也是惰性的)
16
+ */
17
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
18
+ import { basename, dirname, join } from 'node:path'
19
+ import { MECHANISM } from './preset-mechanism.mjs'
20
+ import { npmNameOf } from './plugin-packages.mjs'
21
+
22
+ /** 仓库里的**目录名**(`plugins/argent-presets`)——W9 之后它**不再等于**包名,⛔ 不要混用。 */
23
+ export const BUNDLE_DIR_NAME = 'argent-presets'
24
+ /**
25
+ * 本 bundle 的 **npm 包名**(W9,2026-10-12)—— profile 里三处**必须**用它:
26
+ * · `node_modules/<包名>` 落位路径;· `dependencies` 的**键**;· `dsh.profile.bundles` 的**条目**。
27
+ * 旧裸名(`argent-presets`)是**存量形态**:读侧一律两个都找(见 {@link resolveBundleDir}),
28
+ * 写侧一律迁到新名(见 `installPresetRows`)。口径见 `scripts/lib/plugin-packages.mjs`。
29
+ */
30
+ export const BUNDLE_NPM_NAME = npmNameOf(BUNDLE_DIR_NAME) || BUNDLE_DIR_NAME
31
+ /** 兼容别名:老调用方/夹具里的 `BUNDLE_NAME` 语义 = **包名**(写侧的规范形态)。 */
32
+ const BUNDLE_NAME = BUNDLE_NPM_NAME
33
+
34
+ /**
35
+ * profile 里本 bundle 的实际目录(**两个名字都找**;都没落 ⇒ 回**规范名**的路径给调用方判"缺")。
36
+ * 为什么要双认:存量机器上是裸名目录,新装是 scoped 目录 —— 读侧任何一处只认一个名字,
37
+ * 都会在另一半机器上判成"没装"(⛔ 且不报错,只是 preset 静默不挂)。
38
+ */
39
+ export function resolveBundleDir(profileDir, { exists = existsSync } = {}) {
40
+ for (const n of [BUNDLE_NPM_NAME, BUNDLE_DIR_NAME]) {
41
+ const d = join(profileDir, 'node_modules', ...String(n).split('/'))
42
+ if (exists(d)) return d
43
+ }
44
+ return join(profileDir, 'node_modules', ...String(BUNDLE_NPM_NAME).split('/'))
45
+ }
46
+ const REGISTRY_ROW_ID = 'agent-preset-registry'
47
+ /** 落盘集合的唯一留痕文件名(**已装集的唯一判据**,见 `installedSelectionIds`)。 */
48
+ export const SELECTION_FILE = 'presets.selection.json'
49
+ const MARK = '# ── argent-presets:注册表默认角色(由安装器维护,勿手改)──'
50
+
51
+ /**
52
+ * 全新 profile 的**基座 bundle**(P0-④,2026-10-08)。
53
+ *
54
+ * 为什么是这两条:`@deepseek-ai/dsh-base` 提供 agentPresets / llm / tools / webServer 这些服务,
55
+ * `@deepseek-ai/dsh-web-app` 提供 web 应用层与界面——**缺了它们,profile 引导根本不知道要组合哪些
56
+ * bundle**,运行期表现为 `16 entries did not activate`(13 个 preset 行 `waiting for service:
57
+ * agentPresets`、`llm-whyshu` `waiting for service: llm`…)⇒ 端口通、`/api` 401 正常,但 **Argent
58
+ * 一个条目都没起来**(实测现场见 docs/Argent-Windows-安装与HR能力验证-2026-10-08.md §3.8)。
59
+ * dsh 自己的 `initProfile()`(DEFAULT_PROFILE_BUNDLES)只给 dsh-base,这里按 web 实例补齐 web-app。
60
+ */
61
+ export const BASE_BUNDLES = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']
62
+
63
+ function readJson(p, dflt = null) {
64
+ try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return dflt }
65
+ }
66
+
67
+ /**
68
+ * 幂等确保 profile 的 `package.json` 存在且**含基座 bundle**(缺则补在最前,不动其余键)。
69
+ *
70
+ * ⚠️ 为什么"补在最前":`dsh.profile.bundles` 的顺序 = 各 bundle patch 的**应用顺序**(基座先、
71
+ * preset 行/皮肤后),所以基座不能被 append 到 `argent-presets` 后面。
72
+ * @returns {{ path: string, created: boolean, added: string[], changed: boolean }}
73
+ */
74
+ export function ensureBaseBundles(profileDir, { bundles: want = BASE_BUNDLES, log = () => {} } = {}) {
75
+ const pkgPath = join(profileDir, 'package.json')
76
+ const created = !existsSync(pkgPath)
77
+ const pkg = readJson(pkgPath, null) || { name: `dsh-profile-${basename(profileDir)}`, private: true }
78
+ pkg.name = pkg.name || `dsh-profile-${basename(profileDir)}`
79
+ if (pkg.private !== true) pkg.private = true
80
+ pkg.dependencies = pkg.dependencies || {}
81
+ pkg.dsh = pkg.dsh || {}
82
+ pkg.dsh.profile = pkg.dsh.profile || {}
83
+ const list = Array.isArray(pkg.dsh.profile.bundles) ? pkg.dsh.profile.bundles : []
84
+ const added = want.filter((b) => !list.includes(b))
85
+ pkg.dsh.profile.bundles = [...added, ...list]
86
+ const text = `${JSON.stringify(pkg, null, 2)}\n`
87
+ let before = ''
88
+ try { before = readFileSync(pkgPath, 'utf8') } catch { /* 新建 */ }
89
+ if (before !== text) writeFileSync(pkgPath, text)
90
+ if (added.length) {
91
+ log(` profile 基座 bundle 已登记:${added.join(', ')}${created ? '(新建 package.json)' : ''}`)
92
+ }
93
+ return { path: pkgPath, created, added, changed: before !== text, bundles: pkg.dsh.profile.bundles }
94
+ }
95
+
96
+ /** 目录式安装:拷角色目录 + 写 settings.yaml 默认值(0.1.5 行为,原样保留)。 */
97
+ export function installPresetDirectory({ repo, dshHome, presetRoot, presetId = 'argent-general', log = () => {} }) {
98
+ const src = join(repo, 'presets', presetId)
99
+ if (!existsSync(src)) return { ok: false, reason: `源角色不存在:${src}` }
100
+ mkdirSync(presetRoot, { recursive: true })
101
+ const target = join(presetRoot, presetId)
102
+ if (existsSync(target)) {
103
+ cpSync(src, `${target}.new`, { recursive: true })
104
+ rmSync(target, { recursive: true, force: true })
105
+ renameSync(`${target}.new`, target)
106
+ } else {
107
+ cpSync(src, target, { recursive: true })
108
+ }
109
+ log(` 角色已落位(目录式)→ ${target}`)
110
+
111
+ // settings.yaml 的默认角色(0.1.5 的默认 mode 机制)
112
+ const settingsPath = join(dshHome, 'settings.yaml')
113
+ let text = ''
114
+ try { text = readFileSync(settingsPath, 'utf8') } catch { /* 不存在则新建 */ }
115
+ if (!text.includes('agent-presets:')) {
116
+ writeFileSync(settingsPath, `${text.trimEnd()}\n\nagent-presets:\n default: ${presetId}\n`)
117
+ log(` 默认角色已写入 settings.yaml(agent-presets.default = ${presetId})`)
118
+ }
119
+ return { ok: true, target }
120
+ }
121
+
122
+ /**
123
+ * 行式 bundle 目录里**当前登记的行**(= `package.json#dsh.bundle.patch` 的内容)。
124
+ *
125
+ * ⛔ **不得用作"本机已装集"的判据**(缺陷 1 的机制就在这儿,2026-10-12 真机实测):
126
+ * ① 仓库/包**自带**的货架 `plugins/argent-presets/package.json` 里这 14 条**恒在**
127
+ * (它是"这个包能提供什么",不是"这台机器装了什么");
128
+ * ② pnpm 对 `file:` 依赖建的是**指向货架的链接农场**(实测:`node_modules/argent-presets`
129
+ * → `.pnpm/…/node_modules/argent-presets`,文件与仓库**同 inode**)⇒ 装完"读这份"读到的
130
+ * 就是货架的 14 —— 于是"已装集"被判成 14,收窄被自己的安装步骤抹掉。
131
+ * 要"本机已装了什么"请用 `installedSelectionIds()`(读选择痕迹)。
132
+ */
133
+ export function installedRowIds(bundleDir) {
134
+ const pkg = readJson(join(bundleDir, 'package.json'), null)
135
+ const list = (pkg && pkg.dsh && pkg.dsh.bundle && pkg.dsh.bundle.patch) || []
136
+ return list.map((rel) => basename(String(rel)).replace(/\.patch\.yml$/, '')).filter(Boolean)
137
+ }
138
+
139
+ /** 读「落盘选择痕迹」`presets.selection.json`(不存在/坏形状 ⇒ null)。 */
140
+ export function readPresetSelection(bundleDir) {
141
+ const doc = readJson(join(bundleDir, SELECTION_FILE), null)
142
+ if (!doc || typeof doc !== 'object' || !Array.isArray(doc.kept)) return null
143
+ return doc
144
+ }
145
+
146
+ /**
147
+ * bundle 的角色**元数据**(`presets.json` 的键 → 该角色的声明;读不到 ⇒ `{}`)。
148
+ * 不变式判据(`visibility: internal`)只读这里,⛔ 不另建第二份名单。
149
+ */
150
+ export function bundleShelfMeta(bundleDir) {
151
+ const doc = readJson(join(bundleDir, 'presets.json'), null)
152
+ return doc && typeof doc === 'object' ? doc : {}
153
+ }
154
+
155
+ /**
156
+ * `ids` 里**声明为内部**的角色(`visibility: internal`)。
157
+ *
158
+ * 判据只有一处:货架/包副本的 `presets.json`。缺声明的角色按**客户可见**处理
159
+ * (与 `scripts/argent-agents.mjs#presetSelectionForAgents` 的默认值一致 —— 一个判据两处用,
160
+ * ⛔ 不在这里另立一套默认值,否则两处会各自漂移)。
161
+ */
162
+ export function internalIdsAmong(bundleDir, ids) {
163
+ const meta = bundleShelfMeta(bundleDir)
164
+ return [...ids].filter((id) => (meta[id] || {}).visibility === 'internal')
165
+ }
166
+
167
+ /** `agents.json` 里各 agent 的 `roles` **并集**(坏形状/缺失 ⇒ 空集)。 */
168
+ export function agentRoleUnion(ledger) {
169
+ const agents = (ledger && Array.isArray(ledger.agents)) ? ledger.agents : []
170
+ const out = []
171
+ for (const a of agents) for (const r of ((a && a.roles) || [])) if (!out.includes(String(r))) out.push(String(r))
172
+ return out
173
+ }
174
+
175
+ /** `agents.json#internalRoles === true`(唯一判据;缺文件/坏形状 ⇒ false)。 */
176
+ export function ledgerAllowsInternal(ledger) {
177
+ return !!(ledger && ledger.internalRoles === true)
178
+ }
179
+
180
+ /**
181
+ * **已装集的唯一来源**(升级路径与安装路径共用这一份实现;2026-10-12 追修批)。
182
+ *
183
+ * 允许的两个来源,**只有这两个**:
184
+ * ① **可信痕迹** `presets.selection.json#kept`;
185
+ * ② 痕迹不可用(缺失/坏形状/**不可信**)时 ⇒ `agents.json` 里各 agent 的 `roles` 并集;
186
+ * ③ 两者都没有 ⇒ **空集**(= 首次安装语义)。
187
+ * ⛔ **绝不允许**把"当前 profile 里的 patch 实体 / `presets.json` 行 / `dsh.bundle.patch` 条目"
188
+ * 当成已装集 —— 那三份都是**货架**("这个包能提供什么",恒 14),不是"这台机器装了什么"。
189
+ * 本缺陷与上一批缺陷的共同机制就是这条:`preset 同步(行式)`这一步把货架铺进 profile **之后**,
190
+ * 若拿铺进来的那份当"已装",则 `客户可见 ∪ 已装 ∪ 本智能体` 恒等于货架全集 ⇒ 收窄被自己的安装步骤抹掉。
191
+ *
192
+ * ⚠️ **"可信"是有判据的,不是"有文件就信"**(2026-10-12 winpc 现场的真实形态):
193
+ * 痕迹里含 `visibility: internal` 的角色、而本机 `internalRoles !== true`(且未给 `--internal-roles`)
194
+ * ⇒ 该痕迹**只可能由放大缺陷写出来**(现场:`kept` 14 个、`excluded` 空)⇒ 判为**不可信**,
195
+ * 按"痕迹缺失"处理(退回来源②)。这条**响亮报告**,⛔ 不静默采信、也不静默改痕迹。
196
+ *
197
+ * @param {object} o
198
+ * @param {string} o.bundleDir profile 副本目录(读痕迹 + 读角色元数据判内部角色)
199
+ * @param {object|null} [o.ledger] `agents.json` 的内容(调用方读;⛔ 本函数不猜 home)
200
+ * @param {boolean} [o.allowInternal] `--internal-roles` 或 `agents.json#internalRoles`
201
+ * @returns {{ids:string[], source:'trace'|'agents'|'none', trustedTrace:boolean, rejected:string, internalInTrace:string[], ledgerRoles:string[]}}
202
+ */
203
+ export function resolveInstalledSelection({ bundleDir, ledger = null, allowInternal = false }) {
204
+ const ledgerRoles = agentRoleUnion(ledger)
205
+ const fallback = {
206
+ ids: ledgerRoles, source: ledgerRoles.length ? 'agents' : 'none',
207
+ trustedTrace: false, rejected: '', internalInTrace: [], ledgerRoles,
208
+ }
209
+ const trace = readPresetSelection(bundleDir)
210
+ if (!trace) return fallback
211
+ const kept = trace.kept.map((x) => String(x)).filter(Boolean)
212
+ const internalInTrace = internalIdsAmong(bundleDir, kept)
213
+ if (internalInTrace.length && !allowInternal) {
214
+ return { ...fallback, rejected: `痕迹声称落了 ${internalInTrace.length} 个内部角色(${internalInTrace.join('、')}),`
215
+ + `但本机 internalRoles≠true 且未给 --internal-roles ⇒ **不可信**(只可能由"收窄被抹掉"的缺陷写出)` }
216
+ }
217
+ return { ...fallback, ids: kept, source: 'trace', trustedTrace: true, internalInTrace }
218
+ }
219
+
220
+ /**
221
+ * **落盘集合不得含内部角色**的不变式(硬断言;2026-10-12 追修批,防这一类复发)。
222
+ *
223
+ * 判据:`keep ∩ {visibility: internal}` 非空 ∧ 未授权 ⇒ 违反。
224
+ * 授权 = `--internal-roles`(安装路径)或 `agents.json#internalRoles === true`(升级路径),
225
+ * 由调用方算成 `allowInternal` 传进来(⛔ 本函数不自己读 CLI/账本,避免第二份判据)。
226
+ * @returns {{ok:boolean, offenders:string[], reason:string}}
227
+ */
228
+ export function checkNoInternalLanding(bundleDir, keep, { allowInternal = false } = {}) {
229
+ const offenders = internalIdsAmong(bundleDir, keep)
230
+ if (!offenders.length || allowInternal) return { ok: true, offenders: [], reason: '' }
231
+ return {
232
+ ok: false,
233
+ offenders,
234
+ reason: `落盘集合含 ${offenders.length} 个 visibility=internal 的角色(${offenders.join('、')}),`
235
+ + '但本机未授权内部角色(需 --internal-roles,或 agents.json#internalRoles = true)⇒ 判红并**拒绝写出**',
236
+ }
237
+ }
238
+
239
+ /**
240
+ * **选择痕迹的裸读**(`presets.selection.json#kept`;无痕迹 ⇒ 空集)。
241
+ *
242
+ * ⚠️ 它只回答"痕迹上写着什么",**不判可信性**。要"本机已装集"请用
243
+ * `resolveInstalledSelection()` —— 那才是唯一入口(可信痕迹 → `agents.json#roles` 并集 → 空集)。
244
+ *
245
+ * 为什么必须是痕迹而不是"包里的 patch 清单":见 `installedRowIds` 的两条机制。
246
+ * 没有痕迹 ≠ "装了货架全集",而是**没有可信的已装证据** ⇒ 空集(等价于"首次安装",
247
+ * 于是落盘集合精确等于该档的目标集合)。这条同时兜住两件事:
248
+ * · 全新机器:无痕迹 ⇒ 只落客户可见集(⛔ 不会因为货架自带 14 个 patch 就落 14 个);
249
+ * · 升级路径:pnpm/doctor 把 profile 副本换回货架(14)之后,**下一轮不会被判成"已装 14"
250
+ * 而把用户已收窄的集合放大回去**(回归④⑤)。
251
+ * @returns {string[]}
252
+ */
253
+ export function installedSelectionIds(bundleDir) {
254
+ const doc = readPresetSelection(bundleDir)
255
+ if (!doc) return []
256
+ return doc.kept.map((x) => String(x)).filter(Boolean)
257
+ }
258
+
259
+ /** bundle 里的 patch **实体文件**清单(排序;不依赖 package.json)。 */
260
+ export function bundlePatchEntities(bundleDir) {
261
+ try {
262
+ return readdirSync(join(bundleDir, 'presets'))
263
+ .filter((f) => f.endsWith('.patch.yml'))
264
+ .map((f) => f.replace(/\.patch\.yml$/, ''))
265
+ .sort()
266
+ } catch { return [] }
267
+ }
268
+
269
+ /** `bundleDir` 是不是**软链**(pnpm 的 `file:` 依赖就是这种形态)。 */
270
+ export function isLinkedBundle(bundleDir) {
271
+ try { return lstatSync(bundleDir).isSymbolicLink() } catch { return false }
272
+ }
273
+
274
+ /**
275
+ * 把**软链形态**的 profile 副本**实体化**(删链 + 从货架拷贝),返回是否动过手。
276
+ *
277
+ * ⚠️ 为什么必须有这一步(2026-10-12 实测,两条都真会发生):
278
+ * ① pnpm 建的链最终指向货架,而它可能是**硬链接农场** ⇒ 走链**写**(改 `presets.json` /
279
+ * `package.json`)会**改到仓库文件本身**(实测 inode 相同、内容当场被改);
280
+ * 删文件则只删链、仓库不动 —— 所以"半收窄"(行还在、实体没了)正是这样来的。
281
+ * ② 收窄必须落在**真实体**上,否则下次 pnpm/doctor 一跑又回到货架全集。
282
+ */
283
+ export function materializeBundleDir(bundleDir, shelfSrc) {
284
+ if (!isLinkedBundle(bundleDir)) return { materialized: false, ok: true }
285
+ if (!shelfSrc || !existsSync(shelfSrc)) {
286
+ return { materialized: false, ok: false, reason: `profile 副本是软链,但货架不可用(${shelfSrc || '未给 shelfSrc'})⇒ 拒绝就地改写(会改到仓库本身)` }
287
+ }
288
+ rmSync(bundleDir, { force: true }) // 只删链,不追进去
289
+ cpSync(shelfSrc, bundleDir, { recursive: true })
290
+ return { materialized: true, ok: true }
291
+ }
292
+
293
+ /**
294
+ * 把 profile 副本**按货架重播**(无论当前是软链还是真目录)。
295
+ *
296
+ * 为什么"收尾重施"必须重播而不是就地再收窄一次(2026-10-12 追修批实测):
297
+ * `applyPresetSelection` 的 universe 取自**当前副本**的 `presets.json` ⇒ 副本已被收窄到 7 时,
298
+ * 目标集合即使是 14 也只能落 7(`kept ⊆ universe`)⇒ ① `--internal-roles` 永远装不回来;
299
+ * ② 收尾自检会拿"期望 14 ≠ 落盘 7"判红(**假红**)。货架是**恒全集**、收窄是"按目标集合",
300
+ * 所以收尾这一步的正确姿势 = **货架 → 收窄**,⛔ 不是"在当前副本上再过滤一次"。
301
+ *
302
+ * ⚠️ 只删 profile 副本(`rmSync` 不追链);货架本身一个字节都不动。
303
+ */
304
+ export function reseedBundleDir(bundleDir, shelfSrc) {
305
+ if (!shelfSrc || !existsSync(shelfSrc)) {
306
+ return { ok: false, reseeded: false, reason: `货架不可用(${shelfSrc || '未给 shelfSrc'})⇒ 拒绝重播(否则 deps 里 file: 指向的目录会被清掉)` }
307
+ }
308
+ rmSync(bundleDir, { recursive: true, force: true })
309
+ mkdirSync(dirname(bundleDir), { recursive: true })
310
+ cpSync(shelfSrc, bundleDir, { recursive: true })
311
+ return { ok: true, reseeded: true }
312
+ }
313
+
314
+ /**
315
+ * 「三处集合相等」不变量 + 落盘痕迹一致性(回归⑦ / 收尾自检用)。
316
+ *
317
+ * 判据:`presets.json` 行 == `package.json#dsh.bundle.patch` == `presets/*.patch.yml` 实体
318
+ * == `presets.selection.json#kept`(四者相等,⛔ 缺一即"清单里有、注册表里没有"的假红/假绿)。
319
+ * @returns {{ok:boolean, kept:string[], shelf:string[], rows:string[], entities:string[], problems:string[]}}
320
+ */
321
+ export function checkPresetSelectionConsistency(bundleDir, expectKept = null) {
322
+ const shelf = bundleShelfIds(bundleDir)
323
+ const rows = installedRowIds(bundleDir)
324
+ const entities = bundlePatchEntities(bundleDir)
325
+ const sel = readPresetSelection(bundleDir)
326
+ const kept = sel ? sel.kept.map((x) => String(x)) : []
327
+ const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i])
328
+ const sorted = (xs) => [...xs].sort()
329
+ const problems = []
330
+ if (!sel) problems.push('缺 presets.selection.json(落盘集合没有痕迹 ⇒ 下一轮会被判成"无已装证据")')
331
+ if (shelf.length && !same(shelf, rows)) problems.push(`presets.json 行(${shelf.length}) ≠ package.json#dsh.bundle.patch(${rows.length})`)
332
+ if (shelf.length && !same(sorted(shelf), sorted(entities))) problems.push(`presets.json 行(${shelf.length}) ≠ patch 实体(${entities.length})`)
333
+ if (sel && !same(sorted(shelf), sorted(kept))) problems.push(`presets.selection.json#kept(${kept.length}) ≠ 落盘集合(${shelf.length})`)
334
+ if (expectKept && !same(sorted(expectKept), sorted(shelf))) problems.push(`期望集合(${expectKept.length}) ≠ 落盘集合(${shelf.length})`)
335
+ return { ok: problems.length === 0, kept, shelf, rows, entities, problems }
336
+ }
337
+
338
+ /** bundle 的完整角色清单(仓库产物的**货架**:`presets.json` 的键)。 */
339
+ export function bundleShelfIds(bundleDir) {
340
+ const shelf = readJson(join(bundleDir, 'presets.json'), null)
341
+ return shelf && typeof shelf === 'object' ? Object.keys(shelf) : []
342
+ }
343
+
344
+ /**
345
+ * 把**已落位**的行式 bundle 收窄到 `keep`(A 批,2026-10-10)。
346
+ *
347
+ * 为什么改的是 profile 里的**副本**而不是仓库:`dsh.bundle.patch` 决定"哪些 preset 行被 insert
348
+ * 进注册表",而用户口径是"选一个智能体装 ⇒ **只落它的角色**,其余不落"。仓库产物是**货架**
349
+ * (全集,13/14 个),每台机器落哪些是**装机决策** ⇒ 只能写 profile 副本。
350
+ *
351
+ * 三处必须**同时**收窄,否则会出现"清单里有、注册表里没有"的假红/假绿:
352
+ * ① `package.json#dsh.bundle.patch`(Loader 真正读的那份 —— 删掉即不 insert);
353
+ * ② `presets/<id>.patch.yml` 实体(删掉,防手工/工具把它重新挂回去);
354
+ * ③ `presets.json`(探针 `plugin-bootcheck` 与诊断读它取角色清单)。
355
+ * 另写 `presets.selection.json` 留痕(谁在什么时候因什么被排除 —— 可复核,且**下一轮
356
+ * "已装集"的唯一判据**,见 `installedSelectionIds`)。
357
+ *
358
+ * ⚠️ `shelfSrc`(货架目录,即仓库 `plugins/argent-presets`):`bundleDir` 是**软链**时必须给 ——
359
+ * 本函数会先把它**实体化**再改写;不给则**拒绝动手**(返回 `refused:true`),⛔ 绝不走链写进仓库。
360
+ *
361
+ * ## 不变式(2026-10-12 追修批,**每次调用都自检**)
362
+ * ① **落盘集合不得含 `visibility: internal` 的角色**,除非 `allowInternal`(= `--internal-roles`
363
+ * 或 `agents.json#internalRoles === true`);违反 ⇒ `ok:false, refused:true` 且**一个字节都不写**
364
+ * (⛔ 不静默剔除、不静默修 —— 静默修正好会掩盖"痕迹不可信/参数没传对"这类真问题);
365
+ * ② 写完(或无需收窄时)自检 **patch 实体 == `presets.json` 行 == `dsh.bundle.patch` == 痕迹**
366
+ * 四者相等,结果放进 `consistency`(调用方据此判红)。
367
+ *
368
+ * @param {object} o
369
+ * @param {boolean} [o.allowInternal=false] 授权内部角色(默认**否** = fail-closed)
370
+ * @param {boolean} [o.reseedFromShelf=false] 先从 `shelfSrc` **重播**副本再收窄(收尾重施用;见 `reseedBundleDir`)
371
+ * @returns {{kept: string[], excluded: string[], note: string, ok?: boolean, refused?: boolean,
372
+ * materialized?: boolean, reseeded?: boolean, missing?: string[], consistency?: object}}
373
+ */
374
+ export function applyPresetSelection(bundleDir, keep, { why = '', log = () => {}, shelfSrc = '', allowInternal = false, reseedFromShelf = false } = {}) {
375
+ let materialized = false
376
+ let reseeded = false
377
+ if (reseedFromShelf && shelfSrc) {
378
+ const rs = reseedBundleDir(bundleDir, shelfSrc)
379
+ if (!rs.ok) {
380
+ log(` ⚠️ preset 落盘集合**未收窄**:${rs.reason}`)
381
+ return { kept: [], excluded: [], note: rs.reason, refused: true, ok: false }
382
+ }
383
+ reseeded = true
384
+ }
385
+ if (isLinkedBundle(bundleDir)) {
386
+ const mat = materializeBundleDir(bundleDir, shelfSrc)
387
+ if (!mat.ok) {
388
+ log(` ⚠️ preset 落盘集合**未收窄**:${mat.reason}`)
389
+ return { kept: [], excluded: [], note: mat.reason, refused: true, ok: false }
390
+ }
391
+ materialized = true
392
+ log(' ℹ️ profile 副本是软链(pnpm 的 `file:` 形态)⇒ 已实体化再从货架收窄')
393
+ }
394
+ const shelf = bundleShelfIds(bundleDir)
395
+ const current = installedRowIds(bundleDir)
396
+ const universe = shelf.length ? shelf : current
397
+ if (!universe.length) return { kept: [], excluded: [], note: 'bundle 里没有角色清单(presets.json/package.json 都读不到)', materialized, reseeded, ok: false }
398
+ const asked = [...new Set([...keep].map((x) => String(x)).filter(Boolean))]
399
+ const keepSet = new Set(asked.filter((id) => universe.includes(id)))
400
+ const kept = universe.filter((id) => keepSet.has(id))
401
+ const excluded = universe.filter((id) => !keepSet.has(id))
402
+ const missing = asked.filter((id) => !universe.includes(id))
403
+
404
+ // ── 不变式①:落盘集合不得含内部角色(未授权时)。**写之前**判,违反 ⇒ 拒绝写出。──────
405
+ const inv = checkNoInternalLanding(bundleDir, kept, { allowInternal })
406
+ if (!inv.ok) {
407
+ log(` ✗ preset 落盘集合**被判红并拒绝写出**:${inv.reason}`)
408
+ return { kept: [], excluded: [], note: inv.reason, refused: true, ok: false, invariant: inv, missing, reseeded, materialized }
409
+ }
410
+ if (missing.length) {
411
+ log(` ⚠️ 目标集合里有 ${missing.length} 个角色不在本副本(${missing.join('、')})——⛔ 未被落盘(收尾重施请给 shelfSrc + reseedFromShelf)`)
412
+ }
413
+
414
+ // ⚠️ `presets.selection.json` **无论是否收窄都要写**:它是"这台机器上落了哪些角色"的**唯一留痕**,
415
+ // 判据(回归/巡检)读它 —— 只在"有排除项"时才写会让"全集落盘"这一态**没有证据**(实测踩到)。
416
+ const writeSelection = () => writeFileSync(join(bundleDir, SELECTION_FILE), `${JSON.stringify({
417
+ kept, excluded, at: new Date().toISOString(), why: why || '(未说明)',
418
+ _note: 'profile 副本的 preset 落盘集合(安装器维护,勿手改)——**下一轮"已装集"的唯一判据**。要装回被排除的角色:给安装器 --internal-roles(内部角色)或 --agent <id> / --capability <id> 点名它的能力。',
419
+ }, null, 2)}\n`)
420
+ const done = (note) => {
421
+ // ── 不变式②:三处集合 + 痕迹必须四者相等(写完即自检,结果交调用方判红)──────────
422
+ const consistency = checkPresetSelectionConsistency(bundleDir, kept)
423
+ if (!consistency.ok) log(` ✗ 三处集合 + 痕迹**不一致**:${consistency.problems.join(';')}`)
424
+ return { kept, excluded, note, ok: consistency.ok, missing, materialized, reseeded, consistency }
425
+ }
426
+ if (!excluded.length) {
427
+ writeSelection()
428
+ log(` preset 落盘集合:${kept.length}/${universe.length}(全部保留,无需收窄)`)
429
+ return done('')
430
+ }
431
+ const pkgPath = join(bundleDir, 'package.json')
432
+ const pkg = readJson(pkgPath, null)
433
+ if (pkg) {
434
+ pkg.dsh = pkg.dsh || {}
435
+ pkg.dsh.bundle = pkg.dsh.bundle || {}
436
+ pkg.dsh.bundle.patch = kept.map((id) => `./presets/${id}.patch.yml`)
437
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
438
+ // ② patch 实体:排除项一律删(否则"文件还在"会被误读成"已装")
439
+ for (const id of excluded) rmSync(join(bundleDir, 'presets', `${id}.patch.yml`), { force: true })
440
+ // ③ 探针/诊断读的清单同步收窄
441
+ const shelfPath = join(bundleDir, 'presets.json')
442
+ const full = readJson(shelfPath, null)
443
+ if (full && typeof full === 'object') {
444
+ const narrowed = {}
445
+ for (const id of kept) if (full[id]) narrowed[id] = full[id]
446
+ writeFileSync(shelfPath, `${JSON.stringify(narrowed, null, 2)}\n`)
447
+ }
448
+ }
449
+ writeSelection()
450
+ log(` preset 落盘集合收窄:${kept.length}/${universe.length};**刻意未落** ${excluded.length} 个(${excluded.join('、')})`)
451
+ return done(why)
452
+ }
453
+
454
+ /**
455
+ * 行式安装:装 bundle + 登记 bundles/deps + 覆盖注册表默认值(0.1.7+)。
456
+ *
457
+ * ⚠️ **本函数落的收窄会被后续 pnpm 覆盖**(2026-10-12 真机实测):profile 的
458
+ * `dependencies.argent-presets = file:<仓库>/plugins/argent-presets`,而安装器第 7 步
459
+ * (`pnpm add file:…` 装 argent-core/skin)会把这个目录**换成指向货架的链**(14 个 patch 全回来、
460
+ * `presets.selection.json` 消失)⇒ "声称收窄、实际没落"。调用方**必须在所有会动 profile 的步骤
461
+ * 之后**再收尾施加一次(安装器 `install-argent.mjs` 第 7c2 步 / 升级器第 8 步),
462
+ * 且施加前先 `materializeBundleDir()`。
463
+ * @param {object} o
464
+ * @param {string[]|null} [o.onlyPresets] 给定时**只落**这些角色 id(A 批:按智能体装 / 内部角色策略);
465
+ * `null`/缺省 ⇒ 全部(**与改动前逐字节一致**,向后兼容)。
466
+ * @param {boolean} [o.allowInternal=false] 授权内部角色(`--internal-roles` / `agents.json#internalRoles`);
467
+ * 未授权时落盘集合若含内部角色 ⇒ `applyPresetSelection` 判红并拒绝写出(不变式,见该函数)。
468
+ * @returns {{ ok: boolean, copied?: string, bundlesAdded?: string[], registryDefault?: string, selection?: object, reason?: string }}
469
+ */
470
+ export function installPresetRows({ repo, profileDir, presetId = 'argent-general', onlyPresets = null, selectionWhy = '', allowInternal = false, log = () => {} }) {
471
+ const src = join(repo, 'plugins', BUNDLE_DIR_NAME)
472
+ if (!existsSync(src)) return { ok: false, reason: `缺行式产物:${src}(先跑 node scripts/gen-preset-rows.mjs)` }
473
+ const nm = join(profileDir, 'node_modules')
474
+ mkdirSync(nm, { recursive: true })
475
+ // ── W9:落位路径/依赖键/bundle 名一律用**包名**(scoped);旧裸名目录一并清掉并迁移 ──
476
+ const dest = join(nm, ...String(BUNDLE_NAME).split('/'))
477
+ rmSync(dest, { recursive: true, force: true })
478
+ if (BUNDLE_NAME !== BUNDLE_DIR_NAME) {
479
+ // 存量裸名目录/依赖键/bundles 条目:同一个 bundle 的旧名字 ⇒ 一起迁走(⛔ 留双份 = 重复挂载)
480
+ rmSync(join(nm, BUNDLE_DIR_NAME), { recursive: true, force: true })
481
+ }
482
+ cpSync(src, dest, { recursive: true })
483
+ log(` bundle 已落位(行式)→ ${dest}`)
484
+ let selection = null
485
+ if (Array.isArray(onlyPresets)) selection = applyPresetSelection(dest, onlyPresets, { why: selectionWhy, log, shelfSrc: src, allowInternal })
486
+ if (selection && selection.refused) {
487
+ // 不变式判红 ⇒ **不登记**(否则会留下"注册表里有一行、patch 实体已被清"的半态)
488
+ return { ok: false, reason: selection.note, selection, refused: true }
489
+ }
490
+
491
+ // package.json:**先**基座 bundle(缺则补、并排在最前),**再**登记本 bundle 与依赖。
492
+ // ⚠️ P0-④(2026-10-08):老实现以 `{name, private}` 默认值新建 package.json,**不带**任何
493
+ // `dsh.profile.bundles` ⇒ 安装器第 7 步那段"补基座"的引导(判据是 package.json 不存在)成了
494
+ // **死代码** ⇒ 全新机器启动后 `16 entries did not activate`,Argent 整体不工作。
495
+ ensureBaseBundles(profileDir, { log })
496
+ const pkgPath = join(profileDir, 'package.json')
497
+ const pkg = readJson(pkgPath, { name: 'dsh-profile-web', private: true })
498
+ pkg.dependencies = pkg.dependencies || {}
499
+ pkg.dependencies[BUNDLE_NAME] = `file:${src}`
500
+ // W9 迁移:旧裸名依赖键与旧 bundles 条目一起换掉(⛔ 同一个 bundle 不许留两个名字)
501
+ if (BUNDLE_NAME !== BUNDLE_DIR_NAME) delete pkg.dependencies[BUNDLE_DIR_NAME]
502
+ pkg.dsh = pkg.dsh || {}
503
+ pkg.dsh.profile = pkg.dsh.profile || {}
504
+ const bundles = Array.isArray(pkg.dsh.profile.bundles) ? pkg.dsh.profile.bundles : (pkg.dsh.profile.bundles = [])
505
+ const bundlesAdded = []
506
+ const oldIdx = BUNDLE_NAME === BUNDLE_DIR_NAME ? -1 : bundles.indexOf(BUNDLE_DIR_NAME)
507
+ if (oldIdx >= 0) bundles.splice(oldIdx, 1) // 原位删旧名(顺序即 patch 应用顺序 ⇒ 不把它挪到末尾)
508
+ if (!bundles.includes(BUNDLE_NAME)) {
509
+ if (oldIdx >= 0) bundles.splice(oldIdx, 0, BUNDLE_NAME)
510
+ else bundles.push(BUNDLE_NAME)
511
+ bundlesAdded.push(BUNDLE_NAME)
512
+ }
513
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
514
+ if (bundlesAdded.length) log(` bundles 已登记:${bundlesAdded.join(', ')}`)
515
+
516
+ // 注册表默认角色(profile patch 覆盖该行 config;0.1.7 的默认值机制)
517
+ const patchPath = join(profileDir, 'cordis.patch.yml')
518
+ let patch = ''
519
+ try { patch = readFileSync(patchPath, 'utf8') } catch { /* 新建 */ }
520
+ patch = upsertRegistryDefault(patch, presetId)
521
+ writeFileSync(patchPath, patch)
522
+ log(` 注册表默认角色已设:${presetId}`)
523
+
524
+ return { ok: true, copied: dest, bundlesAdded, registryDefault: presetId, selection }
525
+ }
526
+
527
+ /** 按机制分派。 */
528
+ export function installPresets({ mechanism, ...opts }) {
529
+ if (mechanism === MECHANISM.ROWS) return { mechanism, ...installPresetRows(opts) }
530
+ if (mechanism === MECHANISM.DIRECTORY) return { mechanism, ...installPresetDirectory(opts) }
531
+ return { mechanism, ok: false, reason: `未知 preset 机制(scope 里既无 dsh-agent-preset 也无 dsh-agent-presets)` }
532
+ }
533
+
534
+ /**
535
+ * 行式机器上,把**旧机制的残留**说清楚(不删,只报告):`~/.dsh/.agent-presets/` 里的目录
536
+ * 在 0.1.7 上不会再被扫描,留着是惰性的;但若有人以为"改目录就生效"会踩坑。
537
+ */
538
+ export function reportStaleDirectories(presetRoot, exists = existsSync) {
539
+ if (!exists(presetRoot)) return []
540
+ try {
541
+ return readdirSync(presetRoot).filter((d) => d.startsWith('argent-'))
542
+ } catch { return [] }
543
+ }
544
+
545
+ /**
546
+ * 幂等地写入"注册表默认角色"覆盖块。
547
+ * 逐行 upsert(不用正则):标记行 + 紧随其后的 `- id: agent-preset-registry` 行块整体替换,
548
+ * 保证重复安装不会留下第二个块(2026-09-28 实测:非贪婪正则只吃掉标记行、旧块残留 ⇒ 重复定义)。
549
+ */
550
+ export function upsertRegistryDefault(patchText, presetId, mark = MARK, rowId = REGISTRY_ROW_ID) {
551
+ const lines = String(patchText || '').split('\n')
552
+ const block = [mark, `- id: ${rowId}`, ' config:', ` default: ${presetId}`]
553
+ const out = []
554
+ let i = 0
555
+ let done = false
556
+ while (i < lines.length) {
557
+ if (lines[i].trim() === mark.trim()) {
558
+ i += 1
559
+ // 吃掉旧块:`- id: ...` 行 + 其后的缩进续行(config/default 等)
560
+ if (i < lines.length && /^- /.test(lines[i])) {
561
+ i += 1
562
+ while (i < lines.length && /^\s+\S/.test(lines[i])) i += 1
563
+ }
564
+ if (!done) { out.push(...block); done = true }
565
+ continue
566
+ }
567
+ out.push(lines[i])
568
+ i += 1
569
+ }
570
+ if (!done) {
571
+ while (out.length && out[out.length - 1].trim() === '') out.pop()
572
+ out.push('', ...block)
573
+ }
574
+ return `${out.join('\n').replace(/\n{3,}/g, '\n\n').replace(/\s*$/, '')}\n`
575
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 角色 preset 的两套声明机制(**唯一真源**)。
3
+ *
4
+ * dsh 0.1.5 与 0.1.7 的 preset 架构完全不同:
5
+ *
6
+ * | | 0.1.5(现网/客户机) | 0.1.7(开发机候选) |
7
+ * |---|---|---|
8
+ * | 声明位置 | `~/.dsh/.agent-presets/<id>/agent.cordis.yml`(**扫目录**) | profile 组合里的**一行插件** |
9
+ * | 载体 | `@deepseek-ai/dsh-agent-presets`(复数) | `@deepseek-ai/dsh-agent-preset`(单数)+ `-registry` |
10
+ * | 新增角色 | 拷目录 | 改组合(bundle patch 的 `insert` 行) |
11
+ *
12
+ * 依据(0.1.7 官方 README 原文):"The registry neither scans directories nor accepts preset
13
+ * paths. A new preset or an override of a shipped one is a bundle patch: an `insert` of a
14
+ * `@deepseek-ai/dsh-agent-preset` row"。
15
+ *
16
+ * **判定用"能力探测"而不是版本号**:版本字符串会漂(rc 线尤其),而"scope 里有没有
17
+ * `dsh-agent-preset` 这个包"是事实。0.1.7 实测:装上后 13 个目录式 preset 全部
18
+ * `Unknown agent preset`(挂载失败),因为没人再扫目录。
19
+ */
20
+ import { existsSync } from 'node:fs'
21
+ import { join } from 'node:path'
22
+
23
+ /** 机制常量。 */
24
+ export const MECHANISM = { ROWS: 'rows', DIRECTORY: 'directory' }
25
+
26
+ /**
27
+ * 探测某台机器(某 scope)支持哪套 preset 机制。
28
+ * @param {string} scopeDir `.../node_modules/@deepseek-ai`
29
+ * @param {(p: string) => boolean} exists
30
+ * @returns {'rows'|'directory'|'unknown'}
31
+ */
32
+ export function detectPresetMechanism(scopeDir, exists = existsSync) {
33
+ if (!scopeDir) return 'unknown'
34
+ if (exists(join(scopeDir, 'dsh-agent-preset'))) return MECHANISM.ROWS
35
+ if (exists(join(scopeDir, 'dsh-agent-presets'))) return MECHANISM.DIRECTORY
36
+ return 'unknown'
37
+ }
38
+
39
+ /** 该机制下 preset 的"安装形态"说明(日志/文档用,避免各处自己编词)。 */
40
+ export function mechanismNote(mechanism) {
41
+ return mechanism === MECHANISM.ROWS
42
+ ? '行式(profile 组合里的 @deepseek-ai/dsh-agent-preset 行;bundle patch 安装)'
43
+ : mechanism === MECHANISM.DIRECTORY
44
+ ? '目录式(~/.dsh/.agent-presets/<id>/agent.cordis.yml,由 dsh-agent-presets 扫描)'
45
+ : '未知(scope 里既没有 dsh-agent-preset 也没有 dsh-agent-presets)'
46
+ }
47
+
48
+ /**
49
+ * 跑某套机制该跑哪套门禁。
50
+ * @returns {{ schema: 'rows'|'directory'|null, idsFrom: 'composition'|'directory'|null }}
51
+ */
52
+ export function gatePlan(mechanism) {
53
+ if (mechanism === MECHANISM.ROWS) return { schema: 'rows', idsFrom: 'composition' }
54
+ if (mechanism === MECHANISM.DIRECTORY) return { schema: 'directory', idsFrom: 'directory' }
55
+ return { schema: null, idsFrom: null }
56
+ }