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:
@@ -0,0 +1,7 @@
|
||||
export const COUNCIL_DEFAULTS = {
|
||||
CLEANUP_DELAY_MS: 30 * 60 * 1000,
|
||||
BACKGROUND_WAIT_TIMEOUT_MS: 30000,
|
||||
STUCK_THRESHOLD_SECONDS: 120,
|
||||
MEMBER_MAX_RUNNING_SECONDS: 1800,
|
||||
ARCHIVE_ID_BYTES: 2,
|
||||
} as const
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { COUNCIL_MEMBER_PROMPT } from "./council-member-agent"
|
||||
import { COUNCIL_MEMBER_PROMPT, createCouncilMemberAgent } from "./council-member-agent"
|
||||
|
||||
describe("COUNCIL_MEMBER_PROMPT", () => {
|
||||
describe("#given the prompt constant", () => {
|
||||
@@ -44,3 +44,79 @@ describe("COUNCIL_MEMBER_PROMPT", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("createCouncilMemberAgent", () => {
|
||||
describe("#given a model string", () => {
|
||||
describe("#when creating a council member agent", () => {
|
||||
const agent = createCouncilMemberAgent("openai/gpt-5-nano")
|
||||
|
||||
it("#then returns an object with the given model", () => {
|
||||
expect(agent.model).toBe("openai/gpt-5-nano")
|
||||
})
|
||||
|
||||
it("#then has temperature 0.1", () => {
|
||||
expect(agent.temperature).toBe(0.1)
|
||||
})
|
||||
|
||||
it("#then has the COUNCIL_MEMBER_PROMPT as prompt", () => {
|
||||
expect(agent.prompt).toBe(COUNCIL_MEMBER_PROMPT)
|
||||
})
|
||||
|
||||
it("#then has mode subagent", () => {
|
||||
expect(agent.mode).toBe("subagent")
|
||||
})
|
||||
|
||||
it("#then has tool restrictions with permission object", () => {
|
||||
expect(agent.permission).toBeDefined()
|
||||
})
|
||||
|
||||
it("#then allows read tool", () => {
|
||||
const perm = agent.permission as Record<string, string>
|
||||
expect(perm.read).toBe("allow")
|
||||
})
|
||||
|
||||
it("#then allows grep tool", () => {
|
||||
const perm = agent.permission as Record<string, string>
|
||||
expect(perm.grep).toBe("allow")
|
||||
})
|
||||
|
||||
it("#then allows glob tool", () => {
|
||||
const perm = agent.permission as Record<string, string>
|
||||
expect(perm.glob).toBe("allow")
|
||||
})
|
||||
|
||||
it("#then allows lsp_goto_definition tool", () => {
|
||||
const perm = agent.permission as Record<string, string>
|
||||
expect(perm.lsp_goto_definition).toBe("allow")
|
||||
})
|
||||
|
||||
it("#then allows ast_grep_search tool", () => {
|
||||
const perm = agent.permission as Record<string, string>
|
||||
expect(perm.ast_grep_search).toBe("allow")
|
||||
})
|
||||
|
||||
it("#then denies all other tools via wildcard", () => {
|
||||
const perm = agent.permission as Record<string, string>
|
||||
expect(perm["*"]).toBe("deny")
|
||||
})
|
||||
|
||||
it("#then explicitly denies todowrite", () => {
|
||||
const perm = agent.permission as Record<string, string>
|
||||
expect(perm.todowrite).toBe("deny")
|
||||
})
|
||||
|
||||
it("#then explicitly denies todoread", () => {
|
||||
const perm = agent.permission as Record<string, string>
|
||||
expect(perm.todoread).toBe("deny")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given the factory function", () => {
|
||||
describe("#when checking the static mode property", () => {
|
||||
it("#then has mode 'subagent'", () => {
|
||||
expect(createCouncilMemberAgent.mode).toBe("subagent")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,7 +36,8 @@ Example:
|
||||
Your analysis here...
|
||||
</COUNCIL_MEMBER_RESPONSE>
|
||||
|
||||
If you do not wrap your response in <COUNCIL_MEMBER_RESPONSE> tags, your analysis will not be included in the synthesis.`
|
||||
If you do not wrap your response in <COUNCIL_MEMBER_RESPONSE> tags, your analysis will not be included in the synthesis.
|
||||
Your response inside the tags must be at least 100 characters of substantive content. Empty or trivially short responses will be treated as missing and will not count toward quorum.`
|
||||
|
||||
export const COUNCIL_SOLO_ADDENDUM = `
|
||||
## Solo Analysis Mode
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { resolveCouncilIntent, buildAthenaRuntimeGuidance, getValidCouncilIntents } from "./council-runtime-guidance"
|
||||
|
||||
describe("council-runtime-guidance", () => {
|
||||
describe("resolveCouncilIntent", () => {
|
||||
describe("#given valid uppercase intents", () => {
|
||||
describe("#when called with each valid intent", () => {
|
||||
const validIntents = ["DIAGNOSE", "AUDIT", "PLAN", "EVALUATE", "EXPLAIN", "CREATE", "PERSPECTIVES", "FREEFORM"] as const
|
||||
|
||||
for (const intent of validIntents) {
|
||||
it(`#then returns "${intent}" for "${intent}"`, () => {
|
||||
expect(resolveCouncilIntent(intent)).toBe(intent)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given lowercase intents", () => {
|
||||
describe("#when called with lowercase versions", () => {
|
||||
it("#then normalizes 'diagnose' to 'DIAGNOSE'", () => {
|
||||
expect(resolveCouncilIntent("diagnose")).toBe("DIAGNOSE")
|
||||
})
|
||||
|
||||
it("#then normalizes 'audit' to 'AUDIT'", () => {
|
||||
expect(resolveCouncilIntent("audit")).toBe("AUDIT")
|
||||
})
|
||||
|
||||
it("#then normalizes 'freeform' to 'FREEFORM'", () => {
|
||||
expect(resolveCouncilIntent("freeform")).toBe("FREEFORM")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given mixed case intents", () => {
|
||||
describe("#when called with mixed case", () => {
|
||||
it("#then normalizes 'Audit' to 'AUDIT'", () => {
|
||||
expect(resolveCouncilIntent("Audit")).toBe("AUDIT")
|
||||
})
|
||||
|
||||
it("#then normalizes 'DiAgNoSe' to 'DIAGNOSE'", () => {
|
||||
expect(resolveCouncilIntent("DiAgNoSe")).toBe("DIAGNOSE")
|
||||
})
|
||||
|
||||
it("#then normalizes 'Perspectives' to 'PERSPECTIVES'", () => {
|
||||
expect(resolveCouncilIntent("Perspectives")).toBe("PERSPECTIVES")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given invalid inputs", () => {
|
||||
describe("#when called with an unrecognized intent", () => {
|
||||
it("#then returns null for 'INVALID'", () => {
|
||||
expect(resolveCouncilIntent("INVALID")).toBeNull()
|
||||
})
|
||||
|
||||
it("#then returns null for 'COMPARISON'", () => {
|
||||
expect(resolveCouncilIntent("COMPARISON")).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when called with undefined", () => {
|
||||
it("#then returns null", () => {
|
||||
expect(resolveCouncilIntent(undefined)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when called with empty string", () => {
|
||||
it("#then returns null", () => {
|
||||
expect(resolveCouncilIntent("")).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildAthenaRuntimeGuidance", () => {
|
||||
describe("#given a valid intent", () => {
|
||||
describe("#when building guidance for DIAGNOSE", () => {
|
||||
it("#then wraps content in athena_runtime_guidance tags", () => {
|
||||
const result = buildAthenaRuntimeGuidance("DIAGNOSE")
|
||||
expect(result).toContain("<athena_runtime_guidance>")
|
||||
expect(result).toContain("</athena_runtime_guidance>")
|
||||
})
|
||||
|
||||
it("#then contains the intent name", () => {
|
||||
const result = buildAthenaRuntimeGuidance("DIAGNOSE")
|
||||
expect(result).toContain("intent: DIAGNOSE")
|
||||
})
|
||||
|
||||
it("#then contains DIAGNOSE-specific content about root cause", () => {
|
||||
const result = buildAthenaRuntimeGuidance("DIAGNOSE")
|
||||
expect(result).toContain("root cause")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when building guidance for AUDIT", () => {
|
||||
it("#then contains the AUDIT intent name", () => {
|
||||
const result = buildAthenaRuntimeGuidance("AUDIT")
|
||||
expect(result).toContain("intent: AUDIT")
|
||||
})
|
||||
|
||||
it("#then contains AUDIT-specific synthesis rules", () => {
|
||||
const result = buildAthenaRuntimeGuidance("AUDIT")
|
||||
expect(result).toContain("AUDIT synthesis")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when building guidance for FREEFORM", () => {
|
||||
it("#then contains FREEFORM intent name", () => {
|
||||
const result = buildAthenaRuntimeGuidance("FREEFORM")
|
||||
expect(result).toContain("intent: FREEFORM")
|
||||
})
|
||||
|
||||
it("#then contains FREEFORM-specific content", () => {
|
||||
const result = buildAthenaRuntimeGuidance("FREEFORM")
|
||||
expect(result).toContain("FREEFORM synthesis")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when building guidance for each intent", () => {
|
||||
const allIntents = ["DIAGNOSE", "AUDIT", "PLAN", "EVALUATE", "EXPLAIN", "CREATE", "PERSPECTIVES", "FREEFORM"] as const
|
||||
|
||||
for (const intent of allIntents) {
|
||||
it(`#then ${intent} guidance contains runtime_synthesis_rules`, () => {
|
||||
const result = buildAthenaRuntimeGuidance(intent)
|
||||
expect(result).toContain("runtime_synthesis_rules")
|
||||
})
|
||||
|
||||
it(`#then ${intent} guidance contains runtime_action_paths`, () => {
|
||||
const result = buildAthenaRuntimeGuidance(intent)
|
||||
expect(result).toContain("runtime_action_paths")
|
||||
})
|
||||
|
||||
it(`#then ${intent} guidance contains source: council_finalize`, () => {
|
||||
const result = buildAthenaRuntimeGuidance(intent)
|
||||
expect(result).toContain("source: council_finalize")
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getValidCouncilIntents", () => {
|
||||
describe("#given the function is called", () => {
|
||||
describe("#when retrieving valid intents", () => {
|
||||
it("#then returns an array of 8 intents", () => {
|
||||
const intents = getValidCouncilIntents()
|
||||
expect(intents).toHaveLength(8)
|
||||
})
|
||||
|
||||
it("#then contains all expected intent values", () => {
|
||||
const intents = getValidCouncilIntents()
|
||||
expect(intents).toContain("DIAGNOSE")
|
||||
expect(intents).toContain("AUDIT")
|
||||
expect(intents).toContain("PLAN")
|
||||
expect(intents).toContain("EVALUATE")
|
||||
expect(intents).toContain("EXPLAIN")
|
||||
expect(intents).toContain("CREATE")
|
||||
expect(intents).toContain("PERSPECTIVES")
|
||||
expect(intents).toContain("FREEFORM")
|
||||
})
|
||||
|
||||
it("#then returns a readonly array", () => {
|
||||
const intents1 = getValidCouncilIntents()
|
||||
const intents2 = getValidCouncilIntents()
|
||||
expect(intents1).toBe(intents2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -7,3 +7,4 @@ export {
|
||||
resolveCouncilIntent,
|
||||
} from "./council-runtime-guidance"
|
||||
export type { CouncilIntent } from "./council-runtime-guidance"
|
||||
export { COUNCIL_DEFAULTS } from "./constants"
|
||||
|
||||
@@ -83,4 +83,75 @@ describe("council-member-agents", () => {
|
||||
expect(result.registeredKeys).toHaveLength(0)
|
||||
expect(result.agents).toEqual({})
|
||||
})
|
||||
|
||||
test("returns skippedMembers with reason for invalid model format", () => {
|
||||
//#given
|
||||
const config = {
|
||||
members: [
|
||||
{ model: "openai/gpt-5.3-codex", name: "GPT" },
|
||||
{ model: "no-slash", name: "Bad" },
|
||||
{ model: "anthropic/claude-opus-4-6", name: "Claude" },
|
||||
],
|
||||
retry_on_fail: 0,
|
||||
retry_failed_if_others_finished: false,
|
||||
cancel_retrying_on_quorum: true,
|
||||
stuck_threshold_seconds: 120,
|
||||
member_max_running_seconds: 1800,
|
||||
}
|
||||
//#when
|
||||
const result = registerCouncilMemberAgents(config)
|
||||
//#then
|
||||
expect(result.skippedMembers).toHaveLength(1)
|
||||
expect(result.skippedMembers[0].name).toBe("Bad")
|
||||
expect(result.skippedMembers[0].reason).toContain("Invalid model format")
|
||||
expect(result.skippedMembers[0].reason).toContain("no-slash")
|
||||
})
|
||||
|
||||
test("returns skippedMembers with reason for duplicate names", () => {
|
||||
//#given
|
||||
const config = {
|
||||
members: [
|
||||
{ model: "openai/gpt-5.3-codex", name: "Alpha" },
|
||||
{ model: "anthropic/claude-opus-4-6", name: "Beta" },
|
||||
{ model: "google/gemini-3-pro", name: "alpha" },
|
||||
],
|
||||
retry_on_fail: 0,
|
||||
retry_failed_if_others_finished: false,
|
||||
cancel_retrying_on_quorum: true,
|
||||
stuck_threshold_seconds: 120,
|
||||
member_max_running_seconds: 1800,
|
||||
}
|
||||
//#when
|
||||
const result = registerCouncilMemberAgents(config)
|
||||
//#then
|
||||
expect(result.registeredKeys).toHaveLength(2)
|
||||
expect(result.skippedMembers).toHaveLength(1)
|
||||
expect(result.skippedMembers[0].name).toBe("alpha")
|
||||
expect(result.skippedMembers[0].reason).toContain("Duplicate name")
|
||||
})
|
||||
|
||||
test("returns skippedMembers combining both invalid model and duplicate reasons", () => {
|
||||
//#given
|
||||
const config = {
|
||||
members: [
|
||||
{ model: "openai/gpt-5.3-codex", name: "GPT" },
|
||||
{ model: "bad-model", name: "Invalid" },
|
||||
{ model: "anthropic/claude-opus-4-6", name: "Claude" },
|
||||
{ model: "google/gemini-3-pro", name: "gpt" },
|
||||
],
|
||||
retry_on_fail: 0,
|
||||
retry_failed_if_others_finished: false,
|
||||
cancel_retrying_on_quorum: true,
|
||||
stuck_threshold_seconds: 120,
|
||||
member_max_running_seconds: 1800,
|
||||
}
|
||||
//#when
|
||||
const result = registerCouncilMemberAgents(config)
|
||||
//#then
|
||||
expect(result.skippedMembers).toHaveLength(2)
|
||||
expect(result.skippedMembers[0].name).toBe("Invalid")
|
||||
expect(result.skippedMembers[0].reason).toContain("Invalid model format")
|
||||
expect(result.skippedMembers[1].name).toBe("gpt")
|
||||
expect(result.skippedMembers[1].reason).toContain("Duplicate name")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -140,13 +140,13 @@ describe("hasCouncilResponseTag", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given assistant message contains the response tag", () => {
|
||||
describe("#given assistant message contains a complete council response", () => {
|
||||
it("#then should return true", () => {
|
||||
//#given
|
||||
const messages = [
|
||||
{
|
||||
info: { role: "assistant" },
|
||||
parts: [{ type: "text", text: "My analysis </COUNCIL_MEMBER_RESPONSE>" }],
|
||||
parts: [{ type: "text", text: "<COUNCIL_MEMBER_RESPONSE>This is a comprehensive analysis of the council member findings. The investigation reveals multiple significant patterns across the entire codebase that require careful refactoring.</COUNCIL_MEMBER_RESPONSE>" }],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("hasCouncilResponseTag", () => {
|
||||
},
|
||||
{
|
||||
info: { role: "assistant" },
|
||||
parts: [{ type: "text", text: "final answer </COUNCIL_MEMBER_RESPONSE>" }],
|
||||
parts: [{ type: "text", text: "<COUNCIL_MEMBER_RESPONSE>This is a comprehensive analysis of the council member findings. The investigation reveals multiple significant patterns across the entire codebase that require careful refactoring.</COUNCIL_MEMBER_RESPONSE>" }],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -6,12 +6,11 @@ import {
|
||||
createInternalAgentTextPart,
|
||||
} from "../../shared"
|
||||
import { setSessionTools } from "../../shared/session-tools-store"
|
||||
import { extractCouncilResponse } from "../../tools/council-archive/council-response-extractor"
|
||||
import { COUNCIL_MEMBER_KEY_PREFIX } from "../../agents/builtin-agents/council-member-agents"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
const COUNCIL_RESPONSE_TAG = "</COUNCIL_MEMBER_RESPONSE>"
|
||||
|
||||
const CONTINUATION_PROMPT =
|
||||
"You have not yet produced your final <COUNCIL_MEMBER_RESPONSE>. Continue your analysis and wrap your findings in <COUNCIL_MEMBER_RESPONSE> tags. If you are waiting for background tasks, use background_wait to block until they complete, then produce your response."
|
||||
|
||||
@@ -28,17 +27,18 @@ export function resetCouncilNudgeCount(taskId: string): void {
|
||||
}
|
||||
|
||||
export function hasCouncilResponseTag(sessionMessages: Array<{ info?: { role?: string }; parts?: Array<{ type?: string; text?: string }> }>): boolean {
|
||||
for (let i = sessionMessages.length - 1; i >= 0; i--) {
|
||||
const msg = sessionMessages[i]
|
||||
const assistantTexts: string[] = []
|
||||
for (const msg of sessionMessages) {
|
||||
if (msg.info?.role !== "assistant") continue
|
||||
const parts = msg.parts ?? []
|
||||
for (const part of parts) {
|
||||
if (part.type === "text" && part.text?.includes(COUNCIL_RESPONSE_TAG)) {
|
||||
return true
|
||||
for (const part of msg.parts ?? []) {
|
||||
if (part.type === "text" && part.text) {
|
||||
assistantTexts.push(part.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
if (assistantTexts.length === 0) return false
|
||||
const extraction = extractCouncilResponse(assistantTexts.join("\n"))
|
||||
return extraction.has_response && extraction.response_complete
|
||||
}
|
||||
|
||||
export function sendCouncilContinuationNudge(
|
||||
|
||||
@@ -13,13 +13,13 @@ function createMockClient(
|
||||
}
|
||||
|
||||
describe("sessionHasCouncilResponse", () => {
|
||||
describe("#given assistant message with closing council tag", () => {
|
||||
describe("#given assistant message with complete council response", () => {
|
||||
it("#when tag is in text part #then should return true", async () => {
|
||||
//#given
|
||||
const client = createMockClient([
|
||||
{
|
||||
info: { role: "assistant" },
|
||||
parts: [{ type: "text", text: "Some response</COUNCIL_MEMBER_RESPONSE>" }],
|
||||
parts: [{ type: "text", text: "<COUNCIL_MEMBER_RESPONSE>This is a comprehensive analysis of the council member findings. The investigation reveals multiple significant patterns across the entire codebase that require careful refactoring.</COUNCIL_MEMBER_RESPONSE>" }],
|
||||
},
|
||||
])
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("sessionHasCouncilResponse", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given assistant message with tag buried in longer text", () => {
|
||||
describe("#given assistant message with complete council response in longer text", () => {
|
||||
it("#when tag appears mid-text #then should return true", async () => {
|
||||
//#given
|
||||
const client = createMockClient([
|
||||
@@ -111,7 +111,7 @@ describe("sessionHasCouncilResponse", () => {
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Here is my analysis of the situation.\n\nLong content here.\n\n</COUNCIL_MEMBER_RESPONSE>\n\nMore text after.",
|
||||
text: "Here is my analysis. <COUNCIL_MEMBER_RESPONSE>This is a comprehensive analysis of the council member findings. The investigation reveals multiple significant patterns across the entire codebase that require careful refactoring.</COUNCIL_MEMBER_RESPONSE> More text after.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { log, normalizeSDKResponse } from "../../shared"
|
||||
import { hasCouncilResponseTag } from "./council-continuation-enforcer"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
const COUNCIL_RESPONSE_TAG = "</COUNCIL_MEMBER_RESPONSE>"
|
||||
|
||||
export async function sessionHasCouncilResponse(
|
||||
client: OpencodeClient,
|
||||
sessionID: string,
|
||||
@@ -20,18 +19,7 @@ export async function sessionHasCouncilResponse(
|
||||
{ preferResponseOnMissingData: true },
|
||||
)
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i]
|
||||
if (msg.info?.role !== "assistant") continue
|
||||
const parts = msg.parts ?? []
|
||||
for (const part of parts) {
|
||||
if (part.type === "text" && part.text?.includes(COUNCIL_RESPONSE_TAG)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return hasCouncilResponseTag(messages)
|
||||
} catch (error) {
|
||||
log("[council-response-checker] Error checking session for response tag:", {
|
||||
sessionID,
|
||||
|
||||
@@ -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,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,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")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user