akm-cli 0.9.26-alpha.2 → 0.9.27-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.
- package/CHANGELOG.md +294 -0
- package/dist/assets/hints/cli-hints-full.md +16 -10
- package/dist/assets/hints/cli-hints-short.md +5 -5
- package/dist/assets/prompts/consolidate-system.md +2 -2
- package/dist/assets/prompts/extract-session.md +2 -2
- package/dist/assets/stash-skeleton/README.md +4 -3
- package/dist/commands/feedback-cli.js +244 -43
- package/dist/commands/improve/consolidate/pair-pass.js +1 -1
- package/dist/commands/improve/consolidate.js +14 -4
- package/dist/commands/improve/distill.js +70 -7
- package/dist/commands/improve/extract-prompt.js +61 -39
- package/dist/commands/improve/extract.js +2 -1
- package/dist/commands/improve/loop-stages.js +16 -1
- package/dist/commands/improve/memory/memory-belief.js +1 -1
- package/dist/commands/improve/preparation.js +46 -0
- package/dist/commands/improve/reflect.js +51 -8
- package/dist/commands/improve/retrieval-gate.js +1 -1
- package/dist/commands/improve/session-asset.js +3 -2
- package/dist/commands/improve/stage.js +1 -1
- package/dist/commands/proposal/repository.js +22 -6
- package/dist/core/asset/akm-markdown.js +40 -16
- package/dist/core/asset/frontmatter.js +67 -7
- package/dist/core/config/config-schema.js +1 -1
- package/dist/core/config/config.js +0 -4
- package/dist/core/config/schema/feedback.js +2 -19
- package/dist/indexer/indexer.js +35 -19
- package/dist/integrations/harnesses/codex/agent-builder.js +23 -15
- package/dist/llm/client.js +44 -14
- package/dist/llm/memory-infer.js +1 -1
- package/dist/scripts/akm-migrate-node.js +26 -22
- package/dist/scripts/akm-migrate.js +26 -22
- package/dist/storage/repositories/index-entry-schema.js +20 -4
- package/dist/storage/repositories/index-fts-repository.js +44 -3
- package/dist/storage/repositories/index-schema.js +14 -8
- package/docs/reference/cli.md +36 -17
- package/docs/reference/configuration.md +12 -6
- package/docs/reference/data-and-telemetry.md +3 -3
- package/package.json +1 -1
- package/schemas/akm-config.json +0 -14
|
@@ -24967,6 +24967,7 @@ var $visitAsync = visit.visitAsync;
|
|
|
24967
24967
|
import path7 from "path";
|
|
24968
24968
|
// src/runtime.ts
|
|
24969
24969
|
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
|
|
24970
|
+
import { createHash } from "crypto";
|
|
24970
24971
|
import { createWriteStream, statfsSync } from "fs";
|
|
24971
24972
|
import { createRequire } from "module";
|
|
24972
24973
|
import path6 from "path";
|
|
@@ -26131,6 +26132,9 @@ async function writeResponseToFileCapped(filePath, res, options) {
|
|
|
26131
26132
|
reader.releaseLock?.();
|
|
26132
26133
|
}
|
|
26133
26134
|
}
|
|
26135
|
+
function sha256Hex(data) {
|
|
26136
|
+
return createHash("sha256").update(data).digest("hex");
|
|
26137
|
+
}
|
|
26134
26138
|
function getDirname(importMetaUrl) {
|
|
26135
26139
|
return path6.dirname(fileURLToPath(importMetaUrl));
|
|
26136
26140
|
}
|
|
@@ -35173,8 +35177,7 @@ var ExperimentalConfigSchema = exports_external.object({
|
|
|
35173
35177
|
|
|
35174
35178
|
// src/core/config/schema/feedback.ts
|
|
35175
35179
|
var FeedbackConfigSchema = exports_external.object({
|
|
35176
|
-
requireReason: exports_external.boolean().optional()
|
|
35177
|
-
allowedFailureModes: exports_external.array(nonEmptyString).optional()
|
|
35180
|
+
requireReason: exports_external.boolean().optional()
|
|
35178
35181
|
}).passthrough();
|
|
35179
35182
|
|
|
35180
35183
|
// src/core/config/schema/improve-processes.ts
|
|
@@ -36178,7 +36181,7 @@ function withConfigLock(fn) {
|
|
|
36178
36181
|
}
|
|
36179
36182
|
|
|
36180
36183
|
// src/core/config/config-sources.ts
|
|
36181
|
-
import { createHash } from "crypto";
|
|
36184
|
+
import { createHash as createHash2 } from "crypto";
|
|
36182
36185
|
import fs9 from "fs";
|
|
36183
36186
|
import path13 from "path";
|
|
36184
36187
|
function bundleComponentConfig(bundle) {
|
|
@@ -36241,7 +36244,7 @@ function filesystemBundleSourceId(contentRoot) {
|
|
|
36241
36244
|
});
|
|
36242
36245
|
}
|
|
36243
36246
|
function hashBundleSourceIdentity(identity3) {
|
|
36244
|
-
return `sha256:${
|
|
36247
|
+
return `sha256:${createHash2("sha256").update("akm.bundle-source\x00v1\x00").update(JSON.stringify(identity3)).digest("hex")}`;
|
|
36245
36248
|
}
|
|
36246
36249
|
function bundleContentRoots(config) {
|
|
36247
36250
|
const bundles = config.bundles ?? {};
|
|
@@ -37703,7 +37706,7 @@ async function tryFetchJson(url, headers) {
|
|
|
37703
37706
|
}
|
|
37704
37707
|
|
|
37705
37708
|
// src/sources/providers/provider-utils.ts
|
|
37706
|
-
import { createHash as
|
|
37709
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
|
|
37707
37710
|
import fs27 from "fs";
|
|
37708
37711
|
import path34 from "path";
|
|
37709
37712
|
|
|
@@ -37798,7 +37801,7 @@ function countLines(text) {
|
|
|
37798
37801
|
}
|
|
37799
37802
|
|
|
37800
37803
|
// src/core/adapter/adapters/shared.ts
|
|
37801
|
-
import { createHash as
|
|
37804
|
+
import { createHash as createHash3 } from "crypto";
|
|
37802
37805
|
|
|
37803
37806
|
// src/core/asset/frontmatter-lint.ts
|
|
37804
37807
|
function checkUnquotedDescriptionColon(frontmatterText) {
|
|
@@ -37820,7 +37823,7 @@ function checkUnquotedDescriptionColon(frontmatterText) {
|
|
|
37820
37823
|
|
|
37821
37824
|
// src/core/adapter/adapters/shared.ts
|
|
37822
37825
|
function hashContent(content) {
|
|
37823
|
-
return
|
|
37826
|
+
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
37824
37827
|
}
|
|
37825
37828
|
function nonEmptyString2(value) {
|
|
37826
37829
|
if (typeof value !== "string")
|
|
@@ -39003,7 +39006,7 @@ function extractDirTagsFromName(name) {
|
|
|
39003
39006
|
}
|
|
39004
39007
|
|
|
39005
39008
|
// src/core/adapter/execution-source.ts
|
|
39006
|
-
import { createHash as
|
|
39009
|
+
import { createHash as createHash4 } from "crypto";
|
|
39007
39010
|
|
|
39008
39011
|
// src/execution/source.ts
|
|
39009
39012
|
var EXECUTION_SOURCE_SCHEMA_VERSION = 1;
|
|
@@ -39341,7 +39344,7 @@ function renderMarkdownExecutionSource(input) {
|
|
|
39341
39344
|
bundle: identity3.bundle,
|
|
39342
39345
|
adapter: identity3.adapter,
|
|
39343
39346
|
file: identity3.file,
|
|
39344
|
-
hash:
|
|
39347
|
+
hash: createHash4("sha256").update(raw, "utf8").digest("hex")
|
|
39345
39348
|
},
|
|
39346
39349
|
...hasDefaults ? { defaults } : {},
|
|
39347
39350
|
...extensions === undefined ? {} : { extensions }
|
|
@@ -45896,7 +45899,7 @@ function uniqueSlug() {
|
|
|
45896
45899
|
}
|
|
45897
45900
|
function buildInstallCacheDir(cacheRootDir, source, id, version) {
|
|
45898
45901
|
const safeId = id.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
45899
|
-
const identitySuffix = version === "writable" ? `-${
|
|
45902
|
+
const identitySuffix = version === "writable" ? `-${createHash5("sha256").update(`${source}:${id}`).digest("hex").slice(0, 12)}` : "";
|
|
45900
45903
|
const slug = `${source}-${safeId}${identitySuffix}`;
|
|
45901
45904
|
const versionSlug = source === "local" ? uniqueSlug() : version?.replace(/[^a-zA-Z0-9_.-]+/g, "-") ?? uniqueSlug();
|
|
45902
45905
|
return path34.join(cacheRootDir, slug || source, versionSlug);
|
|
@@ -45917,7 +45920,7 @@ async function downloadArchive(url, destination) {
|
|
|
45917
45920
|
}
|
|
45918
45921
|
async function computeFileHash(filePath) {
|
|
45919
45922
|
const data = fs27.readFileSync(filePath);
|
|
45920
|
-
const hash =
|
|
45923
|
+
const hash = createHash5("sha256").update(data).digest("hex");
|
|
45921
45924
|
return `sha256:${hash}`;
|
|
45922
45925
|
}
|
|
45923
45926
|
function isDirectory(target) {
|
|
@@ -46092,7 +46095,7 @@ function redactUrlUserinfo(text) {
|
|
|
46092
46095
|
return text.replace(/\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^\s/@]+)@/g, "$1[REDACTED]@");
|
|
46093
46096
|
}
|
|
46094
46097
|
// src/sources/providers/git-provider.ts
|
|
46095
|
-
import { createHash as
|
|
46098
|
+
import { createHash as createHash6 } from "crypto";
|
|
46096
46099
|
import fs30 from "fs";
|
|
46097
46100
|
import path36 from "path";
|
|
46098
46101
|
|
|
@@ -46153,7 +46156,7 @@ function resolveGitContentDir(config) {
|
|
|
46153
46156
|
throw new ConfigError("git source entry must have either `path` or `url`");
|
|
46154
46157
|
}
|
|
46155
46158
|
function getCachePaths(repoUrl, cacheRootOverride) {
|
|
46156
|
-
const key =
|
|
46159
|
+
const key = createHash6("sha256").update(repoUrl).digest("hex").slice(0, 16);
|
|
46157
46160
|
const cacheRoot = cacheRootOverride ?? getRegistryIndexCacheDir();
|
|
46158
46161
|
const rootDir = path36.join(cacheRoot, `git-${key}`);
|
|
46159
46162
|
return {
|
|
@@ -46374,7 +46377,7 @@ import fs33 from "fs";
|
|
|
46374
46377
|
import path39 from "path";
|
|
46375
46378
|
|
|
46376
46379
|
// src/sources/providers/tar-utils.ts
|
|
46377
|
-
import { createHash as
|
|
46380
|
+
import { createHash as createHash7 } from "crypto";
|
|
46378
46381
|
import fs32 from "fs";
|
|
46379
46382
|
import path38 from "path";
|
|
46380
46383
|
function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
@@ -46387,7 +46390,7 @@ function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
|
46387
46390
|
const dashIndex = expected.indexOf("-");
|
|
46388
46391
|
const algorithm = expected.slice(0, dashIndex);
|
|
46389
46392
|
const expectedBase64 = expected.slice(dashIndex + 1);
|
|
46390
|
-
const actualBase64 =
|
|
46393
|
+
const actualBase64 = createHash7(algorithm).update(fileBuffer).digest("base64");
|
|
46391
46394
|
if (actualBase64 !== expectedBase64) {
|
|
46392
46395
|
fs32.unlinkSync(archivePath);
|
|
46393
46396
|
throw new Error(`Integrity check failed for ${archivePath}: expected ${algorithm} digest ${expectedBase64}, got ${actualBase64}`);
|
|
@@ -46395,7 +46398,7 @@ function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
|
46395
46398
|
return;
|
|
46396
46399
|
}
|
|
46397
46400
|
if (/^[0-9a-f]{40}$/i.test(expected)) {
|
|
46398
|
-
const actualHex =
|
|
46401
|
+
const actualHex = createHash7("sha1").update(fileBuffer).digest("hex");
|
|
46399
46402
|
if (actualHex.toLowerCase() !== expected.toLowerCase()) {
|
|
46400
46403
|
fs32.unlinkSync(archivePath);
|
|
46401
46404
|
throw new Error(`Integrity check failed for ${archivePath}: expected sha1 ${expected}, got ${actualHex}`);
|
|
@@ -51665,14 +51668,15 @@ class ClaudeHarness extends BaseHarness {
|
|
|
51665
51668
|
}
|
|
51666
51669
|
|
|
51667
51670
|
// src/integrations/harnesses/codex/agent-builder.ts
|
|
51668
|
-
import {
|
|
51669
|
-
import { tmpdir } from "os";
|
|
51671
|
+
import { mkdirSync } from "fs";
|
|
51670
51672
|
import { join as join2 } from "path";
|
|
51671
51673
|
function writeCodexOutputSchemaFile(schema) {
|
|
51672
|
-
const
|
|
51673
|
-
|
|
51674
|
-
|
|
51675
|
-
|
|
51674
|
+
const text = `${JSON.stringify(schema, null, 2)}
|
|
51675
|
+
`;
|
|
51676
|
+
const dir = join2(getCacheDir(), "codex-output-schemas");
|
|
51677
|
+
mkdirSync(dir, { recursive: true });
|
|
51678
|
+
const file = join2(dir, `${sha256Hex(text)}.json`);
|
|
51679
|
+
writeFileAtomic(file, text);
|
|
51676
51680
|
return file;
|
|
51677
51681
|
}
|
|
51678
51682
|
function ensureSandboxFlags(base) {
|
|
@@ -120,20 +120,36 @@ const REQUIRED_ENTRY_COLUMNS = [
|
|
|
120
120
|
"document_json",
|
|
121
121
|
"derived_from",
|
|
122
122
|
];
|
|
123
|
+
/**
|
|
124
|
+
* The transitional columns layout 20 and earlier kept beside the current ones
|
|
125
|
+
* and layout 21 removed (`entry_key` was the key). They are NOT NULL, so a
|
|
126
|
+
* table that still has one takes none of this release's inserts, and its
|
|
127
|
+
* `document_json` is empty (the entry sat in `entry_json`), so none of its rows
|
|
128
|
+
* can be read either. A 0.9.1 index is such a table.
|
|
129
|
+
*/
|
|
130
|
+
const RETIRED_ENTRY_COLUMNS = ["entry_key", "dir_path", "stash_dir", "entry_json", "entry_type"];
|
|
131
|
+
function entryColumnNames(db) {
|
|
132
|
+
return new Set(db.prepare("PRAGMA table_info(entries)").all().map((c) => c.name));
|
|
133
|
+
}
|
|
123
134
|
/** Required `entries` columns the table lacks; every column when there is no `entries` table. */
|
|
124
135
|
export function missingEntryColumns(db) {
|
|
125
|
-
const present =
|
|
136
|
+
const present = entryColumnNames(db);
|
|
126
137
|
return REQUIRED_ENTRY_COLUMNS.filter((column) => !present.has(column));
|
|
127
138
|
}
|
|
139
|
+
/** Retired `entries` columns the table still has: it was written by layout 20 or earlier. */
|
|
140
|
+
export function retiredEntryColumns(db) {
|
|
141
|
+
const present = entryColumnNames(db);
|
|
142
|
+
return RETIRED_ENTRY_COLUMNS.filter((column) => present.has(column));
|
|
143
|
+
}
|
|
128
144
|
/**
|
|
129
145
|
* Whether an index database has an `entries` table this release can read. A
|
|
130
146
|
* fresh or empty file has none; an index older than layout 21 has one keyed
|
|
131
|
-
* by columns this release no longer reads
|
|
132
|
-
* `akm index` recreates it.
|
|
147
|
+
* by columns this release no longer reads (or lacks the ones it does), and
|
|
148
|
+
* serves nothing until the next `akm index` recreates it.
|
|
133
149
|
*/
|
|
134
150
|
export function hasCurrentEntriesTable(db) {
|
|
135
151
|
try {
|
|
136
|
-
return missingEntryColumns(db).length === 0;
|
|
152
|
+
return missingEntryColumns(db).length === 0 && retiredEntryColumns(db).length === 0;
|
|
137
153
|
}
|
|
138
154
|
catch {
|
|
139
155
|
return false;
|
|
@@ -19,9 +19,27 @@ import { splitMarkdownFragments } from "../../core/asset/markdown-fragments.js";
|
|
|
19
19
|
import { warn } from "../../core/warn.js";
|
|
20
20
|
import { ftsOrMatch, ftsQueryTokens } from "../../indexer/search/fts-query.js";
|
|
21
21
|
import { buildSearchFields } from "../../indexer/search/search-fields.js";
|
|
22
|
+
import { isContentlessFtsDdl, readTableSql } from "./index-entry-schema.js";
|
|
23
|
+
import { deleteMeta, getMeta } from "./index-meta-repository.js";
|
|
22
24
|
import { SQLITE_CHUNK_SIZE } from "./index-sql.js";
|
|
23
25
|
// `entries_fts.rowid = entries.id`, so a per-entry delete is a rowid lookup.
|
|
24
26
|
const INSERT_FTS_SQL = "INSERT INTO entries_fts (rowid, entry_id, name, description, tags, hints, content) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
|
27
|
+
/**
|
|
28
|
+
* `index_meta` key stamped when a delete took a row out of `entries_fts`.
|
|
29
|
+
*
|
|
30
|
+
* FTS5 cannot take a deleted row out of a contentless table's BM25 totals (its
|
|
31
|
+
* row count, and the token counts the average document length comes from), so
|
|
32
|
+
* every removal or replacement leaves them one row too high and an updated
|
|
33
|
+
* index scores differently from a fresh one (#1048). SQLite has no command that
|
|
34
|
+
* recomputes them (`delete` and `rebuild` are refused on a contentless table),
|
|
35
|
+
* so the next `akm index` rebuilds the table from `entries`
|
|
36
|
+
* ({@link rebuildFtsIfTotalsStale}).
|
|
37
|
+
*/
|
|
38
|
+
const FTS_TOTALS_STALE_META = "ftsTotalsStale";
|
|
39
|
+
/** The content-bearing table older SQLite gets subtracts a deleted row itself. */
|
|
40
|
+
function isContentlessFts(db) {
|
|
41
|
+
return isContentlessFtsDdl(readTableSql(db, "entries_fts"));
|
|
42
|
+
}
|
|
25
43
|
const ftsMutationStatementsByDb = new WeakMap();
|
|
26
44
|
function getFtsMutationStatements(db) {
|
|
27
45
|
const existing = ftsMutationStatementsByDb.get(db);
|
|
@@ -29,6 +47,7 @@ function getFtsMutationStatements(db) {
|
|
|
29
47
|
return existing;
|
|
30
48
|
const statements = {
|
|
31
49
|
deleteOne: db.prepare("DELETE FROM entries_fts WHERE rowid = ?"),
|
|
50
|
+
markTotalsStale: db.prepare(`INSERT OR IGNORE INTO index_meta (key, value) VALUES ('${FTS_TOTALS_STALE_META}', '1')`),
|
|
32
51
|
insert: db.prepare(INSERT_FTS_SQL),
|
|
33
52
|
upsertFragmentSource: db.prepare("INSERT INTO entry_fragments (entry_id, safe_markdown) VALUES (?, ?) ON CONFLICT(entry_id) DO UPDATE SET safe_markdown = excluded.safe_markdown"),
|
|
34
53
|
deleteFragmentSource: db.prepare("DELETE FROM entry_fragments WHERE entry_id = ?"),
|
|
@@ -36,6 +55,11 @@ function getFtsMutationStatements(db) {
|
|
|
36
55
|
ftsMutationStatementsByDb.set(db, statements);
|
|
37
56
|
return statements;
|
|
38
57
|
}
|
|
58
|
+
/** A delete that removed a row leaves the table's BM25 totals too high until it is rebuilt. */
|
|
59
|
+
function noteRemoval(statements, removed) {
|
|
60
|
+
if (removed.changes > 0)
|
|
61
|
+
statements.markTotalsStale.run();
|
|
62
|
+
}
|
|
39
63
|
/**
|
|
40
64
|
* Replace one entry's derived FTS row, and its fragment source when the scan
|
|
41
65
|
* read Markdown, inside the caller's transaction.
|
|
@@ -43,7 +67,7 @@ function getFtsMutationStatements(db) {
|
|
|
43
67
|
export function replaceFtsEntry(db, entryId, entry, fragmentContent) {
|
|
44
68
|
const fields = buildSearchFields(entry);
|
|
45
69
|
const statements = getFtsMutationStatements(db);
|
|
46
|
-
statements.deleteOne.run(entryId);
|
|
70
|
+
noteRemoval(statements, statements.deleteOne.run(entryId));
|
|
47
71
|
statements.insert.run(entryId, entryId, fields.name, fields.description, fields.tags, fields.hints, fields.content);
|
|
48
72
|
if (fragmentContent === undefined) {
|
|
49
73
|
// Metadata-only re-upserts and re-keys deserialize the public document
|
|
@@ -61,7 +85,7 @@ export function deleteFtsEntries(db, entryIds) {
|
|
|
61
85
|
for (let i = 0; i < entryIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
62
86
|
const chunk = entryIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
63
87
|
const placeholders = chunk.map(() => "?").join(",");
|
|
64
|
-
db.prepare(`DELETE FROM entries_fts WHERE rowid IN (${placeholders})`).run(...chunk);
|
|
88
|
+
noteRemoval(getFtsMutationStatements(db), db.prepare(`DELETE FROM entries_fts WHERE rowid IN (${placeholders})`).run(...chunk));
|
|
65
89
|
db.prepare(`DELETE FROM entry_fragments WHERE entry_id IN (${placeholders})`).run(...chunk);
|
|
66
90
|
}
|
|
67
91
|
}
|
|
@@ -143,7 +167,9 @@ function materializeIndexedMarkdownFragment(fragment, fragments, parentChars) {
|
|
|
143
167
|
*/
|
|
144
168
|
export function rebuildFts(db) {
|
|
145
169
|
db.transaction(() => {
|
|
146
|
-
|
|
170
|
+
// `delete-all` also resets the BM25 totals, which a plain DELETE leaves as they were (#1048).
|
|
171
|
+
db.exec(isContentlessFts(db) ? "INSERT INTO entries_fts(entries_fts) VALUES('delete-all')" : "DELETE FROM entries_fts");
|
|
172
|
+
deleteMeta(db, FTS_TOTALS_STALE_META);
|
|
147
173
|
// Keyset pages, so a large index is never held in memory at once.
|
|
148
174
|
const page = db.prepare("SELECT id, document_json FROM entries WHERE id > ? ORDER BY id LIMIT 500");
|
|
149
175
|
const insertStmt = db.prepare(INSERT_FTS_SQL);
|
|
@@ -171,3 +197,18 @@ export function rebuildFts(db) {
|
|
|
171
197
|
}
|
|
172
198
|
})();
|
|
173
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Rebuild `entries_fts` from `entries` when rows have left it since its BM25
|
|
202
|
+
* totals were last taken (#1048): about a second at 25,000 entries. Returns
|
|
203
|
+
* whether it did.
|
|
204
|
+
*/
|
|
205
|
+
export function rebuildFtsIfTotalsStale(db) {
|
|
206
|
+
if (getMeta(db, FTS_TOTALS_STALE_META) === undefined)
|
|
207
|
+
return false;
|
|
208
|
+
if (!isContentlessFts(db)) {
|
|
209
|
+
deleteMeta(db, FTS_TOTALS_STALE_META);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
rebuildFts(db);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
@@ -25,7 +25,7 @@ import path from "node:path";
|
|
|
25
25
|
import { ConfigError } from "../../core/errors.js";
|
|
26
26
|
import { warn } from "../../core/warn.js";
|
|
27
27
|
import { sha256Hex } from "../../runtime.js";
|
|
28
|
-
import { CANONICAL_ENTRY_SCHEMA_SQL, CANONICAL_INDEX_DB_VERSION, entriesFtsDdl, isContentlessFtsDdl, missingEntryColumns, readTableSql, supportsContentlessDelete, tableExists, } from "./index-entry-schema.js";
|
|
28
|
+
import { CANONICAL_ENTRY_SCHEMA_SQL, CANONICAL_INDEX_DB_VERSION, entriesFtsDdl, isContentlessFtsDdl, missingEntryColumns, readTableSql, retiredEntryColumns, supportsContentlessDelete, tableExists, } from "./index-entry-schema.js";
|
|
29
29
|
import { rebuildFts } from "./index-fts-repository.js";
|
|
30
30
|
import { rebuildAllEntryLinks } from "./index-links-repository.js";
|
|
31
31
|
import { getMeta, setMeta } from "./index-meta-repository.js";
|
|
@@ -67,11 +67,13 @@ const REGISTRY_INDEX_CACHE_DDL = `
|
|
|
67
67
|
ON registry_index_cache(fetched_at);
|
|
68
68
|
`;
|
|
69
69
|
/**
|
|
70
|
-
* An `entries` table missing a required column
|
|
71
|
-
* this release (the last such change was
|
|
72
|
-
* transitional `entry_key`/`dir_path`/... columns
|
|
73
|
-
*
|
|
74
|
-
*
|
|
70
|
+
* An `entries` table missing a required column, or still carrying a retired
|
|
71
|
+
* one, cannot be read or written by this release (the last such change was
|
|
72
|
+
* v20→v21, which removed the transitional `entry_key`/`dir_path`/... columns
|
|
73
|
+
* and made `item_ref` the key). Layouts 18–20 carried the current columns
|
|
74
|
+
* beside the retired ones, so only the retired ones give them away.
|
|
75
|
+
* Recreate only the tables keyed by `entries.id` — their ids are about to be
|
|
76
|
+
* re-minted, so the rows would dangle anyway. The LLM enrichment cache
|
|
75
77
|
* (keyed by ref) is kept. The LLM entity-graph tables are unconditionally
|
|
76
78
|
* dropped elsewhere in this file regardless of this recreation (retired
|
|
77
79
|
* 0.9.17-alpha.9), not kept. The next index run re-walks every source.
|
|
@@ -80,9 +82,13 @@ function ensureEntriesLayout(db) {
|
|
|
80
82
|
if (!tableExists(db, "entries"))
|
|
81
83
|
return;
|
|
82
84
|
const missing = missingEntryColumns(db);
|
|
83
|
-
|
|
85
|
+
const retired = retiredEntryColumns(db);
|
|
86
|
+
if (missing.length === 0 && retired.length === 0)
|
|
84
87
|
return;
|
|
85
|
-
|
|
88
|
+
const why = missing.length > 0
|
|
89
|
+
? `predates the ${missing.join(", ")} column${missing.length === 1 ? "" : "s"}`
|
|
90
|
+
: `still has the retired ${retired.join(", ")} column${retired.length === 1 ? "" : "s"}`;
|
|
91
|
+
warn(`Index database entries table ${why} — ` +
|
|
86
92
|
"recreating the entries-keyed tables (entries, full-text, embeddings, utility scores); the " +
|
|
87
93
|
"LLM enrichment cache is kept. The next index run re-walks every source.");
|
|
88
94
|
db.transaction(() => {
|
package/docs/reference/cli.md
CHANGED
|
@@ -1587,17 +1587,27 @@ preserves it byte-for-byte.
|
|
|
1587
1587
|
|
|
1588
1588
|
### feedback
|
|
1589
1589
|
|
|
1590
|
-
Record positive or negative feedback for any indexed bundle asset.
|
|
1590
|
+
Record positive or negative feedback for any indexed bundle asset. Record
|
|
1591
|
+
`--negative` only when the asset's content is wrong or stale, and say what is
|
|
1592
|
+
wrong and what it should say; a note that simply did not fit your task is not
|
|
1593
|
+
negative feedback, so record nothing for it.
|
|
1591
1594
|
`akm feedback <ref> --negative --reason "<what is wrong and what should change>"`
|
|
1592
1595
|
flags the asset: it ranks lower right away, and the next improve run may repair
|
|
1593
1596
|
its description, title or `when_to_use` from your reason. Improve does not
|
|
1594
|
-
rewrite an asset's text.
|
|
1595
|
-
with `--replace`, `--with` and `--source`: akm checks that each
|
|
1596
|
-
text appears exactly once and that the frontmatter still parses,
|
|
1597
|
-
if either check fails, and queues the edit as a `feedback`
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1597
|
+
rewrite an asset's text. Once you have verified the correct fact, attach the
|
|
1598
|
+
exact fix with `--replace`, `--with` and `--source`: akm checks that each
|
|
1599
|
+
`--replace` text appears exactly once and that the frontmatter still parses,
|
|
1600
|
+
records nothing if either check fails, and queues the edit as a `feedback`
|
|
1601
|
+
proposal for review.
|
|
1602
|
+
To mark the asset's history, with or without a text fix, add `--superseded-by
|
|
1603
|
+
<ref>` (another asset replaces it) or `--outdated` (it describes a past state
|
|
1604
|
+
and no single asset replaces it), with `--reason` and `--source`. The same single
|
|
1605
|
+
proposal sets the asset's `beliefState` (`superseded`, or `deprecated`) and, for
|
|
1606
|
+
`--superseded-by`, adds the successor's ref to its `supersededBy` list, by
|
|
1607
|
+
editing only those lines of the frontmatter. `--positive` records that an asset
|
|
1608
|
+
helped (it raises its ranking) and does not trigger a rewrite. Both signals
|
|
1609
|
+
update the asset's utility score right away, so highly-rated assets rank higher
|
|
1610
|
+
in search results.
|
|
1601
1611
|
|
|
1602
1612
|
```sh
|
|
1603
1613
|
akm feedback scripts/deploy.sh --positive
|
|
@@ -1605,20 +1615,23 @@ akm feedback agents/reviewer --negative
|
|
|
1605
1615
|
akm feedback memories/deployment-notes --positive
|
|
1606
1616
|
akm feedback env/prod --positive
|
|
1607
1617
|
akm feedback skills/code-review --positive --reason "Worked perfectly for PR reviews"
|
|
1608
|
-
akm feedback skills/code-review --negative --
|
|
1618
|
+
akm feedback skills/code-review --negative --reason "references a removed flag"
|
|
1609
1619
|
akm feedback skills/code-review --negative --reason "flaky" --tag slice:train --tag team:platform
|
|
1610
1620
|
akm feedback knowledge/opencode-server --negative --reason "the default port is 4096, not 8000" --replace "port 8000" --with "port 4096" --source "https://opencode.ai/docs/server/"
|
|
1621
|
+
akm feedback knowledge/setup-v1 --negative --reason "the v2 guide replaces it" --superseded-by knowledge/setup-v2 --source "knowledge/setup-v2"
|
|
1622
|
+
akm feedback knowledge/api-v1 --negative --reason "describes the retired v1 API" --outdated --source "https://example.com/changelog"
|
|
1611
1623
|
```
|
|
1612
1624
|
|
|
1613
1625
|
| Flag | Description |
|
|
1614
1626
|
| --- | --- |
|
|
1615
1627
|
| `--positive` | Record that an asset helped: it raises its ranking and does not trigger a rewrite |
|
|
1616
1628
|
| `--negative` | Flag the asset: it ranks lower right away, and the next improve run may repair its frontmatter from `--reason` |
|
|
1617
|
-
| `--reason` | What is wrong with the asset's content and what should change; not for `akm` command errors. Attached to the feedback event (required for negative feedback by default, and always with `--replace`) |
|
|
1629
|
+
| `--reason` | What is wrong with the asset's content and what should change; not for `akm` command errors. Attached to the feedback event (required for negative feedback by default, and always with a fix: `--replace`, `--superseded-by` or `--outdated`) |
|
|
1618
1630
|
| `--replace <text>` | Exact text to correct, copied verbatim from the asset file; it must appear exactly once. Repeatable, each paired in order with a `--with`. Negative feedback only |
|
|
1619
1631
|
| `--with <text>` | The corrected text for the matching `--replace`. Use `--with=<text>` for a value that starts with `-` |
|
|
1620
|
-
| `--source <where>` | The URL, command or file that shows the correct fact. Required with `--replace`; shown to the reviewer with the proposal |
|
|
1621
|
-
| `--
|
|
1632
|
+
| `--source <where>` | The URL, command or file that shows the correct fact. Required with `--replace`, `--superseded-by` and `--outdated`; shown to the reviewer with the proposal |
|
|
1633
|
+
| `--superseded-by <ref>` | The ref of the asset that replaces this one. The proposal sets `beliefState: superseded` and adds the ref, as its `bundle//conceptId`, to `supersededBy`; `contradicted` and `archived` stay, a ref already listed is not added again, and a scalar `supersededBy` becomes a list. The ref must be indexed and must not be the asset itself, or nothing is recorded; nor is anything when the asset already says all this (the fix changes nothing). Negative feedback only; may be combined with `--replace`/`--with`, not with `--outdated`; markdown assets only |
|
|
1634
|
+
| `--outdated` | The asset describes a past state and no single asset replaces it. The proposal sets `beliefState: deprecated`, unless the asset already says `superseded`, `contradicted` or `archived`. Negative feedback only; may be combined with `--replace`/`--with`, not with `--superseded-by`; markdown assets only |
|
|
1622
1635
|
| `--tag` | Tag to attach to the feedback (repeatable, e.g. `--tag slice:train --tag team:platform`) |
|
|
1623
1636
|
| `--applied-to <ref>` | Credit a `lessons/<name>` lesson that helped resolve this task. When combined with `--positive`, appends this feedback ref to the target lesson's `lessonStrength[]` frontmatter array (dedup, idempotent). A non-lesson target, or a missing `--positive`, produces a warning rather than silently doing nothing. |
|
|
1624
1637
|
|
|
@@ -2522,8 +2535,12 @@ feedback in the last 30 days newer than the stage's last ledger attempt, or for
|
|
|
2522
2535
|
an explicit ref scope. A positive or note-only signal never plans one, so
|
|
2523
2536
|
improve does not rewrite an asset from a positive signal. Distill keeps its own
|
|
2524
2537
|
trigger: a memory with feedback of any kind (a signal or a note) in that window,
|
|
2525
|
-
newer than distill's last attempt.
|
|
2526
|
-
feedback
|
|
2538
|
+
newer than distill's last attempt. It skips a memory flagged wrong and not
|
|
2539
|
+
edited since (a negative feedback in that window judged the body it still has,
|
|
2540
|
+
or, recorded without that body's hash, is newer than the file's last write),
|
|
2541
|
+
and a memory whose only feedback in that window is positive with no reason or
|
|
2542
|
+
note, unless the ref is explicit. Two fallback lanes pick refs
|
|
2543
|
+
with no such feedback: high salience (content-scored refs at or above
|
|
2527
2544
|
`improve.salience.salienceThreshold`, default `0.75`, that were never reflected,
|
|
2528
2545
|
capped at 10% of the limit, at least one ref) and, in a strategy that enables
|
|
2529
2546
|
`proactiveMaintenance`, refs due for a revisit. They only select and score refs
|
|
@@ -2763,6 +2780,7 @@ harvest" branch on `skipReasons`, `warnings`, or `sessionsProcessed` /
|
|
|
2763
2780
|
| `engineKind` | `"llm"`, `"sdk"`, or `"agent"` — the kind of runner `engine` resolved to. Same absence condition as `engine`. |
|
|
2764
2781
|
| `skipReasons` | Per-`skipReason` count across `sessions[]` (e.g. `{ "llm_unavailable": 25 }`). Present only when `sessionsSkipped > 0`. |
|
|
2765
2782
|
| `warnings` | Includes one aggregate line per infrastructure skip reason that fired (`llm_unavailable`, `read_failed`, `exception`, `locked_concurrent`) — e.g. `25 of 25 sessions skipped: llm_unavailable (engine "default")` — so an engine outage is visible without inspecting `sessions[]`. Session-content skips (`already_extracted`, `too_short`, `triaged_out`) are counted in `skipReasons` but never produce a warning line. |
|
|
2783
|
+
| `sessions[].warnings` | One line per candidate the model wrote that the output contract refuses, as `<type>:<name> dropped: <reason>` — a lesson without a `when_to_use` of 15 characters, a description under 20, a name that is not a kebab-case slug — and one per candidate held back by the improve ledger. A session whose every candidate was dropped has `candidateCount: 0` and no `rationaleIfEmpty`, so this is where the loss shows. |
|
|
2766
2784
|
|
|
2767
2785
|
#### proposal new
|
|
2768
2786
|
|
|
@@ -3081,9 +3099,10 @@ passed on its current content is accepted (unless its target changed since it
|
|
|
3081
3099
|
was minted — that one is auto-rejected as `stale-target`); an empty diff is
|
|
3082
3100
|
rejected; a proposal that reflect or distill deferred for review is left for a
|
|
3083
3101
|
person; everything else goes to the judgment tier when one is enabled, and
|
|
3084
|
-
is otherwise left for review. A reflect revision that changes the body
|
|
3085
|
-
|
|
3086
|
-
decisions (queue mode); pass
|
|
3102
|
+
is otherwise left for review. A reflect revision that changes the body, and
|
|
3103
|
+
every distill lesson or knowledge promotion, is deferred for review even when
|
|
3104
|
+
its judge passes it. Default mode stages decisions (queue mode); pass
|
|
3105
|
+
`--promote` to actually accept.
|
|
3087
3106
|
|
|
3088
3107
|
```sh
|
|
3089
3108
|
akm proposal drain --dry-run # Preview without writing
|
|
@@ -110,8 +110,11 @@ distill quality-gate judges do too unless the judge's engine sets
|
|
|
110
110
|
`reasoning_effort` when set. Backend support: llama.cpp direct honors both forms
|
|
111
111
|
(`reasoning_effort` from build ≥ b10644); vLLM honors
|
|
112
112
|
`chat_template_kwargs`; Bifrost drops `chat_template_kwargs` and passes
|
|
113
|
-
`reasoning_effort` through, so also set `reasoningEffort: "none"` behind it
|
|
114
|
-
strict hosted API
|
|
113
|
+
`reasoning_effort` through, so also set `reasoningEffort: "none"` behind it. A
|
|
114
|
+
strict hosted API that rejects the two fields (OpenAI answers 400 `Unknown
|
|
115
|
+
parameter: 'chat_template_kwargs'`) gets one retry without both, and AKM stops
|
|
116
|
+
sending them to that endpoint and model for the rest of the process, as it
|
|
117
|
+
does for `response_format` below. Both fields are AKM-owned, not
|
|
115
118
|
settable via `extraParams`. A response with reasoning tokens despite
|
|
116
119
|
`enableThinking: false` triggers a runtime warning and the `akm health`
|
|
117
120
|
`thinking-control` advisory.
|
|
@@ -467,7 +470,11 @@ guidance. When enabled, engine selection is judgment → triage → strategy →
|
|
|
467
470
|
|
|
468
471
|
`processes.reflect.qualityGate` and `processes.distill.qualityGate` control
|
|
469
472
|
each process's LLM-as-judge quality gate. Each is on unless it sets
|
|
470
|
-
`enabled: false`, and each follows only its own switch.
|
|
473
|
+
`enabled: false`, and each follows only its own switch. A reflect revision the
|
|
474
|
+
judge passes is staged for the triage drain to accept; a distill lesson it
|
|
475
|
+
passes is deferred for a person (reason `distill-review`), which the drain and
|
|
476
|
+
its judgment tier leave alone. With the distill gate off nothing is judged, and
|
|
477
|
+
the drain decides. The judge is the
|
|
471
478
|
process's own engine when that is an LLM engine, or the `defaults.llmEngine`
|
|
472
479
|
engine when an agent generates. `engine`, `model`, `timeoutMs` and `llm` give the gate a judge of its own,
|
|
473
480
|
resolved over the process's settings the way `triage.judgment` resolves over
|
|
@@ -745,12 +752,11 @@ or malformed response keeps the fused order.
|
|
|
745
752
|
|
|
746
753
|
## Feedback
|
|
747
754
|
|
|
748
|
-
`feedback`
|
|
755
|
+
`feedback` configures `akm feedback`:
|
|
749
756
|
|
|
750
757
|
| Key | Purpose |
|
|
751
758
|
| --- | --- |
|
|
752
|
-
| `feedback.requireReason` | Whether `akm feedback --negative` without `--reason
|
|
753
|
-
| `feedback.allowedFailureModes` | Restrict `--failure-mode` values accepted by `akm feedback`. Curated set (also the default when unset): `incorrect`, `outdated`, `dangerous`, `incomplete`, `redundant` |
|
|
759
|
+
| `feedback.requireReason` | Whether `akm feedback --negative` without `--reason` is a hard error. **Defaults to `true`** when unset — set `false` to downgrade the check to a warning instead |
|
|
754
760
|
|
|
755
761
|
## Bundles and write target
|
|
756
762
|
|
|
@@ -159,7 +159,7 @@ the set of types the code actually emits at HEAD (verified against every
|
|
|
159
159
|
| `curate` | `akm curate <prompt>` | `query`, `itemCount`, `itemRefs` |
|
|
160
160
|
| `show` | `akm show <ref>` | `ref`, `type`, `name` |
|
|
161
161
|
| `select` | `akm show` after a search returning the same ref | `ref`, `query`, `searchTs`, `rankPosition` |
|
|
162
|
-
| `feedback` | `akm feedback <ref>` | `signal` (positive/negative), `reason`, `
|
|
162
|
+
| `feedback` | `akm feedback <ref>` | `signal` (positive/negative), `reason`, `tags`, `fix` (`source`, the number of replacements and, for `--superseded-by` or `--outdated`, the `beliefState` the proposal leaves and the `supersededBy` ref, when a fix was attached), `contentHash` (sha256 of the asset's body, without its frontmatter, as it stood when the feedback was given: it lets reflect mark feedback given on an earlier version of the text, and the loop's distill pass tell that a memory flagged wrong still has it; left out for an env or secret file and when the file cannot be read) |
|
|
163
163
|
| `sync` | `akm sync` | `name`, `message`, `ok` |
|
|
164
164
|
| `index_db_vacuumed` | `akm index` VACUUMed index.db, after an index layout migration or because more than half its pages were free | `pagesBefore`, `pagesAfter`, `freelistRatioBefore` |
|
|
165
165
|
| `stash_synced` | `akm improve`'s internal auto-sync pass (the `sync.push` feature), **distinct from** the `akm sync` command above | `committed`, `pushed`, `skipped`, `reason`, `attributed` (paths the run wrote and staged), `unattributed` (in-scope paths that went dirty during the run without the run writing them — left for their author) |
|
|
@@ -188,14 +188,14 @@ the set of types the code actually emits at HEAD (verified against every
|
|
|
188
188
|
| `improve_invoked` | Start of an `akm improve` run | `ref` (scope); `strategy`, `scope`, `dryRun`, `eligibleCount` |
|
|
189
189
|
| `improve_completed` | `akm improve` run finished | run stats |
|
|
190
190
|
| `improve_failed` | `akm improve` run errored | error |
|
|
191
|
-
| `improve_skipped` | `akm improve` left a ref, a lane, or a group of refs out | `reason` (`no_new_signal`, `not_retrieved`, `distill_no_new_signal`, `budget_exhausted`, `budget_exhausted_batch`, `asset_missing_on_disk`, `strategy_filtered_all_passes`, `autonomy_gated`, `engine_unavailable`, `pool_below_min_size`, `consolidation_no_memory_updates`, `below_min_new_sessions`, `derived_memory_reflect_skipped`, `memory_distill_requires_feedback`); `count`, `remaining`, `strategy`, `lane` or `configKey` where they apply |
|
|
191
|
+
| `improve_skipped` | `akm improve` left a ref, a lane, or a group of refs out | `reason` (`no_new_signal`, `not_retrieved`, `distill_no_new_signal`, `budget_exhausted`, `budget_exhausted_batch`, `asset_missing_on_disk`, `strategy_filtered_all_passes`, `autonomy_gated`, `engine_unavailable`, `pool_below_min_size`, `consolidation_no_memory_updates`, `below_min_new_sessions`, `derived_memory_reflect_skipped`, `memory_distill_requires_feedback`, `distill_flagged_wrong`, `distill_positive_without_reason`); `count`, `remaining`, `strategy`, `lane` or `configKey` where they apply |
|
|
192
192
|
| `improve_lock_recovered` | Stale improve lock cleared at startup | |
|
|
193
193
|
| `improve_review_needed` | `akm feedback` pushed a high-utility asset's utility below the review threshold — a review-needed escalation is recorded (not a proposal, so it can't accidentally overwrite the asset) | `ref`, `previousUtility`, `nextUtility` |
|
|
194
194
|
| `reflect_invoked` | Start of reflect phase in `akm improve` | `ref`, engine |
|
|
195
195
|
| `reflect_completed` | Reflect phase produced a proposal | `ref` |
|
|
196
196
|
| `improve_reflect_outcome` | Per-asset reflect result | `ref`, `ok`, `durationMs`, `reason` |
|
|
197
197
|
| `propose_invoked` | `akm proposal new` | `ref` |
|
|
198
|
-
| `distill_invoked` | Distill phase inside the `akm improve`/`akm proposal new` pipeline. **`akm distill` is not a CLI command** — there is no standalone verb by that name | `ref`, outcome |
|
|
198
|
+
| `distill_invoked` | Distill phase inside the `akm improve`/`akm proposal new` pipeline. **`akm distill` is not a CLI command** — there is no standalone verb by that name | `ref`, outcome (`queued`, `skipped` with a `skipReason` such as `lesson_exists` or `conflict_noop`, `llm_failed`, `validation_failed`, `quality_rejected`, `review_needed`) |
|
|
199
199
|
| `extract_invoked` | `akm proposal extract --type <harness>` / `--auto`, or improve-stage session extraction | `outcome`, `sessionId`, `harness` |
|
|
200
200
|
| `extract_triaged` | The pre-LLM extract triage gate evaluated at least one session | `evaluated`, `passed`, `triagedOut`, `sourceRun` (aggregated) |
|
|
201
201
|
| `schema_repair_invoked` | The schema-repair pass inside `akm improve` (`runSchemaRepairPass`) attempts to patch missing frontmatter on an asset that failed schema validation. **There is no `akm lint --repair` flag** — `lint` has `--fix`/`--auto-fix`, unrelated to this event | `ref`, outcome |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.27-alpha.1",
|
|
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": [
|
package/schemas/akm-config.json
CHANGED
|
@@ -595,13 +595,6 @@
|
|
|
595
595
|
"properties": {
|
|
596
596
|
"requireReason": {
|
|
597
597
|
"type": "boolean"
|
|
598
|
-
},
|
|
599
|
-
"allowedFailureModes": {
|
|
600
|
-
"type": "array",
|
|
601
|
-
"items": {
|
|
602
|
-
"type": "string",
|
|
603
|
-
"minLength": 1
|
|
604
|
-
}
|
|
605
598
|
}
|
|
606
599
|
},
|
|
607
600
|
"additionalProperties": true
|
|
@@ -2274,13 +2267,6 @@
|
|
|
2274
2267
|
"properties": {
|
|
2275
2268
|
"requireReason": {
|
|
2276
2269
|
"type": "boolean"
|
|
2277
|
-
},
|
|
2278
|
-
"allowedFailureModes": {
|
|
2279
|
-
"type": "array",
|
|
2280
|
-
"items": {
|
|
2281
|
-
"type": "string",
|
|
2282
|
-
"minLength": 1
|
|
2283
|
-
}
|
|
2284
2270
|
}
|
|
2285
2271
|
},
|
|
2286
2272
|
"additionalProperties": true
|