From f5829bf2fb75b7cb98fc721685b1347b40213735 Mon Sep 17 00:00:00 2001 From: ismeth Date: Sat, 28 Feb 2026 11:53:41 +0100 Subject: [PATCH] fix(athena): resolve task output files to project directory instead of process.cwd() task-output-writer wrote to process.cwd()/.sisyphus/task-outputs/ while council_finalize read from ctx.directory/.sisyphus/task-outputs/. When these differ, all output files appear missing. Fix by passing directory to writeTaskOutput from BackgroundManager, and adding basePath to council_read. Also removes process.chdir() from tests that was masking the bug. --- src/features/background-agent/manager.ts | 2 +- .../background-agent/task-output-writer.test.ts | 16 ++++++++-------- .../background-agent/task-output-writer.ts | 5 +++-- src/plugin/tool-registry.ts | 2 +- .../council-flow.integration.test.ts | 12 +++--------- .../council-archive/create-council-read.test.ts | 16 ++++++---------- src/tools/council-archive/create-council-read.ts | 7 +++++-- 7 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 49e20dc78..1dc62b6df 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -1709,7 +1709,7 @@ export class BackgroundManager { // Write output to file if requested (before status flip) if (task.writeOutputToFile) { - const filePath = await writeTaskOutput(task, this.client) + const filePath = await writeTaskOutput(task, this.client, this.directory) if (filePath) task.outputFilePath = filePath } diff --git a/src/features/background-agent/task-output-writer.test.ts b/src/features/background-agent/task-output-writer.test.ts index 213ccbdaf..0e06c6d88 100644 --- a/src/features/background-agent/task-output-writer.test.ts +++ b/src/features/background-agent/task-output-writer.test.ts @@ -74,7 +74,7 @@ describe("writeTaskOutput", () => { ]) const task = createMockTask() - const result = await writeTaskOutput(task, client) + const result = await writeTaskOutput(task, client, ".") expect(result).toBe(`${TEST_OUTPUT_DIR}/bg_test_123.md`) expect(existsSync(result!)).toBe(true) @@ -99,7 +99,7 @@ describe("writeTaskOutput", () => { const client = createMockClient([]) const task = createMockTask({ sessionID: undefined }) - const result = await writeTaskOutput(task, client) + const result = await writeTaskOutput(task, client, ".") expect(result).toBeNull() expect(existsSync(`${TEST_OUTPUT_DIR}/bg_test_123.md`)).toBe(false) @@ -111,7 +111,7 @@ describe("writeTaskOutput", () => { const client = createErrorClient("Session not found") const task = createMockTask() - const result = await writeTaskOutput(task, client) + const result = await writeTaskOutput(task, client, ".") expect(result).toBeNull() expect(existsSync(`${TEST_OUTPUT_DIR}/bg_test_123.md`)).toBe(false) @@ -123,7 +123,7 @@ describe("writeTaskOutput", () => { const client = createMockClient([]) const task = createMockTask() - const result = await writeTaskOutput(task, client) + const result = await writeTaskOutput(task, client, ".") expect(result).toBe(`${TEST_OUTPUT_DIR}/bg_test_123.md`) expect(existsSync(result!)).toBe(true) @@ -149,7 +149,7 @@ describe("writeTaskOutput", () => { ]) const task = createMockTask() - const result = await writeTaskOutput(task, client) + const result = await writeTaskOutput(task, client, ".") expect(result).toBe(`${TEST_OUTPUT_DIR}/bg_test_123.md`) expect(existsSync(TEST_OUTPUT_DIR)).toBe(true) @@ -171,7 +171,7 @@ describe("writeTaskOutput", () => { ]) const task = createMockTask() - const result = await writeTaskOutput(task, client) + const result = await writeTaskOutput(task, client, ".") const content = await readFile(result!, "utf-8") expect(content).toContain("Let me think about this...") @@ -195,7 +195,7 @@ describe("writeTaskOutput", () => { ]) const task = createMockTask() - const result = await writeTaskOutput(task, client) + const result = await writeTaskOutput(task, client, ".") const content = await readFile(result!, "utf-8") const firstIdx = content.indexOf("First message") @@ -209,7 +209,7 @@ describe("writeTaskOutput", () => { const client = createMockClient([]) const task = createMockTask({ completedAt: undefined }) - const result = await writeTaskOutput(task, client) + const result = await writeTaskOutput(task, client, ".") const content = await readFile(result!, "utf-8") expect(content).toContain("completed_at: unknown") diff --git a/src/features/background-agent/task-output-writer.ts b/src/features/background-agent/task-output-writer.ts index b6552c3b9..1e708ca5e 100644 --- a/src/features/background-agent/task-output-writer.ts +++ b/src/features/background-agent/task-output-writer.ts @@ -1,5 +1,5 @@ import { mkdir, writeFile, rename } from "node:fs/promises" -import { dirname } from "node:path" +import { dirname, join } from "node:path" import { log } from "../../shared" import type { BackgroundTask } from "./types" import type { BackgroundOutputClient } from "../../tools/background-task/clients" @@ -53,6 +53,7 @@ function formatTranscript( export async function writeTaskOutput( task: BackgroundTask, client: BackgroundOutputClient, + directory: string, ): Promise { if (!task.sessionID) { return null @@ -74,7 +75,7 @@ export async function writeTaskOutput( const transcript = formatTranscript(messages) const content = `${frontmatter}\n\n${transcript}` - const outputPath = `${OUTPUT_DIR}/${task.id}.md` + const outputPath = join(directory, OUTPUT_DIR, `${task.id}.md`) const tmpPath = `${outputPath}.tmp` await mkdir(dirname(outputPath), { recursive: true }) diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index 114f3ea1f..aa75c22db 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -282,7 +282,7 @@ export function createToolRegistry(args: { ...hashlineToolsRecord, prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory), council_finalize: createCouncilFinalize(ctx.directory), - council_read: createCouncilRead(), + 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 26e072965..a60b69041 100644 --- a/src/tools/council-archive/council-flow.integration.test.ts +++ b/src/tools/council-archive/council-flow.integration.test.ts @@ -75,16 +75,13 @@ function createMockManager(tasks: Record>): Back } 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 }) }) @@ -142,8 +139,7 @@ describe("council archive integration flow", () => { expect(archiveContent).toBe(agents[i].response) } - process.chdir(tmpDir) - const readTool = createCouncilRead() + const readTool = createCouncilRead(tmpDir) for (let i = 0; i < agents.length; i++) { const taskOutputPath = join(".sisyphus", "task-outputs", `${agents[i].id}.md`) @@ -183,8 +179,7 @@ describe("council archive integration flow", () => { 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 readTool = createCouncilRead(tmpDir) const taskOutputPath = join(".sisyphus", "task-outputs", `${taskId}.md`) const readResult = await readTool.execute({ file_path: taskOutputPath }, toolContext) const parsed = JSON.parse(readResult) @@ -284,8 +279,7 @@ describe("council archive integration flow", () => { const fullContent = await readFile(join(tmpDir, member.archive_file!), "utf-8") expect(fullContent).toHaveLength(9000) - process.chdir(tmpDir) - const readTool = createCouncilRead() + const readTool = createCouncilRead(tmpDir) const taskOutputPath = join(".sisyphus", "task-outputs", `${taskId}.md`) const readResult = await readTool.execute({ file_path: taskOutputPath }, toolContext) const parsed = JSON.parse(readResult) diff --git a/src/tools/council-archive/create-council-read.test.ts b/src/tools/council-archive/create-council-read.test.ts index a18b6c82c..0de597f2e 100644 --- a/src/tools/council-archive/create-council-read.test.ts +++ b/src/tools/council-archive/create-council-read.test.ts @@ -32,10 +32,9 @@ describe("createCouncilRead", () => { const archivePath = join(sisyphusDir, "member-1.txt") await writeFile(archivePath, "Some preamble\nFull analysis here") - const tool = createCouncilRead() + const tool = createCouncilRead(tempDir) const relativePath = `.sisyphus/member-1.txt` - process.chdir(tempDir) const result = await tool.execute({ file_path: relativePath }, toolContext) const parsed = JSON.parse(result) @@ -50,10 +49,9 @@ describe("createCouncilRead", () => { const archivePath = join(sisyphusDir, "member-2.txt") await writeFile(archivePath, "Partial analysis still writing...") - const tool = createCouncilRead() + const tool = createCouncilRead(tempDir) const relativePath = `.sisyphus/member-2.txt` - process.chdir(tempDir) const result = await tool.execute({ file_path: relativePath }, toolContext) const parsed = JSON.parse(result) @@ -67,10 +65,9 @@ describe("createCouncilRead", () => { const archivePath = join(sisyphusDir, "member-3.txt") await writeFile(archivePath, "Just some plain text without any tags.") - const tool = createCouncilRead() + const tool = createCouncilRead(tempDir) const relativePath = `.sisyphus/member-3.txt` - process.chdir(tempDir) const result = await tool.execute({ file_path: relativePath }, toolContext) const parsed = JSON.parse(result) @@ -80,7 +77,7 @@ describe("createCouncilRead", () => { describe("#given a path outside .sisyphus/", () => { it("#then returns Access denied error", async () => { - const tool = createCouncilRead() + const tool = createCouncilRead(tempDir) const result = await tool.execute({ file_path: "/etc/passwd" }, toolContext) const parsed = JSON.parse(result) @@ -90,10 +87,9 @@ describe("createCouncilRead", () => { describe("#given a missing file within .sisyphus/", () => { it("#then returns has_response false with File not found error", async () => { - const tool = createCouncilRead() + const tool = createCouncilRead(tempDir) const relativePath = `.sisyphus/nonexistent-file.txt` - process.chdir(tempDir) const result = await tool.execute({ file_path: relativePath }, toolContext) const parsed = JSON.parse(result) @@ -105,7 +101,7 @@ describe("createCouncilRead", () => { describe("#given a path traversal attempt", () => { it("#then returns Access denied error", async () => { - const tool = createCouncilRead() + const tool = createCouncilRead(tempDir) const result = await tool.execute({ file_path: "../../../etc/passwd" }, toolContext) const parsed = JSON.parse(result) diff --git a/src/tools/council-archive/create-council-read.ts b/src/tools/council-archive/create-council-read.ts index 0235a469d..dfa5f318e 100644 --- a/src/tools/council-archive/create-council-read.ts +++ b/src/tools/council-archive/create-council-read.ts @@ -1,8 +1,9 @@ import { tool, type ToolDefinition } from "@opencode-ai/plugin" import { readFile } from "node:fs/promises" +import { join } from "node:path" import { extractCouncilResponse } from "./council-response-extractor" -export function createCouncilRead(): ToolDefinition { +export function createCouncilRead(basePath?: string): ToolDefinition { return tool({ description: "Read a council archive file and extract the council member response. Use this to access full results for truncated members or for follow-up/cross-check analysis.", @@ -15,7 +16,9 @@ export function createCouncilRead(): ToolDefinition { } try { - const content = await readFile(args.file_path, "utf-8") + const base = basePath ?? process.cwd() + const absPath = join(base, args.file_path) + const content = await readFile(absPath, "utf-8") const extraction = extractCouncilResponse(content) return JSON.stringify(extraction, null, 2) } catch {