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
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ owloop is a spec-driven autonomous coding loop for Claude Code — "Your code ev

| Path | Purpose |
|---|---|
| `src/owloop/cli.py` | Python CLI (`init` / `run` / `plan` / `status` / `version` subcommands), rich console output |
| `src/owloop/cli.py` | Thin CLI entry point — defines the click `main` group and wires commands |
| `src/owloop/commands/` | One module per subcommand (`run`, `go`, `spec`, `check`, `status`, `finish`, `logs`, `report`, `agents`, `discover`, `spec-from-issue`, `version`, `init`) |
| `src/owloop/cli_options.py` | Shared click options/decorators (`--model`, `--max-tokens`, etc.) |
| `src/owloop/cli_display.py` | Banner, progress bar, spec table, `AgentStreamDisplay`, and other console helpers |
| `src/owloop/cli_config.py` | Persistent config path and default merging for `.owloop/config.toml` |
| `src/owloop/sessions.py` | Latest session discovery and worktree path lookup |
| `src/owloop/engine.py` | Python loop engine — spawns agent per iteration, manages worktree, drives spec queue |
| `src/owloop/config.py` | Persistent `.owloop/config.toml` `[run]` loader and validation |
| `src/owloop/verification.py` | The shared deterministic gate (acceptance criteria + backpressure via `subprocess`, tamper hash) used by both engine and parallel orchestrator |
| `src/owloop/parallel.py` | File-disjoint parallel workers (`owloop run --workers N`): per-worker worktrees, shared gate, merge-back |
| `src/owloop/notifications.py` | Best-effort completion notifications (webhook/desktop) when a run stops |
Expand Down Expand Up @@ -59,7 +65,7 @@ owloop is a spec-driven autonomous coding loop for Claude Code — "Your code ev

- **Auto Mode, not YOLO** — always `--permission-mode auto`, never `--dangerously-skip-permissions`. This is the core premise of the fork; don't regress it. On the ACP path this means owloop answers `session/request_permission` itself (`allow_once`) and never launches an agent in a bypass mode.
- **ACP-first for new agents** — a new coding agent is integrated by adding an `AgentPreset` row in `presets.py`, not a new adapter class. Bespoke stream parsers are reserved for the pre-existing native adapters (`claude`, `kimi`).
- **Worktree isolation, zero extra deps** — the engine uses plain `git worktree add` / `git worktree list`, nothing beyond git itself.
- **Worktree isolation, zero extra deps** — the engine uses plain `git worktree add` / `git worktree list`. Worktrees are created next to the project as `{project_dir}-owloop-wt/owloop-{date}-{slug}-{id}`, nothing beyond git itself.
- **Constraint-oriented specs** — every spec template carries Requirements, shell-verifiable Acceptance Criteria, Exclusions, Style, and Verification. Exclusions are what keep an unattended loop from wandering; never make that section optional.
- **Fresh context per iteration** — each loop round is a brand-new `claude -p` process. State lives on disk (`.owloop/specs/`, `.owloop/logs/`), never accumulated in memory across iterations. Don't design features that assume continuity between rounds. A failed iteration is rolled back to the last good commit (`--no-rollback` opts out) so the next round starts clean; the discarded diff is saved as a patch under `.owloop/logs/`.
- **Enforced verification, not trusted verification** — the engine, not the agent, owns git and completion. When the agent signals DONE, the engine runs the spec's Acceptance Criteria + `backpressure.json` commands itself (the deterministic gate) and only then commits, pushes, and marks the spec `COMPLETE`. The agent must not commit/push, mark status, or edit its own Acceptance Criteria/Verification (tamper detection fails the iteration). Never move these guarantees back into prompts.
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ owloop go "refactor error handling" # one command: init → spec → review

That's it. owloop auto-initializes, scans your codebase, generates spec(s), asks for approval, and starts the loop.

## Persistent defaults

Tired of repeating the same flags? Put them in `.owloop/config.toml` under `[run]`:

```toml
[run]
model = "claude-sonnet-4"
max_iterations = 20
max_tokens = 200000
max_duration = 3600
workers = 2
notify_desktop = true
keep_retrying = false
```

Supported keys: `model`, `notify_desktop`, `converge`, `workers`, `rollback`, `max_iterations`, `max_duration`, `max_tokens`, `idle_timeout`, `max_tokens_per_iteration`, `max_turns_per_iteration`, `keep_retrying`, `notify_webhook`, `no_tui`, `dry_run`.

CLI flags always win: an explicit `--max-iterations 5` overrides the config file.

## Install as an Agent Skill

Owloop also ships as a set of composable agent skills for any agentskills.io-compatible agent:
Expand Down Expand Up @@ -69,7 +88,9 @@ Skills included:
| `owloop run --max-tokens 200000` | Stop after token budget reached |
| `owloop run --no-tui` / `--plain` | Print plain console output instead of the full-screen TUI |
| `owloop check` | Validate all specs (pre-flight linter) |
| `owloop status` | Show specs and completion progress |
| `owloop status` | Show specs, completion progress, and the latest session/runtime summary |
| `owloop finish` | Show the latest session and optionally merge, push, and clean up |
| `owloop logs` | Inspect per-iteration logs, events, and discarded patches |
| `owloop report` | Generate AI-powered HTML summary report |
| `owloop -v go "goal"` | Verbose mode: show all agent output with timestamps |
| `owloop report --no-ai` | Generate fast, offline report |
Expand Down
14 changes: 13 additions & 1 deletion site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ <h2 class="section-title">From goal to commits, unattended.</h2>
</div>
</div>
<div class="terminal-statusbar">
<span>owloop v0.2.1</span>
<span>owloop v0.4.0</span>
<span>model: claude-sonnet-5</span>
<span>budget: 200k tokens</span>
<button class="replay-btn" id="terminal-replay" type="button" title="Replay demo">↻ Replay</button>
Expand Down Expand Up @@ -298,6 +298,18 @@ <h3>Fix-loop detection</h3>
<h3>Token budget cap</h3>
<p>Set <code>--max-tokens</code> and <code>--max-duration</code>. The loop stops before your wallet does.</p>
</article>
<article class="feature-card">
<div class="feature-glow"></div>
<div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<h3>Persistent defaults</h3>
<p>Store your favorite flags in <code>.owloop/config.toml</code> under <code>[run]</code>. Stop typing the same model and budget every time.</p>
</article>
<article class="feature-card">
<div class="feature-glow"></div>
<div class="feature-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<h3>Resume & finish</h3>
<p><code>owloop run --resume</code> picks up where it stopped. <code>owloop finish</code> merges, pushes, and cleans up the worktree when you're ready.</p>
</article>
</div>

<div class="report-preview">
Expand Down
20 changes: 10 additions & 10 deletions site/terminal-demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,27 +112,27 @@
// ── Rich-style TUI rendered as HTML panels ──

