Author a WORK.md (workspace root or view) for AIP-20
Use this skill when the user asks to draft, extend, or revise a
work.workspace/v2 manifest under AIP-20. The skill produces a
valid manifest (workspace-root or view), with the right collection declarations,
scope axes, status rollups, lint rules, and cross-AIP refs, ready for
defineWorkWorkspace to load.
A WORK.md manifest is the machine-readable contract for an
AIP-20 tracker — which collections are tracked, how the three
scope axes apply, when parent items roll up child status, which
workspace-spanning lints run. The same doctype is used in two modes: a
workspace root at the work tree root (no extends:), and a view in any
operator/company/skill folder (with extends: pointing at a parent). Authoring
either is the same flow, with one branch on step 1.
Critical: AIP-20 delegates ALL per-item-kind concerns (fields, status state
machines, ownership cardinality, deadline kinds, lint rules per kind) to
AIP-18. Do NOT re-specify any of those in WORK.md — declare
collections, then let AIP-18 own the schemas.
When to use
- "Set up a new tracker — write its
WORK.mdfrom scratch." - "Add a per-operator lens on the existing tracker — write a view that extends the workspace."
- "The Acme engagement needs an OKR collection — extend the org view."
- "Bind an AIP-7 governance policy and an AIP-10 wiki to this workspace."
- "Move three workspace-spanning lints from the org root to a per-team view that needs them stricter."
When NOT to use
- The user wants to author per-item-kind schemas (fields, statuses,
ownership rules) — that's AIP-18's
author-collectionskill. - The user wants to write individual items (
ITEM.mdrecords) — also AIP-18. - The user wants to change the AIP-20 spec itself — manifest shape changes are governance, not authoring.
- The user wants to edit an existing
WORK.mdin place without considering the chain — read the parent first, run the merge in your head, then edit. Skipping the merge produces views that override fields the parent already provides correctly, or worse, trip a one-way-switch HARD refusal.
Process
Follow these steps in order. Composition and one-way switches are the central mechanics; steps 1-2 set up the right mode, steps 4-9 fill in the body, steps 11-12 validate.
1. Decide: workspace root or view?
Two questions:
- Is there an existing
WORK.mdupstream that this manifest should adapt? If yes → view; if no → workspace root. - Does the user want the manifest to bind to a specific consumer (operator /
company / skill)? If yes → view (set
appliesTo); if no → workspace root.
Workspace-root mode declares the BASE shape. View mode adapts the base for one
or more consumers. There is no third mode — the schema rejects manifests that
mix workspace-root and view properties (e.g. appliesTo without extends).
If the answer is workspace root, skip step 2 and proceed to step 3.
2. If view: locate parent, set extends:, understand one-way switches
For a view, the extends: field is a RELATIVE path from the new manifest's
directory to the parent WORK.md. The host resolves it bottom-up; recursion is
allowed.
# Operator view at /repo/operators/eng-lead/WORK.md
extends: ../../engineering/WORK.md
Rules:
- Use POSIX path separators in
extends:even on Windows. - Maximum chain depth is eight. Two-to-three levels is the common case; deeper chains usually mean a refactor is overdue.
- If the parent is in another tracker tree, prefer factoring the shared bits
into a small workspace package both can
extends:locally.
One-way switches — read the parent FIRST. Three fields, once set at any ancestor, MUST NOT be relaxed by descendants. Trying to relax triggers a HARD refusal — the view fails to load. Before authoring a view, read the parent (and its parent, if any) and identify which one-way switches are already on:
| Field | One-way condition | HARD refusal code |
|---|---|---|
defaults.auditMutations |
If any ancestor is true, descendants cannot set false. |
work_audit_downgrade |
scope.containment.enabled |
If any ancestor is true, descendants cannot set false. |
work_scope_disable |
scope.applicability.valueClass |
If any ancestor sets a value, descendants cannot change it. | work_scope_value_class_drift |
If the parent has any of these set, do NOT redeclare them on the view — inherit silently. If you absolutely need a different value, the conversation belongs at the parent's level (or in AIP-7 governance), not in this view.
Cycle detection and depth-overflow are runtime warnings, not errors. Do not rely on the warning — write a correct chain.
3. Identity (name, title, description, version)
Every manifest, root or view, declares its identity. These fields are NOT inherited (each manifest has its own).
schema: work.workspace/v2
name: eng-lead-view # kebab-case, stable
title: Engineering lead view # human-readable
description: |
The eng lead's lens on the shared engineering tracker. Surfaces
projects + initiatives, hides per-task noise, adds a stricter
stale-tree lint.
version: 1.0.0 # semver of the SHAPE, not content
Bump version whenever you change collections, scope.*, statusRollup.*,
lints, or defaults.*. Patch bumps for cosmetic edits to description,
display.*, or metadata.
4. Collections — inline vs ref vs aliased
collections: is the bridge to AIP-18. Three forms:
- Inline. Full AIP-18 collection.schema/v1 frontmatter embedded in
WORK.md. Useful for small, single-tenant trackers. - File ref (
./collections/<name>/COLLECTION.md). Useful when the collection is shared with peer workspaces. - Registry import (
ws://collections/<slug>). Useful for third-party or org-shared collections.
Aliasing (any ref form):
collections:
- ref: ws://collections/issue
alias: bug # workspace-local rename
version: "1.x" # pin schema range
Resolution order (highest priority wins):
- Inline (declared on this
WORK.md). - File ref (resolved relative to this manifest's directory).
- Registry import (
ws://collections/<slug>).
Two collection entries resolving to the same effective name (alias or upstream
name) is a HARD failure (work_collection_alias_conflict). Pick aliases
deliberately.
The merged collections[] array is computed across the extends: chain via
merge-by-effective-name. Inheriting from the parent is the default; only
redeclare collections you want to override.
When extending an AIP-18 starter collection (e.g.
agentwork-v1-compat/task) with team-specific fields, write the extended
collection inline OR as a sibling file with its own extends: — and then ref
the extended file from WORK.md. Do NOT mutate the starter file in place.
5. Cross-AIP bindings
executor: ws://operators/eng-triage
governance: ../policies/engineering.yaml
knowledge: ws://wikis/engineering/KNOWLEDGE.md
agency: ws://agencies/internal-eng
playbook: ws://playbooks/eng-quarterly
| Field | Required | When to set |
|---|---|---|
executor |
optional | Default executor for items without an explicit assignee. |
governance |
optional | Set when AIP-7 approval gates apply. Workspace-root manifests usually set this; views may override only if the parent's policy permits. |
knowledge |
optional | Set when items reference an AIP-10 wiki by default. |
agency |
optional | Set when work is billable and tracked under an AIP-8 agency. |
playbook |
optional | Set when an AIP-12 playbook governs routine plays for this workspace. |
appliesTo |
conditional | REQUIRED in view mode (whenever extends is set AND the view binds to a consumer). MUST NOT be set in workspace-root mode. |
The host MUST refuse a view whose appliesTo references a non-existent consumer
(work_appliesto_unresolvable) — verify the consumer's workspace exists before
declaring the binding.
The host also refuses workspaces with unresolvable executor, governance,
knowledge, agency, or playbook refs (work_xref_unresolvable, HARD). Do
not bind to a workspace that hasn't been created yet.
6. Scope axes (containment / applicability / ownership)
The three orthogonal axes:
scope:
containment:
enabled: true
field: parent
rules:
allowedKinds: [project, initiative]
maxDepth: 4
applicability:
enabled: true
field: appliesTo
valueClass: role-and-company
ownership:
enabled: true
field: owner
policy: inherit
Guidance:
- Containment controls parent/child. Use
allowedKindsto prevent miscoded relationships (ataskhaving abugparent when onlyprojectandinitiativeare intended). UsemaxDepthto cap recursion. - Applicability controls visibility. Pick
valueClassbased on what the team actually puts inappliesTo:roleif items are about roles,companyif about companies,role-and-companyfor multi-tenant deployments. Once set, descendants CANNOT change it. - Ownership is mostly delegated to per-collection AIP-18
ownership rules. The workspace-level
policy(strict / inherit / open) is the cross-collection knob.
Recall the one-way switches — scope.containment.enabled: true and
scope.applicability.valueClass cannot be relaxed by descendants.
7. Status rollup
Per-collection statuses live on AIP-18; status rollup is the workspace-level aggregation:
statusRollup:
enabled: true
policy:
- when: all-children-terminal
bubbleParentStatus: done
- when: any-child-blocked
bubbleParentStatus: blocked
- when: any-child-overdue
bubbleParentStatus: at-risk
exposeViaField: rolledStatus
Rules:
bubbleParentStatus:MUST be a status id that exists on EVERY parent collection (perscope.containment.rules.allowedKinds). The host warns and degrades to no-op for non-conforming parents.- First-match wins: clauses are evaluated in declaration order, the first true clause's status is bubbled.
exposeViaField:is materialization. Set it when consumers need the rolled status to appear in items on disk; leave it unset for query-time evaluation.
8. Workspace-spanning lints
AIP-18 lints are per-collection (missing-owner, overdue, required-field).
AIP-20 lints span collections:
lints:
- id: orphan-task
kind: orphan-across-collections
severity: error
params:
collections: [task, bug]
- id: stale-tree-30d
kind: stale-tree
severity: warn
params:
days: 30
- id: broken-parent
kind: broken-parent-ref
severity: error
- id: scope-mismatch-role
kind: scope-mismatch
severity: warn
params:
axis: applicability
Workspace-spanning lint kinds:
| Kind | Purpose | params |
|---|---|---|
orphan-across-collections |
Item has no inbound parent ref AND no outbound containment ref. | collections: [...] |
stale-tree |
Items in a containment tree where no descendant has been updated within days. |
days: <n>, collections: [...] |
broken-parent-ref |
Item's parent ref doesn't resolve. |
none |
scope-mismatch |
Item's applicability conflicts with parent's. | axis: applicability | ownership |
custom |
Host-defined, identified by id. |
host-defined |
Severity guidance — same as AIP-10:
error— block writes that fail the lint.warn— surface in the audit log, do not block.info— surface in tooling only.
Child views may soften severity (warn → info). A parent's governance: policy
MAY forbid softening — the host enforces.
9. Routine workflow defaults
defaults:
workflow: ./workflows/nightly-sweep/WORKFLOW.md
approvalClass: on-mutate
auditMutations: true
| Field | Values |
|---|---|
workflow |
path or ws:// ref to an AIP-15 WORKFLOW.md |
approvalClass |
auto / always / on-mutate / policy:<ref> |
auditMutations |
boolean — ONE-WAY SWITCH |
auditMutations: true is one of the three one-way switches. Enable it
deliberately: once on at any ancestor, no descendant can disable it without
triggering work_audit_downgrade (HARD).
10. Display / UX hints
display:
homePage: PROJ-eng-q2
defaultGrouping: parent # kind | status | owner | parent
defaultView: tree # list | board | tree | timeline
Pure UI hints; no validation impact. Pick what makes the workspace landable on first open.
11. Body prose (purpose, conventions, what NOT to track)
The frontmatter ends; the body is markdown. Conventional sections:
# <title>
## Purpose
What this workspace tracks, who uses it.
## Conventions
When an item belongs in collection A vs 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.
Keep the body short. The frontmatter is the contract; the body explains the choices.
12. Validate against WORK.schema.json; if view, dry-run merge
Validate the new manifest's frontmatter against AIP-20's schema:
npx ajv validate \
-s apps/agentik/sites/content/docs/agentproto/resources/aip-20/draft/WORK.schema.json \
-d "<workspaceDir>/WORK.md"
Common errors:
appliesToset butextendsmissing → addextends.collections[].aliasproduces a name collision → rename or remove the alias.statusRollup.policy[].bubbleParentStatusreferences a status id not declared on the parent collection → either add the status to the collection or change the bubble target.lints[].idcollisions inside one manifest → ids must be unique per manifest; merge happens across manifests, not within one.versionnot semver →1.0.0, not1orv1.
If view, run the host's resolution algorithm in dry-run mode and explicitly check that no one-way switch is relaxed:
## Merge diff: eng-lead-view (vs parent engineering)
Inherited (no change):
- collections: project, initiative, task, bug
- scope.containment: enabled=true, allowedKinds=[project, initiative],
maxDepth=3
- scope.applicability: enabled=true, field=appliesTo,
valueClass=role-and-company
- defaults.auditMutations: true (one-way; descendants cannot disable)
- governance: ../policies/engineering.yaml
Overridden:
- display.homePage: PROJ-onboarding → PROJ-eng-q2
- display.defaultView: board → tree
Added:
- lints.lead-stale-projects (kind=stale-tree, severity=warn)
- appliesTo: [ws://operators/eng-lead]
One-way switch check: PASS
- defaults.auditMutations: parent=true, view=undefined → inherits true OK
- scope.containment.enabled: parent=true, view=undefined → inherits true OK
- scope.applicability.valueClass: parent=role-and-company, view=undefined →
inherits OK
Resolution chain: 2 levels (engineering → eng-lead-view) Warnings: none
If the merge diff shows the view RELAXING any one-way switch (e.g.
auditMutations: true → false, containment.enabled: true → false,
valueClass: company → role), the view will be HARD-refused at load — fix it
before declaring success.
If the diff includes an unintentional override, edit the view to remove it (deletion of a field reverts to parent's value via the merge).
Final checklist
Before declaring done:
-
schema: work.workspace/v2is set. -
name,title,description,versionare present. - If view:
extends:is a valid relative path to an existingWORK.md;appliesTo:references existing consumers. - If workspace root:
extends:andappliesTo:are absent. -
collections[]entries have unique effective names (alias orname); refs resolve; inline frontmatters validate against AIP-18's COLLECTION.schema.json. - Per-item-kind concerns (fields, statuses, ownership rules) are NOT in
WORK.md— they live onCOLLECTION.mdfiles. -
scope.containment.rules.allowedKindsreferences real collection names. -
scope.applicability.valueClassis set deliberately; it cannot change in descendants. -
statusRollup.policy[].bubbleParentStatusreferences status ids declared on every eligible parent collection. -
lints[]have uniqueids within this manifest; severities respect any parent governance constraints. -
defaults.auditMutationsis set deliberately (one-way). - Cross-AIP refs (
executor,governance,knowledge,agency,playbook) all resolve. -
metadata.<vendor>.*is namespaced. - Frontmatter validates against
WORK.schema.json. - Body is short and prose-only.
- If view: dry-run merge diff was reviewed; no one-way switch is relaxed.
- If governance binding changed: the change is itself routed through AIP-7 approval before the manifest lands on disk.
Output
Reply to the user with:
- The path of the manifest written.
- Mode (workspace root vs view).
- Resolution chain (for a view): root → … → leaf, one path per level.
- Effective config summary — the merged shape, in particular which collections are active, which scope axes are enabled, and which one-way switches are now in effect.
- Bindings —
executor,governance,knowledge,agency,playbook,appliesTo(if set), each with a one-line note on what it does. - One-way switch report — for a view, an explicit per-switch line:
auditMutations: parent=<x>, view=<y>, status=PASS|FAIL. - Validation result — schema clean, dry-run merge clean, warnings (if any).
- Open assumptions — fields you guessed (rollup policy, stale-tree thresholds, lint severities) that the user might want to override.
Do NOT mutate the parent manifest, the workspace root, or any existing view as a side-effect. Authoring a new manifest is a LEAF operation — touch only the file you are creating.
See also
- AIP-20 — agentwork/v2 spec
- AIP-18 — COLLECTION.md / ITEM.md — substrate this skill composes on
- AIP-3 — SKILL.md
- AIP-6 — agentcompanies/v1
- AIP-7 — governance, approval, audit
- AIP-8 — agentagencies/v1
- AIP-9 — agentoperators/v1
- AIP-10 — agentknowledge/v1 — sibling Workspace AIP
- AIP-12 — agentplaybooks/v1
- AIP-15 — WORKFLOW.md
../../ADAPTER.md— implementer's guide../../EXAMPLES.md— reference manifests../../WORK.schema.json— frontmatter validator../../starters/agentwork-v1-compat/— starter collection library