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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/instruction-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Rules:
- Workspace instructions are checked first, then global instructions
- The first matching section wins (at most one section is used)
- The section's content is everything until the next heading of the same or higher level
- Mode sections are stripped from the general `<custom-instructions>` block; only the active mode's content is re-sent via its `<mode>` tag.
- Missing sections are ignored (no error)

<!-- Note to developers: This behavior is implemented in src/services/systemMessage.ts (search for extractModeSection). Keep this documentation in sync with code changes. -->
Expand Down Expand Up @@ -62,6 +63,32 @@ When compacting conversation history:

Customizing the `compact` mode is particularly useful for controlling what information is preserved during automatic history compaction.

## Model Prompts

Similar to modes, mux reads headings titled `Model: <regex>` to scope instructions to specific models or families. The `<regex>` is matched against the full model identifier (for example, `openai:gpt-5.1-codex`).

Rules:

- Workspace instructions are evaluated before global instructions; the first matching section wins.
- Regexes are case-insensitive by default. Use `/pattern/flags` syntax to opt into custom flags (e.g., `/openai:.*codex/i`).
- Invalid regex patterns are ignored instead of breaking the parse.
- Model sections are also removed from `<custom-instructions>`; only the first regex match (if any) is injected via its `<model-…>` tag.
- Only the content under the first matching heading is injected.

<!-- Developers: See extractModelSection in src/node/utils/main/markdown.ts for the implementation. -->

Example:

```markdown
## Model: sonnet

Be terse and to the point.

## Model: openai:.\*codex

Use status reporting tools every few minutes.
```

## Practical layout

```
Expand Down
3 changes: 2 additions & 1 deletion src/node/services/aiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,8 @@ export class AIService extends EventEmitter {
runtime,
workspacePath,
mode,
additionalSystemInstructions
additionalSystemInstructions,
modelString
);

// Count system message tokens for cost tracking
Expand Down
223 changes: 220 additions & 3 deletions src/node/services/systemMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import * as path from "path";
import { buildSystemMessage } from "./systemMessage";
import type { WorkspaceMetadata } from "@/common/types/workspace";
import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace";

const extractTagContent = (message: string, tagName: string): string | null => {
const pattern = new RegExp(`<${tagName}>\\s*([\\s\\S]*?)\\s*</${tagName}>`, "i");
const match = pattern.exec(message);
return match ? match[1].trim() : null;
};
import { describe, test, expect, beforeEach, afterEach, spyOn, type Mock } from "bun:test";
import { LocalRuntime } from "@/node/runtime/LocalRuntime";

Expand Down Expand Up @@ -63,6 +69,10 @@ Use diagrams where appropriate.

const systemMessage = await buildSystemMessage(metadata, runtime, workspaceDir, "plan");

const customInstructions = extractTagContent(systemMessage, "custom-instructions") ?? "";
expect(customInstructions).toContain("Always be helpful.");
expect(customInstructions).not.toContain("Focus on planning and design.");

// Should include the mode-specific content
expect(systemMessage).toContain("<plan>");
expect(systemMessage).toContain("Focus on planning and design");
Expand Down Expand Up @@ -99,9 +109,9 @@ Focus on planning and design.
expect(systemMessage).not.toContain("<plan>");
expect(systemMessage).not.toContain("</plan>");

// All instructions are still in <custom-instructions> (both general and mode section)
expect(systemMessage).toContain("Always be helpful");
expect(systemMessage).toContain("Focus on planning and design");
const customInstructions = extractTagContent(systemMessage, "custom-instructions") ?? "";
expect(customInstructions).toContain("Always be helpful.");
expect(customInstructions).not.toContain("Focus on planning and design.");
});

test("prefers project mode section over global mode section", async () => {
Expand Down Expand Up @@ -203,4 +213,211 @@ Special mode instructions.
expect(systemMessage).toContain("Special mode instructions");
expect(systemMessage).toContain("</my-special_mode->");
});

test("includes model-specific section when regex matches active model", async () => {
await fs.writeFile(
path.join(projectDir, "AGENTS.md"),
`# Instructions
## Model: sonnet
Respond to Sonnet tickets in two sentences max.
`
);

const metadata: WorkspaceMetadata = {
id: "test-workspace",
name: "test-workspace",
projectName: "test-project",
projectPath: projectDir,
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
};

const systemMessage = await buildSystemMessage(
metadata,
runtime,
workspaceDir,
undefined,
undefined,
"anthropic:claude-3.5-sonnet"
);

const customInstructions = extractTagContent(systemMessage, "custom-instructions") ?? "";
expect(customInstructions).not.toContain("Respond to Sonnet tickets in two sentences max.");

expect(systemMessage).toContain("<model-anthropic-claude-3-5-sonnet>");
expect(systemMessage).toContain("Respond to Sonnet tickets in two sentences max.");
expect(systemMessage).toContain("</model-anthropic-claude-3-5-sonnet>");
});

test("falls back to global model section when project lacks a match", async () => {
await fs.writeFile(
path.join(globalDir, "AGENTS.md"),
`# Global Instructions
## Model: /openai:.*codex/i
OpenAI's GPT-5.1 Codex models already default to terse replies.
`
);

await fs.writeFile(
path.join(projectDir, "AGENTS.md"),
`# Project Instructions
General details only.
`
);

const metadata: WorkspaceMetadata = {
id: "test-workspace",
name: "test-workspace",
projectName: "test-project",
projectPath: projectDir,
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
};

const systemMessage = await buildSystemMessage(
metadata,
runtime,
workspaceDir,
undefined,
undefined,
"openai:gpt-5.1-codex"
);

const customInstructions = extractTagContent(systemMessage, "custom-instructions") ?? "";
expect(customInstructions).not.toContain(
"OpenAI's GPT-5.1 Codex models already default to terse replies."
);

expect(systemMessage).toContain("<model-openai-gpt-5-1-codex>");
expect(systemMessage).toContain(
"OpenAI's GPT-5.1 Codex models already default to terse replies."
);
});

describe("instruction scoping matrix", () => {
interface Scenario {
name: string;
mdContent: string;
mode?: string;
model?: string;
assert: (message: string) => void;
}

const scopingScenarios: Scenario[] = [
{
name: "strips scoped sections when no mode or model provided",
mdContent: `# Notes
General guidance for everyone.

