fix(daemon): --verbose actually lowers log level + reports effective level

--verbose was wired only to the stderr-mirror path. Debug entries got
filtered by Logger.level (default 'info' from config) before reaching
the mirror — so passing --verbose produced almost no extra output, which
made it look broken on a fresh start.

Now --verbose lowers the level to 'debug' AND mirrors. Logger exposes
`effectiveLevel` so the "daemon started" banner reports what the logger
is actually using, not what was in the config file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-02 22:41:48 +02:00
parent 7bec2dc2d0
commit c9609459e4
3 changed files with 24 additions and 3 deletions

View file

@ -13,7 +13,9 @@ Alias: sf-daemon
Options:
--config <path> Path to YAML config file (default: ~/.sf/daemon.yaml)
--verbose Print log entries to stderr in addition to the log file
--verbose Lower log level to debug AND mirror entries to stderr
--start <path> Start an autonomous SF session for this project path
--command <text> Command to send for --start (default: /sf autonomous)
--install Install the launchd LaunchAgent (auto-starts on login)
--uninstall Uninstall the launchd LaunchAgent
--status Show launchd agent status (registered, PID, exit code)
@ -25,6 +27,8 @@ export async function main(): Promise<void> {
options: {
config: { type: 'string', short: 'c' },
verbose: { type: 'boolean', short: 'v', default: false },
start: { type: 'string' },
command: { type: 'string' },
install: { type: 'boolean', default: false },
uninstall: { type: 'boolean', default: false },
status: { type: 'boolean', default: false },
@ -80,14 +84,26 @@ export async function main(): Promise<void> {
const configPath = resolveConfigPath(values.config);
const config = loadConfig(configPath);
// --verbose lowers log level to debug AND mirrors to stderr. Without
// this, debug entries are dropped before reaching the verbose stderr
// mirror — making --verbose silently useless on a default-info config.
const logger = new Logger({
filePath: config.log.file,
level: config.log.level,
level: values.verbose ? 'debug' : config.log.level,
verbose: values.verbose,
});
const daemon = new Daemon(config, logger);
await daemon.start();
if (values.start !== undefined) {
const projectDir = values.start.trim() === '' ? process.cwd() : values.start;
const sessionId = await daemon.getSessionManager().startSession({
projectDir,
...(values.command ? { command: values.command } : {}),
});
logger.info('batch session started', { sessionId, projectDir });
}
}
export function handleFatalError(err: unknown): never {

View file

@ -35,7 +35,7 @@ export class Daemon {
this.sessionManager = new SessionManager(this.logger);
this.logger.info('daemon started', {
log_level: this.config.log.level,
log_level: this.logger.effectiveLevel,
scan_roots: this.config.projects.scan_roots.length,
discord_configured: !!this.config.discord,
});

View file

@ -24,6 +24,10 @@ export class Logger {
private readonly stream: WriteStream;
private readonly level: number;
private readonly verbose: boolean;
/** The level name actually in effect surfaced so callers (e.g. the
* startup banner) report the level the logger is using, not the level
* that was originally requested in config. */
readonly effectiveLevel: LogLevel;
constructor(opts: LoggerOptions) {
// Ensure parent directory exists
@ -37,6 +41,7 @@ export class Logger {
this.stream = createWriteStream(opts.filePath, { flags: 'a' });
this.level = LEVEL_ORDER[opts.level] ?? LEVEL_ORDER.info;
this.effectiveLevel = opts.level;
this.verbose = opts.verbose ?? false;
}