Skip to content

feat(whaleflow): add typed workflow foundation#2810

Merged
Hmbown merged 1 commit into
codex/v0.9.0-stewardshipfrom
codex/v090-whaleflow-foundation
Jun 6, 2026
Merged

feat(whaleflow): add typed workflow foundation#2810
Hmbown merged 1 commit into
codex/v0.9.0-stewardshipfrom
codex/v090-whaleflow-foundation

Conversation

@Hmbown
Copy link
Copy Markdown
Owner

@Hmbown Hmbown commented Jun 6, 2026

Summary

  • add the first pure codewhale-whaleflow crate with typed WorkflowConfig, Phase, Task, FailurePolicy, TaskMode, AgentType, and WorkflowPlan validation/planning
  • keep this slice behind the Rust-owned IR boundary: no TUI registry wiring, no model-visible workflow_run, no worktree executor, and no parallel sub-agent fan-out yet
  • add deterministic validation coverage for phase ordering, duplicate ids, cycles, task-result dependency availability, parallel write scopes, path-component scope overlap, max concurrency, and snake_case enum JSON
  • wire the crate into workspace metadata, lockfile, release publish order, and changelogs

Stewardship / 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_run yet 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 -- --check
  • cargo test -p codewhale-whaleflow --locked
  • cargo clippy -p codewhale-whaleflow --all-targets --locked -- -D warnings
  • ./scripts/release/check-versions.sh
  • ./scripts/release/check-ohos-deps.sh
  • git diff --check
  • cargo check --workspace --all-targets --locked
  • cargo publish --dry-run --locked --allow-dirty -p codewhale-whaleflow

No release, tag, publish, or runtime workflow tool exposure.

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>
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmbown has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Hmbown Hmbown merged commit ef239df into codex/v0.9.0-stewardship Jun 6, 2026
2 checks passed
@Hmbown Hmbown deleted the codex/v090-whaleflow-foundation branch June 6, 2026 02:08
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +393 to +408
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)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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:

  1. If left is * or **, glob_prefix returns "". Since "" != "src/auth/login.rs", it returns false even though * matches everything.
  2. If left is src/auth* and right is src/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)
}

Comment on lines +410 to +417
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()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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()
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant