Skip to content

Enhance report-driven adapter workflows and evidence handling - #2960

Merged
esokullu merged 68 commits into
webbrain-one:mainfrom
esokullu:main
Sep 1, 2026
Merged

Enhance report-driven adapter workflows and evidence handling#2960
esokullu merged 68 commits into
webbrain-one:mainfrom
esokullu:main

Conversation

@esokullu

@esokullu esokullu commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

esokullu and others added 30 commits August 28, 2026 01:01
Stop chasing per-site completeness folklore: exhaustive AX reads must report depth truncation, skipped rows cannot prove success, and checkbox/Next actions stale the snapshot until a fresh root read.

Co-authored-by: Cursor <cursoragent@cursor.com>
Compact now gets a brief execution contract and a shorter
progress_update.workflowReconciliation schema. The evidence kernel
matches AX-truncated metadata, keeps valid fields when one name is
unknown, emits live required=false, and fail-closes only on form-relevant
depth or iframe omission.

Co-authored-by: Emre Sokullu <esokullu@gmail.com>
Co-authored-by: Emre Sokullu <esokullu@gmail.com>
Truncated AX values now bind value_len and value_fp so a shared
60-character prefix is not exact saved-state evidence. Failed
same-site or urlFilter-targeted frames stay in inventory documents.
Discarded classifier metadata fields keep verification incomplete
without wiping the rest of the list.

Co-authored-by: Emre Sokullu <esokullu@gmail.com>
Resolve agent.js conflicts by keeping the PR's unified planner
follow-up path and combining siteWorkflow continuation binding
with main's task-authority keys.
Emit required= only for native required or an explicit aria-required
value, so React/app validation cannot be skipped as optional. Hash AX
metadata values after the same NFKC normalization used by verification.
Omit empty third-party iframe noise only when another frame already
inventoried form controls; a lone failed cross-origin application frame
stays incomplete.

Co-authored-by: Emre Sokullu <esokullu@gmail.com>
…8d73

Shrink Compact workflow prompts and fix evidence false positives
CodeQL flagged incomplete sanitization because input/textarea values
escaped quotes without first escaping backslashes. Always escape \\
then " in value= tokens, and parse those escapes back so metadata
readback still sees the app-owned string.

Co-authored-by: Emre Sokullu <esokullu@gmail.com>
Escape AX inventory values to clear CodeQL alerts on #320
Re-resolve the planner site-workflow against the live tab after a
reviewed-text edit so a wording fix cannot drop transaction_fulfilled.
Include ARIA searchboxes in form inventories and text-action evidence.

Co-authored-by: Emre Sokullu <esokullu@gmail.com>
…ntory-8d73

Keep live workflow contracts after plan wording edits
esokullu and others added 28 commits August 30, 2026 05:00
The inner planner gate returned draft_recipients but the outer gate
reconstructed its result field by field and copied only messaging, so
_startPlanExecutionGuard received null and draft verification fell back to
accepting any nonempty To line. The last commit's binding never reached the
guard. The outer gate now normalizes and passes it through, and a test drives
_maybeRunPlannerGate end to end so the handoff cannot be dropped again.

Cumulative form inventories kept every prior item on a fresh root read, which
is right for a wizard section that scrolls away after being answered but wrong
for a question a branching answer hid before anyone touched it. That row stayed
required with no possible action evidence and no way to skip it, so a form that
is complete on screen could never reconcile. A complete exhaustive root read now
rebuilds the current document's item set and carries over only rows the run has
accounted for, by per-control action evidence or a terminal ledger row. Handled
sections and resolved threads survive; an untouched question the page dropped
does not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
Gmail's floating compose never renders the sent message inline, so the
compose fallback accepts the body captured at dispatch without an outgoing
match. That was fine as a delivery signal but it never compared the text to
the classified body requirement, so a stale composer sent to the right person
passed as the reviewed message. The subject check now covers body too, over a
composerBody the binding records whether or not a recipient binding exists.
A classified field the composer cannot prove fails closed.

