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.
- package/CHANGELOG.md +128 -0
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +560 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/shared.js +21 -5
- package/dist/cli.js +36 -5
- package/dist/commands/config-cli.js +0 -10
- package/dist/commands/health/html-report.js +448 -0
- package/dist/commands/health.js +97 -6
- package/dist/commands/improve/extract.js +38 -2
- package/dist/commands/improve/improve-auto-accept.js +27 -1
- package/dist/commands/improve/improve-cli.js +7 -0
- package/dist/commands/improve/improve.js +201 -66
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +25 -0
- package/dist/commands/proposal/drain.js +73 -6
- package/dist/commands/proposal/proposal-cli.js +22 -10
- package/dist/commands/proposal/proposal.js +12 -1
- package/dist/commands/proposal/validators/proposals.js +361 -338
- package/dist/commands/remember.js +6 -2
- package/dist/commands/tasks/tasks.js +32 -8
- package/dist/core/config/config-schema.js +5 -0
- package/dist/core/logs-db.js +304 -0
- package/dist/core/state-db.js +107 -14
- package/dist/indexer/db/db.js +2 -2
- package/dist/indexer/passes/memory-inference.js +61 -22
- package/dist/integrations/harnesses/claude/session-log.js +16 -4
- package/dist/llm/client.js +15 -0
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +3 -2
- package/dist/output/html-render.js +73 -0
- package/dist/output/shapes/helpers.js +17 -1
- package/dist/output/text/helpers.js +69 -1
- package/dist/scripts/migrate-storage.js +65 -14
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +14 -2
- package/dist/tasks/backends/cron.js +46 -9
- package/dist/tasks/runner.js +99 -16
- package/dist/workflows/db.js +4 -0
- package/package.json +1 -1
- 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 =
|
|
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
|
|
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 =
|
|
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,
|
|
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(
|
|
10349
|
+
return fs9.existsSync(claudeProjectsDir());
|
|
10298
10350
|
}
|
|
10299
10351
|
*readEvents(input) {
|
|
10300
10352
|
try {
|
|
10301
|
-
for (const jsonlPath of this.#walkJsonl(
|
|
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 ??
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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, "");
|
package/dist/tasks/runner.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
291
|
+
dbLines.push({ level: "error", line: `error=${input.error.message}` });
|
|
259
292
|
}
|
|
260
|
-
return `${
|
|
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
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/workflows/db.js
CHANGED
|
@@ -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.
|
|
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": [
|