akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +756 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +363 -679
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -427
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. package/dist/workflows/validator.js +0 -60
@@ -3,72 +3,55 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { defaultWhich } from "./detect.js";
5
5
  import { OPENCODE_SDK_SERVER_BIN } from "./profiles.js";
6
- /**
7
- * Engine name used for the synthesized entry. Matches the platform id so it
8
- * reads correctly in `akm workflow status`, frozen plans, and error messages.
9
- * A user-configured engine of the same name is preferred over synthesizing.
10
- */
6
+ /** Engine name used for the synthesized entry; matches the platform id. */
11
7
  export const FALLBACK_ENGINE_NAME = "opencode-sdk";
12
8
  /** Announcement text, surfaced once per run/dispatch. */
13
9
  export const FALLBACK_ANNOUNCEMENT = `No engine is configured; falling back to \`${FALLBACK_ENGINE_NAME}\` via the ` +
14
10
  `\`${OPENCODE_SDK_SERVER_BIN}\` binary on PATH — provider, model, and auth come ` +
15
11
  "from opencode's own configuration. " +
16
12
  "Run `akm setup`, or set `defaults.engine`, to choose explicitly.";
17
- /**
18
- * Failure suffix for every surface that needs an engine and found none.
19
- * Owned HERE, beside the probe whose outcome it describes: the probe follows a
20
- * configured `bin` when one is pinned, so a consumer restating "not on PATH"
21
- * would misdescribe a missing absolute path.
22
- */
13
+ /** Failure suffix for every surface that needs an engine and found none. */
23
14
  export const NO_ENGINE_MESSAGE_SUFFIX = `has no selected engine, and no usable \`${OPENCODE_SDK_SERVER_BIN}\` binary was found to fall back to.`;
24
- /**
25
- * Guidance used when the fallback itself is unavailable.
26
- *
27
- * The opencode route names a BINARY install, not `npm i @opencode-ai/sdk`:
28
- * that package is an HTTP client with no dependencies and cannot serve
29
- * anything on its own (see the module doc).
30
- */
15
+ /** Guidance used when the fallback itself is unavailable. */
31
16
  export const NO_ENGINE_REMEDY = "Run `akm setup` to detect an installed agent, or set one explicitly: " +
32
17
  '`akm config set engines.claude \'{"kind":"agent","platform":"claude"}\'` ' +
33
18
  "then `akm config set defaults.engine claude`. " +
34
19
  `Installing the \`${OPENCODE_SDK_SERVER_BIN}\` binary also works ` +
35
20
  "(`npm i -g opencode-ai`) — akm falls back to it automatically.";
36
21
  /**
37
- * Return a config whose `defaults.engine` resolves, applying the implicit
38
- * `opencode-sdk` fallback when it does not and an opencode binary is present.
39
- *
40
- * Pure with respect to the input: never mutates `config`. Returns the input
41
- * object identity unchanged when no fallback is needed, so callers can cheaply
42
- * detect the common case.
22
+ * The fallback engine when its `opencode` binary is present: an
23
+ * operator-configured `opencode-sdk` entry (whose pinned `bin`, possibly
24
+ * outside PATH, is what gets probed), else a bare opencode-sdk agent engine.
25
+ */
26
+ export function fallbackEngineConfig(config, whichFn = defaultWhich) {
27
+ const existing = config.engines?.[FALLBACK_ENGINE_NAME];
28
+ const bin = existing?.kind === "agent" ? existing.bin : undefined;
29
+ if (!whichFn(bin ?? OPENCODE_SDK_SERVER_BIN))
30
+ return undefined;
31
+ return existing ?? { kind: "agent", platform: "opencode-sdk" };
32
+ }
33
+ /**
34
+ * For diagnostics that report the effective engine: a config whose
35
+ * `defaults.engine` resolves, with the implicit `opencode-sdk` fallback when it
36
+ * does not and an opencode binary is present. Never mutates `config`; returns
37
+ * the same object when no fallback is needed.
43
38
  */
44
39
  export function withEngineFallback(config, whichFn = defaultWhich) {
45
40
  if (config.defaults?.engine)
46
41
  return { config };
47
- // An operator-configured engine of this name wins over a synthesized one —
48
- // theirs may carry a model, an llmEngine fallback, or a pinned bin. Probe
49
- // THAT bin rather than the bare command: a configured absolute path outside
50
- // PATH is still a usable engine, and reporting it as missing would contradict
51
- // the operator-configured-wins rule.
52
- const existing = config.engines?.[FALLBACK_ENGINE_NAME];
53
- if (!whichFn(existing?.bin ?? OPENCODE_SDK_SERVER_BIN))
42
+ const engine = fallbackEngineConfig(config, whichFn);
43
+ if (!engine)
54
44
  return { config };
55
- const engines = existing
56
- ? config.engines
57
- : { ...(config.engines ?? {}), [FALLBACK_ENGINE_NAME]: { kind: "agent", platform: "opencode-sdk" } };
58
45
  return {
59
46
  config: {
60
47
  ...config,
61
- engines,
48
+ engines: { ...(config.engines ?? {}), [FALLBACK_ENGINE_NAME]: engine },
62
49
  defaults: { ...(config.defaults ?? {}), engine: FALLBACK_ENGINE_NAME },
63
50
  },
64
51
  fallbackEngineName: FALLBACK_ENGINE_NAME,
65
52
  };
66
53
  }
67
- /**
68
- * The announcement, but only when the fallback candidate is the engine that
69
- * actually won selection. Returns `undefined` otherwise, so an explicitly
70
- * selected engine never triggers a claim that opencode supplied the model.
71
- */
54
+ /** The announcement, but only when the fallback candidate is the engine that actually won selection. */
72
55
  export function fallbackAnnouncement(fallbackEngineName, selectedEngineName) {
73
56
  if (!fallbackEngineName || selectedEngineName !== fallbackEngineName)
74
57
  return undefined;
@@ -14,21 +14,23 @@ import { warn } from "../../core/warn.js";
14
14
  import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js";
15
15
  import { getHarness } from "../harnesses/index.js";
16
16
  import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
17
- import { getBuiltinAgentProfile } from "./profiles.js";
18
- function hasOwn(value, key) {
19
- return Object.hasOwn(value, key);
20
- }
21
- function ownValue(value, key) {
22
- return hasOwn(value, key) ? value[key] : undefined;
23
- }
24
- function sterileRecord(value) {
25
- return Object.assign(Object.create(null), value);
26
- }
17
+ import { getBuiltinAgentProfile, OPENCODE_SDK_SERVER_BIN } from "./profiles.js";
18
+ const LLM_CONNECTION_FIELDS = [
19
+ "provider",
20
+ "endpoint",
21
+ "model",
22
+ "temperature",
23
+ "maxTokens",
24
+ "supportsJsonSchema",
25
+ "extraParams",
26
+ "contextLength",
27
+ "enableThinking",
28
+ "reasoningEffort",
29
+ ];
27
30
  function envName(reference) {
28
31
  const match = /^\$(?:\{)?([A-Za-z_][A-Za-z0-9_]*)(?:\})?$/.exec(reference);
29
32
  return match?.[1];
30
33
  }
31
- /** Expand a leading `~` the same way `loadSetupConfigFromFile` does for `--from <file>`. */
32
34
  function expandHomePath(filePath) {
33
35
  return filePath.startsWith("~") ? path.join(os.homedir(), filePath.slice(1)) : filePath;
34
36
  }
@@ -36,12 +38,7 @@ function expandHomePath(filePath) {
36
38
  function trimTrailingNewline(raw) {
37
39
  return raw.replace(/\r?\n$/, "");
38
40
  }
39
- /**
40
- * Read a file-backed credential (#905) at the dispatch boundary. Never
41
- * includes the file's content in a thrown message — only the engine name and
42
- * path, so a misconfigured `apiKeyFile` cannot leak its (partial) contents
43
- * into a log or error report.
44
- */
41
+ /** Read a file-backed credential; errors name the engine and path, never the contents. */
45
42
  function readApiKeyFile(engineName, filePath) {
46
43
  let raw;
47
44
  try {
@@ -58,12 +55,7 @@ function readApiKeyFile(engineName, filePath) {
58
55
  }
59
56
  return value;
60
57
  }
61
- /**
62
- * Best-effort, non-throwing read of a file-backed credential's current value
63
- * (#905), for redaction inventories and health probes that must never fail
64
- * just because a value collector ran ahead of the real dispatch — a missing
65
- * or empty file is reported by {@link readApiKeyFile} at the actual call.
66
- */
58
+ /** Best-effort, non-throwing read of a file-backed credential for redaction and health. */
67
59
  export function lookupApiKeyFileValue(filePath) {
68
60
  try {
69
61
  const value = trimTrailingNewline(fs.readFileSync(filePath, "utf8"));
@@ -73,13 +65,7 @@ export function lookupApiKeyFileValue(filePath) {
73
65
  return undefined;
74
66
  }
75
67
  }
76
- /**
77
- * Best-effort, non-throwing read of a secret-store-backed credential's
78
- * current value (#953), for redaction inventories and health probes that
79
- * must never fail just because a value collector ran ahead of the real
80
- * dispatch — an unresolvable reference is reported by
81
- * {@link resolveLlmCredentialValue} at the actual call.
82
- */
68
+ /** Best-effort, non-throwing read of a secret-store credential for redaction and health. */
83
69
  export function lookupApiKeySecretRefValue(ref) {
84
70
  try {
85
71
  return resolveSecret(ref, resolveSecretFromStore);
@@ -90,52 +76,39 @@ export function lookupApiKeySecretRefValue(ref) {
90
76
  }
91
77
  function selectedEngineName(config, layers, llmOnly) {
92
78
  for (let index = layers.length - 1; index >= 0; index--) {
93
- const layer = layers[index];
94
- if (!layer)
95
- continue;
96
- const engine = ownValue(layer, "engine");
79
+ const engine = layers[index]?.engine;
97
80
  if (engine !== undefined)
98
81
  return engine;
99
82
  }
100
- const defaults = ownValue(config, "defaults");
101
- return defaults ? ownValue(defaults, llmOnly ? "llmEngine" : "engine") : undefined;
83
+ return llmOnly ? config.defaults?.llmEngine : config.defaults?.engine;
102
84
  }
103
- function resolveEngineConfig(name, config) {
104
- const engines = ownValue(config, "engines");
105
- const engine = engines && hasOwn(engines, name) ? engines[name] : undefined;
106
- if (!engine) {
85
+ /** The configured engine of this name; an unconfigured name is an error. */
86
+ export function configuredEngine(name, config) {
87
+ const engine = config.engines && Object.hasOwn(config.engines, name) ? config.engines[name] : undefined;
88
+ if (!engine)
107
89
  throw new ConfigError(`Engine "${name}" is not configured.`, "INVALID_CONFIG_FILE");
108
- }
109
90
  return engine;
110
91
  }
111
92
  function resolveCredential(name, engine, config) {
112
- const apiKey = ownValue(engine, "apiKey");
113
- if (apiKey !== undefined) {
114
- const explicit = envName(apiKey);
93
+ if (engine.apiKey !== undefined) {
94
+ const explicit = envName(engine.apiKey);
115
95
  if (explicit)
116
96
  return { names: [explicit], required: true };
117
- // #953: a secret-store reference has no env descriptor — resolved
118
- // separately onto `ResolvedLlmUse.apiKeySecretRef` — mirroring how
119
- // apiKeyFile above is its own credential source.
120
- if (SECRET_STORE_REFERENCE_PATTERN.test(apiKey))
97
+ // A secret-store reference is carried as `apiKeySecretRef` instead.
98
+ if (SECRET_STORE_REFERENCE_PATTERN.test(engine.apiKey))
121
99
  return undefined;
122
100
  throw new ConfigError(`Engine "${name}" has an invalid symbolic apiKey reference.`, "INVALID_CONFIG_FILE");
123
101
  }
124
- // #905: an explicit apiKeyFile is its own credential source — resolved
125
- // separately onto `ResolvedLlmUse.apiKeyFile` — so it does not also fall
126
- // through to the implicit AKM_ENGINE_<NAME>_API_KEY convention below.
127
- if (ownValue(engine, "apiKeyFile") !== undefined)
102
+ // An explicit apiKeyFile is its own credential source; it does not also
103
+ // fall through to the implicit AKM_ENGINE_<NAME>_API_KEY convention.
104
+ if (engine.apiKeyFile !== undefined)
128
105
  return undefined;
129
106
  const specific = `AKM_ENGINE_${name.toUpperCase().replaceAll("-", "_")}_API_KEY`;
130
- const defaults = ownValue(config, "defaults");
131
- return (defaults ? ownValue(defaults, "llmEngine") : undefined) === name
107
+ return config.defaults?.llmEngine === name
132
108
  ? { names: [specific, "AKM_LLM_API_KEY"], required: false }
133
109
  : { names: [specific], required: false };
134
110
  }
135
- /**
136
- * Lookup-only credential projection used by redaction inventories. Returns the
137
- * first non-empty trimmed value without enforcing a required descriptor.
138
- */
111
+ /** Lookup-only credential projection used by redaction inventories. */
139
112
  export function lookupCredentialFromEnv(credential, envSource = process.env) {
140
113
  for (const name of credential?.names ?? []) {
141
114
  const candidate = envSource[name]?.trim();
@@ -144,10 +117,7 @@ export function lookupCredentialFromEnv(credential, envSource = process.env) {
144
117
  }
145
118
  return undefined;
146
119
  }
147
- /**
148
- * The enforcing env-credential seam. Live and frozen dispatch use the same
149
- * ordered lookup; a missing required descriptor names its primary variable.
150
- */
120
+ /** The enforcing env-credential lookup; a missing required descriptor names its primary variable. */
151
121
  export function resolveCredentialFromEnv(credential, envSource = process.env) {
152
122
  const value = lookupCredentialFromEnv(credential, envSource);
153
123
  if (value)
@@ -158,12 +128,9 @@ export function resolveCredentialFromEnv(credential, envSource = process.env) {
158
128
  return undefined;
159
129
  }
160
130
  /**
161
- * The enforcing credential seam for one resolved LLM engine or SDK fallback
162
- * (#905): the symbolic env-var descriptor first (throws if a required one is
163
- * missing), then the file-backed alternative when no env descriptor applies.
164
- * Call this once per operation — lease acquisition, or direct materialize —
165
- * so a whole operation observes one stable credential value instead of
166
- * re-reading the file on every dispatch within it.
131
+ * Resolve one LLM credential value at the dispatch boundary: the env
132
+ * descriptor first, then the file-backed value, then the secret-store
133
+ * reference. Nothing before dispatch reads any of them.
167
134
  */
168
135
  export function resolveLlmCredentialValue(engine, credential, apiKeyFile, apiKeySecretRef, envSource = process.env) {
169
136
  const envValue = resolveCredentialFromEnv(credential, envSource);
@@ -171,25 +138,12 @@ export function resolveLlmCredentialValue(engine, credential, apiKeyFile, apiKey
171
138
  return envValue;
172
139
  if (apiKeyFile !== undefined)
173
140
  return readApiKeyFile(engine, apiKeyFile);
174
- // #953: a secret-store reference is the last fallback tier, reusing the
175
- // same resolveSecret() helper llm/client.ts and embedders/remote.ts call
176
- // directly — throws SECRET_REFERENCE_UNRESOLVED naming only the reference.
177
141
  return apiKeySecretRef !== undefined ? resolveSecret(apiKeySecretRef, resolveSecretFromStore) : undefined;
178
142
  }
179
143
  /**
180
- * Non-throwing credential-presence check for `akm health`, the
181
- * improve-strategy probe (#953), and improve's own plan builder (#957): an env
182
- * value is present, or a file-backed credential is readable and non-empty, or
183
- * a secret-store reference resolves. Never reads env/disk/store speculatively
184
- * beyond what's needed to answer "is something here", and never throws on a
185
- * broken/missing source — that is reported by {@link resolveLlmCredentialValue}
186
- * at the real dispatch.
187
- *
188
- * On failure, `reference` and `reason` name WHICH env var / file / secret
189
- * reference is missing (never its value) — the operator's own shell often
190
- * passes the same check a scheduler's stripped-down environment fails, so the
191
- * caller needs to say which reference is the problem. Callers that must never
192
- * name the reference (`akm health`'s evidence/message) use only `.available`.
144
+ * Non-throwing credential-presence check for `akm health` and improve's
145
+ * strategy probe. Names WHICH env var / file / secret reference is missing,
146
+ * never its value; a broken source is reported by the real dispatch.
193
147
  */
194
148
  export function describeLlmCredentialAvailability(resolved, env = process.env) {
195
149
  if (resolved.credential?.required) {
@@ -218,18 +172,13 @@ export function describeLlmCredentialAvailability(resolved, env = process.env) {
218
172
  }
219
173
  return { available: true };
220
174
  }
221
- /**
222
- * Thin boolean wrapper over {@link describeLlmCredentialAvailability} for
223
- * callers (`akm health`'s engine-reachability probes) that only need a
224
- * yes/no answer and never surface the reference.
225
- */
226
175
  export function isLlmCredentialAvailable(resolved, env = process.env) {
227
176
  return describeLlmCredentialAvailability(resolved, env).available;
228
177
  }
229
- /** Collect materialized engine credentials for output and persistence redaction. */
178
+ /** Collect every configured engine's current credential value for output and persistence redaction. */
230
179
  export function collectEngineCredentialValues(config, envSource = process.env) {
231
180
  const values = new Set();
232
- for (const [name, engine] of Object.entries(ownValue(config, "engines") ?? {})) {
181
+ for (const [name, engine] of Object.entries(config.engines ?? {})) {
233
182
  if (engine.kind !== "llm")
234
183
  continue;
235
184
  for (const envVar of resolveCredential(name, engine, config)?.names ?? []) {
@@ -237,19 +186,15 @@ export function collectEngineCredentialValues(config, envSource = process.env) {
237
186
  if (value)
238
187
  values.add(value);
239
188
  }
240
- // #905: file-backed credential — best-effort, so a broken apiKeyFile on
241
- // one engine never stops redaction from collecting every other engine's
242
- // credential too.
243
- const apiKeyFile = ownValue(engine, "apiKeyFile");
244
- if (apiKeyFile !== undefined) {
245
- const value = lookupApiKeyFileValue(expandHomePath(apiKeyFile));
189
+ // File and secret-store lookups are best-effort so one broken engine never
190
+ // stops redaction from collecting every other engine's value.
191
+ if (engine.apiKeyFile !== undefined) {
192
+ const value = lookupApiKeyFileValue(expandHomePath(engine.apiKeyFile));
246
193
  if (value)
247
194
  values.add(value);
248
195
  }
249
- // #953: secret-store-backed credential — best-effort for the same reason.
250
- const apiKey = ownValue(engine, "apiKey");
251
- if (apiKey !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(apiKey)) {
252
- const value = lookupApiKeySecretRefValue(apiKey);
196
+ if (engine.apiKey !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(engine.apiKey)) {
197
+ const value = lookupApiKeySecretRefValue(engine.apiKey);
253
198
  if (value)
254
199
  values.add(value);
255
200
  }
@@ -258,27 +203,17 @@ export function collectEngineCredentialValues(config, envSource = process.env) {
258
203
  }
259
204
  function effectiveTimeout(engine, layers, fallback) {
260
205
  for (let index = layers.length - 1; index >= 0; index--) {
261
- if (hasOwn(layers[index] ?? {}, "timeoutMs"))
262
- return layers[index]?.timeoutMs ?? null;
206
+ const layer = layers[index];
207
+ if (layer && Object.hasOwn(layer, "timeoutMs"))
208
+ return layer.timeoutMs ?? null;
263
209
  }
264
- return hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : fallback;
210
+ return Object.hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : fallback;
265
211
  }
266
212
  function rawLlmConnection(engine) {
267
- const connection = {
268
- provider: ownValue(engine, "provider"),
269
- endpoint: ownValue(engine, "endpoint"),
270
- model: ownValue(engine, "model"),
271
- temperature: ownValue(engine, "temperature"),
272
- maxTokens: ownValue(engine, "maxTokens"),
273
- supportsJsonSchema: ownValue(engine, "supportsJsonSchema"),
274
- extraParams: ownValue(engine, "extraParams"),
275
- contextLength: ownValue(engine, "contextLength"),
276
- enableThinking: ownValue(engine, "enableThinking"),
277
- reasoningEffort: ownValue(engine, "reasoningEffort"),
278
- };
279
- for (const key of Object.keys(connection)) {
280
- if (connection[key] === undefined)
281
- delete connection[key];
213
+ const connection = {};
214
+ for (const key of LLM_CONNECTION_FIELDS) {
215
+ if (engine[key] !== undefined)
216
+ connection[key] = engine[key];
282
217
  }
283
218
  return connection;
284
219
  }
@@ -289,11 +224,10 @@ export function resolveLlmEngineUse(config, layers, options = {}) {
289
224
  return undefined;
290
225
  throw new ConfigError("No LLM engine is selected. Set defaults.llmEngine or specify engine.", "LLM_NOT_CONFIGURED");
291
226
  }
292
- const engine = resolveEngineConfig(name, config);
227
+ const engine = configuredEngine(name, config);
293
228
  if (engine.kind !== "llm") {
294
- const defaults = ownValue(config, "defaults");
295
- const fallbackName = ownValue(engine, "llmEngine") ?? (defaults ? ownValue(defaults, "llmEngine") : undefined);
296
- const fallbackEngine = fallbackName ? resolveEngineConfig(fallbackName, config) : undefined;
229
+ const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
230
+ const fallbackEngine = fallbackName ? configuredEngine(fallbackName, config) : undefined;
297
231
  if (!fallbackEngine || fallbackEngine.kind !== "llm") {
298
232
  if (options.optional)
299
233
  return undefined;
@@ -308,55 +242,41 @@ export function resolveLlmEngineUse(config, layers, options = {}) {
308
242
  }
309
243
  let connection = rawLlmConnection(engine);
310
244
  for (const layer of layers) {
311
- const llm = ownValue(layer, "llm");
312
- const model = ownValue(layer, "model");
313
- if (llm)
314
- connection = deepMergeConfig(connection, llm);
315
- if (model !== undefined)
316
- connection.model = model;
245
+ if (layer.llm)
246
+ connection = deepMergeConfig(connection, layer.llm);
247
+ if (layer.model !== undefined)
248
+ connection.model = layer.model;
317
249
  }
318
250
  for (const key of Object.keys(connection)) {
319
251
  if (connection[key] === undefined)
320
252
  delete connection[key];
321
253
  }
322
- const apiKeyFile = ownValue(engine, "apiKeyFile");
323
- const apiKeyRaw = ownValue(engine, "apiKey");
324
- const apiKeySecretRef = apiKeyRaw !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(apiKeyRaw) ? apiKeyRaw : undefined;
254
+ const apiKeySecretRef = engine.apiKey !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(engine.apiKey) ? engine.apiKey : undefined;
325
255
  return {
326
256
  engine: name,
327
- connection: sterileRecord(connection),
257
+ connection: connection,
328
258
  credential: resolveCredential(name, engine, config),
329
- ...(apiKeyFile !== undefined ? { apiKeyFile: expandHomePath(apiKeyFile) } : {}),
259
+ ...(engine.apiKeyFile !== undefined ? { apiKeyFile: expandHomePath(engine.apiKeyFile) } : {}),
330
260
  ...(apiKeySecretRef !== undefined ? { apiKeySecretRef } : {}),
331
261
  timeoutMs: effectiveTimeout(engine, layers, DEFAULT_LLM_TIMEOUT_MS),
332
262
  };
333
263
  }
334
- /**
335
- * Inject an already-resolved credential value into a connection. Callers
336
- * resolve the value themselves via {@link resolveLlmCredentialValue} (or its
337
- * lease-cached equivalent) — this function never reads env or disk itself, so
338
- * a frozen `ResolvedLlmUse`/`RunnerSpec` plan object can be materialized
339
- * repeatedly without re-triggering I/O per call.
340
- */
264
+ /** Inject an already-resolved credential value into a connection; reads nothing itself. */
341
265
  export function materializeLlmConnectionWithCredential(resolved, credentialValue) {
342
- const extraParams = ownValue(resolved.connection, "extraParams");
266
+ const extraParams = resolved.connection.extraParams;
343
267
  if (extraParams !== undefined) {
344
268
  const issue = validateExtraParams(extraParams)[0];
345
269
  if (issue) {
346
270
  throw new ConfigError(formatExtraParamsIssue(`Engine "${resolved.engine}" extraParams`, issue), "INVALID_CONFIG_FILE");
347
271
  }
348
272
  }
349
- return sterileRecord({
273
+ return {
350
274
  ...resolved.connection,
351
275
  ...(credentialValue ? { apiKey: credentialValue } : {}),
352
276
  timeoutMs: resolved.timeoutMs,
353
- });
277
+ };
354
278
  }
355
- /**
356
- * Read and inject one resolved credential at the runtime boundary: the
357
- * symbolic `$VAR` reference, or the file-backed alternative (#905) when the
358
- * engine has no env descriptor.
359
- */
279
+ /** Read and inject one resolved credential at the runtime boundary. */
360
280
  export function materializeLlmConnection(resolved, envSource = process.env) {
361
281
  return materializeLlmConnectionWithCredential(resolved, resolveLlmCredentialValue(resolved.engine, resolved.credential, resolved.apiKeyFile, resolved.apiKeySecretRef, envSource));
362
282
  }
@@ -368,33 +288,29 @@ function lowerAgentEngine(name, engine, config) {
368
288
  const platform = harness.id;
369
289
  const sdk = platform === "opencode-sdk";
370
290
  const builtin = getBuiltinAgentProfile(platform);
371
- const bin = ownValue(engine, "bin");
372
- const args = ownValue(engine, "args");
373
- const workspace = ownValue(engine, "workspace");
374
- const model = ownValue(engine, "model");
375
- const profile = sterileRecord({
291
+ const profile = {
376
292
  name,
377
293
  platform,
378
294
  personaChannel: sdk ? "native" : (harness.agentBuilder?.personaChannel ?? "prompt"),
379
- bin: bin ?? builtin?.bin ?? (sdk ? "opencode" : platform),
380
- args: args ?? builtin?.args ?? [],
295
+ bin: engine.bin ?? builtin?.bin ?? (sdk ? OPENCODE_SDK_SERVER_BIN : platform),
296
+ args: engine.args ?? builtin?.args ?? [],
381
297
  stdio: "captured",
382
298
  ...(builtin?.env ? { env: builtin.env } : {}),
383
299
  envPassthrough: builtin?.envPassthrough ?? [],
384
300
  parseOutput: "text",
385
- ...(workspace ? { workspace: path.resolve(workspace) } : {}),
386
- ...(model ? { model } : {}),
387
- });
301
+ ...(engine.workspace ? { workspace: path.resolve(engine.workspace) } : {}),
302
+ ...(engine.model ? { model: engine.model } : {}),
303
+ };
304
+ const ownTimeout = Object.hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : undefined;
388
305
  if (!sdk) {
389
306
  return {
390
307
  kind: "agent",
391
308
  engine: name,
392
309
  profile,
393
- timeoutMs: hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : DEFAULT_AGENT_TIMEOUT_MS,
310
+ timeoutMs: ownTimeout !== undefined ? ownTimeout : DEFAULT_AGENT_TIMEOUT_MS,
394
311
  };
395
312
  }
396
- const defaults = ownValue(config, "defaults");
397
- const fallbackName = ownValue(engine, "llmEngine") ?? (defaults ? ownValue(defaults, "llmEngine") : undefined);
313
+ const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
398
314
  const fallback = fallbackName
399
315
  ? resolveLlmEngineUse(config, [{ engine: fallbackName }], { optional: true })
400
316
  : undefined;
@@ -411,27 +327,21 @@ function lowerAgentEngine(name, engine, config) {
411
327
  fallbackTimeoutMs: fallback.timeoutMs,
412
328
  }
413
329
  : {}),
414
- timeoutMs: hasOwn(engine, "timeoutMs")
415
- ? (engine.timeoutMs ?? null)
416
- : (fallback?.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS),
330
+ timeoutMs: ownTimeout !== undefined ? ownTimeout : (fallback?.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS),
417
331
  };
418
332
  }
419
- /** Lower a named engine through the canonical harness platform. */
420
- export function resolveEngine(name, config) {
421
- const engine = resolveEngineConfig(name, config);
422
- if (engine.kind === "llm") {
423
- const resolved = resolveLlmEngineUse(config, [{ engine: name }]);
424
- if (!resolved)
425
- throw new ConfigError(`LLM engine "${name}" could not be resolved.`, "LLM_NOT_CONFIGURED");
426
- return {
427
- kind: "llm",
428
- engine: name,
429
- connection: resolved.connection,
430
- ...(resolved.credential ? { credential: resolved.credential } : {}),
431
- ...(resolved.apiKeyFile ? { apiKeyFile: resolved.apiKeyFile } : {}),
432
- ...(resolved.apiKeySecretRef ? { apiKeySecretRef: resolved.apiKeySecretRef } : {}),
433
- timeoutMs: resolved.timeoutMs,
434
- };
435
- }
436
- return lowerAgentEngine(name, engine, config);
333
+ /** Resolve a configured engine name to its runner: an LLM connection, a spawned agent, or the SDK. */
334
+ export function resolveEngine(name, config, engine = configuredEngine(name, config)) {
335
+ if (engine.kind !== "llm")
336
+ return lowerAgentEngine(name, engine, config);
337
+ const resolved = resolveLlmEngineUse(config, [{ engine: name }]);
338
+ return {
339
+ kind: "llm",
340
+ engine: name,
341
+ connection: resolved.connection,
342
+ ...(resolved.credential ? { credential: resolved.credential } : {}),
343
+ ...(resolved.apiKeyFile ? { apiKeyFile: resolved.apiKeyFile } : {}),
344
+ ...(resolved.apiKeySecretRef ? { apiKeySecretRef: resolved.apiKeySecretRef } : {}),
345
+ timeoutMs: resolved.timeoutMs,
346
+ };
437
347
  }