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
@@ -34,7 +34,11 @@
34
34
  * enforced at save time via `superRefine` on the top-level schema.
35
35
  */
36
36
  import { z } from "zod";
37
- import { VALID_HARNESS_IDS } from "./config-types.js";
37
+ import { validateExtraParams } from "../extra-params.js";
38
+ import { HARNESS_BY_ID, VALID_HARNESS_IDS } from "./config-types.js";
39
+ import { BUILTIN_IMPROVE_STRATEGY_NAMES, ENGINE_NAME_PATTERN_SOURCE, IMPROVE_PROCESS_ENGINE_CAPABILITIES, } from "./engine-semantics.js";
40
+ /** Persisted config schema version. Package prerelease/patch versions do not change this value. */
41
+ export const CURRENT_CONFIG_VERSION = "0.9.0";
38
42
  // ── Reusable atomic schemas ─────────────────────────────────────────────────
39
43
  /** Positive integer (used for tokens, timeouts, batch sizes). */
40
44
  const positiveInt = z.number().int().positive();
@@ -49,6 +53,57 @@ const nonEmptyString = z
49
53
  const httpUrl = z.string().refine((v) => v.startsWith("http://") || v.startsWith("https://"), {
50
54
  message: "endpoint must start with http:// or https://",
51
55
  });
56
+ const ENGINE_NAME_PATTERN = new RegExp(ENGINE_NAME_PATTERN_SOURCE);
57
+ const ENV_REFERENCE_PATTERN = /^\$[A-Za-z_][A-Za-z0-9_]*$|^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/;
58
+ const engineName = z
59
+ .string()
60
+ .max(63)
61
+ .regex(ENGINE_NAME_PATTERN, "names must be lowercase kebab-case and must not begin with reserved akm-");
62
+ const chatCompletionsEndpoint = z.string().superRefine((value, ctx) => {
63
+ try {
64
+ const url = new URL(value);
65
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
66
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "endpoint must use http:// or https://" });
67
+ }
68
+ if (url.username || url.password || url.search || url.hash || !url.pathname.endsWith("/chat/completions")) {
69
+ ctx.addIssue({
70
+ code: z.ZodIssueCode.custom,
71
+ message: "endpoint must be a credential-free OpenAI chat-completions URL without query or fragment",
72
+ });
73
+ }
74
+ }
75
+ catch {
76
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "endpoint must be a complete URL" });
77
+ }
78
+ });
79
+ const ExtraParamsSchema = z.record(z.unknown()).superRefine((value, ctx) => {
80
+ for (const issue of validateExtraParams(value)) {
81
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: issue.path, message: issue.message });
82
+ }
83
+ });
84
+ function normalizeAliasKeys(raw, ctx) {
85
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
86
+ return raw;
87
+ const normalized = {};
88
+ const originalByKey = new Map();
89
+ for (const [key, value] of Object.entries(raw)) {
90
+ const lower = key.toLowerCase();
91
+ const previous = originalByKey.get(lower);
92
+ if (previous !== undefined) {
93
+ ctx.addIssue({
94
+ code: z.ZodIssueCode.custom,
95
+ path: [key],
96
+ message: `alias collides case-insensitively with ${previous}`,
97
+ });
98
+ continue;
99
+ }
100
+ originalByKey.set(lower, key);
101
+ normalized[lower] = value;
102
+ }
103
+ return normalized;
104
+ }
105
+ const ModelAliasMapSchema = z.preprocess((raw, ctx) => normalizeAliasKeys(raw, ctx), z.record(z.string().min(1), z.string().min(1)));
106
+ const GlobalModelAliasesSchema = z.preprocess((raw, ctx) => normalizeAliasKeys(raw, ctx), z.record(z.string().min(1), z.record(z.string().min(1), z.string().min(1))));
52
107
  // ── Feedback failure modes (F-3 / #384) ─────────────────────────────────────
