akm-cli 0.9.3 → 0.9.5
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 +233 -1
- package/README.md +1 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/cli.js +5 -5
- package/dist/commands/health/improve-metrics.js +17 -0
- package/dist/commands/health/windows.js +2 -2
- package/dist/commands/health.js +2 -2
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/improve/preparation.js +8 -1
- package/dist/commands/lint/index.js +3 -7
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/tasks/tasks-cli.js +81 -3
- package/dist/commands/tasks/tasks.js +117 -2
- package/dist/core/adapter/adapters/akm-adapter.js +23 -14
- package/dist/core/adapter/adapters/akm-lint.js +3 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +9 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +13 -11
- package/dist/core/adapter/recognize-match.js +1 -20
- package/dist/core/asset/asset-placement.js +21 -2
- package/dist/core/common.js +21 -1
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +6 -6
- package/dist/core/improve-result.js +35 -14
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/db-search.js +6 -0
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/indexer/walk/matchers.js +0 -22
- package/dist/output/shapes/helpers.js +19 -1
- package/dist/output/shapes/passthrough.js +18 -5
- package/dist/output/text/command-format.js +4 -0
- package/dist/registry/pinned-request-helper.js +2 -2
- package/dist/registry/pinned-transport.js +6 -6
- package/dist/scripts/akm-migrate-node.js +12678 -12601
- package/dist/scripts/akm-migrate.js +12678 -12601
- package/dist/storage/repositories/proposals-repository.js +65 -7
- package/dist/storage/repositories/task-history-repository.js +22 -10
- package/dist/tasks/run/task-history.js +23 -3
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +45 -0
- package/dist/tasks/scheduler-sync.js +77 -41
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/parse-task-source.js +77 -11
- package/dist/tasks/source/task-source-v3-frozen.js +428 -0
- package/dist/tasks/source/task-to-v3.js +512 -0
- package/dist/tasks/source/task-to-v4.js +457 -0
- package/docs/reference/cli.md +33 -6
- package/docs/reference/configuration.md +27 -6
- package/docs/reference/tasks.md +10 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,239 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.9.5] - 2026-08-30
|
|
8
|
+
|
|
9
|
+
### Action required after upgrading
|
|
10
|
+
|
|
11
|
+
- **Run `akm index --full` once (#862).** Search ranking previously counted
|
|
12
|
+
every search *hit* as a small win for that entry's utility score, whether
|
|
13
|
+
or not you ever opened it — a plain impression, not a selection. Over
|
|
14
|
+
enough repeat searches this compounded into a real feedback loop: appear
|
|
15
|
+
in results -> score goes up -> rank higher next time -> appear again. On
|
|
16
|
+
the install this was diagnosed against, one entry had been searched 19
|
|
17
|
+
times, opened 0 times, and still carried a utility score of 0.83 — on par
|
|
18
|
+
with entries a user had actually picked every time; the single
|
|
19
|
+
highest-utility entry in the whole table had an 8% select rate. That bias
|
|
20
|
+
is baked into every existing install's stored scores and does not go away
|
|
21
|
+
on its own — the live bump has been removed (search impressions no longer
|
|
22
|
+
write utility scores at all; only an actual `akm show`/select or explicit
|
|
23
|
+
`akm feedback` feeds the offline recompute now), but the already-inflated
|
|
24
|
+
numbers stay in `utility_scores`/`utility_scores_scoped` until you
|
|
25
|
+
recompute them. Run `akm index --full` once after upgrading to rebuild
|
|
26
|
+
scores purely from selection rate and feedback. **Search result order will
|
|
27
|
+
change after that rebuild — that is intended**, not a regression.
|
|
28
|
+
- **The first `akm task sync` after upgrading may report a large number of
|
|
29
|
+
updates.** This is a backlog of reconciles that were being silently
|
|
30
|
+
refused, not new changes to your tasks. Two independent bugs combined to
|
|
31
|
+
make `sync` refuse work it should have done: (1) an installed scheduler
|
|
32
|
+
entry written by a pre-`--bundle` akm release couldn't prove ownership
|
|
33
|
+
under the newer, stricter check and was reported `(unproven owner)`,
|
|
34
|
+
which made `sync` refuse the *entire* run rather than reconcile everything
|
|
35
|
+
else; (2) separately, one task or workflow source that failed to compile
|
|
36
|
+
(e.g. a task still on schema v2 in a shape the shim can't convert) also
|
|
37
|
+
blocked every other, unrelated task from reconciling. Both are fixed —
|
|
38
|
+
ownership is now re-derived from the entry's own akm markers instead of
|
|
39
|
+
requiring a literal `--bundle` token, and a source that fails to compile
|
|
40
|
+
is now excluded and reported in `failed`/`failures` while every source
|
|
41
|
+
that DID compile still reconciles. On the install this was diagnosed
|
|
42
|
+
against, all 18 scheduled tasks showed up as updates on the first `sync`
|
|
43
|
+
after the fix — that is the backlog, not a sign your tasks changed.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **`akm task prune` reclaims orphaned scheduler entries `sync` cannot reach
|
|
48
|
+
(#851).** `akm task sync` only ever reconciles entries that resolve to a
|
|
49
|
+
desired task/workflow definition in a live bundle; an entry whose own
|
|
50
|
+
`--scheduler-context` descriptor is corrupt/missing, or whose owning
|
|
51
|
+
bundle directory has since been deleted, was permanently invisible to it
|
|
52
|
+
and had to be removed by hand-editing the crontab/launchd plist/Task
|
|
53
|
+
Scheduler. `akm task prune` finds those and nothing else: it never
|
|
54
|
+
touches an entry that still resolves to a live bundle, and there is no
|
|
55
|
+
`--force`/"remove everything silently" mode. Defaults to a dry-run
|
|
56
|
+
preview that makes zero scheduler writes and exits non-zero when it finds
|
|
57
|
+
removal candidates (usable as a CI/health-check guard, mirroring `task
|
|
58
|
+
sync --dry-run`'s exit-code convention); `--yes` executes the printed
|
|
59
|
+
plan; `--id a,b` narrows a run to specific binding ids and refuses (with
|
|
60
|
+
no writes) any id that isn't a current orphan candidate — including a
|
|
61
|
+
live entry, which cannot be pruned even if you name it explicitly.
|
|
62
|
+
- **`configVersion` read shim (#863).** Config loading now tolerates a
|
|
63
|
+
known older `configVersion` by upgrading the parsed document in memory
|
|
64
|
+
(never rewriting the file), with a one-line stderr warning naming the old
|
|
65
|
+
and new versions. The upgrade is silenced the next time any command
|
|
66
|
+
writes the config (`akm config set`, etc.), since every config write
|
|
67
|
+
already stamps the current version. Anything else — unknown, newer, or
|
|
68
|
+
malformed `configVersion` — still fails closed with the same actionable
|
|
69
|
+
error as before. No current release needed this shim yet (akm has only
|
|
70
|
+
ever shipped `configVersion: "0.9.0"`); it's in place now so the next
|
|
71
|
+
real bump doesn't break every existing config on upgrade.
|
|
72
|
+
- **Truncated indexed content is no longer silent.** The two indexer caps
|
|
73
|
+
that truncate an entry's content before it's written to the search index
|
|
74
|
+
(a markdown-body cap and a search-text cap) previously truncated without
|
|
75
|
+
any signal. Indexed entries now carry a `contentTruncated: true` flag
|
|
76
|
+
when either cap fired, and truncation logs a `--verbose` diagnostic
|
|
77
|
+
naming the entry, so an unexpectedly-worse search match on a very large
|
|
78
|
+
file has a visible cause instead of a silent one. The cap sizes
|
|
79
|
+
themselves are unchanged.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **`akm health --report` and `akm proposal list --status accepted` no
|
|
84
|
+
longer crash, and `improve`'s accepted-proposal counts are no longer
|
|
85
|
+
silently zero, on proposals written before every optional envelope field
|
|
86
|
+
existed (#859).** A prior fix already tolerated a missing `changes` key;
|
|
87
|
+
this closes the same gap for `proposedTarget`, which was still hard-required
|
|
88
|
+
at decode and — on the real archive this was checked against — absent
|
|
89
|
+
from 93% of accepted rows. Decoding a proposal now tolerates a genuinely
|
|
90
|
+
*absent* `proposedTarget` (accept-time resolution already had a
|
|
91
|
+
ref-derived fallback for this case, previously unreachable because decode
|
|
92
|
+
threw first); a *present but malformed* value still throws, so real
|
|
93
|
+
corruption is never silently accepted. Writing a new (`pending`) proposal
|
|
94
|
+
still requires the full envelope — this only widens what can be *read*,
|
|
95
|
+
not what akm will *write*.
|
|
96
|
+
- **`akm task sync` no longer refuses to reconcile an entry it can't fully
|
|
97
|
+
re-prove ownership of, and no longer lets one broken task/workflow source
|
|
98
|
+
block every other source (#867, plus the scheduler-ownership fix
|
|
99
|
+
described above).** The v2 task-source shim also no longer hard-fails a
|
|
100
|
+
command wrapped in `env NAME=value... cmd`, a shape that's common for
|
|
101
|
+
cron entries (e.g. `env AKM_BIN=/path/akm bash script.sh`) — the shim now
|
|
102
|
+
looks past a leading `env` and its assignments to the real command before
|
|
103
|
+
deciding whether the conversion to v3/v4 is safe, instead of checking
|
|
104
|
+
`env` itself.
|
|
105
|
+
- **`akm task sync --dry-run` no longer crashes on a real install with a
|
|
106
|
+
pre-`--bundle` cron entry.** The reconcile fix above made those entries
|
|
107
|
+
reachable for the first time, and the dry-run preview's frozen result
|
|
108
|
+
object was then mutated in place while stamping output metadata, throwing
|
|
109
|
+
"Attempting to define property on object that is not extensible" (exit
|
|
110
|
+
70). Output stamping now copies instead of mutating, so it tolerates a
|
|
111
|
+
frozen (or any) result uniformly.
|
|
112
|
+
- **`akm migrate apply` no longer aborts an entire batch because one file in
|
|
113
|
+
it is blocked (#866).** The task v2->v3 and v3->v4 migrators refused to
|
|
114
|
+
write *any* file — including files with no problem at all — the moment a
|
|
115
|
+
single file in the batch was classified `blocked`. Both migrators now
|
|
116
|
+
skip and report the blocked file(s) and still migrate everything else;
|
|
117
|
+
the run still exits non-zero whenever anything was skipped.
|
|
118
|
+
- Two flaky integration/unit tests fixed at the root cause rather than
|
|
119
|
+
quarantined (#864): a `state.db` byte-identity assertion that raced
|
|
120
|
+
SQLite's own WAL checkpoint timing (now forces a checkpoint before
|
|
121
|
+
comparing, so the assertion reflects real durable mutations only), and a
|
|
122
|
+
"resolveProjectContext when cwd is the home directory" test that did
|
|
123
|
+
real, un-mocked filesystem walks against the actual `$HOME` (now isolates
|
|
124
|
+
`os.homedir()` and the stash/XDG dirs like the rest of the suite).
|
|
125
|
+
|
|
126
|
+
### Changed
|
|
127
|
+
|
|
128
|
+
- **`test:integration` is green-by-default (#861).** Verified there is no
|
|
129
|
+
CI mechanism silently swallowing a failing test (no `continue-on-error`,
|
|
130
|
+
no lost exit codes, no allowlist of "expected" failures) — the fixes in
|
|
131
|
+
this release were the actual cause of the prior red baseline. Raised the
|
|
132
|
+
integration suite's minimum-test-count floor (`AKM_MIN_INTEGRATION_TESTS`,
|
|
133
|
+
5500 -> 5700) so a large silent test loss is still caught with headroom
|
|
134
|
+
for ordinary future deletions, and documented in `AGENTS.md` that `TMPDIR`
|
|
135
|
+
must be a real `/tmp`-family path for the suite to pass — some guards
|
|
136
|
+
(stash-path safety, `akm-eval`'s Docker twin) intentionally hardcode that
|
|
137
|
+
assumption rather than reading `TMPDIR`.
|
|
138
|
+
- Simplified several areas flagged in the #866 complexity review without
|
|
139
|
+
behavior changes: `improve_runs.result_json`'s `schemaVersion` decoding is
|
|
140
|
+
now an explicit, extensible table instead of one large inline
|
|
141
|
+
conditional, ready for a future schema bump to add a case rather than
|
|
142
|
+
restructure the function.
|
|
143
|
+
|
|
144
|
+
### Removed
|
|
145
|
+
|
|
146
|
+
- **Three defensive checks that only ever refused work a human explicitly
|
|
147
|
+
asked for, with no evidence (via telemetry) they ever caught a real
|
|
148
|
+
problem, were removed:**
|
|
149
|
+
- The 1 MiB hard cap on reading a bundle file (command/agent/script/task/
|
|
150
|
+
workflow source, or a frozen workflow secret/env source) into memory.
|
|
151
|
+
Reads remain exact and integrity-checked (hashed, CAS-verified, fail-
|
|
152
|
+
closed on read-time mutation) — there is simply no longer a size ceiling
|
|
153
|
+
that refuses to read a file you put in your own bundle.
|
|
154
|
+
- The task migrator's inode/hard-link-count/change-time identity
|
|
155
|
+
fencing on top of its existing lockfile + backup-before-write +
|
|
156
|
+
byte-for-byte drift check. The extra fencing modeled a multi-tenant
|
|
157
|
+
race that doesn't apply to a single-user CLI holding an exclusive lock,
|
|
158
|
+
and its failure mode (refusing to migrate a file that's byte-identical
|
|
159
|
+
to what was already previewed, or refusing to touch a file merely
|
|
160
|
+
because it has a hard link elsewhere) was worse than the risk it
|
|
161
|
+
guarded against. Symlink/path-escape rejection — a real hazard — is
|
|
162
|
+
unchanged.
|
|
163
|
+
- Dead anti-collapse "hard refusal" guard functions
|
|
164
|
+
(`checkGenerationGuard`, `checkMergeInformationFloor`) that had zero
|
|
165
|
+
production callers.
|
|
166
|
+
|
|
167
|
+
## [0.9.4] - 2026-08-30
|
|
168
|
+
|
|
169
|
+
### Changed
|
|
170
|
+
|
|
171
|
+
- **Node.js 22 support is restored for the npm package.** 0.9.3 raised the
|
|
172
|
+
npm bootstrap floor to Node >= 24 as a policy simplification; no code in
|
|
173
|
+
the package actually requires a Node-24-only API, and the pinned
|
|
174
|
+
better-sqlite3 12.11.1 ships a prebuilt binary for Node 22 (ABI 127), so
|
|
175
|
+
nothing compiles from source. The floor returns to Node >= 22 across the
|
|
176
|
+
preinstall check, the CLI bootstrap guard, and the pinned-registry
|
|
177
|
+
helper, and the CI node-smoke matrix again runs BOTH Node 22 and 24 so
|
|
178
|
+
the supported floor is tested on every run, not merely declared.
|
|
179
|
+
|
|
180
|
+
### Added
|
|
181
|
+
|
|
182
|
+
- **`akm task sync --dry-run` previews the reconcile without touching the
|
|
183
|
+
scheduler (#849).** Prints the planned adds/updates/removes — removals
|
|
184
|
+
now carry their owning bundle — makes zero scheduler writes, and exits
|
|
185
|
+
non-zero when removals are pending so scripts can gate on it. The plan
|
|
186
|
+
renderer is shared, ready for the planned `task prune` (#851) to reuse.
|
|
187
|
+
- **Search hits now report which stage of the progressive AND->OR lexical
|
|
188
|
+
ladder produced them (#856).** `akm search` already ran strict AND, then
|
|
189
|
+
prefix AND, then an OR/prefix-OR recovery, stopping at the first stage
|
|
190
|
+
that returned candidates — but callers had no way to tell a strict match
|
|
191
|
+
from a heavily relaxed one. Local bundle hits now carry an optional
|
|
192
|
+
`matchStage: "exact" | "prefix" | "relaxed"` field (omitted for hits with
|
|
193
|
+
no FTS component, e.g. a pure-semantic hybrid contribution), surfaced at
|
|
194
|
+
`--detail normal`, `--detail full`, and `--shape agent` across all output
|
|
195
|
+
formats. Purely additive; no `schemaVersion` bump.
|
|
196
|
+
- **Previous-release corpus test.** New
|
|
197
|
+
`tests/integration/previous-release-corpus.test.ts` holds fixtures of
|
|
198
|
+
data shapes prior releases actually wrote (task v2/v3 sources, pre-#858
|
|
199
|
+
proposal rows) and asserts the current CLI reads or auto-handles every
|
|
200
|
+
one. Policy: every schema bump must add the old shape here — this suite
|
|
201
|
+
failing means an upgrade break was about to ship.
|
|
202
|
+
|
|
203
|
+
### Fixed
|
|
204
|
+
|
|
205
|
+
- **`akm show`, `task run`/`explain`, `workflow run`/`plan`, and
|
|
206
|
+
`command run` no longer fail on bundles past 16,384 files (#857).**
|
|
207
|
+
Single-ref owner resolution walked the entire bundle tree on every
|
|
208
|
+
lookup, aborting with `file limit 16384 exceeded` once a bundle grew past
|
|
209
|
+
the cap — normal `improve` output accumulation was enough to get there.
|
|
210
|
+
Path-to-conceptId derivation is deterministic, so its inverse is now
|
|
211
|
+
computed in closed form: each adapter's `readCandidates` enumerates every
|
|
212
|
+
physical spelling that could own a conceptId (canonical placement, loose
|
|
213
|
+
off-canonical fallback, env `.env` duality) and the existing
|
|
214
|
+
verification/collision pipeline runs over that fixed set. The tree walk,
|
|
215
|
+
both scan caps, and `AdapterConceptScanError` are gone — no cap is needed
|
|
216
|
+
when nothing walks. Collision guarantees are unchanged (the pinned
|
|
217
|
+
loose-vs-canonical conformance case still throws), and symlinked
|
|
218
|
+
off-canonical files are now reachable where the old scan skipped them.
|
|
219
|
+
- **`akm health --report` and `akm proposal list --status accepted` no
|
|
220
|
+
longer crash on proposal rows written before the `changes` metadata
|
|
221
|
+
envelope existed (#858), and `improve` outcome-score salience no longer
|
|
222
|
+
silently computes from zero accepted-counts (#859).** ~89% of real
|
|
223
|
+
archived accepted/rejected rows predate the field and can never recover
|
|
224
|
+
it; `storedToChanges` now treats a fully-absent `changes` key as a
|
|
225
|
+
documented legacy gap (empty change list — the rows still count toward
|
|
226
|
+
accepted/rejected history), `listStateProposals` skips-and-warns on
|
|
227
|
+
genuinely corrupt rows instead of aborting the whole list, and the write
|
|
228
|
+
path still refuses to persist a new proposal without changes.
|
|
229
|
+
- **Task v2/v3 sources are auto-read as v4 instead of hard-failing with
|
|
230
|
+
`TASK_SCHEMA_VERSION_UNSUPPORTED`.** The v4 source gate would have broken
|
|
231
|
+
every pre-0.9.4 scheduled task headlessly on upgrade until the operator
|
|
232
|
+
manually ran `akm migrate apply`. `parseTaskSource` now runs the same
|
|
233
|
+
pure, deterministic migration planners the migrator uses (chained
|
|
234
|
+
v2->v3->v4) entirely in memory, emits a one-line stderr deprecation
|
|
235
|
+
warning, and never writes the file; the hard error survives only for
|
|
236
|
+
sources the deterministic conversion genuinely cannot translate.
|
|
237
|
+
`akm migrate apply` remains the way to rewrite files on disk and silence
|
|
238
|
+
the warning. The planner core moved from `scripts/akm-migrate/` into
|
|
239
|
+
`src/tasks/source/` so there is exactly one copy.
|
|
8
240
|
|
|
9
241
|
## [0.9.3] - 2026-08-29
|
|
10
242
|
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ assistant, including [Claude Code](https://claude.ai/code),
|
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
19
|
-
**Option 1 — npm package (recommended; requires [Node.js](https://nodejs.org) >=
|
|
19
|
+
**Option 1 — npm package (recommended; requires [Node.js](https://nodejs.org) >= 22):**
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
22
|
npm install -g akm-cli
|
package/SECURITY.md
CHANGED
|
@@ -96,7 +96,7 @@ containing secrets or private notes.
|
|
|
96
96
|
|
|
97
97
|
## Known non-issues
|
|
98
98
|
|
|
99
|
-
- **The `akm-cli` npm package requires Node.js >=
|
|
99
|
+
- **The `akm-cli` npm package requires Node.js >= 22 as its bootstrap.** A
|
|
100
100
|
working Bun >= 1.0 is preferred for execution when it is also on `PATH`; old,
|
|
101
101
|
unusable, or absent Bun installations fall back to Node.js. Bun does not
|
|
102
102
|
remove the package's Node.js requirement. Standalone binaries are
|
package/STABILITY.md
CHANGED
|
@@ -220,7 +220,7 @@ enumeration of the whole `proposal` noun group.
|
|
|
220
220
|
| `78` | Configuration error |
|
|
221
221
|
- **Install scripts** — `install.sh` and `install.ps1` URLs; the `--prefix`
|
|
222
222
|
/ `AKM_INSTALL_DIR` environment override.
|
|
223
|
-
- **Runtime** — the npm package requires Node.js >=
|
|
223
|
+
- **Runtime** — the npm package requires Node.js >= 22 as its bootstrap and
|
|
224
224
|
prefers a working Bun >= 1.0 for execution when both are available; old,
|
|
225
225
|
unusable, or absent Bun installations fall back to Node.js. Standalone
|
|
226
226
|
binaries are runtime-free.
|
package/dist/akm
CHANGED
|
@@ -120,8 +120,8 @@ if (contextValid) {
|
|
|
120
120
|
|
|
121
121
|
if (!process.versions.bun) {
|
|
122
122
|
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
123
|
-
if (major <
|
|
124
|
-
console.error("The akm-cli npm package requires Node.js >=
|
|
123
|
+
if (major < 22) {
|
|
124
|
+
console.error("The akm-cli npm package requires Node.js >= 22 to bootstrap.");
|
|
125
125
|
process.exit(1);
|
|
126
126
|
}
|
|
127
127
|
}
|
package/dist/akm-migrate
CHANGED
|
@@ -8,8 +8,8 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
|
|
9
9
|
if (!process.versions.bun) {
|
|
10
10
|
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
11
|
-
if (major <
|
|
12
|
-
console.error("The akm-cli npm package requires Node.js >=
|
|
11
|
+
if (major < 22) {
|
|
12
|
+
console.error("The akm-cli npm package requires Node.js >= 22 to bootstrap.");
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
15
|
}
|
package/dist/cli.js
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
3
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
4
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
-
// Runtime guard: the akm-cli npm package bootstraps with Node.js >=
|
|
5
|
+
// Runtime guard: the akm-cli npm package bootstraps with Node.js >= 22
|
|
6
6
|
// (#465, #560), then its launcher prefers a working Bun >= 1.0 when available.
|
|
7
7
|
// The runtime boundary (src/runtime.ts, src/storage/database.ts) supports both.
|
|
8
8
|
// Under Node the CLI must be launched via the
|
|
9
9
|
// `dist/cli-node.mjs` wrapper, which registers the text-import loader hook
|
|
10
10
|
// before this module graph loads; running `node dist/cli.js` directly still
|
|
11
11
|
// works for code paths that touch no embedded text asset, but the wrapper is
|
|
12
|
-
// the supported entry. The hard floor is Node
|
|
13
|
-
//
|
|
12
|
+
// the supported entry. The hard floor is Node 22 (Node 20 support dropped 2026-07; `@clack/core` imports
|
|
13
|
+
// `node:util`'s `styleText` (added in Node 20.12) — Node 18 (EOL) throws at import.
|
|
14
14
|
{
|
|
15
15
|
const isBun = typeof globalThis.Bun !== "undefined";
|
|
16
16
|
if (!isBun) {
|
|
17
17
|
const [major = 0] = (process.versions.node ?? "0").split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
18
|
-
const nodeOk = major >=
|
|
18
|
+
const nodeOk = major >= 22;
|
|
19
19
|
if (!nodeOk) {
|
|
20
|
-
console.error("\n ERROR: the akm-cli npm package requires Node.js >=
|
|
20
|
+
console.error("\n ERROR: the akm-cli npm package requires Node.js >= 22.\n" +
|
|
21
21
|
` Detected Node.js ${process.versions.node ?? "unknown"}.\n` +
|
|
22
22
|
" Bun >= 1.0 is optional for execution; it does not replace the Node.js bootstrap.\n" +
|
|
23
23
|
" Upgrade Node.js (https://nodejs.org), or install the runtime-free standalone binary:\n" +
|
|
@@ -15,6 +15,23 @@ export function parseTaskMetadata(row) {
|
|
|
15
15
|
...(metadata.engine !== undefined ? { engine: metadata.engine } : {}),
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* `parseTaskMetadata`, but per-row skip-and-warn instead of throwing (mirrors
|
|
20
|
+
* `listStateProposals`). `decodeTaskHistoryMetadata` already tolerates
|
|
21
|
+
* legacy/additive shapes; only genuine corruption reaches this catch, and a
|
|
22
|
+
* corrupt row must degrade the metric (excluded, not fatal) rather than abort
|
|
23
|
+
* the whole `akm health` computation.
|
|
24
|
+
*/
|
|
25
|
+
export function taskFailureDetail(row) {
|
|
26
|
+
try {
|
|
27
|
+
return parseTaskMetadata(row).detail;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
console.warn(`[akm] Skipping unparseable task_history row in agent-failure-rate (task_id=${row.task_id}, started_at=${row.started_at}): ${message}`);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
18
35
|
/**
|
|
19
36
|
* D8 read-boundary predicate (spec docs/plans/specs/p1b-model-extraction.md
|
|
20
37
|
* §5.3) for `akm health`'s `agentFailureRate`: true for a `task_history` row
|
|
@@ -11,7 +11,7 @@ import { readEvents } from "../../core/events.js";
|
|
|
11
11
|
import { buildTaskRunId, getLoggedRunIds } from "../../core/logs-db.js";
|
|
12
12
|
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
13
13
|
import { queryTaskHistory } from "../../storage/repositories/task-history-repository.js";
|
|
14
|
-
import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow,
|
|
14
|
+
import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, roundRate, summarizeImproveCompleted, summarizeImproveRuns, taskFailureDetail, } from "./improve-metrics.js";
|
|
15
15
|
import { readLlmUsageAggregate } from "./llm-usage.js";
|
|
16
16
|
import { computeDegradationMetrics, computeDenominatorFixedCoverage } from "./metrics.js";
|
|
17
17
|
import { buildPerRunSummaries } from "./task-runs.js";
|
|
@@ -154,7 +154,7 @@ export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Da
|
|
|
154
154
|
// isAgentTaskHistoryRow's header comment for the full mapping).
|
|
155
155
|
const agentRows = taskRows.filter((row) => isAgentTaskHistoryRow(row));
|
|
156
156
|
const agentFailures = agentRows.filter((row) => {
|
|
157
|
-
const detail =
|
|
157
|
+
const detail = taskFailureDetail(row);
|
|
158
158
|
return typeof detail?.reason === "string" && detail.reason.length > 0;
|
|
159
159
|
});
|
|
160
160
|
const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
|
package/dist/commands/health.js
CHANGED
|
@@ -20,7 +20,7 @@ import { queryTaskHistory } from "../storage/repositories/task-history-repositor
|
|
|
20
20
|
import { pkgVersion } from "../version.js";
|
|
21
21
|
import { collectImproveAdvisories } from "./health/advisories.js";
|
|
22
22
|
import { HEALTH_CHECKS, runHealthEngineProbes } from "./health/checks.js";
|
|
23
|
-
import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow,
|
|
23
|
+
import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, roundRate, summarizeImproveCompleted, summarizeImproveRuns, taskFailureDetail, } from "./health/improve-metrics.js";
|
|
24
24
|
import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
|
|
25
25
|
import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, } from "./health/metrics.js";
|
|
26
26
|
import { collectPluginStalenessAdvisories } from "./health/plugin-staleness.js";
|
|
@@ -130,7 +130,7 @@ function gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now) {
|
|
|
130
130
|
// isAgentTaskHistoryRow's header comment for the full mapping).
|
|
131
131
|
const agentRows = taskRows.filter((row) => isAgentTaskHistoryRow(row));
|
|
132
132
|
const agentFailures = agentRows.filter((row) => {
|
|
133
|
-
const detail =
|
|
133
|
+
const detail = taskFailureDetail(row);
|
|
134
134
|
return typeof detail?.reason === "string" && detail.reason.length > 0;
|
|
135
135
|
});
|
|
136
136
|
const logBackingRate = taskRowsWithLogs.length === 0 ? 1 : existingLogRows.length / taskRowsWithLogs.length;
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* WS-3b Step 8 — Anti-collapse merge guards.
|
|
6
6
|
*
|
|
7
|
-
* (a) Generation counter: merged.generation = max(sources)+1;
|
|
8
|
-
*
|
|
7
|
+
* (a) Generation counter: merged.generation = max(sources)+1; merges cite
|
|
8
|
+
* sources. `over_generation_count` (collapse-detector.ts) tracks assets
|
|
9
|
+
* above the generation threshold as an advisory metric only — there is
|
|
10
|
+
* no merge-refusal path wired in.
|
|
9
11
|
* (b) Lexical-diversity check: low n-gram diversity ⇒ raise merge threshold.
|
|
10
|
-
* (c) Merge-information floor (R5 §4.2): provenance union must not shrink and
|
|
11
|
-
* the merged body must retain a minimum fraction of the source tokens.
|
|
12
12
|
* (d) Occasional random non-similar cluster in the pool.
|
|
13
13
|
*
|
|
14
14
|
* @module anti-collapse
|
|
@@ -37,93 +37,6 @@ export function computeMergedGeneration(sourceGenerations) {
|
|
|
37
37
|
return 1;
|
|
38
38
|
return Math.max(...sourceGenerations) + 1;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Check whether a merge of the given assets should be refused due to the
|
|
42
|
-
* anti-collapse generation guard.
|
|
43
|
-
*
|
|
44
|
-
* Returns `{ refused: true, reason }` when BOTH assets have generation > maxGeneration.
|
|
45
|
-
* Returns `{ refused: false }` when the merge is allowed.
|
|
46
|
-
*
|
|
47
|
-
* @param sourceGenerations - Generation values for all merge participants.
|
|
48
|
-
* @param config - Anti-collapse config.
|
|
49
|
-
*/
|
|
50
|
-
export function checkGenerationGuard(sourceGenerations, config) {
|
|
51
|
-
// R5: default ON — only an explicit opt-out disables the guard.
|
|
52
|
-
if (config.enabled === false)
|
|
53
|
-
return { refused: false };
|
|
54
|
-
const maxGen = config.maxGeneration ?? DEFAULT_MAX_GENERATION;
|
|
55
|
-
const highGenCount = sourceGenerations.filter((g) => g > maxGen).length;
|
|
56
|
-
if (highGenCount >= 2) {
|
|
57
|
-
return {
|
|
58
|
-
refused: true,
|
|
59
|
-
reason: `Anti-collapse: ${highGenCount} merge participants have generation > ${maxGen} (${sourceGenerations.join(", ")}); refusing to merge over-consolidated assets.`,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
return { refused: false };
|
|
63
|
-
}
|
|
64
|
-
/** Distinct-token retention floor default (R5 §4.2). */
|
|
65
|
-
export const DEFAULT_MIN_SPECIFICITY_RETENTION = 0.6;
|
|
66
|
-
function distinctTokens(text) {
|
|
67
|
-
// Same lowercase whitespace tokenization computeBigramDiversity uses.
|
|
68
|
-
return new Set(text
|
|
69
|
-
.toLowerCase()
|
|
70
|
-
.split(/\s+/)
|
|
71
|
-
.filter((w) => w.length > 0));
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* A merge must strictly increase information (R5 §4.2):
|
|
75
|
-
* 1. Provenance: the merged asset's `xrefs` must be a superset of the union of
|
|
76
|
-
* all participants' `xrefs` plus the participant refs
|
|
77
|
-
* themselves — provenance never shrinks through a merge.
|
|
78
|
-
* 2. Specificity: distinctTokens(mergedBody) ≥ minSpecificityRetention ×
|
|
79
|
-
* |union(distinctTokens(participant bodies))| — a merge that only
|
|
80
|
-
* shortens/genericizes fails.
|
|
81
|
-
*
|
|
82
|
-
* Pure and deterministic; ADVISORY in v1 (the caller counts violations, it
|
|
83
|
-
* does not refuse the merge). Returns `passed: true` immediately when the
|
|
84
|
-
* anti-collapse suite or the floor itself is opted out.
|
|
85
|
-
*/
|
|
86
|
-
export function checkMergeInformationFloor(mergedBody, mergedSourceRefs, participants, config) {
|
|
87
|
-
if (config.enabled === false || config.mergeInformationFloor === false || participants.length === 0) {
|
|
88
|
-
return { passed: true, provenanceBefore: 0, provenanceAfter: 0, specificityRetention: 1 };
|
|
89
|
-
}
|
|
90
|
-
// 1. Provenance union: participants + everything they already cited.
|
|
91
|
-
const required = new Set();
|
|
92
|
-
for (const p of participants) {
|
|
93
|
-
required.add(p.ref);
|
|
94
|
-
for (const xref of p.xrefs)
|
|
95
|
-
required.add(xref);
|
|
96
|
-
}
|
|
97
|
-
const after = new Set(mergedSourceRefs);
|
|
98
|
-
const missing = [...required].filter((r) => !after.has(r));
|
|
99
|
-
// 2. Specificity retention over the union of source tokens.
|
|
100
|
-
const sourceTokens = new Set();
|
|
101
|
-
for (const p of participants) {
|
|
102
|
-
for (const t of distinctTokens(p.body))
|
|
103
|
-
sourceTokens.add(t);
|
|
104
|
-
}
|
|
105
|
-
const mergedTokens = distinctTokens(mergedBody);
|
|
106
|
-
// Clamped at computation so the pass/fail decision, the reason string, and
|
|
107
|
-
// the reported field all describe the same value.
|
|
108
|
-
const specificityRetention = Math.min(1, sourceTokens.size === 0 ? 1 : mergedTokens.size / sourceTokens.size);
|
|
109
|
-
const minRetention = config.minSpecificityRetention ?? DEFAULT_MIN_SPECIFICITY_RETENTION;
|
|
110
|
-
const provenanceOk = missing.length === 0;
|
|
111
|
-
const specificityOk = specificityRetention >= minRetention;
|
|
112
|
-
const reasons = [];
|
|
113
|
-
if (!provenanceOk) {
|
|
114
|
-
reasons.push(`provenance shrank: merged xrefs missing ${missing.length} ref(s) (e.g. ${missing[0]})`);
|
|
115
|
-
}
|
|
116
|
-
if (!specificityOk) {
|
|
117
|
-
reasons.push(`specificity retention ${specificityRetention.toFixed(2)} < ${minRetention} (merge genericized/shortened)`);
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
passed: provenanceOk && specificityOk,
|
|
121
|
-
provenanceBefore: required.size,
|
|
122
|
-
provenanceAfter: after.size,
|
|
123
|
-
specificityRetention,
|
|
124
|
-
...(reasons.length > 0 ? { reason: reasons.join("; ") } : {}),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
40
|
/**
|
|
128
41
|
* Compute the bigram n-gram diversity of a text string.
|
|
129
42
|
* Returns a value in [0, 1] where 0 = all identical bigrams, 1 = all unique.
|
|
@@ -1744,6 +1744,13 @@ function updateOutcomeScores(args) {
|
|
|
1744
1744
|
// inflate counts with proposals from other stashes.
|
|
1745
1745
|
const acceptedCountByRef = new Map();
|
|
1746
1746
|
try {
|
|
1747
|
+
// #858/#859: listStateProposals() now skips-and-warns on individual
|
|
1748
|
+
// unparseable rows (including legacy pre-#578 rows with no
|
|
1749
|
+
// persisted `changes`, which it tolerates directly) instead of
|
|
1750
|
+
// throwing, so this no longer silently zeroes out every ref's
|
|
1751
|
+
// count on a single bad row. The outer try/catch stays as a
|
|
1752
|
+
// defense-in-depth fallback for unexpected failures (e.g. a query
|
|
1753
|
+
// error), not the primary safeguard it used to be.
|
|
1747
1754
|
const acceptedProposals = listStateProposals(outcomeDb, {
|
|
1748
1755
|
status: "accepted",
|
|
1749
1756
|
...(primaryStashDir ? { stashDir: primaryStashDir } : {}),
|
|
@@ -1753,7 +1760,7 @@ function updateOutcomeScores(args) {
|
|
|
1753
1760
|
}
|
|
1754
1761
|
}
|
|
1755
1762
|
catch {
|
|
1756
|
-
// best-effort: if
|
|
1763
|
+
// best-effort: if the query itself fails, accepted counts stay at 0
|
|
1757
1764
|
}
|
|
1758
1765
|
// Update each ref's outcome row and collect the resulting outcome scores.
|
|
1759
1766
|
const rawOutcomeScores = new Map();
|
|
@@ -11,7 +11,7 @@ import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
|
11
11
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
12
12
|
import { conceptIdForStashFile, displayRefForConceptId } from "../../core/asset/resolve-ref.js";
|
|
13
13
|
import { deriveBundleIds } from "../../core/bundle-id.js";
|
|
14
|
-
import { resolveStashDir } from "../../core/common.js";
|
|
14
|
+
import { isAkmRegistryCachePath, resolveStashDir } from "../../core/common.js";
|
|
15
15
|
import { loadConfig, primaryBundlePath } from "../../core/config/config.js";
|
|
16
16
|
import { UsageError } from "../../core/errors.js";
|
|
17
17
|
import { warn } from "../../core/warn.js";
|
|
@@ -80,10 +80,6 @@ function collectMarkdownFiles(dir, caseInsensitive = false) {
|
|
|
80
80
|
}
|
|
81
81
|
return results;
|
|
82
82
|
}
|
|
83
|
-
function isCachedLintPath(filePath) {
|
|
84
|
-
const posixPath = filePath.replace(/\\/g, "/");
|
|
85
|
-
return posixPath.includes("/.cache/") || posixPath.includes("/registry/");
|
|
86
|
-
}
|
|
87
83
|
/** Peer workflow sources accepted by the source-IR compiler; `.yaml` remains unsupported. */
|
|
88
84
|
function collectWorkflowFiles(dir) {
|
|
89
85
|
if (!fs.existsSync(dir))
|
|
@@ -546,7 +542,7 @@ function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
|
546
542
|
: collectMarkdownFiles(dirPath, true);
|
|
547
543
|
let assetFiles = subdir === "workflows" ? files.filter((file) => path.basename(file).toLowerCase() !== "readme.md") : files;
|
|
548
544
|
if (subdir === "workflows") {
|
|
549
|
-
assetFiles = assetFiles.filter((file) => !
|
|
545
|
+
assetFiles = assetFiles.filter((file) => !isAkmRegistryCachePath(file));
|
|
550
546
|
const ownership = resolveWorkflowLintOwnership(stashRoot, assetFiles);
|
|
551
547
|
assetFiles = ownership.files;
|
|
552
548
|
flagged.push(...ownership.issues);
|
|
@@ -575,7 +571,7 @@ function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
|
575
571
|
// Compare on a separator-normalized copy: on Windows these paths carry
|
|
576
572
|
// backslashes, so the forward-slash substring never matched and --fix
|
|
577
573
|
// rewrote files inside the registry cache.
|
|
578
|
-
if (
|
|
574
|
+
if (isAkmRegistryCachePath(filePath))
|
|
579
575
|
continue;
|
|
580
576
|
const relPath = path.relative(stashRoot, filePath);
|
|
581
577
|
let raw;
|
|
@@ -67,6 +67,18 @@ const canonicalProposalValidators = {
|
|
|
67
67
|
const content = proposalContent(proposal);
|
|
68
68
|
if (!content.trim())
|
|
69
69
|
return [];
|
|
70
|
+
// #859: proposedTarget is absent on legacy archived rows, but this
|
|
71
|
+
// validator only ever runs on a proposal about to be minted or promoted
|
|
72
|
+
// (both always carry proposedTarget — see the Proposal.proposedTarget
|
|
73
|
+
// doc comment) — so hitting this is a genuine defect, not a legacy gap.
|
|
74
|
+
if (!proposal.proposedTarget) {
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
kind: "invalid-workflow-structure",
|
|
78
|
+
message: `Workflow proposal ${proposal.id} (${proposal.ref}) is missing proposedTarget and cannot be validated.`,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
70
82
|
const sourcePath = proposal.changes[0]?.path || proposal.ref;
|
|
71
83
|
const result = compileWorkflowSource(content, {
|
|
72
84
|
path: sourcePath,
|