Skip to content

v6.4.0 — one list of comments, a round that must be answered, and the design guide's palette - #6

Merged
DeyangChan merged 22 commits into
mainfrom
release/v6.4.0
Aug 9, 2026
Merged

v6.4.0 — one list of comments, a round that must be answered, and the design guide's palette#6
DeyangChan merged 22 commits into
mainfrom
release/v6.4.0

Conversation

@DeyangChan

Copy link
Copy Markdown
Contributor

Ships everything on main since v5.0.0 as v6.4.0. Both host manifests
declare 6.4.0 and CHANGELOG.md carries an entry for every version in
between, so the merge tags and publishes the release.

What changed

Breaking (6.0.0) — rounds are gone. A review is one list of comments, each
open or closed, and only the agent closes one. publish --addressed is now
publish --close. Comments live in comments.json, the brief in brief.md,
and a version file is a label rather than a record of comments.

The review loop finishes what it starts. unanswered reports a comment the
agent took delivery of and has not answered; a Stop hook runs it and blocks the
end of the turn. A round belongs to the session whose watcher took delivery, so
a second session in the same project is not gated on a review it never saw.

The workspace says more. publish --summary records the account of the
round, reply --option … --recommend turns a question into a set of choices,
and the comments panel resizes by drag or keyboard. Clear all leaves the
comments you are still working on.

Codex is told when a new version is out. The update check recognises a Codex
install by the version directory Codex unpacks it into, and the banner shows the
two commands that take the update.

One place decides what the product looks like. The palette comes from
design/tokens.css, and tests/design-tokens.mjs fails when the shell drifts
from it. Dark comes from the same source.

Repository work. Security scans and SonarQube gate every pull request, the
Gherkin end-to-end suite drives the real server and CLI under both hosts, a
plugin change is required to carry its own version and changelog entry, and the
README demo is regenerated by a script anyone can run.

How it was driven

  • All six test suites pass: review-lifecycle, host-profiles, workdir,
    round-gate, update-check, design-tokens.
  • build-shell.mjs check reports no drift across all five stamped pages.
  • claude plugin validate passes --strict on both the marketplace manifest
    and the plugin manifest.
  • The reporter verified the loop by hand under both Claude Code and Codex.

🤖 Generated with Claude Code

DeyangChan and others added 22 commits August 6, 2026 00:36
A review kept the same fact in five places and gated publishing on two of
them agreeing. The round record cached a fingerprint of comment content it
did not own, so a reply — including the reviewer answering the agent's own
question — invalidated it, and publish refused. The workspace meanwhile held
back the re-send that would have refreshed it. Neither side could move.

There is now one list. A comment is open or closed; `sentAt` and
`deliveredAt` say where it has got to between the two people. The agent takes
delivery of every open comment, closes what it did, and whatever it does not
name comes back next time. Nothing can refuse a close — that property is
stated in the contract and asserted in the test, which is what neither had
before.

Sending freezes a comment's words, so two writers can no longer disagree
about what was asked and the merge heuristics that arbitrated them are gone,
along with rounds, the pending sentinel, per-version comment copies, claim,
check, and the workspace's flight state.

A store written by an earlier version is read where it lies.
Installing this plugin runs its code on a stranger's machine, so the pipeline
now proves more than that the tests pass.

- CodeQL, Gitleaks over the full history, and a zizmor audit of the workflows
  themselves run on every pull request, on main, and weekly. A gates job fails
  unless all of them report success, so a skipped scan blocks a merge too.
- A SonarQube Cloud scan runs where SONAR_TOKEN is set, and reports a skip
  rather than failing where it is not.
- OpenSSF Scorecard runs weekly on main and publishes the score the README
  badge reads.
- Every action is pinned by commit SHA with its version in a trailing comment,
  and Dependabot moves the pins weekly after a seven-day cooldown.

SECURITY.md owns what each gate enforces; CONTRIBUTING.md and the pull request
checklist say what a contributor has to do about it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ssions

`.sonarcloud.properties` replaces `sonar-project.properties`: SonarQube Cloud
reads it from `main`, so a change to it takes effect after the merge rather
than on the pull request that makes it.

