Merge pull request #4135 from code-yeongyu/fix/3893-team-mode-fresh-install
fix: add team mode fresh install diagnostics
This commit is contained in:
@@ -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/),
|
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).
|
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
|
## [4.2.0] - 2026-05-15
|
||||||
|
|
||||||
### Added
|
### 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.
|
- **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.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
|
||||||
|
|||||||
@@ -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.
|
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)
|
## Config schema (11 fields)
|
||||||
|
|
||||||
All fields live under `team_mode`:
|
All fields live under `team_mode`:
|
||||||
|
|||||||
@@ -179,7 +179,10 @@ export function loadConfigFromPath(
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
addAgentOrderWarnings(configPath, result.data.agent_order);
|
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;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +198,10 @@ export function loadConfigFromPath(
|
|||||||
const partialResult = parseConfigPartially(rawConfig);
|
const partialResult = parseConfigPartially(rawConfig);
|
||||||
if (partialResult) {
|
if (partialResult) {
|
||||||
addAgentOrderWarnings(configPath, partialResult.agent_order);
|
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;
|
return partialResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,6 +400,7 @@ export function loadPluginConfig(
|
|||||||
|
|
||||||
log("Final merged config", {
|
log("Final merged config", {
|
||||||
agents: config.agents,
|
agents: config.agents,
|
||||||
|
team_mode: config.team_mode,
|
||||||
disabled_agents: config.disabled_agents,
|
disabled_agents: config.disabled_agents,
|
||||||
disabled_mcps: config.disabled_mcps,
|
disabled_mcps: config.disabled_mcps,
|
||||||
disabled_hooks: config.disabled_hooks,
|
disabled_hooks: config.disabled_hooks,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
/// <reference types="bun-types" />
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
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"
|
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<typeof import("../plugin-config")> {
|
||||||
|
return import(`../plugin-config?team-mode-fresh-install=${Date.now()}-${Math.random()}`)
|
||||||
|
}
|
||||||
|
|
||||||
function createPluginConfig() {
|
function createPluginConfig() {
|
||||||
return OhMyOpenCodeConfigSchema.parse({
|
return OhMyOpenCodeConfigSchema.parse({
|
||||||
git_master: {
|
git_master: {
|
||||||
@@ -30,6 +47,83 @@ function createPluginConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("team-mode tool registry wiring", () => {
|
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<typeof createToolRegistry>[0]["ctx"],
|
||||||
|
pluginConfig,
|
||||||
|
managers: {
|
||||||
|
backgroundManager: {},
|
||||||
|
tmuxSessionManager: {},
|
||||||
|
skillMcpManager: {},
|
||||||
|
} as Parameters<typeof createToolRegistry>[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", () => {
|
test("passes ctx.client into every team tool factory", () => {
|
||||||
// given
|
// given
|
||||||
const client = {} as OpencodeClient
|
const client = {} as OpencodeClient
|
||||||
|
|||||||
@@ -356,6 +356,14 @@ export function createToolRegistry(args: {
|
|||||||
...hashlineToolsRecord,
|
...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)) {
|
for (const toolDefinition of Object.values(allTools)) {
|
||||||
normalizeToolArgSchemas(toolDefinition)
|
normalizeToolArgSchemas(toolDefinition)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ interface LoggerTestOverrides {
|
|||||||
|
|
||||||
/** @internal test-only seam */
|
/** @internal test-only seam */
|
||||||
export function _setLoggerForTesting(overrides: LoggerTestOverrides): void {
|
export function _setLoggerForTesting(overrides: LoggerTestOverrides): void {
|
||||||
|
buffer = []
|
||||||
|
if (flushTimer) {
|
||||||
|
clearTimeout(flushTimer)
|
||||||
|
flushTimer = null
|
||||||
|
}
|
||||||
if (overrides.filePath !== undefined) logFile = overrides.filePath
|
if (overrides.filePath !== undefined) logFile = overrides.filePath
|
||||||
if (overrides.maxSizeBytes !== undefined) maxLogFileSizeBytes = overrides.maxSizeBytes
|
if (overrides.maxSizeBytes !== undefined) maxLogFileSizeBytes = overrides.maxSizeBytes
|
||||||
if (overrides.maxBackups !== undefined) maxLogFileBackups = overrides.maxBackups
|
if (overrides.maxBackups !== undefined) maxLogFileBackups = overrides.maxBackups
|
||||||
|
|||||||
Reference in New Issue
Block a user