akm-cli 0.9.2-alpha.4 → 0.9.2-alpha.5

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 +463 -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 +233 -45
  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 +55 -12
  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 +19 -5
  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 +2 -2
  127. package/docs/migration/v0.8-to-v0.9.md +9 -7
  128. package/docs/migration/v0.9.0-troubleshooting.md +14 -7
  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 +3 -3
  133. package/docs/reference/supported-formats.md +9 -5
  134. package/docs/reference/tasks.md +338 -75
  135. package/docs/reference/workflow-schema.md +281 -8
  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
@@ -26,14 +26,19 @@ import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetFor
26
26
  import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
27
27
  import { resolveAssetPath } from "../../sources/resolve.js";
28
28
  import { backendNameForPlatform, selectBackend } from "../../tasks/backends/index.js";
29
+ import { prepareTaskV3Execution } from "../../tasks/prepare/prepare.js";
29
30
  import { resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
30
- import { exitCodeForStatus, readTaskHistory, runTask } from "../../tasks/runner.js";
31
- import { prepareTaskV3Execution } from "../../tasks/runtime-v3.js";
31
+ import { createExecutionProvenanceContext } from "../../tasks/run/provenance.js";
32
+ import { runTask } from "../../tasks/run/run-task.js";
33
+ import { readTaskHistory } from "../../tasks/run/task-history.js";
34
+ import { exitCodeForStatus } from "../../tasks/run/task-result.js";
32
35
  import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT } from "../../tasks/schedule.js";
33
36
  import { assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeBindingId, } from "../../tasks/scheduler-binding.js";
34
37
  import { schedulerContextDescriptor, schedulerContextPath, validateSchedulerContextDescriptor, writeSchedulerContextDescriptor, } from "../../tasks/scheduler-invocation.js";
35
38
  import { assertSchedulerNativeArtifactOwnership, assertSchedulerSourceSnapshot, finalizeSchedulerSyncPlan, prepareSchedulerSyncSourceSet, } from "../../tasks/scheduler-sync.js";
36
- import { parseTaskV3Yaml, TASK_V3_MAX_SOURCE_BYTES } from "../../tasks/source-v3.js";
39
+ import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
40
+ import { projectTaskSourceV4 } from "../../tasks/source/project-v4.js";
41
+ import { TASK_V3_MAX_SOURCE_BYTES } from "../../tasks/source-v3.js";
37
42
  import { normaliseTaskConceptId, normaliseTaskId } from "../../tasks/task-id.js";
38
43
  import { applyAutonomyGate, configuredDirectAutonomyLanes, describeGatedLanes } from "../improve/autonomy-gate.js";
39
44
  import { resolveImproveStrategy } from "../improve/improve-strategies.js";
@@ -77,7 +82,8 @@ export async function akmTasksAdd(input, deps = {}) {
77
82
  tags: input.tags,
78
83
  enabled: input.disabled !== true,
79
84
  });
80
- const task = parseTaskV3Yaml({ yaml, filePath: assetPath, workspaceRoot: stashDir });
85
+ const parsedTask = parseTaskSource({ yaml, filePath: assetPath, workspaceRoot: stashDir });
86
+ const task = projectTaskSourceV4(parsedTask.v4);
81
87
  const qualifiedRef = makeBundleRef(bundle.bundleName, `tasks/${id}`);
