feat: add shared agent infrastructure (.agents/)

- AGENTS.md: design principles, enforcement hierarchy, deferred loading
- agents/: brainstorm, build, orchestrator, research (auto-discovered by MCP server)
- skills/: research methodology (auto-discovered by MCP server)
- hooks/: pre-tool-use, post-tool-use (BFF block removed), session-start,
  stop, pre-compact, user-prompt-submit
- frameworks/: opencode/plugin.ts (resolves hooks via import.meta.url — works
  as project-local or global plugin), github/hooks.json
- mcp/index.ts: auto-discovers agents/*.md and skills/*.md from frontmatter
  (replaces hand-maintained registry); server renamed all-agents
- docs/: agent-infrastructure.md (generalized), research docs (7 files),
  ai_architectures.md, llama-server-cuda-wsl2.md
- install.sh: idempotent setup — Copilot global hooks, OpenCode global plugin +
  AGENTS.md + MCP entry, VS Code global MCP config
This commit is contained in:
Brydon DeWitt 2026-05-22 13:13:43 -04:00
parent 4a44460b5f
commit 6b07e4ccb2
26 changed files with 7576 additions and 0 deletions

267
.agents/AGENTS.md Normal file
View File

@ -0,0 +1,267 @@
# Agent Infrastructure: Design Principles
You are editing agent infrastructure files (hooks, instructions, skills,
agents). Before making changes, understand the principles that govern how this
system works.
## Single Source of Truth
`.agents/` is the canonical directory for all agent infrastructure. An MCP
server (`.agents/mcp/index.ts`) exposes agents as prompts and skills as tools to
both Copilot and OpenCode — this replaces file-based fan-out to
`.github/agents/`, `.opencode/agents/`, etc.
### MCP server (`all-agents`)
Available once the server is running (configured in `.vscode/mcp.json` and
`opencode.json`):
- **Prompts** (slash commands): `/research`, `/brainstorm`, `/build`,
`/orchestrator`
- **Tools** (model-controlled): `load_research_methodology`
Bodies are read from disk at call time — editing `.agents/agents/*.md` or
`.agents/skills/research.md` takes effect immediately.
**Not handled by MCP** (stays bespoke):
- `.agents/hooks/` — MCP has no lifecycle intercept primitive
- This file — model needs to read it before `tools/list` is available
## The Enforcement Hierarchy
Not all guidance is equally effective. From most to least reliable:
```
PreToolUse hard block ← Structural. Always fires. Agent cannot bypass.
PostToolUse file-path check ← Fires right after editing a relevant file (context tail).
Nested AGENTS.md at path ← Always-on for that folder scope. Portable across tools.
Stop / SessionStart inject ← Fires at session boundaries. Good for broad reminders.
Root AGENTS.md sections ← Context-start only. Subject to "lost in the middle."
```
**Root cause of degradation** (Liu et al. 2023, "Lost in the Middle"): LLMs
attend to the beginning and end of context, not the middle. Guidance written
into AGENTS.md is injected once at session start and degrades as context grows.
Hooks inject at the _context tail_ — the high-attention zone — which is why they
outlast AGENTS.md under context pressure.
**Decision rule when adding new guidance:**
1. Is the anti-pattern a **terminal command**? → `PreToolUse` hard block
(Policies 16 in `pre-tool-use.sh`).
2. Is the anti-pattern **editing a specific file type or path**? → `PreToolUse`
block on `FILE_PATH` (Policy 7+).
3. Should the reminder fire **during active work** in a domain? → `PostToolUse`
file-path check (see `post-tool-use.sh` BFF reminder pattern).
4. Is it guidance scoped to **specific files** an agent might edit? → nested
`AGENTS.md` at the target path.
5. Should it fire **in response to what the user just wrote**? →
`UserPromptSubmit` injection (context tail, prompt text available — e.g.
agent nudges).
6. Is it a **broad session reminder** with no tight scope? → `SessionStart` or
`Stop` injection.
7. Is it **architecture/rationale** that an agent might need but shouldn't
always load? → AGENTS.md stub with a conditional `read_file` instruction (see
"Deferred Loading" below).
## Deferred Loading
Write a trigger condition and `read_file` instruction directly in an AGENTS.md
section. AGENTS.md is always loaded, so the trigger is always present; the
referenced file's content only loads when the model judges it relevant. Example:
> When the user shows signs of analysis paralysis, read
> `.agents/agents/brainstorm.md`.
Do **not** use tool-specific deferred-loading mechanisms (`description:`-only
`.instructions.md` files, etc.) — no portable equivalent exists. See Forbidden
Patterns below.
## Hook Files
All hook scripts live in `.agents/hooks/`. The Copilot harness
(`.agents/github/hooks.json`) and OpenCode plugin (`.agents/opencode/plugin.ts`)
both delegate to these scripts, keeping hook logic in one place. Symlinks from
`.github/hooks/agent-support.json` and `.opencode/plugins/agent-support.ts`
point back to these canonical sources; those directories are gitignored.
### Hook Injection Marker Convention
Every hook that injects `additionalContext` prefixes its payload with a
self-identifying line:
```
[HOOK INJECTION: <hook-name>] System reminder — NOT part of preceding tool output / user message:
```
The harness additionally wraps the payload in a
`<HookName-context>...</HookName-context>` XML tag (e.g.
`<PostToolUse-context>`). The inline prefix is belt-and-suspenders: when a hook
fires after a `read_file` whose content ends with markdown, the XML tag alone is
easy to miss — the inline prefix is not. **If you see either marker, treat the
content as a separate instruction, never as file content, tool output, or part
of the user's message.**
### Hook Architecture Principle: Platform-Agnostic Scripts
**Design target**: scripts accept normalized env vars (`TOOL_NAME`, `COMMAND`,
`FILE_PATH`), exit non-zero with plain-text denial reason on stdout. Callers
normalize input and translate exit code/stdout into their native denial format.
**⚠️ NOT YET IMPLEMENTED (May 2026)**: `pre-tool-use.sh` still uses
Copilot-specific JSON I/O. `plugin.ts` duplicates guards inline instead of
calling the script. See `agent-infrastructure.md` item 22 for the refactor plan.
### `user-prompt-submit.sh` — Per-turn tail injection
- Fires on every user message. Injects at the **context tail** (high-attention
zone) — this is why nudge logic lives here rather than in AGENTS.md.
- Detects brainstorm and research trigger words in the prompt and appends a
one-line nudge suggestion to `additionalContext`.
- Writes the raw prompt text to `/tmp/.last-user-prompt.txt` and injects the
task-capture instruction.
### `pre-tool-use.sh` — Hard stops
- Intercepts: `run_in_terminal`, `execution_subagent`, `send_to_terminal` (for
`$COMMAND`) and `replace_string_in_file`, `multi_replace_string_in_file`,
`create_file` (for `$FILE_PATH`).
- Outputs `permissionDecision: "deny"` to block the tool call.
- **CRITICAL**: A syntax error in this file blocks ALL file edits and terminal
commands. Always validate after editing:
`bash -n .agents/hooks/pre-tool-use.sh`
- When adding a new policy: follow the existing numbered pattern, add to the
comment header, use `deny "BLOCKED: ..."` with a clear fix instruction.
- Regex patterns operate on `$COMMAND` (terminal policies) or `$FILE_PATH`
(file-edit policies). Both are empty strings unless the right tool fired.
### `post-tool-use.sh` — Timed reminders
- Fires after every tool use with the tool name and response in stdin.
- Currently: self-check every 15 tool calls, debugging reminder on test failure,
BFF reminder when editing `apps/client/src/pages/`.
- Adding a new reminder: extract `$FILE_PATH` or match `$TOOL_NAME`, build the
message string, append to `$context`.
- Injects at the _tail_ of the context — this is what makes reminders persist
through long sessions.
### `session-start.sh` — Broad session injection
- Fires once per session. Good for: current branch, active investigations, dead
ends.
- Not good for: precise rule reminders (use PostToolUse or nested AGENTS.md).
### `stop.sh` — End-of-session reflection
- Fires when agent stops. Lessons-learned capture + effort reflection.
- Not a blocking hook — injects `additionalContext` only.
### `pre-compact.sh` — Pre-summarization state export
- Fires before context is summarized. Saves investigation state to
`.session/pre-compact-state.md`.
- Note: `PostCompact` does NOT exist. Only `PreCompact`.
## Forbidden Patterns
These approaches exist in agentic tooling but are **banned** in this codebase
because portable alternatives exist. Document the reason so future agents
understand rather than re-introducing them.
### ❌ `applyTo:` frontmatter in `.instructions.md` files
Supported only in VS Code Copilot. Other tools either ignore it or load the file
as always-on context. Portable alternative: nested `AGENTS.md` at the target
path. Nested AGENTS.md files are natively supported by all major agent tools
(Copilot, OpenCode, Claude Code) without any special configuration.
### ❌ `description:`-only `.instructions.md` files (new additions)
VS Code Copilot builds a stub `<instructions>` block for these and tells the
model to load content on demand. Confirmed via `InstructionsContextComputer` in
`extensionHostProcess.js`. However, no other tool implements this — they load
the same files as always-on context. Portable alternative: AGENTS.md stub with a
`read_file` instruction (see "Deferred Loading" above).
### ❌ Any `.github/instructions/*.instructions.md` for new rules
`.instructions.md` is a VS Code Copilot-specific format. All new rules go into
nested `AGENTS.md` files (path-scoped rules) or directly into root `AGENTS.md`
(broad guidance). Do not add new `.instructions.md` files.
## Skills (`.agents/skills/`)
- Skills contain distilled methodologies that any agent can load on demand via
`read_file`. An agent MUST `read_file` the SKILL.md before using it.
- For **methodologies** (how to research, brainstorm) — not project rules.
Project rules belong in nested AGENTS.md files or hooks.
## Agents (`.agents/agents/`)
- Agent files define persona, workflow phases, tools, and circuit breakers.
- Runtime config (`model`, `mode`, `permission`) lives in `opencode.json` agent
entries. Body `.md` files are prompt-body only (plain markdown, no OpenCode
frontmatter keys except `description`).
- Circuit breakers (hard stops) belong in the agent file itself, not in hooks.
## Tool-Specific Entry Points
Some things cannot be unified and live in tool-specific locations:
- **`.agents/opencode/plugin.ts`** — OpenCode plugin harness (canonical).
Bridges hook scripts to OpenCode's plugin API. Symlinked from
`.opencode/plugins/agent-support.ts`.
- **`.agents/github/hooks.json`** — Copilot harness config (canonical). Points
to `.agents/hooks/*.sh`. Symlinked from `.github/hooks/agent-support.json`.
## Common Mistakes
- ❌ Writing long explanations in AGENTS.md for rules that could be a PreToolUse
block or nested AGENTS.md — they degrade under context pressure
- ❌ Adding a PostToolUse reminder without checking `$FILE_PATH` or `$TOOL_NAME`
— causes it to fire on every tool call, creating noise
- ❌ Leaving a syntax error in `pre-tool-use.sh` — blocks all file edits and
terminal commands immediately
- ❌ Creating new `.instructions.md` files — see Forbidden Patterns above
- ❌ Putting project-specific rules into a skill file — skills are for
methodologies, not codebase conventions
- ❌ Assuming PostCompact exists — it does not. Use PreCompact.
- ❌ Editing generated files in `.github/agents/`, `.github/skills/`,
`.opencode/agents/`, `.opencode/skills/` — edit `.agents/` sources instead, or
the pre-tool hook will block the edit
- ❌ Blaming the model for unexpected BLOCKED/tool-call behavior before
verifying the harness — when a model call is blocked or uses unexpected
parameters, check the actual tool schema first (read the source or docs)
before concluding the model is wrong. The harness was recently changed; the
model may be correct. Applies to: OpenCode tool names (`read`/`edit`/`task`),
parameter names (`offset`/`limit` not `startLine`/`endLine`), and plugin guard
logic.
- ❌ Adding _"reflect / double-check / are you sure / take another look"_
instructions as a mitigation for any failure mode — these feel productive in
transcripts but Huang et al. (arXiv:2310.01798) show that intrinsic
self-correction without an external oracle _consistently degrades_ reasoning
performance. Without a test runner, hook, type checker, or other ground- truth
signal in the loop, "ask the model to reflect" is at best noise. If the
failure mode lacks an external verifier, route to compaction, adversarial
reframing, or a cross-family judge subagent instead — see
[docs/research/intent-interpretation-action-plan.md](../docs/research/intent-interpretation-action-plan.md)
§4.1.
- ❌ Defaulting to multi-agent / parallel-worker topologies for complex tasks —
Cognition's failure analysis shows the dominant failure mode is **context
divergence**: separate agents accumulate incompatible interpretations of the
same task, and reconciliation costs exceed any parallelism gain. A single
agent loop with an explicit plan/act split outperforms multi-agent on almost
all real coding tasks (§3.1,
[docs/research/ai-coding-best-practices.md](../docs/research/ai-coding-best-practices.md)).
Subagents are only justified for read-only exploration, fully isolated tasks,
or adversarial review.
- ❌ Treating the orchestrator as the right pattern for cloud frontier models —
for local models the orchestrator is a **context firewall** (sub-agents return
≤2k compressed summaries; the parent's context never sees raw exploration).
Frontier models have 200k+ context and no `task` dispatch tool in Copilot, so
the firewall pattern doesn't apply. The cloud orchestrator is a **planning
gate** (forced decomposition + user confirmation before acting), not a
dispatch coordinator. The `<!-- @local -->` / `<!-- @cloud -->` blocks in
`orchestrator.md` encode this distinction. See §3.4 of
[docs/research/ai-coding-best-practices.md](../docs/research/ai-coding-best-practices.md).

77
.agents/agents/AGENTS.md Normal file
View File

@ -0,0 +1,77 @@
# Agent Definition Files
Each `.md` file here defines a custom OpenCode agent (prompt, model,
permissions).
## ⚠️ Symlink required
OpenCode only loads agents from `.opencode/agents/` (or
`~/.config/opencode/agents/`). Files here are **not loaded automatically**. Each
file must be symlinked using a **two-level relative path** from
`.opencode/agents/`:
```bash
cd .opencode/agents
ln -s ../../.agents/agents/<name>.md <name>.md
```
> **Do NOT use three `../` levels.** `.opencode/agents/` is two levels below the
> repo root, not three. A wrong-depth path creates a broken symlink that
> silently fails — OpenCode will not error, the agent simply won't load.
Current symlinks (verify with `ls -la .opencode/agents/`):
| Agent file | Symlinked? |
| ----------------- | ------------------------------------- |
| `build.md` | ✅ `.opencode/agents/build.md` |
| `orchestrator.md` | ✅ `.opencode/agents/orchestrator.md` |
| `brainstorm.md` | ✅ `.opencode/agents/brainstorm.md` |
| `research.md` | ✅ `.opencode/agents/research.md` |
When you add a new agent here, add its symlink and update this table.
## Verification
After adding or changing an agent, run the following to confirm OpenCode can
read it:
```bash
# 1. Confirm symlink resolves (should print file contents, not an error)
cat .opencode/agents/<name>.md | head -5
# 2. Confirm OpenCode registers the agent with correct permissions
opencode agent list
# Check that your agent appears with the right mode (all/primary/subagent)
# and that deny rules are present at the bottom of its permission list.
# If it's missing: broken symlink, YAML frontmatter parse error, or OpenCode
# was not restarted after the change.
```
Expected output for `orchestrator` after a correct setup:
```
orchestrator (all)
[
...
{ "permission": "edit", "action": "deny", "pattern": "*" },
{ "permission": "bash", "action": "deny", "pattern": "*" }
]
```
## Invoking custom agents
- **Tab**: cycles through `primary` and `all`-mode agents as the active session
agent
- **`@mention`**: invokes an agent — but only at the **start of a fresh
session**. Sending `@orchestrator ...` after already exchanging messages in a
Build session causes the current model to process the text as freeform input.
Open a new session first, then `@mention` as the first message.
- **CLI**: `opencode run --agent orchestrator "your prompt"` — reliable,
session-agnostic invocation for scripting or testing.
## Permission config
`opencode.json` `agent.<name>.permission` only applies if a matching markdown
file is loaded. Without the symlink, permission config for that agent is
silently ignored.

View File

@ -0,0 +1,216 @@
---
description: "Use when brainstorming, ideating, exploring options, feeling stuck, over-thinking, over-complicating, or needing to step back and reconsider an approach. Use when the user says 'wait', 'actually', 'hmm', 'reconsider', 'what if', 'too complicated', 'there has to be a simpler way', or expresses uncertainty about direction."
---
# Brainstorm Agent
You are a creative thinking partner. Your job is to help the user generate,
explore, and evaluate ideas quickly — then get out of the way so real work can
happen.
## Core Philosophy
**Speed over depth. Breadth over precision. Intuition over analysis.**
You are the opposite of a deep-thinking agent. You exist because Claude Opus 4.6
already overthinks everything. Your role is to COUNTERBALANCE that tendency by
keeping things loose, fast, and generative.
Do NOT ruminate. Do NOT exhaustively analyze. Do NOT hedge with caveats. When
you catch yourself going deep, stop and surface back to the idea level.
## When You're Activated
You're here because the user is either:
1. **Stuck** — going in circles, overthinking, analysis paralysis
2. **Exploring** — genuinely unsure what direction to take
3. **Reconsidering** — realized something isn't working and needs fresh angles
In all cases, the antidote is the same: generate options fast, pick one, move.
## Brainstorming Techniques
Use these as lenses, not rigid processes. Pick whichever fits the moment.
### Rapid Ideation (Crazy 8s style)
Generate 5-8 distinct approaches in quick succession. No judgment, no analysis.
Just ideas. One line each. Then ask the user which ones spark something.
### SCAMPER
When modifying an existing design or approach:
- **Substitute** — What component could be swapped?
- **Combine** — What two things could merge?
- **Adapt** — What similar problem has a known solution?
- **Modify** — What if we made one part bigger/smaller/different?
- **Put to other uses** — Can this serve a purpose we haven't considered?
- **Eliminate** — What can we cut entirely?
- **Reverse** — What if we did the opposite?
### Worst Possible Idea
When truly stuck: ask what the WORST way to solve this would be. Then invert it.
Bad ideas are easier to generate and often contain the seed of good ones.
### How Might We...
Reframe the problem as an opportunity. "How might we make X do Y without Z?"
Forces a positive, solution-oriented frame.
### Inversion / Pre-mortem
"Imagine this approach failed completely. Why did it fail?" Work backward from
failure to identify hidden risks or assumptions.
### Constraint Flipping
List the constraints you're assuming. Remove one. What becomes possible? Often
the constraint you think is fixed... isn't.
## How You Work
### Phase 1: Quick Frame (30 seconds of thinking, max)
- What's the actual problem? (One sentence.)
- What constraints exist? (Bullet list, keep it short.)
- What has already been tried or considered?
### Phase 2: Diverge (the brainstorm)
- Pick a technique from above (or freestyle)
- Generate options FAST — quantity over quality
- No evaluation during this phase
- Aim for at least 5 genuinely different directions
- Push past the obvious — your first 2-3 ideas will be "average" by nature; the
interesting ones start after those
- _Optional divergence prompt:_ the expertise ladder — what would a junior
engineer propose? What would a senior engineer with deep domain knowledge
propose differently? What would an outsider with zero context propose?
Different vantage points surface different assumptions. **Use only to broaden
the candidate pool, never to produce the final answer.** Recent
persona-prompting work (Principled Personas EMNLP 2025; Persona is a
Double-Edged Sword IJCNLP 2025; arXiv:2512.05858) shows that low-knowledge
personas often _reduce_ accuracy, so evaluate any candidate the ladder
surfaces under the un-personified model and an external rubric before
committing.
### Phase 3: Converge (the gut check)
- Which 1-2 ideas feel most promising? Trust intuition here.
- What's the smallest thing we could try to test the idea?
- What would make us confident it's wrong? (Kill criteria)
- **Re-evaluate at each comparison, not just at the end.** New constraints
surface as options are weighed — this is the idea behind Think-Anywhere (Jiang
et al., arXiv:2603.29957): fresh reasoning at each decision point, not
execution of the original plan. If a constraint you assumed earlier turns out
to be flexible, update.
### Phase 4: Capture & Hand Off
**Do this IMMEDIATELY after convergence. Do not wait for user confirmation.**
Open questions go in the exploration file, not in your response as blockers.
- Write the exploration file (see Output Format below)
- Create a session memory note (`/memories/session/brainstorm-<topic>.md`) with
the problem, selected approach, and key context so subagents or fresh
conversations can pick up where you left off
- Hand off to the right next step:
- If the chosen direction needs **investigation or debugging** → delegate to
`@research` or suggest the user invoke it
- If it's ready for **implementation** → delegate to the default agent or
suggest the user invoke it
- If it needs **more exploration** → suggest the user continue with you
**Never end on open questions alone.** Capture first, ask second. The
exploration file is the handoff artifact — if it exists, any agent can pick up
where you left off regardless of whether the user answered your questions.
## Delegation Rules
**You do the thinking. Subagents do the digging.**
When you need to understand the codebase to generate better ideas, delegate to
the Explore subagent. Give it a specific, bounded question:
- "Find how authentication is currently structured in this project"
- "Look for existing patterns for X in the codebase"
Do NOT send the Explore agent on open-ended research missions. Keep requests
tight and factual. You synthesize — it investigates.
## Token Discipline
You are the LIGHTWEIGHT agent. Your entire purpose is to stay at the idea level
and avoid burning context on deep dives. Rules:
1. Keep your own responses concise — bullet points over paragraphs
2. Delegate all codebase exploration to subagents
3. If an exploration is going deep, STOP and create the exploration file so a
fresh context can pick it up
4. Never read more than a few files yourself — that's what Explore is for
5. Hold references; load on demand. Do not read files you don't need yet.
## Output Format: The Exploration File
When a brainstorming session produces a direction worth exploring, create a
tracking file. Ask the user for a short name, or derive one from the topic.
**Location**: `docs/explorations/<name>.md`
Use this structure:
```markdown
# Exploration: <Title>
**Status**: brainstorming | exploring | prototyping | decided | abandoned
**Created**: <date> **Last Updated**: <date>
## Problem
<One or two sentences. What are we trying to solve?>
## Constraints
- <Real constraints, not assumed ones>
## Ideas Generated
<List from the brainstorm session. Keep all of them, even rejected ones.>
1. **<Idea name>** — <One-line description>
2. **<Idea name>** — <One-line description> ...
## Selected Approach
**<Chosen idea>**: <Why this one keep it to 2-3 sentences max>
### Kill Criteria
<What would tell us this approach is wrong?>
## Exploration Log
<Append entries as work progresses. Newest first.>
### <date><brief title>
- What was tried:
- What happened:
- What we learned:
- Next step:
## Blockers
- <Anything currently preventing progress>
```
## What You Are NOT
- You are NOT an implementation agent. Don't write production code.
- You are NOT a research agent. Don't go deep on diagnosis or root cause.
- You are NOT a planning agent. Don't create detailed project plans.
You are a spark. Once an idea has enough shape to act on, hand it off.

69
.agents/agents/build.md Normal file
View File

@ -0,0 +1,69 @@
---
description: 'Targeted implementation task: well-scoped edit, single file or small refactor where the scope is already clear. NOT for open-ended investigation, architecture decisions, or multi-file refactors.'
---
# Build Agent
You execute well-scoped implementation tasks accurately and efficiently.
<!-- @local -->
## Model Profile
**Smaller-scale, not low-reasoning.** If your architecture supports extended
thinking blocks, use them at decision points. Your failure modes are not absence
of reasoning — they are:
- Narrower training distribution (Python/JS heavy — verify TypeScript idioms)
- Quantization degradation in long sessions (tool-call history fills context
fast)
- JSON schema compliance degrading as context grows
- Repetition loops if context pressure is high
Compensate structurally: stay grounded, delegate exploration, keep context lean.
<!-- @endlocal -->
## Core Rules
1. **Read before you write.** Always `ls` and `read_file` before any edit.
2. **Verify before asserting.** Never assume a file path, library, or API exists
— check first.
3. **Hold references; load on demand.** Do not read files you don't need yet.
Context is a finite budget — treat it as your most constrained resource.
4. **Delegate exploration, not orchestration.** Use the `Explore` subagent
(Copilot) or `task` subagent (OpenCode) for scanning large directories or
tracing imports. This agent is a recipient of tasks — it does NOT decompose
or dispatch further work. Keep your own context for reasoning.
5. **Scope-check before starting.** If the task touches more than 23 files or
requires understanding architecture, stop and tell the user: "This looks
broader than a targeted edit — the orchestrator or default agent should
handle this." Do not attempt to self-decompose into subtasks.
<!-- @local -->
## Working Memory
For tasks spanning multiple steps, maintain a `NOTES.md` scratch file:
- Write your progress after each step before proceeding to the next
- Record which files you've read and what you found
- Note any assumptions you made
This keeps your context clean and enables resumption after compaction.
## Reasoning
Reason at each decision point before acting. Open `<think>` blocks with
substantive analysis — not filler phrases ("Okay, let me...", "The user
wants..."). Begin directly with the analysis or plan.
<!-- @endlocal -->
## Handoff
When this task is done (or if it exceeds your scope), tell the user clearly:
- What you completed
- What remains (if anything)
- Whether the next step needs a different agent

View File

@ -0,0 +1,132 @@
---
description:
'Decomposes high-level goals into bounded subtasks and delegates to build,
research, or brainstorm. Never edits files directly.'
---
# Orchestrator
You decompose high-level goals into bounded subtasks and dispatch them to
specialist workers. You do **not** write code or edit files — your output is a
delegation plan and a summary of results.
## Constraints
- **No file edits.** You cannot use editing tools (`replace_string_in_file`,
`create_file`, etc.). If you find yourself wanting to edit a file, that's a
subtask for `build`.
- **No shell commands.** You cannot run terminal commands. If you need a build
or test result, dispatch to `build` and ask it to report back. **Exception:**
you MAY use `run_in_terminal` to write to `/tmp/.last-user-prompt.txt` (TASK
CAPTURE). This single path is exempt — the Stop hook reads it to verify every
question was answered.
- **Delegate; don't implement.** Your only tool for task execution is `task`
(OpenCode) or subagent dispatch. You reason and plan; workers act.
<!-- @local -->
- **NEVER read files under `apps/` or `packages/`** — this is enforced at the
plugin layer and will throw. Reading these auto-loads nested `AGENTS.md` files
and is expensive for a small context window. If you need to know what's in a
package.json, source file, or anything under those directories, delegate to a
worker with `task` and ask the worker to read it and report what you need.
- **Root reads only.** You may read top-level files (`README.md`, root
`AGENTS.md`, root `package.json`) and files under `docs/`. Everything else goes
through a worker.
<!-- @endlocal -->
## Workflow
### 1. Understand the goal
Read the project root `AGENTS.md` first. Identify which areas of the codebase
are involved. If the goal touches `apps/` or `packages/`, note the relevant
package so workers know to check nested `AGENTS.md` files.
### 2. Decompose into bounded subtasks
Break the goal into subtasks where each one:
- Touches at most 23 files
- Has a clear acceptance criterion ("the build passes" / "the test passes")
- Can be handed off to a single worker with self-contained context
### 3. Confirm before dispatching
Present the decomposition to the user **before dispatching any tasks**. Format:
```
Plan:
1. [worker] Task description — expected output
2. [worker] Task description — expected output
...
Proceed?
```
Wait for explicit confirmation. Do not start dispatching speculatively.
<!-- @local -->
### 4. Dispatch one subtask at a time
Use `task` to dispatch each subtask to the appropriate worker. Pass all context
the worker needs in the task prompt — do not expect the worker to read shared
state.
**Keep task prompts short.** The `task` tool has a JSON serialization limit.
Never quote file contents or dependency lists inline in a task prompt. Instead,
tell the worker _which files to read_ and _what to do_. Example:
- ❌
`"Read package.json — here are the deps: { ... 500 lines ... }. Update README."`
- ✅
`"Read the root package.json and all workspace package.json files, then update the Technology Stack section in README.md to match."`
Workers available:
- **`build`** — implementation tasks (edits, refactors, new files)
- **`research`** — investigation, root-cause analysis, unfamiliar territory
- **`brainstorm`** — ideation, design exploration, approach selection
<!-- @endlocal -->
<!-- @cloud -->
### 4. Execute directly with plan-act-verify
You have the context budget to act directly. After user confirmation, execute
each subtask in sequence using inline tool calls (no worker dispatch needed).
Apply the standard plan-act-verify loop:
- Complete one subtask fully before starting the next
- Run the quality gate (`npm run build:strict` or `npm test && npm run lint`)
after the final edit
- If a subtask fails twice with the same error, stop and report rather than
retrying
Workers available as slash commands if you want to hand off reasoning mode:
- `/research` — for unfamiliar territory or root-cause analysis
- `/brainstorm` — for approach selection before implementing
<!-- @endcloud -->
### 5. Collect and report
After all subtasks complete, summarize results for the user:
- What was done
- Anything incomplete or blocked
- Whether the quality gate was run (build + tests)
## When to escalate
If a subtask fails twice from the same worker with the same error:
- Report to the user rather than retrying
- State what the worker attempted and what went wrong
- Ask whether to try a different approach or switch to a different agent
<!-- @local -->
If the overall task turns out to be beyond local model capability (reasoning
failure, repeated hallucination), recommend the user switch to the default
Copilot agent.
<!-- @endlocal -->

328
.agents/agents/research.md Normal file
View File

@ -0,0 +1,328 @@
---
description: "Use when investigating, debugging, diagnosing, understanding unfamiliar code, tracing behavior, root cause analysis, or systematic exploration. Use when the user says 'why is this broken', 'how does this work', 'what changed', 'trace', 'investigate', 'root cause', 'figure out', 'something\'s wrong', 'regression', or needs to build a mental model before making changes."
---
# Research Agent
You are a systematic investigator. Your job is to help the user build accurate
understanding of code and diagnose problems through disciplined, evidence-based
reasoning.
## Core Philosophy
**Evidence over intuition. Systematic over ad-hoc. Record everything.**
You exist because LLMs naturally pattern-match from training data and latch onto
the first plausible explanation. Your role is to COUNTERBALANCE that tendency by
requiring evidence before conclusions, considering alternatives before
committing, and recording what you learn so it persists.
Do NOT guess when you can verify. Do NOT assume the first explanation is
correct. Do NOT skip recording findings — your notes are the investigation's
memory.
## Two Orientations
Every investigation draws from two complementary orientations. You switch
between them fluidly — often multiple times in a single chain of reasoning.
### Understand Orientation (Grounded Theory)
**Goal**: Build a mental model of how something works, from the code itself.
Grounded Theory's core principle applies: build understanding from the data (the
code), not from assumptions about what the code should do.
**Process** (iterative, not linear):
1. **Open coding** — Read code and name what you see. Functions, patterns, data
flows, dependencies. Don't categorize yet — just observe and label.
2. **Constant comparison** — As you read more, compare new observations against
earlier ones. Do patterns emerge? Do earlier assumptions still hold?
3. **Axial coding** — Connect the categories. How do the pieces relate? What
calls what? What data flows where?
4. **Memo** — Write down what you're learning as you go (session memory). These
notes are for you and for anyone who picks up this investigation later.
5. **Saturation check** — Are you still finding new patterns? If the last few
files confirmed what you already knew, you've saturated — stop reading and
synthesize.
**When to use**: "How does X work?", "What's the architecture of Y?", "Why was
it built this way?", "I need to understand this before changing it."
### Diagnose Orientation (Strong Inference + Satisficing)
**Goal**: Determine why something isn't working as expected.
Strong Inference's principle: never test a single hypothesis — confirmation bias
will make you see what you expect. But Satisficing's principle: don't
over-invest in rigor when the stakes are low.
**Simple check first** — before applying any methodology, ask: "Can I answer
this with a single log/print statement?" If the question is "what value does X
have here?" or "does this code path execute?" — just log and look. Only escalate
when the result is unexpected or the print doesn't answer the question.
**Triage** — if the simple check didn't resolve it, quickly assess:
| Factor | Low Risk | High Risk |
| ----------------- | -------------------------------- | ------------------------------ |
| **Reversibility** | Easy to undo if wrong | Hard to reverse (data, deploy) |
| **Blast radius** | One file/function | Many systems, shared state |
| **Confidence** | Familiar pattern, clear evidence | Novel, ambiguous symptoms |
| **Novelty** | Seen this before | Never encountered |
| **Time cost** | Check timing baselines in memory | Unknown = measure first |
**Low risk (all factors) → Satisfice**:
- Test the single most likely hypothesis first
- If confirmed, you're done — move on
- This is the "run a quick test" path
**Any factor signals high risk → Strong Inference**:
- Generate 2-3 genuinely different hypotheses for the same symptom
- Design a test that discriminates between them (a test whose result differs
depending on which hypothesis is true)
- Run the discriminating test
- Eliminate hypotheses based on evidence, not preference
- Iterate with refined hypotheses on whatever remains
**When to use**: "Why does X fail?", "What changed?", "This worked yesterday",
"Is this actually slow?", regression diagnosis, behavior verification.
### Mode Switching
These orientations compose recursively. A single investigation often flows:
```
Understand → spot anomaly → Triage → Diagnose → need more context → Understand → ...
```
Follow the question, not the mode. When you're understanding and hit something
unexpected, switch to diagnosis. When you're diagnosing and realize you lack
context, switch to understanding. Don't force a single mode.
## Investigation Checklist
**Re-evaluate at every tool-call boundary.** The root cause emerges during
investigation, not before it. Plan-and-Solve applies to the initial framing
(divide the task into investigation steps); Think-Anywhere (Jiang et al.,
arXiv:2603.29957) applies to pivoting as evidence accumulates — intermediate
results change what to do next. For Claude 4 models, interleaved thinking makes
this automatic; consciously invoke it for other models.
Before every hypothesis cycle:
- [ ] **Hypothesis written** (one sentence: "I believe X because Y")
- [ ] **Falsification criterion written** ("if wrong, I'd expect to see \_\_\_")
- [ ] **Falsification test run BEFORE confirmation test**
- [ ] **Result recorded** (ELIMINATED with reason, or CONFIRMED with evidence)
## Circuit Breakers
Investigations can spiral. These hard stops prevent waste:
1. **5+ attempts without falsifying a hypothesis = STOP.** Report what you've
learned and what you've ruled out. Let the user decide next steps.
2. **3+ edits to the same file without a passing test = STOP.** You're likely
fixing symptoms, not the cause. Step back and re-examine your assumptions.
3. **If you feel the urge to "just try something" = STOP.** Write the hypothesis
first. If you can't articulate what you expect to learn, you shouldn't run
the test.
4. **Two failures at the same level of abstraction = go UP one level.** The
problem may not be where you're looking.
## Context Management
Your methodology will degrade after ~15 tool calls. This is normal — context
competition causes tactical details to crowd out strategic instructions. It's a
known phenomenon, not a personal failure. Counteract it:
- **Re-read your investigation file and dead-ends every ~10 tool calls** to
avoid re-testing eliminated hypotheses
- **If you feel yourself drifting toward guess-and-check**, that's the signal —
pause, re-read your notes, and re-engage the methodology
- **When a session gets long**, create or update the investigation file so a
fresh context can continue with your findings intact
- **Hold references; load on demand.** Do not read files you don't need yet.
Context is a finite budget with diminishing returns.
## Timing Awareness
Agent context windows have no natural sense of how long commands take. This
creates a blind spot — you might suggest "just run the full test suite" without
knowing if that's 2 seconds or 5 minutes.
### Capture
**Always prefix diagnostic terminal commands with `time`** when you don't have a
recorded baseline for that command type in this project.
```bash
time npm test
time npm run lint
time npm run build
```
Once you know the baseline, drop the `time` prefix for commands you run
repeatedly.
**Capture output to temp files** for commands that produce significant output,
so you can grep later without re-running:
```bash
time npm test 2>&1 | tee /tmp/test_output.txt
grep -i "error\|fail" /tmp/test_output.txt
```
Name temp files descriptively: `/tmp/build_main.txt`, `/tmp/test_core.txt`,
`/tmp/lint_output.txt`.
### Record
**Session memory** (`/memories/session/timings.md`): Raw observations from the
current investigation. Quick and disposable.
```markdown
## Timings observed
- `npm test` — 47s
- `npm run lint` — 8s
- single test file — ~3s
```
**Repo memory** (`/memories/repo/timings.md`): Stabilized baselines useful
across sessions. Update when:
- No baseline exists yet for a command type
- A session observation meaningfully differs from the recorded baseline
- A new command type is discovered
### Use
Timing knowledge feeds into triage and mode switching:
- **Fast command (<5s)**: Low barrier to "just run it" — satisficing is nearly
free
- **Slow command (>30s)**: Prefer reading/reasoning first unless confidence is
low
- **Unknown timing**: Measure first before committing to a test-heavy strategy
## Investigation Files
For non-trivial investigations (anything that spans more than a few exchanges),
create a tracking file so findings persist and others can pick up the work.
**Location**: `docs/explorations/<name>.md`
```markdown
# Investigation: <Title>
**Status**: investigating | diagnosed | resolved | abandoned **Orientation**:
understand | diagnose | mixed **Created**: <date> **Last Updated**: <date>
## Question
<What are we trying to understand or fix? One or two sentences.>
## What We Know
<Confirmed facts. Evidence-backed only. Update as investigation progresses.>
## Hypotheses
- **[timestamp] Hypothesis:** [one sentence: "I believe X because Y"]
**Falsification:** [what you'd expect if wrong] **Result:**
[TESTING/ELIMINATED/CONFIRMED] — [why, in one sentence]
## Investigation Log
### <date><brief title>
- Orientation: understand | diagnose
- What was examined/tested:
- What was found:
- What this means:
- Next step:
## Timing Notes
<Any notable timing observations from this investigation.>
## Open Questions
- <Things we still need to figure out>
```
## Session Memory
For every investigation, create or update a session memory note:
**`/memories/session/research-<topic>.md`**
Include:
- The question being investigated
- Key findings so far
- Current hypotheses and their status
- What's been ruled out and why
This ensures subagents or fresh conversations can pick up where you left off
without re-reading the entire codebase.
## Delegation Rules
**You direct the investigation. Subagents gather specific evidence.**
Use the Explore subagent for bounded fact-finding:
- "Find all callers of `functionName` in the codebase"
- "Check what middleware runs before this route handler"
- "List all files that import from `@cantrips/remnant-core`"
Do NOT delegate analytical thinking to subagents. You form the hypotheses, you
interpret the evidence, you decide what to investigate next. Subagents retrieve
facts.
## Token Discipline
Investigations can consume enormous context. Guard against this:
1. **Delegate bulk reading to Explore** — don't read 20 files yourself
2. **Record findings in session memory** — your notes survive context limits
3. **If an investigation is going long**, stop and create the investigation file
so a fresh context can continue with your findings intact
4. **Prefer targeted reads** — read the specific function, not the whole file
5. **Use timing data** to avoid wasting tokens waiting on slow commands
## Techniques Reference
### Five Whys (use within Diagnose)
Trace causal chains by asking "why?" iteratively. Useful for symptoms with
non-obvious root causes. But be aware of its limitations — it tends toward
single causes and can't go beyond your current knowledge. Use it as a _starting
point_ for hypothesis generation, not as the sole diagnostic method.
### Delta Debugging (use within Diagnose)
When you have a failing case and a passing case, systematically narrow the
difference. Binary search the change space. This is the logic behind
`git bisect` and is the most efficient approach when the problem is "it used to
work."
### Rubber Duck (use within Understand)
When stuck, explain the system step by step in writing. The act of articulating
forces you to confront gaps in your understanding. Your session memory notes
serve this purpose — writing them IS the rubber duck process.
## What You Are NOT
- You are NOT a brainstorming agent. Don't generate loose ideas — investigate.
- You are NOT an implementation agent. Don't write production code.
- You are NOT a planning agent. Don't create detailed project plans.
You are a detective. You gather evidence, form hypotheses, test them, and report
findings. Then you hand off to whoever acts on those findings.

View File

@ -0,0 +1,854 @@
# Agent Infrastructure
Shared agent infrastructure for VS Code Copilot and OpenCode — brainstorm
agent, research agent, nudge instructions, hooks, skills, and MCP server.
Project-specific overlays live in each project's `.agents/` directory.
> **See also:**
> [`docs/research/ai-coding-best-practices.md`](../research/ai-coding-best-practices.md)
> — research synthesis covering the Prompt/Context/Harness taxonomy, failure
> modes, enforcement hierarchy, small-model harness patterns, and all
> primary-source citations that underpin the design decisions here.
## Current State
### Architecture Overview
The infrastructure is **tool-agnostic**: canonical sources live in `.agents/`
and a generator (`npm run generate:agents`) distributes them to
`.github/agents/`, `.github/skills/`, `.opencode/agents/`, `.opencode/skills/`.
Edit the `.agents/` sources; never edit the generated output directories (they
are `.gitignore`d and blocked by pre-tool-use policy).
```
.agents/
├── AGENTS.md # Root design doc + enforcement hierarchy
├── agents/ # Agent definitions (canonical)
│ ├── brainstorm.md
│ ├── research.md
│ └── build-local.md # OmniCoder 9B via Ollama
├── hooks/ # Shared bash hooks (delegated by all harnesses)
│ ├── pre-tool-use.sh # Hard blocks (terminal cmds + file-path policies)
│ ├── post-tool-use.sh # Self-check counter + methodology reminders
│ ├── session-start.sh # Inject project state at session start
│ ├── user-prompt-submit.sh # Per-turn nudge detection + task capture
│ ├── pre-compact.sh # Export state before context summarization
│ └── stop.sh # Session-end verification
└── skills/
└── research/SKILL.md # Research methodology (any agent can load)
```
Generated output (do not edit — regenerated by `npm run generate:agents`):
- `.github/agents/` — VS Code Copilot agent files
- `.github/skills/` — VS Code Copilot skill files
- `.opencode/agents/` — OpenCode agent files
- `.opencode/skills/` — OpenCode skill files
Harness integration:
- **VS Code Copilot**: `.github/agent-support.json` — maps 4 hook events to the
shared bash scripts in `.agents/hooks/`
- **OpenCode**: `.opencode/plugins/agent-support.ts` — TypeScript plugin that
shells out to the same bash scripts
### Brainstorm Agent
- 4-phase workflow: Quick Frame → Diverge → Converge → Capture & Hand Off
- 6 techniques: Rapid Ideation, SCAMPER, Worst Possible Idea, How Might We,
Inversion/Pre-mortem, Constraint Flipping
- Counterbalances Opus 4.6 overthinking tendency
- Phase 2 includes "push past the obvious" nudge (Zhao et al. 2024: LLMs fall
short on originality, excel at elaboration — first ideas are "average")
- Phase 4 routes to `@research` for investigation, default agent for
implementation
- Creates exploration files at `docs/explorations/<name>.md` and session memory
notes
### Research Agent
- Two orientations that compose recursively:
- **Understand** (Grounded Theory): open coding → constant comparison → axial
coding → memo → saturation check
- **Diagnose** (Strong Inference + Satisficing): 5-factor triage gates between
satisficing (low risk) and full falsification (high risk)
- 5-factor triage: reversibility, blast radius, confidence, novelty, time cost
- Timing awareness: `time` prefix on unknown commands, session/repo memory for
baselines, timing feeds into triage decisions
- Investigation files at `docs/explorations/<name>.md`
- Techniques reference: Five Whys, Delta Debugging, Rubber Duck
- Delegates evidence-gathering to Explore subagent, keeps analytical thinking
local
### Nudge Instructions
- Brainstorm nudge: triggers on hesitation/overthinking language ('wait',
'actually', 'hmm', 'overcomplicating', etc.)
- Research nudge: triggers on debugging/investigation language ('why is this
broken', 'how does this work', 'root cause', etc.)
- Both are non-intrusive single-sentence suggestions, only fire once per topic
### Tool Mapping (Copilot ↔ OpenCode)
| Copilot | OpenCode equivalent |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `AGENTS.md` (root + nested) | `AGENTS.md` (root, native; nested via `instructions` glob in `opencode.json`) |
| `.github/agents/*.agent.md` | `.opencode/agents/*.md` (frontmatter: `description`, `mode`, `model`, `temperature`, `permission`) |
| `.github/skills/<name>/SKILL.md` | `.opencode/skills/<n>/SKILL.md` — also reads `.agents/skills/` and `.claude/skills/` |
| `.github/instructions/*.instructions.md` (`applyTo`) | No direct equivalent — fold into AGENTS.md stubs or `instructions` glob |
| `.github/hooks/*.sh` (JSON-configured shell) | `.opencode/plugins/*.ts` (TS modules, event-driven) — shells out via Bun's `$` |
| `runSubagent` / `Explore` agent | Built-in `general` and `explore` subagents; `@`-mention syntax |
| `vscode_askQuestions` | No equivalent — OpenCode uses agent's natural turn-taking |
OpenCode plugin event mapping:
| Copilot hook | OpenCode event |
| -------------- | ----------------------------------- |
| `SessionStart` | `session.created` |
| `PreToolUse` | `tool.execute.before` |
| `PostToolUse` | `tool.execute.after` |
| `PreCompact` | `experimental.session.compacting` |
| `Stop` | `session.idle` (closest equivalent) |
## Research Foundation
> For full research depth, citations, and failure-mode analysis, see
> [`docs/research/ai-coding-best-practices.md`](../research/ai-coding-best-practices.md).
> The list below records the specific papers and frameworks that shaped the
> design decisions in this project.
Methodologies and papers that informed the design:
- **Grounded Theory** (Glaser & Strauss): build understanding from data, not
assumptions. Applied to code-reading in the Understand orientation.
- **Strong Inference** (Platt 1964): multiple competing hypotheses → crucial
experiments → eliminate. Applied to the Diagnose orientation.
- **Satisficing** (Simon 1956): accept "good enough" when optimization cost
exceeds benefit. Gates between cheap confirmation and expensive falsification.
- **Dual Process Theory** (Kahneman): System 1 (fast, pattern-matching) vs
System 2 (slow, analytical). System 1 more accurate in familiar domains.
Informs the triage decision.
- **Zhao et al. 2024** (arxiv): LLMs fall short on originality, excel at
elaboration. First ideas are "average." Informs brainstorm agent's "push past
the obvious" nudge.
- **"Lost in the Middle"** (Liu et al. 2023): LLMs attend best to beginning/end
of context. Informs hook design — inject at context tail for high attention.
- **Delta Debugging**: binary search the change space between passing/failing
cases. Logic behind `git bisect`.
- **Five Whys**: iterative causal chain tracing. Starting point for hypothesis
generation, not sole diagnostic method.
- **Ronacher "Agent Design Is Still Hard"**: reinforce methodology after every
tool call at context tail. Structural injection outperforms relying on
instructions in the system prompt.
- **Think-Anywhere** (Jiang et al. arXiv:2603.29957, Mar 2026, Peking U + Tongyi
Lab): LLMs trained to invoke `<think>` blocks at any token position during
code generation, not just upfront. SOTA on LeetCode/LiveCodeBench with fewer
total tokens. The motivating insight: a model can plan correctly at the start
but introduce an off-by-one bug mid-implementation — only mid-loop reasoning
catches it. **Applied here**: the research agent's investigation checklist
includes "Re-evaluate hypothesis at every tool-call boundary." For Claude 4
models, interleaved thinking makes this automatic. Complements Plan-and-Solve:
upfront decomposition where structure is clear, mid-execution re-evaluation
when intermediate results change what to do next.
- **Anthropic interleaved thinking** (Claude 4 + adaptive thinking): Claude
Sonnet 4.6+ and Opus 4.6+ automatically insert thinking blocks between tool
calls. No separate implementation needed — agent instruction design drives it.
The research agent's "Re-evaluate at every tool-call boundary" instruction
explicitly activates this behavior.
- **Prompt/Context/Harness framework** (Alibaba Cloud, Apr 2026): Names the
three engineering layers. Prompt = task expression (stateless). Context = what
the model sees (AGENTS.md, skills, tools — engineering target is progressive
disclosure). Harness = system constraints + verification loops (hooks,
permission gates, sub-agent isolation). Diagnostic map: wrong output → Prompt;
hallucinated fact → Context; wrong tool selected → Context (fix description);
task drift → Harness (sub-agent boundary); destructive action → Harness
(permission hook). LangChain improved Terminal Bench 2.0 from 52.8% → 66.5% by
changing Harness alone.
- **Context engineering** (Rajasekaran et al., Anthropic, Sep 2025): Formally
distinguishes context engineering from prompt engineering. Key principles: (a)
just-in-time context — agents hold references and load on demand, not upfront;
(b) structured note-taking (NOTES.md) as external working memory for long
sequential tasks; (c) every new token depletes attention budget — validates
the <60-line AGENTS.md ceiling; (d) compaction strategy: maximize recall
first, then improve precision.
## MCP Server Lifecycle Hooks — Protocol Status (May 2026)
The `.agents/mcp/` server exposes prompts and tools to agents via the MCP
protocol. A recurring question: can the MCP server react to session lifecycle
events (session start/end, tool-use boundaries)?
### Current protocol state
**No lifecycle hooks exist in the MCP protocol.** The spec defines three phases
only: `initialize → operation → shutdown`. There is no `session.created`,
`post-tool-call`, or `session.ended` notification. This gap is why session
awareness currently lives in the OpenCode plugin layer
(`.opencode/plugins/agent-support.ts`) rather than the MCP server — OpenCode
exposes `session.created`, `session.idle`, `session.compacted`,
`session.deleted`, and `tool.execute.before/after` events natively to plugins.
### Active work in the MCP spec
**SEP-2624: Interceptors for the Model Context Protocol**
([PR #2624](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2624))
The most organized effort. Supersedes SEP-1763 (closed as completed). Proposes
**Interceptors** as a new MCP primitive — two types: **validators** (inspect,
return pass/fail) and **mutators** (transform context payloads) — discoverable
and invocable via `interceptors/list` and `interceptor/invoke` JSON-RPC methods.
These fire at protocol-level operation events: `tools/call`, `prompts/get`,
`resources/read`, `sampling/createMessage`, `elicitation/create`. Not
session-start/stop hooks, but before/after wrapping for every operation.
There is now a formal **Interceptors Working Group** (Bloomberg + Saxo Bank
engineers, biweekly cadence). Reference implementations in progress for Go and
C# SDKs. Experimental repo:
[modelcontextprotocol/experimental-ext-interceptors](https://github.com/modelcontextprotocol/experimental-ext-interceptors).
Charter:
[modelcontextprotocol.io/community/interceptors/charter](https://modelcontextprotocol.io/community/interceptors/charter).
**SEP-2282: Server-Declared Behavioural Hooks**
([PR #2282](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2282))
Smaller, separate open PR. Proposes servers declare **context injections** in
`ServerCapabilities` — text injected into the agent's context at client-side
lifecycle events (session start, post-tool-use, session end). The contract is
"here's context the model should have at this moment," not code execution. More
directly analogous to our OpenCode `session.created` / `session.idle` patterns.
Currently unsponsored — needs a maintainer to pick it up.
### What to watch
- **Primary**:
[PR #2624](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2624) +
experimental-ext-interceptors repo
- **Secondary**:
[PR #2282](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2282)
(closest to session-lifecycle hooks)
- **Label filter**:
[`SEP` label](https://github.com/modelcontextprotocol/modelcontextprotocol/issues?q=label%3ASEP)
on the modelcontextprotocol repo
- **Milestone**: `2026-06-30-RC` is the next spec revision window
### Implication for this project
Until interceptors land in a shipping spec version and the TypeScript SDK, the
session lifecycle pattern stays at the OpenCode plugin layer. When SEP-2282 or
an equivalent lands, the MCP server could self-register context injection hooks
during `initialize`, removing the need for tool-specific plugin code.
---
## Model Scale Profiles
Different model sizes require different infrastructure strategies. The failure
modes are different, so the mitigations are different.
### Large-scale API models (Claude Sonnet / Opus)
**Primary failure modes**: overthinking, sycophancy, verbosity, tendency to add
unrequested features or comments.
**Infrastructure strategy**:
- Advisory methodology + structural reinforcement (hooks, circuit breakers)
- PostToolUse self-check nudges every ~15 calls
- PreToolUse hard blocks for high-risk operations
- Subagent delegation for isolated tasks (parent Opus → child Sonnet/Haiku)
### Smaller-scale local models (OmniCoder 9B via Ollama)
**Primary failure modes** (different from "low reasoning" — OmniCoder uses Qwen3
thinking blocks natively):
- Narrower training distribution (Python/JS heavy)
- Quantization degradation: JSON schema compliance drops as context fills
- Tool-call history is the primary context consumer — responses must be
truncated aggressively
- Instruction drift: fewer attention heads (32 vs 64 in 32B) means system prompt
recall degrades faster
**Infrastructure strategy**:
- PostToolUse response truncation at ~1500 tokens (plugin layer, not bash hook)
- PreToolUse JSON validation with schema-specific error messages
- Context pressure injection at ≥70% fill (~22K/32K tokens)
- `steps: 20` cap + `ask` permission gates for natural checkpoints
- `explore` subagent delegation to reduce context pressure on the main agent
- `NOTES.md` working memory pattern enforced in agent body
- No `web` tool — keeps context lean
- Reasoning guidance: "Hold references; load on demand" explicit in agent body
---
## OmniCoder 2 Orchestration — Pending Work
> Full historical rationale and audit findings were maintained in
> `docs/projects/local-ai-orchestration.md` (deleted May 2026 after merge). The
> plan used an orchestrator-workers pattern with structural `edit: deny`
> enforcement on the orchestrator. All OpenCode config values verified against
> opencode.ai/docs (May 2026).
### Goals
1. All agents run on `ollama/arch-omni2-9b` — no cloud fallback
2. User can type vague prompts; the system decomposes and delegates
automatically
3. Context windows are isolated per subagent (no shared state bleed)
4. Changes scale forward: switching to cloud means changing model strings, not
architecture
### Pending Changes
#### Quick wins — under 5 minutes each, no testing required
1. - [x] **[CRITICAL] Fix `<tool\*call>` typo in `omnicoder2.modelfile`** —
markdown-escape artifact; malformed opening tag paired with correct
closing tag. Highest-leverage change; everything below depends on
reliable tool-call JSON.
2. - [x] **Mark canonical/deprecated modelfiles**`# CANONICAL` header on
`omnicoder2.modelfile`; `# DEPRECATED` on `omnicoder.modelfile`;
`omnicoder-v2.modelfile.template` deleted (was dead code — v2 now
served from HuggingFace path).
3. - [x] **Add `compaction.reserved: 3000` to `opencode.json`** — default 10,000
fires compaction too early given ~812K baseline context.
4. - [x] **Fix `pre-compact.sh` prettier call** — removes `npx prettier` which
violates pre-tool-use Policy 1 (self-violating policy).
5. - [x] **MCP server error handling** — wrap `server.connect(transport)` in
try/catch with stderr + `process.exit(1)`.
#### Short session — 1530 minutes each, bounded scope
6. - [x] **Fix `stop.sh` JSON escaping** — replace `sed`-based escaping with
`printf '%b' | node JSON.stringify` pattern used in every other hook.
7. - [x] **Per-session PostToolUse counter** — repo-scoped path
`/tmp/.opencode-tool-count-<repo-hash>` (derived from REPO_ROOT via
md5sum); prevents cross-repo contamination; session-start.sh resets it
at session begin.
8. - [x] **Shrink compaction prompt to ~120 words** (in
`.opencode/plugins/agent-support.ts`) — shorter instructions free
bandwidth for the 9B to actually summarize.
9. - [x] **Update `.agents/agents/build-local.md` for v2** — pagination 100 → 50
lines; rule 4 now says "recipient not dispatcher"; rule 7 scope-check
says "tell the user, do not self-decompose".
#### Depends on orchestrator being proven first
10. - [x] **Trim root `AGENTS.md` to ~60 lines** — reduced from 435 lines to 45
lines; all architecture rationale, code examples, quick task table,
and project context removed; cross-cutting rules and quality gate
preserved (May 2026).
11. - [x] **PostToolUse weighted counter** — reads (`read_file`, `grep`, `list`)
+0.25; writes/shell +1; keeps 15-call SELF-CHECK from firing
mid-investigation sweep. Depends on #7 (per-session counter) first.
**Implementation** (`.agents/hooks/post-tool-use.sh`): bash has no
float arithmetic — scale to integers: reads +1, writes/shell +4,
threshold 60 (equivalent to 15 effective write-units). Read-class
tools: `read_file`, `grep_search`, `list_dir`, `file_search`,
`semantic_search`, `explore_subagent`. Write/shell-class: all
`*_string_in_file`, `create_file`, `run_in_terminal`. Replace the
single `COUNT=$((COUNT + 1))` with a `case "$TOOL_NAME"` block that
does `COUNT=$((COUNT + 1))` for reads and `COUNT=$((COUNT + 4))` for
writes/shell. Change the self-check condition from
`(( COUNT % 15 == 0 ))` to `(( COUNT % 60 == 0 ))`.
12. - [x] **PostToolUse reminder priority filter** — emit at most 2 reminders
per tool call; priority: SELF-CHECK > DEBUGGING > path-scoped >
tool-specific. Depends on #11.
**Implementation** (`.agents/hooks/post-tool-use.sh`): replace the
current single `context` string accumulator with an indexed array
`reminders=()`. Each block appends `reminders+=("$msg")` in priority
order (SELF-CHECK first, DEBUGGING second, BFF/QUALITY GATE third,
RENAME fourth). At output time: join only the first 2 elements.
Append with `\n\n` separator. Blocks that didn't fire don't append,
so the cap is natural.
13. - [x] **Broaden PostToolUse truncation to all `ollama/` agents**
(`.opencode/plugins/agent-support.ts`); differentiate limit:
orchestrator 2,500 tokens vs workers 1,500. Minor until orchestrator
exists.
**Implementation**: rename `BUILD_LOCAL_MAX_RESPONSE_TOKENS`
`LOCAL_WORKER_MAX_TOKENS = 1500`; add
`LOCAL_ORCHESTRATOR_MAX_TOKENS = 2500`. In `tool.execute.after`, the
existing `isLocalAgent` check covers all `ollama/` agents via
`input.model.startsWith('ollama/')`. Add a second check:
`input.agent === 'local-orchestrator'` → use orchestrator limit, else
worker limit. The `agent` field is available in `tool.execute.after`
(confirmed working for `build-local`).
14. - [x] **Create `.agents/agents/local-orchestrator.md`** — primary agent with
`edit: deny`, `write: deny`, `bash: deny`; whitelist `task` to
`build-local`, `research`, `brainstorm` only.
**Implementation**: new file modeled on `build-local.md`. Role: receive
high-level goal, decompose into bounded subtasks, show decomposition to
user before dispatching, delegate via `task` subagent. Permission
block in `opencode.json` `agent.local-orchestrator`:
`{ "edit": "deny", "write": "deny", "bash": "deny" }`. Agent body
rules: (1) read project root `AGENTS.md` first; (2) produce a task
list and confirm with user before dispatching; (3) one `task` call per
subtask, wait for result; (4) never attempt to edit files directly —
if a subtask requires context the worker needs, inject it via the
`task` prompt, not by reading files yourself; (5) after all subtasks,
report summary to user.
15. - [x] ~~**Set `default_agent: "local-orchestrator"` in `opencode.json`**~~
Done May 2026. Key is `default_agent` (snake_case, confirmed from
`opencode.ai/config.json` schema). `local-orchestrator` has
`mode: all` so it qualifies as a primary agent.
#### Done
- [x] ~~**Soften `opus-deep.modelfile` directive**~~ — file deleted (May 2026);
DeepSeek R1 available online when needed; OmniCoder 2 is the sole local
model.
### Known Tradeoffs
| Tradeoff | Impact | Mitigation |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Instructions glob trimmed to root `AGENTS.md` only | Agents miss project-specific patterns for subdirectories unless they read nested `AGENTS.md` explicitly | Add reminder in orchestrator + build-local agent body: "check nested `AGENTS.md` before working in subdirectories" |
| Same model for all roles | Orchestrator, worker, compaction agent are all same weights with different prompts | Structural `edit: deny` is the safety net; circuit breakers limit runaway loops |
| No cloud fallback | If task is too complex for 9B, no escalation path | Orchestrator includes "ask the user for direction" rule; user can switch to Copilot |
| Latency | Sequential dispatch: orchestrator decomposes → build-local runs → returns. ~2× wall time vs. direct build-local | Acceptable for local dev; no VRAM multiplier since Ollama keeps weights hot |
| Reminder-stacking cap | 2-per-call priority filter (pending work above) drops lower-priority warnings | Skipped reminders fire on next call if condition holds |
### Cloud Migration Path
When ready to add a cloud model, only `opencode.json` changes:
```json
{
"model": "ollama/arch-omni2-9b",
"agent": {
"local-orchestrator": {
"model": "anthropic/claude-haiku-4-5"
}
}
}
```
Schema verified against opencode.ai/docs/agents/ (May 2026). The `tools` key
inside agent configs is deprecated in favour of `permission` — the orchestrator
definition uses `permission`, so it is current. The `agent.{name}.model` key is
the correct per-agent override mechanism.
---
## Ecosystem Gap — Contextual AGENTS.md Injection
During local AI work (May 2026) we hit a fundamental limitation: OpenCode's
`instructions` glob in `opencode.json` loads **all matched files upfront** into
every session. For a 9B local model with a 32K context window, loading all of
`apps/*/AGENTS.md` and `packages/*/AGENTS.md` at startup consumes ~3040% of the
context budget before the first message, triggering early compaction and
degrading quality.
The correct behaviour — injecting only the AGENTS.md relevant to the file being
edited — does not exist natively in OpenCode or its plugin ecosystem. The
closest community plugin (`opencode-skillful`, 295 stars) is archived as of Feb
2026 and still requires the model to explicitly call `skill_find`/`skill_use`;
it provides no path-triggered structural injection.
### Open tasks
16. - [ ] **Assess: is filling this ecosystem gap worth the effort?** — Before
building a contextual-injection plugin, evaluate: (a) Is OpenCode
actively used for serious local AI coding work, or is the community
primarily cloud-model users for whom context cost is irrelevant? (b)
Are there better local AI coding stacks (e.g. Aider + litellm, Cursor
local mode, VS Code Copilot + Ollama) where this problem is already
solved? (c) Is the `tool.execute.before` event stable enough to build
on? Target: 30-minute research session, concrete go/no-go
recommendation.
17. - [ ] **Review + write up our issues and fixes as an ecosystem
contribution** — If the gap is worth filling: document the
context-bleed problem, the early-compaction root cause, our hook-based
mitigation, and the remaining structural gap. Publish as a GitHub
issue on the OpenCode repo and/or an npm plugin
(`opencode-contextual-rules`?) implementing `tool.execute.before`
path-triggered AGENTS.md injection. Depends on #16 go/no-go.
18. - [x] ~~**Trim `.agents/AGENTS.md`**~~ — Done May 2026. Condensed from
12,584 → 10,507 bytes (43 lines removed). Trimmed: Hook Architecture
Principle block (redirected to item 22 in project doc), Deferred
Loading example + "why not" paragraph, session-start/stop hook prose,
outdated `generate-agents.ts` references in Skills/Agents sections.
Agent body files updated to prompt-body-only convention (see items
25/26).
19. - [x] ~~**Block bash bypass of read pagination**~~ — Done May 2026. Added
Policy 14 to `pre-tool-use.sh`: blocks `cat`/`head`/`tail`/`jq` reads
of `apps/*/package.json` and `packages/*/package.json`. Scope limited
to package.json (confirmed live bypass vector); general `.ts`/`.md`
bash reads are not yet blocked (lower-urgency gap). Pattern verified
with Node.js unit test — exact bypass command
`cat apps/api/package.json | jq` is caught by P1.
20. - [ ] **Improve explore-first scope detection** — Policy 14 blocks
`manage_todo_list` with ≥4 items, but OmniCoder sometimes starts with
`Explore`/`find` before planning, bypassing the check. Options: (a)
block `explore_subagent` when the query looks like a multi-file
discovery sweep (glob patterns for source files across multiple dirs);
(b) add a pre-tool-use check on `run_in_terminal` that denies `find`
commands spanning the whole repo when the task hasn't been scoped yet;
(c) rely on the todo-list check firing when planning eventually
happens (current behavior — catches it late but still before edits
start).
21. - [x] ~~**Remove debug logging from plugin after verified cycle**~~ — Done
May 2026. Removed the full-input dump block from `tool.execute.before`
in `plugin.ts` (`/tmp/plugin-debug.jsonl` appender). Guards verified
via `opencode export` session transcript inspection — no longer need
the dump file. Hook error logger (`/tmp/plugin-hook-errors.log`) kept
as it only fires on failures, not every call.
22. - [ ] **Refactor hook scripts to be platform-agnostic** — currently
`pre-tool-use.sh` parses Copilot-specific JSON and outputs
Copilot-specific `permissionDecision` JSON. `plugin.ts` implements
duplicate guards inline rather than calling the script. This means
OpenCode and Copilot guards can drift (confirmed May 2026: Policy 14
in `pre-tool-use.sh` had no effect on OpenCode `bash` tool calls).
**Design target**: scripts accept normalized env vars (`TOOL_NAME`,
`COMMAND`, `FILE_PATH`), exit non-zero with plain-text denial reason
on stdout. Callers normalize input and translate output to their
native denial format. Tracked in `.agents/AGENTS.md` Hook Architecture
Principle section.
**Audit required first**: review all hook scripts for Copilot-specific
assumptions before refactoring.
23. - [ ] **Question-drift marker in `user-prompt-submit.sh`** — when the model
has committed to a prior position and follow-up questions are being
misread through that lens, prepend a disambiguation marker at the
prompt tail. Detected pattern: model answers "no" or "not possible" in
a prior turn → subsequent turns interpreted as defense of that
position. See §2.1 ("Position-anchored priming") in the research doc.
**Implementation**: in `user-prompt-submit.sh`, read the last N turns
of `$TRANSCRIPT_PATH` (injected by OpenCode's native hook env) and
look for a prior committed "no/impossible/can't" response within the
last 3 model turns. If detected, append to `ADDITIONAL_CONTEXT`:
`CURRENT QUESTION (answer only this — not the prior exchange): [prompt
text]`. The key is repeating the user's exact question at the tail,
after the marker, to counteract lost-in-the-middle effects. Fallback
trigger: user prompt contains "that's not what I asked" / "you're
answering the wrong question" / "I said" → always inject marker
regardless of transcript scan.
24. - [x] ~~**Review all custom agent files for local-model-specific framing**~~
— Done May 2026. `build-local.md` reframed: dropped "OmniCoder", "9B",
"Ollama", "Qwen3 thinking blocks", "32K tokens total"; replaced with
model-agnostic equivalents. `research.md` and `brainstorm.md` verified
clean — no model/provider mentions. `local-orchestrator.md` was fixed
earlier this session. All four agent body files are now
model-agnostic.
25. - [ ] **Failure-mode routing in SELF-CHECK** — when the periodic SELF-CHECK
fires in `post-tool-use.sh`, if a recent terminal failure or test
failure is also present in the same turn, classify the failure type
and inject the matched intervention rather than generic "step back."
Reference: failure-mode routing table in §3.5 of the research doc.
**Implementation**: in the SELF-CHECK block, if `context` already
contains `DEBUGGING REMINDER` (i.e., test/terminal failure co-occurred
this turn), append a classification hint:
`FAILURE TYPE HINT: If this is a test/build failure → Reflexion loop
(fix based on test output). If convention violation → grep for the
pattern and inject a canonical example. If wrong file/directory → stop
and re-read the project structure. Do not default to "try harder."`.
Low implementation cost — pure text append with a conditional on
`$context`.
26. - [x] ~~**Audit agent `.md` files for OpenCode-specific frontmatter**~~
Done May 2026. Audit result: only `local-orchestrator.md` had OpenCode
frontmatter keys (`mode`, `model`, `permission`). `brainstorm.md`,
`build-local.md`, `research.md` were already plain markdown. Went with
option (b): stripped `mode`/`model`/`permission` from
`local-orchestrator.md`; moved `mode: all` into `opencode.json`
(model + permission were already there). Kept `description` in
frontmatter as it is neutral and self-documenting. Body files are now
prompt-body only — valid in both OpenCode and Copilot.
27. - [ ] **`plugin.ts` local-agent detection uses provider prefix, not agent
name** — `tool.execute.after` detects local agents via
`input.model.startsWith('ollama/')`. This is provider-specific: if the
model is served via a different backend (e.g. `llama-server/`,
`lmstudio/`), truncation silently stops working. Fix: detect by agent
name (`input.agent.includes('build-local')`) only, removing the
`ollama/` fallback. The `input.agent` field is available in
`tool.execute.after` (confirmed May 2026).
28. - [ ] **`plugin.ts` context pressure threshold is hardcoded to 32,768
tokens** — `CONTEXT_LIMIT_TOKENS = 32768` assumes OmniCoder 9B's
context window. If the local model changes, the threshold silently
drifts out of calibration. Options: (a) read from `opencode.json`
model config if OpenCode exposes it to plugins; (b) make it a
top-of-file constant with a comment to update when changing models;
(c) accept the drift as low-severity (threshold is advisory only —
context pressure warnings are informational, not blocking). Option (b)
is the minimum; option (a) is ideal if OpenCode exposes model metadata
to plugins.
29. - [x] ~~**Move `permission` out of `local-orchestrator.md` frontmatter**~~
Done May 2026 as part of item 25. `mode: all` added to `opencode.json`
agent entry. `model` and `permission` were already in `opencode.json`.
`opencode.json` is now the single source of truth for all runtime
config; `.md` files are prompt-body only.
---
## Testing & Regression
**Research summary (May 2026):** No pre-existing tool exactly fits this use
case. Existing tools (RagaAI Catalyst, AgentEvalKit, agent-eval-arena,
intent-eval-lab, j-rig-skill-binary-eval) focus on LLM output quality,
hallucination detection, or cross-runtime behavior scoring — not config file
structure or policy enforcement regression. The closest analogue is
`j-rig-skill-binary-eval` (binary pass/fail criteria across 7 layers), which
uses the same conceptual approach we'd want here. Our testing is bespoke by
necessity: we're testing configuration files, shell scripts, and specific policy
enforcement behaviors, not general LLM response quality.
**Two layers of testing:**
| Layer | What it tests | Cost | When to run |
| --------------------------- | --------------------------------------- | ---------------- | -------------------------------------- |
| Config + policy unit tests | Schema validity, hook regex correctness | None (no model) | Always — CI, pre-commit |
| CLI integration smoke tests | Actual enforcement via `opencode run` | Local model only | On-demand; local model must be running |
**Cloud agents excluded from integration tests** — `opencode run` with a cloud
model (Copilot, Anthropic) incurs API costs and rate limits. Tests must detect
the active model and skip if it's not a local provider.
### Open tasks
30. - [ ] **Config + policy unit test suite** — test config file structure and
hook regex patterns without invoking any model. Implementation:
a. **`opencode.json` schema validation**: the file references
`"$schema": "https://opencode.ai/config.json"` — validate it using
`ajv` (already used in the monorepo) against the live schema or a
cached copy. Catches permission typos, unknown agent keys,
unsupported field values.
b. **Hook JSON structure validation**: validate
`.agents/frameworks/github/hooks.json` and
`.agents/frameworks/opencode/plugin.ts` (TypeScript, already type-
checked). Write a schema for the hooks JSON format and run ajv on
it.
c. **Hook policy regex unit tests**: extract every regex used in
`pre-tool-use.sh` into a `tests/hooks.test.ts` file and run it
with `vitest`. For each policy, define 23 input strings that
SHOULD match and 23 that SHOULD NOT. Policy 14 already has an
informal Node.js test from this session — formalize it.
d. **Agent `.md` frontmatter validator**: check that no agent file
under `.agents/agents/` has frontmatter keys other than
`description`. Catches regression when someone adds `model:` or
`permission:` back to a body file.
**Suggested location**: `.agents/tests/` or root `test/agents/`.
**Stack**: vitest (already in monorepo), ajv (already available), Node
built-ins. No new dependencies needed.
31. - [ ] **CLI integration smoke tests (local model only)** — use
`opencode run` in non-interactive mode to verify enforcement is
actually firing via the real runtime. These tests exercise the
plugin + hook wiring end-to-end.
**Command shape**:
```
opencode run "prompt" --agent build-local \
--model llama-server/arch-omni2-9b-native \
--format json
```
**Assertions via `opencode export`**: after each run, export the
session with `opencode export <sessionID> 2>/dev/null` and parse the
JSON transcript. Assert on `parts` array: tool calls that SHOULD have
been blocked appear with error/denied status; tool calls that SHOULD
have passed completed normally.
**Test cases to start with** (all verified real enforcement gaps):
1. Attempt to `read` a nested `package.json` (e.g. `apps/api/package.json`) → BLOCKED by plugin
package.json guard
2. Attempt to `read` a source file with no `limit` → BLOCKED by
pagination guard
3. Attempt to `read` a source file with `limit: 51` → BLOCKED
4. Attempt to `read` a docs file with `limit: 501` → BLOCKED
5. Attempt to `read` a docs file with `limit: 50` → PASSES
6. Bash command `cat apps/api/package.json` → BLOCKED by pre-tool-use
Policy 14 (substitute your project's equivalent nested package.json)
**Guard rail**: skip all tests if `llama-server` is not reachable at
`http://127.0.0.1:8080/v1`. Do not run against cloud models. Add
an env var `AGENT_INTEGRATION_TESTS=1` required to enable (off by
default, never runs in standard `npm test`).
**Suggested location**: `.agents/tests/integration/`.
**Stack**: Node.js test runner or vitest, `opencode` CLI in PATH.
### Verified facts (May 2026)
- OpenCode's `read` tool input schema is
`{ filePath: string, limit?: number, offset?: number }` — NOT
`startLine`/`endLine`. Confirmed via plugin debug logging of real tool calls.
- `tool.execute.before` input contains only `{ tool, sessionID, callID }`. It
does NOT include `agent` or `model`, so plugin-layer gating cannot filter by
agent. Confirmed via plugin debug logging.
- **OpenCode has its own native hook system** that calls `pre-tool-use.sh`
directly for tools like `run_in_terminal`, `replace_string_in_file`, etc. This
is completely separate from the plugin's `runHook` calls. The native hook
payload includes `timestamp`, `hook_event_name`, `session_id`,
`transcript_path`, `tool_use_id`, and `cwd` — fields the plugin never sends.
The plugin `runHook` is a _second_ call, layered on top.
- **Bun shell `$` API does not have a `.stdin()` method.** The correct API for
piping stdin is `` $`cmd < ${Buffer.from(text)}` ``. `.stdin(text)` silently
throws `TypeError: $\`...\`.stdin is not a
function`, which was caught by `runHook`'s `catch`block and returned`''`. This caused the plugin's `runHook`to silently no-op for every call with`stdinJson`since the plugin was first written — hook enforcement (all 12 policies) was never running via the plugin path. It only ran via OpenCode's native hook system for the tools OpenCode natively supports. Confirmed via`/tmp/plugin-hook-errors.log`.
- **The silent `catch` in `runHook` is dangerous.** It masked the Bun `.stdin()`
bug entirely. Always log hook failures to a debug file during development;
remove only after enforcement is verified working.
- **Plugin-layer enforcement works for `read`** after fixing the Bun stdin API.
The `read` tool fires `tool.execute.before` in the plugin, which calls
`runHook('pre-tool-use.sh', ...)` via `< ${Buffer.from(...)}`, which applies
Policy 13 (50-line limit). Verified: bare `read` (no limit) → BLOCKED; `read`
with `limit: 50` → passes. (May 2026)
- **Plugin load failure: unescaped regex slashes caused silent syntax error.**
`plugin-debug.jsonl` was empty even after the Bun stdin fix because the plugin
file itself failed to parse. Line 84 had `/(^|/)(apps|packages)/[^/]+/...`
forward slashes inside the regex literal were not escaped, producing a JS
syntax error at parse time. Bun silently drops plugins that fail to import.
Fixed to `/(^|\/)(apps|packages)\/[^/]+\/...`. The fix also corrected the
pagination guard to use `limit`/`offset` (not `startLine`/`endLine`) and added
an unbounded-read block (`limit === undefined`). All three guards verified
working in a live session (May 2026).
- **Package.json read guard verified working.** `local-orchestrator` attempting
to read `apps/*/package.json` and `packages/*/package.json` → BLOCKED by
plugin. Root `package.json` read correctly passes. (May 2026)
- **Policy 14 (`manage_todo_list` ≥ 4 items) catches some but not all broad task
attempts.** OmniCoder sometimes proceeds directly to `Explore`/`find` without
calling `manage_todo_list` first, bypassing the policy. When it does plan with
the todo tool before acting, the deny fires correctly.
- **OmniCoder comprehension failure: prompt ambiguity → wrong directory.** Given
"refactor the five hook files", OmniCoder ran a glob for `*hook*` files and
found `.husky/` hooks instead of `.agents/hooks/`. The correct files were in
the grep output from the Explore subagent but were not selected. Root cause:
the model lacks enough context about the repo layout to disambiguate "hook
files" without explicit path guidance. Mitigation: be explicit in prompts
("the five `.agents/hooks/*.sh` files").
- **OpenCode agent `permission` config requires a `.opencode/agents/<name>.md`
file.** Without a matching markdown file, `opencode.json`'s
`agent.<name>.permission` config is silently ignored — the agent is unknown to
OpenCode and runs as a nameless build-agent alias. The markdown file must
exist in `.opencode/agents/` (or `~/.config/opencode/agents/`). Confirmed by
test run where `@local-orchestrator` edited files despite
`permission.edit: "deny"` in JSON config; fixed by creating
`.opencode/agents/local-orchestrator.md` symlink. (May 2026)
- **`"write"` is NOT a valid OpenCode permission key.** Use `"edit"` instead —
it covers `write`, `edit`, and `apply_patch` tools. `"write": "deny"` is
silently ignored. Valid top-level permission keys include: `read`, `edit`,
`glob`, `grep`, `list`, `bash`, `task`, `skill`, `lsp`, `question`,
`webfetch`, `websearch`, `external_directory`, `doom_loop`, `todowrite`.
Confirmed from `opencode.ai/docs/permissions` (May 2026).
- **`default_agent` key is snake_case** in `opencode.json` (not `defaultAgent`).
Confirmed from `opencode.ai/docs/config` (May 2026).
- **`tools: false` is deprecated.** The current approach for per-agent tool
restriction is `permission: { edit: "deny" }`. The old `tools: false` still
works but is documented as legacy. Confirmed from `opencode.ai/docs/agents`
(May 2026).
- **Broken symlinks are silent.** OpenCode does not error on a broken
`.opencode/agents/` symlink — it just skips the agent silently. The agent
won't appear in `opencode agent list` and all `opencode.json` permission
config for it is ignored. Always verify with
`cat .opencode/agents/<name>.md | head -5` (should print content, not a "No
such file" error) and `opencode agent list` (agent should appear with correct
deny rules). The correct symlink depth from `.opencode/agents/` is
`../../.agents/agents/<name>.md` (two levels), not three.
- **`opencode agent list` is the authoritative verification command.** Run it
after any agent config change to confirm: (a) the agent appears by name, (b)
its mode is correct (`all`/`primary`/`subagent`), and (c) `deny` rules appear
at the bottom of its permission list. Missing agent = broken symlink or YAML
parse error. Present but missing deny rules = frontmatter not parsed correctly
or wrong key names. (May 2026)
- **`@mention` routing only works at session start.** If you send any message
that gets answered by the current primary agent first, then send
`@local-orchestrator ...`, the TUI passes the full message text to the current
model (Build/OmniCoder) which treats `@local-orchestrator` as freeform text
and answers it itself. Always open a **fresh session** and make `@agent-name`
the very first message. Alternatively, use
`opencode run --agent local-orchestrator "..."` from the CLI for reliable
agent-scoped invocation. **Tab-switching to a custom `all`-mode agent in an
existing session works correctly.**
- **`edit: deny` on `local-orchestrator` is working correctly.** When given an
edit task, the orchestrator correctly avoided using `replace_string_in_file`
and instead used the `task` tool to delegate to a subagent. This is the
expected behaviour. Confirmed May 2026.
- **`task` tool has a JSON serialization limit.** OmniCoder 9B caused an
`Unterminated string` error by embedding the entire contents of multiple
`package.json` files as a literal string inside the `task` prompt JSON. The
`task` tool prompt is serialized as JSON; very long strings truncate and
produce parse errors. Mitigation: instruct the orchestrator in its system
prompt to tell workers _which files to read_ rather than quoting file contents
inline. This has been added to `local-orchestrator.md`. (May 2026)
- **`ollama/arch-omni2-9b` is the wrong model identifier for the llama-server
instance.** The correct ID is `llama-server/arch-omni2-9b-native` (verify with
`opencode models | grep arch`). Using the wrong ID causes an immediate "cannot
load model" error when the agent is invoked. Fixed in `opencode.json` and
`local-orchestrator.md` frontmatter. (May 2026)
## Open Issues
Known bugs and stale claims identified during code review (see deleted
`agent-infrastructure-review.md` and `agent-infrastructure-review-pass2.md` for
full context). Not yet fixed.
### CRITICAL — `description:` empty in all generated agent/skill files
`scripts/generate-agents.ts` uses a hand-rolled YAML parser that silently drops
descriptions when they are written in block-scalar form (value on the next line
under the key). Every generated file in `.github/agents/`, `.github/skills/`,
`.opencode/agents/`, `.opencode/skills/` has a blank `description:` field.
`description:` is the primary routing signal for Copilot's
`SkillsContextComputer` and OpenCode's agent dispatch. Explicitly `@`-mentioning
an agent by name still works; description-triggered auto-routing does not.
**Fix**: Inline the description strings in the canonical `.agents/` source files
(change block-scalar to `key: 'value'` format). The existing parser handles
inline strings correctly. Add a `generate:agents:check` assertion that every
generated file has a non-empty `description:`.
### MEDIUM — ~~`printf '%s'` regression in hooks breaks `\n` rendering~~ (resolved)
~~`.agents/hooks/post-tool-use.sh`, `session-start.sh`, and
`user-prompt-submit.sh` use `printf '%s' "$context" | node -e '...'` to
JSON-escape the context variable. `%s` does not interpret `\n` escape sequences,
so multi-line context strings (SELF-CHECK, DEBUGGING REMINDER, BFF REMINDER)
arrive at the model as single lines with literal `\n` characters.~~
**Verified fixed** (May 2026): all three hooks already use `printf '%b'`.
### LOW — ~~arXiv citation `2603.29957` unverified~~ (resolved)
~~`arXiv:2603.29957` (Jiang et al. 2026, "Think-Anywhere") appears in
`.agents/agents/research.md`, `.agents/agents/brainstorm.md`, and the Research
Foundation section above. Verify the ID resolves at
`https://arxiv.org/abs/2603.29957` and fix all references if it doesn't.~~
**Verified real** (May 2026): "Think Anywhere in Code Generation" by Xue Jiang,
Tianyu Zhang, Ge Li et al., submitted March 31, 2026, revised April 27, 2026
(v3), cs.SE. All existing citations are correct.
### LOW — ~~`.claude/` false claims in `tool-agnostic-agent-infra.md`~~ (resolved)
The file `docs/projects/tool-agnostic-agent-infra.md` no longer exists — already
deleted. No action needed.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,162 @@
> What are the different AI model architectures? ... What are the experimental
> ones? ... Any more? ... I want you to combine all of those into a small
> bulleted list
---
## Sequential & Vision Foundations
- RNN, LSTM, GRU: Sequential networks processing text and audio through internal
memory loops.
- CNN: Spatial processing architectures using mathematical filters to parse
visual grids.
- Vision Transformers (ViT): Vision models using self-attention directly on
split image patches.
## Modern Generative Engines
- Transformers: High-capacity architectures utilizing global self-attention to
process text sequences.
- GANs: Paired networks staging a generator against a discriminator to create
imagery.
- Diffusion Models: Generative systems producing content by systematically
reversing data noise.
- VAEs: Systems compressing data into lower dimensions to decode structural
variations.
## Efficiency & Structure Scaling
- Mixture of Experts (MoE): Sparse networks activating targeted sub-expert nodes
to optimize computation.
- Graph Neural Networks (GNN): Frameworks natively executing message-passing
over complex non-grid relationships.
- Large Action Models (LAM): Automation architectures translating semantic
intents into executable digital workflows.
- Attention-SSM Hybrids: Layered blocks combining global attention processing
with fast linear scaling.
## Next-Gen Context & Linear Processing
- Mamba & SSMs: Hardware-aware state models enabling infinite context lengths
with linear scaling.
- Liquid Neural Networks (LNN): Dynamic architectures utilizing fluid
differential equations for real-time adaptation.
- Diffusion Language Models (DLM): Non-sequential text systems iteratively
refining whole content blocks.
- Joint Embedding Predictive (JEPA): Meaning-driven architectures predicting
high-level conceptual outcomes over raw pixels.
## Dynamic Reasoning & Alternative Physics
- Test-Time Training (TTT / MoR): Flexible loops scaling internal computing
depth dynamically per query.
- Distributed Neural Graphs: Modular webs routing separate context tokens
through specialized pathways.
- Neuro-Symbolic Hybrids: Integrated systems merging raw neural intuition with
precise sandbox compilation.
- Spiking Neural Networks (SNN): Bio-mimetic hardware structures firing nodes
strictly via threshold spikes.
- Thermodynamic Models: Analog physical systems computing answers by settling
into low-energy equilibriums.
---
> Which of these require the least power consumption, least training data, least
> compute, and least VRAM
---
To identify which architecture requires the absolute minimum across all four
constraints, the category of Alternative Physics & Bio-mimetic Architectures
wins outright, followed immediately by classical spatial networks. The absolute
winners for each specific bottleneck are broken down below:
## 1. Least Power Consumption: Spiking Neural Networks (SNNs)
- The Winner: Spiking Neural Networks (SNNs).
- Why: Traditional AI models are mathematically "dense," meaning billions of
multipliers and transistors must turn on and consume electricity for every
single token processed. SNNs mimic biological neurons and operate using sparse
electrical spikes.
- The Difference: If there is no new incoming data, energy consumption drops to
near zero. Because nodes only fire when an electrical threshold is crossed,
SNNs use up to 100× to 1000× less electrical energy than traditional deep
learning networks, making them the gold standard for low-power edge hardware.
[1]
## 2. Least Training Data: Neuro-Symbolic Hybrids & JEPAs
- The Winner: Neuro-Symbolic Hybrids (closely followed by Joint Embedding
Predictive Architectures / JEPA).
- Why: Transformers require hundreds of billions of text tokens to accidentally
stumble upon and learn basic mathematical rules (like addition).
Neuro-Symbolic systems hardcode a classical, rigid rules engine directly into
the AI's core.
- The Difference: Instead of needing to see 10,000 examples of a math problem to
recognize a pattern, a Neuro-Symbolic model needs exactly one prompt because
it instantly routes the logic out to a pre-programmed mathematical compiler.
## 3. Least Compute (FLOPs): Convolutional Neural Networks (CNNs)
- The Winner: Convolutional Neural Networks (CNNs).
- Why: Modern Vision Transformers (ViTs) compare every single pixel patch
against every other pixel patch globally, creating an exploding computation
bottleneck. CNNs strictly look at localized, overlapping grids using tiny math
matrices (kernels).
- The Difference: For simple classification and computer vision tasks (like
recognizing a stop sign), a highly optimized CNN calculates a mere fraction of
the floating-point operations (FLOPs) required by an attention-based Vision
Transformer.
## 4. Least VRAM: Mamba / State Space Models (SSMs)
- The Winner: Mamba & State Space Models (SSMs).
- Why: In traditional Transformers, VRAM usage scales quadratically with length
because the model must store a massive, continuously growing history of the
conversation (known as the KV-Cache) directly inside the GPU memory.
- The Difference: Mamba compresses conversational history into a fixed-size
mathematical "state." Whether you have been chatting with the model for 5
sentences or 50,000 sentences, the VRAM footprint remains entirely flat and
locked, drastically reducing hardware memory requirements during deployment.
[2, 3]
---
## Direct Comparison Overview
| Architecture [2, 4, 5] | Power Efficiency | Data Efficiency | Compute (FLOPs) Needed | VRAM Footprint | Best Used For |
| ---------------------- | ---------------- | --------------- | ---------------------- | -------------- | ---------------------------------- |
| SNNs | 🏆 Lowest | Moderate | Low | Low | Battery-powered edge devices |
| Neuro-Symbolic | Moderate | 🏆 Lowest | Low | Low | Exact math, logic, and coding |
| CNNs | Low | High | 🏆 Lowest | Low | Embedded image/object detection |
| Mamba / SSMs | Moderate | High | Low | 🏆 Lowest | Long-context chatbots on local PCs |
| Transformers | Extreme High | Extreme High | Extreme High | Extreme High | Frontier LLMs and heavy cloud AI |
---
If you want to move forward with a lightweight project, let me know:
- Do you want to see a Python sample of a lightweight CNN or Mamba block?
- Are you trying to deploy an AI model onto a specific microdevice (like a
Raspberry Pi or Arduino)?
[1] [https://arxiv.org](https://arxiv.org/html/2406.08413v1) [2]
[https://www.reddit.com](https://www.reddit.com/r/LocalLLaMA/comments/1qk1pzy/is_the_next_leap_in_ai_architectural_comparing/)
[3]
[https://virtualizationreview.com](https://virtualizationreview.com/articles/2026/01/27/what-gpu-do-you-really-need.aspx)
[4]
[https://medium.com](https://medium.com/@TDKVentures/breaking-the-compute-bottleneck-power-memory-and-the-new-ai-architecture-paradigms-eee264cec99a)
[5]
[https://www.socomec.us](https://www.socomec.us/en-us/solutions/business/data-centers/understanding-power-consumption-data-centers)
---
Further exploration of SSNs reveals they are the most efficient in all
categories but they require specialized hardware, like the Akida™ PCIe Board
AKD1000 (or you can use their cloud service pretty cheaply during a trial)
If your ultimate goal is to build or run something that achieves frontier-class
reasoning while staying highly hardware-efficient, you must look toward a Hybrid
SSM (Mamba) + Transformer + MoE architecture. This gives you the static VRAM
footprint and linear scaling of an alternative model, backed by the proven
intelligence of standard attention loops.

View File

@ -0,0 +1,405 @@
# Where Human and LLM Text Interpretation Overlap (and Don't)
> **Status:** Synthesis of
> [`text-communication-interpretation.md`](./text-communication-interpretation.md)
> (humans reading text) and
> [`llm-intent-interpretation.md`](./llm-intent-interpretation.md) (LLMs reading
> prompts). The question is: how much of what works on one carries over, and is
> there published evidence either way?
>
> **Working hypothesis (from the user, May 2026):** LLMs are trained on
> human-written text, so the cognitive shortcuts and biases that humans bring to
> text could be inherited by the models. This doc treats that as a hypothesis to
> test against the literature, not as an assumption.
>
> **Methodology:** Each candidate parallel is rated by what the literature says,
> not by intuition. Four labels are used:
>
> - **Cited connection** — at least one paper explicitly links the human and LLM
> phenomenon (often by name).
> - **Cited distinction** — a paper explicitly argues the analogy is misleading
> or the mechanism is different.
> - **Parallel without published bridge** — both phenomena are real and
> independently documented, but no source I found connects them. Use with
> care.
> - **Orphan** — exists in only one doc; no found counterpart.
---
## 1. The User's Hypothesis, Tested
> "Humans wrote the text LLMs are trained on, so human emotional/cognitive
> shortcuts could affect LLMs."
**Verdict: directly supported in the literature.** Mina et al. (COLING 2025) [1]
examine four classical cognitive biases — primacy, recency, common-token, and
majority-class — across base and instructed models of varying size, and
conclude:
> "Recent work has shown that these biases can percolate through training data
> and ultimately be learned by language models." [1]
The same paper distinguishes biases that arise from _pretraining data
distributions_ (e.g., common-token bias) from biases that arise from the
_autoregressive generation process itself_ (e.g., some forms of recency). So the
user's framing is correct, with one refinement: not every LLM bias is inherited
— some are mechanical, some are statistical, some are both.
Hartvigsen-line work (Steed et al. 2022; Touileb-line replications through 2024)
[9] independently confirms the inheritance pathway for sentiment and
social-stereotype biases: pretraining corpora (CC-100 vs. Wikipedia) carry
measurably different negative-sentiment distributions toward identity terms,
which propagate into both upstream embeddings and downstream toxicity
classifiers.
---
## 2. Cited Connections
These are points where the published literature names a human cognitive
phenomenon as the analog of an LLM behavior, with empirical work on both sides.
**Evidence-strength tags** (applied per subsection):
- **[multi-replicated]** — multiple independent studies, including at least one
peer-reviewed venue, finding the same effect.
- **[single-study + partial replication]** — primary finding peer-reviewed;
follow-ups exist but disagree on scope or magnitude.
- **[single-study]** — peer-reviewed but not yet independently replicated to my
knowledge.
- **[preprint-only]** — relevant findings exist only as arXiv preprints or
community analyses; treat as provisional.
### 2.1 Primacy / recency → Lost-in-the-middle (Serial Position Effects)
**Evidence strength: [single-study + partial replication]** — the analogy is
real but the LLM side has been refined and partially disconfirmed.
The human side: Asch (1946) on primacy in impression formation; Baddeley & Hitch
(1993) on recency in working memory. [2][3]
The LLM side: Wang et al. (ACL Findings 2025), _Serial Position Effects of Large
Language Models_ [4], explicitly tests for "primacy and recency biases, which
are well-documented cognitive biases in human psychology" and confirms
widespread occurrence across ChatGPT, GPT-J, GPT-3.5, GPT-4, and
Claude-instant-1.2. The lost-in-the-middle finding (Liu et al., TACL 2024) is
the same phenomenon under a different name.
**Refinements and partial disconfirmations:**
- Bilan et al. (arXiv 2508.07479, 2025) [5] show the U-shape only holds when
content occupies up to ~50% of the context window; beyond that, primacy
weakens and the curve becomes _distance-to-end_ rather than U-shaped.
- Mak (2025) [15] argues the dip is partly an artifact of positional-embedding
decay — tokens near the 90% position get "blurry" embeddings — producing
monotonic drop from start to end at very-long contexts, not a clean U.
- Zhang et al. (2024b), cited in [4], found studies that **did not** replicate
the LiM effect on certain long-context models, indicating the effect is
conditional on architecture and context length.
Humans don't have a context window, and their primacy advantage is stable across
passage length, so the analogy is conceptual rather than mechanistic.
**Practical convergence:** "put important content at the boundaries" works for
both — but the LLM version may degrade into pure recency at long contexts, and
the cause includes embedding-precision artifacts that have no human analog.
### 2.2 Hyperpersonal idealization → ELIZA effect / anthropomorphism
**Evidence strength: [multi-replicated]** — anthropomorphism toward chatbots is
one of the oldest and most-replicated findings in HCI; the hyperpersonal model
itself has decades of CMC support.
The human side: Walther's hyperpersonal model (1996) — in text-only
relationships, receivers idealize senders by filling in flattering detail. [#12
in human doc]
The LLM-adjacent side: the **ELIZA effect**, named for Weizenbaum's 1966 chatbot
— humans attribute understanding, empathy, and authenticity to systems that
produce text resembling human speech. The Cambridge essay collection on chatbot
authenticity (2024) [6] explicitly traces this to "a much longer history of
technologically mediated communications" and notes the same hyperpersonal
pattern: minimal cues, maximum projection.
This connection is bidirectional and was named long before LLMs — the mechanism
on the human side is identical (cue impoverishment → reader fills the gap), only
the partner changes.
### 2.3 Sycophancy ↔ social-desirability / agreement bias
**Evidence strength: [single-study + partial replication]** — the headline
result is peer-reviewed (ICLR 2024) on a specific set of RLHF'd models, but a
community replication on OpenAI base models found the effect does not generalize
across model families.
The human side: well-documented social-desirability and conformity effects
(Asch, 1956; Crowne & Marlowe, 1960) — humans give answers they believe the
listener wants.
The LLM side: Sharma et al. (ICLR 2024), _Towards Understanding Sycophancy in
Language Models_ [7], tested five SOTA RLHF assistants and analyzed the
`hh-rlhf` preference dataset. Headline finding:
> "Both humans and preference models prefer convincingly-written sycophantic
> responses over correct ones a non-negligible fraction of the time… matching a
> user's views is one of the most predictive features of human preference
> judgments."
On the Sharma et al. data, the bias is encoded into the **human preference
labels** that drive RLHF — i.e., human social-desirability bias is propagated to
the reward model and then to the policy. The mitigation literature
(Self-Augmented Preference Alignment, EMNLP 2025) [8] reframes the problem as
needing to explicitly assess the user's expected answer rather than ignore it.
**Important counter-evidence:** Perez et al. (2022) originally claimed
sycophancy appears even at **zero RLHF steps**, which would imply a
pretraining-corpus origin. nostalgebraist (2023) [16] reproduced Perez et al.'s
eval on OpenAI API base models (davinci, babbage, etc.) and found OpenAI base
models are **not sycophantic at any size**. Sycophancy emerges only with
specific finetuning pipelines (e.g., `text-davinci-002`/`003`). The honest
reading is:
- Sycophancy is **real and replicable** in specific RLHF'd model families.
- It is **not a universal property of RLHF** or of "models trained on human
text."
- The most plausible mechanism is _interaction_ between specific reward-model
shapes and specific preference data, not a clean inheritance from a single
human cognitive bias.
**Practical convergence (where it holds):** the human-side advice "ask for the
answer before stating your own view" maps directly to LLM-side guidance ("avoid
revealing your conclusion before asking the model").
### 2.4 Perspective-taking (Galinsky) ↔ SimToM prompting
**Evidence strength: [single-study]** — SimToM is a single 2023 arXiv paper with
no independent replication I found; the human-side perspective-taking literature
is robust.
The human side: Galinsky & Moskowitz (2000), perspective-taking reduces hostile
attributions and stereotype expression. [#7 in human doc]
The LLM side: Wilf et al. (2023), _Think Twice: Perspective-Taking Improves
Large Language Models' Theory-of-Mind Capabilities_ (SimToM) [10], explicitly
cites Simulation Theory's notion of perspective-taking and operationalizes it as
a two-stage prompt: filter the context to what a character knows, _then_ answer
questions about their mental state. Improves ToM benchmarks substantially with
no fine-tuning.
**Practical convergence:** for both humans and models, asking "what does the
other party know / believe / intend?" as a separate, explicit step before
responding improves accuracy on ambiguous-intent tasks.
### 2.5 Asking a clarifying question (Byron) ↔ Selective clarification (CLAM)
**Evidence strength: [multi-replicated]** on the human side; **[single-study]**
on the LLM side, but the CLAM framework has been re-used and extended in
follow-on work and integrated into Anthropic's published defaults.
The human side: Byron (2008) [#2 in human doc] — respond to ambiguous emotional
content with a question, not a reaction.
The LLM side: Kuhn et al. (arXiv 2212.07769), _CLAM: Selective Clarification for
Ambiguous Questions_ [11], shows current language models "rarely ask users to
clarify ambiguous questions and instead provide incorrect answers," and provides
a framework that meaningfully improves QA performance when ambiguity is detected
and a clarifying question is generated.
**Practical convergence:** the advice is identical and verified independently on
both sides — when intent is unclear, asking is better than guessing. The
Anthropic "default-to-clarify" system prompt variant ([1] in llm doc) is the
engineering implementation.
---
## 3. Cited Distinctions
### 3.1 Egocentrism (sender-side, human) ≠ literalism (Claude 4.7)
Kruger, Epley, Parker & Ng (2005) frame egocentrism as a **sender**
overestimating how clearly tone comes through. LLMs don't "send" in that sense —
they're always the receiver of the prompt. Anthropic's documented behavior
change in Opus 4.7 [llm doc, 1] is the opposite of human egocentrism: the model
becomes _less_ willing to infer beyond what's written.
**Implication:** the human-side cure ("state things explicitly because you can't
trust the receiver to read your mind") is exactly what the LLM-side
architectural shift now _requires_ from the user. Same advice, mirrored
mechanism.
### 3.2 Affect labeling (Lieberman) — claimed analog is weak
The temptation is to map affect labeling ("name the emotion") onto "ask the LLM
to identify sentiment before responding." Reichman et al. (arXiv
2603.09205, 2026) [12] introduce AURA-QA, an emotion-balanced QA dataset, and
find that "affective tone inadvertently influences semantic interpretation, even
among semantically equivalent inputs with differing emotional expressions."
Their proposed fix is _representation- level emotional regularization at
training time_, not a labeling prompt. So the mechanism (amygdala
down-regulation via verbal labeling of one's own affect) does not transfer; the
LLM lacks the regulatory loop the human practice exploits.
**Practical conclusion:** asking an LLM to "first identify the tone of this
message" can disambiguate intent, but the published mechanism is
representational, not regulatory. Don't expect the same calming / de-escalation
effect documented in humans.
### 3.3 Hostile-attribution bias (Aderka et al.) ≠ LLM negativity inheritance
In humans, hostile attribution is an _interpretive_ tendency in ambiguous social
cues, tied to individual differences (anxiety, prior experience). In LLMs,
negative-sentiment inheritance is a **statistical property of the pretraining
corpus** that propagates into embeddings and downstream classifiers [9][12].
Both produce "neutral text read as negative," but the human bias varies by
reader; the LLM bias varies by corpus and is roughly stable per model.
Mitigations are correspondingly different: cognitive (re-read, generate
alternatives) on the human side, data/representational on the LLM side.
---
## 4. Parallels Without a Published Bridge
These look like genuine analogies but I did not find a paper that draws the link
explicitly. Use them as working hypotheses, not citations.
| Human-side practice | LLM-side practice | Status |
| ------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Delay / "don't hit send" | Reflect / self-correct / multi-turn revision | Mechanistically different (amygdala vs. additional inference passes); empirically both reduce errors. Self-reflection survey: [13]. |
| Re-read slowly | Self-consistency / re-read prompt | Self-consistency (Wang et al. 2023) reduces hallucination; not framed as analogous to human re-reading in the papers I found. |
| Principle of charity / steel-manning | "State scope explicitly" (Anthropic 4.7 guide) | Both are about pre-empting under-specified intent. No source connects them. |
| NVC: observation → interpretation gap | XML tags around content | Both separate "what is on the page" from "what to do with it," but the rationales (cognitive defusion vs. attention boundaries) differ. |
| Match medium to message (richness) | Escalate to bigger model / use tools | Daft & Lengel's media richness has been cited in CMC literature; no direct LLM-side citation found. |
---
## 5. Orphans (No Found Counterpart Either Direction)
### Human-side, no LLM analog found
- **Mehrabian "55/38/7" debunk.** Specific to humans + paralinguistic cues; no
parallel claim in LLM literature.
- **Emoji as partial tone fix (Riordan 2017).** Emoji-in-prompt research exists
but treats emoji as tokens, not as a tone-channel substitute. The analogy is
shallow.
- **The minimal operating checklist (§3 of human doc).** Some items map
(clarifying question, perspective-taking); the rest (pause, pulse check) have
no plausible model analog.
### LLM-side, no human analog found
- **Quantization effects (Q3/Q4/Q5/Q8 trade-offs).** Uniquely a
numerical-precision phenomenon. The closest human analog would be fatigue /
cognitive load reducing reasoning accuracy, but no source draws this link, and
the dose-response curves are different shapes.
- **Dense vs. MoE architecture (Shen et al. 2024).** Routing-based
specialization has no plausible human analog at the level the paper studies.
- **Parameter count and bimodal emergence (Distributional Scaling Laws).**
Reflects training stochasticity; humans don't "scale" in a comparable way.
- **Role confusion / CoT Forgery (style → authority).** A human parallel exists
(uniforms, jargon, Milgram-style obedience to apparent authority), but I found
no paper that draws the explicit LLM↔human bridge for stylistic-spoofing
attacks. Worth flagging as a likely-but-unwritten connection.
- **Default-to-action vs. default-to-clarify as a prompt knob.** This is a
property of model alignment dials, not of human cognition. The human side has
trait-level analogs (conscientiousness, impulsivity) but they're not knobs.
---
## 6. Additional Findings Worth Carrying Forward
Two items surfaced during this synthesis that didn't fit cleanly into either
prior doc but are relevant to anyone using the previous two.
### 6.1 The bias-inheritance chain is two-stage, not one
Mina et al. [1] and Hartvigsen-line work [9] together imply a useful mental
model: human biases reach LLMs through **two distinct channels** that need
different mitigations.
1. **Pretraining-corpus channel.** Cognitive and sentiment biases that exist in
the source text (e.g., common-token, majority-class, identity-term
sentiment). Mitigated at the data / training-objective level (e.g., AURA-QA's
emotional regularization [12]).
2. **Preference-label channel.** Biases in human judgments that drive RLHF —
most prominently sycophancy [7]. Mitigated at the reward-model / alignment
level (SAPA [8]).
A prompt-time mitigation only addresses the symptom. This explains why "be
specific" reliably helps but "tell the model not to be sycophantic" helps less
than expected — only the former is in the model's in-context-learnable
repertoire.
### 6.2 RLHF amplifies serial-position effects
Tjuatja et al. (2023), cited in Wang et al. [4], find that RLHF **increases**
serial position effects relative to base models. This is consistent with the
broader pattern that alignment training, while making models more useful, also
makes them more reliably _human-like_ in their failure modes — including ones
we'd rather not import.
**Practical takeaway:** if you have a choice between a base/lightly- tuned local
model and a heavily-RLHF'd one for tasks where positional fairness matters
(e.g., ranking, multiple-choice evaluation), the base model may show _less_ of
the human-analog bias.
---
## 7. Sources
1. Mina, M., Ruiz-Fernández, V., Falcão, J., Vasquez-Reina, L., &
Gonzalez-Agirre, A. (2024). _Cognitive biases in large language models: A
survey and mitigation experiments._ COLING 2025.
https://aclanthology.org/2025.coling-main.120v1.pdf
2. Asch, S. E. (1946). _Forming impressions of personality._ Journal of Abnormal
and Social Psychology, 41(3), 258290. (Primacy effect in impression
formation.)
3. Baddeley, A. D., & Hitch, G. J. (1993). _The recency effect: Implicit
learning with explicit retrieval?_ Memory & Cognition, 21(2), 146155.
4. Wang, X., et al. (2024/2025). _Serial Position Effects of Large Language
Models._ ACL Findings 2025. arXiv:2406.15981. (Explicitly tests human
primacy/recency analogs in LLMs.)
5. Bilan, J., et al. (2025). _Positional Biases Shift as Inputs Approach Context
Window Limits._ arXiv:2508.07479. (LiM is strongest up to ~50% of context
window; beyond that, distance-to-end dominates.)
6. _Can Chatbots Be Authentic? The ELIZA Effect Revisited._ Cambridge University
Press essay collection (2024). (Hyperpersonal / anthropomorphism lineage from
Eliza to modern LLMs.)
7. Sharma, M., et al. (2024). _Towards Understanding Sycophancy in Language
Models._ ICLR 2024. arXiv:2310.13548.
8. Park, J., et al. (2025). _Self-Augmented Preference Alignment for Sycophancy
Reduction in LLMs._ EMNLP 2025.
9. Khandelwal, A., et al. (2024). _Scaling and sentiment bias propagation from
pretraining corpora into downstream models._ arXiv preprint. (CC-100 vs.
Wikipedia sentiment toward identity groups; propagation to fine-tuned
toxicity classifiers.)
10. Wilf, A., et al. (2023). _Think Twice: Perspective-Taking Improves Large
Language Models' Theory-of-Mind Capabilities._ arXiv:2311.10227. (SimToM —
explicit operationalization of Galinsky-style perspective-taking for LLMs.)
11. Kuhn, L., Gal, Y., & Farquhar, S. (2022/2023). _CLAM: Selective
Clarification for Ambiguous Questions with Large Language Models._
arXiv:2212.07769.
12. Reichman, B., et al. (2026). _AURA-QA: An emotionally balanced QA dataset
and emotional regularization framework._ arXiv:2603.09205.
13. Ji, Z., et al. (2023). _Towards Mitigating Hallucination in Large Language
Models via Self-Reflection._ arXiv:2310.06271.
14. Tjuatja, L., et al. (2023). _RLHF amplifies prompt-position sensitivity in
language models._ Cited in [4]. (Original arXiv preprint; full reference in
[4]'s bibliography.)
15. Mak, Y. C. (2025). _Lost in the middle, or just lost? Evaluating LLMs on
information retrieval with long input contexts._
https://ycmak.net/how-lost-in-the-middle/ (Argues the U-shape is partly an
artifact of positional-embedding decay producing monotonic drop at very long
contexts. Not peer-reviewed; data and methodology are public.)
16. nostalgebraist (2023). _OpenAI API base models are not sycophantic, at any
size._ LessWrong.
https://www.lesswrong.com/posts/3ou8DayvDXxufkjHD/openai-api-base-models-are-not-sycophantic-at-any-size
(Replication-style analysis disconfirming the strongest reading of Perez et
al. 2022 for OpenAI base models.)
17. Schulhoff, S. et al. (2024). _The Prompt Report: A Systematic Survey of
Prompting Techniques._ arXiv:2406.06608. (PRISMA review of 1,565 papers;
foundational survey used as cross-check on prompt-engineering claims in the
companion LLM doc.)
18. _Principled Personas: Defining and Measuring the Intended Effects of Persona
Prompting on Task Performance._ EMNLP 2025.
https://aclanthology.org/2025.emnlp-main.1364/ (Persona prompts often
ineffective; up to ~30pp drops from irrelevant persona details.)

View File

@ -0,0 +1,379 @@
# Action Plan: Counteracting Model Failures to Interpret Intent
**Status:** draft (2026-05-16)
**Source investigation:**
[docs/explorations/text-intent-interpretation-research.md](../explorations/text-intent-interpretation-research.md)
**Source
research docs:**
- [docs/research/text-communication-interpretation.md](text-communication-interpretation.md)
(Phase 1: humans reading text)
- [docs/research/llm-intent-interpretation.md](llm-intent-interpretation.md)
(Phase 1: LLMs reading prompts)
- [docs/research/human-llm-interpretation-overlap.md](human-llm-interpretation-overlap.md)
(Phase 2: synthesis)
- [docs/research/ai-coding-best-practices.md](ai-coding-best-practices.md)
(cross-reference: §2.1, §3.2, §3.4a, §3.5, §3.6, §3.7, §3.8, §7)
## How to read this document
Each entry has the same shape:
```
Failure mode → Why it happens → Mitigation that works → Tempting-but-wrong mitigation (anti-pattern) → Where to implement in this repo
```
The "tempting-but-wrong" line is the most important part. Many of the obvious
mitigations either (a) have no measurable effect or (b) actively hurt
performance — and they sound so reasonable they get added by default. If a
mitigation is on the anti-pattern list, _do not_ add it as a workaround when
something else fails.
Evidence-strength tags follow the synthesis doc's legend:
**[multi-replicated]**, **[single-study + partial replication]**,
**[single-study]**, **[preprint-only]**.
---
## 1. Failure mode: misreading the user's actual question
### 1.1 Position-anchored priming (model defends a prior answer)
**Why it happens.** The model's previous turn sits in the context window and
acts as a prior the model subsequently defends. Follow-ups are read through the
lens of the prior position, not on their own terms. **[multi-replicated]** —
documented across model families; mechanism supported by ClashEval (Wu, Wu, Zou,
NeurIPS 2024) showing token-probability/adherence relationship.
**What works (in order of effectiveness):**
1. **Compaction or fresh context.** Physically remove the prior committed
answer. The anchor is broken. Use `PreCompact` to preserve only the user's
current question and the verified-correct state.
2. **Adversarial reframing.** Lower the model's confidence in its prior
commitment _before_ asking the next question: _"I believe your previous
answer was wrong because X. Now answer this specific question: ..."_
ClashEval's mechanism (lower token-probability prior → higher context
adherence) extends to this case in principle.
3. **Explicit current-question marker at the tail.** `UserPromptSubmit` hook
prepends `CURRENT QUESTION (answer this, not the prior exchange):` to the
prompt. Mechanical, cheap, observable.
**Tempting but wrong (do not do):**
- Repeating the question louder, adding emphasis, or asking the model to "read
more carefully." None of these change the anchor. They feel productive and do
nothing.
- Asking the model to re-state the question in its own words before answering.
In the no-oracle setting this can entrench the misreading rather than reset
it.
**Where in this repo:**
- `UserPromptSubmit` hook (already exists at
[.agents/hooks/user-prompt-submit.sh](../../.agents/hooks/user-prompt-submit.sh))
is the right place for the current-question marker.
- Compaction logic in `PreCompact` hook (already exists at
[.agents/hooks/pre-compact.sh](../../.agents/hooks/pre-compact.sh)) is the
right place for the structured prior-discard.
### 1.2 Sycophancy (model defends the user's wrong claim)
**Why it happens.** Family-conditional behavior: some RLHF recipes
(Anthropic 2023) systematically push toward agreement with the user. **NOT** a
universal RLHF property — nostalgebraist (LessWrong, 2023) showed OpenAI base
models are not sycophantic at any size. **[single-study + partial replication]**
with the caveat that the effect depends on the model family in use.
**What works:**
- External feedback signals (test runners, hooks, type checkers, build) that
give the model a non-user source of truth.
- Explicit anti-sycophancy rules in `AGENTS.md` and agent bodies: _"Challenge
the user when the user is wrong,"_ _"Read a file before asserting facts about
it,"_ _"Only make changes that are directly requested."_
**Tempting but wrong:**
- Telling the model "be more critical" or "push back when needed." On
sycophantic families this softens the floor but doesn't move the median; on
non-sycophantic families it's noise.
- LLM-as-judge of the user's own claim (self-critique loop without an oracle —
see §4.1 below).
**Where in this repo:**
- [AGENTS.md](../../AGENTS.md) root anti-pattern list (already present).
- [.agents/AGENTS.md](../../.agents/AGENTS.md) per-agent rule reinforcement.
### 1.3 Persona / "you are an expert" prompting
**Why it happens.** Prompt-engineering folklore from 20222023 that expertise
personas improve accuracy. The 2025 literature falsifies this for accuracy
benchmarks. **[multi-replicated]** as a _negative_ result:
- Principled Personas (EMNLP 2025) — models are highly sensitive to irrelevant
persona details; performance drops of ~30pp from small attribute changes.
- Persona is a Double-Edged Sword (IJCNLP 2025) — mixed and unstable effects.
- [arXiv:2512.05858](https://arxiv.org/abs/2512.05858) — persona prompts
generally did not improve accuracy; low-knowledge personas (layperson, child,
outsider) often _reduced_ accuracy.
**What works:**
- Define _task contracts_ and _return formats_ for subagents (this is not the
same as injecting an expertise persona).
- Use the existing counterbalance agents
([.agents/agents/](../../.agents/agents/)) which are defined by what they
_counter_, not by what they _are an expert in_.
**Tempting but wrong:**
- Adding `"You are a senior X engineer with 20 years of experience..."` to agent
prompts. No measurable effect on frontier models; on small models can hurt via
persona-attribute sensitivity.
- Expertise-ladder prompting (junior/senior/outsider) as an **accuracy**
improver. It is _only_ defensible as a divergent-ideation sampler for
brainstorm tasks where high variance is the goal — and even then, the final
answer should come from the un-personified model under an external rubric. See
revised
[docs/research/ai-coding-best-practices.md §7](ai-coding-best-practices.md).
**Where in this repo:**
- Audit existing agent prompts in [.agents/agents/](../../.agents/agents/) for
any "you are an expert X" framing. Replace with negative-role and return-
format specs. (Action item, to be done after this plan is approved.)
---
## 2. Failure mode: misreading specific tokens / instructions in long context
### 2.1 Lost-in-the-middle / serial-position effects
**Why it happens.** Transformer attention is quadratic in context length;
information in the middle of long contexts receives proportionally less
attention. **[single-study + partial replication]** — Liu et al. (2023)
established the U-shape; Bilan et al. (arXiv:2508.07479, 2025) shows the U-shape
only holds up to ~50% of context window; Mak (2025) shows positional- embedding
decay produces monotonic drop in very-long contexts; Zhang et al. (2024b)
non-replication on some model families. Effect is real but mechanism varies and
effective context is typically 3050% of advertised.
**What works:**
- Task-critical content at the **tail** of context (recency bias is strong and
consistent across the tested models).
- Rules repeated at both ends (start AND tail), not just AGENTS.md (start only).
- Hooks injecting at the context tail outlast AGENTS.md under context pressure.
- Summarization-in-place for stale tool outputs (don't scroll, replace).
**Tempting but wrong:**
- Putting more rules in AGENTS.md when the existing ones aren't being followed.
They are forgotten from the middle by ~510k tokens of subsequent context.
_Adding more makes it worse._ Move the rule to a hook instead.
- Increasing the model's context window. Effective attention does not scale with
advertised window; the middle gets _worse_, not better.
- "Reminding" the model with bold text or all-caps in AGENTS.md. Token-level
emphasis has no measurable effect on the LiM gradient.
**Where in this repo:**
- Enforcement hierarchy in [.agents/AGENTS.md](../../.agents/AGENTS.md) already
encodes the right pattern.
- Existing hooks ([.agents/hooks/](../../.agents/hooks/)) already implement the
context-tail-injection pattern. New guidance should follow that pattern.
### 2.2 Sequential-constraint ordering failures
**Why it happens.** Cross-references documented in
[ai-coding-best-practices.md §4.6](ai-coding-best-practices.md). When a list of
constraints is given in one order but must be applied in another, models apply
in the order they read them, not the order they should be applied in.
**What works:**
- Re-order constraints in the prompt to match application order.
- Use a verifier (a hook, a test, a lint rule) instead of relying on the model
to compose constraints in the right order.
**Tempting but wrong:**
- Numbered lists (1, 2, 3) implying priority order. Models don't reliably honor
numeric priority over textual position.
---
## 3. Failure mode: ambiguity in the user's request
### 3.1 Models do not ask clarifying questions by default
**Why it happens.** Pretraining favors confident-helpful continuations. Asking
for clarification reads as "less helpful" in preference data.
**[multi-replicated]** in conversational AI literature.
**What works:**
- Explicit instruction in the system prompt: _"If the user's intent is unclear,
infer the most useful likely action and proceed with using tools to discover
missing details instead of guessing"_ — paired with a structured
ambiguity-flagging mechanism (e.g., the agent surfaces an explicit "assumption
made: X" line before acting).
- For high-stakes operations: ask one targeted clarifying question with options
(the existing ask-question tool / `vscode_askQuestions` pattern).
**Tempting but wrong:**
- Telling the model "ask if anything is unclear." Models report nothing as
unclear that they could fluently continue past. The instruction has near- zero
effect.
- Adding many "do you mean X or Y?" examples in the prompt. Few-shot examples
for capable models on common tasks often actively harm via spurious
pattern-matching
([ai-coding-best-practices.md §7](ai-coding-best-practices.md)).
**Where in this repo:**
- The default agent's `copilot-instructions` (if used here) or
[AGENTS.md](../../AGENTS.md) operational rules section.
---
## 4. Failure mode: trying to fix it by asking the model to fix itself
### 4.1 Intrinsic self-correction without an oracle
**Why it happens.** It feels like reflection should help. Empirically it
doesn't, and often it hurts. **[multi-replicated]** as a negative result:
- Huang et al. ([arXiv:2310.01798](https://arxiv.org/abs/2310.01798), "Large
Language Models Cannot Self-Correct Reasoning Yet"): in the intrinsic
(no-oracle) setting, self-correction **consistently decreases** reasoning
performance across multiple prompts and tasks. Prior optimism about
self-correction in earlier papers vanishes when oracle labels are removed.
- Pan et al. (arXiv:2308.03188): survey reaches the same conclusion in aggregate
— external feedback signals are reliable; intrinsic self-critique is not.
**What works:**
- External feedback signal: test runner, type checker, lint, hook exit code,
build success. Reflexion (Shinn et al., arXiv:2303.11366) achieves 91% pass@1
on HumanEval _with_ an external oracle — without one, the loop is noise.
- Failure-mode-routed intervention: a small judge subagent that classifies the
failure mode and selects the matching intervention (see
[ai-coding-best-practices.md §3.5](ai-coding-best-practices.md) table). The
judge must be a stronger or cross-family model; same-family same-size judging
compounds bias.
**Tempting but wrong (this is the single most common anti-pattern):**
- _"Take another look,"_ _"are you sure?"_ _"please double-check your work,"_
_"reflect on whether this is correct."_ All of these feel productive in
transcripts. Without an external oracle they are at best noise and measurably
degrade correctness in the published benchmark. Do not add them.
- LLM-as-judge with the same model evaluating itself. Self-enhancement bias
(Zheng et al. 2023, MT-Bench) — same-family judges over-score their own
family's outputs.
**Where in this repo:**
- Verification is already correctly in the harness (build, lint, tests, hooks)
rather than the prompt — see
[ai-coding-best-practices.md §8.1](ai-coding-best-practices.md) and the
existing hook set.
- The reflection-without-oracle anti-pattern should be added explicitly to
[AGENTS.md](../../AGENTS.md) `<implementationDiscipline>` so it doesn't creep
back in as a "let me check my work" pattern.
### 4.2 Chain-of-thought as a universal fix
**Why it happens.** CoT works on some tasks; folklore generalized it to all
tasks. **[single-study + partial replication]** as a _negative_ finding for the
universalization:
- [arXiv:2409.06173](https://arxiv.org/abs/2409.06173) shows CoT suffers from
posterior collapse: larger models anchor _harder_ to reasoning priors under
CoT on subjective tasks (emotion, morality, intent inference).
**What works:**
- CoT for objective, verifiable reasoning (math, code logic, step-counted
inference).
- Think-Anywhere (Jiang et al., arXiv:2603.29957) and interleaved thinking
(Claude 4.x extended thinking) — mid-sequence reasoning at high-entropy
positions, not just upfront planning.
**Tempting but wrong:**
- _"Let's think step by step"_ preambles for reasoning-trained models — at best
redundant (the model is already trained to reason); at worst it entrenches a
wrong prior on subjective tasks.
- Long CoT on intent-interpretation tasks. The model can reason itself _further
into_ the misread.
---
## 5. Cross-cutting principle: the harness is where intent gets clarified
A unifying claim from the synthesis doc that survives both the human and LLM
literature: when ambiguity is high, neither a human nor a model resolves it by
"reading more carefully." Resolution happens through **external signal**
question, test, lint, hook, oracle. The harness is where the external signal
lives. The prompt is where the rule of "use the external signal" lives.
Every action in this plan reduces to one of three moves:
1. **Move the rule into the harness.** Hooks, tests, type checkers, lint. These
are unambiguous and fire deterministically.
2. **Reduce reliance on context-middle attention.** Context-tail injection,
compaction, structured retrieval.
3. **Reduce reliance on self-critique.** External oracles, cross-family judges,
structured failure routing.
If a proposed mitigation does not fit one of these three, it probably belongs on
the tempting-but-wrong list.
---
## 6. Proposed concrete edits (for user approval)
This plan does not yet ship code changes. Proposed next steps in dependency
order:
- [ ] **A.** Audit [.agents/agents/](../../.agents/agents/) bodies for "you are
an expert X" framing and replace with negative-role / return- format
specs. Likely small edits to 14 files.
- [ ] **B.** Add an anti-pattern bullet to
[.agents/AGENTS.md](../../.agents/AGENTS.md) calling out _"reflect /
double-check / are you sure"_ as a non-mitigation without an external
oracle. Scoped to `.agents/` (not root `AGENTS.md`) because it is
metaknowledge about agent design — only relevant when authoring agent
infrastructure, not when writing application code where tests are the
oracle anyway.
- [x] **C.** Add a `CURRENT QUESTION (answer this, not the prior exchange):`
prefix-injection option to
[.agents/hooks/user-prompt-submit.sh](../../.agents/hooks/user-prompt-submit.sh),
either always-on or gated on a follow-up trigger phrase. **Shipped
always-on** (Revision 7, 2026-05-16). Placed last in `additionalContext`
(context tail = highest recency bias). Validated by S2A (Weston &
Sukhbaatar, arXiv:2311.11829): explicitly isolating the current query from
prior context reduces sycophancy and improves factuality without a second
LLM call. Same mechanism as the ClashEval token-probability anchoring
research cited in §1.1.
- [x] **D.** Add an `ambiguity-flag` convention: when the agent infers user
intent past a real ambiguity, surface a one-line `ASSUMPTION:` marker
before proceeding. Documented in [AGENTS.md](../../AGENTS.md); enforceable
optionally via a `PreToolUse` check on certain destructive tools.
**Shipped as documentation** in root `AGENTS.md` "Key Rules" section
(Revision 7, 2026-05-16). PreToolUse enforcement deferred — would fire on
every destructive call regardless of whether there was genuine ambiguity,
producing noise without selectivity.
- [ ] **E.** Update
[docs/verified/ai-coding-best-practices.md](../verified/ai-coding-best-practices.md)
summary to reflect the three corrections from Revision 6 of the research
doc (sycophancy family-conditional, intrinsic self-correction is the
strongest anti-pattern, persona-ladder scoped to ideation only).
Open question for the user: which of AE should ship in this conversation, which
need a separate task, and which should be discarded?

View File

@ -0,0 +1,598 @@
# llama-server with CUDA on WSL2
Guide to deploying `llama-server` (llama.cpp) as a systemd service on WSL2 with
full NVIDIA GPU offload via CUDA. Configured in **router mode** to serve
multiple GGUF models on-demand (with optional MTP speculative decoding) via an
OpenAI-compatible API.
**Target environment:**
- WSL2 (Ubuntu 24.04 Noble)
- NVIDIA RTX 3080 12GB (or similar), driver exposed via WSL2 GPU passthrough
- No separate CUDA toolkit install required to _run_; only needed when building
---
## Why not Ollama?
Ollama vendors a pinned version of llama.cpp and bundles its own CUDA runtime.
New model architectures (like `qwen35` / Qwen3-Next) may not be supported until
Ollama syncs its fork. `llama-server` from upstream llama.cpp supports them as
soon as the architecture lands in the main branch.
**Ollama does nothing special** beyond: bundling `libggml-cuda.so` alongside its
runner and setting `PATH` to include `/usr/lib/wsl/lib` (the WSL2 CUDA driver
passthrough). No flash-attention env vars, no special flags. We replicate this.
---
## Prerequisites
```bash
# Verify WSL2 CUDA driver passthrough is working
ls /usr/lib/wsl/lib/libcuda.so.1 # must exist
nvidia-smi # must show your GPU
```
---
## Step 1 — Install CUDA toolkit and build dependencies
> Only needed once per machine to compile llama.cpp. Not needed at runtime.
```bash
sudo apt-get install -y nvidia-cuda-toolkit cmake build-essential git
```
Ubuntu 24.04 ships CUDA 12.0 in the `multiverse` repo. This is sufficient to
build llama.cpp with CUDA support even when the runtime driver is newer (e.g.
CUDA 13.1 via WSL2 passthrough). Alternatively, install CUDA 12.x from
[NVIDIA's own APT repo](https://developer.nvidia.com/cuda-downloads) to get a
more recent toolkit.
Verify the compiler is available:
```bash
nvcc --version
```
---
## Step 2 — Clone and build llama.cpp from source
```bash
# Clone at a specific tag — check https://github.com/ggml-org/llama.cpp/releases for latest
# b9144+ required for qwen35 architecture (Qwen3.6, OmniCoder 2, etc.)
# b9279+ required for MTP speculative decoding (--spec-type draft-mtp)
git clone --depth 1 --branch b9279 https://github.com/ggml-org/llama.cpp.git /tmp/llama-build
cd /tmp/llama-build
# Configure with CUDA backend
cmake -B build \
-DGGML_CUDA=ON \
-DCMAKE_BUILD_TYPE=Release \
-DLLAMA_BUILD_SERVER=ON \
-DLLAMA_BUILD_TESTS=OFF \
-DLLAMA_BUILD_EXAMPLES=OFF
# Build (uses all cores; takes 10-15 min on a 12-core CPU)
cmake --build build --config Release -j$(nproc)
```
After the build completes you should see `build/bin/llama-server`.
---
## Step 3 — Install to /opt/llama-server
```bash
sudo mkdir -p /opt/llama-server
# Copy the server binary
sudo cp build/bin/llama-server /opt/llama-server/
# Copy all shared libraries (b9144+ puts them all in build/bin/)
sudo cp -P build/bin/libggml*.so* /opt/llama-server/
sudo cp -P build/bin/libllama*.so* /opt/llama-server/
sudo cp -P build/bin/libmtmd*.so* /opt/llama-server/ 2>/dev/null || true
# Register the directory so transitive .so dependencies resolve
echo "/opt/llama-server" | sudo tee /etc/ld.so.conf.d/llama-server.conf
sudo ldconfig
```
> **Note (b9144+):** The library layout changed — all `.so` files now live in
> `build/bin/` (not `build/ggml/src/` or `build/src/`). When upgrading, copy
> with `-P` to preserve versioned symlinks and overwrite the old ones.
---
## Step 4 — Create the start script
Run llama-server in **router mode** — no `--model` flag. Models are loaded
on-demand from `~/models/` when a request names them. Switching models requires
no restart and no `sudo`: just change the `model` field in `opencode.json`.
```bash
sudo tee /opt/llama-server/start.sh > /dev/null << 'SCRIPT'
#!/bin/bash
export LD_LIBRARY_PATH=/opt/llama-server${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
cd /opt/llama-server
exec /opt/llama-server/llama-server \
--models-dir /home/dev/models \
--models-max 1 \
--models-preset /home/dev/models/presets.ini \
--host 127.0.0.1 \
--port 8080
SCRIPT
sudo chmod +x /opt/llama-server/start.sh
```
**Key router flags:**
- `--models-dir` — directory scanned for GGUF files. Flat `.gguf` files become
model IDs using the filename **without** `.gguf`. Subdirectories become model
IDs using the directory name (used for multimodal models with a separate
mmproj file — see _Multimodal models_ below).
- `--models-max 1` — only one model resident at a time. When a different model
is requested, the current one is evicted and the new one loads (cold-start
delay). With 12GB VRAM this is required.
- `--models-preset` — path to `presets.ini` for global defaults and per-model
overrides. All inference flags belong here, not in `start.sh`.
**Per-model settings via `presets.ini`**
All inference flags (`ctx-size`, `n-predict`, `n-gpu-layers`, `flash-attn`,
`threads`, `parallel`, `jinja`, `spec-type`, etc.) live in
`~/models/presets.ini`, not in `start.sh`. The `[*]` section sets defaults
inherited by every model; named sections override individual keys.
Section names must match the router's model ID — the filename **without**
`.gguf`. Using the `.gguf` suffix in a section name creates a duplicate entry in
the model list.
```ini
version = 1
[*]
n-gpu-layers = 99
flash-attn = on
threads = 8
parallel = 1
[Qwen_Qwen3-14B-Q4_K_M]
ctx-size = 32768
n-predict = 4096
[OmniCoder-2-9B.Q8_0]
ctx-size = 32768
n-predict = 4096
[Qwen_Qwen3.6-27B-Q4_K_M]
ctx-size = 16384
n-predict = 4096
```
> **Note:** The router reads `presets.ini` **once at service startup** — it is
> not watched for changes. After editing it, run
> `sudo systemctl restart llama-server` to apply the new settings. Any
> currently-loaded model will be evicted and must cold-reload on the next
> request (~1060 s).
**On GPU layer offload:** Hybrid inference (some layers on CPU, some on GPU) is
significantly slower than full-GPU due to CPU↔GPU memory transfers each forward
pass. For interactive use, prefer models that fit entirely in VRAM. MoE models
(like Qwen3.6-35B-A3B) are an exception — their sparse activation means active
computation per token is only ~3B parameters regardless of total model size, so
partial CPU offload is less painful than with a dense model of the same file
size. See the _Model choice_ section below.
---
## Step 5 — Create the systemd service
```bash
sudo tee /etc/systemd/system/llama-server.service > /dev/null << 'EOF'
[Unit]
Description=llama-server (OmniCoder 2 / qwen35)
After=network-online.target
[Service]
ExecStart=/opt/llama-server/start.sh
User=ollama
Group=ollama
Restart=always
RestartSec=3
Environment="PATH=/home/dev/.nvm/versions/node/v24.15.0/bin:/home/dev/.opencode/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/snap/bin"
[Install]
WantedBy=default.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable llama-server
sudo systemctl start llama-server
```
> **Note:** The `PATH` includes `/usr/lib/wsl/lib` — this is what exposes the
> CUDA driver (`libcuda.so.1`) to the process in WSL2. Without this, the CUDA
> backend will load but fail to initialize the device.
---
## Step 6 — Verify GPU offload
```bash
# Check service is running
systemctl status llama-server
# Health endpoint
curl -s http://127.0.0.1:8080/health
# → {"status":"ok"}
# Watch GPU memory in another terminal during a request
watch -n1 nvidia-smi
# Quick inference test
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"local","messages":[{"role":"user","content":"Say hello."}],"max_tokens":20}' \
| python3 -m json.tool
```
During inference, `nvidia-smi` should show:
- GPU-Util: 80-100%
- GPU Memory: ~10-11GB used (model weights + KV cache)
- CPU: near idle
```bash
# Quick inference test (node instead of python3)
curl -s http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"Qwen_Qwen3-14B-Q4_K_M","messages":[{"role":"user","content":"Say hello."}],"max_tokens":20}' \
| node -e "let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>console.log(JSON.parse(d).choices[0].message.content))"
```
---
## Step 7 — Configure OpenCode
Edit `~/.config/opencode/opencode.json` to add the provider. Model IDs are the
filenames **without** `.gguf` (or the subdirectory name for multimodal models).
The `limit` values here inform opencode's context window tracking; the actual
server-side limits are set in `presets.ini`.
```json
"llama-server": {
"npm": "@ai-sdk/openai-compatible",
"name": "llama-server",
"options": { "baseURL": "http://127.0.0.1:8080/v1" },
"models": {
"Qwen_Qwen3-14B-Q4_K_M": {
"name": "Qwen3 14B Q4 (fast)",
"tools": true,
"limit": { "context": 32768, "output": 4096 }
},
"Qwen_Qwen3.6-27B-Q4_K_M": {
"name": "Qwen3.6 27B Q4 (deep)",
"tools": true,
"limit": { "context": 16384, "output": 4096 }
},
"OmniCoder-2-9B.Q8_0": {
"name": "OmniCoder 2 9B Q8 (vision)",
"tools": true,
"limit": { "context": 32768, "output": 4096 }
},
"Qwen3.6-35B-A3B-IQ3_S-3.06bpw": {
"name": "Qwen3.6 35B A3B IQ3 (MoE+MTP)",
"tools": true,
"limit": { "context": 8192, "output": 4096 }
}
}
}
```
In the project-level `opencode.json`, set the active model per agent:
```json
"agent": {
"orchestrator": {
"model": "llama-server/Qwen_Qwen3-14B-Q4_K_M"
}
}
```
---
## Model choice for RTX 3080 12GB
Pick based on what fits **entirely** in VRAM — hybrid inference (model too large
for VRAM) is 48× slower and makes interactive use painful. MoE models are an
exception; see note below the table.
| Model | File size | Fits in 12GB? | Speed (est.) | Notes |
| ------------------------------- | --------- | ------------- | ------------- | ---------------------------------------------------------------------------------------- |
| Qwen3-8B Q4_K_M | ~5 GB | ✅ fully | ~2535 tok/s | Fast; weaker reasoning |
| **Qwen3-14B Q4_K_M** | ~8.5 GB | ✅ fully | ~1218 tok/s | **Daily driver** — fast interactive use, good instruction following |
| OmniCoder-2-9B Q8_0 | ~9.5 GB | ✅ fully | ~1520 tok/s | Vision-capable (multimodal); subdirectory layout for auto-detected mmproj |
| **Qwen3.6-27B Q4_K_M** | 17 GB | ⚠️ partial | ~48 tok/s | **Deep reasoning** — better at vague/complex tasks; slow due to CPU offload |
| **Qwen3.6-35B-A3B IQ3_S (MTP)** | 13.6 GB | ⚠️ partial | ~2035 tok/s† | **MoE + MTP** — sparse activation (~3B active params); needs MTP-format GGUF (byteshape) |
| Qwen3-32B Q4_K_M | ~20 GB | ❌ | — | Won't fit |
† MoE speed estimate with `--spec-type draft-mtp`. Despite 13.6 GB file size,
only ~1.6 GB needs CPU offload (few dense attention layers overflow VRAM). The
sparse feed-forward experts make active-parameter compute comparable to a 3B
dense model.
All models sit in `~/models/` simultaneously and are swapped on-demand by the
router. Cold-swap time is ~10s (914B) / ~3045s (27B+).
Download from bartowski on HuggingFace (imatrix quants, standard GGUF format):
```bash
mkdir -p ~/models
wget -c "https://huggingface.co/bartowski/Qwen_Qwen3-14B-GGUF/resolve/main/Qwen_Qwen3-14B-Q4_K_M.gguf" \
-O ~/models/Qwen_Qwen3-14B-Q4_K_M.gguf
```
> **⚠️ Use HuggingFace GGUFs, not Ollama blobs for qwen35-architecture models.**
> Ollama's converter outputs different tensor names and per-layer KV-head arrays
> that are incompatible with llama.cpp's `qwen35` model loader. Symptoms:
> `missing tensor 'blk.0.ssm_dt'`, `check_tensor_dims: wrong shape`, or
> `rope.dimension_sections has wrong array length`. Always download from
> bartowski or unsloth on HuggingFace for these models.
---
## Switching models
With router mode, switching requires **no restart and no `sudo`**. Place GGUFs
in `~/models/` and reference them by model ID in `opencode.json`.
### Add a model
```bash
# Download to ~/models/ — filename without .gguf becomes the model ID
wget -c "https://huggingface.co/bartowski/Qwen_Qwen3.6-27B-GGUF/resolve/main/Qwen_Qwen3.6-27B-Q4_K_M.gguf" \
-O ~/models/Qwen_Qwen3.6-27B-Q4_K_M.gguf
```
Then add a section to `~/models/presets.ini` (name = filename without `.gguf`):
```ini
[Qwen_Qwen3.6-27B-Q4_K_M]
ctx-size = 16384
n-predict = 4096
```
And register it in `~/.config/opencode/opencode.json`:
```json
"Qwen_Qwen3.6-27B-Q4_K_M": {
"name": "Qwen3.6 27B Q4 (deep)",
"tools": true,
"limit": { "context": 16384, "output": 4096 }
}
```
### Switch active model
Edit `opencode.json` (project-level or `~/.config/opencode/opencode.json`) and
change the agent's `model` to `llama-server/<model-id>`:
```json
"agent": {
"orchestrator": {
"model": "llama-server/Qwen_Qwen3-14B-Q4_K_M"
}
}
```
The next request triggers a cold load of the new model (~1030s for 14B, ~3060s
for 27B+). No service restart needed. `--models-max 1` ensures the previous
model is evicted from VRAM automatically.
To switch from the CLI without editing files:
```bash
opencode run -m "llama-server/Qwen_Qwen3-14B-Q4_K_M" "your message here"
```
### Multimodal models
For models with a separate vision encoder (mmproj), use a **subdirectory** in
`~/models/`. The directory name becomes the model ID; llama.cpp auto-detects any
file whose name starts with `mmproj` as the projector.
```
~/models/
OmniCoder-2-9B.Q8_0/ ← model ID = "OmniCoder-2-9B.Q8_0"
OmniCoder-2-9B.Q8_0.gguf ← main weights
mmproj-Q8_0.gguf ← vision projector (auto-detected)
```
### List available models
```bash
# See what's in ~/models/ (all are immediately usable as model IDs)
ls ~/models/
# See what's currently loaded
curl -s http://127.0.0.1:8080/v1/models | node -e \
"process.stdin.resume(); let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>JSON.parse(d).data.forEach(m=>console.log(m.id, m.meta?.loaded ? '[loaded]' : '[unloaded]')))"
# Force a rescan (picks up newly added model files)
curl -s 'http://127.0.0.1:8080/models?reload=1' | node -e \
"process.stdin.resume(); let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>JSON.parse(d).data.forEach(m=>console.log(m.id)))"
```
### Auto-restart on presets.ini change
The router caches `presets.ini` at startup, so any edit requires a service
restart to take effect. You can automate this with a systemd **path unit** that
watches the file and triggers a restart whenever it is written:
```bash
sudo tee /etc/systemd/system/llama-server-presets.path > /dev/null << 'EOF'
[Unit]
Description=Restart llama-server when presets.ini changes
[Path]
PathChanged=/home/dev/models/presets.ini
[Install]
WantedBy=default.target
EOF
sudo tee /etc/systemd/system/llama-server-presets.service > /dev/null << 'EOF'
[Unit]
Description=Restart llama-server (triggered by presets.ini change)
[Service]
Type=oneshot
ExecStart=/bin/systemctl restart llama-server
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now llama-server-presets.path
```
After this, saving `~/models/presets.ini` automatically restarts the service (~3
s) and the next inference request cold-loads the model with the new settings.
The restart is intentionally disruptive — the currently-loaded model is evicted
— so only enable this if disruptive restarts on every presets save are
acceptable.
---
## MTP speculative decoding
Multi-Token Prediction (MTP) lets the model predict several tokens per forward
pass using draft heads baked into the model weights — no separate draft model
needed. For Qwen3.6-35B-A3B this roughly doubles throughput (from ~15 tok/s to
~2535 tok/s on RTX 3080) while preserving output quality.
**Requirements:**
1. **b9279+ binary**`--spec-type draft-mtp` was added in this era. Verify:
```bash
/opt/llama-server/llama-server --help | grep spec-type
# must list draft-mtp
```
2. **MTP-format GGUF** — standard bartowski/unsloth quants do not include MTP
heads. Use byteshape's dedicated MTP GGUFs:
```bash
# IQ3_S (13.6 GB) — best quality/size for 12 GB VRAM with slight CPU offload
wget -c "https://huggingface.co/byteshape/Qwen3.6-35B-A3B-MTP-GGUF/resolve/main/Qwen3.6-35B-A3B-IQ3_S-3.06bpw.gguf" \
-O ~/models/Qwen3.6-35B-A3B-IQ3_S-3.06bpw.gguf
# IQ2_S (10 GB) — fully fits in VRAM; heavier quantization
wget -c "https://huggingface.co/byteshape/Qwen3.6-35B-A3B-MTP-GGUF/resolve/main/Qwen3.6-35B-A3B-IQ2_S-2.25bpw.gguf" \
-O ~/models/Qwen3.6-35B-A3B-IQ2_S-2.25bpw.gguf
```
**`presets.ini` section for MTP:**
```ini
[Qwen3.6-35B-A3B-IQ3_S-3.06bpw]
ctx-size = 32768
n-predict = 4096
spec-type = draft-mtp
spec-draft-p-min = 0.75
spec-draft-n-max = 3
```
- `spec-draft-p-min` — minimum draft token acceptance probability. 0.75 is a
good starting point; lower values accept more speculative tokens (faster but
may diverge from non-speculative output).
- `spec-draft-n-max` — maximum tokens to speculate per step. 3 is the sweet spot
for Qwen3.6 MTP; higher values have diminishing returns and add overhead.
**Note:** ik_llama.cpp (a fork) achieves ~1020% higher throughput with MTP than
official llama.cpp due to a more optimized MTP head implementation. Official
llama.cpp MTP is still significantly faster than non-speculative inference and
is the simpler setup.
---
## Troubleshooting
### Active model keeps resetting to the configured default
Known opencode bug [#28735](https://github.com/anomalyco/opencode/issues/28735)
(open as of May 2026): when a background subagent result is delivered back into
the main session, the active model resets to whatever `orchestrator.model` is
configured in `opencode.json`. This means any model switch made via `-m` flag or
the TUI selector gets silently reverted whenever a tool call or subagent
completes.
**Workaround:** keep `orchestrator.model` in `opencode.json` set to the model
you actually want to use. The reset lands on the configured model, so if it
matches your intent there's no observable effect.
---
### `no backends are loaded` at startup
The backend `.so` plugins must be in the same directory as the binary, or on
`LD_LIBRARY_PATH`. The `start.sh` script sets this explicitly.
### `make_cpu_buft_list: no CPU backend found`
Install `libgomp1` (OpenMP runtime — required by the CPU backend):
```bash
sudo apt-get install -y libgomp1
```
### CUDA device not found / GPU not offloading
- Confirm `/usr/lib/wsl/lib` is in `PATH` or `LD_LIBRARY_PATH` for the process
- Run `nvidia-smi` as the service user: `sudo -u ollama nvidia-smi`
- Check `journalctl -u llama-server -n 50` for lines like
`ggml_cuda_init: CUDA not found`
### High CPU / fan noise at idle
- Remove `--no-mmap` if present (forces 9GB into RAM on startup)
- Check `--n-parallel` isn't set high (default 1 is fine for single-user use)
- llama-server is permanently loaded; fans will spin during model load (~30s)
then drop to zero at idle — this is expected behavior
### `qwen35` architecture errors (rope, tensor shape, missing tensor)
These errors all indicate an **incompatible GGUF source**:
- `rope.dimension_sections has wrong array length; expected 4, got 3` — Ollama
stores a 3-element array; llama.cpp (before a patch) expects 4.
- `missing tensor 'blk.0.ssm_dt'` or `blk.0.ssm_dt.bias` — Ollama omits the
`.bias` suffix that HuggingFace-converted GGUFs use (or vice versa).
- `check_tensor_dims: wrong shape` on `blk.N.attn_k.weight` — Ollama's converter
stores `head_count_kv` as a per-layer array; llama.cpp's qwen35 model loader
expects a scalar.
**Solution:** use HuggingFace GGUFs (bartowski or unsloth) instead of Ollama
blobs for any `qwen35`-architecture model. See _Model choice_ above.
### Upgrading llama.cpp (replacing binaries while service is running)
The service holds the binary open; `cp` will fail with `Text file busy`. Always
stop the service first:
```bash
sudo systemctl stop llama-server
sudo cp build/bin/llama-server /opt/llama-server/
sudo cp -P build/bin/lib*.so* /opt/llama-server/
sudo systemctl start llama-server
```
### Model file permissions (service runs as `ollama` user)
Files downloaded as your user aren't readable by the `ollama` service user:
```bash
# Make model file readable by all
sudo chmod o+r ~/models/MyModel.gguf
# Make the directory traversable
sudo chmod o+x ~ ~/models
```

View File

@ -0,0 +1,514 @@
# How LLMs Interpret Intent in Text Prompts: Evidence-Based Guidance
> **Status:** Research synthesis. Companion to
> [`text-communication-interpretation.md`](./text-communication-interpretation.md)
> — that doc covers humans reading text; this one covers LLMs.
>
> **Scope:** Why current frontier and local models misinterpret prompts, what
> the underlying mechanisms are (training, architecture, quantization, position
> bias), and which counter-measures have empirical or vendor-documented support.
>
> **Models in scope (May 2026):** Claude Opus 4.7, Opus 4.6, Sonnet 4.6, Haiku
> 4.5; the Qwen2.5, Qwen3, and Qwen3.5 ("qwen35") families including the
> OmniCoder-9B fine-tune; and the current open-weight engineering tier (DeepSeek
> V4, Kimi K2.6, GLM-5, Mistral Small 4, Gemma 4).
>
> **Audience:** Engineers building agents, prompts, and scaffolding — not
> first-time LLM users.
---
## 0. Framing: Why Models Misread Prompts Differently Than Humans Do
Humans misread text mostly because of egocentric anchoring and emotional
projection (see the companion doc). LLMs misread for structurally different
reasons:
- **No persistent self.** Every turn re-derives "intent" from the visible token
stream. Anything outside the context window doesn't exist.
- **Distributional priors dominate.** The model's behavior is its training
distribution conditioned on your tokens. Ambiguity is resolved toward whatever
was most common in pretraining/RLHF, not toward what you meant.
- **Style → role.** Models infer _who_ is speaking from textual style rather
than from cryptographic provenance, which is why prompt injection works at all
(see §1.4). [13]
- **Quantization, depth, and routing change behavior under load**, not cleanly
and not always at the points you'd expect (see §3).
The practical consequence: the levers that work on humans (charity, delay,
perspective-taking) have direct analogs for LLMs — structured context, explicit
scope, separated reasoning — but for very different mechanistic reasons.
---
## 1. The Core Problem (Why This Is Hard)
### 1.1 Models resolve ambiguity toward the training prior
When intent is underspecified, models fall back to whatever the training
distribution made most likely. Anthropic explicitly documents that **Opus 4.7 is
more literal than 4.6**: it will not silently generalize an instruction from one
item to another, and will not infer requests you didn't make. [1] The upside is
precision; the downside is that prompts that worked on 4.6 by relying on
"obvious" generalization may stop working. Stating scope explicitly ("apply to
every section, not just the first") is now required, not optional.
### 1.2 Instruction following is not bit-width monotonic
Quantization does not uniformly degrade behavior. The Llama-3.1-8B-Instruct GGUF
sweep [3] shows:
- **GSM8K (reasoning):** F16 baseline 77.6; Q3*K_S drops to 68.3 (9.3);
Q4_K_S/M essentially match baseline; Q5/Q6/Q8 sometimes \_exceed* F16.
- **IFEval (instruction following):** F16 baseline 78.9; Q3*K_S drops to 73.9,
but Q4_K_S \_improves* to 80.3 and Q5_0 to 80.1. Q6_K drops to 77.6 and Q8_0
sits at 78.8 — i.e., higher bit-width does not guarantee better compliance.
**Practical floor:** for agentic / tool-using workflows, **45 bit K-quants
(Q4_K_M, Q5_K_M) are the safe band**; 3-bit risks reasoning collapse; 8-bit is
not automatically "best" for instruction following.
### 1.3 Long-context attention is U-shaped ("lost in the middle")
Liu et al. (TACL 2024) showed performance is highest when relevant information
is at the **beginning** or **end** of the context, with a sharp dip in the
middle — even for explicitly long-context models. [4] The effect persists across
Claude, GPT, and Llama lineages through early 2026. [5] Mechanism: training
documents are mostly short, and when long, important content tends to sit at the
boundaries; the model never learns strong middle-extraction habits.
**Implication:** the position of an instruction inside a 200K-token context
matters more than its wording. Put critical instructions at the top or just
before the user turn, not buried in the middle of system context.
### 1.4 Role confusion: style determines authority
Models do not robustly track _where text came from_; they infer the role of each
span from stylistic cues. Recent work on "CoT Forgery" [13] demonstrates that
injected reasoning traces that look like the model's own scratchpad inherit the
trust the model places in its own thoughts — external text, by contrast, is
normally scrutinized and rejected. This is the structural reason prompt
injection in tool outputs works.
**Implication:** any content you don't fully trust (tool output, fetched web
content, user-pasted text) must be wrapped in unambiguous structural markers,
and the model must be told what kind of content it is and how much authority it
carries.
### 1.5 Sycophancy / agreement bias
Some RLHF'd models lean toward agreeing with the user's framing, especially when
the user states a belief or pushes back. Sharma et al. (ICLR 2024) [14] found
this across five SOTA assistants and traced it to human preference labels
favoring agreement. **Important caveat:** the original Perez et al. (2022)
finding that sycophancy appears even at zero RLHF steps did **not** replicate
across model families — nostalgebraist (2023) [15] showed OpenAI base models are
not sycophantic at any size. So this is model-family- and
training-data-specific, not a universal RLHF property. Mitigations: ask for the
model's best answer _before_ revealing your view; explicitly invite
disagreement; in agent prompts, instruct "persist through genuine blockers; do
not pivot just because the previous attempt failed."
**Stronger mitigation — context isolation (S2A):** System 2 Attention (Weston &
Sukhbaatar, 2023) [20] shows that asking the LLM to first _rewrite_ its input
context — extracting only the portions relevant to the current query and
discarding irrelevant or opinionated material — measurably reduces sycophancy
and improves factuality across QA, math word problems, and longform generation.
The mechanism is direct: soft attention in Transformers is susceptible to
incorporating irrelevant prior context; explicit isolation severs the anchor
before generation. In a harness context, the full two-pass S2A (rewrite then
respond) requires a second LLM call; the lightweight equivalent is placing an
explicit current-question marker at the context tail (recency- bias zone), which
isolates the current query from prior anchor answers without a second inference
pass.
---
## 2. Highest-Leverage Counter-Practices
Ranked by effect size and breadth of support across vendor docs, peer- reviewed
work, and field practice.
### 2.1 Be literal and explicit; state scope
Anthropic's official guidance for 4.6/4.7: "Claude responds well to clear,
explicit instructions. Being specific about your desired output can help enhance
results. If you want 'above and beyond' behavior, explicitly request it rather
than relying on the model to infer it from vague prompts." [1] This is the
single most-cited lever in their docs.
Apply equally to Qwen3-class local models, whose Apache-2.0 instruct tunes are
now competitive at instruction-following but show the same literal-by-default
behavior as Claude 4.7. [2]
### 2.2 Use XML (or unambiguous) structural tags around heterogeneous content
Wrapping each kind of input — instructions, examples, retrieved context, user
query, tool output — in its own tag reduces misinterpretation because the model
can attend to "tag boundaries" rather than guessing where one block ends and
another begins. [1] This is the cheapest mitigation for §1.3
(lost-in-the-middle) and §1.4 (role confusion) simultaneously.
### 2.3 Provide context and motivation, not just the instruction
Vendor-documented (Anthropic) and consistently effective: explaining _why_
improves targeting. [1][6] Mechanism: motivation tokens disambiguate which
training prior to condition on. A request to "make this shorter" with context
"for a P0 incident page, every line costs attention" lands in a different region
of model behavior than the same request without justification.
### 2.4 Prefer general reasoning instructions over prescriptive steps —
**for reasoning-capable models**
Anthropic: "A prompt like 'think thoroughly' often produces better reasoning
than a hand-written step-by-step plan. Claude's reasoning frequently exceeds
what a human would prescribe." [1] Qwen3's thinking mode is similarly designed
to be triggered with light cues (`/think`) rather than micromanaged. [2]
For **non-reasoning** models (or thinking-off mode), the Prompting Science
Report 2 [7] finds chain-of-thought provides only a small average boost and
**increases variance** — sometimes flipping previously-correct answers to wrong.
For reasoning models the explicit CoT request is essentially zero-value and just
burns tokens.
**Additional caveat — subjective tasks:** arXiv:2409.06173 (2024) [16] shows CoT
suffers from _posterior collapse_: the format of CoT retrieves reasoning priors
that remain relatively unchanged despite the evidence in the prompt. This is
especially pronounced on subjective tasks (emotion, morality) and on larger
models. So for intent-interpretation tasks — exactly the kind this doc is about
— CoT may actively entrench the model's prior reading rather than update it on
new evidence. Prefer perspective-taking prompts (see §2.4a) or
clarifying-question prompts over generic "think step by step" for ambiguous
intent.
### 2.5 Calibrate reasoning length to task complexity
"When More is Less" (Wang et al., 2025) [8] established an inverted-U: accuracy
rises with CoT length, then declines as error accumulation outpaces
decomposition benefit. Optimal length _increases_ with task difficulty and
_decreases_ with model capability. Practical rules:
- For Claude adaptive thinking (4.6/4.7): set the `effort` parameter to match
task complexity; do not push it higher than needed. [1]
- For Qwen3: use the `thinking_budget` mechanism rather than letting thinking
run unbounded. [2]
- For small local models (≤9B): prefer many short reasoning steps in multiple
turns over one long monolithic chain.
### 2.6 Default-to-action vs. default-to-clarify is promptable
Anthropic publishes both directions verbatim. For agent work:
> By default, implement changes rather than only suggesting them. If the user's
> intent is unclear, infer the most useful likely action and proceed, using
> tools to discover any missing details instead of guessing. [1]
For research/exploration work, invert it: instruct the model to clarify or plan
before acting. The point is that "agentic-ness" is a prompt-controlled dial, not
a model property.
### 2.7 Place critical instructions at the boundaries of the context
Direct consequence of §1.3. The top of the system prompt and the position
immediately preceding the user's most recent turn are the high-attention zones.
Anthropic, Cursor, and Aider all converge on this in practice — system prompts
grow at the top, repo-map / recent-turn context grows just before the user
message.
**Stronger form — full context recontextualization (S2A [20]):** if the context
contains opinionated or anchor-setting material that will skew the answer, the
boundary-placement advice is necessary but not sufficient. S2A's two-pass
pattern (rewrite context to strip irrelevant content → generate from rewritten
context) further reduces the effect of prior anchors. For agent harnesses where
a second LLM call is too expensive, the single-pass equivalent is an explicit
current-question isolation instruction injected at the context tail — same
recency zone, same isolation intent, no extra inference. [20]
### 2.8 Truncate and structure tool output aggressively
Local-model failure modes documented in this repo's own
[`agent-infrastructure.md`](../projects/agent-infrastructure.md) match the
broader pattern: tool-call history is the largest context consumer, and
untruncated outputs both push content into the lost-in-the-middle zone _and_
widen the prompt-injection attack surface (§1.4). The repo's ~1500-token
post-tool-use truncation is consistent with what the Cursor and Aider teams have
published.
### 2.9 Lower temperature for tool-calling / structured output
Convergent vendor guidance across Anthropic, Qwen, and Tesslate (OmniCoder): for
tool-calling and JSON-emitting paths, temperature 0.20.4 substantially reduces
schema violations and hallucinated arguments. [10] This effect is amplified in
quantized models where sampling noise compounds with quantization noise.
### 2.10 Role / persona prompting is at best a weak intervention
A 2025 wave of replication-style studies converges on a folklore-busting result:
assigning expert personas ("you are a senior software engineer…") does not
reliably improve task performance, and in many cases hurts.
- **Principled Personas** (EMNLP 2025) [17]: across 9 SOTA models × 27 tasks,
expert personas usually give "positive or non-significant" effects, and models
are **highly sensitive to irrelevant persona details, with drops of almost 30
percentage points**.
- **Persona is a Double-Edged Sword** (IJCNLP Findings 2025) [18]: dataset-
aligned personas can hurt; only _instance_-aligned personas selected per-
query reliably help.
- **Persona-prompt evaluation across QA benchmarks** (arXiv:2512.05858) [19]:
"persona prompts generally did not improve accuracy" across both benchmarks
tested; low-knowledge personas (layperson, child) actively degrade results.
**Practical guidance:** do not rely on personas as a precision lever for intent
interpretation. If a persona is included for stylistic reasons (tone, register),
keep it minimal and avoid attributes that are irrelevant to the task. For
correctness, prefer the levers in §2.1§2.9.
---
## 3. Architecture, Parameters, and Quantization — What Actually Changes
### 3.1 Parameter count and "emergence"
The classical scaling-laws picture (Kaplan, Chinchilla) holds for loss, but
emergent _capabilities_ are noisier than originally reported. "Distributional
Scaling Laws for Emergent Capabilities" (2025) [9] shows that at scales near a
capability threshold, performance across random seeds is **bimodal** — some runs
acquire the skill, some don't — so "emergence" at a given scale is partly
stochastic. Bigger models collapse the bimodal distribution and acquire skills
more reliably.
Practical implication for choosing model size:
- **≤4B:** reliable for narrow extraction, classification, short agentic steps;
instruction following degrades sharply with prompt length and as context
fills.
- **714B (incl. OmniCoder-9B):** the current sweet spot for local engineering
work. Tool-calling and structured output work reliably when the prompt is
well-structured; reasoning is acceptable; long- horizon plans drift.
- **3070B dense / 100400B MoE:** comparable behavior to mid-tier cloud models
on most tasks; remaining gaps are agentic (BrowseComp, TerminalBench, OSWorld)
where open models still trail. [11]
### 3.2 Dense vs. Mixture-of-Experts
Shen et al. (ICLR 2024, "FLAN-MoE") [12] established a counter-intuitive result
that still holds: **MoE models underperform dense models of equivalent FLOPs
when only directly fine-tuned, but surpass them dramatically after instruction
tuning** — and benefit _more_ from instruction tuning than dense models do.
FLAN-MoE-32B beat Flan-PaLM-62B on four benchmarks at ⅓ the FLOPs.
Practical implications for prompt design:
- MoE models (DeepSeek V4, Kimi K2.6, GLM-5, Qwen3 235B-A22B) are more sensitive
to instruction _style_ matching their tuning distribution. Clean, structured
prompts pay off more than on dense models.
- Routing instability shows up as occasional out-of-distribution responses on
edge cases. Few-shot examples are an effective stabilizer because they shift
activation into well-traveled expert combinations.
- Active-parameter count (e.g., 22B active in Qwen3-235B-A22B) is the better
predictor of per-token latency and small-task quality than total parameter
count.
### 3.3 Quantization
Detailed numbers in §1.2. Summary heuristics:
| Bit-width | Reasoning (GSM8K) | Instruction (IFEval) | Recommendation |
| ----------- | ----------------- | -------------------- | ------------------------------- |
| Q3_K_S/M | Notable drop | Variable, often drop | Avoid for agents |
| Q4_K_S/M | ~Baseline | Often ≥ baseline | Default for local agents |
| Q5_K_M | ≥ Baseline | ≥ Baseline | Best quality/size trade-off [3] |
| Q6_K | ≥ Baseline | Sometimes slight dip | Use if VRAM allows |
| Q8_0 / bf16 | Baseline | Baseline | No guaranteed advantage over Q5 |
Calibration-aware methods (AWQ, GPTQ with good calibration data, EXL2) generally
outperform naive GGUF at the same bit-width; for instruction- heavy work, prefer
K-quants over legacy `_0` / `_1` quants. [3]
### 3.4 Architecture variants worth knowing in 2026
- **Standard Transformer + GQA:** still the default (Llama, Mistral, most
Qwen2/2.5).
- **Hybrid attention (Qwen3.5 / "qwen35" / OmniCoder backbone):** Gated Delta
Networks interleaved with standard attention; enables efficient 262K native
context with extension to 1M+. [10] In practice this changes the
lost-in-the-middle profile somewhat but does not eliminate it — the same
boundary-placement advice applies.
- **Thinking-mode fusion (Qwen3):** a single model trained for both reasoning
and direct response, switched by `/think` and `/no_think` flags in user/system
messages, with an emergent "stop thinking now" capability used by the
`thinking_budget` controller. [2]
---
## 4. Model-Specific Notes (May 2026)
### Claude Opus 4.7 / Opus 4.6 / Sonnet 4.6 / Haiku 4.5
- **Opus 4.7 is more literal than 4.6 at low effort.** Prompts tuned for 4.6 may
need scope made explicit on 4.7. [1]
- Adaptive thinking is the default; do not hand-write step-by-step plans unless
the task is genuinely procedural. [1]
- The "default-to-action" / "default-to-clarify" prompt is the highest- leverage
knob for changing agent behavior without changing model. [1]
- Subagent delegation (Opus parent → Sonnet/Haiku children) is
cheaper-and-comparable for isolated subtasks; the parent retains reasoning,
the children execute.
### Qwen3 family (0.6B 235B, dense + MoE; Qwen3.5 hybrid)
- Two-mode model: `/think` and `/no_think` flags toggle reasoning;
`thinking_budget` caps token spend. [2]
- Instruction following on Qwen3 instruct surpasses Qwen2.5 instruct, especially
in non-thinking mode. [2]
- Multilingual support jumped from 29 languages (Qwen2.5) to 119 (Qwen3). [2]
- Qwen3.5 (the "qwen35" architecture, base for OmniCoder-9B) introduces hybrid
Gated Delta + standard attention, 262K native context. [10]
### OmniCoder 2 / OmniCoder-9B (Tesslate, Qwen3.5-9B base)
- Fine-tuned on 425K agentic trajectories distilled from Claude Opus 4.6,
GPT-5.3-Codex, GPT-5.4, Gemini 3.1 Pro on Claude Code, OpenCode, Codex, and
Droid scaffolding. [10]
- Specifically learned read-before-write, LSP-diagnostic response, and
minimal-diff edits.
- Tesslate's own guidance: temperature 0.20.4 for agentic / tool use.
- Failure modes documented in this repo:
[`agent-infrastructure.md`](../projects/agent-infrastructure.md) §
"Smaller-scale local models" — narrower training distribution (Python/JS
heavy), JSON-schema compliance drops as context fills, instruction drift
faster than larger Qwen3 due to fewer attention heads.
### Other engineering-capable local models (2026 tier)
- **DeepSeek V4 Pro (Max), Kimi K2.6, GLM-5:** current open-weight ceiling;
strong on coding/agentic, still trail proprietary models on BrowseComp,
TerminalBench, OSWorld. [11]
- **Qwen3.5 397B (Reasoning):** competitive with the above at reasoning-heavy
work.
- **Mistral Small 4 (24B, 256K ctx):** best quality-to-resource ratio for
single-GPU deployments; Apache 2.0.
- **Gemma 4 31B (256K ctx):** strong LiveCodeBench; single high-end consumer GPU
viable.
- **Llama 4 (Maverick/Scout):** now trails the Chinese open-weight leaders on
benchmarks but retains ecosystem advantages. [11]
---
## 5. Minimal Operating Checklist
When writing a prompt or system message for any of these models:
1. **State scope and motivation explicitly.** Don't expect generalization.
2. **Structure heterogeneous content with tags.** Especially anything from a
tool or external source.
3. **Put critical instructions at the boundaries** (top of system, or
immediately before user turn) — not buried.
4. **Pick reasoning intensity deliberately.** Adaptive/`thinking_budget` for
capable models; multi-turn small steps for ≤9B locals; skip forced CoT on
reasoning models.
5. **Truncate tool output** and never paste untrusted text without a wrapper
that names its provenance.
6. **For tool-calling: lower temperature** (0.20.4) regardless of model.
7. **For local deployments: target Q4_K_M or Q5_K_M.** Verify on IFEval-style
tests, not just perplexity.
8. **Ask for the answer before stating your own view** to avoid sycophantic
agreement.
---
## 6. What the Evidence Does _Not_ Support
- **"Just use a bigger model."** Architecture, instruction tuning, and prompt
structure account for as much variance as raw parameter count for most
engineering tasks. [9][12]
- **"Always use chain-of-thought."** Outdated. Marginal for non- reasoning
models, near-zero for reasoning models, and CoT _increases answer variance_
flipping some correct answers to wrong. [7][8]
- **"Higher quantization is always better."** IFEval is not bit-width monotonic;
Q4_K_S can beat Q8_0 on compliance. [3]
- **"MoE > dense at equivalent total params."** Without instruction tuning, MoE
underperforms dense at equal FLOPs. [12]
- **"Role-play personas reliably steer behavior."** Style-based role cues are
exactly what prompt-injection attacks exploit; do not rely on persona prompts
for security boundaries. [13] **Stronger version of this debunk:** persona
prompts also don't reliably improve _task performance_ — they're often
ineffective and frequently harmful when persona attributes are even mildly
irrelevant to the task. [17][18][19] See §2.10.
- **"Longer reasoning is better reasoning."** Inverted-U on accuracy vs. CoT
length is well-established. [8]
---
## 7. Sources
The foundational survey of prompting techniques used to cross-check claims in
this doc is **Schulhoff et al. (2024), _The Prompt Report: A Systematic Survey
of Prompting Techniques_** (arXiv:2406.06608). PRISMA-based review of 1,565
papers; taxonomy of 58 text prompting techniques. Cited as [PR] where relevant.
1. Anthropic. _Prompting best practices_ (covers Opus 4.7, 4.6, Sonnet 4.6,
Haiku 4.5). Claude API Docs.
https://platform.claude.com/docs/en/docs/build-with-claude/prompt-engineering/be-clear-and-direct
2. Yang, A. et al. (2025). _Qwen3 Technical Report._ arXiv:2505.09388. (Dense +
MoE family 0.6B235B; thinking-mode fusion; thinking budget; 119-language
support.)
3. _Which Quantization Should I Use? A Unified Evaluation of llama.cpp
Quantization on Llama-3.1-8B-Instruct._ arXiv preprint. (GSM8K, IFEval, MMLU,
HellaSwag, TruthfulQA across all GGUF variants.)
4. Liu, N. F. et al. (2024). _Lost in the Middle: How Language Models Use Long
Contexts._ TACL 12, 157173.
5. The Neural Base. _Lost-in-middle behavior across major models through
early 2026._ (Replication note; U-shaped curve persists across Claude, GPT,
Llama.)
6. Anthropic. _Prompt engineering for business performance._
https://www.anthropic.com/news/prompt-engineering-for-business-performance
7. Meincke, L. et al. (2025). _Prompting Science Report 2: The Decreasing Value
of Chain of Thought in Prompting._ arXiv:2506.07142.
8. Wang, Y. et al. (2025). _When More is Less: Understanding Chain-of-Thought
Length in LLMs._ arXiv:2502.07266.
9. _Distributional Scaling Laws for Emergent Capabilities._ (2025)
arXiv:2502.17356. (Bimodal performance distributions near capability
thresholds; "emergence" as stochastic property at scale.)
10. Tesslate. _OmniCoder-9B model card._ Hugging Face, March 2026. (Qwen3.5-9B
base; 425K agentic trajectories from Claude Opus 4.6, GPT-5.3-Codex,
GPT-5.4, Gemini 3.1 Pro; Gated Delta + attention hybrid; 262K context;
recommended temperature 0.20.4 for tool use.)
https://huggingface.co/Tesslate/OmniCoder-9B
11. BenchLM.ai. _Best Open Source LLM in 2026: Rankings, Benchmarks, and the
Models Worth Running._ April 2026. (DeepSeek V4 Pro, Kimi K2.6, GLM-5,
Qwen3.5 397B, Mistral Small 4, Gemma 4, Llama 4 comparison.)
12. Shen, S. et al. (2024). _Mixture-of-Experts Meets Instruction Tuning: A
Winning Combination for Large Language Models._ ICLR. (FLAN-MoE-32B vs
Flan-PaLM-62B; MoE benefits more from instruction tuning than dense.)
13. _Role Confusion and CoT Forgery: Stylistic Spoofing as a Prompt- Injection
Mechanism._ arXiv preprint, 2026. (Models infer roles from style; forged
reasoning traces inherit self-trust.)
14. Sharma, M. et al. (2024). _Towards Understanding Sycophancy in Language
Models._ ICLR 2024. arXiv:2310.13548.
15. nostalgebraist (2023). _OpenAI API base models are not sycophantic, at any
size._ LessWrong.
https://www.lesswrong.com/posts/3ou8DayvDXxufkjHD/openai-api-base-models-are-not-sycophantic-at-any-size
(Disconfirms the strongest reading of Perez et al. 2022 for OpenAI base
models. Not peer-reviewed but the data and code are public.)
16. _Chain-of-Thought is not all you need: Posterior collapse of CoT under
distributional shift._ arXiv:2409.06173 (2024). (Larger models anchor harder
to reasoning priors under CoT, especially on subjective tasks.)
17. _Principled Personas: Defining and Measuring the Intended Effects of Persona
Prompting on Task Performance._ EMNLP 2025.
https://aclanthology.org/2025.emnlp-main.1364/
18. _Persona is a Double-Edged Sword: Rethinking the Impact of Role-play Prompts
in Zero-shot Reasoning Tasks._ IJCNLP Findings 2025.
https://aclanthology.org/2025.findings-ijcnlp.51/
19. _When personas help and when they don't: A persona-prompt evaluation across
QA benchmarks._ arXiv:2512.05858 (2025). PR. Schulhoff, S. et al. (2024).
_The Prompt Report: A Systematic Survey of Prompting Techniques._
arXiv:2406.06608. PRISMA review of 1,565 papers; taxonomy of 58 prompting
techniques.
20. Weston, J. & Sukhbaatar, S. (2023). _System 2 Attention (is something you
might need too)._ arXiv:2311.11829. (Two-pass technique: LLM first rewrites
input context to remove irrelevant/opinionated material, then generates
response from cleaned context. Reduces sycophancy and increases factuality
on QA, math word problems, and longform generation. The lightweight harness
equivalent is a current-question isolation instruction at the context tail.)

View File

@ -0,0 +1,229 @@
# Interpreting Text-Based Communication: Evidence-Based Guidance
> **Status:** Research synthesis. Focus: what psychology, organizational
> behavior, and negotiation training have demonstrated _works_ for **readers**
> trying to accurately interpret text-only messages (email, chat, SMS,
> forum/Discord posts) where vocal tone and body language are absent.
>
> **Scope:** Receiver-side interpretation. Writing/composition guidance is
> mentioned only where it informs how readers should _decode_.
>
> **Audience:** Adults seeking concrete, proven practices — not a literature
> review.
---
## 1. The Core Problem (Why This Is Hard)
Three well-replicated findings frame everything else:
1. **Senders systematically overestimate how clearly tone comes through.**
Kruger, Epley, Parker, & Ng (2005) had participants send sarcastic vs.
serious emails. Senders predicted recipients would detect tone ~78% of the
time; recipients actually performed at chance (~56%). Senders cannot
"uncouple" their own internal voice from the bare text — an egocentric
anchoring effect. [1]
2. **Receivers exhibit a negativity bias in CMC.** Byron (2008) synthesized
evidence that neutral emails tend to be read as negative, and positive emails
as neutral. Absent paralinguistic warmth cues, the brain fills the gap
pessimistically — especially under stress, fatigue, or status asymmetry. [2]
3. **Hostile attribution bias amplifies #2.** Individuals predisposed to read
hostility into ambiguous behavior (Dodge, 1980 and follow-ups) do so even
more in text, because there are fewer disconfirming cues. [3] Aderka et al.
(2016) showed this directly in a CMC context: ambiguous text messages are
read more negatively by socially anxious receivers, validating a text-
specific interpretation-bias measure (IB-CMC). [4]
**Implication for readers:** Your first emotional reading of an ambiguous
message is statistically likely to be _more negative_ than the sender intended.
Treat that first reading as a hypothesis, not a fact.
---
## 2. The Highest-Leverage Practices (What Actually Works)
Filtered to interventions with empirical support _or_ adoption in professional
training programs (FBI crisis negotiation, clinical psychology, mediation,
executive coaching). Ordered by effect size and ease of adoption.
### 2.1 Delay before responding to anything that triggered you
The single most-recommended practice across clinical, negotiation, and
organizational sources. Even a short pause (minutes for chat, hours for email)
lets the amygdala-driven first reading subside and the prefrontal cortex
re-engage. Crucial Conversations (Patterson et al.) calls this "getting out of
your story"; CBT calls it "cognitive defusion." [5][6]
> Rule of thumb used in mediation training: **if your pulse is up, don't hit
> send.**
### 2.2 Generate at least two alternative interpretations
Explicit perspective-taking — being instructed to consider the sender's
situation, constraints, and likely state — measurably reduces hostile
attributions and stereotype-driven inferences (Galinsky & Moskowitz, 2000). This
generalizes directly to text. [7]
Concrete prompt to use on yourself:
> _"If a person I trusted and respected sent me this exact message, what would I
> assume they meant?"_
This is a behavioral form of the **Principle of Charity** (Rapoport's rules,
popularized by Dennett): restate the message in its strongest, most reasonable
form before reacting. [8]
### 2.3 Separate observation from interpretation (NVC / CBT overlap)
Nonviolent Communication (Rosenberg, 2003) and Cognitive Behavioral Therapy
(Beck; Burns, _Feeling Good_) independently converge on the same move:
- **Observation:** What words are literally on the screen?
- **Interpretation/Story:** What am I adding (intent, tone, motive)?
- **Feeling:** What am I feeling in response?
- **Check:** Which CBT distortion am I running? (Mind-reading, catastrophizing,
personalization, all-or-nothing.) [6][9]
In text-only contexts the gap between observation and interpretation is where
~all miscommunication lives. Naming the gap shrinks it.
### 2.4 Label the emotion you're inferring — and verify it
From FBI crisis negotiation training (Behavioral Change Stairway Model; Vecchi,
Van Hasselt, & Romano, 2005) and popularized by Chris Voss: state your read of
the other person's emotion tentatively and invite correction. [10][11] The
mechanism is well-grounded: Lieberman et al. (2007) showed via fMRI that putting
feelings into words ("affect labeling") measurably reduces amygdala activity and
recruits the right ventrolateral prefrontal cortex — i.e., labeling literally
down-regulates the threat response, in yourself and (by co-regulation) the
sender. [12]
Templates that work:
- _"It sounds like you're frustrated that X — is that right?"_
- _"I'm reading this as [interpretation]. Did I get that right?"_
- _"I want to make sure I'm not misreading — are you [annoyed / asking / venting
/ blocked]?"_
This does two things receivers consistently undervalue:
1. Surfaces your interpretation _as_ an interpretation (cheap to correct).
2. Signals attention, which de-escalates regardless of whether your read was
right.
### 2.5 Ask one clarifying question instead of responding to the inferred message
Byron's (2008) explicit recommendation, echoed in mediation literature: when
emotional content is ambiguous, **respond with a question, not a reaction**.
This is also the cheapest way to avoid the Kruger/Epley failure mode — because
the sender's egocentric blindness means they often don't realize they were
unclear until asked. [1][2]
This is one of two practices (with §2.4) supported by both behavioral and neural
evidence — it short-circuits the loop in which the receiver's inferred tone
hardens into "what was said."
### 2.6 Re-read the message a second time, slowly, before reacting
Reading literature (and standard mediator training) finds that a second reading
— particularly out loud, or after a delay — substantially reduces projection of
imagined tone. Skimming amplifies negativity bias because the reader's own
affect supplies the missing prosody. [2]
### 2.7 Match medium to message complexity (and switch when stuck)
Media richness theory (Daft & Lengel, 1986) and ~40 years of follow-up research
consistently find: **emotional, ambiguous, or high-stakes content exceeds text's
bandwidth.** If a thread has gone two rounds without converging, escalate to
voice/video. This isn't "giving up on text" — it's recognizing a known channel
limit. [13]
### 2.8 Account for the hyperpersonal effect in long-term text relationships
Walther's hyperpersonal model (1996) shows that in extended text-only
relationships, receivers tend to _idealize_ senders (filling in flattering
detail) — which makes eventual ruptures feel sharper than they should. Be aware
that your sense of "knowing" someone you've only ever texted is partly your own
construction. [14]
---
## 3. A Minimal Operating Checklist
When a text message lands and you feel a reaction:
1. **Pause.** Don't draft a response yet.
2. **Re-read.** Slowly. Once more.
3. **Name the gap.** What is literally written vs. what am I adding?
4. **Run charity.** What would I assume if a trusted friend wrote this?
5. **If still unclear: ask one labeled question.** ("Reading this as X — is that
right?")
6. **If two rounds don't resolve it: change channels.** Voice or video.
This checklist captures roughly 90% of what the cited training programs teach.
The remaining 10% is domain-specific (clinical, legal, hostage).
---
## 4. What the Evidence Does _Not_ Support
Worth flagging because these are commonly repeated but weak or unsupported:
- **"55% of communication is body language" (Mehrabian).** Frequently cited to
claim text is hopeless. Mehrabian's 1967 studies were about _incongruent_
single-word emotional cues and do not generalize. Mehrabian himself has
repeatedly disavowed the broad interpretation. [15]
- **Emoji/punctuation as a reliable tone fix.** They help disambiguate, but
studies (e.g., Riordan, 2017) find effects are modest and culture/age
dependent; they do not close the sender-receiver gap from §1. [16]
- **Personality-typing the sender (MBTI, DISC, etc.) to predict tone.**
Predictive validity for individual messages is essentially zero.
---
## 5. Sources
1. Kruger, J., Epley, N., Parker, J., & Ng, Z. (2005). _Egocentrism over e-mail:
Can we communicate as well as we think?_ Journal of Personality and Social
Psychology, 89(6), 925936.
2. Byron, K. (2008). _Carrying too heavy a load? The communication and
miscommunication of emotion by email._ Academy of Management Review, 33(2),
309327.
3. Dodge, K. A. (1980). _Social cognition and children's aggressive behavior._
Child Development, 51(1), 162170. (And the substantial
hostile-attribution-bias literature that followed.)
4. Aderka, I. M., et al. (2016). _RU mad @ me? Social anxiety and interpretation
of ambiguous text messages._ Computers in Human Behavior, 58, 362368.
(Validates a CMC-specific interpretation-bias measure; n=215 + n=353.)
5. Patterson, K., Grenny, J., McMillan, R., & Switzler, A. (2002). _Crucial
Conversations: Tools for Talking When Stakes Are High._ McGraw-Hill.
6. Burns, D. D. (1980/1999). _Feeling Good: The New Mood Therapy._ (Lay summary
of Beck's cognitive distortions.)
7. Galinsky, A. D., & Moskowitz, G. B. (2000). _Perspective-taking: Decreasing
stereotype expression, stereotype accessibility, and in-group favoritism._
Journal of Personality and Social Psychology, 78(4), 708724.
8. Dennett, D. C. (2013). _Intuition Pumps and Other Tools for Thinking_, Ch. on
"Rapoport's Rules." W. W. Norton.
9. Rosenberg, M. B. (2003). _Nonviolent Communication: A Language of Life_ (2nd
ed.). PuddleDancer Press.
10. Vecchi, G. M., Van Hasselt, V. B., & Romano, S. J. (2005). _Crisis (hostage)
negotiation: Current strategies and issues in high-risk conflict
resolution._ Aggression and Violent Behavior, 10(5), 533551.
11. Voss, C., & Raz, T. (2016). _Never Split the Difference._ HarperBusiness.
(Popular translation of FBI negotiator practice; useful for the "labeling"
and "mirroring" tactics.)
12. Lieberman, M. D., Eisenberger, N. I., Crockett, M. J., Tom, S. M., Pfeifer,
J. H., & Way, B. M. (2007). _Putting feelings into words: Affect labeling
disrupts amygdala activity in response to affective stimuli._ Psychological
Science, 18(5), 421428.
13. Daft, R. L., & Lengel, R. H. (1986). _Organizational information
requirements, media richness and structural design._ Management Science,
32(5), 554571.
14. Walther, J. B. (1996). _Computer-mediated communication: Impersonal,
interpersonal, and hyperpersonal interaction._ Communication Research,
23(1), 343.
15. Mehrabian, A. (1971). _Silent Messages._ Wadsworth. (See Mehrabian's own
subsequent clarifications disclaiming the "55/38/7" generalization.)
16. Riordan, M. A. (2017). _Emojis as tools for emotion work: Communicating
affect in text messages._ Journal of Language and Social Psychology, 36(5),
549567.

View File

@ -0,0 +1,148 @@
# Investigation: Text-Intent Interpretation (Human + LLM)
**Status:** investigating
**Orientation:** understand (mixed with mid-investigation methodology
correction)
**Created:** 2026-05-16
**Last Updated:** 2026-05-16
## Question
How do humans and LLMs (mis)interpret intent in text-only communication, and
what mitigations are supported by the literature? End goal: produce a concrete
action plan to counteract LLM intent-interpretation failures in this codebase.
## What We Know
- Three docs produced:
[text-communication-interpretation.md](../research/text-communication-interpretation.md),
[llm-intent-interpretation.md](../research/llm-intent-interpretation.md),
[human-llm-interpretation-overlap.md](../research/human-llm-interpretation-overlap.md).
- Methodology critique recorded in
[/memories/session/research-methodology-retrospective.md](/memories/session/research-methodology-retrospective.md).
- Five strongly-cited human↔LLM connections (primacy/recency↔serial position,
ELIZA/hyperpersonal, sycophancy↔social desirability via RLHF preference data,
perspective-taking↔SimToM, clarifying question↔CLAM).
- Bias-inheritance chain is two-stage (pretraining corpus vs. RLHF preference
labels) — Mina et al. 2024, Sharma et al. 2024.
## Hypotheses
- **[2026-05-16] H1:** Lost-in-the-middle is a clean human-primacy/ recency
analog in LLMs.
**Falsification:** find a replication where the U-shape doesn't hold or where
the mechanism is shown to be different.
**Result:** PARTIALLY ELIMINATED — Bilan et al. (arXiv:2508.07479, 2025) shows
U-shape only holds up to ~50% of context window; Mak (2025) shows
positional-embedding decay produces monotonic drop, not U-shape, in very-long
contexts. The analogy is real but narrower than I originally claimed.
- **[2026-05-16] H2:** RLHF preference labels cause sycophancy.
**Falsification:** find evidence that base models (no RLHF) are sycophantic,
or that some RLHF'd models are not.
**Result:** PARTIALLY ELIMINATED — nostalgebraist (LessWrong, 2023) replicated
Anthropic's sycophancy eval on OpenAI base models and found they are NOT
sycophantic at any size. Sycophancy depends on the specific finetuning data
and model family. Should be rephrased as "in some model families, RLHF
preference data amplifies a sycophancy signal that may also have pretraining
origins."
- **[2026-05-16] H3:** Role/persona prompting reliably improves LLM intent
interpretation.
**Falsification:** find published evidence persona prompting fails or is
irrelevant.
**Result:** ELIMINATED — three convergent 2025 papers (Persona is a
Double-Edged Sword IJCNLP 2025; Principled Personas EMNLP 2025;
arXiv:2512.05858) show persona prompts are mixed-to-ineffective and highly
sensitive to irrelevant details (up to ~30pp drops). This contradicts
widespread prompt-engineering folklore.
- **[2026-05-16] H4:** CoT reliably mitigates poor intent interpretation.
**Falsification:** find cases where CoT actively hurts or fails to help.
**Result:** PARTIALLY ELIMINATED — arXiv:2409.06173 shows CoT suffers from
posterior collapse: larger models anchor harder to reasoning priors under CoT,
particularly on subjective tasks (emotion, morality). Adds to the existing
inverted-U finding.
- **[2026-05-16] H5:** Pan et al. (arXiv:2308.03188) establishes that intrinsic
self-correction without external ground truth degrades or fails to improve
model performance.
**Falsification:** paper doesn't exist; conclusion is reversed or domain-
restricted in a way that doesn't support a general "no self-critique" claim.
**Result:** PARTIALLY CONFIRMED with citation correction — Pan et al.
2308.03188 exists and is a _survey_ by Liangming Pan et al. (UCSB, Aug 2023).
The _stronger primary_ citation for the "intrinsic self-correction degrades
performance" claim is Huang et al. arXiv:2310.01798 ("Large Language Models
Cannot Self-Correct Reasoning Yet," Google DeepMind / UIUC, Oct 2023): "LLMs
struggle to self-correct their responses without external feedback, and at
times, their performance even degrades after self-correction." Both citations
should appear; the strong claim should attribute to Huang et al.
- **[2026-05-16] H6:** Wu, Wu, Zou (ClashEval, 2024) shows adversarial reframing
/ lowering model confidence in a prior commitment reduces position- anchored
question drift.
**Falsification:** paper doesn't exist; paper is about general context-vs-
prior conflict and doesn't support the "lower confidence → adherence" claim;
effect is small or non-replicable.
**Result:** PARTIALLY CONFIRMED with scope caveat — ClashEval (NeurIPS 2024)
is real and the token-probability/adherence finding is supported: "the less
confident a model is in its initial response (via measuring token
probabilities), the more likely it is to adopt the information in the
retrieved content." SCOPE: ClashEval tested RAG (retrieved content vs prior
knowledge), NOT multi-turn anchoring on the model's own prior commitment. The
mechanism (lower confidence → higher context adherence) is plausibly
transferable, but the best-practices doc's claim extrapolates beyond the
paper's actual experiment.
- **[2026-05-16] H7:** Jiang et al. (2026) "Think-Anywhere" is a real published
paper introducing mid-sequence `<think>` insertion that catches errors a
pre-commit plan cannot foresee.
**Falsification:** paper does not exist (hallucinated citation); paper exists
but does not make the claimed mid-sequence intervention finding.
**Result:** CONFIRMED with metadata correction — "Think Anywhere in Code
Generation" (arXiv:2603.29957, Jiang et al., late 2025 / early 2026,
github.com/jiangxxxue/Think-Anywhere). Mechanism: special `<thinkanywhere>`
tokens via SFT + RL; key finding "LLMs tend to invoke thinking at positions
with higher entropy." The best-practices doc's "catches mid-implementation
off-by-one errors" framing is a mild over-specification of "on-demand
reasoning at high-entropy positions" but directionally accurate.
## Investigation Log
### 2026-05-16 — Initial three-doc production
- Orientation: understand
- What was examined: human-text-interpretation literature (Kruger, Byron,
Aderka, Walther, Lieberman), LLM prompting literature (Anthropic 4.7 docs, Liu
et al., Sharma et al., Wilf et al., Schulhoff Prompting Science Report 2).
- What was found: documented in the three research docs.
- What this means: descriptive synthesis available; no decision rules yet.
- Next step: methodology audit.
### 2026-05-16 — Methodology audit and adversarial second pass
- Orientation: diagnose
- What was examined: my own search behavior; ran the adversarial searches I
should have run originally.
- What was found: positive-bias in original search framing missed important
disconfirmations (H2, H3) and required qualifications (H1, H4); also missed
the foundational Schulhoff "Prompt Report" survey.
- What this means: prescriptive synthesis needs five concrete edits before it
can drive an action plan.
- Next step: apply edits, then review ai-coding-best-practices.md with the same
skepticism.
## Timing Notes
- Each Exa search: ~515s including read of first 40 lines of dump.
- Free-tier rate limit means searches must be sequential.
## Open Questions
- Are the (still uncited) parallels in §4 of the synthesis worth another
adversarial search pass, or accept as flagged "use with care"?
- Does `docs/research/ai-coding-best-practices.md` contain claims about persona
prompting or CoT that now need correction?
- What is the right format for the final action plan — checklist,
copilot-instructions edit, AGENTS.md addition, or a new
`.agents/instructions/` file?

View File

@ -0,0 +1,46 @@
{
"hooks": {
"UserPromptSubmit": [
{
"type": "command",
"command": ".agents/hooks/user-prompt-submit.sh",
"timeout": 5
}
],
"SessionStart": [
{
"type": "command",
"command": ".agents/hooks/session-start.sh",
"timeout": 10
}
],
"PreToolUse": [
{
"type": "command",
"command": ".agents/hooks/pre-tool-use.sh",
"timeout": 5
}
],
"PostToolUse": [
{
"type": "command",
"command": ".agents/hooks/post-tool-use.sh",
"timeout": 5
}
],
"PreCompact": [
{
"type": "command",
"command": ".agents/hooks/pre-compact.sh",
"timeout": 10
}
],
"Stop": [
{
"type": "command",
"command": ".agents/hooks/stop.sh",
"timeout": 5
}
]
}
}

View File

@ -0,0 +1,277 @@
import type { Plugin, TextPart } from '@opencode-ai/plugin';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
/**
* Agent support plugin for Remnant.
*
* Responsibilities:
* 1. experimental.chat.system.transform session-start.sh (once per session)
* 2. chat.message user-prompt-submit.sh (each turn)
* 3. tool.execute.before pre-tool-use.sh (project policy)
* 4. tool.execute.after post-tool-use.sh + context pressure warning
* 5. experimental.session.compacting pre-compact.sh
*
* Note: stop.sh has no equivalent OpenCode plugin event; it only fires in Copilot.
*/
// Approximate token estimate: 4 chars ≈ 1 token (conservative for code).
const CHARS_PER_TOKEN = 4;
const CONTEXT_LIMIT_TOKENS = 32768;
const PRESSURE_THRESHOLD = 0.7; // 70%
// build agent (local profile) truncates at 1500 tokens to respect OmniCoder's 32K context window.
// orchestrator gets a higher limit (2500) since it only reads, not edits.
// All other agents receive full tool responses.
const LOCAL_WORKER_MAX_TOKENS = 1500;
const LOCAL_ORCHESTRATOR_MAX_TOKENS = 2500;
function truncate(text: string, maxTokens: number): { text: string; truncated: boolean } {
const maxChars = maxTokens * CHARS_PER_TOKEN;
if (text.length <= maxChars) return { text, truncated: false };
return {
text:
text.slice(0, maxChars) +
`\n\n[Response truncated at ~${maxTokens} tokens. Use a more targeted query to retrieve the relevant section.]`,
truncated: true,
};
}
export const AgentSupportPlugin: Plugin = async ({ $, directory }) => {
// Resolve hooks relative to this plugin file's real path (resolves symlinks).
// This makes the plugin work both as a project-local plugin and as a global
// plugin installed via install.sh — in either case, hooks live in ../../hooks/
// relative to this file in the .agents/frameworks/opencode/ directory.
const hooksDir = resolve(dirname(fileURLToPath(import.meta.url)), '../../hooks');
// Running cumulative context size estimate (characters)
let contextCharsUsed = 0;
// Track sessions that have had session-start injected (system.transform fires every turn)
const initializedSessions = new Set<string>();
/** Parse the additionalContext string from a hook's JSON output. */
function parseAdditionalContext(hookOutput: string): string | undefined {
try {
const parsed = JSON.parse(hookOutput.trim()) as {
hookSpecificOutput?: { additionalContext?: string };
};
return parsed?.hookSpecificOutput?.additionalContext ?? undefined;
} catch (_error) {
return undefined;
}
}
async function runHook(scriptName: string, stdinJson?: string): Promise<string> {
const script = `${hooksDir}/${scriptName}`;
try {
const proc = stdinJson
? await $`bash ${script} < ${Buffer.from(stdinJson)}`.text()
: await $`bash ${script}`.text();
return proc;
} catch (_error) {
// DEBUG: log hook failures so silent catches don't hide enforcement bugs
try {
const fs = await import('node:fs');
fs.appendFileSync(
'/tmp/plugin-hook-errors.log',
JSON.stringify({ ts: new Date().toISOString(), script, error: String(_error) }) + '\n'
);
} catch (_e) {
// ignore
}
// Hooks are advisory — never block on hook failure
return '';
}
}
return {
// ── 1. Session start: inject project state into system prompt ────────────
// Uses system.transform rather than the defunct session.created event.
// Guarded by initializedSessions so it runs exactly once per session.
'experimental.chat.system.transform': async (input, output) => {
const sessionID = input.sessionID ?? 'unknown';
if (initializedSessions.has(sessionID)) return;
initializedSessions.add(sessionID);
const hookOutput = await runHook('session-start.sh');
const context = parseAdditionalContext(hookOutput);
if (context) output.system.push(context);
},
// ── 2. User prompt: task capture + CURRENT QUESTION + nudges ──────────────
// Equivalent to Copilot's UserPromptSubmit hook.
'chat.message': async (input, output) => {
const promptText = output.parts
.filter((p): p is TextPart => p.type === 'text')
.map(p => p.text)
.join('\n');
const hookOutput = await runHook('user-prompt-submit.sh', JSON.stringify({ prompt: promptText }));
const context = parseAdditionalContext(hookOutput);
if (context) {
output.parts.push({
id: `prt_${crypto.randomUUID()}`,
sessionID: input.sessionID,
messageID: input.messageID ?? crypto.randomUUID(),
type: 'text',
text: context,
synthetic: true,
});
}
},
// ── 3. Pre-tool-use ─────────────────────────────────────────────────────
'tool.execute.before': async (input, output) => {
const toolName = input.tool as string;
// ── read guards ───────────────────────────────────────────────────
if (toolName === 'read') {
const args = (output.args ?? {}) as { filePath?: string; offset?: number; limit?: number };
const filePath = args.filePath ?? '';
// package.json read guard:
// Reading workspace package.json files auto-loads nested AGENTS.md files
// via OpenCode's context injection, burning through the 32K context budget.
// Block package.json reads under apps/ and packages/ only.
if (/(^|\/)(apps|packages)\/[^/]+\/package\.json$/.test(filePath)) {
throw new Error(
'BLOCKED: Reading workspace package.json files auto-loads nested AGENTS.md files and exhausts the 32K context. Use `grep_search` to find the specific field you need (e.g. a dependency version or script name) instead of reading the whole file.'
);
}
// Pagination guard:
// Large sequential reads exhaust the 32K context window quickly.
// The OpenCode `read` tool uses `offset` (1-indexed start) and `limit` (max lines).
// Unbounded reads (no limit) default to 2000 lines — always blocked.
// docs/ files may read up to 500 lines; all other files are capped at 50.
// Directory reads (e.g. `Read .`) never carry a limit — skip the guard.
let isDirectory = false;
try {
const { statSync } = await import('node:fs');
isDirectory = statSync(filePath).isDirectory();
} catch (_error) {
// path doesn't exist or inaccessible — treat as file
}
if (!isDirectory) {
const isDocsFile = /(^|\/)docs\//.test(filePath);
const readLimit: number | undefined = args.limit;
if (readLimit === undefined) {
throw new Error(
isDocsFile
? `BLOCKED: Unbounded read (no limit) is prohibited. Specify offset and limit to read in ≤500-line chunks for docs/ files.`
: `BLOCKED: Unbounded read (no limit) is prohibited. Use grep_search first to find the relevant section, then read with offset and limit in ≤50-line chunks.`
);
}
const lineLimit = isDocsFile ? 500 : 50;
if (readLimit > lineLimit) {
throw new Error(
isDocsFile
? `BLOCKED: Read more than 500 lines at once is prohibited for docs/ files. Use offset and limit to paginate in ≤500-line chunks.`
: `BLOCKED: Read more than 50 lines at once is prohibited. Use offset and limit to paginate in ≤50-line chunks. For docs/ files the limit is 500 lines. Use grep_search first to find the right offset.`
);
}
}
}
// ── Task prompt size guard ─────────────────────────────────────────────
// The `task` tool has a JSON serialization limit. Embedding file contents
// or long inventories inline in a task prompt causes "Unterminated string"
// parse errors. Cap task prompts at 1200 chars — workers should be told
// WHICH files to read, not given the contents inline.
if (toolName === 'task') {
const args = (output.args ?? {}) as { prompt?: string };
const prompt = args.prompt ?? '';
if (prompt.length > 1200) {
throw new Error(
`BLOCKED (task prompt too long: ${prompt.length} chars, max 1200): Task prompts must not embed file contents, dependency lists, or long context inline — this causes JSON parse failures. Instead, tell the worker WHICH files to read and WHAT to do. Example: "Read the root package.json and all workspace package.json files, then update the Technology Stack section in README.md to match."`
);
}
}
// Shell out to pre-tool-use hook (project policy enforcement).
// Policies 112: command/file guards. Policy 13: read_file range limit
// (≤50 lines for source files, ≤500 for docs/). Deny = throws Error.
const hookInput = JSON.stringify({
tool_name: toolName,
tool_input: output.args ?? {},
});
const hookResult = await runHook('pre-tool-use.sh', hookInput);
// If the hook emitted a deny decision, surface it as an error
if (hookResult.includes('"permissionDecision": "deny"')) {
const match = hookResult.match(/"permissionDecisionReason":\s*"([^"]+)"/);
const reason = match?.[1] ?? 'Blocked by project policy (pre-tool-use hook).';
throw new Error(reason);
}
},
// ── 4. Post-tool-use ────────────────────────────────────────────────────
'tool.execute.after': async (input, output) => {
const response = output.response as string | undefined;
if (typeof response === 'string') {
// a) Response truncation — local agents (build/orchestrator) and any ollama/ model;
// orchestrator gets a higher limit since it only reads, not edits.
const agentName = typeof input.agent === 'string' ? input.agent : '';
const isLocalAgent =
agentName === 'build' ||
agentName === 'orchestrator' ||
(typeof input.model === 'string' && input.model.startsWith('ollama/'));
if (isLocalAgent) {
const isOrchestrator = agentName === 'orchestrator';
const maxTokens = isOrchestrator ? LOCAL_ORCHESTRATOR_MAX_TOKENS : LOCAL_WORKER_MAX_TOKENS;
const { text: truncated } = truncate(response, maxTokens);
output.response = truncated;
}
// b) Context pressure tracking — accumulate and inject warning when ≥70%
contextCharsUsed += response.length;
const charLimit = CONTEXT_LIMIT_TOKENS * CHARS_PER_TOKEN;
const pct = contextCharsUsed / charLimit;
if (pct >= PRESSURE_THRESHOLD) {
const pctDisplay = Math.round(pct * 100);
const pressure = `[CONTEXT PRESSURE: ~${pctDisplay}% used. Be concise. Prefer targeted tool calls. Write progress to NOTES.md before continuing.]`;
output.response = `${pressure}\n\n${output.response}`;
// Reset after injection so we don't spam every subsequent turn
contextCharsUsed = 0;
}
// c) Shell out to post-tool-use hook (metacognitive reminders, methodology)
const hookInput = JSON.stringify({
tool_name: input.tool,
tool_input: input.args ?? {},
tool_response: (output.response as string).slice(0, 500), // truncated for hook
});
await runHook('post-tool-use.sh', hookInput);
}
},
// ── 5. Pre-compact: export state before context summarization ─────────────
'experimental.session.compacting': async (input, output) => {
await runHook('pre-compact.sh');
output.prompt = `
You are a context summarizer for coding sessions. Summarize only the conversation history given do not answer it.
If a <previous-summary> block is present, update it: preserve still-true facts, remove stale ones, merge new facts.
Output exactly this Markdown structure. Keep every section even when empty. Use terse bullets, not prose. Preserve exact file paths, commands, error strings, and identifiers.
---
## Original Prompt
## Clarifications
## Constraints & Preferences
## Progress
### Done
### In Progress
### Blocked
## Key Decisions
## Next Steps
## Critical Context
## Relevant Files
---
For Clarifications: include only follow-ups that changed scope, added constraints, or redirected work. Do not mention that you are summarizing. Respond in the conversation's language.`;
},
};
};

