akm-cli 0.9.0-rc.0 → 0.9.0-rc.2

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 (262) hide show
  1. package/CHANGELOG.md +339 -2
  2. package/SECURITY.md +23 -24
  3. package/dist/assets/help/help-improve.md +10 -10
  4. package/dist/assets/hints/cli-hints-full.md +44 -10
  5. package/dist/assets/hints/cli-hints-short.md +6 -2
  6. package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
  7. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
  8. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
  9. package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
  10. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  11. package/dist/assets/stash-skeleton/README.md +28 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
  24. package/dist/assets/tasks/core/backup.yml +1 -0
  25. package/dist/assets/tasks/core/extract.yml +1 -0
  26. package/dist/assets/tasks/core/improve.yml +1 -0
  27. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  28. package/dist/assets/tasks/core/sync.yml +1 -0
  29. package/dist/assets/tasks/core/version-check.yml +1 -0
  30. package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
  31. package/dist/assets/templates/html/health.html +5 -1
  32. package/dist/cli/config-migrate.js +31 -138
  33. package/dist/cli/config-validate.js +10 -8
  34. package/dist/cli.js +48 -14
  35. package/dist/commands/agent/agent-dispatch.js +17 -16
  36. package/dist/commands/agent/agent-support.js +0 -24
  37. package/dist/commands/agent/contribute-cli.js +5 -15
  38. package/dist/commands/backup-cli.js +54 -0
  39. package/dist/commands/config-cli.js +45 -159
  40. package/dist/commands/env/env-binding.js +95 -0
  41. package/dist/commands/env/env-cli.js +8 -65
  42. package/dist/commands/env/secret.js +8 -5
  43. package/dist/commands/health/checks.js +130 -83
  44. package/dist/commands/health/html-report.js +4 -0
  45. package/dist/commands/health/improve-metrics.js +30 -32
  46. package/dist/commands/health/llm-usage.js +19 -19
  47. package/dist/commands/health/md-report.js +4 -0
  48. package/dist/commands/health/metrics.js +2 -1
  49. package/dist/commands/health/surfaces.js +5 -4
  50. package/dist/commands/health.js +1 -1
  51. package/dist/commands/improve/consolidate/chunking.js +2 -2
  52. package/dist/commands/improve/consolidate.js +28 -25
  53. package/dist/commands/improve/distill/promote-memory.js +5 -12
  54. package/dist/commands/improve/distill/quality-gate.js +5 -7
  55. package/dist/commands/improve/distill.js +16 -5
  56. package/dist/commands/improve/eligibility.js +22 -12
  57. package/dist/commands/improve/extract-cli.js +47 -19
  58. package/dist/commands/improve/extract.js +110 -62
  59. package/dist/commands/improve/improve-cli.js +38 -16
  60. package/dist/commands/improve/improve-result-file.js +30 -24
  61. package/dist/commands/improve/improve-strategies.js +137 -0
  62. package/dist/commands/improve/improve.js +60 -30
  63. package/dist/commands/improve/locks.js +66 -45
  64. package/dist/commands/improve/loop-stages.js +75 -33
  65. package/dist/commands/improve/memory/memory-belief.js +79 -7
  66. package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
  67. package/dist/commands/improve/preparation.js +71 -73
  68. package/dist/commands/improve/procedural.js +3 -2
  69. package/dist/commands/improve/recombine.js +2 -1
  70. package/dist/commands/improve/reflect.js +119 -214
  71. package/dist/commands/improve/shared.js +11 -5
  72. package/dist/commands/lint/base-linter.js +152 -42
  73. package/dist/commands/mv-cli.js +809 -0
  74. package/dist/commands/proposal/proposal-cli.js +18 -8
  75. package/dist/commands/proposal/propose.js +64 -69
  76. package/dist/commands/read/knowledge.js +436 -4
  77. package/dist/commands/read/remember-cli.js +39 -2
  78. package/dist/commands/read/search-cli.js +6 -1
  79. package/dist/commands/registry-cli.js +29 -14
  80. package/dist/commands/remember.js +2 -0
  81. package/dist/commands/sources/init.js +13 -14
  82. package/dist/commands/sources/migration-help.js +7 -4
  83. package/dist/commands/sources/schema-repair.js +2 -4
  84. package/dist/commands/sources/source-add.js +62 -73
  85. package/dist/commands/sources/source-manage.js +50 -46
  86. package/dist/commands/sources/stash-cli.js +41 -4
  87. package/dist/commands/tasks/default-tasks.js +12 -12
  88. package/dist/commands/tasks/tasks-cli.js +7 -3
  89. package/dist/commands/tasks/tasks.js +113 -18
  90. package/dist/commands/wiki-cli.js +9 -10
  91. package/dist/commands/workflow-cli.js +276 -12
  92. package/dist/core/asset/asset-spec.js +58 -1
  93. package/dist/core/asset/frontmatter.js +12 -2
  94. package/dist/core/common.js +5 -3
  95. package/dist/core/config/config-io.js +28 -17
  96. package/dist/core/config/config-schema.js +379 -66
  97. package/dist/core/config/config-types.js +3 -3
  98. package/dist/core/config/config-version.js +29 -0
  99. package/dist/core/config/config-walker.js +98 -27
  100. package/dist/core/config/config.js +132 -266
  101. package/dist/core/config/deep-merge.js +41 -0
  102. package/dist/core/config/engine-semantics.js +32 -0
  103. package/dist/core/errors.js +2 -2
  104. package/dist/core/extra-params.js +61 -0
  105. package/dist/core/file-lock.js +201 -56
  106. package/dist/core/improve-result.js +178 -0
  107. package/dist/core/json-schema.js +142 -0
  108. package/dist/core/maintenance-barrier.js +119 -0
  109. package/dist/core/migration-backup.js +416 -0
  110. package/dist/core/paths.js +3 -0
  111. package/dist/core/redaction.js +358 -0
  112. package/dist/core/state/migrations.js +17 -2
  113. package/dist/core/state-db.js +44 -1
  114. package/dist/indexer/db/db.js +118 -2
  115. package/dist/indexer/graph/graph-extraction.js +28 -16
  116. package/dist/indexer/index-writer-lock.js +31 -24
  117. package/dist/indexer/index-written-assets.js +15 -6
  118. package/dist/indexer/indexer.js +47 -2
  119. package/dist/indexer/passes/memory-inference.js +10 -6
  120. package/dist/indexer/passes/metadata.js +250 -0
  121. package/dist/indexer/search/db-search.js +111 -44
  122. package/dist/indexer/search/fts-query.js +41 -0
  123. package/dist/indexer/search/ranking-contributors.js +48 -0
  124. package/dist/indexer/search/ranking.js +36 -23
  125. package/dist/indexer/search/search-fields.js +11 -1
  126. package/dist/indexer/walk/matchers.js +39 -0
  127. package/dist/integrations/agent/builder-shared.js +7 -0
  128. package/dist/integrations/agent/builders.js +5 -50
  129. package/dist/integrations/agent/config.js +3 -143
  130. package/dist/integrations/agent/detect.js +17 -2
  131. package/dist/integrations/agent/engine-resolution.js +202 -0
  132. package/dist/integrations/agent/index.js +1 -2
  133. package/dist/integrations/agent/model-aliases.js +16 -2
  134. package/dist/integrations/agent/profiles.js +36 -62
  135. package/dist/integrations/agent/runner-dispatch.js +91 -4
  136. package/dist/integrations/agent/runner.js +76 -207
  137. package/dist/integrations/agent/spawn.js +141 -20
  138. package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
  139. package/dist/integrations/harnesses/aider/index.js +57 -0
  140. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  141. package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
  142. package/dist/integrations/harnesses/amazonq/index.js +58 -0
  143. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  144. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  145. package/dist/integrations/harnesses/claude/index.js +25 -25
  146. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  147. package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
  148. package/dist/integrations/harnesses/codex/index.js +62 -0
  149. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  150. package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
  151. package/dist/integrations/harnesses/copilot/index.js +59 -0
  152. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  153. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  154. package/dist/integrations/harnesses/gemini/index.js +59 -0
  155. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  156. package/dist/integrations/harnesses/index.js +27 -28
  157. package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
  158. package/dist/integrations/harnesses/opencode/index.js +15 -22
  159. package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
  160. package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
  161. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  162. package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
  163. package/dist/integrations/harnesses/openhands/index.js +57 -0
  164. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  165. package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
  166. package/dist/integrations/harnesses/pi/index.js +57 -0
  167. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  168. package/dist/integrations/harnesses/types.js +8 -32
  169. package/dist/integrations/lockfile.js +32 -21
  170. package/dist/integrations/session-logs/index.js +24 -11
  171. package/dist/llm/client.js +48 -14
  172. package/dist/llm/feature-gate.js +15 -47
  173. package/dist/llm/graph-extract.js +1 -1
  174. package/dist/llm/index-passes.js +8 -42
  175. package/dist/llm/memory-infer-impl.js +1 -1
  176. package/dist/llm/usage-persist.js +4 -0
  177. package/dist/llm/usage-telemetry.js +35 -5
  178. package/dist/output/renderers.js +3 -2
  179. package/dist/output/shapes/helpers.js +2 -1
  180. package/dist/output/shapes/passthrough.js +6 -0
  181. package/dist/output/text/helpers.js +215 -2
  182. package/dist/output/text/workflow.js +3 -1
  183. package/dist/schemas/akm-config.json +16638 -0
  184. package/dist/schemas/akm-task.json +87 -0
  185. package/dist/schemas/akm-workflow.json +372 -0
  186. package/dist/scripts/migrate-storage.js +10944 -8801
  187. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
  188. package/dist/setup/detected-engines.js +142 -0
  189. package/dist/setup/engine-config.js +89 -0
  190. package/dist/setup/setup.js +236 -132
  191. package/dist/setup/steps/connection.js +61 -32
  192. package/dist/setup/steps/platforms.js +4 -4
  193. package/dist/setup/steps.js +3 -2
  194. package/dist/storage/database.js +13 -1
  195. package/dist/storage/engines/sqlite-migrations.js +1 -0
  196. package/dist/storage/repositories/improve-runs-repository.js +5 -5
  197. package/dist/storage/repositories/task-history-repository.js +78 -0
  198. package/dist/storage/repositories/workflow-runs-repository.js +190 -1
  199. package/dist/tasks/parser.js +138 -52
  200. package/dist/tasks/runner.js +71 -75
  201. package/dist/tasks/schema.js +1 -1
  202. package/dist/tasks/validator.js +11 -6
  203. package/dist/text-import-hook.mjs +1 -1
  204. package/dist/wiki/wiki.js +9 -8
  205. package/dist/workflows/authoring/authoring.js +123 -10
  206. package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
  207. package/dist/workflows/cli.js +4 -0
  208. package/dist/workflows/concurrency-policy.js +15 -0
  209. package/dist/workflows/db.js +200 -13
  210. package/dist/workflows/exec/brief.js +478 -0
  211. package/dist/workflows/exec/frozen-judge.js +47 -0
  212. package/dist/workflows/exec/native-executor.js +1034 -0
  213. package/dist/workflows/exec/param-secrets.js +115 -0
  214. package/dist/workflows/exec/report.js +1355 -0
  215. package/dist/workflows/exec/run-workflow.js +609 -0
  216. package/dist/workflows/exec/scheduler.js +71 -0
  217. package/dist/workflows/exec/step-work.js +1212 -0
  218. package/dist/workflows/exec/unit-writer.js +23 -0
  219. package/dist/workflows/exec/watch.js +116 -0
  220. package/dist/workflows/exec/worktree.js +171 -0
  221. package/dist/workflows/ir/compile.js +375 -0
  222. package/dist/workflows/ir/freeze.js +243 -0
  223. package/dist/workflows/ir/params.js +54 -0
  224. package/dist/workflows/ir/plan-hash.js +68 -0
  225. package/dist/workflows/ir/schema.js +545 -0
  226. package/dist/workflows/parser.js +10 -1
  227. package/dist/workflows/program/expressions.js +369 -0
  228. package/dist/workflows/program/parser.js +869 -0
  229. package/dist/workflows/program/project.js +104 -0
  230. package/dist/workflows/program/schema.js +54 -0
  231. package/dist/workflows/renderer.js +82 -5
  232. package/dist/workflows/resource-limits.js +20 -0
  233. package/dist/workflows/runtime/agent-identity.js +59 -14
  234. package/dist/workflows/runtime/plan-classifier.js +187 -0
  235. package/dist/workflows/runtime/runs.js +246 -69
  236. package/dist/workflows/runtime/unit-checkin.js +45 -0
  237. package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
  238. package/dist/workflows/validate-summary.js +24 -3
  239. package/dist/workflows/validator.js +26 -1
  240. package/docs/data-and-telemetry.md +4 -3
  241. package/docs/migration/release-notes/0.6.0.md +1 -1
  242. package/docs/migration/release-notes/0.7.0.md +5 -4
  243. package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
  244. package/docs/migration/v0.8-to-v0.9.md +401 -0
  245. package/package.json +4 -2
  246. package/schemas/akm-config.json +16638 -0
  247. package/schemas/akm-task.json +87 -0
  248. package/schemas/akm-workflow.json +372 -0
  249. package/dist/commands/improve/improve-profiles.js +0 -168
  250. package/dist/core/config/config-migration.js +0 -602
  251. package/dist/core/deep-merge.js +0 -38
  252. package/dist/llm/call-ai.js +0 -62
  253. package/dist/setup/legacy-config.js +0 -106
  254. package/docs/README.md +0 -104
  255. /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
  256. /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
  257. /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
  258. /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
  259. /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
  260. /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
  261. /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
  262. /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
