Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/storage/src/migrations/013-scout-executor-proposals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const version = 13;
export const name = 'scout-executor-proposals';

export const sql = `
ALTER TABLE tasks ADD COLUMN proposal_status TEXT CHECK(proposal_status IN ('proposed','approved','archived'));
ALTER TABLE tasks ADD COLUMN approved_by TEXT;
ALTER TABLE tasks ADD COLUMN observation_evidence_ids TEXT;

ALTER TABLE agent_profiles ADD COLUMN role TEXT NOT NULL DEFAULT 'executor';
ALTER TABLE agent_profiles ADD COLUMN open_proposal_count INTEGER NOT NULL DEFAULT 0;

CREATE INDEX IF NOT EXISTS idx_task_threads_proposal_status
ON tasks(proposal_status)
WHERE proposal_status IS NOT NULL;
`;
38 changes: 37 additions & 1 deletion packages/storage/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,14 @@ CREATE TABLE IF NOT EXISTS tasks (
created_by TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
proposal_status TEXT CHECK(proposal_status IN ('proposed','approved','archived')),
approved_by TEXT,
observation_evidence_ids TEXT,
UNIQUE(repo_root, branch)
);
CREATE INDEX IF NOT EXISTS idx_task_threads_proposal_status
ON tasks(proposal_status)
WHERE proposal_status IS NOT NULL;

