Skip to content

ARCHIVED — Original Agent Task Orchestrator concept

Historical input preserved verbatim below. This file is not current architecture. Use docs/product/, docs/architecture/, docs/design/, and docs/context.md for current truth. This archive exists so earlier ideas and research are never lost.


Agent Task Orchestrator — Spec & Build Plan

Stack: Node + TypeScript + Effect-TS + SQLite (via @effect/sql-sqlite-node, wrapping better-sqlite3) Purpose: primarily a vehicle for learning Effect-TS in a real, if modest, concurrency/process-supervision context.


1. The full lifecycle

Two linked loops: an optional Plan loop (grill it until it's right, then split into tasks), and a Task loop (work it, review it twice, done). Both loops share the same shape: draft → review → (reject → back to work, repeat) → (approve → advance).

                                    PLAN LOOP (optional)
   ┌─────────────────────────────────────────────────────────────┐
   │                                                               │
   ▼                                                               │
 draft ──submit──► in_review ──annotate/grill──┬─ approved ──► decomposed
                                                 └─ rejected ───────┘
                                                    (back to draft, round++)

                            decomposed emits N tasks (+ dependency edges)


                                    TASK LOOP (per task)
   backlog ──[optional planning sub-loop, same shape as above]──► todo


                                          in_progress  ◄────────────────┐
                                     (agent "doing/working")            │
                                                │                       │
                                                ▼                       │
                                          agent_review                  │
                                        ┌───────┴────────┐              │
                                   approved          rejected ──────────┤
                                        │             (round++, back to in_progress)

                                   human_review                        │
                                 ┌───────┴────────┐                    │
                            approved          rejected ────────────────┘
                                 │             (round++, back to in_progress)

                               done ──(optional, later)──► archived

Key rule, both loops: rejection never sends work to a "revise the review" step — it sends it straight back to the doing state (draft for plans, in_progress for tasks), carrying the annotations as context. Rinse and repeat until approved. Every round is counted, not just timestamped, so you can see at a glance how many iterations something took.


2. Data model (SQLite)

sql
CREATE TABLE projects (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  repo_path TEXT NOT NULL,
  default_branch TEXT NOT NULL DEFAULT 'main',
  created_at TEXT NOT NULL
);

CREATE TABLE agents (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  role TEXT NOT NULL,            -- 'worker' | 'reviewer' | 'planner'
  command_template TEXT NOT NULL
);

-- PLAN LOOP -----------------------------------------------------------

CREATE TABLE plans (
  id TEXT PRIMARY KEY,
  project_id TEXT NOT NULL REFERENCES projects(id),
  title TEXT NOT NULL,
  content TEXT NOT NULL,                  -- markdown, the plan document
  status TEXT NOT NULL DEFAULT 'draft',   -- draft | in_review | approved | decomposed
  round INTEGER NOT NULL DEFAULT 0,       -- increments on every rejection
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE plan_reviews (
  id TEXT PRIMARY KEY,
  plan_id TEXT NOT NULL REFERENCES plans(id),
  round INTEGER NOT NULL,
  reviewer_kind TEXT NOT NULL,            -- 'agent' | 'human'
  verdict TEXT NOT NULL,                  -- 'approved' | 'rejected'
  annotations TEXT,                       -- JSON: [{lineRange, comment, suggestedReplacement}]
  created_at TEXT NOT NULL
);

-- TASK LOOP -------------------------------------------------------------

CREATE TABLE tasks (
  id TEXT PRIMARY KEY,
  project_id TEXT NOT NULL REFERENCES projects(id),
  plan_id TEXT REFERENCES plans(id),          -- nullable: not every task comes from a Plan
  title TEXT NOT NULL,
  description TEXT,
  status TEXT NOT NULL DEFAULT 'backlog',
    -- backlog | planning | todo | in_progress | agent_review | human_review | done | archived
  requires_planning INTEGER NOT NULL DEFAULT 0,  -- per-task execution-plan sub-loop, optional
  execution_plan TEXT,                        -- approved per-task plan content, if any
  planning_round INTEGER NOT NULL DEFAULT 0,
  review_round INTEGER NOT NULL DEFAULT 0,    -- increments on every agent_review/human_review rejection
  assigned_agent_id TEXT REFERENCES agents(id),
  reviewer_agent_id TEXT REFERENCES agents(id),
  worktree_path TEXT,
  branch_name TEXT,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE task_dependencies (
  task_id TEXT NOT NULL REFERENCES tasks(id),
  depends_on_task_id TEXT NOT NULL REFERENCES tasks(id),
  PRIMARY KEY (task_id, depends_on_task_id)
);

CREATE TABLE task_events (
  id TEXT PRIMARY KEY,
  task_id TEXT NOT NULL REFERENCES tasks(id),
  event_type TEXT NOT NULL,     -- 'status_change' | 'agent_started' | 'agent_finished' | 'review_submitted' | ...
  payload TEXT,                 -- JSON
  created_at TEXT NOT NULL
);

CREATE TABLE reviews (
  id TEXT PRIMARY KEY,
  task_id TEXT NOT NULL REFERENCES tasks(id),
  stage TEXT NOT NULL,          -- 'agent_review' | 'human_review'
  round INTEGER NOT NULL,
  reviewer_kind TEXT NOT NULL,  -- 'agent' | 'human'
  verdict TEXT NOT NULL,        -- 'approved' | 'rejected'
  annotations TEXT,             -- JSON, Plannotator diff-mode output
  created_at TEXT NOT NULL
);

3. Architecture (Effect-TS layers)

AppLayer =
  SqlLive
  .pipe(Layer.provideMerge(PlanRepoLive))
  .pipe(Layer.provideMerge(TaskRepoLive))
  .pipe(Layer.provideMerge(WorktreeManagerLive))
  .pipe(Layer.provideMerge(AgentRunnerLive))
  .pipe(Layer.provideMerge(ReviewServiceLive))    // Plannotator: annotate-mode & diff-mode
  .pipe(Layer.provideMerge(PlanServiceLive))      // draft ⇄ review loop → decompose
  .pipe(Layer.provideMerge(TaskOrchestratorLive)) // state machine, both review loops, dependency-aware queue
  .pipe(Layer.provideMerge(HttpApiLive))
  • PlanService
    • submitForReview(planId) → Plannotator annotate-mode on content.
    • recordReview(planId, verdict, annotations) → if rejected: round++, status back to draft, annotations attached as context for the next draft pass. If approved: status → approved.
    • decompose(planId) → planner agent proposes { title, description, dependsOnIndices, requiresPlanning }[]; caller reviews/edits; on confirm, persists as real tasks + task_dependencies and flips plan to decomposed.
  • TaskOrchestrator — the shared "loop" logic is one function reused for both review stages and the optional per-task planning sub-loop:
    ts
    const handleReviewOutcome = (task, stage, verdict, annotations) =>
      verdict === "rejected"
        ? TaskRepo.update(task.id, { status: "in_progress", review_round: task.review_round + 1 })
            .pipe(Effect.zipRight(recordAnnotationsAsContext(task.id, annotations)))
        : stage === "agent_review"
          ? TaskRepo.update(task.id, { status: "human_review" })
          : TaskRepo.update(task.id, { status: "done" })
    Same shape handles planning sub-loop rejections back to backlog/re-draft of execution_plan.
  • Queue watcher only assigns a todo task once every task_dependencies row for it points to a done task (unchanged from before).
  • ReviewServicereviewPlan(plan) (annotate-mode) and reviewDiff(task, stage) (diff-mode for agent_review/human_review, or ExitPlanMode mode for the per-task planning sub-loop).
  • WorktreeManager / AgentRunner / HttpApi unchanged.

4. Per-task planning sub-loop vs. the Plan screen

Plan screen Per-task planning flag
ScopeEpic/feature, produces many tasksOne task's own implementation approach
TriggerStarted manuallyrequires_planning = 1
Loop shapedraft ⇄ in_review (annotate), rinse-repeatsame shape, scoped to execution_plan field
OutputN new tasks + dependency edgesOne approved plan attached to that task

A Plan's decomposition can set requires_planning = 1 on any task it creates that's still underspecified — that task then runs its own mini plan-loop before an agent starts touching code.


5. Dependencies & parallelism

  • Tasks with no task_dependencies rows are eligible for assignment immediately — independent worktrees, true parallelism.
  • A dependent task is assigned only once every dependency is done.
  • Branch dependent tasks off the default branch after the dependency merges — not off an unmerged sibling branch. Simpler, avoids conflicting concurrent edits. Branching off an unmerged sibling (to start earlier) is a real optimization — Phase 4+ item, not part of the initial build.
  • Validate the dependency graph is acyclic (DFS) on every edge insert; reject cycles at write time.

6. Which codebase to start from

Backend: fresh, in Effect-TS (§3). Frontend: build your own — none of the surveyed boards are worth forking wholesale:

  • Vibe Kanban (Rust/Axum backend + React frontend, Apache-2.0): backend not reusable (Rust); frontend passed over too now — own UI opinions win over saving scaffold time here.
  • Nimbalyst/Crystal: reference only, for the diff-review pane UX.
  • Cline's kanban: confirms the "board as a layer over git state" pattern; not a base to fork.
  • Orca (AO): not worth untangling.
  • Plannotator: run as a self-hosted local sidecar process for both annotate-mode (plans) and diff-mode (task reviews) — this one you do keep, it's a backend integration, not a frontend fork.

Since the board is now greenfield too, sketch the actual screens (board view, plan view, task detail/review pane) before writing components — worth doing as its own short pass rather than improvising column-by-column.


7. Agent Runner interface

ts
interface Agent {
  id: string
  role: "worker" | "reviewer" | "planner"
  commandTemplate: string
}

interface AgentRunner {
  run(agent: Agent, worktreePath: string, prompt: string): Effect.Effect<AgentResult, AgentError>
}

interface AgentResult {
  exitCode: number
  stdout: string
  diffSummary: { filesChanged: number; insertions: number; deletions: number }
}

CLI-agnostic by design — Claude Code, Codex, or anything else is just a row in agents with a different command_template.


8. Build order

Phase 0 — skeleton

  • Full schema (plans, plan_reviews, tasks, task_dependencies, reviews) from day one.
  • TaskRepo + PlanRepo CRUD, bare HTTP API.

Phase 1 — one agent, one task, manual

  • WorktreeManager, AgentRunner with a single hardcoded agent.
  • Manually drive a task to a diff. No review loop yet.

Phase 2 — task review loop (the core of the whole system)

  • Plannotator diff-mode as ReviewService.
  • Wire agent_review ⇄ in_progress and human_review ⇄ in_progress with round-counting.
  • This is the point where "rinse and repeat until approved" actually exists end-to-end.

Phase 3 — Plan loop

  • PlanService draft ⇄ review loop (annotate-mode).
  • decompose with confirm-before-commit UI.

Phase 4 — dependencies & parallelism

  • task_dependencies enforcement in the queue watcher, cycle guard.
  • (Later) branch-off-unmerged-sibling optimization.

Phase 5 — board UI (greenfield)

  • Screen sketch first: board view (columns per status), plan view (draft/review/decompose), task detail (diff + annotation pane, round history).
  • Build against the HTTP API that's already stable by this point — backend contracts shouldn't need to change to fit the UI.
  • Activity feed from task_events; round counters visible on cards.

Phase 6 — polish

  • Multi-agent pools, retries, timeouts, done → archived lifecycle, cross-project dashboard.

9. Open questions to settle before Phase 0

  • Single-user only, or auth/multi-user from the start?
  • One agent CLI to start with, or multi-agent from day one?
  • Rejection re-run: same agent session with feedback appended, or fresh session with feedback injected into the prompt? (Fresh is simpler/more robust; recommend starting there.)
  • Does the "planner" agent role use the same CLI/model as workers, or a separate, more deliberate model call?
  • Any cap on rounds before a task/plan gets flagged for direct human intervention instead of another automated pass? (Worth adding early — an infinite rinse-repeat loop between two agents is a real failure mode.)

Forge is local-first. The docs are part of the product engineering system.