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
@@ -1,700 +1,25 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /**
5
- * Canonical task-v3 source contract.
6
- *
7
- * This module owns the strict source grammar and the typed result consumed by
8
- * current task execution, scheduler binding, and workflow-source classifiers.
9
- * Legacy task input is read only by the explicit one-way migration command.
10
- */
11
- import fs from "node:fs";
12
- import path from "node:path";
13
- import { types as utilTypes } from "node:util";
14
- import { isAlias, isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
15
- import { parseBuiltinCommandAction } from "../commands/command/builtin-action.js";
16
- import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
17
- import { UsageError } from "../core/errors.js";
18
- import { checkJsonSchemaDefinition } from "../core/json-schema.js";
19
- import { DURATION_UNITS, parseDuration } from "../core/time.js";
20
- import { EXECUTION_MAX_TIMEOUT_MS } from "../execution/limits.js";
21
- import { snapshotStrictRecord } from "../execution/record.js";
22
- import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_RETRIES, } from "../workflows/resource-limits.js";
23
- export const TASK_V3_SCHEMA_VERSION = 3;
4
+ import { TASK_V3_MAX_SCHEDULES, TASK_V3_MAX_SOURCE_BYTES } from "./source/bounded-document.js";
5
+ // D2-N4 (spec docs/plans/specs/p2a-task-source-v4.md §3.1, §9): re-exported
6
+ // at their EXISTING names so no importer changes. Only the two names a
7
+ // surviving `src` consumer still imports (task-source-v4.ts,
8
+ // commands/tasks/tasks.ts) every other bounded-document re-export this
9
+ // file used to carry was dropped in P4 §3.2.3.
10
+ export { TASK_V3_MAX_SCHEDULES, TASK_V3_MAX_SOURCE_BYTES };
24
11
  export const TASK_EXTENSION = ".yml";
25
12
  export const TASK_NEAR_MISS_EXTENSION = ".yaml";
26
- export const TASK_V3_MAX_SOURCE_BYTES = 1024 * 1024;
27
- export const TASK_V3_MAX_JSON_DEPTH = 64;
28
- export const TASK_V3_MAX_JSON_NODES = 10_000;
29
- export const TASK_V3_MAX_COLLECTION_ITEMS = 1024;
30
- export const TASK_V3_MAX_OBJECT_KEYS = 256;
31
- export const TASK_V3_MAX_STRING_BYTES = 256 * 1024;
32
- export const TASK_V3_MAX_SCHEDULES = 64;
33
- /** Closed authoring vocabulary. Arbitrary GitHub `{0}` shell templates are not accepted. */
34
- export const TASK_V3_HOST_SHELLS = ["bash", "sh", "zsh", "pwsh", "powershell", "cmd"];
35
- export const TASK_V2_MIGRATION_HINT = "Run `akm migrate apply --dry-run` to preview the task-v2 to task-v3 conversion, then run `akm migrate apply`.";
36
- export function taskV2UnsupportedError(filePath, id) {
37
- const label = id ? `Task "${id}"` : "Task";
38
- return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: ${label} uses task schema version 2, which normal execution does not accept. File: ${filePath}`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_V2_MIGRATION_HINT);
39
- }
40
13
  /** Explain why the near-miss `.yaml` spelling is not a task source. */
41
14
  export function taskExtensionDetail(relPath) {
42
15
  const base = relPath.replace(/\.yaml$/i, "");
43
16
  return (`task file uses the ${TASK_NEAR_MISS_EXTENSION} extension; akm recognizes tasks only as ` +
44
17
  `${TASK_EXTENSION}, so this file is never indexed or scheduled — rename it to ${base}${TASK_EXTENSION}.`);
45
18
  }
46
- const TOP_LEVEL_KEYS = ["version", "name", "uses", "run", "with", "env", "shell", "working-directory", "akm", "on"];
47
- const AKM_KEYS = [
48
- "schedule",
49
- "enabled",
50
- "description",
51
- "when_to_use",
52
- "tags",
53
- "agent",
54
- "engine",
55
- "model",
56
- "inference",
57
- "outputSchema",
58
- "tools",
59
- "timeout",
60
- "redact",
61
- "maxSteps",
62
- "maxRetries",
63
- ];
64
- const ON_KEYS = ["schedule", "workflow_dispatch"];
65
- const SHELL_SET = new Set(TASK_V3_HOST_SHELLS);
66
- const GITHUB_OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
67
- const GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+$/;
68
- const GITHUB_ACTION_PATH_SEGMENT = /^[A-Za-z0-9_.-]+$/;
69
- const GITHUB_REF_FORBIDDEN = new Set(["~", "^", ":", "?", "*", "[", "\\"]);
70
- function own(value, key) {
71
- return Object.hasOwn(value, key);
72
- }
73
- function utf8Bytes(value) {
74
- return new TextEncoder().encode(value).byteLength;
75
- }
76
- function wellFormedUnicode(value) {
77
- for (let index = 0; index < value.length; index += 1) {
78
- const code = value.charCodeAt(index);
79
- if (code >= 0xd800 && code <= 0xdbff) {
80
- const next = value.charCodeAt(index + 1);
81
- if (!(next >= 0xdc00 && next <= 0xdfff))
82
- return false;
83
- index += 1;
84
- }
85
- else if (code >= 0xdc00 && code <= 0xdfff)
86
- return false;
87
- }
88
- return true;
89
- }
90
- function hasForbiddenGithubRefCharacter(value) {
91
- for (const character of value) {
92
- const codePoint = character.codePointAt(0) ?? 0;
93
- if (codePoint <= 0x20 || codePoint === 0x7f || GITHUB_REF_FORBIDDEN.has(character))
94
- return true;
95
- }
96
- return false;
97
- }
98
- function sourceError(ctx, fieldPath, detail) {
99
- const dotted = fieldPath.length === 0
100
- ? "$"
101
- : fieldPath.reduce((display, segment) => typeof segment === "number"
102
- ? `${display}[${segment}]`
103
- : display.length > 0
104
- ? `${display}.${segment}`
105
- : segment, "");
106
- const line = ctx.lineAt?.(fieldPath);
107
- const location = `${ctx.filePath}${line === undefined ? "" : `:${line}`}`;
108
- throw new UsageError(`Invalid task v3 source at ${location}: ${dotted} ${detail}`, "INVALID_FLAG_VALUE");
109
- }
110
- function cloneBoundedJson(value, ctx, fieldPath, state, depth = 0, ancestors = new Set()) {
111
- state.nodes += 1;
112
- if (state.nodes > TASK_V3_MAX_JSON_NODES)
113
- sourceError(ctx, fieldPath, `exceeds the ${TASK_V3_MAX_JSON_NODES}-node limit.`);
114
- if (depth > TASK_V3_MAX_JSON_DEPTH)
115
- sourceError(ctx, fieldPath, `exceeds the nesting depth of ${TASK_V3_MAX_JSON_DEPTH}.`);
116
- if (value === null || typeof value === "boolean")
117
- return value;
118
- if (typeof value === "number") {
119
- if (!Number.isFinite(value))
120
- sourceError(ctx, fieldPath, "must be a finite JSON number.");
121
- return value;
122
- }
123
- if (typeof value === "string") {
124
- if (!wellFormedUnicode(value))
125
- sourceError(ctx, fieldPath, "must contain well-formed Unicode.");
126
- if (utf8Bytes(value) > TASK_V3_MAX_STRING_BYTES) {
127
- sourceError(ctx, fieldPath, `exceeds the ${TASK_V3_MAX_STRING_BYTES}-byte string limit.`);
128
- }
129
- return value;
130
- }
131
- if (value === undefined)
132
- sourceError(ctx, fieldPath, "must be omitted instead of set to undefined.");
133
- if (typeof value !== "object")
134
- sourceError(ctx, fieldPath, "must be JSON-safe.");
135
- if (utilTypes.isProxy(value))
136
- sourceError(ctx, fieldPath, "must not be a Proxy object.");
137
- if (ancestors.has(value))
138
- sourceError(ctx, fieldPath, "must not contain a cycle.");
139
- const nextAncestors = new Set(ancestors).add(value);
140
- if (Array.isArray(value)) {
141
- if (Object.getPrototypeOf(value) !== Array.prototype)
142
- sourceError(ctx, fieldPath, "array must use the standard prototype.");
143
- const rawLength = Reflect.getOwnPropertyDescriptor(value, "length")?.value;
144
- if (typeof rawLength !== "number" ||
145
- !Number.isInteger(rawLength) ||
146
- rawLength < 0 ||
147
- rawLength > TASK_V3_MAX_COLLECTION_ITEMS) {
148
- sourceError(ctx, fieldPath, `array exceeds the ${TASK_V3_MAX_COLLECTION_ITEMS}-item limit.`);
149
- }
150
- const length = rawLength;
151
- const keys = Reflect.ownKeys(value);
152
- if (keys.length !== length + 1)
153
- sourceError(ctx, fieldPath, "array must be dense and contain no extra fields.");
154
- const result = [];
155
- for (let index = 0; index < length; index += 1) {
156
- const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
157
- if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
158
- sourceError(ctx, [...fieldPath, index], "array item must be an enumerable data property in a dense array.");
159
- }
160
- result.push(cloneBoundedJson(descriptor.value, ctx, [...fieldPath, index], state, depth + 1, nextAncestors));
161
- }
162
- return Object.freeze(result);
163
- }
164
- let snapshot;
165
- try {
166
- snapshot = snapshotStrictRecord(value, fieldPath.map(String).join(".") || "task source");
167
- }
168
- catch (cause) {
169
- sourceError(ctx, fieldPath, cause instanceof Error ? cause.message : String(cause));
170
- }
171
- const entries = Object.entries(snapshot);
172
- if (entries.length > TASK_V3_MAX_OBJECT_KEYS) {
173
- sourceError(ctx, fieldPath, `mapping exceeds the ${TASK_V3_MAX_OBJECT_KEYS}-key limit.`);
174
- }
175
- const result = Object.create(null);
176
- for (const [key, child] of entries) {
177
- if (!wellFormedUnicode(key))
178
- sourceError(ctx, fieldPath, "contains a mapping key with malformed Unicode.");
179
- if (utf8Bytes(key) > TASK_V3_MAX_STRING_BYTES) {
180
- sourceError(ctx, fieldPath, `contains a mapping key exceeding the ${TASK_V3_MAX_STRING_BYTES}-byte string limit.`);
181
- }
182
- Object.defineProperty(result, key, {
183
- value: cloneBoundedJson(child, ctx, [...fieldPath, key], state, depth + 1, nextAncestors),
184
- enumerable: true,
185
- configurable: false,
186
- writable: false,
187
- });
188
- }
189
- return Object.freeze(result);
190
- }
191
- function asRecord(value, ctx, fieldPath) {
192
- if (value === null || Array.isArray(value) || typeof value !== "object")
193
- sourceError(ctx, fieldPath, "must be a mapping.");
194
- return value;
195
- }
196
- function checkKeys(value, allowed, ctx, fieldPath) {
197
- const allow = new Set(allowed);
198
- const firstUnknown = Object.keys(value).find((key) => !allow.has(key));
199
- if (firstUnknown !== undefined)
200
- sourceError(ctx, [...fieldPath, firstUnknown], "is an unsupported field.");
201
- }
202
- function presentJsonValue(value, ctx, fieldPath) {
203
- if (value === undefined)
204
- sourceError(ctx, fieldPath, "must be omitted instead of set to undefined.");
205
- return value;
206
- }
207
- function stringField(value, ctx, fieldPath, options = {}) {
208
- if (value === null && options.nullable)
209
- return null;
210
- if (typeof value !== "string")
211
- sourceError(ctx, fieldPath, options.nullable ? "must be a string or null." : "must be a string.");
212
- if (options.nonempty && value.trim().length === 0)
213
- sourceError(ctx, fieldPath, "must be a non-empty string.");
214
- return value;
215
- }
216
- function noGithubExpression(value, ctx, fieldPath) {
217
- if (value.includes("${{"))
218
- sourceError(ctx, fieldPath, "contains an unsupported GitHub expression.");
219
- }
220
- function parseEnvironment(value, ctx) {
221
- const environment = asRecord(value, ctx, ["env"]);
222
- for (const [key, child] of Object.entries(environment)) {
223
- if (!WORKFLOW_ENV_VAR_NAME_PATTERN.test(key))
224
- sourceError(ctx, ["env", key], "has an invalid environment variable name.");
225
- if (typeof child !== "string" && typeof child !== "number" && typeof child !== "boolean") {
226
- sourceError(ctx, ["env", key], "must be a string, finite number, or boolean.");
227
- }
228
- }
229
- return environment;
230
- }
231
- function nullableSelector(value, ctx, key) {
232
- const selector = stringField(value, ctx, ["akm", key], { nullable: true });
233
- if (selector !== null && selector.trim().length === 0)
234
- sourceError(ctx, ["akm", key], "must be null or a non-empty string.");
235
- return selector;
236
- }
237
- function parseTimeout(value, ctx) {
238
- if (value === null)
239
- return null;
240
- if (typeof value === "string" && value.trim() !== value) {
241
- sourceError(ctx, ["akm", "timeout"], "must not contain surrounding whitespace.");
242
- }
243
- const milliseconds = typeof value === "string" ? parseDuration(value, DURATION_UNITS) : value;
244
- if (milliseconds === null ||
245
- typeof milliseconds !== "number" ||
246
- !Number.isSafeInteger(milliseconds) ||
247
- milliseconds < 0 ||
248
- milliseconds > EXECUTION_MAX_TIMEOUT_MS) {
249
- sourceError(ctx, ["akm", "timeout"], `must be null, 0 through ${EXECUTION_MAX_TIMEOUT_MS} milliseconds, or a common duration such as 20m.`);
250
- }
251
- return value;
252
- }
253
- function parseStringArray(value, ctx, fieldPath, options = {}) {
254
- if (!Array.isArray(value))
255
- sourceError(ctx, fieldPath, "must be an array of strings.");
256
- if (options.max !== undefined && value.length > options.max)
257
- sourceError(ctx, fieldPath, `accepts at most ${options.max} items.`);
258
- const strings = [];
259
- for (const [index, entry] of value.entries()) {
260
- if (typeof entry !== "string" || entry.length === 0)
261
- sourceError(ctx, [...fieldPath, index], "must be a non-empty string.");
262
- if (options.pattern && !options.pattern.test(entry))
263
- sourceError(ctx, [...fieldPath, index], "has an invalid value.");
264
- strings.push(entry);
265
- }
266
- return Object.freeze(strings);
267
- }
268
- function parseTools(value, ctx) {
269
- if (value === null || typeof value === "string")
270
- return value;
271
- if (Array.isArray(value)) {
272
- if (value.some((entry) => typeof entry !== "string"))
273
- sourceError(ctx, ["akm", "tools"], "array values must be strings.");
274
- return value;
275
- }
276
- if (typeof value === "object")
277
- return value;
278
- sourceError(ctx, ["akm", "tools"], "must be a string, string array, mapping, or null.");
279
- }
280
- function parseAkm(value, ctx) {
281
- const input = asRecord(value, ctx, ["akm"]);
282
- checkKeys(input, AKM_KEYS, ctx, ["akm"]);
283
- const out = {};
284
- if (own(input, "schedule")) {
285
- const schedule = stringField(input.schedule, ctx, ["akm", "schedule"], { nonempty: true });
286
- noGithubExpression(schedule, ctx, ["akm", "schedule"]);
287
- out.schedule = schedule;
288
- }
289
- if (own(input, "enabled")) {
290
- if (typeof input.enabled !== "boolean")
291
- sourceError(ctx, ["akm", "enabled"], "must be a boolean.");
292
- out.enabled = input.enabled;
293
- }
294
- for (const key of ["description", "when_to_use"]) {
295
- if (own(input, key))
296
- out[key] = stringField(input[key], ctx, ["akm", key]);
297
- }
298
- if (own(input, "tags"))
299
- out.tags = parseStringArray(input.tags, ctx, ["akm", "tags"]);
300
- for (const key of ["agent", "engine", "model"]) {
301
- if (own(input, key))
302
- out[key] = nullableSelector(input[key], ctx, key);
303
- }
304
- if (own(input, "inference")) {
305
- const inference = presentJsonValue(input.inference, ctx, ["akm", "inference"]);
306
- out.inference = inference === null ? null : asRecord(inference, ctx, ["akm", "inference"]);
307
- }
308
- if (own(input, "outputSchema")) {
309
- const outputSchema = presentJsonValue(input.outputSchema, ctx, ["akm", "outputSchema"]);
310
- if (outputSchema === null)
311
- out.outputSchema = null;
312
- else {
313
- const schema = asRecord(outputSchema, ctx, ["akm", "outputSchema"]);
314
- const issue = checkJsonSchemaDefinition(schema)[0];
315
- if (issue)
316
- sourceError(ctx, ["akm", "outputSchema"], `is not a supported JSON schema: ${issue.message}`);
317
- out.outputSchema = schema;
318
- }
319
- }
320
- if (own(input, "tools"))
321
- out.tools = parseTools(presentJsonValue(input.tools, ctx, ["akm", "tools"]), ctx);
322
- if (own(input, "timeout"))
323
- out.timeout = parseTimeout(input.timeout, ctx);
324
- if (own(input, "redact")) {
325
- const names = parseStringArray(input.redact, ctx, ["akm", "redact"], {
326
- max: WORKFLOW_MAX_EXEC_PASS_ENV,
327
- pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
328
- });
329
- if (new Set(names).size !== names.length)
330
- sourceError(ctx, ["akm", "redact"], "must not contain duplicate names.");
331
- out.redact = names;
332
- }
333
- if (own(input, "maxSteps")) {
334
- if (!Number.isSafeInteger(input.maxSteps) || input.maxSteps < 1) {
335
- sourceError(ctx, ["akm", "maxSteps"], "must be a positive safe integer.");
336
- }
337
- out.maxSteps = input.maxSteps;
338
- }
339
- if (own(input, "maxRetries")) {
340
- if (!Number.isSafeInteger(input.maxRetries) ||
341
- input.maxRetries < 0 ||
342
- input.maxRetries > WORKFLOW_MAX_RETRIES) {
343
- sourceError(ctx, ["akm", "maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
344
- }
345
- out.maxRetries = input.maxRetries;
346
- }
347
- return Object.freeze(out);
348
- }
349
- function validGithubRevision(revision) {
350
- if (revision.length === 0 ||
351
- hasForbiddenGithubRefCharacter(revision) ||
352
- revision.startsWith("/") ||
353
- revision.endsWith("/") ||
354
- revision.includes("..") ||
355
- revision.includes("@{") ||
356
- revision.includes("@")) {
357
- return false;
358
- }
359
- return revision
360
- .split("/")
361
- .every((segment) => segment.length > 0 &&
362
- segment !== "." &&
363
- segment !== ".." &&
364
- !segment.startsWith(".") &&
365
- !segment.endsWith(".") &&
366
- !segment.endsWith(".lock"));
367
- }
368
- /** Classify one exact `uses` string. This function never resolves or guesses. */
369
- export function classifyTaskV3Uses(value) {
370
- if (typeof value !== "string" ||
371
- value.length === 0 ||
372
- value.trim() !== value ||
373
- /\s/.test(value) ||
374
- value.includes("${{")) {
375
- throw new UsageError("Task v3 uses must be one exact, non-empty executable ref without expressions.", "INVALID_FLAG_VALUE");
376
- }
377
- if (value === "akm/command")
378
- return Object.freeze({ kind: "builtin-command", ref: "akm/command" });
379
- try {
380
- const parsed = parseBundleRef(value);
381
- if (parsed.fragment === undefined && bundleRefToString(parsed) === value) {
382
- const slash = parsed.conceptId.indexOf("/");
383
- const family = slash < 0 ? "" : parsed.conceptId.slice(0, slash);
384
- const name = slash < 0 ? "" : parsed.conceptId.slice(slash + 1);
385
- if (name.length > 0 && (family === "commands" || family === "workflows" || family === "scripts")) {
386
- const kind = family === "commands" ? "command" : family === "workflows" ? "workflow" : "script";
387
- return Object.freeze({ kind, ref: value });
388
- }
389
- if (family === "agents") {
390
- throw new UsageError("An agent ref selects a persona and is not executable through task v3 uses.", "INVALID_FLAG_VALUE");
391
- }
392
- if (family === "tasks") {
393
- throw new UsageError("A task ref is not an executable task-v3 uses target.", "INVALID_FLAG_VALUE");
394
- }
395
- }
396
- }
397
- catch (error) {
398
- if (error instanceof UsageError && /agent ref|task ref/i.test(error.message))
399
- throw error;
400
- }
401
- const at = value.lastIndexOf("@");
402
- if (at > 0 && at === value.indexOf("@")) {
403
- const locator = value.slice(0, at);
404
- const revision = value.slice(at + 1);
405
- const segments = locator.split("/");
406
- const [owner, repository, ...actionPath] = segments;
407
- if (owner &&
408
- repository &&
409
- GITHUB_OWNER.test(owner) &&
410
- GITHUB_REPOSITORY.test(repository) &&
411
- repository !== "." &&
412
- repository !== ".." &&
413
- actionPath.every((segment) => GITHUB_ACTION_PATH_SEGMENT.test(segment) && segment !== "." && segment !== "..") &&
414
- validGithubRevision(revision)) {
415
- const action = {
416
- kind: "github-action",
417
- ref: value,
418
- owner,
419
- repository,
420
- ...(actionPath.length > 0 ? { path: actionPath.join("/") } : {}),
421
- revision,
422
- };
423
- return Object.freeze(action);
424
- }
425
- }
426
- throw new UsageError("Task v3 uses must be akm/command, a canonical commands/, workflows/, or scripts/ asset ref, or owner/repo[/path]@ref. Agent/task/local/Docker/ambiguous targets are not executable.", "INVALID_FLAG_VALUE");
427
- }
428
- function parseOn(value, ctx) {
429
- const input = asRecord(value, ctx, ["on"]);
430
- const keys = Object.keys(input);
431
- if (keys.length === 0)
432
- sourceError(ctx, ["on"], "must declare schedule and/or workflow_dispatch.");
433
- const unsupported = keys.find((key) => !ON_KEYS.includes(key));
434
- if (unsupported)
435
- sourceError(ctx, ["on", unsupported], "is an unsupported local service event; no scheduler binding was created.");
436
- const schedules = [];
437
- if (own(input, "schedule")) {
438
- if (!Array.isArray(input.schedule) || input.schedule.length === 0) {
439
- sourceError(ctx, ["on", "schedule"], "must be a non-empty list of {cron: string} records.");
440
- }
441
- if (input.schedule.length > TASK_V3_MAX_SCHEDULES) {
442
- sourceError(ctx, ["on", "schedule"], `accepts at most ${TASK_V3_MAX_SCHEDULES} entries.`);
443
- }
444
- for (const [index, raw] of input.schedule.entries()) {
445
- const entry = asRecord(raw, ctx, ["on", "schedule", index]);
446
- checkKeys(entry, ["cron"], ctx, ["on", "schedule", index]);
447
- if (!own(entry, "cron"))
448
- sourceError(ctx, ["on", "schedule", index, "cron"], "is required.");
449
- const cron = stringField(entry.cron, ctx, ["on", "schedule", index, "cron"], { nonempty: true });
450
- noGithubExpression(cron, ctx, ["on", "schedule", index, "cron"]);
451
- schedules.push(Object.freeze({ cron, source: `on.schedule[${index}].cron`, ordinal: index }));
452
- }
453
- }
454
- let manual = false;
455
- if (own(input, "workflow_dispatch")) {
456
- const dispatch = input.workflow_dispatch;
457
- if (dispatch !== null) {
458
- const mapping = asRecord(presentJsonValue(dispatch, ctx, ["on", "workflow_dispatch"]), ctx, [
459
- "on",
460
- "workflow_dispatch",
461
- ]);
462
- if (Object.keys(mapping).length > 0) {
463
- sourceError(ctx, ["on", "workflow_dispatch"], "must be null or an empty mapping; inputs are unsupported.");
464
- }
465
- }
466
- manual = true;
467
- }
468
- return Object.freeze({ manual, schedules: Object.freeze(schedules) });
469
- }
470
- function compileTriggers(input, akm, ctx) {
471
- const hasSchedule = akm !== undefined && own(akm, "schedule");
472
- const hasOn = own(input, "on");
473
- if (hasSchedule === hasOn) {
474
- sourceError(ctx, [], "must declare exactly one scheduling source: akm.schedule or on.");
475
- }
476
- if (hasOn)
477
- return parseOn(presentJsonValue(input.on, ctx, ["on"]), ctx);
478
- return Object.freeze({
479
- manual: false,
480
- schedules: Object.freeze([Object.freeze({ cron: akm?.schedule, source: "akm.schedule", ordinal: 0 })]),
481
- });
482
- }
483
- function parseTaskV3TriggerFields(input, ctx) {
484
- const akm = own(input, "akm") ? parseAkm(presentJsonValue(input.akm, ctx, ["akm"]), ctx) : undefined;
485
- return Object.freeze({ ...(akm ? { akm } : {}), triggers: compileTriggers(input, akm, ctx) });
486
- }
487
- /**
488
- * Classify the strict trigger fragment `{akm?, on?}` into deterministic local
489
- * scheduler bindings. Full workflow adapters pass only those two fields; this
490
- * rejects `jobs` and every other workflow field rather than owning WP7's
491
- * document grammar.
492
- */
493
- export function classifyTaskV3Triggers(value, options) {
494
- const ctx = options;
495
- const cloned = cloneBoundedJson(value, ctx, [], { nodes: 0 });
496
- const input = asRecord(cloned, ctx, []);
497
- checkKeys(input, ["akm", "on"], ctx, []);
498
- return parseTaskV3TriggerFields(input, ctx).triggers;
499
- }
500
- function validateWorkingDirectory(value, ctx) {
501
- if (value.trim().length === 0 ||
502
- value.includes("\0") ||
503
- path.posix.isAbsolute(value.replaceAll("\\", "/")) ||
504
- /^[A-Za-z]:[\\/]/.test(value) ||
505
- value.startsWith("\\\\")) {
506
- sourceError(ctx, ["working-directory"], "must be a non-empty relative path contained by the workspace root.");
507
- }
508
- const segments = value.replaceAll("\\", "/").split("/");
509
- if (segments.some((segment) => segment === ".." || segment.length === 0)) {
510
- sourceError(ctx, ["working-directory"], "must not contain empty or escaping path segments.");
511
- }
512
- if (!ctx.workspaceRoot) {
513
- sourceError(ctx, ["working-directory"], "requires a workspace root so physical containment can be verified.");
514
- }
515
- let realRoot;
516
- let realCandidate;
517
- try {
518
- realRoot = fs.realpathSync(ctx.workspaceRoot);
519
- const candidate = path.resolve(realRoot, value);
520
- const stat = fs.statSync(candidate);
521
- if (!stat.isDirectory())
522
- sourceError(ctx, ["working-directory"], "must resolve to a directory.");
523
- realCandidate = fs.realpathSync(candidate);
524
- }
525
- catch (cause) {
526
- if (cause instanceof UsageError)
527
- throw cause;
528
- sourceError(ctx, ["working-directory"], `cannot be physically verified: ${cause instanceof Error ? cause.message : String(cause)}.`);
529
- }
530
- const relative = path.relative(realRoot, realCandidate);
531
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
532
- sourceError(ctx, ["working-directory"], "resolves outside the workspace root and is not physically contained.");
533
- }
534
- }
535
- export function parseTaskV3Document(value, options) {
536
- const ctx = options;
537
- const cloned = cloneBoundedJson(value, ctx, [], { nodes: 0 });
538
- const input = asRecord(cloned, ctx, []);
539
- if (!own(input, "version"))
540
- sourceError(ctx, ["version"], "is required and must be 3.");
541
- if (input.version === 2)
542
- throw taskV2UnsupportedError(options.filePath);
543
- if (input.version !== TASK_V3_SCHEMA_VERSION)
544
- sourceError(ctx, ["version"], "must be exactly 3.");
545
- checkKeys(input, TOP_LEVEL_KEYS, ctx, []);
546
- const hasUses = own(input, "uses");
547
- const hasRun = own(input, "run");
548
- if (hasUses === hasRun)
549
- sourceError(ctx, [], "requires exactly one executable selector: uses or run.");
550
- const name = own(input, "name") ? stringField(input.name, ctx, ["name"]) : undefined;
551
- const env = own(input, "env") ? parseEnvironment(presentJsonValue(input.env, ctx, ["env"]), ctx) : undefined;
552
- const { akm, triggers } = parseTaskV3TriggerFields(input, ctx);
553
- let target;
554
- if (hasUses) {
555
- if (own(input, "shell"))
556
- sourceError(ctx, ["shell"], "is legal only with run.");
557
- if (own(input, "working-directory"))
558
- sourceError(ctx, ["working-directory"], "is legal only with run.");
559
- const usesText = stringField(input.uses, ctx, ["uses"], { nonempty: true });
560
- let uses;
561
- try {
562
- uses = classifyTaskV3Uses(usesText);
563
- }
564
- catch (cause) {
565
- sourceError(ctx, ["uses"], cause instanceof Error ? cause.message : String(cause));
566
- }
567
- let withValues;
568
- if (own(input, "with"))
569
- withValues = asRecord(presentJsonValue(input.with, ctx, ["with"]), ctx, ["with"]);
570
- if (uses.kind === "builtin-command") {
571
- let command;
572
- try {
573
- command = parseBuiltinCommandAction(withValues);
574
- }
575
- catch (cause) {
576
- sourceError(ctx, ["with"], cause instanceof Error ? cause.message : String(cause));
577
- }
578
- target = Object.freeze({ kind: "uses", uses, ...(withValues ? { with: withValues } : {}), command });
579
- }
580
- else {
581
- target = Object.freeze({ kind: "uses", uses, ...(withValues ? { with: withValues } : {}) });
582
- }
583
- }
584
- else {
585
- if (own(input, "with"))
586
- sourceError(ctx, ["with"], "is legal only with uses.");
587
- const run = stringField(input.run, ctx, ["run"], { nonempty: true });
588
- noGithubExpression(run, ctx, ["run"]);
589
- let shell;
590
- if (own(input, "shell")) {
591
- const rawShell = stringField(input.shell, ctx, ["shell"], { nonempty: true });
592
- if (!SHELL_SET.has(rawShell)) {
593
- sourceError(ctx, ["shell"], `must be one of the closed host-shell table: ${TASK_V3_HOST_SHELLS.join(", ")}.`);
594
- }
595
- shell = rawShell;
596
- }
597
- let workingDirectory;
598
- if (own(input, "working-directory")) {
599
- workingDirectory = stringField(input["working-directory"], ctx, ["working-directory"], {
600
- nonempty: true,
601
- });
602
- validateWorkingDirectory(workingDirectory, ctx);
603
- }
604
- target = Object.freeze({
605
- kind: "run",
606
- run,
607
- ...(shell ? { shell } : {}),
608
- ...(workingDirectory ? { workingDirectory } : {}),
609
- });
610
- }
611
- return Object.freeze({
612
- version: TASK_V3_SCHEMA_VERSION,
613
- ...(name !== undefined ? { name } : {}),
614
- target,
615
- ...(env !== undefined ? { env } : {}),
616
- ...(akm !== undefined ? { akm } : {}),
617
- triggers,
618
- source: Object.freeze({ path: options.filePath }),
619
- });
620
- }
621
- function yamlProblem(message) {
622
- return message.split("\n")[0]?.trim() || "invalid YAML";
623
- }
624
- function yamlAstError(options, node, detail) {
625
- const range = node?.range;
626
- const line = range && options.lineCounter ? options.lineCounter.linePos(range[0] ?? 0).line : undefined;
627
- throw new UsageError(`Invalid ${options.sourceLabel} at ${options.filePath}${line === undefined ? "" : `:${line}`}: ${detail}`, "INVALID_FLAG_VALUE");
628
- }
629
- /**
630
- * Bound and close the YAML AST before `toJS` can allocate or recurse through
631
- * it. This is shared by the v3 parser and the explicit v2 migration reader.
632
- */
633
- export function assertBoundedTaskYamlDocument(document, options) {
634
- const stack = [{ node: document.contents, depth: 0 }];
635
- let nodes = 0;
636
- while (stack.length > 0) {
637
- const current = stack.pop();
638
- if (!current)
639
- break;
640
- const node = current.node;
641
- if (node === null || node === undefined)
642
- continue;
643
- nodes += 1;
644
- if (nodes > TASK_V3_MAX_JSON_NODES) {
645
- yamlAstError(options, node, `YAML exceeds the ${TASK_V3_MAX_JSON_NODES}-node limit.`);
646
- }
647
- if (current.depth > TASK_V3_MAX_JSON_DEPTH) {
648
- yamlAstError(options, node, `YAML exceeds the nesting depth of ${TASK_V3_MAX_JSON_DEPTH}.`);
649
- }
650
- if (isAlias(node))
651
- yamlAstError(options, node, "YAML aliases are unsupported.");
652
- if (node.anchor !== undefined) {
653
- yamlAstError(options, node, "YAML anchors are unsupported.");
654
- }
655
- if (node.tag) {
656
- yamlAstError(options, node, "custom or explicit YAML tags are unsupported.");
657
- }
658
- if (isScalar(node))
659
- continue;
660
- if (isSeq(node)) {
661
- if (node.items.length > TASK_V3_MAX_COLLECTION_ITEMS) {
662
- yamlAstError(options, node, `YAML sequence exceeds the ${TASK_V3_MAX_COLLECTION_ITEMS}-item limit.`);
663
- }
664
- for (let index = node.items.length - 1; index >= 0; index -= 1) {
665
- stack.push({ node: node.items[index], depth: current.depth + 1 });
666
- }
667
- continue;
668
- }
669
- if (isMap(node)) {
670
- if (node.items.length > TASK_V3_MAX_OBJECT_KEYS) {
671
- yamlAstError(options, node, `YAML mapping exceeds the ${TASK_V3_MAX_OBJECT_KEYS}-key limit.`);
672
- }
673
- for (let index = node.items.length - 1; index >= 0; index -= 1) {
674
- const pair = node.items[index];
675
- if (!pair)
676
- yamlAstError(options, node, "sparse YAML mappings are unsupported.");
677
- if (!isScalar(pair.key) || typeof pair.key.value !== "string") {
678
- yamlAstError(options, pair.key, "non-string YAML mapping keys are unsupported.");
679
- }
680
- if (!wellFormedUnicode(pair.key.value)) {
681
- yamlAstError(options, pair.key, "YAML mapping key must contain well-formed Unicode.");
682
- }
683
- if (utf8Bytes(pair.key.value) > TASK_V3_MAX_STRING_BYTES) {
684
- yamlAstError(options, pair.key, `YAML mapping key exceeds the ${TASK_V3_MAX_STRING_BYTES}-byte string limit.`);
685
- }
686
- if (pair.key.value === "<<")
687
- yamlAstError(options, pair.key, "YAML merge keys are unsupported.");
688
- stack.push({ node: pair.value, depth: current.depth + 1 });
689
- stack.push({ node: pair.key, depth: current.depth + 1 });
690
- }
691
- continue;
692
- }
693
- yamlAstError(options, node, "unsupported YAML node kind.");
694
- }
695
- }
19
+ /** Closed authoring vocabulary. Arbitrary GitHub `{0}` shell templates are not accepted. */
20
+ export const TASK_V3_HOST_SHELLS = ["bash", "sh", "zsh", "pwsh", "powershell", "cmd"];
696
21
  /** Preserve actionable classified hints when a parser failure becomes a diagnostic. */
697
- export function taskV3SourceErrorDetail(cause) {
22
+ export function taskSourceErrorDetail(cause) {
698
23
  if (!(cause instanceof Error))
699
24
  return String(cause);
700
25
  const hint = "hint" in cause && typeof cause.hint === "function"
@@ -702,51 +27,3 @@ export function taskV3SourceErrorDetail(cause) {
702
27
  : undefined;
703
28
  return hint ? `${cause.message} ${hint}` : cause.message;
704
29
  }
705
- /** Parse hostile YAML without aliases/tags/merges, then enter the canonical object parser. */
706
- export function parseTaskV3Yaml(input) {
707
- if (typeof input.yaml !== "string") {
708
- throw new UsageError(`Invalid task v3 source at ${input.filePath}: source must be a string.`, "INVALID_FLAG_VALUE");
709
- }
710
- if (utf8Bytes(input.yaml) > TASK_V3_MAX_SOURCE_BYTES) {
711
- throw new UsageError(`Invalid task v3 source at ${input.filePath}: source exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) resource limit.`, "INVALID_FLAG_VALUE");
712
- }
713
- const lineCounter = new LineCounter();
714
- let document;
715
- try {
716
- document = parseDocument(input.yaml, { lineCounter, uniqueKeys: true });
717
- }
718
- catch (cause) {
719
- throw new UsageError(`Invalid task v3 source at ${input.filePath}: YAML parsing failed: ${cause instanceof Error ? cause.message : String(cause)}`, "INVALID_FLAG_VALUE");
720
- }
721
- const [problem] = document.errors;
722
- if (problem) {
723
- const offset = Array.isArray(problem.pos) ? problem.pos[0] : 0;
724
- throw new UsageError(`Invalid task v3 source at ${input.filePath}:${lineCounter.linePos(offset).line}: ${yamlProblem(problem.message)}`, "INVALID_FLAG_VALUE");
725
- }
726
- const [warning] = document.warnings;
727
- if (warning) {
728
- throw new UsageError(`Invalid task v3 source at ${input.filePath}: unsupported YAML construct: ${yamlProblem(warning.message)}`, "INVALID_FLAG_VALUE");
729
- }
730
- assertBoundedTaskYamlDocument(document, { filePath: input.filePath, sourceLabel: "task v3 source", lineCounter });
731
- let root;
732
- try {
733
- root = document.toJS({ maxAliasCount: 0 });
734
- }
735
- catch (cause) {
736
- throw new UsageError(`Invalid task v3 source at ${input.filePath}: YAML expansion failed: ${cause instanceof Error ? cause.message : String(cause)}`, "INVALID_FLAG_VALUE");
737
- }
738
- const lineAt = (fieldPath) => {
739
- for (let depth = fieldPath.length; depth >= 0; depth -= 1) {
740
- const node = depth === 0 ? document.contents : document.getIn(fieldPath.slice(0, depth), true);
741
- const range = node?.range;
742
- if (range)
743
- return lineCounter.linePos(range[0]).line;
744
- }
745
- return undefined;
746
- };
747
- return parseTaskV3Document(root, {
748
- filePath: input.filePath,
749
- ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
750
- lineAt,
751
- });
752
- }