ADR-008 — Execution Packet
Step: 3 of 5 (packet)
Date: 2026-05-06
Branch: feature/adr-008-mode-enforcement
Worktree: .worktrees/claude/adr-008-mode-enforcement
Audit: docs/audits/adr-008-mode-enforcement-audit.md
Contract: docs/contracts/adr-008-mode-enforcement-contract.md
1. Files this packet writes
| Order | File | Action | Approx LOC |
|---|---|---|---|
| 1 | docs/architecture/decisions/ADR-008-mode-enforcement.md |
new | ~280 lines markdown |
| 2 | docs/architecture/decisions/index.md |
edit (one bullet add) | +1 |
| 3 | docs/architecture/decisions/README.md |
edit (one row add) | +1 |
| 4 | docs/verification/adr-008-mode-enforcement-verification.md |
new | ~110 lines markdown |
The audit and contract files were written in Steps 1 and 2 and are already on disk.
2. ADR section outline
The ADR file ADR-008-mode-enforcement.md is structured as follows. Each section’s purpose is locked here so the writer cannot drift; the §Decision is also locked to Option A by audit Step 1.
2.1. Frontmatter
---
title: "ADR-008: Mode Enforcement — Capability-Gated Tool Dispatch"
description: ...
tags: [adr, decision, modes, gamma, capabilities, server-lifecycle]
type: adr
round: R84
status: proposed
parent: Architecture Decision Records
updated: 2026-05-06
nav_order: 80
---
2.2. Header block
# ADR-008: Mode Enforcement — Capability-Gated Tool Dispatch
**Status:** Proposed
**Date:** 2026-05-06
**Round:** R84
**Supersedes:** None
**Superseded by:** None
2.3. §Context (~400 words)
State Finding #1. Cite src/server.ts:229, src/modes.ts:174, docs/2-plugin/modes.md:31, docs/reference/mcp-tools-phase-0.md:930, ADR-004:68. Quote the docs that imply enforcement and the code that does not. Refer the reader to docs/audits/adr-008-mode-enforcement-audit.md for the full inventory.
2.4. §Decision (~300 words)
Option A — enforcement via tool config flag. State the choice in one sentence. Then give the mechanism in five bullets:
- New optional
mutates: booleanfield onColibriToolConfig. - Default
false(a tool that doesn’t declare itself non-mutating is treated as a no-op for capability gating, OR — alternative the ADR will pin — defaulttrueto fail safe). Decision: defaultfalsefor ergonomic non-breakage, but warn at registration if a tool declaredmutates: trueinREADONLYmode. - Stage 4 (dispatch) of the middleware reads
capabilitiesFor(ctx.mode)once at boot per tool; ifmutates && !canWriteDatabase, the wrapper short-circuits with a typed error envelope before the handler is called. - New error code
ERR_READONLY_MODEjoins the standard envelope. - Per-tool changes: 14 tool registrations gain a one-keyword declaration each.
2.5. §Alternatives Considered (~600 words)
Three subsections (A, B, C) with the per-axis tradeoff matrix:
| Axis | A — config flag | B — decorator/wrapper | C — amend docs |
|---|---|---|---|
| Implementation cost | ~50 LOC + per-tool 1-line | ~30 LOC + per-tool 1-line | ~50 LOC docs only |
| Runtime cost | one branch per dispatch | one wrapper per dispatch | zero |
| Test surface | mode × tool matrix | tool authors must add tests | doc-link audit |
| Schema impact | one optional field on ColibriToolConfig |
none | none |
| Tool author ergonomics | declaration is at the registration site | opt-in is easy to forget | no obligation, but no safety either |
| Failure mode if author forgets | tool defaults to false (read-only enforced safe) |
tool silently bypasses gating | n/a |
Subsection A: recommended. Reasons it wins.
Subsection B: rejected because the failure mode is silent. A forgotten opt-in is undetectable; a typo in a decorator name does not show up in any test.
Subsection C: rejected because it discards the safety story. READONLY becomes a label, not a guarantee. Auditors using COLIBRI_MODE=READONLY for compliance evidence lose the contract.
2.6. §Consequences (~250 words)
Positive. Closes Finding #1. READONLY is honest. New tool authors must explicitly declare mutation intent at the registration site (one keyword). server_health payload becomes useful for clients to skip mutating calls.
Negative. Adds one branch to every dispatch (negligible: a single boolean read against a frozen capability record). Adds a new error code to the canonical envelope. Adds a small test matrix.
Neutral. Does not affect Phase 1 κ Rule Engine — when κ admission ships, mode-gating becomes one input among many. ADR-008 is the floor, not the ceiling.
2.7. §Implementation (~400 words)
Concrete file deltas for the follow-up task:
src/server.ts—ColibriToolConfiggetsreadonly mutates?: boolean.registerColibriToolreadsctx.mode, computescaps = capabilitiesFor(ctx.mode)once, and bindsenforcedMutates = (toolConfig.mutates ?? false) && !caps.canWriteDatabase. The wrapped handler checksenforcedMutatesbefore stage 4 dispatch and returns theERR_READONLY_MODEenvelope if true. The existingvoid capabilitiesFor(mode)call increateServeris removed (no longer needed — the read moves to per-registration).src/server.ts(continued) — theserver_pingregistration inbootstrapand themutatesdeclarations for the 5 mutating tools.src/tools/health.ts—mutates: falseon theserver_healthregistration.src/tools/merkle.ts—mutates: trueonaudit_session_start,merkle_finalize;mutates: falseonmerkle_root.src/domains/tasks/repository.ts—mutates: trueontask_create,task_update;mutates: falseontask_get,task_list,task_next_actions.src/domains/skills/repository.ts—mutates: falseonskill_list.src/domains/trail/repository.ts—mutates: trueonthought_record;mutates: falseonthought_record_list.src/domains/trail/verifier.ts—mutates: falseonaudit_verify_chain.src/__tests__/middleware/mode-enforcement.test.ts— new test suite. One test per(mode, tool)admission pair (4 modes × 14 tools = 56 cases minus the read-only fast-path = 5 mutating tools rejected inREADONLYandMINIMAL= 10 explicit refuse-cases, plus 9 read-only allow-cases × 4 modes = 36 explicit allow-cases). The exact count is the implementer’s call; the floor is “every mutating tool refused inREADONLYandMINIMAL, every read-only tool admitted in every mode”.docs/2-plugin/modes.md— line 31 updated to use the termERR_READONLY_MODEand Stage 4 (dispatch) instead of Stage 1 (tool-lock). The ADR-008 §Implementation explicitly says “ADR-008 supersedes the modes.md line 31 prose about Stage 1; enforcement happens at Stage 4”.docs/reference/mcp-tools-phase-0.md—ERR_READONLY_MODEadded to the canonical error code list (line 988 area).
The follow-up task is one cohesive PR. Estimate: ~120 LOC code + ~250 LOC tests + ~30 LOC docs.
2.8. §Verification (~150 words)
Sigma confirms ADR-008 is implemented if and only if:
src/server.tshas themutatesfield onColibriToolConfigand Stage 4 readscapabilitiesFor.- All 5 mutating tools declare
mutates: true. - A test asserts
task_updatereturnsERR_READONLY_MODEwhenCOLIBRI_MODE=READONLY. npm run build && npm run lint && npm testis green.docs/2-plugin/modes.md:31and the error-code list inmcp-tools-phase-0.mdare updated to match the runtime.
2.9. §References
- Audit, contract, packet, verification of ADR-008
src/modes.ts,src/server.tsdocs/2-plugin/modes.md,docs/reference/mcp-tools-phase-0.md,docs/architecture/decisions/ADR-004-tool-surface.md- ADR-007 (independent sibling, η session lifecycle)
3. Index update mechanics
3.1. docs/architecture/decisions/index.md
Add one bullet between ADR-006 and the existing ## See also heading:
- [ADR-008 — Mode Enforcement: Capability-Gated Tool Dispatch](ADR-008-mode-enforcement.md)
ADR-007 is being drafted by a sibling worktree; do not add an ADR-007 bullet here. The merge will resolve naturally because both ADRs are line-additions in different positions.
3.2. docs/architecture/decisions/README.md
Add one row to the existing status table (currently has rows for ADR-001..003):
| [ADR-008](ADR-008-mode-enforcement.md) | Mode Enforcement — Capability-Gated Tool Dispatch | PROPOSED | γ Server Lifecycle |
Leave ADR-004, ADR-005, ADR-006, and ADR-007 rows alone — they are the responsibility of those ADRs’ worktrees. (Note: README.md’s table is currently incomplete; it stops at ADR-003. ADR-008 only adds itself; a separate hygiene pass will fill the gaps.)
4. Step 5 (verification) outline
The verification document records:
- Frontmatter validation pass
- Section-presence checklist (8 sections)
- Code citation accuracy check (each line cited in the ADR exists at the worktree base)
- Index update presence check
npm run build && npm run lint && npm testexit code 0 and stable test count- Commit SHAs for the three implementation commits (one per file group: ADR + index + verification)
The verification doc is written after the ADR and the gate runs successfully.
5. Gate sequence
1. Write ADR-008-mode-enforcement.md
2. Edit index.md (one bullet)
3. Edit README.md (one row)
4. npm run build (must be green)
5. npm run lint (must be green)
6. npm test (must be green; same count as base)
7. Write verification doc
8. Commit each step:
- audit
- contract
- packet
- feat (ADR + index + README)
- verify
9. Push branch (no PR; the dispatching agent handles PR creation)
6. Rollback plan
No source code is touched. Rollback is git reset --hard origin/main on the feature branch, which deletes all five docs files and both index edits. Safe.
7. Risks and mitigations
| Risk | Mitigation |
|---|---|
| Test gate fails despite no source changes | Fall back to npm test --runInBand to rule out parallel-test flake (the known startup — subprocess smoke flake is the most likely culprit per memory). Re-run once before declaring failure. |
| Word count drifts toward 4,000 in the ADR | Stick to the section budgets in §2; cut from §Alternatives first. |
| ADR-007 worktree commits its index update first and ours conflicts | Pull origin/main right before commit; resolve by accepting both line additions. The ADRs are independent. |
| Frontmatter date format diverges from peer ADRs | Use 2026-05-06 (ISO-8601, no quotes). ADR-005 uses 2026-04-09. ADR-006 uses 2026-04-09. Match style. |
| Reader thinks ADR-008 changes runtime today | §Verification opens with “Implementation lands in a follow-up task; this ADR is the architectural decision, not the patch.” |
8. References
- Audit:
docs/audits/adr-008-mode-enforcement-audit.md - Contract:
docs/contracts/adr-008-mode-enforcement-contract.md - ADR format:
docs/architecture/decisions/README.md - Code:
src/modes.ts:174,src/server.ts:229 - Docs:
docs/2-plugin/modes.md:31