fix(athena): address 11 audit findings (H2,H3,H5,M1-M4,M8-M11)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
ismeth
2026-03-01 16:48:12 +01:00
committed by YeonGyu-Kim
parent a13cc7b877
commit 7d2749cfe1
21 changed files with 610 additions and 213 deletions
@@ -0,0 +1,54 @@
import { readFile, writeFile, rename } from "node:fs/promises"
import { join, isAbsolute, resolve, relative } from "node:path"
import { log } from "../../shared/logger"
export const TASK_ID_PATTERN = /^[a-zA-Z0-9_-]+$/
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
}
export function toPosixPath(pathValue: string): string {
return pathValue.replace(/\\/g, "/")
}
export function extractAgentFromFrontmatter(content: string): string | null {
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
if (!fmMatch) return null
const agentLine = fmMatch[1].match(/^agent:\s*(.+)$/m)
return agentLine ? agentLine[1].trim() : null
}
export function isPathEscaping(expectedRoot: string, targetPath: string): boolean {
const rel = relative(expectedRoot, targetPath)
return rel.startsWith("..") || isAbsolute(rel)
}
export async function movePromptFile(
promptFilePath: string,
base: string,
absArchiveDir: string,
relArchiveDir: string,
): Promise<string | undefined> {
try {
const promptFilename = "council-prompt.md"
const absPromptSrc = isAbsolute(promptFilePath) ? promptFilePath : resolve(base, promptFilePath)
const expectedPromptRoot = join(base, ".sisyphus", "tmp")
if (isPathEscaping(expectedPromptRoot, absPromptSrc)) {
log("[council-finalize] Rejected prompt_file outside .sisyphus/tmp/", { promptFile: promptFilePath })
return undefined
}
const absPromptDest = join(absArchiveDir, promptFilename)
await rename(absPromptSrc, absPromptDest).catch(async () => {
const content = await readFile(absPromptSrc, "utf-8")
await writeFile(absPromptDest, content, "utf-8")
})
return toPosixPath(join(relArchiveDir, promptFilename))
} catch (err) {
log("[council-finalize] Failed to move prompt file", { promptFile: promptFilePath, error: String(err) })
return undefined
}
}
@@ -89,9 +89,9 @@ describe("council archive integration flow", () => {
describe("#when finalize is called and then each archive is read", () => {
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" },
{ id: "bg_gemini", agent: "Council: Gemini", response: "Gemini creative alternative approach" },
{ id: "bg_opus", agent: "Council: Claude Opus", response: "Opus deep analysis of architecture: This is a detailed analysis from Opus model covering the full scope of the council question." },
{ id: "bg_gpt", agent: "Council: GPT-5", response: "GPT pragmatic code review: This is a detailed analysis from GPT model covering the full scope of the council question." },
{ id: "bg_gemini", agent: "Council: Gemini", response: "Gemini creative alternative approach: This is a detailed analysis from Gemini model covering the full scope of the council question." },
]
for (const a of agents) {
@@ -104,7 +104,7 @@ describe("council archive integration flow", () => {
const finalizeTool = createCouncilFinalize(tmpDir)
const resultStr = await finalizeTool.execute(
{ task_ids: agents.map((a) => a.id), name: "test" },
{ task_ids: agents.map((a) => a.id), name: "test", intent: "FREEFORM" },
toolContext,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -152,7 +152,7 @@ describe("council archive integration flow", () => {
const finalizeTool = createCouncilFinalize(tmpDir)
const resultStr = await finalizeTool.execute(
{ task_ids: [taskId], name: "partial" },
{ task_ids: [taskId], name: "partial", intent: "FREEFORM" },
toolContext,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -180,7 +180,7 @@ describe("council archive integration flow", () => {
const finalizeTool = createCouncilFinalize(tmpDir)
const resultStr = await finalizeTool.execute(
{ task_ids: ["bg_notags"], name: "notags" },
{ task_ids: ["bg_notags"], name: "notags", intent: "FREEFORM" },
toolContext,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -197,18 +197,18 @@ describe("council archive integration flow", () => {
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"),
mockTaskOutput("Council: First", "First analysis: This is a detailed analysis from First model covering the full scope of the council question."),
"utf-8",
)
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_third.md"),
mockTaskOutput("Council: Third", "Third analysis"),
mockTaskOutput("Council: Third", "Third analysis: This is a detailed analysis from Third model covering the full scope of the council question."),
"utf-8",
)
const finalizeTool = createCouncilFinalize(tmpDir)
const resultStr = await finalizeTool.execute(
{ task_ids: ["bg_first", "bg_missing", "bg_third"], name: "partial" },
{ task_ids: ["bg_first", "bg_missing", "bg_third"], name: "partial", intent: "FREEFORM" },
toolContext,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -241,7 +241,7 @@ describe("council archive integration flow", () => {
const finalizeTool = createCouncilFinalize(tmpDir)
const resultStr = await finalizeTool.execute(
{ task_ids: [taskId], name: "large" },
{ task_ids: [taskId], name: "large", intent: "FREEFORM" },
toolContext,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -263,7 +263,7 @@ describe("council archive integration flow", () => {
const taskId = "bg_with_meta"
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", `${taskId}.md`),
mockTaskOutput("Council: Opus", "Analysis result"),
mockTaskOutput("Council: Opus", "Analysis result: This is a detailed analysis from Opus model covering the full scope of the council question."),
"utf-8",
)
@@ -277,6 +277,7 @@ describe("council archive integration flow", () => {
{
task_ids: [taskId],
name: "meta-test",
intent: "FREEFORM",
question: "What is the best architecture for this app?",
prompt_file: promptFile,
},
@@ -304,7 +305,7 @@ describe("council archive integration flow", () => {
const taskId = "bg_question_only"
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", `${taskId}.md`),
mockTaskOutput("Council: GPT", "GPT analysis"),
mockTaskOutput("Council: GPT", "GPT analysis: This is a detailed analysis from GPT model covering the full scope of the council question."),
"utf-8",
)
@@ -313,6 +314,7 @@ describe("council archive integration flow", () => {
{
task_ids: [taskId],
name: "question-only",
intent: "FREEFORM",
question: "How should we handle auth?",
},
toolContext,
@@ -4,12 +4,12 @@ import { extractCouncilResponse } from "./council-response-extractor"
describe("extractCouncilResponse", () => {
describe("#given complete COUNCIL_MEMBER_RESPONSE tags", () => {
it("#then returns has_response true, response_complete true, and the content", () => {
const result = extractCouncilResponse("<COUNCIL_MEMBER_RESPONSE>analysis here</COUNCIL_MEMBER_RESPONSE>")
const result = extractCouncilResponse("<COUNCIL_MEMBER_RESPONSE>" + "a".repeat(100) + "</COUNCIL_MEMBER_RESPONSE>")
expect(result).toEqual({
has_response: true,
response_complete: true,
result: "analysis here",
result: "a".repeat(100),
})
})
})
@@ -39,11 +39,11 @@ describe("extractCouncilResponse", () => {
})
describe("#given empty content between tags", () => {
it("#then returns has_response true, response_complete true, and empty string result", () => {
it("#then returns has_response false, response_complete true, and empty string result", () => {
const result = extractCouncilResponse("<COUNCIL_MEMBER_RESPONSE></COUNCIL_MEMBER_RESPONSE>")
expect(result).toEqual({
has_response: true,
has_response: false,
response_complete: true,
result: "",
})
@@ -53,13 +53,13 @@ describe("extractCouncilResponse", () => {
describe("#given multiple tag pairs", () => {
it("#then returns content from the last opening tag", () => {
const text =
"<COUNCIL_MEMBER_RESPONSE>first analysis</COUNCIL_MEMBER_RESPONSE>\nSome interim text\n<COUNCIL_MEMBER_RESPONSE>final analysis</COUNCIL_MEMBER_RESPONSE>"
`<COUNCIL_MEMBER_RESPONSE>${"first".repeat(20)}</COUNCIL_MEMBER_RESPONSE>\nSome interim text\n<COUNCIL_MEMBER_RESPONSE>${"final".repeat(20)}</COUNCIL_MEMBER_RESPONSE>`
const result = extractCouncilResponse(text)
expect(result).toEqual({
has_response: true,
response_complete: true,
result: "final analysis",
result: "final".repeat(20),
})
})
})
@@ -72,14 +72,14 @@ describe("extractCouncilResponse", () => {
"Now here is my actual response:",
"<COUNCIL_MEMBER_RESPONSE>",
"## Finding 1: Tag discussion in body",
"The extractor uses lastIndexOf to find the opening tag.",
"The extractor uses lastIndexOf to find the opening tag, which ensures the last response is extracted.",
"</COUNCIL_MEMBER_RESPONSE>",
].join("\n")
const result = extractCouncilResponse(text)
expect(result).toEqual({
has_response: true,
response_complete: true,
result: "## Finding 1: Tag discussion in body\nThe extractor uses lastIndexOf to find the opening tag.",
result: "## Finding 1: Tag discussion in body\nThe extractor uses lastIndexOf to find the opening tag, which ensures the last response is extracted.",
})
})
})
@@ -97,11 +97,11 @@ describe("extractCouncilResponse", () => {
})
describe("#given whitespace-only content between tags", () => {
it("#then returns has_response true, response_complete true, and empty string result", () => {
it("#then returns has_response false, response_complete true, and empty string result", () => {
const result = extractCouncilResponse("<COUNCIL_MEMBER_RESPONSE> </COUNCIL_MEMBER_RESPONSE>")
expect(result).toEqual({
has_response: true,
has_response: false,
response_complete: true,
result: "",
})
@@ -110,13 +110,52 @@ describe("extractCouncilResponse", () => {
describe("#given content with surrounding text before the opening tag", () => {
it("#then returns only the tagged content", () => {
const text = "Some preamble text\n<COUNCIL_MEMBER_RESPONSE>the actual response</COUNCIL_MEMBER_RESPONSE>"
const text = `Some preamble text\n<COUNCIL_MEMBER_RESPONSE>${"a".repeat(100)}</COUNCIL_MEMBER_RESPONSE>`
const result = extractCouncilResponse(text)
expect(result).toEqual({
has_response: true,
response_complete: true,
result: "the actual response",
result: "a".repeat(100),
})
})
})
describe("#given 99-char content between tags (below MIN_RESPONSE_LENGTH)", () => {
it("#then returns has_response false, response_complete true", () => {
const content = "a".repeat(99)
const result = extractCouncilResponse(`<COUNCIL_MEMBER_RESPONSE>${content}</COUNCIL_MEMBER_RESPONSE>`)
expect(result).toEqual({
has_response: false,
response_complete: true,
result: content,
})
})
})
describe("#given 100-char content between tags (exactly MIN_RESPONSE_LENGTH)", () => {
it("#then returns has_response true, response_complete true", () => {
const content = "a".repeat(100)
const result = extractCouncilResponse(`<COUNCIL_MEMBER_RESPONSE>${content}</COUNCIL_MEMBER_RESPONSE>`)
expect(result).toEqual({
has_response: true,
response_complete: true,
result: content,
})
})
})
describe("#given 101-char content between tags (above MIN_RESPONSE_LENGTH)", () => {
it("#then returns has_response true, response_complete true", () => {
const content = "a".repeat(101)
const result = extractCouncilResponse(`<COUNCIL_MEMBER_RESPONSE>${content}</COUNCIL_MEMBER_RESPONSE>`)
expect(result).toEqual({
has_response: true,
response_complete: true,
result: content,
})
})
})
@@ -1,3 +1,5 @@
export const MIN_RESPONSE_LENGTH = 100
export const OPENING_TAG = "<COUNCIL_MEMBER_RESPONSE>"
export const CLOSING_TAG = "</COUNCIL_MEMBER_RESPONSE>"
@@ -22,5 +24,8 @@ export function extractCouncilResponse(fullText: string): CouncilResponseExtract
}
const content = fullText.slice(contentStart, closingAfterLastOpen).trim()
if (content.length < MIN_RESPONSE_LENGTH) {
return { has_response: false, response_complete: true, result: content }
}
return { has_response: true, response_complete: true, result: content }
}
@@ -44,9 +44,9 @@ describe("createCouncilFinalize", () => {
describe("#given 3 members with valid output files", () => {
it("#then creates full archive with all has_response true", async () => {
const agents = [
{ id: "bg_001", agent: "Council: Claude Opus", response: "Opus analysis" },
{ id: "bg_002", agent: "Council: GPT-5", response: "GPT analysis" },
{ id: "bg_003", agent: "Council: Gemini", response: "Gemini analysis" },
{ id: "bg_001", agent: "Council: Claude Opus", response: "Opus analysis: This is a detailed analysis from Opus model covering the full scope of the council question." },
{ id: "bg_002", agent: "Council: GPT-5", response: "GPT analysis: This is a detailed analysis from GPT model covering the full scope of the council question." },
{ id: "bg_003", agent: "Council: Gemini", response: "Gemini analysis: This is a detailed analysis from Gemini model covering the full scope of the council question." },
]
for (const a of agents) {
@@ -59,7 +59,7 @@ describe("createCouncilFinalize", () => {
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: agents.map((a) => a.id), name: "test" },
{ task_ids: agents.map((a) => a.id), name: "test", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -73,14 +73,12 @@ describe("createCouncilFinalize", () => {
expect(member.task_id).toBe(agents[i].id)
expect(member.has_response).toBe(true)
expect(member.response_complete).toBe(true)
expect(member).not.toHaveProperty("result")
expect(member).not.toHaveProperty("result_truncated")
expect(member.error).toBeUndefined()
expect(member.archive_file).toBeDefined()
}
const opusArchive = await readFile(join(tmpDir, result.members[0].archive_file!), "utf-8")
expect(opusArchive).toBe("Opus analysis")
expect(opusArchive).toBe("Opus analysis: This is a detailed analysis from Opus model covering the full scope of the council question.")
const metaContent = await readFile(join(tmpDir, result.meta_file), "utf-8")
expect(metaContent).toContain("archive_name: council-test-")
@@ -96,18 +94,18 @@ describe("createCouncilFinalize", () => {
it("#then returns partial success with error for missing member", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_001.md"),
mockTaskOutput("Council: Claude Opus", "Opus findings"),
mockTaskOutput("Council: Claude Opus", "Opus findings: This is a detailed analysis from Opus model covering the full scope of the council question."),
"utf-8",
)
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_003.md"),
mockTaskOutput("Council: Gemini", "Gemini findings"),
mockTaskOutput("Council: Gemini", "Gemini findings: This is a detailed analysis from Gemini model covering the full scope of the council question."),
"utf-8",
)
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_001", "bg_002", "bg_003"], name: "partial" },
{ task_ids: ["bg_001", "bg_002", "bg_003"], name: "partial", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -116,8 +114,6 @@ describe("createCouncilFinalize", () => {
expect(result.members[0].has_response).toBe(true)
expect(result.members[0].archive_file).toBeDefined()
expect(result.members[0]).not.toHaveProperty("result")
expect(result.members[0]).not.toHaveProperty("result_truncated")
expect(result.members[1].has_response).toBe(false)
expect(result.members[1].error).toBe("Task output file not found")
@@ -125,8 +121,6 @@ describe("createCouncilFinalize", () => {
expect(result.members[2].has_response).toBe(true)
expect(result.members[2].archive_file).toBeDefined()
expect(result.members[2]).not.toHaveProperty("result")
expect(result.members[2]).not.toHaveProperty("result_truncated")
})
})
@@ -141,7 +135,7 @@ describe("createCouncilFinalize", () => {
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_large"], name: "large" },
{ task_ids: ["bg_large"], name: "large", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -149,8 +143,6 @@ describe("createCouncilFinalize", () => {
const member = result.members[0]
expect(member.has_response).toBe(true)
expect(member.archive_file).toBeDefined()
expect(member).not.toHaveProperty("result")
expect(member).not.toHaveProperty("result_truncated")
const archiveContent = await readFile(join(tmpDir, member.archive_file!), "utf-8")
expect(archiveContent).toHaveLength(9000)
@@ -159,7 +151,7 @@ describe("createCouncilFinalize", () => {
})
describe("#given empty response between tags", () => {
it("#then returns has_response true with empty string result", async () => {
it("#then returns has_response false with empty string result", async () => {
const emptyOutput = [
"---",
"task_id: bg_empty",
@@ -182,17 +174,13 @@ describe("createCouncilFinalize", () => {
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_empty"], name: "empty" },
{ task_ids: ["bg_empty"], name: "empty", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
const member = result.members[0]
expect(member.has_response).toBe(true)
expect(member.response_complete).toBe(true)
expect(member.archive_file).toBeDefined()
expect(member).not.toHaveProperty("result")
expect(member).not.toHaveProperty("result_truncated")
expect(member.has_response).toBe(false)
})
})
@@ -200,18 +188,18 @@ describe("createCouncilFinalize", () => {
it("#then each member gets a unique archive_file keyed by task id", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_alpha.md"),
mockTaskOutput("Agent A+B", "First response"),
mockTaskOutput("Agent A+B", "First response: This is a detailed analysis from Agent A+B covering the full scope of the council question."),
"utf-8",
)
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_beta.md"),
mockTaskOutput("Agent A B", "Second response"),
mockTaskOutput("Agent A B", "Second response: This is a detailed analysis from Agent A B covering the full scope of the council question."),
"utf-8",
)
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_alpha", "bg_beta"], name: "collision" },
{ task_ids: ["bg_alpha", "bg_beta"], name: "collision", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -233,7 +221,7 @@ describe("createCouncilFinalize", () => {
it("#then registers critical custom context for Athena runtime guidance", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_intent.md"),
mockTaskOutput("Council: GPT-5", "Plan proposal"),
mockTaskOutput("Council: GPT-5", "Plan proposal: This is a detailed analysis from GPT model covering the full scope of the council question."),
"utf-8",
)
@@ -273,7 +261,7 @@ describe("createCouncilFinalize", () => {
it("#then emits diagnose action options for hephaestus and sisyphus", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_diagnose.md"),
mockTaskOutput("Council: Claude", "Root cause found"),
mockTaskOutput("Council: Claude", "Root cause found: This is a detailed analysis from Claude model covering the full scope of the council question."),
"utf-8",
)
@@ -305,7 +293,7 @@ describe("createCouncilFinalize", () => {
it("#then emits audit processing mode and batching guidance", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_audit.md"),
mockTaskOutput("Council: Claude", "Audit findings"),
mockTaskOutput("Council: Claude", "Audit findings: This is a detailed analysis from Claude model covering the full scope of the council question."),
"utf-8",
)
@@ -352,7 +340,7 @@ describe("createCouncilFinalize", () => {
it("#then emits informational write-to-document path without atlas delegation", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_eval.md"),
mockTaskOutput("Council: Claude", "Option comparison"),
mockTaskOutput("Council: Claude", "Option comparison: This is a detailed analysis from Claude model covering the full scope of the council question."),
"utf-8",
)
@@ -394,13 +382,13 @@ describe("createCouncilFinalize", () => {
it("#then rejects task IDs with path traversal characters", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_valid.md"),
mockTaskOutput("Agent", "Valid response"),
mockTaskOutput("Agent", "Valid response: This is a detailed analysis from Agent covering the full scope of the council question."),
"utf-8",
)
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["../../etc/passwd", "bg_valid", "foo/bar"], name: "traversal" },
{ task_ids: ["../../etc/passwd", "bg_valid", "foo/bar"], name: "traversal", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -416,13 +404,13 @@ describe("createCouncilFinalize", () => {
it("#then sanitizes name with path traversal characters", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_safe.md"),
mockTaskOutput("Agent", "Response"),
mockTaskOutput("Agent", "Response: This is a detailed analysis from Agent covering the full scope of the council question here."),
"utf-8",
)
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_safe"], name: "../../etc" },
{ task_ids: ["bg_safe"], name: "../../etc", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -432,11 +420,11 @@ describe("createCouncilFinalize", () => {
})
it("#then sanitizes name with absolute path", async () => {
await writeFile(join(tmpDir, ".sisyphus", "task-outputs", "bg_abs.md"), mockTaskOutput("Agent", "Response"), "utf-8")
await writeFile(join(tmpDir, ".sisyphus", "task-outputs", "bg_abs.md"), mockTaskOutput("Agent", "Response: This is a detailed analysis from Agent covering the full scope of the council question here."), "utf-8")
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_abs"], name: "/absolute/path" },
{ task_ids: ["bg_abs"], name: "/absolute/path", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -446,11 +434,11 @@ describe("createCouncilFinalize", () => {
})
it("#then sanitizes name with dot-dot-slash in the middle", async () => {
await writeFile(join(tmpDir, ".sisyphus", "task-outputs", "bg_mid.md"), mockTaskOutput("Agent", "Response"), "utf-8")
await writeFile(join(tmpDir, ".sisyphus", "task-outputs", "bg_mid.md"), mockTaskOutput("Agent", "Response: This is a detailed analysis from Agent covering the full scope of the council question here."), "utf-8")
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_mid"], name: "foo/../bar" },
{ task_ids: ["bg_mid"], name: "foo/../bar", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -460,11 +448,11 @@ describe("createCouncilFinalize", () => {
})
it("#then accepts valid task IDs", async () => {
await writeFile(join(tmpDir, ".sisyphus", "task-outputs", "valid-id_123.md"), mockTaskOutput("Agent", "Response"), "utf-8")
await writeFile(join(tmpDir, ".sisyphus", "task-outputs", "valid-id_123.md"), mockTaskOutput("Agent", "Response: This is a detailed analysis from Agent covering the full scope of the council question here."), "utf-8")
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["valid-id_123"], name: "valid" },
{ task_ids: ["valid-id_123"], name: "valid", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -477,13 +465,13 @@ describe("createCouncilFinalize", () => {
it("#then rejects prompt_file with absolute path outside workspace", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_prompt.md"),
mockTaskOutput("Agent", "Response"),
mockTaskOutput("Agent", "Response: This is a detailed analysis from Agent covering the full scope of the council question here."),
"utf-8",
)
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_prompt"], name: "prompt-test", prompt_file: "/etc/passwd" },
{ task_ids: ["bg_prompt"], name: "prompt-test", prompt_file: "/etc/passwd", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -496,13 +484,13 @@ describe("createCouncilFinalize", () => {
it("#then rejects prompt_file with relative traversal", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_rel.md"),
mockTaskOutput("Agent", "Response"),
mockTaskOutput("Agent", "Response: This is a detailed analysis from Agent covering the full scope of the council question here."),
"utf-8",
)
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_rel"], name: "rel-test", prompt_file: "../../etc/passwd" },
{ task_ids: ["bg_rel"], name: "rel-test", prompt_file: "../../etc/passwd", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -517,13 +505,13 @@ describe("createCouncilFinalize", () => {
await writeFile(join(tmpDir, ".sisyphus", "tmp", "athena-council-test.md"), "Test prompt", "utf-8")
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_ok.md"),
mockTaskOutput("Agent", "Response"),
mockTaskOutput("Agent", "Response: This is a detailed analysis from Agent covering the full scope of the council question here."),
"utf-8",
)
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_ok"], name: "valid-prompt", prompt_file: ".sisyphus/tmp/athena-council-test.md" },
{ task_ids: ["bg_ok"], name: "valid-prompt", prompt_file: ".sisyphus/tmp/athena-council-test.md", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -535,7 +523,7 @@ describe("createCouncilFinalize", () => {
it("#then rejects task IDs with backslash characters", async () => {
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg\\evil"], name: "backslash" },
{ task_ids: ["bg\\evil"], name: "backslash", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -548,13 +536,13 @@ describe("createCouncilFinalize", () => {
it("#then handles name that slugifies to empty string", async () => {
await writeFile(
join(tmpDir, ".sisyphus", "task-outputs", "bg_empty_name.md"),
mockTaskOutput("Agent", "Response"),
mockTaskOutput("Agent", "Response: This is a detailed analysis from Agent covering the full scope of the council question here."),
"utf-8",
)
const toolDef = createCouncilFinalize(tmpDir)
const resultStr = await toolDef.execute(
{ task_ids: ["bg_empty_name"], name: "///..." },
{ task_ids: ["bg_empty_name"], name: "///...", intent: "FREEFORM" },
mockCtx,
)
const result: CouncilFinalizeResult = JSON.parse(resultStr)
@@ -1,84 +1,24 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
import { readFile, writeFile, mkdir, rename } from "node:fs/promises"
import { join, isAbsolute, resolve, relative } from "node:path"
import { readFile, writeFile, mkdir } from "node:fs/promises"
import { join } from "node:path"
import { randomBytes } from "node:crypto"
import { extractCouncilResponse } from "./council-response-extractor"
import { TASK_ID_PATTERN, slugify, toPosixPath, extractAgentFromFrontmatter, isPathEscaping, movePromptFile } from "./council-finalize-helpers"
import { formatMetaYaml, type MetaMember } from "./meta-yaml-formatter"
import {
buildAthenaRuntimeGuidance,
getValidCouncilIntents,
resolveCouncilIntent,
COUNCIL_DEFAULTS,
} from "../../agents/athena"
import { log } from "../../shared/logger"
import type { ContextCollector } from "../../features/context-injector"
import type { CouncilFinalizeArgs, CouncilMemberResult, CouncilFinalizeResult } from "./types"
interface MetaMember {
task_id: string
member: string
member_slug: string
task_output_path: string
archive_file: string
has_response: boolean
response_complete: boolean
}
type RegisterContext = Pick<ContextCollector, "register">
type CouncilFinalizeToolContext = {
sessionID?: string
}
const TASK_ID_PATTERN = /^[a-zA-Z0-9_-]+$/
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
}
function toPosixPath(pathValue: string): string {
return pathValue.replace(/\\/g, "/")
}
function extractAgentFromFrontmatter(content: string): string | null {
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
if (!fmMatch) return null
const agentLine = fmMatch[1].match(/^agent:\s*(.+)$/m)
return agentLine ? agentLine[1].trim() : null
}
function formatMetaYaml(archiveName: string, createdAt: string, members: MetaMember[], question?: string, promptFile?: string): string {
const lines: string[] = [
`archive_name: ${archiveName}`,
`created_at: ${createdAt}`,
]
if (question) {
lines.push(`question: |`)
for (const qLine of question.split("\n")) {
lines.push(` ${qLine}`)
}
}
if (promptFile) {
lines.push(`prompt_file: ${promptFile}`)
}
lines.push("members:")
for (const m of members) {
lines.push(` - task_id: ${m.task_id}`)
lines.push(` member: "${m.member}"`)
lines.push(` member_slug: ${m.member_slug}`)
lines.push(` task_output_path: ${m.task_output_path}`)
lines.push(` archive_file: ${m.archive_file}`)
lines.push(` has_response: ${m.has_response}`)
lines.push(` response_complete: ${m.response_complete}`)
}
return lines.join("\n") + "\n"
}
export function createCouncilFinalize(
basePath?: string,
options?: { contextCollector?: RegisterContext }
@@ -95,14 +35,13 @@ export function createCouncilFinalize(
name: tool.schema.string().describe("Council name used in the archive directory name"),
intent: tool.schema
.string()
.optional()
.describe(`Classified question intent used for runtime Athena guidance injection. Valid intents: ${getValidCouncilIntents().join(", ")}`),
question: tool.schema.string().optional().describe("Original user question that triggered the council"),
prompt_file: tool.schema.string().optional().describe("Path to the council prompt temp file (will be moved into the archive)"),
},
async execute(args: CouncilFinalizeArgs, toolContext: CouncilFinalizeToolContext) {
const resolvedIntent = resolveCouncilIntent(args.intent)
if (args.intent && !resolvedIntent) {
if (!resolvedIntent) {
return `Invalid intent: "${args.intent}". Valid intents: ${getValidCouncilIntents().join(", ")}.`
}
@@ -120,16 +59,14 @@ export function createCouncilFinalize(
}
const base = basePath ?? process.cwd()
const hexId = randomBytes(2).toString("hex")
const hexId = randomBytes(COUNCIL_DEFAULTS.ARCHIVE_ID_BYTES).toString("hex")
const safeName = slugify(args.name) || "unnamed"
const archiveName = `council-${safeName}-${hexId}`
const relArchiveDir = join(".sisyphus", "athena", archiveName)
const relArchiveDirForOutput = toPosixPath(relArchiveDir)
const absArchiveDir = join(base, relArchiveDir)
const expectedArchiveRoot = join(base, ".sisyphus", "athena")
const relFromArchiveRoot = relative(expectedArchiveRoot, absArchiveDir)
if (relFromArchiveRoot.startsWith("..") || isAbsolute(relFromArchiveRoot)) {
if (isPathEscaping(join(base, ".sisyphus", "athena"), absArchiveDir)) {
return `Security error: archive directory would escape .sisyphus/athena/`
}
@@ -162,9 +99,7 @@ export function createCouncilFinalize(
const relTaskOutputForOutput = toPosixPath(relTaskOutput)
const absTaskOutput = join(base, relTaskOutput)
const expectedTaskOutputRoot = join(base, ".sisyphus", "task-outputs")
const relFromTaskOutputRoot = relative(expectedTaskOutputRoot, absTaskOutput)
if (relFromTaskOutputRoot.startsWith("..") || isAbsolute(relFromTaskOutputRoot)) {
if (isPathEscaping(join(base, ".sisyphus", "task-outputs"), absTaskOutput)) {
members.push({
task_id: taskId,
member: "unknown",
@@ -241,27 +176,9 @@ export function createCouncilFinalize(
})
}
let relPromptFile: string | undefined
if (args.prompt_file) {
try {
const promptFilename = "council-prompt.md"
const absPromptSrc = isAbsolute(args.prompt_file) ? args.prompt_file : resolve(base, args.prompt_file)
const expectedPromptRoot = join(base, ".sisyphus", "tmp")
const relFromPromptRoot = relative(expectedPromptRoot, absPromptSrc)
if (relFromPromptRoot.startsWith("..") || isAbsolute(relFromPromptRoot)) {
log("[council-finalize] Rejected prompt_file outside .sisyphus/tmp/", { promptFile: args.prompt_file })
} else {
const absPromptDest = join(absArchiveDir, promptFilename)
await rename(absPromptSrc, absPromptDest).catch(async () => {
const content = await readFile(absPromptSrc, "utf-8")
await writeFile(absPromptDest, content, "utf-8")
})
relPromptFile = toPosixPath(join(relArchiveDir, promptFilename))
}
} catch (err) {
log("[council-finalize] Failed to move prompt file", { promptFile: args.prompt_file, error: String(err) })
}
}
const relPromptFile = args.prompt_file
? await movePromptFile(args.prompt_file, base, absArchiveDir, relArchiveDir)
: undefined
const relMetaFile = join(relArchiveDir, "meta.yaml")
const relMetaFileForOutput = toPosixPath(relMetaFile)
+1 -1
View File
@@ -1,4 +1,4 @@
export { createCouncilFinalize } from "./create-council-finalize"
export { extractCouncilResponse, OPENING_TAG, CLOSING_TAG } from "./council-response-extractor"
export { extractCouncilResponse, OPENING_TAG, CLOSING_TAG, MIN_RESPONSE_LENGTH } from "./council-response-extractor"
export type { CouncilResponseExtraction } from "./council-response-extractor"
export type { CouncilFinalizeArgs, CouncilMemberResult, CouncilFinalizeResult } from "./types"
@@ -0,0 +1,41 @@
export interface MetaMember {
task_id: string
member: string
member_slug: string
task_output_path: string
archive_file: string
has_response: boolean
response_complete: boolean
}
export function formatMetaYaml(archiveName: string, createdAt: string, members: MetaMember[], question?: string, promptFile?: string): string {
const lines: string[] = [
`archive_name: ${archiveName}`,
`created_at: ${createdAt}`,
]
if (question) {
lines.push(`question: |`)
for (const qLine of question.split("\n")) {
lines.push(` ${qLine}`)
}
}
if (promptFile) {
lines.push(`prompt_file: ${promptFile}`)
}
lines.push("members:")
for (const m of members) {
lines.push(` - task_id: ${m.task_id}`)
lines.push(` member: "${m.member}"`)
lines.push(` member_slug: ${m.member_slug}`)
lines.push(` task_output_path: ${m.task_output_path}`)
lines.push(` archive_file: ${m.archive_file}`)
lines.push(` has_response: ${m.has_response}`)
lines.push(` response_complete: ${m.response_complete}`)
}
return lines.join("\n") + "\n"
}
+1 -1
View File
@@ -1,7 +1,7 @@
export interface CouncilFinalizeArgs {
task_ids: string[]
name: string
intent?: string
intent: string
question?: string
prompt_file?: string
}
@@ -197,5 +197,33 @@ describe("createPrepareCouncilPromptTool", () => {
expect(intentIdx).toBeLessThan(questionIdx)
})
})
describe("#when called with lowercase intent 'audit'", () => {
it("#then resolves correctly and produces file with AUDIT addendum", async () => {
tmpDir = await mkdtemp(join(tmpdir(), "council-test-"))
const toolDef = createPrepareCouncilPromptTool(tmpDir)
const result = await toolDef.execute({ prompt: "Review security", intent: "audit" }, mockContext)
expect(result).not.toContain("Invalid intent")
expect(result).toContain("intent: AUDIT")
const filePath = extractFilePath(result)
const content = await readFile(filePath, "utf-8")
expect(content).toContain("## Analysis Intent: AUDIT")
})
})
describe("#when called with mixed case intent 'Diagnose'", () => {
it("#then resolves correctly and produces file with DIAGNOSE addendum", async () => {
tmpDir = await mkdtemp(join(tmpdir(), "council-test-"))
const toolDef = createPrepareCouncilPromptTool(tmpDir)
const result = await toolDef.execute({ prompt: "Why is the API failing?", intent: "Diagnose" }, mockContext)
expect(result).not.toContain("Invalid intent")
expect(result).toContain("intent: DIAGNOSE")
const filePath = extractFilePath(result)
const content = await readFile(filePath, "utf-8")
expect(content).toContain("## Analysis Intent: DIAGNOSE")
})
})
})
})
+19 -10
View File
@@ -3,9 +3,9 @@ import { randomUUID } from "node:crypto"
import { writeFile, unlink, mkdir, readdir, stat } from "node:fs/promises"
import { join } from "node:path"
import { log } from "../../shared/logger"
import { COUNCIL_SOLO_ADDENDUM, COUNCIL_DELEGATION_ADDENDUM, COUNCIL_INTENT_ADDENDUMS, getValidCouncilIntents, type CouncilIntent } from "../../agents/athena"
import { COUNCIL_SOLO_ADDENDUM, COUNCIL_DELEGATION_ADDENDUM, COUNCIL_INTENT_ADDENDUMS, getValidCouncilIntents, resolveCouncilIntent, type CouncilIntent, COUNCIL_DEFAULTS } from "../../agents/athena"
const CLEANUP_DELAY_MS = 30 * 60 * 1000
const CLEANUP_DELAY_MS = COUNCIL_DEFAULTS.CLEANUP_DELAY_MS
const COUNCIL_TMP_DIR = ".sisyphus/tmp"
const COUNCIL_FILE_PREFIX = "athena-council-"
@@ -24,12 +24,18 @@ async function cleanupStaleTempFiles(directory: string): Promise<void> {
await unlink(filePath)
log("[prepare-council-prompt] Cleaned up stale temp file", { filePath })
}
} catch {
// File may have been deleted between readdir and stat
} catch (err: unknown) {
const code = (err as NodeJS.ErrnoException).code
if (code !== "ENOENT") {
log("[prepare-council-prompt] Unexpected error during temp file cleanup", { filePath, error: String(err), code })
}
}
}
} catch {
// Directory may not exist yet — nothing to clean
} catch (err: unknown) {
const code = (err as NodeJS.ErrnoException).code
if (code !== "ENOENT") {
log("[prepare-council-prompt] Unexpected error reading temp directory", { directory: tmpDir, error: String(err), code })
}
}
}
@@ -78,12 +84,15 @@ Returns the file path to reference in subsequent task() calls.`
return `Invalid mode: "${args.mode}". Valid modes: "solo", "delegation".`
}
const validIntents = getValidCouncilIntents()
if (args.intent !== undefined && !(validIntents as readonly string[]).includes(args.intent.toUpperCase())) {
return `Invalid intent: "${args.intent}". Valid intents: ${validIntents.map((i) => `"${i}"`).join(", ")}.`
if (args.intent !== undefined) {
const resolved = resolveCouncilIntent(args.intent)
if (!resolved) {
const validIntents = getValidCouncilIntents()
return `Invalid intent: "${args.intent}". Valid intents: ${validIntents.map((i) => `"${i}"`).join(", ")}.`
}
}
const resolvedIntent = args.intent?.toUpperCase()
const resolvedIntent = args.intent ? resolveCouncilIntent(args.intent) : undefined
const mode = args.mode === "delegation" ? "delegation" : "solo"