Skip to content

bench(lab): turn the loop - #300

Merged
luuuc merged 6 commits into
mainfrom
lab/09-03-the-crank
Aug 19, 2026
Merged

bench(lab): turn the loop#300
luuuc merged 6 commits into
mainfrom
lab/09-03-the-crank

Conversation

@luuuc

@luuuc luuuc commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Stacked on #299, which is stacked on #298. Merge in order; this PR's diff is only its own changes.

Problem

Nothing in the binary spawns a phase agent. internal/loop has no importer, the eleven files in lab/plans/ are opened by a human, and the routing rules cycle 05 built and tested are exercised in production by an operator remembering them.

That operator is the leak. Six authoring cycles on one repository is roughly thirty hand-spawns, each one an opportunity to skip the plan, read only the latest rejection, or advance on an agent that produced a confident summary and no artifact. "The authoring cycle is unattended and bounded" is a law, and it currently describes an intention.

Summary

Advance takes a context and a repository id, and nothing else. It reads a position, runs the one phase that comes next, guards what came back, records it and returns. -until is a loop in the caller around that call, so no mode is threaded through the dispatcher.

A phase reports its own verdict, which makes every phase an unreliable narrator about itself. Four things about identity are checked against the job that was dispatched and the plan that was declared — the verdict names this phase, this repository and this cycle, and carries a verdict the plan declares — and then the one thing that is not the phase talking: the artifact it owed is on disk, or nothing advances.

Two numbers left Go. A phase's wall is declared beside the plan its agent runs, because a wall held in a variable is the number that gets raised in a hurry to rescue a stalled phase. And a verdict is now a document a program can read, rather than prose inside an artifact.

The crank stops at the pay call. PAY prints the exact command and exits waiting on a human; bench, report, harvest and board stay hand-run, because an unattended loop that can reach a paid cell is a different product with a different risk.

Changes

  • lab/internal/crank: the dispatcher, the verdict document and the guard
  • lab/internal/cli/crankcmd.go: the real spawner, at the edge — a disposable environment carrying no more than a run arm's credential, the agent spawned in the repository under study under its declared wall
  • lab/internal/plans: a wall: key in the plan header; each plan gains the verdict document it owes
  • lab/internal/position: an attempt records how it ended when it produced no verdict, and where it sits in its cycle
  • .golangci.yml: the deciding packages may not import os/exec, enforced by the linter rather than by review

A defect this found

Ordered by the graph, a re-entered author sorted before the mini-bench that sent it back, so the position routed from the rejection on every turn and -until would have re-run the same phase forever, at whatever an agent costs. Attempts now record their place in the cycle, and both the ordering and the turn after a re-entry are asserted.

Test plan

  • make ci green: coverage floor with no new exception, zero complexity suppressions, lint clean
  • new files hold 95.9–100% line coverage
  • the whole crank is exercised with a fake spawner: every lever the crank can reach, the ceiling, a PAY, a stalled phase, a refused verdict and a missing artifact — no agent, no network, no spend
  • the levers past the pay call are asserted to be refused rather than skipped, against a list written down in the test rather than read from the one the crank uses
  • end to end through a real subprocess: a stand-in agent walks author → mini-bench → expand → preflight → validate and stops at the pay call; the credential that reached it is asserted on the document it was handed; an agent that sleeps past a one-second declared wall is recorded as out of clock; an agent that writes no artifact does not advance
  • seven mutations that the first draft of the tests let through are now killed, each verified by making the mutation and watching a named test fail
  • nothing in the run path reads a terminal, checked over the source

luuuc added 6 commits August 19, 2026 09:37
…icts

A repository's position lived in the operator's head. The old driver held it in
a JSON map of repository to phase, edited by whoever was debugging, which made
it the one number in the system with no evidence behind it.

It is now a function of two inputs, and which input a fact comes from is the
whole design. The run tree holds which artifacts exist and the authoring cycle,
which is already a directory name: those are derived every time and never
stored, so there is no counter to drift from the thing it counts. The attempt
records hold the verdict, the table that rejected it and the anchor: a judgment
is not recoverable by reading a tree, so it is recorded once and never
rewritten.

