Skip to content

Commit 1e824f3

Browse files
authored
feat: scope Lab execution placement flags (#8182)
1 parent 3938124 commit 1e824f3

11 files changed

Lines changed: 282 additions & 50 deletions

File tree

src/cli_runtime.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use clap::{ArgMatches, Command, CommandFactory};
1+
use clap::{ArgMatches, Command};
22
use std::io::IsTerminal;
33
use std::sync::OnceLock;
44

@@ -56,7 +56,7 @@ enum StartupFastPath {
5656
pub fn run_startup_fast_path(args: &[String]) -> Option<std::process::ExitCode> {
5757
match startup_fast_path(args)? {
5858
StartupFastPath::Help => {
59-
let mut cmd = Cli::command();
59+
let mut cmd = Cli::command_with_scoped_lab_args();
6060
cmd.print_help().expect("Failed to print help");
6161
println!();
6262
}
@@ -106,7 +106,7 @@ impl CliRuntime {
106106
}
107107

108108
fn parse_matches(&self, normalized: Vec<String>) -> ArgMatches {
109-
match Cli::command().try_get_matches_from(normalized.clone()) {
109+
match Cli::command_with_scoped_lab_args().try_get_matches_from(normalized.clone()) {
110110
Ok(matches) => matches,
111111
Err(static_err) => match self
112112
.build_augmented_command()
@@ -465,7 +465,7 @@ fn build_augmented_command(
465465
extension_info: &[ExtensionCliInfo],
466466
extension_health: &ExtensionCliHealth,
467467
) -> Command {
468-
let mut cmd = Cli::command();
468+
let mut cmd = Cli::command_with_scoped_lab_args();
469469

470470
for info in extension_info {
471471
let project_id_help = info
@@ -1276,7 +1276,7 @@ mod tests {
12761276

12771277
#[test]
12781278
fn wrapper_global_runner_preserves_trailing_output_request() {
1279-
let matches = Cli::command()
1279+
let matches = Cli::command_with_scoped_lab_args()
12801280
.try_get_matches_from([
12811281
"homeboy",
12821282
"--runner",

src/cli_surface.rs

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub enum Placement {
2323
Auto,
2424
Local,
2525
Lab,
26+
#[value(name = "lab-or-local")]
27+
LabOrLocal,
2628
}
2729

2830
impl Default for Placement {
@@ -79,10 +81,6 @@ pub struct Cli {
7981
#[arg(long, global = true, value_name = "RUNNER_ID")]
8082
pub runner: Option<String>,
8183

82-
/// Permit a selected Lab runner to fall back to local execution after offload preflight fails.
83-
#[arg(long, global = true)]
84-
pub allow_local_fallback: bool,
85-
8684
/// Permit Lab git workspace materialization to overwrite a dirty runner-side checkout.
8785
#[arg(long, global = true)]
8886
pub allow_dirty_lab_workspace: bool,
@@ -111,6 +109,12 @@ pub struct Cli {
111109
}
112110

113111
impl Cli {
112+
/// Builds the user-facing command tree with Lab options shown only where
113+
/// the Lab portability contract can honor them.
114+
pub fn command_with_scoped_lab_args() -> Command {
115+
crate::command_contract::scope_lab_cli_arguments(Self::command())
116+
}
117+
114118
pub fn from_registered_arg_matches(
115119
matches: &ArgMatches,
116120
) -> Result<(Self, &'static crate::command_contract::CommandSpec), clap::Error> {
@@ -1091,8 +1095,9 @@ mod tests {
10911095
for args in [
10921096
["homeboy", "--placement=local", "bench", "example"].as_slice(),
10931097
["homeboy", "bench", "--placement", "lab", "example"].as_slice(),
1098+
["homeboy", "bench", "--placement", "lab-or-local", "example"].as_slice(),
10941099
] {
1095-
let matches = Cli::command()
1100+
let matches = Cli::command_with_scoped_lab_args()
10961101
.try_get_matches_from(args)
10971102
.expect("registered command parses placement");
10981103
let (cli, _) = Cli::from_registered_arg_matches(&matches)
@@ -1101,6 +1106,16 @@ mod tests {
11011106
}
11021107
}
11031108

1109+
#[test]
1110+
fn placement_exposes_explicit_lab_or_local_fallback() {
1111+
let cli =
1112+
Cli::try_parse_from(["homeboy", "bench", "example", "--placement", "lab-or-local"])
1113+
.expect("lab-or-local placement should parse");
1114+
1115+
assert_eq!(cli.placement, Placement::LabOrLocal);
1116+
assert!(cli.placement.allows_local_fallback());
1117+
}
1118+
11041119
#[test]
11051120
fn placement_does_not_consume_passthrough_arguments() {
11061121
let cli = Cli::try_parse_from([
@@ -1118,13 +1133,61 @@ mod tests {
11181133
}
11191134

11201135
#[test]
1121-
fn placement_is_visible() {
1122-
let command = Cli::command();
1123-
let placement = command
1124-
.get_arguments()
1125-
.find(|arg| arg.get_id() == "placement")
1126-
.expect("placement argument");
1127-
assert!(!placement.is_hide_set());
1136+
fn lab_flags_are_hidden_from_non_portable_command_help() {
1137+
let help = scoped_help(&["contract", "manifest"]);
1138+
1139+
for flag in [
1140+
"--placement",
1141+
"--runner",
1142+
"--detach-after-handoff",
1143+
"--allow-dirty-lab-workspace",
1144+
"--skip-deps-hydration",
1145+
"--runner-env",
1146+
"--lab-env-json",
1147+
"--runner-workspace-root",
1148+
"--artifact-root",
1149+
] {
1150+
assert!(
1151+
!help.contains(flag),
1152+
"contract manifest must not advertise {flag}"
1153+
);
1154+
}
1155+
}
1156+
1157+
#[test]
1158+
fn lab_flags_remain_visible_for_portable_command_help() {
1159+
let help = scoped_help(&["bench"]);
1160+
1161+
for flag in [
1162+
"--placement",
1163+
"--runner",
1164+
"--detach-after-handoff",
1165+
"--allow-dirty-lab-workspace",
1166+
"--skip-deps-hydration",
1167+
"--runner-env",
1168+
"--lab-env-json",
1169+
"--runner-workspace-root",
1170+
] {
1171+
assert!(help.contains(flag), "bench must advertise {flag}");
1172+
}
1173+
}
1174+
1175+
#[test]
1176+
fn portable_review_subcommand_help_keeps_lab_flags() {
1177+
let help = scoped_help(&["review", "lint"]);
1178+
assert!(help.contains("--placement"));
1179+
assert!(help.contains("--runner"));
1180+
}
1181+
1182+
fn scoped_help(path: &[&str]) -> String {
1183+
let mut command = Cli::command_with_scoped_lab_args();
1184+
for segment in path {
1185+
command = command
1186+
.find_subcommand(segment)
1187+
.unwrap_or_else(|| panic!("missing command path segment `{segment}`"))
1188+
.clone();
1189+
}
1190+
command.render_help().to_string()
11281191
}
11291192

11301193
#[test]

src/command_contract.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,18 @@ pub use constants::{
4848
pub use lab::{
4949
lab_runner_support_summary, lab_runner_supported_contract_labels, lab_runner_supported_labels,
5050
lab_runner_supports_contract_label, lab_runner_unsupported_hint,
51-
lab_runner_unsupported_message, run_location_index_path, AgentTaskDispatchIdentity,
52-
CommandPortabilityContract, LabCommandContract, LabCommandPortability, LabCommandRouteContract,
53-
LabRigWorkloadArguments, LabRigWorkloadKind, LabRoutingPolicy, LabRunnerSupportSummary,
54-
LabSecretEnvSource, LabSourcePathMode, LabWorkspaceModePolicy, RunLocationIndex,
55-
RunnerHandoffArtifactManifestRef, RunnerHandoffEnvelope, RunnerHandoffFollowCommands,
56-
RunnerWorkload, RunnerWorkloadAgentTask, RunnerWorkloadAgentTaskDispatchKind,
57-
RunnerWorkloadAgentTaskLifecycleMirrorPolicy, RunnerWorkloadArtifactRef,
58-
RunnerWorkloadAssignment, RunnerWorkloadCapability, RunnerWorkloadCommandFamily,
59-
RunnerWorkloadExtensionRevision, RunnerWorkloadKind, RunnerWorkloadMutationPolicy,
60-
RunnerWorkloadResultRefs, RunnerWorkloadSecrets, RunnerWorkloadState,
61-
RunnerWorkloadWorkspaceMappings, LAB_CAPABILITY_PLAYWRIGHT, LAB_TRACE_EXTRA_CAPABILITIES,
62-
RUNNER_ARTIFACT_MANIFEST_FILE, RUNNER_ARTIFACT_MANIFEST_REF_NAME,
51+
lab_runner_unsupported_message, run_location_index_path, scope_lab_cli_arguments,
52+
AgentTaskDispatchIdentity, CommandPortabilityContract, LabCommandContract,
53+
LabCommandPortability, LabCommandRouteContract, LabRigWorkloadArguments, LabRigWorkloadKind,
54+
LabRoutingPolicy, LabRunnerSupportSummary, LabSecretEnvSource, LabSourcePathMode,
55+
LabWorkspaceModePolicy, RunLocationIndex, RunnerHandoffArtifactManifestRef,
56+
RunnerHandoffEnvelope, RunnerHandoffFollowCommands, RunnerWorkload, RunnerWorkloadAgentTask,
57+
RunnerWorkloadAgentTaskDispatchKind, RunnerWorkloadAgentTaskLifecycleMirrorPolicy,
58+
RunnerWorkloadArtifactRef, RunnerWorkloadAssignment, RunnerWorkloadCapability,
59+
RunnerWorkloadCommandFamily, RunnerWorkloadExtensionRevision, RunnerWorkloadKind,
60+
RunnerWorkloadMutationPolicy, RunnerWorkloadResultRefs, RunnerWorkloadSecrets,
61+
RunnerWorkloadState, RunnerWorkloadWorkspaceMappings, LAB_CAPABILITY_PLAYWRIGHT,
62+
LAB_TRACE_EXTRA_CAPABILITIES, RUNNER_ARTIFACT_MANIFEST_FILE, RUNNER_ARTIFACT_MANIFEST_REF_NAME,
6363
RUNNER_ARTIFACT_MANIFEST_REF_SCHEMA, RUNNER_ARTIFACT_MANIFEST_SCHEMA,
6464
RUNNER_ARTIFACT_ROOT_DIR_SUFFIX, RUNNER_HANDOFF_ENVELOPE_SCHEMA, RUNNER_WORKLOAD_SCHEMA,
6565
RUN_LOCATION_INDEX_SCHEMA,

src/command_contract/lab.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::core::runner_execution_envelope::{
1919
PathMaterializationPlan, RunnerExecutionArtifactRef, RunnerExecutionNextAction,
2020
RunnerExecutionRecord,
2121
};
22+
use clap::Command;
2223
use std::collections::BTreeSet;
2324

2425
use super::spec::{
@@ -42,6 +43,127 @@ pub const RUNNER_ARTIFACT_MANIFEST_SCHEMA: &str = crate::core::artifacts::ARTIFA
4243
pub const RUNNER_ARTIFACT_MANIFEST_FILE: &str = crate::core::artifacts::ARTIFACT_MANIFEST_FILE;
4344
pub const RUNNER_ARTIFACT_ROOT_DIR_SUFFIX: &str = "-homeboy-artifacts";
4445

46+
const LAB_CLI_ARGUMENT_IDS: &[&str] = &[
47+
"placement",
48+
"detach_after_handoff",
49+
"artifact_root",
50+
"runner",
51+
"allow_dirty_lab_workspace",
52+
"skip_deps_hydration",
53+
"runner_env",
54+
"lab_env_json",
55+
"runner_workspace_root",
56+
];
57+
58+
impl crate::cli_surface::Placement {
59+
/// Explicitly permit controller execution when an intended Lab offload
60+
/// cannot proceed. `Auto` retains the existing default routing behavior.
61+
pub const fn allows_local_fallback(self) -> bool {
62+
matches!(self, Self::LabOrLocal)
63+
}
64+
65+
/// Whether the operator requested a Lab attempt instead of leaving the
66+
/// command to its automatic routing policy.
67+
pub const fn requests_lab(self) -> bool {
68+
matches!(self, Self::Lab | Self::LabOrLocal)
69+
}
70+
}
71+
72+
/// Projects the generated Clap tree onto the commands whose portability
73+
/// contract can route work through Lab. Parsing remains global so placement
74+
/// options keep working before or after a subcommand, but help only advertises
75+
/// options a command can honor.
76+
pub fn scope_lab_cli_arguments(command: Command) -> Command {
77+
let lab_args = command
78+
.get_arguments()
79+
.filter(|arg| LAB_CLI_ARGUMENT_IDS.contains(&arg.get_id().as_str()))
80+
.cloned()
81+
.collect::<Vec<_>>();
82+
let command = LAB_CLI_ARGUMENT_IDS.iter().fold(command, |command, id| {
83+
command.mut_arg(id, |arg| arg.hide(true))
84+
});
85+
86+
scope_lab_cli_arguments_at_path(command, &[], &lab_args)
87+
}
88+
89+
fn scope_lab_cli_arguments_at_path(
90+
command: Command,
91+
path: &[String],
92+
lab_args: &[clap::Arg],
93+
) -> Command {
94+
let visible = lab_cli_arguments_are_visible_for_path(path);
95+
let command = if visible {
96+
lab_args.iter().fold(command, |command, arg| {
97+
command.arg(arg.clone().global(false).hide(false))
98+
})
99+
} else {
100+
command
101+
};
102+
103+
command.mut_subcommands(|subcommand| {
104+
let mut subcommand_path = path.to_vec();
105+
subcommand_path.push(subcommand.get_name().to_string());
106+
scope_lab_cli_arguments_at_path(subcommand, &subcommand_path, lab_args)
107+
})
108+
}
109+
110+
fn lab_cli_arguments_are_visible_for_path(path: &[String]) -> bool {
111+
matches!(
112+
path.iter()
113+
.map(String::as_str)
114+
.collect::<Vec<_>>()
115+
.as_slice(),
116+
["agent-task", "cook"]
117+
| ["agent-task", "run-plan"]
118+
| ["agent-task", "run"]
119+
| ["agent-task", "run-next"]
120+
| ["agent-task", "status"]
121+
| ["agent-task", "list"]
122+
| ["agent-task", "active"]
123+
| ["agent-task", "latest"]
124+
| ["agent-task", "logs"]
125+
| ["agent-task", "artifacts"]
126+
| ["agent-task", "evidence"]
127+
| ["agent-task", "review"]
128+
| ["agent-task", "retry"]
129+
| ["agent-task", "promote"]
130+
| ["agent-task", "providers"]
131+
| ["agent-task", "fanout", "submit-batch"]
132+
| ["agent-task", "fanout", "status"]
133+
| ["agent-task", "fanout", "artifacts"]
134+
| ["agent-task", "auth", "status"]
135+
| ["agent-task", "controller", "from-spec"]
136+
| ["agent-task", "controller", "run-from-spec"]
137+
| ["agent-task", "controller", "materialize"]
138+
| ["agent-task", "controller", "resume"]
139+
| ["bench"]
140+
| ["bench", "matrix"]
141+
| ["fuzz"]
142+
| ["fuzz", "run"]
143+
| ["fuzz", "run-campaign"]
144+
| ["fuzz", "list"]
145+
| ["fuzz", "plan"]
146+
| ["fuzz", "doctor"]
147+
| ["review"]
148+
| ["review", "audit"]
149+
| ["review", "lint"]
150+
| ["review", "test"]
151+
| ["trace"]
152+
| ["refactor"]
153+
| ["rig", "check"]
154+
| ["rig", "run"]
155+
| ["runtime", "refresh"]
156+
| ["worktree", "cleanup"]
157+
| ["extension", "update"]
158+
| ["extension", "refresh"]
159+
| ["extension", "dev-run"]
160+
| ["extension", "show"]
161+
| ["tunnel", "preview-consumer", "run"]
162+
| ["tunnel", "service", "expose"]
163+
| ["tunnel", "service", "start"]
164+
)
165+
}
166+
45167
/// Routing-policy flags owned by the Lab command contract and retained through
46168
/// route planning, offload, and runner dispatch.
47169
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]

src/commands/infra/route.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ pub fn route_after_parse(
145145
normalized_args: routed_args,
146146
explicit_runner: cli.runner.as_deref(),
147147
placement: cli.placement,
148-
allow_local_fallback: cli.allow_local_fallback,
148+
allow_local_fallback: cli.placement.allows_local_fallback(),
149149
allow_dirty_lab_workspace: cli.allow_dirty_lab_workspace,
150150
skip_deps_hydration: cli.skip_deps_hydration,
151151
capture_patch: capture_mutation_patch,
@@ -962,10 +962,7 @@ fn runner_rig_source_management_command(
962962
iter.next();
963963
continue;
964964
}
965-
if arg == "--allow-local-fallback"
966-
|| arg == "--allow-dirty-lab-workspace"
967-
|| arg == "--detach-after-handoff"
968-
{
965+
if arg == "--allow-dirty-lab-workspace" || arg == "--detach-after-handoff" {
969966
continue;
970967
}
971968
if arg.starts_with("--runner=")
@@ -2137,7 +2134,8 @@ mod tests {
21372134
"--runner".to_string(),
21382135
"homeboy-lab".to_string(),
21392136
"--output=./sources.json".to_string(),
2140-
"--allow-local-fallback".to_string(),
2137+
"--placement".to_string(),
2138+
"lab-or-local".to_string(),
21412139
"--placement=lab".to_string(),
21422140
"--detach-after-handoff".to_string(),
21432141
];

0 commit comments

Comments
 (0)