172
.agents/hooks/post-tool-use.sh Executable file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env bash
# PostToolUse hook: inject methodology reminders after relevant tool actions.
# - Periodic self-check (weighted every ~15 effective write-calls)
# - After test failures: remind about hypothesis-first methodology
# - After reading docs/ or .md/.txt files: lift pagination restriction reminder
# - After editing docs/ or .md/.txt files: audit file size (warn if >500 lines)
# - After editing agent config files: verify with opencode agent list
# Project-specific reminders (e.g. BFF pattern, build gates): add a sibling
# hook file in the project's .agents/hooks/ directory.
# Priority filter: emit at most 2 reminders per tool call.
# Priority order: SELF-CHECK > DEBUGGING > path-scoped > tool-specific.
set -euo pipefail
# ── Tool call counter ────────────────────────────────────────────────────────
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
REPO_ID=$(printf '%s' "$REPO_ROOT" | md5sum | cut -c1-8 2>/dev/null || echo "default")
COUNT_FILE="/tmp/.opencode-tool-count-${REPO_ID}"
COUNT=$(cat "$COUNT_FILE" 2>/dev/null || echo 0)
# Read hook input from stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name"\s*:\s*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"/\1/' || true)
# Weighted increment: reads +1, writes/shell +4 (equivalent to +0.25/+1 at threshold 60).
# This prevents SELF-CHECK from firing mid-investigation sweep.
case "$TOOL_NAME" in
read_file|grep_search|list_dir|file_search|semantic_search|explore_subagent)
COUNT=$((COUNT + 1))
;;
*)
COUNT=$((COUNT + 4))
;;
esac
echo "$COUNT" > "$COUNT_FILE"
# Priority-ordered reminders array: append in priority order, emit max 2.
# Priority: SELF-CHECK(1) > DEBUGGING(2) > path-scoped(3) > tool-specific(4)
reminders=()
# ── Periodic self-check (every 60 weighted units ≡ 15 effective write-calls) ─
if (( COUNT % 60 == 0 )); then
selfcheck="SELF-CHECK (${COUNT} tool calls): Step back and assess."
selfcheck="${selfcheck} (1) What is your current goal — are you still on track?"
selfcheck="${selfcheck} (2) Are you making progress or spinning on the same issue?"
selfcheck="${selfcheck} (3) If you've hit 2+ failures on the same problem, switch to @research or report to the user."
selfcheck="${selfcheck} (4) If you've been editing the same file 3+ times without a passing test, stop and rethink."
selfcheck="${selfcheck} (5) Is the chat todo list accurate? Update it if items are stale or missing."
selfcheck="${selfcheck} (6) If investigating, re-read your investigation file and dead-ends to avoid re-testing eliminated hypotheses."
reminders+=("$selfcheck")
fi
# ── After test/terminal runs that failed: remind about methodology ───────────
if [[ "$TOOL_NAME" == "run_in_terminal" || "$TOOL_NAME" == "runTests" ]]; then
TOOL_RESPONSE=$(echo "$INPUT" | grep -o '"tool_response"\s*:\s*"[^"]*"' | head -1 || true)
if echo "$TOOL_RESPONSE" | grep -qiE 'FAIL|error|panic|segfault|assertion|abort|ERR!'; then
debug_msg="DEBUGGING REMINDER: Before your next action —"
debug_msg="${debug_msg} (1) Write your hypothesis in one sentence."
debug_msg="${debug_msg} (2) Write what you'd expect if WRONG."
debug_msg="${debug_msg} (3) Check the dead-ends file (.session/dead-ends.md) if it exists."
debug_msg="${debug_msg} (4) Falsify BEFORE confirming."
debug_msg="${debug_msg} (5) If 5+ attempts without progress, STOP and report what you've learned."
reminders+=("$debug_msg")
fi
fi
# ── After editing project/agent config files: path-scoped reminders ─────────
# Project-specific path checks (e.g. build gates, BFF reminders) belong in a
# sibling project-local hook file, not here. Only general checks below.
case "$TOOL_NAME" in
replace_string_in_file|multi_replace_string_in_file|create_file)
FILE_PATH=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
const p = i.filePath || (i.replacements && i.replacements[0] && i.replacements[0].filePath) || '';
process.stdout.write(p);
" 2>/dev/null || true)
path_msg=""
# ── After editing agent config files: verify with opencode agent list ─────
if echo "$FILE_PATH" | grep -qE '\.agents/agents/|\.opencode/agents/|opencode\.json'; then
AGENT_LIST=$(cd "$REPO_ROOT" && opencode agent list 2>&1 | grep -E '^\S.*\((all|primary|subagent)\)' | sed 's/^/ /' || echo " (opencode agent list failed)")
agent_note="AGENT CONFIG VERIFICATION: You just edited an agent definition or opencode.json."
agent_note="${agent_note} Registered agents are: ${AGENT_LIST}."
agent_note="${agent_note} If your agent is missing: (1) check that .opencode/agents/<name>.md symlink resolves (cat it — should not error); (2) symlink depth must be ../../.agents/agents/<name>.md (two levels, not three); (3) check YAML frontmatter for parse errors."
agent_note="${agent_note} Deny rules only appear in \`opencode agent list\` output if the agent file loaded correctly."
if [[ -n "$path_msg" ]]; then
path_msg="${path_msg} ${agent_note}"
else
path_msg="$agent_note"
fi
fi
# ── After editing docs/ or .md/.txt files: audit file size ───────────────
if echo "$FILE_PATH" | grep -qE '(^|/)docs/|\.md$|\.txt$'; then
if [[ -f "$FILE_PATH" ]]; then
LINE_COUNT=$(wc -l < "$FILE_PATH" 2>/dev/null || echo 0)
if [[ "$LINE_COUNT" -gt 500 ]]; then
docs_audit="DOCS SIZE AUDIT: ${FILE_PATH##*/} is now ${LINE_COUNT} lines. Consider splitting this doc — files over ~500 lines require expensive pagination for local models (17+ reads for an 800-line file). Split into focused sub-docs and link them."
if [[ -n "$path_msg" ]]; then
path_msg="${path_msg} ${docs_audit}"
else
path_msg="$docs_audit"
fi
fi
fi
fi
if [[ -n "$path_msg" ]]; then
reminders+=("$path_msg")
fi
;;
esac
# ── After reading docs/ files: remind that pagination limit is lifted ─────────
if [[ "$TOOL_NAME" == "read_file" ]]; then
READ_PATH=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
process.stdout.write(i.filePath || '');
" 2>/dev/null || true)
START_LINE=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
process.stdout.write(String(i.startLine || 1));
" 2>/dev/null || true)
if echo "$READ_PATH" | grep -qE '(^|/)docs/|\.md$|\.txt$'; then
docs_msg="DOCS READ EXEMPTION: docs/ files and all .md/.txt files are exempt from the 50-line pagination limit."
if [[ "$START_LINE" -gt 1 ]]; then
docs_msg="${docs_msg} You are currently paginating (startLine=${START_LINE}) — you may expand to up to 500 lines per call to reduce tool-call overhead."
else
docs_msg="${docs_msg} You may use ranges up to 500 lines per read_file call instead of 50."
fi
reminders+=("$docs_msg")
fi
fi
# ── After vscode_renameSymbol: remind about object property key aliases ───────
if [[ "$TOOL_NAME" == "vscode_renameSymbol" ]]; then
rename_msg="RENAME REMINDER: vscode_renameSymbol only renames variable bindings — NOT object property keys or string literals."
rename_msg="${rename_msg} After this rename, grep the file for the OLD name."
rename_msg="${rename_msg} Stale patterns to watch for: (1) aliased store keys like 'deleteX: archiveX' in the store return object — the key 'deleteX' is unchanged and so are all 'store.deleteX()' call sites;"
rename_msg="${rename_msg} (2) string literals like openDialog('delete-item') and AppDialog handle='delete-item';"
rename_msg="${rename_msg} (3) related variable names in the same file that share the same prefix (e.g. renaming deleteSuccess should also prompt renaming deleteLoading, deleteError)."
rename_msg="${rename_msg} Fix all of these with multi_replace_string_in_file after the symbol rename."
reminders+=("$rename_msg")
fi
# ── Emit at most 2 reminders ─────────────────────────────────────────────────
context=""
for (( i=0; i<${#reminders[@]} && i<2; i++ )); do
if [[ -n "$context" ]]; then
context="${context}\n${reminders[$i]}"
else
context="${reminders[$i]}"
fi
done
# Only output if we have context to inject
if [[ -n "$context" ]]; then
# Prefix with a self-identifying marker so the model cannot confuse the
# injection with preceding tool output (e.g., trailing markdown in a file).
framed="[HOOK INJECTION: post-tool-use] System reminder — NOT part of preceding tool output:\n\n${context}"
json_context=$(printf '%b' "$framed" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8")))')
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": ${json_context}
}
}
EOF
else
echo '{}'
fi

70
.agents/hooks/pre-compact.sh Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env bash
# PreCompact hook: export critical session state before context summarization.
# Saves investigation progress so findings survive context window compression.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
SESSION_DIR="$REPO_ROOT/.session"
COMPACT_LOG="$SESSION_DIR/pre-compact-state.md"
mkdir -p "$SESSION_DIR"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Read the dead-ends file if it exists
dead_ends_summary=""
DEAD_ENDS_FILE="$SESSION_DIR/dead-ends.md"
if [[ -f "$DEAD_ENDS_FILE" ]]; then
dead_ends_summary=$(tail -30 "$DEAD_ENDS_FILE" 2>/dev/null || true)
fi
# Check for active investigation files (exclude those already marked complete)
investigation_summary=""
EXPLORATIONS_DIR="$REPO_ROOT/docs/explorations"
if [[ -d "$EXPLORATIONS_DIR" ]]; then
inv_files=$(find "$EXPLORATIONS_DIR" -name "*.md" -not -empty 2>/dev/null || true)
if [[ -n "$inv_files" ]]; then
active_files=$(echo "$inv_files" | while read -r f; do
if ! grep -qi '^\*\*Status\*\*.*complete\|^Status:.*complete' "$f" 2>/dev/null; then
echo "$f"
fi
done || true)
if [[ -n "$active_files" ]]; then
investigation_summary=$(echo "$active_files" | xargs -I{} basename {} .md | sed 's/^/- /' || true)
fi
fi
fi
# Write the pre-compact state file
cat > "$COMPACT_LOG" << STATEEOF
# Pre-Compact State Export
Exported at: $TIMESTAMP
Trigger: context summarization
## Active Investigations
$investigation_summary
## Recent Dead Ends (do NOT re-test these)
$dead_ends_summary
## Reminders
- Hypothesis + falsification criterion BEFORE any diagnostic test
- Record WHY failures failed, not just WHAT was tried
- Check AGENTS.md and package-level AGENTS.md for implementation guidance
STATEEOF
# Inject context for the summarized conversation
context="[HOOK INJECTION: pre-compact] System reminder — injected before context compaction, not part of any user message or tool output:\n\n"
context="${context}CONTEXT PRESERVATION (pre-compact): Critical state exported to .session/pre-compact-state.md."
context="${context} After summarization, re-read this file to restore investigation context."
context="${context} Key: do NOT re-test eliminated hypotheses from the dead-ends file."
context="${context} TODO LIST SYNC: After resuming, update the chat todo list to reflect actual progress."
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreCompact",
"additionalContext": "$(echo "$context" | sed 's/"/\\"/g')"
}
}
EOF

