diff options
| author | Ben Sima <ben@bsima.me> | 2025-11-20 16:23:29 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bsima.me> | 2025-11-20 16:23:29 -0500 |
| commit | b6334da771d764b7c29b33522db06b6cc716c6cb (patch) | |
| tree | 644805fb7f3276ea6a5a8af022f4b1f67c316a6f /Omni | |
| parent | 9ab85c33379f98229235a30dbf5108ad31a01d1f (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.md | 104 | ||||
| -rwxr-xr-x | Omni/Agent/merge-tasks.sh | 30 | ||||
| -rwxr-xr-x | Omni/Agent/setup-worker.sh | 27 | ||||
| -rwxr-xr-x | Omni/Agent/start-worker.sh | 34 | ||||
| -rwxr-xr-x | Omni/Agent/sync-tasks.sh | 46 | ||||
| -rw-r--r-- | Omni/Task.hs | 10 | ||||
| -rw-r--r-- | Omni/Task/Core.hs | 13 |
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)) |
