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.
Files changed (132) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/dist/assets/prompts/consolidate-system.md +23 -0
  3. package/dist/assets/prompts/contradiction-judge.md +33 -0
  4. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  5. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  6. package/dist/assets/prompts/extract-session.md +6 -2
  7. package/dist/assets/prompts/graph-extract-system.md +1 -0
  8. package/dist/assets/prompts/memory-infer-system.md +1 -0
  9. package/dist/assets/prompts/memory-infer-user.md +5 -0
  10. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  11. package/dist/assets/prompts/procedural-system.md +44 -0
  12. package/dist/assets/prompts/recombine-system.md +40 -0
  13. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  14. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +22 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +22 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +24 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +22 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +25 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +21 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +21 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +23 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +22 -0
  24. package/dist/assets/templates/html/health.html +281 -111
  25. package/dist/cli.js +14 -3
  26. package/dist/commands/agent/contribute-cli.js +16 -3
  27. package/dist/commands/feedback-cli.js +15 -6
  28. package/dist/commands/graph/graph.js +75 -71
  29. package/dist/commands/health/checks.js +48 -0
  30. package/dist/commands/health/html-report.js +422 -80
  31. package/dist/commands/health.js +381 -9
  32. package/dist/commands/improve/calibration.js +161 -0
  33. package/dist/commands/improve/consolidate.js +631 -111
  34. package/dist/commands/improve/dedup.js +482 -0
  35. package/dist/commands/improve/distill.js +163 -69
  36. package/dist/commands/improve/encoding-salience.js +205 -0
  37. package/dist/commands/improve/extract-cli.js +115 -1
  38. package/dist/commands/improve/extract-prompt.js +39 -2
  39. package/dist/commands/improve/extract-watch.js +140 -0
  40. package/dist/commands/improve/extract.js +403 -40
  41. package/dist/commands/improve/feedback-valence.js +54 -0
  42. package/dist/commands/improve/homeostatic.js +467 -0
  43. package/dist/commands/improve/improve-auto-accept.js +113 -6
  44. package/dist/commands/improve/improve-profiles.js +12 -0
  45. package/dist/commands/improve/improve.js +2042 -612
  46. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  47. package/dist/commands/improve/outcome-loop.js +256 -0
  48. package/dist/commands/improve/proactive-maintenance.js +115 -0
  49. package/dist/commands/improve/procedural.js +418 -0
  50. package/dist/commands/improve/recombine.js +602 -0
  51. package/dist/commands/improve/reflect-noise.js +0 -0
  52. package/dist/commands/improve/reflect.js +46 -4
  53. package/dist/commands/improve/related-sessions.js +120 -0
  54. package/dist/commands/improve/salience.js +438 -0
  55. package/dist/commands/improve/triage.js +93 -0
  56. package/dist/commands/lint/agent-linter.js +19 -24
  57. package/dist/commands/lint/base-linter.js +173 -60
  58. package/dist/commands/lint/command-linter.js +19 -24
  59. package/dist/commands/lint/env-key-rules.js +34 -1
  60. package/dist/commands/lint/fact-linter.js +39 -0
  61. package/dist/commands/lint/index.js +31 -13
  62. package/dist/commands/lint/memory-linter.js +1 -1
  63. package/dist/commands/lint/registry.js +7 -2
  64. package/dist/commands/lint/task-linter.js +3 -3
  65. package/dist/commands/lint/workflow-linter.js +26 -1
  66. package/dist/commands/proposal/drain-policies.js +5 -0
  67. package/dist/commands/proposal/drain.js +17 -1
  68. package/dist/commands/proposal/proposal.js +5 -0
  69. package/dist/commands/proposal/propose.js +5 -0
  70. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  71. package/dist/commands/proposal/validators/proposals.js +187 -57
  72. package/dist/commands/read/curate.js +344 -80
  73. package/dist/commands/read/search-cli.js +7 -0
  74. package/dist/commands/read/search.js +1 -0
  75. package/dist/commands/read/show.js +67 -2
  76. package/dist/commands/sources/init.js +36 -9
  77. package/dist/commands/sources/installed-stashes.js +5 -1
  78. package/dist/commands/sources/schema-repair.js +13 -1
  79. package/dist/commands/sources/stash-cli.js +19 -3
  80. package/dist/commands/sources/stash-skeleton.js +23 -8
  81. package/dist/core/asset/asset-registry.js +2 -0
  82. package/dist/core/asset/asset-spec.js +14 -0
  83. package/dist/core/asset/frontmatter.js +166 -167
  84. package/dist/core/asset/markdown.js +8 -0
  85. package/dist/core/authoring-rules.js +83 -0
  86. package/dist/core/config/config-schema.js +274 -2
  87. package/dist/core/config/config.js +2 -2
  88. package/dist/core/logs-db.js +4 -3
  89. package/dist/core/paths.js +3 -0
  90. package/dist/core/standards/resolve-standards-context.js +87 -0
  91. package/dist/core/standards/resolve-stash-standards.js +99 -0
  92. package/dist/core/standards/resolve-type-conventions.js +66 -0
  93. package/dist/core/state-db.js +691 -30
  94. package/dist/indexer/db/db.js +364 -38
  95. package/dist/indexer/db/graph-db.js +129 -86
  96. package/dist/indexer/ensure-index.js +152 -17
  97. package/dist/indexer/graph/graph-boost.js +51 -41
  98. package/dist/indexer/graph/graph-extraction.js +203 -3
  99. package/dist/indexer/index-writer-lock.js +99 -0
  100. package/dist/indexer/indexer.js +114 -111
  101. package/dist/indexer/passes/memory-inference.js +10 -3
  102. package/dist/indexer/passes/staleness-detect.js +2 -5
  103. package/dist/indexer/search/db-search.js +15 -4
  104. package/dist/indexer/search/ranking-contributors.js +22 -0
  105. package/dist/indexer/search/ranking.js +4 -0
  106. package/dist/indexer/walk/matchers.js +9 -0
  107. package/dist/integrations/agent/prompts.js +33 -0
  108. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  109. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  110. package/dist/integrations/session-logs/index.js +16 -0
  111. package/dist/llm/client.js +23 -4
  112. package/dist/llm/embedder.js +27 -3
  113. package/dist/llm/embedders/local.js +66 -2
  114. package/dist/llm/feature-gate.js +8 -4
  115. package/dist/llm/graph-extract.js +2 -1
  116. package/dist/llm/memory-infer.js +4 -8
  117. package/dist/llm/metadata-enhance.js +9 -1
  118. package/dist/output/renderers.js +73 -1
  119. package/dist/output/shapes/curate.js +14 -2
  120. package/dist/output/text/helpers.js +16 -1
  121. package/dist/runtime.js +25 -1
  122. package/dist/scripts/migrate-storage.js +1378 -599
  123. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +479 -270
  124. package/dist/setup/setup.js +3 -3
  125. package/dist/sources/providers/git.js +71 -61
  126. package/dist/sources/providers/tar-utils.js +16 -8
  127. package/dist/storage/sqlite-pragmas.js +146 -0
  128. package/dist/wiki/wiki.js +37 -0
  129. package/dist/workflows/db.js +3 -4
  130. package/dist/workflows/validate-summary.js +2 -7
  131. package/docs/data-and-telemetry.md +1 -0
  132. package/package.json +8 -6
