From 75a66e0e23340f9d672cd26f6eeef25ea00f9b08 Mon Sep 17 00:00:00 2001 From: Pavel Fadeev Date: Tue, 18 Aug 2026 00:51:05 +0200 Subject: [PATCH 1/9] feat: escalate Board risks to read-only Head --- packages/advisor/src/board-watcher.test.ts | 4 +- packages/advisor/src/board-watcher.ts | 8 ++- packages/advisor/src/completions.test.ts | 3 +- packages/advisor/src/completions.ts | 3 +- packages/advisor/src/extension.test.ts | 33 ++++++++++ packages/advisor/src/extension.ts | 72 +++++++++++++++++++++- 6 files changed, 117 insertions(+), 6 deletions(-) diff --git a/packages/advisor/src/board-watcher.test.ts b/packages/advisor/src/board-watcher.test.ts index 1ef7c0e..5022c45 100644 --- a/packages/advisor/src/board-watcher.test.ts +++ b/packages/advisor/src/board-watcher.test.ts @@ -12,8 +12,8 @@ function riskyLedger(turn = 1) { describe("Board watcher", () => { it("defaults to deterministic shadow mode", () => { - expect(defaultBoardWatchConfig()).toEqual({ mode: "shadow", cooldownTurns: 3, maxInterventions: 4 }); - expect(normalizeBoardWatchConfig({ mode: "intervene", cooldownTurns: 999, maxInterventions: -1 })).toEqual({ mode: "intervene", cooldownTurns: 100, maxInterventions: 0 }); + expect(defaultBoardWatchConfig()).toEqual({ mode: "shadow", cooldownTurns: 3, maxInterventions: 4, headEscalation: "off", headMaxCalls: 1 }); + expect(normalizeBoardWatchConfig({ mode: "intervene", cooldownTurns: 999, maxInterventions: -1, headEscalation: "enabled", headMaxCalls: 99 })).toEqual({ mode: "intervene", cooldownTurns: 100, maxInterventions: 0, headEscalation: "enabled", headMaxCalls: 4 }); }); it("records material risks without calling a model or queuing advice in shadow mode", () => { diff --git a/packages/advisor/src/board-watcher.ts b/packages/advisor/src/board-watcher.ts index cbb7234..0219210 100644 --- a/packages/advisor/src/board-watcher.ts +++ b/packages/advisor/src/board-watcher.ts @@ -7,6 +7,8 @@ export interface BoardWatchConfig { mode: BoardWatchMode; cooldownTurns: number; maxInterventions: number; + headEscalation: "off" | "enabled"; + headMaxCalls: number; } export interface BoardWatchState { @@ -17,6 +19,7 @@ export interface BoardWatchState { lastTurn?: number; lastInterventionTurn?: number; lastRiskFingerprint?: string; + lastEscalatedRiskFingerprint?: string; lastDecision?: BoardDecision; } @@ -42,7 +45,7 @@ export interface BoardWatchResult { } export function defaultBoardWatchConfig(): BoardWatchConfig { - return { mode: "shadow", cooldownTurns: 3, maxInterventions: 4 }; + return { mode: "shadow", cooldownTurns: 3, maxInterventions: 4, headEscalation: "off", headMaxCalls: 1 }; } export function normalizeBoardWatchConfig(raw: unknown): BoardWatchConfig { @@ -57,6 +60,8 @@ export function normalizeBoardWatchConfig(raw: unknown): BoardWatchConfig { mode: record.mode === "off" || record.mode === "intervene" ? record.mode : "shadow", cooldownTurns: bounded(record.cooldownTurns, defaults.cooldownTurns, 0, 100), maxInterventions: bounded(record.maxInterventions, defaults.maxInterventions, 0, 32), + headEscalation: record.headEscalation === "enabled" ? "enabled" : "off", + headMaxCalls: bounded(record.headMaxCalls, defaults.headMaxCalls, 0, 4), }; } @@ -76,6 +81,7 @@ export function normalizeBoardWatchState(raw: unknown): BoardWatchState { lastTurn: Number.isFinite(Number(record.lastTurn)) ? Math.max(0, Math.floor(Number(record.lastTurn))) : undefined, lastInterventionTurn: Number.isFinite(Number(record.lastInterventionTurn)) ? Math.max(0, Math.floor(Number(record.lastInterventionTurn))) : undefined, lastRiskFingerprint: typeof record.lastRiskFingerprint === "string" ? record.lastRiskFingerprint : undefined, + lastEscalatedRiskFingerprint: typeof record.lastEscalatedRiskFingerprint === "string" ? record.lastEscalatedRiskFingerprint : undefined, lastDecision: record.lastDecision as BoardDecision | undefined, }; } diff --git a/packages/advisor/src/completions.test.ts b/packages/advisor/src/completions.test.ts index 9c2cead..ecfd351 100644 --- a/packages/advisor/src/completions.test.ts +++ b/packages/advisor/src/completions.test.ts @@ -29,7 +29,8 @@ describe("advisor completions", () => { it("offers explicit Board controls and roles", () => { expect(advisorArgumentCompletions("board ")?.map((i) => i.value)).toEqual(["watch", "specialist", "head"]); - expect(advisorArgumentCompletions("board watch ")?.map((i) => i.value)).toEqual(["status", "off", "shadow", "intervene"]); + expect(advisorArgumentCompletions("board watch ")?.map((i) => i.value)).toEqual(["status", "off", "shadow", "intervene", "head"]); + expect(advisorArgumentCompletions("board watch head ")?.map((i) => i.value)).toEqual(["status", "on", "off"]); expect(advisorArgumentCompletions("review ")).toBeNull(); expect(advisorArgumentCompletions("profile ")).toBeNull(); }); diff --git a/packages/advisor/src/completions.ts b/packages/advisor/src/completions.ts index 0c4d8d5..d3da61d 100644 --- a/packages/advisor/src/completions.ts +++ b/packages/advisor/src/completions.ts @@ -47,7 +47,8 @@ const advisorNested: Record> = { model: [["list", "inspect available role candidates"], ["advisor"], ["specialist"], ["head"], ["null"]], "model list": [["advisor"], ["specialist"], ["head"]], board: [["watch"], ["specialist"], ["head"]], - "board watch": [["status"], ["off"], ["shadow"], ["intervene"]], + "board watch": [["status"], ["off"], ["shadow"], ["intervene"], ["head"]], + "board watch head": [["status"], ["on"], ["off"]], }; const piRogueTopLevel: Array<[string, string?]> = [ diff --git a/packages/advisor/src/extension.test.ts b/packages/advisor/src/extension.test.ts index 837ca15..ff1144f 100644 --- a/packages/advisor/src/extension.test.ts +++ b/packages/advisor/src/extension.test.ts @@ -263,6 +263,39 @@ describe("Advisor PR1 lifecycle", () => { } }); + it("escalates one material Board risk to a bounded read-only Head", async () => { + const handlers = new Map void>(); + const sendMessage = vi.fn(); + const pi = { + on: (event: string, handler: (event: unknown, ctx: any) => void) => { handlers.set(event, handler); }, + registerMessageRenderer: vi.fn(), + registerTool: vi.fn(), + registerCommand: vi.fn(), + sendMessage, + } as unknown as ExtensionAPI; + vi.mocked(completeSimple).mockResolvedValue({ content: [{ type: "text", text: "Validate the changed file before proceeding." }] } as any); + writeFileSync(advisorBoardWatchConfigPath(), JSON.stringify({ mode: "intervene", cooldownTurns: 0, maxInterventions: 4, headEscalation: "enabled", headMaxCalls: 1 })); + try { + registerAdvisor(pi); + const ctx = { + session: { id: `advisor-head-watch-${Date.now()}-${Math.random()}` }, + cwd: process.cwd(), + ui: { setStatus: vi.fn() }, + modelRegistry: { + find: (provider: string, id: string) => provider === "openai-codex" && id === "gpt-5.5" ? { provider, id, input: ["text"] } : undefined, + getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "test-key" }), + }, + }; + handlers.get("session_start")?.({}, ctx); + handlers.get("turn_end")?.({ turnIndex: 0, toolResults: [{ toolName: "edit", input: { path: "packages/advisor/src/head-watched.ts" }, status: "success" }] }, ctx); + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "advisor:board", details: expect.objectContaining({ readOnly: true }) }), expect.objectContaining({ triggerTurn: false, deliverAs: "nextTurn" })); + expect(vi.mocked(completeSimple)).toHaveBeenCalledTimes(1); + } finally { + unlinkSync(advisorBoardWatchConfigPath()); + } + }); + it("deduplicates repeated lifecycle events while preserving same-turn failures", () => { const handlers = new Map void>(); const pi = { diff --git a/packages/advisor/src/extension.ts b/packages/advisor/src/extension.ts index 7b515c6..c80ad58 100644 --- a/packages/advisor/src/extension.ts +++ b/packages/advisor/src/extension.ts @@ -1844,6 +1844,62 @@ function collectLifecycleBoardEvents(state: SessionState, toolResults: unknown[] state.boardEvents = [...state.boardEvents, ...events].slice(-MAX_BOARD_EVENTS); } +async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, state: SessionState, riskFingerprint: string): Promise { + const watch = loadBoardWatchConfig(); + if (watch.headEscalation !== "enabled" || watch.headMaxCalls <= 0 || state.boardWatch.lastEscalatedRiskFingerprint === riskFingerprint) return; + const headConfig = { + ...defaultHeadOfBoardConfig(), + mode: "enabled" as const, + maxTokens: cfg.board.headMaxTokens, + maxCallsPerSession: watch.headMaxCalls, + callsUsed: state.headOfBoard?.calls ?? 0, + }; + try { + const ledger = currentBoardLedger(ctx, state); + const decision = decideBoardAction(ledger); + const result = await callHeadOfBoardAdapter(headConfig, { + ledger, + decision, + question: "Review this material Board risk and recommend the next safe step. Do not request or perform mutations.", + reason: "architecture_risk", + }, async (systemPrompt, messages, options) => completeWithHigherAdvisorModel(ctx, cfg, systemPrompt, messages, { + ...options, + role: "head", + maxAttempts: 1, + })); + state.headOfBoard = state.headOfBoard ?? { calls: 0 }; + if (result.skipped) { + state.headOfBoard.lastSkipped = result.skipped; + saveState(state); + return; + } + if (!result.response) { + state.headOfBoard.lastSkipped = "no_response"; + saveState(state); + return; + } + state.headOfBoard.calls += result.accounting.headOfBoardCalls; + state.headOfBoard.lastAt = new Date().toISOString(); + state.headOfBoard.lastModel = result.response.model; + state.headOfBoard.lastSkipped = undefined; + state.boardWatch.lastEscalatedRiskFingerprint = riskFingerprint; + saveState(state); + if (typeof pi.sendMessage === "function") { + pi.sendMessage({ + customType: "advisor:board", + content: `Head-of-Board advice (read-only, non-binding; model ${result.response.model}):\n${String(result.response.text).slice(0, 1800)}`, + display: true, + details: { kind: "board-head", decision: "would_whisper", severity: decision.action === "would_whisper" ? decision.severity : "important", reason: "Board escalation", nonBinding: true, readOnly: true }, + }, { triggerTurn: false, deliverAs: "nextTurn" }); + } + } catch (error) { + state.headOfBoard = state.headOfBoard ?? { calls: 0 }; + state.headOfBoard.lastSkipped = "error"; + saveState(state); + appendAdvisorDiagnostic("board_head_escalation_failed", { error: error instanceof Error ? error.message : String(error) }); + } +} + function recordBoardWatchIfEnabled(pi: ExtensionAPI, ctx: any, state: SessionState): void { const config = loadBoardWatchConfig(); if (config.mode === "off") return; @@ -1856,6 +1912,7 @@ function recordBoardWatchIfEnabled(pi: ExtensionAPI, ctx: any, state: SessionSta display: true, details: result.advice.details, }, { triggerTurn: false, deliverAs: "nextTurn" }); + if (config.headEscalation === "enabled") void escalateBoardHead(pi, ctx, loadConfig(), state, result.advice.fingerprint); } } @@ -2078,6 +2135,7 @@ export function registerAdvisor(pi: ExtensionAPI): void { `Board watcher: ${current.mode}`, `Runs: ${state.boardWatch.runs}; interventions: ${state.boardWatch.interventions}; suppressed: ${state.boardWatch.suppressed}`, `Cooldown: ${current.cooldownTurns} turn(s); max interventions: ${current.maxInterventions}`, + `Head escalation: ${current.headEscalation}; max calls: ${current.headMaxCalls}`, "Watcher is deterministic and read-only; interventions are non-binding next-turn advice.", ].join("\n"), "info"); return; @@ -2087,7 +2145,19 @@ export function registerAdvisor(pi: ExtensionAPI): void { ctx.ui.notify(`Board watcher mode set to ${action}.`, "info"); return; } - ctx.ui.notify("Usage: board watch status|off|shadow|intervene", "error"); + if (action === "head") { + const headAction = String(parts[3] ?? "status").toLowerCase(); + if (headAction === "status") { + ctx.ui.notify(`Board Head escalation: ${current.headEscalation} (max ${current.headMaxCalls} call(s)).`, "info"); + return; + } + if (headAction === "on" || headAction === "off") { + saveBoardWatchConfig({ ...current, headEscalation: headAction === "on" ? "enabled" : "off" }); + ctx.ui.notify(`Board Head escalation ${headAction === "on" ? "enabled" : "disabled"}.`, "info"); + return; + } + } + ctx.ui.notify("Usage: board watch status|off|shadow|intervene|head status|on|off", "error"); return; } if (area === "head") { From 88f88096e1a9bb47e68f7da1c47fd0f296c0e92b Mon Sep 17 00:00:00 2001 From: Pavel Fadeev Date: Tue, 18 Aug 2026 01:00:01 +0200 Subject: [PATCH 2/9] fix: cap Head attempts and redact auth secrets --- packages/advisor/src/board-head.test.ts | 6 ++++-- packages/advisor/src/board-head.ts | 6 ++++-- packages/advisor/src/board-watcher.test.ts | 1 + packages/advisor/src/board-watcher.ts | 4 +++- packages/advisor/src/extension.test.ts | 3 +++ packages/advisor/src/extension.ts | 8 +++++--- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/advisor/src/board-head.test.ts b/packages/advisor/src/board-head.test.ts index fc8ec9a..ac2395b 100644 --- a/packages/advisor/src/board-head.test.ts +++ b/packages/advisor/src/board-head.test.ts @@ -114,8 +114,8 @@ describe("head-of-board adapter", () => { const ledger = ledgerFrom([{ type: "file_changed", path: "packages/advisor/src/board-head.ts", turn: 4 }]); const request = buildHeadOfBoardRequest({ ledger, - decision: { action: "would_whisper", severity: "important", reason: "rerun with Authorization: Bearer abcdef1234567890 token=abcd1234 AWS_ACCESS_KEY_ID=AKIAABCDEFGHIJKLMNOP", riskIds: ["risk:token=abcd1234"] }, - question: "Assess release readiness with MY_SECRET=shhhhhhh", + decision: { action: "would_whisper", severity: "important", reason: "rerun with Authorization: Bearer abcdef1234567890 Authorization: Basic dXNlcjpwYXNz token=abcd1234 AWS_ACCESS_KEY_ID=AKIAABCDEFGHIJKLMNOP", riskIds: ["risk:token=abcd1234"] }, + question: "Assess release readiness with MY_SECRET=shhhhhhh and {\"apiKey\": \"json-secret-value\"}", }); const payload = JSON.stringify({ content: request.messages[0]?.content, escalation: request.escalation }); @@ -123,6 +123,8 @@ describe("head-of-board adapter", () => { expect(payload).not.toContain("abcdef1234567890"); expect(payload).not.toContain("AKIAABCDEFGHIJKLMNOP"); expect(payload).not.toContain("shhhhhhh"); + expect(payload).not.toContain("dXNlcjpwYXNz"); + expect(payload).not.toContain("json-secret-value"); expect(payload).toContain("[secret]"); }); diff --git a/packages/advisor/src/board-head.ts b/packages/advisor/src/board-head.ts index b19e723..95bc6b1 100644 --- a/packages/advisor/src/board-head.ts +++ b/packages/advisor/src/board-head.ts @@ -83,7 +83,8 @@ export interface HeadOfBoardResult { export type HeadOfBoardComplete = (systemPrompt: string, messages: Array<{ role: "user"; content: string }>, options: { maxTokens: number; reasoning: ThinkingLevel }) => Promise; const SECRET_RE = /\b(?:(?:sk|ghp|gho|github_pat|xox[abprs]|hf)[-_][A-Za-z0-9_\-]{8,}|AKIA[A-Z0-9]{12,})\b/g; -const KEYED_SECRET_RE = /\b(?:api[_-]?key|token|secret|password|authorization)\b\s*[:=]\s*["']?[^\s"',;}]{4,}/gi; +const KEYED_SECRET_RE = /(["']?\b(?:api[_-]?key|token|secret|password|authorization)\b["']?)\s*[:=]\s*["']?[^\s"',;}]{4,}/gi; +const BASIC_AUTH_RE = /(["']?authorization["']?\s*[:=]\s*["']?)Basic\s+[A-Za-z0-9+/=]+/gi; const NAMED_SECRET_ASSIGNMENT_RE = /\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY)[A-Z0-9_]*\s*=\s*[^\s"',;}]{4,}/gi; const BARE_BEARER_RE = /\bbearer\s+[A-Za-z0-9._~+/=-]{8,}/gi; @@ -125,8 +126,9 @@ export function normalizeHeadOfBoardConfig(raw: unknown): HeadOfBoardConfig { function cleanText(value: unknown, max = 500): string { return String(value ?? "") .replace(BARE_BEARER_RE, "Bearer [secret]") + .replace(BASIC_AUTH_RE, "$1[secret]") .replace(SECRET_RE, "[secret]") - .replace(KEYED_SECRET_RE, (match) => `${match.split(/[:=]/, 1)[0]}=[secret]`) + .replace(KEYED_SECRET_RE, (_match, key: string) => `${key}=[secret]`) .replace(NAMED_SECRET_ASSIGNMENT_RE, (match) => `${match.split(/=/, 1)[0].trim()}=[secret]`) .replace(/\s+/g, " ") .trim() diff --git a/packages/advisor/src/board-watcher.test.ts b/packages/advisor/src/board-watcher.test.ts index 5022c45..83a2e74 100644 --- a/packages/advisor/src/board-watcher.test.ts +++ b/packages/advisor/src/board-watcher.test.ts @@ -13,6 +13,7 @@ function riskyLedger(turn = 1) { describe("Board watcher", () => { it("defaults to deterministic shadow mode", () => { expect(defaultBoardWatchConfig()).toEqual({ mode: "shadow", cooldownTurns: 3, maxInterventions: 4, headEscalation: "off", headMaxCalls: 1 }); + expect(defaultBoardWatchState()).toEqual({ runs: 0, interventions: 0, headAttempts: 0, suppressed: 0 }); expect(normalizeBoardWatchConfig({ mode: "intervene", cooldownTurns: 999, maxInterventions: -1, headEscalation: "enabled", headMaxCalls: 99 })).toEqual({ mode: "intervene", cooldownTurns: 100, maxInterventions: 0, headEscalation: "enabled", headMaxCalls: 4 }); }); diff --git a/packages/advisor/src/board-watcher.ts b/packages/advisor/src/board-watcher.ts index 0219210..20894cf 100644 --- a/packages/advisor/src/board-watcher.ts +++ b/packages/advisor/src/board-watcher.ts @@ -14,6 +14,7 @@ export interface BoardWatchConfig { export interface BoardWatchState { runs: number; interventions: number; + headAttempts: number; suppressed: number; lastAt?: string; lastTurn?: number; @@ -66,7 +67,7 @@ export function normalizeBoardWatchConfig(raw: unknown): BoardWatchConfig { } export function defaultBoardWatchState(): BoardWatchState { - return { runs: 0, interventions: 0, suppressed: 0 }; + return { runs: 0, interventions: 0, headAttempts: 0, suppressed: 0 }; } export function normalizeBoardWatchState(raw: unknown): BoardWatchState { @@ -76,6 +77,7 @@ export function normalizeBoardWatchState(raw: unknown): BoardWatchState { return { runs: count(record.runs), interventions: count(record.interventions), + headAttempts: count(record.headAttempts), suppressed: count(record.suppressed), lastAt: typeof record.lastAt === "string" ? record.lastAt : undefined, lastTurn: Number.isFinite(Number(record.lastTurn)) ? Math.max(0, Math.floor(Number(record.lastTurn))) : undefined, diff --git a/packages/advisor/src/extension.test.ts b/packages/advisor/src/extension.test.ts index ff1144f..724e321 100644 --- a/packages/advisor/src/extension.test.ts +++ b/packages/advisor/src/extension.test.ts @@ -291,6 +291,9 @@ describe("Advisor PR1 lifecycle", () => { await new Promise((resolve) => setTimeout(resolve, 20)); expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "advisor:board", details: expect.objectContaining({ readOnly: true }) }), expect.objectContaining({ triggerTurn: false, deliverAs: "nextTurn" })); expect(vi.mocked(completeSimple)).toHaveBeenCalledTimes(1); + handlers.get("turn_end")?.({ turnIndex: 1, toolResults: [{ toolName: "edit", input: { path: "packages/advisor/src/another-head-watched.ts" }, status: "success" }] }, ctx); + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(vi.mocked(completeSimple)).toHaveBeenCalledTimes(1); } finally { unlinkSync(advisorBoardWatchConfigPath()); } diff --git a/packages/advisor/src/extension.ts b/packages/advisor/src/extension.ts index c80ad58..f6a5255 100644 --- a/packages/advisor/src/extension.ts +++ b/packages/advisor/src/extension.ts @@ -1846,7 +1846,10 @@ function collectLifecycleBoardEvents(state: SessionState, toolResults: unknown[] async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, state: SessionState, riskFingerprint: string): Promise { const watch = loadBoardWatchConfig(); - if (watch.headEscalation !== "enabled" || watch.headMaxCalls <= 0 || state.boardWatch.lastEscalatedRiskFingerprint === riskFingerprint) return; + if (watch.headEscalation !== "enabled" || watch.headMaxCalls <= 0 || state.boardWatch.lastEscalatedRiskFingerprint === riskFingerprint || state.boardWatch.headAttempts >= watch.headMaxCalls) return; + state.boardWatch.headAttempts += 1; + state.boardWatch.lastEscalatedRiskFingerprint = riskFingerprint; + saveState(state); const headConfig = { ...defaultHeadOfBoardConfig(), mode: "enabled" as const, @@ -1882,7 +1885,6 @@ async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, state.headOfBoard.lastAt = new Date().toISOString(); state.headOfBoard.lastModel = result.response.model; state.headOfBoard.lastSkipped = undefined; - state.boardWatch.lastEscalatedRiskFingerprint = riskFingerprint; saveState(state); if (typeof pi.sendMessage === "function") { pi.sendMessage({ @@ -2133,7 +2135,7 @@ export function registerAdvisor(pi: ExtensionAPI): void { if (action === "status") { ctx.ui.notify([ `Board watcher: ${current.mode}`, - `Runs: ${state.boardWatch.runs}; interventions: ${state.boardWatch.interventions}; suppressed: ${state.boardWatch.suppressed}`, + `Runs: ${state.boardWatch.runs}; interventions: ${state.boardWatch.interventions}; Head attempts: ${state.boardWatch.headAttempts}; suppressed: ${state.boardWatch.suppressed}`, `Cooldown: ${current.cooldownTurns} turn(s); max interventions: ${current.maxInterventions}`, `Head escalation: ${current.headEscalation}; max calls: ${current.headMaxCalls}`, "Watcher is deterministic and read-only; interventions are non-binding next-turn advice.", From 933cd20644e8dd77cd96caababf789e0bb9e7758 Mon Sep 17 00:00:00 2001 From: Pavel Fadeev Date: Tue, 18 Aug 2026 01:09:15 +0200 Subject: [PATCH 3/9] fix: preserve Head state and redact AWS credentials --- packages/advisor/src/board-head.test.ts | 14 +++++++++- packages/advisor/src/board-head.ts | 2 +- packages/advisor/src/extension.ts | 35 ++++++++++++++++--------- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/packages/advisor/src/board-head.test.ts b/packages/advisor/src/board-head.test.ts index ac2395b..1f166c9 100644 --- a/packages/advisor/src/board-head.test.ts +++ b/packages/advisor/src/board-head.test.ts @@ -89,6 +89,16 @@ describe("head-of-board adapter", () => { expect(request.messages[0]?.content).toContain("decision_needed"); }); + it("derives automatic escalation reason from the material risk", () => { + const ledger = ledgerFrom([ + { type: "tool_failure", tool: "bash", key: "npm-test", message: "failed", turn: 1 }, + { type: "tool_failure", tool: "bash", key: "npm-test", message: "failed", turn: 2 }, + { type: "tool_failure", tool: "bash", key: "npm-test", message: "failed", turn: 3 }, + ]); + const request = buildHeadOfBoardRequest({ ledger, decision: decideBoardAction(ledger), question: "What next?" }); + expect(request.escalation.reason).toBe("repeated_failure"); + }); + it("preserves promoted shadow risks when rebuilding a head-of-board ledger", () => { const ledger = ledgerFrom([{ type: "turn", turn: 3, progress: true }]); const risk = { id: "repeated_failure:npm-test", type: "repeated_failure" as const, severity: "important" as const, evidence: "npm test failed repeatedly", evidencePointers: ["failure:npm-test"] }; @@ -114,7 +124,7 @@ describe("head-of-board adapter", () => { const ledger = ledgerFrom([{ type: "file_changed", path: "packages/advisor/src/board-head.ts", turn: 4 }]); const request = buildHeadOfBoardRequest({ ledger, - decision: { action: "would_whisper", severity: "important", reason: "rerun with Authorization: Bearer abcdef1234567890 Authorization: Basic dXNlcjpwYXNz token=abcd1234 AWS_ACCESS_KEY_ID=AKIAABCDEFGHIJKLMNOP", riskIds: ["risk:token=abcd1234"] }, + decision: { action: "would_whisper", severity: "important", reason: "rerun with Authorization: Bearer abcdef1234567890 Authorization: Basic dXNlcjpwYXNz token=abcd1234 AWS_ACCESS_KEY_ID=AKIAABCDEFGHIJKLMNOP ASIAABCDEFGHIJKLMNOP", riskIds: ["risk:token=abcd1234"] }, question: "Assess release readiness with MY_SECRET=shhhhhhh and {\"apiKey\": \"json-secret-value\"}", }); const payload = JSON.stringify({ content: request.messages[0]?.content, escalation: request.escalation }); @@ -122,6 +132,8 @@ describe("head-of-board adapter", () => { expect(payload).not.toContain("abcd1234"); expect(payload).not.toContain("abcdef1234567890"); expect(payload).not.toContain("AKIAABCDEFGHIJKLMNOP"); + expect(payload).not.toContain("ASIAABCDEFGHIJKLMNOP"); + expect(payload).not.toContain("EFGH"); expect(payload).not.toContain("shhhhhhh"); expect(payload).not.toContain("dXNlcjpwYXNz"); expect(payload).not.toContain("json-secret-value"); diff --git a/packages/advisor/src/board-head.ts b/packages/advisor/src/board-head.ts index 95bc6b1..2b6cce3 100644 --- a/packages/advisor/src/board-head.ts +++ b/packages/advisor/src/board-head.ts @@ -82,7 +82,7 @@ export interface HeadOfBoardResult { export type HeadOfBoardComplete = (systemPrompt: string, messages: Array<{ role: "user"; content: string }>, options: { maxTokens: number; reasoning: ThinkingLevel }) => Promise; -const SECRET_RE = /\b(?:(?:sk|ghp|gho|github_pat|xox[abprs]|hf)[-_][A-Za-z0-9_\-]{8,}|AKIA[A-Z0-9]{12,})\b/g; +const SECRET_RE = /\b(?:(?:sk|ghp|gho|github_pat|xox[abprs]|hf)[-_][A-Za-z0-9_\-]{8,}|(?:AKIA|ASIA)[A-Z0-9]{16})\b/g; const KEYED_SECRET_RE = /(["']?\b(?:api[_-]?key|token|secret|password|authorization)\b["']?)\s*[:=]\s*["']?[^\s"',;}]{4,}/gi; const BASIC_AUTH_RE = /(["']?authorization["']?\s*[:=]\s*["']?)Basic\s+[A-Za-z0-9+/=]+/gi; const NAMED_SECRET_ASSIGNMENT_RE = /\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY)[A-Z0-9_]*\s*=\s*[^\s"',;}]{4,}/gi; diff --git a/packages/advisor/src/extension.ts b/packages/advisor/src/extension.ts index f6a5255..b4080f6 100644 --- a/packages/advisor/src/extension.ts +++ b/packages/advisor/src/extension.ts @@ -1864,7 +1864,6 @@ async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, ledger, decision, question: "Review this material Board risk and recommend the next safe step. Do not request or perform mutations.", - reason: "architecture_risk", }, async (systemPrompt, messages, options) => completeWithHigherAdvisorModel(ctx, cfg, systemPrompt, messages, { ...options, role: "head", @@ -1872,20 +1871,29 @@ async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, })); state.headOfBoard = state.headOfBoard ?? { calls: 0 }; if (result.skipped) { - state.headOfBoard.lastSkipped = result.skipped; - saveState(state); + const latest = loadState(ctx); + latest.headOfBoard = { ...(latest.headOfBoard ?? { calls: 0 }), lastSkipped: result.skipped }; + latest.boardWatch.headAttempts = Math.max(latest.boardWatch.headAttempts, state.boardWatch.headAttempts); + saveState(latest); return; } if (!result.response) { - state.headOfBoard.lastSkipped = "no_response"; - saveState(state); + const latest = loadState(ctx); + latest.headOfBoard = { ...(latest.headOfBoard ?? { calls: 0 }), lastSkipped: "no_response" }; + latest.boardWatch.headAttempts = Math.max(latest.boardWatch.headAttempts, state.boardWatch.headAttempts); + saveState(latest); return; } - state.headOfBoard.calls += result.accounting.headOfBoardCalls; - state.headOfBoard.lastAt = new Date().toISOString(); - state.headOfBoard.lastModel = result.response.model; - state.headOfBoard.lastSkipped = undefined; - saveState(state); + const latest = loadState(ctx); + latest.headOfBoard = { + ...(latest.headOfBoard ?? { calls: 0 }), + calls: (latest.headOfBoard?.calls ?? 0) + result.accounting.headOfBoardCalls, + lastAt: new Date().toISOString(), + lastModel: result.response.model, + lastSkipped: undefined, + }; + latest.boardWatch.headAttempts = Math.max(latest.boardWatch.headAttempts, state.boardWatch.headAttempts); + saveState(latest); if (typeof pi.sendMessage === "function") { pi.sendMessage({ customType: "advisor:board", @@ -1895,9 +1903,10 @@ async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, }, { triggerTurn: false, deliverAs: "nextTurn" }); } } catch (error) { - state.headOfBoard = state.headOfBoard ?? { calls: 0 }; - state.headOfBoard.lastSkipped = "error"; - saveState(state); + const latest = loadState(ctx); + latest.headOfBoard = { ...(latest.headOfBoard ?? { calls: 0 }), lastSkipped: "error" }; + latest.boardWatch.headAttempts = Math.max(latest.boardWatch.headAttempts, state.boardWatch.headAttempts); + saveState(latest); appendAdvisorDiagnostic("board_head_escalation_failed", { error: error instanceof Error ? error.message : String(error) }); } } From 1589460703dce34c6c988b6c99c892607e2f77f5 Mon Sep 17 00:00:00 2001 From: Pavel Fadeev Date: Tue, 18 Aug 2026 01:17:43 +0200 Subject: [PATCH 4/9] fix: redact JSON AWS credentials --- packages/advisor/src/board-head.test.ts | 3 ++- packages/advisor/src/board-head.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/advisor/src/board-head.test.ts b/packages/advisor/src/board-head.test.ts index 1f166c9..1cd688b 100644 --- a/packages/advisor/src/board-head.test.ts +++ b/packages/advisor/src/board-head.test.ts @@ -125,7 +125,7 @@ describe("head-of-board adapter", () => { const request = buildHeadOfBoardRequest({ ledger, decision: { action: "would_whisper", severity: "important", reason: "rerun with Authorization: Bearer abcdef1234567890 Authorization: Basic dXNlcjpwYXNz token=abcd1234 AWS_ACCESS_KEY_ID=AKIAABCDEFGHIJKLMNOP ASIAABCDEFGHIJKLMNOP", riskIds: ["risk:token=abcd1234"] }, - question: "Assess release readiness with MY_SECRET=shhhhhhh and {\"apiKey\": \"json-secret-value\"}", + question: "Assess release readiness with MY_SECRET=shhhhhhh and {\"apiKey\": \"json-secret-value\", \"AWS_SECRET_ACCESS_KEY\": \"json-aws-secret-value\"}", }); const payload = JSON.stringify({ content: request.messages[0]?.content, escalation: request.escalation }); @@ -137,6 +137,7 @@ describe("head-of-board adapter", () => { expect(payload).not.toContain("shhhhhhh"); expect(payload).not.toContain("dXNlcjpwYXNz"); expect(payload).not.toContain("json-secret-value"); + expect(payload).not.toContain("json-aws-secret-value"); expect(payload).toContain("[secret]"); }); diff --git a/packages/advisor/src/board-head.ts b/packages/advisor/src/board-head.ts index 2b6cce3..a51f2ce 100644 --- a/packages/advisor/src/board-head.ts +++ b/packages/advisor/src/board-head.ts @@ -85,7 +85,7 @@ export type HeadOfBoardComplete = (systemPrompt: string, messages: Array<{ role: const SECRET_RE = /\b(?:(?:sk|ghp|gho|github_pat|xox[abprs]|hf)[-_][A-Za-z0-9_\-]{8,}|(?:AKIA|ASIA)[A-Z0-9]{16})\b/g; const KEYED_SECRET_RE = /(["']?\b(?:api[_-]?key|token|secret|password|authorization)\b["']?)\s*[:=]\s*["']?[^\s"',;}]{4,}/gi; const BASIC_AUTH_RE = /(["']?authorization["']?\s*[:=]\s*["']?)Basic\s+[A-Za-z0-9+/=]+/gi; -const NAMED_SECRET_ASSIGNMENT_RE = /\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY)[A-Z0-9_]*\s*=\s*[^\s"',;}]{4,}/gi; +const NAMED_SECRET_ASSIGNMENT_RE = /(["']?\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY)[A-Z0-9_]*["']?)\s*[:=]\s*["']?[^\s"',;}]{4,}/gi; const BARE_BEARER_RE = /\bbearer\s+[A-Za-z0-9._~+/=-]{8,}/gi; export function defaultHeadOfBoardConfig(): HeadOfBoardConfig { @@ -129,7 +129,7 @@ function cleanText(value: unknown, max = 500): string { .replace(BASIC_AUTH_RE, "$1[secret]") .replace(SECRET_RE, "[secret]") .replace(KEYED_SECRET_RE, (_match, key: string) => `${key}=[secret]`) - .replace(NAMED_SECRET_ASSIGNMENT_RE, (match) => `${match.split(/=/, 1)[0].trim()}=[secret]`) + .replace(NAMED_SECRET_ASSIGNMENT_RE, (_match, key: string) => `${key}=[secret]`) .replace(/\s+/g, " ") .trim() .slice(0, max); From 65f0675b37a0aff24cb119854ced0505307246a3 Mon Sep 17 00:00:00 2001 From: Pavel Fadeev Date: Tue, 18 Aug 2026 01:20:11 +0200 Subject: [PATCH 5/9] fix: complete bounded Board Head escalation --- packages/advisor/src/extension.test.ts | 33 ++++++++++++++++++++++++++ packages/advisor/src/extension.ts | 4 +++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/advisor/src/extension.test.ts b/packages/advisor/src/extension.test.ts index 724e321..447469a 100644 --- a/packages/advisor/src/extension.test.ts +++ b/packages/advisor/src/extension.test.ts @@ -299,6 +299,39 @@ describe("Advisor PR1 lifecycle", () => { } }); + it("can escalate a shadow risk to Head without enabling Board intervention", async () => { + const handlers = new Map void>(); + const sendMessage = vi.fn(); + const pi = { + on: (event: string, handler: (event: unknown, ctx: any) => void) => { handlers.set(event, handler); }, + registerMessageRenderer: vi.fn(), + registerTool: vi.fn(), + registerCommand: vi.fn(), + sendMessage, + } as unknown as ExtensionAPI; + const callsBefore = vi.mocked(completeSimple).mock.calls.length; + vi.mocked(completeSimple).mockResolvedValue({ content: [{ type: "text", text: "Validate before continuing." }] } as any); + writeFileSync(advisorBoardWatchConfigPath(), JSON.stringify({ mode: "shadow", headEscalation: "enabled", headMaxCalls: 1 })); + try { + registerAdvisor(pi); + const ctx = { + session: { id: `advisor-shadow-head-${Date.now()}-${Math.random()}` }, + cwd: process.cwd(), + ui: { setStatus: vi.fn() }, + modelRegistry: { + find: (provider: string, id: string) => provider === "openai-codex" && id === "gpt-5.5" ? { provider, id, input: ["text"] } : undefined, + getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "test-key" }), + }, + }; + handlers.get("turn_end")?.({ turnIndex: 0, toolResults: [{ toolName: "edit", input: { path: "packages/advisor/src/shadow-head.ts" }, status: "success" }] }, ctx); + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(vi.mocked(completeSimple)).toHaveBeenCalledTimes(callsBefore + 1); + expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "advisor:board", details: expect.objectContaining({ kind: "board-head", readOnly: true }) }), expect.objectContaining({ triggerTurn: false, deliverAs: "nextTurn" })); + } finally { + unlinkSync(advisorBoardWatchConfigPath()); + } + }); + it("deduplicates repeated lifecycle events while preserving same-turn failures", () => { const handlers = new Map void>(); const pi = { diff --git a/packages/advisor/src/extension.ts b/packages/advisor/src/extension.ts index b4080f6..dce50dd 100644 --- a/packages/advisor/src/extension.ts +++ b/packages/advisor/src/extension.ts @@ -1923,7 +1923,9 @@ function recordBoardWatchIfEnabled(pi: ExtensionAPI, ctx: any, state: SessionSta display: true, details: result.advice.details, }, { triggerTurn: false, deliverAs: "nextTurn" }); - if (config.headEscalation === "enabled") void escalateBoardHead(pi, ctx, loadConfig(), state, result.advice.fingerprint); + } + if (config.headEscalation === "enabled" && result.decision.action === "would_whisper" && result.riskFingerprint) { + void escalateBoardHead(pi, ctx, loadConfig(), state, result.riskFingerprint); } } From 98e9a98e895eaf5d6732285ef50dbf7b98399d4c Mon Sep 17 00:00:00 2001 From: Pavel Fadeev Date: Tue, 18 Aug 2026 01:21:52 +0200 Subject: [PATCH 6/9] test: isolate shared watcher config cleanup --- packages/advisor/src/extension.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/advisor/src/extension.test.ts b/packages/advisor/src/extension.test.ts index 447469a..e553fea 100644 --- a/packages/advisor/src/extension.test.ts +++ b/packages/advisor/src/extension.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { readFileSync, unlinkSync, writeFileSync } from "node:fs"; +import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { completeSimple } from "@earendil-works/pi-ai/compat"; import { advisorBoardWatchConfigPath, advisorModelInspectionText, inspectAdvisorModels, advisorSessionStatePath, normalizeAdvisorConfig, rankAvailableAdvisorModels, registerAdvisor, resolveModelCandidates, type AdvisorConfig } from "./extension.js"; @@ -259,7 +259,7 @@ describe("Advisor PR1 lifecycle", () => { expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "advisor:board", details: expect.objectContaining({ nonBinding: true, readOnly: true }) }), { triggerTurn: false, deliverAs: "nextTurn" }); expect(vi.mocked(completeSimple)).not.toHaveBeenCalled(); } finally { - unlinkSync(advisorBoardWatchConfigPath()); + if (existsSync(advisorBoardWatchConfigPath())) unlinkSync(advisorBoardWatchConfigPath()); } }); @@ -295,7 +295,7 @@ describe("Advisor PR1 lifecycle", () => { await new Promise((resolve) => setTimeout(resolve, 20)); expect(vi.mocked(completeSimple)).toHaveBeenCalledTimes(1); } finally { - unlinkSync(advisorBoardWatchConfigPath()); + if (existsSync(advisorBoardWatchConfigPath())) unlinkSync(advisorBoardWatchConfigPath()); } }); @@ -328,7 +328,7 @@ describe("Advisor PR1 lifecycle", () => { expect(vi.mocked(completeSimple)).toHaveBeenCalledTimes(callsBefore + 1); expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "advisor:board", details: expect.objectContaining({ kind: "board-head", readOnly: true }) }), expect.objectContaining({ triggerTurn: false, deliverAs: "nextTurn" })); } finally { - unlinkSync(advisorBoardWatchConfigPath()); + if (existsSync(advisorBoardWatchConfigPath())) unlinkSync(advisorBoardWatchConfigPath()); } }); From ced2c42b8ec31677ffa682d87e7075fcc5b2e8ec Mon Sep 17 00:00:00 2001 From: Pavel Fadeev Date: Tue, 18 Aug 2026 01:29:11 +0200 Subject: [PATCH 7/9] fix: reserve Head escalations atomically --- packages/advisor/src/board-head.test.ts | 4 +++- packages/advisor/src/board-head.ts | 2 +- packages/advisor/src/extension.test.ts | 1 + packages/advisor/src/extension.ts | 13 +++++++++---- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/advisor/src/board-head.test.ts b/packages/advisor/src/board-head.test.ts index 1cd688b..a8574e8 100644 --- a/packages/advisor/src/board-head.test.ts +++ b/packages/advisor/src/board-head.test.ts @@ -125,7 +125,7 @@ describe("head-of-board adapter", () => { const request = buildHeadOfBoardRequest({ ledger, decision: { action: "would_whisper", severity: "important", reason: "rerun with Authorization: Bearer abcdef1234567890 Authorization: Basic dXNlcjpwYXNz token=abcd1234 AWS_ACCESS_KEY_ID=AKIAABCDEFGHIJKLMNOP ASIAABCDEFGHIJKLMNOP", riskIds: ["risk:token=abcd1234"] }, - question: "Assess release readiness with MY_SECRET=shhhhhhh and {\"apiKey\": \"json-secret-value\", \"AWS_SECRET_ACCESS_KEY\": \"json-aws-secret-value\"}", + question: "Assess release readiness with MY_SECRET=shhhhhhh and {\"apiKey\": \"json-secret-value\", \"AWS_SECRET_ACCESS_KEY\": \"json-aws-secret-value\", \"SecretAccessKey\": \"camel-aws-secret\", \"SessionToken\": \"camel-session-token\"}", }); const payload = JSON.stringify({ content: request.messages[0]?.content, escalation: request.escalation }); @@ -138,6 +138,8 @@ describe("head-of-board adapter", () => { expect(payload).not.toContain("dXNlcjpwYXNz"); expect(payload).not.toContain("json-secret-value"); expect(payload).not.toContain("json-aws-secret-value"); + expect(payload).not.toContain("camel-aws-secret"); + expect(payload).not.toContain("camel-session-token"); expect(payload).toContain("[secret]"); }); diff --git a/packages/advisor/src/board-head.ts b/packages/advisor/src/board-head.ts index a51f2ce..bc4a8d3 100644 --- a/packages/advisor/src/board-head.ts +++ b/packages/advisor/src/board-head.ts @@ -85,7 +85,7 @@ export type HeadOfBoardComplete = (systemPrompt: string, messages: Array<{ role: const SECRET_RE = /\b(?:(?:sk|ghp|gho|github_pat|xox[abprs]|hf)[-_][A-Za-z0-9_\-]{8,}|(?:AKIA|ASIA)[A-Z0-9]{16})\b/g; const KEYED_SECRET_RE = /(["']?\b(?:api[_-]?key|token|secret|password|authorization)\b["']?)\s*[:=]\s*["']?[^\s"',;}]{4,}/gi; const BASIC_AUTH_RE = /(["']?authorization["']?\s*[:=]\s*["']?)Basic\s+[A-Za-z0-9+/=]+/gi; -const NAMED_SECRET_ASSIGNMENT_RE = /(["']?\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY)[A-Z0-9_]*["']?)\s*[:=]\s*["']?[^\s"',;}]{4,}/gi; +const NAMED_SECRET_ASSIGNMENT_RE = /(["']?\b[A-Za-z0-9_-]*(?:secret|token|password|api[_-]?key|access[_-]?key)[A-Za-z0-9_-]*["']?)\s*[:=]\s*["']?[^\s"',;}]{4,}/gi; const BARE_BEARER_RE = /\bbearer\s+[A-Za-z0-9._~+/=-]{8,}/gi; export function defaultHeadOfBoardConfig(): HeadOfBoardConfig { diff --git a/packages/advisor/src/extension.test.ts b/packages/advisor/src/extension.test.ts index e553fea..d63901c 100644 --- a/packages/advisor/src/extension.test.ts +++ b/packages/advisor/src/extension.test.ts @@ -324,6 +324,7 @@ describe("Advisor PR1 lifecycle", () => { }, }; handlers.get("turn_end")?.({ turnIndex: 0, toolResults: [{ toolName: "edit", input: { path: "packages/advisor/src/shadow-head.ts" }, status: "success" }] }, ctx); + handlers.get("turn_end")?.({ turnIndex: 1, toolResults: [{ toolName: "edit", input: { path: "packages/advisor/src/shadow-head-2.ts" }, status: "success" }] }, ctx); await new Promise((resolve) => setTimeout(resolve, 100)); expect(vi.mocked(completeSimple)).toHaveBeenCalledTimes(callsBefore + 1); expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "advisor:board", details: expect.objectContaining({ kind: "board-head", readOnly: true }) }), expect.objectContaining({ triggerTurn: false, deliverAs: "nextTurn" })); diff --git a/packages/advisor/src/extension.ts b/packages/advisor/src/extension.ts index dce50dd..9fe5767 100644 --- a/packages/advisor/src/extension.ts +++ b/packages/advisor/src/extension.ts @@ -1844,12 +1844,17 @@ function collectLifecycleBoardEvents(state: SessionState, toolResults: unknown[] state.boardEvents = [...state.boardEvents, ...events].slice(-MAX_BOARD_EVENTS); } -async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, state: SessionState, riskFingerprint: string): Promise { - const watch = loadBoardWatchConfig(); - if (watch.headEscalation !== "enabled" || watch.headMaxCalls <= 0 || state.boardWatch.lastEscalatedRiskFingerprint === riskFingerprint || state.boardWatch.headAttempts >= watch.headMaxCalls) return; +function reserveBoardHeadEscalation(config: BoardWatchConfig, state: SessionState, riskFingerprint: string): boolean { + if (config.headEscalation !== "enabled" || config.headMaxCalls <= 0) return false; + if (state.boardWatch.lastEscalatedRiskFingerprint === riskFingerprint || state.boardWatch.headAttempts >= config.headMaxCalls) return false; state.boardWatch.headAttempts += 1; state.boardWatch.lastEscalatedRiskFingerprint = riskFingerprint; saveState(state); + return true; +} + +async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, state: SessionState, riskFingerprint: string): Promise { + const watch = loadBoardWatchConfig(); const headConfig = { ...defaultHeadOfBoardConfig(), mode: "enabled" as const, @@ -1924,7 +1929,7 @@ function recordBoardWatchIfEnabled(pi: ExtensionAPI, ctx: any, state: SessionSta details: result.advice.details, }, { triggerTurn: false, deliverAs: "nextTurn" }); } - if (config.headEscalation === "enabled" && result.decision.action === "would_whisper" && result.riskFingerprint) { + if (result.decision.action === "would_whisper" && result.riskFingerprint && reserveBoardHeadEscalation(config, state, result.riskFingerprint)) { void escalateBoardHead(pi, ctx, loadConfig(), state, result.riskFingerprint); } } From bc2c75a3a7ef77bd0564fd11483e5fcff164b0d0 Mon Sep 17 00:00:00 2001 From: Pavel Fadeev Date: Tue, 18 Aug 2026 01:36:39 +0200 Subject: [PATCH 8/9] fix: reject stale Head advice and preserve shadow escalation --- packages/advisor/src/board-watcher.ts | 6 +++++- packages/advisor/src/extension.test.ts | 4 ++-- packages/advisor/src/extension.ts | 12 ++++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/advisor/src/board-watcher.ts b/packages/advisor/src/board-watcher.ts index 20894cf..3ff341f 100644 --- a/packages/advisor/src/board-watcher.ts +++ b/packages/advisor/src/board-watcher.ts @@ -107,6 +107,10 @@ function adviceText(decision: Extract= config.maxInterventions) { + if (config.mode === "intervene" && prior.interventions >= config.maxInterventions) { state.suppressed += 1; return { state, decision, riskFingerprint: id, skipped: "limit" }; } diff --git a/packages/advisor/src/extension.test.ts b/packages/advisor/src/extension.test.ts index d63901c..1f87800 100644 --- a/packages/advisor/src/extension.test.ts +++ b/packages/advisor/src/extension.test.ts @@ -311,7 +311,7 @@ describe("Advisor PR1 lifecycle", () => { } as unknown as ExtensionAPI; const callsBefore = vi.mocked(completeSimple).mock.calls.length; vi.mocked(completeSimple).mockResolvedValue({ content: [{ type: "text", text: "Validate before continuing." }] } as any); - writeFileSync(advisorBoardWatchConfigPath(), JSON.stringify({ mode: "shadow", headEscalation: "enabled", headMaxCalls: 1 })); + writeFileSync(advisorBoardWatchConfigPath(), JSON.stringify({ mode: "shadow", maxInterventions: 0, headEscalation: "enabled", headMaxCalls: 1 })); try { registerAdvisor(pi); const ctx = { @@ -327,7 +327,7 @@ describe("Advisor PR1 lifecycle", () => { handlers.get("turn_end")?.({ turnIndex: 1, toolResults: [{ toolName: "edit", input: { path: "packages/advisor/src/shadow-head-2.ts" }, status: "success" }] }, ctx); await new Promise((resolve) => setTimeout(resolve, 100)); expect(vi.mocked(completeSimple)).toHaveBeenCalledTimes(callsBefore + 1); - expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "advisor:board", details: expect.objectContaining({ kind: "board-head", readOnly: true }) }), expect.objectContaining({ triggerTurn: false, deliverAs: "nextTurn" })); + expect(sendMessage).not.toHaveBeenCalled(); } finally { if (existsSync(advisorBoardWatchConfigPath())) unlinkSync(advisorBoardWatchConfigPath()); } diff --git a/packages/advisor/src/extension.ts b/packages/advisor/src/extension.ts index 9fe5767..2ddbb79 100644 --- a/packages/advisor/src/extension.ts +++ b/packages/advisor/src/extension.ts @@ -23,7 +23,7 @@ import { type SpecialistCallState, } from "./board-specialist.js"; import { loadBoardRoleBody, loadBoardRoleCatalog } from "./board-roles.js"; -import { defaultBoardWatchConfig, normalizeBoardWatchConfig, normalizeBoardWatchState, runBoardWatch, type BoardWatchConfig, type BoardWatchState } from "./board-watcher.js"; +import { boardWatchRiskFingerprint, defaultBoardWatchConfig, normalizeBoardWatchConfig, normalizeBoardWatchState, runBoardWatch, type BoardWatchConfig, type BoardWatchState } from "./board-watcher.js"; // ── Explicit-only configuration ───────────────────────────────────────── @@ -1890,6 +1890,14 @@ async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, return; } const latest = loadState(ctx); + const latestLedger = currentBoardLedger(ctx, latest); + const latestDecision = decideBoardAction(latestLedger); + if (boardWatchRiskFingerprint(latestLedger, latestDecision) !== riskFingerprint) { + latest.headOfBoard = { ...(latest.headOfBoard ?? { calls: 0 }), lastSkipped: "stale_risk" }; + latest.boardWatch.headAttempts = Math.max(latest.boardWatch.headAttempts, state.boardWatch.headAttempts); + saveState(latest); + return; + } latest.headOfBoard = { ...(latest.headOfBoard ?? { calls: 0 }), calls: (latest.headOfBoard?.calls ?? 0) + result.accounting.headOfBoardCalls, @@ -1904,7 +1912,7 @@ async function escalateBoardHead(pi: ExtensionAPI, ctx: any, cfg: AdvisorConfig, customType: "advisor:board", content: `Head-of-Board advice (read-only, non-binding; model ${result.response.model}):\n${String(result.response.text).slice(0, 1800)}`, display: true, - details: { kind: "board-head", decision: "would_whisper", severity: decision.action === "would_whisper" ? decision.severity : "important", reason: "Board escalation", nonBinding: true, readOnly: true }, + details: { kind: "board-head", decision: "would_whisper", severity: latestDecision.action === "would_whisper" ? latestDecision.severity : "important", reason: "Board escalation", nonBinding: true, readOnly: true }, }, { triggerTurn: false, deliverAs: "nextTurn" }); } } catch (error) { From d3d58b32f7c22f146be39a823cdf6e93023a871a Mon Sep 17 00:00:00 2001 From: Pavel Fadeev Date: Tue, 18 Aug 2026 01:43:16 +0200 Subject: [PATCH 9/9] fix: decouple Head escalation from watcher suppression --- packages/advisor/src/board-watcher.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/advisor/src/board-watcher.test.ts b/packages/advisor/src/board-watcher.test.ts index 83a2e74..d27bbdc 100644 --- a/packages/advisor/src/board-watcher.test.ts +++ b/packages/advisor/src/board-watcher.test.ts @@ -41,6 +41,7 @@ describe("Board watcher", () => { const first = runBoardWatch(config, defaultBoardWatchState(), riskyLedger(1), 1); const changed = runBoardWatch(config, first.state, riskyLedger(2), 2); expect(changed.advice).toBeUndefined(); + expect(changed.riskFingerprint).toBeDefined(); expect(changed.skipped).toBe("cooldown"); const consecutive = runBoardWatch(config, changed.state, riskyLedger(3), 3); expect(consecutive.advice).toBeUndefined();