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
package/dist/core/subprocess.js
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
6
|
-
|
|
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
|
|
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(`
|
|
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 = [
|