AIP-20: WORK.md — agentwork/v2 (work items, coordination protocol, audit)
The work layer of a workspace node — a WORK.md manifest naming the AIP-18 collections a project tracks, a normative `work-item` base collection every work kind extends, and a file-level coordination protocol (rev CAS, claim + lease, atomic write, approvals, verification, audit) under which many agents and humans share one board whose files are the source of truth.
| Field | Value |
|---|---|
| AIP | 20 |
| Title | WORK.md — agentwork/v2 (work items, coordination protocol, audit) |
| Status | Draft |
| Type | Schema |
| Domain | work.sh |
| Doctypes | work.workspace/v2 (WORK.md, the work manifest of one workspace node) + the normative work-item base collection (AIP-18 collection.schema/v1) |
| Requires | AIP-7 (approvals, audit), AIP-18 (collections, items), AIP-34 (workspace node that owns the work root) |
| Replaces | AIP-13 (agentwork/v1) — Superseded, no compatibility layer |
| Composes with | AIP-46 (sessions — claimants), AIP-54 (refs), AIP-58 (runs), AIP-15 (workflows), AIP-9 (operators) |
| Resources | ./resources/aip-20 — WORK.schema.json, collections/ (work-item, task, initiative), ADAPTER.md, EXAMPLES.md |
Abstract
agentwork/v2 is the work layer of a workspace node. A project is
an AIP-34 workspace node; its work: field points at a
WORK.md (work.workspace/v2) that names the AIP-18
collections the project tracks (initiatives, tasks, bugs, …). Every
work collection extends one normative base collection, work-item,
which fixes the fields and the status-category machine every agent can
rely on: owner, claim history, dependencies, approvals, linked runs,
verification, and a revision token.
The heart of this AIP is the coordination protocol: how many agents
and humans write the same board without double-claims, lost updates or
silent resurrection. Items are files and the files are the truth; a
coordinator (the reference one is the agentproto daemon's
TaskStore) wraps around them, enforcing compare-and-swap on rev,
claim leases released on claimant death, an atomic write discipline,
role-based write rights, approval and verification gates, and an
AIP-7 audit trail. The protocol is the one the agentproto
daemon's task ledger proved in production; this AIP is written from it,
and Appendix A maps every ledger
field onto the spec so that one system remains.
Motivation
Two lessons drive this revision.
The item schema does not belong on the workspace AIP.
AIP-13 hardcoded project, initiative and task;
the moment a team needed a fourth kind it had to fork the spec. AIP-18
carved out the type system; AIP-20 keeps only what is common to work:
the base fields and the coordination rules. A team that needs bug or
okr writes a COLLECTION.md that extends work-item.
A board that only declares shape is not a board. The first v2 draft specified manifests, scope axes and view composition, and left concurrency to "git's problem". Meanwhile the agentproto daemon shipped a separate task ledger (JSON file, CAS claims, owner liveness, approvals, verify gates) because agents needed those guarantees today. The result was two truths: a spec nobody's runtime used and a runtime no spec described. Git does not arbitrate a claim between two agents writing the same checkout within the same second; nothing in a frontmatter-only spec prevents two sessions from both believing they own a task. This AIP therefore specifies the protocol, and requires the runtime to store its state as the spec's files.
The project is the workspace node. A project already has an identity, a wall, agents, secrets, apps and a budget as an AIP-34 node. Making it a work item as well gave it two homes. AIP-20 stops at the node boundary: what lives inside a project's work root (initiatives, tasks, any other kind) is AIP-20; the project itself and the company above it are AIP-34 nodes.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.
1. Hierarchy and location
company AIP-34 node (type: company)
└── project AIP-34 node (type: project), work: → WORK.md
└── work root (the directory holding WORK.md)
├── initiative work item (collection: initiative)
│ └── task work item (collection: task, parent: initiative/…)
└── task work itemA workspace node declares its work root with the AIP-34 v2 work:
field ({ file: <path> } or { ref: … }). Any node type MAY declare
one; project nodes conventionally do. A node has at most one work
root. Conventional layout:
<node>/
├── WORKSPACE.md # AIP-34 v2, work: { file: work/WORK.md }
└── work/ # the work root
├── WORK.md # work.workspace/v2 (REQUIRED)
├── collections/<name>/COLLECTION.md # local collections (OPTIONAL)
├── items/<collection>/<id>.md # one AIP-18 item per file (normative)
├── audit/audit-log.jsonl # AIP-7 audit chain for this root
└── .work/ # coordinator state, NOT committed
├── locks/<collection>/<id>.lock
└── leases/<collection>/<id>.jsonThe filename WORK.md and the items/<collection>/<id>.md filing
rule are normative: the coordination protocol addresses items by path.
A collection's AIP-18 identity.filingPath, if set, MUST equal
items/{collection}/{id}.md for collections enabled in a work root.
Scratch roots. A coordinator MAY allocate work roots that belong to
no workspace node — for example one per supervision tree of agent
sessions — under its own state directory. A scratch root is a full work
root (same WORK.md, same item files, same protocol); it is simply not
committed to any repository. Scratch roots exist for ephemeral
coordination; durable work belongs on a node.
Visibility is the node's: read and write access to a work root is
access to the node, governed by AIP-34 v2 walls and includes /
exports doors. AIP-20 adds no visibility mechanism of its own. A
scratch root's visibility is defined by the coordinator that owns it
(the reference coordinator scopes it to the session lineage, see
Appendix A).
2. WORK.md — frontmatter
---
schema: work.workspace/v2
name: <kebab-case-id> # required
title: <human-readable> # required
description: <one-paragraph purpose> # required
# Collections tracked in this root. Every entry MUST resolve to a
# collection that is `work-item` or extends it (AIP-18 extends: chain).
collections:
- ref: ws://collections/task # registry
- ref: ./collections/bug/COLLECTION.md # local file
- inline: { ...COLLECTION.md frontmatter... }
- ref: ws://collections/issue
alias: bug # optional local name
version: "1.x" # optional semver range
# Containment between kinds inside this root.
containment:
allowedParents: # child collection -> allowed parent collections
task: [initiative, task]
initiative: []
maxDepth: 5
# Derived parent status (never stored as authoritative).
statusRollup:
enabled: true
rules:
- { when: all-children-terminal, category: done }
- { when: any-child-waiting, category: waiting }
# Coordination tunables (§4).
coordination:
leaseTtlSeconds: 0 # 0 = liveness-only leases (default)
lockStaleSeconds: 30
# Workspace-wide lints (per-collection lints live on AIP-18).
lints:
- { id: <kebab-id>, kind: orphan | stale | broken-ref | dangling-dependency | custom, severity: error | warn | info, params: {} }
defaults:
workflow: <ref> # OPTIONAL — AIP-15 routine for this root
auditMutations: true # default true (§8)
# Saved queries (§9).
views:
- id: active
title: In flight
filter: { category: [active, waiting] }
groupBy: owner
layout: board
# Cross-AIP bindings.
executor: ws://operators/<slug> # OPTIONAL — AIP-9 default executor
governance: <path-or-ref> # OPTIONAL — AIP-7 GOVERNANCE.md for approvals/policy
knowledge: ws://wikis/<slug> # OPTIONAL — AIP-10 wiki
metadata: { <vendor>: {} }
---WORK.md has no extends: or appliesTo: and no view mode. A
node has one work manifest; per-consumer lenses are views: (saved
queries), and composition across nodes is AIP-34's job. A host MUST
refuse a WORK.md carrying extends or appliesTo with
work_manifest_invalid.
Collections. Entries resolve per AIP-18's resolution
order (inline → local file → registry). Two entries resolving to the
same effective name (alias, else name) are refused with
work_collection_alias_conflict. An entry whose resolved collection
does not have work-item in its extends: chain (or is not
work-item itself) is refused with work_collection_not_work_item.
AIP-18 extends: is a relative file path, so a collection reaches
work-item through a local or vendored copy of its COLLECTION.md;
installing a registry collection vendors its extends: chain with it.
Containment. An item's parent MUST name an item in the same work
root whose collection is listed in allowedParents[<item collection>]
(an empty list means "no parent allowed"; an absent key means "any
enabled collection"). Depth beyond maxDepth is refused. Violations
are refused at write time with work_containment_violation.
Cross-AIP refs. executor, governance and knowledge MUST
resolve inside the node's own tenant; unresolvable refs are refused
with work_xref_unresolvable.
3. The work-item base collection
work-item is a normative AIP-18 collection
(collection.schema/v1, name: work-item) published with this AIP at
resources/aip-20/draft/collections/work-item/COLLECTION.md.
Every work collection extends it. A host MUST NOT let a child collection
remove, retype or relax any field below (AIP-18's drift refusals
already guarantee this).
3.1 Item frontmatter
---
schema: collection.item/v1
collection: task # a collection extending work-item
id: t-01jc8x2k4r # unique within the work root (§3.4)
title: Wire the billing webhook
status: in-progress # a status id of the collection
rev: 7 # §4.1 — coordinator-maintained
owner: session:sess_4117b2b0 # absent = claimable (§3.3 grammar)
createdBy: host # who created it (same grammar)
sessions: [sess_4117b2b0] # append-only claim history (AIP-46 ids)
runs: [] # append-only linked AIP-58 run ids
parent: initiative/billing-v2 # OPTIONAL containment ref
blockedBy: [task/t-01jc8wz0aa] # OPTIONAL dependency refs (§6)
touches: ["packages/billing/**"] # OPTIONAL advisory path claims (§4.6)
approvals: [] # AIP-7 approval-request ids (§7)
artifacts: [] # evidence links (§3.2)
verify: { command: pnpm, args: [test] } # OPTIONAL done gate (§5)
verification: # REQUIRED once status category is done
kind: gate
policyId: pol_9f2
exitCode: 0
ts: "2026-10-17T15:02:11Z"
lastVerifyError: null
closeReason: null # e.g. denied (§7), set on failed/cancelled
releaseReason: null # set when a lease was released (§4.4)
reopenedBy: null # set on done -> todo (§3.5)
note: null # last free-text note from owner/manager
provenance: { prUrl: "https://github.com/..." } # free-form string map
createdAt: "2026-10-17T14:40:00Z"
updatedAt: "2026-10-17T15:02:11Z"
closedAt: "2026-10-17T15:02:11Z"
---
Longer description in markdown (the ledger's `description`).| Field | Type | Required | Written by | Meaning |
|---|---|---|---|---|
status | status id | yes | protocol | Current status; its category (§3.5) drives the protocol. |
rev | integer ≥ 1 | yes | coordinator only | Revision token, +1 on every applied write (§4.1). |
owner | principal ref | no | claim / manager | Current owner. Absent = claimable. |
createdBy | principal ref | yes | create | Creator; the creator is a manager of the item (§4.5). |
sessions | string[] | no | claim / reassign | Every session that ever owned the item, append-only. |
runs | string[] | no | owner / manager | AIP-58 run ids that executed work for this item, append-only. |
parent | item ref | no | manager | Containment (§2). |
blockedBy | item ref[] | no | manager | Dependencies (§6). |
touches | glob[] | no | manager / owner | Advisory file paths the work will edit (§4.6). |
approvals | string[] | no | owner / manager / link | AIP-7 approval-request ids gating this item (§7). |
artifacts | link[] | no | owner / manager | Evidence trail (§3.2). |
verify | gate spec | no | create / manager | Done gate (§5). |
verification | object | when done | protocol | How done was reached (§5). Never absent on a done item. |
lastVerifyError | string | no | protocol | Why the last gated done did not transition. |
closeReason, releaseReason, reopenedBy, note | string | no | protocol / owner / manager | Typed provenance of system transitions and notes. |
provenance | map string→string | no | owner / manager | Free-form links (PR URL, worktree path, action id). Advisory; never changes protocol meaning. |
createdAt, updatedAt, closedAt | quoted RFC 3339 | createdAt, updatedAt yes | coordinator | closedAt set on entering a terminal category, cleared on reopen. |
The body is the item's description. Fields not listed above are collection-specific (AIP-18 flat fields).
3.2 Artifact links
artifacts:
- { session: sess_4117b2b0, key: report.md, sha256: "9b1c…" } # session artifact
- { approval: apr_01jc9 } # AIP-7 approval payload
- { run: run_01jc9a, key: out/diff.patch } # AIP-58 run artifactThe coordinator stores and returns links; it never dereferences them.
3.3 Principal refs
owner and createdBy use the AIP-54 compact form
<kind>:<body>, restricted to:
| Ref | Meaning |
|---|---|
session:<id> | A live runtime session (AIP-46). Leases on it follow the session's liveness (§4.4). |
operator:<slug> | A durable AIP-9 operator persona. |
user:<id> | A named human. |
user | A human acting through a host surface without a resolved identity. |
host | The coordinator's root principal — the human operating the host directly (CLI, root MCP endpoint, HTTP API). |
A session is an execution instance, not an identity: one operator may
own many sessions over time. Hosts that know the operator behind a
session MAY record it in provenance; the owner stays the session so
that liveness can be checked.
3.4 Item ids and refs
id MUST match ^[a-z0-9][a-z0-9-]{0,63}$ and be unique within the
work root (not only within its collection). Hosts SHOULD generate
sortable ids (a lowercase ULID with a collection prefix, e.g.
t-01jc8x2k4r…).
An item ref is <collection>/<id> within the same work root, or
ws://workspaces/<node-id>#work/<collection>/<id> across work roots
(the AIP-34 includes fragment form). parent MUST be a local ref;
blockedBy MAY cross work roots when the target node exports its work
root to the caller's node.
3.5 Status categories and the protocol machine
AIP-18 statuses carry a category. For work collections
the category is REQUIRED on every status, and the protocol is defined
over categories, so a collection may name and add statuses freely while
every agent still knows what they mean.
| Category | Terminal | Meaning |
|---|---|---|
todo | no | Not being worked. Claimable when owner is absent. |
active | no | Owned and being worked. |
waiting | no | Owned, paused on something outside the owner (approval, external party). Never released by liveness. |
done | yes | Completed. verification MUST be present. |
failed | yes | Attempted and failed. |
cancelled | yes | Abandoned or denied. |
A status's AIP-18 terminal flag MUST be true exactly when its
category is done, failed or cancelled. A collection MUST declare exactly one initialStatus, and
its category MUST be todo.
Category transitions (the only legal moves; a collection's
transitionsTo may narrow them, never widen them). todo → active
appears twice on purpose: an unowned item only enters active by
claim; an item pre-assigned at creation is started by its owner with an
ordinary update.
| From → to | Who | Verb |
|---|---|---|
todo → active | any caller with write access, item unowned | claim only (§4.3) |
todo → active | owner (pre-assigned) | update |
active → todo | owner or manager | release (§4.4) or update |
active → waiting | owner or manager; approvals non-empty when entering an approval wait | update / link-approval |
waiting → active | owner or manager, or system on approval granted | update / system |
active → done | owner or manager, subject to the verify gate | update (§5) |
active → failed | owner or manager | update |
todo / active / waiting → cancelled | manager, or system on approval denied | update / system |
done → todo | manager only | reopen: records reopenedBy, clears closedAt and verification |
failed, cancelled | — | final; no transition out |
Within one category a collection MAY declare several statuses (e.g.
in-progress and in-review both active); moves between them are
ordinary updates subject to transitionsTo.
Base statuses. work-item declares: pending (todo, initial),
in-progress (active), awaiting-approval (waiting), done (done),
failed (failed), cancelled (cancelled). A child collection MAY add
statuses; it MUST NOT remove these.
A blocked condition is derived, not a status: an item in todo
whose blockedBy contains a non-terminal item is blocked (§6).
Collections SHOULD NOT add a blocked status.
4. Coordination protocol
All writes to a work root go through a coordinator: a process that implements the verbs below over the files. Several coordinators MAY serve the same root (for example the daemon and a standalone CLI); they coordinate through the lock files of §4.2. Agents and tools MUST NOT edit item frontmatter directly while a coordinator serves the root; direct edits are handled as external edits (§4.7).
4.1 Revision CAS
Every item carries rev. Every write verb except create and
link-approval takes the rev the caller last read. The coordinator
applies the write only if the supplied rev equals the file's current
rev; on success it writes rev + 1. On mismatch it MUST answer a
conflict carrying the current item, and MUST NOT modify the file.
System transitions (§4.4, §5, §7) also increment rev.
Every verb answers one of three shapes:
type WorkWriteResult =
| { ok: true; item: WorkItem; verifying?: true } // applied (verifying: §5)
| { ok: false; conflict: true; current: WorkItem } // rebase and retry
| { ok: false; conflict: false; code: WorkErrorCode; message: string }4.2 Atomic write
A coordinator applies a write to items/<c>/<id>.md as follows:
- Lock. Create
.work/locks/<c>/<id>.lockwith exclusive-create semantics (O_CREAT|O_EXCL), containing{host, pid, acquiredAt}. If it exists, retry with backoff; a lock older thancoordination.lockStaleSecondsMAY be broken (delete, then retry exclusive-create). A coordinator that is the only writer of a root MAY use an in-process mutex instead, but MUST still honour foreign lock files. - Read and check. Read the current file, verify
rev(§4.1), validate the transition (§3.5), the caller's rights (§4.5) and the resulting item against the collection's resolved schema (AIP-18). - Write. Write the full new file to
<id>.md.tmp-<host>-<pid>in the same directory, flush it to stable storage, then rename it over<id>.md. Hosts SHOULD also flush the directory. - Audit. Append the audit event (§8) before releasing the lock.
- Unlock. Delete the lock file.
create takes the lock on the new id and fails with work_id_taken
if <id>.md exists. Readers never take locks: rename atomicity
guarantees a reader sees either the old or the new file.
Shared indexes (one file listing many items) MUST NOT be a source of truth. A coordinator MAY keep an in-memory or cached index, rebuilt from the item files at start and kept current by its own writes and a file watcher.
4.3 Claim
claim(itemRef, rev) succeeds only if owner is absent and rev
matches — either failure is a conflict carrying the current item, so
two racing claimants can never both succeed. The item MUST be in a
todo status (else work_not_claimable). On success the coordinator
sets owner to the caller, moves the item to the collection's first
active status (in-progress for the base), appends the caller's
session id to sessions (session callers only) and writes the lease
(§4.4).
Pre-assignment is not a claim: create or a manager update MAY set
owner on a todo item; the item stays todo until its owner moves
it to active. Moving an unowned item to active by update is refused
with work_unowned_start.
4.4 Lease and liveness
A claim by a session: owner creates a lease,
.work/leases/<c>/<id>.json:
{ "item": "task/t-01jc8x2k4r", "owner": "session:sess_4117b2b0",
"host": "studio-mac", "claimedAt": "…", "renewedAt": "…", "ttlSeconds": 0 }Leases are coordinator state, not item content: they are not committed and their renewals never touch the item file.
- The issuing host MUST release the item when the owning session exits, and at start-up for every lease whose session did not survive.
- Release = clear
owner, move anactiveitem back to the collection's initialtodostatus, setreleaseReason(e.g.owner-exited,host-restart), delete the lease, bumprev, audit. - An item in a
waitingcategory is never released by liveness: a human decision may be outstanding. - When
ttlSeconds > 0(fromcoordination.leaseTtlSeconds), the owner's host renewsrenewedAt; any coordinator MAY release a lease not renewed forttlSeconds. WithttlSeconds: 0(the default) only the issuing host releases, on observed session death. - Owners
operator:,user,user:andhosthave no lease and are never released automatically.
Liveness release never fails an item: an item is intent, not a run; its work may be resumed by another claimant.
4.5 Rights
Two roles, no per-field matrix:
- Owner — the principal in
owner(hostandusercallers act as owner of items owned byhostoruser). The owner MAY changestatuswithin the category machine, release itself, setnote,approvals,artifacts,runs,touches,provenance. - Manager — the item's
createdBy, or thehostprincipal, or any principal the bound AIP-7 governance grantswork.manageon the root. A manager MAY additionally edittitle, body,blockedBy,parent,verify, reassign or clearowner, cancel and reopen.
Anyone with write access to the root MAY create and claim. A write
by neither owner nor manager is refused with work_forbidden. System
transitions (liveness release, approval decided, verify settled) are
not subject to rights; they are attributed to host in the audit.
4.6 Path claims (touches)
touches lists globs relative to the node's root that the work will
edit. It is advisory: at claim, a coordinator SHOULD warn the
claimant (work_touches_overlap, a warning in the applied result) when
the globs overlap those of another active or waiting item in the
same root. It never refuses a claim and never locks files.
4.7 External edits
A coordinator records the content hash of every file it writes. A file whose hash differs from the last one written (a human edit, a git pull, another tool) is an external edit:
- If the file still validates and its
revis greater than or equal to the last knownrev, the coordinator adopts it, writesrev = max(known, file) + 1and appends anwork.item.external-editaudit event. - If it does not validate, the coordinator MUST NOT load it as an item:
it surfaces
work_item_invalidand keeps serving the last valid version read-only until the file is fixed.
4.8 Git
The files are designed to be committed. Rules that keep one truth:
- A work root MUST be read and written in one authoritative checkout. Agents working in per-task git worktrees or branches MUST reach the board through the coordinator, never through their own copy of the item files — otherwise each branch holds its own board.
.work/MUST be excluded from version control.- Coordinators SHOULD batch commits of item and audit files rather than commit per write.
- On a merge conflict in an item file, the version with the higher
revwins; equalrevwith different content is surfaced aswork_merge_conflictfor a manager to resolve.
4.9 Verbs
| Verb | Input | Notes |
|---|---|---|
create | collection, title, body?, owner?, parent?, blockedBy?, verify?, touches?, provenance? | Status = collection initialStatus; createdBy = caller; rev: 1. |
get | item ref | |
list | filter: collection?, status?, category?, owner?, parent?, includeClosed? (default false) | Terminal items excluded unless asked. |
claim | item ref, rev | §4.3. |
update | item ref, rev, and any of: status, title, body, owner (string = reassign, null = release), blockedBy, parent, approvals, artifacts, runs, touches, note, provenance, evidence | Release cannot be combined with a status change. evidence is only valid with a move to done (§5). |
link-approval | item ref, approval id | §7. No rev (idempotent). |
Error codes: work_not_found, work_forbidden, work_invalid_transition,
work_not_claimable, work_unowned_start, work_approval_required,
work_verify_in_flight, work_evidence_not_passed, work_id_taken,
work_containment_violation, work_item_invalid, work_merge_conflict.
Hosts expose these verbs over their own transports (the reference host:
MCP task_create / task_claim / task_update / task_list, and
HTTP /tasks).
5. Verification (done integrity)
Two tiers, chosen per item:
- Tier 0 — no
verify. A move to adonestatus is applied immediately and stampedverification: {kind: self-report, by, ts}(or{kind: human, ts}when the caller isuser/host). Visible as self-reported, never silently casual. - Tier 1 —
verifydeclared. A move todoneis accepted but not applied: the answer is{ok: true, verifying: true}, the item staysactive, and the coordinator runs the gate in the background. Pass →donewithverification: {kind: gate, policyId?, exitCode?, ts},lastVerifyErrorcleared. Fail → item staysactive,lastVerifyErrorset. Both outcomes bumprevand are audited. A second done while a gate runs is refused withwork_verify_in_flight. An outcome that arrives after the item leftactive(released, cancelled) is discarded. - Evidence shortcut.
update {status: done, evidence: {policyId}}appliesdoneimmediately if the host can independently confirm that policy passed; otherwisework_evidence_not_passed.
verify is a host gate spec (the reference host accepts its
completion-policy gate: {command, args?, cwd?, timeoutMs?} or a judge
gate). A host that cannot run a declared gate MUST fail it, never pass
it. A coordinator MUST NOT launder a failed gate into done, and MUST
NOT infer done from a session's turn ending.
6. Dependencies
blockedBy lists item refs that must reach a terminal category before
this item should start. It is informational for the state machine:
it never refuses a transition. Hosts MUST expose the derived condition
blocked (a todo item with a non-terminal blockedBy target) in
get/list results, and SHOULD prefer unblocked items when
recommending what to claim. The lint dangling-dependency flags refs
that do not resolve. Cycles in blockedBy are reported by lint, not
refused.
7. Approvals
Approvals are AIP-7 approval-request records; AIP-20
only links them.
- When an owner needs a human decision, it creates an AIP-7 approval
request with
taskIdset to the item ref, then callslink-approval(item, approvalId): the id is appended toapprovals(idempotent) and anactiveitem moves to the collection's approval-wait status (awaiting-approval). Only the owner or a manager may link, and only on anactiveorwaitingitem. - Entering a
waitingstatus by update with emptyapprovalsis refused withwork_approval_requiredwhen the target status is the approval-wait status. - On the AIP-7 decision, the coordinator applies a system
transition to every
waitingitem listing that approval:approved→ back toin-progress;denied→cancelledwithcloseReason: denied. An item that already leftwaitingis not touched.expiredtriggers no transition; it surfaces on the item. - The owner or a manager MAY leave the wait manually
(
waiting → active) or cancel.
Whether a transition requires an approval is a governance decision:
the bound GOVERNANCE.md policies (AIP-7 defaultApproval, policy:
refs) decide; AIP-20 carries no approval-class field of its own.
8. Audit and history
With defaults.auditMutations: true (the default), every applied
write and every system transition appends one AIP-7 audit-event to
<work root>/audit/audit-log.jsonl (the root is the AIP-7 scope), in
the same lock as the item write (§4.2). Event fields:
action:work.item.created,.claimed,.status(any applied write that changesstatus, including system transitions),.updated(an update that changes other fields only),.released,.reopened,.approval-linked,.verify-passed,.verify-failed,.external-edit— exactly one action per applied write;actor: the caller's principal ref (hostfor system transitions);entity: the item ref;input:{ fromRev, toRev, changes }wherechangeslists changed field names with old and new values forstatus,ownerandparent.
An item's history is the audit log filtered by entity; sessions
and runs are its denormalised claim and execution edges. Setting
auditMutations: false is allowed only for scratch roots.
9. Views
views: are saved queries over the root, not manifests:
views:
- id: <kebab-id>
title: <label>
filter: { collection?, status?, category?, owner?, parent?, blocked?, includeClosed? }
groupBy: category | status | owner | parent | collection
layout: list | board | tree | timelineThe first view is the default. A consumer (an operator, a dashboard) selects a view by id; it cannot change the root's schema or protocol.
10. Status rollup
Rollup is derived at read time and never stored as the parent's
authoritative status. Rules map a condition over a parent's direct
children to a category exposed as rolledCategory on read:
when | Condition |
|---|---|
all-children-terminal | every child is in a terminal category |
any-child-waiting | at least one child is waiting |
any-child-blocked | at least one child is derived-blocked (§6) |
any-child-overdue | a child has a past dueAt and a non-terminal category |
no-children | no children |
Rules are evaluated in order; the first match wins.
Rationale
Why files as the truth, with a coordinator. Files give agents and
humans one readable, diffable, committable record. Files alone do not
give mutual exclusion; the coordinator supplies it with the two
mechanisms that survive every filesystem the hosts run on: exclusive
lock-file creation and atomic rename. The rev in the frontmatter
makes the CAS visible in the record itself, so any coordinator — not
only the one that issued the last write — can check it.
Why leases outside the item. A lease changes on every heartbeat and
on every host restart; putting it in frontmatter would make every
liveness tick a git diff and a merge conflict. The item records durable
intent (owner); the lease records who vouches that the owner is
alive.
Why categories rather than fixed statuses. The protocol needs to know "claimable", "in flight", "parked" and "closed"; teams want their own names. Categories give both, and rollups and agents read the category, not a literal id.
Why blockedBy does not gate. The ledger shipped it informational
and that has been enough; a gating dependency machine needs a
scheduler and a deadlock story. The derived blocked condition gives
agents the signal without the machine.
Why no view mode. extends:/appliesTo: on WORK.md duplicated
AIP-34's tree composition and AIP-18's collection composition — three
merge mechanisms for one board — and allowed a malformed chain to fall
back to local-only, silently dropping inherited guarantees. One
manifest per node plus saved queries covers the use cases.
Why no compatibility layer. The family has no external
agentwork/v1 deployments to protect. AIP-13 is Superseded without a
loader promise; keeping a v1 starter library would keep a second shape
alive for no user.
Reference Implementation
@agentproto/work— WORK.md validation; the file-backed coordinator (item store, locks, leases, audit) is to land here.@agentproto/collection— AIP-18 collection resolution and item validation, includingcategory.- The agentproto daemon's task ledger (
packages/runtime/src/task-ledger.ts) is the protocol's origin; Appendix A maps it field by field, and the non-normative migration section describes its move onto the file store.
Backwards Compatibility
None is provided. AIP-13 is Superseded; hosts need not
load work.workspace/v1 manifests or work/v1 items. The earlier
AIP-20 draft's extends, appliesTo, scope, display,
defaults.approvalClass fields and the agentwork-v1-compat starter
collections are removed; manifests using them are refused with
work_manifest_invalid. A former AIP-13 project becomes an AIP-34
project node whose work: points at a WORK.md.
Security Considerations
- Double claim / lost update — prevented by the rev CAS and the exclusive lock (§4.1–4.3). A coordinator that skips the lock on a root served by others is non-conforming.
- Forged ownership or self-approval — rights (§4.5) restrict who may reassign, cancel or reopen; approvals are decided only through AIP-7 human channels, and agents never decide them (AIP-7).
- Done laundering — Tier-1 gates cannot be bypassed by update; the evidence shortcut requires an independently checkable passed policy; failed gates are recorded (§5).
- Silent tampering by direct file edit — external edits are detected by hash, re-validated and audited (§4.7); the AIP-7 audit chain is hash-linked, so a rewritten history is detectable.
- Stale-lock denial of service — locks expire after
lockStaleSeconds; leases on dead sessions are released (§4.4). - Path traversal — item ids are restricted (§3.4) and filing is
fixed, so an id cannot address a file outside
items/. - Cross-root references —
blockedByacross roots resolves only through an AIP-34 export to the caller's node; refs are never a read grant.
Migration (non-normative)
How the reference daemon moves from ~/.agentproto/tasks.json to work
roots without a long coexistence:
- TaskStore seam. The ledger's verbs keep their MCP/HTTP surface;
persistence moves behind a
TaskStoreinterface with the current JSON file as its first implementation. - FileStore. A store implementing §4 over work roots ships in
@agentproto/work: item files, lock files, leases, audit chain. Boards map to roots per Appendix A (ws:<slug>→ the node's work root;tree:<root>and explicit board ids → scratch roots). - Short dual write. For one release, writes go to both stores with the JSON store authoritative; at start-up and on every write the daemon compares the two and logs divergences. Status ids are written in kebab case in files; the tools accept the old underscore spellings during this release only.
- Switch-over. The FileStore becomes authoritative; a one-shot
import converts any remaining
tasks.jsonrecords into items, then the JSON store, the underscore aliases andtasks.jsonare deleted. No fallback reader is kept.
Appendix A — ledger field mapping
Every field and rule of the daemon task ledger lands in exactly one place in this spec; nothing is kept twice.
Ledger (task-ledger.ts) | AIP-20 | Note |
|---|---|---|
taskId (UUID) | id (+ collection, filing path) | New ids are ULID-based; imported UUIDs are prefixed (t-<uuid>). |
boardId ws:<slug> | the work root of AIP-34 node <slug> | |
boardId tree:<rootSessionId>, spawn-stamped meta.boardId, explicit board | a scratch root owned by the coordinator | Lineage visibility rule unchanged (§1). |
title / description | title / markdown body | |
status pending / in_progress / awaiting_approval / done / failed / cancelled | base statuses pending / in-progress / awaiting-approval / done / failed / cancelled (categories todo / active / waiting / done / failed / cancelled) | Same machine (§3.5). |
ALLOWED_TRANSITIONS | category transitions (§3.5) | done → pending = reopen. |
owner sessionId / "operator" / "human" | session:<id> / host / user | §3.3. Absent = claimable, unchanged. |
createdBy | createdBy | Same grammar. |
blockedBy (informational) | blockedBy + derived blocked | Still non-gating (§6). |
approvalIds, linkApproval, approval:decided handler | approvals, link-approval, system transition (§7) | Denied → closeReason: denied (was meta.reason). |
artifacts ({sessionId,key,sha256} / {approvalId}) | artifacts ({session,key,sha256} / {approval} / {run,key}) | Run links added. |
sessions[] | sessions | Append-only, unchanged. |
| — | runs | New: AIP-58 edge. |
verify (GateSpec) | verify | §5. |
verification {self-report,gate,human} | verification | Unchanged. |
lastVerifyError | lastVerifyError | Unchanged. |
evidence shortcut {policyId} | update … evidence | Unchanged. |
rev CAS on every write, conflict returns current | rev (§4.1) | Now in the file. |
claim CAS (owner absent ∧ rev match) | claim (§4.3) | Unchanged. |
| owner / manager (creator or operator) rights | §4.5 | Plus governance-granted managers. |
release on session:exited and at boot; no release while awaiting_approval | lease + liveness (§4.4) | Lease moved to .work/leases/. |
meta.reopenedBy, meta.releasedReason, meta.reason, meta.note | reopenedBy, releaseReason, closeReason, note | Typed fields. |
other meta keys (prUrl, worktreePath, actionId) | provenance | |
createdAt / updatedAt / closedAt | same | |
task:changed bus event | the audit event (§8); hosts MAY keep an in-process event | |
atomic tmp + rename of tasks.json, debounced | per-item atomic write (§4.2) | No shared file. |
list excludes closed by default | list includeClosed default false | |
| — | parent, touches, views, rollup, collections | New in AIP-20. |
See also
- AIP-7 — governance, approval, audit — approval requests and the audit chain
- AIP-13 — agentwork/v1 — Superseded by this AIP
- AIP-18 — COLLECTION.md / ITEM.md — collections, items, status categories
- AIP-34 — WORKSPACE.md — the node that owns a work root (
work:) - AIP-46 — agent sessions — session claimants
- AIP-54 — refs — principal and item ref grammar
- AIP-58 — RUN — runs linked from items
Resources
Supporting artifacts for AIP-20. Links open the file on GitHub — markdown and JSON render natively in GitHub's viewer. Browse the full resource tree →
- ADAPTER.mdaip-20/draft/ADAPTER.md
- EXAMPLES.mdaip-20/draft/EXAMPLES.md
- WORK.schema.jsonaip-20/draft/WORK.schema.json
- COLLECTION.mdaip-20/draft/collections/initiative/COLLECTION.md
- COLLECTION.mdaip-20/draft/collections/task/COLLECTION.md
- COLLECTION.mdaip-20/draft/collections/work-item/COLLECTION.md
- SKILL.mdaip-20/draft/skills/author-work-workspace/SKILL.md
AIP-19: SECRETS.md — secret inventory + reveal contract
A workspace-level manifest format for declaring secret slugs, their purpose, access grants, and audit metadata — without ever storing the values themselves. Hosts resolve slugs against a real vault at reveal time.
AIP-21: AGENCY.md — agentagencies/v2 (commercial agency workspace on AIP-18 collections)
A workspace-only successor to AIP-8 that drops the eleven hardcoded agency doctypes (service, engagement, agreement, deliverable, invoice, counterparty, procedure, pricing-model, routine, capacity, agency) and delegates all per-doctype schema work to AIP-18 collections — owning only the workspace root manifest, the engagement lifecycle helpers that span collections, scope axes, and cross-AIP composition with strong governance and work bindings.