An attempt is one file, written with O_EXCL. Append-only as a convention
survives until a bad night; append-only as an open flag survives the bad night.

Out of the two comes a standing. A park and a PAY are the two most worth telling
apart — one is the method failing on this repository, the other is the method
succeeding and waiting on a wallet — and so are the two refusals: a verdict its
phase cannot emit is an agent that misbehaved, and a usable verdict with no
artifact is an agent that died.

The fixture tree is copied from the jellyfin campaign's layout and carries one
repository of each shape it takes.
`sense-lab repo <id>` now prints where the repository stands and exits with the
code that carries it. The codes are this command's API rather than a display,
because it belongs in a loop:

    while sense-lab repo <id>; do :; done

That loop stops on a park, on a PAY and on either refusal instead of spinning on
any of them, and `-show` never returns 0 so it cannot be dropped into it.

`-show` stops at the announcement, which is the point before an admission writes
anything, so it is a preview of what is about to happen rather than a second
reading of the same inputs.
A phase's wall was nowhere, and its verdict was prose inside its artifact, which
is why a parked campaign could be read by a person and by nothing else.

The wall is declared beside the plan the agent runs rather than as a constant in
Go. A wall held in a variable is the number that gets raised in a hurry at 3am
to rescue a stalled phase, and that is what CANNOT-FINISH-AT-BUDGET IS A RESULT
forbids. Declared in a file, changing it is an edit somebody reviews.

Each plan gains the document it owes: the phase, the repository, the cycle, the
verdict and the table that carries it. Nothing else in them changes.
Two additions the crank needs, both facts nothing can re-derive.

An attempt that produced no verdict says how it ended. An agent that ran past
its wall and an agent whose output is not a verdict are diagnosed differently —
one died, the other misbehaved — so they are separate outcomes and separate
exit codes. Without them a phase that stalled leaves no record, the position is
unchanged, and an unattended loop runs that phase again, forever.

And every attempt records where it sits in its cycle. A cycle is not a walk
down the graph: a re-entry sends work back, so cycle 1 can hold an author, a
mini-bench that re-questions it, and the author again. Ordered by the graph the
mini-bench sorts last, the position routes from the verdict that sent work back
rather than from the attempt that answered it, and the loop re-runs the author
on every turn. Measured against the crank before the order was recorded.
Nothing in the binary spawned a phase agent. The eleven plans were opened by a
human, and the routing rules cycle 05 built and tested were exercised in
production by an operator remembering them. That operator is the leak: six
authoring cycles on one repository is roughly thirty hand-spawns, each one a
chance to skip the plan, read only the latest rejection, or advance on an agent
that produced a confident summary and no artifact.

Advance takes a context and a repository id, and nothing else. It reads a
position, runs the one phase that comes next, guards what came back and records
it. Everything a phase needs is resolved from the declared graph and the config,
because an options struct handed in as six arguments is the same options struct.

A phase reports its own verdict, which makes every phase an unreliable narrator
about itself. So four things about identity are checked, and then the one that
is not the phase talking: the artifact it owed is on disk, or nothing advances.

The deciding layer may not spawn. The spawner is a value the edge hands in, and
depguard holds that rather than review.
`sense-lab repo <id>` now runs the phase the position owes, and `-until` cranks
until it stops on its own. The loop is in the caller, so the dispatcher never
knows which of the two it is in.

A phase agent is spawned in the repository under study, under the wall its plan
declares, inside a disposable environment carrying no more than a run arm's
credential and reaching no keychain of its own.

It stops at the pay call: `PAY` prints the exact command and exits waiting on a
human. bench, report, harvest and board stay hand-run, because an unattended
loop that can reach a paid cell is a different product with a different risk.
@luuuc
luuuc changed the base branch from lab/09-02-where-a-repository-stands to main August 19, 2026 16:40
@luuuc
luuuc merged commit 14a1fb3 into main Aug 19, 2026
@luuuc
luuuc deleted the lab/09-03-the-crank branch August 19, 2026 16:41
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