@@ -3,10 +3,11 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import { makeAssetRef } from "../../core/asset/asset-ref.js";
6
+ import { acquireMaintenanceActivitySync } from "../../core/maintenance-barrier.js";
6
7
  import { getStateDbPath } from "../../core/state-db.js";
7
8
  import { openDatabase } from "../../storage/database.js";
8
9
  import { getUtilityScoresByIds } from "../db/db.js";
9
- import { applyContributorAblation, applyScoreContributors, applyUtilityContributors, defaultRankingContributors, defaultUtilityRankingContributors, } from "./ranking-contributors.js";
10
+ import { applyBeliefStateScoreCeiling, applyContributorAblation, applyScoreContributors, applyUtilityContributors, defaultRankingContributors, defaultUtilityRankingContributors, } from "./ranking-contributors.js";
10
11
  /**
11
12
  * R2 — best-effort load of `asset_salience.rank_score` from state.db for the
12
13
  * ranked items. Fail-open: any error (state.db locked by a concurrent improve
@@ -27,35 +28,41 @@ export function loadSalienceRankScores(items) {
27
28
  const dbPath = getStateDbPath();
28
29
  if (!fs.existsSync(dbPath))
29
30
  return result; // improve loop has never run here
31
+ const releaseActivity = acquireMaintenanceActivitySync("state-db");
30
32
  const idByRef = new Map();
31
- for (const item of items) {
32
- idByRef.set(makeAssetRef(item.entry.type, item.entry.name), item.id);
33
- }
34
- const stateDb = openDatabase(dbPath, { readonly: true });
35
33
  try {
36
- try {
37
- stateDb.exec("PRAGMA busy_timeout = 250");
34
+ for (const item of items) {
35
+ idByRef.set(makeAssetRef(item.entry.type, item.entry.name), item.id);
38
36
  }
39
- catch {
40
- // pragma failure on a readonly handle is fine — default timeout applies
41
- }
42
- const refs = [...idByRef.keys()];
43
- const CHUNK = 500;
44
- for (let i = 0; i < refs.length; i += CHUNK) {
45
- const chunk = refs.slice(i, i + CHUNK);
46
- const placeholders = chunk.map(() => "?").join(",");
47
- const rows = stateDb
48
- .prepare(`SELECT asset_ref, rank_score FROM asset_salience WHERE asset_ref IN (${placeholders})`)
49
- .all(...chunk);
50
- for (const row of rows) {
51
- const id = idByRef.get(row.asset_ref);
52
- if (id !== undefined)
53
- result.set(id, row.rank_score);
37
+ const stateDb = openDatabase(dbPath, { readonly: true });
38
+ try {
39
+ try {
40
+ stateDb.exec("PRAGMA busy_timeout = 250");
54
41
  }
42
+ catch {
43
+ // pragma failure on a readonly handle is fine — default timeout applies
44
+ }
45
+ const refs = [...idByRef.keys()];
46
+ const CHUNK = 500;
47
+ for (let i = 0; i < refs.length; i += CHUNK) {
48
+ const chunk = refs.slice(i, i + CHUNK);
49
+ const placeholders = chunk.map(() => "?").join(",");
50
+ const rows = stateDb
51
+ .prepare(`SELECT asset_ref, rank_score FROM asset_salience WHERE asset_ref IN (${placeholders})`)
52
+ .all(...chunk);
53
+ for (const row of rows) {
54
+ const id = idByRef.get(row.asset_ref);
55
+ if (id !== undefined)
56
+ result.set(id, row.rank_score);
57
+ }
58
+ }
59
+ }
60
+ finally {
61
+ stateDb.close();
55
62
  }
56
63
  }
57
64
  finally {
58
- stateDb.close();
65
+ releaseActivity();
59
66
  }
60
67
  }
61
68
  catch {
@@ -152,6 +159,12 @@ export function applyRankingRules(options) {
152
159
  };
153
160
  for (const item of options.items) {
154
161
  applyUtilityContributors(item, utilityContext, activeUtilityContributors);
162
+ // SPEC-5: demoting belief states (superseded/contradicted/archived/
163
+ // deprecated) cap the FINAL score. The additive belief penalty inside the
164
+ // multiplicative boost sum cannot overcome the FTS min-max normalization
165
+ // spread, so without the ceiling a superseded incumbent that is the best
166
+ // keyword match outranks its own correction forever.
167
+ applyBeliefStateScoreCeiling(item);
155
168
  }
156
169
  return options.items;
157
170
  }
@@ -9,7 +9,8 @@
9
9
  * - description: entry description
10
10
  * - tags: tags + aliases joined
11
11
  * - hints: searchHints + examples + usage + intent fields
12
- * - content: TOC headings (lowest-weight catch-all)
12
+ * - content: TOC headings + parameters + the config-gated body opening
13
+ * (lowest-weight catch-all)
13
14
  */
