Skip to content
Open
Show file tree
Hide file tree
Changes from 85 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
9fcfcf2
docs: spec for hapi-pi-agent-backend
zhushanwen321 Jun 5, 2026
d629bab
docs: spec retrospect for hapi-pi-agent-backend
zhushanwen321 Jun 5, 2026
dfd4fd1
docs: plan for hapi-pi-agent-backend
zhushanwen321 Jun 5, 2026
8a193ac
docs: plan retrospect for hapi-pi-agent-backend
zhushanwen321 Jun 5, 2026
862be6b
feat(pi): add hapi pi command with JSONL transport and event converter
zhushanwen321 Jun 5, 2026
5b907b0
fix(pi): add Pi RPC types, fix double-cleanup/double-start/converter …
zhushanwen321 Jun 5, 2026
ad66c3f
docs: dev phase reviews and test results for hapi-pi-agent-backend
zhushanwen321 Jun 5, 2026
c17ec11
docs: taste review v2 pass after type definition fixes
zhushanwen321 Jun 5, 2026
ad061c9
docs: dev retrospect for hapi-pi-agent-backend
zhushanwen321 Jun 5, 2026
1652b97
test: test execution for hapi-pi-agent-backend (20/20 pass)
zhushanwen321 Jun 5, 2026
7d937aa
fix: add taste_review symlink for gate pattern match
zhushanwen321 Jun 5, 2026
1297f6a
docs: test retrospect for hapi-pi-agent-backend
zhushanwen321 Jun 5, 2026
e71b743
fix(web): add pi to MODEL_OPTIONS Record type
zhushanwen321 Jun 5, 2026
25492f0
ci: PR and CI evidence for hapi-pi-agent-backend
zhushanwen321 Jun 5, 2026
c88f84e
docs: overall retrospect for hapi-pi-agent-backend (all 5 phases)
zhushanwen321 Jun 5, 2026
8c59637
test(pi): add buffer split, missing fields, and handleResponse tests
zhushanwen321 Jun 6, 2026
845d093
fix(pi): set requiresRuntimeAssets to false — pi runs as subprocess, …
zhushanwen321 Jun 6, 2026
80ef884
refactor(cli): lazy import ensureRuntimeAssets to reduce startup over…
zhushanwen321 Jun 6, 2026
72ecd85
docs: add 15 manual E2E protocol test cases (TC-4-xx) based on real P…
zhushanwen321 Jun 6, 2026
51e49ef
test: E2E protocol test results for hapi-pi (11/15 pass)
zhushanwen321 Jun 6, 2026
ba6f356
test: fix TC-4-11 result — Pi set_model works with correct provider/m…
zhushanwen321 Jun 6, 2026
387ea3d
chore: remove .xyz-harness/ from git tracking, add to .gitignore
zhushanwen321 Jun 6, 2026
6c28949
fix(pi): resolve web UI bugs for hapi-pi integration
zhushanwen321 Jun 6, 2026
1acfe9f
fix(pi): address 4 web UI display bugs in hapi-pi integration
zhushanwen321 Jun 7, 2026
2031e1b
fix(pi): review round 1 - 1 must-fix issue
zhushanwen321 Jun 7, 2026
3437b78
fix(pi): review round 2 - 4 must-fix issues
Jun 7, 2026
19f5872
fix(pi): add session resume support and fix review issues
zhushanwen321 Jun 7, 2026
8554eb6
fix(pi): review round 1 - 3 must-fix issues
zhushanwen321 Jun 7, 2026
6eb4264
fix(pi): review round 2 - 2 must-fix issues
zhushanwen321 Jun 7, 2026
49d618b
refactor(workflow): improve pi-adaptation-review-loop robustness
zhushanwen321 Jun 8, 2026
2432074
test(pi): add coverage for pi flavor across shared, cli, and web
zhushanwen321 Jun 8, 2026
67b0cd9
feat(pi): implement P0 — context budget bar + dynamic model discovery
zhushanwen321 Jun 8, 2026
fb578d4
fix(pi): address code review findings + pre-existing test issue
zhushanwen321 Jun 8, 2026
2aaf3b1
feat(pi): P1 — session rename sync, thinking level UI, skills/commands
zhushanwen321 Jun 8, 2026
a2ba76f
feat(pi): implement P2 features — steer, queue modes, history, native…
zhushanwen321 Jun 8, 2026
b15ae77
feat(pi): implement P3 advanced features — compact, fork, clone, swit…
zhushanwen321 Jun 8, 2026
6787f3e
refactor(pi): clean up runPi.ts imports and readability
zhushanwen321 Jun 8, 2026
3fadc54
fix(pi): remove native image passing, fix version pollution
zhushanwen321 Jun 8, 2026
6010a6f
refactor(pi): extract hub helper, unify web hooks, fix import style
zhushanwen321 Jun 8, 2026
e033aac
chore: untrack .agents/skills and .pi, fix .xyz-harness in gitignore
zhushanwen321 Jun 8, 2026
da1d8a0
refactor: remove unused text message id from converter layer, update …
zhushanwen321 Jun 8, 2026
e83b77d
fix: update tests for pi resume support and text id removal
zhushanwen321 Jun 8, 2026
743c95c
fix: restore cursor resume branch in buildCliArgs
zhushanwen321 Jun 8, 2026
5ace364
refactor: remove pi-specific rename from syncEngine, align with other…
zhushanwen321 Jun 8, 2026
7d640ac
refactor: remove effort field from sessionConfigRpc, Pi self-handles RPC
zhushanwen321 Jun 8, 2026
dd1f028
refactor: consolidate Pi RPC layer from 36 methods to 3 generics
zhushanwen321 Jun 8, 2026
d32cc0f
chore: revert unrelated apiMachine test change
zhushanwen321 Jun 8, 2026
04b5c93
refactor: remove unused ThinkingLevel capability from flavors
zhushanwen321 Jun 8, 2026
f38b29a
refactor: drop Pi prefix from generic RPC method names
zhushanwen321 Jun 8, 2026
84a5dcb
refactor: remove 13 Pi RPC methods with no UI consumers
zhushanwen321 Jun 9, 2026
8eede35
refactor: extract session.ts and loop.ts from runPi.ts
zhushanwen321 Jun 9, 2026
b3820b2
refactor: normalize Pi file naming and improve test coverage
zhushanwen321 Jun 9, 2026
7899429
test: add E2E harness with 4 core helpers and integration specs
zhushanwen321 Jun 9, 2026
53312ec
fix: resolve Pi model selection and thinking level issues
zhushanwen321 Jun 9, 2026
95d96ad
refactor: remove 29 dead exports from feat-pi-support
zhushanwen321 Jun 9, 2026
b68b718
fix: resolve 7 PR review issues in Pi support
zhushanwen321 Jun 10, 2026
9f81ccf
fix: normalize Pi model object to string in hub sessionCache (#5), re…
zhushanwen321 Jun 10, 2026
510ca2c
fix(pi): preserve piAvailableModels on resume, document SetSessionCon…
zhushanwen321 Jun 10, 2026
b1fab89
refactor: remove unused Pi types, extract JsonLineParser, clean up re…
zhushanwen321 Jun 10, 2026
d1e5b4c
chore: remove unrelated E2E test harness from Pi support PR
zhushanwen321 Jun 10, 2026
ecc4710
merge: resolve conflicts with upstream/main
zhushanwen321 Jun 10, 2026
1ce5a1e
fix: wrap cursor model change handler for union type compatibility
zhushanwen321 Jun 10, 2026
157ea6b
fix: apply startup --model to Pi and remove duplicate lockfile entry
zhushanwen321 Jun 10, 2026
7f47a26
fix: update test expectation for effort endpoint error message
zhushanwen321 Jun 10, 2026
0d9c0d6
fix(pi): resolve 8 link-review defects + abort session termination
zhushanwen321 Jun 11, 2026
462894a
fix: restore cli version from integration test placeholder
zhushanwen321 Jun 11, 2026
3d892d0
fix(pi): send restored thinking level to Pi subprocess on startup
zhushanwen321 Jun 13, 2026
f277cf2
fix: restore cli package version from integration test residue
zhushanwen321 Jun 13, 2026
00cd34c
Merge remote-tracking branch 'upstream/main' into feat-pi-support
zhushanwen321 Jun 14, 2026
ebd4556
fix(pi): switch-to-remote handler preserves session instead of termin…
zhushanwen321 Jun 14, 2026
39d6997
fix(pi): remove permission mode selector (Pi RPC has no runtime switc…
zhushanwen321 Jun 15, 2026
a046ed5
fix(cli): realpath workspace root in apiMachine test assertion
zhushanwen321 Jun 15, 2026
ba0ffc8
fix(pi): keepalive reads current mode instead of constructor-time sta…
zhushanwen321 Jun 15, 2026
7dbd6c5
fix(pi): runner no longer passes permission flags to Pi subprocess
zhushanwen321 Jun 15, 2026
b2a67a6
fix(pi): preserve provider identity when persisting selected Pi model
zhushanwen321 Jun 16, 2026
326f758
fix(pi): model picker checkmark follows provider-qualified selection
zhushanwen321 Jun 16, 2026
56c1eeb
fix(pi): steer messages consumed immediately, not queued in pendingLo…
zhushanwen321 Jun 16, 2026
1211cdc
fix(pi): clear stale thinking level when switching to non-reasoning m…
zhushanwen321 Jun 16, 2026
c639dfb
fix(pi): return provider-qualified model in SetSessionConfig applied
zhushanwen321 Jun 16, 2026
264bc88
fix(pi): preserve piSelectedModel in bootstrapExistingSession metadata
zhushanwen321 Jun 16, 2026
b5a931a
fix(pi): await Pi confirmation before reporting model/effort applied
zhushanwen321 Jun 16, 2026
a1310e0
fix(pi): resolve set_model RPC so awaited model switch does not time out
zhushanwen321 Jun 16, 2026
a8b155c
fix(pi): drain pending localId on turn_start only; throw when set_mod…
zhushanwen321 Jun 16, 2026
b7343ae
fix(pi): exclude Pi from generic Ctrl/Cmd+M model cycler
zhushanwen321 Jun 16, 2026
f203816
fix(pi): commit PiSession config only after Pi confirms the RPC
zhushanwen321 Jun 16, 2026
6deb6d6
fix(pi): apply startup model only after Pi confirms set_model
zhushanwen321 Jun 16, 2026
ae7222a
fix(pi): do not persist startup model before Pi confirms set_model
zhushanwen321 Jun 16, 2026
c4927a3
fix(pi): apply startup effort only after Pi confirms set_thinking_level
zhushanwen321 Jun 16, 2026
197f26b
fix(pi): omit unknown runtime config from keepalive, don't clear pers…
zhushanwen321 Jun 16, 2026
693a32a
fix(pi): disable Ctrl/Cmd+M model cycler for Pi entirely
zhushanwen321 Jun 16, 2026
494762b
fix(pi): persist piSelectedModel from get_state and startup set_model…
zhushanwen321 Jun 16, 2026
1d8839a
fix(pi): default startingMode to remote — Pi has no local TUI path
zhushanwen321 Jun 16, 2026
08bbf24
fix(pi): resume with remote startingMode — no local TUI path
zhushanwen321 Jun 16, 2026
613df6a
fix: restore e2e/scratchlist.spec.ts deleted from main by mistake
zhushanwen321 Jun 16, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ cli/npm/main/
test-results/
playwright-report/
e2e-output/
.xyz-harness
.agents/
.pi/
2 changes: 2 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,4 @@
"@types/parse-path": "7.0.3"
},
"packageManager": "bun@1.3.14"
}
}
1 change: 1 addition & 0 deletions cli/src/agent/localHandoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('registerLocalHandoffHandler', () => {
const lifecycle = {
setArchiveReason: vi.fn(),
setSessionEndReason: vi.fn(),
hasExplicitSessionEndReason: vi.fn(() => false),
cleanupAndExit: vi.fn(async () => {})
}

Expand Down
87 changes: 87 additions & 0 deletions cli/src/agent/runnerLifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createRunnerLifecycle } from './runnerLifecycle';
import type { RunnerLifecycle } from './runnerLifecycle';

// Mock heavy deps
vi.mock('@/ui/logger', () => ({
logger: {
debug: vi.fn(),
getLogPath: vi.fn(() => '/tmp/test.log'),
},
}));

vi.mock('@/ui/terminalState', () => ({
restoreTerminalState: vi.fn(),
}));

function createMockApiSession() {
return {
updateMetadata: vi.fn(),
sendSessionDeath: vi.fn(),
flush: vi.fn(),
close: vi.fn(),
} as unknown as Parameters<typeof createRunnerLifecycle>[0]['session'];
}

describe('createRunnerLifecycle', () => {
let lifecycle: RunnerLifecycle;

beforeEach(() => {
vi.clearAllMocks();
lifecycle = createRunnerLifecycle({
session: createMockApiSession(),
logTag: 'test',
});
});

// --- D-9: hasExplicitSessionEndReason ---

describe('hasExplicitSessionEndReason', () => {
it('returns false initially', () => {
expect(lifecycle.hasExplicitSessionEndReason()).toBe(false);
});

it('returns true after setSessionEndReason is called', () => {
lifecycle.setSessionEndReason('completed');
expect(lifecycle.hasExplicitSessionEndReason()).toBe(true);
});

it('returns false after markCrash — markCrash does NOT set explicit flag', () => {
lifecycle.markCrash(new Error('boom'));
expect(lifecycle.hasExplicitSessionEndReason()).toBe(false);
});

it('stays true once set — subsequent markCrash does not clear it', () => {
lifecycle.setSessionEndReason('handoff');
lifecycle.markCrash(new Error('late crash'));
expect(lifecycle.hasExplicitSessionEndReason()).toBe(true);
});
});

// --- markCrash sets reason to 'error' but not explicit ---

describe('markCrash', () => {
it('sets sessionEndReason to error via sendSessionDeath during cleanup', async () => {
const session = createMockApiSession();
const lc = createRunnerLifecycle({ session, logTag: 'test' });
lc.markCrash(new Error('fatal'));

// cleanup triggers sendSessionDeath — verify 'error' reason
await lc.cleanup();
expect(session.sendSessionDeath).toHaveBeenCalledWith('error');
});
});

// --- setSessionEndReason + cleanup propagates correct reason ---

describe('setSessionEndReason + cleanup', () => {
it('sends explicit reason via sendSessionDeath during cleanup', async () => {
const session = createMockApiSession();
const lc = createRunnerLifecycle({ session, logTag: 'test' });
lc.setSessionEndReason('completed');

await lc.cleanup();
expect(session.sendSessionDeath).toHaveBeenCalledWith('completed');
});
});
});
6 changes: 6 additions & 0 deletions cli/src/agent/runnerLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type RunnerLifecycle = {
setExitCode: (code: number) => void
setArchiveReason: (reason: string) => void
setSessionEndReason: (reason: SessionEndReason) => void
hasExplicitSessionEndReason: () => boolean
markCrash: (error: unknown) => void
cleanup: () => Promise<void>
cleanupAndExit: (codeOverride?: number) => Promise<void>
Expand All @@ -25,6 +26,7 @@ export function createRunnerLifecycle(options: RunnerLifecycleOptions): RunnerLi
let exitCode = 0
let archiveReason = 'User terminated'
let sessionEndReason: SessionEndReason = 'terminated'
let sessionEndReasonExplicit = false
let cleanupStarted = false
let cleanupPromise: Promise<void> | null = null

Expand Down Expand Up @@ -95,8 +97,11 @@ export function createRunnerLifecycle(options: RunnerLifecycleOptions): RunnerLi

const setSessionEndReason = (reason: SessionEndReason) => {
sessionEndReason = reason
sessionEndReasonExplicit = true
}

const hasExplicitSessionEndReason = () => sessionEndReasonExplicit

const markCrash = (error: unknown) => {
logger.debug(`${logPrefix} Unhandled error:`, error)
exitCode = 1
Expand Down Expand Up @@ -128,6 +133,7 @@ export function createRunnerLifecycle(options: RunnerLifecycleOptions): RunnerLi
setExitCode,
setArchiveReason,
setSessionEndReason,
hasExplicitSessionEndReason,
markCrash,
cleanup,
cleanupAndExit,
Expand Down
12 changes: 12 additions & 0 deletions cli/src/agent/sessionConfigRpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,22 @@ export function resolveSessionConfigPermissionMode<TPermissionMode extends Permi
return parsed.data as TPermissionMode
}

/** Extract `modelId` from either a plain string or a `{ provider, modelId }`
* object (the form Pi sessions receive from the hub). Other agents only pass
* plain strings; the object branch is here for schema consistency so this
* function doesn't throw if the hub later sends the union form to any agent. */
export function resolveNullableSessionModel(value: unknown): string | null {
if (value === null) {
return null
}
// Pi sessions receive model as { provider, modelId }; extract modelId
if (typeof value === 'object' && value !== null) {
const modelObj = value as { modelId?: unknown }
if (typeof modelObj.modelId === 'string' && modelObj.modelId.trim().length > 0) {
return modelObj.modelId.trim()
}
throw new Error('Invalid model')
}
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error('Invalid model')
}
Expand Down
6 changes: 6 additions & 0 deletions cli/src/agent/sessionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,15 @@ function pickExistingSessionMetadata(metadata: Metadata | null | undefined): Par
if (metadata.cursorSessionId !== undefined) preserved.cursorSessionId = metadata.cursorSessionId
if (metadata.cursorSessionProtocol !== undefined) preserved.cursorSessionProtocol = metadata.cursorSessionProtocol
if (metadata.kimiSessionId !== undefined) preserved.kimiSessionId = metadata.kimiSessionId
if (metadata.piSessionId !== undefined) preserved.piSessionId = metadata.piSessionId
if (metadata.tools !== undefined) preserved.tools = metadata.tools
if (metadata.slashCommands !== undefined) preserved.slashCommands = metadata.slashCommands
if (metadata.worktree !== undefined) preserved.worktree = metadata.worktree
// Preserve cached Pi model list so the web can show models immediately
// on inactive-session view without waiting for an RPC round-trip.
if (metadata.piAvailableModels !== undefined) preserved.piAvailableModels = metadata.piAvailableModels

@github-actions github-actions Bot Jun 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] Preserve Pi provider selection when reusing an existing HAPI session

This PR persists provider-qualified metadata.piSelectedModel so duplicate modelIds can be disambiguated, but bootstrapExistingSession() rebuilds metadata from this whitelist. The added Pi preservation keeps piAvailableModels and omits piSelectedModel, so the first resume/local handoff update drops the provider identity again; after that the web falls back to modelId-only matching and startup model application can choose the wrong provider.

Suggested fix:

if (metadata.piAvailableModels !== undefined) preserved.piAvailableModels = metadata.piAvailableModels
if (metadata.piSelectedModel !== undefined) preserved.piSelectedModel = metadata.piSelectedModel

// Preserve provider-qualified Pi model selection (disambiguates duplicate modelIds).
if (metadata.piSelectedModel !== undefined) preserved.piSelectedModel = metadata.piSelectedModel

return preserved
}
Expand Down
6 changes: 4 additions & 2 deletions cli/src/api/apiMachine.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs'
import { mkdtempSync, rmSync, mkdirSync, realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

Expand Down Expand Up @@ -135,7 +135,9 @@ describe('ApiMachineClient listOpencodeModelsForCwd handler', () => {
availableModels: [{ modelId: 'x/y' }],
currentModelId: 'x/y'
})
expect(listOpencodeModelsForCwdMock).toHaveBeenCalledWith(secondWorkspaceRoot)
// The handler realpaths the cwd (security: prevents symlink escape),
// so on macOS /var/folders/... resolves to /private/var/folders/...
expect(listOpencodeModelsForCwdMock).toHaveBeenCalledWith(realpathSync(secondWorkspaceRoot))
} finally {
rmSync(secondWorkspaceRoot, { recursive: true, force: true })
client.shutdown()
Expand Down
26 changes: 5 additions & 21 deletions cli/src/codex/codexAppServerClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
import { logger } from '@/ui/logger';
import { JsonLineParser } from '@/utils/jsonLineParser';
import { killProcessByChildProcess } from '@/utils/process';
import type {
CollaborationModeListResponse,
Expand Down Expand Up @@ -69,10 +70,9 @@ function createAbortError(): Error {
return error;
}

export class CodexAppServerClient {
export class CodexAppServerClient extends JsonLineParser {
private process: ChildProcessWithoutNullStreams | null = null;
private connected = false;
private buffer = '';
private nextId = 1;
private readonly pending = new Map<number, PendingRequest>();
private readonly requestHandlers = new Map<string, RequestHandler>();
Expand Down Expand Up @@ -103,7 +103,7 @@ export class CodexAppServerClient {
});

this.process.stdout.setEncoding('utf8');
this.process.stdout.on('data', (chunk) => this.handleStdout(chunk));
this.process.stdout.on('data', (chunk) => this.feed(chunk));

this.process.stderr.setEncoding('utf8');
this.process.stderr.on('data', (chunk) => {
Expand Down Expand Up @@ -354,23 +354,7 @@ export class CodexAppServerClient {
this.writePayload(payload);
}

private handleStdout(chunk: string): void {
this.buffer += chunk;
let newlineIndex = this.buffer.indexOf('\n');

while (newlineIndex >= 0) {
const line = this.buffer.slice(0, newlineIndex).trim();
this.buffer = this.buffer.slice(newlineIndex + 1);

if (line.length > 0) {
this.handleLine(line);
}

newlineIndex = this.buffer.indexOf('\n');
}
}

private handleLine(line: string): void {
protected handleLine(line: string): void {
if (this.protocolError) {
return;
}
Expand Down Expand Up @@ -482,7 +466,7 @@ export class CodexAppServerClient {
}

private resetParserState(): void {
this.buffer = '';
this.reset();
this.protocolError = null;
}

Expand Down
3 changes: 2 additions & 1 deletion cli/src/codex/runCodex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ const lifecycleMock = vi.hoisted(() => ({
markCrash: vi.fn(),
setExitCode: vi.fn(),
setArchiveReason: vi.fn(),
setSessionEndReason: vi.fn()
setSessionEndReason: vi.fn(),
hasExplicitSessionEndReason: vi.fn(() => false)
}))

vi.mock('@/agent/runnerLifecycle', () => ({
Expand Down
108 changes: 108 additions & 0 deletions cli/src/commands/agentCommandOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,111 @@ describe('parseRemoteAgentCommandOptions', () => {
expect(() => parseRemoteAgentCommandOptions(['--model-reasoning-effort'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --model-reasoning-effort value')
})
})

describe('parseRemoteAgentCommandOptions — pi flavor', () => {
// Pi RPC mode has no permission switching, so the command passes an empty
// allow-list. These tests cover the non-permission flags using a non-empty
// allow-list purely as a parser fixture — the parser's behavior is
// independent of the modes' contents.
const ALLOWED = OPENCODE_PERMISSION_MODES

it('accepts --model and stores it on options', () => {
const result = parseRemoteAgentCommandOptions(
['--model', 'claude-sonnet-4-5'],
ALLOWED
)
expect(result.model).toBe('claude-sonnet-4-5')
})

it('--session-id stores the value as resumeSessionId (Pi-specific flag)', () => {
// Pi uses --session-id for exact session resume (RPC mode), not the
// generic --resume that other flavors use.
const result = parseRemoteAgentCommandOptions(
['--session-id', 'pi-sess-123'],
ALLOWED
)
expect(result.resumeSessionId).toBe('pi-sess-123')
})

it('--resume is also accepted as an alias for session resume', () => {
// Some flavor paths pass --resume; the parser should accept it
// uniformly so callers do not need to branch on flavor.
const result = parseRemoteAgentCommandOptions(
['--resume', 'sess-id'],
ALLOWED
)
expect(result.resumeSessionId).toBe('sess-id')
})

it('a later --resume overrides a prior --session-id (last-write-wins)', () => {
const result = parseRemoteAgentCommandOptions(
['--session-id', 'first', '--resume', 'second'],
ALLOWED
)
expect(result.resumeSessionId).toBe('second')
})

it('rejects --session-id with no value', () => {
expect(() => parseRemoteAgentCommandOptions(
['--session-id'],
ALLOWED
)).toThrow('Missing --session-id value')
})

it('parses --started-by runner', () => {
const result = parseRemoteAgentCommandOptions(
['--started-by', 'runner'],
ALLOWED
)
expect(result.startedBy).toBe('runner')
})

it('parses --started-by terminal', () => {
const result = parseRemoteAgentCommandOptions(
['--started-by', 'terminal'],
ALLOWED
)
expect(result.startedBy).toBe('terminal')
})

it('parses --hapi-starting-mode remote', () => {
const result = parseRemoteAgentCommandOptions(
['--hapi-starting-mode', 'remote'],
ALLOWED
)
expect(result.startingMode).toBe('remote')
})

it('parses --hapi-starting-mode local', () => {
const result = parseRemoteAgentCommandOptions(
['--hapi-starting-mode', 'local'],
ALLOWED
)
expect(result.startingMode).toBe('local')
})

it('rejects invalid --hapi-starting-mode', () => {
expect(() => parseRemoteAgentCommandOptions(
['--hapi-starting-mode', 'invalid'],
ALLOWED
)).toThrow('Invalid --hapi-starting-mode')
})

it('handles a full pi invocation end-to-end', () => {
const result = parseRemoteAgentCommandOptions(
[
'--started-by', 'runner',
'--hapi-starting-mode', 'remote',
'--model', 'claude-sonnet-4-5',
'--session-id', 'pi-sess-full',
],
ALLOWED
)
expect(result).toEqual({
startedBy: 'runner',
startingMode: 'remote',
model: 'claude-sonnet-4-5',
resumeSessionId: 'pi-sess-full',
})
})
})
Loading
Loading