Skip to content

Review desk: tick, comment and submit each stop; the agent wakes on submit (bin/vh desk) - #54

Merged
ZLHad merged 6 commits into
mainfrom
claude/review-desk
Oct 4, 2026
Merged

ZLHad merged 6 commits into
mainfrom
claude/review-desk

Conversation

@ZLHad

@ZLHad ZLHad commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

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 wait blocks 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 at studio.

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 .md links), 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:

  • outlines split by content, not by 起承转合 ("一般我们还是放内容划分段落阶段的");
  • an automatic hand-back ("返回还是要手动给你触发,我觉得可以加一个监控");
  • basic infrastructure that other projects can reuse;
  • the desk at 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 *.env are refused on the requested name and on the resolved file, ignoring case.
    • Every /p/ response carries Content-Security-Policy: sandbox allow-scripts allow-popups (no allow-same-origin). A script HTML or SVG in the project therefore runs in an opaque origin and can't read the token or post.
    • Malformed bodies get a 400, and a 30 s socket timeout frees stalled threads.
    • A second start prints the running server's URL; out/review/.desk.json records 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), writes out/review/feedback/<round>-<time>.md, then a dated, fenced section in REVIEW.md with the reviewer's words verbatim (placed before the template's reference sections), then the .json that the watcher waits for.
    • What wait or desk feedback printed is recorded in feedback/.consumed. A submission for the current page that arrived before wait was armed (after a timeout) is handed over at once.
    • .listening records 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/vh

  • desk <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 -h goes through the same -h handling as other commands (bin/vh <command> -h prints its usage instead of running the command #50).
  • new --lang zh|en writes a new BRIEF line, Review language. One rule for the desk and bin/vh review: the page's gate JSON lang, else the BRIEF line, else zh. bin/vh review is otherwise unchanged.

Effort wiring

  • CLAUDE.md gets a 在哪审 row in the effort table: quick has no gates; standard keeps the static page, with the desk optional; studio uses the desk and arms the watcher after each stop, falling back to chat for agents without background wake-ups.
  • The same rule is written into step 5, director mode, playbook/01-pipeline.md (new 审阅台 section) and bin/vh new's next-step line.
  • AGENTS.md is regenerated.

Templates

  • BRIEF: the Review language line, and an Outline table named by content with an optional 标签 column.
  • SCRIPT: the caption-table variant, with an optional 术语 column.
  • DECISIONS: strike through superseded entries.
  • NOTES: an optional 上屏说法 column.

Docs learned on the film (defaults and examples, not rules)

  • Outline segments are named by content or stage; 起承转合 is at most a small tag. Changed in playbook/09 §3 and §9, the playbook/01 director table, and the BRIEF template.
  • playbook/12 §5 gets check 7: the concept serves the focus the user named. The example is the rejected "两个星座" pun. §4 adds a plain, direct card when the user names the focus.
  • playbook/09: an ending can lift the theme without a slogan by answering the hook's question again at a human level.
  • video-types/02: an optional 术语卡 (a small term label, then a plain-language caption). English abbreviations stay off screen unless asked for.
  • playbook/01 look-dev: when the look varies, at least one variant isn't a night palette (kept out of playbook/03's default ban list).
  • playbook/01: after a gate changes the concept, sweep DECISIONS and NOTES' 上屏说法 for stale lines.

The data contract, in brief

tools/desk/README.md lists every input the desk reads and what it writes:

  • BRIEF: Spec and Content bullets, plus the Outline table: # 段落 标签 环节 时间|小节 观众 关键画面.
  • SCRIPT: the first table with a 字幕 or 旁白 column: # 段落 时间|小节 术语 字幕|旁白 画面 素材.
  • NOTES: the 素材清单 table (struck rows count as dropped) and the 待核实 checkboxes.
  • DECISIONS: the ledger table, plus the bullets under "agent 定的事", where ~~…~~ marks an entry as superseded.
  • REVIEW: 原话 quotes and the 定了 / 改了 lines.
  • shots.json: the same file bin/vh storyboard reads.
  • Gate JSON: the same file bin/vh review reads, plus three optional keys: listen, music (audio, beat map, section labels) and film. These replace the prototype's hardcoded listen prompts, section names, 19.2 s duration and frame folder.
  • Output: the feedback JSON format, written by the desk.
  • English projects: every table and section above has an English header equivalent.

Verification

  • tools/ci.sh --committed and VH_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 warning on bin/vh and every *.sh; pyflakes on tools/desk/*.py and tools/review.py): clean.
  • New CI checks (desk_checks):
    • the reader on the committed fixtures tools/desk/fixtures/zh and en. .gitignore now lets their out/ in. The en fixture's NOTES has no table, which exercises the fallback.
    • a server on a free port: data with markup escaped, a 206 Range, and each refusal (other Host, paths out of the project, hidden files, no token, cross-origin, not JSON, unknown round, option or target);
    • a submission that wakes 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, and bin/vh review's language default.
    • after the independent review: the /p/ sandbox header (an SVG too), a symlink to .env and keys.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, -10 after -2, a submission made before wait, desk feedback consuming, and a copied project with a foreign .listening.
  • A hostile SVG in a scratch project, opened in a browser from /p/, reported origin=null; its fetch of / failed, and nothing was written.
  • Manual: the page was clicked through in a browser on a scratch copy of the fixture: marking, comments, the C-key timecoded note, submitting with a watcher armed (the watcher woke and REVIEW.md got the section), English, and dark mode. A scratch copy of the real project (copied so nothing under projects/ was written) renders all views, including the previous round's feedback.
  • The reader also runs under macOS's system Python 3.9.

ZLHad added 6 commits October 4, 2026 03:16
…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
ZLHad marked this pull request as ready for review October 4, 2026 04:12
@ZLHad
ZLHad enabled auto-merge (squash) October 4, 2026 04:12
@ZLHad
ZLHad merged commit 3e07fd4 into main Oct 4, 2026
2 checks passed
@ZLHad
ZLHad deleted the claude/review-desk branch October 4, 2026 04:13
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.
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.

1 participant