mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-11 03:43:30 +08:00
feat: add SQLite state store and ECC status CLI
This commit is contained in:
@@ -70,6 +70,8 @@
|
||||
"scripts/lib/",
|
||||
"scripts/claw.js",
|
||||
"scripts/doctor.js",
|
||||
"scripts/status.js",
|
||||
"scripts/sessions-cli.js",
|
||||
"scripts/install-apply.js",
|
||||
"scripts/install-plan.js",
|
||||
"scripts/list-installed.js",
|
||||
@@ -102,6 +104,9 @@
|
||||
"test": "node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-no-personal-paths.js && node tests/run-all.js",
|
||||
"coverage": "c8 --all --include=\"scripts/**/*.js\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^11.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"ajv": "^8.18.0",
|
||||
|
||||
316
schemas/state-store.schema.json
Normal file
316
schemas/state-store.schema.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "ecc.state-store.v1",
|
||||
"title": "ECC State Store Schema",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"sessions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/session"
|
||||
}
|
||||
},
|
||||
"skillRuns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/skillRun"
|
||||
}
|
||||
},
|
||||
"skillVersions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/skillVersion"
|
||||
}
|
||||
},
|
||||
"decisions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/decision"
|
||||
}
|
||||
},
|
||||
"installState": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/installState"
|
||||
}
|
||||
},
|
||||
"governanceEvents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/governanceEvent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"nonEmptyString": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"nullableString": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"nullableInteger": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"minimum": 0
|
||||
},
|
||||
"jsonValue": {
|
||||
"type": [
|
||||
"object",
|
||||
"array",
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"jsonArray": {
|
||||
"type": "array"
|
||||
},
|
||||
"session": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"adapterId",
|
||||
"harness",
|
||||
"state",
|
||||
"repoRoot",
|
||||
"startedAt",
|
||||
"endedAt",
|
||||
"snapshot"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"adapterId": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"harness": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"repoRoot": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"startedAt": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"endedAt": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"snapshot": {
|
||||
"type": [
|
||||
"object",
|
||||
"array"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"skillRun": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"skillId",
|
||||
"skillVersion",
|
||||
"sessionId",
|
||||
"taskDescription",
|
||||
"outcome",
|
||||
"failureReason",
|
||||
"tokensUsed",
|
||||
"durationMs",
|
||||
"userFeedback",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"skillId": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"skillVersion": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"sessionId": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"taskDescription": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"outcome": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"failureReason": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"tokensUsed": {
|
||||
"$ref": "#/$defs/nullableInteger"
|
||||
},
|
||||
"durationMs": {
|
||||
"$ref": "#/$defs/nullableInteger"
|
||||
},
|
||||
"userFeedback": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"createdAt": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"skillVersion": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"skillId",
|
||||
"version",
|
||||
"contentHash",
|
||||
"amendmentReason",
|
||||
"promotedAt",
|
||||
"rolledBackAt"
|
||||
],
|
||||
"properties": {
|
||||
"skillId": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"version": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"contentHash": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"amendmentReason": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"promotedAt": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"rolledBackAt": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"decision": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"sessionId",
|
||||
"title",
|
||||
"rationale",
|
||||
"alternatives",
|
||||
"supersedes",
|
||||
"status",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"sessionId": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"title": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"rationale": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"alternatives": {
|
||||
"$ref": "#/$defs/jsonArray"
|
||||
},
|
||||
"supersedes": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"createdAt": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"installState": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"targetId",
|
||||
"targetRoot",
|
||||
"profile",
|
||||
"modules",
|
||||
"operations",
|
||||
"installedAt",
|
||||
"sourceVersion"
|
||||
],
|
||||
"properties": {
|
||||
"targetId": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"targetRoot": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"profile": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"modules": {
|
||||
"$ref": "#/$defs/jsonArray"
|
||||
},
|
||||
"operations": {
|
||||
"$ref": "#/$defs/jsonArray"
|
||||
},
|
||||
"installedAt": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"sourceVersion": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"governanceEvent": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"sessionId",
|
||||
"eventType",
|
||||
"payload",
|
||||
"resolvedAt",
|
||||
"resolution",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"sessionId": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"eventType": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/$defs/jsonValue"
|
||||
},
|
||||
"resolvedAt": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"resolution": {
|
||||
"$ref": "#/$defs/nullableString"
|
||||
},
|
||||
"createdAt": {
|
||||
"$ref": "#/$defs/nonEmptyString"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,14 @@ const COMMANDS = {
|
||||
script: 'repair.js',
|
||||
description: 'Restore drifted or missing ECC-managed files',
|
||||
},
|
||||
status: {
|
||||
script: 'status.js',
|
||||
description: 'Query the ECC SQLite state store status summary',
|
||||
},
|
||||
sessions: {
|
||||
script: 'sessions-cli.js',
|
||||
description: 'List or inspect ECC sessions from the SQLite state store',
|
||||
},
|
||||
'session-inspect': {
|
||||
script: 'session-inspect.js',
|
||||
description: 'Emit canonical ECC session snapshots from dmux or Claude history targets',
|
||||
@@ -45,6 +53,8 @@ const PRIMARY_COMMANDS = [
|
||||
'list-installed',
|
||||
'doctor',
|
||||
'repair',
|
||||
'status',
|
||||
'sessions',
|
||||
'session-inspect',
|
||||
'uninstall',
|
||||
];
|
||||
@@ -72,6 +82,9 @@ Examples:
|
||||
ecc list-installed --json
|
||||
ecc doctor --target cursor
|
||||
ecc repair --dry-run
|
||||
ecc status --json
|
||||
ecc sessions
|
||||
ecc sessions session-active --json
|
||||
ecc session-inspect claude:latest
|
||||
ecc uninstall --target antigravity --dry-run
|
||||
`);
|
||||
|
||||
67
scripts/lib/state-store/index.js
Normal file
67
scripts/lib/state-store/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const { applyMigrations, getAppliedMigrations } = require('./migrations');
|
||||
const { createQueryApi } = require('./queries');
|
||||
const { assertValidEntity, validateEntity } = require('./schema');
|
||||
|
||||
const DEFAULT_STATE_STORE_RELATIVE_PATH = path.join('.claude', 'ecc', 'state.db');
|
||||
|
||||
function resolveStateStorePath(options = {}) {
|
||||
if (options.dbPath) {
|
||||
if (options.dbPath === ':memory:') {
|
||||
return options.dbPath;
|
||||
}
|
||||
return path.resolve(options.dbPath);
|
||||
}
|
||||
|
||||
const homeDir = options.homeDir || process.env.HOME || os.homedir();
|
||||
return path.join(homeDir, DEFAULT_STATE_STORE_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
function openDatabase(dbPath) {
|
||||
if (dbPath !== ':memory:') {
|
||||
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('foreign_keys = ON');
|
||||
try {
|
||||
db.pragma('journal_mode = WAL');
|
||||
} catch (_error) {
|
||||
// Some SQLite environments reject WAL for in-memory or readonly contexts.
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
function createStateStore(options = {}) {
|
||||
const dbPath = resolveStateStorePath(options);
|
||||
const db = openDatabase(dbPath);
|
||||
const appliedMigrations = applyMigrations(db);
|
||||
const queryApi = createQueryApi(db);
|
||||
|
||||
return {
|
||||
dbPath,
|
||||
close() {
|
||||
db.close();
|
||||
},
|
||||
getAppliedMigrations() {
|
||||
return getAppliedMigrations(db);
|
||||
},
|
||||
validateEntity,
|
||||
assertValidEntity,
|
||||
...queryApi,
|
||||
_database: db,
|
||||
_migrations: appliedMigrations,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_STATE_STORE_RELATIVE_PATH,
|
||||
createStateStore,
|
||||
resolveStateStorePath,
|
||||
};
|
||||
178
scripts/lib/state-store/migrations.js
Normal file
178
scripts/lib/state-store/migrations.js
Normal file
@@ -0,0 +1,178 @@
|
||||
'use strict';
|
||||
|
||||
const INITIAL_SCHEMA_SQL = `
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
adapter_id TEXT NOT NULL,
|
||||
harness TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
repo_root TEXT,
|
||||
started_at TEXT,
|
||||
ended_at TEXT,
|
||||
snapshot TEXT NOT NULL CHECK (json_valid(snapshot))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_state_started_at
|
||||
ON sessions (state, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_started_at
|
||||
ON sessions (started_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skill_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
skill_id TEXT NOT NULL,
|
||||
skill_version TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
task_description TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL,
|
||||
failure_reason TEXT,
|
||||
tokens_used INTEGER,
|
||||
duration_ms INTEGER,
|
||||
user_feedback TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_runs_session_id_created_at
|
||||
ON skill_runs (session_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_runs_created_at
|
||||
ON skill_runs (created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_runs_outcome_created_at
|
||||
ON skill_runs (outcome, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skill_versions (
|
||||
skill_id TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
content_hash TEXT NOT NULL,
|
||||
amendment_reason TEXT,
|
||||
promoted_at TEXT,
|
||||
rolled_back_at TEXT,
|
||||
PRIMARY KEY (skill_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_skill_versions_promoted_at
|
||||
ON skill_versions (promoted_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
rationale TEXT NOT NULL,
|
||||
alternatives TEXT NOT NULL CHECK (json_valid(alternatives)),
|
||||
supersedes TEXT,
|
||||
status TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (supersedes) REFERENCES decisions (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_session_id_created_at
|
||||
ON decisions (session_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_status_created_at
|
||||
ON decisions (status, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS install_state (
|
||||
target_id TEXT NOT NULL,
|
||||
target_root TEXT NOT NULL,
|
||||
profile TEXT,
|
||||
modules TEXT NOT NULL CHECK (json_valid(modules)),
|
||||
operations TEXT NOT NULL CHECK (json_valid(operations)),
|
||||
installed_at TEXT NOT NULL,
|
||||
source_version TEXT,
|
||||
PRIMARY KEY (target_id, target_root)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_install_state_installed_at
|
||||
ON install_state (installed_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS governance_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT,
|
||||
event_type TEXT NOT NULL,
|
||||
payload TEXT NOT NULL CHECK (json_valid(payload)),
|
||||
resolved_at TEXT,
|
||||
resolution TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_governance_events_resolved_at_created_at
|
||||
ON governance_events (resolved_at, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_governance_events_session_id_created_at
|
||||
ON governance_events (session_id, created_at DESC);
|
||||
`;
|
||||
|
||||
const MIGRATIONS = [
|
||||
{
|
||||
version: 1,
|
||||
name: '001_initial_state_store',
|
||||
sql: INITIAL_SCHEMA_SQL,
|
||||
},
|
||||
];
|
||||
|
||||
function ensureMigrationTable(db) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
applied_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
function getAppliedMigrations(db) {
|
||||
ensureMigrationTable(db);
|
||||
return db
|
||||
.prepare(`
|
||||
SELECT version, name, applied_at
|
||||
FROM schema_migrations
|
||||
ORDER BY version ASC
|
||||
`)
|
||||
.all()
|
||||
.map(row => ({
|
||||
version: row.version,
|
||||
name: row.name,
|
||||
appliedAt: row.applied_at,
|
||||
}));
|
||||
}
|
||||
|
||||
function applyMigrations(db) {
|
||||
ensureMigrationTable(db);
|
||||
|
||||
const appliedVersions = new Set(
|
||||
db.prepare('SELECT version FROM schema_migrations').all().map(row => row.version)
|
||||
);
|
||||
const insertMigration = db.prepare(`
|
||||
INSERT INTO schema_migrations (version, name, applied_at)
|
||||
VALUES (@version, @name, @applied_at)
|
||||
`);
|
||||
|
||||
const applyPending = db.transaction(() => {
|
||||
for (const migration of MIGRATIONS) {
|
||||
if (appliedVersions.has(migration.version)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
db.exec(migration.sql);
|
||||
insertMigration.run({
|
||||
version: migration.version,
|
||||
name: migration.name,
|
||||
applied_at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
applyPending();
|
||||
return getAppliedMigrations(db);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MIGRATIONS,
|
||||
applyMigrations,
|
||||
getAppliedMigrations,
|
||||
};
|
||||
697
scripts/lib/state-store/queries.js
Normal file
697
scripts/lib/state-store/queries.js
Normal file
@@ -0,0 +1,697 @@
|
||||
'use strict';
|
||||
|
||||
const { assertValidEntity } = require('./schema');
|
||||
|
||||
const ACTIVE_SESSION_STATES = ['active', 'running', 'idle'];
|
||||
const SUCCESS_OUTCOMES = new Set(['success', 'succeeded', 'passed']);
|
||||
const FAILURE_OUTCOMES = new Set(['failure', 'failed', 'error']);
|
||||
|
||||
function normalizeLimit(value, fallback) {
|
||||
if (value === undefined || value === null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
throw new Error(`Invalid limit: ${value}`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseJsonColumn(value, fallback) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
function stringifyJson(value, label) {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to serialize ${label}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function mapSessionRow(row) {
|
||||
const snapshot = parseJsonColumn(row.snapshot, {});
|
||||
return {
|
||||
id: row.id,
|
||||
adapterId: row.adapter_id,
|
||||
harness: row.harness,
|
||||
state: row.state,
|
||||
repoRoot: row.repo_root,
|
||||
startedAt: row.started_at,
|
||||
endedAt: row.ended_at,
|
||||
snapshot,
|
||||
workerCount: Array.isArray(snapshot && snapshot.workers) ? snapshot.workers.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function mapSkillRunRow(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
skillId: row.skill_id,
|
||||
skillVersion: row.skill_version,
|
||||
sessionId: row.session_id,
|
||||
taskDescription: row.task_description,
|
||||
outcome: row.outcome,
|
||||
failureReason: row.failure_reason,
|
||||
tokensUsed: row.tokens_used,
|
||||
durationMs: row.duration_ms,
|
||||
userFeedback: row.user_feedback,
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
function mapSkillVersionRow(row) {
|
||||
return {
|
||||
skillId: row.skill_id,
|
||||
version: row.version,
|
||||
contentHash: row.content_hash,
|
||||
amendmentReason: row.amendment_reason,
|
||||
promotedAt: row.promoted_at,
|
||||
rolledBackAt: row.rolled_back_at,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDecisionRow(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
sessionId: row.session_id,
|
||||
title: row.title,
|
||||
rationale: row.rationale,
|
||||
alternatives: parseJsonColumn(row.alternatives, []),
|
||||
supersedes: row.supersedes,
|
||||
status: row.status,
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
function mapInstallStateRow(row) {
|
||||
const modules = parseJsonColumn(row.modules, []);
|
||||
const operations = parseJsonColumn(row.operations, []);
|
||||
const status = row.source_version && row.installed_at ? 'healthy' : 'warning';
|
||||
|
||||
return {
|
||||
targetId: row.target_id,
|
||||
targetRoot: row.target_root,
|
||||
profile: row.profile,
|
||||
modules,
|
||||
operations,
|
||||
installedAt: row.installed_at,
|
||||
sourceVersion: row.source_version,
|
||||
moduleCount: Array.isArray(modules) ? modules.length : 0,
|
||||
operationCount: Array.isArray(operations) ? operations.length : 0,
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
function mapGovernanceEventRow(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
sessionId: row.session_id,
|
||||
eventType: row.event_type,
|
||||
payload: parseJsonColumn(row.payload, null),
|
||||
resolvedAt: row.resolved_at,
|
||||
resolution: row.resolution,
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
function classifyOutcome(outcome) {
|
||||
const normalized = String(outcome || '').toLowerCase();
|
||||
if (SUCCESS_OUTCOMES.has(normalized)) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (FAILURE_OUTCOMES.has(normalized)) {
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function toPercent(numerator, denominator) {
|
||||
if (denominator === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Number(((numerator / denominator) * 100).toFixed(1));
|
||||
}
|
||||
|
||||
function summarizeSkillRuns(skillRuns) {
|
||||
const summary = {
|
||||
totalCount: skillRuns.length,
|
||||
knownCount: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
unknownCount: 0,
|
||||
successRate: null,
|
||||
failureRate: null,
|
||||
};
|
||||
|
||||
for (const skillRun of skillRuns) {
|
||||
const classification = classifyOutcome(skillRun.outcome);
|
||||
if (classification === 'success') {
|
||||
summary.successCount += 1;
|
||||
summary.knownCount += 1;
|
||||
} else if (classification === 'failure') {
|
||||
summary.failureCount += 1;
|
||||
summary.knownCount += 1;
|
||||
} else {
|
||||
summary.unknownCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
summary.successRate = toPercent(summary.successCount, summary.knownCount);
|
||||
summary.failureRate = toPercent(summary.failureCount, summary.knownCount);
|
||||
return summary;
|
||||
}
|
||||
|
||||
function summarizeInstallHealth(installations) {
|
||||
if (installations.length === 0) {
|
||||
return {
|
||||
status: 'missing',
|
||||
totalCount: 0,
|
||||
healthyCount: 0,
|
||||
warningCount: 0,
|
||||
installations: [],
|
||||
};
|
||||
}
|
||||
|
||||
const summary = installations.reduce((result, installation) => {
|
||||
if (installation.status === 'healthy') {
|
||||
result.healthyCount += 1;
|
||||
} else {
|
||||
result.warningCount += 1;
|
||||
}
|
||||
return result;
|
||||
}, {
|
||||
totalCount: installations.length,
|
||||
healthyCount: 0,
|
||||
warningCount: 0,
|
||||
});
|
||||
|
||||
return {
|
||||
status: summary.warningCount > 0 ? 'warning' : 'healthy',
|
||||
...summary,
|
||||
installations,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSessionInput(session) {
|
||||
return {
|
||||
id: session.id,
|
||||
adapterId: session.adapterId,
|
||||
harness: session.harness,
|
||||
state: session.state,
|
||||
repoRoot: session.repoRoot ?? null,
|
||||
startedAt: session.startedAt ?? null,
|
||||
endedAt: session.endedAt ?? null,
|
||||
snapshot: session.snapshot ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSkillRunInput(skillRun) {
|
||||
return {
|
||||
id: skillRun.id,
|
||||
skillId: skillRun.skillId,
|
||||
skillVersion: skillRun.skillVersion,
|
||||
sessionId: skillRun.sessionId,
|
||||
taskDescription: skillRun.taskDescription,
|
||||
outcome: skillRun.outcome,
|
||||
failureReason: skillRun.failureReason ?? null,
|
||||
tokensUsed: skillRun.tokensUsed ?? null,
|
||||
durationMs: skillRun.durationMs ?? null,
|
||||
userFeedback: skillRun.userFeedback ?? null,
|
||||
createdAt: skillRun.createdAt || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSkillVersionInput(skillVersion) {
|
||||
return {
|
||||
skillId: skillVersion.skillId,
|
||||
version: skillVersion.version,
|
||||
contentHash: skillVersion.contentHash,
|
||||
amendmentReason: skillVersion.amendmentReason ?? null,
|
||||
promotedAt: skillVersion.promotedAt ?? null,
|
||||
rolledBackAt: skillVersion.rolledBackAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDecisionInput(decision) {
|
||||
return {
|
||||
id: decision.id,
|
||||
sessionId: decision.sessionId,
|
||||
title: decision.title,
|
||||
rationale: decision.rationale,
|
||||
alternatives: decision.alternatives === undefined || decision.alternatives === null
|
||||
? []
|
||||
: decision.alternatives,
|
||||
supersedes: decision.supersedes ?? null,
|
||||
status: decision.status,
|
||||
createdAt: decision.createdAt || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeInstallStateInput(installState) {
|
||||
return {
|
||||
targetId: installState.targetId,
|
||||
targetRoot: installState.targetRoot,
|
||||
profile: installState.profile ?? null,
|
||||
modules: installState.modules === undefined || installState.modules === null
|
||||
? []
|
||||
: installState.modules,
|
||||
operations: installState.operations === undefined || installState.operations === null
|
||||
? []
|
||||
: installState.operations,
|
||||
installedAt: installState.installedAt || new Date().toISOString(),
|
||||
sourceVersion: installState.sourceVersion ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGovernanceEventInput(governanceEvent) {
|
||||
return {
|
||||
id: governanceEvent.id,
|
||||
sessionId: governanceEvent.sessionId ?? null,
|
||||
eventType: governanceEvent.eventType,
|
||||
payload: governanceEvent.payload ?? null,
|
||||
resolvedAt: governanceEvent.resolvedAt ?? null,
|
||||
resolution: governanceEvent.resolution ?? null,
|
||||
createdAt: governanceEvent.createdAt || new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function createQueryApi(db) {
|
||||
const listRecentSessionsStatement = db.prepare(`
|
||||
SELECT *
|
||||
FROM sessions
|
||||
ORDER BY COALESCE(started_at, ended_at, '') DESC, id DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
const countSessionsStatement = db.prepare(`
|
||||
SELECT COUNT(*) AS total_count
|
||||
FROM sessions
|
||||
`);
|
||||
const getSessionStatement = db.prepare(`
|
||||
SELECT *
|
||||
FROM sessions
|
||||
WHERE id = ?
|
||||
`);
|
||||
const getSessionSkillRunsStatement = db.prepare(`
|
||||
SELECT *
|
||||
FROM skill_runs
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at DESC, id DESC
|
||||
`);
|
||||
const getSessionDecisionsStatement = db.prepare(`
|
||||
SELECT *
|
||||
FROM decisions
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at DESC, id DESC
|
||||
`);
|
||||
const listActiveSessionsStatement = db.prepare(`
|
||||
SELECT *
|
||||
FROM sessions
|
||||
WHERE ended_at IS NULL
|
||||
AND state IN ('active', 'running', 'idle')
|
||||
ORDER BY COALESCE(started_at, ended_at, '') DESC, id DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
const countActiveSessionsStatement = db.prepare(`
|
||||
SELECT COUNT(*) AS total_count
|
||||
FROM sessions
|
||||
WHERE ended_at IS NULL
|
||||
AND state IN ('active', 'running', 'idle')
|
||||
`);
|
||||
const listRecentSkillRunsStatement = db.prepare(`
|
||||
SELECT *
|
||||
FROM skill_runs
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
const listInstallStateStatement = db.prepare(`
|
||||
SELECT *
|
||||
FROM install_state
|
||||
ORDER BY installed_at DESC, target_id ASC
|
||||
`);
|
||||
const countPendingGovernanceStatement = db.prepare(`
|
||||
SELECT COUNT(*) AS total_count
|
||||
FROM governance_events
|
||||
WHERE resolved_at IS NULL
|
||||
`);
|
||||
const listPendingGovernanceStatement = db.prepare(`
|
||||
SELECT *
|
||||
FROM governance_events
|
||||
WHERE resolved_at IS NULL
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
const getSkillVersionStatement = db.prepare(`
|
||||
SELECT *
|
||||
FROM skill_versions
|
||||
WHERE skill_id = ? AND version = ?
|
||||
`);
|
||||
|
||||
const upsertSessionStatement = db.prepare(`
|
||||
INSERT INTO sessions (
|
||||
id,
|
||||
adapter_id,
|
||||
harness,
|
||||
state,
|
||||
repo_root,
|
||||
started_at,
|
||||
ended_at,
|
||||
snapshot
|
||||
) VALUES (
|
||||
@id,
|
||||
@adapter_id,
|
||||
@harness,
|
||||
@state,
|
||||
@repo_root,
|
||||
@started_at,
|
||||
@ended_at,
|
||||
@snapshot
|
||||
)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
adapter_id = excluded.adapter_id,
|
||||
harness = excluded.harness,
|
||||
state = excluded.state,
|
||||
repo_root = excluded.repo_root,
|
||||
started_at = excluded.started_at,
|
||||
ended_at = excluded.ended_at,
|
||||
snapshot = excluded.snapshot
|
||||
`);
|
||||
|
||||
const insertSkillRunStatement = db.prepare(`
|
||||
INSERT INTO skill_runs (
|
||||
id,
|
||||
skill_id,
|
||||
skill_version,
|
||||
session_id,
|
||||
task_description,
|
||||
outcome,
|
||||
failure_reason,
|
||||
tokens_used,
|
||||
duration_ms,
|
||||
user_feedback,
|
||||
created_at
|
||||
) VALUES (
|
||||
@id,
|
||||
@skill_id,
|
||||
@skill_version,
|
||||
@session_id,
|
||||
@task_description,
|
||||
@outcome,
|
||||
@failure_reason,
|
||||
@tokens_used,
|
||||
@duration_ms,
|
||||
@user_feedback,
|
||||
@created_at
|
||||
)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
skill_id = excluded.skill_id,
|
||||
skill_version = excluded.skill_version,
|
||||
session_id = excluded.session_id,
|
||||
task_description = excluded.task_description,
|
||||
outcome = excluded.outcome,
|
||||
failure_reason = excluded.failure_reason,
|
||||
tokens_used = excluded.tokens_used,
|
||||
duration_ms = excluded.duration_ms,
|
||||
user_feedback = excluded.user_feedback,
|
||||
created_at = excluded.created_at
|
||||
`);
|
||||
|
||||
const upsertSkillVersionStatement = db.prepare(`
|
||||
INSERT INTO skill_versions (
|
||||
skill_id,
|
||||
version,
|
||||
content_hash,
|
||||
amendment_reason,
|
||||
promoted_at,
|
||||
rolled_back_at
|
||||
) VALUES (
|
||||
@skill_id,
|
||||
@version,
|
||||
@content_hash,
|
||||
@amendment_reason,
|
||||
@promoted_at,
|
||||
@rolled_back_at
|
||||
)
|
||||
ON CONFLICT(skill_id, version) DO UPDATE SET
|
||||
content_hash = excluded.content_hash,
|
||||
amendment_reason = excluded.amendment_reason,
|
||||
promoted_at = excluded.promoted_at,
|
||||
rolled_back_at = excluded.rolled_back_at
|
||||
`);
|
||||
|
||||
const insertDecisionStatement = db.prepare(`
|
||||
INSERT INTO decisions (
|
||||
id,
|
||||
session_id,
|
||||
title,
|
||||
rationale,
|
||||
alternatives,
|
||||
supersedes,
|
||||
status,
|
||||
created_at
|
||||
) VALUES (
|
||||
@id,
|
||||
@session_id,
|
||||
@title,
|
||||
@rationale,
|
||||
@alternatives,
|
||||
@supersedes,
|
||||
@status,
|
||||
@created_at
|
||||
)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
session_id = excluded.session_id,
|
||||
title = excluded.title,
|
||||
rationale = excluded.rationale,
|
||||
alternatives = excluded.alternatives,
|
||||
supersedes = excluded.supersedes,
|
||||
status = excluded.status,
|
||||
created_at = excluded.created_at
|
||||
`);
|
||||
|
||||
const upsertInstallStateStatement = db.prepare(`
|
||||
INSERT INTO install_state (
|
||||
target_id,
|
||||
target_root,
|
||||
profile,
|
||||
modules,
|
||||
operations,
|
||||
installed_at,
|
||||
source_version
|
||||
) VALUES (
|
||||
@target_id,
|
||||
@target_root,
|
||||
@profile,
|
||||
@modules,
|
||||
@operations,
|
||||
@installed_at,
|
||||
@source_version
|
||||
)
|
||||
ON CONFLICT(target_id, target_root) DO UPDATE SET
|
||||
profile = excluded.profile,
|
||||
modules = excluded.modules,
|
||||
operations = excluded.operations,
|
||||
installed_at = excluded.installed_at,
|
||||
source_version = excluded.source_version
|
||||
`);
|
||||
|
||||
const insertGovernanceEventStatement = db.prepare(`
|
||||
INSERT INTO governance_events (
|
||||
id,
|
||||
session_id,
|
||||
event_type,
|
||||
payload,
|
||||
resolved_at,
|
||||
resolution,
|
||||
created_at
|
||||
) VALUES (
|
||||
@id,
|
||||
@session_id,
|
||||
@event_type,
|
||||
@payload,
|
||||
@resolved_at,
|
||||
@resolution,
|
||||
@created_at
|
||||
)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
session_id = excluded.session_id,
|
||||
event_type = excluded.event_type,
|
||||
payload = excluded.payload,
|
||||
resolved_at = excluded.resolved_at,
|
||||
resolution = excluded.resolution,
|
||||
created_at = excluded.created_at
|
||||
`);
|
||||
|
||||
function getSessionById(id) {
|
||||
const row = getSessionStatement.get(id);
|
||||
return row ? mapSessionRow(row) : null;
|
||||
}
|
||||
|
||||
function listRecentSessions(options = {}) {
|
||||
const limit = normalizeLimit(options.limit, 10);
|
||||
return {
|
||||
totalCount: countSessionsStatement.get().total_count,
|
||||
sessions: listRecentSessionsStatement.all(limit).map(mapSessionRow),
|
||||
};
|
||||
}
|
||||
|
||||
function getSessionDetail(id) {
|
||||
const session = getSessionById(id);
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workers = Array.isArray(session.snapshot && session.snapshot.workers)
|
||||
? session.snapshot.workers.map(worker => ({ ...worker }))
|
||||
: [];
|
||||
|
||||
return {
|
||||
session,
|
||||
workers,
|
||||
skillRuns: getSessionSkillRunsStatement.all(id).map(mapSkillRunRow),
|
||||
decisions: getSessionDecisionsStatement.all(id).map(mapDecisionRow),
|
||||
};
|
||||
}
|
||||
|
||||
function getStatus(options = {}) {
|
||||
const activeLimit = normalizeLimit(options.activeLimit, 5);
|
||||
const recentSkillRunLimit = normalizeLimit(options.recentSkillRunLimit, 20);
|
||||
const pendingLimit = normalizeLimit(options.pendingLimit, 5);
|
||||
|
||||
const activeSessions = listActiveSessionsStatement.all(activeLimit).map(mapSessionRow);
|
||||
const recentSkillRuns = listRecentSkillRunsStatement.all(recentSkillRunLimit).map(mapSkillRunRow);
|
||||
const installations = listInstallStateStatement.all().map(mapInstallStateRow);
|
||||
const pendingGovernanceEvents = listPendingGovernanceStatement.all(pendingLimit).map(mapGovernanceEventRow);
|
||||
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
activeSessions: {
|
||||
activeCount: countActiveSessionsStatement.get().total_count,
|
||||
sessions: activeSessions,
|
||||
},
|
||||
skillRuns: {
|
||||
windowSize: recentSkillRunLimit,
|
||||
summary: summarizeSkillRuns(recentSkillRuns),
|
||||
recent: recentSkillRuns,
|
||||
},
|
||||
installHealth: summarizeInstallHealth(installations),
|
||||
governance: {
|
||||
pendingCount: countPendingGovernanceStatement.get().total_count,
|
||||
events: pendingGovernanceEvents,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
getSessionById,
|
||||
getSessionDetail,
|
||||
getStatus,
|
||||
insertDecision(decision) {
|
||||
const normalized = normalizeDecisionInput(decision);
|
||||
assertValidEntity('decision', normalized);
|
||||
insertDecisionStatement.run({
|
||||
id: normalized.id,
|
||||
session_id: normalized.sessionId,
|
||||
title: normalized.title,
|
||||
rationale: normalized.rationale,
|
||||
alternatives: stringifyJson(normalized.alternatives, 'decision.alternatives'),
|
||||
supersedes: normalized.supersedes,
|
||||
status: normalized.status,
|
||||
created_at: normalized.createdAt,
|
||||
});
|
||||
return normalized;
|
||||
},
|
||||
insertGovernanceEvent(governanceEvent) {
|
||||
const normalized = normalizeGovernanceEventInput(governanceEvent);
|
||||
assertValidEntity('governanceEvent', normalized);
|
||||
insertGovernanceEventStatement.run({
|
||||
id: normalized.id,
|
||||
session_id: normalized.sessionId,
|
||||
event_type: normalized.eventType,
|
||||
payload: stringifyJson(normalized.payload, 'governanceEvent.payload'),
|
||||
resolved_at: normalized.resolvedAt,
|
||||
resolution: normalized.resolution,
|
||||
created_at: normalized.createdAt,
|
||||
});
|
||||
return normalized;
|
||||
},
|
||||
insertSkillRun(skillRun) {
|
||||
const normalized = normalizeSkillRunInput(skillRun);
|
||||
assertValidEntity('skillRun', normalized);
|
||||
insertSkillRunStatement.run({
|
||||
id: normalized.id,
|
||||
skill_id: normalized.skillId,
|
||||
skill_version: normalized.skillVersion,
|
||||
session_id: normalized.sessionId,
|
||||
task_description: normalized.taskDescription,
|
||||
outcome: normalized.outcome,
|
||||
failure_reason: normalized.failureReason,
|
||||
tokens_used: normalized.tokensUsed,
|
||||
duration_ms: normalized.durationMs,
|
||||
user_feedback: normalized.userFeedback,
|
||||
created_at: normalized.createdAt,
|
||||
});
|
||||
return normalized;
|
||||
},
|
||||
listRecentSessions,
|
||||
upsertInstallState(installState) {
|
||||
const normalized = normalizeInstallStateInput(installState);
|
||||
assertValidEntity('installState', normalized);
|
||||
upsertInstallStateStatement.run({
|
||||
target_id: normalized.targetId,
|
||||
target_root: normalized.targetRoot,
|
||||
profile: normalized.profile,
|
||||
modules: stringifyJson(normalized.modules, 'installState.modules'),
|
||||
operations: stringifyJson(normalized.operations, 'installState.operations'),
|
||||
installed_at: normalized.installedAt,
|
||||
source_version: normalized.sourceVersion,
|
||||
});
|
||||
return normalized;
|
||||
},
|
||||
upsertSession(session) {
|
||||
const normalized = normalizeSessionInput(session);
|
||||
assertValidEntity('session', normalized);
|
||||
upsertSessionStatement.run({
|
||||
id: normalized.id,
|
||||
adapter_id: normalized.adapterId,
|
||||
harness: normalized.harness,
|
||||
state: normalized.state,
|
||||
repo_root: normalized.repoRoot,
|
||||
started_at: normalized.startedAt,
|
||||
ended_at: normalized.endedAt,
|
||||
snapshot: stringifyJson(normalized.snapshot, 'session.snapshot'),
|
||||
});
|
||||
return getSessionById(normalized.id);
|
||||
},
|
||||
upsertSkillVersion(skillVersion) {
|
||||
const normalized = normalizeSkillVersionInput(skillVersion);
|
||||
assertValidEntity('skillVersion', normalized);
|
||||
upsertSkillVersionStatement.run({
|
||||
skill_id: normalized.skillId,
|
||||
version: normalized.version,
|
||||
content_hash: normalized.contentHash,
|
||||
amendment_reason: normalized.amendmentReason,
|
||||
promoted_at: normalized.promotedAt,
|
||||
rolled_back_at: normalized.rolledBackAt,
|
||||
});
|
||||
const row = getSkillVersionStatement.get(normalized.skillId, normalized.version);
|
||||
return row ? mapSkillVersionRow(row) : null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ACTIVE_SESSION_STATES,
|
||||
FAILURE_OUTCOMES,
|
||||
SUCCESS_OUTCOMES,
|
||||
createQueryApi,
|
||||
};
|
||||
92
scripts/lib/state-store/schema.js
Normal file
92
scripts/lib/state-store/schema.js
Normal file
@@ -0,0 +1,92 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Ajv = require('ajv');
|
||||
|
||||
const SCHEMA_PATH = path.join(__dirname, '..', '..', '..', 'schemas', 'state-store.schema.json');
|
||||
|
||||
const ENTITY_DEFINITIONS = {
|
||||
session: 'session',
|
||||
skillRun: 'skillRun',
|
||||
skillVersion: 'skillVersion',
|
||||
decision: 'decision',
|
||||
installState: 'installState',
|
||||
governanceEvent: 'governanceEvent',
|
||||
};
|
||||
|
||||
let cachedSchema = null;
|
||||
let cachedAjv = null;
|
||||
const cachedValidators = new Map();
|
||||
|
||||
function readSchema() {
|
||||
if (cachedSchema) {
|
||||
return cachedSchema;
|
||||
}
|
||||
|
||||
cachedSchema = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
||||
return cachedSchema;
|
||||
}
|
||||
|
||||
function getAjv() {
|
||||
if (cachedAjv) {
|
||||
return cachedAjv;
|
||||
}
|
||||
|
||||
cachedAjv = new Ajv({
|
||||
allErrors: true,
|
||||
strict: false,
|
||||
});
|
||||
return cachedAjv;
|
||||
}
|
||||
|
||||
function getEntityValidator(entityName) {
|
||||
if (cachedValidators.has(entityName)) {
|
||||
return cachedValidators.get(entityName);
|
||||
}
|
||||
|
||||
const schema = readSchema();
|
||||
const definitionName = ENTITY_DEFINITIONS[entityName];
|
||||
|
||||
if (!definitionName || !schema.$defs || !schema.$defs[definitionName]) {
|
||||
throw new Error(`Unknown state-store schema entity: ${entityName}`);
|
||||
}
|
||||
|
||||
const validatorSchema = {
|
||||
$schema: schema.$schema,
|
||||
...schema.$defs[definitionName],
|
||||
$defs: schema.$defs,
|
||||
};
|
||||
const validator = getAjv().compile(validatorSchema);
|
||||
cachedValidators.set(entityName, validator);
|
||||
return validator;
|
||||
}
|
||||
|
||||
function formatValidationErrors(errors = []) {
|
||||
return errors
|
||||
.map(error => `${error.instancePath || '/'} ${error.message}`)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
function validateEntity(entityName, payload) {
|
||||
const validator = getEntityValidator(entityName);
|
||||
const valid = validator(payload);
|
||||
return {
|
||||
valid,
|
||||
errors: validator.errors || [],
|
||||
};
|
||||
}
|
||||
|
||||
function assertValidEntity(entityName, payload, label) {
|
||||
const result = validateEntity(entityName, payload);
|
||||
if (!result.valid) {
|
||||
throw new Error(`Invalid ${entityName}${label ? ` (${label})` : ''}: ${formatValidationErrors(result.errors)}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
assertValidEntity,
|
||||
formatValidationErrors,
|
||||
readSchema,
|
||||
validateEntity,
|
||||
};
|
||||
177
scripts/sessions-cli.js
Normal file
177
scripts/sessions-cli.js
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { createStateStore } = require('./lib/state-store');
|
||||
|
||||
function showHelp(exitCode = 0) {
|
||||
console.log(`
|
||||
Usage: node scripts/sessions-cli.js [<session-id>] [--db <path>] [--json] [--limit <n>]
|
||||
|
||||
List recent ECC sessions from the SQLite state store or inspect a single session
|
||||
with worker, skill-run, and decision detail.
|
||||
`);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = argv.slice(2);
|
||||
const parsed = {
|
||||
dbPath: null,
|
||||
help: false,
|
||||
json: false,
|
||||
limit: 10,
|
||||
sessionId: null,
|
||||
};
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === '--db') {
|
||||
parsed.dbPath = args[index + 1] || null;
|
||||
index += 1;
|
||||
} else if (arg === '--json') {
|
||||
parsed.json = true;
|
||||
} else if (arg === '--limit') {
|
||||
parsed.limit = args[index + 1] || null;
|
||||
index += 1;
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
parsed.help = true;
|
||||
} else if (!arg.startsWith('--') && !parsed.sessionId) {
|
||||
parsed.sessionId = arg;
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printSessionList(payload) {
|
||||
console.log('Recent sessions:\n');
|
||||
|
||||
if (payload.sessions.length === 0) {
|
||||
console.log('No sessions found.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const session of payload.sessions) {
|
||||
console.log(`- ${session.id} [${session.harness}/${session.adapterId}] ${session.state}`);
|
||||
console.log(` Repo: ${session.repoRoot || '(unknown)'}`);
|
||||
console.log(` Started: ${session.startedAt || '(unknown)'}`);
|
||||
console.log(` Ended: ${session.endedAt || '(active)'}`);
|
||||
console.log(` Workers: ${session.workerCount}`);
|
||||
}
|
||||
|
||||
console.log(`\nTotal sessions: ${payload.totalCount}`);
|
||||
}
|
||||
|
||||
function printWorkers(workers) {
|
||||
console.log(`Workers: ${workers.length}`);
|
||||
if (workers.length === 0) {
|
||||
console.log(' - none');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const worker of workers) {
|
||||
console.log(` - ${worker.id || worker.label || '(unknown)'} ${worker.state || 'unknown'}`);
|
||||
console.log(` Branch: ${worker.branch || '(unknown)'}`);
|
||||
console.log(` Worktree: ${worker.worktree || '(unknown)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printSkillRuns(skillRuns) {
|
||||
console.log(`Skill runs: ${skillRuns.length}`);
|
||||
if (skillRuns.length === 0) {
|
||||
console.log(' - none');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const skillRun of skillRuns) {
|
||||
console.log(` - ${skillRun.id} ${skillRun.outcome} ${skillRun.skillId}@${skillRun.skillVersion}`);
|
||||
console.log(` Task: ${skillRun.taskDescription}`);
|
||||
console.log(` Duration: ${skillRun.durationMs ?? '(unknown)'} ms`);
|
||||
}
|
||||
}
|
||||
|
||||
function printDecisions(decisions) {
|
||||
console.log(`Decisions: ${decisions.length}`);
|
||||
if (decisions.length === 0) {
|
||||
console.log(' - none');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const decision of decisions) {
|
||||
console.log(` - ${decision.id} ${decision.status}`);
|
||||
console.log(` Title: ${decision.title}`);
|
||||
console.log(` Alternatives: ${decision.alternatives.join(', ') || '(none)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printSessionDetail(payload) {
|
||||
console.log(`Session: ${payload.session.id}`);
|
||||
console.log(`Harness: ${payload.session.harness}`);
|
||||
console.log(`Adapter: ${payload.session.adapterId}`);
|
||||
console.log(`State: ${payload.session.state}`);
|
||||
console.log(`Repo: ${payload.session.repoRoot || '(unknown)'}`);
|
||||
console.log(`Started: ${payload.session.startedAt || '(unknown)'}`);
|
||||
console.log(`Ended: ${payload.session.endedAt || '(active)'}`);
|
||||
console.log();
|
||||
printWorkers(payload.workers);
|
||||
console.log();
|
||||
printSkillRuns(payload.skillRuns);
|
||||
console.log();
|
||||
printDecisions(payload.decisions);
|
||||
}
|
||||
|
||||
function main() {
|
||||
let store = null;
|
||||
|
||||
try {
|
||||
const options = parseArgs(process.argv);
|
||||
if (options.help) {
|
||||
showHelp(0);
|
||||
}
|
||||
|
||||
store = createStateStore({
|
||||
dbPath: options.dbPath,
|
||||
homeDir: process.env.HOME,
|
||||
});
|
||||
|
||||
if (!options.sessionId) {
|
||||
const payload = store.listRecentSessions({ limit: options.limit });
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
} else {
|
||||
printSessionList(payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = store.getSessionDetail(options.sessionId);
|
||||
if (!payload) {
|
||||
throw new Error(`Session not found: ${options.sessionId}`);
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
} else {
|
||||
printSessionDetail(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (store) {
|
||||
store.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
main,
|
||||
parseArgs,
|
||||
};
|
||||
176
scripts/status.js
Normal file
176
scripts/status.js
Normal file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { createStateStore } = require('./lib/state-store');
|
||||
|
||||
function showHelp(exitCode = 0) {
|
||||
console.log(`
|
||||
Usage: node scripts/status.js [--db <path>] [--json] [--limit <n>]
|
||||
|
||||
Query the ECC SQLite state store for active sessions, recent skill runs,
|
||||
install health, and pending governance events.
|
||||
`);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = argv.slice(2);
|
||||
const parsed = {
|
||||
dbPath: null,
|
||||
json: false,
|
||||
help: false,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === '--db') {
|
||||
parsed.dbPath = args[index + 1] || null;
|
||||
index += 1;
|
||||
} else if (arg === '--json') {
|
||||
parsed.json = true;
|
||||
} else if (arg === '--limit') {
|
||||
parsed.limit = args[index + 1] || null;
|
||||
index += 1;
|
||||
} else if (arg === '--help' || arg === '-h') {
|
||||
parsed.help = true;
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printActiveSessions(section) {
|
||||
console.log(`Active sessions: ${section.activeCount}`);
|
||||
if (section.sessions.length === 0) {
|
||||
console.log(' - none');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const session of section.sessions) {
|
||||
console.log(` - ${session.id} [${session.harness}/${session.adapterId}] ${session.state}`);
|
||||
console.log(` Repo: ${session.repoRoot || '(unknown)'}`);
|
||||
console.log(` Started: ${session.startedAt || '(unknown)'}`);
|
||||
console.log(` Workers: ${session.workerCount}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printSkillRuns(section) {
|
||||
const summary = section.summary;
|
||||
const successRate = summary.successRate === null ? 'n/a' : `${summary.successRate}%`;
|
||||
const failureRate = summary.failureRate === null ? 'n/a' : `${summary.failureRate}%`;
|
||||
|
||||
console.log(`Skill runs (last ${section.windowSize}):`);
|
||||
console.log(` Success: ${summary.successCount}`);
|
||||
console.log(` Failure: ${summary.failureCount}`);
|
||||
console.log(` Unknown: ${summary.unknownCount}`);
|
||||
console.log(` Success rate: ${successRate}`);
|
||||
console.log(` Failure rate: ${failureRate}`);
|
||||
|
||||
if (section.recent.length === 0) {
|
||||
console.log(' Recent runs: none');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(' Recent runs:');
|
||||
for (const skillRun of section.recent.slice(0, 5)) {
|
||||
console.log(` - ${skillRun.id} ${skillRun.outcome} ${skillRun.skillId}@${skillRun.skillVersion}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printInstallHealth(section) {
|
||||
console.log(`Install health: ${section.status}`);
|
||||
console.log(` Targets recorded: ${section.totalCount}`);
|
||||
console.log(` Healthy: ${section.healthyCount}`);
|
||||
console.log(` Warning: ${section.warningCount}`);
|
||||
|
||||
if (section.installations.length === 0) {
|
||||
console.log(' Installations: none');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(' Installations:');
|
||||
for (const installation of section.installations.slice(0, 5)) {
|
||||
console.log(` - ${installation.targetId} ${installation.status}`);
|
||||
console.log(` Root: ${installation.targetRoot}`);
|
||||
console.log(` Profile: ${installation.profile || '(custom)'}`);
|
||||
console.log(` Modules: ${installation.moduleCount}`);
|
||||
console.log(` Source version: ${installation.sourceVersion || '(unknown)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printGovernance(section) {
|
||||
console.log(`Pending governance events: ${section.pendingCount}`);
|
||||
if (section.events.length === 0) {
|
||||
console.log(' - none');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of section.events) {
|
||||
console.log(` - ${event.id} ${event.eventType}`);
|
||||
console.log(` Session: ${event.sessionId || '(none)'}`);
|
||||
console.log(` Created: ${event.createdAt}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printHuman(payload) {
|
||||
console.log('ECC status\n');
|
||||
console.log(`Database: ${payload.dbPath}\n`);
|
||||
printActiveSessions(payload.activeSessions);
|
||||
console.log();
|
||||
printSkillRuns(payload.skillRuns);
|
||||
console.log();
|
||||
printInstallHealth(payload.installHealth);
|
||||
console.log();
|
||||
printGovernance(payload.governance);
|
||||
}
|
||||
|
||||
function main() {
|
||||
let store = null;
|
||||
|
||||
try {
|
||||
const options = parseArgs(process.argv);
|
||||
if (options.help) {
|
||||
showHelp(0);
|
||||
}
|
||||
|
||||
store = createStateStore({
|
||||
dbPath: options.dbPath,
|
||||
homeDir: process.env.HOME,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
dbPath: store.dbPath,
|
||||
...store.getStatus({
|
||||
activeLimit: options.limit,
|
||||
recentSkillRunLimit: 20,
|
||||
pendingLimit: options.limit,
|
||||
}),
|
||||
};
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
} else {
|
||||
printHuman(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (store) {
|
||||
store.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
main,
|
||||
parseArgs,
|
||||
};
|
||||
489
tests/lib/state-store.test.js
Normal file
489
tests/lib/state-store.test.js
Normal file
@@ -0,0 +1,489 @@
|
||||
/**
|
||||
* Tests for the SQLite-backed ECC state store and CLI commands.
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const {
|
||||
createStateStore,
|
||||
resolveStateStorePath,
|
||||
} = require('../../scripts/lib/state-store');
|
||||
|
||||
const ECC_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'ecc.js');
|
||||
const STATUS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'status.js');
|
||||
const SESSIONS_SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'sessions-cli.js');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createTempDir(prefix) {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
function cleanupTempDir(dirPath) {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function runNode(scriptPath, args = [], options = {}) {
|
||||
return spawnSync('node', [scriptPath, ...args], {
|
||||
encoding: 'utf8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
...(options.env || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function parseJson(stdout) {
|
||||
return JSON.parse(stdout.trim());
|
||||
}
|
||||
|
||||
function seedStore(dbPath) {
|
||||
const store = createStateStore({ dbPath });
|
||||
|
||||
store.upsertSession({
|
||||
id: 'session-active',
|
||||
adapterId: 'dmux-tmux',
|
||||
harness: 'claude',
|
||||
state: 'active',
|
||||
repoRoot: '/tmp/ecc-repo',
|
||||
startedAt: '2026-03-15T08:00:00.000Z',
|
||||
endedAt: null,
|
||||
snapshot: {
|
||||
schemaVersion: 'ecc.session.v1',
|
||||
adapterId: 'dmux-tmux',
|
||||
session: {
|
||||
id: 'session-active',
|
||||
kind: 'orchestrated',
|
||||
state: 'active',
|
||||
repoRoot: '/tmp/ecc-repo',
|
||||
},
|
||||
workers: [
|
||||
{
|
||||
id: 'worker-1',
|
||||
label: 'Worker 1',
|
||||
state: 'active',
|
||||
branch: 'feat/state-store',
|
||||
worktree: '/tmp/ecc-repo/.worktrees/worker-1',
|
||||
},
|
||||
{
|
||||
id: 'worker-2',
|
||||
label: 'Worker 2',
|
||||
state: 'idle',
|
||||
branch: 'feat/state-store',
|
||||
worktree: '/tmp/ecc-repo/.worktrees/worker-2',
|
||||
},
|
||||
],
|
||||
aggregates: {
|
||||
workerCount: 2,
|
||||
states: {
|
||||
active: 1,
|
||||
idle: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
store.upsertSession({
|
||||
id: 'session-recorded',
|
||||
adapterId: 'claude-history',
|
||||
harness: 'claude',
|
||||
state: 'recorded',
|
||||
repoRoot: '/tmp/ecc-repo',
|
||||
startedAt: '2026-03-14T18:00:00.000Z',
|
||||
endedAt: '2026-03-14T19:00:00.000Z',
|
||||
snapshot: {
|
||||
schemaVersion: 'ecc.session.v1',
|
||||
adapterId: 'claude-history',
|
||||
session: {
|
||||
id: 'session-recorded',
|
||||
kind: 'history',
|
||||
state: 'recorded',
|
||||
repoRoot: '/tmp/ecc-repo',
|
||||
},
|
||||
workers: [
|
||||
{
|
||||
id: 'worker-hist',
|
||||
label: 'History Worker',
|
||||
state: 'recorded',
|
||||
branch: 'main',
|
||||
worktree: '/tmp/ecc-repo',
|
||||
},
|
||||
],
|
||||
aggregates: {
|
||||
workerCount: 1,
|
||||
states: {
|
||||
recorded: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
store.insertSkillRun({
|
||||
id: 'skill-run-1',
|
||||
skillId: 'tdd-workflow',
|
||||
skillVersion: '1.0.0',
|
||||
sessionId: 'session-active',
|
||||
taskDescription: 'Write store tests',
|
||||
outcome: 'success',
|
||||
failureReason: null,
|
||||
tokensUsed: 1200,
|
||||
durationMs: 3500,
|
||||
userFeedback: 'useful',
|
||||
createdAt: '2026-03-15T08:05:00.000Z',
|
||||
});
|
||||
|
||||
store.insertSkillRun({
|
||||
id: 'skill-run-2',
|
||||
skillId: 'security-review',
|
||||
skillVersion: '1.0.0',
|
||||
sessionId: 'session-active',
|
||||
taskDescription: 'Review state-store design',
|
||||
outcome: 'failed',
|
||||
failureReason: 'timeout',
|
||||
tokensUsed: 800,
|
||||
durationMs: 1800,
|
||||
userFeedback: null,
|
||||
createdAt: '2026-03-15T08:06:00.000Z',
|
||||
});
|
||||
|
||||
store.insertSkillRun({
|
||||
id: 'skill-run-3',
|
||||
skillId: 'code-reviewer',
|
||||
skillVersion: '1.0.0',
|
||||
sessionId: 'session-recorded',
|
||||
taskDescription: 'Inspect CLI formatting',
|
||||
outcome: 'success',
|
||||
failureReason: null,
|
||||
tokensUsed: 500,
|
||||
durationMs: 900,
|
||||
userFeedback: 'clear',
|
||||
createdAt: '2026-03-15T08:07:00.000Z',
|
||||
});
|
||||
|
||||
store.insertSkillRun({
|
||||
id: 'skill-run-4',
|
||||
skillId: 'planner',
|
||||
skillVersion: '1.0.0',
|
||||
sessionId: 'session-recorded',
|
||||
taskDescription: 'Outline ECC 2.0 work',
|
||||
outcome: 'unknown',
|
||||
failureReason: null,
|
||||
tokensUsed: 300,
|
||||
durationMs: 500,
|
||||
userFeedback: null,
|
||||
createdAt: '2026-03-15T08:08:00.000Z',
|
||||
});
|
||||
|
||||
store.upsertSkillVersion({
|
||||
skillId: 'tdd-workflow',
|
||||
version: '1.0.0',
|
||||
contentHash: 'abc123',
|
||||
amendmentReason: 'initial',
|
||||
promotedAt: '2026-03-10T00:00:00.000Z',
|
||||
rolledBackAt: null,
|
||||
});
|
||||
|
||||
store.insertDecision({
|
||||
id: 'decision-1',
|
||||
sessionId: 'session-active',
|
||||
title: 'Use SQLite for durable state',
|
||||
rationale: 'Need queryable local state for ECC control plane',
|
||||
alternatives: ['json-files', 'memory-only'],
|
||||
supersedes: null,
|
||||
status: 'active',
|
||||
createdAt: '2026-03-15T08:09:00.000Z',
|
||||
});
|
||||
|
||||
store.upsertInstallState({
|
||||
targetId: 'claude-home',
|
||||
targetRoot: '/tmp/home/.claude',
|
||||
profile: 'developer',
|
||||
modules: ['rules-core', 'orchestration'],
|
||||
operations: [
|
||||
{
|
||||
kind: 'copy-file',
|
||||
destinationPath: '/tmp/home/.claude/agents/planner.md',
|
||||
},
|
||||
],
|
||||
installedAt: '2026-03-15T07:00:00.000Z',
|
||||
sourceVersion: '1.8.0',
|
||||
});
|
||||
|
||||
store.insertGovernanceEvent({
|
||||
id: 'gov-1',
|
||||
sessionId: 'session-active',
|
||||
eventType: 'policy-review-required',
|
||||
payload: {
|
||||
severity: 'warning',
|
||||
owner: 'security-reviewer',
|
||||
},
|
||||
resolvedAt: null,
|
||||
resolution: null,
|
||||
createdAt: '2026-03-15T08:10:00.000Z',
|
||||
});
|
||||
|
||||
store.insertGovernanceEvent({
|
||||
id: 'gov-2',
|
||||
sessionId: 'session-recorded',
|
||||
eventType: 'decision-accepted',
|
||||
payload: {
|
||||
severity: 'info',
|
||||
},
|
||||
resolvedAt: '2026-03-15T08:11:00.000Z',
|
||||
resolution: 'accepted',
|
||||
createdAt: '2026-03-15T08:09:30.000Z',
|
||||
});
|
||||
|
||||
store.close();
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Testing state-store ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (test('creates the default state.db path and applies migrations idempotently', () => {
|
||||
const homeDir = createTempDir('ecc-state-home-');
|
||||
|
||||
try {
|
||||
const expectedPath = path.join(homeDir, '.claude', 'ecc', 'state.db');
|
||||
assert.strictEqual(resolveStateStorePath({ homeDir }), expectedPath);
|
||||
|
||||
const firstStore = createStateStore({ homeDir });
|
||||
const firstMigrations = firstStore.getAppliedMigrations();
|
||||
firstStore.close();
|
||||
|
||||
assert.strictEqual(firstMigrations.length, 1);
|
||||
assert.strictEqual(firstMigrations[0].version, 1);
|
||||
assert.ok(fs.existsSync(expectedPath));
|
||||
|
||||
const secondStore = createStateStore({ homeDir });
|
||||
const secondMigrations = secondStore.getAppliedMigrations();
|
||||
secondStore.close();
|
||||
|
||||
assert.strictEqual(secondMigrations.length, 1);
|
||||
assert.strictEqual(secondMigrations[0].version, 1);
|
||||
} finally {
|
||||
cleanupTempDir(homeDir);
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (test('preserves SQLite special database names like :memory:', () => {
|
||||
const tempDir = createTempDir('ecc-state-memory-');
|
||||
const previousCwd = process.cwd();
|
||||
|
||||
try {
|
||||
process.chdir(tempDir);
|
||||
assert.strictEqual(resolveStateStorePath({ dbPath: ':memory:' }), ':memory:');
|
||||
|
||||
const store = createStateStore({ dbPath: ':memory:' });
|
||||
assert.strictEqual(store.dbPath, ':memory:');
|
||||
assert.strictEqual(store.getAppliedMigrations().length, 1);
|
||||
store.close();
|
||||
|
||||
assert.ok(!fs.existsSync(path.join(tempDir, ':memory:')));
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
cleanupTempDir(tempDir);
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (test('stores sessions and returns detailed session views with workers, skill runs, and decisions', () => {
|
||||
const testDir = createTempDir('ecc-state-db-');
|
||||
const dbPath = path.join(testDir, 'state.db');
|
||||
|
||||
try {
|
||||
seedStore(dbPath);
|
||||
|
||||
const store = createStateStore({ dbPath });
|
||||
const listResult = store.listRecentSessions({ limit: 10 });
|
||||
const detail = store.getSessionDetail('session-active');
|
||||
store.close();
|
||||
|
||||
assert.strictEqual(listResult.totalCount, 2);
|
||||
assert.strictEqual(listResult.sessions[0].id, 'session-active');
|
||||
assert.strictEqual(detail.session.id, 'session-active');
|
||||
assert.strictEqual(detail.workers.length, 2);
|
||||
assert.strictEqual(detail.skillRuns.length, 2);
|
||||
assert.strictEqual(detail.decisions.length, 1);
|
||||
assert.deepStrictEqual(detail.decisions[0].alternatives, ['json-files', 'memory-only']);
|
||||
} finally {
|
||||
cleanupTempDir(testDir);
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (test('builds a status snapshot with active sessions, skill rates, install health, and pending governance', () => {
|
||||
const testDir = createTempDir('ecc-state-db-');
|
||||
const dbPath = path.join(testDir, 'state.db');
|
||||
|
||||
try {
|
||||
seedStore(dbPath);
|
||||
|
||||
const store = createStateStore({ dbPath });
|
||||
const status = store.getStatus();
|
||||
store.close();
|
||||
|
||||
assert.strictEqual(status.activeSessions.activeCount, 1);
|
||||
assert.strictEqual(status.activeSessions.sessions[0].id, 'session-active');
|
||||
assert.strictEqual(status.skillRuns.summary.totalCount, 4);
|
||||
assert.strictEqual(status.skillRuns.summary.successCount, 2);
|
||||
assert.strictEqual(status.skillRuns.summary.failureCount, 1);
|
||||
assert.strictEqual(status.skillRuns.summary.unknownCount, 1);
|
||||
assert.strictEqual(status.installHealth.status, 'healthy');
|
||||
assert.strictEqual(status.installHealth.totalCount, 1);
|
||||
assert.strictEqual(status.governance.pendingCount, 1);
|
||||
assert.strictEqual(status.governance.events[0].id, 'gov-1');
|
||||
} finally {
|
||||
cleanupTempDir(testDir);
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (test('validates entity payloads before writing to the database', () => {
|
||||
const testDir = createTempDir('ecc-state-db-');
|
||||
const dbPath = path.join(testDir, 'state.db');
|
||||
|
||||
try {
|
||||
const store = createStateStore({ dbPath });
|
||||
assert.throws(() => {
|
||||
store.upsertSession({
|
||||
id: '',
|
||||
adapterId: 'dmux-tmux',
|
||||
harness: 'claude',
|
||||
state: 'active',
|
||||
repoRoot: '/tmp/repo',
|
||||
startedAt: '2026-03-15T08:00:00.000Z',
|
||||
endedAt: null,
|
||||
snapshot: {},
|
||||
});
|
||||
}, /Invalid session/);
|
||||
|
||||
assert.throws(() => {
|
||||
store.insertDecision({
|
||||
id: 'decision-invalid',
|
||||
sessionId: 'missing-session',
|
||||
title: 'Reject non-array alternatives',
|
||||
rationale: 'alternatives must be an array',
|
||||
alternatives: { unexpected: true },
|
||||
supersedes: null,
|
||||
status: 'active',
|
||||
createdAt: '2026-03-15T08:15:00.000Z',
|
||||
});
|
||||
}, /Invalid decision/);
|
||||
|
||||
assert.throws(() => {
|
||||
store.upsertInstallState({
|
||||
targetId: 'claude-home',
|
||||
targetRoot: '/tmp/home/.claude',
|
||||
profile: 'developer',
|
||||
modules: 'rules-core',
|
||||
operations: [],
|
||||
installedAt: '2026-03-15T07:00:00.000Z',
|
||||
sourceVersion: '1.8.0',
|
||||
});
|
||||
}, /Invalid installState/);
|
||||
|
||||
store.close();
|
||||
} finally {
|
||||
cleanupTempDir(testDir);
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (test('status CLI supports human-readable and --json output', () => {
|
||||
const testDir = createTempDir('ecc-state-cli-');
|
||||
const dbPath = path.join(testDir, 'state.db');
|
||||
|
||||
try {
|
||||
seedStore(dbPath);
|
||||
|
||||
const jsonResult = runNode(STATUS_SCRIPT, ['--db', dbPath, '--json']);
|
||||
assert.strictEqual(jsonResult.status, 0, jsonResult.stderr);
|
||||
const jsonPayload = parseJson(jsonResult.stdout);
|
||||
assert.strictEqual(jsonPayload.activeSessions.activeCount, 1);
|
||||
assert.strictEqual(jsonPayload.governance.pendingCount, 1);
|
||||
|
||||
const humanResult = runNode(STATUS_SCRIPT, ['--db', dbPath]);
|
||||
assert.strictEqual(humanResult.status, 0, humanResult.stderr);
|
||||
assert.match(humanResult.stdout, /Active sessions: 1/);
|
||||
assert.match(humanResult.stdout, /Skill runs \(last 20\):/);
|
||||
assert.match(humanResult.stdout, /Install health: healthy/);
|
||||
assert.match(humanResult.stdout, /Pending governance events: 1/);
|
||||
} finally {
|
||||
cleanupTempDir(testDir);
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (test('sessions CLI supports list and detail views in human-readable and --json output', () => {
|
||||
const testDir = createTempDir('ecc-state-cli-');
|
||||
const dbPath = path.join(testDir, 'state.db');
|
||||
|
||||
try {
|
||||
seedStore(dbPath);
|
||||
|
||||
const listJsonResult = runNode(SESSIONS_SCRIPT, ['--db', dbPath, '--json']);
|
||||
assert.strictEqual(listJsonResult.status, 0, listJsonResult.stderr);
|
||||
const listPayload = parseJson(listJsonResult.stdout);
|
||||
assert.strictEqual(listPayload.totalCount, 2);
|
||||
assert.strictEqual(listPayload.sessions[0].id, 'session-active');
|
||||
|
||||
const detailJsonResult = runNode(SESSIONS_SCRIPT, ['session-active', '--db', dbPath, '--json']);
|
||||
assert.strictEqual(detailJsonResult.status, 0, detailJsonResult.stderr);
|
||||
const detailPayload = parseJson(detailJsonResult.stdout);
|
||||
assert.strictEqual(detailPayload.session.id, 'session-active');
|
||||
assert.strictEqual(detailPayload.workers.length, 2);
|
||||
assert.strictEqual(detailPayload.skillRuns.length, 2);
|
||||
assert.strictEqual(detailPayload.decisions.length, 1);
|
||||
|
||||
const detailHumanResult = runNode(SESSIONS_SCRIPT, ['session-active', '--db', dbPath]);
|
||||
assert.strictEqual(detailHumanResult.status, 0, detailHumanResult.stderr);
|
||||
assert.match(detailHumanResult.stdout, /Session: session-active/);
|
||||
assert.match(detailHumanResult.stdout, /Workers: 2/);
|
||||
assert.match(detailHumanResult.stdout, /Skill runs: 2/);
|
||||
assert.match(detailHumanResult.stdout, /Decisions: 1/);
|
||||
} finally {
|
||||
cleanupTempDir(testDir);
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (test('ecc CLI delegates the new status and sessions subcommands', () => {
|
||||
const testDir = createTempDir('ecc-state-cli-');
|
||||
const dbPath = path.join(testDir, 'state.db');
|
||||
|
||||
try {
|
||||
seedStore(dbPath);
|
||||
|
||||
const statusResult = runNode(ECC_SCRIPT, ['status', '--db', dbPath, '--json']);
|
||||
assert.strictEqual(statusResult.status, 0, statusResult.stderr);
|
||||
const statusPayload = parseJson(statusResult.stdout);
|
||||
assert.strictEqual(statusPayload.activeSessions.activeCount, 1);
|
||||
|
||||
const sessionsResult = runNode(ECC_SCRIPT, ['sessions', 'session-active', '--db', dbPath, '--json']);
|
||||
assert.strictEqual(sessionsResult.status, 0, sessionsResult.stderr);
|
||||
const sessionsPayload = parseJson(sessionsResult.stdout);
|
||||
assert.strictEqual(sessionsPayload.session.id, 'session-active');
|
||||
assert.strictEqual(sessionsPayload.skillRuns.length, 2);
|
||||
} finally {
|
||||
cleanupTempDir(testDir);
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
Reference in New Issue
Block a user