chore: update orchestration and plugin tickets

This commit is contained in:
2026-06-14 23:49:45 +09:00
parent adebedc021
commit d66dfbbbc2
35 changed files with 410 additions and 70 deletions
+98 -7
View File
@@ -8,7 +8,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::path::{Component, Path, PathBuf};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -16,6 +16,9 @@ use thiserror::Error;
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";
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`.
///
@@ -36,7 +39,7 @@ pub fn ticket_config_scaffold() -> String {
"\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 branch. When unset, Panel uses orchestration/<workspace-orchestrator-pod-name>.\n# [orchestration]\n# branch = \"orchestration/<workspace-orchestrator-pod-name>\"\n",
"\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",
);
for role in TicketRole::ALL {
out.push_str(&format!(
@@ -77,12 +80,30 @@ pub struct TicketConfig {
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TicketOrchestrationConfig {
pub branch: Option<GitBranchName>,
pub worktree_dir: Option<PathBuf>,
pub worktree_name: Option<PathBuf>,
}
impl TicketOrchestrationConfig {
pub fn branch_name(&self) -> Option<&str> {
self.branch.as_ref().map(GitBranchName::as_str)
}
pub fn effective_branch_name(&self) -> &str {
self.branch_name().unwrap_or(DEFAULT_ORCHESTRATION_BRANCH)
}
pub fn worktree_dir(&self) -> &Path {
self.worktree_dir
.as_deref()
.unwrap_or_else(|| Path::new(DEFAULT_ORCHESTRATION_WORKTREE_DIR))
}
pub fn worktree_name(&self) -> &Path {
self.worktree_name
.as_deref()
.unwrap_or_else(|| Path::new(DEFAULT_ORCHESTRATION_WORKTREE_NAME))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
@@ -167,6 +188,27 @@ fn validate_git_branch_name_value(value: &str) -> Result<(), String> {
Ok(())
}
fn validate_orchestration_relative_path(path: &Path, label: &str) -> Result<(), String> {
if path.as_os_str().is_empty() {
return Err(format!("{label} must not be empty"));
}
if path.is_absolute() {
return Err(format!("{label} must be workspace-relative"));
}
for component in path.components() {
match component {
Component::Normal(_) => {}
Component::CurDir | Component::ParentDir => {
return Err(format!("{label} must not contain `.` or `..` components"));
}
Component::RootDir | Component::Prefix(_) => {
return Err(format!("{label} must be workspace-relative"));
}
}
}
Ok(())
}
impl TicketConfig {
pub fn default_for_workspace(workspace_root: impl AsRef<Path>) -> Self {
let workspace_root = workspace_root.as_ref();
@@ -636,13 +678,25 @@ struct RawTicketConfig {
struct RawTicketOrchestrationConfig {
#[serde(default)]
branch: Option<GitBranchName>,
#[serde(default)]
worktree_dir: Option<PathBuf>,
#[serde(default)]
worktree_name: Option<PathBuf>,
}
impl RawTicketOrchestrationConfig {
fn resolve(self) -> TicketOrchestrationConfig {
TicketOrchestrationConfig {
branch: self.branch,
fn resolve(self) -> Result<TicketOrchestrationConfig, String> {
if let Some(path) = &self.worktree_dir {
validate_orchestration_relative_path(path, "orchestration.worktree_dir")?;
}
if let Some(path) = &self.worktree_name {
validate_orchestration_relative_path(path, "orchestration.worktree_name")?;
}
Ok(TicketOrchestrationConfig {
branch: self.branch,
worktree_dir: self.worktree_dir,
worktree_name: self.worktree_name,
})
}
}
@@ -691,7 +745,12 @@ impl RawTicketConfig {
}
})?,
ticket: self.ticket.resolve(),
orchestration: self.orchestration.resolve(),
orchestration: self.orchestration.resolve().map_err(|message| {
TicketConfigError::Invalid {
path: path.to_path_buf(),
message,
}
})?,
roles,
})
}
@@ -797,6 +856,15 @@ mod tests {
);
assert_eq!(config.ticket_record_language(), None);
assert_eq!(config.orchestration.branch_name(), None);
assert_eq!(
config.orchestration.effective_branch_name(),
"orchestration"
);
assert_eq!(config.orchestration.worktree_dir(), Path::new(".worktree"));
assert_eq!(
config.orchestration.worktree_name(),
Path::new("orchestration")
);
for role in TicketRole::ALL {
let role_config = config.role(role);
assert_eq!(role_config.profile.as_str(), "inherit");
@@ -820,6 +888,8 @@ language = "Japanese"
[orchestration]
branch = "orchestration/custom-panel"
worktree_dir = "custom-worktrees"
worktree_name = "custom-orchestrator"
[roles.intake]
profile = "project:intake"
@@ -854,6 +924,18 @@ workflow = "multi-agent-workflow"
config.orchestration.branch_name(),
Some("orchestration/custom-panel")
);
assert_eq!(
config.orchestration.effective_branch_name(),
"orchestration/custom-panel"
);
assert_eq!(
config.orchestration.worktree_dir(),
Path::new("custom-worktrees")
);
assert_eq!(
config.orchestration.worktree_name(),
Path::new("custom-orchestrator")
);
assert_eq!(
config.profile_for(TicketRole::Intake).as_str(),
"project:intake"
@@ -881,7 +963,7 @@ workflow = "multi-agent-workflow"
assert!(scaffold.contains("root = \".yoi/tickets\""));
assert!(scaffold.contains("# [ticket]\n# language = \"Japanese\""));
assert!(scaffold.contains(
"# [orchestration]\n# branch = \"orchestration/<workspace-orchestrator-pod-name>\""
"# [orchestration]\n# branch = \"orchestration\"\n# worktree_dir = \".worktree\"\n# worktree_name = \"orchestration\""
));
for role in TicketRole::ALL {
assert!(scaffold.contains(&format!("[roles.{role}]")));
@@ -901,6 +983,15 @@ workflow = "multi-agent-workflow"
.unwrap();
assert_eq!(config.backend_root(), temp.path().join(".yoi/tickets"));
assert_eq!(config.orchestration.branch_name(), None);
assert_eq!(
config.orchestration.effective_branch_name(),
"orchestration"
);
assert_eq!(config.orchestration.worktree_dir(), Path::new(".worktree"));
assert_eq!(
config.orchestration.worktree_name(),
Path::new("orchestration")
);
for role in TicketRole::ALL {
let role_config = config.role_launch_config(role).unwrap();
assert_eq!(role_config.profile.as_str(), role.default_profile());
+23 -33
View File
@@ -27,7 +27,7 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap};
use serde::Serialize;
use session_store::FsStore;
use ticket::config::{GitBranchName, TicketConfig};
use ticket::config::{GitBranchName, TicketConfig, TicketOrchestrationConfig};
use ticket::{
LocalTicketBackend, MarkdownText, TicketBackend, TicketIdOrSlug, TicketStateChange,
TicketWorkflowState,
@@ -2517,40 +2517,37 @@ struct OrchestrationWorktreeReady {
status: OrchestrationWorktreeStatus,
}
fn orchestration_worktree_layout_for_branch(
fn orchestration_worktree_layout_for_config(
workspace_root: &Path,
branch: String,
orchestration: &TicketOrchestrationConfig,
) -> OrchestrationWorktreeLayout {
let stem = workspace_orchestrator_pod_name(workspace_root);
OrchestrationWorktreeLayout {
path: workspace_root
.join(".worktree")
.join("orchestration")
.join(&stem),
branch,
.join(orchestration.worktree_dir())
.join(orchestration.worktree_name()),
branch: orchestration.effective_branch_name().to_string(),
}
}
#[cfg(test)]
fn orchestration_worktree_layout(workspace_root: &Path) -> OrchestrationWorktreeLayout {
let stem = workspace_orchestrator_pod_name(workspace_root);
orchestration_worktree_layout_for_branch(workspace_root, format!("orchestration/{stem}"))
OrchestrationWorktreeLayout {
path: workspace_root.join(".worktree").join("orchestration"),
branch: "orchestration".to_string(),
}
}
fn resolved_orchestration_worktree_layout(
workspace_root: &Path,
) -> Result<OrchestrationWorktreeLayout, String> {
let config = TicketConfig::load_workspace(workspace_root)
.map_err(|err| format!("failed to load ticket config for orchestration branch: {err}"))?;
let branch = if let Some(branch) = config.orchestration.branch_name() {
branch.to_string()
} else {
orchestration_worktree_layout(workspace_root).branch
};
GitBranchName::new(branch.clone())
.map_err(|err| format!("failed to load ticket config for orchestration worktree: {err}"))?;
let branch = config.orchestration.effective_branch_name();
GitBranchName::new(branch.to_string())
.map_err(|message| format!("invalid orchestration branch `{branch}`: {message}"))?;
Ok(orchestration_worktree_layout_for_branch(
Ok(orchestration_worktree_layout_for_config(
workspace_root,
branch,
&config.orchestration,
))
}
@@ -5757,9 +5754,9 @@ mod tests {
let layout = orchestration_worktree_layout(root);
assert_eq!(
layout.path,
PathBuf::from("/tmp/Yoi Workspace/.worktree/orchestration/yoi-workspace-orchestrator")
PathBuf::from("/tmp/Yoi Workspace/.worktree/orchestration")
);
assert_eq!(layout.branch, "orchestration/yoi-workspace-orchestrator");
assert_eq!(layout.branch, "orchestration");
}
#[test]
@@ -5834,7 +5831,7 @@ mod tests {
}
#[test]
fn ensure_and_restore_use_configured_orchestration_branch() {
fn ensure_and_restore_use_configured_orchestration_layout() {
let temp = TempDir::new().unwrap();
let root = temp.path().join("repo");
init_test_repo(&root);
@@ -5843,6 +5840,8 @@ mod tests {
r#"
[orchestration]
branch = "orchestration/custom-panel"
worktree_dir = "custom-worktrees"
worktree_name = "panel"
"#,
);
run_test_git(&root, &["add", ".yoi/ticket.config.toml"]).unwrap();
@@ -5850,11 +5849,7 @@ branch = "orchestration/custom-panel"
let resolved = resolved_orchestration_worktree_layout(&root).unwrap();
assert_eq!(resolved.branch, "orchestration/custom-panel");
assert!(
resolved
.path
.ends_with(".worktree/orchestration/repo-orchestrator")
);
assert!(resolved.path.ends_with("custom-worktrees/panel"));
let created = ensure_orchestration_worktree(&root).unwrap();
assert_eq!(created.status, OrchestrationWorktreeStatus::Created);
@@ -5934,12 +5929,7 @@ branch = "orchestration/custom-panel"
assert_eq!(restored.status, OrchestrationWorktreeStatus::Reused);
assert_eq!(restored.layout.path, created.layout.path);
assert_ne!(restored.layout.path, root);
assert!(
restored
.layout
.path
.ends_with(".worktree/orchestration/repo-orchestrator")
);
assert!(restored.layout.path.ends_with(".worktree/orchestration"));
}
#[test]