@sema-agent/server 7.43.0 → 7.44.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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
package/dist/run-local.js CHANGED
@@ -1,47 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * `run-local` — a ONE-SHOT local CLI for the agent engine.
4
- *
5
- * Boots the SAME runtime as `src/main.ts` (createBrain + Runner + the scenario/skills capability layer +
6
- * the host execution env) IN-PROCESS, runs ONE task from the CLI objective to completion, prints the
7
- * TaskResult, and exits. There is NO long-running HTTP server and NO submission auth gate — this is the
8
- * whole point: it removes two friction points (the SERVICE_AUTH_TOKEN 503 on submit and the
9
- * manual server lifecycle). The user fills `~/.ai-agent/.env` + `config.d/` (the LOCAL config source) and
10
- * just runs `run-local "<objective>"`.
11
- *
12
- * Posture: LOCAL config mode (CONFIG_PROVIDER=local) → createConfigProvider → LocalConfigProvider →
13
- * applyEffective (config.d/.env drives models/roles/mcp), and the `host` exec lane (run on THIS machine,
14
- * no container) — identical building blocks to the server path so behavior matches (no drift). It reuses
15
- * main.ts's helpers verbatim; the only thing it skips is the server-only surface (createHttpServer, listen,
16
- * auth/rateLimit/quota/run-store/leader/durable-approval-decide/reapers).
17
- *
18
- * 🔴 **DEPLOYMENT GOVERNANCE IS NOT IN THAT SKIP LIST** (design/181 件三 / #180). Until 7.4.x this file
19
- * assembled its `TaskSpec` with the governance chain entirely ABSENT — `loadConfig` + `applyEffective` had
20
- * already parsed `autonomy` / `commandPolicy` / `sensitiveWritePatterns` into the config, and the spec
21
- * literal simply never read them (compute-then-discard), so the same operator knob was live on the HTTP leg
22
- * and structurally non-existent here. It now goes through the SAME single construction port as the other two
23
- * legs ({@link applyLocalGovernance} → `src/deployment-governance.ts` → `applyRuntimeGovernance` → core's
24
- * `tightenTaskSpec`). What this leg genuinely does NOT have is the **durable approval** face (a one-shot CLI
25
- * has no `/v1/approvals/:id/decide` leg to resume from), so the approval baseline is the adjudicated
26
- * allow-all arm and a gated `ask` is answered by {@link createLocalApprover} — the TTY human, or a
27
- * fail-closed deny — instead of parking a checkpoint nobody will ever redeem.
28
- *
29
- * 🔴 **design/201 的 permissionMode → shellGate 翻译表 SCOPED-OUT 本腿**(设计稿 §5,显式裁定而非欠账)。
30
- * 这条腿的 operator 就是亲手起这个进程的用户,它**没有 permission mode 这个概念**(一次性 CLI 收不到
31
- * 客户端表态),今昔都落 core 的缺省 `shellGate: off`。给它硬塞一个 classify 档,在没有 HITL 面的形态下
32
- * 等于把每一次 shell 调用送进 auto-deny —— 那不是加固,是废掉 run-local。部署治理段(上一段)照旧全接:
33
- * `MANUAL_MODE_SHELL_GATE` 想要门,operator 在自己的 `.env` 里说一句就有。同族的另一条 scoped-out 是
34
- * parked-revive 赎回腿(body 全盲,core 的 `InheritedGate` 以 `max(seed, live)` 兜底,自洽)。
35
- * 「三腿不共享翻译点」是设计,不是漏接:名册对账钉在
36
- * `test/operator-knob-client-posture-matrix.test.ts`(`shellGateForMode` 已并入构造点名册,第四条腿哪天
37
- * 接上它必须同时挂矩阵腿)。
38
- *
39
- * Usage: run-local "<objective>" [--root <dir>] [--json] [--scenario <name>]
40
- *
41
- * `main()` is a thin wrapper over the testable {@link runLocal}, which takes argv + injectable
42
- * deps (a mock Brain, a captured logger/printer, and an `exit` shim) so the assembly + one-shot flow can be
43
- * exercised deterministically with no real LLM / network / keys.
44
- */
45
2
  import { homedir } from "node:os";
46
3
  import { join } from "node:path";
47
4
  import { realpathSync } from "node:fs";
@@ -65,7 +22,7 @@ import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-me
65
22
  import { loadSkills } from "./capabilities/skills.js";
66
23
  import { createRepoClient } from "./capabilities/repo-tools.js";
67
24
  import { webSearchConfigFromEnv, createWebSearchBackend } from "./plugins/web-search.js";
68
- import { buildScenarios, selectScenario, centerScenarios, canonicalScenarioName } from "./capabilities/scenarios.js";
25
+ import { buildScenarios, selectScenario, centerScenarios } from "./capabilities/scenarios.js";
69
26
  import { createTaskListLane } from "./boot/task-list-lane.js";
70
27
  import { pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.js";
71
28
  import { HttpError } from "./security.js";
@@ -74,14 +31,13 @@ import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync
74
31
  import { buildPricing, cappedCeiling, createTracer } from "./budget.js";
75
32
  import { createSharedRunnerDeps } from "./boot/runner-deps.js";
76
33
  import { createGovernanceSeams } from "./boot/governance-seams.js";
77
- import { assertRetentionLaneWirable } from "./boot/retention-lane.js"; // #270 车2:lane 自持门(本地形恒无执行器)
34
+ import { assertRetentionLaneWirable } from "./boot/retention-lane.js";
78
35
  import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
79
36
  import { createPermissionDeniedMeter } from "./observability/tool-trace.js";
80
37
  import { createKeyResolver } from "./key-resolver.js";
81
38
  import { createLogger } from "./observability/logger.js";
82
39
  import { createMetrics } from "./observability/metrics.js";
83
40
  import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.js";
84
- /** Parse argv (everything AFTER `node run-local.js`). Throws a clear Error on a missing objective. */
85
41
  export function parseArgs(argv) {
86
42
  let objective;
87
43
  let root;
@@ -92,11 +48,6 @@ export function parseArgs(argv) {
92
48
  let json = false;
93
49
  const valueFlag = (i, name) => {
94
50
  const v = argv[i];
95
- // 空串/纯空白 = **没给参数**,不是"用户就想要空"(core [1689] 第 3 条同款:`??` 兜不住 `""`)。
96
- // 收下它会让空串变成**隔离键**:`--session ""` → `args.session ?? uuidv7()` 得到 `""`,所有这类调用
97
- // 复用同一个空 id「会话」;`--user ""` → `args.user ?? "local"` 得到 `""`,长期记忆落进匿名桶而不是
98
- // 文档写的默认身份,且「传空串」与「不传」成为两个不同的桶。在解析器这一层拒才对 —— 这个函数存在的
99
- // 意义本就是「这个 flag 需要一个参数」。
100
51
  if (v === undefined || v.trim() === "" || v.startsWith("--"))
101
52
  throw new UsageError(`${name} needs an argument`);
102
53
  return v;
@@ -122,7 +73,7 @@ export function parseArgs(argv) {
122
73
  throw new UsageError(`unknown flag: ${a}`);
123
74
  }
124
75
  else if (objective === undefined) {
125
- objective = a; // first positional = the objective
76
+ objective = a;
126
77
  }
127
78
  else {
128
79
  throw new UsageError(`unexpected extra argument: ${a} (quote the objective as one argument)`);
@@ -133,35 +84,15 @@ export function parseArgs(argv) {
133
84
  }
134
85
  return { objective, root, json, ...(scenario ? { scenario } : {}), ...(session ? { session } : {}), ...(workspace ? { workspace } : {}), ...(user ? { user } : {}) };
135
86
  }
136
- /** A bad-usage error → print to stderr + exit 2 (distinct from a task failure exit 1). */
137
87
  export class UsageError extends Error {
138
88
  }
139
- /**
140
- * The model GATEWAY pre-flight (mirrors the doctor message): the engine cannot run a real
141
- * task without (a) a gateway base URL that ends in `/v1` and (b) the gateway/model API key resolvable from
142
- * env. We surface a "you still need to set: …" list rather than letting the first brain call crash with a
143
- * cryptic auth/connect error. Returns a list of missing items (empty = OK).
144
- *
145
- * NOTE: with an injected mock `brain` the gateway is irrelevant (no real call is made), so the caller skips
146
- * this check in that case — the check is about a REAL run, not the assembly.
147
- */
148
89
  export function missingGatewayRequirements(config) {
149
90
  const missing = [];
150
- // (a) base URL: loadConfig always has a default, but a real local user must point it at THEIR gateway and
151
- // it must carry the OpenAI-compatible `/v1` suffix (the brain appends `/chat/completions`).
152
91
  if (!config.gatewayBaseUrl || !/\/v1\/?$/.test(config.gatewayBaseUrl)) {
153
92
  missing.push("MODEL_GATEWAY_BASEURL (the OpenAI-compatible gateway URL, must end in /v1)");
154
93
  }
155
- // (b) the key: either the single gateway key (MODEL_API_KEY) OR — when the catalog pins a per-model
156
- // apiKeyEnv (sema-registry / config.d) — that named env var must be present. A model without its own
157
- // apiKeyEnv falls back to the gateway key, so the gateway key is the floor.
158
94
  const perModelEnvs = Object.values(config.modelApiKeyEnv ?? {});
159
95
  const haveGatewayKey = !!config.gatewayApiKey;
160
- // The gateway key is the FLOOR: per-model keys only WAIVE it when EVERY catalog model the resolver may be
161
- // asked about carries (and has set) its own apiKeyEnv. A model with no per-model key falls back to the
162
- // gateway key, so a MIXED catalog (some models keyed, some not) with the gateway key unset would crash on
163
- // the unkeyed model's first brain call. We dedupe via the model objects' `.name` so the synthetic `default`
164
- // / model-id ALIAS entries of config.models (which mirror an existing per-name model) are not over-counted.
165
96
  const catalogModelNames = new Set(Object.values(config.models ?? {}).map((m) => m.name));
166
97
  const allModelsHaveOwnKey = catalogModelNames.size > 0 &&
167
98
  [...catalogModelNames].every((n) => {
@@ -180,47 +111,9 @@ export function missingGatewayRequirements(config) {
180
111
  }
181
112
  return missing;
182
113
  }
183
- /**
184
- * design/181 件三:run-local 腿的**部署治理链装配**(#180)。
185
- *
186
- * 与另外两条腿(HTTP 的 `resolveSpec`、durable park 的赎回腿)装的是**同一条**链,经**同一个**构造口:
187
- * 审批基线作 base 的 `toolPolicy` 座,再经**折叠属主** `applyRuntimeGovernance`(→ core `tightenTaskSpec`)
188
- * 叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE / 守卫集)。本函数一条 policy 都不
189
- * 自己合成。
190
- *
191
- * · **审批基线 = adjudicated allow-all**:一次性 CLI 没有 durable 审批设施(没有 `/decide` 腿可赎回),
192
- * 所以 `createApprovalBaselinePolicy` 走 durable 缺席那一臂。它是一条**在场的** effect-aware 策略
193
- * (满足 core 的 `hasEffectAwareGate`),运维照旧用 `AUTONOMY` / `commandPolicy` / 守卫集在其上收紧。
194
- * · **裁决 env 恒 host 形,锚 `workspaceDir`**:run-local 的手就跑在这台机器的这个目录上
195
- * (`--workspace` / config.d workdir / cwd 三选一,调用方已解析好),所以 `cwd` 恒在场 —— 相对形写目标
196
- * 由 core 按 `rootPath` 解析进真身裁决,构造口的词法补层(沙箱腿那条)在这里自动不铺。
197
- * ✅ **旧残余面已收口(core 5.19.0 #108,2026-08-08 校正)**:`rootPath` 是装配期的**静态**值,而 core
198
- * 的写工具解析相对形用的是被 Bash `cd` 就地改写的活 cwd —— 于是 `cd .git/hooks` 之后
199
- * `Write("pre-commit")` 落进守卫段却判不出来(两条 host 腿同形,本腿与 resolve-spec)。5.19.0 起守卫
200
- * 按 `ToolCallRequest.cwd`(引擎每次调用从同一只 tracked cwd 盖戳)解析写目标,本腿零改动即得保护;
201
- * **不对称是 core 故意的**:目标跟活 cwd 走,守卫自己配置的目录仍锚静态根,`cd` 搬不动栅栏。全文与
202
- * 证据见 `deployment-governance.ts` 的 `RelativeTargetLexicalEnv` 顶注,端到端正控钉见
203
- * test/run-local.test.ts(该钉已由 🟡 特征化翻成 🔴 正控)。
204
- * · **禁 memoize**:四个旋钮都是热改字段,每次装配现读活 config(与另两条腿同一姿势)。
205
- */
206
114
  export function applyLocalGovernance(base, config, workspaceDir) {
207
115
  return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }));
208
116
  }
209
- /**
210
- * design/181 件三:**onAsk 座**(§0 潜雷 / §5 锁 6)。
211
- *
212
- * 🔴 为什么这个座位是接治理链的**同窗必做项**:run-local 有 checkpointStore(file backend),所以
213
- * `AUTONOMY=ask` ⇒ `shellGate:"always"` ⇒ Bash 进 `irreversibleTools` ⇒ park lane effective ⇒ **每一次
214
- * shell 调用都 durable park 成一张永远没人 resume 的 checkpoint**,任务挂起、CLI 退 1。一次性 CLI 里
215
- * 没有任何赎回腿,那不是「等人来批」,是卡死。core 的 `isLiveApproverSeat(onAsk)` 为真时 `suspendAsk`
216
- * 早退(prepare-task dist 亲读),ask 于是落回本回调 —— 把 park 换成「问人 / 当场拒」的唯一正当接法。
217
- *
218
- * ⛔ **不在 toolPolicy 层做 ask→deny 映射**:core 自己就有 headless auto-deny 的语义与文案,在策略层
219
- * 复刻一份 = 同源谎(两处判据日后各改各的)。裁决归 core,座位归这里。
220
- *
221
- * 🔴 非 TTY 臂答 `false`(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
222
- * `suspendAsk`**(dist `core/hooks.js:296` 的 `approverUnavailable` 重投),上面那颗雷会原样回来。
223
- */
224
117
  export function createLocalApprover(io) {
225
118
  return async (req, signal) => {
226
119
  if (!io.isTty) {
@@ -232,15 +125,6 @@ export function createLocalApprover(io) {
232
125
  return io.askYesNo(`approve "${req.toolName}"? ${req.message} [y/N] `, signal);
233
126
  };
234
127
  }
235
- /** 默认的 TTY 问人器:一问一答,回答写 **stderr**(stdout 是任务结果的专用管道,别污染它)。
236
- * streams 可注入=EOF/abort 两臂测得动(默认 process.stdin/stderr)。
237
- *
238
- * 🔴 决断三臂(codex 验收轮 high,红先修):`readline/promises` 的 `question` 在输入流 EOF/close 时
239
- * **不决**(promise 悬挂)——Ctrl-D、关闭的 stdin、任务取消都会把 runLocal 永久挂住,文件后端的锁
240
- * 跟着被拖住,下一次同 root 调用直接卡死。三臂全部 fail-closed 决 false:
241
- * · `signal` abort(core `OnAsk` 递进来的取消,含预先 aborted);
242
- * · readline `close`(输入流 end/close 时 readline 自关);
243
- * · `question` 自身 reject(abort throw / 流错误)。 */
244
128
  export async function promptYesNoOnTty(prompt, signal, streams = { input: process.stdin, output: process.stderr }) {
245
129
  if (signal?.aborted)
246
130
  return false;
@@ -263,11 +147,6 @@ export async function promptYesNoOnTty(prompt, signal, streams = { input: proces
263
147
  rl.close();
264
148
  }
265
149
  }
266
- /**
267
- * Boot the engine in LOCAL mode, run ONE task to completion, print the result. Returns the process exit
268
- * code (0 = completed, 1 = the task did not complete / a fatal error, 2 = bad usage / unmet env). Pure of
269
- * `process.exit` so a test can assert the code; `main()` applies it.
270
- */
271
150
  export async function runLocal(argv, deps = {}) {
272
151
  const print = deps.print ?? ((l) => console.log(l));
273
152
  const printErr = deps.printErr ?? ((l) => console.error(l));
@@ -282,47 +161,18 @@ export async function runLocal(argv, deps = {}) {
282
161
  }
283
162
  throw err;
284
163
  }
285
- // FORCE local posture: a one-shot CLI is a single-machine run. Set the knob BEFORE loadConfig so
286
- // (1) createConfigProvider selects the LocalConfigProvider and (2) config.ts defaults the `host` exec lane
287
- // (the TOC first-run default for CONFIG_PROVIDER=local with no explicit REMOTE_EXEC). Honor an operator who
288
- // already set it to something else (don't override an explicit choice) — but never leave it remote-by-default.
289
164
  if (!process.env.CONFIG_PROVIDER)
290
165
  process.env.CONFIG_PROVIDER = "local";
291
- // FORCE the durable backends OFF: a one-shot, single-machine CLI has nothing durable to persist
292
- // and deliberately does NOT assemble the server-only StoreBackend. A shared .env that carries
293
- // SESSION_BACKEND set to anything but the enum's "memory"/"mysql"/"auto" would otherwise boot-reject
294
- // outright (SESSION_BACKEND is an enum env, validated at load — an old value like the retired public
295
- // name "tidb" is rejected, not silently coerced), and even a legit "mysql" value would pull in a
296
- // server-only SQL session store this one-shot CLI has no business assembling. Unlike CONFIG_PROVIDER/
297
- // REMOTE_EXEC (where an explicit operator choice can be HONORED), a DB session store cannot work here
298
- // at all, so we FORCE — never honor — this knob to in-memory so the CLI just works regardless of the
299
- // shared config. Long-term memory needs no forcing: the design/138 memory ENGINE is file-based and
300
- // default-ON here (single-user; MEMORY_ENGINE=off and MEMORY_ENGINE_DIR pass through loadConfig as-is).
301
166
  process.env.SESSION_BACKEND = "memory";
302
- // 🔴 REQUIRE_PRINCIPAL is the same shared-config footgun class: run-local IS the single
303
- // user at their own terminal — there is no untrusted caller to gate — but a server .env carrying
304
- // REQUIRE_PRINCIPAL=true would flip `memoryEngineBackendFor`/`memoryScopeFor` into the multi-tenant
305
- // memory-dark branch (plus the host-power gates), silently killing the CLI's long-term memory. FORCE it off.
306
167
  process.env.REQUIRE_PRINCIPAL = "false";
307
- // --root wins, then the standard local roots (mirrors main.ts's local-root resolution).
308
168
  const root = args.root ?? process.env.CONFIG_LOCAL_DIR ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
309
169
  if (args.root)
310
- process.env.CONFIG_LOCAL_DIR = args.root; // so loadConfig's local-dir + the provider agree
170
+ process.env.CONFIG_LOCAL_DIR = args.root;
311
171
  const config = loadConfig();
312
172
  const logger = deps.logger ?? createLogger(config.logLevel);
313
173
  const metrics = createMetrics();
314
- // A-033.3(#256,codex 复审 medium 真修):run-local **也**要装密钥剥除的留痕面。这不是 main.ts 的
315
- // 复制粘贴 —— 它恰恰是最该有的那条腿:run-local 是 TOC 本地形,模型可驱动的 shell 跑在**用户自己的
316
- // 机器**上、继承的是用户**真实的** env(脑钥/仓库令牌/DB 凭据),而这两条剥除腿(hostExecutionEnvFactory
317
- // 的 mergeEnv 与 makeLoadProjectMemory 的 git 子进程)在本入口全都接线了。不装 ⇒ 收集器有了、sink 是空的,
318
- // 这条腿保留 A-033.3 要消灭的那个「零披露」原状(而且进程一退,连计数都没了)。
319
174
  installSecretEnvScrubRecorder({ logger, metrics });
320
- // F10 (codex audit): run-local used to drop the pre-logger config diagnostics on the floor (S20 soft-knob
321
- // warnings + [792]④ boot notices) — main.ts drained them, this entrypoint didn't. Shared helper, no drift.
322
175
  logConfigDiagnostics(logger);
323
- // ── Config source (DUAL-MODE §4): resolve the effective config from the LOCAL FileConfigStore and apply it
324
- // OVER the env defaults (config.d/.env drives models/roles). Unreachable/empty → keep the env-derived
325
- // config (the local root may not exist yet — "fill .env and run" still works off env). ──
326
176
  const configProvider = createConfigProvider({
327
177
  provider: config.configProvider ?? "local",
328
178
  ...(config.configLocalDir ? { localDir: config.configLocalDir } : { localDir: root }),
@@ -331,30 +181,14 @@ export async function runLocal(argv, deps = {}) {
331
181
  try {
332
182
  const r = await configProvider.fetchEffective(undefined);
333
183
  if (r) {
334
- // [898] tolerant read(registry-core 0.10.12):catalog 域坏文件不再连坐整包回落 env——坏域按 default
335
- // 落+这里 per-domain warn 点名(文件路径在 error 文案里),好域照常生效。gate 域坏仍走下面整包 catch。
336
184
  for (const de of r.domainErrors ?? [])
337
185
  logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "domain fell to schema defaults; other domains applied normally" });
338
186
  effective = r.effective;
339
187
  applyEffective(config, r.effective, logger);
340
- // MCP servers (config.d/mcp.json or center) — applied here (NOT inside applyEffective, which only
341
- // covers models/roles/gates/teams) so run-local matches the server path (main.ts:446). Restart-to-apply.
342
188
  if (effective?.mcp)
343
189
  config.mcpServers = resolveMcpServers(effective.mcp, logger);
344
- // DESIGN-269 车1 件1:A2A peers, same line as MCP — applied HERE so run-local matches the server path
345
- // (boot/config-center.ts). Restart-to-apply.
346
- // ⚠️ 如实记(codex 对抗复审 finding 4,验真后采纳为**文档更正**):今天只有**远端 center** 的
347
- // `/effective` 里带 `a2a` 域时这一行才写得进值。**本地 config.d 车道还接不上** —— registry-core
348
- // 锁定版的可移植域表里没有 a2a,`config-provider.ts` 的 `mapToServiceEffective` 闭式投影也就没有这
349
- // 一域可投。补齐属于 registry-core(上游件),不在本车射程;在那之前别在文档里承诺 config.d/a2a.json。
350
190
  if (effective?.a2a)
351
191
  config.a2aPeers = resolveA2aPeers(effective.a2a, logger);
352
- // #A4([4147] 件1):READ 容纳面四键的 center 腿 —— 与 server 腿同一只函数、同一条 restart-to-apply
353
- // 线(本腿是一次性 CLI,"restart" 即下次运行)。env 显式恒赢的判据在函数内,两腿零分家。
354
- // ⚠️ 如实记(与上面 a2a 那条同款诚实句):**本地 config.d 车道今天接不上** —— registry-core 的
355
- // 可移植域表里没有 readFace 域,`config-provider.ts` 的 `mapToServiceEffective` 闭式投影也就没有这一
356
- // 域可投;这一行今天只在 `CONFIG_PROVIDER=remote` 的 run-local 形上写得进值。补齐属于 registry-core
357
- // (上游件),在那之前别在文档里承诺 config.d/read-face.json。
358
192
  applyCenterReadFace(config, r.effective, logger);
359
193
  logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version });
360
194
  }
@@ -362,10 +196,6 @@ export async function runLocal(argv, deps = {}) {
362
196
  catch (err) {
363
197
  logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
364
198
  }
365
- // ── remoteExec (DUAL-MODE §5): in local mode, honor the SHARED config.d/remote-exec.json via the registry
366
- // loader (the SAME file the client's `agent-cli config` writes). MISSING → keep config.ts's host default;
367
- // PRESENT host → it wins (workdir→workspaceBase); corrupt → fail-closed. Mirrors main.ts's block; the
368
- // isolated lanes (e2b/k8s/ssh/adb) stay env-sourced (a one-shot CLI is the host lane by design). ──
369
199
  try {
370
200
  const fileRx = await loadRemoteExec(root);
371
201
  if (fileRx?.provider === "host") {
@@ -385,10 +215,6 @@ export async function runLocal(argv, deps = {}) {
385
215
  printErr(`config.d/remote-exec.json is invalid: ${String(err)}`);
386
216
  return 2;
387
217
  }
388
- // ── design/181 件三 boot 期两件(与 HTTP 腿同一只判据)────────────────────────────────────────
389
- // ① 守卫集的**可编译性**门:编译发生在每一次装配上,一条「一个路径段都没有」的模式(`"/"`)会 THROW,
390
- // 运维从那条错误里看不出是自己的 env 写错了。共享 .env 的坏值必须炸在启动上——这条腿按 run-local 的
391
- // 既定 UX 落地(doctor 文案 + 退 2),而不是把栈抛给用户。
392
218
  try {
393
219
  assertGuardPatternsUsable(config);
394
220
  }
@@ -397,18 +223,12 @@ export async function runLocal(argv, deps = {}) {
397
223
  printErr(`(local config root: ${root} — fix it in ${join(root, ".env")} or your shell env; \`SENSITIVE_WRITE_PATTERNS=off\` disables the guard set)`);
398
224
  return 2;
399
225
  }
400
- // ② UNGATED 补偿:接上审批基线后 core 的 `hasEffectAwareGate` 恒真 ⇒ 它那条 "write-capable hand tools
401
- // are present but UNGATED" 的信号在这条腿上结构性消失。判据与文案是**同一只**构造口的
402
- // (buildOnlySensitiveBaselineWarning),两个座位量按本腿的真形取:durable 门恒不在场(一次性 CLI
403
- // 没有 /decide 赎回腿),单用户 auto-accept 基线的适用性判据与 main.ts 逐字同源。
404
226
  const ungatedCompensation = buildOnlySensitiveBaselineWarning(config, {
405
227
  durableEnabled: false,
406
228
  singleUserAutoAcceptBaseline: config.requirePrincipal !== true && !hasOperatorGateIntent(config),
407
229
  });
408
230
  if (ungatedCompensation)
409
231
  logger.warn(ungatedCompensation.event, ungatedCompensation.fields);
410
- // ── Pre-flight: with a REAL brain, validate the gateway + key and print the doctor-style guidance instead
411
- // of crashing on the first brain call. With an injected mock brain, no real call happens → skip. ──
412
232
  if (!deps.brain) {
413
233
  const missing = missingGatewayRequirements(config);
414
234
  if (missing.length > 0) {
@@ -419,22 +239,11 @@ export async function runLocal(argv, deps = {}) {
419
239
  return 2;
420
240
  }
421
241
  }
422
- // ── Minimal runtime assembly (the SAME building blocks as main.ts, server-only bits omitted). ──
423
242
  const brain = deps.brain ?? createBrain(config, deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {});
424
243
  const pricing = buildPricing(config.models);
425
- // Persistent LOCAL stores (TOC): core's file-backed StorageBackend under the data root → session + long-term
426
- // memory + checkpoints SURVIVE across invocations (no DB). flock'd single-instance per data dir; a second
427
- // concurrent instance throws in the constructor — surface a clear message instead of a stack.
428
244
  let fileBackend;
429
245
  try {
430
246
  fileBackend = new FileStorageBackend({
431
- // 🔴 腐读披露座(codex 复审 R1-medium,#205 件3 连带):本批把 `sessionPolicyStore` 接进了两只
432
- // Runner,而 core 对**坏掉的**策略文件是 documented fail-open —— 读不出来按「没有规则」处理。
433
- // 这条 fail-open 本身是 core 的裁定(抛会让整条任务死、也会自锁修复写),但它**不许无声**
434
- // (#157 安全轴纪律)。一个座覆盖 core 今天转发的三张脸,后果**各不相同**,所以文案只陈述共同事实
435
- // (「一次持久读被当成缺席」)并把后果按脸分列,不下统一结论(codex R2-medium:原文案把「少了
436
- // 会话规则这一层」写成「整个任务无约束」——那会误导事故定级,而且对另外两张脸根本不成立)。
437
- // ENOENT 不触发(那是真缺席)。
438
247
  root,
439
248
  onCorruptRead: (info) => logger.warn("file_store_corrupt_read", {
440
249
  path: info.path,
@@ -450,47 +259,24 @@ export async function runLocal(argv, deps = {}) {
450
259
  }
451
260
  catch (e) {
452
261
  printErr(`cannot open local data dir ${root}: ${e instanceof Error ? e.message : String(e)}`);
453
- // 🔴 core 5.23.0([3372] 件④):这个构造器的第二个拒绝面是 design/183 的 I6 adoption boot 门。
454
- // 那一类失败**不能**再补「换一个 --root」这句 —— 换根 = 丢下一个迁移到一半的数据根(与
455
- // `plugins/store-backend.ts` 的 local 臂同案同判)。core 的拒绝句已自带出路,上面那行已如实打印。
456
262
  if (!(e instanceof AdoptionError)) {
457
263
  printErr("(another run-local/engine instance may own it; finish it first, or use a different --root)");
458
264
  }
459
265
  return 2;
460
266
  }
461
267
  const sessionStore = fileBackend.sessionStore;
462
- const keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys); // [2006] 后续:三参全链(sealed→env→undefined),与 boot 正形同源——二参形曾让 sealed 模型静默落网关 key
463
- // Host exec lane: run on THIS machine, in a FIXED workspace (CC-like) — the cwd by default (or --workspace /
464
- // a config.d workdir). The agent's file edits PERSIST there + are user-visible; the dir is the user's OWN and
465
- // is NEVER deleted (workspaceDir mode). One env per task; the Runner owns its lifetime.
268
+ const keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
466
269
  const hostRx = config.remoteExec?.provider === "host" ? config.remoteExec : undefined;
467
270
  const workspaceDir = args.workspace ?? hostRx?.workspaceBase ?? process.cwd();
468
271
  const executionEnvFactory = hostRx
469
272
  ? hostExecutionEnvFactory({
470
273
  workspaceDir,
471
- // systematic-audit (uniformity with main.ts:442 + the worktree-lane gates): gate background-shell by tenancy.
472
- // run-local is a single-user CLI (no untrusted caller) so requirePrincipal is ~always false → ON; passing the
473
- // gate keeps the policy uniform if a shared .env ever sets REQUIRE_PRINCIPAL, never relying on the `?? true`.
474
274
  backgroundShell: config.requirePrincipal !== true,
475
275
  ...(hostRx.commandTimeoutMs != null ? { commandTimeoutMs: hostRx.commandTimeoutMs } : {}),
476
276
  })
477
277
  : undefined;
478
- // TOC local LSP (core 1.190): run-local IS the pure-local host lane, so mount core's `NodeLspManager` (local
479
- // child_process language servers over stdio, CC `services/lsp` parity) on the host lane. Default ON (degrades
480
- // gracefully to grep/read if a server binary is absent); `LSP_HOST_ENABLED=false` opts out (design/158 B4 split
481
- // this lane off `LSP_ENABLED`; 🪦 server 3.0.0 起 `LSP_ENABLED=false` 是 fail-loud 墓碑,拒启指路新名,
482
- // 不再是可用的 opt-out)。`requirePrincipal !== true`
483
- // keeps it uniform with main.ts (a no-op here — run-local is single-user — but never relies on that). Both runners.
484
278
  const lspManager = hostRx && config.lspHostEnabled && config.requirePrincipal !== true ? new NodeLspManager({ log: (event, fields) => logger.info(event, fields) }) : undefined;
485
- // Long-term memory (design/138 S1): run-local IS the single-user lane, so the file-based memory ENGINE is
486
- // default-ON (MEMORY_ENGINE=off opts out). The data root doubles as the engine config-root fallback so a
487
- // run-local and the local HTTP service on one box share ONE memory home (MEMORY_ENGINE_DIR overrides).
488
279
  const memoryEngine = memoryEngineBackendFor(config, root);
489
- // 142-S2.5-W1: TOC 同步 client 腿——run-local 是 file memory 形态本尊(loadConfig 已拒
490
- // DB backend 上的 MEMORY_SYNC_*)。run-local 没接 onMemoryHarvestReport 钩子(一次性 CLI:harvest 只在
491
- // 任务收尾发生一次,fire-and-forget 轮会被进程退出腰斩),按设计的后备姿势:boot fire-and-forget 一轮
492
- // (拉中心增量,失败 warn 不阻断;进程退出腰斩也安全——cursor 保留纪律下轮重收敛)+ 任务结束后 await
493
- // 一轮(推本次 harvest 的新记忆;syncOnce 串行链自动排在 boot 轮之后)。
494
280
  let memorySync;
495
281
  if (config.memorySync && memoryEngine) {
496
282
  memorySync = createMemorySyncRunner({
@@ -506,12 +292,7 @@ export async function runLocal(argv, deps = {}) {
506
292
  else if (config.memorySync) {
507
293
  logger.warn("memory_sync_configured_but_memory_dark", { note: "MEMORY_SYNC_URL is set but the file memory engine is dark (MEMORY_ENGINE=off / remote exec lane) — no sync rounds will run" });
508
294
  }
509
- // codex-2 F3([985]e run-local 半场):config.d/prompts.json 的 catalog 轴在这条 lane 也要真消费——
510
- // 判形(与 server 同一候选门)→ CenterPromptSource(File 形,<root>/prompt-epoch 自管)→ 两 Runner 的
511
- // promptSource。坏形/apply 失败=warn + 内置照跑(单进程本地形,无 refresh 重试拍;下次启动再试)。
512
295
  const promptEpochDir = join(root, "prompt-epoch");
513
- // codex-7:守护工厂与 main.ts 同姿势——File 店构造同步 mkdir,坏 prompt-epoch 路径不得把可选特性
514
- // 变成 run-local 启动致命(Memory 兜底=candidate 不跨启动持久;内置照跑)。
515
296
  const promptSource = (() => {
516
297
  try {
517
298
  return new CenterPromptSource({
@@ -526,7 +307,7 @@ export async function runLocal(argv, deps = {}) {
526
307
  }
527
308
  })();
528
309
  try {
529
- await promptSource.restore(); // run-local IS the owning local provider — the on-disk candidate is authoritative
310
+ await promptSource.restore();
530
311
  const promptsRaw = effective?.prompts;
531
312
  if (promptsRaw !== undefined) {
532
313
  const pv = validatePromptsDomain(promptsRaw);
@@ -536,33 +317,23 @@ export async function runLocal(argv, deps = {}) {
536
317
  logger.warn("center_prompts_invalid", { phase: "run-local", error: pv.error, note: "malformed prompts face — built-ins serve" });
537
318
  }
538
319
  else {
539
- // no prompts face on this boot: a persisted candidate whose file was removed must not survive (same
540
- // cleared semantics as the server lane) — applyCatalogToSource's current() probe handles the owed disable.
541
320
  await applyCatalogToSource(promptSource, undefined, undefined, logger, "run-local");
542
321
  }
543
322
  }
544
323
  catch (e) {
545
324
  logger.warn("center_prompts_catalog_restore_failed", { err: String(e), note: "catalog candidate absent — built-ins serve" });
546
325
  }
547
- // design/181 件三:**onAsk 座**(全文理由见 {@link createLocalApprover})。两只 Runner 同装 —— 委派出去
548
- // 的子任务也在同一台机器上动手,少装一只就是「主任务问人、子任务 park 卡死」。
549
326
  const onAsk = createLocalApprover({
550
327
  isTty: deps.isTty ?? process.stdin.isTTY === true,
551
328
  askYesNo: deps.askYesNo ?? promptYesNoOnTty,
552
329
  printErr,
553
330
  });
554
- /** core 的 `RunnerDeps.onError` 座(两只 Runner 共用一只闭包 —— 两处各写一份正是漂移的成因)。 */
555
331
  const engineOnError = (err, ctx) => logger.warn("engine_error", {
556
332
  phase: ctx.phase,
557
333
  sessionId: ctx.sessionId,
558
334
  ...(ctx.classification !== undefined ? { classification: ctx.classification } : {}),
559
335
  err: String(err),
560
336
  });
561
- // ⚠️ `createOrgMemoryAdmissionWiring` 有一条 **throw** 路径:`MEMORY_ORG_DIRECTORY_JSON` 形坏时
562
- // `parseOrgDirectoryStatic` 启动期炸(config 层按 A4 分层刻意不校验它)。server 那边炸=进程拒启,
563
- // 正确;但本腿的既定 UX 是「doctor 文案 + 退 2」(同 assertGuardPatternsUsable / remote-exec-file-invalid
564
- // 两处),把栈抛给在终端里敲命令的人是回归。另一条 throw(多租户 org 键 + 无目录源)在本腿**结构上
565
- // 不可达**——上面已把 `REQUIRE_PRINCIPAL` 强制成 false。
566
337
  let orgMemoryAdmission;
567
338
  try {
568
339
  orgMemoryAdmission = createOrgMemoryAdmissionWiring({ config, logger, metrics });
@@ -573,50 +344,10 @@ export async function runLocal(argv, deps = {}) {
573
344
  await fileBackend.dispose();
574
345
  return 2;
575
346
  }
576
- /**
577
- * [3397]①②③ / #205 件3 —— run-local 的两份 Runner deps 走 **server 同一只共享基座**
578
- * (`createSharedRunnerDeps`,main.ts:432 的同款展开)。
579
- *
580
- * 为什么必须收编:这两份 deps 此前各自手写同源键,而漏配**没有任何编译期或运行期信号** —— 复扫亲验
581
- * 到三处真漂移:`toolResultStore` 只在主 runner(委派子代的 `ReadToolResult(ref)` 恒空,正是 core
582
- * 1.219 修过的那个病在 sub 腿复发)、`config.tiers` **全文件零接线**(而上面的 `applyEffective` 真在
583
- * 填它 ⇒ tier 词 / CC 别名在这条腿上解不出来)、`hooks` / `tracer` / `sessionPolicyStore` 两处都没有。
584
- * 基座是一份,新增共享键不可能只挂一半。
585
- *
586
- * ── 逐参裁定(local 车道**真差异**保留,不为收编硬造 stub)───────────────────────────────────
587
- * · `brain` / `pricing` / `promptSource` / `executionEnvFactory` / `lspManager`:本腿真有,直供。
588
- * · `config`(基座据以取 models / roles / **tiers** / usageWindows):本腿真有 —— tiers 正是本件修的漏。
589
- * · `tracer`:`createTracer(metrics)` 真形。配额/舰队/prompt-manifest 四个可选跟踪器是 server 面的
590
- * 设施,本腿没有 ⇒ 不传(不是 stub:那几个参数本就可选,缺席即该轴不记账)。
591
- * · `deploymentHooks`:`createPermissionDeniedMeter(metrics)` —— main.ts 在**没有** toolTracer 时的
592
- * 同一只值(tool-trace 落库面是 server 侧设施,一次性 CLI 无处落)。
593
- * · `sessionPolicyStore`:`fileBackend.sessionPolicyStore`(core 的 File 形,其文档原话就是「本地部署
594
- * 要让会话规则跨重启存活就接这里」)。E6 规则是 subtract-only ⇒ 方向只会更紧;同一个数据根上的
595
- * 本地 HTTP 服务写下的规则,这条腿本就该认。
596
- * · `usageWindowStore`:与 `config.usageWindows` **成对**(main.ts 同规:两键同真同假)—— 配了窗才接
597
- * File 账本,没配窗时接一个空账本只会让 core 以为这条腿有治理窗。
598
- * · `toolResultStore`:`fileBackend.toolResultStore`(本件的主修点:现在主/子同源)。
599
- * · `orgMemoryAdmission`:`createOrgMemoryAdmissionWiring(...)` 真形。本腿 `REQUIRE_PRINCIPAL` 被强制
600
- * false(见上),所以那条多租户拒启探测结构上不可达;产物在无 center / 无 `MEMORY_ORG_DIRECTORY_JSON`
601
- * 时是「resolver 缺席 + deployment 自证集」——即 `MEMORY_SCOPE=org:x` 的本地用户从此走得通自证通道。
602
- * · `backgroundAgentStore` / `mailboxStore` / `rosterStore`:**undefined** —— core 的 `FileStorageBackend`
603
- * 根本不供这三个店(durable 后台子代 / tier-3 懒复活 / 持久名册都是 server+SQL 面的设施)。造个内存
604
- * 冒牌货等于让 core 以为「具名子代能跨进程复活」,而一次性 CLI 的进程下一秒就没了。
605
- * · `sharedMemoryStores`:**undefined** —— 供给面只有 SQL 形(`store-backend.ts` 明写:单机造 file 形
606
- * 等于给一个人的部署做「团队共享」,能力面会因此说谎)。
607
- */
608
- // design/170 件B/C/D(#252):三个部署治理座席 —— 本地形与 server 形**同一只装配口**。为什么本地形也要
609
- // 有:`COMPLIANCE_PROFILE` / `LOCKED_CONFIG_KEYS` / `RETENTION_MAX_AGE_DAYS` 是从同一份本地 `.env` 读出来
610
- // 的,一个写下了档位的用户在 `sema` 与本机 HTTP 服务上必须得到同一个答案(两条腿对同一份配置两套行为
611
- // 正是本仓在册的病族)。三键未配 = 三座席缺席 = 本腿逐字零变化。
612
- // #270 车2:lane 自持门在本地形也要跑,理由与上一段逐字相同 —— `RETENTION_SWEEP_INTERVAL_SEC>0`
613
- // 是从同一份 `.env` 读出来的,而本地形**结构上**没有执行器(file 店三方法不存在,诚实声明 "none")。
614
- // 少了这道门,一个写下了那根旋钮的用户会在 `sema` 上得到一台自称有留存治理、其实什么都不删的机器,
615
- // 而在 HTTP 形上得到一条拒启 —— 两条腿对同一份配置两套行为,正是本仓在册的病族。
616
347
  assertRetentionLaneWirable({
617
348
  intervalSec: config.retentionSweep.intervalSec,
618
349
  policy: config.retentionPolicy,
619
- executor: undefined, // 本地形恒无执行器(见上)
350
+ executor: undefined,
620
351
  stores: [
621
352
  { name: "sessionStore", store: sessionStore },
622
353
  { name: "checkpointStore", store: fileBackend.checkpointStore },
@@ -636,11 +367,7 @@ export async function runLocal(argv, deps = {}) {
636
367
  const sharedRunnerDeps = createSharedRunnerDeps({
637
368
  config,
638
369
  governanceSeams,
639
- logger, // #240②:onNotice 席(共享基座键,本地形同席——结构化通告落同一 logger)
640
- // 交接件⑤:commit 尾注署名座进**共享基座** ⇒ 主 runner 与 subRunner 同源。
641
- // [931]① clay 拍:run-local = Sema 品牌本地形态,commit 尾注接 Sema 署名(core 1.300 缺省已翻转不署)。
642
- // 修前它只写在下面主 runner 的差异键里 ⇒ **委派出去的子代提交不带署名**,而它提交进的是同一个仓、
643
- // 代表的是同一个部署。那不是一次有理由的分歧(差异键注释块逐条列了理由,唯独没提它),是漏配。
370
+ logger,
644
371
  hands: { commitCoAuthor: "Sema <noreply@vivi-ai.com>" },
645
372
  brain,
646
373
  pricing,
@@ -657,87 +384,42 @@ export async function runLocal(argv, deps = {}) {
657
384
  usageWindowStore: config.usageWindows ? fileBackend.usageWindowStore : undefined,
658
385
  orgMemoryAdmission,
659
386
  sharedMemoryStores: undefined,
660
- // #324:run-local 无 configCenter runtime = 无中心撤销面 ⇒ 席诚实缺席(pre-338 语义)。
661
387
  mcpRevocations: undefined,
662
388
  });
663
389
  const runnerDeps = {
664
390
  ...sharedRunnerDeps,
665
- // ── 以下为主 runner 的差异键(不在共享基座;逐个有因)──────────────────────────────────────
666
- // (`hands` 已随共享基座展开 —— 交接件⑤;这里手写同名键会 override 展开、把 subRunner 重新甩开。)
667
391
  sessionStore,
668
392
  ...(memoryEngine ? { memoryBackend: memoryEngine.backend, memoryEngineDir: memoryEngine.root } : {}),
669
393
  checkpointStore: fileBackend.checkpointStore,
670
- // design/181 件三:core 的降级/配置类告警此前被 `deps.onError?.()` 的可选链整条吞掉(这条腿从不接这
671
- // 个座),包括那条 "write-capable hand tools are present but UNGATED"。接上 stderr(logger.warn 走
672
- // stderr)后其余告警——compaction / prompt-cache / mcp / memory / hook 的降级——也不再无声。
673
394
  onError: engineOnError,
674
395
  onAsk,
675
- // design/113 C4: run-local IS the single-user host lane (cwd = --workspace ?? process.cwd()) — the CLI path where
676
- // CLAUDE.md project-awareness most belongs. Wire it unless explicitly disabled.
677
396
  ...(config.projectMemoryEnabled
678
397
  ? { loadProjectMemory: makeLoadProjectMemory({ logger }), probeInstructionSources: makeProbeInstructionSources() }
679
398
  : {}),
680
399
  };
681
400
  const runner = new Runner(runnerDeps);
682
- // Sub-task runner (council lenses/discussion members) — mirrors main.ts's subRunner, INCLUDING executionEnvFactory so a delegated
683
- // child gets hands (Bash/Read/Edit) on the host lane. Without it the child's handsEnabled is false — the exact
684
- // "subagent couldn't run pwd" defect fixed at main.ts:799; this is its run-local twin, caught by the systematic audit.
685
- // [1367]① fork server half(main.ts subRunner 同款):ForkRoutingSessionStore——fork/resume 形
686
- // (requireExisting)→ host 文件店优先(core 1.350 hostSessionFork 在宿主店 fork,拆店=响亮
687
- // resume.session_not_found),普通子任务 → 私有 TTL 店(throwaway 姿势保留)。
688
- // #205 件3:与主 runner **同一只**共享基座展开(上面那份 `sharedRunnerDeps`)——此前这里是一行手写
689
- // 字面量,`toolResultStore`/`tiers`/`hooks`/`tracer`/`sessionPolicyStore` 全漏,子代 lane 静默降级。
690
- // 差异键只剩会话店与两个座位;**checkpointStore 有意不给**(main.ts 的 sub 腿给它是为了子代 durable
691
- // park,而本腿的 park 设施结构上不存在:一次性 CLI 没有 `/decide` 赎回腿,gated ask 由 TTY 真人当场答
692
- // ——上面 `buildOnlySensitiveBaselineWarning` 传 `durableEnabled: false` 记的就是同一件事实)。
693
401
  const subRunnerDeps = { ...sharedRunnerDeps, sessionStore: new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 })), onError: engineOnError, onAsk };
694
402
  const subRunner = new Runner(subRunnerDeps);
695
- // #196:无手孪生一对 —— **mirrors main.ts**(那边是 `handslessRunner` / `handslessSubRunner` 两只,同 deps
696
- // 摘掉 executionEnvFactory)。run-local 是同形装配点,漏这一对 = 本地 lane 上 scan/council/discuss 的最终
697
- // roster 仍被并集进全量可写手(server 修了、CLI 静默留病 —— 这条腿的 subRunner 当年就是这么漏配的)。
698
403
  const handslessRunner = new Runner(withoutExecutionEnv(runnerDeps));
699
404
  const handslessSubRunner = new Runner(withoutExecutionEnv(subRunnerDeps));
700
- // Capability layer (loadSkills + buildScenarios + selectScenario) — assembled exactly like main.ts.
701
405
  const skills = loadSkills(config.skillsDir);
702
406
  const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
703
- // systematic-audit: wire the env-configured WebSearch backend (WEB_SEARCH_PROVIDER) exactly like main.ts, so the
704
- // default scenario's WebSearch tool is assembled on the CLI path too (it was silently never built before).
705
407
  const webSearchCfg = webSearchConfigFromEnv();
706
408
  const webSearch = webSearchCfg ? createWebSearchBackend(webSearchCfg) : undefined;
707
- // #318:会话级任务清单车道。run-local 无 SQL 后端 ⇒ 内存臂(进程内 per-session)。一次性 CLI 的
708
- // 单进程内本来就只解析一次场景,接它的意义在于**装配面同源**:server 与 run-local 走同一条
709
- // `ScenarioDeps.taskListStoreFor` 契约,不让 CLI 腿静默留在「per-call 私店」的修前形上。
710
- // `--session` 跨调用续聊的待办清单不跨进程存活(local 形的诚实边界,USAGE 已登记)。
711
409
  const taskListLane = createTaskListLane({ logger });
712
410
  const scenarioDeps = {
713
411
  runner, subRunner, handslessSubRunner, model: "default", skills, repoClient, metrics, logger,
714
412
  ...(webSearch ? { webSearch } : {}),
715
413
  taskListStoreFor: taskListLane.storeFor,
716
- brandIdentity: true, // run-local is always a local/TOC deployment → brand the default scenario as Sema.
414
+ brandIdentity: true,
717
415
  };
718
416
  const scenarios = buildScenarios(scenarioDeps);
719
- // Center-/config.d-declared scenarios (composition-as-config): overlay over the built-ins, center wins —
720
- // mirrors main.ts:453-457 so a config.d/scenarios.json scenario resolves identically in run-local (no drift).
721
417
  if (effective?.scenarios) {
722
418
  const { overlay } = centerScenarios(scenarioDeps, effective.scenarios.scenarios, Object.keys(scenarios), logger);
723
419
  Object.assign(scenarios, overlay);
724
420
  }
725
- // ── Build the TaskSpec the way the /v1/tasks sync handler's resolveSpec does (minus server-only fields:
726
- // no auth/principal/durable/approval/budget-cap-from-body). Fresh sessionId; @-model mention honored. ──
727
- // #273 alias:run-local lane 的唯一归一点(旧名 team → discuss + 响亮 warn),与 HTTP lane 的
728
- // `gateScenarioRequest` 同一只属主。归一之后 `scenarioName` 只可能是现役主名——下面的墙钟/mcp/a2a
729
- // 判据点按主名写,不做双名判。
730
- const requested = canonicalScenarioName(args.scenario ?? config.defaultScenario, Object.keys(scenarios), logger);
421
+ const requested = args.scenario ?? config.defaultScenario;
731
422
  const scenarioName = scenarios[requested] ? requested : "default";
732
- // A scenario may THROW HttpError synchronously when a prerequisite is unmet (e.g. `scan` needs
733
- // GIT_API_BASEURL + a repo — which a one-shot CLI may not
734
- // supplies). The server catches this in resolveSpec/prepareSpec and returns a clean 4xx; here we mirror that
735
- // with the doctor-style UX (message to stderr + exit 2) instead of letting it bubble to the fatal exit-1 path.
736
- // --session REUSES a session (durable multi-turn across invocations, via the file session store); absent →
737
- // a fresh uuidv7 (still persisted). --user is the stable memory/identity scope so long-term memory
738
- // accumulates per user (default "local" = the single TOC user). The file stores acquire/append lazily.
739
- // #318:解析点上提到场景装配**之前** —— 场景工厂现在要 `ScenarioContext.sessionId`(会话级任务
740
- // 清单的分区键)。纯移动,取值逐字不变(uuidv7() 无前置依赖)。
741
423
  const sessionId = args.session ?? uuidv7();
742
424
  let cap;
743
425
  try {
@@ -748,7 +430,7 @@ export async function runLocal(argv, deps = {}) {
748
430
  printErr(`cannot run scenario "${scenarioName}": ${err.message}`);
749
431
  if (scenarioName === "scan")
750
432
  printErr("(the `scan` scenario needs a git API base URL and a repo — run-local cannot supply a repo; use the default scenario)");
751
- void lspManager?.dispose(); // symmetry (harmless no-op here — servers spawn lazily during runTask, not yet)
433
+ void lspManager?.dispose();
752
434
  await fileBackend.dispose();
753
435
  return 2;
754
436
  }
@@ -756,87 +438,49 @@ export async function runLocal(argv, deps = {}) {
756
438
  }
757
439
  const mention = parseModelMention(args.objective, Object.keys(config.models));
758
440
  const scope = args.user ?? "local";
759
- // Timeout policy mirrors the server (taskWallClockSec): run-local IS the pure single-user
760
- // TOC lane → NO wall clock by default (core's wedge safety-net still applies); explicit TASK_TIMEOUT_SEC
761
- // wins with its raise-only floor (discuss 3600s — a multi-agent coordinator legitimately exceeds the base).
762
- // [854]④ 不落此腿:per-request body.limits 是 HTTP body 车道(resolveTaskLimits,main.ts resolveSpec);
763
- // run-local 无 body,配速仍走 env(要 CLI flag 时另开件,别在这里半做)。
764
- // 🔴 2026-08-01 env fail-loud 族:原为 `Math.max(0, Math.floor(Number(env ?? "0")) || 0)` ——
765
- // `TASK_TIMEOUT_SEC="3600s"`(带单位后缀,运维最常见的写法)得 NaN,再被 `|| 0` 折成 0,
766
- // 而 0 在这里的语义是**无超时**。运维以为设了一小时上限,实际把超时关掉了,方向恰好相反,
767
- // 且启动期零提示。改走 numEnv:非法值当场 fail-loud 并指名键与实际值。
768
- // (numEnv 的注释早就预言了这个失败模式,只是它没 export、跨文件够不到,于是这两处手写了。)
769
441
  const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
770
442
  const timeoutSec = taskWallClockSec(taskTimeoutSec, false, scenarioName === "discuss");
771
443
  const mcp = mcpForScenario(config.mcpServers, scenarioName);
772
- // DESIGN-269 车1 件3(本腿半场):A2A peers 同样按场景过滤进 spec。**只有 center/config 基线这一半**
773
- // —— run-local 无 HTTP body,所以没有「调用方自带 peer」那条腿(与 [854]④ 的 body.limits 同姿势:
774
- // 要 CLI flag 时另开件,别在这里半做)。缺席仍是缺席:`a2aForScenario` 恒返回 undefined 而非 []。
775
444
  const a2a = a2aForScenario(config.a2aPeers, scenarioName);
776
- // A-002.9(#180 governance 缺口的同文件兄弟残余):运营方预算天花板两枚 —— 与 server 主路径
777
- // (boot/resolve-spec.ts)**同键同算法**,共用 budget.ts 的 `cappedCeiling`。此腿无 body(见上一段
778
- // 的 [854]④ 记账),所以 requested 恒缺席 ⇒ env 天花板在场时**直接成为** spec 值,天花板为 0/未设时
779
- // 仍是 undefined(键缺席=无预算,与改前逐字等价)。
780
- // ⚠️ 只接这两枚:`degrade`(MODEL_DEGRADE_*)是另一条旋钮线,不在本条案射程内。
781
445
  const maxCostUsd = cappedCeiling(undefined, config.maxTaskCostUsd);
782
446
  const maxTokens = cappedCeiling(undefined, config.maxTaskTokens);
783
- // core 5.8.0:预算族与墙钟同住 limits;三键各自缺席就不写(全缺 ⇒ 整个 limits 键缺席)。
784
447
  const limits = {
785
448
  ...(timeoutSec !== undefined ? { maxWalltimeMs: timeoutSec * 1000 } : {}),
786
449
  ...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
787
450
  ...(maxTokens !== undefined ? { maxTokens } : {}),
788
451
  };
789
- // design/181 件三:自建的 spec 字面量经**同一条**部署治理链(见 {@link applyLocalGovernance})——
790
- // 审批基线 + autonomy/commandPolicy/MANUAL_MODE_SHELL_GATE/守卫集,tighten-only,折叠属主仍是 core。
791
452
  const spec = applyLocalGovernance({
792
453
  objective: mention.cleanedText || args.objective,
793
454
  sessionId,
794
- // design/129: run-local IS the pure TOC lane → session-scoped background children
795
- // (CC Backgrounded semantics; parity with the server's single-user posture).
796
455
  backgroundScope: "session",
797
- // [1909]⑧(core 5.23.0 `TaskSpec.oneShot`,#205 件1 的**本腿**半场,codex R2-high 抓获):这条腿
798
- // 是**定义上**的一次性提交 —— 本文件头注第一句就是「跑完一条任务、打印 TaskResult、退出」,
799
- // `runLocal` 返回后 Runner 全被 dispose、进程即散。缺席这个键时,core 会照默认(交互态)给后台
800
- // 委派 / run_workflow 发「结束回合,你会被通知」的回执,而这里**没有下一个回合**能接住那条通知 ——
801
- // 那正是本键被造出来要消灭的丢结果病族(BGB drilldown case 2)。
802
- // 与 HTTP 腿的差别在**来源**而不在语义:那边是调用方表态(缺席不写键),这边是**进程形态的事实**,
803
- // 所以无条件为真,不看任何 body/表态(本腿根本收不到客户端表态,同 permissionMode 的 scoped-out 理由)。
804
456
  oneShot: true,
805
- principal: scope, // stable local memory/identity scope (single-user TOC; --user overrides)
806
- // design/138 S1: enable the memory engine for this run (spec.memory is core's per-task activation half of
807
- // the deps.memoryBackend switch). Scope = the --user identity (default "local", the single TOC user).
457
+ principal: scope,
808
458
  ...(memoryEngine ? { memory: memorySpecForRequest(scope, undefined) } : {}),
809
459
  model: mention.model ?? "default",
810
460
  getApiKeyAndHeaders: keyResolver,
811
461
  tools: cap.tools,
812
462
  skills: cap.skills,
813
463
  ...(cap.promptProvider ? { promptProvider: cap.promptProvider } : {}),
814
- // [849]→[2400] 场景层定死终验已无生产者(autonomous 退役);OR 折入形保留,与 resolveSpec 同语义。
815
464
  ...(cap.finalVerification === true ? { finalVerification: true } : {}),
816
465
  ...(mcp ? { mcp } : {}),
817
466
  ...(a2a ? { a2a } : {}),
818
- // core 5.8.0:timeoutSec 键退役 → maxWalltimeMs(毫秒);taskWallClockSec 仍产秒,换算在上方 limits 合成处。
819
467
  ...(Object.keys(limits).length > 0 ? { limits } : {}),
820
468
  }, config, workspaceDir);
821
469
  logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: config.remoteExec?.provider ?? "in-process" });
822
- // ── Run ONE task to completion (the sync /v1/tasks path: plain runTask, no verify/cascade). ──
823
470
  let result;
824
471
  let exitCode = 0;
825
- // #196:与 HTTP 腿同一条选路 —— 场景声明 hands:"none" 时任务跑无手 Runner(band 不 mount)。
826
472
  const taskRunner = pickHandsRunner(cap.hands, { full: runner, handsless: handslessRunner });
827
473
  try {
828
474
  result = await taskRunner.runTask(spec);
829
475
  }
830
476
  catch (err) {
831
477
  printErr(`task crashed: ${err instanceof Error ? err.message : String(err)}`);
832
- // Clean shutdown even on a crash.
833
478
  runner.sessions.dispose();
834
479
  subRunner.sessions.dispose();
835
- void lspManager?.dispose(); // Kill local child-process language servers so a re-run doesn't orphan them
480
+ void lspManager?.dispose();
836
481
  await fileBackend.dispose();
837
482
  return 1;
838
483
  }
839
- // ── Output: --json → the full TaskResult; else the final assistant text (or salvaged/blocked/error info). ──
840
484
  if (args.json) {
841
485
  print(JSON.stringify(result, null, 2));
842
486
  }
@@ -844,7 +488,6 @@ export async function runLocal(argv, deps = {}) {
844
488
  print(result.result);
845
489
  }
846
490
  else {
847
- // Non-completed: surface the most useful text we have, then a one-line status footer to stderr.
848
491
  const salvage = result.result || result.salvagedOutput || result.blockedReason || "";
849
492
  if (salvage)
850
493
  print(salvage);
@@ -852,9 +495,6 @@ export async function runLocal(argv, deps = {}) {
852
495
  }
853
496
  if (result.status !== "completed")
854
497
  exitCode = 1;
855
- // 142-S2.5-W1: 任务结束后 await 一轮同步(推本次 harvest 的新记忆到中心;串行链自动排在 boot 轮
856
- // 之后,绝不并发互踩)。await 保证轮次在进程退出前完成(fire-and-forget 会被 exit 腰斩);失败只
857
- // warn——同步是增益面,绝不改变任务本身的退出码。输出已打印完,这里的等待不挡用户看结果。
858
498
  if (memorySync) {
859
499
  try {
860
500
  await memorySync.syncOnce("post-task");
@@ -863,23 +503,16 @@ export async function runLocal(argv, deps = {}) {
863
503
  logger.warn("memory_sync_failed", { reason: "post-task", err: String(err) });
864
504
  }
865
505
  }
866
- // ── Clean shutdown: dispose the warm sessions; the host env workspace is rm -rf'd by the Runner on task
867
- // end (executionEnvFactory env → destroy()). No server/pool/reaper to tear down (none were created). ──
868
506
  runner.sessions.dispose();
869
507
  subRunner.sessions.dispose();
870
- void lspManager?.dispose(); // SAME as the crash path — kill local child-process language servers on the
871
- // SUCCESS path too. `runLocal` is exported/reusable (tests/embeddings call it directly, no process.exit), so
872
- // without this a completed LSP-touching run orphans a fresh set of stdio servers; the leak is per-invocation.
508
+ void lspManager?.dispose();
873
509
  await fileBackend.dispose();
874
510
  return exitCode;
875
511
  }
876
- /** Process entrypoint: run, print, exit with the resolved code. */
877
512
  async function main() {
878
513
  const code = await runLocal(process.argv.slice(2));
879
514
  process.exit(code);
880
515
  }
881
- // Only auto-run when invoked as a script (not when imported by the test). Compare REALPATH-resolved paths
882
- // so symlinked roots (/tmp→/private/tmp) and the tsx loader's file URL still match the entrypoint argv[1].
883
516
  function isMainModule() {
884
517
  const entry = process.argv[1];
885
518
  if (!entry)