| Status | Draft |
| Version | 0.3 (2026-10-27); formerly Tasks 0.2 and Human Task Handoff (HTH) 0.1 |
| Extension | acts/0.3, an extension of Principal Handoff Core 0.3 |
| License | Apache-2.0 |
| Schemas | schemas/ (record schema in ../core/schemas/) |
| Conformance suite | conformance/ |
| Reference | ../reference/ |
Agents working for someone often find work only that someone can do: pay an invoice, sign a contract, buy a part, renew a domain. This extension of Principal Handoff covers such acts. It defines their terms and outcomes on top of the core registers, and a Markdown binding in which agents declare acts as lines in a project’s state document, the document they already read and write, and a registrar turns those lines into records and applies the principal side’s settlements back to the document.
Appendix A: Worked example · Appendix B: Mapping notes for the reference deployment
The key words in this document are to be interpreted as in the core, which also defines principal, principal side, requester, delegate, item, key, terms, record, claim, relation and note. This extension adds:
act: work only the principal can do, because it needs their identity, credentials, money, legal
capacity or physical presence, or because they reserved it. Agents SHOULD NOT declare acts for work an agent could do
with the access it has.principal are acts; agent and undecided lines are parsed so that every
tool reads the section the same way, but are never raised.principal, agent, or undecided (a decision is needed before the work can be assigned; such a decision may be
raised under the Decisions extension).{"text": …, "due"?: "YYYY-MM-DD"}. text is a single line.done (the principal did it), dropped (it will not be done), already_done (it had
been done before it was raised, or repeats an item already settled; the settlement SHOULD be accompanied by a
same_as relation to that item).done, dropped.not_needed.Records use the core record with ext: "acts/0.3".
type |
origin |
Emitted by | Schema | Additional requirements |
|---|---|---|---|---|
raised |
requester |
the requester, or the registrar on its behalf | raised |
terms.text, terms_rev: 1 |
amended |
requester |
as above | amended |
terms, terms_rev |
withdrawn |
requester |
as above | withdrawn |
reason |
settled |
principal |
the principal side | settled |
outcome of §2, authority, terms_rev |
reopened |
principal |
the principal side | reopened |
supersedes, restore_text |
claimed |
agent, registrar, requester |
anyone else | claimed |
outcome of §2; evidence.basis |
related |
principal (same_as), requester (derived_from) |
related |
relation |
|
note |
any | note |
about |
restore_text on reopened is a copy of the item’s current terms text, so that a registrar without storage can
re-add the line. It is not an amendment. Need records MUST list the item’s current need record in supersedes
(core §6).
The registrar is the binding’s only writer of key markers and the only component that applies records mechanically (core §8). It holds the project’s requester role (core §1) for the records it raises. In one run over a project’s document it SHOULD, in this order:
settled (any outcome), strike every
line carrying the key (§5.5.2); for a current reopened, re-add the line from its restore_text if no line
carries the key (§5.5.3). A settled for a key that no line carries is consumed and changes nothing. Notes and
relations are delivered to the project (core §8), never applied to the document.raised with
terms_rev: 1, and after the transport accepts it, stamp the line. If the send fails, it MUST NOT stamp. If
stamping fails, the next run re-derives the same key and re-sends the same record, which the principal side
discards as a duplicate.amended with the
new text, terms_rev increased by one, and the current need record in supersedes. An agent changes what it asks
by editing the text and keeping the marker.claimed with outcome: "done" and evidence.basis saying why it believes the work
is done (for example the change that removed the line and its stated reason). A vanished line is never a
withdrawal: a deletion carries no intent the registrar can read. At most one such claim per key per open period
(§7). A withdrawn act whose line is deleted gets no claim: its requester already said it no longer needs it.Agents that saw the principal do the work (for example in a session where the principal says “I paid it”) send a
claimed; an agent that no longer needs an act sends withdrawn through its transport, outside the document.
Invariants:
This binding is normative for implementations that declare acts in Markdown. Settlements, claims and notes reach the document only through the registrar (§4).
A profile fixes the concrete strings of the binding. A conforming implementation MUST support at least one profile and MUST state which. Profile parameters:
| Parameter | Meaning |
|---|---|
heading |
The exact line that starts the tasks section. MUST be a level-2 ATX heading (## …). |
label_open, label_close |
The characters that enclose an assignee label. |
labels.principal, labels.agent, labels.undecided |
The label text for each assignee class. Labels MUST be distinct and MUST NOT contain label_close. |
label_separator |
What the registrar writes between the closing bracket and the text when it adds a line. |
placeholder |
The line that means “no tasks”. MUST NOT begin with - [. |
marker_render |
Template of the key marker the registrar appends, containing {key}. |
marker_pattern |
Regular expression that recognises a key marker at the end of a line; group 1 captures the key. MUST match the output of marker_render for every valid key, and SHOULD be lenient about spacing and punctuation width. |
A profile MAY itself have parameters (for example, the principal’s tag in §5.7), which an implementation fills in from
configuration. Machine-readable definitions of both profiles below are in
conformance/profiles.json.
Notation used below: O and C are label_open and label_close; L is any of the three labels; regex fragments
use the common subset of PCRE, Python re and ECMAScript.
The tasks section is the body following the first line that equals heading (ignoring trailing whitespace), up to
but not including the next line that begins with # or ## , or the end of the document. A document without such
a line has no tasks section and therefore no tasks.
Readers MUST classify each line of the tasks section, in order, with trailing whitespace removed, as follows. A current group starts empty at the top of the section.
^#{3,4}\s*O([^C]+)C\s*$. If the captured text is one of the labels, the current group
becomes that assignee class; otherwise the current group becomes unknown. Group headings are not tasks.- [ ], - [x] or - [X] followed by whitespace or end of line are ignored:
blank lines, the placeholder, prose, block quotes, other headings, and indented lines (continuations, sub-lists).
Ignored lines do not change the current group.^- \[ \] O(L)C\s*(.+)$. Assignee class from the label; body = group 2. A labelled
item’s own label wins over the current group.^- \[ \] (?!O)(.+)$ while the current group is an assignee class. Assignee class
from the group; body = group 1.- [x], which agents MUST remove instead of ticking), an
unknown label, an empty body, or an unlabelled item outside a known group. Readers MUST NOT treat a malformed line
as a task and SHOULD report it so a person or agent can fix it.For each task, the reader then looks for a key marker by searching the body with marker_pattern. If found, the key
is group 1 and the text is the body before the match; otherwise there is no key and the text is the whole body. The
text is trimmed of surrounding whitespace (internal whitespace is kept). A task whose text is then empty, or whose key
does not satisfy §6.1, is malformed.
Writers (agents and the registrar) SHOULD use the labelled form for new lines. The grouped form exists because people and models naturally write it.
The key marker is marker_render with {key} replaced by the key, appended to the end of the task line. Because
marker_pattern is anchored at the end of the line, a marker-like string elsewhere in the text is part of the text,
not a key.
To stamp a line with key k, the registrar removes trailing whitespace from the line and appends the rendered marker
for k. Nothing else on the line changes.
To strike key k, the registrar deletes every checkbox line in the tasks section whose key is k. If at least one
line was deleted and the section body then contains only blank lines, the registrar MUST insert placeholder as the
first line of the body. (A body that still holds, say, an empty group heading is not empty.)
To re-add key k with text t, the registrar builds the line
- [ ] + O + labels.principal + C + label_separator + normalise(t) + rendered marker for k
(normalisation as in §6.1), and:
heading, a blank line and the new line to the document;placeholder, replaces it with the new line;Blank lines are not significant: conformance compares documents as their sequences of non-blank lines (§9).
en)| Parameter | Value |
|---|---|
heading |
## Tasks |
label_open / label_close |
[ / ] |
| labels | principal: owner; agent: agent; undecided: undecided |
label_separator |
one space |
placeholder |
- (none) |
marker_render |
` (waiting on owner: {key})` (leading space) |
marker_pattern |
\(waiting on owner:\s*([^\s()]+)\s*\)\s*$ |
## Tasks
- [ ] [owner] Renew the domain before it expires (waiting on owner: task-demo-37863bc42c)
- [ ] [owner] Approve the logo direction
- [ ] [agent] Add retries to the uploader
- [ ] [undecided] Postgres or SQLite for the cache?
### [owner]
- [ ] Sign the hosting contract
In the grouped form, a task text cannot begin with [ (it would read as a label); use the labelled form for such
text.
zh-hant)The reference deployment writes its state documents in Traditional Chinese. Its profile has one parameter,
principal_tag, the short name the principal is known by in task lines (the conformance suite uses Owner).
| Parameter | Value |
|---|---|
heading |
## 待辦 (“to do”) |
label_open / label_close |
〔 / 〕 (U+3014 / U+3015) |
| labels | principal: {principal_tag}; agent: agent; undecided: 待定 (“to be decided”) |
label_separator |
empty |
placeholder |
- (無) (“none”) |
marker_render |
(等 {principal_tag}:{key}) (“waiting for {principal_tag}”), ASCII parentheses and colon, no leading space |
marker_pattern |
[((]等 ?{principal_tag}[::]\s*([^()()]+?)\s*[))]\s*$ with {principal_tag} regex-escaped: either width of parenthesis and colon, optional space after 等 |
## 待辦
- [ ] 〔Owner〕續約網域(等 Owner:task-demo-544fefce49)
- [ ] 〔agent〕上傳加重試
- [ ] 〔待定〕快取用 Postgres 還是 SQLite?
### 〔Owner〕
- [ ] 簽主機合約
^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$. (No whitespace, parentheses or colons, so that it is safe
inside every marker syntax.)White_Space=yes, including U+3000 IDEOGRAPHIC SPACE) with a single U+0020 and removes leading and trailing
whitespace.The reference deployment derives
key = "task-" + project + "-" + hex(sha1(utf8(normalise(text))))[0:10]
with lowercase hex. In Python, " ".join(text.split()) performs the normalisation of §6.1 for all practical text.
conformance/keys.json holds vectors for this recipe; implementations that use another
recipe skip that file. SHA-1 is used only as a cheap, stable fingerprint, not for security; ten hex digits make an
accidental collision within one project negligible.
Ids carry a type prefix and a colon, which keys cannot contain (§6.1), so ids of different records never collide.
raised: r:<key>, so that a crashed registrar re-sends the same id.amended: a:<key>:<terms_rev>.claimed for a vanished key: c:<key>:r<n>, where n is the number of reopened records the
registrar has applied for the key (0 before the first). A task that vanishes again after being reopened thus gets a
new claim.| Tasks 0.2 event | Acts 0.3 record |
|---|---|
registered (registrar), id = key |
raised, origin requester, sent by the registrar, id r:<key>, terms_rev: 1 |
done, dropped, already_done (principal) |
settled with that outcome; authority.kind principal, or delegated/attested where 0.2 had decided_by: assistant |
reopened (principal) |
reopened superseding the settlement; 0.2’s text becomes restore_text |
done, dropped (agent) |
claimed with that outcome |
inferred done (registrar), id done-<key> |
claimed, origin registrar, outcome: "done", evidence.basis, id c:<key>:r0 |
a resolution’s note |
the settlement’s note, delivered to the project |
conformance/ holds JSON fixtures; its README documents the formats.
| File | Exercises |
|---|---|
profiles.json |
the en and zh-hant profiles |
parse.json |
§5.2–§5.4: document → task lines and malformed lines |
keys.json |
§6.2 example recipe (only for implementations that use it) |
relink.json |
§4 step 2: document + vanished open keys → re-stamped document |
register.json |
§4 step 3 and §5.5.1: document → stamped document and raised records |
amend.json |
§4 step 4: document with edited keyed lines + latest terms → amended records |
apply.json |
§4 step 1, I2, §5.5.2–§5.5.3: document + record → document |
reconcile.json |
§4 step 5: document + open keys → claims or warnings |
records.json |
§3: record → schema it validates against, or invalid |
lifecycle.json |
core §6 and §2’s keep policy: the records one receiver holds for one act → its state |
Documents are compared as their sequences of non-blank lines with trailing whitespace removed.
The core considerations apply. In addition:
settled makes the registrar delete a line; a forged reopened
makes it add one with attacker-chosen restore_text that agents will later read. Registrars MUST accept settlements
only from the principal side. Keeping state documents under version control makes any wrong strike recoverable.raised is discarded as a duplicate. Until a later version addresses this (for example with a
registrar-held generation in the derivation), agents SHOULD make re-declared texts distinct, and registrars SHOULD
warn when a new line derives a key that is already settled.terms.due has no Markdown syntax yet.Non-normative.
| Implementation | Roles | Profiles | Status |
|---|---|---|---|
../reference/ in this repository |
reader, registrar logic (no transport) | en, zh-hant |
passes the conformance suite |
| agent-hub (to be published) | registrar, requesters; its personal assistant is the principal side | zh-hant |
the reference deployment this document is generalised from; implements HTH 0.1, not yet 0.3 |
Non-normative. Profile en, project demo, principal p:owner, principal side ps:assistant.
A coding agent finds the domain is about to expire and adds to STATE.md:
## Tasks
- [ ] [owner] Renew the domain
That night the registrar derives task-demo-c3d8af57d4, sends
{"ph": "0.3", "ext": "acts/0.3", "id": "r:task-demo-c3d8af57d4", "type": "raised", "origin": "requester",
"by": "registrar:demo", "key": "task-demo-c3d8af57d4", "project": "demo", "ts": "2026-10-27T02:10:00Z",
"terms": {"text": "Renew the domain"}, "terms_rev": 1}
and stamps the line: - [ ] [owner] Renew the domain (waiting on owner: task-demo-c3d8af57d4).
The principal renews the domain and ticks it off in their assistant, which sends
{"ph": "0.3", "ext": "acts/0.3", "id": "s-81f2", "type": "settled", "origin": "principal", "by": "ps:assistant",
"key": "task-demo-c3d8af57d4", "project": "demo", "ts": "2026-10-28T08:30:00Z", "terms_rev": 1,
"outcome": "done", "authority": {"kind": "principal", "decider": "p:owner"}}
The next run strikes the line; the section is now empty, so it becomes - (none).
Had an agent deleted the line instead, the registrar would have sent a claimed with evidence.basis naming the
change that removed it, and the assistant would have asked the principal “Did you renew the domain?” before settling
anything.
Non-normative. The reference deployment uses its own message names on an internal event bus:
| Reference deployment | Acts 0.3 |
|---|---|
message type task sent to the principal, with a fixed id equal to the key |
raised (id = r:<key>) |
principal-side handoff with effect done / dropped / already_done |
settled with that outcome |
principal-side handoff with effect reopened |
reopened |
handoff field reply_to |
key |
handoff field item |
restore_text on reopened, otherwise the project’s own wording in a note |
agent message done / drop with a task key |
claimed with outcome done / dropped |
done with inferred: true, id done-<key> |
registrar claimed, id c:<key>:r0 |
handoff effect resolves / changes / relevant on an unkeyed to-do |
note with about.quote |
context: migration on the first run |
context |