akm-cli 0.9.24 → 0.9.25-alpha.1

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 (74) hide show
  1. package/CHANGELOG.md +294 -0
  2. package/dist/commands/health/checks.js +10 -11
  3. package/dist/commands/improve/consolidate/pair-pass.js +3 -2
  4. package/dist/commands/improve/consolidate.js +3 -2
  5. package/dist/commands/improve/execution.js +4 -10
  6. package/dist/commands/improve/extract-prompt.js +4 -4
  7. package/dist/commands/improve/extract.js +10 -13
  8. package/dist/commands/improve/improve-cli.js +32 -33
  9. package/dist/commands/improve/improve-strategies.js +49 -43
  10. package/dist/commands/improve/improve-usage-report.js +8 -17
  11. package/dist/commands/improve/preparation.js +3 -1
  12. package/dist/commands/improve/reflect.js +108 -172
  13. package/dist/commands/improve/stage.js +69 -18
  14. package/dist/commands/proposal/drain.js +14 -2
  15. package/dist/commands/proposal/proposal-cli.js +1 -5
  16. package/dist/commands/proposal/propose-cli.js +2 -2
  17. package/dist/commands/proposal/propose.js +80 -83
  18. package/dist/commands/remember.js +3 -3
  19. package/dist/commands/sources/schema-repair.js +1 -1
  20. package/dist/core/config/config-schema.js +37 -60
  21. package/dist/core/config/engine-semantics.js +15 -11
  22. package/dist/core/config/schema/engines.js +33 -15
  23. package/dist/core/config/schema/improve-processes.js +2 -2
  24. package/dist/core/improve-result.js +3 -3
  25. package/dist/core/structured.js +10 -0
  26. package/dist/execution/source.js +14 -0
  27. package/dist/indexer/passes/memory-inference.js +2 -1
  28. package/dist/integrations/agent/builder-shared.js +15 -0
  29. package/dist/integrations/agent/config.js +2 -0
  30. package/dist/integrations/agent/engine-resolution.js +16 -31
  31. package/dist/integrations/agent/execution.js +46 -21
  32. package/dist/integrations/agent/index.js +1 -1
  33. package/dist/integrations/agent/model-map.js +16 -15
  34. package/dist/integrations/agent/prompts.js +53 -76
  35. package/dist/integrations/agent/request-lowering.js +20 -9
  36. package/dist/integrations/agent/runner-dispatch.js +103 -4
  37. package/dist/integrations/agent/runner.js +8 -2
  38. package/dist/integrations/harnesses/aider/agent-builder.js +11 -23
  39. package/dist/integrations/harnesses/aider/index.js +0 -5
  40. package/dist/integrations/harnesses/amazonq/agent-builder.js +10 -21
  41. package/dist/integrations/harnesses/amazonq/index.js +0 -5
  42. package/dist/integrations/harnesses/claude/agent-builder.js +61 -38
  43. package/dist/integrations/harnesses/claude/index.js +0 -14
  44. package/dist/integrations/harnesses/codex/agent-builder.js +4 -4
  45. package/dist/integrations/harnesses/codex/index.js +0 -4
  46. package/dist/integrations/harnesses/copilot/agent-builder.js +4 -13
  47. package/dist/integrations/harnesses/copilot/index.js +2 -7
  48. package/dist/integrations/harnesses/gemini/agent-builder.js +4 -13
  49. package/dist/integrations/harnesses/gemini/index.js +0 -5
  50. package/dist/integrations/harnesses/ids.js +18 -10
  51. package/dist/integrations/harnesses/opencode/agent-builder.js +52 -10
  52. package/dist/integrations/harnesses/opencode/index.js +0 -8
  53. package/dist/integrations/harnesses/opencode/model-config.js +80 -0
  54. package/dist/integrations/harnesses/opencode/model-work-agent.js +80 -0
  55. package/dist/integrations/harnesses/opencode-sdk/harness.js +6 -7
  56. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +168 -24
  57. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -21
  58. package/dist/integrations/harnesses/openhands/index.js +0 -5
  59. package/dist/integrations/harnesses/pi/agent-builder.js +8 -21
  60. package/dist/integrations/harnesses/pi/index.js +0 -5
  61. package/dist/llm/client.js +5 -0
  62. package/dist/llm/feature-gate.js +10 -4
  63. package/dist/llm/index-passes.js +3 -6
  64. package/dist/llm/memory-infer.js +6 -5
  65. package/dist/llm/structured-call.js +33 -11
  66. package/dist/scripts/akm-migrate-node.js +298 -204
  67. package/dist/scripts/akm-migrate.js +298 -204
  68. package/dist/workflows/exec/step-work.js +6 -5
  69. package/dist/workflows/freeze/step-values.js +1 -1
  70. package/docs/reference/cli.md +29 -11
  71. package/docs/reference/configuration.md +168 -11
  72. package/docs/reference/workflow-schema.md +13 -9
  73. package/package.json +1 -1
  74. package/schemas/akm-config.json +36 -0
