akm-cli 0.9.0-rc.1 → 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 (228) hide show
  1. package/CHANGELOG.md +223 -9
  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/stash-skeleton/README.md +28 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
  23. package/dist/assets/tasks/core/backup.yml +1 -0
  24. package/dist/assets/tasks/core/extract.yml +1 -0
  25. package/dist/assets/tasks/core/improve.yml +1 -0
  26. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  27. package/dist/assets/tasks/core/sync.yml +1 -0
  28. package/dist/assets/tasks/core/version-check.yml +1 -0
  29. package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
  30. package/dist/assets/templates/html/health.html +5 -1
  31. package/dist/cli/config-migrate.js +31 -138
  32. package/dist/cli/config-validate.js +10 -8
  33. package/dist/cli.js +48 -14
  34. package/dist/commands/agent/agent-dispatch.js +17 -16
  35. package/dist/commands/agent/agent-support.js +0 -24
  36. package/dist/commands/agent/contribute-cli.js +5 -15
  37. package/dist/commands/backup-cli.js +54 -0
  38. package/dist/commands/config-cli.js +45 -159
  39. package/dist/commands/env/secret.js +8 -5
  40. package/dist/commands/health/checks.js +130 -83
  41. package/dist/commands/health/html-report.js +4 -0
  42. package/dist/commands/health/improve-metrics.js +30 -32
  43. package/dist/commands/health/llm-usage.js +19 -19
  44. package/dist/commands/health/md-report.js +4 -0
  45. package/dist/commands/health/metrics.js +2 -1
  46. package/dist/commands/health/surfaces.js +5 -4
  47. package/dist/commands/health.js +1 -1
  48. package/dist/commands/improve/consolidate/chunking.js +2 -2
  49. package/dist/commands/improve/consolidate.js +28 -25
  50. package/dist/commands/improve/distill/promote-memory.js +5 -12
  51. package/dist/commands/improve/distill/quality-gate.js +5 -7
  52. package/dist/commands/improve/distill.js +16 -5
  53. package/dist/commands/improve/eligibility.js +22 -12
  54. package/dist/commands/improve/extract-cli.js +47 -19
  55. package/dist/commands/improve/extract.js +110 -62
  56. package/dist/commands/improve/improve-cli.js +38 -16
  57. package/dist/commands/improve/improve-result-file.js +30 -24
  58. package/dist/commands/improve/improve-strategies.js +137 -0
  59. package/dist/commands/improve/improve.js +60 -30
  60. package/dist/commands/improve/locks.js +66 -45
  61. package/dist/commands/improve/loop-stages.js +75 -33
  62. package/dist/commands/improve/memory/memory-belief.js +79 -7
  63. package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
  64. package/dist/commands/improve/preparation.js +71 -73
  65. package/dist/commands/improve/procedural.js +3 -2
  66. package/dist/commands/improve/recombine.js +2 -1
  67. package/dist/commands/improve/reflect.js +119 -214
  68. package/dist/commands/improve/shared.js +11 -5
  69. package/dist/commands/lint/base-linter.js +152 -42
  70. package/dist/commands/mv-cli.js +809 -0
  71. package/dist/commands/proposal/proposal-cli.js +18 -8
  72. package/dist/commands/proposal/propose.js +64 -69
  73. package/dist/commands/read/knowledge.js +436 -4
  74. package/dist/commands/read/remember-cli.js +39 -2
  75. package/dist/commands/read/search-cli.js +6 -1
  76. package/dist/commands/registry-cli.js +29 -14
  77. package/dist/commands/remember.js +2 -0
  78. package/dist/commands/sources/init.js +13 -14
  79. package/dist/commands/sources/schema-repair.js +2 -4
  80. package/dist/commands/sources/source-add.js +62 -73
  81. package/dist/commands/sources/source-manage.js +50 -46
  82. package/dist/commands/sources/stash-cli.js +41 -4
  83. package/dist/commands/tasks/default-tasks.js +12 -12
  84. package/dist/commands/tasks/tasks-cli.js +7 -3
  85. package/dist/commands/tasks/tasks.js +113 -18
  86. package/dist/commands/wiki-cli.js +9 -10
  87. package/dist/core/asset/frontmatter.js +12 -2
  88. package/dist/core/common.js +5 -3
  89. package/dist/core/config/config-io.js +28 -17
  90. package/dist/core/config/config-schema.js +358 -66
  91. package/dist/core/config/config-types.js +3 -3
  92. package/dist/core/config/config-version.js +29 -0
  93. package/dist/core/config/config-walker.js +98 -27
  94. package/dist/core/config/config.js +132 -266
  95. package/dist/core/config/deep-merge.js +41 -0
  96. package/dist/core/config/engine-semantics.js +32 -0
  97. package/dist/core/errors.js +2 -2
  98. package/dist/core/extra-params.js +61 -0
  99. package/dist/core/file-lock.js +201 -56
  100. package/dist/core/improve-result.js +178 -0
  101. package/dist/core/maintenance-barrier.js +119 -0
  102. package/dist/core/migration-backup.js +416 -0
  103. package/dist/core/paths.js +3 -0
  104. package/dist/core/redaction.js +358 -0
  105. package/dist/core/state/migrations.js +17 -2
  106. package/dist/core/state-db.js +44 -1
  107. package/dist/indexer/db/db.js +116 -1
  108. package/dist/indexer/graph/graph-extraction.js +28 -16
  109. package/dist/indexer/index-writer-lock.js +31 -24
  110. package/dist/indexer/index-written-assets.js +15 -6
  111. package/dist/indexer/indexer.js +47 -2
  112. package/dist/indexer/passes/memory-inference.js +10 -6
  113. package/dist/indexer/passes/metadata.js +250 -0
  114. package/dist/indexer/search/db-search.js +111 -44
  115. package/dist/indexer/search/fts-query.js +41 -0
  116. package/dist/indexer/search/ranking-contributors.js +48 -0
  117. package/dist/indexer/search/ranking.js +36 -23
  118. package/dist/indexer/search/search-fields.js +11 -1
  119. package/dist/integrations/agent/builder-shared.js +7 -0
  120. package/dist/integrations/agent/builders.js +5 -52
  121. package/dist/integrations/agent/config.js +3 -143
  122. package/dist/integrations/agent/detect.js +17 -2
  123. package/dist/integrations/agent/engine-resolution.js +202 -0
  124. package/dist/integrations/agent/index.js +1 -2
  125. package/dist/integrations/agent/model-aliases.js +7 -2
  126. package/dist/integrations/agent/profiles.js +6 -99
  127. package/dist/integrations/agent/runner-dispatch.js +76 -13
  128. package/dist/integrations/agent/runner.js +76 -207
  129. package/dist/integrations/agent/spawn.js +4 -6
  130. package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
  131. package/dist/integrations/harnesses/aider/index.js +0 -1
  132. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
  133. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  134. package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
  135. package/dist/integrations/harnesses/claude/index.js +0 -2
  136. package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
  137. package/dist/integrations/harnesses/codex/index.js +0 -1
  138. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
  139. package/dist/integrations/harnesses/copilot/index.js +0 -1
  140. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
  141. package/dist/integrations/harnesses/gemini/index.js +0 -1
  142. package/dist/integrations/harnesses/index.js +1 -24
  143. package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
  144. package/dist/integrations/harnesses/opencode/index.js +0 -6
  145. package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
  146. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
  147. package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
  148. package/dist/integrations/harnesses/openhands/index.js +0 -1
  149. package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
  150. package/dist/integrations/harnesses/pi/index.js +0 -1
  151. package/dist/integrations/harnesses/types.js +1 -32
  152. package/dist/integrations/lockfile.js +32 -21
  153. package/dist/llm/client.js +48 -14
  154. package/dist/llm/feature-gate.js +15 -47
  155. package/dist/llm/graph-extract.js +1 -1
  156. package/dist/llm/index-passes.js +8 -42
  157. package/dist/llm/memory-infer-impl.js +1 -1
  158. package/dist/llm/usage-persist.js +4 -0
  159. package/dist/llm/usage-telemetry.js +35 -5
  160. package/dist/output/shapes/helpers.js +2 -1
  161. package/dist/output/shapes/passthrough.js +2 -0
  162. package/dist/output/text/helpers.js +3 -1
  163. package/dist/schemas/akm-config.json +11013 -8600
  164. package/dist/schemas/akm-task.json +87 -0
  165. package/dist/schemas/akm-workflow.json +57 -13
  166. package/dist/scripts/migrate-storage.js +8591 -509
  167. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
  168. package/dist/setup/detected-engines.js +142 -0
  169. package/dist/setup/engine-config.js +89 -0
  170. package/dist/setup/setup.js +236 -132
  171. package/dist/setup/steps/connection.js +61 -32
  172. package/dist/setup/steps/platforms.js +4 -4
  173. package/dist/setup/steps.js +3 -2
  174. package/dist/storage/database.js +13 -1
  175. package/dist/storage/engines/sqlite-migrations.js +1 -0
  176. package/dist/storage/repositories/improve-runs-repository.js +5 -5
  177. package/dist/storage/repositories/task-history-repository.js +78 -0
  178. package/dist/storage/repositories/workflow-runs-repository.js +9 -8
  179. package/dist/tasks/parser.js +138 -52
  180. package/dist/tasks/runner.js +71 -75
  181. package/dist/tasks/schema.js +1 -1
  182. package/dist/tasks/validator.js +11 -6
  183. package/dist/wiki/wiki.js +9 -8
  184. package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
  185. package/dist/workflows/concurrency-policy.js +15 -0
  186. package/dist/workflows/db.js +65 -13
  187. package/dist/workflows/exec/brief.js +18 -24
  188. package/dist/workflows/exec/frozen-judge.js +47 -0
  189. package/dist/workflows/exec/native-executor.js +124 -65
  190. package/dist/workflows/exec/report.js +93 -33
  191. package/dist/workflows/exec/run-workflow.js +38 -25
  192. package/dist/workflows/exec/scheduler.js +12 -41
  193. package/dist/workflows/exec/step-work.js +91 -35
  194. package/dist/workflows/ir/compile.js +13 -26
  195. package/dist/workflows/ir/freeze.js +243 -0
  196. package/dist/workflows/ir/plan-hash.js +40 -5
  197. package/dist/workflows/ir/schema.js +542 -1
  198. package/dist/workflows/parser.js +7 -0
  199. package/dist/workflows/program/parser.js +132 -23
  200. package/dist/workflows/program/project.js +3 -4
  201. package/dist/workflows/program/schema.js +2 -2
  202. package/dist/workflows/resource-limits.js +20 -0
  203. package/dist/workflows/runtime/plan-classifier.js +187 -0
  204. package/dist/workflows/runtime/runs.js +54 -47
  205. package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
  206. package/dist/workflows/validator.js +25 -0
  207. package/docs/data-and-telemetry.md +2 -2
  208. package/docs/migration/release-notes/0.6.0.md +1 -1
  209. package/docs/migration/release-notes/0.7.0.md +5 -4
  210. package/docs/migration/v0.8-to-v0.9.md +401 -0
  211. package/package.json +4 -2
  212. package/schemas/akm-config.json +16638 -0
  213. package/schemas/akm-task.json +87 -0
  214. package/schemas/akm-workflow.json +372 -0
  215. package/dist/commands/improve/improve-profiles.js +0 -168
  216. package/dist/core/config/config-migration.js +0 -602
  217. package/dist/core/deep-merge.js +0 -38
  218. package/dist/llm/call-ai.js +0 -62
  219. package/dist/setup/legacy-config.js +0 -106
  220. package/docs/README.md +0 -104
  221. /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
  222. /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
  223. /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
  224. /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
  225. /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
  226. /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
  227. /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
  228. /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
