mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
* Let automations pass extra agent arguments on each run Automations could only use the host's default agent Arguments, so a per-automation choice such as a different model needed a settings change that affected every run. Automations can now save extra arguments that are appended to the host's defaults on each fresh run. Extras are allowlisted to model, effort, and Claude's --add-dir, and are refused for permission, config, prompt, and session options. - Add the field through the CLI (`automations create/edit --extra-agent-args`), the RPC contract, the automation store, and the editor's Advanced section, with validation at save and at launch. - Merge extras over the defaults by replacing a model or effort option the defaults already set, then re-tokenize to confirm the launch argv matches what was validated. - Gate writes behind a new runtime capability, and refuse to save or migrate extras to a host that would silently drop them. - Require a fresh session, since reused sessions cannot take new arguments. * Simplify automation argument validation and launch handling * improve the extra args display * fix translation * Validate automation extra args during ORCA migration Dormant automations imported from the migration manifest skipped the extra-args check. A disallowed flag, or Reuse combined with extra args, could persist and then fail every later run. The manifest parser now rejects those rows. Also move the automation runtime capability constants into `automation-runtime-capabilities.ts`. `protocol-version.ts` re-exports them, so existing imports keep working. * add more demo with effort
148 lines
5.9 KiB
TypeScript
148 lines
5.9 KiB
TypeScript
/**
|
|
* Human-readable automation output for the CLI.
|
|
*
|
|
* The host lines come from the authority's own projection, never from the
|
|
* stored `executionTarget*` fields: a target that was removed — or removed and
|
|
* re-registered under the same id — leaves those fields untouched, so printing
|
|
* them alone would render a dead host identically to a healthy one and leave
|
|
* `orca automations show` unable to complete the recovery it is named in.
|
|
*/
|
|
|
|
import type { Automation, AutomationRun } from '../shared/automations-types'
|
|
import type { AutomationListItem } from '../shared/automation-list-scope'
|
|
import type { AutomationOwnerPrecondition } from '../shared/automation-owner-precondition'
|
|
import { getAutomationLegacyRepoId } from '../shared/automation-run-identity'
|
|
import { formatAutomationPrecheckTimeout } from '../shared/automation-precheck'
|
|
import { formatAutomationSchedule } from '../shared/automation-schedules'
|
|
|
|
export type AutomationListPayload = {
|
|
automations: Automation[]
|
|
/** Present only when the authority qualifies its rows; an older host sends none. */
|
|
items?: AutomationListItem[]
|
|
}
|
|
|
|
export type AutomationShowPayload = {
|
|
automation: Automation
|
|
/** Present only when the authority can project an owner; an older host sends none. */
|
|
owner?: AutomationOwnerPrecondition
|
|
}
|
|
|
|
const ORPHAN_HOST_LABEL = 'orphan (no host can run this automation)'
|
|
|
|
function formatOwnerSelector(selector: AutomationOwnerPrecondition['selector']): string {
|
|
if (selector.kind === 'ssh') {
|
|
return `ssh:${selector.targetId} (generation ${selector.targetGeneration})`
|
|
}
|
|
return selector.kind === 'orphan' ? ORPHAN_HOST_LABEL : 'self'
|
|
}
|
|
|
|
function formatListItemSelector(item: AutomationListItem): string {
|
|
const selector = item.selector
|
|
if (selector.kind === 'ssh') {
|
|
return `ssh:${selector.targetId} (generation ${selector.targetGeneration})`
|
|
}
|
|
return selector.kind === 'orphan' ? `orphan — ${selector.issue}` : 'self'
|
|
}
|
|
|
|
export function formatAutomationList(result: AutomationListPayload): string {
|
|
if (result.automations.length === 0) {
|
|
return 'No automations found.'
|
|
}
|
|
const hosts = new Map(result.items?.map((item) => [item.automationId, item]) ?? [])
|
|
return result.automations
|
|
.map((automation) => {
|
|
const status = automation.enabled ? 'enabled' : 'disabled'
|
|
const item = hosts.get(automation.id)
|
|
const host = item ? `\nhost: ${formatListItemSelector(item)}` : ''
|
|
return `${automation.id} ${automation.name} ${automation.agentId} ${status}\n${formatAutomationSchedule(automation.rrule)} next: ${new Date(automation.nextRunAt).toISOString()}${host}`
|
|
})
|
|
.join('\n\n')
|
|
}
|
|
|
|
export function formatAutomationShow(result: AutomationShowPayload): string {
|
|
const automation = result.automation
|
|
const runContext = automation.runContext ?? null
|
|
const projectLines = runContext
|
|
? [
|
|
`runProjectId: ${runContext.projectId}`,
|
|
`runHostId: ${runContext.hostId}`,
|
|
`projectHostSetupId: ${runContext.projectHostSetupId}`,
|
|
`runRepoId: ${runContext.repoId}`,
|
|
`runPath: ${runContext.path}`,
|
|
`legacyRepoId: ${getAutomationLegacyRepoId(automation)}`
|
|
]
|
|
: [`legacyRepoId: ${getAutomationLegacyRepoId(automation)}`]
|
|
return [
|
|
`id: ${automation.id}`,
|
|
`name: ${automation.name}`,
|
|
`provider: ${automation.agentId}`,
|
|
...(automation.extraAgentArgs ? [`extraAgentArgs: ${automation.extraAgentArgs}`] : []),
|
|
`enabled: ${automation.enabled}`,
|
|
`schedule: ${formatAutomationSchedule(automation.rrule)}`,
|
|
`rrule: ${automation.rrule}`,
|
|
`precheck: ${
|
|
automation.precheck
|
|
? `${automation.precheck.command} (timeout ${formatAutomationPrecheckTimeout(
|
|
automation.precheck.timeoutSeconds
|
|
)})`
|
|
: 'none'
|
|
}`,
|
|
`nextRunAt: ${new Date(automation.nextRunAt).toISOString()}`,
|
|
...projectLines,
|
|
`workspaceMode: ${automation.workspaceMode}`,
|
|
`workspaceId: ${automation.workspaceId ?? 'null'}`,
|
|
`baseBranch: ${automation.baseBranch ?? 'null'}`,
|
|
`reuseSession: ${automation.reuseSession}`,
|
|
`target: ${automation.executionTargetType}:${automation.executionTargetId}`,
|
|
// Omitted rather than guessed: a host that reports no owner has none to report.
|
|
...(result.owner ? [`host: ${formatOwnerSelector(result.owner.selector)}`] : []),
|
|
`prompt: ${automation.prompt}`
|
|
].join('\n')
|
|
}
|
|
|
|
export function formatAutomationRemoved(result: { removed: boolean; id: string }): string {
|
|
return result.removed
|
|
? `Removed automation ${result.id}.`
|
|
: `Automation ${result.id} not removed.`
|
|
}
|
|
|
|
export function formatAutomationRun(result: { run: AutomationRun }): string {
|
|
return [
|
|
`id: ${result.run.id}`,
|
|
`automationId: ${result.run.automationId}`,
|
|
`title: ${result.run.title}`,
|
|
`status: ${result.run.status}`,
|
|
`trigger: ${result.run.trigger}`,
|
|
`scheduledFor: ${new Date(result.run.scheduledFor).toISOString()}`,
|
|
`workspaceId: ${result.run.workspaceId ?? 'null'}`,
|
|
`precheck: ${formatAutomationRunPrecheck(result.run)}`,
|
|
`error: ${result.run.error ?? 'null'}`
|
|
].join('\n')
|
|
}
|
|
|
|
function formatAutomationRunPrecheck(run: AutomationRun): string {
|
|
const result = run.precheckResult
|
|
if (!result) {
|
|
return 'none'
|
|
}
|
|
const outcome = result.timedOut
|
|
? 'timed out'
|
|
: result.error
|
|
? 'error'
|
|
: `exit ${result.exitCode ?? 'unknown'}`
|
|
const output = result.stderr.trim() || result.stdout.trim()
|
|
return output ? `${outcome}; ${output}` : outcome
|
|
}
|
|
|
|
export function formatAutomationRuns(result: { runs: AutomationRun[] }): string {
|
|
if (result.runs.length === 0) {
|
|
return 'No automation runs found.'
|
|
}
|
|
return result.runs
|
|
.map(
|
|
(run) =>
|
|
`${run.id} ${run.automationId} ${run.status} ${run.trigger} ${new Date(run.scheduledFor).toISOString()}\n${run.title}${run.precheckResult ? `\nprecheck: ${formatAutomationRunPrecheck(run)}` : ''}${run.error ? `\nerror: ${run.error}` : ''}`
|
|
)
|
|
.join('\n\n')
|
|
}
|