akm-cli 0.9.0-rc.13 → 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 +162 -23
- package/dist/akm-migrate +3 -1
- package/dist/assets/hints/cli-hints-full.md +3 -4
- package/dist/assets/hints/cli-hints-short.md +5 -5
- package/dist/assets/workflows/workflow-template.md +4 -3
- package/dist/cli/invocation.js +3 -2
- package/dist/cli/retired-commands.js +5 -0
- package/dist/cli/unknown-flags.js +226 -0
- package/dist/cli.js +19 -35
- package/dist/commands/agent/agent-dispatch.js +7 -3
- package/dist/commands/agent/contribute-cli.js +15 -3
- package/dist/commands/feedback-cli.js +6 -3
- package/dist/commands/health/checks.js +6 -3
- package/dist/commands/improve/collapse-detector.js +2 -3
- package/dist/commands/improve/reflect.js +5 -3
- package/dist/commands/lint/base-linter.js +4 -16
- package/dist/commands/lint/index.js +13 -13
- package/dist/commands/log.js +6 -1
- package/dist/commands/migration-tool.js +4 -5
- package/dist/commands/observability-cli.js +1 -1
- package/dist/commands/proposal/propose.js +5 -3
- package/dist/commands/proposal/repository.js +5 -5
- package/dist/commands/read/knowledge.js +2 -0
- package/dist/commands/registry-cli.js +5 -3
- package/dist/commands/sources/add-cli.js +6 -6
- package/dist/commands/sources/init.js +1 -1
- package/dist/commands/sources/self-update.js +30 -7
- package/dist/commands/sources/source-add.js +17 -2
- package/dist/commands/tasks/tasks.js +10 -10
- package/dist/commands/workflow-cli.js +133 -259
- package/dist/core/adapter/adapters/akm-lint.js +17 -13
- package/dist/core/adapter/adapters/akm-task-adapter.js +14 -11
- package/dist/core/asset/akm-markdown.js +41 -8
- package/dist/core/asset/frontmatter.js +22 -0
- package/dist/core/asset/resolve-ref.js +23 -3
- package/dist/core/common.js +45 -2
- package/dist/core/config/config-schema.js +8 -0
- package/dist/core/config/schema/experimental.js +0 -17
- package/dist/core/config/schema/sources-bundles.js +11 -0
- package/dist/core/config/schema/workflow.js +3 -3
- package/dist/core/errors.js +5 -1
- package/dist/core/logs-db.js +2 -1
- package/dist/core/parse.js +4 -1
- package/dist/core/state/migrations.js +11 -14
- package/dist/core/state-db.js +4 -6
- package/dist/core/subprocess.js +6 -4
- package/dist/core/type-presentation.js +1 -1
- package/dist/indexer/indexer.js +16 -1
- package/dist/indexer/search/search-source.js +1 -1
- package/dist/indexer/walk/matchers.js +3 -1
- package/dist/integrations/agent/config.js +2 -2
- package/dist/integrations/agent/detect.js +49 -19
- package/dist/integrations/agent/engine-fallback.js +76 -0
- package/dist/integrations/agent/profiles.js +14 -0
- package/dist/integrations/agent/spawn.js +1 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
- package/dist/integrations/lockfile.js +11 -56
- package/dist/output/shapes/passthrough.js +0 -6
- package/dist/output/text/command-format.js +3 -3
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/show-directives.js +4 -6
- package/dist/output/text/workflow-format.js +15 -181
- package/dist/output/text/workflow.js +1 -6
- package/dist/scripts/akm-migrate-node.js +58666 -0
- package/dist/scripts/akm-migrate.js +33976 -11498
- package/dist/setup/detect.js +42 -15
- package/dist/setup/setup.js +1 -1
- package/dist/sources/providers/git-stash.js +4 -2
- package/dist/sources/providers/website.js +5 -0
- package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
- package/dist/sources/snapshot-fetchers/content-extract.js +370 -0
- package/dist/sources/snapshot-fetchers/fetcher-util.js +40 -0
- package/dist/sources/snapshot-fetchers/host-guard.js +199 -0
- package/dist/sources/snapshot-fetchers/registry.js +10 -1
- package/dist/sources/snapshot-fetchers/robots.js +348 -0
- package/dist/sources/snapshot-fetchers/rss.js +279 -0
- package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
- package/dist/sources/snapshot-fetchers/website-ingest.js +488 -257
- package/dist/sources/snapshot-fetchers/x.js +193 -0
- package/dist/storage/engines/sqlite-migrations.js +22 -107
- package/dist/storage/repositories/workflow-runs-repository.js +9 -23
- package/dist/tasks/runner.js +43 -19
- package/dist/tasks/schema.js +24 -1
- package/dist/tasks/validator.js +6 -2
- package/dist/workflows/exec/frozen-judge.js +28 -2
- package/dist/workflows/exec/native-executor.js +44 -28
- package/dist/workflows/exec/param-secrets.js +9 -9
- package/dist/workflows/exec/run-workflow.js +122 -60
- package/dist/workflows/exec/step-work.js +64 -175
- package/dist/workflows/exec/unit-dispatch.js +4 -0
- package/dist/workflows/ir/compile.js +2 -2
- package/dist/workflows/ir/freeze.js +29 -9
- package/dist/workflows/ir/params.js +135 -11
- package/dist/workflows/ir/plan-hash.js +1 -1
- package/dist/workflows/ir/schema.js +6 -2
- package/dist/workflows/renderer.js +2 -2
- package/dist/workflows/runtime/checkin.js +3 -3
- package/dist/workflows/runtime/runs.js +72 -92
- package/dist/workflows/runtime/unit-checkin.js +1 -1
- package/dist/workflows/runtime/unit-phases.js +2 -2
- package/dist/workflows/validate-summary.js +30 -14
- package/docs/migration/release-notes/0.9.0.md +31 -4
- package/docs/migration/v0.8-to-v0.9.md +70 -100
- package/docs/reference/data-and-telemetry.md +5 -5
- package/package.json +7 -3
- package/schemas/akm-config.json +24 -6
- package/schemas/akm-workflow.json +1 -1
- package/dist/workflows/cli.js +0 -33
- package/dist/workflows/exec/brief.js +0 -450
- package/dist/workflows/exec/report.js +0 -1460
- package/dist/workflows/exec/workflow-engine-gate.js +0 -67
|
@@ -5,7 +5,7 @@ import * as childProcess from "node:child_process";
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { fetchWithRetry, IS_WINDOWS, ResponseTooLargeError, readBodyWithByteCap } from "../../core/common.js";
|
|
8
|
+
import { fetchWithRetry, IS_WINDOWS, ResponseTooLargeError, readBodyWithByteCap, readChunkWithDeadline, } from "../../core/common.js";
|
|
9
9
|
import { ConfigError } from "../../core/errors.js";
|
|
10
10
|
import { warn } from "../../core/warn.js";
|
|
11
11
|
import { githubHeaders } from "../../integrations/github.js";
|
|
@@ -29,7 +29,19 @@ export function defaultMigrationCommand(preparedConfigPath) {
|
|
|
29
29
|
apply: (akmBin) => runRequiredCommand(akmBin, ["migrate", "apply", ...configArgs], "Migration apply"),
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Bounds on the binary body read. `fetchWithTimeout`'s timer only covers
|
|
34
|
+
* time-to-HEADERS — it is cleared the moment `fetch` resolves — so without
|
|
35
|
+
* these a compromised or misconfigured endpoint could dribble bytes forever
|
|
36
|
+
* and hang `akm upgrade` with no output and no way out but Ctrl-C. Two
|
|
37
|
+
* bounds, because either alone is evadable: a per-chunk STALL deadline (a
|
|
38
|
+
* connection making no progress dies quickly, while a slow-but-progressing
|
|
39
|
+
* one is left alone) and an OVERALL deadline (a trickle that sends one byte
|
|
40
|
+
* per stall-window can't stretch the download indefinitely).
|
|
41
|
+
*/
|
|
42
|
+
const BINARY_STALL_TIMEOUT_MS = 60_000;
|
|
43
|
+
const BINARY_TOTAL_TIMEOUT_MS = 30 * 60_000;
|
|
44
|
+
export async function streamResponseToFile(response, destination, maxBytes, limits) {
|
|
33
45
|
const declaredText = response.headers.get("content-length");
|
|
34
46
|
if (declaredText) {
|
|
35
47
|
const declared = Number(declaredText);
|
|
@@ -40,22 +52,30 @@ async function streamResponseToFile(response, destination, maxBytes) {
|
|
|
40
52
|
}
|
|
41
53
|
if (!response.body)
|
|
42
54
|
throw new Error("Binary download response did not provide a streaming body.");
|
|
55
|
+
const url = response.url || "binary download";
|
|
56
|
+
const stallTimeoutMs = limits?.stallTimeoutMs ?? BINARY_STALL_TIMEOUT_MS;
|
|
57
|
+
const totalTimeoutMs = limits?.totalTimeoutMs ?? BINARY_TOTAL_TIMEOUT_MS;
|
|
58
|
+
const overallDeadlineAt = Date.now() + totalTimeoutMs;
|
|
43
59
|
const fd = fs.openSync(destination, "w", 0o600);
|
|
44
60
|
const reader = response.body.getReader();
|
|
45
61
|
const hash = createHash("sha256");
|
|
46
62
|
let byteSize = 0;
|
|
47
63
|
try {
|
|
48
64
|
while (true) {
|
|
49
|
-
|
|
65
|
+
// Whichever bound bites first wins, and the error reports THAT bound —
|
|
66
|
+
// "no data for 60s" and "download exceeded 30m" are different diagnoses.
|
|
67
|
+
const stallDeadlineAt = Date.now() + stallTimeoutMs;
|
|
68
|
+
const deadlineAt = Math.min(stallDeadlineAt, overallDeadlineAt);
|
|
69
|
+
const reportedTimeoutMs = deadlineAt === overallDeadlineAt ? totalTimeoutMs : stallTimeoutMs;
|
|
70
|
+
const { done, value } = await readChunkWithDeadline(reader, deadlineAt, undefined, url, reportedTimeoutMs);
|
|
50
71
|
if (done)
|
|
51
72
|
break;
|
|
52
73
|
if (!value)
|
|
53
74
|
continue;
|
|
54
75
|
byteSize += value.byteLength;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new ResponseTooLargeError(
|
|
58
|
-
}
|
|
76
|
+
// The catch block cancels the reader for every throw path, this included.
|
|
77
|
+
if (byteSize > maxBytes)
|
|
78
|
+
throw new ResponseTooLargeError(url, maxBytes, byteSize);
|
|
59
79
|
hash.update(value);
|
|
60
80
|
let written = 0;
|
|
61
81
|
while (written < value.byteLength)
|
|
@@ -65,6 +85,9 @@ async function streamResponseToFile(response, destination, maxBytes) {
|
|
|
65
85
|
return { byteSize, sha256: hash.digest("hex") };
|
|
66
86
|
}
|
|
67
87
|
catch (error) {
|
|
88
|
+
// Cancel so a timed-out (still-open) connection releases its socket rather
|
|
89
|
+
// than lingering until process exit. Already-cancelled readers no-op.
|
|
90
|
+
await reader.cancel().catch(() => undefined);
|
|
68
91
|
removeFileBestEffort(destination);
|
|
69
92
|
throw error;
|
|
70
93
|
}
|
|
@@ -14,6 +14,7 @@ import { readLockfile, removeLockEntry, upsertLockEntry } from "../../integratio
|
|
|
14
14
|
import { parseRegistryRef } from "../../registry/resolve.js";
|
|
15
15
|
import { detectStashRoot } from "../../sources/providers/provider-utils.js";
|
|
16
16
|
import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
|
|
17
|
+
import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
17
18
|
import { ensureWebsiteMirror, shouldAllowPrivateWebsiteUrlForTests, validateWebsiteInputUrl, } from "../../sources/snapshot-fetchers/website-ingest.js";
|
|
18
19
|
import { bundleKeyForPath, bundleKeyForUrl, nextBundleKey } from "./bundle-config-ops.js";
|
|
19
20
|
export async function akmAdd(input) {
|
|
@@ -95,13 +96,26 @@ async function addLocalSource(ref, sourcePath, stashDir, explicitName) {
|
|
|
95
96
|
async function addWebsiteSource(ref, stashDir, name, options) {
|
|
96
97
|
const allowPrivateHosts = shouldAllowPrivateWebsiteUrlForTests(ref);
|
|
97
98
|
const normalizedUrl = validateWebsiteInputUrl(ref, { allowPrivateHosts });
|
|
98
|
-
const
|
|
99
|
+
const numberOption = (value) => (typeof value === "number" ? value : undefined);
|
|
100
|
+
const maxPages = numberOption(options?.maxPages);
|
|
101
|
+
const maxDepth = numberOption(options?.maxDepth);
|
|
99
102
|
let entry;
|
|
100
103
|
mutateConfig((config) => {
|
|
101
104
|
const bundles = { ...(config.bundles ?? {}) };
|
|
102
105
|
const existingKey = bundleKeyForUrl(config, normalizedUrl);
|
|
103
106
|
const key = existingKey ?? nextBundleKey(bundles, name ?? toWebsiteName(normalizedUrl), normalizedUrl);
|
|
104
|
-
|
|
107
|
+
// Merge onto the existing descriptor rather than replacing it: re-running
|
|
108
|
+
// `bundle add` for a URL that already has a bundle would otherwise drop
|
|
109
|
+
// respectRobots / refresh, silently restoring default robots enforcement
|
|
110
|
+
// and changing what the next update fetches. Explicitly-passed maxPages /
|
|
111
|
+
// maxDepth still win over the stored values.
|
|
112
|
+
const existingWebsite = existingKey ? (bundles[key]?.website ?? {}) : {};
|
|
113
|
+
const website = {
|
|
114
|
+
...existingWebsite,
|
|
115
|
+
url: normalizedUrl,
|
|
116
|
+
...(maxPages !== undefined ? { maxPages } : {}),
|
|
117
|
+
...(maxDepth !== undefined ? { maxDepth } : {}),
|
|
118
|
+
};
|
|
105
119
|
const nextBundle = {
|
|
106
120
|
...(existingKey ? bundles[key] : {}),
|
|
107
121
|
website,
|
|
@@ -117,6 +131,7 @@ async function addWebsiteSource(ref, stashDir, name, options) {
|
|
|
117
131
|
});
|
|
118
132
|
const cachePaths = await ensureWebsiteMirror(entry, {
|
|
119
133
|
requireStashDir: true,
|
|
134
|
+
resolveSecret: storeSecretResolver.resolveSecret,
|
|
120
135
|
...(allowPrivateHosts ? { allowPrivateHosts: true } : {}),
|
|
121
136
|
});
|
|
122
137
|
const index = await akmIndex({ stashDir });
|
|
@@ -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 = {}) {
|
|
@@ -60,7 +60,12 @@ export async function akmTasksAdd(input, deps = {}) {
|
|
|
60
60
|
if (!isWithin(assetPath, typeRoot)) {
|
|
61
61
|
throw new UsageError(`Resolved task path escapes the stash: "${id}".`, "PATH_ESCAPE_VIOLATION");
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
// Pre-0.8.0 tasks were markdown; the 0.8.0 cutover moved them to pure YAML
|
|
64
|
+
// (see the tasks dir rule in src/indexer/walk/matchers.ts). A leftover
|
|
65
|
+
// `<id>.md` still names the same task, so creating `<id>.yml` beside it
|
|
66
|
+
// must collide loudly rather than silently minting a duplicate.
|
|
67
|
+
const legacyAssetPath = path.join(typeRoot, `${id}.md`);
|
|
68
|
+
if ((fs.existsSync(assetPath) || fs.existsSync(legacyAssetPath)) && !input.force) {
|
|
64
69
|
throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or delete its file and run \`akm task sync\` first.`, "RESOURCE_ALREADY_EXISTS");
|
|
65
70
|
}
|
|
66
71
|
const yaml = renderTaskYaml({
|
|
@@ -387,7 +392,9 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
387
392
|
}
|
|
388
393
|
}
|
|
389
394
|
const bindings = groupInstalledBindings(installed, invocation);
|
|
390
|
-
|
|
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());
|
|
391
398
|
const defaultEngine = config.defaults?.engine;
|
|
392
399
|
const engines = Object.keys(config.engines ?? {});
|
|
393
400
|
// §6.1: surface the effective triage settings for the default improve
|
|
@@ -408,12 +415,6 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
408
415
|
configKey: IMPROVE_AUTONOMY_CONFIG_KEY,
|
|
409
416
|
gatedLanes: allGated.map((entry) => ({ lane: entry.lane, reason: entry.reason })),
|
|
410
417
|
};
|
|
411
|
-
// Q-05 — report the workflow engine gate's state alongside the autonomy
|
|
412
|
-
// gate: both are `experimental.*` opt-ins doctor exists to surface.
|
|
413
|
-
const workflowEngine = {
|
|
414
|
-
enabled: isWorkflowEngineEnabled(config),
|
|
415
|
-
configKey: WORKFLOW_ENGINE_CONFIG_KEY,
|
|
416
|
-
};
|
|
417
418
|
const triage = effectiveStrategy.processes?.triage;
|
|
418
419
|
const improveTriage = triage
|
|
419
420
|
? {
|
|
@@ -437,7 +438,6 @@ export async function akmTasksDoctor(deps = {}) {
|
|
|
437
438
|
scheduleSubset: SCHEDULE_SUPPORTED_SUBSET_HINT,
|
|
438
439
|
warnings,
|
|
439
440
|
improveAutonomy,
|
|
440
|
-
workflowEngine,
|
|
441
441
|
...(improveTriage ? { improveTriage } : {}),
|
|
442
442
|
};
|
|
443
443
|
}
|
|
@@ -2,111 +2,19 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
* `akm workflow` command family.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* which emits the same JSON envelope (stdout/stderr/exit-code) as the inline
|
|
10
|
-
* form.
|
|
11
|
-
*
|
|
12
|
-
* 0.9.0 CLI overhaul (S5): `workflow template` is dropped — `workflow create
|
|
13
|
-
* --print` prints the same content without writing. `workflow validate` is
|
|
14
|
-
* dropped — `akm lint --type workflows` covers structural validation.
|
|
15
|
-
* `workflow watch` is dropped — poll `akm log --since '@offset:<id>' --run
|
|
16
|
-
* <run-id>` instead. Workflows are markdown-only (workflow-format-
|
|
17
|
-
* unification) — `workflow create <name>.yaml` is a usage error.
|
|
5
|
+
* `akm workflow` command family. `run` is the canonical start/resume/execute
|
|
6
|
+
* surface; the former public `start`, `next`, and `complete` lifecycle is gone.
|
|
7
|
+
* Workflows are markdown-only; authoring uses `create --print` and validation
|
|
8
|
+
* uses `akm lint --type workflows`.
|
|
18
9
|
*/
|
|
19
|
-
import { getParsedInvocation } from "../cli/invocation.js";
|
|
20
10
|
import { getStringArg } from "../cli/parse-args.js";
|
|
21
|
-
import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
|
|
11
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
|
|
22
12
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
|
|
23
|
-
import { loadConfig } from "../core/config/config.js";
|
|
24
13
|
import { NotFoundError, UsageError } from "../core/errors.js";
|
|
25
14
|
import { akmIndex } from "../indexer/indexer.js";
|
|
26
15
|
import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { abandonWorkflowRun, completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, hasWorkflowRun, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "../workflows/runtime/runs.js";
|
|
30
|
-
const workflowStartCommand = defineJsonCommand({
|
|
31
|
-
meta: {
|
|
32
|
-
name: "start",
|
|
33
|
-
description: "Start a new workflow run in the current working scope",
|
|
34
|
-
},
|
|
35
|
-
args: {
|
|
36
|
-
ref: { type: "positional", description: "Workflow ref (workflows/<name>)", required: true },
|
|
37
|
-
params: { type: "string", description: "Workflow parameters as a JSON object" },
|
|
38
|
-
force: {
|
|
39
|
-
type: "boolean",
|
|
40
|
-
description: "Allow a parallel run when an active run already exists in this scope",
|
|
41
|
-
default: false,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
async run({ args }) {
|
|
45
|
-
const result = await startWorkflowRun(args.ref, parseWorkflowJsonObject(args.params, "--params"), {
|
|
46
|
-
force: args.force === true,
|
|
47
|
-
});
|
|
48
|
-
output("workflow-start", result);
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
const workflowNextCommand = defineJsonCommand({
|
|
52
|
-
meta: {
|
|
53
|
-
name: "next",
|
|
54
|
-
description: "Show the next actionable workflow step in the current scope, auto-starting a run when passed a workflow ref",
|
|
55
|
-
},
|
|
56
|
-
args: {
|
|
57
|
-
target: { type: "positional", description: "Workflow run id or workflow ref", required: true },
|
|
58
|
-
params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
|
|
59
|
-
},
|
|
60
|
-
async run({ args }) {
|
|
61
|
-
// `--dry-run` is intentionally NOT a declared arg (so it stays out of
|
|
62
|
-
// --help). The guard reads it straight from the invocation singleton so
|
|
63
|
-
// existing callers still get a clear, actionable error instead of a
|
|
64
|
-
// generic "unknown flag" from citty.
|
|
65
|
-
if (getParsedInvocation().hasFlag("--dry-run")) {
|
|
66
|
-
throw new UsageError("`akm workflow next` does not support --dry-run. Remove the flag to start or resume a run.", "INVALID_FLAG_VALUE");
|
|
67
|
-
}
|
|
68
|
-
const parsedParams = args.params ? parseWorkflowJsonObject(args.params, "--params") : undefined;
|
|
69
|
-
const result = await getNextWorkflowStep(args.target, parsedParams);
|
|
70
|
-
output("workflow-next", result);
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
const workflowCompleteCommand = defineJsonCommand({
|
|
74
|
-
meta: {
|
|
75
|
-
name: "complete",
|
|
76
|
-
description: "Update a workflow step state and persist notes/evidence",
|
|
77
|
-
},
|
|
78
|
-
args: {
|
|
79
|
-
runId: { type: "positional", description: "Workflow run id", required: true },
|
|
80
|
-
step: { type: "string", description: "Workflow step id", required: true },
|
|
81
|
-
state: {
|
|
82
|
-
type: "string",
|
|
83
|
-
description: `Step state (default: completed). One of: ${WORKFLOW_STEP_STATES.join(", ")}.`,
|
|
84
|
-
},
|
|
85
|
-
notes: { type: "string", description: "Notes for the completed step" },
|
|
86
|
-
summary: {
|
|
87
|
-
type: "string",
|
|
88
|
-
description: "Summary of work done (required when completing a step); validated against completion criteria",
|
|
89
|
-
},
|
|
90
|
-
evidence: { type: "string", description: "Evidence JSON object for the step" },
|
|
91
|
-
},
|
|
92
|
-
async run({ args }) {
|
|
93
|
-
const result = await completeWorkflowStep({
|
|
94
|
-
runId: args.runId,
|
|
95
|
-
stepId: args.step,
|
|
96
|
-
status: parseWorkflowStepState(args.state),
|
|
97
|
-
notes: args.notes,
|
|
98
|
-
summary: args.summary,
|
|
99
|
-
evidence: args.evidence ? parseWorkflowJsonObject(args.evidence, "--evidence") : undefined,
|
|
100
|
-
});
|
|
101
|
-
if ("ok" in result && result.ok === false) {
|
|
102
|
-
// Summary failed the completion-criteria validation gate (#506): the
|
|
103
|
-
// step stays pending and the agent receives corrective feedback.
|
|
104
|
-
output("workflow-complete-rejected", result);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
output("workflow-complete", result);
|
|
108
|
-
},
|
|
109
|
-
});
|
|
16
|
+
import { WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
|
|
17
|
+
import { abandonWorkflowRun, getWorkflowStatus, hasWorkflowRun, listWorkflowRuns, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
|
|
110
18
|
const workflowStatusCommand = defineJsonCommand({
|
|
111
19
|
meta: {
|
|
112
20
|
name: "status",
|
|
@@ -219,7 +127,7 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
219
127
|
from: args.from,
|
|
220
128
|
force: args.force,
|
|
221
129
|
});
|
|
222
|
-
// Index the newly-written workflow so `akm workflow
|
|
130
|
+
// Index the newly-written workflow so `akm workflow run` can resolve
|
|
223
131
|
// a workflowEntryId without requiring an explicit `akm index` call
|
|
224
132
|
// first. Uses the same incremental index path that `akm add` uses.
|
|
225
133
|
await akmIndex({ stashDir: result.stashDir });
|
|
@@ -229,177 +137,148 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
229
137
|
const workflowRunCommand = defineJsonCommand({
|
|
230
138
|
meta: {
|
|
231
139
|
name: "run",
|
|
232
|
-
description: "
|
|
233
|
-
"engine — akm dispatches each step's units (fan-out, schema output) to the configured runner and advances " +
|
|
234
|
-
"the run through the normal completion gates",
|
|
140
|
+
description: "Start or resume a workflow and execute it through completion, failure, a verification gate, or an explicit limit",
|
|
235
141
|
},
|
|
236
142
|
args: {
|
|
237
143
|
target: { type: "positional", description: "Workflow run id or workflow ref (auto-starts a run)", required: true },
|
|
238
|
-
params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
|
|
239
144
|
"max-steps": { type: "string", description: "Stop after executing this many steps" },
|
|
145
|
+
"max-retries": { type: "string", description: "Retry a failed workflow step this many additional times" },
|
|
146
|
+
timeout: { type: "string", description: "Whole-run timeout: N, Nms, Ns, or Nm (bare N is milliseconds)" },
|
|
240
147
|
},
|
|
241
|
-
async run({ args }) {
|
|
242
|
-
requireWorkflowEngineEnabled(loadConfig(), "run");
|
|
148
|
+
async run({ args, rawArgs }) {
|
|
243
149
|
const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
150
|
+
const parameterFlags = parseWorkflowParameterFlags(rawArgs, args.target);
|
|
151
|
+
const maxSteps = parseIntegerFlag(getStringArg(args, "max-steps"), "--max-steps", 1);
|
|
152
|
+
const maxRetries = parseIntegerFlag(getStringArg(args, "max-retries"), "--max-retries", 0, WORKFLOW_MAX_RETRIES);
|
|
153
|
+
const timeoutMs = parseWorkflowTimeout(getStringArg(args, "timeout"));
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
let timedOut = false;
|
|
156
|
+
let signalExitCode;
|
|
157
|
+
const interrupt = (signal) => {
|
|
158
|
+
signalExitCode = signal === "SIGINT" ? 130 : 143;
|
|
159
|
+
controller.abort(new Error(`Workflow run interrupted by ${signal}.`));
|
|
160
|
+
};
|
|
161
|
+
const onSigint = () => interrupt("SIGINT");
|
|
162
|
+
const onSigterm = () => interrupt("SIGTERM");
|
|
163
|
+
process.once("SIGINT", onSigint);
|
|
164
|
+
process.once("SIGTERM", onSigterm);
|
|
165
|
+
const timer = timeoutMs === undefined
|
|
166
|
+
? undefined
|
|
167
|
+
: setTimeout(() => {
|
|
168
|
+
timedOut = true;
|
|
169
|
+
controller.abort(new Error(`Workflow run timed out after ${timeoutMs}ms.`));
|
|
170
|
+
}, timeoutMs);
|
|
171
|
+
timer?.unref?.();
|
|
172
|
+
try {
|
|
173
|
+
const result = await runWorkflowSteps({
|
|
174
|
+
target: args.target,
|
|
175
|
+
parameterFlags,
|
|
176
|
+
...(maxSteps !== undefined ? { maxSteps } : {}),
|
|
177
|
+
...(maxRetries !== undefined ? { maxRetries } : {}),
|
|
178
|
+
signal: controller.signal,
|
|
179
|
+
});
|
|
180
|
+
const rendered = { ...result, ...(timedOut ? { timedOut: true } : {}) };
|
|
181
|
+
output("workflow-run", rendered);
|
|
182
|
+
if (result.run.status === "failed" || result.gateRejection || result.aborted) {
|
|
183
|
+
process.exitCode = signalExitCode ?? EXIT_CODES.GENERAL;
|
|
250
184
|
}
|
|
251
185
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
const workflowBriefCommand = defineJsonCommand({
|
|
261
|
-
meta: {
|
|
262
|
-
name: "brief",
|
|
263
|
-
description: "EXPERIMENTAL, gated behind `experimental.workflowEngine`: describe a run's active step as an executable " +
|
|
264
|
-
"work-list for ANY agent session (the harness-neutral driver protocol) — read-only, takes no engine lease, " +
|
|
265
|
-
"mutates nothing; prints per-unit instructions, output schema, env binding names, and the exact " +
|
|
266
|
-
"`akm workflow report` command lines",
|
|
267
|
-
},
|
|
268
|
-
args: {
|
|
269
|
-
target: {
|
|
270
|
-
type: "positional",
|
|
271
|
-
description: "Workflow run id (or a workflow ref with an active run)",
|
|
272
|
-
required: true,
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
async run({ args }) {
|
|
276
|
-
requireWorkflowEngineEnabled(loadConfig(), "brief");
|
|
277
|
-
const { buildWorkflowBrief } = await import("../workflows/exec/brief.js");
|
|
278
|
-
const result = await buildWorkflowBrief(args.target);
|
|
279
|
-
output("workflow-brief", result);
|
|
186
|
+
finally {
|
|
187
|
+
if (timer)
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
process.off("SIGINT", onSigint);
|
|
190
|
+
process.off("SIGTERM", onSigterm);
|
|
191
|
+
}
|
|
280
192
|
},
|
|
281
193
|
});
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
type: "string",
|
|
310
|
-
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)",
|
|
311
|
-
},
|
|
312
|
-
status: { type: "string", description: `Unit status: ${WORKFLOW_REPORT_STATES.join(", ")}` },
|
|
313
|
-
result: { type: "string", description: "Result payload (JSON for a schema unit, else text). completed only." },
|
|
314
|
-
"result-file": { type: "string", description: "Read the result payload from this file instead of --result/stdin" },
|
|
315
|
-
tokens: { type: "string", description: "Tokens spent on this unit (counts against a declared budget)" },
|
|
316
|
-
"session-id": { type: "string", description: "Harness-native session id revealed while executing the unit" },
|
|
317
|
-
"failure-reason": { type: "string", description: "Structured failure vocabulary for a --status failed report" },
|
|
318
|
-
note: { type: "string", description: "Short progress note for a --status running heartbeat (not persisted)" },
|
|
319
|
-
rerun: {
|
|
320
|
-
type: "boolean",
|
|
321
|
-
description: "Re-run an already-FAILED unit: record a NEW attempt (re-applies budget) instead of refusing a differing re-report",
|
|
322
|
-
default: false,
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
async run({ args }) {
|
|
326
|
-
requireWorkflowEngineEnabled(loadConfig(), "report");
|
|
327
|
-
// --settle: the unit-less verb that advances a run parked on a
|
|
328
|
-
// non-dispatching step. Mutually exclusive with the per-unit report flags.
|
|
329
|
-
if (args.settle === true) {
|
|
330
|
-
if (getStringArg(args, "unit") !== undefined || getStringArg(args, "status") !== undefined) {
|
|
331
|
-
throw new UsageError("--settle advances a route-only/empty step and takes no --unit or --status. Drop them, or report a " +
|
|
332
|
-
"specific unit with `--unit <id> --status <state>` instead.", "INVALID_FLAG_VALUE");
|
|
194
|
+
const WORKFLOW_RUN_VALUE_FLAGS = new Set([
|
|
195
|
+
"max-steps",
|
|
196
|
+
"maxSteps",
|
|
197
|
+
"max-retries",
|
|
198
|
+
"maxRetries",
|
|
199
|
+
"timeout",
|
|
200
|
+
"format",
|
|
201
|
+
"detail",
|
|
202
|
+
"shape",
|
|
203
|
+
"output",
|
|
204
|
+
]);
|
|
205
|
+
const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set(["quiet", "verbose", "help", "no-quiet", "no-verbose"]);
|
|
206
|
+
export function parseWorkflowParameterFlags(rawArgs, target) {
|
|
207
|
+
const flags = [];
|
|
208
|
+
let targetSeen = false;
|
|
209
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
210
|
+
const token = rawArgs[index];
|
|
211
|
+
if (token === "--") {
|
|
212
|
+
throw new UsageError("`akm workflow run` does not accept positional arguments after `--`.", "INVALID_FLAG_VALUE");
|
|
213
|
+
}
|
|
214
|
+
if (!token.startsWith("-") || token === "-" || /^-\d/.test(token)) {
|
|
215
|
+
if (!targetSeen) {
|
|
216
|
+
if (token !== target) {
|
|
217
|
+
throw new UsageError("Workflow parameter flags must come after the workflow ref or run id.", "INVALID_FLAG_VALUE");
|
|
218
|
+
}
|
|
219
|
+
targetSeen = true;
|
|
220
|
+
continue;
|
|
333
221
|
}
|
|
334
|
-
|
|
335
|
-
const result = await settleWorkflowSpine({
|
|
336
|
-
target: args.target,
|
|
337
|
-
...(getStringArg(args, "expect-step") !== undefined ? { expectStep: getStringArg(args, "expect-step") } : {}),
|
|
338
|
-
});
|
|
339
|
-
output("workflow-report", result);
|
|
340
|
-
return;
|
|
222
|
+
throw new UsageError(`Unexpected positional workflow argument "${token}".`, "INVALID_FLAG_VALUE");
|
|
341
223
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
224
|
+
if (!token.startsWith("--"))
|
|
225
|
+
continue;
|
|
226
|
+
const body = token.slice(2);
|
|
227
|
+
const equalsAt = body.indexOf("=");
|
|
228
|
+
const name = equalsAt === -1 ? body : body.slice(0, equalsAt);
|
|
229
|
+
const inlineValue = equalsAt === -1 ? undefined : body.slice(equalsAt + 1);
|
|
230
|
+
if (name === "params") {
|
|
231
|
+
throw new UsageError("--params was removed. Pass each declared workflow parameter as its own flag, for example `--version=1.2.3`.", "INVALID_FLAG_VALUE");
|
|
345
232
|
}
|
|
346
|
-
if (
|
|
347
|
-
|
|
233
|
+
if (WORKFLOW_RUN_VALUE_FLAGS.has(name)) {
|
|
234
|
+
if (inlineValue === undefined)
|
|
235
|
+
index += 1;
|
|
236
|
+
continue;
|
|
348
237
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
238
|
+
if (WORKFLOW_RUN_BOOLEAN_FLAGS.has(name))
|
|
239
|
+
continue;
|
|
240
|
+
if (!targetSeen) {
|
|
241
|
+
throw new UsageError("Workflow parameter flags must come after the workflow ref or run id.", "INVALID_FLAG_VALUE");
|
|
352
242
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
tokens = Number.parseInt(rawTokens, 10);
|
|
357
|
-
if (!/^\d+$/.test(rawTokens)) {
|
|
358
|
-
throw new UsageError(`--tokens must be a non-negative integer, got "${rawTokens}".`, "INVALID_FLAG_VALUE");
|
|
359
|
-
}
|
|
243
|
+
if (inlineValue !== undefined) {
|
|
244
|
+
flags.push({ name, value: inlineValue });
|
|
245
|
+
continue;
|
|
360
246
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const resultFile = getStringArg(args, "result-file");
|
|
366
|
-
if (args.result !== undefined && resultFile !== undefined) {
|
|
367
|
-
throw new UsageError("Pass at most one of --result or --result-file.", "INVALID_FLAG_VALUE");
|
|
368
|
-
}
|
|
369
|
-
if (args.result !== undefined) {
|
|
370
|
-
resultRaw = String(args.result);
|
|
371
|
-
}
|
|
372
|
-
else if (resultFile !== undefined) {
|
|
373
|
-
const fs = await import("node:fs");
|
|
374
|
-
resultRaw = fs.readFileSync(resultFile, "utf8");
|
|
375
|
-
}
|
|
376
|
-
else if (!process.stdin.isTTY) {
|
|
377
|
-
resultRaw = await readStdin();
|
|
378
|
-
}
|
|
247
|
+
const next = rawArgs[index + 1];
|
|
248
|
+
if (next !== undefined && (!next.startsWith("-") || /^-\d/.test(next))) {
|
|
249
|
+
flags.push({ name, value: next });
|
|
250
|
+
index += 1;
|
|
379
251
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
252
|
+
else {
|
|
253
|
+
flags.push({ name, value: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return flags;
|
|
257
|
+
}
|
|
258
|
+
function parseIntegerFlag(raw, name, minimum, maximum) {
|
|
259
|
+
if (raw === undefined)
|
|
260
|
+
return undefined;
|
|
261
|
+
const value = Number.parseInt(raw, 10);
|
|
262
|
+
if (!/^\d+$/.test(raw) || value < minimum || (maximum !== undefined && value > maximum)) {
|
|
263
|
+
const range = maximum === undefined ? `at least ${minimum}` : `from ${minimum} through ${maximum}`;
|
|
264
|
+
throw new UsageError(`${name} must be an integer ${range}, got "${raw}".`, "INVALID_FLAG_VALUE");
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
function parseWorkflowTimeout(raw) {
|
|
269
|
+
if (raw === undefined)
|
|
270
|
+
return undefined;
|
|
271
|
+
const match = /^(\d+)(ms|s|m)?$/.exec(raw);
|
|
272
|
+
if (!match) {
|
|
273
|
+
throw new UsageError(`--timeout must be N, Nms, Ns, or Nm, got "${raw}".`, "INVALID_FLAG_VALUE");
|
|
274
|
+
}
|
|
275
|
+
const amount = Number(match[1]);
|
|
276
|
+
const multiplier = match[2] === "m" ? 60_000 : match[2] === "s" ? 1_000 : 1;
|
|
277
|
+
const timeoutMs = amount * multiplier;
|
|
278
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > WORKFLOW_MAX_TIMEOUT_MS) {
|
|
279
|
+
throw new UsageError(`--timeout must resolve to 1 through ${WORKFLOW_MAX_TIMEOUT_MS} milliseconds, got "${raw}".`, "INVALID_FLAG_VALUE");
|
|
280
|
+
}
|
|
281
|
+
return timeoutMs;
|
|
403
282
|
}
|
|
404
283
|
const workflowAbandonCommand = defineJsonCommand({
|
|
405
284
|
meta: {
|
|
@@ -433,17 +312,12 @@ export const workflowCommand = defineGroupCommand({
|
|
|
433
312
|
description: "Author, inspect, and execute step-by-step workflow assets",
|
|
434
313
|
},
|
|
435
314
|
subCommands: {
|
|
436
|
-
start: workflowStartCommand,
|
|
437
|
-
next: workflowNextCommand,
|
|
438
|
-
complete: workflowCompleteCommand,
|
|
439
315
|
status: workflowStatusCommand,
|
|
440
316
|
list: workflowListCommand,
|
|
441
317
|
create: workflowCreateCommand,
|
|
442
318
|
resume: workflowResumeCommand,
|
|
443
319
|
abandon: workflowAbandonCommand,
|
|
444
320
|
run: workflowRunCommand,
|
|
445
|
-
brief: workflowBriefCommand,
|
|
446
|
-
report: workflowReportCommand,
|
|
447
321
|
},
|
|
448
322
|
// No `defaultRun`: bare `akm workflow` is a usage error (exit 2), the
|
|
449
323
|
// canonical bare-group behavior — owner ruling 12. Run `akm workflow list
|