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.
This commit is contained in:
ismeth
2026-02-28 11:53:41 +01:00
committed by YeonGyu-Kim
parent aa91c1fb19
commit f5829bf2fb
7 changed files with 27 additions and 33 deletions
+1 -1
View File
@@ -1709,7 +1709,7 @@ export class BackgroundManager {
// Write output to file if requested (before status flip) // Write output to file if requested (before status flip)
if (task.writeOutputToFile) { if (task.writeOutputToFile) {
const filePath = await writeTaskOutput(task, this.client) const filePath = await writeTaskOutput(task, this.client, this.directory)
if (filePath) task.outputFilePath = filePath if (filePath) task.outputFilePath = filePath
} }
@@ -74,7 +74,7 @@ describe("writeTaskOutput", () => {
]) ])
const task = createMockTask() 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(result).toBe(`${TEST_OUTPUT_DIR}/bg_test_123.md`)
expect(existsSync(result!)).toBe(true) expect(existsSync(result!)).toBe(true)
@@ -99,7 +99,7 @@ describe("writeTaskOutput", () => {
const client = createMockClient([]) const client = createMockClient([])
const task = createMockTask({ sessionID: undefined }) const task = createMockTask({ sessionID: undefined })
const result = await writeTaskOutput(task, client) const result = await writeTaskOutput(task, client, ".")
expect(result).toBeNull() expect(result).toBeNull()
expect(existsSync(`${TEST_OUTPUT_DIR}/bg_test_123.md`)).toBe(false) expect(existsSync(`${TEST_OUTPUT_DIR}/bg_test_123.md`)).toBe(false)
@@ -111,7 +111,7 @@ describe("writeTaskOutput", () => {
const client = createErrorClient("Session not found") const client = createErrorClient("Session not found")
const task = createMockTask() const task = createMockTask()
const result = await writeTaskOutput(task, client) const result = await writeTaskOutput(task, client, ".")
expect(result).toBeNull() expect(result).toBeNull()
expect(existsSync(`${TEST_OUTPUT_DIR}/bg_test_123.md`)).toBe(false) expect(existsSync(`${TEST_OUTPUT_DIR}/bg_test_123.md`)).toBe(false)
@@ -123,7 +123,7 @@ describe("writeTaskOutput", () => {
const client = createMockClient([]) const client = createMockClient([])
const task = createMockTask() 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(result).toBe(`${TEST_OUTPUT_DIR}/bg_test_123.md`)
expect(existsSync(result!)).toBe(true) expect(existsSync(result!)).toBe(true)
@@ -149,7 +149,7 @@ describe("writeTaskOutput", () => {
]) ])
const task = createMockTask() 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(result).toBe(`${TEST_OUTPUT_DIR}/bg_test_123.md`)
expect(existsSync(TEST_OUTPUT_DIR)).toBe(true) expect(existsSync(TEST_OUTPUT_DIR)).toBe(true)
@@ -171,7 +171,7 @@ describe("writeTaskOutput", () => {
]) ])
const task = createMockTask() const task = createMockTask()
const result = await writeTaskOutput(task, client) const result = await writeTaskOutput(task, client, ".")
const content = await readFile(result!, "utf-8") const content = await readFile(result!, "utf-8")
expect(content).toContain("Let me think about this...") expect(content).toContain("Let me think about this...")
@@ -195,7 +195,7 @@ describe("writeTaskOutput", () => {
]) ])
const task = createMockTask() const task = createMockTask()
const result = await writeTaskOutput(task, client) const result = await writeTaskOutput(task, client, ".")
const content = await readFile(result!, "utf-8") const content = await readFile(result!, "utf-8")
const firstIdx = content.indexOf("First message") const firstIdx = content.indexOf("First message")
@@ -209,7 +209,7 @@ describe("writeTaskOutput", () => {
const client = createMockClient([]) const client = createMockClient([])
const task = createMockTask({ completedAt: undefined }) const task = createMockTask({ completedAt: undefined })
const result = await writeTaskOutput(task, client) const result = await writeTaskOutput(task, client, ".")
const content = await readFile(result!, "utf-8") const content = await readFile(result!, "utf-8")
expect(content).toContain("completed_at: unknown") expect(content).toContain("completed_at: unknown")
@@ -1,5 +1,5 @@
import { mkdir, writeFile, rename } from "node:fs/promises" import { mkdir, writeFile, rename } from "node:fs/promises"
import { dirname } from "node:path" import { dirname, join } from "node:path"
import { log } from "../../shared" import { log } from "../../shared"
import type { BackgroundTask } from "./types" import type { BackgroundTask } from "./types"
import type { BackgroundOutputClient } from "../../tools/background-task/clients" import type { BackgroundOutputClient } from "../../tools/background-task/clients"
@@ -53,6 +53,7 @@ function formatTranscript(
export async function writeTaskOutput( export async function writeTaskOutput(
task: BackgroundTask, task: BackgroundTask,
client: BackgroundOutputClient, client: BackgroundOutputClient,
directory: string,
): Promise<string | null> { ): Promise<string | null> {
if (!task.sessionID) { if (!task.sessionID) {
return null return null
@@ -74,7 +75,7 @@ export async function writeTaskOutput(
const transcript = formatTranscript(messages) const transcript = formatTranscript(messages)
const content = `${frontmatter}\n\n${transcript}` 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` const tmpPath = `${outputPath}.tmp`
await mkdir(dirname(outputPath), { recursive: true }) await mkdir(dirname(outputPath), { recursive: true })
+1 -1
View File
@@ -282,7 +282,7 @@ export function createToolRegistry(args: {
...hashlineToolsRecord, ...hashlineToolsRecord,
prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory), prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory),
council_finalize: createCouncilFinalize(ctx.directory), council_finalize: createCouncilFinalize(ctx.directory),
council_read: createCouncilRead(), council_read: createCouncilRead(ctx.directory),
} }
for (const toolDefinition of Object.values(allTools)) { for (const toolDefinition of Object.values(allTools)) {
@@ -75,16 +75,13 @@ function createMockManager(tasks: Record<string, Partial<BackgroundTask>>): Back
} }
let tmpDir: string let tmpDir: string
let originalCwd: string
beforeEach(async () => { beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "council-flow-")) tmpDir = await mkdtemp(join(tmpdir(), "council-flow-"))
await mkdir(join(tmpDir, ".sisyphus", "task-outputs"), { recursive: true }) await mkdir(join(tmpDir, ".sisyphus", "task-outputs"), { recursive: true })
originalCwd = process.cwd()
}) })
afterEach(async () => { afterEach(async () => {
process.chdir(originalCwd)
await rm(tmpDir, { recursive: true, force: true }) await rm(tmpDir, { recursive: true, force: true })
}) })
@@ -142,8 +139,7 @@ describe("council archive integration flow", () => {
expect(archiveContent).toBe(agents[i].response) expect(archiveContent).toBe(agents[i].response)
} }
process.chdir(tmpDir) const readTool = createCouncilRead(tmpDir)
const readTool = createCouncilRead()
for (let i = 0; i < agents.length; i++) { for (let i = 0; i < agents.length; i++) {
const taskOutputPath = join(".sisyphus", "task-outputs", `${agents[i].id}.md`) 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") const archiveContent = await readFile(join(tmpDir, member.archive_file!), "utf-8")
expect(archiveContent).toBe("Analysis still in progress...") expect(archiveContent).toBe("Analysis still in progress...")
process.chdir(tmpDir) const readTool = createCouncilRead(tmpDir)
const readTool = createCouncilRead()
const taskOutputPath = join(".sisyphus", "task-outputs", `${taskId}.md`) const taskOutputPath = join(".sisyphus", "task-outputs", `${taskId}.md`)
const readResult = await readTool.execute({ file_path: taskOutputPath }, toolContext) const readResult = await readTool.execute({ file_path: taskOutputPath }, toolContext)
const parsed = JSON.parse(readResult) 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") const fullContent = await readFile(join(tmpDir, member.archive_file!), "utf-8")
expect(fullContent).toHaveLength(9000) expect(fullContent).toHaveLength(9000)
process.chdir(tmpDir) const readTool = createCouncilRead(tmpDir)
const readTool = createCouncilRead()
const taskOutputPath = join(".sisyphus", "task-outputs", `${taskId}.md`) const taskOutputPath = join(".sisyphus", "task-outputs", `${taskId}.md`)
const readResult = await readTool.execute({ file_path: taskOutputPath }, toolContext) const readResult = await readTool.execute({ file_path: taskOutputPath }, toolContext)
const parsed = JSON.parse(readResult) const parsed = JSON.parse(readResult)
@@ -32,10 +32,9 @@ describe("createCouncilRead", () => {
const archivePath = join(sisyphusDir, "member-1.txt") const archivePath = join(sisyphusDir, "member-1.txt")
await writeFile(archivePath, "Some preamble\n<COUNCIL_MEMBER_RESPONSE>Full analysis here</COUNCIL_MEMBER_RESPONSE>") await writeFile(archivePath, "Some preamble\n<COUNCIL_MEMBER_RESPONSE>Full analysis here</COUNCIL_MEMBER_RESPONSE>")
const tool = createCouncilRead() const tool = createCouncilRead(tempDir)
const relativePath = `.sisyphus/member-1.txt` const relativePath = `.sisyphus/member-1.txt`
process.chdir(tempDir)
const result = await tool.execute({ file_path: relativePath }, toolContext) const result = await tool.execute({ file_path: relativePath }, toolContext)
const parsed = JSON.parse(result) const parsed = JSON.parse(result)
@@ -50,10 +49,9 @@ describe("createCouncilRead", () => {
const archivePath = join(sisyphusDir, "member-2.txt") const archivePath = join(sisyphusDir, "member-2.txt")
await writeFile(archivePath, "<COUNCIL_MEMBER_RESPONSE>Partial analysis still writing...") await writeFile(archivePath, "<COUNCIL_MEMBER_RESPONSE>Partial analysis still writing...")
const tool = createCouncilRead() const tool = createCouncilRead(tempDir)
const relativePath = `.sisyphus/member-2.txt` const relativePath = `.sisyphus/member-2.txt`
process.chdir(tempDir)
const result = await tool.execute({ file_path: relativePath }, toolContext) const result = await tool.execute({ file_path: relativePath }, toolContext)
const parsed = JSON.parse(result) const parsed = JSON.parse(result)
@@ -67,10 +65,9 @@ describe("createCouncilRead", () => {
const archivePath = join(sisyphusDir, "member-3.txt") const archivePath = join(sisyphusDir, "member-3.txt")
await writeFile(archivePath, "Just some plain text without any tags.") await writeFile(archivePath, "Just some plain text without any tags.")
const tool = createCouncilRead() const tool = createCouncilRead(tempDir)
const relativePath = `.sisyphus/member-3.txt` const relativePath = `.sisyphus/member-3.txt`
process.chdir(tempDir)
const result = await tool.execute({ file_path: relativePath }, toolContext) const result = await tool.execute({ file_path: relativePath }, toolContext)
const parsed = JSON.parse(result) const parsed = JSON.parse(result)
@@ -80,7 +77,7 @@ describe("createCouncilRead", () => {
describe("#given a path outside .sisyphus/", () => { describe("#given a path outside .sisyphus/", () => {
it("#then returns Access denied error", async () => { 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 result = await tool.execute({ file_path: "/etc/passwd" }, toolContext)
const parsed = JSON.parse(result) const parsed = JSON.parse(result)
@@ -90,10 +87,9 @@ describe("createCouncilRead", () => {
describe("#given a missing file within .sisyphus/", () => { describe("#given a missing file within .sisyphus/", () => {
it("#then returns has_response false with File not found error", async () => { 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` const relativePath = `.sisyphus/nonexistent-file.txt`
process.chdir(tempDir)
const result = await tool.execute({ file_path: relativePath }, toolContext) const result = await tool.execute({ file_path: relativePath }, toolContext)
const parsed = JSON.parse(result) const parsed = JSON.parse(result)
@@ -105,7 +101,7 @@ describe("createCouncilRead", () => {
describe("#given a path traversal attempt", () => { describe("#given a path traversal attempt", () => {
it("#then returns Access denied error", async () => { 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 result = await tool.execute({ file_path: "../../../etc/passwd" }, toolContext)
const parsed = JSON.parse(result) const parsed = JSON.parse(result)
@@ -1,8 +1,9 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin" import { tool, type ToolDefinition } from "@opencode-ai/plugin"
import { readFile } from "node:fs/promises" import { readFile } from "node:fs/promises"
import { join } from "node:path"
import { extractCouncilResponse } from "./council-response-extractor" import { extractCouncilResponse } from "./council-response-extractor"
export function createCouncilRead(): ToolDefinition { export function createCouncilRead(basePath?: string): ToolDefinition {
return tool({ return tool({
description: 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.", "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 { 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) const extraction = extractCouncilResponse(content)
return JSON.stringify(extraction, null, 2) return JSON.stringify(extraction, null, 2)
} catch { } catch {