Skip to content

harn review-pack

harn review-pack create [options] <target>
harn review-pack judge [options] <dir>
harn review-pack expand [options] <dir> --tile <id>
harn review-pack reviews add [options] <dir>
harn review-pack findings add [options] <dir>
harn review-pack disposition [options] <dir> <context>/<tile>#<n> <disposition>
harn review-pack verdict [options] <dir>
harn review-pack list [options]
harn review-pack show [options] <dir>
harn review-pack clean [options]

A page review pack is the on-disk evidence for reviewing one rendered page. For every rendering context (viewport × theme × state) it holds the full-page screenshot, the critique tiles as PNG files, the serialized DOM, and the QA signature. On top sit a review.md entry point, a bounded inspection plan, coverage, deterministic gate results, machine findings once a judge has run, and a delegated-review findings.json. Review subagents can inspect the page from the pack alone, with no browser and no third-party tool. The coordinating agent assigns the tiles and synthesizes their reports; it does not open tile images itself.

The pack is also what makes judging cheap. Capture needs a browser and takes seconds; judging needs a vision model and takes minutes. Splitting them means every browser closes before the first model call, and one bounded pool judges every tile of every context. The judge never opens a browser. harn qa-run writes a pack in every run directory; this command builds or judges one directly.

A pack is working evidence, not a record. It expires 90 minutes after its judge finishes (after capture when no judge runs) and the whole directory is then deleted, leaving only pack-expired.json (target, created and expiry times, context count, machine outcome). The run’s page-qa-result.json carries every machine finding inline, so a run stays reportable after its pack is gone. Anything a reader must keep (a cited tile, a disposition, a reviewed outcome) goes into the report while the pack is alive. --retain <minutes> on create, judge, and qa-run changes the window for one pack; a pack written to an explicit --out is unmanaged and is never deleted automatically.

Renders each context once through child harn browse --review-pack processes into <out>/contexts/<id>/, closes each browser as soon as its files are written, then writes review.md, the inspection plan, coverage, the manifest, and a findings.json skeleton. No vision call is made.

Flag Description
--out <dir> Pack directory. Default: a managed workspace under .harnery/artifacts/.
--context <spec> Rendering context <viewport>[:<theme>[:<state>]], repeatable. Default: desktop and mobile, light and dark.
--scope <selector> Also tile one screenshot per element matching the selector (repeatable).
--max-tiles <n> Tile ceiling per context (default 96), including scopes and gate-hit priorities. Page edges remain required. Use qa-run --tile-budget for one shared budget across the complete matrix.
--concurrency <n> Concurrent capture browsers (1–8; default 2).
--timeout <ms> Per-capture timeout (default 120000).
--retain <minutes> Minutes until the pack expires and is deleted (default 90).
--json Print the pack manifest.

Exit 0 when every context was captured; 4 when at least one capture failed (the pack still holds the others and names the gap under warnings); 1 for usage errors.

Capture fidelity. A full-page screenshot can differ from what a scrolling reader sees. Each capture compares native scrolled probes with the corresponding full-page regions. Agreement allows full-page tiles; disagreement selects scrolled bands and records capture_fidelity.source: "scrolled-bands". A missing probe or incomplete fallback cannot establish a pass. Coarse images are diagnostic only. Snapshot reuse compares the saved native tiles, including valid scrolled tiles, within the same context, DPR, capture recipe, rubric, and critique version. The half-size overview never serves as native evidence.

Tile selection. harn qa-run plans every context before capture, reserves page edges, prioritizes candidates containing gate findings, and distributes remaining slots by uncovered height. Scopes consume the same total. Capture validates and follows the exact allocation, with no extra gate-hit path. Coverage records native rectangles, the union of reviewed intervals, gaps, and omitted gate hits or scopes. A failed deterministic gate stays failed even when its tile cannot fit.

Reads the pack’s tiles back from disk and sends every tile of every context through one pool of vision calls. Findings cite the pack’s stable tile ids. evidence/critique.json is written and review.md and the inspection plan are refreshed so the machine findings become primary tiles.

Flag Description
--pool <n> Vision calls in flight across the pack (1–16; default: the provider’s own concurrency).
--allow-metered Permit the provider’s metered-API fallback. Default: subscription-backed headless harnesses only (HARNERY_CRITIQUE_HEADLESS_ONLY=1 is set while the judge runs).
--context <id> Judge only this context id (repeatable).
--retain <minutes> Reset the pack’s expiry to this many minutes after the judge finishes (default 90).
--json Print the judge result.

Exit 0 when every judged context passed; 2 on a high-severity finding; 4 when no provider judged (the host injected none, or the headless list was exhausted); 1 for usage errors.

harnery ships no model client. The judge uses the same host-injected critiqueProvider as browse --check-critique; without one every context reports skipped, never a pass. Each tile goes to the provider with the rubric plus a short position preamble (band N of M, document y range, page height, overlap with its neighbours, whether it touches the page top or bottom, and the capture source), so the model can tell a slice edge from a container edge.

reviews add, findings add, disposition, verdict

Section titled “reviews add, findings add, disposition, verdict”

The reviewer half of the loop is delegated. The coordinating agent assigns every primary tile to review subagents, and each tile must receive at least one completed native-pixel subagent review. It chooses the first available model in this order: GPT-5.6 Luna (gpt-5.6-luna where an adapter requires an id), Composer 2.5, then Haiku 4.5. It uses that model for every tile-review subagent and does not substitute another model. The coordinator never substitutes its own image read. Machine findings in evidence/critique.json are claims; review subagents confirm or dismiss each one and record defects the machine missed. The coordinator serializes those reports into findings.json, then the verdict command turns both into a reviewed outcome beside the machine outcome. If no listed model is available, subagents are unavailable, or any primary tile is uncovered, the review is incomplete. evidence/critique.json is never edited.

