akm-cli 0.9.0-beta.4 → 0.9.0-beta.41
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 +646 -0
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +6 -2
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +22 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +22 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +24 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +22 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +25 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +21 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +21 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +23 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +22 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/cli.js +14 -3
- package/dist/commands/agent/contribute-cli.js +16 -3
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph.js +75 -71
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +381 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate.js +631 -111
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +163 -69
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +115 -1
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +403 -40
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +113 -6
- package/dist/commands/improve/improve-profiles.js +12 -0
- package/dist/commands/improve/improve.js +2042 -612
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +602 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +46 -4
- package/dist/commands/improve/related-sessions.js +120 -0
- package/dist/commands/improve/salience.js +438 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +34 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +17 -1
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +5 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +187 -57
- package/dist/commands/read/curate.js +344 -80
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/read/show.js +67 -2
- package/dist/commands/sources/init.js +36 -9
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/schema-repair.js +13 -1
- package/dist/commands/sources/stash-cli.js +19 -3
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/core/asset/asset-registry.js +2 -0
- package/dist/core/asset/asset-spec.js +14 -0
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +83 -0
- package/dist/core/config/config-schema.js +274 -2
- package/dist/core/config/config.js +2 -2
- package/dist/core/logs-db.js +4 -3
- package/dist/core/paths.js +3 -0
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state-db.js +691 -30
- package/dist/indexer/db/db.js +364 -38
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +203 -3
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +10 -3
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/prompts.js +33 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +23 -4
- package/dist/llm/embedder.js +27 -3
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- package/dist/llm/graph-extract.js +2 -1
- package/dist/llm/memory-infer.js +4 -8
- package/dist/llm/metadata-enhance.js +9 -1
- package/dist/output/renderers.js +73 -1
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/text/helpers.js +16 -1
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +1378 -599
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +479 -270
- package/dist/setup/setup.js +3 -3
- package/dist/sources/providers/git.js +71 -61
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +8 -6
package/dist/setup/setup.js
CHANGED
|
@@ -1791,7 +1791,7 @@ export async function runSetupWizard(opts) {
|
|
|
1791
1791
|
// Bootstrap directory structure before any prompts so the stash exists
|
|
1792
1792
|
// even if the wizard is interrupted after this point.
|
|
1793
1793
|
if (!opts?.noInit) {
|
|
1794
|
-
await akmInit({ dir: resolvedStashDir });
|
|
1794
|
+
await akmInit({ dir: resolvedStashDir, setDefault: true });
|
|
1795
1795
|
}
|
|
1796
1796
|
// Quick connectivity check — skip network-dependent steps when offline
|
|
1797
1797
|
const online = await isOnline();
|
|
@@ -1986,7 +1986,7 @@ export async function runSetupWithDefaults(opts) {
|
|
|
1986
1986
|
// Bootstrap directory structure first
|
|
1987
1987
|
let initResult;
|
|
1988
1988
|
if (!opts.noInit) {
|
|
1989
|
-
initResult = await akmInit({ dir: stashDir });
|
|
1989
|
+
initResult = await akmInit({ dir: stashDir, setDefault: true });
|
|
1990
1990
|
}
|
|
1991
1991
|
// Run steps in non-interactive mode (applies defaults, skips prompts)
|
|
1992
1992
|
const ctx = createSetupContext(current, { nonInteractive: true });
|
|
@@ -2283,7 +2283,7 @@ export async function runSetupFromConfig(opts) {
|
|
|
2283
2283
|
// Bootstrap directory structure
|
|
2284
2284
|
let initResult;
|
|
2285
2285
|
if (!opts.noInit) {
|
|
2286
|
-
initResult = await akmInit({ dir: stashDir });
|
|
2286
|
+
initResult = await akmInit({ dir: stashDir, setDefault: true });
|
|
2287
2287
|
}
|
|
2288
2288
|
// Optional probe
|
|
2289
2289
|
const mergedLlm = getDefaultLlmConfig(merged);
|
|
@@ -479,28 +479,32 @@ export function saveGitStash(name, message, writableOverride, options) {
|
|
|
479
479
|
if (!statusResult.stdout.trim()) {
|
|
480
480
|
return { committed: false, pushed: false, skipped: false, output: "nothing to commit, working tree clean" };
|
|
481
481
|
}
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
482
|
+
// Scoped staging (#476 + the auto-sync incident): NEVER refuse akm's commit
|
|
483
|
+
// because unrelated non-akm files exist in the working tree. When the stash
|
|
484
|
+
// dir is shared with a non-akm project (stash root == project repo root), a
|
|
485
|
+
// blunt `git add -A` would sweep the user's unrelated WIP into the stash's
|
|
486
|
+
// remote. We avoid that by SCOPING what we stage, not by refusing the commit.
|
|
487
|
+
//
|
|
488
|
+
// Precedence:
|
|
489
|
+
// 1. Explicit modified-file list (`options.paths`) — stage exactly those.
|
|
490
|
+
// 2. Managed pathspecs (TYPE_DIRS values + `.akm`) that exist on disk —
|
|
491
|
+
// stages everything akm owns and, by construction, never stages non-akm
|
|
492
|
+
// WIP. This preserves the #476 protection WITHOUT refusing.
|
|
493
|
+
// 3. Last-resort `git add -A` — ONLY when neither an explicit list nor any
|
|
494
|
+
// managed pathspec can be resolved. This is the maintainer-approved
|
|
495
|
+
// "or all files if we cannot determine the exact file list" fallback and
|
|
496
|
+
// is the one (rare) path that could include unrelated non-akm files.
|
|
497
|
+
if (!stageScopedChanges(repoDir, options?.paths)) {
|
|
498
|
+
throw new Error(`git add failed while staging akm changes in ${repoDir}`);
|
|
499
|
+
}
|
|
500
|
+
// Nothing actually staged → don't create an empty commit. This happens when
|
|
501
|
+
// only non-akm files were dirty (precedence 2 staged nothing).
|
|
502
|
+
const stagedResult = runGit(["-C", repoDir, "diff", "--cached", "--quiet"]);
|
|
503
|
+
if (stagedResult.status === 0) {
|
|
504
|
+
return { committed: false, pushed: false, skipped: false, output: "nothing to commit" };
|
|
505
|
+
}
|
|
506
|
+
// Commit — supply fallback identity so fresh environments without
|
|
498
507
|
// user.name/user.email configured can always commit to the default stash.
|
|
499
|
-
// `add -A` is safe here because nonAkmDirty was just verified empty.
|
|
500
|
-
const addResult = runGit(["-C", repoDir, "add", "-A"]);
|
|
501
|
-
if (addResult.status !== 0) {
|
|
502
|
-
throw new Error(`git add failed: ${addResult.stderr?.trim() || "unknown error"}`);
|
|
503
|
-
}
|
|
504
508
|
const commitResult = runGit([
|
|
505
509
|
"-C",
|
|
506
510
|
repoDir,
|
|
@@ -535,6 +539,51 @@ export function saveGitStash(name, message, writableOverride, options) {
|
|
|
535
539
|
output: (commitResult.stdout + pushResult.stdout).trim() || "changes committed and pushed",
|
|
536
540
|
};
|
|
537
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Stage akm's changes in `repoDir` using the scoped-staging precedence
|
|
544
|
+
* documented at the call site (#476). Returns `false` only when a `git add`
|
|
545
|
+
* subprocess fails; returns `true` otherwise (including when nothing matched —
|
|
546
|
+
* the caller then detects "nothing staged" via `git diff --cached --quiet`).
|
|
547
|
+
*
|
|
548
|
+
* @param paths Optional explicit repo-relative paths akm wrote this run. When
|
|
549
|
+
* provided and non-empty, exactly those are staged (chunked to stay under
|
|
550
|
+
* argv length limits). Otherwise we fall back to the managed pathspecs, and
|
|
551
|
+
* finally to `git add -A` only if no managed pathspec exists on disk.
|
|
552
|
+
*/
|
|
553
|
+
function stageScopedChanges(repoDir, paths) {
|
|
554
|
+
// Precedence 1: explicit modified-file list.
|
|
555
|
+
const explicit = (paths ?? []).filter((p) => typeof p === "string" && p.length > 0);
|
|
556
|
+
if (explicit.length > 0) {
|
|
557
|
+
return addPathspecsChunked(repoDir, explicit);
|
|
558
|
+
}
|
|
559
|
+
// Precedence 2: managed pathspecs that exist on disk (TYPE_DIRS + `.akm`).
|
|
560
|
+
const managed = [...Object.values(TYPE_DIRS), ".akm"].filter((dir) => fs.existsSync(path.join(repoDir, dir)));
|
|
561
|
+
if (managed.length > 0) {
|
|
562
|
+
return addPathspecsChunked(repoDir, managed);
|
|
563
|
+
}
|
|
564
|
+
// Precedence 3 (last resort): nothing akm-managed resolved on disk. Stage
|
|
565
|
+
// everything. This is the ONLY branch that can include unrelated non-akm
|
|
566
|
+
// files — it is the explicit, maintainer-approved "or all files if we cannot
|
|
567
|
+
// determine the exact file list" fallback and should be rare/never in
|
|
568
|
+
// practice (a git-backed stash always has at least one managed subtree once
|
|
569
|
+
// akm has written to it).
|
|
570
|
+
const addAll = runGit(["-C", repoDir, "add", "-A"]);
|
|
571
|
+
return addAll.status === 0;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Run `git add -- <pathspec>...` in chunks so a very large path list never
|
|
575
|
+
* exceeds the OS argv-length limit. Each chunk must succeed.
|
|
576
|
+
*/
|
|
577
|
+
function addPathspecsChunked(repoDir, pathspecs) {
|
|
578
|
+
const CHUNK = 500;
|
|
579
|
+
for (let i = 0; i < pathspecs.length; i += CHUNK) {
|
|
580
|
+
const chunk = pathspecs.slice(i, i + CHUNK);
|
|
581
|
+
const result = runGit(["-C", repoDir, "add", "--", ...chunk]);
|
|
582
|
+
if (result.status !== 0)
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
538
587
|
function findGitStashByTarget(stashes, target) {
|
|
539
588
|
return stashes.find((stash) => matchesGitStashTarget(stash, target));
|
|
540
589
|
}
|
|
@@ -620,45 +669,6 @@ export function classifyCloneFailure(url, stderr, spawnError) {
|
|
|
620
669
|
const detail = raw || spawnMsg || "unknown error";
|
|
621
670
|
return `Failed to clone ${url}: ${detail}`;
|
|
622
671
|
}
|
|
623
|
-
// ── Stash-safety helpers (#476) ──────────────────────────────────────────────
|
|
624
|
-
/**
|
|
625
|
-
* Inspect `git status --porcelain` output and return every dirty path that is
|
|
626
|
-
* NOT inside an akm-managed subtree. Used by `runUpstreamPush` to refuse
|
|
627
|
-
* pushing unrelated WIP when a writable stash shares its root with a project
|
|
628
|
-
* repo.
|
|
629
|
-
*
|
|
630
|
-
* Porcelain v1 format: `XY <path>` or `XY <orig> -> <new>` for renames. We
|
|
631
|
-
* key off the post-rename path (or the only path) — that is the working-tree
|
|
632
|
-
* file at risk of being staged by `git add -A`.
|
|
633
|
-
*/
|
|
634
|
-
function collectNonAkmDirtyPaths(porcelainOutput) {
|
|
635
|
-
const akmDirs = new Set(Object.values(TYPE_DIRS));
|
|
636
|
-
const result = [];
|
|
637
|
-
for (const rawLine of porcelainOutput.split("\n")) {
|
|
638
|
-
const line = rawLine.replace(/\r$/, "");
|
|
639
|
-
if (line.length === 0)
|
|
640
|
-
continue;
|
|
641
|
-
// Skip the 2-char status code + 1 space.
|
|
642
|
-
let p = line.length > 3 ? line.slice(3) : "";
|
|
643
|
-
// Renames / copies: `from -> to`. Stage decision applies to `to`.
|
|
644
|
-
const arrow = p.lastIndexOf(" -> ");
|
|
645
|
-
if (arrow !== -1) {
|
|
646
|
-
p = p.slice(arrow + 4);
|
|
647
|
-
}
|
|
648
|
-
// Strip surrounding quotes for paths with special chars.
|
|
649
|
-
if (p.startsWith('"') && p.endsWith('"') && p.length >= 2) {
|
|
650
|
-
p = p.slice(1, -1);
|
|
651
|
-
}
|
|
652
|
-
if (!p)
|
|
653
|
-
continue;
|
|
654
|
-
const segments = p.split("/");
|
|
655
|
-
const top = segments[0];
|
|
656
|
-
if (top === ".akm" || akmDirs.has(top))
|
|
657
|
-
continue;
|
|
658
|
-
result.push(p);
|
|
659
|
-
}
|
|
660
|
-
return result;
|
|
661
|
-
}
|
|
662
672
|
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
663
|
-
export {
|
|
673
|
+
export { ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
|
|
664
674
|
// resolveWritableOverride is exported at its declaration above.
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
* providers that fetch tarballs (currently `NpmSourceProvider` and the
|
|
13
13
|
* registry index builder).
|
|
14
14
|
*/
|
|
15
|
-
import { spawnSync } from "node:child_process";
|
|
16
15
|
import { createHash } from "node:crypto";
|
|
17
16
|
import fs from "node:fs";
|
|
18
17
|
import path from "node:path";
|
|
19
18
|
import { isWithin } from "../../core/common.js";
|
|
20
19
|
import { warn } from "../../core/warn.js";
|
|
20
|
+
import { spawnSync } from "../../runtime.js";
|
|
21
21
|
/**
|
|
22
22
|
* Verify an archive's integrity against a known hash. Throws and removes
|
|
23
23
|
* the archive when verification fails.
|
|
@@ -65,17 +65,25 @@ export function verifyArchiveIntegrity(archivePath, expected, source) {
|
|
|
65
65
|
* tree for symlinks that would escape the destination.
|
|
66
66
|
*/
|
|
67
67
|
export function extractTarGzSecure(archivePath, destinationDir) {
|
|
68
|
-
const listResult = spawnSync("tar",
|
|
69
|
-
if (listResult.
|
|
70
|
-
const err = listResult.stderr?.trim() ||
|
|
68
|
+
const listResult = spawnSync(["tar", "tzf", archivePath]);
|
|
69
|
+
if (!listResult.success) {
|
|
70
|
+
const err = listResult.stderr?.toString().trim() || "unknown error";
|
|
71
71
|
throw new Error(`Failed to inspect archive ${archivePath}: ${err}`);
|
|
72
72
|
}
|
|
73
|
-
validateTarEntries(listResult.stdout);
|
|
73
|
+
validateTarEntries(listResult.stdout.toString());
|
|
74
74
|
fs.rmSync(destinationDir, { recursive: true, force: true });
|
|
75
75
|
fs.mkdirSync(destinationDir, { recursive: true });
|
|
76
|
-
const extractResult = spawnSync(
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
const extractResult = spawnSync([
|
|
77
|
+
"tar",
|
|
78
|
+
"xzf",
|
|
79
|
+
archivePath,
|
|
80
|
+
"--no-same-owner",
|
|
81
|
+
"--strip-components=1",
|
|
82
|
+
"-C",
|
|
83
|
+
destinationDir,
|
|
84
|
+
]);
|
|
85
|
+
if (!extractResult.success) {
|
|
86
|
+
const err = extractResult.stderr?.toString().trim() || "unknown error";
|
|
79
87
|
throw new Error(`Failed to extract archive ${archivePath}: ${err}`);
|
|
80
88
|
}
|
|
81
89
|
// Post-extraction scan: verify all extracted files are within destinationDir
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Shared SQLite PRAGMA application + journal-mode resolution (#628).
|
|
6
|
+
*
|
|
7
|
+
* Every AKM SQLite opener applies the same opening PRAGMAs: a journal mode, a
|
|
8
|
+
* 30 s busy_timeout, and (for most) foreign_keys = ON. Historically each opener
|
|
9
|
+
* hard-coded `PRAGMA journal_mode = WAL`. WAL is impossible on network
|
|
10
|
+
* filesystems (NFS/SMB) because its `-shm` shared-memory wal-index cannot be
|
|
11
|
+
* mmap'd over a network mount, so AKM could not run with its data dir on a
|
|
12
|
+
* network share.
|
|
13
|
+
*
|
|
14
|
+
* This module centralises that PRAGMA block behind {@link applyStandardPragmas}
|
|
15
|
+
* and makes the journal mode configurable via the `AKM_SQLITE_JOURNAL_MODE`
|
|
16
|
+
* env var (WAL | DELETE | TRUNCATE; default WAL = unchanged behaviour). When the
|
|
17
|
+
* mode is the WAL default and the data directory is detected to live on a
|
|
18
|
+
* network filesystem, it auto-falls-back to DELETE with a one-line warning.
|
|
19
|
+
*
|
|
20
|
+
* Boundary note: this is a PLAIN module, not a runtime-boundary file. It only
|
|
21
|
+
* does pure string work plus `db.exec()` (allowed everywhere). The single
|
|
22
|
+
* runtime primitive it needs — a filesystem-type probe (statfs) — lives in
|
|
23
|
+
* src/runtime.ts and is injected here, keeping the network-FS classifier a
|
|
24
|
+
* pure, unit-testable function.
|
|
25
|
+
*
|
|
26
|
+
* @module storage/sqlite-pragmas
|
|
27
|
+
*/
|
|
28
|
+
import { warn } from "../core/warn.js";
|
|
29
|
+
import { statfsType } from "../runtime.js";
|
|
30
|
+
const VALID_MODES = new Set(["WAL", "DELETE", "TRUNCATE"]);
|
|
31
|
+
// One-shot warning guards so a misconfigured env var or a network-FS fallback
|
|
32
|
+
// each emit AT MOST ONCE per process rather than on every db open.
|
|
33
|
+
let warnedInvalid = false;
|
|
34
|
+
let warnedNetworkFallback = false;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a raw `AKM_SQLITE_JOURNAL_MODE` value to a canonical {@link JournalMode}.
|
|
37
|
+
*
|
|
38
|
+
* PURE and unit-testable: the raw string is passed in (not read from
|
|
39
|
+
* `process.env` here). Trims + uppercases; an empty/undefined value yields the
|
|
40
|
+
* WAL default; a recognised value yields its canonical form; any other
|
|
41
|
+
* non-empty value warns once and falls back to WAL. Never throws.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveJournalMode(raw) {
|
|
44
|
+
if (raw === undefined)
|
|
45
|
+
return "WAL";
|
|
46
|
+
const normalized = raw.trim().toUpperCase();
|
|
47
|
+
if (normalized === "")
|
|
48
|
+
return "WAL";
|
|
49
|
+
if (VALID_MODES.has(normalized)) {
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
warnInvalidJournalModeOnce(raw);
|
|
53
|
+
return "WAL";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The single env-reading seam: resolve the configured journal mode from
|
|
57
|
+
* `process.env.AKM_SQLITE_JOURNAL_MODE`. Read at call time (per open) so tests
|
|
58
|
+
* that set the env per-case see the right value and we avoid stale-env flakes.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveConfiguredJournalMode() {
|
|
61
|
+
return resolveJournalMode(process.env.AKM_SQLITE_JOURNAL_MODE);
|
|
62
|
+
}
|
|
63
|
+
function warnInvalidJournalModeOnce(raw) {
|
|
64
|
+
if (warnedInvalid)
|
|
65
|
+
return;
|
|
66
|
+
warnedInvalid = true;
|
|
67
|
+
warn(`[akm] invalid AKM_SQLITE_JOURNAL_MODE=${JSON.stringify(raw)} — using WAL (valid: WAL, DELETE, TRUNCATE)`);
|
|
68
|
+
}
|
|
69
|
+
// Known Linux f_type magic numbers for network filesystems. node's statfs
|
|
70
|
+
// normalises `type` to this numeric f_type magic on all platforms.
|
|
71
|
+
const FS_MAGIC_NFS = 0x6969; // 26985 — NFS
|
|
72
|
+
const FS_MAGIC_SMB = 0x517b; // 20859 — older SMB_SUPER_MAGIC
|
|
73
|
+
const FS_MAGIC_CIFS = 0xff534d42; // 4283649346 — SMB/CIFS
|
|
74
|
+
const FS_MAGIC_SMB2 = 0xfe534d42; // 4267272514 — SMB2
|
|
75
|
+
const FS_MAGIC_FUSE = 0x65735546; // 1702057286 — FUSE (sshfs + many network FUSE mounts)
|
|
76
|
+
const NETWORK_FS_MAGICS = new Set([
|
|
77
|
+
FS_MAGIC_NFS,
|
|
78
|
+
FS_MAGIC_SMB,
|
|
79
|
+
FS_MAGIC_CIFS,
|
|
80
|
+
FS_MAGIC_SMB2,
|
|
81
|
+
// FUSE is a judgment call: it backs BOTH network mounts (sshfs) and local
|
|
82
|
+
// mounts (some encrypted/overlay FS). Treating it as network falls back to
|
|
83
|
+
// DELETE — conservative-but-safe (DELETE works everywhere; the only cost is
|
|
84
|
+
// losing WAL concurrency). An operator can always force WAL via the env var.
|
|
85
|
+
FS_MAGIC_FUSE,
|
|
86
|
+
]);
|
|
87
|
+
/**
|
|
88
|
+
* PURE classifier: is `fsType` a known network-filesystem magic number?
|
|
89
|
+
* Returns false for `undefined` (probe failed/unsupported) and for local
|
|
90
|
+
* magics (ext4 0xEF53, btrfs, xfs, tmpfs, apfs, …). Unit-testable with
|
|
91
|
+
* injected magic numbers — no real mount required.
|
|
92
|
+
*/
|
|
93
|
+
export function isNetworkFilesystem(fsType) {
|
|
94
|
+
if (fsType === undefined)
|
|
95
|
+
return false;
|
|
96
|
+
return NETWORK_FS_MAGICS.has(fsType);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Apply AKM's standard opening PRAGMAs to `db`, in order:
|
|
100
|
+
* 1. `journal_mode` = the configured mode (with WAL→DELETE network-FS fallback)
|
|
101
|
+
* 2. `busy_timeout = 30000`
|
|
102
|
+
* 3. `foreign_keys = ON` (unless `opts.foreignKeys === false`)
|
|
103
|
+
* 4. `synchronous = FULL` (only in a rollback-journal mode — DELETE/TRUNCATE)
|
|
104
|
+
*
|
|
105
|
+
* Returns the effective {@link JournalMode} so callers/tests can assert it.
|
|
106
|
+
*
|
|
107
|
+
* `synchronous = FULL` is set explicitly only in DELETE/TRUNCATE so durability
|
|
108
|
+
* intent is clear: rollback journals need FULL for crash-durability across
|
|
109
|
+
* power loss, whereas WAL is durable at NORMAL. SQLite's default synchronous is
|
|
110
|
+
* already FULL when unset, so this never changes WAL-default behaviour — the
|
|
111
|
+
* WAL path emits no `synchronous` pragma, exactly as before.
|
|
112
|
+
*/
|
|
113
|
+
export function applyStandardPragmas(db, opts = {}) {
|
|
114
|
+
let mode = resolveConfiguredJournalMode();
|
|
115
|
+
// Network-FS fallback only fires for the WAL default and only when we have a
|
|
116
|
+
// directory to probe. An explicitly-requested DELETE/TRUNCATE is never
|
|
117
|
+
// overridden, and a failed/unsupported probe (undefined) keeps WAL.
|
|
118
|
+
if (mode === "WAL" && opts.dataDir) {
|
|
119
|
+
const probe = opts.fsTypeProbe ?? statfsType;
|
|
120
|
+
if (isNetworkFilesystem(probe(opts.dataDir))) {
|
|
121
|
+
mode = "DELETE";
|
|
122
|
+
warnNetworkFallbackOnce(opts.dataDir);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// PRAGMAs must run before any DDL or DML. busy_timeout is applied FIRST so a
|
|
126
|
+
// journal-mode change that must reclaim a leftover `-wal` file (WAL→DELETE on
|
|
127
|
+
// reopen of an unclean WAL db, e.g. after a crash) can wait out a transient
|
|
128
|
+
// lock instead of failing immediately with SQLITE_BUSY. For the WAL default
|
|
129
|
+
// this is a no-op (WAL→WAL changes nothing), so byte-identical behaviour is
|
|
130
|
+
// preserved.
|
|
131
|
+
db.exec("PRAGMA busy_timeout = 30000");
|
|
132
|
+
db.exec(`PRAGMA journal_mode = ${mode}`);
|
|
133
|
+
if (opts.foreignKeys !== false) {
|
|
134
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
135
|
+
}
|
|
136
|
+
if (mode !== "WAL") {
|
|
137
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
138
|
+
}
|
|
139
|
+
return mode;
|
|
140
|
+
}
|
|
141
|
+
function warnNetworkFallbackOnce(dataDir) {
|
|
142
|
+
if (warnedNetworkFallback)
|
|
143
|
+
return;
|
|
144
|
+
warnedNetworkFallback = true;
|
|
145
|
+
warn(`[akm] network filesystem detected at ${dataDir} — WAL unsupported, using DELETE journal mode`);
|
|
146
|
+
}
|
package/dist/wiki/wiki.js
CHANGED
|
@@ -224,6 +224,43 @@ function readSchemaDescription(wikiDir) {
|
|
|
224
224
|
return undefined;
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Load a wiki's `schema.md` body and frontmatter.
|
|
229
|
+
*
|
|
230
|
+
* Unlike {@link readSchemaDescription} (which returns only the frontmatter
|
|
231
|
+
* `description` for `listWikis` summaries), this returns the markdown **body**
|
|
232
|
+
* — everything after the closing `---` of the frontmatter — which is where the
|
|
233
|
+
* page contract, operations, and hard rules live. The body is the rulebook
|
|
234
|
+
* injected into the write-time prompt for wiki-page edits.
|
|
235
|
+
*
|
|
236
|
+
* Swallow-and-degrade like the existing reader: a missing file, an unresolvable
|
|
237
|
+
* wiki dir, or malformed content yields `{ body: "", frontmatter: {} }`. Never
|
|
238
|
+
* throws.
|
|
239
|
+
*/
|
|
240
|
+
export function loadWikiSchema(stashRoot, name) {
|
|
241
|
+
const empty = { body: "", frontmatter: {} };
|
|
242
|
+
let wikiDir;
|
|
243
|
+
try {
|
|
244
|
+
wikiDir = resolveWikiDir(stashRoot, name);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return empty;
|
|
248
|
+
}
|
|
249
|
+
let raw;
|
|
250
|
+
try {
|
|
251
|
+
raw = fs.readFileSync(path.join(wikiDir, SCHEMA_MD), "utf8");
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return empty;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const parsed = parseFrontmatter(raw);
|
|
258
|
+
return { body: parsed.content, frontmatter: parsed.data };
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return empty;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
227
264
|
function toIsoDate(ms) {
|
|
228
265
|
return new Date(ms).toISOString();
|
|
229
266
|
}
|
package/dist/workflows/db.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { getWorkflowDbPath } from "../core/paths.js";
|
|
7
7
|
import { openDatabase } from "../storage/database.js";
|
|
8
8
|
import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
|
|
9
|
+
import { applyStandardPragmas } from "../storage/sqlite-pragmas.js";
|
|
9
10
|
/**
|
|
10
11
|
* workflow.db — Durable SQLite database for workflow run state.
|
|
11
12
|
*
|
|
@@ -46,12 +47,10 @@ export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
|
|
|
46
47
|
fs.mkdirSync(dir, { recursive: true });
|
|
47
48
|
}
|
|
48
49
|
const db = openDatabase(dbPath);
|
|
49
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
50
50
|
// #589: 30 s busy timeout, matching index.db / state.db. Without it the
|
|
51
51
|
// default is 0 ms, so any concurrent writer fails immediately with
|
|
52
|
-
// SQLITE_BUSY.
|
|
53
|
-
db
|
|
54
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
52
|
+
// SQLITE_BUSY. #628: journal_mode is configurable via AKM_SQLITE_JOURNAL_MODE.
|
|
53
|
+
applyStandardPragmas(db, { dataDir: dir });
|
|
55
54
|
ensureBaseSchema(db);
|
|
56
55
|
runMigrations(db);
|
|
57
56
|
return db;
|
|
@@ -16,14 +16,9 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module workflows/validate-summary
|
|
18
18
|
*/
|
|
19
|
+
import validateSummaryJudgePrompt from "../assets/prompts/validate-summary-judge.md" with { type: "text" };
|
|
19
20
|
import { parseJsonResponse } from "../core/parse.js";
|
|
20
|
-
const JUDGE_SYSTEM =
|
|
21
|
-
"Given a step's completion criteria and a summary of the work an agent claims to have done, " +
|
|
22
|
-
"judge whether the summary provides concrete evidence that EVERY criterion is satisfied. " +
|
|
23
|
-
"Be skeptical: vague, hand-wavy, or unsubstantiated claims do NOT satisfy a criterion. " +
|
|
24
|
-
'Respond with ONLY a JSON object: {"complete": boolean, "missing": string[], "feedback": string}. ' +
|
|
25
|
-
'"missing" lists the exact criteria that are not yet satisfied; "feedback" is a short directive ' +
|
|
26
|
-
"telling the agent what to finish or fix. No prose, no markdown fences.";
|
|
21
|
+
const JUDGE_SYSTEM = validateSummaryJudgePrompt;
|
|
27
22
|
function buildUserPrompt(input) {
|
|
28
23
|
const criteria = input.completionCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
29
24
|
return [
|
|
@@ -216,6 +216,7 @@ You can redirect any AKM directory to a custom path:
|
|
|
216
216
|
|---|---|
|
|
217
217
|
| `AKM_CONFIG_DIR` | Config directory (`~/.config/akm/`) |
|
|
218
218
|
| `AKM_DATA_DIR` | Data directory (`~/.local/share/akm/`) |
|
|
219
|
+
| `AKM_SQLITE_JOURNAL_MODE` | SQLite journal mode: `WAL` (default), `DELETE`, or `TRUNCATE`. Use `DELETE`/`TRUNCATE` on network filesystems (NFS/SMB) where WAL is impossible. When left at the `WAL` default, akm auto-detects a network FS for the data dir and falls back to `DELETE`. |
|
|
219
220
|
| `AKM_STATE_DIR` | State directory (`~/.local/state/akm/`) |
|
|
220
221
|
| `AKM_CACHE_DIR` | Cache directory (`~/.cache/akm/`) |
|
|
221
222
|
| `AKM_STASH_DIR` | Default stash directory (`~/akm/`) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.41",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|
|
@@ -51,16 +51,18 @@
|
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"preinstall": "node -e \"var ua=process.env.npm_config_user_agent||'';var major=parseInt((process.versions.node||'0').split('.')[0],10);if(process.versions.bun||ua.startsWith('bun/')||process.env.BUN_INSTALL||major>=20){process.exit(0)}console.error('\\n ERROR: akm-cli requires the Bun runtime (https://bun.sh), Node.js >= 20, or the prebuilt binary.\\n Install options:\\n 1. Bun: curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli\\n 2. Binary: curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\\n');process.exit(1)\"",
|
|
54
|
-
"build": "rm -rf dist && bun run tsc --project ./tsconfig.build.json && bun scripts/copy-assets.ts && bun scripts/fix-esm-extensions.ts",
|
|
54
|
+
"build": "rm -rf dist && bun scripts/gen-config-schema.ts &&bun run tsc --project ./tsconfig.build.json && bun scripts/copy-assets.ts && bun scripts/fix-esm-extensions.ts",
|
|
55
55
|
"check": "bun run lint && bunx tsc --noEmit && bun run test:unit && bun run test:integration",
|
|
56
56
|
"check:fast": "bun run lint && bunx tsc --noEmit && bun run test:unit",
|
|
57
57
|
"check:changed": "bun test tests/output-baseline.test.ts tests/integration/e2e.test.ts tests/stash-search.test.ts && bun run lint && bunx tsc --noEmit",
|
|
58
|
-
"
|
|
59
|
-
"test
|
|
60
|
-
"test:
|
|
58
|
+
"sweep:tmp": "bun scripts/sweep-test-tmp.ts",
|
|
59
|
+
"test": "bun run sweep:tmp && bun test --parallel=${TEST_PARALLEL:-1} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
|
|
60
|
+
"test:unit": "bun run sweep:tmp && bun test --parallel=${TEST_PARALLEL:-1} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
|
|
61
|
+
"test:integration": "bun run sweep:tmp && bun test --parallel=${TEST_PARALLEL:-1} --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows",
|
|
62
|
+
"test:unit:shard": "bun run sweep:tmp && bun test --parallel=1 --timeout=30000 ./tests --path-ignore-patterns=tests/integration --shard=${SHARD:?set SHARD=k/N}",
|
|
63
|
+
"test:integration:shard": "bun run sweep:tmp && bun test --parallel=1 --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows --shard=${SHARD:?set SHARD=k/N}",
|
|
61
64
|
"test:node-smoke": "bun scripts/node-smoke.ts",
|
|
62
65
|
"test:node-compat": "AKM_NODE_COMPAT_TESTS=1 bun test --timeout=120000 tests/integration/node-compat.test.ts",
|
|
63
|
-
"test:sharded": "bun test ./tests --shard=1/4 & bun test ./tests --shard=2/4 & bun test ./tests --shard=3/4 & bun test ./tests --shard=4/4 & wait",
|
|
64
66
|
"test:time": "bun scripts/test-timing-report.ts",
|
|
65
67
|
"lint:isolation": "bun scripts/lint-tests-isolation.ts",
|
|
66
68
|
"lint:devto-posts": "bun scripts/lint-devto-posts.ts",
|