akm-cli 0.9.6 → 0.9.7
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 +190 -0
- package/dist/assets/hints/cli-hints-full.md +3 -3
- package/dist/assets/improve-strategies/catchup.json +40 -11
- package/dist/assets/improve-strategies/thorough.json +45 -7
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
- package/dist/commands/agent/contribute-cli.js +11 -0
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +0 -4
- package/dist/commands/improve/memory/memory-improve.js +2 -1
- package/dist/commands/improve/preparation.js +1 -1
- package/dist/commands/improve/reflect.js +1 -1
- package/dist/commands/lint/base-linter.js +141 -18
- package/dist/commands/lint/index.js +17 -4
- package/dist/commands/read/curate.js +47 -0
- package/dist/commands/read/search-cli.js +24 -1
- package/dist/core/asset/asset-placement.js +13 -2
- package/dist/core/asset/frontmatter.js +116 -0
- package/dist/core/asset/memory-archive.js +97 -0
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/scripts/akm-migrate-node.js +13 -7
- package/dist/scripts/akm-migrate.js +13 -7
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
- package/dist/storage/repositories/index-connection.js +45 -3
- package/dist/tasks/backends/cron.js +49 -9
- package/dist/tasks/resolve-akm-bin.js +17 -2
- package/dist/tasks/scheduler-invocation.js +8 -1
- package/dist/tasks/source/parse-task-source.js +23 -9
- package/docs/reference/cli.md +5 -1
- package/package.json +1 -1
- package/dist/assets/improve-strategies/frequent.json +0 -15
- package/dist/assets/improve-strategies/memory-focus.json +0 -15
|
@@ -11525,6 +11525,7 @@ var SCRIPT_EXTENSIONS = new Set([
|
|
|
11525
11525
|
".kts"
|
|
11526
11526
|
]);
|
|
11527
11527
|
var WORKFLOW_EXTENSIONS = [".md", ".yml"];
|
|
11528
|
+
var DERIVED_SUFFIX = ".derived";
|
|
11528
11529
|
var KNOWN_TYPES = [
|
|
11529
11530
|
"skill",
|
|
11530
11531
|
"command",
|
|
@@ -11685,6 +11686,9 @@ function assetPathForName(assetType, typeRoot, name) {
|
|
|
11685
11686
|
}
|
|
11686
11687
|
function assetPathCandidatesForName(assetType, typeRoot, name) {
|
|
11687
11688
|
const primary = assetPathForName(assetType, typeRoot, name);
|
|
11689
|
+
if (assetType === "memory" && !name.endsWith(DERIVED_SUFFIX)) {
|
|
11690
|
+
return [primary, assetPathForName(assetType, typeRoot, `${name}${DERIVED_SUFFIX}`)];
|
|
11691
|
+
}
|
|
11688
11692
|
if (assetType !== "env")
|
|
11689
11693
|
return [primary];
|
|
11690
11694
|
const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
|
|
@@ -17606,6 +17610,9 @@ function peekTaskSourceVersion(root) {
|
|
|
17606
17610
|
return typeof value === "number" ? value : undefined;
|
|
17607
17611
|
}
|
|
17608
17612
|
var TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
|
|
17613
|
+
function unmigratableVersionError(filePath, version, reason, detail) {
|
|
17614
|
+
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version} and needs a human decision before it can run — the deterministic migrator cannot convert it automatically (${reason}${detail ? `: ${detail}` : ""}).`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Review the file and resolve the ambiguity by hand, then it will convert normally; `akm migrate status` reports the same reason.");
|
|
17615
|
+
}
|
|
17609
17616
|
function unsupportedVersionError(filePath, version) {
|
|
17610
17617
|
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
|
|
17611
17618
|
}
|
|
@@ -17625,12 +17632,12 @@ function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
|
|
|
17625
17632
|
} else {
|
|
17626
17633
|
const v3Outcome = planTaskToV3File(baseInput);
|
|
17627
17634
|
if (v3Outcome.status !== "changed")
|
|
17628
|
-
return;
|
|
17635
|
+
return { reason: v3Outcome.reason, detail: v3Outcome.detail };
|
|
17629
17636
|
v3Bytes = v3Outcome.after;
|
|
17630
17637
|
}
|
|
17631
17638
|
const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
|
|
17632
17639
|
if (v4Outcome.status !== "changed")
|
|
17633
|
-
return;
|
|
17640
|
+
return { reason: v4Outcome.reason, detail: v4Outcome.detail };
|
|
17634
17641
|
return v4Outcome.after.toString("utf8");
|
|
17635
17642
|
}
|
|
17636
17643
|
function parseTaskSource(input) {
|
|
@@ -17638,16 +17645,17 @@ function parseTaskSource(input) {
|
|
|
17638
17645
|
const version = peekTaskSourceVersion(root);
|
|
17639
17646
|
if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
|
|
17640
17647
|
if (version === 2 || version === 3) {
|
|
17641
|
-
const
|
|
17642
|
-
if (
|
|
17648
|
+
const shimmed = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
|
|
17649
|
+
if (typeof shimmed === "string") {
|
|
17643
17650
|
const v4 = parseTaskSourceV4({
|
|
17644
|
-
yaml:
|
|
17651
|
+
yaml: shimmed,
|
|
17645
17652
|
filePath: input.filePath,
|
|
17646
17653
|
...input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}
|
|
17647
17654
|
});
|
|
17648
17655
|
warn(`akm: task ${input.filePath} uses schema v${version} — auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
|
|
17649
17656
|
return Object.freeze({ version: 4, v4 });
|
|
17650
17657
|
}
|
|
17658
|
+
throw unmigratableVersionError(input.filePath, version, shimmed.reason, shimmed.detail);
|
|
17651
17659
|
}
|
|
17652
17660
|
throw unsupportedVersionError(input.filePath, version);
|
|
17653
17661
|
}
|
|
@@ -27990,9 +27998,7 @@ var BUILTIN_IMPROVE_STRATEGY_NAMES = [
|
|
|
27990
27998
|
"default",
|
|
27991
27999
|
"quick",
|
|
27992
28000
|
"thorough",
|
|
27993
|
-
"memory-focus",
|
|
27994
28001
|
"graph-refresh",
|
|
27995
|
-
"frequent",
|
|
27996
28002
|
"consolidate",
|
|
27997
28003
|
"catchup",
|
|
27998
28004
|
"reflect-distill",
|
|
@@ -11524,6 +11524,7 @@ var SCRIPT_EXTENSIONS = new Set([
|
|
|
11524
11524
|
".kts"
|
|
11525
11525
|
]);
|
|
11526
11526
|
var WORKFLOW_EXTENSIONS = [".md", ".yml"];
|
|
11527
|
+
var DERIVED_SUFFIX = ".derived";
|
|
11527
11528
|
var KNOWN_TYPES = [
|
|
11528
11529
|
"skill",
|
|
11529
11530
|
"command",
|
|
@@ -11684,6 +11685,9 @@ function assetPathForName(assetType, typeRoot, name) {
|
|
|
11684
11685
|
}
|
|
11685
11686
|
function assetPathCandidatesForName(assetType, typeRoot, name) {
|
|
11686
11687
|
const primary = assetPathForName(assetType, typeRoot, name);
|
|
11688
|
+
if (assetType === "memory" && !name.endsWith(DERIVED_SUFFIX)) {
|
|
11689
|
+
return [primary, assetPathForName(assetType, typeRoot, `${name}${DERIVED_SUFFIX}`)];
|
|
11690
|
+
}
|
|
11687
11691
|
if (assetType !== "env")
|
|
11688
11692
|
return [primary];
|
|
11689
11693
|
const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
|
|
@@ -17528,6 +17532,9 @@ function peekTaskSourceVersion(root) {
|
|
|
17528
17532
|
return typeof value === "number" ? value : undefined;
|
|
17529
17533
|
}
|
|
17530
17534
|
var TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
|
|
17535
|
+
function unmigratableVersionError(filePath, version, reason, detail) {
|
|
17536
|
+
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version} and needs a human decision before it can run \u2014 the deterministic migrator cannot convert it automatically (${reason}${detail ? `: ${detail}` : ""}).`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Review the file and resolve the ambiguity by hand, then it will convert normally; `akm migrate status` reports the same reason.");
|
|
17537
|
+
}
|
|
17531
17538
|
function unsupportedVersionError(filePath, version) {
|
|
17532
17539
|
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
|
|
17533
17540
|
}
|
|
@@ -17547,12 +17554,12 @@ function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
|
|
|
17547
17554
|
} else {
|
|
17548
17555
|
const v3Outcome = planTaskToV3File(baseInput);
|
|
17549
17556
|
if (v3Outcome.status !== "changed")
|
|
17550
|
-
return;
|
|
17557
|
+
return { reason: v3Outcome.reason, detail: v3Outcome.detail };
|
|
17551
17558
|
v3Bytes = v3Outcome.after;
|
|
17552
17559
|
}
|
|
17553
17560
|
const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
|
|
17554
17561
|
if (v4Outcome.status !== "changed")
|
|
17555
|
-
return;
|
|
17562
|
+
return { reason: v4Outcome.reason, detail: v4Outcome.detail };
|
|
17556
17563
|
return v4Outcome.after.toString("utf8");
|
|
17557
17564
|
}
|
|
17558
17565
|
function parseTaskSource(input) {
|
|
@@ -17560,16 +17567,17 @@ function parseTaskSource(input) {
|
|
|
17560
17567
|
const version = peekTaskSourceVersion(root);
|
|
17561
17568
|
if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
|
|
17562
17569
|
if (version === 2 || version === 3) {
|
|
17563
|
-
const
|
|
17564
|
-
if (
|
|
17570
|
+
const shimmed = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
|
|
17571
|
+
if (typeof shimmed === "string") {
|
|
17565
17572
|
const v4 = parseTaskSourceV4({
|
|
17566
|
-
yaml:
|
|
17573
|
+
yaml: shimmed,
|
|
17567
17574
|
filePath: input.filePath,
|
|
17568
17575
|
...input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}
|
|
17569
17576
|
});
|
|
17570
17577
|
warn(`akm: task ${input.filePath} uses schema v${version} \u2014 auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
|
|
17571
17578
|
return Object.freeze({ version: 4, v4 });
|
|
17572
17579
|
}
|
|
17580
|
+
throw unmigratableVersionError(input.filePath, version, shimmed.reason, shimmed.detail);
|
|
17573
17581
|
}
|
|
17574
17582
|
throw unsupportedVersionError(input.filePath, version);
|
|
17575
17583
|
}
|
|
@@ -27912,9 +27920,7 @@ var BUILTIN_IMPROVE_STRATEGY_NAMES = [
|
|
|
27912
27920
|
"default",
|
|
27913
27921
|
"quick",
|
|
27914
27922
|
"thorough",
|
|
27915
|
-
"memory-focus",
|
|
27916
27923
|
"graph-refresh",
|
|
27917
|
-
"frequent",
|
|
27918
27924
|
"consolidate",
|
|
27919
27925
|
"catchup",
|
|
27920
27926
|
"reflect-distill",
|
|
@@ -23,6 +23,9 @@ const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
23
23
|
const QUEUE_EXPANSION_FACTOR = 5;
|
|
24
24
|
const MAX_PAGES_DEFAULT = 50;
|
|
25
25
|
const MAX_DEPTH_DEFAULT = 3;
|
|
26
|
+
/** Byte cap for the `llms.txt` manifest itself — a curated link list, never a large file. */
|
|
27
|
+
const LLMS_TXT_BYTE_CAP = 512 * 1024;
|
|
28
|
+
const LLMS_TXT_BODY_TIMEOUT_MS = 15_000;
|
|
26
29
|
/**
|
|
27
30
|
* Per-page body cap for website scraping. HTML pages this large are
|
|
28
31
|
* almost never useful as agent knowledge sources and a runaway server
|
|
@@ -531,6 +534,38 @@ async function crawlWebsite(startUrl, options) {
|
|
|
531
534
|
? createAllowAllRobotsPolicy()
|
|
532
535
|
: createRobotsPolicy((robotsUrl) => loadRobotsTxt(robotsUrl, { allowPrivateHosts: options.allowPrivateHosts, signal: crawlSignal }));
|
|
533
536
|
await assertStartUrlAllowedByRobots(robots, start, options.rawStartUrl);
|
|
537
|
+
// llms.txt fast path: an increasing number of doc sites publish a curated,
|
|
538
|
+
// deduplicated link list at `/llms.txt` specifically for tools like this
|
|
539
|
+
// one. When present, use it as the crawl frontier instead of discovering
|
|
540
|
+
// links by parsing HTML — each linked page still goes through the exact
|
|
541
|
+
// same robots-compliant, host-guarded `fetchWebsitePage` call below, so
|
|
542
|
+
// ingested pages stay individually addressable. Gated to origin-root start
|
|
543
|
+
// URLs only (mirrors `extractGithubRepository`'s repo-root restriction):
|
|
544
|
+
// adding a specific page must fetch that page, not silently pull in the
|
|
545
|
+
// whole site's manifest.
|
|
546
|
+
if (isOriginRootUrl(start)) {
|
|
547
|
+
const manifest = await fetchLlmsManifest(start, robots, {
|
|
548
|
+
allowPrivateHosts: options.allowPrivateHosts,
|
|
549
|
+
signal: crawlSignal,
|
|
550
|
+
});
|
|
551
|
+
if (manifest) {
|
|
552
|
+
warn("[akm] Using llms.txt manifest from %s", manifest.manifestUrl);
|
|
553
|
+
queue.length = 0;
|
|
554
|
+
for (const link of manifest.links) {
|
|
555
|
+
// A manifest can name arbitrary hosts; only same-origin links are
|
|
556
|
+
// honored by default, same as links discovered mid-crawl below.
|
|
557
|
+
if (link.origin !== allowedOrigin)
|
|
558
|
+
continue;
|
|
559
|
+
const candidate = normalizeCrawlUrl(link.toString());
|
|
560
|
+
if (!candidate)
|
|
561
|
+
continue;
|
|
562
|
+
// depth = maxDepth: fetch each manifest page individually, but don't
|
|
563
|
+
// treat it as a fresh BFS seed — the manifest is already the
|
|
564
|
+
// author-curated set of pages worth ingesting.
|
|
565
|
+
queue.push({ url: candidate, rawUrl: link.toString(), depth: options.maxDepth, deferrals: 0 });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
534
569
|
// Counts actual `fetchWebsitePage` invocations (regardless of outcome) so
|
|
535
570
|
// Crawl-delay pacing skips the first fetch and never charges a delay slot
|
|
536
571
|
// to a URL that robots.txt skipped without ever being fetched (C-11).
|
|
@@ -870,6 +905,97 @@ function buildMarkdownSnapshot(page, slug, tags) {
|
|
|
870
905
|
"",
|
|
871
906
|
].join("\n");
|
|
872
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* True for a start URL that names an origin's root (no path, no query).
|
|
910
|
+
* Matches how `extractGithubRepository` restricts its own special-case match
|
|
911
|
+
* to repository-root URLs — the llms.txt probe must not fire for a
|
|
912
|
+
* user-supplied deep link, or `akm bundle add <site>/guides/foo` would
|
|
913
|
+
* silently ingest the whole site's manifest instead of the page requested.
|
|
914
|
+
*/
|
|
915
|
+
export function isOriginRootUrl(url) {
|
|
916
|
+
return url.pathname === "/" && !url.search;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Parses the `llms.txt` link-list format: list items shaped like
|
|
920
|
+
* `- [title](path) - description` (the description, and its separator, are
|
|
921
|
+
* ignored — only the link target is needed). Any line that isn't a markdown
|
|
922
|
+
* link list item — headings, the leading `# Title`/`> summary` lines, prose —
|
|
923
|
+
* is simply not a link line and is skipped.
|
|
924
|
+
*/
|
|
925
|
+
export function parseLlmsTxtLinks(text, baseUrl) {
|
|
926
|
+
const links = [];
|
|
927
|
+
const seen = new Set();
|
|
928
|
+
for (const line of text.split(/\r?\n/)) {
|
|
929
|
+
const match = line.trim().match(/^-\s*\[[^\]]*\]\(([^)\s]+)\)/);
|
|
930
|
+
const href = match?.[1];
|
|
931
|
+
if (!href)
|
|
932
|
+
continue;
|
|
933
|
+
let resolved;
|
|
934
|
+
try {
|
|
935
|
+
resolved = new URL(href, baseUrl);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:")
|
|
941
|
+
continue;
|
|
942
|
+
const key = resolved.toString();
|
|
943
|
+
if (seen.has(key))
|
|
944
|
+
continue;
|
|
945
|
+
seen.add(key);
|
|
946
|
+
links.push(resolved);
|
|
947
|
+
}
|
|
948
|
+
return links;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Probes `<origin>/llms.txt` and, if present, returns its parsed link list.
|
|
952
|
+
* Reuses `fetchWebsiteResponse` so the manifest fetch itself gets the exact
|
|
953
|
+
* same SSRF host guard, redirect handling, and (via `robots`) robots.txt
|
|
954
|
+
* compliance as any other page fetch — this is still a fetch against a
|
|
955
|
+
* user-supplied host, no different from the rest of the crawl.
|
|
956
|
+
*
|
|
957
|
+
* `llms-full.txt` (the single-file concatenation of every page) is
|
|
958
|
+
* deliberately NOT read here. Its `## <path>` separators are ambiguous — page
|
|
959
|
+
* content legitimately contains `##` headings too — so recovered page
|
|
960
|
+
* boundaries can't be trusted, whereas per-page fetches through the existing
|
|
961
|
+
* pipeline are cheap, bounded by this author-curated list, and produce
|
|
962
|
+
* cleanly addressable assets. See the issue's "alternatives considered".
|
|
963
|
+
*/
|
|
964
|
+
async function fetchLlmsManifest(start, robots, options) {
|
|
965
|
+
const manifestUrl = new URL("/llms.txt", start.origin).toString();
|
|
966
|
+
const decision = await resolveCrawlRobotsDecision(robots, manifestUrl);
|
|
967
|
+
if (!decision.allowed)
|
|
968
|
+
return null;
|
|
969
|
+
let fetched;
|
|
970
|
+
try {
|
|
971
|
+
fetched = await fetchWebsiteResponse(decision.fetchUrl, 0, {
|
|
972
|
+
allowPrivateHosts: options.allowPrivateHosts,
|
|
973
|
+
signal: options.signal,
|
|
974
|
+
robots,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
if (!fetched.response.ok) {
|
|
981
|
+
await fetched.response.body?.cancel().catch(() => undefined);
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
let text;
|
|
985
|
+
try {
|
|
986
|
+
text = await readBodyWithByteCap(fetched.response, LLMS_TXT_BYTE_CAP, {
|
|
987
|
+
bodyTimeoutMs: LLMS_TXT_BODY_TIMEOUT_MS,
|
|
988
|
+
signal: options.signal,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
const links = parseLlmsTxtLinks(text, fetched.finalUrl);
|
|
995
|
+
if (links.length === 0)
|
|
996
|
+
return null;
|
|
997
|
+
return { manifestUrl: fetched.finalUrl, links };
|
|
998
|
+
}
|
|
873
999
|
function normalizeCrawlUrl(rawUrl) {
|
|
874
1000
|
try {
|
|
875
1001
|
const parsed = new URL(rawUrl);
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* import their opener from a sibling here instead of reaching up into the
|
|
11
11
|
* indexer — inverting the old storage→indexer arrow.
|
|
12
12
|
*/
|
|
13
|
+
import fs from "node:fs";
|
|
13
14
|
import { createRequire } from "node:module";
|
|
14
15
|
import { ConfigError } from "../../core/errors.js";
|
|
15
16
|
import { classifyPathAccess, describeInaccessiblePath } from "../../core/path-access.js";
|
|
16
17
|
import { getDbPath } from "../../core/paths.js";
|
|
18
|
+
import { warn } from "../../core/warn.js";
|
|
17
19
|
import { openDatabase } from "../database.js";
|
|
18
20
|
import { openManagedDatabase } from "../managed-db.js";
|
|
19
21
|
import { SQLITE_BUSY_TIMEOUT_MS } from "../sqlite-pragmas.js";
|
|
@@ -21,9 +23,23 @@ import { openSqliteReadSnapshot } from "../sqlite-read-snapshot.js";
|
|
|
21
23
|
import { isCanonicalIndexGeneration } from "./index-entry-schema.js";
|
|
22
24
|
import { ensureSchema } from "./index-schema.js";
|
|
23
25
|
import { loadVecExtension, warnIfVecMissing } from "./index-vec-repository.js";
|
|
26
|
+
/**
|
|
27
|
+
* Whether `error` is SQLite reporting on-disk corruption (`SQLITE_CORRUPT`,
|
|
28
|
+
* "database disk image is malformed") rather than a permission, lock, or
|
|
29
|
+
* schema problem. Matched on both `code` (bun:sqlite, better-sqlite3) and
|
|
30
|
+
* message text, since driver error shapes are not perfectly uniform.
|
|
31
|
+
*/
|
|
32
|
+
function isCorruptionError(error) {
|
|
33
|
+
const code = error?.code;
|
|
34
|
+
if (code === "SQLITE_CORRUPT")
|
|
35
|
+
return true;
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
return message.includes("database disk image is malformed") || message.includes("SQLITE_CORRUPT");
|
|
38
|
+
}
|
|
24
39
|
export function openIndexDatabase(dbPath, options) {
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
const resolvedPath = dbPath ?? getDbPath();
|
|
41
|
+
const spec = {
|
|
42
|
+
path: resolvedPath,
|
|
27
43
|
init: (db) => {
|
|
28
44
|
// Try to load sqlite-vec extension
|
|
29
45
|
loadVecExtension(db);
|
|
@@ -41,7 +57,33 @@ export function openIndexDatabase(dbPath, options) {
|
|
|
41
57
|
// Warn once at init if using JS fallback with many entries
|
|
42
58
|
warnIfVecMissing(db, { once: true });
|
|
43
59
|
},
|
|
44
|
-
}
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
return openManagedDatabase(spec);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// index.db is a derived cache, fully regenerable from the stash on disk
|
|
66
|
+
// (see src/core/state-db.ts's "Why a separate database from index.db"
|
|
67
|
+
// note) — so real on-disk corruption is recovered by deleting the file
|
|
68
|
+
// and rebuilding, not by surfacing a raw SQLITE_CORRUPT to the caller or
|
|
69
|
+
// quietly falling through to an unreadable index. This mirrors the
|
|
70
|
+
// existing stale-version-marker rebuild below, one layer further down
|
|
71
|
+
// (that path opens fine and rewrites tables in place; corruption prevents
|
|
72
|
+
// even opening, so the file itself has to go first).
|
|
73
|
+
if (!isCorruptionError(error))
|
|
74
|
+
throw error;
|
|
75
|
+
warn(`Index database is corrupt at ${resolvedPath} — rebuilding.`);
|
|
76
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
77
|
+
try {
|
|
78
|
+
fs.rmSync(`${resolvedPath}${suffix}`, { force: true });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Best-effort cleanup; the retried open below still fails loudly if
|
|
82
|
+
// the file could not actually be removed.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return openManagedDatabase(spec);
|
|
86
|
+
}
|
|
45
87
|
}
|
|
46
88
|
/**
|
|
47
89
|
* Read the operator-configured embedding dimension from the on-disk config.
|
|
@@ -31,7 +31,7 @@ import { getTaskLogDir } from "../../core/paths.js";
|
|
|
31
31
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
32
32
|
import { parseSchedule, translateToCron } from "../schedule.js";
|
|
33
33
|
import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
|
|
34
|
-
import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
34
|
+
import { buildScheduledBindingInvocation, parsePublicSchedulerInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, SCHEDULER_CONTEXT_ARG, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
35
35
|
import { nodeFs, throwIfNotOk } from "./exec-utils.js";
|
|
36
36
|
const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
|
|
37
37
|
const END = (id) => `# akm:task ${assertCronValue(id)} END`;
|
|
@@ -105,7 +105,7 @@ export function CRON_BACKEND(options = {}) {
|
|
|
105
105
|
replaceCrontab(exec, existing, next);
|
|
106
106
|
},
|
|
107
107
|
list() {
|
|
108
|
-
return [...inspectCronState(readCrontab(exec)).installed];
|
|
108
|
+
return [...inspectCronState(readCrontab(exec), defaultContextPath).installed];
|
|
109
109
|
},
|
|
110
110
|
listForRebind() {
|
|
111
111
|
const existing = readCrontab(exec);
|
|
@@ -123,14 +123,14 @@ export function CRON_BACKEND(options = {}) {
|
|
|
123
123
|
});
|
|
124
124
|
},
|
|
125
125
|
listNativeArtifacts() {
|
|
126
|
-
return [...inspectCronState(readCrontab(exec)).artifacts];
|
|
126
|
+
return [...inspectCronState(readCrontab(exec), defaultContextPath).artifacts];
|
|
127
127
|
},
|
|
128
128
|
inspectBindings() {
|
|
129
|
-
return inspectCronState(readCrontab(exec));
|
|
129
|
+
return inspectCronState(readCrontab(exec), defaultContextPath);
|
|
130
130
|
},
|
|
131
131
|
snapshotBindings(ids) {
|
|
132
132
|
const crontab = readCrontab(exec);
|
|
133
|
-
const inspection = inspectCronState(crontab);
|
|
133
|
+
const inspection = inspectCronState(crontab, defaultContextPath);
|
|
134
134
|
const keys = new Set(ids.map(schedulerNativeArtifactKey));
|
|
135
135
|
return Object.freeze({
|
|
136
136
|
kind: CRON_SNAPSHOT,
|
|
@@ -144,7 +144,7 @@ export function CRON_BACKEND(options = {}) {
|
|
|
144
144
|
throw new ConfigError("Invalid cron scheduler snapshot.", "INVALID_CONFIG_FILE");
|
|
145
145
|
}
|
|
146
146
|
const existing = readCrontab(exec);
|
|
147
|
-
const current = inspectCronState(existing);
|
|
147
|
+
const current = inspectCronState(existing, defaultContextPath);
|
|
148
148
|
const safeNativeIds = [];
|
|
149
149
|
const errors = [];
|
|
150
150
|
if (expectedCurrent) {
|
|
@@ -192,7 +192,7 @@ export function CRON_BACKEND(options = {}) {
|
|
|
192
192
|
},
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
-
function inspectCronState(crontab) {
|
|
195
|
+
function inspectCronState(crontab, fallbackContextPath) {
|
|
196
196
|
const installed = [];
|
|
197
197
|
const artifacts = [];
|
|
198
198
|
for (const { id, body } of listBlocks(crontab)) {
|
|
@@ -207,7 +207,14 @@ function inspectCronState(crontab) {
|
|
|
207
207
|
signature: fingerprint,
|
|
208
208
|
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
209
209
|
binding: parsed.binding,
|
|
210
|
-
|
|
210
|
+
// A legacy (pre-`--scheduler-context`) row has no real descriptor
|
|
211
|
+
// path to report — `extractLegacyCronInvocation` leaves it "". Fall
|
|
212
|
+
// back to the current default so downstream consumers (context
|
|
213
|
+
// validation in `akm task prune`/`explain`, `sync`'s reuse of an
|
|
214
|
+
// existing binding's contextPath) see a real, resolvable descriptor
|
|
215
|
+
// rather than an empty path, since the row is about to be reconciled
|
|
216
|
+
// to a current one anyway (#881).
|
|
217
|
+
contextPath: parsed.contextPath || fallbackContextPath,
|
|
211
218
|
};
|
|
212
219
|
Object.defineProperty(ref, "nativeId", { value: id });
|
|
213
220
|
Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
|
|
@@ -323,7 +330,40 @@ export function extractCronInvocation(body) {
|
|
|
323
330
|
const redirectIndex = fields.indexOf(">>", commandStart);
|
|
324
331
|
if (redirectIndex === -1)
|
|
325
332
|
return undefined;
|
|
326
|
-
|
|
333
|
+
const tail = fields.slice(commandStart, redirectIndex);
|
|
334
|
+
const parsed = parseScheduledBindingArgv(tail);
|
|
335
|
+
if (parsed)
|
|
336
|
+
return parsed;
|
|
337
|
+
// Rows written by akm < 0.9.2 (before `--scheduler-context` existed) have
|
|
338
|
+
// no context argument at all — just the akm argv immediately followed by
|
|
339
|
+
// the public `task run …` / `workflow run …` tail. `extractCronInvocation`
|
|
340
|
+
// only ever runs on a body already isolated between this backend's own
|
|
341
|
+
// `# akm:task … BEGIN/END` sentinels (see `parseBlocks`), so recognizing
|
|
342
|
+
// this older shape here doesn't extend trust to any unmarked crontab
|
|
343
|
+
// line — it only lets sync see and reconcile a row akm already owns
|
|
344
|
+
// instead of treating it as absent and colliding with the still-present
|
|
345
|
+
// artifact (#881). Guarded on the marker's absence so a row that DOES
|
|
346
|
+
// carry `--scheduler-context` but fails to parse for some other reason
|
|
347
|
+
// is never silently reinterpreted as legacy.
|
|
348
|
+
if (tail.includes(SCHEDULER_CONTEXT_ARG))
|
|
349
|
+
return undefined;
|
|
350
|
+
return extractLegacyCronInvocation(tail);
|
|
351
|
+
}
|
|
352
|
+
function extractLegacyCronInvocation(tail) {
|
|
353
|
+
for (let index = 0; index < tail.length - 1; index += 1) {
|
|
354
|
+
if ((tail[index] === "task" || tail[index] === "workflow") && tail[index + 1] === "run") {
|
|
355
|
+
const publicInvocation = parsePublicSchedulerInvocation(tail.slice(index));
|
|
356
|
+
if (!publicInvocation)
|
|
357
|
+
return undefined;
|
|
358
|
+
return {
|
|
359
|
+
binding: tail.slice(0, index),
|
|
360
|
+
contextPath: "",
|
|
361
|
+
invocation: publicInvocation.invocation,
|
|
362
|
+
...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
327
367
|
}
|
|
328
368
|
/** Reverse {@link quoteForCron} for a single whitespace-free token. */
|
|
329
369
|
function splitCronShellWords(value) {
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
* Resolution order:
|
|
11
11
|
*
|
|
12
12
|
* 1. `process.execPath` alone for a Bun standalone executable.
|
|
13
|
-
* 2. Absolute Node plus the public `dist/akm` package launcher
|
|
13
|
+
* 2. Absolute Node plus the public `dist/akm` package launcher — eligible
|
|
14
|
+
* when it is the active npm global install, or when this process
|
|
15
|
+
* cannot write to the directory containing it (a read-only mount, e.g.
|
|
16
|
+
* an image-baked install, gives the same "won't change out from under
|
|
17
|
+
* the scheduler" guarantee npm-global ownership does).
|
|
14
18
|
* 3. Absolute runtime plus the source/build CLI entry, classified as a
|
|
15
19
|
* checkout that requires explicit `--rebind` for scheduler writes.
|
|
16
20
|
*
|
|
@@ -51,12 +55,13 @@ export function resolveAkmInvocation(options = {}) {
|
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
const npmGlobal = !checkout && packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot);
|
|
58
|
+
const readOnlyInstall = !checkout && !npmGlobal && !(options.isPathWritable ?? isPathWritable)(path.dirname(launcherPath));
|
|
54
59
|
const kind = checkout ? "checkout" : npmGlobal ? "npm" : "package-local";
|
|
55
60
|
return {
|
|
56
61
|
argv: [absoluteInvocationPath(nodePath), absoluteInvocationPath(launcherPath)],
|
|
57
62
|
via: kind,
|
|
58
63
|
kind,
|
|
59
|
-
eligible: npmGlobal,
|
|
64
|
+
eligible: npmGlobal || readOnlyInstall,
|
|
60
65
|
};
|
|
61
66
|
}
|
|
62
67
|
const checkoutEntry = resolveCheckoutEntry(options.cliEntryUrl ?? import.meta.url, runtime, mainPath);
|
|
@@ -117,6 +122,16 @@ function isCheckoutLauncher(file) {
|
|
|
117
122
|
const packageRoot = path.dirname(path.dirname(launcher));
|
|
118
123
|
return fs.existsSync(path.join(packageRoot, ".git"));
|
|
119
124
|
}
|
|
125
|
+
/** Whether this process can write to `dir` — false also covers a read-only mount. */
|
|
126
|
+
function isPathWritable(dir) {
|
|
127
|
+
try {
|
|
128
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
120
135
|
function packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot) {
|
|
121
136
|
if (!npmGlobalRoot)
|
|
122
137
|
return false;
|
|
@@ -189,7 +189,14 @@ export function parseScheduledBindingArgv(argv) {
|
|
|
189
189
|
...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Parse just the public `task run …` / `workflow run …` tail, with no
|
|
194
|
+
* `--scheduler-context` wrapper. Exported so a backend can recognize a
|
|
195
|
+
* pre-`--scheduler-context` invocation still sitting inside akm's own
|
|
196
|
+
* ownership-marked block (see `extractCronInvocation` in
|
|
197
|
+
* `src/tasks/backends/cron.ts`) without re-implementing this grammar.
|
|
198
|
+
*/
|
|
199
|
+
export function parsePublicSchedulerInvocation(invocation) {
|
|
193
200
|
if (invocation[0] === "task" && invocation[1] === "run" && invocation[2]) {
|
|
194
201
|
try {
|
|
195
202
|
if (normaliseTaskConceptId(invocation[2]) !== invocation[2])
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* | root `version` | outcome |
|
|
18
18
|
* |------------------------|-----------------------------------------------------------------|
|
|
19
19
|
* | `4` | `parseTaskSourceV4Document` — the new grammar (row B-13) |
|
|
20
|
-
* | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED
|
|
20
|
+
* | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape — the file needs a human decision, not a re-run), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED` naming the specific blocked reason (issue #869) — the shim removes friction for the deterministic case, it never hides a real problem |
|
|
21
21
|
* | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) |
|
|
22
22
|
* | absent / not a number | `parseTaskSourceV4Document` — its own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording (row B-16) |
|
|
23
23
|
*
|
|
@@ -57,6 +57,18 @@ export function peekTaskSourceVersion(root) {
|
|
|
57
57
|
return typeof value === "number" ? value : undefined;
|
|
58
58
|
}
|
|
59
59
|
const TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
|
|
60
|
+
/**
|
|
61
|
+
* Thrown only when the deterministic conversion itself could not produce a
|
|
62
|
+
* task source v4 document — a case where a person must decide the intended
|
|
63
|
+
* behavior (e.g. an ambiguous shell command), not one the migrator can just
|
|
64
|
+
* be re-run to fix. `reason`/`detail` are the SAME blocked outcome
|
|
65
|
+
* `akm migrate status`/`apply` reports for this file, so the message names
|
|
66
|
+
* the actual decision instead of pointing at a command that will report the
|
|
67
|
+
* identical block.
|
|
68
|
+
*/
|
|
69
|
+
function unmigratableVersionError(filePath, version, reason, detail) {
|
|
70
|
+
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version} and needs a human decision before it can run — the deterministic migrator cannot convert it automatically (${reason}${detail ? `: ${detail}` : ""}).`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Review the file and resolve the ambiguity by hand, then it will convert normally; `akm migrate status` reports the same reason.");
|
|
71
|
+
}
|
|
60
72
|
function unsupportedVersionError(filePath, version) {
|
|
61
73
|
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
|
|
62
74
|
}
|
|
@@ -64,9 +76,10 @@ function unsupportedVersionError(filePath, version) {
|
|
|
64
76
|
* Plan the SAME bytes already in hand through the pure v3->v4 (and, for v2,
|
|
65
77
|
* chained v2->v3->v4) migration planner(s) — never touches disk, never
|
|
66
78
|
* writes the file, never re-reads it from disk. Returns the produced v4
|
|
67
|
-
* YAML text, or
|
|
68
|
-
* proceed (an unmigratable v2/v3 shape) — the caller falls back to
|
|
69
|
-
* hard error this gate threw before the shim existed
|
|
79
|
+
* YAML text, or the blocked reason/detail when the deterministic conversion
|
|
80
|
+
* cannot proceed (an unmigratable v2/v3 shape) — the caller falls back to
|
|
81
|
+
* the same hard error this gate threw before the shim existed, now naming
|
|
82
|
+
* that reason.
|
|
70
83
|
*/
|
|
71
84
|
function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
|
|
72
85
|
const bytes = Buffer.from(yaml, "utf8");
|
|
@@ -89,12 +102,12 @@ function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
|
|
|
89
102
|
else {
|
|
90
103
|
const v3Outcome = planTaskToV3File(baseInput);
|
|
91
104
|
if (v3Outcome.status !== "changed")
|
|
92
|
-
return
|
|
105
|
+
return { reason: v3Outcome.reason, detail: v3Outcome.detail };
|
|
93
106
|
v3Bytes = v3Outcome.after;
|
|
94
107
|
}
|
|
95
108
|
const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
|
|
96
109
|
if (v4Outcome.status !== "changed")
|
|
97
|
-
return
|
|
110
|
+
return { reason: v4Outcome.reason, detail: v4Outcome.detail };
|
|
98
111
|
return v4Outcome.after.toString("utf8");
|
|
99
112
|
}
|
|
100
113
|
/** Parse task source YAML, routing per the terminal table above. */
|
|
@@ -103,16 +116,17 @@ export function parseTaskSource(input) {
|
|
|
103
116
|
const version = peekTaskSourceVersion(root);
|
|
104
117
|
if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
|
|
105
118
|
if (version === 2 || version === 3) {
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
119
|
+
const shimmed = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
|
|
120
|
+
if (typeof shimmed === "string") {
|
|
108
121
|
const v4 = parseTaskSourceV4({
|
|
109
|
-
yaml:
|
|
122
|
+
yaml: shimmed,
|
|
110
123
|
filePath: input.filePath,
|
|
111
124
|
...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
|
|
112
125
|
});
|
|
113
126
|
warn(`akm: task ${input.filePath} uses schema v${version} — auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
|
|
114
127
|
return Object.freeze({ version: 4, v4 });
|
|
115
128
|
}
|
|
129
|
+
throw unmigratableVersionError(input.filePath, version, shimmed.reason, shimmed.detail);
|
|
116
130
|
}
|
|
117
131
|
throw unsupportedVersionError(input.filePath, version);
|
|
118
132
|
}
|
package/docs/reference/cli.md
CHANGED
|
@@ -2032,7 +2032,9 @@ when one exists.
|
|
|
2032
2032
|
Scan bundle markdown files for structural issues: unquoted colons, missing
|
|
2033
2033
|
`updated` field, orphaned stubs, placeholder stubs, missing `name`/`type`,
|
|
2034
2034
|
stale paths, and broken refs — in body text and in
|
|
2035
|
-
`refs`/`xrefs`/`supersededBy`/`contradictedBy` frontmatter.
|
|
2035
|
+
`refs`/`xrefs`/`supersededBy`/`contradictedBy` frontmatter. A belief edge
|
|
2036
|
+
pointing at a memory that `akm improve` pruned resolves through the archive
|
|
2037
|
+
tombstone under `.akm/memory-cleanup/archive/` and is not reported (#884). Also reports
|
|
2036
2038
|
`dangerous-env-key` findings for env files (the same key set `akm bundle add`
|
|
2037
2039
|
enforces — see [Dangerous env key audit](#dangerous-env-key-audit) — but
|
|
2038
2040
|
non-blocking here; `lint` only warns). `--type workflows` structurally parses
|
|
@@ -2046,6 +2048,7 @@ akm lint --fix # Auto-fix Tier-1 issues in place
|
|
|
2046
2048
|
akm lint --type workflows # Only lint one asset type
|
|
2047
2049
|
akm lint --dir ~/other-bundle # Override the bundle root (default: from config)
|
|
2048
2050
|
akm lint --fail-on-flagged # CI-friendly: exit non-zero when summary.flagged > 0
|
|
2051
|
+
akm lint --prune-dangling-edges # Opt-in: drop belief edges whose target is gone
|
|
2049
2052
|
```
|
|
2050
2053
|
|
|
2051
2054
|
| Flag | Description |
|
|
@@ -2054,6 +2057,7 @@ akm lint --fail-on-flagged # CI-friendly: exit non-zero when summary.flagge
|
|
|
2054
2057
|
| `--dir` | Override the bundle root directory (default: from config) |
|
|
2055
2058
|
| `--type` | Only lint assets of this type (e.g. `workflows`, `tasks`, `memories`). **akm bundles only** — every other adapter validates the whole bundle and warns on stderr that the flag had no effect. |
|
|
2056
2059
|
| `--fail-on-flagged` | Exit non-zero when `summary.flagged > 0`. Default: exit 0 regardless of findings. |
|
|
2060
|
+
| `--prune-dangling-edges` | Opt-in repair (#884): drop `supersededBy`/`contradictedBy` entries whose target has neither a file nor a prune tombstone. **Not** implied by `--fix` — it edits well-formed files to delete a belief-graph claim, so review a plain `akm lint` report first. Clears the same `writable: false` gate `--fix` does. |
|
|
2057
2061
|
|
|
2058
2062
|
Returns `fixed[]` and `flagged[]` arrays plus a `summary: { fixed, flagged }`
|
|
2059
2063
|
count. Each entry carries `file`, `issue`, `detail`, and whether it was
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
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": [
|