upload-release-assets is a publish job with a ledger, which
_workflowJobBindsPublicationPayload excludes, so it skipped payload matching
entirely. Filenames say which assets were uploaded and nothing about where
they landed, and the same names saved on another release satisfied the
terminal check. The saved release URL is now bound to the repository the
dispatch came from and, when the request named one, to that exact tag. The
job collects payload fields for that, and the ledger classifier is told to
return the tag when the user names it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
Waiting for rootReadComplete meant the rebuild only ever fired on a root read
that finished in one page. A paginated re-read has continuationPending on page
1 and never starts a root read on its continuations, so a control the branch
hid stayed in the inventory and reconciliation stayed impossible. The rebuild
now runs on the first page of any fresh exhaustive root read and the
continuations accumulate onto it. That first page also drops the document's
earlier completeness, since a half-rebuilt item set is not complete coverage;
the terminal page restores it.

Message body normalization collapsed every whitespace run to a single space,
so two paragraphs accidentally joined into one line compared equal to the
requested body and the new exact-body check could not see the difference.
Bodies now normalize per line: horizontal whitespace and blank-line counts
still collapse, because editors rewrite those on their own, but line structure
survives. The content probe's copy has to agree, since it compares page text
against a body the agent already normalized, and a test pins the two
implementations together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
A submit binding carried adapter and job identity but nothing about which
form was submitted, so another Microsoft Form or Greenhouse application on
the same host produced the same generic confirmation and satisfied a run
that had reconciled a different form entirely. Inventory documents now
record the form they were read from, host plus path plus the app-owned form
id, and the confirmation requires the dispatch to have come from that same
complete document. Two forms on one host no longer look alike.

Reconciliation let any aria-required=false row be skipped, which is right
for a referral code nobody asked about and wrong for the cover letter the
user asked to attach. Nothing carried that distinction, because form
workflows reconcile against the AX inventory rather than classifier rows.
The intent classifier now reports which fields the request named, in the
user's own words, and an optional control whose label matches one of them
has to be processed like a required one. The match is label to label, and
either side may contain the other, since the request and the page rarely
word it the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…fields

Keeping only named targets in draft_recipients discarded the one kind a
"save a reply in this thread" request produces. Both messaging and
draft_recipients came out null, the draft bound to nobody, and any recipient
in the composer passed. The conversation kind is kept now and pinned at the
gate exactly as messaging is, so the draft binds to the verified recipients
of the thread that was open when the run was authorized. It still grants no
send authorization: messaging stays null whenever the plan submits nothing.

The unresolved-thread matcher only knew English. GitHub ships a localized UI,
so a page full of unresolved conversations read as zero items, and this job
treats a complete empty inventory as a successful no-op. It now matches the
resolve control across the languages GitHub ships.

filter="all" deliberately keeps aria-hidden and invisible nodes, which pulled
conditional and honeypot inputs into form inventories. They are rarely marked
optional, so reconciliation demanded rows that can never earn a verified user
action. The tree marks those controls hidden and the inventory leaves them
out, rather than deadlocking the form or pushing the agent to fill something
the user was never shown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…honest

A structural action marked every inventory document incomplete, including
steps the run had already finished and navigated away from. On a multi-page
Workday or Forms flow the read on the next route completes only its own
document, so the earlier step stayed incomplete forever and the cumulative
inventory could never be trusted again. Invalidation is now scoped to the
document the action ran in, and a step that was complete when a Next click
left it is handed back as finished history once a different document root is
read.

The review-thread inventory kept only Resolve buttons, so a request to post a
specific reply and resolve each thread could reconcile every button row while
the reply went missing. Each thread's reply box now joins the inventory,
paired to the resolve control that follows it, as an optional row. A
resolve-only request still skips it; once the request names a reply, the same
requested-label rule that covers any optional control makes it mandatory. The
conversation-level comment box has no resolve control after it and stays out,
so a pull request with nothing left to resolve is still an empty inventory.

The hidden-control marker only reached AX lines. iframe_read now reports which
matches the page hides and the iframe inventory drops them, so a display:none
conditional field or honeypot in an embedded application cannot demand a
ledger row that no verified interaction can ever fill.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
The completion probe collapsed the whole page into one line, but published
payload verification only accepts a requested title, notes, or body as a
whole line or as the entire page. So a real GitHub release, LinkedIn post, or
Douyin publication could never verify its payload, while the unit tests fed
the matcher newline-preserving strings and passed. The probe now collapses
only horizontal whitespace and keeps line boundaries, and a test runs the
probe's own expression into the real matcher so the two cannot drift apart.

