micode 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/index.js +21096 -0
  2. package/package.json +6 -5
  3. package/src/agents/artifact-searcher.ts +0 -1
  4. package/src/agents/bootstrapper.ts +164 -0
  5. package/src/agents/brainstormer.ts +140 -33
  6. package/src/agents/codebase-analyzer.ts +0 -1
  7. package/src/agents/codebase-locator.ts +0 -1
  8. package/src/agents/commander.ts +99 -10
  9. package/src/agents/executor.ts +133 -76
  10. package/src/agents/implementer.ts +126 -41
  11. package/src/agents/index.ts +29 -19
  12. package/src/agents/ledger-creator.ts +0 -1
  13. package/src/agents/octto.ts +132 -0
  14. package/src/agents/pattern-finder.ts +0 -1
  15. package/src/agents/planner.ts +227 -107
  16. package/src/agents/probe.ts +152 -0
  17. package/src/agents/project-initializer.ts +0 -1
  18. package/src/agents/reviewer.ts +89 -21
  19. package/src/config-loader.test.ts +226 -0
  20. package/src/config-loader.ts +132 -6
  21. package/src/hooks/artifact-auto-index.ts +2 -1
  22. package/src/hooks/auto-compact.ts +14 -21
  23. package/src/hooks/context-injector.ts +6 -13
  24. package/src/hooks/context-window-monitor.ts +8 -13
  25. package/src/hooks/ledger-loader.ts +4 -6
  26. package/src/hooks/token-aware-truncation.ts +11 -17
  27. package/src/index.ts +54 -22
  28. package/src/indexing/milestone-artifact-classifier.ts +26 -0
  29. package/src/indexing/milestone-artifact-ingest.ts +42 -0
  30. package/src/octto/constants.ts +20 -0
  31. package/src/octto/session/browser.ts +32 -0
  32. package/src/octto/session/index.ts +25 -0
  33. package/src/octto/session/server.ts +89 -0
  34. package/src/octto/session/sessions.ts +383 -0
  35. package/src/octto/session/types.ts +305 -0
  36. package/src/octto/session/utils.ts +25 -0
  37. package/src/octto/session/waiter.ts +139 -0
  38. package/src/octto/state/index.ts +5 -0
  39. package/src/octto/state/persistence.ts +65 -0
  40. package/src/octto/state/store.ts +161 -0
  41. package/src/octto/state/types.ts +51 -0
  42. package/src/octto/types.ts +376 -0
  43. package/src/octto/ui/bundle.ts +1650 -0
  44. package/src/octto/ui/index.ts +2 -0
  45. package/src/tools/artifact-index/index.ts +152 -3
  46. package/src/tools/artifact-index/schema.sql +21 -0
  47. package/src/tools/milestone-artifact-search.ts +48 -0
  48. package/src/tools/octto/brainstorm.ts +332 -0
  49. package/src/tools/octto/extractor.ts +95 -0
  50. package/src/tools/octto/factory.ts +89 -0
  51. package/src/tools/octto/formatters.ts +63 -0
  52. package/src/tools/octto/index.ts +27 -0
  53. package/src/tools/octto/processor.ts +165 -0
  54. package/src/tools/octto/questions.ts +508 -0
  55. package/src/tools/octto/responses.ts +135 -0
  56. package/src/tools/octto/session.ts +114 -0
  57. package/src/tools/octto/types.ts +21 -0
  58. package/src/tools/octto/utils.ts +4 -0
  59. package/src/tools/pty/manager.ts +13 -7
  60. package/src/tools/spawn-agent.ts +1 -3
  61. package/src/utils/config.ts +123 -0
  62. package/src/utils/errors.ts +57 -0
  63. package/src/utils/logger.ts +50 -0
