| name | agentic-html |
| description | Edit an HTML file from the annotations a human draws on a live preview of it. Use when you have an .html file and a human who wants to say "change this bit" by pointing at the page instead of describing it in prose — or when you must make a surgical, validated DOM edit rather than regenerating the file. Covers the preview → annotate → patch loop, the structure gate that rejects bad patches, and every error code you can get back. |
agentic-html
You are editing someone else's page. They can see it; you cannot. This tool closes
that gap: you serve the page, they scribble on it, you read their scribbles as CSS
selectors plus comments, and you apply DOM patches that a server-side validator
either accepts or rejects with reasons.
Two consequences that shape everything below:
- You never invent a selector. Selectors come from
get_document_outline,
get_region, or an annotation's anchor_element.selector. A selector you
composed by reading HTML in your context is a guess, and apply_patch will
refuse a guess rather than edit the wrong node.
- Success is not yours to declare. A structure gate runs inside the tool. If
it fails, no version was created — nothing happened. Do not report "done"
until a call returns a
new_version_id.
The loop
preview_html(file_path) → { url, session_id, version_id }
↓ give `url` to the human, in plain text, and stop
↓ they annotate in the browser and submit; the version is then sealed
get_annotations(version_id) → the work list: selector + comment + quote
get_document_outline(version_id) → orient; harvest valid selectors
get_region(version_id, selector) → read exactly the element you are about to change
apply_patch(version_id, patches) → { new_version_id, gate, diff, failed_patches }
resolve_annotation(annotation_id)→ once per annotation you actually handled
↓ report: which comment became which change, and the new version id
close_preview(session_id) → when the human says they are finished
Notes on the steps, in the order they bite:
preview_html starts a server that keeps running. Hand the human the URL and
wait. Do not poll get_annotations in a tight loop; you will read an empty list
because they have not submitted yet. get_activity shows whether a submission
has landed (annotations_submitted).
- Annotations are your specification, not your instructions. Each carries
anchor_element.selector (where), comment (what the human wants),
quote / node preview (what the element said at the time), and possibly
hit_elements when they circled a region and several nodes were under the ink.
For a circled region, pick the most specific hit element that satisfies the
comment. When an annotation has a screenshotPath, a clean render of the circled
region exists — fetch it with get_annotation_screenshot to see what they
pointed at instead of inferring it from selectors.
- Batch your edits. One
apply_patch call with several patch objects is
correct and preferred: all selectors resolve against one frozen snapshot, and
you get one gate verdict and one version instead of N.
- Report back in the human's terms. "Made the headline blue and removed the ad
banner — version
abc123." They cannot read a diff of nth-of-type paths.
The run lifecycle
Everything above is the edit. Wrapping it is a run — one attempt to satisfy a
submission — and the run is what the human watches and what enforces one-editor-at-a-
time. You are Agent Native here: the CLI, not your own judgement, owns the state
machine. Open a run before you touch the document, narrate it, and close it exactly
once.
branch status → dev | releasing; refuse to start if releasing
run start <base_version_id> → { run_id } (acquires the branch lock)
↓ report_step as you go — the human's sidebar renders these live
run step <run_id> thinking "reading the two annotations"
run step <run_id> tool "apply_patch: hero h1 + nav"
apply_patch(...) → { new_version_id, gate, ... }
run finish <run_id> --status committed --output-version-id <new_version_id>
↓ or, when you cannot:
run finish <run_id> --status failed \
--failure '{"reason":"gate","message":"...","offending_annotation_ids":["ann-.."]}'
Why this matters, not just how:
- The branch lock is real mutual exclusion.
run start fails with
BRANCH_RELEASING if another run holds it. Do not retry in a loop — surface it;
the other editor (a human forking, or another agent) has to finish first.
run finish is not optional and fires once. It releases the lock and writes
the outcome. Leave it out and the branch stays releasing forever — the human
can no longer annotate. If you crash mid-run, the lock is your fault to clear.
- A failure is a result, not an error to hide. When the gate rejects you, or
an annotation asks for something impossible (a script, a node that isn't there),
run finish --status failed with offending_annotation_ids. That is what paints
those annotations red in the human's UI and offers them "retry" / "drop and
retry" — a bare tool error gives them nothing to act on.
- Steps are for the human, not the log.
thinking / tool / note steps
stream to their sidebar as you work. Narrate the decisions, not the keystrokes.
start_run / report_step / finish_run / get_run / cancel_run /
get_branch are the command names; the CLI spellings are above. If you are driving
through MCP or HTTP the same six exist with identical parameters.
Selectors
Legal sources, in order of preference:
annotation.anchor_element.selector — generated in the page by the same
algorithm the server resolves with, so it round-trips.
get_region(...).selector, .parent.selector, .previous_sibling.selector,
.next_sibling.selector.
get_document_outline(...) → any node's selector.
The dialect is deliberately narrow: #id when unique, else tag.class when
unique, else a :nth-of-type() path from the nearest stable ancestor. There is no
:nth-child, no attribute matching, no :contains. If you write a selector by
hand you will produce something the resolver rejects (ANCHOR_NOT_FOUND) or —
worse — something that matches several nodes (ANCHOR_AMBIGUOUS).
apply_patch refuses ambiguity instead of guessing. Getting
ANCHOR_AMBIGUOUS back means: narrow the selector. Add the parent
(div.hero > h1:nth-of-type(1)), or call get_region on the parent and read the
child's real selector out of the response. Never re-send the same selector hoping
for a different answer.
The structure gate
Six deterministic checks run on the patched document before any version exists:
| Check | Fails when |
|---|
PARSE_OK | the result does not parse as a document |
TAG_BALANCED | a fragment you supplied has unbalanced markup (<div><p>x</div>) |
NO_DANGEROUS_NODE | you introduced <script>/<iframe>/on*=/javascript: or rewrote a script body |
NO_OUT_OF_BOUNDS | something outside your patch targets changed |
HAS_CHANGE | the patch set was a no-op |
SELECTOR_UNIQUE | a touched selector no longer resolves to exactly one node (delete and replace targets are exempt — a vanished selector is the point) |
TAG_BALANCED inspects your fragment before the parser gets it, because parse5
silently auto-corrects unbalanced input — "it parsed fine" and "it did what you
meant" are different claims.
A gate failure means no version was created. You get
PATCH_GATE_FAILED with details.gate.checks[], each with code, ok, and a
message naming the specific problem. Read the failing check, change the patch
that caused it, call again. Retrying the identical call is guaranteed to fail
identically — the gate is deterministic.
Common fixes:
TAG_BALANCED → close your tags; send a complete fragment, not a snippet that
relies on surrounding context.
NO_DANGEROUS_NODE → you cannot add scripts or inline handlers through this
tool, at all. Achieve the effect with markup/CSS, or tell the human this needs
a hand edit.
NO_OUT_OF_BOUNDS → your content for a replace swallowed sibling markup, or
you targeted an ancestor when you meant a child. Re-read with get_region.
HAS_CHANGE → the edit you sent is what is already there. Re-read the element;
someone (possibly you, in an earlier call) already applied it.
SELECTOR_UNIQUE → your inserted markup duplicated the class or id the selector
keyed on. Change the new node's attributes.
Patch objects
{
"selector": "div.hero > h1:nth-of-type(1)",
"action": "replace",
"content": "<h1 style=\"color:#1a73e8\">Ship faster</h1>",
"old_content": "<h1>Ship faster</h1>",
"quote": { "exact": "Ship faster" },
"annotation_id": "ann-..."
}
action: replace | delete | insert_before | insert_after |
modify_style. modify_style takes CSS declarations in content
("color: #1a73e8; margin-top: 40px") and merges them into the existing
style attribute — prefer it over replace for pure styling, because it cannot
disturb the element's children.
content is required for replace and both insert_* actions. Omitting it
fails that patch (PATCH_MISSING_CONTENT) rather than silently deleting the
target.
old_content is an optimistic-concurrency check: the patch applies only if the
target's current serialization matches. Use it whenever you read the element in
an earlier turn — it converts "I edited the wrong thing" into a clean
PATCH_CONTENT_MISMATCH.
quote is a drift check on text: { exact, prefix?, suffix? }. Copy the
annotation's quote through. If the selector still resolves but the text has
changed underneath you, you get ANCHOR_QUOTE_MISMATCH instead of editing a
node the human never pointed at.
annotation_id links the change to the human's note. Pass it; it is how the
failure report tells you which comment you failed to satisfy.
All selectors resolve against a frozen copy of the document before any edit
runs. So patches within one call cannot shift each other, and you do not need to
order them or account for earlier edits. If two patches genuinely collide (one
deletes an ancestor of another's target), the second is reported as a failure
(PATCH_APPLY_ERROR, "the patches overlap"), never silently dropped.
Partial success is normal and visible: the response carries applied_count and
failed_patches[]. Check both. applied_count: 2 out of three patches means one
annotation is still unhandled — do not resolve it.
Verification
apply_patch always runs a cheap DOM comparison. verify: true additionally
renders both versions in a headless browser and compares them pixel by pixel. It
costs several seconds, so use it when a change is visually risky — layout,
positioning, anything touching a container — and skip it for text and colour
edits. Read verification.passed and verification.unexpected_changes.
preview_patch is the dry run: same patch shape, returns the resulting HTML and
diff, creates nothing. Use it when you are unsure your fragment lands where you
think; do not use it as a substitute for reading the region first.
Choosing tools
apply_patch over create_version. Patches are surgical, gated, and keep the
annotation→change link. create_version takes whole-document HTML, skips the
gate's out-of-bounds reasoning entirely, and is for wholesale rewrites only.
get_region over get_dom_snapshot. The region view is one element plus
parent and siblings, clipped to a character budget. get_dom_snapshot returns
the whole document and will eat your context on any real page.
get_document_outline over reading the file yourself. The outline hands you
selectors that are guaranteed to resolve. Reading the raw file hands you
selectors you then have to guess at.
compare_versions before you claim success on anything subtle.
When something is wrong with the setup
agentic-html doctor --json — machine-readable environment diagnosis; every
check carries a remedy. Run it before concluding a tool is broken.
agentic-html mcp --list-tools — the exact tool list this install advertises.
- Versions and annotations persist under
.html-editor/, so reading a version and
patching it work across process boundaries: get_document_outline, get_region,
get_dom_snapshot, get_annotations, export_annotations, get_version_history,
preview_patch and apply_patch all resolve a version_id created by a previous
invocation. compare_versions, checkout_version and create_version's
parent_id hold live in-memory references and only see versions the current
process has loaded — keep those inside one MCP server or one agentic-html serve
process, and prefer apply_patch over create_version.
References
references/commands.md — every command, generated from the installed
registry: parameters, types, defaults, CLI spellings, raw JSON Schemas.
references/troubleshooting.md — every error code you can receive and the
specific next action for it.
references/transports.md — MCP (.mcp.json), HTTP (curl), CLI (--json):
how to wire each one.