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
@@ -30,11 +30,6 @@ export class AmazonqHarness extends BaseHarness {
30
30
  agentBuilder = amazonqBuilder;
31
31
  resultExtractor = amazonqResultExtractor;
32
32
  // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
33
- // akm spawns `q chat` locally per unit ⇒ local-runner.
34
- pattern = "local-runner";
35
- // No documented structured output: akm injects the schema into the prompt
36
- // and extracts embedded JSON from plain-text stdout.
37
- structuredOutput = "none";
38
33
  // No `identityEnv`: the matrix lists Q's identity markers as uncertain, and
39
34
  // Q stamps no session var onto child processes.
40
35
  capabilities = caps({
@@ -11,51 +11,64 @@
11
11
  * in `agent/builders.ts`, which imports this builder back into
12
12
  * `BUILTIN_BUILDERS`.
13
13
  *
14
- * ## Structured output (Codex round-3 finding A)
14
+ * ## Structured output
15
15
  *
16
- * The headless `claude -p` (`--print`) CLI has NO native output-SCHEMA flag
17
- * (unlike Codex's `--output-schema <file>`). Its documented structured path is
18
- * `--output-format json`, which wraps the run in a RESULT ENVELOPE
19
- * (`{"type":"result","result":"<final answer>","session_id":"…", …}`) — the
20
- * "native-json" tier, NOT "native-schema". (The registry's earlier
21
- * `native-schema` claim described Claude Code's IN-HARNESS `Workflow`/`agent()`
22
- * tool-input-schema path, which is a different execution surface than the
23
- * agentBuilder dispatch akm's local-runner uses; the descriptor is aligned to
24
- * `native-json` to match this builder honestly.)
16
+ * For a schema-bearing request this builder emits `--output-format json`,
17
+ * which wraps the run in a RESULT ENVELOPE
18
+ * (`{"type":"result","result":"<final answer>","session_id":"…", …}`); the
19
+ * shared request lowering has already appended the schema instruction to the
20
+ * prompt. The envelope is unwrapped by `./result-extractor.ts`, and the
21
+ * engine's shared `runStructured` retry-until-valid loop validates the
22
+ * extracted text against the schema (hinted output is trusted but verified).
23
+ * Without a schema the argv carries no output flag.
25
24
  *
26
- * So for a schema-bearing unit this builder emits `--output-format json` and
27
- * appends the SAME schema directive the engine's prompt assembly uses
28
- * (`step-work.ts` `buildUnitPrompt`) so a direct (non-workflow) dispatch is
29
- * self-sufficient — matching the copilot/gemini native-json builders. The
30
- * result envelope is unwrapped by `./result-extractor.ts`, and the engine's
31
- * shared `runStructured` retry-until-valid loop still validates the extracted
32
- * text against the node schema (constrained/hinted output is trusted but
33
- * verified). Without a schema the argv is byte-identical to the pre-fix shape.
25
+ * Claude Code 2.1.283 also has `--json-schema <schema>` (JSON Schema for
26
+ * structured output validation, with `--print`). akm does not pass it; the
27
+ * instruction and the validation loop above are what enforce a schema.
34
28
  *
35
29
  * The builder's `platform` stays `'claude'` (the canonical harness id).
36
30
  */
37
- import { normalizeTools, resolveDispatchModel, } from "../../agent/builder-shared.js";
31
+ import { isModelWorkTools } from "../../../execution/source.js";
32
+ import { modelFromArgs, normalizeTools, resolveDispatchModel, } from "../../agent/builder-shared.js";
38
33
  import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
39
- /**
40
- * Assemble the positional prompt: the task prompt and — when a schema is
41
- * requested — the same schema directive the workflow engine's prompt assembly
42
- * uses (`step-work.ts` `buildUnitPrompt`), so both dispatch paths speak one
43
- * dialect. Claude Code takes the system prompt as a `--system-prompt` FLAG (it
44
- * has one, unlike copilot/gemini), so only the schema directive is folded in
45
- * here.
46
- */
47
- function buildPromptPayload(req) {
48
- if (!req.schema)
49
- return req.prompt;
50
- return `${req.prompt}\n\nRespond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`;
51
- }
34
+ /** The model-work tool policy on Claude Code: read, edit in the working directory, `akm search`, `akm show`. */
35
+ export const MODEL_WORK_CLAUDE_FLAGS = Object.freeze([
36
+ "--restricted",
37
+ "--strict-mcp-config",
38
+ "--tools",
39
+ "Read,Edit,Bash",
40
+ "--allowedTools",
41
+ "Read,Edit,Bash(akm search *),Bash(akm show *)",
42
+ "--permission-mode",
43
+ "dontAsk",
44
+ ]);
52
45
  /**
53
46
  * Claude Code builder.
54
47
  * Command shape:
55
- * claude [--agent <name>] [--system-prompt "..."] [--model <m>] [--allowedTools <t>]
56
- * [--output-format json] --print -- "<prompt (+ schema directive)>"
48
+ * claude [--agent <name>] [--system-prompt "..."] [--model <m>] [--effort <level>]
49
+ * [--allowedTools <t>] [--output-format json] --print -- "<prompt>"
57
50
  *
58
51
  * --print switches Claude Code to non-interactive captured output mode.
52
+ *
53
+ * `--effort` carries the request's `reasoningEffort` as the harness's own
54
+ * level (`low`, `medium`, `high`, `xhigh` or `max` in 2.1.283), passed through
55
+ * as given: a value Claude Code rejects fails the dispatch. It is the only
56
+ * inference field Claude Code takes on its command line, so the others are
57
+ * reported as untranslated.
58
+ *
59
+ * The model-work tool policy lowers to {@link MODEL_WORK_CLAUDE_FLAGS} in place
60
+ * of the engine's `args` and `--allowedTools`, verified against Claude Code
61
+ * 2.1.283 and a local stub:
62
+ * - `--restricted` ignores the user, project and local settings files (whose
63
+ * allow rules would otherwise pre-approve any command or path) and confines
64
+ * the file tools to the working directory;
65
+ * - `--strict-mcp-config` starts no MCP server, and `--tools` offers only
66
+ * Read, Edit and Bash;
67
+ * - `--allowedTools` pre-approves Read, Edit, `akm search` and `akm show`,
68
+ * and `--permission-mode dontAsk` denies everything else instead of
69
+ * prompting, including a compound, redirected or substituted command.
70
+ * The engine's `args` are left out because `--add-dir`, `--settings` or a
71
+ * second `--allowedTools` would widen that. Only the model they name is kept.
59
72
  */
60
73
  export const claudeBuilder = {
61
74
  platform: "claude",
@@ -65,10 +78,12 @@ export const claudeBuilder = {
65
78
  personaChannel: "native",
66
79
  nativeAgentSelector: true,
67
80
  tools: "all",
68
- outputSchema: true,
81
+ modelWorkTools: true,
82
+ inference: ["reasoningEffort"],
69
83
  }),
70
84
  build(profile, req) {
71
- const args = [...profile.args];
85
+ const modelWork = isModelWorkTools(req.tools);
86
+ const args = modelWork ? [...MODEL_WORK_CLAUDE_FLAGS] : [...profile.args];
72
87
  if (req.agent) {
73
88
  args.push("--agent", req.agent);
74
89
  }
@@ -79,7 +94,15 @@ export const claudeBuilder = {
79
94
  const resolved = resolveDispatchModel(req, profile, "claude");
80
95
  args.push("--model", resolved);
81
96
  }
82
- if (req.tools) {
97
+ else if (modelWork) {
98
+ const model = modelFromArgs(profile.args);
99
+ if (model)
100
+ args.push("--model", model);
101
+ }
102
+ const effort = req.inference?.reasoningEffort;
103
+ if (typeof effort === "string" && effort.length > 0)
104
+ args.push("--effort", effort);
105
+ if (req.tools && !modelWork) {
83
106
  args.push("--allowedTools", normalizeTools(req.tools));
84
107
  }
85
108
  if (req.schema) {
@@ -90,7 +113,7 @@ export const claudeBuilder = {
90
113
  // --print = non-interactive, outputs to stdout — required for captured mode
91
114
  args.push("--print");
92
115
  args.push("--");
93
- args.push(buildPromptPayload(req));
116
+ args.push(req.prompt);
94
117
  return { argv: [profile.bin, ...args] };
95
118
  },
96
119
  };
@@ -24,20 +24,6 @@ export class ClaudeHarness extends BaseHarness {
24
24
  agentBuilder = claudeBuilder;
25
25
  resultExtractor = claudeResultExtractor;
26
26
  // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
27
- // Claude Code is the in-harness pattern: the orchestrating session itself
28
- // drives units via the `akm workflow` gate spine (`claude -p` headless
29
- // dispatch also exists via `agentBuilder`, but the pattern classification
30
- // follows the matrix row).
31
- pattern = "in-harness";
32
- // Structured output tier for the AGENT-DISPATCH (`claude -p`) path akm's
33
- // local runner uses (Codex round-3 finding A). The headless CLI has NO
34
- // output-schema flag — its documented structured path is `--output-format
35
- // json`, a RESULT ENVELOPE akm parses (`./result-extractor.ts`) and then
36
- // validates against the node schema ⇒ the "native-json" tier. (Claude Code's
37
- // in-harness `Workflow`/`agent()` tool-input-schema path IS native-schema,
38
- // but that is a different surface than the dispatch builder — the descriptor
39
- // is aligned to what the builder honestly does.)
40
- structuredOutput = "native-json";
41
27
  // Session-id env marker: presence of a concrete session id (not the bare
42
28
  // "running under Claude Code" flag) attributes a run to this harness.
43
29
  identityEnv = ["CLAUDE_SESSION_ID"];
@@ -42,9 +42,10 @@
42
42
  * not expressible through `AgentDispatchRequest` (which has no session
43
43
  * field yet); {@link codexResumeArgs} exposes the argv prefix for the
44
44
  * integration task that wires session-id reuse from `workflow_run_units`.
45
- * - `req.effort` stays unconsumed (reserved; codex would take it as
46
- * `-c model_reasoning_effort=<v>` — left to the integration task so the
47
- * shared request contract's "no builder consumes it yet" note stays true).
45
+ * - The request's inference is not translated: the shared lowering reports
46
+ * each field as untranslated (`inference` in `harnesses/ids.ts` lists none
47
+ * for codex). codex would take `reasoningEffort` as
48
+ * `-c model_reasoning_effort=<v>`, which is left to the integration task.
48
49
  *
49
50
  * Registered: `codexBuilder` is `CodexHarness.agentBuilder` (`./index.ts`),
50
51
  * one of the ten harnesses `HARNESS_REGISTRY` constructs
@@ -110,7 +111,6 @@ export const codexBuilder = {
110
111
  adapter: "codex",
111
112
  personaChannel: "prompt",
112
113
  tools: "none",
113
- outputSchema: true,
114
114
  }),
115
115
  build(profile, req) {
116
116
  // Built-in codex profiles ship `args: []`; headless dispatch is the `exec`
@@ -29,10 +29,6 @@ export class CodexHarness extends BaseHarness {
29
29
  agentBuilder = codexBuilder;
30
30
  resultExtractor = codexResultExtractor;
31
31
  // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
32
- // akm spawns `codex exec` locally per unit ⇒ local-runner.
33
- pattern = "local-runner";
34
- // `--output-schema <file>` enforces a caller-supplied JSON schema natively.
35
- structuredOutput = "native-schema";
36
32
  // No flag-shaped resume: codex resume is the `exec resume <id>` SUBCOMMAND
37
33
  // chain (see `codexResumeArgs` in ./agent-builder.ts).
38
34
  // Presence flag: CODEX_SANDBOX is stamped only on processes codex itself
@@ -24,9 +24,8 @@
24
24
  * blank line.
25
25
  * - **schema** — the matrix places Copilot in the "via prompt+validate" tier
26
26
  * (no native `--output-schema` equivalent, unlike Codex), so the JSON
27
- * Schema is passed through the prompt: a directive matching the engine's
28
- * wording (`step-work.ts` `buildUnitPrompt`) is appended to the `-p`
29
- * payload, and `--output-format json` is emitted so stdout is the
27
+ * Schema reaches it as the instruction the shared request lowering appends
28
+ * to the prompt, and `--output-format json` is emitted so stdout is the
30
29
  * documented JSON envelope the copilot result extractor normalizes. The
31
30
  * engine's shared retry-until-valid loop performs the actual validation.
32
31
  * - **tools** — a string/array tool policy maps to repeated
@@ -66,26 +65,19 @@ function toolPolicyEntries(tools) {
66
65
  }
67
66
  return undefined;
68
67
  }
69
- /**
70
- * Assemble the `-p` payload: optional system prompt, the task prompt, and —
71
- * when a schema is requested — the same schema directive the workflow
72
- * engine's prompt assembly uses, so both dispatch paths speak one dialect.
73
- */
68
+ /** Assemble the `-p` payload: optional system prompt, then the task prompt. */
74
69
  function buildPromptPayload(req) {
75
70
  const sections = [];
76
71
  if (req.systemPrompt)
77
72
  sections.push(req.systemPrompt);
78
73
  sections.push(req.prompt);
79
- if (req.schema) {
80
- sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
81
- }
82
74
  return sections.join("\n\n");
83
75
  }
84
76
  /**
85
77
  * GitHub Copilot CLI builder.
86
78
  * Command shape:
87
79
  * copilot [--model <m>] (--allow-all-tools | --allow-tool <t> ...)
88
- * [--output-format json] -p "<systemPrompt?\n\nprompt\n\nschema?>"
80
+ * [--output-format json] -p "<systemPrompt?\n\nprompt>"
89
81
  */
90
82
  export const copilotBuilder = {
91
83
  platform: COPILOT_PLATFORM,
@@ -94,7 +86,6 @@ export const copilotBuilder = {
94
86
  adapter: COPILOT_PLATFORM,
95
87
  personaChannel: "prompt",
96
88
  tools: "flat",
97
- outputSchema: true,
98
89
  }),
99
90
  build(profile, req) {
100
91
  const args = [...profile.args];
@@ -10,8 +10,8 @@
10
10
  *
11
11
  * It also defines {@link CopilotHarness}, the {@link AkmHarness} descriptor
12
12
  * that `HARNESS_REGISTRY` registers. This is the LOCAL Copilot CLI
13
- * (`copilot -p …`); the cloud "Copilot coding agent" is the plan's
14
- * cloud-delegate pattern and is a separate, future descriptor.
13
+ * (`copilot -p …`); the cloud "Copilot coding agent" is a separate, future
14
+ * descriptor.
15
15
  */
16
16
  import { caps } from "../shared.js";
17
17
  import { BaseHarness } from "../types.js";
@@ -30,11 +30,6 @@ export class CopilotHarness extends BaseHarness {
30
30
  agentBuilder = copilotBuilder;
31
31
  resultExtractor = copilotResultExtractor;
32
32
  // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
33
- // akm spawns the `copilot` CLI locally per unit ⇒ local-runner.
34
- pattern = "local-runner";
35
- // `--output-format json` emits a documented JSON envelope akm parses, then
36
- // validates against the node schema ⇒ native-json tier.
37
- structuredOutput = "native-json";
38
33
  // Session-id env marker only. The matrix's other candidates (GH_TOKEN,
39
34
  // bare COPILOT_* presence vars) are credential/presence flags that would
40
35
  // stamp identity onto manual runs, so they are deliberately NOT registered
@@ -26,9 +26,8 @@
26
26
  * prompt, separated by a blank line.
27
27
  * - **schema** — the matrix places Gemini in the "via prompt+validate" tier
28
28
  * (no native `--output-schema` equivalent, unlike Codex — so no temp-file
29
- * plumbing here), so the JSON Schema is passed through the prompt: a
30
- * directive matching the engine's wording (`step-work.ts`
31
- * `buildUnitPrompt`) is appended to the `-p` payload, and
29
+ * plumbing here), so the JSON Schema reaches it as the instruction the
30
+ * shared request lowering appends to the prompt, and
32
31
  * `--output-format json` is emitted so stdout is the documented JSON
33
32
  * envelope the gemini result extractor normalizes. The engine's shared
34
33
  * retry-until-valid loop performs the actual validation.
@@ -69,26 +68,19 @@ function toolPolicyEntries(tools) {
69
68
  }
70
69
  return undefined;
71
70
  }
72
- /**
73
- * Assemble the `-p` payload: optional system prompt, the task prompt, and —
74
- * when a schema is requested — the same schema directive the workflow
75
- * engine's prompt assembly uses, so both dispatch paths speak one dialect.
76
- */
71
+ /** Assemble the `-p` payload: optional system prompt, then the task prompt. */
77
72
  function buildPromptPayload(req) {
78
73
  const sections = [];
79
74
  if (req.systemPrompt)
80
75
  sections.push(req.systemPrompt);
81
76
  sections.push(req.prompt);
82
- if (req.schema) {
83
- sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
84
- }
85
77
  return sections.join("\n\n");
86
78
  }
87
79
  /**
88
80
  * Gemini CLI builder.
89
81
  * Command shape:
90
82
  * gemini [--model <m>] [--allowed-tools <t> ...]
91
- * [--output-format json] -p "<systemPrompt?\n\nprompt\n\nschema?>"
83
+ * [--output-format json] -p "<systemPrompt?\n\nprompt>"
92
84
  */
93
85
  export const geminiBuilder = {
94
86
  platform: GEMINI_PLATFORM,
@@ -97,7 +89,6 @@ export const geminiBuilder = {
97
89
  adapter: GEMINI_PLATFORM,
98
90
  personaChannel: "prompt",
99
91
  tools: "flat",
100
- outputSchema: true,
101
92
  }),
102
93
  build(profile, req) {
103
94
  const args = [...profile.args];
@@ -29,11 +29,6 @@ export class GeminiHarness extends BaseHarness {
29
29
  agentBuilder = geminiBuilder;
30
30
  resultExtractor = geminiResultExtractor;
31
31
  // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
32
- // akm spawns the `gemini` CLI locally per unit ⇒ local-runner.
33
- pattern = "local-runner";
34
- // `--output-format json` emits a documented JSON envelope akm parses, then
35
- // validates against the node schema ⇒ native-json tier.
36
- structuredOutput = "native-json";
37
32
  // The matrix's identity marker: Gemini CLI stamps GEMINI_CLI=1 only on
38
33
  // processes it spawns, so it genuinely means "running under gemini" (it is
39
34
  // not a user-profile config var) — but its VALUE ("1") is a bare flag, not
@@ -1,17 +1,19 @@
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
+ /** What opencode carries on a model; see `harnesses/opencode/model-config.ts`. */
5
+ const OPENCODE_INFERENCE = ["temperature", "maxTokens", "contextLength", "enableThinking", "reasoningEffort"];
4
6
  export const HARNESS_ID_TABLE = [
5
- { id: "opencode", agentDispatch: true },
6
- { id: "claude", agentDispatch: true },
7
- { id: "opencode-sdk", agentDispatch: true },
8
- { id: "codex", agentDispatch: true },
9
- { id: "copilot", agentDispatch: true },
10
- { id: "pi", agentDispatch: true },
11
- { id: "gemini", agentDispatch: true },
12
- { id: "aider", agentDispatch: true },
13
- { id: "amazonq", agentDispatch: true },
14
- { id: "openhands", agentDispatch: true },
7
+ { id: "opencode", agentDispatch: true, enforcesModelWorkTools: true, inference: OPENCODE_INFERENCE },
8
+ { id: "claude", agentDispatch: true, enforcesModelWorkTools: true, inference: ["reasoningEffort"] },
9
+ { id: "opencode-sdk", agentDispatch: true, enforcesModelWorkTools: true, inference: OPENCODE_INFERENCE },
10
+ { id: "codex", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
11
+ { id: "copilot", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
12
+ { id: "pi", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
13
+ { id: "gemini", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
14
+ { id: "aider", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
15
+ { id: "amazonq", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
16
+ { id: "openhands", agentDispatch: true, enforcesModelWorkTools: false, inference: [] },
15
17
  ];
16
18
  /**
17
19
  * Canonical, ordered list of valid harness / platform ids — the
@@ -22,3 +24,9 @@ export const HARNESS_ID_TABLE = [
22
24
  export const VALID_HARNESS_IDS = Object.freeze(HARNESS_ID_TABLE.map((h) => h.id));
23
25
  /** Harness ids whose `capabilities.agentDispatch` is `true`. */
24
26
  export const HARNESS_AGENT_DISPATCH_IDS = new Set(HARNESS_ID_TABLE.filter((h) => h.agentDispatch).map((h) => h.id));
27
+ /** Harness ids that confine the model-work tool policy, so unattended model work may run on them. */
28
+ export const HARNESS_MODEL_WORK_IDS = new Set(HARNESS_ID_TABLE.filter((h) => h.enforcesModelWorkTools).map((h) => h.id));
29
+ /** The inference keys an agent engine on `platform` may set; none for a platform that is not registered. */
30
+ export function harnessInferenceKeys(platform) {
31
+ return HARNESS_ID_TABLE.find((h) => h.id === platform)?.inference ?? [];
32
+ }
@@ -14,26 +14,68 @@
14
14
  * pre-migration `opencodeBuilder`. The builder's `platform` stays `'opencode'`
15
15
  * (the canonical harness id).
16
16
  */
17
- import { resolveDispatchModel } from "../../agent/builder-shared.js";
17
+ import { isModelWorkTools } from "../../../execution/source.js";
18
+ import { modelFromArgs, resolveDispatchModel } from "../../agent/builder-shared.js";
18
19
  import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
20
+ import { opencodeCarriedKeys, opencodeInferenceConfig, opencodeModelConfig, splitOpencodeModel } from "./model-config.js";
21
+ import { MODEL_WORK_OPENCODE_AGENT, modelWorkOpencodeConfig } from "./model-work-agent.js";
19
22
  /**
20
23
  * OpenCode builder.
21
- * Command shape: opencode run [--system-prompt "..."] [--agent <name>] [--model <m>] "<prompt>"
24
+ * Command shape: opencode run [--agent <name>] [--model <m>] -- "<prompt>"
25
+ *
26
+ * `opencode run` has no system-prompt option (1.18.25 prints its usage and
27
+ * exits 1 on `--system-prompt`), so the shared lowerer composes a persona
28
+ * into the prompt.
22
29
  *
23
30
  * Tool policy is omitted — opencode manages tool access through its own agent
24
- * config files, not via CLI flags.
31
+ * config files, not via CLI flags. The one exception is the model-work tool
32
+ * policy: the builder injects its confined agent through
33
+ * `OPENCODE_CONFIG_CONTENT` and selects it with `--agent`. That command is
34
+ * akm's own (`opencode run --agent akm-model-work`): the engine's `args` are
35
+ * left out, because one such as `--attach` or `--dir` would move the run out
36
+ * of the injected config or the scratch working directory. Only the model they
37
+ * name is kept.
38
+ *
39
+ * Inference reaches the run the same way (`./model-config.ts`): the model it
40
+ * names gets an entry in the injected config, which merges over the user's own
41
+ * config for that model, and model work's agent carries the options, whether
42
+ * or not a model is named. A dispatch whose request carries no translatable
43
+ * inference injects nothing, so its argv and env are as they were.
25
44
  */
26
45
  export const opencodeBuilder = {
27
46
  platform: "opencode",
28
- personaChannel: "native",
47
+ personaChannel: "prompt",
29
48
  lower: createAgentRequestLowerer({
30
49
  adapter: "opencode",
31
- personaChannel: "native",
50
+ personaChannel: "prompt",
32
51
  nativeAgentSelector: true,
33
52
  tools: "none",
34
- outputSchema: false,
53
+ modelWorkTools: true,
54
+ inference: (profile, request) => {
55
+ const model = request.model?.resolved ?? modelFromArgs(profile.args);
56
+ return opencodeCarriedKeys(model !== undefined && splitOpencodeModel(model) !== undefined, request.inference, isModelWorkTools(request.tools));
57
+ },
35
58
  }),
36
59
  build(profile, req) {
60
+ const model = req.model ?? modelFromArgs(profile.args);
61
+ const modelWork = isModelWorkTools(req.tools);
62
+ // What goes on the model needs a `provider/model` to go on; model work's agent options do not.
63
+ const { entry, agentOptions } = opencodeInferenceConfig(req.inference, modelWork);
64
+ const modelConfig = opencodeModelConfig(model, entry);
65
+ if (modelWork) {
66
+ return {
67
+ argv: [
68
+ profile.bin,
69
+ "run",
70
+ "--agent",
71
+ MODEL_WORK_OPENCODE_AGENT,
72
+ ...(model ? ["--model", model] : []),
73
+ "--",
74
+ req.prompt,
75
+ ],
76
+ env: { OPENCODE_CONFIG_CONTENT: JSON.stringify({ ...modelWorkOpencodeConfig(agentOptions), ...modelConfig }) },
77
+ };
78
+ }
37
79
  const args = req.model ? [] : [...profile.args];
38
80
  if (req.model) {
39
81
  for (let index = 0; index < profile.args.length; index += 1) {
@@ -48,9 +90,6 @@ export const opencodeBuilder = {
48
90
  }
49
91
  }
50
92
  }
51
- if (req.systemPrompt) {
52
- args.push("--system-prompt", req.systemPrompt);
53
- }
54
93
  if (req.agent) {
55
94
  args.push("--agent", req.agent);
56
95
  }
@@ -60,6 +99,9 @@ export const opencodeBuilder = {
60
99
  }
61
100
  args.push("--");
62
101
  args.push(req.prompt);
63
- return { argv: [profile.bin, ...args] };
102
+ return {
103
+ argv: [profile.bin, ...args],
104
+ ...(modelConfig ? { env: { OPENCODE_CONFIG_CONTENT: JSON.stringify(modelConfig) } } : {}),
105
+ };
64
106
  },
65
107
  };
@@ -21,14 +21,6 @@ export class OpencodeHarness extends BaseHarness {
21
21
  setupDetectionDir = ".config/opencode";
22
22
  agentBuilder = opencodeBuilder;
23
23
  // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
24
- // This entry is the CLI spawn path (`opencode run …`): akm launches the
25
- // harness locally per unit ⇒ local-runner. (The SDK path is the separate
26
- // `opencode-sdk` harness.)
27
- pattern = "local-runner";
28
- // The CLI path emits plain text — no JSON stream akm consumes — so the
29
- // engine uses the prompt-injected schema + embedded-JSON extraction tier
30
- // (the matrix's "via prompt+validate"). The SDK entry is native-json.
31
- structuredOutput = "none";
32
24
  // Session-id env marker for run attribution.
33
25
  identityEnv = ["OPENCODE_SESSION_ID"];
34
26
  sessionLogProvider = () => new OpenCodeProvider();
@@ -0,0 +1,80 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ const isPositiveInteger = (value) => typeof value === "number" && Number.isInteger(value) && value > 0;
5
+ /**
6
+ * What opencode carries of `inference`, and the keys that covers. A key it
7
+ * cannot carry is left out: one of the wrong type, or `maxTokens` or
8
+ * `contextLength` without the other. An explicit `null` clears a field, so it
9
+ * is carried as nothing.
10
+ */
11
+ export function carriedInference(inference) {
12
+ const carried = {};
13
+ const { temperature, reasoningEffort, enableThinking, maxTokens, contextLength } = inference ?? {};
14
+ if (typeof temperature === "number" && Number.isFinite(temperature))
15
+ carried.temperature = temperature;
16
+ if (typeof reasoningEffort === "string" && reasoningEffort.length > 0)
17
+ carried.reasoningEffort = reasoningEffort;
18
+ if (typeof enableThinking === "boolean")
19
+ carried.enableThinking = enableThinking;
20
+ const limit = isPositiveInteger(maxTokens) && isPositiveInteger(contextLength);
21
+ if (limit)
22
+ carried.limit = { context: contextLength, output: maxTokens };
23
+ const keys = [
24
+ ["temperature", carried.temperature !== undefined, temperature],
25
+ ["reasoningEffort", carried.reasoningEffort !== undefined, reasoningEffort],
26
+ ["enableThinking", carried.enableThinking !== undefined, enableThinking],
27
+ ["maxTokens", limit, maxTokens],
28
+ ["contextLength", limit, contextLength],
29
+ ]
30
+ .filter(([, isCarried, value]) => isCarried || value === null)
31
+ .map(([key]) => key);
32
+ return { carried, keys };
33
+ }
34
+ /** opencode's own split of `provider/model`: at the first slash, so a model id may contain slashes. */
35
+ export function splitOpencodeModel(model) {
36
+ const slash = model.indexOf("/");
37
+ if (slash <= 0 || slash === model.length - 1)
38
+ return undefined;
39
+ return { providerID: model.slice(0, slash), modelID: model.slice(slash + 1) };
40
+ }
41
+ /**
42
+ * The inference keys opencode carries for a dispatch. With a model to attach
43
+ * them to (`attachable`) it carries all of them. Without one it carries only
44
+ * what the model-work agent can, which is every option but the limit; any
45
+ * other dispatch carries nothing.
46
+ */
47
+ export function opencodeCarriedKeys(attachable, inference, modelWork) {
48
+ const { keys } = carriedInference(inference);
49
+ if (attachable)
50
+ return keys;
51
+ return modelWork ? keys.filter((key) => key !== "maxTokens" && key !== "contextLength") : [];
52
+ }
53
+ /** Split `inference` between the model and, for model work, the confined agent (see the module comment). */
54
+ export function opencodeInferenceConfig(inference, modelWork) {
55
+ const { carried } = carriedInference(inference);
56
+ const options = {};
57
+ if (carried.temperature !== undefined)
58
+ options.temperature = carried.temperature;
59
+ if (carried.reasoningEffort !== undefined)
60
+ options.reasoningEffort = carried.reasoningEffort;
61
+ if (carried.enableThinking !== undefined) {
62
+ options.chat_template_kwargs = { enable_thinking: carried.enableThinking };
63
+ options.enable_thinking = carried.enableThinking;
64
+ }
65
+ const hasOptions = Object.keys(options).length > 0;
66
+ return {
67
+ entry: {
68
+ ...(hasOptions && !modelWork ? { options } : {}),
69
+ ...(carried.limit ? { limit: { ...carried.limit } } : {}),
70
+ },
71
+ ...(hasOptions && modelWork ? { agentOptions: options } : {}),
72
+ };
73
+ }
74
+ /** The config that gives `model` (`provider/model`) `entry`: undefined when the model cannot be split or `entry` is empty. */
75
+ export function opencodeModelConfig(model, entry) {
76
+ const target = model === undefined ? undefined : splitOpencodeModel(model);
77
+ if (!target || Object.keys(entry).length === 0)
78
+ return undefined;
79
+ return { provider: { [target.providerID]: { models: { [target.modelID]: entry } } } };
80
+ }