akm-cli 0.9.28-alpha.10 → 0.9.28-alpha.11
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 +23 -0
- package/dist/commands/improve/consolidate/coverage.js +1 -1
- package/dist/commands/proposal/drain.js +29 -4
- package/dist/commands/proposal/proposal-cli.js +7 -8
- package/dist/commands/sources/plugin-upgrade.js +26 -4
- package/dist/core/adapter/adapters/akm-adapter.js +2 -1
- package/dist/indexer/walk/matchers.js +4 -2
- package/dist/scripts/akm-migrate-node.js +2 -2
- package/dist/scripts/akm-migrate.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.28-alpha.11] - 2026-10-08
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Skill resources that document `$ARGUMENTS` command templates no longer index as commands.** The command
|
|
14
|
+
placeholder matcher now respects a skill folder as declared context, and the indexer re-files existing entries
|
|
15
|
+
on the next incremental index (#1084).
|
|
16
|
+
- **A proposal drain judge now sees overlapping promotions accepted earlier in the same drain (#1085).**
|
|
17
|
+
- **`akm proposal drain --strategy <name>` now honors enabled triage judgment.** A named strategy with
|
|
18
|
+
`processes.triage.judgment.enabled: true` runs its configured judge without `--judgment`; the flag still
|
|
19
|
+
forces judgment for disabled strategies, and drains without `--strategy` keep their existing behavior (#1086).
|
|
20
|
+
- **`akm upgrade` no longer defers the OpenCode plugin refresh for OpenCode servers running in containers
|
|
21
|
+
(#1099).** The "is OpenCode running" check counted an `opencode serve` inside a Docker container, which
|
|
22
|
+
keeps its own cache, so the host's refresh was deferred for as long as the container ran. On Linux a matching
|
|
23
|
+
process now counts only when it shares akm's mount namespace (or, where that is unreadable, when its cgroup
|
|
24
|
+
is not a docker/containerd/podman/lxc scope); a process that cannot be inspected still defers.
|
|
25
|
+
- **Five test suites no longer leak their temp folders on a direct `bun test` (#1088).** The outcome-loop,
|
|
26
|
+
outcome-invariance, memory-improve-archive, plan-flag-cli and require-engines-cli suites created `akm-*`
|
|
27
|
+
directories under the OS temp dir and never removed them; `scripts/sweep-test-tmp.ts` swept them only for
|
|
28
|
+
`test-unit.sh`/`test-integration.sh` runs, so running a file straight with `bun test` left a folder per test
|
|
29
|
+
behind. Each suite now registers its temp dirs with the shared test sandbox helper (`makeSandboxDir`, drained
|
|
30
|
+
by an `afterEach`), the pattern the other suites already use.
|
|
31
|
+
|
|
9
32
|
## [0.9.28-alpha.10] - 2026-10-08
|
|
10
33
|
|
|
11
34
|
### Fixed
|
|
@@ -125,7 +125,7 @@ export function findCoveringKnowledge(db, bundleId, filePath, body) {
|
|
|
125
125
|
}
|
|
126
126
|
/** Knowledge docs a reviewer is shown for a promotion, nearest first. */
|
|
127
127
|
export const NEIGHBOUR_NOTE_COUNT = 5;
|
|
128
|
-
const NEIGHBOUR_EXCERPT_CHARS = 300;
|
|
128
|
+
export const NEIGHBOUR_EXCERPT_CHARS = 300;
|
|
129
129
|
/**
|
|
130
130
|
* The knowledge notes nearest to the memory at `memoryPath`, for the drain's
|
|
131
131
|
* judge to compare a promotion against: the nearest {@link NEIGHBOUR_NOTE_COUNT}
|
|
@@ -28,7 +28,7 @@ import { info, warn } from "../../core/warn.js";
|
|
|
28
28
|
import { DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js";
|
|
29
29
|
import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
|
|
30
30
|
import { assertRunnerCredentials, runExecution, } from "../../integrations/agent/runner-dispatch.js";
|
|
31
|
-
import { nearestKnowledgeNotes } from "../improve/consolidate/coverage.js";
|
|
31
|
+
import { NEIGHBOUR_EXCERPT_CHARS, NEIGHBOUR_NOTE_COUNT, nearestKnowledgeNotes, shingleContainment, wordShingles, } from "../improve/consolidate/coverage.js";
|
|
32
32
|
import { errMessage, noticeSet } from "../improve/stage.js";
|
|
33
33
|
import { akmProposalAccept, akmProposalReject } from "./proposal.js";
|
|
34
34
|
import { isRetireProposal, PAIR_PASS_GATE, STALE_TARGET_GATE_REASON } from "./proposal-types.js";
|
|
@@ -242,6 +242,7 @@ async function runJudgmentTier(opts, result, pending, acceptBudget, promoteFn, r
|
|
|
242
242
|
const byId = new Map(pending.map((p) => [p.id, p]));
|
|
243
243
|
const notices = noticeSet();
|
|
244
244
|
const stillDeferred = [];
|
|
245
|
+
const acceptedPromotions = [];
|
|
245
246
|
const cappedBefore = result.skippedByCap.length;
|
|
246
247
|
for (const item of result.deferred) {
|
|
247
248
|
const proposal = byId.get(item.id);
|
|
@@ -254,7 +255,9 @@ async function runJudgmentTier(opts, result, pending, acceptBudget, promoteFn, r
|
|
|
254
255
|
liveAsset,
|
|
255
256
|
siblings: pending.filter((p) => p.ref === proposal.ref && p.id !== proposal.id),
|
|
256
257
|
// A create the model otherwise judges blind: the model never sees knowledge/.
|
|
257
|
-
...(liveAsset === undefined
|
|
258
|
+
...(liveAsset === undefined
|
|
259
|
+
? { neighbours: promotionNeighbours(opts.stashDir, proposal, acceptedPromotions) }
|
|
260
|
+
: {}),
|
|
258
261
|
});
|
|
259
262
|
const dispatch = await dispatchJudgment(opts.judgment, prompt, seams);
|
|
260
263
|
notices.add(dispatch.notices);
|
|
@@ -314,6 +317,8 @@ async function runJudgmentTier(opts, result, pending, acceptBudget, promoteFn, r
|
|
|
314
317
|
const outcome = await acceptProposal(opts, proposal, item.id, "judgment-accept", promoteFn, rejectFn, verdict.reason);
|
|
315
318
|
if (outcome === "promoted") {
|
|
316
319
|
result.promoted.push(item.id);
|
|
320
|
+
if (proposal.source === "consolidate" && proposal.promotionSource !== undefined)
|
|
321
|
+
acceptedPromotions.push(proposal);
|
|
317
322
|
acceptBudget -= 1;
|
|
318
323
|
}
|
|
319
324
|
else if (outcome === "rejected") {
|
|
@@ -333,7 +338,7 @@ async function runJudgmentTier(opts, result, pending, acceptBudget, promoteFn, r
|
|
|
333
338
|
result.deferred = stillDeferred;
|
|
334
339
|
}
|
|
335
340
|
/** The knowledge notes nearest to a consolidate promotion's source memory; none for any other proposal. */
|
|
336
|
-
function promotionNeighbours(stashDir, proposal) {
|
|
341
|
+
function promotionNeighbours(stashDir, proposal, acceptedPromotions) {
|
|
337
342
|
if (proposal.source !== "consolidate" || proposal.promotionSource === undefined)
|
|
338
343
|
return [];
|
|
339
344
|
try {
|
|
@@ -341,7 +346,27 @@ function promotionNeighbours(stashDir, proposal) {
|
|
|
341
346
|
const typeDir = stashDirFor(parsed.type);
|
|
342
347
|
if (!typeDir)
|
|
343
348
|
return [];
|
|
344
|
-
|
|
349
|
+
const memoryPath = assetPathForName(parsed.type, path.join(stashDir, typeDir), parsed.name);
|
|
350
|
+
const existing = nearestKnowledgeNotes(memoryPath);
|
|
351
|
+
const source = fs.readFileSync(memoryPath, "utf8");
|
|
352
|
+
const shingles = wordShingles(source);
|
|
353
|
+
// Promotions accepted earlier in this drain are not in the index yet; the
|
|
354
|
+
// ones sharing text with this memory join its neighbours (#1085).
|
|
355
|
+
const inDrain = acceptedPromotions
|
|
356
|
+
.map((accepted) => {
|
|
357
|
+
const parsedContent = parseFrontmatter(proposalContent(accepted));
|
|
358
|
+
const body = parsedContent.content;
|
|
359
|
+
return {
|
|
360
|
+
ref: accepted.ref,
|
|
361
|
+
description: typeof parsedContent.data.description === "string" ? parsedContent.data.description : "",
|
|
362
|
+
excerpt: body.length > NEIGHBOUR_EXCERPT_CHARS ? `${body.slice(0, NEIGHBOUR_EXCERPT_CHARS)}...` : body,
|
|
363
|
+
containment: shingleContainment(shingles, body),
|
|
364
|
+
};
|
|
365
|
+
})
|
|
366
|
+
.filter((note) => note.containment > 0)
|
|
367
|
+
.sort((a, b) => b.containment - a.containment)
|
|
368
|
+
.map(({ containment: _, ...note }) => note);
|
|
369
|
+
return [...inDrain, ...existing].slice(0, NEIGHBOUR_NOTE_COUNT);
|
|
345
370
|
}
|
|
346
371
|
catch {
|
|
347
372
|
return [];
|
|
@@ -401,12 +401,12 @@ const proposalDrainCommand = defineJsonCommand({
|
|
|
401
401
|
},
|
|
402
402
|
judgment: {
|
|
403
403
|
type: "boolean",
|
|
404
|
-
description: "
|
|
404
|
+
description: "Enable the judgment tier for this drain (overrides judgment.enabled=false; agent/sdk per config). No-op with a logged triage_deferred summary when no runner is configured.",
|
|
405
405
|
default: false,
|
|
406
406
|
},
|
|
407
407
|
strategy: {
|
|
408
408
|
type: "string",
|
|
409
|
-
description: "Read the triage block (applyMode, ceilings, judgment) from this improve strategy.",
|
|
409
|
+
description: "Read the triage block (applyMode, ceilings, judgment) from this improve strategy; enabled judgment runs the judge.",
|
|
410
410
|
},
|
|
411
411
|
},
|
|
412
412
|
async run({ args, rawArgs }) {
|
|
@@ -455,12 +455,11 @@ const proposalDrainCommand = defineJsonCommand({
|
|
|
455
455
|
})
|
|
456
456
|
.map((proposal) => proposal.id));
|
|
457
457
|
}
|
|
458
|
-
// Phase 3: --judgment is an invocation-level opt-in
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
const judgmentResolution = args.judgment === true
|
|
458
|
+
// Phase 3: --judgment is an invocation-level opt-in that overrides a
|
|
459
|
+
// disabled block. An explicitly selected strategy may opt in through its
|
|
460
|
+
// judgment block; the implicit default strategy keeps standalone drain's
|
|
461
|
+
// historical no-judge behavior.
|
|
462
|
+
const judgmentResolution = args.judgment === true || (args.strategy !== undefined && triageConfig?.judgment?.enabled === true)
|
|
464
463
|
? resolveImproveExecution({
|
|
465
464
|
config: cfg,
|
|
466
465
|
profile: selectedStrategy.config,
|
|
@@ -237,21 +237,43 @@ export function lookupOpenCodeNext() {
|
|
|
237
237
|
}
|
|
238
238
|
return next;
|
|
239
239
|
}
|
|
240
|
+
const CONTAINER_CGROUP = /docker|containerd|podman|libpod|lxc/;
|
|
241
|
+
/**
|
|
242
|
+
* Whether the process could use this host's OpenCode cache. A process in another mount namespace (a container's
|
|
243
|
+
* `opencode serve`) has its own cache; when that cannot be read, a container cgroup says the same. Anything
|
|
244
|
+
* unreadable counts as a host process, so the answer errs towards deferring.
|
|
245
|
+
*/
|
|
246
|
+
export function usesHostCache(pid, procRoot = "/proc") {
|
|
247
|
+
try {
|
|
248
|
+
return fs.readlinkSync(`${procRoot}/${pid}/ns/mnt`) === fs.readlinkSync(`${procRoot}/self/ns/mnt`);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// not readable (another user's process, or no such namespace file): look at the cgroup instead
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
return !CONTAINER_CGROUP.test(fs.readFileSync(`${procRoot}/${pid}/cgroup`, "utf8"));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
240
260
|
/** Whether an OpenCode process is running (its prefetch would be replaced under it). `undefined` when that cannot be told. */
|
|
241
261
|
function openCodeRunning() {
|
|
242
262
|
if (IS_WINDOWS) {
|
|
243
263
|
const tasks = runCommand("tasklist", ["/FI", "IMAGENAME eq opencode.exe", "/NH"], READ_TIMEOUT_MS);
|
|
244
264
|
return tasks.ok ? /opencode\.exe/i.test(tasks.stdout) : undefined;
|
|
245
265
|
}
|
|
266
|
+
const linux = process.platform === "linux";
|
|
246
267
|
// `-f` because a Node-wrapped opencode shows up as `node …/opencode`; the
|
|
247
268
|
// pattern needs `opencode` to be a whole path segment so `akm-opencode`
|
|
248
269
|
// in some other command's arguments does not match.
|
|
249
270
|
const pgrep = runCommand("pgrep", ["-f", "(^|/)opencode( |$)"], READ_TIMEOUT_MS);
|
|
250
|
-
if (pgrep.ok)
|
|
251
|
-
return
|
|
271
|
+
if (pgrep.ok) {
|
|
272
|
+
return !linux || pgrep.stdout.split(/\s+/).some((pid) => /^\d+$/.test(pid) && usesHostCache(pid));
|
|
273
|
+
}
|
|
252
274
|
if (!pgrep.missing)
|
|
253
275
|
return false; // pgrep exits 1 when nothing matched
|
|
254
|
-
if (
|
|
276
|
+
if (!linux)
|
|
255
277
|
return undefined;
|
|
256
278
|
try {
|
|
257
279
|
return fs.readdirSync("/proc").some((pid) => {
|
|
@@ -259,7 +281,7 @@ function openCodeRunning() {
|
|
|
259
281
|
return false;
|
|
260
282
|
try {
|
|
261
283
|
const argv = fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0");
|
|
262
|
-
return argv.slice(0, 2).some((arg) => path.basename(arg) === "opencode");
|
|
284
|
+
return argv.slice(0, 2).some((arg) => path.basename(arg) === "opencode") && usesHostCache(pid);
|
|
263
285
|
}
|
|
264
286
|
catch {
|
|
265
287
|
return false;
|
|
@@ -457,7 +457,8 @@ export const akmAdapter = {
|
|
|
457
457
|
// file that has not changed: the version is folded into each directory's
|
|
458
458
|
// freshness, so the next incremental `akm index` re-drains it and re-files
|
|
459
459
|
// those rows (#1063). 0.9.1: a skill's resource is never retyped by a `$1`.
|
|
460
|
-
|
|
460
|
+
// 0.9.2: a skill's resource is never retyped by a `$ARGUMENTS` (#1084).
|
|
461
|
+
version: "0.9.2",
|
|
461
462
|
// Recognized-extension HINT, derived from what the matchers accept (§6):
|
|
462
463
|
// `.md` (Markdown types + workflow peer), `.yml` (task and workflow YAML),
|
|
463
464
|
// `.yaml` (task near-miss diagnostics), `.env` (env files), and the 16
|
|
@@ -231,8 +231,10 @@ function classifyBySmartMd(ctx) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
// `$ARGUMENTS` is unambiguous, so it keeps its long-standing precedence: a
|
|
234
|
-
// command dropped under `knowledge/` is still found as a command.
|
|
235
|
-
|
|
234
|
+
// command dropped under `knowledge/` is still found as a command. A skill's
|
|
235
|
+
// folder is the one declared context it defers to — a file there is a skill
|
|
236
|
+
// resource, so documenting a command template must not retype it (#1084).
|
|
237
|
+
if (ARGUMENTS_PLACEHOLDER_RE.test(body) && !isNestedSkillResource(ctx)) {
|
|
236
238
|
return SMART_MD_FACTS.command;
|
|
237
239
|
}
|
|
238
240
|
// A NUMERIC placeholder is a guess, and a typed directory is a declaration.
|
|
@@ -40534,7 +40534,7 @@ function classifyBySmartMd(ctx) {
|
|
|
40534
40534
|
return SMART_MD_FACTS.command;
|
|
40535
40535
|
}
|
|
40536
40536
|
}
|
|
40537
|
-
if (ARGUMENTS_PLACEHOLDER_RE.test(body)) {
|
|
40537
|
+
if (ARGUMENTS_PLACEHOLDER_RE.test(body) && !isNestedSkillResource(ctx)) {
|
|
40538
40538
|
return SMART_MD_FACTS.command;
|
|
40539
40539
|
}
|
|
40540
40540
|
if (NUMERIC_PLACEHOLDER_RE.test(body) && !hasDeclaredDirType(ctx)) {
|
|
@@ -45095,7 +45095,7 @@ async function validate2(c, changes, ctx) {
|
|
|
45095
45095
|
}
|
|
45096
45096
|
var akmAdapter = {
|
|
45097
45097
|
id: "akm",
|
|
45098
|
-
version: "0.9.
|
|
45098
|
+
version: "0.9.2",
|
|
45099
45099
|
extensions: [
|
|
45100
45100
|
".md",
|
|
45101
45101
|
".yaml",
|
|
@@ -39862,7 +39862,7 @@ function classifyBySmartMd(ctx) {
|
|
|
39862
39862
|
return SMART_MD_FACTS.command;
|
|
39863
39863
|
}
|
|
39864
39864
|
}
|
|
39865
|
-
if (ARGUMENTS_PLACEHOLDER_RE.test(body)) {
|
|
39865
|
+
if (ARGUMENTS_PLACEHOLDER_RE.test(body) && !isNestedSkillResource(ctx)) {
|
|
39866
39866
|
return SMART_MD_FACTS.command;
|
|
39867
39867
|
}
|
|
39868
39868
|
if (NUMERIC_PLACEHOLDER_RE.test(body) && !hasDeclaredDirType(ctx)) {
|
|
@@ -44423,7 +44423,7 @@ async function validate2(c, changes, ctx) {
|
|
|
44423
44423
|
}
|
|
44424
44424
|
var akmAdapter = {
|
|
44425
44425
|
id: "akm",
|
|
44426
|
-
version: "0.9.
|
|
44426
|
+
version: "0.9.2",
|
|
44427
44427
|
extensions: [
|
|
44428
44428
|
".md",
|
|
44429
44429
|
".yaml",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.28-alpha.
|
|
3
|
+
"version": "0.9.28-alpha.11",
|
|
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": [
|