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
|
@@ -3,44 +3,18 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
5
|
* The `exec` unit runner — the ONE place a frozen workflow spawns a shell
|
|
6
|
-
* command as a unit
|
|
6
|
+
* command as a unit: argv-only (never a shell string), non-blocking,
|
|
7
|
+
* detached with a SIGTERM→SIGKILL ladder against the whole process group,
|
|
8
|
+
* cwd-contained by a resolved-path recheck, resource-bounded (timeout,
|
|
9
|
+
* output bytes, context size), and allowlisted-environment (see
|
|
10
|
+
* {@link childEnv}). `env` values reaching this module are already resolved
|
|
11
|
+
* from `env:` bindings by NAME — the caller scrubs the outcome with
|
|
12
|
+
* `redactUnitOutcome` before anything is journaled. A LEAF module by
|
|
13
|
+
* layering (Node built-ins, `core/spawn-env`, `core/subprocess`, `core/warn`,
|
|
14
|
+
* the import-free `workflows/resource-limits` only).
|
|
7
15
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* directly, so `;`, `|`, `&&`, `$(…)`, backticks, `>` and `*` are inert
|
|
11
|
-
* literal argument BYTES. A workflow that wants a pipeline names the
|
|
12
|
-
* interpreter itself (`["bash", "-lc", "a | b"]`), visibly in frontmatter.
|
|
13
|
-
* - NON-BLOCKING. Everything on this path is async. A synchronous call here
|
|
14
|
-
* blocks the event loop and with it every concurrently-scheduled unit, the
|
|
15
|
-
* run's lease heartbeat, and abort handling.
|
|
16
|
-
* - NO LEAKED CHILDREN. {@link runManagedSubprocess} spawns DETACHED and runs
|
|
17
|
-
* a SIGTERM→SIGKILL ladder against the whole process group, so `--timeout`
|
|
18
|
-
* and Ctrl-C really do stop a running command and its descendants.
|
|
19
|
-
* - CONTAINMENT. `exec.cwd` is relative and `..`-free by construction (parser
|
|
20
|
-
* and frozen-plan decoder), which is necessary but not sufficient: a
|
|
21
|
-
* subdirectory can be a symlink. The RESOLVED path is therefore re-checked
|
|
22
|
-
* against the RESOLVED base immediately before spawning.
|
|
23
|
-
* - BOUNDED SPEND. A command is arbitrary code with no resource discipline of
|
|
24
|
-
* its own, so each resource it spends on akm's behalf has a ceiling in
|
|
25
|
-
* `workflows/resource-limits.ts`: wall clock ({@link DEFAULT_EXEC_TIMEOUT_MS}
|
|
26
|
-
* or the authored `timeout:`), retained output
|
|
27
|
-
* ({@link WORKFLOW_MAX_EXEC_OUTPUT_BYTES} per pipe) and the context
|
|
28
|
-
* environment ({@link execContextLimits}, checked BEFORE the spawn so an
|
|
29
|
-
* oversized artifact yields an actionable akm error, not a bare `E2BIG`).
|
|
30
|
-
* - ALLOWLISTED ENVIRONMENT. The child does NOT inherit akm's environment: it
|
|
31
|
-
* starts EMPTY and receives exactly {@link EXEC_DEFAULT_ENV_PASSTHROUGH}
|
|
32
|
-
* plus the unit's `exec.passEnv`, then the resolved `env:` bindings, then
|
|
33
|
-
* the engine-authored `AKM_*` context. See {@link childEnv}.
|
|
34
|
-
*
|
|
35
|
-
* Secrets: `env` values reaching this module are already resolved from `env:`
|
|
36
|
-
* bindings by NAME (`resolveEnvBinding`) — the plan never carries inline secrets
|
|
37
|
-
* and the input hash only ever carries names. The caller scrubs the outcome with
|
|
38
|
-
* `redactUnitOutcome` BEFORE anything is journaled, which is why this module may
|
|
39
|
-
* return raw stdout/stderr diagnostics without knowing anything about redaction.
|
|
40
|
-
*
|
|
41
|
-
* Layering: a LEAF. Node built-ins, `core/spawn-env`, `core/subprocess`,
|
|
42
|
-
* `core/warn` and the import-free `workflows/resource-limits` (plus erased
|
|
43
|
-
* types) only, so the executor can consume it without opening an import cycle.
|
|
16
|
+
* See docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md
|
|
17
|
+
* for the full per-invariant design history.
|
|
44
18
|
*
|
|
45
19
|
* @module workflows/exec/exec-unit
|
|
46
20
|
*/
|
|
@@ -65,58 +39,17 @@ import { execContextLimits, utf8Bytes, WORKFLOW_EXEC_OUTPUT_TRUNCATED_MARKER, WO
|
|
|
65
39
|
const EXEC_STDERR_DIAGNOSTIC_CLIP = WORKFLOW_UNIT_DIAGNOSTIC_CLIP - 500;
|
|
66
40
|
/**
|
|
67
41
|
* The DEFAULT environment allowlist for an exec unit's child — the single
|
|
68
|
-
* definition of the EXEC list
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* extending the same {@link COMMON_SPAWN_ENV_PASSTHROUGH} baseline those
|
|
77
|
-
* profiles start from, so the two child-spawn allowlists share one floor.
|
|
78
|
-
* Every entry earns its place by being load-bearing for ordinary commands on
|
|
79
|
-
* some supported platform:
|
|
80
|
-
*
|
|
81
|
-
* - `PATH` — command resolution. Without it only an absolute `argv[0]`
|
|
82
|
-
* can ever be spawned.
|
|
83
|
-
* - `HOME` — the config/cache root essentially every toolchain reads
|
|
84
|
-
* (git, npm, bun, cargo, ssh). Absent, tools fall back to
|
|
85
|
-
* `/` or fail outright.
|
|
86
|
-
* - `USER`, `LOGNAME` — process identity; git and ssh read them to attribute
|
|
87
|
-
* and authenticate.
|
|
88
|
-
* - `SHELL` — read by tools that re-exec a login shell for the user's
|
|
89
|
-
* own environment (an explicit `["bash", "-lc", …]` argv
|
|
90
|
-
* does not need it, but `git`'s pagers/editors do).
|
|
91
|
-
* - `LANG`, `LC_ALL`, `LC_CTYPE` — text encoding. Without a locale a command
|
|
92
|
-
* falls back to the C locale and mangles non-ASCII stdout,
|
|
93
|
-
* which IS this unit's artifact.
|
|
94
|
-
* - `TERM` — some CLIs abort or emit raw escape bytes with no TERM.
|
|
95
|
-
* - `TZ` — timestamps a command prints would otherwise silently
|
|
96
|
-
* switch to the host default.
|
|
97
|
-
* - `TMPDIR` — POSIX scratch space; absent, tools write to `/tmp` or
|
|
98
|
-
* fail on read-only hosts.
|
|
99
|
-
* - {@link WIN32_SPAWN_ENV_FLOOR} — what Windows itself requires of any
|
|
100
|
-
* child (process creation, command resolution, and the
|
|
101
|
-
* win32 analogues of `HOME`/`TMPDIR`). Spread in rather
|
|
102
|
-
* than re-spelled: `spawnEnvNamesFor` appends the same
|
|
103
|
-
* names on win32, and two hand-written copies of one OS
|
|
104
|
-
* requirement is exactly how a floor drifts.
|
|
105
|
-
* - `APPDATA`, `LOCALAPPDATA` — Windows config/cache roots (npm, bun, git).
|
|
106
|
-
* - `ProgramData`, `ProgramFiles` — machine-wide install roots that Windows
|
|
107
|
-
* toolchain shims resolve against. These four are NOT on
|
|
108
|
-
* the floor (a child is creatable without them), so an
|
|
109
|
-
* arbitrary shell command asks for them here.
|
|
110
|
-
* - `AKM_EVENT_SOURCE` — provenance, never a secret: an exec unit that calls
|
|
111
|
-
* `akm` must record machine traffic rather than user
|
|
112
|
-
* demand, exactly as the agent passthrough list does
|
|
113
|
-
* (`integrations/agent/profiles.ts`, DRIFT-6).
|
|
42
|
+
* definition of the EXEC list; `exec.passEnv` extends it per unit. Extends
|
|
43
|
+
* {@link COMMON_SPAWN_ENV_PASSTHROUGH} (the same baseline agent-harness
|
|
44
|
+
* children use) plus POSIX/Windows names load-bearing for ordinary commands
|
|
45
|
+
* (PATH, HOME, USER/LOGNAME, SHELL, locale, TERM, TZ, TMPDIR, the
|
|
46
|
+
* {@link WIN32_SPAWN_ENV_FLOOR}, Windows toolchain roots) and
|
|
47
|
+
* `AKM_EVENT_SOURCE` (provenance, DRIFT-6). Deliberately ABSENT and reachable
|
|
48
|
+
* only through `exec.passEnv` / `env:`: credentials, cloud/CI vars, and the
|
|
49
|
+
* proxy family.
|
|
114
50
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* family (`HTTP_PROXY` & friends) — proxy URLs routinely embed credentials,
|
|
118
|
-
* which is why akm's redaction policy already treats URL-shaped passthrough
|
|
119
|
-
* values as credential-bearing.
|
|
51
|
+
* See docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md
|
|
52
|
+
* for the per-entry rationale and why the default is an allowlist at all.
|
|
120
53
|
*/
|
|
121
54
|
export const EXEC_DEFAULT_ENV_PASSTHROUGH = [
|
|
122
55
|
// PATH, HOME, USER, LANG, LC_ALL, TERM, TMPDIR, AKM_EVENT_SOURCE
|
|
@@ -139,57 +72,19 @@ export const EXEC_DEFAULT_ENV_PASSTHROUGH = [
|
|
|
139
72
|
/**
|
|
140
73
|
* Run one exec unit and map its process outcome onto the dispatch vocabulary:
|
|
141
74
|
* non-zero exit → `non_zero_exit`, wall-clock expiry → `timeout`, cancellation
|
|
142
|
-
* → `aborted`, a child that never started → `spawn_failed
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* `exec_context_too_large` and `exec_capture_incomplete` are deliberate: each
|
|
146
|
-
* tampering, a runaway, an authoring bug, or work that ALREADY RAN — never
|
|
147
|
-
* transient — so no `retry.on` value can ever re-dispatch one.
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* can error, and the stream-drain timeout can fire while the command LEADER has
|
|
153
|
-
* already exited 0 because a background descendant still holds the stdout fd
|
|
154
|
-
* open. Both leave a PREFIX, and promoting it would hand the next step, the gate
|
|
155
|
-
* judge and `steps.<id>.output` a silently truncated artifact. So the unit fails
|
|
156
|
-
* instead, through the same shared classifier (`streamCaptureFailure`) the agent
|
|
157
|
-
* spawn path uses.
|
|
158
|
-
*
|
|
159
|
-
* Only STDOUT is fatal here. stderr is a diagnostic channel that never
|
|
160
|
-
* contributes to the artifact, so a stderr drain that did not finish leaves the
|
|
161
|
-
* unit's actual result — a completed command, a fully captured stdout — intact;
|
|
162
|
-
* failing it would throw away a valid artifact over a lost log tail. The agent
|
|
163
|
-
* path classifies both pipes because there stderr genuinely feeds its
|
|
164
|
-
* diagnostics; the difference lives in the CALLERS, not in the shared
|
|
165
|
-
* classifier.
|
|
166
|
-
*
|
|
167
|
-
* The stdout reason is `exec_capture_incomplete`, deliberately OUTSIDE the
|
|
168
|
-
* `retry.on` taxonomy, for the same reason `journal_write_failed` is: the
|
|
169
|
-
* command RAN TO COMPLETION and exited 0 — what failed is akm's record of it. A
|
|
170
|
-
* retryable reason here would let `retry.on: [spawn_failed]` re-dispatch
|
|
171
|
-
* byte-identical argv for a command that already deployed, already published,
|
|
172
|
-
* already migrated. `spawn_failed` keeps its documented meaning — the child
|
|
173
|
-
* never started.
|
|
75
|
+
* → `aborted`, a child that never started → `spawn_failed` (all pre-existing
|
|
76
|
+
* `AgentFailureReason` members, so `retry.on` keeps working). The
|
|
77
|
+
* out-of-taxonomy `exec_cwd_escape`, `exec_output_limit`,
|
|
78
|
+
* `exec_context_too_large` and `exec_capture_incomplete` are deliberate: each
|
|
79
|
+
* is tampering, a runaway, an authoring bug, or work that ALREADY RAN — never
|
|
80
|
+
* a transient — so no `retry.on` value can ever re-dispatch one. An
|
|
81
|
+
* INCOMPLETE stdout capture is always a failure, never a partial artifact;
|
|
82
|
+
* output OVERFLOW past {@link WORKFLOW_MAX_EXEC_OUTPUT_BYTES} does not fail a
|
|
83
|
+
* command that otherwise passed unless the unit declared an `output:` schema
|
|
84
|
+
* (a truncated JSON prefix cannot parse).
|
|
174
85
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* Crossing {@link WORKFLOW_MAX_EXEC_OUTPUT_BYTES} is a different condition: the
|
|
178
|
-
* reader DID drain the pipe to its end, it just stopped RETAINING, so the child
|
|
179
|
-
* never blocked and its exit code is real. Failing a passing-but-chatty test
|
|
180
|
-
* suite over its log volume would be a tripwire, so overflow splits by what the
|
|
181
|
-
* unit PROMISED about its output:
|
|
182
|
-
*
|
|
183
|
-
* - NO declared `output:` schema → success, with the artifact carrying a
|
|
184
|
-
* {@link WORKFLOW_EXEC_OUTPUT_TRUNCATED_MARKER} block naming both byte
|
|
185
|
-
* counts, so truncated text can never pass for complete text.
|
|
186
|
-
* - a declared `output:` schema → `exec_output_limit`: stdout must parse as
|
|
187
|
-
* EXACTLY one JSON value, a truncated prefix cannot, and promoting it would
|
|
188
|
-
* corrupt every downstream reference to the typed artifact.
|
|
189
|
-
*
|
|
190
|
-
* stderr overflow never fails anything: stderr is a diagnostic channel, and
|
|
191
|
-
* {@link EXEC_STDERR_DIAGNOSTIC_CLIP} already bounds and marks what reaches the
|
|
192
|
-
* journal.
|
|
86
|
+
* See docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md
|
|
87
|
+
* for the full capture/overflow reasoning.
|
|
193
88
|
*/
|
|
194
89
|
export async function runExecUnit(input) {
|
|
195
90
|
const cwd = await resolveExecCwd(input);
|
|
@@ -201,7 +96,7 @@ export async function runExecUnit(input) {
|
|
|
201
96
|
const result = await runManagedSubprocess([...input.exec.command], {
|
|
202
97
|
capture: true,
|
|
203
98
|
cwd: cwd.path,
|
|
204
|
-
env: childEnv(input.exec, input.env, input.context),
|
|
99
|
+
env: childEnv(input.exec, input.env, input.context, input.eventSource),
|
|
205
100
|
timeoutMs: input.timeoutMs,
|
|
206
101
|
// stdout IS this unit's artifact, so RETENTION is BOUNDED: an unbounded
|
|
207
102
|
// capture is memory the akm process spends on a command's behalf with no
|
|
@@ -457,40 +352,25 @@ async function isExistingDirectory(candidate) {
|
|
|
457
352
|
}
|
|
458
353
|
}
|
|
459
354
|
/**
|
|
460
|
-
* The child's environment, in three layers with fixed precedence:
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* 3. the engine-authored `AKM_*` context, LAST so a workflow-supplied binding
|
|
466
|
-
* can never shadow the ids/item the engine is telling the command the
|
|
467
|
-
* truth about.
|
|
468
|
-
*
|
|
469
|
-
* ## Why the default is an allowlist
|
|
470
|
-
*
|
|
471
|
-
* Not because it stops an attacker: a command that runs at all can read the
|
|
472
|
-
* same credentials off disk that the environment would have handed it, and a
|
|
473
|
-
* workflow source is executed code either way (`docs/guides/run-workflows.md`,
|
|
474
|
-
* "workflow sources are executed code"). The allowlist earns its place for
|
|
475
|
-
* three narrower, real reasons:
|
|
476
|
-
*
|
|
477
|
-
* - it bounds ACCIDENTAL exposure — the ambient shell of whoever ran
|
|
478
|
-
* `akm workflow run` (or the CI job that did) routinely carries tokens for
|
|
479
|
-
* unrelated services, and a third-party workflow step that merely prints
|
|
480
|
-
* its environment, or a tool that ships one in a crash report, should not
|
|
481
|
-
* get them for free;
|
|
482
|
-
* - it makes the environment surface EXPLICIT and REVIEWABLE — what a
|
|
483
|
-
* command can see is this constant plus lines in the frontmatter diff,
|
|
484
|
-
* rather than "whatever the invoking shell happened to export";
|
|
485
|
-
* - it matches the convention akm already applies to spawned children —
|
|
486
|
-
* `profile.envPassthrough` in `integrations/agent/spawn.ts` has always
|
|
487
|
-
* built agent-harness children this way, and the SAME
|
|
488
|
-
* {@link collectAllowlistedEnv} does it here, so there is one mechanism to
|
|
489
|
-
* review instead of two.
|
|
355
|
+
* The child's environment, in three layers with fixed precedence: (1) the
|
|
356
|
+
* BASE — {@link EXEC_DEFAULT_ENV_PASSTHROUGH} plus the unit's `exec.passEnv`
|
|
357
|
+
* names; (2) the unit's resolved `env:` bindings; (3) the engine-authored
|
|
358
|
+
* `AKM_*` context, LAST so a workflow-supplied binding can never shadow the
|
|
359
|
+
* ids/item the engine is telling the command the truth about.
|
|
490
360
|
*
|
|
361
|
+
* See docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md
|
|
362
|
+
* for why the default is an allowlist rather than full inheritance.
|
|
491
363
|
*/
|
|
492
|
-
function childEnv(exec, bindings, context) {
|
|
364
|
+
function childEnv(exec, bindings, context, eventSource) {
|
|
493
365
|
const env = collectAllowlistedEnv(execAllowlist(exec));
|
|
366
|
+
// F-1 (spec §5.2 point 2): applied to the allowlisted BASE only when the
|
|
367
|
+
// ambient passthrough above left the name absent — an ambient
|
|
368
|
+
// AKM_EVENT_SOURCE already collected into `env` still wins, and this runs
|
|
369
|
+
// strictly BEFORE the bindings/context overlays below, so an authored
|
|
370
|
+
// `env:` binding (or the engine-authored context) still wins too.
|
|
371
|
+
if (eventSource !== undefined && env.AKM_EVENT_SOURCE === undefined) {
|
|
372
|
+
env.AKM_EVENT_SOURCE = eventSource;
|
|
373
|
+
}
|
|
494
374
|
for (const [name, value] of Object.entries(bindings ?? {}))
|
|
495
375
|
env[name] = value;
|
|
496
376
|
for (const [name, value] of Object.entries(context ?? {}))
|
|
@@ -67,8 +67,24 @@ function warnLoweringNotices(...groups) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Build a gate judge from the normalized frozen target without consulting live
|
|
72
|
+
* config.
|
|
73
|
+
*
|
|
74
|
+
* `eventSource` (P1b spec §5.2 point 2, gap closed — code review): the task
|
|
75
|
+
* runner's resolved provenance event source, threaded through exactly like
|
|
76
|
+
* `executeStepSubgraph`'s units so a workflow-task run's judge dispatch is not
|
|
77
|
+
* the one dispatch left silently unstamped. Spread onto the built
|
|
78
|
+
* {@link UnitDispatchRequest} the same way every other optional field here is
|
|
79
|
+
* — `undefined` for the manual `akm workflow step complete` judge
|
|
80
|
+
* (`runtime/runs.ts`, which passes no 5th argument) and for `akm workflow run`
|
|
81
|
+
* (no task context), so both stay byte-identical. When present, it flows
|
|
82
|
+
* through the same `forwardedDispatchEventSource` precedence gate every other
|
|
83
|
+
* "command"-kind dispatch uses (`unit-dispatch.ts`): the module doc above
|
|
84
|
+
* already establishes a judge request never carries an authored `env:`
|
|
85
|
+
* binding, so the gate always forwards it here.
|
|
86
|
+
*/
|
|
87
|
+
export function frozenSummaryJudge(target, signal, dispatcher, owner, eventSource) {
|
|
72
88
|
if (!target)
|
|
73
89
|
return null;
|
|
74
90
|
const dispatch = withDispatchRedaction(dispatcher ?? dispatchWorkflowExecution);
|
|
@@ -89,6 +105,7 @@ export function frozenSummaryJudge(target, signal, dispatcher, owner) {
|
|
|
89
105
|
...commonRequest,
|
|
90
106
|
frozenTarget: target,
|
|
91
107
|
timeoutMs: target.runner.timeoutMs ?? null,
|
|
108
|
+
...(eventSource !== undefined ? { eventSource } : {}),
|
|
92
109
|
};
|
|
93
110
|
// Lowering and authorization precede every live credential/passthrough
|
|
94
111
|
// sample. The injected dispatcher may be a test seam, but it receives the
|
|
@@ -8,31 +8,12 @@
|
|
|
8
8
|
* persistence through the serialized writer queue, and `workflow_unit_*`
|
|
9
9
|
* events for observability.
|
|
10
10
|
*
|
|
11
|
-
* Data flow
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* (
|
|
17
|
-
* the run params, a map unit's item + index, and the artifacts named by the
|
|
18
|
-
* step's `inputs:`. Because nothing is ever substituted INTO the prose, the P1
|
|
19
|
-
* `{{item}}` re-scan injection class is structurally impossible.
|
|
20
|
-
*
|
|
21
|
-
* References survive only in the three whole-value FRONTMATTER positions the
|
|
22
|
-
* closed two-root grammar occupies (`program/expressions.ts`): `map.over`,
|
|
23
|
-
* `route.input`, and each `inputs[]` entry. They resolve ONCE per step against
|
|
24
|
-
* `{ params, stepOutputs }`. There is NO ambient key search: a
|
|
25
|
-
* `steps.<id>.output.<path>` reference addresses INTO that step's recorded
|
|
26
|
-
* output explicitly.
|
|
27
|
-
*
|
|
28
|
-
* Step outputs (`steps.<id>.output…`): every engine-executed step journals a
|
|
29
|
-
* promoted ARTIFACT under `evidence.output` — the solo unit's result/text, the
|
|
30
|
-
* collect reducer's per-item array, or the vote reducer's winner — and that
|
|
31
|
-
* artifact is what the reference scope exposes ({@link projectStepOutput}).
|
|
32
|
-
* The documented addressing (`steps.discover.output.files`) therefore resolves
|
|
33
|
-
* against real step results, never the raw evidence envelope (peer review R1).
|
|
34
|
-
* Steps completed manually (no `output` key in their evidence) expose their
|
|
35
|
-
* recorded evidence object as-is.
|
|
11
|
+
* Data flow: there is no interpolation language — a unit's instructions are
|
|
12
|
+
* the step's body prose byte-exact, and data reaches it as attached
|
|
13
|
+
* structured context instead. References resolve once per step, only in the
|
|
14
|
+
* closed frontmatter positions, against the promoted step-output artifact.
|
|
15
|
+
* See docs/architecture/decisions/0001-no-interpolation-attached-structured-context.md
|
|
16
|
+
* for the full design history (peer review R1, the P1 injection class it closes).
|
|
36
17
|
*
|
|
37
18
|
* Empty free-text outputs (peer review): a SUCCESSFUL schemaless unit that
|
|
38
19
|
* returns the empty string is normalized to "no output" — {@link dispatchUnit}
|
|
@@ -139,6 +120,10 @@ import { assertFrozenExecutableIdentity } from "../../execution/executable-ident
|
|
|
139
120
|
import { withWorkflowRunsConnection, withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
140
121
|
import { materializeFrozenWorkflowEnvironment } from "../ir/environment-v4.js";
|
|
141
122
|
import { WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
|
|
123
|
+
// The ONE child-workflow drive (P3b §3.2) — publishes and drives a
|
|
124
|
+
// `child-workflow`-targeted unit; this module's dispatch seam is its only
|
|
125
|
+
// production caller.
|
|
126
|
+
import { driveChildWorkflowUnit } from "./child-workflow.js";
|
|
142
127
|
// The ONE dispatch redaction contract, shared with the gate-judge path
|
|
143
128
|
// (exec/frozen-judge.ts). Consumers import the leaf directly — this module is
|
|
144
129
|
// not a second front door onto the seam.
|
|
@@ -563,6 +548,11 @@ async function runUnit(input) {
|
|
|
563
548
|
...(env ? { env } : {}),
|
|
564
549
|
...(sensitiveValues ? { sensitiveValues } : {}),
|
|
565
550
|
...(input.signal ? { signal: input.signal } : {}),
|
|
551
|
+
// F-1 (spec §5.2 point 2): forwarded to exec-unit.ts's childEnv for a
|
|
552
|
+
// "script"/"shell" unit, and to dispatchWorkflowExecution's
|
|
553
|
+
// dispatchLoweredExecutionRequest eventSource option (unit-dispatch.ts)
|
|
554
|
+
// for a "command" unit — both arms observe it.
|
|
555
|
+
...(ctx.eventSource !== undefined ? { eventSource: ctx.eventSource } : {}),
|
|
566
556
|
};
|
|
567
557
|
// One content-derived unit id is retained across every retry; the append-only
|
|
568
558
|
// attempt table supplies the 1-based attempt identity.
|
|
@@ -700,7 +690,17 @@ function journaledUnitResultJson(outcome) {
|
|
|
700
690
|
async function prepareAttemptWorktree(input) {
|
|
701
691
|
if (input.worktreeBase === undefined)
|
|
702
692
|
return { ok: true, request: input.request };
|
|
703
|
-
const created = await createUnitWorktree(input.worktreeBase, input.ctx.runId, input.attemptId,
|
|
693
|
+
const created = await createUnitWorktree(input.worktreeBase, input.ctx.runId, input.attemptId,
|
|
694
|
+
// A child-workflow target (P3a, schema-v4.ts) carries no gitCommitOid of
|
|
695
|
+
// its own — it is a composition target, never a worktree-isolated exec
|
|
696
|
+
// one. This arm IS reachable — a step that composes a child workflow and
|
|
697
|
+
// also declares `isolation: worktree` gets a worktree prepared here
|
|
698
|
+
// (worktree prep runs ahead of dispatch), but the child executor
|
|
699
|
+
// (child-workflow.ts, P3b §3.2) never dispatches through it: driving a
|
|
700
|
+
// child publishes and drives a RUN, not a command/exec unit, so the
|
|
701
|
+
// prepared worktree is simply unused by the drive. This ternary keeps the
|
|
702
|
+
// field access total over the frozen-target union either way.
|
|
703
|
+
input.workUnit.frozenTarget.kind === "child-workflow" ? undefined : input.workUnit.frozenTarget.gitCommitOid);
|
|
704
704
|
if (created.preservedLeftover !== undefined) {
|
|
705
705
|
warn(`Workflow unit ${input.attemptId}: a previous attempt left uncollected work in its isolation worktree; ` +
|
|
706
706
|
`preserved at ${created.preservedLeftover}`);
|
|
@@ -831,7 +831,27 @@ async function dispatchJournaledAttempt(input) {
|
|
|
831
831
|
attempt: durableAttempt.attempt,
|
|
832
832
|
dispatchId: durableAttempt.dispatch_id,
|
|
833
833
|
};
|
|
834
|
-
|
|
834
|
+
// P3b §3.2: the ONE dispatch-seam branch. A `child-workflow`-targeted unit
|
|
835
|
+
// never reaches `UnitDispatcher` — it is routed to the child executor
|
|
836
|
+
// instead (src/workflows/exec/child-workflow.ts), which publishes the
|
|
837
|
+
// child idempotently and drives it with the SAME engine
|
|
838
|
+
// (`runWorkflowSteps`) the top-level path uses. Placed HERE — after
|
|
839
|
+
// `reserveJournaledDispatch` claims this attempt row, before
|
|
840
|
+
// `finishJournaledDispatch`/the worktree epilogue below — so a
|
|
841
|
+
// child-workflow unit is journaled exactly like any other unit, and a
|
|
842
|
+
// crash between reservation and child publication leaves a `running`
|
|
843
|
+
// parent row with no child, recovered by resume (which re-dispatches the
|
|
844
|
+
// parent unit and republishes the child idempotently).
|
|
845
|
+
const dispatched = request.frozenTarget.kind === "child-workflow"
|
|
846
|
+
? await driveChildWorkflowUnit({
|
|
847
|
+
request,
|
|
848
|
+
target: request.frozenTarget,
|
|
849
|
+
ctx,
|
|
850
|
+
childParams: workUnit.childParams ?? {},
|
|
851
|
+
inputHash: input.inputHash,
|
|
852
|
+
dispatcher,
|
|
853
|
+
})
|
|
854
|
+
: await dispatchUnit(request, dispatcher);
|
|
835
855
|
// Credential and passthrough values are intentionally sampled only AFTER
|
|
836
856
|
// the default dispatcher has authorized/lowered the frozen request and
|
|
837
857
|
// materialized credentials at its terminal dispatch boundary. Custom test
|
|
@@ -1070,6 +1090,7 @@ export const defaultUnitDispatcher = async (request, feedback) => {
|
|
|
1070
1090
|
...(request.schema ? { hasOutputSchema: true } : {}),
|
|
1071
1091
|
timeoutMs: request.timeoutMs,
|
|
1072
1092
|
...(request.signal ? { signal: request.signal } : {}),
|
|
1093
|
+
...(request.eventSource !== undefined ? { eventSource: request.eventSource } : {}),
|
|
1073
1094
|
});
|
|
1074
1095
|
}
|
|
1075
1096
|
finally {
|
|
@@ -1094,6 +1115,7 @@ export const defaultUnitDispatcher = async (request, feedback) => {
|
|
|
1094
1115
|
...(request.schema ? { hasOutputSchema: true } : {}),
|
|
1095
1116
|
timeoutMs: request.timeoutMs,
|
|
1096
1117
|
...(request.signal ? { signal: request.signal } : {}),
|
|
1118
|
+
...(request.eventSource !== undefined ? { eventSource: request.eventSource } : {}),
|
|
1097
1119
|
});
|
|
1098
1120
|
}
|
|
1099
1121
|
return dispatchWorkflowExecution(request, feedback);
|
|
@@ -23,6 +23,18 @@
|
|
|
23
23
|
* strings. It is purely advisory — it NEVER blocks a run and NEVER mutates
|
|
24
24
|
* params — and is surfaced when a run starts. False positives and false
|
|
25
25
|
* negatives are expected; it is a nudge, not a scanner.
|
|
26
|
+
*
|
|
27
|
+
* ## Reused as `akm task explain`'s redaction check
|
|
28
|
+
*
|
|
29
|
+
* `src/commands/tasks/explain.ts` reuses this same heuristic (via its own
|
|
30
|
+
* `isSecretShapedValue` wrapper) to decide which task-input values to print
|
|
31
|
+
* as `"<redacted>"` instead of in full. That reuse does NOT upgrade this
|
|
32
|
+
* detector into a hard guarantee: `explain`'s redaction is exactly as
|
|
33
|
+
* best-effort as the warnings above — a short, low-entropy, or
|
|
34
|
+
* unusually-named credential that this function does not flag prints
|
|
35
|
+
* UNREDACTED there too. Do not describe either surface as "secret-free by
|
|
36
|
+
* construction"; describe it as "secret-shaped values are redacted on a
|
|
37
|
+
* best-effort basis."
|
|
26
38
|
*/
|
|
27
39
|
/**
|
|
28
40
|
* Substrings that, when present in a param KEY (case-insensitive), suggest the
|
|
@@ -4,63 +4,15 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Engine-driven workflow execution — the `akm workflow run`
|
|
6
6
|
* start/resume/execute path, and the single execution surface for a run: akm
|
|
7
|
-
* walks the frozen plan and dispatches every unit itself.
|
|
7
|
+
* walks the frozen plan and dispatches every unit itself. Every step
|
|
8
|
+
* advances through `completeWorkflowStep` (never a direct step-row write),
|
|
9
|
+
* the plan is read from its frozen `plan_json` row rather than live source,
|
|
10
|
+
* a run lease enforces one driving engine invocation at a time, gate loops
|
|
11
|
+
* are bounded, and the SDK dispatch registry is drained in a `finally` on
|
|
12
|
+
* every exit path so no child process keeps the event loop open.
|
|
8
13
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* summary-validation gate and run-state derivation stay authoritative. A gate
|
|
12
|
-
* rejection (SummaryValidationFailure) STOPS the engine and surfaces the
|
|
13
|
-
* corrective feedback — a gate is a gate, even for the engine.
|
|
14
|
-
*
|
|
15
|
-
* Artifact-judging gates (redesign addendum, R2): when a step declares
|
|
16
|
-
* completion criteria, the engine hands the gate a summary BUILT FROM the
|
|
17
|
-
* step's promoted artifact (canonical JSON, clipped, prefixed with a one-line
|
|
18
|
-
* unit count — `buildArtifactSummary`) instead of the machine-prose execution
|
|
19
|
-
* summary, so the judge evaluates real results. Each engine-driven judge call
|
|
20
|
-
* is journaled as a unit row (`node_id "<stepId>.gate"`, `unit_id
|
|
21
|
-
* "<stepId>.gate:l<loop>"`, runner "llm", result_json = the verdict) through
|
|
22
|
-
* the writer queue — it is an LLM call like any other. Human approvals are
|
|
23
|
-
* never cached: a blocked gate stays blocked.
|
|
24
|
-
*
|
|
25
|
-
* Bounded gate loops (`gate.max_loops`, addendum R2): a rejection on a step
|
|
26
|
-
* with maxLoops > 1 re-executes the step subgraph with the judge's feedback +
|
|
27
|
-
* missing[] threaded into every unit prompt (`gateFeedback` on
|
|
28
|
-
* StepExecutionContext) — the feedback changes each unit's input hash, so the
|
|
29
|
-
* loop re-dispatches naturally instead of reusing the rejected rows. After
|
|
30
|
-
* maxLoops rejections the engine stops with the gate feedback, exactly like
|
|
31
|
-
* the one-shot case. A typed-artifact schema mismatch feeds the same loop
|
|
32
|
-
* (the validation errors are the feedback; no judge ran, so no gate unit is
|
|
33
|
-
* journaled for that attempt) — only the FINAL loop's mismatch fails the run.
|
|
34
|
-
* A step whose subgraph is an `exec` unit is judged but NEVER looped
|
|
35
|
-
* (`effectiveGateMaxLoops`): its argv cannot read the feedback, so a second
|
|
36
|
-
* loop would only re-run the identical side effect.
|
|
37
|
-
*
|
|
38
|
-
* Frozen plan (redesign addendum, R1): the plan graph is read from the run
|
|
39
|
-
* row (`plan_json`, persisted by `startWorkflowRun` under migration 006) with
|
|
40
|
-
* a `plan_hash` integrity check — the workflow asset file is NEVER re-read
|
|
41
|
-
* for an in-flight run, so a mid-run asset edit cannot change behavior.
|
|
42
|
-
* Durable-row resume: re-invoking a partially-executed run re-dispatches only
|
|
43
|
-
* work that never completed.
|
|
44
|
-
*
|
|
45
|
-
* Run lease (redesign addendum, R2): exactly one engine invocation drives a
|
|
46
|
-
* run at a time. The lease (random holder id + 90s expiry on the run row) is
|
|
47
|
-
* acquired before any dispatch, renewed between steps, and released in a
|
|
48
|
-
* `finally` unless a failed run retains it as forensic state; a second
|
|
49
|
-
* `workflow run` on a live-leased run refuses up front,
|
|
50
|
-
* and an expired lease is claimable (crash recovery). While the lease is
|
|
51
|
-
* live, any competing spine advance is refused — the engine owns the run while
|
|
52
|
-
* driving (enforced inside `completeWorkflowStep`).
|
|
53
|
-
*
|
|
54
|
-
* Process-lifecycle contract (owner finding 4 — no leaked handles): the SDK
|
|
55
|
-
* dispatch path caches `opencode serve` CHILD PROCESSES in a per-env registry
|
|
56
|
-
* for reuse across units. Each live child is an OS handle that keeps Bun's
|
|
57
|
-
* event loop open; the registry's own teardown is wired only to
|
|
58
|
-
* `process.once('exit')`, which never fires while a child holds the loop open.
|
|
59
|
-
* That deadlock hangs a one-shot CLI (`akm workflow run` has no `process.exit`
|
|
60
|
-
* on success — it relies on the loop draining). The engine therefore DRAINS
|
|
61
|
-
* the dispatch registry ({@link disposeDispatchResources}) in its run `finally`,
|
|
62
|
-
* on EVERY exit path, so the process exits cleanly the moment the run resolves.
|
|
63
|
-
* The drain is synchronous, idempotent, and a no-op when no SDK server started.
|
|
14
|
+
* See docs/architecture/decisions/0011-engine-run-loop-invariants.md for the
|
|
15
|
+
* full design history behind each of these invariants.
|
|
64
16
|
*/
|
|
65
17
|
import { randomUUID } from "node:crypto";
|
|
66
18
|
import { UsageError } from "../../core/errors.js";
|
|
@@ -373,7 +325,11 @@ function workflowSummaryJudge(options, stepPlan, signal, owner) {
|
|
|
373
325
|
return options.summaryJudge;
|
|
374
326
|
// The judge dispatches under the REAL run/step identity; the per-loop gate row
|
|
375
327
|
// identity is threaded in per call by the completion path that journals it.
|
|
376
|
-
|
|
328
|
+
// eventSource (gap closed, code review): the judge's dispatch is a "command"
|
|
329
|
+
// request like any other exec/agent/sdk unit, so it goes through the same
|
|
330
|
+
// provenance thread `executeStepSubgraph` uses below — undefined for every
|
|
331
|
+
// non-task caller, byte-identical.
|
|
332
|
+
return frozenSummaryJudge(stepPlan.gate.frozenJudge, signal, options.dispatcher ?? defaultUnitDispatcher, owner, options.eventSource);
|
|
377
333
|
}
|
|
378
334
|
/**
|
|
379
335
|
* Seed the lifetime unit cap AND the budget ceilings from the journal so
|
|
@@ -477,7 +433,10 @@ async function recoverGateLoopState(runId, stepPlan) {
|
|
|
477
433
|
* The kinds that FINISHED the step (completed / failed / gate-exhausted) — the
|
|
478
434
|
* ONE `maxSteps` consumption for its whole gate loop. An abort and a judge
|
|
479
435
|
* outage leave the step unfinished and consume nothing: the next invocation
|
|
480
|
-
* still owes the work.
|
|
436
|
+
* still owes the work. A blocked child is the SAME shape as a judge outage
|
|
437
|
+
* (P3b §3.4) — a gate is a gate for a child workflow too, so it is likewise
|
|
438
|
+
* NOT in this set: `akm workflow resume` is what clears it, never an
|
|
439
|
+
* automatic in-step re-dispatch.
|
|
481
440
|
*/
|
|
482
441
|
const STEP_FINISHED_KINDS = new Set(["advanced", "failed", "gate-exhausted"]);
|
|
483
442
|
/**
|
|
@@ -510,6 +469,9 @@ async function executeStepSubgraph(ctx, loop) {
|
|
|
510
469
|
...(plan.budget ? { budget: plan.budget } : {}),
|
|
511
470
|
gateLoop,
|
|
512
471
|
...(gateFeedback ? { gateFeedback } : {}),
|
|
472
|
+
// F-1 (spec §5.2 point 2): threaded to an exec unit's child env;
|
|
473
|
+
// undefined for every non-task caller (byte-identical, RunWorkflowOptions doc).
|
|
474
|
+
...(options.eventSource !== undefined ? { eventSource: options.eventSource } : {}),
|
|
513
475
|
// The heartbeat's signal is the effective dispatch signal: a lost
|
|
514
476
|
// lease (or a caller abort) aborts in-flight units promptly.
|
|
515
477
|
...(dispatchSignal ? { signal: dispatchSignal } : {}),
|
|
@@ -633,6 +595,29 @@ async function runStepGateLoop(ctx, gate, totals) {
|
|
|
633
595
|
executed[executed.length - 1] = { ...executed[executed.length - 1], summary: finalize.summary };
|
|
634
596
|
return outcome({ kind: "judge-failed", judgeFailure: { stepId: step.id, message: finalize.summary } });
|
|
635
597
|
}
|
|
598
|
+
if (finalize.kind === "child-blocked") {
|
|
599
|
+
// P3b §3.4: a composed child workflow is blocked. Like judge-failed,
|
|
600
|
+
// NO gate loop was consumed and the step does not count against
|
|
601
|
+
// maxSteps. `result.childBlocked` (set by reduceStepOutcomes off the
|
|
602
|
+
// failed unit's live-only childRun field) carries the identity
|
|
603
|
+
// finalizeExecutedStep's own return value deliberately omits.
|
|
604
|
+
executed[executed.length - 1] = { ...executed[executed.length - 1], summary: finalize.summary };
|
|
605
|
+
const child = result.childBlocked;
|
|
606
|
+
return outcome({
|
|
607
|
+
kind: "child-blocked",
|
|
608
|
+
...(child
|
|
609
|
+
? {
|
|
610
|
+
childBlocked: {
|
|
611
|
+
stepId: step.id,
|
|
612
|
+
childRunId: child.childRunId,
|
|
613
|
+
childRef: child.childRef,
|
|
614
|
+
resume: `akm workflow resume ${child.childRunId}`,
|
|
615
|
+
resumeParentCommand: `akm workflow resume ${next.run.id} && akm workflow run ${next.run.id}`,
|
|
616
|
+
},
|
|
617
|
+
}
|
|
618
|
+
: {}),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
636
621
|
if (finalize.kind === "failed") {
|
|
637
622
|
// A route-failure was pushed as ok:true (the units succeeded); reflect
|
|
638
623
|
// the deterministic route failure in the executed report.
|
|
@@ -679,6 +664,7 @@ liveEvidence) {
|
|
|
679
664
|
const executed = [];
|
|
680
665
|
let gateRejection;
|
|
681
666
|
let judgeFailure;
|
|
667
|
+
let childBlocked;
|
|
682
668
|
let aborted = false;
|
|
683
669
|
const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
|
|
684
670
|
// The `maxSteps` budget counts DISTINCT spine steps that finished processing
|
|
@@ -820,10 +806,12 @@ liveEvidence) {
|
|
|
820
806
|
gateRejection = outcome.gateRejection;
|
|
821
807
|
if (outcome.judgeFailure)
|
|
822
808
|
judgeFailure = outcome.judgeFailure;
|
|
809
|
+
if (outcome.childBlocked)
|
|
810
|
+
childBlocked = outcome.childBlocked;
|
|
823
811
|
if (STEP_FINISHED_KINDS.has(outcome.kind))
|
|
824
812
|
stepsProcessed += 1;
|
|
825
813
|
// Only an advance leaves the spine walkable; every other kind ends this
|
|
826
|
-
// invocation (failure, exhausted gate, judge outage, abort).
|
|
814
|
+
// invocation (failure, exhausted gate, judge outage, blocked child, abort).
|
|
827
815
|
if (outcome.kind !== "advanced")
|
|
828
816
|
break;
|
|
829
817
|
next = await getNextWorkflowStep(next.run.id);
|
|
@@ -839,6 +827,7 @@ liveEvidence) {
|
|
|
839
827
|
...(finalState.run.status === "completed" ? { done: true } : {}),
|
|
840
828
|
...(gateRejection ? { gateRejection } : {}),
|
|
841
829
|
...(judgeFailure ? { judgeFailure } : {}),
|
|
830
|
+
...(childBlocked ? { childBlocked } : {}),
|
|
842
831
|
...(aborted ? { aborted: true } : {}),
|
|
843
832
|
};
|
|
844
833
|
}
|