@@ -0,0 +1,152 @@
1
+ // src/agents/probe.ts
2
+ import type { AgentConfig } from "@opencode-ai/sdk";
3
+
4
+ export const probeAgent: AgentConfig = {
5
+ description: "Evaluates octto branch Q&A and decides whether to ask more or complete with finding",
6
+ mode: "subagent",
7
+ temperature: 0.5,
8
+ prompt: `<identity>
9
+ You are a SENIOR ENGINEER evaluating design options, not a passive questionnaire.
10
+ - ALWAYS propose what YOU think the answer should be
11
+ - Generate 2-4 concrete options with your recommendation marked
12
+ - Avoid ask_text - if you can predict reasonable options, use pick_one/pick_many
13
+ - State your reasoning: "I'm recommending X because Y"
14
+ </identity>
15
+
16
+ <question-philosophy>
17
+ Every question should ADVANCE the design, not just gather information.
18
+
19
+ **Preferred question types (use these):**
20
+ - pick_one: Present 2-4 options with recommendation. "Which approach? [A (recommended), B, C]"
21
+ - pick_many: Multiple non-exclusive choices with sensible defaults pre-selected
22
+ - confirm: Yes/no with clear statement of what happens on confirm
23
+ - show_options: Complex trade-offs with pros/cons
24
+ - slider: Numeric preferences (priority, confidence, scale)
25
+ - thumbs: Quick approval/rejection of a specific proposal
26
+
27
+ **Discouraged question types (avoid):**
28
+ - ask_text: Only when you genuinely cannot predict options (project name, custom domain)
29
+ - ask_code: Rarely needed - propose code patterns yourself
30
+
31
+ **Why:** Free-text puts cognitive burden on the user. Your job is to do the thinking.
32
+ </question-philosophy>
33
+
34
+ <purpose>
35
+ You evaluate a brainstorming branch's Q&A history and decide:
36
+ 1. Need more information? Return a follow-up question
37
+ 2. Have enough? Return a finding that synthesizes the user's preferences
38
+ </purpose>
39
+
40
+ <context>
41
+ You receive:
42
+ - The original user request
43
+ - All branches with their scopes (to understand the full picture)
44
+ - The Q&A history for the branch you're evaluating
45
+ </context>
46
+
47
+ <output-format>
48
+ Return ONLY a JSON object. No markdown, no explanation.
49
+
50
+ If MORE information needed:
51
+ {
52
+ "done": false,
53
+ "question": {
54
+ "type": "pick_one|pick_many|...",
55
+ "config": { ... }
56
+ }
57
+ }
58
+
59
+ If ENOUGH information gathered:
60
+ {
61
+ "done": true,
62
+ "finding": "Clear summary of what the user wants for this aspect"
63
+ }
64
+ </output-format>
65
+
66
+ <guidance>
67
+ <principle>Stay within the branch's scope - don't ask about other branches' concerns</principle>
68
+ <principle>2-4 questions per branch is usually enough - be concise</principle>
69
+ <principle>Complete when you understand the user's intent for this aspect</principle>
70
+ <principle>Synthesize a finding that captures the decision/preference clearly</principle>
71
+ <principle>ALWAYS include a recommended option - never present naked choices</principle>
72
+ <principle>Form a hypothesis FIRST, then validate it with the user</principle>
73
+ <principle>If user gives vague feedback, interpret it and propose specific options</principle>
74
+ </guidance>
75
+
76
+ <question-types>
77
+ <type name="pick_one">
78
+ Single choice. config: { question, options: [{id, label, description?}], recommended?, context? }
79
+ </type>
80
+
81
+ <type name="pick_many">
82
+ Multiple choice. config: { question, options: [{id, label, description?}], recommended?: string[], min?, max?, context? }
83
+ </type>
84
+
85
+ <type name="confirm">
86
+ Yes/no. config: { question, context?, yesLabel?, noLabel?, allowCancel? }
87
+ </type>
88
+
89
+ <type name="ask_text">
90
+ Free text. config: { question, placeholder?, context?, multiline? }
91
+ </type>
92
+
93
+ <type name="slider">
94
+ Numeric range. config: { question, min, max, step?, defaultValue?, context? }
95
+ </type>
96
+
97
+ <type name="rank">
98
+ Order items. config: { question, options: [{id, label, description?}], context? }
99
+ </type>
100
+
101
+ <type name="rate">
102
+ Rate items (stars). config: { question, options: [{id, label, description?}], min?, max?, context? }
103
+ </type>
104
+
105
+ <type name="thumbs">
106
+ Thumbs up/down. config: { question, context? }
107
+ </type>
108
+
109
+ <type name="show_options">
110
+ Options with pros/cons. config: { question, options: [{id, label, description?, pros?: string[], cons?: string[]}], recommended?, allowFeedback?, context? }
111
+ </type>
112
+
113
+ <type name="show_diff">
114
+ Code diff review. config: { question, before, after, filePath?, language? }
115
+ </type>
116
+
117
+ <type name="ask_code">
118
+ Code input. config: { question, language?, placeholder?, context? }
119
+ </type>
120
+
121
+ <type name="ask_image">
122
+ Image upload. config: { question, multiple?, maxImages?, context? }
123
+ </type>
124
+
125
+ <type name="ask_file">
126
+ File upload. config: { question, multiple?, maxFiles?, accept?: string[], context? }
127
+ </type>
128
+
129
+ <type name="emoji_react">
130
+ Emoji selection. config: { question, emojis?: string[], context? }
131
+ </type>
132
+
133
+ <type name="review_section">
134
+ Section review. config: { question, content, context? }
135
+ </type>
136
+
137
+ <type name="show_plan">
138
+ Plan review. config: { question, sections: [{id, title, content}] }
139
+ </type>
140
+ </question-types>
141
+
142
+ <never-do>
143
+ <forbidden>Never ask questions outside the branch's scope</forbidden>
144
+ <forbidden>Never ask more than needed - if you understand, complete the branch</forbidden>
145
+ <forbidden>Never wrap output in markdown code blocks</forbidden>
146
+ <forbidden>Never include text outside the JSON</forbidden>
147
+ <forbidden>Never repeat questions that were already asked</forbidden>
148
+ <forbidden>Never use ask_text when you can propose options instead</forbidden>
149
+ <forbidden>Never present options without marking one as recommended</forbidden>
150
+ <forbidden>Never ask "what do you want?" - propose what YOU think they want</forbidden>
151
+ </never-do>`,
152
+ };
@@ -218,7 +218,6 @@ Available micode agents: codebase-locator, codebase-analyzer, pattern-finder.
218
218
 
