Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/hooks/hermes/wiki-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ async function main(): Promise<void> {
"--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" },
});
Expand Down
13 changes: 9 additions & 4 deletions src/hooks/wiki-worker-spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
};
}

Expand All @@ -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 },
};
}
3 changes: 3 additions & 0 deletions src/skillify/gate-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
15 changes: 15 additions & 0 deletions tests/claude-code/skillify-gate-runner.test.ts
Original file line number Diff line number Diff line change
@@ -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[]) {
Expand Down
23 changes: 23 additions & 0 deletions tests/claude-code/wiki-worker-windows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
8 changes: 8 additions & 0 deletions tests/hermes/hermes-wiki-worker-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading