feat(athena): inject intent runtime guidance and action flows

This commit is contained in:
ismeth
2026-03-01 00:18:39 +01:00
committed by YeonGyu-Kim
parent ed2bb1ead7
commit c265200f5f
8 changed files with 825 additions and 12 deletions
@@ -1,3 +1,5 @@
/// <reference types="bun-types" />
import { describe, expect, it } from "bun:test" import { describe, expect, it } from "bun:test"
import { createAthenaAgent } from "./agent" import { createAthenaAgent } from "./agent"
@@ -40,9 +42,9 @@ describe("Athena prompt config injection placeholders", () => {
it("#then uses sequential workflow step numbering", () => { it("#then uses sequential workflow step numbering", () => {
expect(athenaConfig.prompt).toContain("Step 12: Synthesize") expect(athenaConfig.prompt).toContain("Step 12: Synthesize")
expect(athenaConfig.prompt).toContain("Step 13: Determine the follow-up path") expect(athenaConfig.prompt).toContain("Step 13: Determine follow-up path from council_finalize runtime guidance")
expect(athenaConfig.prompt).toContain("Step 14: ACTIONABLE findings") expect(athenaConfig.prompt).toContain("Step 14: Execute the runtime guidance action flow")
expect(athenaConfig.prompt).toContain("Step 15: INFORMATIONAL findings") expect(athenaConfig.prompt).toContain("Step 15: Fallback behavior if runtime guidance is missing")
}) })
it("#then omits legacy mixed step labels", () => { it("#then omits legacy mixed step labels", () => {
@@ -71,11 +73,10 @@ describe("Athena prompt config injection placeholders", () => {
expect(step2Index).toBeGreaterThan(step1Index) 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 ?? "" const prompt = athenaConfig.prompt ?? ""
expect(prompt).toContain("Step 3: Classify the question intent.") expect(prompt).toContain("Step 3: Classify the question intent by primary objective.")
expect(prompt).toContain("Classification disambiguation rule:") expect(prompt).not.toContain("Classification disambiguation rule:")
expect(prompt).not.toContain("Before selecting a route, apply this interpretation rule")
}) })
it("#then requires question tool routing in self-answerable path", () => { 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", () => { 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).toContain("Then proceed to Step 2.")
expect(athenaConfig.prompt).not.toContain("Then classify intent and 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.") 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", () => { it("#then excludes non-interactive mode branch from runtime prompt", () => {
expect(athenaConfig.prompt).not.toContain("Non-interactive mode (Question tool unavailable)") 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("<athena_runtime_guidance>")
})
}) })
}) })
}) })
@@ -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<CouncilIntent, string> = {
DIAGNOSE: `
<runtime_synthesis_rules>
Use DIAGNOSE synthesis.
- Build: Symptom -> hypotheses -> root cause -> contributing factors -> recommended fix.
- Anchor confidence to agreement level and evidence quality.
</runtime_synthesis_rules>
<runtime_action_paths>
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
</runtime_action_paths>`,
AUDIT: `
<runtime_synthesis_rules>
Use AUDIT synthesis.
- Output numbered findings grouped by confidence: unanimous, majority, minority, solo.
- For each finding: issue, impact, evidence, fix direction.
</runtime_synthesis_rules>
<runtime_action_paths>
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
</runtime_action_paths>`,
PLAN: `
<runtime_synthesis_rules>
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.
</runtime_synthesis_rules>
<runtime_action_paths>
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.
</runtime_action_paths>`,
EVALUATE: `
<runtime_synthesis_rules>
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.
</runtime_synthesis_rules>
<runtime_action_paths>
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.
</runtime_action_paths>`,
EXPLAIN: `
<runtime_synthesis_rules>
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.
</runtime_synthesis_rules>
<runtime_action_paths>
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.
</runtime_action_paths>`,
CREATE: `
<runtime_synthesis_rules>
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.
</runtime_synthesis_rules>
<runtime_action_paths>
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.
</runtime_action_paths>`,
PERSPECTIVES: `
<runtime_synthesis_rules>
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.
</runtime_synthesis_rules>
<runtime_action_paths>
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.
</runtime_action_paths>`,
FREEFORM: `
<runtime_synthesis_rules>
Use FREEFORM synthesis.
- Preserve meaningful diversity across member responses.
- Avoid forcing rigid structure.
- Produce a clear bottom-line answer plus notable alternatives.
</runtime_synthesis_rules>
<runtime_action_paths>
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.
</runtime_action_paths>`,
}
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 [
"<athena_runtime_guidance>",
"source: council_finalize",
`intent: ${intent}`,
RUNTIME_GUIDANCE_BY_INTENT[intent].trim(),
"</athena_runtime_guidance>",
].join("\n\n")
}
+6
View File
@@ -1,3 +1,9 @@
export { createAthenaAgent, ATHENA_PROMPT_METADATA } from "./agent" export { createAthenaAgent, ATHENA_PROMPT_METADATA } from "./agent"
export { createCouncilMemberAgent, COUNCIL_MEMBER_PROMPT, COUNCIL_SOLO_ADDENDUM, COUNCIL_DELEGATION_ADDENDUM } from "./council-member-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 { COUNCIL_INTENT_ADDENDUMS } from "./council-intent-addendums"
export {
buildAthenaRuntimeGuidance,
getValidCouncilIntents,
resolveCouncilIntent,
} from "./council-runtime-guidance"
export type { CouncilIntent } from "./council-runtime-guidance"
+2 -1
View File
@@ -32,6 +32,7 @@ import {
createPrepareCouncilPromptTool, createPrepareCouncilPromptTool,
} from "../tools" } from "../tools"
import { createCouncilFinalize, createCouncilRead } from "../tools/council-archive" import { createCouncilFinalize, createCouncilRead } from "../tools/council-archive"
import { contextCollector } from "../features/context-injector"
import { getMainSessionID } from "../features/claude-code-session-state" import { getMainSessionID } from "../features/claude-code-session-state"
import { filterDisabledTools } from "../shared/disabled-tools" import { filterDisabledTools } from "../shared/disabled-tools"
import { isTaskSystemEnabled, log } from "../shared" import { isTaskSystemEnabled, log } from "../shared"
@@ -281,7 +282,7 @@ export function createToolRegistry(args: {
...taskToolsRecord, ...taskToolsRecord,
...hashlineToolsRecord, ...hashlineToolsRecord,
prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory), prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory),
council_finalize: createCouncilFinalize(ctx.directory), council_finalize: createCouncilFinalize(ctx.directory, { contextCollector }),
council_read: createCouncilRead(ctx.directory), council_read: createCouncilRead(ctx.directory),
} }
@@ -1,3 +1,5 @@
/// <reference types="bun-types" />
import { describe, expect, it, beforeEach } from "bun:test" import { describe, expect, it, beforeEach } from "bun:test"
import { mkdtemp, mkdir, writeFile, readFile } from "node:fs/promises" import { mkdtemp, mkdir, writeFile, readFile } from "node:fs/promises"
import { join } from "node:path" import { join } from "node:path"
@@ -232,4 +234,165 @@ describe("createCouncilFinalize", () => {
expect(secondContent).toContain("Second response") 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("<athena_runtime_guidance>")
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")
})
})
}) })
@@ -3,7 +3,13 @@ import { readFile, writeFile, mkdir, rename } from "node:fs/promises"
import { join, isAbsolute, resolve } from "node:path" import { join, isAbsolute, resolve } from "node:path"
import { randomBytes } from "node:crypto" import { randomBytes } from "node:crypto"
import { extractCouncilResponse } from "./council-response-extractor" import { extractCouncilResponse } from "./council-response-extractor"
import {
buildAthenaRuntimeGuidance,
getValidCouncilIntents,
resolveCouncilIntent,
} from "../../agents/athena"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import type { ContextCollector } from "../../features/context-injector"
import type { CouncilFinalizeArgs, CouncilMemberResult, CouncilFinalizeResult } from "./types" import type { CouncilFinalizeArgs, CouncilMemberResult, CouncilFinalizeResult } from "./types"
interface MetaMember { interface MetaMember {
@@ -16,6 +22,12 @@ interface MetaMember {
response_complete: boolean response_complete: boolean
} }
type RegisterContext = Pick<ContextCollector, "register">
type CouncilFinalizeToolContext = {
sessionID?: string
}
function slugify(text: string): string { function slugify(text: string): string {
return text return text
.toLowerCase() .toLowerCase()
@@ -66,19 +78,46 @@ function formatMetaYaml(archiveName: string, createdAt: string, members: MetaMem
return lines.join("\n") + "\n" 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({ return tool({
description: 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: { args: {
task_ids: tool.schema task_ids: tool.schema
.array(tool.schema.string()) .array(tool.schema.string())
.describe("Array of background task IDs whose output files should be processed"), .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"), 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"), 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)"), 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 base = basePath ?? process.cwd()
const hexId = randomBytes(2).toString("hex") const hexId = randomBytes(2).toString("hex")
const archiveName = `council-${args.name}-${hexId}` const archiveName = `council-${args.name}-${hexId}`
+1
View File
@@ -1,6 +1,7 @@
export interface CouncilFinalizeArgs { export interface CouncilFinalizeArgs {
task_ids: string[] task_ids: string[]
name: string name: string
intent?: string
question?: string question?: string
prompt_file?: string prompt_file?: string
} }
+34
View File
@@ -86,6 +86,40 @@ describe("switch_agent tool", () => {
expect(entry?.agent).toBe("prometheus") 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 //#given an invalid agent name
//#when execute is called //#when execute is called
//#then it returns an error //#then it returns an error