Skip to content

fit-for-purpose: integration, first run, outcome taxonomy, resumable attempts, budgets, holds, telemetry - #145

Open
thedancingdeveloper wants to merge 61 commits into
mainfrom
fix/validator-rejects-valid-patches
Open

fit-for-purpose: integration, first run, outcome taxonomy, resumable attempts, budgets, holds, telemetry#145
thedancingdeveloper wants to merge 61 commits into
mainfrom
fix/validator-rejects-valid-patches

Conversation

@thedancingdeveloper

@thedancingdeveloper thedancingdeveloper commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Opened as a three-part fix to the patch validator. It has since become the branch the fit-for-purpose programme landed on, and now carries stages F, D, K, H, L, J and M as well. The original validator fix is unchanged and is described at the bottom.

1063 tests, ruff check, ruff format --check and full-project strict mypy all clean.

Full context: docs/evidence/2026-08-04-programme-report.md — the §11 report over every delivered stage. Each stage has its own report beside it.


What this contains

Stage What landed
F integration Merged the four unmerged stage branches (B → G → C → E1) and verified them together for the first time. One semantic conflict a clean auto-merge hid: B and G stated the same generic-core rule and only one encoded it as a test. Resolved by making G's resolver lookup use B's entry-point door. backlog.json renamed to backlog-seed-2026-08-02.json; #146 closed.
D first run agent-harness init --demorun --demo takes an item plan→implement→apply→checks→commit→review with no credentials and no network. agent-harness doctor reports what a real run would need and contacts nothing. Local-provider path documented.
K outcome taxonomy Checks.run() returned one bit; it now returns one of five typed outcomes. An attempt carries a disposition and a reason kind, so a reviewer rejection and a crashed worker stopped being the same failed.
H resumable attempts Six durable stages per attempt; a killed worker resumes at the last one instead of re-paying for the planner and implementer — 3 model calls against 5 on the measured fixture. Three durability modes.
L item budgets Per-item wall-clock and spend ceilings, stopping at a boundary, never parking an endpoint, defaults unlimited.
J durable hold held as a real state: survives worker death, answerable from any process, expires to blocked with the question preserved.
M telemetry export Events projected to OpenTelemetry spans — opt-in, lazily loaded, export only.

Decisions taken

D11 — a resumed attempt continues the existing one, so max_attempts bounds genuine failures rather than crashes. D12 — a hold suspends the lease and keeps the claim. Both were put to the owner and recorded in the proposal's §11.1 before the code depending on them was written. D7, D8 and D9 remain open and untouched.

Eight defects found on the way, all by a test

Three of them by pre-existing tests failing, which is the system working:

Also: run --project X set X running and then claimed from default; a killed worker left its half-applied diff in the tree so the next attempt's patch failed against a tree already containing it; a relative --work failed mid-apply with a git error naming the wrong thing.

What this is not

  • Not evidence about live behaviour. No provider was contacted at any point, in any stage. Deterministic success is proven; live success is not. Stage 8 is blocked on credentials, network, a real second repository and NGMS decisions.
  • Session mode is behind. Stages H and L are the direct-API executor only.
  • Stage M has never sent a span to a real collector.

Every stage report carries a blind-spots section saying which of its own claims are untested.

A note on the commit identifiers

This branch was rebased into a linear history because main requires one and the integration had four merge commits. The rebased tree is byte-identical to the verified commit (3607b82d either way). The original, with its merge commits, is preserved on archive/fit-for-purpose-2026-08-04, and each stage on its codex/fit-stage-* branch — that is where the shas quoted in the evidence reports resolve. Explained in the programme report.


The original validator fix

Found by pointing the harness at a real backlog instead of a toy repo. Three ways a well-formed patch was refused before git ever saw it: new file mode 100644 and its siblings were missing from _BETWEEN_HUNKS so a file creation read as corrupt; a hunk header that over-declares was marked fatal, contradicting PatchProblem's own docstring; and the recount path discarded a patch git would have applied.