53
108
  /**
54
109
  * Curated taxonomy of failure modes for negative feedback.
@@ -71,8 +126,8 @@ const LlmCapabilitiesSchema = z
71
126
  })
72
127
  .passthrough();
73
128
  /**
74
- * Connection config used for both top-level `llm` (after migration) and
75
- * `profiles.llm[*]`. `model` is required at schema level — partial entries
129
+ * OpenAI-compatible connection fields shared by named LLM engines and bounded
130
+ * internal call helpers. `model` is required at schema level — partial entries
76
131
  * created by `akm config set llm.endpoint <url>` (where model is left absent)
77
132
  * are normalized to `model: ""` *before* Zod sees them by the load-time
78
133
  * pre-Zod migrator hook, so this strict shape gates CLI writes without
@@ -86,12 +141,11 @@ export const LlmConnectionConfigSchema = z
86
141
  apiKey: z.string().optional(),
87
142
  temperature: z.number().finite().optional(),
88
143
  maxTokens: positiveInt.optional(),
89
- timeoutMs: positiveInt.optional(),
144
+ timeoutMs: z.union([positiveInt, z.null()]).optional(),
90
145
  concurrency: positiveInt.optional(),
91
146
  capabilities: LlmCapabilitiesSchema.optional(),
92
- extraParams: z.record(z.unknown()).optional(),
147
+ extraParams: ExtraParamsSchema.optional(),
93
148
  contextLength: positiveInt.optional(),
94
- judgeModel: z.string().min(1).optional(),
95
149
  enableThinking: z.boolean().optional(),
96
150
  })
97
151
  .passthrough();
@@ -117,7 +171,7 @@ export const EmbeddingConnectionConfigSchema = z
117
171
  provider: z.string().optional(),
118
172
  endpoint: z.string().optional(),
119
173
  model: z.string().optional(),
120
- apiKey: z.string().optional(),
174
+ apiKey: z.string().regex(ENV_REFERENCE_PATTERN, `apiKey must be $VAR or \${VAR}`).optional(),
121
175
  dimension: positiveInt.optional(),
122
176
  localModel: z.string().min(1).optional(),
123
177
  maxTokens: positiveInt.optional(),
@@ -127,34 +181,94 @@ export const EmbeddingConnectionConfigSchema = z
127
181
  ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
128
182
  })
129
183
  .passthrough();
130
- // ── Agent profiles ──────────────────────────────────────────────────────────
184
+ // ── Agent engines ───────────────────────────────────────────────────────────
131
185
  // Derives from the canonical VALID_HARNESS_IDS (#565) so the Zod gate cannot
132
186
  // drift from the TS union / parse check / setup detection.
133
187
  const AgentPlatformSchema = z.enum(VALID_HARNESS_IDS);
134
- export const AgentProfileConfigSchema = z
188
+ export const LlmInvocationOverridesSchema = z
189
+ .object({
190
+ temperature: z.number().finite().optional(),
191
+ maxTokens: positiveInt.optional(),
192
+ supportsJsonSchema: z.boolean().optional(),
193
+ extraParams: ExtraParamsSchema.optional(),
194
+ contextLength: positiveInt.optional(),
195
+ enableThinking: z.boolean().optional(),
196
+ })
197
+ .passthrough();
198
+ const LlmEngineSchema = z
135
199
  .object({
136
- platform: AgentPlatformSchema,
137
- bin: z.string().min(1).optional(),
200
+ kind: z.literal("llm"),
201
+ provider: z.string().optional(),
202
+ endpoint: chatCompletionsEndpoint,
203
+ model: nonEmptyString,
204
+ apiKey: z.string().regex(ENV_REFERENCE_PATTERN, `apiKey must be $VAR or \${VAR}`).optional(),
205
+ temperature: z.number().finite().optional(),
206
+ maxTokens: positiveInt.optional(),
207
+ timeoutMs: z.union([positiveInt, z.null()]).optional(),
208
+ concurrency: positiveInt.optional(),
209
+ supportsJsonSchema: z.boolean().optional(),
210
+ extraParams: ExtraParamsSchema.optional(),
211
+ contextLength: positiveInt.optional(),
212
+ enableThinking: z.boolean().optional(),
213
+ })
214
+ .passthrough()
215
+ .superRefine((value, ctx) => {
216
+ for (const key of ["platform", "bin", "args", "workspace", "modelAliases", "llmEngine"]) {
217
+ if (key in value)
218
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [key], message: `${key} is not valid on an LLM engine` });
219
+ }
220
+ });
221
+ const AgentEngineSchema = z
222
+ .object({
223
+ kind: z.literal("agent"),
224
+ platform: AgentPlatformSchema.refine((platform) => HARNESS_BY_ID.get(platform)?.capabilities.agentDispatch === true, {
225
+ message: "platform does not support agent dispatch",
226
+ }),
227
+ bin: nonEmptyString.optional(),
138
228
  args: z.array(z.string()).optional(),
139
- workspace: z.string().min(1).optional(),
140
- model: z.string().min(1).optional(),
141
- // Per-call timeout (ms) used when a command does not pass an explicit
142
- // timeout (e.g. `akm wiki ingest` without `--timeout-ms`). null = no
143
- // timeout. Honored by resolveAgentProfile (integrations/agent/config.ts).
229
+ workspace: nonEmptyString.optional(),
230
+ model: nonEmptyString.optional(),
144
231
  timeoutMs: z.union([positiveInt, z.null()]).optional(),
145
- // Per-profile model aliases: lowercase alias → exact model string for
146
- // this profile's CLI. Highest-precedence tier in resolveModel
147
- // (integrations/agent/model-aliases.ts) — beats the config-root
148
- // `modelAliases` table and the built-in opus/sonnet/haiku entries.
149
- modelAliases: z.record(z.string().min(1), z.string().min(1)).optional(),
232
+ modelAliases: ModelAliasMapSchema.optional(),
233
+ llmEngine: engineName.optional(),
150
234
  })
151
- .passthrough();
235
+ .passthrough()
236
+ .superRefine((value, ctx) => {
237
+ for (const key of [
238
+ "provider",
239
+ "endpoint",
240
+ "apiKey",
241
+ "temperature",
242
+ "maxTokens",
243
+ "concurrency",
244
+ "supportsJsonSchema",
245
+ "extraParams",
246
+ "contextLength",
247
+ "enableThinking",
248
+ ]) {
249
+ if (key in value)
250
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [key], message: `${key} is not valid on an agent engine` });
251
+ }
252
+ if (value.platform !== "opencode-sdk" && value.llmEngine !== undefined) {
253
+ ctx.addIssue({
254
+ code: z.ZodIssueCode.custom,
255
+ path: ["llmEngine"],
256
+ message: "llmEngine is only valid on opencode-sdk",
257
+ });
258
+ }
259
+ if (value.platform === "opencode-sdk" && value.args !== undefined) {
260
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["args"], message: "args is not valid on opencode-sdk" });
261
+ }
262
+ });
263
+ export const EngineConfigSchema = z.union([LlmEngineSchema, AgentEngineSchema]);
264
+ export const EnginesSchema = z.record(engineName, EngineConfigSchema);
152
265
  // ── Improve profile / process ──────────────────────────────────────────────
153
266
  export const ImproveProcessConfigSchema = z
154
267
  .object({
268
+ engine: engineName.optional(),
269
+ model: nonEmptyString.optional(),
270
+ llm: LlmInvocationOverridesSchema.optional(),
155
271
  enabled: z.boolean().optional(),
156
- mode: z.enum(["llm", "agent", "sdk"]).optional(),
157
- profile: z.string().min(1).optional(),
158
272
  timeoutMs: z.union([positiveInt, z.null()]).optional(),
159
273
  allowedTypes: z.array(z.string().min(1)).optional(),
160
274
  // Consolidate process: minimum eligible-memory pool size below which the
@@ -226,6 +340,10 @@ export const ImproveProcessConfigSchema = z
226
340
  // high-signal-first sweep). Unset = process all eligible (current
227
341
  // behavior). Only meaningful on `graphExtraction`.
228
342
  topN: positiveInt.optional(),
343
+ // Improve-owned graph extraction scope and batching. These are passed to
344
+ // the invocation directly and never inherited from standalone index.graph.
345
+ includeTypes: z.array(z.string().min(1)).min(1).optional(),
346
+ batchSize: positiveInt.optional(),
229
347
  // graphExtraction process: full-corpus scan. When true, graph extraction
230
348
  // runs on ALL stash files instead of only files touched by actionable refs
231
349
  // in the current run. Used by the `graph-refresh` built-in profile / a
@@ -399,14 +517,34 @@ export const ImproveProcessConfigSchema = z
399
517
  rejectEmpty: z.boolean().optional(),
400
518
  judgment: z
401
519
  .object({
402
- mode: z.enum(["llm", "agent", "sdk"]).optional(),
403
- profile: z.string().min(1).optional(),
520
+ engine: engineName.optional(),
521
+ model: nonEmptyString.optional(),
404
522
  timeoutMs: z.union([positiveInt, z.null()]).optional(),
523
+ llm: LlmInvocationOverridesSchema.optional(),
405
524
  })
406
525
  .passthrough()
407
526
  .optional(),
408
527
  })
409
- .passthrough();
528
+ .passthrough()
529
+ .superRefine((value, ctx) => {
530
+ for (const key of ["mode", "profile"]) {
531
+ if (key in value) {
532
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [key], message: `${key} is retired; use engine` });
533
+ }
534
+ }
535
+ const judgment = value.judgment;
536
+ if (judgment) {
537
+ for (const key of ["mode", "profile"]) {
538
+ if (key in judgment) {
539
+ ctx.addIssue({
540
+ code: z.ZodIssueCode.custom,
541
+ path: ["judgment", key],
542
+ message: `${key} is retired; use engine`,
543
+ });
544
+ }
545
+ }
546
+ }
547
+ });
410
548
  const ImproveProfileProcessesSchema = z
411
549
  .object({
412
550
  reflect: ImproveProcessConfigSchema.optional(),
@@ -439,6 +577,10 @@ const ImproveProfileProcessesSchema = z
439
577
  });
440
578
  export const ImproveProfileConfigSchema = z
441
579
  .object({
580
+ engine: engineName.optional(),
581
+ model: nonEmptyString.optional(),
582
+ timeoutMs: z.union([positiveInt, z.null()]).optional(),
583
+ llm: LlmInvocationOverridesSchema.optional(),
442
584
  description: z.string().min(1).optional(),
443
585
  processes: ImproveProfileProcessesSchema.optional(),
444
586
  autoAccept: nonNegativeNumber.optional(),
@@ -463,19 +605,12 @@ export const ImproveProfileConfigSchema = z
463
605
  .optional(),
464
606
  })
465
607
  .passthrough();
466
- // ── Profiles / defaults ────────────────────────────────────────────────────
467
- export const ProfilesSchema = z
468
- .object({
469
- llm: z.record(z.string(), LlmProfileConfigSchema).optional(),
470
- agent: z.record(z.string(), AgentProfileConfigSchema).optional(),
471
- improve: z.record(z.string(), ImproveProfileConfigSchema).optional(),
472
- })
473
- .passthrough();
608
+ // ── Defaults ───────────────────────────────────────────────────────────────
474
609
  export const DefaultsSchema = z
475
610
  .object({
476
- llm: z.string().min(1).optional(),
477
- agent: z.string().min(1).optional(),
478
- improve: z.string().min(1).optional(),
611
+ engine: engineName.optional(),
612
+ llmEngine: engineName.optional(),
613
+ improveStrategy: engineName.optional(),
479
614
  })
480
615
  .passthrough();
481
616
  // ── Sources / registries / installed ────────────────────────────────────────
@@ -503,6 +638,13 @@ export const SourceConfigEntrySchema = z
503
638
  })
504
639
  .passthrough()
505
640
  .superRefine((entry, ctx) => {
641
+ if (!["filesystem", "git", "website", "npm"].includes(entry.type)) {
642
+ ctx.addIssue({
643
+ code: z.ZodIssueCode.custom,
644
+ path: ["type"],
645
+ message: `unsupported source type "${entry.type}"; expected filesystem, git, website, or npm`,
646
+ });
647
+ }
506
648
  if (entry.writable === true && (entry.type === "website" || entry.type === "npm")) {
507
649
  ctx.addIssue({
508
650
  code: z.ZodIssueCode.custom,
@@ -673,6 +815,7 @@ const ImproveCollapseDetectorSchema = z
673
815
  .passthrough();
674
816
  export const ImproveConfigSchema = z
675
817
  .object({
818
+ strategies: z.record(engineName, ImproveProfileConfigSchema).optional(),
676
819
  utilityDecay: ImproveUtilityDecaySchema.optional(),
677
820
  eventRetentionDays: nonNegativeNumber.optional(),
678
821
  calibration: ImproveCalibrationSchema.optional(),
@@ -694,9 +837,8 @@ const GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED = [
694
837
  "wiki",
695
838
  "fact",
696
839
  ];
697
- const INDEX_PASS_PROVIDER_KEYS = new Set([
840
+ const INDEX_PASS_RETIRED_KEYS = new Set([
698
841
  "endpoint",
699
- "model",
700
842
  "provider",
701
843
  "apiKey",
702
844
  "baseUrl",
@@ -705,16 +847,19 @@ const INDEX_PASS_PROVIDER_KEYS = new Set([
705
847
  "capabilities",
706
848
  ]);
707
849
  const INDEX_PASS_KNOWN_KEYS = new Set([
850
+ "engine",
851
+ "model",
852
+ "timeoutMs",
853
+ "enabled",
708
854
  "llm",
709
855
  "graphExtractionBatchSize",
710
856
  "graphExtractionIncludeTypes",
711
- "memoryInferenceBatchSize",
712
857
  "lazyGraphExtraction",
713
858
  ]);
714
859
  /**
715
860
  * Per-pass `index.<pass>` entry. Uses preprocess + manual validation so we can
716
- * emit the legacy parser's targeted error messages ("Duplicate LLM provider
717
- * configuration", "Unknown key `index.<pass>.<key>`", "expected a boolean")
861
+ * emit targeted error messages ("Retired or misplaced engine setting",
862
+ * "Unknown key `index.<pass>.<key>`")
718
863
  * instead of Zod's generic `Unrecognized key` / `Expected boolean, received
719
864
  * string` strings — keeps `akm` startup errors actionable.
720
865
  */
