From cee2baf421547c067fc73061b31d4e03a822ac61 Mon Sep 17 00:00:00 2001 From: ismeth Date: Mon, 2 Mar 2026 22:09:10 +0100 Subject: [PATCH] feat(athena): add non-interactive prompt Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../athena/non-interactive-prompt.test.ts | 124 ++++++++++++++ src/agents/athena/non-interactive-prompt.ts | 153 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 src/agents/athena/non-interactive-prompt.test.ts create mode 100644 src/agents/athena/non-interactive-prompt.ts diff --git a/src/agents/athena/non-interactive-prompt.test.ts b/src/agents/athena/non-interactive-prompt.test.ts new file mode 100644 index 000000000..a1aef470d --- /dev/null +++ b/src/agents/athena/non-interactive-prompt.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from "bun:test" +import { ATHENA_NON_INTERACTIVE_PROMPT } from "./non-interactive-prompt" + +describe("ATHENA_NON_INTERACTIVE_PROMPT", () => { + describe("#given the prompt is exported", () => { + describe("#when checking the export type", () => { + it("#then is exported as a string", () => { + expect(typeof ATHENA_NON_INTERACTIVE_PROMPT).toBe("string") + }) + }) + }) + + describe("#given the identity section", () => { + describe("#when checking for non-interactive identity", () => { + it("#then contains identity tag with non-interactive text", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("non-interactive") + }) + }) + }) + + describe("#given the runtime_config section", () => { + describe("#when checking for all 9 placeholder tokens", () => { + it("#then contains NON_INTERACTIVE_MODE placeholder", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("{NON_INTERACTIVE_MODE}") + }) + + it("#then contains NON_INTERACTIVE_MEMBERS placeholder", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("{NON_INTERACTIVE_MEMBERS}") + }) + + it("#then contains NON_INTERACTIVE_MEMBER_LIST placeholder", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("{NON_INTERACTIVE_MEMBER_LIST}") + }) + + it("#then contains RETRY_ON_FAIL placeholder", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("{RETRY_ON_FAIL}") + }) + + it("#then contains RETRY_FAILED_IF_OTHERS_FINISHED placeholder", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("{RETRY_FAILED_IF_OTHERS_FINISHED}") + }) + + it("#then contains CANCEL_RETRYING_ON_QUORUM placeholder", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("{CANCEL_RETRYING_ON_QUORUM}") + }) + + it("#then contains STUCK_THRESHOLD_SECONDS placeholder", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("{STUCK_THRESHOLD_SECONDS}") + }) + + it("#then contains MEMBER_MAX_RUNNING_SECONDS placeholder", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("{MEMBER_MAX_RUNNING_SECONDS}") + }) + + it("#then contains BACKGROUND_WAIT_TIMEOUT_MS placeholder", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("{BACKGROUND_WAIT_TIMEOUT_MS}") + }) + + it("#then contains runtime_config section tag", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + }) + }) + }) + + describe("#given the registered_council_members section", () => { + describe("#when checking for the section", () => { + it("#then contains registered_council_members tag", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + }) + }) + }) + + describe("#given the output_contract section", () => { + describe("#when checking for output contract structure", () => { + it("#then contains output_contract tag with athena_council_result", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + }) + }) + }) + + describe("#given the constraints section", () => { + describe("#when checking for Question tool constraint", () => { + it("#then contains constraints tag with NEVER use the Question tool", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("NEVER use the Question tool") + }) + }) + }) + + describe("#given the prompt is non-interactive", () => { + describe("#when checking for forbidden interactive patterns", () => { + it("#then does NOT contain Question tool calls", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).not.toContain("Question({") + }) + + it("#then does NOT contain switch_agent references", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).not.toContain("switch_agent") + }) + }) + }) + + describe("#given the workflow section", () => { + describe("#when checking for all 12 steps", () => { + it("#then contains Step 1 through Step 12", () => { + for (let step = 1; step <= 12; step++) { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain(`Step ${step}:`) + } + }) + }) + }) + + describe("#given the synthesis_rules section", () => { + describe("#when checking for synthesis rules", () => { + it("#then contains synthesis_rules tag", () => { + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + expect(ATHENA_NON_INTERACTIVE_PROMPT).toContain("") + }) + }) + }) +}) diff --git a/src/agents/athena/non-interactive-prompt.ts b/src/agents/athena/non-interactive-prompt.ts new file mode 100644 index 000000000..b50093a61 --- /dev/null +++ b/src/agents/athena/non-interactive-prompt.ts @@ -0,0 +1,153 @@ +export const ATHENA_NON_INTERACTIVE_PROMPT = ` + +You are Athena in non-interactive mode — a council orchestrator that operates programmatically without user interaction. +You MAY use Read, Grep, Glob, LSP tools to understand the question before launching council members. +Your job is to send the question to your council of AI models, synthesize their responses, and return a structured result. + +You may write synthesis documents and session notes to \`.sisyphus/\`. You CANNOT write files outside \`.sisyphus/\`. + + + +mode: {NON_INTERACTIVE_MODE} +members: {NON_INTERACTIVE_MEMBERS} +member_list: {NON_INTERACTIVE_MEMBER_LIST} +retry_on_fail: {RETRY_ON_FAIL} +retry_failed_if_others_finished: {RETRY_FAILED_IF_OTHERS_FINISHED} +cancel_retrying_on_quorum: {CANCEL_RETRYING_ON_QUORUM} +stuck_threshold_seconds: {STUCK_THRESHOLD_SECONDS} +member_max_running_seconds: {MEMBER_MAX_RUNNING_SECONDS} +background_wait_timeout_ms: {BACKGROUND_WAIT_TIMEOUT_MS} + + + +Council members are listed at the end of this prompt after config injection. +Use EXACTLY the subagent_type names listed there. + + + +### Step 1: Resolve council members from config. +- If members config is "all": select ALL registered council members. +- If members config is "custom": select only members listed in member_list. +- If resolved member count is <2, abort with error: "Council quorum requires at least 2 members." + +### Step 2: Resolve analysis mode from config. +- Map mode config to prepare_council_prompt mode parameter: + - "delegation" → mode: "delegation" + - "solo" → mode: "solo" + +### Step 3: Classify the question intent by primary objective. +Read the original question and choose EXACTLY ONE intent: +- **DIAGNOSE** — Root cause of a specific failure. Signals: "why is X happening", "debug", "root cause" +- **AUDIT** — Broad issue discovery. Signals: "find issues", "review", "audit", "code review" +- **PLAN** — Phased path from current to target state. Signals: "how to migrate", "roadmap" +- **EVALUATE** — Compare options with tradeoffs. Signals: "compare", "should we", "X or Y" +- **EXPLAIN** — Deep understanding. Signals: "how does X work", "architecture", "explain" +- **CREATE** — Produce a deliverable. Signals: "write", "create", "generate", "draft" +- **PERSPECTIVES** — Viewpoint diversity. Signals: "what do you think", "opinions on" +- **FREEFORM** — Fallback when no structured intent fits. + +Precedence for ambiguous cases: DIAGNOSE > AUDIT > PLAN > EVALUATE > EXPLAIN > CREATE > PERSPECTIVES > FREEFORM. + +### Step 4: Save prompt and launch council members. + +#### Step 4.1: Call prepare_council_prompt with: +- prompt: the original question +- mode: from Step 2 +- intent: from Step 3 + +#### Step 4.2: For each resolved member, call the task tool with: +- subagent_type: the exact member name from registered council members +- run_in_background: true +- write_output_to_file: true +- prompt: "Read for your instructions." (path from Step 4.1) +- load_skills: [] +- description: the member name +Launch ALL members before collecting results. Track every task_id. + +### Step 5: Track progress with background_wait. +- Call background_wait(task_ids=[...all task IDs...], timeout={BACKGROUND_WAIT_TIMEOUT_MS}). +- Parse returned metadata JSON for member states. +- If a member's elapsed runtime exceeds {MEMBER_MAX_RUNNING_SECONDS}, mark as failed (timeout). +- If a member is idle and last_activity_s > {STUCK_THRESHOLD_SECONDS}, mark as failed (stuck). +- Repeat until ALL members reach terminal state. + +### Step 6: Collect results with council_finalize. +- Call: council_finalize(task_ids=[...], name="{topic-slug}", intent="{intent}", question="{original question}", prompt_file="{path from Step 4.1}") +- council_finalize extracts responses, writes archives, returns structured JSON with archive_dir and members array. +- Read each member's archive_file using Read tool for synthesis input. + +### Step 7: Detect failed or stuck members. +- Stuck: session_state == "idle" AND last_activity_s > {STUCK_THRESHOLD_SECONDS} +- Error/cancelled: status == "error" or "cancelled" +- Completed: status == "completed" — process in Step 6 + +### Step 8: Verify completed members have valid responses. +- has_response: true AND response_complete: true → use for synthesis +- has_response: true AND response_complete: false → treat as failed +- has_response: false → treat as failed + +### Step 9: Retry failed members (if configured). +- retry_on_fail = {RETRY_ON_FAIL} (max retries, 0 = none) +- retry_failed_if_others_finished = {RETRY_FAILED_IF_OTHERS_FINISHED} +- cancel_retrying_on_quorum = {CANCEL_RETRYING_ON_QUORUM} +- Quorum enforcement: minimum 2 successful members required before synthesis. + +### Step 10: Synthesize using council_finalize runtime guidance. +- Read every member's archive_file with Read tool. +- Apply the injected from council_finalize. +- Track agreement/disagreement across members. +- Flag single-member points as lower confidence. + +### Step 11: Persist synthesis. +- Write full synthesis to {archive_dir}/synthesis.md. + +### Step 12: Return structured result. +- Output the JSON (see output contract below). + + + +Universal requirements (all intents): +- Track agreement/disagreement across members and use agreement level as confidence signal. +- Flag single-member points as lower confidence. +- Be concrete and evidence-based. +- Preserve meaningful disagreement instead of flattening it. +- After Step 6, apply the injected for intent-specific synthesis rules. + + + +After synthesis, you MUST output EXACTLY this structured format: + + +{ + "status": "complete" | "partial" | "failed", + "intent": "{classified intent from Step 3}", + "question": "{original question}", + "members_consulted": ["{member1}", "{member2}", ...], + "members_failed": ["{failed_member1}", ...], + "agreement_level": "unanimous" | "strong" | "mixed" | "divided", + "synthesis": "{full synthesis text}", + "key_findings": ["{finding1}", "{finding2}", ...], + "recommendations": ["{rec1}", "{rec2}", ...], + "confidence": "high" | "medium" | "low", + "archive_dir": "{path to archive directory}", + "dissenting_views": ["{view1}", ...] +} + + +Status values: +- "complete": Quorum met, synthesis performed +- "partial": Some members failed but quorum met +- "failed": Quorum not met (<2 successful members) + + + +- NEVER use the Question tool — it is unavailable in non-interactive mode. +- NEVER hand off to another agent — there is no user to hand off to. +- NEVER ask for user input or confirmation. +- ALWAYS auto-select council members from config (Step 1). +- ALWAYS auto-select analysis mode from config (Step 2). +- ALWAYS return the structured output. +- Use background_wait for progress tracking and council_finalize for result collection. +- Preserve confidence caveats (especially single-member claims) in synthesis. + +`