akm-cli 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +233 -1
  2. package/README.md +1 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/cli.js +5 -5
  8. package/dist/commands/health/improve-metrics.js +17 -0
  9. package/dist/commands/health/windows.js +2 -2
  10. package/dist/commands/health.js +2 -2
  11. package/dist/commands/improve/anti-collapse.js +4 -91
  12. package/dist/commands/improve/preparation.js +8 -1
  13. package/dist/commands/lint/index.js +3 -7
  14. package/dist/commands/proposal/validators/proposal-validators.js +12 -0
  15. package/dist/commands/read/search.js +14 -24
  16. package/dist/commands/tasks/tasks-cli.js +81 -3
  17. package/dist/commands/tasks/tasks.js +117 -2
  18. package/dist/core/adapter/adapters/akm-adapter.js +23 -14
  19. package/dist/core/adapter/adapters/akm-lint.js +3 -2
  20. package/dist/core/adapter/adapters/akm-task-adapter.js +9 -6
  21. package/dist/core/adapter/adapters/dotenv-adapter.js +13 -11
  22. package/dist/core/adapter/recognize-match.js +1 -20
  23. package/dist/core/asset/asset-placement.js +21 -2
  24. package/dist/core/common.js +21 -1
  25. package/dist/core/config/config-version-shim.js +101 -0
  26. package/dist/core/config/config.js +6 -6
  27. package/dist/core/improve-result.js +35 -14
  28. package/dist/execution/guarded-source.js +0 -10
  29. package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
  30. package/dist/indexer/passes/metadata.js +12 -4
  31. package/dist/indexer/scan/doc-to-entry.js +2 -0
  32. package/dist/indexer/search/db-search.js +6 -0
  33. package/dist/indexer/search/search-fields.js +16 -1
  34. package/dist/indexer/walk/matchers.js +0 -22
  35. package/dist/output/shapes/helpers.js +19 -1
  36. package/dist/output/shapes/passthrough.js +18 -5
  37. package/dist/output/text/command-format.js +4 -0
  38. package/dist/registry/pinned-request-helper.js +2 -2
  39. package/dist/registry/pinned-transport.js +6 -6
  40. package/dist/scripts/akm-migrate-node.js +12678 -12601
  41. package/dist/scripts/akm-migrate.js +12678 -12601
  42. package/dist/storage/repositories/proposals-repository.js +65 -7
  43. package/dist/storage/repositories/task-history-repository.js +22 -10
  44. package/dist/tasks/run/task-history.js +23 -3
  45. package/dist/tasks/scheduler-binding.js +15 -5
  46. package/dist/tasks/scheduler-sync-preview.js +45 -0
  47. package/dist/tasks/scheduler-sync.js +77 -41
  48. package/dist/tasks/source/bounded-document.js +1 -1
  49. package/dist/tasks/source/parse-task-source.js +77 -11
  50. package/dist/tasks/source/task-source-v3-frozen.js +428 -0
  51. package/dist/tasks/source/task-to-v3.js +512 -0
  52. package/dist/tasks/source/task-to-v4.js +457 -0
  53. package/docs/reference/cli.md +33 -6
  54. package/docs/reference/configuration.md +27 -6
  55. package/docs/reference/tasks.md +10 -0
  56. package/package.json +4 -4
