From dd0106bc20cd10e80d4427371420369becb1bd15 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 12:20:04 -0400 Subject: [PATCH 1/4] CS-10695: Replace console.log/error/warn with @cardstack/logger in software-factory Replace all console.log, console.error, and console.warn calls across 25 files in packages/software-factory/ with categorized @cardstack/logger instances. Each module gets a named logger (e.g., 'factory-entrypoint', 'serve-realm', 'factory-skill-smoke') for level-controlled, filterable output. - Add setup-logger import as first import in all entry points (CLI scripts, smoke tests, test runner, harness scripts) - Replace console calls in library modules (realm-operations, test-run-execution, factory-implement, factory-tool-builder, factory-skill-loader, etc.) - Default test log level to *=error for quieter test output - Leave test file that mocks console.warn untouched (factory-skill-loader.test.ts) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../software-factory/scripts/boxel-search.ts | 8 +- .../software-factory/scripts/boxel-session.ts | 8 +- .../software-factory/scripts/lib/boxel.ts | 5 +- .../scripts/lib/darkfactory-schemas.ts | 7 +- .../scripts/lib/factory-implement.ts | 20 +++-- .../scripts/lib/factory-skill-loader.ts | 7 +- .../scripts/lib/factory-tool-builder.ts | 5 +- .../scripts/lib/realm-operations.ts | 7 +- .../scripts/lib/test-run-execution.ts | 14 ++-- .../software-factory/scripts/pick-ticket.ts | 8 +- .../scripts/run-realm-tests.ts | 8 +- .../smoke-tests/factory-agent-smoke.ts | 28 ++++--- .../smoke-tests/factory-context-smoke.ts | 54 ++++++------ .../scripts/smoke-tests/factory-loop-smoke.ts | 78 ++++++++++-------- .../smoke-tests/factory-prompt-smoke.ts | 30 ++++--- .../smoke-tests/factory-skill-smoke.ts | 38 +++++---- .../smoke-tests/factory-tools-smoke.ts | 74 +++++++++-------- packages/software-factory/scripts/test.ts | 11 ++- .../software-factory/src/cli/cache-realm.ts | 18 ++-- .../src/cli/factory-entrypoint.ts | 26 +++--- .../software-factory/src/cli/serve-realm.ts | 10 ++- .../software-factory/src/cli/serve-support.ts | 10 ++- .../software-factory/src/cli/smoke-realm.ts | 10 ++- .../src/cli/smoke-test-realm.ts | 82 ++++++++++--------- .../src/harness/support-services.ts | 7 +- 25 files changed, 349 insertions(+), 224 deletions(-) diff --git a/packages/software-factory/scripts/boxel-search.ts b/packages/software-factory/scripts/boxel-search.ts index 86030a0919b..7467cf0364e 100644 --- a/packages/software-factory/scripts/boxel-search.ts +++ b/packages/software-factory/scripts/boxel-search.ts @@ -1,3 +1,6 @@ +// This should be first +import '../src/setup-logger'; + import { fieldPairs, forceArray, @@ -9,6 +12,9 @@ import { type SearchQuery, type SearchSort, } from './lib/boxel'; +import { logger } from '../src/logger'; + +let log = logger('boxel-search'); async function main(): Promise { let args = parseArgs(process.argv.slice(2)); @@ -89,6 +95,6 @@ async function main(): Promise { main().catch((error: unknown) => { let message = error instanceof Error ? (error.stack ?? error.message) : String(error); - console.error(message); + log.error(message); process.exit(1); }); diff --git a/packages/software-factory/scripts/boxel-session.ts b/packages/software-factory/scripts/boxel-session.ts index c9473c8ab61..fc770ddcaac 100644 --- a/packages/software-factory/scripts/boxel-session.ts +++ b/packages/software-factory/scripts/boxel-session.ts @@ -1,3 +1,6 @@ +// This should be first +import '../src/setup-logger'; + import { buildBrowserAuth, buildBrowserSession, @@ -6,6 +9,9 @@ import { parseArgs, printJson, } from './lib/boxel'; +import { logger } from '../src/logger'; + +let log = logger('boxel-session'); async function main(): Promise { let args = parseArgs(process.argv.slice(2)); @@ -30,6 +36,6 @@ async function main(): Promise { main().catch((error: unknown) => { let message = error instanceof Error ? (error.stack ?? error.message) : String(error); - console.error(message); + log.error(message); process.exit(1); }); diff --git a/packages/software-factory/scripts/lib/boxel.ts b/packages/software-factory/scripts/lib/boxel.ts index ae7f0b5470e..0eedb2c369c 100644 --- a/packages/software-factory/scripts/lib/boxel.ts +++ b/packages/software-factory/scripts/lib/boxel.ts @@ -2,9 +2,12 @@ import { existsSync, readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; +import { logger } from '../../src/logger'; import { formatErrorResponse } from '../../src/error-format'; import { ensureTrailingSlash, SupportedMimeType } from './realm-operations'; +let log = logger('boxel'); + const PROFILES_FILE = join(homedir(), '.boxel-cli', 'profiles.json'); type BoxelStoredProfile = { @@ -354,5 +357,5 @@ export function fieldPairs( } export function printJson(value: unknown): void { - console.log(JSON.stringify(value, null, 2)); + log.info(JSON.stringify(value, null, 2)); } diff --git a/packages/software-factory/scripts/lib/darkfactory-schemas.ts b/packages/software-factory/scripts/lib/darkfactory-schemas.ts index 4a7ce59b1d4..1a0b9c76910 100644 --- a/packages/software-factory/scripts/lib/darkfactory-schemas.ts +++ b/packages/software-factory/scripts/lib/darkfactory-schemas.ts @@ -7,6 +7,7 @@ * are always derived from the actual card definitions. */ +import { logger } from '../../src/logger'; import type { ResolvedCodeRef, LooseSingleCardDocument, @@ -23,6 +24,8 @@ import { // Runtime schema fetching // --------------------------------------------------------------------------- +let log = logger('darkfactory-schemas'); + const GET_CARD_TYPE_SCHEMA_COMMAND = '@cardstack/boxel-host/commands/get-card-type-schema/default'; @@ -68,7 +71,7 @@ export async function fetchCardTypeSchema( ); if (response.status !== 'ready' || !response.result) { - console.warn( + log.warn( `[darkfactory-schemas] Failed to fetch schema for ${cacheKey}: ${response.error ?? response.status}`, ); return undefined; @@ -85,7 +88,7 @@ export async function fetchCardTypeSchema( schemaCache.set(cacheKey, result); return result; } catch { - console.warn( + log.warn( `[darkfactory-schemas] Failed to parse schema for ${cacheKey}`, ); return undefined; diff --git a/packages/software-factory/scripts/lib/factory-implement.ts b/packages/software-factory/scripts/lib/factory-implement.ts index 39f43847508..191b607b8fa 100644 --- a/packages/software-factory/scripts/lib/factory-implement.ts +++ b/packages/software-factory/scripts/lib/factory-implement.ts @@ -15,6 +15,8 @@ import { resolve } from 'node:path'; +import { logger } from '../../src/logger'; + import type { KnowledgeArticle, ProjectCard, @@ -71,6 +73,8 @@ import type { FactoryBootstrapResult } from '../../src/factory-bootstrap'; // Constants // --------------------------------------------------------------------------- +let log = logger('factory-implement'); + const PACKAGE_ROOT = resolve(__dirname, '../..'); // --------------------------------------------------------------------------- @@ -248,9 +252,9 @@ export async function runFactoryImplement( if (loopResult.outcome === 'tests_passed' || loopResult.outcome === 'done') { try { await updateTicketStatus(targetRealmUrl, ticket.id, 'done', fetchOptions); - console.error('[factory-implement] Updated ticket status to done'); + log.error('[factory-implement] Updated ticket status to done'); } catch (error) { - console.warn( + log.warn( `[factory-implement] Could not update ticket status: ${ error instanceof Error ? error.message : String(error) }`, @@ -405,7 +409,7 @@ async function fetchCardData( knowledge.push(card); } catch { // Non-fatal: knowledge articles are supplementary - console.warn( + log.warn( `[factory-implement] Could not fetch knowledge article: ${ka.id}`, ); } @@ -512,7 +516,7 @@ function buildTestRunner( let start = Date.now(); try { - console.error( + log.error( `[factory-implement] Running test file(s) for ticket: ${slug}`, ); @@ -529,7 +533,7 @@ function buildTestRunner( }); let durationMs = Date.now() - start; - console.error( + log.error( `[factory-implement] Test run complete: status=${handle.status} (${durationMs}ms)`, ); @@ -579,7 +583,7 @@ function buildTestRunner( } } catch (error) { let durationMs = Date.now() - start; - console.error( + log.error( `[factory-implement] Test execution error: ${ error instanceof Error ? error.message : String(error) }`, @@ -771,7 +775,7 @@ async function loadDarkFactorySchemas( schemas.set(cardName, schema); } } catch (error) { - console.warn( + log.warn( `[factory-implement] Could not fetch schema for ${cardName}: ${ error instanceof Error ? error.message : String(error) }`, @@ -792,7 +796,7 @@ async function loadDarkFactorySchemas( schemas.set(name, schema); } } catch (error) { - console.warn( + log.warn( `[factory-implement] Could not fetch schema for ${name}: ${ error instanceof Error ? error.message : String(error) }`, diff --git a/packages/software-factory/scripts/lib/factory-skill-loader.ts b/packages/software-factory/scripts/lib/factory-skill-loader.ts index 82d74bdc7b9..581752a2a57 100644 --- a/packages/software-factory/scripts/lib/factory-skill-loader.ts +++ b/packages/software-factory/scripts/lib/factory-skill-loader.ts @@ -1,6 +1,7 @@ import { readdir, readFile, stat } from 'node:fs/promises'; import { join, resolve } from 'node:path'; +import { logger } from '../../src/logger'; import type { ProjectCard, ResolvedSkill, TicketCard } from './factory-agent'; // --------------------------------------------------------------------------- @@ -18,6 +19,8 @@ const DEFAULT_SKILLS_DIR = join(PACKAGE_ROOT, '.agents', 'skills'); */ const DEFAULT_FALLBACK_DIRS = [join(MONOREPO_ROOT, '.agents', 'skills')]; +let log = logger('factory-skill-loader'); + /** Approximate characters per token for budget estimation. */ const CHARS_PER_TOKEN = 4; @@ -230,7 +233,7 @@ export class SkillLoader implements SkillLoaderInterface { let skill = await this.load(name, ticket); results.push(skill); } catch (error) { - console.warn( + log.warn( `[SkillLoader] Skipping unavailable skill "${name}": ${ error instanceof Error ? error.message : String(error) }`, @@ -378,7 +381,7 @@ export function enforceSkillBudget( let skillTokens = estimateTokens(skill); if (usedTokens + skillTokens > maxTokens) { - console.warn( + log.warn( `[SkillBudget] Dropping skill "${skill.name}" (${skillTokens} tokens) — ` + `would exceed budget of ${maxTokens} (used: ${usedTokens})`, ); diff --git a/packages/software-factory/scripts/lib/factory-tool-builder.ts b/packages/software-factory/scripts/lib/factory-tool-builder.ts index a990364100b..fdb9112a794 100644 --- a/packages/software-factory/scripts/lib/factory-tool-builder.ts +++ b/packages/software-factory/scripts/lib/factory-tool-builder.ts @@ -7,6 +7,7 @@ * (realm protection, per-realm JWT auth, logging). */ +import { logger } from '../../src/logger'; import type { LooseSingleCardDocument, Relationship, @@ -31,6 +32,8 @@ import { // Constants // --------------------------------------------------------------------------- +let log = logger('factory-tool-builder'); + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -130,7 +133,7 @@ export function buildFactoryTools( if (schemas?.has(cardName)) { tools.push(buildFn()); } else { - console.warn( + log.warn( `[factory-tool-builder] Omitting ${toolName} tool: no schema for ${cardName}`, ); } diff --git a/packages/software-factory/scripts/lib/realm-operations.ts b/packages/software-factory/scripts/lib/realm-operations.ts index 8797475113b..8506588b635 100644 --- a/packages/software-factory/scripts/lib/realm-operations.ts +++ b/packages/software-factory/scripts/lib/realm-operations.ts @@ -8,6 +8,7 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; +import { logger } from '../../src/logger'; import type { LooseSingleCardDocument } from '@cardstack/runtime-common'; import { APP_BOXEL_REALMS_EVENT_TYPE } from '@cardstack/runtime-common/matrix-constants'; import { @@ -19,6 +20,8 @@ import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type export { SupportedMimeType }; +let log = logger('realm-operations'); + export function ensureTrailingSlash(url: string): string { return url.endsWith('/') ? url : `${url}/`; } @@ -777,12 +780,12 @@ async function addRealmToMatrixAccountData( body: JSON.stringify({ realms: existingRealms }), }); if (!putResponse.ok) { - console.warn( + log.warn( `Warning: failed to update Matrix account data for realm ${realmUrl}: HTTP ${putResponse.status}`, ); } } catch (err) { - console.warn( + log.warn( `Warning: failed to update Matrix account data for realm ${realmUrl}: ${err instanceof Error ? err.message : String(err)}`, ); } diff --git a/packages/software-factory/scripts/lib/test-run-execution.ts b/packages/software-factory/scripts/lib/test-run-execution.ts index b891c76427f..c7d1a7e58da 100644 --- a/packages/software-factory/scripts/lib/test-run-execution.ts +++ b/packages/software-factory/scripts/lib/test-run-execution.ts @@ -2,6 +2,8 @@ import { createServer, type Server } from 'node:http'; import { readFileSync } from 'node:fs'; import { normalize, resolve } from 'node:path'; +import { logger } from '../../src/logger'; + import { chromium } from '@playwright/test'; import { ensureTrailingSlash, searchRealm } from './realm-operations'; @@ -14,6 +16,8 @@ import type { TestRunRealmOptions, } from './test-run-types'; +let log = logger('test-run-execution'); + // --------------------------------------------------------------------------- // Resume Logic // --------------------------------------------------------------------------- @@ -449,7 +453,7 @@ export async function executeTestRunFromRealm( }); setHtml(html); - console.error( + log.error( `[test-run-execution] Serving QUnit page at ${testPageUrl} for realm ${options.targetRealmUrl}`, ); @@ -459,10 +463,10 @@ export async function executeTestRunFromRealm( // Forward browser console when debug is enabled if (options.debug) { page.on('console', (msg) => { - console.error(`[browser] ${msg.type()}: ${msg.text()}`); + log.error(`[browser] ${msg.type()}: ${msg.text()}`); }); page.on('pageerror', (err) => { - console.error(`[browser] PAGE ERROR: ${err.message}`); + log.error(`[browser] PAGE ERROR: ${err.message}`); }); } @@ -497,7 +501,7 @@ export async function executeTestRunFromRealm( ); let durationMs = Date.now() - start; - console.error( + log.error( `[test-run-execution] QUnit completed in ${durationMs}ms: ${qunitResults.runEnd?.testCounts?.total ?? 0} test(s)`, ); @@ -520,7 +524,7 @@ export async function executeTestRunFromRealm( } catch (err) { let durationMs = Date.now() - start; let errorMessage = err instanceof Error ? err.message : String(err); - console.error( + log.error( `[test-run-execution] Error: ${errorMessage} (${durationMs}ms)`, ); try { diff --git a/packages/software-factory/scripts/pick-ticket.ts b/packages/software-factory/scripts/pick-ticket.ts index 7892b347ca6..b003f6cb706 100644 --- a/packages/software-factory/scripts/pick-ticket.ts +++ b/packages/software-factory/scripts/pick-ticket.ts @@ -1,3 +1,6 @@ +// This should be first +import '../src/setup-logger'; + import { getAccessibleRealmTokens, matrixLogin, @@ -7,6 +10,9 @@ import { type SearchResultCard, type SearchSort, } from './lib/boxel'; +import { logger } from '../src/logger'; + +let log = logger('pick-ticket'); type TicketStatus = 'backlog' | 'in_progress' | 'blocked' | 'review' | 'done'; type TicketPriority = 'critical' | 'high' | 'medium' | 'low'; @@ -160,6 +166,6 @@ async function main(): Promise { main().catch((error: unknown) => { let message = error instanceof Error ? (error.stack ?? error.message) : String(error); - console.error(message); + log.error(message); process.exit(1); }); diff --git a/packages/software-factory/scripts/run-realm-tests.ts b/packages/software-factory/scripts/run-realm-tests.ts index 8493a2fb3d2..72fc6bb1ec2 100644 --- a/packages/software-factory/scripts/run-realm-tests.ts +++ b/packages/software-factory/scripts/run-realm-tests.ts @@ -1,3 +1,6 @@ +// This should be first +import '../src/setup-logger'; + import { spawnSync, type SpawnSyncOptionsWithStringEncoding, @@ -14,6 +17,9 @@ import { basename, dirname, join, relative, resolve } from 'node:path'; import { getActiveProfile, parseArgs } from './lib/boxel'; import { ensureTrailingSlash } from './lib/realm-operations'; +import { logger } from '../src/logger'; + +let log = logger('run-realm-tests'); type CommandOptions = { cwd?: string; @@ -310,7 +316,7 @@ let summary = { failures, }; -console.log(JSON.stringify(summary, null, 2)); +log.info(JSON.stringify(summary, null, 2)); if (testRun.status !== 0) { process.exit(testRun.status ?? 1); diff --git a/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts index 92edfd83ac7..f1170700ae6 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts @@ -47,7 +47,11 @@ * Falls back to FACTORY_DEFAULT_MODEL (anthropic/claude-sonnet-4) */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import { OpenRouterFactoryAgent, @@ -55,6 +59,8 @@ import { type AgentContext, } from '../lib/factory-agent'; +let log = logger('factory-agent-smoke'); + async function main(): Promise { let argv = process.argv.slice(2); // Strip leading '--' that pnpm/ts-node passes through @@ -73,7 +79,7 @@ async function main(): Promise { let realmServerUrl = values['realm-server-url']; if (!realmServerUrl) { - console.error( + log.error( 'Usage: pnpm smoke:agent --realm-server-url [--model ]', ); process.exit(1); @@ -85,9 +91,9 @@ async function main(): Promise { } let model = resolveFactoryModel(values.model); - console.log(`Model: ${model}`); - console.log(`Realm server: ${realmServerUrl}`); - console.log(); + log.info(`Model: ${model}`); + log.info(`Realm server: ${realmServerUrl}`); + log.info(); let agent = new OpenRouterFactoryAgent({ model, @@ -113,18 +119,18 @@ async function main(): Promise { testRealmUrl: 'https://example.test/user/target-tests/', }; - console.log('Sending plan() request...'); - console.log(); + log.info('Sending plan() request...'); + log.info(); let actions = await agent.plan(context); - console.log(`Received ${actions.length} action(s):`); - console.log(JSON.stringify(actions, null, 2)); - console.log(); - console.log('Smoke test passed.'); + log.info(`Received ${actions.length} action(s):`); + log.info(JSON.stringify(actions, null, 2)); + log.info(); + log.info('Smoke test passed.'); } main().catch((err) => { - console.error('Smoke test failed:', err); + log.error('Smoke test failed:', err); process.exit(1); }); diff --git a/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts index 963ee61a305..fffa74c03f4 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-context-smoke.ts @@ -9,7 +9,11 @@ * pnpm smoke:context --max-tokens 8000 */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import type { KnowledgeArticle, @@ -27,16 +31,18 @@ import { // Helpers // --------------------------------------------------------------------------- +let log = logger('factory-context-smoke'); + let passed = 0; let failed = 0; function check(label: string, ok: boolean, detail?: string): void { if (ok) { passed++; - console.log(` \u2713 ${label}`); + log.info(` \u2713 ${label}`); } else { failed++; - console.log(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); + log.info(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); } } @@ -115,7 +121,7 @@ async function main(): Promise { !Number.isFinite(maxSkillTokens) || maxSkillTokens <= 0) ) { - console.error( + log.error( `Invalid value for --max-tokens: "${values['max-tokens']}". ` + 'Please provide a positive numeric value.', ); @@ -128,14 +134,14 @@ async function main(): Promise { maxSkillTokens, }); - console.log(''); - console.log('=== Context Builder Smoke Test ==='); - console.log(''); + log.info(''); + log.info('=== Context Builder Smoke Test ==='); + log.info(''); for (let { label, ticket } of SAMPLE_TICKETS) { - console.log(`--- ${label} ---`); - console.log(` Ticket: ${ticket.title}`); - console.log(''); + log.info(`--- ${label} ---`); + log.info(` Ticket: ${ticket.title}`); + log.info(''); // ------------------------------------------------------------------- // First pass (no test results) @@ -149,7 +155,7 @@ async function main(): Promise { testRealmUrl: 'https://example.test/user/target-test-artifacts/', }); - console.log(' First pass (no test results):'); + log.info(' First pass (no test results):'); check('project.id set', ctx.project.id === SAMPLE_PROJECT.id); check('ticket.id set', ctx.ticket.id === ticket.id); check( @@ -172,12 +178,12 @@ async function main(): Promise { ); let totalTokens = ctx.skills.reduce((s, sk) => s + estimateTokens(sk), 0); - console.log(` Skill breakdown (~${totalTokens} total tokens):`); + log.info(` Skill breakdown (~${totalTokens} total tokens):`); for (let skill of ctx.skills) { let tokens = estimateTokens(skill); let refCount = skill.references?.length ?? 0; let refNote = refCount > 0 ? ` + ${refCount} ref(s)` : ''; - console.log(` - ${skill.name}: ~${tokens} tokens${refNote}`); + log.info(` - ${skill.name}: ~${tokens} tokens${refNote}`); } // ------------------------------------------------------------------- @@ -205,8 +211,8 @@ async function main(): Promise { }, }); - console.log(''); - console.log(' Iteration pass (with failed test results):'); + log.info(''); + log.info(' Iteration pass (with failed test results):'); check( 'testResults.status = failed', ctxWithResults.testResults?.status === 'failed', @@ -232,7 +238,7 @@ async function main(): Promise { ctxWithResults.iteration === undefined, ); - console.log(''); + log.info(''); } // ----------------------------------------------------------------------- @@ -240,8 +246,8 @@ async function main(): Promise { // ----------------------------------------------------------------------- if (maxSkillTokens) { - console.log(`--- Budget enforcement (${maxSkillTokens} tokens) ---`); - console.log(''); + log.info(`--- Budget enforcement (${maxSkillTokens} tokens) ---`); + log.info(''); let ctx = await builder.build({ project: SAMPLE_PROJECT, @@ -257,19 +263,19 @@ async function main(): Promise { totalTokens <= maxSkillTokens, ); for (let skill of ctx.skills) { - console.log(` - ${skill.name}: ~${estimateTokens(skill)} tokens`); + log.info(` - ${skill.name}: ~${estimateTokens(skill)} tokens`); } - console.log(''); + log.info(''); } // ----------------------------------------------------------------------- // Summary // ----------------------------------------------------------------------- - console.log('==========================='); - console.log(` ${passed} passed, ${failed} failed`); - console.log('==========================='); - console.log(''); + log.info('==========================='); + log.info(` ${passed} passed, ${failed} failed`); + log.info('==========================='); + log.info(''); if (failed > 0) { process.exit(1); @@ -277,7 +283,7 @@ async function main(): Promise { } main().catch((err: unknown) => { - console.error( + log.error( 'Smoke test failed:', err instanceof Error ? err.message : String(err), ); diff --git a/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts index d62810d9b5f..942f4ad73b8 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-loop-smoke.ts @@ -9,7 +9,11 @@ * pnpm smoke:loop --max-iterations 3 */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import type { AgentContext, @@ -36,16 +40,18 @@ import { // Helpers // --------------------------------------------------------------------------- +let log = logger('factory-loop-smoke'); + let passed = 0; let failed = 0; function check(label: string, ok: boolean, detail?: string): void { if (ok) { passed++; - console.log(` \u2713 ${label}`); + log.info(` \u2713 ${label}`); } else { failed++; - console.log(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); + log.info(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); } } @@ -195,16 +201,16 @@ function makeBaseConfig( } function printResult(result: FactoryLoopResult): void { - console.log(` outcome: ${result.outcome}`); - console.log(` iterations: ${result.iterations}`); - console.log(` tool calls: ${result.toolCallLog.length}`); + log.info(` outcome: ${result.outcome}`); + log.info(` iterations: ${result.iterations}`); + log.info(` tool calls: ${result.toolCallLog.length}`); if (result.testResults) { - console.log( + log.info( ` tests: ${result.testResults.passedCount} passed, ${result.testResults.failedCount} failed`, ); } if (result.message) { - console.log(` message: ${result.message}`); + log.info(` message: ${result.message}`); } } @@ -213,8 +219,8 @@ function printResult(result: FactoryLoopResult): void { // --------------------------------------------------------------------------- async function scenarioHappyPath(maxIterations: number): Promise { - console.log('--- Scenario 1: Happy path (implement + test pass) ---'); - console.log(''); + log.info('--- Scenario 1: Happy path (implement + test pass) ---'); + log.info(''); let agent = new MockLoopAgent([ { @@ -264,12 +270,12 @@ async function scenarioHappyPath(maxIterations: number): Promise { check('completed in 1 iteration', result.iterations === 1); check('4 tool calls logged', result.toolCallLog.length === 4); check('tests passed', result.testResults?.status === 'passed'); - console.log(''); + log.info(''); } async function scenarioIterationPath(maxIterations: number): Promise { - console.log('--- Scenario 2: Iteration path (fail then fix) ---'); - console.log(''); + log.info('--- Scenario 2: Iteration path (fail then fix) ---'); + log.info(''); let agent = new MockLoopAgent([ { @@ -326,12 +332,12 @@ async function scenarioIterationPath(maxIterations: number): Promise { 'context threading: second call had failing testResults', agent.receivedContexts[1].testResults?.status === 'failed', ); - console.log(''); + log.info(''); } async function scenarioMaxIterations(maxIterations: number): Promise { - console.log(`--- Scenario 3: Max iterations (${maxIterations} failures) ---`); - console.log(''); + log.info(`--- Scenario 3: Max iterations (${maxIterations} failures) ---`); + log.info(''); let configs: MockRunConfig[] = []; let testResults: TestResult[] = []; @@ -369,12 +375,12 @@ async function scenarioMaxIterations(maxIterations: number): Promise { result.iterations === maxIterations, ); check('last test result was failed', result.testResults?.status === 'failed'); - console.log(''); + log.info(''); } async function scenarioDoneSignal(): Promise { - console.log('--- Scenario 4: Bare done signal (no work) ---'); - console.log(''); + log.info('--- Scenario 4: Bare done signal (no work) ---'); + log.info(''); let result = await runFactoryLoop( makeBaseConfig({ @@ -387,12 +393,12 @@ async function scenarioDoneSignal(): Promise { check('1 iteration', result.iterations === 1); check('no tool calls', result.toolCallLog.length === 0); check('no test results', result.testResults === undefined); - console.log(''); + log.info(''); } async function scenarioClarification(): Promise { - console.log('--- Scenario 5: Clarification needed (blocked) ---'); - console.log(''); + log.info('--- Scenario 5: Clarification needed (blocked) ---'); + log.info(''); let result = await runFactoryLoop( makeBaseConfig({ @@ -413,12 +419,12 @@ async function scenarioClarification(): Promise { ); check('message preserved', result.message?.includes('color scheme') ?? false); check('tool calls before block recorded', result.toolCallLog.length === 1); - console.log(''); + log.info(''); } async function scenarioToolOnlyRound(): Promise { - console.log('--- Scenario 6: Tool-only round (read then write) ---'); - console.log(''); + log.info('--- Scenario 6: Tool-only round (read then write) ---'); + log.info(''); let agent = new MockLoopAgent([ { @@ -465,7 +471,7 @@ async function scenarioToolOnlyRound(): Promise { 'all 4 tool calls across both rounds logged', result.toolCallLog.length === 4, ); - console.log(''); + log.info(''); } // --------------------------------------------------------------------------- @@ -487,17 +493,17 @@ async function main(): Promise { : 5; if (!Number.isFinite(maxIterations) || maxIterations <= 0) { - console.error( + log.error( `Invalid --max-iterations: "${values['max-iterations']}". Must be a positive number.`, ); process.exit(1); } - console.log(''); - console.log('=== Factory Loop Smoke Test ==='); - console.log(''); - console.log(`maxIterations: ${maxIterations}`); - console.log(''); + log.info(''); + log.info('=== Factory Loop Smoke Test ==='); + log.info(''); + log.info(`maxIterations: ${maxIterations}`); + log.info(''); await scenarioHappyPath(maxIterations); await scenarioIterationPath(maxIterations); @@ -506,10 +512,10 @@ async function main(): Promise { await scenarioClarification(); await scenarioToolOnlyRound(); - console.log('==========================='); - console.log(` ${passed} passed, ${failed} failed`); - console.log('==========================='); - console.log(''); + log.info('==========================='); + log.info(` ${passed} passed, ${failed} failed`); + log.info('==========================='); + log.info(''); if (failed > 0) { process.exit(1); @@ -517,7 +523,7 @@ async function main(): Promise { } main().catch((err: unknown) => { - console.error( + log.error( 'Smoke test failed:', err instanceof Error ? err.message : String(err), ); diff --git a/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts index d81a41b8283..dc7d122ca35 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts @@ -12,7 +12,11 @@ * pnpm smoke:prompt -- --stage all (default) */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import type { AgentAction, AgentContext } from '../lib/factory-agent'; @@ -136,18 +140,20 @@ const SAMPLE_PREVIOUS_ACTIONS: AgentAction[] = [ // Helpers // --------------------------------------------------------------------------- +let log = logger('factory-prompt-smoke'); + function separator(label: string): void { let line = '═'.repeat(72); - console.log(`\n${line}`); - console.log(` ${label}`); - console.log(`${line}\n`); + log.info(`\n${line}`); + log.info(` ${label}`); + log.info(`${line}\n`); } function printMessages(messages: { role: string; content: string }[]): void { for (let msg of messages) { - console.log(`── [${msg.role.toUpperCase()}] ──────────────────────────`); - console.log(msg.content); - console.log(); + log.info(`── [${msg.role.toUpperCase()}] ──────────────────────────`); + log.info(msg.content); + log.info(); } } @@ -169,7 +175,7 @@ function main(): void { let stage = (values.stage ?? 'all') as Stage; if (!['all', 'implement', 'iterate', 'test'].includes(stage)) { - console.error( + log.error( `Invalid stage: "${stage}". Must be one of: all, implement, iterate, test`, ); process.exit(1); @@ -186,7 +192,7 @@ function main(): void { let userPrompt = assembleImplementPrompt({ context: ctx, loader }); let messages = buildOneShotMessages(systemPrompt, userPrompt); printMessages(messages); - console.log( + log.info( `📊 System: ${systemPrompt.length} chars | User: ${userPrompt.length} chars`, ); } @@ -221,7 +227,7 @@ function main(): void { }); let messages = buildOneShotMessages(systemPrompt, userPrompt); printMessages(messages); - console.log( + log.info( `📊 System: ${systemPrompt.length} chars | User: ${userPrompt.length} chars`, ); } @@ -242,14 +248,14 @@ function main(): void { }); let messages = buildOneShotMessages(systemPrompt, userPrompt); printMessages(messages); - console.log( + log.info( `📊 System: ${systemPrompt.length} chars | User: ${userPrompt.length} chars`, ); } separator('DONE'); - console.log('All prompt templates assembled successfully.'); - console.log( + log.info('All prompt templates assembled successfully.'); + log.info( 'The prompts above are exactly what the LLM would receive in a one-shot call.', ); } diff --git a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts index bd58a9c30be..1960b083b94 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts @@ -9,7 +9,11 @@ * pnpm smoke:skill --ticket-text "Create a .gts component with styling" */ +// This should be first +import '../../src/setup-logger'; + import { parseArgs } from 'node:util'; +import { logger } from '../../src/logger'; import { DefaultSkillResolver, @@ -58,6 +62,8 @@ const SAMPLE_TICKETS: { label: string; ticket: TicketCard }[] = [ }, ]; +let log = logger('factory-skill-smoke'); + async function main(): Promise { let { values } = parseArgs({ args: process.argv.slice(2), @@ -77,7 +83,7 @@ async function main(): Promise { values['max-tokens'] !== undefined && (maxTokens === undefined || !Number.isFinite(maxTokens) || maxTokens <= 0) ) { - console.error( + log.error( `Invalid value for --max-tokens: "${values['max-tokens']}". ` + 'Please provide a positive numeric value.', ); @@ -90,7 +96,7 @@ async function main(): Promise { let loader = new SkillLoader(); let project: ProjectCard = { id: 'Projects/smoke-test' }; - console.log('=== Skill Loader & Resolver Smoke Test ===\n'); + log.info('=== Skill Loader & Resolver Smoke Test ===\n'); // If custom ticket text is provided, use only that let tickets = customTicketText @@ -107,22 +113,22 @@ async function main(): Promise { : SAMPLE_TICKETS; for (let { label, ticket } of tickets) { - console.log(`--- ${label} ---`); - console.log(` Ticket: ${ticket.title}`); + log.info(`--- ${label} ---`); + log.info(` Ticket: ${ticket.title}`); // 1. Resolve let skillNames = resolver.resolve(ticket, project); - console.log(` Resolved skills: [${skillNames.join(', ')}]`); + log.info(` Resolved skills: [${skillNames.join(', ')}]`); // 2. Load (with ticket context for reference filtering) let skills = await loader.loadAll(skillNames, ticket); - console.log(` Loaded: ${skills.length}/${skillNames.length} skills`); + log.info(` Loaded: ${skills.length}/${skillNames.length} skills`); for (let skill of skills) { let tokens = estimateTokens(skill); let refCount = skill.references?.length ?? 0; let refNote = refCount > 0 ? ` + ${refCount} reference(s)` : ''; - console.log(` - ${skill.name}: ~${tokens} tokens${refNote}`); + log.info(` - ${skill.name}: ~${tokens} tokens${refNote}`); } // 3. Budget enforcement @@ -130,7 +136,7 @@ async function main(): Promise { let budgeted = enforceSkillBudget(skills, maxTokens); let totalBefore = skills.reduce((s, sk) => s + estimateTokens(sk), 0); let totalAfter = budgeted.reduce((s, sk) => s + estimateTokens(sk), 0); - console.log( + log.info( ` Budget (${maxTokens} tokens): ` + `${budgeted.length}/${skills.length} skills kept ` + `(${totalAfter}/${totalBefore} tokens)`, @@ -139,15 +145,15 @@ async function main(): Promise { let dropped = skills.filter( (s) => !budgeted.find((b) => b.name === s.name), ); - console.log(` Dropped: [${dropped.map((d) => d.name).join(', ')}]`); + log.info(` Dropped: [${dropped.map((d) => d.name).join(', ')}]`); } } - console.log(); + log.info(); } // Summary: list all discoverable skills - console.log('--- All discoverable skills ---'); + log.info('--- All discoverable skills ---'); let allSkillNames = [ 'boxel-development', 'boxel-file-structure', @@ -168,22 +174,22 @@ async function main(): Promise { grandTotal += tokens; let refCount = skill.references?.length ?? 0; let refNote = refCount > 0 ? ` (${refCount} refs)` : ''; - console.log(` ${skill.name}: ~${tokens} tokens${refNote}`); + log.info(` ${skill.name}: ~${tokens} tokens${refNote}`); } let missing = allSkillNames.filter( (n) => !allSkills.find((s) => s.name === n), ); if (missing.length > 0) { - console.log(` Not found: [${missing.join(', ')}]`); + log.info(` Not found: [${missing.join(', ')}]`); } - console.log( + log.info( ` Total: ~${grandTotal} tokens across ${allSkills.length} skills`, ); - console.log('\nSmoke test passed.'); + log.info('\nSmoke test passed.'); } main().catch((err) => { - console.error('Smoke test failed:', err); + log.error('Smoke test failed:', err); process.exit(1); }); diff --git a/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts index e56a50cf70f..d7c813ff67a 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-tools-smoke.ts @@ -9,7 +9,11 @@ * pnpm smoke:tools */ +// This should be first +import '../../src/setup-logger'; + import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type'; +import { logger } from '../../src/logger'; import { ToolExecutor, @@ -29,16 +33,18 @@ import { ToolRegistry } from '../lib/factory-tool-registry'; // Helpers // --------------------------------------------------------------------------- +let log = logger('factory-tools-smoke'); + let passed = 0; let failed = 0; function check(label: string, ok: boolean, detail?: string): void { if (ok) { passed++; - console.log(` \u2713 ${label}`); + log.info(` \u2713 ${label}`); } else { failed++; - console.log(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); + log.info(` \u2717 ${label}${detail ? ` -- ${detail}` : ''}`); } } @@ -51,15 +57,15 @@ async function main(): Promise { // 1. Registry // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Tool Registry ==='); - console.log(''); + log.info(''); + log.info('=== Tool Registry ==='); + log.info(''); let registry = new ToolRegistry(); let manifests = registry.getManifests(); - console.log(`Registered tools: ${manifests.length}`); - console.log(''); + log.info(`Registered tools: ${manifests.length}`); + log.info(''); let byCategory: Record = {}; for (let m of manifests) { @@ -67,7 +73,7 @@ async function main(): Promise { } for (let [category, names] of Object.entries(byCategory)) { - console.log(` ${category} (${names.length}):`); + log.info(` ${category} (${names.length}):`); for (let name of names) { let manifest = registry.getManifest(name)!; let requiredArgs = manifest.args @@ -76,7 +82,7 @@ async function main(): Promise { let optionalArgs = manifest.args .filter((a) => !a.required) .map((a) => a.name); - console.log( + log.info( ` - ${name} [${manifest.outputFormat}]` + (requiredArgs.length ? ` required: ${requiredArgs.join(', ')}` @@ -84,7 +90,7 @@ async function main(): Promise { (optionalArgs.length ? ` optional: ${optionalArgs.join(', ')}` : ''), ); } - console.log(''); + log.info(''); } check('has script tools', byCategory['script']?.length === 4); @@ -99,9 +105,9 @@ async function main(): Promise { // 2. Argument validation // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Argument Validation ==='); - console.log(''); + log.info(''); + log.info('=== Argument Validation ==='); + log.info(''); let validErrors = registry.validateArgs('search-realm', { realm: 'http://example.test/', @@ -124,9 +130,9 @@ async function main(): Promise { // 3. Safety constraints // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Safety Constraints ==='); - console.log(''); + log.info(''); + log.info('=== Safety Constraints ==='); + log.info(''); let executor = new ToolExecutor(registry, { packageRoot: process.cwd(), @@ -169,9 +175,9 @@ async function main(): Promise { // 4. Mocked realm-api round-trip // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Realm API Round-Trip (mock) ==='); - console.log(''); + log.info(''); + log.info('=== Realm API Round-Trip (mock) ==='); + log.info(''); let mockCallCount = 0; @@ -183,7 +189,7 @@ async function main(): Promise { mockCallCount++; let url = String(input); let method = init?.method ?? 'GET'; - console.log(` -> ${method} ${url}`); + log.info(` -> ${method} ${url}`); return new Response( JSON.stringify({ data: [{ id: 'CardDef/hello', type: 'card' }], @@ -223,9 +229,9 @@ async function main(): Promise { // 5. FactoryTool builder // ----------------------------------------------------------------------- - console.log(''); - console.log('=== Factory Tool Builder ==='); - console.log(''); + log.info(''); + log.info('=== Factory Tool Builder ==='); + log.info(''); let toolBuilderFetchCount = 0; let toolBuilderFetch = (async ( @@ -235,7 +241,7 @@ async function main(): Promise { toolBuilderFetchCount++; let url = String(input); let method = init?.method ?? 'GET'; - console.log(` -> ${method} ${url}`); + log.info(` -> ${method} ${url}`); return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': SupportedMimeType.JSON }, @@ -264,14 +270,14 @@ async function main(): Promise { ); let toolNames = factoryTools.map((t) => t.name); - console.log(` Built ${factoryTools.length} tools:`); - console.log( + log.info(` Built ${factoryTools.length} tools:`); + log.info( ` factory: ${toolNames.filter((n) => ['write_file', 'read_file', 'search_realm', 'run_command', 'signal_done', 'request_clarification', 'update_project', 'update_ticket', 'create_knowledge'].includes(n)).join(', ')}`, ); - console.log( + log.info( ` registered: ${toolNames.filter((n) => !['write_file', 'read_file', 'search_realm', 'run_command', 'signal_done', 'request_clarification', 'update_project', 'update_ticket', 'create_knowledge'].includes(n)).join(', ')}`, ); - console.log(''); + log.info(''); check('has write_file tool', toolNames.includes('write_file')); check('has read_file tool', toolNames.includes('read_file')); @@ -340,11 +346,11 @@ async function main(): Promise { // Summary // ----------------------------------------------------------------------- - console.log(''); - console.log('==========================='); - console.log(` ${passed} passed, ${failed} failed`); - console.log('==========================='); - console.log(''); + log.info(''); + log.info('==========================='); + log.info(` ${passed} passed, ${failed} failed`); + log.info('==========================='); + log.info(''); if (failed > 0) { process.exit(1); @@ -352,7 +358,7 @@ async function main(): Promise { } main().catch((err: unknown) => { - console.error( + log.error( 'Smoke test failed:', err instanceof Error ? err.message : String(err), ); diff --git a/packages/software-factory/scripts/test.ts b/packages/software-factory/scripts/test.ts index f6692c84ede..0e1109c1eb5 100644 --- a/packages/software-factory/scripts/test.ts +++ b/packages/software-factory/scripts/test.ts @@ -1,5 +1,12 @@ +process.env.LOG_LEVELS = process.env.LOG_LEVELS || '*=error'; +// This should be first +import '../src/setup-logger'; + import { spawn } from 'node:child_process'; import { resolve } from 'node:path'; +import { logger } from '../src/logger'; + +let log = logger('test'); const packageRoot = resolve(__dirname, '..'); @@ -9,7 +16,7 @@ type TestRunnerOptions = { headed: boolean; }; -const defaultNodeTestLogLevels = '*=info,prerenderer-chrome=none'; +const defaultNodeTestLogLevels = '*=error'; function parseArgs(argv: string[]): TestRunnerOptions { let options: TestRunnerOptions = { @@ -89,6 +96,6 @@ async function main(): Promise { main().catch((error: unknown) => { let message = error instanceof Error ? error.message : String(error); - console.error(message); + log.error(message); process.exit(1); }); diff --git a/packages/software-factory/src/cli/cache-realm.ts b/packages/software-factory/src/cli/cache-realm.ts index a290d3b6283..f09de3bf6fb 100644 --- a/packages/software-factory/src/cli/cache-realm.ts +++ b/packages/software-factory/src/cli/cache-realm.ts @@ -1,3 +1,6 @@ +// This should be first +import '../setup-logger'; + import { mkdirSync, writeFileSync } from 'node:fs'; import { basename, dirname, resolve } from 'node:path'; @@ -7,6 +10,9 @@ import { } from '../harness'; import { isFactorySupportContext } from '../harness/shared'; import { readSupportContext } from '../runtime-metadata'; +import { logger } from '../logger'; + +let log = logger('cache-realm'); async function main(): Promise { let args = process.argv.slice(2).filter((arg) => !arg.startsWith('--')); @@ -38,13 +44,13 @@ async function main(): Promise { try { let response = await fetch(hostURL); if (!response.ok) { - console.warn( + log.warn( `Stale support context: hostURL ${hostURL} returned ${response.status}, ignoring cached context`, ); supportContext = undefined; } } catch { - console.warn( + log.warn( `Stale support context: hostURL ${hostURL} is not reachable, ignoring cached context`, ); supportContext = undefined; @@ -60,13 +66,13 @@ async function main(): Promise { try { let response = await fetch(`${matrixURL}/_matrix/client/versions`); if (!response.ok) { - console.warn( + log.warn( `Stale support context: matrixURL ${matrixURL} returned ${response.status}, ignoring cached context`, ); supportContext = undefined; } } catch { - console.warn( + log.warn( `Stale support context: matrixURL ${matrixURL} is not reachable, ignoring cached context`, ); supportContext = undefined; @@ -147,10 +153,10 @@ async function main(): Promise { JSON.stringify(payload, null, 2), ); } - console.log(JSON.stringify(payload, null, 2)); + log.info(JSON.stringify(payload, null, 2)); } main().catch((error: unknown) => { - console.error(error); + log.error(String(error)); process.exitCode = 1; }); diff --git a/packages/software-factory/src/cli/factory-entrypoint.ts b/packages/software-factory/src/cli/factory-entrypoint.ts index 0bf0291cabb..f5b1802d6c7 100644 --- a/packages/software-factory/src/cli/factory-entrypoint.ts +++ b/packages/software-factory/src/cli/factory-entrypoint.ts @@ -1,3 +1,6 @@ +// This should be first +import '../setup-logger'; + import { FactoryEntrypointUsageError, getFactoryEntrypointUsage, @@ -6,29 +9,28 @@ import { wantsFactoryEntrypointHelp, } from '../factory-entrypoint'; import { FactoryBriefError } from '../factory-brief'; +import { logger } from '../logger'; -function log(message: string): void { - process.stderr.write(`[factory:go] ${message}\n`); -} +let log = logger('factory-entrypoint'); async function main(): Promise { try { if (wantsFactoryEntrypointHelp(process.argv.slice(2))) { - console.log(getFactoryEntrypointUsage()); + log.info(getFactoryEntrypointUsage()); return; } let options = parseFactoryEntrypointArgs(process.argv.slice(2)); - log(`mode=${options.mode} brief=${options.briefUrl}`); + log.info(`mode=${options.mode} brief=${options.briefUrl}`); if (options.mode === 'implement') { - log('Starting bootstrap + implement flow...'); + log.info('Starting bootstrap + implement flow...'); } let summary = await runFactoryEntrypoint(options); if (summary.implement) { - log( + log.info( `Implement complete: outcome=${summary.implement.outcome} ` + `iterations=${summary.implement.iterations} ` + `toolCalls=${summary.implement.toolCallCount}`, @@ -43,13 +45,13 @@ async function main(): Promise { } } catch (error) { if (error instanceof FactoryEntrypointUsageError) { - console.error(error.message); - console.error(''); - console.error(getFactoryEntrypointUsage()); + log.error(error.message); + log.error(''); + log.error(getFactoryEntrypointUsage()); } else if (error instanceof FactoryBriefError) { - console.error(error.message); + log.error(error.message); } else { - console.error(error); + log.error(String(error)); } process.exit(1); diff --git a/packages/software-factory/src/cli/serve-realm.ts b/packages/software-factory/src/cli/serve-realm.ts index 803801f90c0..f6cee9711f1 100644 --- a/packages/software-factory/src/cli/serve-realm.ts +++ b/packages/software-factory/src/cli/serve-realm.ts @@ -1,4 +1,8 @@ +// This should be first +import '../setup-logger'; + import { readSupportContext } from '../runtime-metadata'; +import { logger } from '../logger'; import { writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; @@ -6,6 +10,8 @@ import type { RealmPermissions } from '@cardstack/runtime-common'; import { startFactoryRealmServer } from '../harness'; +let log = logger('serve-realm'); + async function main(): Promise { let realmDir = resolve( process.cwd(), @@ -57,7 +63,7 @@ async function main(): Promise { ); } - console.log(JSON.stringify(payload, null, 2)); + log.info(JSON.stringify(payload, null, 2)); let cleanExit = false; process.on('exit', () => { @@ -91,6 +97,6 @@ async function main(): Promise { } main().catch((error: unknown) => { - console.error(error); + log.error(String(error)); process.exitCode = 1; }); diff --git a/packages/software-factory/src/cli/serve-support.ts b/packages/software-factory/src/cli/serve-support.ts index e75522d7dd7..b0e304e596a 100644 --- a/packages/software-factory/src/cli/serve-support.ts +++ b/packages/software-factory/src/cli/serve-support.ts @@ -1,8 +1,14 @@ +// This should be first +import '../setup-logger'; + import { mkdirSync } from 'node:fs'; import { resolve } from 'node:path'; import { startFactorySupportServices } from '../harness'; import { sharedRuntimeDir, writeSupportMetadata } from '../runtime-metadata'; +import { logger } from '../logger'; + +let log = logger('serve-support'); async function main(): Promise { let realmDir = resolve( @@ -20,7 +26,7 @@ async function main(): Promise { mkdirSync(sharedRuntimeDir, { recursive: true }); writeSupportMetadata(payload); - console.log(JSON.stringify(payload, null, 2)); + log.info(JSON.stringify(payload, null, 2)); let stop = async () => { await support.stop(); @@ -40,6 +46,6 @@ async function main(): Promise { } main().catch((error: unknown) => { - console.error(error); + log.error(String(error)); process.exitCode = 1; }); diff --git a/packages/software-factory/src/cli/smoke-realm.ts b/packages/software-factory/src/cli/smoke-realm.ts index 3ccd9f62275..7fce7c2682b 100644 --- a/packages/software-factory/src/cli/smoke-realm.ts +++ b/packages/software-factory/src/cli/smoke-realm.ts @@ -1,7 +1,13 @@ +// This should be first +import '../setup-logger'; + import { resolve } from 'node:path'; import { fetchRealmCardJson } from '../harness'; import { readSupportContext } from '../runtime-metadata'; +import { logger } from '../logger'; + +let log = logger('smoke-realm'); async function main(): Promise { let realmDir = resolve( @@ -18,7 +24,7 @@ async function main(): Promise { } let response = await fetchRealmCardJson(cardPath, { realmDir }); - console.log( + log.info( JSON.stringify( { realmDir, @@ -34,6 +40,6 @@ async function main(): Promise { } main().catch((error: unknown) => { - console.error(error); + log.error(String(error)); process.exit(1); }); diff --git a/packages/software-factory/src/cli/smoke-test-realm.ts b/packages/software-factory/src/cli/smoke-test-realm.ts index f9680888f18..45e06c7e407 100644 --- a/packages/software-factory/src/cli/smoke-test-realm.ts +++ b/packages/software-factory/src/cli/smoke-test-realm.ts @@ -34,11 +34,15 @@ * --target-realm-url */ +// This should be first +import '../setup-logger'; + import { getRealmServerToken, matrixLogin, parseArgs, } from '../../scripts/lib/boxel'; +import { logger } from '../logger'; import { executeTestRunFromRealm } from '../../scripts/lib/test-run-execution'; import { createRealm, @@ -144,6 +148,8 @@ export function runTests() { // Main // --------------------------------------------------------------------------- +let log = logger('smoke-test-realm'); + async function main() { let args = parseArgs(process.argv.slice(2)); let targetRealmUrl = (args['target-realm-url'] as string) ?? ''; @@ -151,14 +157,14 @@ async function main() { if (!targetRealmUrl) { let username = process.env.MATRIX_USERNAME; if (!username) { - console.error('Usage: pnpm smoke:test-realm -- --target-realm-url '); - console.error( + log.error('Usage: pnpm smoke:test-realm -- --target-realm-url '); + log.error( '\nRequires MATRIX_USERNAME and MATRIX_PASSWORD environment variables.', ); process.exit(1); } targetRealmUrl = `http://localhost:4201/${username}/smoke-test-realm/`; - console.log( + log.info( `No --target-realm-url specified, using default: ${targetRealmUrl}\n`, ); } @@ -188,15 +194,15 @@ async function main() { process.env.REALM_SERVER_URL = realmServerUrl; } - console.log('=== Factory Test Realm Smoke Test (QUnit) ===\n'); - console.log(`Target realm: ${targetRealmUrl}`); - console.log(`Realm server: ${realmServerUrl}`); - console.log(`Test results module: ${testResultsModuleUrl}`); + log.info('=== Factory Test Realm Smoke Test (QUnit) ===\n'); + log.info(`Target realm: ${targetRealmUrl}`); + log.info(`Realm server: ${realmServerUrl}`); + log.info(`Test results module: ${testResultsModuleUrl}`); // Authenticate via Matrix to get a realm server JWT for realm creation let matrixAuth = await matrixLogin(); let serverToken = await getRealmServerToken(matrixAuth); - console.log(`Auth: server token obtained\n`); + log.info(`Auth: server token obtained\n`); let fetchImpl = globalThis.fetch; let authorization: string | undefined = serverToken; @@ -205,10 +211,10 @@ async function main() { // Phase 0: Ensure the target realm exists // ------------------------------------------------------------------------- - console.log('--- Phase 0: Ensuring target realm exists ---\n'); + log.info('--- Phase 0: Ensuring target realm exists ---\n'); let realmDisplayName = realmEndpoint.replace(/-/g, ' '); - console.log(` Creating realm: ${realmEndpoint}...`); + log.info(` Creating realm: ${realmEndpoint}...`); let createResult = await createRealm(realmServerUrl, { name: realmDisplayName, endpoint: realmEndpoint, @@ -221,19 +227,19 @@ async function main() { }); if (createResult.created) { - console.log(` Created: ${createResult.realmUrl}\n`); + log.info(` Created: ${createResult.realmUrl}\n`); } else if (createResult.error?.includes('already exists')) { - console.log(` Realm already exists.\n`); + log.info(` Realm already exists.\n`); } else { - console.error(` Failed to create realm: ${createResult.error}`); + log.error(` Failed to create realm: ${createResult.error}`); process.exit(1); } // Get realm-scoped JWT now that the realm exists - console.log(' Authenticating with new realm...'); + log.info(' Authenticating with new realm...'); let realmAuth = await getRealmScopedAuth(realmServerUrl, serverToken); if (realmAuth.error) { - console.warn( + log.warn( ` Warning: could not get realm-scoped auth: ${realmAuth.error}`, ); } else { @@ -241,9 +247,9 @@ async function main() { let realmToken = realmAuth.tokens[targetRealmUrl]; if (realmToken) { authorization = realmToken; - console.log(' Realm-scoped JWT obtained.\n'); + log.info(' Realm-scoped JWT obtained.\n'); } else { - console.warn( + log.warn( ` Warning: no token for ${targetRealmUrl} in realm-auth response\n`, ); } @@ -258,52 +264,52 @@ async function main() { // Phase 1: Simulate LLM implementation output // ------------------------------------------------------------------------- - console.log( + log.info( '--- Phase 1: Writing LLM implementation output to target realm ---\n', ); // 1. Card definition - console.log(' Writing hello.gts (HelloCard definition)...'); + log.info(' Writing hello.gts (HelloCard definition)...'); let defResult = await writeFile( targetRealmUrl, 'hello.gts', HELLO_CARD_GTS, fetchOptions, ); - console.log( + log.info( defResult.ok ? ' ✓ hello.gts' : ` ✗ hello.gts: ${defResult.error}`, ); // 2. Spec card instance pointing to the card definition - console.log(' Writing Spec/hello-card.json (Spec card for HelloCard)...'); + log.info(' Writing Spec/hello-card.json (Spec card for HelloCard)...'); let specCardResult = await writeFile( targetRealmUrl, 'Spec/hello-card.json', JSON.stringify(HELLO_SPEC_CARD, null, 2), fetchOptions, ); - console.log( + log.info( specCardResult.ok ? ' ✓ Spec/hello-card.json' : ` ✗ Spec/hello-card.json: ${specCardResult.error}`, ); // 3. QUnit passing test (imports HelloCard from the realm) - console.log(' Writing hello.test.gts (QUnit passing test)...'); + log.info(' Writing hello.test.gts (QUnit passing test)...'); let testResult = await writeFile( targetRealmUrl, 'hello.test.gts', HELLO_TEST_GTS, fetchOptions, ); - console.log( + log.info( testResult.ok ? ' ✓ hello.test.gts' : ` ✗ hello.test.gts: ${testResult.error}`, ); // 4. QUnit deliberately failing test (imports HelloCard from the realm) - console.log( + log.info( ' Writing hello-fail.test.gts (QUnit deliberately failing test)...', ); let failTestResult = await writeFile( @@ -312,7 +318,7 @@ async function main() { HELLO_FAILING_TEST_GTS, fetchOptions, ); - console.log( + log.info( failTestResult.ok ? ' ✓ hello-fail.test.gts' : ` ✗ hello-fail.test.gts: ${failTestResult.error}`, @@ -322,7 +328,7 @@ async function main() { // Phase 2: Run QUnit tests via executeTestRunFromRealm // ------------------------------------------------------------------------- - console.log( + log.info( '\n--- Phase 2: Running QUnit tests via executeTestRunFromRealm ---\n', ); @@ -338,13 +344,13 @@ async function main() { hostAppUrl: realmServerUrl, }); - console.log(` TestRun ID: ${handle.testRunId}`); - console.log(` Status: ${handle.status}`); + log.info(` TestRun ID: ${handle.testRunId}`); + log.info(` Status: ${handle.status}`); if (handle.errorMessage) { - console.log(` Error: ${handle.errorMessage}`); + log.info(` Error: ${handle.errorMessage}`); } if ((handle as unknown as Record).error) { - console.log( + log.info( ` Complete error: ${(handle as unknown as Record).error}`, ); } @@ -353,25 +359,25 @@ async function main() { // Results // ------------------------------------------------------------------------- - console.log('\n--- Results ---\n'); + log.info('\n--- Results ---\n'); // The TestRun should have status 'failed' because it contains both a // passing and a deliberately failing QUnit test. The module results inside // should show one test passed and one test failed. let expectedStatus = handle.status === 'failed'; - console.log( + log.info( ` TestRun status: ${expectedStatus ? '✓ failed (as expected -- one test passes, one fails)' : `✗ expected failed, got ${handle.status}`}`, ); - console.log(`\n View in Boxel: ${targetRealmUrl}${handle.testRunId}`); + log.info(`\n View in Boxel: ${targetRealmUrl}${handle.testRunId}`); if (expectedStatus) { - console.log( + log.info( '\n✓ Smoke test passed! TestRun contains both pass and fail QUnit results.', ); } else { - console.log('\n✗ Smoke test had unexpected results.'); - console.log( + log.info('\n✗ Smoke test had unexpected results.'); + log.info( ` Expected "failed" (mixed pass/fail QUnit tests) but got "${handle.status}"`, ); process.exit(1); @@ -379,6 +385,6 @@ async function main() { } main().catch((err) => { - console.error(err); + log.error(String(err)); process.exit(1); }); diff --git a/packages/software-factory/src/harness/support-services.ts b/packages/software-factory/src/harness/support-services.ts index c4d925ea836..a0319973a78 100644 --- a/packages/software-factory/src/harness/support-services.ts +++ b/packages/software-factory/src/harness/support-services.ts @@ -2,6 +2,8 @@ import { spawn, spawnSync, type ChildProcess } from 'node:child_process'; import { existsSync, readFileSync, rmSync, symlinkSync } from 'node:fs'; import { join } from 'node:path'; +import { logger } from '../logger'; + import { boxelIconsDir, browserPassword, @@ -30,6 +32,7 @@ import { } from './shared'; import { canConnectToPg } from './database'; +let log = logger('support-services'); let preparePgPromise: Promise | undefined; function hostStartupLooksLikePortContention(logs: string): boolean { @@ -411,10 +414,10 @@ export async function startHarnessPrerenderServer(options: { ); child.stdout?.on('data', (data: Buffer) => { - console.log(`prerender: ${data.toString()}`); + log.info(`prerender: ${data.toString()}`); }); child.stderr?.on('data', (data: Buffer) => { - console.error(`prerender: ${data.toString()}`); + log.error(`prerender: ${data.toString()}`); }); let exitPromise = new Promise((_, reject) => { From 7cfae959046288608b2a0a1b0d444557edbd23be Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 12:30:30 -0400 Subject: [PATCH 2/4] Fix lint and test failures from logger migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix prettier violations: collapse multi-line template literals to single lines - Remove zero-argument log.info() calls (logger requires at least 1 arg) - Update test mocks: console.warn → console.error since @cardstack/logger routes all output through console.error Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/lib/darkfactory-schemas.ts | 4 +--- .../scripts/lib/factory-implement.ts | 4 +--- .../scripts/lib/test-run-execution.ts | 4 +--- .../smoke-tests/factory-agent-smoke.ts | 4 ---- .../smoke-tests/factory-prompt-smoke.ts | 1 - .../smoke-tests/factory-skill-smoke.ts | 5 +--- .../src/cli/smoke-test-realm.ts | 4 +--- .../tests/factory-skill-loader.test.ts | 24 +++++++++---------- 8 files changed, 17 insertions(+), 33 deletions(-) diff --git a/packages/software-factory/scripts/lib/darkfactory-schemas.ts b/packages/software-factory/scripts/lib/darkfactory-schemas.ts index 1a0b9c76910..c432ed37b76 100644 --- a/packages/software-factory/scripts/lib/darkfactory-schemas.ts +++ b/packages/software-factory/scripts/lib/darkfactory-schemas.ts @@ -88,9 +88,7 @@ export async function fetchCardTypeSchema( schemaCache.set(cacheKey, result); return result; } catch { - log.warn( - `[darkfactory-schemas] Failed to parse schema for ${cacheKey}`, - ); + log.warn(`[darkfactory-schemas] Failed to parse schema for ${cacheKey}`); return undefined; } } diff --git a/packages/software-factory/scripts/lib/factory-implement.ts b/packages/software-factory/scripts/lib/factory-implement.ts index 191b607b8fa..dc58dce0825 100644 --- a/packages/software-factory/scripts/lib/factory-implement.ts +++ b/packages/software-factory/scripts/lib/factory-implement.ts @@ -516,9 +516,7 @@ function buildTestRunner( let start = Date.now(); try { - log.error( - `[factory-implement] Running test file(s) for ticket: ${slug}`, - ); + log.error(`[factory-implement] Running test file(s) for ticket: ${slug}`); let handle = await executeTestRunFromRealm({ targetRealmUrl, diff --git a/packages/software-factory/scripts/lib/test-run-execution.ts b/packages/software-factory/scripts/lib/test-run-execution.ts index c7d1a7e58da..dcf55f5f9d5 100644 --- a/packages/software-factory/scripts/lib/test-run-execution.ts +++ b/packages/software-factory/scripts/lib/test-run-execution.ts @@ -524,9 +524,7 @@ export async function executeTestRunFromRealm( } catch (err) { let durationMs = Date.now() - start; let errorMessage = err instanceof Error ? err.message : String(err); - log.error( - `[test-run-execution] Error: ${errorMessage} (${durationMs}ms)`, - ); + log.error(`[test-run-execution] Error: ${errorMessage} (${durationMs}ms)`); try { await completeTestRun( testRunId, diff --git a/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts index f1170700ae6..c381842d2e9 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-agent-smoke.ts @@ -93,8 +93,6 @@ async function main(): Promise { let model = resolveFactoryModel(values.model); log.info(`Model: ${model}`); log.info(`Realm server: ${realmServerUrl}`); - log.info(); - let agent = new OpenRouterFactoryAgent({ model, realmServerUrl, @@ -120,13 +118,11 @@ async function main(): Promise { }; log.info('Sending plan() request...'); - log.info(); let actions = await agent.plan(context); log.info(`Received ${actions.length} action(s):`); log.info(JSON.stringify(actions, null, 2)); - log.info(); log.info('Smoke test passed.'); } diff --git a/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts index dc7d122ca35..e16db7520e6 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-prompt-smoke.ts @@ -153,7 +153,6 @@ function printMessages(messages: { role: string; content: string }[]): void { for (let msg of messages) { log.info(`── [${msg.role.toUpperCase()}] ──────────────────────────`); log.info(msg.content); - log.info(); } } diff --git a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts index 1960b083b94..a4bacaa1f6d 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts @@ -149,7 +149,6 @@ async function main(): Promise { } } - log.info(); } // Summary: list all discoverable skills @@ -182,9 +181,7 @@ async function main(): Promise { if (missing.length > 0) { log.info(` Not found: [${missing.join(', ')}]`); } - log.info( - ` Total: ~${grandTotal} tokens across ${allSkills.length} skills`, - ); + log.info(` Total: ~${grandTotal} tokens across ${allSkills.length} skills`); log.info('\nSmoke test passed.'); } diff --git a/packages/software-factory/src/cli/smoke-test-realm.ts b/packages/software-factory/src/cli/smoke-test-realm.ts index 45e06c7e407..2dc595864b0 100644 --- a/packages/software-factory/src/cli/smoke-test-realm.ts +++ b/packages/software-factory/src/cli/smoke-test-realm.ts @@ -239,9 +239,7 @@ async function main() { log.info(' Authenticating with new realm...'); let realmAuth = await getRealmScopedAuth(realmServerUrl, serverToken); if (realmAuth.error) { - log.warn( - ` Warning: could not get realm-scoped auth: ${realmAuth.error}`, - ); + log.warn(` Warning: could not get realm-scoped auth: ${realmAuth.error}`); } else { // Find the token for our target realm let realmToken = realmAuth.tokens[targetRealmUrl]; diff --git a/packages/software-factory/tests/factory-skill-loader.test.ts b/packages/software-factory/tests/factory-skill-loader.test.ts index f46eaa03eb0..8d194c0fa9f 100644 --- a/packages/software-factory/tests/factory-skill-loader.test.ts +++ b/packages/software-factory/tests/factory-skill-loader.test.ts @@ -522,8 +522,8 @@ module('factory-skill-loader > SkillLoader', function (hooks) { writeSkill(tempDir, 'exists', '# Exists'); let warnings: string[] = []; - let originalWarn = console.warn; - console.warn = (...args: unknown[]) => { + let originalError = console.error; + console.error = (...args: unknown[]) => { warnings.push(args.map(String).join(' ')); }; @@ -538,7 +538,7 @@ module('factory-skill-loader > SkillLoader', function (hooks) { 'logged a warning for missing skill', ); } finally { - console.warn = originalWarn; + console.error = originalError; } }); @@ -846,8 +846,8 @@ module('factory-skill-loader > enforceSkillBudget', function () { test('drops low-priority skills when over budget', function (assert) { let warnings: string[] = []; - let originalWarn = console.warn; - console.warn = (...args: unknown[]) => { + let originalError = console.error; + console.error = (...args: unknown[]) => { warnings.push(args.map(String).join(' ')); }; @@ -877,13 +877,13 @@ module('factory-skill-loader > enforceSkillBudget', function () { 'logged warning about dropped skill', ); } finally { - console.warn = originalWarn; + console.error = originalError; } }); test('sorts skills by priority before applying budget', function (assert) { - let originalWarn = console.warn; - console.warn = () => {}; // suppress warnings + let originalError = console.error; + console.error = () => {}; // suppress warnings try { let skills: ResolvedSkill[] = [ @@ -908,13 +908,13 @@ module('factory-skill-loader > enforceSkillBudget', function () { 'second priority second', ); } finally { - console.warn = originalWarn; + console.error = originalError; } }); test('unknown skills get lowest priority', function (assert) { - let originalWarn = console.warn; - console.warn = () => {}; + let originalError = console.error; + console.error = () => {}; try { let skills: ResolvedSkill[] = [ @@ -931,7 +931,7 @@ module('factory-skill-loader > enforceSkillBudget', function () { 'known skill kept over unknown', ); } finally { - console.warn = originalWarn; + console.error = originalError; } }); }); From 2721dca9529716c5f881d6df1b8274ae69404989 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 12:42:57 -0400 Subject: [PATCH 3/4] Address PR review feedback - Rename 'boxel' logger category to 'software-factory' (less ambiguous) - Remove redundant hardcoded category prefixes from log messages (logger already adds the category name) - Use process.stdout.write for machine-readable JSON output in CLI scripts (printJson, serve-realm, serve-support, cache-realm, run-realm-tests) so it isn't prefixed/filtered by the logger - Fix log levels: use info for progress/status messages, debug for forwarded browser console output, keep error only for actual failures - Add error.stack logging in catch handlers (serve-realm, factory-entrypoint) - Fix test.ts: use explicit configureLogger() call instead of relying on process.env before static import (import hoisting) - Remove unused log imports from boxel.ts and run-realm-tests.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- .../software-factory/scripts/lib/boxel.ts | 5 +---- .../scripts/lib/darkfactory-schemas.ts | 2 +- .../scripts/lib/factory-implement.ts | 20 ++++++++----------- .../scripts/lib/factory-skill-loader.ts | 4 ++-- .../scripts/lib/test-run-execution.ts | 14 ++++++------- .../scripts/run-realm-tests.ts | 5 +---- .../smoke-tests/factory-skill-smoke.ts | 1 - packages/software-factory/scripts/test.ts | 8 +++----- .../software-factory/src/cli/cache-realm.ts | 2 +- .../src/cli/factory-entrypoint.ts | 2 ++ .../software-factory/src/cli/serve-realm.ts | 8 ++++++-- .../software-factory/src/cli/serve-support.ts | 2 +- 12 files changed, 33 insertions(+), 40 deletions(-) diff --git a/packages/software-factory/scripts/lib/boxel.ts b/packages/software-factory/scripts/lib/boxel.ts index 0eedb2c369c..02bdac1b9da 100644 --- a/packages/software-factory/scripts/lib/boxel.ts +++ b/packages/software-factory/scripts/lib/boxel.ts @@ -2,12 +2,9 @@ import { existsSync, readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; -import { logger } from '../../src/logger'; import { formatErrorResponse } from '../../src/error-format'; import { ensureTrailingSlash, SupportedMimeType } from './realm-operations'; -let log = logger('boxel'); - const PROFILES_FILE = join(homedir(), '.boxel-cli', 'profiles.json'); type BoxelStoredProfile = { @@ -357,5 +354,5 @@ export function fieldPairs( } export function printJson(value: unknown): void { - log.info(JSON.stringify(value, null, 2)); + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); } diff --git a/packages/software-factory/scripts/lib/darkfactory-schemas.ts b/packages/software-factory/scripts/lib/darkfactory-schemas.ts index c432ed37b76..72bc637e018 100644 --- a/packages/software-factory/scripts/lib/darkfactory-schemas.ts +++ b/packages/software-factory/scripts/lib/darkfactory-schemas.ts @@ -88,7 +88,7 @@ export async function fetchCardTypeSchema( schemaCache.set(cacheKey, result); return result; } catch { - log.warn(`[darkfactory-schemas] Failed to parse schema for ${cacheKey}`); + log.warn(`Failed to parse schema for ${cacheKey}`); return undefined; } } diff --git a/packages/software-factory/scripts/lib/factory-implement.ts b/packages/software-factory/scripts/lib/factory-implement.ts index dc58dce0825..ea5d1db7717 100644 --- a/packages/software-factory/scripts/lib/factory-implement.ts +++ b/packages/software-factory/scripts/lib/factory-implement.ts @@ -252,10 +252,10 @@ export async function runFactoryImplement( if (loopResult.outcome === 'tests_passed' || loopResult.outcome === 'done') { try { await updateTicketStatus(targetRealmUrl, ticket.id, 'done', fetchOptions); - log.error('[factory-implement] Updated ticket status to done'); + log.info('Updated ticket status to done'); } catch (error) { log.warn( - `[factory-implement] Could not update ticket status: ${ + `Could not update ticket status: ${ error instanceof Error ? error.message : String(error) }`, ); @@ -409,9 +409,7 @@ async function fetchCardData( knowledge.push(card); } catch { // Non-fatal: knowledge articles are supplementary - log.warn( - `[factory-implement] Could not fetch knowledge article: ${ka.id}`, - ); + log.warn(`Could not fetch knowledge article: ${ka.id}`); } } @@ -516,7 +514,7 @@ function buildTestRunner( let start = Date.now(); try { - log.error(`[factory-implement] Running test file(s) for ticket: ${slug}`); + log.info(`Running test file(s) for ticket: ${slug}`); let handle = await executeTestRunFromRealm({ targetRealmUrl, @@ -531,9 +529,7 @@ function buildTestRunner( }); let durationMs = Date.now() - start; - log.error( - `[factory-implement] Test run complete: status=${handle.status} (${durationMs}ms)`, - ); + log.info(`Test run complete: status=${handle.status} (${durationMs}ms)`); if (handle.status === 'passed') { return { @@ -582,7 +578,7 @@ function buildTestRunner( } catch (error) { let durationMs = Date.now() - start; log.error( - `[factory-implement] Test execution error: ${ + `Test execution error: ${ error instanceof Error ? error.message : String(error) }`, ); @@ -774,7 +770,7 @@ async function loadDarkFactorySchemas( } } catch (error) { log.warn( - `[factory-implement] Could not fetch schema for ${cardName}: ${ + `Could not fetch schema for ${cardName}: ${ error instanceof Error ? error.message : String(error) }`, ); @@ -795,7 +791,7 @@ async function loadDarkFactorySchemas( } } catch (error) { log.warn( - `[factory-implement] Could not fetch schema for ${name}: ${ + `Could not fetch schema for ${name}: ${ error instanceof Error ? error.message : String(error) }`, ); diff --git a/packages/software-factory/scripts/lib/factory-skill-loader.ts b/packages/software-factory/scripts/lib/factory-skill-loader.ts index 581752a2a57..33ddc3383ce 100644 --- a/packages/software-factory/scripts/lib/factory-skill-loader.ts +++ b/packages/software-factory/scripts/lib/factory-skill-loader.ts @@ -234,7 +234,7 @@ export class SkillLoader implements SkillLoaderInterface { results.push(skill); } catch (error) { log.warn( - `[SkillLoader] Skipping unavailable skill "${name}": ${ + `Skipping unavailable skill "${name}": ${ error instanceof Error ? error.message : String(error) }`, ); @@ -382,7 +382,7 @@ export function enforceSkillBudget( if (usedTokens + skillTokens > maxTokens) { log.warn( - `[SkillBudget] Dropping skill "${skill.name}" (${skillTokens} tokens) — ` + + `Dropping skill "${skill.name}" (${skillTokens} tokens) — ` + `would exceed budget of ${maxTokens} (used: ${usedTokens})`, ); continue; diff --git a/packages/software-factory/scripts/lib/test-run-execution.ts b/packages/software-factory/scripts/lib/test-run-execution.ts index dcf55f5f9d5..742112f96a9 100644 --- a/packages/software-factory/scripts/lib/test-run-execution.ts +++ b/packages/software-factory/scripts/lib/test-run-execution.ts @@ -453,8 +453,8 @@ export async function executeTestRunFromRealm( }); setHtml(html); - log.error( - `[test-run-execution] Serving QUnit page at ${testPageUrl} for realm ${options.targetRealmUrl}`, + log.info( + `Serving QUnit page at ${testPageUrl} for realm ${options.targetRealmUrl}`, ); browser = await chromium.launch({ headless: true }); @@ -463,10 +463,10 @@ export async function executeTestRunFromRealm( // Forward browser console when debug is enabled if (options.debug) { page.on('console', (msg) => { - log.error(`[browser] ${msg.type()}: ${msg.text()}`); + log.debug(`[browser] ${msg.type()}: ${msg.text()}`); }); page.on('pageerror', (err) => { - log.error(`[browser] PAGE ERROR: ${err.message}`); + log.debug(`[browser] PAGE ERROR: ${err.message}`); }); } @@ -501,8 +501,8 @@ export async function executeTestRunFromRealm( ); let durationMs = Date.now() - start; - log.error( - `[test-run-execution] QUnit completed in ${durationMs}ms: ${qunitResults.runEnd?.testCounts?.total ?? 0} test(s)`, + log.info( + `QUnit completed in ${durationMs}ms: ${qunitResults.runEnd?.testCounts?.total ?? 0} test(s)`, ); // Step 3: Parse results and complete the TestRun card. @@ -524,7 +524,7 @@ export async function executeTestRunFromRealm( } catch (err) { let durationMs = Date.now() - start; let errorMessage = err instanceof Error ? err.message : String(err); - log.error(`[test-run-execution] Error: ${errorMessage} (${durationMs}ms)`); + log.error(`Error: ${errorMessage} (${durationMs}ms)`); try { await completeTestRun( testRunId, diff --git a/packages/software-factory/scripts/run-realm-tests.ts b/packages/software-factory/scripts/run-realm-tests.ts index 72fc6bb1ec2..9fdc716d4d9 100644 --- a/packages/software-factory/scripts/run-realm-tests.ts +++ b/packages/software-factory/scripts/run-realm-tests.ts @@ -17,9 +17,6 @@ import { basename, dirname, join, relative, resolve } from 'node:path'; import { getActiveProfile, parseArgs } from './lib/boxel'; import { ensureTrailingSlash } from './lib/realm-operations'; -import { logger } from '../src/logger'; - -let log = logger('run-realm-tests'); type CommandOptions = { cwd?: string; @@ -316,7 +313,7 @@ let summary = { failures, }; -log.info(JSON.stringify(summary, null, 2)); +process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); if (testRun.status !== 0) { process.exit(testRun.status ?? 1); diff --git a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts index a4bacaa1f6d..e9238da5048 100644 --- a/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts +++ b/packages/software-factory/scripts/smoke-tests/factory-skill-smoke.ts @@ -148,7 +148,6 @@ async function main(): Promise { log.info(` Dropped: [${dropped.map((d) => d.name).join(', ')}]`); } } - } // Summary: list all discoverable skills diff --git a/packages/software-factory/scripts/test.ts b/packages/software-factory/scripts/test.ts index 0e1109c1eb5..d4b44fd25f9 100644 --- a/packages/software-factory/scripts/test.ts +++ b/packages/software-factory/scripts/test.ts @@ -1,10 +1,8 @@ -process.env.LOG_LEVELS = process.env.LOG_LEVELS || '*=error'; -// This should be first -import '../src/setup-logger'; - import { spawn } from 'node:child_process'; import { resolve } from 'node:path'; -import { logger } from '../src/logger'; +import { configureLogger, logger } from '../src/logger'; + +configureLogger(process.env.LOG_LEVELS || '*=error'); let log = logger('test'); diff --git a/packages/software-factory/src/cli/cache-realm.ts b/packages/software-factory/src/cli/cache-realm.ts index f09de3bf6fb..913450adf55 100644 --- a/packages/software-factory/src/cli/cache-realm.ts +++ b/packages/software-factory/src/cli/cache-realm.ts @@ -153,7 +153,7 @@ async function main(): Promise { JSON.stringify(payload, null, 2), ); } - log.info(JSON.stringify(payload, null, 2)); + process.stdout.write(JSON.stringify(payload, null, 2) + '\n'); } main().catch((error: unknown) => { diff --git a/packages/software-factory/src/cli/factory-entrypoint.ts b/packages/software-factory/src/cli/factory-entrypoint.ts index f5b1802d6c7..5128ca4a008 100644 --- a/packages/software-factory/src/cli/factory-entrypoint.ts +++ b/packages/software-factory/src/cli/factory-entrypoint.ts @@ -50,6 +50,8 @@ async function main(): Promise { log.error(getFactoryEntrypointUsage()); } else if (error instanceof FactoryBriefError) { log.error(error.message); + } else if (error instanceof Error) { + log.error(error.stack ?? error.message); } else { log.error(String(error)); } diff --git a/packages/software-factory/src/cli/serve-realm.ts b/packages/software-factory/src/cli/serve-realm.ts index f6cee9711f1..d6f07230e28 100644 --- a/packages/software-factory/src/cli/serve-realm.ts +++ b/packages/software-factory/src/cli/serve-realm.ts @@ -63,7 +63,7 @@ async function main(): Promise { ); } - log.info(JSON.stringify(payload, null, 2)); + process.stdout.write(JSON.stringify(payload, null, 2) + '\n'); let cleanExit = false; process.on('exit', () => { @@ -97,6 +97,10 @@ async function main(): Promise { } main().catch((error: unknown) => { - log.error(String(error)); + if (error instanceof Error) { + log.error(error.stack ?? error.message); + } else { + log.error(String(error)); + } process.exitCode = 1; }); diff --git a/packages/software-factory/src/cli/serve-support.ts b/packages/software-factory/src/cli/serve-support.ts index b0e304e596a..b389afa7435 100644 --- a/packages/software-factory/src/cli/serve-support.ts +++ b/packages/software-factory/src/cli/serve-support.ts @@ -26,7 +26,7 @@ async function main(): Promise { mkdirSync(sharedRuntimeDir, { recursive: true }); writeSupportMetadata(payload); - log.info(JSON.stringify(payload, null, 2)); + process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); let stop = async () => { await support.stop(); From 1508e26b3e24b0a17ea15a2344a7b81eec862600 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Wed, 8 Apr 2026 12:53:38 -0400 Subject: [PATCH 4/4] Fix 3 failing node tests - factory-entrypoint: use console.log for --help usage text (stdout, not logger/stderr) so the integration test can find it on result.stdout - factory-skill-loader: use console.warn for skill budget/loader warnings since tests mock console.warn to verify them, and log.warn is suppressed by LOG_LEVELS='*=error' in the test runner - Revert test mocks back to console.warn accordingly Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/lib/factory-skill-loader.ts | 7 ++---- .../src/cli/factory-entrypoint.ts | 2 +- .../tests/factory-skill-loader.test.ts | 24 +++++++++---------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/software-factory/scripts/lib/factory-skill-loader.ts b/packages/software-factory/scripts/lib/factory-skill-loader.ts index 33ddc3383ce..f316c300538 100644 --- a/packages/software-factory/scripts/lib/factory-skill-loader.ts +++ b/packages/software-factory/scripts/lib/factory-skill-loader.ts @@ -1,7 +1,6 @@ import { readdir, readFile, stat } from 'node:fs/promises'; import { join, resolve } from 'node:path'; -import { logger } from '../../src/logger'; import type { ProjectCard, ResolvedSkill, TicketCard } from './factory-agent'; // --------------------------------------------------------------------------- @@ -19,8 +18,6 @@ const DEFAULT_SKILLS_DIR = join(PACKAGE_ROOT, '.agents', 'skills'); */ const DEFAULT_FALLBACK_DIRS = [join(MONOREPO_ROOT, '.agents', 'skills')]; -let log = logger('factory-skill-loader'); - /** Approximate characters per token for budget estimation. */ const CHARS_PER_TOKEN = 4; @@ -233,7 +230,7 @@ export class SkillLoader implements SkillLoaderInterface { let skill = await this.load(name, ticket); results.push(skill); } catch (error) { - log.warn( + console.warn( `Skipping unavailable skill "${name}": ${ error instanceof Error ? error.message : String(error) }`, @@ -381,7 +378,7 @@ export function enforceSkillBudget( let skillTokens = estimateTokens(skill); if (usedTokens + skillTokens > maxTokens) { - log.warn( + console.warn( `Dropping skill "${skill.name}" (${skillTokens} tokens) — ` + `would exceed budget of ${maxTokens} (used: ${usedTokens})`, ); diff --git a/packages/software-factory/src/cli/factory-entrypoint.ts b/packages/software-factory/src/cli/factory-entrypoint.ts index 5128ca4a008..9063a0b02d6 100644 --- a/packages/software-factory/src/cli/factory-entrypoint.ts +++ b/packages/software-factory/src/cli/factory-entrypoint.ts @@ -16,7 +16,7 @@ let log = logger('factory-entrypoint'); async function main(): Promise { try { if (wantsFactoryEntrypointHelp(process.argv.slice(2))) { - log.info(getFactoryEntrypointUsage()); + console.log(getFactoryEntrypointUsage()); return; } diff --git a/packages/software-factory/tests/factory-skill-loader.test.ts b/packages/software-factory/tests/factory-skill-loader.test.ts index 8d194c0fa9f..f46eaa03eb0 100644 --- a/packages/software-factory/tests/factory-skill-loader.test.ts +++ b/packages/software-factory/tests/factory-skill-loader.test.ts @@ -522,8 +522,8 @@ module('factory-skill-loader > SkillLoader', function (hooks) { writeSkill(tempDir, 'exists', '# Exists'); let warnings: string[] = []; - let originalError = console.error; - console.error = (...args: unknown[]) => { + let originalWarn = console.warn; + console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(' ')); }; @@ -538,7 +538,7 @@ module('factory-skill-loader > SkillLoader', function (hooks) { 'logged a warning for missing skill', ); } finally { - console.error = originalError; + console.warn = originalWarn; } }); @@ -846,8 +846,8 @@ module('factory-skill-loader > enforceSkillBudget', function () { test('drops low-priority skills when over budget', function (assert) { let warnings: string[] = []; - let originalError = console.error; - console.error = (...args: unknown[]) => { + let originalWarn = console.warn; + console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(' ')); }; @@ -877,13 +877,13 @@ module('factory-skill-loader > enforceSkillBudget', function () { 'logged warning about dropped skill', ); } finally { - console.error = originalError; + console.warn = originalWarn; } }); test('sorts skills by priority before applying budget', function (assert) { - let originalError = console.error; - console.error = () => {}; // suppress warnings + let originalWarn = console.warn; + console.warn = () => {}; // suppress warnings try { let skills: ResolvedSkill[] = [ @@ -908,13 +908,13 @@ module('factory-skill-loader > enforceSkillBudget', function () { 'second priority second', ); } finally { - console.error = originalError; + console.warn = originalWarn; } }); test('unknown skills get lowest priority', function (assert) { - let originalError = console.error; - console.error = () => {}; + let originalWarn = console.warn; + console.warn = () => {}; try { let skills: ResolvedSkill[] = [ @@ -931,7 +931,7 @@ module('factory-skill-loader > enforceSkillBudget', function () { 'known skill kept over unknown', ); } finally { - console.error = originalError; + console.warn = originalWarn; } }); });