Binding the form confirmation to the inventoried document assumed an AX
document key. A Workday or Greenhouse form that lives entirely in an iframe
is inventoried under iframe:<frameId>:<frameUrl> keys, so the exact lookup
missed and a valid confirmation could never be accepted. When the dispatch
scope is absent from the inventory, the bound form identity now selects the
frames of that same form, all of which must be complete. A different
application on the same host still has a different identity and still fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
Excluding hidden controls swept up the file input behind every styled upload
widget. Greenhouse and Workday hide the real input under a visible label, and
upload_file finds and drives it on purpose, so dropping it left a requested
resume with no inventory obligation at all and reconciliation could close
without the attachment. File inputs are exempt from the exclusion now;
ordinary hidden controls still stay out.

The requested-field matcher compared whole normalized labels, which fails the
example in its own comment: "attach my cover letter" and "Cover letter
(optional)" do not contain each other, so the row stayed skippable. It now
compares content words, dropping the optionality decoration a page adds and
the verbs and pronouns a request wraps around the field it names. A match is
one side's identity words being fully covered by the other's. Whole-string
containment stays as the fallback for scripts that do not separate words, and
the reconciliation test now uses the wording from the report rather than the
already-contained phrase that let the old matcher pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
Preserving the form inventory across a submit is correct where the submit is
the job, but prepare-form and prepare-application exist to leave the form
unsubmitted. An accidental Submit took the same early return, kept the
inventory valid, and since those jobs need no submission evidence the run
could report success from the resulting confirmation page. A submit in a
prepare-only job is now a recorded contract violation: it invalidates the
inventory, blocks a success outcome, and gets its own recovery text telling
the model to report plainly what happened rather than submit again.

Job evidence was satisfied automatically for every non-submit job except
Gmail drafts, so count-results and read-complete-thread could finish on a
single accessibility-tree read. Both declare a specific contract, and both
now have to meet it: an exact count needs a verified result from the
deterministic count tool, and a complete conversation needs read coverage
through its terminal page. Selecting the thread job arms that coverage
tracking, and the requirement is only imposed where the run can actually
track it, so a job with no contract of its own is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
Passing the outer AX document token to the invalidation meant an iframe write
matched no inventory key, so every frame stayed complete and the workflow
could reconcile without rereading the frame it had just changed. The changed
frame is now resolved from the action's own frame id, and an action whose
frame cannot be resolved invalidates all frames rather than none.

The evidence allowlist covered only two Gmail jobs, so read-transcript,
collect-ranked-products, collect-comments and review-pull-request still
finished on any single successful tool call. Each declared contract now has a
condition: a deterministic tool where one exists, reconciled rows for a
collection, and for a reading job a successful read of the resource the job
was selected on. That last one is the weakest of the four and does not prove
full coverage; it rules out answering from a listing, a title, or another
tab, which is what an adapter-wide host match let through. It also needs the
URL the job was selected on, so that is threaded through the gates, and where
it is missing the requirement is not imposed at all rather than pretended to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…mpty sets

The count contract required result.countVerified, which _countGmailResults
returns only when it fails to restore the route. Its success path reports
verified and exact, so a correct deterministic count could never satisfy the
job it was written for. The gate reads the real fields now, and a test derives
the producer's success shape from source and fails if the gate ever asks for a
field the tool does not emit. The old test passed only because its fixture
carried the field I assumed rather than the one the tool returns.

Calling the transcript tool counted as transcript evidence regardless of what
came back, so an empty window or a first page with more text pending could
close a complete-summary question. Evidence is now the content: text or
segments present, and no further window pending from either the provider or
the result trimmer. It is re-evaluated per call, so paging to the end
satisfies it and stopping halfway does not.

Requiring at least one collected row made a genuinely empty result
impossible, and a Douyin video with no comments is a real answer. That job
may now finish with no rows, but only when the run actually observed the
video the job was selected on. A collection that cannot legitimately be
empty, such as ranked products, still needs its rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
The scope identity ignored the hash and the video parameter, so every Gmail
view collapsed to one path and every YouTube video to /watch. A job scope now
keeps both, and three contracts hang off it. A transcript counts only for the
video the job selected, so a complete transcript of another video no longer
answers for this one. A Gmail count counts only for a result set this run
actually read: the tool reports an exact number for whatever route it ran on
and warns that it does not validate the query, so it now names that route and
the guard matches it against the sets the run observed.