@@ -0,0 +1,428 @@
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
+ * The frozen task-v3 reader (spec docs/plans/specs/p4-deletions-closeout.md
6
+ * §3.2.4, P4-N1).
7
+ *
8
+ * `src/` no longer accepts task v3; this copy exists so the migrator can
9
+ * still READ what it converts. It is frozen: it is never extended, and a
10
+ * change to task source v4 never propagates here. It deliberately
11
+ * duplicates code that used to live in `src/tasks/source-v3.ts` — moved
12
+ * body-intact, verbatim, from that file as it stood at the parent of P4's
13
+ * commit 2 (`refactor(p4): remove github-action locator grammar from
14
+ * native classification`) — i.e. INCLUDING the GitHub Action locator
15
+ * grammar that commit deleted from native classification.
16
+ * `src/tasks/source/task-to-v4.ts`'s `github-action-target-removed`
17
+ * blocked reason needs this migrator to still be able to CLASSIFY a
18
+ * locator (not merely detect its shape) so it can name the target
19
+ * explicitly rather than guess (`tests/migrate/task-v3-to-v4.test.ts`'s
20
+ * `github-action-target-removed` case).
21
+ *
22
+ * This is the ONLY place the task v3 grammar survives. The frozen-migrator
23
+ * rule runs one way — this file may import from `src/`, `src/` may never
24
+ * import from `scripts/`:
25
+ * `rg -n 'from "\.\./\.\./scripts|from "\.\./scripts|scripts/akm-migrate' src/`
26
+ * returns zero hits, before and after this file exists.
27
+ *
28
+ * Do not "fix" a bug found here by porting a task source v4 change, and do
29
+ * not add a task source v4 feature here. A behavior change to what this
30
+ * file accepts is a behavior change to what the migrator can read FROM —
31
+ * exactly the drift a frozen migrator exists to prevent. Two things this
32
+ * file does NOT carry over from the original, because no migrator module
33
+ * needs them: the bounded-document re-export block (D2-N4's own comment
34
+ * explained that block existed only so OTHER `src/` consumers did not need
35
+ * to import `./source/bounded-document` directly — every consumer here
36
+ * already imports what it needs from `../../tasks/source/bounded-document`
37
+ * on its own), and `taskExtensionDetail`/`TASK_EXTENSION`/
38
+ * `TASK_NEAR_MISS_EXTENSION`/`taskV3SourceErrorDetail` (presentation
39
+ * helpers for `src/`-side consumers, not part of reading a document).
40
+ */
41
+ import { parseBuiltinCommandAction } from "../../commands/command/builtin-action.js";
42
+ import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
43
+ import { UsageError } from "../../core/errors.js";
44
+ import { checkJsonSchemaDefinition } from "../../core/json-schema.js";
45
+ import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_RETRIES, } from "../../workflows/resource-limits.js";
46
+ import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, parseTimeout, parseTools, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_SCHEDULES, validateWorkingDirectory, } from "./bounded-document.js";
47
+ export const TASK_V3_SCHEMA_VERSION = 3;
48
+ /** Closed authoring vocabulary. Arbitrary GitHub `{0}` shell templates are not accepted. */
49
+ export const TASK_V3_HOST_SHELLS = ["bash", "sh", "zsh", "pwsh", "powershell", "cmd"];
50
+ 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`.";
51
+ export function taskV2UnsupportedError(filePath, id) {
52
+ const label = id ? `Task "${id}"` : "Task";
53
+ 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);
54
+ }
55
+ const SOURCE_LABEL = "task v3 source";
56
+ /** Build the shared `BoundedDocumentContext` from this file's own (narrower) options shapes. */
57
+ function ctxFrom(options) {
58
+ return {
59
+ filePath: options.filePath,
60
+ sourceLabel: SOURCE_LABEL,
61
+ ...(options.workspaceRoot ? { workspaceRoot: options.workspaceRoot } : {}),
62
+ ...(options.lineAt ? { lineAt: options.lineAt } : {}),
63
+ };
64
+ }
65
+ const TOP_LEVEL_KEYS = ["version", "name", "uses", "run", "with", "env", "shell", "working-directory", "akm", "on"];
66
+ const AKM_KEYS = [
67
+ "schedule",
68
+ "enabled",
69
+ "description",
70
+ "when_to_use",
71
+ "tags",
72
+ "agent",
73
+ "engine",
74
+ "model",
75
+ "inference",
76
+ "outputSchema",
77
+ "tools",
78
+ "timeout",
79
+ "redact",
80
+ "maxSteps",
81
+ "maxRetries",
82
+ ];
83
+ const ON_KEYS = ["schedule", "workflow_dispatch"];
84
+ const SHELL_SET = new Set(TASK_V3_HOST_SHELLS);
85
+ const GITHUB_OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
86
+ const GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+$/;
87
+ const GITHUB_ACTION_PATH_SEGMENT = /^[A-Za-z0-9_.-]+$/;
88
+ const GITHUB_REF_FORBIDDEN = new Set(["~", "^", ":", "?", "*", "[", "\\"]);
89
+ function hasForbiddenGithubRefCharacter(value) {
90
+ for (const character of value) {
91
+ const codePoint = character.codePointAt(0) ?? 0;
92
+ if (codePoint <= 0x20 || codePoint === 0x7f || GITHUB_REF_FORBIDDEN.has(character))
93
+ return true;
94
+ }
95
+ return false;
96
+ }
97
+ function nullableSelector(value, ctx, key) {
98
+ const selector = stringField(value, ctx, ["akm", key], { nullable: true });
99
+ if (selector !== null && selector.trim().length === 0)
100
+ sourceError(ctx, ["akm", key], "must be null or a non-empty string.");
101
+ return selector;
102
+ }
103
+ function parseAkm(value, ctx) {
104
+ const input = asRecord(value, ctx, ["akm"]);
105
+ checkKeys(input, AKM_KEYS, ctx, ["akm"]);
106
+ const out = {};
107
+ if (own(input, "schedule")) {
108
+ const schedule = stringField(input.schedule, ctx, ["akm", "schedule"], { nonempty: true });
109
+ noGithubExpression(schedule, ctx, ["akm", "schedule"]);
110
+ out.schedule = schedule;
111
+ }
112
+ if (own(input, "enabled")) {
113
+ if (typeof input.enabled !== "boolean")
114
+ sourceError(ctx, ["akm", "enabled"], "must be a boolean.");
115
+ out.enabled = input.enabled;
116
+ }
117
+ for (const key of ["description", "when_to_use"]) {
118
+ if (own(input, key))
119
+ out[key] = stringField(input[key], ctx, ["akm", key]);
120
+ }
121
+ if (own(input, "tags"))
122
+ out.tags = parseStringArray(input.tags, ctx, ["akm", "tags"]);
123
+ for (const key of ["agent", "engine", "model"]) {
124
+ if (own(input, key))
125
+ out[key] = nullableSelector(input[key], ctx, key);
126
+ }
127
+ if (own(input, "inference")) {
128
+ const inference = presentJsonValue(input.inference, ctx, ["akm", "inference"]);
129
+ out.inference = inference === null ? null : asRecord(inference, ctx, ["akm", "inference"]);
130
+ }
131
+ if (own(input, "outputSchema")) {
132
+ const outputSchema = presentJsonValue(input.outputSchema, ctx, ["akm", "outputSchema"]);
133
+ if (outputSchema === null)
134
+ out.outputSchema = null;
135
+ else {
136
+ const schema = asRecord(outputSchema, ctx, ["akm", "outputSchema"]);
137
+ const issue = checkJsonSchemaDefinition(schema)[0];
138
+ if (issue)
139
+ sourceError(ctx, ["akm", "outputSchema"], `is not a supported JSON schema: ${issue.message}`);
140
+ out.outputSchema = schema;
141
+ }
142
+ }
143
+ if (own(input, "tools"))
144
+ out.tools = parseTools(presentJsonValue(input.tools, ctx, ["akm", "tools"]), ctx);
145
+ if (own(input, "timeout"))
146
+ out.timeout = parseTimeout(input.timeout, ctx);
147
+ if (own(input, "redact")) {
148
+ const names = parseStringArray(input.redact, ctx, ["akm", "redact"], {
149
+ max: WORKFLOW_MAX_EXEC_PASS_ENV,
150
+ pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
151
+ });
152
+ if (new Set(names).size !== names.length)
153
+ sourceError(ctx, ["akm", "redact"], "must not contain duplicate names.");
154
+ out.redact = names;
155
+ }
156
+ if (own(input, "maxSteps")) {
157
+ if (!Number.isSafeInteger(input.maxSteps) || input.maxSteps < 1) {
158
+ sourceError(ctx, ["akm", "maxSteps"], "must be a positive safe integer.");
159
+ }
160
+ out.maxSteps = input.maxSteps;
161
+ }
162
+ if (own(input, "maxRetries")) {
163
+ if (!Number.isSafeInteger(input.maxRetries) ||
164
+ input.maxRetries < 0 ||
165
+ input.maxRetries > WORKFLOW_MAX_RETRIES) {
166
+ sourceError(ctx, ["akm", "maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
167
+ }
168
+ out.maxRetries = input.maxRetries;
169
+ }
170
+ return Object.freeze(out);
171
+ }
172
+ function validGithubRevision(revision) {
173
+ if (revision.length === 0 ||
174
+ hasForbiddenGithubRefCharacter(revision) ||
175
+ revision.startsWith("/") ||
176
+ revision.endsWith("/") ||
177
+ revision.includes("..") ||
178
+ revision.includes("@{") ||
179
+ revision.includes("@")) {
180
+ return false;
181
+ }
182
+ return revision
183
+ .split("/")
184
+ .every((segment) => segment.length > 0 &&
185
+ segment !== "." &&
186
+ segment !== ".." &&
187
+ !segment.startsWith(".") &&
188
+ !segment.endsWith(".") &&
189
+ !segment.endsWith(".lock"));
190
+ }
191
+ /** Classify one exact `uses` string. This function never resolves or guesses. */
192
+ export function classifyTaskV3Uses(value) {
193
+ if (typeof value !== "string" ||
194
+ value.length === 0 ||
195
+ value.trim() !== value ||
196
+ /\s/.test(value) ||
197
+ value.includes("${{")) {
198
+ throw new UsageError("Task v3 uses must be one exact, non-empty executable ref without expressions.", "INVALID_FLAG_VALUE");
199
+ }
200
+ if (value === "akm/command")
201
+ return Object.freeze({ kind: "builtin-command", ref: "akm/command" });
202
+ try {
203
+ const parsed = parseBundleRef(value);
204
+ if (parsed.fragment === undefined && bundleRefToString(parsed) === value) {
205
+ const slash = parsed.conceptId.indexOf("/");
206
+ const family = slash < 0 ? "" : parsed.conceptId.slice(0, slash);
207
+ const name = slash < 0 ? "" : parsed.conceptId.slice(slash + 1);
208
+ if (name.length > 0 && (family === "commands" || family === "workflows" || family === "scripts")) {
209
+ const kind = family === "commands" ? "command" : family === "workflows" ? "workflow" : "script";
210
+ return Object.freeze({ kind, ref: value });
211
+ }
212
+ if (family === "agents") {
213
+ throw new UsageError("An agent ref selects a persona and is not executable through task v3 uses.", "INVALID_FLAG_VALUE");
214
+ }
215
+ if (family === "tasks") {
216
+ throw new UsageError("A task ref is not an executable task-v3 uses target.", "INVALID_FLAG_VALUE");
217
+ }
218
+ }
219
+ }
220
+ catch (error) {
221
+ if (error instanceof UsageError && /agent ref|task ref/i.test(error.message))
222
+ throw error;
223
+ }
224
+ const at = value.lastIndexOf("@");
225
+ if (at > 0 && at === value.indexOf("@")) {
226
+ const locator = value.slice(0, at);
227
+ const revision = value.slice(at + 1);
228
+ const segments = locator.split("/");
229
+ const [owner, repository, ...actionPath] = segments;
230
+ if (owner &&
231
+ repository &&
232
+ GITHUB_OWNER.test(owner) &&
233
+ GITHUB_REPOSITORY.test(repository) &&
234
+ repository !== "." &&
235
+ repository !== ".." &&
236
+ actionPath.every((segment) => GITHUB_ACTION_PATH_SEGMENT.test(segment) && segment !== "." && segment !== "..") &&
237
+ validGithubRevision(revision)) {
238
+ const action = {
239
+ kind: "github-action",
240
+ ref: value,
241
+ owner,
242
+ repository,
243
+ ...(actionPath.length > 0 ? { path: actionPath.join("/") } : {}),
244
+ revision,
245
+ };
246
+ return Object.freeze(action);
247
+ }
248
+ }
249
+ 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");
250
+ }
251
+ function parseOn(value, ctx) {
252
+ const input = asRecord(value, ctx, ["on"]);
253
+ const keys = Object.keys(input);
254
+ if (keys.length === 0)
255
+ sourceError(ctx, ["on"], "must declare schedule and/or workflow_dispatch.");
256
+ const unsupported = keys.find((key) => !ON_KEYS.includes(key));
257
+ if (unsupported)
258
+ sourceError(ctx, ["on", unsupported], "is an unsupported local service event; no scheduler binding was created.");
259
+ const schedules = [];
260
+ if (own(input, "schedule")) {
261
+ if (!Array.isArray(input.schedule) || input.schedule.length === 0) {
262
+ sourceError(ctx, ["on", "schedule"], "must be a non-empty list of {cron: string} records.");
263
+ }
264
+ if (input.schedule.length > TASK_V3_MAX_SCHEDULES) {
265
+ sourceError(ctx, ["on", "schedule"], `accepts at most ${TASK_V3_MAX_SCHEDULES} entries.`);
266
+ }
267
+ for (const [index, raw] of input.schedule.entries()) {
268
+ const entry = asRecord(raw, ctx, ["on", "schedule", index]);
269
+ checkKeys(entry, ["cron"], ctx, ["on", "schedule", index]);
270
+ if (!own(entry, "cron"))
271
+ sourceError(ctx, ["on", "schedule", index, "cron"], "is required.");
272
+ const cron = stringField(entry.cron, ctx, ["on", "schedule", index, "cron"], { nonempty: true });
273
+ noGithubExpression(cron, ctx, ["on", "schedule", index, "cron"]);
274
+ schedules.push(Object.freeze({ cron, source: `on.schedule[${index}].cron`, ordinal: index }));
275
+ }
276
+ }
277
+ let manual = false;
278
+ if (own(input, "workflow_dispatch")) {
279
+ const dispatch = input.workflow_dispatch;
280
+ if (dispatch !== null) {
281
+ const mapping = asRecord(presentJsonValue(dispatch, ctx, ["on", "workflow_dispatch"]), ctx, [
282
+ "on",
283
+ "workflow_dispatch",
284
+ ]);
285
+ if (Object.keys(mapping).length > 0) {
286
+ sourceError(ctx, ["on", "workflow_dispatch"], "must be null or an empty mapping; inputs are unsupported.");
287
+ }
288
+ }
289
+ manual = true;
290
+ }
291
+ return Object.freeze({ manual, schedules: Object.freeze(schedules) });
292
+ }
293
+ function compileTriggers(input, akm, ctx) {
294
+ const hasSchedule = akm !== undefined && own(akm, "schedule");
295
+ const hasOn = own(input, "on");
296
+ if (hasSchedule === hasOn) {
297
+ sourceError(ctx, [], "must declare exactly one scheduling source: akm.schedule or on.");
298
+ }
299
+ if (hasOn)
300
+ return parseOn(presentJsonValue(input.on, ctx, ["on"]), ctx);
301
+ return Object.freeze({
302
+ manual: false,
303
+ schedules: Object.freeze([Object.freeze({ cron: akm?.schedule, source: "akm.schedule", ordinal: 0 })]),
304
+ });
305
+ }
306
+ function parseTaskV3TriggerFields(input, ctx) {
307
+ const akm = own(input, "akm") ? parseAkm(presentJsonValue(input.akm, ctx, ["akm"]), ctx) : undefined;
308
+ return Object.freeze({ ...(akm ? { akm } : {}), triggers: compileTriggers(input, akm, ctx) });
309
+ }
310
+ /**
311
+ * Classify the strict trigger fragment `{akm?, on?}` into deterministic local
312
+ * scheduler bindings. Unused by the migrator today (the three migrator
313
+ * modules only call `parseTaskV3Yaml`) — vendored anyway for fidelity with
314
+ * the original file this was moved body-intact from, and because
315
+ * `parseTaskV3Document` shares its trigger-parsing helpers
316
+ * (`parseTaskV3TriggerFields`/`compileTriggers`/`parseOn`/`parseAkm`) with
317
+ * it. The LIVE, `src`-side classifier this was re-homed from is
318
+ * `classifyWorkflowYamlTriggers` (`src/workflows/source-ir/triggers.ts`,
319
+ * P4-N3) — that one, not this frozen copy, is what
320
+ * `src/workflows/source-ir/compile.ts` injects.
321
+ */
322
+ export function classifyTaskV3Triggers(value, options) {
323
+ const ctx = ctxFrom(options);
324
+ const cloned = cloneBoundedJson(value, ctx, [], { nodes: 0 });
325
+ const input = asRecord(cloned, ctx, []);
326
+ checkKeys(input, ["akm", "on"], ctx, []);
327
+ return parseTaskV3TriggerFields(input, ctx).triggers;
328
+ }
329
+ export function parseTaskV3Document(value, options) {
330
+ const ctx = ctxFrom(options);
331
+ const cloned = cloneBoundedJson(value, ctx, [], { nodes: 0 });
332
+ const input = asRecord(cloned, ctx, []);
333
+ if (!own(input, "version"))
334
+ sourceError(ctx, ["version"], "is required and must be 3.");
335
+ if (input.version === 2)
336
+ throw taskV2UnsupportedError(options.filePath);
337
+ if (input.version !== TASK_V3_SCHEMA_VERSION)
338
+ sourceError(ctx, ["version"], "must be exactly 3.");
339
+ checkKeys(input, TOP_LEVEL_KEYS, ctx, []);
340
+ const hasUses = own(input, "uses");
341
+ const hasRun = own(input, "run");
342
+ if (hasUses === hasRun)
343
+ sourceError(ctx, [], "requires exactly one executable selector: uses or run.");
344
+ const name = own(input, "name") ? stringField(input.name, ctx, ["name"]) : undefined;
345
+ const env = own(input, "env") ? parseEnvironment(presentJsonValue(input.env, ctx, ["env"]), ctx) : undefined;
346
+ const { akm, triggers } = parseTaskV3TriggerFields(input, ctx);
347
+ let target;
348
+ if (hasUses) {
349
+ if (own(input, "shell"))
350
+ sourceError(ctx, ["shell"], "is legal only with run.");
351
+ if (own(input, "working-directory"))
352
+ sourceError(ctx, ["working-directory"], "is legal only with run.");
353
+ const usesText = stringField(input.uses, ctx, ["uses"], { nonempty: true });
354
+ let uses;
355
+ try {
356
+ uses = classifyTaskV3Uses(usesText);
357
+ }
358
+ catch (cause) {
359
+ sourceError(ctx, ["uses"], cause instanceof Error ? cause.message : String(cause));
360
+ }
361
+ let withValues;
362
+ if (own(input, "with"))
363
+ withValues = asRecord(presentJsonValue(input.with, ctx, ["with"]), ctx, ["with"]);
364
+ if (uses.kind === "builtin-command") {
365
+ let command;
366
+ try {
367
+ command = parseBuiltinCommandAction(withValues);
368
+ }
369
+ catch (cause) {
370
+ sourceError(ctx, ["with"], cause instanceof Error ? cause.message : String(cause));
371
+ }
372
+ target = Object.freeze({ kind: "uses", uses, ...(withValues ? { with: withValues } : {}), command });
373
+ }
374
+ else {
375
+ target = Object.freeze({ kind: "uses", uses, ...(withValues ? { with: withValues } : {}) });
376
+ }
377
+ }
378
+ else {
379
+ if (own(input, "with"))
380
+ sourceError(ctx, ["with"], "is legal only with uses.");
381
+ const run = stringField(input.run, ctx, ["run"], { nonempty: true });
382
+ noGithubExpression(run, ctx, ["run"]);
383
+ let shell;
384
+ if (own(input, "shell")) {
385
+ const rawShell = stringField(input.shell, ctx, ["shell"], { nonempty: true });
386
+ if (!SHELL_SET.has(rawShell)) {
387
+ sourceError(ctx, ["shell"], `must be one of the closed host-shell table: ${TASK_V3_HOST_SHELLS.join(", ")}.`);
388
+ }
389
+ shell = rawShell;
390
+ }
391
+ let workingDirectory;
392
+ if (own(input, "working-directory")) {
393
+ workingDirectory = stringField(input["working-directory"], ctx, ["working-directory"], {
394
+ nonempty: true,
395
+ });
396
+ validateWorkingDirectory(workingDirectory, ctx);
397
+ }
398
+ target = Object.freeze({
399
+ kind: "run",
400
+ run,
401
+ ...(shell ? { shell } : {}),
402
+ ...(workingDirectory ? { workingDirectory } : {}),
403
+ });
404
+ }
405
+ return Object.freeze({
406
+ version: TASK_V3_SCHEMA_VERSION,
407
+ ...(name !== undefined ? { name } : {}),
408
+ target,
409
+ ...(env !== undefined ? { env } : {}),
410
+ ...(akm !== undefined ? { akm } : {}),
411
+ triggers,
412
+ source: Object.freeze({ path: options.filePath }),
413
+ });
414
+ }
415
+ /**
416
+ * Parse hostile YAML without aliases/tags/merges, then enter the canonical
417
+ * object parser. The bounded-YAML front end itself is
418
+ * `readBoundedTaskSourceYaml` (`src/tasks/source/bounded-document.ts`) —
419
+ * this is a thin wrapper passing `sourceLabel: "task v3 source"`.
420
+ */
421
+ export function parseTaskV3Yaml(input) {
422
+ const { root, lineAt } = readBoundedTaskSourceYaml(input, { sourceLabel: SOURCE_LABEL });
423
+ return parseTaskV3Document(root, {
424
+ filePath: input.filePath,
425
+ ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
426
+ lineAt,
427
+ });
428
+ }