@@ -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
- // Safety check (#476): when the stash dir is shared with a non-akm project
483
- // (stash root == project repo root), `git add -A` would stage every dirty
484
- // file in the user's working tree and push their unrelated WIP to the
485
- // stash's remote. Refuse if any dirty path is outside the known akm-
486
- // managed subtrees (TYPE_DIRS + `.akm/` state).
487
- const nonAkmDirty = collectNonAkmDirtyPaths(statusResult.stdout);
488
- if (nonAkmDirty.length > 0) {
489
- const sample = nonAkmDirty.slice(0, 10);
490
- const more = nonAkmDirty.length > sample.length ? `\n ...and ${nonAkmDirty.length - sample.length} more` : "";
491
- throw new Error(`refusing to push: stash repo at ${repoDir} has uncommitted non-akm changes:\n` +
492
- sample.map((p) => ` ${p}`).join("\n") +
493
- more +
494
- `\nCommit or stash these manually before running an akm push. ` +
495
- `Akm-managed paths are: ${Object.values(TYPE_DIRS).join(", ")}, .akm/`);
496
- }
497
- // Stage and commit — supply fallback identity so fresh environments without
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 { collectNonAkmDirtyPaths, ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
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", ["tzf", archivePath], { encoding: "utf8" });
69
- if (listResult.status !== 0) {
70
- const err = listResult.stderr?.trim() || listResult.error?.message || "unknown error";
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("tar", ["xzf", archivePath, "--no-same-owner", "--strip-components=1", "-C", destinationDir], { encoding: "utf8" });
77
- if (extractResult.status !== 0) {
78
- const err = extractResult.stderr?.trim() || extractResult.error?.message || "unknown error";
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
  }
@@ -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.exec("PRAGMA busy_timeout = 30000");
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 = "You are a strict completion auditor for a software workflow engine. " +
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.4",
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
- "test": "bun test --parallel=${TEST_PARALLEL:-12} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
59
- "test:unit": "bun test --parallel=${TEST_PARALLEL:-12} --timeout=30000 ./tests --path-ignore-patterns=tests/integration",
60
- "test:integration": "bun test --parallel=${TEST_PARALLEL:-12} --timeout=30000 ./tests/integration ./tests/commands ./tests/workflows",
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",