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

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 (56) hide show
  1. package/CHANGELOG.md +492 -0
  2. package/dist/assets/templates/html/default.html +78 -0
  3. package/dist/assets/templates/html/health.html +732 -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 +47 -5
  7. package/dist/commands/config-cli.js +0 -10
  8. package/dist/commands/feedback-cli.js +42 -37
  9. package/dist/commands/graph/graph.js +75 -71
  10. package/dist/commands/health/checks.js +48 -0
  11. package/dist/commands/health/html-report.js +666 -0
  12. package/dist/commands/health.js +201 -14
  13. package/dist/commands/improve/consolidate.js +39 -6
  14. package/dist/commands/improve/distill.js +26 -5
  15. package/dist/commands/improve/extract-prompt.js +1 -1
  16. package/dist/commands/improve/extract.js +73 -8
  17. package/dist/commands/improve/improve-auto-accept.js +63 -3
  18. package/dist/commands/improve/improve-cli.js +7 -0
  19. package/dist/commands/improve/improve-profiles.js +4 -0
  20. package/dist/commands/improve/improve.js +874 -447
  21. package/dist/commands/improve/proactive-maintenance.js +113 -0
  22. package/dist/commands/improve/reflect-noise.js +0 -0
  23. package/dist/commands/improve/reflect.js +31 -0
  24. package/dist/commands/proposal/drain.js +73 -6
  25. package/dist/commands/proposal/proposal-cli.js +22 -10
  26. package/dist/commands/proposal/proposal.js +17 -1
  27. package/dist/commands/proposal/validators/proposals.js +365 -329
  28. package/dist/commands/read/curate.js +17 -0
  29. package/dist/commands/remember.js +6 -2
  30. package/dist/commands/sources/stash-cli.js +10 -2
  31. package/dist/commands/tasks/tasks.js +32 -8
  32. package/dist/core/config/config-schema.js +33 -0
  33. package/dist/core/logs-db.js +304 -0
  34. package/dist/core/paths.js +3 -0
  35. package/dist/core/state-db.js +152 -14
  36. package/dist/indexer/db/db.js +99 -13
  37. package/dist/indexer/ensure-index.js +152 -17
  38. package/dist/indexer/index-writer-lock.js +99 -0
  39. package/dist/indexer/indexer.js +114 -111
  40. package/dist/indexer/passes/memory-inference.js +61 -22
  41. package/dist/integrations/harnesses/claude/session-log.js +17 -5
  42. package/dist/llm/client.js +38 -4
  43. package/dist/llm/usage-persist.js +77 -0
  44. package/dist/llm/usage-telemetry.js +103 -0
  45. package/dist/output/context.js +3 -2
  46. package/dist/output/html-render.js +73 -0
  47. package/dist/output/shapes/helpers.js +17 -1
  48. package/dist/output/text/helpers.js +69 -1
  49. package/dist/scripts/migrate-storage.js +154 -25
  50. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +21 -2
  51. package/dist/sources/providers/tar-utils.js +16 -8
  52. package/dist/tasks/backends/cron.js +46 -9
  53. package/dist/tasks/runner.js +99 -16
  54. package/dist/workflows/db.js +4 -0
  55. package/package.json +3 -2
  56. package/dist/commands/config-edit.js +0 -344
@@ -8882,12 +8882,14 @@ var init_improve_types = () => {};
8882
8882
  // src/core/state-db.ts
8883
8883
  var exports_state_db = {};
