@arcbridge/core 0.1.1 → 0.1.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/dist/index.js CHANGED
@@ -44,6 +44,11 @@ var ArcBridgeConfigSchema = z.object({
44
44
  "File paths or prefixes to ignore in undocumented_module drift checks. Framework files (e.g. next.config.ts, root layout/page) are auto-ignored for known project types."
45
45
  )
46
46
  }).default({}),
47
+ metrics: z.object({
48
+ auto_record: z.boolean().default(false).describe(
49
+ "Automatically record agent activity (tool name, duration, quality snapshot) when key MCP tools are invoked. Token/model info is optional and caller-provided."
50
+ )
51
+ }).default({}),
47
52
  sync: z.object({
48
53
  auto_detect_drift: z.boolean().default(true),
49
54
  drift_severity_threshold: z.enum(["info", "warning", "error"]).default("warning"),
@@ -205,19 +210,97 @@ var AgentRoleSchema = z6.object({
205
210
  });
206
211
 
207
212
  // src/db/connection.ts
208
- import Database from "better-sqlite3";
213
+ import { DatabaseSync } from "node:sqlite";
214
+ var suppressed = false;
215
+ function suppressSqliteWarning() {
216
+ if (suppressed) return;
217
+ suppressed = true;
218
+ const origEmit = process.emit;
219
+ process.emit = function(event, ...args) {
220
+ if (event === "warning" && args[0]?.name === "ExperimentalWarning" && typeof args[0]?.message === "string" && args[0].message.includes("SQLite")) {
221
+ return true;
222
+ }
223
+ return origEmit.apply(process, [event, ...args]);
224
+ };
225
+ }
226
+ function sanitizeParams(params) {
227
+ return params.map((p) => p === void 0 ? null : p);
228
+ }
209
229
  function openDatabase(dbPath) {
210
- const db = new Database(dbPath);
211
- db.pragma("journal_mode = WAL");
212
- db.pragma("foreign_keys = ON");
230
+ const db = new DatabaseSync(dbPath);
231
+ db.exec("PRAGMA journal_mode = WAL");
232
+ db.exec("PRAGMA foreign_keys = ON");
233
+ patchPrepare(db);
213
234
  return db;
214
235
  }
215
236
  function openMemoryDatabase() {
216
- return openDatabase(":memory:");
237
+ const db = new DatabaseSync(":memory:");
238
+ db.exec("PRAGMA foreign_keys = ON");
239
+ patchPrepare(db);
240
+ return db;
241
+ }
242
+ function patchPrepare(db) {
243
+ const originalPrepare = db.prepare.bind(db);
244
+ db.prepare = (sql) => {
245
+ const stmt = originalPrepare(sql);
246
+ const origRun = stmt.run.bind(stmt);
247
+ const origGet = stmt.get.bind(stmt);
248
+ const origAll = stmt.all.bind(stmt);
249
+ stmt.run = (...params) => origRun(...sanitizeParams(params));
250
+ stmt.get = (...params) => origGet(...sanitizeParams(params));
251
+ stmt.all = (...params) => origAll(...sanitizeParams(params));
252
+ return stmt;
253
+ };
254
+ }
255
+ function rejectAsync(fn) {
256
+ if (fn.constructor.name === "AsyncFunction") {
257
+ throw new Error(
258
+ "transaction() received an async function. Use a synchronous function \u2014 node:sqlite is synchronous and the transaction would commit before the async work completes."
259
+ );
260
+ }
261
+ }
262
+ var txDepth = /* @__PURE__ */ new WeakMap();
263
+ function transaction(db, fn) {
264
+ rejectAsync(fn);
265
+ const depth = txDepth.get(db) ?? 0;
266
+ if (depth === 0) {
267
+ db.exec("BEGIN");
268
+ txDepth.set(db, 1);
269
+ try {
270
+ const result = fn();
271
+ db.exec("COMMIT");
272
+ return result;
273
+ } catch (err) {
274
+ try {
275
+ db.exec("ROLLBACK");
276
+ } catch {
277
+ }
278
+ throw err;
279
+ } finally {
280
+ txDepth.set(db, 0);
281
+ }
282
+ }
283
+ const name = `sp_${depth}`;
284
+ db.exec(`SAVEPOINT ${name}`);
285
+ txDepth.set(db, depth + 1);
286
+ try {
287
+ const result = fn();
288
+ db.exec(`RELEASE ${name}`);
289
+ return result;
290
+ } catch (err) {
291
+ try {
292
+ db.exec(`ROLLBACK TO ${name}`);
293
+ db.exec(`RELEASE ${name}`);
294
+ } catch {
295
+ }
296
+ throw err;
297
+ } finally {
298
+ txDepth.set(db, depth);
299
+ }
217
300
  }
218
301
 
219
302
  // src/db/schema.ts
220
- var CURRENT_SCHEMA_VERSION = 1;
303
+ var CURRENT_SCHEMA_VERSION = 2;
221
304
  var SCHEMA_SQL = `
222
305
  -- Metadata
223
306
  CREATE TABLE IF NOT EXISTS arcbridge_meta (
@@ -383,6 +466,36 @@ CREATE TABLE IF NOT EXISTS drift_log (
383
466
  resolution TEXT CHECK(resolution IN ('accepted','fixed','deferred') OR resolution IS NULL),
384
467
  resolved_at TEXT
385
468
  );
469
+
470
+ -- Agent Activity Metrics (operational telemetry, not rebuilt by refreshFromDocs)
471
+ CREATE TABLE IF NOT EXISTS agent_activity (
472
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
473
+ tool_name TEXT NOT NULL,
474
+ action TEXT,
475
+ model TEXT,
476
+ agent_role TEXT,
477
+ task_id TEXT,
478
+ phase_id TEXT,
479
+ input_tokens INTEGER,
480
+ output_tokens INTEGER,
481
+ total_tokens INTEGER,
482
+ cost_usd REAL,
483
+ duration_ms INTEGER,
484
+ drift_count INTEGER,
485
+ drift_errors INTEGER,
486
+ test_pass_count INTEGER,
487
+ test_fail_count INTEGER,
488
+ lint_clean INTEGER,
489
+ typecheck_clean INTEGER,
490
+ notes TEXT,
491
+ metadata TEXT NOT NULL DEFAULT '{}',
492
+ recorded_at TEXT NOT NULL
493
+ );
494
+
495
+ CREATE INDEX IF NOT EXISTS idx_activity_recorded_at ON agent_activity(recorded_at);
496
+ CREATE INDEX IF NOT EXISTS idx_activity_model ON agent_activity(model);
497
+ CREATE INDEX IF NOT EXISTS idx_activity_task ON agent_activity(task_id);
498
+ CREATE INDEX IF NOT EXISTS idx_activity_phase ON agent_activity(phase_id);
386
499
  `;
387
500
  function initializeSchema(db) {
388
501
  db.exec(SCHEMA_SQL);
@@ -398,7 +511,42 @@ function initializeSchema(db) {
398
511
  }
399
512
 
400
513
  // src/db/migrations.ts
401
- var migrations = [];
514
+ var migrations = [
515
+ {
516
+ version: 2,
517
+ up: (db) => {
518
+ db.exec(`
519
+ CREATE TABLE IF NOT EXISTS agent_activity (
520
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
521
+ tool_name TEXT NOT NULL,
522
+ action TEXT,
523
+ model TEXT,
524
+ agent_role TEXT,
525
+ task_id TEXT,
526
+ phase_id TEXT,
527
+ input_tokens INTEGER,
528
+ output_tokens INTEGER,
529
+ total_tokens INTEGER,
530
+ cost_usd REAL,
531
+ duration_ms INTEGER,
532
+ drift_count INTEGER,
533
+ drift_errors INTEGER,
534
+ test_pass_count INTEGER,
535
+ test_fail_count INTEGER,
536
+ lint_clean INTEGER,
537
+ typecheck_clean INTEGER,
538
+ notes TEXT,
539
+ metadata TEXT NOT NULL DEFAULT '{}',
540
+ recorded_at TEXT NOT NULL
541
+ );
542
+ CREATE INDEX IF NOT EXISTS idx_activity_recorded_at ON agent_activity(recorded_at);
543
+ CREATE INDEX IF NOT EXISTS idx_activity_model ON agent_activity(model);
544
+ CREATE INDEX IF NOT EXISTS idx_activity_task ON agent_activity(task_id);
545
+ CREATE INDEX IF NOT EXISTS idx_activity_phase ON agent_activity(phase_id);
546
+ `);
547
+ }
548
+ }
549
+ ];
402
550
  function migrate(db) {
403
551
  const row = db.prepare("SELECT value FROM arcbridge_meta WHERE key = 'schema_version'").get();
404
552
  const currentVersion = row ? Number(row.value) : 0;
@@ -407,12 +555,12 @@ function migrate(db) {
407
555
  }
408
556
  const pending = migrations.filter((m) => m.version > currentVersion).sort((a, b) => a.version - b.version);
409
557
  for (const migration of pending) {
410
- db.transaction(() => {
558
+ transaction(db, () => {
411
559
  migration.up(db);
412
560
  db.prepare(
413
561
  "UPDATE arcbridge_meta SET value = ? WHERE key = 'schema_version'"
414
562
  ).run(String(migration.version));
415
- })();
563
+ });
416
564
  }
417
565
  }
418
566
 
@@ -450,6 +598,7 @@ function configTemplate(input) {
450
598
  drift: {
451
599
  ignore_paths: []
452
600
  },
601
+ metrics: { auto_record: false },
453
602
  sync: {
454
603
  auto_detect_drift: true,
455
604
  drift_severity_threshold: "warning",
@@ -487,6 +636,7 @@ function configTemplate2(input) {
487
636
  drift: {
488
637
  ignore_paths: []
489
638
  },
639
+ metrics: { auto_record: false },
490
640
  sync: {
491
641
  auto_detect_drift: true,
492
642
  drift_severity_threshold: "warning",
@@ -524,6 +674,7 @@ function configTemplate3(input) {
524
674
  drift: {
525
675
  ignore_paths: []
526
676
  },
677
+ metrics: { auto_record: false },
527
678
  sync: {
528
679
  auto_detect_drift: true,
529
680
  drift_severity_threshold: "warning",
@@ -559,6 +710,7 @@ function configTemplate4(input) {
559
710
  drift: {
560
711
  ignore_paths: []
561
712
  },
713
+ metrics: { auto_record: false },
562
714
  sync: {
563
715
  auto_detect_drift: true,
564
716
  drift_severity_threshold: "warning",
@@ -3201,9 +3353,11 @@ function refreshFromDocs(db, targetDir) {
3201
3353
  const scenarioStatusMap = new Map(
3202
3354
  existingScenarios.map((s) => [s.id, s.status])
3203
3355
  );
3204
- const refresh = db.transaction(() => {
3356
+ const refresh = () => transaction(db, () => {
3205
3357
  db.prepare("DELETE FROM tasks").run();
3206
3358
  db.prepare("DELETE FROM phases").run();
3359
+ db.prepare("UPDATE contracts SET building_block = NULL").run();
3360
+ db.prepare("UPDATE building_blocks SET parent_id = NULL").run();
3207
3361
  db.prepare("DELETE FROM building_blocks").run();
3208
3362
  db.prepare("DELETE FROM quality_scenarios").run();
3209
3363
  db.prepare("DELETE FROM adrs").run();
@@ -3250,12 +3404,12 @@ function generateDatabase(targetDir, input) {
3250
3404
  upsert.run("project_name", input.name);
3251
3405
  upsert.run("project_type", input.template);
3252
3406
  upsert.run("last_full_index", (/* @__PURE__ */ new Date()).toISOString());
3253
- db.transaction(() => {
3407
+ transaction(db, () => {
3254
3408
  allWarnings.push(...populateBuildingBlocks(db, targetDir));
3255
3409
  allWarnings.push(...populateQualityScenarios(db, targetDir));
3256
3410
  allWarnings.push(...populatePhases(db, targetDir));
3257
3411
  allWarnings.push(...populateAdrs(db, targetDir));
3258
- })();
3412
+ });
3259
3413
  ensureGitignore(targetDir);
3260
3414
  return { db, warnings: allWarnings };
3261
3415
  }
@@ -4054,7 +4208,7 @@ function writeComponents(db, components) {
4054
4208
  const existingIds = new Set(
4055
4209
  db.prepare("SELECT id FROM symbols").all().map((r) => r.id)
4056
4210
  );
4057
- const run = db.transaction(() => {
4211
+ transaction(db, () => {
4058
4212
  for (const c of components) {
4059
4213
  if (!existingIds.has(c.symbolId)) continue;
4060
4214
  insert.run(
@@ -4068,7 +4222,6 @@ function writeComponents(db, components) {
4068
4222
  );
4069
4223
  }
4070
4224
  });
4071
- run();
4072
4225
  }
4073
4226
 
4074
4227
  // src/indexer/route-analyzer.ts
@@ -4197,7 +4350,7 @@ function writeRoutes(db, routes) {
4197
4350
  id, route_path, kind, http_methods, has_auth, parent_layout, service
4198
4351
  ) VALUES (?, ?, ?, ?, ?, ?, ?)
4199
4352
  `);
4200
- const run = db.transaction(() => {
4353
+ transaction(db, () => {
4201
4354
  for (const r of routes) {
4202
4355
  insert.run(
4203
4356
  r.id,
@@ -4210,7 +4363,6 @@ function writeRoutes(db, routes) {
4210
4363
  );
4211
4364
  }
4212
4365
  });
4213
- run();
4214
4366
  }
4215
4367
 
4216
4368
  // src/indexer/content-hash.ts
@@ -4244,7 +4396,7 @@ function removeSymbolsForFiles(db, filePaths) {
4244
4396
  const deleteComponents = db.prepare(
4245
4397
  "DELETE FROM components WHERE symbol_id IN (SELECT id FROM symbols WHERE file_path = ?)"
4246
4398
  );
4247
- const run = db.transaction(() => {
4399
+ transaction(db, () => {
4248
4400
  for (const fp of filePaths) {
4249
4401
  deleteDepsSource.run(fp);
4250
4402
  deleteDepsTarget.run(fp);
@@ -4252,7 +4404,6 @@ function removeSymbolsForFiles(db, filePaths) {
4252
4404
  deleteSymbols.run(fp);
4253
4405
  }
4254
4406
  });
4255
- run();
4256
4407
  }
4257
4408
  function writeSymbols(db, symbols, service, language = "typescript") {
4258
4409
  if (symbols.length === 0) return;
@@ -4272,7 +4423,7 @@ function writeSymbols(db, symbols, service, language = "typescript") {
4272
4423
  )
4273
4424
  `);
4274
4425
  const now = (/* @__PURE__ */ new Date()).toISOString();
4275
- const run = db.transaction(() => {
4426
+ transaction(db, () => {
4276
4427
  for (const s of symbols) {
4277
4428
  insert.run(
4278
4429
  s.id,
@@ -4296,7 +4447,6 @@ function writeSymbols(db, symbols, service, language = "typescript") {
4296
4447
  );
4297
4448
  }
4298
4449
  });
4299
- run();
4300
4450
  }
4301
4451
  function writeDependencies(db, dependencies) {
4302
4452
  if (dependencies.length === 0) return;
@@ -4304,12 +4454,11 @@ function writeDependencies(db, dependencies) {
4304
4454
  INSERT OR IGNORE INTO dependencies (source_symbol, target_symbol, kind)
4305
4455
  VALUES (?, ?, ?)
4306
4456
  `);
4307
- const run = db.transaction(() => {
4457
+ transaction(db, () => {
4308
4458
  for (const dep of dependencies) {
4309
4459
  insert.run(dep.sourceSymbolId, dep.targetSymbolId, dep.kind);
4310
4460
  }
4311
4461
  });
4312
- run();
4313
4462
  }
4314
4463
 
4315
4464
  // src/indexer/dotnet-indexer.ts
@@ -4506,7 +4655,7 @@ function indexDotnetProjectRoslyn(db, options) {
4506
4655
  INSERT OR REPLACE INTO routes (id, route_path, kind, http_methods, has_auth, service)
4507
4656
  VALUES (?, ?, ?, ?, ?, ?)
4508
4657
  `);
4509
- const runRoutes = db.transaction(() => {
4658
+ transaction(db, () => {
4510
4659
  for (const route of output.routes) {
4511
4660
  insertRoute.run(
4512
4661
  route.id,
@@ -4518,7 +4667,6 @@ function indexDotnetProjectRoslyn(db, options) {
4518
4667
  );
4519
4668
  }
4520
4669
  });
4521
- runRoutes();
4522
4670
  }
4523
4671
  return {
4524
4672
  symbolsIndexed: output.symbols.length,
@@ -5437,7 +5585,7 @@ async function indexCSharpTreeSitter(db, options) {
5437
5585
  INSERT OR REPLACE INTO routes (id, route_path, kind, http_methods, has_auth, service)
5438
5586
  VALUES (?, ?, ?, ?, ?, ?)
5439
5587
  `);
5440
- const runRoutes = db.transaction(() => {
5588
+ transaction(db, () => {
5441
5589
  for (const route of allRoutes) {
5442
5590
  insertRoute.run(
5443
5591
  route.id,
@@ -5449,7 +5597,6 @@ async function indexCSharpTreeSitter(db, options) {
5449
5597
  );
5450
5598
  }
5451
5599
  });
5452
- runRoutes();
5453
5600
  }
5454
5601
  return {
5455
5602
  symbolsIndexed: allNewSymbols.length,
@@ -5481,12 +5628,11 @@ function indexPackageDependencies(db, projectRoot, service = "main") {
5481
5628
  const insert = db.prepare(
5482
5629
  "INSERT OR IGNORE INTO package_dependencies (name, version, source, service) VALUES (?, ?, ?, ?)"
5483
5630
  );
5484
- const run = db.transaction(() => {
5631
+ transaction(db, () => {
5485
5632
  for (const dep of deps) {
5486
5633
  insert.run(dep.name, dep.version, dep.source, service);
5487
5634
  }
5488
5635
  });
5489
- run();
5490
5636
  return deps.length;
5491
5637
  }
5492
5638
  function parsePackageJson(filePath) {
@@ -5739,12 +5885,11 @@ function writeDriftLog(db, entries) {
5739
5885
  VALUES (?, ?, ?, ?, ?, ?)
5740
5886
  `);
5741
5887
  const now = (/* @__PURE__ */ new Date()).toISOString();
5742
- const run = db.transaction(() => {
5888
+ transaction(db, () => {
5743
5889
  for (const e of entries) {
5744
5890
  insert.run(now, e.kind, e.severity, e.description, e.affectedBlock, e.affectedFile);
5745
5891
  }
5746
5892
  });
5747
- run();
5748
5893
  }
5749
5894
  function detectUndocumentedModules(db, entries, ignorePaths = []) {
5750
5895
  const blocks = db.prepare("SELECT id, name, code_paths FROM building_blocks").all();
@@ -6063,12 +6208,11 @@ function applyInferences(db, inferences, projectRoot) {
6063
6208
  "UPDATE tasks SET status = ?, completed_at = CASE WHEN ? = 'done' THEN ? ELSE completed_at END WHERE id = ?"
6064
6209
  );
6065
6210
  const now = (/* @__PURE__ */ new Date()).toISOString();
6066
- const run = db.transaction(() => {
6211
+ transaction(db, () => {
6067
6212
  for (const inf of inferences) {
6068
6213
  update.run(inf.inferredStatus, inf.inferredStatus, now, inf.taskId);
6069
6214
  }
6070
6215
  });
6071
- run();
6072
6216
  for (const inf of inferences) {
6073
6217
  const task = db.prepare("SELECT phase_id FROM tasks WHERE id = ?").get(inf.taskId);
6074
6218
  if (task) {
@@ -6373,6 +6517,329 @@ function generateSyncFiles(targetDir, config) {
6373
6517
  return generated;
6374
6518
  }
6375
6519
 
6520
+ // src/metrics/activity.ts
6521
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
6522
+ import { join as join16 } from "path";
6523
+ function insertActivity(db, params) {
6524
+ const totalTokens = params.totalTokens ?? (params.inputTokens != null && params.outputTokens != null ? params.inputTokens + params.outputTokens : null);
6525
+ const stmt = db.prepare(`
6526
+ INSERT INTO agent_activity (
6527
+ tool_name, action, model, agent_role,
6528
+ task_id, phase_id,
6529
+ input_tokens, output_tokens, total_tokens, cost_usd, duration_ms,
6530
+ drift_count, drift_errors, test_pass_count, test_fail_count,
6531
+ lint_clean, typecheck_clean,
6532
+ notes, metadata, recorded_at
6533
+ ) VALUES (
6534
+ ?, ?, ?, ?,
6535
+ ?, ?,
6536
+ ?, ?, ?, ?, ?,
6537
+ ?, ?, ?, ?,
6538
+ ?, ?,
6539
+ ?, ?, ?
6540
+ )
6541
+ `);
6542
+ const result = stmt.run(
6543
+ params.toolName,
6544
+ params.action ?? null,
6545
+ params.model ?? null,
6546
+ params.agentRole ?? null,
6547
+ params.taskId ?? null,
6548
+ params.phaseId ?? null,
6549
+ params.inputTokens ?? null,
6550
+ params.outputTokens ?? null,
6551
+ totalTokens,
6552
+ params.costUsd ?? null,
6553
+ params.durationMs ?? null,
6554
+ params.driftCount ?? null,
6555
+ params.driftErrors ?? null,
6556
+ params.testPassCount ?? null,
6557
+ params.testFailCount ?? null,
6558
+ params.lintClean != null ? params.lintClean ? 1 : 0 : null,
6559
+ params.typecheckClean != null ? params.typecheckClean ? 1 : 0 : null,
6560
+ params.notes ?? null,
6561
+ JSON.stringify(params.metadata ?? {}),
6562
+ (/* @__PURE__ */ new Date()).toISOString()
6563
+ );
6564
+ return Number(result.lastInsertRowid);
6565
+ }
6566
+ function getSessionTotals(db, since, model) {
6567
+ const conditions = [];
6568
+ const values = [];
6569
+ if (since) {
6570
+ conditions.push("recorded_at >= ?");
6571
+ values.push(since);
6572
+ }
6573
+ if (model) {
6574
+ conditions.push("model = ?");
6575
+ values.push(model);
6576
+ }
6577
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
6578
+ const row = db.prepare(
6579
+ `SELECT
6580
+ COALESCE(SUM(cost_usd), 0) as total_cost,
6581
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
6582
+ COUNT(*) as activity_count
6583
+ FROM agent_activity ${where}`
6584
+ ).get(...values);
6585
+ return {
6586
+ totalCost: row.total_cost,
6587
+ totalTokens: row.total_tokens,
6588
+ activityCount: row.activity_count
6589
+ };
6590
+ }
6591
+ function queryMetrics(db, params) {
6592
+ const conditions = [];
6593
+ const values = [];
6594
+ if (params.taskId) {
6595
+ conditions.push("task_id = ?");
6596
+ values.push(params.taskId);
6597
+ }
6598
+ if (params.phaseId) {
6599
+ conditions.push("phase_id = ?");
6600
+ values.push(params.phaseId);
6601
+ }
6602
+ if (params.model) {
6603
+ conditions.push("model = ?");
6604
+ values.push(params.model);
6605
+ }
6606
+ if (params.agentRole) {
6607
+ conditions.push("agent_role = ?");
6608
+ values.push(params.agentRole);
6609
+ }
6610
+ if (params.toolName) {
6611
+ conditions.push("tool_name = ?");
6612
+ values.push(params.toolName);
6613
+ }
6614
+ if (params.since) {
6615
+ conditions.push("recorded_at >= ?");
6616
+ values.push(params.since);
6617
+ }
6618
+ if (params.until) {
6619
+ conditions.push("recorded_at <= ?");
6620
+ values.push(params.until);
6621
+ }
6622
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
6623
+ const qualitySnapshot = getLatestQualitySnapshot(db, where, values);
6624
+ const totalsRow = db.prepare(
6625
+ `SELECT
6626
+ COALESCE(SUM(cost_usd), 0) as total_cost,
6627
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
6628
+ COUNT(*) as activity_count,
6629
+ MIN(recorded_at) as first_at,
6630
+ MAX(recorded_at) as last_at
6631
+ FROM agent_activity ${where}`
6632
+ ).get(...values);
6633
+ const totals = {
6634
+ totalCost: totalsRow.total_cost,
6635
+ totalTokens: totalsRow.total_tokens,
6636
+ activityCount: totalsRow.activity_count
6637
+ };
6638
+ const timeSpan = totalsRow.first_at && totalsRow.last_at ? { first: totalsRow.first_at, last: totalsRow.last_at } : null;
6639
+ if (params.groupBy === "none") {
6640
+ const rows2 = db.prepare(
6641
+ `SELECT * FROM agent_activity ${where} ORDER BY recorded_at DESC LIMIT ?`
6642
+ ).all(...values, params.limit);
6643
+ return { rows: rows2, grouped: false, qualitySnapshot, totals, timeSpan };
6644
+ }
6645
+ const groupColumn = getGroupColumn(params.groupBy);
6646
+ const rows = db.prepare(
6647
+ `SELECT
6648
+ ${groupColumn} as group_key,
6649
+ COUNT(*) as activity_count,
6650
+ SUM(total_tokens) as sum_tokens,
6651
+ ROUND(AVG(total_tokens)) as avg_tokens,
6652
+ ROUND(SUM(cost_usd), 4) as sum_cost,
6653
+ ROUND(AVG(duration_ms)) as avg_duration,
6654
+ MIN(recorded_at) as first_activity,
6655
+ MAX(recorded_at) as last_activity
6656
+ FROM agent_activity ${where}
6657
+ GROUP BY ${groupColumn}
6658
+ ORDER BY sum_cost DESC`
6659
+ ).all(...values);
6660
+ const aggregated = rows.map((r) => ({
6661
+ groupKey: r.group_key ?? "(none)",
6662
+ activityCount: r.activity_count,
6663
+ sumTokens: r.sum_tokens,
6664
+ avgTokens: r.avg_tokens,
6665
+ sumCost: r.sum_cost,
6666
+ avgDuration: r.avg_duration,
6667
+ firstActivity: r.first_activity,
6668
+ lastActivity: r.last_activity
6669
+ }));
6670
+ return { rows: aggregated, grouped: true, qualitySnapshot, totals, timeSpan };
6671
+ }
6672
+ function exportMetrics(db, projectRoot, format, params, maxRows = 1e5) {
6673
+ const result = queryMetrics(db, {
6674
+ ...params,
6675
+ groupBy: "none",
6676
+ limit: maxRows
6677
+ });
6678
+ const rows = result.rows;
6679
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
6680
+ const dir = join16(projectRoot, ".arcbridge", "metrics");
6681
+ mkdirSync7(dir, { recursive: true });
6682
+ let content;
6683
+ let filename;
6684
+ switch (format) {
6685
+ case "json": {
6686
+ filename = `activity-${timestamp}.json`;
6687
+ content = JSON.stringify(
6688
+ {
6689
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
6690
+ totals: result.totals,
6691
+ quality_snapshot: result.qualitySnapshot,
6692
+ activities: rows.map((r) => ({
6693
+ ...r,
6694
+ metadata: safeParseJson3(r.metadata)
6695
+ }))
6696
+ },
6697
+ null,
6698
+ 2
6699
+ );
6700
+ break;
6701
+ }
6702
+ case "csv": {
6703
+ filename = `activity-${timestamp}.csv`;
6704
+ const headers = [
6705
+ "id",
6706
+ "recorded_at",
6707
+ "tool_name",
6708
+ "action",
6709
+ "model",
6710
+ "agent_role",
6711
+ "task_id",
6712
+ "phase_id",
6713
+ "input_tokens",
6714
+ "output_tokens",
6715
+ "total_tokens",
6716
+ "cost_usd",
6717
+ "duration_ms",
6718
+ "drift_count",
6719
+ "drift_errors",
6720
+ "test_pass_count",
6721
+ "test_fail_count",
6722
+ "lint_clean",
6723
+ "typecheck_clean",
6724
+ "notes"
6725
+ ];
6726
+ const csvRows = rows.map(
6727
+ (r) => headers.map((h) => {
6728
+ const val = r[h];
6729
+ if (val == null) return "";
6730
+ const str = String(val);
6731
+ return str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r") ? `"${str.replace(/"/g, '""')}"` : str;
6732
+ }).join(",")
6733
+ );
6734
+ content = [headers.join(","), ...csvRows].join("\n");
6735
+ break;
6736
+ }
6737
+ case "markdown": {
6738
+ filename = `activity-${timestamp}.md`;
6739
+ const lines = [
6740
+ `# Agent Activity Report`,
6741
+ "",
6742
+ `**Exported:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
6743
+ `**Activities:** ${result.totals.activityCount}`,
6744
+ `**Total cost:** $${result.totals.totalCost.toFixed(4)}`,
6745
+ `**Total tokens:** ${result.totals.totalTokens.toLocaleString()}`,
6746
+ ""
6747
+ ];
6748
+ if (result.qualitySnapshot.capturedAt) {
6749
+ const q = result.qualitySnapshot;
6750
+ lines.push(
6751
+ "## Latest Quality Snapshot",
6752
+ "",
6753
+ `| Metric | Value |`,
6754
+ `|--------|-------|`
6755
+ );
6756
+ if (q.driftCount != null) lines.push(`| Drift issues | ${q.driftCount} (${q.driftErrors ?? 0} errors) |`);
6757
+ if (q.testPassCount != null) lines.push(`| Tests | ${q.testPassCount} pass / ${q.testFailCount ?? 0} fail |`);
6758
+ if (q.lintClean != null) lines.push(`| Lint | ${q.lintClean ? "clean" : "errors"} |`);
6759
+ if (q.typecheckClean != null) lines.push(`| Typecheck | ${q.typecheckClean ? "clean" : "errors"} |`);
6760
+ lines.push("");
6761
+ }
6762
+ lines.push(
6763
+ "## Activities",
6764
+ "",
6765
+ "| Time | Tool | Action | Model | Tokens | Cost | Duration |",
6766
+ "|------|------|--------|-------|--------|------|----------|"
6767
+ );
6768
+ for (const r of rows) {
6769
+ lines.push(
6770
+ `| ${r.recorded_at.slice(0, 19)} | ${esc(r.tool_name)} | ${esc(r.action)} | ${esc(r.model)} | ${r.total_tokens ?? ""} | ${r.cost_usd != null ? "$" + r.cost_usd.toFixed(4) : ""} | ${r.duration_ms != null ? r.duration_ms + "ms" : ""} |`
6771
+ );
6772
+ }
6773
+ content = lines.join("\n") + "\n";
6774
+ break;
6775
+ }
6776
+ }
6777
+ const filePath = join16(dir, filename);
6778
+ writeFileSync7(filePath, content, "utf-8");
6779
+ return filePath;
6780
+ }
6781
+ function getGroupColumn(groupBy) {
6782
+ switch (groupBy) {
6783
+ case "model":
6784
+ return "model";
6785
+ case "task":
6786
+ return "task_id";
6787
+ case "phase":
6788
+ return "phase_id";
6789
+ case "tool":
6790
+ return "tool_name";
6791
+ case "day":
6792
+ return "DATE(recorded_at)";
6793
+ default:
6794
+ return "model";
6795
+ }
6796
+ }
6797
+ function getLatestQualitySnapshot(db, where, values) {
6798
+ const row = db.prepare(
6799
+ `SELECT
6800
+ drift_count, drift_errors,
6801
+ test_pass_count, test_fail_count,
6802
+ lint_clean, typecheck_clean,
6803
+ recorded_at
6804
+ FROM agent_activity
6805
+ ${where ? where + " AND" : "WHERE"}
6806
+ (drift_count IS NOT NULL OR test_pass_count IS NOT NULL OR lint_clean IS NOT NULL OR typecheck_clean IS NOT NULL)
6807
+ ORDER BY recorded_at DESC
6808
+ LIMIT 1`
6809
+ ).get(...values);
6810
+ if (!row) {
6811
+ return {
6812
+ driftCount: null,
6813
+ driftErrors: null,
6814
+ testPassCount: null,
6815
+ testFailCount: null,
6816
+ lintClean: null,
6817
+ typecheckClean: null,
6818
+ capturedAt: null
6819
+ };
6820
+ }
6821
+ return {
6822
+ driftCount: row.drift_count,
6823
+ driftErrors: row.drift_errors,
6824
+ testPassCount: row.test_pass_count,
6825
+ testFailCount: row.test_fail_count,
6826
+ lintClean: row.lint_clean != null ? row.lint_clean === 1 : null,
6827
+ typecheckClean: row.typecheck_clean != null ? row.typecheck_clean === 1 : null,
6828
+ capturedAt: row.recorded_at
6829
+ };
6830
+ }
6831
+ function safeParseJson3(raw) {
6832
+ try {
6833
+ return JSON.parse(raw);
6834
+ } catch {
6835
+ return {};
6836
+ }
6837
+ }
6838
+ function esc(val) {
6839
+ if (val == null) return "";
6840
+ return val.replace(/\|/g, "\\|").replace(/\r?\n|\r/g, " ");
6841
+ }
6842
+
6376
6843
  // src/git/helpers.ts
6377
6844
  import { execFileSync as execFileSync3 } from "child_process";
6378
6845
  function resolveRef(projectRoot, since, db) {
@@ -6579,10 +7046,10 @@ ${output}`;
6579
7046
 
6580
7047
  // src/roles/loader.ts
6581
7048
  import { readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
6582
- import { join as join16 } from "path";
7049
+ import { join as join17 } from "path";
6583
7050
  import matter4 from "gray-matter";
6584
7051
  function loadRoles(projectRoot) {
6585
- const agentsDir = join16(projectRoot, ".arcbridge", "agents");
7052
+ const agentsDir = join17(projectRoot, ".arcbridge", "agents");
6586
7053
  const roles = [];
6587
7054
  const errors = [];
6588
7055
  let files;
@@ -6592,7 +7059,7 @@ function loadRoles(projectRoot) {
6592
7059
  return { roles: [], errors: [`Agent directory not found: ${agentsDir}`] };
6593
7060
  }
6594
7061
  for (const file of files) {
6595
- const filePath = join16(agentsDir, file);
7062
+ const filePath = join17(agentsDir, file);
6596
7063
  try {
6597
7064
  const raw = readFileSync9(filePath, "utf-8");
6598
7065
  const parsed = matter4(raw);
@@ -6619,7 +7086,7 @@ function loadRole(projectRoot, roleId) {
6619
7086
  if (!/^[a-z0-9-]+$/.test(roleId)) {
6620
7087
  return { role: null, error: `Invalid role ID: "${roleId}" (must be kebab-case)` };
6621
7088
  }
6622
- const filePath = join16(projectRoot, ".arcbridge", "agents", `${roleId}.md`);
7089
+ const filePath = join17(projectRoot, ".arcbridge", "agents", `${roleId}.md`);
6623
7090
  try {
6624
7091
  const raw = readFileSync9(filePath, "utf-8");
6625
7092
  const parsed = matter4(raw);
@@ -6661,6 +7128,7 @@ export {
6661
7128
  detectDrift,
6662
7129
  detectProjectLanguage,
6663
7130
  discoverDotnetServices,
7131
+ exportMetrics,
6664
7132
  generateAgentRoles,
6665
7133
  generateArc42,
6666
7134
  generateConfig,
@@ -6669,23 +7137,28 @@ export {
6669
7137
  generateSyncFiles,
6670
7138
  getChangedFiles,
6671
7139
  getHeadSha,
7140
+ getSessionTotals,
6672
7141
  getUncommittedChanges,
6673
7142
  indexPackageDependencies,
6674
7143
  indexProject,
6675
7144
  inferTaskStatuses,
6676
7145
  initializeSchema,
7146
+ insertActivity,
6677
7147
  loadConfig,
6678
7148
  loadRole,
6679
7149
  loadRoles,
6680
7150
  migrate,
6681
7151
  openDatabase,
6682
7152
  openMemoryDatabase,
7153
+ queryMetrics,
6683
7154
  refreshFromDocs,
6684
7155
  resolveRef,
6685
7156
  setSyncCommit,
7157
+ suppressSqliteWarning,
6686
7158
  syncPhaseToYaml,
6687
7159
  syncScenarioToYaml,
6688
7160
  syncTaskToYaml,
7161
+ transaction,
6689
7162
  verifyScenarios,
6690
7163
  writeDriftLog
6691
7164
  };