14
15
  // NOTE (R5): the collapse detector's frozen canary queries are built from the
15
16
  // same surface this function indexes (name tokens / tags / description) and
@@ -59,6 +60,15 @@ export function buildSearchFields(entry) {
59
60
  contentParts.push(param.description);
60
61
  }
61
62
  }
63
+ // Stash-organization conventions (SPEC-8): the self-situating body opening
64
+ // (captured by the metadata pass only when `index.indexBodyOpening` is on)
65
+ // folds into the lowest-weight catch-all column — never name/description/
66
+ // tags/hints — so orientation prose is retrievable without outranking
67
+ // structured-field matches. The fold is unconditional on the entry field:
68
+ // `rebuildFts` rebuilds FTS rows from stored entry_json and must reproduce
69
+ // the same fields without re-reading config.
70
+ if (entry.bodyOpening)
71
+ contentParts.push(entry.bodyOpening);
62
72
  const content = contentParts.join(" ").toLowerCase();
63
73
  return { name, description, tags, hints, content };
64
74
  }
@@ -11,6 +11,8 @@
11
11
  import { defaultRendererRegistry } from "../../core/asset/asset-registry.js";
12
12
  import { SCRIPT_EXTENSIONS } from "../../core/asset/asset-spec.js";
13
13
  import { looksLikeWorkflow } from "../../workflows/parser.js";
