feat: add SQLite state store and ECC status CLI

This commit is contained in:
Affaan Mustafa
2026-03-15 21:07:14 -07:00
parent fcaf78e449
commit 9799f3d2a8
10 changed files with 2210 additions and 0 deletions

View File

@@ -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
`);

View 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,
};

View 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,
};

View 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,
};

View 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
View 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
View 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,
};