## Mode: Plan
Plan depth instructions.

## Model: sonnet
Anthropic-only instructions.
`,
assert: (message) => {
const custom = extractTagContent(message, "custom-instructions") ?? "";
expect(custom).toContain("General guidance for everyone.");
expect(custom).not.toContain("Plan depth instructions.");
expect(custom).not.toContain("Anthropic-only instructions.");
expect(message).not.toContain("Plan depth instructions.");
expect(message).not.toContain("Anthropic-only instructions.");
},
},
{
name: "injects only the requested mode section",
mdContent: `General context for all contributors.

## Mode: Plan
Plan-only reminders.

## Mode: Exec
Exec reminders.
`,
mode: "plan",
assert: (message) => {
const custom = extractTagContent(message, "custom-instructions") ?? "";
expect(custom).toContain("General context for all contributors.");
expect(custom).not.toContain("Plan-only reminders.");
expect(custom).not.toContain("Exec reminders.");

const planSection = extractTagContent(message, "plan") ?? "";
expect(planSection).toContain("Plan-only reminders.");
expect(planSection).not.toContain("Exec reminders.");
},
},
{
name: "injects only the matching model section",
mdContent: `General base instructions.

## Model: sonnet
Anthropic-only instructions.

## Model: /openai:.*/
OpenAI-only instructions.
`,
model: "openai:gpt-5.1-codex",
assert: (message) => {
const custom = extractTagContent(message, "custom-instructions") ?? "";
expect(custom).toContain("General base instructions.");
expect(custom).not.toContain("Anthropic-only instructions.");
expect(custom).not.toContain("OpenAI-only instructions.");

const openaiSection = extractTagContent(message, "model-openai-gpt-5-1-codex") ?? "";
expect(openaiSection).toContain("OpenAI-only instructions.");
expect(openaiSection).not.toContain("Anthropic-only instructions.");
expect(message).not.toContain("Anthropic-only instructions.");
},
},
{
name: "supports simultaneous mode and model scoping",
mdContent: `General instructions for everyone.

## Mode: Exec
Stay focused on implementation details.

## Model: sonnet
Answer in two sentences max.
`,
mode: "exec",
model: "anthropic:claude-3.5-sonnet",
assert: (message) => {
const custom = extractTagContent(message, "custom-instructions") ?? "";
expect(custom).toContain("General instructions for everyone.");
expect(custom).not.toContain("Stay focused on implementation details.");
expect(custom).not.toContain("Answer in two sentences max.");

const execSection = extractTagContent(message, "exec") ?? "";
expect(execSection).toContain("Stay focused on implementation details.");

const sonnetSection =
extractTagContent(message, "model-anthropic-claude-3-5-sonnet") ?? "";
expect(sonnetSection).toContain("Answer in two sentences max.");
},
},
];

for (const scenario of scopingScenarios) {
test(scenario.name, async () => {
await fs.writeFile(path.join(projectDir, "AGENTS.md"), scenario.mdContent);

const metadata: WorkspaceMetadata = {
id: "test-workspace",
name: "test-workspace",
projectName: "test-project",
projectPath: projectDir,
runtimeConfig: DEFAULT_RUNTIME_CONFIG,
};

const systemMessage = await buildSystemMessage(
metadata,
runtime,
workspaceDir,
scenario.mode,
undefined,
scenario.model
);

scenario.assert(systemMessage);
});
}
});
});
Loading