RSS Amplifier

Roland Rodriguez · Jul 27, 2026

Signalbox: Structural Trust for AI Development Workflows

0
Sign in to vote or save

Roland Rodriguez · rodriguez.today

Prompt-driven development workflows ask models to enforce rules they can also forget, reinterpret, or talk around. The usual response is to add more instructions: remember the review verdict, stop after four rounds, never merge an unapproved change, wait for every parallel worker. Each sentence describes behavior I want, but none of them makes the unwanted behavior impossible.

I built signalbox around a stricter standard. Plan, implement, review, and promote are an event topology. Models do the work that requires judgment. The workflow engine, shell scripts, and Unix primitives enforce the transitions around them. I call the distinction instructed trust versus structural trust. Instructed trust says a model has been told what not to do. Structural trust says there is no route by which it can do it.

The name comes from railway signaling. A signal box contains the interlocking that connects levers, switches, and signals. A signalman cannot clear two conflicting movements because the mechanism will not permit that combination. The safety property does not depend on remembering a paragraph in the operating manual at the right moment. I wanted the same property at the seams of an AI development workflow.

What I kept from TRIP

Signalbox descends from TRIP by PiLastDigit. TRIP put clean names and discipline around a loop I was already running by hand: plan, implement, review, promote, with verdict sentinels and a human gate. My business partner pointed me at the repository. I ran it for a few days, kept what it got right, and rebuilt the loop on Emergent.

The parts that survive are substantial. Signalbox keeps the sentinels, the review and fix bounce, local-only tooling under .claude, the architectural memory in ARCHI.md, ARCHI-rules.md, and TESTING.md, and the rule that release remains the human's step. The translation is mechanical: sentinels become event topics, the bounce becomes a feedback cycle, and the round cap becomes a guard in the wiring.

I rebuilt instead of sending a pull request upstream because the implementation reflects choices specific to how I work. TRIP aims to be broadly adaptable. I wanted fixed commitments about which models occupy which roles, Linux facilities such as symlinks and flock, an Obsidian vault as project memory, and Emergent's event store as the record of the run. Those are product decisions, not general improvements to somebody else's workflow.

Emergent is my event-driven workflow engine, which I wrote about back in March. A TOML topology connects processes by topics, routes every event through the engine, and records the traffic in an event store.

The missing routes are the guarantee

Consider the review gate. A reviewer publishes a raw verdict. Two small jq handlers split that event into review.approved or review.changes_requested. An approved review goes to an assessor, then to a gate that compares earned readiness with the assessed floor. Only two routes publish approval.granted: the automatic gate and the human webhook. Promotion and vault synchronization subscribe to that topic.

What matters is what the topology does not contain. There is no subscription from review.raw to promotion. There is no fallback from approval.requested to promotion. The human approval path re-enters the same fabric by publishing approval.granted; it does not set a side-channel flag that downstream code might or might not check.

review.raw -> review.approved -> gate.assessed -> gate.decided
                                                      |
                              auto -------------------+-> approval.granted
                              human -> approval.requested
                                         |
                                         +-> approval webhook -> approval.granted
approval.granted -> promoter
approval.granted -> docs sync

This is the interlocking. A prompt can insist that approval is mandatory, but the same model still owns the next tool call. In signalbox, nothing downstream subscribes around the gate. A persuasive model has nobody to persuade.

The round cap works the same way. review.changes_requested reaches the fixer only through a jq guard that selects rounds below four. At round four, a separate guard publishes review.escalated. “Do not try again after four rounds” is no longer advice buried in the fixer's context. The fifth trip around the loop has no edge.

Planning separates judgment from mechanics just as deliberately. Claude reads the issue, the source tree, and the vault, then proposes a staged shard plan. A pure jq validator checks the schema, the feature slug, substantive prompts, declared files, and file disjointness within each stage. If two concurrent shards both claim the same file, the plan is invalid. Only plan.approved reaches the writer, so an overlapping plan never becomes the plan.json that creates worktrees.

That validator is intentionally dull. Asking another model whether two arrays of file paths overlap would spend inference on a question with an exact answer. Worse, it would turn a property into an opinion. Signalbox uses models where the answer requires interpretation and ordinary programs where it does not.

Concurrency needs a physical shape

A single chat session can describe parallel work, but it cannot be in two worktrees at once. Signalbox can. The plan groups work into sequential stages and conflict-free shards. Within a stage, workers receive the same event, take different slices, and build in separate Git worktrees from the same integration tip. The next stage remains held until the current one acknowledges completion.

Each shard then enters its own review and fix loop. Only the approved splitter moves a shard from the pending queue into the done set. The collector subscribes to shard.done, a topic that is reached only after every shard in that worker slice has been approved. It accumulates arrivals under flock and emits nothing until the expected count is present. The merger then rebases and fast-forward merges the branches before acknowledging the next stage.

The distinction between a fan-in barrier and a prompt saying “wait for all workers” is the distinction between state and recollection. The barrier counts arrivals in a locked file. Empty stdout means no event, so an incomplete stage literally produces no stage.done. There is no coordinator model keeping a mental checklist while also trying to reason about code.

The outer pipeline applies the same discipline between plan, implement, review, and promotion. A phase runner launches each child engine and watches fresh disk artifacts for its terminal condition. It does not trust buffered narration in a log. At the seam, a Fable operator independently inspects the artifacts and emits PROCEED or HALT. Only PROCEED feeds the next phase request.