@@ -9,7 +9,8 @@ import * as p from "../../cli/clack.js";
9
9
  import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../../integrations/agent/index.js";
10
10
  import { probeLlmCapabilities } from "../../llm/client.js";
11
11
  import { detectLMStudio, detectOllama } from "../detect.js";
12
- import { cloneLlmConfig, getCurrentAgentBlock, getCurrentLlm } from "../legacy-config.js";
12
+ import { verifyOpenAiCompatibleEndpoint } from "../detected-engines.js";
13
+ import { cloneLlmConnection, readAgentEngineSelection, readCurrentLlmEngine, } from "../engine-config.js";
13
14
  import { prompt, promptOrBack } from "../prompt.js";
14
15
  export async function stepOllama(current) {
15
16
  const spin = p.spinner();
@@ -154,7 +155,7 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels, lmStudi
154
155
  options.push({ value: "lmstudio", label: "LM Studio / local server", hint: lmStudioHint });
155
156
  options.push({ value: "custom", label: "Custom OpenAI-compatible endpoint" });
156
157
  options.push({ value: "none", label: "Skip LLM", hint: "no metadata enhancement during indexing" });
157
- const currentLlm = getCurrentLlm(current);
158
+ const currentLlm = readCurrentLlmEngine(current);
158
159
  if (currentLlm) {
159
160
  options.push({
160
161
  value: "keep",
@@ -169,7 +170,7 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels, lmStudi
169
170
  initialValue,
170
171
  }));
171
172
  if (choice === "keep")
172
- return cloneLlmConfig(currentLlm);
173
+ return cloneLlmConnection(currentLlm);
173
174
  if (choice === "none")
174
175
  return undefined;
175
176
  // Handle "Import from <Harness>" choices
@@ -180,16 +181,28 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels, lmStudi
180
181
  // Show a summary before accepting
181
182
  p.log.info(`Importing LLM config from ${harness.harnessName}: ` +
182
183
  [harness.provider, harness.model, harness.baseUrl].filter(Boolean).join(", "));
184
+ if (!harness.baseUrl || !harness.model) {
185
+ p.log.warn(`Skipping ${harness.harnessName}: no complete endpoint/model was detected.`);
186
+ return undefined;
187
+ }
188
+ const verified = await verifyOpenAiCompatibleEndpoint({
189
+ endpoint: harness.baseUrl,
190
+ model: harness.model,
191
+ apiKeyEnvVar: harness.apiKeyEnvVar,
192
+ });
193
+ if (!verified.ok) {
194
+ p.log.warn(`Skipping ${harness.harnessName}: ${verified.reason}. Fix its endpoint/credential and retry setup.`);
195
+ return undefined;
196
+ }
183
197
  const llmConfig = {
184
- endpoint: harness.baseUrl ?? "",
198
+ endpoint: verified.endpoint,
185
199
  model: harness.model ?? "",
186
200
  temperature: 0.3,
187
201
  maxTokens: 1024,
202
+ ...(harness.apiKeyEnvVar ? { apiKey: `\${${harness.apiKeyEnvVar}}` } : {}),
188
203
  };
189
204
  if (harness.provider)
190
205
  llmConfig.provider = harness.provider;
191
- if (harness.baseUrl)
192
- llmConfig.endpoint = harness.baseUrl;
193
206
  return llmConfig;
194
207
  }
