akm-cli 0.9.0 → 0.9.1-beta.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 +724 -0
- package/README.md +28 -63
- package/STABILITY.md +4 -2
- package/dist/cli/parse-args.js +7 -1
- package/dist/commands/agent/contribute-cli.js +1 -1
- package/dist/commands/env/child-env.js +14 -0
- package/dist/commands/feedback-cli.js +7 -1
- package/dist/commands/health/llm-usage.js +2 -1
- package/dist/commands/health/surfaces.js +4 -77
- package/dist/commands/health.js +65 -11
- package/dist/commands/improve/distill/quality-gate.js +6 -1
- package/dist/commands/improve/eligibility.js +7 -1
- package/dist/commands/improve/eval-cases.js +2 -0
- package/dist/commands/improve/improve.js +126 -10
- package/dist/commands/improve/locks.js +7 -0
- package/dist/commands/improve/memory/memory-improve.js +9 -0
- package/dist/commands/improve/run-context.js +5 -0
- package/dist/commands/improve/session-asset.js +4 -0
- package/dist/commands/lint/base-linter.js +31 -7
- package/dist/commands/lint/index.js +205 -51
- package/dist/commands/lint/types.js +22 -1
- package/dist/commands/proposal/repository.js +17 -1
- package/dist/commands/sources/add-cli.js +8 -2
- package/dist/commands/sources/info.js +12 -2
- package/dist/commands/sources/installed-stashes.js +6 -1
- package/dist/commands/sources/migration-help.js +12 -3
- package/dist/commands/sources/self-update.js +9 -1
- package/dist/commands/tasks/tasks.js +8 -2
- package/dist/commands/workflow-cli.js +17 -11
- package/dist/core/abort-deadline.js +28 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +83 -5
- package/dist/core/adapter/adapters/akm-adapter.js +13 -10
- package/dist/core/adapter/adapters/akm-lint.js +78 -22
- package/dist/core/adapter/adapters/akm-task-adapter.js +43 -20
- package/dist/core/adapter/adapters/dotenv-adapter.js +21 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +5 -3
- package/dist/core/asset/frontmatter.js +10 -1
- package/dist/core/common.js +147 -9
- package/dist/core/concurrent.js +32 -0
- package/dist/core/config/config-io.js +5 -45
- package/dist/core/config/schema/engines.js +14 -3
- package/dist/core/config/schema/workflow.js +11 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/events.js +30 -24
- package/dist/core/extra-params.js +11 -0
- package/dist/core/file-lock.js +7 -1
- package/dist/core/fs-txn.js +15 -2
- package/dist/core/improve-result.js +5 -0
- package/dist/core/json-schema.js +344 -9
- package/dist/core/loopback.js +89 -0
- package/dist/core/migration-operation.js +17 -2
- package/dist/core/path-access.js +107 -0
- package/dist/core/paths.js +16 -2
- package/dist/core/redaction.js +86 -18
- package/dist/core/spawn-env.js +234 -0
- package/dist/core/state-db-scope.js +134 -0
- package/dist/core/state-db.js +1 -0
- package/dist/core/subprocess.js +181 -37
- package/dist/core/write-provenance.js +85 -0
- package/dist/core/write-source.js +33 -2
- package/dist/indexer/db/graph-db.js +17 -6
- package/dist/indexer/ensure-index.js +10 -3
- package/dist/indexer/index-written-assets.js +17 -2
- package/dist/indexer/indexer.js +86 -21
- package/dist/indexer/passes/memory-inference.js +4 -0
- package/dist/indexer/search/db-search.js +25 -17
- package/dist/indexer/walk/walker.js +6 -1
- package/dist/integrations/agent/detect.js +13 -1
- package/dist/integrations/agent/engine-resolution.js +24 -11
- package/dist/integrations/agent/model-aliases.js +1 -1
- package/dist/integrations/agent/profiles.js +9 -1
- package/dist/integrations/agent/spawn.js +15 -87
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +21 -0
- package/dist/integrations/lockfile.js +55 -2
- package/dist/llm/client.js +14 -19
- package/dist/llm/embedder.js +23 -3
- package/dist/llm/embedders/remote.js +27 -2
- package/dist/output/html-render.js +40 -1
- package/dist/output/text/lint-format.js +17 -4
- package/dist/runtime.js +23 -1
- package/dist/scripts/akm-migrate-node.js +1714 -836
- package/dist/scripts/akm-migrate.js +1682 -804
- package/dist/setup/setup.js +22 -7
- package/dist/sources/providers/git-install.js +25 -2
- package/dist/sources/providers/git-stash.js +19 -0
- package/dist/sources/providers/git.js +1 -1
- package/dist/sources/snapshot-fetchers/content-extract.js +63 -1
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -20
- package/dist/storage/database.js +71 -7
- package/dist/storage/engines/sqlite-migrations.js +61 -2
- package/dist/storage/managed-db.js +19 -0
- package/dist/storage/repositories/index-connection.js +39 -4
- package/dist/storage/repositories/index-entries-repository.js +6 -1
- package/dist/storage/repositories/index-meta-repository.js +11 -0
- package/dist/storage/repositories/index-schema.js +17 -2
- package/dist/storage/repositories/index-vec-repository.js +43 -5
- package/dist/storage/repositories/workflow-runs-repository.js +66 -13
- package/dist/storage/sqlite-pragmas.js +12 -1
- package/dist/tasks/log-redaction.js +156 -0
- package/dist/tasks/parser.js +82 -5
- package/dist/tasks/runner.js +222 -17
- package/dist/tasks/scheduler-invocation.js +19 -0
- package/dist/tasks/schema.js +86 -1
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/concurrency-policy.js +95 -1
- package/dist/workflows/exec/dispatch-redaction.js +114 -0
- package/dist/workflows/exec/exec-unit.js +542 -0
- package/dist/workflows/exec/frozen-judge.js +114 -42
- package/dist/workflows/exec/native-executor.js +465 -238
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +424 -219
- package/dist/workflows/exec/step-work.js +506 -167
- package/dist/workflows/exec/unit-dispatch.js +31 -1
- package/dist/workflows/exec/unit-writer.js +53 -13
- package/dist/workflows/exec/worktree.js +454 -41
- package/dist/workflows/ir/compile.js +26 -2
- package/dist/workflows/ir/freeze.js +82 -15
- package/dist/workflows/ir/schema.js +105 -20
- package/dist/workflows/parser.js +242 -19
- package/dist/workflows/program/schema.js +24 -0
- package/dist/workflows/renderer.js +32 -4
- package/dist/workflows/resource-limits.js +182 -0
- package/dist/workflows/runtime/runs.js +146 -6
- package/dist/workflows/validate-summary.js +17 -2
- package/docs/README.md +74 -32
- package/docs/migration/release-notes/0.9.0.md +2 -1
- package/docs/migration/v0.7-to-v0.8.md +2 -1
- package/docs/migration/v0.8-to-v0.9.md +3 -1
- package/docs/reference/README.md +11 -4
- package/docs/reference/bundle-types.md +19 -0
- package/docs/reference/cli.md +105 -16
- package/docs/reference/configuration.md +15 -2
- package/docs/reference/data-and-telemetry.md +30 -10
- package/docs/reference/supported-formats.md +50 -0
- package/docs/reference/workflow-schema.md +1014 -0
- package/docs/reference/workflows.md +37 -633
- package/package.json +13 -6
- package/schemas/akm-config.json +18 -5
- package/schemas/akm-task.json +27 -5
- package/schemas/akm-workflow.json +92 -13
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,730 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.1-beta.2] - 2026-08-17
|
|
10
|
+
|
|
11
|
+
### Breaking changes & migration
|
|
12
|
+
|
|
13
|
+
The 0.9.x series carries breaking changes as it works toward the 0.10.x
|
|
14
|
+
stabilization line. Every item here is detailed further down; this section is
|
|
15
|
+
what an upgrader reads first.
|
|
16
|
+
|
|
17
|
+
- **A data directory akm cannot READ is now an error, not an empty result.**
|
|
18
|
+
Commands that previously returned `hits: []` / `entryCount: 0` / "nothing
|
|
19
|
+
eligible" at exit 0 for an index, lockfile or database they lacked permission
|
|
20
|
+
on now raise `DATA_DIR_UNREADABLE` (exit 78) naming the path, errno, mode,
|
|
21
|
+
owner and running uid. *Affected:* anyone whose data dir is partly unreadable
|
|
22
|
+
— most often a `$XDG_DATA_HOME` shared across uids. *Remedy:* fix the
|
|
23
|
+
ownership or mode the error names, or point `AKM_DATA_DIR` somewhere this
|
|
24
|
+
user owns. The old behaviour was a false success, so a script that treated
|
|
25
|
+
exit 0 as "no results" was already being lied to.
|
|
26
|
+
|
|
27
|
+
- **Lockfile writes refuse to run against an unreadable `akm.lock`.**
|
|
28
|
+
`akm bundle add` / `remove` / `update` now fail closed instead of reading the
|
|
29
|
+
lock as empty and writing the single incoming entry over the whole record.
|
|
30
|
+
*Remedy:* as above. This one prevented real data loss — see Fixed.
|
|
31
|
+
|
|
32
|
+
- **`akm workflow run` exits 1 when a run ends `blocked`.** Previously 0.
|
|
33
|
+
*Affected:* CI steps and scheduled wrappers that branched only on `failed`.
|
|
34
|
+
*Remedy:* treat nonzero as "not verified"; resume with
|
|
35
|
+
`akm workflow resume <id>`.
|
|
36
|
+
|
|
37
|
+
- **`akm index --clean` no longer deletes entries whose file it cannot read.**
|
|
38
|
+
It keeps and names them. *Affected:* anyone relying on `--clean` to prune
|
|
39
|
+
aggressively; it is now conservative where it cannot see.
|
|
40
|
+
|
|
41
|
+
- **Workflow documents are bounds-checked at authoring time.** `engine:` name
|
|
42
|
+
grammar, `retry.max` 0–100, `gate.max_loops` 1–100, `map.concurrency` and
|
|
43
|
+
`engines.<name>.concurrency` 1–64, and any `timeout:` ≤ 2 147 483 647 ms are
|
|
44
|
+
now enforced by the parser. *Affected:* documents that parsed at 0.9.0 but
|
|
45
|
+
could never actually run — the frozen-plan decoder already refused them.
|
|
46
|
+
*Remedy:* edit the offending field; the error is now line-anchored.
|
|
47
|
+
|
|
48
|
+
- **`akm health` no longer emits `secret-file-perms`, and no longer exits 4 for
|
|
49
|
+
it.** The check is gone. *Affected:* anything parsing health output for that
|
|
50
|
+
check name.
|
|
51
|
+
|
|
52
|
+
- **Command-target task logs are now redacted.** Output that previously
|
|
53
|
+
persisted verbatim may now contain `[REDACTED]`. *Affected:* anything
|
|
54
|
+
grepping task logs for values that are now recognised as secrets.
|
|
55
|
+
|
|
56
|
+
- **Leftover `isolation: worktree` trees are garbage-collected after 7 days.**
|
|
57
|
+
*Remedy:* copy anything you want to keep out of a retained worktree within a
|
|
58
|
+
week.
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- **`exec` workflow units — run a shell command as a workflow step.** A step
|
|
63
|
+
whose `unit:` block declares `exec:` runs a command directly instead of
|
|
64
|
+
dispatching to an LLM or an agent, so deterministic work (test suites,
|
|
65
|
+
builds, lint, scripts) no longer costs a model dispatch, its latency, its
|
|
66
|
+
tokens, or its nondeterminism.
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
- id: test
|
|
70
|
+
unit:
|
|
71
|
+
exec:
|
|
72
|
+
command: ["bun", "run", "test:unit"]
|
|
73
|
+
pass_env: [CARGO_HOME] # optional: widen the default env allowlist
|
|
74
|
+
timeout: "10m"
|
|
75
|
+
retry: { max: 1, on: [timeout] }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- **`command:` is an argv array; there is no shell-string spelling.** The
|
|
79
|
+
child is spawned directly, so `;`, `|`, `&&`, `$(…)` and `*` inside an
|
|
80
|
+
argument are inert literal bytes — the quoting/injection class is
|
|
81
|
+
structurally absent, not defended against. Write `["bash", "-lc", "…"]`
|
|
82
|
+
when a pipeline is genuinely wanted, and own that choice in the diff.
|
|
83
|
+
- **An exec unit names no engine.** It rejects `engine`/`model`/`llm`, spends
|
|
84
|
+
no tokens, and a workflow made only of exec steps runs on an install with
|
|
85
|
+
no engine configured at all.
|
|
86
|
+
- **Everything else about a unit still applies:** `timeout`, `retry`,
|
|
87
|
+
`on_error`, `output`, `env`, `isolation: worktree`, `map` fan-out and its
|
|
88
|
+
concurrency limits, the unit journal, budget accounting, and replay/reuse
|
|
89
|
+
(a completed exec unit is never re-run on resume).
|
|
90
|
+
- **Output rule:** stdout is the promoted artifact with trailing newlines
|
|
91
|
+
stripped (like shell `$(…)`); with an `output:` schema on the unit, stdout
|
|
92
|
+
must be exactly one JSON value, strictly parsed and validated. stderr is a
|
|
93
|
+
diagnostic channel only. A schema miss is *not* re-prompted — a fixed argv
|
|
94
|
+
cannot answer feedback, but re-running it could deploy twice.
|
|
95
|
+
- **Exit codes:** non-zero → `non_zero_exit`, wall-clock expiry → `timeout`,
|
|
96
|
+
cancellation → `aborted`, failure to start → `spawn_failed`. Those are
|
|
97
|
+
pre-existing `retry.on` reasons. With the default `on_error: fail`, a
|
|
98
|
+
non-zero exit fails the step and the run, which is what makes a `test` step
|
|
99
|
+
a gate.
|
|
100
|
+
- **A partial capture is never promoted as the artifact.** Exiting 0 does not
|
|
101
|
+
prove stdout was read to the end: a pipe can error, and a background
|
|
102
|
+
descendant holding the stdout handle open after the command leader exits
|
|
103
|
+
keeps the pipe alive past the drain deadline. Both leave a *prefix* of the
|
|
104
|
+
real output, so the unit fails — with its own reason,
|
|
105
|
+
`exec_capture_incomplete`, which is deliberately **not** a `retry.on` value.
|
|
106
|
+
The command already ran; re-dispatching identical argv to fix a capture
|
|
107
|
+
problem would run its side effects a second time.
|
|
108
|
+
- **Everything the command can spend is bounded — without inventing failures.**
|
|
109
|
+
Alongside the wall-clock timeout, akm bounds the memory it spends on the
|
|
110
|
+
command's behalf and the environment it can hand the command. Both bounds are
|
|
111
|
+
built so that they only ever *explain* a failure that was going to happen
|
|
112
|
+
anyway; neither fails a run that would otherwise have succeeded.
|
|
113
|
+
|
|
114
|
+
**Retained output: 8 MiB per stream, drain-and-discard.** akm keeps at most
|
|
115
|
+
8 MiB of stdout and 8 MiB of stderr. Past the cap it keeps *reading* the pipe
|
|
116
|
+
and throws the extra bytes away, so the child never blocks on backpressure:
|
|
117
|
+
the command runs to completion and its real exit code decides the unit. A
|
|
118
|
+
verbose-but-passing test suite is not failed over its log volume. What
|
|
119
|
+
overflow costs is completeness of the artifact, and that is never hidden —
|
|
120
|
+
a step with **no** `output:` schema succeeds and its artifact is the retained
|
|
121
|
+
head with a `__akm_exec_output_truncated__` block appended (naming bytes
|
|
122
|
+
written vs bytes retained), so truncated data can never be mistaken for
|
|
123
|
+
complete data by `steps.<id>.output`, a gate judge, or a human. A step **with**
|
|
124
|
+
an `output:` schema still fails `exec_output_limit`: stdout must parse as
|
|
125
|
+
exactly one JSON value, a truncated prefix cannot, and promoting it would
|
|
126
|
+
corrupt every downstream reference to the typed artifact.
|
|
127
|
+
|
|
128
|
+
**Context environment: this platform's ceiling, not the smallest one.** The
|
|
129
|
+
engine-authored `AKM_*` context is capped at **96 KiB per variable / 128 KiB
|
|
130
|
+
total** on Linux, macOS and BSD, and at **32 767 bytes per variable / 64 000
|
|
131
|
+
bytes total** on Windows. The numbers cite their sources: Linux's
|
|
132
|
+
`MAX_ARG_STRLEN` (`32 * PAGE_SIZE` = 131 072 bytes per `argv`/`environ`
|
|
133
|
+
string), macOS's 256 KiB `ARG_MAX` over argv + environ combined, and Win32
|
|
134
|
+
`SetEnvironmentVariable`'s 32 767-character per-variable limit. Crossing the
|
|
135
|
+
bound fails `exec_context_too_large` *before* the spawn, with an error naming
|
|
136
|
+
the variable, its size, this platform's limit and where that limit comes from
|
|
137
|
+
— replacing a bare `E2BIG` from the spawn syscall that named neither the
|
|
138
|
+
variable nor the data behind it. Converting that inevitable failure into an
|
|
139
|
+
actionable one is the check's *only* job, so it uses the ceiling of the
|
|
140
|
+
platform the run is on: previously it applied Windows' limit everywhere and
|
|
141
|
+
refused spawns Linux and macOS would have accepted. Workflows that must also
|
|
142
|
+
run on Windows should stay under the smaller bound — that is documented
|
|
143
|
+
guidance now, not something a Linux host enforces.
|
|
144
|
+
|
|
145
|
+
`exec_output_limit` and `exec_context_too_large` keep their meanings and
|
|
146
|
+
their place outside the `retry.on` vocabulary, alongside `exec_cwd_escape`:
|
|
147
|
+
each is deterministic, so re-dispatching could only spend the budget again.
|
|
148
|
+
`PROGRAM_RETRY_REASONS` is unchanged.
|
|
149
|
+
- **A failing command's stderr survives to a durable surface.** The unit
|
|
150
|
+
journal now keeps each failed unit's redacted diagnostic (clipped to 2000
|
|
151
|
+
characters), and the step summary carries the first failure's. For an exec
|
|
152
|
+
unit that is the difference between `akm workflow status --units` saying
|
|
153
|
+
`non_zero_exit` and it saying *why* — a command that explains itself only on
|
|
154
|
+
stderr with empty stdout previously left no diagnostic anywhere durable.
|
|
155
|
+
This is an output surface only: the unit input hash is computed from
|
|
156
|
+
plan-frozen inputs, so no completed unit re-dispatches because of it.
|
|
157
|
+
- **The child's environment is an ALLOWLIST, not an inheritance.** The
|
|
158
|
+
command starts from an empty environment and receives `PATH`, `HOME`, the
|
|
159
|
+
identity/locale/temp/terminal variables, the Windows process-creation
|
|
160
|
+
essentials (`SystemRoot`, `SystemDrive`, `WINDIR`, `COMSPEC`, `PATHEXT`)
|
|
161
|
+
and the Windows home/config roots, plus `AKM_EVENT_SOURCE` — then the
|
|
162
|
+
unit's `env:` bindings, then the `AKM_*` context. `exec.pass_env: [NAME…]`
|
|
163
|
+
adds a few more names (for a per-machine toolchain variable like
|
|
164
|
+
`CARGO_HOME`, which a committed `env:` asset cannot express);
|
|
165
|
+
`exec.inherit_env: true` opts all the way back into akm's whole
|
|
166
|
+
environment. Both keys live inside `exec:` because the unit-level `env:`
|
|
167
|
+
key already means "env asset binding refs", and both are dispatch-
|
|
168
|
+
significant, so both are in the input hash.
|
|
169
|
+
|
|
170
|
+
This is not a claim to stop a determined attacker — a command that runs at
|
|
171
|
+
all can read the same credentials off disk. It bounds **accidental**
|
|
172
|
+
exposure (the invoking shell or CI job routinely exports tokens for
|
|
173
|
+
unrelated services), makes the environment surface **explicit and
|
|
174
|
+
reviewable**, and **matches the convention akm already applies** to
|
|
175
|
+
agent-harness children (`profile.envPassthrough`), which now share one
|
|
176
|
+
mechanism with exec units instead of two.
|
|
177
|
+
- **Security:** commands run inside the existing workflow trust model.
|
|
178
|
+
Secrets come from `env:` bindings by NAME — the frozen plan and the replay
|
|
179
|
+
hash carry only ref names, and resolved values are scrubbed from stdout,
|
|
180
|
+
stderr, and failure diagnostics by the same redaction contract every other
|
|
181
|
+
dispatch uses, before anything is journaled. `cwd:` is relative and
|
|
182
|
+
`..`-free, re-checked against the resolved base (symlinks included) before
|
|
183
|
+
spawning.
|
|
184
|
+
- **Cancellation is real:** the child is spawned in its own process group and
|
|
185
|
+
gets a SIGTERM→SIGKILL ladder on timeout or abort, so `--timeout` / Ctrl-C
|
|
186
|
+
stop a running command without orphaning its children.
|
|
187
|
+
- **No replay churn:** the exec spec was added to the unit input-hash preimage
|
|
188
|
+
as a key present only on exec units, so `hashVersion` stays 4 and every
|
|
189
|
+
previously-frozen llm/agent/sdk unit hashes byte-identically — runs already
|
|
190
|
+
in flight neither re-dispatch nor diverge. The env-scope keys are inside
|
|
191
|
+
that same spec and are frozen only in their non-default form (`inherit_env`
|
|
192
|
+
only when `true`, `pass_env` only when non-empty), so an exec unit that says
|
|
193
|
+
nothing about its environment hashes byte-identically too.
|
|
194
|
+
|
|
195
|
+
See [Workflow Schema: Exec (shell) units](docs/reference/workflow-schema.md#exec-shell-units)
|
|
196
|
+
and the worked example in
|
|
197
|
+
[Author's Guide](https://github.com/itlackey/akm/blob/main/docs/guides/author-workflows.md#deterministic-steps-run-a-command-gate-on-it).
|
|
198
|
+
|
|
199
|
+
### Changed
|
|
200
|
+
|
|
201
|
+
- **`akm workflow run` now exits non-zero when the run ends `blocked`.** A
|
|
202
|
+
verification judge that throws, cannot be resolved, or returns a malformed
|
|
203
|
+
verdict stops the run `blocked` — unverified, and resumable with `akm
|
|
204
|
+
workflow resume <id>`. That previously exited 0, so a CI step or scheduled
|
|
205
|
+
wrapper read an unverified run as a passing one. It now exits 1, matching
|
|
206
|
+
`failed` and gate rejections, and matching how the scheduled-task path
|
|
207
|
+
already reported it.
|
|
208
|
+
|
|
209
|
+
- **Workflow dispatch bounds are enforced at authoring time, not only by the
|
|
210
|
+
frozen-plan decoder.** `engine:` names must match the decoder's own grammar
|
|
211
|
+
(lowercase dash-separated letters/digits, starting with a letter, ≤63
|
|
212
|
+
chars); `retry.max` is 0–100; `gate.max_loops` is 1–100; `map.concurrency`
|
|
213
|
+
and `engines.<name>.concurrency` are 1–64; any `timeout:` must resolve to at
|
|
214
|
+
most 2 147 483 647 ms (~24.8 days, `setTimeout`'s 32-bit ceiling). Every one
|
|
215
|
+
of these was already refused by the frozen-plan decoder, so such a document
|
|
216
|
+
could never actually run — but it *parsed*, so `akm lint`, `akm workflow
|
|
217
|
+
show` and `akm workflow create` all reported it clean and the failure arrived
|
|
218
|
+
at `workflow run` as an unlocated "Invalid frozen workflow plan". The error is
|
|
219
|
+
now line-anchored at parse time. Nothing changes for a document already
|
|
220
|
+
inside the bounds.
|
|
221
|
+
|
|
222
|
+
- **`akm lint` gained an advisory channel.** The result envelope carries
|
|
223
|
+
`warnings: LintIssue[]` alongside `fixed`/`flagged`, `summary` gains a
|
|
224
|
+
`warnings` count, and text output prints a `warnings` section. Advisories
|
|
225
|
+
never route into `flagged`, so `--fail-on-flagged` cannot fail a run over
|
|
226
|
+
one. Workflow compile advisories (`workflow-warning`) are surfaced for the
|
|
227
|
+
first time — a step with no `output:` schema, a `params.<name>` reference to
|
|
228
|
+
an undeclared param, a `gate.max_loops` above 1 on an `exec` step — so a
|
|
229
|
+
bundle that linted clean at 0.9.0 may now report warnings without becoming a
|
|
230
|
+
failure. Findings that know a location carry `line` in `--format json` and
|
|
231
|
+
render as `file:line` in text. A new `lint-failed` code reports a file the
|
|
232
|
+
sweep reached but could not finish.
|
|
233
|
+
|
|
234
|
+
- **Leftover `isolation: worktree` trees are now garbage-collected.** A run
|
|
235
|
+
that crashed, or one whose worktree was retained after a dirty unit, used to
|
|
236
|
+
leave its tree under the worktrees root forever. akm now opportunistically
|
|
237
|
+
removes such trees once they are 7 days old, confined to the worktrees root,
|
|
238
|
+
symlinks skipped, containment re-checked. A worktree still in use is never
|
|
239
|
+
collected: every live tree carries a liveness marker (pid, host, resolved
|
|
240
|
+
path) in git's administrative directory for it, and the sweep skips a
|
|
241
|
+
candidate whose holder is still running here.
|
|
242
|
+
|
|
243
|
+
- **The workflow JSON Schema subset now enforces `allOf`/`anyOf`/`oneOf`/`not`.**
|
|
244
|
+
A step `output:` or `params:` schema may use the combinators, and the runtime
|
|
245
|
+
now evaluates them. Previously it ignored them: a schema using one was
|
|
246
|
+
accepted and simply constrained less than it appeared to. Evaluation stays
|
|
247
|
+
bounded — nesting is capped at 64 levels and one validation at 100 000 checks,
|
|
248
|
+
and exhausting either is reported as an error rather than a truncated pass.
|
|
249
|
+
|
|
250
|
+
**This one reaches runs already in flight.** The combinators live in the
|
|
251
|
+
frozen plan, which the decoder still accepts unchanged, so a run frozen before
|
|
252
|
+
the upgrade is resumed against the *new* evaluation: an artifact that passed
|
|
253
|
+
when the combinators were ignored can fail validation now. There is no
|
|
254
|
+
`irVersion` bump to gate it, because the plan bytes did not change — only what
|
|
255
|
+
they mean. Runs whose schemas use no combinators are unaffected, as is every
|
|
256
|
+
step already completed.
|
|
257
|
+
|
|
258
|
+
`pattern` is **not** part of the subset. It is a recognized-but-unsupported
|
|
259
|
+
keyword like `format` or `const`: using one is a loud, line-anchored authoring
|
|
260
|
+
error naming the keyword, so no schema silently fails to constrain what it
|
|
261
|
+
looks like it constrains. Enforcing it would mean screening every author
|
|
262
|
+
regex for catastrophic backtracking before the match — and any such screen
|
|
263
|
+
also refuses regexes authors legitimately write (the usual hand-rolled email
|
|
264
|
+
pattern among them), which is authoring friction with no workflow asking for
|
|
265
|
+
it. Where a string's shape matters, `enum` lists the allowed values,
|
|
266
|
+
`minLength`/`maxLength` bound the size, and a step's `### gate` rubric can
|
|
267
|
+
check a shape and explain a mismatch. The `format` hint now points at `enum`
|
|
268
|
+
rather than at `pattern`.
|
|
269
|
+
|
|
270
|
+
**Existing workflows that use one of these keywords must be edited before
|
|
271
|
+
they load again.** They previously parsed — the keyword was silently
|
|
272
|
+
non-constraining — so a workflow carrying `format: date-time` or `pattern:`
|
|
273
|
+
ran fine and now fails to parse for every caller: `workflow run`, `workflow
|
|
274
|
+
show`, `workflow create`, and `akm lint`. The quietest surface is `akm index`,
|
|
275
|
+
which skips an asset it cannot parse with a scan warning, so the workflow
|
|
276
|
+
simply stops appearing in the stash index. A run already frozen from such an
|
|
277
|
+
asset still resumes — the frozen-plan decoder does not re-screen keywords —
|
|
278
|
+
so resuming works while re-creating the same workflow errors until it is
|
|
279
|
+
edited.
|
|
280
|
+
|
|
281
|
+
- **A document-level `defaults.llm` is now rejected at freeze when any step
|
|
282
|
+
resolves onto an agent engine**, naming the step and the engine. The guard
|
|
283
|
+
existed before but was unreachable: overrides were computed only for `llm`
|
|
284
|
+
engines, so `defaults.llm` on a document with an agent step was silently
|
|
285
|
+
DROPPED for that step — the run proceeded with the author's sampling settings
|
|
286
|
+
quietly discarded. Failing loudly is the point, but it means a document that
|
|
287
|
+
mixes `defaults.llm` with any agent-engine step no longer freezes.
|
|
288
|
+
|
|
289
|
+
There is no per-step opt-out: `llm: {}` is a no-op, `llm: null` is a parse
|
|
290
|
+
error, and the layer merge is additive. Move the `llm:` block from
|
|
291
|
+
`defaults:` onto the `unit:` of each LLM step that wants it.
|
|
292
|
+
|
|
293
|
+
- **A scheduled workflow task now gets a 6-hour whole-run timeout by default.**
|
|
294
|
+
This applies to task files that declare no `timeoutMs:` — which is every task
|
|
295
|
+
file written before this release, since the key was previously rejected on
|
|
296
|
+
workflow targets. An unattended run that legitimately takes longer will be
|
|
297
|
+
aborted and the attempt reported failed on every firing until the task is
|
|
298
|
+
edited. `timeoutMs: null` opts out entirely, and any number overrides the
|
|
299
|
+
default. The abort itself is graceful: it lands at a step boundary, the
|
|
300
|
+
journal and lease are kept, and the run stays resumable with
|
|
301
|
+
`akm workflow resume <id>` — which the failure message names.
|
|
302
|
+
|
|
303
|
+
- **Workflow `map` steps now fan out in parallel by default.** A `map` step
|
|
304
|
+
that declares no `concurrency:` freezes a width of **4** instead of 1, and an
|
|
305
|
+
LLM engine that declares no `engines.<name>.concurrency` freezes **4** for a
|
|
306
|
+
remote endpoint (loopback endpoints stay at **1** — a local model server holds
|
|
307
|
+
one loaded model and returns HTTP 500 under concurrent inference). Both
|
|
308
|
+
defaults previously froze 1, which made every fan-out serial unless the author
|
|
309
|
+
opted in at two independent layers, and left `workflow.maxConcurrency` and the
|
|
310
|
+
host CPU cap binding on nothing.
|
|
311
|
+
|
|
312
|
+
This is a behavior change on a patch release, so every escape hatch is
|
|
313
|
+
explicit:
|
|
314
|
+
- `map.concurrency: 1` on a step is honored exactly as before — an authored
|
|
315
|
+
`1` is kept distinct from an unset field and always wins.
|
|
316
|
+
- New config key **`workflow.defaultMapConcurrency`** sets the default for
|
|
317
|
+
every workflow on the machine. `akm config set workflow.defaultMapConcurrency 1`
|
|
318
|
+
restores the pre-0.9.1 serial default wholesale.
|
|
319
|
+
- `engines.<name>.concurrency` pins any engine's own limit (and is now clamped
|
|
320
|
+
to `1..64` at freeze time instead of freezing a plan the decoder would then
|
|
321
|
+
refuse to load).
|
|
322
|
+
- **Runs already in flight are unaffected.** Both values are frozen into
|
|
323
|
+
`plan_json` when a run starts and the frozen-plan decoder requires them, so
|
|
324
|
+
a resumed run keeps the widths it began with. The new defaults apply only to
|
|
325
|
+
runs started after the upgrade.
|
|
326
|
+
|
|
327
|
+
The effective width remains the minimum of the step's `concurrency`, the run's
|
|
328
|
+
frozen `workflow.maxConcurrency`, the selected engine's concurrency, and the
|
|
329
|
+
current host's CPU cap.
|
|
330
|
+
|
|
331
|
+
- **`--max-steps` now counts steps, not engine-loop iterations.** The budget is
|
|
332
|
+
spent by the DISTINCT spine steps that finished — completed, failed, or
|
|
333
|
+
gate-rejected with the loop budget spent. It was previously spent by entries
|
|
334
|
+
in the `executed` report,
|
|
335
|
+
which gains one per gate-loop iteration and one per route-skip, so
|
|
336
|
+
`--max-steps 3` against a step with `gate.max_loops: 3` could stop after a
|
|
337
|
+
single step had finished, and an unselected branch target consumed budget for
|
|
338
|
+
work that was never dispatched. Three steps now means three steps, which is
|
|
339
|
+
what the flag has always said (`Stop after executing this many steps`). A step
|
|
340
|
+
the invocation left unfinished — an abort, a judge outage — still consumes
|
|
341
|
+
nothing, because the work is still owed. The same accounting is what a
|
|
342
|
+
`--max-retries` reopen subtracts, so loops and skips no longer shrink a
|
|
343
|
+
retry's remaining budget either, and `maxSteps:` in a workflow task file is
|
|
344
|
+
the same knob and moves with it. The count is now reported: `akm workflow run`
|
|
345
|
+
carries a `stepsProcessed` field alongside `executed`, so the number the
|
|
346
|
+
budget is spent on is visible rather than inferred from a list that counts
|
|
347
|
+
something else.
|
|
348
|
+
|
|
349
|
+
**This loosens the dispatch exposure of one invocation, and the loosening is
|
|
350
|
+
cumulative across steps.** A step's whole bounded gate loop now costs one step
|
|
351
|
+
instead of one per iteration, so the rounds a single `akm workflow run` can
|
|
352
|
+
dispatch go from roughly `N + max_loops` to `N × max_loops`.
|
|
353
|
+
|
|
354
|
+
What did **not** change is what the flag bounds within one step. `--max-steps`
|
|
355
|
+
was never a cap on total dispatch rounds on either version: the budget is
|
|
356
|
+
tested only BETWEEN steps, so a single step's gate loop could always run out
|
|
357
|
+
its full `gate.max_loops` no matter how little budget was left. The per-step
|
|
358
|
+
ceiling is `gate.max_loops` (1–100); the whole-run ceilings are
|
|
359
|
+
`budget.max_units` and `budget.max_tokens`, which are seeded from the unit
|
|
360
|
+
journal and hold across resumes.
|
|
361
|
+
|
|
362
|
+
### Fixed
|
|
363
|
+
|
|
364
|
+
- **Upgrading Node after installing akm now explains itself.** A native binding
|
|
365
|
+
is built for the Node ABI present at install time, so upgrading Node major
|
|
366
|
+
versions afterwards leaves akm reporting a bare Node internals message —
|
|
367
|
+
*"The module … was compiled against a different Node.js version"*, or on a
|
|
368
|
+
second attempt the even less helpful *"Module did not self-register"*. akm now
|
|
369
|
+
recognises that failure and answers with the one command that fixes it
|
|
370
|
+
(`npm rebuild better-sqlite3`), names the ABI actually running, and says
|
|
371
|
+
plainly that this is not a broken install.
|
|
372
|
+
|
|
373
|
+
The diagnostic had to move to do this. It wrapped the `require`, but
|
|
374
|
+
`require("better-sqlite3")` **succeeds** against a mismatched binding — the
|
|
375
|
+
package resolves its `.node` file lazily — so the error lands at
|
|
376
|
+
`new Database(...)` and the loader's handler never saw it. The previous text
|
|
377
|
+
telling the user to look for a version mismatch "in the error below" was
|
|
378
|
+
unreachable. Found by installing the published build under Node 22 and running
|
|
379
|
+
it under Node 24.
|
|
380
|
+
|
|
381
|
+
- **akm's Node fallback no longer aborts at teardown on Node 24.** On Node
|
|
382
|
+
24.19.0 and later, any command that opened a database could intermittently
|
|
383
|
+
die with `node::RemoveEnvironmentCleanupHook … Assertion (env) != nullptr`
|
|
384
|
+
and exit 134 — after its work was done, so the failure looked random and
|
|
385
|
+
depended on garbage-collection timing.
|
|
386
|
+
|
|
387
|
+
The cause was upstream and nothing to do with akm's own code.
|
|
388
|
+
`better-sqlite3` ships one prebuilt binary per Node ABI and falls back to
|
|
389
|
+
`node-gyp rebuild` when none matches, and the 11.x line publishes no prebuild
|
|
390
|
+
for Node 24 — so installing it there silently compiled the driver from source.
|
|
391
|
+
Node 24.19.0 had just changed the public `node_object_wrap.h` so that
|
|
392
|
+
`ObjectWrap`'s constructor and destructor register and unregister an
|
|
393
|
+
environment cleanup hook; a binding compiled against those headers
|
|
394
|
+
unregisters the hook after the environment is already gone, and aborts from
|
|
395
|
+
V8's teardown path. Only the Node 24 line was affected, and only from that
|
|
396
|
+
release on.
|
|
397
|
+
|
|
398
|
+
akm now pins `better-sqlite3` to `12.11.1`, which publishes prebuilt binaries
|
|
399
|
+
for Node 22, 24, 25 and 26 — so no Node version akm supports compiles the
|
|
400
|
+
driver at all. This affected real installs, not just CI: an npm user on Node
|
|
401
|
+
24 LTS was getting the same crash-prone from-source build.
|
|
402
|
+
|
|
403
|
+
The Node-fallback CI job now installs the exact spec `package.json` declares
|
|
404
|
+
instead of carrying a range of its own, and both that job and the smoke
|
|
405
|
+
script fail loudly on a native crash banner — previously an abort was
|
|
406
|
+
reported only as missing output, and the one step that tolerates a non-zero
|
|
407
|
+
exit would not have failed at all.
|
|
408
|
+
|
|
409
|
+
- **A website source interrupted mid-refresh no longer loses the snapshot it
|
|
410
|
+
already had.** A refresh deleted the whole mirror and then rebuilt it page by
|
|
411
|
+
page, so a process killed inside that loop left an empty or partial directory
|
|
412
|
+
with the old content already gone — and the freshness marker still looked
|
|
413
|
+
recent, so the next `sync()` served the wreckage instead of rebuilding. The
|
|
414
|
+
new snapshot is built in a dot-prefixed sibling directory and swapped in with
|
|
415
|
+
renames: an interrupted refresh leaves the PREVIOUS complete snapshot
|
|
416
|
+
untouched. Abandoned staging directories are dot-prefixed so the indexer's
|
|
417
|
+
walk skips them, and are swept by the next refresh once an hour old.
|
|
418
|
+
|
|
419
|
+
- **A resumed workflow run no longer re-dispatches work that already ran.** The
|
|
420
|
+
single-driver guard was checked at the run level, so a run whose lease had
|
|
421
|
+
been stolen left its still-owned unit row `running` and discarded the real
|
|
422
|
+
outcome — the resume then re-dispatched a unit that had already executed its
|
|
423
|
+
side effects and already spent its tokens. The guard now lives on the row, so
|
|
424
|
+
a stale driver's finish matches nothing and a live outcome is never dropped.
|
|
425
|
+
|
|
426
|
+
- **Lowering `retry.max` no longer re-runs finished work.** The completed-attempt
|
|
427
|
+
scan matched only attempts the *current* retry policy could have produced, so
|
|
428
|
+
reducing `retry.max` between invocations hid a journaled `~rN` row and the
|
|
429
|
+
unit was dispatched again. It now matches any journaled attempt of the unit.
|
|
430
|
+
|
|
431
|
+
- **A scheduled `command` task no longer writes your secrets into its log.**
|
|
432
|
+
Task logs were scrubbed for credential *shapes* — `Bearer …`, `sk-…`, webhook
|
|
433
|
+
URLs — but only prompt- and workflow-target runs also scrubbed exact secret
|
|
434
|
+
*values*. A command that echoed a configured secret shaped like nothing in
|
|
435
|
+
particular persisted it verbatim into both the run `.log` and `logs.db`, for
|
|
436
|
+
the whole retention window. Exact-value redaction now runs in the one sink all
|
|
437
|
+
three target kinds share, so every task kind is covered.
|
|
438
|
+
|
|
439
|
+
akm treats a value as secret when your config declares it
|
|
440
|
+
(`engines.<name>.apiKey`, `embedding.apiKey`, and the
|
|
441
|
+
`AKM_ENGINE_<NAME>_API_KEY` / `AKM_LLM_API_KEY` / `AKM_EMBED_API_KEY`
|
|
442
|
+
recipes), and infers others from the variable name (`*_TOKEN`, `*_SECRET`,
|
|
443
|
+
`*_API_KEY`, `*_PASSWORD`, …) when the value is at least 8 characters. The
|
|
444
|
+
floor applies only to the *guesses*: a declared secret is redacted at any
|
|
445
|
+
length. Redaction replaces substrings, so an over-eager rule does real damage
|
|
446
|
+
— treating every non-allowlisted variable in the inherited environment as a
|
|
447
|
+
secret classified 127 of 132 variables as credentials, 25 of them one
|
|
448
|
+
character long, and turned `3 tests passed, 0 failed` into `[REDACTED] tests
|
|
449
|
+
passed, [REDACTED] failed`.
|
|
450
|
+
|
|
451
|
+
For a secret exported under a name none of those rules recognise, any task may
|
|
452
|
+
name it:
|
|
453
|
+
|
|
454
|
+
```yaml
|
|
455
|
+
command: ./deploy.sh
|
|
456
|
+
redact: [ACME_DEPLOY_TOKEN] # NAMES, never values — max 32
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Names only, and a name that is unset at run time contributes nothing. A
|
|
460
|
+
literal secret in a task file would leak far more widely than the redaction
|
|
461
|
+
closes: task files are indexed into the search database, can be sent to an
|
|
462
|
+
embedding provider, are printed verbatim by `akm show`, and ship inside
|
|
463
|
+
bundles over git and npm — the same rule exec units' `pass_env:` follows.
|
|
464
|
+
|
|
465
|
+
- **Redacting a log can no longer explode it.** Exact-value redaction took a
|
|
466
|
+
fast path that rewrote the text once per secret, over an accumulator it had
|
|
467
|
+
already rewritten — so a secret containing any of the letters in `[REDACTED]`
|
|
468
|
+
matched the tokens it had just inserted, and the output grew geometrically.
|
|
469
|
+
Fifty characters against six single-letter values produced 32,450 characters,
|
|
470
|
+
a 649x blowup reachable from ordinary command output. Matches are now found
|
|
471
|
+
against the original text and the result emitted once. Overlapping matches
|
|
472
|
+
merge into a single `[REDACTED]`, and the two redaction paths no longer
|
|
473
|
+
disagree about output shape depending on whether the text happened to contain
|
|
474
|
+
a `%`.
|
|
475
|
+
|
|
476
|
+
- **Redacting a structured value no longer drops fields.** When two distinct
|
|
477
|
+
object keys redacted to the same string, the rebuilt object silently kept only
|
|
478
|
+
the last — `{a, b, ab}` came back with two entries, one of them simply gone
|
|
479
|
+
rather than redacted. Colliding keys are now suffixed, so the value survives
|
|
480
|
+
with its key still hidden. This affected persisted improve results and
|
|
481
|
+
journaled workflow outcomes.
|
|
482
|
+
|
|
483
|
+
- **`akm improve` auto-sync now commits exactly the files the run wrote.**
|
|
484
|
+
Every akm write path records the file it mutated into a run-scoped
|
|
485
|
+
write-provenance journal, and the end-of-run (and crash-path) commit stages
|
|
486
|
+
precisely those paths. A managed-directory file someone else edits while a
|
|
487
|
+
long run is in flight is left dirty for its author instead of being swept into
|
|
488
|
+
akm's commit, and a file that was already dirty when the run started and was
|
|
489
|
+
then rewritten by the run is now committed instead of being silently skipped.
|
|
490
|
+
Deletions are journaled like writes, so a path written and then reverted or
|
|
491
|
+
purged stages its final on-disk state — or produces no commit at all. The run
|
|
492
|
+
reports its journal as `writtenPaths` on the improve result, and the
|
|
493
|
+
`stash_synced` event gains `attributed` / `unattributed` counts. `akm sync` /
|
|
494
|
+
`akm push`, which supply no explicit path list, keep the managed-pathspec
|
|
495
|
+
fallback unchanged. (#652)
|
|
496
|
+
|
|
497
|
+
- **`akm lint` no longer reports a clean scan for a task file that cannot run.**
|
|
498
|
+
A `tasks/*.yml` whose YAML does not parse (bad indentation, an unterminated
|
|
499
|
+
quote, tab characters) produced `flagged: 0`: every task reader collapsed a
|
|
500
|
+
parse failure onto an empty mapping, and every task rule short-circuits on
|
|
501
|
+
one — so a CI gate on `--fail-on-flagged` passed a task that would die at
|
|
502
|
+
schedule time. The parse failure is now its own `invalid-task-yaml` finding.
|
|
503
|
+
A `tasks/*.yaml` file — a spelling akm never indexes and never schedules —
|
|
504
|
+
used to be skipped by the directory walk entirely; it is now collected and
|
|
505
|
+
flagged for the extension, with the rename in the message. Fixed on all three
|
|
506
|
+
task-lint surfaces (the CLI sweep, the `akm` adapter's `validate`, and the
|
|
507
|
+
`akm-task` format adapter) from one shared parse, so they cannot disagree.
|
|
508
|
+
|
|
509
|
+
- **`akm lint --fix` refuses a bundle configured `writable: false`.** Every
|
|
510
|
+
other mutating command checks the flag before touching disk; `--fix` wrote
|
|
511
|
+
directly and never consulted it, so it rewrote frontmatter in a bundle
|
|
512
|
+
explicitly marked read-only. It is now a usage error raised before any file
|
|
513
|
+
is modified.
|
|
514
|
+
|
|
515
|
+
- **A `--fix` write failure no longer aborts the run and hides the fixes that
|
|
516
|
+
already landed.** One unwritable file (read-only file, full disk) threw
|
|
517
|
+
straight out of `akm lint`, so the caller got an exception instead of a
|
|
518
|
+
result — with no way to tell which earlier files in the same sweep had
|
|
519
|
+
already been rewritten. A failed fix is now reported in-band on its own file
|
|
520
|
+
as `fixed: "failed"`, and the sweep continues through the rest of the bundle.
|
|
521
|
+
|
|
522
|
+
- **`akm lint --type` says so when it does nothing.** For a non-akm bundle the
|
|
523
|
+
adapter validates the whole bundle regardless of `--type`, so scoping a run
|
|
524
|
+
silently had no effect. It now warns, naming the flag and the adapter.
|
|
525
|
+
Findings are unchanged (full-bundle validation was already a superset), and
|
|
526
|
+
it is deliberately a warning, not an error, so scripts passing one `--type`
|
|
527
|
+
across mixed-adapter bundle sets keep working.
|
|
528
|
+
|
|
529
|
+
- **`missing-skill-md` fires again for an `agent-skills` package with no
|
|
530
|
+
manifest.** The check iterated pending CHANGES, and a change is always a
|
|
531
|
+
file — so a package directory holding resources but no `SKILL.md`
|
|
532
|
+
contributed nothing it could see, and a skills pack with a broken package
|
|
533
|
+
linted clean. It is now a real directory pass over the bundle root. Related:
|
|
534
|
+
under opencode's supported singular `skill/` alias the same package went
|
|
535
|
+
unflagged while an identical one under `skills/` was caught; both spellings
|
|
536
|
+
are now checked.
|
|
537
|
+
|
|
538
|
+
- **An index akm cannot read no longer reports as an index that does not
|
|
539
|
+
exist.** `fs.existsSync()` answers `false` for a permission error exactly as
|
|
540
|
+
it does for a missing file, and the read path used it as its "is there an
|
|
541
|
+
index?" gate — so `akm search` and `akm curate` returned no hits at **exit 0**
|
|
542
|
+
with the tip *"No search index available. Run 'akm index' to build one."* for
|
|
543
|
+
a populated index sitting right there on disk, and `akm info` reported
|
|
544
|
+
`entryCount: 0, vecAvailable: false` for the same index. Nothing said
|
|
545
|
+
"permission". A consuming agent had no way to tell that from a genuine empty
|
|
546
|
+
result, so it relayed the false answer to its user with an explanation it had
|
|
547
|
+
invented.
|
|
548
|
+
|
|
549
|
+
Absent and inaccessible are now distinct everywhere it matters:
|
|
550
|
+
|
|
551
|
+
- `search` / `curate` / the index openers raise a `ConfigError`
|
|
552
|
+
(`DATA_DIR_UNREADABLE`, exit 78) naming the path, the errno, the mode and
|
|
553
|
+
owner, and the uid actually running — instead of an empty success.
|
|
554
|
+
- `akm info` reports an `indexStats.unreadable` diagnostic rather than zeros
|
|
555
|
+
that look healthy. The field is absent on every healthy run.
|
|
556
|
+
- `akm health` now *diagnoses* an unreadable `state.db` as a failing
|
|
557
|
+
`state-db-readable` check instead of dying on the open before it could
|
|
558
|
+
report anything — it is the command you reach for when this happens.
|
|
559
|
+
- `probeLock` returns a distinct `inaccessible` state instead of classifying a
|
|
560
|
+
permission error as a stale lock. "I cannot read this lock" and "the holder
|
|
561
|
+
is dead" are opposite facts, and `akm improve` now stops rather than
|
|
562
|
+
reclaiming a lease that may be genuinely held.
|
|
563
|
+
|
|
564
|
+
The same conflation existed on the write paths, where the consequence was
|
|
565
|
+
worse than a wrong answer:
|
|
566
|
+
|
|
567
|
+
- **An unreadable `akm.lock` could destroy every bundle record in it.** The
|
|
568
|
+
lockfile read that exists specifically so a write path never sees `[]`
|
|
569
|
+
returned `[]` for *any* read failure, permission errors included — and
|
|
570
|
+
every lockfile write is read-modify-write, so the next atomic write
|
|
571
|
+
replaced the operator's whole lock record with the single entry being
|
|
572
|
+
added. Verified by probe: the symlink was replaced by a regular file
|
|
573
|
+
holding one entry. Lockfile writes now refuse to run against a lock they
|
|
574
|
+
cannot read.
|
|
575
|
+
- **The migration recovery gate failed open.** "I cannot tell whether a
|
|
576
|
+
recovery is pending" cleared the gate exactly as "no recovery is pending"
|
|
577
|
+
did, so akm would open the canonical databases on top of a half-applied
|
|
578
|
+
migration. It now fails closed.
|
|
579
|
+
- **`akm index --clean` deleted rows for files it merely could not look at**,
|
|
580
|
+
and reported the deletions as a clean success. Unreadable entries are now
|
|
581
|
+
kept and named.
|
|
582
|
+
- `indexWrittenAssets` returned `true` — "the index is as you expect" — for
|
|
583
|
+
an index it could not open, on the strength of which `acceptProposal`
|
|
584
|
+
advanced its journal to `index-finalized`.
|
|
585
|
+
- `akm improve` eligibility, `akm feedback`, `akm bundle list` and the graph
|
|
586
|
+
loaders each turned a permission fault into an empty result, a zero count,
|
|
587
|
+
or the advice to "Run `akm index` first".
|
|
588
|
+
|
|
589
|
+
- **akm no longer manages permissions on your data directory, its databases, or
|
|
590
|
+
your task logs — and no longer reports on them either.** Those take your
|
|
591
|
+
process umask; their mode is yours to set, and `chmod`/`umask` are your
|
|
592
|
+
levers.
|
|
593
|
+
|
|
594
|
+
This is scoped, not blanket: akm still creates a handful of files at
|
|
595
|
+
restrictive modes *at creation time*, as it always has — `env` and `secret`
|
|
596
|
+
assets and config backups at `0600`, their directories at `0700`, and the
|
|
597
|
+
scheduler invocation files it writes for cron/launchd/schtasks. Those are
|
|
598
|
+
files akm authors itself and whose contents are credentials; setting their
|
|
599
|
+
mode when creating them is not the same as re-permissioning a directory you
|
|
600
|
+
already owned.
|
|
601
|
+
|
|
602
|
+
Two 0.9.1 pre-release changes are gone. The first chmodded akm's databases
|
|
603
|
+
and task logs to `0600`/`0700` on every open — reverted because
|
|
604
|
+
re-permissioning a directory akm did not create silently broke installs that
|
|
605
|
+
share `$XDG_DATA_HOME` between two uids (agent sandboxes, containers, service
|
|
606
|
+
accounts). If a pre-release tightened your data directory, `chmod` it back.
|
|
607
|
+
The second was an `akm health` advisory (`secret-file-perms`) that reported
|
|
608
|
+
group/other-readable `env`, `secrets` and `config-backups` paths — removed
|
|
609
|
+
too: it is meaningless on Windows, and nagging about modes akm does not set
|
|
610
|
+
is not health reporting. `akm health` no longer emits this check, and no
|
|
611
|
+
longer exits `4` on account of it.
|
|
612
|
+
|
|
613
|
+
- **`timeout: none` on an exec unit is genuinely unbounded again.** The
|
|
614
|
+
stream-drain safety net — a one-hour bound on a pipe still being read after
|
|
615
|
+
the child is gone — was armed when capture STARTED, so a command that ran
|
|
616
|
+
past an hour had its output reader cancelled mid-run and was then failed for
|
|
617
|
+
an incomplete capture even though it exited 0. It is now armed from the
|
|
618
|
+
child's exit, which is the only window it was ever meant to bound.
|
|
619
|
+
|
|
620
|
+
- **A bounded exec unit no longer waits out its whole `timeout` after the
|
|
621
|
+
command has already exited.** The drain deadline for a unit WITH a wall budget
|
|
622
|
+
ran from the moment capture started — budget plus a 2 s grace — so a command
|
|
623
|
+
that exited in milliseconds while a background descendant held a pipe open
|
|
624
|
+
kept the unit, and with it a fan-out slot, occupied for the entire declared
|
|
625
|
+
timeout before reporting. It now runs from the moment nothing living owns the
|
|
626
|
+
pipe: the child's exit, or (for a child that outlived its own kill ladder) the
|
|
627
|
+
budget's expiry, plus the same 2 s grace. A command that really does spend its
|
|
628
|
+
whole budget sees the identical ceiling it saw before; only the case that used
|
|
629
|
+
to stall stopped stalling.
|
|
630
|
+
|
|
631
|
+
- **A stderr drain that never finished no longer fails an exec unit whose
|
|
632
|
+
command succeeded.** `exec_capture_incomplete` was raised when EITHER pipe
|
|
633
|
+
failed to drain, so a command that exited 0 with its stdout captured whole was
|
|
634
|
+
failed — and a valid artifact thrown away — because a background descendant
|
|
635
|
+
was still holding STDERR open. stderr is a diagnostic channel that never
|
|
636
|
+
contributes to the artifact, so only an incomplete STDOUT capture fails the
|
|
637
|
+
unit now; an incomplete stderr drain is reported on the warn stream instead,
|
|
638
|
+
naming the unit and warning that any stderr shown for it may be missing its
|
|
639
|
+
tail.
|
|
640
|
+
|
|
641
|
+
- **A step artifact larger than 1 MiB no longer breaks the next step of the run
|
|
642
|
+
that produced it.** Step evidence is clipped to bound one SQLite row, and the
|
|
643
|
+
engine rebuilt each downstream `steps.<id>.output` scope by re-reading those
|
|
644
|
+
rows — so a large artifact (an exec unit's stdout retains up to 8 MiB) reached
|
|
645
|
+
the very next step as a truncation marker: a path reference failed with a
|
|
646
|
+
missing-property error that never mentioned truncation, and a whole-value
|
|
647
|
+
reference silently handed the marker to the unit as its input. The run now
|
|
648
|
+
carries its own complete values forward; the row stays clipped for resume,
|
|
649
|
+
where a reference into a clipped artifact fails by name.
|
|
650
|
+
|
|
651
|
+
- **A workflow run that completed is no longer reported as timed out.** The
|
|
652
|
+
deadline is observed between steps, so one landing during a run's final
|
|
653
|
+
bookkeeping set the timed-out flag on a run that then finished. On a scheduled
|
|
654
|
+
workflow task that recorded the attempt as failed, with a hint to resume a run
|
|
655
|
+
that had nothing left to resume; under `akm workflow run --timeout` it
|
|
656
|
+
rendered a `timedOut` marker on a `completed` run and exited nonzero. Both
|
|
657
|
+
surfaces now drop the marker once the run reached `completed` — a deadline
|
|
658
|
+
that lands with nothing left to abort has nothing to report.
|
|
659
|
+
|
|
660
|
+
- **A rejected gate on an `exec` step no longer re-runs the command.** A gate
|
|
661
|
+
loop earns its re-dispatch by handing the judge's feedback to a unit that can
|
|
662
|
+
answer it. An exec unit cannot: its argv is frozen and never interpolated, and
|
|
663
|
+
the exec context environment carries no feedback variable — so the loop could
|
|
664
|
+
only re-run the byte-identical command, performing a deploy, a publish, or a
|
|
665
|
+
migration a second time for a verdict that could not change. The gate still
|
|
666
|
+
EVALUATES on an exec step and can still fail it: a rejection is final on the
|
|
667
|
+
first evaluation, carrying the judge's missing criteria and feedback exactly
|
|
668
|
+
as in the one-shot case. What an author sees is
|
|
669
|
+
that `gate.max_loops` is capped at 1 on a step whose unit is `exec:` — not an
|
|
670
|
+
authoring error, and no change at all to an engine step, where a declared
|
|
671
|
+
`max_loops` is still honored in full. This is the same reasoning that already
|
|
672
|
+
makes an exec unit's `output:` schema miss fail without a corrective
|
|
673
|
+
re-dispatch.
|
|
674
|
+
|
|
675
|
+
- **The stale-worktree sweep no longer collects a worktree that is still in
|
|
676
|
+
use.** The opportunistic age-based GC of leftover `isolation: worktree` trees
|
|
677
|
+
judged staleness from the worktree root's mtime, which a unit writing only
|
|
678
|
+
inside subdirectories never touches — so another akm process minting a
|
|
679
|
+
worktree could delete the tree a long-running unit was working in. Every live
|
|
680
|
+
worktree now carries a liveness marker (pid, host, resolved path) in git's own
|
|
681
|
+
administrative directory for it, and the sweep skips a candidate whose holder
|
|
682
|
+
is still running here. A marker from a dead pid, from another host, or for a
|
|
683
|
+
different path is not liveness: crashed runs and retained dirty trees stay
|
|
684
|
+
collectible, which is what the sweep exists for.
|
|
685
|
+
|
|
686
|
+
- On Windows, an agent CLI, gate judge, or prompt task was spawned into an
|
|
687
|
+
environment the loader cannot start from: the shared passthrough allowlist
|
|
688
|
+
named no `SystemRoot`/`SystemDrive`/`WINDIR`, and without `PATHEXT` a
|
|
689
|
+
`bin: "bun"` profile was unresolvable — while an exec unit on the same host
|
|
690
|
+
worked, because its own allowlist names them. Those variables are now added
|
|
691
|
+
when any allowlisted child environment is built.
|
|
692
|
+
|
|
693
|
+
- Scheduler PATH repair skipped itself in the environments it exists for. It
|
|
694
|
+
decided a PATH was "interactive" by testing whether any entry began with the
|
|
695
|
+
user's home directory as a *string*, so a home of `/` — system crontab,
|
|
696
|
+
launchd, service accounts — matched every absolute entry, and a sibling home
|
|
697
|
+
(`/home/alice/bin` against `/home/al`) matched too.
|
|
698
|
+
|
|
699
|
+
- A directory whose name merely begins with two dots (`..data`) was treated as
|
|
700
|
+
a path escape. For a workflow exec `cwd` that meant the parser and the frozen
|
|
701
|
+
plan accepted a spelling the executor then failed as tampering, with a reason
|
|
702
|
+
no retry can clear.
|
|
703
|
+
|
|
704
|
+
- `appendEvent` resolved the state.db path outside its own error handling, and
|
|
705
|
+
did so even when the caller supplied an open connection — so a caller holding
|
|
706
|
+
a perfectly good handle could take a configuration error from a function whose
|
|
707
|
+
contract is that it never propagates one.
|
|
708
|
+
|
|
709
|
+
- Workflow freeze attributed per-step `engine`/`model`/`timeout`/`llm` overrides
|
|
710
|
+
by matching the compiled draft step list against the source document
|
|
711
|
+
**positionally**. That was correct only because compilation happens to be 1:1
|
|
712
|
+
and order-preserving; a compile pass that filtered or reordered steps would
|
|
713
|
+
have silently applied one step's overrides to another. Attribution is now
|
|
714
|
+
keyed by `stepId`.
|
|
715
|
+
|
|
716
|
+
### Security
|
|
717
|
+
|
|
718
|
+
- **A gate judge's response is now scrubbed before it is journaled.** The judge
|
|
719
|
+
verdict is written into the gate row's `result_json`, and a judge failure's
|
|
720
|
+
message becomes the blocked step's notes — but the judge dispatch bypassed
|
|
721
|
+
the redaction contract every unit dispatch goes through, so a judge that
|
|
722
|
+
echoed a credential out of the promoted artifact persisted it unredacted into
|
|
723
|
+
the workflow journal. Both judge paths (agent and llm) now wrap their
|
|
724
|
+
dispatch in the same scrub, with the sensitive-value set collected per
|
|
725
|
+
dispatch rather than at build time, so a credential rotated between the two
|
|
726
|
+
reads is still caught. The dispatch also carries the real run/step/gate ids
|
|
727
|
+
instead of a synthetic `"gate"` placeholder, so a gate row and its telemetry
|
|
728
|
+
describe the same thing.
|
|
729
|
+
|
|
730
|
+
- **Command-target task logs are scrubbed of exact secret values**, closing the
|
|
731
|
+
last redaction lane — see the `### Fixed` entry above for the full account.
|
|
732
|
+
|
|
9
733
|
## [0.9.0] - 2026-08-06
|
|
10
734
|
|
|
11
735
|
0.9.0 is the format-neutral **bundle / adapter** refactor: it replaces the flat
|