akm-cli 0.9.17-alpha.6 → 0.9.17-alpha.8

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +285 -0
  2. package/STABILITY.md +2 -2
  3. package/dist/akm +62 -29
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  6. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +5 -3
  7. package/dist/commands/improve/consolidate.js +11 -0
  8. package/dist/commands/improve/improve-cli.js +27 -7
  9. package/dist/commands/improve/ledger.js +7 -3
  10. package/dist/commands/improve/loop-stages.js +4 -3
  11. package/dist/commands/improve/preparation.js +40 -10
  12. package/dist/commands/improve/reflect.js +46 -22
  13. package/dist/commands/improve/retrieval-gate.js +127 -0
  14. package/dist/commands/improve/retrieval-scope.js +77 -0
  15. package/dist/commands/read/curate.js +41 -30
  16. package/dist/commands/read/show.js +55 -2
  17. package/dist/commands/sources/info.js +3 -0
  18. package/dist/commands/tasks/tasks-cli.js +10 -12
  19. package/dist/commands/tasks/tasks.js +57 -56
  20. package/dist/commands/tasks/validate.js +27 -46
  21. package/dist/core/adapter/adapters/akm-adapter.js +2 -0
  22. package/dist/core/adapter/adapters/akm-metadata.js +31 -0
  23. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  24. package/dist/core/improve-result.js +4 -1
  25. package/dist/core/non-task-input.js +20 -0
  26. package/dist/core/paths.js +0 -4
  27. package/dist/indexer/db/graph-db.js +0 -32
  28. package/dist/indexer/graph/graph-extraction.js +3 -1
  29. package/dist/indexer/indexer.js +1 -3
  30. package/dist/indexer/links/declared-links.js +90 -0
  31. package/dist/indexer/scan/doc-to-entry.js +1 -0
  32. package/dist/indexer/usage/usage-events.js +34 -0
  33. package/dist/llm/graph-extract.js +26 -37
  34. package/dist/output/shapes/helpers.js +3 -0
  35. package/dist/output/text/show-format.js +16 -0
  36. package/dist/scripts/akm-migrate-node.js +6377 -6437
  37. package/dist/scripts/akm-migrate.js +6860 -6920
  38. package/dist/storage/repositories/index-entries-repository.js +12 -6
  39. package/dist/storage/repositories/index-entry-schema.js +18 -1
  40. package/dist/storage/repositories/index-links-repository.js +143 -0
  41. package/dist/storage/repositories/index-schema.js +27 -0
  42. package/dist/storage/repositories/proposals-repository.js +4 -0
  43. package/dist/tasks/backends/cron.js +80 -43
  44. package/dist/tasks/backends/launchd.js +28 -15
  45. package/dist/tasks/backends/schtasks.js +25 -10
  46. package/dist/tasks/run/load-task.js +1 -1
  47. package/dist/tasks/scheduler-binding.js +4 -2
  48. package/dist/tasks/scheduler-invocation.js +127 -235
  49. package/dist/tasks/scheduler-sync.js +13 -8
  50. package/dist/tasks/source/parse-task-source.js +22 -126
  51. package/dist/tasks/source/task-to-v4.js +463 -87
  52. package/docs/migration/release-notes/0.9.17.md +7 -5
  53. package/docs/migration/v0.9.1-to-v0.9.2.md +7 -3
  54. package/docs/reference/cli.md +26 -10
  55. package/docs/reference/tasks.md +58 -40
  56. package/package.json +1 -1
  57. package/dist/tasks/source/task-to-v3.js +0 -507
