mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-14 02:10:07 +08:00
feat: track linked work items in status
This commit is contained in:
parent
579284c9be
commit
8926ea925e
@ -94,7 +94,7 @@ This repo is the raw code only. The guides explain everything.
|
|||||||
- **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system.
|
- **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system.
|
||||||
- **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone.
|
- **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone.
|
||||||
- **ECC 2.0 alpha is in-tree** — the Rust control-plane prototype in `ecc2/` now builds locally and exposes `dashboard`, `start`, `sessions`, `status`, `stop`, `resume`, and `daemon` commands. It is usable as an alpha, not yet a general release.
|
- **ECC 2.0 alpha is in-tree** — the Rust control-plane prototype in `ecc2/` now builds locally and exposes `dashboard`, `start`, `sessions`, `status`, `stop`, `resume`, and `daemon` commands. It is usable as an alpha, not yet a general release.
|
||||||
- **Operator status snapshots** — `ecc status --markdown --write status.md` turns the local state store into a portable handoff covering readiness, active sessions, skill-run health, install health, and pending governance events.
|
- **Operator status snapshots** — `ecc status --markdown --write status.md` turns the local state store into a portable handoff covering readiness, active sessions, skill-run health, install health, pending governance events, and linked work items from Linear/GitHub/handoffs.
|
||||||
- **Ecosystem hardening** — AgentShield, ECC Tools cost controls, billing portal work, and website refreshes continue to ship around the core plugin instead of drifting into separate silos.
|
- **Ecosystem hardening** — AgentShield, ECC Tools cost controls, billing portal work, and website refreshes continue to ship around the core plugin instead of drifting into separate silos.
|
||||||
|
|
||||||
### v1.9.0 — Selective Install & Language Expansion (Mar 2026)
|
### v1.9.0 — Selective Install & Language Expansion (Mar 2026)
|
||||||
|
|||||||
@ -40,6 +40,12 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/governanceEvent"
|
"$ref": "#/$defs/governanceEvent"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"workItems": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/workItem"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
@ -311,6 +317,66 @@
|
|||||||
"$ref": "#/$defs/nonEmptyString"
|
"$ref": "#/$defs/nonEmptyString"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"workItem": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"source",
|
||||||
|
"sourceId",
|
||||||
|
"title",
|
||||||
|
"status",
|
||||||
|
"priority",
|
||||||
|
"url",
|
||||||
|
"owner",
|
||||||
|
"repoRoot",
|
||||||
|
"sessionId",
|
||||||
|
"metadata",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/nonEmptyString"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"$ref": "#/$defs/nonEmptyString"
|
||||||
|
},
|
||||||
|
"sourceId": {
|
||||||
|
"$ref": "#/$defs/nullableString"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"$ref": "#/$defs/nonEmptyString"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"$ref": "#/$defs/nonEmptyString"
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"$ref": "#/$defs/nullableString"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"$ref": "#/$defs/nullableString"
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"$ref": "#/$defs/nullableString"
|
||||||
|
},
|
||||||
|
"repoRoot": {
|
||||||
|
"$ref": "#/$defs/nullableString"
|
||||||
|
},
|
||||||
|
"sessionId": {
|
||||||
|
"$ref": "#/$defs/nullableString"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"$ref": "#/$defs/jsonValue"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"$ref": "#/$defs/nonEmptyString"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"$ref": "#/$defs/nonEmptyString"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,12 +107,43 @@ CREATE INDEX IF NOT EXISTS idx_governance_events_session_id_created_at
|
|||||||
ON governance_events (session_id, created_at DESC);
|
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 = [
|
const MIGRATIONS = [
|
||||||
{
|
{
|
||||||
version: 1,
|
version: 1,
|
||||||
name: '001_initial_state_store',
|
name: '001_initial_state_store',
|
||||||
sql: INITIAL_SCHEMA_SQL,
|
sql: INITIAL_SCHEMA_SQL,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
name: '002_work_items',
|
||||||
|
sql: WORK_ITEMS_SQL,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function ensureMigrationTable(db) {
|
function ensureMigrationTable(db) {
|
||||||
|
|||||||
@ -5,6 +5,8 @@ const { assertValidEntity } = require('./schema');
|
|||||||
const ACTIVE_SESSION_STATES = ['active', 'running', 'idle'];
|
const ACTIVE_SESSION_STATES = ['active', 'running', 'idle'];
|
||||||
const SUCCESS_OUTCOMES = new Set(['success', 'succeeded', 'passed']);
|
const SUCCESS_OUTCOMES = new Set(['success', 'succeeded', 'passed']);
|
||||||
const FAILURE_OUTCOMES = new Set(['failure', 'failed', 'error']);
|
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) {
|
function normalizeLimit(value, fallback) {
|
||||||
if (value === undefined || value === null) {
|
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) {
|
function classifyOutcome(outcome) {
|
||||||
const normalized = String(outcome || '').toLowerCase();
|
const normalized = String(outcome || '').toLowerCase();
|
||||||
if (SUCCESS_OUTCOMES.has(normalized)) {
|
if (SUCCESS_OUTCOMES.has(normalized)) {
|
||||||
@ -134,6 +154,19 @@ function classifyOutcome(outcome) {
|
|||||||
return 'unknown';
|
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) {
|
function toPercent(numerator, denominator) {
|
||||||
if (denominator === 0) {
|
if (denominator === 0) {
|
||||||
return null;
|
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 failedSkillRuns = skillRuns.summary.failureCount;
|
||||||
const warningInstallations = installHealth.warningCount;
|
const warningInstallations = installHealth.warningCount;
|
||||||
const pendingGovernanceEvents = pendingGovernanceCount;
|
const pendingGovernanceEvents = pendingGovernanceCount;
|
||||||
const attentionCount = failedSkillRuns + warningInstallations + pendingGovernanceEvents;
|
const blockedWorkItems = workItems.blockedCount;
|
||||||
|
const attentionCount = failedSkillRuns + warningInstallations + pendingGovernanceEvents + blockedWorkItems;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: attentionCount > 0 ? 'attention' : 'ok',
|
status: attentionCount > 0 ? 'attention' : 'ok',
|
||||||
@ -215,6 +273,7 @@ function summarizeReadiness({ activeSessionCount, skillRuns, installHealth, pend
|
|||||||
failedSkillRuns,
|
failedSkillRuns,
|
||||||
warningInstallations,
|
warningInstallations,
|
||||||
pendingGovernanceEvents,
|
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) {
|
function createQueryApi(db) {
|
||||||
const listRecentSessionsStatement = db.prepare(`
|
const listRecentSessionsStatement = db.prepare(`
|
||||||
SELECT *
|
SELECT *
|
||||||
@ -366,6 +444,22 @@ function createQueryApi(db) {
|
|||||||
ORDER BY created_at DESC, id DESC
|
ORDER BY created_at DESC, id DESC
|
||||||
LIMIT ?
|
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(`
|
const getSkillVersionStatement = db.prepare(`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM skill_versions
|
FROM skill_versions
|
||||||
@ -547,6 +641,50 @@ function createQueryApi(db) {
|
|||||||
created_at = excluded.created_at
|
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) {
|
function getSessionById(id) {
|
||||||
const row = getSessionStatement.get(id);
|
const row = getSessionStatement.get(id);
|
||||||
return row ? mapSessionRow(row) : null;
|
return row ? mapSessionRow(row) : null;
|
||||||
@ -582,12 +720,15 @@ function createQueryApi(db) {
|
|||||||
const activeLimit = normalizeLimit(options.activeLimit, 5);
|
const activeLimit = normalizeLimit(options.activeLimit, 5);
|
||||||
const recentSkillRunLimit = normalizeLimit(options.recentSkillRunLimit, 20);
|
const recentSkillRunLimit = normalizeLimit(options.recentSkillRunLimit, 20);
|
||||||
const pendingLimit = normalizeLimit(options.pendingLimit, 5);
|
const pendingLimit = normalizeLimit(options.pendingLimit, 5);
|
||||||
|
const workItemLimit = normalizeLimit(options.workItemLimit, 10);
|
||||||
|
|
||||||
const activeSessions = listActiveSessionsStatement.all(activeLimit).map(mapSessionRow);
|
const activeSessions = listActiveSessionsStatement.all(activeLimit).map(mapSessionRow);
|
||||||
const activeSessionCount = countActiveSessionsStatement.get().total_count;
|
const activeSessionCount = countActiveSessionsStatement.get().total_count;
|
||||||
const recentSkillRuns = listRecentSkillRunsStatement.all(recentSkillRunLimit).map(mapSkillRunRow);
|
const recentSkillRuns = listRecentSkillRunsStatement.all(recentSkillRunLimit).map(mapSkillRunRow);
|
||||||
const installations = listInstallStateStatement.all().map(mapInstallStateRow);
|
const installations = listInstallStateStatement.all().map(mapInstallStateRow);
|
||||||
const pendingGovernanceEvents = listPendingGovernanceStatement.all(pendingLimit).map(mapGovernanceEventRow);
|
const pendingGovernanceEvents = listPendingGovernanceStatement.all(pendingLimit).map(mapGovernanceEventRow);
|
||||||
|
const workItems = summarizeWorkItems(listAllWorkItemsStatement.all().map(mapWorkItemRow));
|
||||||
|
workItems.items = listWorkItemsStatement.all(workItemLimit).map(mapWorkItemRow);
|
||||||
const skillRuns = {
|
const skillRuns = {
|
||||||
windowSize: recentSkillRunLimit,
|
windowSize: recentSkillRunLimit,
|
||||||
summary: summarizeSkillRuns(recentSkillRuns),
|
summary: summarizeSkillRuns(recentSkillRuns),
|
||||||
@ -603,6 +744,7 @@ function createQueryApi(db) {
|
|||||||
skillRuns,
|
skillRuns,
|
||||||
installHealth,
|
installHealth,
|
||||||
pendingGovernanceCount,
|
pendingGovernanceCount,
|
||||||
|
workItems,
|
||||||
}),
|
}),
|
||||||
activeSessions: {
|
activeSessions: {
|
||||||
activeCount: activeSessionCount,
|
activeCount: activeSessionCount,
|
||||||
@ -614,6 +756,7 @@ function createQueryApi(db) {
|
|||||||
pendingCount: pendingGovernanceCount,
|
pendingCount: pendingGovernanceCount,
|
||||||
events: pendingGovernanceEvents,
|
events: pendingGovernanceEvents,
|
||||||
},
|
},
|
||||||
|
workItems,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -683,6 +826,27 @@ function createQueryApi(db) {
|
|||||||
});
|
});
|
||||||
return normalized;
|
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) {
|
upsertSession(session) {
|
||||||
const normalized = normalizeSessionInput(session);
|
const normalized = normalizeSessionInput(session);
|
||||||
assertValidEntity('session', normalized);
|
assertValidEntity('session', normalized);
|
||||||
|
|||||||
@ -13,6 +13,7 @@ const ENTITY_DEFINITIONS = {
|
|||||||
decision: 'decision',
|
decision: 'decision',
|
||||||
installState: 'installState',
|
installState: 'installState',
|
||||||
governanceEvent: 'governanceEvent',
|
governanceEvent: 'governanceEvent',
|
||||||
|
workItem: 'workItem',
|
||||||
};
|
};
|
||||||
|
|
||||||
let cachedSchema = null;
|
let cachedSchema = null;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ function showHelp(exitCode = 0) {
|
|||||||
Usage: node scripts/status.js [--db <path>] [--json|--markdown] [--write <path>] [--limit <n>]
|
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,
|
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);
|
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) {
|
function printReadiness(section) {
|
||||||
console.log(`Readiness: ${section.status}`);
|
console.log(`Readiness: ${section.status}`);
|
||||||
console.log(` Attention items: ${section.attentionCount}`);
|
console.log(` Attention items: ${section.attentionCount}`);
|
||||||
@ -149,6 +167,7 @@ function printReadiness(section) {
|
|||||||
console.log(` Failed skill runs: ${section.failedSkillRuns}`);
|
console.log(` Failed skill runs: ${section.failedSkillRuns}`);
|
||||||
console.log(` Warning installs: ${section.warningInstallations}`);
|
console.log(` Warning installs: ${section.warningInstallations}`);
|
||||||
console.log(` Pending governance: ${section.pendingGovernanceEvents}`);
|
console.log(` Pending governance: ${section.pendingGovernanceEvents}`);
|
||||||
|
console.log(` Blocked work items: ${section.blockedWorkItems}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function printHuman(payload) {
|
function printHuman(payload) {
|
||||||
@ -163,6 +182,8 @@ function printHuman(payload) {
|
|||||||
printInstallHealth(payload.installHealth);
|
printInstallHealth(payload.installHealth);
|
||||||
console.log();
|
console.log();
|
||||||
printGovernance(payload.governance);
|
printGovernance(payload.governance);
|
||||||
|
console.log();
|
||||||
|
printWorkItems(payload.workItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPercent(value) {
|
function formatPercent(value) {
|
||||||
@ -188,6 +209,7 @@ function renderMarkdown(payload) {
|
|||||||
`Failed skill runs: ${payload.readiness.failedSkillRuns}`,
|
`Failed skill runs: ${payload.readiness.failedSkillRuns}`,
|
||||||
`Warning installs: ${payload.readiness.warningInstallations}`,
|
`Warning installs: ${payload.readiness.warningInstallations}`,
|
||||||
`Pending governance: ${payload.readiness.pendingGovernanceEvents}`,
|
`Pending governance: ${payload.readiness.pendingGovernanceEvents}`,
|
||||||
|
`Blocked work items: ${payload.readiness.blockedWorkItems}`,
|
||||||
'',
|
'',
|
||||||
'## Active Sessions',
|
'## 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`;
|
return `${lines.join('\n')}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,6 +342,7 @@ async function main() {
|
|||||||
activeLimit: options.limit,
|
activeLimit: options.limit,
|
||||||
recentSkillRunLimit: 20,
|
recentSkillRunLimit: 20,
|
||||||
pendingLimit: options.limit,
|
pendingLimit: options.limit,
|
||||||
|
workItemLimit: options.limit,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -269,15 +269,16 @@ async function runTests() {
|
|||||||
const firstMigrations = firstStore.getAppliedMigrations();
|
const firstMigrations = firstStore.getAppliedMigrations();
|
||||||
firstStore.close();
|
firstStore.close();
|
||||||
|
|
||||||
assert.strictEqual(firstMigrations.length, 1);
|
assert.strictEqual(firstMigrations.length, 2);
|
||||||
assert.strictEqual(firstMigrations[0].version, 1);
|
assert.strictEqual(firstMigrations[0].version, 1);
|
||||||
|
assert.strictEqual(firstMigrations[1].version, 2);
|
||||||
assert.ok(fs.existsSync(expectedPath));
|
assert.ok(fs.existsSync(expectedPath));
|
||||||
|
|
||||||
const secondStore = await createStateStore({ homeDir });
|
const secondStore = await createStateStore({ homeDir });
|
||||||
const secondMigrations = secondStore.getAppliedMigrations();
|
const secondMigrations = secondStore.getAppliedMigrations();
|
||||||
secondStore.close();
|
secondStore.close();
|
||||||
|
|
||||||
assert.strictEqual(secondMigrations.length, 1);
|
assert.strictEqual(secondMigrations.length, 2);
|
||||||
assert.strictEqual(secondMigrations[0].version, 1);
|
assert.strictEqual(secondMigrations[0].version, 1);
|
||||||
} finally {
|
} finally {
|
||||||
cleanupTempDir(homeDir);
|
cleanupTempDir(homeDir);
|
||||||
@ -294,7 +295,7 @@ async function runTests() {
|
|||||||
|
|
||||||
const store = await createStateStore({ dbPath: ':memory:' });
|
const store = await createStateStore({ dbPath: ':memory:' });
|
||||||
assert.strictEqual(store.dbPath, ':memory:');
|
assert.strictEqual(store.dbPath, ':memory:');
|
||||||
assert.strictEqual(store.getAppliedMigrations().length, 1);
|
assert.strictEqual(store.getAppliedMigrations().length, 2);
|
||||||
store.close();
|
store.close();
|
||||||
|
|
||||||
assert.ok(!fs.existsSync(path.join(tempDir, ':memory:')));
|
assert.ok(!fs.existsSync(path.join(tempDir, ':memory:')));
|
||||||
@ -345,6 +346,7 @@ async function runTests() {
|
|||||||
assert.strictEqual(status.readiness.failedSkillRuns, 1);
|
assert.strictEqual(status.readiness.failedSkillRuns, 1);
|
||||||
assert.strictEqual(status.readiness.warningInstallations, 0);
|
assert.strictEqual(status.readiness.warningInstallations, 0);
|
||||||
assert.strictEqual(status.readiness.pendingGovernanceEvents, 1);
|
assert.strictEqual(status.readiness.pendingGovernanceEvents, 1);
|
||||||
|
assert.strictEqual(status.readiness.blockedWorkItems, 0);
|
||||||
assert.strictEqual(status.activeSessions.activeCount, 1);
|
assert.strictEqual(status.activeSessions.activeCount, 1);
|
||||||
assert.strictEqual(status.activeSessions.sessions[0].id, 'session-active');
|
assert.strictEqual(status.activeSessions.sessions[0].id, 'session-active');
|
||||||
assert.strictEqual(status.skillRuns.summary.totalCount, 4);
|
assert.strictEqual(status.skillRuns.summary.totalCount, 4);
|
||||||
@ -355,6 +357,7 @@ async function runTests() {
|
|||||||
assert.strictEqual(status.installHealth.totalCount, 1);
|
assert.strictEqual(status.installHealth.totalCount, 1);
|
||||||
assert.strictEqual(status.governance.pendingCount, 1);
|
assert.strictEqual(status.governance.pendingCount, 1);
|
||||||
assert.strictEqual(status.governance.events[0].id, 'gov-1');
|
assert.strictEqual(status.governance.events[0].id, 'gov-1');
|
||||||
|
assert.strictEqual(status.workItems.openCount, 0);
|
||||||
} finally {
|
} finally {
|
||||||
cleanupTempDir(testDir);
|
cleanupTempDir(testDir);
|
||||||
}
|
}
|
||||||
@ -385,6 +388,80 @@ async function runTests() {
|
|||||||
assert.deepStrictEqual(status.installHealth.installations, []);
|
assert.deepStrictEqual(status.installHealth.installations, []);
|
||||||
assert.strictEqual(status.governance.pendingCount, 0);
|
assert.strictEqual(status.governance.pendingCount, 0);
|
||||||
assert.deepStrictEqual(status.governance.events, []);
|
assert.deepStrictEqual(status.governance.events, []);
|
||||||
|
assert.strictEqual(status.workItems.totalCount, 0);
|
||||||
|
assert.deepStrictEqual(status.workItems.items, []);
|
||||||
|
} finally {
|
||||||
|
cleanupTempDir(testDir);
|
||||||
|
}
|
||||||
|
})) passed += 1; else failed += 1;
|
||||||
|
|
||||||
|
if (await test('tracks linked work items for Linear, GitHub, and handoff progress', async () => {
|
||||||
|
const testDir = createTempDir('ecc-state-work-items-');
|
||||||
|
const dbPath = path.join(testDir, 'state.db');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await seedStore(dbPath);
|
||||||
|
|
||||||
|
const store = await createStateStore({ dbPath });
|
||||||
|
const linearItem = store.upsertWorkItem({
|
||||||
|
id: 'linear-ecc-20-control-plane',
|
||||||
|
source: 'linear',
|
||||||
|
sourceId: 'ECC-20',
|
||||||
|
title: 'Define harness-neutral session/worktree contract',
|
||||||
|
status: 'in-progress',
|
||||||
|
priority: 'high',
|
||||||
|
url: 'https://linear.app/ecctools/issue/ECC-20',
|
||||||
|
owner: 'control-plane',
|
||||||
|
repoRoot: '/tmp/ecc-repo',
|
||||||
|
sessionId: 'session-active',
|
||||||
|
metadata: {
|
||||||
|
project: 'ECC 2.0: Control Plane',
|
||||||
|
},
|
||||||
|
createdAt: '2026-03-15T08:12:00.000Z',
|
||||||
|
updatedAt: '2026-03-15T08:15:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
store.upsertWorkItem({
|
||||||
|
id: 'handoff-release-gate',
|
||||||
|
source: 'handoff',
|
||||||
|
sourceId: 'ecc-rc1-release-decision-20260511.md',
|
||||||
|
title: 'Rerun rc.1 release gate before tag',
|
||||||
|
status: 'blocked',
|
||||||
|
priority: 'high',
|
||||||
|
owner: 'release',
|
||||||
|
repoRoot: '/tmp/ecc-repo',
|
||||||
|
metadata: {
|
||||||
|
blocker: 'tag decision pending',
|
||||||
|
},
|
||||||
|
createdAt: '2026-03-15T08:13:00.000Z',
|
||||||
|
updatedAt: '2026-03-15T08:16:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
store.upsertWorkItem({
|
||||||
|
id: 'github-pr-1738',
|
||||||
|
source: 'github',
|
||||||
|
sourceId: '1738',
|
||||||
|
title: 'Add Qwen install target',
|
||||||
|
status: 'merged',
|
||||||
|
priority: 'normal',
|
||||||
|
url: 'https://github.com/affaan-m/everything-claude-code/pull/1738',
|
||||||
|
owner: 'maintainer',
|
||||||
|
createdAt: '2026-03-15T08:14:00.000Z',
|
||||||
|
updatedAt: '2026-03-15T08:17:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = store.getStatus();
|
||||||
|
store.close();
|
||||||
|
|
||||||
|
assert.strictEqual(linearItem.id, 'linear-ecc-20-control-plane');
|
||||||
|
assert.strictEqual(linearItem.metadata.project, 'ECC 2.0: Control Plane');
|
||||||
|
assert.strictEqual(status.workItems.totalCount, 3);
|
||||||
|
assert.strictEqual(status.workItems.openCount, 2);
|
||||||
|
assert.strictEqual(status.workItems.blockedCount, 1);
|
||||||
|
assert.strictEqual(status.workItems.closedCount, 1);
|
||||||
|
assert.strictEqual(status.readiness.blockedWorkItems, 1);
|
||||||
|
assert.strictEqual(status.readiness.attentionCount, 3);
|
||||||
|
assert.strictEqual(status.workItems.items[0].id, 'github-pr-1738');
|
||||||
} finally {
|
} finally {
|
||||||
cleanupTempDir(testDir);
|
cleanupTempDir(testDir);
|
||||||
}
|
}
|
||||||
@ -608,10 +685,13 @@ async function runTests() {
|
|||||||
assert.match(written, /## Readiness/);
|
assert.match(written, /## Readiness/);
|
||||||
assert.match(written, /Status: attention/);
|
assert.match(written, /Status: attention/);
|
||||||
assert.match(written, /Attention items: 2/);
|
assert.match(written, /Attention items: 2/);
|
||||||
|
assert.match(written, /Blocked work items: 0/);
|
||||||
assert.match(written, /- `session-active` \[claude\/dmux-tmux\] active/);
|
assert.match(written, /- `session-active` \[claude\/dmux-tmux\] active/);
|
||||||
assert.match(written, /Success rate: 66\.7%/);
|
assert.match(written, /Success rate: 66\.7%/);
|
||||||
assert.match(written, /Install health: healthy/);
|
assert.match(written, /Install health: healthy/);
|
||||||
assert.match(written, /Pending governance events: 1/);
|
assert.match(written, /Pending governance events: 1/);
|
||||||
|
assert.match(written, /## Work Items/);
|
||||||
|
assert.match(written, /Open: 0/);
|
||||||
} finally {
|
} finally {
|
||||||
cleanupTempDir(testDir);
|
cleanupTempDir(testDir);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user