diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e83d6e7..55995339e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.2.1] - Unreleased + +### Fixed + +- Team Mode fresh-install diagnostics now log the resolved `team_mode` config and tool-registry team tool count, making #3893-style missing `team_*` registrations visible instead of silent. +- Added a regression test proving a fresh minimal user config with `{ "team_mode": { "enabled": true } }` registers all 12 `team_*` tools. + ## [4.2.0] - 2026-05-15 ### Added @@ -37,3 +44,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **First-prompt watchdog supersession history (L16)**: PR #3952 was superseded by PR #4051 (rebased over #4007/factory refactor with `internallyAbortedSessions` threading). The supersession represents conflict resolution, not a feature pivot. The final watchdog logic shipped via #4051 + `a130fa70d` covers subagent first-prompt silence past 90 seconds with cleanup via session.deleted. [4.2.0]: https://github.com/code-yeongyu/oh-my-openagent/compare/v4.1.2...v4.2.0 +[4.2.1]: https://github.com/code-yeongyu/oh-my-openagent/compare/v4.2.0...HEAD diff --git a/docs/guide/team-mode.md b/docs/guide/team-mode.md index be2eaf7e5..e6d00d2bf 100644 --- a/docs/guide/team-mode.md +++ b/docs/guide/team-mode.md @@ -29,6 +29,8 @@ Add to user config `~/.config/opencode/oh-my-openagent.jsonc` or project config After enabling, restart opencode. The 12 `team_*` tools become available. +> Bug-fix note: v4.2.1 adds a fresh-install regression test for this minimal config and logs the resolved `team_mode` state plus team tool count during startup. If the tools still do not appear after restart, inspect `oh-my-opencode.log` for the loaded config path and `[tool-registry] Built tool registry` entry. + ## Config schema (11 fields) All fields live under `team_mode`: diff --git a/src/plugin-config.ts b/src/plugin-config.ts index 0914be7b8..b203b00ac 100644 --- a/src/plugin-config.ts +++ b/src/plugin-config.ts @@ -179,7 +179,10 @@ export function loadConfigFromPath( if (result.success) { addAgentOrderWarnings(configPath, result.data.agent_order); - log(`Config loaded from ${configPath}`, { agents: result.data.agents }); + log(`Config loaded from ${configPath}`, { + agents: result.data.agents, + team_mode: result.data.team_mode, + }); return result.data; } @@ -195,7 +198,10 @@ export function loadConfigFromPath( const partialResult = parseConfigPartially(rawConfig); if (partialResult) { addAgentOrderWarnings(configPath, partialResult.agent_order); - log(`Partial config loaded from ${configPath}`, { agents: partialResult.agents }); + log(`Partial config loaded from ${configPath}`, { + agents: partialResult.agents, + team_mode: partialResult.team_mode, + }); return partialResult; } @@ -394,6 +400,7 @@ export function loadPluginConfig( log("Final merged config", { agents: config.agents, + team_mode: config.team_mode, disabled_agents: config.disabled_agents, disabled_mcps: config.disabled_mcps, disabled_hooks: config.disabled_hooks, diff --git a/src/plugin/tool-registry.team-mode.test.ts b/src/plugin/tool-registry.team-mode.test.ts index d858dee45..787c7012a 100644 --- a/src/plugin/tool-registry.team-mode.test.ts +++ b/src/plugin/tool-registry.team-mode.test.ts @@ -1,6 +1,9 @@ /// -import { describe, expect, mock, test } from "bun:test" +import { afterEach, describe, expect, mock, test } from "bun:test" +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" import { tool } from "@opencode-ai/plugin" @@ -16,6 +19,20 @@ const fakeTool = tool({ }, }) +const tempDirs: string[] = [] + +afterEach(() => { + delete process.env.OPENCODE_CONFIG_DIR + + for (const tempDir of tempDirs.splice(0)) { + rmSync(tempDir, { recursive: true, force: true }) + } +}) + +async function importFreshPluginConfigModule(): Promise { + return import(`../plugin-config?team-mode-fresh-install=${Date.now()}-${Math.random()}`) +} + function createPluginConfig() { return OhMyOpenCodeConfigSchema.parse({ git_master: { @@ -30,6 +47,83 @@ function createPluginConfig() { } describe("team-mode tool registry wiring", () => { + test("registers team tools from a fresh-install minimal user config", async () => { + // given + const rootDir = mkdtempSync(join(tmpdir(), "omo-team-mode-fresh-install-")) + tempDirs.push(rootDir) + const userConfigDir = join(rootDir, "home", ".config", "opencode") + const projectDir = join(rootDir, "project") + + mkdirSync(userConfigDir, { recursive: true }) + mkdirSync(projectDir, { recursive: true }) + writeFileSync( + join(userConfigDir, "oh-my-openagent.json"), + JSON.stringify({ team_mode: { enabled: true } }), + ) + process.env.OPENCODE_CONFIG_DIR = userConfigDir + + const { loadPluginConfig } = await importFreshPluginConfigModule() + const pluginConfig = loadPluginConfig(projectDir, {}) + + // when + const result = createToolRegistry({ + ctx: { directory: projectDir, client: {} } as Parameters[0]["ctx"], + pluginConfig, + managers: { + backgroundManager: {}, + tmuxSessionManager: {}, + skillMcpManager: {}, + } as Parameters[0]["managers"], + skillContext: { + mergedSkills: [], + availableSkills: [], + browserProvider: "playwright", + disabledSkills: new Set(), + }, + availableCategories: [], + toolFactories: { + builtinTools: { bash: fakeTool, read: fakeTool }, + createBackgroundTools: mock(() => ({})), + createCallOmoAgent: mock(() => fakeTool), + createLookAt: mock(() => fakeTool), + createSkillMcpTool: mock(() => fakeTool), + createSkillTool: mock(() => fakeTool), + createGrepTools: mock(() => ({})), + createGlobTools: mock(() => ({})), + createAstGrepTools: mock(() => ({})), + createSessionManagerTools: mock(() => ({})), + createDelegateTask: mock(() => fakeTool), + discoverCommandsSync: mock(() => []), + interactive_bash: fakeTool, + createTaskCreateTool: mock(() => fakeTool), + createTaskGetTool: mock(() => fakeTool), + createTaskList: mock(() => fakeTool), + createTaskUpdateTool: mock(() => fakeTool), + createHashlineEditTool: mock(() => fakeTool), + createTeamCreateTool: mock(() => fakeTool), + createTeamDeleteTool: mock(() => fakeTool), + createTeamShutdownRequestTool: mock(() => fakeTool), + createTeamApproveShutdownTool: mock(() => fakeTool), + createTeamRejectShutdownTool: mock(() => fakeTool), + createTeamSendMessageTool: mock(() => fakeTool), + createTeamTaskCreateTool: mock(() => fakeTool), + createTeamTaskListTool: mock(() => fakeTool), + createTeamTaskUpdateTool: mock(() => fakeTool), + createTeamTaskGetTool: mock(() => fakeTool), + createTeamStatusTool: mock(() => fakeTool), + createTeamListTool: mock(() => fakeTool), + }, + }) + + // then + expect(pluginConfig.team_mode?.enabled).toBe(true) + expect(result.filteredTools).toHaveProperty("team_create") + expect(result.filteredTools).toHaveProperty("team_send_message") + expect(result.filteredTools).toHaveProperty("team_task_create") + expect(result.filteredTools).toHaveProperty("team_status") + expect(Object.keys(result.filteredTools).filter((toolName) => toolName.startsWith("team_"))).toHaveLength(12) + }) + test("passes ctx.client into every team tool factory", () => { // given const client = {} as OpencodeClient diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index 30c311c2e..a95bc9ab9 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -356,6 +356,14 @@ export function createToolRegistry(args: { ...hashlineToolsRecord, } + const allToolNames = Object.keys(allTools) + const teamToolCount = allToolNames.filter((toolName) => toolName.startsWith("team_")).length + log("[tool-registry] Built tool registry", { + totalTools: allToolNames.length, + teamModeEnabled: pluginConfig.team_mode?.enabled ?? false, + teamToolCount, + }) + for (const toolDefinition of Object.values(allTools)) { normalizeToolArgSchemas(toolDefinition) } diff --git a/src/shared/logger.ts b/src/shared/logger.ts index 0fbe58c16..c9b8dde65 100644 --- a/src/shared/logger.ts +++ b/src/shared/logger.ts @@ -89,6 +89,11 @@ interface LoggerTestOverrides { /** @internal test-only seam */ export function _setLoggerForTesting(overrides: LoggerTestOverrides): void { + buffer = [] + if (flushTimer) { + clearTimeout(flushTimer) + flushTimer = null + } if (overrides.filePath !== undefined) logFile = overrides.filePath if (overrides.maxSizeBytes !== undefined) maxLogFileSizeBytes = overrides.maxSizeBytes if (overrides.maxBackups !== undefined) maxLogFileBackups = overrides.maxBackups