akm-cli 0.9.24 → 0.9.25-alpha.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 (84) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/dist/cli.js +1 -1
  3. package/dist/commands/health/checks.js +10 -11
  4. package/dist/commands/improve/consolidate/pair-pass.js +4 -2
  5. package/dist/commands/improve/consolidate.js +10 -4
  6. package/dist/commands/improve/execution.js +4 -11
  7. package/dist/commands/improve/extract-prompt.js +4 -4
  8. package/dist/commands/improve/extract.js +11 -13
  9. package/dist/commands/improve/improve-cli.js +65 -34
  10. package/dist/commands/improve/improve-strategies.js +49 -43
  11. package/dist/commands/improve/improve-usage-report.js +8 -17
  12. package/dist/commands/improve/loop-stages.js +3 -0
  13. package/dist/commands/improve/preparation.js +3 -1
  14. package/dist/commands/improve/reflect-noise.js +125 -0
  15. package/dist/commands/improve/reflect.js +105 -172
  16. package/dist/commands/improve/retrieval-gate.js +7 -2
  17. package/dist/commands/improve/stage.js +67 -24
  18. package/dist/commands/proposal/drain.js +11 -2
  19. package/dist/commands/proposal/proposal-cli.js +1 -5
  20. package/dist/commands/proposal/propose-cli.js +2 -2
  21. package/dist/commands/proposal/propose.js +72 -84
  22. package/dist/commands/proposal/validators/proposal-quality-validators.js +4 -2
  23. package/dist/commands/read/search-cli.js +0 -38
  24. package/dist/commands/remember.js +3 -3
  25. package/dist/commands/sources/schema-repair.js +1 -1
  26. package/dist/core/config/config-schema.js +37 -60
  27. package/dist/core/config/engine-semantics.js +15 -11
  28. package/dist/core/config/schema/improve-processes.js +18 -2
  29. package/dist/core/improve-result.js +3 -3
  30. package/dist/core/redaction.js +4 -0
  31. package/dist/core/spawn-env.js +25 -0
  32. package/dist/core/structured.js +11 -1
  33. package/dist/execution/source.js +10 -0
  34. package/dist/indexer/passes/memory-inference.js +2 -1
  35. package/dist/integrations/agent/builder-shared.js +15 -0
  36. package/dist/integrations/agent/config.js +1 -1
  37. package/dist/integrations/agent/engine-resolution.js +13 -31
  38. package/dist/integrations/agent/execution.js +48 -22
  39. package/dist/integrations/agent/index.js +1 -1
  40. package/dist/integrations/agent/profiles.js +2 -2
  41. package/dist/integrations/agent/prompts.js +55 -114
  42. package/dist/integrations/agent/request-lowering.js +21 -8
  43. package/dist/integrations/agent/runner-dispatch.js +96 -3
  44. package/dist/integrations/agent/runner.js +8 -2
  45. package/dist/integrations/harnesses/aider/agent-builder.js +10 -23
  46. package/dist/integrations/harnesses/aider/index.js +0 -5
  47. package/dist/integrations/harnesses/amazonq/agent-builder.js +9 -21
  48. package/dist/integrations/harnesses/amazonq/index.js +0 -5
  49. package/dist/integrations/harnesses/claude/agent-builder.js +47 -36
  50. package/dist/integrations/harnesses/claude/index.js +0 -14
  51. package/dist/integrations/harnesses/codex/agent-builder.js +3 -4
  52. package/dist/integrations/harnesses/codex/index.js +0 -4
  53. package/dist/integrations/harnesses/copilot/agent-builder.js +4 -13
  54. package/dist/integrations/harnesses/copilot/index.js +2 -7
  55. package/dist/integrations/harnesses/gemini/agent-builder.js +4 -13
  56. package/dist/integrations/harnesses/gemini/index.js +0 -5
  57. package/dist/integrations/harnesses/ids.js +12 -10
  58. package/dist/integrations/harnesses/opencode/agent-builder.js +41 -9
  59. package/dist/integrations/harnesses/opencode/index.js +0 -8
  60. package/dist/integrations/harnesses/opencode/model-config.js +33 -0
  61. package/dist/integrations/harnesses/opencode/model-work-agent.js +115 -0
  62. package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -7
  63. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +114 -28
  64. package/dist/integrations/harnesses/openhands/agent-builder.js +7 -21
  65. package/dist/integrations/harnesses/openhands/index.js +0 -5
  66. package/dist/integrations/harnesses/pi/agent-builder.js +7 -21
  67. package/dist/integrations/harnesses/pi/index.js +0 -5
  68. package/dist/llm/client.js +5 -0
  69. package/dist/llm/feature-gate.js +12 -9
  70. package/dist/llm/index-passes.js +2 -5
  71. package/dist/llm/memory-infer.js +6 -5
  72. package/dist/llm/structured-call.js +33 -11
  73. package/dist/output/shapes/passthrough.js +1 -0
  74. package/dist/scripts/akm-migrate-node.js +298 -228
  75. package/dist/scripts/akm-migrate.js +298 -228
  76. package/dist/workflows/exec/step-work.js +6 -5
  77. package/dist/workflows/exec/unit-dispatch.js +4 -13
  78. package/dist/workflows/freeze/step-values.js +1 -1
  79. package/docs/reference/cli.md +41 -18
  80. package/docs/reference/configuration.md +165 -12
  81. package/docs/reference/data-and-telemetry.md +2 -3
  82. package/docs/reference/workflow-schema.md +10 -9
  83. package/package.json +1 -1
  84. package/schemas/akm-config.json +108 -0
