refactor(paseo-loop): support self and new-agent targets with max-time

This commit is contained in:
Mohamed Boudra
2026-03-24 00:04:17 +07:00
parent e969f42c68
commit 0e6aba2886
2 changed files with 605 additions and 321 deletions

View File

@@ -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 `<previous-iteration-result>`. 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=<explicit value or sensible default>`
- 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 `<previous-iteration-result>`.
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/<loop-id>/
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
```

View File

@@ -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
<previous-iteration-result>
The previous iteration reported the following:
$prior_reason
</previous-iteration-result>"
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
<previous-iteration-result>
The previous iteration reported the following:
$last_reason
</previous-iteration-result>"
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 ""