package/CHANGELOG.md CHANGED
@@ -6,6 +6,291 @@ 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.17-alpha.8] - 2026-09-28
10
+
11
+ `akm index` now records the links a bundle already declares (`xrefs`,
12
+ `supersededBy`, a `.derived` memory's parent, wiki sources, page links, and
13
+ workflow and task targets) as typed links, with no model. `akm show` lists
14
+ them, and curate's support refs come from them instead of the LLM entity
15
+ graph. The index moves to layout 26 in place on its first writable open;
16
+ 0.9.17-alpha.4 through alpha.7 cannot open it. `index.graph.enabled: false`
17
+ now stops graph extraction in `akm improve`, and a partly failed extraction is
18
+ retried. `akm migrate` converts a v2 or v3 task file to v4 in one pass, and
19
+ the launchers no longer lose a signal that arrives before their child starts.
20
+
21
+ ### Added
22
+
23
+ - **Declared links (#935).** The relations a bundle already declares are now
24
+ stored as typed links: `xrefs:` (`xref`), `supersededBy:`
25
+ (`superseded_by`), `contradictedBy:` (`contradicted_by`),
26
+ `currentBeliefRefs:` (`belief_peer`), a `.derived` memory's parent
27
+ (`derived_from`), wiki `sources:` that name an asset (`cites`), the page
28
+ links an llm-wiki or OKF bundle resolves (`links_to`), and a workflow step's
29
+ or a task's target (`uses`). `akm index` reads them from what it already
30
+ parses, with no model, so an install without an LLM engine gets them. They
31
+ live in their own table (`asset_links`), apart from the LLM entity graph:
32
+ each link belongs to the entry that declares it and is written, replaced
33
+ and deleted with it, including on incremental and write-path (`akm
34
+ remember`) indexing. A target resolves when it is indexed, not when its
35
+ citer was, so a note that cites something added later links to it without
36
+ being reindexed. Retired spellings convert in memory (`memory:<name>`,
37
+ `wiki:<wiki>/<page>`, a `.md` suffix), and, as lint already allows (#882),
38
+ a memory whose own file is gone resolves to its `.derived` child.
39
+ `akm show` lists an asset's links grouped by kind: `outgoing`, `incoming`
40
+ and the `unresolved` tokens it names, at most 10 per kind with a `total`.
41
+ `akm info` reports links per kind with how many are unresolved. Links do
42
+ not change search ranking, and `related` is unchanged: on the retrieval
43
+ suite, against 0.9.17-alpha.7 on the same index with two runs per arm,
44
+ search nDCG@10 moved +0.002 [−0.005, +0.009] (a rerun of alpha.7 alone
45
+ moved +0.006) and curate P@5 +0.000 [−0.001, +0.001].
46
+ On the retrieval snapshot of the maintainer's 21 bundles (23,979 entries)
47
+ there are 14,917 links: 7,393 `contradicted_by`, 4,401 `xref`, 2,577
48
+ `derived_from`, 540 `cites` and 6 `superseded_by`. 1,801 are unresolved;
49
+ 1,744 of those are `.derived` memories whose parent memory no longer
50
+ exists. (`src/indexer/links/declared-links.ts`,
51
+ `src/storage/repositories/index-links-repository.ts`,
52
+ `src/commands/read/show.ts`, `src/commands/sources/info.ts`)
53
+
54
+ ### Changed
55
+
56
+ - **`akm migrate` converts a v2 or v3 task file straight to v4 in one pass.**
57
+ The chain that read every file as v2, converted it to an intermediate v3
58
+ shape, then converted that to v4 (`src/tasks/source/task-to-v3.ts` ->
59
+ `task-to-v4.ts`, composed by `scripts/akm-migrate/migrate/task-files.ts`)
60
+ is now one planner: `task-to-v4.ts` reads a file once and, for v2, builds
61
+ the v3-shape record in memory — never written to disk or reported as its
62
+ own outcome — before hoisting it to v4 through the same code path a real
63
+ v3 file goes through. `akm migrate status`/`apply` output shapes, and
64
+ every blocked/changed reason code, are unchanged, checked fixture by
65
+ fixture against the prior two-hop chain's actual output (one exception:
66
+ a v3 document that fails only the typed pre-check's own "exactly one
67
+ scheduling source" rule — unreachable through the real chain, which
68
+ always ran that same pre-check first — now reports `invalid-v3-task`
69
+ instead of the raw hoist stage's own `ambiguous-scheduling-source`,
70
+ matching what `akm migrate apply` already returned end to end). The
71
+ `already-v3` and `pending-v2-to-v3-migration` intermediate states are
72
+ gone with the generation split that produced them.
73
+ `src/tasks/source/task-to-v3.ts` (500 lines) is deleted; its logic moved
74
+ into `task-to-v4.ts`, which also drops the duplicate raw-YAML reader and
75
+ outcome-base helpers the two files each carried their own copy of.
76
+ (`src/tasks/source/task-to-v4.ts`, `scripts/akm-migrate/migrate/task-files.ts`)
77
+ - **Curate's support refs come from declared links.** Each curated item's
78
+ support refs (at most two) are now assets its declared links name: what
79
+ it links to, then what links to it, in the order `akm show` lists them,
80
+ skipping assets curate already selected. They no longer come from the LLM
81
+ entity graph's `related` list. On the retrieval snapshot, `related` offered
82
+ support refs for 47 of 867 curate items (5.4%) and declared links for 257
83
+ (29.6%). The retrieval judge graded 157 of those items, asking whether each
84
+ support ref is worth opening next: 56% of declared support refs were useful
85
+ against 68% of `related`'s, so curate attaches about 24 useful support refs
86
+ per 100 items instead of 7. Curate's items are unchanged.
87
+ (`src/commands/read/curate.ts`)
88
+ - **Index layout 26.** The first writable open of an older index derives
89
+ every entry's links from its stored `document_json` in place, reading no
90
+ file: on the 23,979-entry snapshot index that takes 0.8 s and adds 3.5 MB.
91
+ Earlier layouts never stored a workflow's or a task's targets, so only the
92
+ directories holding workflows and tasks re-read on the next `akm index`.
93
+ The open leaves `index_meta.vacuumPending` like every layout migration.
94
+ 0.9.17-alpha.4 through alpha.7 refuse an index at layout 26
95
+ (`INDEX_SCHEMA_INCOMPATIBLE`, naming the upgrade), for writing as well as
96
+ reading, so going back to one of them needs a new index: move `index.db`
97
+ aside and run `akm index` under that release (the LLM graph and enrichment
98
+ cache it held are not rebuilt by `akm index`).
99
+ (`src/storage/repositories/index-schema.ts`,
100
+ `src/storage/repositories/index-entry-schema.ts`)
101
+
102
+ ### Fixed
103
+
104
+ - **`index.graph.enabled: false` stops graph extraction in `akm improve`.**
105
+ The switch was read only when improve had no strategy plan, which is never
106
+ the case in a real run, so the nightly and weekly graph tasks kept
107
+ extracting with it set. Improve now skips its graph extraction stage
108
+ whenever `index.graph.enabled` is `false`, whatever the strategy enables.
109
+ This also makes the graph ablation harness's "graph off" arm, which sets
110
+ exactly this key, turn extraction off. Improve still does not read
111
+ `index.defaults` when it picks the engine for graph extraction.
112
+ (`src/commands/improve/loop-stages.ts`)
113
+ - **Graph extraction never has more calls in flight than the run's
114
+ concurrency.** Batches run side by side up to the runner's concurrency, and
115
+ each batch also sent its per-file calls (long bodies, a non-array response)
116
+ up to that limit at once, so at a concurrency of 2 four calls could be in
117
+ flight. A batch now makes its per-file calls one at a time. At the default
118
+ concurrency of 1 nothing changes. (`src/llm/graph-extract.ts`)
119
+ - **A long document whose extraction partly failed is extracted again.** A
120
+ body over 1,600 characters is extracted in chunks. When some chunks failed
121
+ (a timeout, an error, an empty response) and others found entities, the
122
+ file was recorded and cached as extracted, so the failed chunks were never
123
+ retried. Such a file is now recorded as failed and not cached, and the next
124
+ run extracts it again, every chunk: partial failures are rare outside
125
+ provider outages, and keeping per-chunk results to skip the chunks that
126
+ succeeded would need a second cache. Until then the entities the other
127
+ chunks found are stored when the file had no graph rows yet; a file with
128
+ rows keeps them. (`src/llm/graph-extract.ts`)
129
+ - **`scripts/node-runtime/akm` could die from a raw signal instead of
130
+ forwarding it to its child.** The launcher registered its
131
+ SIGTERM/SIGINT/SIGHUP forwarding listeners only after spawning the child;
132
+ under load, a signal could arrive in that window and fall through to the
133
+ runtime's default (process-terminating) disposition, killing the launcher
134
+ before the child ever saw it. The listeners now go up before anything else
135
+ runs, with a small queue for a signal that arrives before the child exists.
136
+ - **`scripts/node-runtime/akm-migrate` carried the same pre-spawn signal
137
+ race** as `scripts/node-runtime/akm` above, for the same reason (listeners
138
+ registered only after `spawn()`), with no test covering it. Fixed the same
139
+ way, and added `tests/integration/akm-migrate-signal-forwarding.test.ts`
140
+ (modelled on `launcher-signal-forwarding.test.ts`) for its forwarding.
141
+ - **The launcher signal tests failed intermittently because of their own
142
+ fixture.** The fake child wrote its ready file before it registered its
143
+ signal handler, so a forwarded signal could reach it in between and kill it,
144
+ and the launcher then reported that signal. Each fixture now registers its
145
+ handler first. The launcher's pre-spawn window above could not cause this:
146
+ the tests signal only after the child is running.
147
+
148
+ ## [0.9.17-alpha.7] - 2026-09-28
149
+
150
+ A scheduled task is now just a command and a schedule. Each native row
151
+ carries its own `AKM_BUNDLE_DIR` instead of pointing at a descriptor file,
152
+ and the runtime reads only v4 task files; older files convert once with
153
+ `akm migrate apply`. The first `akm task sync` after upgrading rewrites each
154
+ row once, keeping every task and schedule. `akm improve` reworks only assets
155
+ that retrieval returned or that are new, and reflect refuses a rewrite that
156
+ grades worse on the asset's own searches. `--require-engines` no longer
157
+ skips a run because the LLM endpoint is busy.
158
+
159
+ ### Changed
160
+
161
+ - **akm reads only task source v4.** A `version: 2` or `version: 3` task
162
+ file, or a `version: 4` file that still carries 0.9.15's retired
163
+ `schedule[].enabled`, now fails on its own with a message naming
164
+ `akm migrate apply`, which converts it once, under a backup (`akm upgrade`
165
+ runs it after an install). Until now every read converted such a file in
166
+ memory. `akm task sync` reports each one as a failure, leaves its installed
167
+ row as it is, and keeps reconciling every other task; `akm task run`,
168
+ `akm lint` and `akm task validate` report it the same way, and
169
+ `akm task validate`'s `converts` outcome is gone (such a file is
170
+ `blocked`). `akm migrate apply` now also converts the tasks of the stash
171
+ `AKM_BUNDLE_DIR` selects when no configured bundle names it, since the
172
+ runtime reads those too, and a root whose top-level task files are all
173
+ v2/v3 is still detected as an `akm-task` bundle, so they are found and
174
+ converted. A host whose task files are all v4 (`akm migrate status`
175
+ reports `current`) sees no difference.
176
+ (`src/tasks/source/parse-task-source.ts`, `src/commands/tasks/validate.ts`,
177
+ `scripts/akm-migrate/task-migrate.ts`,
178
+ `src/core/adapter/adapters/akm-task-adapter.ts`)
179
+ - **A scheduled row is its command plus its schedule, and carries its own
180
+ context.** Rows no longer name a `--scheduler-context` descriptor file;
181
+ they set what it held themselves. Every row sets `AKM_BUNDLE_DIR` to the
182
+ working stash of the shell that ran `akm task sync`, plus any
183
+ `AKM_CONFIG_DIR`, `AKM_DATA_DIR`, `AKM_CACHE_DIR` or `AKM_STATE_DIR` that
184
+ shell set explicitly: a `VAR=value` prefix in the crontab, an
185
+ `EnvironmentVariables` entry in a launchd plist, a `$env:VAR='value';`
186
+ assignment ahead of the command in Task Scheduler (its action has no
187
+ environment of its own). Scheduled runs see the same environment as
188
+ before, and sync still tells installations sharing a crontab apart by
189
+ that path (#846).
190
+ **What hosts see:** the first `akm task sync` after upgrading rewrites
191
+ every akm row once. `akm task sync --dry-run` lists each one as an update,
192
+ never an add or a remove; each keeps its launcher and its schedule, and
193
+ the `--scheduler-context <file>` argument becomes an inline
194
+ `AKM_BUNDLE_DIR=<working stash>`. On a host whose working stash is
195
+ `/home/u/akm` a row changes from
196
+
197
+ ```text
198
+ 30 8 * * * /home/u/.bun/bin/bun /home/u/.bun/lib/node_modules/akm-cli/dist/akm --scheduler-context /home/u/.local/share/akm/tasks/context/e898….json task run capture --bundle akm --scheduled > /home/u/.cache/akm/tasks/logs/capture.log 2>&1
199
+ ```
200
+
201
+ to
202
+
203
+ ```text
204
+ 30 8 * * * AKM_BUNDLE_DIR=/home/u/akm /home/u/.bun/bin/bun /home/u/.bun/lib/node_modules/akm-cli/dist/akm task run capture --bundle akm --scheduled > /home/u/.cache/akm/tasks/logs/capture.log 2>&1
205
+ ```
206
+
207
+ Rows written by 0.9.0 through 0.9.17-alpha.6 keep firing until that sync:
208
+ the CLI still accepts `--scheduler-context <file>` and applies the file's
209
+ environment (PATH included, for a 0.9.16 row). The files under
210
+ `$DATA/tasks/context/` are no longer written, and the uid, mode, symlink
211
+ and content-hash checks made on every scheduled run are gone. A sync
212
+ leaves some rows as they are (one whose task file failed to load, one a
213
+ `--bundle` sync did not cover), and those still name their file: once
214
+ `akm task doctor` lists no binding with a `contextPath`, nothing reads
215
+ them and they can be deleted. `akm task prune` now
216
+ finds rows whose `AKM_BUNDLE_DIR` names a directory that is gone, and older
217
+ rows whose descriptor cannot be read; `akm task doctor` lists `contextPath`
218
+ only for an older row. (`src/tasks/scheduler-invocation.ts`,
219
+ `src/tasks/backends/cron.ts`, `src/tasks/backends/launchd.ts`,
220
+ `src/tasks/backends/schtasks.ts`, `src/tasks/scheduler-sync.ts`,
221
+ `src/commands/tasks/tasks.ts`)
222
+ - **`akm improve` reworks only what gets read (#986).** An asset with fresh
223
+ feedback, or one you name (`akm improve skills/x`), is handled as before.
224
+ Every other pick must now be in the retrieval scope. That covers the
225
+ proactive-maintenance, high-salience and forgetting-safety lanes, and the
226
+ memories consolidation judges. An asset is in scope if a user `search`,
227
+ `curate` or `show` returned it, or user `feedback` named it, in the last 90
228
+ days, which is the usage log's retention. A hit on a `.derived` memory counts
229
+ for its parent. New material that no improve stage has processed yet is also
230
+ in scope. There is no new config key.
231
+
232
+ Measured with `akm improve --dry-run` on a copy of the maintainer's bundle
233
+ (19,870 assets), against 0.9.17-alpha.6:
234
+ - The fallback lanes pick from 6,450 assets instead of 15,686, and 9,236
235
+ refs are left out. No lane setting can reach the unread tail any more. In
236
+ July the proactive lane rewrote 3,069 assets, and 3,059 of them had not
237
+ been retrieved since the usage log began on 1 July.
238
+ - Consolidation judges 59 memories instead of 69.
239
+ - The high-salience lane no longer admits distill outputs nobody has read (2
240
+ today).
241
+ - Under the scheduled caps, today's nightly work is unchanged. The default
242
+ strategy selects the same 50 feedback-driven refs, and weekly proactive
243
+ maintenance selects the same 25, because salience ranking already puts
244
+ retrieved assets first.
245
+
246
+ `akm improve --dry-run` and the run result report the left-out refs as a new
247
+ `retrieval` gate. Health reports them under the skip reason `not_retrieved`.
248
+ Improve results stored by earlier releases, which have no such gate, still
249
+ decode. (`src/commands/improve/retrieval-scope.ts`,
250
+ `src/commands/improve/preparation.ts`, `src/commands/improve/consolidate.ts`)
251
+
252
+ - **Reflect refuses a rewrite that makes an asset worse for its own searches
253
+ (#722).** Before reflect proposes a rewrite of an existing asset, it grades
254
+ the old and the new content on up to five of the queries that actually
255
+ retrieved the asset (user `search` and `curate`). It uses the retrieval
256
+ eval's relevance prompt, which agrees with human grades at kappa 0.83. When
257
+ the new content grades lower on average, the rewrite is refused the same way
258
+ a quality-judge rejection is: `quality_rejected`, with the 14-day reflect
259
+ window. An asset without retrieval queries is not graded.
260
+
261
+ This was measured before it was built. Of 60 accepted rewrites since July,
262
+ judged this way, 14 graded lower (23%, 95% CI 14–35%) and 12 graded higher.
263
+ The gate was built because the lower bound cleared the 10% threshold set
264
+ before any judging. It costs two judge calls per query on the engine that
265
+ already runs the quality judge. On the maintainer's 2026-09-28 nightly run,
266
+ whose 30 rewrites had 102 usable queries, that is 204 calls, about 6 more
267
+ minutes on a 73-minute run. (`src/commands/improve/retrieval-gate.ts`,
268
+ `src/commands/improve/reflect.ts`)
269
+
270
+ ### Fixed
271
+
272
+ - **A cron row too long for one line is seen by `akm task sync` again.** A
273
+ command over 1,000 bytes runs from a wrapper script, and sync could not
274
+ read which task such a row ran: every sync, and every `--dry-run`, showed
275
+ it as an add and wrote it again. Sync now reads the script, so the row is
276
+ unchanged or an update like any other. (`src/tasks/backends/cron.ts`)
277
+ - **A `$` or a backslash in a scheduled row's value is kept.** launchd and
278
+ Task Scheduler rows passed their values through a string replacement that
279
+ read `$'`, `$&` and `$$` as patterns, so a path such as a Windows admin
280
+ share (`\\nas\share\akm$`) came out corrupted; reading a crontab row
281
+ back dropped a backslash inside a single-quoted value.
282
+ (`src/tasks/backends/launchd.ts`, `src/tasks/backends/schtasks.ts`,
283
+ `src/tasks/backends/cron.ts`)
284
+ - **`akm improve --require-engines` no longer skips a run because the LLM
285
+ endpoint is busy.** Its reachability probe, one short completion, gave up
286
+ after 3 seconds, so a local server busy with another job looked
287
+ unreachable and the whole scheduled run failed (all four scheduled runs on
288
+ 2026-09-27). The probe now waits up to the engine's own `timeoutMs`, at
289
+ most two minutes, so a busy server can answer while a hung one still fails
290
+ fast. The error's hint now says to check the endpoint rather than to run
291
+ `akm setup`.
292
+ (`src/commands/improve/improve-cli.ts`)
293
+
9
294
  ## [0.9.17-alpha.6] - 2026-09-27
10
295
 
11
296
  Graph extraction stops losing and wasting work. A timed-out extraction is
package/STABILITY.md CHANGED
@@ -281,8 +281,8 @@ CHANGELOG with a migration note.
281
281
  print unredacted. `akm task validate <path>` (new in 0.9.11) is the same
282
282
  kind of zero-write introspection as `explain`, but takes a bare filesystem
283
283
  path rather than a bundle-qualified ref — it reports whether that ONE file
284
- would parse cleanly (`valid`), auto-convert from task v2/v3 (`converts`),
285
- need a human decision the deterministic migrator can't make (`blocked`),
284
+ would parse cleanly (`valid`), need `akm migrate apply` first (`blocked`:
285
+ task v2/v3, or a retired `schedule[].enabled`),
286
286
  fail schema validation (`invalid`), or isn't a task source at all
287
287
  (`not-a-task`) — exactly the diagnostic `akm task sync` would produce for
288
288
  it, before the file is ever wired into a bundle or the scheduler.
package/dist/akm CHANGED
@@ -6,13 +6,55 @@
6
6
  import { spawn, spawnSync } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
- // `--scheduler-context <descriptor>` passes through untouched: the CLI loads
10
- // and validates it before anything else (`consumeSchedulerContextArg` in
11
- // src/cli.ts). The scheduler supplies PATH itself (the crontab's `# akm:env`
12
- // line, a launchd plist's EnvironmentVariables), so runtime selection below
13
- // needs nothing from the descriptor. This launcher used to re-validate the
14
- // descriptor with its own copy of the schema, and that copy rejected every
15
- // descriptor the CLI writes since 0.9.17.
9
+ // #956 round 3: install the signal-forwarding scaffolding before ANYTHING
10
+ // else in this process — env var setup, the bun-version probe, spawning the
11
+ // child — gets a chance to run. The three `process.once` listeners used to
12
+ // go up only after spawn() (below) returned; a scheduler preemption right
13
+ // after that syscall was enough for a signal to arrive with no listener yet,
14
+ // and the runtime's default (process-terminating) disposition killed the
15
+ // launcher outright, never reaching the child at all. `child` starts
16
+ // unset: a signal that arrives before it exists is queued and flushed the
17
+ // moment it does; if this process ends up never spawning one (the
18
+ // direct-import branch below), the queued signal is re-raised against
19
+ // ourselves once we hand default disposition back, so it behaves exactly
20
+ // like the runtime's own default rather than being silently swallowed.
21
+ let child;
22
+ let childExited = false;
23
+ const pendingSignals = [];
24
+ const forwardHandlers = new Map();
25
+ const forwardSignal = (signal) => {
26
+ if (childExited) return;
27
+ if (!child) {
28
+ pendingSignals.push(signal);
29
+ return;
30
+ }
31
+ try {
32
+ child.kill(signal);
33
+ } catch {
34
+ // Child exited in the race between the check above and here.
35
+ }
36
+ };
37
+ // `.once`, not `.on`: Node/Bun suppress a signal's default
38
+ // (process-terminating) disposition for as long as ANY listener stays
39
+ // registered for it. A persistent `.on` listener would still be registered
40
+ // when the `process.kill(process.pid, result.signal)` re-raise below runs at
41
+ // shutdown, swallowing it and leaving this launcher exiting 0 instead of
42
+ // reflecting the child's signal. `.once` consumes only the
43
+ // externally-delivered signal that triggers the forward, so the re-raise
44
+ // correctly falls through to the OS default.
45
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
46
+ const handler = () => forwardSignal(signal);
47
+ forwardHandlers.set(signal, handler);
48
+ process.once(signal, handler);
49
+ }
50
+
51
+ // A scheduled row sets its environment itself (a cron `VAR=value` prefix, a
52
+ // launchd plist's EnvironmentVariables), and the scheduler supplies PATH, so
53
+ // runtime selection below needs nothing from it. A row written by 0.9.0 –
54
+ // 0.9.17-alpha.6 passes `--scheduler-context <descriptor>` instead; it passes
55
+ // through untouched, and the CLI applies it (`consumeSchedulerContextArg` in
56
+ // src/cli.ts). This launcher used to re-validate the descriptor with its own
57
+ // copy of the schema, and that copy rejected every descriptor 0.9.17 wrote.
16
58
 
17
59
  process.env.AKM_LAUNCHER_NODE = process.execPath;
18
60
  process.env.AKM_LAUNCHER_PATH = fileURLToPath(import.meta.url);
@@ -42,13 +84,21 @@ const bunEntry = fileURLToPath(new URL("./cli.js", import.meta.url));
42
84
  const nodeEntry = fileURLToPath(new URL("./cli-node.mjs", import.meta.url));
43
85
 
44
86
  if (!useBun && !process.versions.bun) {
45
- await import("./cli-node.mjs");
87
+ // No child is ever spawned on this path — the imported module runs
88
+ // in-process, so it IS the work, and the runtime's ordinary default signal
89
+ // disposition is exactly correct for it. Hand that back (the forwarding
90
+ // listeners above no longer apply here), re-raising anything that already
91
+ // arrived so it is not silently swallowed by a listener that now does
92
+ // nothing.
93
+ for (const [signal, handler] of forwardHandlers) process.removeListener(signal, handler);
94
+ if (pendingSignals.length > 0) process.kill(process.pid, pendingSignals[0]);
95
+ else await import("./cli-node.mjs");
46
96
  } else {
47
97
  const command = useBun ? (process.versions.bun ? process.execPath : "bun") : "node";
48
98
  const entry = useBun ? bunEntry : nodeEntry;
49
99
  const runtime = useBun ? "Bun" : "Node.js";
50
100
  const result = await new Promise((resolve) => {
51
- const child = spawn(command, [entry, ...process.argv.slice(2)], {
101
+ child = spawn(command, [entry, ...process.argv.slice(2)], {
52
102
  stdio: "inherit",
53
103
  env: process.env,
54
104
  // #956: give the child its OWN process group on POSIX (`setsid()` —
@@ -75,29 +125,12 @@ if (!useBun && !process.versions.bun) {
75
125
  // forward once the child has already exited: forwarding to a
76
126
  // dead/replaced pid would be at best a no-op and at worst a signal to
77
127
  // an unrelated process that reused the pid.
78
- let childExited = false;
79
128
  child.once("exit", () => {
80
129
  childExited = true;
81
130
  });
82
- const forwardSignal = (signal) => {
83
- if (childExited) return;
84
- try {
85
- child.kill(signal);
86
- } catch {
87
- // Child exited in the race between the check above and here.
88
- }
89
- };
90
- // `.once`, not `.on`: Node/Bun suppress a signal's default
91
- // (process-terminating) disposition for as long as ANY listener stays
92
- // registered for it. A persistent `.on` listener would still be
93
- // registered when the `process.kill(process.pid, result.signal)`
94
- // re-raise below runs at shutdown, swallowing it and leaving this
95
- // launcher exiting 0 instead of reflecting the child's signal. `.once`
96
- // consumes only the externally-delivered signal that triggers the
97
- // forward, so the re-raise correctly falls through to the OS default.
98
- for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
99
- process.once(signal, () => forwardSignal(signal));
100
- }
131
+ // Flush whatever arrived in the (now much smaller) window between the
132
+ // listeners going up and the child existing to receive them.
133
+ for (const signal of pendingSignals.splice(0)) forwardSignal(signal);
101
134
  child.once("error", (error) => resolve({ error }));
102
135
  child.once("exit", (code, signal) => resolve({ code, signal }));
103
136
  });
package/dist/akm-migrate CHANGED
@@ -6,6 +6,40 @@
6
6
  import { spawn, spawnSync } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
+ // #956 round 3: install the signal-forwarding scaffolding before ANYTHING
10
+ // else in this process — the Node-version check, the bun-version probe,
11
+ // spawning the child — gets a chance to run. See the matching fix and
12
+ // comment in scripts/node-runtime/akm: the three `process.once` listeners
13
+ // used to go up only after spawn() (below) returned; a scheduler preemption
14
+ // right after that syscall was enough for a signal to arrive with no
15
+ // listener yet, and the runtime's default (process-terminating) disposition
16
+ // killed the launcher outright, never reaching the child at all. `child`
17
+ // starts unset: a signal that arrives before it exists is queued and
18
+ // flushed the moment it does.
19
+ let child;
20
+ let childExited = false;
21
+ const pendingSignals = [];
22
+ const forwardSignal = (signal) => {
23
+ if (childExited) return;
24
+ if (!child) {
25
+ pendingSignals.push(signal);
26
+ return;
27
+ }
28
+ try {
29
+ child.kill(signal);
30
+ } catch {
31
+ // Child exited in the race between the check above and here.
32
+ }
33
+ };
34
+ // `.once`, not `.on`: see the matching comment in scripts/node-runtime/akm —
35
+ // a persistent `.on` listener would still be registered when the
36
+ // `process.kill(process.pid, result.signal)` re-raise below runs,
37
+ // suppressing the OS default disposition and leaving this launcher exiting
38
+ // 0 instead of reflecting the child's signal.
39
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
40
+ process.once(signal, () => forwardSignal(signal));
41
+ }
42
+
9
43
  if (!process.versions.bun) {
10
44
  const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
11
45
  if (major < 22) {
@@ -34,7 +68,7 @@ const nodeEntry = fileURLToPath(new URL("./scripts/akm-migrate-node.js", import.
34
68
  const command = process.versions.bun ? process.execPath : useBun ? "bun" : process.execPath;
35
69
  const entry = process.versions.bun || useBun ? bunEntry : nodeEntry;
36
70
  const result = await new Promise((resolve) => {
37
- const child = spawn(command, [entry, ...process.argv.slice(2)], {
71
+ child = spawn(command, [entry, ...process.argv.slice(2)], {
38
72
  stdio: "inherit",
39
73
  env,
40
74
  // #956: own process group on POSIX so the launcher's forward below is
@@ -43,27 +77,12 @@ const nodeEntry = fileURLToPath(new URL("./scripts/akm-migrate-node.js", import.
43
77
  // directly — see the matching comment in scripts/node-runtime/akm.
44
78
  detached: process.platform !== "win32",
45
79
  });
46
- let childExited = false;
47
80
  child.once("exit", () => {
48
81
  childExited = true;
49
82
  });
50
- const forwardSignal = (signal) => {
51
- if (childExited) return;
52
- try {
53
- child.kill(signal);
54
- } catch {
55
- // Child exited in the race between the check above and here.
56
- }
57
- };
58
- // `.once`, not `.on`: see the matching comment in
59
- // scripts/node-runtime/akm — a persistent `.on` listener would still be
60
- // registered when the `process.kill(process.pid, result.signal)`
61
- // re-raise below runs, suppressing the OS default disposition and
62
- // leaving this launcher exiting 0 instead of reflecting the child's
63
- // signal.
64
- for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
65
- process.once(signal, () => forwardSignal(signal));
66
- }
83
+ // Flush whatever arrived in the (now much smaller) window between the
84
+ // listeners going up and the child existing to receive them.
85
+ for (const signal of pendingSignals.splice(0)) forwardSignal(signal);
67
86
  child.once("error", (error) => resolve({ error }));
68
87
  child.once("exit", (code, signal) => resolve({ code, signal }));
69
88
  });
@@ -0,0 +1,6 @@
1
+ You are grading search-and-retrieval results for an AI coding agent's knowledge base (the akm tool). For the given query and ONE candidate asset, grade how useful loading this asset would be, on this scale:
2
+ 3 = exactly the asset an agent should load for this query or task; it directly answers or performs the request.
3
+ 2 = relevant and clearly useful, though not the single best asset for the query.
4
+ 1 = same general topic as the query, but would not actually help complete this specific query or task.
5
+ 0 = unrelated to the query.
6
+ Reply with ONLY a JSON object: {"grade": <integer 0-3>, "reason": "<=25 words"}.
@@ -3,7 +3,7 @@ type: fact
3
3
  category: convention
4
4
  description: How to cross-link assets so retrieval compounds — a provenance xref when derived, sparse real associative xrefs, corrections as new assets, and canonical entity naming.
5
5
  when_to_use: Surfaced to authoring agents when they create or revise any asset that derives from, corrects, or relates to another asset.
6
- updated: 2026-07-28
6
+ updated: 2026-09-28
7
7
  ---
8
8
 
9
9
  <!--
@@ -19,8 +19,10 @@ updated: 2026-07-28
19
19
 
20
20
  Cross-references are how knowledge compounds instead of being re-derived every
21
21
  session. In AKM they are also **indexed**: the strings in an asset's `xrefs:`
22
- frontmatter fold into its search-hint text, and knowledge/memory bodies feed an
23
- LLM-extracted entity/relation graph that boosts ranking. So links are a retrieval
22
+ frontmatter fold into its search-hint text and are stored as links that
23
+ `akm show` lists on both assets (with `supersededBy:`, `contradictedBy:` and a
24
+ `.derived` memory's parent), and knowledge/memory bodies feed an LLM-extracted
25
+ entity/relation graph that boosts ranking. So links are a retrieval
24
26
  lever — which means both too few and too many hurt.
25
27
 
26
28
  ```yaml
@@ -26,6 +26,7 @@ import { warn, warnVerbose } from "../../core/warn.js";
26
26
  import { resolveWriteTarget } from "../../core/write-source.js";
27
27
  import { deriveInstallations } from "../../indexer/installations.js";
28
28
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
29
+ import { USAGE_EVENT_RETENTION_DAYS } from "../../indexer/usage/usage-events.js";
29
30
  import { assertRunnerCredentials } from "../../integrations/agent/runner-dispatch.js";
30
31
  import { cosineSimilarity, embedBatch, resolveEmbeddingModelId } from "../../llm/embedder.js";
31
32
  import { getBodyEmbeddings, upsertBodyEmbeddings } from "../../storage/repositories/embeddings-repository.js";
@@ -39,6 +40,7 @@ import { sanitizeMergedContent } from "./consolidate/sanitize.js";
39
40
  import { contentHash } from "./content-hash.js";
40
41
  import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
41
42
  import { isLedgerBlocked, ledgerKey, loadLedgerSnapshot, recordLedgerAttempt } from "./ledger.js";
43
+ import { isInRetrievalScope, loadRetrievalScope } from "./retrieval-scope.js";
42
44
  import { callStage, mintProposal, noticeSet, stageRunner } from "./stage.js";
43
45
  /** A plan op worth acting on. Retired advisory ops (merge/delete/contradict) are dropped, never thrown on. */
44
46
  export function isValidOp(op) {
@@ -428,6 +430,11 @@ export function inspectConsolidationPool(opts, stashDir, warnings, existingKnowl
428
430
  });
429
431
  }
430
432
  const judgedUnchanged = poolSize - memories.length;
433
+ // Only what retrieval returned or new material improve never processed (#986).
434
+ const retrievalScope = loadRetrievalScope({ proposalsCtx: opts.proposalsCtx, readOnly }, stashDir);
435
+ const beforeScope = memories.length;
436
+ memories = memories.filter((memory) => isInRetrievalScope(retrievalScope, conceptIdFromTypeName("memory", memory.name), memory.filePath));
437
+ const outsideRetrievalScope = beforeScope - memories.length;
431
438
  if (opts.incrementalSince && memories.length > 0) {
432
439
  memories = narrowToIncrementalCandidates(memories, opts.incrementalSince, warnings, opts.neighborsPerChanged, readOnly);
433
440
  }
@@ -465,6 +472,7 @@ export function inspectConsolidationPool(opts, stashDir, warnings, existingKnowl
465
472
  memories,
466
473
  prefilteredAlreadyPromoted,
467
474
  judgedUnchanged,
475
+ outsideRetrievalScope,
468
476
  };
469
477
  }
470
478
  const ABORT_MIN_CHUNKS = 4;
@@ -641,6 +649,9 @@ async function consolidate(opts, config, stashDir, startMs, stateDb) {
641
649
  if (pool.judgedUnchanged > 0) {
642
650
  warnings.push(`Consolidation: skipped ${pool.judgedUnchanged} ${plural(pool.judgedUnchanged)} judged within the revisit window and unchanged since.`);
643
651
  }
652
+ if (pool.outsideRetrievalScope > 0) {
653
+ warnings.push(`Consolidation: skipped ${pool.outsideRetrievalScope} ${plural(pool.outsideRetrievalScope)} already judged that retrieval has not returned in the last ${USAGE_EVENT_RETENTION_DAYS} days.`);
654
+ }
644
655
  if (prefilteredAlreadyPromoted > 0) {
645
656
  warnings.push(`Consolidation: pre-filtered ${prefilteredAlreadyPromoted} ${plural(prefilteredAlreadyPromoted)} whose body already exists verbatim in knowledge/ before chunking.`);
646
657
  }