fix(gsd): suppress workflow stderr during /gsd

This commit is contained in:
Jeremy 2026-04-09 15:49:27 -05:00
parent 6c708f7795
commit f5c6c1d94c
3 changed files with 42 additions and 4 deletions

View file

@ -8,7 +8,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
getArgumentCompletions: getGsdArgumentCompletions,
handler: async (args: string, ctx: ExtensionCommandContext) => {
const { handleGSDCommand } = await import("./dispatcher.js");
await handleGSDCommand(args, ctx, pi);
const { setStderrLoggingEnabled } = await import("../workflow-logger.js");
const previousStderrSetting = setStderrLoggingEnabled(false);
try {
await handleGSDCommand(args, ctx, pi);
} finally {
setStderrLoggingEnabled(previousStderrSetting);
}
},
});
}

View file

@ -18,6 +18,7 @@ import {
summarizeLogs,
formatForNotification,
setLogBasePath,
setStderrLoggingEnabled,
_resetLogs,
} from "../workflow-logger.ts";
@ -375,5 +376,20 @@ describe("workflow-logger", () => {
logError("tool", "failed", { cmd: "complete_task" });
assert.ok(written[0].includes('"cmd":"complete_task"'));
});
test("suppresses stderr when disabled", (t) => {
const written: string[] = [];
const orig = process.stderr.write.bind(process.stderr);
const previous = setStderrLoggingEnabled(false);
// @ts-ignore — patching for test
process.stderr.write = (chunk: string) => { written.push(chunk); return true; };
t.after(() => {
process.stderr.write = orig;
setStderrLoggingEnabled(previous);
});
logWarning("engine", "hidden warning");
assert.deepEqual(written, []);
});
});
});

View file

@ -67,6 +67,7 @@ export interface LogEntry {
const MAX_BUFFER = 100;
let _buffer: LogEntry[] = [];
let _auditBasePath: string | null = null;
let _stderrEnabled = true;
/**
* Set the base path for persistent audit log writes.
@ -77,6 +78,16 @@ export function setLogBasePath(basePath: string): void {
_auditBasePath = basePath;
}
/**
* Enable or disable immediate stderr writes for workflow logs.
* Returns the previous setting so callers can restore it.
*/
export function setStderrLoggingEnabled(enabled: boolean): boolean {
const previous = _stderrEnabled;
_stderrEnabled = enabled;
return previous;
}
// ─── Public API ─────────────────────────────────────────────────────────
/**
@ -245,7 +256,7 @@ function _push(
// Always forward to stderr so terminal watchers see it (see module header for policy)
const prefix = severity === "error" ? "ERROR" : "WARN";
const ctxStr = context ? ` ${JSON.stringify(context)}` : "";
process.stderr.write(`[gsd:${component}] ${prefix}: ${message}${ctxStr}\n`);
_writeStderr(`[gsd:${component}] ${prefix}: ${message}${ctxStr}\n`);
// Persist to notification store (both warnings and errors)
try {
@ -255,7 +266,7 @@ function _push(
"workflow-logger",
);
} catch (notifErr) {
process.stderr.write(`[gsd:workflow-logger] notification-store append failed: ${(notifErr as Error).message}\n`);
_writeStderr(`[gsd:workflow-logger] notification-store append failed: ${(notifErr as Error).message}\n`);
}
// Buffer for auto-loop to drain
@ -275,11 +286,16 @@ function _push(
appendFileSync(join(auditDir, "audit-log.jsonl"), JSON.stringify(sanitized) + "\n", "utf-8");
} catch (auditErr) {
// Best-effort — never let audit write failures bubble up
process.stderr.write(`[gsd:audit] failed to persist log entry: ${(auditErr as Error).message}\n`);
_writeStderr(`[gsd:audit] failed to persist log entry: ${(auditErr as Error).message}\n`);
}
}
}
function _writeStderr(message: string): void {
if (!_stderrEnabled) return;
process.stderr.write(message);
}
/**
* Sanitize a log entry before persisting to the audit JSONL file.
* Strips potentially sensitive context (raw paths, cwd, full error text)