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.
- package/CHANGELOG.md +45 -0
- package/dist/cli/retired-commands.js +3 -1
- package/dist/commands/agent/agent-dispatch.js +7 -3
- package/dist/commands/health/checks.js +6 -3
- package/dist/commands/improve/reflect.js +5 -3
- package/dist/commands/proposal/propose.js +5 -3
- package/dist/commands/sources/init.js +1 -1
- package/dist/commands/tasks/tasks.js +4 -9
- package/dist/commands/workflow-cli.js +0 -149
- package/dist/core/config/schema/experimental.js +0 -9
- package/dist/core/config/schema/workflow.js +0 -2
- package/dist/core/errors.js +0 -1
- package/dist/integrations/agent/engine-fallback.js +76 -0
- package/dist/integrations/agent/profiles.js +7 -3
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +6 -1
- package/dist/output/shapes/passthrough.js +0 -2
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/workflow-format.js +8 -119
- package/dist/output/text/workflow.js +1 -2
- package/dist/scripts/akm-migrate-node.js +5 -112
- package/dist/scripts/akm-migrate.js +5 -112
- package/dist/setup/detect.js +3 -1
- package/dist/storage/repositories/workflow-runs-repository.js +9 -23
- package/dist/tasks/runner.js +25 -4
- package/dist/tasks/validator.js +6 -2
- package/dist/workflows/exec/native-executor.js +26 -27
- package/dist/workflows/exec/param-secrets.js +9 -9
- package/dist/workflows/exec/run-workflow.js +22 -19
- package/dist/workflows/exec/step-work.js +45 -157
- package/dist/workflows/ir/freeze.js +20 -8
- package/dist/workflows/ir/params.js +1 -1
- package/dist/workflows/runtime/runs.js +23 -64
- package/dist/workflows/runtime/unit-checkin.js +1 -1
- package/dist/workflows/runtime/unit-phases.js +2 -2
- package/docs/migration/v0.8-to-v0.9.md +2 -1
- package/package.json +2 -2
- package/schemas/akm-config.json +0 -6
- package/dist/workflows/exec/brief.js +0 -450
- package/dist/workflows/exec/report.js +0 -1467
- 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 —
|
|
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
|
-
|
|
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(
|
|
66
|
-
const runner = resolveEngine(engineName,
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
1189
|
+
const { config: engineConfig } = withEngineFallback(config);
|
|
1190
|
+
const defaultEngine = engineConfig.defaults?.engine;
|
|
1189
1191
|
if (!defaultEngine) {
|
|
1190
|
-
throw new ConfigError(
|
|
1192
|
+
throw new ConfigError(`reflect ${NO_ENGINE_MESSAGE_SUFFIX} ${NO_ENGINE_REMEDY}`, "INVALID_CONFIG_FILE");
|
|
1191
1193
|
}
|
|
1192
|
-
runnerSpec = resolveEngine(defaultEngine,
|
|
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
|
|
87
|
+
const { config: engineConfig } = withEngineFallback(config);
|
|
88
|
+
const engineName = options.engine ?? engineConfig.defaults?.engine;
|
|
87
89
|
if (!engineName)
|
|
88
|
-
throw new ConfigError(
|
|
89
|
-
const runner = resolveEngine(engineName,
|
|
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
|
-
|
|
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({
|
package/dist/core/errors.js
CHANGED
|
@@ -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
|
|
98
|
-
*
|
|
99
|
-
* `
|
|
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;
|
|
@@ -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 {
|
|
21
|
+
export { formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./workflow-format.js";
|