Any successful observation used to ground a reading job, including a
screenshot, a scroll, or a window probe. Only content reads ground a contract
now. review-pull-request also got its own condition: the changed files of that
exact pull request have to be read, not the overview and not a screenshot.
That still does not prove every file was inspected, and it is not claimed to.

Collection jobs treated the model's own ledger as proof of its own
completeness. Where the planner seeded an expected set, the existing ordered
check now has to pass. Where it did not, each row must carry the fields the
adapter's job contract declares, with no duplicates, so a bare skipped row
cannot stand in for a collected item. Those fields are part of the app-owned
job record, validated with the rest of the contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
A transcript window can end the video while starting anywhere, so checking
only whether another window follows accepted the final fragment of a video
nobody read the start of. Coverage is now a chain: it has to begin at offset
zero and each window has to continue where the last one ended. A gap resets
it. The arguments live on the completion path, so the chain is recorded there
and the guard reads the result.

An empty comment collection rested on having read the video page, which says
nothing about comments. The page itself has to say there are none now, the
same way a form confirmation has to say the response was submitted. Douyin is
Chinese-first so the signal covers its wording along with a few others; where
the page says nothing recognizable the run reports partial rather than
claiming zero.

An unbounded collection stayed the weakest link: one well-formed row passed
for a whole ranking, because row shape says nothing about coverage. Nothing
app-owned enumerates that list, so a ledger the model wrote cannot prove it
collected all of it. Completeness is now claimable only against the expected
set the app seeded from the request, with every ordered row processed and
carrying the fields the job declares. An unbounded request still collects and
still reports its rows; it reports them as partial, which is what they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…ds stable

Gmail defers pinning "reply here" until the composer opens. Nothing carried
the thread across that wait, so a tab that moved to another conversation had
its composer pinned instead and the message could go to a thread the user
never chose. The thread that was open when the run was authorized now keeps
its identity on the guard, and both the deferred setup click and the eventual
pin require the page to still be on it.

A single content word was enough to claim any label containing it, so "attach
my resume" marked a "consent to automated resume screening" checkbox as
requested and reconciliation would not let it be skipped. That pressures the
agent toward granting consent nobody asked for. One token now matches only a
label it actually heads, and only a short one; two or more still match by
subset, which is what the request wording usually gives.

A React-style form that replaces a control on every change mints a new ref for
the same field, so the inventory kept the old id for its action evidence and
added the new one, and the same question had to be answered twice. The tree
now emits the app-owned id and name for every inventory control, not just file
inputs, and identity prefers those. A radio group shares one name, so its
options separate by label. A control with neither still falls back to the ref,
which the test states rather than hides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
Marking every invisible control hidden swept up the standard custom-control
pattern: a checkbox, radio, or select made transparent or clipped and driven
through a visible label or wrapper. set_checked and label activation still
operate those, and when the wrapper exposes no ARIA control of its own the
field simply vanished from the trusted inventory. A prepare-only workflow, or
a submitted one with an optional requested field, could then reconcile without
ever processing it.

Hidden now means what it says: aria-hidden, or removed from layout by display
or visibility. A control that is merely transparent or zero-sized stays only if
nothing can drive it; a checkbox, radio, select, or file input with a visible
label or wrapper is a custom control and remains in the inventory. A text input
is not label-driven, so a transparent text honeypot is still excluded, and a
display:none conditional field still is too. Both the accessibility tree and
the iframe probe apply the same rule.

The test harness stubbed the visibility helper, which is why this passed
before. It now slices the real helpers out of the module and runs them, so the
decision is exercised rather than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…frame steps

Read coverage completes against whatever conversation root the tab is on, and
nothing checked which one. A complete read of a thread the user never asked
about could authorize the answer for the one they did. The job's conversation
scope is now compared before the evidence is set, and the requirement is only
imposed where that scope exists.

The tree left optionality unstated for ordinary native controls, because HTML
reports required === false without any attribute. Reconciliation only lets
explicitly optional rows be skipped, so every optional field had to be touched
and a required radio group was impossible: each unchosen option needed action
evidence of its own. Native inputs, textareas, and selects now state
required=false, custom ARIA controls stay unknown, and a radio group counts as
answered when one of its options is processed, so the alternatives that answer
rejects may be skipped. A group nobody answered still cannot reconcile.