219
219
  export const projectInitializerAgent: AgentConfig = {
220
220
  mode: "subagent",
221
- model: "openai/gpt-5.2-codex",
222
221
  temperature: 0.3,
223
222
  maxTokens: 32000,
224
223
  prompt: PROMPT,
@@ -1,9 +1,8 @@
1
1
  import type { AgentConfig } from "@opencode-ai/sdk";
2
2
 
3
3
  export const reviewerAgent: AgentConfig = {
4
- description: "Reviews implementation for correctness and style",
4
+ description: "Reviews ONE micro-task: verifies file + test match plan, test passes",
5
5
  mode: "subagent",
6
- model: "openai/gpt-5.2-codex",
7
6
  temperature: 0.3,
8
7
  tools: {
9
8
  write: false,
@@ -15,8 +14,18 @@ You are running as part of the "micode" OpenCode plugin (NOT Claude Code).
15
14
  You are a SUBAGENT spawned by the executor to review implementations.
16
15
  </environment>
17
16
 
17
+ <identity>
18
+ You are a SENIOR ENGINEER who helps fix problems, not just reports them.
19
+ - For every issue, suggest a concrete fix
20
+ - Don't just say "this is wrong" - say "this is wrong, fix by doing X"
21
+ - Provide code snippets for non-trivial fixes
22
+ - Make your review actionable, not just informative
23
+ </identity>
24
+
18
25
  <purpose>
19
- Check correctness and style. Be specific. Run code, don't just read.
26
+ Review ONE micro-task (one file + its test).
27
+ Verify: file exists, test exists, test passes, implementation matches plan.
28
+ Quick review - you're one of 10-20 reviewers running in parallel.
20
29
  </purpose>
21
30
 
22
31
  <rules>
@@ -62,14 +71,23 @@ Check correctness and style. Be specific. Run code, don't just read.
62
71
  </checklist>
63
72
 
64
73
  <process>
65
- <step>Read the plan</step>
66
- <step>Read all changed files</step>
67
- <step>Run tests</step>
68
- <step>Compare implementation to plan</step>
69
- <step>Check each item above</step>
70
- <step>Report with precise references</step>
74
+ <step>Parse prompt for: task ID, file path, test path</step>
75
+ <step>Read the implementation file</step>
76
+ <step>Read the test file</step>
77
+ <step>Run the test command</step>
78
+ <step>Verify test passes</step>
79
+ <step>Quick check: no obvious bugs, follows basic patterns</step>
80
+ <step>Report APPROVED or CHANGES REQUESTED</step>
71
81
  </process>
72
82
 
83
+ <micro-task-scope>
84
+ You review ONE file. Keep review focused:
85
+ - Does the file exist and have correct content?
86
+ - Does the test exist and pass?
87
+ - Any obvious bugs or security issues?
88
+ - Don't nitpick style if functionality is correct.
89
+ </micro-task-scope>
90
+
73
91
  <terminal-verification>
74
92
  <rule>If implementation includes PTY usage, verify sessions are properly cleaned up</rule>
75
93
  <rule>If tests require a running server, check that pty_spawn was used appropriately</rule>
@@ -78,22 +96,18 @@ Check correctness and style. Be specific. Run code, don't just read.
78
96
 
79
97
  <output-format>
80
98
  <template>
81
- ## Review: [Component]
99
+ ## Review Task [X.Y]: [file name]
82
100
 
83
101
  **Status**: APPROVED / CHANGES REQUESTED
84
102
 
85
- ### Critical
86
- - \`file:line\` - [issue and why it matters]
87
-
88
- ### Suggestions
89
- - \`file:line\` - [optional improvement]
103
+ **Test**: PASS / FAIL
104
+ - Command: \`bun test path/to/test.ts\`
90
105
 
91
- ### Verification
92
- - [x] Tests run: [pass/fail]
93
- - [x] Plan match: [yes/no]
94
- - [x] Style check: [issues if any]
106
+ **Issues** (if CHANGES REQUESTED):
107
+ 1. \`file:line\` - [issue]
108
+ **Fix:** [specific fix with code]
95
109
 
96
- **Summary**: [One sentence]
110
+ **Summary**: [One sentence - what's good or what needs fixing]
97
111
  </template>
98
112
  </output-format>
99
113
 
@@ -103,5 +117,59 @@ Check correctness and style. Be specific. Run code, don't just read.
103
117
  <priority order="3">Missing functionality</priority>
104
118
  <priority order="4">Test coverage</priority>
105
119
  <priority order="5">Style/readability</priority>
106
- </priority-order>`,
120
+ </priority-order>
121
+
122
+ <fix-suggestions>
123
+ Every issue MUST include a suggested fix:
124
+
125
+ <critical-issue-format>
126
+ Issue: [What's wrong]
127
+ Why it matters: [Impact]
128
+ Fix: [Specific action]
129
+ Code: [If non-trivial, show before/after]
130
+ </critical-issue-format>
131
+
132
+ <examples>
133
+ <example type="security">
134
+ Issue: SQL injection vulnerability at db.ts:45
135
+ Why: User input directly interpolated into query
136
+ Fix: Use parameterized query
137
+ Code:
138
+ \`\`\`typescript
139
+ // Before
140
+ const query = \`SELECT * FROM users WHERE id = \${userId}\`;
141
+
142
+ // After
143
+ const query = 'SELECT * FROM users WHERE id = $1';
144
+ const result = await db.query(query, [userId]);
145
+ \`\`\`
146
+ </example>
147
+
148
+ <example type="correctness">
149
+ Issue: Off-by-one error at utils.ts:23
150
+ Why: Loop excludes last element
151
+ Fix: Change < to <=
152
+ Code: \`for (let i = 0; i <= arr.length - 1; i++)\`
153
+ </example>
154
+ </examples>
155
+
156
+ <rule>Never report an issue without a fix suggestion</rule>
157
+ <rule>For complex fixes, provide code snippets</rule>
158
+ <rule>For simple fixes, one-line description is enough</rule>
159
+ </fix-suggestions>
160
+
161
+ <autonomy-rules>
162
+ <rule>You are a SUBAGENT - complete your review without asking for confirmation</rule>
163
+ <rule>NEVER ask "Does this look right?" or "Should I continue?" - just review</rule>
164
+ <rule>NEVER ask for permission to run tests or checks - just run them</rule>
165
+ <rule>Report APPROVED or CHANGES REQUESTED - don't ask what to do next</rule>
166
+ <rule>Make a decision and state it clearly - executor handles next steps</rule>
167
+ </autonomy-rules>
168
+
169
+ <never-do>
170
+ <forbidden>NEVER ask for confirmation - you're a subagent, just review</forbidden>
171
+ <forbidden>NEVER ask "Does this look right?" or "Should I proceed?"</forbidden>
172
+ <forbidden>NEVER hedge your verdict - state APPROVED or CHANGES REQUESTED clearly</forbidden>
173
+ <forbidden>Don't defer decisions to executor - make the call yourself</forbidden>
174
+ </never-do>`,
107
175
  };
@@ -0,0 +1,226 @@
1
+ // src/config-loader.test.ts
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { type MicodeConfig, type ProviderInfo, validateAgentModels } from "./config-loader";
5
+
6
+ // Helper to create a minimal ProviderInfo for testing
7
+ function createProvider(id: string, modelIds: string[]): ProviderInfo {
8
+ const models: Record<string, unknown> = {};
9
+ for (const modelId of modelIds) {
10
+ models[modelId] = { id: modelId };
11
+ }
12
+ return { id, models };
13
+ }
14
+
15
+ describe("validateAgentModels", () => {
16
+ test("returns config unchanged when all models are valid", () => {
17
+ const userConfig: MicodeConfig = {
18
+ agents: {
19
+ commander: { model: "openai/gpt-4" },
20
+ brainstormer: { model: "anthropic/claude-3" },
21
+ },
22
+ };
23
+
24
+ const providers: ProviderInfo[] = [
25
+ createProvider("openai", ["gpt-4", "gpt-3.5"]),
26
+ createProvider("anthropic", ["claude-3", "claude-2"]),
27
+ ];
28
+
29
+ const result = validateAgentModels(userConfig, providers);
30
+
31
+ expect(result.agents?.commander?.model).toBe("openai/gpt-4");
32
+ expect(result.agents?.brainstormer?.model).toBe("anthropic/claude-3");
33
+ });
34
+
35
+ test("removes model override when provider does not exist", () => {
36
+ const userConfig: MicodeConfig = {
37
+ agents: {
38
+ commander: { model: "nonexistent/gpt-4" },
39
+ },
40
+ };
41
+
42
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
43
+
44
+ const result = validateAgentModels(userConfig, providers);
45
+
46
+ // Model should be removed, falling back to default
47
+ expect(result.agents?.commander?.model).toBeUndefined();
48
+ });
49
+
50
+ test("removes model override when model does not exist in provider", () => {
51
+ const userConfig: MicodeConfig = {
52
+ agents: {
53
+ commander: { model: "openai/nonexistent-model" },
54
+ },
55
+ };
56
+
57
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4", "gpt-3.5"])];
58
+
59
+ const result = validateAgentModels(userConfig, providers);
60
+
61
+ // Model should be removed, falling back to default
62
+ expect(result.agents?.commander?.model).toBeUndefined();
63
+ });
64
+
65
+ test("preserves other properties when model is invalid", () => {
66
+ const userConfig: MicodeConfig = {
67
+ agents: {
68
+ commander: {
69
+ model: "nonexistent/model",
70
+ temperature: 0.7,
71
+ maxTokens: 4000,
72
+ },
73
+ },
74
+ };
75
+
76
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
77
+
78
+ const result = validateAgentModels(userConfig, providers);
79
+
80
+ // Model removed but other properties preserved
81
+ expect(result.agents?.commander?.model).toBeUndefined();
82
+ expect(result.agents?.commander?.temperature).toBe(0.7);
83
+ expect(result.agents?.commander?.maxTokens).toBe(4000);
84
+ });
85
+
86
+ test("handles config with no agents", () => {
87
+ const userConfig: MicodeConfig = {};
88
+
89
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
90
+
91
+ const result = validateAgentModels(userConfig, providers);
92
+
93
+ expect(result).toEqual({});
94
+ });
95
+
96
+ test("handles agent override with no model specified", () => {
97
+ const userConfig: MicodeConfig = {
98
+ agents: {
99
+ commander: { temperature: 0.5 },
100
+ },
101
+ };
102
+
103
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
104
+
105
+ const result = validateAgentModels(userConfig, providers);
106
+
107
+ // No model to validate, config unchanged
108
+ expect(result.agents?.commander?.temperature).toBe(0.5);
109
+ expect(result.agents?.commander?.model).toBeUndefined();
110
+ });
111
+
112
+ test("handles empty providers list", () => {
113
+ const userConfig: MicodeConfig = {
114
+ agents: {
115
+ commander: { model: "openai/gpt-4" },
116
+ },
117
+ };
118
+
119
+ const providers: ProviderInfo[] = [];
120
+
121
+ const result = validateAgentModels(userConfig, providers);
122
+
123
+ // No providers available, config should remain unchanged
124
+ expect(result).toEqual(userConfig);
125
+ });
126
+
127
+ test("handles providers with no models", () => {
128
+ const userConfig: MicodeConfig = {
129
+ agents: {
130
+ commander: { model: "openai/gpt-4" },
131
+ },
132
+ };
133
+
134
+ const providers: ProviderInfo[] = [{ id: "openai", models: {} }];
135
+
136
+ const result = validateAgentModels(userConfig, providers);
137
+
138
+ // No provider models available, config should remain unchanged
139
+ expect(result).toEqual(userConfig);
140
+ });
141
+
142
+ test("validates multiple agents with mixed valid/invalid models", () => {
143
+ const userConfig: MicodeConfig = {
144
+ agents: {
145
+ commander: { model: "openai/gpt-4" }, // valid
146
+ brainstormer: { model: "fake/model" }, // invalid provider
147
+ planner: { model: "openai/fake-model" }, // invalid model
148
+ reviewer: { model: "anthropic/claude-3" }, // valid
149
+ },
150
+ };
151
+
152
+ const providers: ProviderInfo[] = [
153
+ createProvider("openai", ["gpt-4", "gpt-3.5"]),
154
+ createProvider("anthropic", ["claude-3"]),
155
+ ];
156
+
157
+ const result = validateAgentModels(userConfig, providers);
158
+
159
+ expect(result.agents?.commander?.model).toBe("openai/gpt-4");
160
+ expect(result.agents?.brainstormer?.model).toBeUndefined();
161
+ expect(result.agents?.planner?.model).toBeUndefined();
162
+ expect(result.agents?.reviewer?.model).toBe("anthropic/claude-3");
163
+ });
164
+
165
+ test("removes empty string model", () => {
166
+ const userConfig: MicodeConfig = {
167
+ agents: {
168
+ commander: { model: "", temperature: 0.5 },
169
+ },
170
+ };
171
+
172
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
173
+
174
+ const result = validateAgentModels(userConfig, providers);
175
+
176
+ // Empty string model should be removed as invalid
177
+ expect(result.agents?.commander?.model).toBeUndefined();
178
+ expect(result.agents?.commander?.temperature).toBe(0.5);
179
+ });
180
+
181
+ test("removes model string without slash (malformed)", () => {
182
+ const userConfig: MicodeConfig = {
183
+ agents: {
184
+ commander: { model: "gpt-4-no-provider" },
185
+ },
186
+ };
187
+
188
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
189
+
190
+ const result = validateAgentModels(userConfig, providers);
191
+
192
+ // Malformed model (no slash) should be removed
193
+ expect(result.agents?.commander?.model).toBeUndefined();
194
+ });
195
+
196
+ test("handles model with multiple slashes in model ID", () => {
197
+ const userConfig: MicodeConfig = {
198
+ agents: {
199
+ commander: { model: "openai/gpt-4/turbo" },
200
+ },
201
+ };
202
+
203
+ // Model ID is "gpt-4/turbo" (contains slash)
204
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4/turbo"])];
205
+
206
+ const result = validateAgentModels(userConfig, providers);
207
+
208
+ // Should be valid - "gpt-4/turbo" is the full model ID
209
+ expect(result.agents?.commander?.model).toBe("openai/gpt-4/turbo");
210
+ });
211
+
212
+ test("returns consistent shape when all agents have invalid models", () => {
213
+ const userConfig: MicodeConfig = {
214
+ agents: {
215
+ commander: { model: "invalid/model" },
216
+ },
217
+ };
218
+
219
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
220
+
221
+ const result = validateAgentModels(userConfig, providers);
222
+
223
+ // Should return { agents: {} } for consistency, not {}
224
+ expect(result).toEqual({ agents: {} });
225
+ });
226
+ });