From 1d5424b1345f1a8904fab2e72fb85fb613b6455b Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 23 Apr 2026 18:05:48 +0200 Subject: [PATCH] Add a bounded hivemind MVP inside the monorepo This introduces apps/hivemind-demo, a deterministic coordinator/researcher/builder/reviewer/verifier loop that writes JSON state, compacts checkpoints, exposes a CLI, and ships tests/docs. The package stays self-contained so the existing cavemem MCP server remains unchanged while we prove the anti-context-bloat orchestration shape. Constraint: Existing repo already centers on memory + MCP, so the MVP had to land as an isolated workspace app Constraint: Verification required local dependency install because the worktree had no node_modules Rejected: Extending apps/mcp-server/hivemind into a full runtime | would mix read-only telemetry with execution orchestration Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep this package deterministic until a real ModelProvider/ToolRunner is wired behind the same checkpoint contract Tested: pnpm --filter @cavemem/hivemind-demo typecheck; pnpm --filter @cavemem/hivemind-demo test; pnpm --filter @cavemem/hivemind-demo build; node apps/hivemind-demo/dist/cli/index.js --demo --quiet --data-dir /tmp/hivemind-demo-run-final Not-tested: GitHub PR/merge flow blocked by invalid gh auth token --- .gitignore | 2 + apps/hivemind-demo/README.md | 129 +++++++++++++ apps/hivemind-demo/data/runs/.gitkeep | 0 apps/hivemind-demo/package.json | 22 +++ apps/hivemind-demo/src/agents/builder.ts | 91 ++++++++++ apps/hivemind-demo/src/agents/coordinator.ts | 161 +++++++++++++++++ apps/hivemind-demo/src/agents/researcher.ts | 72 ++++++++ apps/hivemind-demo/src/agents/reviewer.ts | 91 ++++++++++ apps/hivemind-demo/src/agents/verifier.ts | 111 ++++++++++++ apps/hivemind-demo/src/cli/index.ts | 164 +++++++++++++++++ apps/hivemind-demo/src/core/checkpoint.ts | 36 ++++ apps/hivemind-demo/src/core/orchestrator.ts | 114 ++++++++++++ apps/hivemind-demo/src/core/state.ts | 180 +++++++++++++++++++ apps/hivemind-demo/src/core/types.ts | 172 ++++++++++++++++++ apps/hivemind-demo/src/index.ts | 22 +++ apps/hivemind-demo/src/utils/ids.ts | 15 ++ apps/hivemind-demo/src/utils/logger.ts | 10 ++ apps/hivemind-demo/test/orchestrator.test.ts | 130 ++++++++++++++ apps/hivemind-demo/tsconfig.json | 7 + pnpm-lock.yaml | 12 ++ 20 files changed, 1541 insertions(+) create mode 100644 apps/hivemind-demo/README.md create mode 100644 apps/hivemind-demo/data/runs/.gitkeep create mode 100644 apps/hivemind-demo/package.json create mode 100644 apps/hivemind-demo/src/agents/builder.ts create mode 100644 apps/hivemind-demo/src/agents/coordinator.ts create mode 100644 apps/hivemind-demo/src/agents/researcher.ts create mode 100644 apps/hivemind-demo/src/agents/reviewer.ts create mode 100644 apps/hivemind-demo/src/agents/verifier.ts create mode 100644 apps/hivemind-demo/src/cli/index.ts create mode 100644 apps/hivemind-demo/src/core/checkpoint.ts create mode 100644 apps/hivemind-demo/src/core/orchestrator.ts create mode 100644 apps/hivemind-demo/src/core/state.ts create mode 100644 apps/hivemind-demo/src/core/types.ts create mode 100644 apps/hivemind-demo/src/index.ts create mode 100644 apps/hivemind-demo/src/utils/ids.ts create mode 100644 apps/hivemind-demo/src/utils/logger.ts create mode 100644 apps/hivemind-demo/test/orchestrator.test.ts create mode 100644 apps/hivemind-demo/tsconfig.json diff --git a/.gitignore b/.gitignore index eeeb1a5..61e0946 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ scripts/install-vscode-active-agents-extension.js oh-my-codex/ .omx/state/agent-file-locks.json # multiagent-safety:END +apps/hivemind-demo/data/runs/* +!apps/hivemind-demo/data/runs/.gitkeep diff --git a/apps/hivemind-demo/README.md b/apps/hivemind-demo/README.md new file mode 100644 index 0000000..2675717 --- /dev/null +++ b/apps/hivemind-demo/README.md @@ -0,0 +1,129 @@ +# Hivemind Demo + +Minimal local multi-agent MVP for bounded software-task execution. + +This package simulates a small hivemind with five deterministic agents: + +- `Coordinator` +- `Researcher` +- `Builder` +- `Reviewer` +- `Verifier` + +Each run writes shared JSON state after every step, compacts progress into checkpoints, and requires verifier approval before returning success. + +## File Tree + +```text +apps/hivemind-demo/ + data/ + runs/ + src/ + agents/ + coordinator.ts + researcher.ts + builder.ts + reviewer.ts + verifier.ts + core/ + orchestrator.ts + state.ts + checkpoint.ts + types.ts + cli/ + index.ts + utils/ + logger.ts + ids.ts + index.ts + test/ + orchestrator.test.ts + package.json + tsconfig.json + README.md +``` + +## What It Does + +- Accepts one task from the CLI +- Creates a bounded plan +- Runs research, build, review, and verification phases +- Writes `state.json` after every phase +- Creates checkpoint summaries every few steps +- Stops on verifier approval, retry exhaustion, or max-turn failure + +Persisted run output lives under `data/runs//`: + +- `state.json` +- `checkpoints/*.json` +- `final-output.json` when verification succeeds + +## Run It + +From the package directory: + +```sh +pnpm install +pnpm run demo +pnpm build +node dist/cli/index.js "Create a local TypeScript CLI that plans, reviews, and verifies a task with checkpoint summaries." +``` + +Or point output somewhere disposable: + +```sh +node dist/cli/index.js --data-dir /tmp/hivemind-runs --demo +``` + +## Example Usage + +```sh +node dist/cli/index.js "Build a local TypeScript CLI with JSON state, checkpoints, README, and verifier approval." +``` + +Example output: + +```text +[hivemind] coordinator/plan: Coordinator planned a 4-step hivemind loop. +[hivemind] researcher/research: Researcher captured 5 constraints and 4 focus areas. +[hivemind] builder/build: Builder produced attempt 1 with 16 files in scope. +[hivemind] reviewer/review: Reviewer approved builder output. +[hivemind] coordinator/decide: Coordinator sends approved build to verifier. +[hivemind] verifier/verify: Verifier approved final output. +Run: run-2026-04-23T18-00-00-000Z +Status: completed +Run dir: /.../apps/hivemind-demo/data/runs/run-2026-04-23T18-00-00-000Z +Checkpoints: 3 +``` + +## Demo Flow + +1. Coordinator creates a bounded plan and subtasks. +2. Researcher extracts constraints, assumptions, and focus areas from the task text. +3. Builder turns that research into a file tree, implementation plan, test plan, and checkpoint policy. +4. Reviewer checks for missing required files, tests, and compaction rules. +5. Coordinator decides whether to retry the builder or continue. +6. Verifier approves only when plan, artifacts, and checkpoints exist. + +## Limitations + +- Agent behavior is deterministic. No real LLM reasoning is exercised. +- Execution is sequential. There is no parallel worker scheduling yet. +- Tool use is not wired in. The system only simulates agent decisions. +- Checkpoints are simple summaries, not semantic compression or retrieval. + +## Extending With Real Model / Tool Calls + +The extension points already exist in `src/core/types.ts`: + +- `ModelProvider` +- `ToolRunner` + +To replace the deterministic agents later: + +1. Implement a `ModelProvider` that turns agent prompts + state into model output. +2. Implement a `ToolRunner` for file, web, or code tools. +3. Swap the current agent bodies to call those interfaces instead of heuristic rules. +4. Keep the checkpoint contract unchanged so coordination cost stays bounded. + +That keeps the MVP shape stable while the inner reasoning surface becomes real. diff --git a/apps/hivemind-demo/data/runs/.gitkeep b/apps/hivemind-demo/data/runs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/hivemind-demo/package.json b/apps/hivemind-demo/package.json new file mode 100644 index 0000000..f69f064 --- /dev/null +++ b/apps/hivemind-demo/package.json @@ -0,0 +1,22 @@ +{ + "name": "@cavemem/hivemind-demo", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "bin": { + "cavemem-hivemind-demo": "./dist/cli/index.js" + }, + "scripts": { + "build": "tsup src/index.ts src/cli/index.ts --format esm --dts --clean", + "demo": "pnpm build && node dist/cli/index.js --demo", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "tsup": "^8.3.5", + "typescript": "^5.6.3", + "vitest": "^2.1.5" + } +} diff --git a/apps/hivemind-demo/src/agents/builder.ts b/apps/hivemind-demo/src/agents/builder.ts new file mode 100644 index 0000000..ac80c12 --- /dev/null +++ b/apps/hivemind-demo/src/agents/builder.ts @@ -0,0 +1,91 @@ +import { findLatestArtifact } from '../core/state.js'; +import type { + Agent, + AgentInput, + AgentResult, + BuildArtifactContent, + ResearchArtifactContent, + ReviewArtifactContent, + RunState, +} from '../core/types.js'; + +const BASE_FILE_TREE = [ + 'src/agents/coordinator.ts', + 'src/agents/researcher.ts', + 'src/agents/builder.ts', + 'src/agents/reviewer.ts', + 'src/agents/verifier.ts', + 'src/core/orchestrator.ts', + 'src/core/state.ts', + 'src/core/checkpoint.ts', + 'src/core/types.ts', + 'src/cli/index.ts', + 'src/index.ts', + 'src/utils/logger.ts', + 'src/utils/ids.ts', + 'test/orchestrator.test.ts', + 'README.md', + 'data/runs/.gitkeep', +]; + +export class BuilderAgent implements Agent { + readonly name = 'Builder'; + readonly role = 'builder' as const; + + run(input: AgentInput, state: RunState): AgentResult { + const research = findLatestArtifact(state, 'research')?.content; + const priorReview = findLatestArtifact(state, 'review')?.content; + const revisionNotes = + input.attempt > 1 && priorReview ? priorReview.requiredFixes.map((issue) => `Address review feedback: ${issue}`) : []; + + const content: BuildArtifactContent = { + fileTree: [...BASE_FILE_TREE], + implementationSteps: [ + 'Define core types for run state, subtasks, artifacts, checkpoints, and final output.', + 'Persist `state.json` after every agent step under `data/runs//`.', + 'Run coordinator -> researcher -> builder -> reviewer -> verifier with max-turn and retry guards.', + 'Create compact checkpoints every few steps so workers share only Goal / Done / Blocker / Next.', + 'Require verifier approval before final success is emitted.', + ], + testPlan: [ + 'Happy path: orchestrator completes a bounded task and writes checkpoints.', + 'Failure path: reviewer rejects a build with no test plan or checkpoint policy.', + 'CLI path: the command prints run status, result, and run directory.', + ], + checkpointPolicy: [ + 'Default checkpoint interval = 2 agent steps.', + 'Checkpoint payload carries Goal, Done, Current blocker, and Next batch only.', + 'Coordinator decisions consume checkpoints and active blockers instead of raw history.', + ], + notes: unique([ + ...(research?.recommendedFocus ?? []), + ...revisionNotes, + 'Provider and tool interfaces stay swappable for future real integrations.', + ]), + }; + + return { + status: 'completed', + summary: `Builder produced attempt ${input.attempt} with ${content.fileTree.length} files in scope.`, + details: [ + content.implementationSteps[0] ?? '', + content.implementationSteps[1] ?? '', + content.testPlan[0] ?? '', + ].filter(Boolean), + artifact: { + type: 'build', + label: input.attempt > 1 ? `build-attempt-${input.attempt}` : 'initial-build', + content, + }, + markSubtasks: [ + { id: 'build', status: 'completed', note: `Builder attempt ${input.attempt} ready for review.` }, + { id: 'review', status: 'in_progress', note: 'Reviewing current build.' }, + ], + replaceBlockers: [], + }; + } +} + +function unique(values: string[]): string[] { + return [...new Set(values)]; +} diff --git a/apps/hivemind-demo/src/agents/coordinator.ts b/apps/hivemind-demo/src/agents/coordinator.ts new file mode 100644 index 0000000..f095c8c --- /dev/null +++ b/apps/hivemind-demo/src/agents/coordinator.ts @@ -0,0 +1,161 @@ +import { findLatestArtifact } from '../core/state.js'; +import type { + Agent, + AgentInput, + AgentResult, + PlanArtifactContent, + ReviewArtifactContent, + RunState, + RunSubtask, +} from '../core/types.js'; + +export class CoordinatorAgent implements Agent { + readonly name = 'Coordinator'; + readonly role = 'coordinator' as const; + + run(input: AgentInput, state: RunState): AgentResult { + if (input.phase === 'plan') { + const focus = detectFocus(input.task); + const subtasks = createSubtasks(); + const steps = [ + 'Research constraints and active requirements from the user task.', + 'Build one bounded implementation package with explicit file ownership.', + 'Review the build for gaps, risks, and missing verification.', + 'Verify the run before returning a final result.', + ]; + const artifact: PlanArtifactContent = { + focus, + steps, + subtasks: subtasks.map(({ id, title, owner }) => ({ id, title, owner })), + }; + + return { + status: 'completed', + summary: `Coordinator planned a ${steps.length}-step hivemind loop.`, + details: focus, + artifact: { + type: 'plan', + label: 'coordinator-plan', + content: artifact, + }, + plan: { + steps, + subtasks, + }, + }; + } + + const review = findLatestArtifact(state, 'review')?.content; + if (!review) { + return { + status: 'blocked', + summary: 'Coordinator cannot decide without reviewer output.', + details: ['Review artifact missing.'], + replaceBlockers: ['Review artifact missing.'], + decision: 'escalate', + runStatus: 'blocked', + }; + } + + if (review.approved) { + return { + status: 'completed', + summary: 'Coordinator sends approved build to verifier.', + details: review.strengths, + replaceBlockers: [], + decision: 'send_to_verifier', + markSubtasks: [ + { id: 'review', status: 'completed', note: 'Review approved.' }, + { id: 'verify', status: 'in_progress', note: 'Verifier ready.' }, + ], + }; + } + + if (state.retryCount < state.maxRetries) { + return { + status: 'completed', + summary: `Coordinator requests builder retry ${state.retryCount + 1}/${state.maxRetries}.`, + details: review.requiredFixes, + replaceBlockers: review.requiredFixes, + decision: 'retry_builder', + markSubtasks: [ + { id: 'build', status: 'pending', note: 'Coordinator requested a revised build.' }, + { id: 'review', status: 'pending', note: 'Awaiting revised build.' }, + ], + }; + } + + return { + status: 'blocked', + summary: 'Coordinator escalates after exhausting retries.', + details: review.requiredFixes, + replaceBlockers: review.requiredFixes, + decision: 'escalate', + runStatus: 'blocked', + markSubtasks: [{ id: 'review', status: 'blocked', note: 'Retry budget exhausted.' }], + }; + } +} + +function createSubtasks(): RunSubtask[] { + return [ + { + id: 'research', + title: 'Research facts and constraints', + owner: 'researcher', + status: 'pending', + notes: [], + retryCount: 0, + }, + { + id: 'build', + title: 'Build one bounded implementation proposal', + owner: 'builder', + status: 'pending', + notes: [], + retryCount: 0, + }, + { + id: 'review', + title: 'Review output for risks and missing edges', + owner: 'reviewer', + status: 'pending', + notes: [], + retryCount: 0, + }, + { + id: 'verify', + title: 'Verify result before success', + owner: 'verifier', + status: 'pending', + notes: [], + retryCount: 0, + }, + ]; +} + +function detectFocus(task: string): string[] { + const lower = task.toLowerCase(); + const focus = [ + 'Keep the loop bounded: inspect once, patch once, verify once.', + 'Pass checkpoints instead of full raw history between phases.', + ]; + + if (lower.includes('cli')) { + focus.push('Expose a thin CLI entrypoint for single-task execution.'); + } + if (lower.includes('json') || lower.includes('state') || lower.includes('memory')) { + focus.push('Persist shared state to lightweight JSON files.'); + } + if (lower.includes('checkpoint') || lower.includes('compact')) { + focus.push('Make checkpoint compaction a first-class artifact.'); + } + if (lower.includes('review') || lower.includes('verify')) { + focus.push('Keep reviewer and verifier as mandatory gates.'); + } + if (lower.includes('readme') || lower.includes('demo')) { + focus.push('Ship a README and a runnable demo task.'); + } + + return [...new Set(focus)]; +} diff --git a/apps/hivemind-demo/src/agents/researcher.ts b/apps/hivemind-demo/src/agents/researcher.ts new file mode 100644 index 0000000..b80d700 --- /dev/null +++ b/apps/hivemind-demo/src/agents/researcher.ts @@ -0,0 +1,72 @@ +import type { Agent, AgentInput, AgentResult, ResearchArtifactContent, RunState } from '../core/types.js'; + +export class ResearcherAgent implements Agent { + readonly name = 'Researcher'; + readonly role = 'researcher' as const; + + run(input: AgentInput, _state: RunState): AgentResult { + const lower = input.task.toLowerCase(); + const facts = unique([ + 'Task expects a local MVP rather than remote infrastructure.', + lower.includes('typescript') || lower.includes('node') + ? 'Delivery target is a TypeScript / Node.js codebase.' + : 'Current repo toolchain is TypeScript-first, so the MVP should stay there.', + lower.includes('cli') + ? 'A CLI entrypoint is part of the requested behavior.' + : 'The runnable entrypoint still needs to be explicit.', + ]); + const constraints = unique([ + lower.includes('json') || lower.includes('state') + ? 'Persist shared state in JSON files.' + : 'Keep persistence lightweight and file-based.', + lower.includes('mvp') + ? 'Prefer an MVP over a generalized framework.' + : 'Keep the first version intentionally small.', + 'Avoid external APIs so the demo runs offline.', + 'Checkpoint summaries should replace transcript replay between phases.', + 'Final output must include result, reasoning summary, open risks, and next steps.', + ]); + const assumptions = unique([ + 'Deterministic agent logic is enough to prove the orchestration loop.', + 'One run handles one user task from start to finish.', + 'A reviewer plus verifier gate is enough for the first cut.', + ]); + const recommendedFocus = unique([ + 'Write state after every agent step.', + 'Batch the executor phase instead of drifting through micro-steps.', + lower.includes('readme') || lower.includes('demo') + ? 'Document the flow with a runnable demo task.' + : 'Add README guidance and one demo flow for handoff clarity.', + lower.includes('checkpoint') || lower.includes('compact') + ? 'Checkpoint format should be Goal / Done / Current blocker / Next batch.' + : 'Use compact checkpoints to keep shared context short.', + ]); + + const content: ResearchArtifactContent = { + facts, + constraints, + assumptions, + recommendedFocus, + }; + + return { + status: 'completed', + summary: `Researcher captured ${constraints.length} constraints and ${recommendedFocus.length} focus areas.`, + details: [...facts.slice(0, 2), ...recommendedFocus.slice(0, 2)], + artifact: { + type: 'research', + label: 'task-research', + content, + }, + markSubtasks: [ + { id: 'research', status: 'completed', note: `Captured ${constraints.length} constraints.` }, + { id: 'build', status: 'in_progress', note: 'Research ready for builder.' }, + ], + replaceBlockers: [], + }; + } +} + +function unique(values: string[]): string[] { + return [...new Set(values)]; +} diff --git a/apps/hivemind-demo/src/agents/reviewer.ts b/apps/hivemind-demo/src/agents/reviewer.ts new file mode 100644 index 0000000..546dc51 --- /dev/null +++ b/apps/hivemind-demo/src/agents/reviewer.ts @@ -0,0 +1,91 @@ +import { findLatestArtifact } from '../core/state.js'; +import type { + Agent, + AgentInput, + AgentResult, + BuildArtifactContent, + ReviewArtifactContent, + RunState, +} from '../core/types.js'; + +const REQUIRED_FILES = [ + 'src/agents/coordinator.ts', + 'src/agents/researcher.ts', + 'src/agents/builder.ts', + 'src/agents/reviewer.ts', + 'src/agents/verifier.ts', + 'src/core/orchestrator.ts', + 'src/core/state.ts', + 'src/core/checkpoint.ts', + 'src/core/types.ts', + 'src/cli/index.ts', + 'README.md', + 'test/orchestrator.test.ts', +]; + +export class ReviewerAgent implements Agent { + readonly name = 'Reviewer'; + readonly role = 'reviewer' as const; + + run(_input: AgentInput, state: RunState): AgentResult { + const build = findLatestArtifact(state, 'build')?.content; + if (!build) { + return { + status: 'blocked', + summary: 'Reviewer missing build artifact.', + details: ['Builder must run before reviewer.'], + replaceBlockers: ['Build artifact missing.'], + markSubtasks: [{ id: 'review', status: 'blocked', note: 'No build artifact found.' }], + }; + } + + const issues: string[] = []; + for (const requiredFile of REQUIRED_FILES) { + if (!build.fileTree.includes(requiredFile)) { + issues.push(`Missing required file: ${requiredFile}`); + } + } + if (build.implementationSteps.length < 4) { + issues.push('Implementation steps are too thin.'); + } + if (build.testPlan.length === 0) { + issues.push('Test plan missing.'); + } + if (build.checkpointPolicy.length === 0) { + issues.push('Checkpoint policy missing.'); + } + + const strengths = [ + `File tree covers ${build.fileTree.length} entries.`, + `Implementation plan has ${build.implementationSteps.length} steps.`, + `Test plan has ${build.testPlan.length} verification item(s).`, + `Checkpoint policy has ${build.checkpointPolicy.length} rule(s).`, + ]; + const approved = issues.length === 0; + const content: ReviewArtifactContent = { + approved, + strengths, + issues, + requiredFixes: issues, + }; + + return { + status: approved ? 'completed' : 'blocked', + summary: approved ? 'Reviewer approved builder output.' : `Reviewer found ${issues.length} gap(s).`, + details: approved ? strengths.slice(0, 3) : issues, + artifact: { + type: 'review', + label: approved ? 'review-approved' : 'review-needs-work', + content, + }, + markSubtasks: [ + { + id: 'review', + status: approved ? 'completed' : 'blocked', + note: approved ? 'Ready for verifier.' : 'Builder revision required.', + }, + ], + replaceBlockers: approved ? [] : issues, + }; + } +} diff --git a/apps/hivemind-demo/src/agents/verifier.ts b/apps/hivemind-demo/src/agents/verifier.ts new file mode 100644 index 0000000..bebc8da --- /dev/null +++ b/apps/hivemind-demo/src/agents/verifier.ts @@ -0,0 +1,111 @@ +import { findLatestArtifact } from '../core/state.js'; +import type { + Agent, + AgentInput, + AgentResult, + FinalOutput, + ReviewArtifactContent, + RunState, + VerificationArtifactContent, +} from '../core/types.js'; + +export class VerifierAgent implements Agent { + readonly name = 'Verifier'; + readonly role = 'verifier' as const; + + run(_input: AgentInput, state: RunState): AgentResult { + const blockers: string[] = []; + const review = findLatestArtifact(state, 'review')?.content; + + if (!review) { + blockers.push('Review artifact missing.'); + } else if (!review.approved) { + blockers.push('Latest review is not approved.'); + } + if (state.currentPlan.length === 0) { + blockers.push('Coordinator plan missing.'); + } + if (state.checkpoints.length === 0) { + blockers.push('Checkpoint history missing.'); + } + if (!findLatestArtifact(state, 'research')) { + blockers.push('Research artifact missing.'); + } + if (!findLatestArtifact(state, 'build')) { + blockers.push('Build artifact missing.'); + } + + const evidence = [ + `Plan steps: ${state.currentPlan.length}`, + `Artifacts captured: ${state.artifacts.length}`, + `Checkpoints captured: ${state.checkpoints.length}`, + `Retry count: ${state.retryCount}`, + ]; + + if (blockers.length > 0) { + const blockedContent: VerificationArtifactContent = { + approved: false, + evidence, + openRisks: ['Run stopped before the verifier could approve final success.'], + nextSteps: ['Resolve blockers and rerun verifier.'], + }; + + return { + status: 'blocked', + summary: 'Verifier blocked final success.', + details: blockers, + artifact: { + type: 'verification', + label: 'verifier-blocked', + content: blockedContent, + }, + replaceBlockers: blockers, + runStatus: 'blocked', + markSubtasks: [{ id: 'verify', status: 'blocked', note: 'Verification failed.' }], + }; + } + + const openRisks = [ + 'Agent behavior is deterministic and does not yet exercise real model quality.', + 'The MVP stays sequential; parallel dispatch and conflict handling are future work.', + ]; + const nextSteps = [ + 'Swap deterministic agent bodies with a real `ModelProvider` implementation.', + 'Attach `ToolRunner` adapters for file, web, and code tools.', + 'Track eval metrics like turns, retries, and checkpoint counts across many runs.', + ]; + const finalResult: FinalOutput = { + result: `Completed verified hivemind run for: ${state.originalTask}`, + reasoningSummary: [ + 'Coordinator created a bounded research/build/review/verify loop.', + 'Researcher translated the raw task into constraints and focus areas.', + 'Builder produced a concrete file tree, implementation plan, test plan, and checkpoint policy.', + 'Reviewer approved the proposal before verifier closed the run.', + ], + openRisks, + nextSteps, + verified: true, + }; + const verificationContent: VerificationArtifactContent = { + approved: true, + evidence, + openRisks, + nextSteps, + }; + + return { + status: 'completed', + summary: 'Verifier approved final output.', + details: evidence, + artifact: { + type: 'verification', + label: 'verifier-report', + content: verificationContent, + }, + finalResult, + runStatus: 'completed', + replaceBlockers: [], + markSubtasks: [{ id: 'verify', status: 'completed', note: `Verified with ${state.checkpoints.length} checkpoint(s).` }], + }; + } +} diff --git a/apps/hivemind-demo/src/cli/index.ts b/apps/hivemind-demo/src/cli/index.ts new file mode 100644 index 0000000..b788a46 --- /dev/null +++ b/apps/hivemind-demo/src/cli/index.ts @@ -0,0 +1,164 @@ +#!/usr/bin/env node +import { resolve } from 'node:path'; +import { HivemindOrchestrator } from '../core/orchestrator.js'; +import { createLogger } from '../utils/logger.js'; + +const DEMO_TASK = + 'Create a local TypeScript CLI that breaks one task into research, build, review, and verify phases, stores JSON run state, and keeps compact checkpoints.'; + +interface CliOptions { + task: string; + dataDir: string; + maxTurns: number; + maxRetries: number; + checkpointInterval: number; + demo: boolean; + quiet: boolean; + help: boolean; +} + +main(); + +function main(): void { + try { + const options = parseArgs(process.argv.slice(2)); + if (options.help) { + printUsage(); + return; + } + + const task = options.demo ? DEMO_TASK : options.task; + if (!task) { + printUsage(); + process.exitCode = 1; + return; + } + + const orchestrator = new HivemindOrchestrator({ + dataDir: options.dataDir, + maxTurns: options.maxTurns, + maxRetries: options.maxRetries, + checkpointInterval: options.checkpointInterval, + logger: createLogger(!options.quiet), + }); + const state = orchestrator.run(task); + const runDir = resolve(options.dataDir, state.runId); + + console.log(`Run: ${state.runId}`); + console.log(`Status: ${state.status}`); + console.log(`Run dir: ${runDir}`); + console.log(`Checkpoints: ${state.checkpoints.length}`); + + if (state.finalResult) { + console.log(`Result: ${state.finalResult.result}`); + console.log('Reasoning summary:'); + for (const line of state.finalResult.reasoningSummary) { + console.log(`- ${line}`); + } + console.log('Open risks:'); + for (const line of state.finalResult.openRisks) { + console.log(`- ${line}`); + } + console.log('Next steps:'); + for (const line of state.finalResult.nextSteps) { + console.log(`- ${line}`); + } + } else if (state.blockers.length > 0) { + console.log('Blockers:'); + for (const blocker of state.blockers) { + console.log(`- ${blocker}`); + } + } + + if (state.status !== 'completed') { + process.exitCode = 1; + } + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + } +} + +function parseArgs(argv: string[]): CliOptions { + const taskParts: string[] = []; + const options: CliOptions = { + task: '', + dataDir: resolve(process.cwd(), 'data', 'runs'), + maxTurns: 10, + maxRetries: 1, + checkpointInterval: 2, + demo: false, + quiet: false, + help: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (!arg) continue; + + switch (arg) { + case '--data-dir': + options.dataDir = resolve(readValue(argv, index, '--data-dir')); + index += 1; + break; + case '--max-turns': + options.maxTurns = readInteger(argv, index, '--max-turns'); + index += 1; + break; + case '--max-retries': + options.maxRetries = readInteger(argv, index, '--max-retries'); + index += 1; + break; + case '--checkpoint-interval': + options.checkpointInterval = readInteger(argv, index, '--checkpoint-interval'); + index += 1; + break; + case '--demo': + options.demo = true; + break; + case '--quiet': + options.quiet = true; + break; + case '--help': + case '-h': + options.help = true; + break; + default: + taskParts.push(arg); + break; + } + } + + options.task = taskParts.join(' ').trim(); + return options; +} + +function readValue(argv: string[], index: number, flag: string): string { + const value = argv[index + 1]; + if (!value) { + throw new Error(`${flag} requires a value.`); + } + return value; +} + +function readInteger(argv: string[], index: number, flag: string): number { + const raw = readValue(argv, index, flag); + const value = Number.parseInt(raw, 10); + if (!Number.isInteger(value) || value <= 0) { + throw new Error(`${flag} must be a positive integer.`); + } + return value; +} + +function printUsage(): void { + console.log(`Usage: cavemem-hivemind-demo [options] ""`); + console.log(''); + console.log('Options:'); + console.log(' --demo Run the built-in demo task'); + console.log(' --data-dir Directory for persisted run state'); + console.log(' --max-turns Maximum total agent turns (default: 10)'); + console.log(' --max-retries Builder retry budget after review failures (default: 1)'); + console.log(' --checkpoint-interval Create a checkpoint every N steps (default: 2)'); + console.log(' --quiet Suppress per-step logs'); + console.log(' --help, -h Show this usage text'); +} diff --git a/apps/hivemind-demo/src/core/checkpoint.ts b/apps/hivemind-demo/src/core/checkpoint.ts new file mode 100644 index 0000000..fab967f --- /dev/null +++ b/apps/hivemind-demo/src/core/checkpoint.ts @@ -0,0 +1,36 @@ +import { createCheckpointId } from '../utils/ids.js'; +import type { RunCheckpoint, RunState, StepStatus } from './types.js'; + +const ACTIVE_STATUSES: StepStatus[] = ['pending', 'in_progress', 'blocked']; + +export function shouldCreateCheckpoint(state: RunState, interval: number): boolean { + if (interval <= 0 || state.turn === 0) return false; + const lastCheckpointTurn = state.checkpoints[state.checkpoints.length - 1]?.turn ?? 0; + return state.turn - lastCheckpointTurn >= interval; +} + +export function createCheckpoint(state: RunState): RunCheckpoint { + const done = state.completedSteps.slice(-4); + const blocker = state.blockers[0] ?? null; + const nextBatch = state.subtasks + .filter((subtask) => ACTIVE_STATUSES.includes(subtask.status)) + .slice(0, 3) + .map((subtask) => subtask.title); + const compactSummary = [ + `Goal: ${state.originalTask}`, + `Done: ${done.length > 0 ? done.join('; ') : 'none'}`, + `Current blocker: ${blocker ?? 'none'}`, + `Next batch: ${nextBatch.length > 0 ? nextBatch.join('; ') : 'finish verification'}`, + ].join('\n'); + + return { + id: createCheckpointId(state.checkpoints.length + 1), + turn: state.turn, + goal: state.originalTask, + done: done.length > 0 ? done : ['No completed steps yet.'], + blocker, + nextBatch: nextBatch.length > 0 ? nextBatch : ['Return verified final result.'], + compactSummary, + createdAt: new Date().toISOString(), + }; +} diff --git a/apps/hivemind-demo/src/core/orchestrator.ts b/apps/hivemind-demo/src/core/orchestrator.ts new file mode 100644 index 0000000..2759c0a --- /dev/null +++ b/apps/hivemind-demo/src/core/orchestrator.ts @@ -0,0 +1,114 @@ +import { BuilderAgent } from '../agents/builder.js'; +import { CoordinatorAgent } from '../agents/coordinator.js'; +import { ResearcherAgent } from '../agents/researcher.js'; +import { ReviewerAgent } from '../agents/reviewer.js'; +import { VerifierAgent } from '../agents/verifier.js'; +import { createLogger } from '../utils/logger.js'; +import { createCheckpoint, shouldCreateCheckpoint } from './checkpoint.js'; +import { appendCheckpoint, applyAgentResult, createInitialState, incrementRetryCount, RunStore } from './state.js'; +import type { Agent, AgentInput, AgentResult, OrchestratorOptions, RunState } from './types.js'; + +const DEFAULT_MAX_TURNS = 10; +const DEFAULT_MAX_RETRIES = 1; +const DEFAULT_CHECKPOINT_INTERVAL = 2; + +export class HivemindOrchestrator { + private readonly coordinator = new CoordinatorAgent(); + private readonly researcher = new ResearcherAgent(); + private readonly builder = new BuilderAgent(); + private readonly reviewer = new ReviewerAgent(); + private readonly verifier = new VerifierAgent(); + private readonly options: OrchestratorOptions; + + constructor(options: Partial = {}) { + this.options = { + dataDir: options.dataDir ?? 'data/runs', + maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS, + maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES, + checkpointInterval: options.checkpointInterval ?? DEFAULT_CHECKPOINT_INTERVAL, + logger: options.logger ?? createLogger(true), + }; + } + + run(task: string): RunState { + let state = createInitialState(task, this.options); + const store = new RunStore(this.options.dataDir, state.runId); + store.writeState(state); + + state = this.runAgent(this.coordinator, { task, phase: 'plan', attempt: 0 }, state, store); + state = this.runAgent(this.researcher, { task, phase: 'research', attempt: 0 }, state, store); + + while (state.status === 'in_progress') { + if (state.turn >= state.maxTurns) { + return this.failForTurnBudget(state, store); + } + + state = this.runAgent(this.builder, { task, phase: 'build', attempt: state.retryCount + 1 }, state, store); + state = this.runAgent(this.reviewer, { task, phase: 'review', attempt: state.retryCount + 1 }, state, store); + + const decisionInput: AgentInput = { + task, + phase: 'decide', + attempt: state.retryCount + 1, + }; + const decisionResult = this.coordinator.run(decisionInput, state); + state = this.commitResult(this.coordinator, decisionInput, state, decisionResult, store); + + if (decisionResult.decision === 'retry_builder') { + state = incrementRetryCount(state); + store.writeState(state); + continue; + } + if (decisionResult.decision === 'send_to_verifier') { + break; + } + if (decisionResult.decision === 'escalate') { + return state; + } + } + + if (state.status !== 'in_progress') { + return state; + } + if (state.turn >= state.maxTurns) { + return this.failForTurnBudget(state, store); + } + + return this.runAgent(this.verifier, { task, phase: 'verify', attempt: state.retryCount + 1 }, state, store); + } + + private runAgent(agent: Agent, input: AgentInput, state: RunState, store: RunStore): RunState { + const result = agent.run(input, state); + return this.commitResult(agent, input, state, result, store); + } + + private commitResult( + agent: Agent, + input: AgentInput, + state: RunState, + result: AgentResult, + store: RunStore, + ): RunState { + this.options.logger?.info(`${agent.role}/${input.phase}: ${result.summary}`); + let nextState = applyAgentResult(state, agent.role, input.phase, result); + if (shouldCreateCheckpoint(nextState, this.options.checkpointInterval)) { + const checkpoint = createCheckpoint(nextState); + nextState = appendCheckpoint(nextState, checkpoint); + store.writeCheckpoint(checkpoint); + } + store.writeState(nextState); + store.writeFinal(nextState.finalResult); + return nextState; + } + + private failForTurnBudget(state: RunState, store: RunStore): RunState { + const blocker = `Max turn limit (${state.maxTurns}) reached before verifier approval.`; + const failedState: RunState = { + ...state, + status: 'failed', + blockers: [...state.blockers, blocker], + }; + store.writeState(failedState); + return failedState; + } +} diff --git a/apps/hivemind-demo/src/core/state.ts b/apps/hivemind-demo/src/core/state.ts new file mode 100644 index 0000000..642d76c --- /dev/null +++ b/apps/hivemind-demo/src/core/state.ts @@ -0,0 +1,180 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { createArtifactId, createMessageId, createRunId } from '../utils/ids.js'; +import type { + AgentResult, + AgentRole, + ArtifactContent, + ArtifactType, + FinalOutput, + OrchestratorOptions, + RunArtifact, + RunCheckpoint, + RunState, + RunSubtask, +} from './types.js'; + +export function createInitialState(task: string, options: OrchestratorOptions): RunState { + return { + runId: createRunId(), + originalTask: task, + status: 'pending', + currentPlan: [], + subtasks: [], + completedSteps: [], + messages: [], + artifacts: [], + blockers: [], + checkpoints: [], + finalResult: null, + turn: 0, + maxTurns: options.maxTurns, + retryCount: 0, + maxRetries: options.maxRetries, + }; +} + +export function applyAgentResult( + state: RunState, + role: AgentRole, + phase: string, + result: AgentResult, +): RunState { + const nextState: RunState = { + ...state, + status: + result.runStatus ?? + (result.status === 'failed' + ? 'failed' + : state.status === 'pending' + ? 'in_progress' + : state.status), + currentPlan: result.plan ? [...result.plan.steps] : [...state.currentPlan], + subtasks: result.plan ? result.plan.subtasks.map(cloneSubtask) : state.subtasks.map(cloneSubtask), + completedSteps: [...state.completedSteps], + messages: [...state.messages], + artifacts: [...state.artifacts], + blockers: result.replaceBlockers ? [...result.replaceBlockers] : [...state.blockers], + checkpoints: [...state.checkpoints], + finalResult: result.finalResult ?? state.finalResult, + turn: state.turn + 1, + }; + + nextState.messages.push({ + id: createMessageId(nextState.turn), + role, + phase, + summary: result.summary, + details: [...result.details], + turn: nextState.turn, + createdAt: new Date().toISOString(), + }); + + if (result.status === 'completed') { + nextState.completedSteps.push(result.summary); + } + + if (result.artifact) { + nextState.artifacts.push(createArtifact(result.artifact.type, result.artifact.label, result.artifact.content, nextState.artifacts.length + 1)); + } + + for (const update of result.markSubtasks ?? []) { + markSubtask(nextState.subtasks, update.id, update.status, update.note); + } + + return nextState; +} + +export function appendCheckpoint(state: RunState, checkpoint: RunCheckpoint): RunState { + return { + ...state, + checkpoints: [...state.checkpoints, checkpoint], + }; +} + +export function incrementRetryCount(state: RunState): RunState { + return { + ...state, + retryCount: state.retryCount + 1, + }; +} + +export function findLatestArtifact( + state: RunState, + type: ArtifactType, +): RunArtifact | null { + for (let index = state.artifacts.length - 1; index >= 0; index -= 1) { + const artifact = state.artifacts[index]; + if (artifact?.type === type) { + return artifact as RunArtifact; + } + } + return null; +} + +export class RunStore { + readonly runDir: string; + + constructor(dataDir: string, runId: string) { + this.runDir = join(dataDir, runId); + mkdirSync(this.runDir, { recursive: true }); + } + + writeState(state: RunState): void { + writeJson(join(this.runDir, 'state.json'), state); + } + + writeCheckpoint(checkpoint: RunCheckpoint): void { + const checkpointDir = join(this.runDir, 'checkpoints'); + mkdirSync(checkpointDir, { recursive: true }); + writeJson(join(checkpointDir, `${checkpoint.id}.json`), checkpoint); + } + + writeFinal(finalResult: FinalOutput | null): void { + if (!finalResult) return; + writeJson(join(this.runDir, 'final-output.json'), finalResult); + } +} + +function createArtifact( + type: ArtifactType, + label: string, + content: ArtifactContent, + index: number, +): RunArtifact { + return { + id: createArtifactId(type, index), + type, + label, + content, + createdAt: new Date().toISOString(), + }; +} + +function markSubtask( + subtasks: RunSubtask[], + id: string, + status: RunSubtask['status'], + note?: string, +): void { + const subtask = subtasks.find((entry) => entry.id === id); + if (!subtask) return; + subtask.status = status; + if (note && !subtask.notes.includes(note)) { + subtask.notes.push(note); + } + if (status === 'pending' && note) { + subtask.retryCount += 1; + } +} + +function cloneSubtask(subtask: RunSubtask): RunSubtask { + return { + ...subtask, + notes: [...subtask.notes], + }; +} + +function writeJson(filePath: string, payload: unknown): void { + writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); +} diff --git a/apps/hivemind-demo/src/core/types.ts b/apps/hivemind-demo/src/core/types.ts new file mode 100644 index 0000000..c5a24b3 --- /dev/null +++ b/apps/hivemind-demo/src/core/types.ts @@ -0,0 +1,172 @@ +export type StepStatus = 'pending' | 'in_progress' | 'blocked' | 'completed' | 'failed'; +export type RunStatus = StepStatus; +export type AgentRole = 'coordinator' | 'researcher' | 'builder' | 'reviewer' | 'verifier'; +export type ArtifactType = 'plan' | 'research' | 'build' | 'review' | 'verification'; + +export interface RunSubtask { + id: string; + title: string; + owner: AgentRole; + status: StepStatus; + notes: string[]; + retryCount: number; +} + +export interface PlanArtifactContent { + focus: string[]; + steps: string[]; + subtasks: Array>; +} + +export interface ResearchArtifactContent { + facts: string[]; + constraints: string[]; + assumptions: string[]; + recommendedFocus: string[]; +} + +export interface BuildArtifactContent { + fileTree: string[]; + implementationSteps: string[]; + testPlan: string[]; + checkpointPolicy: string[]; + notes: string[]; +} + +export interface ReviewArtifactContent { + approved: boolean; + strengths: string[]; + issues: string[]; + requiredFixes: string[]; +} + +export interface VerificationArtifactContent { + approved: boolean; + evidence: string[]; + openRisks: string[]; + nextSteps: string[]; +} + +export type ArtifactContent = + | PlanArtifactContent + | ResearchArtifactContent + | BuildArtifactContent + | ReviewArtifactContent + | VerificationArtifactContent; + +export interface RunArtifact { + id: string; + type: ArtifactType; + label: string; + content: T; + createdAt: string; +} + +export interface RunMessage { + id: string; + role: AgentRole; + phase: string; + summary: string; + details: string[]; + turn: number; + createdAt: string; +} + +export interface RunCheckpoint { + id: string; + turn: number; + goal: string; + done: string[]; + blocker: string | null; + nextBatch: string[]; + compactSummary: string; + createdAt: string; +} + +export interface FinalOutput { + result: string; + reasoningSummary: string[]; + openRisks: string[]; + nextSteps: string[]; + verified: boolean; +} + +export interface RunState { + runId: string; + originalTask: string; + status: RunStatus; + currentPlan: string[]; + subtasks: RunSubtask[]; + completedSteps: string[]; + messages: RunMessage[]; + artifacts: RunArtifact[]; + blockers: string[]; + checkpoints: RunCheckpoint[]; + finalResult: FinalOutput | null; + turn: number; + maxTurns: number; + retryCount: number; + maxRetries: number; +} + +export interface ArtifactDraft { + type: ArtifactType; + label: string; + content: T; +} + +export interface PlanDraft { + steps: string[]; + subtasks: RunSubtask[]; +} + +export interface AgentInput { + task: string; + phase: string; + attempt: number; +} + +export type CoordinatorDecision = 'retry_builder' | 'send_to_verifier' | 'escalate'; + +export interface AgentResult { + status: StepStatus; + summary: string; + details: string[]; + artifact?: ArtifactDraft; + plan?: PlanDraft; + markSubtasks?: Array<{ + id: string; + status: StepStatus; + note?: string; + }>; + replaceBlockers?: string[]; + decision?: CoordinatorDecision; + finalResult?: FinalOutput; + runStatus?: RunStatus; +} + +export interface Agent { + name: string; + role: AgentRole; + run(input: AgentInput, state: RunState): AgentResult; +} + +export interface HivemindLogger { + info(message: string): void; +} + +export interface OrchestratorOptions { + dataDir: string; + maxTurns: number; + maxRetries: number; + checkpointInterval: number; + logger?: HivemindLogger; +} + +export interface ModelProvider { + generate(input: { role: AgentRole; prompt: string; state: RunState }): Promise; +} + +export interface ToolRunner { + invoke(name: string, input: Record): Promise; +} diff --git a/apps/hivemind-demo/src/index.ts b/apps/hivemind-demo/src/index.ts new file mode 100644 index 0000000..e3efcf8 --- /dev/null +++ b/apps/hivemind-demo/src/index.ts @@ -0,0 +1,22 @@ +export { HivemindOrchestrator } from './core/orchestrator.js'; +export { createLogger } from './utils/logger.js'; +export type { + Agent, + AgentInput, + AgentResult, + ArtifactContent, + ArtifactDraft, + BuildArtifactContent, + FinalOutput, + HivemindLogger, + ModelProvider, + OrchestratorOptions, + ResearchArtifactContent, + ReviewArtifactContent, + RunCheckpoint, + RunMessage, + RunState, + RunSubtask, + ToolRunner, + VerificationArtifactContent, +} from './core/types.js'; diff --git a/apps/hivemind-demo/src/utils/ids.ts b/apps/hivemind-demo/src/utils/ids.ts new file mode 100644 index 0000000..3a378d4 --- /dev/null +++ b/apps/hivemind-demo/src/utils/ids.ts @@ -0,0 +1,15 @@ +export function createRunId(now: Date = new Date()): string { + return `run-${now.toISOString().replaceAll(':', '-').replaceAll('.', '-')}`; +} + +export function createMessageId(turn: number): string { + return `msg-${String(turn).padStart(2, '0')}`; +} + +export function createArtifactId(type: string, index: number): string { + return `${type}-${String(index).padStart(2, '0')}`; +} + +export function createCheckpointId(index: number): string { + return `checkpoint-${String(index).padStart(2, '0')}`; +} diff --git a/apps/hivemind-demo/src/utils/logger.ts b/apps/hivemind-demo/src/utils/logger.ts new file mode 100644 index 0000000..f0cffae --- /dev/null +++ b/apps/hivemind-demo/src/utils/logger.ts @@ -0,0 +1,10 @@ +import type { HivemindLogger } from '../core/types.js'; + +export function createLogger(verbose = true): HivemindLogger { + return { + info(message: string): void { + if (!verbose) return; + console.log(`[hivemind] ${message}`); + }, + }; +} diff --git a/apps/hivemind-demo/test/orchestrator.test.ts b/apps/hivemind-demo/test/orchestrator.test.ts new file mode 100644 index 0000000..49d442c --- /dev/null +++ b/apps/hivemind-demo/test/orchestrator.test.ts @@ -0,0 +1,130 @@ +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { ReviewerAgent } from '../src/agents/reviewer.js'; +import { createCheckpoint } from '../src/core/checkpoint.js'; +import { createInitialState } from '../src/core/state.js'; +import { HivemindOrchestrator } from '../src/index.js'; +import type { BuildArtifactContent, OrchestratorOptions } from '../src/core/types.js'; + +const tempDirs: string[] = []; +const silentLogger = { info: () => {} }; + +afterEach(() => { + while (tempDirs.length > 0) { + const path = tempDirs.pop(); + if (path) { + rmSync(path, { recursive: true, force: true }); + } + } +}); + +describe('HivemindOrchestrator', () => { + it('completes a happy-path task and writes persisted state', () => { + const dataDir = createTempDir(); + const orchestrator = new HivemindOrchestrator({ + dataDir, + logger: silentLogger, + }); + + const state = orchestrator.run( + 'Build a local TypeScript CLI with JSON state, checkpoints, a README, and verifier approval.', + ); + + expect(state.status).toBe('completed'); + expect(state.finalResult?.verified).toBe(true); + expect(state.checkpoints.length).toBeGreaterThan(0); + + const persistedState = JSON.parse( + readFileSync(join(dataDir, state.runId, 'state.json'), 'utf8'), + ) as { + status: string; + checkpoints: unknown[]; + finalResult: { verified: boolean } | null; + }; + + expect(persistedState.status).toBe('completed'); + expect(persistedState.checkpoints.length).toBe(state.checkpoints.length); + expect(persistedState.finalResult?.verified).toBe(true); + }); + + it('reviewer blocks builds without tests or checkpoint policy', () => { + const options = baseOptions(createTempDir()); + const state = createInitialState('Ship a bounded MVP', options); + const thinBuild: BuildArtifactContent = { + fileTree: ['src/core/types.ts'], + implementationSteps: ['Define types.'], + testPlan: [], + checkpointPolicy: [], + notes: [], + }; + + state.artifacts.push({ + id: 'build-01', + type: 'build', + label: 'thin-build', + content: thinBuild, + createdAt: new Date().toISOString(), + }); + + const reviewer = new ReviewerAgent(); + const result = reviewer.run( + { task: 'Ship a bounded MVP', phase: 'review', attempt: 1 }, + state, + ); + + expect(result.status).toBe('blocked'); + expect(result.replaceBlockers).toContain('Test plan missing.'); + expect(result.replaceBlockers).toContain('Checkpoint policy missing.'); + }); + + it('creates compact checkpoints with goal, done, blocker, and next batch', () => { + const options = baseOptions(createTempDir()); + const state = createInitialState('Tighten context handoffs', options); + state.completedSteps.push('Researcher captured constraints.'); + state.blockers.push('Awaiting verifier approval.'); + state.subtasks.push( + { + id: 'review', + title: 'Review output for risks and missing edges', + owner: 'reviewer', + status: 'blocked', + notes: [], + retryCount: 0, + }, + { + id: 'verify', + title: 'Verify result before success', + owner: 'verifier', + status: 'pending', + notes: [], + retryCount: 0, + }, + ); + state.turn = 4; + + const checkpoint = createCheckpoint(state); + + expect(checkpoint.compactSummary).toContain('Goal: Tighten context handoffs'); + expect(checkpoint.compactSummary).toContain('Done: Researcher captured constraints.'); + expect(checkpoint.compactSummary).toContain('Current blocker: Awaiting verifier approval.'); + expect(checkpoint.compactSummary).toContain('Next batch: Review output for risks and missing edges'); + }); +}); + +function createTempDir(): string { + const dir = mkdtempSync(join(tmpdir(), 'hivemind-demo-')); + tempDirs.push(dir); + return dir; +} + +function baseOptions(dataDir: string): OrchestratorOptions { + return { + dataDir, + maxTurns: 10, + maxRetries: 1, + checkpointInterval: 2, + logger: silentLogger, + }; +} diff --git a/apps/hivemind-demo/tsconfig.json b/apps/hivemind-demo/tsconfig.json new file mode 100644 index 0000000..ae5ba21 --- /dev/null +++ b/apps/hivemind-demo/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src", "test"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a957673..6a86854 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,18 @@ importers: specifier: ^2.1.5 version: 2.1.9(@types/node@22.19.17) + apps/hivemind-demo: + devDependencies: + tsup: + specifier: ^8.3.5 + version: 8.5.1(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vitest: + specifier: ^2.1.5 + version: 2.1.9(@types/node@22.19.17) + apps/mcp-server: dependencies: '@cavemem/compress':