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.
- package/CHANGELOG.md +493 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +8 -4
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +265 -52
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +77 -14
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +24 -7
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +3 -2
- package/docs/migration/v0.8-to-v0.9.md +13 -11
- package/docs/migration/v0.9.0-troubleshooting.md +20 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +6 -5
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +290 -16
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- 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
|
+
}
|