sprooty added 30 commits August 4, 2026 10:35
…e CLI

Chains shipped in the CLI and in the stored role map, and `RoleRoute` -- the
only way to configure a deployed pod -- could not express one. Found while
repointing Node B: the models an operator had chosen could be set locally and
not on the machine that runs the work, which makes the feature unshipped where
it matters.

`models` names the chain in preference order; `model` stays the preferred one
and is filled in from the first when omitted, so every existing map, client and
reader that knows only `model` is unaffected. Giving both is allowed and
checked: a route whose two fields contradict each other behaves differently
depending on which one a reader consults, so it is refused with the mismatch
named rather than silently resolved.
Three ways a well-formed patch was called corrupt, all found by pointing the
harness at a real backlog rather than a toy repository.

**A patch that creates a file.** `new file mode 100644` is what git emits for
the ordinary way to add a module, and it was not in the list of lines allowed
between hunks -- so the validator read it as body content and refused with
`line inside a hunk starts with 'n'`. `deleted file mode`, `similarity index`,
`rename from`/`to` and `copy from`/`to` were missing for the same reason.

**A hunk header that over-declares, mid-diff.** Both places that detect it
marked the problem fatal, which contradicts what `PatchProblem` documents
about itself: line counts that are wrong are "routinely repaired, notably
hunk headers whose line counts are wrong... refusing the second would throw
away work `--unidiff-zero` rescues every day". They are now reported and not
refused, because another hunk or another file follows -- so nothing was cut
off, and `recount_hunks` derives the right counts from the body. The
end-of-input case is unchanged: there, a genuine truncation is possible, so it
is still refused unless both shortfalls match.

**A patch with no hunks at all.** A rename, a delete or a mode change is a
complete patch with nothing to hunk. The no-hunk check exists to catch a model
answering in prose, so it now looks for a real file header before refusing.

Live effect on NGMS: two items that failed at `parse` now reach `apply` and
fail, honestly, on what git actually says -- `Cargo.toml: patch does not
apply` rather than `corrupt patch at line 53`.
…ent must not assume

Four stages sit finished on their own branches and none is merged. This
says where each one is, what its report proves, what it leaves unmeasured,
and which numbers must not be carried forward.
`Provider` classified failures. The CLI's transport separately assumed one
gateway's completion path, one authentication header and one response
envelope. Neither knew about the other, so "add a vendor" meant editing the
transport, and changing the classifier changed nothing about the wire.

A route now names a preset, and a preset supplies the request adapter, the
authentication strategy, the response/usage reader and the classifier
together. The core preset is generic and claims nothing about any vendor;
`chat-completions` and `claw-bay` moved to adapters and are resolved by name
through entry-point metadata, so no core module imports one and adding a
vendor is a module plus a name.

The older `provider` field keeps working and keeps meaning what it always
meant -- the classifier only -- so a role map written before this calls the
same URL with the same credential and classifies the same way.
Stage A's deterministic transport becomes the classifier conformance
fixture. Four configurations -- the generic core route, two adapter presets
and one declared outside the package entirely -- are asserted against the
same six documented failure shapes: the classification, and then the
reaction. What was retried, with which injected backoff and jitter; what was
refused; what parked an endpoint and what did not; and what the event said
about the route, protocol, classifier and role.

Each configuration declares what it produces for each shape, so a build
cannot claim a distinction it does not make: a classifier with only HTTP to
read collapses caps and refusals into "going too fast", and the table says so
rather than excusing it.

