diff --git a/src/agents/athena/athena-config-injection.test.ts b/src/agents/athena/athena-config-injection.test.ts
index cc2155b22..c11afc379 100644
--- a/src/agents/athena/athena-config-injection.test.ts
+++ b/src/agents/athena/athena-config-injection.test.ts
@@ -1,3 +1,5 @@
+///
+
import { describe, expect, it } from "bun:test"
import { createAthenaAgent } from "./agent"
@@ -40,9 +42,9 @@ describe("Athena prompt config injection placeholders", () => {
it("#then uses sequential workflow step numbering", () => {
expect(athenaConfig.prompt).toContain("Step 12: Synthesize")
- expect(athenaConfig.prompt).toContain("Step 13: Determine the follow-up path")
- expect(athenaConfig.prompt).toContain("Step 14: ACTIONABLE findings")
- expect(athenaConfig.prompt).toContain("Step 15: INFORMATIONAL findings")
+ expect(athenaConfig.prompt).toContain("Step 13: Determine follow-up path from council_finalize runtime guidance")
+ expect(athenaConfig.prompt).toContain("Step 14: Execute the runtime guidance action flow")
+ expect(athenaConfig.prompt).toContain("Step 15: Fallback behavior if runtime guidance is missing")
})
it("#then omits legacy mixed step labels", () => {
@@ -71,11 +73,10 @@ describe("Athena prompt config injection placeholders", () => {
expect(step2Index).toBeGreaterThan(step1Index)
})
- it("#then keeps edge-case disambiguation inside step 3 classification", () => {
+ it("#then uses primary-objective wording for step 3 classification", () => {
const prompt = athenaConfig.prompt ?? ""
- expect(prompt).toContain("Step 3: Classify the question intent.")
- expect(prompt).toContain("Classification disambiguation rule:")
- expect(prompt).not.toContain("Before selecting a route, apply this interpretation rule")
+ expect(prompt).toContain("Step 3: Classify the question intent by primary objective.")
+ expect(prompt).not.toContain("Classification disambiguation rule:")
})
it("#then requires question tool routing in self-answerable path", () => {
@@ -93,7 +94,7 @@ describe("Athena prompt config injection placeholders", () => {
})
it("#then keeps intent classification anchored to step 3 wording", () => {
- expect(athenaConfig.prompt).toContain("Step 3: Classify the question intent.")
+ expect(athenaConfig.prompt).toContain("Step 3: Classify the question intent by primary objective.")
expect(athenaConfig.prompt).toContain("Then proceed to Step 2.")
expect(athenaConfig.prompt).not.toContain("Then classify intent and proceed to Step 2.")
expect(athenaConfig.prompt).not.toContain("Classify intent immediately and proceed to Step 2.")
@@ -102,6 +103,12 @@ describe("Athena prompt config injection placeholders", () => {
it("#then excludes non-interactive mode branch from runtime prompt", () => {
expect(athenaConfig.prompt).not.toContain("Non-interactive mode (Question tool unavailable)")
})
+
+ it("#then requires passing intent to council_finalize and honoring injected runtime guidance", () => {
+ expect(athenaConfig.prompt).toContain("intent=\"{intent from Step 3}\"")
+ expect(athenaConfig.prompt).toContain("runtime guidance message injected by council_finalize")
+ expect(athenaConfig.prompt).toContain("")
+ })
})
})
})
diff --git a/src/agents/athena/council-runtime-guidance.ts b/src/agents/athena/council-runtime-guidance.ts
new file mode 100644
index 000000000..4a7ff3557
--- /dev/null
+++ b/src/agents/athena/council-runtime-guidance.ts
@@ -0,0 +1,562 @@
+const VALID_INTENTS = [
+ "DIAGNOSE",
+ "AUDIT",
+ "PLAN",
+ "EVALUATE",
+ "EXPLAIN",
+ "CREATE",
+ "PERSPECTIVES",
+ "FREEFORM",
+] as const
+
+export type CouncilIntent = (typeof VALID_INTENTS)[number]
+
+const RUNTIME_GUIDANCE_BY_INTENT: Record = {
+ DIAGNOSE: `
+
+Use DIAGNOSE synthesis.
+- Build: Symptom -> hypotheses -> root cause -> contributing factors -> recommended fix.
+- Anchor confidence to agreement level and evidence quality.
+
+
+
+Path type: ACTIONABLE.
+
+1) Treat DIAGNOSE as a single-incident flow.
+- Identify the primary root-cause fix candidate as one scoped item.
+
+2) If no actionable fix candidate is identified, ask:
+Question({
+ questions: [{
+ question: "No actionable findings were identified. What should we do next?",
+ header: "Next Step",
+ options: [
+ { label: "Ask follow-up", description: "Ask a clarifying question and run another council pass" },
+ { label: "Done", description: "No further action needed" }
+ ],
+ multiple: false
+ }]
+})
+
+3) If an actionable fix candidate exists, ask action directly (no findings multi-select):
+Question({
+ questions: [{
+ question: "How should we execute this diagnosis fix?",
+ header: "Action",
+ options: [
+ { label: "Implement (Hephaestus)", description: "Hand off to Hephaestus for direct implementation" },
+ { label: "Implement (Sisyphus)", description: "Hand off to Sisyphus for implementation" },
+ { label: "Implement (Sisyphus ultrawork)", description: "Hand off to Sisyphus with ultrawork mode" },
+ { label: "No action", description: "Review only - no delegation" }
+ ],
+ multiple: false
+ }]
+})
+
+4) Execute selected action:
+- Implement (Hephaestus) -> switch_agent(agent="hephaestus")
+- Implement (Sisyphus) -> switch_agent(agent="sisyphus")
+- Implement (Sisyphus ultrawork) -> switch_agent(agent="sisyphus") and prefix the handoff context with "ultrawork "
+- No action -> acknowledge and end
+`,
+
+ AUDIT: `
+
+Use AUDIT synthesis.
+- Output numbered findings grouped by confidence: unanimous, majority, minority, solo.
+- For each finding: issue, impact, evidence, fix direction.
+
+
+
+Path type: ACTIONABLE.
+
+1) Build actionable findings list from audit synthesis.
+
+2) Ask how the user wants to process findings:
+Question({
+ questions: [{
+ question: "How would you like to process the findings?",
+ header: "Processing Mode",
+ options: [
+ { label: "One by one", description: "Review findings individually with per-finding decisions" },
+ { label: "By severity/urgency", description: "Select findings by criticality first" },
+ { label: "By quorum", description: "Select findings by agreement level" }
+ ],
+ multiple: false
+ }]
+})
+
+3) Branch by processing mode.
+
+Mode: One by one
+- Process findings in batches using ONE Question call per batch.
+- Default batch size: 3 findings per batch.
+- Hard cap: 5 findings only when findings are short and options are limited.
+- For each finding in the batch, provide dynamic options from synthesis (for example A/B/C fix options) plus:
+ - Skip
+ - Defer
+ - Stop review
+- Example Question tool call (batch of 3 findings):
+Question({
+ questions: [
+ {
+ question: "Finding #10: choose how to proceed.",
+ header: "#10 Action",
+ options: [
+ { label: "Option A", description: "Apply fix option A from synthesis" },
+ { label: "Option B", description: "Apply fix option B from synthesis" },
+ { label: "Skip", description: "Do not act on this finding now" },
+ { label: "Defer", description: "Keep for later" },
+ { label: "Stop review", description: "End one-by-one processing" }
+ ],
+ multiple: false
+ },
+ {
+ question: "Finding #11: choose how to proceed.",
+ header: "#11 Action",
+ options: [
+ { label: "Option A", description: "Apply fix option A from synthesis" },
+ { label: "Option C", description: "Apply fix option C from synthesis" },
+ { label: "Skip", description: "Do not act on this finding now" },
+ { label: "Defer", description: "Keep for later" },
+ { label: "Stop review", description: "End one-by-one processing" }
+ ],
+ multiple: false
+ },
+ {
+ question: "Finding #12: choose how to proceed.",
+ header: "#12 Action",
+ options: [
+ { label: "Option B", description: "Apply fix option B from synthesis" },
+ { label: "Skip", description: "Do not act on this finding now" },
+ { label: "Defer", description: "Keep for later" },
+ { label: "Stop review", description: "End one-by-one processing" }
+ ],
+ multiple: false
+ }
+ ]
+})
+- Keep free-form answers enabled (user may type custom choices like "#10:A, #11:skip"). Parse them.
+- Continue batch-by-batch until user stops or all findings are processed.
+
+Mode: By severity/urgency
+- Ask multi-select severity buckets using only non-empty groups.
+- Typical groups: Critical, High, Medium, Low.
+- Example Question tool call:
+Question({
+ questions: [{
+ question: "Which findings should we act on by severity? You can also type specific finding numbers (e.g. #1, #3, #7).",
+ header: "Select Findings",
+ options: [
+ // Include ONLY severities that actually have findings. Skip empty ones.
+ // Replace N with the actual count for each category.
+ { label: "All Critical (N)", description: "Highest urgency findings" },
+ { label: "All High (N)", description: "High-priority findings" },
+ { label: "All Medium (N)", description: "Medium-priority findings" },
+ { label: "All Low (N)", description: "Lower-priority findings" },
+ ],
+ multiple: true
+ }]
+})
+- Resolve selected buckets into concrete finding IDs.
+
+Mode: By quorum
+- Ask multi-select quorum buckets using only non-empty groups.
+- Typical groups: Unanimous, Majority, Minority, Solo.
+- Example Question tool call:
+Question({
+ questions: [{
+ question: "Which findings should we act on? You can also type specific finding numbers (e.g. #1, #3, #7).",
+ header: "Select Findings",
+ options: [
+ // Include ONLY categories that actually have findings. Skip empty ones.
+ // Replace N with the actual count for each category.
+ { label: "All Unanimous (N)", description: "Findings agreed on by all members" },
+ { label: "All Majority (N)", description: "Findings agreed on by most members" },
+ { label: "All Minority (N)", description: "Findings from 2+ members - higher false-positive risk" },
+ { label: "All Solo (N)", description: "Single-member findings - potential false positives" },
+ ],
+ multiple: true
+ }]
+})
+- Resolve selected buckets into concrete finding IDs.
+
+4) If actionable findings count is 0 OR user selected no findings, do NOT ask execution action. Ask:
+Question({
+ questions: [{
+ question: "No findings were selected for action. What should we do next?",
+ header: "Next Step",
+ options: [
+ { label: "Ask follow-up", description: "Ask a clarifying question and run another council pass" },
+ { label: "Done", description: "No further action needed" }
+ ],
+ multiple: false
+ }]
+})
+
+5) If selected findings exist, ask what action to take on the selected findings:
+Question({
+ questions: [{
+ question: "How should we handle the selected findings?",
+ header: "Action",
+ options: [
+ { label: "Fix now (Atlas)", description: "Hand off to Atlas for direct implementation" },
+ { label: "Create plan (Prometheus)", description: "Hand off to Prometheus for planning and phased execution" },
+ { label: "No action", description: "Review only - no delegation" }
+ ],
+ multiple: false
+ }]
+})
+
+6) Execute selected action:
+- Fix now (Atlas) -> switch_agent(agent="atlas") with ONLY selected findings
+- Create plan (Prometheus) -> switch_agent(agent="prometheus") with ONLY selected findings
+- No action -> acknowledge and end
+`,
+
+ PLAN: `
+
+Use PLAN synthesis.
+- Consolidate into one execution-ready phased plan.
+- For each phase include: goal, tasks, dependencies, risks, effort estimate, and exit criteria.
+- If members disagree on sequencing or strategy, preserve alternatives and pick a default recommendation with rationale.
+- End with critical path and immediate first step.
+
+
+
+Path type: PLAN_EXECUTION.
+
+1) Ask what to do with this plan:
+Question({
+ questions: [{
+ question: "What should we do with this plan?",
+ header: "Plan Next Step",
+ options: [
+ { label: "Execute full plan (Prometheus)", description: "Hand off all phases to Prometheus for execution" },
+ { label: "Execute selected phase (Prometheus)", description: "Choose one phase and execute only that phase first" },
+ { label: "Write to document", description: "Write the plan under .sisyphus/athena/notes/{council-session-name}" },
+ { label: "Ask follow-up", description: "Ask another planning question" },
+ { label: "Done", description: "No further action needed" }
+ ],
+ multiple: false
+ }]
+})
+
+2) If user chooses Execute selected phase (Prometheus), ask:
+Question({
+ questions: [{
+ question: "Which phase should we execute first?",
+ header: "Select Phase",
+ options: [
+ // Build from synthesized plan phases (for example: Phase 1, Phase 2, Phase 3).
+ // Include concise phase goal in each description.
+ ],
+ multiple: false
+ }]
+})
+
+3) Execute selected action:
+- Execute full plan (Prometheus) -> switch_agent(agent="prometheus") with full synthesized plan.
+- Execute selected phase (Prometheus) -> switch_agent(agent="prometheus") with only the selected phase plus dependencies.
+- Write to document -> write the document directly to ".sisyphus/athena/notes/{council-session-name}" and then report the exact path to the user.
+- Ask follow-up -> ask user then restart at Step 3.
+- Done -> acknowledge and end.
+`,
+
+ EVALUATE: `
+
+Use EVALUATE synthesis.
+- Compare options against explicit criteria.
+- Surface tradeoffs and finish with a primary recommendation plus fallback conditions.
+- State confidence and the key uncertainty that could change the recommendation.
+
+
+
+Path type: INFORMATIONAL.
+
+1) Ask what to do with the evaluation:
+Question({
+ questions: [{
+ question: "What should we do with this evaluation?",
+ header: "Evaluation Next Step",
+ options: [
+ { label: "Adopt option -> create plan (Prometheus)", description: "Turn a selected option into an execution plan" },
+ { label: "Adopt option -> implement now", description: "Implement a selected option immediately" },
+ { label: "Write to document", description: "Write under .sisyphus/athena/notes/{council-session-name}" },
+ { label: "Ask follow-up", description: "Ask another comparison question" },
+ { label: "Done", description: "No further action needed" }
+ ],
+ multiple: false
+ }]
+})
+
+2) If user chooses either adopt-option path, ask:
+Question({
+ questions: [{
+ question: "Which option should we adopt?",
+ header: "Select Option",
+ options: [
+ // Build from synthesized options list (e.g., Option A, Option B, Option C).
+ ],
+ multiple: false
+ }]
+})
+
+3) If user chooses "Adopt option -> implement now", ask execution agent:
+Question({
+ questions: [{
+ question: "Which execution agent should implement the selected option?",
+ header: "Execution Agent",
+ options: [
+ { label: "Hephaestus", description: "Direct implementation with Hephaestus" },
+ { label: "Sisyphus", description: "Implementation with Sisyphus" },
+ { label: "Sisyphus ultrawork", description: "Implementation with Sisyphus using ultrawork mode" }
+ ],
+ multiple: false
+ }]
+})
+
+4) Execute selected action:
+- Adopt option -> create plan (Prometheus) -> switch_agent(agent="prometheus") with selected option and rationale.
+- Adopt option -> implement now + Hephaestus -> switch_agent(agent="hephaestus") with selected option.
+- Adopt option -> implement now + Sisyphus -> switch_agent(agent="sisyphus") with selected option.
+- Adopt option -> implement now + Sisyphus ultrawork -> switch_agent(agent="sisyphus") and prefix handoff context with "ultrawork ".
+- Write to document -> write directly to ".sisyphus/athena/notes/{council-session-name}" and then report the exact path.
+- Ask follow-up -> ask user then restart at Step 3.
+- Done -> acknowledge and end.
+`,
+
+ EXPLAIN: `
+
+Use EXPLAIN synthesis.
+- Start with thesis.
+- Then mechanisms/interactions.
+- Then uncertainties and confidence.
+- Include a concise "why this matters" section tied to the user question.
+
+
+
+Path type: INFORMATIONAL.
+
+1) Ask what to do with the explanation:
+Question({
+ questions: [{
+ question: "What should we do with this explanation?",
+ header: "Explanation Next Step",
+ options: [
+ { label: "Convert to action plan (Prometheus)", description: "Turn insights into a phased plan" },
+ { label: "Write to document", description: "Write under .sisyphus/athena/notes/{council-session-name}" },
+ { label: "Ask follow-up", description: "Ask another explanatory question" },
+ { label: "Done", description: "No further action needed" }
+ ],
+ multiple: false
+ }]
+})
+
+2) Execute selected action:
+- Convert to action plan (Prometheus) -> switch_agent(agent="prometheus") with synthesized explanation and target outcome.
+- Write to document -> write directly to ".sisyphus/athena/notes/{council-session-name}" and then report the exact path.
+- Ask follow-up -> ask user then restart at Step 3.
+- Done -> acknowledge and end.
+`,
+
+ CREATE: `
+
+Use CREATE synthesis.
+- Preserve creations side-by-side as a gallery.
+- Do not collapse into a single merged artifact unless the user asks.
+- Assign stable IDs to creations (e.g., C1, C2, C3) so user selections are unambiguous.
+
+
+
+Path type: INFORMATIONAL.
+
+1) Ask which creations to carry forward:
+Question({
+ questions: [{
+ question: "Which creations should we carry forward? You can also type IDs (e.g. C1, C3).",
+ header: "Select Creations",
+ options: [
+ // Build from synthesized gallery IDs and titles.
+ ],
+ multiple: true
+ }]
+})
+
+2) If no creations are selected, ask:
+Question({
+ questions: [{
+ question: "No creations selected. What should we do next?",
+ header: "Next Step",
+ options: [
+ { label: "Ask follow-up", description: "Ask for clearer selection criteria" },
+ { label: "Done", description: "No further action needed" }
+ ],
+ multiple: false
+ }]
+})
+
+3) If creations are selected, ask:
+Question({
+ questions: [{
+ question: "How should we proceed with the selected creations?",
+ header: "Creation Next Step",
+ options: [
+ { label: "Implement selected creation (Hephaestus)", description: "Direct implementation with Hephaestus" },
+ { label: "Implement selected creation (Sisyphus)", description: "Implementation with Sisyphus" },
+ { label: "Implement selected creation (Sisyphus ultrawork)", description: "Implementation with Sisyphus using ultrawork mode" },
+ { label: "Write selected creation to document", description: "Write under .sisyphus/athena/notes/{council-session-name}" },
+ { label: "Ask follow-up", description: "Ask another creation-focused question" },
+ { label: "Done", description: "No further action needed" }
+ ],
+ multiple: false
+ }]
+})
+
+4) Execute selected action:
+- Implement selected creation (Hephaestus) -> switch_agent(agent="hephaestus") with only selected creation(s).
+- Implement selected creation (Sisyphus) -> switch_agent(agent="sisyphus") with only selected creation(s).
+- Implement selected creation (Sisyphus ultrawork) -> switch_agent(agent="sisyphus") and prefix handoff context with "ultrawork ", including only selected creation(s).
+- Write selected creation to document -> write directly to ".sisyphus/athena/notes/{council-session-name}" and then report the exact path.
+- Ask follow-up -> ask user then restart at Step 3.
+- Done -> acknowledge and end.
+`,
+
+ PERSPECTIVES: `
+
+Use PERSPECTIVES synthesis.
+- Map positions.
+- Identify tensions.
+- Evaluate evidence strength.
+- Take a final stance with conditions.
+- Name strongest counter-position and what evidence could overturn the final stance.
+
+
+
+Path type: INFORMATIONAL.
+
+1) Ask what to do with these perspectives:
+Question({
+ questions: [{
+ question: "What should we do with this perspectives analysis?",
+ header: "Perspectives Next Step",
+ options: [
+ { label: "Commit to stance -> create plan (Prometheus)", description: "Turn a chosen stance into a phased plan" },
+ { label: "Commit to stance -> implement now", description: "Implement based on a chosen stance immediately" },
+ { label: "Write to document", description: "Write under .sisyphus/athena/notes/{council-session-name}" },
+ { label: "Ask follow-up", description: "Ask another perspective question" },
+ { label: "Done", description: "No further action needed" }
+ ],
+ multiple: false
+ }]
+})
+
+2) If user chooses a commit-to-stance path, ask:
+Question({
+ questions: [{
+ question: "Which stance should we commit to?",
+ header: "Select Stance",
+ options: [
+ // Build from synthesized perspective labels and final stance.
+ ],
+ multiple: false
+ }]
+})
+
+3) If user chooses "Commit to stance -> implement now", ask execution agent:
+Question({
+ questions: [{
+ question: "Which execution agent should implement this stance?",
+ header: "Execution Agent",
+ options: [
+ { label: "Hephaestus", description: "Direct implementation with Hephaestus" },
+ { label: "Sisyphus", description: "Implementation with Sisyphus" },
+ { label: "Sisyphus ultrawork", description: "Implementation with Sisyphus using ultrawork mode" }
+ ],
+ multiple: false
+ }]
+})
+
+4) Execute selected action:
+- Commit to stance -> create plan (Prometheus) -> switch_agent(agent="prometheus") with selected stance and rationale.
+- Commit to stance -> implement now + Hephaestus -> switch_agent(agent="hephaestus") with selected stance.
+- Commit to stance -> implement now + Sisyphus -> switch_agent(agent="sisyphus") with selected stance.
+- Commit to stance -> implement now + Sisyphus ultrawork -> switch_agent(agent="sisyphus") and prefix handoff context with "ultrawork ".
+- Write to document -> write directly to ".sisyphus/athena/notes/{council-session-name}" and then report the exact path.
+- Ask follow-up -> ask user then restart at Step 3.
+- Done -> acknowledge and end.
+`,
+
+ FREEFORM: `
+
+Use FREEFORM synthesis.
+- Preserve meaningful diversity across member responses.
+- Avoid forcing rigid structure.
+- Produce a clear bottom-line answer plus notable alternatives.
+
+
+
+Path type: INFORMATIONAL.
+
+1) Ask what to do next:
+Question({
+ questions: [{
+ question: "What should we do with this result?",
+ header: "Next Step",
+ options: [
+ { label: "Create plan (Prometheus)", description: "Turn the result into a phased execution plan" },
+ { label: "Implement now", description: "Implement directly from this result" },
+ { label: "Write to document", description: "Write under .sisyphus/athena/notes/{council-session-name}" },
+ { label: "Ask follow-up", description: "Ask another question" },
+ { label: "Done", description: "No further action needed" }
+ ],
+ multiple: false
+ }]
+})
+
+2) If user chooses Implement now, ask execution agent:
+Question({
+ questions: [{
+ question: "Which execution agent should implement this?",
+ header: "Execution Agent",
+ options: [
+ { label: "Hephaestus", description: "Direct implementation with Hephaestus" },
+ { label: "Sisyphus", description: "Implementation with Sisyphus" },
+ { label: "Sisyphus ultrawork", description: "Implementation with Sisyphus using ultrawork mode" }
+ ],
+ multiple: false
+ }]
+})
+
+3) Execute selected action:
+- Create plan (Prometheus) -> switch_agent(agent="prometheus") with synthesized result.
+- Implement now + Hephaestus -> switch_agent(agent="hephaestus") with synthesized result.
+- Implement now + Sisyphus -> switch_agent(agent="sisyphus") with synthesized result.
+- Implement now + Sisyphus ultrawork -> switch_agent(agent="sisyphus") and prefix handoff context with "ultrawork ".
+- Write to document -> write directly to ".sisyphus/athena/notes/{council-session-name}" and then report the exact path.
+- Ask follow-up -> ask user then restart at Step 3.
+- Done -> acknowledge and end.
+`,
+}
+
+export function getValidCouncilIntents(): readonly CouncilIntent[] {
+ return VALID_INTENTS
+}
+
+export function resolveCouncilIntent(intent?: string): CouncilIntent | null {
+ if (!intent) return null
+ const normalized = intent.toUpperCase()
+ return (VALID_INTENTS as readonly string[]).includes(normalized)
+ ? (normalized as CouncilIntent)
+ : null
+}
+
+export function buildAthenaRuntimeGuidance(intent: CouncilIntent): string {
+ return [
+ "",
+ "source: council_finalize",
+ `intent: ${intent}`,
+ RUNTIME_GUIDANCE_BY_INTENT[intent].trim(),
+ "",
+ ].join("\n\n")
+}
diff --git a/src/agents/athena/index.ts b/src/agents/athena/index.ts
index 6d812c3b5..88adce0c2 100644
--- a/src/agents/athena/index.ts
+++ b/src/agents/athena/index.ts
@@ -1,3 +1,9 @@
export { createAthenaAgent, ATHENA_PROMPT_METADATA } from "./agent"
export { createCouncilMemberAgent, COUNCIL_MEMBER_PROMPT, COUNCIL_SOLO_ADDENDUM, COUNCIL_DELEGATION_ADDENDUM } from "./council-member-agent"
export { COUNCIL_INTENT_ADDENDUMS } from "./council-intent-addendums"
+export {
+ buildAthenaRuntimeGuidance,
+ getValidCouncilIntents,
+ resolveCouncilIntent,
+} from "./council-runtime-guidance"
+export type { CouncilIntent } from "./council-runtime-guidance"
diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts
index aa75c22db..f66cdc0a4 100644
--- a/src/plugin/tool-registry.ts
+++ b/src/plugin/tool-registry.ts
@@ -32,6 +32,7 @@ import {
createPrepareCouncilPromptTool,
} from "../tools"
import { createCouncilFinalize, createCouncilRead } from "../tools/council-archive"
+import { contextCollector } from "../features/context-injector"
import { getMainSessionID } from "../features/claude-code-session-state"
import { filterDisabledTools } from "../shared/disabled-tools"
import { isTaskSystemEnabled, log } from "../shared"
@@ -281,7 +282,7 @@ export function createToolRegistry(args: {
...taskToolsRecord,
...hashlineToolsRecord,
prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory),
- council_finalize: createCouncilFinalize(ctx.directory),
+ council_finalize: createCouncilFinalize(ctx.directory, { contextCollector }),
council_read: createCouncilRead(ctx.directory),
}
diff --git a/src/tools/council-archive/create-council-finalize.test.ts b/src/tools/council-archive/create-council-finalize.test.ts
index 8f6f8f14f..7bcd3522b 100644
--- a/src/tools/council-archive/create-council-finalize.test.ts
+++ b/src/tools/council-archive/create-council-finalize.test.ts
@@ -1,3 +1,5 @@
+///
+
import { describe, expect, it, beforeEach } from "bun:test"
import { mkdtemp, mkdir, writeFile, readFile } from "node:fs/promises"
import { join } from "node:path"
@@ -232,4 +234,165 @@ describe("createCouncilFinalize", () => {
expect(secondContent).toContain("Second response")
})
})
+
+ describe("#given intent-based runtime guidance injection", () => {
+ 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"),
+ "utf-8",
+ )
+
+ const calls: Array<{ sessionID: string; content: string; priority?: string; source: string; id: string }> = []
+ const collector = {
+ register: (sessionID: string, options: { id: string; source: string; content: string; priority?: string }) => {
+ calls.push({
+ sessionID,
+ id: options.id,
+ source: options.source,
+ content: options.content,
+ priority: options.priority,
+ })
+ },
+ }
+
+ const toolDef = createCouncilFinalize(tmpDir, { contextCollector: collector })
+ const result = await toolDef.execute(
+ { task_ids: ["bg_intent"], name: "intent", intent: "PLAN" },
+ mockCtx,
+ )
+
+ expect(() => JSON.parse(result)).not.toThrow()
+ expect(calls).toHaveLength(1)
+ expect(calls[0].sessionID).toBe(mockCtx.sessionID)
+ expect(calls[0].id).toBe("athena-runtime-guidance")
+ expect(calls[0].source).toBe("custom")
+ expect(calls[0].priority).toBe("critical")
+ expect(calls[0].content).toContain("")
+ expect(calls[0].content).toContain("intent: PLAN")
+ expect(calls[0].content).toContain("Execute full plan (Prometheus)")
+ expect(calls[0].content).toContain("Execute selected phase (Prometheus)")
+ expect(calls[0].content).toContain(".sisyphus/athena/notes/{council-session-name}")
+ expect(calls[0].content).not.toContain("Hand off to Atlas to save the plan as .md")
+ })
+
+ 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"),
+ "utf-8",
+ )
+
+ const calls: Array<{ content: string }> = []
+ const collector = {
+ register: (_sessionID: string, options: { content: string }) => {
+ calls.push({ content: options.content })
+ },
+ }
+
+ const toolDef = createCouncilFinalize(tmpDir, { contextCollector: collector })
+ const result = await toolDef.execute(
+ { task_ids: ["bg_diagnose"], name: "diagnose", intent: "DIAGNOSE" },
+ mockCtx,
+ )
+
+ expect(() => JSON.parse(result)).not.toThrow()
+ expect(calls).toHaveLength(1)
+ expect(calls[0].content).toContain("Implement (Hephaestus)")
+ expect(calls[0].content).toContain("Implement (Sisyphus)")
+ expect(calls[0].content).toContain("Implement (Sisyphus ultrawork)")
+ expect(calls[0].content).toContain("switch_agent(agent=\"hephaestus\")")
+ expect(calls[0].content).toContain("switch_agent(agent=\"sisyphus\")")
+ expect(calls[0].content).toContain("prefix the handoff context with \"ultrawork \"")
+ expect(calls[0].content).not.toContain("Fix now (Atlas)")
+ expect(calls[0].content).not.toContain("Create plan (Prometheus)")
+ })
+
+ 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"),
+ "utf-8",
+ )
+
+ const calls: Array<{ content: string }> = []
+ const collector = {
+ register: (_sessionID: string, options: { content: string }) => {
+ calls.push({ content: options.content })
+ },
+ }
+
+ const toolDef = createCouncilFinalize(tmpDir, { contextCollector: collector })
+ const result = await toolDef.execute(
+ { task_ids: ["bg_audit"], name: "audit", intent: "AUDIT" },
+ mockCtx,
+ )
+
+ expect(() => JSON.parse(result)).not.toThrow()
+ expect(calls).toHaveLength(1)
+ expect(calls[0].content).toContain("How would you like to process the findings?")
+ expect(calls[0].content).toContain("One by one")
+ expect(calls[0].content).toContain("By severity/urgency")
+ expect(calls[0].content).toContain("By quorum")
+ expect(calls[0].content).toContain("Default batch size: 3 findings per batch")
+ expect(calls[0].content).toContain("Hard cap: 5 findings")
+ expect(calls[0].content).toContain("Example Question tool call (batch of 3 findings)")
+ expect(calls[0].content).toContain("Finding #10: choose how to proceed.")
+ expect(calls[0].content).toContain("#10 Action")
+ expect(calls[0].content).toContain("Stop review")
+ expect(calls[0].content).toContain("#10:A, #11:skip")
+ expect(calls[0].content).toContain("Which findings should we act on by severity?")
+ expect(calls[0].content).toContain("All Critical (N)")
+ expect(calls[0].content).toContain("All High (N)")
+ expect(calls[0].content).toContain("All Medium (N)")
+ expect(calls[0].content).toContain("All Low (N)")
+ expect(calls[0].content).toContain("Which findings should we act on? You can also type specific finding numbers")
+ expect(calls[0].content).toContain("All Unanimous (N)")
+ expect(calls[0].content).toContain("All Majority (N)")
+ expect(calls[0].content).toContain("All Minority (N)")
+ expect(calls[0].content).toContain("All Solo (N)")
+ expect(calls[0].content).toContain("Fix now (Atlas)")
+ expect(calls[0].content).toContain("Create plan (Prometheus)")
+ })
+
+ 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"),
+ "utf-8",
+ )
+
+ const calls: Array<{ content: string }> = []
+ const collector = {
+ register: (_sessionID: string, options: { content: string }) => {
+ calls.push({ content: options.content })
+ },
+ }
+
+ const toolDef = createCouncilFinalize(tmpDir, { contextCollector: collector })
+ const result = await toolDef.execute(
+ { task_ids: ["bg_eval"], name: "eval", intent: "EVALUATE" },
+ mockCtx,
+ )
+
+ expect(() => JSON.parse(result)).not.toThrow()
+ expect(calls).toHaveLength(1)
+ expect(calls[0].content).toContain("What should we do with this evaluation?")
+ expect(calls[0].content).toContain("Adopt option -> create plan (Prometheus)")
+ expect(calls[0].content).toContain("Adopt option -> implement now")
+ expect(calls[0].content).toContain(".sisyphus/athena/notes/{council-session-name}")
+ expect(calls[0].content).not.toContain("Write to document (Atlas)")
+ })
+
+ it("#then rejects invalid intent values", async () => {
+ const toolDef = createCouncilFinalize(tmpDir)
+ const result = await toolDef.execute(
+ { task_ids: ["bg_none"], name: "invalid-intent", intent: "NOT_A_REAL_INTENT" },
+ mockCtx,
+ )
+
+ expect(result).toContain("Invalid intent")
+ expect(result).toContain("NOT_A_REAL_INTENT")
+ })
+ })
})
diff --git a/src/tools/council-archive/create-council-finalize.ts b/src/tools/council-archive/create-council-finalize.ts
index 7de8f78c8..edcd337ab 100644
--- a/src/tools/council-archive/create-council-finalize.ts
+++ b/src/tools/council-archive/create-council-finalize.ts
@@ -3,7 +3,13 @@ import { readFile, writeFile, mkdir, rename } from "node:fs/promises"
import { join, isAbsolute, resolve } from "node:path"
import { randomBytes } from "node:crypto"
import { extractCouncilResponse } from "./council-response-extractor"
+import {
+ buildAthenaRuntimeGuidance,
+ getValidCouncilIntents,
+ resolveCouncilIntent,
+} from "../../agents/athena"
import { log } from "../../shared/logger"
+import type { ContextCollector } from "../../features/context-injector"
import type { CouncilFinalizeArgs, CouncilMemberResult, CouncilFinalizeResult } from "./types"
interface MetaMember {
@@ -16,6 +22,12 @@ interface MetaMember {
response_complete: boolean
}
+type RegisterContext = Pick
+
+type CouncilFinalizeToolContext = {
+ sessionID?: string
+}
+
function slugify(text: string): string {
return text
.toLowerCase()
@@ -66,19 +78,46 @@ function formatMetaYaml(archiveName: string, createdAt: string, members: MetaMem
return lines.join("\n") + "\n"
}
-export function createCouncilFinalize(basePath?: string): ToolDefinition {
+export function createCouncilFinalize(
+ basePath?: string,
+ options?: { contextCollector?: RegisterContext }
+): ToolDefinition {
+ const collector = options?.contextCollector
+
return tool({
description:
- "Finalize council task outputs: extract COUNCIL_MEMBER_RESPONSE content from raw task output files, write per-member archive files, and create meta.yaml.",
+ "Finalize council task outputs: extract COUNCIL_MEMBER_RESPONSE content from raw task output files, write per-member archive files, inject intent-specific Athena runtime guidance, and create meta.yaml.",
args: {
task_ids: tool.schema
.array(tool.schema.string())
.describe("Array of background task IDs whose output files should be processed"),
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) {
+ async execute(args: CouncilFinalizeArgs, toolContext: CouncilFinalizeToolContext) {
+ const resolvedIntent = resolveCouncilIntent(args.intent)
+ if (args.intent && !resolvedIntent) {
+ return `Invalid intent: "${args.intent}". Valid intents: ${getValidCouncilIntents().join(", ")}.`
+ }
+
+ if (collector && resolvedIntent && toolContext.sessionID) {
+ collector.register(toolContext.sessionID, {
+ id: "athena-runtime-guidance",
+ source: "custom",
+ priority: "critical",
+ content: buildAthenaRuntimeGuidance(resolvedIntent),
+ metadata: {
+ intent: resolvedIntent,
+ source: "council_finalize",
+ },
+ })
+ }
+
const base = basePath ?? process.cwd()
const hexId = randomBytes(2).toString("hex")
const archiveName = `council-${args.name}-${hexId}`
diff --git a/src/tools/council-archive/types.ts b/src/tools/council-archive/types.ts
index db1e707dd..ae6e3d598 100644
--- a/src/tools/council-archive/types.ts
+++ b/src/tools/council-archive/types.ts
@@ -1,6 +1,7 @@
export interface CouncilFinalizeArgs {
task_ids: string[]
name: string
+ intent?: string
question?: string
prompt_file?: string
}
diff --git a/src/tools/switch-agent/tools.test.ts b/src/tools/switch-agent/tools.test.ts
index 2a91780aa..248e43262 100644
--- a/src/tools/switch-agent/tools.test.ts
+++ b/src/tools/switch-agent/tools.test.ts
@@ -86,6 +86,40 @@ describe("switch_agent tool", () => {
expect(entry?.agent).toBe("prometheus")
})
+ //#given valid hephaestus switch args
+ //#when execute is called
+ //#then it stores pending switch for hephaestus
+ test("should queue switch to hephaestus", async () => {
+ const tool = createToolWithMockClient()
+ const result = await tool.execute(
+ { agent: "Hephaestus", context: "Implement the selected diagnosis fix" },
+ toolContext
+ )
+
+ expect(result).toContain("hephaestus")
+ expect(result).toContain("switch")
+
+ const entry = consumePendingSwitch(sessionID)
+ expect(entry?.agent).toBe("hephaestus")
+ })
+
+ //#given valid sisyphus switch args
+ //#when execute is called
+ //#then it stores pending switch for sisyphus
+ test("should queue switch to sisyphus", async () => {
+ const tool = createToolWithMockClient()
+ const result = await tool.execute(
+ { agent: "Sisyphus", context: "Implement the selected diagnosis fix" },
+ toolContext
+ )
+
+ expect(result).toContain("sisyphus")
+ expect(result).toContain("switch")
+
+ const entry = consumePendingSwitch(sessionID)
+ expect(entry?.agent).toBe("sisyphus")
+ })
+
//#given an invalid agent name
//#when execute is called
//#then it returns an error