8884
8884
  __export(exports_state_db, {
8885
+ withImmediateTransaction: () => withImmediateTransaction,
8885
8886
  upsertTaskHistory: () => upsertTaskHistory,
8886
8887
  upsertProposal: () => upsertProposal,
8887
8888
  upsertExtractedSession: () => upsertExtractedSession,
8888
8889
  shouldSkipAlreadyExtractedSession: () => shouldSkipAlreadyExtractedSession,
8889
8890
  runMigrations: () => runMigrations2,
8890
8891
  recordImproveRun: () => recordImproveRun,
8892
+ recordFsProposalsImport: () => recordFsProposalsImport,
8891
8893
  readStateEvents: () => readStateEvents,
8892
8894
  queryTaskHistory: () => queryTaskHistory,
8893
8895
  queryImproveRuns: () => queryImproveRuns,
@@ -8898,9 +8900,12 @@ __export(exports_state_db, {
8898
8900
  proposalRowToProposal: () => proposalRowToProposal,
8899
8901
  openStateDatabase: () => openStateDatabase,
8900
8902
  listStateProposals: () => listStateProposals,
8903
+ listStateProposalIdsByPrefix: () => listStateProposalIdsByPrefix,
8901
8904
  listExistingTableNames: () => listExistingTableNames,
8905
+ insertProposalIfAbsent: () => insertProposalIfAbsent,
8902
8906
  insertEvent: () => insertEvent,
8903
8907
  importEventsJsonl: () => importEventsJsonl,
8908
+ hasImportedFsProposals: () => hasImportedFsProposals,
8904
8909
  getTaskHistoryRuns: () => getTaskHistoryRuns,
8905
8910
  getTaskHistory: () => getTaskHistory,
8906
8911
  getStateProposal: () => getStateProposal,
@@ -8925,7 +8930,7 @@ function openStateDatabase(dbPath) {
8925
8930
  const db = openDatabase(resolvedPath);
8926
8931
  db.exec("PRAGMA journal_mode = WAL");
8927
8932
  db.exec("PRAGMA foreign_keys = ON");
8928
- db.exec("PRAGMA busy_timeout = 5000");
8933
+ db.exec("PRAGMA busy_timeout = 30000");
8929
8934
  runMigrations2(db);
8930
8935
  return db;
8931
8936
  }
@@ -8972,7 +8977,11 @@ function proposalRowToProposal(row) {
8972
8977
  content: row.content,
8973
8978
  ...frontmatter !== undefined ? { frontmatter } : {}
8974
8979
  },
8975
- ...meta.review !== undefined ? { review: meta.review } : {}
8980
+ ...meta.review !== undefined ? { review: meta.review } : {},
8981
+ ...typeof meta.confidence === "number" ? { confidence: meta.confidence } : {},
8982
+ ...meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {},
8983
+ ...typeof meta.backupContent === "string" ? { backupContent: meta.backupContent } : {},
8984
+ ...typeof meta.eligibilitySource === "string" ? { eligibilitySource: meta.eligibilitySource } : {}
8976
8985
  };
8977
8986
  }
8978
8987
  function proposalToRowValues(proposal, stashDir) {
@@ -8981,6 +8990,14 @@ function proposalToRowValues(proposal, stashDir) {
8981
8990
  metaObj.sourceRun = proposal.sourceRun;
8982
8991
  if (proposal.review !== undefined)
8983
8992
  metaObj.review = proposal.review;
8993
+ if (proposal.confidence !== undefined)
8994
+ metaObj.confidence = proposal.confidence;
8995
+ if (proposal.gateDecision !== undefined)
8996
+ metaObj.gateDecision = proposal.gateDecision;
8997
+ if (proposal.backupContent !== undefined)
8998
+ metaObj.backupContent = proposal.backupContent;
8999
+ if (proposal.eligibilitySource !== undefined)
9000
+ metaObj.eligibilitySource = proposal.eligibilitySource;
8984
9001
  return {
8985
9002
  id: proposal.id,
8986
9003
  stash_dir: stashDir,
@@ -9074,15 +9091,52 @@ function listStateProposals(db, options = {}) {
9074
9091
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
9075
9092
  const rows = db.prepare(`SELECT id, stash_dir, ref, status, source, created_at, updated_at,
9076
9093
  content, frontmatter_json, metadata_json
9077
- FROM proposals ${where} ORDER BY created_at ASC`).all(...params);
9094
+ FROM proposals ${where} ORDER BY created_at ASC, rowid ASC`).all(...params);
9078
9095
  return rows.map(proposalRowToProposal);
9079
9096
  }
9080
- function getStateProposal(db, id) {
9081
- const row = db.prepare(`SELECT id, stash_dir, ref, status, source, created_at, updated_at,
9097
+ function getStateProposal(db, id, stashDir) {
9098
+ const sql = `SELECT id, stash_dir, ref, status, source, created_at, updated_at,
9082
9099
  content, frontmatter_json, metadata_json
9083
- FROM proposals WHERE id = ?`).get(id);
9100
+ FROM proposals WHERE id = ?${stashDir ? " AND stash_dir = ?" : ""}`;
9101
+ const row = stashDir ? db.prepare(sql).get(id, stashDir) : db.prepare(sql).get(id);
9084
9102
  return row ? proposalRowToProposal(row) : undefined;
9085
9103
  }
9104
+ function listStateProposalIdsByPrefix(db, stashDir, idPrefix) {
9105
+ const escaped = idPrefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
9106
+ const rows = db.prepare(`SELECT id FROM proposals
9107
+ WHERE stash_dir = ? AND status = 'pending' AND id LIKE ? ESCAPE '\\'
9108
+ ORDER BY id ASC`).all(stashDir, `${escaped}%`);
9109
+ return rows.map((r) => r.id);
9110
+ }
9111
+ function hasImportedFsProposals(db, stashDir) {
9112
+ return Boolean(db.prepare("SELECT 1 FROM proposal_fs_imports WHERE stash_dir = ?").get(stashDir));
9113
+ }
9114
+ function recordFsProposalsImport(db, stashDir, importedCount) {
9115
+ db.prepare("INSERT OR REPLACE INTO proposal_fs_imports (stash_dir, imported_at, imported_count) VALUES (?, ?, ?)").run(stashDir, new Date().toISOString(), importedCount);
9116
+ }
9117
+ function insertProposalIfAbsent(db, proposal, stashDir) {
9118
+ const v = proposalToRowValues(proposal, stashDir);
9119
+ const result = db.prepare(`
9120
+ INSERT OR IGNORE INTO proposals
9121
+ (id, stash_dir, ref, status, source, created_at, updated_at, content, frontmatter_json, metadata_json)
9122
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
9123
+ `).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);
9124
+ const changes = result.changes ?? 0;
9125
+ return Number(changes) > 0;
9126
+ }
9127
+ function withImmediateTransaction(db, fn) {
9128
+ db.exec("BEGIN IMMEDIATE");
9129
+ try {
9130
+ const result = fn();
9131
+ db.exec("COMMIT");
9132
+ return result;
9133
+ } catch (err) {
9134
+ try {
9135
+ db.exec("ROLLBACK");
9136
+ } catch {}
9137
+ throw err;
9138
+ }
9139
+ }
9086
9140
  function upsertTaskHistory(db, row) {
9087
9141
  db.prepare(`
9088
9142
  INSERT OR IGNORE INTO task_history
@@ -9363,7 +9417,9 @@ var init_state_db = __esm(() => {
9363
9417
  --
9364
9418
  -- Extensible (metadata_json) columns:
9365
9419
  -- metadata_json TEXT \u2014 JSON object for future proposal fields.
9366
- -- Current fields stored here: sourceRun, review.
9420
+ -- Current fields stored here: sourceRun,
9421
+ -- review, confidence, gateDecision (#577),
9422
+ -- backupContent, eligibilitySource.
9367
9423
  --
9368
9424
  -- ADD COLUMN extension points (future migrations):
9369
9425
  -- ALTER TABLE proposals ADD COLUMN source_run TEXT DEFAULT NULL;
@@ -9550,6 +9606,23 @@ var init_state_db = __esm(() => {
9550
9606
  CREATE INDEX IF NOT EXISTS idx_extract_sessions_processed
9551
9607
  ON extract_sessions_seen(processed_at);
9552
9608
  `
9609
+ },
9610
+ {
9611
+ id: "005-proposal-fs-imports",
9612
+ up: `
9613
+ CREATE TABLE IF NOT EXISTS proposal_fs_imports (
9614
+ stash_dir TEXT PRIMARY KEY,
9615
+ imported_at TEXT NOT NULL,
9616
+ imported_count INTEGER NOT NULL DEFAULT 0
9617
+ );
9618
+ `
9619
+ },
9620
+ {
9621
+ id: "006-proposals-pending-ref-source",
9622
+ up: `
9623
+ CREATE INDEX IF NOT EXISTS idx_proposals_stash_status_ref_source
9624
+ ON proposals(stash_dir, status, ref, source);
9625
+ `
9553
9626
  }
9554
9627
  ];
9555
9628
  });
@@ -10238,6 +10311,9 @@ var init_inline_refs = __esm(() => {
10238
10311
  import fs9 from "fs";
10239
10312
  import os from "os";
10240
10313
  import path8 from "path";
10314
+ function claudeProjectsDir() {
10315
+ return process.env.AKM_CLAUDE_PROJECTS_DIR ?? path8.join(os.homedir(), ".claude", "projects");
10316
+ }
10241
10317
  function parseClaudeEvent(entry, sessionId, filePath, fallbackTsMs) {
10242
10318
  if (!entry || typeof entry !== "object")
10243
10319
  return;
@@ -10294,11 +10370,11 @@ function parseClaudeEvent(entry, sessionId, filePath, fallbackTsMs) {
10294
10370
  class ClaudeCodeProvider {
10295
10371
  name = "claude-code";
10296
10372
  isAvailable() {
10297
- return fs9.existsSync(CLAUDE_PROJECTS_DIR);
10373
+ return fs9.existsSync(claudeProjectsDir());
10298
10374
  }
10299
10375
  *readEvents(input) {
10300
10376
  try {
10301
- for (const jsonlPath of this.#walkJsonl(CLAUDE_PROJECTS_DIR)) {
10377
+ for (const jsonlPath of this.#walkJsonl(claudeProjectsDir())) {
10302
10378
  const stat = fs9.statSync(jsonlPath);
10303
10379
  if (stat.mtimeMs < input.sinceMs)
10304
10380
  continue;
@@ -10326,7 +10402,7 @@ class ClaudeCodeProvider {
10326
10402
  }
10327
10403
  }
10328
10404
  listSessions(input = {}) {
10329
- const root = input.location ?? CLAUDE_PROJECTS_DIR;
10405
+ const root = input.location ?? claudeProjectsDir();
10330
10406
  const sinceMs = input.sinceMs ?? 0;
10331
10407
  const summaries = [];
10332
10408
  try {
@@ -10460,16 +10536,14 @@ class ClaudeCodeProvider {
10460
10536
  const full = path8.join(dir, entry.name);
10461
10537
  if (entry.isDirectory())
10462
10538
  yield* this.#walkJsonl(full);
10463
- else if (entry.name.endsWith(".jsonl"))
10539
+ else if (entry.name.endsWith(".jsonl") && entry.name !== "journal.jsonl")
10464
10540
  yield full;
10465
10541
  }
10466
10542
  } catch {}
10467
10543
  }
10468
10544
  }
10469
- var CLAUDE_PROJECTS_DIR;
10470
10545
  var init_session_log = __esm(() => {
10471
10546
  init_inline_refs();
10472
- CLAUDE_PROJECTS_DIR = path8.join(os.homedir(), ".claude", "projects");
10473
10547
  });
10474
10548
 
10475
10549
  // src/integrations/harnesses/claude/index.ts
@@ -15458,8 +15532,18 @@ var init_config_schema = __esm(() => {
15458
15532
  contradictionDetection: exports_external.object({ enabled: exports_external.boolean().optional() }).strict().optional(),
15459
15533
  defaultSince: exports_external.string().min(1).optional(),
15460
15534
  maxTotalChars: positiveInt.optional(),
15535
+ minContentChars: exports_external.number().int().min(0).optional(),
15461
15536
  maxChunkSize: exports_external.number().int().min(1).max(50).optional(),
15537
+ incrementalSince: exports_external.string().optional(),
15538
+ limit: positiveInt.optional(),
15539
+ neighborsPerChanged: exports_external.number().int().min(1).optional(),
15540
+ requirePlannedRefs: exports_external.boolean().optional(),
15541
+ dueDays: exports_external.number().int().min(0).optional(),
15542
+ maxPerRun: positiveInt.optional(),
15543
+ importanceWeights: exports_external.record(exports_external.string().min(1), exports_external.number()).optional(),
15544
+ minPendingCount: exports_external.number().int().min(0).optional(),
15462
15545
  minNewSessions: exports_external.number().int().min(0).optional(),
15546
+ maxSessionsPerRun: exports_external.number().int().min(0).optional(),
15463
15547
  indexSessions: exports_external.boolean().optional(),
15464
15548
  minSessionDuration: exports_external.number().min(0).optional(),
15465
15549
  applyMode: exports_external.enum(["queue", "promote"]).optional(),
@@ -15480,7 +15564,8 @@ var init_config_schema = __esm(() => {
15480
15564
  memoryInference: ImproveProcessConfigSchema.optional(),
15481
15565
  graphExtraction: ImproveProcessConfigSchema.optional(),
15482
15566
  validation: ImproveProcessConfigSchema.optional(),
15483
- triage: ImproveProcessConfigSchema.optional()
15567
+ triage: ImproveProcessConfigSchema.optional(),
15568
+ proactiveMaintenance: ImproveProcessConfigSchema.optional()
15484
15569
  }).passthrough().superRefine((val, ctx) => {
15485
15570
  const raw = val;
15486
15571
  if ("feedbackDistillation" in raw) {
@@ -15498,7 +15583,8 @@ var init_config_schema = __esm(() => {
15498
15583
  "graphExtraction",
15499
15584
  "validation",
15500
15585
  "extract",
15501
- "triage"
15586
+ "triage",
15587
+ "proactiveMaintenance"
15502
15588
  ]);
15503
15589
  for (const k of Object.keys(raw)) {
15504
15590
  if (!allowed.has(k)) {
@@ -16237,6 +16323,7 @@ __export(exports_db, {
16237
16323
  getMeta: () => getMeta,
16238
16324
  getLlmCacheEntry: () => getLlmCacheEntry,
16239
16325
  getLlmCacheEntriesByRefs: () => getLlmCacheEntriesByRefs,
16326
+ getIndexedFilePaths: () => getIndexedFilePaths,
16240
16327
  getIndexDirState: () => getIndexDirState,
16241
16328
  getEntryRefRowsForStashRoot: () => getEntryRefRowsForStashRoot,
16242
16329
  getEntryIdByFilePath: () => getEntryIdByFilePath,
@@ -16280,7 +16367,7 @@ function openDatabase2(dbPath, options) {
16280
16367
  }
16281
16368
  const db = openDatabase(resolvedPath);
16282
16369
  db.exec("PRAGMA journal_mode = WAL");
16283
- db.exec("PRAGMA busy_timeout = 5000");
16370
+ db.exec("PRAGMA busy_timeout = 30000");
16284
16371
  db.exec("PRAGMA foreign_keys = ON");
16285
16372
  loadVecExtension(db);
16286
16373
  const resolvedDim = options?.embeddingDim ?? resolveConfiguredEmbeddingDim();
@@ -16304,7 +16391,7 @@ function openExistingDatabase(dbPath) {
16304
16391
  const resolvedPath = dbPath ?? getDbPath();
16305
16392
  const db = openDatabase(resolvedPath);
16306
16393
  db.exec("PRAGMA journal_mode = WAL");
16307
- db.exec("PRAGMA busy_timeout = 5000");
16394
+ db.exec("PRAGMA busy_timeout = 30000");
16308
16395
  db.exec("PRAGMA foreign_keys = ON");
16309
16396
  loadVecExtension(db);
16310
16397
  return db;
@@ -17136,6 +17223,10 @@ function getEntryIdByFilePath(db, filePath) {
17136
17223
  const row = db.prepare("SELECT id FROM entries WHERE file_path = ? LIMIT 1").get(filePath);
17137
17224
  return row?.id;
17138
17225
  }
17226
+ function getIndexedFilePaths(db) {
17227
+ const rows = db.prepare("SELECT DISTINCT file_path FROM entries WHERE file_path IS NOT NULL AND file_path <> ''").all();
17228
+ return new Set(rows.map((r) => r.file_path));
17229
+ }
17139
17230
  function getEntryFilePathById(db, id) {
17140
17231
  const row = db.prepare("SELECT file_path FROM entries WHERE id = ?").get(id);
17141
17232
  return row?.file_path;
@@ -17263,18 +17354,56 @@ function clearStaleCacheEntries(db) {
17263
17354
  function computeBodyHash(body) {
17264
17355
  return sha256Hex(body);
17265
17356
  }
17357
+ function bareRef(ref) {
17358
+ try {
17359
+ const parsed = parseAssetRef(ref);
17360
+ return `${parsed.type}:${parsed.name}`;
17361
+ } catch {
17362
+ return ref;
17363
+ }
17364
+ }
17266
17365
  function getRetrievalCounts(db, refs) {
17267
17366
  if (refs.length === 0)
17268
17367
  return new Map;
17269
- const result = new Map;
17270
- for (let i = 0;i < refs.length; i += SQLITE_CHUNK_SIZE) {
17271
- const chunk = refs.slice(i, i + SQLITE_CHUNK_SIZE);
17368
+ const bareToInputs = new Map;
17369
+ for (const ref of refs) {
17370
+ const bare = bareRef(ref);
17371
+ const existing = bareToInputs.get(bare);
17372
+ if (existing)
17373
+ existing.push(ref);
17374
+ else
17375
+ bareToInputs.set(bare, [ref]);
17376
+ }
17377
+ const bareForms = [...bareToInputs.keys()];
17378
+ const countsByBare = new Map;
17379
+ for (let i = 0;i < bareForms.length; i += SQLITE_CHUNK_SIZE) {
17380
+ const chunk = bareForms.slice(i, i + SQLITE_CHUNK_SIZE);
17272
17381
  const placeholders = chunk.map(() => "?").join(", ");
17273
- const rows = db.prepare(`SELECT entry_ref, COUNT(*) AS cnt FROM usage_events
17274
- WHERE event_type IN ('search','show') AND entry_ref IN (${placeholders})
17275
- GROUP BY entry_ref`).all(...chunk);
17276
- for (const r of rows)
17277
- result.set(r.entry_ref, r.cnt);
17382
+ const rows = db.prepare(`SELECT
17383
+ CASE
17384
+ WHEN instr(entry_ref, '//') > 0
17385
+ THEN substr(entry_ref, instr(entry_ref, '//') + 2)
17386
+ ELSE entry_ref
17387
+ END AS bare_ref,
17388
+ COUNT(*) AS cnt
17389
+ FROM usage_events
17390
+ WHERE event_type IN ('search','show','curate')
17391
+ AND entry_ref IS NOT NULL
17392
+ AND CASE
17393
+ WHEN instr(entry_ref, '//') > 0
17394
+ THEN substr(entry_ref, instr(entry_ref, '//') + 2)
17395
+ ELSE entry_ref
17396
+ END IN (${placeholders})
17397
+ GROUP BY bare_ref`).all(...chunk);
17398
+ for (const r of rows) {
17399
+ countsByBare.set(r.bare_ref, (countsByBare.get(r.bare_ref) ?? 0) + r.cnt);
17400
+ }
17401
+ }
17402
+ const result = new Map;
17403
+ for (const [bare, count] of countsByBare) {
17404
+ for (const input of bareToInputs.get(bare) ?? []) {
17405
+ result.set(input, count);
17406
+ }
17278
17407
  }
17279
17408
  return result;
17280
17409
  }
@@ -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, eligibilitySource.
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,23 @@ 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
+ `
8908
+ },
8909
+ {
8910
+ id: "006-proposals-pending-ref-source",
8911
+ up: `
8912
+ CREATE INDEX IF NOT EXISTS idx_proposals_stash_status_ref_source
8913
+ ON proposals(stash_dir, status, ref, source);
8914
+ `
8896
8915
  }
8897
8916
  ];
8898
8917
  function runMigrations2(db) {
@@ -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
@@ -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, "");