summaryrefslogtreecommitdiff
path: root/Omni
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-11-20 16:23:29 -0500
committerBen Sima <ben@bsima.me>2025-11-20 16:23:29 -0500
commitb6334da771d764b7c29b33522db06b6cc716c6cb (patch)
tree644805fb7f3276ea6a5a8af022f4b1f67c316a6f /Omni
parent9ab85c33379f98229235a30dbf5108ad31a01d1f (diff)
feat: implement multi-agent workflow infrastructure
- Add 'Review' status to Task tool - Add Omni/Agent/ directory with setup and sync scripts - Add WORKER_AGENT_GUIDE.md - Configure custom merge driver for tasks.jsonl
Diffstat (limited to 'Omni')
-rw-r--r--Omni/Agent/WORKER_AGENT_GUIDE.md104
-rwxr-xr-xOmni/Agent/merge-tasks.sh30
-rwxr-xr-xOmni/Agent/setup-worker.sh27
-rwxr-xr-xOmni/Agent/start-worker.sh34
-rwxr-xr-xOmni/Agent/sync-tasks.sh46
-rw-r--r--Omni/Task.hs10
-rw-r--r--Omni/Task/Core.hs13
7 files changed, 257 insertions, 7 deletions
diff --git a/Omni/Agent/WORKER_AGENT_GUIDE.md b/Omni/Agent/WORKER_AGENT_GUIDE.md
new file mode 100644
index 0000000..f1f6f60
--- /dev/null
+++ b/Omni/Agent/WORKER_AGENT_GUIDE.md
@@ -0,0 +1,104 @@
+# Worker Agent Guide
+
+This guide describes how to run a headless Worker Agent using the Multi-Agent Workflow.
+
+## 1. Setup
+
+First, create a dedicated worktree for the worker:
+
+```bash
+./Omni/Agent/setup-worker.sh omni-worker-1
+```
+
+This creates `../omni-worker-1` sharing the same git history but with its own workspace and branch (`omni-worker-1`).
+
+## 2. Worker Loop
+
+The Worker Agent should run the following loop continuously:
+
+### Step 1: Sync and Find Work
+
+```bash
+# Go to worker directory
+cd ../omni-worker-1
+
+# Sync tasks from the live branch
+./Omni/Agent/sync-tasks.sh
+
+# Check for ready tasks
+task ready --json
+```
+
+### Step 2: Claim Task
+
+If a task is found (e.g., `t-123`):
+
+```bash
+# Mark in progress
+task update t-123 in-progress
+
+# Commit the claim locally
+./Omni/Agent/sync-tasks.sh --commit
+```
+
+### Step 3: Create Workspace
+
+**CRITICAL: Determine the correct base branch.**
+
+1. **Check Dependencies**: Run `task deps t-123 --json`.
+2. **Check for Unmerged Work**: Look for dependencies that have existing branches (e.g., `task/t-parent-id`) which are NOT yet merged into `live`.
+3. **Select Base**:
+ * If you find an unmerged dependency branch, check it out: `git checkout task/t-parent-id`.
+ * Otherwise, start from fresh live code: `git checkout omni-worker-1` (which tracks `live`).
+
+4. **Create Feature Branch**:
+ ```bash
+ git checkout -b task/t-123
+ ```
+
+### Step 4: Implement
+
+1. Read task details: `task show t-123`
+2. Implement changes.
+3. **Run Tests**: `bild --test Omni/YourNamespace.hs`
+
+### Step 5: Submit for Review
+
+```bash
+# 1. Mark for review
+task update t-123 review
+
+# 2. Commit implementation
+git add .
+git commit -m "feat: implement t-123"
+
+# 3. Switch back to worker base branch to prepare for next loop
+# This detaches the worker from the task branch so the Planner can check it out
+git checkout omni-worker-1
+./Omni/Agent/sync-tasks.sh
+```
+
+## 3. Planner (Reviewer) Workflow
+
+The Planner Agent (running in the main repo) will:
+1. See tasks in `Review` status (after checking out the worker's branch or waiting for them to be merged).
+ * *Note: Since workers commit to local branches, you verify work by checking out their branch.*
+2. Check out the worker's branch: `git checkout task/t-123`.
+3. Review code and tests.
+4. Merge to `live`:
+ ```bash
+ git checkout live
+ git merge task/t-123
+ # Conflicts in tasks.jsonl are handled automatically by .gitattributes driver
+ ```
+5. Mark Done:
+ ```bash
+ task update t-123 done
+ git commit -am "task: t-123 done"
+ ```
+
+## Troubleshooting
+
+If `sync-tasks.sh` reports a conflict:
+1. Manually run `task import -i .tasks/live-tasks.jsonl`
+2. If git merge conflicts occur in `tasks.jsonl`, the custom merge driver should handle them. If not, resolve by keeping the union of tasks (or letting `task import` decide).
diff --git a/Omni/Agent/merge-tasks.sh b/Omni/Agent/merge-tasks.sh
new file mode 100755
index 0000000..833afcf
--- /dev/null
+++ b/Omni/Agent/merge-tasks.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+# Omni/Ide/merge-tasks.sh
+# Git merge driver for .tasks/tasks.jsonl
+# Usage: merge-tasks.sh %O %A %B
+# %O = ancestor, %A = current (ours), %B = other (theirs)
+
+# ANCESTOR="$1" (unused)
+OURS="$2"
+THEIRS="$3"
+
+# We want to merge THEIRS into OURS using the task tool's import logic.
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+TASK_BIN="$REPO_ROOT/_/bin/task"
+
+# If binary doesn't exist, try to build it? Or just fail safely.
+if [ ! -x "$TASK_BIN" ]; then
+ # Try to find it in the build output if _/bin isn't populated
+ # But for now, let's just fail if not found, forcing manual merge
+ exit 1
+fi
+
+# Use the task tool to merge
+# We tell it that the DB is the 'OURS' file
+# And we import the 'THEIRS' file
+export TASK_DB_PATH="$OURS"
+if "$TASK_BIN" import -i "$THEIRS" >/dev/null 2>&1; then
+ exit 0
+else
+ exit 1
+fi
diff --git a/Omni/Agent/setup-worker.sh b/Omni/Agent/setup-worker.sh
new file mode 100755
index 0000000..27a9939
--- /dev/null
+++ b/Omni/Agent/setup-worker.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+set -e
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 <worker-name>"
+ echo "Example: $0 omni-worker-1"
+ exit 1
+fi
+
+WORKER_NAME="$1"
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+WORKTREE_PATH="$REPO_ROOT/../$WORKER_NAME"
+
+echo "Creating worktree at $WORKTREE_PATH..."
+git worktree add "$WORKTREE_PATH" live
+
+# Copy .envrc.local if it exists (user-specific config)
+if [ -f "$REPO_ROOT/.envrc.local" ]; then
+ echo "Copying .envrc.local..."
+ cp "$REPO_ROOT/.envrc.local" "$WORKTREE_PATH/"
+fi
+
+# We create a new branch for the worker based on 'live'
+# This avoids the "branch already checked out" error if 'live' is checked out elsewhere
+BRANCH_NAME="${WORKER_NAME}"
+echo "Creating worktree '$WORKTREE_PATH' on branch '$BRANCH_NAME' (from live)..."
+git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" live
diff --git a/Omni/Agent/start-worker.sh b/Omni/Agent/start-worker.sh
new file mode 100755
index 0000000..2c5eee4
--- /dev/null
+++ b/Omni/Agent/start-worker.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+set -e
+
+# Omni/Agent/start-worker.sh
+# Launches an Amp worker agent in the specified worktree.
+# Usage: ./Omni/Agent/start-worker.sh [worker-directory-name]
+# Example: ./Omni/Agent/start-worker.sh omni-worker-1
+
+WORKER_NAME="${1:-omni-worker-1}"
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+WORKER_PATH="$REPO_ROOT/../$WORKER_NAME"
+AMP_BIN="$REPO_ROOT/node_modules/.bin/amp"
+
+if [ ! -d "$WORKER_PATH" ]; then
+ echo "Error: Worker directory '$WORKER_PATH' does not exist."
+ echo "Please run './Omni/Agent/setup-worker.sh $WORKER_NAME' first."
+ exit 1
+fi
+
+if [ ! -x "$AMP_BIN" ]; then
+ echo "Error: Amp binary not found at '$AMP_BIN'."
+ echo "Please ensure npm dependencies are installed in the main repository."
+ exit 1
+fi
+
+echo "Starting Worker Agent in '$WORKER_PATH'..."
+echo "Using Amp binary: $AMP_BIN"
+
+cd "$WORKER_PATH"
+
+# Launch Amp with the worker persona and instructions
+"$AMP_BIN" -- "You are a Worker Agent. Your goal is to process tasks from the task manager.
+Please read Omni/Agent/WORKER_AGENT_GUIDE.md and follow the 'Worker Loop' instructions exactly.
+Start by syncing tasks and checking for ready work."
diff --git a/Omni/Agent/sync-tasks.sh b/Omni/Agent/sync-tasks.sh
new file mode 100755
index 0000000..f4669b7
--- /dev/null
+++ b/Omni/Agent/sync-tasks.sh
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+set -e
+
+# Omni/Ide/sync-tasks.sh
+# Synchronizes the task database with the live branch safely.
+# Usage: sync-tasks.sh [--commit]
+
+COMMIT=0
+if [[ "$1" == "--commit" ]]; then
+ COMMIT=1
+fi
+
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+cd "$REPO_ROOT"
+
+echo "Syncing tasks..."
+
+# 1. Import latest tasks from 'live' branch
+# We use git show to get the file content from the reference branch without checking it out
+mkdir -p .tasks
+git show live:.tasks/tasks.jsonl > .tasks/live-tasks.jsonl
+
+# 2. Merge logic: Import live tasks into our local DB
+# The 'task import' command uses timestamps to resolve conflicts (last write wins)
+if [ -s .tasks/live-tasks.jsonl ]; then
+ echo "Importing tasks from live branch..."
+ "$REPO_ROOT/_/bin/task" import -i .tasks/live-tasks.jsonl
+fi
+
+# 3. Clean up
+rm .tasks/live-tasks.jsonl
+
+# 4. Export current state to ensure it's clean/deduplicated
+"$REPO_ROOT/_/bin/task" export --flush
+
+# 5. Commit changes to .tasks/tasks.jsonl if requested and there are changes
+if [[ "$COMMIT" -eq 1 ]]; then
+ if [[ -n $(git status --porcelain .tasks/tasks.jsonl) ]]; then
+ echo "Committing task updates..."
+ git add .tasks/tasks.jsonl
+ git commit -m "task: sync database" || true
+ echo "Task updates committed to current branch."
+ else
+ echo "No task changes to commit."
+ fi
+fi
diff --git a/Omni/Task.hs b/Omni/Task.hs
index bbb78bb..d1e672a 100644
--- a/Omni/Task.hs
+++ b/Omni/Task.hs
@@ -72,7 +72,7 @@ Options:
--type=<type> Task type: epic or task (default: task)
--parent=<id> Parent epic ID
--priority=<p> Priority: 0-4 (0=critical, 4=backlog, default: 2)
- --status=<status> Filter by status: open, in-progress, done
+ --status=<status> Filter by status: open, in-progress, review, done
--deps=<ids> Comma-separated list of dependency IDs
--dep-type=<type> Dependency type: blocks, discovered-from, parent-child, related (default: blocks)
--discovered-from=<id> Shortcut for --deps=<id> --dep-type=discovered-from
@@ -85,7 +85,7 @@ Options:
Arguments:
<title> Task title
<id> Task ID
- <status> Task status (open, in-progress, done)
+ <status> Task status (open, in-progress, review, done)
<file> JSONL file to import
|]
@@ -171,8 +171,9 @@ move args
Nothing -> pure Nothing
Just "open" -> pure <| Just Open
Just "in-progress" -> pure <| Just InProgress
+ Just "review" -> pure <| Just Review
Just "done" -> pure <| Just Done
- Just other -> panic <| "Invalid status: " <> T.pack other <> ". Use: open, in-progress, or done"
+ Just other -> panic <| "Invalid status: " <> T.pack other <> ". Use: open, in-progress, review, or done"
maybeNamespace <- case Cli.getArg args (Cli.longOption "namespace") of
Nothing -> pure Nothing
Just ns -> do
@@ -205,8 +206,9 @@ move args
let newStatus = case statusStr of
"open" -> Open
"in-progress" -> InProgress
+ "review" -> Review
"done" -> Done
- _ -> panic "Invalid status. Use: open, in-progress, or done"
+ _ -> panic "Invalid status. Use: open, in-progress, review, or done"
updateTaskStatus tid newStatus
if isJsonMode args
then outputSuccess <| "Updated task " <> tid
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index 3da47aa..af105de 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -36,7 +36,7 @@ data Task = Task
data TaskType = Epic | WorkTask
deriving (Show, Eq, Generic)
-data Status = Open | InProgress | Done
+data Status = Open | InProgress | Review | Done
deriving (Show, Eq, Generic)
-- Priority levels (matching beads convention)
@@ -83,9 +83,11 @@ instance FromJSON Task
-- Get the tasks database file path (use test file if TASK_TEST_MODE is set)
getTasksFilePath :: IO FilePath
getTasksFilePath = do
+ customPath <- lookupEnv "TASK_DB_PATH"
testMode <- lookupEnv "TASK_TEST_MODE"
- pure <| case testMode of
- Just "1" -> ".tasks/tasks-test.jsonl"
+ pure <| case (customPath, testMode) of
+ (Just path, _) -> path
+ (_, Just "1") -> ".tasks/tasks-test.jsonl"
_ -> ".tasks/tasks.jsonl"
-- Initialize the task database
@@ -347,6 +349,7 @@ showTaskTree maybeId = do
WorkTask -> case taskStatus task of
Open -> "[ ]"
InProgress -> "[~]"
+ Review -> "[?]"
Done -> "[✓]"
nsStr = case taskNamespace task of
Nothing -> ""
@@ -462,6 +465,7 @@ data TaskStats = TaskStats
{ totalTasks :: Int,
openTasks :: Int,
inProgressTasks :: Int,
+ reviewTasks :: Int,
doneTasks :: Int,
totalEpics :: Int,
readyTasks :: Int,
@@ -483,6 +487,7 @@ getTaskStats = do
let total = length tasks
open = length <| filter (\t -> taskStatus t == Open) tasks
inProg = length <| filter (\t -> taskStatus t == InProgress) tasks
+ review = length <| filter (\t -> taskStatus t == Review) tasks
done = length <| filter (\t -> taskStatus t == Done) tasks
epics = length <| filter (\t -> taskType t == Epic) tasks
readyCount = length ready
@@ -504,6 +509,7 @@ getTaskStats = do
{ totalTasks = total,
openTasks = open,
inProgressTasks = inProg,
+ reviewTasks = review,
doneTasks = done,
totalEpics = epics,
readyTasks = readyCount,
@@ -522,6 +528,7 @@ showTaskStats = do
putText <| "Total tasks: " <> T.pack (show (totalTasks stats))
putText <| " Open: " <> T.pack (show (openTasks stats))
putText <| " In Progress: " <> T.pack (show (inProgressTasks stats))
+ putText <| " Review: " <> T.pack (show (reviewTasks stats))
putText <| " Done: " <> T.pack (show (doneTasks stats))
putText ""
putText <| "Epics: " <> T.pack (show (totalEpics stats))