Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,13 @@ When running from source (`bun src/loop.ts`), auto-update is disabled — use `g

- `-a, --agent <claude|codex>`: agent to run (default: `codex`)
- `-p, --prompt <text|.md file>`: prompt text or a `.md` prompt file path. Plain text auto-creates `PLAN.md` first.
- `--proof <text>`: required proof criteria for task completion
- `--proof <text>`: optional proof criteria for task completion
- `--codex-model <model>`: set the model passed to codex (`LOOP_CODEX_MODEL` can also set this by default)
- `-m, --max-iterations <number>`: max loop count (default: infinite)
- `-d, --done <signal>`: done signal string (default: `<promise>DONE</promise>`)
- `--format <pretty|raw>`: output format (default: `pretty`)
- `--review [claude|codex|claudex]`: run a review when done (default: `claudex`; bare `--review` also uses `claudex`). With `claudex`, both reviews run in parallel, then both comments are passed back to the original agent so it can decide what to address. If both reviews found the same issue, that is a stronger signal to fix it.
- `--review-plan [other|claude|codex]`: reviewer for the automatic plan review pass that runs after plain-text prompts create `PLAN.md` (default: `other`, the non-primary model).
- `--tmux`: run `loop` in a detached tmux session so it survives SSH disconnects (auto-attaches when interactive). Session name format: `repo-loop-X`
- `--worktree`: create and run inside a fresh git worktree + branch automatically. Worktree/branch format: `repo-loop-X`
- `-h, --help`: help
Expand All @@ -154,9 +155,12 @@ loop --proof "Use {skill} to verify your changes"
# two iteration, raw JSON/event output
loop -m 2 --proof "Use {skill} to verify your changes" "Implement {feature}" --format raw

# plain text prompt: auto-creates PLAN.md, then runs from PLAN.md
# plain text prompt: auto-creates PLAN.md, then auto-reviews with the other model (default)
loop --proof "Use {skill} to verify your changes" "Implement {feature}"

# plain text prompt: override the plan reviewer
loop --proof "Use {skill} to verify your changes" --review-plan claude "Implement {feature}"

# run with claude
loop --proof "Use {skill} to verify your changes" --agent claude --prompt PLAN.md

Expand Down
49 changes: 43 additions & 6 deletions src/loop/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import {
LOOP_VERSION,
VALUE_FLAGS,
} from "./constants";
import type { Agent, Format, Options, ReviewMode, ValueFlag } from "./types";
import type {
Agent,
Format,
Options,
PlanReviewMode,
ReviewMode,
ValueFlag,
} from "./types";

const REQUIRED_PROOF_ERROR = "Missing required --proof value.";
const EMPTY_DONE_SIGNAL_ERROR = "Invalid --done value: cannot be empty";

const parseAgent = (value: string): Agent => {
Expand All @@ -32,6 +38,13 @@ const parseReviewValue = (value: string): ReviewMode => {
throw new Error(`Invalid --review value: ${value}`);
};

const parsePlanReviewValue = (value: string): PlanReviewMode => {
if (value === "other" || value === "claude" || value === "codex") {
return value;
}
throw new Error(`Invalid --review-plan value: ${value}`);
};

const applyValueFlag = (
flag: ValueFlag,
value: string,
Expand Down Expand Up @@ -101,6 +114,27 @@ const parseReviewArg = (
return index;
};

const parsePlanReviewArg = (
argv: string[],
index: number,
opts: Options,
arg: string
): number => {
if (arg.startsWith("--review-plan=")) {
opts.reviewPlan = parsePlanReviewValue(arg.slice("--review-plan=".length));
return index;
}

const next = argv[index + 1];
if (next === "other" || next === "claude" || next === "codex") {
opts.reviewPlan = next;
return index + 1;
}

opts.reviewPlan = "other";
return index;
};

const consumeArg = (
argv: string[],
index: number,
Expand Down Expand Up @@ -136,6 +170,13 @@ const consumeArg = (
};
}

if (arg === "--review-plan" || arg.startsWith("--review-plan=")) {
return {
nextIndex: parsePlanReviewArg(argv, index, opts, arg) + 1,
stop: false,
};
}

if (arg === "--tmux") {
opts.tmux = true;
return { nextIndex: index + 1, stop: false };
Expand Down Expand Up @@ -195,9 +236,5 @@ export const parseArgs = (argv: string[]): Options => {
opts.promptInput = positional.join(" ");
}

if (!opts.proof) {
throw new Error(REQUIRED_PROOF_ERROR);
}

return opts;
};
31 changes: 16 additions & 15 deletions src/loop/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,25 @@ export const HELP = `
loop - v${LOOP_VERSION} - meta agent loop runner

