Repository navigation
Review desk: tick, comment and submit each stop; the agent wakes on submit (bin/vh desk) - #54
Merged
Merged
Conversation
…wakes on submit (bin/vh desk) The static review page could only be read: replies went back through chat as codes, outlines and captions were raw .md links, and every page was a one-off. The maintainer tried a prototype desk on a real film, submitted through it twice, and asked for an automatic hand-back, outlines split by content, reusable infrastructure, studio as its home, and zh/en by the first request. - tools/desk/: stdlib server on 127.0.0.1 (Host check, per-run token, same origin, project files only, HTTP Range), a reader that turns the project's own files into the desk's data and renders anything it can't parse as escaped markdown with a warning, feedback that lands in out/review/feedback/ and verbatim in REVIEW.md, and a watcher. - bin/vh desk <project> | desk wait | desk feedback | desk data; bin/vh new --lang zh|en writes the BRIEF's new "Review language" line, which the desk and bin/vh review (when the gate JSON has no lang) follow. - The data contract in tools/desk/README.md, including three optional gate JSON keys (listen, music labels, film) that replace the prototype's hardcoded bits; templates updated to match (BRIEF Outline table named by content, SCRIPT caption table with 术语, DECISIONS strikethrough, NOTES 上屏说法). - Effort wiring: studio stops in the desk with the watcher armed in the background, chat as the fallback (CLAUDE.md, playbook/01, bin/vh effort). - Docs learned on the film: outline names by content, a concept serves the stated focus, endings that lift the theme without slogans, the optional 术语卡, at least one light look, and sweeping stale lines after a gate. - CI: the reader on committed zh/en fixtures (incl. the fallback), a server on a free port, a submission waking the watcher and landing in REVIEW.md, the timeout, new --lang and review's language default.
…ys why a start failed
On GitHub's macOS runner the desk server never printed its address within the
test's 10 s wait, so CI killed it and reported an empty reason. The server was
stuck in http.server.HTTPServer.server_bind, which calls
socket.getfqdn("127.0.0.1"), a reverse DNS lookup that can stall there. The
desk only answers on 127.0.0.1, so Desk.server_bind now binds and sets the
server name directly (a bind takes no time).
tools/ci.sh: the startup wait polls for up to 30 s and stops early if the
server exits; on failure it says whether the server was still running or
exited (with its code) and prints its output, or "(none)".
…nd order, hand-over, copies, bad requests) - /p/ serves every project file with "Content-Security-Policy: sandbox allow-scripts allow-popups" (no allow-same-origin). A script SVG or HTML in the project, opened from the desk, could read "/" for the token and post a forged "verbatim" line into REVIEW.md; in an opaque origin it can't (checked in a browser: origin=null, the fetch fails, nothing written). - Hidden files and *.env are refused on the resolved path too, ignoring case (a symlink to .env, keys.ENV). - "This round" is the last gate page in review order (1, E0, E1, 2, E2, E3, E4, 3, E5, then 1 < 1b < 1c), not the newest file, so cp -R or touching an old gate JSON can't make a rejected page current again. - bin/vh desk wait hands over at once a submission for the current page that came in before it was armed (after a timeout) and was never handed over; what wait or desk feedback printed is recorded in feedback/.consumed. - .desk.json and .listening record the project's real path; a copied project starts its own server, and a .listening from elsewhere (the prototype's) doesn't make the desk say an agent is waiting. - Malformed submissions get a 400 instead of a dropped connection (defaulted / lang of the wrong type, deep nesting, a body shorter than its Content-Length); the token is compared as bytes (a non-ASCII one is a 403); a 30 s socket timeout frees a stalled thread. - render_md: inline patterns stop at their delimiter (linear), lines over 5,000 characters skip inline markup, quotes nest at most 8 deep. - One language rule for the desk and bin/vh review: the page's gate JSON lang, else the BRIEF's Review language, else zh. - latest() orders -2 before -10 within one second. - The night-palette note leaves playbook/03's default ban list and becomes a look-dev line in playbook/01: offer at least one non-night variant. tools/ci.sh covers each: the sandbox header on /p/ (an SVG too), the symlink and upper-case .ENV, the round order after a touch, the language rule in both tools, long and deeply nested markdown in bounded time, -10 after -2, the malformed bodies and token, a submission made before wait, desk feedback consuming, and a copied project with a foreign .listening.
…watcher snapshots before the hand-over Review order alone picked the wrong page when work went back a stop: gate 2 then a new concept page 1b stayed on 2, gate 3 then a voice page E3b stayed on 3, and an upper-case 2B or a custom name like "final" sorted first and could never be current; pending() then filtered on the wrong round too. - bin/vh review <project> <n> writes <n> to out/review/.current. The desk and bin/vh desk wait read it first; review order (gate_pages, page letters now compared case-insensitively) is only the fallback when .current is missing or names a page that is gone. - bin/vh desk wait takes its snapshot of existing submissions before the early hand-over check, so one landing between the two is no longer lost. - tools/desk/README.md: the .current rule, and a limits section (a stale tab submits to its own round; the .consumed lock is per process, so a race can hand a submission over twice but never drops one). playbook/01 and the CHANGELOG follow. - CI: 2 -> 1b, 3 -> E3b, "final" and "2B" become current after bin/vh review; pending() follows .current; a removed .current page falls back to review order.
…e page's on-disk name, this round by default, valid ids - feedback._write used a fixed "<file>.part" temp name: two bin/vh desk processes writing .consumed (or two waits writing .listening) at once made the slower os.replace raise FileNotFoundError and exit 1 after printing the feedback. Each write now gets its own mkstemp file (keeping the target's mode, so REVIEW.md stays 0644), and .consumed's read-modify-write holds an flock on .consumed.lock. Worst case is a duplicate hand-over, never a crash. - bin/vh review <project> 1D on APFS found gate-1d.json but wrote .current "1D" and a separate gate-1D.html, which the desk then ignored. The page id now comes from the file found on disk (exact match first, then the single case-insensitive one) for both .current and the html. - bin/vh review <project> with no page renders this round (.current, else the last in review order) instead of the newest file, and says when a gate file is newer than it. - Page ids are checked with the desk's rule (1-24 letters, digits, - or _); anything else stops with a message. - Gate files whose real path is outside the project are skipped. The gate page helpers (current_page, gate_pages) move to tools/review.py, which owns the gate files and .current; the reader reuses them. - tools/desk/README.md: limits and the naming rule; CHANGELOG follows. - CI: 8 processes writing .listening and .consumed at once all exit 0 with all 320 names kept and no temp files left (the old _write fails 7 of 8); 1D renders as 1d; no gate renders this round and names a newer file; a 25-character id and a gate symlink out of the project are refused.
ZLHad
marked this pull request as ready for review
October 4, 2026 04:12
ZLHad
enabled auto-merge (squash)
October 4, 2026 04:12
ZLHad
added a commit
that referenced
this pull request
Oct 4, 2026
…ed; reviewer nits The previous commit replaced everything between the new entry and 'Videos move out of git', which removed main's review-desk and 'what one film's gates taught' entries. CHANGELOG is now main's file with only the new entry added. Nits: the Iwanau 37% film's picture is marked unseen; the 0.92 glyph/size ratio is a rule of thumb; the full-screen reading is phrased as an inference; the TYPE line says to switch to feed with --watch feed; Kepler marked as the film's claim; the like range leaves out the film posted that day.
ZLHad
added a commit
that referenced
this pull request
Oct 4, 2026
…ubmit (bin/vh desk) (#54) * Review desk: tick, comment and submit each stop in a page; the agent wakes on submit (bin/vh desk) The static review page could only be read: replies went back through chat as codes, outlines and captions were raw .md links, and every page was a one-off. The maintainer tried a prototype desk on a real film, submitted through it twice, and asked for an automatic hand-back, outlines split by content, reusable infrastructure, studio as its home, and zh/en by the first request. - tools/desk/: stdlib server on 127.0.0.1 (Host check, per-run token, same origin, project files only, HTTP Range), a reader that turns the project's own files into the desk's data and renders anything it can't parse as escaped markdown with a warning, feedback that lands in out/review/feedback/ and verbatim in REVIEW.md, and a watcher. - bin/vh desk <project> | desk wait | desk feedback | desk data; bin/vh new --lang zh|en writes the BRIEF's new "Review language" line, which the desk and bin/vh review (when the gate JSON has no lang) follow. - The data contract in tools/desk/README.md, including three optional gate JSON keys (listen, music labels, film) that replace the prototype's hardcoded bits; templates updated to match (BRIEF Outline table named by content, SCRIPT caption table with 术语, DECISIONS strikethrough, NOTES 上屏说法). - Effort wiring: studio stops in the desk with the watcher armed in the background, chat as the fallback (CLAUDE.md, playbook/01, bin/vh effort). - Docs learned on the film: outline names by content, a concept serves the stated focus, endings that lift the theme without slogans, the optional 术语卡, at least one light look, and sweeping stale lines after a gate. - CI: the reader on committed zh/en fixtures (incl. the fallback), a server on a free port, a submission waking the watcher and landing in REVIEW.md, the timeout, new --lang and review's language default. * desk: skip the reverse DNS lookup at bind; CI waits up to 30 s and says why a start failed On GitHub's macOS runner the desk server never printed its address within the test's 10 s wait, so CI killed it and reported an empty reason. The server was stuck in http.server.HTTPServer.server_bind, which calls socket.getfqdn("127.0.0.1"), a reverse DNS lookup that can stall there. The desk only answers on 127.0.0.1, so Desk.server_bind now binds and sets the server name directly (a bind takes no time). tools/ci.sh: the startup wait polls for up to 30 s and stops early if the server exits; on failure it says whether the server was still running or exited (with its code) and prints its output, or "(none)". * desk: fixes from the independent review (sandboxed project files, round order, hand-over, copies, bad requests) - /p/ serves every project file with "Content-Security-Policy: sandbox allow-scripts allow-popups" (no allow-same-origin). A script SVG or HTML in the project, opened from the desk, could read "/" for the token and post a forged "verbatim" line into REVIEW.md; in an opaque origin it can't (checked in a browser: origin=null, the fetch fails, nothing written). - Hidden files and *.env are refused on the resolved path too, ignoring case (a symlink to .env, keys.ENV). - "This round" is the last gate page in review order (1, E0, E1, 2, E2, E3, E4, 3, E5, then 1 < 1b < 1c), not the newest file, so cp -R or touching an old gate JSON can't make a rejected page current again. - bin/vh desk wait hands over at once a submission for the current page that came in before it was armed (after a timeout) and was never handed over; what wait or desk feedback printed is recorded in feedback/.consumed. - .desk.json and .listening record the project's real path; a copied project starts its own server, and a .listening from elsewhere (the prototype's) doesn't make the desk say an agent is waiting. - Malformed submissions get a 400 instead of a dropped connection (defaulted / lang of the wrong type, deep nesting, a body shorter than its Content-Length); the token is compared as bytes (a non-ASCII one is a 403); a 30 s socket timeout frees a stalled thread. - render_md: inline patterns stop at their delimiter (linear), lines over 5,000 characters skip inline markup, quotes nest at most 8 deep. - One language rule for the desk and bin/vh review: the page's gate JSON lang, else the BRIEF's Review language, else zh. - latest() orders -2 before -10 within one second. - The night-palette note leaves playbook/03's default ban list and becomes a look-dev line in playbook/01: offer at least one non-night variant. tools/ci.sh covers each: the sandbox header on /p/ (an SVG too), the symlink and upper-case .ENV, the round order after a touch, the language rule in both tools, long and deeply nested markdown in bounded time, -10 after -2, the malformed bodies and token, a submission made before wait, desk feedback consuming, and a copied project with a foreign .listening. * desk: this round is the page bin/vh review made last (.current); the watcher snapshots before the hand-over Review order alone picked the wrong page when work went back a stop: gate 2 then a new concept page 1b stayed on 2, gate 3 then a voice page E3b stayed on 3, and an upper-case 2B or a custom name like "final" sorted first and could never be current; pending() then filtered on the wrong round too. - bin/vh review <project> <n> writes <n> to out/review/.current. The desk and bin/vh desk wait read it first; review order (gate_pages, page letters now compared case-insensitively) is only the fallback when .current is missing or names a page that is gone. - bin/vh desk wait takes its snapshot of existing submissions before the early hand-over check, so one landing between the two is no longer lost. - tools/desk/README.md: the .current rule, and a limits section (a stale tab submits to its own round; the .consumed lock is per process, so a race can hand a submission over twice but never drops one). playbook/01 and the CHANGELOG follow. - CI: 2 -> 1b, 3 -> E3b, "final" and "2B" become current after bin/vh review; pending() follows .current; a removed .current page falls back to review order. * desk: unique temp files and a locked .consumed; bin/vh review uses the page's on-disk name, this round by default, valid ids - feedback._write used a fixed "<file>.part" temp name: two bin/vh desk processes writing .consumed (or two waits writing .listening) at once made the slower os.replace raise FileNotFoundError and exit 1 after printing the feedback. Each write now gets its own mkstemp file (keeping the target's mode, so REVIEW.md stays 0644), and .consumed's read-modify-write holds an flock on .consumed.lock. Worst case is a duplicate hand-over, never a crash. - bin/vh review <project> 1D on APFS found gate-1d.json but wrote .current "1D" and a separate gate-1D.html, which the desk then ignored. The page id now comes from the file found on disk (exact match first, then the single case-insensitive one) for both .current and the html. - bin/vh review <project> with no page renders this round (.current, else the last in review order) instead of the newest file, and says when a gate file is newer than it. - Page ids are checked with the desk's rule (1-24 letters, digits, - or _); anything else stops with a message. - Gate files whose real path is outside the project are skipped. The gate page helpers (current_page, gate_pages) move to tools/review.py, which owns the gate files and .current; the reader reuses them. - tools/desk/README.md: limits and the naming rule; CHANGELOG follows. - CI: 8 processes writing .listening and .consumed at once all exit 0 with all 320 names kept and no temp files left (the old _write fails 7 of 8); 1D renders as 1d; no gate renders this round and names a newer file; a 25-character id and a gate symlink out of the project are refused.
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.
What
The review desk becomes a module of the repo:
bin/vh desk <project>serves a local page (127.0.0.1 only) where the reviewer marks any item ok / change / question, writes a line or a "change to", and submits once.bin/vh desk waitblocks until a submission arrives and prints it, so an agent that armed it in the background carries on without the reviewer going back to the chat. It is the default review surface atstudio.Same PR: a few small doc improvements learned while producing one film.
Why
After two static review pages on a real film, the maintainer found them hard to use: the outline, captions and stages were unclear (some were raw
.mdlinks), there was no way to tick or comment in place, replies had to be typed in chat as codes like "1 S1", and every page was a one-off. A prototype desk fixed that; after submitting through it twice the maintainer asked for:studio, in Chinese or English chosen from the first request.Changes
tools/desk/(stdlib Python, offline, no CDN, light and dark, zh and en)server.py: answers only its own Host (no DNS rebinding); a POST needs the page's per-run token (compared as bytes), JSON, and a same-origin Origin when one is sent./p/serves only ordinary files inside the project, with HTTP Range for video and audio. No.., no symlink escapes; hidden files and*.envare refused on the requested name and on the resolved file, ignoring case./p/response carriesContent-Security-Policy: sandbox allow-scripts allow-popups(noallow-same-origin). A script HTML or SVG in the project therefore runs in an opaque origin and can't read the token or post.out/review/.desk.jsonrecords the project's real path, so a copied project starts its own. Stopping the server removes the file.reader.py: reads the project's own files into the desk's data (file-is-truth: nothing is stored, every refresh re-reads). "This round" is the last gate page in review order (stop, then page), not the newest file. Anything it can't parse comes back as escaped, formatted markdown with a warning, so older projects without the tables still show every document. Inline markdown patterns run in linear time and quote nesting is capped.feedback.py: validates a submission (a real gate page, its decisions and options, field types, sizes, targets), writesout/review/feedback/<round>-<time>.md, then a dated, fenced section inREVIEW.mdwith the reviewer's words verbatim (placed before the template's reference sections), then the.jsonthat the watcher waits for.waitordesk feedbackprinted is recorded infeedback/.consumed. A submission for the current page that arrived beforewaitwas armed (after a timeout) is handed over at once..listeningrecords the project's real path, so a copied or stale flag doesn't count.static/: ten views: this round, concept and outline, captions or narration, storyboard synced to its video, sound, fact check, timecoded film notes, decision ledger, history, and documents.README.md: the data contract (below).bin/vhdesk <project> [--port N],desk wait <project> [--timeout s],desk feedback <project> [--round r] [--json],desk data <project>. The help lines follow the dispatcher;bin/vh desk -hgoes through the same-hhandling as other commands (bin/vh <command> -h prints its usage instead of running the command #50).new --lang zh|enwrites a new BRIEF line,Review language. One rule for the desk andbin/vh review: the page's gate JSONlang, else the BRIEF line, else zh.bin/vh reviewis otherwise unchanged.Effort wiring
quickhas no gates;standardkeeps the static page, with the desk optional;studiouses the desk and arms the watcher after each stop, falling back to chat for agents without background wake-ups.playbook/01-pipeline.md(new 审阅台 section) andbin/vh new's next-step line.Templates
Review languageline, and an Outline table named by content with an optional 标签 column.Docs learned on the film (defaults and examples, not rules)
The data contract, in brief
tools/desk/README.mdlists every input the desk reads and what it writes:# 段落 标签 环节 时间|小节 观众 关键画面.# 段落 时间|小节 术语 字幕|旁白 画面 素材.~~…~~marks an entry as superseded.shots.json: the same filebin/vh storyboardreads.bin/vh reviewreads, plus three optional keys:listen,music(audio, beat map, section labels) andfilm. These replace the prototype's hardcoded listen prompts, section names, 19.2 s duration and frame folder.Verification
tools/ci.sh --committedandVH_BASH=/bin/bash tools/ci.sh --committed(macOS, bash 3.2): all checks pass on both, 76 ✓ each. shellcheck and pyflakes aren't installed locally, so they were run separately through throwaway uv environments (shellcheck -S warningonbin/vhand every*.sh; pyflakes ontools/desk/*.pyandtools/review.py): clean.desk_checks):tools/desk/fixtures/zhanden..gitignorenow lets theirout/in. The en fixture's NOTES has no table, which exercises the fallback.desk wait(exit 0) and lands verbatim in REVIEW.md;desk feedback, a second start, cleanup on stop, the wait timeout (exit 3),new --lang, andbin/vh review's language default./p/sandbox header (an SVG too), a symlink to.envandkeys.ENV, malformed bodies and a non-ASCII token, the round order after a touch, the language rule in both tools, long and deeply nested markdown in bounded time,-10after-2, a submission made beforewait,desk feedbackconsuming, and a copied project with a foreign.listening./p/, reportedorigin=null; its fetch of/failed, and nothing was written.projects/was written) renders all views, including the previous round's feedback.