@@ -724,38 +869,34 @@ export const IndexPassConfigSchema = z.preprocess((raw, ctx) => {
724
869
  }
725
870
  const obj = raw;
726
871
  for (const key of Object.keys(obj)) {
727
- if (INDEX_PASS_PROVIDER_KEYS.has(key)) {
872
+ if (INDEX_PASS_RETIRED_KEYS.has(key)) {
728
873
  ctx.addIssue({
729
874
  code: z.ZodIssueCode.custom,
730
- message: `Duplicate LLM provider configuration: \`${[...(ctx.path ?? []), key].join(".")}\` is not allowed. ` +
731
- "Configure provider/model/endpoint under `profiles.llm` only; per-pass entries support `{ llm: false }` opt-out.",
875
+ message: `Retired or misplaced engine setting: \`${[...(ctx.path ?? []), key].join(".")}\` is not allowed. ` +
876
+ "Select a named engine and use typed invocation fields instead.",
732
877
  });
733
878
  return raw;
734
879
  }
735
880
  if (!INDEX_PASS_KNOWN_KEYS.has(key)) {
736
881
  ctx.addIssue({
737
882
  code: z.ZodIssueCode.custom,
738
- message: `Unknown key \`${[...(ctx.path ?? []), key].join(".")}\`. Per-pass entries support \`llm\` ` +
739
- "(boolean opt-out), `graphExtractionBatchSize`, `graphExtractionIncludeTypes`, " +
740
- "`memoryInferenceBatchSize`, and `lazyGraphExtraction`.",
883
+ message: `Unknown key \`${[...(ctx.path ?? []), key].join(".")}\`. Per-pass entries support ` +
884
+ "`engine`, `model`, `timeoutMs`, `enabled`, `llm`, `graphExtractionBatchSize`, " +
885
+ "`graphExtractionIncludeTypes`, and `lazyGraphExtraction`.",
741
886
  });
742
887
  return raw;
743
888
  }
744
889
  }
745
- if ("llm" in obj && typeof obj.llm !== "boolean") {
746
- ctx.addIssue({
747
- code: z.ZodIssueCode.custom,
748
- message: `Invalid \`${[...(ctx.path ?? []), "llm"].join(".")}\`: expected a boolean (true to use the default LLM profile, false to opt out). Got ${typeof obj.llm}.`,
749
- });
750
- return raw;
751
- }
752
890
  return raw;
753
891
  }, z
754
892
  .object({
755
- llm: z.boolean().optional(),
893
+ engine: engineName.optional(),
894
+ model: nonEmptyString.optional(),
895
+ timeoutMs: z.union([positiveInt, z.null()]).optional(),
896
+ enabled: z.boolean().optional(),
897
+ llm: LlmInvocationOverridesSchema.optional(),
756
898
  graphExtractionBatchSize: positiveInt.optional(),
757
899
  graphExtractionIncludeTypes: z.array(z.enum(GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED)).nonempty().optional(),
758
- memoryInferenceBatchSize: positiveInt.optional(),
759
900
  lazyGraphExtraction: z.boolean().optional(),
760
901
  })
761
902
  .passthrough());
