Skip to content

Agentic loops: Ralph worker + manager, two-terminal pane, Dash-driven scheduler - #179

Open
nthomsencph wants to merge 10 commits into
mainfrom
claude/agentic-loops-dash-ka6xwn
Open

nthomsencph wants to merge 10 commits into
mainfrom
claude/agentic-loops-dash-ka6xwn

Conversation

@nthomsencph

Copy link
Copy Markdown
Collaborator

What this is

First-class support for agentic loops in Dash. A task can be a loop, which runs two terminals side-by-side in the main pane — a Ralph worker that iterates on a goal (fresh context every pass) and a persistent manager that orchestrates — driven by a Dash-owned scheduler. Conceptual base: loop-engineering (role separation + durable state spine) × the Ralph loop (fresh context each iteration, completion judged by an external check, never self-grading).

Design + rationale live in docs/agentic-loops-plan.md.

Locked decisions baked into the code

  • Dash owns the iteration while (not the CLI's /loop//ralph-loop) so budget gates, the stop check, and pause/kill are clean control points.
  • Worker resets context every pass; manager persists. Durable memory lives in .dash/loop/ files, not the conversation.
  • Completion is an external check — the scheduler only advances on busy→idle and runs the stop predicate itself.
  • Manager model ≥ worker (evaluation is at least as hard as generation); a weaker-manager config is flagged.
  • Manager runs unprompted but write-denied via claude --settings (Write/Edit/MultiEdit/NotebookEdit + git-write Bash) — default permission would wrongly block its own reads/greps.

What's landed (this PR)

  • Data foundation — taskKind + loopConfig across shared types, Drizzle schema, guarded migration, DatabaseService (+duplicateTask).
  • State spine — LoopService seeds/reads .dash/loop/ (PROMPT/LOOP/STATE/loop-constraints/loop-run-log/manager-notes).
  • Fresh-context spawn — ptyManager opt-in freshContext (no --resume), --model, merged --settings, distinct taskId for loop:/mgr: ids; threaded through the renderer spawn path.
  • Scheduler core — LoopScheduler, a dependency-injected state machine (per-policy stop check, maxIterations, cadence waits, token-budget auto-pause, pause/resume/stop) + ActivityMonitor.subscribe().
  • Per-agent policy — loopSpawn.ts centralizes worker/manager model/permission/prompt/deny-settings in main; the renderer only passes loopRole.
  • Two-terminal split pane — LoopTerminalPane; MainContent branches on taskKind; standard tasks untouched.

Testing

  • pnpm type-check clean (both processes).
  • 18 unit tests pass — LoopScheduler (12, incl. a pause/stop-races-the-check regression and the cadence-runaway guard) and loopSpawn (6).
  • A self-review (/code-review) found and fixed two concurrency/runaway bugs in the scheduler.

Not in this PR (needs a machine that can run the Electron app to verify)

  • Scheduler↔PTY adapter (LoopController) + loop:start/pause/resume/stop IPC.
  • Loop MCP bridge (loop_status/steer/pause/kill/escalate/...) + injecting the manager deny-settings/hook at spawn.
  • TaskModal loop mode + duplicate-as-loop UI.

Verification caveat: the --model and --settings deny-list args follow Claude Code's documented flags but haven't been exercised end-to-end here — confirming the manager's write-deny actually blocks edits is the first check when wiring the MCP bridge. Until the creation modal lands, a loop task can be created by setting task_kind='loop' on a row to exercise the split pane.

🤖 Generated with Claude Code


Generated by Claude Code

Design doc for first-class agentic loops: Ralph-primary loop core
(Dash-owned scheduler, fresh context per iteration, external verification),
two-terminal worker+manager model with an MCP control bridge, durable state
spine, loop policies (ralph/goal/cadence/count), and a file-by-file change list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmYjhqiW9Wxb9wXRpNzFbc
Introduce the `loop` task kind and its LoopConfig (policy/level/goal/budget/
constraints) across shared types, the Drizzle schema, a guarded ALTER migration,
and DatabaseService (map/persist as INSERT-only deep config + a duplicateTask
helper for "duplicate as loop"). Type-checks clean; no behavior change for
existing standard tasks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmYjhqiW9Wxb9wXRpNzFbc
Seeds and reads the durable loop files under <worktree>/.dash/loop/
(PROMPT/LOOP/STATE/loop-constraints/loop-run-log). Derived files refresh from
config; evolving STATE/run-log are preserved across re-seeds. Templates encode
loop-engineering practice: role separation, external verification over
self-grading, one-task-per-iteration, constraints injected each cycle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmYjhqiW9Wxb9wXRpNzFbc
Add LoopScheduler — the dependency-injected state machine that owns the Ralph
iteration while-loop (per-policy stop check, maxIterations, cadence waits, token
budget auto-pause, pause/resume/stop). Only busy→idle edges advance the loop, so
completion is judged by an external check, never self-grading. ptyManager gains
an opt-in fresh-context spawn (no --resume) plus a distinct taskId for composite
loop:/mgr: PTY ids; ActivityMonitor gains an in-process subscribe() the scheduler
watches. 10 unit tests cover the policy/iteration logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmYjhqiW9Wxb9wXRpNzFbc
Render a horizontal worker|manager split for loop tasks (LoopTerminalPane),
branched in MainContent on taskKind. Both terminals spawn fresh-context with the
real task id threaded through TerminalPane → SessionRegistry → session manager →
ptyStartDirect IPC, so their Claude sessions don't collide on the shared worktree
cwd; each seeds from the on-disk .dash/loop spine. projectsStore disposes the
loop:/mgr: PTYs on task close. Standard tasks are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmYjhqiW9Wxb9wXRpNzFbc
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmYjhqiW9Wxb9wXRpNzFbc
Re-check run state after the awaited stop check in onIterationComplete: a
pause()/stop() that lands while the check is in flight no longer gets overridden
by spawning the next iteration. A cadence loop with no positive interval now
pauses with a clear reason instead of busy-spawning workers with zero delay.
Two regression tests added (12 total).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmYjhqiW9Wxb9wXRpNzFbc
Execute the locked agent-settings decisions:
- LoopConfig gains per-agent worker/manager config (model/effort/permission),
  specRefs (PRD/spec files), verifier, and subAgentCap.
- loopSpawn.ts centralises per-role spawn policy in main: manager model defaults
  to the worker's tier (managerWeakerThanWorker flags the anti-pattern), worker
  permission follows the L1/L2/L3 level, and the manager runs unprompted with a
  write-deny policy (MANAGER_DENY) passed via `claude --settings` — the right
  enforcement since `default` would also block the manager's reads/greps.
- buildClaudeArgs/startDirectPty learn --model and merged --settings; the renderer
  threads only loopRole and main derives the rest (it can't be spoofed).
- LoopService adds the manager-notes steer file (worker reads it each pass) and a
  canonical managerPrompt, removing the duplicated renderer seed prompts.
- 6 loopSpawn unit tests; plan doc gains the authority/model/worktree section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FmYjhqiW9Wxb9wXRpNzFbc
@nthomsencph
nthomsencph force-pushed the claude/agentic-loops-dash-ka6xwn branch from ba6b105 to 971a7e3 Compare September 9, 2026 07:56
Claude Code shipped a family of parallelism surfaces since this plan was
written — subagents, agent view, agent teams, dynamic workflows. Agent teams
is the one that superficially matches our manager/worker split, so §1 now
records why it is rejected as the loop engine: coordination lives inside the
lead's process (no insertion point for the scheduler tick, budget gate or
pause/kill), there is no fresh-context reset, teammate permissions are
inherited and cannot be set per-teammate at spawn, and teammates render into
the lead's agent panel or tmux rather than a PTY Dash can attach to.

Also notes what is worth adopting from the newer CLI instead (`--agents`,
`--permission-mode auto`, teams inside the manager session post-MVP) and the
flags the branch's spawn path depends on, verified present at v2.1.240.

There is no Claude Code feature called "workstreams" — that is Cowork
vocabulary — and the section says so, to stop the question being re-asked.

Claude goes brr.. via Dash
Claude-Session: https://claude.ai/code/session_017gtmbWYyAsYEBiYQDnf4vy
Completes the runtime wiring the plan listed as remaining (§6–§8), leaving
only duplicate-as-loop.

LoopController is the single owner of both agents. It implements LoopDriver
over ptyManager (startDirectPty / killPtyAwait plus a `loop:` output tap for
the count policy), child_process for the stop check, LoopService for the
state spine and run log, and webContents for status. The renderer's loop
panes are display-only: they reattach via attachOnly and never spawn, so a
mount or reload cannot race the scheduler into a second Claude. The manager
is spawned once and kept alive; the worker is reset each pass.

The loop MCP bridge is a per-task, stateless streamable-HTTP server on the
existing HookServer (/mcp/loop?taskId=), attached to the manager only via an
inline --mcp-config at spawn so it is trusted without an approval prompt.
Control tools are level-gated — at L1 pause and kill stay human.

TaskModal gains a Task | Loop toggle; Loop mode collects goal, policy, level,
stop check, max iterations, cadence, token budget and constraints, forces a
new-branch worktree, and creates the task with taskKind='loop'. The spine is
seeded lazily on first Start, so creation needs no seed step.

Carries the SpawnModel reconciliation from the rebase onto main: the task
picker keeps the narrow TaskModel alias set while the PTY boundary accepts
any id `claude --model` takes, which is what loop agents need.

Claude goes brr.. via Dash
Claude-Session: https://claude.ai/code/session_017gtmbWYyAsYEBiYQDnf4vy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants