Skip to content

feat(live): Ask and Act are separate buttons, and the request says which - #31

Merged
fiddur merged 12 commits into
developfrom
ask-and-act
Aug 25, 2026
Merged

feat(live): Ask and Act are separate buttons, and the request says which#31
fiddur merged 12 commits into
developfrom
ask-and-act

Conversation

@fiddur

Copy link
Copy Markdown
Collaborator

One button meaning "hand this over and let the agent decide" put the decision in the wrong place.

Two buttons, and a field rather than salted text

Ask and Act sit beside Cancel and Comment. The request carries an intent, so the instruction is
part of the protocol instead of being mixed into the comment body — the reader's words stay theirs in
the thread, and the agent is told in a field.

It is still said in plain words on the way in, because that is what an agent actually acts on:

ask → The reader asked a question. Answer it in the thread, or amend the finding it is
about. Do not change code — they pressed Ask, not Act.
act → The reader asked for a change. Read it, make the change, and reply in the thread
with what you did. Do not commit, push or merge.

An intent that is absent or unrecognised is a question. Least privilege, and it means every
request written before this — and anything malformed arriving at the route — gets an answer rather
than an edit.

Act is not offered where it would be refused

/api/live/status now reports mayChangeCode, derived from who wrote the pull request, and the page
does not render the Act button when it is false. Better than offering a button and then refusing it:
a button that is there is a promise.

The refusal still exists underneath, since a request can arrive by means other than the button — an
act on somebody else's pull request is answered rather than acted on, and the agent is told to say
that is why.

Editing a comment

A fixed three-row box meant editing a long finding through a letterbox. It is sized to what is in it
now, between four rows and twenty-four — past that the box would push the diff off screen and
scrolling inside it is the lesser evil.

Verified

parser 43, git 44, github 21, ui 345, cli 148 — 11 new. UI typecheck at its 6 pre-existing errors.

Driven by hand as well as asserted: an ask and an act posted through the API came back to
agent await with the right intent and the right directive, in that order.

🤖 Generated with Claude Code

https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs

fiddurand others added 12 commits August 25, 2026 08:02
One button meaning "hand this over and let the agent decide" left the
decision in the wrong place. There are two now, and the request carries an
`intent` rather than the instruction being mixed into the comment — the
reader's text stays theirs, and the agent is told in a field.
`await` still says it in plain words, because that is what an agent acts
on: a question gets "Do not change code — they pressed Ask, not Act". An
intent that is absent or unrecognised is a question, so a request that does
not say what it wants gets an answer rather than an edit, and so does every
request written before this.
Act is not offered at all on a pull request somebody else wrote — the page
asks the server, which derives it from who wrote it. Better than offering a
button and then refusing it: a button that is there is a promise. Asking
for a change there is still refused, since a request can arrive by other
means than the button.
Also: editing a comment no longer happens through a three-row letterbox.
The box is sized to what is in it, between four rows and twenty-four, past
which scrolling inside it is the lesser evil.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
Nothing tested that Act is absent where changes are not allowed — the only
rule in the change, held up by a prop being undefined. The decision is now
canAskAgent and canActOnCode, with ten cases across the two, including that
somebody else's pull request gets Ask and no Act and that nobody listening
is not a reason to hide either.
The edit box measures its own wrapped height instead of dividing by an
assumed 80 characters a line, which was a different number in split and
unified view. The module only clamps now. I had reached for arithmetic
because it was easy to unit-test, which made the test easy and the result
approximate.
And the thread says which button was pressed: "change asked for" and "done"
rather than "asked" and "answered", now that the two mean different things.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
…count
It appeared in the bottom right for a thread that was above. `threadBounds`
returns null for a thread that is not rendered — far from the reader, so
virtualised away — and null fell through to "below". Where the file sits in
the reading order answers it without the DOM, and is used when there is
nothing to measure.
It now sits over the old side rather than the new: the note should not be
on the code being reviewed. Top or bottom still follows the thread.
It leaves after ten seconds, with a bar running down so that is not a
surprise, and what it leaves behind is a count — a red badge and an arrow
at the top of the diff over the gutter, straddling the file header. Going
to it takes the oldest first, so following it repeatedly walks through them
in the order they arrived. Scrolling a thread into view still clears it,
now from the count as well as the note.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
…review
Authorship is a proxy for whose work this is, and it stops being one the
moment work is handed over: take over a colleague's branch and the pull
request still says they opened it, so Act disappeared and a change request
was refused on your own work in progress.
The agent bringing diffity up knows which it is doing. `--work` says you
are working on this branch, `--review` says you are reviewing it — which
also covers reviewing a pull request you wrote yourself, where authorship
would have said yes. Neither still means derived from authorship, so
nothing changes for anyone who says nothing.
Not a setting. It lasts as long as that server, so there is nothing to
leave switched on, and handing work over means restarting rather than
remembering to untick something. diffity-review now launches with
--review, since reviewing is the one thing it is for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
… on reload
Half the comments added on this branch went. What is left is the handful a
reader could not get from the code: a non-obvious invariant, a choice that
looks wrong until you know why. The rest was me narrating decisions.
Also: threads are an empty array while the query loads, and recording that
as "what I have seen" made every existing answer look new on the next poll
— so a page rebuilt underneath the reader announced the whole conversation
back at them. An empty first look now announces nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
The page said "No agent" while an agent was parked. The status route read
`?session=` and the page sends `?ref=`, so it fell back to the shared
current-session file — which every worktree using this data directory
writes to — and answered about whichever review was opened last.
Both live routes resolve the session from the ref, the way /api/info
already did, and `agent await` asks the server which session it is serving
instead of reading that file. That file was the ambient-session problem
flagged earlier today; this is the path where it actually bit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
…wering
Between taking a request and answering it the listener is not parked on the
claim route, so presence alone reported nobody there — at exactly the
moment an agent had the reader's question in hand. Which is the window a
reader is most likely to be looking at it.
Three states now: waiting, working, none. Working is derived from a
request claimed and not yet answered, which is a fact about this review
rather than about connections.
Also: the test helper that drained the queue claimed without answering, so
everything it touched counted as being worked on forever. It answers what
it takes now, and the two new cases assert a delta — sessions on one branch
share their threads, so an absolute count is about the whole file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
…tore it
The restore fired on the first frame after the diff loaded, when the view
had not mounted, so the scroll went nowhere — and the guard had already
been set, so it never tried again. The reader was left at the top of the
diff, which is where the general comments and their reply box are. That is
the jump, not the bubble.
It waits for the handle now, and gives up after sixty frames rather than
spinning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
The count was a badge hung over the gutter, and every attempt to place it
was wrong in a new way: clipped by the container that scrolls, offset by
the file list, on the wrong gutter in one view mode. A toolbar element has
none of those problems, and a bell with a count is what everyone already
knows how to read.
Left of the agent indicator. Quiet with nothing unread, a red count when
there is, and a list on click — who answered, which file, the first lines
— oldest first, so working through them follows the order they arrived.
Also: the note's edge now follows the order the reader sees rather than the
raw diff order, which is why a note about a file above them arrived in the
bottom corner while the walkthrough had reordered the files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
The reader is looking at the page having just asked, and between taking
their request and answering it nobody is parked — so it said "No agent" at
the moment they were most likely watching. Re-arming first closes that
window, and a second request arriving while the answer is written is queued
rather than lost.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
Right-aligned against the midpoint in split view, so it covers the right of
the old side and never the new code being read. Unified keeps to the left,
having no midpoint to sit against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
The bell stopped counting down once a note expired: the watcher that
clears an answer when the reader scrolls to it only ran while a note was
showing, so the count quietly changed meaning from "unseen" to
"unclicked". It runs whenever either list has anything in it now.
The list has a maximum height, since a batch of asks is how this gets used
and thirty would have run off the bottom of the window.
And the purpose flag is tested from flag to page rather than only at the
function that resolves it — the path deciding whether an agent may edit
somebody else's branch had been verified by hand and by nothing else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
@fiddur
fiddur marked this pull request as ready for review August 25, 2026 07:58
@fiddur
fiddur merged commit 7b0853e into developAug 25, 2026
@fiddur
fiddur deleted the ask-and-act branch August 25, 2026 07:58
fiddur added a commit that referenced this pull request Aug 25, 2026
A running instance replaces itself only when the binary version differs
from the one it registered (`isStale` in `packages/cli/src/index.ts`).
So a merge without a bump leaves every instance that is already open
serving the old build, silently. #31 and #32 both went in that way — six
instances here are still on 0.9.6.
Three things:
- **`AGENTS.md`** with the rule. It is the first agent-facing doc in the
repo.
- **`--no-git` on `scripts/release.ts`**, without which the rule cannot
be followed: the script commits and tags, and since develop takes squash
merges the commit is rewritten on the way in and the tag is left
pointing at a commit that never lands. Default behaviour is unchanged.
- **v0.9.7**, the rule applied to its own PR.
parser 43, git 44, github 21, ui 371, cli 157; the built binary reports
0.9.7.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
fiddur added a commit that referenced this pull request Aug 26, 2026
#36)
Everything on the known list, plus the reason two of them were
invisible.
### A review taken by an unrelated one
`reviewScope` maps every base sha to one scope, because a branch's base
moves as the branch is updated. With no branch that reasoning is gone —
so a second PR opened in the same detached checkout looked like the same
review continuing, and `gatherOpenWork` moved the first one's findings
into it. Work may now only move when a branch connects two sessions, or
when the ref is identical.
Redirecting a stale tab is deliberately **not** guarded: pointing
someone at a newer session shows them something they can check, and the
legacy-migration tests depend on it. Only taking work is narrowed.
### A detached checkout called its branch HEAD
`rev-parse --abbrev-ref HEAD` returns the literal `HEAD`, which matches
nothing, stranding any session opened before `gh pr checkout` moved the
worktree onto a real branch. Recorded as unknown now, which is what it
is.
### The wrong diff decided where a comment could go
`gh pr diff --patch` is the mbox commit series, not the PR diff.
Measured on our own PRs:
| | `--patch` | correct |
|---|---|---|
| #34 `github-resolution.test.ts` | 25 lines | **89** |
| #34 `github-resolution.ts` | 29 | **59** |
| #31 `agent.ts` | 15 | **32** |
| #31 file count | 49 | 47 |
Two of #31's "files" were historical paths from before a rename. And the
error message sent you auditing your own line numbers; it names the diff
now.
### Text typed into the page was rewritten
Shell unescaping lived in `threads.ts`/`tours.ts`, which the browser
routes share, so a comment discussing `split('\n')` was stored with a
real line break. Moved to the CLI boundary. The CLI still unescapes —
verified end to end both ways.
### The two type errors, and why they survived
`packages/cli` had **no typecheck script at all**, and the UI's failed
on four TS6059 errors from its own tsconfig (`rootDir: src` against an
`include` that reaches outside it), so its output had been noise long
enough to ignore. Both fixed, `npm test` now runs `typecheck` first, and
**the count is zero**.
Mutation-checked: reverting each session fix turns the new tests red (1
and 4 of 4 respectively), and putting the unescape back in storage turns
2 of 3 red.
parser 43, git 44, github 21, ui 390, cli 179, scripts 4. v0.9.11.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@fiddur