I also fixed the model assignments instead of letting one general agent own the entire loop. Codex implements and reviews deltas in separate sessions. Headless Claude plans, fixes, and assesses. Fable judges phase seams and promotion. The role and vendor boundaries are deliberate: the model instance that produces a change does not get to certify its own account of that change. The operator verifies commits, declared file scope, gate artifacts, and review output from disk before the topology advances.

Trust should be specific and perishable

Permanent human approval does not create safety. It creates a rubber stamp and a queue. Permanent autonomy has the opposite defect: it grants trust before there is evidence for it.

I carried an R1 through R4 situational-delegation ladder from my TRIP fork into signalbox. It moves from directing and coaching toward supporting and delegating as readiness rises. Readiness is earned per action. A track record promoting a local review artifact says nothing about readiness to push a shared remote. Clean convergence can move an action up one level; escalation moves it down. Levels above the R2 default decay after idle time, so old evidence does not become permanent authority.

The required floor is also assessed per action, per run. assess.sh gives an LLM the mechanics of the requested action, its reversibility and visibility, the live change scope, the review round, and the reviewer's closing summary. The model returns a floor and rationale. There is no hand-authored table declaring every push to be R3 forever. If the assessment cannot be parsed, the floor becomes R4 and a human decides.

After that judgment, the gate is ordinary jq: compare readiness with floor, then publish either approval.granted or approval.requested. This is an important division of labor. The model decides how much readiness the situation demands. It does not decide whether the measured readiness satisfies its own decision.

Memory that survives the chat

Every run receives a correlation ID that travels in the event payload through planning, shards, reviews, fixes, gates, and merges. Emergent already stores the routed events, so bin/audit.sh can reconstruct a feature across the child engines with one filter. The result is a timestamped trail of topics, sources, rounds, verdicts, decisions, and merge tips. It is evidence about the run, not a transcript curated by the agent that performed it.

The architectural memory has a similar shape. .claude/docs is a symlink into my Obsidian vault, and every worktree sees the same ARCHI.md, ARCHI-rules.md, and TESTING.md. Initialization researches those documents. Each approved feature then runs a documentation sync against its actual diff. The sync records an explicit updated, unchanged, or error result, and promotion blocks until fresh, correlation-matched evidence exists. A no-op is recorded as a no-op rather than inferred from silence.

This matters because the next feature's planner reads those pages before it writes a plan. Architectural memory that waits for a release retrospective is already late. The vault makes maintaining the system's shared model part of the feature path, while the topology makes the maintenance result a checked precondition rather than a polite reminder.

The track diagram above the levers

Every transition in signalbox is already an event on the rails, so a live dashboard costs almost nothing to add. I built one as another sink. bin/sink-serve.sh is a single file: a bash script that execs an embedded Python HTTP server, roughly 1,600 lines including the one HTML page it serves. There is no framework, no build step, and no dependency beyond python3. One systemd user unit, signalbox-sink.service, owns the board for the entire machine on a fixed port, 8099. Installing signalbox into a second repository refreshes that canonical copy instead of starting a competitor, so every repository's runs land on the same board.

The topologies carry small forwarder sinks, one bin/sse-forward.sh per interesting topic. Each takes an event payload, wraps it in an envelope stamping the repository, run slug, issue, engine name, PID, exact process start identity, and correlation ID, then POSTs it to the sink's /ingest route. Delivery is fire-and-forget by contract. A slow sink, a dead sink, or no sink at all never stalls a phase and never fails one. That is the missing-edge property pointed the other way: observability subscribes to the interlocking and has no route back into it. Identity riding inside the envelope also retires a class of attribution bug, because there is no separately polled owner table that can pair an event with the wrong run.

The page draws each run as a railway block diagram. Four track sections, plan, implement, review, and promote, carry signal lamps: green for done, amber for active, red for failed or escalated, violet for parked awaiting a human. Inside the implement block, sidings show one lamp per planned shard with barrier bars between stages, which puts the fan-out and fan-in points on screen where a log buries them. plan.json names every shard up front, so a shard that has produced no event yet renders hollow rather than absent. Review rounds surface on the track as R2 and R3.

I grouped the board by claim on the operator's attention rather than by time. An Attention section, holding halts and parks, pins to the top, then Running, then Stopped. The sort order is the design: the page answers what needs me before it answers what is happening. A sticky right rail, the register, carries the live event log as an SSE stream with a 500-event replay, filter chips per repository and issue, and verbatim payloads, including which agent and model produced each phase verdict.

The dashboard renders claims made by events, and it stats the artifacts those claims are about. Its /status route reads the same plan.json, gate.json, and CR.md on disk that I read when I supervise a run, so the board and the operator work from one set of evidence. A prompt-driven workflow cannot buy this at the same price. When the loop lives inside one chat session, the only available observer is the agent's own narration, and asking for a status report asks the thing being watched to describe itself. Signalbox routes the state through an engine already, so watching is a matter of attaching one more subscriber.

Signalbox is still opinionated local tooling. It assumes Linux, Git worktrees, command-line models, and a willingness to make workflow policy visible in TOML and shell. That is exactly the point. I would rather expose the interlocking and defend its constraints than hide safety inside a larger prompt.

The code and topologies are at github.com/rrrodzilla/signalbox. Look at the missing edges first.

Read the original on rodriguez.today

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.