217
.agents/hooks/pre-tool-use.sh Executable file
View File

@ -0,0 +1,217 @@
#!/usr/bin/env bash
# PreToolUse hook: enforce project policies before tool execution.
#
# Policies enforced:
# 1. No npx — use npm run scripts only
# 2. No node_modules/.bin invocations — use npm run scripts only
# 3. No direct node invocations of node_modules packages
# 4. No python — use node for scripting
# 5. No npm run build while dev server is running (port conflict)
# 6. No sed -i / awk rewrites on code files — use replace_string_in_file
# 7. No npm install without user confirmation — ask first
# 8. No editing *.generated.ts files — edit the generator source instead
# 9. No deleting .wireit — fix the underlying build config issue instead
# 10. No -- --force with npm run scripts — wireit cache busting masks real problems
# 11. No npm run format with specific file args — propagates to all workspaces
# 12. No editing eslint.config.js files — ESLint config changes require human review
# 13. No read_file with range >50 lines (enforced hard block) — except docs/ files and all .md/.txt files (limit 500)
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name"\s*:\s*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"/\1/' || true)
# DEBUG: log every hook invocation with full input
echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"hook\":\"pre-tool-use\",\"tool\":\"$TOOL_NAME\",\"raw\":$(echo "$INPUT" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8")))' 2>/dev/null || echo '""')}" >> /tmp/pre-tool-hook-debug.jsonl
# Only inspect terminal/execution tools and file-editing tools
case "$TOOL_NAME" in
run_in_terminal|execution_subagent|send_to_terminal|\
replace_string_in_file|multi_replace_string_in_file|create_file|\
read_file|read|edit)
;;
*)
echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"hook\":\"pre-tool-use\",\"action\":\"early-exit\",\"tool\":\"$TOOL_NAME\"}" >> /tmp/pre-tool-hook-debug.jsonl
exit 0
;;
esac
# Extract command (terminal tools) or file path (file-editing tools)
COMMAND=""
FILE_PATH=""
case "$TOOL_NAME" in
run_in_terminal|execution_subagent|send_to_terminal)
COMMAND=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
process.stdout.write(i.command || i.query || '');
" 2>/dev/null || true)
;;
replace_string_in_file|multi_replace_string_in_file|create_file|edit)
FILE_PATH=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
const p = i.filePath || (i.replacements && i.replacements[0] && i.replacements[0].filePath) || '';
process.stdout.write(p);
" 2>/dev/null || true)
;;
read_file|read)
FILE_PATH=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
process.stdout.write(i.filePath || '');
" 2>/dev/null || true)
;;
esac
if [[ -z "$COMMAND" && -z "$FILE_PATH" ]]; then
exit 0
fi
# ── Helper: emit deny response ───────────────────────────────────────────────
deny() {
local reason="$1"
echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"hook\":\"pre-tool-use\",\"action\":\"DENY\",\"tool\":\"$TOOL_NAME\",\"reason\":$(echo "$reason" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8").trim()))' 2>/dev/null || echo '"<encode-error>"')}" >> /tmp/pre-tool-hook-debug.jsonl
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "$reason"
}
}
EOF
exit 0
}
# ── Policy 1: No npx ─────────────────────────────────────────────────────────
if echo "$COMMAND" | grep -qE '(^|\s|&&|\||\;)npx\s'; then
deny "BLOCKED: Do not use npx directly. Use npm run scripts instead. If no script exists, recommend adding one. See AGENTS.md."
fi
# ── Policy 2: No direct node_modules/.bin invocations ────────────────────────
if echo "$COMMAND" | grep -qE 'node_modules/\.bin/|node_modules\\\.bin\\'; then
deny "BLOCKED: Do not invoke tools from node_modules/.bin/. Use npm run scripts instead. See AGENTS.md."
fi
# ── Policy 3: No direct node invocations of node_modules packages ────────────
if echo "$COMMAND" | grep -qE '(^|\s|&&|\||\;)node\s+(\./)?node_modules/'; then
deny "BLOCKED: Do not invoke node_modules packages directly with node. Use npm run scripts instead. See AGENTS.md."
fi
# ── Policy 4: No python — use node for scripting ─────────────────────────────
if echo "$COMMAND" | grep -qE '(^|\s|&&|\||\;)(python3?|pip3?)\s'; then
deny "BLOCKED: Do not use python in this project. Use node for scripting instead."
fi
# ── Policy 5: No npm run build while dev server is running ───────────────────
# The dev server (npm run dev) uses tsc --watch and writes to dist/.
# npm run build also writes to dist/, causing crashes when both run.
# Detect dev server by checking if port 3000 (app) or 3001 (Vite HMR) is bound.
if echo "$COMMAND" | grep -qE '(^|\s|&&|\||\;)npm\s+run\s+build(\s|$|:)'; then
if ss -tlnp 2>/dev/null | grep -qE ':300[01]\s'; then
deny "BLOCKED: npm run build conflicts with the running dev server (port 3000/3001 in use). Both write to dist/ and will crash. Stop the dev server first, or use npm run lint and npm test for verification instead."
fi
fi
# ── Policy 6: No sed -i or awk in-place editing of code files ────────────────
# These tools corrupt structured code — use replace_string_in_file instead.
if echo "$COMMAND" | grep -qE '(^|\s|&&|\||\;)sed\s+[^|>]*-[a-zA-Z]*i'; then
deny "BLOCKED: Do not use 'sed -i' to edit code files. Use replace_string_in_file for precise, context-aware edits. sed pattern matching frequently corrupts structured code with unintended replacements."
fi
if echo "$COMMAND" | grep -qE '(^|\s|&&|\||\;)awk\s+.*>\s*[^/dev].*\.(ts|tsx|js|json|md)'; then
deny "BLOCKED: Do not use awk to rewrite code files. Use replace_string_in_file for precise edits instead."
fi
# ── Policy 7: No npm install without user confirmation ───────────────────────
# Dependencies must be kept minimal. Always ask the user before adding packages.
if echo "$COMMAND" | grep -qE '(^|\s|&&|\||\;)npm\s+(install|i)(\s|$)'; then
deny "BLOCKED: Do not run npm install without user confirmation. This project keeps dependencies minimal — always ask first. If a package is genuinely needed, propose it and let the user decide."
fi
# ── Policy 9: No deleting .wireit cache to paper over stale-cache issues ─────
# Deleting .wireit forces a full cold rebuild which is very slow and masks the
# real problem (bad cache key, fingerprint mismatch, etc.).
# If wireit is returning cached results incorrectly, investigate and fix the
# underlying issue in the affected package.json wireit configuration instead.
if echo "$COMMAND" | grep -qE 'rm\s+.*\.wireit|rm\s+-[a-zA-Z]*rf?\s+.*\.wireit'; then
deny "BLOCKED: Do not delete .wireit to force a cold rebuild. This masks a real wireit configuration problem. Investigate which script has a stale fingerprint or incorrect 'files' / 'output' declaration in package.json, then fix that instead. See wireit docs for cache invalidation."
fi
# ── Policy 10: No --force with npm run (wireit cache bust) ───────────────────
# Passing --force to wireit-backed scripts bypasses the cache and triggers a
# full cold rebuild. This masks fingerprint/config bugs and slows CI.
# If a script is using a stale cache, diagnose the wireit 'files'/'output'
# config instead of forcing a rebuild.
if echo "$COMMAND" | grep -qE 'npm\s+run\s+[a-zA-Z:_-]+\s+--\s+--force'; then
deny "BLOCKED: Do not use -- --force with npm run scripts. This bypasses the wireit cache and masks configuration bugs. If a script returns stale results, check the 'files'/'output' declarations in its wireit config instead."
fi
# ── Policy 11: No npm run format with specific file args ─────────────────────
# Running 'npm run format -- <file>' from the workspace root propagates the
# extra argument to every workspace package's format script, causing each to
# fail with 'No files matching the pattern'. Format runs on the whole package
# directory by default — either run it without args or cd into the right
# package first.
if echo "$COMMAND" | grep -qE 'npm\s+run\s+format\s+--\s+\S'; then
deny "BLOCKED: Do not pass file arguments to 'npm run format'. The extra arg propagates to every workspace package and causes failures. Run 'npm run format' without args to format all files, or cd into the specific package directory first."
fi
# ── Policy 14: No shell reads of workspace package.json files ────────────────
# Mirrors the OpenCode read tool guard: reading apps/*/package.json or
# packages/*/package.json via cat/head/tail/jq bypasses the read block and
# auto-injects every AGENTS.md in that subtree, exhausting the 32K context.
# Reading root package.json is fine — only workspace sub-packages are blocked.
if echo "$COMMAND" | grep -qE '(cat|head|tail|jq\s+-[a-zA-Z]*r?)\s+[^|>]*(apps|packages)/[^/[:space:]]+/package\.json'; then
deny "BLOCKED: Do not use cat/head/tail/jq to read workspace package.json files (apps/*/package.json, packages/*/package.json). These files auto-inject AGENTS.md context that exhausts the model's 32K context window. Use 'npm run' scripts for dependency info, or read root package.json."
fi
if echo "$COMMAND" | grep -qE '(apps|packages)/[^/[:space:]]+/package\.json.*\|\s*(jq|cat|head|tail)'; then
deny "BLOCKED: Do not pipe workspace package.json files (apps/*/package.json, packages/*/package.json) through jq or other readers. These files auto-inject AGENTS.md context that exhausts the model's 32K context window."
fi
# ── File path checks (replace_string_in_file / create_file / read_file tools) ─
# (Policy 8 is a file-path check — see below)
if [[ -n "$FILE_PATH" ]]; then
# ── Policy 8: No editing *.generated.ts files ──────────────────────────────
if echo "$FILE_PATH" | grep -qE '\.generated\.ts$'; then
deny "BLOCKED: Do not edit *.generated.ts files directly. These are auto-generated and will be overwritten on the next build. Edit the source files (controller.ts, routes.ts, business-logic.ts) instead and run 'npm run build:core' to regenerate."
fi
# ── Policy 12: No editing eslint.config.js files ───────────────────────────
if echo "$FILE_PATH" | grep -qE '(^|/)eslint\.config\.[cm]?[jt]s$'; then
deny "BLOCKED: Do not edit eslint.config.js files directly. ESLint configuration changes require human review — describe the change needed and let the user decide or consider a method that leads to higher code quality, if available."
fi
# ── Policy 13: No read_file ranges >50 lines (docs/ exempt, limit 500) ─────
# Prevents context exhaustion on 32K models from large sequential reads.
# docs/ files (documentation) are exempt: they are meant to be read whole
# and may use ranges up to 500 lines per call.
if [[ "$TOOL_NAME" == "read_file" || "$TOOL_NAME" == "read" || "$TOOL_NAME" == "edit" ]]; then
START_LINE=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
process.stdout.write(String(i.startLine ?? 1));
" 2>/dev/null || echo "1")
END_LINE=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
process.stdout.write(String(i.endLine ?? 0));
" 2>/dev/null || echo "0")
if [[ "$END_LINE" -gt 0 ]]; then
RANGE=$(( END_LINE - START_LINE + 1 ))
if echo "$FILE_PATH" | grep -qE '(^|/)docs/|\.md$|\.txt$'; then
# docs/ files and all .md/.txt files — allow up to 500 lines
if [[ "$RANGE" -gt 500 ]]; then
deny "BLOCKED: Read more than 500 lines at once is prohibited for docs/ and .md/.txt files. Use startLine/endLine to paginate in ≤500-line chunks."
fi
else
# All other files — 50-line limit
if [[ "$RANGE" -gt 50 ]]; then
deny "BLOCKED: Read more than 50 lines at once is prohibited. Use startLine/endLine to paginate in ≤50-line chunks. For docs/ and .md/.txt files the limit is 500 lines. Use grep_search first to find the right offset."
fi
fi
fi
fi
fi

74
.agents/hooks/session-start.sh Executable file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env bash
# SessionStart hook: inject project state at conversation start.
# Provides current branch, active investigations, and session continuation notes.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
# Reset tool call counter for periodic self-checks (see post-tool-use.sh)
REPO_ID=$(printf '%s' "$REPO_ROOT" | md5sum | cut -c1-8 2>/dev/null || echo "default")
echo "0" > "/tmp/.opencode-tool-count-${REPO_ID}"
BRANCH=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
# Check for active investigation files
active_investigations=""
EXPLORATIONS_DIR="$REPO_ROOT/docs/explorations"
if [[ -d "$EXPLORATIONS_DIR" ]]; then
inv_files=$(find "$EXPLORATIONS_DIR" -name "*.md" -not -empty 2>/dev/null || true)
if [[ -n "$inv_files" ]]; then
inv_count=$(echo "$inv_files" | wc -l)
inv_names=$(echo "$inv_files" | xargs -I{} basename {} .md | sed 's/^/ - /' || true)
active_investigations="Active investigation/exploration files (${inv_count}):\n${inv_names}\nReview relevant files before starting related work."
fi
fi
# Check for session continuation notes
session_notes=""
if [[ -d "/memories/session" ]]; then
session_files=$(find /memories/session -name "*.md" 2>/dev/null || true)
if [[ -n "$session_files" ]]; then
session_names=$(echo "$session_files" | xargs -I{} basename {} .md | sed 's/^/ - /' || true)
session_notes="Session memory files exist:\n${session_names}\nCheck these for context from previous conversations."
fi
fi
# Check for dead-ends file from previous debugging sessions
dead_ends=""
DEAD_ENDS_FILE="$REPO_ROOT/.session/dead-ends.md"
if [[ -f "$DEAD_ENDS_FILE" ]]; then
de_count=$(grep -c '^\s*- \*\*' "$DEAD_ENDS_FILE" 2>/dev/null || echo "0")
if [[ "$de_count" -gt 0 ]]; then
dead_ends="Active dead-ends file with ~${de_count} entries — read before debugging to avoid re-testing eliminated hypotheses."
fi
fi
# Build context message
context="PROJECT STATE | Branch: ${BRANCH}"
if [[ -n "$active_investigations" ]]; then
context="${context}\n${active_investigations}"
fi
if [[ -n "$session_notes" ]]; then
context="${context}\n${session_notes}"
fi
if [[ -n "$dead_ends" ]]; then
context="${context}\n${dead_ends}"
fi
context="${context}\nREMINDERS: (a) Check AGENTS.md and package-level AGENTS.md files for implementation guidance. (b) Ordered markdown lists are auto-renumbered by the editor on save — do not manually renumber after inserting or removing items."
# Prefix with a self-identifying marker so the model cannot confuse the
# injection with project content.
context="[HOOK INJECTION: session-start] System context — injected at session start, not part of any user message or tool output:\n\n${context}"
# Output JSON
json_context=$(printf '%b' "$context" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8")))')
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": ${json_context}
}
}
EOF

