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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { loadAdapterExecutionSource } from "../../commands/command/execution-source-loader.js";
|
|
7
|
+
import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
8
|
+
import { parseEnvRef } from "../../core/env-secret-ref.js";
|
|
9
|
+
import { UsageError } from "../../core/errors.js";
|
|
10
|
+
import { deriveInstallations } from "../../indexer/installations.js";
|
|
11
|
+
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
12
|
+
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
13
|
+
import { freezeWorkflowEnvironment } from "../ir/environment-v4.js";
|
|
14
|
+
export function freezeEnvironment(source, exec, context) {
|
|
15
|
+
const literals = Object.entries(source.env ?? {}).map(([name, value]) => Object.freeze({ kind: "literal", name, value: String(value) }));
|
|
16
|
+
const passThrough = (exec?.passEnv ?? []).map((name) => Object.freeze({ kind: "pass-through", name }));
|
|
17
|
+
const refs = source.unit?.env ?? [];
|
|
18
|
+
const envRefs = freezeWorkflowEnvironment(refs, {
|
|
19
|
+
collector: context.collector,
|
|
20
|
+
resolveRef: (ref) => {
|
|
21
|
+
const parsedEnv = parseEnvRef(ref);
|
|
22
|
+
if (parsedEnv.type !== "env")
|
|
23
|
+
throw new UsageError(`Expected an env ref; got ${ref}.`, "WORKFLOW_SOURCE_INVALID");
|
|
24
|
+
const owned = resolveOwnedAssetSync(ref, "env", context);
|
|
25
|
+
return { ref: owned.ref, bundle: owned.bundle, adapter: owned.adapter, root: owned.root, path: owned.file };
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return [...literals, ...passThrough, ...envRefs];
|
|
29
|
+
}
|
|
30
|
+
export async function guardedExecutionSource(ref, kind, context) {
|
|
31
|
+
const owned = await resolveOwnedAsset(ref, kind === "command" ? "command" : "agent", context);
|
|
32
|
+
captureOwned(owned, context.collector);
|
|
33
|
+
const options = {
|
|
34
|
+
config: context.config,
|
|
35
|
+
fileContext: () => context.collector.fileContext(owned.root, owned.file),
|
|
36
|
+
};
|
|
37
|
+
const rendered = kind === "command"
|
|
38
|
+
? await loadAdapterExecutionSource(owned.ref, "command", options)
|
|
39
|
+
: await loadAdapterExecutionSource(owned.ref, "persona", options);
|
|
40
|
+
context.collector.bindIdentity(owned.file, owned.root, rendered.identity);
|
|
41
|
+
return rendered;
|
|
42
|
+
}
|
|
43
|
+
export async function resolveOwnedAsset(ref, type, context) {
|
|
44
|
+
return resolveOwnedAssetCore(ref, type, context, false);
|
|
45
|
+
}
|
|
46
|
+
export function resolveOwnedAssetSync(ref, type, context) {
|
|
47
|
+
return resolveOwnedAssetCore(ref, type, context, true);
|
|
48
|
+
}
|
|
49
|
+
export function resolveOwnedAssetCore(refInput, type, context, sync) {
|
|
50
|
+
const parsed = parseBundleRef(refInput);
|
|
51
|
+
const plural = type === "env" ? "env" : `${type}s`;
|
|
52
|
+
const conceptId = parsed.conceptId.startsWith(`${plural}/`) ? parsed.conceptId : `${plural}/${parsed.conceptId}`;
|
|
53
|
+
const name = conceptId.slice(plural.length + 1);
|
|
54
|
+
const direct = parsed.bundle ? configuredOwner(parsed.bundle, context.config) : undefined;
|
|
55
|
+
const sources = resolveSourceEntries(undefined, context.config);
|
|
56
|
+
const installations = deriveInstallations(sources);
|
|
57
|
+
const candidates = direct
|
|
58
|
+
? [direct]
|
|
59
|
+
: sources.flatMap((source, index) => {
|
|
60
|
+
const installation = installations[index];
|
|
61
|
+
if (!installation || (parsed.bundle && installation.id !== parsed.bundle))
|
|
62
|
+
return [];
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
bundle: installation.id,
|
|
66
|
+
root: source.path,
|
|
67
|
+
adapter: source.adapterId ?? installation.components[0]?.adapter ?? "akm",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
});
|
|
71
|
+
const findSync = () => {
|
|
72
|
+
for (const candidate of candidates) {
|
|
73
|
+
const directory = path.join(candidate.root, plural);
|
|
74
|
+
for (const extension of assetExtensions(type)) {
|
|
75
|
+
const file = path.resolve(directory, `${name}${extension}`);
|
|
76
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile()) {
|
|
77
|
+
return { ...candidate, ref: makeBundleRef(candidate.bundle, conceptId), file };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// P4 (docs/plans/specs/p4-deletions-closeout.md §5.2, row B-11 preservation
|
|
82
|
+
// gate): PRESERVED, not re-coded — tests/workflows/child-workflow-freeze.test.ts's
|
|
83
|
+
// "B-11: workflows/<ref> that does not resolve fails the existing
|
|
84
|
+
// asset-resolution failure, unchanged in code and shape" pins this exact
|
|
85
|
+
// code by name and by its own title. Recorded deviation from §5.2's flat
|
|
86
|
+
// "→ WORKFLOW_SOURCE_INVALID" for this file's 3 sites.
|
|
87
|
+
throw new UsageError(`Workflow source target ${refInput} was not found.`, "INVALID_FLAG_VALUE");
|
|
88
|
+
};
|
|
89
|
+
if (sync)
|
|
90
|
+
return findSync();
|
|
91
|
+
return (async () => {
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
try {
|
|
94
|
+
const file = await resolveAssetPath(candidate.root, type, name);
|
|
95
|
+
return { ...candidate, ref: makeBundleRef(candidate.bundle, conceptId), file };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Continue in installation priority order.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return findSync();
|
|
102
|
+
})();
|
|
103
|
+
}
|
|
104
|
+
export function configuredOwner(bundle, config) {
|
|
105
|
+
const entry = config.bundles?.[bundle];
|
|
106
|
+
if (!entry || typeof entry.path !== "string")
|
|
107
|
+
return undefined;
|
|
108
|
+
const components = entry.components ? Object.values(entry.components) : [];
|
|
109
|
+
const component = components[0];
|
|
110
|
+
return {
|
|
111
|
+
bundle,
|
|
112
|
+
root: path.resolve(entry.path, component?.root ?? "."),
|
|
113
|
+
adapter: component?.adapter ?? "akm",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function assetExtensions(type) {
|
|
117
|
+
if (type === "script")
|
|
118
|
+
return [
|
|
119
|
+
"",
|
|
120
|
+
".sh",
|
|
121
|
+
".ts",
|
|
122
|
+
".js",
|
|
123
|
+
".py",
|
|
124
|
+
".rb",
|
|
125
|
+
".go",
|
|
126
|
+
".pl",
|
|
127
|
+
".php",
|
|
128
|
+
".lua",
|
|
129
|
+
".r",
|
|
130
|
+
".swift",
|
|
131
|
+
".kt",
|
|
132
|
+
".kts",
|
|
133
|
+
".ps1",
|
|
134
|
+
".cmd",
|
|
135
|
+
".bat",
|
|
136
|
+
];
|
|
137
|
+
if (type === "env")
|
|
138
|
+
return ["", ".env"];
|
|
139
|
+
return ["", ".md", ".yml"];
|
|
140
|
+
}
|
|
141
|
+
export function captureOwned(owned, collector) {
|
|
142
|
+
trackAncestry(collector, owned.root, owned.file);
|
|
143
|
+
const retained = collector.capture(owned.file, owned.root, { authored: true });
|
|
144
|
+
return collector.bindIdentity(owned.file, owned.root, {
|
|
145
|
+
ref: owned.ref,
|
|
146
|
+
bundle: owned.bundle,
|
|
147
|
+
adapter: owned.adapter,
|
|
148
|
+
file: retained.relativePath,
|
|
149
|
+
hash: retained.sha256,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export function trackAncestry(collector, rootInput, file) {
|
|
153
|
+
const root = path.resolve(rootInput);
|
|
154
|
+
collector.trackDirectory(root, root);
|
|
155
|
+
const relative = path.relative(root, path.dirname(file));
|
|
156
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
157
|
+
throw new UsageError(`${file} resolves outside its owning root.`, "PATH_ESCAPE_VIOLATION");
|
|
158
|
+
}
|
|
159
|
+
let current = root;
|
|
160
|
+
for (const segment of relative === "" ? [] : relative.split(path.sep)) {
|
|
161
|
+
current = path.join(current, segment);
|
|
162
|
+
collector.trackDirectory(current, root);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function qualifyRef(ref, plural, asset, config) {
|
|
166
|
+
const parsed = parseBundleRef(ref);
|
|
167
|
+
if (parsed.bundle)
|
|
168
|
+
return ref;
|
|
169
|
+
const bundle = parseBundleRef(asset.ref).bundle ?? config.defaultBundle;
|
|
170
|
+
if (!bundle)
|
|
171
|
+
throw new UsageError(`Workflow ref ${ref} has no owning bundle.`, "WORKFLOW_SOURCE_INVALID");
|
|
172
|
+
const concept = parsed.conceptId.startsWith(`${plural}/`) ? parsed.conceptId : `${plural}/${parsed.conceptId}`;
|
|
173
|
+
return makeBundleRef(bundle, concept);
|
|
174
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { UsageError } from "../../core/errors.js";
|
|
6
|
+
export function scriptExecutable(interpreter) {
|
|
7
|
+
if (interpreter === "bun" || interpreter === "bun-standalone")
|
|
8
|
+
return process.execPath;
|
|
9
|
+
if (interpreter === "kotlin")
|
|
10
|
+
return "kotlin";
|
|
11
|
+
return interpreter;
|
|
12
|
+
}
|
|
13
|
+
export function gitIdentity(unit, root) {
|
|
14
|
+
if (unit.isolation !== "worktree")
|
|
15
|
+
return {};
|
|
16
|
+
const result = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
17
|
+
const oid = result.status === 0 ? result.stdout.trim() : "";
|
|
18
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(oid)) {
|
|
19
|
+
throw new UsageError(`Worktree-isolated workflow root ${root} has no immutable Git HEAD OID.`, "WORKFLOW_SOURCE_INVALID");
|
|
20
|
+
}
|
|
21
|
+
return { gitCommitOid: oid };
|
|
22
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
5
|
+
import { prepareInlineExecution } from "../../integrations/agent/inline-execution.js";
|
|
6
|
+
import { sourceStepProgramUnit, sourceStepRef } from "../source-ir/program.js";
|
|
7
|
+
import { classifyWorkflowStepUses } from "../source-ir/semantics.js";
|
|
8
|
+
import { qualifyRef } from "./environment.js";
|
|
9
|
+
import { childWorkflowDispatch } from "./targets/child-workflow.js";
|
|
10
|
+
import { commandDispatch, commandResult, inlineDispatch } from "./targets/command.js";
|
|
11
|
+
import { directScript } from "./targets/script.js";
|
|
12
|
+
import { directShell } from "./targets/shell.js";
|
|
13
|
+
import { taskDispatch } from "./targets/task.js";
|
|
14
|
+
export async function resolveStep(source, context) {
|
|
15
|
+
const baseUnit = sourceStepProgramUnit(source);
|
|
16
|
+
if (source.exec || source.run !== undefined)
|
|
17
|
+
return directShell(source, baseUnit, context);
|
|
18
|
+
if (!source.uses)
|
|
19
|
+
return inlineDispatch(source, baseUnit, context);
|
|
20
|
+
const target = classifyWorkflowStepUses(source.uses);
|
|
21
|
+
if (target.kind === "task")
|
|
22
|
+
return taskDispatch(source, baseUnit, target.ref, context);
|
|
23
|
+
// A direct `uses: workflows/<ref>` step (spec docs/plans/specs/
|
|
24
|
+
// p3a-plan-v5-child-freeze.md §4.2, row B-01/B-04). `with:` on this target
|
|
25
|
+
// IS a valid binding surface (A-N8) — unlike scripts/commands below,
|
|
26
|
+
// `rejectNonTaskBindingWith` must NOT be extended to it.
|
|
27
|
+
if (target.kind === "workflow") {
|
|
28
|
+
return childWorkflowDispatch({
|
|
29
|
+
source,
|
|
30
|
+
baseUnit,
|
|
31
|
+
childRefInput: target.ref,
|
|
32
|
+
context,
|
|
33
|
+
via: "direct",
|
|
34
|
+
authoredInputs: { kind: "with", value: source.with },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (target.kind === "script") {
|
|
38
|
+
rejectNonTaskBindingWith(source, target.ref, "script");
|
|
39
|
+
return directScript(source, baseUnit, target.ref, context);
|
|
40
|
+
}
|
|
41
|
+
if (target.kind === "command" || target.kind === "builtin-command") {
|
|
42
|
+
if (target.kind === "command")
|
|
43
|
+
rejectNonTaskBindingWith(source, target.ref, "command");
|
|
44
|
+
const action = target.kind === "builtin-command"
|
|
45
|
+
? source.with
|
|
46
|
+
: { ref: qualifyRef(target.ref, "commands", context.asset, context.config) };
|
|
47
|
+
return commandDispatch(source, baseUnit, action, context);
|
|
48
|
+
}
|
|
49
|
+
throw new UsageError(`Workflow target ${source.uses} is not executable in 0.9.2.`, "WORKFLOW_SOURCE_INVALID");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A-N5 (spec docs/plans/specs/p2b-input-bindings.md §1.7): `commands/<ref>`
|
|
53
|
+
* and `scripts/<ref>` are not binding surfaces — a `with:` authored on either
|
|
54
|
+
* now fails closed instead of being silently dropped (the same defect P1a
|
|
55
|
+
* closed for `tasks/<ref>`, now repeated for these two targets, B-22/B-23).
|
|
56
|
+
* `akm/command`'s `with:` is its own argument bag
|
|
57
|
+
* (`parseBuiltinCommandAction`) and never routes through here.
|
|
58
|
+
*/
|
|
59
|
+
function rejectNonTaskBindingWith(source, ref, kind) {
|
|
60
|
+
if (source.with === undefined)
|
|
61
|
+
return;
|
|
62
|
+
const family = kind === "command" ? "commands" : "scripts";
|
|
63
|
+
throw new UsageError(`Workflow step ${source.id} cannot pass with: to ${family} target ${ref}; a ${kind} ref is not a binding surface.`, "COMPOSITION_INVALID");
|
|
64
|
+
}
|
|
65
|
+
export function resolveJudge(source, context) {
|
|
66
|
+
const engine = context.config.workflow?.judgeEngine;
|
|
67
|
+
if (!engine) {
|
|
68
|
+
throw new ConfigError("This workflow declares completion criteria but no verification engine is configured. Set workflow.judgeEngine to a named LLM or agent engine.", "INVALID_CONFIG_FILE");
|
|
69
|
+
}
|
|
70
|
+
const content = source.gate?.rubric?.trim() ?? "Judge workflow completion.";
|
|
71
|
+
const prepared = prepareInlineExecution({
|
|
72
|
+
content,
|
|
73
|
+
config: context.config,
|
|
74
|
+
invocationKind: "workflow",
|
|
75
|
+
current: { engine },
|
|
76
|
+
});
|
|
77
|
+
return commandResult(source, { onError: "fail", source: sourceStepRef(source) }, prepared, context);
|
|
78
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { COMPOSITION_INVALID_MULTI_JOB_HINT, UsageError } from "../../core/errors.js";
|
|
5
|
+
import { compileWorkflowSource } from "../source-ir/compile.js";
|
|
6
|
+
import { resolveJudge, resolveStep } from "./resolve-steps.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve every authored target through the shared command/task authorities
|
|
9
|
+
* before v4 publication.
|
|
10
|
+
*
|
|
11
|
+
* `composition` and `freezeChild` (spec docs/plans/specs/
|
|
12
|
+
* p3a-plan-v5-child-freeze.md §4.1/§4.3) thread the recursive
|
|
13
|
+
* child-workflow-freeze state down into every step's `ResolutionContext`;
|
|
14
|
+
* `ir/freeze-v4.ts`'s `compileResolveFreezeWorkflowV4` is this function's
|
|
15
|
+
* ONLY caller (directly — P4 deleted the `ir/source-freeze-v4.ts` shim that
|
|
16
|
+
* used to sit on this edge) and supplies both — the root default
|
|
17
|
+
* composition, or the composition
|
|
18
|
+
* `targets/child-workflow.ts` built for a recursive child freeze.
|
|
19
|
+
*/
|
|
20
|
+
export async function resolveWorkflowSourceV4(asset, workflowSource, config, collector, composition, freezeChild) {
|
|
21
|
+
const compiled = compileWorkflowSource(workflowSource.content, { path: asset.path, workspaceRoot: asset.sourcePath });
|
|
22
|
+
if (!compiled.ok) {
|
|
23
|
+
// P4-N2 (docs/plans/specs/p4-deletions-closeout.md §3.3.4): a compile
|
|
24
|
+
// result whose single error is the adapter's multi-job rejection
|
|
25
|
+
// surfaces as COMPOSITION_INVALID — one authoritative composition
|
|
26
|
+
// policy; every other compile failure (a YAML syntax error, an
|
|
27
|
+
// unsupported trigger, …) is WORKFLOW_SOURCE_INVALID. Never blanket-recode.
|
|
28
|
+
const isMultiJob = compiled.errors.length === 1 && compiled.errors[0]?.code === "multi-job-unsupported";
|
|
29
|
+
const code = isMultiJob ? "COMPOSITION_INVALID" : "WORKFLOW_SOURCE_INVALID";
|
|
30
|
+
throw new UsageError(`Workflow source cannot be frozen: ${compiled.errors.map((error) => error.message).join("; ")}`, code, isMultiJob ? COMPOSITION_INVALID_MULTI_JOB_HINT : undefined);
|
|
31
|
+
}
|
|
32
|
+
const context = { asset, config, collector, sourceIr: compiled.ir, composition, freezeChild };
|
|
33
|
+
const units = new Map();
|
|
34
|
+
const judges = new Map();
|
|
35
|
+
let engineAnnouncement;
|
|
36
|
+
const sourceSteps = compiled.ir.jobs[0]?.steps ?? [];
|
|
37
|
+
for (const sourceStep of sourceSteps) {
|
|
38
|
+
if (!sourceStep.route) {
|
|
39
|
+
const resolved = await resolveStep(sourceStep, context);
|
|
40
|
+
units.set(sourceStep.id, Object.freeze(resolved));
|
|
41
|
+
engineAnnouncement ??= resolved.engineAnnouncement;
|
|
42
|
+
}
|
|
43
|
+
if (sourceStep.gate?.rubric?.trim()) {
|
|
44
|
+
const judge = resolveJudge(sourceStep, context);
|
|
45
|
+
if (judge.target.kind !== "command")
|
|
46
|
+
throw new Error(`workflow judge ${sourceStep.id} did not resolve to a command target`);
|
|
47
|
+
judges.set(sourceStep.id, judge.target);
|
|
48
|
+
engineAnnouncement ??= judge.engineAnnouncement;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
sourceIr: compiled.ir,
|
|
53
|
+
units,
|
|
54
|
+
judges,
|
|
55
|
+
...(engineAnnouncement ? { engineAnnouncement } : {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { canonicalResolvedExecutionRequest, decodeResolvedExecutionRequest, } from "../../execution/resolved-request.js";
|
|
5
|
+
import { defaultLlmEngineConcurrency } from "../concurrency-policy.js";
|
|
6
|
+
import { DEFAULT_EXEC_TIMEOUT_MS } from "../resource-limits.js";
|
|
7
|
+
export function freezeExecSpec(source, exec, context) {
|
|
8
|
+
const declared = Object.hasOwn(source.unit ?? {}, "timeoutMs")
|
|
9
|
+
? source.unit?.timeoutMs
|
|
10
|
+
: context.sourceIr.defaults && Object.hasOwn(context.sourceIr.defaults, "timeoutMs")
|
|
11
|
+
? context.sourceIr.defaults.timeoutMs
|
|
12
|
+
: undefined;
|
|
13
|
+
return {
|
|
14
|
+
...exec,
|
|
15
|
+
command: exec.command,
|
|
16
|
+
timeoutMs: declared === undefined ? DEFAULT_EXEC_TIMEOUT_MS : declared,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function targetConcurrency(runner, config) {
|
|
20
|
+
if (runner.kind === "llm") {
|
|
21
|
+
const configured = typeof runner.engine === "string" ? config.engines?.[runner.engine] : undefined;
|
|
22
|
+
return defaultLlmEngineConcurrency(runner.connection.endpoint, configured?.kind === "llm" ? configured.concurrency : undefined);
|
|
23
|
+
}
|
|
24
|
+
if (runner.kind !== "sdk" || !runner.fallbackConnection)
|
|
25
|
+
return undefined;
|
|
26
|
+
const selected = typeof runner.engine === "string" ? config.engines?.[runner.engine] : undefined;
|
|
27
|
+
const fallbackName = selected?.kind === "agent" ? (selected.llmEngine ?? config.defaults?.llmEngine) : undefined;
|
|
28
|
+
const fallback = fallbackName ? config.engines?.[fallbackName] : undefined;
|
|
29
|
+
return defaultLlmEngineConcurrency(runner.fallbackConnection.endpoint, fallback?.kind === "llm" ? fallback.concurrency : undefined);
|
|
30
|
+
}
|
|
31
|
+
export function durableRequest(request) {
|
|
32
|
+
const wire = JSON.parse(canonicalResolvedExecutionRequest(request));
|
|
33
|
+
const runtime = { ...wire.runtime };
|
|
34
|
+
delete runtime.environment;
|
|
35
|
+
wire.runtime = runtime;
|
|
36
|
+
return decodeResolvedExecutionRequest(wire);
|
|
37
|
+
}
|
|
38
|
+
export function executionValues(source, workspace) {
|
|
39
|
+
return executionUnitValues(source.unit, workspace);
|
|
40
|
+
}
|
|
41
|
+
export function executionUnitValues(unit, workspace) {
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
...(unit && Object.hasOwn(unit, "engine") ? { engine: unit.engine } : {}),
|
|
44
|
+
...(unit && Object.hasOwn(unit, "model") ? { model: unit.model } : {}),
|
|
45
|
+
...(unit && Object.hasOwn(unit, "llm") ? { inference: unit.llm } : {}),
|
|
46
|
+
...(unit && Object.hasOwn(unit, "timeoutMs") ? { timeout: unit.timeoutMs } : {}),
|
|
47
|
+
...(unit && "output" in unit && Object.hasOwn(unit, "output") ? { outputSchema: unit.output } : {}),
|
|
48
|
+
workspace,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Step ids that appear BEFORE `stepId` in the frozen step order (A-N4) — the
|
|
53
|
+
* SAME ordering map.over/inputs[] rely on. Shared by `targets/task.ts` (a
|
|
54
|
+
* step's own `with:` against its composed task's declared `inputs:`) and
|
|
55
|
+
* `targets/child-workflow.ts` (the SAME step's effective inputs, re-bound
|
|
56
|
+
* against a composed child workflow's declared `params:`, spec A-N8) — moved
|
|
57
|
+
* here rather than duplicated so both can import it without either importing
|
|
58
|
+
* the other.
|
|
59
|
+
*/
|
|
60
|
+
export function earlierStepIds(sourceIr, stepId) {
|
|
61
|
+
const steps = sourceIr.jobs[0]?.steps ?? [];
|
|
62
|
+
const index = steps.findIndex((step) => step.id === stepId);
|
|
63
|
+
return new Set(index < 0 ? [] : steps.slice(0, index).map((step) => step.id));
|
|
64
|
+
}
|
|
65
|
+
/** THIS workflow's own declared param names (A-N4) — never an outer composing task's. See {@link earlierStepIds}. */
|
|
66
|
+
export function declaredParamNames(sourceIr) {
|
|
67
|
+
return new Set(sourceIr.params ? Object.keys(sourceIr.params) : []);
|
|
68
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* The ONE recursive child-workflow resolver (spec docs/plans/specs/
|
|
6
|
+
* p3a-plan-v5-child-freeze.md §4). BOTH composition forms lower here:
|
|
7
|
+
*
|
|
8
|
+
* - a direct step `uses: workflows/<ref>` (`resolve-steps.ts`'s
|
|
9
|
+
* `resolveStep`, `via: "direct"`);
|
|
10
|
+
* - a task-wrapped workflow target — a `uses: tasks/<ref>` step whose task's
|
|
11
|
+
* OWN target is a workflow (`targets/task.ts`'s `taskDispatch`,
|
|
12
|
+
* `via: "task"`).
|
|
13
|
+
*
|
|
14
|
+
* `childWorkflowDispatch` resolves and qualifies the child ref, enforces the
|
|
15
|
+
* three composition bounds (depth, cycle, aggregate embedded bytes — all
|
|
16
|
+
* FREEZE-time, before publication, `COMPOSITION_INVALID`), recursively
|
|
17
|
+
* freezes the child COMPLETELY through the injected `ResolutionContext.freezeChild`
|
|
18
|
+
* (never a direct import of `compileResolveFreezeWorkflowV4` —
|
|
19
|
+
* `freeze/step-values.ts`'s `ChildCompositionContext` doc explains why: this
|
|
20
|
+
* module is downstream of `ir/freeze-v4.ts`, so importing back from it would
|
|
21
|
+
* close a static cycle), absorbs the child's own fresh source collector into
|
|
22
|
+
* the parent's (A-N7), binds the composing step's effective inputs against
|
|
23
|
+
* the child's declared `params:` (A-N8 — see `AuthoredChildInputs` below for
|
|
24
|
+
* the two ways that mapping arrives), and returns the standard
|
|
25
|
+
* `ResolvedDispatch` envelope carrying a `FrozenChildWorkflowTarget`
|
|
26
|
+
* (`ir/schema-v4.ts`).
|
|
27
|
+
*/
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
import { parseBundleRef } from "../../../core/asset/asset-ref.js";
|
|
30
|
+
import { UsageError } from "../../../core/errors.js";
|
|
31
|
+
import { GuardedExecutionSourceCollector } from "../../../execution/guarded-source.js";
|
|
32
|
+
import { workflowParamContract } from "../../ir/params.js";
|
|
33
|
+
import { canonicalJson, canonicalPlanJson } from "../../ir/plan-hash.js";
|
|
34
|
+
import { utf8Bytes, WORKFLOW_MAX_COMPOSITION_DEPTH, WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES, } from "../../resource-limits.js";
|
|
35
|
+
import { loadWorkflowAsset } from "../../runtime/workflow-asset-loader.js";
|
|
36
|
+
import { resolveOwnedAsset } from "../environment.js";
|
|
37
|
+
import { declaredParamNames, earlierStepIds } from "../step-values.js";
|
|
38
|
+
import { freezeTaskInputBindings, rebindTaskInputBindings } from "../task-bindings.js";
|
|
39
|
+
/** §3.5's exact `contentHash` formula — mirrors `ir/schema-v4.ts`'s private decode-side `childWorkflowContentHash` byte-for-byte (the decoder re-verifies what this produces); duplicated rather than imported so this freeze-side module needs no edit to Lane A's already-landed decoder file. */
|
|
40
|
+
function childWorkflowContentHash(fields) {
|
|
41
|
+
return createHash("sha256")
|
|
42
|
+
.update("akm.workflow.child-workflow\0v1\0")
|
|
43
|
+
.update(canonicalJson({
|
|
44
|
+
ref: fields.ref,
|
|
45
|
+
planHash: fields.planHash,
|
|
46
|
+
via: fields.via,
|
|
47
|
+
taskRef: fields.taskRef ?? null,
|
|
48
|
+
inputBindings: fields.inputBindings.length > 0 ? fields.inputBindings : null,
|
|
49
|
+
}))
|
|
50
|
+
.digest("hex");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Code-review finding (see this file's `environment: []` return field): a
|
|
54
|
+
* step composing a child workflow has no path to honor its own authored
|
|
55
|
+
* `env:` — the child run carries its own frozen environment inside its own
|
|
56
|
+
* plan, so `freezeEnvironment` (the ONE mechanism a step's `env:` reaches a
|
|
57
|
+
* frozen unit through, `../environment.ts`) is never called for this
|
|
58
|
+
* target. Leaving that silent would repeat exactly the defect A-N5 already
|
|
59
|
+
* closed for `with:` on a non-binding surface — an authored construct that
|
|
60
|
+
* cannot be honored on this composing target now rejects instead of
|
|
61
|
+
* vanishing. Checks BOTH shapes a step's `env:` can take (`../environment.ts`'s
|
|
62
|
+
* `freezeEnvironment`): literal `env:` values and `unit: {env: [...]}` refs.
|
|
63
|
+
* An absent/empty `env:` is not authored and stays valid.
|
|
64
|
+
*/
|
|
65
|
+
function assertNoStepEnvironment(stepId, childRef, source) {
|
|
66
|
+
const hasLiteralEnv = Object.keys(source.env ?? {}).length > 0;
|
|
67
|
+
const hasEnvRefs = (source.unit?.env ?? []).length > 0;
|
|
68
|
+
if (!hasLiteralEnv && !hasEnvRefs)
|
|
69
|
+
return;
|
|
70
|
+
throw new UsageError(`Workflow step ${stepId} cannot pass env: while composing ${childRef}: a child run carries its own frozen ` +
|
|
71
|
+
`environment inside its own plan, so a parent-level env: on the composing step cannot be honored. Remove ` +
|
|
72
|
+
`env: from this step, or move it into ${childRef}'s own source.`, "COMPOSITION_INVALID", "Remove the env: (or unit: env:) block from this step, or set those variables inside the child workflow's " +
|
|
73
|
+
"own source — a composing step's environment is never delivered into a child run.");
|
|
74
|
+
}
|
|
75
|
+
/** A workflow entry of a composition `refPath` — the only entries a cycle can close through (§4.5: a task target can never itself be a task, so no task->task chain exists to close one). */
|
|
76
|
+
function isWorkflowRef(ref) {
|
|
77
|
+
try {
|
|
78
|
+
return parseBundleRef(ref).conceptId.startsWith("workflows/");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function compositionPath(refPath, childRef) {
|
|
85
|
+
return [...refPath, childRef].join(" -> ");
|
|
86
|
+
}
|
|
87
|
+
function assertNoCompositionCycle(stepId, childRef, refPath) {
|
|
88
|
+
if (!refPath.filter(isWorkflowRef).includes(childRef))
|
|
89
|
+
return;
|
|
90
|
+
throw new UsageError(`Workflow step ${stepId} cannot compose ${childRef}: that would create a composition cycle. ` +
|
|
91
|
+
`Path: ${compositionPath(refPath, childRef)}.`, "COMPOSITION_INVALID", "Break the cycle: remove or redirect one of the compositions in the path above so no workflow ends up " +
|
|
92
|
+
"composing itself, directly or through intermediates.");
|
|
93
|
+
}
|
|
94
|
+
function assertCompositionDepthAllowed(stepId, childRef, childDepth, refPath) {
|
|
95
|
+
if (childDepth <= WORKFLOW_MAX_COMPOSITION_DEPTH)
|
|
96
|
+
return;
|
|
97
|
+
throw new UsageError(`Workflow step ${stepId} cannot compose ${childRef}: workflow composition is limited to ` +
|
|
98
|
+
`${WORKFLOW_MAX_COMPOSITION_DEPTH} levels. Path: ${compositionPath(refPath, childRef)}.`, "COMPOSITION_INVALID", `Flatten the composition chain to ${WORKFLOW_MAX_COMPOSITION_DEPTH} levels or fewer — inline one of the ` +
|
|
99
|
+
"intermediate workflows, or restructure the chain so fewer child compositions are nested.");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Add the child's embedded plan bytes to the shared, tree-wide budget
|
|
103
|
+
* (A-N6) and fail before publication if the AGGREGATE crosses the cap.
|
|
104
|
+
* Mutates `budget` only on success — a rejected step leaves the running
|
|
105
|
+
* total exactly as it was, matching every other freeze-time failure's
|
|
106
|
+
* no-partial-effect shape.
|
|
107
|
+
*/
|
|
108
|
+
function chargeEmbeddedBudget(stepId, childRef, childPlanBytes, budget) {
|
|
109
|
+
const projected = budget.embeddedBytes + childPlanBytes;
|
|
110
|
+
if (projected > WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES) {
|
|
111
|
+
throw new UsageError(`Workflow step ${stepId} cannot compose ${childRef}: the embedded child plans would total ${projected} ` +
|
|
112
|
+
`bytes, over the ${WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES}-byte limit for one workflow run.`, "COMPOSITION_INVALID", "Reduce the number or size of workflows composed into this run — split the work across separate " +
|
|
113
|
+
"top-level runs, or trim the composed children's own plans.");
|
|
114
|
+
}
|
|
115
|
+
budget.embeddedBytes = projected;
|
|
116
|
+
}
|
|
117
|
+
export async function childWorkflowDispatch(input) {
|
|
118
|
+
const { source, baseUnit, childRefInput, context, via, taskRef, authoredInputs } = input;
|
|
119
|
+
// §4.2 step 1: resolve + qualify. Resolution failures propagate unchanged,
|
|
120
|
+
// in code and shape (row B-11) — the same authority every other
|
|
121
|
+
// composition target (command/script/task) already resolves through.
|
|
122
|
+
const owned = await resolveOwnedAsset(childRefInput, "workflow", context);
|
|
123
|
+
const childAsset = await loadWorkflowAsset(owned.ref);
|
|
124
|
+
const childRef = childAsset.ref;
|
|
125
|
+
// Code-review finding: an authored env: on the composing step has no
|
|
126
|
+
// path to reach the child run and must reject, not vanish (see
|
|
127
|
+
// assertNoStepEnvironment's doc comment).
|
|
128
|
+
assertNoStepEnvironment(source.id, childRef, source);
|
|
129
|
+
// §4.2 steps 2-3: the composition bounds, before any child compilation.
|
|
130
|
+
assertNoCompositionCycle(source.id, childRef, context.composition.refPath);
|
|
131
|
+
const childDepth = context.composition.depth + 1;
|
|
132
|
+
assertCompositionDepthAllowed(source.id, childRef, childDepth, context.composition.refPath);
|
|
133
|
+
// §4.2 step 4: freeze the child COMPLETELY (compile -> validate -> freeze),
|
|
134
|
+
// with its OWN fresh collector (A-N7) so its plan is a pure function of its
|
|
135
|
+
// own source, and the SAME mutable budget object so the aggregate bound
|
|
136
|
+
// sees every descendant across the whole tree.
|
|
137
|
+
const childRefPath = via === "task" && taskRef !== undefined
|
|
138
|
+
? [...context.composition.refPath, taskRef, childRef]
|
|
139
|
+
: [...context.composition.refPath, childRef];
|
|
140
|
+
const child = await context.freezeChild({
|
|
141
|
+
asset: childAsset,
|
|
142
|
+
sourceCollector: new GuardedExecutionSourceCollector(),
|
|
143
|
+
composition: { depth: childDepth, refPath: childRefPath, budget: context.composition.budget },
|
|
144
|
+
});
|
|
145
|
+
// The embedded `frozenPlan` must be a PLAIN, symbol-free JSON structure —
|
|
146
|
+
// exactly the shape `decodeWorkflowPlanV4` will (recursively) re-verify it
|
|
147
|
+
// as, both right now (the PARENT's own top-level `decodeWorkflowPlanV4`
|
|
148
|
+
// call in `ir/freeze-v4.ts` walks straight into this target) and later,
|
|
149
|
+
// read back from a stored run's `plan_json`. `child.plan` — the in-memory
|
|
150
|
+
// result of the child's OWN already-decoded freeze — carries internal
|
|
151
|
+
// resolved-request construction brands (`execution/resolved-request.ts`)
|
|
152
|
+
// that `decodeResolvedExecutionRequest` deliberately rejects on ANY
|
|
153
|
+
// "fresh rehydrate" input; round-tripping through canonical JSON strips
|
|
154
|
+
// them, the same way persisting and re-reading `plan_json` naturally would.
|
|
155
|
+
const embeddedPlanJson = canonicalPlanJson(child.plan);
|
|
156
|
+
const frozenPlan = JSON.parse(embeddedPlanJson);
|
|
157
|
+
// §4.2 step 5.
|
|
158
|
+
chargeEmbeddedBudget(source.id, childRef, utf8Bytes(embeddedPlanJson), context.composition.budget);
|
|
159
|
+
// §4.2 step 6 (A-N7): absorb the child's captured sources so the parent's
|
|
160
|
+
// final pre-publication CAS (`revalidate()`) covers every child file too.
|
|
161
|
+
context.collector.absorb(child.sourceCollector);
|
|
162
|
+
// §4.2 step 7 (A-N8): bind the composing step's effective inputs against
|
|
163
|
+
// the child's declared params:. A genuinely authored `with:` goes through
|
|
164
|
+
// the SAME normalizer every other binding surface uses; an
|
|
165
|
+
// already-classified binding set (a v4 task's effective inputs) is
|
|
166
|
+
// RE-bound instead, never round-tripped back through the `with:` grammar
|
|
167
|
+
// (code-review finding — see AuthoredChildInputs above).
|
|
168
|
+
const inputBindings = authoredInputs.kind === "bindings"
|
|
169
|
+
? rebindTaskInputBindings({
|
|
170
|
+
stepId: source.id,
|
|
171
|
+
targetRef: childRef,
|
|
172
|
+
bindings: authoredInputs.value,
|
|
173
|
+
contract: workflowParamContract(frozenPlan),
|
|
174
|
+
})
|
|
175
|
+
: freezeTaskInputBindings({
|
|
176
|
+
stepId: source.id,
|
|
177
|
+
targetRef: childRef,
|
|
178
|
+
with: authoredInputs.value,
|
|
179
|
+
contract: workflowParamContract(frozenPlan),
|
|
180
|
+
earlierStepIds: earlierStepIds(context.sourceIr, source.id),
|
|
181
|
+
declaredParamNames: declaredParamNames(context.sourceIr),
|
|
182
|
+
});
|
|
183
|
+
// §4.2 step 8: build the frozen target.
|
|
184
|
+
const planHash = createHash("sha256").update(embeddedPlanJson).digest("hex");
|
|
185
|
+
const contentHash = childWorkflowContentHash({ ref: childRef, planHash, via, taskRef, inputBindings });
|
|
186
|
+
const target = Object.freeze({
|
|
187
|
+
kind: "child-workflow",
|
|
188
|
+
ref: childRef,
|
|
189
|
+
planHash,
|
|
190
|
+
frozenPlan,
|
|
191
|
+
contentHash,
|
|
192
|
+
via,
|
|
193
|
+
...(taskRef !== undefined ? { taskRef } : {}),
|
|
194
|
+
...(inputBindings.length > 0 ? { inputBindings } : {}),
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
target,
|
|
198
|
+
// A child run carries its own frozen environment inside its own plan.
|
|
199
|
+
// A composing step's own env: cannot reach it, so assertNoStepEnvironment
|
|
200
|
+
// above rejects one instead of it silently vanishing here (§4.2 step 8).
|
|
201
|
+
environment: [],
|
|
202
|
+
unit: baseUnit,
|
|
203
|
+
instructions: source.instructions ??
|
|
204
|
+
(via === "task" && taskRef !== undefined ? `Run task ${taskRef}.` : `Run workflow ${childRef}.`),
|
|
205
|
+
};
|
|
206
|
+
}
|