Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/scripts/changelog.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Finding a release's section in CHANGELOG.md.
*
* Both the pull-request check and the release publisher need the same answer,
* so the heading is recognised in one place. Matching is done on plain strings
* rather than by building a pattern out of the version: a version is data, and
* a pattern built from data is only ever as correct as its escaping.
*/

/** True when this line is the heading for exactly this version. A heading runs
* `## 6.4.0 — 2026-08-09`, so anything may follow the number as long as the
* number itself ends there — `## 6.4.01` is a different release. */
export const isHeadingFor = (line, version) => {
const heading = `## ${version}`
if (!line.startsWith(heading)) return false
const next = line.slice(heading.length)[0]
return next === undefined || !(next === "." || (next >= "0" && next <= "9"))
}

/** The line index of that heading, or -1. */
export const headingIndex = (lines, version) =>
lines.findIndex(line => isHeadingFor(line, version))

/**
* Everything under this version's heading, up to the next release heading.
* Null when the changelog has no entry for it.
*/
export const sectionFor = (changelog, version) => {
const lines = changelog.split("\n")
const start = headingIndex(lines, version)
if (start === -1) return null
const rest = lines.slice(start + 1)
const next = rest.findIndex(line => line.startsWith("## "))
return (next === -1 ? rest : rest.slice(0, next)).join("\n").trim()
}
4 changes: 2 additions & 2 deletions .github/scripts/check-version.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
import { execFileSync } from "node:child_process"
import { readFileSync } from "node:fs"
import { headingIndex } from "./changelog.mjs"