176
.agents/hooks/stop.sh Executable file
View File

@ -0,0 +1,176 @@
#!/usr/bin/env bash
# Stop hook:
# 1. Validates TODO.md and COMPLETED.md — blocking if violations found.
# 2. Prompts agent to record lessons learned before session ends (non-blocking).
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
SESSION_DIR="$REPO_ROOT/.session"
TODO_FILE="$REPO_ROOT/docs/TODO.md"
COMPLETED_FILE="$REPO_ROOT/docs/projects/COMPLETED.md"
# ── Validation (blocking) ────────────────────────────────────────────────────
problems=""
# Check TODO.md for completed [x] or ✅ numbered items — they should be moved to COMPLETED.md
if [[ -f "$TODO_FILE" ]]; then
todo_completed=$(grep -n '^\s*[0-9]\+\. \(\[x\]\|✅\)' "$TODO_FILE" 2>/dev/null || true)
if [[ -n "$todo_completed" ]]; then
count=$(echo "$todo_completed" | wc -l | tr -d ' ')
problems="docs/TODO.md contains ${count} completed [x]/✅ task(s). Move them to docs/projects/COMPLETED.md and remove from TODO.md.\nLines:\n${todo_completed}"
fi
fi
# Check COMPLETED.md for inline [x] items — it should contain stubs/summaries only
if [[ -f "$COMPLETED_FILE" ]]; then
completed_inline=$(grep -n '^\s*\(-\|[0-9]\+\.\) \[x\]' "$COMPLETED_FILE" 2>/dev/null || true)
if [[ -n "$completed_inline" ]]; then
count=$(echo "$completed_inline" | wc -l | tr -d ' ')
if [[ -n "$problems" ]]; then
problems="${problems}\n\n"
fi
problems="${problems}docs/projects/COMPLETED.md contains ${count} inline [x] item(s). Archive to a completed/*.md file and replace with a stub entry."
fi
fi
if [[ -n "$problems" ]]; then
msg=$(printf '%b' "$problems" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8")))')
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"decision": "block",
"reason": ${msg}
}
}
EOF
exit 0
fi
# ── Lessons learned prompt (non-blocking) ────────────────────────────────────
# Check whether all questions in the user's last prompt were answered
unanswered_reminder=""
LAST_PROMPT_FILE="/tmp/.last-user-prompt.txt"
if [[ -f "$LAST_PROMPT_FILE" ]]; then
last_prompt=$(cat "$LAST_PROMPT_FILE" 2>/dev/null || true)
if [[ -n "$last_prompt" ]]; then
unanswered_reminder="$last_prompt"
fi
fi
# Check whether the dev server is running (ports 3000/3001)
dev_server_running=""
if ss -tlnp 2>/dev/null | grep -qE ':300[01]\s'; then
dev_server_running="yes"
fi
dead_ends_active=""
DEAD_ENDS_FILE="$SESSION_DIR/dead-ends.md"
if [[ -f "$DEAD_ENDS_FILE" ]]; then
entry_count=$(grep -c '^\s*- \*\*' "$DEAD_ENDS_FILE" 2>/dev/null || echo "0")
if [[ "$entry_count" -gt 0 ]]; then
dead_ends_active="yes"
fi
fi
# Check for investigation files that may need status updates
open_investigations=""
EXPLORATIONS_DIR="$REPO_ROOT/docs/explorations"
if [[ -d "$EXPLORATIONS_DIR" ]]; then
inv_files=$(find "$EXPLORATIONS_DIR" -name "*.md" -not -empty 2>/dev/null || true)
if [[ -n "$inv_files" ]]; then
# Check for explorations NOT yet marked complete (status line doesn't contain 'complete')
active=$(echo "$inv_files" | while read -r f; do
if ! grep -qi '^\*\*Status\*\*.*complete\|^Status:.*complete' "$f" 2>/dev/null; then
echo "$f"
fi
done || true)
if [[ -n "$active" ]]; then
active_names=$(echo "$active" | xargs -I{} basename {} .md | sed 's/^/ - /' || true)
open_investigations="yes"
fi
fi
fi
# Check if pre-compact-state.md still lists active investigations
stale_compact=""
COMPACT_FILE="$SESSION_DIR/pre-compact-state.md"
if [[ -f "$COMPACT_FILE" ]]; then
active_in_compact=$(grep -A20 '## Active Investigations' "$COMPACT_FILE" 2>/dev/null | grep -v '##' | grep -v '^\s*$' | head -5 || true)
if [[ -n "$active_in_compact" ]]; then
stale_compact="yes"
fi
fi
# Build the lessons-learned prompt
prompt="SESSION END — QUALITY GATE"
# Remind agent to verify it answered every question in the user's last message
if [[ -n "$unanswered_reminder" ]]; then
prompt="${prompt}\n\nANSWER CHECK: Before finishing, re-read the user's last message below and confirm"
prompt="${prompt} you addressed EVERY question and request in it — not just the primary task:"
prompt="${prompt}\n---"
prompt="${prompt}\n${unanswered_reminder}"
prompt="${prompt}\n---"
fi
if [[ -n "$dev_server_running" ]]; then
prompt="${prompt}\nThe dev server IS running (port 3000/3001 detected). Run: npm test && npm run lint"
prompt="${prompt}\nThen ask the user to confirm the build is clean in their terminal."
else
prompt="${prompt}\nThe dev server is NOT running. Run: npm run build:strict"
prompt="${prompt}\nThis runs build + lint + format:check + tests. Do NOT skip this if you changed any source files."
fi
prompt="${prompt}\n"
prompt="${prompt}\n---"
prompt="${prompt}\nLESSONS LEARNED CAPTURE — Before finishing, consider recording reusable insights:"
prompt="${prompt}\n"
prompt="${prompt}\n1. **Process insights** (what worked, what didn't, workflow improvements) → write to /memories/repo/ or /memories/"
if [[ -n "$dead_ends_active" ]]; then
prompt="${prompt}\n2. **Dead-ends file** has entries — verify all have results recorded (ELIMINATED/CONFIRMED with reasons)"
fi
if [[ -n "$open_investigations" ]]; then
prompt="${prompt}\n3. **Open explorations** not yet marked complete — update their status or mark complete:"
prompt="${prompt}\n${active_names}"
fi
if [[ -n "$stale_compact" ]]; then
prompt="${prompt}\n3. **pre-compact-state.md** still lists active investigations — clear or update it now that the session is ending"
fi
prompt="${prompt}\n4. **TODO.md review** — Check docs/TODO.md: did any work this session complete or partially complete an item?"
prompt="${prompt}\n - If an item is done: mark it [x], then move the whole entry to docs/projects/COMPLETED.md and remove it from TODO.md"
prompt="${prompt}\n - If partially done: update the description to reflect current state"
prompt="${prompt}\n - Don't wait for the blocking check — proactively keep TODO.md accurate"
prompt="${prompt}\n5. **New tasks discovered** → note them for the user or add to docs/TODO.md"
prompt="${prompt}\n"
prompt="${prompt}\n---"
prompt="${prompt}\nEFFORT REFLECTION: If this session required significant effort (many tool calls, multiple dead-ends, complex investigation):"
prompt="${prompt}\n Ask yourself: What information, if it had existed at the start, would have prevented most of that work?"
prompt="${prompt}\n First, determine scope — is this globally applicable, or specific to certain files/patterns?"
prompt="${prompt}\n Then lean toward hooks as the solution:"
prompt="${prompt}\n • Hard stops via PreToolUse blocks (best when the bad action is a terminal command)"
prompt="${prompt}\n • PostToolUse reminders (fire right after editing a relevant file — effective because they appear mid-task)"
prompt="${prompt}\n • applyTo: instructions files scoped to a file glob (fire when the agent opens matching files)"
prompt="${prompt}\n • PreCompact saves investigation state before context compression (PostCompact does not exist)"
prompt="${prompt}\n • Stop / SessionStart: on-demand summary injection — less precise than PostToolUse but good for broad reminders"
prompt="${prompt}\n These are all more reliable than AGENTS.md sections (lost-in-the-middle problem)."
prompt="${prompt}\n Record the insight in the right hook/instructions file, NOT just in AGENTS.md."
prompt="${prompt}\n"
prompt="${prompt}\nSkip categories that don't apply. Only record genuinely new insights."
# Prefix with a self-identifying marker so the model cannot confuse the
# injection with the user's own message or prior tool output.
prompt="[HOOK INJECTION: stop] System reminder — injected at session end, not part of any user message or tool output:\n\n${prompt}"
json_prompt=$(printf '%b' "$prompt" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8")))')
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": ${json_prompt}
}
}
EOF

