protocol agentproto.shcli cli.agentproto.shpanel /panel
agentproto

AIP-20: WORK.md — agentwork/v2 (typed coordination workspace on AIP-18 collections)

A workspace-only successor to AIP-13 that drops hardcoded project/initiative/task doctypes and delegates all per-item-kind schema work to AIP-18 collections — owning only the workspace root manifest, scope axes, status rollups, and cross-AIP composition.

FieldValue
AIP20
TitleWORK.md — agentwork/v2 (typed coordination workspace on AIP-18 collections)
StatusDraft
TypeSchema
Domainwork.sh
Doctypeswork.workspace/v2 (workspace manifest + view, written as WORK.md)
RequiresAIP-18
ReplacesAIP-13 (agentwork/v1) — once Final
Composes withAIP-3 (skills), AIP-6 (companies), AIP-7 (governance), AIP-8 (agencies), AIP-9 (operators), AIP-10 (knowledge), AIP-12 (playbooks), AIP-15 (workflows), AIP-18 (collections)
Resources./resources/aip-20 — WORK.schema.json, ADAPTER.md, EXAMPLES.md, SKILL.md, starters/

Abstract

agentwork/v2 is a workspace-only successor to AIP-13. It defines a single doctype — work.workspace/v2, written as WORK.md — that declares the shape of a coordination tracker without baking the per-item-kind schemas into the spec. Where AIP-13 hardcoded project, initiative, and task as first-class doctypes, AIP-20 lifts every per-item-kind concern (fields, status state machine, ownership cardinality, deadline kind, lint rules) to AIP-18 COLLECTION.md files and keeps only the workspace-level concerns at the WORK.md layer: which collections are enabled, how scope axes apply across collections, when parent items roll up child statuses, what lints span the whole workspace, and how the workspace binds to the rest of the AIP family. The same doctype, used recursively via extends:, also expresses per-context views — an operator (AIP-9), a company (AIP-6), or a skill (AIP-3) ships its OWN WORK.md that adapts the base workspace for its lens. AIP-20 ships a small starter library (starters/agentwork-v1-compat/) of three AIP-18 collections that mirror AIP-13's hardcoded doctypes so existing v1 trackers can be loaded under v2 without breaking.

Motivation

The hard lesson from AIP-13 was that the per-item-kind schema does not belong on the workspace AIP. AIP-13's three hardcoded doctypes — project, initiative, task — were the right call for shipping v1 (concrete, debuggable, narrow), but they collapsed the moment a real team needed a fourth kind. An engineering team wants bug. A revenue team wants okr. An incident team wants postmortem. With v1, every additional kind required either a fork of the workspace AIP or a smuggled-in metadata.<vendor> extension that the spec cannot validate. The cost of mixing workspace concerns with item-schema concerns shows up as soon as the workspace tries to serve more than one team's vocabulary.

The fix is to separate workspace concerns from item-schema concerns along the seam that AIP-18 carved out. AIP-18 owns the type system: a COLLECTION.md declares the schema for a class of records (fields, statuses, ownership cardinality, deadline kind, lints, identity rules); an ITEM.md is a single record validated against a named collection. AIP-20 owns everything above the type system: which collections a workspace tracks, how scope axes apply across them, when parent items aggregate child status, what lints span the whole tracker, and how the workspace binds to operators, governance, knowledge, agencies, playbooks, and workflows. Two AIPs, two surfaces, one mental model. A team that needs a fourth kind writes a fourth COLLECTION.md (an authoring problem), not a revision to the workspace spec (a registry problem).

This is exactly the registry-of-views pattern codified in AIP-2 and applied to wikis in AIP-10 and to governance in AIP-7. One workspace doctype, two modes:

  • Workspace-root mode (<work-root>/WORK.md, no extends:) declares the BASE shape — which collections are enabled, what the scope axes mean, how rollups behave.
  • View mode (<consumer>/WORK.md, extends: set) adapts the base for a specific operator/company/skill — narrowing the visible collections, adding a workspace-level lint, rebinding governance for that context. The tracker is one; the views are many.

This composability is what makes the registry trustworthy: every view is just another WORK.md, validated against the same schema, merged via the same algorithm. A reviewer auditing a deployed tracker hashes the merged effective config; a third-party importer follows extends: to its terminal root; a runtime swap re-derives the same lens from disk.

AIP-13 is deliberately preserved as Draft for the duration of this transition. The migration path is conservative: AIP-20 ships a starter library (starters/agentwork-v1-compat/) containing three AIP-18 collections — project, initiative, task — whose fields, statuses, ownership rules, and deadline semantics mirror AIP-13's hardcoded doctypes. An existing AIP-13 workspace can opt into v2 by adding a WORK.md at its tracker root and pointing collections: at the starter library; existing items load unchanged. Richer migrations (introducing new kinds, extending starter collections with team-specific fields) are an operator concern, not a spec concern. AIP-13 will move to Superseded once AIP-20 reaches Final, with the deprecation banner on AIP-13 documenting the path.

Specification

A conforming agentwork/v2 deployment is a single doctype: work.workspace/v2, written as WORK.md. The workspace's items live under AIP-18 collections — AIP-20 does NOT define an item doctype of its own. Every per-item-kind concern is deferred to AIP-18. Every workspace-level concern is owned by this AIP.

File location

The filename WORK.md is normative — discovery, install, and toolchains key off it. The path is conventional, not normative. Conventional layout:

