feat: track linked work items in status

This commit is contained in:
Affaan Mustafa
2026-05-11 11:51:45 -04:00
committed by Affaan Mustafa
parent 579284c9be
commit 8926ea925e
7 changed files with 396 additions and 7 deletions

View File

@@ -107,12 +107,43 @@ CREATE INDEX IF NOT EXISTS idx_governance_events_session_id_created_at
ON governance_events (session_id, created_at DESC);
`;
const WORK_ITEMS_SQL = `
CREATE TABLE IF NOT EXISTS work_items (
id TEXT PRIMARY KEY,
source TEXT NOT NULL,
source_id TEXT,
title TEXT NOT NULL,
status TEXT NOT NULL,
priority TEXT,
url TEXT,
owner TEXT,
repo_root TEXT,
session_id TEXT,
metadata TEXT NOT NULL CHECK (json_valid(metadata)),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_work_items_status_updated_at
ON work_items (status, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_work_items_source_source_id
ON work_items (source, source_id);
CREATE INDEX IF NOT EXISTS idx_work_items_session_id_updated_at
ON work_items (session_id, updated_at DESC);
`;
const MIGRATIONS = [
{
version: 1,
name: '001_initial_state_store',
sql: INITIAL_SCHEMA_SQL,
},
{
version: 2,
name: '002_work_items',
sql: WORK_ITEMS_SQL,
},
];
function ensureMigrationTable(db) {

View File

@@ -5,6 +5,8 @@ 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']);
const CLOSED_WORK_ITEM_STATUSES = new Set(['done', 'closed', 'resolved', 'merged', 'cancelled']);
const ATTENTION_WORK_ITEM_STATUSES = new Set(['blocked', 'needs-review', 'failed', 'stalled']);
function normalizeLimit(value, fallback) {
if (value === undefined || value === null) {
@@ -121,6 +123,24 @@ function mapGovernanceEventRow(row) {
};
}
function mapWorkItemRow(row) {
return {
id: row.id,
source: row.source,
sourceId: row.source_id,
title: row.title,
status: row.status,
priority: row.priority,
url: row.url,
owner: row.owner,
repoRoot: row.repo_root,
sessionId: row.session_id,
metadata: parseJsonColumn(row.metadata, null),
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
function classifyOutcome(outcome) {
const normalized = String(outcome || '').toLowerCase();
if (SUCCESS_OUTCOMES.has(normalized)) {
@@ -134,6 +154,19 @@ function classifyOutcome(outcome) {
return 'unknown';
}
function classifyWorkItemStatus(status) {
const normalized = String(status || '').toLowerCase();
if (CLOSED_WORK_ITEM_STATUSES.has(normalized)) {
return 'closed';
}
if (ATTENTION_WORK_ITEM_STATUSES.has(normalized)) {
return 'attention';
}
return 'open';
}
function toPercent(numerator, denominator) {
if (denominator === 0) {
return null;
@@ -202,11 +235,36 @@ function summarizeInstallHealth(installations) {
};
}
function summarizeReadiness({ activeSessionCount, skillRuns, installHealth, pendingGovernanceCount }) {
function summarizeWorkItems(workItems) {
const summary = {
totalCount: workItems.length,
openCount: 0,
blockedCount: 0,
closedCount: 0,
items: workItems,
};
for (const workItem of workItems) {
const classification = classifyWorkItemStatus(workItem.status);
if (classification === 'closed') {
summary.closedCount += 1;
} else if (classification === 'attention') {
summary.openCount += 1;
summary.blockedCount += 1;
} else {
summary.openCount += 1;
}
}
return summary;
}
function summarizeReadiness({ activeSessionCount, skillRuns, installHealth, pendingGovernanceCount, workItems }) {
const failedSkillRuns = skillRuns.summary.failureCount;
const warningInstallations = installHealth.warningCount;
const pendingGovernanceEvents = pendingGovernanceCount;
const attentionCount = failedSkillRuns + warningInstallations + pendingGovernanceEvents;
const blockedWorkItems = workItems.blockedCount;
const attentionCount = failedSkillRuns + warningInstallations + pendingGovernanceEvents + blockedWorkItems;
return {
status: attentionCount > 0 ? 'attention' : 'ok',
@@ -215,6 +273,7 @@ function summarizeReadiness({ activeSessionCount, skillRuns, installHealth, pend
failedSkillRuns,
warningInstallations,
pendingGovernanceEvents,
blockedWorkItems,
};
}
@@ -301,6 +360,25 @@ function normalizeGovernanceEventInput(governanceEvent) {
};
}
function normalizeWorkItemInput(workItem) {
const now = new Date().toISOString();
return {
id: workItem.id,
source: workItem.source,
sourceId: workItem.sourceId ?? null,
title: workItem.title,
status: workItem.status,
priority: workItem.priority ?? null,
url: workItem.url ?? null,
owner: workItem.owner ?? null,
repoRoot: workItem.repoRoot ?? null,
sessionId: workItem.sessionId ?? null,
metadata: workItem.metadata ?? null,
createdAt: workItem.createdAt || now,
updatedAt: workItem.updatedAt || now,
};
}
function createQueryApi(db) {
const listRecentSessionsStatement = db.prepare(`
SELECT *
@@ -366,6 +444,22 @@ function createQueryApi(db) {
ORDER BY created_at DESC, id DESC
LIMIT ?
`);
const listWorkItemsStatement = db.prepare(`
SELECT *
FROM work_items
ORDER BY updated_at DESC, id DESC
LIMIT ?
`);
const listAllWorkItemsStatement = db.prepare(`
SELECT *
FROM work_items
ORDER BY updated_at DESC, id DESC
`);
const getWorkItemStatement = db.prepare(`
SELECT *
FROM work_items
WHERE id = ?
`);
const getSkillVersionStatement = db.prepare(`
SELECT *
FROM skill_versions
@@ -547,6 +641,50 @@ function createQueryApi(db) {
created_at = excluded.created_at
`);
const upsertWorkItemStatement = db.prepare(`
INSERT INTO work_items (
id,
source,
source_id,
title,
status,
priority,
url,
owner,
repo_root,
session_id,
metadata,
created_at,
updated_at
) VALUES (
@id,
@source,
@source_id,
@title,
@status,
@priority,
@url,
@owner,
@repo_root,
@session_id,
@metadata,
@created_at,
@updated_at
)
ON CONFLICT(id) DO UPDATE SET
source = excluded.source,
source_id = excluded.source_id,
title = excluded.title,
status = excluded.status,
priority = excluded.priority,
url = excluded.url,
owner = excluded.owner,
repo_root = excluded.repo_root,
session_id = excluded.session_id,
metadata = excluded.metadata,
updated_at = excluded.updated_at
`);
function getSessionById(id) {
const row = getSessionStatement.get(id);
return row ? mapSessionRow(row) : null;
@@ -582,12 +720,15 @@ function createQueryApi(db) {
const activeLimit = normalizeLimit(options.activeLimit, 5);
const recentSkillRunLimit = normalizeLimit(options.recentSkillRunLimit, 20);
const pendingLimit = normalizeLimit(options.pendingLimit, 5);
const workItemLimit = normalizeLimit(options.workItemLimit, 10);
const activeSessions = listActiveSessionsStatement.all(activeLimit).map(mapSessionRow);
const activeSessionCount = countActiveSessionsStatement.get().total_count;
const recentSkillRuns = listRecentSkillRunsStatement.all(recentSkillRunLimit).map(mapSkillRunRow);
const installations = listInstallStateStatement.all().map(mapInstallStateRow);
const pendingGovernanceEvents = listPendingGovernanceStatement.all(pendingLimit).map(mapGovernanceEventRow);
const workItems = summarizeWorkItems(listAllWorkItemsStatement.all().map(mapWorkItemRow));
workItems.items = listWorkItemsStatement.all(workItemLimit).map(mapWorkItemRow);
const skillRuns = {
windowSize: recentSkillRunLimit,
summary: summarizeSkillRuns(recentSkillRuns),
@@ -603,6 +744,7 @@ function createQueryApi(db) {
skillRuns,
installHealth,
pendingGovernanceCount,
workItems,
}),
activeSessions: {
activeCount: activeSessionCount,
@@ -614,6 +756,7 @@ function createQueryApi(db) {
pendingCount: pendingGovernanceCount,
events: pendingGovernanceEvents,
},
workItems,
};
}
@@ -683,6 +826,27 @@ function createQueryApi(db) {
});
return normalized;
},
upsertWorkItem(workItem) {
const normalized = normalizeWorkItemInput(workItem);
assertValidEntity('workItem', normalized);
upsertWorkItemStatement.run({
id: normalized.id,
source: normalized.source,
source_id: normalized.sourceId,
title: normalized.title,
status: normalized.status,
priority: normalized.priority,
url: normalized.url,
owner: normalized.owner,
repo_root: normalized.repoRoot,
session_id: normalized.sessionId,
metadata: stringifyJson(normalized.metadata, 'workItem.metadata'),
created_at: normalized.createdAt,
updated_at: normalized.updatedAt,
});
const row = getWorkItemStatement.get(normalized.id);
return row ? mapWorkItemRow(row) : null;
},
upsertSession(session) {
const normalized = normalizeSessionInput(session);
assertValidEntity('session', normalized);

View File

@@ -13,6 +13,7 @@ const ENTITY_DEFINITIONS = {
decision: 'decision',
installState: 'installState',
governanceEvent: 'governanceEvent',
workItem: 'workItem',
};
let cachedSchema = null;

View File

@@ -11,7 +11,7 @@ function showHelp(exitCode = 0) {
Usage: node scripts/status.js [--db <path>] [--json|--markdown] [--write <path>] [--limit <n>]
Query the ECC SQLite state store for active sessions, recent skill runs,
install health, and pending governance events.
install health, pending governance events, and linked work items.
`);
process.exit(exitCode);
}
@@ -142,6 +142,24 @@ function printGovernance(section) {
}
}
function printWorkItems(section) {
console.log(`Work items: ${section.openCount} open, ${section.blockedCount} blocked, ${section.closedCount} closed`);
if (section.items.length === 0) {
console.log(' - none');
return;
}
for (const item of section.items.slice(0, 10)) {
const sourceId = item.sourceId ? `#${item.sourceId}` : item.id;
console.log(` - ${item.source}/${sourceId} ${item.status}: ${item.title}`);
console.log(` Owner: ${item.owner || '(unassigned)'}`);
console.log(` Updated: ${item.updatedAt}`);
if (item.url) {
console.log(` URL: ${item.url}`);
}
}
}
function printReadiness(section) {
console.log(`Readiness: ${section.status}`);
console.log(` Attention items: ${section.attentionCount}`);
@@ -149,6 +167,7 @@ function printReadiness(section) {
console.log(` Failed skill runs: ${section.failedSkillRuns}`);
console.log(` Warning installs: ${section.warningInstallations}`);
console.log(` Pending governance: ${section.pendingGovernanceEvents}`);
console.log(` Blocked work items: ${section.blockedWorkItems}`);
}
function printHuman(payload) {
@@ -163,6 +182,8 @@ function printHuman(payload) {
printInstallHealth(payload.installHealth);
console.log();
printGovernance(payload.governance);
console.log();
printWorkItems(payload.workItems);
}
function formatPercent(value) {
@@ -188,6 +209,7 @@ function renderMarkdown(payload) {
`Failed skill runs: ${payload.readiness.failedSkillRuns}`,
`Warning installs: ${payload.readiness.warningInstallations}`,
`Pending governance: ${payload.readiness.pendingGovernanceEvents}`,
`Blocked work items: ${payload.readiness.blockedWorkItems}`,
'',
'## Active Sessions',
'',
@@ -267,6 +289,30 @@ function renderMarkdown(payload) {
}
}
lines.push(
'',
'## Work Items',
'',
`Open: ${payload.workItems.openCount}`,
`Blocked: ${payload.workItems.blockedCount}`,
`Closed: ${payload.workItems.closedCount}`
);
if (payload.workItems.items.length === 0) {
lines.push('', '- none');
} else {
lines.push('', 'Recent work items:');
for (const item of payload.workItems.items.slice(0, 10)) {
const sourceId = item.sourceId ? `#${item.sourceId}` : item.id;
lines.push(`- ${formatCode(item.source)} ${formatCode(sourceId)} ${item.status}: ${item.title}`);
lines.push(` - Owner: ${item.owner || '(unassigned)'}`);
lines.push(` - Updated: ${item.updatedAt}`);
if (item.url) {
lines.push(` - URL: ${item.url}`);
}
}
}
return `${lines.join('\n')}\n`;
}
@@ -296,6 +342,7 @@ async function main() {
activeLimit: options.limit,
recentSkillRunLimit: 20,
pendingLimit: options.limit,
workItemLimit: options.limit,
}),
};