This file provides guidance to coding agents when working with code in this repository.
annotate-it is itself a Claude Code skill — not an app you build, but a tool an agent invokes. The repo's product is SKILL.md plus two Node scripts and one HTML page. It lets a human highlight spans of a draft in a browser and have those inline comments flow back to the agent. Read SKILL.md first: it is both the skill manifest and the loop the agent is expected to follow.
No build, no test suite, no lint, no package.json, no dependencies — Node's built-in modules only, Node 18+. To exercise the pieces manually:
# Serve a draft (writes server-info.json, prints {"url":...}); --port 0 picks a free port
node server.mjs --dir /tmp/review --port 8080
# Block until a new feedback line lands, then exit (exit 0 = new feedback, 2 = timeout)
node wait-for-feedback.mjs --dir /tmp/review --timeout-min 30Open the printed URL, select text, comment, click Send → a JSON line appends to <dir>/feedback.jsonl and the waiter exits.
The model only runs when the harness invokes it; a browser click cannot reach it directly. The whole design works around that one constraint:
server.mjs— stateless HTTP server.GET /servesreview.html,GET /draftreturnsdraft.txt(sentno-storeso a refresh always re-reads),POST /feedbackvalidates JSON and appends one line tofeedback.jsonl. It self-exits onSIGTERM/SIGINTand after idle (--idleminutes, default 60), unlinkingserver-info.jsonso a later run can't read a dead URL.wait-for-feedback.mjs— blocks untilfeedback.jsonlgrows past a baseline line count, then exits. A background task exiting is what re-invokes the agent. This script is the bridge from "user clicked Send" to "agent wakes up." Detection isfs.watch(fast) plus a 1s poll fallback (covers network/synced filesystems wherefs.watchsilently drops events) — keep both.review.html— the entire client, one self-contained file (inline CSS + IIFE, no framework, no bundler).
Both scripts must be launched in the background by the agent; running them in the foreground blocks the turn and they die.
A --dir working directory carries three runtime files (all gitignored), the only state shared between server, waiter, and agent:
| File | Written by | Read by |
|---|---|---|
draft.txt |
agent | server (/draft) |
feedback.jsonl |
server (one line per Send) | agent (last line = current round) and waiter (line count) |
server-info.json |
server on boot | agent (to recover the URL) |
Each round: agent overwrites draft.txt, user refreshes the page, agent re-launches the waiter. Feedback is append-only — always take the last line, never re-parse the whole file.
- Comments are stored as
{start, end}character offsets into the draft.line/before/aftercontext is derived on demand (contextFor), so a one-word selection still comes back fully situated. The wire payload shape lives infeedbackPayload. - Rendering uses boundary segmentation (
renderText): the text is split at every comment start/end, each segment wrapped once and shaded by how many comments cover it (data-depth1–3). This is what allows overlapping / nested highlights — a single linear pass cannot wrap nested spans. Don't revert to a linear renderer. - Popovers are positioned from a captured viewport rect, not the live element, because opening a popover re-renders the marks and detaches the clicked node (
onMarkClick→positionPopover). - Multi-click selections are debounced (
onMouseUp, 250ms) so a triple-click yields one line comment instead of first committing the double-click's word. - Comments persist in
localStoragekeyed by a hash of the draft, so a refresh keeps work but a changed draft auto-resets. - Copy as Markdown is the fallback channel if
POST /feedbackfails — keep it working alongside Send.
Dependency-free is a hard design constraint, not an accident — do not add npm packages, a build step, or WebSockets. Match the existing terse, comment-dense style in the .mjs files and the compact IIFE in review.html.