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,81 @@
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 { createHash } from "node:crypto";
5
+ import { prepareCommandInvocation } from "../../../commands/command/command-execution.js";
6
+ import { captureFrozenDirectoryIdentity } from "../../../execution/directory-identity.js";
7
+ import { freezeExecutableIdentity } from "../../../execution/executable-identity.js";
8
+ import { canonicalResolvedExecutionRequest, } from "../../../execution/resolved-request.js";
9
+ import { fallbackAnnouncement } from "../../../integrations/agent/engine-fallback.js";
10
+ import { requireAuthorizedExecutionPlan } from "../../../integrations/agent/execution-cascade.js";
11
+ import { lowerResolvedExecutionRequest } from "../../../integrations/agent/execution-lowering.js";
12
+ import { prepareInlineExecution } from "../../../integrations/agent/inline-execution.js";
13
+ import { freezeEnvironment, guardedExecutionSource } from "../environment.js";
14
+ import { gitIdentity } from "../identity.js";
15
+ import { durableRequest, executionUnitValues, executionValues, targetConcurrency, } from "../step-values.js";
16
+ export async function commandDispatch(source, baseUnit, action, context) {
17
+ const prepared = await prepareCommandInvocation({
18
+ action,
19
+ config: context.config,
20
+ invocationKind: "workflow",
21
+ ...(context.sourceIr.defaults
22
+ ? { invocationDefaults: executionUnitValues(context.sourceIr.defaults, context.asset.sourcePath) }
23
+ : {}),
24
+ ...(source.commandMode === "literal" ? { inlineContentMode: "literal" } : {}),
25
+ current: executionValues(source, context.asset.sourcePath),
26
+ sourceLoader: (ref, kind) => guardedExecutionSource(ref, kind, context),
27
+ });
28
+ return commandResult(source, baseUnit, prepared, context);
29
+ }
30
+ export function inlineDispatch(source, baseUnit, context) {
31
+ const content = source.instructions ?? `Execute workflow step ${source.id}.`;
32
+ const prepared = prepareInlineExecution({
33
+ content,
34
+ config: context.config,
35
+ invocationKind: "workflow",
36
+ ...(context.sourceIr.defaults
37
+ ? { invocationDefaults: executionUnitValues(context.sourceIr.defaults, context.asset.sourcePath) }
38
+ : {}),
39
+ current: executionValues(source, context.asset.sourcePath),
40
+ });
41
+ return commandResult(source, baseUnit, prepared, context);
42
+ }
43
+ export function commandResult(source, baseUnit, prepared, context, literals = []) {
44
+ const request = durableRequest(requireAuthorizedExecutionPlan(prepared.plan));
45
+ const lowered = lowerResolvedExecutionRequest(request, prepared.config);
46
+ const cwdIdentity = captureFrozenDirectoryIdentity(context.asset.sourcePath);
47
+ let runner = lowered.runner;
48
+ let executable;
49
+ if (runner.kind === "agent") {
50
+ executable = freezeExecutableIdentity(runner.profile.bin, { cwd: cwdIdentity.realCwd });
51
+ runner = Object.freeze({ ...runner, profile: Object.freeze({ ...runner.profile, bin: executable.absolutePath }) });
52
+ }
53
+ const unit = {
54
+ ...baseUnit,
55
+ engine: request.engine.name,
56
+ ...(request.model ? { model: request.model.resolved } : {}),
57
+ ...(Object.hasOwn(request.runtime, "timeoutMs") ? { timeoutMs: request.runtime.timeoutMs } : {}),
58
+ ...(request.inference ? { llm: request.inference } : {}),
59
+ ...(request.outputSchema ? { output: request.outputSchema } : {}),
60
+ };
61
+ const environment = Object.freeze([...literals, ...freezeEnvironment(source, undefined, context)]);
62
+ const target = Object.freeze({
63
+ kind: "command",
64
+ ref: request.command.source?.ref ?? null,
65
+ contentHash: createHash("sha256").update(request.command.content).digest("hex"),
66
+ request: JSON.parse(canonicalResolvedExecutionRequest(request)),
67
+ runner,
68
+ ...(targetConcurrency(runner, context.config) ? { concurrency: targetConcurrency(runner, context.config) } : {}),
69
+ cwdIdentity,
70
+ ...(executable ? { executable } : {}),
71
+ ...gitIdentity(baseUnit, cwdIdentity.realRoot),
72
+ });
73
+ const engineAnnouncement = fallbackAnnouncement(prepared.fallbackEngineName, request.engine.name);
74
+ return {
75
+ target,
76
+ environment,
77
+ unit,
78
+ instructions: request.command.content,
79
+ ...(engineAnnouncement ? { engineAnnouncement } : {}),
80
+ };
81
+ }
@@ -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 { freezeExecutableIdentity } from "../../../execution/executable-identity.js";
5
+ import { prepareScriptTarget } from "../../../tasks/prepare/prepare-script-target.js";
6
+ import { captureOwned, freezeEnvironment, resolveOwnedAsset } from "../environment.js";
7
+ import { gitIdentity, scriptExecutable } from "../identity.js";
8
+ import { freezeExecSpec } from "../step-values.js";
9
+ export async function directScript(source, baseUnit, refInput, context) {
10
+ const owned = await resolveOwnedAsset(refInput, "script", context);
11
+ captureOwned(owned, context.collector);
12
+ // Typed preparer (P1b spec §4.3) — no synthetic task YAML, no parseTaskV3Yaml
13
+ // call, no fabricated schedule/filePath/taskId/taskRef. The script's own
14
+ // owned identity (ref/file/bundleRoot) is all prepareScriptTarget needs.
15
+ const captured = prepareScriptTarget({
16
+ ref: owned.ref,
17
+ file: owned.file,
18
+ bundleRoot: owned.root,
19
+ readFile: () => context.collector.readBytes(owned.file, owned.root),
20
+ });
21
+ return scriptResult(source, baseUnit, {
22
+ sourceRef: captured.ref,
23
+ interpreter: captured.interpreter,
24
+ extension: captured.extension,
25
+ bytesBase64: captured.bytesBase64,
26
+ byteLength: captured.byteLength,
27
+ sha256: captured.sha256,
28
+ cwdIdentity: captured.cwdIdentity,
29
+ }, context, []);
30
+ }
31
+ export function scriptResult(source, baseUnit, prepared, context, literals) {
32
+ const requestedExecutable = scriptExecutable(prepared.interpreter);
33
+ const executable = freezeExecutableIdentity(requestedExecutable, { cwd: prepared.cwdIdentity.realCwd });
34
+ const authoredExec = { command: [executable.absolutePath, "<frozen-script>"] };
35
+ const exec = freezeExecSpec(source, authoredExec, context);
36
+ const environment = Object.freeze([...literals, ...freezeEnvironment(source, authoredExec, context)]);
37
+ const target = Object.freeze({
38
+ kind: "script",
39
+ ref: prepared.sourceRef,
40
+ contentHash: prepared.sha256,
41
+ exec,
42
+ interpreter: prepared.interpreter,
43
+ extension: prepared.extension,
44
+ bytesBase64: prepared.bytesBase64,
45
+ byteLength: prepared.byteLength,
46
+ cwdIdentity: prepared.cwdIdentity,
47
+ materialization: "ephemeral-0700-delete",
48
+ executable,
49
+ ...gitIdentity(baseUnit, prepared.cwdIdentity.realRoot),
50
+ });
51
+ return {
52
+ target,
53
+ environment,
54
+ unit: { ...baseUnit, exec: authoredExec },
55
+ instructions: source.instructions ?? `Run script ${prepared.sourceRef}.`,
56
+ };
57
+ }
@@ -0,0 +1,31 @@
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 { captureFrozenDirectoryIdentity } from "../../../execution/directory-identity.js";
5
+ import { freezeExecutableIdentity } from "../../../execution/executable-identity.js";
6
+ import { freezeEnvironment } from "../environment.js";
7
+ import { gitIdentity } from "../identity.js";
8
+ import { freezeExecSpec } from "../step-values.js";
9
+ export function directShell(source, baseUnit, context) {
10
+ const authoredExec = baseUnit.exec;
11
+ if (!authoredExec)
12
+ throw new Error(`workflow shell step ${source.id} lost its source-IR execution spec`);
13
+ const exec = freezeExecSpec(source, authoredExec, context);
14
+ const cwdIdentity = captureFrozenDirectoryIdentity(context.asset.sourcePath, authoredExec.cwd);
15
+ const executable = freezeExecutableIdentity(authoredExec.command[0], { cwd: cwdIdentity.realCwd });
16
+ const environment = Object.freeze(freezeEnvironment(source, authoredExec, context));
17
+ const target = Object.freeze({
18
+ kind: "shell",
19
+ contentHash: "",
20
+ exec,
21
+ cwdIdentity,
22
+ executable,
23
+ ...gitIdentity(baseUnit, cwdIdentity.realRoot),
24
+ });
25
+ return {
26
+ target,
27
+ environment,
28
+ unit: { ...baseUnit, exec },
29
+ instructions: source.instructions ?? `Run ${source.run ?? authoredExec.command.join(" ")}.`,
30
+ };
31
+ }
@@ -0,0 +1,179 @@
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 path from "node:path";
5
+ import { parseBundleRef } from "../../../core/asset/asset-ref.js";
6
+ import { UsageError } from "../../../core/errors.js";
7
+ import { freezeExecutableIdentity } from "../../../execution/executable-identity.js";
8
+ import { prepareTaskV3Execution } from "../../../tasks/prepare/prepare.js";
9
+ import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
10
+ import { projectTaskSourceV4 } from "../../../tasks/source/project-v4.js";
11
+ import { workflowShellCommand } from "../../source-ir/program.js";
12
+ import { captureOwned, freezeEnvironment, guardedExecutionSource, resolveOwnedAsset } from "../environment.js";
13
+ import { gitIdentity } from "../identity.js";
14
+ import { declaredParamNames, earlierStepIds, freezeExecSpec, } from "../step-values.js";
15
+ import { freezeTaskInputBindings } from "../task-bindings.js";
16
+ import { childWorkflowDispatch } from "./child-workflow.js";
17
+ import { commandResult } from "./command.js";
18
+ import { scriptResult } from "./script.js";
19
+ /** The one message shared by every "cannot prove this is a valid binding surface" rejection (A-N5). */
20
+ function noDeclaredInputsError(stepId, ref) {
21
+ return new UsageError(`Workflow step ${stepId} cannot pass with: to task target ${ref}; ${ref} declares no inputs.`, "COMPOSITION_INVALID");
22
+ }
23
+ /**
24
+ * A-N6 (spec docs/plans/specs/p2b-input-bindings.md §1.7): LC-N1's
25
+ * peek-and-throw (p2a §1.5) is GONE — a version: 4 target now composes.
26
+ * `parseTaskSource` parses the YAML ONCE and routes on the SAME {root,
27
+ * lineAt} the v3 arm always used, so a v3 composition stays byte-identical
28
+ * (B-07, B-08). `contract` is undefined for a v3 task (which can never
29
+ * declare `inputs:`, P2a §1.2 D2) or a v4 task with no `inputs:` key at all —
30
+ * either way, "no declared inputs" (A-N5).
31
+ *
32
+ * RECORDED TENSION (spec §0, Review log): A-N5's "no declared inputs"
33
+ * rejection is reasoned from the target's PARSED `inputs:` contract, which
34
+ * needs the target to resolve — yet `tests/workflows/with-rejection.test.ts`
35
+ * B-02b pins `COMPOSITION_INVALID` (not an asset-resolution error) for an
36
+ * UNRESOLVABLE task ref with an authored `with:`. Reconciled here: an
37
+ * authored `with:` whose target cannot even be resolved/parsed "cannot be
38
+ * proven a valid binding surface", so it is refused the same
39
+ * no-declared-inputs way — a `with:`-free step's resolution failure is
40
+ * untouched and propagates as before.
41
+ *
42
+ * Code-review finding: that reconciliation must stay scoped to ASSET
43
+ * resolution (`resolveOwnedAsset`/`captureOwned`) only. Once the target
44
+ * resolves to a real file, `parseTaskSource`/`projectTaskSourceV4` reports a
45
+ * genuine, path-and-line-anchored defect in the composed task's OWN source
46
+ * (e.g. `TASK_SOURCE_INVALID` for an unsupported `inputs.<name>` field) —
47
+ * that is never "cannot be proven a valid binding surface" and must not be
48
+ * repainted as `noDeclaredInputsError`. Parsing therefore happens OUTSIDE
49
+ * the try/catch below, so its errors propagate unchanged regardless of
50
+ * whether this step authored a `with:`.
51
+ */
52
+ async function resolveTaskForComposition(source, refInput, context) {
53
+ const { owned, retained } = await resolveAndCaptureTaskAsset(source, refInput, context);
54
+ const parsed = parseTaskSource({ yaml: retained.content, filePath: owned.file, workspaceRoot: owned.root });
55
+ const contract = parsed.v4.inputs;
56
+ const task = projectTaskSourceV4(parsed.v4);
57
+ return { owned, task, contract };
58
+ }
59
+ /** The asset-resolution-only half of {@link resolveTaskForComposition} — the sole failure mode an authored `with:` on an unresolvable ref may repaint as `noDeclaredInputsError` (A-N5's `with-rejection.test.ts` B-02b). */
60
+ async function resolveAndCaptureTaskAsset(source, refInput, context) {
61
+ try {
62
+ const owned = await resolveOwnedAsset(refInput, "task", context);
63
+ const retained = captureOwned(owned, context.collector);
64
+ return { owned, retained };
65
+ }
66
+ catch (cause) {
67
+ if (source.with !== undefined)
68
+ throw noDeclaredInputsError(source.id, refInput);
69
+ throw cause;
70
+ }
71
+ }
72
+ export async function taskDispatch(source, baseUnit, refInput, context) {
73
+ const { owned, task, contract } = await resolveTaskForComposition(source, refInput, context);
74
+ // A-N5: a with: on a target that declares no inputs: at all is
75
+ // COMPOSITION_INVALID. Fires on ANY authored with: shape, including `{}`
76
+ // (the check is `!== undefined`, not "non-empty").
77
+ if (source.with !== undefined && contract === undefined) {
78
+ throw noDeclaredInputsError(source.id, refInput);
79
+ }
80
+ // P3a (docs/plans/specs/p3a-plan-v5-child-freeze.md §4.2, row B-12): a task
81
+ // whose OWN target is `uses: workflows/<ref>` used to fast-fail here,
82
+ // before ever calling prepareTaskV3Execution. That rejection is REMOVED —
83
+ // flow continues to the bindings computation and the prepare call exactly
84
+ // as for any other task target, and `prepared.kind === "workflow"` below
85
+ // routes to the ONE child-workflow resolver instead.
86
+ // Lane A2 (§3.2-§3.5): normalize THIS step's own with: against THIS task's
87
+ // OWN declared inputs — no merge across a composition chain (B-29). A task
88
+ // with no inputs: has contract === undefined, so an empty {} contract is
89
+ // used; freezeTaskInputBindings then produces no bindings for it (there is
90
+ // nothing to bind, and the COMPOSITION_INVALID check above already fired
91
+ // for any authored with:).
92
+ const bindings = freezeTaskInputBindings({
93
+ stepId: source.id,
94
+ targetRef: refInput,
95
+ with: source.with,
96
+ contract: contract ?? {},
97
+ earlierStepIds: earlierStepIds(context.sourceIr, source.id),
98
+ declaredParamNames: declaredParamNames(context.sourceIr),
99
+ });
100
+ const prepared = await prepareTaskV3Execution(task, {
101
+ taskId: parseBundleRef(owned.ref).conceptId.slice("tasks/".length),
102
+ taskRef: owned.ref,
103
+ bundleName: owned.bundle,
104
+ bundleRoot: owned.root,
105
+ config: context.config,
106
+ commandSourceLoader: (ref, kind) => guardedExecutionSource(ref, kind, context),
107
+ resolveAsset: async ({ ref, type }) => {
108
+ const target = await resolveOwnedAsset(ref, type, context);
109
+ captureOwned(target, context.collector);
110
+ return { file: target.file, bundleRoot: target.root };
111
+ },
112
+ readFile: (file, root = owned.root) => context.collector.readBytes(file, root),
113
+ });
114
+ if (prepared.kind === "workflow") {
115
+ // P3a (spec §4.2, rows B-12/B-13): routes to the ONE child-workflow
116
+ // resolver instead of rejecting. `prepared.ref`/`prepared.taskRef` are
117
+ // already qualified (§4.2 step 1 re-resolves and re-canonicalizes them
118
+ // regardless). Always hands over the bindings just computed above — this
119
+ // task's OWN effective inputs, already classified once against its own
120
+ // contract — to be RE-bound against the child's declared `params:`
121
+ // (never round-tripped through the `with:` grammar: doing so would let a
122
+ // literal value shaped like `{from: ...}` be silently reinterpreted as a
123
+ // reference, code-review finding). Task source v4 rejects `with:` on any
124
+ // target but `uses: akm/command` (row B-28), so a workflow-target task
125
+ // can no longer author `with:` at all — the `{kind:"with"}` arm this
126
+ // ternary used to reach for a v3 task, or a v4 task with no `inputs:`,
127
+ // is unreachable from here and deleted (P4 §3.2.7, row B-30).
128
+ // `AuthoredChildInputs`'s `{kind:"with"}` member itself stays — the
129
+ // DIRECT composition path (`resolve-steps.ts`) still produces it.
130
+ return childWorkflowDispatch({
131
+ source,
132
+ baseUnit,
133
+ childRefInput: prepared.ref,
134
+ context,
135
+ via: "task",
136
+ taskRef: prepared.taskRef,
137
+ authoredInputs: { kind: "bindings", value: bindings },
138
+ });
139
+ }
140
+ const taskLiterals = Object.entries(prepared.environment).map(([name, value]) => Object.freeze({ kind: "literal", name, value }));
141
+ if (prepared.kind === "command") {
142
+ return withInputBindings(commandResult(source, baseUnit, prepared.invocation, context, taskLiterals), bindings);
143
+ }
144
+ if (prepared.kind === "shell") {
145
+ const authoredExec = {
146
+ command: workflowShellCommand(prepared.shell, prepared.command),
147
+ ...(prepared.cwdIdentity.realCwd !== prepared.cwdIdentity.realRoot
148
+ ? { cwd: path.relative(prepared.cwdIdentity.realRoot, prepared.cwdIdentity.realCwd) }
149
+ : {}),
150
+ };
151
+ const exec = freezeExecSpec(source, authoredExec, context);
152
+ const environment = Object.freeze([...taskLiterals, ...freezeEnvironment(source, authoredExec, context)]);
153
+ const executable = freezeExecutableIdentity(exec.command[0], { cwd: prepared.cwdIdentity.realCwd });
154
+ const target = Object.freeze({
155
+ kind: "shell",
156
+ contentHash: "",
157
+ exec,
158
+ cwdIdentity: prepared.cwdIdentity,
159
+ executable,
160
+ ...gitIdentity(baseUnit, prepared.cwdIdentity.realRoot),
161
+ });
162
+ return withInputBindings({
163
+ target,
164
+ environment,
165
+ unit: { ...baseUnit, exec: authoredExec },
166
+ instructions: source.instructions ?? `Run task ${owned.ref}.`,
167
+ }, bindings);
168
+ }
169
+ return withInputBindings(scriptResult(source, baseUnit, prepared, context, taskLiterals), bindings);
170
+ }
171
+ /** Attach the frozen inputBindings (A-N7) to whichever target shape taskDispatch produced. Absent, never [], when empty. */
172
+ function withInputBindings(resolved, bindings) {
173
+ if (bindings.length === 0)
174
+ return resolved;
175
+ return {
176
+ ...resolved,
177
+ target: Object.freeze({ ...resolved.target, inputBindings: bindings }),
178
+ };
179
+ }
@@ -0,0 +1,180 @@
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 pure `with:` -> `TaskInputBinding[]` normalizer, plus the pure
6
+ * re-binder a recursive composition needs on top of it.
7
+ * {@link freezeTaskInputBindings} classifies a genuinely AUTHORED `with:`
8
+ * record against a contract (called by `freeze/targets/task.ts`'s
9
+ * `taskDispatch`, this step's own `with:` against the composed task's own
10
+ * contract only — no merge across a composition chain).
11
+ * {@link rebindTaskInputBindings} instead re-binds an ALREADY-classified
12
+ * `TaskInputBinding[]` by name against a DIFFERENT contract, trusting each
13
+ * entry's existing `kind` rather than re-deriving it from the value's shape
14
+ * (`freeze/targets/child-workflow.ts`'s only caller). Everything decidable
15
+ * at FREEZE time is decided here; a reference's *resolved* value is
16
+ * validated PRE-ATTEMPT instead (`exec/step-work.ts`). Pure function: no IO,
17
+ * no config reads.
18
+ *
19
+ * See docs/architecture/decisions/0008-task-binding-normalization.md for the
20
+ * full design history, including why re-binding cannot reuse the
21
+ * shape-driven normalizer (a code-review finding).
22
+ */
23
+ import { UsageError } from "../../core/errors.js";
24
+ import { validateInputs } from "../../execution/input-contract.js";
25
+ import { parseReference } from "../program/expressions.js";
26
+ function inputBindingInvalid(message) {
27
+ return new UsageError(message, "INPUT_BINDING_INVALID");
28
+ }
29
+ function isPlainObject(value) {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
32
+ function unknownBindingNameError(stepId, targetRef, name, declaredNames) {
33
+ return inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name}, which is not a declared input. ` +
34
+ `Declared inputs: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}.`);
35
+ }
36
+ /**
37
+ * Normalize the workflow source front end's authored `with:` record into the
38
+ * `TaskInputBinding[]` a task-composing step's frozen target carries (spec
39
+ * §3.3). Throws `UsageError`/`INPUT_BINDING_INVALID` for the first violation
40
+ * found — per authored entry first (B-11, B-15, B-16, B-17, B-18), then over
41
+ * the whole contract (B-12, B-13). The result is sorted by name; an entry
42
+ * exists only for a declared input with an effective value (authored
43
+ * literal, authored reference, or an applied default) — never for an
44
+ * unsupplied optional input with no default (B-20).
45
+ */
46
+ export function freezeTaskInputBindings(input) {
47
+ const { stepId, targetRef, contract, earlierStepIds, declaredParamNames } = input;
48
+ const authored = input.with ?? {};
49
+ const declaredNames = Object.keys(contract).sort();
50
+ const byName = new Map();
51
+ for (const [name, value] of Object.entries(authored)) {
52
+ if (!Object.hasOwn(contract, name))
53
+ throw unknownBindingNameError(stepId, targetRef, name, declaredNames);
54
+ const declaration = contract[name];
55
+ if (!declaration)
56
+ throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} is invalid.`);
57
+ byName.set(name, normalizeOneEntry(stepId, targetRef, name, value, declaration.schema, earlierStepIds, declaredParamNames));
58
+ }
59
+ return finalizeBindings(stepId, targetRef, contract, byName);
60
+ }
61
+ /**
62
+ * Re-bind an ALREADY-NORMALIZED `TaskInputBinding[]` — a v4 task's own
63
+ * effective inputs, classified once against the TASK's own declared
64
+ * `inputs:` contract by {@link freezeTaskInputBindings} — against a
65
+ * DIFFERENT contract (the child workflow's declared `params:`) by NAME,
66
+ * without re-deriving each entry's literal/reference classification from
67
+ * its value's shape (a code-review finding — see
68
+ * docs/architecture/decisions/0008-task-binding-normalization.md for why a
69
+ * shape-shifting round-trip through {@link normalizeOneEntry} would
70
+ * silently misclassify a literal value shaped like a reference).
71
+ *
72
+ * Per-entry rules, otherwise identical to {@link freezeTaskInputBindings}:
73
+ * an entry naming a key the new `contract` does not declare is
74
+ * `INPUT_BINDING_INVALID` (same message shape); a `kind: "literal"` entry
75
+ * keeps its value verbatim and is validated against the NEW contract's
76
+ * declared schema for that name; a `kind: "reference"` entry keeps its
77
+ * `from` verbatim (the reference target — an earlier step or declared param
78
+ * of the composing workflow — does not change with which contract it is
79
+ * bound against) and its `schema` is re-derived from the NEW contract,
80
+ * matching {@link normalizeOneEntry}'s existing rule that a reference
81
+ * binding's schema always comes from the contract it is bound against; a
82
+ * contract key absent from `bindings` is defaulted or required exactly as
83
+ * {@link freezeTaskInputBindings} does.
84
+ */
85
+ export function rebindTaskInputBindings(input) {
86
+ const { stepId, targetRef, contract } = input;
87
+ const declaredNames = Object.keys(contract).sort();
88
+ const byName = new Map();
89
+ for (const binding of input.bindings ?? []) {
90
+ if (!Object.hasOwn(contract, binding.name)) {
91
+ throw unknownBindingNameError(stepId, targetRef, binding.name, declaredNames);
92
+ }
93
+ const declaration = contract[binding.name];
94
+ if (!declaration)
95
+ throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${binding.name} is invalid.`);
96
+ byName.set(binding.name, binding.kind === "literal"
97
+ ? binding
98
+ : Object.freeze({ kind: "reference", name: binding.name, from: binding.from, schema: declaration.schema }));
99
+ }
100
+ return finalizeBindings(stepId, targetRef, contract, byName);
101
+ }
102
+ /**
103
+ * The tail shared by {@link freezeTaskInputBindings} and
104
+ * {@link rebindTaskInputBindings} once `byName` holds one classified entry
105
+ * per AUTHORED/bound name: apply declared defaults for every remaining
106
+ * contract key (or throw for a required one with none), schema-validate
107
+ * every literal (authored, re-bound, or defaulted) against the contract, and
108
+ * return the result sorted by name.
109
+ */
110
+ function finalizeBindings(stepId, targetRef, contract, byName) {
111
+ for (const [name, declaration] of Object.entries(contract)) {
112
+ if (byName.has(name))
113
+ continue;
114
+ if (Object.hasOwn(declaration, "default")) {
115
+ byName.set(name, Object.freeze({ kind: "literal", name, value: declaration.default }));
116
+ continue;
117
+ }
118
+ if (declaration.required) {
119
+ throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef}, which declares required input "${name}" with no default; ` +
120
+ `supply it with with.${name}.`);
121
+ }
122
+ }
123
+ // Every LITERAL value (authored, re-bound, or defaulted) is validated
124
+ // against its own declared schema — a contract NARROWED to just the
125
+ // literal-bound names, so a required input bound via REFERENCE (whose
126
+ // value is not known until pre-attempt, §3.6) is never wrongly flagged
127
+ // "missing" here.
128
+ const literalContract = {};
129
+ const literalValues = {};
130
+ for (const binding of byName.values()) {
131
+ if (binding.kind !== "literal")
132
+ continue;
133
+ const declaration = contract[binding.name];
134
+ if (declaration)
135
+ literalContract[binding.name] = declaration;
136
+ literalValues[binding.name] = binding.value;
137
+ }
138
+ const schemaErrors = validateInputs(literalContract, literalValues, { pathRoot: "with" });
139
+ if (schemaErrors.length > 0) {
140
+ throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef}: ${schemaErrors.join("; ")}`);
141
+ }
142
+ const sorted = [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
143
+ return Object.freeze(sorted);
144
+ }
145
+ /**
146
+ * Classify and validate ONE authored `with:` entry whose key is already
147
+ * known to be a declared input name. §3.3 point 2/3: a value is a
148
+ * `{kind:"reference"}` binding IFF it is a non-null, non-array plain object
149
+ * whose OWN key set is exactly `["from"]` and whose `from` is a string
150
+ * `parseReference` accepts — the hard-fail band (B-15, B-16) means any OTHER
151
+ * shape carrying an own `from` key is `INPUT_BINDING_INVALID`, never
152
+ * reinterpreted as a literal.
153
+ */
154
+ function normalizeOneEntry(stepId, targetRef, name, value, schema, earlierStepIds, declaredParamNames) {
155
+ if (!isPlainObject(value) || !Object.hasOwn(value, "from")) {
156
+ return Object.freeze({ kind: "literal", name, value });
157
+ }
158
+ const keys = Object.keys(value);
159
+ if (keys.length !== 1 || typeof value.from !== "string") {
160
+ throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} looks like a reference binding ({from: ...}) ` +
161
+ `but is not one: it must have exactly one key, "from", whose value is a reference string.`);
162
+ }
163
+ const parsed = parseReference(value.from);
164
+ if (!parsed.ok) {
165
+ throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} reference ${JSON.stringify(value.from)} is ` +
166
+ `invalid: ${parsed.message}`);
167
+ }
168
+ if (parsed.expr.kind === "stepOutput") {
169
+ if (!earlierStepIds.has(parsed.expr.stepId)) {
170
+ throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} reference ${value.from} does not name an ` +
171
+ `earlier step of this workflow.`);
172
+ }
173
+ }
174
+ else if (!declaredParamNames.has(parsed.expr.name)) {
175
+ const sortedParams = [...declaredParamNames].sort();
176
+ throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} reference ${value.from} does not name a ` +
177
+ `declared workflow param; declared params: ${sortedParams.length > 0 ? sortedParams.join(", ") : "(none)"}.`);
178
+ }
179
+ return Object.freeze({ kind: "reference", name, from: value.from, schema });
180
+ }
@@ -38,17 +38,9 @@ import { decodeWorkflowSourceIrV1, } from "../source-ir/schema.js";
38
38
  export function compileWorkflowPlan(input, title, resolvedUnits = new Map()) {
39
39
  const sourceIr = decodeWorkflowSourceIrV1(input);
40
40
  const errors = [];
41
- if (sourceIr.jobs.length !== 1) {
42
- return {
43
- ok: false,
44
- errors: [
45
- {
46
- line: sourceIr.jobs[1]?.source.start ?? sourceIr.source.start,
47
- message: "Current workflow execution requires exactly one source-IR job.",
48
- },
49
- ],
50
- };
51
- }
41
+ // The decoder guarantees exactly one job (P4 §3.3, docs/plans/specs/
42
+ // p4-deletions-closeout.md, row B-43) — a 2+-job document is rejected at
43
+ // the adapter boundary and never reaches this compiler.
52
44
  const sourceSteps = sourceIr.jobs[0]?.steps ?? [];
53
45
  const allStepIds = new Set(sourceSteps.map((step) => step.id));
54
46
  const earlierStepIds = new Set();
@@ -75,6 +67,13 @@ export function compileWorkflowPlan(input, title, resolvedUnits = new Map()) {
75
67
  steps.push(compileStep(step, sequenceIndex, sourceIr.defaults, resolvedUnits.get(step.id)));
76
68
  earlierStepIds.add(step.id);
77
69
  });
70
+ // P3b (spec §4.2, rows B-06/B-07): `outputs:` references are validated
71
+ // against the FULL step id set (never `earlierStepIds` — resolution happens
72
+ // at RUN COMPLETION, after every step has run, so an output may legitimately
73
+ // name any declared step regardless of its position).
74
+ for (const [name, declaration] of Object.entries(sourceIr.outputs ?? {})) {
75
+ checkOutputReference(name, declaration.from, { errors, allStepIds, line: sourceIr.source.start });
76
+ }
78
77
  if (errors.length > 0)
79
78
  return { ok: false, errors };
80
79
  const paramNames = sourceIr.params ? Object.keys(sourceIr.params) : [];
@@ -87,6 +86,7 @@ export function compileWorkflowPlan(input, title, resolvedUnits = new Map()) {
87
86
  ...(sourceIr.params && paramNames.length > 0
88
87
  ? { paramSchemas: sourceIr.params }
89
88
  : {}),
89
+ ...(sourceIr.outputs ? { outputs: sortedOutputs(sourceIr.outputs) } : {}),
90
90
  ...(sourceIr.budget
91
91
  ? {
92
92
  budget: {
@@ -99,6 +99,24 @@ export function compileWorkflowPlan(input, title, resolvedUnits = new Map()) {
99
99
  },
100
100
  };
101
101
  }
102
+ /**
103
+ * Re-key `outputs:` into canonical wire order (code-point-ascending by name)
104
+ * — the order {@link decodeWorkflowOutputs} in `ir/schema-v4.ts` requires,
105
+ * mirroring how `freeze/task-bindings.ts`'s `finalizeBindings` sorts
106
+ * `inputBindings` before freezing. The source parser preserves AUTHOR order
107
+ * (P3b spec §4.2 places no ordering requirement on authoring), so this is the
108
+ * one place that must impose it.
109
+ */
110
+ function sortedOutputs(outputs) {
111
+ const sorted = {};
112
+ for (const name of Object.keys(outputs).sort(compareCodePoints)) {
113
+ sorted[name] = outputs[name];
114
+ }
115
+ return sorted;
116
+ }
117
+ function compareCodePoints(left, right) {
118
+ return left < right ? -1 : left > right ? 1 : 0;
119
+ }
102
120
  function compileStep(step, sequenceIndex, defaults, resolved) {
103
121
  const gate = {
104
122
  kind: "gate",
@@ -213,6 +231,36 @@ function checkInputReference(text, index, check) {
213
231
  });
214
232
  }
215
233
  }
234
+ /**
235
+ * Validate one `outputs.<name>.from` reference (P3b, spec §4.2, rows
236
+ * B-06/B-07): it must parse, it must name a STEP output — never a param (an
237
+ * output projects a step artifact; a param is already on the run row) — and
238
+ * that step must be DECLARED somewhere in the document. Unlike
239
+ * {@link checkInputReference}, the named step need not be EARLIER: an output
240
+ * resolves at run completion, after every step has already run.
241
+ */
242
+ function checkOutputReference(name, text, check) {
243
+ const parsed = parseReference(text);
244
+ if (!parsed.ok) {
245
+ check.errors.push({ line: check.line, message: `Output "${name}" from: ${parsed.message}` });
246
+ return;
247
+ }
248
+ if (parsed.expr.kind === "param") {
249
+ check.errors.push({
250
+ line: check.line,
251
+ message: `Output "${name}" from: "${formatReference(parsed.expr)}" names a param, not a step output — an output ` +
252
+ `projects a STEP artifact, never a param. "outputs:" only names step outputs (steps.<id>.output...).`,
253
+ });
254
+ return;
255
+ }
256
+ if (!check.allStepIds.has(parsed.expr.stepId)) {
257
+ check.errors.push({
258
+ line: check.line,
259
+ message: `Output "${name}" from: "${formatReference(parsed.expr)}" cannot be resolved — "${parsed.expr.stepId}" is ` +
260
+ `not a step in this workflow.`,
261
+ });
262
+ }
263
+ }
216
264
  // ── Non-fatal warnings ───────────────────────────────────────────────────────
217
265
  /**
218
266
  * Collect the document's non-fatal WARNINGS — advisories that never fail
@@ -30,10 +30,10 @@ export function freezeWorkflowEnvironment(refs, options) {
30
30
  const resolved = options.resolveRef(inputRef);
31
31
  const match = QUALIFIED_ENV_REF_RE.exec(resolved.ref);
32
32
  if (!match || match[1] !== resolved.bundle) {
33
- throw new UsageError(`Workflow env ref ${JSON.stringify(inputRef)} did not resolve to a canonical fully-qualified owner.`, "INVALID_FLAG_VALUE");
33
+ throw new UsageError(`Workflow env ref ${JSON.stringify(inputRef)} did not resolve to a canonical fully-qualified owner.`, "WORKFLOW_SOURCE_INVALID");
34
34
  }
35
35
  if (logical.has(resolved.ref)) {
36
- throw new UsageError(`Workflow environment contains duplicate ref ${resolved.ref}.`, "INVALID_FLAG_VALUE");
36
+ throw new UsageError(`Workflow environment contains duplicate ref ${resolved.ref}.`, "WORKFLOW_SOURCE_INVALID");
37
37
  }
38
38
  logical.add(resolved.ref);
39
39
  trackParentDirectories(collector, resolved.root, resolved.path);
@@ -324,5 +324,5 @@ function physicalIdentity(realPath, stat) {
324
324
  return stat.ino === 0n ? `path:${realPath}` : `inode:${stat.dev}:${stat.ino}`;
325
325
  }
326
326
  function invalid(message) {
327
- throw new UsageError(`Invalid frozen workflow environment: ${message}.`, "INVALID_FLAG_VALUE");
327
+ throw new UsageError(`Invalid frozen workflow environment: ${message}.`, "WORKFLOW_SOURCE_INVALID");
328
328
  }