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,103 @@
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
+ * Declared-output resolution and the exported result (P3b, spec
6
+ * docs/plans/specs/p3b-child-executor.md §4.3, §4.4).
7
+ *
8
+ * Pure; no IO. `resolveWorkflowRunOutputs` reads PERSISTED step rows (never
9
+ * live in-memory evidence — `completeWorkflowStep` sees only the current
10
+ * step's; a resumed run has nothing else to rebuild the scope from, B-N12)
11
+ * and fails loudly, by name, when a declared output's source artifact was
12
+ * replaced by a truncation envelope at persistence.
13
+ *
14
+ * This module must stay IMPORT-CYCLE-FREE with `./runs.ts` (which imports
15
+ * it): `WORKFLOW_EVIDENCE_TRUNCATED_MARKER`'s value is therefore reproduced
16
+ * locally rather than imported — the same "reproduce across a boundary
17
+ * rather than import" idiom `ir/schema-v4.ts` already uses for
18
+ * `CHILD_WORKFLOW_DECODE_MAX_DEPTH`.
19
+ */
20
+ import { validateJsonSchemaSubset } from "../../core/json-schema.js";
21
+ import { parseReference, resolveReferenceString } from "../program/expressions.js";
22
+ /** Mirrors `runs.ts`'s `WORKFLOW_EVIDENCE_TRUNCATED_MARKER` byte-for-byte — see this file's header for why it is reproduced, not imported. */
23
+ const EVIDENCE_TRUNCATED_MARKER = "__akm_evidence_truncated__";
24
+ function isTruncatedEvidenceValue(value) {
25
+ return (typeof value === "object" &&
26
+ value !== null &&
27
+ value[EVIDENCE_TRUNCATED_MARKER] === true);
28
+ }
29
+ /** Project a step artifact out of its persisted evidence — mirrors `exec/step-work.ts`'s `projectStepOutput`. */
30
+ function projectStepOutput(evidence) {
31
+ return Object.hasOwn(evidence, "output") ? evidence.output : evidence;
32
+ }
33
+ /**
34
+ * Resolve a plan's declared `outputs:` from a run's PERSISTED step rows, in
35
+ * declaration order (the frozen plan's `outputs` keys are already
36
+ * sorted-unique, `ir/schema-v4.ts`'s `decodeWorkflowOutputs`). Every failure
37
+ * mode is collected (not stopped-at-first) so a completion failure names
38
+ * every offending output at once.
39
+ */
40
+ export function resolveWorkflowRunOutputs(plan, steps) {
41
+ const stepOutputs = {};
42
+ for (const row of steps) {
43
+ if (!row.evidence_json)
44
+ continue;
45
+ let evidence;
46
+ try {
47
+ evidence = JSON.parse(row.evidence_json);
48
+ }
49
+ catch {
50
+ continue;
51
+ }
52
+ stepOutputs[row.step_id] = projectStepOutput(evidence);
53
+ }
54
+ const scope = { params: {}, stepOutputs };
55
+ const errors = [];
56
+ const outputs = {};
57
+ for (const [name, declaration] of Object.entries(plan.outputs ?? {})) {
58
+ const parsed = parseReference(declaration.from);
59
+ if (!parsed.ok || parsed.expr.kind !== "stepOutput") {
60
+ errors.push(`output "${name}": "${declaration.from}" is not a valid step-output reference.`);
61
+ continue;
62
+ }
63
+ const rootValue = Object.hasOwn(stepOutputs, parsed.expr.stepId) ? stepOutputs[parsed.expr.stepId] : undefined;
64
+ if (isTruncatedEvidenceValue(rootValue)) {
65
+ errors.push(`output "${name}" reads step "${parsed.expr.stepId}"'s artifact, which was truncated — it exceeded the ` +
66
+ `evidence persistence cap and was not stored.`);
67
+ continue;
68
+ }
69
+ const resolved = resolveReferenceString(declaration.from, scope);
70
+ if (!resolved.ok) {
71
+ errors.push(`output "${name}": ${resolved.error.message}`);
72
+ continue;
73
+ }
74
+ if (isTruncatedEvidenceValue(resolved.value)) {
75
+ errors.push(`output "${name}" reads step "${parsed.expr.stepId}"'s artifact, which was truncated — it exceeded the ` +
76
+ `evidence persistence cap and was not stored.`);
77
+ continue;
78
+ }
79
+ if (declaration.schema) {
80
+ const schemaErrors = validateJsonSchemaSubset(resolved.value, declaration.schema);
81
+ if (schemaErrors.length > 0) {
82
+ for (const schemaError of schemaErrors)
83
+ errors.push(`output "${name}": ${schemaError}`);
84
+ continue;
85
+ }
86
+ }
87
+ outputs[name] = resolved.value;
88
+ }
89
+ if (errors.length > 0)
90
+ return { ok: false, errors };
91
+ return { ok: true, outputs };
92
+ }
93
+ /**
94
+ * What a completed run EXPORTS: the resolved declared outputs, or
95
+ * `{runId, status}` metadata when the plan declared none. The `{runId,
96
+ * status}` form is synthesized on read and never stored (row B-25).
97
+ */
98
+ export function workflowRunExportedResult(row) {
99
+ if (row.outputs_json) {
100
+ return JSON.parse(row.outputs_json);
101
+ }
102
+ return { runId: row.id, status: row.status };
103
+ }
@@ -21,6 +21,7 @@ import { validateStepSummary } from "../validate-summary.js";
21
21
  import { resolveAgentIdentity } from "./agent-identity.js";
22
22
  import { evaluateCheckin } from "./checkin.js";
23
23
  import { assertWorkflowSpineMatchesPlan, classifyWorkflowRunPlan, frozenStepRows, requireExecutableWorkflowPlan, } from "./plan-classifier.js";
24
+ import { resolveWorkflowRunOutputs } from "./run-outputs.js";
24
25
  import { evaluateStaleUnits } from "./unit-checkin.js";
25
26
  import { canonicalizeWorkflowRefInput, loadWorkflowAsset, resolveWorkflowEntryId } from "./workflow-asset-loader.js";
26
27
  /**
@@ -176,7 +177,7 @@ export async function getWorkflowStatus(runId, opts) {
176
177
  return withWorkflowRunsRepo((repo) => {
177
178
  const run = readWorkflowRun(repo, runId);
178
179
  const steps = readWorkflowRunSteps(repo, run.id);
179
- const detail = buildWorkflowRunDetail(run, steps);
180
+ const detail = buildWorkflowRunDetail(repo, run, steps);
180
181
  if (opts?.includeUnits) {
181
182
  // The honest diagnostic surface (#22): read the unit journal straight and
182
183
  // project each row, INCLUDING failures whose diagnostic text the
@@ -197,9 +198,16 @@ export async function hasWorkflowRun(runId) {
197
198
  export async function listWorkflowRuns(input) {
198
199
  const scopeKey = getCurrentWorkflowScopeKey();
199
200
  const activeOnly = input?.activeOnly === true;
201
+ const includeChildren = input?.includeChildren === true;
200
202
  if (input?.workflowRef === undefined) {
201
203
  return withWorkflowRunsRepo((repo) => ({
202
- runs: repo.listRuns({ scopeKey, ...(activeOnly ? { activeOnly: true } : {}) }).map(toWorkflowRunSummary),
204
+ runs: repo
205
+ .listRuns({
206
+ scopeKey,
207
+ ...(activeOnly ? { activeOnly: true } : {}),
208
+ ...(includeChildren ? { includeChildren: true } : {}),
209
+ })
210
+ .map(toWorkflowRunSummary),
203
211
  }));
204
212
  }
205
213
  const exactRef = input.workflowRef.trim();
@@ -217,7 +225,7 @@ export async function listWorkflowRuns(input) {
217
225
  }
218
226
  catch (error) {
219
227
  if (parsedExactRef.bundle !== undefined) {
220
- const exactRows = await withWorkflowRunsRepo((repo) => repo.listRuns({ scopeKey, workflowRef: exactRef }));
228
+ const exactRows = await withWorkflowRunsRepo((repo) => repo.listRuns({ scopeKey, workflowRef: exactRef, ...(includeChildren ? { includeChildren: true } : {}) }));
221
229
  if (exactRows.length === 0)
222
230
  throw error;
223
231
  return {
@@ -229,7 +237,12 @@ export async function listWorkflowRuns(input) {
229
237
  }
230
238
  return withWorkflowRunsRepo((repo) => ({
231
239
  runs: repo
232
- .listRuns({ scopeKey, workflowRefs, ...(activeOnly ? { activeOnly: true } : {}) })
240
+ .listRuns({
241
+ scopeKey,
242
+ workflowRefs,
243
+ ...(activeOnly ? { activeOnly: true } : {}),
244
+ ...(includeChildren ? { includeChildren: true } : {}),
245
+ })
233
246
  .map(toWorkflowRunSummary),
234
247
  }));
235
248
  }
@@ -286,7 +299,7 @@ export async function resumeWorkflowRun(runId) {
286
299
  throw new UsageError(`Workflow run ${run.id} is already completed and cannot be resumed.`);
287
300
  }
288
301
  if (run.status === "active") {
289
- return buildWorkflowRunDetail(run, steps);
302
+ return buildWorkflowRunDetail(repo, run, steps);
290
303
  }
291
304
  // blocked or failed → flip back to active and re-open the current step so
292
305
  // it can be reclassified (completed, failed, skipped) after resuming.
@@ -299,7 +312,7 @@ export async function resumeWorkflowRun(runId) {
299
312
  });
300
313
  const updated = { ...run, status: "active", updated_at: now };
301
314
  const refreshedSteps = readWorkflowRunSteps(repo, run.id);
302
- return buildWorkflowRunDetail(updated, refreshedSteps);
315
+ return buildWorkflowRunDetail(repo, updated, refreshedSteps);
303
316
  });
304
317
  }
305
318
  /**
@@ -337,7 +350,7 @@ export async function abandonWorkflowRun(runId) {
337
350
  checkin_armed_at: now,
338
351
  };
339
352
  const steps = readWorkflowRunSteps(repo, run.id);
340
- const detail = buildWorkflowRunDetail(updated, steps);
353
+ const detail = buildWorkflowRunDetail(repo, updated, steps);
341
354
  return detail;
342
355
  });
343
356
  }
@@ -575,6 +588,23 @@ export async function completeWorkflowStep(input) {
575
588
  });
576
589
  refreshedSteps = readWorkflowRunSteps(repo, run.id);
577
590
  const state = deriveRunState(refreshedSteps);
591
+ // P3b (spec §4.3, B-N13): resolve + persist declared outputs INSIDE
592
+ // this same transaction, immediately after the run is known to have
593
+ // COMPLETED. A resolution failure throws here, and the transaction
594
+ // rolls back whole — the step completion included — so the observable
595
+ // outcome is fail-before-mutation: the step stays pending, the run
596
+ // stays active, and (since appendEvent runs outside this transaction)
597
+ // no event is appended.
598
+ let outputsJson; // undefined = untouched, keep the row's existing value
599
+ if (state.status === "completed" && plan.outputs) {
600
+ const resolved = resolveWorkflowRunOutputs(plan, refreshedSteps);
601
+ if (!resolved.ok) {
602
+ throw new UsageError(`Workflow run ${run.id} completed its final step but its declared outputs could not be resolved:\n` +
603
+ resolved.errors.map((e) => ` - ${e}`).join("\n"), "WORKFLOW_OUTPUT_INVALID");
604
+ }
605
+ outputsJson = JSON.stringify(resolved.outputs);
606
+ repo.setRunOutputs(run.id, outputsJson);
607
+ }
578
608
  // Re-arm the check-in on every state change: a healthy, progressing run
579
609
  // keeps pushing the stall window forward so the directive never fires.
580
610
  repo.updateRunState({
@@ -592,9 +622,10 @@ export async function completeWorkflowStep(input) {
592
622
  updated_at: completedAt,
593
623
  completed_at: state.completedAt,
594
624
  checkin_armed_at: completedAt,
625
+ ...(outputsJson !== undefined ? { outputs_json: outputsJson } : {}),
595
626
  };
596
627
  });
597
- const detail = buildWorkflowRunDetail(updatedRun, refreshedSteps);
628
+ const detail = buildWorkflowRunDetail(repo, updatedRun, refreshedSteps);
598
629
  // #11: emit `workflow_step_completed` ONLY for a genuine `completed`
599
630
  // transition; every other non-pending status (failed/skipped/blocked)
600
631
  // carries the honest `workflow_step_updated` name. The status is ALWAYS
@@ -693,7 +724,7 @@ function readWorkflowRun(repo, runId) {
693
724
  function readWorkflowRunSteps(repo, runId) {
694
725
  return repo.getStepsForRun(runId);
695
726
  }
696
- function buildWorkflowRunDetail(run, steps) {
727
+ function buildWorkflowRunDetail(repo, run, steps) {
697
728
  // Review M1: `workflow status` (and every other detail-shaped response) now
698
729
  // evaluates the check-in, not just `workflow run`. Pure timestamp check —
699
730
  // no background thread (see checkin.ts).
@@ -704,6 +735,7 @@ function buildWorkflowRunDetail(run, steps) {
704
735
  agentHarness: run.agent_harness,
705
736
  agentSessionId: run.agent_session_id,
706
737
  });
738
+ const children = childRunTree(repo, run.id, run.id);
707
739
  return {
708
740
  run: toWorkflowRunSummary(run),
709
741
  workflow: {
@@ -712,6 +744,73 @@ function buildWorkflowRunDetail(run, steps) {
712
744
  steps: steps.map(toWorkflowRunStepState),
713
745
  },
714
746
  ...(checkin ? { checkin } : {}),
747
+ ...(children ? { children } : {}),
748
+ };
749
+ }
750
+ /**
751
+ * Build the parent-child status tree rooted at `rootRunId`, recursively, for
752
+ * whatever run's children are being listed (`forRunId`) — P3b, spec §4.5.
753
+ * `rootRunId` is threaded unchanged through the recursion, so every blocked
754
+ * node's `resume.then` names the SAME top-of-query run regardless of nesting
755
+ * depth: `akm workflow resume <rootRunId> && akm workflow run <rootRunId>` —
756
+ * never each node's own immediate parent, which the tree's caller has no
757
+ * command for.
758
+ *
759
+ * That command is sufficient to clear a block exactly ONE level deep (the
760
+ * root's own composing step blocked directly on this node) but NOT deeper
761
+ * (code-review round 4, finding 6 / Review log R6 — corrects a false claim
762
+ * this comment used to make here). Re-driving the root does **not** cascade
763
+ * back down through every intermediate composing step: `driveChildWorkflowUnit`
764
+ * (child-workflow.ts) never re-drives a child whose OWN status is `blocked`
765
+ * (row A-22) — no lease is even taken — so a re-drive just RE-OBSERVES the
766
+ * still-blocked status and re-propagates the block upward (an intermediate
767
+ * run is always blocked when a descendant is, row A-21, applied
768
+ * recursively), never reaching the deepest blocked node. Clearing a
769
+ * depth-2-or-deeper block requires resuming EVERY blocked run in the chain,
770
+ * deepest first, then re-running only the root — see "Recovering a blocked
771
+ * child" in docs/guides/run-workflows.md and "Blocked-child recovery" in
772
+ * docs/reference/workflow-schema.md for the worked multi-level sequence.
773
+ * `resume.then` is deliberately not widened to enumerate that chain (no
774
+ * envelope change, no new field) — the docs carry the multi-level sequence
775
+ * instead.
776
+ *
777
+ * Absent, never `[]`, when `forRunId` has no children (P3a's `childRunsOf`
778
+ * order: `created_at, id`).
779
+ */
780
+ function childRunTree(repo, rootRunId, forRunId) {
781
+ const rows = repo.childRunsOf(forRunId);
782
+ if (rows.length === 0)
783
+ return undefined;
784
+ return rows.map((row) => toChildRunNode(repo, rootRunId, row));
785
+ }
786
+ function toChildRunNode(repo, rootRunId, row) {
787
+ const spawnedByUnitId = row.parent_unit_id ?? "";
788
+ // B-36: the parent STEP that spawned it, resolved via the real journaled
789
+ // unit row — null when that unit row is gone.
790
+ const stepId = row.parent_run_id && row.parent_unit_id
791
+ ? (repo.getUnit(row.parent_run_id, row.parent_unit_id)?.step_id ?? null)
792
+ : null;
793
+ const children = childRunTree(repo, rootRunId, row.id);
794
+ return {
795
+ runId: row.id,
796
+ workflowRef: row.workflow_ref,
797
+ workflowTitle: row.workflow_title,
798
+ status: row.status,
799
+ spawnedByUnitId,
800
+ stepId,
801
+ currentStepId: row.current_step_id,
802
+ createdAt: row.created_at,
803
+ updatedAt: row.updated_at,
804
+ ...(row.status === "blocked"
805
+ ? {
806
+ resume: {
807
+ command: `akm workflow resume ${row.id}`,
808
+ // biome-ignore lint/suspicious/noThenProperty: mirrors spec §4.5's real WorkflowChildRunNode.resume.then field name
809
+ then: `akm workflow resume ${rootRunId} && akm workflow run ${rootRunId}`,
810
+ },
811
+ }
812
+ : {}),
813
+ ...(children ? { children } : {}),
715
814
  };
716
815
  }
717
816
  function toWorkflowRunSummary(run) {
@@ -738,6 +837,12 @@ function toWorkflowRunSummary(run) {
738
837
  ...(run.engine_lease_holder && run.engine_lease_until
739
838
  ? { engineLease: { holder: run.engine_lease_holder, until: run.engine_lease_until } }
740
839
  : {}),
840
+ // P3b (spec §4.5): all three optional and conditionally spread, so every
841
+ // pre-existing (non-child, no-outputs-declared) run's envelope is
842
+ // byte-identical (Stable tier, rows B-27, B-45).
843
+ ...(run.outputs_json ? { outputs: parseJsonObject(run.outputs_json) ?? {} } : {}),
844
+ ...(run.parent_run_id ? { parentRunId: run.parent_run_id } : {}),
845
+ ...(run.parent_unit_id ? { spawnedByUnitId: run.parent_unit_id } : {}),
741
846
  };
742
847
  }
743
848
  /**
@@ -5,7 +5,7 @@ import fs from "node:fs";
5
5
  import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
6
6
  import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
7
7
  import { loadConfig } from "../../core/config/config.js";
8
- import { NotFoundError, UsageError } from "../../core/errors.js";
8
+ import { COMPOSITION_INVALID_MULTI_JOB_HINT, NotFoundError, UsageError } from "../../core/errors.js";
9
9
  import { getDbPath } from "../../core/paths.js";
10
10
  import { canonicalizeWorkflowName } from "../../core/recognition-util.js";
11
11
  import { deriveInstallations } from "../../indexer/installations.js";
@@ -24,10 +24,10 @@ export function canonicalWorkflowRunRef(bundle, canonicalName) {
24
24
  export function parseWorkflowRefInput(ref) {
25
25
  const parsed = parseBundleRef(ref.trim());
26
26
  if (parsed.fragment !== undefined) {
27
- throw new UsageError(`Export fragment "#${parsed.fragment}" is not accepted in a workflow ref.`, "INVALID_FLAG_VALUE");
27
+ throw new UsageError(`Export fragment "#${parsed.fragment}" is not accepted in a workflow ref.`, "TARGET_REF_INVALID");
28
28
  }
29
29
  if (parsed.conceptId.startsWith("workflow:")) {
30
- throw new UsageError(`Invalid workflow ref "${ref.trim()}". Use [bundle//]conceptId, such as workflows/release.`, "INVALID_FLAG_VALUE");
30
+ throw new UsageError(`Invalid workflow ref "${ref.trim()}". Use [bundle//]conceptId, such as workflows/release.`, "TARGET_REF_INVALID");
31
31
  }
32
32
  return parsed;
33
33
  }
@@ -50,7 +50,7 @@ export async function loadWorkflowAsset(ref) {
50
50
  return [{ source, bundleId }];
51
51
  });
52
52
  if (bundleRef.bundle && searchSources.length === 0) {
53
- throw new UsageError(`Bundle "${bundleRef.bundle}" was not found among configured sources.`, "INVALID_FLAG_VALUE");
53
+ throw new UsageError(`Bundle "${bundleRef.bundle}" was not found among configured sources.`, "WORKFLOW_SOURCE_INVALID");
54
54
  }
55
55
  let assetPath;
56
56
  let sourcePath;
@@ -80,7 +80,7 @@ export async function loadWorkflowAsset(ref) {
80
80
  if (rejectedSource) {
81
81
  const sourceName = rejectedSource.bundleId;
82
82
  const adapterId = rejectedSource.source.adapterId ?? "unassigned";
83
- throw new UsageError(`Bundle "${sourceName}" uses adapter "${adapterId}", which does not support native workflow execution.`, "INVALID_FLAG_VALUE");
83
+ throw new UsageError(`Bundle "${sourceName}" uses adapter "${adapterId}", which does not support native workflow execution.`, "WORKFLOW_SOURCE_INVALID");
84
84
  }
85
85
  throw new NotFoundError(`Workflow not found for ref: ${ref}`);
86
86
  }
@@ -124,8 +124,16 @@ function compileWorkflowSourceFromDisk(assetPath, workspaceRoot) {
124
124
  const content = fs.readFileSync(assetPath, "utf8");
125
125
  const result = compileWorkflowSource(content, { path: assetPath, workspaceRoot });
126
126
  if (!result.ok) {
127
+ // P4-N2's mapping (docs/plans/specs/p4-deletions-closeout.md §3.3.4): this
128
+ // is the FIRST compile of a workflow's own source that a ref resolves
129
+ // through (loadWorkflowAsset runs before compileResolveFreezeWorkflowV4),
130
+ // so it must apply the same code split as the freeze wrapper — otherwise
131
+ // row B-44's COMPOSITION_INVALID promise never reaches `startWorkflowRun`
132
+ // callers, which never get past this point on a multi-job source.
127
133
  const details = result.errors.map((error) => ` ${error.path}:${error.line} — ${error.message}`).join("\n");
128
- throw new UsageError(`Workflow source has ${result.errors.length} error(s):\n${details}`);
134
+ const isMultiJob = result.errors.length === 1 && result.errors[0]?.code === "multi-job-unsupported";
135
+ const code = isMultiJob ? "COMPOSITION_INVALID" : "WORKFLOW_SOURCE_INVALID";
136
+ throw new UsageError(`Workflow source has ${result.errors.length} error(s):\n${details}`, code, isMultiJob ? COMPOSITION_INVALID_MULTI_JOB_HINT : undefined);
129
137
  }
130
138
  return result.ir;
131
139
  }
@@ -40,7 +40,7 @@ export class WorkflowSourceDomainError extends WorkflowSourceRejectionError {
40
40
  const sortedCollisions = [...collidingSourcePaths].sort(comparePaths);
41
41
  const code = issues.some((issue) => issue.code === "PATH_ESCAPE_VIOLATION")
42
42
  ? "PATH_ESCAPE_VIOLATION"
43
- : "INVALID_FLAG_VALUE";
43
+ : "WORKFLOW_SOURCE_INVALID";
44
44
  const collisionDetail = sortedCollisions.length > 1 ? ` Valid owners also collide: ${sortedCollisions.join(", ")}.` : "";
45
45
  super(`Workflow "${canonicalName}" has an invalid source ownership domain across candidates: ${sortedPaths.join(", ")}. ` +
46
46
  `Problems: ${issues.map((issue) => issue.message).join(" ")}${collisionDetail} ` +
@@ -53,7 +53,7 @@ export class WorkflowSourceDomainError extends WorkflowSourceRejectionError {
53
53
  export class WorkflowSourceIdentityError extends UsageError {
54
54
  constructor(ref, indexedPath, authoritativePath) {
55
55
  super(`Indexed workflow source identity for "${ref}" points to ${indexedPath}, but the authoritative source is ${authoritativePath}. ` +
56
- "Refusing the stale index/cache identity; run `akm index --full` to reconcile it.", "INVALID_FLAG_VALUE");
56
+ "Refusing the stale index/cache identity; run `akm index --full` to reconcile it.", "WORKFLOW_SOURCE_INVALID");
57
57
  this.name = "WorkflowSourceIdentityError";
58
58
  Object.setPrototypeOf(this, new.target.prototype);
59
59
  }
@@ -61,7 +61,7 @@ export class WorkflowSourceIdentityError extends UsageError {
61
61
  export class WorkflowSourceNameError extends WorkflowSourceRejectionError {
62
62
  constructor(sourcePath, nestedSuffix) {
63
63
  super(`Workflow source filename ${sourcePath} has an extensionless stem ending in recognized workflow suffix "${nestedSuffix}". ` +
64
- "Nested workflow suffixes are invalid; remove the inner .md or .yml suffix instead of relying on repeated stripping.", "INVALID_FLAG_VALUE", [sourcePath]);
64
+ "Nested workflow suffixes are invalid; remove the inner .md or .yml suffix instead of relying on repeated stripping.", "WORKFLOW_SOURCE_INVALID", [sourcePath]);
65
65
  this.name = "WorkflowSourceNameError";
66
66
  Object.setPrototypeOf(this, new.target.prototype);
67
67
  }
@@ -69,14 +69,14 @@ export class WorkflowSourceNameError extends WorkflowSourceRejectionError {
69
69
  export class WorkflowSourceLinkIdentityError extends WorkflowSourceRejectionError {
70
70
  constructor(sourcePath, targetPath) {
71
71
  super(`Workflow source ${sourcePath} resolves through a symlink to ${targetPath} with a different source format. ` +
72
- "The authored workflow path and resolved source must use the same .md or .yml format.", "INVALID_FLAG_VALUE", [sourcePath]);
72
+ "The authored workflow path and resolved source must use the same .md or .yml format.", "WORKFLOW_SOURCE_INVALID", [sourcePath]);
73
73
  this.name = "WorkflowSourceLinkIdentityError";
74
74
  Object.setPrototypeOf(this, new.target.prototype);
75
75
  }
76
76
  }
77
77
  export class WorkflowSourceLinkResolutionError extends WorkflowSourceRejectionError {
78
78
  constructor(sourcePath) {
79
- super(`Workflow source symlink ${sourcePath} cannot be resolved to a regular file.`, "INVALID_FLAG_VALUE", [
79
+ super(`Workflow source symlink ${sourcePath} cannot be resolved to a regular file.`, "WORKFLOW_SOURCE_INVALID", [
80
80
  sourcePath,
81
81
  ]);
82
82
  this.name = "WorkflowSourceLinkResolutionError";
@@ -0,0 +1,17 @@
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
+ * Locale-independent code-point ordering, shared by the workflow source-IR
6
+ * lane (job `needs` canonicalization) and `akm lint` (name sorting).
7
+ *
8
+ * Split out of the deleted `source-ir/ordering.ts` (P4 §3.3, docs/plans/specs/
9
+ * p4-deletions-closeout.md): that file's other export,
10
+ * `canonicalTopologicalJobs`, existed only to order MULTIPLE ready jobs —
11
+ * moot once the adapter confines a workflow source to exactly one job. This
12
+ * comparator has an unrelated consumer (`src/commands/lint/index.ts`) and
13
+ * survives on its own.
14
+ */
15
+ export function compareWorkflowSourceCodePoints(left, right) {
16
+ return left < right ? -1 : left > right ? 1 : 0;
17
+ }
@@ -3,13 +3,14 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import path from "node:path";
5
5
  import { parseFrontmatterBlock } from "../../core/asset/frontmatter.js";
6
- import { classifyTaskV3Triggers, classifyTaskV3Uses } from "../../tasks/source-v3.js";
7
6
  import { parseWorkflow } from "../parser.js";
8
7
  import { projectExecCore } from "../program/schema.js";
9
8
  import { parseGithubWorkflowSource } from "./github-yaml.js";
10
9
  import { sourceFailureResult, WorkflowSourceFailure } from "./result.js";
11
10
  import { decodeWorkflowSourceIrV1, } from "./schema.js";
12
11
  import { canonicalizeWorkflowWorkingDirectory, WorkflowSourceSemanticError } from "./semantics.js";
12
+ import { classifyWorkflowYamlTriggers } from "./triggers.js";
13
+ import { classifyWorkflowSourceUses } from "./uses.js";
13
14
  export { looksLikeGithubWorkflowSource } from "./github-yaml.js";
14
15
  export function compileGithubWorkflowSource(source, options) {
15
16
  try {
@@ -17,8 +18,8 @@ export function compileGithubWorkflowSource(source, options) {
17
18
  ok: true,
18
19
  ir: decodeWorkflowSourceIrV1(parseGithubWorkflowSource(source, {
19
20
  ...options,
20
- classifyUses: options.classifyUses ?? classifyTaskV3Uses,
21
- classifyTriggers: options.classifyTriggers ?? classifyTaskV3Triggers,
21
+ classifyUses: options.classifyUses ?? classifyWorkflowSourceUses,
22
+ classifyTriggers: options.classifyTriggers ?? classifyWorkflowYamlTriggers,
22
23
  }), {
23
24
  workspaceRoot: options.workspaceRoot,
24
25
  }),
@@ -67,6 +68,9 @@ export function compileMarkdownWorkflowSource(source, options) {
67
68
  ...(parsed.document.description ? { description: parsed.document.description } : {}),
68
69
  ...(parsed.document.tags ? { tags: [...parsed.document.tags] } : {}),
69
70
  ...(parsed.document.params ? { params: jsonClone(parsed.document.params) } : {}),
71
+ ...(parsed.document.outputs
72
+ ? { outputs: jsonClone(parsed.document.outputs) }
73
+ : {}),
70
74
  ...(parsed.document.defaults
71
75
  ? { defaults: jsonClone(parsed.document.defaults) }
72
76
  : {}),
@@ -3,7 +3,6 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { isAlias, isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
5
5
  import { utf8Bytes, WORKFLOW_MAX_SOURCE_BYTES } from "../resource-limits.js";
6
- import { canonicalTopologicalJobs } from "./ordering.js";
7
6
  import { WorkflowSourceFailure } from "./result.js";
8
7
  import { WORKFLOW_SOURCE_HOST_SHELLS, } from "./schema.js";
9
8
  import { canonicalizeWorkflowCron, canonicalizeWorkflowRun, canonicalizeWorkflowWorkingDirectory, classifyWorkflowStepUses, validateWorkflowBuiltinCommand, WorkflowSourceSemanticError, } from "./semantics.js";
@@ -387,7 +386,7 @@ function verifyOwnerTriggerPlan(reader, root, onNode, triggers, options) {
387
386
  binding.source === `on.schedule[${expected.ordinal}].cron`);
388
387
  });
389
388
  if (!matches) {
390
- throw new WorkflowSourceFailure("trigger-classifier-drift", "The workflow trigger parser disagrees with the canonical task-v3 trigger classifier.", reader.span(onNode));
389
+ throw new WorkflowSourceFailure("trigger-classifier-drift", "The workflow trigger parser disagrees with the canonical workflow YAML trigger classifier.", reader.span(onNode));
391
390
  }
392
391
  }
393
392
  function validateCron(reader, cron, node) {
@@ -398,19 +397,34 @@ function validateCron(reader, cron, node) {
398
397
  semanticReaderFail(reader, cause, node);
399
398
  }
400
399
  }
400
+ /**
401
+ * The ONE place a job-count or job-dependency policy is enforced (P4 §3.3,
402
+ * docs/plans/specs/p4-deletions-closeout.md): AKM's YAML adapter accepts a
403
+ * familiar GitHub-step-shaped `name:`/`on:`/`jobs:` document, but requires
404
+ * exactly one job (brief §10) — it is an AKM workflow format executed by
405
+ * AKM's native engine, not a GitHub Actions graph. Job ordering, dependency
406
+ * validation and the 256-job bound all existed only to support MULTIPLE
407
+ * jobs; they are gone with the machinery, not relocated.
408
+ */
401
409
  function parseJobs(reader, node, options) {
402
410
  const fields = reader.arbitraryFields(node, "workflow.jobs");
403
- if (fields.size === 0 || fields.size > 256) {
404
- reader.fail("job-count-limit", "workflow.jobs must contain 1 through 256 jobs.", node);
405
- }
406
- const jobs = [...fields.entries()].map(([id, pair]) => parseJob(reader, id, pair, options));
407
- const ordered = canonicalTopologicalJobs(jobs);
408
- if (ordered.ok)
409
- return ordered.jobs;
410
- if (ordered.kind === "missing") {
411
- throw new WorkflowSourceFailure("missing-job-dependency", `Job ${ordered.job.id} needs missing job ${ordered.dependency}.`, ordered.job.source);
412
- }
413
- throw new WorkflowSourceFailure("job-dependency-cycle", "Workflow jobs contain a dependency cycle.", ordered.job.source);
411
+ let first;
412
+ let second;
413
+ for (const entry of fields) {
414
+ if (!first)
415
+ first = entry;
416
+ else if (!second)
417
+ second = entry;
418
+ }
419
+ if (fields.size !== 1 || !first) {
420
+ reader.fail("multi-job-unsupported", `AKM workflow YAML requires exactly one job; this document declares ${fields.size}. AKM's YAML is an AKM workflow format executed by AKM's native engine, not GitHub Actions — split the jobs into separate workflows.`, second ? second[1].key : node);
421
+ }
422
+ const [id, pair] = first;
423
+ const job = parseJob(reader, id, pair, options);
424
+ if (job.needs.length > 0) {
425
+ reader.fail("multi-job-unsupported", `Job ${job.id} declares needs, but an AKM workflow has exactly one job; remove needs.`, pair.key);
426
+ }
427
+ return [job];
414
428
  }
415
429
  function parseJob(reader, id, pair, options) {
416
430
  const node = pair.value;
@@ -450,9 +464,10 @@ function parseNeeds(reader, pair, jobId) {
450
464
  for (const need of values)
451
465
  if (!SOURCE_ID.test(need))
452
466
  reader.fail("invalid-job-id", `Invalid needs id ${need}.`, pair.value);
453
- if (new Set(values).size !== values.length) {
454
- reader.fail("duplicate-job-dependency", `Job ${jobId} has duplicate needs entries.`, pair.value);
455
- }
467
+ // Duplicate-entry checking (code duplicate-job-dependency) deleted with the
468
+ // rest of the multi-job dependency machinery (P4 §3.3): ANY non-empty
469
+ // needs — duplicated or not — is multi-job-unsupported at the caller
470
+ // (parseJobs), since a single-job workflow has nothing to depend on.
456
471
  return values.sort();
457
472
  }
458
473
  function parseStep(reader, node, jobId, index, stepIds, options) {
@@ -487,7 +502,18 @@ function parseUsesStep(reader, usesPair, fields, options, common) {
487
502
  }
488
503
  const uses = reader.string(usesPair.value, "step.uses");
489
504
  const target = classifyUses(reader, uses, usesPair.value, options.classifyUses ?? classifyWorkflowSourceUses);
490
- const withValues = parseScalarMap(reader, fields.get("with"), "step.with", INPUT_KEY, true);
505
+ // A-N3 (P2b, docs/plans/specs/p2b-input-bindings.md §1.7), widened in P3a
506
+ // (docs/plans/specs/p3a-plan-v5-child-freeze.md §4.2 step 7, row B-10) to
507
+ // ALSO cover a workflows/<ref> target: a tasks/<ref> or workflows/<ref>
508
+ // step's with: may bind any JSON value the composed target's declared
509
+ // input/param needs (an object/array literal, or a {from: "..."}
510
+ // reference) — decoding it through the scalar-only parseScalarMap would
511
+ // reject the very shapes both A-N3 and A-N8 exist to accept before
512
+ // decodeWorkflowSourceIrV1 (schema.ts) is ever reached. Every other target
513
+ // keeps the byte-identical scalar-only grammar.
514
+ const withValues = target.kind === "task" || target.kind === "workflow"
515
+ ? parsePlainMap(reader, fields.get("with"), "step.with", INPUT_KEY)
516
+ : parseScalarMap(reader, fields.get("with"), "step.with", INPUT_KEY, true);
491
517
  const commandMode = target.kind === "builtin-command"
492
518
  ? validateBuiltinCommand(reader, withValues, fields.get("with")?.value ?? usesPair.value)
493
519
  : undefined;
@@ -560,6 +586,27 @@ function parseScalarMap(reader, pair, context, keyPattern, allowNull) {
560
586
  }
561
587
  return out;
562
588
  }
589
+ /**
590
+ * Like {@link parseScalarMap} but accepts an arbitrary JSON value per key —
591
+ * a task-composition with: binding may be the declared input's own shape (an
592
+ * object/array literal, or a `{from: "..."}` reference), not just a scalar
593
+ * (A-N3). Depth/node bounds are already enforced document-wide by
594
+ * `checkTree`/`rejectAliases` before any field-level parsing runs, so this
595
+ * adds no new bound. `decodeWorkflowSourceIrV1` (schema.ts) decides what a
596
+ * declared input actually accepts.
597
+ */
598
+ function parsePlainMap(reader, pair, context, keyPattern) {
599
+ if (!pair)
600
+ return undefined;
601
+ const fields = reader.arbitraryFields(pair.value, context);
602
+ const out = {};
603
+ for (const [key, valuePair] of fields) {
604
+ if (!keyPattern.test(key))
605
+ reader.fail("invalid-mapping-key", `${context} has invalid key ${JSON.stringify(key)}.`, valuePair.key);
606
+ out[key] = reader.plain(valuePair.value, `${context}.${key}`);
607
+ }
608
+ return out;
609
+ }
563
610
  function wholeSourceSpan(source, filePath) {
564
611
  return { path: filePath, start: 1, end: Math.max(1, source.split(/\r?\n/).length) };
565
612
  }