feat(orchestra): add custom commands with typed arguments#3339
feat(orchestra): add custom commands with typed arguments#3339sidferreira wants to merge 11 commits into
Conversation
Allow flow files to declare themselves as reusable, first-class commands via
a `command:` + `arguments:` header in the YAML config block. Callers invoke
them like built-ins (e.g. `- greet: { who: "world" }`) instead of going
through `runFlow:` + `env:`.
- Workspace mode auto-registers any flow with a `command:` header
- Single-file mode (`maestro test foo.yaml`) auto-discovers commands from
`<flow parent>/subflows/`
- Arguments support string/number/boolean with required/default
- Validation happens at parse time (unknown keys, missing required, type
mismatch)
- Custom-command bodies cannot invoke other custom commands (no nesting),
enforced explicitly during workspace pre-pass
- A custom-command call desugars at parse time into a `RunFlowCommand` with
a prepended `DefineVariablesCommand` — no `Orchestra` dispatcher changes
needed
The key cross-cutting fix is in `TestCommand.makeChunkPlans`, which used to
reconstruct `ExecutionPlan` and silently drop the discovered registry — even
single-file runs went through chunking, so the registry was being thrown
away between `plan()` and the actual run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self code-review findings (Kotlin best practices + code quality)Pre-merge findings I'd like reviewers to weigh in on. None are blocking compile/tests, but some are worth addressing before merge. Counts: 0 Critical / 4 Important / 6 Minor. Update: All 10 findings have been addressed in follow-up commits ( Important
Minor
What's well-done
All findings resolved. Each fix is in its own commit (see |
…nner
Files.walk returns a Stream backed by an open directory handle that must
be explicitly closed to release the FD. Wrap with .use {} to release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Set subtraction (call.args.keys - knownNames) allocates a fresh Set even in the common (empty) case. filterNot reads more naturally and avoids the allocation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YamlFlowArgument.coerceDefault and YamlFluentCommand.coerceArg both validate a string as numeric via toDoubleOrNull-or-throw and return the same string back. Extracted into a shared top-level helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the requireNumeric extraction: both YamlFlowArgument.coerceDefault and YamlFluentCommand.coerceArg validate true/false the same way. Pull the logic into a shared top-level helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…line Three CLI files declared customCommands: Map<String, maestro.orchestra.CustomCommandDef> with the FQN inline. Import once at the top of each file per Kotlin convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Only isBuiltInCommand() is called externally. Inline the lookup against the existing private builtInCommands list and remove the public Set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ternal Called from YamlConfig.toCommand() and WorkspaceExecutionPlanner — both in the same module. Drop public visibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…scovery Replace catch (_: Throwable) with catch (e: Exception) in both discovery and nesting-check loops. Throwable was catching VM errors (OutOfMemoryError, StackOverflowError). Add DEBUG-level logging so a silently-skipped file is recoverable from logs. Adds a fixture+test (022_malformed_command_file) that confirms a malformed YAML in a subflows/ subdir is skipped without crashing the planner. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…etection Replace the implicit String contract (RunFlowCommand.sourceDescription == CustomCommandDef.sourceFile.toString()) with an explicit RunFlowCommand field, populated by buildCustomCommandRunFlow when expanding a custom- command call. Nesting detection then becomes a direct map lookup by name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion files Before custom commands, init was 'appId = url ?: _appId!!' — the bang-bang was safe because the guard above enforced at least one of url/_appId was non-null. Adding command-definition files relaxed that guard, which forced a unconditional '?: ""' fallback and dropped the assertion for all flows. Branch on isCommandDefinition instead: runnable flows keep the original _appId!! assertion; command-definition files take the explicit fallback path with a comment explaining why "" is safe (those files are excluded from flowsToRun and their RunFlowCommand has config = null). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Manual reproduction —
|
Proposed changes
Adds support for declaring custom commands directly in YAML flows. A flow file can declare itself as a reusable, first-class command via a
command:+arguments:header in its config block; callers invoke it like a built-in instead of going throughrunFlow:+env:.Example — declare:
Example — call:
Highlights:
command:header.maestro test foo.yaml) auto-discovers commands from<flow parent>/subflows/.string/number/booleanwithrequiredanddefault.RunFlowCommandwith a prependedDefineVariablesCommand— zero changes toOrchestra's dispatcher.A cross-cutting fix is bundled:
TestCommand.makeChunkPlanswas reconstructingExecutionPlanand silently dropping the discovered registry — even single-file runs go through chunking, so the registry was being lost betweenplan()and the run. This had no prior visible effect (the registry was always empty before this change) but had to be fixed for the feature to function end-to-end.Testing
YamlCommandReaderTest(header parsing, call expansion, all four validation error types, suggestion-list inclusion).WorkspaceExecutionPlannerTest(workspace-mode discovery, single-filesubflows/discovery, command-definition files excluded from runnable flows, nested-custom-command rejection, malformed-command-file is skipped during discovery).maestro-test/IntegrationTest(Case 100) — drivesOrchestrawith a fake driver and asserts the executed sequence../gradlew :maestro-orchestra-models:test :maestro-orchestra:test :maestro-test:test).Issues fixed
N/A