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
@@ -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
  };
@@ -284,18 +337,34 @@ export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
284
337
  }
285
338
  const inspection = await sched.inspectBindings({ rebind: options.rebind === true });
286
339
  const rawEntries = [...inspection.installed];
287
- const allEntries = rawEntries.map((entry) => ({
288
- ...entry,
289
- ...(entry.nativeId !== undefined ? { nativeId: entry.nativeId } : {}),
290
- ...(entry.invocation !== undefined ? { invocation: Object.freeze([...entry.invocation]) } : {}),
291
- binding: "binding" in entry ? [...entry.binding] : [],
292
- contextPath: "contextPath" in entry ? entry.contextPath : "",
293
- }));
340
+ const allEntries = rawEntries.map((entry) => {
341
+ const contextPath = "contextPath" in entry ? entry.contextPath : "";
342
+ // #846: recover the resolved bundle path this entry was installed
343
+ // under from its own scheduler-context descriptor. Any failure (no
344
+ // descriptor, unreadable, corrupt, owned by another user) leaves
345
+ // ownerBundlePath unset belongsToBundle must never treat that as
346
+ // "mine".
347
+ const ownerBundlePath = contextPath ? resolveInstalledOwnerPath(contextPath) : undefined;
348
+ return {
349
+ ...entry,
350
+ ...(entry.nativeId !== undefined ? { nativeId: entry.nativeId } : {}),
351
+ ...(entry.invocation !== undefined ? { invocation: Object.freeze([...entry.invocation]) } : {}),
352
+ binding: "binding" in entry ? [...entry.binding] : [],
353
+ contextPath,
354
+ ...(ownerBundlePath !== undefined ? { ownerBundlePath } : {}),
355
+ };
356
+ });
294
357
  const nativeArtifacts = inspection.artifacts;
295
358
  const common = {
296
359
  sourceRoot: stashDir,
297
360
  adapterId: resolved.source.adapterId ?? detectAdapterId(stashDir),
298
361
  bundleName: resolved.source.name,
362
+ // #846: only meaningful for a primary/unconfigured-bundle sync. A
363
+ // `--bundle <target>` entry's scheduler-context descriptor records the
364
+ // invoking process's OWN primary AKM_BUNDLE_DIR, not the targeted
365
+ // bundle's directory, so path-scoping stays gated on the case it's
366
+ // actually valid for (see belongsToBundle).
367
+ ...(syncTarget === undefined ? { bundlePath: path.resolve(stashDir) } : {}),
299
368
  ...(syncTarget ? { bundleTarget: syncTarget } : {}),
300
369
  backend: sched.name,
301
370
  installed: allEntries,
@@ -720,6 +789,15 @@ function groupInstalledBindings(entries, invocation) {
720
789
  }
721
790
  return [...groups.values()].map((group) => ({ ...group, taskIds: group.taskIds.sort() }));
722
791
  }
792
+ /** Best-effort recovery of an installed binding's owning bundle path (#846). */
793
+ function resolveInstalledOwnerPath(contextPath) {
794
+ try {
795
+ return validateSchedulerContextDescriptor(contextPath).environment.AKM_BUNDLE_DIR;
796
+ }
797
+ catch {
798
+ return undefined;
799
+ }
800
+ }
723
801
  function inspectInstalledBinding(entry, invocation) {
724
802
  const status = [];
725
803
  const binding = entry.binding;
@@ -905,7 +983,14 @@ function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
905
983
  };
906
984
  };
907
985
  }
