akm-cli 0.9.0-beta.1 → 0.9.0-beta.3

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 (41) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/assets/templates/html/default.html +78 -0
  3. package/dist/assets/templates/html/health.html +560 -0
  4. package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
  5. package/dist/cli/shared.js +21 -5
  6. package/dist/cli.js +36 -5
  7. package/dist/commands/config-cli.js +0 -10
  8. package/dist/commands/health/html-report.js +448 -0
  9. package/dist/commands/health.js +97 -6
  10. package/dist/commands/improve/extract.js +38 -2
  11. package/dist/commands/improve/improve-auto-accept.js +27 -1
  12. package/dist/commands/improve/improve-cli.js +7 -0
  13. package/dist/commands/improve/improve.js +201 -66
  14. package/dist/commands/improve/reflect-noise.js +0 -0
  15. package/dist/commands/improve/reflect.js +25 -0
  16. package/dist/commands/proposal/drain.js +73 -6
  17. package/dist/commands/proposal/proposal-cli.js +22 -10
  18. package/dist/commands/proposal/proposal.js +12 -1
  19. package/dist/commands/proposal/validators/proposals.js +361 -338
  20. package/dist/commands/remember.js +6 -2
  21. package/dist/commands/tasks/tasks.js +32 -8
  22. package/dist/core/config/config-schema.js +5 -0
  23. package/dist/core/logs-db.js +304 -0
  24. package/dist/core/state-db.js +107 -14
  25. package/dist/indexer/db/db.js +2 -2
  26. package/dist/indexer/passes/memory-inference.js +61 -22
  27. package/dist/integrations/harnesses/claude/session-log.js +16 -4
  28. package/dist/llm/client.js +15 -0
  29. package/dist/llm/usage-persist.js +77 -0
  30. package/dist/llm/usage-telemetry.js +103 -0
  31. package/dist/output/context.js +3 -2
  32. package/dist/output/html-render.js +73 -0
  33. package/dist/output/shapes/helpers.js +17 -1
  34. package/dist/output/text/helpers.js +69 -1
  35. package/dist/scripts/migrate-storage.js +65 -14
  36. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +14 -2
  37. package/dist/tasks/backends/cron.js +46 -9
  38. package/dist/tasks/runner.js +99 -16
  39. package/dist/workflows/db.js +4 -0
  40. package/package.json +1 -1
  41. package/dist/commands/config-edit.js +0 -344
