From 0e6aba28865c1ddf527bbd413915c12892ab15e5 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 24 Mar 2026 00:04:17 +0700 Subject: [PATCH] refactor(paseo-loop): support self and new-agent targets with max-time --- skills/paseo-loop/SKILL.md | 450 +++++++++++++++++++------------- skills/paseo-loop/bin/loop.sh | 476 ++++++++++++++++++++++++---------- 2 files changed, 605 insertions(+), 321 deletions(-) diff --git a/skills/paseo-loop/SKILL.md b/skills/paseo-loop/SKILL.md index a019511f9..471889038 100644 --- a/skills/paseo-loop/SKILL.md +++ b/skills/paseo-loop/SKILL.md @@ -1,12 +1,18 @@ --- name: paseo-loop -description: Run a task in a loop until an exit condition is met. Use when the user says "loop", "loop this", "keep trying until", "babysit", "poll", or wants iterative autonomous execution. +description: Run an agent loop until an exit condition is met. Use when the user says "loop", "babysit", "keep trying until", "check every X", "watch", or wants iterative autonomous execution. user-invocable: true --- -# Loop Skill +# Paseo Loop Skill -You are setting up an autonomous loop. An agent runs repeatedly until an exit condition is met. +You are setting up `/paseo-loop` as a flexible loop primitive. + +Think of it like a `while` loop: +- each iteration sends work to a target +- an optional verifier judges completion +- optional sleep schedules the next iteration +- hard caps stop the loop if it runs too long **User's arguments:** $ARGUMENTS @@ -14,235 +20,319 @@ You are setting up an autonomous loop. An agent runs repeatedly until an exit co ## Prerequisites -Load the **Paseo skill** first — it contains the CLI reference for all agent commands. +Load the **Paseo skill** first. It contains the CLI reference for `paseo run`, `paseo send`, `paseo wait`, and related commands. -## What Is a Loop +## Core Model -A loop runs an agent repeatedly until it's done. There are two modes: +Every loop has these parts: -### Self-terminating (no verifier) +1. **Target**: who acts each iteration +2. **Prompt**: what the target does each iteration +3. **Verifier**: optional independent judge +4. **Sleep**: optional pause between iterations +5. **Stop conditions**: max iterations and/or max total runtime -A single worker agent runs each iteration. It returns `{ done: boolean, reason: string }` via structured output. The loop exits when `done` is true. +### Target -### Worker + Verifier +There are two target modes: -A worker agent runs each iteration (detached, no structured output). After the worker finishes, a separate verifier agent evaluates the verification prompt and returns `{ done: boolean, reason: string }`. The loop exits when `done` is true. +#### `self` -### Feedback between iterations +The loop sends the prompt back to the current agent each iteration. The current agent is identified by `$PASEO_AGENT_ID`. -In both modes, the `reason` from the previous iteration is fed back to the next worker as ``. This gives the worker context about what happened last time. +Use `self` when: +- the current agent should keep ownership of the task +- the user says "babysit", "watch", "check every X", "poll", or "monitor" +- waking the same agent is cheaper and more natural than starting fresh + +#### `new-agent` + +The loop launches a fresh worker agent each iteration. + +Use `new-agent` when: +- the user says "create a loop", "spin up a loop", or "launch a codex agent" +- the task benefits from fresh context per iteration +- you want isolated retries, often in a worktree + +### Verifier + +The verifier is orthogonal to the target: +- no verifier: the target decides whether the loop is done +- verifier present: the verifier decides whether the loop is done + +If a verifier exists, it is the source of truth for loop completion. + +## Defaults by User Intent + +Infer defaults from the user's phrasing: + +### Babysit / watch / check every X + +Default to: +- `target=self` +- `sleep=` +- no verifier unless the user asks for independent verification +- `max-time=1h` if the user gives no bound and the task could run indefinitely + +### Ensure X is done + +Default to: +- `target=self` +- verifier enabled +- no sleep unless the task is waiting on an external system + +Reason: by default, do not trust the same agent to judge its own completion when the user is asking for assurance. + +### Create a loop / launch a loop / loop a codex agent + +Default to: +- `target=new-agent` +- verifier enabled when success criteria need independent judgment +- worktree enabled when code changes are involved + +## Stop Conditions + +Support both: +- `--max-iterations N` +- `--max-time DURATION` + +Use at least one bound for open-ended or polling loops when the user does not specify one. + +## Feedback Between Iterations + +Each iteration should receive the previous result as ``. + +This applies in all cases: +- if there is a verifier, feed back the verifier's `reason` +- otherwise feed back the target's `reason` ## Live Steering -Each loop run persists state in: +Each loop persists state in: ```text ~/.paseo/loops// - worker-prompt.md # worker prompt (live-editable) - verifier-prompt.md # verifier prompt (live-editable, only when verifier is used) - last_reason.md # latest iteration result - history.log # per-iteration records + target-prompt.md # prompt sent to self or worker (live-editable) + verifier-prompt.md # verifier prompt (live-editable, optional) + last_reason.md # latest reason used for feedback + history.log # per-iteration records ``` Edits to prompt files are picked up on the next iteration without restarting the loop. -## Parsing Arguments +## Script Interface -Parse `$ARGUMENTS` to determine: - -1. **Worker prompt** — what the worker does each iteration -2. **Verifier prompt** (optional) — what the verifier checks after each worker iteration -3. **Worker** — which agent does the work (default: Codex) -4. **Verifier** — which agent verifies (default: Claude sonnet) -5. **Sleep** (optional) — delay between iterations -6. **Name** — a short name for tracking -7. **Max iterations** — safety cap (default: unlimited) -8. **Archive** — whether to archive agents after each iteration -9. **Worktree** — whether to run in an isolated git worktree - -### Examples - -``` -/loop babysit PR #42 until CI is green, check every 5 minutes -→ worker-prompt: "Check the CI status of PR #42 using `gh pr checks 42`. Report done when ALL checks - have passed (no pending, no failures). If any checks are still running or have failed, report not - done and list which checks are pending or failing." - No verifier (self-terminating: the worker inspects CI and reports done when green) - sleep: 5m - archive: yes - name: babysit-pr-42 - -/loop implement the auth refactor from the plan in /tmp/plan.md -→ worker-prompt-file: /tmp/plan.md - verifier-prompt: "Verify every step of the plan was implemented. Check file changes, types, and - run `npm run typecheck` and `npm test`. Report each criterion individually with evidence." - name: auth-refactor - worktree: auth-refactor - -/loop run the test suite until it passes -→ worker-prompt: "Run `npm test`. If any tests fail, read the failure output, investigate the root - cause in the source code, and fix it. Report done when `npm test` exits with code 0 and all - tests pass. Report not done if any test still fails, and explain which tests failed and why." - No verifier (self-terminating: worker reports done when tests pass) - name: fix-tests - -/loop watch error rates after deploy, check every 10 minutes -→ worker-prompt: "Check the error rate for the canary deployment by running `./scripts/check-canary.sh`. - Report done when the error rate has been below 0.1% for at least 2 consecutive checks. Report not - done with the current error rate and trend." - No verifier (self-terminating: worker reports done when error rate is stable) - sleep: 10m - max-iterations: 30 - archive: yes - name: canary-watch -``` - -## Using the Script - -The loop is implemented as a bash script at `~/.claude/skills/paseo-loop/bin/loop.sh`. +The loop is implemented at: ```bash -# Self-terminating: worker checks PR CI and reports done when all checks pass -~/.claude/skills/paseo-loop/bin/loop.sh \ - --worker-prompt "Check CI status of PR #42 using gh pr checks 42. Report done when ALL checks pass. Report not done with a list of pending/failing checks." \ - --name "babysit-pr" \ - --sleep 5m \ - --archive - -# Worker + verifier: worker implements, verifier independently checks -~/.claude/skills/paseo-loop/bin/loop.sh \ - --worker-prompt "Implement the auth refactor: ..." \ - --verifier-prompt "Verify the auth refactor is complete. Run npm run typecheck and npm test. Check that all file changes match the plan. Report each criterion with evidence." \ - --name "auth-refactor" \ - --worktree "auth-refactor" +skills/paseo-loop/bin/loop.sh ``` -### Arguments +### Flags | Flag | Required | Default | Description | |---|---|---|---| -| `--worker-prompt` | Yes* | — | Prompt given to the worker each iteration | -| `--worker-prompt-file` | Yes* | — | Read the worker prompt from a file | -| `--worker` | No | `codex` | Worker agent (`provider/model`, e.g. `codex/gpt-5.4`, `claude/sonnet`) | -| `--verifier-prompt` | No* | — | Verification prompt for a separate verifier agent | -| `--verifier-prompt-file` | No* | — | Read the verifier prompt from a file | -| `--verifier` | No | `claude/sonnet` | Verifier agent (`provider/model`) | -| `--name` | Yes | — | Name prefix for agents | -| `--sleep` | No | — | Delay between iterations (e.g. `30s`, `5m`, `1h`) | -| `--max-iterations` | No | unlimited | Safety cap on iterations | -| `--archive` | No | off | Archive agents after each iteration | -| `--worktree` | No | — | Worktree name. Created on first use, reused after. | +| `--target self|new-agent` | No | inferred / `new-agent` in raw script | Who acts each iteration | +| `--target-prompt` | Yes* | — | Prompt given to the target each iteration | +| `--target-prompt-file` | Yes* | — | Read the target prompt from a file | +| `--worker` | No | `codex` | Worker agent for `new-agent` loops | +| `--agent-id` | No | `$PASEO_AGENT_ID` | Existing agent id for `self` loops | +| `--verifier-prompt` | No* | — | Prompt for an independent verifier | +| `--verifier-prompt-file` | No* | — | Read verifier prompt from a file | +| `--verifier` | No | `claude/sonnet` | Verifier agent | +| `--name` | Yes | — | Name prefix for loop tracking | +| `--sleep` | No | — | Delay between iterations | +| `--max-iterations` | No | unlimited | Hard cap on iteration count | +| `--max-time` | No | unlimited | Hard cap on total wall-clock runtime | +| `--archive` | No | off | Archive newly created agents after iteration | +| `--worktree` | No | — | Worktree name for `new-agent` loops | | `--thinking` | No | `medium` | Thinking level for worker | -\* Provide exactly one of `--worker-prompt` or `--worker-prompt-file`. Provide at most one of `--verifier-prompt` or `--verifier-prompt-file`. +\* Provide exactly one of `--target-prompt` or `--target-prompt-file`. Provide at most one of `--verifier-prompt` or `--verifier-prompt-file`. -### Behavior by parameters +## Behavior Rules -| Parameters | Mode | Use case | -|---|---|---| -| `--worker-prompt` only | Self-terminating worker | Worker does work and decides when done | -| `--worker-prompt` + `--verifier-prompt` | Worker + verifier | Worker implements, verifier independently checks | -| `--worker-prompt` + `--sleep` | Polling with self-termination | Periodic check until a condition is met | -| `--worker-prompt` + `--verifier-prompt` + `--sleep` | Periodic work + verifier | Periodic work with independent verification | +### `target=self`, no verifier -### Agent Naming +The current agent must return structured JSON: -Without verifier: agents are named `{name}-{N}`: -``` -babysit-1 # First iteration -babysit-2 # Second iteration +```json +{ "done": true, "reason": "..." } ``` -With verifier: workers are `{name}-{N}`, verifiers are `{name}-verify-{N}`: -``` -feat-1 # First worker -feat-verify-1 # First verifier -feat-2 # Second worker (with previous result context) -feat-verify-2 # Second verifier -``` +Use this for: +- babysitting a PR +- scheduled status checks +- monitoring a deployment +- repeating an objective task the current agent can judge itself -### Worktree Support +### `target=self`, verifier enabled -When `--worktree` is passed, all agents run in the same git worktree. The worktree is created on first launch and reused for all subsequent agents. +The current agent does the work. A separate verifier decides whether the loop is done. + +Use this for: +- "ensure X is done" +- "work until the tests are passing" +- cases where the user wants independent judgment but keeping the same agent is still the right execution model + +### `target=new-agent`, no verifier + +A fresh worker launches each iteration and must return structured JSON itself. + +Use this when: +- the task is naturally self-judging +- you want fresh context each retry + +### `target=new-agent`, verifier enabled + +A fresh worker launches each iteration. After it finishes, a separate verifier judges completion. + +Use this for: +- implementation loops +- fix-and-verify cycles +- loops the user explicitly asks you to create + +## Examples + +### Babysit the PR + +Interpretation: +- `target=self` +- `sleep=2m` +- no verifier unless requested +- reasonable `max-time` if none given + +Example: ```bash -~/.claude/skills/paseo-loop/bin/loop.sh \ - --worker-prompt "Implement the feature..." \ - --verifier-prompt "Verify the feature works and typecheck passes" \ - --name "feature-x" \ - --worktree "feature-x" +skills/paseo-loop/bin/loop.sh \ + --target self \ + --target-prompt "Check PR #42. Review CI, review comments, and branch status. Fix issues as they arise. Return JSON with done=true only when the PR is fully green and ready." \ + --sleep 2m \ + --max-time 1h \ + --name babysit-pr-42 +``` + +### Launch a Codex agent to babysit the PR + +Interpretation: +- `target=new-agent` +- `worker=codex` +- `sleep=2m` +- usually archive + +```bash +skills/paseo-loop/bin/loop.sh \ + --target new-agent \ + --worker codex \ + --target-prompt "Check PR #42. Review CI, review comments, and branch status. Fix issues as they arise. Return JSON with done=true only when the PR is fully green and ready." \ + --sleep 2m \ + --max-time 1h \ + --archive \ + --name babysit-pr-42 +``` + +### Work until the tests are passing + +Interpretation: +- `target=self` +- verifier enabled +- no sleep + +```bash +skills/paseo-loop/bin/loop.sh \ + --target self \ + --target-prompt "Run the test suite, investigate failures, and fix the code. Stop after you have made a coherent attempt for this iteration." \ + --verifier-prompt "Run the relevant test suite. Return done=true only if all tests pass. Reason must cite the exact command and outcome." \ + --max-iterations 10 \ + --name fix-tests +``` + +### Create a loop to fix the tests + +Interpretation: +- `target=new-agent` +- verifier enabled +- often use a worktree + +```bash +skills/paseo-loop/bin/loop.sh \ + --target new-agent \ + --worker codex \ + --target-prompt "Run the test suite, investigate failures, fix the code, and leave the repo in a clean verifiable state for this iteration." \ + --verifier-prompt "Run the relevant test suite. Return done=true only if all tests pass. Reason must cite the exact command and outcome." \ + --worktree fix-tests \ + --max-iterations 10 \ + --name fix-tests +``` + +### Loop a Codex agent to complete issue 456 in a worktree + +Interpretation: +- `target=new-agent` +- worker codex +- verifier enabled +- worktree enabled + +```bash +skills/paseo-loop/bin/loop.sh \ + --target new-agent \ + --worker codex \ + --target-prompt "Implement issue #456 in this repo. Use the issue description and surrounding code as context. Make incremental progress each iteration and leave clear evidence of what changed." \ + --verifier-prompt "Verify issue #456 is complete. Check changed files, run typecheck and relevant tests, and return done=true only if the implementation meets the issue requirements with evidence." \ + --worktree issue-456 \ + --max-iterations 8 \ + --max-time 2h \ + --name issue-456 ``` ## Your Job -1. **Understand the task** from the conversation context and `$ARGUMENTS` -2. **Decide the mode** — does this need a separate verifier, or can the worker self-terminate? -3. **Write the worker prompt** — what the worker does each iteration. Must be self-contained (the agent has zero prior context). -4. **Write the verifier prompt** (if needed) — what the verifier checks. Should be factual and verifiable. -5. **Choose sleep** — if the task is polling/monitoring, add a sleep duration -6. **Choose archive** — use `--archive` for long-running or polling loops to keep the agent list clean -7. **Choose agents** — default: `codex` worker + `claude/sonnet` verifier -8. **Choose a name** — short, descriptive -9. **Run the script** — call `loop.sh` with all the arguments +1. Understand the user's intent from the conversation and `$ARGUMENTS` +2. Decide `target=self` or `target=new-agent` +3. Decide whether a verifier is needed +4. Write the target prompt so it is self-contained for the selected target +5. Write the verifier prompt, if used, so it is factual and evidence-based +6. Choose sleep only when the task is naturally scheduled or polling +7. Add sensible stop conditions +8. Choose worker / verifier agents +9. Choose a short name +10. Run `skills/paseo-loop/bin/loop.sh` with the final arguments -### When to use a verifier vs self-terminating +## Prompt Writing Rules -Use a verifier (`--verifier-prompt`) when: -- The worker's job is to implement something and you want independent verification -- You want the verification done by a different agent than the worker -- The worker should focus on doing work, not on judging its own work +### Target prompt -Use self-terminating (no verifier) when: -- The worker is checking/polling an external condition (CI status, deployment health) -- The worker can objectively determine when it's done (tests pass, file exists) -- You want a single agent doing both the work and the evaluation +The target prompt must be: +- self-contained +- concrete about commands, files, branches, tests, PRs, or systems to inspect +- explicit about what counts as progress this iteration -### Writing a Good Worker Prompt +If there is no verifier, the target prompt must instruct the target to end with strict JSON matching: -The worker prompt is what the agent receives each iteration. It must be: +```json +{ "done": boolean, "reason": "string" } +``` -1. **Self-contained** — The agent starts with zero context. Everything needed is in the prompt. -2. **Specific** — Name files, functions, types, URLs. Be concrete. -3. **Action-oriented** — Tell the agent what to do, not what to think about. +### Verifier prompt -### Writing a Good Verifier Prompt +The verifier prompt should: +- check facts, not offer fixes +- cite commands, outputs, or file evidence +- return strict JSON matching: -The verifier prompt defines what the verifier checks after each worker iteration. It can range from strict factual checks to qualitative code review: +```json +{ "done": boolean, "reason": "string" } +``` -**Factual / objective checks:** -- "Run `npm test` and `npm run typecheck`. Report done only if both pass with zero failures." -- "Check that PR #42 has all CI checks green and no unresolved review comments." -- "Verify the API responds to `GET /health` with status 200 within 500ms." +## Skill Stacking -**Code quality / style checks:** -- "Review the changes for DRY violations. Report done when there is no duplicated logic across files." -- "Check that the implementation does not over-engineer. No unnecessary abstractions, no premature generalization, no feature flags for single-use code." -- "Verify the code follows the project's conventions: functional style, no classes, explicit types, no `any`." - -**Performance / resource checks:** -- "Run the benchmark suite. Report done when p99 latency is under 100ms." -- "Check bundle size. Report done when the production build is under 500KB gzipped." - -**Comprehensive verification:** -- "Verify every step of the plan was implemented. Check file changes, types, test output. Run `npm run typecheck` and `npm test`. Report each criterion individually with evidence." - -The verifier should report facts with evidence, not suggest fixes. - -### Skill Stacking - -You can instruct the worker to use other skills: +The target prompt can instruct the worker to use other skills: ```bash -~/.claude/skills/paseo-loop/bin/loop.sh \ - --worker-prompt "Use /committee to plan, then fix the provider list bug. The bug is..." \ - --verifier-prompt "The provider list renders correctly and npm run typecheck passes" \ - --name "provider-fix" -``` - -### Composing with Handoff - -A handoff can launch a loop: - -``` -/handoff a loop in a worktree to babysit PR #42 +skills/paseo-loop/bin/loop.sh \ + --target new-agent \ + --target-prompt "Use /committee first if you are stuck, then fix the provider list bug." \ + --verifier-prompt "Verify the provider list renders correctly and typecheck passes." \ + --name provider-fix ``` diff --git a/skills/paseo-loop/bin/loop.sh b/skills/paseo-loop/bin/loop.sh index e4b9b37f1..43d8a1e81 100755 --- a/skills/paseo-loop/bin/loop.sh +++ b/skills/paseo-loop/bin/loop.sh @@ -3,29 +3,34 @@ set -euo pipefail usage() { cat <<'EOF' -Usage: loop.sh --worker-prompt TEXT --name NAME [options] +Usage: loop.sh --name NAME --target-prompt TEXT [options] -Worker (required): - --worker-prompt TEXT Prompt given to the worker agent each iteration - --worker-prompt-file PATH Read the worker prompt from a file - --worker PROVIDER/MODEL Worker agent (e.g. codex, codex/gpt-5.4, claude/sonnet). Default: codex +Target (required): + --target self|new-agent Who acts each iteration. Default: new-agent + --target-prompt TEXT Prompt given to the target each iteration + --target-prompt-file PATH Read the target prompt from a file + +Target options: + --agent-id ID Existing agent id for --target self + Default: $PASEO_AGENT_ID + --worker PROVIDER/MODEL Worker agent for --target new-agent + Default: codex Verifier (optional): - --verifier-prompt TEXT Verification prompt evaluated by a separate agent after each iteration - --verifier-prompt-file PATH Read the verification prompt from a file - --verifier PROVIDER/MODEL Verifier agent (e.g. claude/sonnet, codex/gpt-5.4). Default: claude/sonnet + --verifier-prompt TEXT Verification prompt evaluated after each iteration + --verifier-prompt-file PATH Read the verification prompt from a file + --verifier PROVIDER/MODEL Verifier agent. Default: claude/sonnet - When a verifier prompt is provided, a separate verifier agent evaluates the - condition after each worker iteration. When omitted, the worker itself returns - { done: boolean, reason: string } and decides when the loop is done. +Limits: + --sleep DURATION Sleep between iterations (e.g. 30s, 5m, 1h) + --max-iterations N Maximum loop iterations (default: unlimited) + --max-time DURATION Maximum total runtime (e.g. 30m, 2h) -Options: - --name NAME Name prefix for agents (required) - --sleep DURATION Sleep between iterations (e.g. 30s, 5m, 1h). Default: no delay. - --max-iterations N Maximum loop iterations (default: unlimited) - --archive Archive agents after each iteration - --worktree NAME Run all agents in this worktree (created on first use, reused after) - --thinking LEVEL Thinking level for worker (default: medium) +Other options: + --name NAME Name prefix for loop tracking (required) + --archive Archive newly created agents after each iteration + --worktree NAME Run new agents in this worktree + --thinking LEVEL Thinking level for new-agent worker (default: medium) EOF exit 1 } @@ -47,39 +52,37 @@ parse_agent_spec() { fi } -# Defaults -max_iterations=0 -archive=false -worker_spec="" -verifier_spec="" -worker_prompt_input="" -worker_prompt_file_input="" -verifier_prompt_input="" -verifier_prompt_file_input="" -name="" -thinking="medium" -worktree="" -sleep_duration="" -state_root="${HOME}/.paseo/loops" +parse_duration_to_seconds() { + local raw="$1" + if [[ -z "$raw" ]]; then + echo "Error: duration cannot be empty" >&2 + exit 1 + fi -while [[ $# -gt 0 ]]; do - case "$1" in - --worker-prompt) worker_prompt_input="$2"; shift 2 ;; - --worker-prompt-file) worker_prompt_file_input="$2"; shift 2 ;; - --worker) worker_spec="$2"; shift 2 ;; - --verifier-prompt) verifier_prompt_input="$2"; shift 2 ;; - --verifier-prompt-file) verifier_prompt_file_input="$2"; shift 2 ;; - --verifier) verifier_spec="$2"; shift 2 ;; - --name) name="$2"; shift 2 ;; - --max-iterations) max_iterations="$2"; shift 2 ;; - --archive) archive=true; shift ;; - --sleep) sleep_duration="$2"; shift 2 ;; - --worktree) worktree="$2"; shift 2 ;; - --thinking) thinking="$2"; shift 2 ;; - --help|-h) usage ;; - *) echo "Unknown option: $1"; usage ;; - esac -done + if [[ "$raw" =~ ^[0-9]+$ ]]; then + echo "$raw" + return + fi + + if [[ "$raw" =~ ^([0-9]+)(s|m|h|d)$ ]]; then + local value="${BASH_REMATCH[1]}" + local unit="${BASH_REMATCH[2]}" + case "$unit" in + s) echo "$value" ;; + m) echo $((value * 60)) ;; + h) echo $((value * 3600)) ;; + d) echo $((value * 86400)) ;; + *) + echo "Error: unsupported duration unit: $unit" >&2 + exit 1 + ;; + esac + return + fi + + echo "Error: invalid duration: $raw (use Ns, Nm, Nh, Nd, or seconds)" >&2 + exit 1 +} load_prompt() { local inline_value="$1" @@ -114,10 +117,171 @@ load_prompt() { return 1 } -worker_prompt="$(load_prompt "$worker_prompt_input" "$worker_prompt_file_input" "worker-prompt" "true")" +build_target_prompt() { + local prompt="$1" + local prior_reason="$2" + local require_structured_output="$3" + local full_prompt="$prompt" + + if [[ -n "$prior_reason" ]]; then + full_prompt="$full_prompt + + +The previous iteration reported the following: + +$prior_reason +" + fi + + if [[ "$require_structured_output" == "true" ]]; then + full_prompt="$full_prompt + +End your response with strict JSON matching: +{ \"done\": true/false, \"reason\": \"...\" } + +Rules: +- Return only valid JSON +- done=true only if the loop's goal is complete +- done=false if more work is needed +- reason must briefly explain the current state" + fi + + printf '%s' "$full_prompt" +} + +extract_last_assistant_message() { + local agent_id="$1" + PASEO_LOOP_REPO_ROOT="$repo_root" TARGET_AGENT_ID="$agent_id" npx tsx --eval ' + import { pathToFileURL } from "node:url"; + + const repoRoot = process.env.PASEO_LOOP_REPO_ROOT; + const agentId = process.env.TARGET_AGENT_ID; + if (!repoRoot || !agentId) { + throw new Error("Missing repo root or agent id"); + } + + const { connectToDaemon } = await import( + pathToFileURL(`${repoRoot}/packages/cli/src/utils/client.ts`).href + ); + const { resolveStructuredResponseMessage } = await import( + pathToFileURL(`${repoRoot}/packages/cli/src/commands/agent/run.ts`).href + ); + + const client = await connectToDaemon({}); + try { + const message = await resolveStructuredResponseMessage({ + client, + agentId, + lastMessage: null, + }); + if (message) { + process.stdout.write(message); + } + } finally { + await client.close(); + } + ' +} + +parse_done_reason() { + local raw="$1" + local context_label="$2" + + if ! echo "$raw" | jq -e '.done | type == "boolean"' >/dev/null 2>&1; then + echo "Error: ${context_label} response did not include boolean .done" >&2 + echo "$raw" >&2 + exit 1 + fi + + if ! echo "$raw" | jq -e '.reason | type == "string"' >/dev/null 2>&1; then + echo "Error: ${context_label} response did not include string .reason" >&2 + echo "$raw" >&2 + exit 1 + fi +} + +ensure_time_remaining() { + if [[ "$max_time_seconds" -le 0 ]]; then + return + fi + + local now + now="$(date +%s)" + local elapsed=$((now - start_epoch)) + if [[ "$elapsed" -ge "$max_time_seconds" ]]; then + echo "=== Loop exhausted: max time reached (${max_time_raw}) ===" + exit 1 + fi +} + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../../.." && pwd)" + +target="new-agent" +max_iterations=0 +max_time_raw="" +max_time_seconds=0 +archive=false +worker_spec="" +verifier_spec="" +target_prompt_input="" +target_prompt_file_input="" +verifier_prompt_input="" +verifier_prompt_file_input="" +name="" +thinking="medium" +worktree="" +sleep_raw="" +sleep_seconds=0 +agent_id="${PASEO_AGENT_ID:-}" +state_root="${HOME}/.paseo/loops" + +while [[ $# -gt 0 ]]; do + case "$1" in + --target) target="$2"; shift 2 ;; + --target-prompt) target_prompt_input="$2"; shift 2 ;; + --target-prompt-file) target_prompt_file_input="$2"; shift 2 ;; + --agent-id) agent_id="$2"; shift 2 ;; + --worker) worker_spec="$2"; shift 2 ;; + --verifier-prompt) verifier_prompt_input="$2"; shift 2 ;; + --verifier-prompt-file) verifier_prompt_file_input="$2"; shift 2 ;; + --verifier) verifier_spec="$2"; shift 2 ;; + --name) name="$2"; shift 2 ;; + --max-iterations) max_iterations="$2"; shift 2 ;; + --max-time) max_time_raw="$2"; shift 2 ;; + --archive) archive=true; shift ;; + --sleep) sleep_raw="$2"; shift 2 ;; + --worktree) worktree="$2"; shift 2 ;; + --thinking) thinking="$2"; shift 2 ;; + --help|-h) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +case "$target" in + self|new-agent) ;; + *) + echo "Error: --target must be 'self' or 'new-agent'" + usage + ;; +esac + +target_prompt="$(load_prompt "$target_prompt_input" "$target_prompt_file_input" "target-prompt" "true")" [[ -z "$name" ]] && { echo "Error: --name is required"; usage; } -# Load verifier prompt if provided (optional) +if [[ "$target" == "self" && -z "$agent_id" ]]; then + echo "Error: --target self requires --agent-id or \$PASEO_AGENT_ID" + exit 1 +fi + +if [[ -n "$sleep_raw" ]]; then + sleep_seconds="$(parse_duration_to_seconds "$sleep_raw")" +fi + +if [[ -n "$max_time_raw" ]]; then + max_time_seconds="$(parse_duration_to_seconds "$max_time_raw")" +fi + has_verifier=false verifier_prompt_text="" if [[ -n "$verifier_prompt_input" || -n "$verifier_prompt_file_input" ]]; then @@ -125,7 +289,6 @@ if [[ -n "$verifier_prompt_input" || -n "$verifier_prompt_file_input" ]]; then has_verifier=true fi -# Parse agent specs read -r worker_provider worker_model <<< "$(parse_agent_spec "$worker_spec" "codex" "")" read -r verifier_provider verifier_model <<< "$(parse_agent_spec "$verifier_spec" "claude" "sonnet")" @@ -141,14 +304,13 @@ while [[ -e "$state_dir" ]]; do loop_id="$(generate_loop_id)" state_dir="${state_root}/${loop_id}" done - mkdir -p "$state_dir" -prompt_file="${state_dir}/worker-prompt.md" +target_prompt_file="${state_dir}/target-prompt.md" last_reason_file="${state_dir}/last_reason.md" history_log="${state_dir}/history.log" -printf '%s\n' "$worker_prompt" > "$prompt_file" +printf '%s\n' "$target_prompt" > "$target_prompt_file" printf '' > "$last_reason_file" printf '' > "$history_log" @@ -157,135 +319,181 @@ if [[ "$has_verifier" == true ]]; then printf '%s\n' "$verifier_prompt_text" > "$verifier_prompt_file" fi -# Build worker flags worker_flags=() if [[ "$worker_provider" == "codex" ]]; then worker_flags+=(--mode full-access --provider codex) elif [[ "$worker_provider" == "claude" ]]; then - worker_flags+=(--mode bypassPermissions) + worker_flags+=(--mode bypassPermissions --provider claude) fi [[ -n "$worker_model" ]] && worker_flags+=(--model "$worker_model") [[ -n "$thinking" ]] && worker_flags+=(--thinking "$thinking") -# Build verifier flags verifier_flags=() if [[ "$verifier_provider" == "codex" ]]; then verifier_flags+=(--mode full-access --provider codex) elif [[ "$verifier_provider" == "claude" ]]; then - verifier_flags+=(--mode bypassPermissions) + verifier_flags+=(--mode bypassPermissions --provider claude) fi [[ -n "$verifier_model" ]] && verifier_flags+=(--model "$verifier_model") -# Worktree flags — passed to every paseo run call worktree_flags=() if [[ -n "$worktree" ]]; then base_branch="$(git branch --show-current 2>/dev/null || echo "main")" worktree_flags+=(--worktree "$worktree" --base "$base_branch") fi -# Structured output schema done_schema='{"type":"object","properties":{"done":{"type":"boolean"},"reason":{"type":"string"}},"required":["done","reason"],"additionalProperties":false}' - +start_epoch="$(date +%s)" iteration=0 echo "=== Loop started: $name ===" echo " Loop ID: $loop_id" echo " State dir: $state_dir" -echo " Worker prompt: $prompt_file (live-editable)" +echo " Target: $target" +if [[ "$target" == "self" ]]; then + echo " Agent ID: $agent_id" +else + echo " Worker: $worker_provider/${worker_model:-(default)}" +fi +echo " Target prompt: $target_prompt_file (live-editable)" if [[ "$has_verifier" == true ]]; then echo " Verifier prompt: $verifier_prompt_file (live-editable)" - echo " Mode: worker + verifier" + echo " Verifier: $verifier_provider/${verifier_model:-(default)}" else - echo " Mode: self-terminating worker" + echo " Verifier: none" fi echo " Last reason file: $last_reason_file" echo " History log: $history_log" -echo " Worker: $worker_provider/${worker_model:-(default)}" -if [[ "$has_verifier" == true ]]; then - echo " Verifier: $verifier_provider/${verifier_model:-(default)}" +if [[ -n "$sleep_raw" ]]; then + echo " Sleep: $sleep_raw between iterations" fi -if [[ -n "$sleep_duration" ]]; then - echo " Sleep: $sleep_duration between iterations" +if [[ -n "$max_time_raw" ]]; then + echo " Max time: $max_time_raw" +else + echo " Max time: unlimited" fi if [[ "$archive" == true ]]; then - echo " Archive: agents archived after each iteration" + echo " Archive: newly created agents archived after each iteration" fi if [[ -n "$worktree" ]]; then echo " Worktree: $worktree (base: $base_branch)" fi -if [[ $max_iterations -gt 0 ]]; then +if [[ "$max_iterations" -gt 0 ]]; then echo " Max iterations: $max_iterations" else echo " Max iterations: unlimited" fi echo "" -while [[ $max_iterations -eq 0 || $iteration -lt $max_iterations ]]; do +while [[ "$max_iterations" -eq 0 || "$iteration" -lt "$max_iterations" ]]; do + ensure_time_remaining + iteration=$((iteration + 1)) - if [[ $max_iterations -gt 0 ]]; then + if [[ "$max_iterations" -gt 0 ]]; then echo "--- Iteration $iteration/$max_iterations ---" else echo "--- Iteration $iteration ---" fi - if [[ ! -s "$prompt_file" ]]; then - echo "Error: worker prompt file is missing or empty: $prompt_file" + if [[ ! -s "$target_prompt_file" ]]; then + echo "Error: target prompt file is missing or empty: $target_prompt_file" exit 1 fi - current_prompt="$(cat "$prompt_file")" + current_target_prompt="$(cat "$target_prompt_file")" last_reason="$(cat "$last_reason_file" 2>/dev/null || true)" - # Build the full worker prompt - full_worker_prompt="$current_prompt" - - if [[ -n "$last_reason" ]]; then - full_worker_prompt="$full_worker_prompt - - -The previous iteration reported the following: - -$last_reason -" + target_needs_structured_output=false + if [[ "$has_verifier" == false ]]; then + target_needs_structured_output=true fi - worker_name="${name}-${iteration}" + full_target_prompt="$(build_target_prompt "$current_target_prompt" "$last_reason" "$target_needs_structured_output")" + + if [[ "$target" == "new-agent" ]]; then + worker_name="${name}-${iteration}" + + if [[ "$has_verifier" == true ]]; then + echo "Launching worker: $worker_name" + worker_id=$(paseo run -d "${worker_flags[@]}" "${worktree_flags[@]}" --name "$worker_name" "$full_target_prompt" -q) + echo "Worker [$worker_name] launched. ID: $worker_id" + echo " Stream logs: paseo logs $worker_id -f" + echo " Inspect: paseo inspect $worker_id" + + echo "" + echo "Waiting for worker to complete..." + paseo wait "$worker_id" + echo "Worker done." + + if [[ "$archive" == true ]]; then + paseo agent archive "$worker_name" 2>/dev/null || true + fi + else + echo "Launching worker: $worker_name" + verdict=$(paseo run "${worker_flags[@]}" "${worktree_flags[@]}" --name "$worker_name" --output-schema "$done_schema" "$full_target_prompt") + echo "Result: $verdict" + parse_done_reason "$verdict" "worker" + done_value=$(echo "$verdict" | jq -r '.done') + reason=$(echo "$verdict" | jq -r '.reason') + + if [[ "$archive" == true ]]; then + paseo agent archive "$worker_name" 2>/dev/null || true + fi + + printf '[%s] iteration=%s target=%s target_agent=%s done=%s reason=%s\n' \ + "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + "$iteration" \ + "$target" \ + "$worker_name" \ + "$done_value" \ + "$(echo "$reason" | tr '\n' ' ')" >> "$history_log" + fi + else + iteration_label="self:${agent_id}" + iteration_prompt_file="$(mktemp)" + trap 'rm -f "$iteration_prompt_file"' EXIT + printf '%s\n' "$full_target_prompt" > "$iteration_prompt_file" + + echo "Sending iteration prompt to existing agent: $agent_id" + paseo send "$agent_id" --prompt-file "$iteration_prompt_file" >/dev/null + rm -f "$iteration_prompt_file" + trap - EXIT + + if [[ "$has_verifier" == false ]]; then + verdict="$(extract_last_assistant_message "$agent_id")" + echo "Result: $verdict" + parse_done_reason "$verdict" "self target" + done_value=$(echo "$verdict" | jq -r '.done') + reason=$(echo "$verdict" | jq -r '.reason') + + printf '[%s] iteration=%s target=%s target_agent=%s done=%s reason=%s\n' \ + "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + "$iteration" \ + "$target" \ + "$iteration_label" \ + "$done_value" \ + "$(echo "$reason" | tr '\n' ' ')" >> "$history_log" + fi + fi if [[ "$has_verifier" == true ]]; then - # --- Mode: worker + separate verifier --- - - # Worker runs detached (no structured output) - echo "Launching worker: $worker_name" - worker_id=$(paseo run -d "${worker_flags[@]}" "${worktree_flags[@]}" --name "$worker_name" "$full_worker_prompt" -q) - echo "Worker [$worker_name] launched. ID: $worker_id" - echo " Stream logs: paseo logs $worker_id -f" - echo " Inspect: paseo inspect $worker_id" - - echo "" - echo "Waiting for worker to complete..." - paseo wait "$worker_id" - echo "Worker done." - - if [[ "$archive" == true ]]; then - paseo agent archive "$worker_name" 2>/dev/null || true - fi - - # Re-read verifier prompt for live steering current_verifier_prompt="$(cat "$verifier_prompt_file")" - - # Verifier runs synchronously with structured output verifier_name="${name}-verify-${iteration}" full_verifier_prompt="$current_verifier_prompt -Respond with { \"done\": true/false, \"reason\": \"...\" }. -- done: true if the condition is met, false otherwise -- reason: explain what you found with evidence" +Respond with strict JSON matching: +{ \"done\": true/false, \"reason\": \"...\" } + +Rules: +- done=true only if the loop goal has been met +- done=false if another iteration is needed +- reason must explain what you found with evidence" echo "" echo "Launching verifier: $verifier_name" verdict=$(paseo run "${verifier_flags[@]}" "${worktree_flags[@]}" --name "$verifier_name" --output-schema "$done_schema" "$full_verifier_prompt") echo "Verdict: $verdict" - + parse_done_reason "$verdict" "verifier" done_value=$(echo "$verdict" | jq -r '.done') reason=$(echo "$verdict" | jq -r '.reason') @@ -293,33 +501,18 @@ Respond with { \"done\": true/false, \"reason\": \"...\" }. paseo agent archive "$verifier_name" 2>/dev/null || true fi - printf '[%s] iteration=%s worker=%s verifier=%s done=%s reason=%s\n' \ - "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ - "$iteration" \ - "$worker_name" \ - "$verifier_name" \ - "$done_value" \ - "$(echo "$reason" | tr '\n' ' ')" >> "$history_log" - - else - # --- Mode: self-terminating worker --- - - # Worker runs synchronously with structured output - echo "Launching worker: $worker_name" - verdict=$(paseo run "${worker_flags[@]}" "${worktree_flags[@]}" --name "$worker_name" --output-schema "$done_schema" "$full_worker_prompt") - echo "Result: $verdict" - - done_value=$(echo "$verdict" | jq -r '.done') - reason=$(echo "$verdict" | jq -r '.reason') - - if [[ "$archive" == true ]]; then - paseo agent archive "$worker_name" 2>/dev/null || true + if [[ "$target" == "new-agent" ]]; then + target_label="$worker_name" + else + target_label="self:${agent_id}" fi - printf '[%s] iteration=%s worker=%s done=%s reason=%s\n' \ + printf '[%s] iteration=%s target=%s target_agent=%s verifier=%s done=%s reason=%s\n' \ "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ "$iteration" \ - "$worker_name" \ + "$target" \ + "$target_label" \ + "$verifier_name" \ "$done_value" \ "$(echo "$reason" | tr '\n' ' ')" >> "$history_log" fi @@ -334,9 +527,10 @@ Respond with { \"done\": true/false, \"reason\": \"...\" }. echo "Not done: $reason" printf '%s\n' "$reason" > "$last_reason_file" - if [[ -n "$sleep_duration" ]] && [[ $max_iterations -eq 0 || $iteration -lt $max_iterations ]]; then - echo "Sleeping $sleep_duration before next iteration..." - sleep "$sleep_duration" + ensure_time_remaining + if [[ "$sleep_seconds" -gt 0 ]] && [[ "$max_iterations" -eq 0 || "$iteration" -lt "$max_iterations" ]]; then + echo "Sleeping $sleep_raw before next iteration..." + sleep "$sleep_seconds" fi echo ""