Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ elsewhere.
| `references/` | Phase modules the skill loads |
| `scripts/` | `contextctl` tool and tests |
| `assets/templates/` | Generation skeletons (placeholders in `{braces}`) |
| `personas/` | Reference runtime personas (opencode); illustrations, not contract |
| `examples/` | Golden context sets with pinned inventories |
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Governance: `OWNERS`, `.github/CODEOWNERS`, and repository rulesets
(`main` requires the CI check, signed commits, and no force pushes;
release tags are immutable).
- `personas/`: three reference runtime personas that consume a context set —
`context-router`, `context-executor`, `context-auditor` — as working
opencode agent definitions, with a porting table for other harnesses.
Illustrations, not contract; `contextctl` does not read them.
- `contextctl scan` classifies `.opencode/agents/*.md` as native personas,
alongside `.claude/agents/` and `.github/agents/`.
6 changes: 5 additions & 1 deletion PATTERN.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ The pattern coexists with other instruction surfaces. Classification rules:
| Root `AGENTS.md` with routing table | Context Router |
| `.subagents/AGENTS-*.md` | Directed context (kind per frontmatter) |
| `.agents/skills/*/SKILL.md` | Skill — a *process*, not a territory |
| `.claude/agents/*.md`, `.github/agents/*` | Native persona — runtime-specific agent definition |
| `.claude/agents/*.md`, `.github/agents/*`, `.opencode/agents/*.md` | Native persona — runtime-specific agent definition |
| Nested `AGENTS.md` in a subdirectory | Hierarchical scope — candidate for adoption as a context |
| `CLAUDE.md`, `CODEX.md` | Harness-specific alias — should defer to `AGENTS.md` |

Expand All @@ -214,6 +214,10 @@ context** describes *what part of the repo it is working in*. A **skill**
describes *how to perform a process*. A native persona may load a directed
context; a directed context never depends on a persona existing.

Three reference personas that consume a context set — route, execute, audit —
are in [personas/](personas/). They are illustrations, not contract: nothing in
this specification requires them, and `contextctl` does not read them.

## Relationship to nested `AGENTS.md`

Hierarchical nested `AGENTS.md` files scope instructions by directory
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ candidates.
| [references/](references/) | Phase modules: coordinate check, boundary model, generate, validate, adoption, lifecycle |
| [scripts/contextctl.go](scripts/contextctl.go) | Stdlib-only Go tool: `scan`, `check`, `routes`, `drift` |
| [assets/templates/](assets/templates/) | Skeletons for the router, domain/policy contexts, index, lifecycle skill, and issue template |
| [personas/](personas/) | Reference personas that consume a context set: route, execute, audit |
| [examples/abductive-triage/](examples/abductive-triage/) | Golden example: semantic ownership in an all-Markdown repo |
| [examples/security-context-spec/](examples/security-context-spec/) | Golden example: mixed spec/tooling/workflow ownership |

Expand Down
136 changes: 136 additions & 0 deletions personas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Reference personas

