Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ export namespace Flag {
export const OPENCODE_DISABLE_CLAUDE_CODE = truthy("OPENCODE_DISABLE_CLAUDE_CODE")
export const OPENCODE_DISABLE_CLAUDE_CODE_PROMPT =
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT")
export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS =
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
export const OPENCODE_DISABLE_EXTERNAL_SKILLS =
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS || truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS")
export declare const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: boolean
export declare const OPENCODE_DISABLE_EXTERNAL_SKILLS: boolean
export declare const OPENCODE_DISABLE_PROJECT_CONFIG: boolean
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export declare const OPENCODE_CLIENT: string
Expand All @@ -47,6 +45,7 @@ export namespace Flag {
export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL")
export const OPENCODE_DISABLE_FILETIME_CHECK = truthy("OPENCODE_DISABLE_FILETIME_CHECK")
export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE")
export declare const OPENCODE_EXPERIMENTAL_AGENT_TEAMS: boolean
export const OPENCODE_EXPERIMENTAL_MARKDOWN = truthy("OPENCODE_EXPERIMENTAL_MARKDOWN")
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"]
Expand Down Expand Up @@ -91,3 +90,35 @@ Object.defineProperty(Flag, "OPENCODE_CLIENT", {
enumerable: true,
configurable: false,
})

// Dynamic getter for OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
// Evaluated at access time so tests and external tooling can toggle it
Object.defineProperty(Flag, "OPENCODE_DISABLE_CLAUDE_CODE_SKILLS", {
get() {
return truthy("OPENCODE_DISABLE_CLAUDE_CODE") || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
},
enumerable: true,
configurable: false,
})

// Dynamic getter for OPENCODE_DISABLE_EXTERNAL_SKILLS
// Independent of OPENCODE_DISABLE_CLAUDE_CODE so disabling Claude Code
// doesn't block .agents/skills/ loading
Object.defineProperty(Flag, "OPENCODE_DISABLE_EXTERNAL_SKILLS", {
get() {
return truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS")
},
enumerable: true,
configurable: false,
})

// Dynamic getter for OPENCODE_EXPERIMENTAL_AGENT_TEAMS
// This must be evaluated at access time, not module load time,
// because integration tests and external tooling may set this env var at runtime
Object.defineProperty(Flag, "OPENCODE_EXPERIMENTAL_AGENT_TEAMS", {
get() {
return Flag.OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_AGENT_TEAMS")
},
enumerable: true,
configurable: false,
})
16 changes: 15 additions & 1 deletion packages/opencode/src/server/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,21 @@ export const SessionRoutes = lazy(() =>
}),
),
async (c) => {
SessionPrompt.cancel(c.req.valid("param").sessionID)
const sessionID = c.req.valid("param").sessionID
SessionPrompt.cancel(sessionID)

// Propagate abort to active teammates if this is a team lead session.
// Mirrors the Task tool's abort propagation pattern (task.ts:121-125).
try {
const { Team } = await import("@/team")
const match = await Team.findBySession(sessionID)
if (match?.role === "lead") {
await Team.cancelAllMembers(match.team.name)
}
} catch {
// Team module may not be loaded — safe to ignore
}

return c.json(true)
},
)
Expand Down
159 changes: 159 additions & 0 deletions packages/opencode/src/server/routes/team.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Hono } from "hono"
import z from "zod"
import { describeRoute, validator, resolver } from "hono-openapi"
import { Team, TeamTasks, TeamInfoSchema, TeamTaskSchema, WRITE_TOOLS } from "@/team"
import { Session } from "@/session"
import { lazy } from "../../util/lazy"
import { errors } from "../error"

const Delegate = z.object({ enabled: z.boolean() })