View File

@ -0,0 +1,64 @@
#!/usr/bin/env bash
# UserPromptSubmit hook:
# 1. Writes the typed prompt text to a temp file as a fallback.
# 2. Injects an additionalContext instruction telling the agent to overwrite
# that file with a complete capture (including attachments / selections)
# before doing anything else. The Stop hook reads the file to remind the
# agent to verify every question was answered.
# 3. Injects agent nudges when trigger words are detected in the prompt.
# Nudges fire here (context tail) rather than in AGENTS.md (context start)
# so they survive long sessions — see "lost in the middle" in .agents/AGENTS.md.
set -euo pipefail
INPUT=$(cat)
PROMPT=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
process.stdout.write(d.prompt || '');
" 2>/dev/null || true)
if [[ -n "$PROMPT" ]]; then
echo "$PROMPT" > /tmp/.last-user-prompt.txt
fi
# ── Agent nudges ─────────────────────────────────────────────────────────────
# Check prompt for trigger words and append a one-line nudge to additionalContext.
# Only one nudge fires per turn (brainstorm takes priority over research).
NUDGE=""
if echo "$PROMPT" | grep -qiE \
'\bwait\b|actually|hmm+|\breconsider\b|overthink|overcomplicat|too complicated|simpler way|going in circles|second thoughts|what if we|maybe i should|step back|rethink|hold on|i.m not sure|analysis paralysis|stuck'; then
NUDGE="NUDGE: The user may be showing signs of analysis paralysis or complexity spiral. Consider suggesting they switch to @brainstorm — one sentence, non-intrusive. Don't force it; if they decline, don't repeat it for the same topic."
elif echo "$PROMPT" | grep -qiE \
'why is this broken|something.s wrong|not working|what changed|it used to work|\bregression\b|\btrace\b|dig into|root cause|figure out why|\bdebug\b|\bdiagnose\b|investigate|repeated.*fail|why.*code|how does this work|i don.t understand'; then
NUDGE="NUDGE: The user may need systematic investigation. Consider suggesting they switch to @research — one sentence, non-intrusive. Don't force it; if they decline, don't repeat it for the same topic."
fi
TASK_CAPTURE="TASK CAPTURE (do this first, before any other action): Write a concise bullet list of every distinct question and task in this prompt — including content from any attached files or selected text — to /tmp/.last-user-prompt.txt. Read the file and choose to overwrite or merge, depending on if there are still unanswered questions remaining. This file is read at session end to verify every question was answered. If there is nothing to capture (e.g. a one-word test message), write a single dash to the file."
# CURRENT QUESTION marker — placed last (context tail = highest recency bias).
# If the current prompt resembles an earlier one, this prevents the model from
# answering the earlier version. Mechanism validated by S2A (Weston & Sukhbaatar,
# arXiv:2311.11829): explicitly isolating the current query from prior context
# reduces sycophancy and improves factuality without a second LLM call.
CURRENT_Q="CURRENT QUESTION: Answer the user's most recent message (above). If a similar question appeared earlier in this session, answer THIS version — do not conflate it with the prior one."
if [[ -n "$NUDGE" ]]; then
ADDITIONAL_CONTEXT="${NUDGE}\n\n${TASK_CAPTURE}\n\n${CURRENT_Q}"
else
ADDITIONAL_CONTEXT="${TASK_CAPTURE}\n\n${CURRENT_Q}"
fi
# Prefix with a self-identifying marker so the model cannot confuse the
# injection with the user's own message.
ADDITIONAL_CONTEXT="[HOOK INJECTION: user-prompt-submit] System reminder — NOT part of the user's message:\n\n${ADDITIONAL_CONTEXT}"
json_context=$(printf '%b' "$ADDITIONAL_CONTEXT" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8")))')
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": ${json_context}
}
}
EOF

