You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This upgrades owloop from a dev tool you babysit into an automation teammate: "every night at 2 a.m., owloop picks up the tasks marked auto, runs them to green, and posts a report by 8 a.m." The tagline writes itself: cron for Claude Code.
This issue is the design + research record for that track. It deliberately builds on top of the trust core (#32 deterministic gate, #33 terminal states, #34 rollback) — unattended scheduling without those is how you wake up to a wrecked branch.
Part 1 — Where owloop stands today (code audit)
Capability
Status
Evidence
Task queue
✅ solid, file-based
spec_queue.py: priority, ## Depends On, topological get_next_ready_spec() — already a miniature dependency-aware task DB
Task intake from outside
🟡 embryonic
spec-from-issue (spec_from_issue.py): GitHub-only, one-shot, one-way; fetches via gh, renders spec template, checklist items → candidate criteria. Jira is a NotImplementedError stub. No provenance stored, no idempotency, no state write-back
Scheduling
❌ none
owloop run is a foreground process; "run overnight" currently means the user hand-writes a cron entry with no lock, no budget enforcement, no report delivery
Result output
🟡 local only
owloop_summary_latest.json, events.jsonl, HTML report (report.py + report_ai.py). Nothing leaves the machine; a loop that stops on BLOCKED at 2 a.m. wastes the night silently (already noted in #37 Phase 2)
Mid-run steering
✅
STEERING.md, run-notes.md, .owloop/learnings.md — good primitives to carry task-source context into iterations
Sessions/resume
✅
session_latest.json, --resume, per-session worktrees — the building blocks a scheduler needs to recover from interrupted nights
Key structural observation: the loop engine never needs to know about external systems.specs/ is already the interface. External integration should be an airlock around the loop (sync-in before, write-back after), not a modification of the loop itself. This preserves the "fresh context per iteration, state on disk" principle verbatim.
Part 2 — How comparable systems do it (research survey, verified 2026-07-06)
clarify gate before generation; EARS acceptance criteria; dependency waves
Cross-framework patterns that matter here
Label-as-trigger, label-removal-as-ack (OpenHands) is the cleanest idempotency contract for issue-tracker intake.
Failure must still produce an artifact — WIP branch + comment beats silent retry, everywhere it's tried.
Task intake converges on structured files in git; issue trackers are the second channel, synced in, never driving the loop live.
Multiple independent bounds, always: iteration + cost + wall-clock + per-task attempt caps; mature systems layer at least two.
Nobody auto-closes the source task. PRs/comments propose; humans dispose. Copilot, OpenHands, Jules, Codex all keep a human on merge.
A scheduled agent needs a scratchpad and a lock: PID-file locking with stale detection and persisted resume state are standard reliability plumbing for cron-driven agent runs.
Scheduled human intent-review every N tasks/hours — tests verify behavior, humans verify intent; drift that passes CI is still drift.
Part 3 — Proposed design (five layers, each independently shippable)
GitHub Issues first (gh already used), Jira next (stub exists), TAPD/Feishu/linear behind the same interface. A generic todo.md source (checklist file in-repo) covers the no-tracker case and is the cheapest to ship.
Trigger contract: label owloop:auto (or checklist tag @auto). On sync, flip to owloop:queued — the trigger is consumed exactly once even if sync runs twice.
source_hash makes re-sync idempotent (same issue → same spec, updated only if upstream changed) and makes divergence detectable: if the upstream issue changed after sync, the spec is flagged stale rather than silently running against yesterday's requirements.
The loop never reads external systems. Sync-in and write-back are separate commands around the run — an airlock, not a live pipe. Fresh-context-per-iteration stays intact; a network-dead night still runs everything already in specs/.
Layer 3 — Night shift: owloop schedule / owloop night
owloop schedule "02:00" --max-duration 300 --max-tokens 2m emits the right artifact per platform: cron/systemd-timer/launchd entry, or a GitHub Actions workflow (schedule: + claude-code-action-style bounded run) for repo-hosted nights.
owloop night = the composed pipeline sync → run → write-back → report as one command, so the scheduler entry is a single line.
Single-writer lock: PID lockfile with stale detection in .owloop/ — a 2 a.m. cron firing while yesterday's run is still going must no-op loudly, not double-run. Overlap is the feat: separate verifier agent from executor agent #1 source of the "错乱" this issue exists to prevent.
Pre-run gate: only specs that pass owloop check and have ≥1 runnable acceptance command are eligible; the rest are skipped and reported, not attempted.
Layer 4 — Write-back: results flow to where the task came from
Per-spec terminal state (from #33's taxonomy) maps to source-side actions:
WIP branch + discarded-diff patch (#34) — failure still produces an artifact
blocked / decide
owloop:blocked
the BLOCKED:/DECIDE: payload, verbatim — the morning human's first read
—
Never auto-close the source issue. owloop proposes; the human disposes (unanimous across Copilot/OpenHands/Jules/Codex). Optionally auto-close only when a linked PR merges — GitHub's own machinery, not ours.
Write-back is at-least-once with dedup: comments carry a <!-- owloop:run-id:spec --> marker so a re-run after a crash updates rather than duplicates.
Layer 5 — Morning report
Extend report.py's existing summary/events into a digest: per-spec outcome table, tokens/cost, stuck items with reasons, links to branches/PRs. Deliver via generic webhook (Slack/Feishu/Discord all accept a JSON POST) and/or a pinned "night report" issue comment.
Relates to: #32#33#34 (prerequisites for Phase II) · #35#15 (budget caps the scheduler depends on) · #36 (clarify gate reused at intake) · #37 (this is the "integration track" alongside its trust track) · #49 (perf track; parallelism explicitly out of scope here) · #9#11 (linter/dry-run as the intake gate's mechanical checks)
External task integration: make owloop the night shift for tasks defined during the day
Vision
Today's flow: user writes spec →
owloop run→ agent executes → results stay on local disk.Target flow:
This upgrades owloop from a dev tool you babysit into an automation teammate: "every night at 2 a.m., owloop picks up the tasks marked
auto, runs them to green, and posts a report by 8 a.m." The tagline writes itself: cron for Claude Code.This issue is the design + research record for that track. It deliberately builds on top of the trust core (#32 deterministic gate, #33 terminal states, #34 rollback) — unattended scheduling without those is how you wake up to a wrecked branch.
Part 1 — Where owloop stands today (code audit)
spec_queue.py: priority,## Depends On, topologicalget_next_ready_spec()— already a miniature dependency-aware task DBspec-from-issue(spec_from_issue.py): GitHub-only, one-shot, one-way; fetches viagh, renders spec template, checklist items → candidate criteria. Jira is aNotImplementedErrorstub. No provenance stored, no idempotency, no state write-backowloop runis a foreground process; "run overnight" currently means the user hand-writes a cron entry with no lock, no budget enforcement, no report deliveryowloop_summary_latest.json,events.jsonl, HTML report (report.py+report_ai.py). Nothing leaves the machine; a loop that stops onBLOCKEDat 2 a.m. wastes the night silently (already noted in #37 Phase 2)STEERING.md,run-notes.md,.owloop/learnings.md— good primitives to carry task-source context into iterationssession_latest.json,--resume, per-session worktrees — the building blocks a scheduler needs to recover from interrupted nightsKey structural observation: the loop engine never needs to know about external systems.
specs/is already the interface. External integration should be an airlock around the loop (sync-in before, write-back after), not a modification of the loop itself. This preserves the "fresh context per iteration, state on disk" principle verbatim.Part 2 — How comparable systems do it (research survey, verified 2026-07-06)
specs/*.md,IMPLEMENTATION_PLAN.mdon diskfix-melabel on an issue or@openhands-agentcommentAGENTS.mdstanding instructions@claudementions,schedule:cron with explicit prompt--max-turnsper invocation; scoped app permissions; workflow timeouts--message/--message-file, looped by shell--test-cmd/--lint-cmdgatesSHARED_TASK_NOTES.mdscratchpad--max-costUSD cap,--max-duration; done-signal required 3 iterations in a row; stall threshold → pause + diagnostics for humanCross-framework patterns that matter here
Part 3 — Proposed design (five layers, each independently shippable)
Layer 1 — Task intake:
owloop sync+ pluggableTaskSourceEvolve
spec_from_issue.pyinto aTaskSourceadapter interface (mirroringadapters.pyfor agents):ghalready used), Jira next (stub exists), TAPD/Feishu/linear behind the same interface. A generictodo.mdsource (checklist file in-repo) covers the no-tracker case and is the cheapest to ship.owloop:auto(or checklist tag@auto). On sync, flip toowloop:queued— the trigger is consumed exactly once even if sync runs twice.owloop:needs-specwith a comment listing what's missing (spec linter feat: inject linter rules into spec generation and auto-retry on lint errors #9 +--dry-runcalibration feat: add dry-run / one-shot mode to validate specs without burning tokens #11 as the mechanical check). Ambiguity that survives into a spec becomes an overnight run in the wrong direction — filter it at intake, in daylight, when a human can answer.Layer 2 — Spec as contract: provenance frontmatter
Synced specs carry a machine-readable provenance block:
source_hashmakes re-sync idempotent (same issue → same spec, updated only if upstream changed) and makes divergence detectable: if the upstream issue changed after sync, the spec is flagged stale rather than silently running against yesterday's requirements.specs/.Layer 3 — Night shift:
owloop schedule/owloop nightowloop schedule "02:00" --max-duration 300 --max-tokens 2memits the right artifact per platform: cron/systemd-timer/launchd entry, or a GitHub Actions workflow (schedule:+ claude-code-action-style bounded run) for repo-hosted nights.owloop night= the composed pipelinesync → run → write-back → reportas one command, so the scheduler entry is a single line.--max-iterations/--max-duration/--max-tokens(plus--max-costwhen feat(adapter): pass native hard limits (--max-turns, budget caps) through to claude -p #35/feat: real-time token tracking with per-iteration budget cap #15 land) must be set.exhausted ≠ successper feat(engine): named terminal states + hard stop on stall (stuck detection as a first-class citizen) #33..owloop/— a 2 a.m. cron firing while yesterday's run is still going must no-op loudly, not double-run. Overlap is the feat: separate verifier agent from executor agent #1 source of the "错乱" this issue exists to prevent.owloop checkand have ≥1 runnable acceptance command are eligible; the rest are skipped and reported, not attempted.Layer 4 — Write-back: results flow to where the task came from
Per-spec terminal state (from #33's taxonomy) maps to source-side actions:
owloop:done--pr-per-spec)owloop:stuckowloop:blockedBLOCKED:/DECIDE:payload, verbatim — the morning human's first read<!-- owloop:run-id:spec -->marker so a re-run after a crash updates rather than duplicates.Layer 5 — Morning report
report.py's existing summary/events into a digest: per-spec outcome table, tokens/cost, stuck items with reasons, links to branches/PRs. Deliver via generic webhook (Slack/Feishu/Discord all accept a JSON POST) and/or a pinned "night report" issue comment.BLOCKED/DECIDE/stalled— meta: loop-engineering roadmap — from prompt-level conventions to enforced invariants #37 Phase 2 already lists this; it's required for scheduled mode, since a silently-stopped 2 a.m. loop wastes the whole night.Part 4 — Why this doesn't produce chaos (guarantee map)
source_hashidempotencyneeds-specin daylightPhased plan
Phase I — intake foundations (no scheduler yet, immediately useful interactively)
TaskSourceprotocol; refactorspec_from_issue.pyintoGitHubIssueSourcesource_hashidempotent re-synctodo.mdsource (in-repo checklist with@autotag)needs-specoutcomePhase II — night shift (gated on #32 + #33 + #34)
owloop nightcomposed command; scheduled mode requires boundsowloop scheduleemitting cron/systemd/launchd/GitHub Actions artifactsPhase III — write-back & report
--pr-per-specoptionPhase IV — more sources
TaskSourceNon-goals
References
ghuntley.com/ralph · anthropics/claude-code ralph-wiggum plugin · fstandhartinger/ralph-wiggum · OpenHands resolver docs · GitHub Copilot coding agent docs · jules.google · developers.openai.com/codex/cloud · anthropics/claude-code-action · SWE-agent/mini-swe-agent batch mode · aider.chat/docs/scripting · AnandChowdhary/continuous-claude · steveyegge/beads · MrLesk/Backlog.md · BloopAI/vibe-kanban · github/spec-kit · kiro.dev/docs/specs
Relates to: #32 #33 #34 (prerequisites for Phase II) · #35 #15 (budget caps the scheduler depends on) · #36 (clarify gate reused at intake) · #37 (this is the "integration track" alongside its trust track) · #49 (perf track; parallelism explicitly out of scope here) · #9 #11 (linter/dry-run as the intake gate's mechanical checks)