native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358// ProwlCLI/Commands/WorkflowCommand.swift// `prowl workflow`: workflow definition discovery, authoring, and execution.
import ArgumentParserimport Foundationimport ProwlCLIShared
struct WorkflowCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "workflow", abstract: "Discover, validate, and run Agent Workflow definitions.", discussion: """ Definitions are .pwlworkflow bundles (`prowl.workflow/v1`) found in the app bundle, ~/.prowl/workflows, \ and <repo>/.prowl/workflows. `validate` and `schema` work with Prowl closed; every other \ subcommand needs the running app. """, subcommands: [ WorkflowListCommand.self, WorkflowReadCommand.self, WorkflowRunCommand.self, WorkflowTestActionCommand.self, WorkflowStatusCommand.self, WorkflowDeliverCommand.self, WorkflowCancelCommand.self, WorkflowValidateCommand.self, WorkflowSchemaCommand.self, ] )}
struct WorkflowListCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "list", abstract: "List the workflow definitions visible to a worktree, with validation status." )
@OptionGroup var selector: SelectorOptions @OptionGroup var options: GlobalOptions
@Argument(help: "Worktree id/name/path or a pane/tab handle. Defaults to the caller's pane.") var target: String?
mutating func run() throws { try WorkflowSocketCommand.execute(options: options) { CommandEnvelope( output: options.outputMode, command: .workflow( WorkflowInput(action: .list, target: try selector.resolve(positionalTarget: target))) ) } }}
struct WorkflowRunCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "run", abstract: "Start a validated workflow in a source pane or worktree." )
@Argument(help: "Workflow id or unique workflow name.") var workflow: String @Argument( help: "Optional source pane, tab, or worktree. Defaults to the caller pane when required.") var source: String? @OptionGroup var selector: SelectorOptions @Option( name: .long, help: "Role binding: launch=<profile name|UUID|auto>, pick=<pane handle|UUID>.") var role: [String] = [] @Option(name: .long, help: "Workflow input as name=value. Repeat for multiple inputs.") var input: [String] = [] @Option(name: .long, help: "Skip an awaited step at start. Repeat for multiple steps.") var skip: [String] = [] @OptionGroup var options: GlobalOptions
mutating func run() throws { try WorkflowSocketCommand.execute(options: options) { CommandEnvelope( output: options.outputMode, command: .workflow( WorkflowInput( action: .run, target: try selector.resolve(positionalTarget: source), workflow: workflow, roleBindings: role, inputValues: input, skippedSteps: skip )) ) } }}
struct WorkflowStatusCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "status", abstract: "Show an active run, or the run awaiting delivery from this pane." )
@Argument(help: "Workflow run UUID. Omit to inspect the calling pane's run.") var runID: String? @OptionGroup var options: GlobalOptions
mutating func run() throws { try WorkflowSocketCommand.execute(options: options) { CommandEnvelope( output: options.outputMode, command: .workflow(WorkflowInput(action: .status, runID: runID)) ) } }}
struct WorkflowDeliverCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "deliver", abstract: "Deliver one workflow step's output from stdin or a UTF-8 file." )
/// The body always travels with the request; the hard cap of dsl-spec §5 (`OUTPUT_TOO_LARGE`). static let maximumBodyBytes = WorkflowSizeLimits.payload
@Argument(help: "'-' reads the output body from piped stdin (or use --file).") var input: String? @Option(name: .long, help: "Read the UTF-8 output body from this file instead of stdin.") var file: String? @Option(name: .long, help: "Declared verdict value, when this step requires one.") var verdict: String? @Option(name: .long, help: "Delivery token; defaults to $PROWL_WORKFLOW_TOKEN.") var token: String? @Option(name: .customLong("run"), help: "Run UUID of a manual delivery (with --step).") var runID: String? @Option(name: .long, help: "Step id of a manual delivery (with --run).") var step: String? @Flag( name: .long, help: "Deliver to the explicit --run/--step even when this pane belongs to another step.") var force = false @OptionGroup var options: GlobalOptions
mutating func run() throws { let body = try bodyValue() try WorkflowSocketCommand.execute(options: options) { CommandEnvelope( output: options.outputMode, command: .workflow( WorkflowInput( action: .deliver, runID: runID, stepID: step, body: body, verdict: verdict, token: token ?? ProcessInfo.processInfo.environment[WorkflowSchema.tokenEnvironmentKey], force: force )) ) } }
func validate() throws { try Self.validate(input: input, file: file, runID: runID, step: step, force: force) }
/// Argument rules, shared with the parser tests. static func validate(input: String?, file: String?, runID: String?, step: String?, force: Bool) throws { guard input != nil || file != nil else { throw ValidationError("Pass the output body through stdin ('-') or --file <path>.") } guard !(input != nil && file != nil) else { throw ValidationError("Pass the body through stdin ('-') or --file, not both.") } guard input == nil || input == "-" else { throw ValidationError("The only positional output source is '-'.") } guard (runID == nil) == (step == nil) else { throw ValidationError("--run and --step must be passed together.") } guard !force || runID != nil else { throw ValidationError("--force applies to an explicit --run/--step target.") } }
private func bodyValue() throws -> String { let data: Data if let file { guard let contents = FileManager.default.contents(atPath: file) else { throw ExitError(code: CLIErrorCode.pathNotFound, message: "Could not read --file \(file).") } data = contents } else { guard isatty(fileno(stdin)) == 0 else { throw ExitError( code: CLIErrorCode.emptyInput, message: "workflow deliver - reads the output body from piped stdin.") } data = (try? FileHandle.standardInput.readToEnd()) ?? Data() } guard data.count <= Self.maximumBodyBytes else { throw ExitError( code: CLIErrorCode.outputTooLarge, message: "The output body is \(data.count) bytes; the maximum is \(Self.maximumBodyBytes).") } guard let text = String(data: data, encoding: .utf8) else { throw ExitError( code: CLIErrorCode.invalidArgument, message: "The output body is not valid UTF-8.") } return text }}
struct WorkflowCancelCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "cancel", abstract: "Cancel an active workflow run.")
@Argument(help: "Workflow run UUID.") var runID: String @OptionGroup var options: GlobalOptions
mutating func run() throws { try WorkflowSocketCommand.execute(options: options) { CommandEnvelope( output: options.outputMode, command: .workflow(WorkflowInput(action: .cancel, runID: runID)) ) } }}
struct WorkflowValidateCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "validate", abstract: "Parse and validate a workflow file locally; exits non-zero when it has errors." )
enum Scope: String, ExpressibleByArgument, CaseIterable { case bundle case user case repo
var value: WorkflowScope { switch self { case .bundle: .bundle case .user: .user case .repo: .repo } } }
@Argument(help: "Path to a .pwlworkflow bundle directory.") var file: String @Option(name: .long, help: "Source scope (bundle, user, repo); inferred when omitted.") var scope: Scope? @OptionGroup var options: GlobalOptions
mutating func run() throws { try CLIExecution.run( command: WorkflowCommandPayload.commandName, output: options.outputMode, colorEnabled: options.colorEnabled ) { let payload = try WorkflowCommandExecutor.current().validate(path: file, scope: scope?.value) if payload.valid { try WorkflowCommandRunner.render(.validate(payload), options: options) return } let response = CommandResponse( ok: false, command: WorkflowCommandPayload.commandName, schemaVersion: WorkflowCommandPayload.schemaVersion, error: CommandError( code: CLIErrorCode.workflowInvalid, message: "\(payload.path) has \(payload.diagnostics.filter { $0.severity == .error }.count) error(s).", details: try RawJSON(encoding: payload) ) ) switch options.outputMode { case .json: OutputRenderer.render(response, mode: .json) case .text: print(OutputRenderer.workflowValidateText(payload)) } throw ExitCode.failure } }}
struct WorkflowSchemaCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "schema", abstract: "Print the JSON Schema of a workflow bundle (for authoring agents and editors)." )
@Flag(help: "Print the local script action manifest schema.") var action = false @OptionGroup var options: GlobalOptions
mutating func run() throws { try CLIExecution.run( command: WorkflowCommandPayload.commandName, output: options.outputMode, colorEnabled: options.colorEnabled ) { try WorkflowCommandRunner.render( .schema(try WorkflowCommandExecutor.current().schema(action: action)), options: options) } }}
enum WorkflowSocketCommand { static func execute(options: GlobalOptions, makeEnvelope: () throws -> CommandEnvelope) throws { try CLIExecution.run( command: WorkflowCommandPayload.commandName, output: options.outputMode, colorEnabled: options.colorEnabled ) { try CLIRunner.execute(try makeEnvelope()) } }}
enum WorkflowCommandRunner { static func render(_ payload: WorkflowCommandPayload, options: GlobalOptions) throws { let response = CommandResponse( ok: true, command: WorkflowCommandPayload.commandName, schemaVersion: WorkflowCommandPayload.schemaVersion, data: try RawJSON(encoding: payload) ) OutputRenderer.render(response, mode: options.outputMode) }}
struct WorkflowTestActionCommand: ParsableCommand { static let configuration = CommandConfiguration(commandName: "test-action", abstract: "Run one action from an installed workflow bundle with the same native approval policy.")
@Argument(help: "Workflow id or unique name.") var workflow: String @Argument(help: "builtin:collect-worktree-context or local:<action-id>.") var action: String @Argument(help: "Source worktree or pane.") var source: String? @Option(name: .long, help: "JSON object supplied to the action.") var inputJSON = "{}" @OptionGroup var selector: SelectorOptions @OptionGroup var options: GlobalOptions
mutating func run() throws { guard let data = inputJSON.data(using: .utf8), data.count <= WorkflowSizeLimits.payload, let value = try? JSONDecoder().decode(WorkflowJSONValue.self, from: data), case .object(let inputs) = value else { throw ValidationError("--input-json must be a JSON object of at most 16 MiB.") } try WorkflowJSON.validate(value) try WorkflowSocketCommand.execute(options: options) { CommandEnvelope(output: options.outputMode, command: .workflow(WorkflowInput(action: .run, target: try selector.resolve(positionalTarget: source), workflow: workflow, testAction: action, actionInputs: inputs))) } }}
struct WorkflowReadCommand: ParsableCommand { static let configuration = CommandConfiguration( commandName: "read", abstract: "Read the current workflow task or one of its granted resources.") @Argument(help: "Resource ID from the task response; omit to read the task.") var resource: String? @Option(name: .long, help: "Byte offset from the previous response; reads at most 256 KiB.") var offset: Int64 = 0 @Option(name: .customLong("run"), help: "Assigned workflow run UUID.") var runID: String @Option(name: .long, help: "Assigned invocation number.") var invocation: Int @OptionGroup var options: GlobalOptions
mutating func run() throws { try WorkflowSocketCommand.execute(options: options) { CommandEnvelope( output: options.outputMode, command: .workflow(WorkflowInput( action: .read, invocation: invocation, contentOffset: offset, contentResource: resource, runID: runID))) } }}