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
@@ -8,8 +8,9 @@
8
8
  * CLI spawn wrapper so non-agent subprocess callers (task commands, setup
9
9
  * probes/installers) get the same guarantees the agent path already had:
10
10
  *
11
- * • Process-GROUP spawn (`detached: true` when capturing) so a negative-pid
12
- * kill reaps the whole descendant tree — no orphaned children.
11
+ * • Process-GROUP spawn (`detached: true` when capturing ON POSIX) so a
12
+ * negative-pid kill reaps the whole descendant tree — no orphaned children.
13
+ * See {@link spawnsOwnProcessGroup} for why Windows is excluded.
13
14
  * • A SIGTERM→SIGKILL kill ladder on timeout/abort — a child that ignores
14
15
  * SIGTERM is force-killed after a grace period instead of wedging forever.
15
16
  * • Time-bounded output capture ({@link readStream}) that cannot block past
@@ -33,6 +34,49 @@ import { spawn as runtimeSpawn } from "../runtime.js";
33
34
  * reaped alongside the node wrapper. The fallback keeps test fakes working
34
35
  * without modification.
35
36
  */
37
+ /**
38
+ * Whether a captured spawn should ask for its own process group.
39
+ *
40
+ * POSIX: yes. `detached` is `setsid()`, which is what makes {@link killGroup}'s
41
+ * `process.kill(-pid, …)` reap the whole descendant tree. It does not touch the
42
+ * child's stdio.
43
+ *
44
+ * Windows: no — the same flag means something unrelated and actively harmful.
45
+ * There it maps to `DETACHED_PROCESS`, so the child is created WITHOUT A
46
+ * CONSOLE; a console host started that way (powershell.exe, cmd.exe) allocates
47
+ * its own console during startup, which REPLACES the std handles it was handed,
48
+ * and everything it writes goes to that phantom console instead of our pipe.
49
+ * The command still runs and its exit code still propagates — the output just
50
+ * vanishes, which is exactly how it failed: a scheduled `akm --version` logged
51
+ * `exit_code=0` with both captured streams empty. Windows gains nothing in
52
+ * return, because `process.kill(-pid, …)` is a POSIX process-group call that
53
+ * throws there; {@link killGroup} already falls back to `proc.kill(signal)`.
54
+ *
55
+ * Exported for direct unit testing with an explicit platform.
56
+ */
57
+ export function spawnsOwnProcessGroup(platform = process.platform) {
58
+ return platform !== "win32";
59
+ }
60
+ /**
61
+ * The exact options one managed run hands its spawn — pure, so the platform
62
+ * dependency above is testable on any host rather than only on the platform
63
+ * that would break.
64
+ */
65
+ export function buildSpawnOptions(opts, platform = process.platform) {
66
+ const capture = opts.capture;
67
+ return {
68
+ stdin: capture ? (opts.stdin !== undefined ? "pipe" : "ignore") : "inherit",
69
+ stdout: capture ? "pipe" : "inherit",
70
+ stderr: capture ? "pipe" : "inherit",
71
+ ...(opts.env ? { env: opts.env } : {}),
72
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
73
+ // Only in captured mode — interactive mode inherits the parent terminal's
74
+ // process group intentionally — and only where the flag means a process
75
+ // group at all (see spawnsOwnProcessGroup).
76
+ ...(capture && spawnsOwnProcessGroup(platform) ? { detached: true } : {}),
77
+ ...(opts.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
78
+ };
79
+ }
36
80
  export function killGroup(proc, signal) {
37
81
  if (typeof proc.pid === "number") {
38
82
  try {
@@ -296,17 +340,7 @@ export async function runManagedSubprocess(cmd, opts) {
296
340
  }
297
341
  let proc;
298
342
  try {
299
- proc = spawnFn(cmd, {
300
- stdin: capture ? (opts.stdin !== undefined ? "pipe" : "ignore") : "inherit",
301
- stdout: capture ? "pipe" : "inherit",
302
- stderr: capture ? "pipe" : "inherit",
303
- ...(opts.env ? { env: opts.env } : {}),
304
- ...(opts.cwd ? { cwd: opts.cwd } : {}),
305
- // Spawn in its own process group so killGroup(-pid, signal) reaches all
306
- // descendants. Only in captured mode — interactive mode inherits the
307
- // parent terminal's process group intentionally.
308
- ...(capture ? { detached: true } : {}),
309
- });
343
+ proc = spawnFn(cmd, buildSpawnOptions(opts));
310
344
  }
311
345
  catch (err) {
312
346
  return {
@@ -352,6 +352,50 @@ export class GuardedExecutionSourceCollector {
352
352
  directoryManifests: Object.freeze([...this.#directories.values()].sort((left, right) => compareCodePoints(directorySortKey(left), directorySortKey(right)))),
353
353
  });
354
354
  }
355
+ /**
356
+ * Merge another collector's captured sources and directory manifests into
357
+ * this one (A-N7, spec docs/plans/specs/p3a-plan-v5-child-freeze.md §4.2
358
+ * step 6). The recursive child-workflow freeze gives each child its OWN
359
+ * fresh collector, so a child's plan (and therefore its `planHash`) is a
360
+ * pure function of its own source, independent of its position in the
361
+ * parent (A-N7's rejected alternative: sharing the parent's collector,
362
+ * which would make an identical child hash differently depending on what
363
+ * the parent had already captured). The parent then absorbs the child's
364
+ * records so its own final pre-publication CAS ({@link revalidate}) covers
365
+ * every child file too.
366
+ *
367
+ * Re-runs `#assertNoPhysicalOwnerAlias` for each newly-absorbed source
368
+ * record, so the shared-physical-owner authority holds ACROSS the
369
+ * composition, not just within one workflow's own freeze. A record whose
370
+ * key (resolved source path) is already present is required to be
371
+ * byte-identical to the one already held — two DISJOINT branches
372
+ * composing the SAME child (a diamond, not a cycle) absorb the same file
373
+ * twice with identical content and pass silently; two branches that
374
+ * somehow captured the same path with different content fail closed.
375
+ */
376
+ absorb(other) {
377
+ for (const [key, record] of other.#sources) {
378
+ const existing = this.#sources.get(key);
379
+ if (existing) {
380
+ if (!sameSnapshotValue(existing.source, record.source)) {
381
+ throw new UsageError(`${record.source.sourcePath} was captured with conflicting content across a composed workflow freeze.`, "RESOURCE_ALREADY_EXISTS");
382
+ }
383
+ continue;
384
+ }
385
+ this.#assertNoPhysicalOwnerAlias(record.source);
386
+ this.#sources.set(key, record);
387
+ }
388
+ for (const [key, manifest] of other.#directories) {
389
+ const existing = this.#directories.get(key);
390
+ if (existing) {
391
+ if (!sameSnapshotValue(existing, manifest)) {
392
+ throw new UsageError(`${manifest.directoryPath} changed between guarded directory reads across a composed workflow freeze.`, "RESOURCE_ALREADY_EXISTS");
393
+ }
394
+ continue;
395
+ }
396
+ this.#directories.set(key, manifest);
397
+ }
398
+ }
355
399
  revalidate() {
356
400
  for (const record of this.#sources.values()) {
357
401
  let current;
@@ -0,0 +1,250 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * The shared input contract, generalizing the pure module
6
+ * `src/workflows/ir/params.ts` (workflow parameters) into a contract-shaped
7
+ * vocabulary both workflow params AND task source v4's `inputs:`
8
+ * declarations consume — one validation/coercion implementation, injected
9
+ * per-caller diagnostics (D3-N3). Binding boundary: `src/execution/**` must
10
+ * never import `src/workflows/**` (D3-N1) — `INPUT_NAME_PATTERN` and the
11
+ * canonical-JSON helpers are therefore defined locally rather than imported
12
+ * from the workflow side (D3-N1/D3-N2), verified by
13
+ * `tests/execution/input-contract.test.ts`'s purity/byte-equality scans.
14
+ * Pure module: no IO, no engine imports.
15
+ *
16
+ * See docs/architecture/decisions/0004-task-input-contract-and-flag-coercion.md
17
+ * for the full generalization history and the D3-N1/D3-N2/D3-N3 rationale.
18
+ */
19
+ import { createHash } from "node:crypto";
20
+ import { validateJsonSchemaSubset } from "../core/json-schema.js";
21
+ /**
22
+ * Input/param names must be addressable as a plain identifier
23
+ * (`params.<ident>` / `inputs.<ident>`): a letter or underscore, then
24
+ * letters, digits, or underscores. Byte-identical source/flags to
25
+ * `src/workflows/program/schema.ts`'s `PROGRAM_PARAM_NAME_PATTERN`, which
26
+ * re-exports this constant (D3-N1) rather than defining its own copy.
27
+ */
28
+ export const INPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
29
+ /**
30
+ * Apply declared defaults on top of supplied values. A supplied value always
31
+ * wins, including an explicit falsy one (`false`, `0`, `""`) — PRESENCE, not
32
+ * truthiness, decides. A declared input with no `default` that is absent from
33
+ * `values` is left absent from the result (never filled with `undefined`); a
34
+ * value not named by the contract passes through unchanged — defaults are
35
+ * additive, not a filter. Returns a NEW object; `values` is never mutated.
36
+ */
37
+ export function applyInputDefaults(contract, values) {
38
+ const result = { ...values };
39
+ for (const [name, declaration] of Object.entries(contract)) {
40
+ if (Object.hasOwn(result, name))
41
+ continue;
42
+ if (Object.hasOwn(declaration, "default"))
43
+ result[name] = declaration.default;
44
+ }
45
+ return result;
46
+ }
47
+ /**
48
+ * Validate `values` against the contract's declared schemas plus its
49
+ * `required` flags. Builds the same synthetic `{type:"object",
50
+ * properties:<schemas>}` object schema the workflow param validator built,
51
+ * re-roots {@link validateJsonSchemaSubset}'s leading `$` at `pathRoot`
52
+ * (default `"$"`), and additionally appends one
53
+ * `"<pathRoot>.<name>: is required"` string per declared-required input
54
+ * absent from `values` (schema violations first, missing-required after). A
55
+ * name absent from the contract is never constrained. Returns `[]` when
56
+ * `values` fully satisfies the contract.
57
+ */
58
+ export function validateInputs(contract, values, options) {
59
+ const pathRoot = options?.pathRoot ?? "$";
60
+ const properties = {};
61
+ for (const [name, declaration] of Object.entries(contract))
62
+ properties[name] = declaration.schema;
63
+ const schemaErrors = Object.keys(properties).length === 0
64
+ ? []
65
+ : // redactValues: true — declared inputs can carry credentials, and this
66
+ // contract-violation text lands in stderr envelopes that get pasted into
67
+ // CI logs. See `coerceFlagValue`'s matching comment above.
68
+ validateJsonSchemaSubset(values, { type: "object", properties }, { redactValues: true }).map((error) => error.replace(/^\$/, pathRoot));
69
+ const missingRequired = [];
70
+ for (const [name, declaration] of Object.entries(contract)) {
71
+ if (declaration.required && !Object.hasOwn(values, name)) {
72
+ missingRequired.push(`${pathRoot}.${name}: is required`);
73
+ }
74
+ }
75
+ return [...schemaErrors, ...missingRequired];
76
+ }
77
+ /**
78
+ * Materialize exact-name CLI input flags against a contract. The CLI
79
+ * deliberately carries RAW string/boolean flag values to this one boundary
80
+ * so type coercion cannot race or drift from the declared contract:
81
+ * exact-name matching (a mistyped or undeclared flag name, or one that fails
82
+ * {@link INPUT_NAME_PATTERN}, is `diagnostics.unknownFlag` — the full
83
+ * declared set, sorted), array grouping (repeated flags on an array-declared
84
+ * input collect into an array in supplied order), the `[`-prefixed
85
+ * JSON-array shorthand, repeated-flag rejection on a non-array declaration
86
+ * (`diagnostics.duplicateNonArray`), and string-preserving coercion (a union
87
+ * type that permits `string` keeps the caller's exact text — `--version 001`
88
+ * on `type:"string"` stays the string `"001"`, never a silently-converted
89
+ * number, B-30). Ends by running {@link validateInputs} over the
90
+ * materialized result and raising `diagnostics.contractViolation` with its
91
+ * (still `"$"`-rooted) errors when non-empty.
92
+ *
93
+ * Returns `{}` immediately for zero flags, regardless of the contract, and
94
+ * calls no diagnostic — callers combine this with
95
+ * {@link applyInputDefaults} and a separate {@link validateInputs} call
96
+ * (after defaults are applied) to catch a `required` input that was never
97
+ * supplied as a flag at all.
98
+ */
99
+ export function materializeInputFlags(contract, flags, diagnostics) {
100
+ if (flags.length === 0)
101
+ return {};
102
+ const declared = new Set(Object.keys(contract));
103
+ const grouped = new Map();
104
+ for (const flag of flags) {
105
+ if (!INPUT_NAME_PATTERN.test(flag.name) || !declared.has(flag.name)) {
106
+ throw diagnostics.unknownFlag(flag.name, [...declared].sort());
107
+ }
108
+ const values = grouped.get(flag.name) ?? [];
109
+ values.push(flag.value);
110
+ grouped.set(flag.name, values);
111
+ }
112
+ const entries = [];
113
+ for (const [name, rawValues] of grouped) {
114
+ entries.push([name, materializeFlagValues(name, rawValues, contract[name]?.schema, diagnostics)]);
115
+ }
116
+ const values = Object.fromEntries(entries);
117
+ const errors = validateInputs(contract, values);
118
+ if (errors.length > 0)
119
+ throw diagnostics.contractViolation(errors);
120
+ return values;
121
+ }
122
+ function materializeFlagValues(name, values, schema, diagnostics) {
123
+ const types = schemaTypes(schema);
124
+ if (types.includes("array")) {
125
+ if (values.length === 1 && typeof values[0] === "string" && values[0].trim().startsWith("[")) {
126
+ const parsed = parseJsonFlag(name, values[0], diagnostics);
127
+ if (!Array.isArray(parsed))
128
+ throw diagnostics.invalidValue(name, "must be a JSON array");
129
+ return parsed;
130
+ }
131
+ // "array" is the sole declared type, or there is more than one supplied
132
+ // flag occurrence: a lone-array declaration always wraps its value(s),
133
+ // and repeated flags on an array-CAPABLE declaration always group,
134
+ // regardless of what else the type union permits. A union that ALSO
135
+ // permits a scalar type (e.g. `["array","string"]`) must not force a
136
+ // single, non-bracketed value into this branch, though — F1: without
137
+ // this guard, `--x hello` against `type:["array","string"]` silently
138
+ // became `["hello"]` instead of staying the string "hello", because the
139
+ // per-element map below coerces each value against `items` (or nothing)
140
+ // rather than trying the union's scalar alternatives first. Route that
141
+ // single-value case through `coerceFlagValue` with the FULL schema
142
+ // instead, so its string-preservation (B-30) and null/number/boolean/
143
+ // object arms run before "array" is ever assumed.
144
+ if (types.length === 1 || values.length > 1) {
145
+ const itemSchema = isRecord(schema?.items) ? schema.items : undefined;
146
+ return values.map((value) => coerceFlagValue(name, value, itemSchema, diagnostics));
147
+ }
148
+ return coerceFlagValue(name, values[0], schema, diagnostics);
149
+ }
150
+ if (values.length > 1)
151
+ throw diagnostics.duplicateNonArray(name);
152
+ return coerceFlagValue(name, values[0], schema, diagnostics);
153
+ }
154
+ function coerceFlagValue(name, raw, schema, diagnostics) {
155
+ const types = schemaTypes(schema);
156
+ if (types.length === 0)
157
+ return raw;
158
+ if (typeof raw === "boolean") {
159
+ if (types.includes("boolean"))
160
+ return raw;
161
+ if (types.includes("string"))
162
+ return String(raw);
163
+ throw diagnostics.invalidValue(name, `requires a value of type ${types.join(" | ")}`);
164
+ }
165
+ // A union that permits strings keeps the caller's exact text. This
166
+ // prevents a value such as "001" from being silently converted to a
167
+ // number (B-30).
168
+ if (types.includes("string"))
169
+ return raw;
170
+ for (const type of types) {
171
+ switch (type) {
172
+ case "boolean":
173
+ if (raw === "true")
174
+ return true;
175
+ if (raw === "false")
176
+ return false;
177
+ break;
178
+ case "number": {
179
+ const value = Number(raw);
180
+ if (raw.trim() !== "" && Number.isFinite(value))
181
+ return value;
182
+ break;
183
+ }
184
+ case "integer": {
185
+ const value = Number(raw);
186
+ if (raw.trim() !== "" && Number.isSafeInteger(value))
187
+ return value;
188
+ break;
189
+ }
190
+ case "null":
191
+ if (raw === "null")
192
+ return null;
193
+ break;
194
+ case "object": {
195
+ const parsed = parseJsonFlag(name, raw, diagnostics);
196
+ if (isRecord(parsed))
197
+ return parsed;
198
+ break;
199
+ }
200
+ case "array": {
201
+ const parsed = parseJsonFlag(name, raw, diagnostics);
202
+ if (Array.isArray(parsed))
203
+ return parsed;
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ // Never echo the supplied value: typed-input flags can carry credentials,
209
+ // and this detail lands in stderr envelopes that get pasted into CI logs.
210
+ throw diagnostics.invalidValue(name, `must be ${types.join(" | ")}`);
211
+ }
212
+ function schemaTypes(schema) {
213
+ const declared = schema?.type;
214
+ if (typeof declared === "string")
215
+ return [declared];
216
+ return Array.isArray(declared) ? declared.filter((value) => typeof value === "string") : [];
217
+ }
218
+ function parseJsonFlag(name, raw, diagnostics) {
219
+ try {
220
+ return JSON.parse(raw);
221
+ }
222
+ catch {
223
+ throw diagnostics.malformedJson(name);
224
+ }
225
+ }
226
+ function isRecord(value) {
227
+ return typeof value === "object" && value !== null && !Array.isArray(value);
228
+ }
229
+ /**
230
+ * Canonical JSON used for input hashing: object keys recursively sorted, so
231
+ * two structurally-equal input value sets hash identically regardless of key
232
+ * insertion order. Byte-equal to `canonicalJson`
233
+ * (`src/workflows/ir/plan-hash.ts`) — see D3-N2 above.
234
+ */
235
+ export function canonicalInputJson(value) {
236
+ return JSON.stringify(sortInputJsonKeys(value));
237
+ }
238
+ /** sha256 hex digest of {@link canonicalInputJson}'s output — a stable input-value fingerprint (B-39, for P2b execution identity). */
239
+ export function canonicalInputHash(value) {
240
+ return createHash("sha256").update(canonicalInputJson(value)).digest("hex");
241
+ }
242
+ function sortInputJsonKeys(value) {
243
+ if (value === null || typeof value !== "object")
244
+ return value;
245
+ if (Array.isArray(value))
246
+ return value.map((item) => sortInputJsonKeys(item));
247
+ return Object.fromEntries(Object.entries(value)
248
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
249
+ .map(([key, child]) => [key, sortInputJsonKeys(child)]));
250
+ }
@@ -0,0 +1,63 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * The target-ref classifier (P1a Lane B, docs/plans/specs/p1a-with-rejection-classifier.md §4.1).
6
+ *
7
+ * `classifyTargetRef` is the canonical classifier for a canonical asset ref
8
+ * used as an execution target: `commands/<name>`, `scripts/<name>`,
9
+ * `tasks/<name>`, or `workflows/<name>`, each optionally bundle-qualified
10
+ * (`<bundle>//commands/<name>`). It reuses the repo's one ref parser
11
+ * (`parseBundleRef` / `bundleRefToString`, src/core/asset/asset-ref.ts) and
12
+ * accepts a value only when all of the following hold:
13
+ *
14
+ * 1. `parseBundleRef(value)` does not throw;
15
+ * 2. the parsed ref carries no `#fragment`;
16
+ * 3. `bundleRefToString(parsed) === value` — the value round-trips, which
17
+ * rejects non-canonical spellings (`akm:commands/review`,
18
+ * `bad.bundle//commands/review`) even when they happen to parse;
19
+ * 4. the concept id contains a `/`, and the family segment before the
20
+ * first `/` is one of `commands`, `scripts`, `tasks`, `workflows`;
21
+ * 5. the name segment after the first `/` is non-empty.
22
+ *
23
+ * Explicit non-goals (binding, spec §4.1): no GitHub locator grammar, no
24
+ * `akm/command` builtin special case, no resolution, no filesystem access, no
25
+ * guessing. Callers layer builtin detection on top (see
26
+ * `classifyWorkflowSourceUses` in src/workflows/source-ir/uses.ts).
27
+ */
28
+ import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
29
+ import { UsageError } from "../core/errors.js";
30
+ const FAMILY_KIND = {
31
+ commands: "command",
32
+ scripts: "script",
33
+ tasks: "task",
34
+ workflows: "workflow",
35
+ };
36
+ function targetRefInvalid(value) {
37
+ return new UsageError(`Target ref ${JSON.stringify(value)} must be a canonical commands/, scripts/, tasks/, or workflows/ asset ref.`, "TARGET_REF_INVALID");
38
+ }
39
+ /** Classify one exact canonical asset ref as an execution target. Never resolves or guesses. */
40
+ export function classifyTargetRef(value) {
41
+ let parsed;
42
+ try {
43
+ parsed = parseBundleRef(value);
44
+ }
45
+ catch {
46
+ throw targetRefInvalid(value);
47
+ }
48
+ if (parsed.fragment !== undefined)
49
+ throw targetRefInvalid(value);
50
+ if (bundleRefToString(parsed) !== value)
51
+ throw targetRefInvalid(value);
52
+ const slash = parsed.conceptId.indexOf("/");
53
+ if (slash < 0)
54
+ throw targetRefInvalid(value);
55
+ const family = parsed.conceptId.slice(0, slash);
56
+ const name = parsed.conceptId.slice(slash + 1);
57
+ if (name.length === 0)
58
+ throw targetRefInvalid(value);
59
+ const kind = FAMILY_KIND[family];
60
+ if (kind === undefined)
61
+ throw targetRefInvalid(value);
62
+ return Object.freeze({ kind, ref: value });
63
+ }
@@ -2,11 +2,22 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  const USAGE_EVENT_SOURCES = new Set(["user", "improve", "task", "audit", "unknown"]);
5
- /** Resolve subprocess provenance without treating an invalid value as user demand. */
6
- export function resolveUsageEventSource(env = process.env) {
5
+ /**
6
+ * Resolve subprocess provenance without treating an invalid value as user
7
+ * demand.
8
+ *
9
+ * `fallback` (spec docs/plans/specs/p1b-model-extraction.md §5.2, F-1) is what
10
+ * an unset/empty ambient value resolves to — it defaults to `"user"`, which
11
+ * reproduces every pre-P1b call site byte-for-byte (P-07). A caller that
12
+ * already knows the invocation's provenance (the task runner, threading its
13
+ * `ExecutionProvenanceContext`) passes its resolved value as the fallback
14
+ * instead, so a recognized ambient `AKM_EVENT_SOURCE` still wins everywhere it
15
+ * won before, and only the *default* changes.
16
+ */
17
+ export function resolveUsageEventSource(env = process.env, fallback = "user") {
7
18
  const raw = env.AKM_EVENT_SOURCE;
8
19
  if (raw === undefined || raw === "")
9
- return "user";
20
+ return fallback;
10
21
  return USAGE_EVENT_SOURCES.has(raw) ? raw : "unknown";
11
22
  }
12
23
  // ── Schema ──────────────────────────────────────────────────────────────────
@@ -723,11 +723,14 @@ export async function executeInteractiveAgentInvocation(input, seams = {}) {
723
723
  export async function dispatchLoweredExecutionRequest(lowered, options = {}) {
724
724
  requireLoweredExecutionProvenance(lowered);
725
725
  const optionSnapshot = snapshotStrictRecord(options, "lowered execution dispatch options");
726
- assertSnapshotKeys(optionSnapshot, ["executeRunner", "chat", "onRetryAttempt", "runAgent", "runSdk", "lease", "runOptions"], "lowered execution dispatch options");
726
+ assertSnapshotKeys(optionSnapshot, ["executeRunner", "chat", "onRetryAttempt", "runAgent", "runSdk", "lease", "runOptions", "eventSource"], "lowered execution dispatch options");
727
727
  const strictOptions = optionSnapshot;
728
728
  if (strictOptions.onRetryAttempt !== undefined && typeof strictOptions.onRetryAttempt !== "function") {
729
729
  throw new TypeError("lowered execution dispatch options.onRetryAttempt must be a function");
730
730
  }
731
+ if (strictOptions.eventSource !== undefined && typeof strictOptions.eventSource !== "string") {
732
+ throw new TypeError("lowered execution dispatch options.eventSource must be a string");
733
+ }
731
734
  const usesDefaultRunner = strictOptions.executeRunner === undefined;
732
735
  const run = runnerDispatcher(strictOptions.executeRunner);
733
736
  const operationalSnapshot = snapshotStrictRecord(strictOptions.runOptions ?? {}, "lowered execution operational options");
@@ -759,6 +762,14 @@ export async function dispatchLoweredExecutionRequest(lowered, options = {}) {
759
762
  ...(operational.setTimeoutFn !== undefined ? { setTimeoutFn: operational.setTimeoutFn } : {}),
760
763
  ...(operational.clearTimeoutFn !== undefined ? { clearTimeoutFn: operational.clearTimeoutFn } : {}),
761
764
  ...(operational.onEvent !== undefined ? { onEvent: operational.onEvent } : {}),
765
+ // F-1 (spec docs/plans/specs/p1b-model-extraction.md §5.2 point 3): the
766
+ // ONE narrowly-scoped exception to "resolved content cannot be
767
+ // overridden" — sets exactly the one named key, never a caller-supplied
768
+ // env bag (runOptions.env stays unhonored, same as before P1b; see
769
+ // DispatchLoweredExecutionOptions.eventSource's doc).
770
+ ...(strictOptions.eventSource !== undefined
771
+ ? { env: { ...lowered.options.env, AKM_EVENT_SOURCE: strictOptions.eventSource } }
772
+ : {}),
762
773
  });
763
774
  const ownedLease = strictOptions.lease === undefined && usesDefaultRunner;
764
775
  const lease = strictOptions.lease ??
@@ -53,6 +53,7 @@ const PASSTHROUGH_COMMANDS = [
53
53
  "sync",
54
54
  "task-add",
55
55
  "task-doctor",
56
+ "task-explain",
56
57
  "task-history",
57
58
  "task-run",
58
59
  "task-sync",
@@ -61,6 +62,7 @@ const PASSTHROUGH_COMMANDS = [
61
62
  "workflow-abandon",
62
63
  "workflow-create",
63
64
  "workflow-list",
65
+ "workflow-plan",
64
66
  "workflow-resume",
65
67
  "workflow-run",
66
68
  "workflow-status",
@@ -20,4 +20,4 @@ export { formatHealthPlain } from "./health-format.js";
20
20
  export { formatLintPlain } from "./lint-format.js";
21
21
  export { formatGateDecisionSummary, formatProposalAcceptPlain, formatProposalDiffPlain, formatProposalDrainPlain, formatProposalListPlain, formatProposalProducerPlain, formatProposalRejectPlain, formatProposalShowPlain, } from "./proposal-format.js";
22
22
  export { formatShowPlain } from "./show-format.js";
23
- export { formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./workflow-format.js";
23
+ export { formatWorkflowCreatePlain, formatWorkflowListPlain, formatWorkflowPlanPlain, formatWorkflowResumePlain, formatWorkflowRunPlain, formatWorkflowStatusPlain, } from "./workflow-format.js";
@@ -20,16 +20,25 @@ export function formatMigratePlain(result) {
20
20
  const lines = [`${planGlyph(plan.status)} ${plan.status}`];
21
21
  if (plan.taskV3Migration) {
22
22
  const tasks = plan.taskV3Migration;
23
- lines.push(` tasks: ${tasks.changed} change, ${tasks.skipped} current, ${tasks.blocked} blocked`);
23
+ lines.push(` task-v2->v3: ${tasks.changed} change, ${tasks.skipped} current, ${tasks.blocked} blocked`);
24
+ }
25
+ if (plan.taskV4Migration) {
26
+ const tasks = plan.taskV4Migration;
27
+ lines.push(` task-v3->v4: ${tasks.changed} change, ${tasks.skipped} current, ${tasks.blocked} blocked`);
24
28
  }
25
29
  if (plan.blockers?.length) {
26
30
  lines.push("", "blockers:", ...plan.blockers.map((blocker) => ` - ${blocker}`));
27
31
  }
28
32
  if (plan.backupPath) {
29
- lines.push("", `backup: ${plan.backupPath}`);
33
+ lines.push("", `backup (v2->v3): ${plan.backupPath}`);
30
34
  }
31
35
  if (plan.applied !== undefined)
32
- lines.push(`applied: ${plan.applied}`);
36
+ lines.push(`applied (v2->v3): ${plan.applied}`);
37
+ if (plan.taskV4BackupPath) {
38
+ lines.push(`backup (v3->v4): ${plan.taskV4BackupPath}`);
39
+ }
40
+ if (plan.taskV4Applied !== undefined)
41
+ lines.push(`applied (v3->v4): ${plan.taskV4Applied}`);
33
42
  return lines.join("\n");
34
43
  }
35
44
  export const migrateFormatters = [