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.

FieldValue
AIP20
TitleWORK.md — agentwork/v2 (work items, coordination protocol, audit)
StatusDraft
TypeSchema
Domainwork.sh
Doctypeswork.workspace/v2 (WORK.md, the work manifest of one workspace node) + the normative work-item base collection (AIP-18 collection.schema/v1)
RequiresAIP-7 (approvals, audit), AIP-18 (collections, items), AIP-34 (workspace node that owns the work root)
ReplacesAIP-13 (agentwork/v1) — Superseded, no compatibility layer
Composes withAIP-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 item

A 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>.json

The 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`).
FieldTypeRequiredWritten byMeaning
statusstatus idyesprotocolCurrent status; its category (§3.5) drives the protocol.
revinteger ≥ 1yescoordinator onlyRevision token, +1 on every applied write (§4.1).
ownerprincipal refnoclaim / managerCurrent owner. Absent = claimable.
createdByprincipal refyescreateCreator; the creator is a manager of the item (§4.5).
sessionsstring[]noclaim / reassignEvery session that ever owned the item, append-only.
runsstring[]noowner / managerAIP-58 run ids that executed work for this item, append-only.
parentitem refnomanagerContainment (§2).
blockedByitem ref[]nomanagerDependencies (§6).
touchesglob[]nomanager / ownerAdvisory file paths the work will edit (§4.6).
approvalsstring[]noowner / manager / linkAIP-7 approval-request ids gating this item (§7).
artifactslink[]noowner / managerEvidence trail (§3.2).
verifygate specnocreate / managerDone gate (§5).
verificationobjectwhen doneprotocolHow done was reached (§5). Never absent on a done item.
lastVerifyErrorstringnoprotocolWhy the last gated done did not transition.
closeReason, releaseReason, reopenedBy, notestringnoprotocol / owner / managerTyped provenance of system transitions and notes.
provenancemap string→stringnoowner / managerFree-form links (PR URL, worktree path, action id). Advisory; never changes protocol meaning.
createdAt, updatedAt, closedAtquoted RFC 3339createdAt, updatedAt yescoordinatorclosedAt 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).

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 artifact

The 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:

RefMeaning
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.
userA human acting through a host surface without a resolved identity.
hostThe 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.

CategoryTerminalMeaning
todonoNot being worked. Claimable when owner is absent.
activenoOwned and being worked.
waitingnoOwned, paused on something outside the owner (approval, external party). Never released by liveness.
doneyesCompleted. verification MUST be present.
failedyesAttempted and failed.
cancelledyesAbandoned 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 → toWhoVerb
todo → activeany caller with write access, item unownedclaim only (§4.3)
todo → activeowner (pre-assigned)update
active → todoowner or managerrelease (§4.4) or update
active → waitingowner or manager; approvals non-empty when entering an approval waitupdate / link-approval
waiting → activeowner or manager, or system on approval grantedupdate / system
active → doneowner or manager, subject to the verify gateupdate (§5)
active → failedowner or managerupdate
todo / active / waiting → cancelledmanager, or system on approval deniedupdate / system
done → todomanager onlyreopen: 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:

  1. Lock. Create .work/locks/<c>/<id>.lock with exclusive-create semantics (O_CREAT|O_EXCL), containing {host, pid, acquiredAt}. If it exists, retry with backoff; a lock older than coordination.lockStaleSeconds MAY 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.
  2. 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).
  3. 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.
  4. Audit. Append the audit event (§8) before releasing the lock.
  5. 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 an active item back to the collection's initial todo status, set releaseReason (e.g. owner-exited, host-restart), delete the lease, bump rev, audit.
  • An item in a waiting category is never released by liveness: a human decision may be outstanding.
  • When ttlSeconds > 0 (from coordination.leaseTtlSeconds), the owner's host renews renewedAt; any coordinator MAY release a lease not renewed for ttlSeconds. With ttlSeconds: 0 (the default) only the issuing host releases, on observed session death.
  • Owners operator:, user, user: and host have 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 (host and user callers act as owner of items owned by host or user). The owner MAY change status within the category machine, release itself, set note, approvals, artifacts, runs, touches, provenance.
  • Manager — the item's createdBy, or the host principal, or any principal the bound AIP-7 governance grants work.manage on the root. A manager MAY additionally edit title, body, blockedBy, parent, verify, reassign or clear owner, 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 rev is greater than or equal to the last known rev, the coordinator adopts it, writes rev = max(known, file) + 1 and appends an work.item.external-edit audit event.
  • If it does not validate, the coordinator MUST NOT load it as an item: it surfaces work_item_invalid and 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 rev wins; equal rev with different content is surfaced as work_merge_conflict for a manager to resolve.

4.9 Verbs

VerbInputNotes
createcollection, title, body?, owner?, parent?, blockedBy?, verify?, touches?, provenance?Status = collection initialStatus; createdBy = caller; rev: 1.
getitem ref
listfilter: collection?, status?, category?, owner?, parent?, includeClosed? (default false)Terminal items excluded unless asked.
claimitem ref, rev§4.3.
updateitem ref, rev, and any of: status, title, body, owner (string = reassign, null = release), blockedBy, parent, approvals, artifacts, runs, touches, note, provenance, evidenceRelease cannot be combined with a status change. evidence is only valid with a move to done (§5).
link-approvalitem 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 a done status is applied immediately and stamped verification: {kind: self-report, by, ts} (or {kind: human, ts} when the caller is user/host). Visible as self-reported, never silently casual.
  • Tier 1 — verify declared. A move to done is accepted but not applied: the answer is {ok: true, verifying: true}, the item stays active, and the coordinator runs the gate in the background. Pass → done with verification: {kind: gate, policyId?, exitCode?, ts}, lastVerifyError cleared. Fail → item stays active, lastVerifyError set. Both outcomes bump rev and are audited. A second done while a gate runs is refused with work_verify_in_flight. An outcome that arrives after the item left active (released, cancelled) is discarded.
  • Evidence shortcut. update {status: done, evidence: {policyId}} applies done immediately if the host can independently confirm that policy passed; otherwise work_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 taskId set to the item ref, then calls link-approval(item, approvalId): the id is appended to approvals (idempotent) and an active item moves to the collection's approval-wait status (awaiting-approval). Only the owner or a manager may link, and only on an active or waiting item.
  • Entering a waiting status by update with empty approvals is refused with work_approval_required when the target status is the approval-wait status.
  • On the AIP-7 decision, the coordinator applies a system transition to every waiting item listing that approval: approved → back to in-progress; denied → cancelled with closeReason: denied. An item that already left waiting is not touched. expired triggers 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 changes status, 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 (host for system transitions);
  • entity: the item ref;
  • input: { fromRev, toRev, changes } where changes lists changed field names with old and new values for status, owner and parent.

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 | timeline

The 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:

whenCondition
all-children-terminalevery child is in a terminal category
any-child-waitingat least one child is waiting
any-child-blockedat least one child is derived-blocked (§6)
any-child-overduea child has a past dueAt and a non-terminal category
no-childrenno 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, including category.
  • 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 — blockedBy across 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:

  1. TaskStore seam. The ledger's verbs keep their MCP/HTTP surface; persistence moves behind a TaskStore interface with the current JSON file as its first implementation.
  2. 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).
  3. 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.
  4. Switch-over. The FileStore becomes authoritative; a one-shot import converts any remaining tasks.json records into items, then the JSON store, the underscore aliases and tasks.json are 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-20Note
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 boarda scratch root owned by the coordinatorLineage visibility rule unchanged (§1).
title / descriptiontitle / markdown body
status pending / in_progress / awaiting_approval / done / failed / cancelledbase statuses pending / in-progress / awaiting-approval / done / failed / cancelled (categories todo / active / waiting / done / failed / cancelled)Same machine (§3.5).
ALLOWED_TRANSITIONScategory transitions (§3.5)done → pending = reopen.
owner sessionId / "operator" / "human"session:<id> / host / user§3.3. Absent = claimable, unchanged.
createdBycreatedBySame grammar.
blockedBy (informational)blockedBy + derived blockedStill non-gating (§6).
approvalIds, linkApproval, approval:decided handlerapprovals, 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[]sessionsAppend-only, unchanged.
—runsNew: AIP-58 edge.
verify (GateSpec)verify§5.
verification {self-report,gate,human}verificationUnchanged.
lastVerifyErrorlastVerifyErrorUnchanged.
evidence shortcut {policyId}update … evidenceUnchanged.
rev CAS on every write, conflict returns currentrev (§4.1)Now in the file.
claim CAS (owner absent ∧ rev match)claim (§4.3)Unchanged.
owner / manager (creator or operator) rights§4.5Plus governance-granted managers.
release on session:exited and at boot; no release while awaiting_approvallease + liveness (§4.4)Lease moved to .work/leases/.
meta.reopenedBy, meta.releasedReason, meta.reason, meta.notereopenedBy, releaseReason, closeReason, noteTyped fields.
other meta keys (prUrl, worktreePath, actionId)provenance
createdAt / updatedAt / closedAtsame
task:changed bus eventthe audit event (§8); hosts MAY keep an in-process event
atomic tmp + rename of tasks.json, debouncedper-item atomic write (§4.2)No shared file.
list excludes closed by defaultlist includeClosed default false
—parent, touches, views, rollup, collectionsNew in AIP-20.

See also

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 →