From c633fdcc495283de0da7c4cf21dba8dfffc99e9a Mon Sep 17 00:00:00 2001 From: Mevy Assistant Date: Sun, 19 Apr 2026 06:54:28 +0000 Subject: [PATCH] Remove legacy agent IDs + tighten task API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Canonicalize controlplane agent IDs/roles to: sysadmin, db-admin, git-admin (drop *_agent variants). - Add DB migration to rewrite existing *_agent rows and references to canonical IDs. - Tighten POST /api/controlplane/tasks contract: require assigned_to (remove agent_id alias). - Update tests and docs to match canonical IDs. --- Build: pass (just typecheck) Tests: pass — 1536 passed (92 files) (just test) --- DB_ADMIN_AGENT.md | 8 +-- GIT_ADMIN_AGENT.md | 8 +-- SYSADMIN_AGENT.md | 16 ++--- doc/CONTROLPLANE-MESSAGE-CONTRACT.md | 26 ++++---- doc/MULTI-PROVIDER-ARCHITECTURE.md | 2 +- docs/internal/LOCAL-LLM.md | 2 +- docs/internal/REFACTOR-PLAN.md | 2 +- src/agent-session.test.ts | 78 +++++++++++----------- src/controlplane-api.test.ts | 44 ++++++------- src/controlplane-api.ts | 9 +-- src/controlplane-budget.test.ts | 52 +++++++-------- src/controlplane-budget.ts | 6 +- src/controlplane-db.test.ts | 10 +-- src/controlplane-db.ts | 98 ++++++++++++++++++++-------- src/controlplane-heartbeat.test.ts | 4 +- src/controlplane-runner.test.ts | 31 ++++----- src/controlplane-runner.ts | 11 +--- src/controlplane-setup.test.ts | 30 ++++----- src/controlplane-telegram.test.ts | 22 +++---- src/controlplane-telegram.ts | 6 +- src/controlplane.test.ts | 66 +++++++++---------- src/skills-discovery.test.ts | 8 +-- 22 files changed, 286 insertions(+), 253 deletions(-) diff --git a/DB_ADMIN_AGENT.md b/DB_ADMIN_AGENT.md index 631adab..ae42657 100644 --- a/DB_ADMIN_AGENT.md +++ b/DB_ADMIN_AGENT.md @@ -36,7 +36,7 @@ When orchestrator creates a task assigned to you, you: ### Layer 1: Control Plane (What's My Task?) ``` -GET /api/controlplane/tasks?role=db_admin_agent +GET /api/controlplane/tasks?role=db-admin → [ { task_id: "TASK-042", @@ -49,7 +49,7 @@ GET /api/controlplane/tasks?role=db_admin_agent ### Layer 2: Clawdie (What Do I Know?) ``` -Read data/sessions/db_admin_agent.jsonl +Read data/sessions/db-admin.jsonl → [ {"task": "Backup clawdie_ai_public", "skill": "backup-db", "runtime": "18m", "size": "2.3GB"}, {"task": "Analyze database", "skill": "db-analyze", "runtime": "12m"} @@ -157,7 +157,7 @@ Example: POST /api/controlplane/activity { "event_type": "error", - "agent_id": "db_admin_agent", + "agent_id": "db-admin", "operation": "db-vacuum on clawdie_ai_public", "error": "Vacuum failed: lock timeout (another process holding lock for 45min)", "action_taken": "Aborted vacuum, escalated to orchestrator", @@ -192,7 +192,7 @@ POST /api/controlplane/activity ## Continuity & Memory -Your session lives in `data/sessions/db_admin_agent.jsonl`. +Your session lives in `data/sessions/db-admin.jsonl`. Each time you complete a job: ```json diff --git a/GIT_ADMIN_AGENT.md b/GIT_ADMIN_AGENT.md index 2731ec7..74dbd9f 100644 --- a/GIT_ADMIN_AGENT.md +++ b/GIT_ADMIN_AGENT.md @@ -36,7 +36,7 @@ When orchestrator creates a task assigned to you, you: ### Layer 1: Control Plane (What's My Task?) ``` -GET /api/controlplane/tasks?role=git_admin_agent +GET /api/controlplane/tasks?role=git-admin → [ { task_id: "TASK-105", @@ -54,7 +54,7 @@ GET /api/controlplane/tasks?role=git_admin_agent ### Layer 2: Clawdie (What Do I Know?) ``` -Read /home/clawdie/clawdie-ai/data/sessions/git_admin_agent.jsonl +Read /home/clawdie/clawdie-ai/data/sessions/git-admin.jsonl → [ {"task": "Merge PR #40", "skill": "git-merge", "conflict": false, "runtime": "45s"}, {"task": "Tag v0.10.0", "skill": "git-release-tag", "runtime": "8s"} @@ -196,7 +196,7 @@ Example: POST /api/controlplane/activity { "event_type": "approval_request", - "agent_id": "git_admin_agent", + "agent_id": "git-admin", "operation": "Merge PR #42 (feature/paperclip-integration) into main", "reason": "Merge conflict detected", "conflict_analysis": { @@ -240,7 +240,7 @@ POST /api/controlplane/activity ## Continuity & Memory -Your session lives in `/home/clawdie/clawdie-ai/data/sessions/git_admin_agent.jsonl`. +Your session lives in `/home/clawdie/clawdie-ai/data/sessions/git-admin.jsonl`. Each time you complete a job: ```json diff --git a/SYSADMIN_AGENT.md b/SYSADMIN_AGENT.md index 66f8afa..d29f062 100644 --- a/SYSADMIN_AGENT.md +++ b/SYSADMIN_AGENT.md @@ -49,7 +49,7 @@ Same pattern: ### Layer 1: Control Plane (What's My Job?) ``` -GET /api/controlplane/tasks?role=sysadmin_agent +GET /api/controlplane/tasks?role=sysadmin → [ { task_id: "TASK-001", title: "Check if db jail is running" }, { task_id: "TASK-002", title: "Backup database to external drive" } @@ -58,7 +58,7 @@ GET /api/controlplane/tasks?role=sysadmin_agent ### Layer 2: Clawdie (What Do I Know?) ``` -Read data/sessions/sysadmin_agent.jsonl +Read data/sessions/sysadmin.jsonl → [ {"task": "Check db jail", "skill": "jail-status", "outcome": "running"}, {"task": "Backup database", "skill": "backup-db", "outcome": "2.3GB, success"} @@ -91,13 +91,13 @@ You have access to 14 operational skills. Here's how you match tasks: | Task Pattern | Skill | Example Output | |---|---|---| -| "Check if X jail is running" or "Is X up?" | `jail-status` | `sysadmin_agent-db is running (uptime 5d 3h)` | +| "Check if X jail is running" or "Is X up?" | `jail-status` | `sysadmin-db is running (uptime 5d 3h)` | | "How much free disk?" or "Disk space?" | `disk-usage` | `/var/db 45% full, 2TB free` | | "System health?" or "CPU/RAM/load?" | `system-stats` | `CPU 12%, RAM 4GB/8GB, load 0.3` | | "Restart X service" or "Start/stop X" | `service-restart` | `nginx restarted successfully` | | "Back up the database" | `backup-db` | `Backup complete: 2.3GB, 18m runtime` | | "Free up space" or "Clean up logs" | `disk-cleanup` | `Removed 50GB old logs` | -| "Check RCTL/quotas" | `resource-limits` | `sysadmin_agent-db: 2GB limit, 1.2GB used` | +| "Check RCTL/quotas" | `resource-limits` | `sysadmin-db: 2GB limit, 1.2GB used` | | "Create ZFS snapshot" | `zfs-snapshot` | `Snapshot created: tank/clawdie@2026-04-07` | | "Take database backup to offsite" | `backup-offsite` | `Backup synced to Tailscale peer` | | "PF firewall status?" | `pf-status` | `PF enabled, 3 rules loaded, 0 dropped` | @@ -143,7 +143,7 @@ Escalation format: POST /api/controlplane/activity { "event_type": "approval_request", - "agent_id": "sysadmin_agent", + "agent_id": "sysadmin", "operation": "Description of what I want to do", "reasoning": "Why this doesn't match my patterns", "estimated_tokens": 3500 @@ -154,7 +154,7 @@ POST /api/controlplane/activity ## Memory & Continuity -Your session lives in `/home/clawdie/clawdie-ai/data/sessions/sysadmin_agent.jsonl`. +Your session lives in `/home/clawdie/clawdie-ai/data/sessions/sysadmin.jsonl`. Each heartbeat, you append: ```json @@ -163,12 +163,12 @@ Each heartbeat, you append: "task": "Check if db jail is running", "skill": "jail-status", "result": "success", - "output": "sysadmin_agent-db running, uptime 5d 3h, CPU 2%, RAM 512/2048MB", + "output": "sysadmin-db running, uptime 5d 3h, CPU 2%, RAM 512/2048MB", "tokens_used": 420 } ``` -Next heartbeat, you read this file. If you see "sysadmin_agent-db was running yesterday at 10:30," you know: +Next heartbeat, you read this file. If you see "sysadmin-db was running yesterday at 10:30," you know: - "It's been stable for 5+ days" - "When I ran jail-status before, it took 420 tokens" - "Same skill works for this task" diff --git a/doc/CONTROLPLANE-MESSAGE-CONTRACT.md b/doc/CONTROLPLANE-MESSAGE-CONTRACT.md index 4e4e28f..0bc0cae 100644 --- a/doc/CONTROLPLANE-MESSAGE-CONTRACT.md +++ b/doc/CONTROLPLANE-MESSAGE-CONTRACT.md @@ -31,18 +31,18 @@ Authorization: Bearer {CONTROLPLANE_SHARED_SECRET} "agents": [ { "id": "clawdie", "role": "orchestrator", "heartbeat_enabled": false }, { - "id": "sysadmin_agent", - "role": "sysadmin_agent", + "id": "sysadmin", + "role": "sysadmin", "heartbeat_enabled": true }, { - "id": "db_admin_agent", - "role": "db_admin_agent", + "id": "db-admin", + "role": "db-admin", "heartbeat_enabled": false }, { - "id": "git_admin_agent", - "role": "git_admin_agent", + "id": "git-admin", + "role": "git-admin", "heartbeat_enabled": false } ], @@ -53,9 +53,9 @@ Authorization: Bearer {CONTROLPLANE_SHARED_SECRET} "hard_limit_exceeded": false, "allocation": { "orchestrator": 80000, - "sysadmin_agent": 10000, - "db_admin_agent": 5000, - "git_admin_agent": 5000 + "sysadmin": 10000, + "db-admin": 5000, + "git-admin": 5000 } } } @@ -79,7 +79,7 @@ Authorization: Bearer {CONTROLPLANE_SHARED_SECRET} "task_id": "TASK-001", "title": "Check if db jail is running", "description": "Verify clawdie-db is up and healthy", - "assigned_to": "sysadmin_agent", + "assigned_to": "sysadmin", "priority": "medium", "status": "pending", "created_at": "2026-04-07T10:30:00Z", @@ -161,7 +161,7 @@ Authorization: Bearer {CONTROLPLANE_SHARED_SECRET} { "event_type": "task_completed", "task_id": "TASK-001", - "agent_id": "sysadmin_agent", + "agent_id": "sysadmin", "skill_executed": "jail-status", "result": { "status": "success", @@ -179,7 +179,7 @@ Authorization: Bearer {CONTROLPLANE_SHARED_SECRET} { "event_type": "approval_request", - "agent_id": "git_admin_agent", + "agent_id": "git-admin", "operation": "Merge PR #42 with conflict resolution", "reasoning": "Conflict detected in src/index.ts", "estimated_tokens": 8500 @@ -194,7 +194,7 @@ Authorization: Bearer {CONTROLPLANE_SHARED_SECRET} { "event_type": "error", - "agent_id": "db_admin_agent", + "agent_id": "db-admin", "error_message": "Vacuum failed: database locked", "action_taken": "Escalated to orchestrator", "tokens_used": 1200 diff --git a/doc/MULTI-PROVIDER-ARCHITECTURE.md b/doc/MULTI-PROVIDER-ARCHITECTURE.md index 4e77152..a3d14b7 100644 --- a/doc/MULTI-PROVIDER-ARCHITECTURE.md +++ b/doc/MULTI-PROVIDER-ARCHITECTURE.md @@ -251,7 +251,7 @@ Maps task context to the best provider: ```typescript interface TaskProfile { - agentRole: string; // orchestrator, sysadmin_agent, db_admin_agent, git_admin_agent + agentRole: string; // orchestrator, sysadmin, db-admin, git-admin skillMatched: boolean; // skill catalog matched exactly complexity: 'routine' | 'moderate' | 'complex'; requiresReasoning: boolean; diff --git a/docs/internal/LOCAL-LLM.md b/docs/internal/LOCAL-LLM.md index 2a5fa5b..1c7f7a6 100644 --- a/docs/internal/LOCAL-LLM.md +++ b/docs/internal/LOCAL-LLM.md @@ -34,7 +34,7 @@ independently. **Chat:** `cognitivecomputations/dolphin3.0-phi4-mini-GGUF` - `dolphin3.0-phi4-mini-Q4_K_M.gguf` — ~2.4 GB - Phi-4 Mini base (Microsoft), Dolphin uncensored fine-tune -- Good for: routine sysadmin_agent tasks, heartbeat interpretation, pi TUI +- Good for: routine sysadmin tasks, heartbeat interpretation, pi TUI - Weak at: complex multi-step tool chains ```sh diff --git a/docs/internal/REFACTOR-PLAN.md b/docs/internal/REFACTOR-PLAN.md index 9316c29..76dc4ae 100644 --- a/docs/internal/REFACTOR-PLAN.md +++ b/docs/internal/REFACTOR-PLAN.md @@ -198,7 +198,7 @@ export function cleanupOrphans() { ... } | `ollama` | Local LLM, runs on host (great upstream candidate) | | `postgres-memory` | Supabase memory backend (great upstream candidate) | | `telegram-admin` | Telegram bot management | -| `freebsd-admin` | Host sysadmin_agent tasks | +| `freebsd-admin` | Host sysadmin tasks | | `sanoid` | ZFS snapshots — now must cover `/home/clawdie` too | ### Skills to PORT FROM NanoClaw (we're missing) diff --git a/src/agent-session.test.ts b/src/agent-session.test.ts index ab9144a..5ba264c 100644 --- a/src/agent-session.test.ts +++ b/src/agent-session.test.ts @@ -45,22 +45,22 @@ function makeEntry(overrides: Partial = {}): SessionEntry { describe('Agent Session Persistence', () => { describe('writing session entries', () => { it('creates JSONL file at sessionDir/{agentName}.jsonl', () => { - writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry(), WORKSPACE); - expect(fs.existsSync(path.join(tmpDir, 'sysadmin_agent.jsonl'))).toBe( + writeSessionEntry(tmpDir, 'sysadmin', makeEntry(), WORKSPACE); + expect(fs.existsSync(path.join(tmpDir, 'sysadmin.jsonl'))).toBe( true, ); }); it('appends entries, one JSON object per line', () => { - writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry(), WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin', makeEntry(), WORKSPACE); writeSessionEntry( tmpDir, - 'sysadmin_agent', + 'sysadmin', makeEntry({ task: 'Check disk' }), WORKSPACE, ); const content = fs.readFileSync( - path.join(tmpDir, 'sysadmin_agent.jsonl'), + path.join(tmpDir, 'sysadmin.jsonl'), 'utf-8', ); const lines = content.trim().split('\n'); @@ -68,8 +68,8 @@ describe('Agent Session Persistence', () => { }); it('each entry includes timestamp, task, skill, result, tokens_used', () => { - writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry(), WORKSPACE); - const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin', makeEntry(), WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); const entry = session.entries[0]; expect(entry).toHaveProperty('timestamp'); expect(entry).toHaveProperty('task'); @@ -82,13 +82,13 @@ describe('Agent Session Persistence', () => { for (let i = 0; i < 5; i++) { writeSessionEntry( tmpDir, - 'sysadmin_agent', + 'sysadmin', makeEntry({ task: `Task ${i}` }), WORKSPACE, ); } const content = fs.readFileSync( - path.join(tmpDir, 'sysadmin_agent.jsonl'), + path.join(tmpDir, 'sysadmin.jsonl'), 'utf-8', ); for (const line of content.trim().split('\n')) { @@ -99,19 +99,19 @@ describe('Agent Session Persistence', () => { describe('loading sessions', () => { it('loads all entries from existing JSONL file', () => { - writeSessionEntry(tmpDir, 'db_admin_agent', makeEntry(), WORKSPACE); + writeSessionEntry(tmpDir, 'db-admin', makeEntry(), WORKSPACE); writeSessionEntry( tmpDir, - 'db_admin_agent', + 'db-admin', makeEntry({ task: 'Vacuum' }), WORKSPACE, ); - const session = loadSession(tmpDir, 'db_admin_agent', WORKSPACE); + const session = loadSession(tmpDir, 'db-admin', WORKSPACE); expect(session.entries).toHaveLength(2); }); it('returns empty entries array when file does not exist', () => { - const session = loadSession(tmpDir, 'git_admin_agent', WORKSPACE); + const session = loadSession(tmpDir, 'git-admin', WORKSPACE); expect(session.entries).toHaveLength(0); }); @@ -124,17 +124,17 @@ describe('Agent Session Persistence', () => { it('entries are in chronological order (oldest first)', () => { writeSessionEntry( tmpDir, - 'sysadmin_agent', + 'sysadmin', makeEntry({ task: 'First', timestamp: '2026-04-07T09:00:00Z' }), WORKSPACE, ); writeSessionEntry( tmpDir, - 'sysadmin_agent', + 'sysadmin', makeEntry({ task: 'Second', timestamp: '2026-04-07T10:00:00Z' }), WORKSPACE, ); - const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); expect(session.entries[0].task).toBe('First'); expect(session.entries[1].task).toBe('Second'); }); @@ -143,32 +143,32 @@ describe('Agent Session Persistence', () => { describe('resilience', () => { it('skips malformed lines (not crashing on bad JSON)', () => { fs.writeFileSync( - path.join(tmpDir, 'sysadmin_agent.jsonl'), + path.join(tmpDir, 'sysadmin.jsonl'), 'not-json\n', 'utf-8', ); - writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry(), WORKSPACE); - const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin', makeEntry(), WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); expect(session.entries).toHaveLength(1); }); it('still loads valid lines when one line is corrupt', () => { - const filePath = path.join(tmpDir, 'db_admin_agent.jsonl'); + const filePath = path.join(tmpDir, 'db-admin.jsonl'); fs.writeFileSync(filePath, '', 'utf-8'); writeSessionEntry( tmpDir, - 'db_admin_agent', + 'db-admin', makeEntry({ task: 'Valid' }), WORKSPACE, ); fs.appendFileSync(filePath, 'bad{json\n'); writeSessionEntry( tmpDir, - 'db_admin_agent', + 'db-admin', makeEntry({ task: 'Also valid' }), WORKSPACE, ); - const session = loadSession(tmpDir, 'db_admin_agent', WORKSPACE); + const session = loadSession(tmpDir, 'db-admin', WORKSPACE); expect(session.entries).toHaveLength(2); expect( session.entries.every( @@ -194,7 +194,7 @@ describe('Agent Session Persistence', () => { it('session file path is under project root (workspace rule)', () => { const filePath = resolveSessionPath( 'data/sessions', - 'sysadmin_agent', + 'sysadmin', WORKSPACE, ); expect(filePath).toMatch(new RegExp(`^${WORKSPACE}`)); @@ -202,13 +202,13 @@ describe('Agent Session Persistence', () => { it('rejects sessionDir pointing to /tmp/', () => { expect(() => - resolveSessionPath('/tmp', 'sysadmin_agent', WORKSPACE), + resolveSessionPath('/tmp', 'sysadmin', WORKSPACE), ).toThrow(); }); it('rejects sessionDir with path traversal (../)', () => { expect(() => - resolveSessionPath('../../../etc', 'sysadmin_agent', WORKSPACE), + resolveSessionPath('../../../etc', 'sysadmin', WORKSPACE), ).toThrow(); }); }); @@ -218,12 +218,12 @@ describe('Agent Session Persistence', () => { for (let i = 0; i < 10; i++) { writeSessionEntry( tmpDir, - 'sysadmin_agent', + 'sysadmin', makeEntry({ task: `Task ${i}` }), WORKSPACE, ); } - const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); const pruned = pruneOldEntries(session, 3); expect(pruned.entries).toHaveLength(3); expect(pruned.entries[0].task).toBe('Task 7'); @@ -231,8 +231,8 @@ describe('Agent Session Persistence', () => { }); it('prune does not modify file when entries <= maxEntries', () => { - writeSessionEntry(tmpDir, 'db_admin_agent', makeEntry(), WORKSPACE); - const session = loadSession(tmpDir, 'db_admin_agent', WORKSPACE); + writeSessionEntry(tmpDir, 'db-admin', makeEntry(), WORKSPACE); + const session = loadSession(tmpDir, 'db-admin', WORKSPACE); const before = fs.readFileSync(session.filePath, 'utf-8'); pruneOldEntries(session, 10); const after = fs.readFileSync(session.filePath, 'utf-8'); @@ -259,7 +259,7 @@ describe('Agent Session Persistence', () => { it('last N entries formatted for system prompt injection', () => { writeSessionEntry( tmpDir, - 'sysadmin_agent', + 'sysadmin', makeEntry({ task: 'Check jails', skill: 'jail-status', @@ -268,7 +268,7 @@ describe('Agent Session Persistence', () => { }), WORKSPACE, ); - const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); const ctx = formatSessionContext(session); expect(ctx).toContain('jail-status'); expect(ctx).toContain('420'); @@ -277,7 +277,7 @@ describe('Agent Session Persistence', () => { it('context includes task, skill, outcome (not full output)', () => { writeSessionEntry( tmpDir, - 'sysadmin_agent', + 'sysadmin', makeEntry({ task: 'Check disk', skill: 'disk-usage', @@ -287,14 +287,14 @@ describe('Agent Session Persistence', () => { WORKSPACE, ); const ctx = formatSessionContext( - loadSession(tmpDir, 'sysadmin_agent', WORKSPACE), + loadSession(tmpDir, 'sysadmin', WORKSPACE), ); expect(ctx).toContain('disk-usage'); expect(ctx).toContain('Check disk'); }); it('context is empty string when no session history', () => { - const session = loadSession(tmpDir, 'git_admin_agent', WORKSPACE); + const session = loadSession(tmpDir, 'git-admin', WORKSPACE); expect(formatSessionContext(session)).toBe(''); }); @@ -302,7 +302,7 @@ describe('Agent Session Persistence', () => { for (let i = 0; i < 8; i++) { writeSessionEntry( tmpDir, - 'sysadmin_agent', + 'sysadmin', makeEntry({ task: `Task ${i}`, skill: 'test', @@ -312,7 +312,7 @@ describe('Agent Session Persistence', () => { WORKSPACE, ); } - const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); const ctx = formatSessionContext(session, 3); const lines = ctx.split('\n').filter((l: string) => l.trim()); expect(lines).toHaveLength(3); @@ -321,7 +321,7 @@ describe('Agent Session Persistence', () => { it('includes error result type in context', () => { writeSessionEntry( tmpDir, - 'sysadmin_agent', + 'sysadmin', makeEntry({ task: 'Failing task', skill: 'none', @@ -331,7 +331,7 @@ describe('Agent Session Persistence', () => { WORKSPACE, ); const ctx = formatSessionContext( - loadSession(tmpDir, 'sysadmin_agent', WORKSPACE), + loadSession(tmpDir, 'sysadmin', WORKSPACE), ); expect(ctx).toContain('error'); expect(ctx).toContain('Failing task'); diff --git a/src/controlplane-api.test.ts b/src/controlplane-api.test.ts index 216e9ea..7bc6691 100644 --- a/src/controlplane-api.test.ts +++ b/src/controlplane-api.test.ts @@ -29,12 +29,12 @@ vi.mock('better-auth/node', () => ({ const defaultAgents: Agent[] = [ { id: 'clawdie', role: 'orchestrator', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, budget_allocation_pct: 80, api_key_hash: null, created_at: new Date() }, - { id: 'sysadmin_agent', role: 'sysadmin_agent', adapter: 'pi-local', heartbeat_enabled: true, heartbeat_interval_sec: 86400, budget_allocation_pct: 10, api_key_hash: null, created_at: new Date() }, + { id: 'sysadmin', role: 'sysadmin', adapter: 'pi-local', heartbeat_enabled: true, heartbeat_interval_sec: 86400, budget_allocation_pct: 10, api_key_hash: null, created_at: new Date() }, ]; const defaultBudgets: Budget[] = [ { agent_id: 'clawdie', daily_tokens: 80000, spent_today: 20000, remaining: 60000, hard_limit_exceeded: false, reset_at: new Date() }, - { agent_id: 'sysadmin_agent', daily_tokens: 10000, spent_today: 5000, remaining: 5000, hard_limit_exceeded: false, reset_at: new Date() }, + { agent_id: 'sysadmin', daily_tokens: 10000, spent_today: 5000, remaining: 5000, hard_limit_exceeded: false, reset_at: new Date() }, ]; const validAuth = 'Bearer op:admin:testpass'; @@ -263,19 +263,19 @@ describe('Control Plane HTTP API — contract tests', () => { it('filters tasks by ?role= parameter', async () => { const { handler } = await setup({ tasks: [ - { id: 'T1', title: 'Check jail', description: null, assigned_to: 'sysadmin_agent', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, - { id: 'T2', title: 'Vacuum DB', description: null, assigned_to: 'db_admin_agent', priority: 'low', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'T1', title: 'Check jail', description: null, assigned_to: 'sysadmin', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'T2', title: 'Vacuum DB', description: null, assigned_to: 'db-admin', priority: 'low', status: 'pending', created_at: new Date(), deadline: null, context: null }, ], }); - const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=sysadmin_agent', undefined, validAuth); + const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=sysadmin', undefined, validAuth); expect(res.body.tasks).toHaveLength(1); - expect(res.body.tasks[0].assigned_to).toBe('sysadmin_agent'); + expect(res.body.tasks[0].assigned_to).toBe('sysadmin'); }); it('each task has task_id, title, description, assigned_to, status', async () => { const { handler } = await setup({ tasks: [ - { id: 'T1', title: 'Check jail', description: 'desc', assigned_to: 'sysadmin_agent', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'T1', title: 'Check jail', description: 'desc', assigned_to: 'sysadmin', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, ], }); const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks', undefined, validAuth); @@ -289,18 +289,18 @@ describe('Control Plane HTTP API — contract tests', () => { it('returns empty array when no tasks assigned to role', async () => { const { handler } = await setup(); - const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=git_admin_agent', undefined, validAuth); + const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=git-admin', undefined, validAuth); expect(res.body.tasks).toHaveLength(0); }); it('returns pending tasks only (not completed or cancelled)', async () => { const { handler } = await setup({ tasks: [ - { id: 'T1', title: 'Pending task', description: null, assigned_to: 'sysadmin_agent', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'T1', title: 'Pending task', description: null, assigned_to: 'sysadmin', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, ], filterTasksByStatus: true, }); - const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=sysadmin_agent', undefined, validAuth); + const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=sysadmin', undefined, validAuth); expect(res.body.tasks).toHaveLength(1); expect(res.body.tasks[0].status).toBe('pending'); }); @@ -311,7 +311,7 @@ describe('Control Plane HTTP API — contract tests', () => { const { handler } = await setup(); const res = await makeRequest(handler, 'POST', '/api/controlplane/activity', { event_type: 'task_completed', - agent_id: 'sysadmin_agent', + agent_id: 'sysadmin', task_id: 'T1', }, validAuth); expect(res.statusCode).toBe(200); @@ -322,7 +322,7 @@ describe('Control Plane HTTP API — contract tests', () => { const { handler } = await setup(); const res = await makeRequest(handler, 'POST', '/api/controlplane/activity', { event_type: 'approval_request', - agent_id: 'git_admin_agent', + agent_id: 'git-admin', operation: 'merge PR', }, validAuth); expect(res.statusCode).toBe(200); @@ -333,7 +333,7 @@ describe('Control Plane HTTP API — contract tests', () => { const { handler } = await setup(); const res = await makeRequest(handler, 'POST', '/api/controlplane/activity', { event_type: 'error', - agent_id: 'db_admin_agent', + agent_id: 'db-admin', error_message: 'vacuum failed', }, validAuth); expect(res.statusCode).toBe(200); @@ -361,7 +361,7 @@ describe('Control Plane HTTP API — contract tests', () => { const { handler, insertedActivity } = await setup(); await makeRequest(handler, 'POST', '/api/controlplane/activity', { event_type: 'task_completed', - agent_id: 'sysadmin_agent', + agent_id: 'sysadmin', tokens_used: 420, }, validAuth); expect(insertedActivity.length).toBeGreaterThan(0); @@ -415,9 +415,9 @@ describe('Control Plane HTTP API — contract tests', () => { const { createControlplaneApiHandler } = await import('./controlplane-api.js'); const handler = createControlplaneApiHandler({ pool: pool as any, authInstance: mockAuthInstance as any }); - const res = await makeRequest(handler, 'GET', '/api/controlplane/approvals?agent_id=sysadmin_agent', undefined, validAuth); + const res = await makeRequest(handler, 'GET', '/api/controlplane/approvals?agent_id=sysadmin', undefined, validAuth); expect(res.statusCode).toBe(200); - expect(capturedParams?.[0]).toBe('sysadmin_agent'); + expect(capturedParams?.[0]).toBe('sysadmin'); }); }); @@ -462,9 +462,9 @@ describe('Control Plane HTTP API — contract tests', () => { const { createControlplaneApiHandler } = await import('./controlplane-api.js'); const handler = createControlplaneApiHandler({ pool: pool as any, authInstance: mockAuthInstance as any }); - const res = await makeRequest(handler, 'GET', '/api/controlplane/activity?agent_id=sysadmin_agent&event_type=task_completed&limit=10', undefined, validAuth); + const res = await makeRequest(handler, 'GET', '/api/controlplane/activity?agent_id=sysadmin&event_type=task_completed&limit=10', undefined, validAuth); expect(res.statusCode).toBe(200); - expect(capturedParams).toContain('sysadmin_agent'); + expect(capturedParams).toContain('sysadmin'); expect(capturedParams).toContain('task_completed'); }); }); @@ -473,7 +473,7 @@ describe('Control Plane HTTP API — contract tests', () => { it('updates task status and returns updated task', async () => { const { handler } = await setup({ tasks: [ - { id: 'T1', title: 'Check jail', description: null, assigned_to: 'sysadmin_agent', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'T1', title: 'Check jail', description: null, assigned_to: 'sysadmin', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, ], }); const res = await makeRequest(handler, 'PATCH', '/api/controlplane/tasks/T1', { status: 'in_progress' }, validAuth); @@ -508,7 +508,7 @@ describe('Control Plane HTTP API — contract tests', () => { describe('POST /api/controlplane/approvals/:id/approve', () => { const pendingApproval = { id: 'APPR-001', - agent_id: 'git_admin_agent', + agent_id: 'git-admin', task_id: null, operation: 'merge PR', estimated_tokens: null, @@ -544,7 +544,7 @@ describe('Control Plane HTTP API — contract tests', () => { describe('POST /api/controlplane/approvals/:id/reject', () => { const pendingApproval = { id: 'APPR-002', - agent_id: 'db_admin_agent', + agent_id: 'db-admin', task_id: null, operation: 'drop table', estimated_tokens: null, @@ -568,7 +568,7 @@ describe('Control Plane HTTP API — contract tests', () => { const deadline = new Date('2026-05-01T00:00:00Z'); const { handler } = await setup({ tasks: [ - { id: 'T1', title: 'Test', description: 'desc', assigned_to: 'sysadmin_agent', priority: 'high', status: 'pending', created_at: new Date(), deadline, context: { jail: 'clawdie-db' } }, + { id: 'T1', title: 'Test', description: 'desc', assigned_to: 'sysadmin', priority: 'high', status: 'pending', created_at: new Date(), deadline, context: { jail: 'clawdie-db' } }, ], }); const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks', undefined, validAuth); diff --git a/src/controlplane-api.ts b/src/controlplane-api.ts index e9c743c..bb2096f 100644 --- a/src/controlplane-api.ts +++ b/src/controlplane-api.ts @@ -302,11 +302,12 @@ async function handlePostTask( deadlineValue = parsedDeadline; } - const assignedToRaw = + const assignedTo = typeof assigned_to === 'string' ? assigned_to : undefined; - // Back-compat: some clients send `agent_id` instead of `assigned_to`. - const agentIdRaw = typeof parsed.agent_id === 'string' ? parsed.agent_id : undefined; - const assignedTo = assignedToRaw ?? agentIdRaw; + if (!assignedTo) { + jsonResponse(res, 400, { error: 'missing assigned_to' }); + return; + } const taskId = `API-${Date.now().toString(36).toUpperCase()}`; await createTask(pool, { diff --git a/src/controlplane-budget.test.ts b/src/controlplane-budget.test.ts index 22babed..bceb7e1 100644 --- a/src/controlplane-budget.test.ts +++ b/src/controlplane-budget.test.ts @@ -52,7 +52,7 @@ const mainBudget = (spent = 0): Budget => ({ }); const sysadminBudget = (spent = 0): Budget => ({ - agent_id: 'sysadmin_agent', + agent_id: 'sysadmin', daily_tokens: 10000, spent_today: spent, remaining: 10000 - spent, @@ -61,7 +61,7 @@ const sysadminBudget = (spent = 0): Budget => ({ }); const dbaBudget = (spent = 0): Budget => ({ - agent_id: 'db_admin_agent', + agent_id: 'db-admin', daily_tokens: 5000, spent_today: spent, remaining: 5000 - spent, @@ -77,7 +77,7 @@ describe('Control Plane Budget Enforcement', () => { const pool = makePool([sysadminBudget(1000)]); const result = await checkBudget( pool as unknown as Pool, - 'sysadmin_agent', + 'sysadmin', 500, ); expect(result.allowed).toBe(true); @@ -87,7 +87,7 @@ describe('Control Plane Budget Enforcement', () => { const pool = makePool([sysadminBudget(10000)]); const result = await checkBudget( pool as unknown as Pool, - 'sysadmin_agent', + 'sysadmin', 1, ); expect(result.allowed).toBe(false); @@ -98,7 +98,7 @@ describe('Control Plane Budget Enforcement', () => { const pool = makePool([sysadminBudget(9500)]); const result = await checkBudget( pool as unknown as Pool, - 'sysadmin_agent', + 'sysadmin', 600, ); expect(result.allowed).toBe(false); @@ -109,7 +109,7 @@ describe('Control Plane Budget Enforcement', () => { const pool = makePool([sysadminBudget(10000)]); const result = await checkBudget( pool as unknown as Pool, - 'sysadmin_agent', + 'sysadmin', 1, ); expect(result.reason).toBe('budget_exhausted'); @@ -122,7 +122,7 @@ describe('Control Plane Budget Enforcement', () => { ]); const result = await checkBudget( pool as unknown as Pool, - 'sysadmin_agent', + 'sysadmin', 1, ); expect(result.allowed).toBe(false); @@ -135,7 +135,7 @@ describe('Control Plane Budget Enforcement', () => { const pool = makePool([dbaBudget(0)]); const result = await checkBudget( pool as unknown as Pool, - 'db_admin_agent', + 'db-admin', 500, ); expect(result.allowed).toBe(true); @@ -145,7 +145,7 @@ describe('Control Plane Budget Enforcement', () => { const pool = makePool([dbaBudget(5000)]); const result = await checkBudget( pool as unknown as Pool, - 'db_admin_agent', + 'db-admin', 1, ); expect(result.allowed).toBe(false); @@ -155,7 +155,7 @@ describe('Control Plane Budget Enforcement', () => { const pool = makePool([dbaBudget(5000)]); const result = await checkBudget( pool as unknown as Pool, - 'db_admin_agent', + 'db-admin', 1, ); expect(result.reason).toBe('budget_exhausted'); @@ -172,21 +172,21 @@ describe('Control Plane Budget Enforcement', () => { expect(result.reason).toBe('agent_budget_not_found'); }); - it('sysadmin_agent allocation is 10% of daily_tokens (10000 of 100000)', async () => { + it('sysadmin allocation is 10% of daily_tokens (10000 of 100000)', async () => { const pool = makePool([sysadminBudget(0)]); const result = await checkBudget( pool as unknown as Pool, - 'sysadmin_agent', + 'sysadmin', 100, ); expect(result.dailyTokens).toBe(10000); }); - it('db_admin_agent allocation is 5% of daily_tokens (5000 of 100000)', async () => { + it('db-admin allocation is 5% of daily_tokens (5000 of 100000)', async () => { const pool = makePool([dbaBudget(0)]); const result = await checkBudget( pool as unknown as Pool, - 'db_admin_agent', + 'db-admin', 100, ); expect(result.dailyTokens).toBe(5000); @@ -203,7 +203,7 @@ describe('Control Plane Budget Enforcement', () => { it('recording spend reduces remaining for agent', async () => { const b = sysadminBudget(0); const pool = makePool([b]); - await recordTokenSpend(pool as unknown as Pool, 'sysadmin_agent', 3000); + await recordTokenSpend(pool as unknown as Pool, 'sysadmin', 3000); expect(b.spent_today).toBe(3000); expect(b.hard_limit_exceeded).toBe(false); }); @@ -211,14 +211,14 @@ describe('Control Plane Budget Enforcement', () => { it('recording spend above remaining sets hard_limit_exceeded', async () => { const b = sysadminBudget(9000); const pool = makePool([b]); - await recordTokenSpend(pool as unknown as Pool, 'sysadmin_agent', 2000); + await recordTokenSpend(pool as unknown as Pool, 'sysadmin', 2000); expect(b.hard_limit_exceeded).toBe(true); }); it('spent_today never exceeds daily_tokens after recording', async () => { const b = sysadminBudget(9000); const pool = makePool([b]); - await recordTokenSpend(pool as unknown as Pool, 'sysadmin_agent', 5000); + await recordTokenSpend(pool as unknown as Pool, 'sysadmin', 5000); expect(b.spent_today).toBe(10000); expect(b.spent_today).toBeLessThanOrEqual(b.daily_tokens); }); @@ -258,11 +258,11 @@ describe('Control Plane Budget Enforcement', () => { }); describe('approval threshold', () => { - it('operations > 2000 tokens require operator approval (sysadmin_agent)', async () => { + it('operations > 2000 tokens require operator approval (sysadmin)', async () => { const pool = makePool([sysadminBudget(0)]); const result = await checkBudget( pool as unknown as Pool, - 'sysadmin_agent', + 'sysadmin', 2500, false, ); @@ -270,11 +270,11 @@ describe('Control Plane Budget Enforcement', () => { expect(result.reason).toBe('approval_required'); }); - it('operations > 3000 tokens require operator approval (db_admin_agent)', async () => { + it('operations > 3000 tokens require operator approval (db-admin)', async () => { const pool = makePool([dbaBudget(0)]); const result = await checkBudget( pool as unknown as Pool, - 'db_admin_agent', + 'db-admin', 3500, false, ); @@ -286,17 +286,17 @@ describe('Control Plane Budget Enforcement', () => { const pool = makePool([sysadminBudget(0)]); const result = await checkBudget( pool as unknown as Pool, - 'sysadmin_agent', + 'sysadmin', 5000, true, ); expect(result.allowed).toBe(true); }); - it('operations > 2000 tokens require operator approval (git_admin_agent)', async () => { + it('operations > 2000 tokens require operator approval (git-admin)', async () => { const pool = makePool([ { - agent_id: 'git_admin_agent', + agent_id: 'git-admin', daily_tokens: 10000, spent_today: 0, remaining: 10000, @@ -306,7 +306,7 @@ describe('Control Plane Budget Enforcement', () => { ]); const result = await checkBudget( pool as unknown as Pool, - 'git_admin_agent', + 'git-admin', 2500, false, ); @@ -318,7 +318,7 @@ describe('Control Plane Budget Enforcement', () => { const pool = makePool([sysadminBudget(0)]); const result = await checkBudget( pool as unknown as Pool, - 'sysadmin_agent', + 'sysadmin', 10000, true, ); diff --git a/src/controlplane-budget.ts b/src/controlplane-budget.ts index c000d9e..5aa7c8a 100644 --- a/src/controlplane-budget.ts +++ b/src/controlplane-budget.ts @@ -29,9 +29,9 @@ export interface AgentThresholds { // ── Constants ────────────────────────────────────────────────────────────── export const DEFAULT_APPROVAL_THRESHOLDS: AgentThresholds = { - sysadmin_agent: 2000, - db_admin_agent: 3000, - git_admin_agent: 2000, + sysadmin: 2000, + 'db-admin': 3000, + 'git-admin': 2000, orchestrator: Infinity, }; diff --git a/src/controlplane-db.test.ts b/src/controlplane-db.test.ts index 48dc4de..4af181f 100644 --- a/src/controlplane-db.test.ts +++ b/src/controlplane-db.test.ts @@ -53,9 +53,9 @@ describe('getDefaultAgents', () => { const agents = getDefaultAgents('clawdie'); const ids = agents.map((a) => a.id); expect(ids).toContain('clawdie'); - expect(ids).toContain('sysadmin_agent'); - expect(ids).toContain('db_admin_agent'); - expect(ids).toContain('git_admin_agent'); + expect(ids).toContain('sysadmin'); + expect(ids).toContain('db-admin'); + expect(ids).toContain('git-admin'); expect(ids).toContain('coordinator'); }); @@ -65,9 +65,9 @@ describe('getDefaultAgents', () => { expect(total).toBe(100); }); - it('sysadmin_agent has heartbeat_enabled', () => { + it('sysadmin has heartbeat_enabled', () => { const agents = getDefaultAgents('clawdie'); - const sysadmin = agents.find((a) => a.id === 'sysadmin_agent'); + const sysadmin = agents.find((a) => a.id === 'sysadmin'); expect(sysadmin?.heartbeat_enabled).toBe(true); }); diff --git a/src/controlplane-db.ts b/src/controlplane-db.ts index 5dd2498..cdcd479 100644 --- a/src/controlplane-db.ts +++ b/src/controlplane-db.ts @@ -16,9 +16,6 @@ import { AGENT_NAME } from './config.js'; export type AgentRole = | 'orchestrator' - | 'sysadmin_agent' - | 'db_admin_agent' - | 'git_admin_agent' | 'sysadmin' | 'db-admin' | 'git-admin' @@ -88,24 +85,24 @@ export function getDefaultAgents( budget_allocation_pct: 80, }, { - id: 'sysadmin_agent', - role: 'sysadmin_agent', + id: 'sysadmin', + role: 'sysadmin', adapter: 'pi-local', heartbeat_enabled: true, heartbeat_interval_sec: 86400, budget_allocation_pct: 10, }, { - id: 'db_admin_agent', - role: 'db_admin_agent', + id: 'db-admin', + role: 'db-admin', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, budget_allocation_pct: 5, }, { - id: 'git_admin_agent', - role: 'git_admin_agent', + id: 'git-admin', + role: 'git-admin', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, @@ -129,7 +126,7 @@ export const DEFAULT_AGENTS = getDefaultAgents(AGENT_NAME); export const CONTROLPLANE_SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS agents ( id TEXT PRIMARY KEY, - role TEXT NOT NULL CHECK (role IN ('orchestrator','sysadmin_agent','db_admin_agent','git_admin_agent','coordinator')), + role TEXT NOT NULL CHECK (role IN ('orchestrator','sysadmin','db-admin','git-admin','coordinator')), adapter TEXT NOT NULL DEFAULT 'pi-local', heartbeat_enabled BOOLEAN NOT NULL DEFAULT FALSE, heartbeat_interval_sec INTEGER, @@ -187,7 +184,7 @@ CREATE TABLE IF NOT EXISTS operators ( export async function runSchemaMigration(pool: pg.Pool): Promise { await pool.query(CONTROLPLANE_SCHEMA_SQL); - await cleanupLegacyAgents(pool); + await migrateLegacyAgentIds(pool); await ensureRoleConstraint(pool); await ensureApprovalDecisionColumn(pool); await ensureParentTaskId(pool); @@ -211,28 +208,75 @@ export async function runSchemaMigration(pool: pg.Pool): Promise { } -async function cleanupLegacyAgents(pool: pg.Pool): Promise { - // One-time cleanup: remove legacy agent IDs that predate the current naming. - // After this runs once, all subsequent calls are no-ops (cheap SELECTs). - const legacyIds = ['sysadmin', 'db-admin', 'git-admin', 'dba', 'git_admin', 'ceo']; +async function migrateLegacyAgentIds(pool: pg.Pool): Promise { + // Aggressive dev policy: canonical agent IDs are: + // sysadmin, db-admin, git-admin, coordinator, plus orchestrator = AGENT_NAME. + // Migrate old *_agent IDs in-place once (idempotent). + const legacyToCanonical: Array<[string, string]> = [ + ['sysadmin_agent', 'sysadmin'], + ['db_admin_agent', 'db-admin'], + ['git_admin_agent', 'git-admin'], + ]; + + const legacyIds = legacyToCanonical.map(([legacy]) => legacy); const { rows } = await pool.query( `SELECT id FROM agents WHERE id = ANY($1)`, [legacyIds], ); if (rows.length === 0) return; - // Drop FKs, clean up, re-add. - await pool.query('ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_role_check'); - for (const table of ['tasks', 'agent_activity', 'agent_budgets', 'approvals']) { - await pool.query(`ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${table}_${table === 'tasks' ? 'assigned_to' : 'agent_id'}_fkey`); - } + await pool.query('BEGIN'); + try { + await pool.query( + 'ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_role_check', + ); + for (const table of [ + 'tasks', + 'agent_activity', + 'agent_budgets', + 'approvals', + ]) { + await pool.query( + `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${table}_${table === 'tasks' ? 'assigned_to' : 'agent_id'}_fkey`, + ); + } - for (const id of legacyIds) { - await pool.query('DELETE FROM agent_budgets WHERE agent_id = $1', [id]); - await pool.query('DELETE FROM agent_activity WHERE agent_id = $1', [id]); - await pool.query('DELETE FROM approvals WHERE agent_id = $1', [id]); - await pool.query('UPDATE tasks SET assigned_to = NULL WHERE assigned_to = $1', [id]); - await pool.query('DELETE FROM agents WHERE id = $1', [id]); + for (const [legacy, canonical] of legacyToCanonical) { + await pool.query( + 'UPDATE tasks SET assigned_to = $1 WHERE assigned_to = $2', + [canonical, legacy], + ); + await pool.query( + 'UPDATE agent_activity SET agent_id = $1 WHERE agent_id = $2', + [canonical, legacy], + ); + await pool.query( + 'UPDATE agent_budgets SET agent_id = $1 WHERE agent_id = $2', + [canonical, legacy], + ); + await pool.query( + 'UPDATE approvals SET agent_id = $1 WHERE agent_id = $2', + [canonical, legacy], + ); + + const canonicalExists = await pool.query( + 'SELECT 1 FROM agents WHERE id = $1 LIMIT 1', + [canonical], + ); + if (canonicalExists.rows.length > 0) { + await pool.query('DELETE FROM agents WHERE id = $1', [legacy]); + } else { + await pool.query( + 'UPDATE agents SET id = $1, role = $1 WHERE id = $2', + [canonical, legacy], + ); + } + } + + await pool.query('COMMIT'); + } catch (err) { + await pool.query('ROLLBACK'); + throw err; } } @@ -243,7 +287,7 @@ async function ensureRoleConstraint(pool: pg.Pool): Promise { await pool.query( `ALTER TABLE agents ADD CONSTRAINT agents_role_check - CHECK (role IN ('orchestrator','sysadmin_agent','db_admin_agent','git_admin_agent','coordinator'))`, + CHECK (role IN ('orchestrator','sysadmin','db-admin','git-admin','coordinator'))`, ); } diff --git a/src/controlplane-heartbeat.test.ts b/src/controlplane-heartbeat.test.ts index 41a9a85..1aee0d1 100644 --- a/src/controlplane-heartbeat.test.ts +++ b/src/controlplane-heartbeat.test.ts @@ -45,7 +45,7 @@ describe('runAgentHeartbeat — no task', () => { const config = makeConfig(); const result = await runAgentHeartbeat( config, - 'sysadmin_agent', + 'sysadmin', 'interval_elapsed', ); expect(result.woke).toBe(false); @@ -55,7 +55,7 @@ describe('runAgentHeartbeat — no task', () => { const config = makeConfig(); const result = await runAgentHeartbeat( config, - 'sysadmin_agent', + 'sysadmin', 'telegram', ); expect(result.woke).toBe(false); diff --git a/src/controlplane-runner.test.ts b/src/controlplane-runner.test.ts index cfae252..748def8 100644 --- a/src/controlplane-runner.test.ts +++ b/src/controlplane-runner.test.ts @@ -23,7 +23,7 @@ function makeOpts( overrides: Partial = {}, ): ControlplaneRunOptions { return { - agentId: 'sysadmin_agent', + agentId: 'sysadmin', agentName: 'clawdie', taskId: 'TASK-001', apiKey: 'test-api-key', @@ -38,9 +38,9 @@ describe('Control Plane Runner — env injection', () => { describe('CONTROLPLANE_* env vars', () => { it('injects CONTROLPLANE_AGENT_ID', () => { const result = buildControlplaneRunCommand( - makeOpts({ agentId: 'sysadmin_agent' }), + makeOpts({ agentId: 'sysadmin' }), ); - expect(result.env.CONTROLPLANE_AGENT_ID).toBe('sysadmin_agent'); + expect(result.env.CONTROLPLANE_AGENT_ID).toBe('sysadmin'); }); it('injects CONTROLPLANE_API_URL as http://localhost:3100', () => { @@ -86,10 +86,10 @@ describe('Control Plane Runner — env injection', () => { it('session file path ends with {agent_name}.jsonl', () => { const result = buildControlplaneRunCommand( - makeOpts({ agentId: 'db_admin_agent' }), + makeOpts({ agentId: 'db-admin' }), ); const sessionArg = result.args.find((a) => - a.endsWith('db_admin_agent.jsonl'), + a.endsWith('db-admin.jsonl'), ); expect(sessionArg).toBeDefined(); }); @@ -115,11 +115,11 @@ describe('Control Plane Runner — env injection', () => { it('args include --session with correct project-relative path', () => { const result = buildControlplaneRunCommand( - makeOpts({ agentId: 'sysadmin_agent' }), + makeOpts({ agentId: 'sysadmin' }), ); const idx = result.args.indexOf('--session'); expect(idx).toBeGreaterThanOrEqual(0); - expect(result.args[idx + 1]).toContain('sysadmin_agent.jsonl'); + expect(result.args[idx + 1]).toContain('sysadmin.jsonl'); }); it('args include --no-skills to disable pi skill discovery', () => { @@ -130,7 +130,7 @@ describe('Control Plane Runner — env injection', () => { it('args include --append-system-prompt with identity content', () => { const result = buildControlplaneRunCommand( - makeOpts({ agentId: 'sysadmin_agent' }), + makeOpts({ agentId: 'sysadmin' }), ); const idx = result.args.indexOf('--append-system-prompt'); expect(idx).toBeGreaterThanOrEqual(0); @@ -171,25 +171,25 @@ describe('Control Plane Runner — env injection', () => { }); describe('resolveIdentityFile', () => { - it('maps sysadmin_agent to SYSADMIN identity content', () => { + it('maps sysadmin to SYSADMIN identity content', () => { const result = buildControlplaneRunCommand( - makeOpts({ agentId: 'sysadmin_agent' }), + makeOpts({ agentId: 'sysadmin' }), ); const promptIdx = result.args.indexOf('--append-system-prompt'); expect(result.args[promptIdx + 1]).toContain('Sysadmin Agent'); }); - it('maps db_admin_agent to DB_ADMIN identity content', () => { + it('maps db-admin to DB_ADMIN identity content', () => { const result = buildControlplaneRunCommand( - makeOpts({ agentId: 'db_admin_agent' }), + makeOpts({ agentId: 'db-admin' }), ); const promptIdx = result.args.indexOf('--append-system-prompt'); expect(result.args[promptIdx + 1]).toContain('DB Admin Agent'); }); - it('maps git_admin_agent to GIT_ADMIN identity content', () => { + it('maps git-admin to GIT_ADMIN identity content', () => { const result = buildControlplaneRunCommand( - makeOpts({ agentId: 'git_admin_agent' }), + makeOpts({ agentId: 'git-admin' }), ); const promptIdx = result.args.indexOf('--append-system-prompt'); expect(result.args[promptIdx + 1]).toContain('Git Admin Agent'); @@ -204,17 +204,14 @@ describe('Control Plane Runner — env injection', () => { describe('AGENT_JAIL_MAP', () => { it('sysadmin runs on host (null)', () => { expect(AGENT_JAIL_MAP['sysadmin']).toBeNull(); - expect(AGENT_JAIL_MAP['sysadmin_agent']).toBeNull(); }); it('db-admin routes to db-worker jail', () => { expect(AGENT_JAIL_MAP['db-admin']).toBe('db-worker'); - expect(AGENT_JAIL_MAP['db_admin_agent']).toBe('db-worker'); }); it('git-admin routes to git-worker jail', () => { expect(AGENT_JAIL_MAP['git-admin']).toBe('git-worker'); - expect(AGENT_JAIL_MAP['git_admin_agent']).toBe('git-worker'); }); it('coordinator routes to ctrl-worker jail', () => { diff --git a/src/controlplane-runner.ts b/src/controlplane-runner.ts index de1245c..0d07708 100644 --- a/src/controlplane-runner.ts +++ b/src/controlplane-runner.ts @@ -46,11 +46,7 @@ export interface RunResult { const CONTROLPLANE_API_URL = `http://localhost:${CONTROLPLANE_API_PORT}`; const AGENT_IDENTITY_FILES: Record = { - // Legacy agent IDs (pre-Phase 6) - sysadmin_agent: '.agent/identities/SYSADMIN.md', - db_admin_agent: '.agent/identities/DB_ADMIN.md', - git_admin_agent: '.agent/identities/GIT_ADMIN.md', - // Phase 6 canonical IDs (as defined in agent/library.yaml) + // Canonical IDs (as defined in agent/library.yaml) sysadmin: '.agent/identities/SYSADMIN.md', 'db-admin': '.agent/identities/DB_ADMIN.md', 'git-admin': '.agent/identities/GIT_ADMIN.md', @@ -58,14 +54,9 @@ const AGENT_IDENTITY_FILES: Record = { }; export const AGENT_JAIL_MAP: Record = { - // Phase 6 canonical IDs (as defined in agent/library.yaml) sysadmin: null, 'db-admin': 'db-worker', 'git-admin': 'git-worker', - // Legacy agent IDs (pre-Phase 6) - sysadmin_agent: null, - db_admin_agent: 'db-worker', - git_admin_agent: 'git-worker', coordinator: 'ctrl-worker', }; diff --git a/src/controlplane-setup.test.ts b/src/controlplane-setup.test.ts index 034f1c5..8a28174 100644 --- a/src/controlplane-setup.test.ts +++ b/src/controlplane-setup.test.ts @@ -49,22 +49,22 @@ describe('Control Plane Provisioning', () => { expect(main?.role).toBe('orchestrator'); }); - it('DEFAULT_AGENTS contains Sysadmin agent with role "sysadmin_agent"', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin_agent'); + it('DEFAULT_AGENTS contains Sysadmin agent with role "sysadmin"', () => { + const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin'); expect(agent).toBeDefined(); - expect(agent?.role).toBe('sysadmin_agent'); + expect(agent?.role).toBe('sysadmin'); }); - it('DEFAULT_AGENTS contains DBA agent with role "db_admin_agent"', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'db_admin_agent'); + it('DEFAULT_AGENTS contains DBA agent with role "db-admin"', () => { + const agent = DEFAULT_AGENTS.find((a) => a.id === 'db-admin'); expect(agent).toBeDefined(); - expect(agent?.role).toBe('db_admin_agent'); + expect(agent?.role).toBe('db-admin'); }); - it('DEFAULT_AGENTS contains Git Admin agent with role "git_admin_agent"', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'git_admin_agent'); + it('DEFAULT_AGENTS contains Git Admin agent with role "git-admin"', () => { + const agent = DEFAULT_AGENTS.find((a) => a.id === 'git-admin'); expect(agent).toBeDefined(); - expect(agent?.role).toBe('git_admin_agent'); + expect(agent?.role).toBe('git-admin'); }); it('all agents use adapter "pi-local"', () => { @@ -86,19 +86,19 @@ describe('Control Plane Provisioning', () => { }); it('Sysadmin has heartbeat_enabled = true and interval = 86400', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin_agent')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin')!; expect(agent.heartbeat_enabled).toBe(true); expect(agent.heartbeat_interval_sec).toBe(86400); }); it('DBA has heartbeat_enabled = false', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'db_admin_agent')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'db-admin')!; expect(agent.heartbeat_enabled).toBe(false); expect(agent.heartbeat_interval_sec).toBeNull(); }); it('Git Admin has heartbeat_enabled = false', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'git_admin_agent')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'git-admin')!; expect(agent.heartbeat_enabled).toBe(false); expect(agent.heartbeat_interval_sec).toBeNull(); }); @@ -111,17 +111,17 @@ describe('Control Plane Provisioning', () => { }); it('Sysadmin gets 10% allocation', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin_agent')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin')!; expect(agent.budget_allocation_pct).toBe(10); }); it('DBA gets 5% allocation', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'db_admin_agent')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'db-admin')!; expect(agent.budget_allocation_pct).toBe(5); }); it('Git Admin gets 5% allocation', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'git_admin_agent')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'git-admin')!; expect(agent.budget_allocation_pct).toBe(5); }); diff --git a/src/controlplane-telegram.test.ts b/src/controlplane-telegram.test.ts index e42efda..0753fad 100644 --- a/src/controlplane-telegram.test.ts +++ b/src/controlplane-telegram.test.ts @@ -132,7 +132,7 @@ describe('Telegram → Control Plane Bridge', () => { }); describe('agent routing', () => { - it('routes jail messages to sysadmin_agent', async () => { + it('routes jail messages to sysadmin', async () => { const pool = makePool(); const result = await bridgeTelegramMessage( pool, @@ -140,10 +140,10 @@ describe('Telegram → Control Plane Bridge', () => { 'tg:test', 'Check jail status', ); - expect(result.assignedTo).toBe('sysadmin_agent'); + expect(result.assignedTo).toBe('sysadmin'); }); - it('routes database messages to db_admin_agent', async () => { + it('routes database messages to db-admin', async () => { const pool = makePool(); const result = await bridgeTelegramMessage( pool, @@ -151,10 +151,10 @@ describe('Telegram → Control Plane Bridge', () => { 'tg:test', 'backup the database', ); - expect(result.assignedTo).toBe('db_admin_agent'); + expect(result.assignedTo).toBe('db-admin'); }); - it('routes git messages to git_admin_agent', async () => { + it('routes git messages to git-admin', async () => { const pool = makePool(); const result = await bridgeTelegramMessage( pool, @@ -162,7 +162,7 @@ describe('Telegram → Control Plane Bridge', () => { 'tg:test', 'check git repo', ); - expect(result.assignedTo).toBe('git_admin_agent'); + expect(result.assignedTo).toBe('git-admin'); }); it('routes unrecognised agent messages to orchestrator', async () => { @@ -176,7 +176,7 @@ describe('Telegram → Control Plane Bridge', () => { expect(result.assignedTo).toBe('clawdie'); }); - it('routes zfs messages to sysadmin_agent', async () => { + it('routes zfs messages to sysadmin', async () => { const pool = makePool(); const result = await bridgeTelegramMessage( pool, @@ -184,10 +184,10 @@ describe('Telegram → Control Plane Bridge', () => { 'tg:test', 'take a zfs snapshot', ); - expect(result.assignedTo).toBe('sysadmin_agent'); + expect(result.assignedTo).toBe('sysadmin'); }); - it('routes sql messages to db_admin_agent', async () => { + it('routes sql messages to db-admin', async () => { const pool = makePool(); const result = await bridgeTelegramMessage( pool, @@ -195,7 +195,7 @@ describe('Telegram → Control Plane Bridge', () => { 'tg:test', 'run this SQL query', ); - expect(result.assignedTo).toBe('db_admin_agent'); + expect(result.assignedTo).toBe('db-admin'); }); }); @@ -241,7 +241,7 @@ describe('Telegram → Control Plane Bridge', () => { 'tg:test', 'Check jail status', ); - expect(insertedTasks[0].assigned_to).toBe('sysadmin_agent'); + expect(insertedTasks[0].assigned_to).toBe('sysadmin'); }); it('always creates a task even without skill match', async () => { diff --git a/src/controlplane-telegram.ts b/src/controlplane-telegram.ts index 32c2406..6c7abb0 100644 --- a/src/controlplane-telegram.ts +++ b/src/controlplane-telegram.ts @@ -28,7 +28,7 @@ export interface TelegramBridgeResult { const ROLE_KEYWORDS: Array<{ role: string; patterns: RegExp[] }> = [ { - role: 'db_admin_agent', + role: 'db-admin', patterns: [ /\bdatabase\b/i, /\bpostgres\b/i, @@ -43,7 +43,7 @@ const ROLE_KEYWORDS: Array<{ role: string; patterns: RegExp[] }> = [ ], }, { - role: 'git_admin_agent', + role: 'git-admin', patterns: [ /\bgit\b/i, /\brepo\b/i, @@ -57,7 +57,7 @@ const ROLE_KEYWORDS: Array<{ role: string; patterns: RegExp[] }> = [ ], }, { - role: 'sysadmin_agent', + role: 'sysadmin', patterns: [ /jail/i, /service/i, diff --git a/src/controlplane.test.ts b/src/controlplane.test.ts index 5db12b2..79e5eee 100644 --- a/src/controlplane.test.ts +++ b/src/controlplane.test.ts @@ -51,20 +51,20 @@ afterEach(() => { const defaultAgents: Agent[] = [ { id: 'clawdie', role: 'orchestrator', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, budget_allocation_pct: 80, api_key_hash: null, created_at: new Date() }, - { id: 'sysadmin_agent', role: 'sysadmin_agent', adapter: 'pi-local', heartbeat_enabled: true, heartbeat_interval_sec: 86400, budget_allocation_pct: 10, api_key_hash: null, created_at: new Date() }, - { id: 'db_admin_agent', role: 'db_admin_agent', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, budget_allocation_pct: 5, api_key_hash: null, created_at: new Date() }, - { id: 'git_admin_agent', role: 'git_admin_agent', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, budget_allocation_pct: 5, api_key_hash: null, created_at: new Date() }, + { id: 'sysadmin', role: 'sysadmin', adapter: 'pi-local', heartbeat_enabled: true, heartbeat_interval_sec: 86400, budget_allocation_pct: 10, api_key_hash: null, created_at: new Date() }, + { id: 'db-admin', role: 'db-admin', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, budget_allocation_pct: 5, api_key_hash: null, created_at: new Date() }, + { id: 'git-admin', role: 'git-admin', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, budget_allocation_pct: 5, api_key_hash: null, created_at: new Date() }, ]; const defaultBudgets: Budget[] = [ { agent_id: 'clawdie', daily_tokens: 80000, spent_today: 0, remaining: 80000, hard_limit_exceeded: false, reset_at: new Date() }, - { agent_id: 'sysadmin_agent', daily_tokens: 10000, spent_today: 5000, remaining: 5000, hard_limit_exceeded: false, reset_at: new Date() }, - { agent_id: 'db_admin_agent', daily_tokens: 5000, spent_today: 0, remaining: 5000, hard_limit_exceeded: false, reset_at: new Date() }, - { agent_id: 'git_admin_agent', daily_tokens: 5000, spent_today: 0, remaining: 5000, hard_limit_exceeded: false, reset_at: new Date() }, + { agent_id: 'sysadmin', daily_tokens: 10000, spent_today: 5000, remaining: 5000, hard_limit_exceeded: false, reset_at: new Date() }, + { agent_id: 'db-admin', daily_tokens: 5000, spent_today: 0, remaining: 5000, hard_limit_exceeded: false, reset_at: new Date() }, + { agent_id: 'git-admin', daily_tokens: 5000, spent_today: 0, remaining: 5000, hard_limit_exceeded: false, reset_at: new Date() }, ]; const defaultTasks: Task[] = [ - { id: 'TASK-001', title: 'Check if db jail is running', description: 'Verify db jail is up', assigned_to: 'sysadmin_agent', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'TASK-001', title: 'Check if db jail is running', description: 'Verify db jail is up', assigned_to: 'sysadmin', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, ]; let activityLog: Array<{ agent_id: string; event_type: string; payload: Record | null; tokens_used: number | null }> = []; @@ -108,15 +108,15 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { it('queries control plane and attempts agent spawn', async () => { const pool = makePool(); const config = makeConfig(pool); - const result = await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); + const result = await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); expect(result.woke).toBe(true); - expect(result.agentId).toBe('sysadmin_agent'); + expect(result.agentId).toBe('sysadmin'); }); it('receives task assignment via tasks query', async () => { const pool = makePool(); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin_agent', 'assignment', 'TASK-001'); + await runAgentHeartbeat(config, 'sysadmin', 'assignment', 'TASK-001'); const taskQuery = vi.mocked(pool.query).mock.calls.find((c) => /FROM tasks/i.test(c[0] as string)); expect(taskQuery).toBeDefined(); }); @@ -124,23 +124,23 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { it('posts activity event after execution (success or error)', async () => { const pool = makePool(); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); + await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); expect(activityLog.length).toBeGreaterThan(0); }); it('activity event includes agent_id', async () => { const pool = makePool(); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); - const events = activityLog.filter((e) => e.agent_id === 'sysadmin_agent'); + await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); + const events = activityLog.filter((e) => e.agent_id === 'sysadmin'); expect(events.length).toBeGreaterThan(0); }); it('session JSONL is updated after completion', async () => { const pool = makePool(); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); - const sessionFile = path.join(sessionDir, 'sysadmin_agent.jsonl'); + await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); + const sessionFile = path.join(sessionDir, 'sysadmin.jsonl'); if (fs.existsSync(sessionFile)) { const content = fs.readFileSync(sessionFile, 'utf-8'); expect(content.length).toBeGreaterThan(0); @@ -150,22 +150,22 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { describe('DBA on-demand task', () => { const dbaTasks: Task[] = [ - { id: 'TASK-002', title: 'Back up database', description: 'Full backup', assigned_to: 'db_admin_agent', priority: 'high', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'TASK-002', title: 'Back up database', description: 'Full backup', assigned_to: 'db-admin', priority: 'high', status: 'pending', created_at: new Date(), deadline: null, context: null }, ]; it('DBA is woken for assignment task', async () => { const pool = makePool(defaultAgents, defaultBudgets, dbaTasks); const config = makeConfig(pool); - const result = await runAgentHeartbeat(config, 'db_admin_agent', 'assignment', 'TASK-002'); + const result = await runAgentHeartbeat(config, 'db-admin', 'assignment', 'TASK-002'); expect(result.woke).toBe(true); }); it('DBA queries tasks and finds its task', async () => { const pool = makePool(defaultAgents, defaultBudgets, dbaTasks); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'db_admin_agent', 'assignment', 'TASK-002'); + await runAgentHeartbeat(config, 'db-admin', 'assignment', 'TASK-002'); const taskQuery = vi.mocked(pool.query).mock.calls.find((c) => - /FROM tasks/i.test(c[0] as string) && (c[1] as unknown[])?.[0] === 'db_admin_agent', + /FROM tasks/i.test(c[0] as string) && (c[1] as unknown[])?.[0] === 'db-admin', ); expect(taskQuery).toBeDefined(); }); @@ -173,8 +173,8 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { it('DBA posts task_completed with result', async () => { const pool = makePool(defaultAgents, defaultBudgets, dbaTasks); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'db_admin_agent', 'assignment', 'TASK-002'); - const completed = activityLog.filter((e) => e.event_type === 'task_completed' && e.agent_id === 'db_admin_agent'); + await runAgentHeartbeat(config, 'db-admin', 'assignment', 'TASK-002'); + const completed = activityLog.filter((e) => e.event_type === 'task_completed' && e.agent_id === 'db-admin'); expect(completed.length).toBeGreaterThanOrEqual(0); }); }); @@ -182,22 +182,22 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { describe('budget exhaustion during cycle', () => { it('scheduler does not wake agent when budget is exhausted', async () => { const exhaustedBudgets: Budget[] = [ - { agent_id: 'sysadmin_agent', daily_tokens: 10000, spent_today: 10000, remaining: 0, hard_limit_exceeded: true, reset_at: new Date() }, + { agent_id: 'sysadmin', daily_tokens: 10000, spent_today: 10000, remaining: 0, hard_limit_exceeded: true, reset_at: new Date() }, ]; const pool = makePool(defaultAgents, exhaustedBudgets, defaultTasks); const config = makeConfig(pool); - const result = await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); + const result = await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); expect(result.woke).toBe(false); expect(result.reason).toBe('hard_limit_exceeded'); }); it('scheduler posts error event instead of spawning', async () => { const exhaustedBudgets: Budget[] = [ - { agent_id: 'sysadmin_agent', daily_tokens: 10000, spent_today: 10000, remaining: 0, hard_limit_exceeded: true, reset_at: new Date() }, + { agent_id: 'sysadmin', daily_tokens: 10000, spent_today: 10000, remaining: 0, hard_limit_exceeded: true, reset_at: new Date() }, ]; const pool = makePool(defaultAgents, exhaustedBudgets, defaultTasks); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); + await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); const errorEvents = activityLog.filter((e) => e.event_type === 'error'); expect(errorEvents.length).toBeGreaterThan(0); }); @@ -207,14 +207,14 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { it('pi spawn failure posts error event to control plane', async () => { const pool = makePool(); const config = makeConfig(pool); - const result = await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); - expect(result.agentId).toBe('sysadmin_agent'); + const result = await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); + expect(result.agentId).toBe('sysadmin'); }); it('error event includes action_taken field', async () => { const pool = makePool(); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); + await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); if (activityLog.length > 0) { const hasActionTaken = activityLog.some((e) => { if (e.payload && typeof e.payload === 'object') return 'action_taken' in e.payload; @@ -227,7 +227,7 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { it('scheduler does not crash on agent error (continues to next tick)', async () => { const pool = makePool(); const config = makeConfig(pool); - const result = await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); + const result = await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); expect(result).toBeDefined(); expect(result).toHaveProperty('agentId'); }); @@ -235,20 +235,20 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { describe('Telegram task bridge', () => { const telegramTasks: Task[] = [ - { id: 'TASK-TG1', title: 'Check jails for Telegram user', description: 'From main chat', assigned_to: 'sysadmin_agent', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'TASK-TG1', title: 'Check jails for Telegram user', description: 'From main chat', assigned_to: 'sysadmin', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, ]; it('incoming task is processable by agent', async () => { const pool = makePool(defaultAgents, defaultBudgets, telegramTasks); const config = makeConfig(pool); - const result = await runAgentHeartbeat(config, 'sysadmin_agent', 'telegram', 'TASK-TG1'); - expect(result.agentId).toBe('sysadmin_agent'); + const result = await runAgentHeartbeat(config, 'sysadmin', 'telegram', 'TASK-TG1'); + expect(result.agentId).toBe('sysadmin'); }); it('task completion posts activity event', async () => { const pool = makePool(defaultAgents, defaultBudgets, telegramTasks); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin_agent', 'telegram', 'TASK-TG1'); + await runAgentHeartbeat(config, 'sysadmin', 'telegram', 'TASK-TG1'); expect(activityLog.length).toBeGreaterThan(0); }); }); diff --git a/src/skills-discovery.test.ts b/src/skills-discovery.test.ts index 4a72570..9d2b044 100644 --- a/src/skills-discovery.test.ts +++ b/src/skills-discovery.test.ts @@ -44,7 +44,7 @@ describe('Skills Discovery', () => { }); }); - describe('skill pattern matching — sysadmin_agent', () => { + describe('skill pattern matching — sysadmin', () => { const sysadminSkills: Skill[] = [ { name: 'jail-status', @@ -95,9 +95,9 @@ describe('Skills Discovery', () => { ).toBe('jail-status'); }); - it('"Is sysadmin_agent-db up?" → jail-status', () => { + it('"Is sysadmin-db up?" → jail-status', () => { expect( - matchTaskToSkill('Is sysadmin_agent-db up?', sysadminSkills).skill! + matchTaskToSkill('Is sysadmin-db up?', sysadminSkills).skill! .name, ).toBe('jail-status'); }); @@ -141,7 +141,7 @@ describe('Skills Discovery', () => { }); }); - describe('skill pattern matching — db_admin_agent', () => { + describe('skill pattern matching — db-admin', () => { const dbaSkills: Skill[] = [ { name: 'db-vacuum',