feat(whaleflow): add typed workflow foundation#2810
Conversation
Harvested from PR #2482 by @AdityaVG13, preserving the typed WhaleFlow config and deterministic planner direction without exposing the runtime workflow_run tool yet. Co-authored-by: AdityaVG13 <44177453+AdityaVG13@users.noreply.github.com>
There was a problem hiding this comment.
Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Code Review
This pull request introduces the codewhale-whaleflow crate, which provides typed workflow configuration, IR validation, and deterministic phase ordering tests. It also integrates this new crate into the workspace configuration, changelogs, release runbooks, and scripts. The review feedback identifies two issues in the scope overlap detection logic: scope_overlaps does not correctly handle root-level wildcards or partial glob prefix matches, and normalize_scope fails to normalize ./ to . (resulting in an empty string), which prevents it from being recognized as a root-level scope. Both issues can be resolved using the provided code suggestions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| fn scope_overlaps(left: &str, right: &str) -> bool { | ||
| let left = normalize_scope(left); | ||
| let right = normalize_scope(right); | ||
|
|
||
| if left == right || left == "." || right == "." { | ||
| return true; | ||
| } | ||
|
|
||
| if left.contains('*') || right.contains('*') { | ||
| return glob_prefix(&left) == glob_prefix(&right); | ||
| } | ||
|
|
||
| let left_path = Path::new(&left); | ||
| let right_path = Path::new(&right); | ||
| left_path.starts_with(right_path) || right_path.starts_with(left_path) | ||
| } |
There was a problem hiding this comment.
The current implementation of scope_overlaps does not correctly detect overlaps when wildcards like * or ** are used at the root level, or when glob patterns have different prefixes but still overlap (e.g., src/*/login.rs and src/auth/login.rs).
Specifically:
- If
leftis*or**,glob_prefixreturns"". Since"" != "src/auth/login.rs", it returnsfalseeven though*matches everything. - If
leftissrc/auth*andrightissrc/auth/login.rs, they are not detected as overlapping because their glob prefixes are not exactly equal.
We can make this more robust by checking if one glob prefix is a path prefix of the other, and treating empty glob prefixes (which represent root-level wildcards) as overlapping with everything.
fn scope_overlaps(left: &str, right: &str) -> bool {
let left = normalize_scope(left);
let right = normalize_scope(right);
if left == right || left == "." || right == "." || left == "*" || right == "*" || left == "**" || right == "**" {
return true;
}
if left.contains('*') || right.contains('*') {
let left_prefix = glob_prefix(&left);
let right_prefix = glob_prefix(&right);
if left_prefix.is_empty() || right_prefix.is_empty() {
return true;
}
let left_path = Path::new(&left_prefix);
let right_path = Path::new(&right_prefix);
return left_path.starts_with(right_path) || right_path.starts_with(left_path);
}
let left_path = Path::new(&left);
let right_path = Path::new(&right);
left_path.starts_with(right_path) || right_path.starts_with(left_path)
}| fn normalize_scope(scope: &str) -> String { | ||
| let trimmed = scope.trim().trim_start_matches("./").trim_end_matches('/'); | ||
| trimmed | ||
| .strip_suffix("/**") | ||
| .or_else(|| trimmed.strip_suffix("/*")) | ||
| .unwrap_or(trimmed) | ||
| .to_string() | ||
| } |
There was a problem hiding this comment.
When scope is ./, trim_start_matches("./") removes the prefix and leaves an empty string "". This empty string is not caught by the left == "." check in scope_overlaps, meaning ./ is not treated as the root directory and fails to trigger overlap detection with other paths.
Normalizing empty or ./ scopes to . ensures they are correctly handled as root-level scopes.
fn normalize_scope(scope: &str) -> String {
let trimmed = scope.trim().trim_start_matches("./").trim_end_matches('/');
if trimmed.is_empty() || trimmed == "." {
".".to_string()
} else {
trimmed
.strip_suffix("/**")
.or_else(|| trimmed.strip_suffix("/*"))
.unwrap_or(trimmed)
.to_string()
}
}
Summary
codewhale-whaleflowcrate with typedWorkflowConfig,Phase,Task,FailurePolicy,TaskMode,AgentType, andWorkflowPlanvalidation/planningworkflow_run, no worktree executor, and no parallel sub-agent fan-out yetStewardship / credit
Harvested from #2482 by @AdityaVG13, preserving the WhaleFlow typed-config and deterministic-planner direction while avoiding the draft PR's unsafe runtime execution surface. #2486 stays open for the later cost/token accounting slice.
This PR intentionally does not expose
workflow_runyet because cancellation, replay/evidence, worktree diff capture, and runtime cost telemetry need separate verified slices before the model can launch write-capable workflows.Verification
cargo fmt --all -- --checkcargo test -p codewhale-whaleflow --lockedcargo clippy -p codewhale-whaleflow --all-targets --locked -- -D warnings./scripts/release/check-versions.sh./scripts/release/check-ohos-deps.shgit diff --checkcargo check --workspace --all-targets --lockedcargo publish --dry-run --locked --allow-dirty -p codewhale-whaleflowNo release, tag, publish, or runtime workflow tool exposure.