From 16063c636dd8af6cd421af1ffec41e57f2913328 Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 16 Jul 2026 01:22:57 +0900 Subject: [PATCH 1/9] ticket: route workspace ticket settings work --- .../artifacts/orchestration-plan.jsonl | 2 + .yoi/tickets/00001KXK9507N/item.md | 2 +- .yoi/tickets/00001KXK9507N/thread.md | 91 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 .yoi/tickets/00001KXK9507N/artifacts/orchestration-plan.jsonl diff --git a/.yoi/tickets/00001KXK9507N/artifacts/orchestration-plan.jsonl b/.yoi/tickets/00001KXK9507N/artifacts/orchestration-plan.jsonl new file mode 100644 index 00000000..1db66a73 --- /dev/null +++ b/.yoi/tickets/00001KXK9507N/artifacts/orchestration-plan.jsonl @@ -0,0 +1,2 @@ +{"id":"orch-plan-20260715-162205-1","ticket_id":"00001KXK9507N","kind":"accepted_plan","accepted_plan":{"summary":"Consolidate durable Ticket configuration from `.yoi/ticket.config.toml` into tracked Workspace project settings. Move Ticket backend provider/root, record language, orchestration defaults, and fixed role launch config under Workspace settings while keeping local backend/runtime operational settings separate. Preserve default `.yoi/tickets` behavior, allow narrow read-only legacy fallback/migration note, and ensure CLI, Worker Ticket tools, workspace-server Ticket endpoint, Panel/role launch paths, and tests resolve the new Workspace settings authority.","branch":"work/00001KXK9507N-ticket-workspace-settings","worktree":"/home/hare/Projects/yoi/.worktree/00001KXK9507N-ticket-workspace-settings","role_plan":"Routing-only pass under explicit workflow instruction: do not spawn role Pods until explicit follow-up. On follow-up, re-check Ticket/relation/plan/worktree state, record `queued -> inprogress`, create a dedicated worktree under `/home/hare/Projects/yoi/.worktree`, then use sibling Coder/Reviewer Pods. Orchestrator retains merge/final-validation/close/worktree-cleanup authority; per current user instruction, do not call StopPod."},"author":"orchestrator","at":"2026-07-15T16:22:05Z"} +{"id":"orch-plan-20260715-162216-2","ticket_id":"00001KXK9507N","kind":"waiting_capacity_note","note":"Routing-only workflow invocation explicitly instructed: “wait for explicit follow-up before spawning role Pods.” Ticket is implementation-ready and no blockers were found, but this pass intentionally does not transition `queued -> inprogress`, create a worktree, or spawn Coder/Reviewer Pods. Next explicit follow-up should re-check state, then accept `queued -> inprogress` before implementation side effects.","author":"orchestrator","at":"2026-07-15T16:22:16Z"} diff --git a/.yoi/tickets/00001KXK9507N/item.md b/.yoi/tickets/00001KXK9507N/item.md index cd1e8ed9..a6578fca 100644 --- a/.yoi/tickets/00001KXK9507N/item.md +++ b/.yoi/tickets/00001KXK9507N/item.md @@ -2,7 +2,7 @@ title: 'Consolidate Ticket configuration into Workspace settings' state: 'queued' created_at: '2026-07-15T16:18:25Z' -updated_at: '2026-07-15T16:19:31Z' +updated_at: '2026-07-15T16:22:50Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-15T16:19:31Z' diff --git a/.yoi/tickets/00001KXK9507N/thread.md b/.yoi/tickets/00001KXK9507N/thread.md index b3b46c74..e117ac84 100644 --- a/.yoi/tickets/00001KXK9507N/thread.md +++ b/.yoi/tickets/00001KXK9507N/thread.md @@ -57,4 +57,95 @@ Marked ready by `yoi ticket state`. Ticket を `workspace-panel` が queued にしました。 +--- + + + +## Decision + +Routing decision: implementation_ready_waiting_explicit_followup + +Reason: +- Ticket は Workspace project settings を Ticket configuration の durable authority にする具体的な移行作業であり、要件・受け入れ条件・migration/fallback 方針が実装可能な粒度で記録されている。 +- `TicketRelationQuery(00001KXK9507N)` は 0 件で、blocking relation はない。 +- `TicketOrchestrationPlanQuery(00001KXK9507N)` は事前 record なし。今回 accepted plan と waiting-capacity note を記録した。 +- `TicketList(inprogress)` は 0 件、`TicketList(queued)` はこの Ticket 1 件のみ。 +- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` は clean。 +- ただし今回の workflow invocation は「explicit follow-up before spawning role Pods」と明示しているため、この routing pass では `queued -> inprogress`、worktree 作成、Coder/Reviewer Pod spawn は行わない。 + +Evidence checked: +- Ticket body / thread / artifacts。 +- `.yoi/workspace.toml` and `.yoi/ticket.config.toml` current files。 +- `docs/development/work-items.md` and `docs/design/workflows-public-dogfood-split.md` Ticket config references。 +- Code map via grep: + - `crates/yoi/src/ticket_cli.rs`, `crates/yoi/src/objective_cli.rs` + - `crates/tui/src/workspace_panel.rs` + - `crates/worker/src/feature/builtin/ticket.rs`, `crates/worker/src/controller.rs` + - workspace identity/settings references in `crates/worker`, `crates/worker-runtime`, workspace-server related code。 +- Current worktrees/branches: no existing `work/00001KXK9507N-*` branch/worktree。 +- Visible Pods: no active role Pod needed for this routing-only pass。 + +IntentPacket: + +Intent: +- `.yoi/ticket.config.toml` を長期 active authority から外し、tracked Workspace project settings の namespace に Ticket configuration を統合する。 +- Ticket backend provider/root、Ticket record language、orchestration defaults、固定 role launch config を Workspace settings authority から解決する。 + +Binding decisions / invariants: +- local-only backend/runtime operational settings(DB path、listen address、runtime endpoint/store 等)と tracked project/workspace Ticket policy を混ぜない。 +- Workspace settings が Ticket configuration の durable authority になる。 +- `.yoi/ticket.config.toml` は必要な場合だけ narrow read-only migration fallback として扱い、長期 active authority として残さない。 +- default behavior は保つ: 設定なし/移行後 default では既存 `.yoi/tickets` data に対して CLI と Worker Ticket tools が動く。 +- workspace-server Ticket backend endpoint は `.yoi/tickets` hard-code ではなく configured Ticket backend root を尊重する。 +- role launch config は fixed role slots を維持し、Profile/workflow selector の authority を Workspace settings 側に移す。 +- Ticket record language policy は durable Ticket records に適用し、通常 worker prose language と混同しない。 + +Requirements / acceptance criteria: +- Workspace settings loading が Ticket backend root/provider、language、orchestration defaults、role slots を読む test を追加/更新する。 +- Ticket CLI / Objective CLI / Worker Ticket feature setup / workspace-server Ticket backend endpoint / Panel role launch path が Workspace settings authority から Ticket settings を解決する。 +- Workspace settings で non-default Ticket backend root を設定した場合、direct CLI/local path と workspace-server-backed Worker Ticket tools の両方が尊重する。 +- `.yoi/ticket.config.toml` obsolete/replaced の docs または migration/in-code note を残す。 +- affected crates の cargo tests と `nix build .#yoi` が通る。 + +Implementation latitude: +- 既存 `.yoi/workspace.toml` を拡張するか、tracked Workspace project settings file を別に導入するかは、既存 Workspace settings architecture に合わせて coder が選んでよい。ただし local-only backend/runtime settings とは分離すること。 +- Legacy `.yoi/ticket.config.toml` fallback の exact scope(read-only import / warning / migration helper)は bounded local tactic としてよいが、active authority として二重管理しないこと。 +- DTO/schema/test layout は existing workspace/ticket config patterns に合わせてよい。 + +Escalate if: +- Workspace project settings の tracked file 形式自体を新規 product decision として固定する必要があり、既存 `.yoi/workspace.toml` / settings architecture から自然に選べない場合。 +- `.yoi/ticket.config.toml` と Workspace settings の二重 authority を長期互換として残す必要が出る場合。 +- remote/backend Ticket authority の設計が、local path migration を越えて protocol/API redesign を要求する場合。 +- existing Ticket storage migration で data loss risk が出る場合。 + +Validation: +- `git diff --check` +- `cargo test -p ticket` if ticket config crate/tests are touched。 +- `cargo test -p yoi --lib --tests` or targeted CLI tests if CLI paths are touched。 +- `cargo test -p worker --lib --tests` if Worker Ticket feature/controller paths are touched。 +- `cargo test -p yoi-workspace-server --lib` if workspace-server Ticket endpoint/settings are touched。 +- `cargo test -p tui --lib --tests` if Panel role launch/config availability is touched。 +- `cargo check -p yoi` +- `cd web/workspace && deno task check && deno task test` if web/API types are touched。 +- `yoi ticket doctor` +- `nix build .#yoi --no-link` + +Current code map / likely touch points: +- `crates/ticket` config model/loading。 +- `crates/yoi/src/ticket_cli.rs`, `crates/yoi/src/objective_cli.rs`。 +- `crates/tui/src/workspace_panel.rs` for Panel config availability/role launch。 +- `crates/worker/src/feature/builtin/ticket.rs`, `crates/worker/src/controller.rs` for Worker Ticket tools/setup。 +- workspace-server Ticket backend endpoint/settings code。 +- `.yoi/workspace.toml` / tracked Workspace settings file and docs under `docs/development/work-items.md`。 + +Critical risks / reviewer focus: +- accidentally keeping `.yoi/ticket.config.toml` as co-equal active authority。 +- mixing local-only backend/runtime settings into tracked project settings。 +- direct CLI and workspace-server-backed Worker Ticket tools resolving different Ticket roots/languages。 +- role launch config losing fixed role semantics or using `inherit` where top-level role launch cannot support it。 +- hard-coded `.yoi/tickets` remaining in workspace-server endpoint or Worker Ticket tools。 + +Next action: +- Wait for explicit follow-up. On follow-up, re-check Ticket/relation/plan/worktree state, then record `queued -> inprogress` before creating `/home/hare/Projects/yoi/.worktree/00001KXK9507N-ticket-workspace-settings` and spawning role Pods。 + --- From c68ed1fdc553860f63178f3be2b41fc279bae5cb Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 16 Jul 2026 01:30:44 +0900 Subject: [PATCH 2/9] ticket: accept workspace ticket settings work --- .yoi/tickets/00001KXK9507N/item.md | 4 ++-- .yoi/tickets/00001KXK9507N/thread.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.yoi/tickets/00001KXK9507N/item.md b/.yoi/tickets/00001KXK9507N/item.md index a6578fca..60fdcd69 100644 --- a/.yoi/tickets/00001KXK9507N/item.md +++ b/.yoi/tickets/00001KXK9507N/item.md @@ -1,8 +1,8 @@ --- title: 'Consolidate Ticket configuration into Workspace settings' -state: 'queued' +state: 'inprogress' created_at: '2026-07-15T16:18:25Z' -updated_at: '2026-07-15T16:22:50Z' +updated_at: '2026-07-15T16:30:37Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-15T16:19:31Z' diff --git a/.yoi/tickets/00001KXK9507N/thread.md b/.yoi/tickets/00001KXK9507N/thread.md index e117ac84..3e68d8bb 100644 --- a/.yoi/tickets/00001KXK9507N/thread.md +++ b/.yoi/tickets/00001KXK9507N/thread.md @@ -149,3 +149,31 @@ Next action: - Wait for explicit follow-up. On follow-up, re-check Ticket/relation/plan/worktree state, then record `queued -> inprogress` before creating `/home/hare/Projects/yoi/.worktree/00001KXK9507N-ticket-workspace-settings` and spawning role Pods。 --- + + + +## Decision + +Implementation follow-up accepted。 + +Evidence rechecked before side effects: +- Ticket remains `queued` and has prior implementation-ready IntentPacket / accepted plan。 +- `TicketRelationQuery(00001KXK9507N)`: 0 blockers。 +- `TicketOrchestrationPlanQuery(00001KXK9507N)`: accepted plan exists for branch/worktree `work/00001KXK9507N-ticket-workspace-settings` / `/home/hare/Projects/yoi/.worktree/00001KXK9507N-ticket-workspace-settings`。 +- `TicketList(inprogress)`: 0 件。 +- Orchestrator worktree is clean。 +- No existing `00001KXK9507N` worktree/branch was reported in the recheck。 + +Next action: +- Record `queued -> inprogress` now, then create the dedicated implementation worktree and route implementation to sibling Coder Pod。 +- Per current user instruction, do not call `StopPod` during later cleanup。 + +--- + + + +## State changed + +Explicit follow-up authorized implementation start. Ticket/relation/plan/worktree state was rechecked and no blockers were found. Moving to inprogress before worktree creation or role Pod spawn. + +--- From b1e3a2ad338a551f6e875087c93ecf709a03c1c8 Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 16 Jul 2026 02:19:04 +0900 Subject: [PATCH 3/9] ticket: move ticket settings into workspace config --- .yoi/ticket.config.toml | 22 -- .yoi/workspace.toml | 23 ++ crates/client/src/ticket_role.rs | 26 +- crates/ticket/src/config.rs | 346 ++++++++++++++---- crates/tui/src/dashboard/tests.rs | 22 +- crates/tui/src/workspace_panel.rs | 51 ++- crates/worker/src/controller.rs | 4 +- crates/worker/src/feature/builtin/ticket.rs | 24 +- crates/workspace-server/src/identity.rs | 11 +- crates/workspace-server/src/records.rs | 50 ++- crates/workspace-server/src/server.rs | 58 ++- crates/yoi/src/ticket_cli.rs | 147 ++++++-- docs/design/workflows-public-dogfood-split.md | 12 +- docs/development/work-items.md | 47 +-- 14 files changed, 630 insertions(+), 213 deletions(-) delete mode 100644 .yoi/ticket.config.toml diff --git a/.yoi/ticket.config.toml b/.yoi/ticket.config.toml deleted file mode 100644 index 4b1da71e..00000000 --- a/.yoi/ticket.config.toml +++ /dev/null @@ -1,22 +0,0 @@ -[backend] -provider = "builtin:yoi_local" -root = ".yoi/tickets" - -[ticket] -language = "Japanese" - -[roles.intake] -profile = "builtin:intake" -workflow = "ticket-intake-workflow" - -[roles.orchestrator] -profile = "builtin:orchestrator" -workflow = "ticket-orchestrator-routing" - -[roles.coder] -profile = "builtin:coder" -workflow = "multi-agent-workflow" - -[roles.reviewer] -profile = "builtin:reviewer" -workflow = "multi-agent-workflow" diff --git a/.yoi/workspace.toml b/.yoi/workspace.toml index 6cfcc713..c31fd9ae 100644 --- a/.yoi/workspace.toml +++ b/.yoi/workspace.toml @@ -1,3 +1,26 @@ workspace_id = "0197a949-4b6b-7f2a-9d9a-1f87e3a4c5b6" created_at = "2026-06-23T00:00:00Z" display_name = "yoi" + +[ticket] +language = "Japanese" + +[ticket.backend] +provider = "builtin:yoi_local" +root = ".yoi/tickets" + +[ticket.roles.intake] +profile = "builtin:intake" +workflow = "ticket-intake-workflow" + +[ticket.roles.orchestrator] +profile = "builtin:orchestrator" +workflow = "ticket-orchestrator-routing" + +[ticket.roles.coder] +profile = "builtin:coder" +workflow = "multi-agent-workflow" + +[ticket.roles.reviewer] +profile = "builtin:reviewer" +workflow = "multi-agent-workflow" diff --git a/crates/client/src/ticket_role.rs b/crates/client/src/ticket_role.rs index 2ce2226d..24214825 100644 --- a/crates/client/src/ticket_role.rs +++ b/crates/client/src/ticket_role.rs @@ -253,7 +253,7 @@ pub enum TicketRoleLaunchError { #[error(transparent)] LaunchConfig(#[from] TicketRoleLaunchConfigError), #[error( - "Ticket role `{role}` profile selector `{selector}` is not resolvable before launch: {message}. Configure `[roles.{role}].profile` with an executable concrete profile selector such as `builtin:default` or a project/user profile" + "Ticket role `{role}` profile selector `{selector}` is not resolvable before launch: {message}. Configure `[ticket.roles.{role}].profile` with an executable concrete profile selector such as `builtin:default` or a project/user profile" )] ProfileResolution { role: TicketRole, @@ -287,7 +287,7 @@ pub enum TicketRoleLaunchError { RunAcceptanceTimeout, } -/// Load `.yoi/ticket.config.toml` from the workspace and construct a launch plan. +/// Load Ticket policy from `.yoi/workspace.toml` and construct a launch plan. pub fn plan_ticket_role_launch( context: TicketRoleLaunchContext, ) -> Result { @@ -700,14 +700,14 @@ mod tests { fn write_config(workspace: &std::path::Path, content: &str) { let dir = workspace.join(".yoi"); std::fs::create_dir_all(&dir).unwrap(); - std::fs::write(dir.join("ticket.config.toml"), content).unwrap(); + std::fs::write(dir.join("workspace.toml"), content).unwrap(); } fn write_builtin_role_config(workspace: &std::path::Path, roles: &[TicketRole]) { - let mut config = String::new(); + let mut config = String::from("[ticket]\n"); for role in roles { config.push_str(&format!( - "\n[roles.{role}]\nprofile = \"builtin:default\"\n" + "\n[ticket.roles.{role}]\nprofile = \"builtin:default\"\n" )); } write_config(workspace, &config); @@ -883,7 +883,7 @@ mod tests { err.to_string() .contains("Ticket role `coder` is not launch-configured") ); - assert!(err.to_string().contains("[roles.coder]")); + assert!(err.to_string().contains("[ticket.roles.coder]")); } #[test] @@ -913,7 +913,7 @@ root = ".yoi/tickets" write_config( temp.path(), r#" -[roles.intake] +[ticket.roles.intake] profile = "inherit" "#, ); @@ -931,7 +931,7 @@ profile = "inherit" write_config( temp.path(), r#" -[roles.intake] +[ticket.roles.intake] profile = "project:no-such-ticket-role-profile" "#, ); @@ -944,7 +944,7 @@ profile = "project:no-such-ticket-role-profile" "profile selector `project:no-such-ticket-role-profile` is not resolvable" ) ); - assert!(err.to_string().contains("[roles.intake].profile")); + assert!(err.to_string().contains("[ticket.roles.intake].profile")); } #[test] @@ -968,7 +968,7 @@ profile = "project:no-such-ticket-role-profile" [ticket] language = "Japanese" -[roles.intake] +[ticket.roles.intake] profile = "builtin:default" "#, ); @@ -1035,7 +1035,7 @@ profile = "builtin:default" write_config( temp.path(), r#" -[roles.reviewer] +[ticket.roles.reviewer] profile = "builtin:default" launch_prompt = "$workspace/ticket/reviewer/launch" workflow = "ticket-review-workflow" @@ -1226,7 +1226,7 @@ workflow = "ticket-review-workflow" write_config( temp.path(), r#" -[roles.coder] +[ticket.roles.coder] profile = "./coder.toml" "#, ); @@ -1243,7 +1243,7 @@ profile = "./coder.toml" write_config( temp.path(), r#" -[roles.coder] +[ticket.roles.coder] profile = "inherit" system_instruction = "$workspace/not-supported" "#, diff --git a/crates/ticket/src/config.rs b/crates/ticket/src/config.rs index 85751a98..f7a4e47e 100644 --- a/crates/ticket/src/config.rs +++ b/crates/ticket/src/config.rs @@ -1,9 +1,12 @@ //! Workspace-local Ticket orchestration configuration. //! -//! The config file lives at `.yoi/ticket.config.toml` under a workspace root. -//! It intentionally stores lightweight string references for Profile selectors, -//! launch prompts, and workflows so this crate remains independent from `worker` -//! and `manifest` runtime resolution. +//! Durable Ticket policy lives under the `[ticket]` table in tracked +//! `.yoi/workspace.toml` workspace settings. The legacy +//! `.yoi/ticket.config.toml` file is only a read-only migration fallback when +//! workspace settings do not contain any Ticket policy. The config intentionally +//! stores lightweight string references for Profile selectors, launch prompts, +//! and workflows so this crate remains independent from `worker` and `manifest` +//! runtime resolution. use std::collections::{BTreeMap, BTreeSet}; use std::fmt; @@ -13,6 +16,8 @@ use std::path::{Component, Path, PathBuf}; use serde::{Deserialize, Serialize}; use thiserror::Error; +pub const WORKSPACE_SETTINGS_RELATIVE_PATH: &str = ".yoi/workspace.toml"; +/// Legacy Ticket config path. This is kept as a narrow read-only migration fallback only. pub const TICKET_CONFIG_RELATIVE_PATH: &str = ".yoi/ticket.config.toml"; /// Workspace-relative default root for the built-in local Ticket backend. pub const DEFAULT_TICKET_BACKEND_RELATIVE_PATH: &str = ".yoi/tickets"; @@ -20,13 +25,17 @@ const DEFAULT_ORCHESTRATION_BRANCH: &str = "orchestration"; const DEFAULT_ORCHESTRATION_WORKTREE_DIR: &str = ".worktree"; const DEFAULT_ORCHESTRATION_WORKTREE_NAME: &str = "orchestration"; -/// Return the explicit workspace Ticket config scaffold written by `yoi ticket init`. +/// Return the explicit Workspace settings Ticket policy scaffold written by `yoi ticket init`. /// -/// The scaffold intentionally configures every fixed Ticket role with a concrete -/// profile so strict role launch planning can validate the config without runtime +/// The scaffold is a valid `.yoi/workspace.toml` fragment rooted at `[ticket]`. +/// It intentionally configures every fixed Ticket role with a concrete profile +/// so strict role launch planning can validate the config without runtime /// fallback. pub fn ticket_config_scaffold() -> String { - let mut out = String::from("[backend]\n"); + let mut out = String::from( + "[ticket]\n# Optional durable Ticket record language. When unset, generated Ticket text keeps current defaults.\n# language = \"Japanese\"\n", + ); + out.push_str("\n[ticket.backend]\n"); out.push_str(&format!( "provider = \"{}\"\n", TicketBackendProvider::BuiltinYoiLocal.as_str() @@ -36,14 +45,11 @@ pub fn ticket_config_scaffold() -> String { DEFAULT_TICKET_BACKEND_RELATIVE_PATH )); out.push_str( - "\n# Optional durable Ticket record language. When unset, generated Ticket text keeps current defaults.\n# [ticket]\n# language = \"Japanese\"\n", - ); - out.push_str( - "\n# Optional Panel Orchestrator worktree settings. When unset, Panel uses branch `orchestration` at `.worktree/orchestration`.\n# [orchestration]\n# branch = \"orchestration\"\n# worktree_dir = \".worktree\"\n# worktree_name = \"orchestration\"\n", + "\n# Optional Panel Orchestrator worktree settings. When unset, Panel uses branch `orchestration` at `.worktree/orchestration`.\n# [ticket.orchestration]\n# branch = \"orchestration\"\n# worktree_dir = \".worktree\"\n# worktree_name = \"orchestration\"\n", ); for role in TicketRole::ALL { out.push_str(&format!( - "\n[roles.{role}]\nprofile = \"{}\"\nworkflow = \"{}\"\n", + "\n[ticket.roles.{role}]\nprofile = \"{}\"\nworkflow = \"{}\"\n", role.default_profile(), role.default_workflow() )); @@ -222,15 +228,81 @@ impl TicketConfig { pub fn load_workspace(workspace_root: impl AsRef) -> Result { let workspace_root = workspace_root.as_ref(); - let path = workspace_root.join(TICKET_CONFIG_RELATIVE_PATH); - let content = match fs::read_to_string(&path) { + let workspace_settings_path = workspace_root.join(WORKSPACE_SETTINGS_RELATIVE_PATH); + match fs::read_to_string(&workspace_settings_path) { + Ok(content) => { + if let Some(config) = Self::from_workspace_settings_toml( + workspace_root, + &workspace_settings_path, + &content, + )? { + return Ok(config); + } + } + Err(source) if source.kind() == std::io::ErrorKind::NotFound => {} + Err(source) => { + return Err(TicketConfigError::Read { + path: workspace_settings_path, + source, + }); + } + } + + // Narrow read-only migration fallback for pre-workspace-settings projects. + // As soon as `.yoi/workspace.toml` contains any `[ticket]` table, the + // workspace settings authority wins and this legacy file is ignored. + let legacy_path = workspace_root.join(TICKET_CONFIG_RELATIVE_PATH); + let legacy_content = match fs::read_to_string(&legacy_path) { Ok(content) => content, Err(source) if source.kind() == std::io::ErrorKind::NotFound => { return Ok(Self::default_for_workspace(workspace_root)); } - Err(source) => return Err(TicketConfigError::Read { path, source }), + Err(source) => { + return Err(TicketConfigError::Read { + path: legacy_path, + source, + }); + } }; - Self::from_toml(workspace_root, &path, &content) + Self::from_toml(workspace_root, &legacy_path, &legacy_content) + } + + pub fn from_workspace_settings_toml( + workspace_root: impl AsRef, + path: impl AsRef, + content: &str, + ) -> Result, TicketConfigError> { + let workspace_root = workspace_root.as_ref(); + let path = path.as_ref(); + let value: toml::Value = + toml::from_str(content).map_err(|source| TicketConfigError::Parse { + path: path.to_path_buf(), + source, + })?; + let Some(ticket_value) = value.get("ticket").cloned() else { + return Ok(None); + }; + let raw: RawWorkspaceTicketConfig = + ticket_value + .try_into() + .map_err(|source| TicketConfigError::Parse { + path: path.to_path_buf(), + source, + })?; + raw.resolve(workspace_root, path).map(Some) + } + + pub fn workspace_settings_has_ticket_config( + path: impl AsRef, + content: &str, + ) -> Result { + let path = path.as_ref(); + let value: toml::Value = + toml::from_str(content).map_err(|source| TicketConfigError::Parse { + path: path.to_path_buf(), + source, + })?; + Ok(value.get("ticket").is_some()) } pub fn from_toml( @@ -487,11 +559,11 @@ impl Default for TicketRoleProfiles { #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum TicketRoleLaunchConfigError { #[error( - "Ticket role `{role}` is not launch-configured; add `[roles.{role}]` with the role builtin profile or another executable concrete profile selector" + "Ticket role `{role}` is not launch-configured; add `[ticket.roles.{role}]` with the role builtin profile or another executable concrete profile selector" )] MissingRoleTable { role: TicketRole }, #[error( - "Ticket role `{role}` has no launch profile; set `[roles.{role}].profile` to the role builtin profile or another executable concrete profile selector" + "Ticket role `{role}` has no launch profile; set `[ticket.roles.{role}].profile` to the role builtin profile or another executable concrete profile selector" )] MissingProfile { role: TicketRole }, #[error( @@ -675,6 +747,19 @@ struct RawTicketConfig { roles: BTreeMap, } +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct RawWorkspaceTicketConfig { + #[serde(default)] + backend: RawBackendConfig, + #[serde(default)] + language: Option, + #[serde(default)] + orchestration: RawTicketOrchestrationConfig, + #[serde(default)] + roles: BTreeMap, +} + #[derive(Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] struct RawTicketOrchestrationConfig { @@ -723,41 +808,78 @@ impl RawTicketConfig { workspace_root: &Path, path: &Path, ) -> Result { - let mut roles = TicketRoleProfiles::default(); - for (name, raw_role) in self.roles { - let role = TicketRole::parse(&name).ok_or_else(|| TicketConfigError::Invalid { - path: path.to_path_buf(), - message: format!( - "unsupported Ticket role `{name}`; supported fixed roles: {}", - TicketRole::supported_names().join(", ") - ), - })?; - let profile_configured = raw_role.profile.is_some(); - roles.inner.insert(role, raw_role.resolve(role)); - roles.configured_roles.insert(role); - if profile_configured { - roles.profile_configured_roles.insert(role); - } - } - Ok(TicketConfig { - backend: self.backend.resolve(workspace_root).map_err(|message| { - TicketConfigError::Invalid { - path: path.to_path_buf(), - message, - } - })?, - ticket: self.ticket.resolve(), - orchestration: self.orchestration.resolve().map_err(|message| { - TicketConfigError::Invalid { - path: path.to_path_buf(), - message, - } - })?, - roles, - }) + resolve_ticket_config_parts( + self.backend, + self.ticket.resolve(), + self.orchestration, + self.roles, + workspace_root, + path, + ) } } +impl RawWorkspaceTicketConfig { + fn resolve( + self, + workspace_root: &Path, + path: &Path, + ) -> Result { + resolve_ticket_config_parts( + self.backend, + TicketRecordConfig { + language: self.language, + }, + self.orchestration, + self.roles, + workspace_root, + path, + ) + } +} + +fn resolve_ticket_config_parts( + backend: RawBackendConfig, + ticket: TicketRecordConfig, + orchestration: RawTicketOrchestrationConfig, + raw_roles: BTreeMap, + workspace_root: &Path, + path: &Path, +) -> Result { + let mut roles = TicketRoleProfiles::default(); + for (name, raw_role) in raw_roles { + let role = TicketRole::parse(&name).ok_or_else(|| TicketConfigError::Invalid { + path: path.to_path_buf(), + message: format!( + "unsupported Ticket role `{name}`; supported fixed roles: {}", + TicketRole::supported_names().join(", ") + ), + })?; + let profile_configured = raw_role.profile.is_some(); + roles.inner.insert(role, raw_role.resolve(role)); + roles.configured_roles.insert(role); + if profile_configured { + roles.profile_configured_roles.insert(role); + } + } + Ok(TicketConfig { + backend: backend + .resolve(workspace_root) + .map_err(|message| TicketConfigError::Invalid { + path: path.to_path_buf(), + message, + })?, + ticket, + orchestration: orchestration + .resolve() + .map_err(|message| TicketConfigError::Invalid { + path: path.to_path_buf(), + message, + })?, + roles, + }) +} + #[derive(Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] struct RawBackendConfig { @@ -843,6 +965,12 @@ mod tests { std::fs::write(dir.join("ticket.config.toml"), content).unwrap(); } + fn write_workspace_settings(workspace: &Path, content: &str) { + let dir = workspace.join(".yoi"); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("workspace.toml"), content).unwrap(); + } + #[test] fn missing_config_returns_documented_defaults() { let temp = TempDir::new().unwrap(); @@ -876,39 +1004,123 @@ mod tests { } #[test] - fn full_config_parses_fixed_role_refs() { + fn workspace_settings_take_precedence_over_legacy_ticket_config() { let temp = TempDir::new().unwrap(); + write_workspace_settings( + temp.path(), + r#" +[ticket] +language = "Japanese" + +[ticket.backend] +provider = "builtin:yoi_local" +root = "workspace-tickets" +"#, + ); write_config( temp.path(), r#" [backend] provider = "builtin:yoi_local" -root = "custom-tickets" +root = "legacy-tickets" + +[ticket] +language = "English" +"#, + ); + + let config = TicketConfig::load_workspace(temp.path()).unwrap(); + assert_eq!(config.backend.root, temp.path().join("workspace-tickets")); + assert_eq!(config.ticket_record_language(), Some("Japanese")); + } + + #[test] + fn legacy_ticket_config_is_read_only_migration_fallback() { + let temp = TempDir::new().unwrap(); + write_workspace_settings( + temp.path(), + r#" +workspace_id = "00000000-0000-7000-8000-000000000000" +display_name = "legacy-fallback" +"#, + ); + write_config( + temp.path(), + r#" +[backend] +provider = "builtin:yoi_local" +root = "legacy-tickets" [ticket] language = "Japanese" +"#, + ); -[orchestration] + let config = TicketConfig::load_workspace(temp.path()).unwrap(); + assert_eq!(config.backend.root, temp.path().join("legacy-tickets")); + assert_eq!(config.ticket_record_language(), Some("Japanese")); + } + + #[test] + fn empty_workspace_ticket_table_uses_workspace_defaults_and_ignores_legacy() { + let temp = TempDir::new().unwrap(); + write_workspace_settings( + temp.path(), + r#" +[ticket] +"#, + ); + write_config( + temp.path(), + r#" +[backend] +provider = "builtin:yoi_local" +root = "legacy-tickets" + +[ticket] +language = "Japanese" +"#, + ); + + let config = TicketConfig::load_workspace(temp.path()).unwrap(); + assert_eq!(config.backend.root, temp.path().join(".yoi/tickets")); + assert_eq!(config.ticket_record_language(), None); + } + + #[test] + fn full_config_parses_fixed_role_refs() { + let temp = TempDir::new().unwrap(); + write_workspace_settings( + temp.path(), + r#" +[ticket] +language = "Japanese" + +[ticket.backend] +provider = "builtin:yoi_local" +root = "custom-tickets" + +[ticket.orchestration] branch = "orchestration/custom-panel" worktree_dir = "custom-worktrees" worktree_name = "custom-orchestrator" -[roles.intake] +[ticket.roles.intake] profile = "project:intake" launch_prompt = "$workspace/ticket/intake/launch" workflow = "ticket-intake-workflow" -[roles.orchestrator] +[ticket.roles.orchestrator] profile = "project:orchestrator" launch_prompt = "$workspace/ticket/orchestrator/launch" workflow = "ticket-orchestrator-routing" -[roles.coder] +[ticket.roles.coder] profile = "inherit" launch_prompt = "$workspace/ticket/coder/launch" workflow = "multi-agent-workflow" -[roles.reviewer] +[ticket.roles.reviewer] profile = "project:reviewer" launch_prompt = "$workspace/ticket/reviewer/launch" workflow = "multi-agent-workflow" @@ -960,28 +1172,30 @@ workflow = "multi-agent-workflow" let temp = TempDir::new().unwrap(); let scaffold = ticket_config_scaffold(); - assert!(scaffold.contains("[backend]\n")); + assert!(scaffold.contains("[ticket]\n")); + assert!(scaffold.contains("[ticket.backend]\n")); assert!(scaffold.contains("provider = \"builtin:yoi_local\"")); assert!(scaffold.contains("root = \".yoi/tickets\"")); - assert!(scaffold.contains("# [ticket]\n# language = \"Japanese\"")); + assert!(scaffold.contains("# language = \"Japanese\"")); assert!(scaffold.contains( - "# [orchestration]\n# branch = \"orchestration\"\n# worktree_dir = \".worktree\"\n# worktree_name = \"orchestration\"" + "# [ticket.orchestration]\n# branch = \"orchestration\"\n# worktree_dir = \".worktree\"\n# worktree_name = \"orchestration\"" )); for role in TicketRole::ALL { - assert!(scaffold.contains(&format!("[roles.{role}]"))); + assert!(scaffold.contains(&format!("[ticket.roles.{role}]"))); assert!(scaffold.contains(&format!( - "[roles.{role}]\nprofile = \"{}\"\nworkflow = \"{}\"", + "[ticket.roles.{role}]\nprofile = \"{}\"\nworkflow = \"{}\"", role.default_profile(), role.default_workflow() ))); } - assert!(!scaffold.contains("[roles.investigator]")); + assert!(!scaffold.contains("[ticket.roles.investigator]")); - let config = TicketConfig::from_toml( + let config = TicketConfig::from_workspace_settings_toml( temp.path(), - temp.path().join(TICKET_CONFIG_RELATIVE_PATH), + temp.path().join(WORKSPACE_SETTINGS_RELATIVE_PATH), &scaffold, ) + .unwrap() .unwrap(); assert_eq!(config.backend_root(), temp.path().join(".yoi/tickets")); assert_eq!(config.orchestration.branch_name(), None); diff --git a/crates/tui/src/dashboard/tests.rs b/crates/tui/src/dashboard/tests.rs index 20597cb3..74070088 100644 --- a/crates/tui/src/dashboard/tests.rs +++ b/crates/tui/src/dashboard/tests.rs @@ -94,13 +94,13 @@ fn ensure_and_restore_use_configured_orchestration_layout() { write_test_ticket_config( &root, r#" -[orchestration] +[ticket.orchestration] branch = "orchestration/custom-panel" worktree_dir = "custom-worktrees" worktree_name = "panel" "#, ); - run_test_git(&root, &["add", ".yoi/ticket.config.toml"]).unwrap(); + run_test_git(&root, &["add", ".yoi/workspace.toml"]).unwrap(); run_test_git(&root, &["commit", "-m", "ticket config"]).unwrap(); let resolved = resolved_orchestration_worktree_layout(&root).unwrap(); @@ -126,7 +126,7 @@ fn invalid_configured_orchestration_branch_is_rejected_before_git_worktree_opera write_test_ticket_config( &root, r#" -[orchestration] +[ticket.orchestration] branch = "orchestration/bad:branch" "#, ); @@ -145,11 +145,11 @@ fn restore_rejects_mismatched_configured_orchestration_branch_without_checkout() write_test_ticket_config( &root, r#" -[orchestration] +[ticket.orchestration] branch = "orchestration/custom-panel" "#, ); - run_test_git(&root, &["add", ".yoi/ticket.config.toml"]).unwrap(); + run_test_git(&root, &["add", ".yoi/workspace.toml"]).unwrap(); run_test_git(&root, &["commit", "-m", "ticket config"]).unwrap(); let layout = resolved_orchestration_worktree_layout(&root).unwrap(); run_test_git( @@ -259,7 +259,7 @@ fn existing_unrelated_repo_with_expected_branch_is_rejected_without_cleanup() { fn write_test_ticket_config(root: &Path, content: &str) { let config_dir = root.join(".yoi"); std::fs::create_dir_all(&config_dir).unwrap(); - std::fs::write(config_dir.join("ticket.config.toml"), content).unwrap(); + std::fs::write(config_dir.join("workspace.toml"), content).unwrap(); } fn init_test_repo(root: &Path) { @@ -337,8 +337,8 @@ fn ticket_workspace( ) .unwrap(); fs::write( - temp.path().join(".yoi/ticket.config.toml"), - "[backend]\nprovider = \"builtin:yoi_local\"\nroot = \".yoi/tickets\"\n", + temp.path().join(".yoi/workspace.toml"), + "[ticket]\n\n[ticket.backend]\nprovider = \"builtin:yoi_local\"\nroot = \".yoi/tickets\"\n", ) .unwrap(); let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); @@ -712,7 +712,7 @@ async fn ticket_close_action_blocks_non_done_ticket_without_mutation() { #[tokio::test] async fn ticket_action_rejects_stale_absent_config_without_mutation() { let (temp, ticket_id, backend) = ready_ticket_workspace("panel-no-config"); - fs::remove_file(temp.path().join(".yoi/ticket.config.toml")).unwrap(); + fs::remove_file(temp.path().join(".yoi/workspace.toml")).unwrap(); let error = dispatch_ticket_action(request_for(&temp, ticket_id.clone(), NextUserAction::Queue)) @@ -1059,8 +1059,8 @@ fn dashboard_ticket_action_rows_precede_pods_and_pod_actions_still_work() { let temp = TempDir::new().unwrap(); fs::create_dir_all(temp.path().join(".yoi")).unwrap(); fs::write( - temp.path().join(".yoi/ticket.config.toml"), - "[backend]\nprovider = \"builtin:yoi_local\"\nroot = \".yoi/tickets\"\n", + temp.path().join(".yoi/workspace.toml"), + "[ticket]\n\n[ticket.backend]\nprovider = \"builtin:yoi_local\"\nroot = \".yoi/tickets\"\n", ) .unwrap(); let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); diff --git a/crates/tui/src/workspace_panel.rs b/crates/tui/src/workspace_panel.rs index 382818f1..b14fe44a 100644 --- a/crates/tui/src/workspace_panel.rs +++ b/crates/tui/src/workspace_panel.rs @@ -7,7 +7,7 @@ use std::time::Instant; use protocol::WorkerStatus; use ticket::config::{ DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TICKET_CONFIG_RELATIVE_PATH, TicketConfig, - TicketOrchestrationConfig, + TicketOrchestrationConfig, WORKSPACE_SETTINGS_RELATIVE_PATH, }; use ticket::{ LocalTicketBackend, TicketBackend, TicketError, TicketEvent, TicketFilter, TicketIdOrSlug, @@ -567,8 +567,41 @@ pub(crate) fn decide_orchestrator_lifecycle( } pub(crate) fn ticket_config_availability(workspace_root: &Path) -> TicketConfigAvailability { - let config_path = workspace_root.join(TICKET_CONFIG_RELATIVE_PATH); - match config_path.symlink_metadata() { + let settings_path = workspace_root.join(WORKSPACE_SETTINGS_RELATIVE_PATH); + match settings_path.symlink_metadata() { + Ok(metadata) if !metadata.is_file() => TicketConfigAvailability::Unusable(format!( + "{} exists but is not a regular file", + WORKSPACE_SETTINGS_RELATIVE_PATH + )), + Ok(_) => match std::fs::read_to_string(&settings_path) { + Ok(content) => { + match TicketConfig::workspace_settings_has_ticket_config(&settings_path, &content) { + Ok(true) => match TicketConfig::load_workspace(workspace_root) { + Ok(_) => TicketConfigAvailability::Usable, + Err(error) => TicketConfigAvailability::Unusable(error.to_string()), + }, + Ok(false) => legacy_ticket_config_availability(workspace_root), + Err(error) => TicketConfigAvailability::Unusable(error.to_string()), + } + } + Err(error) => TicketConfigAvailability::Unusable(format!( + "could not read {}: {error}", + WORKSPACE_SETTINGS_RELATIVE_PATH + )), + }, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + legacy_ticket_config_availability(workspace_root) + } + Err(error) => TicketConfigAvailability::Unusable(format!( + "could not inspect {}: {error}", + WORKSPACE_SETTINGS_RELATIVE_PATH + )), + } +} + +fn legacy_ticket_config_availability(workspace_root: &Path) -> TicketConfigAvailability { + let legacy_path = workspace_root.join(TICKET_CONFIG_RELATIVE_PATH); + match legacy_path.symlink_metadata() { Ok(metadata) if !metadata.is_file() => TicketConfigAvailability::Unusable(format!( "{} exists but is not a regular file", TICKET_CONFIG_RELATIVE_PATH @@ -1766,8 +1799,8 @@ mod tests { let config_dir = workspace_root.join(".yoi"); fs::create_dir_all(&config_dir).unwrap(); fs::write( - config_dir.join("ticket.config.toml"), - "[backend]\nprovider = \"builtin:yoi_local\"\nroot = \".yoi/tickets\"\n", + config_dir.join("workspace.toml"), + "[ticket]\n\n[ticket.backend]\nprovider = \"builtin:yoi_local\"\nroot = \".yoi/tickets\"\n", ) .unwrap(); } @@ -2233,8 +2266,8 @@ mod tests { let config_dir = temp.path().join(".yoi"); fs::create_dir_all(&config_dir).unwrap(); fs::write( - config_dir.join("ticket.config.toml"), - "[backend]\nprovider = \"unknown:provider\"\nroot = \".yoi/tickets\"\n", + config_dir.join("workspace.toml"), + "[ticket]\n\n[ticket.backend]\nprovider = \"unknown:provider\"\nroot = \".yoi/tickets\"\n", ) .unwrap(); @@ -2678,11 +2711,11 @@ mod tests { } #[test] - fn existing_non_file_ticket_config_is_unusable_not_absent() { + fn existing_non_file_workspace_settings_is_unusable_not_absent() { let temp = TempDir::new().unwrap(); let config_parent = temp.path().join(".yoi"); fs::create_dir_all(&config_parent).unwrap(); - fs::create_dir(config_parent.join("ticket.config.toml")).unwrap(); + fs::create_dir(config_parent.join("workspace.toml")).unwrap(); assert!(matches!( ticket_config_availability(temp.path()), diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index 5452c00e..54773c4e 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -736,8 +736,8 @@ where "ticket tools require local Worker filesystem authority", ) })?; - crate::feature::builtin::ticket::TicketFeatureBackend::Local { - root: ticket_cwd.clone(), + crate::feature::builtin::ticket::TicketFeatureBackend::LocalWorkspace { + workspace_root: ticket_cwd.clone(), } } }; diff --git a/crates/worker/src/feature/builtin/ticket.rs b/crates/worker/src/feature/builtin/ticket.rs index a6aab4fc..28ec53e7 100644 --- a/crates/worker/src/feature/builtin/ticket.rs +++ b/crates/worker/src/feature/builtin/ticket.rs @@ -61,6 +61,9 @@ pub enum TicketFeatureBackend { Local { root: PathBuf, }, + LocalWorkspace { + workspace_root: PathBuf, + }, WorkspaceHttp { workspace_id: String, base_url: String, @@ -125,6 +128,13 @@ impl TicketFeature { access: Option, include_orchestration_tools: bool, ) -> Self { + if let TicketFeatureBackend::LocalWorkspace { workspace_root } = backend { + return Self::for_workspace_with_options( + workspace_root, + access, + include_orchestration_tools, + ); + } Self { backend, record_language: None, @@ -180,6 +190,7 @@ impl TicketFeature { pub fn backend_root(&self) -> Option<&Path> { match &self.backend { TicketFeatureBackend::Local { root } => Some(root), + TicketFeatureBackend::LocalWorkspace { workspace_root } => Some(workspace_root), TicketFeatureBackend::WorkspaceHttp { .. } => None, } } @@ -219,7 +230,8 @@ impl TicketFeature { } fn tool_backend(&self, context: &mut FeatureInstallContext<'_>) -> Option { match &self.backend { - TicketFeatureBackend::Local { root: _ } => { + TicketFeatureBackend::Local { root: _ } + | TicketFeatureBackend::LocalWorkspace { workspace_root: _ } => { let usable_root = match self.usable_backend_root() { Ok(root) => root, Err(reason) => { @@ -606,7 +618,7 @@ mod tests { fn write_ticket_config(workspace: &Path, content: &str) { let yoi_dir = workspace.join(".yoi"); std::fs::create_dir_all(&yoi_dir).unwrap(); - std::fs::write(yoi_dir.join("ticket.config.toml"), content).unwrap(); + std::fs::write(yoi_dir.join("workspace.toml"), content).unwrap(); } fn pending_tool_description( @@ -853,11 +865,11 @@ language = "Japanese" write_ticket_config( temp.path(), r#" -[backend] +[ticket.backend] provider = "builtin:yoi_local" root = "tickets" -[roles.coder] +[ticket.roles.coder] profile = "project:coder" "#, ); @@ -886,7 +898,7 @@ profile = "project:coder" write_ticket_config( temp.path(), r#" -[roles.operator] +[ticket.roles.operator] profile = "inherit" "#, ); @@ -911,7 +923,7 @@ profile = "inherit" write_ticket_config( temp.path(), r#" -[backend] +[ticket.backend] provider = "github" "#, ); diff --git a/crates/workspace-server/src/identity.rs b/crates/workspace-server/src/identity.rs index d2209646..3ff7e555 100644 --- a/crates/workspace-server/src/identity.rs +++ b/crates/workspace-server/src/identity.rs @@ -12,10 +12,10 @@ pub const WORKSPACE_IDENTITY_RELATIVE_PATH: &str = ".yoi/workspace.toml"; /// Stable local Workspace identity persisted as a tracked, safe project record. /// -/// The v0 TOML schema intentionally contains identity metadata only: -/// `workspace_id`, `created_at`, and `display_name`. Unknown fields are rejected -/// instead of preserved because this loader cannot safely round-trip future local -/// runtime settings without risking accidental path or secret persistence. +/// The v0 TOML schema contains identity metadata plus optional tracked project +/// policy tables such as `[ticket]`. Runtime/local-only settings remain rejected +/// here because this loader cannot safely round-trip future local runtime settings +/// without risking accidental path or secret persistence. #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorkspaceIdentity { pub workspace_id: String, @@ -29,6 +29,8 @@ struct WorkspaceIdentityFile { workspace_id: String, created_at: String, display_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + ticket: Option, } impl WorkspaceIdentity { @@ -112,6 +114,7 @@ impl WorkspaceIdentity { workspace_id: self.workspace_id.clone(), created_at: self.created_at.clone(), display_name: self.display_name.clone(), + ticket: None, }) .map_err(|error| { workspace_identity_error(path, format!("failed to encode TOML: {error}")) diff --git a/crates/workspace-server/src/records.rs b/crates/workspace-server/src/records.rs index 2f32122b..204c8f08 100644 --- a/crates/workspace-server/src/records.rs +++ b/crates/workspace-server/src/records.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use project_record::validate_record_id; use serde::{Deserialize, Serialize}; +use ticket::config::TicketConfig; use ticket::{LocalTicketBackend, TicketFilter, TicketIdOrSlug}; use crate::{Error, Result}; @@ -17,13 +18,16 @@ pub struct LocalProjectRecordReader { } impl LocalProjectRecordReader { - pub fn new(workspace_root: impl Into) -> Self { + pub fn new(workspace_root: impl Into) -> Result { let workspace_root = workspace_root.into(); - let ticket_root = workspace_root.join(".yoi/tickets"); - Self { + let ticket_config = TicketConfig::load_workspace(&workspace_root) + .map_err(|error| Error::Config(format!("load Ticket workspace settings: {error}")))?; + let ticket_backend = LocalTicketBackend::new(ticket_config.backend_root().to_path_buf()) + .with_record_language(ticket_config.ticket_record_language()); + Ok(Self { workspace_root, - ticket_backend: LocalTicketBackend::new(ticket_root), - } + ticket_backend, + }) } pub fn workspace_root(&self) -> &Path { @@ -297,7 +301,7 @@ mod tests { write_ticket(dir.path(), "00000000001J2", "Read bridge", "ready"); write_objective(dir.path(), "00000000001J3", "Control plane", "active"); - let reader = LocalProjectRecordReader::new(dir.path()); + let reader = LocalProjectRecordReader::new(dir.path()).unwrap(); let tickets = reader.list_tickets(20).unwrap(); assert_eq!(tickets.record_authority, "local_yoi_project_records"); assert_eq!(tickets.items[0].id, "00000000001J2"); @@ -313,9 +317,41 @@ mod tests { let objective = reader.objective("00000000001J3").unwrap(); assert!(objective.body.contains("Objective body")); } + #[test] + fn reads_tickets_from_workspace_settings_backend_root() { + let dir = tempfile::tempdir().unwrap(); + fs::create_dir_all(dir.path().join(".yoi")).unwrap(); + fs::write( + dir.path().join(".yoi/workspace.toml"), + r#" +[ticket] +language = "Japanese" +[ticket.backend] +provider = "builtin:yoi_local" +root = "project-records/tickets" +"#, + ) + .unwrap(); + write_ticket_at( + &dir.path().join("project-records/tickets"), + "00000000001J4", + "Configured root", + "ready", + ); + write_ticket(dir.path(), "00000000001J5", "Default root", "ready"); + + let reader = LocalProjectRecordReader::new(dir.path()).unwrap(); + let tickets = reader.list_tickets(20).unwrap(); + assert_eq!(tickets.items.len(), 1); + assert_eq!(tickets.items[0].id, "00000000001J4"); + } fn write_ticket(root: &Path, id: &str, title: &str, state: &str) { - let ticket_dir = root.join(".yoi/tickets").join(id); + write_ticket_at(&root.join(".yoi/tickets"), id, title, state); + } + + fn write_ticket_at(ticket_root: &Path, id: &str, title: &str, state: &str) { + let ticket_dir = ticket_root.join(id); fs::create_dir_all(&ticket_dir).unwrap(); fs::write( ticket_dir.join("item.md"), diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 964212f0..50719aa8 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -272,7 +272,7 @@ impl WorkspaceApi { let companion = Arc::new(CompanionConsole::disabled()); let observation_proxy = BackendObservationProxy::new(config.runtime_event_sources.clone()); Ok(Self { - records: LocalProjectRecordReader::new(config.workspace_root.clone()), + records: LocalProjectRecordReader::new(config.workspace_root.clone())?, config, store, runtime, @@ -1248,11 +1248,10 @@ async fn scoped_ticket_backend_operation( Json(operation): Json, ) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; - let backend = LocalTicketBackend::new( - api.config - .workspace_root - .join(ticket::config::DEFAULT_TICKET_BACKEND_RELATIVE_PATH), - ); + let config = ticket::config::TicketConfig::load_workspace(&api.config.workspace_root) + .map_err(|error| Error::Config(format!("load Ticket workspace settings: {error}")))?; + let backend = LocalTicketBackend::new(config.backend_root().to_path_buf()) + .with_record_language(config.ticket_record_language()); let response = match execute_ticket_backend_operation(&backend, operation) { Ok(result) => TicketBackendHttpResponse::Ok { result }, Err(error) => TicketBackendHttpResponse::Error { @@ -5800,6 +5799,53 @@ mod tests { } } + #[tokio::test] + async fn ticket_backend_endpoint_uses_workspace_settings_backend_root() { + let dir = tempfile::tempdir().unwrap(); + fs::create_dir_all(dir.path().join(".yoi")).unwrap(); + fs::write( + dir.path().join(".yoi/workspace.toml"), + format!( + "workspace_id = \"{TEST_WORKSPACE_ID}\"\ncreated_at = \"{TEST_CREATED_AT}\"\ndisplay_name = \"Endpoint Test\"\n\n[ticket]\nlanguage = \"Japanese\"\n\n[ticket.backend]\nprovider = \"builtin:yoi_local\"\nroot = \"server-tickets\"\n" + ), + ) + .unwrap(); + let api = test_api(dir.path()).await; + + let Json(response) = scoped_ticket_backend_operation( + State(api), + AxumPath(ScopedWorkspacePath { + workspace_id: TEST_WORKSPACE_ID.to_string(), + }), + Json(TicketBackendOperation::Create { + input: ticket::NewTicket::new("Endpoint configured root"), + }), + ) + .await + .unwrap_or_else(|error| panic!("ticket backend operation failed: {}", error.error)); + + let ticket_ref = match response { + TicketBackendHttpResponse::Ok { + result: ticket::TicketBackendOperationResult::TicketRef(ticket_ref), + } => ticket_ref, + other => panic!("unexpected ticket backend response: {other:?}"), + }; + assert!( + dir.path() + .join("server-tickets") + .join(&ticket_ref.id) + .join("item.md") + .is_file() + ); + assert!( + !dir.path() + .join(".yoi/tickets") + .join(&ticket_ref.id) + .join("item.md") + .exists() + ); + } + async fn test_api(workspace_root: impl Into) -> WorkspaceApi { let store = SqliteWorkspaceStore::in_memory().unwrap(); WorkspaceApi::new_with_execution_backend( diff --git a/crates/yoi/src/ticket_cli.rs b/crates/yoi/src/ticket_cli.rs index a0960a17..ad2e5580 100644 --- a/crates/yoi/src/ticket_cli.rs +++ b/crates/yoi/src/ticket_cli.rs @@ -1,11 +1,13 @@ use std::fmt; use std::fs; +use std::hash::{Hash, Hasher}; use std::io::Write; use std::path::{Path, PathBuf}; +use chrono::{SecondsFormat, Utc}; use ticket::config::{ DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TICKET_CONFIG_RELATIVE_PATH, TicketConfig, - ticket_config_scaffold, + WORKSPACE_SETTINGS_RELATIVE_PATH, ticket_config_scaffold, }; use ticket::{ LocalTicketBackend, MarkdownText, NewTicket, NewTicketEvent, NewTicketRelation, TicketBackend, @@ -252,11 +254,12 @@ fn run_command( } fn init(workspace: &Path) -> Result { - let config_path = workspace.join(TICKET_CONFIG_RELATIVE_PATH); - if config_path.exists() { + let legacy_config_path = workspace.join(TICKET_CONFIG_RELATIVE_PATH); + if legacy_config_path.exists() { return Err(TicketCliError::new(format!( - "ticket config already exists at {}; refusing to overwrite. Edit it manually or remove it before running `yoi ticket init`.", - config_path.display() + "legacy ticket config exists at {}; `.yoi/ticket.config.toml` is obsolete and read-only. Move its policy into {} before running `yoi ticket init`.", + legacy_config_path.display(), + WORKSPACE_SETTINGS_RELATIVE_PATH ))); } @@ -266,28 +269,96 @@ fn init(workspace: &Path) -> Result { fs::create_dir_all(&tickets_dir)?; fs::write(tickets_dir.join(".gitkeep"), b"")?; - let mut file = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&config_path) - .map_err(|error| { - if error.kind() == std::io::ErrorKind::AlreadyExists { - TicketCliError::new(format!( - "ticket config already exists at {}; refusing to overwrite. Edit it manually or remove it before running `yoi ticket init`.", - config_path.display() - )) - } else { - TicketCliError::from(error) - } - })?; - file.write_all(ticket_config_scaffold().as_bytes())?; + let settings_path = workspace.join(WORKSPACE_SETTINGS_RELATIVE_PATH); + let scaffold = ticket_config_scaffold(); + let created_settings = if settings_path.exists() { + let content = fs::read_to_string(&settings_path)?; + if TicketConfig::workspace_settings_has_ticket_config(&settings_path, &content)? { + return Err(TicketCliError::new(format!( + "workspace Ticket settings already exist at {}; refusing to overwrite. Edit the [ticket] table manually before running `yoi ticket init`.", + settings_path.display() + ))); + } + let mut file = fs::OpenOptions::new().append(true).open(&settings_path)?; + if !content.ends_with('\n') { + file.write_all(b"\n")?; + } + file.write_all(b"\n")?; + file.write_all(scaffold.as_bytes())?; + false + } else { + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&settings_path) + .map_err(|error| { + if error.kind() == std::io::ErrorKind::AlreadyExists { + TicketCliError::new(format!( + "workspace settings already exists at {}; retry `yoi ticket init` to append Ticket settings safely.", + settings_path.display() + )) + } else { + TicketCliError::from(error) + } + })?; + let identity = workspace_settings_identity_header(workspace)?; + file.write_all(identity.as_bytes())?; + file.write_all(b"\n")?; + file.write_all(scaffold.as_bytes())?; + true + }; + let verb = if created_settings { + "created" + } else { + "updated" + }; Ok(success(format!( - "created\t{}\nensured\t{}\n", - TICKET_CONFIG_RELATIVE_PATH, DEFAULT_TICKET_BACKEND_RELATIVE_PATH + "{verb}\t{}\nensured\t{}\n", + WORKSPACE_SETTINGS_RELATIVE_PATH, DEFAULT_TICKET_BACKEND_RELATIVE_PATH ))) } +fn workspace_settings_identity_header(workspace: &Path) -> Result { + let display_name = workspace + .file_name() + .and_then(|name| name.to_str()) + .filter(|name| !name.trim().is_empty()) + .unwrap_or("workspace"); + if display_name.contains('\0') || display_name.chars().any(|ch| ch.is_control()) { + return Err(TicketCliError::new( + "workspace display name derived from path must not contain control characters", + )); + } + Ok(format!( + "workspace_id = \"{}\"\ncreated_at = \"{}\"\ndisplay_name = \"{}\"\n", + workspace_settings_uuid_v7(workspace), + Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true), + display_name.replace('\\', "\\\\").replace('"', "\\\"") + )) +} + +fn workspace_settings_uuid_v7(workspace: &Path) -> String { + let now = Utc::now(); + let timestamp_ms = (now.timestamp_millis().max(0) as u64) & 0xffff_ffff_ffff; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + workspace.hash(&mut hasher); + std::process::id().hash(&mut hasher); + now.timestamp_nanos_opt() + .unwrap_or_default() + .hash(&mut hasher); + let random = hasher.finish(); + + let time_low = (timestamp_ms >> 16) as u32; + let time_mid = (timestamp_ms & 0xffff) as u16; + let version_and_rand = 0x7000 | (((random >> 52) as u16) & 0x0fff); + let variant_and_rand = 0x8000 | (((random >> 38) as u16) & 0x3fff); + let node = random & 0xffff_ffff_ffff; + format!( + "{time_low:08x}-{time_mid:04x}-{version_and_rand:04x}-{variant_and_rand:04x}-{node:012x}" + ) +} + fn backend_for_workspace(workspace: &Path) -> Result { let config = TicketConfig::load_workspace(workspace)?; Ok(LocalTicketBackend::new(config.backend_root().to_path_buf()) @@ -1077,7 +1148,7 @@ fn default_author() -> String { } fn help_text() -> &'static str { - "yoi ticket\n\nUsage:\n yoi ticket init\n yoi ticket create --title \n yoi ticket list [--state planning|ready|queued|inprogress|done|closed|all] [--limit <n>]\n yoi ticket show <id>\n yoi ticket comment <id> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket review <id> (--approve|--request-changes) (--file <path>|--message <text>)\n yoi ticket state <id> <planning|ready|queued|inprogress|done|closed>\n yoi ticket close <id> (--resolution <text>|--file <path>)\n yoi ticket relation add --ticket <id> --kind <depends_on|blocks|related|supersedes|duplicate_of> --target <id> [--note <text>]\n yoi ticket relation list [--ticket <id>] [--kind <kind>]\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nBackend:\n `yoi ticket init` writes .yoi/ticket.config.toml with explicit fixed role profiles and an optional commented [ticket].language setting.\n Uses the workspace Ticket config at .yoi/ticket.config.toml when present.\n Supported provider: builtin:yoi_local.\n Without config, the local backend root is <cwd>/.yoi/tickets.\n" + "yoi ticket\n\nUsage:\n yoi ticket init\n yoi ticket create --title <title>\n yoi ticket list [--state planning|ready|queued|inprogress|done|closed|all] [--limit <n>]\n yoi ticket show <id>\n yoi ticket comment <id> [--role comment|plan|decision|implementation_report] (--file <path>|--message <text>)\n yoi ticket review <id> (--approve|--request-changes) (--file <path>|--message <text>)\n yoi ticket state <id> <planning|ready|queued|inprogress|done|closed>\n yoi ticket close <id> (--resolution <text>|--file <path>)\n yoi ticket relation add --ticket <id> --kind <depends_on|blocks|related|supersedes|duplicate_of> --target <id> [--note <text>]\n yoi ticket relation list [--ticket <id>] [--kind <kind>]\n yoi ticket doctor\n\nOptions:\n -h, --help Print help\n\nBackend:\n `yoi ticket init` writes explicit fixed role profiles and optional [ticket].language into .yoi/workspace.toml.\n Uses workspace Ticket settings from .yoi/workspace.toml [ticket] when present; .yoi/ticket.config.toml is a read-only migration fallback only.\n Supported provider: builtin:yoi_local.\n Without configured Ticket settings, the local backend root is <cwd>/.yoi/tickets.\n" } #[cfg(test)] @@ -1106,33 +1177,31 @@ mod tests { } #[test] - fn ticket_cli_init_writes_explicit_ticket_config_scaffold() { + fn ticket_cli_init_writes_explicit_workspace_ticket_settings() { let temp = TempDir::new().unwrap(); let initialized = run(&temp, &["init"]); assert_eq!(initialized.status, TicketCliStatus::Success); - assert!( - initialized - .stdout - .contains("created\t.yoi/ticket.config.toml") - ); + assert!(initialized.stdout.contains("created\t.yoi/workspace.toml")); assert!(initialized.stdout.contains("ensured\t.yoi/tickets")); assert!(temp.path().join(".yoi/tickets").exists()); assert!(temp.path().join(".yoi/tickets/.gitkeep").exists()); - let config = fs::read_to_string(temp.path().join(".yoi/ticket.config.toml")).unwrap(); - assert!(config.contains("[backend]\n")); + let config = fs::read_to_string(temp.path().join(".yoi/workspace.toml")).unwrap(); + assert!(config.contains("workspace_id = \"")); + assert!(config.contains("[ticket]\n")); + assert!(config.contains("[ticket.backend]\n")); assert!(config.contains("provider = \"builtin:yoi_local\"")); assert!(config.contains("root = \".yoi/tickets\"")); - assert!(config.contains("# [ticket]\n# language = \"Japanese\"")); + assert!(config.contains("# language = \"Japanese\"")); for role in TicketRole::ALL { assert!(config.contains(&format!( - "[roles.{role}]\nprofile = \"{}\"\nworkflow = \"{}\"", + "[ticket.roles.{role}]\nprofile = \"{}\"\nworkflow = \"{}\"", role.default_profile(), role.default_workflow() ))); } - assert!(!config.contains("[roles.investigator]")); + assert!(!config.contains("[ticket.roles.investigator]")); } #[test] @@ -1148,9 +1217,9 @@ mod tests { let cli = parse_ticket_args(&args(&["init"])).unwrap(); let err = run_in_workspace(cli, temp.path()).unwrap_err(); - assert!(err.to_string().contains("already exists")); - assert!(err.to_string().contains("refusing to overwrite")); - assert!(err.to_string().contains("yoi ticket init")); + assert!(err.to_string().contains("legacy ticket config exists")); + assert!(err.to_string().contains("obsolete and read-only")); + assert!(err.to_string().contains(WORKSPACE_SETTINGS_RELATIVE_PATH)); assert_eq!( fs::read_to_string(config_path).unwrap(), "[backend]\nprovider = \"builtin:yoi_local\"\n" @@ -1414,8 +1483,8 @@ mod tests { let temp = TempDir::new().unwrap(); fs::create_dir_all(temp.path().join(".yoi")).unwrap(); fs::write( - temp.path().join(".yoi/ticket.config.toml"), - "[backend]\nprovider = \"builtin:yoi_local\"\nroot = \"custom-tickets\"\n", + temp.path().join(".yoi/workspace.toml"), + "[ticket]\nlanguage = \"Japanese\"\n\n[ticket.backend]\nprovider = \"builtin:yoi_local\"\nroot = \"custom-tickets\"\n", ) .unwrap(); diff --git a/docs/design/workflows-public-dogfood-split.md b/docs/design/workflows-public-dogfood-split.md index 3366fe6d..e6589ecb 100644 --- a/docs/design/workflows-public-dogfood-split.md +++ b/docs/design/workflows-public-dogfood-split.md @@ -30,7 +30,7 @@ This document records the design/audit decision for Ticket `00001KTRKZ14C`. It d | `multi-agent-workflow` | Partly. The sibling coder/reviewer loop, intent packet, branch-local review, and merge-ready dossier are useful product concepts. | Heavy Yoi dogfood specifics: Git worktrees, commits, cargo/nix validation, branch cleanup, docs/report conventions, and parent/child policy. | Split into builtin `multi-agent-workflow` for role loop + dossier, and workspace dogfood extension for Git worktree/cargo/nix mechanics. | | `worktree-workflow` | Not resident public core. | Almost entirely this repository's Git worktree mechanics and `.yoi` path exclusions. | Keep workspace-local dogfood workflow. Optionally create a non-resident builtin `git-worktree-isolation` later, disabled unless explicitly selected. | -## Slug and `.yoi/ticket.config.toml` migration decision +## Slug and workspace Ticket settings migration decision Use explicit dogfood slugs for workflows whose semantics differ from the public builtin workflow. Same-slug workspace overrides are allowed for local wording/policy tweaks of the same public contract, but they should not be used to hide Yoi repository Git/worktree/cargo/nix/merge semantics behind a public slug. This avoids accidental shadowing when a user expects `multi-agent-workflow` to mean the generic builtin role loop. @@ -38,10 +38,10 @@ Planned selector mapping for this repository after builtin workflow loading exis | Role/config surface | Workflow selector | Source intent | Rationale | | --- | --- | --- | --- | -| `[roles.intake].workflow` | `ticket-intake-workflow` | builtin core by default; workspace same-slug override only if this repo needs a true local refinement of the same intake contract | Intake is mostly product-generic; dogfood Git/worktree policy does not belong here. | -| `[roles.orchestrator].workflow` | `ticket-orchestrator-routing` | builtin core by default; dogfood merge/worktree instructions come from explicit launch context or a separate dogfood workflow, not a hidden same-slug override | Routing semantics should stay public/generic; repo-specific implementation mechanics should be opt-in and visible. | -| `[roles.coder].workflow` | `yoi-dogfood-multi-agent-workflow` | workspace-local dogfood workflow | Coder work in this repository is tied to the repo worktree/branch/validation contract and must not shadow generic builtin `multi-agent-workflow`. | -| `[roles.reviewer].workflow` | `yoi-dogfood-multi-agent-workflow` | workspace-local dogfood workflow | Reviewer evidence in this repository includes branch/worktree/cargo/nix/Ticket-doctor conventions; use an explicit dogfood slug. | +| `[ticket.roles.intake].workflow` | `ticket-intake-workflow` | builtin core by default; workspace same-slug override only if this repo needs a true local refinement of the same intake contract | Intake is mostly product-generic; dogfood Git/worktree policy does not belong here. | +| `[ticket.roles.orchestrator].workflow` | `ticket-orchestrator-routing` | builtin core by default; dogfood merge/worktree instructions come from explicit launch context or a separate dogfood workflow, not a hidden same-slug override | Routing semantics should stay public/generic; repo-specific implementation mechanics should be opt-in and visible. | +| `[ticket.roles.coder].workflow` | `yoi-dogfood-multi-agent-workflow` | workspace-local dogfood workflow | Coder work in this repository is tied to the repo worktree/branch/validation contract and must not shadow generic builtin `multi-agent-workflow`. | +| `[ticket.roles.reviewer].workflow` | `yoi-dogfood-multi-agent-workflow` | workspace-local dogfood workflow | Reviewer evidence in this repository includes branch/worktree/cargo/nix/Ticket-doctor conventions; use an explicit dogfood slug. | | compatibility references | `ticket-preflight-workflow` | builtin compatibility alias or workspace compatibility file, non-resident | Do not keep it as a role default. It should point to planning/requirements sync language only. | | dogfood worktree helper | `yoi-dogfood-worktree-workflow` | workspace-local helper, not a role default by itself | Referenced from `yoi-dogfood-multi-agent-workflow`; keeps Git worktree mechanics out of generic builtin workflows. | @@ -115,7 +115,7 @@ Builtin workflows and active workspace workflows should remove or replace the fo 3. **Dogfood workflow retention** - Keep or rename workspace-local dogfood workflows for this repository's implementation mechanics. - - Ensure `.yoi/ticket.config.toml` role workflow selectors intentionally point to builtin generic workflows or workspace dogfood overrides, with no accidental slug shadowing. + - Ensure `.yoi/workspace.toml` `[ticket.roles.*].workflow` selectors intentionally point to builtin generic workflows or workspace dogfood overrides, with no accidental slug shadowing. 4. **Vocabulary migration** - Sweep active workflow text, prompts, docs, and Ticket UI wording for `Action required`, `Attention required`, and old preflight lane language. diff --git a/docs/development/work-items.md b/docs/development/work-items.md index a76dcd51..a725a367 100644 --- a/docs/development/work-items.md +++ b/docs/development/work-items.md @@ -107,31 +107,34 @@ The first version intentionally does not implement roadmap scheduling, milestone ## Ticket configuration -Workspace Ticket orchestration is configured by `.yoi/ticket.config.toml` when present. +Workspace Ticket policy is configured by the tracked workspace settings file `.yoi/workspace.toml` under the `[ticket]` table. The old `.yoi/ticket.config.toml` file is obsolete: current code only reads it as a narrow read-only migration fallback when `.yoi/workspace.toml` has no `[ticket]` table. Workspace settings take precedence as soon as `[ticket]` exists. MVP shape: ```toml -[backend] +[ticket] +language = "Japanese" + +[ticket.backend] provider = "builtin:yoi_local" root = ".yoi/tickets" -[roles.intake] +[ticket.roles.intake] profile = "project:intake" launch_prompt = "$workspace/ticket/intake/launch" workflow = "ticket-intake-workflow" -[roles.orchestrator] +[ticket.roles.orchestrator] profile = "project:orchestrator" launch_prompt = "$workspace/ticket/orchestrator/launch" workflow = "ticket-orchestrator-routing" -[roles.coder] +[ticket.roles.coder] profile = "project:coder" launch_prompt = "$workspace/ticket/coder/launch" workflow = "multi-agent-workflow" -[roles.reviewer] +[ticket.roles.reviewer] profile = "project:reviewer" launch_prompt = "$workspace/ticket/reviewer/launch" workflow = "multi-agent-workflow" @@ -145,10 +148,10 @@ Fixed roles are: - `reviewer` This is not an arbitrary role registry. The fixed roles are the roles required by Ticket orchestration. -Stale `[roles.investigator]` config is rejected as an unsupported fixed role; remove it and, +Stale `[ticket.roles.investigator]` config is rejected as an unsupported fixed role; remove it and, when a spike is useful, let the Orchestrator create an ordinary task-specific read-only helper Worker. -`profile` selects the Worker runtime Profile for that role. The selected Profile owns durable role/system behavior. `ticket.config.toml` does not have a role-level `system_instruction` field. +`profile` selects the Worker runtime Profile for that role. The selected Profile owns durable role/system behavior. Workspace Ticket settings do not have a role-level `system_instruction` field. `launch_prompt` is a per-action first-run prompt reference for future prompt resolution. Current launcher behavior exposes the ref but does not treat it as system instruction. @@ -156,7 +159,7 @@ when a spike is useful, let the Orchestrator create an ordinary task-specific re `provider = "builtin:yoi_local"` selects Yoi's built-in local Ticket backend. `root = ".yoi/tickets"` is the canonical local storage root for this repository. Legacy `kind = "local"` is accepted only as a short transitional alias; new configs should use `provider`. -If `.yoi/ticket.config.toml` is missing, defaults are: +If `.yoi/workspace.toml` has no `[ticket]` table and no legacy fallback file exists, defaults are: - backend provider: `builtin:yoi_local` - backend root: `<workspace>/.yoi/tickets` @@ -168,7 +171,7 @@ If `.yoi/ticket.config.toml` is missing, defaults are: - coder: `multi-agent-workflow` - reviewer: `multi-agent-workflow` -Important: top-level Ticket role launches cannot execute `profile = "inherit"` because top-level launch has no parent Profile to inherit from. Configure concrete role profiles in `.yoi/ticket.config.toml` before using `yoi panel` role-launch actions. +Important: top-level Ticket role launches cannot execute `profile = "inherit"` because top-level launch has no parent Profile to inherit from. Configure concrete role profiles in `.yoi/workspace.toml` under `[ticket.roles.*]` before using `yoi panel` role-launch actions. ## Workflow lifecycle @@ -272,7 +275,7 @@ Close with a resolution that summarizes what changed, key commits, validation, r `yoi panel` is the active Ticket/Intake/Orchestrator Dashboard. It owns fixed Ticket role-launch actions and uses the shared client Ticket role launcher. The single-Worker Console no longer supports `:ticket ...` commands; typing them in command mode is treated like any other unknown command. -Role actions map to the same fixed roles configured in `.yoi/ticket.config.toml`: +Role actions map to the same fixed roles configured in `.yoi/workspace.toml` under `[ticket.roles]`: - intake launches the intake role without an existing Ticket and requires freeform context. - route launches the orchestrator role for an existing Ticket. @@ -288,7 +291,7 @@ The role-launch path is: ```text User triggers a Ticket action in yoi panel -> Dashboard builds a TicketRoleLaunchContext - -> client Ticket role launcher reads .yoi/ticket.config.toml + -> client Ticket role launcher reads .yoi/workspace.toml [ticket] settings -> launcher selects the role Profile and workflow -> launcher spawns the role Worker -> launcher sends Method::Run with WorkflowInvoke + Text segments @@ -301,37 +304,37 @@ The launched Worker receives dynamic Ticket/action context as its first committe The first run input contains: - the selected fixed role; -- the workflow canonical id from `.yoi/ticket.config.toml`; +- the workflow canonical id from workspace `[ticket.roles.<role>]` settings; - Ticket id when the action targets an existing Ticket; - freeform user instruction/context from the action; - configured `launch_prompt` reference if present, as an unresolved reference for future prompt resolution. -The selected Profile supplies durable system/role behavior. `ticket.config.toml` does not override system instruction. +The selected Profile supplies durable system/role behavior. Workspace Ticket settings do not override system instruction. ### Dashboard setup Because top-level role launches cannot inherit a parent Profile, configure concrete role profiles before using Dashboard role actions: ```toml -# .yoi/ticket.config.toml +# .yoi/workspace.toml -[backend] +[ticket.backend] provider = "builtin:yoi_local" root = ".yoi/tickets" -[roles.intake] +[ticket.roles.intake] profile = "project:intake" workflow = "ticket-intake-workflow" -[roles.orchestrator] +[ticket.roles.orchestrator] profile = "project:orchestrator" workflow = "ticket-orchestrator-routing" -[roles.coder] +[ticket.roles.coder] profile = "project:coder" workflow = "multi-agent-workflow" -[roles.reviewer] +[ticket.roles.reviewer] profile = "project:reviewer" workflow = "multi-agent-workflow" ``` @@ -340,8 +343,8 @@ If a role still uses `profile = "inherit"`, the Dashboard fails closed with a di ### Dashboard troubleshooting -- `profile = "inherit"`: configure a concrete role Profile in `.yoi/ticket.config.toml`. -- malformed `.yoi/ticket.config.toml`: fix the config and retry. +- `profile = "inherit"`: configure a concrete role Profile in `.yoi/workspace.toml` under `[ticket.roles.<role>]`. +- malformed workspace Ticket settings: fix the `[ticket]` table in `.yoi/workspace.toml` and retry. - missing Ticket id for route, implement, or review actions: provide the target Ticket. - launch success but no visible completion: attach to or inspect the launched Worker; completion notifications are hints, not authority. From 1473e874a8fcc1e1c090ff5d971f16b353027a77 Mon Sep 17 00:00:00 2001 From: Hare <kei.hiracchi.0928@gmail.com> Date: Thu, 16 Jul 2026 02:20:11 +0900 Subject: [PATCH 4/9] ticket: record workspace ticket settings implementation --- .yoi/tickets/00001KXK9507N/item.md | 2 +- .yoi/tickets/00001KXK9507N/thread.md | 58 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/.yoi/tickets/00001KXK9507N/item.md b/.yoi/tickets/00001KXK9507N/item.md index 60fdcd69..e9c74da6 100644 --- a/.yoi/tickets/00001KXK9507N/item.md +++ b/.yoi/tickets/00001KXK9507N/item.md @@ -2,7 +2,7 @@ title: 'Consolidate Ticket configuration into Workspace settings' state: 'inprogress' created_at: '2026-07-15T16:18:25Z' -updated_at: '2026-07-15T16:30:37Z' +updated_at: '2026-07-15T17:20:06Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-15T16:19:31Z' diff --git a/.yoi/tickets/00001KXK9507N/thread.md b/.yoi/tickets/00001KXK9507N/thread.md index 3e68d8bb..8ada8204 100644 --- a/.yoi/tickets/00001KXK9507N/thread.md +++ b/.yoi/tickets/00001KXK9507N/thread.md @@ -177,3 +177,61 @@ Next action: Explicit follow-up authorized implementation start. Ticket/relation/plan/worktree state was rechecked and no blockers were found. Moving to inprogress before worktree creation or role Pod spawn. --- + +<!-- event: implementation_report author: orchestrator at: 2026-07-15T17:20:06Z --> + +## Implementation report + +Implementation progress report: + +- Coder Pod `yoi-coder-00001KXK9507N-ticket-settings` completed implementation in `/home/hare/Projects/yoi/.worktree/00001KXK9507N-ticket-workspace-settings` on branch `work/00001KXK9507N-ticket-workspace-settings`。 +- Implementation commit: `b1e3a2ad338a551f6e875087c93ecf709a03c1c8 ticket: move ticket settings into workspace config`。 +- Orchestrator inspected worktree status, branch log, commit stats, `git diff --check c68ed1fd..HEAD`, and changed files. Worktree was clean and diff check passed。 + +Implementation summary: +- Durable Ticket policy authority を `.yoi/workspace.toml` の `[ticket]` に移動。 +- tracked `.yoi/ticket.config.toml` を削除。 +- Workspace settings に `[ticket]` がない場合のみ、legacy `.yoi/ticket.config.toml` を narrow read-only migration fallback として読む。 +- Ticket settings がない場合の default behavior は `provider = builtin:yoi_local`, `root = .yoi/tickets` として維持。 +- `[ticket]` が存在する場合は legacy `.yoi/ticket.config.toml` を無視する precedence にした。 +- Direct CLI / Objective config consumers / Worker Ticket feature setup / workspace-server Ticket backend endpoint / project record reader / Panel role launch availability / role launch tests を新 authority 解決へ更新。 +- local-only runtime/backend operational settings は tracked workspace Ticket policy と分離。 +- docs で `.yoi/ticket.config.toml` obsolete と新 `[ticket]` workspace settings shape を記録。 + +Files touched: +- `.yoi/ticket.config.toml` removed。 +- `.yoi/workspace.toml` updated。 +- `crates/client/src/ticket_role.rs` +- `crates/ticket/src/config.rs` +- `crates/tui/src/dashboard/tests.rs` +- `crates/tui/src/workspace_panel.rs` +- `crates/worker/src/controller.rs` +- `crates/worker/src/feature/builtin/ticket.rs` +- `crates/workspace-server/src/identity.rs` +- `crates/workspace-server/src/records.rs` +- `crates/workspace-server/src/server.rs` +- `crates/yoi/src/ticket_cli.rs` +- `docs/design/workflows-public-dogfood-split.md` +- `docs/development/work-items.md` + +Coder-reported validation passed: +- `cargo fmt --check` +- `git diff --check` +- `cargo test -p ticket` +- `cargo test -p yoi --tests` +- `cargo test -p client --lib --tests` +- `cargo test -p worker --lib --tests` +- `cargo test -p yoi-workspace-server --lib` +- `cargo test -p tui --lib --tests` +- `cargo check -p yoi` +- `yoi ticket doctor` +- `nix build .#yoi --no-link` + +Not run: +- `cargo test -p yoi --lib --tests`: `yoi` has no library target。 +- web checks: web/API types were not touched。 + +Next action: +- Route to external Reviewer Pod before merge/close decisions。 + +--- From f5c340a120277c95bc87242ab06fce0d91721f3a Mon Sep 17 00:00:00 2001 From: Hare <kei.hiracchi.0928@gmail.com> Date: Thu, 16 Jul 2026 02:26:32 +0900 Subject: [PATCH 5/9] ticket: record workspace ticket settings review blocker --- .yoi/tickets/00001KXK9507N/item.md | 2 +- .yoi/tickets/00001KXK9507N/thread.md | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.yoi/tickets/00001KXK9507N/item.md b/.yoi/tickets/00001KXK9507N/item.md index e9c74da6..52af0a3b 100644 --- a/.yoi/tickets/00001KXK9507N/item.md +++ b/.yoi/tickets/00001KXK9507N/item.md @@ -2,7 +2,7 @@ title: 'Consolidate Ticket configuration into Workspace settings' state: 'inprogress' created_at: '2026-07-15T16:18:25Z' -updated_at: '2026-07-15T17:20:06Z' +updated_at: '2026-07-15T17:26:26Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-15T16:19:31Z' diff --git a/.yoi/tickets/00001KXK9507N/thread.md b/.yoi/tickets/00001KXK9507N/thread.md index 8ada8204..6deb635a 100644 --- a/.yoi/tickets/00001KXK9507N/thread.md +++ b/.yoi/tickets/00001KXK9507N/thread.md @@ -235,3 +235,30 @@ Next action: - Route to external Reviewer Pod before merge/close decisions。 --- + +<!-- event: review author: reviewer at: 2026-07-15T17:26:26Z status: request_changes --> + +## Review: request changes + +External review result: request_changes + +Acceptance blocker: +- Workspace settings が active Panel Ticket reads の全経路で durable authority になっていない。 +- `crates/tui/src/workspace_panel.rs:673-688` は orchestration worktree overlay を `worktree_root.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)` (`.yoi/tickets`) から直接読んでいる。 +- そのため orchestration worktree 側の `TicketConfig` / workspace `[ticket.backend].root` を解決しておらず、non-default Ticket root では Panel overlay が “no .yoi/tickets directory” と扱う、または orchestration state を見落とす。 +- これは configured backend-root authority と Panel resolution-path acceptance focus に違反する。 + +Positive evidence: +- `TicketConfig` は `.yoi/workspace.toml [ticket]` を legacy fallback / precedence / default tests 付きで読むようになっている。 +- Direct Ticket CLI、Objective link validation、Worker feature setup、workspace-server record/backend endpoint、queue handoff、role launch は概ね `TicketConfig::load_workspace` 経由になっている。 +- `.yoi/workspace.toml` に backend/language/role config が入り、docs は legacy config obsolete を記録している。 + +Validation performed by reviewer: +- read-only diff/source inspection。 +- implementation worktree で `git status --short`, `git rev-parse`, `git diff --check c68ed1fd..HEAD`。 +- test rerun はしていない。 + +Non-blocking note: +- non-default-root Panel orchestration-overlay test を追加すると、この経路の regression を防げる。 + +--- From 40f21145415106dabf0905d301dd04b73fabd424 Mon Sep 17 00:00:00 2001 From: Hare <kei.hiracchi.0928@gmail.com> Date: Thu, 16 Jul 2026 02:42:29 +0900 Subject: [PATCH 6/9] tui: use configured ticket root for overlay --- crates/tui/src/workspace_panel.rs | 75 ++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/crates/tui/src/workspace_panel.rs b/crates/tui/src/workspace_panel.rs index b14fe44a..670cd102 100644 --- a/crates/tui/src/workspace_panel.rs +++ b/crates/tui/src/workspace_panel.rs @@ -6,8 +6,8 @@ use std::time::Instant; use protocol::WorkerStatus; use ticket::config::{ - DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TICKET_CONFIG_RELATIVE_PATH, TicketConfig, - TicketOrchestrationConfig, WORKSPACE_SETTINGS_RELATIVE_PATH, + TICKET_CONFIG_RELATIVE_PATH, TicketConfig, TicketOrchestrationConfig, + WORKSPACE_SETTINGS_RELATIVE_PATH, }; use ticket::{ LocalTicketBackend, TicketBackend, TicketError, TicketEvent, TicketFilter, TicketIdOrSlug, @@ -641,13 +641,14 @@ fn load_orchestration_ticket_overlay( } match validate_orchestration_overlay_source(workspace_root, &layout) { Ok(()) => { - load_orchestration_ticket_overlay_states(&layout.path, config.ticket_record_language()) - .unwrap_or_else(|message| OrchestrationTicketOverlay { + load_orchestration_ticket_overlay_states(&layout.path).unwrap_or_else(|message| { + OrchestrationTicketOverlay { states: BTreeMap::new(), diagnostics: vec![bounded_panel_diagnostic(format!( "Orchestration Ticket overlay unavailable: {message}" ))], - }) + } + }) } Err(message) => OrchestrationTicketOverlay { states: BTreeMap::new(), @@ -672,19 +673,25 @@ fn orchestration_worktree_layout( fn load_orchestration_ticket_overlay_states( worktree_root: &Path, - record_language: Option<&str>, ) -> Result<OrchestrationTicketOverlay, String> { - let ticket_root = worktree_root.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH); + let overlay_config = TicketConfig::load_workspace(worktree_root).map_err(|error| { + format!( + "orchestration worktree {} has unusable Ticket settings: {error}", + worktree_root.display() + ) + })?; + let ticket_root = overlay_config.backend_root(); if !ticket_root.is_dir() { return Ok(OrchestrationTicketOverlay { states: BTreeMap::new(), diagnostics: vec![bounded_panel_diagnostic(format!( - "Orchestration worktree has no {} directory", - DEFAULT_TICKET_BACKEND_RELATIVE_PATH + "Orchestration worktree configured Ticket backend root {} is not a directory", + ticket_root.display() ))], }); } - let backend = LocalTicketBackend::new(ticket_root).with_record_language(record_language); + let backend = LocalTicketBackend::new(ticket_root.to_path_buf()) + .with_record_language(overlay_config.ticket_record_language()); let partial = backend .list_partial(TicketFilter::all()) .map_err(|error| error.to_string())?; @@ -1796,11 +1803,17 @@ mod tests { } fn write_ticket_config(workspace_root: &Path) { + write_ticket_config_with_root(workspace_root, ".yoi/tickets"); + } + + fn write_ticket_config_with_root(workspace_root: &Path, root: &str) { let config_dir = workspace_root.join(".yoi"); fs::create_dir_all(&config_dir).unwrap(); fs::write( config_dir.join("workspace.toml"), - "[ticket]\n\n[ticket.backend]\nprovider = \"builtin:yoi_local\"\nroot = \".yoi/tickets\"\n", + format!( + "[ticket]\n\n[ticket.backend]\nprovider = \"builtin:yoi_local\"\nroot = \"{root}\"\n" + ), ) .unwrap(); } @@ -1848,8 +1861,12 @@ mod tests { } fn copy_ticket_to_overlay(workspace_root: &Path, orchestration_root: &Path, id: &str) { + copy_ticket_to_overlay_root(workspace_root, &orchestration_root.join(".yoi/tickets"), id); + } + + fn copy_ticket_to_overlay_root(workspace_root: &Path, overlay_root: &Path, id: &str) { let local_ticket_dir = workspace_root.join(".yoi/tickets").join(id); - let overlay_ticket_dir = orchestration_root.join(".yoi/tickets").join(id); + let overlay_ticket_dir = overlay_root.join(id); fs::create_dir_all(overlay_ticket_dir.parent().unwrap()).unwrap(); fs::create_dir_all(&overlay_ticket_dir).unwrap(); fs::copy( @@ -2020,6 +2037,40 @@ mod tests { assert!(model.rows.iter().all(|row| row.title != "Overlay Only")); } + #[test] + fn workspace_panel_orchestration_overlay_uses_configured_backend_root() { + let temp = TempDir::new().unwrap(); + init_git_repo(temp.path()); + write_ticket_config(temp.path()); + let orchestration_root = add_orchestration_worktree(temp.path(), "orchestration"); + write_ticket_config_with_root(&orchestration_root, "configured-overlay-tickets"); + let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); + let id = create_ticket_with_id(&backend, "Overlay Custom Root", |input| { + input.workflow_state = Some(TicketWorkflowState::Queued); + }); + let configured_overlay_root = orchestration_root.join("configured-overlay-tickets"); + copy_ticket_to_overlay_root(temp.path(), &configured_overlay_root, &id); + let overlay_backend = LocalTicketBackend::new(&configured_overlay_root); + set_ticket_state(&overlay_backend, &id, TicketWorkflowState::InProgress); + + let model = build_workspace_panel(temp.path(), &empty_pods()); + + assert!(model.header.diagnostics.is_empty()); + let row = ticket_row_by_title(&model, "Overlay Custom Root"); + assert_eq!(row.status, "q→prog"); + assert_eq!( + row.ticket + .as_ref() + .unwrap() + .orchestration_overlay + .as_ref() + .unwrap() + .workflow_state, + TicketWorkflowState::InProgress + ); + assert!(!orchestration_root.join(".yoi/tickets").exists()); + } + #[test] fn workspace_panel_displays_queued_plus_orchestration_inprogress_without_mutating_local_ticket() { From 884ced7ecb1d67640dac8557afb90efe5064bb86 Mon Sep 17 00:00:00 2001 From: Hare <kei.hiracchi.0928@gmail.com> Date: Thu, 16 Jul 2026 02:43:12 +0900 Subject: [PATCH 7/9] ticket: record workspace ticket settings fix --- .yoi/tickets/00001KXK9507N/item.md | 2 +- .yoi/tickets/00001KXK9507N/thread.md | 31 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.yoi/tickets/00001KXK9507N/item.md b/.yoi/tickets/00001KXK9507N/item.md index 52af0a3b..33d627f0 100644 --- a/.yoi/tickets/00001KXK9507N/item.md +++ b/.yoi/tickets/00001KXK9507N/item.md @@ -2,7 +2,7 @@ title: 'Consolidate Ticket configuration into Workspace settings' state: 'inprogress' created_at: '2026-07-15T16:18:25Z' -updated_at: '2026-07-15T17:26:26Z' +updated_at: '2026-07-15T17:43:06Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-15T16:19:31Z' diff --git a/.yoi/tickets/00001KXK9507N/thread.md b/.yoi/tickets/00001KXK9507N/thread.md index 6deb635a..65c58754 100644 --- a/.yoi/tickets/00001KXK9507N/thread.md +++ b/.yoi/tickets/00001KXK9507N/thread.md @@ -262,3 +262,34 @@ Non-blocking note: - non-default-root Panel orchestration-overlay test を追加すると、この経路の regression を防げる。 --- + +<!-- event: implementation_report author: orchestrator at: 2026-07-15T17:43:06Z --> + +## Implementation report + +Follow-up implementation progress report after request_changes: + +- Coder Pod `yoi-coder-00001KXK9507N-ticket-settings` completed the Panel overlay configured-root fix in `/home/hare/Projects/yoi/.worktree/00001KXK9507N-ticket-workspace-settings`。 +- Follow-up commit: `40f21145415106dabf0905d301dd04b73fabd424 tui: use configured ticket root for overlay`。 +- Orchestrator inspected worktree status, branch log, commit stats, and `git diff --check c68ed1fd..HEAD`; worktree was clean and diff check passed。 + +Fix summary: +- Panel orchestration overlay loading now uses `TicketConfig::load_workspace(worktree_root)`。 +- Overlay reads use the orchestration worktree’s configured `[ticket.backend].root` and Ticket record language。 +- Hard-coded `.yoi/tickets` overlay read path was removed except through config defaults。 +- Missing/unusable configured overlay backend root diagnostics are preserved。 +- Regression test added: `workspace_panel_orchestration_overlay_uses_configured_backend_root`。 +- Test verifies non-default orchestration overlay Ticket root is used and `.yoi/tickets` is not required。 + +Coder-reported validation passed: +- `cargo fmt --check` +- `git diff --check` +- `cargo test -p tui --lib --tests` (`383 passed`) +- `cargo check -p yoi` +- `yoi ticket doctor` +- `nix build .#yoi --no-link` + +Next action: +- Request focused re-review of the previous Panel overlay blocker and overall acceptance before merge/close decisions。 + +--- From 05cf6e883f4d51b1f759cd590bf05e4cd2b5fb82 Mon Sep 17 00:00:00 2001 From: Hare <kei.hiracchi.0928@gmail.com> Date: Thu, 16 Jul 2026 02:45:40 +0900 Subject: [PATCH 8/9] ticket: approve workspace ticket settings implementation --- .yoi/tickets/00001KXK9507N/item.md | 2 +- .yoi/tickets/00001KXK9507N/thread.md | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.yoi/tickets/00001KXK9507N/item.md b/.yoi/tickets/00001KXK9507N/item.md index 33d627f0..7f7a6257 100644 --- a/.yoi/tickets/00001KXK9507N/item.md +++ b/.yoi/tickets/00001KXK9507N/item.md @@ -2,7 +2,7 @@ title: 'Consolidate Ticket configuration into Workspace settings' state: 'inprogress' created_at: '2026-07-15T16:18:25Z' -updated_at: '2026-07-15T17:43:06Z' +updated_at: '2026-07-15T17:45:34Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-15T16:19:31Z' diff --git a/.yoi/tickets/00001KXK9507N/thread.md b/.yoi/tickets/00001KXK9507N/thread.md index 65c58754..28fd3466 100644 --- a/.yoi/tickets/00001KXK9507N/thread.md +++ b/.yoi/tickets/00001KXK9507N/thread.md @@ -293,3 +293,27 @@ Next action: - Request focused re-review of the previous Panel overlay blocker and overall acceptance before merge/close decisions。 --- + +<!-- event: review author: reviewer at: 2026-07-15T17:45:34Z status: approve --> + +## Review: approve + +Focused re-review result: approve + +Evidence: +- Previous blocker is fixed: `crates/tui/src/workspace_panel.rs:674-694` now loads `TicketConfig::load_workspace(worktree_root)`, uses `overlay_config.backend_root()` and `overlay_config.ticket_record_language()`, and reports a clear diagnostic when the configured root is not a directory。 +- Regression `workspace_panel_orchestration_overlay_uses_configured_backend_root` covers non-default overlay roots and reviewer reran it successfully (`1 passed`)。 +- No new co-equal legacy authority was found: legacy `.yoi/ticket.config.toml` remains only the documented fallback through `TicketConfig::load_workspace` when workspace `[ticket]` is absent。 +- Overall acceptance remains satisfied by inspected paths: direct CLI/Objectives, Worker feature setup, workspace-server records/backend endpoint, Panel/role launch, and queue/overlay paths resolve through workspace Ticket settings/defaults。 +- `.yoi/workspace.toml` carries backend/language/fixed role policy, docs/help note legacy obsolete, and operational/runtime settings remain outside tracked Ticket policy。 + +Validation performed by reviewer: +- clean `git status`。 +- HEAD `40f21145`。 +- `git diff --check c68ed1fd..HEAD`。 +- focused source review。 +- focused TUI regression test。 + +Blockers: none。 + +--- From 3679362f0fef4c7f4cf8445c715982ff850327c3 Mon Sep 17 00:00:00 2001 From: Hare <kei.hiracchi.0928@gmail.com> Date: Thu, 16 Jul 2026 02:51:38 +0900 Subject: [PATCH 9/9] ticket: close workspace ticket settings work --- .yoi/tickets/00001KXK9507N/item.md | 4 +- .yoi/tickets/00001KXK9507N/resolution.md | 37 +++++++++ .yoi/tickets/00001KXK9507N/thread.md | 98 ++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 .yoi/tickets/00001KXK9507N/resolution.md diff --git a/.yoi/tickets/00001KXK9507N/item.md b/.yoi/tickets/00001KXK9507N/item.md index 7f7a6257..5a944148 100644 --- a/.yoi/tickets/00001KXK9507N/item.md +++ b/.yoi/tickets/00001KXK9507N/item.md @@ -1,8 +1,8 @@ --- title: 'Consolidate Ticket configuration into Workspace settings' -state: 'inprogress' +state: 'closed' created_at: '2026-07-15T16:18:25Z' -updated_at: '2026-07-15T17:45:34Z' +updated_at: '2026-07-15T17:51:30Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-15T16:19:31Z' diff --git a/.yoi/tickets/00001KXK9507N/resolution.md b/.yoi/tickets/00001KXK9507N/resolution.md new file mode 100644 index 00000000..0136e0dc --- /dev/null +++ b/.yoi/tickets/00001KXK9507N/resolution.md @@ -0,0 +1,37 @@ +Ticket configuration の Workspace settings 統合を実装・レビュー・merge・検証した。 + +実装内容: +- durable Ticket policy authority を `.yoi/workspace.toml [ticket]` に移動。 +- tracked `.yoi/ticket.config.toml` を削除。 +- Workspace `[ticket]` がない場合のみ legacy `.yoi/ticket.config.toml` を narrow read-only migration fallback として読むようにした。 +- Workspace `[ticket]` が存在する場合は legacy config を無視し、二重 active authority を避ける precedence にした。 +- default behavior は維持し、設定なしでは `provider = builtin:yoi_local`, `root = .yoi/tickets` を使う。 +- Ticket backend provider/root、record language、orchestration defaults、fixed role launch config を Workspace settings authority に移した。 +- Direct Ticket CLI / Objective config consumers / Worker Ticket feature setup / workspace-server Ticket backend endpoint / project record reader / Panel role launch availability / role launch tests を新 authority 解決へ更新。 +- Panel orchestration overlay も orchestration worktree の `TicketConfig::load_workspace(worktree_root)` を使い、configured backend root と record language を尊重するよう修正。 +- docs で `.yoi/ticket.config.toml` obsolete と新 `[ticket]` workspace settings shape を記録。 + +Review: +- 初回 review は Panel orchestration overlay が `.yoi/tickets` を hard-code している blocker で `request_changes`。 +- `40f21145 tui: use configured ticket root for overlay` で overlay read path を configured Ticket root に移行し、non-default root regression test を追加。 +- focused re-review は `approve`。 + +Merge / validation: +- Merge commit: `45783b74 merge: workspace ticket settings`。 +- Final validation passed: + - `cargo fmt --check` + - `git diff --check` + - `cargo test -p ticket` + - `cargo test -p yoi --tests` + - `cargo test -p client --lib --tests` + - `cargo test -p worker --lib --tests` + - `cargo test -p yoi-workspace-server --lib` + - `cargo test -p tui --lib --tests` + - `cargo check -p yoi` + - `yoi ticket doctor` + - `nix build .#yoi --no-link` +- Validation log: `/run/user/1000/yoi/yoi-orchestrator/bash-output/workspace-ticket-settings-final-validation-1784137566.txt` + +Cleanup: +- Implementation worktree/branch cleanup will be performed after close commit。 +- Per user instruction, `StopPod` is not used。 \ No newline at end of file diff --git a/.yoi/tickets/00001KXK9507N/thread.md b/.yoi/tickets/00001KXK9507N/thread.md index 28fd3466..7f40196e 100644 --- a/.yoi/tickets/00001KXK9507N/thread.md +++ b/.yoi/tickets/00001KXK9507N/thread.md @@ -317,3 +317,101 @@ Validation performed by reviewer: Blockers: none。 --- + +<!-- event: implementation_report author: orchestrator at: 2026-07-15T17:51:09Z --> + +## Implementation report + +Merge / final validation report: + +- Reviewer approved the implementation after the Panel overlay configured-root fix。 +- Merged branch `work/00001KXK9507N-ticket-workspace-settings` into orchestration branch with merge commit `45783b74 merge: workspace ticket settings`。 +- Final validation was rerun in `/home/hare/Projects/yoi/.worktree/orchestration` after merge。 + +Validation passed: +- `cargo fmt --check` +- `git diff --check` +- `cargo test -p ticket` +- `cargo test -p yoi --tests` +- `cargo test -p client --lib --tests` +- `cargo test -p worker --lib --tests` +- `cargo test -p yoi-workspace-server --lib` +- `cargo test -p tui --lib --tests` +- `cargo check -p yoi` +- `yoi ticket doctor` +- `nix build .#yoi --no-link` + +Validation log: +- `/run/user/1000/yoi/yoi-orchestrator/bash-output/workspace-ticket-settings-final-validation-1784137566.txt` + +Reviewed/merged implementation commits: +- `b1e3a2ad ticket: move ticket settings into workspace config` +- `40f21145 tui: use configured ticket root for overlay` + +Notes: +- `.yoi/ticket.config.toml` was removed from tracked active project config。 +- `.yoi/workspace.toml [ticket]` is now the durable Ticket settings authority; legacy config remains only as narrow fallback when workspace `[ticket]` is absent。 +- Per user instruction, no `StopPod` cleanup was performed。 + +--- + +<!-- event: state_changed author: orchestrator at: 2026-07-15T17:51:15Z from: inprogress to: done reason: implementation_validated field: state --> + +## State changed + +Implementation was reviewed, merged into orchestration branch, and final validation passed. Moving to done before closure. + +--- + +<!-- event: state_changed author: hare at: 2026-07-15T17:51:30Z from: done to: closed reason: closed field: state --> + +## State changed + +Ticket を closed にしました。 + + +--- + +<!-- event: close author: hare at: 2026-07-15T17:51:30Z status: closed --> + +## 完了 + +Ticket configuration の Workspace settings 統合を実装・レビュー・merge・検証した。 + +実装内容: +- durable Ticket policy authority を `.yoi/workspace.toml [ticket]` に移動。 +- tracked `.yoi/ticket.config.toml` を削除。 +- Workspace `[ticket]` がない場合のみ legacy `.yoi/ticket.config.toml` を narrow read-only migration fallback として読むようにした。 +- Workspace `[ticket]` が存在する場合は legacy config を無視し、二重 active authority を避ける precedence にした。 +- default behavior は維持し、設定なしでは `provider = builtin:yoi_local`, `root = .yoi/tickets` を使う。 +- Ticket backend provider/root、record language、orchestration defaults、fixed role launch config を Workspace settings authority に移した。 +- Direct Ticket CLI / Objective config consumers / Worker Ticket feature setup / workspace-server Ticket backend endpoint / project record reader / Panel role launch availability / role launch tests を新 authority 解決へ更新。 +- Panel orchestration overlay も orchestration worktree の `TicketConfig::load_workspace(worktree_root)` を使い、configured backend root と record language を尊重するよう修正。 +- docs で `.yoi/ticket.config.toml` obsolete と新 `[ticket]` workspace settings shape を記録。 + +Review: +- 初回 review は Panel orchestration overlay が `.yoi/tickets` を hard-code している blocker で `request_changes`。 +- `40f21145 tui: use configured ticket root for overlay` で overlay read path を configured Ticket root に移行し、non-default root regression test を追加。 +- focused re-review は `approve`。 + +Merge / validation: +- Merge commit: `45783b74 merge: workspace ticket settings`。 +- Final validation passed: + - `cargo fmt --check` + - `git diff --check` + - `cargo test -p ticket` + - `cargo test -p yoi --tests` + - `cargo test -p client --lib --tests` + - `cargo test -p worker --lib --tests` + - `cargo test -p yoi-workspace-server --lib` + - `cargo test -p tui --lib --tests` + - `cargo check -p yoi` + - `yoi ticket doctor` + - `nix build .#yoi --no-link` +- Validation log: `/run/user/1000/yoi/yoi-orchestrator/bash-output/workspace-ticket-settings-final-validation-1784137566.txt` + +Cleanup: +- Implementation worktree/branch cleanup will be performed after close commit。 +- Per user instruction, `StopPod` is not used。 + +---