195
208
  let llm;
@@ -376,7 +389,7 @@ export async function stepSmallModelConnection(current) {
376
389
  ? `${lmStudio.models.length} model${lmStudio.models.length === 1 ? "" : "s"} detected`
377
390
  : "http://localhost:1234";
378
391
  providerOptions.push({ value: "openai", label: "OpenAI", hint: "requires AKM_LLM_API_KEY" }, { value: "lmstudio", label: "LM Studio / local server", hint: lmStudioHint }, { value: "custom", label: "Custom OpenAI-compatible endpoint" }, { value: "skip", label: "Skip — disable enrichment features" });
379
- const currentLlmSmall = getCurrentLlm(current);
392
+ const currentLlmSmall = readCurrentLlmEngine(current);
380
393
  if (currentLlmSmall) {
381
394
  providerOptions.push({
382
395
  value: "keep",
@@ -391,7 +404,7 @@ export async function stepSmallModelConnection(current) {
391
404
  initialValue,
392
405
  }));
393
406
  if (providerChoice === "keep") {
394
- return { llm: cloneLlmConfig(currentLlmSmall), skipped: false, ollamaEndpoint };
407
+ return { llm: cloneLlmConnection(currentLlmSmall), skipped: false, ollamaEndpoint };
395
408
  }
396
409
  if (providerChoice === "skip") {
397
410
  p.note([
@@ -539,8 +552,11 @@ export async function stepSmallModelConnection(current) {
539
552
  validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
540
553
  }));
541
554
  const apiKeyInput = await promptOrBack(() => p.text({
542
- message: "API key (optional press Enter to skip):",
543
- placeholder: "",
555
+ message: "API key environment variable name (optional):",
556
+ placeholder: "CUSTOM_LLM_API_KEY",
557
+ validate: (value) => value && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)
558
+ ? "Use an environment variable name, not a key value"
559
+ : undefined,
544
560
  }));
545
561
  llm = {
546
562
  provider: "custom",
@@ -548,7 +564,7 @@ export async function stepSmallModelConnection(current) {
548
564
  model: model.trim(),
549
565
  temperature: 0.3,
550
566
  maxTokens: 1024,
551
- ...(apiKeyInput?.trim() ? { apiKey: apiKeyInput.trim() } : {}),
567
+ ...(apiKeyInput?.trim() ? { apiKey: `\${${apiKeyInput.trim()}}` } : {}),
552
568
  };
553
569
  }
554
570
  // Best-effort probe — never blocks setup.
@@ -584,7 +600,7 @@ export async function stepAgentConnection(current, smallModel) {
584
600
  ].join("\n"));
585
601
  // Detect available CLI agents.
586
602
  const detections = detectAgentCliProfiles(current);
587
- const currentAgentBlock = getCurrentAgentBlock(current);
603
+ const currentAgentBlock = readAgentEngineSelection(current);
588
604
  const availableClis = detections.filter((d) => d.available);
589
605
  const agentOptions = [];
590
606
  if (!smallModel.skipped && smallModel.llm) {
@@ -606,8 +622,8 @@ export async function stepAgentConnection(current, smallModel) {
606
622
  if (currentAgentBlock) {
607
623
  const currentDesc = currentAgentBlock.default
608
624
  ? `CLI: ${currentAgentBlock.default}`
609
- : currentAgentBlock.profiles?.default?.model
610
- ? `SDK: ${currentAgentBlock.profiles.default.model}`
625
+ : currentAgentBlock.engines?.default?.model
626
+ ? `SDK: ${currentAgentBlock.engines.default.model}`
611
627
  : "configured";
612
628
  agentOptions.push({ value: "keep", label: `Keep current: ${currentDesc}` });
613
629
  }
@@ -647,9 +663,9 @@ export async function stepAgentConnection(current, smallModel) {
647
663
  else {
648
664
  const baseEndpoint = smallModel.llm.endpoint.replace("/v1/chat/completions", "");
649
665
  p.log.info(`Endpoint: ${baseEndpoint} (from Step 1)`);
650
- const profileName = smallModel.llm.provider ?? "default";
666
+ const profileName = `${smallModel.llm.provider ?? "default"}-agent`;
651
667
  // Pre-populate from existing agent profile for this provider, if any.
652
- const existingAgentModel = currentAgentBlock?.profiles?.[profileName]?.model ?? smallModel.llm.model ?? undefined;
668
+ const existingAgentModel = currentAgentBlock?.engines?.[profileName]?.model ?? smallModel.llm.model ?? undefined;
653
669
  const agentModel = await prompt(() => p.text({
654
670
  message: "Model to use for agent tasks (same model is fine, larger models work better):",
655
671
  placeholder: existingAgentModel ?? "qwen2.5-coder:32b",
@@ -658,13 +674,14 @@ export async function stepAgentConnection(current, smallModel) {
658
674
  }));
659
675
  return {
660
676
  ...(currentAgentBlock ?? {}),
661
- profiles: {
662
- ...(currentAgentBlock?.profiles ?? {}),
677
+ engines: {
678
+ ...(currentAgentBlock?.engines ?? {}),
663
679
  [profileName]: {
664
- ...(currentAgentBlock?.profiles?.[profileName] ?? {}),
665
- sdkMode: true,
680
+ ...(currentAgentBlock?.engines?.[profileName] ?? {}),
681
+ kind: "agent",
682
+ platform: "opencode-sdk",
666
683
  model: agentModel.trim(),
667
- endpoint: smallModel.llm.endpoint,
684
+ ...(current.defaults?.llmEngine ? { llmEngine: current.defaults.llmEngine } : {}),
668
685
  },
669
686
  },
670
687
  default: profileName,
@@ -693,8 +710,8 @@ export async function stepAgentConnection(current, smallModel) {
693
710
  }
694
711
  // "new-connection" (also fall-through from "same-provider" when Step 1 was skipped)
695
712
  // Pre-populate from current "custom" agent profile if available.
696
- const currentCustomAgentProfile = currentAgentBlock?.profiles?.custom;
697
- const currentNewEndpoint = currentCustomAgentProfile?.endpoint ?? undefined;
713
+ const currentCustomAgentProfile = currentAgentBlock?.engines?.["custom-agent"];
714
+ const currentNewEndpoint = undefined;
698
715
  const currentNewModel = currentCustomAgentProfile?.model ?? undefined;
699
716
  const newEndpoint = await prompt(() => p.text({
700
717
  message: "OpenAI-compatible chat completions endpoint:",
@@ -708,8 +725,11 @@ export async function stepAgentConnection(current, smallModel) {
708
725
  },
709
726
  }));
710
727
  const newApiKeyInput = await promptOrBack(() => p.text({
711
- message: "API key (optional press Enter to skip):",
712
- placeholder: "",
728
+ message: "API key environment variable name (optional):",
729
+ placeholder: "CUSTOM_LLM_API_KEY",
730
+ validate: (value) => value && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)
731
+ ? "Use an environment variable name, not a key value"
732
+ : undefined,
713
733
  }));
714
734
  const newModel = await prompt(() => p.text({
715
735
  message: "Model name (larger is better, e.g. gpt-4o):",
@@ -717,18 +737,27 @@ export async function stepAgentConnection(current, smallModel) {
717
737
  ...(currentNewModel ? { defaultValue: currentNewModel } : {}),
718
738
  validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
719
739
  }));
740
+ const llmEngineName = "custom-llm";
741
+ const agentEngineName = "custom-agent";
720
742
  const customProfile = {
721
- sdkMode: true,
722
- endpoint: newEndpoint.trim(),
743
+ kind: "agent",
744
+ platform: "opencode-sdk",
745
+ llmEngine: llmEngineName,
723
746
  model: newModel.trim(),
724
- ...(newApiKeyInput?.trim() ? { apiKey: newApiKeyInput.trim() } : {}),
725
747
  };
726
748
  return {
727
749
  ...(currentAgentBlock ?? {}),
728
- profiles: {
729
- ...(currentAgentBlock?.profiles ?? {}),
730
- custom: customProfile,
750
+ engines: {
751
+ ...(currentAgentBlock?.engines ?? {}),
752
+ [llmEngineName]: {
753
+ kind: "llm",
754
+ provider: "custom",
755
+ endpoint: newEndpoint.trim(),
756
+ model: newModel.trim(),
757
+ ...(newApiKeyInput?.trim() ? { apiKey: `\${${newApiKeyInput.trim()}}` } : {}),
758
+ },
759
+ [agentEngineName]: customProfile,
731
760
  },
732
- default: "custom",
761
+ default: agentEngineName,
733
762
  };
734
763
  }
@@ -8,7 +8,7 @@
8
8
  import * as p from "../../cli/clack.js";
9
9
  import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../../integrations/agent/index.js";
10
10
  import { detectAgentPlatforms } from "../detect.js";
11
- import { getCurrentAgentBlock } from "../legacy-config.js";
11
+ import { readAgentEngineSelection } from "../engine-config.js";
12
12
  import { prompt } from "../prompt.js";
13
13
  export async function stepAgentPlatforms(current) {
14
14
  const platforms = detectAgentPlatforms();
@@ -66,7 +66,7 @@ export function printCapabilitySummary(smallModelSkipped, agentConfigured) {
66
66
  p.note(lines.join("\n"), "Feature Summary");
67
67
  }
68
68
  export async function stepAgentSelection(current, detections) {
69
- const currentAgentBlock = getCurrentAgentBlock(current);
69
+ const currentAgentBlock = readAgentEngineSelection(current);
70
70
  const available = detections.filter((d) => d.available);
71
71
  if (available.length === 0) {
72
72
  return currentAgentBlock;
@@ -85,7 +85,7 @@ export async function stepAgentSelection(current, detections) {
85
85
  initialValue,
86
86
  }));
87
87
  if (selectedDefault === "disabled") {
88
- if (!currentAgentBlock?.profiles && !currentAgentBlock?.timeoutMs) {
88
+ if (!currentAgentBlock?.engines) {
89
89
  return undefined;
90
90
  }
91
91
  return {
@@ -110,7 +110,7 @@ export async function stepAgentSelection(current, detections) {
110
110
  */
111
111
  export function stepAgentCliDetection(current, detectFn = detectAgentCliProfiles) {
112
112
  const detections = detectFn(current);
113
- const currentAgentBlock = getCurrentAgentBlock(current);
113
+ const currentAgentBlock = readAgentEngineSelection(current);
114
114
  const defaultName = pickDefaultAgentProfile(detections, currentAgentBlock?.default);
115
115
  // No installed agents found and no existing config → leave block absent.
116
116
  if (!defaultName && !currentAgentBlock) {
@@ -1,20 +1,21 @@
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 { deepMergeConfig } from "../core/config/deep-merge.js";
4
5
  /**
5
6
  * Build a fresh `SetupContext` over a starting config. The returned context
6
7
  * applies deltas in-place onto an internal accumulator and exposes the
7
8
  * latest snapshot via `ctx.config`.
8
9
  */
9
10
  export function createSetupContext(initial, options) {
10
- let acc = { ...initial };
11
+ let acc = deepMergeConfig({}, initial);
11
12
  return {
12
13
  get config() {
13
14
  return acc;
14
15
  },
15
16
  nonInteractive: options.nonInteractive,
16
17
  apply(delta) {
17
- acc = { ...acc, ...delta };
18
+ acc = deepMergeConfig(acc, delta);
18
19
  },
19
20
  };
20
21
  }
@@ -133,5 +133,17 @@ function openNodeDatabase(path, opts) {
133
133
  if (opts?.create === false)
134
134
  options.fileMustExist = true;
135
135
  const db = opts ? new BetterSqlite3(path, options) : new BetterSqlite3(path);
136
- return db;
136
+ return {
137
+ prepare: db.prepare.bind(db),
138
+ exec: db.exec.bind(db),
139
+ // better-sqlite3 exposes mutations on prepared statements, while
140
+ // bun:sqlite also provides db.run(). Normalize the latter at the provider
141
+ // boundary so callers and maintenance wrappers can rely on one contract.
142
+ run: (sql, ...params) => db.prepare(sql).run(...params),
143
+ transaction: db.transaction.bind(db),
144
+ get inTransaction() {
145
+ return db.inTransaction;
146
+ },
147
+ close: db.close.bind(db),
148
+ };
137
149
  }
@@ -34,6 +34,7 @@ export function runMigrations(db, migrations, opts) {
34
34
  for (const migration of migrations) {
35
35
  if (applied.has(migration.id))
36
36
  continue;
37
+ opts?.beforeMigration?.(migration);
37
38
  db.transaction(() => {
38
39
  db.exec(migration.up);
39
40
  db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(migration.id);
@@ -103,10 +103,10 @@ export function recordImproveRun(db, input) {
103
103
  const metricsObj = input.metrics ?? computeImproveRunMetrics(input.result);
104
104
  db.prepare(`
105
105
  INSERT INTO improve_runs
106
- (id, started_at, completed_at, stash_dir, dry_run, profile,
106
+ (id, started_at, completed_at, stash_dir, dry_run, profile, strategy,
107
107
  scope_mode, scope_value, guidance, ok, result_json, metrics_json, metadata_json)
108
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
109
- `).run(input.id, input.startedAt, input.completedAt, input.stashDir, input.dryRun ? 1 : 0, input.profile, input.scopeMode, input.scopeValue, input.guidance, input.ok ? 1 : 0, JSON.stringify(input.result), JSON.stringify(metricsObj), JSON.stringify(input.metadata ?? {}));
108
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
109
+ `).run(input.id, input.startedAt, input.completedAt, input.stashDir, input.dryRun ? 1 : 0, input.legacyProfile ?? null, input.strategy ?? null, input.scopeMode, input.scopeValue, input.guidance, input.ok ? 1 : 0, JSON.stringify(input.result), JSON.stringify(metricsObj), JSON.stringify(input.metadata ?? {}));
110
110
  }
111
111
  /**
112
112
  * Read real (non-dry-run) improve_runs rows whose `started_at` falls in the
@@ -124,8 +124,8 @@ export function recordImproveRun(db, input) {
124
124
  */
125
125
  export function queryImproveRuns(db, since, until) {
126
126
  const sql = until
127
- ? "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, result_json FROM improve_runs WHERE started_at >= ? AND started_at < ? AND dry_run = 0 ORDER BY started_at DESC"
128
- : "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, result_json FROM improve_runs WHERE started_at >= ? AND dry_run = 0 ORDER BY started_at DESC";
127
+ ? "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, profile AS legacyProfile, strategy, result_json FROM improve_runs WHERE started_at >= ? AND started_at < ? AND dry_run = 0 ORDER BY started_at DESC"
128
+ : "SELECT id, started_at, completed_at, ok, scope_mode, scope_value, profile AS legacyProfile, strategy, result_json FROM improve_runs WHERE started_at >= ? AND dry_run = 0 ORDER BY started_at DESC";
129
129
  return (until ? db.prepare(sql).all(since, until) : db.prepare(sql).all(since));
130
130
  }
131
131
  /**
@@ -1,6 +1,84 @@
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
+ function metadataError(message) {
5
+ throw new Error(`invalid task_history metadata_json: ${message}`);
6
+ }
7
+ function isRecord(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function validateDetail(value) {
11
+ if (value === undefined || value === null)
12
+ return;
13
+ if (!isRecord(value))
14
+ metadataError("detail must be an object or null");
15
+ const allowed = new Set(["runId", "reason", "error", "exitCode"]);
16
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
17
+ if (unknown.length > 0)
18
+ metadataError(`unknown detail fields: ${unknown.sort().join(", ")}`);
19
+ for (const field of ["runId", "reason", "error"]) {
20
+ if (value[field] !== undefined && typeof value[field] !== "string")
21
+ metadataError(`detail.${field} must be a string`);
22
+ }
23
+ if (value.exitCode !== undefined && value.exitCode !== null && typeof value.exitCode !== "number") {
24
+ metadataError("detail.exitCode must be a number or null");
25
+ }
26
+ }
27
+ /** Decode historical v1 and current v2 metadata without cross-version aliases. */
28
+ export function decodeTaskHistoryMetadata(input) {
29
+ let parsed = input;
30
+ if (typeof input === "string") {
31
+ try {
32
+ parsed = JSON.parse(input);
33
+ }
34
+ catch {
35
+ metadataError("not valid JSON");
36
+ }
37
+ }
38
+ if (!isRecord(parsed))
39
+ metadataError("root must be an object");
40
+ const version = parsed.metadataVersion ?? 1;
41
+ if (version === 1) {
42
+ const allowed = new Set(["metadataVersion", "durationMs", "detail", "profile"]);
43
+ const unknown = Object.keys(parsed).filter((key) => !allowed.has(key));
44
+ if (unknown.length > 0)
45
+ metadataError(`unknown v1 fields: ${unknown.sort().join(", ")}`);
46
+ if (parsed.metadataVersion !== undefined && parsed.metadataVersion !== 1)
47
+ metadataError("invalid v1 metadataVersion");
48
+ if (parsed.durationMs !== undefined && typeof parsed.durationMs !== "number")
49
+ metadataError("durationMs must be a number");
50
+ if (parsed.profile !== undefined && typeof parsed.profile !== "string")
51
+ metadataError("profile must be a string");
52
+ validateDetail(parsed.detail);
53
+ return {
54
+ metadataVersion: 1,
55
+ ...(typeof parsed.durationMs === "number" ? { durationMs: parsed.durationMs } : {}),
56
+ ...(parsed.detail !== undefined && parsed.detail !== null ? { detail: parsed.detail } : {}),
57
+ ...(typeof parsed.profile === "string" ? { legacyProfile: parsed.profile } : {}),
58
+ };
59
+ }
60
+ if (version === 2) {
61
+ const allowed = new Set(["metadataVersion", "durationMs", "detail", "engine"]);
62
+ const unknown = Object.keys(parsed).filter((key) => !allowed.has(key));
63
+ if (unknown.length > 0)
64
+ metadataError(`unknown v2 fields: ${unknown.sort().join(", ")}`);
65
+ if (typeof parsed.durationMs !== "number")
66
+ metadataError("durationMs must be a number");
67
+ if (!("detail" in parsed))
68
+ metadataError("detail is required in v2");
69
+ if (parsed.engine !== undefined && parsed.engine !== null && typeof parsed.engine !== "string") {
70
+ metadataError("engine must be a string or null");
71
+ }
72
+ validateDetail(parsed.detail);
73
+ return {
74
+ metadataVersion: 2,
75
+ durationMs: parsed.durationMs,
76
+ detail: parsed.detail ?? null,
77
+ ...(parsed.engine !== undefined ? { engine: parsed.engine } : {}),
78
+ };
79
+ }
80
+ metadataError(`unsupported metadataVersion: ${String(version)}`);
81
+ }
4
82
  /**
5
83
  * Upsert a task history row.
6
84
  */
@@ -137,10 +137,10 @@ export class WorkflowRunsRepository {
137
137
  * `insertRun`, so a run row never exists without its frozen plan. Read back
138
138
  * via {@link getRunById} (`plan_json` / `plan_hash` on the row).
139
139
  */
140
- setRunPlan(runId, planJson, planHash) {
140
+ setRunPlan(runId, planJson, planHash, planIrVersion = 3) {
141
141
  this.db
142
- .prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ? WHERE id = ?")
143
- .run(planJson, planHash, runId);
142
+ .prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = ? WHERE id = ?")
143
+ .run(planJson, planHash, planIrVersion, runId);
144
144
  }
145
145
  // ── engine run lease (migration 006 columns, R2 enforcement) ──────────────
146
146
  //
@@ -238,15 +238,16 @@ export class WorkflowRunsRepository {
238
238
  insertUnit(input) {
239
239
  this.db
240
240
  .prepare(`INSERT INTO workflow_run_units (
241
- run_id, unit_id, step_id, node_id, parent_unit_id, phase, runner, model,
241
+ run_id, unit_id, step_id, node_id, parent_unit_id, phase, runner, engine, model,
242
242
  status, input_hash, worktree_path, started_at, claim_holder, claim_expires_at
243
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?, ?)
243
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?, ?, ?)
244
244
  ON CONFLICT(run_id, unit_id) DO UPDATE SET
245
245
  step_id = excluded.step_id,
246
246
  node_id = excluded.node_id,
247
247
  parent_unit_id = excluded.parent_unit_id,
248
- phase = excluded.phase,
249
- runner = excluded.runner,
248
+ phase = excluded.phase,
249
+ runner = excluded.runner,
250
+ engine = excluded.engine,
250
251
  model = excluded.model,
251
252
  status = excluded.status,
252
253
  input_hash = excluded.input_hash,
@@ -261,7 +262,7 @@ export class WorkflowRunsRepository {
261
262
  finished_at = NULL,
262
263
  last_checkin_at = NULL,
263
264
  attempts = workflow_run_units.attempts + 1`)
264
- .run(input.runId, input.unitId, input.stepId, input.nodeId, input.parentUnitId, input.phase, input.runner, input.model, input.inputHash, input.worktreePath ?? null, input.startedAt, input.claimHolder ?? null, input.claimExpiresAt ?? null);
265
+ .run(input.runId, input.unitId, input.stepId, input.nodeId, input.parentUnitId, input.phase, input.runner, input.engine ?? null, input.model, input.inputHash, input.worktreePath ?? null, input.startedAt, input.claimHolder ?? null, input.claimExpiresAt ?? null);
265
266
  }
266
267
  /**
267
268
  * Stamp a `--status running` claim + heartbeat on a unit row (migration 009,