Remove legacy agent IDs + tighten task API

- 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)
This commit is contained in:
Mevy Assistant 2026-04-19 06:54:28 +00:00
parent befd8bb3f4
commit c633fdcc49
22 changed files with 286 additions and 253 deletions

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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)

View file

@ -45,22 +45,22 @@ function makeEntry(overrides: Partial<SessionEntry> = {}): 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');

View file

@ -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);

View file

@ -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, {

View file

@ -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,
);

View file

@ -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,
};

View file

@ -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);
});

View file

@ -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<void> {
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<void> {
}
async function cleanupLegacyAgents(pool: pg.Pool): Promise<void> {
// 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<void> {
// 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<void> {
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'))`,
);
}

View file

@ -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);

View file

@ -23,7 +23,7 @@ function makeOpts(
overrides: Partial<ControlplaneRunOptions> = {},
): 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', () => {

View file

@ -46,11 +46,7 @@ export interface RunResult {
const CONTROLPLANE_API_URL = `http://localhost:${CONTROLPLANE_API_PORT}`;
const AGENT_IDENTITY_FILES: Record<string, string> = {
// 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<string, string> = {
};
export const AGENT_JAIL_MAP: Record<string, string | null> = {
// 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',
};

View file

@ -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);
});

View file

@ -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 () => {

View file

@ -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,

View file

@ -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<string, unknown> | 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);
});
});

View file

@ -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',