Permissions move from workflow level onto each job, so a job added later
cannot inherit a scope nobody meant to give it.
… end

`review-server.mjs check` reports every comment the agent took delivery of and
then said nothing about — neither closed nor replied to. That is the one case
nothing else notices: the next tick only fires when the reviewer writes again,
so an unanswered comment sits there for as long as they stay quiet. A Stop hook
in `hooks/` runs it and holds the turn open until the round is answered, at most
once per turn, and only while a review has a server behind it.

When the session dies instead, the workspace says the agent has stalled after a
minute with nothing listening, and offers Send again. A delivered comment is not
`unseen`, so no watcher armed afterwards would ever be handed it — without a way
back to the queue, a restarted session sits idle on a review it cannot be given.
Refused while a heartbeat says an agent still holds them.

Hard reset starts a review over from the cog, behind a confirm that says what
goes. The shell's toast joins the top layer as a popover, so a message raised
over a modal is readable instead of blurred by the dialog's own backdrop.

Rules 14 and 15 in contracts/review-loop.md, and tests for both.
`check` was the wrong name to give it: 6.0.0 removes a command called `check`,
so the same release would have retired the name and brought it back meaning
something else. `unanswered` is the word the contract rule and the command's own
output already use.

The 6.0.0 entry now covers what landed after it was written: the round gate,
Send again, Hard reset, and the message that could not be read over a dialog.
This repo is what a user installs, so merging to main is the release: the code
is live the moment it lands. The version and the changelog entry now travel in
the pull request that changes the plugin, instead of in a release commit after
the fact.

check-version.mjs fails a pull request that touches plugins/ without raising
`version` in the manifest and adding the matching CHANGELOG.md entry. Nothing
downstream can catch that, because by then the code has already shipped to
everyone and told no one: a host decides an update exists by comparing the
version it installed against the one main declares.

release.yml then tags main and publishes the GitHub release from what the tree
already says, with the changelog entry as the notes. It keys on whether the
version is already tagged, so it is safe to re-run and does nothing on a merge
that changed no version. It creates a tag rather than pushing to a branch, so it
needs no exception to the main ruleset.

CI also rehearses the install a user performs, which it previously did not.
The procedure lives in CLAUDE.md, which already owns what a release is. The command points at it rather than restating it, so the two cannot drift.

.claude/commands/ is carved out of the rule against installing anything into this repo. It holds tooling for working on the repo, which ships to nobody and mirrors no part of the plugin.
… request

The Gherkin suite in `e2e/` runs the real review server and CLI with a mock
agent in the agent's seat, so the whole round-trip is exercised without a model
or an API key. The `@browser` scenarios drive the workspace in Chromium.

It is the one directory with a `package.json`, kept outside `plugins/` so the
plugin stays dependency-free. CI runs it as a matrix over the Claude and Codex
profiles and publishes each run's summary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The × on a card and Clear all now reach every comment on the list, whatever
state it is in — queued, delivered or addressed. Asking for a delivered comment
back by writing a reply is gone; the card goes, and the agent carries on with
whatever it was already handed.

Removal is a request of its own, because a save that simply omits a comment
reads as a stale tab and is merged straight back. The card disappears on the
click and comes back, with a toast saying so, if the server refuses. Taking
back a reply works the same way, over a new `/comments/unreply` endpoint that
refuses once something has been said over it.

Also here:

- `unanswered` compares what the agent has said against the reviewer's last
  word it was actually handed. Every tick re-stamps `deliveredAt`, so measuring
  against delivery alone cleared a question the moment the reviewer wrote
  anything at all.
- `liveStores` compares real paths, so a store reached under a symlinked prefix
  is not counted twice.
- The Addressed group reads each comment's own `closedAt`, so taking the newest
  one off no longer promotes the batch beneath it back into view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The frame is sized to the page's full height so comment coordinates are
document coordinates. Inside the frame that makes the CSS viewport the whole
document, so anything the app pins to its viewport — a fixed overlay, a native
dialog's top layer — lands screens away from what is on screen.