82
88
  await prepareTaskV3Execution(task, {
83
89
  taskId: id,
@@ -87,12 +93,27 @@ export async function akmTasksAdd(input, deps = {}) {
87
93
  config: bundle.config,
88
94
  resolveAsset: taskProjectionAssetResolver(bundle.config, bundle.bundleName, stashDir),
89
95
  });
96
+ // Bindings are compiled from the ORIGINAL parsed document, not the
97
+ // `task`/`projectTaskSourceV4` projection above — mirroring
98
+ // scheduler-sync.ts's compileTaskSources (spec §3.2.7's project-v4.ts
99
+ // header): the projection deliberately drops each schedule entry's own
100
+ // `enabled` (P4-N6), so building bindings from it would silently ignore
101
+ // `--disabled`. A task source v4 document has no document-level
102
+ // `akm.enabled`, so `enabled: true` is passed at the document level and
103
+ // every entry's own `enabled` (always present, defaulted at parse time)
104
+ // decides.
90
105
  const taskBindings = compileTaskSchedulerBindings({
91
106
  id,
92
107
  qualifiedRef,
93
108
  ...(bundle.installTarget ? { bundleTarget: bundle.installTarget } : {}),
94
- enabled: task.akm?.enabled !== false,
95
- schedules: task.triggers.schedules,
109
+ enabled: true,
110
+ schedules: parsedTask.v4.schedule.map((schedule) => ({
111
+ cron: schedule.cron,
112
+ ordinal: schedule.ordinal,
113
+ enabled: schedule.enabled,
114
+ source: schedule.source,
115
+ inputs: schedule.inputs,
116
+ })),
96
117
  });
97
118
  const taskBinding = taskBindings[0];
98
119
  if (!taskBinding)
@@ -227,20 +248,52 @@ export async function akmTasksRun(id, options = {}) {
227
248
  const bundle = resolveTaskReadBundle(parsed.bundle, options.target);
228
249
  const adapterId = bundle.source.adapterId ?? detectAdapterId(bundle.source.path);
229
250
  const resolvedId = taskIdForAdapter(parsed.id, adapterId);
251
+ const scheduled = options.scheduled === true;
252
+ // D5 "Construction" (spec docs/plans/specs/p1b-model-extraction.md §1.2/
253
+ // §5.2): built ONCE at this invocation boundary. eventSource is "task"
254
+ // whether or not --scheduled was passed (§1.6 D5-N1) — scheduled stays a
255
+ // separate field carrying its own pre-existing meaning (activation policy,
256
+ // scheduler env), never selecting the event source.
257
+ const provenance = createExecutionProvenanceContext(scheduled);
258
+ // F-3 (spec §5.4): RunTaskOptions.stashDir renamed to bundleDir — VALUE-
259
+ // preserving, no CLI flag change.
260
+ //
261
+ // P2a Lane C (spec docs/plans/specs/p2a-task-source-v4.md §5.1): the raw,
262
+ // exact-name input flags `tasks-cli.ts`'s Stage 1 captures ride through
263
+ // unchanged to `runTask` -> `loadPreparedTask`'s Stage 2 materializer,
264
+ // which owns declaring `inputFlags` on `RunTaskOptions` and attaching the
265
+ // materialized literals to the constructed `TaskInvocation`. This is only
266
+ // the pass-through surface: a valid flag set stays byte-identical to the
267
+ // same run without flags (§0), and P2a delivers nothing to the target.
268
+ //
269
+ // No `as` cast (test-review finding, spec §6 F-5): `RunTaskOptions` (this
270
+ // literal's inferred type is checked directly against it, below) declares
271
+ // every one of these fields, so the compiler — not a suppressed excess-
272
+ // property check — enforces this seam. A future rename or removal on
273
+ // either side now fails `tsc`, not silently at the `runTask` boundary.
230
274
  const runOptions = {
231
- stashDir: bundle.source.path,
275
+ bundleDir: bundle.source.path,
232
276
  bundleName: bundle.source.name,
233
277
  adapterId,
234
- scheduled: options.scheduled === true,
278
+ scheduled,
279
+ provenance,
280
+ inputFlags: options.inputFlags,
235
281
  };
236
282
  // The runner owns the prepare-before-reserve boundary. Invalid source,
237
283
  // projectability, and resolver failures therefore create no history row.
238
284
  const result = await runTask(resolvedId, runOptions);
239
- const exitCode = result.status === "failed" && result.target.kind === "command" && result.detail?.exitCode === 78
285
+ // C-7 (spec §5.6): after D8's result-vocabulary re-code, "command" means
286
+ // the agent/LLM arm — the native shell/script arm now reports "shell" /
287
+ // "script". Rewired in the SAME commit as the vocabulary re-code so a
288
+ // shell/script task's process exit 78 still passes through as CLI exit 78
289
+ // (documented behavior, src/assets/hints/cli-hints-short.md:95).
290
+ const exitCode = result.status === "failed" &&
291
+ (result.target.kind === "shell" || result.target.kind === "script") &&
292
+ result.detail?.exitCode === 78
240
293
  ? 78
241
294
  : exitCodeForStatus(result.status);
242
295
  return {
243
- ok: result.status === "completed" || result.status === "disabled",
296
+ ok: result.status === "completed",
244
297
  result,
245
298
  exitCode,
246
299
  };
@@ -905,7 +958,14 @@ function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
905
958
  };
906
959
  };
907
960
  }
908
- function resolveTaskReadBundle(refBundle, flagBundle) {
961
+ /**
962
+ * Exported for `src/commands/tasks/explain.ts` (P2b Lane B, spec
963
+ * docs/plans/specs/p2b-input-bindings.md §4.5, B-N4): `akm task explain`
964
+ * resolves its `--bundle` axis identically to every other read-only task
965
+ * verb here (`akm task history`, `akm task run`) — the SAME resolver, not a
966
+ * second one.
967
+ */
968
+ export function resolveTaskReadBundle(refBundle, flagBundle) {
909
969
  if (refBundle && flagBundle && refBundle !== flagBundle) {
910
970
  throw new UsageError(`Task ref selects bundle ${JSON.stringify(refBundle)}, but --bundle selects ${JSON.stringify(flagBundle)}.`, "INVALID_FLAG_VALUE");
911
971
  }
@@ -983,12 +1043,43 @@ function assertNoForeignSchedule(entries, id, installTarget) {
983
1043
  if (foreign)
984
1044
  throw new UsageError(foreignScheduleMessage(id, foreign.target), "RESOURCE_ALREADY_EXISTS");
985
1045
  }
1046
+ /**
1047
+ * Infer a JSON-Schema-subset `type` keyword from one `--params` value's
1048
+ * runtime shape (spec docs/plans/specs/p4-deletions-closeout.md §3.2.6, row
1049
+ * B-20). `null` is not one of the five runtime types the spec enumerates
1050
+ * (string/number/boolean/object/array) but is a value JSON.parse can still
1051
+ * produce for a param; `src/core/json-schema.ts`'s subset validator accepts
1052
+ * `"null"` as a `type`, so it is handled rather than mis-typed.
1053
+ */
1054
+ function jsonSchemaTypeOf(value) {
1055
+ if (value === null)
1056
+ return "null";
1057
+ if (Array.isArray(value))
1058
+ return "array";
1059
+ return typeof value;
1060
+ }
1061
+ /**
1062
+ * Render `--params` as typed `inputs:` declarations, one per key, each
1063
+ * carrying a `default:` equal to the authored value and a `type:` inferred
1064
+ * from its own JSON runtime shape (row B-20). Never emitted as `with:` —
1065
+ * task source v4 accepts `with:` only on `uses: akm/command` (§3.2.6's
1066
+ * `parseTarget`/`checkTopLevelKeys`), and a workflow target's declared
1067
+ * inputs are what `load-task.ts`'s existing v4 delivery override binds into
1068
+ * the child run's params.
1069
+ */
1070
+ function renderInputsFromParams(params) {
1071
+ const inputs = {};
1072
+ for (const [name, value] of Object.entries(params)) {
1073
+ inputs[name] = { type: jsonSchemaTypeOf(value), default: value };
1074
+ }
1075
+ return inputs;
1076
+ }
986
1077
  function renderTaskYaml(input) {
987
- const obj = { version: 3 };
1078
+ const obj = { version: 4 };
988
1079
  if (input.workflow) {
989
1080
  obj.uses = input.workflow;
990
1081
  if (input.params)
991
- obj.with = parseJsonObjectArg(input.params);
1082
+ obj.inputs = renderInputsFromParams(parseJsonObjectArg(input.params));
992
1083
  }
993
1084
  else if (input.prompt) {
994
1085
  obj.uses = "akm/command";
@@ -996,22 +1087,40 @@ function renderTaskYaml(input) {
996
1087
  }
997
1088
  else if (input.command !== undefined) {
998
1089
  if (Array.isArray(input.command)) {
999
- throw new UsageError("Task v3 --command accepts one shell string; argv arrays require manual migration.", "INVALID_FLAG_VALUE");
1090
+ throw new UsageError("--command accepts one shell string; argv arrays require manual migration.", "INVALID_FLAG_VALUE");
1000
1091
  }
1001
1092
  obj.run = input.command;
1002
1093
  }
1003
1094
  if (input.name)
1004
1095
  obj.name = input.name;
1005
- obj.akm = {
1006
- schedule: input.schedule,
1007
- enabled: input.enabled,
1008
- ...(input.description !== undefined ? { description: input.description } : {}),
1009
- ...(input.when_to_use !== undefined ? { when_to_use: input.when_to_use } : {}),
1010
- ...(input.tags && input.tags.length > 0 ? { tags: input.tags } : {}),
1011
- ...(input.engine !== undefined ? { engine: input.engine } : {}),
1012
- ...(input.model !== undefined ? { model: input.model } : {}),
1013
- ...(input.timeoutMs !== undefined ? { timeout: input.timeoutMs } : {}),
1014
- };
1096
+ if (input.description !== undefined)
1097
+ obj.description = input.description;
1098
+ if (input.when_to_use !== undefined)
1099
+ obj.when_to_use = input.when_to_use;
1100
+ if (input.tags && input.tags.length > 0)
1101
+ obj.tags = input.tags;
1102
+ if (input.engine !== undefined)
1103
+ obj.engine = input.engine;
1104
+ if (input.model !== undefined)
1105
+ obj.model = input.model;
1106
+ if (input.timeoutMs !== undefined)
1107
+ obj.timeout = input.timeoutMs;
1108
+ // Task source v4's `enabled` is per schedule-binding, not document-level
1109
+ // (P4-N6, row B-21): `--disabled` writes a one-entry schedule[] list
1110
+ // carrying `enabled: false` rather than the v3 `akm.enabled: false` flag.
1111
+ // `TasksAddInput.schedule`/the `add` CLI's `--schedule` are both still
1112
+ // required, so the "no schedule to disable" usage error B-21 also
1113
+ // describes is unreachable through this call site today; the check below
1114
+ // still guards `renderTaskYaml` itself against ever being called with an
1115
+ // empty schedule string.
1116
+ if (input.schedule.length === 0) {
1117
+ if (!input.enabled) {
1118
+ throw new UsageError("--disabled requires --schedule; a task with no schedule is already manual-only.");
1119
+ }
1120
+ }
1121
+ else {
1122
+ obj.schedule = input.enabled ? input.schedule : [{ cron: input.schedule, enabled: false }];
1123
+ }
1015
1124
  return yamlStringify(obj);
1016
1125
  }
1017
1126
  function assertInlineTaskPrompt(input) {
@@ -1036,38 +1145,116 @@ function parseJsonObjectArg(raw) {
1036
1145
  return parsed;
1037
1146
  }
1038
1147
  /**
1039
- * Toggle the v3 `akm.enabled:` value in a task YAML file without a full
1040
- * parse/render round-trip (which would reformat the file). Appends the key
1041
- * if absent.
1148
+ * Toggle a task source v4 YAML file's `enabled` state without a full
1149
+ * parse/render round-trip (which would reformat the file). Task source v4
1150
+ * has no document-level `enabled` flag — it lives on each `schedule[]` entry
1151
+ * instead (D2-N5, P4-N6) — so this walks the top-level `schedule:` block,
1152
+ * finds every list entry in it (each line starting with `-` at the block's
1153
+ * item indent), and toggles that entry's own `enabled:` key, the closest v4
1154
+ * equivalent of v3's single document-level flag broadcasting to every
1155
+ * trigger. Each entry is handled independently — one entry already carrying
1156
+ * `enabled:` and a sibling entry with no such key (D2-N3's `schedule[i]`
1157
+ * shape: `{cron, enabled?, inputs?}`) toggles the first and inserts into the
1158
+ * second, rather than one entry's existing key short-circuiting the other's
1159
+ * insertion.
1160
+ *
1161
+ * A bare string-shorthand schedule (`schedule: "0 9 * * *"`) has nowhere for
1162
+ * `enabled:` to live and is rewritten to the one-entry list form. A list
1163
+ * entry with no explicit `enabled:` key (defaulting to `true` at parse) gets
1164
+ * one inserted rather than being silently left unaffected. A document with
1165
+ * no `schedule:` key at all throws — there is no trigger to enable or
1166
+ * disable (mirrors `renderTaskYaml`'s `--disabled`-with-no-`--schedule`
1167
+ * usage error, row B-21).
1168
+ *
1169
+ * Each entry's own key indent is taken from its `-` line (the indent before
1170
+ * `-`, plus two spaces for the conventional single space after it), so a
1171
+ * nested mapping inside an entry — e.g. `schedule[i].inputs` — sits deeper
1172
+ * and is never mistaken for the entry's own `enabled:` key.
1042
1173
  *
1043
1174
  * Preserves inline comments (e.g. `enabled: true # important`) and uses
1044
1175
  * case-sensitive matching (YAML keys are case-sensitive).
1045
1176
  */
1046
1177
  export function setEnabledInYaml(yaml, enabled) {
1047
1178
  const lines = yaml.replace(/\r\n/g, "\n").split("\n");
1048
- const akmLine = lines.findIndex((line) => /^akm:\s*(?:#.*)?$/.test(line));
1049
- if (akmLine < 0)
1050
- return `${yaml.trimEnd()}\nakm:\n enabled: ${enabled}\n`;
1051
- let insertAt = akmLine + 1;
1052
- for (let index = akmLine + 1; index < lines.length; index += 1) {
1179
+ const scalarLine = lines.findIndex((line) => /^schedule:[ \t]+\S/.test(line));
1180
+ if (scalarLine >= 0) {
1181
+ const line = lines[scalarLine];
1182
+ const match = line?.match(/^schedule:[ \t]+([^\r\n]+?)[ \t]*(#[^\r\n]*)?$/);
1183
+ const cron = match?.[1] ?? "";
1184
+ const comment = match?.[2] ? ` ${match[2]}` : "";
1185
+ lines.splice(scalarLine, 1, "schedule:", ` - cron: ${cron}${comment}`, ` enabled: ${enabled}`);
1186
+ return `${lines.join("\n").trimEnd()}\n`;
1187
+ }
1188
+ const blockLine = lines.findIndex((line) => /^schedule:\s*(?:#.*)?$/.test(line));
1189
+ if (blockLine < 0) {
1190
+ throw new UsageError("Task source v4 must declare a schedule before its enabled state can be toggled.");
1191
+ }
1192
+ // Find the block's extent and every top-level list item (`-`) within it.
1193
+ // Only items at the *first* item's own indent count as entries — anything
1194
+ // deeper belongs to a nested mapping/list inside an entry (e.g. an array
1195
+ // input under `inputs:`) and must not be treated as a sibling entry.
1196
+ let blockEnd = lines.length;
1197
+ const itemStarts = [];
1198
+ let topIndent = null;
1199
+ for (let index = blockLine + 1; index < lines.length; index += 1) {
1053
1200
  const line = lines[index];
1054
- if (line === undefined)
1201
+ if (line === undefined) {
1202
+ blockEnd = index;
1055
1203
  break;
1056
- if (line !== "" && !/^[ \t]/.test(line))
1204
+ }
1205
+ if (line !== "" && !/^[ \t]/.test(line)) {
1206
+ blockEnd = index;
1057
1207
  break;
1058
- insertAt = index + 1;
1059
- const match = line.match(/^([ \t]+enabled:\s*)([^\s#\r\n][^\r\n]*?)(\s*(?:#[^\r\n]*))?$/);
1060
- if (match) {
1061
- lines[index] = `${match[1]}${enabled}${match[3] ?? ""}`;
1062
- return `${lines.join("\n").trimEnd()}\n`;
1063
1208
  }
1064
- const bare = line.match(/^([ \t]+enabled:)\s*$/);
1065
- if (bare) {
1066
- lines[index] = `${bare[1]} ${enabled}`;
1067
- return `${lines.join("\n").trimEnd()}\n`;
1209
+ const itemMatch = line.match(/^([ \t]*)-(?=[ \t]|$)/);
1210
+ if (itemMatch) {
1211
+ const itemIndent = itemMatch[1] ?? "";
1212
+ if (topIndent === null)
1213
+ topIndent = itemIndent;
1214
+ if (itemIndent === topIndent)
1215
+ itemStarts.push(index);
1216
+ }
1217
+ }
1218
+ if (itemStarts.length === 0) {
1219
+ throw new UsageError("Task source v4's schedule: block has no entries to toggle enabled on.");
1220
+ }
1221
+ // Walk entries back-to-front: inserting a missing `enabled:` line shifts
1222
+ // every later line index by one, but never touches `itemStarts[j]` for
1223
+ // j <= i (an insertion for entry i lands at `itemStarts[i] + 1`, which is
1224
+ // at or after entry i's own start), so already-computed start/end bounds
1225
+ // for entries processed later in this loop (earlier in the list) stay valid.
1226
+ for (let i = itemStarts.length - 1; i >= 0; i -= 1) {
1227
+ const start = itemStarts[i];
1228
+ const end = i + 1 < itemStarts.length ? itemStarts[i + 1] : blockEnd;
1229
+ const dashLead = lines[start]?.match(/^([ \t]*)-/)?.[1] ?? "";
1230
+ const keyIndent = `${dashLead} `;
1231
+ let found = false;
1232
+ for (let index = start; index < end; index += 1) {
1233
+ const line = lines[index];
1234
+ if (line === undefined)
1235
+ continue;
1236
+ const isStart = index === start;
1237
+ const prefixMatch = isStart ? line.match(/^([ \t]*-[ \t]*)(.*)$/) : line.match(/^([ \t]*)(.*)$/);
1238
+ const prefix = prefixMatch?.[1] ?? "";
1239
+ const content = prefixMatch?.[2] ?? "";
1240
+ if (prefix.length !== keyIndent.length)
1241
+ continue;
1242
+ const withValue = content.match(/^(enabled:[ \t]*)([^\s#\r\n][^\r\n]*?)([ \t]*(?:#[^\r\n]*))?$/);
1243
+ if (withValue) {
1244
+ lines[index] = `${prefix}${withValue[1]}${enabled}${withValue[3] ?? ""}`;
1245
+ found = true;
1246
+ continue;
1247
+ }
1248
+ const bare = content.match(/^(enabled:)[ \t]*$/);
1249
+ if (bare) {
1250
+ lines[index] = `${prefix}${bare[1]} ${enabled}`;
1251
+ found = true;
1252
+ }
1253
+ }
1254
+ if (!found) {
1255
+ lines.splice(start + 1, 0, `${keyIndent}enabled: ${enabled}`);
1068
1256
  }
1069
1257
  }
1070
- lines.splice(insertAt, 0, ` enabled: ${enabled}`);
1071
1258
  return `${lines.join("\n").trimEnd()}\n`;
1072
1259
  }
1073
1260
  // Re-exported so tests can verify the validator path directly.
@@ -1096,7 +1283,8 @@ export function parseTaskRef(input) {
1096
1283
  }
1097
1284
  return { id: normaliseTaskId(trimmed) };
1098
1285
  }
1099
- function taskIdForAdapter(parsedId, adapterId) {
1286
+ /** Exported for `src/commands/tasks/explain.ts` — see {@link resolveTaskReadBundle}'s header. */
1287
+ export function taskIdForAdapter(parsedId, adapterId) {
1100
1288
  if (adapterId === "akm-task")
1101
1289
  return normaliseTaskConceptId(parsedId);
1102
1290
  if (adapterId === "akm") {
@@ -0,0 +1,159 @@
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
+ * `akm workflow plan <ref>` — compile + freeze WITHOUT publishing (P3b, spec
6
+ * docs/plans/specs/p3b-child-executor.md §4.6). Zero durable writes, zero
7
+ * usage/event rows (row B-48): this module calls exactly the same two
8
+ * functions `startWorkflowRun` does to reach a frozen plan
9
+ * (`loadWorkflowAsset`, `compileResolveFreezeWorkflowV4`) and NOTHING else —
10
+ * never `publishWorkflowRunV4`, `startWorkflowRun`, `warn()`, `appendEvent`,
11
+ * or `akmIndex`.
12
+ *
13
+ * SECRET-FREE, by construction (§4.6's closed print list): a resolved
14
+ * reference VALUE is never printed (references resolve at pre-attempt, not
15
+ * here); a `literal` **environment** binding's value is never printed (only
16
+ * `environment[].kind`/`.name`, and an `env-ref`'s `.ref`/`.keys`/
17
+ * `.secretNames` — all NAMES); `request.command.content`,
18
+ * `request.persona`, `request.conversation`, `request.runtime.environment`,
19
+ * and a script target's `bytesBase64` are never read at all.
20
+ */
21
+ import path from "node:path";
22
+ import { loadConfig } from "../../core/config/config.js";
23
+ import { lowerResolvedExecutionRequest } from "../../integrations/agent/execution-lowering.js";
24
+ import { collectWorkflowWarnings } from "../../workflows/ir/compile.js";
25
+ import { compileResolveFreezeWorkflowV4 } from "../../workflows/ir/freeze-v4.js";
26
+ import { computePlanHash } from "../../workflows/ir/plan-hash.js";
27
+ import { loadWorkflowAsset } from "../../workflows/runtime/workflow-asset-loader.js";
28
+ /** The step's dispatch unit — the map template for a fan-out, else the root unit. Undefined for a route step. */
29
+ function stepUnit(step) {
30
+ const root = step.root;
31
+ if (!root)
32
+ return undefined;
33
+ return root.kind === "map" ? root.template : root;
34
+ }
35
+ function projectEnvironmentBinding(binding) {
36
+ if (binding.kind === "env-ref") {
37
+ return { kind: binding.kind, ref: binding.ref, keys: binding.keys, secretNames: binding.secretNames };
38
+ }
39
+ // literal / pass-through: kind + name only — a literal's VALUE is never printed.
40
+ return { kind: binding.kind, name: binding.name };
41
+ }
42
+ function projectInputBinding(binding) {
43
+ return binding.kind === "literal"
44
+ ? { name: binding.name, kind: "literal", value: binding.value }
45
+ : { name: binding.name, kind: "reference", from: binding.from };
46
+ }
47
+ function childExportedOutputNames(frozenPlan) {
48
+ return frozenPlan.outputs ? Object.keys(frozenPlan.outputs) : ["runId", "status"];
49
+ }
50
+ /**
51
+ * A `child-workflow` target's `expansion`. Recurses into the embedded
52
+ * plan's own steps in the identical shape (§4.6) — `sourceStepsById` is
53
+ * omitted for the recursive call because a nested child's own authored
54
+ * source is not available here (only its already-frozen plan is), so a
55
+ * task-wrapped step nested inside a child conservatively reports `via:
56
+ * "direct"` rather than guessing at its authoring surface.
57
+ */
58
+ function childExpansion(target) {
59
+ return {
60
+ via: "child",
61
+ childRef: target.ref,
62
+ childPlanHash: target.planHash,
63
+ childVia: target.via,
64
+ ...(target.taskRef !== undefined ? { childTaskRef: target.taskRef } : {}),
65
+ childOutputs: childExportedOutputNames(target.frozenPlan),
66
+ steps: target.frozenPlan.steps.map((step, index) => projectStep(step, index, undefined)),
67
+ };
68
+ }
69
+ /** The task/child expansion boundary for one step (§4.6). */
70
+ function stepExpansion(step, frozenTarget, sourceStepsById) {
71
+ if (frozenTarget?.kind === "child-workflow")
72
+ return childExpansion(frozenTarget);
73
+ const uses = sourceStepsById?.get(step.stepId)?.uses;
74
+ if (uses?.startsWith("tasks/"))
75
+ return { via: "task", taskRef: uses };
76
+ return { via: "direct" };
77
+ }
78
+ function projectStep(step, sequenceIndex, sourceStepsById) {
79
+ const unit = stepUnit(step);
80
+ const frozenTarget = unit?.frozenTarget;
81
+ const kind = step.route ? "route" : step.root?.kind === "map" ? "map" : "unit";
82
+ const inputBindings = frozenTarget?.inputBindings;
83
+ return {
84
+ stepId: step.stepId,
85
+ sequenceIndex,
86
+ kind,
87
+ targetKind: frozenTarget?.kind ?? null,
88
+ ...(step.root?.kind === "map" ? { concurrency: step.root.concurrency } : {}),
89
+ inputs: unit?.inputs ?? [],
90
+ environment: (unit?.environment ?? []).map(projectEnvironmentBinding),
91
+ ...(inputBindings && inputBindings.length > 0 ? { inputBindings: inputBindings.map(projectInputBinding) } : {}),
92
+ gate: {
93
+ criteria: step.gate.criteria,
94
+ maxLoops: step.gate.maxLoops,
95
+ judgeEngine: step.gate.frozenJudge ? step.gate.frozenJudge.request.engine.name : null,
96
+ },
97
+ ...(step.outputSchema !== undefined ? { outputSchema: step.outputSchema } : {}),
98
+ expansion: stepExpansion(step, frozenTarget, sourceStepsById),
99
+ };
100
+ }
101
+ /**
102
+ * Every `command`-kind frozen target's lowering notices, recomputed PURELY
103
+ * from its own already-frozen `request` (the identical computation
104
+ * `freeze/targets/command.ts`'s `commandResult` already performs at freeze
105
+ * time and discards) — walked over the whole plan, including gate judges and
106
+ * recursively into every embedded child plan. Read-only: `lowerResolvedExecutionRequest`
107
+ * takes no config it could write through and dispatches nothing.
108
+ */
109
+ function collectLoweringNotices(plan, config) {
110
+ const notices = [];
111
+ const lower = (request) => {
112
+ notices.push(...lowerResolvedExecutionRequest(request, config).notices);
113
+ };
114
+ for (const step of plan.steps) {
115
+ const unit = stepUnit(step);
116
+ if (unit) {
117
+ if (unit.frozenTarget.kind === "command")
118
+ lower(unit.frozenTarget.request);
119
+ else if (unit.frozenTarget.kind === "child-workflow")
120
+ notices.push(...collectLoweringNotices(unit.frozenTarget.frozenPlan, config));
121
+ }
122
+ if (step.gate.frozenJudge)
123
+ lower(step.gate.frozenJudge.request);
124
+ }
125
+ return notices;
126
+ }
127
+ function relativeSourceReadSet(plan) {
128
+ return plan.sourceReadSet.map((snapshot) => snapshot.identity.file);
129
+ }
130
+ /**
131
+ * Compile + freeze `ref` and project the frozen plan into the read-only
132
+ * `akm workflow plan` envelope. Never publishes, never writes, never warns.
133
+ */
134
+ export async function akmWorkflowPlan(ref) {
135
+ const asset = await loadWorkflowAsset(ref);
136
+ const config = loadConfig();
137
+ const frozen = await compileResolveFreezeWorkflowV4(asset, config);
138
+ const plan = frozen.plan;
139
+ const sourceStepsById = new Map((asset.sourceIr.jobs[0]?.steps ?? []).map((step) => [step.id, step]));
140
+ const sourceFormat = path.extname(asset.path).toLowerCase() === ".md" ? "markdown" : "github-yaml";
141
+ return {
142
+ ok: true,
143
+ ref: asset.ref,
144
+ title: asset.title,
145
+ sourceFormat,
146
+ sourcePath: asset.path,
147
+ irVersion: plan.irVersion,
148
+ planHash: computePlanHash(plan),
149
+ published: false,
150
+ execution: plan.execution,
151
+ ...(plan.budget ? { budget: plan.budget } : {}),
152
+ ...(plan.params ? { params: plan.params } : {}),
153
+ ...(plan.outputs ? { outputs: plan.outputs } : {}),
154
+ steps: plan.steps.map((step, index) => projectStep(step, index, sourceStepsById)),
155
+ sourceReadSet: relativeSourceReadSet(plan),
156
+ notices: collectLoweringNotices(plan, config),
157
+ warnings: collectWorkflowWarnings(asset.sourceIr).map((warning) => warning.message),
158
+ };
159
+ }