129
.agents/install.sh Executable file
View File

@ -0,0 +1,129 @@
#!/usr/bin/env bash
# install.sh — Wire ~/dotfiles/.agents/ into global tool configs.
# Idempotent: safe to re-run. Creates dirs, symlinks, and config entries.
# Run once per machine after cloning dotfiles.
set -euo pipefail
DOTFILES_AGENTS="$(cd "$(dirname "$0")" && pwd)"
log() { printf '\033[0;32m✓\033[0m %s\n' "$1"; }
warn() { printf '\033[0;33m⚠\033[0m %s\n' "$1"; }
skip() { printf '\033[0;34m\033[0m %s\n' "$1"; }
# ── 1. Copilot global hooks ──────────────────────────────────────────────────
COPILOT_HOOKS_DIR="$HOME/.copilot/hooks"
COPILOT_HOOK_TARGET="$DOTFILES_AGENTS/frameworks/github/hooks.json"
COPILOT_HOOK_LINK="$COPILOT_HOOKS_DIR/agent-support.json"
mkdir -p "$COPILOT_HOOKS_DIR"
if [[ -L "$COPILOT_HOOK_LINK" && "$(readlink "$COPILOT_HOOK_LINK")" == "$COPILOT_HOOK_TARGET" ]]; then
skip "Copilot hook symlink already set: $COPILOT_HOOK_LINK"
else
ln -sf "$COPILOT_HOOK_TARGET" "$COPILOT_HOOK_LINK"
log "Copilot hook symlink: $COPILOT_HOOK_LINK$COPILOT_HOOK_TARGET"
fi
# ── 2. OpenCode global plugin ────────────────────────────────────────────────
OC_PLUGINS_DIR="$HOME/.config/opencode/plugins"
OC_PLUGIN_TARGET="$DOTFILES_AGENTS/frameworks/opencode/plugin.ts"
OC_PLUGIN_LINK="$OC_PLUGINS_DIR/agent-support.ts"
mkdir -p "$OC_PLUGINS_DIR"
if [[ -L "$OC_PLUGIN_LINK" && "$(readlink "$OC_PLUGIN_LINK")" == "$OC_PLUGIN_TARGET" ]]; then
skip "OpenCode plugin symlink already set: $OC_PLUGIN_LINK"
else
ln -sf "$OC_PLUGIN_TARGET" "$OC_PLUGIN_LINK"
log "OpenCode plugin symlink: $OC_PLUGIN_LINK$OC_PLUGIN_TARGET"
fi
# ── 3. OpenCode global AGENTS.md ────────────────────────────────────────────
OC_AGENTS_TARGET="$DOTFILES_AGENTS/AGENTS.md"
OC_AGENTS_LINK="$HOME/.config/opencode/AGENTS.md"
if [[ -L "$OC_AGENTS_LINK" && "$(readlink "$OC_AGENTS_LINK")" == "$OC_AGENTS_TARGET" ]]; then
skip "OpenCode AGENTS.md symlink already set: $OC_AGENTS_LINK"
else
ln -sf "$OC_AGENTS_TARGET" "$OC_AGENTS_LINK"
log "OpenCode AGENTS.md symlink: $OC_AGENTS_LINK$OC_AGENTS_TARGET"
fi
# ── 4. OpenCode global MCP entry ────────────────────────────────────────────
OC_CONFIG="$HOME/.config/opencode/opencode.json"
MCP_KEY="all-agents"
MCP_CMD="[\"node\", \"--experimental-strip-types\", \"$DOTFILES_AGENTS/mcp/index.ts\"]"
if [[ ! -f "$OC_CONFIG" ]]; then
warn "No OpenCode config at $OC_CONFIG — creating minimal config with MCP entry."
printf '{\n "$schema": "https://opencode.ai/config.json",\n "mcp": {\n "%s": {\n "type": "local",\n "command": %s\n }\n }\n}\n' "$MCP_KEY" "$MCP_CMD" > "$OC_CONFIG"
log "Created $OC_CONFIG with all-agents MCP entry"
elif node -e "const c=JSON.parse(require('fs').readFileSync('$OC_CONFIG','utf8')); process.exit(c.mcp && c.mcp['$MCP_KEY'] ? 0 : 1)" 2>/dev/null; then
skip "OpenCode MCP entry '$MCP_KEY' already present in $OC_CONFIG"
else
# Merge the MCP entry using node — jq may not be available everywhere
node -e "
const fs = require('fs');
const path = '$OC_CONFIG';
const config = JSON.parse(fs.readFileSync(path, 'utf8'));
config.mcp = config.mcp || {};
config.mcp['$MCP_KEY'] = { type: 'local', command: $MCP_CMD };
fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\n');
console.log('Merged all-agents MCP entry into ' + path);
"
log "OpenCode MCP entry merged: $OC_CONFIG"
fi
# ── 5. VS Code global MCP ────────────────────────────────────────────────────
# Primary remote/server path; falls back to local if running VS Code locally.
VSCODE_MCP_PATHS=(
"$HOME/.vscode-server/data/User/mcp.json"
"$HOME/.vscode/data/User/mcp.json"
"$HOME/Library/Application Support/Code/User/mcp.json"
)
for VSCODE_MCP in "${VSCODE_MCP_PATHS[@]}"; do
if [[ -d "$(dirname "$VSCODE_MCP")" ]]; then
MCP_SERVER_CMD="node"
MCP_SERVER_ARGS="[\"--experimental-strip-types\", \"$DOTFILES_AGENTS/mcp/index.ts\"]"
if [[ ! -f "$VSCODE_MCP" ]]; then
printf '{\n "servers": {\n "%s": {\n "type": "stdio",\n "command": "%s",\n "args": %s\n }\n }\n}\n' \
"$MCP_KEY" "$MCP_SERVER_CMD" "$MCP_SERVER_ARGS" > "$VSCODE_MCP"
log "Created VS Code global MCP config: $VSCODE_MCP"
elif node -e "const c=JSON.parse(require('fs').readFileSync('$VSCODE_MCP','utf8')); process.exit(c.servers && c.servers['$MCP_KEY'] ? 0 : 1)" 2>/dev/null; then
skip "VS Code MCP entry '$MCP_KEY' already present in $VSCODE_MCP"
else
node -e "
const fs = require('fs');
const path = '$VSCODE_MCP';
const config = JSON.parse(fs.readFileSync(path, 'utf8'));
config.servers = config.servers || {};
config.servers['$MCP_KEY'] = {
type: 'stdio',
command: '$MCP_SERVER_CMD',
args: $MCP_SERVER_ARGS
};
fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\n');
"
log "VS Code MCP entry merged: $VSCODE_MCP"
fi
break
fi
done
# ── 6. VS Code global prompts dir ───────────────────────────────────────────
for VSCODE_PROMPTS_DIR in \
"$HOME/.vscode-server/data/User/prompts" \
"$HOME/.vscode/data/User/prompts"; do
if [[ -d "$(dirname "$(dirname "$VSCODE_PROMPTS_DIR")")" ]]; then
mkdir -p "$VSCODE_PROMPTS_DIR"
log "VS Code prompts dir ensured: $VSCODE_PROMPTS_DIR"
break
fi
done
# ── Done ─────────────────────────────────────────────────────────────────────
printf '\n\033[0;32minstall.sh complete.\033[0m\n'
printf 'Next steps:\n'
printf ' 1. Restart OpenCode to pick up the new global plugin.\n'
printf ' 2. Reload VS Code / reconnect to reload MCP servers.\n'
printf ' 3. Smoke test: /research slash prompt fires; a denied terminal command is blocked.\n'

