diff --git a/src/features/background-agent/types.ts b/src/features/background-agent/types.ts
index 3c5928872..7ac4d5d14 100644
--- a/src/features/background-agent/types.ts
+++ b/src/features/background-agent/types.ts
@@ -63,6 +63,12 @@ export interface BackgroundTask {
isUnstableAgent?: boolean
/** Category used for this task (e.g., 'quick', 'visual-engineering') */
category?: string
+ /** Flag to write raw output to file on completion */
+ writeOutputToFile?: boolean
+ /** Path to the output file after successful write */
+ outputFilePath?: string
+ /** Race condition guard: prevents concurrent re-entry during async completion */
+ _isCompleting?: boolean
/** Last message count for stability detection */
lastMsgCount?: number
@@ -89,6 +95,8 @@ export interface LaunchInput {
skillContent?: string
category?: string
sessionPermission?: SessionPermissionRule[]
+ /** Flag to write raw output to file on completion */
+ writeOutputToFile?: boolean
}
export interface ResumeInput {
diff --git a/src/tools/council-archive/council-response-extractor.test.ts b/src/tools/council-archive/council-response-extractor.test.ts
new file mode 100644
index 000000000..d86eeb54c
--- /dev/null
+++ b/src/tools/council-archive/council-response-extractor.test.ts
@@ -0,0 +1,103 @@
+import { describe, expect, it } from "bun:test"
+import { extractCouncilResponse } from "./council-response-extractor"
+
+describe("extractCouncilResponse", () => {
+ describe("#given complete COUNCIL_MEMBER_RESPONSE tags", () => {
+ it("#then returns has_response true, response_complete true, and the content", () => {
+ const result = extractCouncilResponse("analysis here")
+
+ expect(result).toEqual({
+ has_response: true,
+ response_complete: true,
+ result: "analysis here",
+ })
+ })
+ })
+
+ describe("#given incomplete tags (opening but no closing)", () => {
+ it("#then returns has_response true, response_complete false, and partial content", () => {
+ const result = extractCouncilResponse("partial analysis")
+
+ expect(result).toEqual({
+ has_response: true,
+ response_complete: false,
+ result: "partial analysis",
+ })
+ })
+ })
+
+ describe("#given missing tags (no opening tag)", () => {
+ it("#then returns has_response false, response_complete false, and null result", () => {
+ const result = extractCouncilResponse("Just some plain text without any tags.")
+
+ expect(result).toEqual({
+ has_response: false,
+ response_complete: false,
+ result: null,
+ })
+ })
+ })
+
+ describe("#given empty content between tags", () => {
+ it("#then returns has_response true, response_complete true, and empty string result", () => {
+ const result = extractCouncilResponse("")
+
+ expect(result).toEqual({
+ has_response: true,
+ response_complete: true,
+ result: "",
+ })
+ })
+ })
+
+ describe("#given multiple tag pairs", () => {
+ it("#then returns content from the last opening tag", () => {
+ const text =
+ "first analysis\nSome interim text\nfinal analysis"
+ const result = extractCouncilResponse(text)
+
+ expect(result).toEqual({
+ has_response: true,
+ response_complete: true,
+ result: "final analysis",
+ })
+ })
+ })
+
+ describe("#given an empty string", () => {
+ it("#then returns has_response false and null result", () => {
+ const result = extractCouncilResponse("")
+
+ expect(result).toEqual({
+ has_response: false,
+ response_complete: false,
+ result: null,
+ })
+ })
+ })
+
+ describe("#given whitespace-only content between tags", () => {
+ it("#then returns has_response true, response_complete true, and empty string result", () => {
+ const result = extractCouncilResponse(" ")
+
+ expect(result).toEqual({
+ has_response: true,
+ response_complete: true,
+ result: "",
+ })
+ })
+ })
+
+ describe("#given content with surrounding text before the opening tag", () => {
+ it("#then returns only the tagged content", () => {
+ const text = "Some preamble text\nthe actual response"
+ const result = extractCouncilResponse(text)
+
+ expect(result).toEqual({
+ has_response: true,
+ response_complete: true,
+ result: "the actual response",
+ })
+ })
+ })
+})
diff --git a/src/tools/council-archive/council-response-extractor.ts b/src/tools/council-archive/council-response-extractor.ts
new file mode 100644
index 000000000..76c439e70
--- /dev/null
+++ b/src/tools/council-archive/council-response-extractor.ts
@@ -0,0 +1,26 @@
+export const OPENING_TAG = ""
+export const CLOSING_TAG = ""
+
+export interface CouncilResponseExtraction {
+ has_response: boolean
+ response_complete: boolean
+ result: string | null
+}
+
+export function extractCouncilResponse(fullText: string): CouncilResponseExtraction {
+ const lastOpenIdx = fullText.lastIndexOf(OPENING_TAG)
+ if (lastOpenIdx === -1) {
+ return { has_response: false, response_complete: false, result: null }
+ }
+
+ const contentStart = lastOpenIdx + OPENING_TAG.length
+ const closingAfterLastOpen = fullText.indexOf(CLOSING_TAG, contentStart)
+
+ if (closingAfterLastOpen === -1) {
+ const partial = fullText.slice(contentStart).trim()
+ return { has_response: true, response_complete: false, result: partial || null }
+ }
+
+ const content = fullText.slice(contentStart, closingAfterLastOpen).trim()
+ return { has_response: true, response_complete: true, result: content }
+}