akm-cli 0.9.2-alpha.4 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +493 -0
  2. package/STABILITY.md +23 -5
  3. package/dist/assets/hints/cli-hints-full.md +12 -7
  4. package/dist/assets/tasks/core/extract.yml +3 -5
  5. package/dist/assets/tasks/core/improve.yml +3 -5
  6. package/dist/assets/tasks/core/index-refresh.yml +3 -5
  7. package/dist/assets/tasks/core/sync.yml +3 -5
  8. package/dist/assets/tasks/core/version-check.yml +3 -5
  9. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
  10. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
  12. package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
  13. package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
  14. package/dist/cli/unknown-flags.js +12 -1
  15. package/dist/cli.js +8 -1
  16. package/dist/commands/command/command-execution.js +23 -2
  17. package/dist/commands/health/improve-metrics.js +38 -0
  18. package/dist/commands/health/windows.js +8 -4
  19. package/dist/commands/health.js +8 -4
  20. package/dist/commands/lint/index.js +1 -1
  21. package/dist/commands/migrate-cli.js +130 -24
  22. package/dist/commands/proposal/validators/proposal-validators.js +7 -2
  23. package/dist/commands/tasks/explain.js +304 -0
  24. package/dist/commands/tasks/tasks-cli.js +185 -3
  25. package/dist/commands/tasks/tasks.js +265 -52
  26. package/dist/commands/workflow/plan.js +159 -0
  27. package/dist/commands/workflow-cli.js +94 -2
  28. package/dist/core/activation-policy.js +2 -12
  29. package/dist/core/adapter/adapters/akm-lint.js +7 -4
  30. package/dist/core/adapter/adapters/akm-metadata.js +26 -14
  31. package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
  32. package/dist/core/errors.js +45 -0
  33. package/dist/core/json-schema.js +15 -5
  34. package/dist/core/state/migrations.js +57 -0
  35. package/dist/core/state-db.js +16 -14
  36. package/dist/core/subprocess.js +47 -13
  37. package/dist/execution/guarded-source.js +44 -0
  38. package/dist/execution/input-contract.js +250 -0
  39. package/dist/execution/target-ref.js +63 -0
  40. package/dist/indexer/usage/usage-events.js +14 -3
  41. package/dist/integrations/agent/execution-lowering.js +12 -1
  42. package/dist/output/shapes/passthrough.js +2 -0
  43. package/dist/output/text/helpers.js +1 -1
  44. package/dist/output/text/migrate.js +12 -3
  45. package/dist/output/text/workflow-format.js +192 -10
  46. package/dist/output/text/workflow.js +2 -1
  47. package/dist/runtime.js +1 -0
  48. package/dist/scripts/akm-migrate-node.js +11838 -10118
  49. package/dist/scripts/akm-migrate.js +11828 -10117
  50. package/dist/setup/steps/tasks.js +34 -17
  51. package/dist/storage/repositories/task-history-repository.js +5 -1
  52. package/dist/storage/repositories/workflow-runs-repository.js +144 -6
  53. package/dist/tasks/backends/launchd.js +31 -84
  54. package/dist/tasks/embedded.js +13 -7
  55. package/dist/tasks/model/invocation.js +4 -0
  56. package/dist/tasks/prepare/prepare-script-target.js +9 -0
  57. package/dist/tasks/prepare/prepare-support.js +154 -0
  58. package/dist/tasks/prepare/prepare.js +117 -0
  59. package/dist/tasks/prepare/prepared-execution.js +4 -0
  60. package/dist/tasks/prepare/script-capture.js +80 -0
  61. package/dist/tasks/run/attempt-lifecycle.js +165 -0
  62. package/dist/tasks/run/load-task.js +117 -0
  63. package/dist/tasks/run/provenance.js +20 -0
  64. package/dist/tasks/run/run-command-task.js +92 -0
  65. package/dist/tasks/run/run-native-task.js +222 -0
  66. package/dist/tasks/run/run-task.js +99 -0
  67. package/dist/tasks/run/run-workflow-task.js +222 -0
  68. package/dist/tasks/run/task-history.js +134 -0
  69. package/dist/tasks/run/task-log.js +179 -0
  70. package/dist/tasks/run/task-result.js +19 -0
  71. package/dist/tasks/scheduler-binding.js +66 -2
  72. package/dist/tasks/scheduler-invocation.js +63 -3
  73. package/dist/tasks/scheduler-sync.js +77 -14
  74. package/dist/tasks/source/bounded-document.js +455 -0
  75. package/dist/tasks/source/parse-task-source.js +59 -0
  76. package/dist/tasks/source/project-v4.js +62 -0
  77. package/dist/tasks/source/task-input-diagnostics.js +36 -0
  78. package/dist/tasks/source/task-source-v4.js +626 -0
  79. package/dist/tasks/source-v3.js +10 -733
  80. package/dist/tasks/task-run-reserved-flags.js +79 -0
  81. package/dist/workflows/authoring/authoring.js +17 -8
  82. package/dist/workflows/exec/child-invocation.js +34 -0
  83. package/dist/workflows/exec/child-workflow.js +370 -0
  84. package/dist/workflows/exec/exec-unit.js +50 -170
  85. package/dist/workflows/exec/frozen-judge.js +19 -2
  86. package/dist/workflows/exec/native-executor.js +49 -27
  87. package/dist/workflows/exec/param-secrets.js +12 -0
  88. package/dist/workflows/exec/run-workflow.js +48 -59
  89. package/dist/workflows/exec/step-work.js +222 -80
  90. package/dist/workflows/exec/unit-dispatch.js +72 -0
  91. package/dist/workflows/freeze/child-output-references.js +94 -0
  92. package/dist/workflows/freeze/environment.js +174 -0
  93. package/dist/workflows/freeze/identity.js +22 -0
  94. package/dist/workflows/freeze/resolve-steps.js +78 -0
  95. package/dist/workflows/freeze/source-freeze.js +57 -0
  96. package/dist/workflows/freeze/step-values.js +68 -0
  97. package/dist/workflows/freeze/targets/child-workflow.js +206 -0
  98. package/dist/workflows/freeze/targets/command.js +81 -0
  99. package/dist/workflows/freeze/targets/script.js +57 -0
  100. package/dist/workflows/freeze/targets/shell.js +31 -0
  101. package/dist/workflows/freeze/targets/task.js +179 -0
  102. package/dist/workflows/freeze/task-bindings.js +180 -0
  103. package/dist/workflows/ir/compile.js +59 -11
  104. package/dist/workflows/ir/environment-v4.js +3 -3
  105. package/dist/workflows/ir/freeze-v4.js +41 -7
  106. package/dist/workflows/ir/params.js +58 -131
  107. package/dist/workflows/ir/plan-hash.js +3 -3
  108. package/dist/workflows/ir/schema-v4.js +246 -17
  109. package/dist/workflows/parser.js +74 -2
  110. package/dist/workflows/program/schema.js +5 -2
  111. package/dist/workflows/resource-limits.js +20 -0
  112. package/dist/workflows/runtime/plan-classifier.js +24 -7
  113. package/dist/workflows/runtime/run-outputs.js +103 -0
  114. package/dist/workflows/runtime/runs.js +114 -9
  115. package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
  116. package/dist/workflows/source-files.js +5 -5
  117. package/dist/workflows/source-ir/compare.js +17 -0
  118. package/dist/workflows/source-ir/compile.js +7 -3
  119. package/dist/workflows/source-ir/github-yaml.js +64 -17
  120. package/dist/workflows/source-ir/schema.js +69 -21
  121. package/dist/workflows/source-ir/semantics.js +7 -25
  122. package/dist/workflows/source-ir/triggers.js +79 -0
  123. package/dist/workflows/source-ir/uses.js +33 -7
  124. package/docs/migration/README.md +1 -1
  125. package/docs/migration/release-notes/0.9.2.md +87 -11
  126. package/docs/migration/release-notes/README.md +3 -2
  127. package/docs/migration/v0.8-to-v0.9.md +13 -11
  128. package/docs/migration/v0.9.0-troubleshooting.md +20 -13
  129. package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
  130. package/docs/reference/README.md +1 -1
  131. package/docs/reference/cli.md +140 -46
  132. package/docs/reference/configuration.md +6 -5
  133. package/docs/reference/supported-formats.md +9 -5
  134. package/docs/reference/tasks.md +338 -75
  135. package/docs/reference/workflow-schema.md +290 -16
  136. package/docs/reference/workflows.md +57 -7
  137. package/package.json +1 -1
  138. package/schemas/akm-task.json +173 -118
  139. package/schemas/akm-workflow.json +28 -0
  140. package/dist/tasks/runner.js +0 -941
  141. package/dist/tasks/runtime-v3.js +0 -281
  142. package/dist/workflows/ir/source-freeze-v4.js +0 -506
  143. package/dist/workflows/source-ir/ordering.js +0 -38
@@ -0,0 +1,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 (!getParsedInvocation().hasFlag("--target"))
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,