akm-cli 0.9.7 → 0.9.8-beta.2
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 +310 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/data-dir-usage.js +165 -0
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +18 -37
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +7 -19
- package/dist/commands/improve/preparation.js +52 -36
- package/dist/commands/improve/reflect.js +13 -17
- package/dist/commands/lint/base-linter.js +42 -2
- package/dist/commands/lint/index.js +4 -5
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +4 -18
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/commands/workflow-cli.js +11 -61
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +1 -3
- package/dist/core/asset/frontmatter.js +96 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-io.js +21 -8
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +74 -2
- package/dist/core/state-db.js +2 -1
- package/dist/core/warn.js +15 -0
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +43 -22
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/dir-staleness.js +30 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +179 -248
- package/dist/scripts/akm-migrate.js +179 -248
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-entries-repository.js +24 -28
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +16 -1
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/proposals-repository.js +4 -1
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +15 -0
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/task-to-v3.js +10 -2
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/migration/v0.9.1-to-v0.9.2.md +18 -4
- package/docs/reference/cli.md +3 -1
- package/docs/reference/data-and-telemetry.md +1 -0
- package/docs/reference/tasks.md +11 -0
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -9
- package/dist/workflows/source-ir/compare.js +0 -17
|
@@ -25,6 +25,7 @@ import fs from "node:fs";
|
|
|
25
25
|
import os from "node:os";
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import launchdTemplate from "../../assets/backends/launchd-template.xml" with { type: "text" };
|
|
28
|
+
import { hasErrnoCode } from "../../core/common.js";
|
|
28
29
|
import { ConfigError } from "../../core/errors.js";
|
|
29
30
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
30
31
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
@@ -851,22 +852,6 @@ export function parseLaunchdLoadedLabels(output) {
|
|
|
851
852
|
}
|
|
852
853
|
return labels;
|
|
853
854
|
}
|
|
854
|
-
function countCharacter(value, needle) {
|
|
855
|
-
let count = 0;
|
|
856
|
-
for (const character of value)
|
|
857
|
-
if (character === needle)
|
|
858
|
-
count += 1;
|
|
859
|
-
return count;
|
|
860
|
-
}
|
|
861
|
-
function hasUnsafeLaunchdControlCharacter(output) {
|
|
862
|
-
for (let index = 0; index < output.length; index += 1) {
|
|
863
|
-
const code = output.charCodeAt(index);
|
|
864
|
-
if (code <= 8 || code === 11 || code === 12 || (code >= 14 && code <= 31) || (code >= 127 && code <= 159)) {
|
|
865
|
-
return true;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
return false;
|
|
869
|
-
}
|
|
870
855
|
function readDisabledLabels(exec) {
|
|
871
856
|
try {
|
|
872
857
|
const result = exec.run(["launchctl", "print-disabled", `gui/${exec.uid()}`]);
|
|
@@ -933,8 +918,13 @@ function defaultLaunchdFs() {
|
|
|
933
918
|
try {
|
|
934
919
|
return fs.readdirSync(dir);
|
|
935
920
|
}
|
|
936
|
-
catch {
|
|
937
|
-
|
|
921
|
+
catch (error) {
|
|
922
|
+
// Genuinely absent is an empty listing; anything else (e.g. EACCES) is
|
|
923
|
+
// not "no plists" — surfacing it as such could make inspectBindings
|
|
924
|
+
// think there is nothing to reconcile.
|
|
925
|
+
if (hasErrnoCode(error, "ENOENT"))
|
|
926
|
+
return [];
|
|
927
|
+
throw new ConfigError(`Unable to read LaunchAgents directory at "${dir}".`, "INVALID_CONFIG_FILE");
|
|
938
928
|
}
|
|
939
929
|
},
|
|
940
930
|
exists(file) {
|
|
@@ -152,7 +152,22 @@ function samePath(left, right) {
|
|
|
152
152
|
};
|
|
153
153
|
return normalize(left) === normalize(right);
|
|
154
154
|
}
|
|
155
|
+
// #901: the global root can't change within a process lifetime, and every
|
|
156
|
+
// `resolveAkmInvocation()` call in a scheduler-sync process re-derives the
|
|
157
|
+
// same `nodePath` — so this spawns `npm root --global` at most once per
|
|
158
|
+
// process instead of once per call. Keyed by nodePath (rather than a bare
|
|
159
|
+
// once-only flag) so a differently-invoked probe later in the same process
|
|
160
|
+
// still gets its own answer instead of a stale one.
|
|
161
|
+
let cachedNpmGlobalRoot;
|
|
155
162
|
function resolveNpmGlobalRoot(nodePath, env) {
|
|
163
|
+
if (cachedNpmGlobalRoot && cachedNpmGlobalRoot.nodePath === nodePath) {
|
|
164
|
+
return cachedNpmGlobalRoot.value;
|
|
165
|
+
}
|
|
166
|
+
const value = resolveNpmGlobalRootUncached(nodePath, env);
|
|
167
|
+
cachedNpmGlobalRoot = { nodePath, value };
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
function resolveNpmGlobalRootUncached(nodePath, env) {
|
|
156
171
|
const npmCli = resolveAssociatedNpmCli(nodePath);
|
|
157
172
|
if (!npmCli)
|
|
158
173
|
return undefined;
|
|
@@ -14,20 +14,15 @@
|
|
|
14
14
|
*
|
|
15
15
|
* D8 result-vocabulary re-code (why, and the WRITE side's exact shape): see
|
|
16
16
|
* docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md.
|
|
17
|
-
* The READ side
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* "shell"/"script"/"prompt" written WITHOUT a marker, which no production
|
|
27
|
-
* writer ever does) -> `"unknown"`. The P0-pinned null fallbacks survive:
|
|
28
|
-
* workflow `ref` falls back to `""`, the command/prompt arm's `engine`
|
|
29
|
-
* falls back to `null`. A row carrying `targetVocab: 2` reads `target_kind`
|
|
30
|
-
* directly in the current vocabulary — no mapping needed.
|
|
17
|
+
* The READ side used to carry a permanent legacy-vocabulary mapping here
|
|
18
|
+
* (row B-51 of docs/plans/specs/p4-deletions-closeout.md called deleting it
|
|
19
|
+
* "review-blocking") — SUPERSEDED: `task_history` is DB-owned data, so the
|
|
20
|
+
* remap is now a one-time schema migration
|
|
21
|
+
* (`025-task-history-vocabulary-backfill` in src/core/state/migrations.ts)
|
|
22
|
+
* instead of a read-side shim run forever. Every row this function sees now
|
|
23
|
+
* carries the current vocabulary's `target_kind` strings; the P0-pinned null
|
|
24
|
+
* fallbacks still apply (workflow `ref` falls back to `""`, the command
|
|
25
|
+
* arm's `engine` falls back to `null`).
|
|
31
26
|
*
|
|
32
27
|
* A DAG leaf with respect to the rest of src/tasks/run/**: this module
|
|
33
28
|
* imports TaskRunResult/TaskRunStatus's TYPE from ./task-result but no VALUE
|
|
@@ -37,6 +32,7 @@
|
|
|
37
32
|
*/
|
|
38
33
|
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
39
34
|
import { withStateDb } from "../../core/state-db.js";
|
|
35
|
+
import { warn } from "../../core/warn.js";
|
|
40
36
|
import { decodeTaskHistoryMetadata, finalizeTaskHistoryAttempt, getTaskHistory, getTaskHistoryRuns, queryTaskHistory, upsertTaskHistory, } from "../../storage/repositories/task-history-repository.js";
|
|
41
37
|
/** Append (or finalize a reserved attempt into) one task_history row. */
|
|
42
38
|
export function appendHistory(result, historyReserved = false) {
|
|
@@ -67,7 +63,11 @@ export function appendHistory(result, historyReserved = false) {
|
|
|
67
63
|
}
|
|
68
64
|
catch (error) {
|
|
69
65
|
rethrowIfTestIsolationError(error);
|
|
70
|
-
// History recording
|
|
66
|
+
// History recording must not alter the task's own result/exit status —
|
|
67
|
+
// the task already ran and its outcome is independent of whether we can
|
|
68
|
+
// persist a history row. But silence would make a failed write
|
|
69
|
+
// indistinguishable from the task never having run, so warn.
|
|
70
|
+
warn(`task history: failed to record history for task ${result.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
export function readTaskHistory(options = {}) {
|
|
@@ -111,32 +111,22 @@ function decodeTaskHistoryRows(rows) {
|
|
|
111
111
|
* Convert a `TaskHistoryRow` from state.db back to a `TaskRunResult` shape
|
|
112
112
|
* that callers of `readTaskHistory()` expect.
|
|
113
113
|
*
|
|
114
|
-
*
|
|
115
|
-
* `
|
|
114
|
+
* Reads `target_kind` directly in the current (post-D8) vocabulary — the
|
|
115
|
+
* `025-task-history-vocabulary-backfill` state migration rewrites every
|
|
116
|
+
* legacy-vocabulary row before this ever runs against it.
|
|
116
117
|
*/
|
|
117
118
|
function taskHistoryRowToResult(row) {
|
|
118
119
|
const meta = decodeTaskHistoryMetadata(row.metadata_json);
|
|
119
|
-
const marked = meta.targetVocab === 2;
|
|
120
120
|
const target = (() => {
|
|
121
121
|
switch (row.target_kind) {
|
|
122
122
|
case "workflow":
|
|
123
|
-
// PRESERVED for both vintages (incl. the null-ref fallback).
|
|
124
123
|
return { kind: "workflow", ref: row.target_ref ?? "" };
|
|
125
124
|
case "command":
|
|
126
|
-
|
|
127
|
-
// LEGACY vocabulary: the native shell/script arm's shared string.
|
|
128
|
-
return marked ? { kind: "command", engine: meta.engine ?? null } : { kind: "shell" };
|
|
125
|
+
return { kind: "command", engine: meta.engine ?? null };
|
|
129
126
|
case "shell":
|
|
130
|
-
|
|
131
|
-
// "shell" row is unreachable from any production writer.
|
|
132
|
-
return marked ? { kind: "shell" } : { kind: "unknown" };
|
|
127
|
+
return { kind: "shell" };
|
|
133
128
|
case "script":
|
|
134
|
-
|
|
135
|
-
// "script" row is unreachable from any production writer.
|
|
136
|
-
return marked ? { kind: "script" } : { kind: "unknown" };
|
|
137
|
-
case "prompt":
|
|
138
|
-
// Only LEGACY rows (pre-P1b) ever wrote this string.
|
|
139
|
-
return marked ? { kind: "unknown" } : { kind: "command", engine: meta.engine ?? null };
|
|
129
|
+
return { kind: "script" };
|
|
140
130
|
default:
|
|
141
131
|
return { kind: "unknown" };
|
|
142
132
|
}
|
|
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { loadAdapterExecutionSource } from "../commands/command/execution-source-loader.js";
|
|
8
8
|
import { makeBundleRef } from "../core/asset/asset-ref.js";
|
|
9
|
+
import { compareCodePoints, toPosix } from "../core/common.js";
|
|
9
10
|
import { UsageError } from "../core/errors.js";
|
|
10
11
|
import { canonicalizeWorkflowName, WORKFLOW_EXTENSIONS } from "../core/recognition-util.js";
|
|
11
12
|
import { captureGuardedDirectoryManifest, captureGuardedExecutionSource, GuardedExecutionSourceCollector, } from "../execution/guarded-source.js";
|
|
@@ -742,12 +743,6 @@ function freezeOperation(operation) {
|
|
|
742
743
|
function sameInvocation(left, right) {
|
|
743
744
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
744
745
|
}
|
|
745
|
-
function compareCodePoints(left, right) {
|
|
746
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
747
|
-
}
|
|
748
|
-
function toPosix(value) {
|
|
749
|
-
return value.replaceAll("\\", "/");
|
|
750
|
-
}
|
|
751
746
|
// Retain the concrete error in this module's public dependency graph so callers
|
|
752
747
|
// can continue to identify ownership failures without importing an adapter.
|
|
753
748
|
export { WorkflowSourceRejectionError };
|
|
@@ -47,6 +47,7 @@ import fs from "node:fs";
|
|
|
47
47
|
import path from "node:path";
|
|
48
48
|
import { types as utilTypes } from "node:util";
|
|
49
49
|
import { isAlias, isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
|
|
50
|
+
import { wellFormedUnicode } from "../../core/common.js";
|
|
50
51
|
import { UsageError } from "../../core/errors.js";
|
|
51
52
|
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
52
53
|
import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
|
|
@@ -67,20 +68,6 @@ export function own(value, key) {
|
|
|
67
68
|
export function utf8Bytes(value) {
|
|
68
69
|
return new TextEncoder().encode(value).byteLength;
|
|
69
70
|
}
|
|
70
|
-
export function wellFormedUnicode(value) {
|
|
71
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
72
|
-
const code = value.charCodeAt(index);
|
|
73
|
-
if (code >= 0xd800 && code <= 0xdbff) {
|
|
74
|
-
const next = value.charCodeAt(index + 1);
|
|
75
|
-
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
76
|
-
return false;
|
|
77
|
-
index += 1;
|
|
78
|
-
}
|
|
79
|
-
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
71
|
/** The one per-field error funnel both grammars render through, distinguished only by `ctx.sourceLabel` (D2-N4). */
|
|
85
72
|
export function sourceError(ctx, fieldPath, detail) {
|
|
86
73
|
const dotted = fieldPath.length === 0
|
|
@@ -112,6 +112,12 @@ function base(input) {
|
|
|
112
112
|
...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* #902: the one blocker with an unambiguous remedy. The sibling shell-safety
|
|
117
|
+
* reasons need a case-by-case judgement and stay reason-only.
|
|
118
|
+
*/
|
|
119
|
+
const ARGV_ARRAY_BLOCK_DETAIL = "Manual conversion required: an array `command:` has no safe v3 `run:` string. Rewrite it by hand as " +
|
|
120
|
+
"`run:` (string) plus `shell:` — see docs/migration/v0.9.1-to-v0.9.2.md for the full v2 to v4 field mapping.";
|
|
115
121
|
function blocked(input, reason, detail) {
|
|
116
122
|
return Object.freeze({ status: "blocked", ...base(input), reason, ...(detail ? { detail } : {}) });
|
|
117
123
|
}
|
|
@@ -389,8 +395,10 @@ export function planLegacyTaskDataToV3(input, data) {
|
|
|
389
395
|
catch (cause) {
|
|
390
396
|
return blocked(input, "invalid-v2-task", cause instanceof Error ? cause.message : String(cause));
|
|
391
397
|
}
|
|
392
|
-
if (isReason(migrated))
|
|
393
|
-
|
|
398
|
+
if (isReason(migrated)) {
|
|
399
|
+
const detail = migrated === "argv-array-has-no-portable-shell-string" ? ARGV_ARRAY_BLOCK_DETAIL : undefined;
|
|
400
|
+
return blocked(input, migrated, detail);
|
|
401
|
+
}
|
|
394
402
|
const after = Buffer.from(stringifyYaml(migrated), "utf8");
|
|
395
403
|
try {
|
|
396
404
|
parseTaskV3Yaml({
|
|
@@ -284,7 +284,7 @@ async function driveChildRun(input, childRow) {
|
|
|
284
284
|
* before step 6 (publication) produces `child_workflow_publish_failed`.
|
|
285
285
|
*/
|
|
286
286
|
export async function driveChildWorkflowUnit(input) {
|
|
287
|
-
const { request,
|
|
287
|
+
const { request, ctx } = input;
|
|
288
288
|
const precheck = precheckAndDeriveInvocationKey(input);
|
|
289
289
|
if (!precheck.ok) {
|
|
290
290
|
return precheck.outcome;
|
|
@@ -708,7 +708,7 @@ async function prepareAttemptWorktree(input) {
|
|
|
708
708
|
}
|
|
709
709
|
return { ok: true, request: { ...input.request, cwd: created.path }, worktreePath: created.path };
|
|
710
710
|
}
|
|
711
|
-
async function reserveJournaledDispatch(input,
|
|
711
|
+
async function reserveJournaledDispatch(input, worktreePath, startedAt) {
|
|
712
712
|
const { plan, workUnit, ctx, attemptId, inputHash } = input;
|
|
713
713
|
let durableAttempt;
|
|
714
714
|
await enqueueUnitWrite(async () => {
|
|
@@ -799,7 +799,7 @@ async function dispatchJournaledAttempt(input) {
|
|
|
799
799
|
const startedAt = new Date().toISOString();
|
|
800
800
|
let durableAttempt;
|
|
801
801
|
try {
|
|
802
|
-
durableAttempt = await reserveJournaledDispatch(input,
|
|
802
|
+
durableAttempt = await reserveJournaledDispatch(input, worktreePath, startedAt);
|
|
803
803
|
}
|
|
804
804
|
catch (err) {
|
|
805
805
|
// A failed dispatch-row insert means NOTHING dispatched (the row is the
|
|
@@ -20,7 +20,7 @@ import { UsageError } from "../../core/errors.js";
|
|
|
20
20
|
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
21
21
|
import { canonicalInputJson, validateInputs } from "../../execution/input-contract.js";
|
|
22
22
|
import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
23
|
-
import { canonicalJson
|
|
23
|
+
import { canonicalJson } from "../ir/plan-hash.js";
|
|
24
24
|
import { parseReference, resolveReferenceString, } from "../program/expressions.js";
|
|
25
25
|
import { clip, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
|
|
26
26
|
import { completeWorkflowStep, isTruncatedEvidence, } from "../runtime/runs.js";
|
|
@@ -463,7 +463,7 @@ function buildExecContextEnv(args) {
|
|
|
463
463
|
function computeUnitInputHash(ctx, item) {
|
|
464
464
|
return createHash("sha256")
|
|
465
465
|
.update("akm.workflow.unit\0v7\0")
|
|
466
|
-
.update(
|
|
466
|
+
.update(canonicalJson({
|
|
467
467
|
hashVersion: 7,
|
|
468
468
|
role: "unit",
|
|
469
469
|
stepId: ctx.plan.stepId,
|
|
@@ -855,20 +855,8 @@ export function unitOutcomeFromRow(unitId, row, hasSchema) {
|
|
|
855
855
|
...(row.tokens !== null ? { tokens: row.tokens } : {}),
|
|
856
856
|
};
|
|
857
857
|
}
|
|
858
|
-
/**
|
|
859
|
-
export
|
|
860
|
-
return JSON.stringify(sortKeys(value));
|
|
861
|
-
}
|
|
862
|
-
function sortKeys(value) {
|
|
863
|
-
if (Array.isArray(value))
|
|
864
|
-
return value.map(sortKeys);
|
|
865
|
-
if (value && typeof value === "object") {
|
|
866
|
-
return Object.fromEntries(Object.entries(value)
|
|
867
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
868
|
-
.map(([k, v]) => [k, sortKeys(v)]));
|
|
869
|
-
}
|
|
870
|
-
return value;
|
|
871
|
-
}
|
|
858
|
+
/** Re-exported so existing importers (`tests/workflows/fuzz/*`) keep resolving; canonical impl lives in `../ir/plan-hash`. */
|
|
859
|
+
export { canonicalJson };
|
|
872
860
|
// ── Gate-feedback recovery (PURE) ────────────────────────────────────────────
|
|
873
861
|
//
|
|
874
862
|
// A gate rejection is journaled as `<stepId>.gate:l<loop>` with result_json
|
|
@@ -1431,7 +1419,7 @@ export async function finalizeExecutedStep(input) {
|
|
|
1431
1419
|
// fields are unchanged).
|
|
1432
1420
|
inputHash: createHash("sha256")
|
|
1433
1421
|
.update("akm.workflow.gate\0v7\0")
|
|
1434
|
-
.update(
|
|
1422
|
+
.update(canonicalJson({
|
|
1435
1423
|
hashVersion: 7,
|
|
1436
1424
|
dispatch: gateTarget,
|
|
1437
1425
|
invocation: null,
|
|
@@ -71,7 +71,22 @@ import { isProcessAlive, isWithinAsync, safeRealpathAsync } from "../../core/com
|
|
|
71
71
|
import { serializeByKey } from "../../core/concurrent.js";
|
|
72
72
|
import { runManagedSubprocess } from "../../core/subprocess.js";
|
|
73
73
|
import { warn } from "../../core/warn.js";
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Timeout for every `git worktree add|prune|remove|status` call this module
|
|
76
|
+
* makes. Was 30s; raised to 120s after #891 confirmed a real product gap:
|
|
77
|
+
* these calls run under {@link withRepoWorktreeLock}, a per-process,
|
|
78
|
+
* per-repository queue, so a machine also busy with OTHER git-heavy work
|
|
79
|
+
* (other akm runs, other agents' worktrees, a loaded CI runner) can genuinely
|
|
80
|
+
* push one `git worktree remove` past 30s without anything being stuck —
|
|
81
|
+
* plain CPU/fork/IO contention. At 30s that showed up as `{ removed: false,
|
|
82
|
+
* error: "... timed out after 30000ms" }` on a perfectly healthy op: a clean
|
|
83
|
+
* worktree left retained, or a create failing a unit outright, purely because
|
|
84
|
+
* the box was busy. 120s matches the same trade already made for
|
|
85
|
+
* {@link GIT_PUSH_TIMEOUT_MS} (`core/write-source.ts`,
|
|
86
|
+
* `sources/providers/git-stash.ts`) for the same class of administrative git
|
|
87
|
+
* call under load; a call that is truly hung (not just slow) is still caught.
|
|
88
|
+
*/
|
|
89
|
+
const GIT_TIMEOUT_MS = 120_000;
|
|
75
90
|
/** Directory under `os.tmpdir()` that owns every run's worktree roots. */
|
|
76
91
|
export const WORKTREES_DIR_NAME = "akm-worktrees";
|
|
77
92
|
/**
|
|
@@ -89,12 +104,10 @@ function gitExitError(args, code, stderr, stdout) {
|
|
|
89
104
|
return `git ${args.join(" ")} exited ${code}${detail ? `: ${detail}` : ""}`;
|
|
90
105
|
}
|
|
91
106
|
/**
|
|
92
|
-
* Run one git command asynchronously; `ok` = exit
|
|
93
|
-
* errors and the
|
|
94
|
-
* promise instead of blocking the event loop; repo-mutating callers must hold
|
|
95
|
-
* {@link withRepoWorktreeLock}.
|
|
107
|
+
* Run one git command asynchronously via the real `git` binary; `ok` = exit
|
|
108
|
+
* 0. Never throws (spawn errors and the timeout → ok: false).
|
|
96
109
|
*/
|
|
97
|
-
async function
|
|
110
|
+
async function realGitExecutor(cwd, args) {
|
|
98
111
|
const result = await runManagedSubprocess(["git", "-C", cwd, ...args], {
|
|
99
112
|
capture: true,
|
|
100
113
|
timeoutMs: GIT_TIMEOUT_MS,
|
|
@@ -114,6 +127,27 @@ async function git(cwd, args) {
|
|
|
114
127
|
}
|
|
115
128
|
return { ok: true, stdout: result.stdout };
|
|
116
129
|
}
|
|
130
|
+
let gitExecutor = realGitExecutor;
|
|
131
|
+
/**
|
|
132
|
+
* TEST-ONLY seam (#891): swap the executor every repo-mutating git call in
|
|
133
|
+
* this module goes through. Lets a test prove those calls genuinely
|
|
134
|
+
* interleave with other event-loop work — the property the old "count
|
|
135
|
+
* setInterval ticks during real git calls" test asserted before it was
|
|
136
|
+
* deleted for timing on the real scheduler instead of on behavior — using a
|
|
137
|
+
* fake, deterministically-timed async git in place of the real subprocess.
|
|
138
|
+
* Call with `undefined` to restore the real spawn-based executor.
|
|
139
|
+
*/
|
|
140
|
+
export function setGitExecutorForTesting(executor) {
|
|
141
|
+
gitExecutor = executor ?? realGitExecutor;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Run one git command asynchronously; `ok` = exit 0. Never throws. Async so a
|
|
145
|
+
* git lock wait parks a promise instead of blocking the event loop;
|
|
146
|
+
* repo-mutating callers must hold {@link withRepoWorktreeLock}.
|
|
147
|
+
*/
|
|
148
|
+
async function git(cwd, args) {
|
|
149
|
+
return gitExecutor(cwd, args);
|
|
150
|
+
}
|
|
117
151
|
/**
|
|
118
152
|
* Synchronous git for the two read-only probes that run before any unit is in
|
|
119
153
|
* flight ({@link isGitAvailable}, {@link assertGitWorkTree}). They take no
|
|
@@ -20,15 +20,13 @@
|
|
|
20
20
|
* full design history, including why re-binding cannot reuse the
|
|
21
21
|
* shape-driven normalizer (a code-review finding).
|
|
22
22
|
*/
|
|
23
|
+
import { isRecord } from "../../core/common.js";
|
|
23
24
|
import { UsageError } from "../../core/errors.js";
|
|
24
25
|
import { validateInputs } from "../../execution/input-contract.js";
|
|
25
26
|
import { parseReference } from "../program/expressions.js";
|
|
26
27
|
function inputBindingInvalid(message) {
|
|
27
28
|
return new UsageError(message, "INPUT_BINDING_INVALID");
|
|
28
29
|
}
|
|
29
|
-
function isPlainObject(value) {
|
|
30
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
-
}
|
|
32
30
|
function unknownBindingNameError(stepId, targetRef, name, declaredNames) {
|
|
33
31
|
return inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name}, which is not a declared input. ` +
|
|
34
32
|
`Declared inputs: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}.`);
|
|
@@ -152,7 +150,7 @@ function finalizeBindings(stepId, targetRef, contract, byName) {
|
|
|
152
150
|
* reinterpreted as a literal.
|
|
153
151
|
*/
|
|
154
152
|
function normalizeOneEntry(stepId, targetRef, name, value, schema, earlierStepIds, declaredParamNames) {
|
|
155
|
-
if (!
|
|
153
|
+
if (!isRecord(value) || !Object.hasOwn(value, "from")) {
|
|
156
154
|
return Object.freeze({ kind: "literal", name, value });
|
|
157
155
|
}
|
|
158
156
|
const keys = Object.keys(value);
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* Returns accumulated `WorkflowError`s rather than throwing. Pure and
|
|
26
26
|
* deterministic: the same document always compiles to the same plan.
|
|
27
27
|
*/
|
|
28
|
+
import { compareCodePoints } from "../../core/common.js";
|
|
28
29
|
import { formatReference, parseReference } from "../program/expressions.js";
|
|
29
30
|
import { projectExecCore } from "../program/schema.js";
|
|
30
31
|
import { sourceStepInstructions, sourceStepProgramUnit, sourceStepRef } from "../source-ir/program.js";
|
|
@@ -114,9 +115,6 @@ function sortedOutputs(outputs) {
|
|
|
114
115
|
}
|
|
115
116
|
return sorted;
|
|
116
117
|
}
|
|
117
|
-
function compareCodePoints(left, right) {
|
|
118
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
119
|
-
}
|
|
120
118
|
function compileStep(step, sequenceIndex, defaults, resolved) {
|
|
121
119
|
const gate = {
|
|
122
120
|
kind: "gate",
|
|
@@ -269,14 +267,12 @@ function checkOutputReference(name, text, check) {
|
|
|
269
267
|
* (human + JSON output, via `core/adapter/adapters/akm-lint.ts#
|
|
270
268
|
* workflowCompileWarnings`) and as `warn()` lines at `workflow run`.
|
|
271
269
|
*
|
|
272
|
-
* A. A
|
|
273
|
-
* raw results as an untyped artifact — permitted, but worth flagging.
|
|
274
|
-
* B. A `params.<name>` reference (in `map.over`/`route.input`) to an
|
|
270
|
+
* A. A `params.<name>` reference (in `map.over`/`route.input`) to an
|
|
275
271
|
* UNDECLARED param, but ONLY when the document declares a `params:`
|
|
276
272
|
* block — a likely typo. Prose can no longer carry param references at
|
|
277
273
|
* all (it is never scanned), so this warning's surface shrinks to the
|
|
278
274
|
* two whole-value fields that can legally contain one.
|
|
279
|
-
*
|
|
275
|
+
* B. `gate.max_loops` above 1 on an `exec` step. The engine judges such a
|
|
280
276
|
* step but never loops it (`exec/step-work.ts#effectiveGateMaxLoops`):
|
|
281
277
|
* a frozen argv cannot read the judge's feedback, so a second loop would
|
|
282
278
|
* only re-run the identical command — and its side effects. The declared
|
|
@@ -299,13 +295,6 @@ export function collectWorkflowWarnings(input) {
|
|
|
299
295
|
`can still fail the step.`,
|
|
300
296
|
});
|
|
301
297
|
}
|
|
302
|
-
if ((step.map || step.route === undefined) && step.output === undefined) {
|
|
303
|
-
warnings.push({
|
|
304
|
-
line: step.source.start,
|
|
305
|
-
message: `Step "${step.id}" declares no \`output:\` schema — its unit results are carried as an untyped ` +
|
|
306
|
-
`artifact (permitted). Add an \`output:\` JSON Schema to type and validate the step artifact.`,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
298
|
if (declaredParams) {
|
|
310
299
|
const declaredList = [...declaredParams].join(", ");
|
|
311
300
|
const scan = (text, label) => {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import path from "node:path";
|
|
13
|
+
import { compareCodePoints } from "../../core/common.js";
|
|
13
14
|
import { UsageError } from "../../core/errors.js";
|
|
14
15
|
import { decodeFrozenExecutableIdentity } from "../../execution/executable-identity.js";
|
|
15
16
|
import { INPUT_NAME_PATTERN } from "../../execution/input-contract.js";
|
|
@@ -38,7 +39,7 @@ export const WORKFLOW_IR_V5_VERSION = 5;
|
|
|
38
39
|
* freeze-time `COMPOSITION_INVALID` gates live in
|
|
39
40
|
* `src/workflows/freeze/targets/child-workflow.ts` (Lane B).
|
|
40
41
|
*/
|
|
41
|
-
export function decodeWorkflowPlanV4(input,
|
|
42
|
+
export function decodeWorkflowPlanV4(input, depth = 0, budget = { embeddedBytes: 0 }) {
|
|
42
43
|
const raw = record(input, "plan");
|
|
43
44
|
if (raw.irVersion !== WORKFLOW_IR_V5_VERSION)
|
|
44
45
|
fail("irVersion must be 5");
|
|
@@ -51,7 +52,7 @@ export function decodeWorkflowPlanV4(input, hooks = {}, depth = 0, budget = { em
|
|
|
51
52
|
planExtraKeys: ["sourceReadSet", "outputs"],
|
|
52
53
|
unitExtraKeys: ["frozenTarget", "environment"],
|
|
53
54
|
gateExtraKeys: ["frozenJudge"],
|
|
54
|
-
}
|
|
55
|
+
});
|
|
55
56
|
const rawSteps = raw.steps;
|
|
56
57
|
const stepIds = new Set(rawSteps.map((rawStep) => rawStep.stepId));
|
|
57
58
|
const requiredSources = [];
|
|
@@ -232,7 +233,7 @@ function decodeChildWorkflowTarget(target, unit, depth, budget) {
|
|
|
232
233
|
fail(`unit ${unit.id} child workflow contentHash does not match its frozen dispatch`);
|
|
233
234
|
}
|
|
234
235
|
const childDepth = depth + 1;
|
|
235
|
-
const frozenPlan = decodeWorkflowPlanV4(target.frozenPlan,
|
|
236
|
+
const frozenPlan = decodeWorkflowPlanV4(target.frozenPlan, childDepth, budget);
|
|
236
237
|
const embeddedPlanJson = canonicalJsonLocal(frozenPlan);
|
|
237
238
|
const actualPlanHash = sha256(embeddedPlanJson);
|
|
238
239
|
if (actualPlanHash !== planHash) {
|
|
@@ -711,9 +712,6 @@ function digest(value, label) {
|
|
|
711
712
|
function sha256(value) {
|
|
712
713
|
return createHash("sha256").update(value).digest("hex");
|
|
713
714
|
}
|
|
714
|
-
function compareCodePoints(left, right) {
|
|
715
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
716
|
-
}
|
|
717
715
|
function canonicalJsonLocal(value) {
|
|
718
716
|
const sort = (entry) => {
|
|
719
717
|
if (Array.isArray(entry))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import { isContainedRelativePath } from "../../core/common.js";
|
|
4
|
+
import { isContainedRelativePath, isRecord } from "../../core/common.js";
|
|
5
5
|
import { UsageError } from "../../core/errors.js";
|
|
6
6
|
import { parseReference } from "../program/expressions.js";
|
|
7
7
|
import { PROGRAM_PARAM_NAME_PATTERN, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN } from "../program/schema.js";
|
|
@@ -13,7 +13,7 @@ export { WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_TIMEOUT_MS };
|
|
|
13
13
|
const MAX_LIST_ITEMS = 1024;
|
|
14
14
|
const MAX_STRING_LENGTH = 1_000_000;
|
|
15
15
|
/** Validate the current durable execution graph. */
|
|
16
|
-
export function validateWorkflowPlanStructure(input, options
|
|
16
|
+
export function validateWorkflowPlanStructure(input, options) {
|
|
17
17
|
if (!isRecord(input) || input.irVersion !== options.expectedVersion) {
|
|
18
18
|
fail(`irVersion must be ${options.expectedVersion}`);
|
|
19
19
|
}
|
|
@@ -351,9 +351,6 @@ function assertString(value, label) {
|
|
|
351
351
|
if (typeof value !== "string" || value.length === 0 || value.length > MAX_STRING_LENGTH)
|
|
352
352
|
fail(`${label} must be a non-empty bounded string`);
|
|
353
353
|
}
|
|
354
|
-
function isRecord(value) {
|
|
355
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
356
|
-
}
|
|
357
354
|
function fail(message) {
|
|
358
355
|
throw new UsageError(`Invalid frozen workflow plan: ${message}.`);
|
|
359
356
|
}
|