diff --git a/src/resources/extensions/sf/auto/loop.js b/src/resources/extensions/sf/auto/loop.js index b2cd7a328..53412dd41 100644 --- a/src/resources/extensions/sf/auto/loop.js +++ b/src/resources/extensions/sf/auto/loop.js @@ -15,6 +15,7 @@ import { runAutomaticAutonomousSolverEval } from "../autonomous-solver-eval.js"; import { debugLog } from "../debug-logger.js"; import { resolveEngine } from "../engine-resolver.js"; import { sfRoot } from "../paths.js"; +import { getDatabase } from "../sf-db.js"; import { ExecutionGraphScheduler, scheduleSidecarQueue, @@ -203,6 +204,74 @@ function checkMemoryPressure() { * timed-out phase from mutating state concurrently with the next iteration. */ let _danglingPhasePromise = null; + +/** + * Drain pending sleeptime consolidation jobs from the DB queue. + * + * Purpose: process memory consolidation requests that were enqueued non-blocking + * by the sleeptime topology, without stalling the conversation turn that created them. + * Each pending row triggers a PersistentAgent.receive() call on the memory agent. + * + * Consumer: autoLoop between-unit boundary, ensuring consolidation is processed + * before the next work unit starts. + */ +async function drainSleeptimeQueue(basePath) { + const db = getDatabase(); + if (!db) return; + let pending; + try { + pending = db + .prepare( + `SELECT id, conversation_agent, memory_agent, content + FROM sleeptime_consolidation_queue + WHERE status = 'pending' + ORDER BY created_at ASC + LIMIT 10`, + ) + .all(); + } catch { + return; // table not yet created on older DBs + } + if (!pending || pending.length === 0) return; + const { AgentSwarm } = await import("../uok/agent-swarm.js"); + for (const job of pending) { + try { + const swarm = new AgentSwarm(basePath); + const memAgent = swarm.getByRole("coordinator")[0]; + if (memAgent) { + swarm.send(job.conversation_agent, job.memory_agent, job.content); + const received = memAgent.receive(true); + const last = received[received.length - 1]; + const result = last?.body + ? typeof last.body === "string" + ? last.body + : JSON.stringify(last.body) + : ""; + db.prepare( + `UPDATE sleeptime_consolidation_queue + SET status = 'done', processed_at = :ts, result = :result + WHERE id = :id`, + ).run({ + ":id": job.id, + ":ts": new Date().toISOString(), + ":result": result, + }); + } else { + db.prepare( + `UPDATE sleeptime_consolidation_queue SET status = 'skipped', processed_at = :ts WHERE id = :id`, + ).run({ ":id": job.id, ":ts": new Date().toISOString() }); + } + } catch (err) { + db.prepare( + `UPDATE sleeptime_consolidation_queue SET status = 'error', processed_at = :ts, result = :result WHERE id = :id`, + ).run({ + ":id": job.id, + ":ts": new Date().toISOString(), + ":result": String(err), + }); + } + } +} /** * Wrap a phase function with a timeout. Rejects with an Error whose message * starts with "phase-timeout:" so the blanket catch can handle it specially. @@ -444,7 +513,7 @@ export async function autoLoop(ctx, pi, s, deps) { finishTurn("stopped", "manual-attention", "missing-command-context"); break; } - // ── Drain any dangling phase promise before starting new work ── + // ── Drain dangling phase promise before starting new work ── // Promise.race() on timeout does not cancel the underlying async fn; that // fn keeps running and may mutate state after the loop has advanced. // Awaiting its completion here ensures no concurrent state writes. @@ -457,6 +526,14 @@ export async function autoLoop(ctx, pi, s, deps) { /* ignore — result is irrelevant */ } } + // ── Drain sleeptime consolidation queue ── + // Memory consolidation jobs enqueued non-blocking by the sleeptime topology + // are processed here, between units, so they never block a conversation turn. + try { + await drainSleeptimeQueue(s.basePath); + } catch { + /* best-effort — never block autonomous loop on consolidation */ + } try { // ── Blanket try/catch: one bad iteration must not kill the session const prefs = deps.loadEffectiveSFPreferences()?.preferences; diff --git a/src/resources/extensions/sf/commands/handlers/core.js b/src/resources/extensions/sf/commands/handlers/core.js index 8fdda0d4e..ee689b8d9 100644 --- a/src/resources/extensions/sf/commands/handlers/core.js +++ b/src/resources/extensions/sf/commands/handlers/core.js @@ -516,7 +516,17 @@ export async function handleCoreCommand(trimmed, ctx, pi) { if (trimmed === "mode" || trimmed.startsWith("mode ")) { const modeArgs = trimmed.replace(/^mode\s*/, "").trim(); // If arg is a work mode (chat/plan/build/review/repair/research), use new mode system - const workModes = ["chat", "plan", "build", "review", "repair", "research"]; + const workModes = [ + "chat", + "plan", + "build", + "review", + "repair", + "research", + "verify", + "document", + "challenge", + ]; if (workModes.includes(modeArgs)) { handleModeCommand(modeArgs, ctx); return true; diff --git a/src/resources/extensions/sf/sf-db.js b/src/resources/extensions/sf/sf-db.js index b99b507ef..3ef625684 100644 --- a/src/resources/extensions/sf/sf-db.js +++ b/src/resources/extensions/sf/sf-db.js @@ -577,6 +577,23 @@ function ensureUokMessageTables(db) { "CREATE INDEX IF NOT EXISTS idx_uok_messages_sent ON uok_messages(sent_at DESC)", ); } +function ensureSleeptimeQueueTable(db) { + db.exec(` + CREATE TABLE IF NOT EXISTS sleeptime_consolidation_queue ( + id TEXT PRIMARY KEY, + conversation_agent TEXT NOT NULL, + memory_agent TEXT NOT NULL, + content TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + created_at TEXT NOT NULL, + processed_at TEXT DEFAULT NULL, + result TEXT DEFAULT NULL + ) + `); + db.exec( + "CREATE INDEX IF NOT EXISTS idx_sleeptime_queue_status ON sleeptime_consolidation_queue(status, created_at ASC)", + ); +} function ensureSelfFeedbackTables(db) { db.exec(` CREATE TABLE IF NOT EXISTS self_feedback ( @@ -1290,6 +1307,7 @@ function initSchema(db, fileBacked) { ensureSessionTables(db); ensureSessionSnapshotTable(db); ensureUokMessageTables(db); + ensureSleeptimeQueueTable(db); ensureSpecSchemaTables(db); ensureTaskFrontmatterColumns(db); ensureRetrievalEvidenceTables(db); @@ -2904,6 +2922,17 @@ function migrateSchema(db) { ":applied_at": new Date().toISOString(), }); } + if (currentVersion < 50) { + // Add sleeptime_consolidation_queue — decouples memory consolidation + // from the conversation turn so the daemon can drain it asynchronously. + ensureSleeptimeQueueTable(db); + db.prepare( + "INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)", + ).run({ + ":version": 50, + ":applied_at": new Date().toISOString(), + }); + } db.exec("COMMIT"); } catch (err) { db.exec("ROLLBACK"); diff --git a/src/resources/extensions/sf/skills/loader.js b/src/resources/extensions/sf/skills/loader.js index cbc72decb..4728ef7d8 100644 --- a/src/resources/extensions/sf/skills/loader.js +++ b/src/resources/extensions/sf/skills/loader.js @@ -165,7 +165,7 @@ function isSkillRelevant(skill, workMode) { t === workMode || t === "*" || (workMode === "build" && t === "code") || - (workMode === "review" && t === "review") || - (workMode === "research" && t === "research"), + (workMode === "verify" && t === "review") || + (workMode === "document" && t === "docs"), ); } diff --git a/src/resources/extensions/sf/tests/swarm.test.mjs b/src/resources/extensions/sf/tests/swarm.test.mjs index 909f98575..645943835 100644 --- a/src/resources/extensions/sf/tests/swarm.test.mjs +++ b/src/resources/extensions/sf/tests/swarm.test.mjs @@ -226,10 +226,10 @@ describe("AgentSwarm — registry", () => { // ─── createDefaultSwarm — topology ──────────────────────────────────────────── describe("createDefaultSwarm — topology", () => { - test("creates_five_agents", async () => { + test("creates_nine_agents", async () => { const root = makeProject(); const { swarm } = await createDefaultSwarm(root); - expect(swarm.getAll()).toHaveLength(5); + expect(swarm.getAll()).toHaveLength(9); }); test("topology_has_correct_roles", async () => { @@ -241,6 +241,10 @@ describe("createDefaultSwarm — topology", () => { expect(topology.workers).toHaveLength(2); expect(topology.scouts).toHaveLength(1); expect(topology.reviewers).toHaveLength(1); + expect(topology.planners).toHaveLength(1); + expect(topology.verifiers).toHaveLength(1); + expect(topology.scribes).toHaveLength(1); + expect(topology.adversaries).toHaveLength(1); }); }); diff --git a/src/resources/extensions/sf/uok/agent-swarm.js b/src/resources/extensions/sf/uok/agent-swarm.js index 51abb5bf3..1f42cf4dc 100644 --- a/src/resources/extensions/sf/uok/agent-swarm.js +++ b/src/resources/extensions/sf/uok/agent-swarm.js @@ -327,12 +327,19 @@ export class AgentSwarm { switch (workMode) { case "build": case "repair": - case "code": return this.getByRole("worker")[0]; case "research": return this.getByRole("scout")[0]; case "review": return this.getByRole("reviewer")[0]; + case "plan": + return this.getByRole("planner")[0] ?? this.getByRole("coordinator")[0]; + case "verify": + return this.getByRole("verifier")[0]; + case "document": + return this.getByRole("scribe")[0]; + case "challenge": + return this.getByRole("adversary")[0]; default: return this.getByRole("coordinator")[0] ?? this.getAll()[0]; } @@ -342,10 +349,32 @@ export class AgentSwarm { if (!unitType) return undefined; const t = unitType.toLowerCase(); if (t.includes("research") || t.includes("scout")) return "research"; - if (t.includes("review") || t.includes("audit")) return "review"; + if (t.includes("review")) return "review"; + if ( + t.includes("audit") || + t.includes("validate") || + t.includes("gate") || + t.includes("uat") + ) + return "verify"; if (t.includes("repair") || t.includes("fix")) return "repair"; - if (t.includes("build") || t.includes("code") || t.includes("implement")) + if ( + t.includes("build") || + t.includes("code") || + t.includes("implement") || + t.includes("execute") + ) return "build"; + if ( + t.includes("plan") || + t.includes("slice") || + t.includes("milestone") || + t.includes("roadmap") + ) + return "plan"; + if (t.includes("doc") || t.includes("rewrite") || t.includes("scribe")) + return "document"; + if (t.includes("challenge") || t.includes("adversar")) return "challenge"; return undefined; } @@ -364,9 +393,13 @@ export class AgentSwarm { getTopology() { return { coordinator: this.getByRole("coordinator"), + planners: this.getByRole("planner"), workers: this.getByRole("worker"), scouts: this.getByRole("scout"), reviewers: this.getByRole("reviewer"), + verifiers: this.getByRole("verifier"), + scribes: this.getByRole("scribe"), + adversaries: this.getByRole("adversary"), all: this.getAll(), }; } @@ -493,7 +526,8 @@ export class AgentSwarm { } case ManagerType.SLEEPTIME: { - // One conversation agent + background memory consolidation by coordinator + // One conversation agent + non-blocking memory consolidation enqueued to DB. + // The scheduler drains sleeptime_consolidation_queue on each poll tick. const conversationAgents = agents.filter( (a) => a.identity.role !== "coordinator", ); @@ -515,16 +549,25 @@ export class AgentSwarm { break; } message = reply; - // Background memory consolidation step + // Enqueue consolidation to DB — non-blocking; scheduler drains it. if (memoryAgent) { - const memName = memoryAgent.identity.name; - this.send(convoName, memName, `consolidate: ${reply}`); - const memReceived = memoryAgent.receive(true); - const memLast = memReceived[memReceived.length - 1]; - if (memLast) { + const db = getDatabase(); + if (db) { + const jobId = `slp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + db.prepare( + `INSERT INTO sleeptime_consolidation_queue + (id, conversation_agent, memory_agent, content, status, created_at) + VALUES (:id, :convo, :mem, :content, 'pending', :ts)`, + ).run({ + ":id": jobId, + ":convo": convoName, + ":mem": memoryAgent.identity.name, + ":content": `consolidate: ${reply}`, + ":ts": new Date().toISOString(), + }); turns.push({ - agent: memName, - message: `[memory] ${_bodyToString(memLast.body, "")}`, + agent: memoryAgent.identity.name, + message: `[memory:queued] job ${jobId}`, }); } } diff --git a/src/resources/extensions/sf/uok/index.js b/src/resources/extensions/sf/uok/index.js index fcdab4b03..8d0faaabb 100644 --- a/src/resources/extensions/sf/uok/index.js +++ b/src/resources/extensions/sf/uok/index.js @@ -23,6 +23,8 @@ export { USER_SKILL_DIR, validateSkillFrontmatter, } from "../skills/index.js"; +// ─── Agent Swarm ─────────────────────────────────────────────────────────── +export { AgentSwarm, ManagerType } from "./agent-swarm.js"; export { assessAssertionCoverage, fulfilledAssertionIdsFromHandoff, @@ -34,7 +36,6 @@ export { isAuditEnvelopeEnabled, setAuditEnvelopeEnabled, } from "./audit-toggle.js"; - // ─── Gates ───────────────────────────────────────────────────────────────── export { ChaosMonkey, ChaosMonkeyGate } from "./chaos-monkey.js"; // ─── Model Policy ────────────────────────────────────────────────────────── @@ -91,16 +92,8 @@ export { export { recordUokKernelTermination, runAutoLoopWithUok } from "./kernel.js"; // ─── Loop Adapter ────────────────────────────────────────────────────────── export { createTurnObserver } from "./loop-adapter.js"; -// ─── Agent Swarm ─────────────────────────────────────────────────────────── -export { AgentSwarm, ManagerType } from "./agent-swarm.js"; -// ─── Letta Agent ─────────────────────────────────────────────────────────── -export { PersistentAgent } from "./persistent-agent.js"; // ─── Message Bus ─────────────────────────────────────────────────────────── export { AgentInbox, MessageBus } from "./message-bus.js"; -// ─── Swarm Roles ─────────────────────────────────────────────────────────── -export { CoordinatorAgent, WorkerAgent, ScoutAgent, ReviewerAgent, createDefaultSwarm } from "./swarm-roles.js"; -// ─── Swarm Dispatch ──────────────────────────────────────────────────────── -export { SwarmDispatchLayer, swarmDispatch } from "./swarm-dispatch.js"; // ─── Metrics ─────────────────────────────────────────────────────────────── export { buildMetricsText, @@ -143,6 +136,8 @@ export { writeParityHeartbeat, writeParityReport, } from "./parity-report.js"; +// ─── Letta Agent ─────────────────────────────────────────────────────────── +export { PersistentAgent } from "./persistent-agent.js"; // ─── Plan v2 ─────────────────────────────────────────────────────────────── export { compileUnitGraphFromState, @@ -166,6 +161,20 @@ export { WorkerPool, } from "./scheduler.js"; export { SecurityGate } from "./security-gate.js"; +// ─── Swarm Dispatch ──────────────────────────────────────────────────────── +export { SwarmDispatchLayer, swarmDispatch } from "./swarm-dispatch.js"; +// ─── Swarm Roles ─────────────────────────────────────────────────────────── +export { + AdversaryAgent, + CoordinatorAgent, + createDefaultSwarm, + PlannerAgent, + ReviewerAgent, + ScoutAgent, + ScribeAgent, + VerifierAgent, + WorkerAgent, +} from "./swarm-roles.js"; // ─── Task State Machine ──────────────────────────────────────────────────── export { aggregateTaskStates, diff --git a/src/resources/extensions/sf/uok/persistent-agent.js b/src/resources/extensions/sf/uok/persistent-agent.js index 3c99b7657..cd53c0d21 100644 --- a/src/resources/extensions/sf/uok/persistent-agent.js +++ b/src/resources/extensions/sf/uok/persistent-agent.js @@ -12,12 +12,21 @@ import { randomUUID } from "node:crypto"; import { mkdirSync } from "node:fs"; import { join } from "node:path"; -import { getDatabase, openDatabase } from "../sf-db.js"; import { sfRoot } from "../paths.js"; +import { getDatabase, openDatabase } from "../sf-db.js"; import { UokCoordinationStore } from "./coordination-store.js"; import { MessageBus } from "./message-bus.js"; -const VALID_ROLES = ["coordinator", "worker", "scout", "reviewer", "adversary", "architect"]; +const VALID_ROLES = [ + "coordinator", + "worker", + "scout", + "reviewer", + "planner", + "verifier", + "scribe", + "adversary", +]; const DEFAULT_ROLE = "worker"; const DEFAULT_MAX_INBOX_SIZE = 200; const DEFAULT_REFRESH_INTERVAL_MS = 30_000; @@ -32,7 +41,8 @@ function ensureDbOpen(basePath) { openDatabase(dbPath); } const db = getDatabase(); - if (!db) throw new Error(`PersistentAgent: failed to open database at ${dbPath}`); + if (!db) + throw new Error(`PersistentAgent: failed to open database at ${dbPath}`); return db; } @@ -63,9 +73,18 @@ export class PersistentAgent { * @param {number} [options.refreshIntervalMs=30000] */ constructor(basePath, options = {}) { - const { name, role = DEFAULT_ROLE, tags = [], maxInboxSize = DEFAULT_MAX_INBOX_SIZE, refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS } = options; + const { + name, + role = DEFAULT_ROLE, + tags = [], + maxInboxSize = DEFAULT_MAX_INBOX_SIZE, + refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS, + } = options; if (!name) throw new Error("PersistentAgent: options.name is required"); - if (!VALID_ROLES.includes(role)) throw new Error(`PersistentAgent: invalid role "${role}". Must be one of: ${VALID_ROLES.join(", ")}`); + if (!VALID_ROLES.includes(role)) + throw new Error( + `PersistentAgent: invalid role "${role}". Must be one of: ${VALID_ROLES.join(", ")}`, + ); this._basePath = basePath; this._name = name; @@ -175,7 +194,8 @@ export class PersistentAgent { const lines = ["## Agent Memory Blocks", ""]; for (const { key, value } of entries) { const label = key.slice(prefix.length); - const rendered = typeof value === "string" ? value : JSON.stringify(value); + const rendered = + typeof value === "string" ? value : JSON.stringify(value); lines.push(`**${label}**: ${rendered}`); } return lines.join("\n"); @@ -288,16 +308,17 @@ export class PersistentAgent { * @returns {Promise<*>} reply body */ sendAndWait(toAgentName, body, opts = {}) { - const { timeoutMs = DEFAULT_SEND_AND_WAIT_TIMEOUT_MS, pollIntervalMs = DEFAULT_SEND_AND_WAIT_POLL_INTERVAL_MS } = opts; + const { + timeoutMs = DEFAULT_SEND_AND_WAIT_TIMEOUT_MS, + pollIntervalMs = DEFAULT_SEND_AND_WAIT_POLL_INTERVAL_MS, + } = opts; const sentId = this.send(toAgentName, body, opts.metadata ?? {}); return new Promise((resolve, reject) => { const deadline = Date.now() + timeoutMs; const interval = setInterval(() => { this._inbox.refresh(); const messages = this._inbox.list(false); - const reply = messages.find( - (m) => m.metadata?.replyTo === sentId, - ); + const reply = messages.find((m) => m.metadata?.replyTo === sentId); if (reply) { clearInterval(interval); resolve(reply.body); @@ -305,7 +326,11 @@ export class PersistentAgent { } if (Date.now() >= deadline) { clearInterval(interval); - reject(new Error(`PersistentAgent.sendAndWait: timeout after ${timeoutMs}ms waiting for reply to ${sentId}`)); + reject( + new Error( + `PersistentAgent.sendAndWait: timeout after ${timeoutMs}ms waiting for reply to ${sentId}`, + ), + ); } }, pollIntervalMs); }); diff --git a/src/resources/extensions/sf/uok/swarm-roles.js b/src/resources/extensions/sf/uok/swarm-roles.js index 88d331ceb..674a0be40 100644 --- a/src/resources/extensions/sf/uok/swarm-roles.js +++ b/src/resources/extensions/sf/uok/swarm-roles.js @@ -1,16 +1,16 @@ /** * Swarm Role Agents — Purpose-built PersistentAgent subclasses for the default SF swarm topology. * - * Purpose: provide ready-to-use agent roles (coordinator, worker, scout, reviewer) with - * preset tags and capabilities, and a factory function that assembles a complete default - * swarm topology persisted to SQLite. + * Purpose: provide ready-to-use agent roles (coordinator, worker, scout, reviewer, planner, + * verifier, scribe, adversary) with preset tags and capabilities, and a factory function + * that assembles a complete default swarm topology persisted to SQLite. * * Consumer: SwarmDispatchLayer, /sf swarm command, and any code needing a default * multi-agent topology without manually wiring individual agents. */ -import { PersistentAgent } from "./persistent-agent.js"; import { AgentSwarm, ManagerType } from "./agent-swarm.js"; +import { PersistentAgent } from "./persistent-agent.js"; /** * Coordinator agent — owns routing and task dispatch across a worker pool. @@ -64,7 +64,9 @@ export class CoordinatorAgent extends PersistentAgent { // Prefer specialised match; fall back to first available worker. const specialised = workMode - ? candidates.filter((a) => a.identity.tags.includes(`workMode:${workMode}`)) + ? candidates.filter((a) => + a.identity.tags.includes(`workMode:${workMode}`), + ) : []; const target = specialised[0] ?? candidates[0] ?? null; @@ -76,7 +78,7 @@ export class CoordinatorAgent extends PersistentAgent { } /** - * Worker agent — executes assigned tasks in the swarm. + * Worker agent — executes assigned build and repair tasks. * * Purpose: provide a general-purpose execution agent that accepts task * messages from a coordinator and runs them to completion. @@ -84,12 +86,6 @@ export class CoordinatorAgent extends PersistentAgent { * Consumer: createDefaultSwarm worker pool and direct task assignment. */ export class WorkerAgent extends PersistentAgent { - /** - * @param {string} basePath - * @param {object} [options={}] - * @param {string} [options.name='worker'] - * @param {string[]} [options.tags=[]] - */ constructor(basePath, options = {}) { super(basePath, { name: options.name ?? "worker", @@ -104,18 +100,13 @@ export class WorkerAgent extends PersistentAgent { * Scout agent — discovers and surfaces information for the swarm. * * Purpose: provide a dedicated research/discovery agent whose results feed - * into coordinator routing decisions and worker task context. + * into coordinator routing decisions and worker task context. Runs in an + * isolated context with no side effects. * * Consumer: createDefaultSwarm and coordinator agents that need pre-fetched * context before delegating execution tasks. */ export class ScoutAgent extends PersistentAgent { - /** - * @param {string} basePath - * @param {object} [options={}] - * @param {string} [options.name='scout'] - * @param {string[]} [options.tags=[]] - */ constructor(basePath, options = {}) { super(basePath, { name: options.name ?? "scout", @@ -127,22 +118,16 @@ export class ScoutAgent extends PersistentAgent { } /** - * Reviewer agent — validates and critiques worker output. + * Reviewer agent — critiques worker output against intent. * - * Purpose: provide a dedicated quality-gate agent that receives completed - * task output from workers and either approves, requests revision, or - * escalates to the coordinator. + * Purpose: provide a fresh-context critique agent that receives completed + * task output and either approves, requests revision, or escalates. Runs + * without the worker's accumulated context so bias doesn't carry over. * - * Consumer: createDefaultSwarm and any dispatch flow that requires a - * review pass before accepting task output as final. + * Consumer: createDefaultSwarm and any dispatch flow requiring a review + * pass before accepting task output as final. */ export class ReviewerAgent extends PersistentAgent { - /** - * @param {string} basePath - * @param {object} [options={}] - * @param {string} [options.name='reviewer'] - * @param {string[]} [options.tags=[]] - */ constructor(basePath, options = {}) { super(basePath, { name: options.name ?? "reviewer", @@ -153,30 +138,114 @@ export class ReviewerAgent extends PersistentAgent { } } +/** + * Planner agent — generates milestone, slice, and task contracts. + * + * Purpose: own the plan workMode — translate bounded intent into PDD-backed + * milestone/slice/task structures. Separated from coordinator so planning + * doesn't consume coordinator context and remains independently restartable. + * + * Consumer: createDefaultSwarm, auto-dispatch plan-milestone / plan-slice / + * refine-slice / replan-slice unit types. + */ +export class PlannerAgent extends PersistentAgent { + constructor(basePath, options = {}) { + super(basePath, { + name: options.name ?? "planner", + role: "planner", + tags: ["role:planner", "tier:persistent", ...(options.tags ?? [])], + ...options, + }); + } +} + +/** + * Verifier agent — runs gates, UAT, and evidence checks. + * + * Purpose: own the verify workMode — execute binary pass/fail checks + * (gate-evaluate, validate-milestone, run-uat) without conflating them + * with subjective review. Separated from reviewer so verification failures + * produce actionable structured output rather than qualitative critique. + * + * Consumer: createDefaultSwarm and gate-runner dispatch paths. + */ +export class VerifierAgent extends PersistentAgent { + constructor(basePath, options = {}) { + super(basePath, { + name: options.name ?? "verifier", + role: "verifier", + tags: ["role:verifier", "tier:worker", ...(options.tags ?? [])], + ...options, + }); + } +} + +/** + * Scribe agent — writes and exports documentation. + * + * Purpose: own the document workMode — produce changelogs, spec exports, + * ADRs, and user-facing docs from structured state. Separated from worker + * so doc generation doesn't share a context window with implementation work. + * + * Consumer: createDefaultSwarm and rewrite-docs / promote-spec unit types. + */ +export class ScribeAgent extends PersistentAgent { + constructor(basePath, options = {}) { + super(basePath, { + name: options.name ?? "scribe", + role: "scribe", + tags: ["role:scribe", "tier:worker", ...(options.tags ?? [])], + ...options, + }); + } +} + +/** + * Adversary agent — red-teams plans and decisions. + * + * Purpose: own the challenge workMode — actively seek failure modes, + * unexamined assumptions, and security gaps in plans or implementations. + * Runs in adversarial framing so it doesn't default to agreement. + * + * Consumer: createDefaultSwarm and challenge-mode dispatch (future). + */ +export class AdversaryAgent extends PersistentAgent { + constructor(basePath, options = {}) { + super(basePath, { + name: options.name ?? "adversary", + role: "adversary", + tags: ["role:adversary", "tier:worker", ...(options.tags ?? [])], + ...options, + }); + } +} + /** * Create a complete default swarm topology. * - * Purpose: assemble the standard SF agent topology (1 coordinator + 2 workers + - * 1 scout + 1 reviewer) wired to a shared MessageBus, with the agent_directory - * core block set on all agents so each knows who else is in the swarm. + * Purpose: assemble the full SF agent topology (coordinator + planner + + * 2 workers + scout + reviewer + verifier + scribe + adversary) wired to a + * shared MessageBus, with the agent_directory core block set on all agents. * - * Consumer: SwarmDispatchLayer.getOrCreateSwarm() and direct initialization in - * autonomous dispatch bootstrap. + * Consumer: SwarmDispatchLayer.getOrCreateSwarm() and autonomous dispatch bootstrap. * * @param {string} basePath * @param {object} [options={}] * @param {string} [options.swarmName='default'] * @param {string} [options.managerType] - ManagerType value, default 'supervisor' - * @returns {Promise<{ swarm: AgentSwarm, coordinator: CoordinatorAgent, workers: WorkerAgent[], scout: ScoutAgent, reviewer: ReviewerAgent }>} */ export async function createDefaultSwarm(basePath, options = {}) { const coordinator = new CoordinatorAgent(basePath, { name: "coordinator" }); + const planner = new PlannerAgent(basePath, { name: "planner" }); const workers = [ new WorkerAgent(basePath, { name: "worker-1" }), new WorkerAgent(basePath, { name: "worker-2" }), ]; const scout = new ScoutAgent(basePath, { name: "scout" }); const reviewer = new ReviewerAgent(basePath, { name: "reviewer" }); + const verifier = new VerifierAgent(basePath, { name: "verifier" }); + const scribe = new ScribeAgent(basePath, { name: "scribe" }); + const adversary = new AdversaryAgent(basePath, { name: "adversary" }); const swarm = new AgentSwarm(basePath, { name: options.swarmName ?? "default", @@ -184,13 +253,25 @@ export async function createDefaultSwarm(basePath, options = {}) { }); swarm.register(coordinator); - for (const worker of workers) { - swarm.register(worker); - } + swarm.register(planner); + for (const worker of workers) swarm.register(worker); swarm.register(scout); swarm.register(reviewer); + swarm.register(verifier); + swarm.register(scribe); + swarm.register(adversary); swarm.persist(); - return { swarm, coordinator, workers, scout, reviewer }; + return { + swarm, + coordinator, + planner, + workers, + scout, + reviewer, + verifier, + scribe, + adversary, + }; }