Skip to content

Commit 36ee03e

Browse files
committed
fix(cli): honor finite takeover loops
1 parent 3177eaa commit 36ee03e

2 files changed

Lines changed: 81 additions & 21 deletions

File tree

packages/cli/src/commands/takeover.test.ts

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
22
import { mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
33
import { join, basename } from 'node:path'
44
import { tmpdir } from 'node:os'
5-
import { execSync } from 'node:child_process'
5+
import { execFileSync, execSync } from 'node:child_process'
66
import {
7+
buildTakeoverRunOptions,
78
detectLanguages,
89
detectPackageManagers,
910
detectFrameworks,
@@ -595,18 +596,15 @@ describe('materializeTakeoverProject', () => {
595596
touch('README.md', '# Local Test')
596597
touch('package.json', JSON.stringify({ name: 'local-test' }))
597598
execSync('git init -b main', { cwd: tmpDir, stdio: 'pipe' })
598-
execSync('git add -A && git commit -m "init"', {
599-
cwd: tmpDir,
600-
stdio: 'pipe',
601-
env: {
602-
...process.env,
603-
GIT_AUTHOR_NAME: 'test',
604-
GIT_AUTHOR_EMAIL: 'test@example.com',
605-
GIT_COMMITTER_NAME: 'test',
606-
GIT_COMMITTER_EMAIL: 'test@example.com',
607-
},
608-
shell: '/bin/bash',
609-
})
599+
const gitEnv = {
600+
...process.env,
601+
GIT_AUTHOR_NAME: 'test',
602+
GIT_AUTHOR_EMAIL: 'test@example.com',
603+
GIT_COMMITTER_NAME: 'test',
604+
GIT_COMMITTER_EMAIL: 'test@example.com',
605+
}
606+
execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe', env: gitEnv })
607+
execFileSync('git', ['commit', '-m', 'init'], { cwd: tmpDir, stdio: 'pipe', env: gitEnv })
610608

611609
const profile = scanProject(tmpDir)
612610
const generated = generateAgentConfig(profile, undefined, 'codex')
@@ -615,13 +613,15 @@ describe('materializeTakeoverProject', () => {
615613
const skill = readFileSync(join(overlayDir, 'skills', 'takeover-context', 'SKILL.md'), 'utf-8')
616614
const agentGuide = readFileSync(join(overlayDir, 'agents', 'ceo', 'AGENT.md'), 'utf-8')
617615
const config = JSON.parse(readFileSync(join(overlayDir, 'agents.json'), 'utf-8')) as { gitRoot: string }
616+
const normalizedSkill = skill.replace(/\\/g, '/')
617+
const normalizedAgentGuide = agentGuide.replace(/\\/g, '/')
618618

619-
expect(skill).toContain(join(tmpDir, '.wanman', 'worktree').replace(/\\/g, '/'))
619+
expect(normalizedSkill).toContain(join(tmpDir, '.wanman', 'worktree').replace(/\\/g, '/'))
620620
expect(skill).toContain('initiative list')
621621
expect(skill).toContain('capsule create')
622622
expect(skill).toContain('wanman initiative list')
623623
expect(skill).not.toContain('packages/cli/dist/index.js')
624-
expect(agentGuide).toContain(join(tmpDir, '.wanman', 'skills', 'takeover-context', 'SKILL.md').replace(/\\/g, '/'))
624+
expect(normalizedAgentGuide).toContain(join(tmpDir, '.wanman', 'skills', 'takeover-context', 'SKILL.md').replace(/\\/g, '/'))
625625
expect(agentGuide).toContain('Create at least 3 tasks in the first CEO cycle')
626626
expect(agentGuide).toContain('visible backlog creation is mandatory')
627627
expect(agentGuide).toContain('capsule create')
@@ -747,6 +747,64 @@ describe('planLocalDynamicClone', () => {
747747
})
748748

749749
describe('takeoverCommand', () => {
750+
it('keeps explicit finite loop counts for takeover runs', () => {
751+
const opts = buildTakeoverRunOptions('Goal', ['--loops', '2'])
752+
753+
expect(opts.infinite).toBe(false)
754+
expect(opts.loops).toBe(2)
755+
})
756+
757+
it('defaults takeover runs to infinite mode when no run mode is supplied', () => {
758+
const opts = buildTakeoverRunOptions('Goal', ['--poll', '5'])
759+
760+
expect(opts.infinite).toBe(true)
761+
expect(opts.loops).toBe(Infinity)
762+
expect(opts.pollInterval).toBe(5)
763+
})
764+
765+
it('keeps explicit infinite mode for takeover runs', () => {
766+
const opts = buildTakeoverRunOptions('Goal', ['--infinite', '--poll', '5'])
767+
768+
expect(opts.infinite).toBe(true)
769+
expect(opts.loops).toBe(Infinity)
770+
expect(opts.pollInterval).toBe(5)
771+
})
772+
773+
it('prints help when invoked without a project path', async () => {
774+
const logs: string[] = []
775+
const logSpy = vi.spyOn(console, 'log').mockImplementation((message = '') => {
776+
logs.push(String(message))
777+
})
778+
779+
try {
780+
await takeoverCommand([])
781+
} finally {
782+
logSpy.mockRestore()
783+
}
784+
785+
expect(logs.join('\n')).toContain('wanman takeover - Take over an existing git repo')
786+
expect(logs.join('\n')).toContain('--loops <n>')
787+
})
788+
789+
it('fails fast when the takeover path does not exist', async () => {
790+
const errors: string[] = []
791+
const errorSpy = vi.spyOn(console, 'error').mockImplementation((message = '') => {
792+
errors.push(String(message))
793+
})
794+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
795+
throw new Error(`process.exit:${code ?? 0}`)
796+
}) as never)
797+
798+
try {
799+
await expect(takeoverCommand([join(tmpDir, 'missing-project')])).rejects.toThrow('process.exit:1')
800+
} finally {
801+
errorSpy.mockRestore()
802+
exitSpy.mockRestore()
803+
}
804+
805+
expect(errors.join('\n')).toContain(`Error: path does not exist: ${join(tmpDir, 'missing-project')}`)
806+
})
807+
750808
it('runs a local dry-run takeover and writes tier-based local overlay config', async () => {
751809
execSync('git init', { cwd: tmpDir, stdio: 'ignore' })
752810
execSync('git config user.email test@example.com', { cwd: tmpDir, stdio: 'ignore' })

packages/cli/src/commands/takeover.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import * as fs from 'node:fs'
1212
import type { AgentRuntime } from '@wanman/core'
13+
import type { RunOptions } from '@wanman/host-sdk'
1314
import {
1415
type GeneratedAgentConfig,
1516
type ProjectDocument,
@@ -138,6 +139,12 @@ function hasExplicitRunMode(runArgs: string[]): boolean {
138139
return runArgs.includes('--infinite') || runArgs.includes('--loops')
139140
}
140141

142+
/** @internal exported for testing */
143+
export function buildTakeoverRunOptions(previewGoal: string, runArgs: string[]): RunOptions {
144+
const normalizedRunArgs = hasExplicitRunMode(runArgs) ? runArgs : ['--infinite', ...runArgs]
145+
return parseOptions([previewGoal, ...normalizedRunArgs]).opts
146+
}
147+
141148
function printProjectSummary(profile: ProjectProfile, generated: GeneratedAgentConfig): void {
142149
console.log('\n Project Profile:')
143150
console.log(` Languages: ${profile.languages.join(', ') || 'none'}`)
@@ -175,14 +182,9 @@ export async function takeoverCommand(args: string[]): Promise<void> {
175182
const profile = scanProject(projectPath)
176183
const generated = generateAgentConfig(profile, goalOverride, runtime)
177184
const previewGoal = generated.goal
178-
const normalizedRunArgs = hasExplicitRunMode(runArgs) ? runArgs : ['--infinite', ...runArgs]
179-
const { opts } = parseOptions([previewGoal, ...normalizedRunArgs])
185+
const opts = buildTakeoverRunOptions(previewGoal, runArgs)
180186
printProjectSummary(profile, generated)
181187

182-
// Force infinite mode for takeover (parseOptions already sets loops=Infinity when --infinite)
183-
opts.infinite = true
184-
opts.loops = Infinity
185-
186188
const overlayDir = materializeLocalTakeoverProject(profile, generated, { enableBrain: !opts.noBrain })
187189

188190
console.log(`\n Runtime: ${runtime}`)

0 commit comments

Comments
 (0)