From f14f8556ffc046423a721d40469dc137cf09ed25 Mon Sep 17 00:00:00 2001 From: Clawdie AI Date: Wed, 8 Apr 2026 19:32:10 +0000 Subject: [PATCH] refactor(controlplane): rename subagent ids --- CHANGELOG.md | 4 +- DBA.md => DB_ADMIN_AGENT.md | 12 ++-- GIT_ADMIN.md => GIT_ADMIN_AGENT.md | 12 ++-- MEMORY.md | 2 +- SYSADMIN.md => SYSADMIN_AGENT.md | 18 +++--- doc/CONTROLPLANE-AGENT-ROLES.md | 8 +-- doc/CONTROLPLANE-IMPLEMENTATION-PLAN.md | 4 +- doc/CONTROLPLANE-MESSAGE-CONTRACT.md | 24 ++++---- doc/DASHBOARD-PHASE-D-HANDOFF.md | 2 +- doc/MULTI-PROVIDER-ARCHITECTURE.md | 2 +- docs/internal/LOCAL-LLM.md | 2 +- docs/internal/REFACTOR-PLAN.md | 2 +- docs/public/architecture/controlplane.md | 4 +- src/agent-session.test.ts | 70 ++++++++++++------------ src/controlplane-api.test.ts | 32 +++++------ src/controlplane-budget.test.ts | 44 +++++++-------- src/controlplane-budget.ts | 6 +- src/controlplane-db.ts | 16 +++--- src/controlplane-runner.test.ts | 18 +++--- src/controlplane-runner.ts | 6 +- src/controlplane-setup.test.ts | 30 +++++----- src/controlplane-telegram.test.ts | 16 +++--- src/controlplane-telegram.ts | 8 +-- src/controlplane.test.ts | 66 +++++++++++----------- src/skills-discovery.test.ts | 8 +-- 25 files changed, 208 insertions(+), 208 deletions(-) rename DBA.md => DB_ADMIN_AGENT.md (96%) rename GIT_ADMIN.md => GIT_ADMIN_AGENT.md (96%) rename SYSADMIN.md => SYSADMIN_AGENT.md (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d612d42..07f0137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `doc/NAMING-HANDOFF.md` deleted — all checklist items complete (vitest 847/847 green). ### Renamed -- Control plane terminology sweep across docs: `CEO`/`'ceo'` → `orchestrator`, `company` → `system`, `Paperclip` → `Control Plane`, `board` → `operator`. Affects `SYSADMIN.md`, `DBA.md`, `GIT_ADMIN.md`, `doc/CONTROLPLANE-*.md`, `doc/MULTI-PROVIDER-ARCHITECTURE.md`, `docs/public/architecture/controlplane.md`. Code rename landed earlier in `061a25f`; this catches the doc tail. (`doc/NAMING-HANDOFF.md` remaining items: `CLAWDIE.md` creation + wiring `resolveIdentityFile()` — tracked for follow-up.) +- Control plane terminology sweep across docs: `CEO`/`'ceo'` → `orchestrator`, `company` → `system`, `Paperclip` → `Control Plane`, `board` → `operator`. Affects `SYSADMIN_AGENT.md`, `DB_ADMIN_AGENT.md`, `GIT_ADMIN_AGENT.md`, `doc/CONTROLPLANE-*.md`, `doc/MULTI-PROVIDER-ARCHITECTURE.md`, `docs/public/architecture/controlplane.md`. Code rename landed earlier in `061a25f`; this catches the doc tail. (`doc/NAMING-HANDOFF.md` remaining items: `CLAWDIE.md` creation + wiring `resolveIdentityFile()` — tracked for follow-up.) ### Added - `setup/agent-cli-check.ts` — fail-fast gate at the top of `setup onboard` requiring at least one of `claude`, `codex`, `gemini`, `pi` on `PATH` (mirrors Paperclip's per-adapter `ensureCommandResolvable` pattern as a single check) @@ -53,7 +53,7 @@ Major architectural addition: Paperclip as multi-agent orchestration layer for C - `doc/PAPERCLIP-INTEGRATION.md` — Full architecture, security model, deployment guide, operations, and troubleshooting for Paperclip control plane - `doc/PAPERCLIP-COMPANY-STRUCTURE.md` — Default organization chart (CEO + Sysadmin + DBA + Git Admin), role definitions, skills mapping, decision logic, approval workflows - `setup/paperclip.ts` — Jail provisioning for Paperclip at 10.0.0.2, company auto-provisioning, skills mounting -- Identity files: `SYSADMIN.md`, `DBA.md`, `GIT_ADMIN.md` — Agent role instructions with skill-aware decision logic (alongside existing `SOUL.md`) +- Identity files: `SYSADMIN_AGENT.md`, `DB_ADMIN_AGENT.md`, `GIT_ADMIN_AGENT.md` — Agent role instructions with skill-aware decision logic (alongside existing `SOUL.md`) - Operational skills: `jail-status`, `backup-db`, `disk-usage`, `service-restart`, `system-stats`, `db-migrate`, `db-vacuum`, `db-analyze`, `git-pull`, `git-merge`, `git-release-tag`, `git-branch-protect`, `git-push-mirror` - All 33 skills in `.agent/skills/` updated with `compatibility: FreeBSD 15.0+` marker in SKILL.md frontmatter - PostgreSQL integration — Paperclip connects to existing PostgreSQL jail at 10.0.0.3, shares database instance with Clawdie diff --git a/DBA.md b/DB_ADMIN_AGENT.md similarity index 96% rename from DBA.md rename to DB_ADMIN_AGENT.md index ea96c51..631adab 100644 --- a/DBA.md +++ b/DB_ADMIN_AGENT.md @@ -1,4 +1,4 @@ -# DBA.md - Data Steward +# DB_ADMIN_AGENT.md - Data Steward _Data is your responsibility. It's not just bytes — it's the system's memory, decisions, and trust._ @@ -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=dba +GET /api/controlplane/tasks?role=db_admin_agent → [ { task_id: "TASK-042", @@ -49,7 +49,7 @@ GET /api/controlplane/tasks?role=dba ### Layer 2: Clawdie (What Do I Know?) ``` -Read data/sessions/dba.jsonl +Read data/sessions/db_admin_agent.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": "dba", + "agent_id": "db_admin_agent", "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/dba.jsonl`. +Your session lives in `data/sessions/db_admin_agent.jsonl`. Each time you complete a job: ```json @@ -223,7 +223,7 @@ This context flows into your system prompt, making you more efficient over time. - `doc/CONTROLPLANE-MESSAGE-CONTRACT.md` — how you query the control plane API, how you post results - `doc/CONTROLPLANE-AGENT-ROLES.md` — your role in the org chart - `SOUL.md` — orchestrator's identity (your boss) -- `SYSADMIN.md` — infrastructure guardian (coordinates with you on backups, migrations) +- `SYSADMIN_AGENT.md` — infrastructure guardian (coordinates with you on backups, migrations) --- diff --git a/GIT_ADMIN.md b/GIT_ADMIN_AGENT.md similarity index 96% rename from GIT_ADMIN.md rename to GIT_ADMIN_AGENT.md index 0ebf2f0..2731ec7 100644 --- a/GIT_ADMIN.md +++ b/GIT_ADMIN_AGENT.md @@ -1,4 +1,4 @@ -# GIT_ADMIN.md - Version Steward +# GIT_ADMIN_AGENT.md - Version Steward _You own the history. Every commit is a decision. Every release is a promise._ @@ -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 +GET /api/controlplane/tasks?role=git_admin_agent → [ { task_id: "TASK-105", @@ -54,7 +54,7 @@ GET /api/controlplane/tasks?role=git_admin ### Layer 2: Clawdie (What Do I Know?) ``` -Read /home/clawdie/clawdie-ai/data/sessions/git_admin.jsonl +Read /home/clawdie/clawdie-ai/data/sessions/git_admin_agent.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_id": "git_admin_agent", "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.jsonl`. +Your session lives in `/home/clawdie/clawdie-ai/data/sessions/git_admin_agent.jsonl`. Each time you complete a job: ```json @@ -272,7 +272,7 @@ This context flows into your system prompt, making you faster and more confident - `doc/CONTROLPLANE-MESSAGE-CONTRACT.md` — how you query the control plane API, how you post results - `doc/CONTROLPLANE-AGENT-ROLES.md` — your role in the org chart - `SOUL.md` — orchestrator's identity (your boss) -- `SYSADMIN.md` — infrastructure guardian (sometimes coordinates with you on releases) +- `SYSADMIN_AGENT.md` — infrastructure guardian (sometimes coordinates with you on releases) - `CHANGELOG.md` — project changelog (you ensure it's updated before releases) --- diff --git a/MEMORY.md b/MEMORY.md index b4b5912..ee1bb15 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -10,7 +10,7 @@ - **Control Plane:** Multi-agent orchestrator running inside clawdie service (port 3100) - Roles: Orchestrator (80% budget), Sysadmin (10%, daily heartbeat), DBA (5%), Git Admin (5%) - Main agent id = AGENT_NAME (e.g., `clawdie`), role = `orchestrator` - - Identity files: `{AGENT_NAME}.md` (falls back to `SOUL.md`), `SYSADMIN.md`, `DBA.md`, `GIT_ADMIN.md` + - Identity files: `{AGENT_NAME}.md` (falls back to `SOUL.md`), `SYSADMIN_AGENT.md`, `DB_ADMIN_AGENT.md`, `GIT_ADMIN_AGENT.md` - 15 operational skills in `.agent/skills/` - **Telegram bridge:** Routes messages to control plane agents via keyword matching - **Dashboard feasibility:** Phases A & B green (Vite 8 + React 19 + Tailwind 4 + native Rust bindings all build on FreeBSD 15.0). Phases C & D pending. diff --git a/SYSADMIN.md b/SYSADMIN_AGENT.md similarity index 93% rename from SYSADMIN.md rename to SYSADMIN_AGENT.md index 4de331b..66f8afa 100644 --- a/SYSADMIN.md +++ b/SYSADMIN_AGENT.md @@ -1,4 +1,4 @@ -# SYSADMIN.md - Infrastructure Guardian +# SYSADMIN_AGENT.md - Infrastructure Guardian _You keep the machines running. You're methodical, preventive, and paranoid about breakage._ @@ -49,7 +49,7 @@ Same pattern: ### Layer 1: Control Plane (What's My Job?) ``` -GET /api/controlplane/tasks?role=sysadmin +GET /api/controlplane/tasks?role=sysadmin_agent → [ { 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 ### Layer 2: Clawdie (What Do I Know?) ``` -Read data/sessions/sysadmin.jsonl +Read data/sessions/sysadmin_agent.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-db is running (uptime 5d 3h)` | +| "Check if X jail is running" or "Is X up?" | `jail-status` | `sysadmin_agent-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-db: 2GB limit, 1.2GB used` | +| "Check RCTL/quotas" | `resource-limits` | `sysadmin_agent-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_id": "sysadmin_agent", "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.jsonl`. +Your session lives in `/home/clawdie/clawdie-ai/data/sessions/sysadmin_agent.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-db running, uptime 5d 3h, CPU 2%, RAM 512/2048MB", + "output": "sysadmin_agent-db running, uptime 5d 3h, CPU 2%, RAM 512/2048MB", "tokens_used": 420 } ``` -Next heartbeat, you read this file. If you see "sysadmin-db was running yesterday at 10:30," you know: +Next heartbeat, you read this file. If you see "sysadmin_agent-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-AGENT-ROLES.md b/doc/CONTROLPLANE-AGENT-ROLES.md index b8f64ab..47971f4 100644 --- a/doc/CONTROLPLANE-AGENT-ROLES.md +++ b/doc/CONTROLPLANE-AGENT-ROLES.md @@ -40,7 +40,7 @@ ## Sysadmin — Systems Administrator -**Identity:** `SYSADMIN.md` +**Identity:** `SYSADMIN_AGENT.md` **Adapter:** `pi-local`, model: `anthropic/claude-3-5-sonnet` - Monitor jail health, manage services, handle incidents @@ -63,7 +63,7 @@ ## DBA — Database Administrator -**Identity:** `DBA.md` +**Identity:** `DB_ADMIN_AGENT.md` **Adapter:** `pi-local`, model: `anthropic/claude-3-5-sonnet` - PostgreSQL operations: migrations, backups, performance tuning @@ -85,7 +85,7 @@ ## Git Admin — Git Administrator -**Identity:** `GIT_ADMIN.md` +**Identity:** `GIT_ADMIN_AGENT.md` **Adapter:** `pi-local`, model: `anthropic/claude-3-5-sonnet` - Manage repositories, branches, releases, merges @@ -121,4 +121,4 @@ Budget limit reached → Control plane pauses work → Operator must approve inc - `doc/CONTROLPLANE-ARCHITECTURE.md` — service architecture - `doc/CONTROLPLANE-MESSAGE-CONTRACT.md` — API contracts -- `SOUL.md`, `SYSADMIN.md`, `DBA.md`, `GIT_ADMIN.md` — agent identities +- `SOUL.md`, `SYSADMIN_AGENT.md`, `DB_ADMIN_AGENT.md`, `GIT_ADMIN_AGENT.md` — agent identities diff --git a/doc/CONTROLPLANE-IMPLEMENTATION-PLAN.md b/doc/CONTROLPLANE-IMPLEMENTATION-PLAN.md index 2a331bd..c40c29b 100644 --- a/doc/CONTROLPLANE-IMPLEMENTATION-PLAN.md +++ b/doc/CONTROLPLANE-IMPLEMENTATION-PLAN.md @@ -9,7 +9,7 @@ Handoff for next agent. Read listed files before touching code. - `doc/CONTROLPLANE-ARCHITECTURE.md` — unified service architecture (single clawdie service, not a separate jail) - `doc/CONTROLPLANE-AGENT-ROLES.md` — agent roles, budgets, heartbeat policy, skill mapping - `doc/CONTROLPLANE-MESSAGE-CONTRACT.md` — API contracts between control plane (governance) and agents (operations) -- `SOUL.md`, `SYSADMIN.md`, `DBA.md`, `GIT_ADMIN.md` — agent identity files with dual-layer decision logic +- `SOUL.md`, `SYSADMIN_AGENT.md`, `DB_ADMIN_AGENT.md`, `GIT_ADMIN_AGENT.md` — agent identity files with dual-layer decision logic **Architecture:** Control plane concepts (agents, tasks, budgets, approvals, activity log) are integrated INTO the existing `clawdie` service on the host. Single unified scheduler (30s ticks), shared PostgreSQL at `10.0.0.3`, agents run via `pi` CLI. @@ -59,7 +59,7 @@ src/controlplane.test.ts — full heartbeat cycle end-to-end ```sql CREATE TABLE IF NOT EXISTS agents ( id TEXT PRIMARY KEY, - role TEXT NOT NULL CHECK (role IN ('orchestrator','sysadmin','dba','git_admin')), + role TEXT NOT NULL CHECK (role IN ('orchestrator','sysadmin_agent','db_admin_agent','git_admin_agent')), adapter TEXT NOT NULL DEFAULT 'pi-local', heartbeat_enabled BOOLEAN DEFAULT FALSE, heartbeat_interval_sec INTEGER, diff --git a/doc/CONTROLPLANE-MESSAGE-CONTRACT.md b/doc/CONTROLPLANE-MESSAGE-CONTRACT.md index 61b5070..99e622a 100644 --- a/doc/CONTROLPLANE-MESSAGE-CONTRACT.md +++ b/doc/CONTROLPLANE-MESSAGE-CONTRACT.md @@ -29,16 +29,16 @@ Authorization: Bearer {agent_api_key} { "agents": [ { "id": "clawdie", "role": "orchestrator", "heartbeat_enabled": false }, - { "id": "sysadmin", "role": "sysadmin", "heartbeat_enabled": true }, - { "id": "dba", "role": "dba", "heartbeat_enabled": false }, - { "id": "git_admin", "role": "git_admin", "heartbeat_enabled": false } + { "id": "sysadmin_agent", "role": "sysadmin_agent", "heartbeat_enabled": true }, + { "id": "db_admin_agent", "role": "db_admin_agent", "heartbeat_enabled": false }, + { "id": "git_admin_agent", "role": "git_admin_agent", "heartbeat_enabled": false } ], "budget": { "daily_tokens": 100000, "spent_today": 25000, "remaining": 75000, "hard_limit_exceeded": false, - "allocation": { "orchestrator": 80000, "sysadmin": 10000, "dba": 5000, "git_admin": 5000 } + "allocation": { "orchestrator": 80000, "sysadmin_agent": 10000, "db_admin_agent": 5000, "git_admin_agent": 5000 } } } ``` @@ -59,12 +59,12 @@ Authorization: Bearer {agent_api_key} { "task_id": "TASK-001", "title": "Check if db jail is running", - "description": "Verify sysadmin-db is up and healthy", - "assigned_to": "sysadmin", + "description": "Verify clawdie-db is up and healthy", + "assigned_to": "sysadmin_agent", "priority": "medium", "status": "pending", "created_at": "2026-04-07T10:30:00Z", - "context": { "jail_name": "sysadmin-db" } + "context": { "jail_name": "clawdie-db" } } ] } @@ -115,11 +115,11 @@ Authorization: Bearer {agent_api_key} { "event_type": "task_completed", "task_id": "TASK-001", - "agent_id": "sysadmin", + "agent_id": "sysadmin_agent", "skill_executed": "jail-status", "result": { "status": "success", - "output": "Jail sysadmin-db running, uptime 5d 3h, CPU 2.1%", + "output": "Jail clawdie-db running, uptime 5d 3h, CPU 2.1%", "tokens_used": 420 } } @@ -133,7 +133,7 @@ Authorization: Bearer {agent_api_key} { "event_type": "approval_request", - "agent_id": "git_admin", + "agent_id": "git_admin_agent", "operation": "Merge PR #42 with conflict resolution", "reasoning": "Conflict detected in src/index.ts", "estimated_tokens": 8500 @@ -148,7 +148,7 @@ Authorization: Bearer {agent_api_key} { "event_type": "error", - "agent_id": "dba", + "agent_id": "db_admin_agent", "error_message": "Vacuum failed: database locked", "action_taken": "Escalated to orchestrator", "tokens_used": 1200 @@ -226,4 +226,4 @@ const agentEnv = { - `doc/CONTROLPLANE-ARCHITECTURE.md` — service architecture - `doc/CONTROLPLANE-AGENT-ROLES.md` — role definitions -- `SOUL.md`, `SYSADMIN.md`, `DBA.md`, `GIT_ADMIN.md` — agent identities +- `SOUL.md`, `SYSADMIN_AGENT.md`, `DB_ADMIN_AGENT.md`, `GIT_ADMIN_AGENT.md` — agent identities diff --git a/doc/DASHBOARD-PHASE-D-HANDOFF.md b/doc/DASHBOARD-PHASE-D-HANDOFF.md index 56c865f..cde3932 100644 --- a/doc/DASHBOARD-PHASE-D-HANDOFF.md +++ b/doc/DASHBOARD-PHASE-D-HANDOFF.md @@ -26,7 +26,7 @@ Sam picked option 3 (submodule) over option 1 (vendor copy) and option 2 (stub). `npm run dashboard` (or equivalent) on a fresh clawdie-ai checkout serves a working dashboard at `localhost:3100/dashboard` (or wherever we mount it on the existing Express server) showing: -1. Agent list (orchestrator, sysadmin, dba, git_admin) with heartbeat status +1. Agent list (orchestrator, sysadmin_agent, db_admin_agent, git_admin_agent) with heartbeat status 2. Task queue (pulled from `/api/controlplane/state`) 3. Approvals pane (pulled from `/api/controlplane/approvals`) 4. Activity log (pulled from `/api/controlplane/activity`) diff --git a/doc/MULTI-PROVIDER-ARCHITECTURE.md b/doc/MULTI-PROVIDER-ARCHITECTURE.md index ef52341..ab25dea 100644 --- a/doc/MULTI-PROVIDER-ARCHITECTURE.md +++ b/doc/MULTI-PROVIDER-ARCHITECTURE.md @@ -208,7 +208,7 @@ Maps task context to the best provider: ```typescript interface TaskProfile { - agentRole: string; // orchestrator, sysadmin, dba, git_admin + agentRole: string; // orchestrator, sysadmin_agent, db_admin_agent, git_admin_agent 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 1c7f7a6..2a5fa5b 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 tasks, heartbeat interpretation, pi TUI +- Good for: routine sysadmin_agent 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 ea3f012..e81b642 100644 --- a/docs/internal/REFACTOR-PLAN.md +++ b/docs/internal/REFACTOR-PLAN.md @@ -197,7 +197,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 tasks | +| `freebsd-admin` | Host sysadmin_agent tasks | | `sanoid` | ZFS snapshots — now must cover `/home/clawdie` too | ### Skills to PORT FROM NanoClaw (we're missing) diff --git a/docs/public/architecture/controlplane.md b/docs/public/architecture/controlplane.md index c830526..fbc67a3 100644 --- a/docs/public/architecture/controlplane.md +++ b/docs/public/architecture/controlplane.md @@ -34,7 +34,7 @@ Single clawdie service (host): ``` Agents run on the host via the `pi` CLI. Each agent gets: -- A system prompt from their identity file (`SYSADMIN.md`, `DBA.md`, etc.) +- A system prompt from their identity file (`SYSADMIN_AGENT.md`, `DB_ADMIN_AGENT.md`, etc.) - A persistent session in `data/sessions/{agent}.jsonl` - Access to the skills catalog in `data/skills/` - `CONTROLPLANE_*` env vars pointing at the local HTTP API @@ -131,4 +131,4 @@ npm run setup -- --step controlplane - `doc/CONTROLPLANE-ARCHITECTURE.md` — detailed service layout - `doc/CONTROLPLANE-MESSAGE-CONTRACT.md` — API contracts (what agents query and post) - `doc/CONTROLPLANE-AGENT-ROLES.md` — role definitions, skill mappings, budgets -- `SOUL.md`, `SYSADMIN.md`, `DBA.md`, `GIT_ADMIN.md` — agent identity files +- `SOUL.md`, `SYSADMIN_AGENT.md`, `DB_ADMIN_AGENT.md`, `GIT_ADMIN_AGENT.md` — agent identity files diff --git a/src/agent-session.test.ts b/src/agent-session.test.ts index 5c59723..b3b6fe6 100644 --- a/src/agent-session.test.ts +++ b/src/agent-session.test.ts @@ -44,21 +44,21 @@ function makeEntry(overrides: Partial = {}): SessionEntry { describe('Agent Session Persistence', () => { describe('writing session entries', () => { it('creates JSONL file at sessionDir/{agentName}.jsonl', () => { - writeSessionEntry(tmpDir, 'sysadmin', makeEntry(), WORKSPACE); - expect(fs.existsSync(path.join(tmpDir, 'sysadmin.jsonl'))).toBe(true); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry(), WORKSPACE); + expect(fs.existsSync(path.join(tmpDir, 'sysadmin_agent.jsonl'))).toBe(true); }); it('appends entries, one JSON object per line', () => { - writeSessionEntry(tmpDir, 'sysadmin', makeEntry(), WORKSPACE); - writeSessionEntry(tmpDir, 'sysadmin', makeEntry({ task: 'Check disk' }), WORKSPACE); - const content = fs.readFileSync(path.join(tmpDir, 'sysadmin.jsonl'), 'utf-8'); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry(), WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry({ task: 'Check disk' }), WORKSPACE); + const content = fs.readFileSync(path.join(tmpDir, 'sysadmin_agent.jsonl'), 'utf-8'); const lines = content.trim().split('\n'); expect(lines).toHaveLength(2); }); it('each entry includes timestamp, task, skill, result, tokens_used', () => { - writeSessionEntry(tmpDir, 'sysadmin', makeEntry(), WORKSPACE); - const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry(), WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); const entry = session.entries[0]; expect(entry).toHaveProperty('timestamp'); expect(entry).toHaveProperty('task'); @@ -69,9 +69,9 @@ describe('Agent Session Persistence', () => { it('multiple entries are each on their own line (valid JSONL)', () => { for (let i = 0; i < 5; i++) { - writeSessionEntry(tmpDir, 'sysadmin', makeEntry({ task: `Task ${i}` }), WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry({ task: `Task ${i}` }), WORKSPACE); } - const content = fs.readFileSync(path.join(tmpDir, 'sysadmin.jsonl'), 'utf-8'); + const content = fs.readFileSync(path.join(tmpDir, 'sysadmin_agent.jsonl'), 'utf-8'); for (const line of content.trim().split('\n')) { expect(() => JSON.parse(line)).not.toThrow(); } @@ -80,14 +80,14 @@ describe('Agent Session Persistence', () => { describe('loading sessions', () => { it('loads all entries from existing JSONL file', () => { - writeSessionEntry(tmpDir, 'dba', makeEntry(), WORKSPACE); - writeSessionEntry(tmpDir, 'dba', makeEntry({ task: 'Vacuum' }), WORKSPACE); - const session = loadSession(tmpDir, 'dba', WORKSPACE); + writeSessionEntry(tmpDir, 'db_admin_agent', makeEntry(), WORKSPACE); + writeSessionEntry(tmpDir, 'db_admin_agent', makeEntry({ task: 'Vacuum' }), WORKSPACE); + const session = loadSession(tmpDir, 'db_admin_agent', WORKSPACE); expect(session.entries).toHaveLength(2); }); it('returns empty entries array when file does not exist', () => { - const session = loadSession(tmpDir, 'git_admin', WORKSPACE); + const session = loadSession(tmpDir, 'git_admin_agent', WORKSPACE); expect(session.entries).toHaveLength(0); }); @@ -98,9 +98,9 @@ describe('Agent Session Persistence', () => { }); it('entries are in chronological order (oldest first)', () => { - writeSessionEntry(tmpDir, 'sysadmin', makeEntry({ task: 'First', timestamp: '2026-04-07T09:00:00Z' }), WORKSPACE); - writeSessionEntry(tmpDir, 'sysadmin', makeEntry({ task: 'Second', timestamp: '2026-04-07T10:00:00Z' }), WORKSPACE); - const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry({ task: 'First', timestamp: '2026-04-07T09:00:00Z' }), WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry({ task: 'Second', timestamp: '2026-04-07T10:00:00Z' }), WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); expect(session.entries[0].task).toBe('First'); expect(session.entries[1].task).toBe('Second'); }); @@ -108,19 +108,19 @@ describe('Agent Session Persistence', () => { describe('resilience', () => { it('skips malformed lines (not crashing on bad JSON)', () => { - fs.writeFileSync(path.join(tmpDir, 'sysadmin.jsonl'), 'not-json\n', 'utf-8'); - writeSessionEntry(tmpDir, 'sysadmin', makeEntry(), WORKSPACE); - const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); + fs.writeFileSync(path.join(tmpDir, 'sysadmin_agent.jsonl'), 'not-json\n', 'utf-8'); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry(), WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); expect(session.entries).toHaveLength(1); }); it('still loads valid lines when one line is corrupt', () => { - const filePath = path.join(tmpDir, 'dba.jsonl'); + const filePath = path.join(tmpDir, 'db_admin_agent.jsonl'); fs.writeFileSync(filePath, '', 'utf-8'); - writeSessionEntry(tmpDir, 'dba', makeEntry({ task: 'Valid' }), WORKSPACE); + writeSessionEntry(tmpDir, 'db_admin_agent', makeEntry({ task: 'Valid' }), WORKSPACE); fs.appendFileSync(filePath, 'bad{json\n'); - writeSessionEntry(tmpDir, 'dba', makeEntry({ task: 'Also valid' }), WORKSPACE); - const session = loadSession(tmpDir, 'dba', WORKSPACE); + writeSessionEntry(tmpDir, 'db_admin_agent', makeEntry({ task: 'Also valid' }), WORKSPACE); + const session = loadSession(tmpDir, 'db_admin_agent', WORKSPACE); expect(session.entries).toHaveLength(2); expect(session.entries.every((e) => e.task.startsWith('Valid') || e.task === 'Also valid')).toBe(true); }); @@ -140,25 +140,25 @@ describe('Agent Session Persistence', () => { describe('session path safety', () => { it('session file path is under project root (workspace rule)', () => { - const filePath = resolveSessionPath('data/sessions', 'sysadmin', WORKSPACE); + const filePath = resolveSessionPath('data/sessions', 'sysadmin_agent', WORKSPACE); expect(filePath).toMatch(new RegExp(`^${WORKSPACE}`)); }); it('rejects sessionDir pointing to /tmp/', () => { - expect(() => resolveSessionPath('/tmp', 'sysadmin', WORKSPACE)).toThrow(); + expect(() => resolveSessionPath('/tmp', 'sysadmin_agent', WORKSPACE)).toThrow(); }); it('rejects sessionDir with path traversal (../)', () => { - expect(() => resolveSessionPath('../../../etc', 'sysadmin', WORKSPACE)).toThrow(); + expect(() => resolveSessionPath('../../../etc', 'sysadmin_agent', WORKSPACE)).toThrow(); }); }); describe('pruning', () => { it('prune keeps newest N entries', () => { for (let i = 0; i < 10; i++) { - writeSessionEntry(tmpDir, 'sysadmin', makeEntry({ task: `Task ${i}` }), WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry({ task: `Task ${i}` }), WORKSPACE); } - const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); const pruned = pruneOldEntries(session, 3); expect(pruned.entries).toHaveLength(3); expect(pruned.entries[0].task).toBe('Task 7'); @@ -166,8 +166,8 @@ describe('Agent Session Persistence', () => { }); it('prune does not modify file when entries <= maxEntries', () => { - writeSessionEntry(tmpDir, 'dba', makeEntry(), WORKSPACE); - const session = loadSession(tmpDir, 'dba', WORKSPACE); + writeSessionEntry(tmpDir, 'db_admin_agent', makeEntry(), WORKSPACE); + const session = loadSession(tmpDir, 'db_admin_agent', WORKSPACE); const before = fs.readFileSync(session.filePath, 'utf-8'); pruneOldEntries(session, 10); const after = fs.readFileSync(session.filePath, 'utf-8'); @@ -187,22 +187,22 @@ describe('Agent Session Persistence', () => { describe('continuity context', () => { it('last N entries formatted for system prompt injection', () => { - writeSessionEntry(tmpDir, 'sysadmin', makeEntry({ task: 'Check jails', skill: 'jail-status', result: 'success', tokens_used: 420 }), WORKSPACE); - const session = loadSession(tmpDir, 'sysadmin', WORKSPACE); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry({ task: 'Check jails', skill: 'jail-status', result: 'success', tokens_used: 420 }), WORKSPACE); + const session = loadSession(tmpDir, 'sysadmin_agent', WORKSPACE); const ctx = formatSessionContext(session); expect(ctx).toContain('jail-status'); expect(ctx).toContain('420'); }); it('context includes task, skill, outcome (not full output)', () => { - writeSessionEntry(tmpDir, 'sysadmin', makeEntry({ task: 'Check disk', skill: 'disk-usage', result: 'success', tokens_used: 300 }), WORKSPACE); - const ctx = formatSessionContext(loadSession(tmpDir, 'sysadmin', WORKSPACE)); + writeSessionEntry(tmpDir, 'sysadmin_agent', makeEntry({ task: 'Check disk', skill: 'disk-usage', result: 'success', tokens_used: 300 }), WORKSPACE); + const ctx = formatSessionContext(loadSession(tmpDir, 'sysadmin_agent', 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', WORKSPACE); + const session = loadSession(tmpDir, 'git_admin_agent', WORKSPACE); expect(formatSessionContext(session)).toBe(''); }); }); diff --git a/src/controlplane-api.test.ts b/src/controlplane-api.test.ts index 778cb56..f7af625 100644 --- a/src/controlplane-api.test.ts +++ b/src/controlplane-api.test.ts @@ -22,12 +22,12 @@ import { 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', 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: '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() }, ]; 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', daily_tokens: 10000, spent_today: 5000, remaining: 5000, 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() }, ]; const validAuth = 'Bearer op:admin:testpass'; @@ -228,19 +228,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', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, - { id: 'T2', title: 'Vacuum DB', description: null, assigned_to: 'dba', priority: 'low', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { 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 }, ], }); - const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=sysadmin', undefined, validAuth); + const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=sysadmin_agent', undefined, validAuth); expect(res.body.tasks).toHaveLength(1); - expect(res.body.tasks[0].assigned_to).toBe('sysadmin'); + expect(res.body.tasks[0].assigned_to).toBe('sysadmin_agent'); }); 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', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'T1', title: 'Check jail', description: 'desc', assigned_to: 'sysadmin_agent', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, ], }); const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks', undefined, validAuth); @@ -254,18 +254,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', undefined, validAuth); + const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=git_admin_agent', 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', priority: 'medium', status: 'pending', created_at: new Date(), deadline: null, context: null }, + { id: 'T1', title: 'Pending task', description: null, assigned_to: 'sysadmin_agent', 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', undefined, validAuth); + const res = await makeRequest(handler, 'GET', '/api/controlplane/tasks?role=sysadmin_agent', undefined, validAuth); expect(res.body.tasks).toHaveLength(1); expect(res.body.tasks[0].status).toBe('pending'); }); @@ -276,7 +276,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_id: 'sysadmin_agent', task_id: 'T1', }, validAuth); expect(res.statusCode).toBe(200); @@ -287,7 +287,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_id: 'git_admin_agent', operation: 'merge PR', }, validAuth); expect(res.statusCode).toBe(200); @@ -298,7 +298,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: 'dba', + agent_id: 'db_admin_agent', error_message: 'vacuum failed', }, validAuth); expect(res.statusCode).toBe(200); @@ -326,7 +326,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_id: 'sysadmin_agent', tokens_used: 420, }, validAuth); expect(insertedActivity.length).toBeGreaterThan(0); @@ -372,9 +372,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', undefined, validAuth); + const res = await makeRequest(handler, 'GET', '/api/controlplane/approvals?agent_id=sysadmin_agent', undefined, validAuth); expect(res.statusCode).toBe(200); - expect(capturedParams?.[0]).toBe('sysadmin'); + expect(capturedParams?.[0]).toBe('sysadmin_agent'); }); }); diff --git a/src/controlplane-budget.test.ts b/src/controlplane-budget.test.ts index 1c92925..a63c5a5 100644 --- a/src/controlplane-budget.test.ts +++ b/src/controlplane-budget.test.ts @@ -46,7 +46,7 @@ const mainBudget = (spent = 0): Budget => ({ }); const sysadminBudget = (spent = 0): Budget => ({ - agent_id: 'sysadmin', + agent_id: 'sysadmin_agent', daily_tokens: 10000, spent_today: spent, remaining: 10000 - spent, @@ -55,7 +55,7 @@ const sysadminBudget = (spent = 0): Budget => ({ }); const dbaBudget = (spent = 0): Budget => ({ - agent_id: 'dba', + agent_id: 'db_admin_agent', daily_tokens: 5000, spent_today: spent, remaining: 5000 - spent, @@ -69,34 +69,34 @@ describe('Control Plane Budget Enforcement', () => { describe('global budget', () => { it('allows spawn when budget has remaining tokens', async () => { const pool = makePool([sysadminBudget(1000)]); - const result = await checkBudget(pool as unknown as Pool, 'sysadmin', 500); + const result = await checkBudget(pool as unknown as Pool, 'sysadmin_agent', 500); expect(result.allowed).toBe(true); }); it('refuses spawn when budget is exactly 0', async () => { const pool = makePool([sysadminBudget(10000)]); - const result = await checkBudget(pool as unknown as Pool, 'sysadmin', 1); + const result = await checkBudget(pool as unknown as Pool, 'sysadmin_agent', 1); expect(result.allowed).toBe(false); expect(result.reason).toBe('budget_exhausted'); }); it('refuses spawn when estimated_tokens exceeds remaining', async () => { const pool = makePool([sysadminBudget(9500)]); - const result = await checkBudget(pool as unknown as Pool, 'sysadmin', 600); + const result = await checkBudget(pool as unknown as Pool, 'sysadmin_agent', 600); expect(result.allowed).toBe(false); expect(result.reason).toBe('budget_exhausted'); }); it('returns reason="budget_exhausted" on refusal', async () => { const pool = makePool([sysadminBudget(10000)]); - const result = await checkBudget(pool as unknown as Pool, 'sysadmin', 1); + const result = await checkBudget(pool as unknown as Pool, 'sysadmin_agent', 1); expect(result.reason).toBe('budget_exhausted'); }); it('hard_limit_exceeded flag triggers immediate stop for all agents', async () => { // Use a budget with remaining > 0 but hard_limit_exceeded forced true (manual override) const pool = makePool([{ ...sysadminBudget(5000), hard_limit_exceeded: true }]); - const result = await checkBudget(pool as unknown as Pool, 'sysadmin', 1); + const result = await checkBudget(pool as unknown as Pool, 'sysadmin_agent', 1); expect(result.allowed).toBe(false); expect(result.reason).toBe('hard_limit_exceeded'); }); @@ -105,19 +105,19 @@ describe('Control Plane Budget Enforcement', () => { describe('per-agent budget', () => { it('allows spawn when agent allocation has remaining tokens', async () => { const pool = makePool([dbaBudget(0)]); - const result = await checkBudget(pool as unknown as Pool, 'dba', 500); + const result = await checkBudget(pool as unknown as Pool, 'db_admin_agent', 500); expect(result.allowed).toBe(true); }); it('refuses spawn when agent allocation is exhausted', async () => { const pool = makePool([dbaBudget(5000)]); - const result = await checkBudget(pool as unknown as Pool, 'dba', 1); + const result = await checkBudget(pool as unknown as Pool, 'db_admin_agent', 1); expect(result.allowed).toBe(false); }); it('returns reason="agent_budget_exhausted" — mapped to budget_exhausted', async () => { const pool = makePool([dbaBudget(5000)]); - const result = await checkBudget(pool as unknown as Pool, 'dba', 1); + const result = await checkBudget(pool as unknown as Pool, 'db_admin_agent', 1); expect(result.reason).toBe('budget_exhausted'); }); @@ -128,15 +128,15 @@ describe('Control Plane Budget Enforcement', () => { expect(result.reason).toBe('agent_budget_not_found'); }); - it('sysadmin allocation is 10% of daily_tokens (10000 of 100000)', async () => { + it('sysadmin_agent 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', 100); + const result = await checkBudget(pool as unknown as Pool, 'sysadmin_agent', 100); expect(result.dailyTokens).toBe(10000); }); - it('dba allocation is 5% of daily_tokens (5000 of 100000)', async () => { + it('db_admin_agent allocation is 5% of daily_tokens (5000 of 100000)', async () => { const pool = makePool([dbaBudget(0)]); - const result = await checkBudget(pool as unknown as Pool, 'dba', 100); + const result = await checkBudget(pool as unknown as Pool, 'db_admin_agent', 100); expect(result.dailyTokens).toBe(5000); }); @@ -151,7 +151,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', 3000); + await recordTokenSpend(pool as unknown as Pool, 'sysadmin_agent', 3000); expect(b.spent_today).toBe(3000); expect(b.hard_limit_exceeded).toBe(false); }); @@ -159,14 +159,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', 2000); + await recordTokenSpend(pool as unknown as Pool, 'sysadmin_agent', 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', 5000); + await recordTokenSpend(pool as unknown as Pool, 'sysadmin_agent', 5000); expect(b.spent_today).toBe(10000); expect(b.spent_today).toBeLessThanOrEqual(b.daily_tokens); }); @@ -198,23 +198,23 @@ describe('Control Plane Budget Enforcement', () => { }); describe('approval threshold', () => { - it('operations > 2000 tokens require operator approval (sysadmin)', async () => { + it('operations > 2000 tokens require operator approval (sysadmin_agent)', async () => { const pool = makePool([sysadminBudget(0)]); - const result = await checkBudget(pool as unknown as Pool, 'sysadmin', 2500, false); + const result = await checkBudget(pool as unknown as Pool, 'sysadmin_agent', 2500, false); expect(result.allowed).toBe(false); expect(result.reason).toBe('approval_required'); }); - it('operations > 3000 tokens require operator approval (dba)', async () => { + it('operations > 3000 tokens require operator approval (db_admin_agent)', async () => { const pool = makePool([dbaBudget(0)]); - const result = await checkBudget(pool as unknown as Pool, 'dba', 3500, false); + const result = await checkBudget(pool as unknown as Pool, 'db_admin_agent', 3500, false); expect(result.allowed).toBe(false); expect(result.reason).toBe('approval_required'); }); it('operator-approved operations bypass per-agent threshold check', async () => { const pool = makePool([sysadminBudget(0)]); - const result = await checkBudget(pool as unknown as Pool, 'sysadmin', 5000, true); + const result = await checkBudget(pool as unknown as Pool, 'sysadmin_agent', 5000, true); expect(result.allowed).toBe(true); }); }); diff --git a/src/controlplane-budget.ts b/src/controlplane-budget.ts index 1738a6f..c000d9e 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: 2000, - dba: 3000, - git_admin: 2000, + sysadmin_agent: 2000, + db_admin_agent: 3000, + git_admin_agent: 2000, orchestrator: Infinity, }; diff --git a/src/controlplane-db.ts b/src/controlplane-db.ts index 82e96ea..6359d30 100644 --- a/src/controlplane-db.ts +++ b/src/controlplane-db.ts @@ -13,7 +13,7 @@ import pg from 'pg'; // ── Types ────────────────────────────────────────────────────────────────── -export type AgentRole = 'orchestrator' | 'sysadmin' | 'dba' | 'git_admin'; +export type AgentRole = 'orchestrator' | 'sysadmin_agent' | 'db_admin_agent' | 'git_admin_agent'; export interface Agent { id: string; @@ -75,24 +75,24 @@ export function getDefaultAgents(agentName: string): Omit = {}): ControlplaneRunOptions { return { - agentId: 'sysadmin', + agentId: 'sysadmin_agent', agentName: 'clawdie', taskId: 'TASK-001', apiKey: 'test-api-key', @@ -30,8 +30,8 @@ function makeOpts(overrides: Partial = {}): Controlplane describe('Control Plane Runner — env injection', () => { describe('CONTROLPLANE_* env vars', () => { it('injects CONTROLPLANE_AGENT_ID', () => { - const result = buildControlplaneRunCommand(makeOpts({ agentId: 'sysadmin' })); - expect(result.env.CONTROLPLANE_AGENT_ID).toBe('sysadmin'); + const result = buildControlplaneRunCommand(makeOpts({ agentId: 'sysadmin_agent' })); + expect(result.env.CONTROLPLANE_AGENT_ID).toBe('sysadmin_agent'); }); it('injects CONTROLPLANE_API_URL as http://localhost:3100', () => { @@ -68,8 +68,8 @@ describe('Control Plane Runner — env injection', () => { }); it('session file path ends with {agent_name}.jsonl', () => { - const result = buildControlplaneRunCommand(makeOpts({ agentId: 'dba' })); - const sessionArg = result.args.find((a) => a.endsWith('dba.jsonl')); + const result = buildControlplaneRunCommand(makeOpts({ agentId: 'db_admin_agent' })); + const sessionArg = result.args.find((a) => a.endsWith('db_admin_agent.jsonl')); expect(sessionArg).toBeDefined(); }); @@ -93,10 +93,10 @@ describe('Control Plane Runner — env injection', () => { }); it('args include --session with correct project-relative path', () => { - const result = buildControlplaneRunCommand(makeOpts({ agentId: 'sysadmin' })); + const result = buildControlplaneRunCommand(makeOpts({ agentId: 'sysadmin_agent' })); const idx = result.args.indexOf('--session'); expect(idx).toBeGreaterThanOrEqual(0); - expect(result.args[idx + 1]).toContain('sysadmin.jsonl'); + expect(result.args[idx + 1]).toContain('sysadmin_agent.jsonl'); }); it('args include --skills pointing to data/skills/', () => { @@ -107,10 +107,10 @@ describe('Control Plane Runner — env injection', () => { }); it('args include --append-system-prompt with identity file path', () => { - const result = buildControlplaneRunCommand(makeOpts({ agentId: 'sysadmin' })); + const result = buildControlplaneRunCommand(makeOpts({ agentId: 'sysadmin_agent' })); const idx = result.args.indexOf('--append-system-prompt'); expect(idx).toBeGreaterThanOrEqual(0); - expect(result.args[idx + 1]).toContain('SYSADMIN.md'); + expect(result.args[idx + 1]).toContain('SYSADMIN_AGENT.md'); }); it('heartbeat prompt text is passed as final arg', () => { diff --git a/src/controlplane-runner.ts b/src/controlplane-runner.ts index ee71024..e5889d4 100644 --- a/src/controlplane-runner.ts +++ b/src/controlplane-runner.ts @@ -38,9 +38,9 @@ export interface RunResult { const CONTROLPLANE_API_URL = 'http://localhost:3100'; const AGENT_IDENTITY_FILES: Record = { - sysadmin: 'SYSADMIN.md', - dba: 'DBA.md', - git_admin: 'GIT_ADMIN.md', + sysadmin_agent: 'SYSADMIN_AGENT.md', + db_admin_agent: 'DB_ADMIN_AGENT.md', + git_admin_agent: 'GIT_ADMIN_AGENT.md', }; export function resolveIdentityFile(agentId: string, agentName: string, workspaceCwd: string): string { diff --git a/src/controlplane-setup.test.ts b/src/controlplane-setup.test.ts index ff4692a..77cb2e0 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"', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin'); + it('DEFAULT_AGENTS contains Sysadmin agent with role "sysadmin_agent"', () => { + const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin_agent'); expect(agent).toBeDefined(); - expect(agent?.role).toBe('sysadmin'); + expect(agent?.role).toBe('sysadmin_agent'); }); - it('DEFAULT_AGENTS contains DBA agent with role "dba"', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'dba'); + it('DEFAULT_AGENTS contains DBA agent with role "db_admin_agent"', () => { + const agent = DEFAULT_AGENTS.find((a) => a.id === 'db_admin_agent'); expect(agent).toBeDefined(); - expect(agent?.role).toBe('dba'); + expect(agent?.role).toBe('db_admin_agent'); }); - it('DEFAULT_AGENTS contains Git Admin agent with role "git_admin"', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'git_admin'); + it('DEFAULT_AGENTS contains Git Admin agent with role "git_admin_agent"', () => { + const agent = DEFAULT_AGENTS.find((a) => a.id === 'git_admin_agent'); expect(agent).toBeDefined(); - expect(agent?.role).toBe('git_admin'); + expect(agent?.role).toBe('git_admin_agent'); }); 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')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin_agent')!; 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 === 'dba')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'db_admin_agent')!; 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')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'git_admin_agent')!; 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')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'sysadmin_agent')!; expect(agent.budget_allocation_pct).toBe(10); }); it('DBA gets 5% allocation', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'dba')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'db_admin_agent')!; expect(agent.budget_allocation_pct).toBe(5); }); it('Git Admin gets 5% allocation', () => { - const agent = DEFAULT_AGENTS.find((a) => a.id === 'git_admin')!; + const agent = DEFAULT_AGENTS.find((a) => a.id === 'git_admin_agent')!; expect(agent.budget_allocation_pct).toBe(5); }); diff --git a/src/controlplane-telegram.test.ts b/src/controlplane-telegram.test.ts index 1a1e53f..096014b 100644 --- a/src/controlplane-telegram.test.ts +++ b/src/controlplane-telegram.test.ts @@ -52,7 +52,7 @@ function makePool(): Pool { return { rows: [] }; } if (/FROM agent_budgets/i.test(sql)) { - return { rows: [{ agent_id: 'sysadmin', daily_tokens: 10000, spent_today: 0, remaining: 10000, hard_limit_exceeded: false, reset_at: new Date() }] }; + return { rows: [{ agent_id: 'sysadmin_agent', daily_tokens: 10000, spent_today: 0, remaining: 10000, hard_limit_exceeded: false, reset_at: new Date() }] }; } if (/FROM agents\b/i.test(sql)) return { rows: [] }; if (/FROM tasks\b/i.test(sql)) return { rows: [] }; @@ -137,25 +137,25 @@ describe('Telegram → Control Plane Bridge', () => { }); describe('agent routing', () => { - it('routes jail messages to sysadmin', async () => { + it('routes jail messages to sysadmin_agent', async () => { writeSkill('jail-status', ['Check jail *', 'Is * jail running']); const pool = makePool(); const result = await bridgeTelegramMessage(pool, makeConfig(pool), 'Check jail db'); - expect(result?.assignedTo).toBe('sysadmin'); + expect(result?.assignedTo).toBe('sysadmin_agent'); }); - it('routes database messages to dba', async () => { + it('routes database messages to db_admin_agent', async () => { writeSkill('backup-db', ['backup * database', 'run database backup']); const pool = makePool(); const result = await bridgeTelegramMessage(pool, makeConfig(pool), 'backup the database'); - expect(result?.assignedTo).toBe('dba'); + expect(result?.assignedTo).toBe('db_admin_agent'); }); - it('routes git messages to git_admin', async () => { + it('routes git messages to git_admin_agent', async () => { writeSkill('git-status', ['check git repo', 'git status *']); const pool = makePool(); const result = await bridgeTelegramMessage(pool, makeConfig(pool), 'check git repo'); - expect(result?.assignedTo).toBe('git_admin'); + expect(result?.assignedTo).toBe('git_admin_agent'); }); it('routes unrecognised agent messages to orchestrator', async () => { @@ -192,7 +192,7 @@ describe('Telegram → Control Plane Bridge', () => { writeSkill('jail-status', ['Check jail *']); const pool = makePool(); await bridgeTelegramMessage(pool, makeConfig(pool), 'Check jail db'); - expect(insertedTasks[0].assigned_to).toBe('sysadmin'); + expect(insertedTasks[0].assigned_to).toBe('sysadmin_agent'); }); it('posts agent activity after waking agent', async () => { diff --git a/src/controlplane-telegram.ts b/src/controlplane-telegram.ts index 2dc00c7..51f65a2 100644 --- a/src/controlplane-telegram.ts +++ b/src/controlplane-telegram.ts @@ -31,11 +31,11 @@ export interface TelegramBridgeResult { // ── Agent routing ────────────────────────────────────────────────────────── // Maps skill category keywords to agent roles. -// Sysadmin handles infra; DBA handles data; git_admin handles code; orchestrator gets everything else. +// Sysadmin handles infra; DBA handles data; git_admin_agent handles code; orchestrator gets everything else. const ROLE_KEYWORDS: Array<{ role: string; patterns: RegExp[] }> = [ - { role: 'sysadmin', patterns: [/jail/i, /service/i, /network/i, /disk/i, /cpu/i, /memory/i, /reboot/i, /cert/i, /nginx/i, /tailscale/i] }, - { role: 'dba', patterns: [/\bdatabase\b/i, /\bpostgres\b/i, /\bvacuum\b/i, /\bdb\b/i, /\bmigration\b/i] }, - { role: 'git_admin', patterns: [/\bgit\b/i, /\brepo\b/i, /\bcommit\b/i, /\bmerge\b/i, /\bbranch\b/i, /pull request/i, /\bpr\b/i] }, + { role: 'sysadmin_agent', patterns: [/jail/i, /service/i, /network/i, /disk/i, /cpu/i, /memory/i, /reboot/i, /cert/i, /nginx/i, /tailscale/i] }, + { role: 'db_admin_agent', patterns: [/\bdatabase\b/i, /\bpostgres\b/i, /\bvacuum\b/i, /\bdb\b/i, /\bmigration\b/i] }, + { role: 'git_admin_agent', patterns: [/\bgit\b/i, /\brepo\b/i, /\bcommit\b/i, /\bmerge\b/i, /\bbranch\b/i, /pull request/i, /\bpr\b/i] }, ]; function inferRole(message: string, skillName: string | undefined, skillConfidence: string, fallbackAgent: string): string { diff --git a/src/controlplane.test.ts b/src/controlplane.test.ts index 2ef5ec1..e5ce26c 100644 --- a/src/controlplane.test.ts +++ b/src/controlplane.test.ts @@ -53,20 +53,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', 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: 'dba', role: 'dba', 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() }, + { 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() }, ]; 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', daily_tokens: 10000, spent_today: 5000, remaining: 5000, hard_limit_exceeded: false, reset_at: new Date() }, - { agent_id: 'dba', 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() }, + { 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() }, ]; const defaultTasks: Task[] = [ - { 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 }, + { 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 }, ]; let activityLog: Array<{ agent_id: string; event_type: string; payload: Record | null; tokens_used: number | null }> = []; @@ -110,15 +110,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', 'interval_elapsed'); + const result = await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); expect(result.woke).toBe(true); - expect(result.agentId).toBe('sysadmin'); + expect(result.agentId).toBe('sysadmin_agent'); }); it('receives task assignment via tasks query', async () => { const pool = makePool(); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin', 'assignment', 'TASK-001'); + await runAgentHeartbeat(config, 'sysadmin_agent', 'assignment', 'TASK-001'); const taskQuery = vi.mocked(pool.query).mock.calls.find((c) => /FROM tasks/i.test(c[0] as string)); expect(taskQuery).toBeDefined(); }); @@ -126,23 +126,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', 'interval_elapsed'); + await runAgentHeartbeat(config, 'sysadmin_agent', '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', 'interval_elapsed'); - const events = activityLog.filter((e) => e.agent_id === 'sysadmin'); + await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); + const events = activityLog.filter((e) => e.agent_id === 'sysadmin_agent'); expect(events.length).toBeGreaterThan(0); }); it('session JSONL is updated after completion', async () => { const pool = makePool(); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); - const sessionFile = path.join(sessionDir, 'sysadmin.jsonl'); + await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); + const sessionFile = path.join(sessionDir, 'sysadmin_agent.jsonl'); if (fs.existsSync(sessionFile)) { const content = fs.readFileSync(sessionFile, 'utf-8'); expect(content.length).toBeGreaterThan(0); @@ -152,22 +152,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: 'dba', 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_agent', 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, 'dba', 'assignment', 'TASK-002'); + const result = await runAgentHeartbeat(config, 'db_admin_agent', '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, 'dba', 'assignment', 'TASK-002'); + await runAgentHeartbeat(config, 'db_admin_agent', '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] === 'dba', + /FROM tasks/i.test(c[0] as string) && (c[1] as unknown[])?.[0] === 'db_admin_agent', ); expect(taskQuery).toBeDefined(); }); @@ -175,8 +175,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, 'dba', 'assignment', 'TASK-002'); - const completed = activityLog.filter((e) => e.event_type === 'task_completed' && e.agent_id === 'dba'); + 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'); expect(completed.length).toBeGreaterThanOrEqual(0); }); }); @@ -184,22 +184,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', daily_tokens: 10000, spent_today: 10000, remaining: 0, hard_limit_exceeded: true, reset_at: new Date() }, + { agent_id: 'sysadmin_agent', 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', 'interval_elapsed'); + const result = await runAgentHeartbeat(config, 'sysadmin_agent', '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', daily_tokens: 10000, spent_today: 10000, remaining: 0, hard_limit_exceeded: true, reset_at: new Date() }, + { agent_id: 'sysadmin_agent', 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', 'interval_elapsed'); + await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); const errorEvents = activityLog.filter((e) => e.event_type === 'error'); expect(errorEvents.length).toBeGreaterThan(0); }); @@ -210,15 +210,15 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { const pool = makePool(); const config = makeConfig(pool); config.skillsDir = '/nonexistent/skills/path'; - const result = await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); - expect(result.agentId).toBe('sysadmin'); + const result = await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); + expect(result.agentId).toBe('sysadmin_agent'); }); it('error event includes action_taken field', async () => { const pool = makePool(); const config = makeConfig(pool); config.skillsDir = '/nonexistent/skills/path'; - await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); + await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); if (activityLog.length > 0) { const hasActionTaken = activityLog.some((e) => { if (e.payload && typeof e.payload === 'object') return 'action_taken' in e.payload; @@ -232,7 +232,7 @@ describe('Control Plane Integration — Full Heartbeat Cycle', () => { const pool = makePool(); const config = makeConfig(pool); config.skillsDir = '/nonexistent'; - const result = await runAgentHeartbeat(config, 'sysadmin', 'interval_elapsed'); + const result = await runAgentHeartbeat(config, 'sysadmin_agent', 'interval_elapsed'); expect(result).toBeDefined(); expect(result).toHaveProperty('agentId'); }); @@ -240,20 +240,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', 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_agent', 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', 'telegram', 'TASK-TG1'); - expect(result.agentId).toBe('sysadmin'); + const result = await runAgentHeartbeat(config, 'sysadmin_agent', 'telegram', 'TASK-TG1'); + expect(result.agentId).toBe('sysadmin_agent'); }); it('task completion posts activity event', async () => { const pool = makePool(defaultAgents, defaultBudgets, telegramTasks); const config = makeConfig(pool); - await runAgentHeartbeat(config, 'sysadmin', 'telegram', 'TASK-TG1'); + await runAgentHeartbeat(config, 'sysadmin_agent', 'telegram', 'TASK-TG1'); expect(activityLog.length).toBeGreaterThan(0); }); }); diff --git a/src/skills-discovery.test.ts b/src/skills-discovery.test.ts index e5dfab8..65b7fa7 100644 --- a/src/skills-discovery.test.ts +++ b/src/skills-discovery.test.ts @@ -101,7 +101,7 @@ describe('Skills Discovery', () => { }); }); - describe('skill pattern matching — sysadmin', () => { + describe('skill pattern matching — sysadmin_agent', () => { const sysadminSkills: Skill[] = [ { name: 'jail-status', description: '', compatibility: 'FreeBSD 15.0+', invoke_patterns: ['Check if * jail is running', 'Is * up', 'Jail status'] }, { name: 'disk-usage', description: '', compatibility: 'FreeBSD 15.0+', invoke_patterns: ['How much free disk*', 'Disk space*'] }, @@ -115,8 +115,8 @@ describe('Skills Discovery', () => { expect(matchTaskToSkill('Check if db jail is running', sysadminSkills).skill!.name).toBe('jail-status'); }); - it('"Is sysadmin-db up?" → jail-status', () => { - expect(matchTaskToSkill('Is sysadmin-db up?', sysadminSkills).skill!.name).toBe('jail-status'); + it('"Is sysadmin_agent-db up?" → jail-status', () => { + expect(matchTaskToSkill('Is sysadmin_agent-db up?', sysadminSkills).skill!.name).toBe('jail-status'); }); it('"How much free disk space?" → disk-usage', () => { @@ -144,7 +144,7 @@ describe('Skills Discovery', () => { }); }); - describe('skill pattern matching — dba', () => { + describe('skill pattern matching — db_admin_agent', () => { const dbaSkills: Skill[] = [ { name: 'db-vacuum', description: '', compatibility: 'FreeBSD 15.0+', invoke_patterns: ['Run vacuum*', 'Vacuum database*'] }, { name: 'db-analyze', description: '', compatibility: 'FreeBSD 15.0+', invoke_patterns: ['Analyze the database*', 'Analyze*'] },