akm-cli 0.9.17 → 0.9.19-alpha.1

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 +157 -0
  2. package/STABILITY.md +2 -1
  3. package/dist/assets/hints/cli-hints-full.md +4 -2
  4. package/dist/assets/hints/cli-hints-short.md +5 -3
  5. package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
  6. package/dist/commands/feedback-cli.js +1 -1
  7. package/dist/commands/health/checks.js +6 -6
  8. package/dist/commands/health.js +3 -3
  9. package/dist/commands/improve/consolidate/coverage.js +132 -0
  10. package/dist/commands/improve/consolidate/pair-pass.js +31 -21
  11. package/dist/commands/improve/consolidate.js +46 -13
  12. package/dist/commands/improve/distill.js +8 -4
  13. package/dist/commands/improve/eligibility.js +39 -9
  14. package/dist/commands/improve/improve-cli.js +9 -6
  15. package/dist/commands/improve/improve.js +13 -4
  16. package/dist/commands/improve/ledger.js +2 -2
  17. package/dist/commands/improve/loop-stages.js +2 -0
  18. package/dist/commands/improve/preparation.js +3 -1
  19. package/dist/commands/improve/reflect.js +2 -2
  20. package/dist/commands/improve/stage.js +43 -12
  21. package/dist/commands/proposal/diff-format.js +21 -0
  22. package/dist/commands/proposal/proposal-cli.js +48 -10
  23. package/dist/commands/proposal/proposal-types.js +11 -0
  24. package/dist/commands/proposal/proposal.js +60 -5
  25. package/dist/commands/proposal/repository.js +250 -18
  26. package/dist/commands/read/knowledge.js +13 -11
  27. package/dist/commands/read/remember-cli.js +7 -3
  28. package/dist/commands/sources/source-clone.js +1 -1
  29. package/dist/commands/tasks/tasks-cli.js +1 -1
  30. package/dist/commands/tasks/tasks.js +10 -3
  31. package/dist/core/mutation-target.js +8 -3
  32. package/dist/core/write-source.js +3 -2
  33. package/dist/indexer/usage/usage-events.js +2 -1
  34. package/dist/output/shapes/helpers.js +7 -0
  35. package/dist/output/shapes/passthrough.js +1 -0
  36. package/dist/output/shapes/proposal/reopen.js +14 -0
  37. package/dist/output/shapes.js +2 -0
  38. package/dist/output/text/helpers.js +1 -1
  39. package/dist/output/text/proposal/proposal.js +3 -1
  40. package/dist/output/text/proposal-format.js +87 -32
  41. package/dist/scripts/akm-migrate-node.js +102 -25
  42. package/dist/scripts/akm-migrate.js +102 -25
  43. package/dist/storage/repositories/improve-ledger-repository.js +65 -6
  44. package/dist/storage/repositories/index-vec-repository.js +13 -8
  45. package/dist/storage/repositories/proposals-repository.js +23 -0
  46. package/dist/storage/sqlite-read-snapshot.js +46 -2
  47. package/dist/storage/state-db-integrity.js +12 -9
  48. package/dist/tasks/run/load-task.js +5 -1
  49. package/docs/migration/README.md +1 -0
  50. package/docs/migration/release-notes/0.9.19.md +134 -0
  51. package/docs/migration/release-notes/README.md +5 -0
  52. package/docs/migration/v0.7-to-v0.8.md +2 -2
  53. package/docs/migration/v0.8-to-v0.9.md +5 -1
  54. package/docs/reference/cli.md +189 -28
  55. package/docs/reference/configuration.md +9 -8
  56. package/docs/reference/data-and-telemetry.md +24 -16
  57. package/package.json +1 -1
@@ -106,6 +106,20 @@ const GATE_OUTCOMES = {
106
106
  staged: true,
107
107
  "auto-rejected": true,
108
108
  };
