akm-cli 0.9.0-rc.14 → 0.9.0-rc.15

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 (40) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cli/retired-commands.js +3 -1
  3. package/dist/commands/agent/agent-dispatch.js +7 -3
  4. package/dist/commands/health/checks.js +6 -3
  5. package/dist/commands/improve/reflect.js +5 -3
  6. package/dist/commands/proposal/propose.js +5 -3
  7. package/dist/commands/sources/init.js +1 -1
  8. package/dist/commands/tasks/tasks.js +4 -9
  9. package/dist/commands/workflow-cli.js +0 -149
  10. package/dist/core/config/schema/experimental.js +0 -9
  11. package/dist/core/config/schema/workflow.js +0 -2
  12. package/dist/core/errors.js +0 -1
  13. package/dist/integrations/agent/engine-fallback.js +76 -0
  14. package/dist/integrations/agent/profiles.js +7 -3
  15. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +6 -1
  16. package/dist/output/shapes/passthrough.js +0 -2
  17. package/dist/output/text/helpers.js +1 -1
  18. package/dist/output/text/workflow-format.js +8 -119
  19. package/dist/output/text/workflow.js +1 -2
  20. package/dist/scripts/akm-migrate-node.js +5 -112
  21. package/dist/scripts/akm-migrate.js +5 -112
  22. package/dist/setup/detect.js +3 -1
  23. package/dist/storage/repositories/workflow-runs-repository.js +9 -23
  24. package/dist/tasks/runner.js +25 -4
  25. package/dist/tasks/validator.js +6 -2
  26. package/dist/workflows/exec/native-executor.js +26 -27
  27. package/dist/workflows/exec/param-secrets.js +9 -9
  28. package/dist/workflows/exec/run-workflow.js +22 -19
  29. package/dist/workflows/exec/step-work.js +45 -157
  30. package/dist/workflows/ir/freeze.js +20 -8
  31. package/dist/workflows/ir/params.js +1 -1
  32. package/dist/workflows/runtime/runs.js +23 -64
  33. package/dist/workflows/runtime/unit-checkin.js +1 -1
  34. package/dist/workflows/runtime/unit-phases.js +2 -2
  35. package/docs/migration/v0.8-to-v0.9.md +2 -1
  36. package/package.json +2 -2
  37. package/schemas/akm-config.json +0 -6
  38. package/dist/workflows/exec/brief.js +0 -450
  39. package/dist/workflows/exec/report.js +0 -1467
  40. package/dist/workflows/exec/workflow-engine-gate.js +0 -65
package/CHANGELOG.md CHANGED
@@ -6,8 +6,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Removed
10
+
11
+ - **The experimental `akm workflow brief` / `akm workflow report`
12
+ external-driver protocol is removed**, along with the
13
+ `experimental.workflowEngine` config key that gated it, its
14
+ `WORKFLOW_ENGINE_NOT_ENABLED` error code, and the `workflowEngine` block in
15
+ `akm task doctor`. `akm workflow run` is now the single execution surface.
16
+
17
+ The protocol let a calling agent session execute a run's units itself
18
+ instead of akm dispatching them. Its stated justification was harness
19
+ neutrality, which measurement did not support: native dispatch already
20
+ covers **ten** harnesses (opencode, claude, opencode-sdk, codex, copilot,
21
+ pi, gemini, aider, amazonq, openhands) in 2,214 LOC total, while the
22
+ protocol cost 2,690 LOC on its own — more than supporting every harness
23
+ natively — and an eleventh harness is ~220 lines, not a protocol. Removing
24
+ it also drops the second consumer of `workflow_run_units` and the
25
+ cross-surface parity obligation on `step-work.ts`, both of which
26
+ constrained every future engine change. The analysis is recorded in
27
+ `docs/architecture/specs/driver-protocol-keep-or-cut.md`.
28
+
29
+ Legacy configs setting `experimental.workflowEngine` remain valid — the
30
+ config schema is `.passthrough()`, so the key is accepted and ignored.
31
+
9
32
  ### Added
10
33
 
34
+ - **`akm workflow run` and prompt tasks fall back to `opencode-sdk` instead of
35
+ refusing when no engine is configured.** A clean install that never ran
36
+ `akm setup` — a bare container, a CI image, an agent-operated session — used
37
+ to fail closed with `INVALID_CONFIG_FILE` (exit 78). When the `opencode`
38
+ binary is on PATH, akm now synthesizes a **config-free** `opencode-sdk`
39
+ engine: it carries no model, endpoint, or credential, so provider, model,
40
+ and auth all resolve from opencode's own configuration and akm never mirrors
41
+ or validates it. With `opencode` absent the failure is unchanged, and its
42
+ remedy now names both routes. An operator-configured `opencode-sdk` engine
43
+ always wins over the synthesized one.
44
+
45
+ The requirement is the **binary**, not the npm package: `@opencode-ai/sdk`
46
+ is an HTTP client that declares no dependencies and whose own
47
+ `createOpencodeServer` spawns `opencode serve`, so a host with the package
48
+ and no binary has no server to reach. Install it with `npm i -g opencode-ai`
49
+ or opencode's own installer.
50
+
51
+ The fallback is **announced, never silent**: a workflow run surfaces it once
52
+ at run creation in the result's `warnings`, and a prompt task writes it to
53
+ the task run log. The frozen plan records the engine actually used, so a
54
+ resume never re-announces a decision it did not make.
55
+
11
56
  - **RSS, Bluesky, and X sources.** `akm bundle add` now recognizes three new
12
57
  kinds of URL and snapshots them as knowledge assets instead of crawling
13
58
  them as ordinary web pages:
@@ -56,7 +56,9 @@ const RETIRED_COMMAND_HINTS = {
56
56
  "workflow watch": "`akm workflow watch` was removed in 0.9 — use `akm log --run <run-id>`.",
57
57
  "workflow start": "`akm workflow start` was removed in 0.9 — use `akm workflow run <ref>`.",
58
58
  "workflow next": "`akm workflow next` was removed in 0.9 — use `akm workflow status <target>` to inspect or `akm workflow run <target>` to execute.",
59
- "workflow complete": "`akm workflow complete` was removed in 0.9 — native runs complete steps automatically; external drivers use `akm workflow brief` and `akm workflow report`.",
59
+ "workflow complete": "`akm workflow complete` was removed in 0.9 — `akm workflow run <target>` completes steps automatically; use `akm workflow status <target>` to inspect.",
60
+ "workflow brief": "`akm workflow brief` was removed — the external-driver protocol is gone; `akm workflow run <target>` executes the run and `akm workflow status <target>` inspects it.",
61
+ "workflow report": "`akm workflow report` was removed — the external-driver protocol is gone; `akm workflow run <target>` dispatches and records units itself.",
60
62
  "config show": "`akm config show` was removed in 0.9 — use `akm config list`.",
61
63
  "config validate": "`akm config validate` was removed in 0.9 — the config file is validated on every load.",
62
64
  "task enable": "`akm task enable` was removed in 0.9 — set `enabled: true` in the task YAML, then `akm task sync`.",
@@ -16,6 +16,7 @@
16
16
  import fs from "node:fs";
17
17
  import { parseRefInput } from "../../core/asset/resolve-ref.js";
18
18
  import { NotFoundError, UsageError } from "../../core/errors.js";
19
+ import { NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, withEngineFallback, } from "../../integrations/agent/engine-fallback.js";
19
20
  import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
20
21
  import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
21
22
  /**
@@ -60,10 +61,13 @@ async function resolveAssetBody(ref) {
60
61
  export async function akmAgentDispatch(options) {
61
62
  if (!options.agentConfig)
62
63
  throw new UsageError("agent requires a valid config with an agent engine.", "MISSING_REQUIRED_ARGUMENT");
63
- const engineName = options.engine ?? options.agentConfig.defaults?.engine;
64
+ // Same implicit opencode-sdk fallback the workflow and task surfaces apply,
65
+ // so an engine-less install is usable everywhere or nowhere — not a mix.
66
+ const { config: agentConfig } = withEngineFallback(options.agentConfig);
67
+ const engineName = options.engine ?? agentConfig.defaults?.engine;
64
68
  if (!engineName)
65
- throw new UsageError("agent requires --engine or defaults.engine.", "MISSING_REQUIRED_ARGUMENT");
66
- const runner = resolveEngine(engineName, options.agentConfig);
69
+ throw new UsageError(`agent ${NO_ENGINE_MESSAGE_SUFFIX} ${NO_ENGINE_REMEDY}`, "MISSING_REQUIRED_ARGUMENT");
70
+ const runner = resolveEngine(engineName, agentConfig);
67
71
  if (runner.kind === "llm") {
68
72
  throw new UsageError(`Engine "${engineName}" is an LLM engine; akm agent requires an agent engine.`, "INVALID_FLAG_VALUE");
69
73
  }
@@ -2,8 +2,8 @@
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
  import { spawnSync } from "node:child_process";
5
- import { createRequire } from "node:module";
6
5
  import { loadConfig } from "../../core/config/config.js";
6
+ import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
7
7
  import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
8
8
  import { resolveModel } from "../../integrations/agent/model-aliases.js";
9
9
  import { resolveImprovePlan } from "../improve/improve-strategies.js";
@@ -28,7 +28,7 @@ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
28
28
  if (configuredEngine?.kind === "agent" && configuredEngine.platform === "opencode-sdk") {
29
29
  let packageAvailable = false;
30
30
  try {
31
- const resolvePackage = deps.resolvePackage ?? ((name) => createRequire(import.meta.url).resolve(name));
31
+ const resolvePackage = deps.resolvePackage ?? ((name) => import.meta.resolve(name));
32
32
  resolvePackage("@opencode-ai/sdk");
33
33
  packageAvailable = true;
34
34
  }
@@ -170,7 +170,10 @@ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
170
170
  }
171
171
  }
172
172
  export function runDefaultEngineProbe(deps = {}) {
173
- const config = deps.loadConfig?.() ?? loadConfig();
173
+ // Probe the effective view: an install with no `defaults.engine` but a usable
174
+ // opencode binary DOES have a working default, and reporting otherwise would
175
+ // contradict what `workflow run` / `task run` actually do.
176
+ const { config } = withEngineFallback(deps.loadConfig?.() ?? loadConfig(), deps.which);
174
177
  return runConfiguredEngineProbe("default-engine", config.defaults?.engine, config, deps);
175
178
  }
176
179
  export function runDefaultLlmEngineProbe(deps = {}) {
@@ -37,6 +37,7 @@ import { redactSensitiveText } from "../../core/redaction.js";
37
37
  import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
38
38
  import { lookup } from "../../indexer/indexer.js";
39
39
  import { DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js";
40
+ import { NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, withEngineFallback, } from "../../integrations/agent/engine-fallback.js";
40
41
  import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
41
42
  import { buildReflectOutputRepairPrompt, buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
42
43
  import { materializeLlmRunnerConnection, resolveImproveProcessRunner, runnerIsLlm, runnerSupportsFileWrite, } from "../../integrations/agent/runner.js";
@@ -1185,11 +1186,12 @@ function resolveReflectRunner(options) {
1185
1186
  runnerSpec = processRunner;
1186
1187
  }
1187
1188
  else {
1188
- const defaultEngine = config.defaults?.engine;
1189
+ const { config: engineConfig } = withEngineFallback(config);
1190
+ const defaultEngine = engineConfig.defaults?.engine;
1189
1191
  if (!defaultEngine) {
1190
- throw new ConfigError("reflect requires --engine or defaults.engine.", "INVALID_CONFIG_FILE");
1192
+ throw new ConfigError(`reflect ${NO_ENGINE_MESSAGE_SUFFIX} ${NO_ENGINE_REMEDY}`, "INVALID_CONFIG_FILE");
1191
1193
  }
1192
- runnerSpec = resolveEngine(defaultEngine, config);
1194
+ runnerSpec = resolveEngine(defaultEngine, engineConfig);
1193
1195
  }
1194
1196
  if (options.eventSource === "improve" && !runnerIsLlm(runnerSpec)) {
1195
1197
  throw new ConfigError(`Unattended improve requires an LLM engine for reflect; engine "${runnerSpec.engine ?? options.engine ?? "unknown"}" is tool-capable.`, "INVALID_CONFIG_FILE", "Set defaults.llmEngine or improve.strategies.<name>.processes.reflect.engine to an LLM engine.");
@@ -21,6 +21,7 @@ import { appendEvent } from "../../core/events.js";
21
21
  import { redactSensitiveText } from "../../core/redaction.js";
22
22
  import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
23
23
  import { deriveEntryProvenance } from "../../indexer/installations.js";
24
+ import { NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, withEngineFallback, } from "../../integrations/agent/engine-fallback.js";
24
25
  import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
25
26
  import { buildProposePrompt, parseAgentProposalPayload } from "../../integrations/agent/prompts.js";
26
27
  import { collectDispatchSensitiveValues, executeRunner } from "../../integrations/agent/runner-dispatch.js";
@@ -83,10 +84,11 @@ export async function akmPropose(options) {
83
84
  const config = options.agentConfig ?? (await import("../../core/config/config.js")).loadConfig();
84
85
  const target = resolveProposalQueueTarget(stash, config);
85
86
  emitProposeInvoked(target.source, options);
86
- const engineName = options.engine ?? config.defaults?.engine;
87
+ const { config: engineConfig } = withEngineFallback(config);
88
+ const engineName = options.engine ?? engineConfig.defaults?.engine;
87
89
  if (!engineName)
88
- throw new ConfigError("propose requires --engine or defaults.engine.", "INVALID_CONFIG_FILE");
89
- const runner = resolveEngine(engineName, config);
90
+ throw new ConfigError(`propose ${NO_ENGINE_MESSAGE_SUFFIX} ${NO_ENGINE_REMEDY}`, "INVALID_CONFIG_FILE");
91
+ const runner = resolveEngine(engineName, engineConfig);
90
92
  const profile = runner.kind === "llm" ? undefined : runner.profile;
91
93
  // 3. Build prompt.
92
94
  // Synthesize a temp draft path so opencode can write the asset content
@@ -87,7 +87,7 @@ async function akmInitReal(options) {
87
87
  for (const sub of stashDirNames()) {
88
88
  const subDir = path.join(stashDir, sub);
89
89
  if (!fs.existsSync(subDir)) {
90
- fs.mkdirSync(subDir, { recursive: true });
90
+ fs.mkdirSync(subDir, { recursive: true, mode: sub === "env" || sub === "secrets" ? 0o700 : undefined });
91
91
  }
92
92
  }
93
93
  // Ensure the default stash is a local git repo (no remote required)
@@ -19,6 +19,7 @@ import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled } from "../../cor
19
19
  import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
20
20
  import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
21
21
  import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetForMutation, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
22
+ import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
22
23
  import { backendNameForPlatform, selectBackend } from "../../tasks/backends/index.js";
23
24
  import { parseTaskDocument } from "../../tasks/parser.js";
24
25
  import { resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
@@ -27,7 +28,6 @@ import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT } from "../../tasks/sched
27
28
  import { schedulerContextDescriptor, validateSchedulerContextDescriptor, writeSchedulerContextDescriptor, } from "../../tasks/scheduler-invocation.js";
28
29
  import { normaliseTaskId } from "../../tasks/task-id.js";
29
30
  import { validateTaskDocument } from "../../tasks/validator.js";
30
- import { isWorkflowEngineEnabled, WORKFLOW_ENGINE_CONFIG_KEY } from "../../workflows/exec/workflow-engine-gate.js";
31
31
  import { applyAutonomyGate, configuredDirectAutonomyLanes, describeGatedLanes } from "../improve/autonomy-gate.js";
32
32
  import { resolveImproveStrategy } from "../improve/improve-strategies.js";
33
33
  export async function akmTasksAdd(input, deps = {}) {
@@ -392,7 +392,9 @@ export async function akmTasksDoctor(deps = {}) {
392
392
  }
393
393
  }
394
394
  const bindings = groupInstalledBindings(installed, invocation);
395
- const config = loadConfig();
395
+ // Report the EFFECTIVE engine view — the same one the runner resolves —
396
+ // so doctor never says "no engine" on an install where tasks actually run.
397
+ const { config } = withEngineFallback(loadConfig());
396
398
  const defaultEngine = config.defaults?.engine;
397
399
  const engines = Object.keys(config.engines ?? {});
398
400
  // §6.1: surface the effective triage settings for the default improve
@@ -413,12 +415,6 @@ export async function akmTasksDoctor(deps = {}) {
413
415
  configKey: IMPROVE_AUTONOMY_CONFIG_KEY,
414
416
  gatedLanes: allGated.map((entry) => ({ lane: entry.lane, reason: entry.reason })),
415
417
  };
416
- // Q-05 — report the external-driver gate alongside the autonomy gate: both
417
- // are `experimental.*` opt-ins doctor exists to surface.
418
- const workflowEngine = {
419
- enabled: isWorkflowEngineEnabled(config),
420
- configKey: WORKFLOW_ENGINE_CONFIG_KEY,
421
- };
422
418
  const triage = effectiveStrategy.processes?.triage;
423
419
  const improveTriage = triage
424
420
  ? {
@@ -442,7 +438,6 @@ export async function akmTasksDoctor(deps = {}) {
442
438
  scheduleSubset: SCHEDULE_SUPPORTED_SUBSET_HINT,
443
439
  warnings,
444
440
  improveAutonomy,
445
- workflowEngine,
446
441
  ...(improveTriage ? { improveTriage } : {}),
447
442
  };
448
443
  }
@@ -4,18 +4,15 @@
4
4
  /**
5
5
  * `akm workflow` command family. `run` is the canonical start/resume/execute
6
6
  * surface; the former public `start`, `next`, and `complete` lifecycle is gone.
7
- * `brief`/`report` retain the experimental harness-neutral driver protocol.
8
7
  * Workflows are markdown-only; authoring uses `create --print` and validation
9
8
  * uses `akm lint --type workflows`.
10
9
  */
11
10
  import { getStringArg } from "../cli/parse-args.js";
12
11
  import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
13
12
  import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
14
- import { loadConfig } from "../core/config/config.js";
15
13
  import { NotFoundError, UsageError } from "../core/errors.js";
16
14
  import { akmIndex } from "../indexer/indexer.js";
17
15
  import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
18
- import { requireWorkflowEngineEnabled } from "../workflows/exec/workflow-engine-gate.js";
19
16
  import { WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
20
17
  import { abandonWorkflowRun, getWorkflowStatus, hasWorkflowRun, listWorkflowRuns, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
21
18
  const workflowStatusCommand = defineJsonCommand({
@@ -283,150 +280,6 @@ function parseWorkflowTimeout(raw) {
283
280
  }
284
281
  return timeoutMs;
285
282
  }
286
- const workflowBriefCommand = defineJsonCommand({
287
- meta: {
288
- name: "brief",
289
- description: "EXPERIMENTAL, gated behind `experimental.workflowEngine`: describe a run's active step as an executable " +
290
- "work-list for ANY agent session (the harness-neutral driver protocol) — read-only, takes no engine lease, " +
291
- "mutates nothing; prints per-unit instructions, output schema, env binding names, and the exact " +
292
- "`akm workflow report` command lines",
293
- },
294
- args: {
295
- target: {
296
- type: "positional",
297
- description: "Workflow run id (or a workflow ref with an active run)",
298
- required: true,
299
- },
300
- },
301
- async run({ args }) {
302
- requireWorkflowEngineEnabled(loadConfig(), "brief");
303
- const { buildWorkflowBrief } = await import("../workflows/exec/brief.js");
304
- const result = await buildWorkflowBrief(args.target);
305
- output("workflow-brief", result);
306
- },
307
- });
308
- const WORKFLOW_REPORT_STATES = ["completed", "failed", "running"];
309
- const workflowReportCommand = defineJsonCommand({
310
- meta: {
311
- name: "report",
312
- description: "EXPERIMENTAL, gated behind `experimental.workflowEngine`: report a unit's result back into a run (the " +
313
- "mutating half of the harness-neutral driver protocol) — ingested through the SAME shared step semantics " +
314
- "the engine uses. --status running claims/" +
315
- "heartbeats a unit; completed/failed records it and, when the step's work-list is fully terminal, runs the " +
316
- "engine's completion path (reducer, artifact + schema validation, gate). --settle (no --unit) advances a run " +
317
- "parked on a route-only/empty step. Refused while a live engine lease exists",
318
- },
319
- args: {
320
- target: {
321
- type: "positional",
322
- description: "Workflow run id (or a workflow ref with an active run)",
323
- required: true,
324
- },
325
- unit: {
326
- type: "string",
327
- description: "Content-derived unit id from `akm workflow brief` (copy it verbatim). Omit with --settle.",
328
- },
329
- settle: {
330
- type: "boolean",
331
- description: "Advance/finalize a run whose active step has NO unit left to report: a non-dispatching step (params-based route, empty fan-out, all-unresolvable) OR a fully-terminal step still needing finalization (every unit ran but the gate never judged — e.g. after resuming a required-gate block). Runs the deterministic completion path. Mutually exclusive with --unit; refused when the step still has genuinely pending units",
332
- default: false,
333
- },
334
- "expect-step": {
335
- type: "string",
336
- description: "Guard: the step id you briefed against. Refuses the report if the run's active step has since moved (from the `brief` report/settle command line)",
337
- },
338
- status: { type: "string", description: `Unit status: ${WORKFLOW_REPORT_STATES.join(", ")}` },
339
- result: { type: "string", description: "Result payload (JSON for a schema unit, else text). completed only." },
340
- "result-file": { type: "string", description: "Read the result payload from this file instead of --result/stdin" },
341
- tokens: { type: "string", description: "Tokens spent on this unit (counts against a declared budget)" },
342
- "session-id": { type: "string", description: "Harness-native session id revealed while executing the unit" },
343
- "failure-reason": { type: "string", description: "Structured failure vocabulary for a --status failed report" },
344
- note: { type: "string", description: "Short progress note for a --status running heartbeat (not persisted)" },
345
- rerun: {
346
- type: "boolean",
347
- description: "Re-run an already-FAILED unit: record a NEW attempt (re-applies budget) instead of refusing a differing re-report",
348
- default: false,
349
- },
350
- },
351
- async run({ args }) {
352
- requireWorkflowEngineEnabled(loadConfig(), "report");
353
- // --settle: the unit-less verb that advances a run parked on a
354
- // non-dispatching step. Mutually exclusive with the per-unit report flags.
355
- if (args.settle === true) {
356
- if (getStringArg(args, "unit") !== undefined || getStringArg(args, "status") !== undefined) {
357
- throw new UsageError("--settle advances a route-only/empty step and takes no --unit or --status. Drop them, or report a " +
358
- "specific unit with `--unit <id> --status <state>` instead.", "INVALID_FLAG_VALUE");
359
- }
360
- const { settleWorkflowSpine } = await import("../workflows/exec/report.js");
361
- const result = await settleWorkflowSpine({
362
- target: args.target,
363
- ...(getStringArg(args, "expect-step") !== undefined ? { expectStep: getStringArg(args, "expect-step") } : {}),
364
- });
365
- output("workflow-report", result);
366
- return;
367
- }
368
- const status = args.status;
369
- if (!status) {
370
- throw new UsageError("--status is required (completed | failed | running), or pass --settle to advance a non-dispatching step.", "MISSING_REQUIRED_ARGUMENT");
371
- }
372
- if (!WORKFLOW_REPORT_STATES.includes(status)) {
373
- throw new UsageError(`Invalid --status "${status}". Expected one of: ${WORKFLOW_REPORT_STATES.join(", ")}.`, "INVALID_FLAG_VALUE");
374
- }
375
- const unitId = getStringArg(args, "unit");
376
- if (!unitId) {
377
- throw new UsageError("--unit is required (the content-derived unit id from `akm workflow brief`), or pass --settle for a route-only/empty step.", "MISSING_REQUIRED_ARGUMENT");
378
- }
379
- let tokens;
380
- const rawTokens = getStringArg(args, "tokens");
381
- if (rawTokens !== undefined) {
382
- tokens = Number.parseInt(rawTokens, 10);
383
- if (!/^\d+$/.test(rawTokens)) {
384
- throw new UsageError(`--tokens must be a non-negative integer, got "${rawTokens}".`, "INVALID_FLAG_VALUE");
385
- }
386
- }
387
- // Result payload precedence: --result, then --result-file, then stdin
388
- // (completed/failed only; a running heartbeat carries no result).
389
- let resultRaw;
390
- if (status !== "running") {
391
- const resultFile = getStringArg(args, "result-file");
392
- if (args.result !== undefined && resultFile !== undefined) {
393
- throw new UsageError("Pass at most one of --result or --result-file.", "INVALID_FLAG_VALUE");
394
- }
395
- if (args.result !== undefined) {
396
- resultRaw = String(args.result);
397
- }
398
- else if (resultFile !== undefined) {
399
- const fs = await import("node:fs");
400
- resultRaw = fs.readFileSync(resultFile, "utf8");
401
- }
402
- else if (!process.stdin.isTTY) {
403
- resultRaw = await readStdin();
404
- }
405
- }
406
- const { reportWorkflowUnit } = await import("../workflows/exec/report.js");
407
- const result = await reportWorkflowUnit({
408
- target: args.target,
409
- unitId,
410
- status: status,
411
- ...(getStringArg(args, "expect-step") !== undefined ? { expectStep: getStringArg(args, "expect-step") } : {}),
412
- ...(resultRaw !== undefined ? { resultRaw } : {}),
413
- ...(tokens !== undefined ? { tokens } : {}),
414
- ...(args.rerun === true ? { rerun: true } : {}),
415
- ...(getStringArg(args, "session-id") !== undefined ? { sessionId: getStringArg(args, "session-id") } : {}),
416
- ...(getStringArg(args, "failure-reason") !== undefined
417
- ? { failureReason: getStringArg(args, "failure-reason") }
418
- : {}),
419
- ...(getStringArg(args, "note") !== undefined ? { note: getStringArg(args, "note") } : {}),
420
- });
421
- output("workflow-report", result);
422
- },
423
- });
424
- async function readStdin() {
425
- const chunks = [];
426
- for await (const chunk of process.stdin)
427
- chunks.push(chunk);
428
- return Buffer.concat(chunks).toString("utf8");
429
- }
430
283
  const workflowAbandonCommand = defineJsonCommand({
431
284
  meta: {
432
285
  name: "abandon",
@@ -465,8 +318,6 @@ export const workflowCommand = defineGroupCommand({
465
318
  resume: workflowResumeCommand,
466
319
  abandon: workflowAbandonCommand,
467
320
  run: workflowRunCommand,
468
- brief: workflowBriefCommand,
469
- report: workflowReportCommand,
470
321
  },
471
322
  // No `defaultRun`: bare `akm workflow` is a usage error (exit 2), the
472
323
  // canonical bare-group behavior — owner ruling 12. Run `akm workflow list
@@ -26,14 +26,5 @@ export const ExperimentalConfigSchema = z
26
26
  * purpose, and it has its own `sync.push: false` / `--no-push` controls.
27
27
  */
28
28
  improveAutonomy: z.boolean().optional(),
29
- /**
30
- * Allow the harness-neutral workflow external-driver protocol (Q-05).
31
- *
32
- * OFF by default. Gates only `akm workflow brief`/`report`. Stable native
33
- * orchestration through `workflow run`, authoring/linting, inspection, and
34
- * recovery remain available regardless of this key. A gated driver call
35
- * refuses outright rather than degrading.
36
- */
37
- workflowEngine: z.boolean().optional(),
38
29
  })
39
30
  .passthrough();
@@ -19,8 +19,6 @@ import { engineName, positiveInt } from "./primitives.js";
19
19
  * `[1, WORKFLOW_MAX_CONCURRENCY_CEILING]` (64). Values above the ceiling
20
20
  * are clamped, not rejected, so a config shared across machines with wildly
21
21
  * different core counts never hard-fails validation.
22
- * The R3 brief/report driver surface does NOT consult this — drivers own their
23
- * own parallelism (the engine only caps native dispatch).
24
22
  */
25
23
  export const WorkflowConfigSchema = z
26
24
  .object({
@@ -15,7 +15,6 @@ const CONFIG_HINTS = {
15
15
  SETUP_TMP_STASH_REFUSED: "Use a persistent directory, or set AKM_FORCE_SETUP_TMP_STASH=1 to opt in to a sandboxed setup (setup also pre-sets AKM_BUNDLE_DIR so config and cache writes auto-isolate into $stashDir/.akm/ — host config is preserved).",
16
16
  UNSAFE_STASH_DIR: "Choose a path inside your home directory (e.g. ~/akm) or another empty workspace. The bundle directory cannot be the filesystem root, your home directory itself, or a sensitive system path like /etc, /var, ~/.config, or ~/.ssh.",
17
17
  UNKNOWN_IMPROVE_STRATEGY: "Pass one of the listed strategy names to `--strategy`, or define it under `improve.strategies`. Names are case-sensitive.",
18
- WORKFLOW_ENGINE_NOT_ENABLED: "Run `akm config set experimental.workflowEngine true` to enable it.",
19
18
  };
20
19
  /** Default hint for each UsageError code. */
21
20
  const USAGE_HINTS = {
@@ -0,0 +1,76 @@
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
+ import { defaultWhich } from "./detect.js";
5
+ import { OPENCODE_SDK_SERVER_BIN } from "./profiles.js";
6
+ /**
7
+ * Engine name used for the synthesized entry. Matches the platform id so it
8
+ * reads correctly in `akm workflow status`, frozen plans, and error messages.
9
+ * A user-configured engine of the same name is preferred over synthesizing.
10
+ */
11
+ export const FALLBACK_ENGINE_NAME = "opencode-sdk";
12
+ /** Announcement text, surfaced once per run/dispatch. */
13
+ export const FALLBACK_ANNOUNCEMENT = `No engine is configured; falling back to \`${FALLBACK_ENGINE_NAME}\` via the ` +
14
+ `\`${OPENCODE_SDK_SERVER_BIN}\` binary on PATH — provider, model, and auth come ` +
15
+ "from opencode's own configuration. " +
16
+ "Run `akm setup`, or set `defaults.engine`, to choose explicitly.";
17
+ /**
18
+ * Failure suffix for every surface that needs an engine and found none.
19
+ * Owned HERE, beside the probe whose outcome it describes: the probe follows a
20
+ * configured `bin` when one is pinned, so a consumer restating "not on PATH"
21
+ * would misdescribe a missing absolute path.
22
+ */
23
+ export const NO_ENGINE_MESSAGE_SUFFIX = `has no selected engine, and no usable \`${OPENCODE_SDK_SERVER_BIN}\` binary was found to fall back to.`;
24
+ /**
25
+ * Guidance used when the fallback itself is unavailable.
26
+ *
27
+ * The opencode route names a BINARY install, not `npm i @opencode-ai/sdk`:
28
+ * that package is an HTTP client with no dependencies and cannot serve
29
+ * anything on its own (see the module doc).
30
+ */
31
+ export const NO_ENGINE_REMEDY = "Run `akm setup` to detect an installed agent, or set one explicitly: " +
32
+ '`akm config set engines.claude \'{"kind":"agent","platform":"claude"}\'` ' +
33
+ "then `akm config set defaults.engine claude`. " +
34
+ `Installing the \`${OPENCODE_SDK_SERVER_BIN}\` binary also works ` +
35
+ "(`npm i -g opencode-ai`) — akm falls back to it automatically.";
36
+ /**
37
+ * Return a config whose `defaults.engine` resolves, applying the implicit
38
+ * `opencode-sdk` fallback when it does not and an opencode binary is present.
39
+ *
40
+ * Pure with respect to the input: never mutates `config`. Returns the input
41
+ * object identity unchanged when no fallback is needed, so callers can cheaply
42
+ * detect the common case.
43
+ */
44
+ export function withEngineFallback(config, whichFn = defaultWhich) {
45
+ if (config.defaults?.engine)
46
+ return { config };
47
+ // An operator-configured engine of this name wins over a synthesized one —
48
+ // theirs may carry a model, an llmEngine fallback, or a pinned bin. Probe
49
+ // THAT bin rather than the bare command: a configured absolute path outside
50
+ // PATH is still a usable engine, and reporting it as missing would contradict
51
+ // the operator-configured-wins rule.
52
+ const existing = config.engines?.[FALLBACK_ENGINE_NAME];
53
+ if (!whichFn(existing?.bin ?? OPENCODE_SDK_SERVER_BIN))
54
+ return { config };
55
+ const engines = existing
56
+ ? config.engines
57
+ : { ...(config.engines ?? {}), [FALLBACK_ENGINE_NAME]: { kind: "agent", platform: "opencode-sdk" } };
58
+ return {
59
+ config: {
60
+ ...config,
61
+ engines,
62
+ defaults: { ...(config.defaults ?? {}), engine: FALLBACK_ENGINE_NAME },
63
+ },
64
+ fallbackEngineName: FALLBACK_ENGINE_NAME,
65
+ };
66
+ }
67
+ /**
68
+ * The announcement, but only when the fallback candidate is the engine that
69
+ * actually won selection. Returns `undefined` otherwise, so an explicitly
70
+ * selected engine never triggers a claim that opencode supplied the model.
71
+ */
72
+ export function fallbackAnnouncement(fallbackEngineName, selectedEngineName) {
73
+ if (!fallbackEngineName || selectedEngineName !== fallbackEngineName)
74
+ return undefined;
75
+ return FALLBACK_ANNOUNCEMENT;
76
+ }
@@ -94,9 +94,13 @@ const BUILTINS = {
94
94
  /**
95
95
  * Binary the `opencode-sdk` harness needs on PATH.
96
96
  *
97
- * The embedded SDK is not self-contained: its runner spawns `opencode serve`
98
- * and talks HTTP to it (see `harnesses/opencode-sdk/sdk-runner.ts`), so the
99
- * `opencode` binary gates the SDK path exactly as it gates the CLI path.
97
+ * The embedded SDK is not self-contained, and that is the SDK's own design
98
+ * rather than a consequence of how akm drives it: `@opencode-ai/sdk` ships
99
+ * with `"dependencies": {}` and its `createOpencodeServer` is itself a
100
+ * `spawn("opencode", ["serve", ...])`. akm's runner spawns the same binary
101
+ * directly and talks HTTP to it (see `harnesses/opencode-sdk/sdk-runner.ts`),
102
+ * so the `opencode` binary gates the SDK path exactly as it gates the CLI
103
+ * path — a host with the npm package but no binary can dispatch neither.
100
104
  * `opencode-sdk` deliberately has no {@link BUILTINS} entry — it dispatches
101
105
  * without argv construction — so this is the one place that pairing lives.
102
106
  */
@@ -74,7 +74,12 @@
74
74
  * until the child ACTUALLY exits — and a real `opencode serve` (a live HTTP
75
75
  * server with provider children) can outlive SIGTERM long enough to hang the
76
76
  * caller indefinitely. {@link createManagedOpencode} therefore owns the spawn
77
- * (the SDK package is used only for `createOpencodeClient`):
77
+ * (the SDK package is used only for `createOpencodeClient`).
78
+ *
79
+ * Owning the spawn changes the LIFECYCLE, not the REQUIREMENT: the SDK's
80
+ * `createOpencodeServer` is itself a `spawn("opencode", ["serve", ...])`, and
81
+ * the package declares `"dependencies": {}`, so `opencode` must be on PATH
82
+ * either way. akm's version differs only in how it supervises the child:
78
83
  *
79
84
  * - after the URL handshake, the child and its stdio are `unref()`ed /
80
85
  * destroyed, so the handle can never hold akm open;
@@ -57,10 +57,8 @@ const PASSTHROUGH_COMMANDS = [
57
57
  "update",
58
58
  "upgrade",
59
59
  "workflow-abandon",
60
- "workflow-brief",
61
60
  "workflow-create",
62
61
  "workflow-list",
63
- "workflow-report",
64
62
  "workflow-resume",
65
63
  "workflow-run",
66
64
  "workflow-status",
@@ -18,4 +18,4 @@
18
18
  export { formatAddPlain, formatBundleShowPlain, formatClonePlain, formatConfigPlain, formatCuratePlain, formatEnvCreatePlain, formatEnvExportPlain, formatEnvListPlain, formatEnvRemovePlain, formatEventLine, formatEventsPlain, formatFeedbackPlain, formatImportPlain, formatIndexPlain, formatInfoPlain, formatInitPlain, formatListPlain, formatRegistryAddPlain, formatRegistryListPlain, formatRegistryRemovePlain, formatRegistrySearchPlain, formatRememberPlain, formatRemovePlain, formatSearchPlain, formatSyncPlain, formatUpdatePlain, formatUpgradePlain, } from "./command-format.js";
19
19
  export { formatGateDecisionSummary, formatProposalAcceptPlain, formatProposalDiffPlain, formatProposalDrainPlain, formatProposalListPlain, formatProposalProducerPlain, formatProposalRejectPlain, formatProposalShowPlain, } from "./proposal-format.js";
20
20
  export { formatShowPlain } from "./show-format.js";
21
- export { formatWorkflowBriefPlain, formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./workflow-format.js";
21
+ export { formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./workflow-format.js";