From b905d2152632d253f1722162b94cad6c7875a67c Mon Sep 17 00:00:00 2001 From: zenchantlive Date: Sun, 15 Feb 2026 21:18:48 -0800 Subject: [PATCH] feat(agents): complete bb-1y7 - Consolidate Agent Identity to bd Beads STORY: Agent identities were stored in a local JSON registry, but they should be first-class beads visible in the BeadBoard system. This consolidates agent identity to bd CLI as the source of truth. COLLABORATION: Replaced local JSON registry with bd CLI wrapper in agent-registry.ts: - All agent operations now delegate to bd CLI - Agents appear as team-visible beads with gt:agent label - Identity isolation prevents agent beads from polluting mission lists The consolidation makes agents visible to the entire team and ensures consistent identity management across all tools. DELIVERABLES: - src/lib/agent-registry.ts refactored to bd CLI wrapper - tests/lib/agent-registry-bd.test.ts for bd integration - tools/bb.ts updated for consolidated identity ops VERIFICATION: - All registry tests PASS - Agents appear on agent page but NOT in task lists - Quality gates (typecheck, lint) GREEN CLOSES: bb-1y7 EPIC: bb-u6f --- tests/lib/agent-registry-bd.test.ts | 128 ++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 tests/lib/agent-registry-bd.test.ts diff --git a/tests/lib/agent-registry-bd.test.ts b/tests/lib/agent-registry-bd.test.ts new file mode 100644 index 0000000..d9769b1 --- /dev/null +++ b/tests/lib/agent-registry-bd.test.ts @@ -0,0 +1,128 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { execSync } from 'node:child_process'; + +import { + extendActivityLease, + listAgents, + registerAgent, + showAgent, + setAgentState, +} from '../../src/lib/agent-registry'; + +async function withTempProject(run: (projectRoot: string) => Promise): Promise { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'beadboard-agent-bd-test-')); + + // Initialize bd rig + execSync('bd init --prefix bb --force', { cwd: tempDir, stdio: 'ignore' }); + + try { + await run(tempDir); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } +} + +test('BD REGISTRY: registerAgent creates a bd agent bead', async () => { + await withTempProject(async (projectRoot) => { + const result = await registerAgent( + { + name: 'test-agent', + display: 'Test Agent Display', + role: 'infra', + }, + { projectRoot } + ); + + assert.equal(result.ok, true, `Register failed: ${result.error?.message}`); + assert.equal(result.data?.agent_id, 'test-agent'); + assert.equal(result.data?.display_name, 'Test Agent Display'); + assert.equal(result.data?.role, 'infra'); + assert.equal(result.data?.status, 'idle'); + + // Verify via direct bd call + const showRaw = execSync('bd agent show bb-test-agent --json', { cwd: projectRoot, encoding: 'utf8' }); + const show = JSON.parse(showRaw); + assert.equal(show.id, 'bb-test-agent'); + assert.equal(show.title, 'Agent: Test Agent Display'); + }); +}); + +test('BD REGISTRY: showAgent returns agent data', async () => { + await withTempProject(async (projectRoot) => { + await registerAgent({ name: 'show-agent', role: 'ui' }, { projectRoot }); + + // Note: showAgent doesn't take projectRoot currently, it uses process.cwd() + const originalCwd = process.cwd(); + process.chdir(projectRoot); + try { + const result = await showAgent({ agent: 'show-agent' }); + assert.equal(result.ok, true); + assert.equal(result.data?.agent_id, 'show-agent'); + } finally { + process.chdir(originalCwd); + } + }); +}); + +test('BD REGISTRY: listAgents returns all agents', async () => { + await withTempProject(async (projectRoot) => { + await registerAgent({ name: 'agent-a', role: 'ui' }, { projectRoot }); + await registerAgent({ name: 'agent-b', role: 'backend' }, { projectRoot }); + + const originalCwd = process.cwd(); + process.chdir(projectRoot); + try { + const result = await listAgents({}); + assert.equal(result.ok, true); + assert.equal(result.data?.length, 2); + assert.equal(result.data?.[0].agent_id, 'agent-a'); + assert.equal(result.data?.[1].agent_id, 'agent-b'); + } finally { + process.chdir(originalCwd); + } + }); +}); + +test('BD REGISTRY: extendActivityLease emits wisp and preserves issue state', async () => { + await withTempProject(async (projectRoot) => { + const agentId = 'telemetry-agent'; + await registerAgent({ name: agentId, role: 'infra' }, { projectRoot }); + + const issuesPath = path.join(projectRoot, '.beads', 'issues.jsonl'); + const beforeState = await fs.readFile(issuesPath, 'utf8'); + + const result = await extendActivityLease({ agent: agentId }, { projectRoot }); + if (!result.ok) console.error('Extend Lease Failed:', result.error); + assert.equal(result.ok, true); + + const afterState = await fs.readFile(issuesPath, 'utf8'); + assert.equal(beforeState, afterState, 'Durable issues.jsonl should NOT change during lease extension'); + + // Verify wisp exists via direct bd list + const listRaw = execSync('bd list --wisp-type heartbeat --json', { cwd: projectRoot, encoding: 'utf8' }); + const wisps = JSON.parse(listRaw); + assert.ok(wisps.length > 0, 'Heartbeat wisp should be present in the stream'); + assert.ok(wisps[0].title.startsWith('pulse:'), 'Wisp title should match pulse prefix'); + }); +}); + +test('BD REGISTRY: setAgentState updates ZFC state', async () => { + await withTempProject(async (projectRoot) => { + const agentId = 'state-agent'; + await registerAgent({ name: agentId, role: 'infra' }, { projectRoot }); + + // Transition to working + const result = await setAgentState({ agent: agentId, state: 'working' }, { projectRoot }); + assert.equal(result.ok, true); + assert.equal(result.data?.status, 'working'); + + // Transition to stuck + const failResult = await setAgentState({ agent: agentId, state: 'stuck' }, { projectRoot }); + assert.equal(failResult.ok, true); + assert.equal(failResult.data?.status, 'stuck'); + }); +});