Will marks what in a document an agent may edit, add to or leave alone. The marks live inside the document: hidden comment lines in Markdown, and native carriers in DOCX, PDF and Google Docs. No registry, sidecar or account.
<!-- will/1 keep: the wording counsel approved -->
The approved sentence.
<!-- /will -->
(Indented one space so this README carries no live marker.) Three laws: edit may change, append adds only at the end, keep stays exactly as it is. Unmarked content is edit. A marker that cannot be read exactly makes the whole document keep.
node will.mjs compare before.md after.mdwill.mjs is the reference reader and evaluator: one file, Node.js 22.23.4 or later, no dependencies. In code:
import {parse, evaluate} from './will.mjs';
const before = Buffer.from('Notes.\n');
parse(before).regions;
evaluate(before, [{start: 6, end: 6, insert: ' More.'}]); // {outcome: 'applied'}parse, evaluate, diffSplices and compare take strings or Uint8Array bytes; offsets count UTF-8 bytes. evaluate defaults to the working path and never writes. Apply the same splices to the same source yourself, once your host has authorized the act.
Types: keep will.d.mts beside will.mjs.
Snapshot comparison infers edits; it cannot detect a rewrite that changes nothing. Large or ambiguous changes can be
refused even when protected bytes match. Use evaluate with the actual splices when your host records them.
A pull request that changes a keep region, rewrites an append region anywhere but its end, or touches a marker fails, with the file and line named. It checks .md and .markdown files, in any case. A new file is the author's; removing a file that keeps or appends is refused.
on: pull_request
jobs:
will:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: {fetch-depth: 0}
- uses: jackskip22/will@mainHonor the markers: change edit, add only at the end of append, leave keep alone, never touch a marker. Intent is the person's words, not permission. If a marker cannot be read exactly, change nothing and say so.
Rapier uses Will for live editing with agents over MCP and WebMCP. Connect to https://mcp.rapier.website/door/mcp and call open; no account or key. Agent guide.
region a piece of the document, identified by the format's own binding
law edit | append | keep
intent? the person's words about what matters here, one line
edit: the region may change.append: what is there stays, in place and in order; additions go at the end.keep: the region stays exactly as it is.
Unmarked content is edit. Regions never overlap. One act of writing that reaches several regions must satisfy
every one of them or the whole act is refused. Intent is the person's words: untrusted, region-scoped document
data that grants no tool, no authority and no reach, whatever it says; it can only narrow what its law allows.
In Markdown, a marker is a whole line at column zero:
<!-- will/1 keep: the wording counsel approved -->
The approved sentence.
<!-- /will -->
(indented one space here to quote it; an indented marker is ordinary content). The opener is exactly
<!-- will/1 <law> --> or <!-- will/1 <law>: <intent> -->; the closer is exactly <!-- /will -->. The first
: after the law begins the intent; later colons belong to the words. The colon form requires 1–512 Unicode
scalar values of intent and never contains --. Preserve intent spaces exactly. Words and spacing are exact:
no other dash, spelling or case. Markers are
recognized by these bytes alone, inside code fences too; <!--will/ and <!--/will at column zero are faults,
and text outside these prefixes is content. Pairs never nest or interleave; a pair copied elsewhere governs
there. Only LF, CRLF and CR end a line. The governed region starts after the opener's line terminator and ends before the closer's line. keep
compares it exactly; append needs the old bytes as an exact prefix, less the final line terminator
before the closer, which belongs to the carrier.
A will that cannot be read exactly faults the whole document closed: a working hand treats all of it as keep
until a person repairs it. The faults are unpaired_marker, malformed_marker, unknown_law,
unknown_version, intent_over_bound and invalid_utf8, each with its byte span, in document order, never
guessed. The version token is the bytes after will/ up to the first space, checked before delimiter grammar.
For UTF-8 faults, lead bits nominate a 2–4-byte sequence: span the remaining suffix if truncated, the lead byte
if a continuation is wrong, or the whole sequence if its scalar is invalid; other invalid leads span one byte.
Validate the whole document as strict UTF-8 before scanning markers. An invalid sequence returns one
invalid_utf8 fault, at the first such sequence, and no regions. For marker lines, check the version, delimiter
and separator grammar, the law, then intent. The law token is nonempty and contains no colon or any of these
scalars: U+0009–000D, U+0020, U+00A0, U+1680, U+2000–200A, U+2028, U+2029, U+202F, U+205F, U+3000, U+FEFF.
After the token, the remaining text before --> is empty or starts with exactly : . Missing tokens or bad
separators are malformed_marker. Any other token than edit, append or keep is unknown_law before
inspecting intent, including empty intent. For a known law, empty intent or -- is malformed_marker, then
more than 512 scalars is intent_over_bound. U+0085, U+2028 and U+2029 inside intent are data, not line endings.
Keep one pending valid opener. A second opener faults as unpaired_marker without replacing it. A valid closer
completes the pending pair, or faults if there is none. A malformed line never opens or closes a pair. Fault
any opener still pending at EOF. Keep completed regions even when other lines fault; sort faults by span start.
Parse returns {faulted, faults, regions}. faulted is true exactly when faults is nonempty. Byte spans are
zero-based, half-open [start, end) offsets into the original bytes; line and region indexes are zero-based.
Marker spans and marker-fault spans include the complete line and its terminator, if present.
| Value | Fields |
|---|---|
| Fault | mode, line (null for invalid UTF-8), byteSpan |
| Region | index, law, intent (null when absent), opener, closer, governedSpan |
| Opener or closer | line, byteSpan |
A host declares which of its paths are the person's own (authoring) and which are a hand's (working). A working
path never writes a marker, moves one or writes one back unchanged. It judges each act by the exact bytes
replaced, against the source as it stood, and answers with one of three words: applied; refused with the
document-law reason; invalid when the question itself could not be read. Every range disclosure names the same four
facts: law, region, intent (when the range lies within one region) and rule. Will authenticates nobody
and grants nothing: the document carries the word, the host carries the path.
Evaluate takes the before-document bytes, a list of splices {start, end, insert}, and working or authoring.
Each splice replaces [start, end) with explicit bytes, including an explicit empty value for deletion. Offsets
are nonnegative integers into the unchanged before-document, with start <= end <= byteLength. Validate input
before consulting document law: check the path, the list, then each supplied item's shape and range in input
order. Sort splices by source offset; refuse overlapping replacements or any two splices with the same start.
An insertion at a preceding replacement's end is allowed. Never derive narrower splices from the resulting text.
A valid authoring act and an empty splice list return {outcome: "applied"}. For any other working act, use
this order and stop at the first refusal:
| Check | rule on refusal |
Additional fields |
|---|---|---|
| Parse the before-document. | before_faulted |
faults |
| Check replaced ranges against original marker spans. | marker_span_touched |
region, law |
| Apply all splices and parse the result. | result_faulted |
faults |
| Require the same ordered sequence of markers. | marker_sequence_mismatch |
None |
| Compare corresponding governed regions. | law_violated |
region, law |
A nonempty replacement touches a marker when their half-open spans intersect. An insertion touches a marker
only strictly inside its span; either endpoint is excluded. Allowed surrounding edits may shift markers'
positions. Each original marker [a, b) must remain the same marker at [a + d, b + d), where d is the sum
of insert.byteLength - (end - start) for splices with end <= a. Compare these surviving marker bytes and
positions in order. Identical copies elsewhere cannot replace originals hidden by surrounding edits.
Name the first touched or violated region in document order, regardless of the caller's splice order.
For keep, compare the complete governed bytes. For append, remove
one final LF, CRLF or CR from each governed byte string, then require the old string as an exact prefix of the
new one. edit imposes no governed-byte comparison.
An applied decision contains only outcome. A refusal contains outcome: "refused", reason: "document_law",
rule, and only the additional fields in the table. Invalid input contains outcome: "invalid",
reason: "precondition", and one rule: unknown_path, malformed_splice, out_of_range_splice or
overlapping_splices. A malformed splice list, item, offset or missing/non-byte insertion is malformed_splice;
an end beyond the document is out_of_range_splice. Intent never changes these mechanical decisions.
| Format | Carrier |
|---|---|
| DOCX | Each marker is one paragraph of a single hidden run with a hidden paragraph mark; the whole paragraphs between the pair are governed. |
| Each marker is one text object that draws no ink: text render mode 3, or text set in an embedded font whose glyphs enclose no area; on its own baseline between visible lines, with a lossless ToUnicode mapping. Reading order is by position (page, then y, then x), never the order of the stream. A reader takes the marker's ASCII frame and law from any text layer, and the intent exactly only where the layer keeps its scalars (ActualText honored). | |
| Google Docs | The named exception: named ranges under a will/1 naming convention; the importer strips a text carrier. |
Conversion that is aware of Will preserves it, says WILL LOST, or refuses; it never drops a marker silently.
vectors.json holds the normative machine truth: every parse and every evaluation, faults with their
byte spans included. will.mjs is the reference reader and evaluator, one file, no dependencies:
node will.mjs check vectors.json # this reference against the vectors
node will.mjs --adapter < cases.ndjson # any implementation over the same casesHosts written independently in Python and Go, from this text and the vectors alone, pass them all. Run each host from this repository's root:
| Host | Requirements | Run all vectors |
|---|---|---|
| Python | Python 3.11+ | python3 hosts/python/will.py check vectors.json |
| Go | Go 1.22+ | (cd hosts/go && go run ./cmd/will check ../../vectors.json) |
Both commands compare complete result objects, including omitted versus null fields, and exit nonzero on a
mismatch. The workflow runs the reference and both independent hosts on every push and pull request. It also
checks both real adapter processes against the vectors and the malformed-byte and request-recovery cases in
hosts/adapter-vectors.json.
Replace check vectors.json with --adapter to read one JSON request per input line and write one response
per output line, in order. A request uses the vector shape without expect: kind: "parse" with doc or
docBase64; or kind: "evaluate" with before or beforeBase64, splices, and path. Each splice uses
insert or insertBase64. Supply exactly one spelling for each byte field. Plain fields are Unicode scalar
strings encoded as UTF-8; Base64 fields use canonical, padded standard Base64 and can carry invalid UTF-8 bytes.
The response is {kind, result}, with the normalized parse or evaluation result. An optional scalar string id
is echoed as id; it never affects evaluation.
Numeric offsets retain their exact JSON value: integral decimal or exponent forms are integers, fractional
values are malformed, and integers beyond the document's byte length are out of range.
Blank lines, malformed JSON, an unknown kind, a nonstring id, a missing path or an invalid document byte
field returns {"error":"invalid_request"}.
Processing continues with the next line; the process exits nonzero if any request was unreadable. Invalid
splice inputs use the evaluation precondition decision. Neither adapter reads expect to produce a result.
Will/1 is final; the wire namespace is will/1. MIT.