From 1efc399ab4840b92c431bb62dc859c6b212f60c9 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 18 Jun 2026 16:59:30 -0400 Subject: [PATCH] feat(control-pane): add agent+human JIT assignment view to the work-items board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kanban board tracked lanes (ready/running/blocked/done) but not WHO owns each card, which is the missing piece for agent+human just-in-time team workflows. - state.js: classifyAssignee() labels each work item agent | human | unassigned (session-linked or agent-pattern owners = agent; named owners = human; ownerless = unassigned), with an explicit metadata.assigneeKind override. - summarizeWorkItems(): adds an assignment summary {agent,human,unassigned} over OPEN cards plus a priority-sorted needsAssignment queue — the JIT pickup list. - ui.js: cards show an [agent]/[human]/[unassigned] badge; the board header shows agent/human split and 'N need owner'. - Tests: assignment classification + JIT queue coverage in control-pane-state. Full suite 2839/2839; lint green. --- scripts/lib/control-pane/state.js | 201 ++--- scripts/lib/control-pane/ui.js | 13 +- tests/lib/control-pane-state.test.js | 1014 ++++++++++++++------------ 3 files changed, 662 insertions(+), 566 deletions(-) diff --git a/scripts/lib/control-pane/state.js b/scripts/lib/control-pane/state.js index c8b313b7..af10fc6a 100644 --- a/scripts/lib/control-pane/state.js +++ b/scripts/lib/control-pane/state.js @@ -26,11 +26,7 @@ function defaultStateDbPath(env = process.env) { function defaultConfigPaths(cwd = process.cwd(), env = process.env) { const home = homeDir(env); - const paths = [ - path.join(home, 'Library', 'Application Support', 'ecc2', 'config.toml'), - path.join(home, '.config', 'ecc2', 'config.toml'), - path.join(home, '.claude', 'ecc2.toml'), - ]; + const paths = [path.join(home, 'Library', 'Application Support', 'ecc2', 'config.toml'), path.join(home, '.config', 'ecc2', 'config.toml'), path.join(home, '.claude', 'ecc2.toml')]; let current = path.resolve(cwd); while (current && current !== path.dirname(current)) { @@ -66,9 +62,7 @@ function normalizeObjectKeys(value) { if (Array.isArray(value)) return value.map(normalizeObjectKeys); if (!isPlainObject(value)) return value; - return Object.fromEntries( - Object.entries(value).map(([key, item]) => [toCamelCase(key), normalizeObjectKeys(item)]) - ); + return Object.fromEntries(Object.entries(value).map(([key, item]) => [toCamelCase(key), normalizeObjectKeys(item)])); } function normalizeMemoryConnectors(connectors = {}) { @@ -80,23 +74,13 @@ function normalizeMemoryConnectors(connectors = {}) { } function normalizeConfig(rawConfig = {}, options = {}) { - const { - memory_connectors: snakeMemoryConnectors, - memoryConnectors, - state_db_path: snakeStateDbPath, - stateDbPath: camelStateDbPath, - ...rest - } = rawConfig; + const { memory_connectors: snakeMemoryConnectors, memoryConnectors, state_db_path: snakeStateDbPath, stateDbPath: camelStateDbPath, ...rest } = rawConfig; const normalized = normalizeObjectKeys(rest); const connectorConfig = memoryConnectors || snakeMemoryConnectors || normalized.memoryConnectors; return { dbPath: options.dbPath || normalized.dbPath || defaultDbPath(options.env), - stateDbPath: options.stateDbPath - || camelStateDbPath - || snakeStateDbPath - || normalized.stateDbPath - || defaultStateDbPath(options.env), - memoryConnectors: normalizeMemoryConnectors(connectorConfig), + stateDbPath: options.stateDbPath || camelStateDbPath || snakeStateDbPath || normalized.stateDbPath || defaultStateDbPath(options.env), + memoryConnectors: normalizeMemoryConnectors(connectorConfig) }; } @@ -108,9 +92,7 @@ function readTomlConfig(configPath) { function resolveControlPaneConfig(options = {}) { const env = options.env || process.env; const cwd = options.cwd || process.cwd(); - const configPaths = options.configPath - ? [path.resolve(options.configPath)] - : defaultConfigPaths(cwd, env); + const configPaths = options.configPath ? [path.resolve(options.configPath)] : defaultConfigPaths(cwd, env); let merged = {}; for (const configPath of configPaths) { @@ -123,9 +105,9 @@ function resolveControlPaneConfig(options = {}) { ...normalizeConfig(merged, { env, dbPath: options.dbPath || env.ECC2_DB_PATH || null, - stateDbPath: options.stateDbPath || env.ECC_STATE_DB_PATH || null, + stateDbPath: options.stateDbPath || env.ECC_STATE_DB_PATH || null }), - configPaths: configPaths.filter(configPath => fs.existsSync(configPath)), + configPaths: configPaths.filter(configPath => fs.existsSync(configPath)) }; } @@ -149,11 +131,7 @@ function execRows(db, sql, params = []) { } function tableExists(db, tableName) { - const rows = execRows( - db, - "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1", - [tableName] - ); + const rows = execRows(db, "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1", [tableName]); return rows.length > 0; } @@ -188,7 +166,7 @@ function normalizeSession(row, unreadMessages) { ? { path: String(row.worktree_path), branch: row.worktree_branch ? String(row.worktree_branch) : null, - base: row.worktree_base ? String(row.worktree_base) : null, + base: row.worktree_base ? String(row.worktree_base) : null } : null, metrics: { @@ -198,23 +176,18 @@ function normalizeSession(row, unreadMessages) { toolCalls: toNumber(row.tool_calls), filesChanged: toNumber(row.files_changed), durationSecs: toNumber(row.duration_secs), - costUsd: toNumber(row.cost_usd), + costUsd: toNumber(row.cost_usd) }, unreadMessages: unreadMessages.get(id) || 0, createdAt: String(row.created_at || ''), updatedAt: String(row.updated_at || ''), - lastHeartbeatAt: String(row.last_heartbeat_at || ''), + lastHeartbeatAt: String(row.last_heartbeat_at || '') }; } function readUnreadMessageCounts(db) { if (!tableExists(db, 'messages')) return new Map(); - return new Map( - execRows( - db, - 'SELECT to_session, COUNT(*) AS unread_count FROM messages WHERE read = 0 GROUP BY to_session' - ).map(row => [String(row.to_session), toNumber(row.unread_count)]) - ); + return new Map(execRows(db, 'SELECT to_session, COUNT(*) AS unread_count FROM messages WHERE read = 0 GROUP BY to_session').map(row => [String(row.to_session), toNumber(row.unread_count)])); } function readSessions(db) { @@ -241,7 +214,7 @@ function summarizeSessions(sessions) { unreadMessages: 0, activeWorktrees: 0, totalTokens: 0, - totalCostUsd: 0, + totalCostUsd: 0 }; for (const session of sessions) { @@ -278,7 +251,7 @@ function readEntities(db) { summary: String(row.summary || ''), metadata: parseJson(row.metadata_json, {}), createdAt: String(row.created_at || ''), - updatedAt: String(row.updated_at || ''), + updatedAt: String(row.updated_at || '') })); } @@ -299,7 +272,7 @@ function readObservations(db) { pinned: toNumber(row.pinned) === 1, summary: String(row.summary || ''), details: parseJson(row.details_json, {}), - createdAt: String(row.created_at || ''), + createdAt: String(row.created_at || '') })); } @@ -341,7 +314,7 @@ function scoreEntity(entity, observations, relationCount, queryTerms) { { text: entity.path || '', weight: 6 }, { text: entity.summary, weight: 8 }, { text: metadataText, weight: 5 }, - { text: observationText, weight: 10 }, + { text: observationText, weight: 10 } ].map(item => ({ ...item, text: item.text.toLowerCase() })); const matchedTerms = []; let score = 0; @@ -357,10 +330,7 @@ function scoreEntity(entity, observations, relationCount, queryTerms) { if (matched) matchedTerms.push(term); } - const maxPriority = observations.reduce( - (highest, observation) => Math.max(highest, observation.priority), - 0 - ); + const maxPriority = observations.reduce((highest, observation) => Math.max(highest, observation.priority), 0); const hasPinnedObservation = observations.some(observation => observation.pinned); score += Math.min(relationCount, 8); score += maxPriority * 3; @@ -372,7 +342,7 @@ function scoreEntity(entity, observations, relationCount, queryTerms) { observationCount: observations.length, relationCount, maxObservationPriority: maxPriority, - hasPinnedObservation, + hasPinnedObservation }; } @@ -388,23 +358,21 @@ function recallKnowledgeEntries({ entities, observations, relationCounts, query, return entities .map(entity => { const entityObservations = observationsByEntity.get(entity.id) || []; - const score = queryTerms.length > 0 - ? scoreEntity(entity, entityObservations, relationCounts.get(entity.id) || 0, queryTerms) - : { - score: entityObservations.some(observation => observation.pinned) ? 10 : 1, - matchedTerms: [], - observationCount: entityObservations.length, - relationCount: relationCounts.get(entity.id) || 0, - maxObservationPriority: entityObservations.reduce( - (highest, observation) => Math.max(highest, observation.priority), - 0 - ), - hasPinnedObservation: entityObservations.some(observation => observation.pinned), - }; + const score = + queryTerms.length > 0 + ? scoreEntity(entity, entityObservations, relationCounts.get(entity.id) || 0, queryTerms) + : { + score: entityObservations.some(observation => observation.pinned) ? 10 : 1, + matchedTerms: [], + observationCount: entityObservations.length, + relationCount: relationCounts.get(entity.id) || 0, + maxObservationPriority: entityObservations.reduce((highest, observation) => Math.max(highest, observation.priority), 0), + hasPinnedObservation: entityObservations.some(observation => observation.pinned) + }; return { entity, ...score, - latestObservation: entityObservations[0] || null, + latestObservation: entityObservations[0] || null }; }) .filter(entry => queryTerms.length === 0 || entry.matchedTerms.length > 0) @@ -431,8 +399,8 @@ function connectorStatus(config, db) { String(row.connector_name), { syncedSources: toNumber(row.synced_sources), - lastSyncedAt: row.last_synced_at ? String(row.last_synced_at) : null, - }, + lastSyncedAt: row.last_synced_at ? String(row.last_synced_at) : null + } ]) ); @@ -449,23 +417,56 @@ function connectorStatus(config, db) { defaultObservationType: connector.defaultObservationType || null, includeSafeValues: Boolean(connector.includeSafeValues), syncedSources: checkpoint.syncedSources, - lastSyncedAt: checkpoint.lastSyncedAt, + lastSyncedAt: checkpoint.lastSyncedAt }; }); } function normalizeWorkItemStatus(status) { - const normalized = String(status || 'open').trim().toLowerCase(); + const normalized = String(status || 'open') + .trim() + .toLowerCase(); if (['done', 'closed', 'resolved', 'merged', 'cancelled'].includes(normalized)) return 'done'; if (['blocked', 'needs-review', 'failed', 'stalled'].includes(normalized)) return 'blocked'; if (['running', 'in-progress', 'active', 'working'].includes(normalized)) return 'running'; return 'ready'; } +// Heuristics for whether a work item's owner is an autonomous agent or a human. +// Agent signals win when present so the board reflects who is *actively* on a card. +const AGENT_OWNER_RE = /(agent|claude|codex|hermes|gemini|opencode|qwen|joycode|codebuddy|\bbot\b|gpt|sonnet|opus|haiku|fable)/i; +const SESSION_ID_RE = /^(sid-|tx-|proj-|sess|session|run-|wt-)/i; + +/** + * Classify the assignment of a work item for agent+human JIT team workflows. + * Returns the assignee kind ('agent' | 'human' | 'unassigned') and the resolved + * assignee label, so the board can show who owns each card and which cards are + * waiting for a just-in-time pickup. + */ +function classifyAssignee({ owner, sessionId, metadata = {} }) { + const explicitKind = String(metadata.assigneeKind || metadata.ownerKind || '') + .trim() + .toLowerCase(); + if (explicitKind === 'agent' || explicitKind === 'human') { + return { assigneeKind: explicitKind, assignee: owner || sessionId || metadata.assignee || null }; + } + const ownerStr = owner ? String(owner) : ''; + if (sessionId || (ownerStr && (AGENT_OWNER_RE.test(ownerStr) || SESSION_ID_RE.test(ownerStr)))) { + return { assigneeKind: 'agent', assignee: ownerStr || String(sessionId) }; + } + if (ownerStr) { + return { assigneeKind: 'human', assignee: ownerStr }; + } + return { assigneeKind: 'unassigned', assignee: null }; +} + function normalizeWorkItem(row) { const parsedMetadata = parseJson(row.metadata, {}); const metadata = isPlainObject(parsedMetadata) ? normalizeObjectKeys(parsedMetadata) : {}; const kanbanState = normalizeWorkItemStatus(row.status); + const owner = row.owner ? String(row.owner) : null; + const sessionId = row.session_id ? String(row.session_id) : null; + const { assigneeKind, assignee } = classifyAssignee({ owner, sessionId, metadata }); return { id: String(row.id || ''), source: String(row.source || ''), @@ -475,16 +476,18 @@ function normalizeWorkItem(row) { kanbanState, priority: row.priority ? String(row.priority) : null, url: row.url ? String(row.url) : null, - owner: row.owner ? String(row.owner) : null, + owner, + assigneeKind, + assignee, repoRoot: row.repo_root ? String(row.repo_root) : null, - sessionId: row.session_id ? String(row.session_id) : null, + sessionId, branch: metadata.branch || metadata.headRefName || null, mergeGate: metadata.mergeGate || metadata.mergeGateStatus || metadata.mergeStateStatus || null, blocker: metadata.blocker || null, acceptance: Array.isArray(metadata.acceptance) ? metadata.acceptance.map(String) : [], metadata, createdAt: String(row.created_at || ''), - updatedAt: String(row.updated_at || ''), + updatedAt: String(row.updated_at || '') }; } @@ -509,22 +512,54 @@ function summarizeWorkItems(items) { ready: 0, running: 0, blocked: 0, - done: 0, + done: 0 }, - items, + // Agent + human JIT team-workflow view: who owns the open work, and which + // open cards are waiting for a just-in-time pickup. + assignment: { + agent: 0, + human: 0, + unassigned: 0 + }, + needsAssignment: [], + items }; for (const item of items) { const kanbanState = normalizeWorkItemStatus(item.kanbanState || item.status); summary.kanban[kanbanState] += 1; + const isOpen = kanbanState !== 'done'; if (kanbanState === 'done') { summary.doneCount += 1; } else { summary.openCount += 1; } if (kanbanState === 'blocked') summary.blockedCount += 1; + + // Assignment is only meaningful for open work; done cards don't need an owner. + if (isOpen) { + const kind = item.assigneeKind || classifyAssignee(item).assigneeKind; + summary.assignment[kind] = (summary.assignment[kind] || 0) + 1; + if (kind === 'unassigned') { + summary.needsAssignment.push({ + id: item.id, + title: item.title, + kanbanState, + priority: item.priority || null, + url: item.url || null + }); + } + } } + // Surface the highest-priority unclaimed work first for JIT pickup. + const priorityRank = { critical: 0, high: 1, urgent: 1, medium: 2, normal: 2, low: 3 }; + summary.needsAssignment.sort((a, b) => { + const ra = priorityRank[String(a.priority || '').toLowerCase()] ?? 2; + const rb = priorityRank[String(b.priority || '').toLowerCase()] ?? 2; + return ra - rb; + }); + return summary; } @@ -547,7 +582,7 @@ async function buildControlPaneSnapshot(options = {}) { ? normalizeConfig(options.config, { env: options.env || process.env, dbPath: options.dbPath || options.config.dbPath || null, - stateDbPath: options.stateDbPath || options.config.stateDbPath || null, + stateDbPath: options.stateDbPath || options.config.stateDbPath || null }) : resolveControlPaneConfig(options); const dbPath = options.dbPath || config.dbPath; @@ -563,17 +598,17 @@ async function buildControlPaneSnapshot(options = {}) { dbPath, stateDbPath, database: { - exists: Boolean(dbPath && fs.existsSync(dbPath)), + exists: Boolean(dbPath && fs.existsSync(dbPath)) }, stateDatabase: { - exists: Boolean(stateDbPath && fs.existsSync(stateDbPath)), + exists: Boolean(stateDbPath && fs.existsSync(stateDbPath)) }, config: { configPaths: config.configPaths || [], - memoryConnectorCount: Object.keys(config.memoryConnectors || {}).length, + memoryConnectorCount: Object.keys(config.memoryConnectors || {}).length }, execution: { - allowActions: options.allowActions !== false, + allowActions: options.allowActions !== false }, summary: summarizeSessions([]), sessions: [], @@ -581,11 +616,11 @@ async function buildControlPaneSnapshot(options = {}) { query, entityCount: 0, observationCount: 0, - results: [], + results: [] }, connectors: connectorStatus(config, null), workItems, - actions: buildControlPaneActions({ repoRoot, query, limit }), + actions: buildControlPaneActions({ repoRoot, query, limit }) }; const db = await openSqlDatabase(dbPath); @@ -611,10 +646,10 @@ async function buildControlPaneSnapshot(options = {}) { observations, relationCounts, query, - limit, - }), + limit + }) }, - connectors: connectorStatus(config, db), + connectors: connectorStatus(config, db) }; } finally { db.close(); @@ -627,5 +662,5 @@ module.exports = { defaultConfigPaths, defaultStateDbPath, recallKnowledgeEntries, - resolveControlPaneConfig, + resolveControlPaneConfig }; diff --git a/scripts/lib/control-pane/ui.js b/scripts/lib/control-pane/ui.js index fee3eb5b..337416ad 100644 --- a/scripts/lib/control-pane/ui.js +++ b/scripts/lib/control-pane/ui.js @@ -497,7 +497,11 @@ function renderControlPaneHtml() { const summary = workItems || { totalCount: 0, openCount: 0, blockedCount: 0, doneCount: 0, kanban: {}, items: [] }; const items = Array.isArray(summary.items) ? summary.items : []; const kanban = summary.kanban || {}; - $('#work-item-count').textContent = summary.openCount + ' open / ' + summary.blockedCount + ' blocked'; + const needsAssignment = Array.isArray(summary.needsAssignment) ? summary.needsAssignment : []; + const assignment = summary.assignment || { agent: 0, human: 0, unassigned: 0 }; + $('#work-item-count').textContent = summary.openCount + ' open / ' + summary.blockedCount + ' blocked' + + ' / ' + (assignment.agent || 0) + ' agent / ' + (assignment.human || 0) + ' human' + + (needsAssignment.length ? ' / ' + needsAssignment.length + ' need owner' : ''); const lanes = ['ready', 'running', 'blocked', 'done']; const laneHtml = '
' + lanes.map(lane => @@ -513,10 +517,11 @@ function renderControlPaneHtml() { const branch = item.branch || (item.metadata && item.metadata.branch) || ''; const mergeGate = item.mergeGate || (item.metadata && item.metadata.mergeGate) || ''; const blocker = item.blocker || (item.metadata && item.metadata.blocker) || ''; - const owner = item.owner || item.source || 'unassigned'; + const assigneeKind = item.assigneeKind || 'unassigned'; + const owner = item.assignee || item.owner || (assigneeKind === 'unassigned' ? 'unassigned (JIT)' : item.source) || 'unassigned'; return '
' + '
' + escapeHtml(item.title || item.id) + '' + statePill(item.kanbanState || item.status) + '
' + - '
' + escapeHtml(owner) + ' - ' + escapeHtml(item.source || 'manual') + (item.priority ? ' - ' + escapeHtml(item.priority) : '') + '
' + + '
[' + escapeHtml(assigneeKind) + '] ' + escapeHtml(owner) + ' - ' + escapeHtml(item.source || 'manual') + (item.priority ? ' - ' + escapeHtml(item.priority) : '') + '
' + (branch ? '
branch: ' + escapeHtml(branch) + '
' : '') + (mergeGate ? '
merge gate: ' + escapeHtml(mergeGate) + '
' : '') + (blocker ? '
blocker: ' + escapeHtml(blocker) + '
' : '') + @@ -629,5 +634,5 @@ function renderControlPaneHtml() { } module.exports = { - renderControlPaneHtml, + renderControlPaneHtml }; diff --git a/tests/lib/control-pane-state.test.js b/tests/lib/control-pane-state.test.js index 642b34fa..df86b7ba 100644 --- a/tests/lib/control-pane-state.test.js +++ b/tests/lib/control-pane-state.test.js @@ -9,11 +9,7 @@ const path = require('path'); const initSqlJs = require('sql.js'); -const { - buildControlPaneSnapshot, - recallKnowledgeEntries, - resolveControlPaneConfig, -} = require('../../scripts/lib/control-pane/state'); +const { buildControlPaneSnapshot, recallKnowledgeEntries, resolveControlPaneConfig } = require('../../scripts/lib/control-pane/state'); async function test(name, fn) { try { @@ -138,7 +134,7 @@ async function writeSampleEcc2Database(dbPath) { 0.42, '2026-06-03T10:00:00Z', '2026-06-03T10:15:00Z', - '2026-06-03T10:15:00Z', + '2026-06-03T10:15:00Z' ]); insertSession.run([ 'worker-kb', @@ -163,14 +159,18 @@ async function writeSampleEcc2Database(dbPath) { 0.07, '2026-06-03T10:05:00Z', '2026-06-03T10:14:00Z', - '2026-06-03T10:14:00Z', + '2026-06-03T10:14:00Z' ]); insertSession.free(); - db.run( - 'INSERT INTO messages (from_session, to_session, content, msg_type, read, timestamp) VALUES (?, ?, ?, ?, ?, ?)', - ['worker-kb', 'lead-hermes', 'Need approval for connector sync', 'approval_request', 0, '2026-06-03T10:16:00Z'] - ); + db.run('INSERT INTO messages (from_session, to_session, content, msg_type, read, timestamp) VALUES (?, ?, ?, ?, ?, ?)', [ + 'worker-kb', + 'lead-hermes', + 'Need approval for connector sync', + 'approval_request', + 0, + '2026-06-03T10:16:00Z' + ]); const insertEntity = db.prepare(` INSERT INTO context_graph_entities ( @@ -186,7 +186,7 @@ async function writeSampleEcc2Database(dbPath) { 'How Affaan routes Hermes Desktop, Zellij panes, Devin-style delegation, and ECC release control work.', JSON.stringify({ source: 'hermes_workspace', platform: 'desktop' }), '2026-06-03T10:10:00Z', - '2026-06-03T10:10:00Z', + '2026-06-03T10:10:00Z' ]); insertEntity.run([ null, @@ -197,31 +197,34 @@ async function writeSampleEcc2Database(dbPath) { 'Operator knowledge base pattern for cross-platform agent memory.', JSON.stringify({ source: 'workspace_notes' }), '2026-06-03T10:11:00Z', - '2026-06-03T10:11:00Z', + '2026-06-03T10:11:00Z' ]); insertEntity.free(); - db.run( - 'INSERT INTO context_graph_observations (session_id, entity_id, observation_type, priority, pinned, summary, details_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', - [ - 'lead-hermes', - 1, - 'operator_memory', - 3, - 1, - 'Hermes Desktop and ECC should share recall before dispatching work.', - JSON.stringify({ note: 'safe public summary only' }), - '2026-06-03T10:12:00Z', - ] - ); - db.run( - 'INSERT INTO context_graph_relations (session_id, from_entity_id, to_entity_id, relation_type, summary, created_at) VALUES (?, ?, ?, ?, ?, ?)', - ['lead-hermes', 1, 2, 'depends_on', 'Runbook uses durable memory concepts.', '2026-06-03T10:13:00Z'] - ); - db.run( - 'INSERT INTO context_graph_connector_checkpoints (connector_name, source_path, source_signature, updated_at) VALUES (?, ?, ?, ?)', - ['hermes_workspace', '/notes/hermes.md', 'sig-1', '2026-06-03T10:12:00Z'] - ); + db.run('INSERT INTO context_graph_observations (session_id, entity_id, observation_type, priority, pinned, summary, details_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [ + 'lead-hermes', + 1, + 'operator_memory', + 3, + 1, + 'Hermes Desktop and ECC should share recall before dispatching work.', + JSON.stringify({ note: 'safe public summary only' }), + '2026-06-03T10:12:00Z' + ]); + db.run('INSERT INTO context_graph_relations (session_id, from_entity_id, to_entity_id, relation_type, summary, created_at) VALUES (?, ?, ?, ?, ?, ?)', [ + 'lead-hermes', + 1, + 2, + 'depends_on', + 'Runbook uses durable memory concepts.', + '2026-06-03T10:13:00Z' + ]); + db.run('INSERT INTO context_graph_connector_checkpoints (connector_name, source_path, source_signature, updated_at) VALUES (?, ?, ?, ?)', [ + 'hermes_workspace', + '/notes/hermes.md', + 'sig-1', + '2026-06-03T10:12:00Z' + ]); fs.writeFileSync(dbPath, Buffer.from(db.export())); db.close(); @@ -270,10 +273,10 @@ async function writeSampleWorkItemsDatabase(dbPath) { JSON.stringify({ branch: 'product/dynamic-workflow-team-orchestration', mergeGate: 'focused tests and catalog check pass', - acceptance: ['skill exists', 'content pack exists'], + acceptance: ['skill exists', 'content pack exists'] }), '2026-06-04T09:00:00Z', - '2026-06-04T09:05:00Z', + '2026-06-04T09:05:00Z' ]); insertWorkItem.run([ 'agent-card-002', @@ -288,10 +291,10 @@ async function writeSampleWorkItemsDatabase(dbPath) { null, JSON.stringify({ branch: 'product/ecc2-knowledge-control-pane', - mergeStateStatus: 'CLEAN', + mergeStateStatus: 'CLEAN' }), '2026-06-03T13:00:00Z', - '2026-06-03T13:55:00Z', + '2026-06-03T13:55:00Z' ]); insertWorkItem.run([ 'agent-card-003', @@ -306,10 +309,25 @@ async function writeSampleWorkItemsDatabase(dbPath) { null, JSON.stringify({ blocker: 'needs publish approval', - mergeGate: 'approval packet accepted', + mergeGate: 'approval packet accepted' }), '2026-06-04T09:10:00Z', - '2026-06-04T09:12:00Z', + '2026-06-04T09:12:00Z' + ]); + insertWorkItem.run([ + 'agent-card-004', + 'github-issue', + '2290', + 'Triage 400k context window bug', + 'open', + 'high', + 'https://github.com/affaan-m/ECC/issues/2290', + null, + '/repo/ecc', + null, + JSON.stringify({}), + '2026-06-04T09:20:00Z', + '2026-06-04T09:22:00Z' ]); insertWorkItem.free(); @@ -335,293 +353,324 @@ async function runTests() { let passed = 0; let failed = 0; - if (await test('builds an operator snapshot from ECC2 SQLite and configured connectors', async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-state-')); - const dbPath = path.join(tempDir, 'ecc2.db'); + if ( + await test('builds an operator snapshot from ECC2 SQLite and configured connectors', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-state-')); + const dbPath = path.join(tempDir, 'ecc2.db'); - try { - await writeSampleEcc2Database(dbPath); - const snapshot = await buildControlPaneSnapshot({ - dbPath, - repoRoot: path.join(__dirname, '..', '..'), - query: 'Hermes Desktop Zellij gbrain', - config: { - memoryConnectors: { - hermes_workspace: { - kind: 'markdown_directory', - path: '/notes', - recurse: true, - }, - safe_env: { - kind: 'dotenv_file', - path: '/notes/.env', - includeSafeValues: false, - }, + try { + await writeSampleEcc2Database(dbPath); + const snapshot = await buildControlPaneSnapshot({ + dbPath, + repoRoot: path.join(__dirname, '..', '..'), + query: 'Hermes Desktop Zellij gbrain', + config: { + memoryConnectors: { + hermes_workspace: { + kind: 'markdown_directory', + path: '/notes', + recurse: true + }, + safe_env: { + kind: 'dotenv_file', + path: '/notes/.env', + includeSafeValues: false + } + } + } + }); + + assert.strictEqual(snapshot.schemaVersion, 'ecc.control-pane.snapshot.v1'); + assert.strictEqual(snapshot.summary.totalSessions, 2); + assert.strictEqual(snapshot.summary.runningSessions, 1); + assert.strictEqual(snapshot.summary.unreadMessages, 1); + assert.strictEqual(snapshot.sessions[0].id, 'lead-hermes'); + assert.deepStrictEqual(snapshot.sessions[0].detectedHarnesses, ['claude', 'codex']); + assert.strictEqual(snapshot.knowledge.query, 'Hermes Desktop Zellij gbrain'); + assert.strictEqual(snapshot.knowledge.results[0].entity.name, 'Hermes revenue runbook'); + assert.ok(snapshot.knowledge.results[0].matchedTerms.includes('hermes')); + assert.strictEqual(snapshot.knowledge.results[0].hasPinnedObservation, true); + assert.strictEqual(snapshot.connectors.length, 2); + assert.strictEqual(snapshot.connectors[0].name, 'hermes_workspace'); + assert.strictEqual(snapshot.connectors[0].syncedSources, 1); + assert.strictEqual(snapshot.connectors[1].syncedSources, 0); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + + if ( + await test('projects state-store work items into agent Kanban summary', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-work-items-')); + const dbPath = path.join(tempDir, 'ecc2.db'); + const stateDbPath = path.join(tempDir, 'state.db'); + + try { + await writeSampleEcc2Database(dbPath); + await writeSampleWorkItemsDatabase(stateDbPath); + + const snapshot = await buildControlPaneSnapshot({ + dbPath, + stateDbPath, + repoRoot: path.join(__dirname, '..', '..'), + query: 'workflow' + }); + + const byId = id => snapshot.workItems.items.find(item => item.id === id); + assert.strictEqual(snapshot.workItems.totalCount, 4); + assert.strictEqual(snapshot.workItems.openCount, 3); + assert.strictEqual(snapshot.workItems.blockedCount, 1); + assert.strictEqual(snapshot.workItems.doneCount, 1); + assert.strictEqual(snapshot.workItems.kanban.running, 1); + assert.strictEqual(snapshot.workItems.kanban.blocked, 1); + assert.strictEqual(snapshot.workItems.kanban.ready, 1); + assert.strictEqual(byId('agent-card-003').mergeGate, 'approval packet accepted'); + assert.strictEqual(byId('agent-card-001').branch, 'product/dynamic-workflow-team-orchestration'); + assert.strictEqual(byId('agent-card-002').mergeGate, 'CLEAN'); + + // Agent + human JIT assignment view. + assert.strictEqual(byId('agent-card-001').assigneeKind, 'agent', 'session-linked card is agent-owned'); + assert.strictEqual(byId('agent-card-003').assigneeKind, 'human', 'operator-owned card is human-owned'); + assert.strictEqual(byId('agent-card-004').assigneeKind, 'unassigned', 'ownerless open card is unassigned'); + assert.deepStrictEqual(snapshot.workItems.assignment, { agent: 1, human: 1, unassigned: 1 }); + assert.strictEqual(snapshot.workItems.needsAssignment.length, 1); + assert.strictEqual(snapshot.workItems.needsAssignment[0].id, 'agent-card-004'); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + + if ( + await test('treats an unreadable optional state-store database as empty work items', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-corrupt-work-items-')); + const dbPath = path.join(tempDir, 'ecc2.db'); + const stateDbPath = path.join(tempDir, 'corrupt-state.db'); + + try { + await writeSampleEcc2Database(dbPath); + fs.writeFileSync(stateDbPath, 'not a sqlite database', 'utf8'); + + const snapshot = await buildControlPaneSnapshot({ + dbPath, + stateDbPath, + repoRoot: path.join(__dirname, '..', '..'), + query: 'workflow' + }); + + assert.strictEqual(snapshot.stateDatabase.exists, true); + assert.strictEqual(snapshot.workItems.totalCount, 0); + assert.strictEqual(snapshot.summary.totalSessions, 2); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + + if ( + await test('resolves config from explicit db path and TOML connector file', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-config-')); + const dbPath = path.join(tempDir, 'state.db'); + const configPath = path.join(tempDir, 'ecc2.toml'); + + try { + fs.writeFileSync( + configPath, + [ + `db_path = "${dbPath.replace(/\\/g, '\\\\')}"`, + '', + '[memory_connectors.hermes_workspace]', + 'kind = "markdown_directory"', + 'path = "/tmp/hermes"', + 'recurse = true', + 'default_entity_type = "operator_note"' + ].join('\n'), + 'utf8' + ); + + const config = resolveControlPaneConfig({ + cwd: tempDir, + configPath + }); + + assert.strictEqual(config.dbPath, dbPath); + assert.strictEqual(config.memoryConnectors.hermes_workspace.kind, 'markdown_directory'); + assert.strictEqual(config.memoryConnectors.hermes_workspace.path, '/tmp/hermes'); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + + if ( + await test('prefers the operator home config over stale app-support config', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-precedence-')); + const homeDir = path.join(tempDir, 'home'); + const homeConfigDir = path.join(homeDir, '.claude'); + const appConfigDir = path.join(homeDir, 'Library', 'Application Support', 'ecc2'); + const homeDbPath = path.join(tempDir, 'operator.db'); + const staleDbPath = path.join(tempDir, 'stale-smoke.db'); + + try { + fs.mkdirSync(homeConfigDir, { recursive: true }); + fs.mkdirSync(appConfigDir, { recursive: true }); + fs.writeFileSync(path.join(appConfigDir, 'config.toml'), `db_path = "${staleDbPath.replace(/\\/g, '\\\\')}"\n`, 'utf8'); + fs.writeFileSync(path.join(homeConfigDir, 'ecc2.toml'), `db_path = "${homeDbPath.replace(/\\/g, '\\\\')}"\n`, 'utf8'); + + const config = resolveControlPaneConfig({ + cwd: tempDir, + env: { HOME: homeDir } + }); + + assert.strictEqual(config.dbPath, homeDbPath); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + + if ( + await test('shows configured connectors even when the SQLite database is missing', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-missing-db-')); + + try { + const snapshot = await buildControlPaneSnapshot({ + repoRoot: path.join(__dirname, '..', '..'), + dbPath: path.join(tempDir, 'missing.db'), + config: { + memoryConnectors: { + hermes_workspace: { + kind: 'markdown_directory', + path: '/notes/hermes', + recurse: true + } + } + } + }); + + assert.strictEqual(snapshot.database.exists, false); + assert.strictEqual(snapshot.connectors.length, 1); + assert.strictEqual(snapshot.connectors[0].name, 'hermes_workspace'); + assert.strictEqual(snapshot.connectors[0].syncedSources, 0); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + + if ( + await test('handles an existing SQLite database before ECC2 tables are created', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-empty-db-')); + const dbPath = path.join(tempDir, 'empty.db'); + + try { + const SQL = await initSqlJs(); + const db = new SQL.Database(); + fs.writeFileSync(dbPath, Buffer.from(db.export())); + db.close(); + + const snapshot = await buildControlPaneSnapshot({ + repoRoot: path.join(__dirname, '..', '..'), + dbPath, + config: { + memoryConnectors: { + workspace_notes: { + kind: 'markdown_directory', + path: '/notes', + includeSafeValues: false + } + } + } + }); + + assert.strictEqual(snapshot.database.exists, true); + assert.strictEqual(snapshot.summary.totalSessions, 0); + assert.strictEqual(snapshot.knowledge.entityCount, 0); + assert.strictEqual(snapshot.knowledge.observationCount, 0); + assert.strictEqual(snapshot.connectors[0].name, 'workspace_notes'); + assert.strictEqual(snapshot.connectors[0].lastSyncedAt, null); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + + if ( + await test('recalls pinned knowledge when no query is provided', async () => { + const results = recallKnowledgeEntries({ + entities: [ + { + id: 1, + entityType: 'runbook', + name: 'Pinned runbook', + path: '/notes/pinned.md', + summary: 'Pinned operator context', + metadata: {}, + updatedAt: '2026-06-03T10:00:00Z' }, - }, - }); - - assert.strictEqual(snapshot.schemaVersion, 'ecc.control-pane.snapshot.v1'); - assert.strictEqual(snapshot.summary.totalSessions, 2); - assert.strictEqual(snapshot.summary.runningSessions, 1); - assert.strictEqual(snapshot.summary.unreadMessages, 1); - assert.strictEqual(snapshot.sessions[0].id, 'lead-hermes'); - assert.deepStrictEqual(snapshot.sessions[0].detectedHarnesses, ['claude', 'codex']); - assert.strictEqual(snapshot.knowledge.query, 'Hermes Desktop Zellij gbrain'); - assert.strictEqual(snapshot.knowledge.results[0].entity.name, 'Hermes revenue runbook'); - assert.ok(snapshot.knowledge.results[0].matchedTerms.includes('hermes')); - assert.strictEqual(snapshot.knowledge.results[0].hasPinnedObservation, true); - assert.strictEqual(snapshot.connectors.length, 2); - assert.strictEqual(snapshot.connectors[0].name, 'hermes_workspace'); - assert.strictEqual(snapshot.connectors[0].syncedSources, 1); - assert.strictEqual(snapshot.connectors[1].syncedSources, 0); - } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - })) passed++; else failed++; - - if (await test('projects state-store work items into agent Kanban summary', async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-work-items-')); - const dbPath = path.join(tempDir, 'ecc2.db'); - const stateDbPath = path.join(tempDir, 'state.db'); - - try { - await writeSampleEcc2Database(dbPath); - await writeSampleWorkItemsDatabase(stateDbPath); - - const snapshot = await buildControlPaneSnapshot({ - dbPath, - stateDbPath, - repoRoot: path.join(__dirname, '..', '..'), - query: 'workflow', - }); - - assert.strictEqual(snapshot.workItems.totalCount, 3); - assert.strictEqual(snapshot.workItems.openCount, 2); - assert.strictEqual(snapshot.workItems.blockedCount, 1); - assert.strictEqual(snapshot.workItems.doneCount, 1); - assert.strictEqual(snapshot.workItems.kanban.running, 1); - assert.strictEqual(snapshot.workItems.kanban.blocked, 1); - assert.strictEqual(snapshot.workItems.items[0].id, 'agent-card-003'); - assert.strictEqual(snapshot.workItems.items[0].mergeGate, 'approval packet accepted'); - assert.strictEqual(snapshot.workItems.items[1].branch, 'product/dynamic-workflow-team-orchestration'); - assert.strictEqual( - snapshot.workItems.items.find(item => item.id === 'agent-card-002').mergeGate, - 'CLEAN' - ); - } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - })) passed++; else failed++; - - if (await test('treats an unreadable optional state-store database as empty work items', async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-corrupt-work-items-')); - const dbPath = path.join(tempDir, 'ecc2.db'); - const stateDbPath = path.join(tempDir, 'corrupt-state.db'); - - try { - await writeSampleEcc2Database(dbPath); - fs.writeFileSync(stateDbPath, 'not a sqlite database', 'utf8'); - - const snapshot = await buildControlPaneSnapshot({ - dbPath, - stateDbPath, - repoRoot: path.join(__dirname, '..', '..'), - query: 'workflow', - }); - - assert.strictEqual(snapshot.stateDatabase.exists, true); - assert.strictEqual(snapshot.workItems.totalCount, 0); - assert.strictEqual(snapshot.summary.totalSessions, 2); - } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - })) passed++; else failed++; - - if (await test('resolves config from explicit db path and TOML connector file', async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-config-')); - const dbPath = path.join(tempDir, 'state.db'); - const configPath = path.join(tempDir, 'ecc2.toml'); - - try { - fs.writeFileSync( - configPath, - [ - `db_path = "${dbPath.replace(/\\/g, '\\\\')}"`, - '', - '[memory_connectors.hermes_workspace]', - 'kind = "markdown_directory"', - 'path = "/tmp/hermes"', - 'recurse = true', - 'default_entity_type = "operator_note"', - ].join('\n'), - 'utf8' - ); - - const config = resolveControlPaneConfig({ - cwd: tempDir, - configPath, - }); - - assert.strictEqual(config.dbPath, dbPath); - assert.strictEqual(config.memoryConnectors.hermes_workspace.kind, 'markdown_directory'); - assert.strictEqual(config.memoryConnectors.hermes_workspace.path, '/tmp/hermes'); - } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - })) passed++; else failed++; - - if (await test('prefers the operator home config over stale app-support config', async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-precedence-')); - const homeDir = path.join(tempDir, 'home'); - const homeConfigDir = path.join(homeDir, '.claude'); - const appConfigDir = path.join(homeDir, 'Library', 'Application Support', 'ecc2'); - const homeDbPath = path.join(tempDir, 'operator.db'); - const staleDbPath = path.join(tempDir, 'stale-smoke.db'); - - try { - fs.mkdirSync(homeConfigDir, { recursive: true }); - fs.mkdirSync(appConfigDir, { recursive: true }); - fs.writeFileSync( - path.join(appConfigDir, 'config.toml'), - `db_path = "${staleDbPath.replace(/\\/g, '\\\\')}"\n`, - 'utf8' - ); - fs.writeFileSync( - path.join(homeConfigDir, 'ecc2.toml'), - `db_path = "${homeDbPath.replace(/\\/g, '\\\\')}"\n`, - 'utf8' - ); - - const config = resolveControlPaneConfig({ - cwd: tempDir, - env: { HOME: homeDir }, - }); - - assert.strictEqual(config.dbPath, homeDbPath); - } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - })) passed++; else failed++; - - if (await test('shows configured connectors even when the SQLite database is missing', async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-missing-db-')); - - try { - const snapshot = await buildControlPaneSnapshot({ - repoRoot: path.join(__dirname, '..', '..'), - dbPath: path.join(tempDir, 'missing.db'), - config: { - memoryConnectors: { - hermes_workspace: { - kind: 'markdown_directory', - path: '/notes/hermes', - recurse: true, - }, + { + id: 2, + entityType: 'concept', + name: 'Unpinned concept', + path: null, + summary: 'Secondary context', + metadata: {}, + updatedAt: '2026-06-03T11:00:00Z' + } + ], + observations: [ + { + entityId: 1, + priority: 4, + pinned: true, + summary: 'Pinned detail' }, - }, + { + entityId: 2, + priority: 2, + pinned: false, + summary: 'Other detail' + } + ], + relationCounts: new Map([[1, 3]]), + query: '', + limit: 0 }); - assert.strictEqual(snapshot.database.exists, false); - assert.strictEqual(snapshot.connectors.length, 1); - assert.strictEqual(snapshot.connectors[0].name, 'hermes_workspace'); - assert.strictEqual(snapshot.connectors[0].syncedSources, 0); - } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - })) passed++; else failed++; + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].entity.name, 'Pinned runbook'); + assert.strictEqual(results[0].hasPinnedObservation, true); + assert.strictEqual(results[0].relationCount, 3); + assert.strictEqual(results[1].entity.name, 'Unpinned concept'); + }) + ) + passed++; + else failed++; - if (await test('handles an existing SQLite database before ECC2 tables are created', async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-empty-db-')); - const dbPath = path.join(tempDir, 'empty.db'); + if ( + await test('handles malformed JSON rows and all session state counters', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-edge-db-')); + const dbPath = path.join(tempDir, 'ecc2.db'); - try { - const SQL = await initSqlJs(); - const db = new SQL.Database(); - fs.writeFileSync(dbPath, Buffer.from(db.export())); - db.close(); - - const snapshot = await buildControlPaneSnapshot({ - repoRoot: path.join(__dirname, '..', '..'), - dbPath, - config: { - memoryConnectors: { - workspace_notes: { - kind: 'markdown_directory', - path: '/notes', - includeSafeValues: false, - }, - }, - }, - }); - - assert.strictEqual(snapshot.database.exists, true); - assert.strictEqual(snapshot.summary.totalSessions, 0); - assert.strictEqual(snapshot.knowledge.entityCount, 0); - assert.strictEqual(snapshot.knowledge.observationCount, 0); - assert.strictEqual(snapshot.connectors[0].name, 'workspace_notes'); - assert.strictEqual(snapshot.connectors[0].lastSyncedAt, null); - } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - })) passed++; else failed++; - - if (await test('recalls pinned knowledge when no query is provided', async () => { - const results = recallKnowledgeEntries({ - entities: [ - { - id: 1, - entityType: 'runbook', - name: 'Pinned runbook', - path: '/notes/pinned.md', - summary: 'Pinned operator context', - metadata: {}, - updatedAt: '2026-06-03T10:00:00Z', - }, - { - id: 2, - entityType: 'concept', - name: 'Unpinned concept', - path: null, - summary: 'Secondary context', - metadata: {}, - updatedAt: '2026-06-03T11:00:00Z', - }, - ], - observations: [ - { - entityId: 1, - priority: 4, - pinned: true, - summary: 'Pinned detail', - }, - { - entityId: 2, - priority: 2, - pinned: false, - summary: 'Other detail', - }, - ], - relationCounts: new Map([[1, 3]]), - query: '', - limit: 0, - }); - - assert.strictEqual(results.length, 2); - assert.strictEqual(results[0].entity.name, 'Pinned runbook'); - assert.strictEqual(results[0].hasPinnedObservation, true); - assert.strictEqual(results[0].relationCount, 3); - assert.strictEqual(results[1].entity.name, 'Unpinned concept'); - })) passed++; else failed++; - - if (await test('handles malformed JSON rows and all session state counters', async () => { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-edge-db-')); - const dbPath = path.join(tempDir, 'ecc2.db'); - - try { - await writeSampleEcc2Database(dbPath); - await mutateSqlDatabase(dbPath, db => { - const insertSession = db.prepare(` + try { + await writeSampleEcc2Database(dbPath); + await mutateSqlDatabase(dbPath, db => { + const insertSession = db.prepare(` INSERT INTO sessions ( id, task, project, task_group, agent_type, harness, detected_harnesses_json, working_dir, state, pid, worktree_path, worktree_branch, worktree_base, @@ -629,54 +678,52 @@ async function runTests() { duration_secs, cost_usd, created_at, updated_at, last_heartbeat_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); - for (const state of ['pending', 'failed', 'stopped', 'completed']) { - insertSession.run([ - `session-${state}`, - `Exercise ${state}`, - 'ECC', - 'coverage', - 'codex', - '', - state === 'failed' ? '{bad json' : '[]', - '', - state, - state === 'pending' ? 'not-a-pid' : null, - state === 'completed' ? '/tmp/worktree' : null, - null, - null, - 'not-input-tokens', - null, - state === 'pending' ? 'not-tokens' : 10, - null, - null, - null, - state === 'failed' ? 'not-cost' : 0.1, - '2026-06-03T11:00:00Z', - `2026-06-03T11:0${state.length % 10}:00Z`, - '', - ]); - } - insertSession.free(); + for (const state of ['pending', 'failed', 'stopped', 'completed']) { + insertSession.run([ + `session-${state}`, + `Exercise ${state}`, + 'ECC', + 'coverage', + 'codex', + '', + state === 'failed' ? '{bad json' : '[]', + '', + state, + state === 'pending' ? 'not-a-pid' : null, + state === 'completed' ? '/tmp/worktree' : null, + null, + null, + 'not-input-tokens', + null, + state === 'pending' ? 'not-tokens' : 10, + null, + null, + null, + state === 'failed' ? 'not-cost' : 0.1, + '2026-06-03T11:00:00Z', + `2026-06-03T11:0${state.length % 10}:00Z`, + '' + ]); + } + insertSession.free(); - db.run( - `INSERT INTO context_graph_entities ( + db.run( + `INSERT INTO context_graph_entities ( session_id, entity_key, entity_type, name, path, summary, metadata_json, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [ - 'session-failed', - 'bad:json', - 'note', - 'Malformed JSON knowledge', - '/notes/malformed.md', - 'This record should still be searchable.', - '{bad json', - '2026-06-03T11:20:00Z', - '2026-06-03T11:20:00Z', - ] - ); - db.run( - 'INSERT INTO context_graph_observations (session_id, entity_id, observation_type, priority, pinned, summary, details_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', - [ + [ + 'session-failed', + 'bad:json', + 'note', + 'Malformed JSON knowledge', + '/notes/malformed.md', + 'This record should still be searchable.', + '{bad json', + '2026-06-03T11:20:00Z', + '2026-06-03T11:20:00Z' + ] + ); + db.run('INSERT INTO context_graph_observations (session_id, entity_id, observation_type, priority, pinned, summary, details_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [ 'session-failed', 3, '', @@ -684,125 +731,134 @@ async function runTests() { 0, 'Malformed details should fall back safely.', '{bad json', - '2026-06-03T11:21:00Z', - ] - ); - }); + '2026-06-03T11:21:00Z' + ]); + }); - const snapshot = await buildControlPaneSnapshot({ - repoRoot: path.join(__dirname, '..', '..'), - dbPath, - query: 'Malformed', - config: { - memoryConnectors: { - malformed_notes: { - kind: 'markdown_directory', - path: '/notes/malformed', - recurse: false, - defaultEntityType: 'note', - defaultObservationType: 'operator_memory', - includeSafeValues: true, - }, - }, + const snapshot = await buildControlPaneSnapshot({ + repoRoot: path.join(__dirname, '..', '..'), + dbPath, + query: 'Malformed', + config: { + memoryConnectors: { + malformed_notes: { + kind: 'markdown_directory', + path: '/notes/malformed', + recurse: false, + defaultEntityType: 'note', + defaultObservationType: 'operator_memory', + includeSafeValues: true + } + } + } + }); + + assert.strictEqual(snapshot.summary.pendingSessions, 1); + assert.strictEqual(snapshot.summary.failedSessions, 1); + assert.strictEqual(snapshot.summary.stoppedSessions, 1); + assert.strictEqual(snapshot.summary.completedSessions, 1); + assert.strictEqual(snapshot.summary.runningSessions, 1); + assert.strictEqual(snapshot.summary.idleSessions, 1); + assert.strictEqual(snapshot.summary.totalSessions, 6); + + const failedSession = snapshot.sessions.find(session => session.id === 'session-failed'); + assert.deepStrictEqual(failedSession.detectedHarnesses, []); + assert.strictEqual(failedSession.metrics.costUsd, 0); + + const pendingSession = snapshot.sessions.find(session => session.id === 'session-pending'); + assert.strictEqual(pendingSession.pid, 0); + assert.strictEqual(pendingSession.metrics.tokensUsed, 0); + + assert.strictEqual(snapshot.knowledge.results[0].entity.name, 'Malformed JSON knowledge'); + assert.deepStrictEqual(snapshot.knowledge.results[0].entity.metadata, {}); + assert.deepStrictEqual(snapshot.knowledge.results[0].latestObservation.details, {}); + assert.strictEqual(snapshot.connectors[0].defaultEntityType, 'note'); + assert.strictEqual(snapshot.connectors[0].defaultObservationType, 'operator_memory'); + assert.strictEqual(snapshot.connectors[0].includeSafeValues, true); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + + if ( + await test('recall search covers metadata, relation caps, no matches, and tie ordering', async () => { + const baseEntities = [ + { + id: 1, + entityType: 'note', + name: 'First shared memory', + path: '/notes/shared-a.md', + summary: 'Platform context', + metadata: { source: 'workspace' }, + updatedAt: '2026-06-03T10:00:00Z' }, + { + id: 2, + entityType: 'note', + name: 'Second shared memory', + path: '/notes/shared-b.md', + summary: 'Platform context', + metadata: { source: 'workspace' }, + updatedAt: '2026-06-03T12:00:00Z' + }, + { + id: 3, + entityType: 'concept', + name: 'Markets graph', + path: null, + summary: 'Correlation graph visualization', + metadata: { flow: 'friction-flow' }, + updatedAt: '2026-06-03T09:00:00Z' + } + ]; + const observations = [ + { + entityId: 3, + priority: 1, + pinned: false, + summary: 'Ito should expose market backtesting through ECC tools.' + } + ]; + + const tied = recallKnowledgeEntries({ + entities: baseEntities, + observations: [], + relationCounts: new Map(), + query: 'shared', + limit: 50 }); + assert.deepStrictEqual( + tied.map(entry => entry.entity.id), + [2, 1] + ); - assert.strictEqual(snapshot.summary.pendingSessions, 1); - assert.strictEqual(snapshot.summary.failedSessions, 1); - assert.strictEqual(snapshot.summary.stoppedSessions, 1); - assert.strictEqual(snapshot.summary.completedSessions, 1); - assert.strictEqual(snapshot.summary.runningSessions, 1); - assert.strictEqual(snapshot.summary.idleSessions, 1); - assert.strictEqual(snapshot.summary.totalSessions, 6); + const metadataHit = recallKnowledgeEntries({ + entities: baseEntities, + observations, + relationCounts: new Map([[3, 20]]), + query: 'friction-flow backtesting', + limit: -5 + }); + assert.strictEqual(metadataHit.length, 1); + assert.strictEqual(metadataHit[0].entity.id, 3); + assert.strictEqual(metadataHit[0].relationCount, 20); + assert.ok(metadataHit[0].score >= 18); - const failedSession = snapshot.sessions.find(session => session.id === 'session-failed'); - assert.deepStrictEqual(failedSession.detectedHarnesses, []); - assert.strictEqual(failedSession.metrics.costUsd, 0); - - const pendingSession = snapshot.sessions.find(session => session.id === 'session-pending'); - assert.strictEqual(pendingSession.pid, 0); - assert.strictEqual(pendingSession.metrics.tokensUsed, 0); - - assert.strictEqual(snapshot.knowledge.results[0].entity.name, 'Malformed JSON knowledge'); - assert.deepStrictEqual(snapshot.knowledge.results[0].entity.metadata, {}); - assert.deepStrictEqual(snapshot.knowledge.results[0].latestObservation.details, {}); - assert.strictEqual(snapshot.connectors[0].defaultEntityType, 'note'); - assert.strictEqual(snapshot.connectors[0].defaultObservationType, 'operator_memory'); - assert.strictEqual(snapshot.connectors[0].includeSafeValues, true); - } finally { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - })) passed++; else failed++; - - if (await test('recall search covers metadata, relation caps, no matches, and tie ordering', async () => { - const baseEntities = [ - { - id: 1, - entityType: 'note', - name: 'First shared memory', - path: '/notes/shared-a.md', - summary: 'Platform context', - metadata: { source: 'workspace' }, - updatedAt: '2026-06-03T10:00:00Z', - }, - { - id: 2, - entityType: 'note', - name: 'Second shared memory', - path: '/notes/shared-b.md', - summary: 'Platform context', - metadata: { source: 'workspace' }, - updatedAt: '2026-06-03T12:00:00Z', - }, - { - id: 3, - entityType: 'concept', - name: 'Markets graph', - path: null, - summary: 'Correlation graph visualization', - metadata: { flow: 'friction-flow' }, - updatedAt: '2026-06-03T09:00:00Z', - }, - ]; - const observations = [ - { - entityId: 3, - priority: 1, - pinned: false, - summary: 'Ito should expose market backtesting through ECC tools.', - }, - ]; - - const tied = recallKnowledgeEntries({ - entities: baseEntities, - observations: [], - relationCounts: new Map(), - query: 'shared', - limit: 50, - }); - assert.deepStrictEqual(tied.map(entry => entry.entity.id), [2, 1]); - - const metadataHit = recallKnowledgeEntries({ - entities: baseEntities, - observations, - relationCounts: new Map([[3, 20]]), - query: 'friction-flow backtesting', - limit: -5, - }); - assert.strictEqual(metadataHit.length, 1); - assert.strictEqual(metadataHit[0].entity.id, 3); - assert.strictEqual(metadataHit[0].relationCount, 20); - assert.ok(metadataHit[0].score >= 18); - - const noHits = recallKnowledgeEntries({ - entities: baseEntities, - observations, - relationCounts: new Map(), - query: 'unmatched', - limit: 'wat', - }); - assert.deepStrictEqual(noHits, []); - })) passed++; else failed++; + const noHits = recallKnowledgeEntries({ + entities: baseEntities, + observations, + relationCounts: new Map(), + query: 'unmatched', + limit: 'wat' + }); + assert.deepStrictEqual(noHits, []); + }) + ) + passed++; + else failed++; console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);