diff --git a/src/hooks/hermes/wiki-worker.ts b/src/hooks/hermes/wiki-worker.ts index eed27e49..30ab8169 100644 --- a/src/hooks/hermes/wiki-worker.ts +++ b/src/hooks/hermes/wiki-worker.ts @@ -186,6 +186,9 @@ async function main(): Promise { "--ignore-user-config", ], { stdio: ["ignore", "pipe", "pipe"], + // Suppress the visible console window Windows would otherwise pop for + // a child of this console-less detached worker. No-op on POSIX. + windowsHide: true, timeout: 120_000, env: { ...process.env, HIVEMIND_WIKI_WORKER: "1", HIVEMIND_CAPTURE: "false" }, }); diff --git a/src/hooks/wiki-worker-spawn.ts b/src/hooks/wiki-worker-spawn.ts index 6359397b..2f8106ec 100644 --- a/src/hooks/wiki-worker-spawn.ts +++ b/src/hooks/wiki-worker-spawn.ts @@ -35,13 +35,16 @@ export function buildClaudeInvocation(claudeBin: string, prompt: string): Claude return { file: claudeBin, args: ["-p", ...CLAUDE_FLAGS], - options: { input: prompt, stdio: ["pipe", "pipe", "pipe"], shell: true }, + // windowsHide: the wiki worker is a detached, console-less process, so + // without CREATE_NO_WINDOW Windows allocates a visible console window + // (titled after the CLI exe) for the child. No-op on POSIX. + options: { input: prompt, stdio: ["pipe", "pipe", "pipe"], shell: true, windowsHide: true }, }; } return { file: claudeBin, args: ["-p", prompt, ...CLAUDE_FLAGS], - options: { stdio: ["ignore", "pipe", "pipe"] }, + options: { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }, }; } @@ -63,12 +66,14 @@ export function buildTrailingPromptInvocation(bin: string, flags: string[], prom return { file: bin, args: [...flags], - options: { input: prompt, stdio: ["pipe", "pipe", "pipe"], shell: true }, + // windowsHide: see buildClaudeInvocation — suppress the visible console + // window Windows would pop for a child of the console-less worker. + options: { input: prompt, stdio: ["pipe", "pipe", "pipe"], shell: true, windowsHide: true }, }; } return { file: bin, args: [...flags, prompt], - options: { stdio: ["ignore", "pipe", "pipe"] }, + options: { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }, }; } diff --git a/src/skillify/gate-runner.ts b/src/skillify/gate-runner.ts index f4029609..6f1f19ab 100644 --- a/src/skillify/gate-runner.ts +++ b/src/skillify/gate-runner.ts @@ -200,6 +200,9 @@ export function runGate(opts: GateRunOptions): GateRunResult { try { const result = runChildProcess(bin, args, { stdio: ["ignore", "pipe", "pipe"], + // Suppress the visible console window Windows would otherwise pop for + // a child of the console-less detached skillify worker. No-op on POSIX. + windowsHide: true, timeout: opts.timeoutMs ?? 120_000, maxBuffer: 8 * 1024 * 1024, env: { ...inheritedEnv.env, HIVEMIND_WIKI_WORKER: "1", HIVEMIND_CAPTURE: "false" }, diff --git a/tests/claude-code/skillify-gate-runner.test.ts b/tests/claude-code/skillify-gate-runner.test.ts index ce342b0b..f7989750 100644 --- a/tests/claude-code/skillify-gate-runner.test.ts +++ b/tests/claude-code/skillify-gate-runner.test.ts @@ -1,6 +1,21 @@ import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; import { runGate, findAgentBin, type Agent } from "../../src/skillify/gate-runner.js"; +describe("gate-runner spawn options", () => { + it("passes windowsHide so the gate CLI never pops a console window on Windows", () => { + // The skillify worker is detached and console-less; without windowsHide + // on the inner CLI spawn, Windows allocates a visible console window. + // Source-level guard because the dispatch tests exec a real /usr/bin/echo + // and can't observe the options object. + // Scoped to the runChildProcess call ([^)]* = no closing paren in + // between) so the guard fails if the option drifts out of the spawn. + const src = readFileSync(join(process.cwd(), "src/skillify/gate-runner.ts"), "utf-8"); + expect(src).toMatch(/runChildProcess\([^)]*windowsHide:\s*true/); + }); +}); + describe("findAgentBin", () => { it("returns a path for each known agent (PATH lookup or fallback)", () => { for (const agent of ["claude_code", "codex", "cursor", "hermes", "pi"] as Agent[]) { diff --git a/tests/claude-code/wiki-worker-windows.test.ts b/tests/claude-code/wiki-worker-windows.test.ts index 6f99ad94..09602657 100644 --- a/tests/claude-code/wiki-worker-windows.test.ts +++ b/tests/claude-code/wiki-worker-windows.test.ts @@ -172,3 +172,26 @@ describe("buildTrailingPromptInvocation (codex / cursor / pi)", () => { expect(inv.args).toEqual([...FLAGS, "PROMPT-TEXT"]); }); }); + +describe("windowsHide — no visible console window for the summarizer CLI", () => { + // The wiki worker is spawned detached and console-less (spawn-detached.ts + // sets windowsHide on the worker itself). Without CREATE_NO_WINDOW on the + // INNER spawn too, Windows allocates a fresh visible console window titled + // after the CLI exe (users reported a bare "claude.exe" window popping up). + it("buildClaudeInvocation sets windowsHide on every branch", () => { + setPlatform("win32"); + expect(buildClaudeInvocation("C:\\npm\\claude.cmd", "P").options.windowsHide).toBe(true); + expect(buildClaudeInvocation("C:\\pf\\claude.exe", "P").options.windowsHide).toBe(true); + setPlatform("linux"); + expect(buildClaudeInvocation("/usr/local/bin/claude", "P").options.windowsHide).toBe(true); + }); + + it("buildTrailingPromptInvocation sets windowsHide on every branch", () => { + const FLAGS = ["exec"]; + setPlatform("win32"); + expect(buildTrailingPromptInvocation("C:\\npm\\codex.cmd", FLAGS, "P").options.windowsHide).toBe(true); + expect(buildTrailingPromptInvocation("C:\\pf\\codex.exe", FLAGS, "P").options.windowsHide).toBe(true); + setPlatform("linux"); + expect(buildTrailingPromptInvocation("/usr/local/bin/codex", FLAGS, "P").options.windowsHide).toBe(true); + }); +}); diff --git a/tests/hermes/hermes-wiki-worker-source.test.ts b/tests/hermes/hermes-wiki-worker-source.test.ts index c36b82f1..68151f30 100644 --- a/tests/hermes/hermes-wiki-worker-source.test.ts +++ b/tests/hermes/hermes-wiki-worker-source.test.ts @@ -26,6 +26,14 @@ describe("hermes wiki-worker source", () => { expect(WORKER_SRC).not.toMatch(/"--dangerously-bypass-approvals-and-sandbox"/); }); + it("hermes spawn passes windowsHide (no visible console window on Windows)", () => { + // hermes is the only worker that calls execFileSync directly instead of + // going through the wiki-worker-spawn builders, so it needs its own guard. + // Scoped to the hermesBin call ([^)]* = no closing paren in between) so + // the guard fails if the option drifts out of the spawn. + expect(WORKER_SRC).toMatch(/execFileSync\(\s*cfg\.hermesBin[^)]*windowsHide:\s*true/); + }); + it("config carries hermesBin + hermesProvider + hermesModel (not codexBin)", () => { expect(WORKER_SRC).toContain("hermesBin: string"); expect(WORKER_SRC).toContain("hermesProvider: string");