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,304 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `akm task explain <ref> [input flags]` — read-only task introspection
|
|
6
|
+
* (spec docs/plans/specs/p2b-input-bindings.md §4.5, §1.7 B-N4).
|
|
7
|
+
*
|
|
8
|
+
* Prints the task source path and owning bundle, its input declarations with
|
|
9
|
+
* defaults, the supplied values WITH PROVENANCE (`default` | `flag` |
|
|
10
|
+
* `schedule-binding`), the resolved target kind + ref, effective execution
|
|
11
|
+
* settings with field-level provenance, and schedule bindings.
|
|
12
|
+
*
|
|
13
|
+
* Read-only, by construction: this module never reserves a durable attempt
|
|
14
|
+
* (`src/tasks/run/attempt-lifecycle.ts` is never imported here), never writes
|
|
15
|
+
* history or a log (`src/tasks/run/task-history.ts` /
|
|
16
|
+
* `src/tasks/run/task-log.ts` are never imported here), never touches the
|
|
17
|
+
* scheduler (`src/tasks/scheduler-*.ts` are never imported here), and never
|
|
18
|
+
* resolves a composed command/persona REF against the local index
|
|
19
|
+
* (`src/tasks/prepare/prepare.ts`'s own command branch always does, via
|
|
20
|
+
* `prepareCommandInvocation`'s default `sourceLoader` — an unindexed bundle
|
|
21
|
+
* would otherwise make a read-only introspection command fail with "Run `akm
|
|
22
|
+
* index` to build it", which is the wrong shape of dependency for a command
|
|
23
|
+
* whose whole point is to work on a task the caller just wrote).
|
|
24
|
+
*
|
|
25
|
+
* SECRET-FREE BY CONSTRUCTION for the structural bans below — these hold
|
|
26
|
+
* regardless of any value's shape, because the excluded data never reaches
|
|
27
|
+
* this module in the first place:
|
|
28
|
+
*
|
|
29
|
+
* - a resolved `env:` value never reaches this module at all — the task's
|
|
30
|
+
* OWN `env:` map is deliberately never read from the parsed document;
|
|
31
|
+
* - the composed target's ref is read STRUCTURALLY from the already-parsed
|
|
32
|
+
* document (`document.target.uses.ref` / `"akm/command"`), never from a
|
|
33
|
+
* rendered command/persona source — so a `run:` command string,
|
|
34
|
+
* `with.content`, an inline `akm/command` prompt body, or a stored
|
|
35
|
+
* command's own rendered content is never read, let alone printed;
|
|
36
|
+
* - effective execution settings (engine/model/timeout) reuse
|
|
37
|
+
* `prepareResolvedExecution` (`src/integrations/agent/execution-preparation.ts`)
|
|
38
|
+
* — the SAME cascade-composition entry point `prepareCommandInvocation`
|
|
39
|
+
* itself calls once a command/persona source is already rendered — fed a
|
|
40
|
+
* BLANK inline placeholder command (`createInlineResolvedCommand`) and an
|
|
41
|
+
* EMPTY command-layer values map, instead of the task's real referenced
|
|
42
|
+
* command source. This is a deliberate, documented trade-off: a
|
|
43
|
+
* referenced command's OWN frontmatter overrides (its own `engine:`/
|
|
44
|
+
* `model:`, if any) do not contribute a layer here, in exchange for never
|
|
45
|
+
* needing the index, never reading the referenced file's bytes, and never
|
|
46
|
+
* risking a leaked `runtime.environment` value (the cascade's own
|
|
47
|
+
* "current" layer, built here from the TASK's own `execution` overrides
|
|
48
|
+
* ONLY — engine/model/inference/outputSchema/tools/timeout, deliberately
|
|
49
|
+
* never `environment`/`workspace`/`agent`, so a portable persona selector
|
|
50
|
+
* can never demand a persona this module never loads). Only
|
|
51
|
+
* `engine`/`model`/`runtime.timeoutMs` and the cascade's own
|
|
52
|
+
* `{layer,kind,via}` provenance map are read back — never
|
|
53
|
+
* `request.command`, `request.persona`, `request.conversation`, or
|
|
54
|
+
* `request.runtime.environment`/`.workspace`.
|
|
55
|
+
*
|
|
56
|
+
* BEST-EFFORT, NOT A GUARANTEE, for input VALUES: a secret-shaped input
|
|
57
|
+
* value is redacted only when `detectSecretShapedParams`
|
|
58
|
+
* (`src/workflows/exec/param-secrets.ts`) recognizes its shape, and that
|
|
59
|
+
* detector is an explicitly best-effort heuristic — its own doc comment
|
|
60
|
+
* acknowledges expected false negatives (a short, low-entropy, or
|
|
61
|
+
* unusually-named credential prints unredacted). A recognized value prints
|
|
62
|
+
* as the literal string `"<redacted>"`, with its row marked
|
|
63
|
+
* `redacted: true`, instead of the real value — applied uniformly across
|
|
64
|
+
* every place a declared or supplied VALUE can appear in this envelope: a
|
|
65
|
+
* declaration's own `default`, each entry of a declaration's own `enum`
|
|
66
|
+
* list (checked per-entry, not as one blanked list — code-review finding,
|
|
67
|
+
* explain.ts:184), a `suppliedInputs` entry regardless of provenance, and a
|
|
68
|
+
* `schedule[].inputs` entry (code-review finding: the declaration row alone
|
|
69
|
+
* left the identical value printed verbatim one section over, in
|
|
70
|
+
* `suppliedInputs`). Do not paste this command's output into an untrusted
|
|
71
|
+
* place on the assumption that it can never contain a credential.
|
|
72
|
+
*
|
|
73
|
+
* Field-level execution provenance is READ from `planExecutionCascade`'s own
|
|
74
|
+
* `ResolvedExecutionPlanV1.provenance` (via `prepareResolvedExecution`) —
|
|
75
|
+
* this module is a CONSUMER of the one common cascade resolver, never a
|
|
76
|
+
* second resolver: it never re-derives engine/model precedence itself.
|
|
77
|
+
*/
|
|
78
|
+
import fs from "node:fs";
|
|
79
|
+
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
80
|
+
import { loadConfig } from "../../core/config/config.js";
|
|
81
|
+
import { NotFoundError } from "../../core/errors.js";
|
|
82
|
+
import { applyInputDefaults, materializeInputFlags, } from "../../execution/input-contract.js";
|
|
83
|
+
import { createInlineResolvedCommand } from "../../execution/resolved-request.js";
|
|
84
|
+
import { resolveAdapterConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
|
|
85
|
+
import { prepareResolvedExecution } from "../../integrations/agent/execution-preparation.js";
|
|
86
|
+
import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
|
|
87
|
+
import { projectTaskSourceV4 } from "../../tasks/source/project-v4.js";
|
|
88
|
+
import { TASK_INPUT_DIAGNOSTICS } from "../../tasks/source/task-input-diagnostics.js";
|
|
89
|
+
import { validateTaskConceptId, validateTaskId } from "../../tasks/task-id.js";
|
|
90
|
+
import { detectSecretShapedParams } from "../../workflows/exec/param-secrets.js";
|
|
91
|
+
import { parseTaskRef, resolveTaskReadBundle, taskIdForAdapter } from "./tasks.js";
|
|
92
|
+
function isSecretShapedValue(name, value) {
|
|
93
|
+
return detectSecretShapedParams({ [name]: value }).length > 0;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Code-review finding (explain.ts:175, B-N4): `buildInputDeclarations`
|
|
97
|
+
* already redacted a secret-shaped DEFAULT, but `buildSuppliedInputs` (and
|
|
98
|
+
* a v4 schedule entry's own `inputs`, printed verbatim from the source)
|
|
99
|
+
* echoed the identical value unredacted in the same envelope — a
|
|
100
|
+
* `sk-live-…` default appeared in full in both output formats. Applied to
|
|
101
|
+
* every supplied/schedule-binding row regardless of provenance, so the same
|
|
102
|
+
* best-effort redaction (see this file's header) applies uniformly across
|
|
103
|
+
* the whole envelope, not just the declaration row.
|
|
104
|
+
*/
|
|
105
|
+
function redactIfSecretShaped(name, value) {
|
|
106
|
+
if (!isSecretShapedValue(name, value))
|
|
107
|
+
return { value };
|
|
108
|
+
return { value: "<redacted>", redacted: true };
|
|
109
|
+
}
|
|
110
|
+
function declarationType(schema) {
|
|
111
|
+
return typeof schema.type === "string" ? schema.type : undefined;
|
|
112
|
+
}
|
|
113
|
+
function declarationEnum(schema) {
|
|
114
|
+
return Array.isArray(schema.enum) ? schema.enum : undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Code-review finding (explain.ts:184, B-N4): `buildInputDeclarations`
|
|
118
|
+
* already redacted a secret-shaped `default`, via {@link redactIfSecretShaped}
|
|
119
|
+
* applied to the WHOLE default value, but the identical value appearing in
|
|
120
|
+
* that same declaration's JSON-Schema `enum:` list printed unredacted one
|
|
121
|
+
* key over — an `enum: [..., "sk-live-…"]` list echoed the credential in
|
|
122
|
+
* full even though the matching `default:` on the same declaration was
|
|
123
|
+
* already redacted. Fixed by applying the SAME per-value check
|
|
124
|
+
* ({@link isSecretShapedValue}) to each entry independently, replacing only
|
|
125
|
+
* the secret-shaped ones with `"<redacted>"` — deliberately NOT collapsing
|
|
126
|
+
* the whole list to one `"<redacted>"` sentinel the way a single `default`
|
|
127
|
+
* value is, since an `enum` list's non-secret alternatives are useful
|
|
128
|
+
* provenance in their own right and dropping them would over-redact.
|
|
129
|
+
*/
|
|
130
|
+
function redactEnum(name, values) {
|
|
131
|
+
let redacted = false;
|
|
132
|
+
const out = values.map((value) => {
|
|
133
|
+
if (!isSecretShapedValue(name, value))
|
|
134
|
+
return value;
|
|
135
|
+
redacted = true;
|
|
136
|
+
return "<redacted>";
|
|
137
|
+
});
|
|
138
|
+
return { enum: out, redacted };
|
|
139
|
+
}
|
|
140
|
+
function buildInputDeclarations(contract) {
|
|
141
|
+
const out = {};
|
|
142
|
+
for (const [name, declaration] of Object.entries(contract)) {
|
|
143
|
+
const typedDeclaration = declaration;
|
|
144
|
+
const hasDefault = Object.hasOwn(typedDeclaration, "default");
|
|
145
|
+
const secretDefault = hasDefault && isSecretShapedValue(name, typedDeclaration.default);
|
|
146
|
+
const enumValues = declarationEnum(typedDeclaration.schema);
|
|
147
|
+
const enumRedaction = enumValues !== undefined ? redactEnum(name, enumValues) : undefined;
|
|
148
|
+
out[name] = {
|
|
149
|
+
...(declarationType(typedDeclaration.schema) !== undefined
|
|
150
|
+
? { type: declarationType(typedDeclaration.schema) }
|
|
151
|
+
: {}),
|
|
152
|
+
...(enumRedaction !== undefined ? { enum: enumRedaction.enum } : {}),
|
|
153
|
+
required: typedDeclaration.required,
|
|
154
|
+
...(hasDefault ? { default: secretDefault ? "<redacted>" : typedDeclaration.default } : {}),
|
|
155
|
+
...(secretDefault || enumRedaction?.redacted ? { redacted: true } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
function buildSuppliedInputs(defaultedInputs, materializedInputs) {
|
|
161
|
+
const out = {};
|
|
162
|
+
for (const [name, value] of Object.entries(defaultedInputs)) {
|
|
163
|
+
const provenance = Object.hasOwn(materializedInputs, name) ? "flag" : "default";
|
|
164
|
+
const redaction = redactIfSecretShaped(name, value);
|
|
165
|
+
out[name] = { value: redaction.value, provenance, ...(redaction.redacted ? { redacted: true } : {}) };
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
/** The composed target's kind + ref, read STRUCTURALLY off the already-parsed document — never from a rendered command/persona source. */
|
|
170
|
+
function targetSection(document) {
|
|
171
|
+
if (document.target.kind === "run")
|
|
172
|
+
return { kind: "shell" };
|
|
173
|
+
const uses = document.target.uses;
|
|
174
|
+
return { kind: uses.kind, ref: uses.ref };
|
|
175
|
+
}
|
|
176
|
+
/** True only for the two `uses:` kinds an execution cascade (engine/model resolution) actually applies to. */
|
|
177
|
+
function isCommandTarget(document) {
|
|
178
|
+
return (document.target.kind === "uses" &&
|
|
179
|
+
(document.target.uses.kind === "builtin-command" || document.target.uses.kind === "command"));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The task's own top-level execution overrides (`document.akm`, D2-N7's
|
|
183
|
+
* home for both a v3 document's authored `akm.*` and a v4 document's
|
|
184
|
+
* projected `execution` block) as a cascade "current" layer — the exact
|
|
185
|
+
* non-secret subset `src/tasks/prepare/prepare-support.ts`'s
|
|
186
|
+
* `currentExecutionValues` also forwards, minus `agent`/`workspace`/
|
|
187
|
+
* `environment` (this module never loads a persona and never reads `env:`,
|
|
188
|
+
* see this file's header).
|
|
189
|
+
*/
|
|
190
|
+
function currentExecutionValues(document) {
|
|
191
|
+
const akm = document.akm;
|
|
192
|
+
if (!akm)
|
|
193
|
+
return {};
|
|
194
|
+
const out = {};
|
|
195
|
+
for (const key of ["engine", "model", "inference", "outputSchema", "tools", "timeout"]) {
|
|
196
|
+
if (Object.hasOwn(akm, key))
|
|
197
|
+
out[key] = akm[key];
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Effective execution settings + field-level provenance for a command-kind
|
|
203
|
+
* target — see this file's header for why this is a BLANK-command cascade
|
|
204
|
+
* call rather than a real command/persona load.
|
|
205
|
+
*/
|
|
206
|
+
function resolveExecutionSettings(document) {
|
|
207
|
+
if (!isCommandTarget(document))
|
|
208
|
+
return { provenance: {} };
|
|
209
|
+
const current = currentExecutionValues(document);
|
|
210
|
+
const prepared = prepareResolvedExecution({
|
|
211
|
+
command: createInlineResolvedCommand({ template: "", content: "" }),
|
|
212
|
+
config: loadConfig(),
|
|
213
|
+
invocationKind: "task",
|
|
214
|
+
commandLayer: { id: "task-explain", values: {} },
|
|
215
|
+
...(Object.keys(current).length > 0 ? { current } : {}),
|
|
216
|
+
});
|
|
217
|
+
const engine = prepared.request.engine;
|
|
218
|
+
return {
|
|
219
|
+
engine: {
|
|
220
|
+
name: engine.name,
|
|
221
|
+
kind: engine.kind,
|
|
222
|
+
...(Object.hasOwn(engine, "platform") ? { platform: engine.platform } : {}),
|
|
223
|
+
},
|
|
224
|
+
...(prepared.request.model !== undefined ? { model: prepared.request.model } : {}),
|
|
225
|
+
...(prepared.request.runtime.timeoutMs !== undefined ? { timeoutMs: prepared.request.runtime.timeoutMs } : {}),
|
|
226
|
+
provenance: prepared.plan.provenance,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Resolve, parse, and project one task asset into an explain envelope with
|
|
231
|
+
* every secret-shaped value redacted on a best-effort basis (see this
|
|
232
|
+
* file's header). Read-only: no history write, no scheduler touch, no
|
|
233
|
+
* execution spawn.
|
|
234
|
+
*/
|
|
235
|
+
export async function akmTaskExplain(ref, options = {}) {
|
|
236
|
+
const parsedRef = parseTaskRef(ref);
|
|
237
|
+
const bundle = resolveTaskReadBundle(parsedRef.bundle, options.target);
|
|
238
|
+
const adapterId = bundle.source.adapterId ?? detectAdapterId(bundle.source.path);
|
|
239
|
+
const id = taskIdForAdapter(parsedRef.id, adapterId);
|
|
240
|
+
if (adapterId === "akm-task")
|
|
241
|
+
validateTaskConceptId(id);
|
|
242
|
+
else
|
|
243
|
+
validateTaskId(id);
|
|
244
|
+
const taskConceptId = adapterId === "akm" ? `tasks/${id}` : id;
|
|
245
|
+
const owner = resolveAdapterConceptOwner(bundle.source.path, adapterId, taskConceptId);
|
|
246
|
+
if (!owner) {
|
|
247
|
+
throw new NotFoundError(`Task ${JSON.stringify(ref)} was not found in the configured ${JSON.stringify(adapterId)} component.`, "ASSET_NOT_FOUND");
|
|
248
|
+
}
|
|
249
|
+
const sourcePath = owner.path;
|
|
250
|
+
const yaml = fs.readFileSync(sourcePath, "utf8");
|
|
251
|
+
const parsed = parseTaskSource({ yaml, filePath: sourcePath, workspaceRoot: bundle.source.path });
|
|
252
|
+
const inputContract = parsed.v4.inputs ?? {};
|
|
253
|
+
// Code-review finding (explain.ts:299, B-N4): `akm task run`'s
|
|
254
|
+
// load-task.ts copies this same materialize -> applyInputDefaults ->
|
|
255
|
+
// validateInputs ladder, but its own trailing `validateInputs` +
|
|
256
|
+
// `contractViolation` throw is load-task's OWN enforcement that a task
|
|
257
|
+
// about to actually EXECUTE never dispatches with a required input still
|
|
258
|
+
// unmet. `explain` never dispatches anything (this file's header) — it is
|
|
259
|
+
// read-only introspection, and a task's declared-but-unsupplied required
|
|
260
|
+
// input is exactly the fact `explain` exists to surface, not a condition
|
|
261
|
+
// that should make the command refuse to print. Deliberately NOT calling
|
|
262
|
+
// `validateInputs` here: `materializeInputFlags` above still runs its own
|
|
263
|
+
// per-flag validation (an unknown flag name still fails UNKNOWN_FLAG,
|
|
264
|
+
// B-55; a supplied value failing its own declared schema still fails
|
|
265
|
+
// here), so only the "required and never supplied at all" case is left
|
|
266
|
+
// unenforced. `buildSuppliedInputs` below naturally renders that case as a
|
|
267
|
+
// declaration row (always present, carrying `required: true`) with no
|
|
268
|
+
// corresponding `suppliedInputs` entry, since `defaultedInputs` has no key
|
|
269
|
+
// for an input with neither a default nor a supplied value.
|
|
270
|
+
const materializedInputs = materializeInputFlags(inputContract, options.inputFlags ?? [], TASK_INPUT_DIAGNOSTICS);
|
|
271
|
+
const defaultedInputs = applyInputDefaults(inputContract, materializedInputs);
|
|
272
|
+
const document = projectTaskSourceV4(parsed.v4);
|
|
273
|
+
const schedule = parsed.v4.schedule.map((entry) => ({
|
|
274
|
+
ordinal: entry.ordinal,
|
|
275
|
+
cron: entry.cron,
|
|
276
|
+
enabled: entry.enabled,
|
|
277
|
+
source: entry.source,
|
|
278
|
+
inputs: Object.fromEntries(Object.entries(entry.inputs).map(([name, value]) => {
|
|
279
|
+
const redaction = redactIfSecretShaped(name, value);
|
|
280
|
+
return [
|
|
281
|
+
name,
|
|
282
|
+
{
|
|
283
|
+
value: redaction.value,
|
|
284
|
+
provenance: "schedule-binding",
|
|
285
|
+
...(redaction.redacted ? { redacted: true } : {}),
|
|
286
|
+
},
|
|
287
|
+
];
|
|
288
|
+
})),
|
|
289
|
+
}));
|
|
290
|
+
return {
|
|
291
|
+
ref,
|
|
292
|
+
taskId: id,
|
|
293
|
+
bundleName: bundle.source.name,
|
|
294
|
+
sourcePath,
|
|
295
|
+
sourceVersion: parsed.version,
|
|
296
|
+
...(parsed.v4.name !== undefined ? { name: parsed.v4.name } : {}),
|
|
297
|
+
...(parsed.v4.description !== undefined ? { description: parsed.v4.description } : {}),
|
|
298
|
+
target: targetSection(document),
|
|
299
|
+
inputDeclarations: buildInputDeclarations(inputContract),
|
|
300
|
+
suppliedInputs: buildSuppliedInputs(defaultedInputs, materializedInputs),
|
|
301
|
+
execution: resolveExecutionSettings(document),
|
|
302
|
+
schedule,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -29,6 +29,8 @@ import { getParsedInvocation } from "../../cli/invocation.js";
|
|
|
29
29
|
import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
30
30
|
import { defineGroupCommand, defineJsonCommand, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
31
31
|
import { UsageError } from "../../core/errors.js";
|
|
32
|
+
import { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS } from "../../tasks/task-run-reserved-flags.js";
|
|
33
|
+
import { akmTaskExplain } from "./explain.js";
|
|
32
34
|
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksRun, akmTasksSync } from "./tasks.js";
|
|
33
35
|
/** Shared `--bundle <bundle>` arg wired onto every task subcommand. */
|
|
34
36
|
const bundleArg = {
|
|
@@ -37,17 +39,163 @@ const bundleArg = {
|
|
|
37
39
|
description: "Bundle to operate on (defaults to the primary/default bundle)",
|
|
38
40
|
},
|
|
39
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* True when argv carries a `--<name>` flag in ANY spelling — bare,
|
|
44
|
+
* `--<name>=<value>` for any value, or a trailing `=` — stopping at a literal
|
|
45
|
+
* `--` separator exactly as `hasFlagIn` (`../../cli/invocation.ts`) does.
|
|
46
|
+
*
|
|
47
|
+
* `ParsedInvocation.hasFlag` cannot be used for the rejections below: it
|
|
48
|
+
* compares WHOLE tokens against `--<name>` and `--<name>=true` only, so every
|
|
49
|
+
* other value spelling (`--<name>=false`, `--<name>=1`, `--<name>=`) walks
|
|
50
|
+
* straight past it and is then absorbed downstream — by citty's non-strict
|
|
51
|
+
* parser, or by `parseTaskInputFlags`' reserved-name skip — exactly the
|
|
52
|
+
* silent-discard defect these rejecters exist to close (review round 1). The
|
|
53
|
+
* name is taken as everything before the FIRST `=`, which is
|
|
54
|
+
* `parseTaskInputFlags`' own split (see its `body.indexOf("=")` below), so the
|
|
55
|
+
* rejecter and the scanner can never disagree about what a token names.
|
|
56
|
+
*/
|
|
57
|
+
function hasFlagNamed(name) {
|
|
58
|
+
for (const token of getParsedInvocation().argv) {
|
|
59
|
+
if (token === "--")
|
|
60
|
+
return false;
|
|
61
|
+
if (!token.startsWith("--"))
|
|
62
|
+
continue;
|
|
63
|
+
const body = token.slice(2);
|
|
64
|
+
const equalsAt = body.indexOf("=");
|
|
65
|
+
if ((equalsAt === -1 ? body : body.slice(0, equalsAt)) === name)
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
40
70
|
/**
|
|
41
71
|
* `--target` was renamed to `--bundle` on `task` in 0.9 (S8.4). citty is
|
|
42
72
|
* non-strict, so the retired spelling is silently absorbed rather than
|
|
43
73
|
* rejected — reject it explicitly instead (mirrors improve-cli.ts /
|
|
44
|
-
* remember-cli.ts).
|
|
74
|
+
* remember-cli.ts). The generic pre-dispatch gate cannot catch it either: it
|
|
75
|
+
* exempts `target` on every `task` subcommand precisely so this handler can
|
|
76
|
+
* answer with the rename (`../../cli/unknown-flags`'s `SELF_DIAGNOSED_FLAGS`),
|
|
77
|
+
* and that exemption is keyed on the flag NAME — so `--target=team` must be
|
|
78
|
+
* rejected here by name too, or nothing rejects it at all.
|
|
79
|
+
*
|
|
80
|
+
* Rejecting by NAME means `--target=<value>` can no longer carry a declared
|
|
81
|
+
* task input named `target` either (0.9.2 review round 2). That is settled on
|
|
82
|
+
* the DECLARATION side, not by narrowing this rejecter back to whole-token
|
|
83
|
+
* matching: `target` is listed in `TASK_RUN_SELF_DIAGNOSED_FLAGS`
|
|
84
|
+
* (`../../tasks/task-run-reserved-flags.ts`), so `parseInputDeclarations`
|
|
85
|
+
* refuses `inputs: {target: …}` with TASK_SOURCE_INVALID at authoring time and
|
|
86
|
+
* no task can reach `akm task run` needing the flag this throws on. Do not
|
|
87
|
+
* re-narrow the match here — the silently-ignored `--target=team` that round 1
|
|
88
|
+
* closed would come straight back.
|
|
45
89
|
*/
|
|
46
90
|
function rejectRetiredTaskTargetFlag() {
|
|
47
|
-
if (!
|
|
91
|
+
if (!hasFlagNamed("target"))
|
|
48
92
|
return;
|
|
49
93
|
throw new UsageError("`akm task --target` was renamed to `--bundle` in 0.9. Use `--bundle <name>` instead.", "INVALID_FLAG_VALUE");
|
|
50
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* `--scheduled` is `akm task run`'s own declared flag (an internal marker for
|
|
97
|
+
* scheduler-generated runs) — `task explain` declares no such flag. Because
|
|
98
|
+
* `explain` reuses `parseTaskInputFlags` (the same exact-name scanner `task
|
|
99
|
+
* run` uses, see that function's docstring below) to capture input flags, and
|
|
100
|
+
* `scheduled` is one of that scanner's reserved boolean-flag names
|
|
101
|
+
* (`TASK_RUN_BOOLEAN_FLAG_SET`, from `../../tasks/task-run-reserved-flags`),
|
|
102
|
+
* the scanner silently skips over `--scheduled` rather than ever surfacing it
|
|
103
|
+
* as an input flag — so it reached neither `materializeInputFlags`' own
|
|
104
|
+
* unknown-flag diagnostic nor the generic pre-dispatch flag gate (which
|
|
105
|
+
* exempts `task explain`'s whole dynamic namespace, `../../cli/unknown-flags`
|
|
106
|
+
* §`dynamicNamedFlagCommands`). The net effect: `akm task explain <ref>
|
|
107
|
+
* --scheduled` silently accepted and discarded the flag instead of rejecting
|
|
108
|
+
* it (finding F7) — `scheduled` can never be a declared input name either
|
|
109
|
+
* (same reserved-name module), so this can never reject a flag that was ever
|
|
110
|
+
* a valid input binding. Reject it explicitly, before the shared scanner ever
|
|
111
|
+
* sees it — same UNKNOWN_FLAG diagnostic family the generic gate uses.
|
|
112
|
+
*
|
|
113
|
+
* Rejection is keyed on the flag NAME (`hasFlagNamed` above), not on a literal
|
|
114
|
+
* token: `parseTaskInputFlags` splits on the first `=` BEFORE its reserved-name
|
|
115
|
+
* check, so `--scheduled=false` and `--scheduled=1` are swallowed by that skip
|
|
116
|
+
* just as the bare token is. A whole-token test would leave every spelling but
|
|
117
|
+
* `--scheduled` / `--scheduled=true` in the hole this exists to close.
|
|
118
|
+
*/
|
|
119
|
+
function rejectExplainScheduledFlag() {
|
|
120
|
+
if (!hasFlagNamed("scheduled"))
|
|
121
|
+
return;
|
|
122
|
+
throw new UsageError('Unknown flag "--scheduled".', "UNKNOWN_FLAG");
|
|
123
|
+
}
|
|
124
|
+
// ── `akm task run` input flags — Stage 1: capture (spec §5.1) ──────────────
|
|
125
|
+
//
|
|
126
|
+
// Mirrors `parseWorkflowParameterFlags` (src/commands/workflow-cli.ts:232-289)
|
|
127
|
+
// exactly: the CLI carries RAW string/boolean flag values to the boundary
|
|
128
|
+
// that knows the task's declared contract (Stage 2, src/tasks/run/load-task.ts)
|
|
129
|
+
// — coercion happens once, there. `akm task run`'s own declared flags
|
|
130
|
+
// (GLOBAL_OUTPUT_ARGS, --bundle, --scheduled) are excluded so they are never
|
|
131
|
+
// mistaken for inputs (B-33); `--target` is excluded too, but only because
|
|
132
|
+
// `rejectRetiredTaskTargetFlag()` above always runs first and throws before
|
|
133
|
+
// this is ever reached (B-32) — it is not itself special-cased below.
|
|
134
|
+
// `TASK_RUN_VALUE_FLAGS` / `TASK_RUN_BOOLEAN_FLAGS` are re-exported here
|
|
135
|
+
// (unchanged in name, location, and value) from a dependency-free leaf module
|
|
136
|
+
// so `src/tasks/source/task-source-v4.ts` can reject a declared `inputs:`
|
|
137
|
+
// name that collides with one of them without importing this CLI file —
|
|
138
|
+
// which would cycle back through `./tasks` -> `../../tasks/source/*` into the
|
|
139
|
+
// parser (code-review finding, docs/plans/specs/p2b-input-bindings.md review
|
|
140
|
+
// round 2; see `../../tasks/task-run-reserved-flags.ts`'s own header).
|
|
141
|
+
export { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS };
|
|
142
|
+
const TASK_RUN_VALUE_FLAG_SET = new Set(TASK_RUN_VALUE_FLAGS);
|
|
143
|
+
const TASK_RUN_BOOLEAN_FLAG_SET = new Set(TASK_RUN_BOOLEAN_FLAGS);
|
|
144
|
+
/**
|
|
145
|
+
* Scan `akm task run`'s raw argv for exact-name input flags, excluding the
|
|
146
|
+
* task id and every declared flag above. Input flags must come after the
|
|
147
|
+
* task id (mirrors `parseWorkflowParameterFlags`'s positional rule); a bare
|
|
148
|
+
* `--` is rejected, matching `workflow run`.
|
|
149
|
+
*/
|
|
150
|
+
export function parseTaskInputFlags(rawArgs, id) {
|
|
151
|
+
const flags = [];
|
|
152
|
+
let idSeen = false;
|
|
153
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
154
|
+
const token = rawArgs[index];
|
|
155
|
+
if (token === "--") {
|
|
156
|
+
throw new UsageError("`akm task run` does not accept positional arguments after `--`.", "INVALID_FLAG_VALUE");
|
|
157
|
+
}
|
|
158
|
+
if (!token.startsWith("-") || token === "-" || /^-\d/.test(token)) {
|
|
159
|
+
if (!idSeen) {
|
|
160
|
+
if (token !== id) {
|
|
161
|
+
throw new UsageError("Task input flags must come after the task id.", "INVALID_FLAG_VALUE");
|
|
162
|
+
}
|
|
163
|
+
idSeen = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
throw new UsageError(`Unexpected positional task argument "${token}".`, "INVALID_FLAG_VALUE");
|
|
167
|
+
}
|
|
168
|
+
if (!token.startsWith("--"))
|
|
169
|
+
continue;
|
|
170
|
+
const body = token.slice(2);
|
|
171
|
+
const equalsAt = body.indexOf("=");
|
|
172
|
+
const name = equalsAt === -1 ? body : body.slice(0, equalsAt);
|
|
173
|
+
const inlineValue = equalsAt === -1 ? undefined : body.slice(equalsAt + 1);
|
|
174
|
+
if (TASK_RUN_VALUE_FLAG_SET.has(name)) {
|
|
175
|
+
if (inlineValue === undefined)
|
|
176
|
+
index += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (TASK_RUN_BOOLEAN_FLAG_SET.has(name))
|
|
180
|
+
continue;
|
|
181
|
+
if (!idSeen) {
|
|
182
|
+
throw new UsageError("Task input flags must come after the task id.", "INVALID_FLAG_VALUE");
|
|
183
|
+
}
|
|
184
|
+
if (inlineValue !== undefined) {
|
|
185
|
+
flags.push({ name, value: inlineValue });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const next = rawArgs[index + 1];
|
|
189
|
+
if (next !== undefined && (!next.startsWith("-") || /^-\d/.test(next))) {
|
|
190
|
+
flags.push({ name, value: next });
|
|
191
|
+
index += 1;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
flags.push({ name, value: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return flags;
|
|
198
|
+
}
|
|
51
199
|
const tasksAddCommand = defineJsonCommand({
|
|
52
200
|
meta: { name: "add", description: "Register a new scheduled task and install it in the OS scheduler" },
|
|
53
201
|
args: {
|
|
@@ -121,12 +269,14 @@ const tasksRunCommand = defineCommand({
|
|
|
121
269
|
...bundleArg,
|
|
122
270
|
scheduled: { type: "boolean", description: "Internal marker for scheduler-generated runs", default: false },
|
|
123
271
|
},
|
|
124
|
-
async run({ args }) {
|
|
272
|
+
async run({ args, rawArgs }) {
|
|
125
273
|
await runWithJsonErrors(async () => {
|
|
126
274
|
rejectRetiredTaskTargetFlag();
|
|
275
|
+
const inputFlags = parseTaskInputFlags(rawArgs, args.id);
|
|
127
276
|
const envelope = await akmTasksRun(args.id, {
|
|
128
277
|
scheduled: args.scheduled === true,
|
|
129
278
|
...(args.bundle !== undefined ? { target: args.bundle } : {}),
|
|
279
|
+
inputFlags,
|
|
130
280
|
});
|
|
131
281
|
output("task-run", envelope);
|
|
132
282
|
// F4: was `process.exit(envelope.exitCode)`, terminating synchronously
|
|
@@ -172,6 +322,37 @@ const tasksSyncCommand = defineJsonCommand({
|
|
|
172
322
|
output("task-sync", result);
|
|
173
323
|
},
|
|
174
324
|
});
|
|
325
|
+
// ── `akm task explain` — read-only introspection (P2b Lane B, spec
|
|
326
|
+
// docs/plans/specs/p2b-input-bindings.md §4.5, §1.7 B-N4) ──────────────────
|
|
327
|
+
const tasksExplainCommand = defineJsonCommand({
|
|
328
|
+
meta: {
|
|
329
|
+
name: "explain",
|
|
330
|
+
description: "Print a task's source, declared inputs (with provenance), resolved target, execution settings, and " +
|
|
331
|
+
"schedule bindings — read-only and secret-free; never spawns anything",
|
|
332
|
+
},
|
|
333
|
+
args: {
|
|
334
|
+
ref: { type: "positional", description: "Task ref or id", required: true },
|
|
335
|
+
...bundleArg,
|
|
336
|
+
},
|
|
337
|
+
async run({ args, rawArgs }) {
|
|
338
|
+
rejectRetiredTaskTargetFlag();
|
|
339
|
+
// `--scheduled` must be rejected BEFORE the shared scanner below ever
|
|
340
|
+
// sees it — the scanner treats it as `task run`'s own reserved flag
|
|
341
|
+
// (silently skipped, never surfaced as unknown) rather than explain's,
|
|
342
|
+
// since it has no way to know which command called it (F7 fix).
|
|
343
|
+
rejectExplainScheduledFlag();
|
|
344
|
+
// Stage 1 (capture): the SAME exact-name flag scanner `akm task run`
|
|
345
|
+
// uses (`parseTaskInputFlags` above) — `explain` never declares
|
|
346
|
+
// `--scheduled`, but reusing the identical scanner is deliberate: one
|
|
347
|
+
// implementation of "which argv tokens are task input flags", not two.
|
|
348
|
+
const inputFlags = parseTaskInputFlags(rawArgs, args.ref);
|
|
349
|
+
const result = await akmTaskExplain(args.ref, {
|
|
350
|
+
...(args.bundle !== undefined ? { target: args.bundle } : {}),
|
|
351
|
+
inputFlags,
|
|
352
|
+
});
|
|
353
|
+
output("task-explain", result);
|
|
354
|
+
},
|
|
355
|
+
});
|
|
175
356
|
const tasksDoctorCommand = defineJsonCommand({
|
|
176
357
|
meta: {
|
|
177
358
|
name: "doctor",
|
|
@@ -191,6 +372,7 @@ export const taskCommand = defineGroupCommand({
|
|
|
191
372
|
subCommands: {
|
|
192
373
|
add: tasksAddCommand,
|
|
193
374
|
run: tasksRunCommand,
|
|
375
|
+
explain: tasksExplainCommand,
|
|
194
376
|
history: tasksHistoryCommand,
|
|
195
377
|
sync: tasksSyncCommand,
|
|
196
378
|
doctor: tasksDoctorCommand,
|