@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.d.ts +111 -2
- package/dist/index.js +492 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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'
|
|
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 =
|
|
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
|
-
|
|
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
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4506
|
+
return execFileSync(
|
|
4399
4507
|
"dotnet",
|
|
4400
|
-
[
|
|
4401
|
-
|
|
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
|
-
|
|
4513
|
+
return execFileSync(
|
|
4422
4514
|
"dotnet",
|
|
4423
|
-
[
|
|
4424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6981
|
+
import { join as join17 } from "path";
|
|
6561
6982
|
import matter4 from "gray-matter";
|
|
6562
6983
|
function loadRoles(projectRoot) {
|
|
6563
|
-
const agentsDir =
|
|
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 =
|
|
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 =
|
|
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,
|