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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@ and sign in to that separately (it's a peer of this package). Once installed, th
CLI is available as `factory`:

```bash
factory factory run-once --config ./factory.config.json --dry-run
factory run-once --config ./factory.config.json --dry-run
```

> Yes, `factory factory` — the `factory` CLI groups its commands under a `factory`
> subcommand. From a source checkout instead of an npm install, run
> `npm ci && npm run build` first, then `node bin/factory.mjs factory <action> …`.
From a source checkout instead of an npm install, run
`npm ci && npm run build` first, then `node bin/factory.mjs <action> …`.

## Quick start

Expand All @@ -78,17 +77,17 @@ factory factory run-once --config ./factory.config.json --dry-run
but writes nothing and spawns no agents:

```bash
factory factory run-once --config ./factory.config.json --dry-run
factory run-once --config ./factory.config.json --dry-run
```

3. **Let it work for real:**

```bash
# One discovery→dispatch cycle, then exit.
factory factory run-once --config ./factory.config.json
factory run-once --config ./factory.config.json

# Or run continuously as a daemon (the production form).
factory factory start --mode live --config ./factory.config.json
factory start --mode live --config ./factory.config.json
```

> **Pulled some issues but dispatched none?** That's the safety gate doing its
Expand Down
2 changes: 1 addition & 1 deletion bin/fleet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ if (forceRebuild || !existsSync(outfile)) {
}

const mod = await import(pathToFileURL(outfile).href)
await mod.main(cliArgs)
await mod.main(['fleet', ...cliArgs])
2 changes: 1 addition & 1 deletion scripts/factory-canary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ cd "$WORKDIR" || { echo "[$TS] factory-canary: cannot cd to $WORKDIR" >&2; exit
# The canary runs the real dry-run triage path (no agents spawned) and prints a
# JSON verdict {ok,issue,status,reason}; exit code mirrors ok. A hung run
# (broker/mount wedge) is bounded by FACTORY_CANARY_TIMEOUT.
RUN=(node "$BIN" factory canary "$ISSUE" --config "$CONFIG" --backend "$BACKEND")
RUN=(node "$BIN" canary "$ISSUE" --config "$CONFIG" --backend "$BACKEND")
# A hung run (broker/mount wedge) MUST be bounded — an unbounded canary on a
# scheduler (launchd/cron) can wedge the slot forever and suppress later alerts.
# macOS has no `timeout` by default; coreutils ships it as `gtimeout`. If neither
Expand Down
102 changes: 80 additions & 22 deletions src/cli/fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const issueFile = {
describe('fleet CLI parsing', () => {
it('parses spawn flags into a FleetClient spawn input shape', () => {
expect(parseFleetCommand([
'fleet',
'spawn',
'spawn:codex',
'--node',
Expand Down Expand Up @@ -77,7 +78,6 @@ describe('fleet CLI parsing', () => {

it('parses global backend, config, and dry-run independently of subcommand position', () => {
expect(parseGlobalOptions([
'factory',
'run-once',
'--dry-run',
'--backend',
Expand All @@ -86,13 +86,12 @@ describe('fleet CLI parsing', () => {
'factory.json',
])).toEqual({
globals: { backend: 'relay', dryRun: true, config: 'factory.json' },
args: ['factory', 'run-once'],
args: ['run-once'],
})
})

it('parses manual probe close command', () => {
expect(parseFleetCommand([
'factory',
'close-probe',
'42',
'--repo',
Expand All @@ -108,20 +107,24 @@ describe('fleet CLI parsing', () => {
})

it('parses the factory orphan reaper command', () => {
expect(parseFleetCommand(['factory', 'reap-orphans'])).toEqual({
expect(parseFleetCommand(['reap-orphans'])).toEqual({
kind: 'factory',
action: 'reap-orphans',
})
})

it('parses the factory live start command', () => {
expect(parseFleetCommand(['factory', 'start', '--mode', 'live'])).toEqual({
expect(parseFleetCommand(['start', '--mode', 'live'])).toEqual({
kind: 'factory',
action: 'start',
mode: 'live',
})
})

it('rejects the removed nested factory namespace', () => {
expect(() => parseFleetCommand(['factory', 'run-once'])).toThrow(/Unknown factory command: factory/)
})

it('resolves a broker connection path by walking up from the command cwd', async () => {
const root = await mkdtemp(join(tmpdir(), 'fleet-cli-broker-'))
try {
Expand All @@ -139,6 +142,40 @@ describe('fleet CLI parsing', () => {
})

describe('fleet CLI runtime', () => {
it('prints factory help for -h without requiring config or showing internal fleet as the binary', async () => {
const output = buffer()
const errors = buffer()

const code = await runFleetCli(['-h'], {
createFleet: () => {
throw new Error('help should not construct a fleet')
},
stdout: output,
stderr: errors,
})

expect(code).toBe(0)
expect(errors.text()).toBe('')
expect(output.text()).toContain('usage: factory <command> [options]')
expect(output.text()).toContain('run-once')
expect(output.text()).toContain('start --mode live')
expect(output.text()).toContain('fleet <command>')
expect(output.text()).not.toContain('usage: fleet')
})

it('prints factory help for --help even when passed after the fleet namespace', async () => {
const output = buffer()

const code = await runFleetCli(['fleet', '--help'], {
stdout: output,
stderr: buffer(),
})

expect(code).toBe(0)
expect(output.text()).toContain('usage: factory <command> [options]')
expect(output.text()).not.toContain('usage: fleet')
})

it('uses real fleet and cloud mount for fixture-less factory configs on the operator path', async () => {
const root = await mkdtemp(join(tmpdir(), 'fleet-cli-real-default-'))
try {
Expand All @@ -151,7 +188,6 @@ describe('fleet CLI runtime', () => {
const output = buffer()

const code = await runFleetCli([
'factory',
'run-once',
'--dry-run',
'--config',
Expand Down Expand Up @@ -193,7 +229,6 @@ describe('fleet CLI runtime', () => {
const output = buffer()

const code = await runFleetCli([
'factory',
'run-once',
'--dry-run',
'--config',
Expand Down Expand Up @@ -233,7 +268,6 @@ describe('fleet CLI runtime', () => {
const stderr = buffer()

const code = await runFleetCli([
'factory',
'run-once',
'--config',
configPath,
Expand All @@ -258,7 +292,6 @@ describe('fleet CLI runtime', () => {
const output = buffer()

const code = await runFleetCli([
'factory',
'run-once',
'--dry-run',
'--config',
Expand All @@ -283,6 +316,43 @@ describe('fleet CLI runtime', () => {
expect(mount.writes).toEqual([])
})

it('prints factory status from the top-level status command', async () => {
const root = await mkdtemp(join(tmpdir(), 'fleet-cli-status-'))
try {
const configPath = await writeConfig(root)
const output = buffer()
const factoryStatus = { inFlight: [], queued: [], counters: { pulled: 0 } }
const factory = {
start: vi.fn(),
stop: vi.fn(),
runLoop: vi.fn(async () => []),
runOnce: vi.fn(),
status: vi.fn(() => factoryStatus),
triageIssue: vi.fn(),
dispatch: vi.fn(),
on: vi.fn(),
dispose: vi.fn(),
} as unknown as Factory

const code = await runFleetCli([
'status',
'--config',
configPath,
], {
fleet: new FakeFleetClient(),
mount: new FakeMountClient(),
createFactory: () => factory,
stdout: output,
stderr: buffer(),
})

expect(code).toBe(0)
expect(JSON.parse(output.text())).toEqual(factoryStatus)
} finally {
await rm(root, { recursive: true, force: true })
}
})

it('drives the real RelayFleetClient when --backend relay is requested', async () => {
// Strip every relay credential so the lazily-built HTTP transport surfaces a
// deterministic auth error instead of attempting a real network request.
Expand All @@ -294,7 +364,7 @@ describe('fleet CLI runtime', () => {
const output = buffer()
const errors = buffer()

const code = await runFleetCli(['roster', '--backend', 'relay'], {
const code = await runFleetCli(['fleet', 'roster', '--backend', 'relay'], {
stdout: output,
stderr: errors,
})
Expand Down Expand Up @@ -323,7 +393,6 @@ describe('fleet CLI runtime', () => {
const errors = buffer()

const code = await runFleetCli([
'factory',
'dispatch',
'AR-77',
'--config',
Expand All @@ -345,7 +414,6 @@ describe('fleet CLI runtime', () => {
const output = buffer()
const calls: unknown[] = []
const code = await runFleetCli([
'factory',
'close-probe',
'42',
'--repo',
Expand Down Expand Up @@ -376,7 +444,6 @@ describe('fleet CLI runtime', () => {
const output = buffer()

const code = await runFleetCli([
'factory',
'loop',
'--dry-run',
'--config',
Expand All @@ -397,7 +464,6 @@ describe('fleet CLI runtime', () => {

const statusOut = buffer()
const statusCode = await runFleetCli([
'factory',
'loop-status',
'--config',
configPath,
Expand Down Expand Up @@ -439,7 +505,6 @@ describe('fleet CLI runtime', () => {
}))

const code = await runFleetCli([
'factory',
'start',
'--mode',
'live',
Expand Down Expand Up @@ -484,7 +549,6 @@ describe('fleet CLI runtime', () => {
const resolveWorkspace = vi.fn(async () => ({ workspaceId: 'rw_unused' }))

const code = await runFleetCli([
'factory',
'start',
'--mode',
'live',
Expand Down Expand Up @@ -530,7 +594,6 @@ describe('fleet CLI runtime', () => {
const ensureLocalMount = vi.fn(async () => {})

const code = await runFleetCli([
'factory',
'start',
'--mode',
'live',
Expand Down Expand Up @@ -594,7 +657,6 @@ describe('fleet CLI runtime', () => {
const daemonExits: number[] = []

const run = runFleetCli([
'factory',
'start',
'--mode',
'live',
Expand Down Expand Up @@ -651,7 +713,6 @@ describe('fleet CLI runtime', () => {

const runOnceCode = await runFleetCli([
'--dry-run',
'factory',
'run-once',
'--config',
configPath,
Expand All @@ -670,7 +731,6 @@ describe('fleet CLI runtime', () => {
})

const reapCode = await runFleetCli([
'factory',
'reap-orphans',
'--config',
configPath,
Expand Down Expand Up @@ -718,7 +778,6 @@ describe('fleet CLI runtime', () => {

const output = buffer()
const code = await runFleetCli([
'factory',
'kill-loop',
'--config',
configPath,
Expand Down Expand Up @@ -761,7 +820,6 @@ describe('fleet CLI runtime', () => {
const output = buffer()

const code = await runFleetCli([
'factory',
'reap-orphans',
'--config',
configPath,
Expand Down
Loading