From 039c9cd0c891e292267825a67a0b6455f91734f1 Mon Sep 17 00:00:00 2001 From: Vlad Temian Date: Fri, 16 Jan 2026 14:19:35 +0200 Subject: [PATCH 01/22] fix(context-injector): use camelCase filePath property name The context-injector hook was using snake_case 'file_path' but OpenCode tools use camelCase 'filePath', causing directory context injection to silently fail for all file read/edit operations. - Change input.args?.file_path to input.args?.filePath - Add tests for context injection behavior --- src/hooks/context-injector.ts | 2 +- tests/hooks/context-injector.test.ts | 76 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/hooks/context-injector.test.ts diff --git a/src/hooks/context-injector.ts b/src/hooks/context-injector.ts index 30048b8..3e5102f 100644 --- a/src/hooks/context-injector.ts +++ b/src/hooks/context-injector.ts @@ -143,7 +143,7 @@ export function createContextInjectorHook(ctx: PluginInput) { ) => { if (!FILE_ACCESS_TOOLS.includes(input.tool)) return; - const filePath = input.args?.file_path as string | undefined; + const filePath = input.args?.filePath as string | undefined; if (!filePath) return; try { diff --git a/tests/hooks/context-injector.test.ts b/tests/hooks/context-injector.test.ts new file mode 100644 index 0000000..6e4f737 --- /dev/null +++ b/tests/hooks/context-injector.test.ts @@ -0,0 +1,76 @@ +// tests/hooks/context-injector.test.ts +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// Mock PluginInput +function createMockCtx(directory: string) { + return { + directory, + client: { + session: {}, + tui: {}, + }, + }; +} + +describe("context-injector", () => { + let testDir: string; + + beforeEach(() => { + testDir = mkdtempSync(join(tmpdir(), "context-injector-test-")); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + describe("tool.execute.after hook", () => { + it("should extract filePath from tool args using camelCase", async () => { + // Create a README.md in a subdirectory + const subDir = join(testDir, "src", "components"); + mkdirSync(subDir, { recursive: true }); + writeFileSync(join(subDir, "README.md"), "# Components\n\nComponent documentation."); + + // Create a file to "read" + const targetFile = join(subDir, "Button.tsx"); + writeFileSync(targetFile, "export const Button = () => '; return html; } + function renderReviewSection(q) { let html = ''; // Render markdown content @@ -1101,6 +1105,7 @@ export function getHtmlBundle(): string { const options = q.config.options || []; const min = q.config.min || 1; const max = q.config.max || 5; + const labels = q.config.labels || {}; let html = '
'; for (const opt of options) { html += '
'; @@ -1109,7 +1114,14 @@ export function getHtmlBundle(): string { for (let i = min; i <= max; i++) { html += ''; } - html += '
'; + + html += ''; + + + if (labels.min || labels.max) { + html += '
' + escapeHtml(labels.min || String(min)) + ' / ' + escapeHtml(labels.max || String(max)) + '
'; + } + html += ''; } html += ''; html += '
'; @@ -1128,14 +1140,16 @@ export function getHtmlBundle(): string { function renderAskImage(q) { let html = ''; const multiple = q.config.multiple ? 'multiple' : ''; + const accept = q.config.accept ? q.config.accept.join(',') : 'image/*'; html += '
'; - html += ''; + html += ''; html += '
'; html += '
'; - html += '
'; + html += '
'; return html; } + function renderAskFile(q) { let html = ''; const multiple = q.config.multiple ? 'multiple' : ''; @@ -1299,11 +1313,36 @@ export function getHtmlBundle(): string { } } + function isAllowedFileType(file, allowed) { + if (!allowed || allowed.length === 0) return true; + const fileType = file.type || ''; + const fileName = file.name || ''; + return allowed.some(entry => { + if (!entry) return false; + if (entry.endsWith('/*')) { + const prefix = entry.slice(0, -1); + return fileType.startsWith(prefix); + } + if (entry.startsWith('.')) { + return fileName.toLowerCase().endsWith(entry.toLowerCase()); + } + return fileType === entry || fileName.toLowerCase().endsWith(entry.toLowerCase()); + }); + } + function previewImages(questionId) { const input = document.getElementById('image_' + questionId); const preview = document.getElementById('preview_' + questionId); preview.innerHTML = ''; + const q = questions.find(q => q.id === questionId); + const allowed = q && q.config.accept ? q.config.accept : null; for (const file of input.files) { + if (allowed && allowed.length > 0 && !isAllowedFileType(file, allowed)) { + const warning = document.createElement('div'); + warning.textContent = 'Warning: ' + file.name + ' does not match allowed types.'; + warning.style.cssText = 'color: var(--accent-error); font-size: 0.75rem; margin: 0.25rem 0;'; + preview.appendChild(warning); + } const img = document.createElement('img'); img.src = URL.createObjectURL(file); img.style.maxWidth = '100px'; @@ -1481,10 +1520,13 @@ export function getHtmlBundle(): string { } function renderAnsweredSlider(q, answer) { + const labels = q.config.labels || {}; + const minLabel = labels.min || String(q.config.min); + const maxLabel = labels.max || String(q.config.max); let html = '
'; html += '
Value
'; html += '' + answer.value + ''; - html += ' (range: ' + q.config.min + ' - ' + q.config.max + ')'; + html += ' (range: ' + escapeHtml(minLabel) + ' - ' + escapeHtml(maxLabel) + ')'; html += '
'; return html; } @@ -1545,6 +1587,9 @@ export function getHtmlBundle(): string { function renderAnsweredRate(q, answer) { const ratings = answer.ratings || {}; + const labels = q.config.labels || {}; + const minLabel = labels.min || String(q.config.min || 1); + const maxLabel = labels.max || String(q.config.max || 5); let html = '
Ratings
'; html += '
'; for (const opt of (q.config.options || [])) { @@ -1555,6 +1600,12 @@ export function getHtmlBundle(): string { html += '
'; } html += ''; + if (labels.min || labels.max) { + html += '
'; + html += '
Scale
'; + html += '
' + escapeHtml(minLabel) + ' → ' + escapeHtml(maxLabel) + '
'; + html += '
'; + } return html; } diff --git a/src/tools/octto/factory.ts b/src/tools/octto/factory.ts index 066ecc4..0381f4b 100644 --- a/src/tools/octto/factory.ts +++ b/src/tools/octto/factory.ts @@ -3,7 +3,6 @@ import { tool } from "@opencode-ai/plugin/tool"; import type { BaseConfig, QuestionType, SessionStore } from "../../octto/session"; - import type { OcttoTool, OcttoTools } from "./types"; type ArgsSchema = Parameters[0]["args"]; @@ -53,12 +52,18 @@ The question will appear in the browser for the user to answer.`, "pick_many", "confirm", "ask_text", + "ask_image", + "ask_file", + "ask_code", + "show_diff", + "show_plan", "show_options", "review_section", "thumbs", "slider", "rank", "rate", + "emoji_react", ]) .describe("Question type"), config: tool.schema diff --git a/src/tools/octto/questions.ts b/src/tools/octto/questions.ts index 7df9422..0711fe8 100644 --- a/src/tools/octto/questions.ts +++ b/src/tools/octto/questions.ts @@ -3,7 +3,6 @@ import { tool } from "@opencode-ai/plugin/tool"; import type { SessionStore } from "../../octto/session"; import type { ConfirmConfig, PickManyConfig, PickOneConfig, RankConfig, RateConfig } from "../../octto/types"; - import { createQuestionToolFactory } from "./factory"; import type { OcttoTools } from "./types"; @@ -120,6 +119,13 @@ Response format: { ratings: Record } where key is option id, val min: tool.schema.number().optional().describe("Minimum rating value (default: 1)"), max: tool.schema.number().optional().describe("Maximum rating value (default: 5)"), step: tool.schema.number().optional().describe("Rating step (default: 1)"), + labels: tool.schema + .object({ + min: tool.schema.string().optional().describe("Label for minimum value"), + max: tool.schema.string().optional().describe("Label for maximum value"), + }) + .optional() + .describe("Optional labels for min/max"), }, validate: (args) => { if (!args.options || args.options.length === 0) return "options array must not be empty"; @@ -134,6 +140,7 @@ Response format: { ratings: Record } where key is option id, val min: args.min ?? 1, max: args.max ?? 5, step: args.step, + labels: args.labels, }), }); @@ -196,6 +203,7 @@ Response format: { text: string }`, context?: string; multiple?: boolean; maxImages?: number; + accept?: string[]; } const ask_image = createTool({ @@ -206,12 +214,14 @@ Response format: { text: string }`, context: tool.schema.string().optional().describe("Instructions/context"), multiple: tool.schema.boolean().optional().describe("Allow multiple images"), maxImages: tool.schema.number().optional().describe("Maximum number of images"), + accept: tool.schema.array(tool.schema.string()).optional().describe("Allowed image types"), }, toConfig: (args) => ({ question: args.question, context: args.context, multiple: args.multiple, maxImages: args.maxImages, + accept: args.accept, }), }); @@ -332,7 +342,7 @@ Response format: { approved: boolean, annotations?: Record }` }, toConfig: (args) => ({ question: args.question, - sections: args.sections || [], + sections: args.sections, markdown: args.markdown, }), }); @@ -456,6 +466,7 @@ Response format: { choice: "up" | "down" }`, step?: number; defaultValue?: number; context?: string; + labels?: { min?: string; max?: string; mid?: string }; } const slider = createTool({ @@ -469,6 +480,14 @@ Response format: { value: number }`, step: tool.schema.number().optional().describe("Step size (default: 1)"), defaultValue: tool.schema.number().optional().describe("Default value"), context: tool.schema.string().optional().describe("Instructions/context"), + labels: tool.schema + .object({ + min: tool.schema.string().optional().describe("Label for minimum value"), + max: tool.schema.string().optional().describe("Label for maximum value"), + mid: tool.schema.string().optional().describe("Label for middle value"), + }) + .optional() + .describe("Optional labels for the slider"), }, validate: (args) => { if (args.min >= args.max) return `min (${args.min}) must be less than max (${args.max})`; @@ -481,6 +500,7 @@ Response format: { value: number }`, step: args.step, defaultValue: args.defaultValue, context: args.context, + labels: args.labels, }), }); diff --git a/src/tools/octto/session.ts b/src/tools/octto/session.ts index f70c5fd..c34a517 100644 --- a/src/tools/octto/session.ts +++ b/src/tools/octto/session.ts @@ -2,10 +2,9 @@ import { tool } from "@opencode-ai/plugin/tool"; import type { SessionStore } from "../../octto/session"; +import type { OcttoSessionTracker, OcttoTools } from "./types"; -import type { OcttoTools } from "./types"; - -export function createSessionTools(sessions: SessionStore): OcttoTools { +export function createSessionTools(sessions: SessionStore, tracker?: OcttoSessionTracker): OcttoTools { const start_session = tool({ description: `Start an interactive octto session with initial questions. Opens a browser window with questions already displayed - no waiting. @@ -21,12 +20,21 @@ REQUIRED: You MUST provide at least 1 question. Will fail without questions.`, "pick_many", "confirm", "ask_text", + "ask_image", + "ask_file", + "ask_code", + "show_diff", + "show_plan", "show_options", "review_section", "thumbs", "slider", + "rank", + "rate", + "emoji_react", ]) .describe("Question type"), + config: tool.schema .looseObject({ question: tool.schema.string().optional(), @@ -37,7 +45,7 @@ REQUIRED: You MUST provide at least 1 question. Will fail without questions.`, ) .describe("REQUIRED: Initial questions to display when browser opens. Must have at least 1."), }, - execute: async (args) => { + execute: async (args, context) => { // ENFORCE: questions are required if (!args.questions || args.questions.length === 0) { return `## ERROR: questions parameter is REQUIRED @@ -60,6 +68,7 @@ Please call start_session again WITH your prepared questions.`; try { const result = await sessions.startSession({ title: args.title, questions: args.questions }); + tracker?.onCreated?.(context.sessionID, result.session_id); let output = `## Session Started @@ -91,9 +100,10 @@ Closes the browser window and cleans up resources.`, args: { session_id: tool.schema.string().describe("Session ID to end"), }, - execute: async (args) => { + execute: async (args, context) => { const result = await sessions.endSession(args.session_id); if (result.ok) { + tracker?.onEnded?.(context.sessionID, args.session_id); return `Session ${args.session_id} ended successfully.`; } return `Failed to end session ${args.session_id}. It may not exist.`; From f1718e0e50052927dd414b4c0c1d7e3a07e70169 Mon Sep 17 00:00:00 2001 From: Vlad Temian Date: Sun, 18 Jan 2026 00:06:40 +0200 Subject: [PATCH 18/22] fix(octto): address review feedback --- package.json | 1 + src/config-loader.test.ts | 23 +++- src/config-loader.ts | 5 + src/index.ts | 17 ++- src/octto/constants.ts | 2 +- src/octto/session/server.ts | 3 +- src/octto/session/sessions.ts | 6 +- src/octto/session/waiter.ts | 15 ++- src/octto/state/store.ts | 6 +- src/tools/octto/brainstorm.ts | 25 +++-- src/tools/octto/formatters.ts | 8 +- src/tools/octto/index.ts | 15 ++- src/tools/octto/types.ts | 5 + src/utils/config.ts | 6 + .../flows/milestone-error-paths.test.ts | 50 +++++---- tests/indexing/flows/milestone-ingest.test.ts | 42 +++---- .../indexing/search/milestone-search.test.ts | 70 ++++++------ tests/tools/artifact-index.test.ts | 106 ++++++++++-------- 18 files changed, 246 insertions(+), 159 deletions(-) diff --git a/package.json b/package.json index 64904b3..60b0d1c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "type": "module", "files": [ "src", + "dist", "INSTALL_CLAUDE.md" ], "scripts": { diff --git a/src/config-loader.test.ts b/src/config-loader.test.ts index 7734bbc..7bf8cf9 100644 --- a/src/config-loader.test.ts +++ b/src/config-loader.test.ts @@ -1,7 +1,9 @@ // src/config-loader.test.ts import { describe, expect, test } from "bun:test"; + import type { AgentConfig } from "@opencode-ai/sdk"; -import { validateAgentModels, type MicodeConfig, type ProviderInfo } from "./config-loader"; + +import { type MicodeConfig, type ProviderInfo, validateAgentModels } from "./config-loader"; // Helper to create a minimal ProviderInfo for testing function createProvider(id: string, modelIds: string[]): ProviderInfo { @@ -157,8 +159,23 @@ describe("validateAgentModels", () => { const result = validateAgentModels(userConfig, providers); - // No providers available, model should be removed - expect(result.agents?.commander?.model).toBeUndefined(); + // No providers available, config should remain unchanged + expect(result).toEqual(userConfig); + }); + + test("handles providers with no models", () => { + const userConfig: MicodeConfig = { + agents: { + commander: { model: "openai/gpt-4" }, + }, + }; + + const providers: ProviderInfo[] = [{ id: "openai", models: {} }]; + + const result = validateAgentModels(userConfig, providers); + + // No provider models available, config should remain unchanged + expect(result).toEqual(userConfig); }); test("validates multiple agents with mixed valid/invalid models", () => { diff --git a/src/config-loader.ts b/src/config-loader.ts index 8379ad4..504398b 100644 --- a/src/config-loader.ts +++ b/src/config-loader.ts @@ -161,6 +161,11 @@ export function validateAgentModels(userConfig: MicodeConfig, providers: Provide return userConfig; } + const hasAnyModels = providers.some((provider) => Object.keys(provider.models).length > 0); + if (!hasAnyModels) { + return userConfig; + } + // Build lookup map for providers and their models const providerMap = new Map>(); for (const provider of providers) { diff --git a/src/index.ts b/src/index.ts index 9918bdb..08afa5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,11 +99,26 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => { // Octto (browser-based brainstorming) tools const octtoSessionStore = createSessionStore(); - const octtoTools = createOcttoTools(octtoSessionStore, ctx.client); // Track octto sessions per opencode session for cleanup const octtoSessionsMap = new Map>(); + const octtoTools = createOcttoTools(octtoSessionStore, ctx.client, { + onCreated: (parentSessionId, octtoSessionId) => { + const sessions = octtoSessionsMap.get(parentSessionId) ?? new Set(); + sessions.add(octtoSessionId); + octtoSessionsMap.set(parentSessionId, sessions); + }, + onEnded: (parentSessionId, octtoSessionId) => { + const sessions = octtoSessionsMap.get(parentSessionId); + if (!sessions) return; + sessions.delete(octtoSessionId); + if (sessions.size === 0) { + octtoSessionsMap.delete(parentSessionId); + } + }, + }); + return { // Tools tool: { diff --git a/src/octto/constants.ts b/src/octto/constants.ts index e016b67..6f35a29 100644 --- a/src/octto/constants.ts +++ b/src/octto/constants.ts @@ -8,7 +8,7 @@ import { config } from "../utils/config"; export const DEFAULT_ANSWER_TIMEOUT_MS = config.octto.answerTimeoutMs; /** Default maximum number of follow-up questions per branch */ -export const DEFAULT_MAX_QUESTIONS = 15; +export const DEFAULT_MAX_QUESTIONS = config.octto.maxQuestions; /** Default timeout for brainstorm review (10 minutes) */ export const DEFAULT_REVIEW_TIMEOUT_MS = config.octto.reviewTimeoutMs; diff --git a/src/octto/session/server.ts b/src/octto/session/server.ts index ecde39e..cee99f9 100644 --- a/src/octto/session/server.ts +++ b/src/octto/session/server.ts @@ -1,8 +1,8 @@ // src/octto/session/server.ts import type { Server, ServerWebSocket } from "bun"; +import { config } from "../../utils/config"; import { getHtmlBundle } from "../ui"; - import type { SessionStore } from "./sessions"; import type { WsClientMessage } from "./types"; @@ -18,6 +18,7 @@ export async function createServer( const server = Bun.serve({ port: 0, // Random available port + hostname: config.octto.allowRemoteBind ? config.octto.bindAddress : "127.0.0.1", fetch(req, server) { const url = new URL(req.url); diff --git a/src/octto/session/sessions.ts b/src/octto/session/sessions.ts index fac6f69..f93ddc4 100644 --- a/src/octto/session/sessions.ts +++ b/src/octto/session/sessions.ts @@ -2,7 +2,6 @@ import type { ServerWebSocket } from "bun"; import { DEFAULT_ANSWER_TIMEOUT_MS } from "../constants"; -import { generateQuestionId, generateSessionId } from "./utils"; import { openBrowser } from "./browser"; import { createServer } from "./server"; import { @@ -25,6 +24,7 @@ import { type WsClientMessage, type WsServerMessage, } from "./types"; +import { generateQuestionId, generateSessionId } from "./utils"; import { createWaiters } from "./waiter"; export interface SessionStoreOptions { @@ -57,7 +57,8 @@ export function createSessionStore(options: SessionStoreOptions = {}): SessionSt async startSession(input: StartSessionInput): Promise { const sessionId = generateSessionId(); const { server, port } = await createServer(sessionId, store); - const url = `http://localhost:${port}`; + const urlHost = server.hostname ?? "localhost"; + const url = `http://${urlHost}:${port}`; const session: Session = { id: sessionId, @@ -205,7 +206,6 @@ export function createSessionStore(options: SessionStoreOptions = {}): SessionSt timeoutId = setTimeout(() => { cleanup(); - question.status = STATUSES.TIMEOUT; resolve({ completed: false, status: STATUSES.TIMEOUT, reason: STATUSES.TIMEOUT }); }, timeout); }); diff --git a/src/octto/session/waiter.ts b/src/octto/session/waiter.ts index f77d57e..108ef02 100644 --- a/src/octto/session/waiter.ts +++ b/src/octto/session/waiter.ts @@ -74,11 +74,18 @@ export function createWaiters(): Waiters { const callbacks = waiters.get(key); if (!callbacks) return; - for (const callback of callbacks) { - callback(data); + try { + for (const callback of callbacks) { + try { + callback(data); + } catch (error) { + console.error("Waiter notifyAll failed", error); + break; + } + } + } finally { + waiters.delete(key); } - - waiters.delete(key); }, /** diff --git a/src/octto/state/store.ts b/src/octto/state/store.ts index 68ad9ea..b129d54 100644 --- a/src/octto/state/store.ts +++ b/src/octto/state/store.ts @@ -1,7 +1,7 @@ // src/octto/state/store.ts -import type { Answer } from "../session"; import { STATE_DIR } from "../constants"; +import type { Answer } from "../session"; import { createStatePersistence } from "./persistence"; import { BRANCH_STATUSES, @@ -153,7 +153,9 @@ export function createStateStore(baseDir = STATE_DIR): StateStore { }, async deleteSession(sessionId: string): Promise { - await persistence.delete(sessionId); + await withSessionLock(sessionId, async () => { + await persistence.delete(sessionId); + }); }, }; } diff --git a/src/tools/octto/brainstorm.ts b/src/tools/octto/brainstorm.ts index f1e3d53..fbdd426 100644 --- a/src/tools/octto/brainstorm.ts +++ b/src/tools/octto/brainstorm.ts @@ -6,10 +6,9 @@ import { QUESTION_TYPES, QUESTIONS, STATUSES } from "../../octto/session"; import { BRANCH_STATUSES, type BrainstormState, createStateStore, type StateStore } from "../../octto/state"; import { config } from "../../utils/config"; import { log } from "../../utils/logger"; - import { formatBranchStatus, formatFindings, formatFindingsList, formatQASummary } from "./formatters"; import { processAnswer } from "./processor"; -import type { OcttoTools, OpencodeClient } from "./types"; +import type { OcttoSessionTracker, OcttoTools, OpencodeClient } from "./types"; import { generateSessionId } from "./utils"; // --- Extracted helper functions --- @@ -141,14 +140,14 @@ function formatSkippedReviewResult(state: BrainstormState): string { ${state.branch_order.length} Browser session ended before review ${formatFindings(state)} - Write the design document to docs/plans/ + Write the design document to thoughts/shared/designs/ `; } function formatCompletionResult(state: BrainstormState, approved: boolean, feedback: string): string { const feedbackXml = feedback ? `\n ${feedback}` : ""; const nextAction = approved - ? "Write the design document to docs/plans/" + ? "Write the design document to thoughts/shared/designs/" : "Review feedback and discuss with user before proceeding"; return ` ${state.request} @@ -160,7 +159,11 @@ function formatCompletionResult(state: BrainstormState, approved: boolean, feedb // --- Tool definitions --- -export function createBrainstormTools(sessions: SessionStore, client: OpencodeClient): OcttoTools { +export function createBrainstormTools( + sessions: SessionStore, + client: OpencodeClient, + tracker?: OcttoSessionTracker, +): OcttoTools { const store = createStateStore(); const create_brainstorm = tool({ @@ -183,7 +186,7 @@ export function createBrainstormTools(sessions: SessionStore, client: OpencodeCl ) .describe("Branches to explore"), }, - execute: async (args) => { + execute: async (args, context) => { const sessionId = generateSessionId(); await store.createSession( @@ -206,6 +209,7 @@ export function createBrainstormTools(sessions: SessionStore, client: OpencodeCl questions: initialQuestions, }); + tracker?.onCreated?.(context.sessionID, browserSession.session_id); await store.setBrowserSessionId(sessionId, browserSession.session_id); for (const [i, branch] of args.branches.entries()) { @@ -261,12 +265,15 @@ ${branches} args: { session_id: tool.schema.string().describe("Brainstorm session ID"), }, - execute: async (args) => { + execute: async (args, context) => { const state = await store.getSession(args.session_id); if (!state) return `Session not found: ${args.session_id}`; if (state.browser_session_id) { - await sessions.endSession(state.browser_session_id); + const result = await sessions.endSession(state.browser_session_id); + if (result.ok) { + tracker?.onEnded?.(context.sessionID, state.browser_session_id); + } } const findings = formatFindingsList(state); @@ -275,7 +282,7 @@ ${branches} return ` ${state.request} ${findings} - Write the design document based on these findings + Write the design document based on these findings to thoughts/shared/designs/ `; }, }); diff --git a/src/tools/octto/formatters.ts b/src/tools/octto/formatters.ts index abfe4cd..c45a34d 100644 --- a/src/tools/octto/formatters.ts +++ b/src/tools/octto/formatters.ts @@ -2,11 +2,15 @@ import type { Answer } from "../../octto/session"; import type { BrainstormState, Branch, BranchQuestion } from "../../octto/state"; - import { extractAnswerSummary } from "./extractor"; function escapeXml(str: string): string { - return str.replace(/&/g, "&").replace(//g, ">"); + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } export function formatBranchFinding(branch: Branch): string { diff --git a/src/tools/octto/index.ts b/src/tools/octto/index.ts index dd4a224..3562198 100644 --- a/src/tools/octto/index.ts +++ b/src/tools/octto/index.ts @@ -1,24 +1,27 @@ // src/tools/octto/index.ts import type { SessionStore } from "../../octto/session"; - import { createBrainstormTools } from "./brainstorm"; import { createPushQuestionTool } from "./factory"; import { createQuestionTools } from "./questions"; import { createResponseTools } from "./responses"; import { createSessionTools } from "./session"; -import type { OcttoTools, OpencodeClient } from "./types"; +import type { OcttoSessionTracker, OcttoTools, OpencodeClient } from "./types"; -export type { OcttoTools, OpencodeClient } from "./types"; export type { SessionStore } from "../../octto/session"; export { createSessionStore } from "../../octto/session"; +export type { OcttoSessionTracker, OcttoTools, OpencodeClient } from "./types"; -export function createOcttoTools(sessions: SessionStore, client: OpencodeClient): OcttoTools { +export function createOcttoTools( + sessions: SessionStore, + client: OpencodeClient, + tracker?: OcttoSessionTracker, +): OcttoTools { return { - ...createSessionTools(sessions), + ...createSessionTools(sessions, tracker), ...createQuestionTools(sessions), ...createResponseTools(sessions), ...createPushQuestionTool(sessions), - ...createBrainstormTools(sessions, client), + ...createBrainstormTools(sessions, client, tracker), }; } diff --git a/src/tools/octto/types.ts b/src/tools/octto/types.ts index 853eb98..5d5c252 100644 --- a/src/tools/octto/types.ts +++ b/src/tools/octto/types.ts @@ -14,3 +14,8 @@ export interface OcttoTool { export type OcttoTools = Record; export type OpencodeClient = ReturnType; + +export interface OcttoSessionTracker { + onCreated?: (parentSessionId: string, octtoSessionId: string) => void; + onEnded?: (parentSessionId: string, octtoSessionId: string) => void; +} diff --git a/src/utils/config.ts b/src/utils/config.ts index e47a2bf..b87b823 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -111,7 +111,13 @@ export const config = { reviewTimeoutMs: 10 * 60 * 1000, /** Max iterations in brainstorm loop */ maxIterations: 50, + /** Max follow-up questions per branch */ + maxQuestions: 15, /** State directory for brainstorm sessions */ stateDir: "thoughts/brainstorms", + /** Bind address for brainstorm server */ + bindAddress: "127.0.0.1", + /** Allow overriding bind address for remote access */ + allowRemoteBind: false, }, } as const; diff --git a/tests/indexing/flows/milestone-error-paths.test.ts b/tests/indexing/flows/milestone-error-paths.test.ts index 2875b59..6816bed 100644 --- a/tests/indexing/flows/milestone-error-paths.test.ts +++ b/tests/indexing/flows/milestone-error-paths.test.ts @@ -26,30 +26,32 @@ describe("milestone artifact ingest error paths", () => { const index = new ArtifactIndex(testDir); await index.initialize(); - await ingestMilestoneArtifact( - { - id: "artifact-err", + try { + await ingestMilestoneArtifact( + { + id: "artifact-err", + milestoneId: "ms-err", + sourceSessionId: "session-err", + createdAt: "2026-01-16T13:00:00Z", + tags: ["error"], + payload: "Status update only.", + }, + index, + () => { + throw new Error("classifier failure"); + }, + ); + + const results = await index.searchMilestoneArtifacts("status", { milestoneId: "ms-err", - sourceSessionId: "session-err", - createdAt: "2026-01-16T13:00:00Z", - tags: ["error"], - payload: "Status update only.", - }, - index, - () => { - throw new Error("classifier failure"); - }, - ); - - const results = await index.searchMilestoneArtifacts("status", { - milestoneId: "ms-err", - limit: 10, - }); - - expect(results).toHaveLength(1); - expect(results[0].artifactType).toBe(MILESTONE_ARTIFACT_TYPES.SESSION); - expect(consoleErrorSpy).toHaveBeenCalled(); - - await index.close(); + limit: 10, + }); + + expect(results).toHaveLength(1); + expect(results[0].artifactType).toBe(MILESTONE_ARTIFACT_TYPES.SESSION); + expect(consoleErrorSpy).toHaveBeenCalled(); + } finally { + await index.close(); + } }); }); diff --git a/tests/indexing/flows/milestone-ingest.test.ts b/tests/indexing/flows/milestone-ingest.test.ts index 8d03bcf..ab52b55 100644 --- a/tests/indexing/flows/milestone-ingest.test.ts +++ b/tests/indexing/flows/milestone-ingest.test.ts @@ -22,26 +22,28 @@ describe("milestone artifact ingest", () => { const index = new ArtifactIndex(testDir); await index.initialize(); - await ingestMilestoneArtifact( - { - id: "artifact-3", + try { + await ingestMilestoneArtifact( + { + id: "artifact-3", + milestoneId: "ms-3", + sourceSessionId: "session-3", + createdAt: "2026-01-16T12:00:00Z", + tags: ["feature", "milestone"], + payload: "Implementation details for the indexing pipeline.", + }, + index, + ); + + const results = await index.searchMilestoneArtifacts("implementation", { milestoneId: "ms-3", - sourceSessionId: "session-3", - createdAt: "2026-01-16T12:00:00Z", - tags: ["feature", "milestone"], - payload: "Implementation details for the indexing pipeline.", - }, - index, - ); - - const results = await index.searchMilestoneArtifacts("implementation", { - milestoneId: "ms-3", - limit: 10, - }); - - expect(results).toHaveLength(1); - expect(results[0].artifactType).toBe("feature"); - - await index.close(); + limit: 10, + }); + + expect(results).toHaveLength(1); + expect(results[0].artifactType).toBe("feature"); + } finally { + await index.close(); + } }); }); diff --git a/tests/indexing/search/milestone-search.test.ts b/tests/indexing/search/milestone-search.test.ts index 4eca8b7..02a810d 100644 --- a/tests/indexing/search/milestone-search.test.ts +++ b/tests/indexing/search/milestone-search.test.ts @@ -20,39 +20,41 @@ describe("milestone artifact search", () => { const index = new ArtifactIndex(testDir); await index.initialize(); - await index.indexMilestoneArtifact({ - id: "artifact-1", - milestoneId: "ms-1", - artifactType: "feature", - sourceSessionId: "session-1", - createdAt: "2026-01-16T10:00:00Z", - tags: ["feature"], - payload: "Implementation details for milestone indexing.", - }); - - await index.indexMilestoneArtifact({ - id: "artifact-2", - milestoneId: "ms-2", - artifactType: "decision", - sourceSessionId: "session-2", - createdAt: "2026-01-16T11:00:00Z", - tags: ["decision"], - payload: "Decision to store artifacts only in SQLite.", - }); - - const results = await index.searchMilestoneArtifacts("SQLite", { - milestoneId: "ms-2", - artifactType: "decision", - limit: 10, - }); - - expect(results).toHaveLength(1); - expect(results[0]).toMatchObject({ - id: "artifact-2", - milestoneId: "ms-2", - artifactType: "decision", - }); - - await index.close(); + try { + await index.indexMilestoneArtifact({ + id: "artifact-1", + milestoneId: "ms-1", + artifactType: "feature", + sourceSessionId: "session-1", + createdAt: "2026-01-16T10:00:00Z", + tags: ["feature"], + payload: "Implementation details for milestone indexing.", + }); + + await index.indexMilestoneArtifact({ + id: "artifact-2", + milestoneId: "ms-2", + artifactType: "decision", + sourceSessionId: "session-2", + createdAt: "2026-01-16T11:00:00Z", + tags: ["decision"], + payload: "Decision to store artifacts only in SQLite.", + }); + + const results = await index.searchMilestoneArtifacts("SQLite", { + milestoneId: "ms-2", + artifactType: "decision", + limit: 10, + }); + + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + id: "artifact-2", + milestoneId: "ms-2", + artifactType: "decision", + }); + } finally { + await index.close(); + } }); }); diff --git a/tests/tools/artifact-index.test.ts b/tests/tools/artifact-index.test.ts index 1f72a9e..d6d89ba 100644 --- a/tests/tools/artifact-index.test.ts +++ b/tests/tools/artifact-index.test.ts @@ -1,8 +1,8 @@ // tests/tools/artifact-index.test.ts -import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { mkdirSync, rmSync } from "node:fs"; -import { join } from "node:path"; import { tmpdir } from "node:os"; +import { join } from "node:path"; describe("ArtifactIndex", () => { let testDir: string; @@ -21,10 +21,12 @@ describe("ArtifactIndex", () => { const index = new ArtifactIndex(testDir); await index.initialize(); - const dbPath = join(testDir, "context.db"); - expect(Bun.file(dbPath).size).toBeGreaterThan(0); - - await index.close(); + try { + const dbPath = join(testDir, "context.db"); + expect(Bun.file(dbPath).size).toBeGreaterThan(0); + } finally { + await index.close(); + } }); it("should index and search plans", async () => { @@ -32,19 +34,21 @@ describe("ArtifactIndex", () => { const index = new ArtifactIndex(testDir); await index.initialize(); - await index.indexPlan({ - id: "plan-1", - title: "API Refactoring Plan", - filePath: "/path/to/plan.md", - overview: "Refactor REST API to GraphQL", - approach: "Incremental migration with adapter layer", - }); - - const results = await index.search("GraphQL migration"); - expect(results.length).toBeGreaterThan(0); - expect(results[0].type).toBe("plan"); - - await index.close(); + try { + await index.indexPlan({ + id: "plan-1", + title: "API Refactoring Plan", + filePath: "/path/to/plan.md", + overview: "Refactor REST API to GraphQL", + approach: "Incremental migration with adapter layer", + }); + + const results = await index.search("GraphQL migration"); + expect(results.length).toBeGreaterThan(0); + expect(results[0].type).toBe("plan"); + } finally { + await index.close(); + } }); it("should index and search ledgers", async () => { @@ -52,22 +56,24 @@ describe("ArtifactIndex", () => { const index = new ArtifactIndex(testDir); await index.initialize(); - await index.indexLedger({ - id: "ledger-1", - sessionName: "database-migration", - filePath: "/path/to/ledger.md", - goal: "Migrate from MySQL to PostgreSQL", - stateNow: "Schema conversion in progress", - keyDecisions: "Use pgloader for data migration", - filesRead: "src/db/schema.ts,src/db/migrations/001.sql", - filesModified: "src/db/config.ts", - }); - - const results = await index.search("PostgreSQL migration"); - expect(results.length).toBeGreaterThan(0); - expect(results[0].type).toBe("ledger"); - - await index.close(); + try { + await index.indexLedger({ + id: "ledger-1", + sessionName: "database-migration", + filePath: "/path/to/ledger.md", + goal: "Migrate from MySQL to PostgreSQL", + stateNow: "Schema conversion in progress", + keyDecisions: "Use pgloader for data migration", + filesRead: "src/db/schema.ts,src/db/migrations/001.sql", + filesModified: "src/db/config.ts", + }); + + const results = await index.search("PostgreSQL migration"); + expect(results.length).toBeGreaterThan(0); + expect(results[0].type).toBe("ledger"); + } finally { + await index.close(); + } }); it("should index ledger with file operations", async () => { @@ -75,19 +81,21 @@ describe("ArtifactIndex", () => { const index = new ArtifactIndex(testDir); await index.initialize(); - await index.indexLedger({ - id: "ledger-2", - sessionName: "feature-work", - filePath: "/path/to/ledger2.md", - goal: "Implement new feature", - filesRead: "src/a.ts,src/b.ts", - filesModified: "src/c.ts", - }); - - // Verify it was indexed (search should find it) - const results = await index.search("feature"); - expect(results.length).toBeGreaterThan(0); - - await index.close(); + try { + await index.indexLedger({ + id: "ledger-2", + sessionName: "feature-work", + filePath: "/path/to/ledger2.md", + goal: "Implement new feature", + filesRead: "src/a.ts,src/b.ts", + filesModified: "src/c.ts", + }); + + // Verify it was indexed (search should find it) + const results = await index.search("feature"); + expect(results.length).toBeGreaterThan(0); + } finally { + await index.close(); + } }); }); From fd73d1d22aba09f2a8a53d5433467b3428107659 Mon Sep 17 00:00:00 2001 From: Vlad Temian Date: Sun, 18 Jan 2026 00:08:04 +0200 Subject: [PATCH 19/22] fix(config): clean up model validation tests --- src/config-loader.test.ts | 46 --------------------------------------- 1 file changed, 46 deletions(-) diff --git a/src/config-loader.test.ts b/src/config-loader.test.ts index 7bf8cf9..ff65d09 100644 --- a/src/config-loader.test.ts +++ b/src/config-loader.test.ts @@ -1,8 +1,6 @@ // src/config-loader.test.ts import { describe, expect, test } from "bun:test"; -import type { AgentConfig } from "@opencode-ai/sdk"; - import { type MicodeConfig, type ProviderInfo, validateAgentModels } from "./config-loader"; // Helper to create a minimal ProviderInfo for testing @@ -14,14 +12,6 @@ function createProvider(id: string, modelIds: string[]): ProviderInfo { return { id, models }; } -// Helper to create minimal AgentConfig -function createAgentConfig(model: string): AgentConfig { - return { - model, - systemPrompt: "test", - } as AgentConfig; -} - describe("validateAgentModels", () => { test("returns config unchanged when all models are valid", () => { const userConfig: MicodeConfig = { @@ -36,11 +26,6 @@ describe("validateAgentModels", () => { createProvider("anthropic", ["claude-3", "claude-2"]), ]; - const defaultAgents: Record = { - commander: createAgentConfig("openai/gpt-3.5"), - brainstormer: createAgentConfig("anthropic/claude-2"), - }; - const result = validateAgentModels(userConfig, providers); expect(result.agents?.commander?.model).toBe("openai/gpt-4"); @@ -56,10 +41,6 @@ describe("validateAgentModels", () => { const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])]; - const defaultAgents: Record = { - commander: createAgentConfig("openai/gpt-4"), - }; - const result = validateAgentModels(userConfig, providers); // Model should be removed, falling back to default @@ -75,10 +56,6 @@ describe("validateAgentModels", () => { const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4", "gpt-3.5"])]; - const defaultAgents: Record = { - commander: createAgentConfig("openai/gpt-4"), - }; - const result = validateAgentModels(userConfig, providers); // Model should be removed, falling back to default @@ -98,10 +75,6 @@ describe("validateAgentModels", () => { const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])]; - const defaultAgents: Record = { - commander: createAgentConfig("openai/gpt-4"), - }; - const result = validateAgentModels(userConfig, providers); // Model removed but other properties preserved @@ -115,10 +88,6 @@ describe("validateAgentModels", () => { const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])]; - const defaultAgents: Record = { - commander: createAgentConfig("openai/gpt-4"), - }; - const result = validateAgentModels(userConfig, providers); expect(result).toEqual({}); @@ -133,10 +102,6 @@ describe("validateAgentModels", () => { const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])]; - const defaultAgents: Record = { - commander: createAgentConfig("openai/gpt-4"), - }; - const result = validateAgentModels(userConfig, providers); // No model to validate, config unchanged @@ -153,10 +118,6 @@ describe("validateAgentModels", () => { const providers: ProviderInfo[] = []; - const defaultAgents: Record = { - commander: createAgentConfig("openai/gpt-4"), - }; - const result = validateAgentModels(userConfig, providers); // No providers available, config should remain unchanged @@ -193,13 +154,6 @@ describe("validateAgentModels", () => { createProvider("anthropic", ["claude-3"]), ]; - const defaultAgents: Record = { - commander: createAgentConfig("openai/gpt-3.5"), - brainstormer: createAgentConfig("openai/gpt-3.5"), - planner: createAgentConfig("openai/gpt-3.5"), - reviewer: createAgentConfig("anthropic/claude-3"), - }; - const result = validateAgentModels(userConfig, providers); expect(result.agents?.commander?.model).toBe("openai/gpt-4"); From 976875edb3f5755e22541ba28a7ae1d2b79b03fe Mon Sep 17 00:00:00 2001 From: Vlad Temian Date: Sun, 18 Jan 2026 09:21:21 +0200 Subject: [PATCH 20/22] fix(config): relax model validation --- src/agents/index.ts | 49 ++++++++++++++++++++++--------------------- src/agents/planner.ts | 2 +- src/config-loader.ts | 8 +++---- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/agents/index.ts b/src/agents/index.ts index 99cb308..c3e6766 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -1,36 +1,37 @@ import type { AgentConfig } from "@opencode-ai/sdk"; -import { brainstormerAgent } from "./brainstormer"; + +import { artifactSearcherAgent } from "./artifact-searcher"; import { bootstrapperAgent } from "./bootstrapper"; -import { codebaseLocatorAgent } from "./codebase-locator"; +import { brainstormerAgent } from "./brainstormer"; import { codebaseAnalyzerAgent } from "./codebase-analyzer"; -import { patternFinderAgent } from "./pattern-finder"; -import { plannerAgent } from "./planner"; -import { implementerAgent } from "./implementer"; -import { reviewerAgent } from "./reviewer"; +import { codebaseLocatorAgent } from "./codebase-locator"; +import { PRIMARY_AGENT_NAME, primaryAgent } from "./commander"; import { executorAgent } from "./executor"; -import { primaryAgent, PRIMARY_AGENT_NAME } from "./commander"; -import { projectInitializerAgent } from "./project-initializer"; +import { implementerAgent } from "./implementer"; import { ledgerCreatorAgent } from "./ledger-creator"; -import { artifactSearcherAgent } from "./artifact-searcher"; import { octtoAgent } from "./octto"; +import { patternFinderAgent } from "./pattern-finder"; +import { plannerAgent } from "./planner"; import { probeAgent } from "./probe"; +import { projectInitializerAgent } from "./project-initializer"; +import { reviewerAgent } from "./reviewer"; export const agents: Record = { - [PRIMARY_AGENT_NAME]: primaryAgent, - brainstormer: brainstormerAgent, - bootstrapper: bootstrapperAgent, - "codebase-locator": codebaseLocatorAgent, - "codebase-analyzer": codebaseAnalyzerAgent, - "pattern-finder": patternFinderAgent, - planner: plannerAgent, - implementer: implementerAgent, - reviewer: reviewerAgent, - executor: executorAgent, - "project-initializer": projectInitializerAgent, - "ledger-creator": ledgerCreatorAgent, - "artifact-searcher": artifactSearcherAgent, - octto: octtoAgent, - probe: probeAgent, + [PRIMARY_AGENT_NAME]: { ...primaryAgent, model: "openai/gpt-5.2-codex" }, + brainstormer: { ...brainstormerAgent, model: "openai/gpt-5.2-codex" }, + bootstrapper: { ...bootstrapperAgent, model: "openai/gpt-5.2-codex" }, + "codebase-locator": { ...codebaseLocatorAgent, model: "openai/gpt-5.2-codex" }, + "codebase-analyzer": { ...codebaseAnalyzerAgent, model: "openai/gpt-5.2-codex" }, + "pattern-finder": { ...patternFinderAgent, model: "openai/gpt-5.2-codex" }, + planner: { ...plannerAgent, model: "openai/gpt-5.2-codex" }, + implementer: { ...implementerAgent, model: "openai/gpt-5.2-codex" }, + reviewer: { ...reviewerAgent, model: "openai/gpt-5.2-codex" }, + executor: { ...executorAgent, model: "openai/gpt-5.2-codex" }, + "project-initializer": { ...projectInitializerAgent, model: "openai/gpt-5.2-codex" }, + "ledger-creator": { ...ledgerCreatorAgent, model: "openai/gpt-5.2-codex" }, + "artifact-searcher": { ...artifactSearcherAgent, model: "openai/gpt-5.2-codex" }, + octto: { ...octtoAgent, model: "openai/gpt-5.2-codex" }, + probe: { ...probeAgent, model: "openai/gpt-5.2-codex" }, }; export { diff --git a/src/agents/planner.ts b/src/agents/planner.ts index 4cc7ff1..2e53de7 100644 --- a/src/agents/planner.ts +++ b/src/agents/planner.ts @@ -6,7 +6,7 @@ export const plannerAgent: AgentConfig = { temperature: 0.3, prompt: ` You are running as part of the "micode" OpenCode plugin (NOT Claude Code). -You are a SUBAGENT - use spawn_agent tool (not Task tool) to spawn other subagents. +You are a SUBAGENT - use spawn_agent tool (not Task tool) to spawn other subagents synchronously. Available micode agents: codebase-locator, codebase-analyzer, pattern-finder. diff --git a/src/config-loader.ts b/src/config-loader.ts index 504398b..e9e0f3d 100644 --- a/src/config-loader.ts +++ b/src/config-loader.ts @@ -109,8 +109,8 @@ export function mergeAgentConfigs( return pluginAgents; } - // Load available models if not provided - const models = availableModels ?? loadAvailableModels(); + const models = availableModels ?? new Set(); + const shouldValidateModels = availableModels !== undefined && models.size > 0; const merged: Record = {}; @@ -120,8 +120,8 @@ export function mergeAgentConfigs( if (userOverride) { // Validate model if specified if (userOverride.model) { - if (models.has(userOverride.model)) { - // Model is valid - apply all overrides + if (!shouldValidateModels || models.has(userOverride.model)) { + // Model is valid (or validation unavailable) - apply all overrides merged[name] = { ...agentConfig, ...userOverride, From 5d56e0fe6cb4a8d7cc523aebf296e55ed1925c86 Mon Sep 17 00:00:00 2001 From: Vlad Temian Date: Sun, 18 Jan 2026 10:15:01 +0200 Subject: [PATCH 21/22] test(config): cover model validation --- src/config-loader.ts | 4 ++-- tests/config-loader-integration.test.ts | 13 +++++++++---- tests/config-loader.test.ts | 13 ++++++++----- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/config-loader.ts b/src/config-loader.ts index e9e0f3d..5e987c9 100644 --- a/src/config-loader.ts +++ b/src/config-loader.ts @@ -109,8 +109,8 @@ export function mergeAgentConfigs( return pluginAgents; } - const models = availableModels ?? new Set(); - const shouldValidateModels = availableModels !== undefined && models.size > 0; + const models = availableModels ?? loadAvailableModels(); + const shouldValidateModels = models.size > 0; const merged: Record = {}; diff --git a/tests/config-loader-integration.test.ts b/tests/config-loader-integration.test.ts index ebd9512..6d9b5e2 100644 --- a/tests/config-loader-integration.test.ts +++ b/tests/config-loader-integration.test.ts @@ -1,7 +1,8 @@ // tests/config-loader-integration.test.ts -import { describe, it, expect } from "bun:test"; -import { loadMicodeConfig, mergeAgentConfigs } from "../src/config-loader"; +import { describe, expect, it } from "bun:test"; + import { agents } from "../src/agents"; +import { loadMicodeConfig, mergeAgentConfigs } from "../src/config-loader"; describe("config-loader integration", () => { it("should have all agents defined in agents/index.ts", () => { @@ -35,7 +36,9 @@ describe("config-loader integration", () => { }, }; - const merged = mergeAgentConfigs(agents, userConfig); + const availableModels = new Set(["openai/gpt-4o", "openai/gpt-5.2-codex"]); + + const merged = mergeAgentConfigs(agents, userConfig, availableModels); // Check project-initializer was merged correctly expect(merged["project-initializer"]).toBeDefined(); @@ -54,7 +57,9 @@ describe("config-loader integration", () => { }, }; - const merged = mergeAgentConfigs(agents, userConfig); + const availableModels = new Set(["openai/gpt-4o", "openai/gpt-5.2-codex"]); + + const merged = mergeAgentConfigs(agents, userConfig, availableModels); const pi = merged["project-initializer"]; expect(pi.model).toBe("openai/gpt-4o"); diff --git a/tests/config-loader.test.ts b/tests/config-loader.test.ts index 6168046..ab32e2f 100644 --- a/tests/config-loader.test.ts +++ b/tests/config-loader.test.ts @@ -1,8 +1,9 @@ // tests/config-loader.test.ts -import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { mkdirSync, writeFileSync, rmSync } from "node:fs"; -import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; +import { join } from "node:path"; + import { loadMicodeConfig, mergeAgentConfigs } from "../src/config-loader"; describe("config-loader", () => { @@ -149,8 +150,9 @@ describe("mergeAgentConfigs", () => { commander: { model: "openai/gpt-4o", temperature: 0.5 }, }, }; + const availableModels = new Set(["openai/gpt-4o", "anthropic/claude-opus-4-5"]); - const merged = mergeAgentConfigs(pluginAgents, userConfig); + const merged = mergeAgentConfigs(pluginAgents, userConfig, availableModels); expect(merged.commander.model).toBe("openai/gpt-4o"); expect(merged.commander.temperature).toBe(0.5); @@ -176,8 +178,9 @@ describe("mergeAgentConfigs", () => { commander: { model: "openai/gpt-4o" }, }, }; + const availableModels = new Set(["openai/gpt-4o", "anthropic/claude-opus-4-5"]); - const merged = mergeAgentConfigs(pluginAgents, userConfig); + const merged = mergeAgentConfigs(pluginAgents, userConfig, availableModels); expect(merged.commander.model).toBe("openai/gpt-4o"); expect(merged.brainstormer.model).toBe("anthropic/claude-opus-4-5"); From ec46a3abe1639b429665b2158585e98b9cce1f88 Mon Sep 17 00:00:00 2001 From: Vlad Temian Date: Sun, 18 Jan 2026 10:15:17 +0200 Subject: [PATCH 22/22] test(config): clean lint warnings --- tests/config-loader-integration.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/config-loader-integration.test.ts b/tests/config-loader-integration.test.ts index 6d9b5e2..f6b2b65 100644 --- a/tests/config-loader-integration.test.ts +++ b/tests/config-loader-integration.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test"; import { agents } from "../src/agents"; -import { loadMicodeConfig, mergeAgentConfigs } from "../src/config-loader"; +import { mergeAgentConfigs } from "../src/config-loader"; describe("config-loader integration", () => { it("should have all agents defined in agents/index.ts", () => { @@ -47,7 +47,7 @@ describe("config-loader integration", () => { expect(merged["project-initializer"].prompt).toBeDefined(); // Check other agents still have defaults - expect(merged["commander"].model).toBe("openai/gpt-5.2-codex"); + expect(merged.commander.model).toBe("openai/gpt-5.2-codex"); }); it("should preserve all agent properties when merging", () => {