var TUI_FRAMES = [
{ moon: "🌒", elapsed: "0:42", iter: "#1", branch: "owloop/refactor-errors", tokens: "2,140 / 200,000", specsDone: "0/3 done", progress: 0, current: "001-refactor-error-handling.md", status: "Working on spec", statusStyle: "amber",
{ moon: "🌒", elapsed: "0:42", iter: "#1", branch: "owloop/20250707-refactor-errors-a1b2c3d4", tokens: "2,140 / 200,000", specsDone: "0/3 done", progress: 0, current: "001-refactor-error-handling.md", status: "Working on spec", statusStyle: "amber",
specs: [["active","001-refactor-error-handling.md"],["pending","002-add-type-annotations.md"],["pending","003-unify-error-codes.md"]],
action: "Reading the spec and codebase", detail: "Scanning backend/app/api/ for ValidationError patterns" },
{ moon: "🌒", elapsed: "1:15", iter: "#1", branch: "owloop/refactor-errors", tokens: "3,480 / 200,000", specsDone: "0/3 done", progress: 8, current: "001-refactor-error-handling.md", status: "Running acceptance criteria", statusStyle: "amber",
{ moon: "🌒", elapsed: "1:15", iter: "#1", branch: "owloop/20250707-refactor-errors-a1b2c3d4", tokens: "3,480 / 200,000", specsDone: "0/3 done", progress: 8, current: "001-refactor-error-handling.md", status: "Running acceptance criteria", statusStyle: "amber",
specs: [["active","001-refactor-error-handling.md"],["pending","002-add-type-annotations.md"],["pending","003-unify-error-codes.md"]],
action: "Running acceptance criteria", detail: 'grep -c "except ValidationError" backend/app/api/*.py → 3 (≤5 ✓)' },
{ moon: "🌓", elapsed: "1:23", iter: "#1", branch: "owloop/refactor-errors", tokens: "4,120 / 200,000", specsDone: "0/3 done", progress: 15, current: "001-refactor-error-handling.md", status: "✓ done signal detected", statusStyle: "green",
{ moon: "🌓", elapsed: "1:23", iter: "#1", branch: "owloop/20250707-refactor-errors-a1b2c3d4", tokens: "4,120 / 200,000", specsDone: "0/3 done", progress: 15, current: "001-refactor-error-handling.md", status: "✓ done signal detected", statusStyle: "green",
specs: [["active","001-refactor-error-handling.md"],["pending","002-add-type-annotations.md"],["pending","003-unify-error-codes.md"]],
action: "Committing changes", detail: "🌙 Loop closed on iteration 1" },
{ moon: "🌓", elapsed: "1:58", iter: "#2", branch: "owloop/refactor-errors", tokens: "6,820 / 200,000", specsDone: "1/3 done", progress: 33, current: "002-add-type-annotations.md", status: "Working on spec", statusStyle: "amber",
{ moon: "🌓", elapsed: "1:58", iter: "#2", branch: "owloop/20250707-refactor-errors-a1b2c3d4", tokens: "6,820 / 200,000", specsDone: "1/3 done", progress: 33, current: "002-add-type-annotations.md", status: "Working on spec", statusStyle: "amber",
specs: [["done","001-refactor-error-handling.md"],["active","002-add-type-annotations.md"],["pending","003-unify-error-codes.md"]],
action: "Running verification commands", detail: "uv run pyright src/ --outputjson → 0 errors" },
{ moon: "🌔", elapsed: "2:12", iter: "#2", branch: "owloop/refactor-errors", tokens: "8,240 / 200,000", specsDone: "1/3 done", progress: 42, current: "002-add-type-annotations.md", status: "✓ done signal detected", statusStyle: "green",
{ moon: "🌔", elapsed: "2:12", iter: "#2", branch: "owloop/20250707-refactor-errors-a1b2c3d4", tokens: "8,240 / 200,000", specsDone: "1/3 done", progress: 42, current: "002-add-type-annotations.md", status: "✓ done signal detected", statusStyle: "green",
specs: [["done","001-refactor-error-handling.md"],["active","002-add-type-annotations.md"],["pending","003-unify-error-codes.md"]],
action: "Committing changes", detail: "🌙 Loop closed on iteration 2" },
{ moon: "🌔", elapsed: "2:47", iter: "#3", branch: "owloop/refactor-errors", tokens: "10,540 / 200,000", specsDone: "2/3 done", progress: 66, current: "003-unify-error-codes.md", status: "Working on spec", statusStyle: "amber",
{ moon: "🌔", elapsed: "2:47", iter: "#3", branch: "owloop/20250707-refactor-errors-a1b2c3d4", tokens: "10,540 / 200,000", specsDone: "2/3 done", progress: 66, current: "003-unify-error-codes.md", status: "Working on spec", statusStyle: "amber",
specs: [["done","001-refactor-error-handling.md"],["done","002-add-type-annotations.md"],["active","003-unify-error-codes.md"]],
action: "Running acceptance criteria", detail: "uv run pytest tests/ -q --tb=short → 148 passed" },
{ moon: "🌕", elapsed: "3:05", iter: "#3", branch: "owloop/refactor-errors", tokens: "12,380 / 200,000", specsDone: "3/3 done", progress: 100, current: "—", status: "🌅 All specs complete", statusStyle: "green",
{ moon: "🌕", elapsed: "3:05", iter: "#3", branch: "owloop/20250707-refactor-errors-a1b2c3d4", tokens: "12,380 / 200,000", specsDone: "3/3 done", progress: 100, current: "—", status: "🌅 All specs complete", statusStyle: "green",
specs: [["done","001-refactor-error-handling.md"],["done","002-add-type-annotations.md"],["done","003-unify-error-codes.md"]],
action: "3 commits pushed to owloop/refactor-errors", detail: "" },
action: "3 commits pushed to owloop/20250707-refactor-errors-a1b2c3d4", detail: "" },
];

function specIcon(state) {
Expand Down Expand Up @@ -218,7 +218,7 @@
'</div>' +

'<div class="rpt-cards">' +
'<div class="rpt-card"><div class="rpt-card-val">owloop/refactor-errors</div><div class="rpt-card-label">Branch</div></div>' +
'<div class="rpt-card"><div class="rpt-card-val">owloop/20250707-refactor-errors-a1b2c3d4</div><div class="rpt-card-label">Branch</div></div>' +
'<div class="rpt-card"><div class="rpt-card-val">3</div><div class="rpt-card-label">Iterations</div></div>' +
'<div class="rpt-card"><div class="rpt-card-val rpt-green">Completed</div><div class="rpt-card-label">Status</div></div>' +
'<div class="rpt-card"><div class="rpt-card-val">8 files · <span class="rpt-green">+328</span> · <span class="rpt-red">-108</span></div><div class="rpt-card-label">Total diff</div></div>' +
Expand Down Expand Up @@ -385,7 +385,7 @@

await printLines([
["Ollie is waking up...", "dim"],
["→ worktree: .worktrees/owloop-refactor-errors", "dim"],
["→ worktree: ../flask-api-owloop-wt/owloop-20250707-refactor-errors-a1b2c3d4", "dim"],
["→ model: claude-sonnet-5 budget: 200k tokens", "dim"],
["→ specs: 3 queued", "dim"],
], LINE_PAUSE * 0.5);
Expand Down
70 changes: 62 additions & 8 deletions src/owloop/_brand.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

import subprocess
from os import PathLike

# ── palette ──
Expand Down Expand Up @@ -134,14 +135,67 @@ def wake_message() -> str:
return f"{OLLIE_NAME} is waking up..."


def exit_hints(branch: str, iterations: int, cwd: str | PathLike[str], main_repo_dir: str | PathLike[str]) -> list[str]:
hints = [f"Branch: {branch}"]
if str(cwd) != str(main_repo_dir):
hints = [
f"Review: git log --oneline HEAD~{iterations}..HEAD",
f"Merge: cd {main_repo_dir} && git merge {branch}",
f"Discard: git worktree remove {cwd}",
]
# Stopped reasons where the user can resume work rather than merge.
_RESUME_REASONS = {
"interrupted",
"stalled",
"exhausted",
"blocked",
"decide",
"fix_loop_blocked",
"max_iterations",
"max_duration",
"max_tokens",
}


def _default_branch(main_repo_dir: str | PathLike[str]) -> str:
"""Return the current branch of the main repo, falling back to ``main``."""
try:
result = subprocess.run(
["git", "-C", str(main_repo_dir), "branch", "--show-current"],
capture_output=True,
text=True,
check=False,
)
branch = result.stdout.strip()
if result.returncode == 0 and branch:
return branch
except Exception:
pass
return "main"


def exit_hints(
branch: str,
iterations: int,
cwd: str | PathLike[str],
main_repo_dir: str | PathLike[str],
stopped_reason: str = "",
report_path: str | None = None,
) -> list[str]:
if str(cwd) == str(main_repo_dir):
return [f"Branch: {branch}"]

review_cmd = (
"git log --oneline -1"
if iterations <= 0
else f"git log --oneline HEAD~{iterations}..HEAD"
)

base_branch = _default_branch(main_repo_dir)

hints: list[str] = []
if report_path:
hints.append(f"Report: {report_path}")
hints.extend([
f"Review: {review_cmd}",
f"Merge: cd {main_repo_dir} && git checkout {base_branch} && git merge {branch}",
f"Push: git push origin {base_branch}",
f"Cleanup: git worktree remove {cwd} && git branch -d {branch}",
])
if stopped_reason.lower() in _RESUME_REASONS:
hints.append("Resume: owloop run --resume")
return hints


Expand Down
Loading
Loading