@@ -81,20 +81,6 @@ export const searchCommand = defineJsonCommand({
81
81
  description: "Include session assets (excluded from default search results via config.search.defaultExcludeTypes).",
82
82
  default: false,
83
83
  },
84
- // Declared as the POSITIVE name with `default: true` so citty's native
85
- // `--no-<name>` negation (it strips a leading `--no-` from ANY token and
86
- // negates the remainder BEFORE consulting the declared-args table — see
87
- // node_modules/citty/dist/index.mjs) does the work, the same pattern
88
- // `sync --push/--no-push` uses. A flag DECLARED as `no-track-usage` could
89
- // never be negated: `--no-track-usage` parses as "negate `track-usage`",
90
- // a name nothing declared, leaving the real key at its default forever
91
- // (F1/A1).
92
- "track-usage": {
93
- type: "boolean",
94
- default: true,
95
- description: "A successful search records usage-events telemetry. Default: on. Use --no-track-usage to run a " +
96
- "search that records nothing.",
97
- },
98
84
  },
99
85
  async run({ args }) {
100
86
  rejectRetiredSourceFlag();
@@ -108,7 +94,6 @@ export const searchCommand = defineJsonCommand({
108
94
  const filters = parseScopeFilterFlags(filterTokens, "--filter");
109
95
  const includeProposed = args["include-proposed"] === true;
110
96
  const belief = parseBeliefFilterMode(typeof args.belief === "string" ? args.belief : undefined);
111
- const skipLogging = args["track-usage"] === false;
112
97
  const includeSessions = args["include-sessions"];
113
98
  const assets = args.assets === true;
114
99
  const outputMode = getOutputMode();
@@ -121,7 +106,6 @@ export const searchCommand = defineJsonCommand({
121
106
  includeProposed,
122
107
  belief,
123
108
  includeSessions,
124
- skipLogging,
125
109
  assets,
126
110
  eventSource: resolveUsageEventSource(),
127
111
  attributionProjection: outputMode.shape === "agent" ? "agent" : outputMode.detail,
@@ -154,15 +138,6 @@ export const curateCommand = defineJsonCommand({
154
138
  "with a workflow asset's own `budget` field (a run-cost cap) — this is a context-size target for this " +
155
139
  "one curate call.",
156
140
  },
157
- // Declared as the POSITIVE name with `default: true` — see the
158
- // `track-usage` comment on `searchCommand` above for why a flag NAME
159
- // must never start with `no-`.
160
- "track-usage": {
161
- type: "boolean",
162
- default: true,
163
- description: "A successful curate records usage-events telemetry for the curated items. Default: on. Use " +
164
- "--no-track-usage to run a curate that records nothing.",
165
- },
166
141
  },
167
142
  async run({ args }) {
168
143
  rejectRetiredSourceFlag();
@@ -173,7 +148,6 @@ export const curateCommand = defineJsonCommand({
173
148
  const limitParsed = parsePositiveIntFlag(args.limit ?? undefined);
174
149
  const limit = limitParsed && limitParsed > 0 ? limitParsed : 4;
175
150
  const source = parseSearchSource(args.from ?? "local");
176
- const skipLogging = args["track-usage"] === false;
177
151
  const outputMode = getOutputMode();
178
152
  const packBudget = parsePositiveIntFlag(args.pack ?? undefined, "--pack");
179
153
  const curated = await akmCurate({
@@ -181,7 +155,6 @@ export const curateCommand = defineJsonCommand({
181
155
  type,
182
156
  limit,
183
157
  source,
184
- skipLogging,
185
158
  eventSource: resolveUsageEventSource(),
186
159
  attributionProjection: outputMode.shape === "agent" ? "agent" : outputMode.detail,
187
160
  });
@@ -285,15 +258,6 @@ export const showCommand = defineJsonCommand({
285
258
  type: "string",
286
259
  description: "Exact context budget in characters. Requires --context lead; mutually exclusive with --max-tokens.",
287
260
  },
288
- // Declared as the POSITIVE name with `default: true` — see the
289
- // `track-usage` comment on `searchCommand` above for why a flag NAME
290
- // must never start with `no-`.
291
- "track-usage": {
292
- type: "boolean",
293
- default: true,
294
- description: "A successful show records usage-events telemetry, including the search-selection linkage when this " +
295
- "show follows a recent search. Default: on. Use --no-track-usage to run a show that records nothing.",
296
- },
297
261
  },
298
262
  async run({ args }) {
299
263
  // `[origin//]meta[:name]` targets the stash `.meta/` convention, which is
@@ -342,14 +306,12 @@ export const showCommand = defineJsonCommand({
342
306
  if (maxContextChars !== undefined && !Number.isSafeInteger(maxContextChars)) {
343
307
  throw new UsageError("Fragment context budget is too large.", "INVALID_FLAG_VALUE");
344
308
  }
345
- const skipLogging = args["track-usage"] === false;
346
309
  const result = await akmShowUnified({
347
310
  ref: args.ref,
348
311
  detail: showDetail,
349
312
  contextMode,
350
313
  maxContextChars,
351
314
  scope,
352
- skipLogging,
353
315
  eventSource: resolveUsageEventSource(),
354
316
  });
355
317
  output("show", result);
@@ -19,7 +19,7 @@ import { warn } from "../core/warn.js";
19
19
  import { SCOPE_KEYS } from "../indexer/passes/metadata.js";
20
20
  import { callStructured } from "../llm/structured-call.js";
21
21
  import { withLlmStage } from "../llm/usage-telemetry.js";
22
- import { resolveImproveLlmExecution } from "./improve/execution.js";
22
+ import { resolveImproveExecution } from "./improve/execution.js";
23
23
  /**
24
24
  * Parse a shorthand duration string to a number of milliseconds.
25
25
  * Supports the CLI-wide canonical grammar: `30d` (days), `12h` (hours),
@@ -224,9 +224,9 @@ const LLM_ENRICH_TIMEOUT_MS = 10_000;
224
224
  */
225
225
  export async function runLlmEnrich(body) {
226
226
  const config = loadConfig();
227
- const resolved = resolveImproveLlmExecution({ config, processName: "remember-enrich" });
227
+ const resolved = resolveImproveExecution({ config, processName: "remember-enrich" });
228
228
  if (!resolved) {
229
- warn("Warning: --enrich requires an LLM to be configured. Run `akm setup` to configure one.");
229
+ warn("Warning: --enrich requires an engine to be configured. Run `akm setup` to configure one.");
230
230
  return { tags: [] };
231
231
  }
232
232
  const runner = resolved.runner;
@@ -105,7 +105,7 @@ export async function runSchemaRepairPass(failures, options) {
105
105
  const { startMs, budgetMs, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, chatFn, } = options;
106
106
  const llmRunner = options.llmRunner ?? null;
107
107
  if (!llmRunner)
108
- throw new Error("runSchemaRepairPass requires a resolved LLM runner");
108
+ throw new Error("runSchemaRepairPass requires a resolved runner");
109
109
  if (!stashDir) {
110
110
  throw new Error("runSchemaRepairPass requires stashDir so repairs route through the proposal queue");
111
111
  }
@@ -39,7 +39,8 @@
39
39
  * enforced at save time via `superRefine` on the top-level schema.
40
40
  */
41
41
  import { z } from "zod";
42
- import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "./engine-semantics.js";
42
+ import { HARNESS_MODEL_WORK_IDS } from "../../integrations/harnesses/ids.js";
43
+ import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_ENGINE_PROCESSES } from "./engine-semantics.js";
43
44
  import { EmbeddingConnectionConfigSchema } from "./schema/embedding.js";
44
45
  import { EnginesSchema } from "./schema/engines.js";
45
46
  import { ExecutionPolicyConfigSchema } from "./schema/execution.js";
@@ -222,13 +223,32 @@ export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) =>
222
223
  message: "engine does not name a configured engine",
223
224
  });
224
225
  }
225
- const defaultLlm = config.defaults?.llmEngine;
226
- if (defaultLlm && config.engines?.[defaultLlm]?.kind !== "llm") {
227
- ctx.addIssue({
228
- code: z.ZodIssueCode.custom,
229
- path: ["defaults", "llmEngine"],
230
- message: "llmEngine must name an LLM engine",
231
- });
226
+ // One rule for every key unattended model work reads its engine from: the
227
+ // engine must confine the model-work tool policy (an LLM, or an agent whose
228
+ // harness does), because that work runs over generated content with no one
229
+ // watching.
230
+ const confining = [...HARNESS_MODEL_WORK_IDS];
231
+ const confiningPlatforms = `${confining.slice(0, -1).join(", ")} or ${confining.at(-1)}`;
232
+ const modelWorkEngine = (name, path) => {
233
+ if (!name)
234
+ return;
235
+ const engine = config.engines?.[name];
236
+ if (!engine) {
237
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path, message: "engine does not name a configured engine" });
238
+ }
239
+ else if (engine.kind !== "llm" && !HARNESS_MODEL_WORK_IDS.has(engine.platform)) {
240
+ ctx.addIssue({
241
+ code: z.ZodIssueCode.custom,
242
+ path,
243
+ message: `engine "${name}" (platform ${engine.platform}) cannot confine the model-work tool policy, which unattended model work requires. Use an LLM engine, or an agent engine on ${confiningPlatforms}.`,
244
+ });
245
+ }
246
+ };
247
+ modelWorkEngine(config.defaults?.llmEngine, ["defaults", "llmEngine"]);
248
+ for (const [passName, pass] of Object.entries(config.index ?? {})) {
249
+ const engine = pass?.engine;
250
+ if (typeof engine === "string")
251
+ modelWorkEngine(engine, ["index", passName, "engine"]);
232
252
  }
233
253
  const workflowJudge = config.workflow?.judgeEngine;
234
254
  if (workflowJudge && !config.engines?.[workflowJudge]) {
@@ -256,68 +276,25 @@ export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) =>
256
276
  });
257
277
  }
258
278
  for (const [strategyName, strategy] of Object.entries(config.improve?.strategies ?? {})) {
259
- const strategyEngine = strategy.engine;
260
- if (strategyEngine) {
261
- const engine = config.engines?.[strategyEngine];
262
- if (!engine || engine.kind !== "llm") {
263
- ctx.addIssue({
264
- code: z.ZodIssueCode.custom,
265
- path: ["improve", "strategies", strategyName, "engine"],
266
- message: engine ? "strategy engine must be an LLM engine" : "engine does not name a configured engine",
267
- });
268
- }
269
- }
279
+ const strategyPath = ["improve", "strategies", strategyName];
280
+ modelWorkEngine(strategy.engine, [...strategyPath, "engine"]);
270
281
  for (const [processName, process] of Object.entries(strategy.processes ?? {})) {
271
282
  const processConfig = process;
272
- const capability = IMPROVE_PROCESS_ENGINE_CAPABILITIES[processName];
273
- if (processConfig.engine && capability === null) {
283
+ const processPath = [...strategyPath, "processes", processName];
284
+ if (processConfig.engine && !IMPROVE_ENGINE_PROCESSES.includes(processName)) {
274
285
  ctx.addIssue({
275
286
  code: z.ZodIssueCode.custom,
276
- path: ["improve", "strategies", strategyName, "processes", processName, "engine"],
287
+ path: [...processPath, "engine"],
277
288
  message: `${processName} does not dispatch an engine`,
278
289
  });
279
290
  }
280
291
  else {
281
- const processEngine = processConfig.engine ?? strategyEngine;
282
- if (processEngine && capability === "llm") {
283
- const engine = config.engines?.[processEngine];
284
- if (!engine || engine.kind !== "llm") {
285
- ctx.addIssue({
286
- code: z.ZodIssueCode.custom,
287
- path: ["improve", "strategies", strategyName, "processes", processName, "engine"],
288
- message: engine ? `${processName} requires an LLM engine` : "engine does not name a configured engine",
289
- });
290
- }
291
- }
292
- else if (processConfig.engine && capability === "runner" && !config.engines?.[processConfig.engine]) {
293
- ctx.addIssue({
294
- code: z.ZodIssueCode.custom,
295
- path: ["improve", "strategies", strategyName, "processes", processName, "engine"],
296
- message: "engine does not name a configured engine",
297
- });
298
- }
292
+ modelWorkEngine(processConfig.engine, [...processPath, "engine"]);
299
293
  }
300
- const judgmentEngine = processConfig.judgment?.engine;
301
- if (processConfig.judgment?.enabled === true && judgmentEngine) {
302
- const engine = config.engines?.[judgmentEngine];
303
- if (!engine) {
304
- ctx.addIssue({
305
- code: z.ZodIssueCode.custom,
306
- path: ["improve", "strategies", strategyName, "processes", processName, "judgment", "engine"],
307
- message: "engine does not name a configured engine",
308
- });
309
- }
310
- }
311
- const gateEngine = processConfig.qualityGate?.engine;
312
- if (gateEngine && config.engines?.[gateEngine]?.kind !== "llm") {
313
- ctx.addIssue({
314
- code: z.ZodIssueCode.custom,
315
- path: ["improve", "strategies", strategyName, "processes", processName, "qualityGate", "engine"],
316
- message: config.engines?.[gateEngine]
317
- ? "a quality-gate judge must be an LLM engine"
318
- : "engine does not name a configured engine",
319
- });
294
+ if (processConfig.judgment?.enabled === true) {
295
+ modelWorkEngine(processConfig.judgment.engine, [...processPath, "judgment", "engine"]);
320
296
  }
297
+ modelWorkEngine(processConfig.qualityGate?.engine, [...processPath, "qualityGate", "engine"]);
321
298
  }
322
299
  }
323
300
  // #464.a: defaultWriteTarget must name a configured source. 0.9.0 (spec
@@ -11,14 +11,18 @@ export const BUILTIN_IMPROVE_STRATEGY_NAMES = [
11
11
  "reflect-distill",
12
12
  "proactive-maintenance",
13
13
  ];
14
- /** Engine capability required by each configured improve process. `null` means engine-free. */
15
- export const IMPROVE_PROCESS_ENGINE_CAPABILITIES = {
16
- reflect: "llm",
17
- distill: "llm",
18
- consolidate: "llm",
19
- memoryInference: "llm",
20
- extract: "llm",
21
- validation: "llm",
22
- triage: "runner",
23
- proactiveMaintenance: null,
24
- };
14
+ /**
15
+ * The improve processes that use an engine. Triage's engine is its judgment's;
16
+ * each of the others makes the process's own model calls.
17
+ */
18
+ export const IMPROVE_ENGINE_PROCESSES = [
19
+ "reflect",
20
+ "distill",
21
+ "consolidate",
22
+ "memoryInference",
23
+ "extract",
24
+ "validation",
25
+ "triage",
26
+ ];
27
+ /** Every improve process, in plan order: the engine processes and `proactiveMaintenance`, which uses none. */
28
+ export const IMPROVE_PROCESS_NAMES = [...IMPROVE_ENGINE_PROCESSES, "proactiveMaintenance"];
@@ -6,7 +6,7 @@
6
6
  * verbatim from the former `config-schema.ts` monolith — no behavior change.
7
7
  */
8
8
  import { z } from "zod";
9
- import { IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "../engine-semantics.js";
9
+ import { IMPROVE_PROCESS_NAMES } from "../engine-semantics.js";
10
10
  import { engineName, LlmInvocationOverridesSchema, nonEmptyString, positiveInt } from "./primitives.js";
11
11
  // ── Improve profile / process ──────────────────────────────────────────────
12
12
  //
@@ -103,6 +103,21 @@ const fidelityCheckField = z.object({ enabled: z.boolean().optional() }).passthr
103
103
  * byte-identical behaviour). Reflect process only.
104
104
  */
105
105
  const lowValueFilterField = z.object({ enabled: z.boolean().optional() }).passthrough().optional();
106
+ /**
107
+ * The wording lists of reflect's pre-judge defect filter (`findReflectDefect`).
108
+ * Each list is optional: one that is set replaces that rule's default list, and
109
+ * an empty one turns the rule off. Phrases (`placeholders`, `metaCommentary`)
110
+ * match as whole words in any case; `frontmatterKeys` are exact key names.
111
+ * Reflect process only.
112
+ */
113
+ const defectFilterField = z
114
+ .object({
115
+ placeholders: z.array(nonEmptyString).optional(),
116
+ metaCommentary: z.array(nonEmptyString).optional(),
117
+ frontmatterKeys: z.array(nonEmptyString).optional(),
118
+ })
119
+ .passthrough()
120
+ .optional();
106
121
  /**
107
122
  * #626 — extract process: pre-LLM heuristic triage gate. When enabled, a
108
123
  * deterministic scorer decides BEFORE the extraction LLM call whether a
@@ -159,6 +174,7 @@ const REFLECT_PROCESS_FIELDS = {
159
174
  limit: processLimitField,
160
175
  qualityGate: qualityGateField,
161
176
  lowValueFilter: lowValueFilterField,
177
+ defectFilter: defectFilterField,
162
178
  };
163
179
  const DISTILL_PROCESS_FIELDS = {
164
180
  allowedTypes: allowedTypesField,
@@ -319,7 +335,7 @@ const ImproveProfileProcessesSchema = z
319
335
  });
320
336
  }
321
337
  for (const [name, process] of Object.entries(val)) {
322
- if (!(name in IMPROVE_PROCESS_ENGINE_CAPABILITIES) &&
338
+ if (!IMPROVE_PROCESS_NAMES.includes(name) &&
323
339
  !RETIRED_PROCESS_NAMES.has(name) &&
324
340
  process !== null &&
325
341
  typeof process === "object" &&
@@ -3,7 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { cloneExecutionJsonObject } from "../execution/json.js";
5
5
  import { isRecord } from "./common.js";
6
- import { IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "./config/engine-semantics.js";
6
+ import { IMPROVE_PROCESS_NAMES } from "./config/engine-semantics.js";
7
7
  const COMMON_FIELDS = [
8
8
  "schemaVersion",
9
9
  "ok",
@@ -192,11 +192,11 @@ function validateProactivePlan(value) {
192
192
  fail("plan.proactive.selected must equal plan.proactive.selectedRefs.length");
193
193
  }
194
194
  }
195
- /** #947 — plan.processes: one row per IMPROVE_PROCESS_ENGINE_CAPABILITIES name, plus an optional "triage.judgment" row. */
195
+ /** #947 — plan.processes: one row per IMPROVE_PROCESS_NAMES name, plus an optional "triage.judgment" row. */
196
196
  function validateProcessRoutingRows(value) {
197
197
  if (!Array.isArray(value))
198
198
  fail("plan.processes must be an array");
199
- const canonicalNames = Object.keys(IMPROVE_PROCESS_ENGINE_CAPABILITIES);
199
+ const canonicalNames = IMPROVE_PROCESS_NAMES;
200
200
  const engineKinds = new Set(["llm", "agent", "sdk"]);
201
201
  const seen = new Set();
202
202
  for (const row of value) {
@@ -19,6 +19,10 @@ const ENV_PASSTHROUGH_REDACTION_POLICY = {
19
19
  OPENCODE_CONFIG: "path",
20
20
  CLAUDE_CONFIG: "path",
21
21
  CODEX_CONFIG: "path",
22
+ XDG_CONFIG_HOME: "path",
23
+ XDG_DATA_HOME: "path",
24
+ XDG_CACHE_HOME: "path",
25
+ XDG_STATE_HOME: "path",
22
26
  AWS_PROFILE: "identifier",
23
27
  AWS_REGION: "identifier",
24
28
  LLM_MODEL: "identifier",
@@ -45,6 +45,31 @@ export const COMMON_SPAWN_ENV_PASSTHROUGH = [
45
45
  "TMPDIR",
46
46
  "AKM_EVENT_SOURCE",
47
47
  ];
48
+ /**
49
+ * The XDG base-directory variables. opencode resolves its config, data, cache
50
+ * and state directories from them, so it must receive them: an akm that runs
51
+ * under a custom `XDG_CONFIG_HOME` otherwise spawns an opencode that reads
52
+ * `$HOME/.config/opencode` and misses the provider config its caller named.
53
+ *
54
+ * Deliberately NOT part of {@link COMMON_SPAWN_ENV_PASSTHROUGH}, which is every
55
+ * harness's baseline, the workflow exec unit's default allowlist (a documented
56
+ * list) and, through profile `envPassthrough`, frozen into workflow plans.
57
+ * codex, gemini and pi keep their own dotdirs under `$HOME`, and handing the
58
+ * names to a shell command would redirect the `git` and `gh` config it reads. A
59
+ * harness that reads them asks for them by name: the opencode profile's list,
60
+ * and the opencode-sdk server's allowlist (`opencodeSdkServerEnvironmentNames`).
61
+ *
62
+ * A name added to a profile's list changes the plans frozen after it (their
63
+ * bytes, so their `plan_hash`); a stored plan keeps the list it was frozen with
64
+ * and still resumes, because nothing gates on that hash. The SDK server's
65
+ * allowlist is not part of a plan, so it takes the names by code.
66
+ */
67
+ export const XDG_BASE_DIR_ENV_PASSTHROUGH = [
68
+ "XDG_CONFIG_HOME",
69
+ "XDG_DATA_HOME",
70
+ "XDG_CACHE_HOME",
71
+ "XDG_STATE_HOME",
72
+ ];
48
73
  /**
49
74
  * The names Windows itself requires of ANY child, whatever the caller's
50
75
  * allowlist says. Applied at build time rather than added to
@@ -24,7 +24,17 @@
24
24
  * jittered retry; `runAgent` has timeout/abort semantics).
25
25
  */
26
26
  import { parseEmbeddedJsonResponse } from "./parse.js";
27
- function defaultFeedback(failure) {
27
+ /**
28
+ * Append the one structured-output instruction to a prompt. Agent lowering
29
+ * appends it to every schema-bearing request, alongside a harness's native
30
+ * channel where one exists (codex `--output-schema`); the workflow engine
31
+ * appends it for a direct-LLM unit. Resumed workflow runs depend on these
32
+ * exact bytes, so do not reword it.
33
+ */
34
+ export function withSchemaInstruction(prompt, schema) {
35
+ return `${prompt}\n\nRespond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(schema)}`;
36
+ }
37
+ export function defaultFeedback(failure) {
28
38
  if (failure.reason === "parse_error") {
29
39
  return "Your previous response contained no parseable JSON. Respond with ONLY a JSON value that matches the requested schema — no prose, no code fences.";
30
40
  }
@@ -5,6 +5,16 @@ import { cloneExecutionJson, cloneExecutionJsonObject } from "./json.js";
5
5
  export const EXECUTION_SOURCE_SCHEMA_VERSION = 1;
6
6
  /** Current internal adapter identifiers are lowercase kebab-case registry keys. */
7
7
  export const EXECUTION_ADAPTER_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
8
+ /**
9
+ * The model-work tool policy: unattended model work (improve, the judges, index
10
+ * passes, remember) may read, edit only inside the dispatch's own scratch
11
+ * working directory, and run `akm search` and `akm show`; the stash stays
12
+ * read-only to it. A transport grants what it can confine and refuses the policy
13
+ * at build when it can confine nothing; an LLM has no tools. A request carries it
14
+ * as `authorization.policy.id`, set only when its caller asks (`modelWork`): no
15
+ * `tools` value names it, so an asset's own `tools:` cannot.
16
+ */
17
+ export const MODEL_WORK_POLICY_ID = "model-work";
8
18
  function requireRecord(value, path) {
9
19
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
10
20
  throw new TypeError(`${path} must be an object`);
@@ -48,6 +48,7 @@ import { ConfigError } from "../../core/errors.js";
48
48
  import { warn } from "../../core/warn.js";
49
49
  import { beginWriteProvenance, recordWrittenPath } from "../../core/write-provenance.js";
50
50
  import { writeAssetToSource } from "../../core/write-source.js";
51
+ import { runnerLlmConnection } from "../../integrations/agent/runner.js";
51
52
  import { assertRunnerCredentials } from "../../integrations/agent/runner-dispatch.js";
52
53
  import { isProcessEnabled } from "../../llm/feature-gate.js";
53
54
  import { resolveIndexPassExecution } from "../../llm/index-passes.js";
@@ -283,7 +284,7 @@ async function runMemoryInferencePassBody(ctx, provenance) {
283
284
  }),
284
285
  // Caller-set connection concurrency or 1: `resolveLlmEngineUse` does
285
286
  // not forward `engines.<name>.concurrency`, so config cannot raise this.
286
- llmRunner.connection.concurrency ?? 1);
287
+ runnerLlmConnection(llmRunner)?.concurrency ?? 1);
287
288
  if (configFailure)
288
289
  throw configFailure;
289
290
  for (let i = 0; i < perRecordResults.length; i++) {
@@ -5,6 +5,21 @@
5
5
  export function resolveDispatchModel(request, _profile, _platform) {
6
6
  return request.model;
7
7
  }
8
+ /**
9
+ * The model an engine's own `args` select, as `--model X` or `--model=X` (the
10
+ * last one wins), for a builder that writes its own argv in place of those args.
11
+ */
12
+ export function modelFromArgs(args) {
13
+ let model;
14
+ for (let index = 0; index < args.length; index += 1) {
15
+ const arg = args[index];
16
+ if (arg === "--model")
17
+ model = args[index + 1];
18
+ else if (arg?.startsWith("--model="))
19
+ model = arg.slice("--model=".length);
20
+ }
21
+ return model;
22
+ }
8
23
  /**
9
24
  * Normalize a toolPolicy value to a comma-separated string suitable for a
10
25
  * CLI flag. Structured policy objects are JSON-serialized.
@@ -3,5 +3,5 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /** Default agent CLI timeout; null means agents run until they finish. */
5
5
  export const DEFAULT_AGENT_TIMEOUT_MS = null;
6
- /** Default hard timeout for direct LLM calls when no engine/use override exists. */
6
+ /** Default hard timeout for a direct LLM call, and the bound on model work on every runner kind, when no engine/use override exists. */
7
7
  export const DEFAULT_LLM_TIMEOUT_MS = 600_000;
@@ -10,10 +10,9 @@ import { SECRET_STORE_REFERENCE_PATTERN } from "../../core/config/schema/primiti
10
10
  import { ConfigError } from "../../core/errors.js";
11
11
  import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
12
12
  import { collectSensitiveValues } from "../../core/redaction.js";
13
- import { warn } from "../../core/warn.js";
14
13
  import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js";
15
14
  import { getHarness } from "../harnesses/index.js";
16
- import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
15
+ import { DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
17
16
  import { getBuiltinAgentProfile, OPENCODE_SDK_SERVER_BIN } from "./profiles.js";
18
17
  const LLM_CONNECTION_FIELDS = [
19
18
  "provider",
@@ -217,29 +216,15 @@ function rawLlmConnection(engine) {
217
216
  }
218
217
  return connection;
219
218
  }
220
- export function resolveLlmEngineUse(config, layers, options = {}) {
219
+ /** Resolve one selected LLM engine and overlays without materializing credentials. */
220
+ export function resolveLlmEngineUse(config, layers) {
221
221
  const name = selectedEngineName(config, layers, true);
222
222
  if (!name) {
223
- if (options.optional)
224
- return undefined;
225
223
  throw new ConfigError("No LLM engine is selected. Set defaults.llmEngine or specify engine.", "LLM_NOT_CONFIGURED");
226
224
  }
227
225
  const engine = configuredEngine(name, config);
228
- if (engine.kind !== "llm") {
229
- const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
230
- const fallbackEngine = fallbackName ? configuredEngine(fallbackName, config) : undefined;
231
- if (!fallbackEngine || fallbackEngine.kind !== "llm") {
232
- if (options.optional)
233
- return undefined;
234
- throw new ConfigError(fallbackName
235
- ? `Engine "${name}" is not an LLM engine, and its llmEngine fallback "${fallbackName}" is not one either.`
236
- : `Engine "${name}" is not an LLM engine, and has no llmEngine fallback configured.`, "INVALID_CONFIG_FILE");
237
- }
238
- warn(`[akm] Engine "${name}" is an agent engine, not an LLM engine; using its llmEngine "${fallbackName}" instead.`);
239
- return options.optional
240
- ? resolveLlmEngineUse(config, [{ engine: fallbackName }], { optional: true })
241
- : resolveLlmEngineUse(config, [{ engine: fallbackName }]);
242
- }
226
+ if (engine.kind !== "llm")
227
+ throw new ConfigError(`Engine "${name}" is not an LLM engine.`, "INVALID_CONFIG_FILE");
243
228
  let connection = rawLlmConnection(engine);
244
229
  for (const layer of layers) {
245
230
  if (layer.llm)
@@ -301,19 +286,16 @@ function lowerAgentEngine(name, engine, config) {
301
286
  ...(engine.workspace ? { workspace: path.resolve(engine.workspace) } : {}),
302
287
  ...(engine.model ? { model: engine.model } : {}),
303
288
  };
289
+ // An engine that sets no timeoutMs leaves it unset, so a caller's own default
290
+ // (model work's 600 s) can apply; a dispatch with none runs unbounded.
304
291
  const ownTimeout = Object.hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : undefined;
305
292
  if (!sdk) {
306
- return {
307
- kind: "agent",
308
- engine: name,
309
- profile,
310
- timeoutMs: ownTimeout !== undefined ? ownTimeout : DEFAULT_AGENT_TIMEOUT_MS,
311
- };
293
+ return { kind: "agent", engine: name, profile, ...(ownTimeout !== undefined ? { timeoutMs: ownTimeout } : {}) };
312
294
  }
313
- const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
314
- const fallback = fallbackName
315
- ? resolveLlmEngineUse(config, [{ engine: fallbackName }], { optional: true })
316
- : undefined;
295
+ // The fallback connection is the engine's own `llmEngine` and nothing else. `defaults.llmEngine`
296
+ // is the default engine for model work, not a connection every SDK engine borrows: with no
297
+ // `llmEngine`, opencode resolves provider, model and auth from its own configuration.
298
+ const fallback = engine.llmEngine ? resolveLlmEngineUse(config, [{ engine: engine.llmEngine }]) : undefined;
317
299
  return {
318
300
  kind: "sdk",
319
301
  engine: name,
@@ -327,7 +309,7 @@ function lowerAgentEngine(name, engine, config) {
327
309
  fallbackTimeoutMs: fallback.timeoutMs,
328
310
  }
329
311
  : {}),
330
- timeoutMs: ownTimeout !== undefined ? ownTimeout : (fallback?.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS),
312
+ ...(ownTimeout !== undefined ? { timeoutMs: ownTimeout } : fallback ? { timeoutMs: fallback.timeoutMs } : {}),
331
313
  };
332
314
  }
333
315
  /** Resolve a configured engine name to its runner: an LLM connection, a spawned agent, or the SDK. */