Skip to content

Commit d5a3678

Browse files
authored
feat(config): add agent configuration overlays (#343)
* docs: add agent configuration overlay plan * feat(config): load agent overlay config * feat(config): validate agent overlays * feat(config): apply agent overlays * docs: document agent overlay config * test: cover agent overlay integration * fix(config): protect agent permission overlays * docs(plan): mark agent overlay plan complete
1 parent dc0bd8d commit d5a3678

13 files changed

Lines changed: 3046 additions & 49 deletions

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,20 @@ Configuration is loaded from multiple locations and merged (later sources overri
297297
{
298298
"disabled_skills": ["git-worktree"],
299299
"disabled_agents": [],
300+
"categories": {
301+
"review": {
302+
"temperature": 0.1,
303+
"steps": 12
304+
}
305+
},
306+
"agents": {
307+
"security-sentinel": {
308+
"model": "anthropic/claude-sonnet-4-5"
309+
},
310+
"workflow/systematic-implementer": {
311+
"steps": 20
312+
}
313+
},
300314
"bootstrap": {
301315
"enabled": true
302316
}
@@ -307,9 +321,21 @@ Configuration is loaded from multiple locations and merged (later sources overri
307321
|--------|------|---------|-------------|
308322
| `disabled_skills` | `string[]` | `[]` | Skills to exclude from registration |
309323
| `disabled_agents` | `string[]` | `[]` | Agents to exclude from registration |
324+
| `categories` | `object` | `{}` | Overlay bundled agents by category (`design`, `docs`, `document-review`, `research`, `review`, `workflow`) |
325+
| `agents` | `object` | `{}` | Overlay exact bundled agents by unique stem or `<category>/<stem>` key |
310326
| `bootstrap.enabled` | `boolean` | `true` | Inject the `using-systematic` guide into system prompts |
311327
| `bootstrap.file` | `string` || Custom bootstrap file path (overrides default) |
312328

329+
Agent overlays support `model`, `variant`, `temperature`, `top_p`, `permission`, `mode`, `color`, `steps`, `hidden`, exact-agent-only `disable`, and managed `skills`. `skills` uses bundled skill frontmatter names like `ce:review`; it is a shortcut that writes OpenCode `permission.skill` rules, not a native OpenCode agent field. Because `permission` and `skills` control tool access, they are only accepted from user config or `$OPENCODE_CONFIG_DIR/systematic.json`; project config may tune behavior but cannot loosen a user's permission policy.
330+
331+
Systematic separates config-source precedence from overlay precedence. Config files merge in this order: user config, project config, then `$OPENCODE_CONFIG_DIR/systematic.json` if set. Higher-priority `agents.<key>` and `categories.<id>` entries replace lower-priority entries wholesale, while unrelated keys survive. Project overlays are the exception for security fields: same-key project overlays preserve user-level `permission` and `skills` fields instead of erasing them. After the effective config is built, exact `agents` overlays beat category overlays, which beat built-in policy defaults, bundled markdown defaults, and OpenCode inherited defaults.
332+
333+
Bundled agents omit `model` by default so OpenCode model inheritance keeps working. Systematic emits a `model` only when you configure one explicitly; provider-specific zero-config model defaults are intentionally deferred.
334+
335+
Native OpenCode agents with the same emitted key are full replacements. An exact Systematic overlay for that key conflicts, while category overlays skip native replacements and continue applying to other bundled agents. Use one canonical agent key form across config sources (`security-sentinel` or `review/security-sentinel`) because alias collisions fail duplicate-target validation.
336+
337+
Category IDs are V1 public API because broad policy overlays are a core use case; future agent reorganizations must preserve aliases or provide migration warnings. Category overlays also apply to future bundled agents added to that category. V1 does not include an MCP allowlist shortcut.
338+
313339
### Project-Specific Content
314340

315341
Add your own skills and agents alongside bundled ones:

docs/plans/2026-05-09-001-feat-agent-model-configuration-plan.md

Lines changed: 407 additions & 0 deletions
Large diffs are not rendered by default.

docs/src/content/docs/getting-started/configuration.mdx

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ Systematic works out of the box with sensible defaults, but it provides several
99

1010
## Plugin Configuration
1111

12-
You can customize the plugin by creating a configuration file. Systematic looks for configuration in two places:
12+
You can customize the plugin by creating a configuration file. Systematic looks for configuration in three places:
1313

1414
1. **Global:** `~/.config/opencode/systematic.json`
15-
2. **Project-specific:** `.opencode/systematic.json` (takes precedence)
15+
2. **Project-specific:** `.opencode/systematic.json`
16+
3. **Custom config directory:** `$OPENCODE_CONFIG_DIR/systematic.json`
1617

1718
### Disabling Bundled Content
1819

@@ -25,6 +26,57 @@ If you want to hide certain bundled skills or agents, you can list them in the `
2526
}
2627
```
2728

29+
### Bundled Agent Overlays
30+
31+
Use top-level `agents` and `categories` maps to tune bundled Systematic agents without copying their markdown files into native OpenCode agent definitions.
32+
33+
```jsonc
34+
{
35+
"categories": {
36+
// Applies to bundled agents in agents/review/*.md unless that agent has
37+
// been replaced by a native OpenCode agent with the same emitted key.
38+
"review": {
39+
"temperature": 0.1,
40+
"steps": 12
41+
}
42+
},
43+
"agents": {
44+
// Unqualified unique stem form.
45+
"security-sentinel": {
46+
"model": "anthropic/claude-sonnet-4-5",
47+
"variant": "thinking",
48+
"steps": 20
49+
},
50+
51+
// Qualified form for the same inventory model: <category>/<agent-stem>.
52+
"workflow/systematic-implementer": {
53+
"mode": "subagent",
54+
"color": "cyan"
55+
}
56+
}
57+
}
58+
```
59+
60+
Valid category IDs are `design`, `docs`, `document-review`, `research`, `review`, and `workflow`. These IDs are V1 public API because broad policy overlays are a primary use case; future reorganizations must preserve aliases or ship migration warnings. Category overlays also apply to future bundled agents added to that category, so use them for policies you want to inherit broadly.
61+
62+
Exact agent overlays accept the unique file stem (`security-sentinel`) or the qualified key (`review/security-sentinel`). Pick one canonical form across all config sources. Systematic merges config sources before resolving aliases, so cross-source alias collisions like user `agents.security-sentinel` plus project `agents.review/security-sentinel` fail duplicate-target validation instead of silently overriding.
63+
64+
Supported overlay fields are `model`, `variant`, `temperature`, `top_p`, `permission`, `mode`, `color`, `steps`, `hidden`, exact-agent-only `disable`, and `skills`.
65+
66+
`skills` is a Systematic-managed shortcut that writes OpenCode `permission.skill` rules using bundled skill frontmatter names such as `ce:review`. It is not a native OpenCode agent field. Because `permission` and `skills` control tool access, they are accepted only from user config or `$OPENCODE_CONFIG_DIR/systematic.json`; project config may tune agent behavior but cannot loosen a user's permission policy. Systematic does not expose an `mcps` shortcut in V1; MCP-specific permission mapping is out of scope for this config surface.
67+
68+
Bundled agent markdown intentionally omits `model`. By default, emitted bundled agents also omit `model` so OpenCode inheritance keeps working. Systematic only emits a model when you configure `agents.<key>.model`; provider-specific zero-config model defaults are intentionally deferred.
69+
70+
Overlay application precedence for a Systematic-owned bundled agent is strongest first:
71+
72+
1. Exact `agents.<key>` overlay
73+
2. `categories.<category-id>` overlay
74+
3. Built-in Systematic policy defaults, such as temperature defaults
75+
4. Bundled markdown/frontmatter defaults
76+
5. OpenCode inherited defaults
77+
78+
Native OpenCode agents with the same emitted key are full replacements. If `config.agent.security-sentinel` already exists in OpenCode config, `agents.security-sentinel` or `agents.review/security-sentinel` conflicts because ownership would be ambiguous. Category overlays skip native replacements and continue applying to the remaining Systematic-owned bundled agents in that category.
79+
2880
## Project-Specific Content
2981

3082
One of Systematic's most powerful features is the ability to add your own skills and agents that live alongside (or override) the bundled ones.
@@ -50,11 +102,14 @@ If you create a project-specific asset with the same name as a bundled one, your
50102

51103
Systematic resolves configuration and content in the following order (highest priority first):
52104

53-
1. **Project-level assets:** Files in `.opencode/`
54-
2. **Project-level config:** `.opencode/systematic.json`
55-
3. **User-level assets:** Files in `~/.config/opencode/`
56-
4. **User-level config:** `~/.config/opencode/systematic.json`
57-
5. **Bundled assets:** Content shipped with the plugin package
105+
1. **Custom config directory:** `$OPENCODE_CONFIG_DIR/systematic.json` and assets under `$OPENCODE_CONFIG_DIR/systematic/`
106+
2. **Project-level assets:** Files in `.opencode/`
107+
3. **Project-level config:** `.opencode/systematic.json`
108+
4. **User-level assets:** Files in `~/.config/opencode/`
109+
5. **User-level config:** `~/.config/opencode/systematic.json`
110+
6. **Bundled assets:** Content shipped with the plugin package
111+
112+
Config-source merge precedence is separate from overlay application precedence. Across Systematic config files, `disabled_*` arrays are union-merged, `bootstrap` is shallow-merged, and top-level `agents`/`categories` maps merge by key. A higher-priority source replaces the whole object for the same `agents.<key>` or `categories.<id>` entry; unrelated keys survive. Project overlays are the exception for security fields: they cannot define `permission` or `skills`, and same-key project overlays preserve user-level `permission`/`skills` fields instead of erasing them. After that effective config is built, Systematic applies category and exact overlays to bundled agents using the overlay precedence above.
58113

59114
## Advanced Configuration
60115

@@ -64,8 +119,19 @@ The Systematic configuration file is parsed as JSONC, meaning you can include co
64119
{
65120
// Disable git-worktree as we use a different branching model
66121
"disabled_skills": ["git-worktree"],
67-
68-
// Custom agents are added in .opencode/agents/
69-
"disabled_agents": []
122+
123+
// Custom agents are added in .opencode/agents/; bundled agents can also be
124+
// tuned with overlays instead of copied.
125+
"disabled_agents": [],
126+
"categories": {
127+
"research": {
128+
"temperature": 0.2
129+
}
130+
},
131+
"agents": {
132+
"repo-research-analyst": {
133+
"model": "anthropic/claude-sonnet-4-5"
134+
}
135+
}
70136
}
71137
```

scripts/content-integrity.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ export interface AgentModelViolation {
155155
message: string
156156
}
157157

158+
export interface AgentStemViolation {
159+
stem: string
160+
files: string[]
161+
message: string
162+
}
163+
158164
export interface CheckResult {
159165
rootDir: string
160166
categories: string[]
@@ -164,6 +170,7 @@ export interface CheckResult {
164170
bannedPatterns: BannedPatternHit[]
165171
frontmatterViolations: FrontmatterViolation[]
166172
agentModelViolations: AgentModelViolation[]
173+
agentStemViolations: AgentStemViolation[]
167174
exemptHits: ExemptHit[]
168175
scanStats: {
169176
markdownFiles: number
@@ -722,6 +729,34 @@ export function checkAgentModel(
722729
return violations
723730
}
724731

732+
export function checkAgentStemUniqueness(
733+
rootDir: string,
734+
markdownFiles: readonly string[],
735+
): AgentStemViolation[] {
736+
const filesByStem = new Map<string, string[]>()
737+
738+
for (const relPath of markdownFiles) {
739+
if (!isAgentFile(relPath)) continue
740+
if (!fs.existsSync(path.join(rootDir, relPath))) continue
741+
742+
const stem = path.basename(relPath, '.md')
743+
const files = filesByStem.get(stem) ?? []
744+
files.push(relPath)
745+
filesByStem.set(stem, files)
746+
}
747+
748+
return [...filesByStem.entries()]
749+
.filter(([, files]) => files.length > 1)
750+
.map(([stem, files]) => ({
751+
stem,
752+
files: [...files].sort(),
753+
message:
754+
`Duplicate bundled agent stem \`${stem}\` found in ${files.length} files. ` +
755+
'V1 emits stem-only OpenCode agent keys, so bundled agent filenames must be globally unique across categories.',
756+
}))
757+
.sort((a, b) => a.stem.localeCompare(b.stem))
758+
}
759+
725760
function isAgentFile(relPath: string): boolean {
726761
const parts = relPath.split('/')
727762
return (
@@ -836,6 +871,10 @@ export function checkContentIntegrity(rootDir: string): CheckResult {
836871
const brokenSubfileRefs = checkSubfileReferences(rootDir, targets.markdown)
837872
const frontmatterViolations = checkFrontmatter(rootDir, targets.markdown)
838873
const agentModelViolations = checkAgentModel(rootDir, targets.markdown)
874+
const agentStemViolations = checkAgentStemUniqueness(
875+
rootDir,
876+
targets.markdown,
877+
)
839878
const { hits: bannedPatterns, exempt: exemptHits } = checkBannedPatterns(
840879
rootDir,
841880
allScannedFiles,
@@ -851,6 +890,7 @@ export function checkContentIntegrity(rootDir: string): CheckResult {
851890
bannedPatterns,
852891
frontmatterViolations,
853892
agentModelViolations,
893+
agentStemViolations,
854894
exemptHits,
855895
scanStats: {
856896
markdownFiles: targets.markdown.length,
@@ -890,6 +930,7 @@ function printResult(result: CheckResult, verbose: boolean): void {
890930
printBannedPatterns(result.bannedPatterns)
891931
printFrontmatterViolations(result.frontmatterViolations)
892932
printAgentModelViolations(result.agentModelViolations)
933+
printAgentStemViolations(result.agentStemViolations)
893934

894935
if (totalViolations(result) === 0) {
895936
process.stdout.write(
@@ -906,6 +947,7 @@ function printResult(result: CheckResult, verbose: boolean): void {
906947
`scanStats: ${result.scanStats.markdownFiles} md + ${result.scanStats.typescriptFiles} ts\n` +
907948
`frontmatterViolations: ${result.frontmatterViolations.length}\n` +
908949
`agentModelViolations: ${result.agentModelViolations.length}\n` +
950+
`agentStemViolations: ${result.agentStemViolations.length}\n` +
909951
`exemptHits: ${result.exemptHits.length}\n`,
910952
)
911953
}
@@ -965,13 +1007,29 @@ function printAgentModelViolations(
9651007
}
9661008
}
9671009

1010+
function printAgentStemViolations(
1011+
violations: readonly AgentStemViolation[],
1012+
): void {
1013+
if (violations.length === 0) return
1014+
process.stderr.write(
1015+
`\nDuplicate bundled agent stems (${violations.length}):\n`,
1016+
)
1017+
for (const v of violations) {
1018+
process.stderr.write(` ${v.stem} ${v.message}\n`)
1019+
for (const file of v.files) {
1020+
process.stderr.write(` ${file}\n`)
1021+
}
1022+
}
1023+
}
1024+
9681025
function totalViolations(result: CheckResult): number {
9691026
return (
9701027
result.phantomRefs.length +
9711028
result.brokenSubfileRefs.length +
9721029
result.bannedPatterns.length +
9731030
result.frontmatterViolations.length +
974-
result.agentModelViolations.length
1031+
result.agentModelViolations.length +
1032+
result.agentStemViolations.length
9751033
)
9761034
}
9771035

0 commit comments

Comments
 (0)