<work-root>/
├── WORK.md                          # workspace manifest (REQUIRED at the work-tree root)
├── collections/                     # AIP-18 collections enabled by this workspace
│   ├── project/
│   │   └── COLLECTION.md            # AIP-18 collection definition
│   ├── initiative/
│   │   └── COLLECTION.md
│   └── task/
│       └── COLLECTION.md
└── items/                           # AIP-18 ITEM.md records, filed by collection
    ├── project/
    │   └── <slug>.md
    ├── initiative/
    │   └── <slug>.md
    └── task/
        └── <slug>.md

Per-context views live alongside their consumer, not under the work root:

operators/eng-lead/WORK.md         # extends ../../<work-root>/WORK.md
companies/acme/WORK.md             # extends ../../<work-root>/WORK.md
skills/triage-tasks/WORK.md        # extends ../../<work-root>/WORK.md

A view's extends: field points to a parent WORK.md (workspace root OR another view). appliesTo: binds the view to one or more operator/company/skill workspace refs. The host resolves the chain on load and exposes the merged effective config to the consumer.

WORK.md — frontmatter shape

---
schema: work.workspace/v2
name: <kebab-case-id>                       # required
title: <human-readable>                     # required
description: <one-paragraph purpose>        # required
version: <semver>                           # required, the WORKSPACE version

# Composition (view mode)
extends: ../path/to/parent/WORK.md          # OPTIONAL
appliesTo:                                  # OPTIONAL — bind this view to consumers
  - ws://operators/<slug>                   #   AIP-9 operator
  - ws://companies/<slug>                   #   AIP-6 company
  - ws://skills/<slug>                      #   AIP-3 skill

# Cross-AIP composition (the centre of gravity)
executor: ws://operators/<slug>             # OPTIONAL — AIP-9 default executor
governance: <path-or-ref>                   # OPTIONAL — AIP-7 governance binding
knowledge: ws://wikis/<slug>/KNOWLEDGE.md   # OPTIONAL — AIP-10 wiki binding
agency: ws://agencies/<slug>                # OPTIONAL — AIP-8 agency context
playbook: ws://playbooks/<slug>             # OPTIONAL — AIP-12 active playbook

# The collections this workspace tracks. Three forms supported.
collections:                                # array; merge-by-name with parent
  # 1. Inline declaration (small workspaces, no sharing):
  - inline:
      schema: collection.schema/v1
      name: task
      title: Task
      description: An atomic unit of work owned by a single operator.
      version: 1.0.0
      fields: [...]
      statuses: [...]
      ownership: { cardinality: single, role: assignee, required: false }
      # full COLLECTION.md frontmatter, parsed in-place under AIP-18
  # 2. File ref (shared with peers):
  - ref: ./collections/eng-bug/COLLECTION.md
  # 3. Registry import:
  - ref: ws://collections/okr
  # With aliasing (for renaming or version pinning at workspace level):
  - ref: ws://collections/issue
    alias: bug
    version: "1.x"

# Scope axes (the AIP-13 distinctive insight, kept at workspace level)
scope:
  containment:
    enabled: true
    field: parent                           # which item field carries the containment ref
    rules:
      allowedKinds: [project, task]         # OPTIONAL constraints
      maxDepth: 5
  applicability:
    enabled: true
    field: appliesTo                        # which item field carries scope-applicability list
    valueClass: company | role | role-and-company | <custom>
  ownership:
    enabled: true
    field: owner                            # delegates to per-collection ownership.role by default
    policy: strict | inherit | open

# Status rollup — workspace-level rules for how parent items aggregate child status
statusRollup:
  enabled: true | false
  policy:
    - when: "all-children-terminal"
      bubbleParentStatus: closed
    - when: "any-child-blocked"
      bubbleParentStatus: blocked
  exposeViaField: rolledStatus              # OPTIONAL — host writes this field on parents

# Workspace-spanning lints (AIP-18 lints are per-collection; these span collections)
lints:                                       # array; merge-by-id with parent
  - id: <kebab-id>
    kind: orphan-across-collections | stale-tree | broken-parent-ref | scope-mismatch | custom
    severity: error | warn | info
    params: { <key>: <value> }

# Routine workflow defaults (composes with AIP-15)
defaults:
  workflow: <ref>                           # OPTIONAL — default WORKFLOW.md path or ref
  approvalClass: auto | always | on-mutate | policy:<ref>
  auditMutations: true | false              # ONE-WAY SWITCH — once true, child can't disable

# Display / UX hints
display:
  homePage: <slug>                          # OPTIONAL — landing item id
  defaultGrouping: kind | status | owner | parent
  defaultView: list | board | tree | timeline

metadata:                                    # vendor extensions, namespaced under <vendor>
  <vendor>:
    <field>: <value>
---

# <body — markdown prose>

Conventional sections in the body include:

- ## Purpose — what this workspace tracks, who uses it
- ## Conventions — when an item belongs in collection A vs collection B
- ## What this workspace does NOT track — set boundaries explicitly
- ## When to extend vs replace — composition guidance

The body is free-form markdown. The frontmatter is the contract.

Collection declaration

The collections: array is the bridge between AIP-20 and AIP-18. Each entry MUST be one of three forms:

1. Inline declaration. A full collection.schema/v1 frontmatter, parsed in-place. The host registers the collection directly via AIP-18's defineCollection without loading a separate file. Useful for small, single-tenant workspaces where the collection is not shared:

collections:
  - inline:
      schema: collection.schema/v1
      name: task
      title: Task
      description: An atomic unit of work.
      version: 1.0.0
      fields:
        - { name: priority, type: enum, enum: [low, normal, high] }
      statuses:
        - { id: open, label: Open }
        - { id: done, label: Done, terminal: true }
      ownership: { cardinality: single, role: assignee, required: false }