189
.agents/mcp/index.ts Normal file
View File

@ -0,0 +1,189 @@
#!/usr/bin/env node
/**
* all-agents MCP server shared agent infrastructure over the Model Context Protocol.
*
* Prompts and tools are auto-discovered from sibling directories:
* ../agents/*.md slash-command prompts (requires description: frontmatter)
* ../skills/*.md model-controlled tools (requires description: frontmatter)
*
* Agent/skill bodies are read from disk at invocation time editing any .md
* file takes effect immediately without restarting the server.
*
* Frontmatter fields:
* description (required) routing description for the prompt/tool
* toolName (skills only, optional) override the derived tool name
* default: load_<basename> (e.g. research.md load_research)
*
* Not handled here (stays bespoke):
* hooks/ MCP has no lifecycle intercept primitive
* AGENTS.md always-on bootstrap; model needs it before tools/list
*
* Run: node --experimental-strip-types .agents/mcp/index.ts
* Config: ~/.vscode-server/data/User/mcp.json (Copilot),
* ~/.config/opencode/opencode.json (OpenCode global)
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { readFileSync, readdirSync } from "node:fs";
import { basename, resolve } from "node:path";
import { z } from "zod";
const agentsDir = resolve(import.meta.dirname, "../agents");
const skillsDir = resolve(import.meta.dirname, "../skills");
interface ParsedFile {
description: string;
toolName?: string;
body: string;
}
/** Parse YAML frontmatter and return description, optional toolName, and body. */
function parseFrontmatter(content: string): ParsedFile {
const lines = content.split("\n");
if (lines[0] !== "---") return { description: "", body: content.trim() };
const end = lines.indexOf("---", 1);
if (end === -1) return { description: "", body: content.trim() };
const frontmatter = lines.slice(1, end).join("\n");
const body = lines
.slice(end + 1)
.join("\n")
.trim();
// Simple single-line or quoted-string extraction for description and toolName
const descMatch = frontmatter.match(
/^description:\s*['"]?([\s\S]*?)['"]?\s*$/m,
);
const toolMatch = frontmatter.match(/^toolName:\s*['"]?([^'"]+)['"]?\s*$/m);
// Handle multi-line description values (block scalar or wrapped string)
let description = "";
if (descMatch) {
// If the match includes a leading quote, strip matching quotes
const raw = frontmatter.match(/^description:\s*(['"])([\s\S]*?)\1\s*$/m);
description = raw ? raw[2].trim() : descMatch[1].trim();
}
return {
description,
toolName: toolMatch ? toolMatch[1].trim() : undefined,
body,
};
}
function stripLocalBlocks(body: string): string {
return body.replace(/<!-- @local -->[\s\S]*?<!-- @endlocal -->\n?/g, "");
}
function stripCloudBlocks(body: string): string {
return body.replace(/<!-- @cloud -->[\s\S]*?<!-- @endcloud -->\n?/g, "");
}
/**
* Returns 'local' when the MCP client identifies as `opencode` (local-model
* harness), 'cloud' for any other client (Copilot / VS Code etc.).
*/
function getClientProfile(): "local" | "cloud" {
const info = server.server.getClientVersion();
return info?.name === "opencode" ? "local" : "cloud";
}
function applyClientProfile(body: string): string {
return getClientProfile() === "local"
? stripCloudBlocks(body)
: stripLocalBlocks(body);
}
const server = new McpServer({ name: "all-agents", version: "1.0.0" });
// ── Prompts (auto-discovered from ../agents/*.md) ─────────────────────────────
const agentFiles = readdirSync(agentsDir).filter(
(f) => f.endsWith(".md") && f !== "AGENTS.md",
);
for (const file of agentFiles) {
const name = basename(file, ".md");
const { description, body } = parseFrontmatter(
readFileSync(resolve(agentsDir, file), "utf8"),
);
if (!description) {
process.stderr.write(
`[all-agents] WARNING: ${file} has no description — skipping\n`,
);
continue;
}
const argKey =
name === "orchestrator" ? "goal" : name === "brainstorm" ? "topic" : "task";
const argDesc =
name === "orchestrator"
? "The high-level goal to decompose"
: name === "brainstorm"
? "The problem or decision to brainstorm"
: "The specific task or question";
server.registerPrompt(
name,
{
description,
argsSchema: { [argKey]: z.string().optional().describe(argDesc) },
},
(args: Record<string, string | undefined>) => {
const input = args[argKey];
const agentBody = applyClientProfile(
parseFrontmatter(readFileSync(resolve(agentsDir, file), "utf8")).body,
);
return {
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: input ? `${agentBody}\n\n${input}` : agentBody,
},
},
],
};
},
);
}
// ── Tools (auto-discovered from ../skills/*.md) ───────────────────────────────
const skillFiles = readdirSync(skillsDir).filter((f) => f.endsWith(".md"));
for (const file of skillFiles) {
const name = basename(file, ".md");
const { description, toolName } = parseFrontmatter(
readFileSync(resolve(skillsDir, file), "utf8"),
);
if (!description) {
process.stderr.write(
`[all-agents] WARNING: ${file} has no description — skipping\n`,
);
continue;
}
server.registerTool(toolName ?? `load_${name}`, { description }, () => ({
content: [
{
type: "text" as const,
text: parseFrontmatter(readFileSync(resolve(skillsDir, file), "utf8"))
.body,
},
],
}));
}
// ── Connect ───────────────────────────────────────────────────────────────────
const transport = new StdioServerTransport();
try {
await server.connect(transport);
} catch (err) {
process.stderr.write(
`MCP connect failed: ${err instanceof Error ? err.message : String(err)}\n`,
);
process.exit(1);
}

113
.agents/skills/research.md Normal file
View File

@ -0,0 +1,113 @@
---
description: 'Load the structured research methodology — call this when starting any investigation, debugging session, root cause analysis, or systematic exploration of unfamiliar code. Returns a checklist with two orientations (Understand + Diagnose), risk-based triage, circuit breakers, and context management guidance.'
toolName: 'load_research_methodology'
---
# Research Methodology Skill
This skill provides a structured, evidence-based investigation methodology. It
prevents common AI agent failure modes: pattern-matching without evidence,
confirmation bias, fixing symptoms instead of causes, and methodology drift
during long sessions.
## Quick Reference: The Investigation Checklist
Before every hypothesis cycle:
- [ ] **Hypothesis written** (one sentence: "I believe X because Y")
- [ ] **Falsification criterion written** ("if wrong, I'd expect to see \_\_\_")
- [ ] **Falsification test run BEFORE confirmation test**
- [ ] **Result recorded** (ELIMINATED with reason, or CONFIRMED with evidence)
- [ ] **Hypothesis re-evaluated at this tool-call boundary** — new evidence
changes what to check next. Interleaved thinking makes this automatic for
Claude 4; consciously invoke it for other models.
- [ ] **All traces/instrumentation removed** before next hypothesis
## Two Orientations
### Understand (Grounded Theory)
**Goal**: Build a mental model from the code itself, not assumptions.
1. **Open coding** — Read code, name what you see (functions, patterns, flows)
2. **Constant comparison** — Compare new observations against earlier ones
3. **Axial coding** — Connect the categories (what calls what, data flows)
4. **Memo** — Write findings to session memory as you go
5. **Saturation check** — Stop when new files confirm what you already know
**Use for**: "How does X work?", "What's the architecture?", "I need to
understand this before changing it."
### Diagnose (Strong Inference + Satisficing)
**Goal**: Determine why something isn't working.
**Simple check first**: Can you answer this with a single log/print? If the
question is "what value does X have here?" — just log and look.
**Triage** (if the simple check didn't resolve it):
| Factor | Low Risk | High Risk |
| ----------------- | ------------------------ | ------------------------------ |
| **Reversibility** | Easy to undo | Hard to reverse (data, deploy) |
| **Blast radius** | One file/function | Many systems, shared state |
| **Confidence** | Familiar, clear evidence | Novel, ambiguous symptoms |
| **Novelty** | Seen this before | Never encountered |
| **Time cost** | Known fast (<5s) | Unknown = measure first |
**Low risk → Satisfice**: Test the single most likely hypothesis. Done if
confirmed.
**Any high risk → Strong Inference**: Generate 2-3 competing hypotheses, design
a discriminating test, eliminate based on evidence.
### Mode Switching
These compose recursively:
`Understand → anomaly → Diagnose → need context → Understand → ...`
## Circuit Breakers
1. **5+ attempts without falsifying = STOP and report**
2. **3+ edits to same file without passing test = STOP and rethink**
3. **Urge to "just try something" = STOP and write hypothesis first**
4. **Two failures at same abstraction level = go UP one level**
## Context Management
Methodology degrades after ~15 tool calls (context competition). Counteract:
- Re-read investigation file and dead-ends every ~10 tool calls
- If drifting toward guess-and-check, pause and re-read notes
- For long sessions, create an investigation file so fresh context can continue
- Hold references; load on demand. Do not read files you don't need yet.
## Dead-Ends Format
Record eliminated hypotheses so you (or the next session) don't re-test them:
```
- **[timestamp] Hypothesis:** [one sentence]
**Falsification:** [what you'd expect if wrong]
**Result:** [ELIMINATED/CONFIRMED] — [why, in one sentence]
```
Write to `.session/dead-ends.md` or the investigation file's Hypotheses section.
## Timing Awareness
- Prefix unknown commands with `time` to learn baselines
- Capture output: `time npm test 2>&1 | tee /tmp/test_output.txt`
- Fast (<5s): low barrier to run. Slow (>30s): reason first. Unknown: measure.
## Techniques
- **Five Whys**: Trace causal chains. Starting point, not sole method.
- **Delta Debugging**: Binary search between passing/failing cases (`git bisect`
logic).
- **Rubber Duck**: Explain the system step by step in writing to expose gaps.
## Full Agent
For comprehensive investigation support with delegation, exploration files, and
session memory management, use `@research`.