Skip to content

Latest commit

 

History

History
217 lines (160 loc) · 8.71 KB

File metadata and controls

217 lines (160 loc) · 8.71 KB

Architecture

How star-loop works, and why it is built this way.

The shape of the problem

Huntley's Ralph is a shell loop:

while :; do cat PROMPT.md | agent; done

Everything important is in what it doesn't do. It does not carry context forward — each run starts clean. It does not track state in memory — the filesystem is the state. It does not decide when to stop — you do, or the prompt does.

That works because a fresh agent reading a half-finished repository is in a better position than a tired agent reading its own 200k-token context. Context rot is real; git is not lossy.

Why a workflow and not a Stop hook

Anthropic's plugin implements Ralph with a Stop hook: intercept the agent's attempt to end its turn, block it, and feed the same prompt back. Elegant, and it keeps everything in one session.

It does not port to Grok. From Grok's hooks documentation:

After 8 continuations (blocks or non-error feedback) in one turn the gate is overridden and the turn ends; hooks are not consulted for that final, forced stop.

Eight is not a loop; it is a stub. A 20-iteration run would need the user to send a message every eight iterations.

Grok's workflow engine is the primitive that actually matches the bash loop:

Bash loop Grok workflow
while :; for step in 1..101 with a cap
a new process per iteration agent() — a fresh agent, fresh context
Ctrl-C /workflow stop, pause, resume
your wallet agent_budget, checked every iteration
exit status a structured output_schema

So the engine is a .rhai workflow, and it needs no runtime beyond Grok itself. That is also why there is no bash, jq, perl, Python or Node anywhere in the shipped plugin.

Phases

Recon   →   Iterate  ⇄  Verify   →   Report
(once)      (n times)  (on claims)    (once)

Recon

One read-only agent, before anything is written. It reports:

  • git state: is this a repo, current branch, default branch, dirty tree
  • the project's real quality gates, found by reading package.json, Makefile, justfile, pyproject.toml, Cargo.toml, CI workflows and CONTRIBUTING — not guessed from the file extensions present
  • PRD validity and story counts, in PRD mode
  • an execution plan and the biggest visible risk

Two things fall out of this. First, safety gates can refuse to start on main or on a dirty tree. Second, the gates are discovered once and handed to every iteration, instead of twenty fresh agents each guessing a toolchain and some of them guessing differently. That inconsistency is a real source of phantom failures in naive loops.

--dry-run stops here and prints the plan.

Iterate

Each iteration is one agent() call with a fresh context. It receives:

  • the same task (prompt mode) or the backlog (PRD mode)
  • the gates recon found, as exact commands
  • the safety rules, as an explicit "Never" list
  • verbatim findings from the verifier, if the last claim was rejected

Its memory is the filesystem: the working tree, git log, .star/journal.md, AGENTS.md, and prd.json.

In prompt mode it maintains .star/plan.md — an ordered checklist it created itself on iteration 1 — and takes the first unticked item. This is the part naive prompt-mode Ralphs miss: re-reading an unchanged prompt gives an agent no way to know what has already been tried, so it re-decides the strategy every time and thrashes. A plan file makes the strategy persistent, which is what the fix_plan.md in Huntley's own setup is for.

In PRD mode it takes the lowest-priority story with passes: false.

Verify

This is the part that does not exist in other implementations.

The implementer never gets to mark its own work as passed. When it claims completion — a finished story, or the whole task — a separate agent runs with no memory of the implementation and an explicitly adversarial brief: try to prove the claim wrong.

It re-runs every gate itself, reads git show HEAD, and checks for the specific patterns by which a loop fakes success:

  • tests deleted, renamed away, or marked skip/xfail/xit
  • assertions removed, loosened, or made tautological
  • snapshots regenerated instead of the bug being fixed
  • a gate that "passed" because it matched no files or exited early
  • stubs, mocks or hard-coded returns standing in for behaviour
  • the feature implemented only in the test file
  • --no-verify, --force, or weakened gate configuration

Failing to run a gate is a fail with that stated as the reason — never a pass by default.

A rejection is fed back verbatim into the next iteration's prompt and counted toward the stall streak. The implementer is told up front that this check exists, which is the point: it makes an honest complete=false the better move even for an agent optimising only for its own scorecard.

--verify every checks every iteration rather than only claims. --verify off disables it, and is a bad idea in an unattended run.

Report

A markdown report with the stop reason, iterations run, units accepted, claims rejected, remaining work, the gates, and the recon plan.

Stall detection

Naive loops detect being stuck by comparing the agent's summary text between iterations. That fails, because prose varies freely while nothing happens — an agent that rewords "still working on the auth tests" five different ways looks like five iterations of progress.

star-loop judges progress on the repository:

moved = (HEAD changed) or (files_changed > 0) or (a story passed)

HEAD comes from git rev-parse --short HEAD, reported by the agent at the end of its iteration. Summary fingerprinting is kept, but only as a secondary signal: an iteration that both fails to move the repo and repeats itself is counted a stall.

After --stall-after consecutive stalls the loop pauses (default), stops, or continues. Pausing is the default because a stalled loop nearly always means a story is too big or a gate is misconfigured, and both are cheap to fix by hand and expensive to grind against.

Safety model

An autonomous committer needs boundaries that are defaults rather than options:

Boundary Enforced by
Not on a default branch Recon reports the branch; the loop pauses before iteration 1
Not on a dirty tree Same
No git push Prompt "Never" list, unless --allow-push
No --force, ever Prompt "Never" list, no override
No history rewriting Prompt "Never" list
No secrets Prompt "Never" list, plus a verifier check
Protected paths Prompt, plus a verifier check

Prompt-level rules are not airtight — nothing at this layer is — which is why the verifier independently checks the ones that matter after the fact, and why branch isolation is enforced before any agent runs at all. The branch is the real containment: everything the loop does is on it, and reviewing or discarding it is one command.

/cancel-star deliberately never reverts, resets, stashes or cleans. Stopping a loop and destroying its work are separate decisions, and the second one is the user's.

Budget

Grok caps total child agents per run via agent_budget. /star-loop requests (max_iterations * 2) + 4 — doubled for the verifier, plus recon and headroom.

The loop keeps one slot in reserve and stops when remaining <= reserve, so it can never exhaust the budget partway through verifying a completion claim. An unverified claim is worse than one fewer iteration.

Rhai notes

The engine is written against a few constraints worth knowing:

  • Functions cannot capture outer scope. All configuration travels in a ctx map passed as a parameter.
  • trim() mutates in place and returns unit: raw.trim(); raw.
  • A missing map key returns (). Agent output is read through s_of, b_of and i_of so a missing or wrong-typed field degrades rather than aborting a run mid-flight.
  • Schemas stay primitive — flat objects of string, boolean and integer. No arrays, no enums, no nesting; lists travel as newline-joined strings. Structured output is markedly more reliable this way across models, and a schema failure mid-run costs an entire iteration.

What this does not solve

Worth being clear about:

  • The verifier is another language model. It is a genuine independent check and it catches the common failure modes it is told to look for; it is not a proof, and a sufficiently subtle wrong implementation can pass it. It raises the floor considerably. It does not replace review.
  • A repository with no automated gates cannot be verified by anything. The loop says so and runs on inspection alone, which is much weaker.
  • Nothing here helps with work that needs judgement. A loop grinds toward a checkable target; if the target is a matter of taste, there is nothing to grind toward.