Skip to content

Commit ca4fdc8

Browse files
committed
🤖 feat: unify include_files and <@path> context tags
1 parent 22b7f7f commit ca4fdc8

19 files changed

+991
-26
lines changed

bun.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/agent-skills.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Optional fields:
5757
- `license`
5858
- `compatibility`
5959
- `metadata` (string key/value map)
60+
- `include_files` (glob patterns, relative to the skill directory, for files to inline when the skill is loaded)
6061

6162
Mux ignores unknown frontmatter keys (for example `allowed-tools`).
6263

@@ -69,6 +70,8 @@ description: Build and validate a release branch.
6970
license: MIT
7071
metadata:
7172
owner: platform
73+
include_files:
74+
- references/**/*.md
7275
---
7376

7477
# My Skill

docs/agents.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ name: My Agent # Display name in UI
7676

7777
# Optional
7878
description: What this agent does # Shown in tooltips
79+
80+
# Auto-include file context (workspace-root relative globs)
81+
include_files:
82+
- docs/**/*.md
83+
- src/**/*.ts
7984
base: exec # Inherit from another agent (exec, plan, or custom agent id)
8085

8186
# UI settings

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"ollama-ai-provider-v2": "^1.5.4",
102102
"openai": "^6.9.1",
103103
"parse-duration": "^2.1.4",
104+
"picomatch": "^4.0.3",
104105
"posthog-node": "^5.17.0",
105106
"quickjs-emscripten": "^0.31.0",
106107
"quickjs-emscripten-core": "^0.31.0",
@@ -147,6 +148,7 @@
147148
"@types/katex": "^0.16.7",
148149
"@types/markdown-it": "^14.1.2",
149150
"@types/minimist": "^1.2.5",
151+
"@types/picomatch": "^4.0.2",
150152
"@types/react": "^18.2.0",
151153
"@types/react-dom": "^18.2.0",
152154
"@types/turndown": "^5.0.6",

src/common/orpc/schemas.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export {
4545
AgentSkillFrontmatterSchema,
4646
AgentSkillPackageSchema,
4747
AgentSkillScopeSchema,
48+
IncludeFileGlobSchema,
49+
IncludedFileSchema,
50+
IncludeFilesContextSchema,
4851
SkillNameSchema,
4952
} from "./schemas/agentSkill";
5053

src/common/orpc/schemas/agentDefinition.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { z } from "zod";
22

3+
import { IncludeFileGlobSchema } from "./agentSkill";
4+
35
export const AgentDefinitionScopeSchema = z.enum(["built-in", "project", "global"]);
46

57
// Agent IDs come from filenames (<agentId>.md).
@@ -46,7 +48,7 @@ const AgentDefinitionAiDefaultsSchema = z
4648

4749
const AgentDefinitionPromptSchema = z
4850
.object({
49-
// When true, append this agent's body to the base agent's body (default: false = replace)
51+
// When true, append this agent's body to the base agent's body (default: true); set false to replace.
5052
append: z.boolean().optional(),
5153
})
5254
.strip();
@@ -67,6 +69,12 @@ export const AgentDefinitionFrontmatterSchema = z
6769
name: z.string().min(1).max(128),
6870
description: z.string().min(1).max(1024).optional(),
6971

72+
/**
73+
* Glob patterns for files to automatically include in context when this agent runs.
74+
* Patterns are relative to the workspace root (where tools run).
75+
* Example: ["docs/**", "src/**"]
76+
*/
77+
include_files: z.array(IncludeFileGlobSchema).max(20).optional(),
7078
// Inheritance: reference a built-in or custom agent ID
7179
base: AgentIdSchema.optional(),
7280

src/common/orpc/schemas/agentSkill.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,35 @@ export const SkillNameSchema = z
1515
.max(64)
1616
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
1717

