akm-cli 0.9.2-alpha.4 → 0.9.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 +493 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +8 -4
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +265 -52
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +77 -14
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +24 -7
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +3 -2
- package/docs/migration/v0.8-to-v0.9.md +13 -11
- package/docs/migration/v0.9.0-troubleshooting.md +20 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +6 -5
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +290 -16
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- package/dist/workflows/source-ir/ordering.js +0 -38
|
@@ -12,27 +12,48 @@ import { createHash } from "node:crypto";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { UsageError } from "../../core/errors.js";
|
|
14
14
|
import { decodeFrozenExecutableIdentity } from "../../execution/executable-identity.js";
|
|
15
|
+
import { INPUT_NAME_PATTERN } from "../../execution/input-contract.js";
|
|
15
16
|
import { canonicalResolvedExecutionRequest, decodeResolvedExecutionRequest, } from "../../execution/resolved-request.js";
|
|
16
17
|
import { decodeExecutionSourceIdentity } from "../../execution/source.js";
|
|
17
18
|
import { decodeFrozenRunnerSpec } from "../../integrations/agent/execution-lowering.js";
|
|
19
|
+
import { parseReference } from "../program/expressions.js";
|
|
20
|
+
import { PROGRAM_PARAM_NAME_PATTERN } from "../program/schema.js";
|
|
21
|
+
import { utf8Bytes, WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES } from "../resource-limits.js";
|
|
18
22
|
import { decodeWorkflowExecSpec, validateWorkflowPlanStructure, } from "./schema.js";
|
|
19
|
-
export const
|
|
20
|
-
/**
|
|
21
|
-
|
|
23
|
+
export const WORKFLOW_IR_V5_VERSION = 5;
|
|
24
|
+
/**
|
|
25
|
+
* Strict v4 corruption gate. No authored source or config is consulted here.
|
|
26
|
+
*
|
|
27
|
+
* `depth` is the composition-depth recursion counter (0 at the root
|
|
28
|
+
* workflow plan; +1 for every embedded child-workflow plan), and `budget` is
|
|
29
|
+
* a MUTABLE, tree-wide running total of embedded child-plan bytes charged so
|
|
30
|
+
* far. Both are internal to the recursive child-workflow decode path below
|
|
31
|
+
* (`decodeChildWorkflowTarget` calls this function again on an embedded
|
|
32
|
+
* `frozenPlan`, sharing the SAME `budget` object across the whole recursion —
|
|
33
|
+
* mirroring `ChildCompositionContext.budget` on the freeze side) — every
|
|
34
|
+
* external caller decodes a plan at the top level and relies on the
|
|
35
|
+
* defaults. Re-enforces the composition depth bound AND the aggregate
|
|
36
|
+
* embedded-bytes bound at decode as corruption gates (spec docs/plans/specs/
|
|
37
|
+
* p3a-plan-v5-child-freeze.md §3.6 step 6, rows A-23, A-N6); the actionable
|
|
38
|
+
* freeze-time `COMPOSITION_INVALID` gates live in
|
|
39
|
+
* `src/workflows/freeze/targets/child-workflow.ts` (Lane B).
|
|
40
|
+
*/
|
|
41
|
+
export function decodeWorkflowPlanV4(input, hooks = {}, depth = 0, budget = { embeddedBytes: 0 }) {
|
|
22
42
|
const raw = record(input, "plan");
|
|
23
|
-
if (raw.irVersion !==
|
|
24
|
-
fail("irVersion must be
|
|
25
|
-
assertKeys(raw, ["irVersion", "title", "params", "paramSchemas", "budget", "execution", "steps", "sourceReadSet"], "plan");
|
|
43
|
+
if (raw.irVersion !== WORKFLOW_IR_V5_VERSION)
|
|
44
|
+
fail("irVersion must be 5");
|
|
45
|
+
assertKeys(raw, ["irVersion", "title", "params", "paramSchemas", "budget", "execution", "steps", "sourceReadSet", "outputs"], "plan");
|
|
26
46
|
if (!Object.hasOwn(raw, "sourceReadSet"))
|
|
27
47
|
fail("sourceReadSet is required");
|
|
28
48
|
const sourceReadSet = decodeSourceReadSet(raw.sourceReadSet);
|
|
29
49
|
validateWorkflowPlanStructure(raw, {
|
|
30
|
-
expectedVersion:
|
|
31
|
-
planExtraKeys: ["sourceReadSet"],
|
|
50
|
+
expectedVersion: WORKFLOW_IR_V5_VERSION,
|
|
51
|
+
planExtraKeys: ["sourceReadSet", "outputs"],
|
|
32
52
|
unitExtraKeys: ["frozenTarget", "environment"],
|
|
33
53
|
gateExtraKeys: ["frozenJudge"],
|
|
34
54
|
}, hooks);
|
|
35
55
|
const rawSteps = raw.steps;
|
|
56
|
+
const stepIds = new Set(rawSteps.map((rawStep) => rawStep.stepId));
|
|
36
57
|
const requiredSources = [];
|
|
37
58
|
const steps = rawSteps.map((rawStep, index) => {
|
|
38
59
|
const step = rawStep;
|
|
@@ -46,28 +67,68 @@ export function decodeWorkflowPlanV4(input, hooks = {}) {
|
|
|
46
67
|
const { template: _template, ...map } = rawRoot;
|
|
47
68
|
return {
|
|
48
69
|
...map,
|
|
49
|
-
template: decodeUnitV4(record(rawRoot.template, `map ${String(rawRoot.id)} template`), requiredSources),
|
|
70
|
+
template: decodeUnitV4(record(rawRoot.template, `map ${String(rawRoot.id)} template`), requiredSources, depth, budget),
|
|
50
71
|
};
|
|
51
72
|
})()
|
|
52
|
-
: decodeUnitV4(rawRoot, requiredSources);
|
|
73
|
+
: decodeUnitV4(rawRoot, requiredSources, depth, budget);
|
|
53
74
|
return Object.freeze({ ...step, root, gate });
|
|
54
75
|
});
|
|
55
76
|
assertRequiredSources(sourceReadSet, requiredSources);
|
|
77
|
+
const outputs = Object.hasOwn(raw, "outputs") ? decodeWorkflowOutputs(raw.outputs, stepIds) : undefined;
|
|
78
|
+
const { outputs: _rawOutputs, ...restRaw } = raw;
|
|
56
79
|
return Object.freeze({
|
|
57
|
-
...
|
|
58
|
-
irVersion:
|
|
80
|
+
...restRaw,
|
|
81
|
+
irVersion: WORKFLOW_IR_V5_VERSION,
|
|
59
82
|
sourceReadSet,
|
|
60
83
|
steps,
|
|
84
|
+
...(outputs ? { outputs } : {}),
|
|
61
85
|
});
|
|
62
86
|
}
|
|
63
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Decode a plan's `outputs` (P3b, spec §4.2): a corruption gate mirroring
|
|
89
|
+
* {@link decodeInputBindings} — non-empty, sorted-unique keys (canonical wire
|
|
90
|
+
* order, same rule `inputBindings` enforces), each name matching the
|
|
91
|
+
* `params:` name pattern, each entry's closed key set, `from` re-parsing as a
|
|
92
|
+
* `stepOutput` reference whose step id is declared in `plan.steps`.
|
|
93
|
+
*/
|
|
94
|
+
function decodeWorkflowOutputs(value, stepIds) {
|
|
95
|
+
const raw = record(value, "plan outputs");
|
|
96
|
+
const names = Object.keys(raw);
|
|
97
|
+
if (names.length === 0)
|
|
98
|
+
fail("plan outputs must be a non-empty object");
|
|
99
|
+
let prior;
|
|
100
|
+
const outputs = {};
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
if (!PROGRAM_PARAM_NAME_PATTERN.test(name))
|
|
103
|
+
fail(`plan outputs has invalid name ${name}`);
|
|
104
|
+
if (prior !== undefined && compareCodePoints(prior, name) >= 0) {
|
|
105
|
+
fail("plan outputs must be sorted by unique name");
|
|
106
|
+
}
|
|
107
|
+
prior = name;
|
|
108
|
+
const entry = record(raw[name], `plan outputs.${name}`);
|
|
109
|
+
assertKeys(entry, ["from", "schema"], `plan outputs.${name}`);
|
|
110
|
+
if (typeof entry.from !== "string" || !entry.from)
|
|
111
|
+
fail(`plan outputs.${name} from is invalid`);
|
|
112
|
+
const parsed = parseReference(entry.from);
|
|
113
|
+
if (!parsed.ok || parsed.expr.kind !== "stepOutput") {
|
|
114
|
+
fail(`plan outputs.${name} from must be a steps.<id>.output reference`);
|
|
115
|
+
}
|
|
116
|
+
if (!stepIds.has(parsed.expr.stepId)) {
|
|
117
|
+
fail(`plan outputs.${name} from names step ${parsed.expr.stepId}, which is not in plan.steps`);
|
|
118
|
+
}
|
|
119
|
+
const schema = Object.hasOwn(entry, "schema") ? record(entry.schema, `plan outputs.${name} schema`) : undefined;
|
|
120
|
+
outputs[name] = Object.freeze({ from: entry.from, ...(schema ? { schema: Object.freeze(schema) } : {}) });
|
|
121
|
+
}
|
|
122
|
+
return Object.freeze(outputs);
|
|
123
|
+
}
|
|
124
|
+
function decodeUnitV4(raw, requiredSources, depth, budget) {
|
|
64
125
|
const id = raw.id;
|
|
65
126
|
if (!Object.hasOwn(raw, "frozenTarget"))
|
|
66
127
|
fail(`unit ${id} frozenTarget is required`);
|
|
67
128
|
if (!Object.hasOwn(raw, "environment"))
|
|
68
129
|
fail(`unit ${id} environment is required`);
|
|
69
130
|
const environment = decodeEnvironment(raw.environment, id);
|
|
70
|
-
const frozenTarget = decodeFrozenTarget(raw.frozenTarget, raw, environment, requiredSources);
|
|
131
|
+
const frozenTarget = decodeFrozenTarget(raw.frozenTarget, raw, environment, requiredSources, depth, budget);
|
|
71
132
|
const { frozenTarget: _target, environment: _environment, ...core } = raw;
|
|
72
133
|
return Object.freeze({
|
|
73
134
|
...core,
|
|
@@ -109,7 +170,7 @@ function decodeGateV4(value, stepId, requiredSources) {
|
|
|
109
170
|
frozenJudge: decodeCommandTarget(record(gate.frozenJudge, `gate ${stepId} frozenJudge`), identity, requiredSources),
|
|
110
171
|
});
|
|
111
172
|
}
|
|
112
|
-
function decodeFrozenTarget(value, unit, environment, requiredSources) {
|
|
173
|
+
function decodeFrozenTarget(value, unit, environment, requiredSources, depth, budget) {
|
|
113
174
|
const target = record(value, `unit ${unit.id} frozenTarget`);
|
|
114
175
|
if (target.kind === "command")
|
|
115
176
|
return decodeCommandTarget(target, unit, requiredSources);
|
|
@@ -117,10 +178,121 @@ function decodeFrozenTarget(value, unit, environment, requiredSources) {
|
|
|
117
178
|
return decodeShellTarget(target, unit, environment);
|
|
118
179
|
if (target.kind === "script")
|
|
119
180
|
return decodeScriptTarget(target, unit, requiredSources);
|
|
181
|
+
if (target.kind === "child-workflow")
|
|
182
|
+
return decodeChildWorkflowTarget(target, unit, depth, budget);
|
|
120
183
|
fail(`unit ${unit.id} frozenTarget has unsupported kind ${String(target.kind)}`);
|
|
121
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Composition-depth corruption bound (spec §3.6 step 6, §4.5, row A-23).
|
|
187
|
+
* Mirrors `WORKFLOW_MAX_COMPOSITION_DEPTH` (= 8), which Lane B adds to
|
|
188
|
+
* `src/workflows/resource-limits.ts` in a later commit as the freeze-time
|
|
189
|
+
* `COMPOSITION_INVALID` gate. Reproduced here — the decode-time corruption
|
|
190
|
+
* gate — rather than imported across the lane boundary, matching
|
|
191
|
+
* `tests/workflows/plan-v5-schema.test.ts`'s own header comment.
|
|
192
|
+
*/
|
|
193
|
+
const CHILD_WORKFLOW_DECODE_MAX_DEPTH = 8;
|
|
194
|
+
/**
|
|
195
|
+
* Decode a `kind: "child-workflow"` frozen target (§3.5): the embedded
|
|
196
|
+
* COMPLETE child plan, re-verified against its own `planHash` and this
|
|
197
|
+
* target's own `contentHash` on every decode (rows A-20, A-21), recursively
|
|
198
|
+
* enforcing `irVersion` 5 (row A-22, via the recursive
|
|
199
|
+
* {@link decodeWorkflowPlanV4} call), the composition depth bound (row A-23),
|
|
200
|
+
* and the AGGREGATE embedded-plan-bytes bound (§3.6 step 6, A-N6) as the
|
|
201
|
+
* decoder recurses. `WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES` — unlike the
|
|
202
|
+
* depth bound above — is imported directly from
|
|
203
|
+
* `src/workflows/resource-limits.ts` rather than reproduced: that constant
|
|
204
|
+
* was already Lane B's when this decode-time re-enforcement was added (no
|
|
205
|
+
* lane-ordering constraint left to honor), and re-deriving a byte cap by
|
|
206
|
+
* hand invites drift a depth integer does not. `budget` is charged with
|
|
207
|
+
* THIS child's own full canonical byte length AFTER it decodes (so any
|
|
208
|
+
* further-nested grandchildren it embeds have already charged themselves,
|
|
209
|
+
* mirroring the freeze-side `chargeEmbeddedBudget`'s post-recursion charge
|
|
210
|
+
* order in `src/workflows/freeze/targets/child-workflow.ts`) — a rejected
|
|
211
|
+
* plan therefore never partially charges the budget. `frozenPlan` is
|
|
212
|
+
* covered wholesale through `planHash`, so it is deliberately NOT
|
|
213
|
+
* re-serialized into `contentHash` (§3.5). No `requiredSources`
|
|
214
|
+
* contribution: unlike a command/script target's own referenced source, the
|
|
215
|
+
* child's transitive sources live in the child's OWN `sourceReadSet`,
|
|
216
|
+
* verified by the recursive decode call itself; the PARENT's
|
|
217
|
+
* `sourceReadSet` absorbing the child's files (row B-05) is a freeze-time
|
|
218
|
+
* concern (Lane B), not a decode-time structural one.
|
|
219
|
+
*/
|
|
220
|
+
function decodeChildWorkflowTarget(target, unit, depth, budget) {
|
|
221
|
+
assertKeys(target, ["kind", "ref", "planHash", "frozenPlan", "contentHash", "via", "taskRef", "inputBindings"], `unit ${unit.id} child workflow target`);
|
|
222
|
+
if (typeof target.ref !== "string" || !target.ref)
|
|
223
|
+
fail(`unit ${unit.id} child workflow ref is invalid`);
|
|
224
|
+
const planHash = digest(target.planHash, `unit ${unit.id} child workflow planHash`);
|
|
225
|
+
if (target.via !== "direct" && target.via !== "task")
|
|
226
|
+
fail(`unit ${unit.id} child workflow via is invalid`);
|
|
227
|
+
const via = target.via;
|
|
228
|
+
if (via === "task") {
|
|
229
|
+
if (typeof target.taskRef !== "string" || !target.taskRef) {
|
|
230
|
+
fail(`unit ${unit.id} child workflow via "task" requires a taskRef`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (target.taskRef !== undefined) {
|
|
234
|
+
fail(`unit ${unit.id} child workflow via "direct" cannot carry a taskRef`);
|
|
235
|
+
}
|
|
236
|
+
const taskRef = target.taskRef;
|
|
237
|
+
const inputBindings = decodeInputBindings(target.inputBindings, `unit ${unit.id} child workflow target`);
|
|
238
|
+
const contentHash = digest(target.contentHash, `unit ${unit.id} child workflow contentHash`);
|
|
239
|
+
const expectedContentHash = childWorkflowContentHash({ ref: target.ref, planHash, via, taskRef, inputBindings });
|
|
240
|
+
if (contentHash !== expectedContentHash) {
|
|
241
|
+
fail(`unit ${unit.id} child workflow contentHash does not match its frozen dispatch`);
|
|
242
|
+
}
|
|
243
|
+
const childDepth = depth + 1;
|
|
244
|
+
if (childDepth > CHILD_WORKFLOW_DECODE_MAX_DEPTH) {
|
|
245
|
+
fail(`unit ${unit.id} child workflow ${target.ref} exceeds the max composition depth of ${CHILD_WORKFLOW_DECODE_MAX_DEPTH}`);
|
|
246
|
+
}
|
|
247
|
+
const frozenPlan = decodeWorkflowPlanV4(target.frozenPlan, {}, childDepth, budget);
|
|
248
|
+
const embeddedPlanJson = canonicalJsonLocal(frozenPlan);
|
|
249
|
+
const actualPlanHash = sha256(embeddedPlanJson);
|
|
250
|
+
if (actualPlanHash !== planHash) {
|
|
251
|
+
fail(`unit ${unit.id} child workflow embedded plan does not match its frozen planHash`);
|
|
252
|
+
}
|
|
253
|
+
const projectedBytes = budget.embeddedBytes + utf8Bytes(embeddedPlanJson);
|
|
254
|
+
if (projectedBytes > WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES) {
|
|
255
|
+
fail(`unit ${unit.id} child workflow ${target.ref} embedded plans total ${projectedBytes} bytes, over the ` +
|
|
256
|
+
`${WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES}-byte limit`);
|
|
257
|
+
}
|
|
258
|
+
budget.embeddedBytes = projectedBytes;
|
|
259
|
+
return Object.freeze({
|
|
260
|
+
kind: "child-workflow",
|
|
261
|
+
ref: target.ref,
|
|
262
|
+
planHash,
|
|
263
|
+
frozenPlan,
|
|
264
|
+
contentHash,
|
|
265
|
+
via,
|
|
266
|
+
...(taskRef !== undefined ? { taskRef } : {}),
|
|
267
|
+
...(inputBindings ? { inputBindings } : {}),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/** §3.5's exact `contentHash` formula. */
|
|
271
|
+
function childWorkflowContentHash(fields) {
|
|
272
|
+
return createHash("sha256")
|
|
273
|
+
.update("akm.workflow.child-workflow\0v1\0")
|
|
274
|
+
.update(canonicalJsonLocal({
|
|
275
|
+
ref: fields.ref,
|
|
276
|
+
planHash: fields.planHash,
|
|
277
|
+
via: fields.via,
|
|
278
|
+
taskRef: fields.taskRef ?? null,
|
|
279
|
+
inputBindings: fields.inputBindings ?? null,
|
|
280
|
+
}))
|
|
281
|
+
.digest("hex");
|
|
282
|
+
}
|
|
122
283
|
function decodeCommandTarget(target, unit, requiredSources) {
|
|
123
|
-
assertKeys(target, [
|
|
284
|
+
assertKeys(target, [
|
|
285
|
+
"kind",
|
|
286
|
+
"ref",
|
|
287
|
+
"contentHash",
|
|
288
|
+
"request",
|
|
289
|
+
"runner",
|
|
290
|
+
"concurrency",
|
|
291
|
+
"cwdIdentity",
|
|
292
|
+
"executable",
|
|
293
|
+
"gitCommitOid",
|
|
294
|
+
"inputBindings",
|
|
295
|
+
], `unit ${unit.id} command target`);
|
|
124
296
|
if (target.ref !== null && typeof target.ref !== "string")
|
|
125
297
|
fail(`unit ${unit.id} command target ref is invalid`);
|
|
126
298
|
const request = decodeResolvedExecutionRequest(target.request);
|
|
@@ -171,6 +343,7 @@ function decodeCommandTarget(target, unit, requiredSources) {
|
|
|
171
343
|
fail(`unit ${unit.id} non-CLI target cannot carry a host executable`);
|
|
172
344
|
}
|
|
173
345
|
const gitCommitOid = decodeGitCommitOid(target.gitCommitOid, unit);
|
|
346
|
+
const inputBindings = decodeInputBindings(target.inputBindings, `unit ${unit.id} command target`);
|
|
174
347
|
// Force the shared request canonicalizer across every accepted wire request.
|
|
175
348
|
canonicalResolvedExecutionRequest(request);
|
|
176
349
|
return Object.freeze({
|
|
@@ -183,10 +356,11 @@ function decodeCommandTarget(target, unit, requiredSources) {
|
|
|
183
356
|
...(cwdIdentity ? { cwdIdentity } : {}),
|
|
184
357
|
...(executable ? { executable } : {}),
|
|
185
358
|
...(gitCommitOid ? { gitCommitOid } : {}),
|
|
359
|
+
...(inputBindings ? { inputBindings } : {}),
|
|
186
360
|
});
|
|
187
361
|
}
|
|
188
362
|
function decodeShellTarget(target, unit, environment) {
|
|
189
|
-
assertKeys(target, ["kind", "contentHash", "exec", "cwdIdentity", "executable", "gitCommitOid"], `unit ${unit.id} shell target`);
|
|
363
|
+
assertKeys(target, ["kind", "contentHash", "exec", "cwdIdentity", "executable", "gitCommitOid", "inputBindings"], `unit ${unit.id} shell target`);
|
|
190
364
|
const exec = decodeWorkflowExecSpec(target.exec, `unit ${unit.id} shell target exec`);
|
|
191
365
|
const cwdIdentity = decodeDirectoryIdentity(target.cwdIdentity, unit.id);
|
|
192
366
|
const executable = Object.hasOwn(target, "executable")
|
|
@@ -197,12 +371,16 @@ function decodeShellTarget(target, unit, environment) {
|
|
|
197
371
|
}
|
|
198
372
|
const gitCommitOid = decodeGitCommitOid(target.gitCommitOid, unit);
|
|
199
373
|
const contentHash = digest(target.contentHash, `unit ${unit.id} shell contentHash`);
|
|
374
|
+
// inputBindings deliberately sits OUTSIDE this preimage (P2b A-N7): identity
|
|
375
|
+
// coverage for a task-composed unit comes from computeUnitInputHash's own
|
|
376
|
+
// frozenTarget field (step-work.ts), which hashes this whole target anyway.
|
|
200
377
|
const expected = createHash("sha256")
|
|
201
378
|
.update("akm.workflow.shell.v1\0")
|
|
202
379
|
.update(canonicalJsonLocal({ exec, environment, cwdIdentity }))
|
|
203
380
|
.digest("hex");
|
|
204
381
|
if (contentHash !== expected)
|
|
205
382
|
fail(`unit ${unit.id} shell contentHash does not match its frozen dispatch`);
|
|
383
|
+
const inputBindings = decodeInputBindings(target.inputBindings, `unit ${unit.id} shell target`);
|
|
206
384
|
return Object.freeze({
|
|
207
385
|
kind: "shell",
|
|
208
386
|
contentHash,
|
|
@@ -210,6 +388,7 @@ function decodeShellTarget(target, unit, environment) {
|
|
|
210
388
|
cwdIdentity,
|
|
211
389
|
...(executable ? { executable } : {}),
|
|
212
390
|
...(gitCommitOid ? { gitCommitOid } : {}),
|
|
391
|
+
...(inputBindings ? { inputBindings } : {}),
|
|
213
392
|
});
|
|
214
393
|
}
|
|
215
394
|
function decodeScriptTarget(target, unit, requiredSources) {
|
|
@@ -226,6 +405,7 @@ function decodeScriptTarget(target, unit, requiredSources) {
|
|
|
226
405
|
"materialization",
|
|
227
406
|
"executable",
|
|
228
407
|
"gitCommitOid",
|
|
408
|
+
"inputBindings",
|
|
229
409
|
], `unit ${unit.id} script target`);
|
|
230
410
|
const exec = decodeWorkflowExecSpec(target.exec, `unit ${unit.id} script target exec`);
|
|
231
411
|
if (typeof target.ref !== "string" || !target.ref.includes("//"))
|
|
@@ -253,6 +433,7 @@ function decodeScriptTarget(target, unit, requiredSources) {
|
|
|
253
433
|
? decodeFrozenExecutableIdentity(target.executable, `unit ${unit.id} executable`)
|
|
254
434
|
: undefined;
|
|
255
435
|
const gitCommitOid = decodeGitCommitOid(target.gitCommitOid, unit);
|
|
436
|
+
const inputBindings = decodeInputBindings(target.inputBindings, `unit ${unit.id} script target`);
|
|
256
437
|
requiredSources.push({ ref: target.ref, bundle: "", adapter: "", file: "", hash: contentHash });
|
|
257
438
|
return Object.freeze({
|
|
258
439
|
kind: "script",
|
|
@@ -267,7 +448,55 @@ function decodeScriptTarget(target, unit, requiredSources) {
|
|
|
267
448
|
materialization: "ephemeral-0700-delete",
|
|
268
449
|
...(executable ? { executable } : {}),
|
|
269
450
|
...(gitCommitOid ? { gitCommitOid } : {}),
|
|
451
|
+
...(inputBindings ? { inputBindings } : {}),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Decode a frozen target's `inputBindings` (P2b §3.2 A-N7): a composing
|
|
456
|
+
* step's `with:` normalized against the composed task's declared inputs.
|
|
457
|
+
* Closed `kind`, `INPUT_NAME_PATTERN` name, unique and sorted by name (never
|
|
458
|
+
* `[]` — absence is the identity-preserving default). A `literal` entry
|
|
459
|
+
* requires `value`; a `reference` entry requires a `parseReference`-valid
|
|
460
|
+
* `from` plus the declaration's bounded `schema` (§3.6's widened reference
|
|
461
|
+
* arm, `src/execution/input-contract.ts`).
|
|
462
|
+
*/
|
|
463
|
+
function decodeInputBindings(value, label) {
|
|
464
|
+
if (value === undefined)
|
|
465
|
+
return undefined;
|
|
466
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
467
|
+
fail(`${label} inputBindings must be a non-empty array`);
|
|
468
|
+
let prior;
|
|
469
|
+
const bindings = value.map((raw, index) => {
|
|
470
|
+
const binding = record(raw, `${label} inputBindings[${index}]`);
|
|
471
|
+
if (typeof binding.name !== "string" || !INPUT_NAME_PATTERN.test(binding.name)) {
|
|
472
|
+
fail(`${label} inputBindings[${index}] name is invalid`);
|
|
473
|
+
}
|
|
474
|
+
if (prior !== undefined && compareCodePoints(prior, binding.name) >= 0) {
|
|
475
|
+
fail(`${label} inputBindings must be sorted by unique name`);
|
|
476
|
+
}
|
|
477
|
+
prior = binding.name;
|
|
478
|
+
if (binding.kind === "literal") {
|
|
479
|
+
assertKeys(binding, ["kind", "name", "value"], `${label} inputBindings[${index}]`);
|
|
480
|
+
if (!Object.hasOwn(binding, "value"))
|
|
481
|
+
fail(`${label} inputBindings[${index}] literal binding requires value`);
|
|
482
|
+
return Object.freeze({ kind: "literal", name: binding.name, value: binding.value });
|
|
483
|
+
}
|
|
484
|
+
if (binding.kind === "reference") {
|
|
485
|
+
assertKeys(binding, ["kind", "name", "from", "schema"], `${label} inputBindings[${index}]`);
|
|
486
|
+
if (typeof binding.from !== "string" || !parseReference(binding.from).ok) {
|
|
487
|
+
fail(`${label} inputBindings[${index}] from is not a valid reference`);
|
|
488
|
+
}
|
|
489
|
+
const schema = record(binding.schema, `${label} inputBindings[${index}] schema`);
|
|
490
|
+
return Object.freeze({
|
|
491
|
+
kind: "reference",
|
|
492
|
+
name: binding.name,
|
|
493
|
+
from: binding.from,
|
|
494
|
+
schema: Object.freeze(schema),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return fail(`${label} inputBindings[${index}] has unsupported kind ${String(binding.kind)}`);
|
|
270
498
|
});
|
|
499
|
+
return Object.freeze(bindings);
|
|
271
500
|
}
|
|
272
501
|
function decodeGitCommitOid(value, unit) {
|
|
273
502
|
if (unit.isolation === "worktree") {
|
package/dist/workflows/parser.js
CHANGED
|
@@ -38,7 +38,7 @@ import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-param
|
|
|
38
38
|
import { checkJsonSchemaDefinition, JSON_SCHEMA_SUBSET_SUPPORTED_KEYWORDS } from "../core/json-schema.js";
|
|
39
39
|
import { parseReference } from "./program/expressions.js";
|
|
40
40
|
import { PROGRAM_ISOLATION_KINDS, PROGRAM_ON_ERROR, PROGRAM_PARAM_NAME_PATTERN, PROGRAM_REDUCERS, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN, } from "./program/schema.js";
|
|
41
|
-
import { jsonBytes, utf8Bytes, WORKFLOW_ENGINE_NAME_PATTERN, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_ENGINE_NAME_LENGTH, WORKFLOW_MAX_EXEC_ARG_BYTES, WORKFLOW_MAX_EXEC_ARGV, WORKFLOW_MAX_EXEC_CWD_LENGTH, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_GATE_LOOPS, WORKFLOW_MAX_INPUTS, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_ROUTE_BRANCHES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_SOURCE_BYTES, WORKFLOW_MAX_STEPS, WORKFLOW_MAX_TIMEOUT_MS, } from "./resource-limits.js";
|
|
41
|
+
import { jsonBytes, utf8Bytes, WORKFLOW_ENGINE_NAME_PATTERN, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_ENGINE_NAME_LENGTH, WORKFLOW_MAX_EXEC_ARG_BYTES, WORKFLOW_MAX_EXEC_ARGV, WORKFLOW_MAX_EXEC_CWD_LENGTH, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_GATE_LOOPS, WORKFLOW_MAX_INPUTS, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_MAX_OUTPUTS, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_ROUTE_BRANCHES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_SOURCE_BYTES, WORKFLOW_MAX_STEPS, WORKFLOW_MAX_TIMEOUT_MS, } from "./resource-limits.js";
|
|
42
42
|
import { WORKFLOW_SCHEMA_VERSION, } from "./schema.js";
|
|
43
43
|
import { runSemanticChecks } from "./validator.js";
|
|
44
44
|
/** Envelope keys every AKM markdown asset carries ($ref'd from schemas/akm-asset-envelope.json). */
|
|
@@ -56,7 +56,7 @@ const ENVELOPE_KEYS = [
|
|
|
56
56
|
"status",
|
|
57
57
|
"stale_after",
|
|
58
58
|
];
|
|
59
|
-
const WORKFLOW_KEYS = ["params", "defaults", "budget", "steps"];
|
|
59
|
+
const WORKFLOW_KEYS = ["params", "outputs", "defaults", "budget", "steps"];
|
|
60
60
|
const TOP_LEVEL_KEYS = [...ENVELOPE_KEYS, ...WORKFLOW_KEYS];
|
|
61
61
|
const DEFAULTS_KEYS = ["engine", "model", "timeout", "on_error", "llm"];
|
|
62
62
|
const BUDGET_KEYS = ["max_tokens", "max_units"];
|
|
@@ -70,6 +70,8 @@ const ROUTE_KEYS = ["input", "when", "default"];
|
|
|
70
70
|
const RETRY_KEYS = ["max", "on"];
|
|
71
71
|
const GATE_KEYS = ["max_loops"];
|
|
72
72
|
const ROUTE_BRANCH_KEYS = ["match", "step"];
|
|
73
|
+
/** Closed key set of one `outputs:` entry — mirrors `params:`'s bare-schema shape, plus `from`. */
|
|
74
|
+
const OUTPUT_ENTRY_KEYS = ["from", "schema"];
|
|
73
75
|
const ACTOR_STAMP_KEYS = ["by", "at"];
|
|
74
76
|
const TIMEOUT_VALUE = /^(\d+)(ms|s|m)?$/;
|
|
75
77
|
const TIMEOUT_HINT = `Use "<n>ms", "<n>s", "<n>m" (e.g. "10m"), or "none"`;
|
|
@@ -174,6 +176,7 @@ export function parseWorkflow(markdown, source) {
|
|
|
174
176
|
const description = typeof root.description === "string" ? root.description : undefined;
|
|
175
177
|
const tags = readTags(ctx, root.tags, frontmatterEndLine);
|
|
176
178
|
const params = parseParams(ctx, root.params);
|
|
179
|
+
const outputs = parseOutputs(ctx, root.outputs);
|
|
177
180
|
const defaults = parseDefaults(ctx, root.defaults);
|
|
178
181
|
const budget = parseBudget(ctx, root.budget);
|
|
179
182
|
const parsedSteps = parseSteps(ctx, root.steps);
|
|
@@ -220,6 +223,7 @@ export function parseWorkflow(markdown, source) {
|
|
|
220
223
|
...(description ? { description } : {}),
|
|
221
224
|
...(tags ? { tags } : {}),
|
|
222
225
|
...(params ? { params } : {}),
|
|
226
|
+
...(outputs ? { outputs } : {}),
|
|
223
227
|
...(defaults ? { defaults } : {}),
|
|
224
228
|
...(budget ? { budget } : {}),
|
|
225
229
|
steps,
|
|
@@ -423,6 +427,74 @@ function parseParams(ctx, raw) {
|
|
|
423
427
|
}
|
|
424
428
|
return Object.keys(params).length > 0 ? params : undefined;
|
|
425
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* `outputs:` (P3b, spec §4.2): named, optionally schema-validated projections
|
|
432
|
+
* of step artifacts, exported when the run completes. Symmetrical with
|
|
433
|
+
* `parseParams` above (B-N4) — same authoring surface, same name grammar,
|
|
434
|
+
* same schema-subset validator, same per-schema byte bound — but each entry
|
|
435
|
+
* is a STRUCTURED `{from, schema?}` mapping rather than a bare JSON Schema.
|
|
436
|
+
*
|
|
437
|
+
* `from` is validated for GRAMMAR only here: it must parse and it must be a
|
|
438
|
+
* `steps.<id>.output(.<seg>)*` reference (never `params.<name>` — an output
|
|
439
|
+
* projects a step artifact, never a param, B-07). Whether the named step is
|
|
440
|
+
* actually DECLARED in this document is a semantic, cross-step check left to
|
|
441
|
+
* `ir/compile.ts`'s reference validation (B-06), mirroring how `inputs[]` /
|
|
442
|
+
* `map.over` / `route.input` already split "syntax here, semantics there".
|
|
443
|
+
*/
|
|
444
|
+
function parseOutputs(ctx, raw) {
|
|
445
|
+
if (raw === undefined)
|
|
446
|
+
return undefined;
|
|
447
|
+
if (!isPlainRecord(raw)) {
|
|
448
|
+
ctx.err(["outputs"], `"outputs" must be a mapping of output name to { from, schema? } (e.g. report: { from: steps.summarize.output }).`);
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
if (Object.keys(raw).length > WORKFLOW_MAX_OUTPUTS) {
|
|
452
|
+
ctx.err(["outputs"], `"outputs" must contain at most ${WORKFLOW_MAX_OUTPUTS} entries.`);
|
|
453
|
+
}
|
|
454
|
+
const outputs = {};
|
|
455
|
+
for (const [outputName, value] of Object.entries(raw)) {
|
|
456
|
+
const path = ["outputs", outputName];
|
|
457
|
+
if (!PROGRAM_PARAM_NAME_PATTERN.test(outputName)) {
|
|
458
|
+
ctx.err(path, `Output name "${outputName}" is invalid. Use letters, digits, and underscores, starting with a letter or ` +
|
|
459
|
+
`underscore, so "steps.<child>.output.${outputName}" can address it.`);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (!isPlainRecord(value)) {
|
|
463
|
+
ctx.err(path, `Output "${outputName}" must be a mapping with "from" (and optional "schema").`);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
checkUnknownKeys(ctx, value, path, OUTPUT_ENTRY_KEYS, `output "${outputName}"`);
|
|
467
|
+
if (typeof value.from !== "string" || value.from.trim() === "") {
|
|
468
|
+
ctx.err([...path, "from"], `Output "${outputName}" must declare "from": a steps.<id>.output(.<seg>)* reference.`);
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const parsedFrom = parseReference(value.from);
|
|
472
|
+
if (!parsedFrom.ok) {
|
|
473
|
+
ctx.err([...path, "from"], `Output "${outputName}" "from": ${parsedFrom.message}`);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (parsedFrom.expr.kind !== "stepOutput") {
|
|
477
|
+
ctx.err([...path, "from"], `Output "${outputName}" "from" must reference a step output (steps.<id>.output...), not a param — an ` +
|
|
478
|
+
`output projects a step artifact, never a param (got "${value.from}").`);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const entry = { from: value.from };
|
|
482
|
+
if (value.schema !== undefined) {
|
|
483
|
+
if (!isPlainRecord(value.schema)) {
|
|
484
|
+
ctx.err([...path, "schema"], `Output "${outputName}" "schema" must be a JSON Schema object.`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
if (jsonBytes(value.schema) > WORKFLOW_MAX_SCHEMA_BYTES) {
|
|
488
|
+
ctx.err([...path, "schema"], `Output "${outputName}" schema exceeds the 256 KiB resource limit.`);
|
|
489
|
+
}
|
|
490
|
+
checkSchemaDefinition(ctx, value.schema, [...path, "schema"], `Output "${outputName}" schema`);
|
|
491
|
+
entry.schema = value.schema;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
outputs[outputName] = entry;
|
|
495
|
+
}
|
|
496
|
+
return Object.keys(outputs).length > 0 ? outputs : undefined;
|
|
497
|
+
}
|
|
426
498
|
function parseDefaults(ctx, raw) {
|
|
427
499
|
if (raw === undefined)
|
|
428
500
|
return undefined;
|
|
@@ -46,9 +46,12 @@ export const PROGRAM_RETRY_REASONS = Object.keys(RETRY_REASON_SET);
|
|
|
46
46
|
export const PROGRAM_STEP_ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
47
47
|
/**
|
|
48
48
|
* Param names must be `params.<ident>`-addressable, so they are plain
|
|
49
|
-
* identifiers (no dots/dashes).
|
|
49
|
+
* identifiers (no dots/dashes). Re-exported from the shared input contract
|
|
50
|
+
* (`src/execution/input-contract.ts`'s `INPUT_NAME_PATTERN`) so task source
|
|
51
|
+
* v4's `inputs:` declarations share the exact same name pattern rather than a
|
|
52
|
+
* second, hand-maintained copy (P2a, D3-N1).
|
|
50
53
|
*/
|
|
51
|
-
export
|
|
54
|
+
export { INPUT_NAME_PATTERN as PROGRAM_PARAM_NAME_PATTERN } from "../../execution/input-contract.js";
|
|
52
55
|
/**
|
|
53
56
|
* The ONE structural projection of an exec spec — ONE encoding per state.
|
|
54
57
|
*
|
|
@@ -15,6 +15,8 @@ export const WORKFLOW_MAX_JSON_DEPTH = 64;
|
|
|
15
15
|
export const WORKFLOW_MAX_MAP_EXPANSION = 10_000;
|
|
16
16
|
/** Max declared `inputs:` reference strings on one unit/map step. */
|
|
17
17
|
export const WORKFLOW_MAX_INPUTS = 64;
|
|
18
|
+
/** Max declared `outputs:` entries on one workflow document (P3b, spec §4.1). */
|
|
19
|
+
export const WORKFLOW_MAX_OUTPUTS = 64;
|
|
18
20
|
// ── Dispatch-significant bounds shared across validation layers ──────────────
|
|
19
21
|
//
|
|
20
22
|
// Defined ONCE here so the three enforcement layers cannot drift:
|
|
@@ -201,3 +203,21 @@ export function utf8Bytes(value) {
|
|
|
201
203
|
export function jsonBytes(value) {
|
|
202
204
|
return utf8Bytes(JSON.stringify(value));
|
|
203
205
|
}
|
|
206
|
+
// ── Recursive child-workflow composition bounds (spec docs/plans/specs/
|
|
207
|
+
// ── p3a-plan-v5-child-freeze.md §4.5, A-N6) ──────────────────────────────────
|
|
208
|
+
//
|
|
209
|
+
// Enforced ONCE, at freeze, before publication, in
|
|
210
|
+
// `src/workflows/freeze/targets/child-workflow.ts` — the ONE resolver both the
|
|
211
|
+
// direct `uses: workflows/<ref>` form and the task-wrapped form route through
|
|
212
|
+
// — and re-enforced as a corruption gate whenever a parent plan is DECODED
|
|
213
|
+
// (`src/workflows/ir/schema-v4.ts`'s recursive `decodeChildWorkflowTarget`).
|
|
214
|
+
// Full design history, including the rejected alternative for the byte cap:
|
|
215
|
+
// docs/architecture/decisions/0007-workflow-composition-bounds.md.
|
|
216
|
+
/** Max workflow composition depth (root = depth 0; a 9th descendant level fails). */
|
|
217
|
+
export const WORKFLOW_MAX_COMPOSITION_DEPTH = 8;
|
|
218
|
+
/**
|
|
219
|
+
* Max AGGREGATE canonical-JSON bytes of every embedded child plan in ONE root
|
|
220
|
+
* freeze (the sum across the whole composition tree, not per child).
|
|
221
|
+
* Deliberately HALF of {@link WORKFLOW_MAX_PLAN_BYTES} — see ADR 0007.
|
|
222
|
+
*/
|
|
223
|
+
export const WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES = 1024 * 1024;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { UsageError } from "../../core/errors.js";
|
|
5
5
|
import { decodeCanonicalPlan } from "../ir/plan-hash.js";
|
|
6
|
-
import {
|
|
6
|
+
import { WORKFLOW_IR_V5_VERSION } from "../ir/schema-v4.js";
|
|
7
7
|
/** Validate that a live run carries exactly the current frozen-plan format. */
|
|
8
8
|
export function classifyWorkflowRunPlan(row) {
|
|
9
9
|
const runId = row.id ?? "(unknown)";
|
|
@@ -16,14 +16,19 @@ export function classifyWorkflowRunPlan(row) {
|
|
|
16
16
|
}
|
|
17
17
|
if (row.plan_ir_version !== null &&
|
|
18
18
|
row.plan_ir_version !== undefined &&
|
|
19
|
-
row.plan_ir_version !==
|
|
19
|
+
row.plan_ir_version !== WORKFLOW_IR_V5_VERSION) {
|
|
20
20
|
return {
|
|
21
21
|
support: "unsupported-version",
|
|
22
22
|
irVersion: row.plan_ir_version,
|
|
23
|
-
|
|
23
|
+
// §3.2's exact complete-or-abandon policy string (A-N2): pre-irVersion-5
|
|
24
|
+
// plans keep status/list/abandon working but can no longer execute.
|
|
25
|
+
error: `Workflow run ${runId} was frozen as workflow plan irVersion ${row.plan_ir_version}; pre-irVersion-5 ` +
|
|
26
|
+
`plans cannot execute after the 0.9.2 upgrade. Complete them before upgrading, or run ` +
|
|
27
|
+
`'akm workflow abandon ${runId}' and start a new run from the authored workflow. ` +
|
|
28
|
+
`'akm workflow status' and 'akm workflow list' still work on this run.`,
|
|
24
29
|
};
|
|
25
30
|
}
|
|
26
|
-
if (row.plan_ir_version !==
|
|
31
|
+
if (row.plan_ir_version !== WORKFLOW_IR_V5_VERSION) {
|
|
27
32
|
return {
|
|
28
33
|
support: "corrupt-plan",
|
|
29
34
|
irVersion: null,
|
|
@@ -45,11 +50,20 @@ export function classifyWorkflowRunPlan(row) {
|
|
|
45
50
|
};
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Reject any operation that requires a valid current frozen plan. A
|
|
55
|
+
* non-current stored `plan_ir_version` (`unsupported-version`) fails closed
|
|
56
|
+
* under `WORKFLOW_IR_VERSION_UNSUPPORTED` (A-N2) — distinct from the
|
|
57
|
+
* `missing-plan` / `corrupt-plan` decode-corruption family, which keeps
|
|
58
|
+
* `INVALID_JSON_ARGUMENT`.
|
|
59
|
+
*/
|
|
49
60
|
export function requireExecutableWorkflowPlan(row) {
|
|
50
61
|
const classified = classifyWorkflowRunPlan(row);
|
|
51
62
|
if (classified.support === "supported")
|
|
52
63
|
return classified.plan;
|
|
64
|
+
if (classified.support === "unsupported-version") {
|
|
65
|
+
throw new UsageError(classified.error, "WORKFLOW_IR_VERSION_UNSUPPORTED");
|
|
66
|
+
}
|
|
53
67
|
throw new UsageError(classified.error, "INVALID_JSON_ARGUMENT");
|
|
54
68
|
}
|
|
55
69
|
/** Project persisted spine rows from the decoded plan, never from the mutable source asset. */
|
|
@@ -108,8 +122,11 @@ export function assertWorkflowSpineMatchesPlan(plan, run, rows) {
|
|
|
108
122
|
}
|
|
109
123
|
else if (run.status === "failed") {
|
|
110
124
|
// `workflow abandon` marks the run failed while intentionally leaving its
|
|
111
|
-
// current step
|
|
112
|
-
|
|
125
|
+
// current step unchanged so `resume` can reopen the same work. An active
|
|
126
|
+
// run leaves a pending step; a blocked run leaves a blocked step; and an
|
|
127
|
+
// execution failure already carries a failed step. All three are honest
|
|
128
|
+
// failed-run spines that `resumeWorkflowRun` normalizes back to pending.
|
|
129
|
+
if (!current || (current.status !== "failed" && current.status !== "pending" && current.status !== "blocked"))
|
|
113
130
|
corruptSpine(run.id, `${run.status} status does not match the current plan step`);
|
|
114
131
|
}
|
|
115
132
|
else if (run.status === "completed") {
|