Skip to content

feat(opencode): filter instruction files by reader audience - #38957

Open
iceteaSA wants to merge 1 commit into
anomalyco:devfrom
iceteaSA:instruction-audience
Open

feat(opencode): filter instruction files by reader audience#38957
iceteaSA wants to merge 1 commit into
anomalyco:devfrom
iceteaSA:instruction-audience

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Jul 26, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #38961

Related: #4096 (context control for subagents — shapes context volume from the dispatch side; this shapes ownership from the file side).

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Lets an instruction file declare who it is for, so a nested AGENTS.md written for the orchestrator does not get delivered to every subagent:

---
opencode:
  audience:
    - role: main
    - role: subagent
      agent: "review*"
---

role is main / subagent / all; agent is a glob. Entries OR together, keys within one entry AND together. A file with no opencode: metadata is included everywhere, byte-identical to today — no behaviour change unless you opt in.

Three decisions worth calling out, because each has a wrong-looking-but-tempting alternative:

Reader role is structural, not configured. It comes from session.parentID == null, not from the agent's mode. A mode: "primary" agent dispatched as a subagent is a subagent — deriving role from config would get that backwards, and the whole point is to reason about the delivery, not the label.

An unknown key under opencode: is fatal and names the file. audiance: is valid YAML, so a permissive schema cannot distinguish a typo from an absent directive — and failing open silently reinstates exactly the delivery the directive was written to prevent. opencode: {} is treated as absence and included, since an empty map asserts nothing.

Filtering happens on every delivery path, not just the obvious one. buildSystem() is the path people think of; two others leak without it, and both were found by inspection rather than by a failing test:

  • Instruction.resolve() — nested AGENTS.md surfaced through the read tool. Needed a reader on Tool.Context, supplied by the production constructors and failing closed when absent.
  • createUserMessage's file: attachment path — hardcoded role: "main", so a subagent leaked main-only instructions whenever a dispatch carried an @file reference.

AudienceError is delivered as a typed failure rather than a defect, so a malformed file surfaces as a normal error at the call site.

Known limitation, documented in rules.mdx: the V2 SessionCore assembles system context through a separate path (packages/core/src/instruction-context.ts) with no audience filtering. That is bounded today because V2 has no task tool, so every V2 session is structurally main — but it stops being bounded the moment V2 gains dispatch. packages/core cannot import from packages/opencode, so sharing the filter means moving it into core; that is a larger change than this PR should carry.

How did you verify your code works?

  • New packages/opencode/test/session/instruction-audience.test.ts covers parsing, role/agent matching, wildcard semantics, OR/AND composition, directive stripping, and malformed-metadata failures.
  • Two-way mutation-tested intersection test in instruction.test.ts: neutering the audience filter leaks wrong-audience project files; neutering the origin filter leaks config files into project scope. Each failure names which filter died.
  • Red-first proof for both leak paths — subagents receiving nested main-only instructions through resolve(), and through an @file attachment — each failing before the fix and passing after.
  • Whole-branch mutation check: making the filter a no-op turns 11 tests red across audience exclusion, frontmatter stripping, and typed-failure delivery; restoring returns 99 pass / 0 fail.
  • bun test in packages/opencode: 3243 pass / 0 fail (dev baseline 3207). bun typecheck clean in packages/opencode and packages/core.
  • Docs updated in packages/web/src/content/docs/rules.mdx, including the V2 limitation above.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Adds an opencode.audience frontmatter directive so an instruction file can
declare who it is for: a role (main / subagent / all) and an agent glob,
matched as OR across entries and AND within one.

Reader role is derived structurally from whether the session has a parent, so
a primary-mode agent dispatched as a child is correctly a subagent. Absent
metadata means included everywhere, byte-identical; an unknown key under
opencode is fatal and names the file, because a typo like `audiance` parses
as valid YAML and failing open would silently reinstate the delivery the
directive exists to prevent.

Filtering covers system-prompt assembly, nearby-file resolution through the
read tool, and the prompt attachment path. The V2 SessionCore assembly path
is a separate implementation and is not covered.
@iceteaSA
iceteaSA force-pushed the instruction-audience branch from 8d0fddb to 136a4f0 Compare August 2, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Instruction files cannot declare which agents they are for

1 participant