akm-cli 0.10.261007-beta → 0.10.261009-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/STABILITY.md +10 -4
- package/dist/api.d.ts +6 -0
- package/dist/api.js +12 -1
- package/dist/assets/prompts/consolidate-pair-check.md +2 -0
- package/dist/assets/prompts/consolidate-pair-same-age.md +1 -0
- package/dist/assets/prompts/consolidate-pair-terms.md +6 -0
- package/dist/assets/prompts/consolidate-pair.md +4 -12
- package/dist/commands/health.js +1 -2
- package/dist/commands/improve/consolidate/pair-pass.js +59 -31
- package/dist/commands/improve/extract.js +18 -12
- package/dist/commands/improve/memory/memory-belief.js +2 -2
- package/dist/commands/proposal/repository.js +44 -13
- package/dist/commands/read/curate.js +77 -18
- package/dist/commands/read/search-cli.js +25 -11
- package/dist/commands/read/search.js +3 -3
- package/dist/commands/read/show.js +47 -1
- package/dist/core/adapter/adapters/akm-adapter.js +1 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +3 -5
- package/dist/core/adapter/adapters/okf-adapter.js +3 -16
- package/dist/core/asset/markdown-link-targets.js +43 -0
- package/dist/core/common.js +17 -2
- package/dist/core/config/config-io.js +14 -4
- package/dist/indexer/passes/metadata.js +17 -0
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/search/db-search.js +16 -7
- package/dist/indexer/search/search-hit-enrichers.js +3 -3
- package/dist/output/shapes/curate.js +3 -1
- package/dist/output/shapes/helpers.js +6 -0
- package/dist/output/text/show-format.js +7 -0
- package/dist/scripts/akm-migrate-node.js +1824 -324
- package/dist/scripts/akm-migrate.js +1824 -324
- package/dist/storage/repositories/index-entries-repository.js +16 -0
- package/dist/storage/repositories/index-fts-repository.js +28 -6
- package/dist/storage/repositories/index-vec-repository.js +19 -3
- package/docs/architecture/0.10-process-rules.md +4 -4
- package/docs/architecture/architecture.md +4 -0
- package/docs/architecture/persisted-data-compat.md +1 -1
- package/docs/reference/api.md +19 -1
- package/docs/reference/cli.md +56 -4
- package/docs/reference/data-and-telemetry.md +1 -1
- package/docs/reference/memory.md +7 -6
- package/package.json +2 -1
|
@@ -573,6 +573,22 @@ export function getEntryCountByType(db) {
|
|
|
573
573
|
export function getEmbeddableEntryCount(db) {
|
|
574
574
|
return getEntryCount(db);
|
|
575
575
|
}
|
|
576
|
+
/** Read one bounded candidate batch using the same mapper as browse. */
|
|
577
|
+
export function getEntriesByIds(db, ids) {
|
|
578
|
+
const entries = [];
|
|
579
|
+
for (let offset = 0; offset < ids.length; offset += SQLITE_CHUNK_SIZE) {
|
|
580
|
+
const chunk = ids.slice(offset, offset + SQLITE_CHUNK_SIZE);
|
|
581
|
+
const rows = db
|
|
582
|
+
.prepare(`SELECT ${ENTRY_COLUMNS} FROM entries WHERE id IN (${chunk.map(() => "?").join(",")})`)
|
|
583
|
+
.all(...chunk);
|
|
584
|
+
for (const row of rows) {
|
|
585
|
+
const entry = rowToIndexedEntry(row, "getEntriesByIds");
|
|
586
|
+
if (entry)
|
|
587
|
+
entries.push(entry);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return entries;
|
|
591
|
+
}
|
|
576
592
|
export function getEntryById(db, id) {
|
|
577
593
|
const row = db
|
|
578
594
|
.prepare("SELECT file_path, document_json, item_ref, bundle_id, component_id, concept_id, adapter_id FROM entries WHERE id = ?")
|
|
@@ -104,7 +104,7 @@ const BM25_COLUMN_WEIGHTS = "0, 1.0, 1.0, 1.0, 1.0, 1.0";
|
|
|
104
104
|
* `entryType` narrows to one type; otherwise `excludeTypes` (#627, the
|
|
105
105
|
* default-hidden types such as `session`) are left out.
|
|
106
106
|
*/
|
|
107
|
-
export function searchFts(db, query, limit, entryType, excludeTypes = []) {
|
|
107
|
+
export function searchFts(db, query, limit, entryType, excludeTypes = [], filterCandidates) {
|
|
108
108
|
const tokens = ftsQueryTokens(query);
|
|
109
109
|
if (limit <= 0 || tokens.length === 0)
|
|
110
110
|
return [];
|
|
@@ -114,17 +114,39 @@ export function searchFts(db, query, limit, entryType, excludeTypes = []) {
|
|
|
114
114
|
: excludeTypes.length > 0
|
|
115
115
|
? `AND e.type NOT IN (${excludeTypes.map(() => "?").join(", ")})`
|
|
116
116
|
: "";
|
|
117
|
-
const params = [ftsOrMatch(tokens), ...(typed ? [entryType] : excludeTypes)
|
|
117
|
+
const params = [ftsOrMatch(tokens), ...(typed ? [entryType] : excludeTypes)];
|
|
118
118
|
// Contentless FTS rows address their entry by rowid; the COALESCE also reads
|
|
119
119
|
// the content-bearing layout older releases wrote.
|
|
120
|
-
|
|
121
|
-
.prepare(`SELECT e.id AS id, e.item_ref AS itemRef
|
|
120
|
+
const statement = db.prepare(`SELECT e.id AS id, e.item_ref AS itemRef
|
|
122
121
|
FROM entries_fts f
|
|
123
122
|
JOIN entries e ON e.id = COALESCE(f.entry_id, f.rowid)
|
|
124
123
|
WHERE entries_fts MATCH ? ${filter}
|
|
125
124
|
ORDER BY bm25(entries_fts, ${BM25_COLUMN_WEIGHTS}), e.item_ref
|
|
126
|
-
LIMIT
|
|
127
|
-
|
|
125
|
+
${filterCandidates ? "" : "LIMIT ?"}`);
|
|
126
|
+
if (!filterCandidates)
|
|
127
|
+
return statement.all(...params, limit);
|
|
128
|
+
// Stream matching rows in BM25 order, stopping at the eligible budget.
|
|
129
|
+
// No full-corpus materialization and no repeated OFFSET/restarted query.
|
|
130
|
+
const selected = [];
|
|
131
|
+
let batch = [];
|
|
132
|
+
const keepBatch = () => {
|
|
133
|
+
const eligible = filterCandidates(batch.map((row) => row.id));
|
|
134
|
+
for (const row of batch)
|
|
135
|
+
if (eligible.has(row.id))
|
|
136
|
+
selected.push(row);
|
|
137
|
+
batch = [];
|
|
138
|
+
};
|
|
139
|
+
for (const row of statement.iterate(...params)) {
|
|
140
|
+
batch.push(row);
|
|
141
|
+
if (batch.length < 100)
|
|
142
|
+
continue;
|
|
143
|
+
keepBatch();
|
|
144
|
+
if (selected.length >= limit)
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (selected.length < limit && batch.length > 0)
|
|
148
|
+
keepBatch();
|
|
149
|
+
return selected.slice(0, limit);
|
|
128
150
|
}
|
|
129
151
|
/**
|
|
130
152
|
* Resolve an opaque fragment selector from the indexed safe projection, not
|
|
@@ -84,7 +84,7 @@ export function upsertEmbedding(db, entryId, embedding, model) {
|
|
|
84
84
|
* scanned: the `k` nearest of those, not the `k` nearest of everything filtered
|
|
85
85
|
* afterwards.
|
|
86
86
|
*/
|
|
87
|
-
export function searchVec(db, query, k, scope) {
|
|
87
|
+
export function searchVec(db, query, k, scope, filterCandidates) {
|
|
88
88
|
const dim = query.length;
|
|
89
89
|
const q = Float64Array.from(query);
|
|
90
90
|
let queryNorm = 0;
|
|
@@ -122,10 +122,26 @@ export function searchVec(db, query, k, scope) {
|
|
|
122
122
|
}
|
|
123
123
|
const order = ids.map((_, index) => index);
|
|
124
124
|
order.sort((a, b) => similarities[b] - similarities[a] || ids[a] - ids[b]);
|
|
125
|
-
|
|
125
|
+
const result = (index) => ({
|
|
126
126
|
id: ids[index],
|
|
127
127
|
distance: Math.sqrt(2 * Math.max(0, 1 - similarities[index])),
|
|
128
|
-
})
|
|
128
|
+
});
|
|
129
|
+
if (!filterCandidates)
|
|
130
|
+
return order.slice(0, k).map(result);
|
|
131
|
+
// Filter in similarity order, a batch at a time, so only the rows near the
|
|
132
|
+
// top are loaded and checked — not every embedded entry.
|
|
133
|
+
const results = [];
|
|
134
|
+
for (let start = 0; start < order.length && results.length < k; start += 100) {
|
|
135
|
+
const batch = order.slice(start, start + 100);
|
|
136
|
+
const eligible = filterCandidates(batch.map((index) => ids[index]));
|
|
137
|
+
for (const index of batch) {
|
|
138
|
+
if (results.length === k)
|
|
139
|
+
break;
|
|
140
|
+
if (eligible.has(ids[index]))
|
|
141
|
+
results.push(result(index));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return results;
|
|
129
145
|
}
|
|
130
146
|
/**
|
|
131
147
|
* The k nearest neighbours of an already-indexed entry, by its stored vector —
|
|
@@ -617,10 +617,10 @@ Paths are relative to the repository root of this branch. `consolidate.ts` is `s
|
|
|
617
617
|
- **Pair candidates** — 20 nearest neighbours are fetched per initiator and the first 5 that pass the filters are kept. Filters: same bundle; type memory, flat knowledge or lesson; file exists; not itself; not its own `.derived` twin or parent; cosine at or above the floor; pair deduplicated by unordered key. A `.derived` memory is not excluded as a candidate, and the guards below inspect the retired side only, so it can be the kept side. *Why:* S2: a few self/twin/bundle/tier misses in the raw top 5 starved initiators to zero candidates. (`pair-pass.ts:80-83,176-196,455-508`)
|
|
618
618
|
- **Pair budget and admission** — At most 300 pairs are judged per run (`MAX_PAIRS_PER_RUN`). Whole initiators are admitted, never single pairs, first-fit. New-or-changed initiators go before any backlog initiator, then by best cosine. A group with any candidate touching an asset in a pending retire proposal (as retired or successor) is skipped before it spends budget. The retire guards run after the judge call with no pre-filter, so budget is also spent on pairs whose only retire outcome would be refused (a `.derived` twin whose parent exists, whether in a pair of twins or flipped against knowledge). *Why:* the old flat top-300 cap left initiators partially judged forever; simulation: 30 nights made 9,000 calls but completed only 455 pairs, and whole-initiator admission drains the backlog in about 12 nights. (`pair-pass.ts:94-95,944-997`)
|
|
619
619
|
- **Pair ledger row** — An initiator gets a ledger row (`proposed` or `judged_no_action`, with body hash) only if all its candidates were admitted and none failed. Otherwise the next run reconsiders it. An initiator with no stored vector never gets a row, because the neighbour query could not run for it (`hasStoredEmbedding`); it is counted in the pair-pass warning, so turning embeddings on later reconsiders that backlog (fixed in this PR, #2). An initiator that was queried and has zero candidates still qualifies (`if (!hasCandidates) return true`) and is stamped `judged_no_action` with its body hash, a content-driven hold with no clock. *Why:* a row means the asset was judged against its neighbours; without a vector nothing was. (`pair-pass.ts:1057-1075`, `index-vec-repository.ts:180-183`)
|
|
620
|
-
- **
|
|
621
|
-
- **Replacement rule (same named subject)** — B replaces A's claim only when both are about the same named thing (service, integration, partner, component, region or file) AND B gives a current new value, meaning B says so (now, instead of, since, renamed, corrected, no longer) or B is created later. Notes about different named things never replace each other, whatever their dates. A different value for the same thing with neither sign is a conflict: both values are listed
|
|
622
|
-
- **Tied dates** — Dates come from git first-add. When they are equal, the prompt labels the sides "A (same age as B)" / "B (same age as A)" instead of older/newer. *Why:* so "B (newer)" is not read as proof that B replaces A. (`pair-pass.ts:566-583`; #1134, #1146)
|
|
623
|
-
- **Retire outcome table** —
|
|
620
|
+
- **Complete claim assessment** — Both the first judge and the second look receive complete stripped bodies with their existing metadata (#1195): ref, type, description and created/updated dates for the first judge; ref and description for the second look. There is no prompt-body cap. Lexical attention hints list tokens unique to each side (only X for the second look); they help locate differences but do not establish claim equivalence or replace the complete notes. The first judge returns reason, `onlyInA`, `onlyInB`, and confidence. AKM derives `duplicate` when both lists are empty, `subsumed` when exactly one is empty, and `overlap` when neither is empty, rather than asking the model to repeat the decision in relation/redundant/stale fields. A missing or malformed claim list is never treated as empty. A context-window or transport failure is a failed assessment; the client does not shorten evidence to fit. A failed first judgment mints nothing; a failed or malformed second look leaves the proposal pending and unstaged. Review and reject/reassess potentially truncated queued retirements before unattended drain: upgrading does not revalidate them. No storage migration is required.
|
|
621
|
+
- **Replacement rule (same named subject)** — B replaces A's claim only when both are about the same named thing (service, integration, partner, component, region or file) AND B gives a current new value, meaning B says so (now, instead of, since, renamed, corrected, no longer) or B is created later. Notes about different named things never replace each other, whatever their dates. A different value for the same thing with neither sign is a conflict: both values are listed, producing `overlap` and no retirement. *Why:* #1146 — gpt-5.6-terra retired 10 of 60 notes holding a claim the survivor lacked, all from `contradicts` pairs it labelled `supersedes`, because akm called B "newer" when the dates tied. #1149 — a bare later date then made gpt-oss:120b retire notes about different named things (Alder vs Birch partners). Measured unsafe retirements went 1, 2, 1 → 0, 0, 0 on gpt-oss per case, but the pool run on gpt-oss went 0, 1, 0 → 0, 1, 1 and terra was not rerun (PR #1149). (`consolidate-pair.md`; #1134, #1146, #1149)
|
|
622
|
+
- **Tied dates** — Dates come from git first-add. When they are equal, the prompt labels the sides "A (same age as B)" / "B (same age as A)" instead of older/newer. Both checks repeat the same-date rule after the bodies and hints so prompt position does not imply currency. *Why:* so "B (newer)" is not read as proof that B replaces A. (`pair-pass.ts:566-583`; #1134, #1146)
|
|
623
|
+
- **Retire outcome table** — A duplicate retires the older note and keeps the newer; a subsumed pair retires the side with an empty distinct-claim list. Both nonempty lists produce no retirement. An updated value that preserves the older claim is handled by the same claim-list rule and receives a `subsumed` retirement label. Existing queued `supersedes` proposals and their `superseded` tombstone reason remain supported without rewriting them. Claim-preservation, same-run chain, knowledge/memory, continuity, freshness, archive and recovery checks still run on the selected sides.
|
|
624
624
|
- **Knowledge is never replaced with a memory** — If the retired side would be a knowledge note and the successor a memory (a `.derived` memory counts as a memory), the pair is flipped to retire the memory instead, but only if the judge lists no claim held only by the memory. Otherwise nothing is minted. The flipped pair then runs every guard like any retirement. *Why:* owner rule 2026-10-08: knowledge is the reviewed form and a memory is raw capture, often the memory the note was promoted from. 21 such retirements had been accepted since 2026-10-01. (`pair-pass.ts:757-763`; #1092)
|
|
625
625
|
- **Retire guards** — Never retire a `.derived` memory whose parent still exists. Within a run, an asset used as a retired side or successor cannot be retired or reused again (same-run chain guard). The "never retire a `.derived` whose parent exists" reasoning is not applied to successors: `foo` can be retired in favour of `bar.derived`, and `bar` later retired against `X`, so `foo`'s claims leave the live tree with no judge having compared `bar.derived` to anything. A retire-worthy verdict dropped by the chain guard counts as failed, so its initiator gets no ledger row and is retried. A pair whose refs and both body hashes match a rejected or reverted retire proposal is skipped before the judge call, except one rejected only by expiry or as a stale target. *Why:* night 1 judged 177 duplicate verdicts into only 59 proposals, so silently abandoning the dropped ones was a bug. A reverted retirement is the same "no, not this" signal as a rejection. (`pair-pass.ts:388-411,713-726,765-789`)
|
|
626
626
|
- **Continuity-risk check (forgetting safety)** — Before minting, up to 5 of the retired asset's most recent cleaned past queries are replayed through the real search with limit 10. For every query where the retired asset ranked in the top 10, the successor must also rank in the top 10. Failures, and unverified queries (the search threw, or fell back to keyword-only, including every later query once an earlier one fell back), set `continuityRisk` on the proposal. It still mints, but is excluded from all bulk accept and from drain staging. No recorded queries, or normalized-identical bodies, means no check and no risk. A flagged proposal has no recovery path: an embedding outage during the pair pass flags every retire proposal minted that night, they can only be accepted by id, no command re-runs the check (`checkRetirementContinuity` is called only from the pair pass), and reopen preserves the flag, so one bad night means about 90 individual accepts. *Why:* this is rule R3, the forgetting-safety lane's purpose moved to a per-proposal search-rank replay. An unverified query must never read as "no risk found". A dead embedding endpoint would otherwise cost about an hour (S2). (`src/commands/improve/consolidate/continuity-check.ts:5-16,26-29,135-192`; `pair-pass.ts:797-809`; `src/commands/proposal/proposal.ts:536-543`)
|
|
@@ -159,6 +159,10 @@ Notes:
|
|
|
159
159
|
- registry results live in `registryHits`, never in `hits`
|
|
160
160
|
- `--from all` keeps registry results in `registryHits` — they are not
|
|
161
161
|
rank-merged with source hits
|
|
162
|
+
- curate carries search's normalized scope/source/type/belief controls through
|
|
163
|
+
selection, derived-surface enrichment, support refs and packing. Search and curate default
|
|
164
|
+
to current; explicit all/historical evidence is labelled as historical context.
|
|
165
|
+
Direct show remains a deliberate inspection path without a belief default.
|
|
162
166
|
- local search and curate expose the actual execution in `searchMode`:
|
|
163
167
|
`semantic`, intentional/unavailable `keyword`, or `fts-fallback` when a
|
|
164
168
|
ready semantic runtime failed during the query; that degradation also
|
|
@@ -43,7 +43,7 @@ and named once, round-trips through ordinary writes, and is dropped by
|
|
|
43
43
|
| Workflow IR (frozen plans) | `src/workflows/freeze/freeze.ts`, read back in `src/workflows/runtime/run-plan.ts` (`readRunPlan`, `decodeWorkflowPlan`) | `irVersion` (`WORKFLOW_PLAN_VERSION`, 6) on the plan and in `plan_ir_version`; `plan_hash` alongside | Every workflow compiles to one plan type. `decodeWorkflowPlan` reads the irVersion 5 shape 0.9.15 and 0.9.16 wrote (its `sourceReadSet` and host-identity fields included) as well as the current one, and keeps keys it does not know. Neither the version nor the hash gates a read: a stored plan that decodes runs whatever `irVersion` froze it, with one warning; one that cannot be decoded is abandoned by `akm workflow run` (status `failed`) with a message naming `akm workflow run <ref>` to start afresh — a status change, never an exception; `status`/`list`/`abandon` never read the plan | A newer akm's plan that still decodes runs the same way; one that does not decode is left untouched and refused with a message naming "Upgrade akm" (`readRunPlan`'s `newer`) | `tests/workflows/run-plan.test.ts`; `tests/integration/workflows/frozen-plan.test.ts` — the rehearsal builds no workflow run |
|
|
44
44
|
| Native scheduler rows (crontab, launchd plists, Task Scheduler) | `src/tasks/backends/cron.ts`, `launchd.ts`, `schtasks.ts`; the invocation grammar in `src/tasks/scheduler-invocation.ts` | The row's own invocation shape: current rows set their environment inline (a cron `VAR=value` prefix, a plist `EnvironmentVariables` entry, a PowerShell `$env:` assignment) — always `AKM_BUNDLE_DIR`, the syncing shell's working stash — and carry no `--scheduler-context`; 0.9.0 – 0.9.17-alpha.6 rows carry `--scheduler-context <descriptor>` holding the same values; older rows carry neither. A cron command over 1,000 bytes runs an akm wrapper script (`sh <log dir>/.akm-cron-wrapper-…sh`) that holds the same line | Every shape is listed and parsed (`parseScheduledInvocationArgv`; a spilled cron row from its wrapper script): a row inside akm's own `# akm:task` markers from before `--scheduler-context` (#881), and a `--scheduler-context` row, which keeps firing (the CLI applies its descriptor, below) until the next plain `akm task sync` rewrites it in place — an update that keeps the row's launcher and schedule, never an add or remove. The primary bundle proves a row by the `AKM_BUNDLE_DIR` it carries, inline or in its descriptor (#846); any other bundle owns rows by name. Rows that still carry the pre-0.9.17 workflow execution-evidence marker (`# akm:workflow-evidence` in the crontab, an `akm-workflow-evidence` plist comment or PowerShell literal) are listed like any other and rewritten once without it. Plain `akm task sync` keeps an installed row's own launcher (`installOptionsFor`, `src/tasks/scheduler-sync.ts`) and recomputes its environment; `akm task sync --rebind` repoints the launcher | A row inside akm's markers, `com.akm.task.` label namespace, or `\akm\` task folder whose argv this binary cannot parse (for example one a newer akm wrote) is not listed: sync leaves it alone unless an enabled source renders a row with the same native id, which then replaces it — the row is derived state and its source is the record. A crontab whose akm markers are malformed is refused unmodified (`parseBlocks`, `cron.ts`) | Upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`: the previous release's `--scheduler-context` rows fire under the candidate, then `task sync --dry-run` shows them only as updates); `tests/tasks-sync.test.ts` (`--scheduler-context` rows and spilled rows are updates, never adds) |
|
|
45
45
|
| Scheduler-context descriptor (`$DATA/tasks/context/<sha256>.json`, written by 0.9.0 – 0.9.17-alpha.6) | No longer written: a row carries its own environment (#987). Read by `src/tasks/scheduler-invocation.ts` (`readLegacySchedulerContext`) | `version: 1`; the environment key set | When a row that names one fires, the CLI reads the file as plain JSON and applies its `AKM_*_DIR` keys and, in one written before 0.9.17, `PATH`; nothing else about the file (name, mode, owner, link) is checked. Sync reads its `AKM_BUNDLE_DIR` to attribute the row. A plain `akm task sync` that reconciles the row rewrites it without the argument; a row a sync leaves as it is (its source failed, a `--bundle` sync did not cover it) keeps naming the file | A key this binary does not know is ignored. A file that is gone or is not JSON fails the run with `INVALID_CONFIG_FILE` naming `akm task sync`; sync cannot attribute such a row to the primary bundle and leaves it in place, and `akm task prune` reports it as `invalid-context` | `tests/tasks-scheduler-invocation.test.ts`; `tests/integration/scheduler-context-launcher.test.ts`; upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`) fires the previous release's rows under the candidate before and after its plain `task sync` |
|
|
46
|
-
| Proposal `metadata_json` | `src/storage/repositories/proposals-repository.ts` | No explicit version field; presence/absence of the `changes` key | Rows from before the `changes` key existed (~89% of archived rows on real installs) are treated as a known legacy gap — decoded with an empty change list rather than thrown (`proposals-repository.ts:47-56`). 0.9.17-alpha.9 adds a consolidate pair-pass `retire` proposal's `changes[0].op: "delete"` (no `after`, already a valid `FileChangeOp`) and its `retirement` / `retiredArchive` / `promotionSource` / `promotionSourceHash` metadata keys, plus the retirement continuity check's `retirement.continuityRisk` (rule R3, item 1) — a person can accept a proposal carrying it by id even though it is excluded from bulk accept; an older reader's `validatePresentMetadata` does not enumerate these keys, so it neither inspects nor rejects them — the row decodes with them silently absent, same as any other field an older release predates. 0.9.19 adds `reviewHistory` (the rejections `akm proposal reopen` undid, oldest first, each with its gate verdict) the same way: a proposal reopened by 0.9.19 and read by an older release is an ordinary pending proposal with its history absent, and that release drops the key if it rewrites the row. The quality judge's evidence, `scores` and `judgeReason`, is added inside the existing `gateDecision` object (Unreleased): an older reader's `validatePresentMetadata` checks only the gate fields it knows and carries the object through whole, so the evidence survives a rewrite by an older release unread. An older release also counts a pending proposal's age (retention expiry, `--older-than`) from `createdAt`, so after a downgrade a reopened proposal can expire at the next `akm improve` run (a retire proposal never expires), or be swept by a scheduled `--older-than` bulk action or drain | 0.9.17-alpha.8 and earlier predate the retire proposal shape entirely — they were never taught to read a delete-primary change with no `after`. `akm proposal show` / `diff` / `drain` exit 70 on one, and drain's own nightly pre-pass failing on the first pending retire proposal it meets stops that run's auto-promotion for the whole stash, not just that one proposal. **Downgrade gate:** every pending retire proposal must be accepted or rejected (`akm proposal accept` / `reject`) before downgrading to 0.9.17-alpha.8 or earlier — once decided it is archived, out of the pending set `show`/`diff`/`drain` iterate, and no longer trips this. **Gap** otherwise (no code-level guard; this is operator guidance only). Downgrading also revives the starvation the second review round's B1 fixed forward-only: 0.9.17-alpha.8's `retrieval-scope.ts` counts a `consolidate-pair` ledger row as processed, so the pair pass's own nightly attempts crowd new material back out of every other improve lane's scope, the same way they did before B1 — measured on the real bundle, two alpha.9 nights left only 341 of 2,183 new-only assets still in scope once read under alpha.8. **Gap** here too (the ledger `source` string carries no version of its own; operator guidance only — do not downgrade across a pair-pass run without expecting this). | `tests/proposal-repository-pure.test.ts` — unit only; the rehearsal builds no proposal |
|
|
46
|
+
| Proposal `metadata_json` | `src/storage/repositories/proposals-repository.ts` | No explicit version field; presence/absence of the `changes` key | Rows from before the `changes` key existed (~89% of archived rows on real installs) are treated as a known legacy gap — decoded with an empty change list rather than thrown (`proposals-repository.ts:47-56`). 0.9.17-alpha.9 adds a consolidate pair-pass `retire` proposal's `changes[0].op: "delete"` (no `after`, already a valid `FileChangeOp`) and its `retirement` / `retiredArchive` / `promotionSource` / `promotionSourceHash` metadata keys, plus the retirement continuity check's `retirement.continuityRisk` (rule R3, item 1) — a person can accept a proposal carrying it by id even though it is excluded from bulk accept; an older reader's `validatePresentMetadata` does not enumerate these keys, so it neither inspects nor rejects them — the row decodes with them silently absent, same as any other field an older release predates. 0.9.19 adds `reviewHistory` (the rejections `akm proposal reopen` undid, oldest first, each with its gate verdict) the same way: a proposal reopened by 0.9.19 and read by an older release is an ordinary pending proposal with its history absent, and that release drops the key if it rewrites the row. `retirement.judgedScope` (Unreleased, #1197) optionally records the shared normalized memory scope tuple; `{}` means known unscoped and absence means unknown. Older rows still decode unchanged; acceptance refuses an unknown tuple and asks for rejection/reassessment, rather than inferring judgment from live files. Staged stale decisions use the existing stale-target rejection/ledger retry path. Older readers carry this nested object through but do not enforce it, so do not downgrade before reviewing pending retirements. No format-version bump or database migration is needed for this additive metadata. The quality judge's evidence, `scores` and `judgeReason`, is added inside the existing `gateDecision` object (Unreleased): an older reader's `validatePresentMetadata` checks only the gate fields it knows and carries the object through whole, so the evidence survives a rewrite by an older release unread. An older release also counts a pending proposal's age (retention expiry, `--older-than`) from `createdAt`, so after a downgrade a reopened proposal can expire at the next `akm improve` run (a retire proposal never expires), or be swept by a scheduled `--older-than` bulk action or drain | 0.9.17-alpha.8 and earlier predate the retire proposal shape entirely — they were never taught to read a delete-primary change with no `after`. `akm proposal show` / `diff` / `drain` exit 70 on one, and drain's own nightly pre-pass failing on the first pending retire proposal it meets stops that run's auto-promotion for the whole stash, not just that one proposal. **Downgrade gate:** every pending retire proposal must be accepted or rejected (`akm proposal accept` / `reject`) before downgrading to 0.9.17-alpha.8 or earlier — once decided it is archived, out of the pending set `show`/`diff`/`drain` iterate, and no longer trips this. **Gap** otherwise (no code-level guard; this is operator guidance only). Downgrading also revives the starvation the second review round's B1 fixed forward-only: 0.9.17-alpha.8's `retrieval-scope.ts` counts a `consolidate-pair` ledger row as processed, so the pair pass's own nightly attempts crowd new material back out of every other improve lane's scope, the same way they did before B1 — measured on the real bundle, two alpha.9 nights left only 341 of 2,183 new-only assets still in scope once read under alpha.8. **Gap** here too (the ledger `source` string carries no version of its own; operator guidance only — do not downgrade across a pair-pass run without expecting this). | `tests/proposal-repository-pure.test.ts` — unit only; the rehearsal builds no proposal |
|
|
47
47
|
| `improve_ledger` rows | `src/storage/repositories/improve-ledger-repository.ts` | No version field of its own; the `content_hash` column (migration `029-improve-ledger-content-hash`) | A consolidate promotion row with no `content_hash` is held by its clock alone (`next_eligible_at`). 0.9.19 records `content_hash`, and no `next_eligible_at`, on a consolidate promotion once it is accepted or rejected, and holds the memory until its body hash differs (`isContentDrivenRow`); a promotion decided by an older release has no hash and keeps its old window (an accepted one none, a rejected one 7 days) | An older release reads a row by `next_eligible_at` alone and ignores the hash, so after a downgrade a memory whose promotion 0.9.19 decided is offered to the promote pass again at once, a rejected one sooner than the 7 days the older release would have held it; the hash stays on the row until the older release records its next attempt on that memory, which clears it. **Gap** (no code-level guard; operator guidance only) | `tests/integration/storage/improve-ledger-repository.test.ts` |
|
|
48
48
|
| `task_history` metadata | `src/storage/repositories/task-history-repository.ts` | `metadataVersion` (currently `2`) | An absent `metadataVersion` is treated as a legacy row (58% of rows written by prior releases) and decoded best-effort; unknown fields are dropped harmlessly rather than round-tripped (`task-history-repository.ts:61-69`) | A `metadataVersion` newer than this binary's `2` is decoded best-effort as version 2 with a warning, rather than rejected (`task-history-repository.ts:85-89`) | Upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`) |
|
|
49
49
|
| Lock payloads | `src/core/file-lock.ts` | No version field; payload is a bare numeric pid string, or a JSON object carrying `pid` and optionally `launcherPid` (`extractLockIdentity`, `file-lock.ts`) | Any payload shape parses permissively: extra fields are ignored, a missing or unparseable `launcherPid` is simply absent, and a payload with no parseable `pid` probes as `stale` (`invalid_pid`, `probeLock`) and is reclaimed rather than rejected | Same permissive parse; a payload from a newer akm that adds fields is read the same way | `tests/integration/file-lock.test.ts` (describe "probeLock: launcherPid (#956)") — not the rehearsal, which does not contend locks |
|
package/docs/reference/api.md
CHANGED
|
@@ -18,7 +18,12 @@ const text = await curate("deploy to prod", { limit: 4, type: "skill", format: "
|
|
|
18
18
|
```ts
|
|
19
19
|
function curate(
|
|
20
20
|
query: string,
|
|
21
|
-
options?: {
|
|
21
|
+
options?: {
|
|
22
|
+
limit?: number; type?: string; source?: string;
|
|
23
|
+
filters?: { user?: string; agent?: string; run?: string; channel?: string };
|
|
24
|
+
belief?: "all" | "current" | "historical";
|
|
25
|
+
format?: "text" | "json";
|
|
26
|
+
},
|
|
22
27
|
): Promise<string>;
|
|
23
28
|
```
|
|
24
29
|
|
|
@@ -28,6 +33,19 @@ function curate(
|
|
|
28
33
|
`"json"`, as the CLI does. It runs the same code as the command (argument
|
|
29
34
|
validation, search, ranking, shaping and rendering are shared, not copied), so
|
|
30
35
|
the two cannot drift.
|
|
36
|
+
- **Recall controls.** `source` is CLI `--from` (local, registry, all, or a named
|
|
37
|
+
bundle); `filters` uses the same normalized scope keys and exact AND semantics
|
|
38
|
+
as repeated CLI `--filter key=value`. `belief` defaults to `"current"`, keeping
|
|
39
|
+
unflagged reference knowledge and active/asserted assets. These labels do not
|
|
40
|
+
prove factual truth. Explicit `"all"` and `"historical"` support investigations.
|
|
41
|
+
The policy also applies to derived surfaces, support refs and content reads;
|
|
42
|
+
historical evidence is labelled. Registry catalogs have no local scope/belief
|
|
43
|
+
metadata and stay separate.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
await curate("deploy to prod", { source: "team", filters: { user: "alice" } });
|
|
47
|
+
await curate("investigate the old deployment port", { belief: "historical" });
|
|
48
|
+
```
|
|
31
49
|
- **Failure.** It rejects with an `Error` whose `message` is the CLI's error
|
|
32
50
|
text and whose `code` is the CLI's error code (`MISSING_REQUIRED_ARGUMENT`,
|
|
33
51
|
`INVALID_FLAG_VALUE`, `INVALID_CONFIG_FILE`, ...) wherever the CLI would have
|
package/docs/reference/cli.md
CHANGED
|
@@ -616,7 +616,7 @@ kept.
|
|
|
616
616
|
| `--assets` | flag | `false` | Include asset-level registry results (only meaningful with `--from registry\|all`; folds in the retired `akm registry search --assets`) |
|
|
617
617
|
| `--filter` | `<key>=<value>` | _(none)_ | Scope filter — repeatable. Valid keys: `user`, `agent`, `run`, `channel`. Example: `--filter user=alice --filter channel=ops`. Narrows the result set; ranking is unchanged. |
|
|
618
618
|
| `--include-proposed` | flag | `false` | Include entries with `quality: "proposed"` in the result set. Default search excludes them; `generated` and `curated` quality entries are always included. Unknown quality values warn once and remain searchable. |
|
|
619
|
-
| `--belief` | `all`, `current`, `historical` | `
|
|
619
|
+
| `--belief` | `all`, `current`, `historical` | `current` | Belief filter on any asset. `current` keeps unflagged/active/asserted assets; `historical` keeps deprecated/contradicted/superseded/archived assets. Labels do not establish truth. |
|
|
620
620
|
| `--include-sessions` | flag | `false` | Include session assets, which are excluded from default results via `config.search.defaultExcludeTypes` |
|
|
621
621
|
| `--format` | `json`, `jsonl`, `yaml`, `text`, `md`, `html` | `json` | Output format |
|
|
622
622
|
| `--detail` | `brief`, `normal`, `full`, `agent` | `brief` | Output verbosity level, or the `agent` projection |
|
|
@@ -669,6 +669,10 @@ ranking.
|
|
|
669
669
|
> registries with different scoring formulas. See
|
|
670
670
|
> `docs/architecture/architecture.md` for the current type-level distinction.
|
|
671
671
|
|
|
672
|
+
Ordinary search defaults to current evidence. Use `--belief all` to enumerate
|
|
673
|
+
or compare every belief state, and `--belief historical` to investigate old
|
|
674
|
+
records. These modes also apply to empty and prefix queries.
|
|
675
|
+
|
|
672
676
|
### curate
|
|
673
677
|
|
|
674
678
|
Pick the assets worth loading for a task. Unlike `akm search`, curate attaches
|
|
@@ -680,13 +684,20 @@ akm curate "plan a release"
|
|
|
680
684
|
akm curate "deploy a Bun app" --limit 3
|
|
681
685
|
akm curate "review an architecture proposal" --type skill
|
|
682
686
|
akm curate "learn the release workflow" --from all --format text
|
|
687
|
+
akm curate "deploy the service" --from team --filter user=alice --filter channel=ops
|
|
688
|
+
akm curate "investigate the previous deployment port" --belief historical
|
|
689
|
+
akm curate "compare deployment decisions" --belief all
|
|
690
|
+
akm curate "deploy the service" --filter user=alice --pack 4000
|
|
683
691
|
```
|
|
684
692
|
|
|
685
693
|
| Flag | Values | Default | Description |
|
|
686
694
|
| --- | --- | --- | --- |
|
|
687
695
|
| `--type` | `skill`, `command`, `agent`, `knowledge`, `instruction`, `workflow`, `script`, `memory`, `env`, `secret`, `lesson`, `task`, `session`, `fact`, `any` | `any` | Filter curated results by asset type |
|
|
688
696
|
| `--limit` | number | `4` | Maximum curated results |
|
|
689
|
-
| `--from` | `local`, `registry`, `all
|
|
697
|
+
| `--from` | `local`, `registry`, `all`, bundle name | `local` | Where to search before curating |
|
|
698
|
+
| `--filter` | `<key>=<value>` | _(none)_ | Repeatable exact scope filter: user, agent, run, channel; supplied keys are AND-joined |
|
|
699
|
+
| `--belief` | `all`, `current`, `historical` | `current` | Current keeps unflagged and active/asserted assets; historical keeps deprecated/contradicted/superseded/archived assets |
|
|
700
|
+
| `--pack` | positive token budget | _(none)_ | Pack eligible local content in rank order; drop tail items before truncating the first oversized item |
|
|
690
701
|
|
|
691
702
|
`akm curate` takes the top `--limit` hits of one search, in search order, and
|
|
692
703
|
enriches each with a preview, run details and up to two support refs: the
|
|
@@ -700,6 +711,15 @@ commands such as `akm show <ref>` or `akm bundle add <ref>` so you can
|
|
|
700
711
|
immediately inspect or install what it found.
|
|
701
712
|
`--detail` (including `agent`) works on curate output.
|
|
702
713
|
Curate preserves the underlying search's `searchMode` and warnings.
|
|
714
|
+
Scope, named source, type and belief controls also constrain derived surfaces,
|
|
715
|
+
support refs and packed content; an empty eligible pool stays empty. Missing or
|
|
716
|
+
newly ineligible content is omitted instead of falling back to unchecked text.
|
|
717
|
+
Explicit all/historical results and support refs expose `beliefState` and label
|
|
718
|
+
historical context; packed historical items also carry a label (including text
|
|
719
|
+
output). Direct `show <ref>` remains available for deliberate inspection.
|
|
720
|
+
Ordinary knowledge without a belief label stays usable. Belief labels are
|
|
721
|
+
metadata, not evidence of factual truth. Existing prose within an eligible
|
|
722
|
+
asset is preserved; filters select assets rather than fact-checking their body.
|
|
703
723
|
Agent-shaped local items include `ref`, `path`, and `editable`, plus `editHint`
|
|
704
724
|
only for read-only items. Their `followUp` remains `akm show <ref>` rather than
|
|
705
725
|
being replaced by clone guidance.
|
|
@@ -787,6 +807,35 @@ them all. The field is omitted when nothing links either way. Links are read
|
|
|
787
807
|
from frontmatter and parsed structure at index time, with no model; they do
|
|
788
808
|
not affect search ranking.
|
|
789
809
|
|
|
810
|
+
Indexed body-link syntax depends on the bundle adapter:
|
|
811
|
+
|
|
812
|
+
| Adapter | Body links that produce `links_to` relationships |
|
|
813
|
+
| --- | --- |
|
|
814
|
+
| `okf` | Inline `[label](./target.md)` and explicit reference `[label][id]` with `[id]: ./target.md` definitions |
|
|
815
|
+
| `llm-wiki` | Inline Markdown links; frontmatter `xrefs:` and `sources:` retain their separate contracts |
|
|
816
|
+
| `akm` and other native adapters | Their declared frontmatter/structural relationships; ordinary Markdown body links do not automatically become `links_to` |
|
|
817
|
+
|
|
818
|
+
For OKF and LLM Wiki, relative paths resolve from the linking file's directory
|
|
819
|
+
and `/`-rooted paths from the component root. Optional titles, fragments and
|
|
820
|
+
queries do not change the target identity. Repeated targets collapse in first
|
|
821
|
+
appearance order. External schemes, protocol-relative URLs, in-page anchors,
|
|
822
|
+
non-`.md` targets and paths escaping the component root are excluded. Missing
|
|
823
|
+
supported targets stay unresolved; their diagnostics are non-blocking.
|
|
824
|
+
|
|
825
|
+
Inline backtick code and backtick/tilde fenced code (including fences inside
|
|
826
|
+
lists and blockquotes) produce no body-link relationships or broken-link
|
|
827
|
+
warnings. Reference definitions inside code cannot supply OKF links. Rendering
|
|
828
|
+
`[[wiki links]]` in the UI does not establish indexed support: neither adapter
|
|
829
|
+
indexes that syntax, and LLM Wiki does not index reference-style body links.
|
|
830
|
+
This is the existing indexed subset, not a full Markdown syntax parser.
|
|
831
|
+
|
|
832
|
+
After upgrading a build that indexed links inside examples, run `akm index
|
|
833
|
+
--full` once. It reparses unchanged files and replaces their stored relationships,
|
|
834
|
+
removing false outgoing links, backlinks and curated support references. Ordinary
|
|
835
|
+
incremental indexing updates changed/deleted entries but can retain unchanged
|
|
836
|
+
older parsing results. No data migration is required; rebuilding the index
|
|
837
|
+
under the selected build is also the recovery action after reverting the fix.
|
|
838
|
+
|
|
790
839
|
Opaque fragment shows and `--context lead` keep `ref` as the canonical parent
|
|
791
840
|
identity and add
|
|
792
841
|
`selectedRef`, `parentRef`, one-based `fragmentOrdinal`, `fragmentCount`,
|
|
@@ -2193,7 +2242,8 @@ service's config; reach for `secret` when one value *is* an auth credential.
|
|
|
2193
2242
|
Manage `.env`-backed **environment files** — a group of related **configuration**
|
|
2194
2243
|
for an app or service (URLs, feature flags, and any credentials it needs),
|
|
2195
2244
|
loaded together. Each `env` asset is an entire `.env` file stored under `env/`
|
|
2196
|
-
in your bundle (mode 0600
|
|
2245
|
+
in your bundle (mode 0600; on a filesystem without POSIX permissions, such as a
|
|
2246
|
+
CIFS/Azure Files share, the mount options decide the mode instead). Values may or may not be sensitive; **akm protects
|
|
2197
2247
|
them all the same** — key *names* are discoverable; values and comment text
|
|
2198
2248
|
never appear in structured output (comments routinely contain commented-out
|
|
2199
2249
|
credentials, so they are treated like values). akm does **not** manage
|
|
@@ -2353,7 +2403,9 @@ Manage **secrets** — a single sensitive value used on its own for
|
|
|
2353
2403
|
service-account JSON. Where an [env](#env) file holds a *group* of related
|
|
2354
2404
|
configuration and exposes key *names*, a secret is *one* value and its **entire
|
|
2355
2405
|
file is the value**, so only the secret's *name* is ever surfaced. Each secret
|
|
2356
|
-
is a mode-0600 file under `secrets/` in your bundle
|
|
2406
|
+
is a mode-0600 file under `secrets/` in your bundle (on a filesystem without
|
|
2407
|
+
POSIX permissions, such as a CIFS/Azure Files share, the mount options decide
|
|
2408
|
+
the mode instead).
|
|
2357
2409
|
|
|
2358
2410
|
This mirrors Docker's secret model (one value per file, mounted at
|
|
2359
2411
|
`/run/secrets/<name>`, read at runtime, never baked into the image or env at
|
|
@@ -97,7 +97,7 @@ Everything in the cache is regenerable. It is safe to delete the entire cache di
|
|
|
97
97
|
|
|
98
98
|
| Path | Contents | Safe to delete? |
|
|
99
99
|
|---|---|---|
|
|
100
|
-
| `config-backups/config-<timestamp>.json` | Pre-save config snapshots (5 retained; owner-only permissions — file `0600`, dir `0700`, since 08-F4) | Yes |
|
|
100
|
+
| `config-backups/config-<timestamp>.json` | Pre-save config snapshots (5 retained; owner-only permissions — file `0600`, dir `0700`, since 08-F4; on a filesystem without POSIX permissions, such as a CIFS/Azure Files share, the mount options decide them instead) | Yes |
|
|
101
101
|
| `config-backups/config.latest.json` | Latest backup alias (owner-only `0600`) | Yes |
|
|
102
102
|
| `registry/` | Downloaded registry tarballs (bundle packages from npm, GitHub, etc.) | Yes — re-downloaded on next `akm bundle add` or `akm bundle update` |
|
|
103
103
|
| `registry-index/` | Legacy per-URL JSON cache (v0.7 artifact) | Yes — fully replaced by `index.db` in 0.8.0 |
|
package/docs/reference/memory.md
CHANGED
|
@@ -55,12 +55,13 @@ supported values, from strongest to weakest authority:
|
|
|
55
55
|
| `contradicted` | Marked as contradicted by other evidence | historical |
|
|
56
56
|
| `archived` | Soft-deleted; retained for audit | historical |
|
|
57
57
|
|
|
58
|
-
`akm search`
|
|
58
|
+
`akm search` and `akm curate` filter via `--belief current|historical|all`:
|
|
59
59
|
|
|
60
|
-
- `current` → `active` + `asserted`
|
|
60
|
+
- `current` (default) → `active` + `asserted`; an asset with no `beliefState`
|
|
61
|
+
counts as `active`, so ordinary skills, knowledge and notes still surface
|
|
61
62
|
- `historical` → `deprecated` + `superseded` + `contradicted` + `archived`
|
|
62
|
-
- `all`
|
|
63
|
-
`contradicted
|
|
63
|
+
- `all` → no filter: every belief state, including `archived` and
|
|
64
|
+
`contradicted`, is eligible to surface.
|
|
64
65
|
|
|
65
66
|
### Derived memories as retrieval shortcuts
|
|
66
67
|
|
|
@@ -86,8 +87,8 @@ automation keeps working — agents that want the deeper summary follow
|
|
|
86
87
|
- Background-derived memories (those inferred from other assets by `akm
|
|
87
88
|
improve`) receive `captureMode: background`.
|
|
88
89
|
- A memory with no explicit `beliefState` defaults to `active`.
|
|
89
|
-
- `akm search --belief`
|
|
90
|
-
|
|
90
|
+
- `akm search --belief` and `akm curate --belief` default to `current`; pass
|
|
91
|
+
`--belief all` (API `belief: "all"`) for every belief state.
|
|
91
92
|
|
|
92
93
|
## Security / persistence implications
|
|
93
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.261009-beta",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|
|
@@ -124,6 +124,7 @@
|
|
|
124
124
|
"citty": "^0.2.2",
|
|
125
125
|
"dotenv": "^17.4.2",
|
|
126
126
|
"fast-xml-parser": "^5.10.1",
|
|
127
|
+
"marked": "^18.1.1",
|
|
127
128
|
"node-html-parser": "^9.0.1",
|
|
128
129
|
"semver": "^7.6.0",
|
|
129
130
|
"turndown": "^7.2.4",
|