Archiving a finished wizard step existed only for accessibility-tree reads. An
iframe wizard advances by replacing the frame, so its completed step stayed
incomplete forever and the aggregate inventory could never close. The iframe
merge now archives it the same way.

A deferred draft reply carries no messaging at all, so the thread identity that
survives the wait has to come from the draft target too. Without it, a draft
saved after navigating away was accepted in whatever conversation was open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
The continuation snapshot covered the evidence kinds that existed when it was
written and nothing since. Two kinds fell through it, both in a way that only
shows up on a long task.

A paged transcript stores where the last window ended so the next one can
continue from it. That chain was reset on every new run, so a video that ran
out of turns partway could only be finished by rereading it from offset zero,
which the contiguity check demands and the model has no reason to expect.

A release-asset proof records that a file was uploaded and read back on the
saved release. Those proofs were dropped while the ledger rows that depend on
them survived, so reconciliation asked for evidence that could only be
recreated by uploading files GitHub already has. Pending proofs waiting on
their readback went too.

Both now travel with the rest of the workflow evidence, under the same
task and conversation binding, and the snapshot fires when either is the only
evidence a turn produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…ole diff

The classifier is told to return workflowFields=[] when the request named no
field. A response that omits the key entirely was read as that same answer, so
an empty requirement list passed vacuously and a Gmail send could be reported
successful with a subject or body the user never asked for. A missing key is
now inconclusive, and a job whose contract binds requested fields cannot
verify until the classification has actually run. An open progress session
used to skip that classification, so it now resolves the fields it needs
before returning, and the resolution travels across Continue with the rest.

Landing on the changed-files view is where a review starts. The gate accepted
it as the whole review, so a truncated or paginated first fragment of a large
diff could stand for the entire thing. Diff coverage now runs as a chain, the
same way the transcript does: from the first page, without a gap, until the
result reports nothing pending.

The test guards had to say that the classifier ran, because they set the
requirements by hand and the real path sets both. That is the honest shape,
and it is why the vacuous case was invisible until now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
The coverage chain accepted every content-read tool, so get_selection or
extract_data on the changed-files route closed it: a highlighted line or a
targeted query carries no continuation metadata, and the tracker read that
absence as complete coverage. Only read_page and the accessibility tree walk a
whole document, so only those can close a diff now.

Those two also paginate differently, and the tracker knew one of them. It
looked for a page number, which read_page never sends, so a window opened
directly at a nonzero offset counted as the first one and a mid-diff fragment
stood for the whole review. Each reader is now tracked in its own coordinates,
read_page by character offset and the tree by page, and a chain belongs to the
reader that started it, since the two numbers do not mean the same thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…rols

An accessibility read closed the diff whatever it covered, so a subtree read
of one changed file or a filter that keeps only interactive nodes counted as
the first window, and a small result with nothing pending finished the review.
An AX window now has to meet the same document-root, filter, and depth
conditions the form inventory already demands of an exhaustive read.

The iframe serializer still answered optionality only from ARIA, so an
ordinary optional input, textarea, or select in an embedded form came back
with unknown requiredness and could not be skipped. It now reads the native
property, as the accessibility tree does.

A form can leave required on a control it has disabled, which is how a
conditional field says it does not apply. Such a control constrains no
submission and cannot take the verified action a processed row needs, so an
otherwise finished form could never reconcile. Disabled controls are skippable
now in both inventories. A request that names one still promotes it, and the
run then has to report what blocked it rather than pass silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
read_page sets textTruncated on any window past the start, because earlier
text is missing from that window. Reading it as pending coverage meant the
final continuation of a multi-window diff, which reports hasMore false and no
next offset, never closed the chain, so a fully read pull request could not
complete. Only hasMore and a usable next offset say something still lies
ahead. The test now builds its windows with the real read_page windowing
instead of hand-written shapes, so the two cannot disagree again.

An embedded wizard can also advance without navigating its frame. The next
exhaustive read then restarts the same scope, and the rebuild dropped every
row it held, including controls already answered whose ledger entries live on.
The exact inventory-to-ledger match then failed for good. Accounted rows
survive a restart here now, as they already do in the accessibility-tree
rebuild; the archival branch added earlier only covered a frame that changed
its URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…ls first

