@arcbridge/core 0.1.0 → 0.1.2

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
@@ -32,7 +32,7 @@ var ArcBridgeConfigSchema = z.object({
32
32
  exclude: z.array(z.string()).default(["node_modules", "dist", ".next", "coverage"]),
33
33
  default_mode: z.enum(["fast", "deep"]).default("fast"),
34
34
  csharp_indexer: z.enum(["auto", "roslyn", "tree-sitter"]).default("auto").describe(
35
- "C# indexer backend: 'auto' uses Roslyn if available, otherwise tree-sitter. 'tree-sitter' works without .NET SDK. 'roslyn' requires .NET SDK + global tool."
35
+ "C# indexer backend: 'auto' prefers the arcbridge-dotnet-indexer global tool, falls back to monorepo source if dotnet CLI is available, else tree-sitter. 'tree-sitter' works without .NET SDK. 'roslyn' requires global tool or monorepo source + .NET SDK."
36
36
  )
37
37
  }).default({}),
38
38
  testing: z.object({
@@ -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"),
@@ -217,7 +222,7 @@ function openMemoryDatabase() {
217
222
  }
218
223
 
219
224
  // src/db/schema.ts
220
- var CURRENT_SCHEMA_VERSION = 1;
225
+ var CURRENT_SCHEMA_VERSION = 2;
221
226
  var SCHEMA_SQL = `
222
227
  -- Metadata
223
228
  CREATE TABLE IF NOT EXISTS arcbridge_meta (
@@ -383,6 +388,36 @@ CREATE TABLE IF NOT EXISTS drift_log (
383
388
  resolution TEXT CHECK(resolution IN ('accepted','fixed','deferred') OR resolution IS NULL),
384
389
  resolved_at TEXT
385
390
  );
391
+
392
+ -- Agent Activity Metrics (operational telemetry, not rebuilt by refreshFromDocs)
393
+ CREATE TABLE IF NOT EXISTS agent_activity (
394
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
395
+ tool_name TEXT NOT NULL,
396
+ action TEXT,
397
+ model TEXT,
398
+ agent_role TEXT,
399
+ task_id TEXT,
400
+ phase_id TEXT,
401
+ input_tokens INTEGER,
402
+ output_tokens INTEGER,
403
+ total_tokens INTEGER,
404
+ cost_usd REAL,
405
+ duration_ms INTEGER,
406
+ drift_count INTEGER,
407
+ drift_errors INTEGER,
408
+ test_pass_count INTEGER,
409
+ test_fail_count INTEGER,
410
+ lint_clean INTEGER,
411
+ typecheck_clean INTEGER,
412
+ notes TEXT,
413
+ metadata TEXT NOT NULL DEFAULT '{}',
414
+ recorded_at TEXT NOT NULL
415
+ );
416
+
417
+ CREATE INDEX IF NOT EXISTS idx_activity_recorded_at ON agent_activity(recorded_at);
418
+ CREATE INDEX IF NOT EXISTS idx_activity_model ON agent_activity(model);
419
+ CREATE INDEX IF NOT EXISTS idx_activity_task ON agent_activity(task_id);
420
+ CREATE INDEX IF NOT EXISTS idx_activity_phase ON agent_activity(phase_id);
386
421
  `;
387
422
  function initializeSchema(db) {
388
423
  db.exec(SCHEMA_SQL);
@@ -398,7 +433,42 @@ function initializeSchema(db) {
398
433
  }
399
434
 
400
435
  // src/db/migrations.ts
401
- var migrations = [];
436
+ var migrations = [
437
+ {
438
+ version: 2,
439
+ up: (db) => {
440
+ db.exec(`
441
+ CREATE TABLE IF NOT EXISTS agent_activity (
442
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
443
+ tool_name TEXT NOT NULL,
444
+ action TEXT,
445
+ model TEXT,
446
+ agent_role TEXT,
447
+ task_id TEXT,
448
+ phase_id TEXT,
449
+ input_tokens INTEGER,
450
+ output_tokens INTEGER,
451
+ total_tokens INTEGER,
452
+ cost_usd REAL,
453
+ duration_ms INTEGER,
454
+ drift_count INTEGER,
455
+ drift_errors INTEGER,
456
+ test_pass_count INTEGER,
457
+ test_fail_count INTEGER,
458
+ lint_clean INTEGER,
459
+ typecheck_clean INTEGER,
460
+ notes TEXT,
461
+ metadata TEXT NOT NULL DEFAULT '{}',
462
+ recorded_at TEXT NOT NULL
463
+ );
464
+ CREATE INDEX IF NOT EXISTS idx_activity_recorded_at ON agent_activity(recorded_at);
465
+ CREATE INDEX IF NOT EXISTS idx_activity_model ON agent_activity(model);
466
+ CREATE INDEX IF NOT EXISTS idx_activity_task ON agent_activity(task_id);
467
+ CREATE INDEX IF NOT EXISTS idx_activity_phase ON agent_activity(phase_id);
468
+ `);
469
+ }
470
+ }
471
+ ];
402
472
  function migrate(db) {
403
473
  const row = db.prepare("SELECT value FROM arcbridge_meta WHERE key = 'schema_version'").get();
404
474
  const currentVersion = row ? Number(row.value) : 0;
@@ -450,6 +520,7 @@ function configTemplate(input) {
450
520
  drift: {
451
521
  ignore_paths: []
452
522
  },
523
+ metrics: { auto_record: false },
453
524
  sync: {
454
525
  auto_detect_drift: true,
455
526
  drift_severity_threshold: "warning",
@@ -487,6 +558,7 @@ function configTemplate2(input) {
487
558
  drift: {
488
559
  ignore_paths: []
489
560
  },
561
+ metrics: { auto_record: false },
490
562
  sync: {
491
563
  auto_detect_drift: true,
492
564
  drift_severity_threshold: "warning",
@@ -524,6 +596,7 @@ function configTemplate3(input) {
524
596
  drift: {
525
597
  ignore_paths: []
526
598
  },
599
+ metrics: { auto_record: false },
527
600
  sync: {
528
601
  auto_detect_drift: true,
529
602
  drift_severity_threshold: "warning",
@@ -559,6 +632,7 @@ function configTemplate4(input) {
559
632
  drift: {
560
633
  ignore_paths: []
561
634
  },
635
+ metrics: { auto_record: false },
562
636
  sync: {
563
637
  auto_detect_drift: true,
564
638
  drift_severity_threshold: "warning",
@@ -3204,6 +3278,8 @@ function refreshFromDocs(db, targetDir) {
3204
3278
  const refresh = db.transaction(() => {
3205
3279
  db.prepare("DELETE FROM tasks").run();
3206
3280
  db.prepare("DELETE FROM phases").run();
3281
+ db.prepare("UPDATE contracts SET building_block = NULL").run();
3282
+ db.prepare("UPDATE building_blocks SET parent_id = NULL").run();
3207
3283
  db.prepare("DELETE FROM building_blocks").run();
3208
3284
  db.prepare("DELETE FROM quality_scenarios").run();
3209
3285
  db.prepare("DELETE FROM adrs").run();
@@ -4315,7 +4391,7 @@ function writeDependencies(db, dependencies) {
4315
4391
  // src/indexer/dotnet-indexer.ts
4316
4392
  import { execFileSync } from "child_process";
4317
4393
  import { resolve, join as join9, dirname, relative as relative4, basename } from "path";
4318
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
4394
+ import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, accessSync, constants } from "fs";
4319
4395
  import { fileURLToPath } from "url";
4320
4396
  function findDotnetProject(projectRoot) {
4321
4397
  try {
@@ -4366,6 +4442,24 @@ function discoverDotnetServices(projectRoot) {
4366
4442
  }
4367
4443
  return parseSolutionProjects(slnPath);
4368
4444
  }
4445
+ function hasGlobalTool() {
4446
+ const pathEnv = process.env.PATH ?? "";
4447
+ const dirs = pathEnv.split(process.platform === "win32" ? ";" : ":");
4448
+ const name = "arcbridge-dotnet-indexer";
4449
+ const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
4450
+ for (const dir of dirs) {
4451
+ if (!dir) continue;
4452
+ for (const ext of extensions) {
4453
+ try {
4454
+ accessSync(join9(dir, `${name}${ext}`), constants.X_OK);
4455
+ return true;
4456
+ } catch {
4457
+ continue;
4458
+ }
4459
+ }
4460
+ }
4461
+ return false;
4462
+ }
4369
4463
  function resolveIndexerProject() {
4370
4464
  const currentDir2 = dirname(fileURLToPath(import.meta.url));
4371
4465
  const candidates = [
@@ -4376,71 +4470,70 @@ function resolveIndexerProject() {
4376
4470
  for (const candidate of candidates) {
4377
4471
  if (existsSync4(candidate)) return candidate;
4378
4472
  }
4379
- throw new Error(
4380
- "Could not find ArcBridge.DotnetIndexer.csproj. Ensure the dotnet-indexer package is present in the monorepo."
4381
- );
4473
+ return null;
4382
4474
  }
4383
- function indexDotnetProjectRoslyn(db, options) {
4384
- const start = Date.now();
4385
- const service = options.service ?? "main";
4386
- const projectRoot = resolve(options.projectRoot);
4387
- const dotnetProject = options.csprojPath ?? findDotnetProject(projectRoot);
4388
- if (!dotnetProject) {
4389
- throw new Error(
4390
- "No .sln or .csproj file found in project root. The .NET indexer requires a project or solution file."
4391
- );
4475
+ function hasIndexerProject() {
4476
+ return resolveIndexerProject() !== null;
4477
+ }
4478
+ var EXEC_OPTIONS = {
4479
+ encoding: "utf-8",
4480
+ maxBuffer: 50 * 1024 * 1024,
4481
+ // 50MB for large projects
4482
+ timeout: 3e5
4483
+ // 5 minutes
4484
+ };
4485
+ function runDotnetIndexer(dotnetProject, hashesJson, cwd) {
4486
+ const args = [dotnetProject, "--existing-hashes", hashesJson];
4487
+ const preferSource = process.env.ARCBRIDGE_PREFER_SOURCE === "1";
4488
+ let globalToolError = null;
4489
+ if (!preferSource && hasGlobalTool()) {
4490
+ try {
4491
+ return execFileSync("arcbridge-dotnet-indexer", args, { ...EXEC_OPTIONS, cwd });
4492
+ } catch (err) {
4493
+ globalToolError = err;
4494
+ }
4392
4495
  }
4393
- const existingHashes = getExistingHashes(db, service);
4394
- const hashesJson = JSON.stringify(Object.fromEntries(existingHashes));
4395
4496
  const indexerProject = resolveIndexerProject();
4396
- let stdout;
4497
+ if (!indexerProject) {
4498
+ const base = "Roslyn C# indexer not available. Either install the global tool (`dotnet tool install -g arcbridge-dotnet-indexer`) or run from the ArcBridge monorepo.";
4499
+ if (globalToolError) {
4500
+ const msg = globalToolError instanceof Error ? globalToolError.message : String(globalToolError);
4501
+ throw new Error(`${base} Global tool was found but failed: ${msg}`, { cause: globalToolError });
4502
+ }
4503
+ throw new Error(base);
4504
+ }
4397
4505
  try {
4398
- stdout = execFileSync(
4506
+ return execFileSync(
4399
4507
  "dotnet",
4400
- [
4401
- "run",
4402
- "--project",
4403
- indexerProject,
4404
- "--no-build",
4405
- "--",
4406
- dotnetProject,
4407
- "--existing-hashes",
4408
- hashesJson
4409
- ],
4410
- {
4411
- encoding: "utf-8",
4412
- maxBuffer: 50 * 1024 * 1024,
4413
- // 50MB for large projects
4414
- timeout: 3e5,
4415
- // 5 minutes
4416
- cwd: projectRoot
4417
- }
4508
+ ["run", "--project", indexerProject, "--no-build", "--", ...args],
4509
+ { ...EXEC_OPTIONS, cwd }
4418
4510
  );
4419
4511
  } catch {
4420
4512
  try {
4421
- stdout = execFileSync(
4513
+ return execFileSync(
4422
4514
  "dotnet",
4423
- [
4424
- "run",
4425
- "--project",
4426
- indexerProject,
4427
- "--",
4428
- dotnetProject,
4429
- "--existing-hashes",
4430
- hashesJson
4431
- ],
4432
- {
4433
- encoding: "utf-8",
4434
- maxBuffer: 50 * 1024 * 1024,
4435
- timeout: 3e5,
4436
- cwd: projectRoot
4437
- }
4515
+ ["run", "--project", indexerProject, "--", ...args],
4516
+ { ...EXEC_OPTIONS, cwd }
4438
4517
  );
4439
4518
  } catch (retryErr) {
4440
4519
  const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
4441
4520
  throw new Error(`.NET indexer failed: ${message}`, { cause: retryErr });
4442
4521
  }
4443
4522
  }
4523
+ }
4524
+ function indexDotnetProjectRoslyn(db, options) {
4525
+ const start = Date.now();
4526
+ const service = options.service ?? "main";
4527
+ const projectRoot = resolve(options.projectRoot);
4528
+ const dotnetProject = options.csprojPath ?? findDotnetProject(projectRoot);
4529
+ if (!dotnetProject) {
4530
+ throw new Error(
4531
+ "No .sln or .csproj file found in project root. The .NET indexer requires a project or solution file."
4532
+ );
4533
+ }
4534
+ const existingHashes = getExistingHashes(db, service);
4535
+ const hashesJson = JSON.stringify(Object.fromEntries(existingHashes));
4536
+ const stdout = runDotnetIndexer(dotnetProject, hashesJson, projectRoot);
4444
4537
  const lines = stdout.trim().split("\n");
4445
4538
  const jsonLine = lines.reverse().find((l) => l.startsWith("{"));
4446
4539
  if (!jsonLine) {
@@ -4522,7 +4615,7 @@ import { join as join10 } from "path";
4522
4615
  import { globbySync } from "globby";
4523
4616
 
4524
4617
  // src/indexer/csharp/parser.ts
4525
- import { accessSync, constants } from "fs";
4618
+ import { accessSync as accessSync2, constants as constants2 } from "fs";
4526
4619
  import { dirname as dirname2, resolve as resolve2 } from "path";
4527
4620
  import { fileURLToPath as fileURLToPath2 } from "url";
4528
4621
  import "web-tree-sitter";
@@ -4538,7 +4631,7 @@ function resolveGrammarPath() {
4538
4631
  ];
4539
4632
  for (const candidate of candidates) {
4540
4633
  try {
4541
- accessSync(candidate, constants.R_OK);
4634
+ accessSync2(candidate, constants2.R_OK);
4542
4635
  return candidate;
4543
4636
  } catch {
4544
4637
  continue;
@@ -5597,15 +5690,20 @@ function resolveCSharpBackend(projectRoot) {
5597
5690
  if (setting === "roslyn" || setting === "tree-sitter") {
5598
5691
  return setting;
5599
5692
  }
5600
- try {
5601
- execFileSync2("dotnet", ["--version"], {
5602
- encoding: "utf-8",
5603
- timeout: 5e3
5604
- });
5693
+ if (hasGlobalTool()) {
5605
5694
  return "roslyn";
5606
- } catch {
5607
- return "tree-sitter";
5608
5695
  }
5696
+ if (hasIndexerProject()) {
5697
+ try {
5698
+ execFileSync2("dotnet", ["--version"], {
5699
+ encoding: "utf-8",
5700
+ timeout: 5e3
5701
+ });
5702
+ return "roslyn";
5703
+ } catch {
5704
+ }
5705
+ }
5706
+ return "tree-sitter";
5609
5707
  }
5610
5708
  function indexTypeScriptProject(db, options) {
5611
5709
  const start = Date.now();
@@ -6351,6 +6449,329 @@ function generateSyncFiles(targetDir, config) {
6351
6449
  return generated;
6352
6450
  }
6353
6451
 
6452
+ // src/metrics/activity.ts
6453
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
6454
+ import { join as join16 } from "path";
6455
+ function insertActivity(db, params) {
6456
+ const totalTokens = params.totalTokens ?? (params.inputTokens != null && params.outputTokens != null ? params.inputTokens + params.outputTokens : null);
6457
+ const stmt = db.prepare(`
6458
+ INSERT INTO agent_activity (
6459
+ tool_name, action, model, agent_role,
6460
+ task_id, phase_id,
6461
+ input_tokens, output_tokens, total_tokens, cost_usd, duration_ms,
6462
+ drift_count, drift_errors, test_pass_count, test_fail_count,
6463
+ lint_clean, typecheck_clean,
6464
+ notes, metadata, recorded_at
6465
+ ) VALUES (
6466
+ ?, ?, ?, ?,
6467
+ ?, ?,
6468
+ ?, ?, ?, ?, ?,
6469
+ ?, ?, ?, ?,
6470
+ ?, ?,
6471
+ ?, ?, ?
6472
+ )
6473
+ `);
6474
+ const result = stmt.run(
6475
+ params.toolName,
6476
+ params.action ?? null,
6477
+ params.model ?? null,
6478
+ params.agentRole ?? null,
6479
+ params.taskId ?? null,
6480
+ params.phaseId ?? null,
6481
+ params.inputTokens ?? null,
6482
+ params.outputTokens ?? null,
6483
+ totalTokens,
6484
+ params.costUsd ?? null,
6485
+ params.durationMs ?? null,
6486
+ params.driftCount ?? null,
6487
+ params.driftErrors ?? null,
6488
+ params.testPassCount ?? null,
6489
+ params.testFailCount ?? null,
6490
+ params.lintClean != null ? params.lintClean ? 1 : 0 : null,
6491
+ params.typecheckClean != null ? params.typecheckClean ? 1 : 0 : null,
6492
+ params.notes ?? null,
6493
+ JSON.stringify(params.metadata ?? {}),
6494
+ (/* @__PURE__ */ new Date()).toISOString()
6495
+ );
6496
+ return Number(result.lastInsertRowid);
6497
+ }
6498
+ function getSessionTotals(db, since, model) {
6499
+ const conditions = [];
6500
+ const values = [];
6501
+ if (since) {
6502
+ conditions.push("recorded_at >= ?");
6503
+ values.push(since);
6504
+ }
6505
+ if (model) {
6506
+ conditions.push("model = ?");
6507
+ values.push(model);
6508
+ }
6509
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
6510
+ const row = db.prepare(
6511
+ `SELECT
6512
+ COALESCE(SUM(cost_usd), 0) as total_cost,
6513
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
6514
+ COUNT(*) as activity_count
6515
+ FROM agent_activity ${where}`
6516
+ ).get(...values);
6517
+ return {
6518
+ totalCost: row.total_cost,
6519
+ totalTokens: row.total_tokens,
6520
+ activityCount: row.activity_count
6521
+ };
6522
+ }
6523
+ function queryMetrics(db, params) {
6524
+ const conditions = [];
6525
+ const values = [];
6526
+ if (params.taskId) {
6527
+ conditions.push("task_id = ?");
6528
+ values.push(params.taskId);
6529
+ }
6530
+ if (params.phaseId) {
6531
+ conditions.push("phase_id = ?");
6532
+ values.push(params.phaseId);
6533
+ }
6534
+ if (params.model) {
6535
+ conditions.push("model = ?");
6536
+ values.push(params.model);
6537
+ }
6538
+ if (params.agentRole) {
6539
+ conditions.push("agent_role = ?");
6540
+ values.push(params.agentRole);
6541
+ }
6542
+ if (params.toolName) {
6543
+ conditions.push("tool_name = ?");
6544
+ values.push(params.toolName);
6545
+ }
6546
+ if (params.since) {
6547
+ conditions.push("recorded_at >= ?");
6548
+ values.push(params.since);
6549
+ }
6550
+ if (params.until) {
6551
+ conditions.push("recorded_at <= ?");
6552
+ values.push(params.until);
6553
+ }
6554
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
6555
+ const qualitySnapshot = getLatestQualitySnapshot(db, where, values);
6556
+ const totalsRow = db.prepare(
6557
+ `SELECT
6558
+ COALESCE(SUM(cost_usd), 0) as total_cost,
6559
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
6560
+ COUNT(*) as activity_count,
6561
+ MIN(recorded_at) as first_at,
6562
+ MAX(recorded_at) as last_at
6563
+ FROM agent_activity ${where}`
6564
+ ).get(...values);
6565
+ const totals = {
6566
+ totalCost: totalsRow.total_cost,
6567
+ totalTokens: totalsRow.total_tokens,
6568
+ activityCount: totalsRow.activity_count
6569
+ };
6570
+ const timeSpan = totalsRow.first_at && totalsRow.last_at ? { first: totalsRow.first_at, last: totalsRow.last_at } : null;
6571
+ if (params.groupBy === "none") {
6572
+ const rows2 = db.prepare(
6573
+ `SELECT * FROM agent_activity ${where} ORDER BY recorded_at DESC LIMIT ?`
6574
+ ).all(...values, params.limit);
6575
+ return { rows: rows2, grouped: false, qualitySnapshot, totals, timeSpan };
6576
+ }
6577
+ const groupColumn = getGroupColumn(params.groupBy);
6578
+ const rows = db.prepare(
6579
+ `SELECT
6580
+ ${groupColumn} as group_key,
6581
+ COUNT(*) as activity_count,
6582
+ SUM(total_tokens) as sum_tokens,
6583
+ ROUND(AVG(total_tokens)) as avg_tokens,
6584
+ ROUND(SUM(cost_usd), 4) as sum_cost,
6585
+ ROUND(AVG(duration_ms)) as avg_duration,
6586
+ MIN(recorded_at) as first_activity,
6587
+ MAX(recorded_at) as last_activity
6588
+ FROM agent_activity ${where}
6589
+ GROUP BY ${groupColumn}
6590
+ ORDER BY sum_cost DESC`
6591
+ ).all(...values);
6592
+ const aggregated = rows.map((r) => ({
6593
+ groupKey: r.group_key ?? "(none)",
6594
+ activityCount: r.activity_count,
6595
+ sumTokens: r.sum_tokens,
6596
+ avgTokens: r.avg_tokens,
6597
+ sumCost: r.sum_cost,
6598
+ avgDuration: r.avg_duration,
6599
+ firstActivity: r.first_activity,
6600
+ lastActivity: r.last_activity
6601
+ }));
6602
+ return { rows: aggregated, grouped: true, qualitySnapshot, totals, timeSpan };
6603
+ }
6604
+ function exportMetrics(db, projectRoot, format, params, maxRows = 1e5) {
6605
+ const result = queryMetrics(db, {
6606
+ ...params,
6607
+ groupBy: "none",
6608
+ limit: maxRows
6609
+ });
6610
+ const rows = result.rows;
6611
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
6612
+ const dir = join16(projectRoot, ".arcbridge", "metrics");
6613
+ mkdirSync7(dir, { recursive: true });
6614
+ let content;
6615
+ let filename;
6616
+ switch (format) {
6617
+ case "json": {
6618
+ filename = `activity-${timestamp}.json`;
6619
+ content = JSON.stringify(
6620
+ {
6621
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
6622
+ totals: result.totals,
6623
+ quality_snapshot: result.qualitySnapshot,
6624
+ activities: rows.map((r) => ({
6625
+ ...r,
6626
+ metadata: safeParseJson3(r.metadata)
6627
+ }))
6628
+ },
6629
+ null,
6630
+ 2
6631
+ );
6632
+ break;
6633
+ }
6634
+ case "csv": {
6635
+ filename = `activity-${timestamp}.csv`;
6636
+ const headers = [
6637
+ "id",
6638
+ "recorded_at",
6639
+ "tool_name",
6640
+ "action",
6641
+ "model",
6642
+ "agent_role",
6643
+ "task_id",
6644
+ "phase_id",
6645
+ "input_tokens",
6646
+ "output_tokens",
6647
+ "total_tokens",
6648
+ "cost_usd",
6649
+ "duration_ms",
6650
+ "drift_count",
6651
+ "drift_errors",
6652
+ "test_pass_count",
6653
+ "test_fail_count",
6654
+ "lint_clean",
6655
+ "typecheck_clean",
6656
+ "notes"
6657
+ ];
6658
+ const csvRows = rows.map(
6659
+ (r) => headers.map((h) => {
6660
+ const val = r[h];
6661
+ if (val == null) return "";
6662
+ const str = String(val);
6663
+ return str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r") ? `"${str.replace(/"/g, '""')}"` : str;
6664
+ }).join(",")
6665
+ );
6666
+ content = [headers.join(","), ...csvRows].join("\n");
6667
+ break;
6668
+ }
6669
+ case "markdown": {
6670
+ filename = `activity-${timestamp}.md`;
6671
+ const lines = [
6672
+ `# Agent Activity Report`,
6673
+ "",
6674
+ `**Exported:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
6675
+ `**Activities:** ${result.totals.activityCount}`,
6676
+ `**Total cost:** $${result.totals.totalCost.toFixed(4)}`,
6677
+ `**Total tokens:** ${result.totals.totalTokens.toLocaleString()}`,
6678
+ ""
6679
+ ];
6680
+ if (result.qualitySnapshot.capturedAt) {
6681
+ const q = result.qualitySnapshot;
6682
+ lines.push(
6683
+ "## Latest Quality Snapshot",
6684
+ "",
6685
+ `| Metric | Value |`,
6686
+ `|--------|-------|`
6687
+ );
6688
+ if (q.driftCount != null) lines.push(`| Drift issues | ${q.driftCount} (${q.driftErrors ?? 0} errors) |`);
6689
+ if (q.testPassCount != null) lines.push(`| Tests | ${q.testPassCount} pass / ${q.testFailCount ?? 0} fail |`);
6690
+ if (q.lintClean != null) lines.push(`| Lint | ${q.lintClean ? "clean" : "errors"} |`);
6691
+ if (q.typecheckClean != null) lines.push(`| Typecheck | ${q.typecheckClean ? "clean" : "errors"} |`);
6692
+ lines.push("");
6693
+ }
6694
+ lines.push(
6695
+ "## Activities",
6696
+ "",
6697
+ "| Time | Tool | Action | Model | Tokens | Cost | Duration |",
6698
+ "|------|------|--------|-------|--------|------|----------|"
6699
+ );
6700
+ for (const r of rows) {
6701
+ lines.push(
6702
+ `| ${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" : ""} |`
6703
+ );
6704
+ }
6705
+ content = lines.join("\n") + "\n";
6706
+ break;
6707
+ }
6708
+ }
6709
+ const filePath = join16(dir, filename);
6710
+ writeFileSync7(filePath, content, "utf-8");
6711
+ return filePath;
6712
+ }
6713
+ function getGroupColumn(groupBy) {
6714
+ switch (groupBy) {
6715
+ case "model":
6716
+ return "model";
6717
+ case "task":
6718
+ return "task_id";
6719
+ case "phase":
6720
+ return "phase_id";
6721
+ case "tool":
6722
+ return "tool_name";
6723
+ case "day":
6724
+ return "DATE(recorded_at)";
6725
+ default:
6726
+ return "model";
6727
+ }
6728
+ }
6729
+ function getLatestQualitySnapshot(db, where, values) {
6730
+ const row = db.prepare(
6731
+ `SELECT
6732
+ drift_count, drift_errors,
6733
+ test_pass_count, test_fail_count,
6734
+ lint_clean, typecheck_clean,
6735
+ recorded_at
6736
+ FROM agent_activity
6737
+ ${where ? where + " AND" : "WHERE"}
6738
+ (drift_count IS NOT NULL OR test_pass_count IS NOT NULL OR lint_clean IS NOT NULL OR typecheck_clean IS NOT NULL)
6739
+ ORDER BY recorded_at DESC
6740
+ LIMIT 1`
6741
+ ).get(...values);
6742
+ if (!row) {
6743
+ return {
6744
+ driftCount: null,
6745
+ driftErrors: null,
6746
+ testPassCount: null,
6747
+ testFailCount: null,
6748
+ lintClean: null,
6749
+ typecheckClean: null,
6750
+ capturedAt: null
6751
+ };
6752
+ }
6753
+ return {
6754
+ driftCount: row.drift_count,
6755
+ driftErrors: row.drift_errors,
6756
+ testPassCount: row.test_pass_count,
6757
+ testFailCount: row.test_fail_count,
6758
+ lintClean: row.lint_clean != null ? row.lint_clean === 1 : null,
6759
+ typecheckClean: row.typecheck_clean != null ? row.typecheck_clean === 1 : null,
6760
+ capturedAt: row.recorded_at
6761
+ };
6762
+ }
6763
+ function safeParseJson3(raw) {
6764
+ try {
6765
+ return JSON.parse(raw);
6766
+ } catch {
6767
+ return {};
6768
+ }
6769
+ }
6770
+ function esc(val) {
6771
+ if (val == null) return "";
6772
+ return val.replace(/\|/g, "\\|").replace(/\r?\n|\r/g, " ");
6773
+ }
6774
+
6354
6775
  // src/git/helpers.ts
6355
6776
  import { execFileSync as execFileSync3 } from "child_process";
6356
6777
  function resolveRef(projectRoot, since, db) {
@@ -6557,10 +6978,10 @@ ${output}`;
6557
6978
 
6558
6979
  // src/roles/loader.ts
6559
6980
  import { readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
6560
- import { join as join16 } from "path";
6981
+ import { join as join17 } from "path";
6561
6982
  import matter4 from "gray-matter";
6562
6983
  function loadRoles(projectRoot) {
6563
- const agentsDir = join16(projectRoot, ".arcbridge", "agents");
6984
+ const agentsDir = join17(projectRoot, ".arcbridge", "agents");
6564
6985
  const roles = [];
6565
6986
  const errors = [];
6566
6987
  let files;
@@ -6570,7 +6991,7 @@ function loadRoles(projectRoot) {
6570
6991
  return { roles: [], errors: [`Agent directory not found: ${agentsDir}`] };
6571
6992
  }
6572
6993
  for (const file of files) {
6573
- const filePath = join16(agentsDir, file);
6994
+ const filePath = join17(agentsDir, file);
6574
6995
  try {
6575
6996
  const raw = readFileSync9(filePath, "utf-8");
6576
6997
  const parsed = matter4(raw);
@@ -6597,7 +7018,7 @@ function loadRole(projectRoot, roleId) {
6597
7018
  if (!/^[a-z0-9-]+$/.test(roleId)) {
6598
7019
  return { role: null, error: `Invalid role ID: "${roleId}" (must be kebab-case)` };
6599
7020
  }
6600
- const filePath = join16(projectRoot, ".arcbridge", "agents", `${roleId}.md`);
7021
+ const filePath = join17(projectRoot, ".arcbridge", "agents", `${roleId}.md`);
6601
7022
  try {
6602
7023
  const raw = readFileSync9(filePath, "utf-8");
6603
7024
  const parsed = matter4(raw);
@@ -6639,6 +7060,7 @@ export {
6639
7060
  detectDrift,
6640
7061
  detectProjectLanguage,
6641
7062
  discoverDotnetServices,
7063
+ exportMetrics,
6642
7064
  generateAgentRoles,
6643
7065
  generateArc42,
6644
7066
  generateConfig,
@@ -6647,17 +7069,20 @@ export {
6647
7069
  generateSyncFiles,
6648
7070
  getChangedFiles,
6649
7071
  getHeadSha,
7072
+ getSessionTotals,
6650
7073
  getUncommittedChanges,
6651
7074
  indexPackageDependencies,
6652
7075
  indexProject,
6653
7076
  inferTaskStatuses,
6654
7077
  initializeSchema,
7078
+ insertActivity,
6655
7079
  loadConfig,
6656
7080
  loadRole,
6657
7081
  loadRoles,
6658
7082
  migrate,
6659
7083
  openDatabase,
6660
7084
  openMemoryDatabase,
7085
+ queryMetrics,
6661
7086
  refreshFromDocs,
6662
7087
  resolveRef,
6663
7088
  setSyncCommit,