v6.4.0 — one list of comments, a round that must be answered, and the design guide's palette - #6
Merged
Conversation
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") |
| 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 {} |
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships everything on
mainsince v5.0.0 as v6.4.0. Both host manifestsdeclare
6.4.0andCHANGELOG.mdcarries an entry for every version inbetween, 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 --addressedis nowpublish --close. Comments live incomments.json, the brief inbrief.md,and a version file is a label rather than a record of comments.
The review loop finishes what it starts.
unansweredreports a comment theagent 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 --summaryrecords the account of theround,
reply --option … --recommendturns 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, andtests/design-tokens.mjsfails when the shell driftsfrom 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
review-lifecycle,host-profiles,workdir,round-gate,update-check,design-tokens.build-shell.mjs checkreports no drift across all five stamped pages.claude plugin validatepasses--stricton both the marketplace manifestand the plugin manifest.
🤖 Generated with Claude Code