109
+ function isRecord(value) {
110
+ return typeof value === "object" && value !== null && !Array.isArray(value);
111
+ }
112
+ /** One undone rejection in `metadata_json.reviewHistory` (see `ProposalReviewHistoryEntry`): only the fields readers rely on are checked. */
113
+ function isReviewHistoryEntry(value) {
114
+ if (!isRecord(value))
115
+ return false;
116
+ const { review, gateDecision, reopenedAt, reopenReason } = value;
117
+ return (typeof reopenedAt === "string" &&
118
+ (reopenReason === undefined || typeof reopenReason === "string") &&
119
+ (review === undefined ||
120
+ (isRecord(review) && typeof review.outcome === "string" && typeof review.decidedAt === "string")) &&
121
+ (gateDecision === undefined || isRecord(gateDecision)));
122
+ }
109
123
  function validatePresentMetadata(meta) {
110
124
  const stringFields = ["sourceRun", "beforeHash", "beforeHashNormalized", "backupContent"];
111
125
  for (const field of stringFields) {
@@ -129,6 +143,12 @@ function validatePresentMetadata(meta) {
129
143
  invalidPresentField("review");
130
144
  }
131
145
  }
146
+ if (Object.hasOwn(meta, "reviewHistory")) {
147
+ const history = meta.reviewHistory;
148
+ if (!Array.isArray(history) || history.some((entry) => !isReviewHistoryEntry(entry))) {
149
+ invalidPresentField("reviewHistory");
150
+ }
151
+ }
132
152
  if (Object.hasOwn(meta, "gateDecision")) {
133
153
  const gate = meta.gateDecision;
134
154
  if (typeof gate !== "object" ||
@@ -270,6 +290,7 @@ export function proposalRowToProposal(row) {
270
290
  ...(typeof meta.beforeHash === "string" ? { beforeHash: meta.beforeHash } : {}),
271
291
  ...(typeof meta.beforeHashNormalized === "string" ? { beforeHashNormalized: meta.beforeHashNormalized } : {}),
272
292
  ...(meta.review !== undefined ? { review: meta.review } : {}),
293
+ ...(meta.reviewHistory !== undefined ? { reviewHistory: meta.reviewHistory } : {}),
273
294
  ...(typeof meta.confidence === "number" ? { confidence: meta.confidence } : {}),
274
295
  ...(meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {}),
275
296
  ...(typeof meta.backupContent === "string" ? { backupContent: meta.backupContent } : {}),
@@ -332,6 +353,8 @@ export function proposalToRowValues(proposal, stashDir) {
332
353
  metaObj.sourceRun = proposal.sourceRun;
333
354
  if (proposal.review !== undefined)
334
355
  metaObj.review = proposal.review;
356
+ if (proposal.reviewHistory !== undefined)
357
+ metaObj.reviewHistory = proposal.reviewHistory;
335
358
  if (proposal.confidence !== undefined)
336
359
  metaObj.confidence = proposal.confidence;
337
360
  if (proposal.gateDecision !== undefined)
@@ -9,7 +9,12 @@
9
9
  * must not do that. This helper copies a stable main/WAL pair and opens that
10
10
  * private copy, so SQLite never attaches to the operator's original
11
11
  * main/WAL/SHM files.
12
+ *
13
+ * The copy itself is made by a child process, never by this one: see
14
+ * {@link copyFileOutsideThisProcess} for why an in-process read of a live
15
+ * database is unsafe.
12
16
  */
17
+ import { spawnSync } from "node:child_process";
13
18
  import fs from "node:fs";
14
19
  import os from "node:os";
15
20
  import path from "node:path";
@@ -57,6 +62,45 @@ function fingerprintsEqual(left, right) {
57
62
  const sameFile = (a, b) => a?.size === b?.size && a?.mtimeNs === b?.mtimeNs && a?.ctimeNs === b?.ctimeNs;
58
63
  return sameFile(left.main, right.main) && sameFile(left.wal, right.wal);
59
64
  }
65
+ /**
66
+ * Copy `source` to `destination` without opening `source` in THIS process.
67
+ *
68
+ * POSIX advisory locks belong to the process, not to the descriptor: when a
69
+ * process closes ANY descriptor for a file, the kernel drops every lock that
70
+ * process holds on it, including the SHARED lock a live SQLite connection in
71
+ * this same process (`akm improve` keeps one on state.db for the whole run)
72
+ * holds. A peer using an older SQLite (< 3.51) read-write would then see no
73
+ * reader, take EXCLUSIVE when it closes, and delete the `-wal`/`-shm` this
74
+ * process is still using. The read therefore happens in a child `cp`, whose
75
+ * descriptors and locks are its own. Windows locks belong to the handle, so
76
+ * an in-process copy cannot release another handle's lock there (and there is
77
+ * no `cp`).
78
+ *
79
+ * A source that vanished (a WAL checkpointed away mid-copy) throws an
80
+ * `ENOENT` error, which {@link openSqliteReadSnapshot} retries.
81
+ */
82
+ function copyFileOutsideThisProcess(source, destination) {
83
+ if (process.platform === "win32") {
84
+ fs.copyFileSync(source, destination);
85
+ return;
86
+ }
87
+ const result = spawnSync("cp", ["--", source, destination], {
88
+ encoding: "utf8",
89
+ stdio: ["ignore", "ignore", "pipe"],
90
+ });
91
+ if (result.error) {
92
+ throw new SqliteReadSnapshotUnavailableError(`cannot run cp to copy ${source}: ${result.error.message}`);
93
+ }
94
+ if (result.status === 0)
95
+ return;
96
+ // `cp` reports its errors as text, so the vanished-source case is recognised
97
+ // by looking at the source itself.
98
+ if (fileFingerprint(source) === undefined) {
99
+ throw Object.assign(new Error(`${source} disappeared while it was being copied`), { code: "ENOENT" });
100
+ }
101
+ const reason = result.stderr.trim() || (result.signal ? `killed by ${result.signal}` : `exit status ${result.status}`);
102
+ throw new Error(`cp could not copy ${source}: ${reason}`);
103
+ }
60
104
  /**
61
105
  * Open an isolated copy of an existing SQLite database.
62
106
  *
@@ -82,9 +126,9 @@ export function openSqliteReadSnapshot(dbPath) {
82
126
  if (pathExists(`${dbPath}-journal`))
83
127
  continue;
84
128
  const before = databaseFingerprint(dbPath);
85
- fs.copyFileSync(dbPath, snapshotPath);
129
+ copyFileOutsideThisProcess(dbPath, snapshotPath);
86
130
  if (before.wal)
87
- fs.copyFileSync(`${dbPath}-wal`, `${snapshotPath}-wal`);
131
+ copyFileOutsideThisProcess(`${dbPath}-wal`, `${snapshotPath}-wal`);
88
132
  else
89
133
  fs.rmSync(`${snapshotPath}-wal`, { force: true });
90
134
  const after = databaseFingerprint(dbPath);
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * `akm health`'s `state-db-integrity` check (src/commands/health/checks.ts)
9
9
  * is a pure projection like every other check, so the actual IO lives here:
10
- * a read-only `PRAGMA quick_check` and a read-only freelist/page-count read.
10
+ * a read-only `PRAGMA integrity_check` and a read-only freelist/page-count read.
11
11
  * Both open their own short-lived read-only connection via the plain
12
12
  * {@link openDatabase} opener rather than `openStateDatabase`
13
13
  * (src/core/state-db.ts), since a corrupt database must not need a clean
@@ -27,8 +27,8 @@
27
27
  import { appendEvent } from "../core/events.js";
28
28
  import { openDatabase } from "./database.js";
29
29
  import { applyReadonlyPragmas } from "./sqlite-pragmas.js";
30
- /** How many corruption errors `PRAGMA quick_check` collects before it stops scanning and returns. */
31
- const QUICK_CHECK_ERROR_LIMIT = 10;
30
+ /** How many corruption errors `PRAGMA integrity_check` collects before it stops scanning and returns. */
31
+ const INTEGRITY_CHECK_ERROR_LIMIT = 10;
32
32
  /**
33
33
  * Above this fraction of free pages, `state-db-integrity` warns, and
34
34
  * {@link vacuumIfReclaimable} compacts state.db (after improve's retention
@@ -48,16 +48,19 @@ function openReadonlyStateDb(dbPath) {
48
48
  return db;
49
49
  }
50
50
  /**
51
- * Run `PRAGMA quick_check(N)` against `dbPath` read-only. Sub-second on a
52
- * healthy multi-hundred-MB file; on a corrupt one, `N` bounds how many errors
53
- * SQLite collects before it stops scanning, which keeps the check's runtime
54
- * bounded even against a badly corrupt file.
51
+ * Run `PRAGMA integrity_check(N)` against `dbPath` read-only. Unlike
52
+ * `quick_check`, it also verifies every index against its table ("row N
53
+ * missing from index", "wrong # of entries in index"): the damage a WAL
54
+ * deleted under a live connection leaves behind, which `quick_check` reports
55
+ * as `ok`. Sub-second on a healthy state.db of a few hundred MB; on a corrupt
56
+ * one, `N` bounds how many errors SQLite collects before it stops scanning,
57
+ * which keeps the check's runtime bounded even against a badly corrupt file.
55
58
  */
56
- export function runStateDbQuickCheck(dbPath) {
59
+ export function runStateDbIntegrityCheck(dbPath) {
57
60
  let db;
58
61
  try {
59
62
  db = openReadonlyStateDb(dbPath);
60
- const rows = db.prepare(`PRAGMA quick_check(${QUICK_CHECK_ERROR_LIMIT})`).all();
63
+ const rows = db.prepare(`PRAGMA integrity_check(${INTEGRITY_CHECK_ERROR_LIMIT})`).all();
61
64
  const lines = rows.map((row) => String(firstColumn(row)));
62
65
  const ok = lines.length === 1 && lines[0] === "ok";
63
66
  return { ok, lines };
@@ -107,7 +107,11 @@ export async function loadPreparedTask(id, options) {
107
107
  return { file: await resolveAssetPath(bundleDir, type, name), bundleRoot: bundleDir };
108
108
  }
109
109
  const resolutionConfig = requiresCommandConfig ? config : loadConfig();
110
- const resolvedBundle = resolveWriteTarget(resolutionConfig, bundle, { requireWritable: false });
110
+ // `bundle` is the qualifier of an asset ref in the task, not a flag.
111
+ const resolvedBundle = resolveWriteTarget(resolutionConfig, bundle, {
112
+ requireWritable: false,
113
+ flag: "The asset ref's bundle",
114
+ });
111
115
  return {
112
116
  file: await resolveAssetPath(resolvedBundle.source.path, type, name),
113
117
  bundleRoot: resolvedBundle.source.path,
@@ -6,6 +6,7 @@ Upgrade guides and per-release migration notes.
6
6
  - [v0.9.2 release note](release-notes/0.9.2.md) -- Self-contained terminal upgrade summary shipped for `akm help migrate 0.9.2`
7
7
  - [v0.9.16 release note](release-notes/0.9.16.md) -- Source-bound scheduler grants, local execution authority, and split unsafe overrides
8
8
  - [v0.9.17 release note](release-notes/0.9.17.md) -- Consolidate's retire proposals and index layout 26, replacing the LLM entity graph with declared links, and `akm improve` scoped to what retrieval actually returns
9
+ - [v0.9.19 release note](release-notes/0.9.19.md) -- `akm improve` scoped to the bundle it writes to (scheduled runs need one `--bundle` run per other bundle), `akm proposal reopen` and the retire-proposal diff, and fewer repeat consolidate promotions
9
10
  - [v0.8 -> current v0.9 migration guide](v0.8-to-v0.9.md) -- Package upgrade with fresh current config/state and explicit task conversion
10
11
  - [v0.7 -> v0.8 migration guide](v0.7-to-v0.8.md) -- Task schema and 0.8-era changes
11
12
  - [v0.5 -> v0.6 migration guide](https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md) -- Terminology cut, registry schema v3, publisher changes
@@ -0,0 +1,134 @@
1
+ Migration notes for akm v0.9.19
2
+
3
+ Nothing has to run after upgrading from 0.9.18: 0.9.19 adds no config keys,
4
+ changes no index layout, adds no state.db migration and leaves scheduler rows
5
+ alone. (0.9.18 asked nothing either. From 0.9.16 or earlier, also read the
6
+ 0.9.17 note: akm help migrate 0.9.17.) What changes is which bundle akm
7
+ improve works on, how a rejected proposal is undone, what consolidation and
8
+ distill queue for review, and what akm proposal diff shows for a retirement.
9
+
10
+ akm improve now improves one bundle: the one it writes to, which is --bundle
11
+ when you pass it, else defaultWriteTarget, else your working bundle
12
+ (AKM_BUNDLE_DIR when set, otherwise defaultBundle). Through 0.9.18 a run also
13
+ picked assets out of your other writable bundles, read each one from the
14
+ bundle that owned it, and filed the proposal in its own write target, so an
15
+ asset that lived elsewhere came back either as a new copy in the write target
16
+ or as an edit of the write target's own copy made from the other bundle's
17
+ text. That no longer happens, and a rewrite filed outside the bundle that
18
+ owns its asset is now refused. If everything you improve lives in one bundle,
19
+ nothing changes for you. If you improve more than one, this is the one action
20
+ in the release: a scheduled akm improve, the shipped akm-improve tasks
21
+ included, now covers only its write target, so add one akm improve --bundle
22
+ run for each other bundle you want improved:
23
+
24
+ akm task add improve-team --schedule "15 3 * * *" \
25
+ --command "akm improve --bundle team --skip-if-locked --require-engines"
26
+
27
+ (The --bundle inside the command string is akm improve's own; the --bundle of
28
+ akm task add itself picks which bundle stores the task.) akm improve skills/x
29
+ for an asset that lives only in another bundle now fails with a not-found
30
+ error whose hint names that bundle, and akm improve team//skills/x works.
31
+ Proposals an older run already queued stay in the queue, so review them with
32
+ akm proposal list: one that creates an asset you already have in another
33
+ bundle, or overwrites this bundle's copy with the other's text, is one of
34
+ those. akm improve --dry-run and --plan now preview the bundle a live run
35
+ improves; with no --bundle or defaultWriteTarget, a dry run used to read
36
+ defaultBundle while a live run started from AKM_BUNDLE_DIR.
37
+
38
+ A rejection is no longer final. akm proposal reopen <id> [<id> ...] puts
39
+ rejected proposals back to pending and keeps each rejection in the proposal's
40
+ reviewHistory, which akm proposal show prints; a proposal_reopened event goes
41
+ into akm log. Reopen refuses a proposal that is not rejected or whose target
42
+ changed since it was created, by the same rule accept applies: an update needs
43
+ its target unchanged, a create needs the target still absent, and a retire
44
+ proposal needs its successor present and both documents unchanged. It also
45
+ refuses a retire proposal while another pending retire proposal involves
46
+ either of its documents. Given several ids it reopens all of them or none and
47
+ lists every refusal. It takes full ids, since a prefix only matches pending
48
+ proposals (an asset ref works too, for the newest proposal on it while none
49
+ is pending, but never for a retire proposal). The age that retention expiry
50
+ and --older-than see starts over at the reopen, so a scheduled sweep does not
51
+ take a proposal you just put back.
52
+
53
+ The case that matters most is consolidate's retire proposals. Through 0.9.18,
54
+ akm proposal diff drew a retirement as the file replaced by one blank line,
55
+ so a correct retirement could pass for data loss. One reviewer rejected all
56
+ 65 of a bundle's retire proposals that way, and each rejection also kept the
57
+ pair pass from proposing that retirement again while both documents stayed
58
+ unchanged. To take them back, check each rejection's reason (akm proposal
59
+ list --status rejected --generator consolidate-pair --detail normal --format
60
+ json shows it as review.reason) and reopen only those rejected over the diff,
61
+ one id at a time so a refusal skips only that one (the pattern matches the
62
+ reason given here; change it to yours):
63
+
64
+ akm proposal list --status rejected --generator consolidate-pair \
65
+ --detail normal --format json \
66
+ | jq -r '.proposals[] | select(.review.reason // "" | test("blank line"))
67
+ | .id' \
68
+ | xargs -r -n 1 akm proposal reopen --reason "diff was misrendered"
69
+
70
+ A proposal refused because another pending retire proposal involves the same
71
+ document can be reopened once that one is decided. A pair whose documents
72
+ changed after the rejection needs no recovery, because the old rejection only
73
+ held while both were unchanged and the pair pass may judge it afresh. Reopened
74
+ retire proposals wait for you like any other. drain never accepts a retire
75
+ proposal, so read each with akm proposal diff and accept or reject it.
76
+
77
+ akm proposal diff now draws a retirement as one. The text output has a retire
78
+ header, the pair's verdict (label, reason, and continuity risk when the pair
79
+ was flagged), a note that accept archives the file and revert restores it byte
80
+ for byte, and then only the removed lines under a
81
+ +++ /dev/null (retired: archived; successor <ref>) line. In JSON, a retire
82
+ proposal's diff result gains op ("delete"), retirement (retiredRef,
83
+ successorRef, judgeLabel, judgeReason, cosine, and continuityRisk when flagged)
84
+ and note; the diff result of every other proposal is unchanged. A script that
85
+ matched the old "(update: ...)" header on a retire proposal should expect
86
+ "(retire: ...)". akm proposal show --detail full no longer ends a retire
87
+ proposal with an empty payload heading, and a reopened proposal's JSON gains
88
+ reviewHistory.
89
+
90
+ Consolidation queues fewer repeat promotions. Before it queues a memory as a
91
+ knowledge proposal, it compares the memory with the 20 knowledge docs in its
92
+ bundle nearest to it by stored vector, and skips it, with skip reason
93
+ dedup_covered_by_knowledge, when one of them already holds at least half of
94
+ the memory's distinct 5-word runs. That needs stored vectors: with semantic
95
+ search off the check does nothing and only the exact slug and whole-body
96
+ checks apply. A memory whose promotion was accepted or rejected is now offered
97
+ again only when its body changes (frontmatter edits do not count); it used to
98
+ come back at once after an accept and after 7 days after a rejection. A
99
+ decision an older release recorded has no body hash to compare and keeps
100
+ those old windows. Expect a shorter promotion queue, with the skipped
101
+ memories showing up as skip reasons and warnings in the run's result; to have
102
+ a memory considered again, edit its text.
103
+
104
+ Three changes keep a tool failure recorded as feedback from becoming a lesson
105
+ about the error or a TODO placeholder in a memory. Reflect's feedback caveat no
106
+ longer offers a TODO: verify placeholder: when feedback asks for something the
107
+ asset lacks, reflect is told to leave the section unchanged. TODO lines earlier
108
+ runs already put in your assets stay until you remove them; grep -rniE
109
+ "TODO:? *verify" over the bundle finds them. Distill's quality judge now also
110
+ scores whether a lesson is about what its source is about, and a lesson judged
111
+ off-subject is dropped as quality_rejected (a ledger row and a distill_invoked
112
+ event, no proposal) instead of passing or waiting in the queue as
113
+ review_needed; the judge also reads the same first 3000 characters of the
114
+ source body, without frontmatter, that the generator saw. The shipped agent
115
+ guidance changed with it: akm help agents now says to record feedback about an
116
+ asset's content, that it helped or turned out wrong, stale or unhelpful, and
117
+ not a failed akm command such as akm show erroring, and akm feedback --help
118
+ says the same of --reason. If you pasted that guide into an AGENTS.md or a
119
+ system prompt, regenerate the block: the old text told agents to record
120
+ --negative "when it fails".
121
+
122
+ Downgrading to 0.9.18 needs no data change, since no schema moved, but it
123
+ brings some of this back. 0.9.18 does not know reviewHistory: a proposal
124
+ reopened under 0.9.19 reads as an ordinary pending proposal, and 0.9.18 drops
125
+ its history if it rewrites the row (accepting or rejecting it, say). It also
126
+ counts a pending proposal's age from its creation, so a reopened proposal can
127
+ be expired by the next akm improve run (retire proposals never expire) or
128
+ swept by a scheduled accept, reject or drain with --older-than. It does not
129
+ hold a promoted memory either: 0.9.19 records a decided promotion with the
130
+ memory's body hash and no retry time, which 0.9.18 reads as eligible now, so
131
+ those memories go back to consolidation on the next run, a rejected one
132
+ sooner than the 7 days 0.9.18 would have waited. Its improve plans assets
133
+ from every writable bundle again, and its akm proposal diff draws a
134
+ retirement as a blank replacement again.
@@ -7,6 +7,11 @@ live one level up in `docs/migration/`.
7
7
 
8
8
  ## Available notes
9
9
 
10
+ - [0.9.19](0.9.19.md) — `akm improve` planning only the bundle it writes to
11
+ (a scheduled run needs one `--bundle` run per other bundle),
12
+ `akm proposal reopen` and the retire-proposal diff, consolidate's coverage
13
+ check and content-driven hold on decided promotions, distill's grounding
14
+ check, and what a downgrade to 0.9.18 undoes
10
15
  - [0.9.17](0.9.17.md) — consolidate's retire proposals and continuity check,
11
16
  promotions archiving their source memory, declared links replacing the LLM
12
17
  entity graph, index layout 26, scheduler rows carrying their own context,
@@ -988,7 +988,7 @@ akm-migrate storage --dry-run
988
988
  and look for import warnings. You can also inspect `state.db` directly:
989
989
 
990
990
  ```sh
991
- sqlite3 ~/.local/share/akm/state.db 'SELECT COUNT(*) FROM events;'
991
+ sqlite3 -readonly ~/.local/share/akm/state.db 'SELECT COUNT(*) FROM events;'
992
992
  ```
993
993
 
994
994
  **`akm list` shows empty stashes.**
@@ -1039,7 +1039,7 @@ If you need to roll back to 0.7.x:
1039
1039
  wrote new events on 0.8.0 and need them in 0.7.x, export them:
1040
1040
 
1041
1041
  ```sh
1042
- sqlite3 ~/.local/share/akm/state.db \
1042
+ sqlite3 -readonly ~/.local/share/akm/state.db \
1043
1043
  "SELECT json_object('schemaVersion', schema_version, 'ts', ts, 'eventType', event_type, 'ref', ref, 'metadata', json(metadata)) FROM events ORDER BY id;" \
1044
1044
  >> ~/.cache/akm/events.jsonl
1045
1045
  ```
@@ -140,7 +140,11 @@ paths (same-filesystem rename, or copy-then-delete across filesystems). A
140
140
  lock file is only ever deleted once the same staleness check `akm improve`
141
141
  itself uses says its holder is dead; a lock a live run still holds (or one
142
142
  this process cannot read) is left in place and reported instead. The whole
143
- step is idempotent — a second run reports nothing pending.
143
+ step is idempotent — a second run reports nothing pending. 0.9.17-alpha.4
144
+ removed that step: `akm migrate` no longer relocates these files, and one left
145
+ at an old path is inert (nothing reads it). Two of the five writers no longer
146
+ exist either — the improve ledger replaced `distill-rejected/`, and the
147
+ write-only `eval-cases/` path was removed.
144
148
  `$STASH/.akm/memory-cleanup/` did not move; it is the one confirmed exception
145
149
  to the rule (see Storage locations, above).
146
150