The suite also proves an unknown price stays unknown instead of becoming
zero, that usage reading stays conservative, and that a preset registered
from outside runs the whole suite unchanged.
What a route is made of, how a preset is named, discovered and configured,
what the older `provider` field still means, and why detection from an
endpoint host is printed for a human and acted on by nobody.
Four route configurations through one conformance suite, what each §7.3
criterion is proven by, the migration for a role map written before presets
existed, and the limits: no gateway was contacted, no money was spent, and
session-mode implementer traffic is still invisible to this telemetry
(issue #128).
sprooty added 26 commits August 4, 2026 10:36
What each acceptance criterion is proven by, and the three things this
result does not establish: real GitHub service behaviour, assessor model
quality, and adoption against a backlog anyone has actually lived in.

Records one deviation honestly — the first report of a project is not
byte-identical to later ones, because the queue rows it created are new
facts the later reports state. Idempotence holds on the effects and as a
fixed point from the second adoption onwards.
docs/backlog.json read like a record of current state and is not one: no state
field, 44 issues on github that it never held, and every item filed under the
superseded P0-P4 phase order. renamed to docs/backlog-seed-2026-08-02.json so
the name says what it is, T43 backfilled so the seed matches what was created,
and D1's ruling -- github is the sole source of truth for issues -- written
into the AGENTS.md table. no attempt is made to sync the two.
… nothing

init --demo builds a real git repository, a plan, a queue and a route in one
directory and leaves the project stopped. one documented command then runs the
real executor against a deterministic in-process transport: plan, implement,
apply, checks, commit, review. only the transport is replaced, so what is
exercised is the harness rather than a demonstration of the demo. the answers
are fixed, and it says so — it proves wiring, not model quality.

doctor reports route completeness, the resolved protocol and classifier,
worktree availability, whether a check command is runnable at all, reviewer
independence, cost visibility and whether anything here can mutate github.
nothing it reports costs a request; asking a model is opt-in and is otherwise
reported as not asked, never as passing.

two defects the demo found on the way. run --project X set X running and then
claimed from the default project, so a full queue reported nothing to do. and
a relative --work failed mid-apply with a git error naming the wrong thing.
README §0 shows init --demo and doctor before anything is configured, and says
plainly that a green demo proves wiring and not model quality. USAGE gains the
worked first run with its real output, and a separate opt-in path for a local
OpenAI-compatible server: who supplies the model, what the endpoint shape is,
that authentication is sent and ignored rather than disabled, and exactly what
offline does and does not mean.

README also now defines tested, observed and proven and says which claims are
which, because a reader cannot otherwise tell a deterministic pass from a live
one. tests assert the documented flags exist and that the command init prints
is the command that works.
Checks.run returned (bool, str). that one bit collapsed four situations: a
check that could not run, a check that ran and the diff is wrong, a failure a
mechanical fix would clear, and a condition needing a person. it now returns a
typed result carrying one of pass, retry, fail, fix_available or escalate, and
still unpacks as (ok, detail) so a caller that only wants the bit is unchanged.

the classification is structural and never semantic: how the subprocess ended,
never what a project's output said. a timeout is a question that was not
answered rather than an answer of no. a missing interpreter and a full disk are
a machine to fix rather than a diff to reject. a declared fix is recorded and
never run.

an attempt now carries a disposition -- completed, refused, crashed, withheld,
escalated -- and a reason kind, both stored on the row and published on
GET /api/work/{id}. a reviewer rejection and a crashed worker are both failed
and are no longer indistinguishable.

attempt accounting is deliberately unchanged. only the two outcomes that did
not exist before decline to consume an attempt; whether a crash should is D11,
it is open, and naming a distinction is not the place to answer it by moving a
number. D8 is untouched: there is no registry here, and a test says so.
a fix is keyed to a check command verbatim and refused if it names one that is
not a check -- dead configuration is almost always a typo, and a fix that
silently never applies is worse than none because somebody believes it is
there. AGENTS.md and USAGE gain the disposition table, the reason kinds and the
five check outcomes.
an attempt was monolithic: any crash sent the next claim back to the planner,
re-paying for the plan, the context selection and the implementer. a durable
per-attempt record now holds one row per stage reached -- planned, implemented,
applied, checked, checkpointed, reviewed -- with the artefact that makes it
resumable and the graph revision it was admitted at.

recording a stage is not the same as resuming at it, and RESUMES_AT says which
is which rather than leaving it implied: an uncommitted working tree does not
survive a crash, so applied re-applies its stored diff, while a checkpoint is
already in git. resumption is never replay -- a model is not deterministic, so
a recorded verdict is reused rather than re-asked, which also stops a crash
being a way to shop for a different answer.

durability is a policy: nothing until exit, at each stage boundary, or with the
intent to perform an external effect recorded before it happens. the pre-review
git checkpoint is unaffected by all three -- the mode governs the record, not
the commit.

D11, resolved: a resumed attempt continues the existing one, so max_attempts
goes on bounding genuine failures. the consequence is named -- a crash loop is
then bounded by a budget, which stage L owes.

three defects found on the way. a killed worker left its half-applied diff in
the tree and the next attempt's patch failed against a tree already containing
it, which read as a bad diff and was not. an operator's retry would have
resumed into the verdict it was retrying, so a decision now seals the attempt
and a requeue forgets it entirely. and a brief that moves under a live claim
discards the position loudly rather than judging old work against a new
question.
--durability on run beats the project's setting beats the deployment default.
an unknown mode is refused at both doors rather than defaulted: a typo that
silently downgraded to exit would look exactly like a harness that had stopped
resuming, and nobody would know which. USAGE gains the resume table, the
sealing rule and what a killed worker now costs.
the lease bounds a worker's absence, not an item's duration: a heartbeat proves
a process is alive and proves nothing about progress. stage H made that worse
before it made it better -- D11 ruled that a resumed attempt continues the
existing one, so max_attempts stopped counting crashes and an item that crashes
in a loop was bounded by nothing. this bounds it.

a per-item wall-clock ceiling and a per-item spend ceiling, declared on the
project and overridable per item, both defaulting to unlimited so an existing
database upgrades with no behaviour change. exceeding either stops the item at
the next boundary -- never mid-stage, because stopping in flight destroys the
context and leaves a half-finished worktree -- as blocked / escalated, with the
ceiling named, and without consuming an attempt.

a spend ceiling is not a provider cost cap and is never classified as one. it
does not park the endpoint: window_cap and terminal_cap are a provider's
statement about our account and belong in the never-retry set, while this is
our statement about one item, and parking a shared endpoint because one item
was expensive would be that conflation made real.

unknown cost stays unknown. a call that reported no usage is unpriced, not
free, and while any call is unpriced the recorded spend is a lower bound and
the ceiling is reported as unenforceable rather than as met. the wall clock is
still checked in that case: an item that has run for a week should stop for the
reason that is knowable.

the ceilings are readable through the api and through doctor, which warns that
unlimited is safe on upgrade and unsafe unattended. whether it should stay the
default is D14 and remains open.
waiting_for_input was a projection over recent events. the row stayed claimed,
the heartbeat kept stamping, and the lease kept renewing -- so a lease whose
whole purpose is to distinguish slow from dead was holding open a human's
inbox. nothing bounded it, nothing survived the worker dying, and the answer
could only come from the process that happened to be attached. issue #103 is
that hole seen from outside.

a held item is now a state of the work item, carrying the question, who may
answer, and a single-use resume token. D12, resolved: the hold suspends the
lease and keeps the claim, so the worktree and the agent's context survive and
answering resumes where the item stopped. no other worker can claim it while a
person is thinking.

the cost of that ruling is named rather than hidden -- a worker slot is tied up
for the whole hold -- which is why the maximum duration is not decoration and
why, unlike the budgets, its default is not unlimited. expiry returns the item
to blocked with the question preserved, never to ready: a hold that times out
has not been approved.

nothing interprets the answer and nothing writes into a session. both are
asserted against the module's code with its prose stripped, because both are
the kind of thing somebody adds later while being helpful.

GET /api/holds is the inbox, oldest first. an item that is claimed, making no
progress and holding no question is a hang, and now reads as one by omission.
an adapter in adapters/, opt-in behind run --otel and lazily imported, so no
core module reaches for the SDK and a build without it runs identically. the
whole test suite runs with the SDK deliberately not installed, which is how
that claim is asserted rather than hoped.

model calls, gates and item lifecycle become spans, each carrying the run,
item, project and sequence that join it back to the append-only record. a span
nobody can join to the events is a second story about the same run.

D13, and the safe answer: export only. nothing is written back, nothing is read
back, and a test greps the module for the types it must never reach for. a
broken collector costs a counter increment -- observation never stops work --
and the tap writes the event before it exports, so telemetry can never come
between an event and the record of it.

it does not close #128 and says so with a number rather than a caveat.
coverage() reports the fraction of known model traffic exported, and a
session-mode agent self-reporting a bare count LOWERS that fraction, because a
count with no per-call record is honestly less than a span. traffic that
self-reports nothing is in neither number, and the note says that too.
not a stage. §13 of the extension proposal records this as worth doing and
explicitly not fitness-for-purpose work, so it is here on its own rather than
smuggled into one.

one machine, one file, one writer, worktrees multiplying peak disk with the
worker count, and a backup that is a file copy only after a WAL checkpoint.
where the ceiling actually is has not been measured and the paragraph says so:
no load test has been run, and the concurrency at which sqlite contention
becomes the limit is unknown.
…ocument that matches reality

one number derived on one tree -- 1063 -- and one sentence for a hurried
reader: every stage that could be run has been run, and not one line of it is
evidence that the harness works against a real model. eight defects, all found
by a test and all fixed, three of them by pre-existing tests failing.

the status document now says where it got to before it says where it was, and
names the three things a later agent would otherwise assume: nothing here is
live evidence, session mode is behind on stages H and L, and the per-branch
test counts are history.
…ot keep them

main requires linear history and the verified branch had four merge commits, so
no way of landing it preserves the shas the evidence reports cite. the rebased
history has a byte-identical tree; the original is preserved on an archive
branch and the seven stage branches. a report citing a sha nobody can resolve
is a report nobody can check.
@thedancingdeveloper
thedancingdeveloper force-pushed the fix/validator-rejects-valid-patches branch from cf5a0e6 to 8324210 Compare August 4, 2026 10:37
@thedancingdeveloper thedancingdeveloper changed the title fix: stop the validator refusing patches git would have applied fit-for-purpose: integration, first run, outcome taxonomy, resumable attempts, budgets, holds, telemetry Aug 4, 2026
…rting from

AGENTS.md carried three claims that were flatly wrong. it said this repository
does not contain the model client until a later phase relocates it -- it is
here and central. it gave a local-run snippet using an ingest --logs flag that
does not exist. and its core-module list was a second, drifting copy of the one
tests/test_generic.py enforces, in the document that forbids exactly that.
it now points at the test, records D10-D14, and states the four gates and the
TMPDIR precondition that otherwise costs an afternoon.

USAGE gains a Which way in? table, because the four routes -- demo, inception,
a plan you wrote, adopt -- were all documented and nothing told a reader which
was theirs. each route now says where it goes next; adopt in particular ended
without mentioning that it writes queue rows and does not register or start a
project. inception is marked API-only, because it has no subcommand and a
reader following it would have curled a port with nothing listening.

ARCHITECTURE listed project inception as designed and not built; it is built.
its component diagram and table were missing graph, adoption, inception,
outcomes, attempts, budgets, holds, preflight and doctor. INTERNALS gains the
dispositions and the two different budget outcomes, and says the event table is
the shape rather than the schema.

COORDINATION-PLANE and MULTI-PROJECT-PLAN both said proposed, not implemented
at the very top while parts of both were live. each now carries a per-section
state table. the stage E1 evidence report gets an appended correction for the
link the backlog rename broke, rather than an edit.
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