mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 10:01:30 -10:00
fix(fleet): search <workspace>/.codewhale/fleets as the workspace origin
The Fleet store saves workspace-scoped Fleets to <workspace>/.codewhale/fleets, but the Workflow loader's `workspace` origin was the workspace root, so it read <workspace>/fleets and never saw them. `workspace` now means <workspace>/.codewhale; the root stays a second origin, `workspace_root`, so checked-in fleets/<name>.toml rosters keep loading. An exact Fleet present in both is ambiguous and can be qualified by either origin. Updates the workflow tool's `fleet` description and FLEET.md (saved roster locations and origin qualification). Checks: in the shared tree, cargo test -p codewhale-tui --lib -- tool_name_ fleet::exact:: hooks::executor: 161 passed, 0 failed, including workspace_fleets_load_from_dot_codewhale_and_the_legacy_root and a_store_saved_workspace_fleet_is_found_by_load_fleet_document. This commit's HEAD-based variant (2-arg load_fleet_document, no peer v2 bridge) was not compiled separately. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01TvKGZz4LrKfrQiAXip48JS
This commit is contained in:
1 parent
7e5793a29e
commit
26cfaf8de4
3 files changed
+101
-6
No files matched your search
@@ -68,13 +68,25 @@ pub(crate) fn personal_fleet_definitions_dir() -> anyhow::Result<std::path::Path
|
||||
Ok(personal_fleet_root()?.join("fleets"))
|
||||
}
|
||||
|
||||
/// The workspace has two origins. `workspace` is `<workspace>/.codewhale`, the
|
||||
/// directory the Fleet store saves workspace-scoped Fleets to, so a Fleet
|
||||
/// saved from the Fleet UI is found by name. `workspace_root` is the workspace
|
||||
/// directory itself, which keeps checked-in `fleets/<name>.toml` rosters
|
||||
/// loading as they always have.
|
||||
#[must_use]
|
||||
pub(crate) fn fleet_search_roots(workspace: &std::path::Path) -> Vec<FleetSearchRoot> {
|
||||
let mut roots = Vec::new();
|
||||
if let Ok(home) = personal_fleet_root() {
|
||||
roots.push(FleetSearchRoot::new("codewhale_home", home));
|
||||
}
|
||||
roots.push(FleetSearchRoot::new("workspace", workspace.to_path_buf()));
|
||||
roots.push(FleetSearchRoot::new(
|
||||
"workspace",
|
||||
workspace.join(".codewhale"),
|
||||
));
|
||||
roots.push(FleetSearchRoot::new(
|
||||
"workspace_root",
|
||||
workspace.to_path_buf(),
|
||||
));
|
||||
roots
|
||||
}
|
||||
|
||||
@@ -2812,4 +2824,82 @@ permissions = "read_only"
|
||||
assert!(line.contains("(role auditor)"), "{line}");
|
||||
assert!(line.contains("posture=explore"), "{line}");
|
||||
}
|
||||
|
||||
// ── Search roots: where a workspace Fleet lives ────────────────────────
|
||||
|
||||
/// The Fleet store saves workspace Fleets under `<workspace>/.codewhale`,
|
||||
/// so that is the primary `workspace` origin; the workspace root stays a
|
||||
/// second origin for checked-in `fleets/<name>.toml` rosters.
|
||||
#[test]
|
||||
fn workspace_fleets_load_from_dot_codewhale_and_the_legacy_root() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let home = tempfile::tempdir().expect("home");
|
||||
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
|
||||
let ws = tempfile::tempdir().expect("workspace");
|
||||
|
||||
let saved = ws.path().join(".codewhale").join("fleets");
|
||||
std::fs::create_dir_all(&saved).expect("saved fleets dir");
|
||||
std::fs::write(saved.join("glm-pair.toml"), GLM_FLEET).expect("write saved");
|
||||
let (document, id) = load_fleet_document("glm-pair", ws.path()).expect("saved fleet loads");
|
||||
assert_eq!(document.name(), "glm-pair");
|
||||
assert_eq!(id.origin, "workspace");
|
||||
|
||||
let checked_in = ws.path().join("fleets");
|
||||
std::fs::create_dir_all(&checked_in).expect("checked-in fleets dir");
|
||||
std::fs::write(
|
||||
checked_in.join("stopship.toml"),
|
||||
"name = \"stopship\"\n\n[roles]\nscout = \"scout\"\n",
|
||||
)
|
||||
.expect("write checked-in");
|
||||
let (document, id) =
|
||||
load_fleet_document("stopship", ws.path()).expect("checked-in fleet still loads");
|
||||
assert_eq!(document.name(), "stopship");
|
||||
assert_eq!(id.origin, "workspace_root");
|
||||
|
||||
// An exact Fleet in both workspace origins is ambiguous, and each
|
||||
// origin can be named explicitly.
|
||||
std::fs::write(checked_in.join("glm-pair.toml"), GLM_FLEET).expect("write twin");
|
||||
assert!(matches!(
|
||||
load_fleet_document("glm-pair", ws.path()),
|
||||
Err(NamedFleetError::AmbiguousFleet { .. })
|
||||
));
|
||||
let (_, id) = load_fleet_document("workspace_root/glm-pair", ws.path()).expect("qualified");
|
||||
assert_eq!(id.origin, "workspace_root");
|
||||
}
|
||||
|
||||
/// A Fleet saved through the store at workspace scope is found by the
|
||||
/// Workflow loader instead of being reported missing. Today the store's
|
||||
/// `schema = "fleet"` revision-2 document is not a schema the Workflow
|
||||
/// loader parses, so the load names that exact file and its schema; if a
|
||||
/// v2 bridge lands, the same call succeeds from the `workspace` origin.
|
||||
#[test]
|
||||
fn a_store_saved_workspace_fleet_is_found_by_load_fleet_document() {
|
||||
use crate::fleet::store::{FleetFile, FleetScope, save_fleet};
|
||||
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let home = tempfile::tempdir().expect("home");
|
||||
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", home.path());
|
||||
let ws = tempfile::tempdir().expect("workspace");
|
||||
|
||||
let fleet = FleetFile::new("Folder Pair".to_string(), None).expect("fleet");
|
||||
let path = save_fleet(&fleet, FleetScope::Workspace, ws.path()).expect("save");
|
||||
|
||||
match load_fleet_document(&fleet.file_slug(), ws.path()) {
|
||||
// A v2 bridge may label the store scope `folder` rather than the
|
||||
// `workspace` search-root origin; either names this workspace.
|
||||
Ok((_, id)) => assert!(
|
||||
matches!(id.origin.as_str(), "workspace" | "folder"),
|
||||
"{}",
|
||||
id.origin
|
||||
),
|
||||
Err(err) => {
|
||||
assert!(
|
||||
!matches!(err, NamedFleetError::NotFound(_)),
|
||||
"the saved Fleet must be found, got {err}"
|
||||
);
|
||||
let message = err.to_string();
|
||||
assert!(message.contains(&path.display().to_string()), "{message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1106,7 +1106,7 @@ impl ToolSpec for WorkflowTool {
|
||||
},
|
||||
"fleet": {
|
||||
"type": "string",
|
||||
"description": "Named Fleet from $CODEWHALE_HOME/fleets/ or workspace fleets/; qualified origin/name accepted. Exact Fleets freeze member identity, route, and reasoning. Runtime derives authority from role and live parent; per-task route/authority overrides are rejected."
|
||||
"description": "Named Fleet from $CODEWHALE_HOME/fleets/, <workspace>/.codewhale/fleets/, or checked-in <workspace>/fleets/; qualified origin/name accepted (codewhale_home, workspace, workspace_root). Exact Fleets freeze member identity, route, and reasoning. Runtime derives authority from role and live parent; per-task route/authority overrides are rejected."
|
||||
},
|
||||
"plan": plan_schema::structured_plan_schema(),
|
||||
"args": {
|
||||
|
||||
+9
-4
@@ -26,7 +26,9 @@ existing workspaces, receipts, or scripts:
|
||||
|
||||
- the durable ledger `.codewhale/fleet.jsonl` and the log directories
|
||||
`.codewhale/fleet/` and `.codewhale/fleet-host/`;
|
||||
- saved rosters `fleets/<name>.toml` and their `schema = "fleet"` header;
|
||||
- saved rosters `fleets/<name>.toml` and their `schema = "fleet"` header, under
|
||||
`$CODEWHALE_HOME/` or the workspace's `.codewhale/` (checked-in rosters at the
|
||||
workspace root's `fleets/` are still read);
|
||||
- the `[fleet]` config table (inline `[fleets.*]` tables were removed in 0.9.14; named fleets live in `fleets/<name>.toml` files);
|
||||
- the `codewhale workflow run --fleet <name>` flag;
|
||||
- wire, receipt, and control-plane operation ids such as `fleet.status`.
|
||||
@@ -314,7 +316,9 @@ header/status signal; avoid repeating emoji-heavy rows for every worker.
|
||||
|
||||
A selected v2 fleet freezes each selected member's id, semantic role, provider,
|
||||
and model identity into the durable run before a Workflow starts. Save the
|
||||
fleet as `fleets/<name>.toml` in the workspace or under `$CODEWHALE_HOME`.
|
||||
fleet as `fleets/<name>.toml` under the workspace's `.codewhale/` (where the
|
||||
fleet editor saves folder fleets) or under `$CODEWHALE_HOME`; a checked-in
|
||||
`fleets/<name>.toml` at the workspace root is also read.
|
||||
Models cannot replace those identity or route assignments at runtime:
|
||||
|
||||
```toml
|
||||
@@ -364,8 +368,9 @@ Router call itself is capped at `off` or `low`; more expensive values are
|
||||
rejected. A manually selected worker reasoning tier makes no Router call. Route
|
||||
and reasoning receipts name the worker model and, when used, the Router's exact
|
||||
provider/model so the operator can see which model did which job. If the same
|
||||
bare Router or fleet name exists in both roots, qualify it as
|
||||
`workspace/<name>` or `codewhale_home/<name>` instead of relying on shadowing.
|
||||
bare Router or fleet name exists in more than one root, qualify it as
|
||||
`codewhale_home/<name>`, `workspace/<name>` (the workspace's `.codewhale/`), or
|
||||
`workspace_root/<name>` (the workspace root) instead of relying on shadowing.
|
||||
|
||||
Compatibility schemas may serialize `reasoning`, `permissions`, tool hints, or
|
||||
other execution settings beside a member. Those values are not fleet identity,
|
||||
|
||||
Reference in new issue
Block a user