akm-cli 0.9.7 → 0.9.8-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 +310 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/data-dir-usage.js +165 -0
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +18 -37
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +7 -19
- package/dist/commands/improve/preparation.js +52 -36
- package/dist/commands/improve/reflect.js +13 -17
- package/dist/commands/lint/base-linter.js +42 -2
- package/dist/commands/lint/index.js +4 -5
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +4 -18
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/commands/workflow-cli.js +11 -61
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +1 -3
- package/dist/core/asset/frontmatter.js +96 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-io.js +21 -8
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +74 -2
- package/dist/core/state-db.js +2 -1
- package/dist/core/warn.js +15 -0
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +43 -22
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/dir-staleness.js +30 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +179 -248
- package/dist/scripts/akm-migrate.js +179 -248
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-entries-repository.js +24 -28
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +16 -1
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/proposals-repository.js +4 -1
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +15 -0
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/task-to-v3.js +10 -2
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/migration/v0.9.1-to-v0.9.2.md +18 -4
- package/docs/reference/cli.md +3 -1
- package/docs/reference/data-and-telemetry.md +1 -0
- package/docs/reference/tasks.md +11 -0
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -9
- package/dist/workflows/source-ir/compare.js +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,316 @@ 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
|
+
## [0.9.8-beta.2] - 2026-09-02
|
|
8
|
+
|
|
9
|
+
> **Adds state migration `026-proposals-strip-legacy-fragment-refs`.** The
|
|
10
|
+
> one-way caveat below applies to it as well: once this build opens
|
|
11
|
+
> `state.db`, 0.9.8-beta.1 and earlier refuse it with `unknown migration ID
|
|
12
|
+
> 026-proposals-strip-legacy-fragment-refs`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`akm health` reports data-dir disk usage** (#896). A `data-dir-usage`
|
|
17
|
+
advisory sums the data directory with a stat-only walk and warns when it is
|
|
18
|
+
more than 3× the three live databases (state.db, index.db, logs.db) or when
|
|
19
|
+
one top-level subdirectory holds more than half of it, naming that
|
|
20
|
+
subdirectory with its size and share (for example `backups/ is 70G (94% of
|
|
21
|
+
data dir)`). The walk stops after 100,000 entries and says so. Silent when
|
|
22
|
+
nothing looks wrong.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **`akm task sync` no longer spawns `npm root --global` on every call** (#901).
|
|
27
|
+
The npm-global-root probe behind `resolveAkmInvocation` is memoized for the
|
|
28
|
+
process, so a `task sync --rebind` cycle spawns npm at most once instead of
|
|
29
|
+
twice, and an installation that loops it every minute stops accumulating an
|
|
30
|
+
npm debug log per spawn.
|
|
31
|
+
- **A blocked v2 task now says how to convert it** (#902, #899). The
|
|
32
|
+
`argv-array-has-no-portable-shell-string` blocker printed by `akm migrate`
|
|
33
|
+
and the `TASK_SCHEMA_VERSION_UNSUPPORTED` read error now state that manual
|
|
34
|
+
conversion is required and name the rewrite (`command:` argv array →
|
|
35
|
+
`run:` string plus `shell:`). The full v2 → v4 field mapping is documented in
|
|
36
|
+
`docs/migration/v0.9.1-to-v0.9.2.md`.
|
|
37
|
+
- **Legacy `#fragment` proposal rows are repaired instead of warned about
|
|
38
|
+
forever** (#898). State migration 026 strips the retired export-fragment
|
|
39
|
+
selector from `proposals.ref` in place so the rows parse again, and an
|
|
40
|
+
unparseable proposal row now warns once per process instead of once per
|
|
41
|
+
read (`akm health --report` read the table seven times).
|
|
42
|
+
|
|
43
|
+
- **A no-op incremental `akm index` no longer costs minutes of CPU** (#900).
|
|
44
|
+
Two causes: the per-directory freshness check ran two full scans of the
|
|
45
|
+
`entries` table for every directory (O(directories × entries)), and every
|
|
46
|
+
file was read, hashed, and parsed before the freshness check decided the
|
|
47
|
+
directory was unchanged. The directory lookup now uses the existing
|
|
48
|
+
`file_path` index, and a stat-based gate over each directory's walked file
|
|
49
|
+
set skips unchanged directories before any file is read. On a synthetic
|
|
50
|
+
800-directory, 4,000-entry corpus a no-op pass fell from ~37 s to under 1 s
|
|
51
|
+
of CPU with identical entries and search results. The persisted directory
|
|
52
|
+
fingerprint now covers every walked file and `index_dir_state` gains a
|
|
53
|
+
`row_count` column; an existing index.db drains each directory once more
|
|
54
|
+
after upgrading, then takes the fast path.
|
|
55
|
+
|
|
56
|
+
- **Task-migration snapshots are capped at the five most recent** (#897).
|
|
57
|
+
`akm migrate apply` writes one snapshot directory per run under
|
|
58
|
+
`backups/task-v3` and `backups/task-v4` and never pruned them; each apply
|
|
59
|
+
now keeps the five newest and removes the rest, the same policy config
|
|
60
|
+
backups already use. Nothing in the current code writes the legacy
|
|
61
|
+
`backups/migrations`, `manual`, `releases`, or `operations` directories,
|
|
62
|
+
so they are left alone; the new health advisory is what surfaces them.
|
|
63
|
+
|
|
64
|
+
## [0.9.8-beta.1] - 2026-09-01
|
|
65
|
+
|
|
66
|
+
A cleanup and stabilization release: deletion of machinery that policed the
|
|
67
|
+
codebase's shape rather than its behaviour, and — because auditing for that
|
|
68
|
+
machinery meant reading the code closely — a run of real defects it had been
|
|
69
|
+
sitting on top of.
|
|
70
|
+
|
|
71
|
+
> **Upgrading is one-way for `state.db`.** This release adds migration
|
|
72
|
+
> `025-task-history-vocabulary-backfill`. Once any 0.9.8 command opens
|
|
73
|
+
> `state.db`, the ledger contains an ID that 0.9.7 does not know, and 0.9.7
|
|
74
|
+
> refuses to open it: `Refusing to open a database with a newer migration
|
|
75
|
+
> ledger: unknown migration ID 025-task-history-vocabulary-backfill`.
|
|
76
|
+
>
|
|
77
|
+
> The refusal is deliberate — an older binary must not write a database whose
|
|
78
|
+
> schema it cannot reason about — but the practical effect is that
|
|
79
|
+
> **downgrading to 0.9.7 requires restoring a `state.db` backup.** Commands
|
|
80
|
+
> that only read the derived index (`akm info`, `akm search`) keep working on
|
|
81
|
+
> 0.9.7; everything that touches `state.db` (`akm health`, `akm task`,
|
|
82
|
+
> `akm improve`, proposals) does not.
|
|
83
|
+
>
|
|
84
|
+
> Snapshot `state.db` before upgrading if you may need to go back:
|
|
85
|
+
>
|
|
86
|
+
> ```sh
|
|
87
|
+
> akm info --format json # confirm your data dir
|
|
88
|
+
> sqlite3 "$DATA_DIR/state.db" "VACUUM INTO '''state.db.pre-0.9.8.bak'''"
|
|
89
|
+
> ```
|
|
90
|
+
|
|
91
|
+
Two security holes, two search-correctness bugs, a locale-dependent hash, a
|
|
92
|
+
deletion shield that failed open, and sixteen places that answered a failure
|
|
93
|
+
with a confident wrong answer instead of an error.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- **`akm workflow plan` returns JSON by default**, like every other command
|
|
98
|
+
(#903). It was the one verb whose unmarked default was a human summary. That
|
|
99
|
+
exception cost a bespoke branch which could not reliably distinguish "no
|
|
100
|
+
format named" from "`--format json` named globally before the subcommand" —
|
|
101
|
+
citty parses each command level against its own argv, so the leaf read
|
|
102
|
+
`undefined` in both cases. Working around that meant reading the invocation
|
|
103
|
+
singleton, folding in a persisted `output.format`, and finally leaving a
|
|
104
|
+
resolved `"json"` on the text branch because it was indistinguishable from
|
|
105
|
+
"nothing configured" — which meant an explicit `--format json` silently did
|
|
106
|
+
nothing for anyone whose config already resolved to json. Deleted, along with
|
|
107
|
+
~60 lines of comment justifying it. `--format text` still renders the same
|
|
108
|
+
summary through the same formatter; it is simply no longer the default.
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- **Historical state migrations are reachable where akm cannot reinstall
|
|
113
|
+
itself** (#895). A migration flagged `historical-destructive` is refused
|
|
114
|
+
during an ordinary open — it needs a verified safety copy, taken under the
|
|
115
|
+
migration writer lock, so an unattended `akm index` can never quietly drop
|
|
116
|
+
operator state. That guard is correct and unchanged. Its *remedy* was not:
|
|
117
|
+
the only code path that admitted the migration ran as a post-install step of
|
|
118
|
+
`akm upgrade`, behind an npm install. Where akm is installed globally by a
|
|
119
|
+
container image and the runtime user is unprivileged, that install fails
|
|
120
|
+
`EACCES` and throws long before the migration is reached, so `akm index
|
|
121
|
+
--full` was blocked with no supported way out — and the two obvious
|
|
122
|
+
workarounds are both wrong (upgrading as root installs a *newer* akm than the
|
|
123
|
+
image ships, which `config-version-shim` then fails closed against; deleting
|
|
124
|
+
`state.db` destroys task history, proposals, and lessons metadata, none of
|
|
125
|
+
which is derived). New `akm upgrade --state-only` applies pending state
|
|
126
|
+
migrations and installs nothing. Nothing about the migration needed the
|
|
127
|
+
network, root, or a new binary; coupling it to one was the bug. The safety
|
|
128
|
+
copy is still taken — this changes who may request the migration, not what it
|
|
129
|
+
does.
|
|
130
|
+
|
|
131
|
+
- **Two security holes closed.** A failed `git ls-remote` made
|
|
132
|
+
`verifyClonedRevision` a no-op, silently skipping the R-011 post-clone
|
|
133
|
+
revision-integrity check — the guard against a compromised mirror — so any
|
|
134
|
+
network blip disabled it without a word. And `scanExtractedFiles`, the
|
|
135
|
+
post-extraction TOCTOU path-traversal rescan, returned silently when a
|
|
136
|
+
directory could not be read: it passed clean in exactly the race it exists
|
|
137
|
+
to catch. Both now fail loudly.
|
|
138
|
+
|
|
139
|
+
- **`akm curate --type` ignored the type filter.** A set `--type` bypassed
|
|
140
|
+
`selectCuratedStashHits` entirely and did a raw slice of the hits as
|
|
141
|
+
received, which neither filtered by type nor ranked by score — so
|
|
142
|
+
`--type command` could return a skill, in arbitrary order. It now narrows
|
|
143
|
+
the candidate pool and runs the full curation pipeline over it. The test
|
|
144
|
+
that should have caught this passed by accident, because the off-type hit
|
|
145
|
+
happened to sit past the result limit.
|
|
146
|
+
|
|
147
|
+
- **Curate silently dropped relevant results.** An undocumented floor excluded
|
|
148
|
+
any hit scoring below `max(0.35, leader * 0.7)`. Measured against a real
|
|
149
|
+
3,265-memory bundle, one query went from one result to four once it was
|
|
150
|
+
removed — the suppressed hits were the ones actually matching. Ordering is
|
|
151
|
+
unchanged: results were already sorted before the floor ran, so it could
|
|
152
|
+
only ever hide the tail.
|
|
153
|
+
|
|
154
|
+
- **A bundle-audit hash depended on the machine's locale.** `canonicalJson`
|
|
155
|
+
in `installed-stashes.ts` sorted keys with `localeCompare` — ICU-dependent —
|
|
156
|
+
and fed the result to `sha256Hex`, so the same object could hash differently
|
|
157
|
+
on two machines. Now codepoint order, matching the other implementations.
|
|
158
|
+
|
|
159
|
+
- **A deletion shield failed open.** `isHotCapturedMemory` returned false when
|
|
160
|
+
it could not read or parse a memory, marking exactly the memories it failed
|
|
161
|
+
to inspect as fair game for consolidate to merge or delete. It now fails
|
|
162
|
+
closed.
|
|
163
|
+
|
|
164
|
+
- **Sixteen places returned a confident wrong answer instead of an error.**
|
|
165
|
+
The pattern (#791) recurred across search, graph, indexing, and scheduling:
|
|
166
|
+
a corrupt or locked index reported "no matches"; a failed inline rebuild
|
|
167
|
+
surfaced as "Index is empty. Run 'akm index'" at exit 0; `akm show` reported
|
|
168
|
+
"0 related files" for an unreadable index; a DB failure in
|
|
169
|
+
`listProposalsReadOnly` returned `[]`, so dedup could not tell "no prior
|
|
170
|
+
proposal" from "database unreadable" and could re-mint an already-rejected
|
|
171
|
+
one; a permission fault on the bundle directory told the user to run
|
|
172
|
+
`akm bundle create`; and `launchd`'s `list()` returned `[]` when the
|
|
173
|
+
LaunchAgents directory could not be read, so task reconciliation concluded
|
|
174
|
+
there was nothing to manage. In each case the failure now surfaces.
|
|
175
|
+
|
|
176
|
+
- **Drifted copies of shared helpers.** `asNonEmptyString` trimmed in two
|
|
177
|
+
places and not in four others, so a whitespace-only session id decoded to
|
|
178
|
+
`" "` in some subsystems and `undefined` in others. `isPlainObject` had two
|
|
179
|
+
incompatible definitions under one name; the loose form would accept a
|
|
180
|
+
`Date` or class instance as a plain record. Both consolidated, along with
|
|
181
|
+
`toPosix` (15 copies, whose "avoid an import cycle" justification was false
|
|
182
|
+
in every case), `isRecord` (11), `compareCodePoints` (8), and five more
|
|
183
|
+
families.
|
|
184
|
+
|
|
185
|
+
- **Pruning a memory no longer leaves dangling belief edges (#885).**
|
|
186
|
+
`writeContradictEdge` — the hardened, test-covered `contradictedBy` writer —
|
|
187
|
+
had no production caller; its docstring named three that did not exist. The
|
|
188
|
+
live pass used a private near-copy that had drifted on exactly the two
|
|
189
|
+
behaviours the original was hardened for: it read the key with
|
|
190
|
+
`Array.isArray` only, so a SCALAR edge (live data the indexer accepts and
|
|
191
|
+
lint never flags) read as "no edges" and was overwritten out of existence;
|
|
192
|
+
and it set `beliefState: "contradicted"` unconditionally, promoting an
|
|
193
|
+
`archived` memory back up the ranking. The tests for both behaviours were
|
|
194
|
+
guarding dead code. The copy is gone.
|
|
195
|
+
|
|
196
|
+
`persistBeliefStateTransition` is deliberately NOT routed through the shared
|
|
197
|
+
primitive: it is a state-transition writer that replaces the edge list
|
|
198
|
+
wholesale and can clear it, which an append-only, never-weaken primitive
|
|
199
|
+
cannot express.
|
|
200
|
+
|
|
201
|
+
- **`git worktree` operations no longer fail on a busy machine (#891).** The
|
|
202
|
+
module's internal `GIT_TIMEOUT_MS` was a flat 30s with no margin, so a
|
|
203
|
+
healthy `git worktree remove` on a loaded host returned
|
|
204
|
+
`{ removed: false, error: "timed out after 30000ms" }`. Anyone running a
|
|
205
|
+
workflow with `isolation: worktree` alongside other git activity could hit
|
|
206
|
+
spurious create failures, or have a clean worktree wrongly retained as
|
|
207
|
+
unremovable. Raised to 120s, matching the existing `GIT_PUSH_TIMEOUT_MS`
|
|
208
|
+
precedent. Note `scripts/test-integration.sh` had already raised *bun's*
|
|
209
|
+
per-test timeout for this reason, but that never touched the production
|
|
210
|
+
subprocess timeout, which fires first.
|
|
211
|
+
|
|
212
|
+
- **`akm health`'s dead-link check no longer reports an unearned all-clear
|
|
213
|
+
(#892).** `checkDeadUrls` capped at 20 URLs across the whole bundle, plus an
|
|
214
|
+
undocumented `slice(0, 3)` per entry, then the caller logged "URL check
|
|
215
|
+
complete (0 dead)". A bundle with thousands of links got a clean bill of
|
|
216
|
+
health after twenty were examined. Both caps removed; every URL is checked
|
|
217
|
+
and a failed request surfaces instead of being swallowed.
|
|
218
|
+
|
|
219
|
+
- **`akm lint --fix` no longer breaks on gitignored drafts (#887).** The doc
|
|
220
|
+
linter walked `docs/` with no gitignore awareness, so unpublished drafts
|
|
221
|
+
under `docs/.pending/` were held to the published-docs contract —
|
|
222
|
+
`bun run lint` failed locally while CI, which never has those files, stayed
|
|
223
|
+
green.
|
|
224
|
+
|
|
225
|
+
### Changed
|
|
226
|
+
|
|
227
|
+
- **Search no longer truncates long queries (#892).** `MAX_LEXICAL_QUERY_TOKENS
|
|
228
|
+
= 16` silently dropped every token past the sixteenth, and tokens are
|
|
229
|
+
collected in order, so the discarded half was the tail — for
|
|
230
|
+
natural-language input, usually where the discriminating words are. It also
|
|
231
|
+
fed ranking, so token-overlap scoring ran on the truncated set too. It was
|
|
232
|
+
unexplained in the code and in the commit that introduced it, and unreachable
|
|
233
|
+
from any flag, config key, or environment variable. Removed: the planner
|
|
234
|
+
handles 10,000 tokens in 9ms, so no performance cliff was being protected.
|
|
235
|
+
|
|
236
|
+
- **Content and memory bodies are no longer silently truncated.**
|
|
237
|
+
`MAX_CONTENT_CHARS` (100k, duplicated across 8 adapters) cut indexed content
|
|
238
|
+
so the tail of a long document was unsearchable; `MAX_BODY_CHARS` (4000) cut
|
|
239
|
+
the text sent for memory inference, so on a large-context engine the model
|
|
240
|
+
saw a fraction of the input while the derived memory looked complete. Both
|
|
241
|
+
removed.
|
|
242
|
+
|
|
243
|
+
- **GitHub Actions are pinned to commit SHAs (#768).** All 29 `uses:` steps
|
|
244
|
+
across every workflow, with the tag preserved in a trailing comment.
|
|
245
|
+
|
|
246
|
+
- **Gated CI runs on schedule, dispatch, and candidate tags only.** The
|
|
247
|
+
`detect-changes` job that selected suites by regex-matching a PR diff is
|
|
248
|
+
gone — its path patterns had gone stale and still named test files this
|
|
249
|
+
release moved or deleted, so it was silently under-selecting suites. Release
|
|
250
|
+
evidence is unchanged; the checklist always required an exact-SHA dispatch.
|
|
251
|
+
|
|
252
|
+
- **`akm-eval` in CI is now a determinism check only.** Its score gates are
|
|
253
|
+
removed. Measured before cutting: the baseline scored a perfect 1.0 against
|
|
254
|
+
a 0.75 gate, and seven of nine case types never ran — CI has no LLM and no
|
|
255
|
+
run history, so everything the eval exists to measure was skipped while the
|
|
256
|
+
job reported green. The harness itself is unchanged and remains a genuine
|
|
257
|
+
quality signal when run against a real bundle.
|
|
258
|
+
|
|
259
|
+
### Removed
|
|
260
|
+
|
|
261
|
+
- **`akm health --clean-dead-residue` and every other compensating shim.**
|
|
262
|
+
A special-purpose flag on `health` that deleted files existed because
|
|
263
|
+
migrations had not finished their own job. Removing a superseded layout IS
|
|
264
|
+
migration, so `akm migrate status` now reports it and `akm migrate apply`
|
|
265
|
+
removes it. Four more of the same class went the same way: the D8
|
|
266
|
+
`task_history` vocabulary is rewritten once by a state migration instead of
|
|
267
|
+
re-decided at three read sites; the legacy `extraParams` config lift happens
|
|
268
|
+
once in `migrate apply` instead of silently on every load forever (an
|
|
269
|
+
unmigrated config now fails closed, naming the command to run); retired
|
|
270
|
+
`type:slug` xrefs are rewritten by `lint --fix`; and stale transaction
|
|
271
|
+
journals are recovered by `migrate apply` rather than reported by an
|
|
272
|
+
advisory that pointed at documentation while the recovery function it needed
|
|
273
|
+
sat with zero callers.
|
|
274
|
+
|
|
275
|
+
- **Dead code with no consumers.** An empty `{}` type threaded through two
|
|
276
|
+
functions as a parameter neither read, with a `{}` placeholder at the call
|
|
277
|
+
site; unused helpers, imports, and parameters throughout.
|
|
278
|
+
|
|
279
|
+
- **14 architecture ratchets, 15 golden/snapshot suites, 12 characterization
|
|
280
|
+
suites.** Function-size and import-cycle ratchets that failed on refactors
|
|
281
|
+
harming nothing; byte-for-byte snapshots "fixed" by regenerating; tests that
|
|
282
|
+
pinned "what the code does today" by definition.
|
|
283
|
+
- **11 of 14 lint scripts (-3,988 lines).** `lint-tests-isolation` alone was
|
|
284
|
+
717 lines standing in front of `src/core/paths.ts`, which already throws
|
|
285
|
+
`TEST_ISOLATION_MISSING` at runtime. Also removed: the `gen-config-schema
|
|
286
|
+
--check` gate (`build` regenerates the schema anyway, so what ships is always
|
|
287
|
+
current), and `lint-devto-posts` (322 lines that were wired to nothing and
|
|
288
|
+
duplicated checks `devto-cli` performs itself). The three kept catch
|
|
289
|
+
user-visible problems: secrets must route through the resolver, no dead refs
|
|
290
|
+
in shipped assets, no docs teaching commands that do not exist.
|
|
291
|
+
- **Both `MIN_TESTS` floors** — arbitrary numbers whose only job was to fail
|
|
292
|
+
the suite when test count dropped.
|
|
293
|
+
- **`release-workflow-syntax.yml`** — a workflow that ran actionlint over the
|
|
294
|
+
other workflows. A broken workflow file already fails at GitHub.
|
|
295
|
+
- **Dead `$STASH/.akm` residue is now reportable (#889).** `akm health` gains a
|
|
296
|
+
read-only `stash-dead-residue` advisory naming each stale path and its size;
|
|
297
|
+
deletion is gated behind an explicit `akm health --clean-dead-residue`. On a
|
|
298
|
+
real bundle 82% of `.akm` (135 MB) was pre-0.9.0 leftovers no code reads.
|
|
299
|
+
|
|
300
|
+
### Testing
|
|
301
|
+
|
|
302
|
+
- Test isolation grandfather list drained 58 -> 2 (#785); the two retained
|
|
303
|
+
entries are the helper-definitions file and a meta-test of the guard itself.
|
|
304
|
+
- The test tree now has a stated unit/integration rule and ~250 files were
|
|
305
|
+
reclassified by evidence — real DB, network, or process spawn — rather than
|
|
306
|
+
by filename (#786).
|
|
307
|
+
- 31 status-only `not.toBe(0)` assertions replaced with exact exit codes and
|
|
308
|
+
machine-readable error codes, every value observed rather than inferred
|
|
309
|
+
(#787).
|
|
310
|
+
- Near-duplicate clusters drained (#788); real-server timing flakes removed or
|
|
311
|
+
made deterministic (#789).
|
|
312
|
+
- Kept deliberately: `registry-network-boundary.test.ts`, which enforces that
|
|
313
|
+
only `pinned-transport.ts` may import `node:http`/`node:https` — the control
|
|
314
|
+
that stops a raw request bypassing address-pinned fetching. No lint script
|
|
315
|
+
covers that boundary.
|
|
316
|
+
|
|
7
317
|
## [0.9.7] - 2026-08-31
|
|
8
318
|
|
|
9
319
|
### Added
|
package/dist/commands/env/env.js
CHANGED
|
@@ -48,28 +48,9 @@
|
|
|
48
48
|
import fs from "node:fs";
|
|
49
49
|
import path from "node:path";
|
|
50
50
|
import dotenv from "dotenv";
|
|
51
|
-
import { writeFileAtomic } from "../../core/common.js";
|
|
51
|
+
import { scanEnvKeyNames, writeFileAtomic } from "../../core/common.js";
|
|
52
52
|
import { sensitiveMarkerPath } from "./marker-path.js";
|
|
53
|
-
|
|
54
|
-
const ASSIGN_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
55
|
-
/** Scan lines and return KEY names in file order, without duplicates. */
|
|
56
|
-
export function scanEnvKeyNames(text) {
|
|
57
|
-
const keys = [];
|
|
58
|
-
const seen = new Set();
|
|
59
|
-
for (const line of text.split(/\r?\n/)) {
|
|
60
|
-
const m = line.match(ASSIGN_RE);
|
|
61
|
-
if (!m)
|
|
62
|
-
continue;
|
|
63
|
-
const key = m[1];
|
|
64
|
-
if (!key)
|
|
65
|
-
continue;
|
|
66
|
-
if (seen.has(key))
|
|
67
|
-
continue;
|
|
68
|
-
seen.add(key);
|
|
69
|
-
keys.push(key);
|
|
70
|
-
}
|
|
71
|
-
return keys;
|
|
72
|
-
}
|
|
53
|
+
export { scanEnvKeyNames };
|
|
73
54
|
/**
|
|
74
55
|
* Read and return ONLY non-secret metadata: key names.
|
|
75
56
|
*
|
|
@@ -706,29 +706,4 @@ export const HEALTH_CHECKS = [
|
|
|
706
706
|
};
|
|
707
707
|
},
|
|
708
708
|
},
|
|
709
|
-
{
|
|
710
|
-
// Item 4: stale durable-transaction journals under $DATA/txn (stranded
|
|
711
|
-
// recovery state seen twice in a real 0.9 migration) had zero health
|
|
712
|
-
// visibility. `count`/`unreadable` already exclude journals younger than
|
|
713
|
-
// the sweeper's grace period (a currently-running operation), so any
|
|
714
|
-
// nonzero count here is a real leftover.
|
|
715
|
-
name: "stale-txn-journals",
|
|
716
|
-
channel: "advisory",
|
|
717
|
-
run: (ctx) => {
|
|
718
|
-
const s = ctx.staleTxnJournals;
|
|
719
|
-
const warn = s.count > 0;
|
|
720
|
-
const unreadablePart = s.unreadable > 0 ? `, ${s.unreadable} unreadable` : "";
|
|
721
|
-
const agePart = s.oldestAgeMs !== null ? `, oldest ${Math.round(s.oldestAgeMs / 60000)}m old` : "";
|
|
722
|
-
return {
|
|
723
|
-
name: "stale-txn-journals",
|
|
724
|
-
kind: "deterministic",
|
|
725
|
-
status: warn ? "warn" : "pass",
|
|
726
|
-
confidence: "high",
|
|
727
|
-
message: warn
|
|
728
|
-
? `${s.count} stale transaction journal(s) found under ${s.dir}${unreadablePart}${agePart} — see docs/migration/v0.9.0-troubleshooting.md for journal reconciliation steps.`
|
|
729
|
-
: "No stale transaction journals found.",
|
|
730
|
-
evidence: { dir: s.dir, count: s.count, unreadable: s.unreadable, oldestAgeMs: s.oldestAgeMs },
|
|
731
|
-
};
|
|
732
|
-
},
|
|
733
|
-
},
|
|
734
709
|
];
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `data-dir-usage` advisory for `akm health` (#896).
|
|
6
|
+
*
|
|
7
|
+
* A real environment's `$XDG_DATA_HOME/akm` grew to 74 GB with none of the
|
|
8
|
+
* ~20 health checks saying a word about disk. 70 of the 74 GB turned out to
|
|
9
|
+
* be `backups/` (unpruned migration snapshots, see #897); the live working
|
|
10
|
+
* set (state.db + index.db + logs.db) was ~4.2 GB. Naming the largest
|
|
11
|
+
* top-level contributor and its share of the total is most of the value —
|
|
12
|
+
* `backups/ is 70G (94% of data dir)` is self-diagnosing where "akm health
|
|
13
|
+
* says nothing" is not.
|
|
14
|
+
*
|
|
15
|
+
* Best-effort and read-only: a plain recursive `fs` stat walk over the data
|
|
16
|
+
* dir, no `du` shell-out. Silent (returns `undefined`) whenever nothing
|
|
17
|
+
* looks wrong, matching the stash-exposure/type-directory-check house
|
|
18
|
+
* pattern — this is not a "always show a pass line" check.
|
|
19
|
+
*/
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
/**
|
|
23
|
+
* Warn when the data dir's total size is more than this many times the
|
|
24
|
+
* combined size of the three live databases (state.db + index.db +
|
|
25
|
+
* logs.db). Chosen so a healthy install (backups roughly comparable to the
|
|
26
|
+
* live working set) stays quiet, while an order-of-magnitude blowup like
|
|
27
|
+
* the 74 GB/4.2 GB (~17x) incident trips it.
|
|
28
|
+
*/
|
|
29
|
+
const DATA_DIR_BLOAT_RATIO_THRESHOLD = 3;
|
|
30
|
+
/**
|
|
31
|
+
* Warn when a single top-level subdirectory accounts for more than this
|
|
32
|
+
* percentage of the data dir's total size — the "one thing ate the disk"
|
|
33
|
+
* signal (94% for `backups/` in the incident).
|
|
34
|
+
*/
|
|
35
|
+
const DOMINANT_SUBDIR_PERCENT_THRESHOLD = 50;
|
|
36
|
+
/**
|
|
37
|
+
* Cap on the number of filesystem entries the recursive size walk will
|
|
38
|
+
* `stat`. A 70 GB tree of a few thousand backup copies is cheap to walk
|
|
39
|
+
* (stat-only), but a data dir polluted with hundreds of thousands of small
|
|
40
|
+
* files (task logs, npm logs) must not make `akm health` slow. Past this
|
|
41
|
+
* cap the walk stops descending further and the advisory says its size
|
|
42
|
+
* figures are a lower bound.
|
|
43
|
+
*/
|
|
44
|
+
const MAX_WALK_ENTRIES = 100_000;
|
|
45
|
+
const LIVE_DB_FILES = ["state.db", "index.db", "logs.db"];
|
|
46
|
+
/**
|
|
47
|
+
* Recursively sum file sizes under `root` (stat-only, symlinks not
|
|
48
|
+
* followed so a cyclic or huge-target symlink can't blow up the walk).
|
|
49
|
+
* `budget` is a shared mutable counter across the whole tree so the
|
|
50
|
+
* `MAX_WALK_ENTRIES` cap applies to the walk as a whole, not per-branch.
|
|
51
|
+
*/
|
|
52
|
+
function sizeOfPath(root, budget) {
|
|
53
|
+
let stat;
|
|
54
|
+
try {
|
|
55
|
+
stat = fs.lstatSync(root);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { bytes: 0, truncated: false };
|
|
59
|
+
}
|
|
60
|
+
if (stat.isSymbolicLink())
|
|
61
|
+
return { bytes: 0, truncated: false };
|
|
62
|
+
if (!stat.isDirectory())
|
|
63
|
+
return { bytes: stat.size, truncated: false };
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { bytes: 0, truncated: false };
|
|
70
|
+
}
|
|
71
|
+
let bytes = 0;
|
|
72
|
+
let truncated = false;
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (budget.remaining <= 0) {
|
|
75
|
+
truncated = true;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
budget.remaining--;
|
|
79
|
+
const sub = sizeOfPath(path.join(root, entry.name), budget);
|
|
80
|
+
bytes += sub.bytes;
|
|
81
|
+
if (sub.truncated)
|
|
82
|
+
truncated = true;
|
|
83
|
+
}
|
|
84
|
+
return { bytes, truncated };
|
|
85
|
+
}
|
|
86
|
+
/** `1610612736` -> `"1.5G"`. Values under 10 in a unit keep one decimal; 10+ round to an integer. */
|
|
87
|
+
function formatBytes(bytes) {
|
|
88
|
+
const units = ["B", "K", "M", "G", "T"];
|
|
89
|
+
let value = bytes;
|
|
90
|
+
let unit = 0;
|
|
91
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
92
|
+
value /= 1024;
|
|
93
|
+
unit++;
|
|
94
|
+
}
|
|
95
|
+
const rendered = unit === 0 ? String(Math.round(value)) : value < 10 ? value.toFixed(1) : String(Math.round(value));
|
|
96
|
+
return `${rendered}${units[unit]}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build the `data-dir-usage` advisory, or `undefined` when the data dir is
|
|
100
|
+
* missing/empty/unreadable or its size looks unremarkable (neither
|
|
101
|
+
* threshold trips). `dataDir` is the caller-resolved `getDataDir()` path —
|
|
102
|
+
* this module never resolves paths or reads env itself.
|
|
103
|
+
*/
|
|
104
|
+
export function collectDataDirUsageAdvisory(dataDir) {
|
|
105
|
+
let topEntries;
|
|
106
|
+
try {
|
|
107
|
+
topEntries = fs.readdirSync(dataDir, { withFileTypes: true });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return undefined; // no data dir yet — nothing to report.
|
|
111
|
+
}
|
|
112
|
+
const budget = { remaining: MAX_WALK_ENTRIES };
|
|
113
|
+
let totalBytes = 0;
|
|
114
|
+
let truncated = false;
|
|
115
|
+
const sizes = new Map();
|
|
116
|
+
for (const entry of topEntries) {
|
|
117
|
+
const size = sizeOfPath(path.join(dataDir, entry.name), budget);
|
|
118
|
+
totalBytes += size.bytes;
|
|
119
|
+
if (size.truncated)
|
|
120
|
+
truncated = true;
|
|
121
|
+
sizes.set(entry.name, { bytes: size.bytes, isDirectory: entry.isDirectory() });
|
|
122
|
+
}
|
|
123
|
+
if (totalBytes === 0)
|
|
124
|
+
return undefined;
|
|
125
|
+
const subdirs = [...sizes]
|
|
126
|
+
.filter(([, size]) => size.isDirectory)
|
|
127
|
+
.map(([name, size]) => ({ name, bytes: size.bytes, percent: (size.bytes / totalBytes) * 100 }))
|
|
128
|
+
.sort((a, b) => b.bytes - a.bytes);
|
|
129
|
+
const largest = subdirs[0];
|
|
130
|
+
const liveDbBreakdown = Object.fromEntries(LIVE_DB_FILES.map((f) => [f, sizes.get(f)?.bytes ?? 0]));
|
|
131
|
+
const liveDbBytes = Object.values(liveDbBreakdown).reduce((a, b) => a + b, 0);
|
|
132
|
+
const ratio = liveDbBytes > 0 ? totalBytes / liveDbBytes : undefined;
|
|
133
|
+
const bloatWarn = ratio !== undefined && ratio > DATA_DIR_BLOAT_RATIO_THRESHOLD;
|
|
134
|
+
const dominantWarn = largest !== undefined && largest.percent > DOMINANT_SUBDIR_PERCENT_THRESHOLD;
|
|
135
|
+
if (!bloatWarn && !dominantWarn)
|
|
136
|
+
return undefined;
|
|
137
|
+
const parts = [`data dir is ${formatBytes(totalBytes)} at ${dataDir}`];
|
|
138
|
+
if (largest) {
|
|
139
|
+
parts.push(`${largest.name}/ is ${formatBytes(largest.bytes)} (${Math.round(largest.percent)}% of data dir)`);
|
|
140
|
+
}
|
|
141
|
+
if (ratio !== undefined) {
|
|
142
|
+
parts.push(`live databases (${LIVE_DB_FILES.join("+")}) total ${formatBytes(liveDbBytes)}, ~${ratio.toFixed(1)}x smaller`);
|
|
143
|
+
}
|
|
144
|
+
if (truncated) {
|
|
145
|
+
parts.push(`size figures are a lower bound — the walk stopped after ${MAX_WALK_ENTRIES} entries`);
|
|
146
|
+
}
|
|
147
|
+
const message = `${parts.join("; ")}.`;
|
|
148
|
+
return {
|
|
149
|
+
name: "data-dir-usage",
|
|
150
|
+
kind: "deterministic",
|
|
151
|
+
status: "warn",
|
|
152
|
+
confidence: "medium",
|
|
153
|
+
message,
|
|
154
|
+
evidence: {
|
|
155
|
+
dataDir,
|
|
156
|
+
totalBytes,
|
|
157
|
+
liveDbBytes,
|
|
158
|
+
liveDbBreakdown,
|
|
159
|
+
largestSubdir: largest,
|
|
160
|
+
ratio,
|
|
161
|
+
walkBounded: truncated,
|
|
162
|
+
maxWalkEntries: MAX_WALK_ENTRIES,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -33,42 +33,16 @@ export function taskFailureDetail(row) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* - LEGACY rows (no marker, written before P1b's F-2 re-code) stored
|
|
44
|
-
* `target_kind: "prompt"` for the same arm — and `"command"` for the
|
|
45
|
-
* UNRELATED native shell/script arm, which must NOT be counted here.
|
|
46
|
-
* So an unmarked `"command"` row is a legacy shell/script run, not an
|
|
47
|
-
* agent/LLM one; a marked `"command"` row (or an unmarked `"prompt"` row) is.
|
|
36
|
+
* `akm health`'s `agentFailureRate` predicate: true for a `task_history` row
|
|
37
|
+
* that represents a prepared command (agent/LLM) result. `target_kind` is
|
|
38
|
+
* read in the current (post-D8) vocabulary — the
|
|
39
|
+
* `025-task-history-vocabulary-backfill` state migration rewrites every
|
|
40
|
+
* legacy-vocabulary row (which stored the agent/LLM arm as `"prompt"` and
|
|
41
|
+
* the native shell/script arm as `"command"`) before this ever runs against
|
|
42
|
+
* it, so a `"command"` row here is unambiguously the agent/LLM arm.
|
|
48
43
|
*/
|
|
49
44
|
export function isAgentTaskHistoryRow(row) {
|
|
50
|
-
|
|
51
|
-
// (e.g. improve-pipeline task_history rows, target_kind "improve") carry
|
|
52
|
-
// metadata_json that predates the metadataVersion:2 shape entirely, and
|
|
53
|
-
// decodeTaskHistoryMetadata throws on that — exactly like the pre-fix
|
|
54
|
-
// `target_kind === "prompt"` filter, which never called it for a row this
|
|
55
|
-
// function isn't going to count anyway. Only decode for the two target
|
|
56
|
-
// kinds this predicate can return true for.
|
|
57
|
-
if (row.target_kind !== "command" && row.target_kind !== "prompt")
|
|
58
|
-
return false;
|
|
59
|
-
// An undecodable metadata_json is by definition unmarked: pre-P1b rows can
|
|
60
|
-
// carry shapes decodeTaskHistoryMetadata rejects, and `akm health` must
|
|
61
|
-
// classify them as legacy rather than throw (round-3 review advisory).
|
|
62
|
-
let marked = false;
|
|
63
|
-
try {
|
|
64
|
-
marked = decodeTaskHistoryMetadata(row.metadata_json).targetVocab === 2;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
marked = false;
|
|
68
|
-
}
|
|
69
|
-
if (row.target_kind === "command")
|
|
70
|
-
return marked;
|
|
71
|
-
return !marked;
|
|
45
|
+
return row.target_kind === "command";
|
|
72
46
|
}
|
|
73
47
|
function createUnknownImproveMetrics() {
|
|
74
48
|
return {
|
|
@@ -148,10 +148,6 @@ export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Da
|
|
|
148
148
|
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
149
149
|
const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
|
|
150
150
|
const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
151
|
-
// D8 (spec §5.3): a marked "command" row or a legacy (unmarked) "prompt"
|
|
152
|
-
// row is the agent/LLM arm; an unmarked "command" row is the legacy
|
|
153
|
-
// native shell/script arm and must not be counted here (see
|
|
154
|
-
// isAgentTaskHistoryRow's header comment for the full mapping).
|
|
155
151
|
const agentRows = taskRows.filter((row) => isAgentTaskHistoryRow(row));
|
|
156
152
|
const agentFailures = agentRows.filter((row) => {
|
|
157
153
|
const detail = taskFailureDetail(row);
|