export const TeamRoutes = lazy(() =>
new Hono()
.get(
"/",
describeRoute({
summary: "List teams",
description: "List all teams in this project.",
operationId: "team.list",
responses: {
200: {
description: "List of teams",
content: { "application/json": { schema: resolver(TeamInfoSchema.array()) } },
},
},
}),
async (c) => {
return c.json(await Team.list())
},
)
.get(
"/:name",
describeRoute({
summary: "Get team",
description: "Retrieve a team by name.",
operationId: "team.get",
responses: {
200: {
description: "Team info",
content: { "application/json": { schema: resolver(TeamInfoSchema) } },
},
...errors(404),
},
}),
validator("param", z.object({ name: z.string() })),
async (c) => {
const team = await Team.get(c.req.valid("param").name)
if (!team) return c.json({ error: "Team not found" }, 404)
return c.json(team)
},
)
.get(
"/:name/tasks",
describeRoute({
summary: "List team tasks",
description: "List all tasks for a team.",
operationId: "team.tasks.list",
responses: {
200: {
description: "List of tasks",
content: { "application/json": { schema: resolver(TeamTaskSchema.array()) } },
},
},
}),
validator("param", z.object({ name: z.string() })),
async (c) => {
return c.json(await TeamTasks.list(c.req.valid("param").name))
},
)
.get(
"/by-session/:sessionID",
describeRoute({
summary: "Find team by session",
description: "Find the team a session belongs to.",
operationId: "team.bySession",
responses: {
200: { description: "Team info with role and tasks" },
},
}),
validator("param", z.object({ sessionID: z.string() })),
async (c) => {
const result = await Team.findBySession(c.req.valid("param").sessionID)
if (!result) return c.json(null)
return c.json({
team: result.team,
tasks: await TeamTasks.list(result.team.name),
role: result.role,
memberName: result.memberName,
})
},
)
.post(
"/:name/delegate",
describeRoute({
summary: "Toggle delegate mode",
description: "Enable or disable delegate mode for a team.",
operationId: "team.delegate",
responses: {
200: { description: "Delegate mode updated" },
...errors(400, 404),
},
}),
validator("param", z.object({ name: z.string() })),
validator("json", Delegate),
async (c) => {
const { name } = c.req.valid("param")
const { enabled } = c.req.valid("json")
const team = await Team.get(name)
if (!team) return c.json({ error: "Team not found" }, 404)

await Session.update(team.leadSessionID, (draft) => {
if (enabled) {
const existing = draft.permission ?? []
draft.permission = [
...existing,
// Filter prevents duplicate deny rules from repeated enable toggles
...WRITE_TOOLS.filter((tool) => !existing.some((r) => r.permission === tool && r.action === "deny")).map(
(tool) => ({ permission: tool, pattern: "*", action: "deny" as const }),
),
]
} else {
draft.permission = (draft.permission ?? []).filter(
(rule) => !((WRITE_TOOLS as readonly string[]).includes(rule.permission) && rule.action === "deny"),
)
}
})

await Team.setDelegate(name, enabled)
return c.json({ ok: true, delegate: enabled })
},
)
.post(
"/:name/cancel",
describeRoute({
summary: "Cancel teammates",
description:
"Cancel active teammates' prompt loops. " + "Pass { member: name } to cancel one, or omit to cancel all.",
operationId: "team.cancel",
responses: {
200: { description: "Number of cancelled members" },
...errors(404),
},
}),
validator("param", z.object({ name: z.string() })),
validator("json", z.object({ member: z.string().optional() })),
async (c) => {
const { name } = c.req.valid("param")
const { member } = c.req.valid("json")
const team = await Team.get(name)
if (!team) return c.json({ error: "Team not found" }, 404)

if (member) {
const ok = await Team.cancelMember(name, member)
return c.json({ ok, cancelled: ok ? 1 : 0 })
}
const cancelled = await Team.cancelAllMembers(name)
return c.json({ ok: true, cancelled })
},
),
)
2 changes: 2 additions & 0 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { McpRoutes } from "./routes/mcp"
import { FileRoutes } from "./routes/file"
import { ConfigRoutes } from "./routes/config"
import { ExperimentalRoutes } from "./routes/experimental"
import { TeamRoutes } from "./routes/team"
import { ProviderRoutes } from "./routes/provider"
import { lazy } from "../util/lazy"
import { InstanceBootstrap } from "../project/bootstrap"
Expand Down Expand Up @@ -234,6 +235,7 @@ export namespace Server {
.route("/provider", ProviderRoutes())
.route("/", FileRoutes())
.route("/mcp", McpRoutes())
.route("/team", TeamRoutes())
.route("/tui", TuiRoutes())
.post(
"/instance/dispose",
Expand Down
Loading
Loading