akm-cli 0.9.18 → 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 (52) hide show
  1. package/CHANGELOG.md +135 -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/improve/consolidate/coverage.js +132 -0
  8. package/dist/commands/improve/consolidate/pair-pass.js +30 -20
  9. package/dist/commands/improve/consolidate.js +43 -10
  10. package/dist/commands/improve/distill.js +7 -3
  11. package/dist/commands/improve/eligibility.js +39 -9
  12. package/dist/commands/improve/improve-cli.js +9 -6
  13. package/dist/commands/improve/improve.js +13 -4
  14. package/dist/commands/improve/ledger.js +2 -2
  15. package/dist/commands/improve/loop-stages.js +2 -0
  16. package/dist/commands/improve/preparation.js +1 -1
  17. package/dist/commands/improve/reflect.js +1 -1
  18. package/dist/commands/improve/stage.js +38 -9
  19. package/dist/commands/proposal/diff-format.js +21 -0
  20. package/dist/commands/proposal/proposal-cli.js +48 -10
  21. package/dist/commands/proposal/proposal-types.js +11 -0
  22. package/dist/commands/proposal/proposal.js +60 -5
  23. package/dist/commands/proposal/repository.js +245 -17
  24. package/dist/commands/read/knowledge.js +13 -11
  25. package/dist/commands/read/remember-cli.js +7 -3
  26. package/dist/commands/sources/source-clone.js +1 -1
  27. package/dist/commands/tasks/tasks-cli.js +1 -1
  28. package/dist/commands/tasks/tasks.js +10 -3
  29. package/dist/core/mutation-target.js +8 -3
  30. package/dist/core/write-source.js +3 -2
  31. package/dist/indexer/usage/usage-events.js +2 -1
  32. package/dist/output/shapes/helpers.js +7 -0
  33. package/dist/output/shapes/passthrough.js +1 -0
  34. package/dist/output/shapes/proposal/reopen.js +14 -0
  35. package/dist/output/shapes.js +2 -0
  36. package/dist/output/text/helpers.js +1 -1
  37. package/dist/output/text/proposal/proposal.js +3 -1
  38. package/dist/output/text/proposal-format.js +87 -32
  39. package/dist/scripts/akm-migrate-node.js +79 -23
  40. package/dist/scripts/akm-migrate.js +79 -23
  41. package/dist/storage/repositories/improve-ledger-repository.js +65 -6
  42. package/dist/storage/repositories/index-vec-repository.js +13 -8
  43. package/dist/storage/repositories/proposals-repository.js +23 -0
  44. package/dist/tasks/run/load-task.js +5 -1
  45. package/docs/migration/README.md +1 -0
  46. package/docs/migration/release-notes/0.9.19.md +134 -0
  47. package/docs/migration/release-notes/README.md +5 -0
  48. package/docs/migration/v0.8-to-v0.9.md +5 -1
  49. package/docs/reference/cli.md +189 -28
  50. package/docs/reference/configuration.md +9 -8
  51. package/docs/reference/data-and-telemetry.md +19 -14
  52. package/package.json +1 -1
@@ -38,6 +38,33 @@ export const LEDGER_REVISIT_CADENCE_DAYS = 7;
38
38
  * eligible again.
39
39
  */
40
40
  export const PAIR_PASS_LEDGER_SOURCE = "consolidate-pair";
41
+ /**
42
+ * The consolidate promote pass's ledger source: one row per source memory,
43
+ * keyed by the memory (`memories/<name>`), not by the knowledge ref the
44
+ * promotion would create.
45
+ */
46
+ export const CONSOLIDATE_LEDGER_SOURCE = "consolidate";
47
+ /**
48
+ * Whether a decision on `(source, outcome)` leaves the ref's next attempt to
49
+ * its content instead of a clock (#998). An accepted or rejected consolidate
50
+ * promotion is a verdict on that memory's text; asking the model about the
51
+ * same text again can only reproduce the proposal (accepted used to be
52
+ * eligible at once, rejected after 7 days), so the memory waits for an edit.
53
+ * The clock stays for a row with no recorded hash — one decided before the
54
+ * hash was recorded — see {@link nextEligibleAt} and {@link isContentDrivenRow}.
55
+ */
56
+ export function isContentDrivenDecision(source, outcome) {
57
+ return source === CONSOLIDATE_LEDGER_SOURCE && (outcome === "accepted" || outcome === "rejected");
58
+ }
59
+ /**
60
+ * Whether this row is held by its content hash: a decided consolidate
61
+ * promotion that recorded the body it was decided against. Such a row has no
62
+ * `next_eligible_at`; the caller compares `contentHash` with the asset's
63
+ * current body hash (the pair pass does the same in `selectInitiators`).
64
+ */
65
+ export function isContentDrivenRow(row) {
66
+ return row.contentHash !== null && isContentDrivenDecision(row.source, row.outcome);
67
+ }
41
68
  /**
42
69
  * Outcomes whose window a fresh signal on the asset (new feedback, a content
43
70
  * change) cannot lift. Every other window is a revisit cadence that a signal
@@ -73,12 +100,17 @@ function windowDays(source, outcome) {
73
100
  }
74
101
  /**
75
102
  * The single cadence function: when a `(source, outcome)` recorded at
76
- * `fromIso` becomes eligible again, or `null` for "immediately".
103
+ * `fromIso` becomes eligible again, or `null` for "immediately". A decision
104
+ * that {@link isContentDrivenDecision} holds by content, recorded together
105
+ * with the body hash it was decided against, starts no clock at all: `null`
106
+ * here means the hash is the whole test, not that the ref is free to retry.
77
107
  */
