diff --git a/.yoi/tickets/00001KTZY8HK2/item.md b/.yoi/tickets/00001KTZY8HK2/item.md
index 00011c9a..cb7f8d70 100644
--- a/.yoi/tickets/00001KTZY8HK2/item.md
+++ b/.yoi/tickets/00001KTZY8HK2/item.md
@@ -2,7 +2,7 @@
title: 'Profile extend API を廃止して import + Lua 代入に寄せる'
state: 'inprogress'
created_at: '2026-06-13T07:31:09Z'
-updated_at: '2026-06-14T06:10:45Z'
+updated_at: '2026-06-14T06:24:18Z'
assignee: null
readiness: 'implementation_ready'
risk_flags: ['profiles', 'lua-api', 'builtin-resources', 'migration']
diff --git a/.yoi/tickets/00001KTZY8HK2/thread.md b/.yoi/tickets/00001KTZY8HK2/thread.md
index 41d887fc..679ea554 100644
--- a/.yoi/tickets/00001KTZY8HK2/thread.md
+++ b/.yoi/tickets/00001KTZY8HK2/thread.md
@@ -82,4 +82,29 @@ Validation:
Ticket evidence, related records, orchestration plan, and clean workspace state were checked. No blockers remain; accept for implementation before worktree/spawn side effects.
+---
+
+
+
+## Implementation report
+
+Implemented removal/deprecation of yoi.profile.extend and migrated builtin role Profile resources to yoi.profile.import plus explicit Lua assignment.
+
+Changes:
+- yoi.profile.extend now fails with a removed-API diagnostic directing users to yoi.profile.import(...) plus assignment; the previous JSON deep-merge implementation was removed.
+- Builtin role Lua profiles (companion/intake/orchestrator/coder/reviewer) import builtin:default, then assign each overridden field explicitly. Worker instruction overrides preserve imported worker defaults by assigning p.worker.instruction.
+- Focused profile tests now cover explicit assignment, object replacement without hidden deep-merge retention, removed extend diagnostics, and role profile resolution/feature policy.
+
+Validation:
+- cargo fmt --check
+- git diff --check
+- cargo test -p manifest profile::tests:: -- --nocapture
+- cargo build -p yoi
+- nix build .#yoi (completed; dirty-tree warning expected before commit, result symlink removed)
+
+Risks/notes:
+- yoi.profile.extend remains present only as a failing diagnostic stub so callers receive an actionable migration error instead of a nil-call Lua error.
+- Scope/delegation authority semantics were not expanded; this change only preserves existing builtin profile scope/delegation declarations while changing composition style.
+
+
---
diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs
index 1322601d..98f4ff6f 100644
--- a/crates/manifest/src/profile.rs
+++ b/crates/manifest/src/profile.rs
@@ -1054,12 +1054,10 @@ fn profile_module(lua: &Lua) -> mlua::Result
{
)?;
module.set(
"extend",
- lua.create_function(|lua, (reference, overrides): (String, LuaValue)| {
- let base_value = import_profile_artifact(lua, &reference)?;
- let mut base_json: serde_json::Value = lua.from_value(base_value)?;
- let override_json: serde_json::Value = lua.from_value(overrides)?;
- deep_merge_profile_json(&mut base_json, override_json);
- lua.to_value(&base_json)
+ lua.create_function(|_, (_reference, _overrides): (String, LuaValue)| {
+ Err::(mlua::Error::RuntimeError(
+ "yoi.profile.extend has been removed; use yoi.profile.import(...) and explicit Lua assignment instead".to_string(),
+ ))
})?,
)?;
let meta = lua.create_table()?;
@@ -1084,23 +1082,6 @@ fn builtin_profile_by_ref(reference: &str) -> Option<&'static BuiltinProfile> {
.iter()
.find(|profile| profile.name == name || profile.label == reference)
}
-fn deep_merge_profile_json(base: &mut serde_json::Value, overrides: serde_json::Value) {
- match (base, overrides) {
- (serde_json::Value::Object(base), serde_json::Value::Object(overrides)) => {
- for (key, value) in overrides {
- match base.get_mut(&key) {
- Some(existing) => deep_merge_profile_json(existing, value),
- None => {
- base.insert(key, value);
- }
- }
- }
- }
- (base, override_value) => {
- *base = override_value;
- }
- }
-}
fn models_module(lua: &Lua) -> mlua::Result {
let t = lua.create_table()?;
t.set(
@@ -1610,9 +1591,9 @@ mod tests {
};
let companion = resolve("companion");
- assert!(!companion.feature.task.enabled);
- assert!(!companion.feature.pods.enabled);
- assert!(!companion.feature.ticket.enabled);
+ assert!(companion.feature.task.enabled);
+ assert!(companion.feature.pods.enabled);
+ assert!(companion.feature.ticket.enabled);
assert_eq!(companion.scope.allow[0].permission, Permission::Write);
assert_eq!(companion.scope.deny.len(), 1);
assert_eq!(companion.scope.deny[0].permission, Permission::Write);
@@ -1646,7 +1627,7 @@ mod tests {
);
let coder = resolve("coder");
- assert!(!coder.feature.task.enabled);
+ assert!(coder.feature.task.enabled);
assert!(!coder.feature.pods.enabled);
assert_eq!(coder.scope.allow[0].permission, Permission::Write);
assert_eq!(coder.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
@@ -1812,23 +1793,23 @@ return yoi.profile {
);
}
#[test]
- fn global_yoi_import_and_extend_builtin_profile() {
+ fn global_yoi_import_supports_explicit_lua_assignment() {
let tmp = TempDir::new().unwrap();
let profile = write_profile(
tmp.path(),
- "extended.lua",
+ "assigned.lua",
r#"
-local imported = yoi.profile.import("builtin:default")
-assert(imported.model.ref == "codex-oauth/gpt-5.5")
-return yoi.profile.extend("builtin:default", {
- slug = "extended",
- model = yoi.models.catalog("anthropic/claude-sonnet-4-6"),
- feature = {
- task = { enabled = false },
- pods = { enabled = true },
- },
- compaction = { kind = "tokens", threshold = 123, request_threshold = 456 },
-})
+local p = yoi.profile.import("builtin:default")
+assert(p.model.ref == "codex-oauth/gpt-5.5")
+p.slug = "assigned"
+p.model = yoi.models.catalog("anthropic/claude-sonnet-4-6")
+p.feature = {
+ task = { enabled = false },
+ pods = { enabled = true },
+}
+p.web = { enabled = false }
+p.compaction = yoi.compact.tokens { threshold = 123, request_threshold = 456 }
+return p
"#,
);
let resolved = ProfileResolver::new()
@@ -1844,25 +1825,27 @@ return yoi.profile.extend("builtin:default", {
);
assert!(!resolved.manifest.feature.task.enabled);
assert!(resolved.manifest.feature.pods.enabled);
+ assert_eq!(resolved.manifest.web.as_ref().unwrap().enabled, Some(false));
+ assert!(resolved.manifest.web.as_ref().unwrap().search.is_none());
assert_eq!(
resolved.manifest.compaction.as_ref().unwrap().threshold,
Some(123)
);
assert_eq!(
resolved.profile.as_ref().unwrap().name.as_deref(),
- Some("extended")
+ Some("assigned")
);
}
#[test]
- fn global_yoi_extend_keeps_profile_validation_boundary() {
+ fn global_yoi_extend_fails_with_removed_api_diagnostic() {
let tmp = TempDir::new().unwrap();
let profile = write_profile(
tmp.path(),
"bad.lua",
r#"
return yoi.profile.extend("builtin:default", {
- pod = { name = "not-runtime" },
+ slug = "bad",
})
"#,
);
@@ -1873,7 +1856,10 @@ return yoi.profile.extend("builtin:default", {
ProfileResolveOptions::with_pod_name("p"),
)
.unwrap_err();
- assert!(err.to_string().contains("field `pod`"));
+ let message = err.to_string();
+ assert!(message.contains("yoi.profile.extend"), "{message}");
+ assert!(message.contains("removed"), "{message}");
+ assert!(message.contains("yoi.profile.import"), "{message}");
}
#[test]
diff --git a/resources/profiles/coder.lua b/resources/profiles/coder.lua
index 68df0b7e..fe93ffb3 100644
--- a/resources/profiles/coder.lua
+++ b/resources/profiles/coder.lua
@@ -1,19 +1,16 @@
-return yoi.profile.extend("builtin:default", {
- slug = "coder",
- description = "Coder role profile with bundled reusable policy",
+local p = yoi.profile.import("builtin:default")
- scope = yoi.scope.workspace_write(),
+p.slug = "coder"
+p.description = "Coder role profile with bundled reusable policy"
+p.scope = yoi.scope.workspace_write()
+p.worker.instruction = "$yoi/role/coder"
+p.feature = {
+ task = { enabled = true },
+ memory = { enabled = true },
+ web = { enabled = true },
+ pods = { enabled = false },
+ ticket = { enabled = false, access = "lifecycle" },
+ ticket_orchestration = { enabled = false },
+}
- worker = {
- instruction = "$yoi/role/coder",
- },
-
- feature = {
- task = { enabled = true },
- memory = { enabled = true },
- web = { enabled = true },
- pods = { enabled = false },
- ticket = { enabled = false, access = "lifecycle" },
- ticket_orchestration = { enabled = false },
- },
-})
+return p
diff --git a/resources/profiles/companion.lua b/resources/profiles/companion.lua
index 51f28c76..dc9c0abf 100644
--- a/resources/profiles/companion.lua
+++ b/resources/profiles/companion.lua
@@ -1,17 +1,17 @@
-return yoi.profile.extend("builtin:default", {
- slug = "companion",
- description = "Companion role profile with bundled reusable policy",
+local p = yoi.profile.import("builtin:default")
- scope = yoi.scope.workspace_write({
- deny_write = { ".worktree" },
- }),
-
- feature = {
- task = { enabled = true },
- memory = { enabled = true },
- web = { enabled = true },
- pods = { enabled = true },
- ticket = { enabled = true, access = "lifecycle" },
- ticket_orchestration = { enabled = false },
- },
+p.slug = "companion"
+p.description = "Companion role profile with bundled reusable policy"
+p.scope = yoi.scope.workspace_write({
+ deny_write = { ".worktree" },
})
+p.feature = {
+ task = { enabled = true },
+ memory = { enabled = true },
+ web = { enabled = true },
+ pods = { enabled = true },
+ ticket = { enabled = true, access = "lifecycle" },
+ ticket_orchestration = { enabled = false },
+}
+
+return p
diff --git a/resources/profiles/intake.lua b/resources/profiles/intake.lua
index 08f9da70..087a7813 100644
--- a/resources/profiles/intake.lua
+++ b/resources/profiles/intake.lua
@@ -1,19 +1,16 @@
-return yoi.profile.extend("builtin:default", {
- slug = "intake",
- description = "Intake role profile with bundled reusable policy",
+local p = yoi.profile.import("builtin:default")
- scope = yoi.scope.workspace_read(),
+p.slug = "intake"
+p.description = "Intake role profile with bundled reusable policy"
+p.scope = yoi.scope.workspace_read()
+p.worker.instruction = "$yoi/role/intake"
+p.feature = {
+ task = { enabled = false },
+ memory = { enabled = true },
+ web = { enabled = true },
+ pods = { enabled = false },
+ ticket = { enabled = true, access = "lifecycle" },
+ ticket_orchestration = { enabled = false },
+}
- worker = {
- instruction = "$yoi/role/intake",
- },
-
- feature = {
- task = { enabled = false },
- memory = { enabled = true },
- web = { enabled = true },
- pods = { enabled = false },
- ticket = { enabled = true, access = "lifecycle" },
- ticket_orchestration = { enabled = false },
- },
-})
+return p
diff --git a/resources/profiles/orchestrator.lua b/resources/profiles/orchestrator.lua
index f02b20b2..71cd3f64 100644
--- a/resources/profiles/orchestrator.lua
+++ b/resources/profiles/orchestrator.lua
@@ -1,21 +1,17 @@
-return yoi.profile.extend("builtin:default", {
- slug = "orchestrator",
- description = "Orchestrator role profile with bundled reusable policy",
+local p = yoi.profile.import("builtin:default")
- scope = "workspace_read",
+p.slug = "orchestrator"
+p.description = "Orchestrator role profile with bundled reusable policy"
+p.scope = "workspace_read"
+p.worker.instruction = "$yoi/role/orchestrator"
+p.feature = {
+ task = { enabled = false },
+ memory = { enabled = true },
+ web = { enabled = true },
+ pods = { enabled = true },
+ ticket = { enabled = true, access = "lifecycle" },
+ ticket_orchestration = { enabled = true },
+}
+p.delegation_scope = "workspace_write"
- worker = {
- instruction = "$yoi/role/orchestrator",
- },
-
- feature = {
- task = { enabled = false },
- memory = { enabled = true },
- web = { enabled = true },
- pods = { enabled = true },
- ticket = { enabled = true, access = "lifecycle" },
- ticket_orchestration = { enabled = true },
- },
-
- delegation_scope = "workspace_write",
-})
+return p
diff --git a/resources/profiles/reviewer.lua b/resources/profiles/reviewer.lua
index 2ad476bc..2c16a6d3 100644
--- a/resources/profiles/reviewer.lua
+++ b/resources/profiles/reviewer.lua
@@ -1,19 +1,16 @@
-return yoi.profile.extend("builtin:default", {
- slug = "reviewer",
- description = "Reviewer role profile with bundled reusable policy",
+local p = yoi.profile.import("builtin:default")
- scope = yoi.scope.workspace_read(),
+p.slug = "reviewer"
+p.description = "Reviewer role profile with bundled reusable policy"
+p.scope = yoi.scope.workspace_read()
+p.worker.instruction = "$yoi/role/reviewer"
+p.feature = {
+ task = { enabled = false },
+ memory = { enabled = true },
+ web = { enabled = true },
+ pods = { enabled = false },
+ ticket = { enabled = false, access = "lifecycle" },
+ ticket_orchestration = { enabled = false },
+}
- worker = {
- instruction = "$yoi/role/reviewer",
- },
-
- feature = {
- task = { enabled = false },
- memory = { enabled = true },
- web = { enabled = true },
- pods = { enabled = false },
- ticket = { enabled = false, access = "lifecycle" },
- ticket_orchestration = { enabled = false },
- },
-})
+return p