18+
/**
19+
* Glob pattern for include_files.
20+
* Patterns must be relative (no absolute paths, no ~, no ..).
21+
*/
22+
export const IncludeFileGlobSchema = z
23+
.string()
24+
.min(1)
25+
.max(256)
26+
.refine(
27+
(pattern) =>
28+
!pattern.startsWith("/") &&
29+
!pattern.startsWith("~") &&
30+
!pattern.includes("..") &&
31+
!/^[A-Za-z]:[\\/]/.test(pattern),
32+
{ message: "Pattern must be relative (no absolute paths, ~, or ..)" }
33+
);
34+
1835
export const AgentSkillFrontmatterSchema = z.object({
1936
name: SkillNameSchema,
2037
description: z.string().min(1).max(1024),
2138
license: z.string().optional(),
2239
compatibility: z.string().min(1).max(500).optional(),
2340
metadata: z.record(z.string(), z.string()).optional(),
41+
/**
42+
* Glob patterns for files to automatically include in context when the skill is read.
43+
* Patterns are relative to the skill directory (where SKILL.md lives).
44+
* Example: ["examples/*.ts", "schemas/**\/*.json"]
45+
*/
46+
include_files: z.array(IncludeFileGlobSchema).max(20).optional(),
2447
});
2548

2649
export const AgentSkillDescriptorSchema = z.object({
@@ -29,12 +52,39 @@ export const AgentSkillDescriptorSchema = z.object({
2952
scope: AgentSkillScopeSchema,
3053
});
3154

55+
/**
56+
* Resolved file from include_files expansion.
57+
*/
58+
export const IncludedFileSchema = z.object({
59+
/** Path relative to skill directory */
60+
path: z.string(),
61+
/** File content (may be truncated) */
62+
content: z.string(),
63+
/** Whether content was truncated due to size/line limits */
64+
truncated: z.boolean(),
65+
});
66+
67+
/**
68+
* Context representation for files included via include_files.
69+
* Rendered as XML using the `<@path>` tag format.
70+
*/
71+
export const IncludeFilesContextSchema = z.object({
72+
/** Successfully resolved files */
73+
files: z.array(IncludedFileSchema),
74+
/** Patterns/files that had errors during resolution */
75+
errors: z.array(z.object({ pattern: z.string(), error: z.string() })),
76+
/** Pre-rendered XML context (for direct injection) */
77+
rendered: z.string(),
78+
});
79+
3280
export const AgentSkillPackageSchema = z
3381
.object({
3482
scope: AgentSkillScopeSchema,
3583
directoryName: SkillNameSchema,
3684
frontmatter: AgentSkillFrontmatterSchema,
3785
body: z.string(),
86+
/** Resolved include_files context (present when frontmatter.include_files is set) */
87+
includeFilesContext: IncludeFilesContextSchema.optional(),
3888
})
3989
.refine((value) => value.directoryName === value.frontmatter.name, {
4090
message: "SKILL.md frontmatter.name must match the parent directory name",

src/node/services/agentDefinitions/agentDefinitionsService.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
discoverAgentDefinitions,
1111
readAgentDefinition,
1212
resolveAgentBody,
13+
resolveAgentIncludeFilesPatterns,
1314
} from "./agentDefinitionsService";
1415

1516
async function writeAgent(root: string, id: string, name: string): Promise<void> {
@@ -129,4 +130,68 @@ Replaced body.
129130
expect(replacerBody).toBe("Replaced body.\n");
130131
expect(replacerBody).not.toContain("Base instructions");
131132
});
133+
134+
test("resolveAgentIncludeFilesPatterns inherits by default, stops when prompt.append is false", async () => {
135+
using tempDir = new DisposableTempDir("agent-include-files-patterns");
136+
const agentsRoot = path.join(tempDir.path, ".mux", "agents");
137+
await fs.mkdir(agentsRoot, { recursive: true });
138+
139+
await fs.writeFile(
140+
path.join(agentsRoot, "base.md"),
141+
`---
142+
name: Base
143+
include_files:
144+
- "README.md"
145+
- "src/*.ts"
146+
---
147+
Base instructions.
148+
`,
149+
"utf-8"
150+
);
151+
152+
await fs.writeFile(
153+
path.join(agentsRoot, "child.md"),
154+
`---
155+
name: Child
156+
base: base
157+
include_files:
158+
- "src/*.ts"
159+
- "docs/**/*.md"
160+
---
161+
Child instructions.
162+
`,
163+
"utf-8"
164+
);
165+
166+
await fs.writeFile(
167+
path.join(agentsRoot, "replacer.md"),
168+
`---
169+
name: Replacer
170+
base: base
171+
prompt:
172+
append: false
173+
include_files:
174+
- "only-child.txt"
175+
---
176+
Replaced body.
177+
`,
178+
"utf-8"
179+
);
180+
181+
const roots = { projectRoot: agentsRoot, globalRoot: agentsRoot };
182+
const runtime = new LocalRuntime(tempDir.path);
183+
184+
const childPatterns = await resolveAgentIncludeFilesPatterns(runtime, tempDir.path, "child", {
185+
roots,
186+
});
187+
expect(childPatterns).toEqual(["README.md", "src/*.ts", "docs/**/*.md"]);
188+
189+
const replacerPatterns = await resolveAgentIncludeFilesPatterns(
190+
runtime,
191+
tempDir.path,
192+
"replacer",
193+
{ roots }
194+
);
195+
expect(replacerPatterns).toEqual(["only-child.txt"]);
196+
});
132197
});

src/node/services/agentDefinitions/agentDefinitionsService.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,3 +412,46 @@ export async function resolveAgentBody(
412412

413413
return resolve(agentId, 0);
414414
}
415+
416+
/**
417+
* Resolve include_files patterns for an agent, including inherited patterns.
418+
*
419+
* Inheritance behavior mirrors resolveAgentBody:
420+
* - If `prompt.append` is false, we do NOT inherit include_files from the base.
421+
* - Otherwise, base patterns are prepended (base first, then child).
422+
*/
423+
export async function resolveAgentIncludeFilesPatterns(
424+
runtime: Runtime,
425+
workspacePath: string,
426+
agentId: AgentId,
427+
options?: { roots?: AgentDefinitionsRoots }
428+
): Promise<string[]> {
429+
const visited = new Set<AgentId>();
430+
431+
async function resolve(id: AgentId, depth: number): Promise<string[]> {
432+
checkInheritanceChain(visited, id, depth);
433+
434+
const pkg = await readAgentDefinition(runtime, workspacePath, id, options);
435+
const baseId = pkg.frontmatter.base;
436+
const shouldAppend = pkg.frontmatter.prompt?.append !== false;
437+
438+
const patterns = pkg.frontmatter.include_files ?? [];
439+
440+
if (!baseId || !shouldAppend) {
441+
return patterns;
442+
}
443+
444+
const basePatterns = await resolve(baseId, depth + 1);
445+
return [...basePatterns, ...patterns];
446+
}
447+
448+
const patterns = await resolve(agentId, 0);
449+
450+
// Preserve order while deduplicating.
451+
const seen = new Set<string>();
452+
return patterns.filter((pattern) => {
453+
if (seen.has(pattern)) return false;
454+
seen.add(pattern);
455+
return true;
456+
});
457+
}

src/node/services/agentDefinitions/parseAgentDefinitionMarkdown.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,58 @@ Body
7373
).toThrow(AgentDefinitionParseError);
7474
});
7575

76+
test("parses include_files patterns", () => {
77+
const content = `---
78+
name: With Files
79+
include_files:
80+
- "docs/**/*.md"
81+
- "src/**/*.ts"
82+
---
83+
Body
84+
`;
85+
86+
const result = parseAgentDefinitionMarkdown({
87+
content,
88+
byteSize: Buffer.byteLength(content, "utf-8"),
89+
});
90+
91+
expect(result.frontmatter.include_files).toEqual(["docs/**/*.md", "src/**/*.ts"]);
92+
});
93+
94+
test("rejects absolute paths in include_files", () => {
95+
const content = `---
96+
name: Bad Paths
97+
include_files:
98+
- "/etc/passwd"
99+
---
100+
Body
101+
`;
102+
103+
expect(() =>
104+
parseAgentDefinitionMarkdown({
105+
content,
106+
byteSize: Buffer.byteLength(content, "utf-8"),
107+
})
108+
).toThrow(AgentDefinitionParseError);
109+
});
110+
111+
test("rejects path traversal in include_files", () => {
112+
const content = `---
113+
name: Traversal
114+
include_files:
115+
- "../secret.txt"
116+
---
117+
Body
118+
`;
119+
120+
expect(() =>
121+
parseAgentDefinitionMarkdown({
122+
content,
123+
byteSize: Buffer.byteLength(content, "utf-8"),
124+
})
125+
).toThrow(AgentDefinitionParseError);
126+
});
127+
76128
test("parses tools as add/remove patterns", () => {
77129
const content = `---
78130
name: Regex Tools

0 commit comments

Comments
 (0)