diff --git a/src/tools/council-archive/council-flow.integration.test.ts b/src/tools/council-archive/council-flow.integration.test.ts
new file mode 100644
index 000000000..26e072965
--- /dev/null
+++ b/src/tools/council-archive/council-flow.integration.test.ts
@@ -0,0 +1,330 @@
+///
+
+import { describe, expect, it, beforeEach, afterEach } from "bun:test"
+import { mkdtemp, mkdir, writeFile, readFile, rm } 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"
+import { createBackgroundWait } from "../background-task/create-background-wait"
+import { resetMessageCursor } from "../../shared/session-cursor"
+
+function mockTaskOutput(agent: string, responseBody: string, complete = true): string {
+ const closing = complete ? "\n" : ""
+ return [
+ "---",
+ "task_id: bg_test",
+ `agent: ${agent}`,
+ "session_id: ses_test",
+ "parent_session_id: ses_parent",
+ "status: completed",
+ "completed_at: 2026-02-27T14:00:00.000Z",
+ "---",
+ "",
+ "[assistant] 14:00:00",
+ "",
+ responseBody,
+ closing,
+ ].join("\n")
+}
+
+function mockTaskOutputNoTags(agent: string, body: string): string {
+ return [
+ "---",
+ "task_id: bg_test",
+ `agent: ${agent}`,
+ "session_id: ses_test",
+ "parent_session_id: ses_parent",
+ "status: completed",
+ "completed_at: 2026-02-27T14:00:00.000Z",
+ "---",
+ "",
+ "[assistant] 14:00:00",
+ body,
+ ].join("\n")
+}
+
+const toolContext = {
+ sessionID: "test-session",
+ messageID: "test-message",
+ agent: "test-agent",
+ abort: new AbortController().signal,
+}
+
+function createMockManager(tasks: Record>): BackgroundOutputManager {
+ return {
+ getTask: (id: string) => {
+ const partial = tasks[id]
+ if (!partial) return undefined
+ return {
+ id,
+ parentSessionID: "parent",
+ parentMessageID: "parent-msg",
+ description: partial.description ?? `Task ${id}`,
+ prompt: "test",
+ agent: partial.agent ?? "test-agent",
+ status: partial.status ?? "running",
+ sessionID: partial.sessionID ?? `session-${id}`,
+ ...partial,
+ } as BackgroundTask
+ },
+ }
+}
+
+let tmpDir: string
+let originalCwd: string
+
+beforeEach(async () => {
+ tmpDir = await mkdtemp(join(tmpdir(), "council-flow-"))
+ await mkdir(join(tmpDir, ".sisyphus", "task-outputs"), { recursive: true })
+ originalCwd = process.cwd()
+})
+
+afterEach(async () => {
+ process.chdir(originalCwd)
+ await rm(tmpDir, { recursive: true, force: true })
+})
+
+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 task outputs", 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" },
+ { id: "bg_gemini", agent: "Council: Gemini", response: "Gemini creative alternative approach" },
+ ]
+
+ for (const a of agents) {
+ await writeFile(
+ join(tmpDir, ".sisyphus", "task-outputs", `${a.id}.md`),
+ mockTaskOutput(a.agent, a.response),
+ "utf-8",
+ )
+ }
+
+ const finalizeTool = createCouncilFinalize(tmpDir)
+ const resultStr = await finalizeTool.execute(
+ { task_ids: agents.map((a) => a.id), name: "test" },
+ toolContext,
+ )
+ const result: CouncilFinalizeResult = JSON.parse(resultStr)
+
+ expect(result.archive_dir).toMatch(/\.sisyphus\/athena\/council-test-[a-f0-9]{4}$/)
+ expect(result.meta_file).toMatch(/meta\.yaml$/)
+ expect(result.members).toHaveLength(3)
+
+ for (let i = 0; i < agents.length; i++) {
+ const member = result.members[i]
+ expect(member.task_id).toBe(agents[i].id)
+ expect(member.has_response).toBe(true)
+ expect(member.response_complete).toBe(true)
+ expect(member.result).toBe(agents[i].response)
+ expect(member.result_truncated).toBeUndefined()
+ expect(member.error).toBeUndefined()
+ expect(member.archive_file).toBeDefined()
+ }
+
+ const metaContent = await readFile(join(tmpDir, result.meta_file), "utf-8")
+ expect(metaContent).toContain("archive_name: council-test-")
+ expect(metaContent).toContain("created_at:")
+ expect(metaContent).toContain('member: "Council: Claude Opus"')
+ expect(metaContent).toContain('member: "Council: GPT-5"')
+ expect(metaContent).toContain('member: "Council: Gemini"')
+ expect(metaContent).toContain("has_response: true")
+ expect(metaContent).toContain("response_complete: true")
+
+ for (let i = 0; i < agents.length; i++) {
+ const archiveContent = await readFile(join(tmpDir, result.members[i].archive_file!), "utf-8")
+ expect(archiveContent).toBe(agents[i].response)
+ }
+
+ process.chdir(tmpDir)
+ const readTool = createCouncilRead()
+
+ for (let i = 0; i < agents.length; i++) {
+ const taskOutputPath = join(".sisyphus", "task-outputs", `${agents[i].id}.md`)
+ const readResult = await readTool.execute({ file_path: taskOutputPath }, 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 has_response is true and response_complete is false in both finalize and read", async () => {
+ const taskId = "bg_partial"
+ await writeFile(
+ join(tmpDir, ".sisyphus", "task-outputs", `${taskId}.md`),
+ mockTaskOutput("Council: Partial Agent", "Analysis still in progress...", false),
+ "utf-8",
+ )
+
+ const finalizeTool = createCouncilFinalize(tmpDir)
+ const resultStr = await finalizeTool.execute(
+ { task_ids: [taskId], name: "partial" },
+ toolContext,
+ )
+ const result: CouncilFinalizeResult = JSON.parse(resultStr)
+
+ const member = result.members[0]
+ expect(member.has_response).toBe(true)
+ expect(member.response_complete).toBe(false)
+ expect(member.archive_file).toBeDefined()
+
+ const archiveContent = await readFile(join(tmpDir, member.archive_file!), "utf-8")
+ expect(archiveContent).toBe("Analysis still in progress...")
+
+ process.chdir(tmpDir)
+ const readTool = createCouncilRead()
+ const taskOutputPath = join(".sisyphus", "task-outputs", `${taskId}.md`)
+ const readResult = await readTool.execute({ file_path: taskOutputPath }, toolContext)
+ const parsed = JSON.parse(readResult)
+
+ expect(parsed.has_response).toBe(true)
+ expect(parsed.response_complete).toBe(false)
+ expect(parsed.result).toBe("Analysis still in progress...")
+ })
+ })
+ })
+
+ describe("#given a member with no COUNCIL_MEMBER_RESPONSE tags at all", () => {
+ describe("#when finalize is called", () => {
+ it("#then has_response is false and no archive file is created", async () => {
+ await writeFile(
+ join(tmpDir, ".sisyphus", "task-outputs", "bg_notags.md"),
+ mockTaskOutputNoTags("Council: Plain Agent", "Just some plain text with no special tags."),
+ "utf-8",
+ )
+
+ const finalizeTool = createCouncilFinalize(tmpDir)
+ const resultStr = await finalizeTool.execute(
+ { task_ids: ["bg_notags"], name: "notags" },
+ toolContext,
+ )
+ const result: CouncilFinalizeResult = JSON.parse(resultStr)
+
+ const member = result.members[0]
+ expect(member.has_response).toBe(false)
+ expect(member.archive_file).toBeUndefined()
+ expect(member.result).toBeUndefined()
+ })
+ })
+ })
+
+ describe("#given 1 of 3 task output files is missing", () => {
+ describe("#when finalize is called with all 3 task_ids", () => {
+ it("#then 2 members succeed and 1 has error 'Task output file not found'", async () => {
+ await writeFile(
+ join(tmpDir, ".sisyphus", "task-outputs", "bg_first.md"),
+ mockTaskOutput("Council: First", "First analysis"),
+ "utf-8",
+ )
+ await writeFile(
+ join(tmpDir, ".sisyphus", "task-outputs", "bg_third.md"),
+ mockTaskOutput("Council: Third", "Third analysis"),
+ "utf-8",
+ )
+
+ const finalizeTool = createCouncilFinalize(tmpDir)
+ const resultStr = await finalizeTool.execute(
+ { task_ids: ["bg_first", "bg_missing", "bg_third"], name: "partial" },
+ toolContext,
+ )
+ const result: CouncilFinalizeResult = JSON.parse(resultStr)
+
+ expect(result.members).toHaveLength(3)
+
+ expect(result.members[0].has_response).toBe(true)
+ expect(result.members[0].result).toBe("First analysis")
+
+ expect(result.members[1].has_response).toBe(false)
+ expect(result.members[1].error).toBe("Task output file not found")
+ expect(result.members[1].member).toBe("unknown")
+
+ expect(result.members[2].has_response).toBe(true)
+ expect(result.members[2].result).toBe("Third analysis")
+ })
+ })
+ })
+
+ describe("#given a very large council response exceeding 8000 chars", () => {
+ describe("#when finalize is called and then archive is read", () => {
+ it("#then finalize truncates to 500 char preview but full content is available via council_read", async () => {
+ const largeResponse = "A".repeat(9000)
+ const taskId = "bg_large"
+ await writeFile(
+ join(tmpDir, ".sisyphus", "task-outputs", `${taskId}.md`),
+ mockTaskOutput("Council: Large Agent", largeResponse),
+ "utf-8",
+ )
+
+ const finalizeTool = createCouncilFinalize(tmpDir)
+ const resultStr = await finalizeTool.execute(
+ { task_ids: [taskId], name: "large" },
+ toolContext,
+ )
+ const result: CouncilFinalizeResult = JSON.parse(resultStr)
+
+ const member = result.members[0]
+ expect(member.has_response).toBe(true)
+ expect(member.result_truncated).toBe(true)
+ expect(member.result).toHaveLength(500)
+ expect(member.result).toBe("A".repeat(500))
+ expect(member.archive_file).toBeDefined()
+
+ const fullContent = await readFile(join(tmpDir, member.archive_file!), "utf-8")
+ expect(fullContent).toHaveLength(9000)
+
+ process.chdir(tmpDir)
+ const readTool = createCouncilRead()
+ const taskOutputPath = join(".sisyphus", "task-outputs", `${taskId}.md`)
+ const readResult = await readTool.execute({ file_path: taskOutputPath }, toolContext)
+ const parsed = JSON.parse(readResult)
+
+ expect(parsed.has_response).toBe(true)
+ expect(parsed.response_complete).toBe(true)
+ expect(parsed.result).toHaveLength(9000)
+ })
+ })
+ })
+
+ describe("#given a completed council task in background_wait", () => {
+ describe("#when background_wait returns for that task", () => {
+ it("#then completed_tasks entry has no result payload", async () => {
+ resetMessageCursor()
+
+ const manager = createMockManager({
+ "council-1": {
+ status: "completed",
+ agent: "Council: Claude Opus",
+ description: "Council analysis",
+ startedAt: new Date(Date.now() - 5000),
+ completedAt: new Date(),
+ },
+ })
+ const waitTool = createBackgroundWait(manager)
+
+ const result = await waitTool.execute({ task_ids: ["council-1"] }, toolContext)
+ const parsed = JSON.parse(result)
+
+ expect(parsed.completed_tasks).toBeArray()
+ expect(parsed.completed_tasks).toHaveLength(1)
+ expect(parsed.completed_tasks[0].task_id).toBe("council-1")
+ expect(parsed.completed_tasks[0].status).toBe("completed")
+ expect(parsed.completed_tasks[0].description).toBe("Council analysis")
+ expect(parsed.completed_tasks[0].result).toBeUndefined()
+ expect(parsed.timeout).toBe(false)
+ expect(parsed.aborted).toBe(false)
+ })
+ })
+ })
+})