While such an overlay is up, hold the frame at one viewport tall and move the
outer scroll into the page itself; hand it back and refit when the overlay
closes. Marks convert through the frame's own scroll at both edges, so they
hold in either state.

Opening an overlay changes no layout the body's ResizeObserver can see, so the
mutation watcher asks directly whether the state flipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The panel is rebuilt from scratch on every change, so a card that moves — the
question you have just answered leaving the top of the list — arrived in its
new place with nothing to follow. Measure every row before the rebuild and play
each one back from where it was. A moving card rides over the rows it passes,
so which one is on top does not depend on which way it is travelling. Nothing
moves under `prefers-reduced-motion: reduce`.

Emptying the list also scrolled the panel back to the top on every render. Put
the reader's scroll position back before anything is measured against it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	.github/PULL_REQUEST_TEMPLATE.md
#	.github/workflows/release.yml
#	CLAUDE.md
#	CONTRIBUTING.md
With two agent sessions open in the same directory, the Stop hook gated
whichever one was ending its turn, and told the uninvolved session it
owed answers on comments it had never seen — ids and close command
included. Delivery recorded when a comment was handed over but not to
whom, so every session standing in the directory looked equally
responsible for every live review under it.

Delivery now records the identity the watcher was started with
(`watch --session <id>`, supplied by the host adapter — on Claude Code,
CLAUDE_CODE_SESSION_ID, which is the same id the Stop payload carries),
and the gate asks `unanswered --session <id>`, so it holds only the
session whose watcher took the round. A delivery recorded without an
identity gates no one: the failure mode is a round that waits, never a
wrong session held.

The same discovery fed `watch --all`, so a second session's sweep could
cover the first session's reviews and take delivery of its comments.
The sweep now leaves a store alone while its `watching` heartbeat is
fresh, and a `--file` name remains the deliberate takeover.