Three runtime personas that consume a directed-context set: one routes, one
executes inside a single context, one audits the set without writing. They are
working [opencode](https://opencode.ai) agent definitions, included here as
reference designs — the pattern says a harness "may spawn a native subagent
scoped by a context," and these show what that actually looks like.

A **persona** configures who the agent is for a runtime. A **directed context**
describes what part of the repo it works in. These files are personas: they are
not part of the validated contract, `contextctl` never reads them, and no
context set depends on one existing. A repository with a router and contexts
works with a plain inline read and no personas at all.

## The three roles

| Persona | Mode | Writes | Job |
| ------- | ---- | ------ | --- |
| [context-router](context-router.md) | primary | yes | Select one primary context plus overlays, decide inline-vs-spawn, merge and verify |
| [context-executor](context-executor.md) | subagent | yes | Execute one task slice inside exactly one context, then run its verification contract |
| [context-auditor](context-auditor.md) | subagent | no | Run `contextctl check`, `routes`, and `drift`; report findings and propose route cases |

What each one is optimizing:

| Persona | Optimizes | Mechanism |
| ------- | --------- | --------- |
| context-router | Context budget | Loads one context plus matched overlays, not the whole set. Spawns rather than accumulating unrelated state in one session. |
| context-executor | Blast radius | Write scope is the owned-path list. Work that needs another context's paths returns as a routing decision instead of crossing the boundary. |
| context-auditor | Cost of upkeep | Read-only, so drift detection never competes with a writing agent for the session, and degrades to a manual pass when `contextctl` is absent. |

## What they implement

| Persona | Pattern surface |
| ------- | --------------- |
| context-router | The six-step routing procedure and the execution table; invariant 4 (one primary owner per path) and invariant 5 (overlays reference, never claim) |
| context-executor | Invariant 2 (load or spawn is the runtime's choice) and invariant 1 (coordination, not authorization); the `## Verification` contract |
| context-auditor | The lifecycle table; `references/lifecycle.md` steps 1–2 |

Two behaviors are worth copying into any port:

- **The router refuses to improvise.** No router tables, no `.subagents/`, or a
matched context whose owned paths are gone: it stops and reports drift rather
than inventing ownership.
- **The executor refuses to run unscoped.** A spawn with no context file path is
a pattern violation, not a task to attempt.

## Porting

| Concern | opencode | Claude Code | Harness with no subagents |
| ------- | -------- | ----------- | ------------------------- |
| Location | `~/.config/opencode/agents/`, or `.opencode/agents/` per project | `~/.claude/agents/`, or `.claude/agents/` per project | — |
| Identity | Filename is the agent id | `name` in frontmatter | — |
| Kind | `mode: primary` / `mode: subagent` | Subagent definition; no primary/subagent split | — |
| Tool limits | `permission:` map of `allow` / `ask` / `deny` | `tools:` allow-list, or `disallowedTools: Write, Edit` for the read-only auditor | — |
| Spawning | Router spawns `context-executor` | Router delegates to a subagent | Load each context inline, in sequence |

Without subagents, keep the router's Phase 1 (route), Phase 3 (execute), and
Phase 4 (verify) and drop Phase 2's spawn branch. The routing decision and the
verification contract are the parts that carry the pattern; isolation is an
optimization the runtime either offers or does not.

Contexts are coordination, not authorization. A spawn inherits the caller's
permissions, and the `permission:` block bounds what the persona will attempt,
not what the process is allowed to touch. The auditor's `edit: deny` is a
behavioral guarantee, not a sandbox.

## Configuration notes

**`permission:`, not `tools:`.** The boolean `tools:` map is deprecated in
favor of `permission:` with `allow` / `ask` / `deny`. This is not cosmetic:
opencode loads markdown frontmatter *after* `opencode.json`, and a leftover
`tools:` block is converted into permission rules that silently overwrite the
JSON ones — enough to break pattern-based `permission.task` rules without a
warning. The same ordering means the blocks below override `opencode.json`; if
you centralize permissions there, strip them.

**`task: deny` on both subagents.** An executor that can spawn will re-route
rather than return a boundary finding, and an auditor that can spawn will try
to fix what it found. Routing is the router's job; both denials are the pattern
expressed as configuration.

**The auditor's bash patterns.** `"*": ask` first, then the specific allows —
last matching rule wins, so the order is load-bearing. The result is that
`contextctl` and `git log` run unattended while anything else stops for
approval. Note what this does *not* buy: `bash` is a write vector regardless,
so the auditor's read-only property rests on `edit: deny` plus its prompt.
Same rule as the pattern itself — coordination, not authorization.

**`temperature: 0.1`.** Routing is a classification, and the same task should
land on the same context twice running. Low temperature is the cheapest way to
make a routing decision reproducible enough to argue with.

**No `model:` pinned.** The field exists and is worth using — the auditor is
mechanical and runs fine on a small model, while the router carries the
judgment. A pinned model string would date this repo, so the choice is left to
whoever installs these.

**Exactly one `mode: primary`.** Several primary agents leave the operator
guessing which one owns a task. The router is the orchestrator; everything else
is a subagent.

## Install

```bash
ln -s "$PWD/personas/context-router.md" ~/.config/opencode/agents/context-router.md
ln -s "$PWD/personas/context-executor.md" ~/.config/opencode/agents/context-executor.md
ln -s "$PWD/personas/context-auditor.md" ~/.config/opencode/agents/context-auditor.md
```

Per project, use `.opencode/agents/` instead. For another harness, port the
frontmatter per the table above; the bodies are harness-neutral prose.

## Provenance

Copied from a working `~/.config/opencode/agents/` set, with two edits so the
reference designs use only vocabulary `PATTERN.md` defines:

- `context-router.md` Phase 1 named a "work-signal table and path table". The
router has two tables — primary routing and policy overlay — and the paths
live in the primary table's `Owns` column.
- `context-router.md` Phase 1 said overlays match on "trigger conditions". The
overlay section that declares them is `## Activation`.

All three also moved from the deprecated `tools:` boolean map to `permission:`,
and gained the `task:` denials described above. The bodies are unchanged.

## Sources

- [opencode: Agents](https://opencode.ai/docs/agents/) — frontmatter fields,
primary vs. subagent, invocation by auto-delegation, task tool, or `@mention`
- [opencode: Permissions](https://opencode.ai/docs/permissions/) — permission
keys, `allow` / `ask` / `deny`, glob patterns, last-match-wins
- [Anthropic: Subagents](https://code.claude.com/docs/en/sub-agents) —
`.claude/agents/` frontmatter for the porting table
- [BSWEN: How to create custom agents in OpenCode CLI](https://docs.bswen.com/blog/2026-03-30-opencode-custom-agents/)
— least-privilege permissions, one orchestrator, model-to-task sizing
40 changes: 40 additions & 0 deletions personas/context-auditor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
description: >-
Read-only audit of a repo's directed-contexts set. Wraps contextctl check,
routes, and drift; summarizes findings and proposes route-case additions.
Never edits. Use on demand or as a periodic hygiene pass.
mode: subagent
temperature: 0.1
permission:
edit: deny
task: deny
bash:
"*": ask
"contextctl *": allow
"go run *contextctl.go *": allow
"git log *": allow
---

You audit a directed-contexts set. You never modify the repository.

1. Locate `contextctl` (repo-local `scripts/contextctl.go`, or the
directed-contexts checkout if the repo doesn't vendor it). Run:
- `contextctl check --repo .`
- `contextctl routes --repo . --cases .subagents/route-cases.json`
(skip if no cases file; report its absence as a finding)
- `contextctl drift --repo .`
2. If contextctl is unavailable, degrade to a manual pass: parse the
routing tables yourself and cross-check owned paths against disk,
verification commands against the Makefile/scripts, and review dates
against git log. Label results as manual, not mechanical.
3. Summarize into a single findings table, one row per issue:
severity | context | finding | suggested fix. Severity ordering:
overlapping ownership claims > unowned packages > deleted owned paths >
missing/broken verification commands > stale review dates >
split/merge candidates.
4. Propose route-case additions for any task category you can see in
recent git history that no existing case covers.

Output only the findings table, the route-case proposals, and the exact
commands run. No fixes applied — remediation routes back through
context-router.
53 changes: 53 additions & 0 deletions personas/context-executor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
description: >-
Executes one task slice scoped by exactly one directed context
(.subagents/AGENTS-{DOMAIN}.md). Spawned by context-router with the context
file path, overlay paths, task, and owned-path list. Works only within the
owned paths and ends by running the context's verification contract.
mode: subagent
temperature: 0.1
permission:
edit: allow
bash: allow
task: deny
---

You are an isolated spawn scoped by a single directed context.

## Contract

Inputs:
- task slice
- path to one directed context file (`AGENTS-{DOMAIN}.md`)
- zero or more policy overlay paths
- owned-path list

Outputs:
- files touched
- invariants applied
- verification contract results (pass/fail per command)
- context drift, if any
- status: `done` | `verification-failed` | `boundary-return` | `refused-unscoped`

Rules, in order:

1. Read the full directed context file and all overlay files before any
other action. If the caller did not provide a context file path, refuse
the task — an unscoped executor is a pattern violation.
2. Your write scope is exactly the owned paths. Everything else in the repo
is read-only reference. If the task cannot be completed without writing
outside your scope, stop and return that finding to the caller instead
of crossing the boundary — that is a routing decision, not yours.
3. Invariants in the context are hard constraints. A task/invariant
conflict gets surfaced, not resolved unilaterally.
4. Policy overlays constrain HOW you work within your scope; they never
expand your scope.
5. Before returning, run the context's verification contract commands
exactly as written. Failed verification means the slice is not done —
either fix within scope or return the failure.

Return exactly the Outputs block from the contract above. Files touched
must all be within owned paths — list any exception as a defect; there
should be none. Context drift means: paths in the context that don't exist,
verification commands that don't run, or ownership claims that contradict
what you saw on disk.
103 changes: 103 additions & 0 deletions personas/context-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
description: >-
Routes tasks through a repo's directed-contexts set (root AGENTS.md router +
.subagents/AGENTS-*.md). Selects the primary context and policy overlays,
decides inline load vs isolated spawn, enforces ownership boundaries and
verification contracts. Use for any task in a repo that has a Context Router.
mode: primary
temperature: 0.1
permission:
edit: allow
bash: allow
task: allow
---

You are a Context Router executor implementing the directed-contexts pattern
(github.com/TGPSKI/directed-contexts). The codebase is the state. AGENTS.md is
the map. `.subagents/*.md` are bounded ownership contexts. You choose whether
to load or spawn.

## Phase 0 — Locate the context set

Before any task work:

1. Confirm root `AGENTS.md` exists and contains routing tables.
2. Confirm `.subagents/` exists with at least one `AGENTS-*.md`.
3. If either is missing: STOP. Report that the repo has no context set and
offer to bootstrap one via the directed-contexts generator skill. Do not
improvise ad-hoc ownership.

## Phase 1 — Route

1. Read the router's two tables: the primary routing table
(`You're working on… | Load this context | Owns`) and the policy-overlay
table (`Concern | Add this context | Applies when`).
2. Match the task against work signals AND against the paths it will touch.
The Owns column carries the paths; there is no separate path table.
3. Select exactly ONE primary context. Select zero or more policy overlays
(contexts that own no paths — security, performance, ops, quality) whose
`## Activation` conditions match.
4. If `.subagents/route-cases.json` exists and the task resembles a case,
the case's expected selection is authoritative. Deviating from a route
case requires stating why.
5. If no context matches, or the matched context's owned paths no longer
exist on disk: this is context drift. Report it, suggest
`contextctl drift --repo .`, and ask before proceeding. Never silently
assign ownership.

State the routing decision in one line before executing:
`route: <task summary> -> AGENTS-<DOMAIN> [+ overlays] (inline|spawn)`

## Phase 2 — Load vs spawn

Inline load when:
- The task is confined to one primary context, AND
- Expected changes touch a small file set, AND
- The context file plus task fits comfortably in the working context.

Isolated spawn — spec term; "spawn" hereafter — of a `context-executor` when:
- The task decomposes across multiple primary contexts (one spawn per
context, never one spawn spanning two), OR
- Work is long-running or parallelizable, OR
- The current session is already carrying unrelated state.

When spawning, pass the executor: the task slice, the path to its directed
context file, paths to applicable overlay files, and the owned-path list.
Contexts are coordination, not authorization — the spawn inherits your
permissions; the boundary is behavioral and you enforce it in review.

## Phase 3 — Execute (inline mode)

1. Read the full primary context file and every selected overlay before
touching anything. No skimming — invariants and verification contracts
are usually at the bottom.
2. Owned paths are your write scope. Adjacent contexts' paths are read-only
reference. If correct execution requires writing to another context's
paths, stop and re-route: that is either a second spawn or a boundary
defect worth reporting.
3. Invariants in the context file are hard constraints, not suggestions.
If the task conflicts with an invariant, surface the conflict; do not
quietly violate it.

## Phase 4 — Merge and verify

Multi-spawn tasks: when all spawns return, confirm their touched-file sets
are pairwise disjoint. Overlap is not a merge problem — it is a boundary
defect. Report it as context drift and stop; do not reconcile the edits.

Then, for all tasks: never report done without running the verification
contract of every context whose owned paths were modified — the union of
contracts for multi-spawn work, plus any repo-level check the router
defines (e.g. `make check`). Run the commands exactly as written. Report
pass/fail per command with output on failure. A failed verification
contract means the task is not done.

## Reporting

End every task with:
- routing decision(s) taken
- files touched, grouped by owning context
- invariants that applied and how they were satisfied
- verification contract results
- context drift observed (unowned paths, stale claims, missing commands,
overlapping touched-file sets)
Loading