The transcript chain tracked offsets and nothing else, so a window opened on
one video at zero and continued on the selected one at the matching offset
looked contiguous, and the final scope check saw only the last window. Every
window now has to belong to the video the job selected, the chain carries that
scope so a Continue cannot splice two videos together, and a provider that
names the video in its own result is held to the same binding.

The requested-label set had the same gap the requested-field set had before
it: a classifier response that omits the key normalized to an empty list,
indistinguishable from an answer that the request named no control, so an
explicitly requested optional field could be skipped and the form still
submit. Resolution is tracked now and an omission is inconclusive. It only
bites where a row was actually left out, since a ledger that skipped nothing
never needed to know which controls were named. An open progress session
resolves the labels it needs the same way it already resolves fields.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
An upload is a structural action. An application that parses the resume it
was given, or that reveals questions depending on the attachment, is a
different form afterwards, yet the pre-upload inventory stayed marked
complete. The upload's own proof and its ledger row could then reconcile
while the newly mounted controls were never inventoried, so a prepare-only
run could report completion with required fields missing.

The transcript identity check read only ?v=, but the adapter also accepts
youtu.be/<id> and the /shorts, /embed, and /live routes, which carry the
video in the path. On those the lookup found nothing and accepted whatever
id the provider reported, so a contiguous transcript of another video could
satisfy the job whenever the result carried no URL of its own and scope
resolution fell back to the open page. The id is now read from every route
the adapter matches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…vator

A form widget that consumes the file it was given leaves the upload recorded
only as pending until a later read finds its filename. That record was missing
from the continuation snapshot while the analogous release-asset one was
there, so a run that reached Continue first could no longer promote the
upload, and the input it was attached to may already be gone. Exact
reconciliation then needed the same file attached twice.

The activator fallback accepted any visible ancestor, which is what a honeypot
sits in. A transparent required checkbox inside an ordinary container lost its
hidden marker, entered the trusted inventory, and either blocked reconciliation
or pushed the agent at an invisible field. A custom control wraps its input in
a label or in something carrying the control's own role, so only those count
now; the label paths are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
The execution guard runs before the completion recorder, so a contract whose
evidence is the window that closes it was being judged against the previous
window. The terminal transcript or diff window arrived, coverage became
complete inside the recorder, and nothing re-evaluated the contract, leaving
done blocked until an extra read that should not have been needed. The empty
comment collection had the same shape: the page's own "no comments" line is
read from that same result.

Those three contracts are settled now once the window they rest on exists,
and the guard no longer judges them from stale state.

The coverage tests called the two helpers in the reverse of production order,
which is why none of them saw this. They follow the real order now, and each
of the three fails without the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
A custom radio group carries no HTML control name, so grouping by name alone
dropped every ARIA option. Once one was chosen the alternatives could not be
skipped as members of an answered group, and the form either blocked or
pushed the agent at every mutually exclusive option. The tree now names the
enclosing radiogroup for an option that has no control name, and the ledger
groups by whichever of the two holds the alternatives together.

A readonly field takes no mutation and browsers leave it out of required-field
validation, but only disabled controls were skippable. A form with a fixed
account or identifier field, marked required and readonly as those often are,
could never reconcile. Readonly is stated now and counts as no obligation,
exactly as disabled does, in both the tree and the iframe serializer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
The 12306 contract bound a newly observed order identifier and a paid or
ticket-issued status, which proves a purchase happened, not that it is the
purchase the user authorized. A wrong train, date, or passenger paid for
successfully read as success. The booking details the request named are
classified now and have to read back from that order's own block on the page,
not from the page at large, so another order on the same screen cannot stand
in for it.

Form workflows kept only the labels the request named, so reconciliation
proved each control had received some verified mutation and nothing about
what it received. A typo, a truncation, or a value the page filled in all
passed. The classifier now returns the values the user supplied alongside
their labels, and a control carrying one has to read that value back exactly,
through the same truncation-aware comparison the metadata readback uses. A
field given a value also counts as a field the request named, so it cannot be
skipped either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6FtgcHXYSp9Av6pFZGCYb
…kflows

Add report-driven adapter workflow contracts
@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

@esokullu is attempting to deploy a commit to the esokullu's projects Team on Vercel.

A member of the Team first needs to authorize it.

@esokullu
esokullu merged commit 3b29ad6 into webbrain-one:main Sep 1, 2026
1 of 2 checks passed
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.

2 participants