2. File ref. A relative path to a COLLECTION.md on disk. The host loads the file via AIP-18 and registers the collection. Useful when the collection is shared with peer workspaces or when the collection's version is managed independently:

collections:
  - ref: ./collections/eng-bug/COLLECTION.md

3. Registry import. A ws://collections/<slug> URI resolved against the host's collection registry. Useful for cross-workspace sharing and for installing third-party collection definitions:

collections:
  - ref: ws://collections/okr

Aliasing. Any ref form MAY carry an alias: to expose the collection in this workspace under a different name. This is the escape hatch for ref-name conflicts (two registry collections both named issue) and for workspace-local naming preferences (bug locally, issue upstream):

collections:
  - ref: ws://collections/issue
    alias: bug
    version: "1.x"

When alias: is set, items in this workspace MUST reference the alias, not the upstream name (collection: bug, not collection: issue). The host MUST refuse a workspace whose two entries resolve to the same effective name (alias or upstream) with work_collection_alias_conflict (HARD).

When version: is set on a ref, it pins the schema version range (semver). Schema bumps outside the range fail with the AIP-18 collection_item_schema_pinned_drift HARD refusal at item load time.

The collections: array merges across the extends: chain by effective name (alias if set, otherwise the collection's name). A child entry with the same effective name replaces the parent's ref or inline; new effective names are appended. A child MAY also disable a parent's collection by setting an explicit override that omits the entry — but the host enforces the AIP-18 guarantee that items remain valid, so disabling a collection that has live items surfaces a work_collection_disabled_with_items warning.

Scope axes

The three orthogonal axes from AIP-13 — containment, applicability, ownership — survive in v2 as a workspace-level declaration rather than a per-item field set. The workspace declares which axes are enabled, which item field carries each axis, and what value class the axis accepts. The actual fields live on items; their semantics are owned by the per-collection schema AIP-18 defines. AIP-20's job is to ensure the three axes remain orthogonal across collections.

AxisQuestion it answersItem field (default)Per-collection counterpart
containmentWhat is this part of?parentAIP-18 parent (top-level)
applicabilityWho is this about / scoped to?appliesToa collection-declared field with type: array of refs
ownershipWho is doing this?ownerAIP-18 ownership.role

Ownership is the cleanest case. AIP-18 already declares per-collection ownership cardinality and role-field name (owner by default, overridable to assignee or lead). AIP-20 does not redefine ownership; it declares the workspace-level policy (strict, inherit, open) and reads the collection's ownership rules at item-validation time. A workspace setting scope.ownership.policy: strict requires every collection's resolved schema to mark ownership.required: true; a workspace with policy: inherit lets collections decide.

Containment is the parent/child axis. The item field parent (borrowed from AIP-18's universal-ish layer) carries the containment ref. The workspace narrows it: allowedKinds: [project, task] means task items may have a parent of kind project or task only — not initiative. maxDepth caps recursion. The host MUST enforce these constraints at item-write time.

Applicability is the visibility axis. The item field (default appliesTo) is a list of refs declaring who the item is about. The workspace's valueClass declares the kind of refs the list accepts: company (refs to AIP-6 companies), role (refs to AIP-6 roles), role-and-company (compound refs), or a custom class the host knows how to resolve. Hosts MUST validate every value against the declared class.

The three axes are declared once at the workspace level, not per-collection. This is the AIP-13 inheritance: keeping applicability orthogonal to containment orthogonal to ownership prevents the conflations that creep into trackers when those concepts collapse into a single field. AIP-18's per-collection schemas are free to add domain-specific fields on top, but the three axes themselves are workspace-uniform.

Per-item override. A collection MAY declare its own field name for any axis (e.g. a task collection that uses assignee instead of owner); when it does, the host reads the collection's field name in preference to the workspace default. Per-collection naming is a local optimization; the workspace's valueClass and policy still govern.

Status rollup

Per-collection statuses live on AIP-18 — every COLLECTION.md declares its own state machine, transitions, and terminal flags. AIP-20 adds a workspace-level concept: when a parent item's children all terminate, the parent's effective status SHOULD reflect that. Rollup is derived, not stored — it's computed at query time (or, optionally, materialized into a rolledStatus field on parents).

statusRollup:
  enabled: true
  policy:
    - when: "all-children-terminal"
      bubbleParentStatus: closed
    - when: "any-child-blocked"
      bubbleParentStatus: blocked
    - when: "any-child-overdue"
      bubbleParentStatus: at-risk
  exposeViaField: rolledStatus

Conditions on when: are limited to a small vocabulary the host MUST recognize:

when clauseMeaning
all-children-terminalEvery direct child's status is terminal (per AIP-18).
any-child-blockedAt least one direct child has status id blocked.
any-child-overdueAt least one direct child has dueAt past now and a non-terminal status.
no-childrenThe parent has no children at all.
custom:<id>A host-defined predicate keyed by id.

bubbleParentStatus: MUST reference a status id that exists in every collection eligible to be a parent (i.e. every collection declared in scope.containment.rules.allowedKinds). The host MUST refuse a workspace whose rollup policy bubbles a status id that does not exist in some eligible parent collection — surface as work_status_rollup_invalid (warn; the rollup just degrades to no-op for non-conforming parents).

exposeViaField: is an optional materialization hint: when set, the host writes the rolled status to the named field on parent items during background passes. When unset, the rolled status is query-time only and never written to disk.

Rollup is one-directional: parent ← children. Cross-collection rollups respect the resolution order: a child of one collection bubbles up to a parent of another collection by following the parent ref through AIP-18's descendants resolution.

Composition (extends: chain)

When a host loads a WORK.md whose extends: is set, it MUST:

  1. Walk the parent chain. Recursively load the parent referenced by extends:; that parent's parent if it has one; until a manifest with no extends: is reached. Maximum chain depth is eight. Hosts MUST detect cycles by tracking visited absolute paths.
  2. Treat depth overflow and cycle detection as warnings, not errors. A view whose chain is malformed MUST still load — the runtime falls back to the local manifest only and surfaces work_extends_cycle (or work_extends_depth_exceeded) as a warning to the consumer's debug surface.
  3. Tolerate a missing parent. If extends: points to a path that does not exist, the host emits work_extends_missing as a warning and uses the local manifest only.
  4. Merge bottom-up. Walk the chain from the workspace root toward the leaf view, merging each manifest into the accumulator using the strategy below.

Merge strategy (child wins on conflicts):

FieldStrategyNotes
name, title, description, versionoverrideChild's identity wins.
extendslocal-onlyNot inherited.
appliesTolocal-onlyNot inherited. Each child declares its own scope.
executor, governance, knowledge, agency, playbookoverrideChild can rebind. Subject to one-way switches and governance gating.
collectionsmerge-by-effective-nameChild entry with same alias-or-name → child replaces parent's; new effective names appended.
scope.containment.*leaf-field deep-mergeenabled, field, rules.* each override independently. ONE-WAY: enabled: true cannot be downgraded by a descendant.
scope.applicability.*leaf-field deep-mergeONE-WAY: valueClass cannot drift across descendants.
scope.ownership.*leaf-field deep-mergepolicy may narrow (open → inherit → strict); widening triggers a governance check.
statusRollup.enabled, statusRollup.exposeViaFieldoverride
statusRollup.policymerge-by-whenChild entry with same when: clause → child replaces parent's; new clauses appended.
lintsmerge-by-idChild lint with same id → child replaces parent's. New ids appended.
lints[].severitychild winsSubject to governance: a parent's policy MAY forbid softening below error.
defaults.*leaf-field overrideworkflow, approvalClass, auditMutations each override independently. ONE-WAY: auditMutations: true cannot be downgraded.
display.*leaf-field override
metadatadeep-mergeRecursive merge; vendor namespaces accumulate.

The host MUST expose both the merged effective config AND the resolution chain (ordered list of absolute paths consumed during merge) on its debug surface. Consumers use the merged config; tooling uses the chain to explain why a field has the value it does.

One-way switches (HARD refusals)

Three fields are one-way switches: once enabled or set at any ancestor in the extends: chain, descendants MUST NOT relax them. Attempting to relax surfaces a HARD refusal — the view does NOT degrade to local-only, it fails to load.

FieldSwitch directionRefusal code
defaults.auditMutations: trueOnce true at any ancestor, child cannot set false.work_audit_downgrade
scope.containment.enabled: trueOnce true at any ancestor, child cannot set false (would orphan items).work_scope_disable
scope.applicability.valueClass: <X>Once set at any ancestor, child cannot change to a different class (would invalidate existing items).work_scope_value_class_drift

These three rules are why a deployed v2 tracker is trustworthy: an auditor inspecting any view can verify that the workspace's audit trail, containment integrity, and applicability semantics hold across every descendant. Without the one-way invariants, a malicious or careless view could silently relax the audit trail or invalidate existing items by changing the value class. (This pattern mirrors AIP-7's governance one-way switches.)

appliesTo enforcement

appliesTo is not inherited. A view's bindings are local; the parent's bindings do not leak into the child. Cross-field constraint: appliesTo present ⇒ extends REQUIRED. A workspace that binds to a consumer must extend a parent — a binding without an extension is semantically a workspace-root with a consumer claim, which the registry-of-views pattern rejects as ill-formed.

Hosts MUST refuse a view whose appliesTo references a non-existent consumer with work_appliesto_unresolvable. This is a HARD failure, not a warning — the same posture AIP-10 takes for its workspace bindings.

Cross-AIP composition

WORK.md is the centre of gravity for AIP-family composition. The table below lists every cross-AIP ref and the AIP that owns the binding's semantics.

FieldTarget AIPPurpose
executorAIP-9 operatorDefault executor for items in this workspace — the operator the host activates when an item with no explicit assignee surfaces.
governanceAIP-7 policy / auditStatus-transition approvals, owner-change audits, scope-widening interventions flow through this binding. Workspace-root manifests usually set this; views may override only if the parent's policy permits.
knowledgeAIP-10 KNOWLEDGE.mdWiki this workspace refers to. Lets cross-references on items resolve against the bound wiki by default.
agencyAIP-8 agencyBinds the workspace to an autonomous-agency context for client-billable engagements, time-tracking, and contractual approval.
playbookAIP-12 playbookActive playbook governing routine plays this workspace runs.
defaults.workflowAIP-15 WORKFLOW.mdDefault routine workflow that runs against work items in this workspace (e.g. nightly stale-task sweep).
collections[].ref / collections[].inlineAIP-18 COLLECTION.mdPer-item-kind schema. EVERY item-schema concern is delegated here.
appliesToAIP-3 skill, AIP-6 company, AIP-9 operatorView binding.
extendsanother WORK.mdComposition.

A host MUST verify that every cross-AIP ref it loads resolves — unresolvable refs surface as work_xref_unresolvable (HARD for executor/governance/knowledge/agency/playbook; mirrors the AIP-10 posture).

Body conventions

The frontmatter ends; the body is markdown. Conventional sections:

  • ## Purpose — what this workspace tracks and who uses it.
  • ## Conventions — when an item belongs in collection A vs collection B; which scope axes apply.
  • ## What this workspace does NOT track — set boundaries explicitly. Helps reviewers reject mis-filed items.
  • ## When to extend vs replace — composition guidance for downstream view authors.
  • ## Examples — short snippets of typical items.

The body is free-form. The contract lives in the frontmatter.

Workspace root manifest (WORK.md)

Per the convention codified in AIP-2, every Workspace AIP MUST define a root manifest doctype. AIP-20's root manifest is the same work.workspace/v2 doctype used in two modes:

  • Workspace-root mode — <work-root>/WORK.md, no extends:. Declares the BASE shape: enabled collections, scope axes, status rollups, lints, defaults. Lives at the root of the work tree.
  • View mode — <consumer>/WORK.md, extends: set. Adapts the base for a specific operator/company/skill — narrows the visible collections, adds a workspace-level lint, rebinds governance for that context.

The same schema validates both modes; the host distinguishes by checking whether extends: is set. The same merge algorithm applies recursively. The same authoring skill (./resources/aip-20/draft/skills/author-work-workspace/SKILL.md) walks an agent through both flows.

AspectWorkspace-root modeView mode
File path<work-root>/WORK.md<consumer>/WORK.md
extends:absentrequired
appliesTo:absentOPTIONAL but conventional
Effective shapethe manifest as writtenmerge of the chain, child wins
Mutabilityedits gated by governancelocal edits adapt the lens, do not affect the workspace
Use casestracker authors, schema designersoperator/company/skill teams who want their own lens
Validationfull schema checkschema check + chain validation + one-way-switch check
Lifecycleversioned with the trackerversioned with the consumer

Runtime binding: work items ↔ a task ledger

Everything above assumes a filesystem-first workspace: items are ITEM.md files, concurrency is git's problem, and WORK.md is a file a host parses off disk. Some hosts back a workspace with a live runtime store instead — an in-memory or journalled ledger with optimistic-concurrency writes, addressed through an API rather than a file path, with no ITEM.md tree and no _index/ to regenerate. This section specifies how such a runtime-backed workspace conforms to agentwork/v2. The reference implementation is agentproto's daemon Task ledger (task-ledger.ts): a TaskRecord store keyed by taskId, with a boardId grouping/visibility scope, a nullable owner (a session id, "human", or "operator"), an append-only sessions[] claim history, an informational blockedBy[], an optional verify gate and a verification record of how done was reached, and a rev CAS token on every write.

Conformance profile

A runtime-backed workspace is a work.workspace/v2 deployment whose items live in a runtime store rather than as ITEM.md files under AIP-18 collections. The following are REQUIRED for conformance:

  1. The runtime MUST expose, per item, the AIP-18 universal-core semantics — a stable id, a title, and single-collection membership (an item belongs to exactly one collection-equivalent type, even if the runtime implements only one implicit collection).
  2. The runtime MUST implement a status machine with an explicitly enumerated status set and a well-defined terminal subset, and MUST validate every transition against it (mirroring AIP-18's status state machine and this AIP's transition-validation posture).
  3. The runtime MUST expose containment and ownership as independently queryable dimensions — the reference ledger already does, via boardId and owner respectively. The runtime SHOULD additionally expose applicability as a dimension separable from containment, per the decomposition in "The three axes" below; the reference ledger does not separate the two today.
  4. The runtime MUST provide a concurrency-safe write path, per "Concurrency" below.

The following are OPTIONAL:

  • A literal WORK.md / COLLECTION.md / ITEM.md file tree. A runtime-backed workspace MAY declare its shape (enabled collections, scope-axis config, status rollup policy) in whatever host-native form it already uses for configuration; it is not required to materialize that shape as on-disk markdown to conform. Likewise _index/work.json-style caches are moot — the runtime store IS the index.
  • Multiple collections. A runtime MAY implement a single implicit collection (as the reference ledger does — every TaskRecord is the same shape) and still conform, provided the single-collection membership rule in point 1 holds.
  • The verification extension (below).

Status mapping

work's status vocabulary (open | claimed | in-progress | blocked | done | archived, from AIP-13) and the reference ledger's TaskStatus (pending | in_progress | done | failed | cancelled) overlap on exactly one value verbatim (done) and one more by rename (in-progress / in_progress). A runtime-backed workspace exposing a work-shaped view MUST translate at the boundary using the following tables, and MUST NOT expose both spellings of the same state as if they were distinct values.

work → ledger:

work statusLedger TaskStatusNotes
openpendingDirect correspondence — unclaimed, available to claim.
claimedin_progressThe ledger has no intermediate "claimed but not started" state — claim() sets status: in_progress in the same write that assigns owner. A runtime MUST NOT synthesize a claimed state that does not correspond to a real, independently-observable ledger state.
in-progressin_progressSame state; hyphen vs underscore is a lexical difference the translation boundary MUST normalize.
blocked— (none)The ledger has no blocked status. See the note on blockedBy below.
donedoneDirect correspondence.
archived— (none)The ledger has no post-terminal archival tier distinct from done / failed / cancelled.

Ledger → work:

Ledger TaskStatuswork statusNotes
pendingopen
in_progressin-progressA work-shaped consumer that also wants to distinguish "claimed, not yet started" from "actively being worked" gets no help from the ledger — both collapse to in_progress.
donedone
failed— (none)No work counterpart. A runtime SHOULD represent this as done plus a queryable failure marker (meta, a label, or a workspace-level status-list extension via AIP-20's statuses[] merge), rather than silently coercing failed into done with no trace.
cancelled— (none)No work counterpart. Same guidance as failed.

Rule for values with no counterpart. A runtime-backed workspace MUST NOT drop information crossing either direction: failed and cancelled MUST remain distinguishable from done somewhere queryable even when surfaced through a work-shaped view, and a work item's blocked or archived state MUST remain distinguishable even when surfaced through a ledger-shaped view. Silent collapse (mapping failed straight to done with nothing to tell them apart) is non-conforming.

Adopting blocked resolves a real ambiguity. The ledger's blockedBy[] is informational only — no scheduler reads it, and it does not gate the status machine. A task that is genuinely blocked on something and a task that is simply unclaimed both read as pending; the only way to tell them apart is to separately inspect blockedBy. work's first-class blocked status names the distinction the ledger currently leaves as a side channel. A runtime adopting the mapping above SHOULD promote blockedBy-non-empty pending items to a real blocked status in its work-shaped view rather than leaving the caller to reconstruct the distinction by hand.

The three axes

AIP-13 §"Scope semantics (normative)" holds containment (parent), applicability (scope), and ownership (assignee / lead) as three orthogonal axes and states that their null states MUST NOT be conflated — that conflation is exactly the bug AIP-13's Motivation names in existing trackers. The reference ledger has a single flat boardId (tree:<rootSessionId>, derived by walking session lineage to its depth-0 root; ws:<slug> for an operator caller; or a spawn-stamped meta.boardId override) that does double duty as both the containment grouping (which tasks belong to the same coordinating unit) and the visibility boundary (a session caller may reach any board within its own lineage but never a sibling lineage's). That is precisely the single-field conflation AIP-13 and AIP-20 (see "Scope axes" above) exist to avoid.

A runtime-backed workspace SHOULD decompose this into two separately addressable fields at its API boundary, even if the underlying store still keeps one physical value. This is a SHOULD, not a MUST: the only reference implementation does not do it today, and no second runtime has validated the split against a differently-shaped grouping model.

  • parent (containment). Identifies the unit of work the item belongs to. For a lineage-scoped runtime, the natural value is the same grouping the runtime already computes (e.g. tree:<rootId>).
  • scope (applicability). Governs visibility independently of containment. Where a runtime implements this split, its DEFAULT MUST inherit from the item's parent — an item contained in tree X is by default visible only within tree X — which reproduces the ledger's current access behavior exactly and satisfies AIP-13's inheritance rule. A runtime MAY allow scope to be set independently of parent (narrowed to a single operator within the tree, or widened to a whole company); the point of splitting the fields is that this becomes possible without redesigning the grouping primitive itself.

This is a RECOMMENDED decomposition for agentwork/v2, not a description of task-ledger.ts as it stands today — the reference ledger's boardId remains a single field. It is the minimal decomposition: default-inherit keeps existing callers' visibility unchanged while making applicability independently settable for a runtime that chooses to adopt it.

Ownership

Ownership is the axis with the cleanest match: the ledger's owner field lines up directly with AIP-20's owner axis field (see "Scope axes" above) — same name, same job. The one gap is vocabulary: AIP-13's assignee.kind is operator | user, but the ledger's owner may hold a bare session id — a runtime execution instance, not a durable AIP-9 operator identity or a human user.

Mapping a session id onto kind: operator would conflate two different lifetimes: an AIP-9 operator is a durable, named persona; a session is an ephemeral runtime instance, and a single operator may have many sessions (sequential or concurrent) claiming work over its lifetime. Collapsing them loses exactly the distinction the rest of this section exists to preserve.

Recommendation. A runtime-backed workspace SHOULD extend the ownership-kind vocabulary with a third value, session, whose ref is a runtime session id: kind: operator | user | session. Resolving a session-kind owner to a durable operator identity, if the host tracks one, is a host mapping concern and out of scope for this AIP. This extension is additive — a host that never surfaces runtime sessions through its work-shaped view has no need to add the third kind.

Verification (OPTIONAL extension)

Neither AIP-13 nor the base of this AIP specifies how a done transition is substantiated — a work item's status: done in the file-first model is exactly as trustworthy as whoever last edited the frontmatter. The reference ledger's two-tier done-integrity model has no counterpart in either spec and is genuine prior art worth naming as an OPTIONAL conformance extension for runtime-backed workspaces:

  • Tier 0 — self-report. No gate declared for the item; a done report is accepted immediately and MUST be stamped with how it was reached (verification: {kind: "self-report" | "human", ...}) — visibly, not silently, casual.
  • Tier 1 — gated. A gate is declared on the item up front. A done report against a gated item MUST NOT transition immediately; the runtime runs the declared gate (synchronously or in the background) and transitions to done only on a passing result, recording verification: {kind: "gate", policyId?}. A failing gate MUST leave the item in its prior non-terminal status and MUST record the failure (e.g. a lastVerifyError-equivalent field) rather than discarding it.

A runtime implementing this extension MUST:

  1. Record verification on every done item — never leave it absent — so a reader can distinguish an asserted done from a verified one.
  2. Refuse or defer a done write against a Tier-1 item unless it is backed by either a fresh gate run or an evidence shortcut naming an already-passed, independently-checkable gate result.
  3. Never launder a failed gate into done — a red result MUST surface to the caller, not silently retry into acceptance.

A runtime MAY skip this extension entirely and treat every done as Tier 0 — that is exactly the base work.workspace posture today, and remains fully conforming.

statusRollup alignment

This AIP's "Status rollup" section (above) already specifies rollup as DERIVED, never stored as authoritative, with when: clauses including any-child-blocked → blocked. A runtime's live-activity projection — a query-time view recomputed on every read — satisfies this requirement naturally: there is no on-disk rolledStatus field that can drift out of sync, because nothing persists it by default. A conforming runtime-backed workspace:

  • MUST compute rollup at query/projection time, matching this AIP's default (exposeViaField unset).
  • MUST NOT persist a rolled/aggregated status as an item's own authoritative status.
  • MAY materialize a rollup into a projection field for read performance, subject to the same staleness caveat this AIP attaches to exposeViaField.

Concurrency (REQUIRED for runtime-backed workspaces)

The file-first model's concurrency story is git: two writers racing on the same ITEM.md produce a merge conflict a human resolves, or a last-write-wins clobber if neither locks first. That story does not extend to a runtime store — there is no working tree, no diff, no merge tool standing between two concurrent callers.

A runtime-backed workspace MUST implement optimistic concurrency control on every write: each item MUST carry a monotonically increasing revision token (rev in the reference implementation), and every write (claim, status transition, field edit) MUST supply the revision it last read. A write whose supplied revision does not match the item's current revision MUST fail with a conflict response — never a silent overwrite — and that response MUST include the current record so the caller can rebase.

This is REQUIRED, not OPTIONAL, because the failure mode without it is silent: two callers racing to claim the same item without CAS can both believe they succeeded, and the resulting double-claim is invisible until the two owners' work collides. Filesystem-first workspaces get this guarantee for free from git; a runtime-backed workspace MUST implement it explicitly.

Open questions

  • Whether failed and cancelled should become normative additions to the base work status vocabulary (AIP-13 / AIP-20), or remain a runtime-local extension applied through this AIP's statuses[] merge mechanism, is undecided.
  • Whether the third ownership kind proposed above (session) belongs in the base spec or should stay a runtime-backed-workspace-only extension is undecided.
  • Whether the parent / scope decomposition should be RAISED from SHOULD to MUST once a second runtime implementation validates it against a differently-shaped grouping model is open — this section currently recommends rather than requires the split precisely because that validation doesn't exist yet.

Rationale

Why split workspace concerns from item-schema concerns. AIP-13 collapsed both layers into one AIP. The workspace AIP owned both "what kinds exist" and "what a single kind looks like", and the cost showed up the moment a fourth kind appeared. AIP-18 carved out the item-schema layer; AIP-20 keeps only the workspace layer. This is the same separation AIP-2 codifies as the registry-of-views pattern: one file, one identity, one importable unit. A workspace is its shape; a collection is a shape; an item is an instance. Three distinct layers, three distinct AIPs, one composition mechanic.

Why keep three-axes scope at the workspace level. The three orthogonal axes — containment, applicability, ownership — could in principle be pushed into per-collection schemas. The reason they stay at the workspace level is cross-collection consistency. A project containing tasks (containment), a task scoped to the eng role (applicability), and a task assigned to alice (ownership) are the same three concepts whether the parent kind is project, okr, or epic. Pushing the axes into per-collection schemas would force every collection to redeclare them, and would let two collections in the same workspace disagree about whether appliesTo accepts roles or operators. Keeping the axes at the workspace level enforces uniformity across the whole tracker — the AIP-13 distinctive insight, preserved.

Why allow inline + ref + aliased collection declarations. Three forms cover three real authoring postures. Inline is for small single-tenant trackers where the collection schema is private and co-located. File ref is for shared-on-disk collections where peer workspaces import the same COLLECTION.md (the cross-team publishing case). Registry import (ws://collections/...) is for third-party collections installed via agentproto install. Aliasing is the escape hatch for ref-name conflicts and workspace-local renaming. The host's three-step resolution order (inline → local file → registry) means authors can prototype inline, graduate to file ref when sharing emerges, and only reach for registry import when third-party collections enter the picture. No mode is mandatory; all three are first-class.

Why one-way switches on auditMutations + scope axis flags. A view can adapt almost everything in a workspace (lints, query hints, collection visibility), but three things must not be adaptable: audit-trail enablement, containment-axis enablement, and applicability value class. Each one breaks an invariant a deployed tracker relies on. Audit downgrade silently disables the audit log for one consumer's view — the auditor cannot trust any tracker that allows it. Containment disable orphans existing items whose parent fields point at consumers in this workspace. Applicability value class drift invalidates every item whose appliesTo was written against the parent's value class. Hard refusal on these three is what makes the registry of views trustworthy: a third party inspecting the chain can verify the invariants hold without reading every leaf. (This mirrors AIP-7's auditRequired one-way switch posture verbatim.)

Why this pattern mirrors AIP-10/AIP-7. The agentproto family converges on a shared mechanic: one <NAMESPACE>.md doctype, two modes (workspace root / view), extends: for composition, appliesTo: for binding, merge-by-id for arrays of objects, hard-refusal on bindings to non-existent consumers, soft-warning on malformed chains. AIP-10 codified it for wikis, AIP-7 for governance, AIP-13 for the v1 tracker. AIP-20 keeps the same convergent shape. A host implementing one Workspace AIP gets the others structurally for free — same loader, same merge algorithm, same effective-config exposure surface. The cost of inventing a per-AIP composition mechanic would have been shipping three subtly different loaders; the cost of converging is one paragraph of "see AIP-10 for the merge strategy" per AIP. The convergence is deliberate.

Why AIP-13 stays Draft, not Superseded, for now. AIP-13 ships working code (the v1 reference implementation) and live workspaces in the field. Moving it to Superseded the moment AIP-20 ships would strand those deployments. The deprecation path is conservative: AIP-13 stays Draft, AIP-20 ships with replaces: 13 set, the starter library makes the migration mechanically simple, and AIP-13 moves to Superseded only after AIP-20 reaches Final. Until then, both AIPs are loadable; hosts MAY support either. The deprecation banner on AIP-13 documents the transition for any author landing there from a search.

Why a starter library, not a normative type list. Shipping project/initiative/task as REQUIRED collections would re-introduce AIP-13's mistake (workspace concerns and item-schema concerns coupled at the AIP level). Shipping them as a STARTER LIBRARY keeps the migration concrete (existing v1 workspaces have a clear on-disk path) without making the type list normative. A team building a v2 workspace from scratch picks zero, one, two, or three of the starter collections, and adds whatever fourth kind their domain needs — without forking an AIP.

Reference Implementation

Reference implementation in progress. The spec leads the implementation; once the in-flight packages/work/v2 package lands, this AIP will absorb the working schema in full as part of moving Draft → Review.

The implementation will compose with AIP-18's packages/collection/core (also in flight) — WORK.md validation delegates per-item-kind validation to AIP-18; both packages share the same merge algorithm and resolution-chain exposure surface.

Backwards Compatibility

AIP-13 (agentwork/v1) is the predecessor. AIP-20 is set up to replace it via the replaces: 13 frontmatter; AIP-13 moves to Superseded once AIP-20 reaches Final. Until then, both AIPs are loadable.

Migration path. AIP-20 ships a starter library at ./resources/aip-20/draft/starters/agentwork-v1-compat/ containing three AIP-18 collections — project, initiative, task — whose fields, statuses, ownership, and deadline semantics mirror AIP-13's hardcoded doctypes. An existing v1 workspace can opt into v2 by:

  1. Adding a WORK.md at its tracker root (workspace-root mode).
  2. Pointing collections: at the three starter collections (file refs or inline).
  3. Setting scope.containment.enabled: true with allowedKinds: [project, initiative, task] to preserve the v1 parent semantics.
  4. Enabling statusRollup if the team relies on derived parent status.

Existing items load unchanged. The starter collections are examples, not normative — teams MAY extend them (AIP-18 extends:) to add team-specific fields (bug.severity, task.estimate) without touching the workspace manifest.

Richer migrations (introducing kinds AIP-13 did not anticipate, restructuring the parent hierarchy, adding new scope axes) are an operator concern, not a spec concern. AIP-20 deliberately avoids prescribing a richer migration path — the starter library is the floor, not the ceiling.

What breaks. Nothing on the item side: AIP-13 items remain valid under AIP-20 once the starter collections are wired in. Workspace-root manifests written against AIP-13's work.workspace/v1 schema do NOT load directly under AIP-20 — the discriminator (schema: work.workspace/v2), the collection declaration form (collections[] instead of itemKinds[]), and the scope-axis declaration shape are all different. Hosts that need to load both v1 and v2 manifests MUST switch on the schema: discriminator and route accordingly.

Deprecation window. AIP-13 stays Draft until AIP-20 reaches Final. Once AIP-20 is Final, AIP-13 moves to Superseded; hosts SHOULD continue supporting v1 for at least one minor-version cycle to give live workspaces time to migrate.

Security Considerations

Workspaces are write surface for coordination authority. Threats:

  • Schema poisoning — a malicious view relaxes lints (turns error into warn, drops required: true indirectly through a re-aliased collection). Mitigation: lints and the one-way switches compose under merge-by-id and merge-by-effective-name. The per-collection schema invariants (AIP-18 collection_field_type_drift, collection_field_removed, collection_status_removed) hold through the chain. Governance (AIP-7) policies SHOULD restrict which lints and severities a child may soften, and the host MUST expose the resolution chain so reviewers can audit the diff.

  • Audit downgrade — a view sets defaults.auditMutations: false to silence the audit log for one consumer's session. Mitigation: covered by the work_audit_downgrade HARD refusal. Once a parent in the chain enables audit, no descendant can disable it.

  • Scope axis drift — a view changes scope.applicability.valueClass from company to role, invalidating every existing item whose appliesTo was written against company refs. Mitigation: covered by the work_scope_value_class_drift HARD refusal. Once a parent declares a value class, no descendant may change it.

  • Containment axis disable — a view disables scope.containment to remove the parent-link enforcement for one consumer. Items whose parent fields no longer validate become orphans on next load. Mitigation: covered by the work_scope_disable HARD refusal.

  • Cross-AIP ref forgery — a view declares executor: ws://operators/<slug> for an operator that exists but is owned by another tenant. Mitigation: hosts MUST resolve cross-AIP refs inside the same tenant scope as the view file; cross-tenant bindings are rejected with work_xref_unresolvable. The same rule applies to governance, knowledge, agency, playbook, and defaults.workflow.

  • Collection alias collision — two collection refs resolve to the same effective name (e.g. one ref aliased to bug, another upstream-named bug). Mitigation: covered by the work_collection_alias_conflict HARD refusal at workspace load.

  • Workspace shadowing via view — a malicious view extends a benign workspace, replaces a collection ref with a malicious inline that softens the per-collection lints. Mitigation: the collection's own composition rules (AIP-18 collection_field_type_drift, etc.) prevent the most dangerous drifts at the collection layer. AIP-20's work_collection_alias_conflict and the merge-by-effective-name rule make the alias-substitution case auditable through the resolution chain.

  • Vendor metadata as policy bypass — a vendor extension under metadata.<vendor> carries a flag the host honours that softens one-way switches. Mitigation: hosts MUST treat vendor metadata as advisory. Nothing under metadata.* may change the meaning of any field defined in this AIP. The one-way switches are spec-level invariants; vendor namespaces cannot bypass them.

The threat model assumes the filesystem itself is trusted (or verified through AIP-1's hash/signature mechanisms). AIP-20 inherits that posture without restating it; per-collection threats (item-schema attacks, status-removal attacks) are documented in AIP-18 and not duplicated here.

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 →