11import * as os from "os" ;
22import * as path from "path" ;
33import type { WorkspaceMetadata } from "@/types/workspace" ;
4- import { readInstructionSet , INSTRUCTION_FILE_NAMES } from "@/utils/main/instructionFiles" ;
4+ import { readInstructionSet , readInstructionSetFromRuntime } from "@/utils/main/instructionFiles" ;
55import { extractModeSection } from "@/utils/main/markdown" ;
66import type { Runtime } from "@/runtime/Runtime" ;
7- import { readFileString } from "@/utils/runtime/helpers" ;
87
98// NOTE: keep this in sync with the docs/models.md file
109
@@ -30,6 +29,9 @@ Use GitHub-style \`<details>/<summary>\` tags to create collapsible sections for
3029</prelude>
3130` ;
3231
32+ /**
33+ * Build environment context XML block describing the workspace.
34+ */
3335function buildEnvironmentContext ( workspacePath : string ) : string {
3436 return `
3537<environment>
@@ -44,121 +46,30 @@ You are in a git worktree at ${workspacePath}
4446}
4547
4648/**
47- * The system directory where global cmux configuration lives.
48- * This is where users can place global AGENTS.md and .cmux/PLAN.md files
49- * that apply to all workspaces.
49+ * Get the system directory where global cmux configuration lives.
50+ * Users can place global AGENTS.md and .cmux/PLAN.md files here.
5051 */
5152function getSystemDirectory ( ) : string {
5253 return path . join ( os . homedir ( ) , ".cmux" ) ;
5354}
5455
5556/**
56- * Read the first available file from a list using runtime .
57+ * Builds a system message for the AI model by combining instruction sources .
5758 *
58- * @param runtime - Runtime instance (may be local or SSH)
59- * @param directory - Directory to search in
60- * @param filenames - List of filenames to try, in priority order
61- * @returns Content of the first file found, or null if none exist
62- */
63- async function readFirstAvailableFileFromRuntime (
64- runtime : Runtime ,
65- directory : string ,
66- filenames : readonly string [ ]
67- ) : Promise < string | null > {
68- for ( const filename of filenames ) {
69- try {
70- const filePath = path . join ( directory , filename ) ;
71- return await readFileString ( runtime , filePath ) ;
72- } catch {
73- // File doesn't exist or can't be read, try next
74- continue ;
75- }
76- }
77- return null ;
78- }
79-
80- /**
81- * Read a file with optional local variant using runtime.
82- * Follows the same pattern as readFileWithLocalVariant but uses Runtime.
59+ * Instruction layers:
60+ * 1. Global: ~/.cmux/AGENTS.md (always included)
61+ * 2. Context: workspace/AGENTS.md OR project/AGENTS.md (workspace takes precedence)
62+ * 3. Mode: Extracts "Mode: <mode>" section from context then global (if mode provided)
8363 *
84- * @param runtime - Runtime instance (may be local or SSH)
85- * @param directory - Directory to search
86- * @param baseFilenames - Base filenames to try in priority order
87- * @param localFilename - Optional local filename to append if present
88- * @returns Combined content or null if no base file exists
89- */
90- async function readFileWithLocalVariantFromRuntime (
91- runtime : Runtime ,
92- directory : string ,
93- baseFilenames : readonly string [ ] ,
94- localFilename ?: string
95- ) : Promise < string | null > {
96- const baseContent = await readFirstAvailableFileFromRuntime ( runtime , directory , baseFilenames ) ;
97-
98- if ( ! baseContent ) {
99- return null ;
100- }
101-
102- if ( ! localFilename ) {
103- return baseContent ;
104- }
105-
106- try {
107- const localFilePath = path . join ( directory , localFilename ) ;
108- const localContent = await readFileString ( runtime , localFilePath ) ;
109- return `${ baseContent } \n\n${ localContent } ` ;
110- } catch {
111- return baseContent ;
112- }
113- }
114-
115- /**
116- * Read instruction set from a workspace using the runtime abstraction.
117- * This supports both local workspaces and remote SSH workspaces.
64+ * File search order: AGENTS.md → AGENT.md → CLAUDE.md
65+ * Local variants: AGENTS.local.md appended if found (for .gitignored personal preferences)
11866 *
119- * @param runtime - Runtime instance (may be local or SSH)
120- * @param workspacePath - Path to workspace directory
121- * @returns Combined instruction content, or null if no base file exists
122- */
123- async function readInstructionSetFromRuntime (
124- runtime : Runtime ,
125- workspacePath : string
126- ) : Promise < string | null > {
127- const LOCAL_INSTRUCTION_FILENAME = "AGENTS.local.md" ;
128- return readFileWithLocalVariantFromRuntime (
129- runtime ,
130- workspacePath ,
131- INSTRUCTION_FILE_NAMES ,
132- LOCAL_INSTRUCTION_FILENAME
133- ) ;
134- }
135-
136- /**
137- * Builds a system message for the AI model by combining multiple instruction sources.
138- *
139- * Instruction sources are layered as follows:
140- * 1. Global instructions: ~/.cmux/AGENTS.md (+ AGENTS.local.md) - always included
141- * 2. Context instructions: EITHER workspace OR project AGENTS.md (not both)
142- * - Workspace: <workspacePath>/AGENTS.md (+ AGENTS.local.md) - if exists (read via runtime)
143- * - Project: <projectPath>/AGENTS.md (+ AGENTS.local.md) - fallback if workspace doesn't exist
144- * 3. Mode-specific context (if mode provided): Extract a section titled "Mode: <mode>"
145- * (case-insensitive) from the instruction file. Priority: context instructions, then global.
146- *
147- * Each instruction file location is searched for in priority order:
148- * - AGENTS.md
149- * - AGENT.md
150- * - CLAUDE.md
151- *
152- * If a base instruction file is found, its corresponding .local.md variant is also
153- * checked and appended when building the instruction set (useful for personal preferences not committed to git).
154- *
155- * @param metadata - Workspace metadata (contains projectPath for reading AGENTS.md)
156- * @param runtime - Runtime instance for reading workspace files (may be remote)
157- * @param workspacePath - Absolute path to the workspace directory (for environment context)
158- * @param mode - Optional mode name (e.g., "plan", "exec") - looks for {MODE}.md files if provided
159- * @param additionalSystemInstructions - Optional additional system instructions to append at the end
160- * @returns System message string with all instruction sources combined
161- * @throws Error if metadata is invalid
67+ * @param metadata - Workspace metadata (contains projectPath)
68+ * @param runtime - Runtime for reading workspace files (supports SSH)
69+ * @param workspacePath - Workspace directory path
70+ * @param mode - Optional mode name (e.g., "plan", "exec")
71+ * @param additionalSystemInstructions - Optional instructions appended last
72+ * @throws Error if metadata or workspacePath invalid
16273 */
16374export async function buildSystemMessage (
16475 metadata : WorkspaceMetadata ,
@@ -167,67 +78,40 @@ export async function buildSystemMessage(
16778 mode ?: string ,
16879 additionalSystemInstructions ?: string
16980) : Promise < string > {
170- // Validate inputs
171- if ( ! metadata ) {
172- throw new Error ( "Invalid workspace metadata: metadata is required" ) ;
173- }
174- if ( ! workspacePath ) {
175- throw new Error ( "Invalid workspace path: workspacePath is required" ) ;
176- }
81+ if ( ! metadata ) throw new Error ( "Invalid workspace metadata: metadata is required" ) ;
82+ if ( ! workspacePath ) throw new Error ( "Invalid workspace path: workspacePath is required" ) ;
17783
178- const systemDir = getSystemDirectory ( ) ;
179- const projectDir = metadata . projectPath ;
84+ // Read instruction sets
85+ const globalInstructions = await readInstructionSet ( getSystemDirectory ( ) ) ;
86+ const workspaceInstructions = await readInstructionSetFromRuntime ( runtime , workspacePath ) ;
87+ const contextInstructions = workspaceInstructions ?? ( await readInstructionSet ( metadata . projectPath ) ) ;
18088
181- // Layer 1: Global instructions (always included)
182- const globalInstructions = await readInstructionSet ( systemDir ) ;
89+ // Combine: global + context (workspace takes precedence over project)
90+ const customInstructions = [ globalInstructions , contextInstructions ]
91+ . filter ( Boolean )
92+ . join ( "\n\n" ) ;
18393
184- // Layer 2: Workspace OR Project instructions (not both)
185- // Try workspace first (via runtime, may be remote for SSH)
186- // Fall back to project if workspace doesn't have AGENTS.md
187- const workspaceInstructions = await readInstructionSetFromRuntime ( runtime , workspacePath ) ;
188- const projectInstructions = workspaceInstructions ? null : await readInstructionSet ( projectDir ) ;
189-
190- // Combine instruction sources
191- // Result: global + (workspace OR project)
192- const instructionSegments = [
193- globalInstructions ,
194- workspaceInstructions ?? projectInstructions ,
195- ] . filter ( Boolean ) ;
196- const customInstructions = instructionSegments . join ( "\n\n" ) ;
197-
198- // Look for a "Mode: <mode>" section inside instruction sets
199- // Priority: workspace (or project fallback), then global
200- // We only check the workspace OR project instructions, not both
201- // This behavior is documented in docs/instruction-files.md - keep both in sync when changing.
94+ // Extract mode-specific section (context first, then global fallback)
20295 let modeContent : string | null = null ;
20396 if ( mode ) {
204- const contextInstructions = workspaceInstructions ?? projectInstructions ;
205- if ( contextInstructions ) {
206- modeContent = extractModeSection ( contextInstructions , mode ) ;
207- }
208- if ( ! modeContent && globalInstructions ) {
209- modeContent = extractModeSection ( globalInstructions , mode ) ;
210- }
97+ modeContent =
98+ ( contextInstructions && extractModeSection ( contextInstructions , mode ) ) ||
99+ ( globalInstructions && extractModeSection ( globalInstructions , mode ) ) ||
100+ null ;
211101 }
212102
213- // Build the final system message
214- // Use workspacePath for environment context (where code actually executes)
215- const environmentContext = buildEnvironmentContext ( workspacePath ) ;
216- const trimmedPrelude = PRELUDE . trim ( ) ;
217- let systemMessage = `${ trimmedPrelude } \n\n${ environmentContext } ` ;
103+ // Build system message
104+ let systemMessage = `${ PRELUDE . trim ( ) } \n\n${ buildEnvironmentContext ( workspacePath ) } ` ;
218105
219- // Add custom instructions if found
220106 if ( customInstructions ) {
221107 systemMessage += `\n<custom-instructions>\n${ customInstructions } \n</custom-instructions>` ;
222108 }
223109
224- // Add mode-specific content if found
225110 if ( modeContent ) {
226111 const tag = ( mode ?? "mode" ) . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 _ - ] / gi, "-" ) ;
227112 systemMessage += `\n\n<${ tag } >\n${ modeContent } \n</${ tag } >` ;
228113 }
229114
230- // Add additional system instructions at the end (highest priority)
231115 if ( additionalSystemInstructions ) {
232116 systemMessage += `\n\n<additional-instructions>\n${ additionalSystemInstructions } \n</additional-instructions>` ;
233117 }
0 commit comments