diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index b78510d16..2a9cc13f7 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -32,7 +32,6 @@ import { createPrepareCouncilPromptTool, } from "../tools" import { createCouncilFinalize } 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" @@ -282,7 +281,7 @@ export function createToolRegistry(args: { ...taskToolsRecord, ...hashlineToolsRecord, prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory), - council_finalize: createCouncilFinalize(ctx.directory, { contextCollector }), + council_finalize: createCouncilFinalize(ctx.directory), } for (const toolDefinition of Object.values(allTools)) { diff --git a/src/tools/council-archive/council-flow.integration.test.ts b/src/tools/council-archive/council-flow.integration.test.ts index eb0f2b5c4..9700165fc 100644 --- a/src/tools/council-archive/council-flow.integration.test.ts +++ b/src/tools/council-archive/council-flow.integration.test.ts @@ -11,6 +11,14 @@ import type { BackgroundOutputManager } from "../background-task/clients" import { createBackgroundWait } from "../background-task/create-background-wait" import { resetMessageCursor } from "../../shared/session-cursor" +function extractJson(output: string): string { + const marker = "" + const idx = output.indexOf(marker) + if (idx === -1) return output + const beforeMarker = output.substring(0, idx) + const lastNewlines = beforeMarker.lastIndexOf("\n\n") + return lastNewlines === -1 ? beforeMarker.trim() : output.substring(0, lastNewlines).trim() +} function mockTaskOutput(agent: string, responseBody: string, complete = true): string { const closing = complete ? "\n" : "" return [ @@ -107,7 +115,7 @@ describe("council archive integration flow", () => { { task_ids: agents.map((a) => a.id), name: "test", intent: "FREEFORM" }, toolContext, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.archive_dir).toMatch(/\.sisyphus\/athena\/council-test-[a-f0-9]{4}$/) expect(result.meta_file).toMatch(/meta\.yaml$/) @@ -155,7 +163,7 @@ describe("council archive integration flow", () => { { task_ids: [taskId], name: "partial", intent: "FREEFORM" }, toolContext, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) const member = result.members[0] expect(member.has_response).toBe(true) @@ -183,7 +191,7 @@ describe("council archive integration flow", () => { { task_ids: ["bg_notags"], name: "notags", intent: "FREEFORM" }, toolContext, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) const member = result.members[0] expect(member.has_response).toBe(false) @@ -211,7 +219,7 @@ describe("council archive integration flow", () => { { task_ids: ["bg_first", "bg_missing", "bg_third"], name: "partial", intent: "FREEFORM" }, toolContext, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.members).toHaveLength(3) @@ -244,7 +252,7 @@ describe("council archive integration flow", () => { { task_ids: [taskId], name: "large", intent: "FREEFORM" }, toolContext, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) const member = result.members[0] expect(member.has_response).toBe(true) @@ -283,7 +291,7 @@ describe("council archive integration flow", () => { }, toolContext, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) const metaContent = await readFile(join(tmpDir, result.meta_file), "utf-8") expect(metaContent).toContain("question: |") @@ -319,7 +327,7 @@ describe("council archive integration flow", () => { }, toolContext, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) const metaContent = await readFile(join(tmpDir, result.meta_file), "utf-8") expect(metaContent).toContain("question: |") diff --git a/src/tools/council-archive/create-council-finalize.test.ts b/src/tools/council-archive/create-council-finalize.test.ts index da741ba7b..bade4ec9a 100644 --- a/src/tools/council-archive/create-council-finalize.test.ts +++ b/src/tools/council-archive/create-council-finalize.test.ts @@ -26,6 +26,15 @@ function mockTaskOutput(agent: string, responseBody: string, complete = true): s ].join("\n") } +function extractJson(output: string): string { + const marker = "" + const idx = output.indexOf(marker) + if (idx === -1) return output + const beforeMarker = output.substring(0, idx) + const lastNewlines = beforeMarker.lastIndexOf("\n\n") + return lastNewlines === -1 ? beforeMarker.trim() : output.substring(0, lastNewlines).trim() +} + const mockCtx = { sessionID: "test-session", messageID: "test-message", @@ -62,7 +71,7 @@ describe("createCouncilFinalize", () => { { task_ids: agents.map((a) => a.id), name: "test", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.archive_dir).toMatch(/\.sisyphus\/athena\/council-test-[a-f0-9]{4}$/) expect(result.meta_file).toMatch(/\.sisyphus\/athena\/council-test-[a-f0-9]{4}\/meta\.yaml$/) @@ -108,7 +117,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_001", "bg_002", "bg_003"], name: "partial", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.members).toHaveLength(3) @@ -138,7 +147,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_large"], name: "large", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) const member = result.members[0] expect(member.has_response).toBe(true) @@ -177,7 +186,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_empty"], name: "empty", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) const member = result.members[0] expect(member.has_response).toBe(false) @@ -202,7 +211,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_alpha", "bg_beta"], name: "collision", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) const firstArchive = result.members[0].archive_file const secondArchive = result.members[1].archive_file @@ -225,37 +234,19 @@ describe("createCouncilFinalize", () => { "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 toolDef = createCouncilFinalize(tmpDir) 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("Plan full scope (Prometheus)") - expect(calls[0].content).toContain("Plan selected phase (Prometheus)") - expect(calls[0].content).toContain(".sisyphus/athena/notes/") - expect(calls[0].content).not.toContain("Hand off to Atlas to save the plan as .md") + expect(() => JSON.parse(extractJson(result))).not.toThrow() + expect(result).toContain("") + expect(result).toContain("intent: PLAN") + expect(result).toContain("Plan full scope (Prometheus)") + expect(result).toContain("Plan selected phase (Prometheus)") + expect(result).toContain(".sisyphus/athena/notes/") + expect(result).not.toContain("Hand off to Atlas to save the plan as .md") }) it("#then emits diagnose action options for hephaestus and sisyphus", async () => { @@ -265,29 +256,22 @@ describe("createCouncilFinalize", () => { "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 toolDef = createCouncilFinalize(tmpDir) 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)") + expect(() => JSON.parse(extractJson(result))).not.toThrow() + expect(result).toContain("") + expect(result).toContain("Implement (Hephaestus)") + expect(result).toContain("Implement (Sisyphus)") + expect(result).toContain("Implement (Sisyphus ultrawork)") + expect(result).toContain("switch_agent(agent=\"hephaestus\")") + expect(result).toContain("switch_agent(agent=\"sisyphus\")") + expect(result).toContain("prefix the handoff context with \"ultrawork \"") + expect(result).not.toContain("Fix now (Atlas)") + expect(result).not.toContain("Create plan (Prometheus)") }) it("#then emits audit processing mode and batching guidance", async () => { @@ -297,44 +281,37 @@ describe("createCouncilFinalize", () => { "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 toolDef = createCouncilFinalize(tmpDir) 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)") + expect(() => JSON.parse(extractJson(result))).not.toThrow() + expect(result).toContain("") + expect(result).toContain("How would you like to process the findings?") + expect(result).toContain("One by one") + expect(result).toContain("By severity/urgency") + expect(result).toContain("By quorum") + expect(result).toContain("Default batch size: 3 findings per batch") + expect(result).toContain("Hard cap: 5 findings") + expect(result).toContain("Example Question tool call (batch of 3 findings)") + expect(result).toContain("Finding #10: choose how to proceed.") + expect(result).toContain("#10 Action") + expect(result).toContain("Stop review") + expect(result).toContain("#10:A, #11:skip") + expect(result).toContain("Which findings should we act on by severity?") + expect(result).toContain("All Critical (N)") + expect(result).toContain("All High (N)") + expect(result).toContain("All Medium (N)") + expect(result).toContain("All Low (N)") + expect(result).toContain("Which findings should we act on? You can also type specific finding numbers") + expect(result).toContain("All Unanimous (N)") + expect(result).toContain("All Majority (N)") + expect(result).toContain("All Minority (N)") + expect(result).toContain("All Solo (N)") + expect(result).toContain("Fix now (Atlas)") + expect(result).toContain("Create plan (Prometheus)") }) it("#then emits informational write-to-document path without atlas delegation", async () => { @@ -344,26 +321,19 @@ describe("createCouncilFinalize", () => { "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 toolDef = createCouncilFinalize(tmpDir) 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/") - expect(calls[0].content).not.toContain("Write to document (Atlas)") + expect(() => JSON.parse(extractJson(result))).not.toThrow() + expect(result).toContain("") + expect(result).toContain("What should we do with this evaluation?") + expect(result).toContain("Adopt option -> create plan (Prometheus)") + expect(result).toContain("Adopt option -> implement now") + expect(result).toContain(".sisyphus/athena/notes/") + expect(result).not.toContain("Write to document (Atlas)") }) it("#then rejects invalid intent values", async () => { @@ -391,7 +361,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["../../etc/passwd", "bg_valid", "foo/bar"], name: "traversal", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.members).toHaveLength(3) expect(result.members[0].has_response).toBe(false) @@ -413,7 +383,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_safe"], name: "../../etc", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.archive_dir).toMatch(/^\.sisyphus\/athena\/council-/) expect(result.archive_dir).not.toContain("..") @@ -427,7 +397,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_abs"], name: "/absolute/path", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.archive_dir).toMatch(/^\.sisyphus\/athena\/council-/) expect(result.archive_dir).not.toContain("/absolute/path") @@ -441,7 +411,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_mid"], name: "foo/../bar", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.archive_dir).toMatch(/^\.sisyphus\/athena\/council-/) expect(result.archive_dir).not.toContain("..") @@ -455,7 +425,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["valid-id_123"], name: "valid", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.members).toHaveLength(1) expect(result.members[0].has_response).toBe(true) @@ -474,7 +444,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_prompt"], name: "prompt-test", prompt_file: "/etc/passwd", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.archive_dir).toBeDefined() const metaContent = await readFile(join(tmpDir, result.meta_file), "utf-8") @@ -493,7 +463,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_rel"], name: "rel-test", prompt_file: "../../etc/passwd", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.archive_dir).toBeDefined() const metaContent = await readFile(join(tmpDir, result.meta_file), "utf-8") @@ -514,7 +484,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_ok"], name: "valid-prompt", prompt_file: ".sisyphus/tmp/athena-council-test.md", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) const metaContent = await readFile(join(tmpDir, result.meta_file), "utf-8") expect(metaContent).toContain("prompt_file:") @@ -526,7 +496,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg\\evil"], name: "backslash", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.members).toHaveLength(1) expect(result.members[0].has_response).toBe(false) @@ -545,7 +515,7 @@ describe("createCouncilFinalize", () => { { task_ids: ["bg_empty_name"], name: "///...", intent: "FREEFORM" }, mockCtx, ) - const result: CouncilFinalizeResult = JSON.parse(resultStr) + const result: CouncilFinalizeResult = JSON.parse(extractJson(resultStr)) expect(result.archive_dir).toMatch(/^\.sisyphus\/athena\/council-unnamed-[a-f0-9]{4}$/) }) diff --git a/src/tools/council-archive/create-council-finalize.ts b/src/tools/council-archive/create-council-finalize.ts index 30af61361..e0fabe48d 100644 --- a/src/tools/council-archive/create-council-finalize.ts +++ b/src/tools/council-archive/create-council-finalize.ts @@ -11,19 +11,14 @@ import { resolveCouncilIntent, COUNCIL_DEFAULTS, } from "../../agents/athena" -import { log } from "../../shared/logger" -import type { ContextCollector } from "../../features/context-injector" import type { CouncilFinalizeArgs, CouncilMemberResult, CouncilFinalizeResult } from "./types" -type RegisterContext = Pick type CouncilFinalizeToolContext = { sessionID?: string } export function createCouncilFinalize( basePath?: string, - options?: { contextCollector?: RegisterContext } ): ToolDefinition { - const collector = options?.contextCollector return tool({ description: @@ -45,19 +40,6 @@ export function createCouncilFinalize( 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(COUNCIL_DEFAULTS.ARCHIVE_ID_BYTES).toString("hex") const safeName = slugify(args.name) || "unnamed" @@ -192,7 +174,8 @@ export function createCouncilFinalize( members, } - return JSON.stringify(result, null, 2) + const guidance = buildAthenaRuntimeGuidance(resolvedIntent) + return JSON.stringify(result, null, 2) + "\n\n" + guidance }, }) }