Skip to content

Make Sandcastle a Blocker-Aware Orchestrator#177

Merged
NandaScott merged 4 commits into
developfrom
sandcastle-setup
May 30, 2026
Merged

Make Sandcastle a Blocker-Aware Orchestrator#177
NandaScott merged 4 commits into
developfrom
sandcastle-setup

Conversation

@NandaScott
Copy link
Copy Markdown
Owner

@NandaScott NandaScott commented May 30, 2026

Description

This replaces the sandcastle simple-loop runner with a blocker-aware issue orchestrator. A run picks up open ready-for-agent issues, works each in an isolated Docker sandbox, opens a PR, and relabels the issue to needs-review.

The initial setup (PR #175) shipped the Python-adapted simple-loop template. This builds on it: the agent now commits only, and the host pushes, opens the PR, and relabels. That split fixes the behavior I did not want from the template, where the agent closed issues before the work was reviewed.

The orchestrator reads the ## Blocked by section of each issue and only dispatches one once every blocker it names is cleared. A blocker is cleared when its issue is closed or when the target branch already carries a commit referencing it (e.g. (#170)). That commit check is what lets PRD branches work: merging a slice into a prd/<slug> branch lands the commit but does not auto-close the issue, since auto-close only fires on the default branch. Issues with a milestone target their prd/<slug> branch; everything else targets develop. This matters for the connector series (#170 to #171 to #172/#173), where a later slice must never branch off a base that lacks the earlier slice's work.

Two correctness fixes came out of the first live run. The agent's gates were missing black, which CI checks first, so #176 opened red even though the agent passed its own checks: the prompt gates now mirror CI exactly. And the host was opening a PR whenever the agent produced any commits, so I gated PR creation on the agent's completion signal (RunResult.completionSignal) so partial work leaves a branch for review instead of a red PR.

Changes:

  • Replaced main.mts with a host orchestrator: list ready-for-agent, fan out one agent per issue, push, open a PR, relabel ready-for-agent to needs-review.
  • Made dispatch respect each issue's ## Blocked by list, clearing a blocker on issue-closed or a (#N) commit on the target branch.
  • Routed milestone issues to a prd/<slug> branch (created from develop on first use) and everything else to develop.
  • Gated PR creation on the agent's completion signal, so a stalled run leaves its branch for review rather than opening a red PR.
  • Rewrote the prompt as a per-issue template whose gates mirror CI: black, ruff check scrython tests, mypy, pytest.
  • Added DRY_RUN=1 (print the plan, spawn nothing) and ONLY_ISSUE=N (single-issue smoke test) controls.
  • Documented the runner in Contributing.md: the label flow, how a run works, setup, and the run commands.

Testing

I replaced the simple-loop runner with a custom orchestrator modeled on the
tubarr setup, adapted to Scrython. A run now lists open `ready-for-agent`
issues, fans out one sandboxed agent per issue, and on the host pushes each
branch, opens a PR, and relabels the issue to `needs-review`. The agent commits
only; it no longer closes issues or pushes, which was closing them before the
work was reviewed.

The orchestrator reads the `## Blocked by` section of each issue and only
dispatches an issue once every blocker it names is cleared. A blocker counts as
cleared when its issue is closed or when the target branch already carries a
commit referencing it (e.g. `(#170)`). That commit check is what lets PRD
branches work, since merging a slice into a `prd/<slug>` branch lands the commit
but does not auto-close the issue. Issues with a milestone target their
`prd/<slug>` branch (created from develop on first use); everything else targets
develop.

DRY_RUN=1 prints the dispatch plan and the blocked list without spawning
anything, and ONLY_ISSUE=N restricts a run to a single issue for smoke tests.
The prompt is now a per-issue template with ruff, mypy, and pytest gates.
The prompt gates omitted black, but CI runs black --check first and fails the whole run before pytest. #176 opened red for exactly this. Gates now mirror tests.yml: black, ruff check scrython tests, mypy, pytest.
run() returns RunResult.completionSignal: the matched signal, or undefined if the agent never signaled before the iteration limit. Open a PR only when the agent both committed and signaled completion; otherwise leave the branch for review instead of opening a red PR on partial work.
Adds a section covering the ready-for-agent to needs-review label flow, how a run works (blocker-aware dispatch, milestone PRD branches, CI-matching gates), one-time setup, and the run commands, so agent-opened PRs and the label flow are not a mystery to contributors.
@NandaScott NandaScott merged commit 1a7f169 into develop May 30, 2026
7 checks passed
@NandaScott NandaScott deleted the sandcastle-setup branch May 30, 2026 03:00
@NandaScott NandaScott restored the sandcastle-setup branch May 30, 2026 03:14
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.

1 participant