@@ -773,35 +914,61 @@ const StalenessDetectionSchema = z
773
914
  thresholdDays: positiveInt.optional(),
774
915
  })
775
916
  .passthrough();
917
+ const IndexDefaultsSchema = z
918
+ .object({
919
+ engine: engineName.optional(),
920
+ model: nonEmptyString.optional(),
921
+ timeoutMs: z.union([positiveInt, z.null()]).optional(),
922
+ llm: LlmInvocationOverridesSchema.optional(),
923
+ })
924
+ .passthrough();
776
925
  /**
777
926
  * Index config is a union of reserved feature sections and per-pass entries.
778
927
  * Passthrough so per-pass entries (keyed by arbitrary pass names like `graph`,
779
928
  * `enrichment`) can live next to the reserved keys.
780
929
  *
930
+ * Reserved scalar key `indexBodyOpening` (stash-conventions SPEC-8, default
931
+ * false): when true, the metadata pass captures the first prose paragraph of
932
+ * each markdown asset body into `entry.bodyOpening`, which folds into the
933
+ * lowest-weight `content` FTS column and the embedding text. It is a boolean,
934
+ * not a per-pass object — the preprocess below exempts it from the
935
+ * object-shape check so it never routes into the per-pass catchall.
936
+ *
781
937
  * The outer preprocess emits the legacy parser's actionable error messages
782
938
  * for the two most common type-shape mistakes:
783
939
  * - An array at the `index` block.
784
940
  * - A non-object at `index.<passName>`.
785
- * Inner field validation (graphExtractionIncludeTypes enum, llm boolean,
786
- * provider-key rejection) is delegated to {@link IndexPassConfigSchema}.
941
+ * Inner field validation (graphExtractionIncludeTypes enum, invocation
942
+ * overrides, provider-key rejection) is delegated to {@link IndexPassConfigSchema}.
787
943
  */
788
- export const IndexConfigSchema = z.preprocess((raw, ctx) => {
944
+ const IndexConfigRuntimeSchema = z.preprocess((raw, ctx) => {
789
945
  if (raw === undefined || raw === null)
790
946
  return raw;
791
947
  if (Array.isArray(raw)) {
792
948
  ctx.addIssue({
793
949
  code: z.ZodIssueCode.custom,
794
- message: 'Invalid `index` config: expected an object keyed by pass name (e.g. `{ "enrichment": { "llm": false } }`).',
950
+ message: 'Invalid `index` config: expected an object keyed by pass name (e.g. `{ "enrichment": { "enabled": false } }`).',
795
951
  });
796
952
  return raw;
797
953
  }
798
954
  if (typeof raw !== "object")
799
955
  return raw;
800
956
  for (const [passName, value] of Object.entries(raw)) {
957
+ if (passName === "indexBodyOpening") {
958
+ if (typeof value !== "boolean") {
959
+ ctx.addIssue({
960
+ code: z.ZodIssueCode.custom,
961
+ message: "Invalid `index.indexBodyOpening`: expected a boolean (true to index the first body paragraph " +
962
+ `of markdown assets into search). Got ${Array.isArray(value) ? "array" : typeof value}.`,
963
+ });
964
+ return raw;
965
+ }
966
+ continue;
967
+ }
801
968
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
802
969
  ctx.addIssue({
803
970
  code: z.ZodIssueCode.custom,
804
- message: `Invalid \`index.${passName}\` config: expected an object like \`{ "llm": false }\`.`,
971
+ message: `Invalid \`index.${passName}\` config: expected an object like \`{ "enabled": false }\`.`,
805
972
  });
806
973
  return raw;
807
974
  }
@@ -828,10 +995,43 @@ export const IndexConfigSchema = z.preprocess((raw, ctx) => {
828
995
  return raw;
829
996
  }, z
830
997
  .object({
998
+ defaults: IndexDefaultsSchema.optional(),
831
999
  metadataEnhance: MetadataEnhanceSchema.optional(),
832
1000
  stalenessDetection: StalenessDetectionSchema.optional(),
1001
+ indexBodyOpening: z
1002
+ .boolean()
1003
+ .optional()
1004
+ .describe("Index the first prose paragraph of each markdown asset body (capped at 280 chars) into the " +
1005
+ "lowest-weight `content` search column and the embedding text (default false). Secret/env files " +
1006
+ "and session-kind memories are never captured. Toggling the flag changes indexed text: run " +
1007
+ "`akm index --full` afterwards to re-extract every entry and regenerate embeddings, and re-mint " +
1008
+ "collapse-detector canary baselines via `akm improve canary --refresh`."),
833
1009
  })
834
1010
  .catchall(IndexPassConfigSchema));
1011
+ // The runtime catchall correctly validates arbitrary pass objects, but its
1012
+ // inferred string index signature also covers reserved scalar keys. Publish a
1013
+ // precise output type while retaining the stricter runtime and JSON schemas.
1014
+ export const IndexConfigSchema = IndexConfigRuntimeSchema;
1015
+ // ── Workflow engine ─────────────────────────────────────────────────────────
1016
+ /**
1017
+ * Workflow-engine settings (`workflow`).
1018
+ *
1019
+ * `maxConcurrency` is the engine-wide ceiling on concurrent units for native
1020
+ * fan-out (`akm workflow run`). It replaces the hard-coded `min(16, cores−2)`
1021
+ * cap (which matched Claude Code) with a user knob:
1022
+ * - UNSET → the CPU-derived default `min(16, max(1, cores−2))`.
1023
+ * - SET → the explicit positive integer, CLAMPED at read time to
1024
+ * `[1, WORKFLOW_MAX_CONCURRENCY_CEILING]` (64). Values above the ceiling
1025
+ * are clamped, not rejected, so a config shared across machines with wildly
1026
+ * different core counts never hard-fails validation.
1027
+ * The R3 brief/report driver surface does NOT consult this — drivers own their
1028
+ * own parallelism (the engine only caps native dispatch).
1029
+ */
1030
+ export const WorkflowConfigSchema = z
1031
+ .object({
1032
+ maxConcurrency: positiveInt.optional(),
1033
+ })
1034
+ .passthrough();
835
1035
  // ── Setup-derived recommendations ──────────────────────────────────────────
836
1036
  /**
837
1037
  * Cron-style schedule hints derived by `akm setup --reset-recommended`.
@@ -863,8 +1063,8 @@ export const SetupConfigSchema = z
863
1063
  * one-time 0.7→0.8 input transforms before the schema sees the value.
864
1064
  */
865
1065
  export const AkmConfigShape = {
866
- configVersion: z.union([z.string().min(1), z.number()]).optional(),
867
- profiles: ProfilesSchema.optional(),
1066
+ configVersion: z.literal(CURRENT_CONFIG_VERSION),
1067
+ engines: EnginesSchema.optional(),
868
1068
  defaults: DefaultsSchema.optional(),
869
1069
  // Global model-alias tiers: alias → platform → exact model string, with a
870
1070
  // reserved `"*"` platform key as fallback. Lets workflows/callers name a
@@ -874,7 +1074,7 @@ export const AkmConfigShape = {
874
1074
  // builder resolves against ("claude", "opencode", "opencode-sdk", or a
875
1075
  // custom profile's name for the default builder) — unknown keys are inert.
876
1076
  // Precedence: profile modelAliases > this table > built-in aliases.
877
- modelAliases: z.record(z.string().min(1), z.record(z.string().min(1), z.string().min(1))).optional(),
1077
+ modelAliases: GlobalModelAliasesSchema.optional(),
878
1078
  stashDir: nonEmptyString.optional(),
879
1079
  semanticSearchMode: z.enum(["off", "auto"]).default("auto"),
880
1080
  embedding: EmbeddingConnectionConfigSchema.optional(),
@@ -895,10 +1095,123 @@ export const AkmConfigShape = {
895
1095
  feedback: FeedbackConfigSchema.optional(),
896
1096
  archiveRetentionDays: nonNegativeNumber.optional(),
897
1097
  improve: ImproveConfigSchema.optional(),
1098
+ workflow: WorkflowConfigSchema.optional(),
898
1099
  setup: SetupConfigSchema.optional(),
899
1100
  };
900
1101
  export const AkmConfigBaseSchema = z.object(AkmConfigShape).passthrough();
901
1102
  export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) => {
1103
+ const raw = config;
1104
+ for (const key of ["profiles", "llm", "agent", "features", "stashes"]) {
1105
+ if (key in raw) {
1106
+ ctx.addIssue({
1107
+ code: z.ZodIssueCode.custom,
1108
+ path: [key],
1109
+ message: `${key} is retired in 0.9; configure engines and improve.strategies instead`,
1110
+ });
1111
+ }
1112
+ }
1113
+ for (const key of ["llm", "agent", "improve"]) {
1114
+ if (config.defaults && key in config.defaults) {
1115
+ ctx.addIssue({
1116
+ code: z.ZodIssueCode.custom,
1117
+ path: ["defaults", key],
1118
+ message: `defaults.${key} is retired in 0.9`,
1119
+ });
1120
+ }
1121
+ }
1122
+ for (const [name, engine] of Object.entries(config.engines ?? {})) {
1123
+ if (engine.kind === "agent" && engine.llmEngine) {
1124
+ const fallback = config.engines?.[engine.llmEngine];
1125
+ if (!fallback || fallback.kind !== "llm") {
1126
+ ctx.addIssue({
1127
+ code: z.ZodIssueCode.custom,
1128
+ path: ["engines", name, "llmEngine"],
1129
+ message: "llmEngine must name an LLM engine",
1130
+ });
1131
+ }
1132
+ }
1133
+ }
1134
+ const defaultEngine = config.defaults?.engine;
1135
+ if (defaultEngine && !config.engines?.[defaultEngine]) {
1136
+ ctx.addIssue({
1137
+ code: z.ZodIssueCode.custom,
1138
+ path: ["defaults", "engine"],
1139
+ message: "engine does not name a configured engine",
1140
+ });
1141
+ }
1142
+ const defaultLlm = config.defaults?.llmEngine;
1143
+ if (defaultLlm && config.engines?.[defaultLlm]?.kind !== "llm") {
1144
+ ctx.addIssue({
1145
+ code: z.ZodIssueCode.custom,
1146
+ path: ["defaults", "llmEngine"],
1147
+ message: "llmEngine must name an LLM engine",
1148
+ });
1149
+ }
1150
+ const defaultStrategy = config.defaults?.improveStrategy;
1151
+ if (defaultStrategy &&
1152
+ !BUILTIN_IMPROVE_STRATEGY_NAMES.includes(defaultStrategy) &&
1153
+ !config.improve?.strategies?.[defaultStrategy]) {
1154
+ ctx.addIssue({
1155
+ code: z.ZodIssueCode.custom,
1156
+ path: ["defaults", "improveStrategy"],
1157
+ message: "improveStrategy does not name a built-in or configured strategy",
1158
+ });
1159
+ }
1160
+ for (const [strategyName, strategy] of Object.entries(config.improve?.strategies ?? {})) {
1161
+ const strategyEngine = strategy.engine;
1162
+ if (strategyEngine) {
1163
+ const engine = config.engines?.[strategyEngine];
1164
+ if (!engine || engine.kind !== "llm") {
1165
+ ctx.addIssue({
1166
+ code: z.ZodIssueCode.custom,
1167
+ path: ["improve", "strategies", strategyName, "engine"],
1168
+ message: engine ? "strategy engine must be an LLM engine" : "engine does not name a configured engine",
1169
+ });
1170
+ }
1171
+ }
1172
+ for (const [processName, process] of Object.entries(strategy.processes ?? {})) {
1173
+ const processConfig = process;
1174
+ const capability = IMPROVE_PROCESS_ENGINE_CAPABILITIES[processName];
1175
+ if (processConfig.engine && capability === null) {
1176
+ ctx.addIssue({
1177
+ code: z.ZodIssueCode.custom,
1178
+ path: ["improve", "strategies", strategyName, "processes", processName, "engine"],
1179
+ message: `${processName} does not dispatch an engine`,
1180
+ });
1181
+ }
1182
+ else {
1183
+ const processEngine = processConfig.engine ?? strategyEngine;
1184
+ if (processEngine && capability === "llm") {
1185
+ const engine = config.engines?.[processEngine];
1186
+ if (!engine || engine.kind !== "llm") {
1187
+ ctx.addIssue({
1188
+ code: z.ZodIssueCode.custom,
1189
+ path: ["improve", "strategies", strategyName, "processes", processName, "engine"],
1190
+ message: engine ? `${processName} requires an LLM engine` : "engine does not name a configured engine",
1191
+ });
1192
+ }
1193
+ }
1194
+ else if (processConfig.engine && capability === "runner" && !config.engines?.[processConfig.engine]) {
1195
+ ctx.addIssue({
1196
+ code: z.ZodIssueCode.custom,
1197
+ path: ["improve", "strategies", strategyName, "processes", processName, "engine"],
1198
+ message: "engine does not name a configured engine",
1199
+ });
1200
+ }
1201
+ }
1202
+ const judgmentEngine = processConfig.judgment?.engine;
1203
+ if (judgmentEngine) {
1204
+ const engine = config.engines?.[judgmentEngine];
1205
+ if (!engine) {
1206
+ ctx.addIssue({
1207
+ code: z.ZodIssueCode.custom,
1208
+ path: ["improve", "strategies", strategyName, "processes", processName, "judgment", "engine"],
1209
+ message: "engine does not name a configured engine",
1210
+ });
1211
+ }
1212
+ }
1213
+ }
1214
+ }
902
1215
  // #464.a: defaultWriteTarget must name a configured source when sources
903
1216
  // are present. With no sources configured, error out instead of silently
904
1217
  // accepting (no implicit "first writable" fallback — see locked decision 3).
@@ -5,12 +5,12 @@
5
5
  // is the single source of truth replacing the previously-disconnected
6
6
  // registries. config ← harnesses is the only import direction (harnesses/ is a
7
7
  // dependency-graph leaf), so there is no cycle.
8
- import { VALID_HARNESS_IDS } from "../../integrations/harnesses/index.js";
8
+ import { HARNESS_BY_ID, VALID_HARNESS_IDS } from "../../integrations/harnesses/index.js";
9
9
  /**
10
10
  * Canonical list of valid agent harness / platform ids. Re-exported from the
11
11
  * unified harness registry (#562) so the Zod `AgentPlatformSchema` enum, the
12
- * `AgentProfileConfig` platform union, and setup's `DetectedHarness` union all
12
+ * agent-engine platform union, and setup's `DetectedHarness` union all
13
13
  * derive from one place and cannot drift. Add a harness in
14
14
  * `src/integrations/harnesses/index.ts`.
15
15
  */
16
- export { VALID_HARNESS_IDS };
16
+ export { HARNESS_BY_ID, VALID_HARNESS_IDS };