akm-cli 0.9.26 → 0.9.27-alpha.2
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 +237 -0
- package/LICENSE +3 -4
- package/dist/assets/prompts/consolidate-system.md +2 -2
- package/dist/assets/prompts/distill-lesson-system.md +29 -7
- package/dist/assets/prompts/extract-session.md +2 -2
- package/dist/commands/improve/consolidate/coverage.js +71 -17
- package/dist/commands/improve/consolidate/pair-pass.js +12 -9
- package/dist/commands/improve/consolidate.js +77 -5
- package/dist/commands/improve/distill-guards.js +9 -10
- package/dist/commands/improve/distill.js +70 -22
- package/dist/commands/improve/extract-prompt.js +61 -39
- package/dist/commands/improve/extract.js +2 -1
- package/dist/commands/improve/preparation.js +14 -1
- package/dist/commands/improve/reflect.js +36 -4
- package/dist/commands/improve/retrieval-gate.js +1 -1
- package/dist/commands/improve/session-asset.js +3 -2
- package/dist/commands/improve/stage.js +35 -53
- package/dist/commands/proposal/drain.js +85 -21
- package/dist/commands/proposal/proposal-types.js +1 -1
- package/dist/core/config/schema/improve-processes.js +3 -3
- package/dist/core/paths.js +0 -9
- 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 +25 -20
- package/dist/scripts/akm-migrate.js +25 -20
- package/dist/storage/repositories/improve-ledger-repository.js +4 -1
- 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/README.md +1 -2
- package/docs/integration/bundling-akm.md +1 -1
- package/docs/migration/v0.8-to-v0.9.md +3 -1
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +8 -4
- package/docs/reference/configuration.md +5 -2
- package/docs/reference/data-and-telemetry.md +1 -1
- package/package.json +1 -1
package/dist/llm/client.js
CHANGED
|
@@ -222,12 +222,18 @@ async function chatCompletionReal(config, messages, options) {
|
|
|
222
222
|
* a config/endpoint change can never leave a stale verdict in place.
|
|
223
223
|
*/
|
|
224
224
|
const jsonSchemaUnsupportedConnections = new Set();
|
|
225
|
+
/**
|
|
226
|
+
* The same, for the two thinking-control fields (`chat_template_kwargs` and
|
|
227
|
+
* `enable_thinking`): a strict hosted API answers 400 `Unknown parameter`.
|
|
228
|
+
*/
|
|
229
|
+
const thinkingControlRejectedConnections = new Set();
|
|
225
230
|
function connectionKey(config) {
|
|
226
231
|
return `${config.endpoint}|${config.model}`;
|
|
227
232
|
}
|
|
228
|
-
/** TEST-ONLY. Clear the in-memory
|
|
229
|
-
export function
|
|
233
|
+
/** TEST-ONLY. Clear the in-memory endpoint-support trackers between tests. */
|
|
234
|
+
export function _resetEndpointSupportTrackersForTests() {
|
|
230
235
|
jsonSchemaUnsupportedConnections.clear();
|
|
236
|
+
thinkingControlRejectedConnections.clear();
|
|
231
237
|
}
|
|
232
238
|
/**
|
|
233
239
|
* Whether this connection has already been proven, this process, not to
|
|
@@ -239,6 +245,24 @@ export function _resetJsonSchemaSupportTrackerForTests() {
|
|
|
239
245
|
export function isJsonSchemaKnownUnsupported(config) {
|
|
240
246
|
return jsonSchemaUnsupportedConnections.has(connectionKey(config));
|
|
241
247
|
}
|
|
248
|
+
/** Whether a real call this process showed this connection rejects the thinking-control fields (#1045). */
|
|
249
|
+
export function isThinkingControlKnownRejected(config) {
|
|
250
|
+
return thinkingControlRejectedConnections.has(connectionKey(config));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Whether `err` is a provider's 4xx (a rate limit is not one) that may be a
|
|
254
|
+
* verdict on the shape of this request rather than on the request's own content.
|
|
255
|
+
*/
|
|
256
|
+
function isRejectedRequest(err) {
|
|
257
|
+
return (err instanceof LlmCallError &&
|
|
258
|
+
err.code === "provider_error" &&
|
|
259
|
+
typeof err.statusCode === "number" &&
|
|
260
|
+
err.statusCode >= 400 &&
|
|
261
|
+
err.statusCode < 500 &&
|
|
262
|
+
err.statusCode !== 429);
|
|
263
|
+
}
|
|
264
|
+
/** A strict API names the field it does not know: `Unknown parameter: 'chat_template_kwargs'.` */
|
|
265
|
+
const THINKING_FIELD_NAMED = /chat_template_kwargs|enable_thinking/;
|
|
242
266
|
/**
|
|
243
267
|
* A single chat-completion attempt: one HTTP request/response cycle, with an
|
|
244
268
|
* inline fallback-once when a schema was requested and the provider 4xx's
|
|
@@ -248,29 +272,35 @@ export function isJsonSchemaKnownUnsupported(config) {
|
|
|
248
272
|
* `config.supportsJsonSchema === false` is the only thing that skips it,
|
|
249
273
|
* and that is a value a human (or workflow author) set explicitly, not one
|
|
250
274
|
* a probe wrote automatically.
|
|
275
|
+
*
|
|
276
|
+
* The thinking-control fields get the same treatment (#1045): a 4xx that
|
|
277
|
+
* names `chat_template_kwargs` or `enable_thinking` is answered by one retry
|
|
278
|
+
* without them, which may in turn fall back without the schema.
|
|
251
279
|
*/
|
|
252
|
-
async function chatCompletionAttempt(config, messages, options, timeoutMs)
|
|
280
|
+
async function chatCompletionAttempt(config, messages, options, timeoutMs, includeThinking = (options?.enableThinking ?? config.enableThinking) !== undefined &&
|
|
281
|
+
!isThinkingControlKnownRejected(config)) {
|
|
253
282
|
const wantsSchema = Boolean(options?.responseSchema) && config.supportsJsonSchema !== false && !isJsonSchemaKnownUnsupported(config);
|
|
254
283
|
try {
|
|
255
|
-
return await chatCompletionAttemptOnce(config, messages, options, timeoutMs, wantsSchema);
|
|
284
|
+
return await chatCompletionAttemptOnce(config, messages, options, timeoutMs, wantsSchema, includeThinking);
|
|
256
285
|
}
|
|
257
286
|
catch (err) {
|
|
258
|
-
if (!
|
|
259
|
-
!(err instanceof LlmCallError) ||
|
|
260
|
-
err.code !== "provider_error" ||
|
|
261
|
-
typeof err.statusCode !== "number" ||
|
|
262
|
-
err.statusCode < 400 ||
|
|
263
|
-
err.statusCode >= 500 ||
|
|
264
|
-
err.statusCode === 429) {
|
|
287
|
+
if (!isRejectedRequest(err))
|
|
265
288
|
throw err;
|
|
289
|
+
if (includeThinking && THINKING_FIELD_NAMED.test(err.message)) {
|
|
290
|
+
warnVerbose(`[akm] LLM rejected chat_template_kwargs/enable_thinking (${err.statusCode}); retrying once without them: ${err.message}`);
|
|
291
|
+
const fallback = await chatCompletionAttempt(config, messages, options, timeoutMs, false);
|
|
292
|
+
thinkingControlRejectedConnections.add(connectionKey(config));
|
|
293
|
+
return fallback;
|
|
266
294
|
}
|
|
295
|
+
if (!wantsSchema)
|
|
296
|
+
throw err;
|
|
267
297
|
warnVerbose(`[akm] LLM rejected response_format:json_schema (${err.statusCode}); retrying once without it: ${err.message}`);
|
|
268
|
-
const fallback = await chatCompletionAttemptOnce(config, messages, options, timeoutMs, false);
|
|
298
|
+
const fallback = await chatCompletionAttemptOnce(config, messages, options, timeoutMs, false, includeThinking);
|
|
269
299
|
jsonSchemaUnsupportedConnections.add(connectionKey(config));
|
|
270
300
|
return fallback;
|
|
271
301
|
}
|
|
272
302
|
}
|
|
273
|
-
async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, includeSchema) {
|
|
303
|
+
async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, includeSchema, includeThinking) {
|
|
274
304
|
if (config.extraParams !== undefined) {
|
|
275
305
|
const issue = validateExtraParams(config.extraParams)[0];
|
|
276
306
|
if (issue)
|
|
@@ -309,7 +339,7 @@ async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, i
|
|
|
309
339
|
// (freellmapi, Bifrost) can drop either one depending on how it was built.
|
|
310
340
|
// Send both whenever thinking is explicitly resolved so the same engine
|
|
311
341
|
// block keeps working across a direct vhost or any gateway in front of it.
|
|
312
|
-
const resolvedEnableThinking = options?.enableThinking ?? config.enableThinking;
|
|
342
|
+
const resolvedEnableThinking = includeThinking ? (options?.enableThinking ?? config.enableThinking) : undefined;
|
|
313
343
|
const thinkingParams = resolvedEnableThinking === undefined
|
|
314
344
|
? {}
|
|
315
345
|
: { chat_template_kwargs: { enable_thinking: resolvedEnableThinking }, enable_thinking: resolvedEnableThinking };
|
package/dist/llm/memory-infer.js
CHANGED
|
@@ -46,7 +46,7 @@ const PROMPT_PLACEHOLDERS = new Set([
|
|
|
46
46
|
* — no more "incomplete derived memory payload from LLM; skipping memory"
|
|
47
47
|
* for shape-only failures.
|
|
48
48
|
*/
|
|
49
|
-
const DERIVED_MEMORY_JSON_SCHEMA = {
|
|
49
|
+
export const DERIVED_MEMORY_JSON_SCHEMA = {
|
|
50
50
|
type: "object",
|
|
51
51
|
properties: {
|
|
52
52
|
title: { type: "string", minLength: 1 },
|
|
@@ -25639,6 +25639,7 @@ var $visitAsync = visit.visitAsync;
|
|
|
25639
25639
|
import path7 from "node:path";
|
|
25640
25640
|
// src/runtime.ts
|
|
25641
25641
|
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process";
|
|
25642
|
+
import { createHash } from "node:crypto";
|
|
25642
25643
|
import { createWriteStream, statfsSync } from "node:fs";
|
|
25643
25644
|
import { createRequire as createRequire2 } from "node:module";
|
|
25644
25645
|
import path6 from "node:path";
|
|
@@ -26803,6 +26804,9 @@ async function writeResponseToFileCapped(filePath, res, options) {
|
|
|
26803
26804
|
reader.releaseLock?.();
|
|
26804
26805
|
}
|
|
26805
26806
|
}
|
|
26807
|
+
function sha256Hex(data) {
|
|
26808
|
+
return createHash("sha256").update(data).digest("hex");
|
|
26809
|
+
}
|
|
26806
26810
|
function getDirname(importMetaUrl) {
|
|
26807
26811
|
return path6.dirname(fileURLToPath(importMetaUrl));
|
|
26808
26812
|
}
|
|
@@ -36849,7 +36853,7 @@ function withConfigLock(fn) {
|
|
|
36849
36853
|
}
|
|
36850
36854
|
|
|
36851
36855
|
// src/core/config/config-sources.ts
|
|
36852
|
-
import { createHash } from "node:crypto";
|
|
36856
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
36853
36857
|
import fs9 from "node:fs";
|
|
36854
36858
|
import path13 from "node:path";
|
|
36855
36859
|
function bundleComponentConfig(bundle) {
|
|
@@ -36912,7 +36916,7 @@ function filesystemBundleSourceId(contentRoot) {
|
|
|
36912
36916
|
});
|
|
36913
36917
|
}
|
|
36914
36918
|
function hashBundleSourceIdentity(identity3) {
|
|
36915
|
-
return `sha256:${
|
|
36919
|
+
return `sha256:${createHash2("sha256").update("akm.bundle-source\x00v1\x00").update(JSON.stringify(identity3)).digest("hex")}`;
|
|
36916
36920
|
}
|
|
36917
36921
|
function bundleContentRoots(config) {
|
|
36918
36922
|
const bundles = config.bundles ?? {};
|
|
@@ -38374,7 +38378,7 @@ async function tryFetchJson(url, headers) {
|
|
|
38374
38378
|
}
|
|
38375
38379
|
|
|
38376
38380
|
// src/sources/providers/provider-utils.ts
|
|
38377
|
-
import { createHash as
|
|
38381
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
|
|
38378
38382
|
import fs27 from "node:fs";
|
|
38379
38383
|
import path34 from "node:path";
|
|
38380
38384
|
|
|
@@ -38469,7 +38473,7 @@ function countLines(text) {
|
|
|
38469
38473
|
}
|
|
38470
38474
|
|
|
38471
38475
|
// src/core/adapter/adapters/shared.ts
|
|
38472
|
-
import { createHash as
|
|
38476
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
38473
38477
|
|
|
38474
38478
|
// src/core/asset/frontmatter-lint.ts
|
|
38475
38479
|
function checkUnquotedDescriptionColon(frontmatterText) {
|
|
@@ -38491,7 +38495,7 @@ function checkUnquotedDescriptionColon(frontmatterText) {
|
|
|
38491
38495
|
|
|
38492
38496
|
// src/core/adapter/adapters/shared.ts
|
|
38493
38497
|
function hashContent(content) {
|
|
38494
|
-
return
|
|
38498
|
+
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
38495
38499
|
}
|
|
38496
38500
|
function nonEmptyString2(value) {
|
|
38497
38501
|
if (typeof value !== "string")
|
|
@@ -39674,7 +39678,7 @@ function extractDirTagsFromName(name) {
|
|
|
39674
39678
|
}
|
|
39675
39679
|
|
|
39676
39680
|
// src/core/adapter/execution-source.ts
|
|
39677
|
-
import { createHash as
|
|
39681
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
39678
39682
|
|
|
39679
39683
|
// src/execution/source.ts
|
|
39680
39684
|
var EXECUTION_SOURCE_SCHEMA_VERSION = 1;
|
|
@@ -40012,7 +40016,7 @@ function renderMarkdownExecutionSource(input) {
|
|
|
40012
40016
|
bundle: identity3.bundle,
|
|
40013
40017
|
adapter: identity3.adapter,
|
|
40014
40018
|
file: identity3.file,
|
|
40015
|
-
hash:
|
|
40019
|
+
hash: createHash4("sha256").update(raw, "utf8").digest("hex")
|
|
40016
40020
|
},
|
|
40017
40021
|
...hasDefaults ? { defaults } : {},
|
|
40018
40022
|
...extensions === undefined ? {} : { extensions }
|
|
@@ -46567,7 +46571,7 @@ function uniqueSlug() {
|
|
|
46567
46571
|
}
|
|
46568
46572
|
function buildInstallCacheDir(cacheRootDir, source, id, version) {
|
|
46569
46573
|
const safeId = id.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
46570
|
-
const identitySuffix = version === "writable" ? `-${
|
|
46574
|
+
const identitySuffix = version === "writable" ? `-${createHash5("sha256").update(`${source}:${id}`).digest("hex").slice(0, 12)}` : "";
|
|
46571
46575
|
const slug = `${source}-${safeId}${identitySuffix}`;
|
|
46572
46576
|
const versionSlug = source === "local" ? uniqueSlug() : version?.replace(/[^a-zA-Z0-9_.-]+/g, "-") ?? uniqueSlug();
|
|
46573
46577
|
return path34.join(cacheRootDir, slug || source, versionSlug);
|
|
@@ -46588,7 +46592,7 @@ async function downloadArchive(url, destination) {
|
|
|
46588
46592
|
}
|
|
46589
46593
|
async function computeFileHash(filePath) {
|
|
46590
46594
|
const data = fs27.readFileSync(filePath);
|
|
46591
|
-
const hash =
|
|
46595
|
+
const hash = createHash5("sha256").update(data).digest("hex");
|
|
46592
46596
|
return `sha256:${hash}`;
|
|
46593
46597
|
}
|
|
46594
46598
|
function isDirectory(target) {
|
|
@@ -46763,7 +46767,7 @@ function redactUrlUserinfo(text) {
|
|
|
46763
46767
|
return text.replace(/\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^\s/@]+)@/g, "$1[REDACTED]@");
|
|
46764
46768
|
}
|
|
46765
46769
|
// src/sources/providers/git-provider.ts
|
|
46766
|
-
import { createHash as
|
|
46770
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
46767
46771
|
import fs30 from "node:fs";
|
|
46768
46772
|
import path36 from "node:path";
|
|
46769
46773
|
|
|
@@ -46824,7 +46828,7 @@ function resolveGitContentDir(config) {
|
|
|
46824
46828
|
throw new ConfigError("git source entry must have either `path` or `url`");
|
|
46825
46829
|
}
|
|
46826
46830
|
function getCachePaths(repoUrl, cacheRootOverride) {
|
|
46827
|
-
const key =
|
|
46831
|
+
const key = createHash6("sha256").update(repoUrl).digest("hex").slice(0, 16);
|
|
46828
46832
|
const cacheRoot = cacheRootOverride ?? getRegistryIndexCacheDir();
|
|
46829
46833
|
const rootDir = path36.join(cacheRoot, `git-${key}`);
|
|
46830
46834
|
return {
|
|
@@ -47045,7 +47049,7 @@ import fs33 from "node:fs";
|
|
|
47045
47049
|
import path39 from "node:path";
|
|
47046
47050
|
|
|
47047
47051
|
// src/sources/providers/tar-utils.ts
|
|
47048
|
-
import { createHash as
|
|
47052
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
47049
47053
|
import fs32 from "node:fs";
|
|
47050
47054
|
import path38 from "node:path";
|
|
47051
47055
|
function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
@@ -47058,7 +47062,7 @@ function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
|
47058
47062
|
const dashIndex = expected.indexOf("-");
|
|
47059
47063
|
const algorithm = expected.slice(0, dashIndex);
|
|
47060
47064
|
const expectedBase64 = expected.slice(dashIndex + 1);
|
|
47061
|
-
const actualBase64 =
|
|
47065
|
+
const actualBase64 = createHash7(algorithm).update(fileBuffer).digest("base64");
|
|
47062
47066
|
if (actualBase64 !== expectedBase64) {
|
|
47063
47067
|
fs32.unlinkSync(archivePath);
|
|
47064
47068
|
throw new Error(`Integrity check failed for ${archivePath}: expected ${algorithm} digest ${expectedBase64}, got ${actualBase64}`);
|
|
@@ -47066,7 +47070,7 @@ function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
|
47066
47070
|
return;
|
|
47067
47071
|
}
|
|
47068
47072
|
if (/^[0-9a-f]{40}$/i.test(expected)) {
|
|
47069
|
-
const actualHex =
|
|
47073
|
+
const actualHex = createHash7("sha1").update(fileBuffer).digest("hex");
|
|
47070
47074
|
if (actualHex.toLowerCase() !== expected.toLowerCase()) {
|
|
47071
47075
|
fs32.unlinkSync(archivePath);
|
|
47072
47076
|
throw new Error(`Integrity check failed for ${archivePath}: expected sha1 ${expected}, got ${actualHex}`);
|
|
@@ -51669,14 +51673,15 @@ class ClaudeHarness extends BaseHarness {
|
|
|
51669
51673
|
}
|
|
51670
51674
|
|
|
51671
51675
|
// src/integrations/harnesses/codex/agent-builder.ts
|
|
51672
|
-
import {
|
|
51673
|
-
import { tmpdir } from "node:os";
|
|
51676
|
+
import { mkdirSync } from "node:fs";
|
|
51674
51677
|
import { join } from "node:path";
|
|
51675
51678
|
function writeCodexOutputSchemaFile(schema) {
|
|
51676
|
-
const
|
|
51677
|
-
|
|
51678
|
-
|
|
51679
|
-
|
|
51679
|
+
const text = `${JSON.stringify(schema, null, 2)}
|
|
51680
|
+
`;
|
|
51681
|
+
const dir = join(getCacheDir(), "codex-output-schemas");
|
|
51682
|
+
mkdirSync(dir, { recursive: true });
|
|
51683
|
+
const file = join(dir, `${sha256Hex(text)}.json`);
|
|
51684
|
+
writeFileAtomic(file, text);
|
|
51680
51685
|
return file;
|
|
51681
51686
|
}
|
|
51682
51687
|
function ensureSandboxFlags(base) {
|
|
@@ -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
|
}
|
|
@@ -36177,7 +36181,7 @@ function withConfigLock(fn) {
|
|
|
36177
36181
|
}
|
|
36178
36182
|
|
|
36179
36183
|
// src/core/config/config-sources.ts
|
|
36180
|
-
import { createHash } from "crypto";
|
|
36184
|
+
import { createHash as createHash2 } from "crypto";
|
|
36181
36185
|
import fs9 from "fs";
|
|
36182
36186
|
import path13 from "path";
|
|
36183
36187
|
function bundleComponentConfig(bundle) {
|
|
@@ -36240,7 +36244,7 @@ function filesystemBundleSourceId(contentRoot) {
|
|
|
36240
36244
|
});
|
|
36241
36245
|
}
|
|
36242
36246
|
function hashBundleSourceIdentity(identity3) {
|
|
36243
|
-
return `sha256:${
|
|
36247
|
+
return `sha256:${createHash2("sha256").update("akm.bundle-source\x00v1\x00").update(JSON.stringify(identity3)).digest("hex")}`;
|
|
36244
36248
|
}
|
|
36245
36249
|
function bundleContentRoots(config) {
|
|
36246
36250
|
const bundles = config.bundles ?? {};
|
|
@@ -37702,7 +37706,7 @@ async function tryFetchJson(url, headers) {
|
|
|
37702
37706
|
}
|
|
37703
37707
|
|
|
37704
37708
|
// src/sources/providers/provider-utils.ts
|
|
37705
|
-
import { createHash as
|
|
37709
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
|
|
37706
37710
|
import fs27 from "fs";
|
|
37707
37711
|
import path34 from "path";
|
|
37708
37712
|
|
|
@@ -37797,7 +37801,7 @@ function countLines(text) {
|
|
|
37797
37801
|
}
|
|
37798
37802
|
|
|
37799
37803
|
// src/core/adapter/adapters/shared.ts
|
|
37800
|
-
import { createHash as
|
|
37804
|
+
import { createHash as createHash3 } from "crypto";
|
|
37801
37805
|
|
|
37802
37806
|
// src/core/asset/frontmatter-lint.ts
|
|
37803
37807
|
function checkUnquotedDescriptionColon(frontmatterText) {
|
|
@@ -37819,7 +37823,7 @@ function checkUnquotedDescriptionColon(frontmatterText) {
|
|
|
37819
37823
|
|
|
37820
37824
|
// src/core/adapter/adapters/shared.ts
|
|
37821
37825
|
function hashContent(content) {
|
|
37822
|
-
return
|
|
37826
|
+
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
37823
37827
|
}
|
|
37824
37828
|
function nonEmptyString2(value) {
|
|
37825
37829
|
if (typeof value !== "string")
|
|
@@ -39002,7 +39006,7 @@ function extractDirTagsFromName(name) {
|
|
|
39002
39006
|
}
|
|
39003
39007
|
|
|
39004
39008
|
// src/core/adapter/execution-source.ts
|
|
39005
|
-
import { createHash as
|
|
39009
|
+
import { createHash as createHash4 } from "crypto";
|
|
39006
39010
|
|
|
39007
39011
|
// src/execution/source.ts
|
|
39008
39012
|
var EXECUTION_SOURCE_SCHEMA_VERSION = 1;
|
|
@@ -39340,7 +39344,7 @@ function renderMarkdownExecutionSource(input) {
|
|
|
39340
39344
|
bundle: identity3.bundle,
|
|
39341
39345
|
adapter: identity3.adapter,
|
|
39342
39346
|
file: identity3.file,
|
|
39343
|
-
hash:
|
|
39347
|
+
hash: createHash4("sha256").update(raw, "utf8").digest("hex")
|
|
39344
39348
|
},
|
|
39345
39349
|
...hasDefaults ? { defaults } : {},
|
|
39346
39350
|
...extensions === undefined ? {} : { extensions }
|
|
@@ -45895,7 +45899,7 @@ function uniqueSlug() {
|
|
|
45895
45899
|
}
|
|
45896
45900
|
function buildInstallCacheDir(cacheRootDir, source, id, version) {
|
|
45897
45901
|
const safeId = id.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
45898
|
-
const identitySuffix = version === "writable" ? `-${
|
|
45902
|
+
const identitySuffix = version === "writable" ? `-${createHash5("sha256").update(`${source}:${id}`).digest("hex").slice(0, 12)}` : "";
|
|
45899
45903
|
const slug = `${source}-${safeId}${identitySuffix}`;
|
|
45900
45904
|
const versionSlug = source === "local" ? uniqueSlug() : version?.replace(/[^a-zA-Z0-9_.-]+/g, "-") ?? uniqueSlug();
|
|
45901
45905
|
return path34.join(cacheRootDir, slug || source, versionSlug);
|
|
@@ -45916,7 +45920,7 @@ async function downloadArchive(url, destination) {
|
|
|
45916
45920
|
}
|
|
45917
45921
|
async function computeFileHash(filePath) {
|
|
45918
45922
|
const data = fs27.readFileSync(filePath);
|
|
45919
|
-
const hash =
|
|
45923
|
+
const hash = createHash5("sha256").update(data).digest("hex");
|
|
45920
45924
|
return `sha256:${hash}`;
|
|
45921
45925
|
}
|
|
45922
45926
|
function isDirectory(target) {
|
|
@@ -46091,7 +46095,7 @@ function redactUrlUserinfo(text) {
|
|
|
46091
46095
|
return text.replace(/\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^\s/@]+)@/g, "$1[REDACTED]@");
|
|
46092
46096
|
}
|
|
46093
46097
|
// src/sources/providers/git-provider.ts
|
|
46094
|
-
import { createHash as
|
|
46098
|
+
import { createHash as createHash6 } from "crypto";
|
|
46095
46099
|
import fs30 from "fs";
|
|
46096
46100
|
import path36 from "path";
|
|
46097
46101
|
|
|
@@ -46152,7 +46156,7 @@ function resolveGitContentDir(config) {
|
|
|
46152
46156
|
throw new ConfigError("git source entry must have either `path` or `url`");
|
|
46153
46157
|
}
|
|
46154
46158
|
function getCachePaths(repoUrl, cacheRootOverride) {
|
|
46155
|
-
const key =
|
|
46159
|
+
const key = createHash6("sha256").update(repoUrl).digest("hex").slice(0, 16);
|
|
46156
46160
|
const cacheRoot = cacheRootOverride ?? getRegistryIndexCacheDir();
|
|
46157
46161
|
const rootDir = path36.join(cacheRoot, `git-${key}`);
|
|
46158
46162
|
return {
|
|
@@ -46373,7 +46377,7 @@ import fs33 from "fs";
|
|
|
46373
46377
|
import path39 from "path";
|
|
46374
46378
|
|
|
46375
46379
|
// src/sources/providers/tar-utils.ts
|
|
46376
|
-
import { createHash as
|
|
46380
|
+
import { createHash as createHash7 } from "crypto";
|
|
46377
46381
|
import fs32 from "fs";
|
|
46378
46382
|
import path38 from "path";
|
|
46379
46383
|
function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
@@ -46386,7 +46390,7 @@ function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
|
46386
46390
|
const dashIndex = expected.indexOf("-");
|
|
46387
46391
|
const algorithm = expected.slice(0, dashIndex);
|
|
46388
46392
|
const expectedBase64 = expected.slice(dashIndex + 1);
|
|
46389
|
-
const actualBase64 =
|
|
46393
|
+
const actualBase64 = createHash7(algorithm).update(fileBuffer).digest("base64");
|
|
46390
46394
|
if (actualBase64 !== expectedBase64) {
|
|
46391
46395
|
fs32.unlinkSync(archivePath);
|
|
46392
46396
|
throw new Error(`Integrity check failed for ${archivePath}: expected ${algorithm} digest ${expectedBase64}, got ${actualBase64}`);
|
|
@@ -46394,7 +46398,7 @@ function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
|
46394
46398
|
return;
|
|
46395
46399
|
}
|
|
46396
46400
|
if (/^[0-9a-f]{40}$/i.test(expected)) {
|
|
46397
|
-
const actualHex =
|
|
46401
|
+
const actualHex = createHash7("sha1").update(fileBuffer).digest("hex");
|
|
46398
46402
|
if (actualHex.toLowerCase() !== expected.toLowerCase()) {
|
|
46399
46403
|
fs32.unlinkSync(archivePath);
|
|
46400
46404
|
throw new Error(`Integrity check failed for ${archivePath}: expected sha1 ${expected}, got ${actualHex}`);
|
|
@@ -51664,14 +51668,15 @@ class ClaudeHarness extends BaseHarness {
|
|
|
51664
51668
|
}
|
|
51665
51669
|
|
|
51666
51670
|
// src/integrations/harnesses/codex/agent-builder.ts
|
|
51667
|
-
import {
|
|
51668
|
-
import { tmpdir } from "os";
|
|
51671
|
+
import { mkdirSync } from "fs";
|
|
51669
51672
|
import { join as join2 } from "path";
|
|
51670
51673
|
function writeCodexOutputSchemaFile(schema) {
|
|
51671
|
-
const
|
|
51672
|
-
|
|
51673
|
-
|
|
51674
|
-
|
|
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);
|
|
51675
51680
|
return file;
|
|
51676
51681
|
}
|
|
51677
51682
|
function ensureSandboxFlags(base) {
|
|
@@ -50,11 +50,14 @@ export const CONSOLIDATE_LEDGER_SOURCE = "consolidate";
|
|
|
50
50
|
* promotion is a verdict on that memory's text; asking the model about the
|
|
51
51
|
* same text again can only reproduce the proposal (accepted used to be
|
|
52
52
|
* eligible at once, rejected after 7 days), so the memory waits for an edit.
|
|
53
|
+
* So does a memory the model judged and left alone (`judged_no_action`):
|
|
54
|
+
* the same text would be judged weekly with the same answer.
|
|
53
55
|
* The clock stays for a row with no recorded hash — one decided before the
|
|
54
56
|
* hash was recorded — see {@link nextEligibleAt} and {@link isContentDrivenRow}.
|
|
55
57
|
*/
|
|
56
58
|
export function isContentDrivenDecision(source, outcome) {
|
|
57
|
-
return source === CONSOLIDATE_LEDGER_SOURCE &&
|
|
59
|
+
return (source === CONSOLIDATE_LEDGER_SOURCE &&
|
|
60
|
+
(outcome === "accepted" || outcome === "rejected" || outcome === "judged_no_action"));
|
|
58
61
|
}
|
|
59
62
|
/**
|
|
60
63
|
* Whether this row is held by its content hash: a decided consolidate
|
|
@@ -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/README.md
CHANGED
|
@@ -49,7 +49,6 @@ Working on akm itself, not just using it.
|
|
|
49
49
|
|
|
50
50
|
- [Maintainer Docs](https://github.com/itlackey/akm/blob/main/docs/maintainers/README.md) -- Start here: local development, measuring improvement, and the curate contract
|
|
51
51
|
- [Local Development](https://github.com/itlackey/akm/blob/main/docs/maintainers/local-development.md) -- Dogfooding akm while editing its own source
|
|
52
|
-
- [akm-eval](https://github.com/itlackey/akm/blob/main/docs/maintainers/eval.md) -- Standalone toolkit for measuring whether `akm improve` is working
|
|
53
52
|
- [Curate Workmap](https://github.com/itlackey/akm/blob/main/docs/maintainers/curate-workmap.md) -- The current `akm curate` contract and the highest-value next fixes
|
|
54
53
|
|
|
55
54
|
## Look up details
|
|
@@ -100,7 +99,7 @@ Source articles for the dev.to publishing pipeline (historical record). See
|
|
|
100
99
|
- [itlackey/akm-registry](https://github.com/itlackey/akm-registry) -- the official registry index that powers built-in discovery
|
|
101
100
|
- [itlackey/akm-plugins](https://github.com/itlackey/akm-plugins) -- optional integrations for tools like OpenCode
|
|
102
101
|
- [itlackey/akm-bench](https://github.com/itlackey/akm-bench) -- the standalone benchmark harness for measuring agent performance with akm
|
|
103
|
-
- [itlackey/akm-eval](https://github.com/itlackey/akm-eval) -- the eval framework and tools for akm asset quality
|
|
102
|
+
- [itlackey/akm-eval](https://github.com/itlackey/akm-eval) -- the eval framework and tools for akm asset quality
|
|
104
103
|
|
|
105
104
|
---
|
|
106
105
|
|