native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076import Foundationimport ProwlCLIShared
nonisolated struct AgentSignalHookCapability: Equatable, Sendable { let runtime: AgentNativeHookRuntime let nativeEvents: [String: AgentSignalEvent] let coveredEvents: [AgentSignalEvent]
init(runtime: AgentNativeHookRuntime, nativeEvents: [String: AgentSignalEvent]) { self.runtime = runtime self.nativeEvents = nativeEvents self.coveredEvents = Array(Set(nativeEvents.values)).sorted { $0.rawValue < $1.rawValue } }}
/// Interactive and headless launch behavior for one supported runtime./// Handoff briefing is authored by the live source agent and does not resume/// native sessions through this adapter boundary (docs-ai 055).nonisolated protocol AgentRuntimeAdapter: Sendable { var runtime: AgentProfileRuntime { get } var displayName: String { get } var supportsModelSelection: Bool { get } var supportsReasoningEffort: Bool { get } /// Empty when the runtime cannot represent both persisted modes honestly. /// Adapters exposing the picker must render every listed mode, including /// inverse guarded flags for CLIs whose default is auto-approved. var executionModeOptions: [AgentExecutionMode] { get } var accountIsolation: AgentProfileHomeRelocation? { get } var signalHooks: AgentSignalHookCapability? { get } var reasoningEffortSuggestions: [String] { get } var modelSuggestions: [String] { get }
func observe(arguments: [String]) -> AgentLaunchObservation func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation}
nonisolated extension AgentRuntimeAdapter { var supportsAccountIsolation: Bool { accountIsolation != nil } var accountHomeEnvironmentVariable: String? { accountIsolation?.environmentVariable } var supportsModelSelection: Bool { false } var supportsReasoningEffort: Bool { false } var executionModeOptions: [AgentExecutionMode] { [] } var accountIsolation: AgentProfileHomeRelocation? { nil } var signalHooks: AgentSignalHookCapability? { nil } var reasoningEffortSuggestions: [String] { [] } var modelSuggestions: [String] { [] }
/// User arguments remain last-wins for ordinary options. A managed home is /// appended after them because account binding is an identity invariant: /// extra arguments must not silently redirect state outside the UUID home. func finalizedOptions(_ generated: [String], request: AgentStartRequest) -> [String] { var options = generated + request.configuration.extraArguments if let home = request.dedicatedHome, let accountIsolation { options += accountIsolation.arguments(for: home) } return options }}
/// One path-valued CLI option in a managed-home relocation contract.nonisolated struct AgentProfileHomePathArgument: Equatable, Sendable { let option: String /// Empty means the managed home itself; otherwise a child path. let relativePath: String}
/// Verified full-state relocation for a runtime. Environment and CLI-argument/// mechanisms share one model so the planner, editor, cleanup, and session/// resolver agree on the same managed root (docs-ai 055).nonisolated struct AgentProfileHomeRelocation: Equatable, Sendable { let environmentVariable: String? let pathArguments: [AgentProfileHomePathArgument] let sessionRootRelativePath: String let reservedEnvironmentVariables: Set<String>
init( environmentVariable: String? = nil, pathArguments: [AgentProfileHomePathArgument] = [], sessionRootRelativePath: String = "", reservedEnvironmentVariables: Set<String> = [] ) { self.environmentVariable = environmentVariable self.pathArguments = pathArguments self.sessionRootRelativePath = sessionRootRelativePath self.reservedEnvironmentVariables = reservedEnvironmentVariables.union( environmentVariable.map { [$0] } ?? [] ) }
func arguments(for home: URL) -> [String] { pathArguments.flatMap { argument in let url = argument.relativePath.isEmpty ? home : home.appending(path: argument.relativePath, directoryHint: .isDirectory) return [argument.option, AgentProfileLaunchPlanner.pathString(url)] } }
func sessionConfigRoot(for home: URL) -> URL { guard !sessionRootRelativePath.isEmpty else { return home } return home.appending(path: sessionRootRelativePath, directoryHint: .isDirectory) }}
nonisolated enum AgentExecutionMode: String, Codable, CaseIterable, Equatable, Identifiable, Sendable { case standard case unrestricted
var id: String { rawValue }
var title: String { switch self { case .standard: "Standard" case .unrestricted: "Unrestricted" } }}
/// How an agent process should begin. An empty prompt sentinel is forbidden by/// construction: interactive-with-no-prompt and prompted starts are distinct/// CLI semantics (docs-ai 053).nonisolated enum AgentStartIntent: Equatable, Sendable { case interactive case prompt(String) case headless(String)
var promptText: String? { switch self { case .interactive: nil case .prompt(let prompt), .headless(let prompt): prompt } }}
nonisolated struct AgentLaunchConfiguration: Codable, Equatable, Sendable { var model: String? var executionMode: AgentExecutionMode var reasoningEffort: String? /// Literal argv tokens appended after adapter-generated options (last-wins) /// and before managed-home arguments and any positional prompt. var extraArguments: [String]
init( model: String? = nil, executionMode: AgentExecutionMode = .standard, reasoningEffort: String? = nil, extraArguments: [String] = [] ) { self.model = model self.executionMode = executionMode self.reasoningEffort = reasoningEffort self.extraArguments = extraArguments }
private enum CodingKeys: String, CodingKey { case model, executionMode, reasoningEffort, extraArguments }
init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) model = try container.decodeIfPresent(String.self, forKey: .model) executionMode = try container.decodeIfPresent(AgentExecutionMode.self, forKey: .executionMode) ?? .standard reasoningEffort = try container.decodeIfPresent(String.self, forKey: .reasoningEffort) extraArguments = try container.decodeIfPresent([String].self, forKey: .extraArguments) ?? [] }}
/// Options observed from a live agent process. Nil denotes an unknown effective/// setting, never an inferred safe default from the absence of an argv flag.nonisolated struct AgentLaunchObservation: Equatable, Sendable { let model: String? let executionMode: AgentExecutionMode?
init(model: String? = nil, executionMode: AgentExecutionMode? = nil) { self.model = model self.executionMode = executionMode }}
nonisolated struct AgentStartRequest: Equatable, Sendable { let runtime: AgentProfileRuntime let intent: AgentStartIntent let configuration: AgentLaunchConfiguration /// Set only by Agent Profile planning. Handoff and ordinary starts are pure /// invocations and therefore leave this nil. let dedicatedHome: URL?
var agent: DetectedAgent { runtime.agent }
init( runtime: AgentProfileRuntime, intent: AgentStartIntent, configuration: AgentLaunchConfiguration = .init(), dedicatedHome: URL? = nil ) { self.runtime = runtime self.intent = intent self.configuration = configuration self.dedicatedHome = dedicatedHome }
init( agent: DetectedAgent, intent: AgentStartIntent, configuration: AgentLaunchConfiguration = .init() ) { self.init(runtime: AgentProfileRuntime(agent: agent), intent: intent, configuration: configuration) }}
nonisolated struct AgentInvocation: Equatable, Sendable { let executable: String let arguments: [String]
init(executable: String, arguments: [String]) { self.executable = executable self.arguments = arguments }
var terminalInput: String { terminalInput(replacingArgumentsWithEnvironmentVariables: [:]) }
/// Profile plans can carry arbitrary argv values through the surface /// environment instead of typing them into a canonical PTY. The logical /// invocation retains the real values while shell rendering substitutes /// quoted variable references at the declared indexes. func terminalInput( replacingArgumentsWithEnvironmentVariables replacements: [Int: String] ) -> String { var tokens = arguments.map(Self.shellQuote) for (index, variable) in replacements where tokens.indices.contains(index) { tokens[index] = "\"$\(variable)\"" } return ([Self.shellQuote(executable)] + tokens).joined(separator: " ") }
func terminalInput(replacingFinalArgumentWithEnvironmentVariable variable: String?) -> String { guard let variable, !arguments.isEmpty else { return terminalInput } return terminalInput( replacingArgumentsWithEnvironmentVariables: [arguments.index(before: arguments.endIndex): variable] ) }
static func shellQuote(_ argument: String) -> String { "'" + argument.replacing("'", with: "'\"'\"'") + "'" }}
nonisolated enum AgentRuntimeError: Error, Equatable, Sendable { case unsupportedAgent(DetectedAgent) case unsupportedStartIntent(AgentProfileRuntime, AgentStartIntent)}
extension AgentProfileRuntime { // An exhaustive switch makes a new agent fail to compile until it is mapped. fileprivate nonisolated var adapter: any AgentRuntimeAdapter { switch self { case .claude: ClaudeCodeRuntimeAdapter() case .codex: CodexRuntimeAdapter() case .gemini: GeminiRuntimeAdapter() case .cursor: CursorRuntimeAdapter() case .cline: ClineRuntimeAdapter() case .opencode: OpenCodeRuntimeAdapter() case .copilot: CopilotRuntimeAdapter() case .kimi: KimiRuntimeAdapter() case .droid: DroidRuntimeAdapter() case .devin: DevinRuntimeAdapter() case .amp: AmpRuntimeAdapter() case .qoder: QoderRuntimeAdapter() case .qwen: QwenRuntimeAdapter() case .grok: GrokRuntimeAdapter() case .antigravity: AntigravityRuntimeAdapter() case .pi: PiRuntimeAdapter() case .omp: OMPRuntimeAdapter() } }}
nonisolated enum AgentRuntimeAdapterRegistry { static func profileAdapter(for runtime: AgentProfileRuntime) -> (any AgentRuntimeAdapter)? { runtime.adapter }
/// Canonical launch adapter for one detected runtime. static func adapter(for agent: DetectedAgent) -> (any AgentRuntimeAdapter)? { profileAdapter(for: AgentProfileRuntime(agent: agent)) }
static var launchableAgents: [DetectedAgent] { DetectedAgent.allCases.filter { adapter(for: $0) != nil } }
static func displayName(for runtime: AgentProfileRuntime) -> String { profileAdapter(for: runtime)?.displayName ?? runtime.rawValue }
static func displayName(for agent: DetectedAgent) -> String { adapter(for: agent)?.displayName ?? agent.displayName }
static func canStart(_ agent: DetectedAgent) -> Bool { adapter(for: agent) != nil }
static func observe(runtime: AgentProfileRuntime, arguments: [String]) -> AgentLaunchObservation { profileAdapter(for: runtime)?.observe(arguments: arguments) ?? .init() }
static func observe(agent: DetectedAgent, arguments: [String]) -> AgentLaunchObservation { return adapter(for: agent)?.observe(arguments: arguments) ?? .init() }
static func inheritedConfiguration( from sourceAgent: DetectedAgent, observation: AgentLaunchObservation?, to destinationAgent: DetectedAgent ) -> AgentLaunchConfiguration { AgentLaunchConfiguration( model: sourceAgent == destinationAgent ? observation?.model : nil, executionMode: observation?.executionMode ?? .standard ) }
static func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { guard let adapter = profileAdapter(for: request.runtime) else { throw AgentRuntimeError.unsupportedAgent(request.agent) } return try adapter.makeStartInvocation(request) }
}
// MARK: - Launch adapters
nonisolated private struct CodexRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .codex let displayName = "Codex" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation(environmentVariable: "CODEX_HOME") let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .codex, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .codex) ) let reasoningEffortSuggestions = ["low", "medium", "high", "xhigh", "max"] let modelSuggestions = [ "gpt-6.1-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", ]
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: arguments.containsAny("--dangerously-bypass-approvals-and-sandbox", "--yolo") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["-c", "model_reasoning_effort=\(effort)"] } if request.configuration.executionMode == .unrestricted { generated.append("--dangerously-bypass-approvals-and-sandbox") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "codex", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "codex", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "codex", arguments: ["exec"] + options + [prompt]) } }}
nonisolated private struct ClaudeCodeRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .claude let displayName = "Claude Code" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "CLAUDE_CONFIG_DIR" ) let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .claude, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .claude) ) let reasoningEffortSuggestions = ["low", "medium", "high", "xhigh", "max"] let modelSuggestions = [ "claude-fable-5-1", "claude-opus-5-5", "claude-sonnet-5", "claude-haiku-4-5-20251001", "claude-fable-5", "claude-opus-5", ]
func observe(arguments: [String]) -> AgentLaunchObservation { let bypasses = arguments.contains("--dangerously-skip-permissions") || arguments.optionValue(long: "--permission-mode") == "bypassPermissions" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: bypasses ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--effort", effort] } if request.configuration.executionMode == .unrestricted { generated.append("--dangerously-skip-permissions") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "claude", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "claude", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "claude", arguments: ["-p"] + options + [prompt]) } }}
nonisolated private struct GeminiRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .gemini let displayName = "Gemini CLI" let supportsModelSelection = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "GEMINI_CLI_HOME", sessionRootRelativePath: ".gemini" )
func observe(arguments: [String]) -> AgentLaunchObservation { let approves = arguments.contains("--yolo") || arguments.optionValue(long: "--approval-mode") == "yolo" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: approves && arguments.booleanOptionIsFalse("--sandbox") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if request.configuration.executionMode == .unrestricted { generated += ["--approval-mode", "yolo", "--sandbox=false"] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "gemini", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "gemini", arguments: options + ["--prompt-interactive", prompt]) case .headless(let prompt): AgentInvocation(executable: "gemini", arguments: options + ["--prompt", prompt]) } }}
nonisolated private struct CursorRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .cursor let displayName = "Cursor Agent" let supportsModelSelection = true let executionModeOptions = AgentExecutionMode.allCases
func observe(arguments: [String]) -> AgentLaunchObservation { let approves = arguments.containsAny("--force", "--yolo") return AgentLaunchObservation( model: arguments.optionValue(long: "--model"), executionMode: approves && arguments.optionValue(long: "--sandbox") == "disabled" ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if request.configuration.executionMode == .unrestricted { generated += ["--yolo", "--sandbox", "disabled"] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "cursor-agent", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "cursor-agent", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "cursor-agent", arguments: options + ["--print", prompt]) } }}
nonisolated private struct ClineRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .cline let displayName = "Cline" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( pathArguments: [ AgentProfileHomePathArgument(option: "--config", relativePath: "config"), AgentProfileHomePathArgument(option: "--data-dir", relativePath: "data"), AgentProfileHomePathArgument(option: "--hooks-dir", relativePath: "hooks"), ], sessionRootRelativePath: "data", reservedEnvironmentVariables: ["CLINE_DATA_DIR"] ) let reasoningEffortSuggestions = ["none", "low", "medium", "high", "xhigh"]
func observe(arguments: [String]) -> AgentLaunchObservation { let autoApprove = arguments.optionValue(long: "--auto-approve") let executionMode: AgentExecutionMode? = switch autoApprove { case "true": .unrestricted case "false": .standard default: nil } return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: executionMode ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--thinking", effort] } generated += [ "--auto-approve", request.configuration.executionMode == .unrestricted ? "true" : "false", ] let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "cline", arguments: options + ["--tui"]) case .prompt(let prompt): AgentInvocation(executable: "cline", arguments: options + ["--tui", prompt]) case .headless(let prompt): AgentInvocation(executable: "cline", arguments: options + [prompt]) } }}
nonisolated private struct OpenCodeRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .opencode let displayName = "OpenCode" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases /// Relayed by the bundled `agent-hooks/opencode/prowl-hooks.ts` plugin. OpenCode creates its /// session lazily at the first prompt and emits nothing on `/new` or resume, so the runtime is /// non-announcing like Codex: the first `session.idle` verifies the channel. `permission.asked` /// is dropped per launch under `--auto`, where OpenCode auto-replies in the same millisecond. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .opencode, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .opencode) )
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: arguments.contains("--auto") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--variant", effort] } if request.configuration.executionMode == .unrestricted { generated.append("--auto") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "opencode", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "opencode", arguments: options + ["--prompt", prompt]) case .headless(let prompt): AgentInvocation(executable: "opencode", arguments: ["run"] + options + [prompt]) } }}
nonisolated private struct CopilotRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .copilot let displayName = "GitHub Copilot" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "COPILOT_HOME" ) /// `PermissionRequest` is excluded on purpose: it fires whenever a tool enters the /// permission service, including when `--allow-all-tools` auto-approves and nobody is /// waiting. `Notification` is the only event that means a human is actually blocked. /// `subagentStop` is excluded because a subagent finishing is not the main turn ending. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .copilot, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .copilot) ) let reasoningEffortSuggestions = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation( model: arguments.optionValue(long: "--model"), executionMode: arguments.contains("--allow-all") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--reasoning-effort", effort] } if request.configuration.executionMode == .unrestricted { generated.append("--allow-all") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "copilot", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "copilot", arguments: options + ["--interactive", prompt]) case .headless(let prompt): AgentInvocation(executable: "copilot", arguments: options + ["--prompt", prompt]) } }}
nonisolated private struct KimiRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .kimi let displayName = "Kimi CLI" let supportsModelSelection = true let executionModeOptions = AgentExecutionMode.allCases
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: arguments.containsAny("--yolo", "--yes") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if request.configuration.executionMode == .unrestricted { generated.append("--yolo") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "kimi", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "kimi", arguments: options + ["--prompt", prompt]) case .headless(let prompt): AgentInvocation(executable: "kimi", arguments: options + ["--print", "--prompt", prompt]) } }}
nonisolated private struct DroidRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .droid let displayName = "Droid" /// Droid has no `PermissionRequest` event; attention arrives through `Notification`. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .droid, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .droid) )
func observe(arguments _: [String]) -> AgentLaunchObservation { .init() }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { let options = finalizedOptions([], request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "droid", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "droid", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "droid", arguments: ["exec"] + options + [prompt]) } }}
nonisolated private struct AmpRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .amp let displayName = "Amp" let supportsReasoningEffort = true let reasoningEffortSuggestions = ["low", "medium", "high"]
func observe(arguments _: [String]) -> AgentLaunchObservation { .init() }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let effort = request.configuration.reasoningEffort { generated += ["--effort", effort] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "amp", arguments: options) case .prompt: throw AgentRuntimeError.unsupportedStartIntent(.amp, request.intent) case .headless(let prompt): AgentInvocation(executable: "amp", arguments: options + ["--execute", prompt]) } }}
nonisolated private struct QoderRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .qoder let displayName = "Qoder CLI" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( pathArguments: [AgentProfileHomePathArgument(option: "--config-dir", relativePath: "")] ) /// `PermissionRequest` is excluded on purpose: Qoder was measured firing it under /// `--permission-mode accept_edits` while the write was auto-approved and nobody was /// waiting. `StopFailure` is included because Qoder reports a failed turn that way. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .qoder, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .qoder) ) let reasoningEffortSuggestions = ["low", "medium", "high"]
func observe(arguments: [String]) -> AgentLaunchObservation { let bypasses = arguments.contains("--dangerously-skip-permissions") || arguments.optionValue(long: "--permission-mode") == "bypass_permissions" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: bypasses ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--reasoning-effort", effort] } if request.configuration.executionMode == .unrestricted { generated.append("--dangerously-skip-permissions") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "qodercli", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "qodercli", arguments: options + ["--prompt-interactive", prompt]) case .headless(let prompt): AgentInvocation(executable: "qodercli", arguments: options + ["--print", prompt]) } }}
nonisolated private struct QwenRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .qwen let displayName = "Qwen Code" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation(environmentVariable: "QWEN_HOME") let reasoningEffortSuggestions = ["low", "medium", "high", "xhigh"]
func observe(arguments: [String]) -> AgentLaunchObservation { let approves = arguments.contains("--yolo") || arguments.optionValue(long: "--approval-mode") == "yolo" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: approves && arguments.booleanOptionIsFalse("--sandbox") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--reasoning-effort", effort] } if request.configuration.executionMode == .unrestricted { generated += ["--approval-mode", "yolo", "--sandbox=false"] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "qwen", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "qwen", arguments: options + ["--prompt-interactive", prompt]) case .headless(let prompt): AgentInvocation(executable: "qwen", arguments: options + ["--prompt", prompt]) } }}
nonisolated private struct GrokRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .grok let displayName = "Grok Build" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let reasoningEffortSuggestions = ["low", "medium", "high"]
func observe(arguments: [String]) -> AgentLaunchObservation { let permissionMode = arguments.optionValue(long: "--permission-mode") let bypassesPermissions = permissionMode == "bypassPermissions" || arguments.contains("--always-approve") let sandboxIsOff = arguments.optionValue(long: "--sandbox") == "off" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: bypassesPermissions && sandboxIsOff ? .unrestricted : permissionMode == "default" ? .standard : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--reasoning-effort", effort] } switch request.configuration.executionMode { case .standard: generated += ["--permission-mode", "default"] case .unrestricted: generated += ["--permission-mode", "bypassPermissions", "--sandbox", "off"] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "grok", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "grok", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "grok", arguments: options + ["--single", prompt]) } }}
// Antigravity CLI (`agy`, verified 1.3.1). `--print`/`-p`/`--prompt`/`-i`/// `--prompt-interactive` are string flags that consume the following token as// the prompt, so the prompt always travels as that flag's final value token.// Exception: agy intercepts bare `--help`/`--version` before flag parsing, so// an exact `--help`/`--version` prompt token is unreachable via the space form// (equals form works). Generated prompts are task text and never hit this.nonisolated private struct AntigravityRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .antigravity let displayName = "Antigravity" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases // `agy --help` 1.3.1: low|medium|high|xhigh|max. let reasoningEffortSuggestions = ["low", "medium", "high", "xhigh", "max"]
/// Flags that consume the following token as a value (`agy --help` 1.3.1 /// plus hidden flags seen in updater argv): prompt text, model, effort, and /// every other string-valued option. Observation skips flag+value so a /// flag-shaped value is never read as an option, then keeps scanning the /// real flags after it. private static let valueFlags: Set<String> = [ "--print", "--prompt", "--prompt-interactive", "--add-dir", "--agent", "--conversation", "--effort", "--input-format", "--json-schema", "--log-file", "--mode", "--output-format", "--print-timeout", "--project", "--app_data_dir", "--gemini_dir", ]
/// Bool flags a session argv can carry (`agy --help` 1.3.1 plus the hidden /// updater flag; each verified to reject a positional operand on 1.3.1 — /// re-verify on agy upgrades, since a string-valued entry here would let /// agy consume a token this parser treats as decisive). Tokens matching /// none of the value/bool tables are unknown to this parser, which the /// permission scan treats as unprovable. private static let booleanFlags: Set<String> = [ "--continue", "--dangerously-skip-permissions", "--disable-slash-commands", "--new-project", "--remote-control", "--sandbox", "--bg-updater", "--help", "--version", ]
/// agy's Go-style flag parser treats one or two leading dashes identically /// (`-model`, `--model`, and even `--p` all work; verified 1.3.1). Matching /// on the bare name resolves the short aliases; any other single-dash token /// gains a second dash so every spelling reaches the same option. private static let shortOptionNames = [ "p": "--print", "i": "--prompt-interactive", "c": "--continue", ]
private static func normalizedFlag(_ token: String) -> String { guard token.hasPrefix("-") else { return token } var name = token var valueSuffix = "" if let eqIndex = token.firstIndex(of: "=") { name = String(token[..<eqIndex]) valueSuffix = String(token[eqIndex...]) } let bareName = name.drop(while: { $0 == "-" }) guard !bareName.isEmpty else { return token } if let mapped = shortOptionNames[String(bareName)] { return mapped + valueSuffix } return name.hasPrefix("--") ? token : "--\(bareName)\(valueSuffix)" }
/// Every `=` spelling Go's `strconv.ParseBool` accepts as false — the only /// forms that explicitly clear the flag. A space-separated `false` is a /// positional, not the flag's value. private static let permissionsOffForms: Set<String> = [ "--dangerously-skip-permissions=0", "--dangerously-skip-permissions=f", "--dangerously-skip-permissions=F", "--dangerously-skip-permissions=FALSE", "--dangerously-skip-permissions=false", "--dangerously-skip-permissions=False", ]
func observe(arguments: [String]) -> AgentLaunchObservation { var model: String? var flags: [String] = [] var index = arguments.startIndex while index < arguments.endIndex { let isLeadingToken = index == arguments.startIndex let token = Self.normalizedFlag(arguments[index]) index = arguments.index(after: index) if token == "--model" { if index < arguments.endIndex { model = arguments[index] index = arguments.index(after: index) } } else if token.hasPrefix("--model=") { model = String(token.dropFirst("--model=".count)) } else if Self.valueFlags.contains(token) { if index < arguments.endIndex { index = arguments.index(after: index) } } else if token == "--" { // Go flag terminator: everything after it is positional. break } else if token == "-" || !token.hasPrefix("-") { // argv0 is the only legitimate non-flag token. Go flag parsing stops // at any later positional — and agy rejects one outright — so nothing // after it is a real option. if !isLeadingToken { break } } else { flags.append(token) } } // Later arguments override earlier ones. A bare token or `=true` sets the // flag; a Go-bool false spelling explicitly clears it (Go-style bool // parsing: a space `false` is a positional, not the flag's value). let flagIndex = flags.indices.last { index in let token = flags[index] return token == "--dangerously-skip-permissions" || token.hasPrefix("--dangerously-skip-permissions=") } var executionMode: AgentExecutionMode? if let flagIndex { // An unrecognized bare flag anywhere before the decisive token could be // a hidden or newer string option that consumed argv tokens — including // the decisive one — so the mode is unprovable. Adjacency is not enough: // known value flags and their values are filtered out of `flags`, so an // unknown flag only looks adjacent to a bool it actually swallowed. let unprovable = flags[..<flagIndex].contains { $0.hasPrefix("-") && !$0.contains("=") && !Self.booleanFlags.contains($0) } if unprovable { executionMode = nil } else if Self.permissionsOffForms.contains(flags[flagIndex]) { executionMode = .standard } else { executionMode = .unrestricted } } return AgentLaunchObservation(model: model, executionMode: executionMode) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--effort", effort] } if request.configuration.executionMode == .unrestricted { generated += ["--dangerously-skip-permissions"] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "agy", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "agy", arguments: options + ["--prompt-interactive", prompt]) case .headless(let prompt): AgentInvocation(executable: "agy", arguments: options + ["--print", prompt]) } }}
nonisolated private struct PiRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .pi let displayName = "Pi" let supportsModelSelection = true let supportsReasoningEffort = true let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "PI_CODING_AGENT_DIR" ) /// Relayed by the bundled `agent-hooks/pi/prowl-hooks.ts` extension (`-e`). `agent_settled` /// is Pi's documented idle point (`agent_end` precedes it); Pi has no permission system. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .pi, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .pi) ) let reasoningEffortSuggestions = ["off", "minimal", "low", "medium", "high", "xhigh", "max"]
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation(model: arguments.optionValue(long: "--model")) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { let options = piOptions(request) return switch request.intent { case .interactive: AgentInvocation(executable: "pi", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "pi", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "pi", arguments: options + ["--print", prompt]) } }
private func piOptions(_ request: AgentStartRequest) -> [String] { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--thinking", effort] } return finalizedOptions(generated, request: request) }}
nonisolated private struct OMPRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .omp let displayName = "Oh My Pi" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "PI_CODING_AGENT_DIR" ) /// Relayed by the bundled `agent-hooks/omp/prowl-hooks.ts` extension (`--hook`). /// `session_stop` is documented main-session only, whereas `agent_end` was measured firing /// once per in-process `task` sub-agent; `/new` rotates through `session_switch`. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .omp, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .omp) ) let reasoningEffortSuggestions = ["off", "minimal", "low", "medium", "high", "xhigh", "max", "auto"]
func observe(arguments: [String]) -> AgentLaunchObservation { let approvalMode = arguments.optionValue(long: "--approval-mode") let bypasses = arguments.contains("--auto-approve") || approvalMode == "yolo" return AgentLaunchObservation( model: arguments.optionValue(long: "--model"), executionMode: bypasses ? .unrestricted : approvalMode == "always-ask" ? .standard : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--thinking", effort] } generated += [ "--approval-mode", request.configuration.executionMode == .unrestricted ? "yolo" : "always-ask", ] let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "omp", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "omp", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "omp", arguments: options + ["--print", prompt]) } }}
nonisolated extension [String] { func optionValue(long: String, short: String? = nil) -> String? { for index in indices.reversed() { let argument = self[index] if argument == long || short == argument { let next = self.index(after: index) guard next < endIndex else { return nil } return self[next] } if argument.hasPrefix(long + "=") { return String(argument.dropFirst(long.count + 1)) } } return nil }
fileprivate func containsAny(_ values: String...) -> Bool { values.contains(where: contains) }
fileprivate func booleanOptionIsFalse(_ option: String) -> Bool { contains("\(option)=false") || optionValue(long: option) == "false" }}
nonisolated extension String { fileprivate var lastPathComponent: String { URL(fileURLWithPath: self).lastPathComponent }}