Lesson 2 of 6
Assign roles, contracts and acceptance criteria
Give every task one owner, a least-privilege OpenCode subagent, a frozen contract, and acceptance criteria a stranger can check.
Ownership is a write-set boundary, not a label
One task, one owner, one write set. If two workers edit the same deliverable, you have two tasks or a missing interface task. Anything outside the write set is out of scope. If a worker thinks it needs another file, it stops and writes the request in its handoff. You either expand the contract or add a task. Silent extra edits are the failure this rule exists to prevent.
For the default due/tags/search plan, each feature worker owns src/features/<name>.ts and src/features/<name>.test.ts. src/types.ts and src/store.ts stay with the interface task. src/main.ts is not secretly owned by everyone: it is the documented merge point. Do not tell a worker “never edit src/main.ts” unless the interface task already extracted a registry file those workers import. This starter’s registration list lives in src/main.ts; Lesson 5 needs that real conflict.
Define each worker as a custom subagent with least privilege
The starter's complete opencode.json declares Agent-DUE, Agent-TAGS, and Agent-SEARCH with a 20-step cap. Each can edit only its own feature implementation and HANDOFF.md; its test and the shared registration list require approval. Shared types, storage, config, and another worker's files are denied. Read the file, then confirm the exact names:
opencode debug config
opencode agent list
These roles use mode: "all" because later lessons start them directly with --agent; that mode also permits use as a subagent. A role with mode: "subagent" is for delegation or @mention, not this direct-session recipe. The configuration contains no model id or key; it uses your existing supported provider connection from /connect. An eligible LintLabs key uses OpenRouter; other supported providers use their own authentication flow. Free lessons do not include API credits. Check costs and spending limits before starting multiple agents. These agent-run exercises require a working connection; reading the instructions alone is not evidence that agents ran.
Pattern order matters: "*": "deny" comes first, then narrow overrides. These are tool gates, not a sandbox. Read any approval request. Do not use --auto. The learner reviews and commits each worker's result.
Freeze the contract the worker may depend on
A contract is the frozen interface between tasks. Write it in the task brief before the worker starts. Name types and fields (Task.due?: string as ISO YYYY-MM-DD), exports (isOverdue(task, today): boolean), DOM hooks (data-feature="due"), and a registration rule for src/main.ts (add your Feature; do not delete another worker’s import). Include a do not touch list: other feature files, tests you do not own, dependency files, and CI config.
If a worker wants a different signature, that is a contract change. You decide, re-run the interface task, then continue. Workers do not invent a second Task shape. Keep the brief short enough to paste; dump the whole orchestration plan into every prompt and the contract disappears under noise.
Copy TASK.md.example to TASK.md in the due worktree. It contains a complete due-date brief and observable checks. For tags/search, make a separate adapted brief with that worker's name, files, contract, port, and checks. HANDOFF.md.example is the output template. These files are ordinary documents, not hidden OpenCode APIs.
Write acceptance criteria a stranger can fail
Each criterion is binary, observable, and checkable by someone who did not write the code. Use Given … when … then …, checked by …. Include one edge and one regression (add / complete / remove still work; npm test and npm run typecheck still pass).
Example for due dates: a task due yesterday, not done, shows an Overdue badge (browser); due today shows none; done + past due shows none (unit test); missing due renders nothing date-related; baseline CRUD still works. “The agent said it is done” is not a criterion. You will re-run these checks yourself in Lesson 4. Least privilege is only real if you try to break it: ask the due worker to edit src/features/tags.ts and record the deny.