Skip to content
Open
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
20 changes: 20 additions & 0 deletions apps/cli/src/commands/orchestrator/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
tildifyPath,
type UnifiedSession,
} from '../../lib/session/renderer.js'
import { formatAvailableSessions } from '../../lib/orchestrator/format.js'

/**
* Detect the terminal emulator from environment variables.
Expand Down Expand Up @@ -137,6 +138,25 @@ export default class OrchestratorAttach extends PromptCommand {
s.agentName.toLowerCase().includes(needle) ||
s.sessionId.toLowerCase().includes(needle),
)

// Named session not found — surface available sessions instead of a generic "not running"
if (filteredOrchestrators.length === 0 && allOrchestrators.length > 0) {
const available = formatAvailableSessions(allOrchestrators)
const message = `No orchestrator session matches --name "${flags.name}". Available: ${available}`
if (jsonMode) {
outputErrorAsJson(
'NAME_NOT_FOUND',
message,
createMetadata('orchestrator attach', flags),
)
return
}
this.log('')
this.log(styles.warning(`No orchestrator session matches --name "${flags.name}".`))
this.log(styles.muted(`Available: ${available}`))
this.log('')
return
}
}

// Group sessions by HQ for scope filtering
Expand Down
22 changes: 21 additions & 1 deletion apps/cli/src/commands/orchestrator/stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
tildifyPath,
type UnifiedSession,
} from '../../lib/session/renderer.js'
import { formatAvailableSessions } from '../../lib/orchestrator/format.js'

export default class OrchestratorStop extends PromptCommand {
static description = 'Stop the running orchestrator'
Expand Down Expand Up @@ -67,13 +68,14 @@ export default class OrchestratorStop extends PromptCommand {
}

// Always query machine-wide for orchestrators.
let orchestrators = collectAllSessions({
const allOrchestrators = collectAllSessions({
hqPathFilter,
roleFilter: 'orchestrator',
includeAll: false,
})

// Optional --name filter
let orchestrators = allOrchestrators
if (flags.name) {
const needle = flags.name.toLowerCase()
orchestrators = orchestrators.filter(
Expand All @@ -82,6 +84,24 @@ export default class OrchestratorStop extends PromptCommand {
s.agentName.toLowerCase().includes(needle) ||
s.sessionId.toLowerCase().includes(needle),
)

// Named session not found — surface available sessions
if (orchestrators.length === 0 && allOrchestrators.length > 0) {
const available = formatAvailableSessions(allOrchestrators)
if (jsonMode) {
outputErrorAsJson(
'NAME_NOT_FOUND',
`No orchestrator session matches --name "${flags.name}". Available: ${available}`,
createMetadata('orchestrator stop', flags),
)
return
}
this.log('')
this.log(styles.warning(`No orchestrator session matches --name "${flags.name}".`))
this.log(styles.muted(`Available: ${available}`))
this.log('')
return
}
}

if (orchestrators.length === 0) {
Expand Down
18 changes: 18 additions & 0 deletions apps/cli/src/lib/orchestrator/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { tildifyPath, type UnifiedSession } from '../session/renderer.js'

/**
* Render a one-line summary of available orchestrator sessions for use in
* "name not found" error messages.
*
* Each entry is "<agentName> (<hqPath-or-host>)" joined by ", ".
*/
export function formatAvailableSessions(sessions: UnifiedSession[]): string {
if (sessions.length === 0) return '(none)'
return sessions
.map(s => {
const location = s.hqPath ? tildifyPath(s.hqPath) : 'host'
const tag = s.environment === 'container' ? ', Docker' : ''
return `${s.agentName} (${location}${tag})`
})
.join(', ')
}
81 changes: 81 additions & 0 deletions apps/cli/test/unit/orchestrator-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect } from 'chai'
import * as os from 'node:os'
import * as path from 'node:path'
import { formatAvailableSessions } from '../../src/lib/orchestrator/format.js'
import type { UnifiedSession } from '../../src/lib/session/renderer.js'

/**
* Tests for the formatAvailableSessions helper used by `prlt orchestrator
* attach` and `prlt orchestrator stop` to produce a useful "name not found"
* error message when --name does not match any running session (PRLT-1271).
*/
describe('formatAvailableSessions (PRLT-1271)', () => {
const fakeOrchestrator = (overrides: Partial<UnifiedSession>): UnifiedSession => ({
id: overrides.id ?? 'orch-1',
sessionId: overrides.sessionId ?? 'prlt-orchestrator-hq-main',
ticketId: 'orchestrator',
agentName: overrides.agentName ?? 'main',
status: 'running',
role: 'orchestrator',
environment: overrides.environment ?? 'host',
exists: true,
source: overrides.source ?? 'discovered',
hqPath: overrides.hqPath,
hqName: overrides.hqName,
repoPath: overrides.repoPath,
startedAt: overrides.startedAt,
containerId: overrides.containerId,
})

it('returns "(none)" for an empty session list', () => {
expect(formatAvailableSessions([])).to.equal('(none)')
})

it('formats a session with an HQ path using ~ expansion', () => {
const home = os.homedir()
const hq = path.join(home, 'Projects', 'backend')
const out = formatAvailableSessions([
fakeOrchestrator({ agentName: 'main', hqPath: hq }),
])
expect(out).to.equal('main (~/Projects/backend)')
})

it('marks sessions with no HQ as host', () => {
const out = formatAvailableSessions([
fakeOrchestrator({ agentName: 'reviewer', hqPath: undefined }),
])
expect(out).to.equal('reviewer (host)')
})

it('annotates Docker sessions', () => {
const out = formatAvailableSessions([
fakeOrchestrator({
agentName: 'backend',
hqPath: undefined,
environment: 'container',
}),
])
expect(out).to.equal('backend (host, Docker)')
})

it('joins multiple sessions with ", "', () => {
const home = os.homedir()
const out = formatAvailableSessions([
fakeOrchestrator({
agentName: 'main',
hqPath: path.join(home, 'Projects', 'backend'),
}),
fakeOrchestrator({
agentName: 'backend',
hqPath: path.join(home, 'Projects', 'proletariat-hq'),
}),
fakeOrchestrator({
agentName: 'reviewer',
hqPath: undefined,
}),
])
expect(out).to.equal(
'main (~/Projects/backend), backend (~/Projects/proletariat-hq), reviewer (host)',
)
})
})
Loading