cordis-plugin-instruction-scope
Verified@argszero/cordis-plugin-instruction-scope Β· v0.1.0 Β· MIT
Per-agent scoping for the dsh workspace instruction chain (discussion #7361). @deepseek-ai/dsh-agent-instructions injects the workspace instruction chain into every agent in the tree; its Config governs which files are discovered, globally, and nothing ex
Install
dsh plugin add @argszero/cordis-plugin-instruction-scope Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Tags
Readme
@argszero/cordis-plugin-instruction-scope
Per-agent scoping for the workspace instruction chain β the missing axis in discussion #7361.
@deepseek-ai/dsh-agent-instructions injects the workspace instruction chain into
every agent in the tree. Its Config governs which files are discovered,
globally; nothing in the harness expresses which agents receive them. For a
delegated child whose task needs zero project context β a bounded review, a source
check, a numeric recheck, a verdict β that is more than overhead:
- the chain arrives as a user-role message, i.e. with the same authority as the dispatcher's own prompt;
- it carries the user-global file (
$DSH_HOME/AGENTS.md) β personal workflow conventions, machine-local paths, credential-handling notes β that has nothing to do with the child's task; - it is appended to the child's durable log, so every later request in that child's session re-derives it, and a child that follows a startup protocol written for the main agent spends its budget loading files the dispatcher explicitly excluded.
This plugin supplies the missing axis without changing the harness: it decides, per agent, whether that agent receives the chain at all.
Install
npm install @argszero/cordis-plugin-instruction-scope
Mount it through a bundle patch (cordis.patch.yml ships with the package) or as a
profile layer:
- insert:
- id: instruction-scope
name: '@argszero/cordis-plugin-instruction-scope'
config:
rules:
- match:
origin: subagent
delegationDepth: { gte: 1 }
policy: omit
reason: bounded child, no project context needed
With no config the plugin changes nothing: defaultPolicy is keep.
Measure first
report is a real dry run β nothing moves, and the plugin still records exactly what
it would have omitted, including the rendered character count of the chain:
config:
defaultPolicy: report
Each verdict is logged once per session (log: first, the default) and retained on
the service API. It reads like this:
instruction-scope: agent=child-1 policy=report rule=default chars=16089 removed=0
That number is the measurement #7361 lists under "What I did not verify".
Selecting an agent
The report's central point is that the decision belongs to the dispatcher, not to
the child β "the child does not know the lane it was dispatched into". A plugin cannot
add a field to the subagent tool, so this one reads the lane from the surfaces that
already carry it, all synchronously:
| fact | source |
|---|---|
origin, delegationDepth, cwd, parentSession, isSeeded, agentPreset |
the child's durable session header β a frozen metadata read, not an event-log scan |
label |
the child's creation label, from the subagent session projection when it is registered |
label is the most dispatcher-controlled key available: the dispatcher literally
names the child. It is read through the projection registry, which is the sanctioned
post-resume seam; the repository prohibits new synchronous scans of arbitrary event
history. When dsh-subagent is not mounted the key is absent and the plugin degrades
to the header facts.
String fields accept * and ? globs. rules is ordered and first match wins, so
a narrow keep can carve a lane back out of a broad omit. A rule with no match
accepts every agent, which makes an ordered list end in a catch-all.
config:
defaultPolicy: keep
rules:
# Only the children dispatched for it.
- match: { label: 'bounded-*' }
policy: omit
reason: named for it
# Everything else delegated, of any depth.
- match: { origin: subagent, delegationDepth: { gte: 1 } }
policy: omit
# Except children working from a scratch tree, which keep their chain.
- match: { cwdPrefix: /tmp/scratch }
policy: keep
Policies
| policy | effect |
|---|---|
keep (default) |
the agent receives the chain exactly as it would without this plugin |
report |
nothing is touched; the verdict and the measured size are recorded |
omit |
the agent receives no workspace instruction chain at all |
How omission works
One listener: agent/pre-step, registered with { prepend: true }.
prepend is load-bearing. Waterfall entries run outermost-first and every listener's
next() continuation runs after all entries, so this plugin's await next() resolves
with the decision agent-instructions has already composed its message into. That is
the only position from which the injected message is both visible and still removable.
The decision is not a display concern: the agent loop's Agent.step() appends exactly
decision.messages to the durable log as user/message events, and the next request is
derived from that log. Removing a message from the decision removes it from the child's
context and from the child's durable transcript.
The loop explicitly supports the resulting shape β its turn loop documents that "an enter decision rewritten to empty still owns the initial turn boundary, but it spends no model call". Rewriting the batch is a designed seam, not a hack against the loop.
A second action keeps the omission stable. agent-instructions stages its composed
message into the pending inbox before it enters the decision, and it decides "already
supplied" by scanning the claimed batch and the durable log for a message whose
source is agent-instructions with baseline: true. This plugin therefore also removes
a staged instruction message from inbox.nextStep / inbox.nextTurn, so no later claim
can pick it up.
API
The plugin provides instructionScope:
ctx.instructionScope.verdicts() // ScopeVerdict[], bounded by maxRecords
ctx.instructionScope.counts() // { keep, report, omit }
ctx.instructionScope.reset()
ctx.instructionScope.decide(agent) // resolve a verdict without recording or applying
Honest boundaries
- This is omission, not narrowing. A matched agent receives the whole chain or none
of it. Dropping only the user-global file while keeping the project chain is not
implemented: it would mean rewriting the text of a message whose source contract
(
baseline,baselineIdentity,changes) belongs toagent-instructions, and that package reconciles later file edits for every scope it rendered β a rewrite that removes a scope from the text would still let an edit to that file surface through the reconciliation channel. Narrowing is a core change, not a wrapper. - Omission costs a re-composition per step.
agent-instructionstreats a visible durable baseline as proof the chain was already supplied. With nothing durable to find it re-discovers and re-reads the instruction files on every pre-step, and this plugin discards each result. That repeated I/O is the price of never writing the chain to the child's log β which is the point of the policy, and exactly what makes the omission airtight. - It cannot name the dispatcher. Rules match on lane facts, so a rule can apply to every subagent or to a named class of child, but a dispatcher that wants a one-off opt-out still has to pick a label or cwd the rule can see.
- It fails open. Any error while resolving a verdict leaves the decision untouched. An instruction-scope guard must never be the reason a turn dies.
Tests
npm test
Behaviour tests run against real Cordis, the real agent loop, real production agents
with their real durable inbox, the real dsh-agent-instructions plugin, the real
filesystem, and the loop's own agent/pre-step waterfall. The control arm is the same
fixture with the plugin absent, and it proves the reported behaviour is real before the
guarded arms prove it is gone. test/packaging.spec.mjs asserts the packaging
invariants that only installing the packed artifact can witness.
Licence
MIT