@@ -3,14 +3,16 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { getImproveProcessConfig } from "../../core/config/config.js";
5
5
  import { ConfigError } from "../../core/errors.js";
6
+ import { validateJsonSchemaSubset } from "../../core/json-schema.js";
6
7
  import { parseEmbeddedJsonResponse } from "../../core/parse.js";
8
+ import { runStructured } from "../../core/structured.js";
7
9
  import { warn } from "../../core/warn.js";
8
- import { LlmCallError } from "../../llm/client.js";
9
- import { callStructured } from "../../llm/structured-call.js";
10
+ import { runnerLlmConnection } from "../../integrations/agent/runner.js";
11
+ import { callStructured, dispatchFailureReason, dispatchFailureResult, } from "../../llm/structured-call.js";
10
12
  import { currentLlmStage, withLlmStage } from "../../llm/usage-telemetry.js";
11
13
  import { isProceduralRejection } from "../proposal/proposal-types.js";
12
14
  import { createProposal, listProposalsReadOnly, proposalContentHash, recordGateDecision, } from "../proposal/repository.js";
13
- import { resolveImproveLlmExecution } from "./execution.js";
15
+ import { resolveImproveExecution } from "./execution.js";
14
16
  /** Normalize an unknown thrown value to a message. */
15
17
  export function errMessage(e) {
16
18
  return e instanceof Error ? e.message : String(e);
@@ -27,13 +29,13 @@ export function noticeSet(forward) {
27
29
  return { add, list, fields: () => (byKey.size > 0 ? { notices: list() } : {}) };
28
30
  }
29
31
  /**
30
- * A stage's LLM runner: the one the improve plan froze for it (an own
32
+ * A stage's runner: the one the improve plan froze for it (an own
31
33
  * `llmRunner` key, `null` meaning "none"), else the process engine cascade.
32
34
  */
33
35
  export function stageRunner(frozen, config, profile, processName, onNotices) {
34
36
  if (Object.hasOwn(frozen, "llmRunner"))
35
37
  return frozen.llmRunner ?? undefined;
36
- const resolved = resolveImproveLlmExecution({
38
+ const resolved = resolveImproveExecution({
37
39
  config,
38
40
  profile,
39
41
  process: getImproveProcessConfig(processName, profile),
@@ -43,11 +45,59 @@ export function stageRunner(frozen, config, profile, processName, onNotices) {
43
45
  onNotices?.(resolved.notices);
44
46
  return resolved?.runner;
45
47
  }
48
+ const TRANSPORT_FAILED = Symbol("stage-transport-failed");
46
49
  /**
47
- * One model call. Provider trouble (transport error, timeout, a disabled
48
- * feature) comes back as `{ ok: false }`; only a configuration failure throws.
50
+ * One model call. Provider trouble (transport error, timeout, abort, a
51
+ * disabled feature) comes back as `{ ok: false }`; only a configuration
52
+ * failure throws. A reply to a call with `request.responseSchema` that fails
53
+ * the schema gets one corrective retry. The caller's own parse still decides
54
+ * what it accepts, so the last reply comes back even when it fails the schema.
49
55
  */
50
56
  export async function callStage(call) {
57
+ const schema = call.request?.responseSchema;
58
+ if (!schema)
59
+ return callStageOnce(call);
60
+ let reply = undefined;
61
+ let failure = undefined;
62
+ try {
63
+ await runStructured({
64
+ dispatch: async (feedback) => {
65
+ const outcome = await callStageOnce(feedback ? { ...call, prompt: `${call.prompt}\n\n${feedback}` } : call);
66
+ if (!outcome.ok) {
67
+ failure = outcome;
68
+ throw TRANSPORT_FAILED;
69
+ }
70
+ reply = outcome;
71
+ return outcome.raw;
72
+ },
73
+ validate: (candidate) => {
74
+ const errors = validateJsonSchemaSubset(candidate, schema);
75
+ return errors.length === 0 ? { ok: true, value: candidate } : { ok: false, errors };
76
+ },
77
+ });
78
+ }
79
+ catch (err) {
80
+ if (err !== TRANSPORT_FAILED)
81
+ throw err;
82
+ }
83
+ // A retry that fails in transport keeps the first reply, which the caller may still accept.
84
+ return reply ?? failure ?? { ok: false, reason: "error" };
85
+ }
86
+ /** Timeout and abort come from the dispatch's own reason, whatever the runner's kind. */
87
+ function failureReason(err) {
88
+ const reason = dispatchFailureReason(err);
89
+ return reason === "timeout" || reason === "aborted" ? reason : "error";
90
+ }
91
+ /** A failed call: its reason and message, and the dispatch's own result when it reached a transport. */
92
+ function failedCall(err) {
93
+ const result = dispatchFailureResult(err);
94
+ return { ok: false, reason: failureReason(err), error: errMessage(err), ...(result ? { result } : {}) };
95
+ }
96
+ /**
97
+ * One dispatch with no validation, for a caller that parses and repairs the
98
+ * reply itself (reflect's repair turn, extract's own structured loop).
99
+ */
100
+ export async function callStageOnce(call) {
51
101
  const messages = [
52
102
  ...(call.system ? [{ role: "system", content: call.system }] : []),
53
103
  ...(call.history ?? []),
@@ -63,10 +113,11 @@ export async function callStage(call) {
63
113
  runner: call.runner,
64
114
  messages,
65
115
  ...(call.request ? { request: call.request } : {}),
116
+ ...(call.current ? { current: call.current } : {}),
66
117
  ...(call.onNotices ? { onNotices: call.onNotices } : {}),
67
118
  parse: (r) => r ?? "",
68
119
  onError: (_cls, err) => {
69
- failure = { ok: false, reason: "error", error: errMessage(err) };
120
+ failure = failedCall(err);
70
121
  return undefined;
71
122
  },
72
123
  fallback: undefined,
@@ -85,8 +136,7 @@ export async function callStage(call) {
85
136
  catch (err) {
86
137
  if (err instanceof ConfigError)
87
138
  throw err;
88
- const timedOut = err instanceof LlmCallError && err.code === "timeout";
89
- return { ok: false, reason: timedOut ? "timeout" : "error", error: errMessage(err) };
139
+ return failedCall(err);
90
140
  }
91
141
  }
92
142
  /** Attribute a stage's LLM calls to its process and planned engine (the usage report). */
@@ -162,9 +212,10 @@ export function stageJudgedProposal(stash, proposal, judged, proposalsCtx) {
162
212
  * The judge a quality gate names for itself (#1011): the gate's `engine`,
163
213
  * `model`, `timeoutMs` and `llm` over the process's own settings, as
164
214
  * `processes.triage.judgment` resolves over triage. `undefined` when the gate
165
- * is off or sets none of them, so the caller keeps its own judge. Throws when
166
- * they resolve to no LLM engine, before anything is generated: a judge must be
167
- * one, and the gate never falls back to another.
215
+ * is off or sets none of them, so the caller keeps its own judge. The engine
216
+ * may be of any kind; config validation has required one that confines the
217
+ * model-work tool policy. Throws when they resolve to no engine at all,
218
+ * before anything is generated: the gate never falls back to another judge.
168
219
  */
169
220
  export function resolveQualityGateJudge(config, profile, processName, onNotices) {
170
221
  const process = profile?.processes?.[processName];
@@ -173,7 +224,7 @@ export function resolveQualityGateJudge(config, profile, processName, onNotices)
173
224
  return undefined;
174
225
  if (!["engine", "model", "timeoutMs", "llm"].some((key) => Object.hasOwn(gate, key)))
175
226
  return undefined;
176
- const resolved = resolveImproveLlmExecution({
227
+ const resolved = resolveImproveExecution({
177
228
  config,
178
229
  processName: `${processName}-quality-judge`,
179
230
  ...(profile ? { profile } : {}),
@@ -181,7 +232,7 @@ export function resolveQualityGateJudge(config, profile, processName, onNotices)
181
232
  current: gate,
182
233
  });
183
234
  if (!resolved) {
184
- throw new ConfigError(`The ${processName} quality gate's judge must be an LLM engine. Set processes.${processName}.qualityGate.engine to one.`, "INVALID_CONFIG_FILE");
235
+ throw new ConfigError(`The ${processName} quality gate's judge has no engine. Set processes.${processName}.qualityGate.engine.`, "INVALID_CONFIG_FILE");
185
236
  }
186
237
  onNotices?.(resolved.notices);
187
238
  return resolved.runner;
@@ -349,13 +400,13 @@ function judgeResponseSchema(keys) {
349
400
  */
350
401
  async function runQualityJudge(feature, config, prompt, keys, chat, options) {
351
402
  const resolved = !options.runnerSelectionFrozen && !options.llmRunner
352
- ? resolveImproveLlmExecution({ config, processName: `${feature}-judge` })
403
+ ? resolveImproveExecution({ config, processName: `${feature}-judge` })
353
404
  : null;
354
405
  if (resolved)
355
406
  options.onNotices?.(resolved.notices);
356
407
  const runner = options.llmRunner ?? resolved?.runner;
357
408
  if (!runner)
358
- return { pass: false, score: -1, reason: "no LLM configured — cannot judge, failing closed" };
409
+ return { pass: false, score: -1, reason: "no engine configured — cannot judge, failing closed" };
359
410
  const outcome = await callStage({
360
411
  feature,
361
412
  runner,
@@ -363,7 +414,7 @@ async function runQualityJudge(feature, config, prompt, keys, chat, options) {
363
414
  prompt,
364
415
  request: {
365
416
  // Off unless the judge's own engine enables thinking (a slower, separate judge engine, #1011).
366
- enableThinking: runner.connection.enableThinking === true,
417
+ enableThinking: runnerLlmConnection(runner)?.enableThinking === true,
367
418
  temperature: 0,
368
419
  responseSchema: judgeResponseSchema(keys),
369
420
  ...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}),
@@ -25,6 +25,8 @@ import { ConfigError } from "../../core/errors.js";
25
25
  import { appendEvent } from "../../core/events.js";
26
26
  import { escapeJsonStringControls, stripCodeFences, stripThinkBlocks } from "../../core/parse.js";
27
27
  import { info, warn } from "../../core/warn.js";
28
+ import { MODEL_WORK_TOOLS } from "../../execution/source.js";
29
+ import { DEFAULT_MODEL_WORK_TIMEOUT_MS } from "../../integrations/agent/config.js";
28
30
  import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
29
31
  import { assertRunnerCredentials, runExecution, } from "../../integrations/agent/runner-dispatch.js";
30
32
  import { errMessage, noticeSet } from "../improve/stage.js";
@@ -170,7 +172,16 @@ export function parseJudgmentVerdict(raw) {
170
172
  async function dispatchJudgment(runner, prompt, seams) {
171
173
  let notices = [];
172
174
  try {
173
- const prepared = resolveExecution({ content: prompt, runner });
175
+ // Model work is bounded on every runner kind: one with no timeout of its own gets the default.
176
+ // The judgment runs under the model-work tool policy.
177
+ const prepared = resolveExecution({
178
+ content: prompt,
179
+ runner,
180
+ current: {
181
+ ...(Object.hasOwn(runner, "timeoutMs") ? {} : { timeout: DEFAULT_MODEL_WORK_TIMEOUT_MS }),
182
+ tools: MODEL_WORK_TOOLS,
183
+ },
184
+ });
174
185
  const lowered = buildExecution(prepared.request, prepared.runner);
175
186
  notices = lowered.notices;
176
187
  const chat = seams.chat;
@@ -330,10 +341,11 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
330
341
  }
331
342
  }
332
343
  if (opts.judgment && result.deferred.length > 0) {
333
- // Symbolic credentials are checked before any gate, reject or promote.
344
+ // Symbolic credentials, and the runner's model-work tool policy, are checked before any gate, reject or promote.
334
345
  const prepared = resolveExecution({
335
346
  content: "Validate the selected proposal judgment runner before mutation.",
336
347
  runner: opts.judgment,
348
+ current: { tools: MODEL_WORK_TOOLS },
337
349
  });
338
350
  assertRunnerCredentials(buildExecution(prepared.request, prepared.runner).runner);
339
351
  }
@@ -18,7 +18,7 @@ import { parsePositiveIntFlag } from "../../cli/parse-args.js";
18
18
  import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
19
19
  import { resolveStashDir } from "../../core/common.js";
20
20
  import { loadConfig } from "../../core/config/config.js";
21
- import { ConfigError, UsageError } from "../../core/errors.js";
21
+ import { UsageError } from "../../core/errors.js";
22
22
  import { installLlmUsagePersistenceIfAbsent } from "../../llm/usage-persist.js";
23
23
  import { withLlmStage } from "../../llm/usage-telemetry.js";
24
24
  import { resolveImproveExecution } from "../improve/execution.js";
@@ -470,10 +470,6 @@ const proposalDrainCommand = defineJsonCommand({
470
470
  })
471
471
  : null;
472
472
  const judgment = judgmentResolution?.runner ?? null;
473
- const effectiveJudgmentLlm = triageConfig?.judgment?.llm ?? triageConfig?.llm ?? selectedStrategy.config.llm;
474
- if (judgment && judgment.kind !== "llm" && effectiveJudgmentLlm) {
475
- throw new ConfigError(`Triage judgment engine "${judgment.engine ?? "unknown"}" is an agent engine and cannot receive llm overrides.`, "INVALID_CONFIG_FILE");
476
- }
477
473
  // #576: persist + attribute per-call LLM usage for the standalone drain
478
474
  // path. `IfAbsent` keeps an enclosing `akm improve` sink in charge when
479
475
  // drain runs as a sub-step; the disposer clears only a sink we installed.
@@ -26,7 +26,7 @@ const EXIT_GENERAL = EXIT_CODES.GENERAL;
26
26
  export const proposeCommand = defineCommand({
27
27
  meta: {
28
28
  name: "new",
29
- description: "Ask the configured agent CLI to author a brand-new asset and queue it as a proposal",
29
+ description: "Ask the configured engine to author a brand-new asset as JSON and queue it as a proposal",
30
30
  },
31
31
  // Raw defineCommand: declare the global output flags so their space-separated
32
32
  // values are consumed rather than shifting the `type` / `name` positionals.
@@ -48,7 +48,7 @@ export const proposeCommand = defineCommand({
48
48
  task: { type: "string", description: "Task description for the agent (what should the asset do?)" },
49
49
  file: { type: "string", description: "Read the task or prompt text from a UTF-8 file" },
50
50
  engine: { type: "string", description: "Engine to use (defaults to defaults.engine)" },
51
- "timeout-ms": { type: "string", description: "Override the agent CLI timeout in milliseconds" },
51
+ "timeout-ms": { type: "string", description: "Override the engine timeout in milliseconds" },
52
52
  },
53
53
  async run({ args }) {
54
54
  await runWithJsonErrors(async () => {
@@ -2,17 +2,17 @@
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
4
  /**
5
- * `akm propose <type> <name> --task ...` — proposal-producing agent
6
- * command (#226).
5
+ * `akm proposal new <type> <name> --task ...` — proposal-producing command
6
+ * (#226).
7
7
  *
8
- * Mirrors {@link akmReflect} but for fresh authoring. The agent receives a
9
- * task description plus per-asset-type schema hints and is asked to author
10
- * a brand-new asset payload. The output lands ONLY in the proposal queue.
8
+ * Mirrors {@link akmReflect} but for fresh authoring. The engine, of any kind,
9
+ * receives a task description plus per-asset-type schema hints and returns a
10
+ * brand-new asset payload as JSON on stdout. The output lands ONLY in the
11
+ * proposal queue.
11
12
  *
12
13
  * Failures use the same {@link AgentFailureReason} discriminants as
13
14
  * `akm reflect`. `propose_invoked` is emitted at command entry.
14
15
  */
15
- import fs from "node:fs";
16
16
  import { placementTypes, stashDirFor } from "../../core/asset/asset-placement.js";
17
17
  import { parseRefInput } from "../../core/asset/resolve-ref.js";
18
18
  import { resolveStashDir } from "../../core/common.js";
@@ -21,12 +21,14 @@ import { UsageError } from "../../core/errors.js";
21
21
  import { appendEvent } from "../../core/events.js";
22
22
  import { redactSensitiveText } from "../../core/redaction.js";
23
23
  import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
24
+ import { runStructured } from "../../core/structured.js";
24
25
  import { warn } from "../../core/warn.js";
25
26
  import { deriveEntryProvenance } from "../../indexer/installations.js";
26
27
  import { fallbackAnnouncement } from "../../integrations/agent/engine-fallback.js";
27
28
  import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
28
- import { buildProposePrompt, parseAgentProposalPayload } from "../../integrations/agent/prompts.js";
29
+ import { buildProposePrompt, PROPOSAL_JSON_SCHEMA, validateProposalPayload, } from "../../integrations/agent/prompts.js";
29
30
  import { assertRunnerCredentials, collectDispatchSensitiveValues, runExecution, } from "../../integrations/agent/runner-dispatch.js";
31
+ import { getHarness } from "../../integrations/harnesses/index.js";
30
32
  import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
31
33
  import { createProposal, resolveProposalQueueTarget, } from "./repository.js";
32
34
  function failureEnvelope(result, type, name, engine, notices, fallbackReason = "non_zero_exit") {
@@ -42,42 +44,71 @@ function failureEnvelope(result, type, name, engine, notices, fallbackReason = "
42
44
  function noticeFields(notices) {
43
45
  return notices.length > 0 ? { notices } : {};
44
46
  }
45
- /** Resolve, lower, and dispatch the already-rendered proposal prompt. */
47
+ const DISPATCH_FAILED = Symbol("proposal-dispatch-failed");
48
+ /** A reply's text, unwrapped from its harness's framing (claude's `--output-format json` envelope). */
49
+ function replyText(execution, result) {
50
+ const runner = execution.runner;
51
+ if (runner.kind !== "agent")
52
+ return result.stdout;
53
+ const extractor = getHarness(runner.profile.platform ?? runner.profile.name)?.resultExtractor;
54
+ return extractor ? extractor(result).text : result.stdout;
55
+ }
56
+ /**
57
+ * Resolve, lower, and dispatch the already-rendered proposal prompt with the
58
+ * proposal's JSON Schema as its output schema, and capture the reply. A reply
59
+ * that is not a proposal gets one corrective retry.
60
+ */
46
61
  async function dispatchProposalPrompt(prompt, config, options, onDispatchReady) {
47
62
  const current = {
48
63
  ...(options.engine !== undefined ? { engine: options.engine } : {}),
49
64
  ...(options.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
65
+ outputSchema: PROPOSAL_JSON_SCHEMA,
50
66
  };
51
- const prepared = resolveExecution({
52
- content: prompt,
53
- config,
54
- ...(Object.keys(current).length > 0 ? { current } : {}),
55
- });
67
+ const lower = (content) => {
68
+ const prepared = resolveExecution({ content, config, current });
69
+ return { prepared, lowered: buildExecution(prepared.request, prepared.runner) };
70
+ };
71
+ const { prepared, lowered } = lower(prompt);
56
72
  const engineName = prepared.request.engine.name;
57
73
  const announcement = fallbackAnnouncement(prepared.fallbackEngineName, engineName);
58
74
  if (announcement)
59
75
  warn(announcement);
60
- const lowered = buildExecution(prepared.request, prepared.runner);
61
- const interactive = !options.runAgentOptions?.spawn;
62
- const runOptions = {
63
- stdio: interactive ? "interactive" : "captured",
64
- parseOutput: "text",
65
- ...(options.runAgentOptions ?? {}),
66
- };
67
76
  // Validate every required symbolic credential before the entry event opens
68
77
  // durable state. Provider/runtime failures still occur after the event,
69
- // preserving the command-attempt observability contract.
70
- assertRunnerCredentials(lowered.runner, runOptions.envSource);
78
+ // preserving the command-attempt observability contract. The dispatch reads
79
+ // the same caller environment.
80
+ const envSource = options.runAgentOptions?.envSource;
81
+ assertRunnerCredentials(lowered.runner, envSource);
71
82
  onDispatchReady();
72
83
  options.onDispatchReady?.();
73
- const result = await runExecution(lowered, { runOptions });
84
+ // runStructured dispatches at least once, so a returned or DISPATCH_FAILED exchange has a result.
85
+ const results = [];
86
+ let reply;
87
+ try {
88
+ reply = await runStructured({
89
+ dispatch: async (feedback) => {
90
+ const execution = feedback ? lower(`${prompt}\n\n${feedback}`).lowered : lowered;
91
+ const result = await runExecution(execution, { runOptions: options.runAgentOptions ?? {} });
92
+ results.push(result);
93
+ if (!result.ok)
94
+ throw DISPATCH_FAILED;
95
+ return replyText(execution, result);
96
+ },
97
+ validate: validateProposalPayload,
98
+ });
99
+ }
100
+ catch (err) {
101
+ if (err !== DISPATCH_FAILED)
102
+ throw err;
103
+ }
74
104
  return {
75
- result,
105
+ result: results.at(-1),
106
+ ...(reply ? { reply } : {}),
107
+ durationMs: results.reduce((total, result) => total + result.durationMs, 0),
76
108
  engineName,
77
109
  ...(lowered.runner.kind === "llm" ? {} : { engineBin: lowered.runner.profile.bin }),
78
110
  notices: lowered.notices,
79
- sensitiveValues: collectDispatchSensitiveValues(lowered.runner, {}, runOptions.envSource),
80
- interactive,
111
+ sensitiveValues: collectDispatchSensitiveValues(lowered.runner, {}, envSource),
81
112
  };
82
113
  }
83
114
  /**
@@ -128,10 +159,6 @@ export async function akmPropose(options) {
128
159
  const config = options.agentConfig ?? (await import("../../core/config/config.js")).loadConfig();
129
160
  const target = resolveProposalQueueTarget(stash, config);
130
161
  // 2. Build terminal user content.
131
- // Synthesize a temp draft path so opencode can write the asset content
132
- // directly using its file tools rather than returning JSON via stdout.
133
- const draftFilePath = import("node:os").then((os) => import("node:path").then((path) => path.join(os.tmpdir(), `akm-propose-${options.type}-${options.name.replace(/[^a-z0-9_-]/gi, "_")}-${Date.now()}.md`)));
134
- const resolvedDraftPath = await draftFilePath;
135
162
  // Standards "rulebook" for this target — wiki schema (wiki page) or stash
136
163
  // convention/meta facts (non-wiki asset); empty when neither fires.
137
164
  const standardsContext = resolveStandardsContext(`${options.type}:${options.name}`, stash);
@@ -140,14 +167,14 @@ export async function akmPropose(options) {
140
167
  name: options.name,
141
168
  task: options.task,
142
169
  ...(standardsContext.trim() ? { standardsContext } : {}),
143
- draftFilePath: resolvedDraftPath,
144
170
  });
145
- // 3. Preserve the fully-authored prompt as the terminal user content;
146
- // no synthetic persona, conversation turn, schema, or tool selection is
147
- // introduced while it crosses the shared resolved/lowered boundary.
171
+ // 3. Preserve the fully-authored prompt as the terminal user content; the
172
+ // proposal's JSON Schema crosses the shared resolved/lowered boundary as the
173
+ // request's output schema, with no synthetic persona, conversation turn, or
174
+ // tool selection.
148
175
  const dispatch = await dispatchProposalPrompt(prompt, config, options, () => emitProposeInvoked(target.source, options));
149
- const { result, engineName, notices, sensitiveValues } = dispatch;
150
- if (!result.ok) {
176
+ const { result, reply, engineName, notices, sensitiveValues } = dispatch;
177
+ if (!reply) {
151
178
  // B3: ENOENT / not-found gives an actionable hint.
152
179
  if (isEnoentFailure(result)) {
153
180
  return {
@@ -157,54 +184,23 @@ export async function akmPropose(options) {
157
184
  }
158
185
  return failureEnvelope(result, options.type, options.name, engineName, notices);
159
186
  }
160
- // 5. Resolve the proposal content.
161
- // Path A: opencode wrote the draft file — read it directly (no stdout parse).
162
- // Path B: fallback to stdout JSON parse for non-file-writing agents.
163
- let payload;
164
- if (fs.existsSync(resolvedDraftPath)) {
165
- const draftContent = fs.readFileSync(resolvedDraftPath, "utf8");
166
- fs.unlinkSync(resolvedDraftPath);
167
- payload = {
168
- ref: proposeItemRef(target.source, options.type, options.name),
169
- content: draftContent,
187
+ // 5. The proposal the engine returned on stdout, validated.
188
+ if (!reply.ok) {
189
+ return {
190
+ schemaVersion: 2,
191
+ ok: false,
192
+ reason: "parse_error",
193
+ error: `Engine "${engineName}" reply was not valid proposal JSON after ${reply.attempts} attempts: ${reply.errors.join("; ")}`,
194
+ type: options.type,
195
+ name: options.name,
196
+ engine: engineName,
197
+ exitCode: result.exitCode,
198
+ stdout: result.stdout,
199
+ ...(result.stderr ? { stderr: result.stderr } : {}),
200
+ ...noticeFields(notices),
170
201
  };
171
202
  }
172
- else {
173
- // B1: When interactive mode was used and stdout is empty, the agent did not
174
- // write the draft file and stdout was not captured — surface an actionable error.
175
- if (dispatch.interactive && (result.stdout ?? "") === "") {
176
- return {
177
- schemaVersion: 2,
178
- ok: false,
179
- reason: "parse_error",
180
- error: "Agent did not write draft file and stdout was not captured (interactive mode). Check that the agent CLI understood the file-write instruction, or configure a headless profile with stdio: 'captured'.",
181
- type: options.type,
182
- name: options.name,
183
- engine: engineName,
184
- exitCode: result.exitCode,
185
- ...(result.stderr ? { stderr: result.stderr } : {}),
186
- ...noticeFields(notices),
187
- };
188
- }
189
- try {
190
- payload = parseAgentProposalPayload(result.stdout ?? "");
191
- }
192
- catch (err) {
193
- return {
194
- schemaVersion: 2,
195
- ok: false,
196
- reason: "parse_error",
197
- error: err instanceof Error ? err.message : String(err),
198
- type: options.type,
199
- name: options.name,
200
- engine: engineName,
201
- exitCode: result.exitCode,
202
- stdout: result.stdout,
203
- ...(result.stderr ? { stderr: result.stderr } : {}),
204
- ...noticeFields(notices),
205
- };
206
- }
207
- }
203
+ const payload = reply.value;
208
204
  const unsafeContent = generatedContentRejection(payload.content, redactSensitiveText(payload.content, sensitiveValues));
209
205
  if (unsafeContent) {
210
206
  return {
@@ -270,6 +266,7 @@ export async function akmPropose(options) {
270
266
  content: payload.content,
271
267
  ...(payload.frontmatter ? { frontmatter: payload.frontmatter } : {}),
272
268
  },
269
+ ...(payload.confidence !== undefined ? { confidence: payload.confidence } : {}),
273
270
  };
274
271
  const proposal = createProposal(stash, createInput, options.ctx);
275
272
  return {
@@ -278,7 +275,7 @@ export async function akmPropose(options) {
278
275
  proposal,
279
276
  ref: proposal.ref,
280
277
  engine: engineName,
281
- durationMs: result.durationMs,
278
+ durationMs: dispatch.durationMs,
282
279
  ...noticeFields(notices),
283
280
  };
284
281
  }
@@ -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
  }