Command Description
reviews add <dir> --reviewer <id> --model <name> --assigned <context>/<tile> --completed <context>/<tile> Record one review subagent’s model, assignment, and completed native-pixel reads. --model accepts GPT-5.6 Luna, Composer 2.5, or Haiku 4.5. --assigned and --completed repeat; `–status complete
findings add <dir> --context <id> --tile <id> --severity <s> --category <c> --observation <text> Append a review-subagent finding (--tile repeatable, at least one; --recommendation, --id F001, --reviewer <name> optional; ids auto-number). Validated against findings.schema.json before the write.
disposition <dir> <context>/<tile>#<n> <disposition> [--note <text>] [--by <name>] Record a verdict on one machine finding: confirmed, artifact, not-a-defect, or duplicate-of-gate. <n> is the finding’s 0-based position among that tile’s findings in evidence/critique.json. A target that does not exist is refused; a repeat replaces the earlier disposition.
verdict <dir> [--json] Compute the reviewed outcome and write evidence/verdict.json (harnery-page-review-verdict/v2), then refresh review.md with a “Reviewed outcome” section. A machine high counts unless dismissed; a review-subagent finding of severity critical or high counts. Missing completed subagent coverage for any primary tile keeps the outcome incomplete. Exit 2 on fail, 4 on incomplete or skipped, 1 on validation errors.

The reviewed outcome remains separate from the machine outcome. Later QA runs apply saved decisions only when disposition acceptance is explicitly enabled.

disposition <dir> <disposition> --all-high --context <id> applies one decision to every machine high in that context. Every target is validated before any write. Reviewer identity defaults to the current agent at the CLI boundary; --by overrides it. A bulk decision still requires actual review of the findings it covers.

Command Description
list [--root <dir>] [--json] Every pack under the artifact store (or --root): path, age, size, contexts, machine outcome, reviewed outcome when a verdict exists, expiry, and whether it has expired.
show <dir> [--json] One live pack’s context table and finding counts, or the retained summary from pack-expired.json.
clean [--root <dir>] [--yes] [--include-unmanaged] [--json] Preview the expired packs under the store and, with --yes, delete them. Deletion empties the pack directory (keeping only a workspace’s own artifact manifest) and writes pack-expired.json. A pack without retention.managed: true is skipped unless --include-unmanaged is passed. Hosts can enable this sweep before qa-run and review-pack create with review_pack.auto_clean: true; it ships off. That setting also adds pack previews to artifacts clean and pack deletion to artifacts clean --yes. Artifact holds still protect pack payloads.

Re-captures one tile region of an existing pack at a higher device scale factor, for a sharper look at a small defect a native-pixel tile only hints at. The same target and context are rendered again through one child browse --review-pack-expand, the whole page is screenshotted at that DPR, and the tile’s rect (scaled by the DPR) is cropped from it in pixel space, the same way the original tiles were cut. The result lands beside the source tile as contexts/<id>/tiles/<tile>@<dpr>x.png; the context record gains an expanded entry (tile, dpr, width, height, file, sha256, bytes, captured_at) and review.md lists it under the context’s tile index. Existing tiles, their digests, and every other pack file stay untouched, so a verdict already written against the pack still holds.

Flag Description
--tile <id> Tile id to expand (T012). Required.
--context <id> Context the tile belongs to. Required when more than one context carries that tile id.
--dpr <n> Device scale factor for the re-capture (1 to 4; default 2).
--timeout <ms> Capture timeout (default 120000).
--json Print the expanded tile record.

Exit 0 when the region was written; 1 when the pack, context, or tile does not exist (the error names what the pack does have); 4 when the target could not be rendered again (the child’s last lines are quoted). Expanding the same tile at the same DPR again replaces that entry; a different DPR adds a second one. The re-render is a fresh navigation, so a page that changed since capture shows its current state, not the captured one; compare against the source tile before citing it.

<pack>/
review.md # start here: protocol, contexts, coverage, findings, inspection plan
manifest.json # harnery-page-review/v2; carries retention.expires_at and size_bytes
findings.json # delegated output (harnery-page-review-findings/v3): reviews + findings + dispositions
findings.schema.json
evidence/
inspection-plan.json # the bounded complete-review budget per context, plus gate_hits → tiles
coverage.json # bands kept vs page height, warnings, not_checked
critique.json # machine findings per context and tile (after judge); never edited
verdict.json # reviewed outcome (after review-pack verdict)
index.json files.json # small map, complete inventory
contexts/<context-id>/
context.json # viewport, theme, state, url, page size, coverage, capture_fidelity, tile records
full-page.png # half-size orientation (full_page_scale: 0.5); native tiles are evidence
contacts.png # every tile downscaled into one grid, ids stamped (orientation only)
tiles/T001.png … # native-pixel tiles, ids stable within the context (bands, hit bands, scopes)
tiles/T012@2x.png # one region re-captured at 2× (review-pack expand), optional
tiles.json dom.html.gz signature.json

After expiry the directory holds only pack-expired.json, plus a workspace manifest when present. The stub retains finding counts and the reviewed result, or result: null when no verdict existed. It cannot support another image review.

Version 1 packs require recapture. The reader refuses them rather than inventing native context metadata. Older cached critique cannot satisfy native-tile reuse.

verdict also saves the latest complete review in a separate review-decisions.json snapshot slot for each canonical target and context. The slot contains finding text, exact pixel and geometry digests, reviewer provenance, and version metadata; it contains no images or expired pack links. Baseline writes preserve it. The command validates delegated coverage and binding to the current machine evidence before saving, and a persistence failure fails the command. A fully reviewed failed outcome can be saved; an incomplete review cannot.

If a new judge result changes the machine evidence, the pack archives its prior findings, critique, inspection plan, and verdict under review-history/ and starts an empty review bound to the new result. An unchanged judge result keeps the existing review. If archiving fails, the prior review remains intact and the command fails.

Subsequent QA runs can use these decisions only with policy.accept_dispositions: true or --accept-dispositions. Acceptance is off by default. Only artifact and not-a-defect match across runs, and only when the normalized finding text, severity, category, native geometry, pixels, and context versions all match. The result retains every machine finding and lists applied dismissals separately.

Tile ids are T001… per context, bands first and then each scope’s tiles in selector order; a finding cites <context-id>/<tile-id>. Every tile record carries its SHA-256, and the judge refuses a tile whose bytes no longer match, so a pack cannot be edited under a verdict.

Each context with tiles also carries a contact sheet, contacts.png: every tile box-filtered down into a fixed grid of four 320 px cells per row (sheet width stays under 1600 px), read row-major in tile id order, each cell stamped with its tile id in a label band. It is built when the context is captured and rebuilt by create, judge, or qa-run when missing; the record’s contact_sheet field carries the file, digest, grid size, and reading order. It lets review subagents orient before opening their assigned tiles; it is never evidence on its own, because the downscale hides exactly the small defects the tiles show.

Deterministic gate results recorded in a pack (by qa-run) can carry hits: every finding in the gate’s browse JSON envelope that has a document-space rectangle. Runts, truncation, contrast, placeholder, image, clip, overlap, crowd, align, gap, overflow, and target-size results all qualify; a finding without a rectangle keeps its place in the gate’s failures text and nothing else. At most 50 hits are kept per gate envelope.

The inspection plan maps each hit to the tiles whose rect intersects it and lists them under gate_hits (check_id, rule, label, rect, tiles); those tiles join primary_tiles with a gate hit (<rule>): <label> reason, so the assigned review subagent opens the tile that shows the runt or clipped element instead of hunting for it. review.md renders the same mapping under each gate line, with the rectangle and links to the tiles. A hit whose rectangle no tile covers (below a capped context’s last band, for example) is reported as such rather than dropped.

review.md carries the protocol. The short version: the coordinating agent reads the context table and coverage, selects GPT-5.6 Luna when available (otherwise Composer 2.5, then Haiku 4.5), and gives review subagents disjoint assignments covering every inspection-plan primary tile. The subagents use contact sheets for orientation, open their assigned tiles at native pixels, treat machine findings as claims, cite deterministic gate failures rather than re-deriving them, and report anything the machine missed. The coordinator does not open the images; it records each reviewer and model with review-pack reviews add, serializes dispositions and findings, then runs review-pack verdict. A missing listed model, missing subagent, or uncovered tile makes the review incomplete. Do it while the pack is alive: 90 minutes after the judge, the tiles are gone and only the report remains.

Terminal window
# Build a pack for a local file at the default four contexts
harn review-pack create file:///$PWD/dist/index.html --out ./pack
# Only a desktop dark context, plus one tile per pricing card
harn review-pack create http://localhost:3000/pricing --context desktop:dark --scope '.price-card'
# Judge with eight vision calls in flight
harn review-pack judge ./pack --pool 8
# Look closer at one tile: re-render it at 2× beside the original
harn review-pack expand ./pack --tile T012 --context desktop-light-default
# Dismiss a machine finding as a capture artifact, record one the machine missed, and write the reviewed outcome
harn review-pack disposition ./pack desktop-light-default/T006#0 artifact --note "callout is intact when scrolled"
harn review-pack findings add ./pack --context mobile-dark-default --tile T015 --severity medium --category contrast --observation "table headers are hard to read on the dark surface"
harn review-pack verdict ./pack
# See what is on disk, then delete what has expired
harn review-pack list
harn review-pack clean --yes
  • harn qa-run: writes a pack in every run and judges it as its critique stage.
  • harn browse: --review-pack, --review-pack-context, --review-pack-scope, --review-pack-expand, --device-scale-factor, and the --check-critique-* tiling knobs.