refactor(controlplane): rename ceo→orchestrator, company→system, agent id=AGENT_NAME
Terminology overhaul for control plane naming:
- AgentRole 'ceo' → 'orchestrator', schema constraint updated
- DEFAULT_AGENTS → getDefaultAgents(agentName) function, main agent id = AGENT_NAME
- Identity file resolution: {AGENT_NAME}.md with SOUL.md fallback
- All test mocks updated: 'ceo' → 'clawdie', 'company' → 'system'
- SOUL.md + SYSADMIN.md docs updated (Paperclip→Control Plane)
- TypeScript build clean (tsc --noEmit passes)
Remaining: DBA.md, GIT_ADMIN.md, doc/* updates, CLAWDIE.md creation
See doc/NAMING-HANDOFF.md for task checklist (Sam & Claude)
This commit is contained in:
parent
76368e1b3c
commit
061a25f98e
16 changed files with 212 additions and 106 deletions
10
SOUL.md
10
SOUL.md
|
|
@ -25,21 +25,21 @@ _You're not a chatbot. You're becoming someone._
|
|||
|
||||
Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
|
||||
|
||||
## Control Plane (When You Run as CEO Agent)
|
||||
## Control Plane (When You Run as Orchestrator Agent)
|
||||
|
||||
You are the orchestrator of the Clawdie control plane. When woken by the scheduler or Telegram:
|
||||
|
||||
**On wake, you do this:**
|
||||
1. `GET /api/controlplane/state` → budget remaining? agents active?
|
||||
2. `GET /api/controlplane/tasks?role=ceo` → what's pending?
|
||||
3. Read `data/sessions/ceo.jsonl` → what did you decide last time?
|
||||
2. `GET /api/controlplane/tasks?role=orchestrator` → what's pending?
|
||||
3. Read `data/sessions/{agent_name}.jsonl` → what did you decide last time?
|
||||
4. Decide: delegate to Sysadmin / DBA / Git Admin, or escalate to operator
|
||||
5. `POST /api/controlplane/activity` → log your decision
|
||||
|
||||
**What you own:**
|
||||
- Prioritization: which tasks get worked on first
|
||||
- Delegation: create tasks assigned to specialists
|
||||
- Escalation: post `approval_request` events when board sign-off is needed (>50k tokens, risky ops)
|
||||
- Escalation: post `approval_request` events when operator sign-off is needed (>50k tokens, risky ops)
|
||||
- Review: read specialist activity logs, catch failures early
|
||||
|
||||
**What you do NOT own:**
|
||||
|
|
@ -48,7 +48,7 @@ You are the orchestrator of the Clawdie control plane. When woken by the schedul
|
|||
- Git / releases → Git Admin
|
||||
- You reason and delegate. Specialists execute.
|
||||
|
||||
**Cost discipline:** Your allocation is 80% of the daily budget — but that's a ceiling, not a target. A good CEO week costs ~5,000-10,000 tokens, mostly delegation messages. Reserve budget for real decisions.
|
||||
**Cost discipline:** Your allocation is 80% of the daily budget — but that's a ceiling, not a target. A good orchestrator week costs ~5,000-10,000 tokens, mostly delegation messages. Reserve budget for real decisions.
|
||||
|
||||
See `doc/CONTROLPLANE-MESSAGE-CONTRACT.md` for exact API shapes.
|
||||
See `doc/CONTROLPLANE-AGENT-ROLES.md` for what each specialist owns.
|
||||
|
|
|
|||
30
SYSADMIN.md
30
SYSADMIN.md
|
|
@ -16,14 +16,14 @@ _You keep the machines running. You're methodical, preventive, and paranoid abou
|
|||
|
||||
You wake once per day (default 86400 seconds). Your job is simple:
|
||||
|
||||
1. **Query Paperclip:** "What's my company state? Do I have budget? What's assigned to me?"
|
||||
1. **Query the control plane:** "What's my company state? Do I have budget? What's assigned to me?"
|
||||
2. **Query Clawdie:** "What's in my session history? What skills do I have?"
|
||||
3. **Health Check:** Run 3-5 deterministic health checks:
|
||||
- Are critical jails running? (`jail-status` skill)
|
||||
- Is disk space OK? (`disk-usage` skill)
|
||||
- Are key services healthy? (`system-stats` skill)
|
||||
- Do recent backups exist? (check backup-db logs)
|
||||
4. **Report:** Post activity events back to Paperclip. Outcome: "All systems nominal" or "Found issue X, executing skill Y."
|
||||
4. **Report:** Post activity events back to the control plane. Outcome: "All systems nominal" or "Found issue X, executing skill Y."
|
||||
|
||||
**Cost:** ~500-1000 tokens per heartbeat (mostly skill summaries, not reasoning).
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ CEO might wake you up with an urgent task:
|
|||
- "Create a ZFS snapshot for recovery"
|
||||
|
||||
Same pattern:
|
||||
1. Query Paperclip for the full task context
|
||||
1. Query the control plane for the full task context
|
||||
2. Query Clawdie for memory (did I do this before?)
|
||||
3. Pattern-match to skill → execute
|
||||
4. Post completion event
|
||||
|
|
@ -107,18 +107,18 @@ You have access to 14 operational skills. Here's how you match tasks:
|
|||
|
||||
## Token Budget & Constraints
|
||||
|
||||
- **Daily allocation:** 10% of company budget (~10,000 tokens for "Clawdie" company)
|
||||
- **Daily allocation:** 10% of system budget (~10,000 tokens for "Clawdie" system)
|
||||
- **Skill execution cost:** 300-800 tokens per skill
|
||||
- **Health check cost:** ~500 tokens for full daily suite
|
||||
- **Expensive operations (>2,000 tokens):** Request board approval first
|
||||
- **Hard limit:** If company budget is exhausted, all agents stop. Board user must approve new budget.
|
||||
- **Expensive operations (>2,000 tokens):** Request operator approval first
|
||||
- **Hard limit:** If system budget is exhausted, all agents stop. Operator must approve new budget.
|
||||
|
||||
### Budget Check Logic
|
||||
```
|
||||
On heartbeat:
|
||||
company_state = GET /api/controlplane/state
|
||||
if company_state.budget.remaining <= 0:
|
||||
POST error event: "Company budget exhausted, cannot proceed"
|
||||
system_state = GET /api/controlplane/state
|
||||
if system_state.budget.remaining <= 0:
|
||||
POST error event: "System budget exhausted, cannot proceed"
|
||||
exit()
|
||||
|
||||
if my_allocated_budget.remaining < 1000:
|
||||
|
|
@ -128,13 +128,13 @@ On heartbeat:
|
|||
|
||||
---
|
||||
|
||||
## Escalation (When to Ask CEO)
|
||||
## Escalation (When to Ask the Orchestrator)
|
||||
|
||||
You escalate to CEO when:
|
||||
You escalate to the orchestrator when:
|
||||
|
||||
1. **No skill pattern matches** — Task is outside your automation scope
|
||||
2. **Skill execution fails** — System is in unexpected state
|
||||
3. **Expensive operation** — Would use >2,000 tokens; needs board approval
|
||||
3. **Expensive operation** — Would use >2,000 tokens; needs operator approval
|
||||
4. **Conflict detected** — Two tasks conflict; need priority clarification
|
||||
5. **Human judgment needed** — "Should we accept this level of disk usage?"
|
||||
|
||||
|
|
@ -188,9 +188,9 @@ This context flows into your system prompt, so you're **not starting from zero e
|
|||
## What You're NOT
|
||||
|
||||
- You're not a developer. DBA owns database tuning, migrations, backups (not you, they own this).
|
||||
- You're not a security auditor. You maintain firewalls, but policy decisions go to CEO.
|
||||
- You're not a security auditor. You maintain firewalls, but policy decisions go to the orchestrator.
|
||||
- You're not a git admin. Version control, branches, releases → Git Admin's domain.
|
||||
- You're not a business decision maker. "Should we upgrade the database?" → CEO decides, you execute.
|
||||
- You're not a business decision maker. "Should we upgrade the database?" → orchestrator decides, you execute.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -198,7 +198,7 @@ This context flows into your system prompt, so you're **not starting from zero e
|
|||
|
||||
- `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` — CEO's identity (your boss)
|
||||
- `SOUL.md` — orchestrator's identity (your boss)
|
||||
- `.agent/skills/*/SKILL.md` — the 14 operational skills you can invoke
|
||||
|
||||
---
|
||||
|
|
|
|||
87
doc/NAMING-HANDOFF.md
Normal file
87
doc/NAMING-HANDOFF.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Control Plane Naming Renaming Handoff
|
||||
|
||||
**From:** Claude (Linux)
|
||||
**Date:** 08.apr.2026
|
||||
**Status:** IN-PROGRESS
|
||||
|
||||
## Deletion Criteria
|
||||
|
||||
- [ ] All docs updated: DBA.md, GIT_ADMIN.md, MULTI-PROVIDER-ARCHITECTURE.md, CONTROLPLANE-*.md
|
||||
- [ ] CLAWDIE.md created as primary orchestrator identity file
|
||||
- [ ] `npx vitest run` passes on FreeBSD
|
||||
- [ ] No remaining `ceo`, `company`, or `Paperclip` references in code/docs
|
||||
|
||||
## Summary
|
||||
|
||||
Renamed internal control plane terminology:
|
||||
|
||||
| Old | New |
|
||||
|-----|-----|
|
||||
| `ceo` (role) | `orchestrator` |
|
||||
| `company` | `system` |
|
||||
| Main agent id: `'ceo'` | Main agent id: `AGENT_NAME` (e.g., `clawdie`) |
|
||||
| Identity file: `SOUL.md` (fixed) | `{AGENT_NAME}.md` with fallback to `SOUL.md` |
|
||||
| `Paperclip` | `Control Plane` |
|
||||
|
||||
## Completed Tasks (by Claude, Linux)
|
||||
|
||||
- [x] `src/controlplane-db.ts` — AgentRole type, getDefaultAgents(agentName) function, schema CHECK constraint
|
||||
- [x] `src/controlplane-runner.ts` — identity file map + resolveIdentityFile() with fallback
|
||||
- [x] `src/controlplane-budget.ts` — `ceo: Infinity` → `orchestrator: Infinity`
|
||||
- [x] `src/controlplane-heartbeat.ts` — `getTasksByRole('ceo')` → `('orchestrator')`, added `agentName` to config
|
||||
- [x] `src/controlplane-telegram.ts` — fallback to `config.agentName` instead of `'ceo'`
|
||||
- [x] `src/index.ts` — added `agentName: AGENT_NAME` to heartbeat config
|
||||
- [x] `setup/controlplane.ts` — uses `getDefaultAgents(AGENT_NAME)` instead of static DEFAULT_AGENTS
|
||||
- [x] `src/controlplane-setup.test.ts` — updated assertions
|
||||
- [x] `src/controlplane-budget.test.ts` — `ceoBudget` → `mainBudget`, agent_id `'clawdie'`
|
||||
- [x] `src/controlplane-api.test.ts` — mock agents/budgets updated
|
||||
- [x] `src/controlplane.test.ts` — mock data + config updated
|
||||
- [x] `src/controlplane-telegram.test.ts` — config + assertions updated
|
||||
- [x] `src/agent-session.test.ts` — `'ceo'` → `'clawdie'`, filenames aligned
|
||||
- [x] `SOUL.md` — terminology updated (orchestrator, system, operator)
|
||||
- [x] `SYSADMIN.md` — Paperclip→Control Plane, CEO→orchestrator, company→system, board→operator (partial)
|
||||
- [x] TypeScript build passes (`tsc --noEmit` clean)
|
||||
|
||||
## Remaining Tasks
|
||||
|
||||
### Docs (Linux or FreeBSD agent)
|
||||
|
||||
- [ ] `DBA.md` — Replace Paperclip→Control Plane, company→system, CEO→orchestrator, board→operator
|
||||
- [ ] `GIT_ADMIN.md` — Same replacements as DBA.md
|
||||
- [ ] `doc/MULTI-PROVIDER-ARCHITECTURE.md` — Update `agentRole === 'ceo'` → `'orchestrator'`
|
||||
- [ ] `doc/CONTROLPLANE-IMPLEMENTATION-PLAN.md` — Update schema references, terminology
|
||||
- [ ] `doc/CONTROLPLANE-MESSAGE-CONTRACT.md` — Update agent examples
|
||||
- [ ] `doc/CONTROLPLANE-AGENT-ROLES.md` — Update role descriptions
|
||||
- [ ] `doc/CONTROLPLANE-ARCHITECTURE.md` — Update terminology
|
||||
- [ ] `docs/public/architecture/controlplane.md` — Update "company" references
|
||||
- [ ] `docs/public/install/controlplane-install.md` — Update terminology
|
||||
- [ ] `CHANGELOG.md` — Add entry for naming rename
|
||||
|
||||
### New File
|
||||
|
||||
- [ ] Create `CLAWDIE.md` — primary orchestrator identity file (copy of SOUL.md with AGENT_NAME-specific content)
|
||||
- If AGENT_NAME changes, this file should be regenerated
|
||||
- `resolveIdentityFile()` in `controlplane-runner.ts` looks for `{AGENT_NAME.toUpperCase()}.md` first, falls back to `SOUL.md`
|
||||
|
||||
### Testing (FreeBSD only)
|
||||
|
||||
- [ ] Run `npx vitest run` — verify all tests pass with new naming
|
||||
- [ ] Run `npm run build` — verify production build works
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **`getDefaultAgents(agentName)`** is a function now, not a static array. The main agent's `id` equals `AGENT_NAME`.
|
||||
2. **`DEFAULT_AGENTS`** export still exists (calls `getDefaultAgents('clawdie')`) for backward compat in tests.
|
||||
3. **Identity file resolution**: `{AGENT_NAME.toUpperCase()}.md` → fallback `SOUL.md`. The `resolveIdentityFile()` function handles this but is currently defined but not yet wired into `buildControlplaneRunCommand` — the FreeBSD agent should wire it in.
|
||||
4. **Schema migration**: The CHECK constraint changed from `'ceo'` to `'orchestrator'`. Existing databases need a migration: `ALTER TABLE agents DROP CONSTRAINT agents_role_check, ADD CONSTRAINT agents_role_check CHECK (role IN ('orchestrator','sysadmin','dba','git_admin'));` and `UPDATE agents SET role='orchestrator' WHERE role='ceo';`.
|
||||
|
||||
## Results
|
||||
|
||||
- Build: pass (`tsc --noEmit` clean)
|
||||
- Tests: NOT YET RUN (needs FreeBSD)
|
||||
|
||||
## Delete After
|
||||
|
||||
```bash
|
||||
git rm doc/NAMING-HANDOFF.md
|
||||
```
|
||||
|
|
@ -17,7 +17,7 @@ import pg from 'pg';
|
|||
|
||||
import { AGENT_NAME, MEMORY_DB_URL } from '../src/config.js';
|
||||
import {
|
||||
DEFAULT_AGENTS,
|
||||
getDefaultAgents,
|
||||
copySkills,
|
||||
generatePassword,
|
||||
getAgents,
|
||||
|
|
@ -64,8 +64,9 @@ export async function run(_args: string[]): Promise<void> {
|
|||
appendSetupLog('schema migration: ready');
|
||||
|
||||
// ── 2. Provision default agents ──────────────────────────────────
|
||||
const defaultAgents = getDefaultAgents(AGENT_NAME);
|
||||
logger.info('Provisioning default agents...');
|
||||
for (const agent of DEFAULT_AGENTS) {
|
||||
for (const agent of defaultAgents) {
|
||||
await upsertAgent(pool, agent);
|
||||
logger.info({ id: agent.id, role: agent.role }, 'Agent ready');
|
||||
appendSetupLog(`agent ready: ${agent.id} (${agent.role})`);
|
||||
|
|
@ -111,7 +112,7 @@ export async function run(_args: string[]): Promise<void> {
|
|||
10,
|
||||
);
|
||||
|
||||
for (const agent of DEFAULT_AGENTS) {
|
||||
for (const agent of defaultAgents) {
|
||||
const agentTokens = Math.floor((dailyTokens * agent.budget_allocation_pct) / 100);
|
||||
await upsertBudget(pool, agent.id, agentTokens);
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ describe('Agent Session Persistence', () => {
|
|||
});
|
||||
|
||||
it('parses timestamps as strings', () => {
|
||||
writeSessionEntry(tmpDir, 'ceo', makeEntry(), WORKSPACE);
|
||||
const session = loadSession(tmpDir, 'ceo', WORKSPACE);
|
||||
writeSessionEntry(tmpDir, 'clawdie', makeEntry(), WORKSPACE);
|
||||
const session = loadSession(tmpDir, 'clawdie', WORKSPACE);
|
||||
expect(typeof session.entries[0].timestamp).toBe('string');
|
||||
});
|
||||
|
||||
|
|
@ -126,14 +126,14 @@ describe('Agent Session Persistence', () => {
|
|||
});
|
||||
|
||||
it('handles empty file without error', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'ceo.jsonl'), '', 'utf-8');
|
||||
const session = loadSession(tmpDir, 'ceo', WORKSPACE);
|
||||
fs.writeFileSync(path.join(tmpDir, 'clawdie.jsonl'), '', 'utf-8');
|
||||
const session = loadSession(tmpDir, 'clawdie', WORKSPACE);
|
||||
expect(session.entries).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles file with only newlines without error', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'ceo.jsonl'), '\n\n\n', 'utf-8');
|
||||
const session = loadSession(tmpDir, 'ceo', WORKSPACE);
|
||||
fs.writeFileSync(path.join(tmpDir, 'clawdie.jsonl'), '\n\n\n', 'utf-8');
|
||||
const session = loadSession(tmpDir, 'clawdie', WORKSPACE);
|
||||
expect(session.entries).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -176,11 +176,11 @@ describe('Agent Session Persistence', () => {
|
|||
|
||||
it('prune rewrites file with only kept entries', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
writeSessionEntry(tmpDir, 'ceo', makeEntry({ task: `Task ${i}` }), WORKSPACE);
|
||||
writeSessionEntry(tmpDir, 'clawdie', makeEntry({ task: `Task ${i}` }), WORKSPACE);
|
||||
}
|
||||
const session = loadSession(tmpDir, 'ceo', WORKSPACE);
|
||||
const session = loadSession(tmpDir, 'clawdie', WORKSPACE);
|
||||
pruneOldEntries(session, 2);
|
||||
const reloaded = loadSession(tmpDir, 'ceo', WORKSPACE);
|
||||
const reloaded = loadSession(tmpDir, 'clawdie', WORKSPACE);
|
||||
expect(reloaded.entries).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ import {
|
|||
// ── Mock pool ──────────────────────────────────────────────────────────────
|
||||
|
||||
const defaultAgents: Agent[] = [
|
||||
{ id: 'ceo', role: 'ceo', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, budget_allocation_pct: 80, api_key_hash: null, created_at: new Date() },
|
||||
{ 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() },
|
||||
];
|
||||
|
||||
const defaultBudgets: Budget[] = [
|
||||
{ agent_id: 'ceo', daily_tokens: 80000, spent_today: 20000, remaining: 60000, hard_limit_exceeded: false, reset_at: new Date() },
|
||||
{ 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() },
|
||||
];
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ describe('Control Plane HTTP API — contract tests', () => {
|
|||
it('hard_limit_exceeded is true when remaining <= 0', async () => {
|
||||
const { handler } = await setup({
|
||||
budgets: [
|
||||
{ agent_id: 'ceo', daily_tokens: 80000, spent_today: 85000, remaining: -5000, hard_limit_exceeded: true, reset_at: new Date() },
|
||||
{ agent_id: 'clawdie', daily_tokens: 80000, spent_today: 85000, remaining: -5000, hard_limit_exceeded: true, reset_at: new Date() },
|
||||
],
|
||||
});
|
||||
const res = await makeRequest(handler, 'GET', '/api/controlplane/state', undefined, validAuth);
|
||||
|
|
@ -289,7 +289,7 @@ describe('Control Plane HTTP API — contract tests', () => {
|
|||
const { handler } = await setup();
|
||||
const res = await makeRequest(handler, 'POST', '/api/controlplane/activity', {
|
||||
event_type: 'unknown_event',
|
||||
agent_id: 'ceo',
|
||||
agent_id: 'clawdie',
|
||||
}, validAuth);
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ function makePool(budgets: Budget[], updateResults?: { rowCount: number }) {
|
|||
};
|
||||
}
|
||||
|
||||
const ceoBudget = (spent = 0): Budget => ({
|
||||
agent_id: 'ceo',
|
||||
const mainBudget = (spent = 0): Budget => ({
|
||||
agent_id: 'clawdie',
|
||||
daily_tokens: 80000,
|
||||
spent_today: spent,
|
||||
remaining: 80000 - spent,
|
||||
|
|
@ -140,9 +140,9 @@ describe('Control Plane Budget Enforcement', () => {
|
|||
expect(result.dailyTokens).toBe(5000);
|
||||
});
|
||||
|
||||
it('ceo allocation is 80% of daily_tokens', async () => {
|
||||
const pool = makePool([ceoBudget(0)]);
|
||||
const result = await checkBudget(pool as unknown as Pool, 'ceo', 100);
|
||||
it('orchestrator allocation is 80% of daily_tokens', async () => {
|
||||
const pool = makePool([mainBudget(0)]);
|
||||
const result = await checkBudget(pool as unknown as Pool, 'clawdie', 100);
|
||||
expect(result.dailyTokens).toBe(80000);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const DEFAULT_APPROVAL_THRESHOLDS: AgentThresholds = {
|
|||
sysadmin: 2000,
|
||||
dba: 3000,
|
||||
git_admin: 2000,
|
||||
ceo: Infinity,
|
||||
orchestrator: Infinity,
|
||||
};
|
||||
|
||||
// ── Check ──────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import pg from 'pg';
|
|||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export type AgentRole = 'ceo' | 'sysadmin' | 'dba' | 'git_admin';
|
||||
export type AgentRole = 'orchestrator' | 'sysadmin' | 'dba' | 'git_admin';
|
||||
|
||||
export interface Agent {
|
||||
id: string;
|
||||
|
|
@ -64,47 +64,51 @@ export interface ActivityEvent {
|
|||
|
||||
// ── Default agent definitions ──────────────────────────────────────────────
|
||||
|
||||
export const DEFAULT_AGENTS: Omit<Agent, 'api_key_hash' | 'created_at'>[] = [
|
||||
{
|
||||
id: 'ceo',
|
||||
role: 'ceo',
|
||||
adapter: 'pi-local',
|
||||
heartbeat_enabled: false,
|
||||
heartbeat_interval_sec: null,
|
||||
budget_allocation_pct: 80,
|
||||
},
|
||||
{
|
||||
id: 'sysadmin',
|
||||
role: 'sysadmin',
|
||||
adapter: 'pi-local',
|
||||
heartbeat_enabled: true,
|
||||
heartbeat_interval_sec: 86400,
|
||||
budget_allocation_pct: 10,
|
||||
},
|
||||
{
|
||||
id: 'dba',
|
||||
role: 'dba',
|
||||
adapter: 'pi-local',
|
||||
heartbeat_enabled: false,
|
||||
heartbeat_interval_sec: null,
|
||||
budget_allocation_pct: 5,
|
||||
},
|
||||
{
|
||||
id: 'git_admin',
|
||||
role: 'git_admin',
|
||||
adapter: 'pi-local',
|
||||
heartbeat_enabled: false,
|
||||
heartbeat_interval_sec: null,
|
||||
budget_allocation_pct: 5,
|
||||
},
|
||||
];
|
||||
export function getDefaultAgents(agentName: string): Omit<Agent, 'api_key_hash' | 'created_at'>[] {
|
||||
return [
|
||||
{
|
||||
id: agentName,
|
||||
role: 'orchestrator',
|
||||
adapter: 'pi-local',
|
||||
heartbeat_enabled: false,
|
||||
heartbeat_interval_sec: null,
|
||||
budget_allocation_pct: 80,
|
||||
},
|
||||
{
|
||||
id: 'sysadmin',
|
||||
role: 'sysadmin',
|
||||
adapter: 'pi-local',
|
||||
heartbeat_enabled: true,
|
||||
heartbeat_interval_sec: 86400,
|
||||
budget_allocation_pct: 10,
|
||||
},
|
||||
{
|
||||
id: 'dba',
|
||||
role: 'dba',
|
||||
adapter: 'pi-local',
|
||||
heartbeat_enabled: false,
|
||||
heartbeat_interval_sec: null,
|
||||
budget_allocation_pct: 5,
|
||||
},
|
||||
{
|
||||
id: 'git_admin',
|
||||
role: 'git_admin',
|
||||
adapter: 'pi-local',
|
||||
heartbeat_enabled: false,
|
||||
heartbeat_interval_sec: null,
|
||||
budget_allocation_pct: 5,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const DEFAULT_AGENTS = getDefaultAgents('clawdie');
|
||||
|
||||
// ── Schema migration ───────────────────────────────────────────────────────
|
||||
|
||||
export const CONTROLPLANE_SCHEMA_SQL = `
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id TEXT PRIMARY KEY,
|
||||
role TEXT NOT NULL CHECK (role IN ('ceo','sysadmin','dba','git_admin')),
|
||||
role TEXT NOT NULL CHECK (role IN ('orchestrator','sysadmin','dba','git_admin')),
|
||||
adapter TEXT NOT NULL DEFAULT 'pi-local',
|
||||
heartbeat_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
heartbeat_interval_sec INTEGER,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface ControlplaneHeartbeatConfig {
|
|||
sessionCwd: string;
|
||||
skillsDir: string;
|
||||
apiKey: string;
|
||||
agentName: string;
|
||||
tickIntervalMs: number;
|
||||
maxSessionEntries: number;
|
||||
}
|
||||
|
|
@ -180,9 +181,9 @@ export function startControlplaneHeartbeatLoop(config: ControlplaneHeartbeatConf
|
|||
await runAgentHeartbeat(config, agent.id, 'interval_elapsed');
|
||||
}
|
||||
|
||||
const onDemandTasks = await getTasksByRole(config.pool, 'ceo');
|
||||
const onDemandTasks = await getTasksByRole(config.pool, 'orchestrator');
|
||||
for (const task of onDemandTasks.filter((t) => t.status === 'pending')) {
|
||||
const assignedTo = task.assigned_to ?? 'ceo';
|
||||
const assignedTo = task.assigned_to ?? config.agentName;
|
||||
await runAgentHeartbeat(config, assignedTo, 'assignment', task.id);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -36,12 +36,23 @@ export interface RunResult {
|
|||
const CONTROLPLANE_API_URL = 'http://localhost:3100';
|
||||
|
||||
const AGENT_IDENTITY_FILES: Record<string, string> = {
|
||||
ceo: 'SOUL.md',
|
||||
sysadmin: 'SYSADMIN.md',
|
||||
dba: 'DBA.md',
|
||||
git_admin: 'GIT_ADMIN.md',
|
||||
};
|
||||
|
||||
export function resolveIdentityFile(agentId: string, agentName: string, workspaceCwd: string): string {
|
||||
if (AGENT_IDENTITY_FILES[agentId]) {
|
||||
return path.join(workspaceCwd, AGENT_IDENTITY_FILES[agentId]);
|
||||
}
|
||||
const fs = require('fs');
|
||||
const primaryPath = path.join(workspaceCwd, `${agentName.toUpperCase()}.md`);
|
||||
if (fs.existsSync(primaryPath)) {
|
||||
return primaryPath;
|
||||
}
|
||||
return path.join(workspaceCwd, 'SOUL.md');
|
||||
}
|
||||
|
||||
// ── Build ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function buildControlplaneRunCommand(opts: ControlplaneRunOptions): RunResult {
|
||||
|
|
@ -60,7 +71,7 @@ export function buildControlplaneRunCommand(opts: ControlplaneRunOptions): RunRe
|
|||
|
||||
const sessionFile = path.join(absSessionCwd, `${agentId}.jsonl`);
|
||||
const skillsDir = path.join(workspaceCwd, 'data', 'skills');
|
||||
const identityFile = opts.identityFile ?? path.join(workspaceCwd, AGENT_IDENTITY_FILES[agentId] ?? 'SOUL.md');
|
||||
const identityFile = opts.identityFile ?? path.join(workspaceCwd, 'SOUL.md');
|
||||
const prompt = opts.prompt ?? `Control plane wake: ${wakeReason}`;
|
||||
|
||||
const args: string[] = [
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ beforeEach(() => {
|
|||
|
||||
describe('Control Plane Provisioning', () => {
|
||||
describe('agent creation', () => {
|
||||
it('DEFAULT_AGENTS contains CEO agent with role "ceo"', () => {
|
||||
const ceo = DEFAULT_AGENTS.find((a) => a.id === 'ceo');
|
||||
expect(ceo).toBeDefined();
|
||||
expect(ceo?.role).toBe('ceo');
|
||||
it('DEFAULT_AGENTS contains orchestrator agent with role "orchestrator"', () => {
|
||||
const main = DEFAULT_AGENTS.find((a) => a.role === 'orchestrator');
|
||||
expect(main).toBeDefined();
|
||||
expect(main?.role).toBe('orchestrator');
|
||||
});
|
||||
|
||||
it('DEFAULT_AGENTS contains Sysadmin agent with role "sysadmin"', () => {
|
||||
|
|
@ -79,10 +79,10 @@ describe('Control Plane Provisioning', () => {
|
|||
});
|
||||
|
||||
describe('heartbeat configuration', () => {
|
||||
it('CEO has heartbeat_enabled = false', () => {
|
||||
const ceo = DEFAULT_AGENTS.find((a) => a.id === 'ceo')!;
|
||||
expect(ceo.heartbeat_enabled).toBe(false);
|
||||
expect(ceo.heartbeat_interval_sec).toBeNull();
|
||||
it('Orchestrator has heartbeat_enabled = false', () => {
|
||||
const main = DEFAULT_AGENTS.find((a) => a.role === 'orchestrator')!;
|
||||
expect(main.heartbeat_enabled).toBe(false);
|
||||
expect(main.heartbeat_interval_sec).toBeNull();
|
||||
});
|
||||
|
||||
it('Sysadmin has heartbeat_enabled = true and interval = 86400', () => {
|
||||
|
|
@ -105,9 +105,9 @@ describe('Control Plane Provisioning', () => {
|
|||
});
|
||||
|
||||
describe('budget allocation', () => {
|
||||
it('CEO gets 80% allocation', () => {
|
||||
const ceo = DEFAULT_AGENTS.find((a) => a.id === 'ceo')!;
|
||||
expect(ceo.budget_allocation_pct).toBe(80);
|
||||
it('Orchestrator gets 80% allocation', () => {
|
||||
const main = DEFAULT_AGENTS.find((a) => a.role === 'orchestrator')!;
|
||||
expect(main.budget_allocation_pct).toBe(80);
|
||||
});
|
||||
|
||||
it('Sysadmin gets 10% allocation', () => {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ function makeConfig(pool: Pool): ControlplaneHeartbeatConfig {
|
|||
sessionCwd: sessionDir,
|
||||
skillsDir,
|
||||
apiKey: 'test-key',
|
||||
agentName: 'clawdie',
|
||||
tickIntervalMs: 30000,
|
||||
maxSessionEntries: 100,
|
||||
};
|
||||
|
|
@ -157,11 +158,11 @@ describe('Telegram → Control Plane Bridge', () => {
|
|||
expect(result?.assignedTo).toBe('git_admin');
|
||||
});
|
||||
|
||||
it('routes unrecognised agent messages to ceo', async () => {
|
||||
writeSkill('general', ['review company status', 'company overview']);
|
||||
it('routes unrecognised agent messages to orchestrator', async () => {
|
||||
writeSkill('general', ['review system status', 'system overview']);
|
||||
const pool = makePool();
|
||||
const result = await bridgeTelegramMessage(pool, makeConfig(pool), 'review company status');
|
||||
expect(result?.assignedTo).toBe('ceo');
|
||||
const result = await bridgeTelegramMessage(pool, makeConfig(pool), 'review system status');
|
||||
expect(result?.assignedTo).toBe('clawdie');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -31,25 +31,23 @@ export interface TelegramBridgeResult {
|
|||
// ── Agent routing ──────────────────────────────────────────────────────────
|
||||
|
||||
// Maps skill category keywords to agent roles.
|
||||
// Sysadmin handles infra; DBA handles data; git_admin handles code; CEO gets everything else.
|
||||
// Sysadmin handles infra; DBA handles data; git_admin 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, /log/i, /reboot/i, /cert/i, /nginx/i, /tailscale/i] },
|
||||
{ role: 'dba', patterns: [/database/i, /postgres/i, /backup/i, /vacuum/i, /query/i, /db\b/i, /migration/i] },
|
||||
{ role: 'git_admin', patterns: [/git\b/i, /repo/i, /commit/i, /merge/i, /branch/i, /pull request/i, /\bpr\b/i] },
|
||||
];
|
||||
|
||||
function inferRole(message: string, skillName: string | undefined): string {
|
||||
// Prefer skill-based routing if we have a match
|
||||
function inferRole(message: string, skillName: string | undefined, fallbackAgent: string): string {
|
||||
if (skillName) {
|
||||
for (const { role, patterns } of ROLE_KEYWORDS) {
|
||||
if (patterns.some((p) => p.test(skillName))) return role;
|
||||
}
|
||||
}
|
||||
// Fall back to message content
|
||||
for (const { role, patterns } of ROLE_KEYWORDS) {
|
||||
if (patterns.some((p) => p.test(message))) return role;
|
||||
}
|
||||
return 'ceo';
|
||||
return fallbackAgent;
|
||||
}
|
||||
|
||||
// ── Bridge ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -69,7 +67,7 @@ export async function bridgeTelegramMessage(
|
|||
}
|
||||
|
||||
const skillName = match.skill?.name;
|
||||
const assignedTo = inferRole(message, skillName);
|
||||
const assignedTo = inferRole(message, skillName, config.agentName);
|
||||
const taskId = `TG-${crypto.randomUUID().slice(0, 8).toUpperCase()}`;
|
||||
|
||||
await createTask(pool, {
|
||||
|
|
|
|||
|
|
@ -52,14 +52,14 @@ afterEach(() => {
|
|||
});
|
||||
|
||||
const defaultAgents: Agent[] = [
|
||||
{ id: 'ceo', role: 'ceo', adapter: 'pi-local', heartbeat_enabled: false, heartbeat_interval_sec: null, budget_allocation_pct: 80, api_key_hash: null, created_at: new Date() },
|
||||
{ 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() },
|
||||
];
|
||||
|
||||
const defaultBudgets: Budget[] = [
|
||||
{ agent_id: 'ceo', daily_tokens: 80000, spent_today: 0, remaining: 80000, hard_limit_exceeded: false, reset_at: new Date() },
|
||||
{ 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() },
|
||||
|
|
@ -99,6 +99,7 @@ function makeConfig(pool: Pool): ControlplaneHeartbeatConfig {
|
|||
sessionCwd: sessionDir,
|
||||
skillsDir,
|
||||
apiKey: 'test-key',
|
||||
agentName: 'clawdie',
|
||||
tickIntervalMs: 30000,
|
||||
maxSessionEntries: 100,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
|
||||
import {
|
||||
AGENT_NAME,
|
||||
ASSISTANT_NAME,
|
||||
CONTROLPLANE_API_PORT,
|
||||
CONTROLPLANE_SHARED_SECRET,
|
||||
|
|
@ -652,10 +653,11 @@ async function main(): Promise<void> {
|
|||
sessionCwd: sessionDir,
|
||||
skillsDir: PI_TUI_SKILLS_PATH,
|
||||
apiKey: CONTROLPLANE_SHARED_SECRET || OPENAI_API_KEY,
|
||||
agentName: AGENT_NAME,
|
||||
tickIntervalMs: 30000,
|
||||
maxSessionEntries: 100,
|
||||
};
|
||||
startControlplaneHeartbeatLoop(heartbeatConfig);
|
||||
startControlplaneHeartbeatLoop(heartbeatConfig!);
|
||||
|
||||
startIpcWatcher({
|
||||
sendMessage: (jid, text) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue