From 3b75dfbc6272be2110378c97f57d5e7f5673439b Mon Sep 17 00:00:00 2001 From: Miya Date: Mon, 22 Jun 2026 14:46:22 +0200 Subject: [PATCH] fix(cli): default factory start to live --- src/cli/fleet.test.ts | 48 ++++++++++++++++++++++++++++++-- src/cli/fleet.ts | 12 ++++---- src/orchestrator/factory.test.ts | 18 ++++++------ src/orchestrator/factory.ts | 2 +- 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/src/cli/fleet.test.ts b/src/cli/fleet.test.ts index f9535ed..4910853 100644 --- a/src/cli/fleet.test.ts +++ b/src/cli/fleet.test.ts @@ -121,6 +121,14 @@ describe('fleet CLI parsing', () => { }) }) + it('defaults factory start to live mode', () => { + expect(parseFleetCommand(['start'])).toEqual({ + kind: 'factory', + action: 'start', + mode: 'live', + }) + }) + it('rejects the removed nested factory namespace', () => { expect(() => parseFleetCommand(['factory', 'run-once'])).toThrow(/Unknown factory command: factory/) }) @@ -158,7 +166,8 @@ describe('fleet CLI runtime', () => { expect(errors.text()).toBe('') expect(output.text()).toContain('usage: factory [options]') expect(output.text()).toContain('run-once') - expect(output.text()).toContain('start --mode live') + expect(output.text()).toContain('start') + expect(output.text()).toContain('default: ./factory.config.json') expect(output.text()).toContain('fleet ') expect(output.text()).not.toContain('usage: fleet') }) @@ -599,8 +608,6 @@ describe('fleet CLI runtime', () => { const code = await runFleetCli([ 'start', - '--mode', - 'live', '--config', configPath, ], { @@ -672,6 +679,41 @@ describe('fleet CLI runtime', () => { } }) + it('uses ./factory.config.json by default for factory commands', async () => { + const root = await mkdtemp(join(tmpdir(), 'fleet-cli-default-config-')) + const previousCwd = process.cwd() + try { + await writeConfig(root) + process.chdir(root) + const factory = { + start: vi.fn(async () => {}), + stop: vi.fn(async () => {}), + runLoop: vi.fn(async () => []), + runOnce: vi.fn(), + status: vi.fn(), + triageIssue: vi.fn(), + dispatch: vi.fn(), + on: vi.fn(), + dispose: vi.fn(), + } as unknown as Factory + const code = await runFleetCli(['start'], { + fleet: new FakeFleetClient(), + mount: new FakeMountClient(), + createFactory: vi.fn(() => factory), + ensureLocalMount: vi.fn(async () => {}), + waitForStopSignal: vi.fn(async () => undefined), + stdout: buffer(), + stderr: buffer(), + }) + + expect(code).toBe(0) + expect(factory.start).toHaveBeenCalledWith({ mode: 'live' }) + } finally { + process.chdir(previousCwd) + await rm(root, { recursive: true, force: true }) + } + }) + it('factory start exits cleanly on SIGTERM after the signal handler stops the factory once', async () => { const root = await mkdtemp(join(tmpdir(), 'fleet-cli-start-sigterm-')) try { diff --git a/src/cli/fleet.ts b/src/cli/fleet.ts index 9b11b16..9310e34 100644 --- a/src/cli/fleet.ts +++ b/src/cli/fleet.ts @@ -444,8 +444,8 @@ function evaluateFactoryCanary( } } -function parseFactoryStartFlags(args: Array): { mode?: 'live' } { - let mode: 'live' | undefined +function parseFactoryStartFlags(args: Array): { mode: 'live' } { + let mode: 'live' = 'live' const flags = args.filter((arg): arg is string => Boolean(arg)) for (let index = 0; index < flags.length; index += 1) { const flag = flags[index] @@ -461,8 +461,8 @@ function parseFactoryStartFlags(args: Array): { mode?: 'live } async function loadConfig(path?: string): Promise { - if (!path) throw new Error('factory commands require --config ') - const raw = JSON.parse(await readFile(path, 'utf8')) as unknown + const configPath = path ?? resolve(process.cwd(), 'factory.config.json') + const raw = JSON.parse(await readFile(configPath, 'utf8')) as unknown const record = asRecord(raw) return { config: FactoryConfigSchema.parse(record.factoryConfig ?? record), @@ -736,7 +736,7 @@ function helpText(): string { Commands: run-once Run one discovery -> triage -> dispatch cycle - start --mode live Run the live factory daemon + start Run the live factory daemon status Print current factory status as JSON loop Run the bounded loop configured in factory.config.json loop-status Print heartbeat/liveness status for the loop @@ -749,7 +749,7 @@ Commands: fleet Low-level fleet commands: spawn, roster, release Options: - --config Factory config JSON path + --config Factory config JSON path (default: ./factory.config.json) --dry-run Discover and triage without writes or agent spawns --backend Fleet backend: internal or relay -h, --help Show this help diff --git a/src/orchestrator/factory.test.ts b/src/orchestrator/factory.test.ts index 7643c40..7577389 100644 --- a/src/orchestrator/factory.test.ts +++ b/src/orchestrator/factory.test.ts @@ -1999,7 +1999,7 @@ describe('FactoryLoop', () => { const fleet = new FakeFleetClient() const factory = createFactory(config(), { mount, fleet, triage: new StaticTriage() }) - await factory.start() + await factory.start({ mode: 'backfill-and-subscribe' }) expect(fleet.spawns).toEqual([]) expect(factory.status().inFlight).toEqual([]) @@ -3296,7 +3296,7 @@ describe('FactoryLoop', () => { const fleet = new FakeFleetClient() const factory = createFactory(config(), { mount, fleet, triage: new StaticTriage() }) - await factory.start() + await factory.start({ mode: 'backfill-and-subscribe' }) expect(fleet.spawns.map((spawn) => spawn.name)).toEqual(['ar-11-impl-pear', 'ar-11-review']) expect(factory.status().inFlight.map((issue) => issue.key)).toEqual(['AR-11']) @@ -3647,7 +3647,7 @@ describe('FactoryLoop', () => { }, }) - await factory.start() + await factory.start({ mode: 'backfill-and-subscribe' }) expect(mount.activeSubscriptions).toBe(1) await expect(factory.stop()).resolves.toBeUndefined() @@ -3727,7 +3727,7 @@ describe('FactoryLoop', () => { } }) - await factory.start() + await factory.start({ mode: 'backfill-and-subscribe' }) expect(fleet.spawns.map((spawn) => spawn.name)).toEqual(['ar-15-impl-pear', 'ar-15-review']) expect(factory.status().inFlight.map((issue) => issue.key)).toEqual(['AR-15']) @@ -3741,7 +3741,7 @@ describe('FactoryLoop', () => { const fleet = new FakeFleetClient() const factory = createFactory(config(), { mount, fleet, triage: new StaticTriage() }) - await Promise.all([factory.start(), factory.start()]) + await Promise.all([factory.start({ mode: 'backfill-and-subscribe' }), factory.start({ mode: 'backfill-and-subscribe' })]) expect(mount.subscribeCount).toBe(1) expect(fleet.spawns.map((spawn) => spawn.name)).toEqual(['ar-12-impl-pear', 'ar-12-review']) @@ -3753,7 +3753,7 @@ describe('FactoryLoop', () => { const fleet = new FakeFleetClient() const factory = createFactory(config(), { mount, fleet, triage: new StaticTriage() }) - await factory.start() + await factory.start({ mode: 'backfill-and-subscribe' }) mount.emit(changeEvent(issuePath(17), 'event-duplicate-1')) mount.emit(changeEvent(issuePath(17), 'event-duplicate-2')) await flush() @@ -4869,7 +4869,7 @@ describe('FactoryLoop', () => { const factory = createFactory(config(), { mount, fleet, triage: new StaticTriage() }) const errors: unknown[] = [] factory.on('error', (payload) => errors.push(payload)) - await factory.start() + await factory.start({ mode: 'backfill-and-subscribe' }) const decision = await factory.triageIssue(parseLinearIssue(issuePath(8), issueFile(8))) await factory.dispatch(decision) @@ -5413,7 +5413,7 @@ describe('FactoryLoop', () => { }, }) - await factory.start() + await factory.start({ mode: 'backfill-and-subscribe' }) expect(factory.status().inFlight.map((issue) => issue.key)).toEqual(['AR-243']) await vi.advanceTimersByTimeAsync(0) @@ -7346,7 +7346,7 @@ describe('FactoryLoop', () => { stateStore: state, }) - await factory.start() + await factory.start({ mode: 'backfill-and-subscribe' }) expect(factory.status().counters.slackWatchersRearmed).toBeUndefined() expect(slack.roots).toEqual([]) diff --git a/src/orchestrator/factory.ts b/src/orchestrator/factory.ts index f909401..78f6192 100644 --- a/src/orchestrator/factory.ts +++ b/src/orchestrator/factory.ts @@ -392,7 +392,7 @@ export class FactoryLoop implements Factory { this.#wireFleetEvents() - if (opts.mode === 'live') { + if ((opts.mode ?? 'live') === 'live') { this.#started = true try { await this.#startLiveSubscription(opts.liveSubscription)