@@ -8888,6 +8888,7 @@ __export(exports_state_db, {
8888
8888
  shouldSkipAlreadyExtractedSession: () => shouldSkipAlreadyExtractedSession,
8889
8889
  runMigrations: () => runMigrations2,
8890
8890
  recordImproveRun: () => recordImproveRun,
8891
+ recordFsProposalsImport: () => recordFsProposalsImport,
8891
8892
  readStateEvents: () => readStateEvents,
8892
8893
  queryTaskHistory: () => queryTaskHistory,
8893
8894
  queryImproveRuns: () => queryImproveRuns,
@@ -8898,9 +8899,12 @@ __export(exports_state_db, {
8898
8899
  proposalRowToProposal: () => proposalRowToProposal,
8899
8900
  openStateDatabase: () => openStateDatabase,
8900
8901
  listStateProposals: () => listStateProposals,
8902
+ listStateProposalIdsByPrefix: () => listStateProposalIdsByPrefix,
8901
8903
  listExistingTableNames: () => listExistingTableNames,
8904
+ insertProposalIfAbsent: () => insertProposalIfAbsent,
8902
8905
  insertEvent: () => insertEvent,
8903
8906
  importEventsJsonl: () => importEventsJsonl,
8907
+ hasImportedFsProposals: () => hasImportedFsProposals,
8904
8908
  getTaskHistoryRuns: () => getTaskHistoryRuns,
8905
8909
  getTaskHistory: () => getTaskHistory,
8906
8910
  getStateProposal: () => getStateProposal,
@@ -8925,7 +8929,7 @@ function openStateDatabase(dbPath) {
8925
8929
  const db = openDatabase(resolvedPath);
8926
8930
  db.exec("PRAGMA journal_mode = WAL");
8927
8931
  db.exec("PRAGMA foreign_keys = ON");
8928
- db.exec("PRAGMA busy_timeout = 5000");
8932
+ db.exec("PRAGMA busy_timeout = 30000");
8929
8933
  runMigrations2(db);
8930
8934
  return db;
8931
8935
  }
@@ -8972,7 +8976,10 @@ function proposalRowToProposal(row) {
8972
8976
  content: row.content,
8973
8977
  ...frontmatter !== undefined ? { frontmatter } : {}
8974
8978
  },
8975
- ...meta.review !== undefined ? { review: meta.review } : {}
8979
+ ...meta.review !== undefined ? { review: meta.review } : {},
8980
+ ...typeof meta.confidence === "number" ? { confidence: meta.confidence } : {},
8981
+ ...meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {},
8982
+ ...typeof meta.backupContent === "string" ? { backupContent: meta.backupContent } : {}
8976
8983
  };
8977
8984
  }
8978
8985
  function proposalToRowValues(proposal, stashDir) {
@@ -8981,6 +8988,12 @@ function proposalToRowValues(proposal, stashDir) {
8981
8988
  metaObj.sourceRun = proposal.sourceRun;
8982
8989
  if (proposal.review !== undefined)
8983
8990
  metaObj.review = proposal.review;
8991
+ if (proposal.confidence !== undefined)
8992
+ metaObj.confidence = proposal.confidence;
8993
+ if (proposal.gateDecision !== undefined)
8994
+ metaObj.gateDecision = proposal.gateDecision;
8995
+ if (proposal.backupContent !== undefined)
8996
+ metaObj.backupContent = proposal.backupContent;
8984
8997
  return {
8985
8998
  id: proposal.id,
8986
8999
  stash_dir: stashDir,
@@ -9074,15 +9087,39 @@ function listStateProposals(db, options = {}) {
9074
9087
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
9075
9088
  const rows = db.prepare(`SELECT id, stash_dir, ref, status, source, created_at, updated_at,
9076
9089
  content, frontmatter_json, metadata_json
9077
- FROM proposals ${where} ORDER BY created_at ASC`).all(...params);
9090
+ FROM proposals ${where} ORDER BY created_at ASC, rowid ASC`).all(...params);
9078
9091
  return rows.map(proposalRowToProposal);
9079
9092
  }
9080
- function getStateProposal(db, id) {
9081
- const row = db.prepare(`SELECT id, stash_dir, ref, status, source, created_at, updated_at,
9093
+ function getStateProposal(db, id, stashDir) {
9094
+ const sql = `SELECT id, stash_dir, ref, status, source, created_at, updated_at,
9082
9095
  content, frontmatter_json, metadata_json
9083
- FROM proposals WHERE id = ?`).get(id);
9096
+ FROM proposals WHERE id = ?${stashDir ? " AND stash_dir = ?" : ""}`;
9097
+ const row = stashDir ? db.prepare(sql).get(id, stashDir) : db.prepare(sql).get(id);
9084
9098
  return row ? proposalRowToProposal(row) : undefined;
9085
9099
  }
9100
+ function listStateProposalIdsByPrefix(db, stashDir, idPrefix) {
9101
+ const escaped = idPrefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
9102
+ const rows = db.prepare(`SELECT id FROM proposals
9103
+ WHERE stash_dir = ? AND status = 'pending' AND id LIKE ? ESCAPE '\\'
9104
+ ORDER BY id ASC`).all(stashDir, `${escaped}%`);
9105
+ return rows.map((r) => r.id);
9106
+ }
9107
+ function hasImportedFsProposals(db, stashDir) {
9108
+ return Boolean(db.prepare("SELECT 1 FROM proposal_fs_imports WHERE stash_dir = ?").get(stashDir));
9109
+ }
9110
+ function recordFsProposalsImport(db, stashDir, importedCount) {
9111
+ db.prepare("INSERT OR REPLACE INTO proposal_fs_imports (stash_dir, imported_at, imported_count) VALUES (?, ?, ?)").run(stashDir, new Date().toISOString(), importedCount);
9112
+ }
9113
+ function insertProposalIfAbsent(db, proposal, stashDir) {
9114
+ const v = proposalToRowValues(proposal, stashDir);
9115
+ const result = db.prepare(`
9116
+ INSERT OR IGNORE INTO proposals
9117
+ (id, stash_dir, ref, status, source, created_at, updated_at, content, frontmatter_json, metadata_json)
9118
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
9119
+ `).run(v.id, v.stash_dir, v.ref, v.status, v.source, v.created_at, v.updated_at, v.content, v.frontmatter_json, v.metadata_json);
9120
+ const changes = result.changes ?? 0;
9121
+ return Number(changes) > 0;
9122
+ }
9086
9123
  function upsertTaskHistory(db, row) {
9087
9124
  db.prepare(`
9088
9125
  INSERT OR IGNORE INTO task_history
@@ -9363,7 +9400,9 @@ var init_state_db = __esm(() => {
9363
9400
  --
9364
9401
  -- Extensible (metadata_json) columns:
9365
9402
  -- metadata_json TEXT \u2014 JSON object for future proposal fields.
9366
- -- Current fields stored here: sourceRun, review.
9403
+ -- Current fields stored here: sourceRun,
9404
+ -- review, confidence, gateDecision (#577),
9405
+ -- backupContent.
9367
9406
  --
9368
9407
  -- ADD COLUMN extension points (future migrations):
9369
9408
  -- ALTER TABLE proposals ADD COLUMN source_run TEXT DEFAULT NULL;
@@ -9550,6 +9589,16 @@ var init_state_db = __esm(() => {
9550
9589
  CREATE INDEX IF NOT EXISTS idx_extract_sessions_processed
9551
9590
  ON extract_sessions_seen(processed_at);
9552
9591
  `
9592
+ },
9593
+ {
9594
+ id: "005-proposal-fs-imports",
9595
+ up: `
9596
+ CREATE TABLE IF NOT EXISTS proposal_fs_imports (
9597
+ stash_dir TEXT PRIMARY KEY,
9598
+ imported_at TEXT NOT NULL,
9599
+ imported_count INTEGER NOT NULL DEFAULT 0
9600
+ );
9601
+ `
9553
9602
  }
9554
9603
  ];
9555
9604
  });
@@ -10238,6 +10287,9 @@ var init_inline_refs = __esm(() => {
10238
10287
  import fs9 from "fs";
10239
10288
  import os from "os";
10240
10289
  import path8 from "path";
10290
+ function claudeProjectsDir() {
10291
+ return process.env.AKM_CLAUDE_PROJECTS_DIR ?? path8.join(os.homedir(), ".claude", "projects");
10292
+ }
10241
10293
  function parseClaudeEvent(entry, sessionId, filePath, fallbackTsMs) {
10242
10294
  if (!entry || typeof entry !== "object")
10243
10295
  return;
@@ -10294,11 +10346,11 @@ function parseClaudeEvent(entry, sessionId, filePath, fallbackTsMs) {
10294
10346
  class ClaudeCodeProvider {
10295
10347
  name = "claude-code";
10296
10348
  isAvailable() {
10297
- return fs9.existsSync(CLAUDE_PROJECTS_DIR);
10349
+ return fs9.existsSync(claudeProjectsDir());
10298
10350
  }
10299
10351
  *readEvents(input) {
10300
10352
  try {
10301
- for (const jsonlPath of this.#walkJsonl(CLAUDE_PROJECTS_DIR)) {
10353
+ for (const jsonlPath of this.#walkJsonl(claudeProjectsDir())) {
10302
10354
  const stat = fs9.statSync(jsonlPath);
10303
10355
  if (stat.mtimeMs < input.sinceMs)
10304
10356
  continue;
@@ -10326,7 +10378,7 @@ class ClaudeCodeProvider {
10326
10378
  }
10327
10379
  }
10328
10380
  listSessions(input = {}) {
10329
- const root = input.location ?? CLAUDE_PROJECTS_DIR;
10381
+ const root = input.location ?? claudeProjectsDir();
10330
10382
  const sinceMs = input.sinceMs ?? 0;
10331
10383
  const summaries = [];
10332
10384
  try {
@@ -10466,10 +10518,8 @@ class ClaudeCodeProvider {
10466
10518
  } catch {}
10467
10519
  }
10468
10520
  }
10469
- var CLAUDE_PROJECTS_DIR;
10470
10521
  var init_session_log = __esm(() => {
10471
10522
  init_inline_refs();
10472
- CLAUDE_PROJECTS_DIR = path8.join(os.homedir(), ".claude", "projects");
10473
10523
  });
10474
10524
 
10475
10525
  // src/integrations/harnesses/claude/index.ts
@@ -15458,6 +15508,7 @@ var init_config_schema = __esm(() => {
15458
15508
  contradictionDetection: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
15459
15509
  defaultSince: exports_external.string().min(1).optional(),
15460
15510
  maxTotalChars: positiveInt.optional(),
15511
+ minContentChars: exports_external.number().int().min(0).optional(),
15461
15512
  maxChunkSize: exports_external.number().int().min(1).max(50).optional(),
15462
15513
  minNewSessions: exports_external.number().int().min(0).optional(),
15463
15514
  indexSessions: exports_external.boolean().optional(),
@@ -16280,7 +16331,7 @@ function openDatabase2(dbPath, options) {
16280
16331
  }
16281
16332
  const db = openDatabase(resolvedPath);
16282
16333
  db.exec("PRAGMA journal_mode = WAL");
16283
- db.exec("PRAGMA busy_timeout = 5000");
16334
+ db.exec("PRAGMA busy_timeout = 30000");
16284
16335
  db.exec("PRAGMA foreign_keys = ON");
16285
16336
  loadVecExtension(db);
16286
16337
  const resolvedDim = options?.embeddingDim ?? resolveConfiguredEmbeddingDim();
@@ -16304,7 +16355,7 @@ function openExistingDatabase(dbPath) {
16304
16355
  const resolvedPath = dbPath ?? getDbPath();
16305
16356
  const db = openDatabase(resolvedPath);
16306
16357
  db.exec("PRAGMA journal_mode = WAL");
16307
- db.exec("PRAGMA busy_timeout = 5000");
16358
+ db.exec("PRAGMA busy_timeout = 30000");
16308
16359
  db.exec("PRAGMA foreign_keys = ON");
16309
16360
  loadVecExtension(db);
16310
16361
  return db;
@@ -8631,7 +8631,7 @@ function openStateDatabase(dbPath) {
8631
8631
  const db = openDatabase(resolvedPath);
8632
8632
  db.exec("PRAGMA journal_mode = WAL");
8633
8633
  db.exec("PRAGMA foreign_keys = ON");
8634
- db.exec("PRAGMA busy_timeout = 5000");
8634
+ db.exec("PRAGMA busy_timeout = 30000");
8635
8635
  runMigrations2(db);
8636
8636
  return db;
8637
8637
  }
@@ -8706,7 +8706,9 @@ var MIGRATIONS = [
8706
8706
  --
8707
8707
  -- Extensible (metadata_json) columns:
8708
8708
  -- metadata_json TEXT \u2014 JSON object for future proposal fields.
8709
- -- Current fields stored here: sourceRun, review.
8709
+ -- Current fields stored here: sourceRun,
8710
+ -- review, confidence, gateDecision (#577),
8711
+ -- backupContent.
8710
8712
  --
8711
8713
  -- ADD COLUMN extension points (future migrations):
8712
8714
  -- ALTER TABLE proposals ADD COLUMN source_run TEXT DEFAULT NULL;
@@ -8893,6 +8895,16 @@ var MIGRATIONS = [
8893
8895
  CREATE INDEX IF NOT EXISTS idx_extract_sessions_processed
8894
8896
  ON extract_sessions_seen(processed_at);
8895
8897
  `
8898
+ },
8899
+ {
8900
+ id: "005-proposal-fs-imports",
8901
+ up: `
8902
+ CREATE TABLE IF NOT EXISTS proposal_fs_imports (
8903
+ stash_dir TEXT PRIMARY KEY,
8904
+ imported_at TEXT NOT NULL,
8905
+ imported_count INTEGER NOT NULL DEFAULT 0
8906
+ );
8907
+ `
8896
8908
  }
8897
8909
  ];
8898
8910
  function runMigrations2(db) {
@@ -64,13 +64,11 @@ export function CRON_BACKEND(options = {}) {
64
64
  },
65
65
  list() {
66
66
  const existing = readCrontab(exec);
67
- const ids = [];
68
- for (const line of existing.split(/\r?\n/)) {
69
- const m = line.match(BLOCK_RE);
70
- if (m)
71
- ids.push(m[1]);
72
- }
73
- return ids.map((id) => ({ id }));
67
+ return listBlocks(existing).map(({ id, body }) => ({ id, signature: normalizeSignature(body) }));
68
+ },
69
+ expectedSignature(task) {
70
+ const cronLine = buildCronLine(task, akmArgv, logDir);
71
+ return normalizeSignature(cronBlockBody(cronLine, task.enabled));
74
72
  },
75
73
  };
76
74
  }
@@ -82,9 +80,48 @@ export function buildCronLine(task, akmArgv, logDir) {
82
80
  const cmd = [...akmArgv, "tasks", "run", task.id].map((part) => quoteForCron(part)).join(" ");
83
81
  return `${cronExpr} ${cmd} >> ${quoteForCron(logPath)} 2>&1`;
84
82
  }
83
+ /** The crontab line as it appears inside a block — commented when disabled. */
84
+ export function cronBlockBody(cronLine, enabled) {
85
+ return enabled ? cronLine : `${DISABLED_PREFIX}${cronLine}`;
86
+ }
85
87
  export function renderBlock(id, cronLine, enabled) {
86
- const body = enabled ? cronLine : `${DISABLED_PREFIX}${cronLine}`;
87
- return [BEGIN(id), body, END(id)].join("\n");
88
+ return [BEGIN(id), cronBlockBody(cronLine, enabled), END(id)].join("\n");
89
+ }
90
+ /**
91
+ * Parse the akm-owned blocks out of a crontab, returning each task id with the
92
+ * raw body line(s) between its BEGIN/END markers. Used by `list()` to build a
93
+ * drift signature, and exported for tests.
94
+ */
95
+ export function listBlocks(existing) {
96
+ const out = [];
97
+ const lines = existing.split(/\r?\n/);
98
+ let currentId = null;
99
+ let body = [];
100
+ for (const line of lines) {
101
+ const begin = line.match(BLOCK_RE);
102
+ if (begin) {
103
+ currentId = begin[1];
104
+ body = [];
105
+ continue;
106
+ }
107
+ if (currentId !== null && line === END(currentId)) {
108
+ out.push({ id: currentId, body: body.join("\n") });
109
+ currentId = null;
110
+ body = [];
111
+ continue;
112
+ }
113
+ if (currentId !== null)
114
+ body.push(line);
115
+ }
116
+ return out;
117
+ }
118
+ /** Collapse incidental whitespace so signature comparison ignores it. */
119
+ function normalizeSignature(body) {
120
+ return body
121
+ .split(/\r?\n/)
122
+ .map((l) => l.trim())
123
+ .filter((l) => l.length > 0)
124
+ .join("\n");
88
125
  }
89
126
  export function upsertBlock(existing, id, block) {
90
127
  const trimmed = existing.replace(/\s+$/g, "");
@@ -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) {
@@ -47,6 +47,10 @@ export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
47
47
  }
48
48
  const db = openDatabase(dbPath);
49
49
  db.exec("PRAGMA journal_mode = WAL");
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.
53
+ db.exec("PRAGMA busy_timeout = 30000");
50
54
  db.exec("PRAGMA foreign_keys = ON");
51
55
  ensureBaseSchema(db);
52
56
  runMigrations(db);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.0-beta.1",
3
+ "version": "0.9.0-beta.3",
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": [