14
+ import { looksLikeWorkflowProgram } from "../../workflows/program/parser.js";
15
+ import { WORKFLOW_PROGRAM_RENDERER_NAME } from "../../workflows/program/project.js";
14
16
  import { registerMatcher } from "./file-context.js";
15
17
  // ---------------------------------------------------------------------------
16
18
  // Private data
@@ -178,6 +180,35 @@ function classifyBySmartMd(ctx) {
178
180
  }
179
181
  return { type: "knowledge", specificity: 5 };
180
182
  }
183
+ /** YAML workflow *programs* (redesign addendum, R1): `.yaml`/`.yml` files. */
184
+ const WORKFLOW_PROGRAM_EXTENSIONS = new Set([".yaml", ".yml"]);
185
+ /**
186
+ * Classify YAML workflow programs. Two claims, mirroring the markdown rules:
187
+ *
188
+ * - any `.yaml`/`.yml` under a `workflows/` dir (the directory rule —
189
+ * specificity 15 when `workflows` is the immediate parent, 10 for a
190
+ * deeper ancestor, same ladder as `matchDirectoryHint`);
191
+ * - anywhere else, a content probe via `looksLikeWorkflowProgram`
192
+ * (`version: 1` + `steps:` at column 0) at specificity 19, the same
193
+ * level as `classifyBySmartMd`'s `looksLikeWorkflow` claim.
194
+ *
195
+ * The fact does NOT go through `toMatchResult` — the workflow TYPE maps to
196
+ * the markdown renderer by default, so this classifier names the
197
+ * `workflow-program-yaml` renderer on its result directly.
198
+ */
199
+ function classifyByWorkflowProgram(ctx) {
200
+ if (!WORKFLOW_PROGRAM_EXTENSIONS.has(ctx.ext))
201
+ return null;
202
+ if (isTypedDirDocFile(ctx.fileName))
203
+ return null;
204
+ if (ctx.parentDir === "workflows")
205
+ return { type: "workflow", specificity: 15 };
206
+ if (ctx.ancestorDirs.includes("workflows"))
207
+ return { type: "workflow", specificity: 10 };
208
+ if (looksLikeWorkflowProgram(ctx.content()))
209
+ return { type: "workflow", specificity: 19 };
210
+ return null;
211
+ }
181
212
  function classifyByWiki(ctx) {
182
213
  if (ctx.ext !== ".md")
183
214
  return null;
@@ -223,12 +254,20 @@ export function smartMdMatcher(ctx) {
223
254
  export function wikiMatcher(ctx) {
224
255
  return toMatchResult(ctx, classifyByWiki);
225
256
  }
257
+ export function workflowProgramMatcher(ctx) {
258
+ const fact = classifyByWorkflowProgram(ctx);
259
+ if (!fact)
260
+ return null;
261
+ // Named directly (not via rendererNameFor) — see classifyByWorkflowProgram.
262
+ return { type: fact.type, specificity: fact.specificity, renderer: WORKFLOW_PROGRAM_RENDERER_NAME };
263
+ }
226
264
  const builtinMatchers = [
227
265
  extensionMatcher,
228
266
  directoryMatcher,
229
267
  parentDirHintMatcher,
230
268
  smartMdMatcher,
231
269
  wikiMatcher,
270
+ workflowProgramMatcher,
232
271
  ];
233
272
  export function registerBuiltinMatchers() {
234
273
  for (const matcher of builtinMatchers) {
@@ -14,6 +14,13 @@
14
14
  * their previous home in `builders.ts`.
15
15
  */
16
16
  import { UsageError } from "../../core/errors.js";
17
+ import { resolveModel } from "./model-aliases.js";
18
+ /** Resolve a raw dispatch model once, while preserving frozen/lowered models verbatim. */
19
+ export function resolveDispatchModel(request, profile, platform) {
20
+ if (!request.model || request.modelIsExact)
21
+ return request.model;
22
+ return resolveModel(request.model, platform, profile.modelAliases, profile.globalModelAliases);
23
+ }
17
24
  /**
18
25
  * Guard against values that start with `--`, which would be mis-interpreted as
19
26
  * CLI flags by the spawned process when used as flag values (model, systemPrompt).
@@ -14,50 +14,17 @@
14
14
  */
15
15
  import { ConfigError } from "../../core/errors.js";
16
16
  import { HARNESS_REGISTRY } from "../harnesses/index.js";
17
- // Types + shared validation helpers live in the leaf module `builder-shared.ts`
18
- // so per-harness builders (harnesses/claude/agent-builder.ts) can depend on them
19
- // without importing this file back — avoiding an init-order cycle through
20
- // BUILTIN_BUILDERS (#563). Re-exported here so existing `agent/builders` import
21
- // sites keep working.
22
- import { assertNotFlag } from "./builder-shared.js";
23
- import { resolveModel } from "./model-aliases.js";
24
- import { getBuiltinAgentProfile } from "./profiles.js";
25
17
  export { assertNotFlag, normalizeTools } from "./builder-shared.js";
26
18
  // ── Platform builders ─────────────────────────────────────────────────────────
27
19
  // The OpenCode builder was migrated to its harness directory in #564
28
20
  // (`harnesses/opencode/agent-builder.ts`) and the Claude Code builder in #563
29
21
  // (`harnesses/claude/agent-builder.ts`). Both are imported back into
30
22
  // BUILTIN_BUILDERS below so platform routing is unchanged.
31
- /**
32
- * Default builder — used for custom profiles and any platform without a
33
- * dedicated builder. Passes systemPrompt and model via the same flags as
34
- * the builtin builders so custom profiles benefit from agent asset metadata.
35
- * Tools are omitted — no standard cross-platform flag exists.
36
- */
37
- const defaultBuilder = {
38
- platform: "default",
39
- build(profile, req) {
40
- assertNotFlag(req.systemPrompt, "systemPrompt");
41
- assertNotFlag(req.model, "model");
42
- const args = [...profile.args];
43
- if (req.systemPrompt) {
44
- args.push("--system-prompt", req.systemPrompt);
45
- }
46
- if (req.model) {
47
- const resolved = resolveModel(req.model, profile.name, profile.modelAliases, profile.globalModelAliases);
48
- args.push("--model", resolved);
49
- }
50
- args.push("--");
51
- args.push(req.prompt);
52
- return { argv: [profile.bin, ...args] };
53
- },
54
- };
55
23
  // ── Registry ──────────────────────────────────────────────────────────────────
56
24
  /**
57
25
  * DERIVED from `HARNESS_REGISTRY` (P0.5 registry-drift fix): each harness that
58
- * owns an `agentBuilder` is registered under its canonical id, its
59
- * `<id>-headless` profile variant, and every alias. Previously this was a
60
- * hand-maintained map that could (and did) drift from the harness registry.
26
+ * owns an `agentBuilder` is registered under its canonical id. Named engines
27
+ * are schema-validated to canonical harness ids before reaching this boundary.
61
28
  */
62
29
  const BUILTIN_BUILDERS = (() => {
63
30
  const registry = {};
@@ -65,30 +32,18 @@ const BUILTIN_BUILDERS = (() => {
65
32
  if (!harness.agentBuilder)
66
33
  continue;
67
34
  registry[harness.id] = harness.agentBuilder;
68
- registry[`${harness.id}-headless`] = harness.agentBuilder;
69
- for (const alias of harness.aliases)
70
- registry[alias] = harness.agentBuilder;
71
35
  }
72
36
  return Object.freeze(registry);
73
37
  })();
74
38
  /**
75
39
  * Return the builder for the given platform name.
76
40
  *
77
- * A *custom* profile (unknown platform) falls back to the default builder
78
- * that generic `--system-prompt`/`--model`/`--` shape is the documented
79
- * contract for user-defined wrappers. A *known built-in agent CLI* with no
80
- * dedicated builder (codex, gemini, aider + their `-headless` variants) is a
81
- * loud `ConfigError` instead: the default flag shape is wrong for those CLIs
82
- * (aider, for one, treats positionals as file names), so the old silent
83
- * fallback produced a broken command that "ran" and failed downstream.
84
- * Custom builders injected via tests can be passed as `registry`.
41
+ * Unknown platforms are rejected: there is no safe generic CLI flag shape.
42
+ * Custom builders injected by tests can be passed as `registry`.
85
43
  */
86
44
  export function getCommandBuilder(platform, registry = BUILTIN_BUILDERS) {
87
45
  const found = registry[platform];
88
46
  if (found)
89
47
  return found;
90
- if (getBuiltinAgentProfile(platform)) {
91
- throw new ConfigError(`agent dispatch for "${platform}" is not supported yet: no command builder exists for this CLI, and the generic flag shape would produce a broken command.`, "INVALID_CONFIG_FILE", 'Use an "opencode" or "claude" profile, or — if your CLI is flag-compatible with one of those — set `profiles.agent.<name>.commandBuilder` to "opencode" or "claude".');
92
- }
93
- return defaultBuilder;
48
+ throw new ConfigError(`Agent platform "${platform}" has no registered command builder.`, "INVALID_CONFIG_FILE", "Select a canonical agent platform in engines.<name>.platform.");
94
49
  }
@@ -1,147 +1,7 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { ConfigError } from "../../core/errors.js";
5
- import { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles.js";
6
- /**
7
- * Default hard timeout for an agent CLI (60s — matches the value used in
8
- * `docs/configuration.md`).
9
- */
4
+ /** Default hard timeout for an agent CLI when neither engine nor call overrides it. */
10
5
  export const DEFAULT_AGENT_TIMEOUT_MS = 60_000;
11
- /**
12
- * Resolve the effective `AgentProfile` for `name` by merging the optional
13
- * user override (`profiles.agent[name]`) on top of the built-in profile (if
14
- * any). Returns `undefined` when neither yields a usable profile.
15
- */
16
- export function resolveAgentProfile(name, overrides, globalModelAliases) {
17
- const builtin = getBuiltinAgentProfile(name);
18
- const platform = overrides?.platform;
19
- // For opencode-sdk profiles, allow synthesizing without a built-in.
20
- const sdkMode = platform === "opencode-sdk";
21
- if (!builtin && !overrides?.bin && !sdkMode)
22
- return undefined;
23
- const base = builtin ??
24
- {
25
- name,
26
- bin: overrides?.bin ?? name,
27
- args: [],
28
- stdio: "captured",
29
- envPassthrough: [],
30
- parseOutput: "text",
31
- ...(sdkMode ? { sdkMode: true } : {}),
32
- };
33
- if (!overrides) {
34
- return globalModelAliases ? { ...base, globalModelAliases } : base;
35
- }
36
- // Per-profile aliases from config extend (and shadow) the built-in
37
- // profile's own entries. (Previously always used base.modelAliases, so the
38
- // documented `profiles.agent.<name>.modelAliases` config was silently
39
- // dropped — same bug class as the timeoutMs override below.)
40
- const modelAliases = overrides.modelAliases || base.modelAliases ? { ...base.modelAliases, ...overrides.modelAliases } : undefined;
41
- return {
42
- name,
43
- bin: overrides.bin ?? base.bin,
44
- args: overrides.args ?? base.args,
45
- stdio: base.stdio,
46
- env: base.env,
47
- envPassthrough: base.envPassthrough,
48
- // Honor a user-configured `profiles.agent.<name>.timeoutMs` override; fall
49
- // back to the built-in profile's value. (Previously always used base, so
50
- // the documented config override was silently ignored — callers had to pass
51
- // a CLI flag like `--timeout-ms`.) runAgent (spawn.ts) reads profile.timeoutMs
52
- // when no per-call timeout is supplied, so this makes the config knob work.
53
- timeoutMs: overrides.timeoutMs ?? base.timeoutMs,
54
- parseOutput: base.parseOutput,
55
- ...(sdkMode ? { sdkMode: true } : {}),
56
- model: overrides.model ?? base.model,
57
- endpoint: base.endpoint,
58
- apiKey: base.apiKey,
59
- commandBuilder: base.commandBuilder,
60
- modelAliases,
61
- ...(globalModelAliases ? { globalModelAliases } : {}),
62
- };
63
- }
64
- /**
65
- * Resolve the runnable profile for `name`, or `undefined` if none is
66
- * available (no built-in and no user override). Threads the config-root
67
- * `modelAliases` tier table onto the resolved profile.
68
- */
69
- export function resolveProfileFromConfig(name, config) {
70
- return resolveAgentProfile(name, config?.profiles?.agent?.[name], config?.modelAliases);
71
- }
72
- /**
73
- * Return the names of every agent profile available in `config` — built-ins
74
- * plus any user-defined entries under `profiles.agent`. Sorted, deduplicated.
75
- */
76
- export function listAgentProfileNames(config) {
77
- const seen = new Set(BUILTIN_AGENT_PROFILE_NAMES);
78
- for (const name of Object.keys(config?.profiles?.agent ?? {}))
79
- seen.add(name);
80
- return [...seen].sort();
81
- }
82
- /**
83
- * Resolve the default agent profile name. Order: explicit `requested` arg →
84
- * `config.defaults.agent` → undefined.
85
- */
86
- export function resolveDefaultProfileName(config, requested) {
87
- if (requested?.trim())
88
- return requested.trim();
89
- const def = config?.defaults?.agent;
90
- if (typeof def === "string" && def.trim())
91
- return def.trim();
92
- return undefined;
93
- }
94
- /**
95
- * Throw a stable `ConfigError` when the caller needs an agent profile but
96
- * none can be resolved.
97
- */
98
- export function requireAgentProfile(config, requested) {
99
- if (!config) {
100
- throw new ConfigError("agent commands are disabled: no agent configuration in config.json.", "INVALID_CONFIG_FILE", "Run `akm setup` to detect and configure an agent CLI, or add an entry under `profiles.agent` and set `defaults.agent`.");
101
- }
102
- const name = resolveDefaultProfileName(config, requested);
103
- if (!name) {
104
- throw new ConfigError("agent commands require a profile: pass --profile or set `defaults.agent` in config.json.", "INVALID_CONFIG_FILE", `Available profiles: ${listAgentProfileNames(config).join(", ")}.`);
105
- }
106
- const profile = resolveProfileFromConfig(name, config);
107
- if (!profile) {
108
- throw new ConfigError(`agent profile "${name}" is not built-in and has no \`bin\` override.`, "INVALID_CONFIG_FILE", `Define profiles.agent."${name}".bin in config.json, or pick one of: ${listAgentProfileNames(config).join(", ")}.`);
109
- }
110
- return profile;
111
- }
112
- /**
113
- * Resolve the agent profile bound to a named improve process. Reads from
114
- * `profiles.improve.default.processes.<processName>` for the profile binding,
115
- * then falls back to `defaults.agent`.
116
- */
117
- export function resolveProcessAgentProfile(processName, config) {
118
- const processEntry = config?.profiles?.improve?.default?.processes;
119
- const entry = processEntry?.[processName];
120
- const profileName = entry?.profile;
121
- const profile = requireAgentProfile(config, profileName);
122
- let resolvedTimeoutMs;
123
- if (entry?.timeoutMs === null) {
124
- resolvedTimeoutMs = null;
125
- }
126
- else if (typeof entry?.timeoutMs === "number") {
127
- resolvedTimeoutMs = entry.timeoutMs;
128
- }
129
- else if (profile.timeoutMs !== undefined) {
130
- resolvedTimeoutMs = profile.timeoutMs;
131
- }
132
- return { profile, timeoutMs: resolvedTimeoutMs };
133
- }
134
- /**
135
- * Convenience: list every fully-resolved profile (built-ins merged with
136
- * user overrides). Used by setup detection to enumerate candidates.
137
- */
138
- export function listResolvedAgentProfiles(config) {
139
- const resolved = [];
140
- const builtins = listBuiltinAgentProfiles();
141
- for (const name of listAgentProfileNames(config)) {
142
- const profile = resolveProfileFromConfig(name, config) ?? builtins[name];
143
- if (profile)
144
- resolved.push(profile);
145
- }
146
- return resolved;
147
- }
6
+ /** Default hard timeout for direct LLM calls when no engine/use override exists. */
7
+ export const DEFAULT_LLM_TIMEOUT_MS = 600_000;
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import fs from "node:fs";
17
17
  import path from "node:path";
18
- import { listResolvedAgentProfiles } from "./config.js";
18
+ import { getBuiltinAgentProfile, listBuiltinAgentProfiles } from "./profiles.js";
19
19
  /**
20
20
  * Default PATH lookup. Walks `process.env.PATH` and returns the first
21
21
  * existing executable file. Returns `undefined` when the bin is not on
@@ -71,7 +71,22 @@ export function _setAgentDetectForTests(fakes) {
71
71
  export function detectAgentCliProfiles(agent, whichFn = defaultWhich) {
72
72
  if (detectOverrides?.detectAgentCliProfiles)
73
73
  return detectOverrides.detectAgentCliProfiles(agent, whichFn);
74
- const profiles = listResolvedAgentProfiles(agent);
74
+ const profilesByName = new Map(Object.values(listBuiltinAgentProfiles()).map((profile) => [profile.name, profile]));
75
+ for (const [name, engine] of Object.entries(agent?.engines ?? {})) {
76
+ if (engine.kind !== "agent")
77
+ continue;
78
+ const builtin = getBuiltinAgentProfile(engine.platform);
79
+ profilesByName.set(name, {
80
+ name,
81
+ platform: engine.platform,
82
+ bin: engine.bin ?? builtin?.bin ?? (engine.platform === "opencode-sdk" ? "opencode" : engine.platform),
83
+ args: engine.args ?? builtin?.args ?? [],
84
+ stdio: "captured",
85
+ envPassthrough: builtin?.envPassthrough ?? [],
86
+ parseOutput: "text",
87
+ });
88
+ }
89
+ const profiles = [...profilesByName.values()];
75
90
  return profiles.map((profile) => probeProfile(profile, whichFn));
76
91
  }
77
92
  function probeProfile(profile, whichFn) {
@@ -0,0 +1,202 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import path from "node:path";
5
+ import { deepMergeConfig } from "../../core/config/deep-merge.js";
6
+ import { ConfigError } from "../../core/errors.js";
7
+ import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
8
+ import { collectSensitiveValues } from "../../core/redaction.js";
9
+ import { getHarness } from "../harnesses/index.js";
10
+ import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
11
+ import { resolveLlmModel, resolveModel } from "./model-aliases.js";
12
+ import { getBuiltinAgentProfile } from "./profiles.js";
13
+ function hasOwn(value, key) {
14
+ return Object.hasOwn(value, key);
15
+ }
16
+ function envName(reference) {
17
+ const match = /^\$(?:\{)?([A-Za-z_][A-Za-z0-9_]*)(?:\})?$/.exec(reference);
18
+ return match?.[1];
19
+ }
20
+ function selectedEngineName(config, layers, llmOnly) {
21
+ for (let index = layers.length - 1; index >= 0; index--) {
22
+ if (layers[index]?.engine !== undefined)
23
+ return layers[index]?.engine;
24
+ }
25
+ return llmOnly ? config.defaults?.llmEngine : config.defaults?.engine;
26
+ }
27
+ function resolveEngineConfig(name, config) {
28
+ const engine = config.engines?.[name];
29
+ if (!engine) {
30
+ throw new ConfigError(`Engine "${name}" is not configured.`, "INVALID_CONFIG_FILE");
31
+ }
32
+ return engine;
33
+ }
34
+ function resolveCredential(name, engine, config) {
35
+ if (engine.apiKey !== undefined) {
36
+ const explicit = envName(engine.apiKey);
37
+ if (!explicit)
38
+ throw new ConfigError(`Engine "${name}" has an invalid symbolic apiKey reference.`, "INVALID_CONFIG_FILE");
39
+ return { names: [explicit], required: true };
40
+ }
41
+ const specific = `AKM_ENGINE_${name.toUpperCase().replaceAll("-", "_")}_API_KEY`;
42
+ return config.defaults?.llmEngine === name
43
+ ? { names: [specific, "AKM_LLM_API_KEY"], required: false }
44
+ : { names: [specific], required: false };
45
+ }
46
+ /** Collect materialized engine credentials for output and persistence redaction. */
47
+ export function collectEngineCredentialValues(config, envSource = process.env) {
48
+ const values = new Set();
49
+ for (const [name, engine] of Object.entries(config.engines ?? {})) {
50
+ if (engine.kind !== "llm")
51
+ continue;
52
+ for (const envVar of resolveCredential(name, engine, config)?.names ?? []) {
53
+ const value = envSource[envVar]?.trim();
54
+ if (value)
55
+ values.add(value);
56
+ }
57
+ }
58
+ return collectSensitiveValues(values);
59
+ }
60
+ function effectiveTimeout(engine, layers, fallback) {
61
+ for (let index = layers.length - 1; index >= 0; index--) {
62
+ if (hasOwn(layers[index] ?? {}, "timeoutMs"))
63
+ return layers[index]?.timeoutMs ?? null;
64
+ }
65
+ return hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : fallback;
66
+ }
67
+ export function resolveLlmEngineUse(config, layers, options = {}) {
68
+ const name = selectedEngineName(config, layers, true);
69
+ if (!name) {
70
+ if (options.optional)
71
+ return undefined;
72
+ throw new ConfigError("No LLM engine is selected. Set defaults.llmEngine or specify engine.", "LLM_NOT_CONFIGURED");
73
+ }
74
+ const engine = resolveEngineConfig(name, config);
75
+ if (engine.kind !== "llm") {
76
+ throw new ConfigError(`Engine "${name}" is not an LLM engine.`, "INVALID_CONFIG_FILE");
77
+ }
78
+ let connection = {
79
+ provider: engine.provider,
80
+ endpoint: engine.endpoint,
81
+ model: engine.model,
82
+ temperature: engine.temperature,
83
+ maxTokens: engine.maxTokens,
84
+ supportsJsonSchema: engine.supportsJsonSchema,
85
+ extraParams: engine.extraParams,
86
+ contextLength: engine.contextLength,
87
+ enableThinking: engine.enableThinking,
88
+ };
89
+ for (const layer of layers) {
90
+ if (layer.llm)
91
+ connection = deepMergeConfig(connection, layer.llm);
92
+ if (layer.model !== undefined)
93
+ connection.model = layer.model;
94
+ }
95
+ for (const key of Object.keys(connection)) {
96
+ if (connection[key] === undefined)
97
+ delete connection[key];
98
+ }
99
+ connection.model = resolveLlmModel(connection.model, name, config.modelAliases);
100
+ return {
101
+ engine: name,
102
+ connection: connection,
103
+ credential: resolveCredential(name, engine, config),
104
+ timeoutMs: effectiveTimeout(engine, layers, DEFAULT_LLM_TIMEOUT_MS),
105
+ };
106
+ }
107
+ /** Read a resolved symbolic credential only at the runtime dispatch boundary. */
108
+ export function materializeLlmConnection(resolved) {
109
+ if (resolved.connection.extraParams !== undefined) {
110
+ const issue = validateExtraParams(resolved.connection.extraParams)[0];
111
+ if (issue) {
112
+ throw new ConfigError(formatExtraParamsIssue(`Engine "${resolved.engine}" extraParams`, issue), "INVALID_CONFIG_FILE");
113
+ }
114
+ }
115
+ let apiKey;
116
+ for (const name of resolved.credential?.names ?? []) {
117
+ const candidate = process.env[name]?.trim();
118
+ if (candidate) {
119
+ apiKey = candidate;
120
+ break;
121
+ }
122
+ }
123
+ if (resolved.credential?.required && !apiKey) {
124
+ throw new ConfigError(`Required engine credential ${resolved.credential.names[0]} is not set.`, "INVALID_CONFIG_FILE");
125
+ }
126
+ return {
127
+ ...resolved.connection,
128
+ ...(apiKey ? { apiKey } : {}),
129
+ timeoutMs: resolved.timeoutMs,
130
+ };
131
+ }
132
+ function lowerAgentEngine(name, engine, config) {
133
+ const harness = getHarness(engine.platform);
134
+ if (!harness?.capabilities.agentDispatch) {
135
+ throw new ConfigError(`Engine "${name}" names a platform that cannot dispatch agents: ${engine.platform}.`, "INVALID_CONFIG_FILE");
136
+ }
137
+ const platform = harness.id;
138
+ const sdk = platform === "opencode-sdk";
139
+ const builtin = getBuiltinAgentProfile(platform);
140
+ const profile = {
141
+ name,
142
+ platform,
143
+ bin: engine.bin ?? builtin?.bin ?? (sdk ? "opencode" : platform),
144
+ args: engine.args ?? builtin?.args ?? [],
145
+ stdio: "captured",
146
+ ...(builtin?.env ? { env: builtin.env } : {}),
147
+ envPassthrough: builtin?.envPassthrough ?? [],
148
+ parseOutput: "text",
149
+ ...(engine.workspace ? { workspace: path.resolve(engine.workspace) } : {}),
150
+ ...(engine.model
151
+ ? {
152
+ model: resolveModel(engine.model, platform, engine.modelAliases, config.modelAliases),
153
+ modelIsExact: true,
154
+ }
155
+ : {}),
156
+ ...(engine.modelAliases ? { modelAliases: engine.modelAliases } : {}),
157
+ ...(config.modelAliases ? { globalModelAliases: config.modelAliases } : {}),
158
+ };
159
+ if (!sdk) {
160
+ return {
161
+ kind: "agent",
162
+ engine: name,
163
+ profile,
164
+ timeoutMs: hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : DEFAULT_AGENT_TIMEOUT_MS,
165
+ };
166
+ }
167
+ const fallback = resolveLlmEngineUse(config, [{ engine: engine.llmEngine ?? config.defaults?.llmEngine }]);
168
+ if (!fallback)
169
+ throw new ConfigError(`SDK engine "${name}" has no fallback LLM engine.`, "LLM_NOT_CONFIGURED");
170
+ return {
171
+ kind: "sdk",
172
+ engine: name,
173
+ profile,
174
+ fallbackConnection: fallback.connection,
175
+ ...(fallback.credential ? { fallbackCredential: fallback.credential } : {}),
176
+ fallbackTimeoutMs: fallback.timeoutMs,
177
+ timeoutMs: hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : fallback.timeoutMs,
178
+ };
179
+ }
180
+ /** Lower a named engine through the canonical harness platform. */
181
+ export function resolveEngine(name, config) {
182
+ const engine = resolveEngineConfig(name, config);
183
+ if (engine.kind === "llm") {
184
+ const resolved = resolveLlmEngineUse(config, [{ engine: name }]);
185
+ if (!resolved)
186
+ throw new ConfigError(`LLM engine "${name}" could not be resolved.`, "LLM_NOT_CONFIGURED");
187
+ return {
188
+ kind: "llm",
189
+ engine: name,
190
+ connection: resolved.connection,
191
+ ...(resolved.credential ? { credential: resolved.credential } : {}),
192
+ timeoutMs: resolved.timeoutMs,
193
+ };
194
+ }
195
+ return lowerAgentEngine(name, engine, config);
196
+ }
197
+ export function resolveDefaultEngine(config) {
198
+ const name = config.defaults?.engine;
199
+ if (!name)
200
+ throw new ConfigError("No default engine is configured.", "INVALID_CONFIG_FILE");
201
+ return resolveEngine(name, config);
202
+ }