const MANIFEST = "plugins/vstack/.claude-plugin/plugin.json"
const CHANGELOG = "CHANGELOG.md"
Expand Down Expand Up @@ -80,8 +81,7 @@ if (previous !== null && !isHigher(parse(declared, MANIFEST), parse(previous, `$
)
}

const heading = new RegExp(`^## ${declared.replace(/\./g, "\\.")}\\b`, "m")
if (!heading.test(readFileSync(CHANGELOG, "utf8"))) {
if (headingIndex(readFileSync(CHANGELOG, "utf8").split("\n"), declared) === -1) {
fail(
`${CHANGELOG} has no entry for ${declared}, and that entry is published as the release notes.`,
"",
Expand Down
16 changes: 5 additions & 11 deletions .github/scripts/publish-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
import { execFileSync } from "node:child_process"
import { readFileSync } from "node:fs"
import { sectionFor } from "./changelog.mjs"

const MANIFEST = "plugins/vstack/.claude-plugin/plugin.json"
const CHANGELOG = "CHANGELOG.md"
Expand All @@ -32,23 +33,16 @@ try {
// No release under that tag yet, which is the case this runs for.
}

// 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 start = changelog.search(heading)
// Everything under this version's heading. Written by a person, so it is
// published as-is rather than regenerated from commits.
const notes = sectionFor(readFileSync(CHANGELOG, "utf8"), version)

if (start === -1) {
if (notes === null) {
console.error(`${CHANGELOG} has no entry for ${version}, so there are no notes to publish.`)
console.error("A pull request cannot merge without one, so this commit did not come through one.")
process.exit(1)
}

const rest = changelog.slice(start)
const nextRelease = rest.indexOf("\n## ", 1)
const section = (nextRelease === -1 ? rest : rest.slice(0, nextRelease)).trim()
const notes = section.slice(section.indexOf("\n") + 1).trim()

gh(
"release", "create", tag,
"--target", process.env.GITHUB_SHA,
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@
# Recording the README demo installs playwright-core at the repo root.
node_modules/

# A host writes this into the plugin directory when it installs from this clone
# as a local marketplace, which is how the plugin is tested before it ships. It
# is that machine's install bookkeeping, not part of the plugin.
plugins/vstack/.orphaned_at

.DS_Store
.env
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,85 @@ The version in `plugins/vstack/.claude-plugin/plugin.json` is what your host
compares against to decide an update is available. See the release checklist in
[`CONTRIBUTING.md`](CONTRIBUTING.md).

## 6.7.0 — 2026-08-10

**Fixed**

- **Comments no longer pile up undelivered when Codex stops polling.** Codex
receives comments through a bounded wait that returns every 25 seconds, and
the review continues only while the agent keeps calling it. Each wait now ends
by printing the exact command that resumes it, so a loop that was about to be
dropped names its own next step. Taking a comment prints the same line, since
answering one comment is not the end of the review.
- **`unanswered` no longer reports all-clear over a review nobody is watching.**
It now names a live review that has comments waiting with no watcher behind
it, and gives the command that starts watching again. Codex runs this check
before ending a turn, and it previously said nothing while a queue sat
undelivered. Comments that were never delivered still do not block the end of
a turn in Claude Code — they are reported, not gated.

## 6.6.0 — 2026-08-10

**Changed**

- **Review comments now reach the agent one at a time, in FIFO order.** New
comments wait without interrupting the comment in progress. Asking a question
releases the queue so the next ready comment can proceed; when the reviewer
answers, that thread rejoins the queue at the time of the reply.
- The workspace shows exactly one comment as in progress. Later comments and
answered threads say Queued, while questions still say that the agent is
waiting on the reviewer.

## 6.5.0 — 2026-08-10

**Changed**

- **Codex now receives review rounds through bounded foreground waits.** Its
terminal does not push a background process's output into an idle agent turn,
so keeping a second persistent shell and polling its buffer could leave a
comment marked as delivered before Codex had read it. Codex now runs a
25-second `watch --next` call, repeats it on `IDLE`, and explicitly `claim`s a
`REVIEW` offer. Until that claim succeeds, the comments remain queued.
- **Linked means a Codex consumer is still calling back.** Each bounded wait
renews a short lease which bridges normal re-arms and expires when the turn
stops. A leftover Node process can no longer keep the workspace falsely
Linked. Claude Code and Grok keep their pushed stream watcher unchanged.
- Two Codex pulls may wait on the same review safely: both see one durable
offer, and only the first claim records delivery. A push watcher remains
exclusive and cannot be taken over by a project-wide pull.

## 6.4.1 — 2026-08-09

**Fixed**

- **A reply written with paragraph breaks arrives with them.** A shell leaves
`\n` inside a quoted argument as two characters, so an agent's multi-paragraph
answer reached the comment thread with `\n` showing as text. `reply --text`,
`reply --option` and `publish --summary` now read `\n` as a line break. Write
`\\n` when you mean the two characters.
- **A comment containing a double quote could break the page it was drawn on.**
The workspace escaped `&`, `<` and `>` but not quotes, and most of what it
builds is an HTML attribute — so a quote in your own words ended the attribute
early and the rest of the note was read as markup. Quotes are now escaped
everywhere the workspace writes them. The story map and the build board escape
the ids they put in attributes for the same reason.
- **A save could change every object in the review server, not just its own
comment.** A saved comment's fields were copied across by name, and a name
like `__proto__` reaches the prototype rather than the object. Those names are
now skipped.
- **The update check no longer keeps its cache in the shared temp directory.**
On a machine with more than one account, anyone could create that file first
and own what the check then wrote to it. It now lives in `~/.vstack/`, owned
by the reader and readable only by them. The move resets what the old cache
held, so a release you had already dismissed can ask once more.
- **A failed action shows what went wrong without the stack behind it.** The
message is the part a reader can act on and is still shown in full.
- **A second session starting at the same moment cannot reset the other's
counter.** The bridge's sequence file is now created in one step rather than
checked and then written.
- The phase-preview comparison reads `</script >` and `</style >` as the closing
tags they are, and strips nested comment markers until none are left.

## 6.4.0 — 2026-08-09

**Changed**
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
# Visual Stack

[![CI](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/ci.yml/badge.svg)](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/ci.yml)
[![Security](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/security.yml/badge.svg)](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/security.yml)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Cavalry-Collective/visual-stack/badge)](https://scorecard.dev/viewer/?uri=github.com/Cavalry-Collective/visual-stack)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

<!-- Hidden until each one reports green again.
Security workflow: failing.
OpenSSF Scorecard: "invalid repo path" — the project is not registered with scorecard.dev.
Sonar quality gate: not computed. Sonar security rating: E.
Sonar has not analysed the repository since v5.0.0: the quality gate is
not computed and the security rating is still the E it was left on.

[![Security](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/security.yml/badge.svg)](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/security.yml)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Cavalry-Collective/visual-stack/badge)](https://scorecard.dev/viewer/?uri=github.com/Cavalry-Collective/visual-stack)
[![Quality gate](https://sonarcloud.io/api/project_badges/measure?project=Cavalry-Collective_visual-stack&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Cavalry-Collective_visual-stack)
[![Security rating](https://sonarcloud.io/api/project_badges/measure?project=Cavalry-Collective_visual-stack&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=Cavalry-Collective_visual-stack)
-->
Expand Down Expand Up @@ -53,7 +52,7 @@ Then run:
/vstack:review Wireframe a desktop personal task manager with minimal aesthetics.
```

### Codex
### Codex<sup>1</sup>

Install, in your terminal:

Expand All @@ -68,6 +67,8 @@ Then run:
$vstack:review Wireframe a desktop personal task manager with minimal aesthetics.
```

<sup>1</sup> Codex support is experimental. Codex does not come with a background monitor tool that allows two-way communication with Visual Stack. A deterministic polling workaround is used, but occasionally the agent stops polling prematurely. If that happens, prompt the agent to resume watching.

## What you can do

- Work in a familiar, Figma-like interface.
Expand Down Expand Up @@ -100,13 +101,13 @@ No scrolling back through the chat. No screenshot graveyard on your desktop. No

### Live Link

Each workspace is linked to one agent session. The link holds while that session is active, its heartbeat is less than 15 seconds old, and every submitted review round has been claimed.
Each workspace is linked to one agent session. The link holds while that session is active and its heartbeat is less than 15 seconds old. Comments wait in one FIFO, and only the active comment is in the agent's hands.

![The workspace page in a browser tab talks over http and SSE to the review server on 127.0.0.1. The server reads and writes a store on disk holding the state, the versions, the comments, the rounds, and the files that carry the link. The agent session watches and writes the same store.](docs/assets/live-link.svg)

### Review Lifecycle

![Your comments are submitted as one review round. The agent claims the round and reads its brief, asking for clarification when a comment is unclear. Comments sent while the round is in progress join it. Publishing is blocked until every comment has been applied, answered, or dismissed, and the published version appears in the same workspace.](docs/assets/review-lifecycle.svg)
![Comments enter one FIFO queue. The agent receives one comment at a time, so later comments never interrupt active work. Asking a question releases the queue while that thread waits for an answer, and the answered thread rejoins in arrival order. Each completed comment publishes into the same workspace.](docs/assets/review-lifecycle.svg)

## Security

Expand Down
Loading