diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts
index f66cdc0a4..b78510d16 100644
--- a/src/plugin/tool-registry.ts
+++ b/src/plugin/tool-registry.ts
@@ -31,7 +31,7 @@ import {
createHashlineEditTool,
createPrepareCouncilPromptTool,
} from "../tools"
-import { createCouncilFinalize, createCouncilRead } from "../tools/council-archive"
+import { createCouncilFinalize } from "../tools/council-archive"
import { contextCollector } from "../features/context-injector"
import { getMainSessionID } from "../features/claude-code-session-state"
import { filterDisabledTools } from "../shared/disabled-tools"
@@ -283,7 +283,6 @@ export function createToolRegistry(args: {
...hashlineToolsRecord,
prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory),
council_finalize: createCouncilFinalize(ctx.directory, { contextCollector }),
- council_read: createCouncilRead(ctx.directory),
}
for (const toolDefinition of Object.values(allTools)) {
diff --git a/src/tools/council-archive/council-flow.integration.test.ts b/src/tools/council-archive/council-flow.integration.test.ts
index 375082036..c5355a196 100644
--- a/src/tools/council-archive/council-flow.integration.test.ts
+++ b/src/tools/council-archive/council-flow.integration.test.ts
@@ -5,7 +5,6 @@ import { mkdtemp, mkdir, writeFile, readFile, rm, stat } from "node:fs/promises"
import { join } from "node:path"
import { tmpdir } from "node:os"
import { createCouncilFinalize } from "./create-council-finalize"
-import { createCouncilRead } from "./create-council-read"
import type { CouncilFinalizeResult } from "./types"
import type { BackgroundTask } from "../../features/background-agent"
import type { BackgroundOutputManager } from "../background-task/clients"
@@ -88,7 +87,7 @@ afterEach(async () => {
describe("council archive integration flow", () => {
describe("#given 3 council members with valid output files", () => {
describe("#when finalize is called and then each archive is read", () => {
- it("#then creates archive with correct structure and council_read extracts content from archives", async () => {
+ it("#then creates archive with correct structure and archives are readable", async () => {
const agents = [
{ id: "bg_opus", agent: "Council: Claude Opus", response: "Opus deep analysis of architecture" },
{ id: "bg_gpt", agent: "Council: GPT-5", response: "GPT pragmatic code review" },
@@ -137,23 +136,13 @@ describe("council archive integration flow", () => {
expect(archiveContent).toBe(agents[i].response)
}
- const readTool = createCouncilRead(tmpDir)
-
- for (let i = 0; i < agents.length; i++) {
- const readResult = await readTool.execute({ file_path: result.members[i].archive_file! }, toolContext)
- const parsed = JSON.parse(readResult)
-
- expect(parsed.has_response).toBe(true)
- expect(parsed.response_complete).toBe(true)
- expect(parsed.result).toBe(agents[i].response)
- }
})
})
})
describe("#given a member with incomplete tags (no closing tag)", () => {
describe("#when finalize is called and archive is read", () => {
- it("#then finalize marks incomplete but council_read still returns raw archived content", async () => {
+ it("#then finalize marks incomplete but archive still contains raw content", async () => {
const taskId = "bg_partial"
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", `${taskId}.md`),
@@ -176,13 +165,6 @@ describe("council archive integration flow", () => {
const archiveContent = await readFile(join(tmpDir, member.archive_file!), "utf-8")
expect(archiveContent).toBe("Analysis still in progress...")
- const readTool = createCouncilRead(tmpDir)
- const readResult = await readTool.execute({ file_path: member.archive_file! }, toolContext)
- const parsed = JSON.parse(readResult)
-
- expect(parsed.has_response).toBe(true)
- expect(parsed.response_complete).toBe(true)
- expect(parsed.result).toBe("Analysis still in progress...")
})
})
})
@@ -248,7 +230,7 @@ describe("council archive integration flow", () => {
describe("#given a very large council response exceeding 8000 chars", () => {
describe("#when finalize is called and then archive is read", () => {
- it("#then finalize stores full output and council_read returns full response", async () => {
+ it("#then finalize stores full output and archive contains full response", async () => {
const largeResponse = "A".repeat(9000)
const taskId = "bg_large"
await writeFile(
@@ -271,13 +253,6 @@ describe("council archive integration flow", () => {
const fullContent = await readFile(join(tmpDir, member.archive_file!), "utf-8")
expect(fullContent).toHaveLength(9000)
- const readTool = createCouncilRead(tmpDir)
- const readResult = await readTool.execute({ file_path: member.archive_file! }, toolContext)
- const parsed = JSON.parse(readResult)
-
- expect(parsed.has_response).toBe(true)
- expect(parsed.response_complete).toBe(true)
- expect(parsed.result).toHaveLength(9000)
})
})
})
diff --git a/src/tools/council-archive/create-council-finalize.test.ts b/src/tools/council-archive/create-council-finalize.test.ts
index 0db238724..9c24c0bdd 100644
--- a/src/tools/council-archive/create-council-finalize.test.ts
+++ b/src/tools/council-archive/create-council-finalize.test.ts
@@ -5,7 +5,6 @@ import { mkdtemp, mkdir, writeFile, readFile } from "node:fs/promises"
import { join } from "node:path"
import { tmpdir } from "node:os"
import { createCouncilFinalize } from "./create-council-finalize"
-import { createCouncilRead } from "./create-council-read"
import type { CouncilFinalizeResult } from "./types"
function mockTaskOutput(agent: string, responseBody: string, complete = true): string {
@@ -132,7 +131,7 @@ describe("createCouncilFinalize", () => {
})
describe("#given large response exceeding 8000 chars", () => {
- it("#then keeps full content in archive and council_read returns full response", async () => {
+ it("#then keeps full content in archive readable via readFile", async () => {
const largeResponse = "x".repeat(9000)
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_large.md"),
@@ -153,14 +152,9 @@ describe("createCouncilFinalize", () => {
expect(member).not.toHaveProperty("result")
expect(member).not.toHaveProperty("result_truncated")
- const readTool = createCouncilRead(tmpDir)
- const readResult = await readTool.execute({ file_path: member.archive_file! }, mockCtx)
- const parsed = JSON.parse(readResult)
-
- expect(parsed.has_response).toBe(true)
- expect(parsed.response_complete).toBe(true)
- expect(parsed.result).toHaveLength(9000)
- expect(parsed.result).toBe(largeResponse)
+ const archiveContent = await readFile(join(tmpDir, member.archive_file!), "utf-8")
+ expect(archiveContent).toHaveLength(9000)
+ expect(archiveContent).toBe(largeResponse)
})
})
diff --git a/src/tools/council-archive/create-council-read.test.ts b/src/tools/council-archive/create-council-read.test.ts
deleted file mode 100644
index 1a91f3127..000000000
--- a/src/tools/council-archive/create-council-read.test.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-///
-
-import { describe, expect, it, beforeEach, afterEach } from "bun:test"
-import { mkdtemp, writeFile, mkdir, rm } from "node:fs/promises"
-import { tmpdir } from "node:os"
-import { join } from "node:path"
-import { createCouncilRead } from "./create-council-read"
-
-let tempDir: string
-let sisyphusDir: string
-
-beforeEach(async () => {
- tempDir = await mkdtemp(join(tmpdir(), "council-read-test-"))
- sisyphusDir = join(tempDir, ".sisyphus")
- await mkdir(sisyphusDir, { recursive: true })
-})
-
-afterEach(async () => {
- await rm(tempDir, { recursive: true, force: true })
-})
-
-const toolContext = {
- sessionID: "test-session",
- messageID: "test-message",
- agent: "test-agent",
- abort: new AbortController().signal,
-}
-
-describe("createCouncilRead", () => {
- describe("#given an archive file with clean extracted content", () => {
- it("#then returns has_response true, response_complete true, and raw file content", async () => {
- const archivePath = join(sisyphusDir, "member-1.txt")
- await writeFile(archivePath, "Full analysis here")
-
- const tool = createCouncilRead(tempDir)
- const relativePath = `.sisyphus/member-1.txt`
-
- const result = await tool.execute({ file_path: relativePath }, toolContext)
- const parsed = JSON.parse(result)
-
- expect(parsed.has_response).toBe(true)
- expect(parsed.response_complete).toBe(true)
- expect(parsed.result).toBe("Full analysis here")
- })
- })
-
- describe("#given an archive file containing tag-like text", () => {
- it("#then returns raw content without re-parsing tags", async () => {
- const archivePath = join(sisyphusDir, "member-2.txt")
- await writeFile(archivePath, "do not parse this")
-
- const tool = createCouncilRead(tempDir)
- const relativePath = `.sisyphus/member-2.txt`
-
- const result = await tool.execute({ file_path: relativePath }, toolContext)
- const parsed = JSON.parse(result)
-
- expect(parsed.has_response).toBe(true)
- expect(parsed.response_complete).toBe(true)
- expect(parsed.result).toBe("do not parse this")
- })
- })
-
- describe("#given an archive file with empty content", () => {
- it("#then returns empty result content", async () => {
- const archivePath = join(sisyphusDir, "member-3.txt")
- await writeFile(archivePath, "")
-
- const tool = createCouncilRead(tempDir)
- const relativePath = `.sisyphus/member-3.txt`
-
- const result = await tool.execute({ file_path: relativePath }, toolContext)
- const parsed = JSON.parse(result)
-
- expect(parsed.has_response).toBe(true)
- expect(parsed.response_complete).toBe(true)
- expect(parsed.result).toBe("")
- })
- })
-
- describe("#given a path outside .sisyphus/", () => {
- it("#then returns Access denied error", async () => {
- const tool = createCouncilRead(tempDir)
- const result = await tool.execute({ file_path: "/etc/passwd" }, toolContext)
- const parsed = JSON.parse(result)
-
- expect(parsed.error).toBe("Access denied: path must be within .sisyphus/")
- })
- })
-
- describe("#given a missing file within .sisyphus/", () => {
- it("#then returns has_response false with File not found error", async () => {
- const tool = createCouncilRead(tempDir)
- const relativePath = `.sisyphus/nonexistent-file.txt`
-
- const result = await tool.execute({ file_path: relativePath }, toolContext)
- const parsed = JSON.parse(result)
-
- expect(parsed.has_response).toBe(false)
- expect(parsed.error).toContain("File not found")
- expect(parsed.error).toContain(relativePath)
- })
- })
-
- describe("#given a path traversal attempt", () => {
- it("#then returns Access denied error", async () => {
- const tool = createCouncilRead(tempDir)
- const result = await tool.execute({ file_path: "../../../etc/passwd" }, toolContext)
- const parsed = JSON.parse(result)
-
- expect(parsed.error).toBe("Access denied: path must be within .sisyphus/")
- })
- })
-
- describe("#given a Windows-style path under .sisyphus", () => {
- it("#then normalizes separators and reads the file successfully", async () => {
- const archivePath = join(sisyphusDir, "windows-path.txt")
- await writeFile(archivePath, "Windows path response")
-
- const tool = createCouncilRead(tempDir)
- const result = await tool.execute({ file_path: ".sisyphus\\windows-path.txt" }, toolContext)
- const parsed = JSON.parse(result)
-
- expect(parsed.has_response).toBe(true)
- expect(parsed.response_complete).toBe(true)
- expect(parsed.result).toBe("Windows path response")
- })
- })
-})
diff --git a/src/tools/council-archive/create-council-read.ts b/src/tools/council-archive/create-council-read.ts
deleted file mode 100644
index 9a1ce3623..000000000
--- a/src/tools/council-archive/create-council-read.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { tool, type ToolDefinition } from "@opencode-ai/plugin"
-import { readFile } from "node:fs/promises"
-import { resolve, sep } from "node:path"
-
-function normalizeInputPath(pathValue: string): string {
- return pathValue.replace(/\\/g, "/")
-}
-
-function isPathWithinDirectory(pathToCheck: string, directory: string): boolean {
- const normalizedDir = directory.endsWith(sep) ? directory : `${directory}${sep}`
- return pathToCheck === directory || pathToCheck.startsWith(normalizedDir)
-}
-
-export function createCouncilRead(basePath?: string): ToolDefinition {
- return tool({
- description:
- "Read a council archive file and return its raw member response content.",
- args: {
- file_path: tool.schema.string().describe("Path to the archive file (must be within .sisyphus/)"),
- },
- async execute(args: { file_path: string }) {
- const normalizedInputPath = normalizeInputPath(args.file_path)
- if (!normalizedInputPath.startsWith(".sisyphus/")) {
- return JSON.stringify({ error: "Access denied: path must be within .sisyphus/" }, null, 2)
- }
-
- try {
- const base = basePath ?? process.cwd()
- const absPath = resolve(base, normalizedInputPath)
- const absSisyphusRoot = resolve(base, ".sisyphus")
- if (!isPathWithinDirectory(absPath, absSisyphusRoot)) {
- return JSON.stringify({ error: "Access denied: path must be within .sisyphus/" }, null, 2)
- }
-
- const content = await readFile(absPath, "utf-8")
- return JSON.stringify({ has_response: true, response_complete: true, result: content }, null, 2)
- } catch {
- return JSON.stringify({ has_response: false, error: `File not found: ${normalizedInputPath}` }, null, 2)
- }
- },
- })
-}
diff --git a/src/tools/council-archive/index.ts b/src/tools/council-archive/index.ts
index eddde2eda..d0258cdc4 100644
--- a/src/tools/council-archive/index.ts
+++ b/src/tools/council-archive/index.ts
@@ -1,5 +1,4 @@
export { createCouncilFinalize } from "./create-council-finalize"
-export { createCouncilRead } from "./create-council-read"
export { extractCouncilResponse, OPENING_TAG, CLOSING_TAG } from "./council-response-extractor"
export type { CouncilResponseExtraction } from "./council-response-extractor"
export type { CouncilFinalizeArgs, CouncilMemberResult, CouncilFinalizeResult } from "./types"
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 6d1d6d3ab..b723a288d 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -47,7 +47,7 @@ export {
} from "./task"
export { createHashlineEditTool } from "./hashline-edit"
export { createPrepareCouncilPromptTool } from "./prepare-council-prompt"
-export { createCouncilFinalize, createCouncilRead } from "./council-archive"
+export { createCouncilFinalize } from "./council-archive"
export function createBackgroundTools(manager: BackgroundManager, client: OpencodeClient): Record {
const outputManager: BackgroundOutputManager = manager