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,626 @@
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
+ * Task source v4 — the second, additive task source grammar (spec
6
+ * docs/plans/specs/p2a-task-source-v4.md §1.1 D1, §1.2 D2, §1.5 D2-N1..D2-N7,
7
+ * §3). Never call this grammar bare "v4" in prose — the workflow plan IR is
8
+ * separately versioned (D1).
9
+ *
10
+ * `version: 4` introduces typed `inputs:`, a single bounded `output:` schema
11
+ * (legal only on command targets, the one kind whose runtime consumes it —
12
+ * see `targetConsumesOutputSchema` below),
13
+ * OPTIONAL scheduling (absent `schedule:` is valid and manual-only, D2-N6),
14
+ * and top-level execution controls (the `akm:` options bag and the `on:`
15
+ * trigger block are both GONE — every `akm:` member that D2 does not
16
+ * re-home survives as a top-level key instead, D2-N7). There is no
17
+ * github-action `uses:` variant.
18
+ *
19
+ * `src/tasks/source-v3.ts` IS edited by this phase (D2-N4): the bounded-document
20
+ * front end and the path-generic field helpers move body-intact out of that
21
+ * file into `./bounded-document.ts`, and `source-v3.ts` now imports (and,
22
+ * for the names that were already part of its public surface, re-exports)
23
+ * them instead of declaring its own copies — see `./bounded-document.ts`'s
24
+ * own header and spec §6 F-2 for the extraction itself. `parseTimeout` and
25
+ * `parseTools` moved that way; `nullableSelector` did not (a recorded
26
+ * deviation from a literal reading of D2-N4's own extraction list — see the
27
+ * P2a Review log) and stays declared directly in `source-v3.ts`.
28
+ *
29
+ * This file implements its OWN top-level-rooted versions of the three v3
30
+ * helpers that hardcode the `["akm", …]` field path —
31
+ * `parseTimeoutTopLevel`/`nullableSelectorTopLevel`/`parseToolsTopLevel`
32
+ * below, siblings of v3's `parseTimeout`/`nullableSelector`/`parseTools` —
33
+ * because task source v4 needs the same accept/reject semantics at a
34
+ * different, un-prefixed field path, not the same field path.
35
+ */
36
+ import { parseBuiltinCommandAction } from "../../commands/command/builtin-action.js";
37
+ import { UsageError } from "../../core/errors.js";
38
+ import { checkJsonSchemaDefinition, JSON_SCHEMA_SUBSET_SUPPORTED_KEYWORDS, validateJsonSchemaSubset, } from "../../core/json-schema.js";
39
+ import { DURATION_UNITS, parseDuration } from "../../core/time.js";
40
+ import { warn } from "../../core/warn.js";
41
+ import { applyInputDefaults, INPUT_NAME_PATTERN, validateInputs, } from "../../execution/input-contract.js";
42
+ import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
43
+ import { classifyTargetRef } from "../../execution/target-ref.js";
44
+ import { detectSecretShapedParams } from "../../workflows/exec/param-secrets.js";
45
+ import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_SCHEMA_BYTES, } from "../../workflows/resource-limits.js";
46
+ import { TASK_V3_HOST_SHELLS, TASK_V3_MAX_SCHEDULES } from "../source-v3.js";
47
+ import { TASK_RUN_RESERVED_FLAG_NAMES, TASK_RUN_SELF_DIAGNOSED_FLAGS } from "../task-run-reserved-flags.js";
48
+ import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, utf8Bytes, validateWorkingDirectory, } from "./bounded-document.js";
49
+ // ── Closed constants (D1, D2-N3, D2-N7) ─────────────────────────────────────
50
+ export const TASK_SOURCE_V4_VERSION = 4;
51
+ /** The exact, closed top-level key set (D2-N7) — `akm` and `on` are deliberately absent. */
52
+ export const TASK_SOURCE_V4_TOP_LEVEL_KEYS = [
53
+ "version",
54
+ "name",
55
+ "description",
56
+ "when_to_use",
57
+ "tags",
58
+ "inputs",
59
+ "output",
60
+ "uses",
61
+ "run",
62
+ "with",
63
+ "env",
64
+ "shell",
65
+ "working-directory",
66
+ "schedule",
67
+ "agent",
68
+ "engine",
69
+ "model",
70
+ "inference",
71
+ "tools",
72
+ "timeout",
73
+ "redact",
74
+ "maxSteps",
75
+ "maxRetries",
76
+ ];
77
+ /** Closes one `schedule:` list entry (D2-N5). */
78
+ export const TASK_SOURCE_V4_SCHEDULE_KEYS = ["cron", "enabled", "inputs"];
79
+ /**
80
+ * The closed key set for one `inputs.<name>` declaration root (D2-N3). The
81
+ * JSON-Schema-subset portion is DERIVED from
82
+ * `JSON_SCHEMA_SUBSET_SUPPORTED_KEYWORDS` (`src/core/json-schema.ts`) rather
83
+ * than restated, so the two lists cannot silently drift; `title`,
84
+ * `description`, and `default` are declaration keys unique to task source
85
+ * v4, layered on top (`required` is already one of the derived subset keywords — at the
86
+ * declaration ROOT it is re-interpreted as the boolean flag, D2-N3).
87
+ */
88
+ const SUBSET_KEYWORD_NAMES = JSON_SCHEMA_SUBSET_SUPPORTED_KEYWORDS.split(",")
89
+ .map((entry) => entry.split(":")[0]?.trim() ?? "")
90
+ .filter((entry) => entry.length > 0);
91
+ export const TASK_INPUT_DECLARATION_KEYS = Object.freeze([...SUBSET_KEYWORD_NAMES, "title", "description", "default"]);
92
+ const SHELL_SET = new Set(TASK_V3_HOST_SHELLS);
93
+ const SOURCE_LABEL = "task source v4";
94
+ function ctxFrom(options) {
95
+ return {
96
+ filePath: options.filePath,
97
+ sourceLabel: SOURCE_LABEL,
98
+ ...(options.workspaceRoot ? { workspaceRoot: options.workspaceRoot } : {}),
99
+ ...(options.lineAt ? { lineAt: options.lineAt } : {}),
100
+ };
101
+ }
102
+ // ── classifyTaskSourceV4Uses (spec §3.3) ────────────────────────────────────
103
+ /**
104
+ * A value SHAPED like `owner/repo[/path]@revision` — used only to produce a
105
+ * good "the github-action target was removed" message (B-13), never to
106
+ * accept. Deliberately a shape test, not a full github-locator grammar:
107
+ * native target classification recognizes no github-action variant at all
108
+ * (P4 deleted the last one, `classifyTaskV3Uses`'s locator branch in
109
+ * `source-v3.ts`) — only the shape needs to be recognized here, so the
110
+ * rejection can name the target the user typed rather than guess.
111
+ *
112
+ * Runs ONLY on {@link classifyTargetRef}'s failure path (0.9.2 review round 2):
113
+ * `@` is a legal character in a canonical asset ref, so a value the canonical
114
+ * classifier accepts (`commands/review@v2`) is a valid target even though it
115
+ * also matches this shape — the shape test upgrades the generic invalid-ref
116
+ * message, and never vetoes a ref classification accepts (spec §3.3: "exists
117
+ * only to produce a good message, never to accept").
118
+ */
119
+ function looksLikeGithubActionLocator(value) {
120
+ const at = value.lastIndexOf("@");
121
+ if (at <= 0)
122
+ return false;
123
+ const locator = value.slice(0, at);
124
+ const revision = value.slice(at + 1);
125
+ if (revision.length === 0 || /\s/.test(revision))
126
+ return false;
127
+ if (locator.length === 0 || /\s/.test(locator) || !locator.includes("/"))
128
+ return false;
129
+ return true;
130
+ }
131
+ /**
132
+ * Classify one exact `uses:` string for task source v4 (spec §3.3). Delegates
133
+ * to {@link classifyTargetRef} (`src/execution/target-ref.ts`) — the repo's
134
+ * one canonical-ref classifier — rather than re-deriving ref grammar; layers
135
+ * the `akm/command` builtin special case, the task-ref rejection (B-14), and
136
+ * — on the classification-failure path only — the github-locator-shape
137
+ * rejection (B-13) on top.
138
+ */
139
+ /**
140
+ * The remedy sentence every `uses:` rejection below ends with. ONE constant so
141
+ * the advice can never drift from what {@link TaskSourceV4UsesTarget} actually
142
+ * admits — `commands/`, `scripts/`, `workflows/`, and the `akm/command`
143
+ * builtin. Notably NOT `tasks/`: a task ref is rejected by B-14 below.
144
+ */
145
+ const TASK_SOURCE_V4_USES_REMEDY = "Use a canonical commands/, scripts/, or workflows/ ref, or akm/command, instead.";
146
+ export function classifyTaskSourceV4Uses(value) {
147
+ // Diagnostic-codes ratchet remedy (tests/architecture/diagnostic-codes.test.ts,
148
+ // established pattern at src/tasks/model/definition.ts:66-79): every
149
+ // `UsageError` below omits its `code` argument rather than spelling it out
150
+ // — the constructor already defaults to the exact code these throws need
151
+ // (src/core/errors.ts), so the thrown type, `.code`, and `.hint()` are all
152
+ // unchanged; this keeps the literal code string out of the ratchet's
153
+ // grep-style count, which only ever declines.
154
+ if (typeof value !== "string" ||
155
+ value.length === 0 ||
156
+ value.trim() !== value ||
157
+ /\s/.test(value) ||
158
+ value.includes("${{")) {
159
+ throw new UsageError("Task source v4 uses must be one exact, non-empty executable ref without expressions.");
160
+ }
161
+ if (value === "akm/command") {
162
+ return Object.freeze({ kind: "builtin-command", ref: "akm/command" });
163
+ }
164
+ let classified;
165
+ try {
166
+ classified = classifyTargetRef(value);
167
+ }
168
+ catch (cause) {
169
+ if (looksLikeGithubActionLocator(value)) {
170
+ throw new UsageError("GitHub Action targets were removed in task source v4 — the github-action uses: variant no longer exists. " +
171
+ `${TASK_SOURCE_V4_USES_REMEDY}`);
172
+ }
173
+ // `classifyTargetRef` is SHARED with the workflow classifier, where a
174
+ // `tasks/` target IS executable, so its message names all four canonical
175
+ // families. Re-raising it verbatim here advertised `tasks/` one branch
176
+ // before the B-14 check below rejects exactly that — the message named a
177
+ // target this function does not accept. Restate it against the set this
178
+ // function actually returns (p2a §6 item 4(b) / p4 R-R14). Anything the
179
+ // shared classifier throws for another reason still propagates unchanged.
180
+ if (cause instanceof UsageError && cause.code === "TARGET_REF_INVALID") {
181
+ throw new UsageError(`${JSON.stringify(value)} is not an executable task source v4 target. ${TASK_SOURCE_V4_USES_REMEDY}`);
182
+ }
183
+ throw cause instanceof Error ? cause : new UsageError(String(cause));
184
+ }
185
+ if (classified.kind === "task") {
186
+ throw new UsageError(`A task ref is not an executable task source v4 target. ${TASK_SOURCE_V4_USES_REMEDY}`);
187
+ }
188
+ return Object.freeze({ kind: classified.kind, ref: classified.ref });
189
+ }
190
+ // ── Top-level scalar/target field parsing ───────────────────────────────────
191
+ function nullableSelectorTopLevel(value, ctx, key) {
192
+ const selector = stringField(value, ctx, [key], { nullable: true });
193
+ if (selector !== null && selector.trim().length === 0)
194
+ sourceError(ctx, [key], "must be null or a non-empty string.");
195
+ return selector;
196
+ }
197
+ function parseTimeoutTopLevel(value, ctx) {
198
+ if (value === null)
199
+ return null;
200
+ if (typeof value === "string" && value.trim() !== value) {
201
+ sourceError(ctx, ["timeout"], "must not contain surrounding whitespace.");
202
+ }
203
+ const milliseconds = typeof value === "string" ? parseDuration(value, DURATION_UNITS) : value;
204
+ if (milliseconds === null ||
205
+ typeof milliseconds !== "number" ||
206
+ !Number.isSafeInteger(milliseconds) ||
207
+ milliseconds < 0 ||
208
+ milliseconds > EXECUTION_MAX_TIMEOUT_MS) {
209
+ sourceError(ctx, ["timeout"], `must be null, 0 through ${EXECUTION_MAX_TIMEOUT_MS} milliseconds, or a common duration such as 20m.`);
210
+ }
211
+ return value;
212
+ }
213
+ function parseToolsTopLevel(value, ctx) {
214
+ if (value === null || typeof value === "string")
215
+ return value;
216
+ if (Array.isArray(value)) {
217
+ if (value.some((entry) => typeof entry !== "string"))
218
+ sourceError(ctx, ["tools"], "array values must be strings.");
219
+ return value;
220
+ }
221
+ if (typeof value === "object")
222
+ return value;
223
+ sourceError(ctx, ["tools"], "must be a string, string array, mapping, or null.");
224
+ }
225
+ function parseTarget(input, ctx) {
226
+ const hasUses = own(input, "uses");
227
+ if (hasUses) {
228
+ if (own(input, "shell"))
229
+ sourceError(ctx, ["shell"], "is legal only with run.");
230
+ if (own(input, "working-directory"))
231
+ sourceError(ctx, ["working-directory"], "is legal only with run.");
232
+ const usesText = stringField(input.uses, ctx, ["uses"], { nonempty: true });
233
+ let uses;
234
+ try {
235
+ uses = classifyTaskSourceV4Uses(usesText);
236
+ }
237
+ catch (cause) {
238
+ sourceError(ctx, ["uses"], cause instanceof Error ? cause.message : String(cause));
239
+ }
240
+ let withValues;
241
+ if (own(input, "with")) {
242
+ if (uses.kind !== "builtin-command") {
243
+ sourceError(ctx, ["with"], "is legal only with uses: akm/command; declare typed inputs: instead.");
244
+ }
245
+ withValues = asRecord(presentJsonValue(input.with, ctx, ["with"]), ctx, ["with"]);
246
+ }
247
+ if (uses.kind === "builtin-command") {
248
+ let command;
249
+ try {
250
+ command = parseBuiltinCommandAction(withValues);
251
+ }
252
+ catch (cause) {
253
+ sourceError(ctx, ["with"], cause instanceof Error ? cause.message : String(cause));
254
+ }
255
+ return Object.freeze({ kind: "uses", uses, ...(withValues ? { with: withValues } : {}), command });
256
+ }
257
+ return Object.freeze({ kind: "uses", uses, ...(withValues ? { with: withValues } : {}) });
258
+ }
259
+ if (own(input, "with"))
260
+ sourceError(ctx, ["with"], "is legal only with uses: akm/command; declare typed inputs: instead.");
261
+ const run = stringField(input.run, ctx, ["run"], { nonempty: true });
262
+ noGithubExpression(run, ctx, ["run"]);
263
+ let shell;
264
+ if (own(input, "shell")) {
265
+ const rawShell = stringField(input.shell, ctx, ["shell"], { nonempty: true });
266
+ if (!SHELL_SET.has(rawShell)) {
267
+ sourceError(ctx, ["shell"], `must be one of the closed host-shell table: ${TASK_V3_HOST_SHELLS.join(", ")}.`);
268
+ }
269
+ shell = rawShell;
270
+ }
271
+ let workingDirectory;
272
+ if (own(input, "working-directory")) {
273
+ workingDirectory = stringField(input["working-directory"], ctx, ["working-directory"], {
274
+ nonempty: true,
275
+ });
276
+ validateWorkingDirectory(workingDirectory, ctx);
277
+ }
278
+ return Object.freeze({
279
+ kind: "run",
280
+ run,
281
+ ...(shell ? { shell } : {}),
282
+ ...(workingDirectory ? { workingDirectory } : {}),
283
+ });
284
+ }
285
+ function parseExecutionControls(input, ctx) {
286
+ const out = {};
287
+ for (const key of ["agent", "engine", "model"]) {
288
+ if (own(input, key))
289
+ out[key] = nullableSelectorTopLevel(input[key], ctx, key);
290
+ }
291
+ if (own(input, "inference")) {
292
+ const inference = presentJsonValue(input.inference, ctx, ["inference"]);
293
+ out.inference = inference === null ? null : asRecord(inference, ctx, ["inference"]);
294
+ }
295
+ if (own(input, "tools"))
296
+ out.tools = parseToolsTopLevel(presentJsonValue(input.tools, ctx, ["tools"]), ctx);
297
+ if (own(input, "timeout"))
298
+ out.timeout = parseTimeoutTopLevel(input.timeout, ctx);
299
+ if (own(input, "redact")) {
300
+ const names = parseStringArray(input.redact, ctx, ["redact"], {
301
+ max: WORKFLOW_MAX_EXEC_PASS_ENV,
302
+ pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
303
+ });
304
+ if (new Set(names).size !== names.length)
305
+ sourceError(ctx, ["redact"], "must not contain duplicate names.");
306
+ out.redact = names;
307
+ }
308
+ if (own(input, "maxSteps")) {
309
+ if (!Number.isSafeInteger(input.maxSteps) || input.maxSteps < 1) {
310
+ sourceError(ctx, ["maxSteps"], "must be a positive safe integer.");
311
+ }
312
+ out.maxSteps = input.maxSteps;
313
+ }
314
+ if (own(input, "maxRetries")) {
315
+ if (!Number.isSafeInteger(input.maxRetries) ||
316
+ input.maxRetries < 0 ||
317
+ input.maxRetries > WORKFLOW_MAX_RETRIES) {
318
+ sourceError(ctx, ["maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
319
+ }
320
+ out.maxRetries = input.maxRetries;
321
+ }
322
+ return Object.freeze(out);
323
+ }
324
+ // ── inputs: -> InputContract (D2-N3) ────────────────────────────────────────
325
+ function stripDeclarationAnnotations(declInput) {
326
+ const schema = {};
327
+ let hasDefault = false;
328
+ let defaultValue;
329
+ let required = false;
330
+ for (const [key, value] of Object.entries(declInput)) {
331
+ if (key === "default") {
332
+ hasDefault = true;
333
+ defaultValue = value;
334
+ continue;
335
+ }
336
+ if (key === "required") {
337
+ required = value;
338
+ continue;
339
+ }
340
+ schema[key] = value;
341
+ }
342
+ return { schema, hasDefault, defaultValue, required };
343
+ }
344
+ function parseInputDeclaration(name, raw, ctx) {
345
+ const declPath = ["inputs", name];
346
+ const declInput = asRecord(raw, ctx, declPath);
347
+ checkKeys(declInput, TASK_INPUT_DECLARATION_KEYS, ctx, declPath);
348
+ if (own(declInput, "required") && typeof declInput.required !== "boolean") {
349
+ sourceError(ctx, [...declPath, "required"], "must be a boolean at the declaration root (nested objects' own required: […] keeps ordinary JSON Schema array semantics).");
350
+ }
351
+ const { schema, hasDefault, defaultValue, required } = stripDeclarationAnnotations(declInput);
352
+ if (hasDefault && required) {
353
+ sourceError(ctx, declPath, "must not declare both default and required: true.");
354
+ }
355
+ const definitionIssue = checkJsonSchemaDefinition(schema)[0];
356
+ if (definitionIssue)
357
+ sourceError(ctx, declPath, `is not a supported JSON schema: ${definitionIssue.message}`);
358
+ if (utf8Bytes(JSON.stringify(schema)) > WORKFLOW_MAX_SCHEMA_BYTES) {
359
+ sourceError(ctx, declPath, `serialized schema exceeds the ${WORKFLOW_MAX_SCHEMA_BYTES}-byte limit.`);
360
+ }
361
+ if (hasDefault) {
362
+ const violations = validateJsonSchemaSubset(defaultValue, schema);
363
+ if (violations.length > 0) {
364
+ sourceError(ctx, [...declPath, "default"], `does not satisfy its own declaration: ${violations.join("; ")}`);
365
+ }
366
+ const secretWarnings = detectSecretShapedParams({ [name]: defaultValue });
367
+ for (const message of secretWarnings)
368
+ warn(message);
369
+ }
370
+ return Object.freeze({
371
+ schema: Object.freeze(schema),
372
+ ...(hasDefault ? { default: defaultValue } : {}),
373
+ required,
374
+ });
375
+ }
376
+ function parseInputDeclarations(value, ctx) {
377
+ const input = asRecord(value, ctx, ["inputs"]);
378
+ const names = Object.keys(input);
379
+ if (names.length > WORKFLOW_MAX_PARAMS) {
380
+ sourceError(ctx, ["inputs"], `accepts at most ${WORKFLOW_MAX_PARAMS} declared inputs.`);
381
+ }
382
+ const result = {};
383
+ for (const name of names) {
384
+ if (!INPUT_NAME_PATTERN.test(name)) {
385
+ sourceError(ctx, ["inputs", name], "must match the input name pattern (a letter/underscore, then letters, digits, or underscores).");
386
+ }
387
+ // Code-review finding (docs/plans/specs/p2b-input-bindings.md review
388
+ // round 2, scheduler-binding.ts:238): a declared input name that
389
+ // collides with a flag `akm task run` already binds to itself (--bundle,
390
+ // --scheduled, …) can never be supplied through that CLI — parseTaskInputFlags
391
+ // (../../commands/tasks/tasks-cli.ts) always treats the name as its OWN
392
+ // flag, so the value is either silently misrouted (a second --bundle
393
+ // re-targets which bundle the task loads from) or left as an orphaned
394
+ // positional token that throws. Rejecting it HERE, at declaration time,
395
+ // closes every caller at once: a bare `akm task run --<name>`, `akm task
396
+ // explain --<name>`, and a `schedule[i].inputs` entry (whose keys are
397
+ // checked against this same contract below, so a banned name can never
398
+ // reach compileTaskSchedulerBindings's invocation tail either) — see
399
+ // ../task-run-reserved-flags.ts's own header.
400
+ //
401
+ // 0.9.2 review round 2: `target` joins that set from the CLI's DIAGNOSTIC
402
+ // side rather than its declared-arg side. `rejectRetiredTaskTargetFlag`
403
+ // (../../commands/tasks/tasks-cli.ts) throws the 0.9 `--target` ->
404
+ // `--bundle` rename hint for every spelling of the name, before
405
+ // parseTaskInputFlags scans argv at all, so an input declared under that
406
+ // name could never be supplied either — it is the same unusable
407
+ // declaration, reached by a different route (TASK_RUN_SELF_DIAGNOSED_FLAGS).
408
+ if (TASK_RUN_RESERVED_FLAG_NAMES.has(name)) {
409
+ sourceError(ctx, ["inputs", name], TASK_RUN_SELF_DIAGNOSED_FLAGS.includes(name)
410
+ ? `collides with the retired \`akm task --${name}\` spelling, which every task subcommand still rejects with a rename hint, so no --${name} flag can ever reach this input; declare the input under a different name.`
411
+ : `collides with akm task run's own --${name} flag; declare the input under a different name.`);
412
+ }
413
+ result[name] = parseInputDeclaration(name, input[name], ctx);
414
+ }
415
+ return Object.freeze(result);
416
+ }
417
+ // ── output: -> bounded JSON Schema (mirrors v3's akm.outputSchema) ─────────
418
+ function parseOutputSchema(value, ctx) {
419
+ const schema = asRecord(value, ctx, ["output"]);
420
+ const issue = checkJsonSchemaDefinition(schema)[0];
421
+ if (issue)
422
+ sourceError(ctx, ["output"], `is not a supported JSON schema: ${issue.message}`);
423
+ return schema;
424
+ }
425
+ /**
426
+ * True for the target kinds whose runtime actually consumes `output:` —
427
+ * command invocations (`uses: commands/<ref>` and `uses: akm/command`), where
428
+ * `prepareTaskV3Execution` forwards it as the invocation's outputSchema
429
+ * (../prepare/prepare.ts, via prepare-support.ts's currentExecutionValues).
430
+ * `run:`, `uses: scripts/`, and `uses: workflows/` executions carry no
431
+ * output schema anywhere (run-native-task.ts decides status from the exit
432
+ * code alone; the workflow arm freezes a child plan without one), so an
433
+ * authored `output:` there would be a silently unenforced contract — the
434
+ * fifth state the fail-closed rule forbids (0.9.2 review round 2; same
435
+ * grammar pattern as `with:` being legal only with `uses: akm/command`).
436
+ */
437
+ function targetConsumesOutputSchema(target) {
438
+ return target.kind === "uses" && (target.uses.kind === "command" || target.uses.kind === "builtin-command");
439
+ }
440
+ // ── schedule: -> TaskSourceV4ScheduleBinding[] (D2-N5, D2-N6, B-06..B-10, B-38) ──
441
+ /**
442
+ * Every schedule binding must be independently runnable (0.9.2 review round 2).
443
+ *
444
+ * A scheduled firing supplies NO input flags — `compileTaskSchedulerBindings`
445
+ * (../scheduler-binding.ts) appends only the entry's own
446
+ * `schedule[i].inputs` to the `["task","run",id,"--bundle",b,"--scheduled"]`
447
+ * tail — so that literal PLUS the declared defaults is the complete value set
448
+ * `akm task run --scheduled` will see, and it is checked there by the very
449
+ * same `applyInputDefaults` + `validateInputs` pair (../run/load-task.ts).
450
+ * A `required: true` declaration may not also carry a `default` (D2-N3,
451
+ * `parseInputDeclaration` above), so an entry that names no value for one can
452
+ * never satisfy the contract, at any hour, ever.
453
+ *
454
+ * The source document alone knows both halves of that contradiction, so it is
455
+ * a grammar error: TASK_SOURCE_INVALID at the entry's own field path, naming
456
+ * the unsatisfied input(s). `akm task sync`'s projectability proof
457
+ * (../scheduler-sync.ts) keeps its own independent copy of this check over
458
+ * the DEFAULTED view — that gate is what makes such a schedule unreachable
459
+ * rather than merely ill-advised, and it still guards a task source reaching
460
+ * sync through any other path — but an author should not have to run `akm
461
+ * task sync` to learn that a document contradicts itself (the parse-time
462
+ * rejection p2a's own review log named as the natural fix). Manual `akm task
463
+ * run` is deliberately unaffected: a manual run takes the value from the
464
+ * input's own `--<name>` flag, so a required, default-less input plus NO
465
+ * `schedule:` stays valid and manual-only (B-06, D2-N6).
466
+ *
467
+ * Runs for every entry, AFTER the raw `schedule[i].inputs` check below, so an
468
+ * entry that authors an `inputs:` mapping keeps that check's exact
469
+ * message/field path; defaults can only add values already validated against
470
+ * their own declaration, so this pass can add nothing but missing-required.
471
+ */
472
+ function checkScheduleEntryRunnable(inputs, contract, ctx, entryPath) {
473
+ const errors = validateInputs(contract, applyInputDefaults(contract, { ...inputs }), { pathRoot: "inputs" });
474
+ if (errors.length === 0)
475
+ return;
476
+ sourceError(ctx, entryPath, `does not satisfy the task's declared inputs once defaults are applied: ${errors.join("; ")}. ` +
477
+ "A scheduled run supplies no input flags — give this schedule entry an inputs: value for each input " +
478
+ "named above, or declare a default: on the input instead (a required: true input may not carry one).");
479
+ }
480
+ function parseScheduleEntry(entryRaw, index, contract, ctx) {
481
+ const entryPath = ["schedule", index];
482
+ const entry = asRecord(entryRaw, ctx, entryPath);
483
+ checkKeys(entry, TASK_SOURCE_V4_SCHEDULE_KEYS, ctx, entryPath);
484
+ if (!own(entry, "cron"))
485
+ sourceError(ctx, [...entryPath, "cron"], "is required.");
486
+ const cron = stringField(entry.cron, ctx, [...entryPath, "cron"], { nonempty: true });
487
+ noGithubExpression(cron, ctx, [...entryPath, "cron"]);
488
+ let enabled = true;
489
+ if (own(entry, "enabled")) {
490
+ if (typeof entry.enabled !== "boolean")
491
+ sourceError(ctx, [...entryPath, "enabled"], "must be a boolean.");
492
+ enabled = entry.enabled;
493
+ }
494
+ let inputsLiteral = Object.freeze({});
495
+ if (own(entry, "inputs")) {
496
+ const inputsValue = asRecord(presentJsonValue(entry.inputs, ctx, [...entryPath, "inputs"]), ctx, [
497
+ ...entryPath,
498
+ "inputs",
499
+ ]);
500
+ // Fail-closed exact-name check (code-review finding, task-source-v4.ts:530):
501
+ // `validateInputs`'s synthetic `{type:"object", properties}` schema (spec
502
+ // §4.2) deliberately carries no `additionalProperties:false` — that is the
503
+ // right default for a general-purpose contract validator with other
504
+ // callers (materializeInputFlags already does its own exact-name check
505
+ // before ever calling validateInputs, per D3-N3's design) — so relying on
506
+ // validateInputs alone here would silently accept a typo'd or wholly
507
+ // undeclared schedule[i].inputs key forever, exactly the fifth state the
508
+ // fail-closed rule forbids. checkKeys mirrors materializeInputFlags' own
509
+ // exact-name rule at the grammar layer: closed against the declared
510
+ // contract, TASK_SOURCE_INVALID at schedule[<i>].inputs.<name>.
511
+ checkKeys(inputsValue, Object.keys(contract), ctx, [...entryPath, "inputs"]);
512
+ // pathRoot "inputs" (matching checkScheduleEntryRunnable below) and the
513
+ // sourceError call rooted at entryPath, not [...entryPath, "inputs"]: the
514
+ // per-error detail already carries "inputs.<name>" (validateInputs), so
515
+ // adding a SECOND ".inputs" segment to the field path here would render
516
+ // two path roots in one message (code-review finding, was the bare "$"
517
+ // default leaking through — see docs/plans/specs/p2a-task-source-v4.md
518
+ // review-log item 4a).
519
+ const errors = validateInputs(contract, inputsValue, { pathRoot: "inputs" });
520
+ if (errors.length > 0)
521
+ sourceError(ctx, entryPath, errors.join("; "));
522
+ inputsLiteral = Object.freeze({ ...inputsValue });
523
+ }
524
+ checkScheduleEntryRunnable(inputsLiteral, contract, ctx, entryPath);
525
+ return Object.freeze({ cron, enabled, inputs: inputsLiteral, source: `schedule[${index}].cron`, ordinal: index });
526
+ }
527
+ function parseSchedule(input, contract, ctx) {
528
+ if (!own(input, "schedule"))
529
+ return Object.freeze([]);
530
+ const raw = presentJsonValue(input.schedule, ctx, ["schedule"]);
531
+ if (typeof raw === "string") {
532
+ const cron = stringField(raw, ctx, ["schedule"], { nonempty: true });
533
+ noGithubExpression(cron, ctx, ["schedule"]);
534
+ // B-08's SHAPE is unchanged — one enabled binding, no inputs — but the
535
+ // shorthand is a schedule entry like any other, so it is held to the same
536
+ // runnability contract, at the `schedule` key's own field path (it has
537
+ // neither an ordinal nor an `inputs:` sub-path to point at).
538
+ checkScheduleEntryRunnable(Object.freeze({}), contract, ctx, ["schedule"]);
539
+ return Object.freeze([
540
+ Object.freeze({ cron, enabled: true, inputs: Object.freeze({}), source: "schedule", ordinal: 0 }),
541
+ ]);
542
+ }
543
+ if (!Array.isArray(raw) || raw.length === 0) {
544
+ sourceError(ctx, ["schedule"], "must be a non-empty string or a non-empty list of {cron, enabled?, inputs?} records.");
545
+ }
546
+ if (raw.length > TASK_V3_MAX_SCHEDULES) {
547
+ sourceError(ctx, ["schedule"], `accepts at most ${TASK_V3_MAX_SCHEDULES} entries.`);
548
+ }
549
+ const bindings = raw.map((entryRaw, index) => parseScheduleEntry(entryRaw, index, contract, ctx));
550
+ return Object.freeze(bindings);
551
+ }
552
+ // ── Top-level key rejection (akm:/on: removal, B-11/B-12; D2-N7) ───────────
553
+ function checkTopLevelKeys(input, ctx) {
554
+ if (own(input, "akm")) {
555
+ sourceError(ctx, ["akm"], "is removed in task source v4; its members are top-level keys now (schedule, timeout, engine, model, redact, " +
556
+ "maxSteps, maxRetries, description, when_to_use, tags, agent, inference, tools, and output for outputSchema) " +
557
+ "— see docs/reference/tasks.md.");
558
+ }
559
+ if (own(input, "on")) {
560
+ sourceError(ctx, ["on"], "is removed in task source v4; declare a top-level schedule: instead.");
561
+ }
562
+ checkKeys(input, TASK_SOURCE_V4_TOP_LEVEL_KEYS, ctx, []);
563
+ }
564
+ // ── parseTaskSourceV4Document (spec §3.2) ───────────────────────────────────
565
+ /** Parse an already-decoded JSON/YAML value as a task source v4 document (spec §3.2). */
566
+ export function parseTaskSourceV4Document(value, options) {
567
+ const ctx = ctxFrom(options);
568
+ const cloned = cloneBoundedJson(value, ctx, [], { nodes: 0 });
569
+ const input = asRecord(cloned, ctx, []);
570
+ if (!own(input, "version"))
571
+ sourceError(ctx, ["version"], "is required and must be 4.");
572
+ if (input.version !== TASK_SOURCE_V4_VERSION)
573
+ sourceError(ctx, ["version"], "must be exactly 4.");
574
+ checkTopLevelKeys(input, ctx);
575
+ const hasUses = own(input, "uses");
576
+ const hasRun = own(input, "run");
577
+ if (hasUses === hasRun)
578
+ sourceError(ctx, [], "requires exactly one executable selector: uses or run.");
579
+ const name = own(input, "name") ? stringField(input.name, ctx, ["name"]) : undefined;
580
+ const description = own(input, "description")
581
+ ? stringField(input.description, ctx, ["description"])
582
+ : undefined;
583
+ const whenToUse = own(input, "when_to_use")
584
+ ? stringField(input.when_to_use, ctx, ["when_to_use"])
585
+ : undefined;
586
+ const tags = own(input, "tags") ? parseStringArray(input.tags, ctx, ["tags"]) : undefined;
587
+ const env = own(input, "env") ? parseEnvironment(presentJsonValue(input.env, ctx, ["env"]), ctx) : undefined;
588
+ const target = parseTarget(input, ctx);
589
+ const inputs = own(input, "inputs")
590
+ ? parseInputDeclarations(presentJsonValue(input.inputs, ctx, ["inputs"]), ctx)
591
+ : undefined;
592
+ let output;
593
+ if (own(input, "output")) {
594
+ if (!targetConsumesOutputSchema(target)) {
595
+ sourceError(ctx, ["output"], "is legal only with a command target (uses: commands/<ref> or uses: akm/command); " +
596
+ "run:, uses: scripts/, and uses: workflows/ targets do not enforce an output schema.");
597
+ }
598
+ output = parseOutputSchema(presentJsonValue(input.output, ctx, ["output"]), ctx);
599
+ }
600
+ const schedule = parseSchedule(input, inputs ?? Object.freeze({}), ctx);
601
+ const execution = parseExecutionControls(input, ctx);
602
+ return Object.freeze({
603
+ version: TASK_SOURCE_V4_VERSION,
604
+ ...(name !== undefined ? { name } : {}),
605
+ ...(description !== undefined ? { description } : {}),
606
+ ...(whenToUse !== undefined ? { when_to_use: whenToUse } : {}),
607
+ ...(tags !== undefined ? { tags } : {}),
608
+ ...(inputs !== undefined ? { inputs } : {}),
609
+ ...(output !== undefined ? { output } : {}),
610
+ target,
611
+ ...(env !== undefined ? { env } : {}),
612
+ execution,
613
+ schedule,
614
+ manualOnly: schedule.length === 0,
615
+ source: Object.freeze({ path: options.filePath }),
616
+ });
617
+ }
618
+ /** Parse hostile YAML text as a task source v4 document — the standalone entry (mirrors `parseTaskV3Yaml`). */
619
+ export function parseTaskSourceV4(input) {
620
+ const { root, lineAt } = readBoundedTaskSourceYaml(input, { sourceLabel: SOURCE_LABEL });
621
+ return parseTaskSourceV4Document(root, {
622
+ filePath: input.filePath,
623
+ ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
624
+ lineAt,
625
+ });
626
+ }