Skip to content

fix(opencode): use native instructions config to load CLAUDE.md and fragments#2153

Merged
gavrielc merged 2 commits into
nanocoai:providersfrom
glifocat:fix/opencode-instructions-pipeline
May 1, 2026
Merged

fix(opencode): use native instructions config to load CLAUDE.md and fragments#2153
gavrielc merged 2 commits into
nanocoai:providersfrom
glifocat:fix/opencode-instructions-pipeline

Conversation

@glifocat
Copy link
Copy Markdown
Collaborator

Type of Change

  • Feature skill - adds a channel or integration (source code changes + SKILL.md)
  • Utility skill - adds a standalone tool (code files in .claude/skills/<name>/, no source changes)
  • Operational/container skill - adds a workflow or agent skill (SKILL.md only, no source changes)
  • Fix - bug fix or security fix to source code
  • Simplification - reduces or simplifies source code
  • Documentation - docs, README, or CONTRIBUTING changes only

Description

Closes #2150.

wrapPromptWithContext previously concatenated /workspace/agent/CLAUDE.md verbatim into a <system>...</system> block in the user-message text. That file is the host-composed entry point that contains only @./... includes (the .claude-shared.md symlink to /app/CLAUDE.md, plus the module fragments). OpenCode does not expand @ in instruction files — the syntax is a Claude Code convention for the model's own Read tool to lazy-load. Result: the model received the literal lines @./.claude-shared.md\n@./.claude-fragments/module-...md as text and saw none of the actual fragment content (workspace, memory, conversation history, agents/core/interactive/scheduling/self-mod modules, OneCLI, etc.). Strong-prior models (Claude, GPT-4) masked the gap; weaker local models visibly lacked NanoClaw conventions.

This PR routes the concrete files through OpenCode's native instructions config field. Per packages/opencode/src/session/instruction.ts on the upstream dev branch (already shipped in @opencode-ai/sdk@1.4.17, the version pinned by /add-opencode), absolute paths and globs are resolved with fs.glob(basename, { cwd: dirname, absolute: true, include: 'file' }), files are read raw, and the resulting strings are concatenated into the LLM's system prompt at packages/opencode/src/session/prompt.ts:1442. That is the canonical channel — same one OpenCode uses for AGENTS.md / CLAUDE.md auto-discovery.

Configured set

const instructions = [
  '/app/CLAUDE.md',                              // shared base
  '/workspace/agent/.claude-fragments/*.md',     // per-skill fragments
  '/workspace/agent/CLAUDE.local.md',            // per-group memory
];
if (process.env.NANOCLAW_IS_MAIN !== '1') {
  instructions.push('/workspace/global/CLAUDE.md');  // cross-group memory (non-main)
}

Removed: readClaudeMdForPrompt, the manual <system> wrap of its output in wrapPromptWithContext, and the now-unused fs import. The dynamic systemInstructions wrap (assistant name + destinations) stays as-is — that's per-call data, not file content.

Verification

Empirically tested with gemma4:31b on local Ollama before the fix: agent replied "I haven't received instructions" to "what self-modification tools do you have". Post-fix: agent listed the exact MCP tool names (install_packages, add_mcp_server) and structured response from module-self-mod.md content. Base model has no priors for those tools, so the only place that knowledge can come from is the fragment now reaching the system prompt.

Note on composeGroupClaudeMd

src/claude-md-compose.ts still produces /workspace/agent/CLAUDE.md with @./... includes. That file remains required for the Claude provider (whose SDK does expand @ correctly) but is now redundant for OpenCode users — OpenCode also auto-discovers it via findUp and adds the literal text again, but instructions takes precedence for actual content. Cleanup is out of scope for this fix; flagging so future work knows.

Related

For Skills

  • SKILL.md contains instructions, not inline code (code goes in separate files)
  • SKILL.md is under 500 lines
  • I tested this skill on a fresh clone

Not a skill PR — section N/A.

Before: wrapPromptWithContext concatenated /workspace/agent/CLAUDE.md
verbatim into a <system>...</system> block in the user-message text.
That file is the host-composed entry point that contains only `@./...`
includes (the .claude-shared.md symlink to /app/CLAUDE.md, plus the
module fragments). OpenCode does not expand `@` in instruction files —
the syntax is a Claude Code convention for the model's own Read tool
to lazy-load. Result: the model received the literal lines
"@./.claude-shared.md\n@./.claude-fragments/module-...md" as text and
saw none of the actual content (workspace, memory, conversation
history, agents/core/interactive/scheduling/self-mod modules, OneCLI,
etc.). Confirmed empirically by dumping the constructed prompt for a
turn before any fix.

After: pass the concrete files via OpenCode's native `instructions`
config field. Per packages/opencode/src/session/instruction.ts on the
upstream `dev` branch, absolute paths and globs are resolved with
`fs.glob(basename, { cwd: dirname, absolute: true, include: 'file' })`,
files are read raw, and the resulting strings are concatenated into
the LLM's system prompt at packages/opencode/src/session/prompt.ts:1442.
That is the canonical channel — same one OpenCode uses for AGENTS.md /
CLAUDE.md auto-discovery.

Configured set:
  /app/CLAUDE.md                              shared base
  /workspace/agent/.claude-fragments/*.md     per-skill fragments
  /workspace/agent/CLAUDE.local.md            per-group memory
  /workspace/global/CLAUDE.md                 cross-group memory (non-main)

Removed: readClaudeMdForPrompt, the manual <system> wrap of its output,
and the now-unused `fs` import. The dynamic systemInstructions wrap
(assistant name + destinations) stays as-is — that varies per call.

Verified post-fix: Citiclaw (gemma4:31b via Ollama) responded to "what
self-modification tools do you have" with the exact MCP tool names and
a structured bullet list — vs. the previous ungrounded "I haven't
received instructions" response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NANOCLAW_IS_MAIN no longer exists in the v2 codebase, and
groups/global/ is explicitly migrated away by composeGroupClaudeMd
(shared base now lives in container/CLAUDE.md → /app/CLAUDE.md, which
the instructions array already includes). Carrying /workspace/global
would give OpenCode more context than the Claude provider sees.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

follows-guidelines PR was created using the current contributing template PR: Fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants