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.
Files changed (143) hide show
  1. package/CHANGELOG.md +493 -0
  2. package/STABILITY.md +23 -5
  3. package/dist/assets/hints/cli-hints-full.md +12 -7
  4. package/dist/assets/tasks/core/extract.yml +3 -5
  5. package/dist/assets/tasks/core/improve.yml +3 -5
  6. package/dist/assets/tasks/core/index-refresh.yml +3 -5
  7. package/dist/assets/tasks/core/sync.yml +3 -5
  8. package/dist/assets/tasks/core/version-check.yml +3 -5
  9. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
  10. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
  12. package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
  13. package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
  14. package/dist/cli/unknown-flags.js +12 -1
  15. package/dist/cli.js +8 -1
  16. package/dist/commands/command/command-execution.js +23 -2
  17. package/dist/commands/health/improve-metrics.js +38 -0
  18. package/dist/commands/health/windows.js +8 -4
  19. package/dist/commands/health.js +8 -4
  20. package/dist/commands/lint/index.js +1 -1
  21. package/dist/commands/migrate-cli.js +130 -24
  22. package/dist/commands/proposal/validators/proposal-validators.js +7 -2
  23. package/dist/commands/tasks/explain.js +304 -0
  24. package/dist/commands/tasks/tasks-cli.js +185 -3
  25. package/dist/commands/tasks/tasks.js +265 -52
  26. package/dist/commands/workflow/plan.js +159 -0
  27. package/dist/commands/workflow-cli.js +94 -2
  28. package/dist/core/activation-policy.js +2 -12
  29. package/dist/core/adapter/adapters/akm-lint.js +7 -4
  30. package/dist/core/adapter/adapters/akm-metadata.js +26 -14
  31. package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
  32. package/dist/core/errors.js +45 -0
  33. package/dist/core/json-schema.js +15 -5
  34. package/dist/core/state/migrations.js +57 -0
  35. package/dist/core/state-db.js +16 -14
  36. package/dist/core/subprocess.js +47 -13
  37. package/dist/execution/guarded-source.js +44 -0
  38. package/dist/execution/input-contract.js +250 -0
  39. package/dist/execution/target-ref.js +63 -0
  40. package/dist/indexer/usage/usage-events.js +14 -3
  41. package/dist/integrations/agent/execution-lowering.js +12 -1
  42. package/dist/output/shapes/passthrough.js +2 -0
  43. package/dist/output/text/helpers.js +1 -1
  44. package/dist/output/text/migrate.js +12 -3
  45. package/dist/output/text/workflow-format.js +192 -10
  46. package/dist/output/text/workflow.js +2 -1
  47. package/dist/runtime.js +1 -0
  48. package/dist/scripts/akm-migrate-node.js +11838 -10118
  49. package/dist/scripts/akm-migrate.js +11828 -10117
  50. package/dist/setup/steps/tasks.js +34 -17
  51. package/dist/storage/repositories/task-history-repository.js +5 -1
  52. package/dist/storage/repositories/workflow-runs-repository.js +144 -6
  53. package/dist/tasks/backends/launchd.js +31 -84
  54. package/dist/tasks/embedded.js +13 -7
  55. package/dist/tasks/model/invocation.js +4 -0
  56. package/dist/tasks/prepare/prepare-script-target.js +9 -0
  57. package/dist/tasks/prepare/prepare-support.js +154 -0
  58. package/dist/tasks/prepare/prepare.js +117 -0
  59. package/dist/tasks/prepare/prepared-execution.js +4 -0
  60. package/dist/tasks/prepare/script-capture.js +80 -0
  61. package/dist/tasks/run/attempt-lifecycle.js +165 -0
  62. package/dist/tasks/run/load-task.js +117 -0
  63. package/dist/tasks/run/provenance.js +20 -0
  64. package/dist/tasks/run/run-command-task.js +92 -0
  65. package/dist/tasks/run/run-native-task.js +222 -0
  66. package/dist/tasks/run/run-task.js +99 -0
  67. package/dist/tasks/run/run-workflow-task.js +222 -0
  68. package/dist/tasks/run/task-history.js +134 -0
  69. package/dist/tasks/run/task-log.js +179 -0
  70. package/dist/tasks/run/task-result.js +19 -0
  71. package/dist/tasks/scheduler-binding.js +66 -2
  72. package/dist/tasks/scheduler-invocation.js +63 -3
  73. package/dist/tasks/scheduler-sync.js +77 -14
  74. package/dist/tasks/source/bounded-document.js +455 -0
  75. package/dist/tasks/source/parse-task-source.js +59 -0
  76. package/dist/tasks/source/project-v4.js +62 -0
  77. package/dist/tasks/source/task-input-diagnostics.js +36 -0
  78. package/dist/tasks/source/task-source-v4.js +626 -0
  79. package/dist/tasks/source-v3.js +10 -733
  80. package/dist/tasks/task-run-reserved-flags.js +79 -0
  81. package/dist/workflows/authoring/authoring.js +17 -8
  82. package/dist/workflows/exec/child-invocation.js +34 -0
  83. package/dist/workflows/exec/child-workflow.js +370 -0
  84. package/dist/workflows/exec/exec-unit.js +50 -170
  85. package/dist/workflows/exec/frozen-judge.js +19 -2
  86. package/dist/workflows/exec/native-executor.js +49 -27
  87. package/dist/workflows/exec/param-secrets.js +12 -0
  88. package/dist/workflows/exec/run-workflow.js +48 -59
  89. package/dist/workflows/exec/step-work.js +222 -80
  90. package/dist/workflows/exec/unit-dispatch.js +72 -0
  91. package/dist/workflows/freeze/child-output-references.js +94 -0
  92. package/dist/workflows/freeze/environment.js +174 -0
  93. package/dist/workflows/freeze/identity.js +22 -0
  94. package/dist/workflows/freeze/resolve-steps.js +78 -0
  95. package/dist/workflows/freeze/source-freeze.js +57 -0
  96. package/dist/workflows/freeze/step-values.js +68 -0
  97. package/dist/workflows/freeze/targets/child-workflow.js +206 -0
  98. package/dist/workflows/freeze/targets/command.js +81 -0
  99. package/dist/workflows/freeze/targets/script.js +57 -0
  100. package/dist/workflows/freeze/targets/shell.js +31 -0
  101. package/dist/workflows/freeze/targets/task.js +179 -0
  102. package/dist/workflows/freeze/task-bindings.js +180 -0
  103. package/dist/workflows/ir/compile.js +59 -11
  104. package/dist/workflows/ir/environment-v4.js +3 -3
  105. package/dist/workflows/ir/freeze-v4.js +41 -7
  106. package/dist/workflows/ir/params.js +58 -131
  107. package/dist/workflows/ir/plan-hash.js +3 -3
  108. package/dist/workflows/ir/schema-v4.js +246 -17
  109. package/dist/workflows/parser.js +74 -2
  110. package/dist/workflows/program/schema.js +5 -2
  111. package/dist/workflows/resource-limits.js +20 -0
  112. package/dist/workflows/runtime/plan-classifier.js +24 -7
  113. package/dist/workflows/runtime/run-outputs.js +103 -0
  114. package/dist/workflows/runtime/runs.js +114 -9
  115. package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
  116. package/dist/workflows/source-files.js +5 -5
  117. package/dist/workflows/source-ir/compare.js +17 -0
  118. package/dist/workflows/source-ir/compile.js +7 -3
  119. package/dist/workflows/source-ir/github-yaml.js +64 -17
  120. package/dist/workflows/source-ir/schema.js +69 -21
  121. package/dist/workflows/source-ir/semantics.js +7 -25
  122. package/dist/workflows/source-ir/triggers.js +79 -0
  123. package/dist/workflows/source-ir/uses.js +33 -7
  124. package/docs/migration/README.md +1 -1
  125. package/docs/migration/release-notes/0.9.2.md +87 -11
  126. package/docs/migration/release-notes/README.md +3 -2
  127. package/docs/migration/v0.8-to-v0.9.md +13 -11
  128. package/docs/migration/v0.9.0-troubleshooting.md +20 -13
  129. package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
  130. package/docs/reference/README.md +1 -1
  131. package/docs/reference/cli.md +140 -46
  132. package/docs/reference/configuration.md +6 -5
  133. package/docs/reference/supported-formats.md +9 -5
  134. package/docs/reference/tasks.md +338 -75
  135. package/docs/reference/workflow-schema.md +290 -16
  136. package/docs/reference/workflows.md +57 -7
  137. package/package.json +1 -1
  138. package/schemas/akm-task.json +173 -118
  139. package/schemas/akm-workflow.json +28 -0
  140. package/dist/tasks/runner.js +0 -941
  141. package/dist/tasks/runtime-v3.js +0 -281
  142. package/dist/workflows/ir/source-freeze-v4.js +0 -506
  143. 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
+ }