2026-02-22 23:08:39 +01:00
|
|
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
|
2026-02-23 11:52:58 +01:00
|
|
|
import { randomUUID } from "node:crypto"
|
2026-02-24 15:12:05 +01:00
|
|
|
import { writeFile, unlink, mkdir, readdir, stat } from "node:fs/promises"
|
2026-02-22 23:08:39 +01:00
|
|
|
import { join } from "node:path"
|
|
|
|
|
import { log } from "../../shared/logger"
|
2026-02-27 16:46:28 +01:00
|
|
|
import { COUNCIL_SOLO_ADDENDUM, COUNCIL_DELEGATION_ADDENDUM, COUNCIL_INTENT_ADDENDUMS } from "../../agents/athena"
|
2026-02-22 23:08:39 +01:00
|
|
|
|
|
|
|
|
const CLEANUP_DELAY_MS = 30 * 60 * 1000
|
2026-02-23 12:46:44 +01:00
|
|
|
const COUNCIL_TMP_DIR = ".sisyphus/tmp"
|
2026-02-22 23:08:39 +01:00
|
|
|
|
2026-02-24 15:12:05 +01:00
|
|
|
const COUNCIL_FILE_PREFIX = "athena-council-"
|
|
|
|
|
|
|
|
|
|
async function cleanupStaleTempFiles(directory: string): Promise<void> {
|
|
|
|
|
const tmpDir = join(directory, COUNCIL_TMP_DIR)
|
|
|
|
|
try {
|
|
|
|
|
const files = await readdir(tmpDir)
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
if (!file.startsWith(COUNCIL_FILE_PREFIX) || !file.endsWith(".md")) continue
|
|
|
|
|
const filePath = join(tmpDir, file)
|
|
|
|
|
try {
|
|
|
|
|
const fileStat = await stat(filePath)
|
|
|
|
|
if (now - fileStat.mtimeMs > CLEANUP_DELAY_MS) {
|
|
|
|
|
await unlink(filePath)
|
|
|
|
|
log("[prepare-council-prompt] Cleaned up stale temp file", { filePath })
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// File may have been deleted between readdir and stat
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Directory may not exist yet — nothing to clean
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 12:46:44 +01:00
|
|
|
export function createPrepareCouncilPromptTool(directory: string): ToolDefinition {
|
2026-02-22 23:08:39 +01:00
|
|
|
const description = `Save a council analysis prompt to a temp file so council members can read it.
|
|
|
|
|
|
|
|
|
|
Athena-only tool. Saves the prompt once, then each council member task() call uses a short
|
|
|
|
|
"Read <path>" instruction instead of repeating the full question. This keeps task() calls
|
|
|
|
|
fast and small.
|
|
|
|
|
|
2026-02-23 19:42:56 +01:00
|
|
|
The "mode" parameter controls whether council members can delegate exploration to subagents:
|
|
|
|
|
- "solo" (default): Members do all exploration themselves. More thorough but uses more tokens.
|
|
|
|
|
- "delegation": Members can delegate to explore/librarian agents. Faster, lighter context.
|
|
|
|
|
|
2026-02-27 16:46:28 +01:00
|
|
|
The "intent" parameter controls the analysis framework injected into the prompt:
|
|
|
|
|
- "AUDIT" (default): Find issues, risks, violations with severity ratings.
|
|
|
|
|
- "EVALUATE": Compare options against criteria, surface tradeoffs.
|
|
|
|
|
- "PLAN": Define current state, target state, phased path.
|
|
|
|
|
- "EXPLAIN": Build understanding of mechanisms and relationships.
|
|
|
|
|
|
2026-02-22 23:08:39 +01:00
|
|
|
Returns the file path to reference in subsequent task() calls.`
|
|
|
|
|
|
2026-02-24 15:12:05 +01:00
|
|
|
cleanupStaleTempFiles(directory).catch((err) => {
|
|
|
|
|
log("[prepare-council-prompt] Startup cleanup failed", { error: String(err) })
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-22 23:08:39 +01:00
|
|
|
return tool({
|
|
|
|
|
description,
|
|
|
|
|
args: {
|
|
|
|
|
prompt: tool.schema.string().describe("The full analysis prompt/question for council members"),
|
2026-02-23 19:42:56 +01:00
|
|
|
mode: tool.schema.string().optional().describe('Analysis mode: "solo" (default) or "delegation"'),
|
2026-02-27 16:46:28 +01:00
|
|
|
intent: tool.schema.string().optional().describe('Question intent: "AUDIT", "EVALUATE", "PLAN", "EXPLAIN"'),
|
2026-02-22 23:08:39 +01:00
|
|
|
},
|
2026-02-27 16:46:28 +01:00
|
|
|
async execute(args: { prompt: string; mode?: string; intent?: string }) {
|
2026-02-22 23:08:39 +01:00
|
|
|
if (!args.prompt?.trim()) {
|
|
|
|
|
return "Prompt cannot be empty."
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 15:12:57 +01:00
|
|
|
if (args.mode !== undefined && args.mode !== "solo" && args.mode !== "delegation") {
|
|
|
|
|
return `Invalid mode: "${args.mode}". Valid modes: "solo", "delegation".`
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 16:46:28 +01:00
|
|
|
const validIntents = ["AUDIT", "EVALUATE", "PLAN", "EXPLAIN"]
|
|
|
|
|
if (args.intent !== undefined && !validIntents.includes(args.intent.toUpperCase())) {
|
|
|
|
|
return `Invalid intent: "${args.intent}". Valid intents: "AUDIT", "EVALUATE", "PLAN", "EXPLAIN".`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resolvedIntent = args.intent?.toUpperCase() ?? "AUDIT"
|
|
|
|
|
|
2026-02-23 19:42:56 +01:00
|
|
|
const mode = args.mode === "delegation" ? "delegation" : "solo"
|
2026-02-23 12:46:44 +01:00
|
|
|
|
2026-02-26 15:12:57 +01:00
|
|
|
try {
|
|
|
|
|
const tmpDir = join(directory, COUNCIL_TMP_DIR)
|
|
|
|
|
await mkdir(tmpDir, { recursive: true })
|
2026-02-23 12:46:44 +01:00
|
|
|
|
2026-02-26 15:12:57 +01:00
|
|
|
const filename = `athena-council-${randomUUID().slice(0, 8)}.md`
|
|
|
|
|
const filePath = join(tmpDir, filename)
|
|
|
|
|
|
|
|
|
|
const modeAddendum = mode === "delegation" ? COUNCIL_DELEGATION_ADDENDUM : COUNCIL_SOLO_ADDENDUM
|
2026-02-27 16:46:28 +01:00
|
|
|
const intentAddendum = COUNCIL_INTENT_ADDENDUMS[resolvedIntent] ?? COUNCIL_INTENT_ADDENDUMS["AUDIT"]
|
2026-02-26 15:12:57 +01:00
|
|
|
const content = `${modeAddendum}
|
2026-02-23 12:46:44 +01:00
|
|
|
|
2026-02-27 16:46:28 +01:00
|
|
|
${intentAddendum}
|
|
|
|
|
|
2026-02-23 12:46:44 +01:00
|
|
|
## Analysis Question
|
|
|
|
|
|
|
|
|
|
${args.prompt}`
|
2026-02-22 23:08:39 +01:00
|
|
|
|
2026-02-26 15:12:57 +01:00
|
|
|
await writeFile(filePath, content, "utf-8")
|
2026-02-22 23:08:39 +01:00
|
|
|
|
2026-02-26 15:12:57 +01:00
|
|
|
setTimeout(() => {
|
|
|
|
|
unlink(filePath).catch((err) => {
|
|
|
|
|
log("[prepare-council-prompt] Failed to clean up temp file", { filePath, error: String(err) })
|
|
|
|
|
})
|
|
|
|
|
}, CLEANUP_DELAY_MS)
|
2026-02-22 23:08:39 +01:00
|
|
|
|
2026-02-26 15:12:57 +01:00
|
|
|
log("[prepare-council-prompt] Saved prompt", { filePath, length: args.prompt.length, mode })
|
2026-02-22 23:08:39 +01:00
|
|
|
|
2026-02-27 16:46:28 +01:00
|
|
|
return `Council prompt saved to: ${filePath} (mode: ${mode}, intent: ${resolvedIntent})
|
2026-02-22 23:08:39 +01:00
|
|
|
|
|
|
|
|
Use this path in each council member's task() call:
|
|
|
|
|
- prompt: "Read ${filePath} for your instructions."
|
|
|
|
|
|
|
|
|
|
The file auto-deletes after 30 minutes.`
|
2026-02-26 15:12:57 +01:00
|
|
|
} catch (err) {
|
|
|
|
|
return `Error saving council prompt: ${String(err)}`
|
|
|
|
|
}
|
2026-02-22 23:08:39 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|