merge: workspace local manifest override
This commit is contained in:
commit
9be37ea123
|
|
@ -17,7 +17,8 @@ pub use paths::user_profiles_path;
|
||||||
pub use profile::{
|
pub use profile::{
|
||||||
ProfileDiscovery, ProfileError, ProfileManifestSnapshot, ProfileMetadata, ProfileRegistry,
|
ProfileDiscovery, ProfileError, ProfileManifestSnapshot, ProfileMetadata, ProfileRegistry,
|
||||||
ProfileRegistryEntry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver,
|
ProfileRegistryEntry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver,
|
||||||
ProfileSelector, ProfileSource, ResolvedProfile, resolve_profile_artifact,
|
ProfileSelector, ProfileSource, ResolvedProfile, WorkspaceOverrideSnapshot,
|
||||||
|
resolve_profile_artifact,
|
||||||
};
|
};
|
||||||
pub use protocol::{Permission, ScopeRule};
|
pub use protocol::{Permission, ScopeRule};
|
||||||
pub use scope::{Scope, ScopeError, SharedScope};
|
pub use scope::{Scope, ScopeError, SharedScope};
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ const BUILTIN_DEFAULT_PROFILE_NAME: &str = "default";
|
||||||
const BUILTIN_DEFAULT_PROFILE: &str = include_str!("../../../resources/profiles/default.lua");
|
const BUILTIN_DEFAULT_PROFILE: &str = include_str!("../../../resources/profiles/default.lua");
|
||||||
const BUILTIN_MODEL_CATALOG: &str = include_str!("../../../resources/models/builtin.toml");
|
const BUILTIN_MODEL_CATALOG: &str = include_str!("../../../resources/models/builtin.toml");
|
||||||
const DEFAULT_POD_NAME: &str = "yoi";
|
const DEFAULT_POD_NAME: &str = "yoi";
|
||||||
|
const WORKSPACE_OVERRIDE_LOCAL_FILENAME: &str = "override.local.toml";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
|
@ -340,6 +341,19 @@ pub struct ProfileManifestSnapshot {
|
||||||
pub source: ProfileSource,
|
pub source: ProfileSource,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub profile: Option<ProfileMetadata>,
|
pub profile: Option<ProfileMetadata>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub workspace_override: Option<WorkspaceOverrideSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct WorkspaceOverrideSnapshot {
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct WorkspaceOverrideLayer {
|
||||||
|
path: PathBuf,
|
||||||
|
config: PodManifestConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -475,6 +489,7 @@ impl ProfileResolver {
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or_else(|| Path::new(".")),
|
.unwrap_or_else(|| Path::new(".")),
|
||||||
)?;
|
)?;
|
||||||
|
let workspace_override = load_workspace_override_from(&workspace_base)?;
|
||||||
let lua_value = evaluate_lua_profile(&absolute_path, &profile_dir)?;
|
let lua_value = evaluate_lua_profile(&absolute_path, &profile_dir)?;
|
||||||
let raw_artifact = lua_value.clone();
|
let raw_artifact = lua_value.clone();
|
||||||
resolve_lua_profile_value(
|
resolve_lua_profile_value(
|
||||||
|
|
@ -484,6 +499,7 @@ impl ProfileResolver {
|
||||||
options,
|
options,
|
||||||
lua_value,
|
lua_value,
|
||||||
raw_artifact,
|
raw_artifact,
|
||||||
|
workspace_override,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -499,6 +515,7 @@ impl ProfileResolver {
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or_else(|| Path::new(".")),
|
.unwrap_or_else(|| Path::new(".")),
|
||||||
)?;
|
)?;
|
||||||
|
let workspace_override = load_workspace_override_from(&workspace_base)?;
|
||||||
let lua_value = evaluate_embedded_lua_profile(label, content)?;
|
let lua_value = evaluate_embedded_lua_profile(label, content)?;
|
||||||
let raw_artifact = lua_value.clone();
|
let raw_artifact = lua_value.clone();
|
||||||
resolve_lua_profile_value(
|
resolve_lua_profile_value(
|
||||||
|
|
@ -508,6 +525,7 @@ impl ProfileResolver {
|
||||||
options,
|
options,
|
||||||
lua_value,
|
lua_value,
|
||||||
raw_artifact,
|
raw_artifact,
|
||||||
|
workspace_override,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -519,6 +537,7 @@ fn resolve_lua_profile_value(
|
||||||
options: ProfileResolveOptions,
|
options: ProfileResolveOptions,
|
||||||
value: serde_json::Value,
|
value: serde_json::Value,
|
||||||
raw_artifact: serde_json::Value,
|
raw_artifact: serde_json::Value,
|
||||||
|
workspace_override: Option<WorkspaceOverrideLayer>,
|
||||||
) -> Result<ResolvedProfile, ProfileError> {
|
) -> Result<ResolvedProfile, ProfileError> {
|
||||||
if !workspace_base.is_absolute() {
|
if !workspace_base.is_absolute() {
|
||||||
return Err(ProfileError::InvalidPath {
|
return Err(ProfileError::InvalidPath {
|
||||||
|
|
@ -554,11 +573,28 @@ fn resolve_lua_profile_value(
|
||||||
memory: profile.memory,
|
memory: profile.memory,
|
||||||
skills: profile.skills,
|
skills: profile.skills,
|
||||||
};
|
};
|
||||||
let config = PodManifestConfig::builtin_defaults().merge(config.resolve_paths(profile_dir));
|
let mut config = PodManifestConfig::builtin_defaults().merge(config.resolve_paths(profile_dir));
|
||||||
|
let workspace_override_snapshot = if let Some(override_layer) = workspace_override {
|
||||||
|
let override_base =
|
||||||
|
override_layer
|
||||||
|
.path
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| ProfileError::InvalidPath {
|
||||||
|
path: override_layer.path.clone(),
|
||||||
|
message: "workspace override path has no parent directory".into(),
|
||||||
|
})?;
|
||||||
|
config = config.merge(override_layer.config.resolve_paths(override_base));
|
||||||
|
Some(WorkspaceOverrideSnapshot {
|
||||||
|
path: override_layer.path,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let mut manifest = PodManifest::try_from(config).map_err(ProfileError::ManifestResolve)?;
|
let mut manifest = PodManifest::try_from(config).map_err(ProfileError::ManifestResolve)?;
|
||||||
manifest.profile = Some(ProfileManifestSnapshot {
|
manifest.profile = Some(ProfileManifestSnapshot {
|
||||||
source: source.clone(),
|
source: source.clone(),
|
||||||
profile: profile_meta.clone(),
|
profile: profile_meta.clone(),
|
||||||
|
workspace_override: workspace_override_snapshot,
|
||||||
});
|
});
|
||||||
let manifest_snapshot =
|
let manifest_snapshot =
|
||||||
serde_json::to_value(&manifest).map_err(ProfileError::SnapshotSerialize)?;
|
serde_json::to_value(&manifest).map_err(ProfileError::SnapshotSerialize)?;
|
||||||
|
|
@ -687,6 +723,54 @@ fn load_profile_registry_file(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_workspace_override_from(
|
||||||
|
workspace_base: &Path,
|
||||||
|
) -> Result<Option<WorkspaceOverrideLayer>, ProfileError> {
|
||||||
|
find_workspace_override_from(workspace_base)
|
||||||
|
.map(|path| load_workspace_override_file(&path))
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_workspace_override_file(path: &Path) -> Result<WorkspaceOverrideLayer, ProfileError> {
|
||||||
|
let content =
|
||||||
|
std::fs::read_to_string(path).map_err(|source| ProfileError::WorkspaceOverrideRead {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
let config = PodManifestConfig::from_toml(&content).map_err(|source| {
|
||||||
|
ProfileError::WorkspaceOverrideParse {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if config.pod.name.is_some() {
|
||||||
|
return Err(ProfileError::InvalidWorkspaceOverride {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
message: "workspace-local manifest overrides cannot set pod.name; Pod identity is a runtime input".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(WorkspaceOverrideLayer {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_workspace_override_from(start: &Path) -> Option<PathBuf> {
|
||||||
|
let start = start
|
||||||
|
.canonicalize()
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_else(|| start.to_path_buf());
|
||||||
|
let mut cur: Option<&Path> = Some(start.as_path());
|
||||||
|
while let Some(dir) = cur {
|
||||||
|
let candidate = dir.join(".yoi").join(WORKSPACE_OVERRIDE_LOCAL_FILENAME);
|
||||||
|
if candidate.is_file() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
cur = dir.parent();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn find_project_profiles_from(start: &Path) -> Option<PathBuf> {
|
fn find_project_profiles_from(start: &Path) -> Option<PathBuf> {
|
||||||
let start = start
|
let start = start
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
|
|
@ -1214,6 +1298,7 @@ pub fn resolve_profile_artifact(
|
||||||
ProfileResolveOptions::default(),
|
ProfileResolveOptions::default(),
|
||||||
raw_artifact.clone(),
|
raw_artifact.clone(),
|
||||||
raw_artifact,
|
raw_artifact,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1241,6 +1326,20 @@ pub enum ProfileError {
|
||||||
#[source]
|
#[source]
|
||||||
source: toml::de::Error,
|
source: toml::de::Error,
|
||||||
},
|
},
|
||||||
|
#[error("failed to read workspace local manifest override {}: {source}", .path.display())]
|
||||||
|
WorkspaceOverrideRead {
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
#[error("failed to parse workspace local manifest override {}: {source}", .path.display())]
|
||||||
|
WorkspaceOverrideParse {
|
||||||
|
path: PathBuf,
|
||||||
|
#[source]
|
||||||
|
source: toml::de::Error,
|
||||||
|
},
|
||||||
|
#[error("invalid workspace local manifest override {}: {message}", .path.display())]
|
||||||
|
InvalidWorkspaceOverride { path: PathBuf, message: String },
|
||||||
#[error("no default profile is configured")]
|
#[error("no default profile is configured")]
|
||||||
NoDefaultProfile,
|
NoDefaultProfile,
|
||||||
#[error("profile not found: {selector}")]
|
#[error("profile not found: {selector}")]
|
||||||
|
|
@ -1498,6 +1597,125 @@ return profile {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn workspace_local_override_layers_over_profile_defaults() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let workspace = tmp.path().join("project");
|
||||||
|
let nested = workspace.join("nested");
|
||||||
|
let yoi_dir = workspace.join(".yoi");
|
||||||
|
std::fs::create_dir_all(&nested).unwrap();
|
||||||
|
std::fs::create_dir_all(&yoi_dir).unwrap();
|
||||||
|
let override_path = yoi_dir.join(WORKSPACE_OVERRIDE_LOCAL_FILENAME);
|
||||||
|
std::fs::write(
|
||||||
|
&override_path,
|
||||||
|
r#"
|
||||||
|
[pod]
|
||||||
|
prompt_pack = "prompts.toml"
|
||||||
|
[worker]
|
||||||
|
language = "ja"
|
||||||
|
[session]
|
||||||
|
record_event_trace = false
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let resolved = ProfileResolver::new()
|
||||||
|
.with_workspace_base(&nested)
|
||||||
|
.resolve(&ProfileSelector::Default, ProfileResolveOptions::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolved.manifest.pod.name, "yoi");
|
||||||
|
assert_eq!(resolved.manifest.worker.language, "ja");
|
||||||
|
assert!(!resolved.manifest.session.record_event_trace);
|
||||||
|
assert_eq!(
|
||||||
|
resolved.manifest.pod.prompt_pack.as_deref(),
|
||||||
|
Some(yoi_dir.join("prompts.toml").as_path())
|
||||||
|
);
|
||||||
|
assert_eq!(resolved.manifest.scope.allow[0].target, nested);
|
||||||
|
assert_eq!(
|
||||||
|
resolved
|
||||||
|
.manifest
|
||||||
|
.profile
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|snapshot| snapshot.workspace_override.as_ref())
|
||||||
|
.map(|snapshot| snapshot.path.as_path()),
|
||||||
|
Some(override_path.as_path())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_local_override_uses_nearest_ancestor() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let workspace = tmp.path().join("project");
|
||||||
|
let nested = workspace.join("nested");
|
||||||
|
let child = nested.join("child");
|
||||||
|
let parent_yoi = workspace.join(".yoi");
|
||||||
|
let nested_yoi = nested.join(".yoi");
|
||||||
|
std::fs::create_dir_all(&child).unwrap();
|
||||||
|
std::fs::create_dir_all(&parent_yoi).unwrap();
|
||||||
|
std::fs::create_dir_all(&nested_yoi).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
parent_yoi.join(WORKSPACE_OVERRIDE_LOCAL_FILENAME),
|
||||||
|
r#"
|
||||||
|
[pod]
|
||||||
|
prompt_pack = "parent-prompts.toml"
|
||||||
|
[worker]
|
||||||
|
language = "parent"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let nested_override_path = nested_yoi.join(WORKSPACE_OVERRIDE_LOCAL_FILENAME);
|
||||||
|
std::fs::write(
|
||||||
|
&nested_override_path,
|
||||||
|
r#"
|
||||||
|
[pod]
|
||||||
|
prompt_pack = "nested-prompts.toml"
|
||||||
|
[worker]
|
||||||
|
language = "nested"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let resolved = ProfileResolver::new()
|
||||||
|
.with_workspace_base(&child)
|
||||||
|
.resolve(&ProfileSelector::Default, ProfileResolveOptions::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolved.manifest.worker.language, "nested");
|
||||||
|
assert_eq!(
|
||||||
|
resolved.manifest.pod.prompt_pack.as_deref(),
|
||||||
|
Some(nested_yoi.join("nested-prompts.toml").as_path())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolved
|
||||||
|
.manifest
|
||||||
|
.profile
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|snapshot| snapshot.workspace_override.as_ref())
|
||||||
|
.map(|snapshot| snapshot.path.as_path()),
|
||||||
|
Some(nested_override_path.as_path())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_local_override_rejects_runtime_pod_name() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let yoi_dir = tmp.path().join(".yoi");
|
||||||
|
std::fs::create_dir_all(&yoi_dir).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
yoi_dir.join(WORKSPACE_OVERRIDE_LOCAL_FILENAME),
|
||||||
|
"[pod]\nname = \"not-local\"\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = ProfileResolver::new()
|
||||||
|
.with_workspace_base(tmp.path())
|
||||||
|
.resolve(&ProfileSelector::Default, ProfileResolveOptions::default())
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, ProfileError::InvalidWorkspaceOverride { .. }));
|
||||||
|
assert!(err.to_string().contains("pod.name"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_profile_extension_has_clear_diagnostic() {
|
fn unsupported_profile_extension_has_clear_diagnostic() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -485,6 +485,51 @@ permission = "write"
|
||||||
assert!(loader.workspace_dir().is_none());
|
assert!(loader.workspace_dir().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn manifest_mode_does_not_apply_workspace_local_override() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let yoi_dir = tmp.path().join(".yoi");
|
||||||
|
std::fs::create_dir_all(&yoi_dir).unwrap();
|
||||||
|
write(
|
||||||
|
&yoi_dir.join("override.local.toml"),
|
||||||
|
r#"
|
||||||
|
[pod]
|
||||||
|
name = "from-local-override"
|
||||||
|
[worker]
|
||||||
|
language = "override"
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let manifest_path = tmp.path().join("manifest.toml");
|
||||||
|
write(
|
||||||
|
&manifest_path,
|
||||||
|
&format!(
|
||||||
|
r#"
|
||||||
|
[pod]
|
||||||
|
name = "from-single-file"
|
||||||
|
|
||||||
|
[model]
|
||||||
|
scheme = "anthropic"
|
||||||
|
model_id = "test-model"
|
||||||
|
|
||||||
|
[worker]
|
||||||
|
language = "manifest"
|
||||||
|
|
||||||
|
[[scope.allow]]
|
||||||
|
target = "{}"
|
||||||
|
permission = "write"
|
||||||
|
"#,
|
||||||
|
tmp.path().display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cli = Cli::try_parse_from(["yoi pod", "--manifest", manifest_path.to_str().unwrap()])
|
||||||
|
.unwrap();
|
||||||
|
let (manifest, _loader) = resolve_manifest(&cli).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(manifest.pod.name, "from-single-file");
|
||||||
|
assert_eq!(manifest.worker.language, "manifest");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn profile_uses_selected_profile() {
|
fn profile_uses_selected_profile() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ Source/partial layers may omit fields. Resolved manifests should be explicit eno
|
||||||
|
|
||||||
`--manifest <path>` exists as an explicit low-level escape hatch. Normal fresh startup should select a Profile through `profiles.toml` / builtin defaults rather than ambient manifest cascades.
|
`--manifest <path>` exists as an explicit low-level escape hatch. Normal fresh startup should select a Profile through `profiles.toml` / builtin defaults rather than ambient manifest cascades.
|
||||||
|
|
||||||
|
For normal Profile/default startup, a workspace may add `.yoi/override.local.toml` as a final local manifest layer. Yoi discovers the nearest ancestor `.yoi/override.local.toml` from the workspace base used for profile resolution, resolves relative paths in that file against its containing `.yoi` directory, and applies it after the selected Profile and builtin defaults. This file is intended for machine-local choices such as provider/model, worker language, prompt pack, and permission policy tweaks; it is ignored by git via the repository `*.local.*` rule. It is not applied in explicit `--manifest <path>` mode, and it cannot set `pod.name` because Pod identity remains a runtime input.
|
||||||
|
|
||||||
## Spawned Pods
|
## Spawned Pods
|
||||||
|
|
||||||
`SpawnPod.profile` is optional and resolves through defaults when omitted. The only concrete capability delegation in the tool call is `SpawnPod.scope`, and it must be a subset of the parent's effective scope.
|
`SpawnPod.profile` is optional and resolves through defaults when omitted. The only concrete capability delegation in the tool call is `SpawnPod.scope`, and it must be a subset of the parent's effective scope.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Implementation report
|
||||||
|
|
||||||
|
Implemented workspace-local manifest override support.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- Normal Profile/default resolution now searches upward from the resolver workspace base for the nearest `.yoi/override.local.toml`.
|
||||||
|
- The override is parsed as a `PodManifestConfig` layer, resolved relative to its parent `.yoi/` directory, and merged after builtin defaults plus the selected Profile but before final `PodManifest` validation and snapshot serialization.
|
||||||
|
- Resolved profile provenance now records the applied override path in `manifest.profile.workspace_override`.
|
||||||
|
- Explicit `--manifest <path>` mode remains a single-file escape hatch and does not apply workspace-local overrides.
|
||||||
|
- Workspace-local overrides are rejected if they set `pod.name`, keeping Pod identity runtime-bound.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `cargo test -p manifest workspace_local_override -- --nocapture`
|
||||||
|
- `cargo test -p pod manifest_mode_does_not_apply_workspace_local_override -- --nocapture`
|
||||||
|
- `cargo test -p manifest -p pod`
|
||||||
|
- `./tickets.sh doctor`
|
||||||
|
- `git diff --check`
|
||||||
|
- `nix build .#yoi`
|
||||||
|
|
||||||
|
Reviewer follow-up validation:
|
||||||
|
|
||||||
|
- `cargo test -p manifest workspace_local_override -- --nocapture`
|
||||||
|
- `./tickets.sh doctor`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
All completed successfully.
|
||||||
|
|
@ -7,7 +7,7 @@ kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
labels: [profile, manifest, config]
|
labels: [profile, manifest, config]
|
||||||
created_at: 2026-06-01T12:52:40Z
|
created_at: 2026-06-01T12:52:40Z
|
||||||
updated_at: 2026-06-01T12:53:58Z
|
updated_at: 2026-06-01T13:05:58Z
|
||||||
assignee: null
|
assignee: null
|
||||||
legacy_ticket: null
|
legacy_ticket: null
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -50,4 +50,34 @@ Validation:
|
||||||
- Record any skipped validation with rationale.
|
- Record any skipped validation with rationale.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: hare at: 2026-06-01T13:05:58Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
# Implementation report
|
||||||
|
|
||||||
|
Implemented workspace-local manifest override support.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- Normal Profile/default resolution now searches upward from the resolver workspace base for the nearest `.yoi/override.local.toml`.
|
||||||
|
- The override is parsed as a `PodManifestConfig` layer, resolved relative to its parent `.yoi/` directory, and merged after builtin defaults plus the selected Profile but before final `PodManifest` validation and snapshot serialization.
|
||||||
|
- Resolved profile provenance now records the applied override path in `manifest.profile.workspace_override`.
|
||||||
|
- Explicit `--manifest <path>` mode remains a single-file escape hatch and does not apply workspace-local overrides.
|
||||||
|
- Workspace-local overrides are rejected if they set `pod.name`, keeping Pod identity runtime-bound.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `cargo test -p manifest workspace_local_override -- --nocapture`
|
||||||
|
- `cargo test -p pod manifest_mode_does_not_apply_workspace_local_override -- --nocapture`
|
||||||
|
- `cargo test -p manifest -p pod`
|
||||||
|
- `./tickets.sh doctor`
|
||||||
|
- `git diff --check`
|
||||||
|
- `nix build .#yoi`
|
||||||
|
|
||||||
|
All completed successfully.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user