78
- export function nextEligibleAt(source, outcome, fromIso) {
108
+ export function nextEligibleAt(source, outcome, fromIso, contentHash) {
79
109
  const from = Date.parse(fromIso);
80
110
  if (!Number.isFinite(from))
81
111
  return null;
112
+ if (contentHash && isContentDrivenDecision(source, outcome))
113
+ return null;
82
114
  const days = windowDays(source, outcome);
83
115
  return days === null ? null : new Date(from + days * MS_PER_DAY).toISOString();
84
116
  }
@@ -127,7 +159,7 @@ export function recordImproveLedger(db, input) {
127
159
  source: input.source,
128
160
  lastAttemptAt: input.at,
129
161
  outcome: input.outcome,
130
- nextEligibleAt: nextEligibleAt(input.source, input.outcome, input.at),
162
+ nextEligibleAt: nextEligibleAt(input.source, input.outcome, input.at, input.contentHash),
131
163
  proposalId: input.proposalId ?? null,
132
164
  detail: trimDetail(input.detail),
133
165
  contentHash: input.contentHash ?? null,
@@ -150,14 +182,16 @@ export function recordImproveLedger(db, input) {
150
182
  * differ from the ledger key (a distill proposal for `lessons/x` is keyed by
151
183
  * its input `memories/x`) — and `last_attempt_at` is kept: the window starts
152
184
  * at the decision, the attempt happened when it happened. A proposal no row
153
- * knows (minted before the ledger existed) gets a row keyed by its own ref.
185
+ * knows (minted before the ledger existed, or whose row a later `judged_no_action`
186
+ * overwrote) gets a row keyed by `input.ref`.
154
187
  */
155
188
  export function recordImproveLedgerDecision(db, input) {
189
+ const hash = isContentDrivenDecision(input.source, input.outcome) ? input.contentHash : undefined;
156
190
  const changes = db
157
191
  .prepare(`UPDATE improve_ledger
158
- SET outcome = ?, next_eligible_at = ?, detail = ?
192
+ SET outcome = ?, next_eligible_at = ?, detail = ?, content_hash = COALESCE(?, content_hash)
159
193
  WHERE stash_dir = ? AND proposal_id = ?`)
160
- .run(input.outcome, nextEligibleAt(input.source, input.outcome, input.at), trimDetail(input.detail), input.stashDir, input.proposalId).changes;
194
+ .run(input.outcome, nextEligibleAt(input.source, input.outcome, input.at, hash), trimDetail(input.detail), hash ?? null, input.stashDir, input.proposalId).changes;
161
195
  if (Number(changes) > 0)
162
196
  return;
163
197
  recordImproveLedger(db, {
@@ -168,8 +202,33 @@ export function recordImproveLedgerDecision(db, input) {
168
202
  at: input.at,
169
203
  proposalId: input.proposalId,
170
204
  ...(input.detail !== undefined ? { detail: input.detail } : {}),
205
+ ...(hash !== undefined ? { contentHash: hash } : {}),
171
206
  });
172
207
  }
208
+ /**
209
+ * A rejected proposal was reopened (`akm proposal reopen`): put the rows
210
+ * {@link recordImproveLedgerDecision} found by `proposal_id` back to what the
211
+ * mint wrote — `proposed`, on the revisit cadence from the reopen, and no
212
+ * `content_hash` (a mint records none) — so the rejection's hard window stops
213
+ * reporting (and blocking) a proposal that is pending again.
214
+ * `last_attempt_at` is kept, as a decision keeps it. A proposal with no such
215
+ * row (its mint wrote none, or a later attempt took the key over) changes
216
+ * nothing.
217
+ */
218
+ export function reopenImproveLedgerDecision(db, input) {
219
+ db.prepare(`UPDATE improve_ledger
220
+ SET outcome = 'proposed', next_eligible_at = ?, detail = ?, content_hash = NULL
221
+ WHERE stash_dir = ? AND proposal_id = ?`).run(nextEligibleAt(input.source, "proposed", input.at), trimDetail(input.detail), input.stashDir, input.proposalId);
222
+ }
223
+ /**
224
+ * The same reopen for a proposal whose mint deliberately wrote no ledger row (a
225
+ * retire proposal — see `createRetireProposal`): the row its rejection created
226
+ * is dropped, returning the ledger to what the mint left. Found by
227
+ * `proposal_id`, like {@link recordImproveLedgerDecision}.
228
+ */
229
+ export function forgetImproveLedgerDecision(db, stashDir, proposalId) {
230
+ db.prepare("DELETE FROM improve_ledger WHERE stash_dir = ? AND proposal_id = ?").run(stashDir, proposalId);
231
+ }
173
232
  /**
174
233
  * Should-fix 6 (second review round): a read-only or dry-run open never
175
234
  * migrates, so it can land on a state.db from before migration 029 added
@@ -80,9 +80,11 @@ export function upsertEmbedding(db, entryId, embedding, model) {
80
80
  * (ties by id): an exact scan of the current model's rows. `distance` is
81
81
  * `sqrt(2 * (1 - cosine))`, the L2 distance between the unit-normalised
82
82
  * vectors. Rows of another width, left by a model that is no longer current,
83
- * never match.
83
+ * never match. With a `scope`, only that bundle's entries of that type are
84
+ * scanned: the `k` nearest of those, not the `k` nearest of everything filtered
85
+ * afterwards.
84
86
  */
85
- export function searchVec(db, query, k) {
87
+ export function searchVec(db, query, k, scope) {
86
88
  const dim = query.length;
87
89
  const q = Float64Array.from(query);
88
90
  let queryNorm = 0;
@@ -92,11 +94,13 @@ export function searchVec(db, query, k) {
92
94
  return [];
93
95
  queryNorm = Math.sqrt(queryNorm);
94
96
  const current = modelPredicate(db, currentEmbeddingModel(db));
97
+ const scoped = scope ? " AND embeddings.id IN (SELECT id FROM entries WHERE type = ? AND bundle_id = ?)" : "";
98
+ const params = scope ? [...current.params, scope.type, scope.bundleId] : current.params;
95
99
  const ids = [];
96
100
  const similarities = [];
97
101
  const rows = db
98
- .prepare(`SELECT id, embedding FROM embeddings WHERE ${current.sql}`)
99
- .iterate(...current.params);
102
+ .prepare(`SELECT id, embedding FROM embeddings WHERE ${current.sql}${scoped}`)
103
+ .iterate(...params);
100
104
  for (const { id, embedding } of rows) {
101
105
  if (embedding.byteLength !== dim * 4)
102
106
  continue;
@@ -126,14 +130,15 @@ export function searchVec(db, query, k) {
126
130
  /**
127
131
  * The k nearest neighbours of an already-indexed entry, by its stored vector —
128
132
  * no re-embedding, no network. Returns [] when the entry has no stored
129
- * vector. The entry itself is typically returned with distance ~0; callers
130
- * filter it out by id.
133
+ * vector. Unscoped, the entry itself is typically returned with distance ~0;
134
+ * callers filter it out by id. A `scope` confines the neighbours to one
135
+ * bundle's entries of one type (see {@link searchVec}).
131
136
  */
132
- export function getNeighborsByEntryId(db, id, k) {
137
+ export function getNeighborsByEntryId(db, id, k, scope) {
133
138
  const row = db.prepare("SELECT embedding FROM embeddings WHERE id = ?").get(id);
134
139
  if (!row || row.embedding.byteLength % 4 !== 0)
135
140
  return [];
136
- return searchVec(db, new Float32Array(row.embedding.slice().buffer), k);
141
+ return searchVec(db, new Float32Array(row.embedding.slice().buffer), k, scope);
137
142
  }
138
143
  /**
139
144
  * Every entry that has no embedding row for `model` (any row when no model is
@@ -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)
@@ -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,
@@ -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