akm-cli 0.9.0-beta.2 → 0.9.0-beta.27

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 (120) hide show
  1. package/CHANGELOG.md +660 -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 +5 -1
  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/templates/html/default.html +78 -0
  16. package/dist/assets/templates/html/health.html +730 -0
  17. package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
  18. package/dist/cli/shared.js +21 -5
  19. package/dist/cli.js +47 -5
  20. package/dist/commands/agent/contribute-cli.js +16 -3
  21. package/dist/commands/feedback-cli.js +15 -6
  22. package/dist/commands/graph/graph.js +75 -71
  23. package/dist/commands/health/checks.js +48 -0
  24. package/dist/commands/health/html-report.js +790 -0
  25. package/dist/commands/health.js +478 -15
  26. package/dist/commands/improve/calibration.js +161 -0
  27. package/dist/commands/improve/consolidate.js +634 -111
  28. package/dist/commands/improve/dedup.js +482 -0
  29. package/dist/commands/improve/distill.js +145 -69
  30. package/dist/commands/improve/encoding-salience.js +205 -0
  31. package/dist/commands/improve/extract-cli.js +115 -1
  32. package/dist/commands/improve/extract-prompt.js +33 -2
  33. package/dist/commands/improve/extract-watch.js +140 -0
  34. package/dist/commands/improve/extract.js +280 -35
  35. package/dist/commands/improve/feedback-valence.js +54 -0
  36. package/dist/commands/improve/homeostatic.js +467 -0
  37. package/dist/commands/improve/improve-auto-accept.js +139 -6
  38. package/dist/commands/improve/improve-profiles.js +12 -0
  39. package/dist/commands/improve/improve.js +2079 -608
  40. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  41. package/dist/commands/improve/outcome-loop.js +256 -0
  42. package/dist/commands/improve/proactive-maintenance.js +87 -0
  43. package/dist/commands/improve/procedural.js +409 -0
  44. package/dist/commands/improve/recombine.js +488 -0
  45. package/dist/commands/improve/reflect-noise.js +0 -0
  46. package/dist/commands/improve/reflect.js +51 -1
  47. package/dist/commands/improve/related-sessions.js +120 -0
  48. package/dist/commands/improve/salience.js +386 -0
  49. package/dist/commands/improve/triage.js +95 -0
  50. package/dist/commands/lint/agent-linter.js +19 -24
  51. package/dist/commands/lint/base-linter.js +173 -60
  52. package/dist/commands/lint/command-linter.js +19 -24
  53. package/dist/commands/lint/env-key-rules.js +34 -1
  54. package/dist/commands/lint/fact-linter.js +39 -0
  55. package/dist/commands/lint/index.js +31 -13
  56. package/dist/commands/lint/memory-linter.js +1 -1
  57. package/dist/commands/lint/registry.js +7 -2
  58. package/dist/commands/lint/task-linter.js +3 -3
  59. package/dist/commands/lint/workflow-linter.js +26 -1
  60. package/dist/commands/proposal/drain.js +73 -6
  61. package/dist/commands/proposal/proposal-cli.js +22 -10
  62. package/dist/commands/proposal/proposal.js +17 -1
  63. package/dist/commands/proposal/validators/proposals.js +369 -329
  64. package/dist/commands/read/curate.js +344 -80
  65. package/dist/commands/read/search-cli.js +7 -0
  66. package/dist/commands/read/search.js +1 -0
  67. package/dist/commands/read/show.js +67 -2
  68. package/dist/commands/remember.js +6 -2
  69. package/dist/commands/sources/installed-stashes.js +5 -1
  70. package/dist/commands/sources/stash-cli.js +10 -2
  71. package/dist/core/asset/asset-registry.js +2 -0
  72. package/dist/core/asset/asset-spec.js +14 -0
  73. package/dist/core/asset/frontmatter.js +166 -167
  74. package/dist/core/asset/markdown.js +8 -0
  75. package/dist/core/config/config-schema.js +255 -2
  76. package/dist/core/config/config.js +2 -2
  77. package/dist/core/logs-db.js +305 -0
  78. package/dist/core/paths.js +3 -0
  79. package/dist/core/state-db.js +706 -42
  80. package/dist/indexer/db/db.js +364 -38
  81. package/dist/indexer/db/graph-db.js +129 -86
  82. package/dist/indexer/ensure-index.js +152 -17
  83. package/dist/indexer/graph/graph-boost.js +51 -41
  84. package/dist/indexer/graph/graph-extraction.js +203 -3
  85. package/dist/indexer/index-writer-lock.js +99 -0
  86. package/dist/indexer/indexer.js +114 -111
  87. package/dist/indexer/passes/memory-inference.js +71 -25
  88. package/dist/indexer/passes/staleness-detect.js +2 -5
  89. package/dist/indexer/search/db-search.js +15 -4
  90. package/dist/indexer/search/ranking-contributors.js +22 -0
  91. package/dist/indexer/search/ranking.js +4 -0
  92. package/dist/indexer/walk/matchers.js +9 -0
  93. package/dist/integrations/agent/prompts.js +1 -0
  94. package/dist/integrations/harnesses/claude/session-log.js +27 -5
  95. package/dist/integrations/harnesses/opencode/session-log.js +9 -0
  96. package/dist/integrations/session-logs/index.js +16 -0
  97. package/dist/llm/client.js +38 -4
  98. package/dist/llm/embedder.js +27 -3
  99. package/dist/llm/embedders/local.js +66 -2
  100. package/dist/llm/graph-extract.js +2 -1
  101. package/dist/llm/memory-infer.js +4 -8
  102. package/dist/llm/metadata-enhance.js +9 -1
  103. package/dist/llm/usage-persist.js +77 -0
  104. package/dist/llm/usage-telemetry.js +103 -0
  105. package/dist/output/context.js +3 -2
  106. package/dist/output/html-render.js +73 -0
  107. package/dist/output/renderers.js +73 -1
  108. package/dist/output/shapes/curate.js +14 -2
  109. package/dist/output/shapes/helpers.js +17 -1
  110. package/dist/output/text/helpers.js +78 -1
  111. package/dist/runtime.js +25 -1
  112. package/dist/scripts/migrate-storage.js +1262 -591
  113. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +485 -270
  114. package/dist/sources/providers/tar-utils.js +16 -8
  115. package/dist/storage/sqlite-pragmas.js +146 -0
  116. package/dist/tasks/runner.js +99 -16
  117. package/dist/workflows/db.js +5 -2
  118. package/dist/workflows/validate-summary.js +2 -7
  119. package/docs/data-and-telemetry.md +1 -0
  120. package/package.json +9 -6
@@ -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
+ }
@@ -16,7 +16,9 @@
16
16
  * 4. Dispatch by target kind:
17
17
  * • workflow → `startWorkflowRun(ref, params)`
18
18
  * • prompt → `runAgent(profile, prompt, { stdio: "captured" })`
19
- * 5. Capture stdout / stderr to `<cacheDir>/tasks/logs/<id>/<ts>.log`.
19
+ * 5. Capture stdout / stderr as structured rows in logs.db (task_logs) and,
20
+ * transitionally, as a flat text tail at `<cacheDir>/tasks/logs/<id>/<ts>.log`
21
+ * (see docs/technical/logs-audit.md).
20
22
  * 6. Write a history row to state.db task_history table.
21
23
  *
22
24
  * Returns a structured result so the CLI handler can shape it for `output()`
@@ -29,6 +31,7 @@ import { parseAssetRef } from "../core/asset/asset-ref.js";
29
31
  import { resolveStashDir } from "../core/common.js";
30
32
  import { loadConfig } from "../core/config/config.js";
31
33
  import { NotFoundError, rethrowIfTestIsolationError } from "../core/errors.js";
34
+ import { buildTaskRunId, insertTaskLogLines, openLogsDatabase, } from "../core/logs-db.js";
32
35
  import { getTaskLogDir } from "../core/paths.js";
33
36
  import { getTaskHistory, openStateDatabase, queryTaskHistory, upsertTaskHistory } from "../core/state-db.js";
34
37
  import { error } from "../core/warn.js";
@@ -70,7 +73,15 @@ export async function runTask(id, options = {}) {
70
73
  log: logPath,
71
74
  target: disabledTarget,
72
75
  };
73
- fs.writeFileSync(logPath, `[akm tasks] task "${id}" is disabled — skipping run.\n`);
76
+ const disabledLine = `[akm tasks] task "${id}" is disabled — skipping run.`;
77
+ persistRunLog({
78
+ taskId: id,
79
+ startedAtIso: startedIso,
80
+ finishedAtIso: result.finishedAt,
81
+ logPath,
82
+ fileText: `${disabledLine}\n`,
83
+ dbLines: [{ line: disabledLine }],
84
+ });
74
85
  appendHistory(result);
75
86
  return result;
76
87
  }
@@ -108,7 +119,9 @@ async function runCommandTask(input) {
108
119
  throw new Error("invariant: command target");
109
120
  const { cmd } = task.target;
110
121
  const timeoutMs = task.timeoutMs !== undefined ? task.timeoutMs : null;
111
- const logLines = [`[akm tasks] task=${task.id} kind=command cmd=${cmd.join(" ")}`];
122
+ const header = `[akm tasks] task=${task.id} kind=command cmd=${cmd.join(" ")}`;
123
+ const logLines = [header];
124
+ const dbLines = [{ line: header }];
112
125
  let stdout = "";
113
126
  let stderr = "";
114
127
  let exitCode = null;
@@ -144,24 +157,36 @@ async function runCommandTask(input) {
144
157
  exitCode = proc.exitCode ?? (timedOut ? 143 : 1);
145
158
  if (timedOut) {
146
159
  logLines.push(`timed_out=true timeout_ms=${timeoutMs}`);
160
+ dbLines.push({ level: "error", line: `timed_out=true timeout_ms=${timeoutMs}` });
147
161
  }
148
162
  logLines.push(`exit_code=${exitCode}`);
163
+ dbLines.push({ level: exitCode === 0 ? "info" : "error", line: `exit_code=${exitCode}` });
149
164
  if (stdout) {
150
165
  logLines.push("--- stdout ---");
151
166
  logLines.push(stdout);
167
+ dbLines.push(...streamLines(stdout, "stdout", "info"));
152
168
  }
153
169
  if (stderr) {
154
170
  logLines.push("--- stderr ---");
155
171
  logLines.push(stderr);
172
+ dbLines.push(...streamLines(stderr, "stderr", "error"));
156
173
  }
157
174
  }
158
175
  catch (e) {
159
176
  const msg = e instanceof Error ? e.message : String(e);
160
177
  logLines.push(`spawn_error=${msg}`);
178
+ dbLines.push({ level: "error", line: `spawn_error=${msg}` });
161
179
  exitCode = 1;
162
180
  }
163
- fs.writeFileSync(logPath, `${logLines.join("\n")}\n`);
164
181
  const finishedAt = now();
182
+ persistRunLog({
183
+ taskId: task.id,
184
+ startedAtIso: startedAt.toISOString(),
185
+ finishedAtIso: finishedAt.toISOString(),
186
+ logPath,
187
+ fileText: `${logLines.join("\n")}\n`,
188
+ dbLines,
189
+ });
165
190
  const status = exitCode === 0 ? "completed" : "failed";
166
191
  const result = {
167
192
  id: task.id,
@@ -196,7 +221,14 @@ async function runWorkflowTask(input) {
196
221
  const finishedAt = now();
197
222
  const status = error ? "failed" : mapWorkflowStatus(detail?.run.status);
198
223
  const log = renderWorkflowLog({ task, detail, error });
199
- fs.writeFileSync(logPath, log);
224
+ persistRunLog({
225
+ taskId: task.id,
226
+ startedAtIso: startedAt.toISOString(),
227
+ finishedAtIso: finishedAt.toISOString(),
228
+ logPath,
229
+ fileText: log.fileText,
230
+ dbLines: log.dbLines,
231
+ });
200
232
  const result = {
201
233
  id: task.id,
202
234
  status,
@@ -248,16 +280,17 @@ function mapWorkflowStatus(status) {
248
280
  }
249
281
  }
250
282
  function renderWorkflowLog(input) {
251
- const lines = [];
252
- lines.push(`[akm tasks] task=${input.task.id} kind=workflow ref=${input.task.target.ref}`);
283
+ const dbLines = [
284
+ { line: `[akm tasks] task=${input.task.id} kind=workflow ref=${input.task.target.ref}` },
285
+ ];
253
286
  if (input.detail) {
254
- lines.push(`run_id=${input.detail.run.id} status=${input.detail.run.status}`);
255
- lines.push(`workflow_title=${input.detail.run.workflowTitle}`);
287
+ dbLines.push({ line: `run_id=${input.detail.run.id} status=${input.detail.run.status}` });
288
+ dbLines.push({ line: `workflow_title=${input.detail.run.workflowTitle}` });
256
289
  }
257
290
  if (input.error) {
258
- lines.push(`error=${input.error.message}`);
291
+ dbLines.push({ level: "error", line: `error=${input.error.message}` });
259
292
  }
260
- return `${lines.join("\n")}\n`;
293
+ return { fileText: `${dbLines.map((entry) => entry.line).join("\n")}\n`, dbLines };
261
294
  }
262
295
  // ── prompt target ───────────────────────────────────────────────────────────
263
296
  async function runPromptTask(input) {
@@ -321,7 +354,14 @@ async function runPromptTask(input) {
321
354
  });
322
355
  const finishedAt = now();
323
356
  const log = renderPromptLog({ task, profileName: profile.name, result });
324
- fs.writeFileSync(logPath, log);
357
+ persistRunLog({
358
+ taskId: task.id,
359
+ startedAtIso: startedAt.toISOString(),
360
+ finishedAtIso: finishedAt.toISOString(),
361
+ logPath,
362
+ fileText: log.fileText,
363
+ dbLines: log.dbLines,
364
+ });
325
365
  const status = result.ok ? "completed" : "failed";
326
366
  const out = {
327
367
  id: task.id,
@@ -359,20 +399,63 @@ async function resolvePromptText(task, stashDir) {
359
399
  }
360
400
  function renderPromptLog(input) {
361
401
  const lines = [];
362
- lines.push(`[akm tasks] task=${input.task.id} kind=prompt profile=${input.profileName}`);
363
- lines.push(`ok=${input.result.ok} exit_code=${input.result.exitCode ?? "null"} duration_ms=${input.result.durationMs}`);
402
+ const dbLines = [];
403
+ const header = `[akm tasks] task=${input.task.id} kind=prompt profile=${input.profileName}`;
404
+ const summary = `ok=${input.result.ok} exit_code=${input.result.exitCode ?? "null"} duration_ms=${input.result.durationMs}`;
405
+ lines.push(header, summary);
406
+ dbLines.push({ line: header }, { level: input.result.ok ? "info" : "error", line: summary });
364
407
  if (!input.result.ok) {
365
- lines.push(`reason=${input.result.reason ?? ""} error=${input.result.error ?? ""}`);
408
+ const failure = `reason=${input.result.reason ?? ""} error=${input.result.error ?? ""}`;
409
+ lines.push(failure);
410
+ dbLines.push({ level: "error", line: failure });
366
411
  }
367
412
  if (input.result.stdout) {
368
413
  lines.push("--- agent stdout ---");
369
414
  lines.push(input.result.stdout);
415
+ dbLines.push(...streamLines(input.result.stdout, "stdout", "info"));
370
416
  }
371
417
  if (input.result.stderr) {
372
418
  lines.push("--- agent stderr ---");
373
419
  lines.push(input.result.stderr);
420
+ dbLines.push(...streamLines(input.result.stderr, "stderr", "error"));
421
+ }
422
+ return { fileText: `${lines.join("\n")}\n`, dbLines };
423
+ }
424
+ /** Split captured pipe output into per-line logs.db rows (blank lines dropped). */
425
+ function streamLines(text, stream, level) {
426
+ return text
427
+ .split("\n")
428
+ .filter((line) => line.length > 0)
429
+ .map((line) => ({ stream, level, line }));
430
+ }
431
+ /**
432
+ * Persist a finished run's log: the flat text file (so `log_path` in
433
+ * task_history keeps resolving for humans and older consumers) plus
434
+ * structured rows in logs.db keyed by `buildTaskRunId(taskId, startedAt)`.
435
+ *
436
+ * The DB write is best-effort, mirroring {@link appendHistory}: an unwritable
437
+ * logs.db must never fail a task run.
438
+ */
439
+ function persistRunLog(input) {
440
+ fs.writeFileSync(input.logPath, input.fileText);
441
+ try {
442
+ const db = openLogsDatabase();
443
+ try {
444
+ insertTaskLogLines(db, {
445
+ taskId: input.taskId,
446
+ runId: buildTaskRunId(input.taskId, input.startedAtIso),
447
+ ts: input.finishedAtIso,
448
+ lines: input.dbLines,
449
+ });
450
+ }
451
+ finally {
452
+ db.close();
453
+ }
454
+ }
455
+ catch (err) {
456
+ rethrowIfTestIsolationError(err);
457
+ error(`[akm] task log DB write failed: ${String(err)}`);
374
458
  }
375
- return `${lines.join("\n")}\n`;
376
459
  }
377
460
  // ── history ─────────────────────────────────────────────────────────────────
378
461
  function appendHistory(result) {
@@ -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,8 +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
- db.exec("PRAGMA foreign_keys = ON");
50
+ // #589: 30 s busy timeout, matching index.db / state.db. Without it the
51
+ // default is 0 ms, so any concurrent writer fails immediately with
52
+ // SQLITE_BUSY. #628: journal_mode is configurable via AKM_SQLITE_JOURNAL_MODE.
53
+ applyStandardPragmas(db, { dataDir: dir });
51
54
  ensureBaseSchema(db);
52
55
  runMigrations(db);
53
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.2",
3
+ "version": "0.9.0-beta.27",
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,15 +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
- "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",
65
+ "test:node-compat": "AKM_NODE_COMPAT_TESTS=1 bun test --timeout=120000 tests/integration/node-compat.test.ts",
63
66
  "test:time": "bun scripts/test-timing-report.ts",
64
67
  "lint:isolation": "bun scripts/lint-tests-isolation.ts",
65
68
  "lint:devto-posts": "bun scripts/lint-devto-posts.ts",