CREATE TABLE IF NOT EXISTS task_participants (
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
Expand Down Expand Up @@ -200,6 +206,8 @@ CREATE INDEX IF NOT EXISTS idx_reinforcements_proposal ON proposal_reinforcement
CREATE TABLE IF NOT EXISTS agent_profiles (
agent TEXT PRIMARY KEY,
capabilities TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'executor',
open_proposal_count INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);

Expand Down Expand Up @@ -317,7 +325,7 @@ CREATE INDEX IF NOT EXISTS idx_task_run_attempts_status
CREATE INDEX IF NOT EXISTS idx_task_run_attempts_parent
ON task_run_attempts(parent_attempt_id);

INSERT OR IGNORE INTO schema_version(version) VALUES (12);
INSERT OR IGNORE INTO schema_version(version) VALUES (13);
`;

/**
Expand Down Expand Up @@ -371,6 +379,31 @@ export const COLUMN_MIGRATIONS: ReadonlyArray<{ table: string; column: string; s
column: 'error_message',
sql: 'ALTER TABLE mcp_metrics ADD COLUMN error_message TEXT',
},
{
table: 'tasks',
column: 'proposal_status',
sql: "ALTER TABLE tasks ADD COLUMN proposal_status TEXT CHECK(proposal_status IN ('proposed','approved','archived'))",
},
{
table: 'tasks',
column: 'approved_by',
sql: 'ALTER TABLE tasks ADD COLUMN approved_by TEXT',
},
{
table: 'tasks',
column: 'observation_evidence_ids',
sql: 'ALTER TABLE tasks ADD COLUMN observation_evidence_ids TEXT',
},
{
table: 'agent_profiles',
column: 'role',
sql: "ALTER TABLE agent_profiles ADD COLUMN role TEXT NOT NULL DEFAULT 'executor'",
},
{
table: 'agent_profiles',
column: 'open_proposal_count',
sql: 'ALTER TABLE agent_profiles ADD COLUMN open_proposal_count INTEGER NOT NULL DEFAULT 0',
},
];

export const POST_MIGRATION_SQL = `
Expand All @@ -381,4 +414,7 @@ CREATE INDEX IF NOT EXISTS idx_observations_task_kind_ts ON observations(task_id
CREATE INDEX IF NOT EXISTS idx_observations_reply_to ON observations(reply_to);
CREATE INDEX IF NOT EXISTS idx_summaries_scope_ts ON summaries(scope, ts DESC);
CREATE INDEX IF NOT EXISTS idx_mcp_metrics_error_ts ON mcp_metrics(ok, error_code, ts DESC);
CREATE INDEX IF NOT EXISTS idx_task_threads_proposal_status
ON tasks(proposal_status)
WHERE proposal_status IS NOT NULL;
`;
10 changes: 8 additions & 2 deletions packages/storage/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,9 @@ export class Storage {
created_by: p.created_by,
created_at: now,
updated_at: now,
proposal_status: null,
approved_by: null,
observation_evidence_ids: null,
};
}

Expand Down Expand Up @@ -2579,11 +2582,14 @@ export class Storage {
*/
upsertAgentProfile(p: NewAgentProfile): void {
const now = p.updated_at ?? Date.now();
const existing = this.getAgentProfile(p.agent);
const role = p.role ?? existing?.role ?? 'executor';
const openProposalCount = p.open_proposal_count ?? existing?.open_proposal_count ?? 0;
this.db
.prepare(
'INSERT OR REPLACE INTO agent_profiles(agent, capabilities, updated_at) VALUES (?, ?, ?)',
'INSERT OR REPLACE INTO agent_profiles(agent, capabilities, role, open_proposal_count, updated_at) VALUES (?, ?, ?, ?, ?)',
)
.run(p.agent, p.capabilities, now);
.run(p.agent, p.capabilities, role, openProposalCount, now);
}

getAgentProfile(agent: string): AgentProfileRow | undefined {
Expand Down
7 changes: 7 additions & 0 deletions packages/storage/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export interface TaskRow {
created_by: string;
created_at: number;
updated_at: number;
proposal_status: 'proposed' | 'approved' | 'archived' | null;
approved_by: string | null;
observation_evidence_ids: string | null;
}

export interface NewTask {
Expand Down Expand Up @@ -222,12 +225,16 @@ export interface NewReinforcement {
export interface AgentProfileRow {
agent: string;
capabilities: string;
role: 'scout' | 'executor' | 'queen';
open_proposal_count: number;
updated_at: number;
}

export interface NewAgentProfile {
agent: string;
capabilities: string;
role?: 'scout' | 'executor' | 'queen';
open_proposal_count?: number;
updated_at?: number;
}

Expand Down
23 changes: 23 additions & 0 deletions packages/storage/test/agent-profiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ describe('agent profiles storage', () => {
expect(row).toEqual({
agent: 'claude',
capabilities: JSON.stringify({ ui_work: 0.9, api_work: 0.3 }),
role: 'executor',
open_proposal_count: 0,
updated_at: 1_000,
});
});

it('upsert + get round-trips scout role and open proposal count', () => {
storage.upsertAgentProfile({
agent: 'scout-a',
capabilities: '{}',
role: 'scout',
open_proposal_count: 2,
updated_at: 1_000,
});
expect(storage.getAgentProfile('scout-a')).toEqual({
agent: 'scout-a',
capabilities: '{}',
role: 'scout',
open_proposal_count: 2,
updated_at: 1_000,
});
});
Expand All @@ -36,6 +55,8 @@ describe('agent profiles storage', () => {
storage.upsertAgentProfile({
agent: 'codex',
capabilities: JSON.stringify({ api_work: 0.5 }),
role: 'scout',
open_proposal_count: 1,
updated_at: 1_000,
});
storage.upsertAgentProfile({
Expand All @@ -46,6 +67,8 @@ describe('agent profiles storage', () => {
const row = storage.getAgentProfile('codex');
if (!row) throw new Error('expected codex profile');
expect(JSON.parse(row.capabilities)).toEqual({ api_work: 0.9, infra_work: 0.8 });
expect(row.role).toBe('scout');
expect(row.open_proposal_count).toBe(1);
expect(row?.updated_at).toBe(2_000);
});

Expand Down
60 changes: 60 additions & 0 deletions packages/storage/test/migrations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import Database from 'better-sqlite3';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { Storage } from '../src/index.js';

let dir: string;
let storage: Storage;

beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'colony-migrations-'));
storage = new Storage(join(dir, 'test.db'));
});

afterEach(() => {
storage.close();
rmSync(dir, { recursive: true, force: true });
});

describe('storage migrations', () => {
it('creates scout/executor proposal columns and defaults on a fresh DB', () => {
const db = new Database(join(dir, 'test.db'));
try {
expect(columnNames(db, 'tasks')).toEqual(
expect.arrayContaining(['proposal_status', 'approved_by', 'observation_evidence_ids']),
);
expect(columnNames(db, 'agent_profiles')).toEqual(
expect.arrayContaining(['role', 'open_proposal_count']),
);

db.prepare('INSERT INTO agent_profiles(agent, capabilities, updated_at) VALUES (?, ?, ?)').run(
'codex',
'{}',
1_000,
);
expect(db.prepare('SELECT role, open_proposal_count FROM agent_profiles').get()).toEqual({
role: 'executor',
open_proposal_count: 0,
});
expect(indexNames(db, 'tasks')).toContain('idx_task_threads_proposal_status');
} finally {
db.close();
}
});
});

function columnNames(db: Database.Database, table: string): string[] {
return db
.prepare(`PRAGMA table_info(${table})`)
.all()
.map((row) => (row as { name: string }).name);
}

function indexNames(db: Database.Database, table: string): string[] {
return db
.prepare(`PRAGMA index_list(${table})`)
.all()
.map((row) => (row as { name: string }).name);
}
Loading