908
- function resolveTaskReadBundle(refBundle, flagBundle) {
986
+ /**
987
+ * Exported for `src/commands/tasks/explain.ts` (P2b Lane B, spec
988
+ * docs/plans/specs/p2b-input-bindings.md §4.5, B-N4): `akm task explain`
989
+ * resolves its `--bundle` axis identically to every other read-only task
990
+ * verb here (`akm task history`, `akm task run`) — the SAME resolver, not a
991
+ * second one.
992
+ */
993
+ export function resolveTaskReadBundle(refBundle, flagBundle) {
909
994
  if (refBundle && flagBundle && refBundle !== flagBundle) {
910
995
  throw new UsageError(`Task ref selects bundle ${JSON.stringify(refBundle)}, but --bundle selects ${JSON.stringify(flagBundle)}.`, "INVALID_FLAG_VALUE");
911
996
  }
@@ -983,12 +1068,43 @@ function assertNoForeignSchedule(entries, id, installTarget) {
983
1068
  if (foreign)
984
1069
  throw new UsageError(foreignScheduleMessage(id, foreign.target), "RESOURCE_ALREADY_EXISTS");
985
1070
  }
1071
+ /**
1072
+ * Infer a JSON-Schema-subset `type` keyword from one `--params` value's
1073
+ * runtime shape (spec docs/plans/specs/p4-deletions-closeout.md §3.2.6, row
1074
+ * B-20). `null` is not one of the five runtime types the spec enumerates
1075
+ * (string/number/boolean/object/array) but is a value JSON.parse can still
1076
+ * produce for a param; `src/core/json-schema.ts`'s subset validator accepts
1077
+ * `"null"` as a `type`, so it is handled rather than mis-typed.
1078
+ */
1079
+ function jsonSchemaTypeOf(value) {
1080
+ if (value === null)
1081
+ return "null";
1082
+ if (Array.isArray(value))
1083
+ return "array";
1084
+ return typeof value;
1085
+ }
1086
+ /**
1087
+ * Render `--params` as typed `inputs:` declarations, one per key, each
1088
+ * carrying a `default:` equal to the authored value and a `type:` inferred
1089
+ * from its own JSON runtime shape (row B-20). Never emitted as `with:` —
1090
+ * task source v4 accepts `with:` only on `uses: akm/command` (§3.2.6's
1091
+ * `parseTarget`/`checkTopLevelKeys`), and a workflow target's declared
1092
+ * inputs are what `load-task.ts`'s existing v4 delivery override binds into
1093
+ * the child run's params.
1094
+ */
1095
+ function renderInputsFromParams(params) {
1096
+ const inputs = {};
1097
+ for (const [name, value] of Object.entries(params)) {
1098
+ inputs[name] = { type: jsonSchemaTypeOf(value), default: value };
1099
+ }
1100
+ return inputs;
1101
+ }
986
1102
  function renderTaskYaml(input) {
987
- const obj = { version: 3 };
1103
+ const obj = { version: 4 };
988
1104
  if (input.workflow) {
989
1105
  obj.uses = input.workflow;
990
1106
  if (input.params)
991
- obj.with = parseJsonObjectArg(input.params);
1107
+ obj.inputs = renderInputsFromParams(parseJsonObjectArg(input.params));
992
1108
  }
993
1109
  else if (input.prompt) {
994
1110
  obj.uses = "akm/command";
@@ -996,22 +1112,40 @@ function renderTaskYaml(input) {
996
1112
  }
997
1113
  else if (input.command !== undefined) {
998
1114
  if (Array.isArray(input.command)) {
999
- throw new UsageError("Task v3 --command accepts one shell string; argv arrays require manual migration.", "INVALID_FLAG_VALUE");
1115
+ throw new UsageError("--command accepts one shell string; argv arrays require manual migration.", "INVALID_FLAG_VALUE");
1000
1116
  }
1001
1117
  obj.run = input.command;
1002
1118
  }
1003
1119
  if (input.name)
1004
1120
  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
- };
1121
+ if (input.description !== undefined)
1122
+ obj.description = input.description;
1123
+ if (input.when_to_use !== undefined)
1124
+ obj.when_to_use = input.when_to_use;
1125
+ if (input.tags && input.tags.length > 0)
1126
+ obj.tags = input.tags;
1127
+ if (input.engine !== undefined)
1128
+ obj.engine = input.engine;
1129
+ if (input.model !== undefined)
1130
+ obj.model = input.model;
1131
+ if (input.timeoutMs !== undefined)
1132
+ obj.timeout = input.timeoutMs;
1133
+ // Task source v4's `enabled` is per schedule-binding, not document-level
1134
+ // (P4-N6, row B-21): `--disabled` writes a one-entry schedule[] list
1135
+ // carrying `enabled: false` rather than the v3 `akm.enabled: false` flag.
1136
+ // `TasksAddInput.schedule`/the `add` CLI's `--schedule` are both still
1137
+ // required, so the "no schedule to disable" usage error B-21 also
1138
+ // describes is unreachable through this call site today; the check below
1139
+ // still guards `renderTaskYaml` itself against ever being called with an
1140
+ // empty schedule string.
1141
+ if (input.schedule.length === 0) {
1142
+ if (!input.enabled) {
1143
+ throw new UsageError("--disabled requires --schedule; a task with no schedule is already manual-only.");
1144
+ }
1145
+ }
1146
+ else {
1147
+ obj.schedule = input.enabled ? input.schedule : [{ cron: input.schedule, enabled: false }];
1148
+ }
1015
1149
  return yamlStringify(obj);
1016
1150
  }
1017
1151
  function assertInlineTaskPrompt(input) {
@@ -1036,38 +1170,116 @@ function parseJsonObjectArg(raw) {
1036
1170
  return parsed;
1037
1171
  }
1038
1172
  /**
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.
1173
+ * Toggle a task source v4 YAML file's `enabled` state without a full
1174
+ * parse/render round-trip (which would reformat the file). Task source v4
1175
+ * has no document-level `enabled` flag — it lives on each `schedule[]` entry
1176
+ * instead (D2-N5, P4-N6) — so this walks the top-level `schedule:` block,
1177
+ * finds every list entry in it (each line starting with `-` at the block's
1178
+ * item indent), and toggles that entry's own `enabled:` key, the closest v4
1179
+ * equivalent of v3's single document-level flag broadcasting to every
1180
+ * trigger. Each entry is handled independently — one entry already carrying
1181
+ * `enabled:` and a sibling entry with no such key (D2-N3's `schedule[i]`
1182
+ * shape: `{cron, enabled?, inputs?}`) toggles the first and inserts into the
1183
+ * second, rather than one entry's existing key short-circuiting the other's
1184
+ * insertion.
1185
+ *
1186
+ * A bare string-shorthand schedule (`schedule: "0 9 * * *"`) has nowhere for
1187
+ * `enabled:` to live and is rewritten to the one-entry list form. A list
1188
+ * entry with no explicit `enabled:` key (defaulting to `true` at parse) gets
1189
+ * one inserted rather than being silently left unaffected. A document with
1190
+ * no `schedule:` key at all throws — there is no trigger to enable or
1191
+ * disable (mirrors `renderTaskYaml`'s `--disabled`-with-no-`--schedule`
1192
+ * usage error, row B-21).
1193
+ *
1194
+ * Each entry's own key indent is taken from its `-` line (the indent before
1195
+ * `-`, plus two spaces for the conventional single space after it), so a
1196
+ * nested mapping inside an entry — e.g. `schedule[i].inputs` — sits deeper
1197
+ * and is never mistaken for the entry's own `enabled:` key.
1042
1198
  *
1043
1199
  * Preserves inline comments (e.g. `enabled: true # important`) and uses
1044
1200
  * case-sensitive matching (YAML keys are case-sensitive).
1045
1201
  */
1046
1202
  export function setEnabledInYaml(yaml, enabled) {
1047
1203
  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) {
1204
+ const scalarLine = lines.findIndex((line) => /^schedule:[ \t]+\S/.test(line));
1205
+ if (scalarLine >= 0) {
1206
+ const line = lines[scalarLine];
1207
+ const match = line?.match(/^schedule:[ \t]+([^\r\n]+?)[ \t]*(#[^\r\n]*)?$/);
1208
+ const cron = match?.[1] ?? "";
1209
+ const comment = match?.[2] ? ` ${match[2]}` : "";
1210
+ lines.splice(scalarLine, 1, "schedule:", ` - cron: ${cron}${comment}`, ` enabled: ${enabled}`);
1211
+ return `${lines.join("\n").trimEnd()}\n`;
1212
+ }
1213
+ const blockLine = lines.findIndex((line) => /^schedule:\s*(?:#.*)?$/.test(line));
1214
+ if (blockLine < 0) {
1215
+ throw new UsageError("Task source v4 must declare a schedule before its enabled state can be toggled.");
1216
+ }
1217
+ // Find the block's extent and every top-level list item (`-`) within it.
1218
+ // Only items at the *first* item's own indent count as entries — anything
1219
+ // deeper belongs to a nested mapping/list inside an entry (e.g. an array
1220
+ // input under `inputs:`) and must not be treated as a sibling entry.
1221
+ let blockEnd = lines.length;
1222
+ const itemStarts = [];
1223
+ let topIndent = null;
1224
+ for (let index = blockLine + 1; index < lines.length; index += 1) {
1053
1225
  const line = lines[index];
1054
- if (line === undefined)
1226
+ if (line === undefined) {
1227
+ blockEnd = index;
1055
1228
  break;
1056
- if (line !== "" && !/^[ \t]/.test(line))
1229
+ }
1230
+ if (line !== "" && !/^[ \t]/.test(line)) {
1231
+ blockEnd = index;
1057
1232
  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
1233
  }
1064
- const bare = line.match(/^([ \t]+enabled:)\s*$/);
1065
- if (bare) {
1066
- lines[index] = `${bare[1]} ${enabled}`;
1067
- return `${lines.join("\n").trimEnd()}\n`;
1234
+ const itemMatch = line.match(/^([ \t]*)-(?=[ \t]|$)/);
1235
+ if (itemMatch) {
1236
+ const itemIndent = itemMatch[1] ?? "";
1237
+ if (topIndent === null)
1238
+ topIndent = itemIndent;
1239
+ if (itemIndent === topIndent)
1240
+ itemStarts.push(index);
1241
+ }
1242
+ }
1243
+ if (itemStarts.length === 0) {
1244
+ throw new UsageError("Task source v4's schedule: block has no entries to toggle enabled on.");
1245
+ }
1246
+ // Walk entries back-to-front: inserting a missing `enabled:` line shifts
1247
+ // every later line index by one, but never touches `itemStarts[j]` for
1248
+ // j <= i (an insertion for entry i lands at `itemStarts[i] + 1`, which is
1249
+ // at or after entry i's own start), so already-computed start/end bounds
1250
+ // for entries processed later in this loop (earlier in the list) stay valid.
1251
+ for (let i = itemStarts.length - 1; i >= 0; i -= 1) {
1252
+ const start = itemStarts[i];
1253
+ const end = i + 1 < itemStarts.length ? itemStarts[i + 1] : blockEnd;
1254
+ const dashLead = lines[start]?.match(/^([ \t]*)-/)?.[1] ?? "";
1255
+ const keyIndent = `${dashLead} `;
1256
+ let found = false;
1257
+ for (let index = start; index < end; index += 1) {
1258
+ const line = lines[index];
1259
+ if (line === undefined)
1260
+ continue;
1261
+ const isStart = index === start;
1262
+ const prefixMatch = isStart ? line.match(/^([ \t]*-[ \t]*)(.*)$/) : line.match(/^([ \t]*)(.*)$/);
1263
+ const prefix = prefixMatch?.[1] ?? "";
1264
+ const content = prefixMatch?.[2] ?? "";
1265
+ if (prefix.length !== keyIndent.length)
1266
+ continue;
1267
+ const withValue = content.match(/^(enabled:[ \t]*)([^\s#\r\n][^\r\n]*?)([ \t]*(?:#[^\r\n]*))?$/);
1268
+ if (withValue) {
1269
+ lines[index] = `${prefix}${withValue[1]}${enabled}${withValue[3] ?? ""}`;
1270
+ found = true;
1271
+ continue;
1272
+ }
1273
+ const bare = content.match(/^(enabled:)[ \t]*$/);
1274
+ if (bare) {
1275
+ lines[index] = `${prefix}${bare[1]} ${enabled}`;
1276
+ found = true;
1277
+ }
1278
+ }
1279
+ if (!found) {
1280
+ lines.splice(start + 1, 0, `${keyIndent}enabled: ${enabled}`);
1068
1281
  }
1069
1282
  }
1070
- lines.splice(insertAt, 0, ` enabled: ${enabled}`);
1071
1283
  return `${lines.join("\n").trimEnd()}\n`;
1072
1284
  }
1073
1285
  // Re-exported so tests can verify the validator path directly.
@@ -1096,7 +1308,8 @@ export function parseTaskRef(input) {
1096
1308
  }
1097
1309
  return { id: normaliseTaskId(trimmed) };
1098
1310
  }
1099
- function taskIdForAdapter(parsedId, adapterId) {
1311
+ /** Exported for `src/commands/tasks/explain.ts` — see {@link resolveTaskReadBundle}'s header. */
1312
+ export function taskIdForAdapter(parsedId, adapterId) {
1100
1313
  if (adapterId === "akm-task")
1101
1314
  return normaliseTaskConceptId(parsedId);
1102
1315
  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
+ }