Rules 16 and 17 of contracts/review-loop.md state both. The event-log
redesign that would also close the simultaneous-adoption race is
recorded in docs/review-wishlist.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A round that lands now brings the agent's account of it with it. `publish
--summary` records what it would have said in chat; the banner shows that
under a green line saying the round is done, on an accordion chevron that
opens the way the reviewer last left it. The line no longer names a version,
because a live review never had one — that banner had never appeared there.

A question can carry the answers to it. `reply --option … --recommend n`
puts them on the comment as buttons, one marked Recommended, and pressing
one answers with those words. The box to type something else stays.

Clear all no longer takes what the reviewer is still working on: it clears
the addressed comments, and a checkbox on a plain confirmation takes the
open ones too. The box is off every time it is asked.

Also: the comments panel resizes on its inner edge and remembers the width;
a general comment has a Save button and Enter saves it as a draft rather
than sending it; a comment closed while the reviewer watched no longer falls
straight into the folded Earlier group, because the close stamp now reaches
the list; a question stops animating progress, since it waits on the
reviewer rather than on the agent; and the parked start tool's catalogue
names the stacks the template actually ships.

Driven end to end through the workspace itself — the review that produced
every comment above ran in vstack, on vstack. Seven browser scenarios cover
the new surface, including the two long-page bugs that had no test:
annotating after the page scrolls in its own window, and an overlay opening
where the reviewer is looking. Each was checked against the reverted fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The security, Scorecard and Sonar badges are commented out until each one
reports green again, with the reason for each beside it. The pitch drops the
in-joke filenames for what the reader actually recognises.
…brow

Options were fixed at 12.5px, so in the panel thread they sat a point larger
than the message they answer. They now inherit from that message: 11.5px in
the panel, 12.5px in the composer.

Recommended moves from a badge beside the answer to an eyebrow above it, in
red, over a red border on the option it marks. The border rests on
--brand-line so hovering it still reads as a change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The demo GIF was made by hand, so redoing it meant rebuilding the whole
rig. docs/demo/record-demo.mjs now does it in one command: publish v1 to a
real review server, drive the shipped workspace in headless Chrome, take
delivery of the round through the review CLI, publish v2, and assemble the
frames with ffmpeg. Nothing is staged or faked, so a change to the
workspace shows up in the next recording.

The demo itself now shows all three ways of marking a page — a point
comment, an area comment, and a strike through words — and v2 answers each
one and nothing else. The recording refuses to send unless all three marks
reached the review, because a gesture that silently captured nothing had
already shipped a demo of two.

CLAUDE.md carries the pacing and the parts that are load-bearing: PNG
frames rather than JPEG, a cursor drawn from real mouse events, watch
--stream so the top bar stays linked, and the layout space that keeps a
mark's note off what the next mark is aimed at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The update banner only knew how to recognise a Claude Code install, and the
Codex profile had the check switched off, so a Codex copy stayed on
whatever release it was installed at with nothing to say so. Codex installs
are now recognised by the version directory Codex unpacks them into, and
the banner names the two commands that take the update.

An install sitting behind a symlinked path — /var and /tmp on macOS, or a
home directory that has moved — was not recognised as an install either,
and got no banner.

Covered by a new test, tests/update-check.mjs, which CI now runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ists

design/ holds the prototypes, the design guide and its tokens. CLAUDE.md
gains the rules that go with them: treat a tweak as a delta rather than a
redesign, reuse the nearest comparable composition, and give a composition
one owner once it appears twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
design/ was a copy of the template repo's design folder, pointing at
apps/frontend and specs/ — paths that exist in a scaffolded project, not
here — while the shell decided the product's colours on its own. Two token
files, each calling itself the source.

design/tokens.css is now the source, and every page picks up the guide's
Cavalry brand: purple-cast neutrals in place of the cool greys, and the
brand red at the step that holds its contrast. Nothing moved and nothing
was renamed, so the 1,227 places a page asks for a role are untouched —
only the values behind them changed.

The guide needed extending on two counts, both of which it allows for. The
shell has three neutral steps where it had two, so it gains --gray-100,
--gray-700 and a --ground role for the surface a window sits on. And it
described dark as a later remap, while all five pages already render both,
so dark is part of the source now.

Two things it asks for cannot follow. A stamped page fetches nothing, so
the type scale is the guide's but the families stay the system stack, and
the shell holds literal values rather than importing them. That copy is
the drift risk, so tests/design-tokens.mjs resolves both files and fails
when they disagree — it already caught the mono stack differing by one
fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
)
}

const heading = new RegExp(`^## ${declared.replace(/\./g, "\\.")}\\b`, "m")
// Everything from this version's heading up to the next one. Written by a
// person, so it is published as-is rather than regenerated from commits.
const changelog = readFileSync(CHANGELOG, "utf8")
const heading = new RegExp(`^## ${version.replace(/\./g, "\\.")}\\b.*$`, "m")
Comment thread e2e/support/world.mjs
const appPort = this.port + 1
this.app = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/html' })
res.end(`<!doctype html><title>Fixture</title><h1>${req.url}</h1><a href="/settings">Settings</a>`)
// a draft and the reviewer may rewrite it however they like.
if (!stored.sentAt) {
for (const [key, value] of Object.entries(raw)) {
if (!OWNED.includes(key) && key !== 'replies') stored[key] = value
stacks in the order things entered it: one promoted before a dialog
would sit under it. Older browsers have no popover and lose nothing but
the stacking. */
try { el.showPopover() } catch {}
stacks in the order things entered it: one promoted before a dialog
would sit under it. Older browsers have no popover and lose nothing but
the stacking. */
try { el.showPopover() } catch {}
stacks in the order things entered it: one promoted before a dialog
would sit under it. Older browsers have no popover and lose nothing but
the stacking. */
try { el.showPopover() } catch {}
stacks in the order things entered it: one promoted before a dialog
would sit under it. Older browsers have no popover and lose nothing but
the stacking. */
try { el.showPopover() } catch {}
stacks in the order things entered it: one promoted before a dialog
would sit under it. Older browsers have no popover and lose nothing but
the stacking. */
try { el.showPopover() } catch {}
@DeyangChan
DeyangChan merged commit feecb5d into main Aug 9, 2026
12 of 13 checks passed
@DeyangChan
DeyangChan deleted the release/v6.4.0 branch August 9, 2026 14: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.

2 participants