Usage:
loop Open live panel for running claude/codex instances
loop Open live panel for running claude/codex instances
loop [options] [prompt]
loop update Check for updates and apply if available
loop upgrade Alias for update
loop update Check for updates and apply if available
loop upgrade Alias for update

Options:
-a, --agent <claude|codex> Agent CLI to run (default: codex)
-p, --prompt <text|.md file> Prompt text or path to a .md prompt file
-m, --max-iterations <number>. Max loops (default: infinite)
-d, --done <signal> Done signal (default: <promise>DONE</promise>)
--proof <text> Proof requirements for task completion (required)
--codex-model <model> Override codex model (default: ${DEFAULT_CODEX_MODEL})
--format <pretty|raw> Log format (default: pretty)
--review [claude|codex|claudex] Review on done (default: claudex)
--tmux Run in a detached tmux session (name: repo-loop-X)
--worktree Create and run in a fresh git worktree (name: repo-loop-X)
-v, --version Show loop version
-h, --help Show this help
-a, --agent <claude|codex> Agent CLI to run (default: codex)
-p, --prompt <text|.md file> Prompt text or path to a .md prompt file
-m, --max-iterations <number> Max loops (default: infinite)
-d, --done <signal> Done signal (default: <promise>DONE</promise>)
--proof <text> Proof requirements for task completion
--codex-model <model> Override codex model (default: ${DEFAULT_CODEX_MODEL})
--format <pretty|raw> Log format (default: pretty)
--review [claude|codex|claudex] Review on done (default: claudex)
--review-plan [other|claude|codex] Review PLAN.md after plain-text planning (default: other)
--tmux Run in a detached tmux session (name: repo-loop-X)
--worktree Create and run in a fresh git worktree (name: repo-loop-X)
-v, --version Show loop version
-h, --help Show this help

Auto-update:
Updates are checked automatically on startup and applied on the next run.
Expand Down
21 changes: 19 additions & 2 deletions src/loop/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ const NEWLINE_RE = /\r?\n/;
const SPAWN_TEAM_WITH_WORKTREE_ISOLATION =
"Spawn a team of agents with worktree isolation.";

const appendProofRequirements = (parts: string[], proof: string): void => {
const trimmed = proof.trim();
if (!trimmed) {
return;
}
parts.push(`Proof requirements:\n${trimmed}`);
};

const hasProofInTask = (task: string, proof: string): boolean => {
const proofLines = proof
.split(NEWLINE_RE)
Expand All @@ -30,6 +38,15 @@ export const buildPlanPrompt = (task: string): string =>
"Only write the plan in this step. Do not implement code yet.",
].join("\n\n");

export const buildPlanReviewPrompt = (task: string): string =>
[
"Plan review mode:",
`Task:\n${task.trim()}`,
"Review PLAN.md for correctness, missing steps, and verification gaps.",
"Update PLAN.md directly if needed.",
"Only edit PLAN.md in this step. Do not implement code yet.",
].join("\n\n");

export const buildWorkPrompt = (
task: string,
doneSignal: string,
Expand All @@ -45,7 +62,7 @@ export const buildWorkPrompt = (
}

if (!hasProofInTask(task, proof)) {
parts.push(`Proof requirements:\n${proof.trim()}`);
appendProofRequirements(parts, proof);
}

parts.push(
Expand All @@ -64,7 +81,7 @@ export const buildReviewPrompt = (
"Run checks/tests/commands as needed and inspect changed files.",
];

parts.push(`Proof requirements:\n${proof.trim()}`);
appendProofRequirements(parts, proof);

parts.push(
`If review is needed, end your response with exactly "${REVIEW_FAIL}" on the final non-empty line. Nothing may follow this line.`
Expand Down
Loading